Skip to content

Commit c10876c

Browse files
committed
fix: a member inside an index archive inherits its workspace; publish reads the one key table (#690)
inherit_as_workspace_member is the one function for a sibling path dependency, a git-hosted member and a member inside an index package's archive (Form A pointer), searched no higher than the install root. normalize.cppm writes [workspace.build] back through kWorkspaceBuildKeys instead of a second copy of the key set. e2e 774.
1 parent 81df9af commit c10876c

3 files changed

Lines changed: 173 additions & 105 deletions

File tree

‎src/build/prepare.cppm‎

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,54 @@ unfolded_defines_error(const mcpp::manifest::Manifest& m) {
734734
d.size(), d.size() == 1 ? "y" : "ies", d.front());
735735
}
736736

737+
// WHAT A MEMBER RECEIVES FROM ITS WORKSPACE WHEN IT IS REACHED AS A DEPENDENCY.
738+
//
739+
// Three parts of the inheritance matter to a dependency: `[workspace.package]`
740+
// (a member may omit `version`), `x.workspace = true` dependency entries
741+
// (without the merge the entry reaches resolution with neither version nor
742+
// path), and `[workspace.build]`. They are applied at the dependency's LOAD
743+
// site, before the conditional merge and the `defines` fold, which is the
744+
// order the root follows; `makePackageRoot` only captures the result (#690).
745+
// `[toolchain]`, `[target.<triple>]` and `[indices]` are decided by the root
746+
// for the whole graph and are not applied to a dependency.
747+
//
748+
// One function for every way a member is reached: a sibling `path`
749+
// dependency, a member of a git-hosted workspace, and a member inside an
750+
// index package's archive. The same commit then compiles the same way in its
751+
// own checkout and in every consumer's graph.
752+
std::optional<std::string>
753+
inherit_as_workspace_member(mcpp::manifest::Manifest& member,
754+
const mcpp::manifest::Manifest& workspace,
755+
const std::filesystem::path& workspaceRoot,
756+
const std::filesystem::path& memberDir) {
757+
mcpp::project::inherit_workspace_package(member, workspace);
758+
mcpp::project::merge_workspace_deps(member, workspace, workspaceRoot);
759+
mcpp::project::inherit_workspace_build(member, workspace, workspaceRoot);
760+
return mcpp::project::workspace_inheritance_error(member, memberDir);
761+
}
762+
763+
// The workspace whose `members` list `memberDir`, searched upward from its
764+
// parent and never above `bound` (an index package's install root: the
765+
// archive is the only tree the package's author wrote).
766+
std::optional<std::pair<mcpp::manifest::Manifest, std::filesystem::path>>
767+
workspace_listing(const std::filesystem::path& memberDir,
768+
const std::filesystem::path& bound) {
769+
auto inside = [&](const std::filesystem::path& p) {
770+
auto rel = p.lexically_normal().lexically_relative(bound.lexically_normal());
771+
return !rel.empty() && *rel.begin() != "..";
772+
};
773+
for (auto p = memberDir.parent_path(); inside(p); p = p.parent_path()) {
774+
if (std::filesystem::exists(p / "mcpp.toml")) {
775+
if (auto ws = mcpp::manifest::load(p / "mcpp.toml");
776+
ws && ws->workspace.present
777+
&& mcpp::project::is_workspace_member(*ws, p, memberDir))
778+
return std::pair{std::move(*ws), p};
779+
}
780+
if (p == p.parent_path()) break;
781+
}
782+
return std::nullopt;
783+
}
784+
737785
// ── The SECOND conditional pass: predicates that name a target-side layer ────
738786
//
739787
// #540/#494. `docs/14` documents a package adapting to the C library it was
@@ -2327,7 +2375,7 @@ prepare_build(bool print_fingerprint,
23272375
//
23282376
// A PRELOADED manifest (a host-tool sub-build) is already effective: the
23292377
// resolver loaded it at the dependency's load site, where a member
2330-
// inherits (see `inheritAsMember`). It is not inherited a second time; the
2378+
// inherits (see `inherit_as_workspace_member`). It is not inherited a second time; the
23312379
// workspace it belongs to is still recorded below, so that its own sibling
23322380
// dependencies inherit as members.
23332381
std::optional<mcpp::project::EffectiveManifest> effective;
@@ -6199,10 +6247,22 @@ prepare_build(bool print_fingerprint,
61996247
auto loadFrom = [&](const std::filesystem::path& mcppToml)
62006248
-> std::expected<void, std::string>
62016249
{
6202-
auto dm = mcpp::manifest::load(mcppToml);
6250+
// A manifest that is a member of a workspace inside the archive
6251+
// receives that workspace's inheritance, as it does from a git
6252+
// clone of the same commit (#690).
6253+
auto repoWorkspace = workspace_listing(mcppToml.parent_path(), verRoot);
6254+
auto dm = mcpp::manifest::load(
6255+
mcppToml, {.insideWorkspace = repoWorkspace.has_value()});
62036256
if (!dm) return std::unexpected(std::format(
62046257
"dependency '{}' (at '{}'): {}",
62056258
depName, mcppToml.string(), dm.error().format()));
6259+
if (repoWorkspace) {
6260+
if (auto bad = inherit_as_workspace_member(
6261+
*dm, repoWorkspace->first, repoWorkspace->second,
6262+
mcppToml.parent_path()))
6263+
return std::unexpected(std::format(
6264+
"dependency '{}': {}", depName, *bad));
6265+
}
62066266
manifest = std::move(*dm);
62076267
effRoot = mcppToml.parent_path();
62086268
return {};
@@ -8279,32 +8339,13 @@ prepare_build(bool print_fingerprint,
82798339
name, dep_root.string(), dm.error().format()));
82808340
}
82818341
dep_manifest = std::move(*dm);
8282-
// A MEMBER REACHED AS A DEPENDENCY INHERITS HERE, AT ITS LOAD SITE,
8283-
// EXACTLY AS THE ROOT INHERITS AT ITS OWN.
8284-
//
8285-
// Three parts of what a member receives from its workspace matter
8286-
// to a dependency: `[workspace.package]` (a member may omit
8287-
// `version`), `x.workspace = true` dependency entries (without the
8288-
// merge the entry reaches resolution with no version and no path,
8289-
// and is reported as an unreadable index entry), and
8290-
// `[workspace.build]`. All three run before the conditional merge
8291-
// and the `defines` fold below, which is the order the root
8292-
// follows; the snapshot in `makePackageRoot` only captures the
8293-
// result (#690). The remaining parts of `inherit_workspace_config`
8294-
// (`[toolchain]`, `[target.<triple>]`, `[indices]`) are decided by
8295-
// the root for the whole graph and are not applied to a dependency.
8296-
//
8297-
// A member of a git-hosted workspace inherits from ITS repository,
8298-
// anchored at the clone, so that the same commit compiles the same
8299-
// way in its own checkout and in a consumer's graph.
8342+
// A member reached as a dependency inherits here, at its load
8343+
// site; see `inherit_as_workspace_member`. A member of a
8344+
// git-hosted workspace inherits from ITS repository, anchored at
8345+
// the clone.
83008346
auto inheritAsMember = [&](const mcpp::manifest::Manifest& ws,
8301-
const std::filesystem::path& wsRoot)
8302-
-> std::optional<std::string> {
8303-
mcpp::project::inherit_workspace_package(*dep_manifest, ws);
8304-
mcpp::project::merge_workspace_deps(*dep_manifest, ws, wsRoot);
8305-
mcpp::project::inherit_workspace_build(*dep_manifest, ws, wsRoot);
8306-
return mcpp::project::workspace_inheritance_error(
8307-
*dep_manifest, dep_root);
8347+
const std::filesystem::path& wsRoot) {
8348+
return inherit_as_workspace_member(*dep_manifest, ws, wsRoot, dep_root);
83088349
};
83098350
if (depIsMember) {
83108351
if (auto bad = inheritAsMember(*wsManifest, runtimeWorkspaceRoot))

‎src/publish/normalize.cppm‎

Lines changed: 46 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -58,46 +58,12 @@ namespace {
5858

5959
namespace t = mcpp::libs::toml;
6060

61-
// `[workspace.build]` keys, each paired with the field of `BuildConfig` that
62-
// carries it. Vectors of flags, vectors of directories and scalars are written
63-
// back by three different rules, so the table records which rule applies.
64-
struct StringVectorKey {
65-
std::string_view key;
66-
std::vector<std::string> mcpp::manifest::BuildConfig::* field;
67-
};
68-
struct PathVectorKey {
69-
std::string_view key;
70-
std::vector<std::filesystem::path> mcpp::manifest::BuildConfig::* field;
71-
};
72-
struct ScalarKey {
73-
std::string_view key;
74-
std::string mcpp::manifest::BuildConfig::* field;
75-
};
76-
61+
// The `[workspace.build]` keys are read from `mcpp::manifest::kWorkspaceBuildKeys`,
62+
// the one statement of the inheritable subset that the parser also reads.
63+
// Vectors of flags, vectors of directories and scalars are written back by
64+
// three different rules, selected by the type of the row's field.
7765
using BC = mcpp::manifest::BuildConfig;
7866

79-
const StringVectorKey kStringVectors[] = {
80-
{"cflags", &BC::cflags},
81-
{"cxxflags", &BC::cxxflags},
82-
{"ldflags", &BC::ldflags},
83-
{"defines", &BC::defines},
84-
{"dialect_cxxflags", &BC::dialectCxxflags},
85-
};
86-
const PathVectorKey kPathVectors[] = {
87-
{"include_dirs", &BC::includeDirs},
88-
{"include_dirs_after", &BC::includeDirsAfter},
89-
{"private_include_dirs", &BC::privateIncludeDirs},
90-
};
91-
const ScalarKey kScalars[] = {
92-
{"c_standard", &BC::cStandard},
93-
{"linkage", &BC::linkage},
94-
{"target", &BC::target},
95-
{"cxx_runtime", &BC::cxxRuntime},
96-
{"dependency_linkage", &BC::dependencyLinkage},
97-
{"macos_deployment_target", &BC::macosDeploymentTarget},
98-
{"ios_deployment_target", &BC::iosDeploymentTarget},
99-
};
100-
10167
t::Value string_array(const std::vector<std::string>& v) {
10268
t::Array a;
10369
for (auto const& s : v) a.emplace_back(s);
@@ -380,47 +346,49 @@ normalize_for_publish(const std::filesystem::path& packageDir,
380346
if (!build) build = table_at(root, "build", "build", explicitTables);
381347
return build;
382348
};
383-
for (auto const& k : kStringVectors) {
384-
if ((w.*k.field).empty()) continue;
385-
auto* bt = build_table();
386-
if (!bt) return std::unexpected(std::format(
387-
"{}: `build` is not a table", manifestPath.string()));
388-
(*bt)[std::string(k.key)] = string_array(b.*k.field);
389-
changed = true;
390-
}
391-
for (auto const& k : kPathVectors) {
392-
const auto n = (w.*k.field).size();
393-
if (n == 0) continue;
394-
std::vector<std::string> dirs;
395-
const auto& all = b.*k.field;
396-
for (std::size_t i = 0; i < all.size(); ++i) {
397-
if (i >= n) { dirs.push_back(all[i].string()); continue; }
398-
if (!inside(packageDir, all[i]))
399-
return std::unexpected(std::format(
400-
"{}: [workspace.build] {} entry '{}' resolves to '{}', "
401-
"outside this package's directory, and the published "
402-
"archive contains only that directory. Move the headers "
403-
"into the package, or declare the directory in the "
404-
"package's own [build] for its own build only.",
405-
manifestPath.string(), k.key, (w.*k.field)[i].string(),
406-
all[i].lexically_normal().string()));
407-
dirs.push_back(all[i].lexically_normal()
408-
.lexically_relative(packageDir.lexically_normal()).generic_string());
349+
for (auto const& row : mcpp::manifest::kWorkspaceBuildKeys) {
350+
const std::string key(row.key);
351+
if (auto const* f = std::get_if<std::vector<std::string> BC::*>(&row.field)) {
352+
if ((w.**f).empty()) continue;
353+
auto* bt = build_table();
354+
if (!bt) return std::unexpected(std::format(
355+
"{}: `build` is not a table", manifestPath.string()));
356+
(*bt)[key] = string_array(b.**f);
357+
changed = true;
358+
} else if (auto const* f = std::get_if<
359+
std::vector<std::filesystem::path> BC::*>(&row.field)) {
360+
const auto n = (w.**f).size();
361+
if (n == 0) continue;
362+
std::vector<std::string> dirs;
363+
const auto& all = b.**f;
364+
for (std::size_t i = 0; i < all.size(); ++i) {
365+
if (i >= n) { dirs.push_back(all[i].string()); continue; }
366+
if (!inside(packageDir, all[i]))
367+
return std::unexpected(std::format(
368+
"{}: [workspace.build] {} entry '{}' resolves to '{}', "
369+
"outside this package's directory, and the published "
370+
"archive contains only that directory. Move the headers "
371+
"into the package, or declare the directory in the "
372+
"package's own [build] for its own build only.",
373+
manifestPath.string(), key, (w.**f)[i].string(),
374+
all[i].lexically_normal().string()));
375+
dirs.push_back(all[i].lexically_normal()
376+
.lexically_relative(packageDir.lexically_normal()).generic_string());
377+
}
378+
auto* bt = build_table();
379+
if (!bt) return std::unexpected(std::format(
380+
"{}: `build` is not a table", manifestPath.string()));
381+
(*bt)[key] = string_array(dirs);
382+
changed = true;
383+
} else if (auto const* f = std::get_if<std::string BC::*>(&row.field)) {
384+
if ((w.**f).empty()) continue;
385+
auto* bt = build_table();
386+
if (!bt) return std::unexpected(std::format(
387+
"{}: `build` is not a table", manifestPath.string()));
388+
if (bt->contains(key)) continue;
389+
(*bt)[key] = t::Value{b.**f};
390+
changed = true;
409391
}
410-
auto* bt = build_table();
411-
if (!bt) return std::unexpected(std::format(
412-
"{}: `build` is not a table", manifestPath.string()));
413-
(*bt)[std::string(k.key)] = string_array(dirs);
414-
changed = true;
415-
}
416-
for (auto const& k : kScalars) {
417-
if ((w.*k.field).empty()) continue;
418-
auto* bt = build_table();
419-
if (!bt) return std::unexpected(std::format(
420-
"{}: `build` is not a table", manifestPath.string()));
421-
if (bt->contains(k.key)) continue;
422-
(*bt)[std::string(k.key)] = t::Value{b.*k.field};
423-
changed = true;
424392
}
425393
}
426394
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env bash
2+
# requires: python3
3+
# 774 -- a member inside an index package's archive receives its workspace's
4+
# inheritance, as the same commit does from its own checkout and from a git
5+
# clone (#690, design record F11).
6+
#
7+
# The index descriptor points at a member manifest (`mcpp = "*/libs/wlib/mcpp.toml"`)
8+
# inside an archive whose root is a workspace declaring `[workspace.package]
9+
# version` and `[workspace.build] defines`. The member omits `version` and
10+
# guards the define with `#error`. Before #690 the member was loaded as a
11+
# stand-alone manifest and refused for the missing version.
12+
#
13+
# The archive is seeded into the install path, so the test needs no network:
14+
# the resolver accepts an installed tree whose layout matches the descriptor.
15+
set -e
16+
17+
T=$(mktemp -d)
18+
trap 'rm -rf "$T"' EXIT
19+
export MCPP_HOME="$T/home"
20+
source "$(dirname "$0")/_inherit_toolchain.sh"
21+
fail() { echo "FAIL: $1"; [ -n "${2:-}" ] && cat "$2"; exit 1; }
22+
23+
X="$MCPP_HOME/registry/data/xpkgs/probe774-x-wlib/1.0.0/wrepo-1.0.0"
24+
mkdir -p "$X/libs/wlib" "$T/idx/pkgs/p" "$T/app/src"
25+
printf '[workspace]\nmembers = ["libs/wlib"]\n\n[workspace.package]\nversion = "1.0.0"\n\n[workspace.build]\ndefines = ["REPO_DEF=4"]\n' > "$X/mcpp.toml"
26+
printf '[package]\nnamespace = "probe774"\nname = "wlib"\n\n[targets.wlib]\nkind = "lib"\n\n[build]\nsources = ["w.cpp"]\n' > "$X/libs/wlib/mcpp.toml"
27+
printf '#ifndef REPO_DEF\n#error "the archive workspace did not reach its member"\n#endif\nint w_v() { return REPO_DEF; }\n' > "$X/libs/wlib/w.cpp"
28+
cat > "$T/idx/pkgs/p/probe774.wlib.lua" <<'EOF'
29+
package = {
30+
spec = "1",
31+
namespace = "probe774",
32+
name = "probe774.wlib",
33+
description = "Form A member inside a workspace archive",
34+
licenses = {"MIT"},
35+
type = "package",
36+
xpm = {
37+
linux = { ["1.0.0"] = { url = "https://example.invalid/wrepo-1.0.0.tar.gz", sha256 = "0000000000000000000000000000000000000000000000000000000000000000" } },
38+
macosx = { ["1.0.0"] = { url = "https://example.invalid/wrepo-1.0.0.tar.gz", sha256 = "0000000000000000000000000000000000000000000000000000000000000000" } },
39+
windows = { ["1.0.0"] = { url = "https://example.invalid/wrepo-1.0.0.tar.gz", sha256 = "0000000000000000000000000000000000000000000000000000000000000000" } },
40+
},
41+
mcpp = "*/libs/wlib/mcpp.toml",
42+
}
43+
EOF
44+
printf '[package]\nname = "app"\nversion = "0.1.0"\n\n[indices]\nprobe774 = { path = "../idx" }\n\n[dependencies.probe774]\nwlib = "1.0.0"\n\n[targets.app]\nkind = "bin"\nmain = "src/main.cpp"\n' > "$T/app/mcpp.toml"
45+
printf 'int w_v();\nint main() { return w_v() == 4 ? 0 : 1; }\n' > "$T/app/src/main.cpp"
46+
cd "$T/app"
47+
"$MCPP" run > run.log 2>&1 || fail "the member inside the archive did not build or run" run.log
48+
cdb=compile_commands.json
49+
n=$(python3 - "$cdb" <<'EOF'
50+
import json, sys
51+
for e in json.load(open(sys.argv[1])):
52+
if e["file"].replace("\\", "/").endswith("libs/wlib/w.cpp"):
53+
print(e["arguments"].count("-DREPO_DEF=4")); sys.exit(0)
54+
print("missing")
55+
EOF
56+
)
57+
[ "$n" = 1 ] || fail "-DREPO_DEF=4 occurs $n times in the member's unit"
58+
59+
echo "PASS: 774_an_index_member_inherits_its_archive_workspace"

0 commit comments

Comments
 (0)