AtlatestRepositorycore-channel

core-channel / tree / src / core / packagesbuild-tools.sgl

1;;; Tools that run during a build and are never linked into its output.
2;;;
3;;; This is the channel's first `build-inputs:` module, and the distinction it
4;;; exists to hold is not cosmetic. A `dependencies:` entry contributes
5;;; `-I<dep>/include`, `-L<dep>/lib` and `<dep>/lib/pkgconfig`, and its store
6;;; path can legitimately end up inside the built artifact. A `build-inputs:`
7;;; entry contributes its `bin/` to PATH -- ahead of the seed, per
8;; builder-base-assignments in (sigil build external), though nothing in this
9;; change measures the ordering because neither tool exists in the seed at all
10;; -- and nothing
11;;; else -- deliberately not `${name}` interpolation, because a baked tool path
12;;; would be an undeclared RUNTIME reference to something that is only a
13;;; build-time input.
14(define-library (core packages build-tools)
15 (import (sigil core)
16 (core build-environments)
17 (core packages python)
18 (sigil env package)
19 (sigil build external))
20 (export byacc file gperf meson samurai)
21 (begin
22 ;; WHY THE CHANNEL NEEDS A YACC AT ALL, since nothing here has a grammar of
23 ;; its own: tmux's configure REFUSES to proceed without one.
24 ;;
25 ;; configure: error: "yacc not found"
26 ;;
27 ;; It fires even though the tmux release tarball ships `cmd-parse.c`
28 ;; pre-generated from `cmd-parse.y`, because the check is
29 ;; `AC_CHECK_PROG(found_yacc, $YACC, yes, no)` followed by an
30 ;; unconditional AC_MSG_ERROR -- configure asks whether the program exists,
31 ;; not whether the build will need it. Measured, not inferred: the real
32 ;; configure was run against a PATH stripped of yacc/bison/byacc, and the
33 ;; seed's `bin/` was enumerated at 0 of those 3.
34 ;;
35 ;; Patching that check out would have been shorter and would have hidden a
36 ;; true statement about the channel: it had no build tools.
37 ;;
38 ;; byacc rather than bison because byacc is a leaf -- plain autotools, no
39 ;; dependencies, no m4 and no gettext. Its makefile installs the program
40 ;; as `bin/yacc` (makefile.in `THIS = yacc`), which is the name configure
41 ;; looks for.
42 ;;
43 ;; Dickey's byacc is a dated rolling release; the archive at
44 ;; invisible-mirror.net keeps every dated tarball, so the URL is stable
45 ;; even though "the latest byacc" is not.
46 (define byacc
47 (package
48 name: "byacc"
49 version: "20260126"
50 source: (external-source
51 location: "https://invisible-mirror.net/archives/byacc/byacc-20260126.tgz"
52 sha256: "b618c5fb44c2f5f048843db90f7d1b24f78f47b07913c8c7ba8c942d3eb24b00")
53 buildenv: core-buildenv
54 phases: (list
55 (external-phase
56 kind: 'configure
57 args: (list "--prefix=${out}"
58 ;; BACKTRACKING IS ENABLED, AND THIS REVERSES
59 ;; AN EARLIER DECISION IN THIS PACKAGE. It was
60 ;; `--disable-btyacc` when tmux was the only
61 ;; consumer, on the grounds that tmux uses plain
62 ;; yacc input and does not want the extension.
63 ;;
64 ;; libxkbcommon does want it. Its grammar uses
65 ;; `%destructor`, which is a btyacc extension,
66 ;; and its own meson.build says so outright:
67 ;; "we use some yacc extensions, which work with
68 ;; either GNU bison (preferred) or byacc (with
69 ;; backtracking enabled)". Without it:
70 ;;
71 ;; byacc: e - line 230 of "parser.y", syntax
72 ;; error %destructor { FreeStmt(...); }
73 ;;
74 ;; MEASURED BOTH WAYS on this toolchain before
75 ;; changing it, with a control for the case the
76 ;; earlier decision was protecting:
77 ;;
78 ;; no btyacc + libxkbcommon grammar -> syntax error
79 ;; btyacc + libxkbcommon grammar -> parser.c, 3393 lines
80 ;; btyacc + a PLAIN grammar -> parser.c, 1177 lines
81 ;; btyacc, installed name -> still bin/yacc
82 ;;
83 ;; So the extension is a superset and tmux loses
84 ;; nothing. In fact tmux never runs this program
85 ;; at all -- its release tarball ships
86 ;; `cmd-parse.c` pre-generated and its configure
87 ;; only does `AC_CHECK_PROG` -- so the flag was
88 ;; never observable there.
89 "--enable-btyacc"))
90 (external-phase kind: 'make)
91 (external-phase kind: 'install)
92 ;; INSTALL THE PROGRAM UNDER BOTH NAMES IT IS KNOWN BY, AND
93 ;; THIS IS DELIBERATE -- do not "tidy" it away.
94 ;;
95 ;; byacc's own makefile installs exactly one binary, named
96 ;; `yacc` (makefile.in:5, `THIS = yacc`), because that is the
97 ;; POSIX name tmux's configure looks for. But `byacc` is the
98 ;; program's ACTUAL name, and it is the name a second consumer
99 ;; asks for: libxkbcommon 1.7.0's meson.build:166 is
100 ;; `find_program('byacc', required: false)`, reached only after
101 ;; bison is not found. A `yacc` on PATH does not satisfy it.
102 ;;
103 ;; Debian and Fedora both ship both names. A package that
104 ;; installs only the POSIX alias is the odd one out, and the
105 ;; alternative -- patching libxkbcommon to look for `yacc` --
106 ;; would write this channel's deficiency into a third party's
107 ;; source, which is the trade `file` in this same module
108 ;; already refused to make.
109 ;;
110 ;; Choosing byacc over bison is what keeps GNU m4 out of this
111 ;; channel: bison needs m4 to build, byacc needs nothing.
112 (external-phase
113 kind: 'run
114 args: (list "sh" "-c"
115 (string-append
116 "set -eu\n"
117 "d=\"$(dirname \"$PWD\")/install-root\"\n"
118 "test -x \"$d${out}/bin/yacc\"\n"
119 ;; `install -m` rather than `cp`: busybox `cp` masks the
120 ;; source mode by the ambient UMASK, which would
121 ;; make the installed permissions a function of the
122 ;; environment rather than of the derivation.
123 "install -m 755 \"$d${out}/bin/yacc\" \"$d${out}/bin/byacc\"\n"
124 "test -x \"$d${out}/bin/byacc\"\n"
125 "echo BYACC-DUAL-NAME-INSTALLED\n"))))))
127 ;; WHY A `file` PACKAGE EXISTS IN A CHANNEL THAT NEVER RUNS `file`.
128 ;;
129 ;; ncurses decides whether the linker honours `-dynamic` by running
130 ;; `file(1)` on a conftest and looking for "static" in the output. The
131 ;; seed has no `file(1)`, so the probe produces nothing, the case statement
132 ;; falls to its default branch, and the ABSENCE OF THE TOOL IS READ AS
133 ;; "yes, switching works". ncurses then links its own `tic` as
134 ;; `-static -lncursesw -lncursesw -dynamic`, which with this toolchain
135 ;; produces an ELF carrying an INTERP segment naming
136 ;; `/lib/ld-musl-x86_64.so.1`. That loader is not a declared sandbox
137 ;; input, exec fails ENOENT, and the seed's shell reports it as
138 ;; `run_tic.sh: line 159: tic: not found`. ncurses cannot run the program
139 ;; it just built and `install.data` fails.
140 ;;
141 ;; Measured, matched pair, same source and same musl wrapper, only PATH
142 ;; differing:
143 ;;
144 ;; file ON PATH -> "switching between static/dynamic... no"
145 ;; LIBS_TIC = -L../lib -lncursesw -lncursesw
146 ;; file ABSENT -> "switching between static/dynamic... yes"
147 ;; LIBS_TIC = -L../lib -static -lncursesw -lncursesw
148 ;; -dynamic
149 ;;
150 ;; So the fix is to make the tool PRESENT rather than to teach ncurses'
151 ;; configure what to conclude when it is missing. A patch to that case
152 ;; statement was written and tested and is NOT used here: it works, but it
153 ;; encodes this channel's deficiency into a third party's source, and it
154 ;; would need retiring the moment the seed gains `file(1)`. Declaring the
155 ;; tool is the same answer the rest of this channel gives -- a package
156 ;; states what it needs instead of the environment being assumed.
157 ;;
158 ;; THIS DOES NOT CLOSE THE SEED GAP (t-46a82e). The seed still has no
159 ;; `file(1)`, which still matters for the seed's own bootstrap and for
160 ;; anything probing before its build inputs are available. This fixes
161 ;; ncurses, not the class.
162 ;;
163 ;; No compression backends: each is a `[default=auto]` probe that would
164 ;; make the output depend on what happens to be visible, and none of them
165 ;; is what ncurses' configure asks this program.
166 (define file
167 (package
168 name: "file"
169 version: "5.48"
170 source: (external-source
171 location: "https://astron.com/pub/file/file-5.48.tar.gz"
172 sha256: "ed14656883b23a364b4057c05595d93252da9bc473d30106519519d0da141283")
173 buildenv: core-buildenv
174 phases: (list
175 (external-phase
176 kind: 'configure
177 args: (list "--prefix=${out}"
178 "--disable-shared"
179 "--enable-static"
180 "--disable-zlib"
181 "--disable-bzlib"
182 "--disable-xzlib"
183 "--disable-zstdlib"
184 "--disable-lzlib"
185 "--disable-lrziplib"
186 "--disable-libseccomp"
187 "--disable-landlock"))
188 (external-phase kind: 'make)
189 (external-phase kind: 'install))))
191 ;;
192 ;; SAMURAI CARRIES A PATCH, AND THE REASON IS NARROWER THAN "PATCHES ARE
193 ;; FINE HERE". This module's `file` package exists because a TOOL WAS
194 ;; MISSING, and the right fix for a missing tool is to supply the tool --
195 ;; a patch there would have written this channel's deficiency into a third
196 ;; party's source. This is the other case: a LATENT PORTABILITY BUG in
197 ;; third-party source, which is exactly what a distribution patch is for.
198 ;;
199 ;; WHAT BREAKS WITHOUT IT. Every fresh build directory:
200 ;;
201 ;; samu: reallocarray: No such file or directory
202 ;;
203 ;; deps.c:262 saves the old dependency-log entries with
204 ;; `xreallocarray(NULL, entrieslen, sizeof(...))`, and on a build directory
205 ;; with no `.ninja_deps` yet `entrieslen` is 0. util.c:61 forwards that to
206 ;; `realloc(p, 0)`, and xreallocarray treats a null result as fatal. The
207 ;; errno in the message is stale from an unrelated open(), which is why it
208 ;; reads like a missing file.
209 ;;
210 ;; THIS TOOLCHAIN'S LIBC RETURNS NULL FROM realloc(NULL, 0) WHILE RETURNING
211 ;; NON-NULL FROM malloc(0). Measured through a volatile function pointer,
212 ;; at -O0 and -O2, with `realloc(NULL, 32)` as a passing positive control --
213 ;; the indirection is necessary because the optimizer rewrites a VISIBLE
214 ;; `realloc(NULL, n)` into `malloc(n)` and hides the real answer.
215 ;;
216 ;; THE MEASUREMENT IS WHAT THIS PACKAGE RELIES ON; THE EXPLANATION IS NOT
217 ;; SETTLED, and an earlier version of this comment overstated it. It said
218 ;; the behaviour was "a property of the zig-bundled libc rather than of musl
219 ;; generally", on the grounds that upstream musl's realloc opens
220 ;; `if (!p) return malloc(n);`. That reasoning does not hold up: zig
221 ;; bundles musl SOURCE and compiles it, so there is no separate
222 ;; implementation for the property to belong to. Either the musl version
223 ;; here differs from the one that reasoning describes, or the reasoning is
224 ;; simply wrong. Nothing in this channel depends on which -- the patch is
225 ;; defensive and correct under either -- so it is left as an open question
226 ;; rather than dressed up as a finding.
227 ;;
228 ;; What IS established and does matter: anything in this channel calling
229 ;; `realloc(p, 0)` and treating NULL as failure will break the same way.
230 ;;
231 ;; REVISIT ON EVERY VERSION BUMP. Upstream master still has the same line,
232 ;; so the patch is expected to keep applying -- but if it stops, the right
233 ;; response is to check whether upstream fixed it, not to force it.
234 ;; samurai is ninja, reimplemented in C99 by Michael Forney.
235 ;;
236 ;; WHY NOT NINJA. ninja is C++, and this channel has no C++ compiler on
237 ;; PATH: `builder-base-assignments` emits no `CXX` and `toolchain/setup.sh`
238 ;; writes wrappers for cc/ar/ranlib/ld only. A package can supply its own
239 ;; `CXX` (see `gperf` below) but a build backend is on the critical path of
240 ;; every meson package here, and a C99 leaf with no dependencies is a
241 ;; better thing to put there than a C++ program with a Python configure
242 ;; script.
243 ;;
244 ;; This is not a substitution meson has to be told about: it searches
245 ;; `['ninja', 'ninja-build', 'samu']` itself
246 ;; (mesonbuild/environment.py:182) and renames what it finds to `samurai`
247 ;; for its own logging. Verified by reading meson 1.8.2's source, not
248 ;; assumed from its documentation.
249 ;;
250 ;; `PREFIX` is passed as an install argument because samurai's Makefile
251 ;; assigns `PREFIX=/usr/local` unconditionally rather than with `?=`, so
252 ;; only a command-line override beats it. Its install target already
253 ;; honours `DESTDIR`, which the builder supplies.
254 (define samurai
255 (package
256 name: "samurai"
257 version: "1.2"
258 source: (external-source
259 location: "https://github.com/michaelforney/samurai/releases/download/1.2/samurai-1.2.tar.gz"
260 sha256: "3b8cf51548dfc49b7efe035e191ff5e1963ebc4fe8f6064a5eefc5343eaf78a5")
261 buildenv: core-buildenv
262 phases: (list
263 (external-phase
264 kind: 'patch
265 patches: (list
266 (channel-patch
267 path: "patches/samurai-realloc-zero-length.patch"
268 sha256: "f160b3d9265a68b88cbcc5e7b04ec5e65f1ef89289dac14919e3702c80e025b3")))
269 (external-phase kind: 'make)
270 (external-phase kind: 'install args: (list "PREFIX=${out}"))
271 ;; ASSERT THE BINARY. Every other package in this change
272 ;; checks what its consumers will look for; samurai and gperf
273 ;; were the two that did not, and gperf is the sharp one --
274 ;; see the note there.
275 (external-phase
276 kind: 'run
277 args: (list "sh" "-c"
278 (string-append
279 "set -eu\n"
280 "d=\"$(dirname \"$PWD\")/install-root\"\n"
281 "test -x \"$d${out}/bin/samu\"\n"
282 "echo SAMURAI-INSTALLED\n"))))))
284 ;; gperf, a perfect-hash generator. Nothing in this channel wants one; it
285 ;; is here because fontconfig's configure RUNS it, at configure time:
286 ;;
287 ;; configure.ac:216 fc_gperf_test="$(echo 'foo' | gperf -L ANSI-C)"
288 ;;
289 ;; and the shape of that line is why the tool is packaged rather than left
290 ;; absent. With gperf missing the command substitution yields the empty
291 ;; string, the two AC_COMPILE_IFELSE arms then compile an empty snippet,
292 ;; the first succeeds, and configure concludes `FC_GPERF_SIZE_T=size_t`
293 ;; FROM A PROBE THAT TESTED NOTHING. Same family as the `file` case in
294 ;; this module and as the ncurses `-dynamic` probe: an absent tool does not
295 ;; announce itself, it produces a confident answer about something else.
296 ;;
297 ;; THIS IS THE CHANNEL'S FIRST C++ PACKAGE, and the `env:` line is what
298 ;; makes it possible. `CXX` is not a builder-owned variable -- the
299 ;; refusal list in `build-environment-assignments` is DERIVED from the
300 ;; assignments the builder emits, and it emits none for C++ -- so a package
301 ;; may declare one. `zig c++` is refused by the toolchain guard without an
302 ;; explicit `-target`, hence the full triple here rather than a bare
303 ;; `zig c++`.
304 ;;
305 ;; THE FALSIFIER, stated so this is a known dependency and not a surprise:
306 ;; if the builder ever starts emitting its own `CXX`, the derived refusal
307 ;; will cover this and the package breaks at once. That is the right
308 ;; trade -- deriving the list is what keeps it honest -- but whoever adds a
309 ;; builder `CXX` inherits converting this.
310 (define gperf
311 (package
312 name: "gperf"
313 version: "3.3"
314 source: (external-source
315 location: "https://ftp.gnu.org/gnu/gperf/gperf-3.3.tar.gz"
316 sha256: "fd87e0aba7e43ae054837afd6cd4db03a3f2693deb3619085e6ed9d8d9604ad8")
317 buildenv: core-buildenv
318 env: (list (cons "CXX" "zig c++ -target x86_64-linux-musl"))
319 phases: (list
320 (external-phase kind: 'configure args: (list "--prefix=${out}"))
321 (external-phase kind: 'make)
322 (external-phase kind: 'install)
323 ;; ASSERT THE BINARY, AND THIS ONE IS LOAD-BEARING RATHER
324 ;; THAN TIDY. The entire argument for packaging gperf is
325 ;; that an ABSENT gperf makes fontconfig's configure.ac:216
326 ;; probe conclude `FC_GPERF_SIZE_T=size_t` from a command
327 ;; substitution that tested nothing -- with configure still
328 ;; exiting 0. Packaging gperf closes that only if
329 ;; `bin/gperf` actually exists. Without this line, a gperf
330 ;; whose `make install` put the binary somewhere else would
331 ;; ship green and hand fontconfig straight back into the
332 ;; silent-wrong-answer state this package was written to
333 ;; prevent, with every gate downstream still passing.
334 (external-phase
335 kind: 'run
336 args: (list "sh" "-c"
337 (string-append
338 "set -eu\n"
339 "d=\"$(dirname \"$PWD\")/install-root\"\n"
340 "test -x \"$d${out}/bin/gperf\"\n"
341 ;; And prove it RUNS, because the thing
342 ;; fontconfig does is run it.
343 "\"$d${out}/bin/gperf\" --version | head -1\n"
344 "echo GPERF-INSTALLED\n"))))))
346 ;; meson. Pure Python, so there is nothing to compile and the whole
347 ;; package is one `run` phase that copies a tree, writes a launcher, and
348 ;; then RUNS that launcher.
349 ;;
350 ;; WHY THE LAUNCHER EXECS `python3` FROM PATH RATHER THAN AN ABSOLUTE PATH.
351 ;; The honest way to write this would be `${python}/bin/python3`, but
352 ;; `${name}` interpolation of a BUILD INPUT is refused at the identity
353 ;; door -- a baked build-input path would be an undeclared runtime
354 ;; reference -- and python is not a runtime dependency of the meson
355 ;; *package*, it is a tool meson needs when it runs.
356 ;;
357 ;; Resolving from PATH is safe here in a way it would not be on a host:
358 ;; inside the build sandbox PATH is exactly the declared build inputs,
359 ;; then the seed, then the toolchain. There is no host filesystem to fall
360 ;; through to. So a consumer that declares `meson` without also declaring
361 ;; `python` gets a loud "python3: not found", not a silent host binary --
362 ;; which is the failure direction to be wrong in. Every meson consumer in
363 ;; this channel declares both, and foot needs python3 directly anyway.
364 (define meson
365 (package
366 name: "meson"
367 version: "1.8.2"
368 source: (external-source
369 location: "https://github.com/mesonbuild/meson/releases/download/1.8.2/meson-1.8.2.tar.gz"
370 sha256: "c105816d8158c76b72adcb9ff60297719096da7d07f6b1f000fd8c013cd387af")
371 buildenv: core-buildenv
372 ;; PYTHON IS A BUILD INPUT SO THAT THIS PACKAGE CAN RUN ITS OWN
373 ;; LAUNCHER. meson compiles nothing, so strictly it needs no inputs at
374 ;; all -- and that was the earlier definition. The cost was that the
375 ;; single most fragile artifact here, a hand-written shell wrapper on
376 ;; the critical path of five packages, was never executed by the
377 ;; package that produces it: the first thing to run it was the next
378 ;; package's `meson setup`, one build later. Declaring python buys the
379 ;; `--version` call at the end of the phase, which is a real gate.
380 build-inputs: (list python)
381 phases: (list
382 (external-phase
383 kind: 'run
384 args: (list "sh" "-c"
385 (string-append
386 "set -eu\n"
387 "d=\"$(dirname \"$PWD\")/install-root\"\n"
388 "mkdir -p \"$d${out}/lib/meson\" \"$d${out}/bin\"\n"
389 "cp -a mesonbuild \"$d${out}/lib/meson/\"\n"
390 ;; `install -m` rather than `cp`: busybox `cp`
391 ;; creates the destination with the source's mode
392 ;; masked by the ambient UMASK, so the installed
393 ;; permissions would be a function of the
394 ;; environment rather than of the derivation.
395 ;; Every autotools `make install` in this channel
396 ;; uses `install -m` for exactly that reason.
397 "install -m 644 meson.py \"$d${out}/lib/meson/meson.py\"\n"
398 ;; THE LAUNCHER LOCATES ITSELF rather than baking
399 ;; `${out}`. `${out}` expands to the SANDBOX
400 ;; view -- /sigil/store/<item> -- which exists
401 ;; inside any build that binds meson and nowhere
402 ;; else. A launcher carrying that path works
403 ;; where meson is actually used and dies under
404 ;; `sigil env shell`, which is a real failure
405 ;; mode this package hit on its first build, and
406 ;; which the same shape gives ncurses'
407 ;; compiled-in terminfo root. Deriving the
408 ;; prefix from `$0` costs one line, works in both
409 ;; places, and leaves no absolute path in the
410 ;; artifact at all.
411 ;;
412 ;; A quoted heredoc delimiter keeps the shell out
413 ;; of `$0` and `$@`; neither uses braces, so
414 ;; sigil's own `${...}` interpolation does not see
415 ;; them either.
416 ;;
417 ;; `-B` IS LOAD-BEARING AND THIS PACKAGE PROVED IT
418 ;; THE EXPENSIVE WAY. Without it, the FIRST
419 ;; `meson --version` wrote 99 `.pyc` files into
420 ;; `__pycache__` directories INSIDE meson's own
421 ;; store item. The item then failed content
422 ;; verification on its next use and sigil
423 ;; quarantined it:
424 ;;
425 ;; sigil env: REFUSING CORRUPT <hash>: output
426 ;; candidates failed content verification
427 ;;
428 ;; Measured, not inferred -- the quarantined
429 ;; directory was inspected and the 99 files
430 ;; counted. The refusal is correct behaviour and
431 ;; the package was wrong.
432 ;;
433 ;; GENERALISE THIS: any interpreted-language
434 ;; package in a content-addressed store must be
435 ;; stopped from writing beside its own sources at
436 ;; run time. Python is merely the first one here.
437 ;; The failure is also delayed by one invocation,
438 ;; so it does not look like a packaging bug -- the
439 ;; run that breaks the item succeeds.
440 "cat > \"$d${out}/bin/meson\" <<'WRAP'\n"
441 "#!/bin/sh\n"
442 "here=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\n"
443 "exec python3 -B \"$here/../lib/meson/meson.py\" \"$@\"\n"
444 "WRAP\n"
445 "chmod 755 \"$d${out}/bin/meson\"\n"
446 "test -d \"$d${out}/lib/meson/mesonbuild\"\n"
447 ;; RUN THE LAUNCHER. This is the gate the earlier
448 ;; definition lacked: it exercises the shebang,
449 ;; the `$0`-relative prefix derivation, the `-B`
450 ;; flag, python3 resolving from PATH, and
451 ;; mesonbuild importing -- none of which the
452 ;; preceding `test -d` touches, because that is
453 ;; subsumed by the `cp -a` above it.
454 "v=\"$(\"$d${out}/bin/meson\" --version)\"\n"
455 "echo \"MESON-LAUNCHER-VERSION=[$v]\"\n"
456 "test \"$v\" = \"1.8.2\"\n"
457 ;; And prove `-B` did its job: running the
458 ;; launcher must NOT have written bytecode beside
459 ;; the sources, or the store item corrupts itself
460 ;; on first use.
461 "n=$(find \"$d${out}/lib/meson\" -name '__pycache__' -type d | wc -l)\n"
462 "echo \"MESON-PYCACHE-DIRS=$n\"\n"
463 "test \"$n\" -eq 0\n"
464 "echo MESON-INSTALLED\n"))))))))