Skip to content

Commit c136000

Browse files
committed
Consume the xlings revision and glibc runtime data
The packaging revision xlings records for a payload (`.xpkg-install.json`, openxlings/xlings#620) is read in one place, `mcpp::xlings::paths::installed_revision`, and used twice. The runtime contract (fingerprint field 11) includes the revision of each runtime payload whose revision is 1 or more, so a C library reinstalled under the same version with changed content selects a new output directory. Revision 0 contributes nothing, so no existing key changes. An index dependency whose recorded revision differs from its descriptor's entry is not installed (#524 A): mcpp's descriptor reader now reads `revision` the way libxpkg does, and a stale payload goes back through xlings, which reinstalls it. A payload with no xlings record is judged by the marker, as before. `mcpp pack --mode self-contained` copies the bundled C library's `lib/locale` and `lib/gconv` from its payload when present and exports LOCPATH and GCONV_PATH in the launcher, keeping a value the user set (openxlings/xlings#621). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MajuX4J8ewFeWvZJjzRJt5
1 parent aac072c commit c136000

7 files changed

Lines changed: 183 additions & 3 deletions

File tree

‎modules/manifest/src/xpkg.cppm‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ struct XpkgVersionEntry {
3838
std::string version; // the literal key, as written
3939
bool alias = false; // entry carries `ref = "..."`
4040
std::string sha256; // payload digest when declared at entry level
41+
// The packaging revision of this version's payload (xpkg V2 `revision`),
42+
// 0 when the entry states none or states something that is not a
43+
// non-negative integer -- the reference implementation's reading.
44+
int revision = 0;
4145
};
4246

4347
// Extract the version entries for `platform` (e.g. "linux", "macosx",
@@ -574,6 +578,40 @@ std::string top_level_table_body_for_key(std::string_view body, std::string_view
574578
return {};
575579
}
576580

581+
// The non-negative integer bound to `wantedKey` at the top level of `body`,
582+
// or 0: `revision = 1` reads 1, and a string, a negative or a fractional value
583+
// reads as absent.
584+
int top_level_nonneg_int_for_key(std::string_view body, std::string_view wantedKey) {
585+
LuaCursor cur { body };
586+
cur.skip_ws_and_comments();
587+
while (!cur.eof()) {
588+
auto key = cur.read_key();
589+
if (key.empty()) {
590+
cur.skip_ws_and_comments();
591+
if (cur.eof()) break;
592+
++cur.pos;
593+
continue;
594+
}
595+
cur.skip_ws_and_comments();
596+
if (!cur.consume('=')) {
597+
cur.skip_ws_and_comments();
598+
continue;
599+
}
600+
cur.skip_ws_and_comments();
601+
if (cur.peek() == '{') { cur.skip_table(); cur.skip_ws_and_comments(); continue; }
602+
if (cur.at_string_start()) { (void)cur.read_string(); cur.skip_ws_and_comments(); continue; }
603+
const auto word = cur.read_bareword();
604+
if (key == wantedKey) {
605+
if (word.empty() || word.size() > 9
606+
|| !std::ranges::all_of(word, [](char c) { return c >= '0' && c <= '9'; }))
607+
return 0;
608+
return std::stoi(std::string(word));
609+
}
610+
cur.skip_ws_and_comments();
611+
}
612+
return 0;
613+
}
614+
577615
std::string top_level_string_value_for_key(std::string_view body, std::string_view wantedKey) {
578616
LuaCursor cur { body };
579617
cur.skip_ws_and_comments();
@@ -1072,6 +1110,8 @@ list_xpkg_version_entries(std::string_view luaContent,
10721110
e.alias = entry_is_alias(v, entry_end);
10731111
e.sha256 = top_level_string_value_for_key(
10741112
luaContent.substr(v + 1, entry_end - v - 1), "sha256");
1113+
e.revision = top_level_nonneg_int_for_key(
1114+
luaContent.substr(v + 1, entry_end - v - 1), "revision");
10751115
versions.push_back(std::move(e));
10761116
q = entry_end + 1;
10771117
continue;

‎src/build/prepare.cppm‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6130,9 +6130,33 @@ prepare_build(bool print_fingerprint,
61306130
return false;
61316131
};
61326132

6133+
// THE DESCRIPTOR'S REVISION IS PART OF WHAT IS INSTALLED (#524 A,
6134+
// openxlings/xlings#620). A descriptor that changes what it installs
6135+
// keeps its version and raises the entry's `revision`; a payload whose
6136+
// recorded revision differs is not this version any more, however
6137+
// complete it is, and goes back through xlings, which reinstalls it
6138+
// and records the new revision. A payload with no xlings record at
6139+
// all is judged by the marker alone, as before.
6140+
const int recipeRevision = [&] {
6141+
if (!luaContent) return 0;
6142+
for (auto const& e : mcpp::manifest::list_xpkg_version_entries(
6143+
*luaContent, targetPlatform))
6144+
if (e.version == version) return e.revision;
6145+
return 0;
6146+
}();
6147+
auto revisionIsCurrent = [&](const std::filesystem::path& p) {
6148+
const auto installed = mcpp::xlings::paths::installed_revision(p);
6149+
if (!installed || *installed == recipeRevision) return true;
6150+
mcpp::log::verbose("fetcher", std::format(
6151+
"{}@{}: installed revision {}, descriptor revision {}; reinstalling",
6152+
depName, version, *installed, recipeRevision));
6153+
return false;
6154+
};
6155+
61336156
auto findCompleteInstalled = [&]() -> std::optional<std::filesystem::path> {
61346157
auto p = findRawInstalled();
61356158
if (!p) return std::nullopt;
6159+
if (!revisionIsCurrent(*p)) return std::nullopt;
61366160
if (mcpp::fallback::is_install_complete(*p)) return p;
61376161
if (installedLayoutMatchesIndex(*p)) {
61386162
mcpp::fallback::mark_install_complete(*p);

‎src/pack/pack.cppm‎

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,8 +1041,21 @@ write_executable_script(const std::filesystem::path& path,
10411041
std::expected<void, std::string>
10421042
write_bundle_all_wrappers(const std::filesystem::path& stagingRoot,
10431043
std::string_view binaryName,
1044-
std::string_view loaderName)
1044+
std::string_view loaderName,
1045+
bool localeData = false,
1046+
bool gconvData = false)
10451047
{
1048+
// glibc's compiled locales and its character-set converters, when the
1049+
// bundle carries them (openxlings/xlings#621). The C library that runs here
1050+
// is the bundled one, and it reads both from paths compiled into it, which
1051+
// name the machine the payload was installed on. This script is the only
1052+
// entry of the bundle, so exporting the variables here reaches every run;
1053+
// a value the user already set is kept.
1054+
std::string runtimeData;
1055+
if (localeData)
1056+
runtimeData += "LOCPATH=\"${LOCPATH:-$here/lib/locale}\"\nexport LOCPATH\n";
1057+
if (gconvData)
1058+
runtimeData += "GCONV_PATH=\"${GCONV_PATH:-$here/lib/gconv}\"\nexport GCONV_PATH\n";
10461059
auto body = std::format(
10471060
"#!/bin/sh\n"
10481061
"# Auto-generated by `mcpp pack --mode self-contained`. Launches the\n"
@@ -1066,8 +1079,9 @@ write_bundle_all_wrappers(const std::filesystem::path& stagingRoot,
10661079
"here=$(cd \"$(dirname \"$0\")\" && pwd)\n"
10671080
"MCPP_BUNDLE_DIR=\"$here\"\n"
10681081
"export MCPP_BUNDLE_DIR\n"
1082+
"{}"
10691083
"exec \"$here/lib/{}\" --library-path \"$here/lib\" \"$here/bin/{}\" \"$@\"\n",
1070-
loaderName, binaryName);
1084+
runtimeData, loaderName, binaryName);
10711085
if (auto r = write_executable_script(stagingRoot / "run.sh", body); !r) return r;
10721086
if (auto r = write_executable_script(stagingRoot / std::string(binaryName), body); !r) return r;
10731087
return {};
@@ -2312,11 +2326,39 @@ run(const Plan& plan, const mcpp::config::GlobalConfig& cfg)
23122326
// which the user typically wouldn't combine with --mode
23132327
// bundle-all. Skip wrapper, ship as-is.
23142328
} else {
2329+
// The runtime data of the C library the bundle carries
2330+
// (openxlings/xlings#621): its compiled locales and its
2331+
// character-set converters, from the payload the bundled
2332+
// loader came from (`<payload>/lib64/ld-linux-*` ->
2333+
// `<payload>/lib/{locale,gconv}`). A payload without them
2334+
// contributes nothing, and the program runs as before.
2335+
bool localeData = false, gconvData = false;
2336+
for (auto const& d : toBundle) {
2337+
if (d.soname != loader) continue;
2338+
std::error_code dec;
2339+
const auto payload =
2340+
std::filesystem::canonical(d.path, dec).parent_path().parent_path();
2341+
if (dec) break;
2342+
auto copy_dir = [&](std::string_view sub) {
2343+
const auto from = payload / "lib" / std::string(sub);
2344+
std::error_code cec;
2345+
if (!std::filesystem::is_directory(from, cec)) return false;
2346+
const auto to = plan.stagingRoot / "lib" / std::string(sub);
2347+
std::filesystem::create_directories(to, cec);
2348+
std::filesystem::copy(from, to,
2349+
std::filesystem::copy_options::recursive
2350+
| std::filesystem::copy_options::overwrite_existing, cec);
2351+
return !cec;
2352+
};
2353+
localeData = copy_dir("locale");
2354+
gconvData = copy_dir("gconv");
2355+
break;
2356+
}
23152357
// Mode B writes BOTH `run.sh` and `<binary_name>` at the
23162358
// bundle root — same content, different names — so users
23172359
// can pick whichever entry point they prefer.
23182360
if (auto r = write_bundle_all_wrappers(plan.stagingRoot,
2319-
plan.binaryName, loader); !r)
2361+
plan.binaryName, loader, localeData, gconvData); !r)
23202362
return std::unexpected(Error{r.error()});
23212363
collect_licenses(toBundle, plan.stagingRoot);
23222364
}

‎src/runtime/binding.cppm‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import mcpp.libs.json;
1616
import mcpp.platform;
1717
import mcpp.xlings.runtime_selection;
1818
import mcpp.xlings.subos_info;
19+
import mcpp.xlings;
1920

2021
export namespace mcpp::platform::runtime {
2122

@@ -178,6 +179,15 @@ std::string canonical_contract(const RuntimeBinding& binding) {
178179
append_field(out, binding.hostLibc);
179180
for (auto const& p : binding.libraryDirs)
180181
append_field(out, p.generic_string());
182+
// A payload reinstalled under the same version with changed content is a
183+
// different runtime (openxlings/xlings#620), so its packaging revision is
184+
// part of the contract. Revision 0 contributes nothing: a payload installed
185+
// before revisions existed keeps the contract, and the output directory,
186+
// it had.
187+
for (auto const& p : binding.libraryDirs)
188+
if (const int r = mcpp::xlings::paths::installed_revision(p.parent_path())
189+
.value_or(0); r > 0)
190+
append_field(out, std::format("revision={}", r));
181191
// The farm participates in the hash because it participates in the
182192
// artifact: it lands in DT_RPATH, so a build made against one farm is not
183193
// interchangeable with a build made against another. `declared` is in for

‎src/xlings/xlings.cppm‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ namespace paths {
210210
std::optional<std::filesystem::path>
211211
recorded_payload(const Env& env, const XpkgRef& ref);
212212

213+
// The packaging revision xlings recorded for the payload in `payloadDir`
214+
// (`.xpkg-install.json`, xlings 2026.9.27.1+; openxlings/xlings#620), and
215+
// 0 when it recorded none -- the reading xlings itself gives a record that
216+
// predates the field. `nullopt` when there is no record at all.
217+
std::optional<int> installed_revision(const std::filesystem::path& payloadDir);
218+
213219
// From compiler binary, climb parent dirs to find "xpkgs" directory.
214220
// Replaces 3 duplicate implementations in flags.cppm, ninja_backend.cppm,
215221
// stdmod.cppm.
@@ -1066,6 +1072,16 @@ void record_resolutions(const Env& env, std::span<const ResolvedTarget> targets)
10661072
}
10671073
}
10681074

1075+
std::optional<int> installed_revision(const std::filesystem::path& payloadDir) {
1076+
std::ifstream in{payloadDir / ".xpkg-install.json", std::ios::binary};
1077+
if (!in) return std::nullopt;
1078+
std::string text{std::istreambuf_iterator<char>(in), {}};
1079+
auto j = nlohmann::json::parse(text, nullptr, /*allow_exceptions=*/false);
1080+
if (!j.is_object()) return std::nullopt;
1081+
if (!j.contains("revision") || !j["revision"].is_number_integer()) return 0;
1082+
return j["revision"].get<int>();
1083+
}
1084+
10691085
std::optional<std::filesystem::path>
10701086
recorded_payload(const Env& env, const XpkgRef& ref) {
10711087
if (ref.name.empty()) return std::nullopt;

‎tests/unit/test_freestanding.cpp‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,23 @@ TEST(XpkgPayload, ABareVersionSelectsWhatXlingsSelected) {
313313
std::filesystem::remove_all(base);
314314
}
315315

316+
// The revision xlings recorded for a payload (openxlings/xlings#620): the
317+
// field when present, 0 for a record that predates it, and nothing at all when
318+
// there is no record -- which is the case mcpp judges by its marker alone.
319+
TEST(XpkgPayload, InstalledRevisionReadsTheXlingsRecord) {
320+
namespace xp = mcpp::xlings::paths;
321+
auto dir = std::filesystem::temp_directory_path()
322+
/ std::format("mcpp-xpkg-rev-{}", ::getpid());
323+
std::filesystem::remove_all(dir);
324+
std::filesystem::create_directories(dir);
325+
EXPECT_FALSE(xp::installed_revision(dir).has_value());
326+
std::ofstream(dir / ".xpkg-install.json") << R"({"os":"linux","version":"2.44.3"})";
327+
EXPECT_EQ(xp::installed_revision(dir), 0);
328+
std::ofstream(dir / ".xpkg-install.json") << R"({"version":"2.44.3","revision":1})";
329+
EXPECT_EQ(xp::installed_revision(dir), 1);
330+
std::filesystem::remove_all(dir);
331+
}
332+
316333
TEST(XpkgEnvVar, BothSpellingsAreDerivedFromOneSanitizer) {
317334
using mcpp::build::xpkg_env_var;
318335
// The two sides of the channel must agree; drifting apart would make the

‎tests/unit/test_manifest.cpp‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,37 @@ package = {
504504
EXPECT_EQ(keys[2], "25.0.4.7.1");
505505
}
506506

507+
// openxlings/xlings#620, mcpp#524 A: a version entry's `revision` is read the
508+
// way the reference implementation reads it -- a non-negative integer, and 0
509+
// for anything else or for its absence -- including the per-arch shape, whose
510+
// arch tables must not be mistaken for the revision.
511+
TEST(ListXpkgVersions, RevisionIsReadPerEntry) {
512+
constexpr auto src = R"(
513+
package = {
514+
name = "rev",
515+
xpm = {
516+
linux = {
517+
["latest"] = { ref = "1.2.0" },
518+
["1.0.0"] = { url = "u", sha256 = "a" },
519+
["1.1.0"] = { url = "u", sha256 = "b", revision = 2 },
520+
["1.2.0"] = { x86_64 = { url = "u", sha256 = "c" }, revision = 1 },
521+
["1.3.0"] = { url = "u", sha256 = "d", revision = "3" },
522+
["1.4.0"] = { url = "u", sha256 = "e", revision = -1 },
523+
},
524+
},
525+
}
526+
)";
527+
auto e = mcpp::manifest::list_xpkg_version_entries(
528+
src, mcpp::platform::TargetPlatform::for_lint_of("linux"));
529+
ASSERT_EQ(e.size(), 6u);
530+
EXPECT_EQ(e[0].revision, 0); // an alias carries none
531+
EXPECT_EQ(e[1].revision, 0);
532+
EXPECT_EQ(e[2].revision, 2);
533+
EXPECT_EQ(e[3].revision, 1);
534+
EXPECT_EQ(e[4].revision, 0); // a string is not a revision
535+
EXPECT_EQ(e[5].revision, 0); // nor is a negative number
536+
}
537+
507538
// The scanner used to walk the platform table character by character, so a
508539
// bracket key nested inside a version's own body (mirror tables write
509540
// `["GLOBAL"] = "https://..."`) counted as a published version.

0 commit comments

Comments
 (0)