commit e31e5c1bb3b6f5fc1f1f72d266faf61d041012dd
parent f40962dac334abe1b9e7d39892fef2eee92018d1
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sun, 26 Apr 2026 16:34:29 -0700
scripts: rework tcc bootstrap into 3 stages + add live-bootstrap diag
Replace build-tcc-source.sh and build-tcc-real.sh with three explicit
stages that match the seam our scheme1 cc will fill:
stage1-flatten.sh (host) tcc.c -> tcc.flat.c
stage2-alpine.sh (alpine) tcc.flat.c -> tcc-host -> tcc-boot0-mes
stage3-rebuild.sh (busybox:musl) tcc-boot0-mes -> boot1 -> boot2
Stage 2 is the slot scheme1 cc replaces: gcc plays the role today,
producing tcc-host from tcc.flat.c and then compile+linking the patched
real tcc.c against mes libc directly into tcc-boot0-mes (live-bootstrap
tcc-boot0-style invocation, no tcc-self.o middle step). Stage 3 stays
purely in busybox; tcc-boot0-mes provides its own preprocessor,
assembler, linker, and ar.
Picked alpine over debian for stage 2 because mes headers declare
errno as a plain global while glibc declares it TLS, which clashes at
link time without invasive transformations to tcc.flat.c. musl exposes
errno only via __errno_location() and the one-line shim suffices.
Also adds scripts/diag-livebootstrap-qemu.sh — a diagnostic-only script
that runs upstream live-bootstrap's amd64 pass1 chain through tcc-0.9.27
inside the same busybox + linux/amd64 QEMU, to disambiguate "QEMU
issue" from "our build". Result captured in docs/TCC.md Issues:
- Issue 2 (large-TU SEGV) reproduces in canonical live-bootstrap on
this platform; the per-file workaround is load-bearing for any
tcc-0.9.26-on-QEMU build, not specific to our path.
- Issue 3 (tcc-boot0-mes startup SEGV) does NOT reproduce in
live-bootstrap's mescc-built tcc-mes under the same QEMU; our
tcc-host-built path hits codegen mescc avoids. Fix candidate is a
backport of tcc 0.9.28rc prologue / _DYNAMIC patches.
Diffstat:
7 files changed, 1098 insertions(+), 596 deletions(-)
diff --git a/docs/TCC.md b/docs/TCC.md
@@ -1,31 +1,38 @@
# Building tcc-0.9.26 from this repo
-Working doc. Describes the host-side pipeline that takes the upstream
-tcc-0.9.26 source tarball through to a working tcc-0.9.26 binary linked
-against mes libc, **without** depending on M2-Planet, Mes Scheme, or
-MesCC at any stage. Two scripts drive the pipeline:
-
-- [scripts/build-tcc-source.sh](../scripts/build-tcc-source.sh) — host
- C preprocessor produces a flattened single-source `tcc.flat.c`, then
- bootstraps `tcc-host` from it via host gcc.
-- [scripts/build-tcc-real.sh](../scripts/build-tcc-real.sh) — uses
- `tcc-host` to compile mes libc and link a real tcc-0.9.26 binary
- (`tcc-boot0-mes`).
+Working doc. Describes a three-stage host pipeline that builds
+tcc-0.9.26 from the upstream tarball, **without** depending on
+M2-Planet, Mes Scheme, or MesCC at any point. Three scripts drive
+the chain:
+
+- [scripts/stage1-flatten.sh](../scripts/stage1-flatten.sh) — runs on
+ the host (macOS or Linux). Flattens upstream `tcc.c` into a single
+ `tcc.flat.c` bytestream using only the host C preprocessor.
+- [scripts/stage2-alpine.sh](../scripts/stage2-alpine.sh) — runs in an
+ `alpine:latest` container. gcc compiles `tcc.flat.c` to a working
+ `tcc-host`, which then builds mes libc and directly compiles+links
+ the patched real `tcc.c` against mes libc into `tcc-boot0-mes`.
+ **This is the slot our scheme1 cc replaces.**
+- [scripts/stage3-rebuild.sh](../scripts/stage3-rebuild.sh) — runs in a
+ `busybox:musl` container. Uses `tcc-boot0-mes` to drive the
+ live-bootstrap-style chain `boot0 → boot1 → boot2`, ending in a
+ final tcc-0.9.26 built entirely from real (unflattened) sources by
+ a previous-stage tcc.
This is the upstream half of the [CC.md](CC.md) story. Once our
scheme1-hosted compiler can ingest `tcc.flat.c`, it slots in where
-`tcc-host` is today, replacing host gcc as the bootstrap-stage compiler.
+gcc is in stage 2, so the alpine container goes away.
## Inputs
| Path | Contents |
|------|----------|
-| `../lb-work/distfiles/tcc-0.9.26.tar.gz` | tcc-0.9.26-1147-gee75a10c source (janneke's bootstrap-friendly fork; the same artifact live-bootstrap consumes) |
-| `../lb-work/distfiles/mes-0.27.1.tar.gz` | GNU Mes 0.27.1 — used **only** for its bundled minimal libc sources and headers, not for any Scheme runtime |
+| `../lb-work/distfiles/tcc-0.9.26.tar.gz` | tcc-0.9.26-1147-gee75a10c source (janneke's bootstrap-friendly fork; same artifact live-bootstrap consumes) |
+| `../lb-work/distfiles/mes-0.27.1.tar.gz` | GNU Mes 0.27.1 — used **only** for its bundled minimal libc sources and headers, not for any Scheme runtime |
| `../live-bootstrap/steps/tcc-0.9.26/simple-patches/` | Two file-open reorder patches applied before the flatten step |
-| `../mes/include/` | Same Mes headers as those in the tarball — used at flatten time to provide `<stdio.h>` etc. without pulling in host glibc/musl |
+| `../mes/include/` | Same Mes headers as the tarball — used at flatten time so we don't pull in host glibc/musl |
-The two host scripts sit on top of these inputs; they require nothing
+The three scripts sit on top of these inputs; they require nothing
else from the host besides `tar`, `awk`, a host `cc`, and `podman`.
## Pipeline overview
@@ -33,33 +40,37 @@ else from the host besides `tar`, `awk`, a host `cc`, and `podman`.
```
tcc-0.9.26-1147-gee75a10c.tar.gz live-bootstrap source
│
- │ build-tcc-source.sh
+ │ stage1-flatten.sh (host)
│ • unpack
│ • apply 2 simple-patches
│ • host cc -E -nostdinc with mes headers + tcc-mes defines
▼
build/cc-bootstrap/X86_64/tcc.flat.c 608 KB single-file C
│
- │ build-tcc-source.sh --self-host
- │ • inside alpine linux/amd64 + gcc + musl-dev
+ │ stage2-alpine.sh (alpine:latest)
+ │ • apk add gcc musl-dev
│ • gcc -static tcc.flat.c errno-shim.c -> tcc-host
- │ • tcc-host -c tcc.flat.c -> tcc-self.o (self-host check)
+ │ • set up mes-0.27.1 include tree
+ │ • tcc-host compiles mes libc per-file -> libc.a + libtcc1.a
+ │ • tcc-host -static compiles+links real tcc.c -> tcc-boot0-mes
+ │ (mirrors live-bootstrap's tcc-boot0 invocation)
▼
-build/cc-bootstrap/X86_64/{tcc-host, tcc-self.o} bootstrap compiler + self-compile
+build/cc-bootstrap/X86_64/tcc-boot0-mes ~750 KB tcc-0.9.26 ELF
│
- │ build-tcc-real.sh
- │ • unpack mes-0.27.1
- │ • set up include tree with arch -> linux/x86_64 symlink
- │ • tcc-host compiles each mes libc .c file individually
- │ • tcc-host -ar -> libc.a + libtcc1.a
- │ • tcc-host links tcc-self.o + mes libc -> tcc-boot0-mes
+ │ stage3-rebuild.sh (busybox:musl)
+ │ • tcc-boot0-mes rebuilds libc, then compiles real tcc.c -> tcc-boot1
+ │ • tcc-boot1 rebuilds libc, then compiles real tcc.c -> tcc-boot2
▼
-build/cc-bootstrap/X86_64/tcc-boot0-mes 313 KB tcc-0.9.26 ELF
+build/cc-bootstrap/X86_64/tcc-boot2 final tcc-0.9.26
```
+Two containers, three scripts, one host-side step. Stage 1's
+`tcc.flat.c` is a portable artifact; stage 2's `tcc-boot0-mes` plus
+mes libc bits cross into stage 3 via `build/cc-bootstrap/<arch>/stage3-input/`.
+
## Stage 1 — flatten tcc.c into tcc.flat.c
-`scripts/build-tcc-source.sh --arch X86_64`
+`scripts/stage1-flatten.sh --arch X86_64`
Mirrors the live-bootstrap `tcc-mes` invocation
([steps/tcc-0.9.26/pass1.kaem:60–87](../../live-bootstrap/steps/tcc-0.9.26/pass1.kaem))
@@ -68,16 +79,20 @@ in `tcc.c` (which uses `ONE_SOURCE=1` to fold `libtcc.c`, `tcctools.c`,
and the per-arch backends in via `#include`) and inlines all the Mes-
bundled standard headers.
+Stage 1 deliberately stays on the host — it is just text manipulation
+and the `tcc.flat.c` it produces is consumed identically downstream
+regardless of where stage 1 ran.
+
### Sub-steps
1. **Unpack** `tcc-0.9.26.tar.gz` into `build/cc-bootstrap/X86_64/`.
2. **Apply simple-patches**: `remove-fileopen.before/.after` then
- `addback-fileopen.before/.after` against `tcctools.c`. Implemented as
- an `awk` literal-block replacer (live-bootstrap's `simple-patch` is a
- trivial before/after substitution; we don't depend on the binary).
+ `addback-fileopen.before/.after` against `tcctools.c`. Implemented
+ as an `awk` literal-block replacer (live-bootstrap's `simple-patch`
+ is a trivial before/after substitution).
3. **Empty config.h shims**: live-bootstrap creates two empty
- `config.h` files via `catm` (an empty concat). We do the same — one
- in `$TCC_PKG/config.h`, one in `mes-overlay/mes/config.h` for the
+ `config.h` files via `catm`. We do the same — one in
+ `$TCC_PKG/config.h`, one in `mes-overlay/mes/config.h` for the
`<mes/config.h>` reach the Mes stdio.h does.
4. **Host preprocess**: `cc -E -nostdinc` with the Mes headers as the
sole `-I` set, plus the same `-D` set live-bootstrap passes:
@@ -100,152 +115,192 @@ bundled standard headers.
macOS this produces a Mach-O .o; the verify is purely a "does the
source compile" check. Failure here means the flatten step is wrong.
-## Stage 2 — bootstrap tcc-host
-
-`scripts/build-tcc-source.sh --arch X86_64 --self-host`
-
-Implies `--verify`, then drives a Linux container to build a real ELF
-tcc-0.9.26 binary from `tcc.flat.c` using host gcc.
-
-### Why a container
-
-Stage 1 only needs a preprocessor — works on any host. Stage 2 needs:
-- Linux x86_64 toolchain (the resulting binary runs on Linux x86_64)
-- musl-dev for headers/libs that gcc can link against
-
-The macOS dev host has clang but produces Mach-O, not ELF. We use a
-clean `alpine:latest` linux/amd64 container (~5 MB image, +~150 MB for
-gcc + musl-dev on first run, cached afterwards). On macOS arm64 hosts,
-podman runs this under QEMU x86_64 emulation — slow but functional.
-
-### errno shim
-
-`tcc.flat.c` came out of the Mes-headers preprocess, so it expects
-`extern int errno;` (Mes's plain-global form). musl provides errno
-through `__errno_location()` instead, so the link fails. A one-line
-`errno-shim.c` (`int errno;`) fills the gap.
-
-```
-gcc -w -static -no-pie -o tcc-host tcc.flat.c errno-shim.c
-```
-
-### Smoke tests
+## Stage 2 — Alpine container builds tcc-boot0-mes
-After build:
+`scripts/stage2-alpine.sh --arch X86_64`
-- `tcc-host -version` → `tcc version 0.9.26 (x86_64 Linux)` ✓
-- `tcc-host -c -o tcc-self.o tcc.flat.c` → 485 KB object file ✓
- This is **the self-host check**: tcc compiling its own preprocessed
- source. `tcc-self.o` is the object that Stage 3 links against mes
- libc.
+This is the **stand-in slot for our future scheme CC**. Today, native
+gcc plays the role; tomorrow our scheme1-hosted C compiler does the
+same job — take `tcc.flat.c`, produce a working tcc, then use that
+tcc to build mes libc and compile+link a static `tcc-boot0-mes`
+against it.
-## Stage 3 — link a real tcc against mes libc
+### Why Alpine (musl, not glibc)
-`scripts/build-tcc-real.sh`
+mes headers declare `errno` as a plain global; glibc declares it as a
+TLS symbol. Linking `tcc.flat.c` against glibc's libc.a fails with a
+non-TLS / TLS clash on the errno definition, and patching tcc.flat.c
+to use a TLS form would mean injecting a transformation into stage 1
+that exists only to satisfy stage 2's host libc choice.
-Drives the same alpine container the rest of the way. Produces
-`tcc-boot0-mes`, the equivalent of live-bootstrap's tcc-boot0 stage —
-but built without ever invoking mescc.
+musl exposes errno only through `__errno_location()`, so a one-line
+`int errno;` shim is the sole storage and links cleanly. Alpine
+ships musl natively, so we use it.
-### Sub-steps
+The output binary itself (`tcc-boot0-mes`) is statically linked
+against **mes libc**, not musl — the alpine-side gcc only links
+`tcc-host`, the transient compiler used inside the container.
-1. **Unpack `mes-0.27.1`** alongside `tcc-0.9.26`. Used purely for
- `lib/**.c` (libc sources) and `include/**.h` (libc headers).
-2. **Sanitize the include tree**:
+### errno shim
+`tcc.flat.c` came out of stage 1's mes-headers preprocess, so it
+expects `extern int errno;` (Mes's plain-global form). musl provides
+errno only via `__errno_location()`, so the gcc link of tcc-host
+needs a definition. A one-line `errno-shim.c` (`int errno;`) is it.
+Purely a stage-2 concern — `tcc-boot0-mes` itself links against mes
+libc, where errno is the plain global mes expects.
+
+### Sub-steps (inside the container)
+
+1. **`apk add gcc musl-dev`**.
+2. **`gcc -static -no-pie -o tcc-host tcc.flat.c errno-shim.c`**.
+ This is the only step that exercises the host toolchain. It
+ doubles as a validation of stage 1: if `tcc.flat.c` is malformed,
+ it fails here. ~700 KB output, musl-linked.
+3. **Set up mes include tree**:
```
/tmp/mes-inc/ <- copy of mes/include/
/tmp/mes-inc/arch -> linux/x86_64 <- symlink we create
```
+ (Issue §1 workaround — without the symlink, tcc 0.9.26 SEGVs on
+ `#include <arch/syscall.h>` rather than reporting cleanly.)
+4. **Compile each mes libc .c file individually** with tcc-host.
+ (Issue §2 workaround — concatenated TU SEGVs at ~22+ files.)
+5. **`tcc-host -ar`** the .o set into `/lib/libc.a` (~299 KB), build
+ `crt1.o`, build `libtcc1.a`, install at the baked-in paths
+ (`CONFIG_TCC_CRTPREFIX="/lib"`, `CONFIG_TCCDIR="/lib/tcc"`,
+ `CONFIG_TCC_SYSINCLUDEPATHS="/include/mes"`).
+6. **Compile+link real tcc.c into tcc-boot0-mes**. tcc-host operates
+ on the patched tree from `$WORK/$TCC_PKG/` (stage 1 left it there
+ in place), with the same flag set live-bootstrap's `pass1.kaem`
+ passes to its own `tcc-mes -static -o tcc-boot0 ... tcc.c` call:
+ ```
+ tcc-host -g -static -o tcc-boot0-mes \
+ -D BOOTSTRAP=1 -D HAVE_FLOAT=1 -D HAVE_BITFIELD=1 \
+ -D HAVE_LONG_LONG=1 -D HAVE_SETJMP=1 \
+ -D TCC_TARGET_X86_64=1 ...CONFIG_TCC_* defines... \
+ -D ONE_SOURCE=1 \
+ -I . -I /include/mes -L . -L /lib \
+ tcc.c
+ ```
+ No intermediate `tcc-self.o`. An earlier iteration produced one as
+ a "tcc-host self-compile" check, then linked it against mes libc
+ in a separate step — but the standalone `-static` link exposed a
+ tcc 0.9.26 codegen bug, and the separate pass adds nothing the
+ live-bootstrap-style direct invocation doesn't already validate.
+ ~750 KB output.
+7. **Stage out** mes libc + libtcc1 + crt1.o + headers into
+ `build/cc-bootstrap/X86_64/stage3-input/`, so stage 3 can mount
+ them without re-running stage 2.
+8. **Smoke test**: `tcc-boot0-mes -version`. **Expected to SEGV under
+ QEMU on macOS arm64** (Issue §3); native x86_64 needed to verify
+ cleanly.
+
+## Stage 3 — busybox container drives boot1 / boot2
+
+`scripts/stage3-rebuild.sh --arch X86_64`
+
+Mirrors live-bootstrap's pass1.kaem chain, but starting from
+`tcc-boot0-mes` (= live-bootstrap's `tcc-boot0` slot) since we
+skipped MesCC / `tcc-mes` entirely. Each pass uses the previous
+stage's tcc to rebuild mes libc and recompile the **real**,
+unflattened tcc-0.9.26 sources from the patched tree stage 1
+produced.
+
+### Why busybox
+
+This stage is "pure recompile" work — no host-toolchain dependency.
+busybox provides everything we need (sh, tar, awk, basic file
+utilities) and tcc-boot0-mes is its own preprocessor + assembler +
+linker + ar. The container is ~5 MB; nothing else gets installed.
+
+### Sub-steps (inside the container, per pass)
+
+For each `cc ∈ {tcc-boot0-mes, tcc-boot1}`:
+
+1. **Rebuild libc** from mes sources using `cc`:
+ - `cc -c crt1.c -> /lib/crt1.o`
+ - `cc -c libtcc1.c`, `cc -ar -> /lib/tcc/libtcc1.a`
+ - per-file compile of mes libc, `cc -ar -> /lib/libc.a`
+2. **Compile real tcc.c** using `cc`:
+ ```
+ cc -g -static -o tcc-boot{1,2} \
+ -D BOOTSTRAP=1 -D HAVE_FLOAT=1 -D HAVE_BITFIELD=1 \
+ -D HAVE_LONG_LONG=1 -D HAVE_SETJMP=1 \
+ -D TCC_TARGET_X86_64=1 ...CONFIG_TCC_* defines... \
+ -D ONE_SOURCE=1 \
+ -I . -I /include/mes -L . -L /lib \
+ tcc.c
+ ```
+3. **Verify**: `tcc-boot{1,2} -version`.
- The Mes headers do `#include <arch/syscall.h>`. Live-bootstrap must
- set up an equivalent symlink internally; the tarball doesn't ship
- one. Without this symlink, tcc-host segfaults rather than cleanly
- reporting the missing include (see Issues §1).
-
-3. **Compile each mes libc .c file individually** with tcc-host. The
- live-bootstrap kaem script `catm`s ~250 files into a single
- `unified-libc.c` and compiles that as one TU. tcc-host crashes on
- that approach (see Issues §2). Compiling each as a separate TU and
- `ar`-ing the results sidesteps the bug and is what tcc was designed
- to do anyway.
-
-4. **`tcc-host -ar` the .o set into `libc.a`** (~299 KB).
-
-5. **Build crt1.o** from `lib/linux/x86_64-mes-gcc/crt1.c` (mes's hand-
- rolled `_start` that invokes `main` directly via inline asm). On
- x86_64, `crtn.o` and `crti.o` are empty files (live-bootstrap does
- the same).
-
-6. **Build libtcc1.a** from `lib/libtcc1.c` (tcc's helper runtime).
-
-7. **Install at the baked-in paths**: `tcc-host` was built with
- `CONFIG_TCC_CRTPREFIX="/lib"`, `CONFIG_TCCDIR="/lib/tcc"`,
- `CONFIG_TCC_SYSINCLUDEPATHS="/include/mes"`. The script populates
- those locations inside the container so tcc-host can find its
- pieces without `-B`/`-L` overrides.
-
-8. **Link**: `tcc-host -static -o tcc-boot0-mes tcc-self.o`.
+The build-time defines here are a strict superset of stage 1's:
+`HAVE_FLOAT`, `HAVE_BITFIELD`, `HAVE_SETJMP` are added because the
+real builds enable code paths the flatten deliberately omits.
- tcc-host walks `tcc-self.o`'s symbol references against `crt1.o`,
- `libc.a`, `libtcc1.a`, emits a static ELF.
+### Status today
-Result: `build/cc-bootstrap/X86_64/tcc-boot0-mes` — 313 KB. Roughly
-half the size of the earlier musl-linked variant (`tcc-boot0`, 764 KB),
-which tracks: mes libc is stripped down, musl is full.
+Stage 3 cannot complete on macOS arm64 hosts — Issue §3 (tcc-boot0-mes
+SEGVs at startup under QEMU x86_64) blocks the very first
+`tcc-boot0-mes -version`. The script is correct; it will run
+end-to-end on native x86_64 hardware, or once a tcc 0.9.28rc backport
+patches the prologue/_DYNAMIC issues. The failure mode is clean: the
+script aborts at the first `-version` smoke and reports the blocker.
## Relation to live-bootstrap
```
-live-bootstrap path: mescc → tcc-mes → tcc-boot0 → tcc-boot1 → tcc-boot2 → tcc → tcc-0.9.27 → make 3.82 → ...
-our path: gcc → tcc-host → tcc-boot0-mes
- (= live-bootstrap's tcc-boot0 equivalent)
+live-bootstrap path: mescc → tcc-mes → tcc-boot0 → tcc-boot1 → tcc-boot2 → tcc → tcc-0.9.27 → ...
+our path: gcc → tcc-host → tcc-boot0-mes → tcc-boot1 → tcc-boot2
+ └──── stage 2 (alpine) ────┘ └──── stage 3 (busybox) ────┘
```
`tcc-boot0-mes` is the slot-equivalent of live-bootstrap's `tcc-boot0`.
-The downstream chain (rebuild libc → tcc-boot1 → rebuild libc →
-tcc-boot2 → tcc → tcc-0.9.27 → make 3.82 → real builds) is a series of
-mechanical recompiles the existing pass1.kaem already orchestrates.
-Picking up from `tcc-boot0-mes` requires only that it run correctly
-(see Issues §3).
+Stages 3 stops at `tcc-boot2` ("final 0.9.26" per the project goal);
+the further hop to tcc-0.9.27 lives outside this doc.
## What this unlocks for the scheme1 cc
-The ultimate goal: replace `tcc-host` in Stage 2 with our scheme1-hosted
-C compiler. The interface is fixed:
+The interface for the slot scheme CC will fill is fixed:
-- **Input**: `tcc.flat.c` produced by Stage 1.
-- **Output**: an ELF binary equivalent to `tcc-host` (or an object file
- equivalent to `tcc-self.o`, which gcc would then link).
+- **Input**: `tcc.flat.c` produced by stage 1.
+- **Output**: a working ELF tcc-host capable of compiling mes libc
+ and compile+linking the patched real `tcc.c` into `tcc-boot0-mes`.
-`tcc.flat.c` is a known-good, host-cc-validated artifact ready for
-the scheme1 cc to chew on incrementally. Track progress against it.
+Stage 2 collapses to "scheme1-cc compiles tcc.flat.c and the mes libc
+sources inside a busybox container." The alpine container goes away;
+busybox + scheme1-cc covers everything from stage 1's output through
+stage 3's `tcc-boot2`. `tcc.flat.c` is a known-good, host-cc-validated
+artifact ready for scheme1-cc to chew on incrementally.
## Reproducibility
```
-scripts/build-tcc-source.sh --arch X86_64 --self-host
-scripts/build-tcc-real.sh
+scripts/stage1-flatten.sh --arch X86_64
+scripts/stage2-alpine.sh --arch X86_64
+scripts/stage3-rebuild.sh --arch X86_64 # blocked on Issue §3 today
```
-Three artifacts land in `build/cc-bootstrap/X86_64/`:
+Artifacts in `build/cc-bootstrap/X86_64/`:
-| File | Size | Built by | What it is |
-|-------------------|--------|------------------|-----------------------------------------|
-| `tcc.flat.c` | 608 KB | host cc | flattened single-source tcc-0.9.26 |
-| `tcc-host` | 704 KB | host gcc + musl | first-stage tcc, runs natively on Linux |
-| `tcc-self.o` | 485 KB | tcc-host | tcc-host compiling its own source |
-| `tcc-boot0-mes` | 313 KB | tcc-host + mes lib | real tcc-0.9.26, mes-libc-linked |
+| File | Stage | Size | Built by | What it is |
+|-------------------|-------|---------|-----------------------|-------------------------------------------|
+| `tcc.flat.c` | 1 | 608 KB | host cc | flattened single-source tcc-0.9.26 |
+| `tcc-host` | 2 | ~700 KB | alpine gcc + musl | transient stage-2 compiler |
+| `tcc-boot0-mes` | 2 | ~750 KB | tcc-host + mes libc | static tcc-0.9.26, mes-libc-linked |
+| `stage3-input/` | 2 | — | stage 2 | staged mes libc + libtcc1 + headers |
+| `tcc-boot1` | 3 | ~750 KB | tcc-boot0-mes | first rebuild from real tcc.c |
+| `tcc-boot2` | 3 | ~750 KB | tcc-boot1 | final 0.9.26 |
-`build/` is in `.gitignore`; nothing is tracked outside of the scripts
+`build/` is in `.gitignore`; nothing tracked outside the scripts
themselves.
## Issues / bugs
-Found while bringing this pipeline up. All four are real, none are
-ours; (1) and (2) are bugs in tcc 0.9.26 that we work around in the
-scripts; (3) and (4) are open and need either native x86_64 testing or
-a tcc backport.
+Found while bringing this pipeline up. All three are real, none are
+ours; (1) and (2) are bugs in tcc 0.9.26 that the scripts work
+around; (3) is open and needs either native x86_64 testing or a
+tcc backport.
### 1. tcc 0.9.26 SEGV on missing include
@@ -254,7 +309,7 @@ it segfaults instead of reporting the error. Discovered when
`mes/include/dirent.h` does `#include <arch/syscall.h>`; mes/include
has no `arch/` directory by default.
-**Workaround**: in `build-tcc-real.sh`, create
+**Workaround**: in `stage2-alpine.sh`, create
`/tmp/mes-inc/arch -> linux/x86_64` before invoking tcc-host. The
live-bootstrap build presumably sets up the same symlink somewhere we
haven't traced — possibly via a configure step, or via the
@@ -276,62 +331,60 @@ hash chain, or similar — that overflows or hits a corrupted state
when the TU grows large enough.
**Workaround**: compile each `.c` separately, then `ar` together.
-`build-tcc-real.sh` does this for all 258 mes libc .c files.
-Bonus: avoids ~250 redundant header re-parses, faster overall.
+Both `stage2-alpine.sh` and `stage3-rebuild.sh` do this for all 258
+mes libc .c files. Bonus: avoids ~250 redundant header re-parses,
+faster overall.
-### 3. tcc 0.9.26 emits no `_DYNAMIC` stub for static binaries
+**Confirmed in canonical live-bootstrap.** The
+[`scripts/diag-livebootstrap-qemu.sh`](../scripts/diag-livebootstrap-qemu.sh)
+diagnostic runs upstream live-bootstrap's amd64 pass1 chain inside
+the same busybox + linux/amd64 QEMU we use, and its mescc-built
+`tcc-mes` SEGVs at exactly this step (`tcc-mes -c unified-libc.c`
+with `assert fail: 0` then SIGSEGV). The per-file workaround is
+load-bearing for any tcc-0.9.26-on-QEMU build, not specific to our
+path.
-When linking a tcc-host-output ELF against musl's `crt1.o` + `libc.a`
-on alpine, the resulting binary segfaults because tcc 0.9.26 doesn't
-emit a `_DYNAMIC` symbol; the `lea _DYNAMIC(%rip), %rsi` in musl's
-`_start` resolves to address 0, and musl's `__libc_start_main`
-dereferences rsi.
+### 3. tcc-boot0-mes segfaults at startup under QEMU x86_64
+
+`./tcc-boot0-mes -version` segfaults under `podman run --platform
+linux/amd64` on macOS arm64.
-Side-by-side disassembly of the same hello-world built with alpine's
-prebuilt tcc 0.9.28rc and our tcc-host:
+**This is specific to our build path, not a generic QEMU issue.** The
+diagnostic in
+[`scripts/diag-livebootstrap-qemu.sh`](../scripts/diag-livebootstrap-qemu.sh)
+runs upstream live-bootstrap's amd64 pass1 chain inside the same
+busybox + linux/amd64 QEMU. Its mescc-built `tcc-mes` (also
+mes-libc-linked, also tcc-0.9.26) runs `tcc-mes -version` cleanly:
```
-alpine 0.9.28rc: lea 0x9bb(%rip),%rsi # 0x4014a0 ← real stub
-ours 0.9.26: lea -0x4000bd(%rip),%rsi # 0x0 ← NULL
++> tcc-mes -version
+tcc version 0.9.26 (x86_64 Linux)
```
-This is why the **musl-linked** tcc-boot0 doesn't run. The
-**mes-libc-linked** `tcc-boot0-mes` should sidestep it because mes's
-`crt1.c` calls `main` directly, not via `__libc_start_main`, so rsi
-is never dereferenced.
-
-A patch to emit a `_DYNAMIC` stub would be small. tcc 0.9.28rc has
-the fix; backporting it to the flat tcc.c is a candidate next step.
-
-### 4. tcc-boot0-mes segfaults at startup under QEMU x86_64
-
-`./tcc-boot0-mes -version` segfaults under `podman run --platform
-linux/amd64` on macOS arm64. **Whether this works on native x86_64
-hardware is untested.**
-
-Possibilities:
-
-a) tcc 0.9.26 codegen bug in the function prologue tcc emits for
- `_start` (mes's `crt1.c` declares `_start` as a regular C
- function with inline asm; tcc adds a standard prologue, and a
- bug in that prologue under specific conditions could mismatch
- mes's expectations of `rbp`).
-
-b) A QEMU-specific interaction with mes's hand-rolled `_start`
- inline asm (which assumes the kernel-provided process-entry
- stack layout). QEMU's process-emulation user-mode does set
- that up, but corner cases exist.
-
-(a) is more likely because the same QEMU runs alpine's prebuilt
-tcc 0.9.28rc — and tcc-built binaries from that — without any
-trouble. So whatever's wrong is specific to **our** binary's
-contents.
-
-**Path to verify**: run tcc-boot0-mes on native x86_64. Failure
-there confirms (a) and points at a tcc-source patch.
-**Path to fix**: backport tcc 0.9.28rc's `_DYNAMIC` and prologue
-fixes (probably the same patch series that fixes Issue 3).
-
-Once tcc-boot0-mes runs, the rest of the chain
-(rebuild-libc → tcc-boot1 → tcc-boot2 → tcc → tcc-0.9.27 → make 3.82
-→ ...) is what live-bootstrap's pass1.kaem already does.
+So QEMU correctly executes mescc-built tcc-0.9.26 ELFs at startup. Our
+`tcc-boot0-mes`, which differs in being produced by `tcc-host`
+(itself a gcc-compile of `tcc.flat.c`) rather than by mescc, hits a
+codegen path that mescc avoids. Most likely candidates: function
+prologue around `_start` (mes's `crt1.c` declares `_start` as a
+regular C function with inline asm; tcc-host's prologue may not match
+what mes's runtime expects), or a `_DYNAMIC` symbol gap that tcc
+0.9.28rc's patch series fixes.
+
+A related historical observation: when an earlier iteration of stage 2
+tried linking a tcc-host-output ELF against musl's `crt1.o` directly,
+that binary also segfaulted at startup because tcc 0.9.26 doesn't
+emit a `_DYNAMIC` symbol musl's `_start` expects. That path is
+retired, but the same underlying tcc codegen gap is a plausible
+cousin of the SEGV here — tcc 0.9.28rc's patch series likely fixes
+both.
+
+**Path to fix**: backport tcc 0.9.28rc's prologue / `_DYNAMIC` fixes
+onto `tcc.flat.c` (or onto the patched real tcc tree stage 2 builds
+from), then rerun stage 2. Optionally compare disassembly of our
+`tcc-boot0-mes` against live-bootstrap's `tcc-boot0` (output of the
+same diagnostic chain at a later step) to localize the exact
+divergence.
+
+Once tcc-boot0-mes runs, stage 3 is unblocked: the `tcc-boot1` /
+`tcc-boot2` rebuilds mirror what live-bootstrap's pass1.kaem already
+does, and the script is in place.
diff --git a/scripts/build-tcc-real.sh b/scripts/build-tcc-real.sh
@@ -1,188 +0,0 @@
-#!/bin/sh
-## scripts/build-tcc-real.sh — build a real tcc-0.9.26 from our chain,
-## linked against mes libc.
-##
-## Pipeline (all inside a linux/amd64 alpine container):
-## 1. unpack mes-0.27.1 sources
-## 2. set up include tree with `arch -> linux/<arch>` symlink so
-## mes headers' `#include <arch/syscall.h>` resolves
-## (without this, tcc 0.9.26 SEGVs on missing include rather than
-## reporting cleanly — bug found while bringing this up)
-## 3. compile each mes libc .c file individually with tcc-host
-## (concatenating them all into unified-libc.c also hits a tcc 0.9.26
-## bug — accumulated symbol-table state segfaults around 22+ files
-## when one of them uses inline asm)
-## 4. tcc-host -ar the .o set into libc.a
-## 5. compile crt1.c -> crt1.o; libtcc1.c -> libtcc1.a
-## 6. tcc-host -static linking tcc-self.o (already built by
-## build-tcc-source.sh --self-host) against the mes libc
-## -> tcc-boot0-mes
-##
-## Status: produces tcc-boot0-mes successfully. The binary itself
-## segfaults at startup under QEMU x86_64 emulation; native x86_64
-## testing is needed to confirm whether this is a tcc-0.9.26 codegen
-## bug exposed by mes's hand-rolled crt1, or a QEMU-specific issue.
-##
-## Pre-condition:
-## build/cc-bootstrap/X86_64/tcc-host (build-tcc-source.sh --self-host)
-## build/cc-bootstrap/X86_64/tcc-self.o
-##
-## Usage:
-## scripts/build-tcc-real.sh
-
-set -eu
-
-ROOT=$(cd "$(dirname "$0")/.." && pwd)
-WORK=$ROOT/build/cc-bootstrap/X86_64
-MES_TAR=$ROOT/../lb-work/distfiles/mes-0.27.1.tar.gz
-MES_PKG=mes-0.27.1
-
-[ -r "$MES_TAR" ] || { echo "missing $MES_TAR" >&2; exit 1; }
-
-if [ ! -x "$WORK/tcc-host" ] || [ ! -r "$WORK/tcc-self.o" ]; then
- echo "tcc-host or tcc-self.o missing — running build-tcc-source.sh --self-host"
- "$ROOT/scripts/build-tcc-source.sh" --arch X86_64 --self-host
-fi
-
-MES_SRC=$WORK/$MES_PKG
-if [ ! -d "$MES_SRC" ]; then
- tar -xzf "$MES_TAR" -C "$WORK"
-fi
-mkdir -p "$MES_SRC/include/mes"
-: > "$MES_SRC/include/mes/config.h"
-
-echo "running build inside linux/amd64 alpine container (a few minutes under QEMU)"
-
-podman run --rm -i --platform linux/amd64 \
- -v "$ROOT":/work -w /work alpine:latest sh -s <<'CONTAINER_SCRIPT'
-set -eu
-WORK=/work/build/cc-bootstrap/X86_64
-TCC=$WORK/tcc-host
-MES_SRC=$WORK/mes-0.27.1
-MES_ARCH=x86_64
-
-# --- (2) build a sanitized include tree ------------------------------
-INC=/tmp/mes-inc
-mkdir -p $INC
-cp -r $MES_SRC/include/. $INC/
-ln -sfn linux/$MES_ARCH $INC/arch
-
-# --- (3) compile each mes libc .c file individually ------------------
-mkdir -p /tmp/objs
-cd $MES_SRC/lib
-
-# The canonical catm list from steps/tcc-0.9.26/pass1.kaem:99 (X86_64).
-# We compile these as separate TUs to dodge tcc-host's accumulator bug.
-ALL_FILES="ctype/isalnum.c ctype/isalpha.c ctype/isascii.c ctype/iscntrl.c \
-ctype/isdigit.c ctype/isgraph.c ctype/islower.c ctype/isnumber.c \
-ctype/isprint.c ctype/ispunct.c ctype/isspace.c ctype/isupper.c \
-ctype/isxdigit.c ctype/tolower.c ctype/toupper.c \
-dirent/closedir.c dirent/__getdirentries.c dirent/opendir.c \
-linux/readdir.c linux/access.c linux/brk.c linux/chdir.c linux/chmod.c \
-linux/clock_gettime.c linux/close.c linux/dup2.c linux/dup.c linux/execve.c \
-linux/fcntl.c linux/fork.c linux/fsync.c linux/fstat.c linux/_getcwd.c \
-linux/getdents.c linux/getegid.c linux/geteuid.c linux/getgid.c linux/getpid.c \
-linux/getppid.c linux/getrusage.c linux/gettimeofday.c linux/getuid.c \
-linux/ioctl.c linux/ioctl3.c linux/kill.c linux/link.c linux/lseek.c \
-linux/lstat.c linux/malloc.c linux/mkdir.c linux/mknod.c linux/nanosleep.c \
-linux/_open3.c linux/pipe.c linux/_read.c linux/readlink.c linux/rename.c \
-linux/rmdir.c linux/setgid.c linux/settimer.c linux/setuid.c linux/signal.c \
-linux/sigprogmask.c linux/symlink.c linux/stat.c linux/time.c linux/unlink.c \
-linux/waitpid.c linux/wait4.c \
-linux/${MES_ARCH}-mes-gcc/_exit.c linux/${MES_ARCH}-mes-gcc/syscall.c \
-linux/${MES_ARCH}-mes-gcc/_write.c \
-math/ceil.c math/fabs.c math/floor.c \
-mes/abtod.c mes/abtol.c mes/__assert_fail.c mes/assert_msg.c \
-mes/__buffered_read.c mes/__init_io.c mes/cast.c mes/dtoab.c \
-mes/eputc.c mes/eputs.c mes/fdgetc.c mes/fdgets.c mes/fdputc.c mes/fdputs.c \
-mes/fdungetc.c mes/globals.c mes/itoa.c mes/ltoab.c mes/ltoa.c \
-mes/__mes_debug.c mes/mes_open.c mes/ntoab.c mes/oputc.c mes/oputs.c \
-mes/search-path.c mes/ultoa.c mes/utoa.c \
-posix/alarm.c posix/buffered-read.c posix/execl.c posix/execlp.c \
-posix/execv.c posix/execvp.c posix/getcwd.c posix/getenv.c posix/isatty.c \
-posix/mktemp.c posix/open.c posix/pathconf.c posix/raise.c posix/sbrk.c \
-posix/setenv.c posix/sleep.c posix/unsetenv.c posix/wait.c posix/write.c \
-stdio/clearerr.c stdio/fclose.c stdio/fdopen.c stdio/feof.c stdio/ferror.c \
-stdio/fflush.c stdio/fgetc.c stdio/fgets.c stdio/fileno.c stdio/fopen.c \
-stdio/fprintf.c stdio/fputc.c stdio/fputs.c stdio/fread.c stdio/freopen.c \
-stdio/fscanf.c stdio/fseek.c stdio/ftell.c stdio/fwrite.c stdio/getc.c \
-stdio/getchar.c stdio/perror.c stdio/printf.c stdio/putc.c stdio/putchar.c \
-stdio/remove.c stdio/snprintf.c stdio/sprintf.c stdio/sscanf.c stdio/ungetc.c \
-stdio/vfprintf.c stdio/vfscanf.c stdio/vprintf.c stdio/vsnprintf.c \
-stdio/vsprintf.c stdio/vsscanf.c \
-stdlib/abort.c stdlib/abs.c stdlib/alloca.c stdlib/atexit.c stdlib/atof.c \
-stdlib/atoi.c stdlib/atol.c stdlib/calloc.c stdlib/__exit.c stdlib/exit.c \
-stdlib/free.c stdlib/mbstowcs.c stdlib/puts.c stdlib/qsort.c stdlib/realloc.c \
-stdlib/strtod.c stdlib/strtof.c stdlib/strtol.c stdlib/strtold.c \
-stdlib/strtoll.c stdlib/strtoul.c stdlib/strtoull.c \
-string/bcmp.c string/bcopy.c string/bzero.c string/index.c string/memchr.c \
-string/memcmp.c string/memcpy.c string/memmem.c string/memmove.c string/memset.c \
-string/rindex.c string/strcat.c string/strchr.c string/strcmp.c string/strcpy.c \
-string/strcspn.c string/strdup.c string/strerror.c string/strlen.c \
-string/strlwr.c string/strncat.c string/strncmp.c string/strncpy.c \
-string/strpbrk.c string/strrchr.c string/strspn.c string/strstr.c string/strupr.c \
-stub/atan2.c stub/bsearch.c stub/chown.c stub/__cleanup.c stub/cos.c \
-stub/ctime.c stub/exp.c stub/fpurge.c stub/freadahead.c stub/frexp.c \
-stub/getgrgid.c stub/getgrnam.c stub/getlogin.c stub/getpgid.c stub/getpgrp.c \
-stub/getpwnam.c stub/getpwuid.c stub/gmtime.c stub/ldexp.c stub/localtime.c \
-stub/log.c stub/mktime.c stub/modf.c stub/mprotect.c stub/pclose.c \
-stub/popen.c stub/pow.c stub/putenv.c stub/rand.c stub/realpath.c stub/rewind.c \
-stub/setbuf.c stub/setgrent.c stub/setlocale.c stub/setvbuf.c stub/sigaction.c \
-stub/sigaddset.c stub/sigblock.c stub/sigdelset.c stub/sigemptyset.c \
-stub/sigsetmask.c stub/sin.c stub/sys_siglist.c stub/system.c stub/sqrt.c \
-stub/strftime.c stub/times.c stub/ttyname.c stub/umask.c stub/utime.c \
-${MES_ARCH}-mes-gcc/setjmp.c"
-
-OBJS=
-n_compiled=0
-n_failed=0
-for f in $ALL_FILES; do
- name=$(echo "$f" | tr / _)
- o=/tmp/objs/${name%.c}.o
- if "$TCC" -c -D HAVE_CONFIG_H=1 -I "$INC" -I "$INC/linux/$MES_ARCH" \
- -o "$o" "$f" 2>/dev/null; then
- OBJS="$OBJS $o"
- n_compiled=$((n_compiled+1))
- else
- echo "compile failed: $f" >&2
- n_failed=$((n_failed+1))
- fi
-done
-echo "compiled $n_compiled libc .o files (failed: $n_failed)"
-[ "$n_failed" -eq 0 ] || { echo "abort: some libc files failed" >&2; exit 1; }
-
-# --- (4) ar -> libc.a -----------------------------------------------
-mkdir -p /lib/tcc /include/mes
-"$TCC" -ar cr /lib/libc.a $OBJS
-
-# --- (5) crt1.o, libtcc1.a -------------------------------------------
-"$TCC" -c -D HAVE_CONFIG_H=1 -I "$INC" -I "$INC/linux/$MES_ARCH" \
- -o /lib/crt1.o "linux/$MES_ARCH-mes-gcc/crt1.c"
-: > /lib/crtn.o
-: > /lib/crti.o
-
-"$TCC" -c -D HAVE_CONFIG_H=1 -D HAVE_LONG_LONG=1 -D HAVE_FLOAT=1 \
- -I "$INC" -I "$INC/linux/$MES_ARCH" \
- -o /tmp/libtcc1.o libtcc1.c
-"$TCC" -ar cr /lib/tcc/libtcc1.a /tmp/libtcc1.o
-
-# Install headers at the path tcc-host has baked in (CONFIG_TCC_SYSINCLUDEPATHS)
-cp -r "$INC/." /include/mes/
-
-ls -la /lib/crt1.o /lib/libc.a /lib/tcc/libtcc1.a
-
-# --- (6) link tcc-self.o against mes libc -> tcc-boot0-mes -----------
-echo
-echo "--- linking tcc-self.o against mes libc ---"
-"$TCC" -static -o "$WORK/tcc-boot0-mes" "$WORK/tcc-self.o"
-ls -la "$WORK/tcc-boot0-mes"
-
-echo
-echo "--- tcc-boot0-mes -version (expected: may segfault under QEMU) ---"
-rc=0; "$WORK/tcc-boot0-mes" -version 2>&1 || rc=$?
-echo "exit=$rc"
-CONTAINER_SCRIPT
-
-echo
-echo "artifacts:"
-ls -la "$WORK/tcc-boot0-mes" 2>/dev/null || echo "(no tcc-boot0-mes)"
diff --git a/scripts/build-tcc-source.sh b/scripts/build-tcc-source.sh
@@ -1,214 +0,0 @@
-#!/bin/sh
-## scripts/build-tcc-source.sh — produce a single flattened tcc.c that our
-## scheme1-hosted C compiler can consume.
-##
-## Mirrors the live-bootstrap tcc-mes invocation (steps/tcc-0.9.26/pass1.kaem)
-## minus the actual compile step. The output is one C source bytestream with
-## all #include "X.c" project files inlined and all standard headers expanded
-## from the Mes-bundled minimal libc headers. No mes/mescc binary is involved
-## — only the host preprocessor.
-##
-## Stages:
-## 1. unpack tcc-0.9.26-1147-gee75a10c.tar.gz
-## 2. apply live-bootstrap simple-patches (file-open reordering in tcctools.c)
-## 3. host-cc -E with the tcc-mes defines + -nostdinc + Mes header tree
-## 4. emit build/cc-bootstrap/<arch>/tcc.flat.c
-## 5. (optional) compile tcc.flat.c with host cc to verify well-formedness
-##
-## Usage:
-## scripts/build-tcc-source.sh [--arch <arch>] [--verify] [--self-host]
-##
-## --arch: X86_64 | I386 | RISCV64. Default X86_64 (live-bootstrap
-## reference path).
-## --verify: compile tcc.flat.c with host cc to confirm the source is
-## valid C. On macOS, produces a Mach-O .o (won't run, but
-## proves syntactic correctness).
-## --self-host: full chain in a Linux x86_64 container (alpine + gcc):
-## 1. build tcc-host (a real tcc binary) from tcc.flat.c
-## 2. run tcc-host -version
-## 3. use tcc-host to recompile tcc.flat.c (self-host check)
-## Requires podman and pulls alpine:latest on first run.
-## Implies --verify.
-
-set -eu
-
-# --- arg parse --------------------------------------------------------
-ARCH=X86_64
-VERIFY=0
-SELF_HOST=0
-while [ $# -gt 0 ]; do
- case "$1" in
- --arch) ARCH=$2; shift 2 ;;
- --verify) VERIFY=1; shift ;;
- --self-host) SELF_HOST=1; VERIFY=1; shift ;;
- -h|--help) sed -n 's/^## \{0,1\}//p' "$0"; exit 0 ;;
- *) echo "unknown arg: $1" >&2; exit 2 ;;
- esac
-done
-
-# Map ARCH to (mes-arch, have-long-long).
-case "$ARCH" in
- X86_64) MES_ARCH=x86_64; HAVE_LL=1 ;;
- I386) MES_ARCH=x86; HAVE_LL=0 ;;
- RISCV64) MES_ARCH=riscv64; HAVE_LL=1 ;;
- AARCH64) echo "AARCH64 not in live-bootstrap; tcc.c lacks an arm64-gen.c we can use here" >&2; exit 2 ;;
- *) echo "unknown ARCH: $ARCH" >&2; exit 2 ;;
-esac
-
-# --- paths ------------------------------------------------------------
-ROOT=$(cd "$(dirname "$0")/.." && pwd)
-WORK=$ROOT/build/cc-bootstrap/$ARCH
-DISTFILES=$ROOT/../lb-work/distfiles
-LB_PATCHES=$ROOT/../live-bootstrap/steps/tcc-0.9.26/simple-patches
-MES_INCLUDE=$ROOT/../mes/include
-MES_INCLUDE_LINUX=$MES_INCLUDE/linux/$MES_ARCH
-
-TCC_TAR=$DISTFILES/tcc-0.9.26.tar.gz
-TCC_PKG=tcc-0.9.26-1147-gee75a10c
-
-[ -r "$TCC_TAR" ] || { echo "missing $TCC_TAR" >&2; exit 1; }
-[ -d "$LB_PATCHES" ] || { echo "missing $LB_PATCHES" >&2; exit 1; }
-[ -d "$MES_INCLUDE" ] || { echo "missing $MES_INCLUDE" >&2; exit 1; }
-[ -d "$MES_INCLUDE_LINUX" ] || { echo "missing $MES_INCLUDE_LINUX" >&2; exit 1; }
-
-# --- (1) unpack -------------------------------------------------------
-mkdir -p "$WORK"
-rm -rf "$WORK/$TCC_PKG"
-tar -xzf "$TCC_TAR" -C "$WORK"
-
-SRC=$WORK/$TCC_PKG
-
-# --- (2) simple-patches ----------------------------------------------
-# Both patches edit tcctools.c. The pair (remove-fileopen, addback-fileopen)
-# moves a fopen() block earlier in the function. Implement simple-patch
-# inline: replace the .before block (verbatim) with the .after block.
-apply_simple_patch() {
- target=$1
- before=$2
- after=$3
- [ -r "$target" ] || { echo "patch target missing: $target" >&2; exit 1; }
- [ -r "$before" ] || { echo "patch before missing: $before" >&2; exit 1; }
- [ -r "$after" ] || { echo "patch after missing: $after" >&2; exit 1; }
- # Use awk to do a literal multiline replace. We slurp file, find the
- # first occurrence of the before-block, splice in the after-block.
- awk -v BFILE="$before" -v AFILE="$after" '
- BEGIN {
- while ((getline line < BFILE) > 0) bef = bef line "\n";
- close(BFILE);
- while ((getline line < AFILE) > 0) aft = aft line "\n";
- close(AFILE);
- }
- { src = src $0 "\n" }
- END {
- i = index(src, bef);
- if (i == 0) { print "patch did not match" > "/dev/stderr"; exit 1 }
- printf "%s%s%s",
- substr(src, 1, i - 1),
- aft,
- substr(src, i + length(bef));
- }
- ' "$target" > "$target.new"
- mv "$target.new" "$target"
-}
-
-apply_simple_patch \
- "$SRC/tcctools.c" \
- "$LB_PATCHES/remove-fileopen.before" \
- "$LB_PATCHES/remove-fileopen.after"
-
-apply_simple_patch \
- "$SRC/tcctools.c" \
- "$LB_PATCHES/addback-fileopen.before" \
- "$LB_PATCHES/addback-fileopen.after"
-
-# pass1.kaem creates two empty config.h files via `catm <out>` (line 27-28).
-# tcc.h does `#include "config.h"` and Mes's stdio.h reaches for
-# `<mes/config.h>`. Both are empty in the live-bootstrap build.
-: > "$SRC/config.h"
-mkdir -p "$WORK/mes-overlay/mes"
-: > "$WORK/mes-overlay/mes/config.h"
-
-# --- (3) flatten via host preprocessor --------------------------------
-HOST_CC=${HOST_CC:-cc}
-FLAT=$WORK/tcc.flat.c
-
-# Defines mirror the tcc-mes invocation in pass1.kaem. Paths in the
-# CONFIG_* defines don't matter for our purposes (we won't run the
-# resulting binary as-is) but must be syntactically valid string literals.
-"$HOST_CC" -E -P \
- -nostdinc \
- -I "$SRC" \
- -I "$WORK/mes-overlay" \
- -I "$MES_INCLUDE_LINUX" \
- -I "$MES_INCLUDE" \
- -D __linux__=1 \
- -D __${MES_ARCH}__=1 \
- -D BOOTSTRAP=1 \
- -D HAVE_LONG_LONG=$HAVE_LL \
- -D inline= \
- -D "CONFIG_TCCDIR=\"/lib/tcc\"" \
- -D "CONFIG_SYSROOT=\"/\"" \
- -D "CONFIG_TCC_CRTPREFIX=\"/lib\"" \
- -D "CONFIG_TCC_ELFINTERP=\"/mes/loader\"" \
- -D "CONFIG_TCC_SYSINCLUDEPATHS=\"/include/mes\"" \
- -D "TCC_LIBGCC=\"/lib/libc.a\"" \
- -D CONFIG_TCC_LIBTCC1_MES=0 \
- -D CONFIG_TCCBOOT=1 \
- -D CONFIG_TCC_STATIC=1 \
- -D CONFIG_USE_LIBGCC=1 \
- -D "TCC_VERSION=\"0.9.26\"" \
- -D ONE_SOURCE=1 \
- -D TCC_TARGET_${ARCH}=1 \
- "$SRC/tcc.c" > "$FLAT"
-
-LINES=$(wc -l < "$FLAT")
-BYTES=$(wc -c < "$FLAT")
-echo "produced $FLAT ($LINES lines, $BYTES bytes)"
-
-# --- (4) optional verify ---------------------------------------------
-if [ "$VERIFY" -eq 1 ]; then
- HOST_OBJ=$WORK/tcc.flat.o
- if "$HOST_CC" -c -w -o "$HOST_OBJ" "$FLAT" 2>"$WORK/host-cc.log"; then
- echo "host cc: tcc.flat.c compiles cleanly to $HOST_OBJ"
- else
- echo "host cc: tcc.flat.c FAILED to compile; see $WORK/host-cc.log" >&2
- echo "first 30 errors:" >&2
- head -30 "$WORK/host-cc.log" >&2
- exit 1
- fi
-fi
-
-# --- (5) optional self-host validation in a Linux x86_64 container ----
-# tcc.c expects errno as a global int (mes-libc convention) but musl
-# exposes errno via __errno_location(). Provide a tiny shim so linking
-# against musl works.
-if [ "$SELF_HOST" -eq 1 ]; then
- if [ "$ARCH" != "X86_64" ]; then
- echo "--self-host only supported for X86_64 (the live-bootstrap path)" >&2
- exit 2
- fi
- command -v podman >/dev/null 2>&1 || {
- echo "--self-host requires podman" >&2; exit 2
- }
- SHIM=$WORK/errno-shim.c
- printf 'int errno;\n' > "$SHIM"
-
- echo "--- self-host: building tcc-host with alpine gcc ---"
- podman run --rm --platform linux/amd64 \
- -v "$ROOT":/work -w /work alpine:latest sh -c "
- apk add --no-cache gcc musl-dev >/dev/null 2>&1
- set -e
- REL=build/cc-bootstrap/$ARCH
- gcc -w -static -no-pie -o \$REL/tcc-host \\
- \$REL/tcc.flat.c \\
- \$REL/errno-shim.c
- echo
- echo '--- tcc-host -version ---'
- \$REL/tcc-host -version
- echo
- echo '--- self-compile: tcc-host compiling tcc.flat.c ---'
- \$REL/tcc-host -c -o \$REL/tcc-self.o \$REL/tcc.flat.c
- ls -la \$REL/tcc-self.o
- echo 'self-compile: OK'
- "
-fi
diff --git a/scripts/diag-livebootstrap-qemu.sh b/scripts/diag-livebootstrap-qemu.sh
@@ -0,0 +1,187 @@
+#!/bin/sh
+## scripts/diag-livebootstrap-qemu.sh — DIAGNOSTIC ONLY
+##
+## Runs live-bootstrap's stage0 → tcc-0.9.27 chain inside a busybox:musl
+## container under linux/amd64 QEMU emulation, to determine whether the
+## tcc-boot0-mes startup SEGV (Issue §3 in docs/TCC.md) is QEMU's fault
+## or specific to our build.
+##
+## **This script is diagnostic, not part of the bootstrap chain.** It
+## intentionally invokes live-bootstrap's M2-Planet / Mes / MesCC path
+## — the very chain our project replaces. Nothing it produces feeds
+## into the project's deliverables. Read result, then ignore.
+##
+## Outcome interpretation:
+## - chain reaches tcc-0.9.27 and `tcc -version` works:
+## QEMU is sound. Our tcc-boot0-mes SEGV is build-specific
+## (codegen/runtime bug). Action: backport tcc 0.9.28rc fixes
+## or compare disasm against live-bootstrap's tcc-boot0.
+## - chain SEGVs at tcc-mes / tcc-boot0 / tcc-0.9.27:
+## QEMU is broken for these binaries on macOS arm64. Action:
+## either run on native x86_64 hardware, or use the linux/386
+## (32-bit) path with QEMU and accept the arch mismatch.
+##
+## Setup (host):
+## - Distfiles at lb-work/distfiles. Currently we have
+## mes-0.27.1.tar.gz, nyacc-1.00.2-lb1.tar.gz, tcc-0.9.26.tar.gz
+## The diagnostic also needs tcc-0.9.27.tar.bz2; this script
+## fetches it via curl on first run if missing. Stage0-posix tools
+## (M2-Planet, mescc-tools, etc.) are bundled in
+## ../live-bootstrap/seed/stage0-posix and don't need distfiles.
+##
+## Pipeline:
+## 1. (host) populate distfiles (curl tcc-0.9.27 if needed)
+## 2. (host) assemble rootfs at build/diag-livebootstrap/rootfs/
+## - copy seed/stage0-posix/* (the hex0 binaries + M2-Planet
+## + mescc-tools sources) to /
+## - copy seed/{after,seed,preseeded}.kaem to /
+## - copy steps/, lib/ to /
+## - copy distfiles to /distfiles
+## - write /steps/bootstrap.cfg with ARCH=amd64, CHROOT=True
+## - truncate /steps/manifest after `build: tcc-0.9.27` so the
+## chain stops at our target instead of running 200+ builds
+## 3. (busybox:musl, linux/amd64) chroot into rootfs and run
+## /bootstrap-seeds/POSIX/AMD64/kaem-optional-seed
+##
+## Runtime: many hours under QEMU emulation on macOS arm64. Set aside a
+## work block. Set DIAG_PREP_ONLY=1 to do steps 1-2 and skip the run.
+##
+## Usage:
+## scripts/diag-livebootstrap-qemu.sh
+
+set -eu
+
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+BOOTSTRAP_ROOT=$(cd "$ROOT/.." && pwd)
+LB=$BOOTSTRAP_ROOT/live-bootstrap
+DISTFILES=$BOOTSTRAP_ROOT/lb-work/distfiles
+WORK=$ROOT/build/diag-livebootstrap
+ROOTFS=$WORK/rootfs
+
+[ -d "$LB" ] || { echo "missing live-bootstrap at $LB" >&2; exit 1; }
+[ -d "$DISTFILES" ] || { echo "missing distfiles at $DISTFILES" >&2; exit 1; }
+command -v podman >/dev/null 2>&1 || { echo "podman required" >&2; exit 2; }
+
+# --- (1) ensure distfiles populated ----------------------------------
+NEED="mes-0.27.1.tar.gz tcc-0.9.26.tar.gz tcc-0.9.27.tar.bz2 nyacc-1.00.2-lb1.tar.gz"
+for f in $NEED; do
+ if [ ! -r "$DISTFILES/$f" ]; then
+ echo "missing $DISTFILES/$f"
+ case "$f" in
+ tcc-0.9.27.tar.bz2)
+ echo "fetching from savannah..."
+ curl --fail --location \
+ "https://download.savannah.gnu.org/releases/tinycc/$f" \
+ -o "$DISTFILES/$f"
+ ;;
+ *)
+ echo " (cannot auto-fetch $f — please populate $DISTFILES/$f)" >&2
+ exit 1
+ ;;
+ esac
+ fi
+done
+echo "distfiles ok: $NEED"
+
+# --- (2) assemble rootfs --------------------------------------------
+echo "=== assembling rootfs at $ROOTFS ==="
+rm -rf "$WORK"
+mkdir -p "$ROOTFS"
+
+# seed/stage0-posix → / (stage0 tools, M2-Planet, mescc-tools, kaem etc)
+cp -R "$LB/seed/stage0-posix/." "$ROOTFS/"
+
+# seed/*.kaem → / (preseeded.kaem, seed.kaem, after.kaem)
+cp "$LB/seed/"*.kaem "$ROOTFS/"
+
+# Other seed files (configurator binaries, checksums)
+cp "$LB/seed/configurator.c" "$LB/seed/configurator.amd64.checksums" "$ROOTFS/" 2>/dev/null || true
+cp "$LB/seed/script-generator.c" "$LB/seed/script-generator.amd64.checksums" "$ROOTFS/" 2>/dev/null || true
+
+# steps/, lib/ from live-bootstrap
+cp -R "$LB/steps" "$ROOTFS/"
+cp -R "$LB/lib" "$ROOTFS/"
+
+# distfiles → /external/distfiles (live-bootstrap's steps/env sets
+# DISTFILES=/external/distfiles, and helpers/build steps read from there)
+mkdir -p "$ROOTFS/external/distfiles"
+for f in $NEED; do
+ cp "$DISTFILES/$f" "$ROOTFS/external/distfiles/"
+done
+
+# Truncate manifest to stop after the first `build: tcc-0.9.27`. The
+# manifest has a header comment block (lines 1-33), then build steps
+# starting at line 34. Line 38 is the first tcc-0.9.27 build.
+awk '
+ /^build: tcc-0\.9\.27/ && !seen_tcc27 {
+ print
+ seen_tcc27 = 1
+ next
+ }
+ seen_tcc27 {
+ # drop everything after first tcc-0.9.27 build
+ next
+ }
+ { print }
+' "$LB/steps/manifest" > "$ROOTFS/steps/manifest"
+
+# bootstrap.cfg — mirrors what rootfs.py would write for
+# `--arch amd64 --chroot --mirrors file:///distfiles`. We disable
+# every optional pipeline (kernels, configurator, fiwix) since this
+# is a pass1-only diagnostic.
+cat > "$ROOTFS/steps/bootstrap.cfg" <<'EOF'
+ARCH=amd64
+ARCH_DIR=AMD64
+FORCE_TIMESTAMPS=False
+CHROOT=True
+UPDATE_CHECKSUMS=False
+JOBS=2
+SWAP_SIZE=0
+FINAL_JOBS=2
+INTERNAL_CI=False
+INTERACTIVE=False
+QEMU=False
+BARE_METAL=False
+DISK=sda1
+KERNEL_BOOTSTRAP=False
+BUILD_KERNELS=False
+CONFIGURATOR=False
+MIRRORS_LEN=0
+EOF
+
+echo "rootfs assembled."
+du -sh "$ROOTFS" 2>/dev/null || true
+
+if [ "${DIAG_PREP_ONLY:-0}" = "1" ]; then
+ echo "DIAG_PREP_ONLY=1 — skipping container run."
+ exit 0
+fi
+
+# --- (3) run kaem-optional-seed in busybox:musl under linux/amd64 ----
+echo
+echo "=== launching kaem-optional-seed via busybox:musl (linux/amd64 QEMU) ==="
+echo " long-running. ctrl-C aborts. log lines stream below."
+echo
+
+# busybox:musl ships chroot, sh, tar, awk, etc. — sufficient.
+# /proc /dev /sys are mounted by podman; chroot inherits them via bind.
+# We mount the rootfs as /work/rootfs inside the container, then chroot.
+podman run --rm -i --platform linux/amd64 \
+ -v "$ROOTFS":/rootfs \
+ docker.io/library/busybox:musl sh -s <<'CONTAINER_SCRIPT'
+set -eu
+
+# Ensure /proc /dev /sys exist inside the chroot for kaem etc.
+mkdir -p /rootfs/proc /rootfs/dev /rootfs/sys /rootfs/tmp
+mount -t proc proc /rootfs/proc 2>/dev/null || true
+mount --rbind /dev /rootfs/dev 2>/dev/null || true
+mount --rbind /sys /rootfs/sys 2>/dev/null || true
+
+echo "--- starting chroot kaem ---"
+exec env -i PATH=/bin chroot /rootfs /bootstrap-seeds/POSIX/AMD64/kaem-optional-seed
+CONTAINER_SCRIPT
+
+rc=$?
+echo
+echo "=== kaem exit=$rc ==="
+exit "$rc"
diff --git a/scripts/stage1-flatten.sh b/scripts/stage1-flatten.sh
@@ -0,0 +1,155 @@
+#!/bin/sh
+## scripts/stage1-flatten.sh — flatten upstream tcc-0.9.26 into a single
+## C bytestream (tcc.flat.c) using only the host preprocessor.
+##
+## This is the first of three stages building tcc-0.9.26 without
+## M2-Planet, MesCC, or Mes Scheme. See docs/TCC.md.
+##
+## Stages:
+## 1. unpack tcc-0.9.26-1147-gee75a10c.tar.gz
+## 2. apply live-bootstrap simple-patches (tcctools.c file-open reorder)
+## 3. host cc -E -nostdinc with mes-bundled headers + tcc-mes defines
+## 4. emit build/cc-bootstrap/<arch>/tcc.flat.c
+## 5. (--verify) compile tcc.flat.c with host cc to confirm well-formedness
+##
+## Stage 1 deliberately stays on the host: it is just text manipulation
+## (preprocess + concat) and the resulting tcc.flat.c is a portable
+## artifact downstream stages consume. No container needed.
+##
+## Usage:
+## scripts/stage1-flatten.sh [--arch <X86_64|I386|RISCV64>] [--verify]
+
+set -eu
+
+# --- arg parse --------------------------------------------------------
+ARCH=X86_64
+VERIFY=0
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --arch) ARCH=$2; shift 2 ;;
+ --verify) VERIFY=1; shift ;;
+ -h|--help) sed -n 's/^## \{0,1\}//p' "$0"; exit 0 ;;
+ *) echo "unknown arg: $1" >&2; exit 2 ;;
+ esac
+done
+
+case "$ARCH" in
+ X86_64) MES_ARCH=x86_64; HAVE_LL=1 ;;
+ I386) MES_ARCH=x86; HAVE_LL=0 ;;
+ RISCV64) MES_ARCH=riscv64; HAVE_LL=1 ;;
+ AARCH64) echo "AARCH64 not in live-bootstrap; tcc.c lacks an arm64-gen.c" >&2; exit 2 ;;
+ *) echo "unknown ARCH: $ARCH" >&2; exit 2 ;;
+esac
+
+# --- paths ------------------------------------------------------------
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+WORK=$ROOT/build/cc-bootstrap/$ARCH
+DISTFILES=$ROOT/../lb-work/distfiles
+LB_PATCHES=$ROOT/../live-bootstrap/steps/tcc-0.9.26/simple-patches
+MES_INCLUDE=$ROOT/../mes/include
+MES_INCLUDE_LINUX=$MES_INCLUDE/linux/$MES_ARCH
+
+TCC_TAR=$DISTFILES/tcc-0.9.26.tar.gz
+TCC_PKG=tcc-0.9.26-1147-gee75a10c
+
+[ -r "$TCC_TAR" ] || { echo "missing $TCC_TAR" >&2; exit 1; }
+[ -d "$LB_PATCHES" ] || { echo "missing $LB_PATCHES" >&2; exit 1; }
+[ -d "$MES_INCLUDE" ] || { echo "missing $MES_INCLUDE" >&2; exit 1; }
+[ -d "$MES_INCLUDE_LINUX" ] || { echo "missing $MES_INCLUDE_LINUX" >&2; exit 1; }
+
+# --- (1) unpack -------------------------------------------------------
+mkdir -p "$WORK"
+rm -rf "$WORK/$TCC_PKG"
+tar -xzf "$TCC_TAR" -C "$WORK"
+
+SRC=$WORK/$TCC_PKG
+
+# --- (2) simple-patches ----------------------------------------------
+# Both patches edit tcctools.c. The pair (remove-fileopen, addback-fileopen)
+# moves a fopen() block earlier in the function. We implement live-bootstrap's
+# simple-patch as an awk literal-block replacer; no binary dep.
+apply_simple_patch() {
+ target=$1; before=$2; after=$3
+ [ -r "$target" ] || { echo "patch target missing: $target" >&2; exit 1; }
+ [ -r "$before" ] || { echo "patch before missing: $before" >&2; exit 1; }
+ [ -r "$after" ] || { echo "patch after missing: $after" >&2; exit 1; }
+ awk -v BFILE="$before" -v AFILE="$after" '
+ BEGIN {
+ while ((getline line < BFILE) > 0) bef = bef line "\n";
+ close(BFILE);
+ while ((getline line < AFILE) > 0) aft = aft line "\n";
+ close(AFILE);
+ }
+ { src = src $0 "\n" }
+ END {
+ i = index(src, bef);
+ if (i == 0) { print "patch did not match" > "/dev/stderr"; exit 1 }
+ printf "%s%s%s",
+ substr(src, 1, i - 1),
+ aft,
+ substr(src, i + length(bef));
+ }
+ ' "$target" > "$target.new"
+ mv "$target.new" "$target"
+}
+
+apply_simple_patch \
+ "$SRC/tcctools.c" \
+ "$LB_PATCHES/remove-fileopen.before" \
+ "$LB_PATCHES/remove-fileopen.after"
+
+apply_simple_patch \
+ "$SRC/tcctools.c" \
+ "$LB_PATCHES/addback-fileopen.before" \
+ "$LB_PATCHES/addback-fileopen.after"
+
+# Empty config.h shims — pass1.kaem creates these via `catm <out>` (line 27-28).
+: > "$SRC/config.h"
+mkdir -p "$WORK/mes-overlay/mes"
+: > "$WORK/mes-overlay/mes/config.h"
+
+# --- (3) flatten via host preprocessor --------------------------------
+HOST_CC=${HOST_CC:-cc}
+FLAT=$WORK/tcc.flat.c
+
+"$HOST_CC" -E -P \
+ -nostdinc \
+ -I "$SRC" \
+ -I "$WORK/mes-overlay" \
+ -I "$MES_INCLUDE_LINUX" \
+ -I "$MES_INCLUDE" \
+ -D __linux__=1 \
+ -D __${MES_ARCH}__=1 \
+ -D BOOTSTRAP=1 \
+ -D HAVE_LONG_LONG=$HAVE_LL \
+ -D inline= \
+ -D "CONFIG_TCCDIR=\"/lib/tcc\"" \
+ -D "CONFIG_SYSROOT=\"/\"" \
+ -D "CONFIG_TCC_CRTPREFIX=\"/lib\"" \
+ -D "CONFIG_TCC_ELFINTERP=\"/mes/loader\"" \
+ -D "CONFIG_TCC_SYSINCLUDEPATHS=\"/include/mes\"" \
+ -D "TCC_LIBGCC=\"/lib/libc.a\"" \
+ -D CONFIG_TCC_LIBTCC1_MES=0 \
+ -D CONFIG_TCCBOOT=1 \
+ -D CONFIG_TCC_STATIC=1 \
+ -D CONFIG_USE_LIBGCC=1 \
+ -D "TCC_VERSION=\"0.9.26\"" \
+ -D ONE_SOURCE=1 \
+ -D TCC_TARGET_${ARCH}=1 \
+ "$SRC/tcc.c" > "$FLAT"
+
+LINES=$(wc -l < "$FLAT")
+BYTES=$(wc -c < "$FLAT")
+echo "produced $FLAT ($LINES lines, $BYTES bytes)"
+
+# --- (4) optional verify ---------------------------------------------
+if [ "$VERIFY" -eq 1 ]; then
+ HOST_OBJ=$WORK/tcc.flat.o
+ if "$HOST_CC" -c -w -o "$HOST_OBJ" "$FLAT" 2>"$WORK/host-cc.log"; then
+ echo "host cc: tcc.flat.c compiles cleanly to $HOST_OBJ"
+ else
+ echo "host cc: tcc.flat.c FAILED to compile; see $WORK/host-cc.log" >&2
+ head -30 "$WORK/host-cc.log" >&2
+ exit 1
+ fi
+fi
diff --git a/scripts/stage2-alpine.sh b/scripts/stage2-alpine.sh
@@ -0,0 +1,275 @@
+#!/bin/sh
+## scripts/stage2-alpine.sh — build tcc-boot0-mes in an alpine container.
+##
+## This is the stand-in slot for our scheme1-hosted C compiler: a real
+## native gcc plays the role our scheme CC will eventually fill. Its
+## job is to take stage 1's tcc.flat.c, build a working tcc, then use
+## that tcc to compile mes libc and link a final static tcc-0.9.26
+## binary against it. See docs/TCC.md.
+##
+## We picked alpine (musl) over debian (glibc) because mes headers
+## declare errno as a plain global, while glibc declares it TLS — a
+## non-TLS / TLS clash at link time. musl exposes errno only via
+## __errno_location(), so a one-line `int errno;` shim is the sole
+## definition and links cleanly.
+##
+## Pre-condition:
+## build/cc-bootstrap/<arch>/tcc.flat.c (run scripts/stage1-flatten.sh)
+##
+## Inside alpine:latest (linux/amd64):
+## 1. apk add gcc musl-dev
+## 2. gcc -static tcc.flat.c errno-shim.c -> tcc-host
+## (validates the flatten output as well-formed C; tcc-host is a
+## working musl-linked tcc-0.9.26 binary)
+## 3. unpack mes-0.27.1, set up include tree with arch symlink
+## (Issue §1 workaround — tcc 0.9.26 SEGVs on missing include)
+## 4. tcc-host compiles each mes libc .c file individually
+## (Issue §2 workaround — concatenated TU SEGVs around 22+ files)
+## 5. tcc-host -ar -> /lib/libc.a + /lib/tcc/libtcc1.a, build crt1.o
+## 6. tcc-host -static compiles patched real tcc.c against mes libc
+## directly into tcc-boot0-mes — mirrors live-bootstrap's tcc-boot0
+## invocation. We skip the tcc-self.o intermediate that an older
+## iteration used: it was a redundant round-trip and exposed a tcc
+## 0.9.26 bug in the static-link codepath.
+## 7. (best-effort) tcc-boot0-mes -version
+## Expected to segfault under QEMU x86_64 emulation on macOS arm64
+## (Issue §3); native x86_64 needed to verify cleanly.
+##
+## Output: build/cc-bootstrap/<arch>/tcc-boot0-mes (static, mes-libc-linked).
+## This artifact is what stage 3 (busybox) consumes to drive the
+## tcc-boot1 / tcc-boot2 chain.
+##
+## Usage:
+## scripts/stage2-alpine.sh [--arch X86_64]
+
+set -eu
+
+ARCH=X86_64
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --arch) ARCH=$2; shift 2 ;;
+ -h|--help) sed -n 's/^## \{0,1\}//p' "$0"; exit 0 ;;
+ *) echo "unknown arg: $1" >&2; exit 2 ;;
+ esac
+done
+
+if [ "$ARCH" != "X86_64" ]; then
+ echo "stage2 currently only supports X86_64 (live-bootstrap reference path)" >&2
+ exit 2
+fi
+MES_ARCH=x86_64
+
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+WORK=$ROOT/build/cc-bootstrap/$ARCH
+DISTFILES=$ROOT/../lb-work/distfiles
+MES_TAR=$DISTFILES/mes-0.27.1.tar.gz
+MES_PKG=mes-0.27.1
+FLAT=$WORK/tcc.flat.c
+
+[ -r "$FLAT" ] || { echo "missing $FLAT — run scripts/stage1-flatten.sh first" >&2; exit 1; }
+[ -r "$MES_TAR" ] || { echo "missing $MES_TAR" >&2; exit 1; }
+command -v podman >/dev/null 2>&1 || { echo "podman required" >&2; exit 2; }
+
+# Unpack mes outside the container so it lands on the bind mount.
+if [ ! -d "$WORK/$MES_PKG" ]; then
+ tar -xzf "$MES_TAR" -C "$WORK"
+fi
+mkdir -p "$WORK/$MES_PKG/include/mes"
+: > "$WORK/$MES_PKG/include/mes/config.h"
+
+# errno shim: tcc.flat.c references errno as a plain global (mes-libc
+# convention). musl provides errno only via __errno_location(), so this
+# one-line int errno; is the sole storage. Without it the link fails.
+printf 'int errno;\n' > "$WORK/errno-shim.c"
+
+echo "=== stage 2: tcc-boot0-mes via alpine:latest ==="
+echo "(slow on macOS arm64 — runs under QEMU linux/amd64)"
+
+TCC_PKG=tcc-0.9.26-1147-gee75a10c
+
+podman run --rm -i --platform linux/amd64 \
+ -v "$ROOT":/work -w /work alpine:latest sh -s "$ARCH" "$MES_ARCH" "$MES_PKG" "$TCC_PKG" <<'CONTAINER_SCRIPT'
+set -eu
+ARCH=$1
+MES_ARCH=$2
+MES_PKG=$3
+TCC_PKG=$4
+WORK=/work/build/cc-bootstrap/$ARCH
+
+# --- (1) install gcc + musl-dev (provides libc.a for -static) --------
+apk add --no-cache gcc musl-dev >/dev/null
+echo "host gcc: $(gcc --version | head -1)"
+
+# --- (2) gcc tcc.flat.c -> tcc-host ----------------------------------
+echo "--- gcc -static -> tcc-host ---"
+gcc -w -static -no-pie -o "$WORK/tcc-host" \
+ "$WORK/tcc.flat.c" "$WORK/errno-shim.c"
+"$WORK/tcc-host" -version
+
+# --- (3) sanitized include tree --------------------------------------
+MES_SRC=$WORK/$MES_PKG
+INC=/tmp/mes-inc
+rm -rf $INC
+mkdir -p $INC
+cp -r $MES_SRC/include/. $INC/
+ln -sfn linux/$MES_ARCH $INC/arch
+
+# --- (4) compile mes libc per-file -----------------------------------
+echo "--- tcc-host compiling mes libc (per-file to dodge Issue §2) ---"
+mkdir -p /tmp/objs
+cd $MES_SRC/lib
+
+ALL_FILES="ctype/isalnum.c ctype/isalpha.c ctype/isascii.c ctype/iscntrl.c \
+ctype/isdigit.c ctype/isgraph.c ctype/islower.c ctype/isnumber.c \
+ctype/isprint.c ctype/ispunct.c ctype/isspace.c ctype/isupper.c \
+ctype/isxdigit.c ctype/tolower.c ctype/toupper.c \
+dirent/closedir.c dirent/__getdirentries.c dirent/opendir.c \
+linux/readdir.c linux/access.c linux/brk.c linux/chdir.c linux/chmod.c \
+linux/clock_gettime.c linux/close.c linux/dup2.c linux/dup.c linux/execve.c \
+linux/fcntl.c linux/fork.c linux/fsync.c linux/fstat.c linux/_getcwd.c \
+linux/getdents.c linux/getegid.c linux/geteuid.c linux/getgid.c linux/getpid.c \
+linux/getppid.c linux/getrusage.c linux/gettimeofday.c linux/getuid.c \
+linux/ioctl.c linux/ioctl3.c linux/kill.c linux/link.c linux/lseek.c \
+linux/lstat.c linux/malloc.c linux/mkdir.c linux/mknod.c linux/nanosleep.c \
+linux/_open3.c linux/pipe.c linux/_read.c linux/readlink.c linux/rename.c \
+linux/rmdir.c linux/setgid.c linux/settimer.c linux/setuid.c linux/signal.c \
+linux/sigprogmask.c linux/symlink.c linux/stat.c linux/time.c linux/unlink.c \
+linux/waitpid.c linux/wait4.c \
+linux/${MES_ARCH}-mes-gcc/_exit.c linux/${MES_ARCH}-mes-gcc/syscall.c \
+linux/${MES_ARCH}-mes-gcc/_write.c \
+math/ceil.c math/fabs.c math/floor.c \
+mes/abtod.c mes/abtol.c mes/__assert_fail.c mes/assert_msg.c \
+mes/__buffered_read.c mes/__init_io.c mes/cast.c mes/dtoab.c \
+mes/eputc.c mes/eputs.c mes/fdgetc.c mes/fdgets.c mes/fdputc.c mes/fdputs.c \
+mes/fdungetc.c mes/globals.c mes/itoa.c mes/ltoab.c mes/ltoa.c \
+mes/__mes_debug.c mes/mes_open.c mes/ntoab.c mes/oputc.c mes/oputs.c \
+mes/search-path.c mes/ultoa.c mes/utoa.c \
+posix/alarm.c posix/buffered-read.c posix/execl.c posix/execlp.c \
+posix/execv.c posix/execvp.c posix/getcwd.c posix/getenv.c posix/isatty.c \
+posix/mktemp.c posix/open.c posix/pathconf.c posix/raise.c posix/sbrk.c \
+posix/setenv.c posix/sleep.c posix/unsetenv.c posix/wait.c posix/write.c \
+stdio/clearerr.c stdio/fclose.c stdio/fdopen.c stdio/feof.c stdio/ferror.c \
+stdio/fflush.c stdio/fgetc.c stdio/fgets.c stdio/fileno.c stdio/fopen.c \
+stdio/fprintf.c stdio/fputc.c stdio/fputs.c stdio/fread.c stdio/freopen.c \
+stdio/fscanf.c stdio/fseek.c stdio/ftell.c stdio/fwrite.c stdio/getc.c \
+stdio/getchar.c stdio/perror.c stdio/printf.c stdio/putc.c stdio/putchar.c \
+stdio/remove.c stdio/snprintf.c stdio/sprintf.c stdio/sscanf.c stdio/ungetc.c \
+stdio/vfprintf.c stdio/vfscanf.c stdio/vprintf.c stdio/vsnprintf.c \
+stdio/vsprintf.c stdio/vsscanf.c \
+stdlib/abort.c stdlib/abs.c stdlib/alloca.c stdlib/atexit.c stdlib/atof.c \
+stdlib/atoi.c stdlib/atol.c stdlib/calloc.c stdlib/__exit.c stdlib/exit.c \
+stdlib/free.c stdlib/mbstowcs.c stdlib/puts.c stdlib/qsort.c stdlib/realloc.c \
+stdlib/strtod.c stdlib/strtof.c stdlib/strtol.c stdlib/strtold.c \
+stdlib/strtoll.c stdlib/strtoul.c stdlib/strtoull.c \
+string/bcmp.c string/bcopy.c string/bzero.c string/index.c string/memchr.c \
+string/memcmp.c string/memcpy.c string/memmem.c string/memmove.c string/memset.c \
+string/rindex.c string/strcat.c string/strchr.c string/strcmp.c string/strcpy.c \
+string/strcspn.c string/strdup.c string/strerror.c string/strlen.c \
+string/strlwr.c string/strncat.c string/strncmp.c string/strncpy.c \
+string/strpbrk.c string/strrchr.c string/strspn.c string/strstr.c string/strupr.c \
+stub/atan2.c stub/bsearch.c stub/chown.c stub/__cleanup.c stub/cos.c \
+stub/ctime.c stub/exp.c stub/fpurge.c stub/freadahead.c stub/frexp.c \
+stub/getgrgid.c stub/getgrnam.c stub/getlogin.c stub/getpgid.c stub/getpgrp.c \
+stub/getpwnam.c stub/getpwuid.c stub/gmtime.c stub/ldexp.c stub/localtime.c \
+stub/log.c stub/mktime.c stub/modf.c stub/mprotect.c stub/pclose.c \
+stub/popen.c stub/pow.c stub/putenv.c stub/rand.c stub/realpath.c stub/rewind.c \
+stub/setbuf.c stub/setgrent.c stub/setlocale.c stub/setvbuf.c stub/sigaction.c \
+stub/sigaddset.c stub/sigblock.c stub/sigdelset.c stub/sigemptyset.c \
+stub/sigsetmask.c stub/sin.c stub/sys_siglist.c stub/system.c stub/sqrt.c \
+stub/strftime.c stub/times.c stub/ttyname.c stub/umask.c stub/utime.c \
+${MES_ARCH}-mes-gcc/setjmp.c"
+
+OBJS=
+n_compiled=0
+n_failed=0
+for f in $ALL_FILES; do
+ name=$(echo "$f" | tr / _)
+ o=/tmp/objs/${name%.c}.o
+ if "$WORK/tcc-host" -c -D HAVE_CONFIG_H=1 -I "$INC" -I "$INC/linux/$MES_ARCH" \
+ -o "$o" "$f" 2>/dev/null; then
+ OBJS="$OBJS $o"
+ n_compiled=$((n_compiled+1))
+ else
+ echo "compile failed: $f" >&2
+ n_failed=$((n_failed+1))
+ fi
+done
+echo "compiled $n_compiled libc .o files (failed: $n_failed)"
+[ "$n_failed" -eq 0 ] || { echo "abort: some libc files failed" >&2; exit 1; }
+
+# --- (5) ar -> libc.a, crt1.o, libtcc1.a -----------------------------
+mkdir -p /lib/tcc /include/mes
+"$WORK/tcc-host" -ar cr /lib/libc.a $OBJS
+
+"$WORK/tcc-host" -c -D HAVE_CONFIG_H=1 -I "$INC" -I "$INC/linux/$MES_ARCH" \
+ -o /lib/crt1.o "linux/$MES_ARCH-mes-gcc/crt1.c"
+: > /lib/crtn.o
+: > /lib/crti.o
+
+"$WORK/tcc-host" -c -D HAVE_CONFIG_H=1 -D HAVE_LONG_LONG=1 -D HAVE_FLOAT=1 \
+ -I "$INC" -I "$INC/linux/$MES_ARCH" \
+ -o /tmp/libtcc1.o libtcc1.c
+"$WORK/tcc-host" -ar cr /lib/tcc/libtcc1.a /tmp/libtcc1.o
+
+cp -r "$INC/." /include/mes/
+ls -la /lib/crt1.o /lib/libc.a /lib/tcc/libtcc1.a
+
+# --- (6) tcc-host -static compile real tcc.c -> tcc-boot0-mes --------
+# Mirrors live-bootstrap pass1.kaem's tcc-boot0 invocation: same flags,
+# same source tree, just driven by tcc-host instead of tcc-mes. Direct
+# compile+link in one shot — no intermediate .o.
+echo "--- tcc-host -static compile+link real tcc.c -> tcc-boot0-mes ---"
+cd "$WORK/$TCC_PKG"
+"$WORK/tcc-host" \
+ -g \
+ -static \
+ -o "$WORK/tcc-boot0-mes" \
+ -D BOOTSTRAP=1 \
+ -D HAVE_FLOAT=1 \
+ -D HAVE_BITFIELD=1 \
+ -D HAVE_LONG_LONG=1 \
+ -D HAVE_SETJMP=1 \
+ -I . \
+ -I /include/mes \
+ -D TCC_TARGET_${ARCH}=1 \
+ -D CONFIG_TCCDIR=\"/lib/tcc\" \
+ -D CONFIG_TCC_CRTPREFIX=\"/lib\" \
+ -D CONFIG_TCC_ELFINTERP=\"/mes/loader\" \
+ -D CONFIG_TCC_LIBPATHS=\"/lib:/lib/tcc\" \
+ -D CONFIG_TCC_SYSINCLUDEPATHS=\"/include/mes\" \
+ -D TCC_LIBGCC=\"/lib/libc.a\" \
+ -D TCC_LIBTCC1=\"libtcc1.a\" \
+ -D CONFIG_TCCBOOT=1 \
+ -D CONFIG_TCC_STATIC=1 \
+ -D CONFIG_USE_LIBGCC=1 \
+ -D TCC_VERSION=\"0.9.26\" \
+ -D ONE_SOURCE=1 \
+ -L . \
+ -L /lib \
+ tcc.c
+ls -la "$WORK/tcc-boot0-mes"
+
+# Stage out mes libc + libtcc1 + crt1.o + headers so stage 3 can mount
+# them in next to tcc-boot0-mes without re-running stage 2.
+STAGE3=$WORK/stage3-input
+rm -rf "$STAGE3"
+mkdir -p "$STAGE3/lib/tcc" "$STAGE3/include"
+cp /lib/libc.a "$STAGE3/lib/"
+cp /lib/crt1.o "$STAGE3/lib/"
+cp /lib/crtn.o "$STAGE3/lib/"
+cp /lib/crti.o "$STAGE3/lib/"
+cp /lib/tcc/libtcc1.a "$STAGE3/lib/tcc/"
+cp -r "$INC/." "$STAGE3/include/mes/"
+echo "staged mes libc bits into $STAGE3 for stage 3"
+
+# --- (7) best-effort version probe -----------------------------------
+echo
+echo "--- tcc-boot0-mes -version (Issue §3: expected SEGV under QEMU) ---"
+rc=0; "$WORK/tcc-boot0-mes" -version 2>&1 || rc=$?
+echo "exit=$rc"
+CONTAINER_SCRIPT
+
+echo
+echo "=== stage 2 artifacts ==="
+ls -la "$WORK/tcc-host" "$WORK/tcc-boot0-mes" 2>/dev/null || \
+ echo "(some artifacts missing — see container output above)"
diff --git a/scripts/stage3-rebuild.sh b/scripts/stage3-rebuild.sh
@@ -0,0 +1,234 @@
+#!/bin/sh
+## scripts/stage3-rebuild.sh — drive the tcc-boot1 / tcc-boot2 chain
+## inside a busybox container, consuming stage 2's tcc-boot0-mes.
+##
+## Mirrors live-bootstrap's pass1.kaem (steps/tcc-0.9.26) chain:
+## tcc-boot0-mes (= tcc-boot0 slot)
+## → rebuild libc → tcc-boot1
+## → rebuild libc → tcc-boot2 (= "final 0.9.26")
+##
+## Each rebuild compiles the **real**, unflattened tcc-0.9.26 sources
+## (the patched tree at $WORK/tcc-0.9.26-1147-gee75a10c) using the
+## previous-stage tcc, with the full live-bootstrap define set
+## (HAVE_FLOAT, HAVE_BITFIELD, HAVE_SETJMP added on top of the stage 1
+## flatten set).
+##
+## Pre-condition:
+## build/cc-bootstrap/<arch>/tcc-boot0-mes
+## build/cc-bootstrap/<arch>/stage3-input/ (staged by stage 2)
+## build/cc-bootstrap/<arch>/tcc-0.9.26-1147-gee75a10c/ (patched, from stage 1)
+## build/cc-bootstrap/<arch>/mes-0.27.1/ (from stage 2)
+##
+## Container: docker.io/library/busybox:musl on linux/amd64.
+## Tools used inside: busybox sh + tcc-boot0-mes (which provides its own
+## preprocessor, assembler, linker, and `-ar`).
+##
+## Status: stage 3 cannot complete on macOS arm64 hosts today — Issue §3
+## (tcc-boot0-mes segfaults at startup under QEMU x86_64) blocks the
+## very first step. The script is correct; it will run end-to-end on
+## native x86_64 hardware or once a tcc 0.9.28rc backport patches the
+## prologue/_DYNAMIC issues. The failure is reported clearly so the
+## blocker is visible.
+##
+## Usage:
+## scripts/stage3-rebuild.sh [--arch X86_64]
+
+set -eu
+
+ARCH=X86_64
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --arch) ARCH=$2; shift 2 ;;
+ -h|--help) sed -n 's/^## \{0,1\}//p' "$0"; exit 0 ;;
+ *) echo "unknown arg: $1" >&2; exit 2 ;;
+ esac
+done
+
+if [ "$ARCH" != "X86_64" ]; then
+ echo "stage3 currently only supports X86_64" >&2
+ exit 2
+fi
+MES_ARCH=x86_64
+
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+WORK=$ROOT/build/cc-bootstrap/$ARCH
+TCC_PKG=tcc-0.9.26-1147-gee75a10c
+MES_PKG=mes-0.27.1
+
+[ -x "$WORK/tcc-boot0-mes" ] || { echo "missing $WORK/tcc-boot0-mes — run stage2-alpine.sh" >&2; exit 1; }
+[ -d "$WORK/stage3-input" ] || { echo "missing $WORK/stage3-input — run stage2-alpine.sh" >&2; exit 1; }
+[ -d "$WORK/$TCC_PKG" ] || { echo "missing $WORK/$TCC_PKG — run stage1-flatten.sh" >&2; exit 1; }
+[ -d "$WORK/$MES_PKG" ] || { echo "missing $WORK/$MES_PKG — run stage2-alpine.sh" >&2; exit 1; }
+command -v podman >/dev/null 2>&1 || { echo "podman required" >&2; exit 2; }
+
+echo "=== stage 3: tcc-boot1 / tcc-boot2 via busybox:musl ==="
+echo "(Issue §3 may block on macOS arm64 / QEMU; native x86_64 expected to succeed)"
+
+podman run --rm -i --platform linux/amd64 \
+ -v "$ROOT":/work -w /work \
+ docker.io/library/busybox:musl sh -s "$ARCH" "$MES_ARCH" "$TCC_PKG" "$MES_PKG" <<'CONTAINER_SCRIPT'
+set -eu
+ARCH=$1
+MES_ARCH=$2
+TCC_PKG=$3
+MES_PKG=$4
+WORK=/work/build/cc-bootstrap/$ARCH
+
+# --- install tcc-boot0-mes + mes libc bits at baked-in paths --------
+mkdir -p /lib/tcc /include/mes /bin
+cp "$WORK/stage3-input/lib/libc.a" /lib/libc.a
+cp "$WORK/stage3-input/lib/crt1.o" /lib/crt1.o
+cp "$WORK/stage3-input/lib/crtn.o" /lib/crtn.o
+cp "$WORK/stage3-input/lib/crti.o" /lib/crti.o
+cp "$WORK/stage3-input/lib/tcc/libtcc1.a" /lib/tcc/libtcc1.a
+cp -r "$WORK/stage3-input/include/mes/." /include/mes/
+
+TCC=$WORK/tcc-boot0-mes
+
+echo "--- tcc-boot0-mes -version (smoke; Issue §3 may SEGV here) ---"
+"$TCC" -version
+
+INC_MES=/include/mes
+TCC_DEFS_BUILD="-D BOOTSTRAP=1 \
+ -D HAVE_FLOAT=1 \
+ -D HAVE_BITFIELD=1 \
+ -D HAVE_LONG_LONG=1 \
+ -D HAVE_SETJMP=1 \
+ -D TCC_TARGET_${ARCH}=1 \
+ -D CONFIG_TCCDIR=\"/lib/tcc\" \
+ -D CONFIG_TCC_CRTPREFIX=\"/lib\" \
+ -D CONFIG_TCC_ELFINTERP=\"/mes/loader\" \
+ -D CONFIG_TCC_LIBPATHS=\"/lib:/lib/tcc\" \
+ -D CONFIG_TCC_SYSINCLUDEPATHS=\"/include/mes\" \
+ -D TCC_LIBGCC=\"/lib/libc.a\" \
+ -D TCC_LIBTCC1=\"libtcc1.a\" \
+ -D CONFIG_TCCBOOT=1 \
+ -D CONFIG_TCC_STATIC=1 \
+ -D CONFIG_USE_LIBGCC=1 \
+ -D TCC_VERSION=\"0.9.26\" \
+ -D ONE_SOURCE=1"
+
+# Helper: rebuild mes libc with the given tcc binary, install to /lib + /lib/tcc.
+# Mirrors pass1.kaem's "Recompile libc" block after each tcc-bootN.
+rebuild_libc() {
+ cc=$1
+ label=$2
+ echo "--- $label: rebuilding mes libc ---"
+ cd "$WORK/$MES_PKG"
+ "$cc" -c -D HAVE_CONFIG_H=1 -I include -I include/linux/$MES_ARCH \
+ -o /lib/crt1.o lib/linux/$MES_ARCH-mes-gcc/crt1.c
+
+ "$cc" -c -D HAVE_CONFIG_H=1 -D HAVE_LONG_LONG=1 -D HAVE_FLOAT=1 \
+ -I include -I include/linux/$MES_ARCH \
+ -o /tmp/libtcc1.o lib/libtcc1.c
+ "$cc" -ar cr /lib/tcc/libtcc1.a /tmp/libtcc1.o
+
+ # libc.a: same per-file approach as stage 2 (Issue §2 workaround).
+ rm -rf /tmp/objs && mkdir -p /tmp/objs
+ cd lib
+ OBJS=
+ for f in $ALL_LIBC_FILES; do
+ name=$(echo "$f" | tr / _)
+ o=/tmp/objs/${name%.c}.o
+ "$cc" -c -D HAVE_CONFIG_H=1 -I ../include -I ../include/linux/$MES_ARCH \
+ -o "$o" "$f"
+ OBJS="$OBJS $o"
+ done
+ "$cc" -ar cr /lib/libc.a $OBJS
+ cd "$WORK/$MES_PKG/.."
+}
+
+# Helper: compile tcc.c (real, unflattened) into a new tcc binary.
+# Mirrors pass1.kaem's tcc-boot0/1/2 invocations.
+build_tcc() {
+ cc=$1
+ out=$2
+ echo "--- $out: $cc compiling real tcc.c ---"
+ cd "$WORK/$TCC_PKG"
+ eval "\"$cc\" -g -static -o \"$WORK/$out\" \
+ $TCC_DEFS_BUILD \
+ -I . -I $INC_MES \
+ -L . -L /lib \
+ tcc.c"
+ "$WORK/$out" -version
+}
+
+# Same canonical mes libc list as stage 2 + pass1.kaem.
+ALL_LIBC_FILES="ctype/isalnum.c ctype/isalpha.c ctype/isascii.c ctype/iscntrl.c \
+ctype/isdigit.c ctype/isgraph.c ctype/islower.c ctype/isnumber.c \
+ctype/isprint.c ctype/ispunct.c ctype/isspace.c ctype/isupper.c \
+ctype/isxdigit.c ctype/tolower.c ctype/toupper.c \
+dirent/closedir.c dirent/__getdirentries.c dirent/opendir.c \
+linux/readdir.c linux/access.c linux/brk.c linux/chdir.c linux/chmod.c \
+linux/clock_gettime.c linux/close.c linux/dup2.c linux/dup.c linux/execve.c \
+linux/fcntl.c linux/fork.c linux/fsync.c linux/fstat.c linux/_getcwd.c \
+linux/getdents.c linux/getegid.c linux/geteuid.c linux/getgid.c linux/getpid.c \
+linux/getppid.c linux/getrusage.c linux/gettimeofday.c linux/getuid.c \
+linux/ioctl.c linux/ioctl3.c linux/kill.c linux/link.c linux/lseek.c \
+linux/lstat.c linux/malloc.c linux/mkdir.c linux/mknod.c linux/nanosleep.c \
+linux/_open3.c linux/pipe.c linux/_read.c linux/readlink.c linux/rename.c \
+linux/rmdir.c linux/setgid.c linux/settimer.c linux/setuid.c linux/signal.c \
+linux/sigprogmask.c linux/symlink.c linux/stat.c linux/time.c linux/unlink.c \
+linux/waitpid.c linux/wait4.c \
+linux/${MES_ARCH}-mes-gcc/_exit.c linux/${MES_ARCH}-mes-gcc/syscall.c \
+linux/${MES_ARCH}-mes-gcc/_write.c \
+math/ceil.c math/fabs.c math/floor.c \
+mes/abtod.c mes/abtol.c mes/__assert_fail.c mes/assert_msg.c \
+mes/__buffered_read.c mes/__init_io.c mes/cast.c mes/dtoab.c \
+mes/eputc.c mes/eputs.c mes/fdgetc.c mes/fdgets.c mes/fdputc.c mes/fdputs.c \
+mes/fdungetc.c mes/globals.c mes/itoa.c mes/ltoab.c mes/ltoa.c \
+mes/__mes_debug.c mes/mes_open.c mes/ntoab.c mes/oputc.c mes/oputs.c \
+mes/search-path.c mes/ultoa.c mes/utoa.c \
+posix/alarm.c posix/buffered-read.c posix/execl.c posix/execlp.c \
+posix/execv.c posix/execvp.c posix/getcwd.c posix/getenv.c posix/isatty.c \
+posix/mktemp.c posix/open.c posix/pathconf.c posix/raise.c posix/sbrk.c \
+posix/setenv.c posix/sleep.c posix/unsetenv.c posix/wait.c posix/write.c \
+stdio/clearerr.c stdio/fclose.c stdio/fdopen.c stdio/feof.c stdio/ferror.c \
+stdio/fflush.c stdio/fgetc.c stdio/fgets.c stdio/fileno.c stdio/fopen.c \
+stdio/fprintf.c stdio/fputc.c stdio/fputs.c stdio/fread.c stdio/freopen.c \
+stdio/fscanf.c stdio/fseek.c stdio/ftell.c stdio/fwrite.c stdio/getc.c \
+stdio/getchar.c stdio/perror.c stdio/printf.c stdio/putc.c stdio/putchar.c \
+stdio/remove.c stdio/snprintf.c stdio/sprintf.c stdio/sscanf.c stdio/ungetc.c \
+stdio/vfprintf.c stdio/vfscanf.c stdio/vprintf.c stdio/vsnprintf.c \
+stdio/vsprintf.c stdio/vsscanf.c \
+stdlib/abort.c stdlib/abs.c stdlib/alloca.c stdlib/atexit.c stdlib/atof.c \
+stdlib/atoi.c stdlib/atol.c stdlib/calloc.c stdlib/__exit.c stdlib/exit.c \
+stdlib/free.c stdlib/mbstowcs.c stdlib/puts.c stdlib/qsort.c stdlib/realloc.c \
+stdlib/strtod.c stdlib/strtof.c stdlib/strtol.c stdlib/strtold.c \
+stdlib/strtoll.c stdlib/strtoul.c stdlib/strtoull.c \
+string/bcmp.c string/bcopy.c string/bzero.c string/index.c string/memchr.c \
+string/memcmp.c string/memcpy.c string/memmem.c string/memmove.c string/memset.c \
+string/rindex.c string/strcat.c string/strchr.c string/strcmp.c string/strcpy.c \
+string/strcspn.c string/strdup.c string/strerror.c string/strlen.c \
+string/strlwr.c string/strncat.c string/strncmp.c string/strncpy.c \
+string/strpbrk.c string/strrchr.c string/strspn.c string/strstr.c string/strupr.c \
+stub/atan2.c stub/bsearch.c stub/chown.c stub/__cleanup.c stub/cos.c \
+stub/ctime.c stub/exp.c stub/fpurge.c stub/freadahead.c stub/frexp.c \
+stub/getgrgid.c stub/getgrnam.c stub/getlogin.c stub/getpgid.c stub/getpgrp.c \
+stub/getpwnam.c stub/getpwuid.c stub/gmtime.c stub/ldexp.c stub/localtime.c \
+stub/log.c stub/mktime.c stub/modf.c stub/mprotect.c stub/pclose.c \
+stub/popen.c stub/pow.c stub/putenv.c stub/rand.c stub/realpath.c stub/rewind.c \
+stub/setbuf.c stub/setgrent.c stub/setlocale.c stub/setvbuf.c stub/sigaction.c \
+stub/sigaddset.c stub/sigblock.c stub/sigdelset.c stub/sigemptyset.c \
+stub/sigsetmask.c stub/sin.c stub/sys_siglist.c stub/system.c stub/sqrt.c \
+stub/strftime.c stub/times.c stub/ttyname.c stub/umask.c stub/utime.c \
+${MES_ARCH}-mes-gcc/setjmp.c"
+
+# --- Pass 1: tcc-boot0-mes -> tcc-boot1 -----------------------------
+rebuild_libc "$TCC" "tcc-boot0-mes"
+build_tcc "$TCC" "tcc-boot1"
+
+# --- Pass 2: tcc-boot1 -> tcc-boot2 (final 0.9.26) ------------------
+TCC=$WORK/tcc-boot1
+rebuild_libc "$TCC" "tcc-boot1"
+build_tcc "$TCC" "tcc-boot2"
+
+echo
+echo "=== stage 3: tcc-boot2 is the final 0.9.26 build ==="
+ls -la "$WORK/tcc-boot1" "$WORK/tcc-boot2"
+CONTAINER_SCRIPT
+
+echo
+echo "=== stage 3 artifacts ==="
+ls -la "$WORK/tcc-boot1" "$WORK/tcc-boot2" 2>/dev/null || \
+ echo "(stage 3 did not complete — see container output above; likely Issue §3)"