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 it4
;;; exists to hold is not cosmetic. A `dependencies:` entry contributes5
;;; `-I<dep>/include`, `-L<dep>/lib` and `<dep>/lib/pkgconfig`, and its store6
;;; path can legitimately end up inside the built artifact. A `build-inputs:`7
;;; entry contributes its `bin/` to PATH -- ahead of the seed, per8
;; builder-base-assignments in (sigil build external), though nothing in this9
;; change measures the ordering because neither tool exists in the seed at all10
;; -- and nothing11
;;; else -- deliberately not `${name}` interpolation, because a baked tool path12
;;; would be an undeclared RUNTIME reference to something that is only a13
;;; 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
(begin22
;; WHY THE CHANNEL NEEDS A YACC AT ALL, since nothing here has a grammar of23
;; 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 is29
;; `AC_CHECK_PROG(found_yacc, $YACC, yes, no)` followed by an30
;; unconditional AC_MSG_ERROR -- configure asks whether the program exists,31
;; not whether the build will need it. Measured, not inferred: the real32
;; configure was run against a PATH stripped of yacc/bison/byacc, and the33
;; seed's `bin/` was enumerated at 0 of those 3.34
;;35
;; Patching that check out would have been shorter and would have hidden a36
;; true statement about the channel: it had no build tools.37
;;38
;; byacc rather than bison because byacc is a leaf -- plain autotools, no39
;; dependencies, no m4 and no gettext. Its makefile installs the program40
;; as `bin/yacc` (makefile.in `THIS = yacc`), which is the name configure41
;; looks for.42
;;43
;; Dickey's byacc is a dated rolling release; the archive at44
;; invisible-mirror.net keeps every dated tarball, so the URL is stable45
;; even though "the latest byacc" is not.46
(define byacc47
(package48
name: "byacc"49
version: "20260126"50
source: (external-source51
location: "https://invisible-mirror.net/archives/byacc/byacc-20260126.tgz"52
sha256: "b618c5fb44c2f5f048843db90f7d1b24f78f47b07913c8c7ba8c942d3eb24b00")53
buildenv: core-buildenv54
phases: (list55
(external-phase56
kind: 'configure57
args: (list "--prefix=${out}"58
;; BACKTRACKING IS ENABLED, AND THIS REVERSES59
;; AN EARLIER DECISION IN THIS PACKAGE. It was60
;; `--disable-btyacc` when tmux was the only61
;; consumer, on the grounds that tmux uses plain62
;; yacc input and does not want the extension.63
;;64
;; libxkbcommon does want it. Its grammar uses65
;; `%destructor`, which is a btyacc extension,66
;; and its own meson.build says so outright:67
;; "we use some yacc extensions, which work with68
;; either GNU bison (preferred) or byacc (with69
;; backtracking enabled)". Without it:70
;;71
;; byacc: e - line 230 of "parser.y", syntax72
;; error %destructor { FreeStmt(...); }73
;;74
;; MEASURED BOTH WAYS on this toolchain before75
;; changing it, with a control for the case the76
;; earlier decision was protecting:77
;;78
;; no btyacc + libxkbcommon grammar -> syntax error79
;; btyacc + libxkbcommon grammar -> parser.c, 3393 lines80
;; btyacc + a PLAIN grammar -> parser.c, 1177 lines81
;; btyacc, installed name -> still bin/yacc82
;;83
;; So the extension is a superset and tmux loses84
;; nothing. In fact tmux never runs this program85
;; at all -- its release tarball ships86
;; `cmd-parse.c` pre-generated and its configure87
;; only does `AC_CHECK_PROG` -- so the flag was88
;; 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, AND93
;; THIS IS DELIBERATE -- do not "tidy" it away.94
;;95
;; byacc's own makefile installs exactly one binary, named96
;; `yacc` (makefile.in:5, `THIS = yacc`), because that is the97
;; POSIX name tmux's configure looks for. But `byacc` is the98
;; program's ACTUAL name, and it is the name a second consumer99
;; asks for: libxkbcommon 1.7.0's meson.build:166 is100
;; `find_program('byacc', required: false)`, reached only after101
;; bison is not found. A `yacc` on PATH does not satisfy it.102
;;103
;; Debian and Fedora both ship both names. A package that104
;; installs only the POSIX alias is the odd one out, and the105
;; alternative -- patching libxkbcommon to look for `yacc` --106
;; would write this channel's deficiency into a third party's107
;; source, which is the trade `file` in this same module108
;; already refused to make.109
;;110
;; Choosing byacc over bison is what keeps GNU m4 out of this111
;; channel: bison needs m4 to build, byacc needs nothing.112
(external-phase113
kind: 'run114
args: (list "sh" "-c"115
(string-append116
"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 the120
;; source mode by the ambient UMASK, which would121
;; make the installed permissions a function of the122
;; 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 running130
;; `file(1)` on a conftest and looking for "static" in the output. The131
;; seed has no `file(1)`, so the probe produces nothing, the case statement132
;; falls to its default branch, and the ABSENCE OF THE TOOL IS READ AS133
;; "yes, switching works". ncurses then links its own `tic` as134
;; `-static -lncursesw -lncursesw -dynamic`, which with this toolchain135
;; produces an ELF carrying an INTERP segment naming136
;; `/lib/ld-musl-x86_64.so.1`. That loader is not a declared sandbox137
;; input, exec fails ENOENT, and the seed's shell reports it as138
;; `run_tic.sh: line 159: tic: not found`. ncurses cannot run the program139
;; it just built and `install.data` fails.140
;;141
;; Measured, matched pair, same source and same musl wrapper, only PATH142
;; differing:143
;;144
;; file ON PATH -> "switching between static/dynamic... no"145
;; LIBS_TIC = -L../lib -lncursesw -lncursesw146
;; file ABSENT -> "switching between static/dynamic... yes"147
;; LIBS_TIC = -L../lib -static -lncursesw -lncursesw148
;; -dynamic149
;;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 case152
;; statement was written and tested and is NOT used here: it works, but it153
;; encodes this channel's deficiency into a third party's source, and it154
;; would need retiring the moment the seed gains `file(1)`. Declaring the155
;; tool is the same answer the rest of this channel gives -- a package156
;; 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 no159
;; `file(1)`, which still matters for the seed's own bootstrap and for160
;; anything probing before its build inputs are available. This fixes161
;; ncurses, not the class.162
;;163
;; No compression backends: each is a `[default=auto]` probe that would164
;; make the output depend on what happens to be visible, and none of them165
;; is what ncurses' configure asks this program.166
(define file167
(package168
name: "file"169
version: "5.48"170
source: (external-source171
location: "https://astron.com/pub/file/file-5.48.tar.gz"172
sha256: "ed14656883b23a364b4057c05595d93252da9bc473d30106519519d0da141283")173
buildenv: core-buildenv174
phases: (list175
(external-phase176
kind: 'configure177
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 ARE193
;; FINE HERE". This module's `file` package exists because a TOOL WAS194
;; 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 third196
;; party's source. This is the other case: a LATENT PORTABILITY BUG in197
;; 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 directory202
;;203
;; deps.c:262 saves the old dependency-log entries with204
;; `xreallocarray(NULL, entrieslen, sizeof(...))`, and on a build directory205
;; with no `.ninja_deps` yet `entrieslen` is 0. util.c:61 forwards that to206
;; `realloc(p, 0)`, and xreallocarray treats a null result as fatal. The207
;; errno in the message is stale from an unrelated open(), which is why it208
;; reads like a missing file.209
;;210
;; THIS TOOLCHAIN'S LIBC RETURNS NULL FROM realloc(NULL, 0) WHILE RETURNING211
;; 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 VISIBLE214
;; `realloc(NULL, n)` into `malloc(n)` and hides the real answer.215
;;216
;; THE MEASUREMENT IS WHAT THIS PACKAGE RELIES ON; THE EXPLANATION IS NOT217
;; SETTLED, and an earlier version of this comment overstated it. It said218
;; the behaviour was "a property of the zig-bundled libc rather than of musl219
;; generally", on the grounds that upstream musl's realloc opens220
;; `if (!p) return malloc(n);`. That reasoning does not hold up: zig221
;; bundles musl SOURCE and compiles it, so there is no separate222
;; implementation for the property to belong to. Either the musl version223
;; here differs from the one that reasoning describes, or the reasoning is224
;; simply wrong. Nothing in this channel depends on which -- the patch is225
;; defensive and correct under either -- so it is left as an open question226
;; rather than dressed up as a finding.227
;;228
;; What IS established and does matter: anything in this channel calling229
;; `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 right233
;; 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 on237
;; 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 own239
;; `CXX` (see `gperf` below) but a build backend is on the critical path of240
;; every meson package here, and a C99 leaf with no dependencies is a241
;; better thing to put there than a C++ program with a Python configure242
;; script.243
;;244
;; This is not a substitution meson has to be told about: it searches245
;; `['ninja', 'ninja-build', 'samu']` itself246
;; (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, not248
;; assumed from its documentation.249
;;250
;; `PREFIX` is passed as an install argument because samurai's Makefile251
;; assigns `PREFIX=/usr/local` unconditionally rather than with `?=`, so252
;; only a command-line override beats it. Its install target already253
;; honours `DESTDIR`, which the builder supplies.254
(define samurai255
(package256
name: "samurai"257
version: "1.2"258
source: (external-source259
location: "https://github.com/michaelforney/samurai/releases/download/1.2/samurai-1.2.tar.gz"260
sha256: "3b8cf51548dfc49b7efe035e191ff5e1963ebc4fe8f6064a5eefc5343eaf78a5")261
buildenv: core-buildenv262
phases: (list263
(external-phase264
kind: 'patch265
patches: (list266
(channel-patch267
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 change272
;; checks what its consumers will look for; samurai and gperf273
;; were the two that did not, and gperf is the sharp one --274
;; see the note there.275
(external-phase276
kind: 'run277
args: (list "sh" "-c"278
(string-append279
"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; it285
;; 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 left290
;; absent. With gperf missing the command substitution yields the empty291
;; 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 in294
;; this module and as the ncurses `-dynamic` probe: an absent tool does not295
;; 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 what298
;; makes it possible. `CXX` is not a builder-owned variable -- the299
;; refusal list in `build-environment-assignments` is DERIVED from the300
;; assignments the builder emits, and it emits none for C++ -- so a package301
;; may declare one. `zig c++` is refused by the toolchain guard without an302
;; explicit `-target`, hence the full triple here rather than a bare303
;; `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 refusal307
;; will cover this and the package breaks at once. That is the right308
;; trade -- deriving the list is what keeps it honest -- but whoever adds a309
;; builder `CXX` inherits converting this.310
(define gperf311
(package312
name: "gperf"313
version: "3.3"314
source: (external-source315
location: "https://ftp.gnu.org/gnu/gperf/gperf-3.3.tar.gz"316
sha256: "fd87e0aba7e43ae054837afd6cd4db03a3f2693deb3619085e6ed9d8d9604ad8")317
buildenv: core-buildenv318
env: (list (cons "CXX" "zig c++ -target x86_64-linux-musl"))319
phases: (list320
(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 RATHER324
;; THAN TIDY. The entire argument for packaging gperf is325
;; that an ABSENT gperf makes fontconfig's configure.ac:216326
;; probe conclude `FC_GPERF_SIZE_T=size_t` from a command327
;; substitution that tested nothing -- with configure still328
;; exiting 0. Packaging gperf closes that only if329
;; `bin/gperf` actually exists. Without this line, a gperf330
;; whose `make install` put the binary somewhere else would331
;; ship green and hand fontconfig straight back into the332
;; silent-wrong-answer state this package was written to333
;; prevent, with every gate downstream still passing.334
(external-phase335
kind: 'run336
args: (list "sh" "-c"337
(string-append338
"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 thing342
;; 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 whole347
;; package is one `run` phase that copies a tree, writes a launcher, and348
;; 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`, but352
;; `${name}` interpolation of a BUILD INPUT is refused at the identity353
;; door -- a baked build-input path would be an undeclared runtime354
;; reference -- and python is not a runtime dependency of the meson355
;; *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 fall360
;; through to. So a consumer that declares `meson` without also declaring361
;; `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 in363
;; this channel declares both, and foot needs python3 directly anyway.364
(define meson365
(package366
name: "meson"367
version: "1.8.2"368
source: (external-source369
location: "https://github.com/mesonbuild/meson/releases/download/1.8.2/meson-1.8.2.tar.gz"370
sha256: "c105816d8158c76b72adcb9ff60297719096da7d07f6b1f000fd8c013cd387af")371
buildenv: core-buildenv372
;; PYTHON IS A BUILD INPUT SO THAT THIS PACKAGE CAN RUN ITS OWN373
;; LAUNCHER. meson compiles nothing, so strictly it needs no inputs at374
;; all -- and that was the earlier definition. The cost was that the375
;; single most fragile artifact here, a hand-written shell wrapper on376
;; the critical path of five packages, was never executed by the377
;; package that produces it: the first thing to run it was the next378
;; package's `meson setup`, one build later. Declaring python buys the379
;; `--version` call at the end of the phase, which is a real gate.380
build-inputs: (list python)381
phases: (list382
(external-phase383
kind: 'run384
args: (list "sh" "-c"385
(string-append386
"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 mode392
;; masked by the ambient UMASK, so the installed393
;; permissions would be a function of the394
;; environment rather than of the derivation.395
;; Every autotools `make install` in this channel396
;; 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 baking399
;; `${out}`. `${out}` expands to the SANDBOX400
;; view -- /sigil/store/<item> -- which exists401
;; inside any build that binds meson and nowhere402
;; else. A launcher carrying that path works403
;; where meson is actually used and dies under404
;; `sigil env shell`, which is a real failure405
;; mode this package hit on its first build, and406
;; which the same shape gives ncurses'407
;; compiled-in terminfo root. Deriving the408
;; prefix from `$0` costs one line, works in both409
;; places, and leaves no absolute path in the410
;; artifact at all.411
;;412
;; A quoted heredoc delimiter keeps the shell out413
;; of `$0` and `$@`; neither uses braces, so414
;; sigil's own `${...}` interpolation does not see415
;; them either.416
;;417
;; `-B` IS LOAD-BEARING AND THIS PACKAGE PROVED IT418
;; THE EXPENSIVE WAY. Without it, the FIRST419
;; `meson --version` wrote 99 `.pyc` files into420
;; `__pycache__` directories INSIDE meson's own421
;; store item. The item then failed content422
;; verification on its next use and sigil423
;; quarantined it:424
;;425
;; sigil env: REFUSING CORRUPT <hash>: output426
;; candidates failed content verification427
;;428
;; Measured, not inferred -- the quarantined429
;; directory was inspected and the 99 files430
;; counted. The refusal is correct behaviour and431
;; the package was wrong.432
;;433
;; GENERALISE THIS: any interpreted-language434
;; package in a content-addressed store must be435
;; stopped from writing beside its own sources at436
;; 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 -- the439
;; 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 earlier448
;; definition lacked: it exercises the shebang,449
;; the `$0`-relative prefix derivation, the `-B`450
;; flag, python3 resolving from PATH, and451
;; mesonbuild importing -- none of which the452
;; preceding `test -d` touches, because that is453
;; 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 the458
;; launcher must NOT have written bytecode beside459
;; the sources, or the store item corrupts itself460
;; 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"))))))))