Skip to content

Commit 77ffe2d

Browse files
authored
Stabilize incremental rebuild caching
Fix BMI cache staging, stabilize toolchain fingerprints, keep e2e toolchain tests within CI budget, and bump version to 0.0.15.
1 parent 4e0cc03 commit 77ffe2d

13 files changed

Lines changed: 558 additions & 31 deletions

.agents/docs/2026-05-15-fingerprint-stability-and-fastpath-coherence.md

Lines changed: 300 additions & 0 deletions
Large diffs are not rendered by default.

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.14"
3+
version = "0.0.15"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/bmi_cache.cppm

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ bool is_cached(const CacheKey& key);
5555
std::expected<DepArtifacts, std::string>
5656
read_manifest(const CacheKey& key);
5757

58-
// Copy cached files into projectTarget/{gcm.cache,obj}, bumping mtime so
59-
// ninja sees them as up-to-date relative to the (untouched) source files.
58+
// Copy missing cached files into projectTarget/{gcm.cache,obj}. Existing
59+
// project outputs are left untouched: GCC BMIs may differ byte-for-byte between
60+
// equivalent builds, and overwriting them would dirty downstream modules.
6061
std::expected<DepArtifacts, std::string>
6162
stage_into(const CacheKey& key,
6263
const std::filesystem::path& projectTargetDir);
@@ -150,6 +151,11 @@ stage_into(const CacheKey& key,
150151
for (auto& g : arts->gcmFiles) {
151152
auto from = key.gcmDir() / g;
152153
auto to = projectGcm / g;
154+
if (std::filesystem::exists(to, ec)) {
155+
ec.clear();
156+
continue;
157+
}
158+
ec.clear();
153159
if (!copy_one(from, to, ec)) {
154160
return std::unexpected(std::format(
155161
"stage gcm '{}': {}", g, ec.message()));
@@ -159,6 +165,11 @@ stage_into(const CacheKey& key,
159165
for (auto& o : arts->objFiles) {
160166
auto from = key.objDir() / o;
161167
auto to = projectObj / o;
168+
if (std::filesystem::exists(to, ec)) {
169+
ec.clear();
170+
continue;
171+
}
172+
ec.clear();
162173
if (!copy_one(from, to, ec)) {
163174
return std::unexpected(std::format(
164175
"stage obj '{}': {}", o, ec.message()));

src/toolchain/detect.cppm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ detect(const std::filesystem::path& explicit_compiler) {
4747
if (!ver_r) return std::unexpected(ver_r.error());
4848

4949
const auto& vstr = *ver_r;
50+
tc.driverIdent = normalize_driver_output(vstr);
5051
auto head = first_line_of(vstr);
5152
auto headLower = lower_copy(head);
5253
auto fullLower = lower_copy(vstr);

src/toolchain/fingerprint.cppm

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// Per docs/06-toolchain-and-fingerprint.md, the fingerprint MUST cover:
44
// 1. compiler id 2. compiler version
5-
// 3. compiler binary hash 4. target triple
5+
// 3. compiler driver identity 4. target triple
66
// 5. stdlib id+version 6. C++ standard
77
// 7. compile flags hash 8. mcpp version
88
// 9. dependency lock hash 10. std module BMI hash
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "0.0.14";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.15";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
@@ -93,7 +93,9 @@ Fingerprint compute_fingerprint(const FingerprintInputs& in) {
9393

9494
fp.parts[0] = std::string(tc.compiler_name());
9595
fp.parts[1] = tc.version;
96-
fp.parts[2] = tc.binaryPath.empty() ? "" : hash_file(tc.binaryPath);
96+
fp.parts[2] = !tc.driverIdent.empty()
97+
? hash_string(tc.driverIdent)
98+
: (tc.binaryPath.empty() ? "" : hash_file(tc.binaryPath));
9799
fp.parts[3] = tc.targetTriple;
98100
fp.parts[4] = std::format("{} {}", tc.stdlibId, tc.stdlibVersion);
99101
fp.parts[5] = in.cppStandard;

src/toolchain/model.cppm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ struct Toolchain {
1212
CompilerId compiler = CompilerId::Unknown;
1313
std::string version; // "15.1.0"
1414
std::filesystem::path binaryPath;
15+
std::string driverIdent; // normalized --version output
1516
std::string targetTriple; // "x86_64-linux-gnu"
1617
std::string stdlibId; // "libstdc++"
1718
std::string stdlibVersion;

src/toolchain/probe.cppm

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ std::string extract_version(std::string_view s);
1818
std::string first_line_of(std::string_view s);
1919
std::string lower_copy(std::string_view s);
2020
std::string trim_line(std::string s);
21+
std::string normalize_driver_output(std::string_view s);
2122

2223
std::vector<std::filesystem::path>
2324
discover_compiler_runtime_dirs(const std::filesystem::path& compilerBin);
@@ -128,6 +129,42 @@ std::string trim_line(std::string s) {
128129
return s;
129130
}
130131

132+
std::string normalize_driver_output(std::string_view s) {
133+
auto replace_local_paths = [](std::string line) {
134+
static constexpr std::array<std::string_view, 3> prefixes{
135+
"/home/", "/tmp/", "/var/"
136+
};
137+
for (auto prefix : prefixes) {
138+
std::size_t pos = 0;
139+
while ((pos = line.find(prefix, pos)) != std::string::npos) {
140+
auto end = pos;
141+
while (end < line.size()) {
142+
unsigned char c = static_cast<unsigned char>(line[end]);
143+
if (std::isspace(c) || line[end] == '\'' || line[end] == '"')
144+
break;
145+
++end;
146+
}
147+
line.replace(pos, end - pos, "<PATH>");
148+
pos += std::string_view("<PATH>").size();
149+
}
150+
}
151+
return line;
152+
};
153+
154+
std::string out;
155+
std::istringstream is(std::string{s});
156+
std::string line;
157+
while (std::getline(is, line)) {
158+
line = trim_line(std::move(line));
159+
if (line.empty()) continue;
160+
if (line.starts_with("PWD=")) continue;
161+
line = replace_local_paths(std::move(line));
162+
if (!out.empty()) out.push_back('\n');
163+
out += line;
164+
}
165+
return out;
166+
}
167+
131168
std::vector<std::filesystem::path>
132169
discover_compiler_runtime_dirs(const std::filesystem::path& compilerBin) {
133170
std::vector<std::filesystem::path> dirs;

tests/e2e/27_namespace_dependencies.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ TMP=$(mktemp -d)
99
trap "rm -rf $TMP" EXIT
1010

1111
export MCPP_HOME="$TMP/mcpp-home"
12+
source "$(dirname "$0")/_inherit_toolchain.sh"
1213

1314
# ── 1. Sibling lib package (acme:util). Pure-modular C++23. ─────────────
1415
mkdir -p "$TMP/util-pkg"

tests/e2e/29_toolchain_partial_versions.sh

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,39 @@
11
#!/usr/bin/env bash
2-
# 29_toolchain_partial_versions.sh — `mcpp toolchain {install,default}` accept
3-
# partial versions and either positional or @-separated form, AND auto-install
4-
# the default toolchain on a first-run `mcpp build` with no toolchain configured.
2+
# 29_toolchain_partial_versions.sh — `mcpp toolchain default` accepts partial
3+
# versions in either positional or @-separated form, AND `mcpp build`
4+
# auto-installs the default toolchain on a first run with no toolchain
5+
# configured. The full install path is covered by 26_toolchain_management.sh.
56
#
6-
# We isolate via MCPP_HOME so we don't touch the user's real ~/.mcpp sandbox.
7+
# We isolate config/default state via MCPP_HOME, while reusing already prepared
8+
# xlings payloads when available so CI does not redownload full toolchains.
79
set -e
810

911
TMP=$(mktemp -d)
1012
trap "rm -rf $TMP" EXIT
1113

12-
# ─── Section 1: dual-form + partial-version toolchain commands ─────────
13-
export MCPP_HOME="$TMP/h1"
14+
inherit_payloads_only() {
15+
MCPP_INHERIT_CONFIG=0 MCPP_INHERIT_SUBOS=0 source "$(dirname "$0")/_inherit_toolchain.sh"
16+
}
1417

15-
# Pre-install both 15 and 16 with different invocation forms.
16-
"$MCPP" toolchain install gcc 15 > "$TMP/inst1.log" 2>&1 || {
17-
cat "$TMP/inst1.log"; echo "install 'gcc 15' failed"; exit 1; }
18-
grep -q '15.1.0' "$TMP/inst1.log" || {
19-
cat "$TMP/inst1.log"; echo "partial '15' didn't resolve to 15.1.0"; exit 1; }
18+
configure_e2e_mirror() {
19+
if [[ -n "${MCPP_E2E_TOOLCHAIN_MIRROR:-}" ]]; then
20+
"$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" > "$TMP/mirror.log" 2>&1 || {
21+
cat "$TMP/mirror.log"
22+
echo "failed to configure e2e mirror"
23+
exit 1
24+
}
25+
fi
26+
}
2027

21-
"$MCPP" toolchain install gcc@16 > "$TMP/inst2.log" 2>&1 || {
22-
cat "$TMP/inst2.log"; echo "install 'gcc@16' failed"; exit 1; }
23-
grep -q '16.1.0' "$TMP/inst2.log" || {
24-
cat "$TMP/inst2.log"; echo "partial '@16' didn't resolve to 16.1.0"; exit 1; }
28+
# ─── Section 1: dual-form + partial-version toolchain commands ─────────
29+
export MCPP_HOME="$TMP/h1"
30+
inherit_payloads_only
31+
configure_e2e_mirror
2532

26-
# Both versions should appear in `list`.
33+
# Reuse the CI-prepared gcc payload. The full install path is covered by
34+
# 26_toolchain_management.sh; this test focuses on partial/default parsing
35+
# without redownloading large toolchain archives.
2736
out=$("$MCPP" toolchain list 2>&1)
28-
[[ "$out" == *"gcc"*"15.1.0"* ]] || { echo "gcc 15.1.0 missing from list:"; echo "$out"; exit 1; }
2937
[[ "$out" == *"gcc"*"16.1.0"* ]] || { echo "gcc 16.1.0 missing from list:"; echo "$out"; exit 1; }
3038

3139
# `default gcc 16` (positional) should pick highest 16.x.y.
@@ -34,17 +42,18 @@ out=$("$MCPP" toolchain list 2>&1)
3442
grep -q 'gcc@16.1.0' "$TMP/def1.log" || {
3543
cat "$TMP/def1.log"; echo "default 'gcc 16' didn't resolve to 16.1.0"; exit 1; }
3644

37-
# `default gcc@15` (@-form) should switch to 15.1.0.
38-
"$MCPP" toolchain default gcc@15 > "$TMP/def2.log" 2>&1 || {
39-
cat "$TMP/def2.log"; echo "default 'gcc@15' failed"; exit 1; }
40-
grep -q 'gcc@15.1.0' "$TMP/def2.log" || {
41-
cat "$TMP/def2.log"; echo "default 'gcc@15' didn't resolve to 15.1.0"; exit 1; }
45+
# `default gcc@16` (@-form) should also resolve to 16.1.0.
46+
"$MCPP" toolchain default gcc@16 > "$TMP/def2.log" 2>&1 || {
47+
cat "$TMP/def2.log"; echo "default 'gcc@16' failed"; exit 1; }
48+
grep -q 'gcc@16.1.0' "$TMP/def2.log" || {
49+
cat "$TMP/def2.log"; echo "default 'gcc@16' didn't resolve to 16.1.0"; exit 1; }
4250

4351
# ─── Section 2: first-run auto-install ──────────────────────────────────
4452
# Brand-new MCPP_HOME, brand-new package with no [toolchain] declared —
4553
# `mcpp build` should auto-install the canonical default (musl-gcc 15.1
4654
# for portable static binaries) + use it. Output should be a static ELF.
4755
export MCPP_HOME="$TMP/h2"
56+
configure_e2e_mirror
4857
mkdir -p "$TMP/proj"
4958
cd "$TMP/proj"
5059
"$MCPP" new hello > /dev/null

tests/e2e/_inherit_toolchain.sh

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,22 @@ if [[ -d "$USER_MCPP/registry/data/xpkgs" ]]; then
1919
[[ -e "$MCPP_HOME/registry/data/xpkgs" ]] \
2020
|| ln -sf "$USER_MCPP/registry/data/xpkgs" "$MCPP_HOME/registry/data/xpkgs"
2121
fi
22-
if [[ -d "$USER_MCPP/registry/subos" ]]; then
22+
if [[ -d "$USER_MCPP/registry/data/xim-pkgindex" ]]; then
23+
mkdir -p "$MCPP_HOME/registry/data"
24+
[[ -e "$MCPP_HOME/registry/data/xim-pkgindex" ]] \
25+
|| ln -sf "$USER_MCPP/registry/data/xim-pkgindex" "$MCPP_HOME/registry/data/xim-pkgindex"
26+
fi
27+
if [[ -d "$USER_MCPP/registry/data/xim-index-repos" ]]; then
28+
mkdir -p "$MCPP_HOME/registry/data"
29+
[[ -e "$MCPP_HOME/registry/data/xim-index-repos" ]] \
30+
|| ln -sf "$USER_MCPP/registry/data/xim-index-repos" "$MCPP_HOME/registry/data/xim-index-repos"
31+
fi
32+
if [[ "${MCPP_INHERIT_SUBOS:-1}" != "0" && -d "$USER_MCPP/registry/subos" ]]; then
2333
mkdir -p "$MCPP_HOME/registry"
2434
[[ -e "$MCPP_HOME/registry/subos" ]] \
2535
|| ln -sf "$USER_MCPP/registry/subos" "$MCPP_HOME/registry/subos"
2636
fi
27-
if [[ -f "$USER_MCPP/config.toml" ]]; then
37+
if [[ "${MCPP_INHERIT_CONFIG:-1}" != "0" && -f "$USER_MCPP/config.toml" ]]; then
2838
cp -f "$USER_MCPP/config.toml" "$MCPP_HOME/config.toml" 2>/dev/null || true
2939
fi
3040
if [[ -d "$USER_MCPP/bin" ]]; then

0 commit comments

Comments
 (0)