Commit0ff698d4Recorded24 Aug 2026Repositorycore-channel

Add LuaJIT, and record why its provenance is a scoped exception

Message

LuaJIT builds under this toolchain. Everything that looks hard about that worked first time -- minilua bootstrapped, DynASM generated x86-64 assembly, clang's integrated assembler consumed it, libluajit.a linked. Two things stopped the final executable:

  • The builder's LDFLAGS ships no DWARF unwinder, so lj_err.o's five Unwind*/__register_frame references were undefined at the last step. The toolchain HAS one; TARGET_XLIBS is extended (not replaced -- its default -lm matters) with -lunwind. All five symbols were confirmed present individually, because the last two are a different family and a control on the first would not have covered them.
  • strip is not in the seed, and LuaJIT's link rule ends with $(TARGET_STRIP). Nothing needed packaging: the builder's -s already strips at link, and the toolchain's copy-strip is a no-op stand-in.

It is a run-phase package rather than make/install phases for a reason worth knowing. LuaJIT's src/Makefile assigns CC= $(DEFAULT_CC), and a makefile assignment beats the environment in make's precedence, so the builder's CC is silently ignored. Only a command-line assignment wins -- but a make phase's arguments are shell-quoted individually, so CC=$CC would be passed as that literal string, and naming the compiler here would defeat the invariant that it is derived from the derivation's target. sh -c lets the shell expand $CC, which forwards whatever the builder set without naming it.

Verified behaviourally rather than by existence: the installed binary computes 6*7, and a two-million-iteration loop returns the right sum with jit.status() still true afterwards -- which is the difference between "a Lua interpreter linked" and "the JIT works".

PROVENANCE. LuaJIT publishes no release tarball of any kind; this pins a git commit fetched as a GitHub auto-generated archive. That is weaker than anything else in this channel and is a deliberate, scoped exception: the rule it appears to break was set for a chain intended to ship, and this is a discovery exercise whose product is knowledge about what the packaging model can host. Neither this nor core/swayimg should be published or signed on this provenance without revisiting the question. Said at the package site too.

Changed
 channel.sgl                  |   6 ++++--
 src/core/packages/luajit.sgl | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 174 insertions(+), 2 deletions(-)
Diff
channel.sglmodified
@@ -16,7 +16,8 @@
16
(core packages datastructures)
17
(core packages wayland)
18
(core packages input)
−19
(core packages capture))
+19
(core packages capture)
+20
(core packages luajit))
21
22
(channel
23
stream: "core-stable"
@@ -51,4 +52,5 @@
52
(channel-binding name: "core/foot" value: foot)
53
(channel-binding name: "core/vim" value: vim)
54
(channel-binding name: "core/emacs" value: emacs)
−54
(channel-binding name: "core/python" value: python)))
+55
(channel-binding name: "core/python" value: python)
+56
(channel-binding name: "core/luajit" value: luajit)))
src/core/packages/luajit.sgladded
@@ -0,0 +1,170 @@
+1
;;; LuaJIT.
+2
;;;
+3
;;; PROVENANCE: READ THIS BEFORE PUBLISHING OR SIGNING ANYTHING HERE.
+4
;;;
+5
;;; This package is pinned to a GIT COMMIT fetched as a GitHub auto-generated
+6
;;; archive, because LuaJIT PUBLISHES NO RELEASE TARBALL OF ANY KIND -- no
+7
;;; release assets, and luajit.org's download page links only to git
+8
;;; repositories. Its last tag is v2.1.0-beta3 from 2017; every distribution
+9
;;; pins a commit.
+10
;;;
+11
;;; That is a WEAKER provenance than anything else in this channel, and it is a
+12
;;; deliberate, scoped exception rather than a precedent. The rule it appears
+13
;;; to break -- that a synthetic archive whose bytes can be regenerated does
+14
;;; not belong on a critical path -- was set for a chain INTENDED TO SHIP.
+15
;;; This package and `core/swayimg` exist for a different reason: they are a
+16
;;; DISCOVERY EXERCISE whose product is knowledge about what this packaging
+17
;;; model can and cannot host. Two auto-generated archives are acceptable for
+18
;;; that and would not be acceptable for a shipped chain.
+19
;;;
+20
;;; SO: NEITHER THIS PACKAGE NOR `core/swayimg` SHOULD BE PUBLISHED OR SIGNED
+21
;;; ON THIS PROVENANCE WITHOUT REVISITING THE QUESTION. If LuaJIT is ever
+22
;;; wanted for real, the question to answer first is what artifact to pin, not
+23
;;; how to build it -- building it is solved, below.
+24
(define-library (core packages luajit)
+25
(import (sigil core)
+26
(core build-environments)
+27
(sigil env package)
+28
(sigil build external))
+29
(export luajit)
+30
(begin
+31
;; THE ONE THING THAT STOPPED IT BUILDING, and it is a property of the
+32
;; CHANNEL rather than of LuaJIT.
+33
;;
+34
;; Everything that looks hard about building a JIT under a foreign
+35
;; toolchain worked first time: minilua bootstrapped, DynASM generated
+36
;; x86-64 assembly, clang's integrated assembler consumed it, and
+37
;; `libluajit.a` linked. Only the FINAL EXECUTABLE link failed:
+38
;;
+39
;; ld.lld: error: undefined symbol: _Unwind_SetIP
+40
;; ld.lld: error: undefined symbol: _Unwind_GetIP
+41
;; ld.lld: error: undefined symbol: _Unwind_RaiseException
+42
;; ld.lld: error: undefined symbol: __register_frame
+43
;; ld.lld: error: undefined symbol: __deregister_frame
+44
;;
+45
;; The builder's LDFLAGS ships no DWARF unwinder. The toolchain HAS one
+46
;; (zig's `lib/libunwind`); it is simply not linked by default. All five
+47
;; symbols were confirmed present in `-lunwind` INDIVIDUALLY -- the last
+48
;; two are a different family, the GCC JIT frame-registration entry points,
+49
;; and a control on only the first would not have covered them.
+50
;; [[topics/core-channel-links-no-dwarf-unwinder]]
+51
;;
+52
;; `TARGET_XLIBS` is EXTENDED, not replaced: its default is `-lm`, and
+53
;; dropping that would break the link for a different reason.
+54
(define luajit
+55
(package
+56
name: "luajit"
+57
version: "2.1.1787165859"
+58
source: (external-source
+59
location: "https://codeload.github.com/LuaJIT/LuaJIT/tar.gz/1ee778a4e37122d8ca7d5733c590a47dafd6b15c"
+60
sha256: "512cdb23dafa39c6247763ae15f29341734b1596067b067db0c0387bbebc7c55")
+61
buildenv: core-buildenv
+62
phases: (list
+63
;; WHY `run` PHASES AND NOT `make`/`install` PHASES, which
+64
;; would be shorter.
+65
;;
+66
;; LuaJIT's src/Makefile:29 says `CC= $(DEFAULT_CC)`. A
+67
;; MAKEFILE assignment beats the ENVIRONMENT in make's
+68
;; precedence rules, so the builder's `CC` is silently
+69
;; ignored and LuaJIT compiles with its own default -- which
+70
;; is not the channel's target-explicit wrapper. Only a
+71
;; command-line assignment beats a makefile one.
+72
;;
+73
;; But a `make` phase's arguments are shell-quoted
+74
;; individually, so `CC=$CC` would be passed as that literal
+75
;; four-character string rather than expanded. And writing
+76
;; the compiler's NAME here would defeat the invariant that
+77
;; the compiler is DERIVED from the derivation's target --
+78
;; the same invariant `build-environment-assignments` refuses
+79
;; an `env: CC=` override to protect.
+80
;;
+81
;; A `run` phase with `sh -c` is the honest way out: the
+82
;; shell expands `$CC`, so this FORWARDS whatever the builder
+83
;; set without naming it. Note `$CC` and not `${CC}` --
+84
;; braces are intercepted by sigil's own interpolation before
+85
;; any shell sees them.
+86
(external-phase
+87
kind: 'run
+88
args: (list "sh" "-c"
+89
(string-append
+90
"set -eu\n"
+91
"echo \"LUAJIT-FORWARDED-CC=[$CC]\"\n"
+92
"make -j\"$(nproc 2>/dev/null || echo 4)\""
+93
" CC=\"$CC\" HOST_CC=\"$CC\""
+94
" BUILDMODE=static"
+95
" TARGET_XLIBS=\"-lm -lunwind\""
+96
;; `TARGET_STRIP=copy-strip` BECAUSE THE SEED HAS
+97
;; NO `strip`. LuaJIT's link rule ends with
+98
;; `$(TARGET_STRIP) $@`, which dies:
+99
;;
+100
;; make[1]: strip: No such file or directory
+101
;; make[1]: *** [Makefile:754: luajit] Error 127
+102
;;
+103
;; Another instance of the family this channel
+104
;; keeps meeting -- `yacc`, `file`, `gperf`,
+105
;; `python3` -- except this one fails LOUDLY with
+106
;; 127 rather than producing a confident wrong
+107
;; answer, which is the better direction.
+108
;;
+109
;; Nothing needs to be packaged for it: the
+110
;; binary is ALREADY stripped, because the
+111
;; builder's LDFLAGS carries `-s`. A second pass
+112
;; would be redundant even if the tool existed.
+113
;; `copy-strip` is the toolchain's own no-op
+114
;; stand-in for exactly this shape
+115
;; (toolchain/setup.sh writes it); invoked with a
+116
;; single argument and no `-o`, it validates the
+117
;; input and does nothing.
+118
" TARGET_STRIP=copy-strip"
+119
"\n"
+120
"echo LUAJIT-BUILT\n")))
+121
;; `install` re-enters make, whose INSTALL_DEP is
+122
;; `src/luajit` -- so it must be invoked with the same CC or
+123
;; a stale-dependency check could relink with the wrong
+124
;; compiler. Same forwarding, same reason.
+125
;;
+126
;; LuaJIT wants `PREFIX` (uppercase); the builder's `install`
+127
;; phase supplies lowercase `prefix`, which LuaJIT ignores.
+128
;; It does honour `DESTDIR`, which is derived here from the
+129
;; phase's cwd as everywhere else in this channel.
+130
(external-phase
+131
kind: 'run
+132
args: (list "sh" "-c"
+133
(string-append
+134
"set -eu\n"
+135
"d=\"$(dirname \"$PWD\")/install-root\"\n"
+136
"make install"
+137
" CC=\"$CC\" HOST_CC=\"$CC\""
+138
" BUILDMODE=static"
+139
" TARGET_XLIBS=\"-lm -lunwind\""
+140
" TARGET_STRIP=copy-strip"
+141
" PREFIX=\"${out}\" DESTDIR=\"$d\"\n"
+142
"echo LUAJIT-INSTALLED\n")))
+143
;; ASSERT THE ARTIFACT BEHAVIOURALLY, not by existence.
+144
;;
+145
;; `test -x bin/luajit` would pass on a binary that cannot
+146
;; run. What is actually in question for an
+147
;; assembly-generating build under a foreign toolchain is
+148
;; whether the JIT WORKS -- so run a loop big enough to be
+149
;; compiled and check the JIT is still enabled afterwards.
+150
;; `jit ~= nil` alone only proves the module exists.
+151
(external-phase
+152
kind: 'run
+153
args: (list "sh" "-c"
+154
(string-append
+155
"set -eu\n"
+156
"d=\"$(dirname \"$PWD\")/install-root\"\n"
+157
"lj=\"$d${out}/bin/luajit\"\n"
+158
"test -x \"$lj\"\n"
+159
"v=$(\"$lj\" -e 'io.write(6*7)')\n"
+160
"echo \"LUAJIT-ARITHMETIC=[$v]\"\n"
+161
"test \"$v\" = 42\n"
+162
"s=$(\"$lj\" -e 'local n=0 for i=1,2e6 do n=n+i end io.write(n,\"|\",tostring(jit.status()))')\n"
+163
"echo \"LUAJIT-HOTLOOP=[$s]\"\n"
+164
"test \"$s\" = '2000001000000|true'\n"
+165
;; The `.pc` matters: swayimg finds this through
+166
;; `dependency('luajit')`, and it must land in
+167
;; lib/pkgconfig because this channel composes
+168
;; PKG_CONFIG_PATH from exactly that directory.
+169
"test -f \"$d${out}/lib/pkgconfig/luajit.pc\"\n"
+170
"echo LUAJIT-VERIFIED\n"))))))))