diff --git a/.github/workflows/ci-macos-ios.yml b/.github/workflows/ci-macos-ios.yml index 3547be48..5d32e7d1 100644 --- a/.github/workflows/ci-macos-ios.yml +++ b/.github/workflows/ci-macos-ios.yml @@ -327,12 +327,12 @@ jobs: run: | set -euo pipefail rm -rf /tmp/iosplain && mkdir -p /tmp/iosplain/src && cd /tmp/iosplain + # NO FLOOR STATED: the row takes the located SDK's version, read + # through xcrun, and the artefact's LC_BUILD_VERSION minos says so. cat > mcpp.toml << 'TOML' [package] name = "iosplain" version = "0.1.0" - [build] - ios_deployment_target = "18.0" TOML printf 'import std;\nint main() { std::print("x\\n"); }\n' > src/main.cpp "$MCPP_DEV" build --target aarch64-ios-sim > mixed.log 2>&1 || { echo "FAIL: import std without llvm.libcxx no longer builds"; cat mixed.log; exit 1; } @@ -350,6 +350,10 @@ jobs: grep -q -- '-isystem[^ ]*xim-x-llvm[^ ]*/c++/v1' "$ninja" && { echo "FAIL: the plain program still takes the payload's C++ headers"; exit 1; } art=$(ls target/aarch64-ios-sim/*/bin/iosplain | head -1) otool -L "$art" | grep -q '/usr/lib/libc++.1.dylib' || { echo "FAIL: the plain program does not link the SDK's libc++"; otool -L "$art"; exit 1; } + sdkver=$(xcrun --sdk iphonesimulator --show-sdk-version) + minos=$(otool -l "$art" | awk '/cmd LC_BUILD_VERSION/{f=1} f && $1=="minos"{print $2; exit}') + [ "$minos" = "$sdkver" ] || { echo "FAIL: with no floor stated, minos is '$minos' and the located SDK is $sdkver"; exit 1; } + echo "ok: an unset floor is the located SDK's version ($sdkver)" echo "ok: import std builds with the hazard named; plain C++ takes the SDK's headers and libc++; the builtins degradation is printed" # THE SUPPORTED PATH, which is what the `verified` tier claims: a runner diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b9e6c88..0ccff9c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ ## [Unreleased] +### 图里的 libc++ 之下,预编译 C 库的头文件也要进 std 模块的命令;iOS 行未写的部署下限取 SDK 的版本 + +`llvm.libcxx` 的 CI 扩到它声明的每一行后量到两处引擎缺口。包提供的 std 模块在 +预编译时只拿到包自己的 `std-module-flags` 与图里各层的头文件目录,预编译 C 库 +(glibc、Apple SDK)的目录一个都没有:Linux 上它读的是 runner 的 `/usr/include` +而不是载荷的 glibc(没有任何报告显示这一点),macOS 原生行停在 `mbstate_t`。现在 +这条命令从每个翻译单元用的同一个生产者(`host_compile_tokens`,C++ 层记为图里的) +取 cfg 旁路、C 库目录、SDK 与部署下限。`docs/20` 曾承诺 iOS 行不写 +`ios_deployment_target` 时取 SDK 的默认;实测 clang 对无版本的 `arm64-apple-ios` +拒绝线程局部存储(libc++abi 用到它),所以那个默认比机器上任何 SDK 都旧。现在 +定位 SDK 时同时读 `xcrun --sdk --show-sdk-version`,写进同一个槽。 + +- 判据:`tests/e2e/663` 断言包 std 模块记录的命令点名载荷的 glibc;`ci-macos-ios` + 的纯 C++ 夹具不写下限,断言产物 `LC_BUILD_VERSION minos` 等于定位到的 SDK 版本; + `mcpplibs/libcxx` 的 CI 在 linux、aarch64-macos、aarch64-ios、两个模拟器行上各量一次。 + ### 一个框架与它的生态库仍会撞到的引擎缺口:#630 的十项 #630 汇总了 HuxerUI 在六个平台上落地后引擎仍欠的十项。每一项都先在 diff --git a/docs/20-toolchains.md b/docs/20-toolchains.md index e1382f1a..42ee6987 100644 --- a/docs/20-toolchains.md +++ b/docs/20-toolchains.md @@ -723,9 +723,12 @@ Target aarch64-ios-sim → arm64-apple-ios18.0-simulator which are Apple's own spellings. No `-miphoneos-version-min` flag is emitted: the triple fully determines the platform and the minimum, and a flag would be a second place answering a question the triple already answers. Leaving the key -unset is legal and means the SDK's own default, which clang supplies for an -Apple target -- unlike Android, where bionic refuses an unversioned triple -outright. +unset is legal and means the located SDK's own version, which mcpp reads from +`xcrun --sdk --show-sdk-version` and writes into the same slot: clang's +own default for an unversioned iOS triple is older than any SDK on the machine +(it refused thread-local storage, which libc++abi uses; measured on Xcode +16.4), so the triple is versioned before the driver sees it. Android differs +in the other direction: bionic refuses an unversioned triple outright. ### The simulator's shape, and its boundary diff --git a/docs/zh/20-toolchains.md b/docs/zh/20-toolchains.md index 19e99d7e..bbe00296 100644 --- a/docs/zh/20-toolchains.md +++ b/docs/zh/20-toolchains.md @@ -649,8 +649,10 @@ Target aarch64-ios-sim → arm64-apple-ios18.0-simulator 这是 Apple 自己的拼法。不发 `-miphoneos-version-min` 标志:三元组已经完全决定了 平台与最低版本,而一个标志会成为第二个回答三元组已经回答过的问题的地方。不写这个键 -是合法的,含义是 SDK 自己的默认值 —— 对一个 Apple 目标,clang 会供给它 —— -这与 Android 不同,那里 bionic 会直接拒绝一个不带版本的三元组。 +是合法的,含义是定位到的 SDK 自己的版本:mcpp 用 `xcrun --sdk --show-sdk-version` +读出它并写进同一个槽。clang 对不带版本的 iOS 三元组给的默认比机器上任何 SDK 都旧 +(它拒绝了 libc++abi 用到的线程局部存储;在 Xcode 16.4 上实测),所以三元组在驱动看到 +之前就带上了版本。Android 的差别在另一个方向:bionic 会直接拒绝一个不带版本的三元组。 ### 模拟器的形状与它的边界 diff --git a/mcpp.toml b/mcpp.toml index 6b9508bc..88dabf19 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.13.2" +version = "2026.9.14.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/platform/src/macos/macos.cppm b/modules/platform/src/macos/macos.cppm index fa0ea00b..a63e34c2 100644 --- a/modules/platform/src/macos/macos.cppm +++ b/modules/platform/src/macos/macos.cppm @@ -74,6 +74,13 @@ bool sdkroot_answers(const std::filesystem::path& candidate, // today's behaviour for today's callers. std::optional sdk_path(std::string_view sdk = sdk_macos); +// The version the located SDK was made for, as `xcrun --sdk +// --show-sdk-version` reports it ("18.5"). Empty off macOS and when xcrun +// cannot answer. Read for an iOS row whose manifest states no deployment +// floor, since clang's own default for an unversioned iOS triple is older +// than any SDK on the machine (it refused thread-local storage; measured). +std::optional sdk_version(std::string_view sdk); + // Built-in default deployment floor (rustc-style: every target has a // baseline). 14.0 = the floor of the official LLVM static libc++ // archives; with the default-static stdlib this makes `mcpp run` @@ -194,6 +201,17 @@ bool sdkroot_answers(const std::filesystem::path& candidate, return sdk == sdk_macos; } +std::optional sdk_version(std::string_view sdk) { +#if defined(__APPLE__) + auto v = run_capture_trimmed(std::format("xcrun --sdk {} --show-sdk-version 2>/dev/null", sdk)); + if (v.empty()) return std::nullopt; + return v; +#else + (void)sdk; + return std::nullopt; +#endif +} + std::optional sdk_path(std::string_view sdk) { auto layout = sdk_layout(sdk); if (!layout) return std::nullopt; diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index dd503e98..b571002a 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.9.13.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.14.1"; } // namespace mcpp diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 3f0dad8f..3ca2c746 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -32,6 +32,7 @@ import mcpp.modgraph.graph; import mcpp.modgraph.scanner; import mcpp.modgraph.validate; import mcpp.toolchain.clang; +import mcpp.toolchain.hostflags; // the compile-token producer the package std module reuses import mcpp.toolchain.cppfly; import mcpp.toolchain.detect; import mcpp.toolchain.dialect; @@ -1664,10 +1665,12 @@ std::string min_platform_version(const mcpp::manifest::Manifest& m, // one target, which is why they still share this function's single return // and the single fingerprint slot behind it. // - // Empty is a legal answer here and not a refusal, unlike Android's. The - // asymmetry is a measured property of the platforms rather than a policy: - // Darwin's driver supplies the SDK's own deployment target when the - // triple carries none, and bionic rejects the unversioned triple outright. + // Empty is a legal answer here and not a refusal, unlike Android's, and + // for the iOS rows prepare fills it with the located SDK's version before + // this is read: an unversioned `arm64-apple-ios` made clang refuse + // thread-local storage (measured, Xcode 16.4), so the driver's own + // default is not the SDK's. Bionic rejects the unversioned triple + // outright, which is the other half of the asymmetry. if (t.is_ios()) return m.buildConfig.iosDeploymentTarget; return mcpp::platform::macos::deployment_target( m.buildConfig.macosDeploymentTarget); @@ -2591,6 +2594,20 @@ prepare_build(bool print_fingerprint, ? mcpp::platform::macos::sdk_iphonesim : mcpp::platform::macos::sdk_iphoneos; appleSdkLocated = mcpp::platform::macos::sdk_path(which); + // AN UNSET FLOOR IS THE LOCATED SDK'S VERSION, READ RATHER THAN + // LEFT TO THE DRIVER. `docs/20` promised that an unversioned + // triple meant the SDK's own default; measured on macos-15 with + // Xcode 16.4, clang given `arm64-apple-ios` with no version + // refused thread-local storage for the target, which libc++abi + // uses, so the default it chose was older than any SDK on the + // machine. The version `xcrun` reports for the located SDK is the + // one the SDK was made for, and it enters the manifest here so + // that the fingerprint slot, the effective triple and every + // report read one value. + if (appleSdkLocated && m->buildConfig.iosDeploymentTarget.empty()) { + if (auto v = mcpp::platform::macos::sdk_version(which)) + m->buildConfig.iosDeploymentTarget = *v; + } if (!appleSdkLocated) { // A CODE, BECAUSE THE MATRIX COMPARES REASONS AND NOT ONLY // OUTCOMES. A refusal with no code is recorded as `other`, @@ -10714,16 +10731,6 @@ prepare_build(bool print_fingerprint, // DWARF. Same function, not a second copy of the decision. for (auto& f : mcpp::toolchain::graph_runtime_compile_flags(*tc)) flags += " " + f; - // AND THE APPLE CROSS TARGET'S SDK, WHICH THE TOOLCHAIN RESOLUTION - // HAD ALREADY PUT ON THIS CHANNEL AND THIS ASSIGNMENT REPLACES. - // The module's C library is the SDK's on the iOS rows (the - // package supplies the C++ layer alone), and without the sysroot - // the precompile stops on `mbstate_t` inside libc++'s own - // headers. Measured on macos-15 with `llvm.libcxx` over - // `arm64-apple-ios18.0`: twenty "reference to unresolved using - // declaration" errors, every one a C library type. - if (!tc->appleSdkRoot.empty()) - flags += " -isysroot " + mcpp::xlings::shq(tc->appleSdkRoot.string()); } // Everything up to here says which machine the module is for; what // follows says where its headers are. The codegen step needs only the @@ -10779,6 +10786,42 @@ prepare_build(bool print_fingerprint, // its own translation units see. Measured: without them the module // reaches musl's and stops on `clockid_t', a name that header // declares only under the macro the package carries. + // THE PREBUILT C LIBRARY'S OWN TOKENS, FROM THE PRODUCER EVERY UNIT + // USES. A package that supplies the C++ layer over a prebuilt C + // library (`llvm.libcxx` over glibc, or over an Apple SDK) has no + // way to name that library's headers in its manifest, and the + // target-side broadcast below carries only graph layers. Without + // these the precompile reads whatever the driver finds on its own: + // on Linux the runner's `/usr/include` rather than the payload's + // glibc, a host dependency no report showed; on macOS nothing, and + // the precompile stops on `mbstate_t`; on the iOS rows nothing, and + // it stopped on the same name. Measured on 2026-09-14 across the + // three. `host_compile_tokens` is what every translation unit of the + // build gets, asked with the C++ layer marked as the graph's so that + // it withholds the payload's libc++ and emits the rest: the cfg + // bypass, the C library's directories, the SDK and the deployment + // floor. Same function, not a second copy. + // + // AND LAST ON THE COMMAND, after the package's own directories: + // `-isystem` order is search order, and libc++'s headers must precede + // the C library's, which libc++ states in as many words (`` + // stops the build if it reaches a `` that is not its own). + // Emitted ahead of them, glibc's `` shadowed libc++'s wrapper + // and `` failed on `std::__builtin_isnan` (measured). + if (tc->cAbiPrebuilt) { + mcpp::toolchain::HostFlagOptions hopt; + hopt.cfgBypass = mcpp::toolchain::HostFlagOptions::CfgBypass::Always; + hopt.cAbiPrebuilt = true; + hopt.cxxFromGraph = true; + hopt.appleSdkRoot = tc->appleSdkRoot; + hopt.macosDeploymentTarget = mcpp::platform::macos::deployment_target( + m->buildConfig.macosDeploymentTarget); + for (auto& t : mcpp::toolchain::host_compile_tokens( + *tc, hopt, mcpp::toolchain::no_escape)) { + const auto q = " " + mcpp::xlings::shq(t); + if (flags.find(q) == std::string::npos) flags += q; + } + } for (auto& f : targetSideUsage.cxxflags) flags += " " + mcpp::xlings::shq(f); tc->stdModuleFlags = flags; diff --git a/tests/e2e/663_a_graph_libcxx_over_the_payloads_c_library.sh b/tests/e2e/663_a_graph_libcxx_over_the_payloads_c_library.sh index a34424be..bc642dfc 100755 --- a/tests/e2e/663_a_graph_libcxx_over_the_payloads_c_library.sh +++ b/tests/e2e/663_a_graph_libcxx_over_the_payloads_c_library.sh @@ -68,6 +68,15 @@ grep -q -- '-nostdinc++' "$ninja" || fail "the compile lines carry no -nostdinc+ grep -q -- '-nostdlib++' "$ninja" || fail "the link line carries no -nostdlib++" "$ninja" grep -q -- 'xim-x-llvm[^ ]*libc++\(abi\)\?\.a' "$ninja" \ && fail "the link line still names the payload's libc++ archives" "$ninja" +# The package's std module was precompiled against the payload's C library, +# not against whatever the driver found on the machine: the recorded command +# names the glibc payload. Before this assertion the command carried only +# `--no-default-config -nostdinc++` and the runner's /usr/include stood in, +# a host dependency no report showed. +stdjson=$(grep -l 'llvm.libcxx\|libcxx' "$MCPP_HOME"/build-cache/v1/std/*/std-module.json 2>/dev/null | xargs -r ls -t | head -1) +[ -n "$stdjson" ] || fail "no std-module.json records the package's module under $MCPP_HOME/build-cache/v1/std" +grep -q 'xim-x-glibc' "$stdjson" \ + || fail "the package std module's command does not name the payload's glibc" "$stdjson" bin=$(ls target/*/*/bin/app | head -1) ldd "$bin" | grep -q 'libc++' && fail "the artefact still links a libc++ shared object" <(ldd "$bin") out=$("$bin") || fail "the program exited non-zero"