Skip to content

Commit 6b62775

Browse files
committed
fix(build): 链接行的三支是按「宿主」分的,而目标侧来自图时三支都不对
if constexpr (is_windows) { … } // 这台机器是 Windows else if constexpr (needs_explicit_libcxx) { … } // 这台机器是 macOS else { … } // 这台机器是 Linux 三个答案各自描述「在那台机器上怎么链接」:一个 SDK 路径、一个部署目标、一条 加载器搜索路径、这台机器的 libatomic。目标是宿主自己、或由载荷伺候时,它们都对。 ⚠️ 而**只有第三支消费 link_toolchain_flags**,`--target=` 就在里面。⇒ 从 Linux 宿主交叉编译 openkal 目标链接正确,从 macOS 或 Windows 宿主则会把一个 Mach-O 或 ELF 递给一个没被告知目标的链接器。那种失败的样子已经有记录 —— PE 那条从另一个 方向撞到了同一堵墙: ld.lld: error: obj/…/types.m.o: unknown file type (× 30) ⭐ 这一条不是靠 CI 发现的,是**改完 PE 之后主动去找第二条通道**找到的。风险表 R4 写着「这个形状已经出现三次,应当在改动后主动找第二条」。 ── 修法:整条替换,而不是编织进去 ────────────────────── 文件自己给了先例 —— freestanding 那块的注释:「Applied LAST and by REPLACEMENT rather than woven in above … 先前做的每个 hosted 链接决定不只是多余,而是错的, 往一条已经带着它们的命令行后面追加 -nostdlib,会让结果取决于驱动的 flag 顺序而 不是取决于任何人做的决定。」目标侧来自图是同一种情况。 ⭐ 而且它**替换掉了一个特例而不是新增一个**:PE 分支里我先前为一种格式加的那份 拷贝去掉了,现在一条规则覆盖 PE / Mach-O / ELF,在每一种宿主上。 留下的,以及每一条为什么不是宿主的: full_static 契约表的,按目标的**格式**取 link_toolchain_flags --target= / --no-default-config / -fuse-ld=lld link_intent_ld 用户要建的是什么(exe/shared/static) user_ldflags 清单自己的话 link_extra -flto / -s 去掉的:b_flag(这台机器的 binutils)、runtime_dirs 和随它的 -rpath、payload_ld、 atomic_ld。 ⭐ 实测收益不止于宿主维度 —— macOS 产物的链接行上原本有 -L…/xim-x-llvm/22.1.8/lib/x86_64-unknown-linux-gnu -Wl,-rpath,…/lib/x86_64-unknown-linux-gnu ld64 接受 -rpath 并把它写进镜像。修完之后 LC_RPATH 为空。 ── ⚠️ 谓词收窄:两个条件,不是一个 ─────────────────────── 第一版写成 !crossTargetFlag.empty(),太宽:它对**每一个**被指向 hosted 目标的 可重定向 clang 都成立,包括由载荷伺候的 musl / glibc 交叉 —— 那些仍然需要这台 机器的 -B、runtime 目录和 C 运行时 flag,因为对它们来说载荷就是目标侧。 targetCxxRuntime 单独用则朝另一个方向太宽:它只说「有个包提供 C++ 运行时」, 而那对该包的**本机**构建同样成立,那里载荷的链接模型是对的。 两个一起才是这次替换所依赖的事实:C 库 / C++ 运行时 / 平台都是包,**并且**我们 把编译器指向了一个不是这台机器的目标。 实测(收窄后重跑):四个目标全部产出,三个能在本机跑的输出逐字相同;另造一个 非 openkal 的 --target x86_64-linux-musl 最小工程,走的仍是旧路径,产物能跑。
1 parent 5e3e1d9 commit 6b62775

1 file changed

Lines changed: 77 additions & 27 deletions

File tree

src/build/flags.cppm

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,27 @@ CompileFlags compute_flags(const BuildPlan& plan) {
533533
? std::string{}
534534
: " " + plan.toolchain.crossTargetFlag;
535535
const bool isClangWithCfg = dm.hasCfg;
536+
537+
// ⭐⭐ THE TARGET SIDE COMES FROM THE DEPENDENCY GRAPH, STATED ONCE.
538+
//
539+
// ⚠️ TWO CONDITIONS AND NOT ONE, and each excludes a case the other admits.
540+
//
541+
// `crossTargetFlag` alone is too wide: it is set for EVERY hosted target a
542+
// retargetable clang is pointed at, including the musl and glibc crosses
543+
// served by a payload — and those still need this host's `-B`, its runtime
544+
// directories and its C-runtime flags, because for them the payload IS the
545+
// target side.
546+
//
547+
// `targetCxxRuntime` alone is too wide the other way: it says a package
548+
// supplies a C++ runtime, which is also true of a NATIVE build of such a
549+
// package, where the payload's link model is right and dropping it would
550+
// remove a working link.
551+
//
552+
// Together they say the thing this replacement depends on: the C library,
553+
// the C++ runtime and the platform are packages, AND we are pointing the
554+
// compiler at a target that is not this machine.
555+
const bool graphTargetSide = plan.toolchain.targetCxxRuntime
556+
&& !plan.toolchain.crossTargetFlag.empty();
536557
// LLVM root of a clang-with-cfg toolchain — used by the macOS link
537558
// path below to locate libc++.a/libc++abi.a for staticStdlib.
538559
std::filesystem::path llvmRootForStdlib;
@@ -1201,42 +1222,25 @@ CompileFlags compute_flags(const BuildPlan& plan) {
12011222
// Windows MinGW build (host≠target). No rpath/loader/payload model. Static
12021223
// + libstdc++exp (std::print's __open_terminal/__write_to_terminal live in
12031224
// libstdc++exp.a, not plain libstdc++). Self-contained binutils → no -B.
1204-
if (isMingwTc) {
1205-
// ⭐⭐ AND THE SAME SENTENCE THAT QUALIFIES THE PARAGRAPH ABOVE: that
1206-
// holds while every PE cross is served by a MinGW PAYLOAD, whose driver
1207-
// IS the target and therefore needs nothing said to it. When the target
1208-
// side comes from the dependency graph the compiler is an ordinary
1209-
// retargetable clang, and this early return was dropping the one flag
1210-
// that tells it which target to link for.
1211-
//
1212-
// ⚠️ Measured 2026-08-23, `--target x86_64-windows-gnu` over openkal —
1213-
// every object compiled, and then:
1214-
//
1215-
// ld.lld: error: obj/…/types.m.o: unknown file type (× 30)
1216-
//
1217-
// COFF objects handed to lld's ELF driver, because the compile line
1218-
// carried `--target=` and the link line did not. Thirty accurate
1219-
// messages, none of which names the missing flag.
1220-
//
1221-
// ⇒ Third time this shape has appeared (payload compile tokens, then
1222-
// the host link model, now the PE early return). The predicate is the
1223-
// same one every time, so it is spelled the same way.
1224-
const std::string graphTargetLd =
1225-
plan.toolchain.crossTargetFlag.empty() ? std::string{}
1226-
: link_toolchain_flags;
1225+
// ⚠️ AND ONLY WHILE THE PAYLOAD IS THE TARGET, which is what the paragraph
1226+
// above assumes without saying so: `x86_64-w64-mingw32-g++` needs no
1227+
// `--target` because it HAS no other. When the target side comes from the
1228+
// dependency graph the compiler is an ordinary retargetable clang, and this
1229+
// branch is one of three shaped by the HOST rather than by the target — see
1230+
// the replacement below, which covers all three at once.
1231+
if (isMingwTc && !graphTargetSide) {
12271232
// `-static` / `-static-libstdc++` now come from the contract table via
12281233
// unit_ldflags (dist::Format::Pe) — the whole-link `-static` is what
12291234
// "self-contained" means here, since the piecemeal recipe still leaves
12301235
// libwinpthread-1.dll behind.
12311236
std::string mingw_stdexp;
12321237
if (caps.stdlib_id == "libstdc++")
12331238
mingw_stdexp = " -lstdc++exp";
1234-
f.ld = std::format("{}{}{}{}{}", graphTargetLd, link_intent_ld,
1235-
user_ldflags, mingw_stdexp, link_extra);
1239+
f.ld = std::format("{}{}{}{}", link_intent_ld, user_ldflags,
1240+
mingw_stdexp, link_extra);
12361241
// `-lstdc++exp` is named explicitly, so swapping g++ for gcc would not
12371242
// drop it — the C line has to leave it out.
1238-
f.ldC = std::format("{}{}{}{}", graphTargetLd, link_intent_ld,
1239-
user_ldflags, link_extra);
1243+
f.ldC = std::format("{}{}{}", link_intent_ld, user_ldflags, link_extra);
12401244
return f;
12411245
}
12421246

@@ -1364,6 +1368,52 @@ CompileFlags compute_flags(const BuildPlan& plan) {
13641368
user_ldflags, link_extra);
13651369
}
13661370

1371+
// ── The target side comes from the graph, so the HOST's link is wrong ──
1372+
//
1373+
// ⭐⭐ THE THREE BRANCHES ABOVE ARE SHAPED BY THIS MACHINE, NOT BY THE
1374+
// TARGET. `if constexpr (is_windows)` / `needs_explicit_libcxx` / else is a
1375+
// question about where mcpp itself was built, and each answer describes a
1376+
// link on that machine: an SDK path, a deployment target, a loader search
1377+
// path, this host's `libatomic`. Every one of them is right when the target
1378+
// is the host or is served by a payload, and wrong when the C library, the
1379+
// C++ runtime and the platform are packages in the dependency graph.
1380+
//
1381+
// ⚠️ ONLY THE THIRD BRANCH EVER CONSUMED `link_toolchain_flags`, WHICH IS
1382+
// WHERE `--target=` LIVES. So a cross build over openkal linked correctly
1383+
// from a Linux host and would have handed a Mach-O or an ELF to a linker
1384+
// told nothing about the target from a macOS or a Windows one. The measured
1385+
// shape of that failure is on record from the PE case, which reached it a
1386+
// different way:
1387+
//
1388+
// ld.lld: error: obj/…/types.m.o: unknown file type (× 30)
1389+
//
1390+
// ⇒ Applied LAST and by REPLACEMENT, exactly as the freestanding block
1391+
// below is and for the same reason it is: what came before is not merely
1392+
// unnecessary but wrong, and appending to it would leave the outcome
1393+
// depending on the driver's flag ordering rather than on a decision.
1394+
//
1395+
// ⚠️ AND IT REPLACES A SPECIAL CASE RATHER THAN ADDING ONE. The PE branch
1396+
// above carried its own copy of this for one format; this covers PE, Mach-O
1397+
// and ELF, on every host, with the predicate stated once.
1398+
//
1399+
// What survives, and why each one is not the host's:
1400+
// full_static the contract table's, keyed on the target's FORMAT
1401+
// link_toolchain_flags `--target=`, `--no-default-config`, `-fuse-ld=lld`
1402+
// link_intent_ld what the user asked to build (exe/shared/static)
1403+
// user_ldflags the manifest's own words
1404+
// link_extra `-flto` / `-s`, profile decisions
1405+
//
1406+
// What does not: `b_flag` (this host's binutils), `runtime_dirs` and the
1407+
// `-rpath` beside them (this host's payload directories — measured on a
1408+
// Mach-O link as `-Wl,-rpath,…/lib/x86_64-unknown-linux-gnu`, which ld64
1409+
// accepts and writes into the image), `payload_ld`, `atomic_ld`.
1410+
if (!isFreestandingTarget && graphTargetSide) {
1411+
f.ld = std::format("{}{}{}{}{}", full_static, link_toolchain_flags,
1412+
link_intent_ld, user_ldflags, link_extra);
1413+
f.ldC = std::format("{}{}{}{}{}", full_static, link_toolchain_flags_c,
1414+
link_intent_ld, user_ldflags, link_extra);
1415+
}
1416+
13671417
// ── Freestanding: the target has no OS, so most of the above is wrong ──
13681418
//
13691419
// Applied LAST and by REPLACEMENT rather than woven in above, for two

0 commit comments

Comments
 (0)