aboutsummaryrefslogtreecommitdiffstats
path: root/sb_builder/hints.py
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-19 20:53:43 +0200
committerDanilo M. <danix@danix.xyz>2026-10-19 20:53:43 +0200
commitb8400f38411818aa54a773dffb2c3a0c56b06dba (patch)
treee6390c860efb005e424b91594c6c619c120d9b75 /sb_builder/hints.py
parentd29fc1cd128b7d6a18e3e50d8ff4d6262f7e88ae (diff)
downloadsb-builder-b8400f38411818aa54a773dffb2c3a0c56b06dba.tar.gz
sb-builder-b8400f38411818aa54a773dffb2c3a0c56b06dba.zip
Add Packages tab, hint view, Run it and signing key unlock
Packages tab over the helper inventory: query bar with cat:/maint:/ req:/status: prefixes, overlay and status chips, check column, and a detail pane with versions, dependencies after hints and a structured read-only view of the hint (hints.py mirrors slackrepo's parser). Actions start slackrepo previews; the Jobs tab offers Run it with a publish box (default from [builds] publish), asking for the signing key passphrase first when the key is locked. Status bar shows the key state; Dashboard tiles open their tabs; inventory reloads after slackrepo and publish jobs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'sb_builder/hints.py')
-rw-r--r--sb_builder/hints.py195
1 files changed, 195 insertions, 0 deletions
diff --git a/sb_builder/hints.py b/sb_builder/hints.py
new file mode 100644
index 0000000..34ef97d
--- /dev/null
+++ b/sb_builder/hints.py
@@ -0,0 +1,195 @@
+# sb-builder - slackrepo hint interpretation
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""What a slackrepo hint file does, the way slackrepo reads it.
+
+Rules from slackrepo's libexec/functions.d/parsefunctions.sh (hint and fixup
+departments) and slackrepo.hint(5). Pure functions, no Qt.
+"""
+
+import os
+import re
+
+KNOWN = {"SKIP", "VERSION", "OPTIONS", "GROUPADD", "USERADD", "NOWARNING", "NOWARNINGS",
+ "ADDREQUIRES", "DELREQUIRES", "BUILDTIME", "CONFLICTS", "INSTALL", "NUMJOBS",
+ "ANSWER", "CLEANUP", "PRAGMA", "SPECIAL", "ARCH", "DOWNLOAD", "MD5SUM", "SHA256SUM",
+ "NODOWNLOAD"} # NODOWNLOAD: written by mkhint, shown as DOWNLOAD="no" by user decision
+PER_ARCH = re.compile(r"^(DOWNLOAD|MD5SUM|SHA256SUM)_\w+$")
+SHELLS = {"sh", "bash", "zsh", "ksh", "csh", "tcsh", "false", "nologin", "true"}
+
+PRAGMAS = {
+ "python3": "force python3 support: adds python3 to the deps, sets PYTHON3=yes",
+ "kernelmodule": "rebuild when the host kernel changes; depmod on uninstall",
+ "kernel": "rebuild when the host kernel changes",
+ "kernelversion": "uname reports $KERNEL as the kernel version",
+ "multilib_ldflags": "patch the SlackBuild to set LDFLAGS on a multilib host",
+ "wget": "download with a fake wget user agent",
+ "curl": "download with a fake curl user agent",
+ "download_basename": "symlink downloads to the URL's basename",
+ "download_urldecode": "URL-decode DOWNLOAD values before use",
+ "need_net": "network not blocked during the build",
+ "need_X": "X display not blocked during the build",
+ "noexport_ARCH": "set ARCH by patching the SlackBuild, not via the environment",
+ "noexport_BUILD": "set BUILD by patching the SlackBuild, not via the environment",
+ "noexport_TAG": "set TAG by patching the SlackBuild, not via the environment",
+ "noremove": "do not uninstall packages before building",
+ "nofakeroot": "build as root with sudo instead of fakeroot",
+ "no_make_test": "comment out 'make test' in the SlackBuild",
+ "stubs-32": "provide /usr/include/gnu/stubs-32.h during the build",
+ "abstar": "patch the SlackBuild to use tar --absolute-names",
+}
+
+
+def _raw(v):
+ return v["value"] if isinstance(v, dict) else v
+
+
+def pragma_meaning(p):
+ if p in PRAGMAS:
+ return PRAGMAS[p]
+ if p.startswith("x86arch="):
+ return f"on 32 bit x86, build with ARCH={p[8:]}"
+ if p.startswith("unset_"):
+ return f"unset {p[6:]} for the build"
+ return "unknown pragma"
+
+
+def parse_groupadd(value, pkg):
+ """[(gid, name)] like slackrepo: numeric field is the gid, other the name."""
+ out = []
+ for spec in value.split():
+ gid, name = None, pkg
+ for field in spec.split(":"):
+ if field[:1].isdigit():
+ gid = field
+ elif field:
+ name = field
+ out.append((gid, name))
+ return out
+
+
+def parse_useradd(value, pkg):
+ """[dict(uid, name, group, home, shell, extra)] with slackrepo's defaults."""
+ out = []
+ for spec in value.split():
+ u = {"uid": None, "name": pkg, "group": "", "home": "/dev/null",
+ "shell": "/bin/false", "extra": []}
+ for field in spec.split(":"):
+ if field.startswith("-g"):
+ u["group"] = field[2:]
+ elif field.startswith("-d"):
+ u["home"] = field[2:]
+ elif field.startswith("-s"):
+ u["shell"] = field[2:]
+ elif field.startswith("-"):
+ u["extra"].append(f"{field[:2]} {field[2:]}".strip())
+ elif field.startswith("/"):
+ # ponytail: slackrepo tests -x on the build host; guess by name instead
+ key = "shell" if os.path.basename(field) in SHELLS else "home"
+ u[key] = field
+ elif field[:1].isdigit():
+ u["uid"] = field
+ elif field:
+ u["name"] = field
+ u["group"] = u["group"] or u["name"]
+ out.append(u)
+ return out
+
+
+def dependencies(hints, requires):
+ """[(dep, mark)] as slackrepo computes them; mark in '', del, pragma, add, build."""
+ deleted = set(hints.get("DELREQUIRES", "").split()) | {"%README%"}
+ out = [(r, "del" if r in deleted else "") for r in requires]
+ if "python3" in hints.get("PRAGMA", hints.get("SPECIAL", "")).split():
+ out.append(("python3", "pragma"))
+ out += [(r, "add") for r in hints.get("ADDREQUIRES", "").split()]
+ out += [(r, "build") for r in hints.get("BUILDTIME", "").split()]
+ return out
+
+
+def interpret(hints, pkg, info_version=None, requires=()):
+ """Structured view of one package's hints.
+
+ hints: {key: value or {"value": raw, "shell": True}} from the helper, or None.
+ Returns {"skip", "sections": [(title, [(label, value)])], "deps",
+ "conflicts", "ignored", "shell"}.
+ """
+ raw = hints or {}
+ shell = sorted(k for k, v in raw.items() if isinstance(v, dict))
+ h = {k: _raw(v) for k, v in raw.items()}
+ view = {"skip": None, "sections": [], "deps": dependencies(h, list(requires)),
+ "conflicts": h.get("CONFLICTS", "").split(), "shell": shell,
+ "ignored": sorted(k for k in h if k not in KNOWN and not PER_ARCH.match(k))}
+
+ skip = h.get("SKIP")
+ if skip is not None and skip.lower() != "no":
+ view["skip"] = "always skipped" if skip.lower() == "yes" else skip
+
+ # version, downloads and checksums, resolved together as slackrepo does
+ arch = h.get("ARCH")
+ resolved = {}
+ for var in ("DOWNLOAD", "MD5SUM", "SHA256SUM"):
+ resolved[var] = h.get(f"{var}_{arch}") if arch and h.get(f"{var}_{arch}") else h.get(var)
+ if h.get("VERSION"):
+ resolved["MD5SUM"] = resolved["MD5SUM"] or "no"
+ resolved["SHA256SUM"] = resolved["SHA256SUM"] or "no"
+ rows = []
+ if h.get("VERSION"):
+ rows.append(("version", f"{info_version or '?'} → {h['VERSION']}"))
+ if arch is not None:
+ rows.append(("arch", arch or "(empty)"))
+ if resolved["DOWNLOAD"] == "no" or h.get("NODOWNLOAD", "").lower() == "yes":
+ rows.append(("download", "no download, the source must already be in the cache"))
+ elif resolved["DOWNLOAD"]:
+ rows += [("download", url) for url in resolved["DOWNLOAD"].split()]
+ if resolved["MD5SUM"] == "no":
+ rows.append(("md5sum", "not checked"))
+ elif resolved["MD5SUM"]:
+ rows.append(("md5sum", resolved["MD5SUM"]))
+ if any(k.startswith("SHA256SUM") for k in h):
+ rows.append(("sha256sum", "not checked" if resolved["SHA256SUM"] == "no"
+ else resolved["SHA256SUM"]))
+ if rows:
+ view["sections"].append(("Version override", rows))
+
+ rows = [("option", o) for o in h.get("OPTIONS", "").split()]
+ if "NUMJOBS" in h:
+ rows.append(("make jobs", h["NUMJOBS"]))
+ if h.get("INSTALL"):
+ rows.append(("install after build", "no" if h["INSTALL"][:1] in "Nn0" else "yes"))
+ if "ANSWER" in h:
+ rows.append(("answers to the build", h["ANSWER"].replace("\\n", " ⏎ ")))
+ nowarning = h.get("NOWARNING") or h.get("NOWARNINGS")
+ if nowarning:
+ rows.append(("hide warnings matching", nowarning))
+ if rows:
+ view["sections"].append(("Build settings", rows))
+
+ pragmas = h.get("PRAGMA") or h.get("SPECIAL") or ""
+ if pragmas:
+ view["sections"].append(("Pragmas", [(p, pragma_meaning(p)) for p in pragmas.split()]))
+
+ rows = [("group", f"{name} (gid {gid or '?'})") for gid, name in parse_groupadd(h.get("GROUPADD", ""), pkg)]
+ for u in parse_useradd(h.get("USERADD", ""), pkg):
+ extra = f", {' '.join(u['extra'])}" if u["extra"] else ""
+ rows.append(("user", f"{u['name']} (uid {u['uid'] or '?'}), group {u['group']}, "
+ f"home {u['home']}, shell {u['shell']}{extra}"))
+ if rows:
+ view["sections"].append(("Host changes", rows))
+
+ if h.get("CLEANUP"):
+ view["sections"].append(("On uninstall", [("run", h["CLEANUP"])]))
+ return view