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

Large diffs are not rendered by default.

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 = "0.0.14"
version = "0.0.15"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
15 changes: 13 additions & 2 deletions src/bmi_cache.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ bool is_cached(const CacheKey& key);
std::expected<DepArtifacts, std::string>
read_manifest(const CacheKey& key);

// Copy cached files into projectTarget/{gcm.cache,obj}, bumping mtime so
// ninja sees them as up-to-date relative to the (untouched) source files.
// Copy missing cached files into projectTarget/{gcm.cache,obj}. Existing
// project outputs are left untouched: GCC BMIs may differ byte-for-byte between
// equivalent builds, and overwriting them would dirty downstream modules.
std::expected<DepArtifacts, std::string>
stage_into(const CacheKey& key,
const std::filesystem::path& projectTargetDir);
Expand Down Expand Up @@ -150,6 +151,11 @@ stage_into(const CacheKey& key,
for (auto& g : arts->gcmFiles) {
auto from = key.gcmDir() / g;
auto to = projectGcm / g;
if (std::filesystem::exists(to, ec)) {
ec.clear();
continue;
}
ec.clear();
if (!copy_one(from, to, ec)) {
return std::unexpected(std::format(
"stage gcm '{}': {}", g, ec.message()));
Expand All @@ -159,6 +165,11 @@ stage_into(const CacheKey& key,
for (auto& o : arts->objFiles) {
auto from = key.objDir() / o;
auto to = projectObj / o;
if (std::filesystem::exists(to, ec)) {
ec.clear();
continue;
}
ec.clear();
if (!copy_one(from, to, ec)) {
return std::unexpected(std::format(
"stage obj '{}': {}", o, ec.message()));
Expand Down
1 change: 1 addition & 0 deletions src/toolchain/detect.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ detect(const std::filesystem::path& explicit_compiler) {
if (!ver_r) return std::unexpected(ver_r.error());

const auto& vstr = *ver_r;
tc.driverIdent = normalize_driver_output(vstr);
auto head = first_line_of(vstr);
auto headLower = lower_copy(head);
auto fullLower = lower_copy(vstr);
Expand Down
8 changes: 5 additions & 3 deletions src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// Per docs/06-toolchain-and-fingerprint.md, the fingerprint MUST cover:
// 1. compiler id 2. compiler version
// 3. compiler binary hash 4. target triple
// 3. compiler driver identity 4. target triple
// 5. stdlib id+version 6. C++ standard
// 7. compile flags hash 8. mcpp version
// 9. dependency lock hash 10. std module BMI hash
Expand All @@ -18,7 +18,7 @@ import mcpp.toolchain.detect;

export namespace mcpp::toolchain {

inline constexpr std::string_view MCPP_VERSION = "0.0.14";
inline constexpr std::string_view MCPP_VERSION = "0.0.15";

struct FingerprintInputs {
Toolchain toolchain;
Expand Down Expand Up @@ -93,7 +93,9 @@ Fingerprint compute_fingerprint(const FingerprintInputs& in) {

fp.parts[0] = std::string(tc.compiler_name());
fp.parts[1] = tc.version;
fp.parts[2] = tc.binaryPath.empty() ? "" : hash_file(tc.binaryPath);
fp.parts[2] = !tc.driverIdent.empty()
? hash_string(tc.driverIdent)
: (tc.binaryPath.empty() ? "" : hash_file(tc.binaryPath));
fp.parts[3] = tc.targetTriple;
fp.parts[4] = std::format("{} {}", tc.stdlibId, tc.stdlibVersion);
fp.parts[5] = in.cppStandard;
Expand Down
1 change: 1 addition & 0 deletions src/toolchain/model.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct Toolchain {
CompilerId compiler = CompilerId::Unknown;
std::string version; // "15.1.0"
std::filesystem::path binaryPath;
std::string driverIdent; // normalized --version output
std::string targetTriple; // "x86_64-linux-gnu"
std::string stdlibId; // "libstdc++"
std::string stdlibVersion;
Expand Down
37 changes: 37 additions & 0 deletions src/toolchain/probe.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ std::string extract_version(std::string_view s);
std::string first_line_of(std::string_view s);
std::string lower_copy(std::string_view s);
std::string trim_line(std::string s);
std::string normalize_driver_output(std::string_view s);

std::vector<std::filesystem::path>
discover_compiler_runtime_dirs(const std::filesystem::path& compilerBin);
Expand Down Expand Up @@ -128,6 +129,42 @@ std::string trim_line(std::string s) {
return s;
}

std::string normalize_driver_output(std::string_view s) {
auto replace_local_paths = [](std::string line) {
static constexpr std::array<std::string_view, 3> prefixes{
"/home/", "/tmp/", "/var/"
};
for (auto prefix : prefixes) {
std::size_t pos = 0;
while ((pos = line.find(prefix, pos)) != std::string::npos) {
auto end = pos;
while (end < line.size()) {
unsigned char c = static_cast<unsigned char>(line[end]);
if (std::isspace(c) || line[end] == '\'' || line[end] == '"')
break;
++end;
}
line.replace(pos, end - pos, "<PATH>");
pos += std::string_view("<PATH>").size();
}
}
return line;
};

std::string out;
std::istringstream is(std::string{s});
std::string line;
while (std::getline(is, line)) {
line = trim_line(std::move(line));
if (line.empty()) continue;
if (line.starts_with("PWD=")) continue;
line = replace_local_paths(std::move(line));
if (!out.empty()) out.push_back('\n');
out += line;
}
return out;
}

std::vector<std::filesystem::path>
discover_compiler_runtime_dirs(const std::filesystem::path& compilerBin) {
std::vector<std::filesystem::path> dirs;
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/27_namespace_dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ TMP=$(mktemp -d)
trap "rm -rf $TMP" EXIT

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

# ── 1. Sibling lib package (acme:util). Pure-modular C++23. ─────────────
mkdir -p "$TMP/util-pkg"
Expand Down
53 changes: 31 additions & 22 deletions tests/e2e/29_toolchain_partial_versions.sh
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
#!/usr/bin/env bash
# 29_toolchain_partial_versions.sh — `mcpp toolchain {install,default}` accept
# partial versions and either positional or @-separated form, AND auto-install
# the default toolchain on a first-run `mcpp build` with no toolchain configured.
# 29_toolchain_partial_versions.sh — `mcpp toolchain default` accepts partial
# versions in either positional or @-separated form, AND `mcpp build`
# auto-installs the default toolchain on a first run with no toolchain
# configured. The full install path is covered by 26_toolchain_management.sh.
#
# We isolate via MCPP_HOME so we don't touch the user's real ~/.mcpp sandbox.
# We isolate config/default state via MCPP_HOME, while reusing already prepared
# xlings payloads when available so CI does not redownload full toolchains.
set -e

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

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

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

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

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

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

# `default gcc@15` (@-form) should switch to 15.1.0.
"$MCPP" toolchain default gcc@15 > "$TMP/def2.log" 2>&1 || {
cat "$TMP/def2.log"; echo "default 'gcc@15' failed"; exit 1; }
grep -q 'gcc@15.1.0' "$TMP/def2.log" || {
cat "$TMP/def2.log"; echo "default 'gcc@15' didn't resolve to 15.1.0"; exit 1; }
# `default gcc@16` (@-form) should also resolve to 16.1.0.
"$MCPP" toolchain default gcc@16 > "$TMP/def2.log" 2>&1 || {
cat "$TMP/def2.log"; echo "default 'gcc@16' failed"; exit 1; }
grep -q 'gcc@16.1.0' "$TMP/def2.log" || {
cat "$TMP/def2.log"; echo "default 'gcc@16' didn't resolve to 16.1.0"; exit 1; }

# ─── Section 2: first-run auto-install ──────────────────────────────────
# Brand-new MCPP_HOME, brand-new package with no [toolchain] declared —
# `mcpp build` should auto-install the canonical default (musl-gcc 15.1
# for portable static binaries) + use it. Output should be a static ELF.
export MCPP_HOME="$TMP/h2"
configure_e2e_mirror
mkdir -p "$TMP/proj"
cd "$TMP/proj"
"$MCPP" new hello > /dev/null
Expand Down
14 changes: 12 additions & 2 deletions tests/e2e/_inherit_toolchain.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,22 @@ if [[ -d "$USER_MCPP/registry/data/xpkgs" ]]; then
[[ -e "$MCPP_HOME/registry/data/xpkgs" ]] \
|| ln -sf "$USER_MCPP/registry/data/xpkgs" "$MCPP_HOME/registry/data/xpkgs"
fi
if [[ -d "$USER_MCPP/registry/subos" ]]; then
if [[ -d "$USER_MCPP/registry/data/xim-pkgindex" ]]; then
mkdir -p "$MCPP_HOME/registry/data"
[[ -e "$MCPP_HOME/registry/data/xim-pkgindex" ]] \
|| ln -sf "$USER_MCPP/registry/data/xim-pkgindex" "$MCPP_HOME/registry/data/xim-pkgindex"
fi
if [[ -d "$USER_MCPP/registry/data/xim-index-repos" ]]; then
mkdir -p "$MCPP_HOME/registry/data"
[[ -e "$MCPP_HOME/registry/data/xim-index-repos" ]] \
|| ln -sf "$USER_MCPP/registry/data/xim-index-repos" "$MCPP_HOME/registry/data/xim-index-repos"
fi
if [[ "${MCPP_INHERIT_SUBOS:-1}" != "0" && -d "$USER_MCPP/registry/subos" ]]; then
mkdir -p "$MCPP_HOME/registry"
[[ -e "$MCPP_HOME/registry/subos" ]] \
|| ln -sf "$USER_MCPP/registry/subos" "$MCPP_HOME/registry/subos"
fi
if [[ -f "$USER_MCPP/config.toml" ]]; then
if [[ "${MCPP_INHERIT_CONFIG:-1}" != "0" && -f "$USER_MCPP/config.toml" ]]; then
cp -f "$USER_MCPP/config.toml" "$MCPP_HOME/config.toml" 2>/dev/null || true
fi
if [[ -d "$USER_MCPP/bin" ]]; then
Expand Down
72 changes: 72 additions & 0 deletions tests/unit/test_bmi_cache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,78 @@ TEST(BmiCache, PopulateThenStageRoundTrip) {
EXPECT_EQ(body, "OBJ-A");
}

TEST(BmiCache, StageIntoDoesNotTouchIdenticalOutputs) {
Tmp t;
auto home = t.path / "home";
auto project = t.path / "proj" / "target";
std::filesystem::create_directories(project / "gcm.cache");
std::filesystem::create_directories(project / "obj");

writeFile(project / "gcm.cache" / "mcpplibs.cmdline.gcm", "GCM-A");
writeFile(project / "obj" / "cmdline.m.o", "OBJ-A");

DepArtifacts arts {
.gcmFiles = { "mcpplibs.cmdline.gcm" },
.objFiles = { "cmdline.m.o" },
};

auto k = makeKey(home);
ASSERT_TRUE(populate_from(k, project, arts));

auto staged = stage_into(k, project);
ASSERT_TRUE(staged) << staged.error();
auto gcmTime = std::filesystem::last_write_time(project / "gcm.cache" / "mcpplibs.cmdline.gcm");
auto objTime = std::filesystem::last_write_time(project / "obj" / "cmdline.m.o");

auto stagedAgain = stage_into(k, project);
ASSERT_TRUE(stagedAgain) << stagedAgain.error();
EXPECT_EQ(std::filesystem::last_write_time(project / "gcm.cache" / "mcpplibs.cmdline.gcm"), gcmTime);
EXPECT_EQ(std::filesystem::last_write_time(project / "obj" / "cmdline.m.o"), objTime);
}

TEST(BmiCache, StageIntoDoesNotOverwriteExistingOutputs) {
Tmp t;
auto home = t.path / "home";
auto cacheProject = t.path / "cache-proj" / "target";
auto project = t.path / "proj" / "target";
std::filesystem::create_directories(cacheProject / "gcm.cache");
std::filesystem::create_directories(cacheProject / "obj");
std::filesystem::create_directories(project / "gcm.cache");
std::filesystem::create_directories(project / "obj");

writeFile(cacheProject / "gcm.cache" / "mcpplibs.cmdline.gcm", "CACHE-GCM");
writeFile(cacheProject / "obj" / "cmdline.m.o", "CACHE-OBJ");

DepArtifacts arts {
.gcmFiles = { "mcpplibs.cmdline.gcm" },
.objFiles = { "cmdline.m.o" },
};

auto k = makeKey(home);
ASSERT_TRUE(populate_from(k, cacheProject, arts));

writeFile(project / "gcm.cache" / "mcpplibs.cmdline.gcm", "PROJECT-GCM");
writeFile(project / "obj" / "cmdline.m.o", "PROJECT-OBJ");
auto gcmTime = std::filesystem::last_write_time(project / "gcm.cache" / "mcpplibs.cmdline.gcm");
auto objTime = std::filesystem::last_write_time(project / "obj" / "cmdline.m.o");

auto staged = stage_into(k, project);
ASSERT_TRUE(staged) << staged.error();

{
std::ifstream is(project / "gcm.cache" / "mcpplibs.cmdline.gcm");
std::string body((std::istreambuf_iterator<char>(is)), {});
EXPECT_EQ(body, "PROJECT-GCM");
}
{
std::ifstream is(project / "obj" / "cmdline.m.o");
std::string body((std::istreambuf_iterator<char>(is)), {});
EXPECT_EQ(body, "PROJECT-OBJ");
}
EXPECT_EQ(std::filesystem::last_write_time(project / "gcm.cache" / "mcpplibs.cmdline.gcm"), gcmTime);
EXPECT_EQ(std::filesystem::last_write_time(project / "obj" / "cmdline.m.o"), objTime);
}

TEST(BmiCache, IsCachedFalseWhenSentinelExistsButFileMissing) {
Tmp t;
auto home = t.path / "home";
Expand Down
12 changes: 11 additions & 1 deletion tests/unit/test_fingerprint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ FingerprintInputs baseline() {
in.toolchain.compiler = CompilerId::GCC;
in.toolchain.version = "16.1.0";
in.toolchain.binaryPath = "/usr/bin/g++";
in.toolchain.driverIdent = "g++ (xim-x-gcc 16.1.0) 16.1.0";
in.toolchain.targetTriple = "x86_64-linux-gnu";
in.toolchain.stdlibId = "libstdc++";
in.toolchain.stdlibVersion = "16.1.0";
Expand Down Expand Up @@ -52,7 +53,7 @@ TEST(Fingerprint, ProducesSixteenHexChars) {
TEST(Fingerprint, AllTenFieldsAffectHash) {
EXPECT_DIFFERENT(in.toolchain.compiler = CompilerId::Clang);
EXPECT_DIFFERENT(in.toolchain.version = "16.0.0");
EXPECT_DIFFERENT(in.toolchain.binaryPath = "/elsewhere/g++");
EXPECT_DIFFERENT(in.toolchain.driverIdent = "g++ (xim-x-gcc 15.1.0) 15.1.0");
EXPECT_DIFFERENT(in.toolchain.targetTriple = "aarch64-linux-gnu");
EXPECT_DIFFERENT(in.toolchain.stdlibId = "libc++");
EXPECT_DIFFERENT(in.cppStandard = "c++26");
Expand All @@ -62,6 +63,15 @@ TEST(Fingerprint, AllTenFieldsAffectHash) {
EXPECT_DIFFERENT(in.stdBmiHash = "ffffffffffffffff");
}

TEST(Fingerprint, StableAcrossBinaryPathsWhenDriverIdentMatches) {
auto a = baseline();
auto b = baseline();
a.toolchain.binaryPath = "/home/speak/.mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++";
b.toolchain.binaryPath = "/home/speak/.xlings/data/xpkgs/xim-x-mcpp/0.0.14/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++";

EXPECT_EQ(compute_fingerprint(a).hex, compute_fingerprint(b).hex);
}

TEST(Fingerprint, HashStringMatchesHashFile) {
auto h1 = hash_string("hello");
auto h2 = hash_string("hello");
Expand Down
Loading
Loading