Skip to content

Commit a5728dd

Browse files
committed
manifest: a key this engine does not know says which engine the package needs
Measured with mcpp-plugins 0.19.0 on the released 2026.10.1.2: the reader is told error: mcpp.toml: error: [feature-xlings.deps-archive] xim:cmake: unknown key 'provision' in a scoped entry; expected 'version' and 'when' and nothing about the version, because the floor check needs the document that this very parse failed to produce. Every release of a plugin collection raises its floor, so this is the first thing a user on an older engine meets. The floor is therefore read from the file's text in the parse-failure path -- `mcpp` inside `[package]`, nothing else -- and when this engine is below it, the refusal says so and names the upgrade, in the words the floor check already uses. `stated_mcpp_floor` is exported and seven cases pin the shapes it reads.
1 parent 7819a28 commit a5728dd

2 files changed

Lines changed: 90 additions & 2 deletions

File tree

‎modules/manifest/src/toml.cppm‎

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import mcpp.pm.index_spec;
1515
import mcpp.platform;
1616
import mcpp.platform.axis; // the one macos/macosx spelling rule
1717
import mcpp.xpkg_version; // the release grammar of `mcpp = ">=V"`
18+
import mcpp.version; // MCPP_VERSION -- a refused key may be a newer one
1819

1920
// ANONYMOUS NAMESPACE, AND THIS COST TWO WINDOWS JOBS TO LEARN.
2021
//
@@ -557,6 +558,14 @@ make_xlings_entry(std::string_view key, std::string_view value) {
557558
std::expected<Manifest, ManifestError> parse_string(std::string_view content,
558559
const std::filesystem::path& origin = "mcpp.toml",
559560
LoadContext ctx = {});
561+
// THE ENGINE FLOOR A MANIFEST STATES, read from its text rather than from the
562+
// parsed document: a manifest that fails to parse still says which engine it was
563+
// written for, and that is exactly the case where the answer matters. Only
564+
// `mcpp` inside `[package]` counts. Empty when the file states none.
565+
//
566+
// Exported for the test that pins the shapes it reads.
567+
std::string stated_mcpp_floor(std::string_view text);
568+
560569
std::expected<Manifest, ManifestError> load(const std::filesystem::path& path,
561570
LoadContext ctx = {});
562571

@@ -4600,6 +4609,45 @@ void apply_defaults_and_infer(Manifest& m, const std::filesystem::path& root) {
46004609

46014610
} // namespace
46024611

4612+
std::string stated_mcpp_floor(std::string_view text) {
4613+
bool inPackage = false;
4614+
std::size_t at = 0;
4615+
while (at <= text.size()) {
4616+
const auto nl = text.find('\n', at);
4617+
std::string_view line = text.substr(at, nl == std::string_view::npos
4618+
? std::string_view::npos : nl - at);
4619+
at = nl == std::string_view::npos ? text.size() + 1 : nl + 1;
4620+
while (!line.empty() && (line.front() == ' ' || line.front() == '\t'))
4621+
line.remove_prefix(1);
4622+
if (line.starts_with('#')) continue;
4623+
if (line.starts_with('[')) {
4624+
// `[package]` only. A table that merely begins with it, such as
4625+
// `[package.metadata]`, states no floor.
4626+
inPackage = line.starts_with("[package]");
4627+
continue;
4628+
}
4629+
if (!inPackage || !line.starts_with("mcpp")) continue;
4630+
auto rest = line.substr(4);
4631+
while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t'))
4632+
rest.remove_prefix(1);
4633+
if (!rest.starts_with('=')) continue; // `mcpp_something = ...`
4634+
rest.remove_prefix(1);
4635+
while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t'))
4636+
rest.remove_prefix(1);
4637+
if (rest.empty() || rest.front() != '"') continue;
4638+
rest.remove_prefix(1);
4639+
const auto end = rest.find('"');
4640+
if (end == std::string_view::npos) continue;
4641+
std::string_view value = rest.substr(0, end);
4642+
// The grammar of the key is `">=V"`, and a bare `"V"` is read the same
4643+
// way the parsed form reads it.
4644+
if (value.starts_with(">=")) value.remove_prefix(2);
4645+
while (!value.empty() && value.front() == ' ') value.remove_prefix(1);
4646+
return std::string(value);
4647+
}
4648+
return {};
4649+
}
4650+
46034651
std::expected<Manifest, ManifestError> load(const std::filesystem::path& path,
46044652
LoadContext ctx) {
46054653
std::ifstream is(path);
@@ -4610,8 +4658,29 @@ std::expected<Manifest, ManifestError> load(const std::filesystem::path& path,
46104658
}
46114659
std::stringstream ss;
46124660
ss << is.rdbuf();
4613-
auto m = parse_string(ss.str(), path, ctx);
4614-
if (!m) return m;
4661+
const std::string text = ss.str();
4662+
auto m = parse_string(text, path, ctx);
4663+
if (!m) {
4664+
// A KEY THIS ENGINE DOES NOT KNOW MAY BE A KEY OF A NEWER ONE, and the
4665+
// package says which engine it was written for. Without this the reader
4666+
// of a new plugin collection on an old engine is told
4667+
// `unknown key 'provision' in a scoped entry` and nothing about the
4668+
// version -- measured with mcpp-plugins 0.19.0 on mcpp 2026.10.1.2,
4669+
// where the floor check never runs because it needs the document this
4670+
// very parse failed to produce.
4671+
const auto floor = stated_mcpp_floor(text);
4672+
const auto need = floor.empty() ? std::nullopt : mcpp::xpkg_version::parse(floor);
4673+
const auto have = mcpp::xpkg_version::parse(mcpp::MCPP_VERSION);
4674+
if (need && have && mcpp::xpkg_version::compare(*have, *need) < 0)
4675+
m.error().message += std::format(
4676+
"\n This package requires mcpp >= {}, and this is mcpp {}, so the "
4677+
"key may be\n"
4678+
" one a newer engine reads.\n"
4679+
" hint: pin \"mcpp\": \"{}\" (or newer) in .xlings.json and run "
4680+
"`xlings install`, or\n run `xlings install mcpp@{}`",
4681+
floor, mcpp::MCPP_VERSION, floor, floor);
4682+
return m;
4683+
}
46154684

46164685
// M5.0: defaults + target inference (uses filesystem context relative to mcpp.toml).
46174686
apply_defaults_and_infer(*m, path.parent_path());

‎tests/unit/test_sources.cpp‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,22 @@ TEST(Sources, PathSpecTakesTheFamilyFromTheDrivers) {
330330
EXPECT_FALSE(none.has_value());
331331
std::filesystem::remove_all(base);
332332
}
333+
334+
// ── The engine floor read from a manifest that does not parse ───────────────
335+
336+
TEST(Sources, StatedMcppFloorIsReadFromPackageOnly) {
337+
using mcpp::manifest::stated_mcpp_floor;
338+
EXPECT_EQ(stated_mcpp_floor("[package]\nname = \"p\"\nmcpp = \">=2026.10.1.3\"\n"),
339+
"2026.10.1.3");
340+
// A bare version is the same statement.
341+
EXPECT_EQ(stated_mcpp_floor("[package]\nmcpp = \"2026.9.28.3\"\n"), "2026.9.28.3");
342+
// Only `[package]`: a dependency named mcpp states a dependency, not a floor.
343+
EXPECT_EQ(stated_mcpp_floor("[package]\nname = \"p\"\n\n[dependencies]\nmcpp = \"1.0\"\n"),
344+
"");
345+
// `[package.metadata]` is another table.
346+
EXPECT_EQ(stated_mcpp_floor("[package.metadata]\nmcpp = \"9.9.9.9\"\n"), "");
347+
// A key that merely starts with the name is not the key.
348+
EXPECT_EQ(stated_mcpp_floor("[package]\nmcpp_home = \"/x\"\n"), "");
349+
EXPECT_EQ(stated_mcpp_floor("[package]\n# mcpp = \"9.9.9.9\"\n"), "");
350+
EXPECT_EQ(stated_mcpp_floor(""), "");
351+
}

0 commit comments

Comments
 (0)