Skip to content
Merged
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
398 changes: 398 additions & 0 deletions .agents/docs/2026-08-03-b3-target-aware-artifact-naming.md

Large diffs are not rendered by default.

43 changes: 29 additions & 14 deletions .agents/docs/2026-08-03-windows-host-linux-cross-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -643,21 +643,36 @@ return std::filesystem::path("bin") /
```

`exe_suffix` / `lib_prefix` / `static_lib_ext` / `shared_lib_ext` **四个都是 host 常量**
(`platform/common.cppm:18-29`),却用来命名 **target** 产物。与 B2 完全同构,而且**对称地错**:
(`platform/common.cppm:18-33`),却用来命名 **target** 产物。与 B2 同构。

| 方向 | 现状 | 应当 |
|---|---|---|
| Windows → linux-musl | `mcpp.exe`(却是 ELF) | `mcpp` |
| Linux → windows-gnu | `mcpp`(却是 PE) | `mcpp.exe` |

**为什么本期不修**:改产物命名是行为变更,会同时动到 `tests/e2e/102_mingw_cross_wine.sh`、
release 打包路径和任何用户脚本;而两个方向的现状都**不致命**(扩展名在 Linux 上无意义,
Windows 命令行也能跑无扩展名的 PE)。把一个有回归面的重命名塞进本 PR,违背了 §1.3 定下的
「单 PR 但提交分层、失败可归因」的初衷。

**修的时候要一起改的四个常量**,并且要注意 `runtime_aliases_for_target()`(`plan.cppm:215`)
依赖 `target_output()` 的结果去比对 soname —— 见 [[soname-alias-explicit-ninja-goals]],
那条边曾经因为类似改动漏生成过。
> ### ⚠️ 本节初版的判断有误,已在专项文档中更正
>
> 初版写的是「对称地错:Linux→Windows 产出 `mcpp` 却是 PE」。**后半句是错的** ——
> 实测 Linux 主机交叉到 `x86_64-windows-gnu`,产物就是 `b3probe.exe`(PE32+),
> 因为 **mingw 的 GCC driver 自己会补 `.exe`**,mcpp 从未参与这个决定。
>
> 真正的症状是另一件事,而且更实际:**ninja 声明的输出是 `bin/foo`,GCC 写出 `bin/foo.exe`,
> 声明的那个文件从来不存在 ⇒ 每次 `mcpp build` 都重跑链接边**。实测连续两次构建
> 产物 mtime 会变。也就是说 Linux→Windows 是**功能缺陷(增量构建失效)**,
> Windows→Linux 才只是「ELF 顶着 `.exe`」的观感问题。

**完整分析、修复方案与验证判据见专项文档:**
`2026-08-03-b3-target-aware-artifact-naming.md`

要点摘录:
- 改动面只有 `src/build/plan.cppm` 一个文件;其余 15 处 `exe_suffix` 引用都是找主机上的
`ninja`/`xlings`/`clang++` 等,**是正确的 host 语义,不要动**
- 正确的命名是 **(os, env) 二元函数**:`windows-gnu` 用 GNU 约定(`libfoo.a`),
`windows-msvc` 才是 `foo.lib` —— 现行 `_WIN32` 分支写死后者,**Windows 主机上用 mingw
构建静态库今天就已经命名错了**,与交叉无关的存量缺陷
- e2e/CI 基本不用改(与本节初版的担心相反):`102_mingw_cross_wine.sh` 找的是真实产物
而非 ninja 声明,改前改后都匹配
- `runtime_aliases_for_target()`(`plan.cppm:215`)依赖 `target_output()` 比对 soname,
必须一起改并补断言 —— 见 [[soname-alias-explicit-ninja-goals]]

**为什么当期没修**:发现时 #339 已进入 CI 验证阶段,把一个尚未查清真实形态的改动塞进去,
会毁掉 §1.3 定下的「提交分层、失败可归因」。事后看这个决定是对的 —— 初版对症状的描述
本身就是错的,当场改只会改错方向。

---

Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/cross-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,25 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
with:
# Do NOT restore target/ in a cross job. This job builds twice — once
# for the host, then once for x86_64-linux-musl — and a restored BMI
# tree makes the second build read `std` BMIs that no longer match
# what the dependency BMIs were compiled against:
#
# mcpplibs.cmdline: error: import 'std' has CRC mismatch
#
# GCC bakes a CRC of each imported module's BMI into the importer, so
# the two have to come from the same build round; a cache that
# restores one without the other is not a partial speedup, it is an
# unbuildable tree. It reproduced on rerun, and only in this job —
# the other two cross jobs below cache ~/.mcpp and ~/.xlings but
# deliberately never target/, which is the convention this now follows.
#
# It stayed hidden until a PR touched neither mcpp.toml nor
# .xlings.json: those two files key the sandbox cache, so every
# earlier run had been a cold miss.
cache-target: 'false'

- name: Build mcpp from source (self-host)
shell: bash
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [2026.8.3.3] — 2026-08-03

### 修复

- **交叉构建每次都重新链接(增量构建对 PE 目标实际失效)。** `plan.cppm` 的 `target_output()` 用 `mcpp::platform::{exe_suffix,lib_prefix,static_lib_ext,shared_lib_ext}` 拼产物名 —— 这四个是**主机**常量,由 `#if defined(_WIN32)/__APPLE__` 选择。主机构建下「这台机器叫什么」与「产物该叫什么」恰好同解,所以它一直没被发现;`host ≠ target` 时两者分岔。

后果不是命名难看:Linux 交叉到 PE 时,ninja 被告知产出 `bin/foo`,而 mingw 的 GCC driver 写出的是 `bin/foo.exe` —— **声明的那个文件从来不存在**,ninja 每次都发现输出缺失并重跑链接边。这条路径正是 CI 每天在跑的。

产物命名现在由 `ArtifactNaming` 决定,每个 plan 从 target triple 求值一次。**它是 (os, env) 二元函数,不是 os 一元**:`x86_64-windows-gnu` 用 GNU 约定(`libfoo.a`),`x86_64-windows-msvc` 才是 `foo.lib`。空 triple 表示「为本机构建」,只有那时主机答案才是对的,因此它作为回退传入而非被直接读取 —— **主机构建逐位不变**。

同样的处理给了 `shared_library_link_flags`:消费者链接一个共享库时用完整路径(PE)、`@loader_path`(Mach-O)还是 `$ORIGIN`(ELF),是**产物**的属性;按主机求值在交叉时方向就是反的。

- **`windows-gnu` 静态库命名错误(行为变更)。** 在 Windows 主机上用 mingw 工具链构建静态库,产物此前叫 `foo.lib` —— 一个 GNU archive 顶着 MSVC 的名字,MSVC 拿不去用。现在按 GNU 约定命名为 `libfoo.a`。这修正的是一个**今天就是错的**名字,与交叉编译无关。

### 改进

- **非 ELF 目标上声明 `SharedLibrary` 现在明确报错,而不是产出未经验证的东西。** 全部 5 个共享库 e2e 都声明 `# requires: elf`,而这个 capability 只在 Linux 上授予 —— 也就是说共享库在 PE 与 Mach-O 上**从未被端到端验证过**。相关分支是推测代码:mingw 的 ld 容忍直接链 `.dll`,MSVC 的 `link.exe` 不行,而两者都没有 import library 可链,因为 mcpp 还没有建模它。

一段既没有测试覆盖、又不肯明确拒绝的分支是最难清理的债 —— 它既不能被信任,也不能被删除,因为没人知道谁在依赖它。先把边界写死,等真要支持时再补(前置条件是先有 PE/Mach-O 的共享库覆盖)。

## [2026.8.3.2] — 2026-08-03

### 新增
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "2026.8.3.2"
version = "2026.8.3.3"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
130 changes: 102 additions & 28 deletions src/build/plan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import mcpp.toolchain.cppfly;
import mcpp.toolchain.detect;
import mcpp.toolchain.dialect;
import mcpp.toolchain.fingerprint;
import mcpp.toolchain.triple;
import mcpp.platform;

export namespace mcpp::build {
Expand Down Expand Up @@ -197,29 +198,52 @@ std::vector<std::string> dependency_name_candidates(
return out;
}

std::filesystem::path target_output(const mcpp::manifest::Target& t) {
// The naming this MACHINE would use for its own binaries. Correct only for a
// host-target build; passed to artifact_naming() as the fallback for an empty
// triple, and never consulted directly when a target triple is present.
mcpp::toolchain::triple::ArtifactNaming host_artifact_naming() {
return {
.exeSuffix = mcpp::platform::exe_suffix,
.libPrefix = mcpp::platform::lib_prefix,
.staticLibExt = mcpp::platform::static_lib_ext,
.sharedLibExt = mcpp::platform::shared_lib_ext,
.sharedNeedsImportLib = mcpp::platform::is_windows,
};
}

mcpp::toolchain::triple::ArtifactNaming naming_for(const mcpp::toolchain::Toolchain& tc) {
auto t = mcpp::toolchain::triple::parse(tc.targetTriple);
return mcpp::toolchain::triple::artifact_naming(
t ? *t : mcpp::toolchain::triple::Triple{}, host_artifact_naming());
}

// What the artifact is CALLED — a property of the target, not of this machine.
// Reading the host constants here made ninja declare an output the compiler
// never writes (Linux -> PE: declared `bin/foo`, produced `bin/foo.exe`), so
// the link edge could never be satisfied and reran on every build.
std::filesystem::path target_output(const mcpp::manifest::Target& t,
const mcpp::toolchain::triple::ArtifactNaming& n) {
if (t.kind == mcpp::manifest::Target::Library) {
return std::filesystem::path("bin") /
std::format("{}{}{}", mcpp::platform::lib_prefix, t.name,
mcpp::platform::static_lib_ext);
std::format("{}{}{}", n.libPrefix, t.name, n.staticLibExt);
}
if (t.kind == mcpp::manifest::Target::SharedLibrary) {
return std::filesystem::path("bin") /
std::format("{}{}{}", mcpp::platform::lib_prefix, t.name,
mcpp::platform::shared_lib_ext);
std::format("{}{}{}", n.libPrefix, t.name, n.sharedLibExt);
}
return std::filesystem::path("bin") /
std::format("{}{}", t.name, mcpp::platform::exe_suffix);
std::format("{}{}", t.name, n.exeSuffix);
}

std::vector<std::filesystem::path> runtime_aliases_for_target(
const mcpp::manifest::Target& t) {
const mcpp::manifest::Target& t,
const mcpp::toolchain::triple::ArtifactNaming& n) {
std::vector<std::filesystem::path> aliases;
if (t.kind != mcpp::manifest::Target::SharedLibrary || t.soname.empty()) {
return aliases;
}

auto output = target_output(t);
auto output = target_output(t, n);
if (t.soname != output.filename().string()) {
aliases.push_back(output.parent_path() / t.soname);
}
Expand All @@ -232,19 +256,30 @@ bool is_implementation_source(const std::filesystem::path& source) {
|| ext == ".S" || ext == ".s" || ext == ".asm";
}

std::vector<std::string> shared_library_link_flags(const mcpp::manifest::Target& t) {
// How a CONSUMER links against a shared library. Also a target property: PE has
// no rpath and wants an import library, Mach-O uses @loader_path, ELF uses
// $ORIGIN. Keying this on the host pointed it the wrong way under cross builds.
//
// NOTE: shared libraries have never been verified end to end on PE or Mach-O —
// every shared-library e2e declares `# requires: elf`, and that capability is
// only granted on Linux. The PE branch here (linking the .dll path directly)
// is therefore unproven: mingw's ld tolerates it, MSVC's link.exe cannot.
// make_plan() rejects SharedLibrary targets on non-ELF targets rather than
// emitting something unverifiable — see the guard there.
std::vector<std::string> shared_library_link_flags(
const mcpp::manifest::Target& t,
const mcpp::toolchain::triple::ArtifactNaming& n,
const mcpp::toolchain::triple::Triple& target) {
std::vector<std::string> flags;
if constexpr (mcpp::platform::is_windows) {
flags.push_back(target_output(t).generic_string());
const bool pe = n.sharedNeedsImportLib;
const bool macho = target.empty() ? bool(mcpp::platform::is_macos)
: target.os == "macos";
if (pe) {
flags.push_back(target_output(t, n).generic_string());
} else {
flags.push_back("-L" + target_output(t).parent_path().generic_string());
if constexpr (mcpp::platform::supports_rpath) {
if constexpr (mcpp::platform::is_macos) {
flags.push_back("-Wl,-rpath,@loader_path");
} else {
flags.push_back("-Wl,-rpath,'$$ORIGIN'");
}
}
flags.push_back("-L" + target_output(t, n).parent_path().generic_string());
flags.push_back(macho ? "-Wl,-rpath,@loader_path"
: "-Wl,-rpath,'$$ORIGIN'");
flags.push_back("-l" + t.name);
}
return flags;
Expand Down Expand Up @@ -384,6 +419,45 @@ make_plan(const mcpp::manifest::Manifest& manifest,
plan.manifest = manifest;
plan.toolchain = tc;
plan.fingerprint = fp;

// Artifact naming and shared-library link shape are properties of the
// TARGET. Resolved once here from tc.targetTriple (empty = host target, in
// which case the host constants ARE the right answer) and threaded down,
// so nothing below reaches for mcpp::platform to describe an output.
const auto targetTriple = [&] {
auto t = mcpp::toolchain::triple::parse(tc.targetTriple);
return t ? *t : mcpp::toolchain::triple::Triple{};
}();
const auto naming = naming_for(tc);

// Shared libraries have never been verified end to end on PE or Mach-O:
// every shared-library e2e declares `# requires: elf`, and run_all.sh only
// grants that capability on Linux. The non-ELF paths through
// shared_library_link_flags are therefore unproven — mingw's ld tolerates
// linking a .dll directly, MSVC's link.exe cannot, and neither has an
// import library to link against because mcpp does not model one.
//
// Refusing is strictly better than emitting something unverifiable: a
// branch that is neither tested nor willing to say no is the hardest kind
// of debt, because it can be neither trusted nor deleted.
if (!targetTriple.empty() && targetTriple.os != "linux") {
for (auto const& t : manifest.targets) {
if (t.kind != mcpp::manifest::Target::SharedLibrary) continue;
return std::unexpected(std::format(
"target '{}': shared libraries are only supported for Linux (ELF) "
"targets today.\n"
" target '{}' is kind=\"shared\"; build it as kind=\"lib\" "
"(static) for this target,\n"
" or build it for a linux target.\n"
" note: PE consumers need an import library and Mach-O needs "
"install-name handling;\n"
" neither is modelled yet, so mcpp refuses rather than "
"producing an artifact\n"
" nothing has ever verified.",
targetTriple.str(), t.name));
}
}

bool experimentalStd = false;
if (auto stdCfg = mcpp::manifest::normalize_cpp_standard(manifest.package.standard)) {
plan.cppStandard = stdCfg->canonical;
Expand Down Expand Up @@ -686,7 +760,7 @@ make_plan(const mcpp::manifest::Manifest& manifest,
.packageIndex = i,
.packageName = qname,
.target = t,
.output = target_output(t),
.output = target_output(t, naming),
});
sharedTargetsByPackage[i].push_back(targetIndex);
}
Expand Down Expand Up @@ -767,9 +841,9 @@ make_plan(const mcpp::manifest::Manifest& manifest,
// (0.0.104-0.0.106). The failure surfaced far away, as
// `libX11.so: undefined reference to xcb_connect` or a test
// exiting 127.
for (auto const& alias : runtime_aliases_for_target(dep.target))
for (auto const& alias : runtime_aliases_for_target(dep.target, naming))
lu.implicitInputs.push_back(alias);
auto flags = shared_library_link_flags(dep.target);
auto flags = shared_library_link_flags(dep.target, naming, targetTriple);
lu.linkFlags.insert(lu.linkFlags.end(), flags.begin(), flags.end());
}
}
Expand Down Expand Up @@ -812,7 +886,7 @@ make_plan(const mcpp::manifest::Manifest& manifest,
lu.kind = LinkUnit::SharedLibrary;
lu.output = dep.output;
lu.soname = dep.target.soname;
lu.runtimeAliases = runtime_aliases_for_target(dep.target);
lu.runtimeAliases = runtime_aliases_for_target(dep.target, naming);
append_package_objects(lu, dep.packageName);
append_direct_shared_deps(lu, dep.packageIndex);
plan.linkUnits.push_back(std::move(lu));
Expand All @@ -833,19 +907,19 @@ make_plan(const mcpp::manifest::Manifest& manifest,
lu.targetName = t.name;
if (t.kind == mcpp::manifest::Target::Library) {
lu.kind = LinkUnit::StaticLibrary;
lu.output = target_output(t);
lu.output = target_output(t, naming);
} else if (t.kind == mcpp::manifest::Target::SharedLibrary) {
lu.kind = LinkUnit::SharedLibrary;
lu.output = target_output(t);
lu.output = target_output(t, naming);
lu.soname = t.soname;
lu.runtimeAliases = runtime_aliases_for_target(t);
lu.runtimeAliases = runtime_aliases_for_target(t, naming);
} else if (t.kind == mcpp::manifest::Target::TestBinary) {
lu.kind = LinkUnit::TestBinary;
lu.output = target_output(t);
lu.output = target_output(t, naming);
if (!t.main.empty()) lu.entryMain = projectRoot / t.main;
} else {
lu.kind = LinkUnit::Binary;
lu.output = target_output(t);
lu.output = target_output(t, naming);
if (!t.main.empty()) lu.entryMain = projectRoot / t.main;
}

Expand Down
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import mcpp.toolchain.detect;

export namespace mcpp::toolchain {

inline constexpr std::string_view MCPP_VERSION = "2026.8.3.2";
inline constexpr std::string_view MCPP_VERSION = "2026.8.3.3";

struct FingerprintInputs {
Toolchain toolchain;
Expand Down
Loading
Loading