diff --git a/.github/workflows/xlings-ci-linux.yml b/.github/workflows/xlings-ci-linux.yml index 8b87bda65..316838052 100644 --- a/.github/workflows/xlings-ci-linux.yml +++ b/.github/workflows/xlings-ci-linux.yml @@ -340,7 +340,7 @@ jobs: echo "XDEV=$xdev" >> "$GITHUB_ENV" export XDEV_BIN="$xdev" mkdir -p target/xdev/linux-xdev - printf '%s\n' '{"name":"linux-xdev","platform":"linux","probes":{}}' > target/xdev/linux-xdev/lane.json + printf '{"name":"linux-xdev","platform":"linux","run":"%s:%s","probes":{}}\n' "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" > target/xdev/linux-xdev/lane.json XTEST_META_OUT="$GITHUB_WORKSPACE/target/xdev/linux-xdev/meta.ndjson" \ XTEST_RESULTS_OUT="$GITHUB_WORKSPACE/target/xdev/linux-xdev/cases.ndjson" \ mcpp test -p xdev @@ -826,7 +826,7 @@ jobs: $dir = "target/xdev/windows-wsl" New-Item -ItemType Directory -Force $dir | Out-Null $name = "wsl: subos export --tar imports with wsl --import (WSL1)" - '{"name":"windows-wsl","platform":"windows","probes":{}}' | Set-Content "$dir/lane.json" + ('{"name":"windows-wsl","platform":"windows","run":"' + $env:GITHUB_RUN_ID + ':' + $env:GITHUB_RUN_ATTEMPT + '","probes":{}}') | Set-Content "$dir/lane.json" ('{"test":"' + $name + '","area":"scenario","covers":["EXPORT-WSL"],"requires":[],"proves":"flow"}') | Set-Content "$dir/meta.ndjson" if ("${{ steps.run.outcome }}" -eq "success") { $st = "pass"; $msg = "" } elseif ("${{ steps.setup.outcome }}" -ne "success") { $st = "skip"; $msg = "WSL1 could not be set up on this runner" } diff --git a/.github/workflows/xlings-ci-macos.yml b/.github/workflows/xlings-ci-macos.yml index 5fe0817dd..d6cbf69a7 100644 --- a/.github/workflows/xlings-ci-macos.yml +++ b/.github/workflows/xlings-ci-macos.yml @@ -290,7 +290,7 @@ jobs: xdev="$GITHUB_WORKSPACE/$xdev" export XDEV_BIN="$xdev" mkdir -p target/xdev/macos-xdev - printf '%s\n' '{"name":"macos-xdev","platform":"macosx","probes":{}}' > target/xdev/macos-xdev/lane.json + printf '{"name":"macos-xdev","platform":"macos","run":"%s:%s","probes":{}}\n' "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" > target/xdev/macos-xdev/lane.json XTEST_META_OUT="$GITHUB_WORKSPACE/target/xdev/macos-xdev/meta.ndjson" \ XTEST_RESULTS_OUT="$GITHUB_WORKSPACE/target/xdev/macos-xdev/cases.ndjson" \ mcpp test -p xdev diff --git a/.github/workflows/xlings-ci-windows.yml b/.github/workflows/xlings-ci-windows.yml index 08407020a..3bc60b930 100644 --- a/.github/workflows/xlings-ci-windows.yml +++ b/.github/workflows/xlings-ci-windows.yml @@ -288,7 +288,7 @@ jobs: if (-not $xdev) { Write-Host "::error::mcpp build -p xdev produced no xdev.exe under target/"; exit 1 } $env:XDEV_BIN = $xdev.FullName New-Item -ItemType Directory -Force target/xdev/windows-xdev | Out-Null - '{"name":"windows-xdev","platform":"windows","probes":{}}' | Set-Content target/xdev/windows-xdev/lane.json + ('{"name":"windows-xdev","platform":"windows","run":"' + $env:GITHUB_RUN_ID + ':' + $env:GITHUB_RUN_ATTEMPT + '","probes":{}}') | Set-Content target/xdev/windows-xdev/lane.json $env:XTEST_META_OUT = "$env:GITHUB_WORKSPACE/target/xdev/windows-xdev/meta.ndjson" $env:XTEST_RESULTS_OUT = "$env:GITHUB_WORKSPACE/target/xdev/windows-xdev/cases.ndjson" mcpp test -p xdev diff --git a/apps/xdev/src/main.cpp b/apps/xdev/src/main.cpp index fd4f5de46..01e68499a 100644 --- a/apps/xdev/src/main.cpp +++ b/apps/xdev/src/main.cpp @@ -696,11 +696,15 @@ int cmd_report_dirs(const std::vector& dirs, bool summary, lanes.push_back(l); } const auto first = records.size(); - std::string execution = fs::absolute(dir).lexically_normal().string(); + // A trend sample needs the execution it came from. A lane that does + // not say (a hand-written lane.json) is reported but never sampled: + // its directory names the same place in every CI run, so it would read + // as one execution with two results. + std::string execution; std::string platform; if (!lanes.empty() && lanes.back().value("name", "") == lane_name) { platform = lanes.back().value("platform", ""); - execution = lanes.back().value("run", execution); + execution = lanes.back().value("run", ""); } for (auto& j : read_ndjson(dir / "mcpp.ndjson")) { if (!j.contains("test")) continue; @@ -735,7 +739,7 @@ int cmd_report_dirs(const std::vector& dirs, bool summary, if (!program.empty()) j["test"] = program + ":" + name; meta[{lane_name, j.value("test", "")}] = j; } - if (!platform.empty()) + if (!platform.empty() && !execution.empty()) for (auto i = first; i < records.size(); ++i) { const auto& r = records[i]; observations.push_back({r.kind, r.name, platform, lane_name, execution, r.status, r.ms}); diff --git a/apps/xdev/tests/test_cli.cpp b/apps/xdev/tests/test_cli.cpp index 0fd637138..a92b59cea 100644 --- a/apps/xdev/tests/test_cli.cpp +++ b/apps/xdev/tests/test_cli.cpp @@ -232,6 +232,21 @@ XTEST(XdevCli, TrendArtifactsPreserveMemberAndPlatformAndFeedFutureShardPlans, "--trend", (failure / "trend.json").string(), "--write", (fixture.home.root() / "repeat").string()}); EXPECT_EQ(duplicate.exit_code, 1); EXPECT_EQ(json::parse(tk::read_file(fixture.home.root() / "repeat/trend.json"))["observations"].size(), 3); + // A lane that does not name its execution is reported, never sampled: + // its directory is the same in every CI run. + const auto unnamed = fixture.home.root() / "input-unnamed"; + tk::write_file(unnamed / "lane.json", json{{"name", "linux-xdev"}, {"platform", "linux"}}.dump()); + tk::write_file(unnamed / "cases.ndjson", json{{"program", "xdev"}, {"test", "Loopback.Case"}, + {"status", "pass"}, {"ms", 7}}.dump() + "\n"); + for (int again = 0; again != 2; ++again) { + const auto out = fixture.home.root() / std::format("unnamed-{}", again); + const auto rendered = fixture.run({"report", "--in", unnamed.string(), "--trend", + (failure / "trend.json").string(), "--write", out.string()}); + EXPECT_EQ(rendered.exit_code, 0) << rendered.transcript(); + EXPECT_EQ(json::parse(tk::read_file(out / "trend.json"))["observations"].size(), 3); + tk::write_file(unnamed / "cases.ndjson", json{{"program", "xdev"}, {"test", "Loopback.Case"}, + {"status", "pass"}, {"ms", 9}}.dump() + "\n"); + } tk::write_file(fixture.home.root() / "tests/unit/slow.cpp", "import std;"); const auto inventory = fixture.discovery({json{{"member", "xdev"}, {"test", "unit/slow"}, {"main", "tests/unit/slow.cpp"}, {"cases", json::array({test_case("Slow.Check")})}}}); diff --git a/mcpp.toml b/mcpp.toml index d6d260e16..d661ebaf2 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -41,7 +41,7 @@ members = [ [package] name = "xlings" -version = "2026.10.9.1" +version = "2026.10.9.2" description = "Universal package management infrastructure tool with SubOS isolation" license = "Apache-2.0" repo = "https://github.com/openxlings/xlings" diff --git a/src/core/config.cppm b/src/core/config.cppm index b3d0e4787..60ed301c8 100644 --- a/src/core/config.cppm +++ b/src/core/config.cppm @@ -11,7 +11,7 @@ import xlings.core.home; namespace xlings { export struct Info { - static constexpr std::string_view VERSION = "2026.10.9.1"; + static constexpr std::string_view VERSION = "2026.10.9.2"; static constexpr std::string_view REPO = "https://github.com/openxlings/xlings"; }; diff --git a/src/core/xvm/materialize.cpp b/src/core/xvm/materialize.cpp index e90521143..ff7726d9a 100644 --- a/src/core/xvm/materialize.cpp +++ b/src/core/xvm/materialize.cpp @@ -395,13 +395,29 @@ std::expected preflight_materialization( if (!expanded) return std::unexpected(expanded.error()); } } + // An asset a recipe declares that this payload does not have is not + // placed -- as before this materializer: recipes declare their assets + // across versions, and a version without one (gcc 15.1.0 ships no + // libasan.so) is not a broken install. What exists and cannot be read + // still refuses: that is not "absent". + for (auto it = operations.begin(); it != operations.end();) { + if (it->second.change.remove) { ++it; continue; } + std::error_code ec; + const auto status = fs::status(it->second.change.source, ec); + if (status.type() == fs::file_type::not_found + && (!ec || ec == std::errc::no_such_file_or_directory)) { + log::debug("[xvm] asset source missing, not placed: {}", it->second.change.source.string()); + it = operations.erase(it); + continue; + } + if (ec) return std::unexpected(it->second.change.source.string() + ": asset source is unreadable: " + ec.message()); + ++it; + } // A declared directory and another package's descendant must merge into // a real directory, never write through the existing payload link. for (auto& [destination, operation] : operations) { if (operation.change.remove) continue; std::error_code ec; - if (!fs::exists(operation.change.source, ec) || ec) - return std::unexpected(operation.change.source.string() + ": asset source is missing or unreadable"); if (fs::is_directory(operation.change.source, ec)) { for (const auto& [child, nested] : operations) if (!nested.change.remove && below_(destination, child)) operation.unwrap = true; diff --git a/tests/unit/test_xvm_materialize.cpp b/tests/unit/test_xvm_materialize.cpp index 797a5b827..16927ab0f 100644 --- a/tests/unit/test_xvm_materialize.cpp +++ b/tests/unit/test_xvm_materialize.cpp @@ -300,3 +300,27 @@ XTEST(XvmMaterialize, UseConflictDoesNotPublishMetadataOrDeleteUserHeaders, .are EXPECT_EQ(tk::read_file(scope / "usr/include/shared.h.xlings-new/sentinel"), "user staging"); EXPECT_EQ(nlohmann::json::parse(tk::read_file(scope / ".xlings.json")), scopeConfig); } + +XTEST(XvmMaterialize, AnAssetThePayloadDoesNotShipIsNotPlacedAndDoesNotRefuseTheRest, + .area = "xvm", .covers = {"HOME-LAYER-RESOLVE"}) { + // gcc 15.1.0's recipe declares lib64/libasan.so, which that build does + // not ship. The 2026.10.9.1 materializer refused the whole update over it + // (fresh-install, gcc suite); an absent source is skipped, as it was + // before -- the others are placed. + auto home = tk::Home::isolated("asset-absent-source"); + const auto scope = home.dir() / "subos/default"; + const auto payload = home.dir() / "data/xpkgs/xim-x-gcc/15.1.0/lib64"; + tk::write_file(payload / "libstdc++.so.6", "present"); + const std::vector changes{ + {payload / "libstdc++.so.6", scope / "lib/libstdc++.so.6", false}, + {payload / "libasan.so", scope / "lib/libasan.so", false}, + }; + auto prepared = m::preflight_materialization(changes, {}, scope); + ASSERT_TRUE(prepared) << prepared.error(); + auto applied = prepared->execute(); + ASSERT_TRUE(applied) << applied.error(); + ASSERT_TRUE(applied->commit()); + EXPECT_TRUE(fs::exists(scope / "lib/libstdc++.so.6")); + std::error_code ec; + EXPECT_FALSE(fs::exists(fs::symlink_status(scope / "lib/libasan.so", ec))) << "absent: not placed"; +}