# sb-builder - slackrepo hint interpretation # Copyright (C) 2026 Danilo M. # # 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