Skip to content

Commit 349f0a2

Browse files
committed
feat(manifest): platform-conditional [target.'cfg(...)'.dependencies]; v0.0.75
L1b: extends the [target.'cfg(...)'] namespace to conditional dependencies — .dependencies / .dev-dependencies / .build-dependencies — evaluated against the resolved target (host for native, --target for cross), so e.g. a Windows-only dependency is declared, not faked out-of-band: [target.'cfg(windows)'.dependencies.compat] openblas = "0.3.33" Mechanism (reuses the 0.0.74 cfg evaluator; the graph stays applicative): - manifest.cppm: refactor the dependency loader into a table-based load_deps_table (same selectors/namespaces/legacy-key handling) so it serves both the global [dependencies] (via doc->get_table) AND the nested conditional tables, which dotted getters can't address (quoted cfg() segment). The [target.<predicate>] loop parses .dependencies/.dev-/.build- into the deferred ConditionalConfig. - prepare.cppm: matching predicates' dep maps merge into m->{dependencies, devDependencies,buildDependencies} in the same window as the conditional flags — before dependency resolution — so they resolve like any dep. insert() preserves an existing unconditional entry (no silent override). lazy fetch + content-hash identity remain TODO (documented). Test: tests/e2e/86_target_cfg_dependencies.sh (a cfg(unix)+cfg(windows) widget always resolves on any host; a never-matching cfg(arch=no_such_arch) bogus-path 'ghost' is excluded — a clean build proves both). Regression: unit 27/0, dep e2e (09/18/24/27/...) all pass (load_deps refactor sound), self-host build at 0.0.75.
1 parent 8f77752 commit 349f0a2

6 files changed

Lines changed: 124 additions & 18 deletions

File tree

.agents/docs/2026-06-29-manifest-environment-and-platform-design.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,15 @@ missing declared outputs as failure.
230230
after `--target` resolution. Grammar: `all/any/not` over `os`/`arch`/`family`/`env`
231231
+ bare `windows`/`unix`/`linux`/`macos`; native build → host coords, `--target`
232232
target coords. Test: `tests/e2e/85_target_cfg_build_flags.sh`.
233-
- **Phase 1b — L1 conditional dependencies + `lazy` fetch.** Same `[target.'cfg(...)']`
234-
namespace, `.dependencies`/`.dev-dependencies`/`.build-dependencies`; merge into
235-
`m->dependencies` in the same window (before dep resolution at `prepare.cppm:~731`).
236-
Add `lazy = true` (fetch only when a gated path requests it) + content-hash identity.
233+
- **✅ Phase 1b — L1 conditional dependencies (mcpp 0.0.75).** Same `[target.'cfg(...)']`
234+
namespace, `.dependencies`/`.dev-dependencies`/`.build-dependencies`, parsed via a
235+
refactored `load_deps_table` (the dep loader, now table-based so it serves both the
236+
global `[dependencies]` and the nested conditional tables that dotted getters can't
237+
address) into `ConditionalConfig`, merged into `m->dependencies` in the same
238+
evaluation window — before dep resolution — so they resolve like any dep.
239+
`insert()` keeps an existing unconditional entry (no silent override). Test:
240+
`tests/e2e/86_target_cfg_dependencies.sh`. **Still TODO:** `lazy = true` (fetch only
241+
when a gated path requests it) + content-hash identity.
237242
- **Phase 2 — L-1 environment.** Surface `[environment]` → extend the project
238243
`.xlings.json` writer (`config.cppm:699-705`) to emit `deps`/`workspace`/`envs`/`subos`;
239244
fold `[toolchain]` into `workspace`; wire `[build-dependencies]`.

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.74"
3+
version = "0.0.75"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/prepare.cppm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,12 @@ prepare_build(bool print_fingerprint,
655655
cc.cxxflags.begin(), cc.cxxflags.end());
656656
m->buildConfig.ldflags.insert(m->buildConfig.ldflags.end(),
657657
cc.ldflags.begin(), cc.ldflags.end());
658+
// Conditional dependencies (Phase 1b): merge into the manifest maps
659+
// before dependency resolution so they resolve like any dep. insert()
660+
// keeps an existing unconditional entry (no silent override).
661+
m->dependencies.insert(cc.dependencies.begin(), cc.dependencies.end());
662+
m->devDependencies.insert(cc.devDependencies.begin(), cc.devDependencies.end());
663+
m->buildDependencies.insert(cc.buildDependencies.begin(), cc.buildDependencies.end());
658664
}
659665
}
660666

src/manifest.cppm

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ struct ConditionalConfig {
183183
std::vector<std::string> cflags;
184184
std::vector<std::string> cxxflags;
185185
std::vector<std::string> ldflags;
186+
// Conditional dependencies (Phase 1b): merged into the corresponding
187+
// manifest maps in prepare_build when the predicate matches the resolved
188+
// target — before dependency resolution, so they resolve like any dep.
189+
std::map<std::string, DependencySpec> dependencies;
190+
std::map<std::string, DependencySpec> devDependencies;
191+
std::map<std::string, DependencySpec> buildDependencies;
186192
};
187193

188194
// `[lib]` — library "root" interface convention.
@@ -999,12 +1005,16 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
9991005
return {};
10001006
};
10011007

1002-
auto load_deps = [&](std::string_view section, std::map<std::string, DependencySpec>& out)
1008+
// Parse a dependency table (already obtained) into `out`. Factored out of
1009+
// load_deps so the same logic serves both [dependencies] (via doc->get_table)
1010+
// and [target.'cfg(...)'.dependencies] (a nested table the dotted getter
1011+
// can't address). `section` is the logical section name, used for error
1012+
// messages and namespace/selector resolution.
1013+
auto load_deps_table = [&](std::string_view section, auto& tt,
1014+
std::map<std::string, DependencySpec>& out)
10031015
-> std::expected<void, ManifestError>
10041016
{
1005-
auto* tt = doc->get_table(section);
1006-
if (!tt) return {};
1007-
for (auto& [k, v] : *tt) {
1017+
for (auto& [k, v] : tt) {
10081018
// (1) string value → flat default-ns short version, or
10091019
// (3) legacy "ns.name" = "ver" (dotted key).
10101020
if (v.is_string()) {
@@ -1067,6 +1077,13 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
10671077
}
10681078
return {};
10691079
};
1080+
auto load_deps = [&](std::string_view section, std::map<std::string, DependencySpec>& out)
1081+
-> std::expected<void, ManifestError>
1082+
{
1083+
auto* tt = doc->get_table(section);
1084+
if (!tt) return {};
1085+
return load_deps_table(section, *tt, out);
1086+
};
10701087
if (auto r = load_deps("dependencies", m.dependencies); !r) return std::unexpected(r.error());
10711088
if (auto r = load_deps("dev-dependencies", m.devDependencies); !r) return std::unexpected(r.error());
10721089
if (auto r = load_deps("build-dependencies", m.buildDependencies); !r) return std::unexpected(r.error());
@@ -1183,14 +1200,14 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
11831200
}
11841201
m.targetOverrides[canon_triple(triple)] = std::move(e);
11851202

1186-
// [target.<predicate>.build] — platform-conditional flags (L1).
1187-
// `triple` here is the predicate key (cfg(...) or a bare triple);
1188-
// stored deferred, evaluated against the resolved target in
1189-
// prepare_build. Reuses the [profile] array-reading idiom.
1203+
// [target.<predicate>.{build,dependencies,...}] — platform-conditional
1204+
// config (L1). `triple` is the predicate key (cfg(...) or a bare
1205+
// triple); stored deferred, evaluated against the resolved target in
1206+
// prepare_build.
1207+
ConditionalConfig cc;
1208+
cc.predicate = triple;
11901209
if (auto bit = body.find("build"); bit != body.end() && bit->second.is_table()) {
11911210
auto& bt = bit->second.as_table();
1192-
ConditionalConfig cc;
1193-
cc.predicate = triple;
11941211
auto read_list = [&](const char* key, std::vector<std::string>& out) {
11951212
if (auto f = bt.find(key); f != bt.end() && f->second.is_array())
11961213
for (auto& v : f->second.as_array())
@@ -1199,9 +1216,24 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
11991216
read_list("cflags", cc.cflags);
12001217
read_list("cxxflags", cc.cxxflags);
12011218
read_list("ldflags", cc.ldflags);
1202-
if (!cc.cflags.empty() || !cc.cxxflags.empty() || !cc.ldflags.empty())
1203-
m.conditionalConfigs.push_back(std::move(cc));
12041219
}
1220+
// [target.<predicate>.{dependencies,dev-dependencies,build-dependencies}]
1221+
// parsed via the shared table-based loader (same selectors/namespaces
1222+
// as the global [dependencies]) into the deferred config.
1223+
auto read_deps = [&](const char* key, std::map<std::string, DependencySpec>& out)
1224+
-> std::expected<void, ManifestError>
1225+
{
1226+
if (auto f = body.find(key); f != body.end() && f->second.is_table())
1227+
return load_deps_table(key, f->second.as_table(), out);
1228+
return {};
1229+
};
1230+
if (auto r = read_deps("dependencies", cc.dependencies); !r) return std::unexpected(r.error());
1231+
if (auto r = read_deps("dev-dependencies", cc.devDependencies); !r) return std::unexpected(r.error());
1232+
if (auto r = read_deps("build-dependencies", cc.buildDependencies); !r) return std::unexpected(r.error());
1233+
if (!cc.cflags.empty() || !cc.cxxflags.empty() || !cc.ldflags.empty()
1234+
|| !cc.dependencies.empty() || !cc.devDependencies.empty()
1235+
|| !cc.buildDependencies.empty())
1236+
m.conditionalConfigs.push_back(std::move(cc));
12051237
}
12061238
}
12071239

src/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "0.0.74";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.75";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env bash
2+
# 86_target_cfg_dependencies.sh — L1b platform-conditional DEPENDENCIES: a normal
3+
# mcpp.toml can scope a dependency to a target predicate via
4+
# `[target.'cfg(...)'.dependencies]`, evaluated against the resolved target (here
5+
# the host). A matching predicate's dep resolves like any dep; a non-matching
6+
# predicate's dep is never fetched/resolved. Host-aware so it runs on all three
7+
# CI platforms. See .agents/docs/2026-06-29-manifest-environment-and-platform-design.md (L1b).
8+
set -e
9+
10+
TMP=$(mktemp -d)
11+
trap "rm -rf $TMP" EXIT
12+
cd "$TMP"
13+
14+
# A real path dependency exposing one symbol.
15+
mkdir -p widget/src
16+
cat > widget/mcpp.toml <<'EOF'
17+
[package]
18+
name = "widget"
19+
version = "0.1.0"
20+
[targets.widget]
21+
kind = "lib"
22+
EOF
23+
cat > widget/src/widget.cppm <<'EOF'
24+
export module widget;
25+
export int widget_anchor() { return 0; }
26+
EOF
27+
28+
mkdir -p app/src
29+
cat > app/mcpp.toml <<'EOF'
30+
[package]
31+
name = "app"
32+
version = "0.1.0"
33+
34+
# widget is pulled under whichever family matches the host — so exactly one of
35+
# these applies and widget is ALWAYS resolved (on any of the 3 platforms).
36+
[target.'cfg(unix)'.dependencies]
37+
widget = { path = "../widget" }
38+
[target.'cfg(windows)'.dependencies]
39+
widget = { path = "../widget" }
40+
41+
# A never-matching predicate (no such arch) points at a non-existent path. If the
42+
# conditional merge wrongly pulled it, resolution of the bogus path would FAIL the
43+
# build — so a clean build proves non-matching deps are excluded.
44+
[target.'cfg(arch = "no_such_arch")'.dependencies]
45+
ghost = { path = "../this_path_does_not_exist" }
46+
EOF
47+
cat > app/src/main.cpp <<'EOF'
48+
import widget; // fails to build unless the cfg-matched widget dep was resolved
49+
int main() { return widget_anchor(); }
50+
EOF
51+
52+
cd app
53+
"$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL: conditional dep not resolved, or non-matching dep wrongly pulled"; exit 1; }
54+
55+
# Sanity: the matched dependency really was wired (widget appears in the lockfile).
56+
if [ -f mcpp.lock ]; then
57+
grep -q 'widget' mcpp.lock || { echo "FAIL: widget missing from lockfile"; cat mcpp.lock; exit 1; }
58+
fi
59+
# The never-matching ghost must NOT be present.
60+
if [ -f mcpp.lock ] && grep -q 'ghost' mcpp.lock; then
61+
echo "FAIL: non-matching cfg dependency 'ghost' leaked into the resolve"; exit 1; fi
62+
63+
echo "OK"

0 commit comments

Comments
 (0)