|
| 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 ] |
0 commit comments