Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5875a53
fix(build): import std 的门该问「有没有」,不该问「是不是 freestanding」
speak-agent Aug 22, 2026
5210c5b
feat(build): 一个包可以带自己的 std 模块源
speak-agent Aug 22, 2026
d8fd41b
feat(build): 那个「不是永久的」的点到了 —— 异常与 RTTI 由图决定
speak-agent Aug 22, 2026
ded81c6
fix(build): 两处「由图决定」的续篇,和一条让改动看起来没生效的指纹缺陷
speak-agent Aug 22, 2026
2a35e6f
fix(build): 裸机上异常抛不出去,真因是编译器为这类目标默认关掉了展开表
speak-agent Aug 22, 2026
00d33a8
feat(build): 交叉一直靠「载荷的 driver 本身就是交叉 driver」,而 openkal 不是那样
speak-agent Aug 22, 2026
5f0e9ff
feat(toolchain): openkal-llvm —— 一个工具链,交叉到每一个实现了 openkal 的平台
speak-agent Aug 22, 2026
37e46de
fix(build): 目标侧来自图时,宿主的链接模型要一处不剩地让开
speak-agent Aug 22, 2026
5e3e1d9
fix(build): PE 的链接行少了 --target;以及两个 flag 决定「编译成什么」却写在包里
speak-agent Aug 22, 2026
6b62775
fix(build): 链接行的三支是按「宿主」分的,而目标侧来自图时三支都不对
speak-agent Aug 22, 2026
45dd44a
feat(build-program): mcpp::compiler() —— 构建程序不该靠猜工具链是谁
speak-agent Aug 22, 2026
e9677b9
fix(std-module): Windows 宿主那条分支不带 extraFlags —— 包自己的头全在里面
speak-agent Aug 23, 2026
456d8b3
fix(macos): 自包含镜像不该把一千多个 libc++ 内部符号交给加载器去合并
speak-agent Aug 23, 2026
1ccf979
fix(macos+windows-host): 两条只在「构建发生在哪台机器上」时才成立的假设
speak-agent Aug 23, 2026
553466c
fix(dist): 产物格式的判据匹配不到 mcpp 自己用的三元组拼法
speak-agent Aug 23, 2026
09a58a4
fix(dist): C++ 运行时已经在对象里时,不该再去找一个来链
speak-agent Aug 23, 2026
9a5b9ec
fix(dist): 图供给 C++ 运行时时,契约是 SelfContained,由对象兑现而不是由点名一个库
speak-agent Aug 23, 2026
ebbe2ff
chore: 删掉 main 上的 binDir —— 一个 shell 重定向留下的空文件
speak-agent Aug 23, 2026
97ba75a
fix(windows-host): 整条 -static 是目标的性质,该出现在每一种宿主的链接行上
speak-agent Aug 23, 2026
19e5fcd
fix(windows-host): --target= 挂在「载荷有没有 cfg 文件」上,而那不是它的性质
speak-agent Aug 23, 2026
aa891f8
feat: MCPP_TARGET_REQUESTED,以及 openkal 的 3×3 交叉验证工作流
speak-agent Aug 23, 2026
ae9d18b
ci(openkal-cross): 用仓库自己的 bootstrap-mcpp,而不是再写一套
speak-agent Aug 23, 2026
f1be1c7
fix(std-module): 一个字符串承载了两个事实,于是第二条命令收到十九个用不上的 flag
speak-agent Aug 23, 2026
671a68a
test: 三个纯函数的单元测试 —— 判据本来在四十分钟外,现在在一秒内
speak-agent Aug 23, 2026
a6a0154
test(e2e): 267 —— MCPP_TARGET_REQUESTED 的契约,以及它为什么不是 MCPP_TARGET
speak-agent Aug 23, 2026
f019980
fix(e2e 267): 我选的目标在 macOS 上没有载荷 —— 那是假设不是实测
speak-agent Aug 23, 2026
084cd74
refactor(dist): 产物格式的判据从一个 lambda 提成 format_for,并给它断言
speak-agent Aug 23, 2026
ea7c434
fix(std-module): Windows 分支把「保留模块名」的抑制挂在 .ixx 上,而它不该有条件
speak-agent Aug 23, 2026
9856cde
fix(resolve): git 依赖的两个网络步骤都没有重试 —— 一次瞬时故障让整个构建红
speak-agent Aug 23, 2026
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
224 changes: 224 additions & 0 deletions .github/workflows/openkal-cross.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
name: openkal cross-build (3 hosts × 3 targets)

# ⭐⭐ WHAT THIS WORKFLOW ASSERTS, AND WHY IT IS A MATRIX RATHER THAN A ROW.
#
# `cross-build-test.yml` verifies the crosses served by a PAYLOAD: a toolchain
# whose driver has exactly one target. There the host and the target are joined
# — `x86_64-w64-mingw32-g++` is the Windows cross and nothing else — so one row
# per supported combination is the honest shape.
#
# openkal changes the shape of the question. The target side — the C library,
# the C++ runtime, the platform's own implementation — is a set of PACKAGES in
# the dependency graph, and the compiler is an ordinary retargetable clang. The
# claim that follows is that N hosts × N targets collapses to N implementations
# plus one tool: **the machine doing the building stops being a variable.**
#
# ⚠️ THAT IS A CLAIM, AND CLAIMS OF THIS SHAPE HAVE BEEN WRONG IN THIS
# REPOSITORY. Reaching PE from a Linux host needed four separate repairs, and
# adding the other two hosts found seven more — every one of them a decision
# that had been keyed on which machine was building rather than on which machine
# the output was for:
#
# the link line's three host-shaped branches, only one of which carried
# `--target=`; the `std` module command's Windows branch, which dropped the
# package's own include paths; `cd X && …` not changing the drive in cmd.exe;
# the artefact-format test matching LLVM's `apple` rather than mcpp's `macos`;
# the C++ runtime contract naming a library to link when one was already in
# the objects; `-nostdinc` missing so a host SDK header could be found; and
# `-lgcc` naming GCC's runtime on a link whose compiler is clang.
#
# None of those was visible from one host. So the matrix is the test.
#
# ── The shape ──────────────────────────────────────────────────────────────
#
# THREE build jobs, one per host, each producing THREE artefacts — nine builds.
# THREE run jobs, one per system, each executing the artefact FOR that system
# produced by ALL THREE hosts.
#
# build on Linux build on macOS build on Windows
# run Linux ✓ ✓ ✓
# run macOS ✓ ✓ ✓
# run Windows ✓ ✓ ✓
#
# ⭐ The diagonal is an ordinary native build. The six off-diagonal cells are
# the claim, and they are what a single-host workflow cannot reach.
#
# ⚠️ THE RUN JOBS INSTALL NOTHING — not mcpp, not a compiler, not a C runtime.
# A program above openkal carries its C library, its C++ runtime and its
# unwinder; what remains is the operating system it was built for. If a
# toolchain step is ever added to one of them because "the program needs it",
# that is the finding rather than the fix.
#
# ⚠️ AND THE ASSERTION IS ON THE OUTPUT, NOT THE EXIT STATUS. The program prints
# four lines, and `unwound: true` is the one a link cannot fake: it says a
# destructor ran while an exception was being carried out of a frame, which
# means the unwinder found this image's own frame descriptions.

on:
pull_request:
workflow_dispatch:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
# ⚠️ No mcpp or xlings version here. `bootstrap-mcpp` owns both, and a second
# statement of them is a second thing to keep in step — the pin check
# (.github/tools/check_version_pins.sh) enforces the ones that exist and would
# not know about a copy in this file.
XLINGS_NON_INTERACTIVE: '1'
# The branch of the openkal packages this change is verified against. They
# move together with it; when they are on `main` this becomes `main`.
OPENKAL_BRANCH: feat/openkal-closure

jobs:
build:
name: build 3 targets on ${{ matrix.host }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- { host: linux, runner: ubuntu-24.04 }
- { host: macos, runner: macos-14 }
- { host: windows, runner: windows-2022 }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4

# ⚠️ THE REPOSITORY'S OWN BOOTSTRAP, NOT A SECOND ONE.
#
# This job first wrote its own: fetch xlings, then
# `xlings install mcpp@<version>`. It failed on the very first run:
#
# xlings: version '2026.8.17.1' not found for 'mcpp'
# available: 2026.8.19.4
#
# `.xlings.json` at this repository's root pins the mcpp that BUILDS mcpp,
# and that pin does not move when mcpp is released — so it names a version
# the index no longer carries, and a bare install inside the checkout
# obeys the pin rather than the argument. `bootstrap-mcpp` already knows
# this (it runs `install_pinned_mcpp.sh`), works on all three systems, and
# shares the cache lineage every other job lands on.
#
# ⇒ Two bootstraps would be two things to keep correct, and the second one
# was wrong within a day of being written.
- uses: ./.github/actions/bootstrap-mcpp

# ⭐ THE mcpp UNDER TEST. Everything after this uses the binary this step
# produces; the bootstrapped one above is only what compiles it.
- name: Build the mcpp in this pull request
run: |
set -euo pipefail
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true
"$MCPP" self config --mirror GLOBAL 2>/dev/null || true
# ⚠️ `--dev` and not `--release`. What is under test is a set of
# decisions about compile and link flags; an optimisation level
# changes none of them and a release self-build is most of the budget
# of a job on a two-core runner.
"$MCPP" build --dev
BUILT=$(find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) | head -1)
[ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; }
BUILT=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT")
echo "MCPP_UNDER_TEST=$BUILT" >> "$GITHUB_ENV"
"$BUILT" --version

- name: Select the toolchain the openkal packages ask for
run: |
set -euo pipefail
# ⚠️ Install, then select. `toolchain default` names one and does not
# fetch it.
"$MCPP_UNDER_TEST" self config --mirror GLOBAL 2>/dev/null || true
"$MCPP_UNDER_TEST" toolchain install llvm 22.1.8
"$MCPP_UNDER_TEST" toolchain default 'llvm@22.1.8'

- name: The program — one source, three targets
run: |
set -euo pipefail
git clone --quiet --depth 1 -b "$OPENKAL_BRANCH" \
https://github.com/mcpplibs/openkal-llvm-runtime "$RUNNER_TEMP/okl"
cd "$RUNNER_TEMP/okl/examples/same-source"
mkdir -p "$RUNNER_TEMP/out"
# ⚠️ The three HOSTED targets. Bare metal is verified by
# `openkal-llvm-runtime`'s own CI under qemu; it has no runner here to
# execute on, and a build-only cell in a workflow whose point is
# running would be the weaker claim.
for t in x86_64-linux-gnu aarch64-macos x86_64-windows-gnu; do
rm -rf target
"$MCPP_UNDER_TEST" build --target "$t"
a=$(find target -type f \( -name 'openkal-same-source' -o -name 'openkal-same-source.exe' \) | head -1)
[ -n "$a" ] || { echo "::error::$t produced no artefact on ${{ matrix.host }}"; exit 1; }
case "$t" in
x86_64-windows-gnu) cp "$a" "$RUNNER_TEMP/out/windows.exe" ;;
aarch64-macos) cp "$a" "$RUNNER_TEMP/out/macos" ;;
*) cp "$a" "$RUNNER_TEMP/out/linux" ;;
esac
echo "${{ matrix.host }} → $t : $(ls -l "$a" | awk '{print $5}') bytes"
done

- uses: actions/upload-artifact@v4
with:
name: openkal-built-on-${{ matrix.host }}
path: ${{ runner.temp }}/out/
if-no-files-found: error

run:
name: run 3 builds on ${{ matrix.system }}
needs: build
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- { system: linux, runner: ubuntu-24.04, file: linux }
- { system: macos, runner: macos-14, file: macos }
- { system: windows, runner: windows-2022, file: windows.exe }
defaults:
run:
shell: bash
steps:
# ⚠️ NO checkout AND NO toolchain. This job is the claim: a program built
# above openkal needs the operating system it was built for and nothing
# else. Anything installed here would weaken what a pass means.
- uses: actions/download-artifact@v4
with: { pattern: openkal-built-on-*, path: art }

- name: The same program, from all three build hosts
run: |
set -euo pipefail
fail=0
for host in linux macos windows; do
bin="art/openkal-built-on-$host/${{ matrix.file }}"
echo "──────── built on $host, running on ${{ matrix.system }} ────────"
if [ ! -f "$bin" ]; then
echo "::error::$bin is missing"; fail=1; continue
fi
# ⚠️ The executable bit does not survive an artefact upload.
chmod +x "$bin" || true
# ⚠️ arm64 macOS refuses an unsigned image, so the signature is
# asserted before the run: a failure here is "the linker did not
# ad-hoc sign it", which is a different repair from "it crashed".
if [ "${{ matrix.system }}" = "macos" ]; then
codesign -dv "$bin" 2>&1 | grep -q 'adhoc\|Signature' \
|| { echo "::error::built on $host: no code signature"; fail=1; continue; }
fi
if ! "./$bin" > out.log 2>&1; then
echo "::error::built on $host: it did not run"; cat out.log; fail=1; continue
fi
cat out.log
ok=1
grep -q 'sorted: 2 4 7' out.log || ok=0
grep -q 'caught: 42' out.log || ok=0
# ⭐ The line a link cannot fake.
grep -q 'unwound: true' out.log || ok=0
grep -q 'import std over openkal: ok' out.log || ok=0
[ "$ok" = 1 ] || { echo "::error::built on $host: wrong output"; fail=1; }
done
[ "$fail" = 0 ] || exit 1
echo "three builds, one system, same four lines"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,9 @@ bench/bench-report.json
# --project mode writes the measured build's stdout/stderr next to the project
bench-child.log
.mcpp.toml.bench-backup

# ⚠️ An empty file a shell redirection left behind. It was committed in #436
# and lived on `main` for a week; the name reads like a variable that was meant
# to be expanded (`> $binDir`) and was not. Listed so the same slip is caught
# next time rather than reviewed again.
binDir
Empty file removed binDir
Empty file.
41 changes: 41 additions & 0 deletions src/build/build_program.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ struct BuildProgramEnv {
// hostprogram::toolchain_dir / sysroot_dir for why declaring was wrong.
std::string toolchainDir;
std::string targetSysroot;
// ⭐⭐ WHICH COMPILER RESOLVED — "gcc" | "clang" | "msvc" | "".
//
// A package should never have to guess this, and until this field existed
// the only way to was to look at `toolchainDir` and recognise a directory
// name. The question is real and recurring: the routines a compiler emits
// calls to and no C library defines live in `libgcc.a` under one and in
// compiler-rt under another, and the tool that turns a `.def` into an
// import library is `dlltool` under one and `llvm-dlltool` under another.
//
// ⚠️ Measured 2026-08-22, both on the same day and both from the same
// missing answer: `openkal-musl` naming `-lgcc` on a link whose compiler was
// clang (`unable to find library -lgcc`), and `openkal-windows` running
// `llvm-dlltool` under a GCC toolchain (`sh: 1: llvm-dlltool: not found`).
// Each package had made the assumption its author's toolchain made true.
std::string compilerId;
// Three more answers a board-support package would otherwise hardcode.
//
// ⚠️ THE COUPLING THESE REMOVE IS INVISIBLE IN A MANIFEST. `riscv-virt-rt`
Expand Down Expand Up @@ -302,6 +317,31 @@ contract_env(const fs::path& root, const fs::path& outDir, const BuildProgramEnv
std::vector<std::pair<std::string, std::string>> e;
auto hostT = mcpp::toolchain::triple::host_triple().str();
e.emplace_back("MCPP_TARGET", env.targetTriple.empty() ? hostT : env.targetTriple);
// ⭐⭐ THE SAME VALUE UNFILLED — EMPTY WHEN NOBODY NAMED A TARGET.
//
// `MCPP_TARGET` above answers "which machine is this for", and filling it
// in with the host is right for that question. It cannot answer a different
// one that a platform package has to ask: **was this build POINTED at a
// target**, or is it an ordinary native build?
//
// The two are not the same even when the triples are equal. `mcpp build
// --target aarch64-macos` on an arm64 Mac names the same machine the host
// is, and yet it is the graph that supplies the target side — so this tool
// puts no system SDK on the link, and the package that knows the system is
// the only thing that can name one. A native build on the same machine gets
// the SDK and needs nothing from the package.
//
// ⚠️ Measured 2026-08-23, `openkal-macos` trying to decide this from what
// was available. From the host: right for the cross, wrong for
// `--target aarch64-macos` ON a Mac (`library not found for -lSystem`).
// From `MCPP_TARGET`: right for the cross, wrong for the native build,
// because it is never empty (`undefined symbol: wcslen`, `strtoul`, … —
// the package's three-name stub had shadowed the vendor's complete one).
//
// ⭐ An older mcpp sets neither, and that is the correct answer for it:
// it has no graph-supplied target side, so the system is always on the
// link and a package should supply nothing.
e.emplace_back("MCPP_TARGET_REQUESTED", env.targetTriple);
// Convenience splits of the resolved target (Cargo CARGO_CFG_TARGET_*
// parity): parsed ONCE here through the canonical triple parser so every
// build.mcpp stops hand-splitting MCPP_TARGET. MCPP_TARGET_ENV is "" when
Expand All @@ -322,6 +362,7 @@ contract_env(const fs::path& root, const fs::path& outDir, const BuildProgramEnv
// absent variable would make the answer depend on whatever the parent
// process happened to export.
e.emplace_back("MCPP_TOOLCHAIN_DIR", env.toolchainDir);
e.emplace_back("MCPP_COMPILER", env.compilerId);
e.emplace_back("MCPP_TARGET_SYSROOT", env.targetSysroot);
e.emplace_back("MCPP_TARGET_BUILTINS_LIB", env.targetBuiltinsLib);
e.emplace_back("MCPP_TARGET_LIBC_PROFILE", env.targetLibcProfile);
Expand Down
63 changes: 62 additions & 1 deletion src/build/distribution.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
export module mcpp.build.distribution;

import std;
import mcpp.toolchain.triple;

export namespace mcpp::build::dist {

Expand Down Expand Up @@ -102,6 +103,45 @@ enum class Contract {
// priority-ordered initializer section, PE has no rpath, ELF has both.
enum class Format { Elf, MachO, Pe };

// ⭐⭐ WHICH FORMAT A TARGET PRODUCES, ASKED OF THE TARGET.
//
// `hostFallback` is what a triple outside the vocabulary falls back to, and it
// is a parameter rather than a compile-time constant so that this function can
// be examined without being the machine it is about.
//
// ⚠️ THIS USED TO BE A LAMBDA INSIDE A FIFTEEN-HUNDRED-LINE FUNCTION, AND THAT
// IS WHY IT HAD NO TEST. It tested the triple for the substrings `apple` and
// `darwin`, which are LLVM's words; mcpp's canonical form is `aarch64-macos`
// and contains neither, so the test fell through to a question about the HOST
// and produced opposite errors on opposite hosts:
//
// Linux host, macOS target → an ELF contract for a Mach-O
// macOS host, Linux target → a Mach-O contract for an ELF, which is
// `ld.lld: error: unable to find library
// -load_hidden` plus the host's own libc++.a on
// an ELF link line
//
// Both were found by running three hosts against three targets. Either would
// have been found by four lines of assertion, once this was a function.
//
// The substring tests remain as a fallback for a triple the vocabulary cannot
// parse — the `[target.X]` escape hatch — where a spelling is all there is.
Format format_for(std::string_view targetTriple, Format hostFallback) {
if (auto parsed = mcpp::toolchain::triple::parse(targetTriple)) {
if (parsed->is_pe()) return Format::Pe;
if (parsed->os == "macos") return Format::MachO;
if (parsed->os == "linux"
|| parsed->os == "none") return Format::Elf;
}
if (targetTriple.find("windows") != std::string_view::npos
|| targetTriple.find("mingw") != std::string_view::npos)
return Format::Pe;
if (targetTriple.find("apple") != std::string_view::npos
|| targetTriple.find("darwin") != std::string_view::npos)
return Format::MachO;
return hostFallback;
}

std::string_view to_string(Contract c) {
switch (c) {
case Contract::SelfContained: return "self-contained";
Expand Down Expand Up @@ -256,6 +296,27 @@ struct MechanismInput {
// (measured 2026-08-19). A target-side C++ runtime, if one is wanted, is
// an ordinary package — the same way the libc is.
bool freestanding = false;
// ⭐⭐ THE HOSTED FORM OF THE LINE ABOVE: a package in the graph supplies
// the C++ runtime, built for this target, and its objects are already on
// the link line.
//
// The table below has three answers and all of them name a runtime to LINK
// — the system's, the toolchain's, or a static form of one. Each is right
// when the runtime is something the artifact has to be JOINED to, and each
// is wrong here, where it is already inside. The archives it would find are
// the host's, which is the same defect the `freestanding` flag above
// exists for; the difference is only that this target has an OS.
//
// ⚠️ Measured 2026-08-23, cross-building for `aarch64-macos` over openkal
// right after the format decision was corrected to key on the target — the
// wrong format had been masking this:
//
// ld64.lld: error: library not found for -lc++
//
// ⇒ Not "pick openkal's here". openkal's IS the objects; there is no
// library to name, and the honest flag is the one that stops the driver
// from adding its own.
bool graphCxxRuntime = false;
};

struct Mechanism {
Expand Down Expand Up @@ -351,7 +412,7 @@ Mechanism resolve(const MechanismInput& in) {
// ELF here, and every ELF cell below reaches for the toolchain's HOST
// archives. One of them silently produced a link line with
// x86-64 libc++.a on a riscv64 link.
if (in.freestanding) {
if (in.freestanding || in.graphCxxRuntime) {
m.effective = Contract::SelfContained;
m.unitFlags = " -nostdlib++";
return m;
Expand Down
Loading
Loading