Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
832 changes: 832 additions & 0 deletions .agents/docs/2026-10-23-target-base-and-execution-host-design.md

Large diffs are not rendered by default.

148 changes: 148 additions & 0 deletions .agents/docs/2026-10-23-target-base-and-execution-host-repro.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Repro for 2026-10-23-target-base-and-execution-host-design.md §1.
#
# Builds, from the payloads an mcpp home already holds:
# toolchain/ gcc 16.1.0 + binutils 2.42.1 in one prefix, pristine specs
# (a "vendor" gcc: not a path inside mcpp's registry)
# xgcc/ aarch64-linux-musl-gcc 16.1.0 (a cross toolchain carrying a sysroot)
# sysroot-glibc244/ glibc 2.44.3 + linux-headers, crosstool-ng layout
# sysroot-glibc231/ Debian 11 glibc 2.31 (only with FETCH_DEBIAN=1; needs network)
# and runs the scenario matrix A-K against $MCPP (J and K drive the compilers directly:
# mcpp has no spelling for a host sysroot yet).
#
# Usage: MCPP=/path/to/mcpp [FETCH_DEBIAN=1] [WORK=/tmp/sysroot-demo] bash this.sh
set -uo pipefail
MCPP=${MCPP:?set MCPP to the mcpp binary under test}
WORK=${WORK:-/tmp/sysroot-demo}
R=${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs
GCC=$R/xim-x-gcc/16.1.0
BINUTILS=$(ls -d $R/xim-x-binutils/* | tail -1)
GLIBC=$R/xim-x-glibc/2.44.3
HEADERS=$(ls -d $R/xim-x-linux-headers/* | tail -1)
XGCC=$(ls -d $R/xim-x-aarch64-linux-musl-gcc/* | tail -1)
export MCPP_NO_AUTO_INSTALL=1 MCPP_JOBS=${MCPP_JOBS:-8}

mkdir -p "$WORK"
T=$WORK/toolchain S=$WORK/sysroot-glibc244 X=$WORK/xgcc

# ── toolchains ─────────────────────────────────────────────────────────────
rm -rf "$T" "$X"
cp -a "$GCC" "$T"; chmod -R u+w "$T"
rm -f "$T"/lib/gcc/x86_64-linux-gnu/*/specs # the registry copy carries baked specs
mkdir -p "$T/x86_64-linux-gnu/bin"
for t in as ld ld.bfd ar nm ranlib objcopy objdump strip readelf; do
cp -a "$BINUTILS/bin/$t" "$T/bin/$t"
ln -sf "../../bin/$t" "$T/x86_64-linux-gnu/bin/$t"
done
cp -a "$XGCC" "$X"; chmod -R u+w "$X"

# ── sysroots (crosstool-ng layout: usr/include, usr/lib64, lib64 -> usr/lib64) ─
layout() { mkdir -p "$1/usr/include" "$1/usr/lib64" "$1/lib" "$1/usr/lib"; ln -sfn usr/lib64 "$1/lib64"; }
relink_scripts() { # $1 sysroot, $2 sed expression; text-only files (-I)
for f in $(grep -lI "GROUP\|INPUT" "$1"/usr/lib64/*.so 2>/dev/null); do sed -i "$2" "$f"; done
}
rm -rf "$S"; layout "$S"
cp -a "$GLIBC/include/." "$HEADERS/include/." "$S/usr/include/"
cp -a "$GLIBC/lib/." "$S/usr/lib64/"; chmod -R u+w "$S"
relink_scripts "$S" "s#$GLIBC/lib/#/lib64/#g"

if [ "${FETCH_DEBIAN:-0}" = 1 ]; then
O=$WORK/sysroot-glibc231; rm -rf "$O"; mkdir -p "$O/x" "$WORK/dl"
for f in g/glibc/libc6_2.31-13+deb11u11_amd64.deb g/glibc/libc6-dev_2.31-13+deb11u11_amd64.deb \
l/linux/linux-libc-dev_5.10.223-1_amd64.deb; do
[ -f "$WORK/dl/${f##*/}" ] || curl -sSfL -o "$WORK/dl/${f##*/}" "https://deb.debian.org/debian/pool/main/$f"
dpkg-deb -x "$WORK/dl/${f##*/}" "$O/x"
done
layout "$O"
cp -a "$O/x/usr/include/." "$O/x/usr/include/x86_64-linux-gnu/." "$O/usr/include/"
cp -a "$O/x/lib/x86_64-linux-gnu/." "$O/x/usr/lib/x86_64-linux-gnu/." "$O/usr/lib64/"
rm -rf "$O/x"
relink_scripts "$O" "s#/usr/lib/x86_64-linux-gnu/#/lib64/#g; s#/lib/x86_64-linux-gnu/#/lib64/#g"
for l in $(find "$O/usr/lib64" -maxdepth 1 -type l); do
t=$(readlink "$l"); case $t in /lib/x86_64-linux-gnu/*) ln -sfn "$(basename "$t")" "$l";; esac
done
fi

# ── sources ────────────────────────────────────────────────────────────────
GLIBC_MAIN='#include <gnu/libc-version.h>
#include <pthread.h>
import std;
int main() {
std::println("compiled against glibc {}.{}", __GLIBC__, __GLIBC_MINOR__);
std::println("running on glibc {}", gnu_get_libc_version());
#if __GLIBC_PREREQ(2, 42)
std::println("pthread_gettid_np = {}", pthread_gettid_np(pthread_self())); // GLIBC_2.42
#endif
}'
PLAIN_MAIN='import std;
int main() { std::println("hello from a {}-bit target", sizeof(void*) * 8); }'

scenario() { # name, main, manifest-tail, [build args]
local d=$WORK/m/$1; rm -rf "$d"; mkdir -p "$d/src"
printf '%s\n' "$2" > "$d/src/main.cpp"
printf '[package]\nname = "m%s"\nversion = "0.1.0"\n\n%s\n' "$1" "$3" > "$d/mcpp.toml"
echo "================ $1 ${4:-}"; sed -n '5,$p' "$d/mcpp.toml"
( cd "$d" && nice -n 10 "$MCPP" build ${4:-} 2>&1 | grep -v '^\s*\(Index\|Inferred\)' | tail -6 )
local b; b=$(ls "$d"/target/*/*/bin/m$1 2>/dev/null | head -1)
[ -n "$b" ] || return 0
file "$b" | cut -d, -f1-2
readelf -lW "$b" 2>/dev/null | grep -o 'interpreter: [^]]*'
readelf -dW "$b" 2>/dev/null | grep -i 'rpath\|runpath'
file "$b" | grep -q x86-64 && { echo "-- run:"; "$b" 2>&1 | head -3; }
}

scenario A "$GLIBC_MAIN" "[toolchain.linux]
path = \"$T\""
scenario B "$GLIBC_MAIN" "[toolchain.linux]
path = \"$T\"
prefix = \"x86_64-linux-gnu-\""
scenario C "$GLIBC_MAIN" "[toolchain.linux]
path = \"$T\"
sysroot = \"$S\""
scenario D "$GLIBC_MAIN" "[toolchain.linux]
path = \"$T\"
sysroot = \"$S\"

[build]
ldflags = [\"-Wl,--sysroot=$S\"]"
scenario E "$GLIBC_MAIN" "[toolchain.linux]
path = \"$T\"
sysroot = \"$S\"

[build]
ldflags = [\"-Wl,--sysroot=$S\", \"-Wl,--dynamic-linker=$S/lib64/ld-linux-x86-64.so.2\", \"-Wl,-rpath,$S/lib64\"]"
scenario F "$GLIBC_MAIN" "[toolchain]
linux = \"gcc@16.1.0\"

[target.x86_64-linux-gnu]
sysroot = \"$S\""
scenario G "$PLAIN_MAIN" "[toolchain.linux]
path = \"$X\"
prefix = \"aarch64-linux-musl-\""
scenario H "$PLAIN_MAIN" "[toolchain.linux]
path = \"$X\"
prefix = \"aarch64-linux-musl-\"
sysroot = \"$X/aarch64-linux-musl\""
if [ -d "$WORK/sysroot-glibc231" ]; then
scenario I "$PLAIN_MAIN" "[toolchain.linux]
path = \"$T\"
sysroot = \"$WORK/sysroot-glibc231\""
fi

# ── J / K: the host's own root as the sysroot (Debian/Ubuntu multiarch) ─────
# Not through mcpp: `glibc@system` is what the design adds. The ecosystem gcc is
# built without multiarch (-print-multiarch is empty), so J fails until the
# multiarch directories are wired; clang (K) resolves them itself.
LLVM=$(ls -d $R/xim-x-llvm/* 2>/dev/null | tail -1)
TUPLE=$(/usr/bin/gcc -print-multiarch 2>/dev/null); [ -n "$TUPLE" ] || TUPLE=$(uname -m)-linux-gnu
mkdir -p "$WORK/m/J"; printf '%s\n' '#include <gnu/libc-version.h>' '#include <stdio.h>' \
'int main(){printf("compiled against %d.%d, running on %s\n",__GLIBC__,__GLIBC_MINOR__,gnu_get_libc_version());}' > "$WORK/m/J/c.c"
echo "================ J (gcc --sysroot=/)"
"$T/bin/gcc" --sysroot=/ "$WORK/m/J/c.c" -o "$WORK/m/J/plain" 2>&1 | tail -2
echo "-- with the multiarch directories wired:"
"$T/bin/gcc" --sysroot=/ -B/usr/lib/$TUPLE -L/usr/lib/$TUPLE -L/lib/$TUPLE -isystem /usr/include/$TUPLE \
-Wl,-rpath-link,/lib/$TUPLE "$WORK/m/J/c.c" -o "$WORK/m/J/wired" 2>&1 | tail -2 && "$WORK/m/J/wired"
if [ -n "$LLVM" ]; then
echo "================ K (clang --sysroot=/)"
"$LLVM/bin/clang" --no-default-config --sysroot=/ "$WORK/m/J/c.c" -o "$WORK/m/J/clang" 2>&1 | tail -2 && "$WORK/m/J/clang"
fi
4 changes: 3 additions & 1 deletion .agents/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
---
```

335 records.
336 records.

## By subject

Expand Down Expand Up @@ -108,6 +108,7 @@ Records that declare one. Everything else is listed by date below.

### toolchain

- [目标基座与执行宿主:sysroot 的通用模型、运行器契约、自备输入,以及同批的发布产物路径(#832)](2026-10-23-target-base-and-execution-host-design.md) — landed
- [LLVM 23.1.3 Part 2:Linux aarch64 默认工具链与 glibc 生态闭环](2026-10-08-llvm-2313-linux-aarch64-ecosystem-part2-design.md) — active
- [LLVM 23.1.3 全平台统一默认:跨仓库联动方案(mcpp × xim-pkgindex)](2026-10-07-llvm-2313-unified-default-cross-repo-design.md) — active

Expand Down Expand Up @@ -138,6 +139,7 @@ Records that declare one. Everything else is listed by date below.

### 2026-10

- [目标基座与执行宿主:sysroot 的通用模型、运行器契约、自备输入,以及同批的发布产物路径(#832)](2026-10-23-target-base-and-execution-host-design.md) — landed
- [依赖 image 的资源归属、模块扫描边界与锁文件稳定写入](2026-10-21-resource-ownership-module-scan-and-lock-write-design.md) — active
- [首次初始化、home 重定向与网络诊断优化设计](2026-10-19-first-run-home-and-network-design.md) — landed
- [升级的连续性:打包修订不在使用路径上替换载荷、替换失败不拆掉可用状态、home 中的悬空记录自愈、真实的升级 lane](2026-10-17-upgrade-continuity-design.md) — active
Expand Down
33 changes: 33 additions & 0 deletions .github/tools/check_release_paths.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# check_release_paths.sh — a released mcpp names no directory of the machine
# that built it (mcpp#832; design 2026-10-23 §15.3, R4).
#
# mcpp's release profile trims the source paths an artifact carries
# (`trim_paths`), so `__FILE__` in an `assert` of a vendored header reads
# `libs@0.1.0/src/json/json.hpp` rather than
# `/home/runner/work/mcpp/mcpp/modules/libs/src/json/json.hpp`. This gate reads
# the shipped binary and fails on the build machine's checkout -- the source
# paths #832 found. It names exactly that directory: the binary legitimately
# carries `/tmp/` and `/home/` as string literals of its own code, and a
# dynamically linked build's loader and run path name the toolchain store,
# which is what `mcpp pack` rewrites, not a source path.
#
# Usage: bash .github/tools/check_release_paths.sh <binary>
set -uo pipefail
bin="${1:?usage: check_release_paths.sh <binary>}"
[[ -f "$bin" ]] || { echo "FAIL: no binary at $bin"; exit 1; }
roots=()
[[ -n "${GITHUB_WORKSPACE:-}" ]] && roots+=("$GITHUB_WORKSPACE")
[[ -n "${CHECKOUT:-}" ]] && roots+=("$CHECKOUT")
(( ${#roots[@]} > 0 )) || { echo "FAIL: neither GITHUB_WORKSPACE nor CHECKOUT names the checkout"; exit 1; }
fail=0
for r in "${roots[@]}"; do
hits=$(strings -a "$bin" | grep -F -- "$r" | head -5)
if [[ -n "$hits" ]]; then
echo "FAIL: $bin carries the build machine's path $r:"
sed 's/^/ /' <<<"$hits"
fail=1
fi
done
[[ $fail -eq 0 ]] && echo "OK: $bin carries no path of the machine that built it (${roots[*]})"
exit $fail
63 changes: 59 additions & 4 deletions .github/workflows/ci-macos-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,67 @@ jobs:
fi
echo "ok: mcpp run --target aarch64-ios-sim printed 1-2-3 and exited 0"

# THE THIRD ROW, ON A HOST THAT CANNOT RUN IT. A simulator runs the host's
# architecture and this runner is Apple silicon, so the claim is the
# artefact alone -- which is exactly the `preview` tier the row carries.
- name: "x86_64-ios-sim: the artefact, on a host that cannot run it"
# THE THIRD ROW, BUILT HERE AND RUN ELSEWHERE (design 2026-10-23 §2.6,
# S15). A simulator runs the host's architecture and this runner is
# Apple silicon, so this job proves the artefact and the SDK's slice; the
# `ios-intel-run` job below executes it on an Intel Mac. The row can be
# built because the SDK carries the slice -- a fact read from the SDK,
# not from a vendor's hardware roadmap.
- name: "x86_64-ios-sim: the SDK carries the slice, and the artefact is built"
run: |
set -euo pipefail
cd /tmp/iostest
sdk=$(xcrun --sdk iphonesimulator --show-sdk-path)
grep -E '^targets:.*x86_64-ios-simulator' "$sdk/usr/lib/libSystem.tbd" \
|| { echo "FAIL: the simulator SDK carries no x86_64 slice"; exit 1; }
"$MCPP_FRESH" build --target x86_64-ios-sim
/tmp/assert-artefact.sh x86_64-ios-sim x86_64 7 18.0
mkdir -p /tmp/ios-x86 && cp /tmp/iostest/target/x86_64-ios-sim/*/bin/iostest /tmp/ios-x86/
- uses: actions/upload-artifact@v4
with:
name: x86_64-ios-sim-iostest
path: /tmp/ios-x86/iostest
retention-days: 3

# S15: AN ARTEFACT BUILT ON APPLE SILICON RUNS ON AN INTEL MAC'S SIMULATOR.
#
# "Built here, executed there", with CI's artifact store as the transport.
# This job runs no mcpp -- there is no Intel macOS mcpp, and none is needed:
# it boots an iOS simulator of the host's architecture and spawns the
# program, comparing its own output and exit status. Passing it is what the
# row's `verified` tier states.
ios-intel-run:
name: iOS - x86_64-ios-sim runs on an Intel Mac
needs: ios-engine
runs-on: macos-15-intel
timeout-minutes: 30
steps:
- uses: actions/download-artifact@v4
with:
name: x86_64-ios-sim-iostest
path: /tmp/ios-x86
- name: "x86_64-ios-sim: the simulator runs the artefact and returns its status"
run: |
set -euo pipefail
art=/tmp/ios-x86/iostest
chmod +x "$art"
file "$art"
udid=$(xcrun simctl list devices available -j | python3 -c '
import json, sys
d = json.load(sys.stdin)["devices"]
for runtime, devs in sorted(d.items(), reverse=True):
if "iOS-18" not in runtime and "iOS-2" not in runtime: continue
for dev in devs:
if dev.get("isAvailable") and "iPhone" in dev["name"]:
print(dev["udid"]); sys.exit(0)
sys.exit(1)')
echo "device $udid"
xcrun simctl boot "$udid" 2>/dev/null || true
xcrun simctl bootstatus "$udid" -b
set +e
out=$(xcrun simctl spawn "$udid" "$art" 2>&1); rc=$?
set -e
printf '%s\n' "$out"
[ "$rc" -eq 0 ] || { echo "FAIL: the program exited $rc in the simulator"; exit 1; }
grep -qx '1-2-3' <<<"$out" || { echo "FAIL: the program's output line '1-2-3' is absent"; exit 1; }
echo "ok: x86_64-ios-sim built on arm64 ran on an Intel simulator and printed 1-2-3"
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ jobs:
env -u MCPP_HOME "$ROOT/mcpp" --version | grep -q "$VERSION"
# MCPP_HOME should auto-resolve to the extracted root.
out=$(env -u MCPP_HOME "$ROOT/bin/mcpp" self env)
# R4 (mcpp#832): the binary pack rebuilt with this release's own
# engine trims the paths it carries.
bash .github/tools/check_release_paths.sh "$ROOT/bin/mcpp"
echo "$out" | grep -q "MCPP_HOME *= *$ROOT"

- name: Generate source tarball + xpkg.lua via mcpp publish
Expand Down Expand Up @@ -612,6 +615,9 @@ jobs:
tar -xzf "dist/${TARBALL_NAME}" -C "$SMOKE"
"$SMOKE/$WRAPPER/bin/mcpp" --version
"$SMOKE/$WRAPPER/mcpp" --version | grep -q "$VERSION"
# R4 (mcpp#832): stage 2 is this release's own engine, and it trims
# the paths the binary carries.
bash .github/tools/check_release_paths.sh "$SMOKE/$WRAPPER/bin/mcpp"

- name: Upload macOS artifacts to release
uses: softprops/action-gh-release@v2
Expand Down
74 changes: 74 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,80 @@
> Each `## [<version>]` section is that release's notes. Entries are written in English
> from 2026.9.28.3 on; earlier entries remain as written.

## [2026.10.23.1] - 2026-10-23

### Added

- **The target base.** `[target.<triple>].sysroot` takes a package, a locator
(`glibc@system`, `musl@system`, `macosx|iphoneos|iphonesimulator@system|<version>`,
`msvc@…`), a table `{ path = "<dir>", provides = [...] }` relative to the file
that writes it, or `""`. One statement is resolved once and every reader uses
it: compile and link lines, the scan, the `std` module, the artifact's loader
and run path, closure validation, the fingerprint, the reports, `mcpp pack` and
the runner. Statements come from `--sysroot` / `MCPP_TARGET_<TRIPLE>_SYSROOT` /
`MCPP_SYSROOT`, then the project, then `[target.<triple>]` in `config.toml`
(the user's, then the system's), then the target row.
- The C library a base supplies is measured: the driver says where the C runtime
is, the preprocessor which library and version. Debian multiarch is supplied to
a gcc that does not know it, so `glibc@system` builds with gcc and clang alike.
A base older than the compiler's runtime requires is refused before compiling,
naming both versions (`libstdc++.so.6: GLIBC_2.38` against glibc 2.31).
- A native artifact binds its base: PT_INTERP and RUNPATH name the base's loader
and C library, and closure validation checks against it. The base's identity
enters the fingerprint, so a sysroot rewritten in place rebuilds. For such a
build `mcpp pack` states the highest glibc the program and its packed
libraries need and writes it to `HOST-REQUIREMENTS`.
- `Using sysroot glibc 2.31 ← third_party/sysroot [custom · mcpp.toml:12]`,
`mcpp why sysroot`, and `base` / `execution` in `resolution.json`.
`--managed-only` names a stated base. A new refusal code, `target-base`.
- `SDKROOT` names an Apple SDK on every host, so an SDK produced from one's own
Xcode on Linux is located as one in Xcode is. An SDK whose `libSystem.tbd`
lacks the row's slice is refused naming it. The clang `*-windows-msvc` row
takes `{ path }` for an unpacked Visual Studio tree or a `/winsysroot` layout.
- **The execution host.** `[target.<triple>].run_under` (also in `config.toml`,
overridden by `MCPP_TARGET_<TRIPLE>_RUN_UNDER`, `none` clears) is composed in
front of the runner. Runners receive `MCPP_RUN_TRIPLE`, `MCPP_RUN_KIND`,
`MCPP_RUN_MODE`, `MCPP_RUN_ID`, `MCPP_RUN_CLOSURE` (a versioned JSON closure)
and `MCPP_SYSROOT`; their exit status is the program's. An Apple artifact on a
host that is not macOS is refused before anything runs, naming `run_under`.
- A toolchain phase may state `sysroot` beside `spec`.
- Profile keys `ndebug` and `trim_paths`, on by default for `release` and `dist`.
`trim_paths` maps each package's sources to `<name>@<version>/…` and the store,
home, build directory, toolchain and sysroot to `/mcpp/…`, and writes
`source-map.gdbinit` / `.lldbinit` when there is debug information. It
applies to what `build`, `run` and `pack` produce; `mcpp test` keeps paths,
because a test binary finds its fixtures through `__FILE__`.
- `x86_64-ios-sim` is `verified`: CI builds it on Apple silicon and runs it on
an Intel Mac's simulator.

### Changed

- **`release` and `dist` define `NDEBUG` and trim source paths.** `assert`
compiles out there, and an artifact no longer carries the directories of the
machine that built it (mcpp#832). Write `ndebug = false` or `trim_paths =
false` under the profile to keep the previous artifact; the first shipping
build after the upgrade says so once. Both profiles rebuild once; `dev` does
not.

### Fixed

- A stated sysroot hands the linker `--sysroot` (gcc's clean link specs drop
the driver's), so a sysroot's linker scripts resolve inside it (`cannot find
/lib64/libm.so.6`). The ecosystem's default build is unchanged.
- A stated sysroot no longer produces an artifact that loads the host's loader
and the SubOS farm's C library, and closure validation no longer compares a
stated base with the ecosystem's glibc.
- mcpp no longer writes into a sysroot it does not own (a toolchain named by
path reports its vendor's).
- A toolchain phase that states `spec` with `prefix`, `launcher`, `family` or
`tool.<role>` is refused by name; those keys were dropped without a word.
- A command printed for pasting quotes a path that holds a space
(`mcpp self init --force --home '/tmp/mc probe/home'`, mcpp#832).
- A fresh home whose path holds a space builds the default project (with
openxlings/xim-pkgindex#1032 and #1035). Under a path outside ASCII, gcc's
module mapper cannot precompile `std`; mcpp now names that cause and the two
ways out after the error.

## [2026.10.22.1] - 2026-10-22

### Fixed
Expand Down
Loading
Loading