1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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
|