diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-19 20:53:43 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-19 20:53:43 +0200 |
| commit | b8400f38411818aa54a773dffb2c3a0c56b06dba (patch) | |
| tree | e6390c860efb005e424b91594c6c619c120d9b75 /sb_builder/hints.py | |
| parent | d29fc1cd128b7d6a18e3e50d8ff4d6262f7e88ae (diff) | |
| download | sb-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.py | 195 |
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 |
