Skip to content

Commit 8eecd2e

Browse files
committed
fix: a BMI's build path in a cache artifact is a vector of pairs, which clang 22 can read back from the module; the sandbox verification script of 2026.10.1.2
1 parent ca6646f commit 8eecd2e

3 files changed

Lines changed: 206 additions & 9 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env bash
2+
# Sandbox verification of the release that implements
3+
# .agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md
4+
# (#751, #753), run against the PUBLISHED release inside an xlings sandbox:
5+
#
6+
# B64=$(base64 -w0 .agents/docs/2026-10-01-pack-drive-and-selection-verify.sh)
7+
# xlings subos new v1002 2>/dev/null || true
8+
# xlings subos use v1002 --sandbox --cmd "echo $B64 | base64 -d > /tmp/v.sh && VER=<version> bash /tmp/v.sh"
9+
#
10+
# VER selects the release under test. Running it with VER=2026.10.1.1 is the
11+
# control: every section marked CHANGE must fail there, and every other
12+
# section must pass on both.
13+
#
14+
# Every probe directory is removed at the start of its section, because the
15+
# sandbox's $HOME persists between runs of the same subos. A section that does
16+
# not run is reported as SKIP and counted apart from a pass.
17+
set -u
18+
VER="${VER:?VER=<version under test>}"
19+
W="${W:-$HOME/v1002}"
20+
fails=0; passes=0; skips=0
21+
pass() { echo "PASS $1"; passes=$((passes+1)); }
22+
fail() { echo "FAIL $1"; [ -n "${2:-}" ] && [ -f "$2" ] && tail -20 "$2"; fails=$((fails+1)); }
23+
skip() { echo "SKIP $1"; skips=$((skips+1)); }
24+
25+
# 0. The release under test, from the published channel, with the CN mirror.
26+
if [ -n "${MCPP_OVERRIDE:-}" ]; then
27+
MCPP="$MCPP_OVERRIDE"
28+
else
29+
xlings config --mirror CN >/dev/null 2>&1 || true
30+
xlings update >/dev/null 2>&1 || true
31+
xlings install "mcpp@$VER" -y > /tmp/v1002-install.log 2>&1 || true
32+
MCPP="$HOME/.xlings/data/xpkgs/xim-x-mcpp/$VER/bin/mcpp"
33+
fi
34+
if [ ! -x "$MCPP" ]; then
35+
echo "FATAL: mcpp $VER is not installable from the index"; tail -20 /tmp/v1002-install.log; exit 2
36+
fi
37+
got=$("$MCPP" --version 2>&1 | head -1)
38+
case "$got" in *"$VER"*) pass "0 installed: $got";; *) fail "0 version: $got";; esac
39+
[ -n "${MCPP_OVERRIDE:-}" ] || "$MCPP" self config --mirror CN >/dev/null 2>&1 || true
40+
mkdir -p "$W"
41+
42+
# compiles_after <log> <lines>: the compile steps `.ninja_log` records after
43+
# its first <lines> lines.
44+
compiles_after() {
45+
tail -n +"$(( $2 + 1 ))" "$1" | awk -F'\t' '$4 ~ /\.(o|obj)$/ { n++ } END { print n + 0 }'
46+
}
47+
# no_compile <label> <selection...>: the build of the selection compiles nothing.
48+
no_compile() {
49+
local label="$1"; shift
50+
local log before n
51+
log=$(find target -name .ninja_log | head -1)
52+
before=$(wc -l < "$log")
53+
"$MCPP" build "$@" > nc.log 2>&1 || { fail "$label: build $* failed" nc.log; return 1; }
54+
n=$(compiles_after "$log" "$before")
55+
[ "$n" = 0 ] || { fail "$label: build $* compiled $n units" nc.log; return 1; }
56+
return 0
57+
}
58+
member() { # <dir> <name> <kind> [<dependency path>]
59+
mkdir -p "$1/src"
60+
{
61+
printf '[package]\nname = "%s"\nversion = "0.1.0"\n' "$2"
62+
[ -n "${4:-}" ] && printf '\n[dependencies]\ncore = { path = "%s" }\n' "$4"
63+
printf '\n[targets.%s]\nkind = "%s"\n' "$2" "$3"
64+
[ "$3" = bin ] && printf 'main = "src/main.cpp"\n'
65+
} > "$1/mcpp.toml"
66+
}
67+
68+
# 1. CHANGE (#751, GalTranslPP's shape): two members compile one file from
69+
# outside their directories; after --workspace, no selection compiles again.
70+
rm -rf "$W/s1"; mkdir -p "$W/s1/shared"; cd "$W/s1"
71+
printf '[workspace]\nmembers = ["core", "app", "tool"]\n' > mcpp.toml
72+
printf 'export module m;\nexport int answer() { return 42; }\n' > shared/m.cppm
73+
member core core lib; member app app bin ../core; member tool tool bin
74+
printf '\n[build]\nsources = ["src/*.cppm", "../shared/m.cppm"]\n' >> core/mcpp.toml
75+
printf '\n[build]\nsources = ["src/*.cpp", "../shared/m.cppm"]\n' >> tool/mcpp.toml
76+
printf 'export module corelib;\nimport m;\nexport int core_value() { return answer(); }\n' > core/src/core.cppm
77+
printf '#include <cstdio>\nimport corelib;\nint main() { std::printf("%%d\\n", core_value()); }\n' > app/src/main.cpp
78+
printf '#include <cstdio>\nimport m;\nint main() { std::printf("%%d\\n", answer() + 1); }\n' > tool/src/main.cpp
79+
if "$MCPP" build --workspace > s1.log 2>&1; then
80+
if no_compile "1" -p app && no_compile "1" -p tool && no_compile "1" --workspace && no_compile "1" -p app; then
81+
pass "1 CHANGE: a file two members list is compiled once for every selection"
82+
fi
83+
else fail "1 the workspace did not build" s1.log; fi
84+
85+
# 2. CHANGE: two members each with their own module `m`; alternating
86+
# selections compile nothing once each was built, and each program prints its
87+
# own value.
88+
rm -rf "$W/s2"; mkdir -p "$W/s2"; cd "$W/s2"
89+
printf '[workspace]\nmembers = ["core", "app", "tool"]\n' > mcpp.toml
90+
member core core lib; member app app bin ../core; member tool tool bin
91+
printf 'export module m;\nexport int answer() { return 42; }\n' > core/src/m.cppm
92+
printf 'export module corelib;\nimport m;\nexport int core_value() { return answer(); }\n' > core/src/core.cppm
93+
printf '#include <cstdio>\nimport corelib;\nint main() { std::printf("%%d\\n", core_value()); }\n' > app/src/main.cpp
94+
printf 'export module m;\nexport int answer() { return 7; }\n' > tool/src/m.cppm
95+
printf '#include <cstdio>\nimport m;\nint main() { std::printf("%%d\\n", answer()); }\n' > tool/src/main.cpp
96+
if "$MCPP" build --workspace > s2.log 2>&1; then
97+
a=$(find target -path '*/bin/app/app' -type f | head -1); t=$(find target -path '*/bin/tool/tool' -type f | head -1)
98+
if [ "$("$a")" = 42 ] && [ "$("$t")" = 7 ] \
99+
&& no_compile "2" -p app && no_compile "2" -p tool && no_compile "2" -p app && no_compile "2" --workspace; then
100+
pass "2 CHANGE: two modules of one name, no recompile across selections, each program its own value"
101+
fi
102+
else fail "2 the workspace did not build" s2.log; fi
103+
104+
# 3. CHANGE: a member that builds a shared library does not change the other
105+
# members' commands.
106+
rm -rf "$W/s3"; mkdir -p "$W/s3"; cd "$W/s3"
107+
printf '[workspace]\nmembers = ["core", "app", "dso"]\n' > mcpp.toml
108+
member core core lib; member app app bin ../core; member dso dso shared
109+
printf 'export module corelib;\nexport int core_value() { return 3; }\n' > core/src/core.cppm
110+
printf 'import corelib;\nint main() { return core_value() == 3 ? 0 : 1; }\n' > app/src/main.cpp
111+
printf 'export module dsolib;\nexport int dso_value() { return 5; }\n' > dso/src/dso.cppm
112+
if "$MCPP" build --workspace > s3.log 2>&1; then
113+
if no_compile "3" -p app && no_compile "3" --workspace; then
114+
pass "3 CHANGE: a shared-library member leaves the other members' commands alone"
115+
fi
116+
else fail "3 the workspace did not build" s3.log; fi
117+
118+
# 4. CHANGE (#753): a pack states its build, and Finished precedes Packing.
119+
rm -rf "$W/s4"; mkdir -p "$W/s4"; cd "$W/s4"
120+
member . p4 bin
121+
printf 'int main() { return 0; }\n' > src/main.cpp
122+
if "$MCPP" pack --format tar > s4.log 2>&1; then
123+
fin=$(grep -n 'Finished' s4.log | head -1 | cut -d: -f1); pk=$(grep -n 'Packing' s4.log | head -1 | cut -d: -f1)
124+
if grep -q 'Compiling p4' s4.log && [ -n "$fin" ] && [ -n "$pk" ] && [ "$fin" -lt "$pk" ]; then
125+
pass "4 CHANGE: the pack states its build, Finished before Packing"
126+
else fail "4 CHANGE: the pack's build is not stated" s4.log; fi
127+
else fail "4 the pack failed" s4.log; fi
128+
129+
# 5. CHANGE: `[build] jobs = 1` bounds mcpp test's compiles.
130+
rm -rf "$W/s5"; mkdir -p "$W/s5"; cd "$W/s5"
131+
member . p5 bin
132+
printf '\n[build]\njobs = 1\n' >> mcpp.toml
133+
mkdir -p tests
134+
for i in 1 2 3 4; do
135+
printf 'constexpr unsigned long long s%s() { unsigned long long x = %s; for (unsigned k = 0; k < 600; ++k) for (unsigned j = 0; j < 600; ++j) x = x * 6364136223846793005ull + 1; return x; }\nunsigned long long v%s() { constexpr auto v = s%s(); return v; }\n' $i $i $i $i > src/u$i.cpp
136+
done
137+
printf 'int main() { return 0; }\n' > src/main.cpp
138+
printf 'int main() { return 0; }\n' > tests/t.cpp
139+
if "$MCPP" test > s5.log 2>&1; then
140+
log=$(find target -name .ninja_log | head -1)
141+
if awk -F'\t' 'NR > 1 && $4 ~ /\.o$/ { s[n] = $1; e[n] = $2; n++ }
142+
END { for (i = 0; i < n; i++) for (j = i + 1; j < n; j++) if (s[i] < e[j] && s[j] < e[i]) exit 1; exit (n < 4) }' "$log"; then
143+
pass "5 CHANGE: mcpp test runs one compile at a time under jobs = 1"
144+
else fail "5 CHANGE: mcpp test overlapped compiles under jobs = 1" "$log"; fi
145+
else fail "5 mcpp test failed" s5.log; fi
146+
147+
# 6. Index packages that provide modules build, run, and are served from the
148+
# global cache by a second project, their BMIs below their packages' directories.
149+
for d in s6a s6b; do
150+
rm -rf "$W/$d"; mkdir -p "$W/$d/src"
151+
printf '[package]\nname = "eco"\nversion = "0.1.0"\n\n[dependencies]\n"compat.zlib" = "*"\n"mcpplibs.cmdline" = "*"\n\n[targets.eco]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$W/$d/mcpp.toml"
152+
cat > "$W/$d/src/main.cpp" <<'EOF'
153+
#include <cstdio>
154+
#include <zlib.h>
155+
import mcpplibs.cmdline;
156+
int main() { std::printf("zlib %s\n", zlibVersion()); return 0; }
157+
EOF
158+
done
159+
cd "$W/s6a"
160+
if "$MCPP" run > s6a.log 2>&1 && grep -q '^zlib ' s6a.log; then
161+
cd "$W/s6b"
162+
if "$MCPP" run > s6b.log 2>&1 && grep -q '^zlib ' s6b.log && grep -q 'Cached mcpplibs.cmdline' s6b.log; then
163+
pass "6a index packages build, run, and are served from the cache"
164+
if [ -n "$(find target -path '*.cache/*cmdline*/*' -type f | head -1)" ]; then
165+
pass "6b CHANGE: a cached dependency's BMIs are staged below its package's directory"
166+
else fail "6b CHANGE: the cached BMIs are staged at their names" s6b.log; fi
167+
else fail "6a the second project was not served from the cache" s6b.log; fi
168+
else fail "6 index packages" s6a.log; fi
169+
170+
# 7. xlings, built from its source with the release under test.
171+
rm -rf "$W/s7"; mkdir -p "$W/s7"; cd "$W/s7"
172+
if git clone -q --depth 1 https://github.com/openxlings/xlings.git xlings > s7-clone.log 2>&1 \
173+
|| git clone -q --depth 1 https://gitee.com/openxlings/xlings.git xlings > s7-clone.log 2>&1; then
174+
cd xlings
175+
if "$MCPP" build > s7.log 2>&1; then
176+
x=$(find target -path '*/bin/xlings' -type f | head -1)
177+
if [ -n "$x" ] && "$x" --version > s7v.log 2>&1; then
178+
if no_compile "7" ; then pass "7 xlings builds with the release, runs, and a second build compiles nothing: $(head -1 s7v.log)"; fi
179+
else fail "7 the xlings that was built does not run" s7v.log; fi
180+
else fail "7 xlings does not build" s7.log; fi
181+
else skip "7 xlings could not be cloned"; fi
182+
183+
echo "---- $passes passed, $fails failed, $skips skipped"
184+
[ "$fails" = 0 ]

‎src/bmi_cache.cppm‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,25 @@ struct ObjArtifact {
126126

127127
// The artifacts belonging to one package's cache entry: BMI basenames plus the
128128
// objects above.
129+
// Where a BMI of an entry lies in the build's BMI directory when not at its
130+
// name: below its package's directory, which is every package but the root's
131+
// (pack drive and selection design 2026-10-01, B1). Read when the entry is
132+
// populated, as `ObjArtifact::buildRel` is, and never written to entry.json,
133+
// whose BMIs are named by module.
134+
//
135+
// A vector of this pair and not a `std::map<std::string, std::string,
136+
// std::less<>>`: with that member in this struct, clang 22.1.8 crashed
137+
// (SIGSEGV in ASTReader::readTypeRecord) compiling every importer of
138+
// mcpp.build.prepare that instantiates a ranges algorithm of its own, on Linux
139+
// and macOS alike, while GCC 16 compiled it.
140+
struct BmiPlacement {
141+
std::string name; // as `DepArtifacts::bmiFiles` names it
142+
std::string buildRel; // relative to the build's BMI directory
143+
};
144+
129145
struct DepArtifacts {
130146
std::vector<std::string> bmiFiles;
131-
// Where an entry of `bmiFiles` lies in the build's BMI directory when not
132-
// at its name: below its package's directory, which is every package but
133-
// the root's (pack drive and selection design 2026-10-01, B1). Read when
134-
// the entry is populated, as `ObjArtifact::buildRel` is, and never written
135-
// to entry.json, whose BMIs are named by module.
136-
std::map<std::string, std::string, std::less<>> bmiBuildRel;
147+
std::vector<BmiPlacement> bmiPlacements;
137148
std::vector<ObjArtifact> objFiles;
138149
};
139150

@@ -375,8 +386,10 @@ populate_from(const CacheKey& key,
375386
auto projectBmi = projectTargetDir / key.bmiDirName;
376387

377388
for (auto& g : arts.bmiFiles) {
378-
auto rel = arts.bmiBuildRel.find(g);
379-
auto from = projectBmi / (rel == arts.bmiBuildRel.end() ? g : rel->second);
389+
std::string rel = g;
390+
for (auto const& p : arts.bmiPlacements)
391+
if (p.name == g) { rel = p.buildRel; break; }
392+
auto from = projectBmi / rel;
380393
if (!std::filesystem::exists(from)) {
381394
return std::unexpected(std::format(
382395
"expected build output missing: {}", from.string()));

‎src/build/prepare/plan.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2069,7 +2069,7 @@ static std::expected<void, std::string> step13_dependency_cache(PrepareState& st
20692069
bmi.push_back(c == ':' ? '-' : c);
20702070
bmi += std::string(bmiT.bmiExt);
20712071
if (!cu.bmiFile.empty() && cu.bmiFile != bmi)
2072-
arts.bmiBuildRel.emplace(bmi, cu.bmiFile);
2072+
arts.bmiPlacements.push_back({bmi, cu.bmiFile});
20732073
arts.bmiFiles.push_back(std::move(bmi));
20742074
}
20752075
arts.objFiles.push_back({cu.packageObjectRel.generic_string(),

0 commit comments

Comments
 (0)