Skip to content

Commit acf2d5c

Browse files
committed
feat: Cargo-style features ([features] + --features + dep features=[...])
[features] declares feature -> implied-features (with a 'default' set); long-form dep specs' features=[...] is now stored (was accepted-but-ignored) and requests features of that dependency. Activation = default ∪ requested, expanded transitively; each active feature becomes -DMCPP_FEATURE_<NAME> on that package's compile flags. Root activation via --features a,b. Verified: default set, --features, and implication closure all observable via #ifdef. Transitive dep->dep feature propagation is a follow-up.
1 parent 1b4c117 commit acf2d5c

3 files changed

Lines changed: 87 additions & 0 deletions

File tree

src/cli.cppm

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,7 @@ struct BuildOverrides {
11571157
bool force_static = false; // --static (or implied by musl target)
11581158
std::string package_filter; // -p <name>: only build this workspace member
11591159
std::string profile; // --profile <name> (default "release")
1160+
std::string features; // --features a,b,c (root package activation)
11601161
};
11611162

11621163
// `prepare_build` builds the BuildContext for any verb that compiles.
@@ -2885,6 +2886,68 @@ prepare_build(bool print_fingerprint,
28852886

28862887
computeUsageRequirements();
28872888

2889+
// ─── Feature activation (Cargo-style, additive) ────────────────────
2890+
// activated(pkg) = pkg.[features].default ∪ features requested for it
2891+
// (root: --features; deps: the root dep spec's `features = [...]`).
2892+
// Implied features expand transitively. Each active feature becomes
2893+
// -DMCPP_FEATURE_<NAME> on that package's compile flags.
2894+
// (Transitive dep→dep feature requests are not yet propagated.)
2895+
{
2896+
auto sanitize = [](std::string f) {
2897+
for (auto& c : f)
2898+
c = std::isalnum(static_cast<unsigned char>(c))
2899+
? static_cast<char>(std::toupper(static_cast<unsigned char>(c))) : '_';
2900+
return f;
2901+
};
2902+
auto activate = [](const mcpp::manifest::Manifest& pm,
2903+
const std::vector<std::string>& requested) {
2904+
std::vector<std::string> act, q;
2905+
if (auto it = pm.featuresMap.find("default"); it != pm.featuresMap.end())
2906+
q.insert(q.end(), it->second.begin(), it->second.end());
2907+
q.insert(q.end(), requested.begin(), requested.end());
2908+
std::set<std::string> seen;
2909+
while (!q.empty()) {
2910+
auto f = q.back(); q.pop_back();
2911+
if (f == "default" || !seen.insert(f).second) continue;
2912+
act.push_back(f);
2913+
if (auto it = pm.featuresMap.find(f); it != pm.featuresMap.end())
2914+
q.insert(q.end(), it->second.begin(), it->second.end());
2915+
}
2916+
return act;
2917+
};
2918+
auto apply = [&](mcpp::modgraph::PackageRoot& pkg,
2919+
const std::vector<std::string>& requested) {
2920+
for (auto& f : activate(pkg.manifest, requested)) {
2921+
auto def = "-DMCPP_FEATURE_" + sanitize(f);
2922+
pkg.manifest.buildConfig.cflags.push_back(def);
2923+
pkg.manifest.buildConfig.cxxflags.push_back(def);
2924+
pkg.privateBuild.cflags.push_back(def);
2925+
pkg.privateBuild.cxxflags.push_back(def);
2926+
}
2927+
};
2928+
if (!packages.empty()) {
2929+
std::vector<std::string> rootReq;
2930+
for (std::size_t p = 0; p < overrides.features.size();) {
2931+
auto c = overrides.features.find_first_of(", ", p);
2932+
auto tok = overrides.features.substr(
2933+
p, c == std::string::npos ? std::string::npos : c - p);
2934+
if (!tok.empty()) rootReq.push_back(tok);
2935+
if (c == std::string::npos) break;
2936+
p = c + 1;
2937+
}
2938+
apply(packages[0], rootReq);
2939+
}
2940+
for (std::size_t i = 1; i < packages.size(); ++i) {
2941+
auto& pname = packages[i].manifest.package.name;
2942+
std::vector<std::string> req;
2943+
for (auto& [dname, dspec] : m->dependencies) {
2944+
if (dname == pname || dspec.shortName == pname) { req = dspec.features; break; }
2945+
}
2946+
if (!req.empty() || packages[i].manifest.featuresMap.contains("default"))
2947+
apply(packages[i], req);
2948+
}
2949+
}
2950+
28882951
// Modgraph: regex scanner by default; opt-in to compiler-driven P1689
28892952
// scanner via env var MCPP_SCANNER=p1689 (see docs/27).
28902953
auto scan = [&] {
@@ -3547,6 +3610,7 @@ int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) {
35473610
if (auto t = parsed.value("target")) ov.target_triple = *t;
35483611
if (auto p = parsed.value("package")) ov.package_filter = *p;
35493612
if (auto pr = parsed.value("profile")) ov.profile = *pr;
3613+
if (auto fs = parsed.value("features")) ov.features = *fs;
35503614
ov.force_static = parsed.is_flag_set("static");
35513615

35523616
// P0: try fast-path if inputs haven't changed.
@@ -5595,6 +5659,8 @@ int run(int argc, char** argv) {
55955659
.help("Build only the named workspace member"))
55965660
.option(cl::Option("profile").takes_value().value_name("NAME")
55975661
.help("Build profile: release (default) | dev | dist | <[profile.*] name>"))
5662+
.option(cl::Option("features").takes_value().value_name("LIST")
5663+
.help("Activate root-package features (comma-separated)"))
55985664
.action(wrap_rc(cmd_build)))
55995665
.subcommand(cl::App("run")
56005666
.description("Build + run a binary target (after `--`, args are passed to it)")

src/manifest.cppm

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ struct Manifest {
205205
BuildConfig buildConfig;
206206
RuntimeConfig runtimeConfig;
207207
std::map<std::string, Profile> profiles; // [profile.<name>]
208+
// [features] — feature name → implied features ("default" = default set).
209+
std::map<std::string, std::vector<std::string>> featuresMap;
208210

209211
// [target.<triple>] tables — empty if user didn't declare any.
210212
std::map<std::string, TargetEntry> targetOverrides;
@@ -501,6 +503,20 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
501503
}
502504
}
503505

506+
// [features] — feature name → implied features. "default" lists the
507+
// default-active set.
508+
if (auto* features_table = doc->get_table("features");
509+
features_table && !features_table->empty()) {
510+
for (auto& [fname, fval] : *features_table) {
511+
std::vector<std::string> implied;
512+
if (fval.is_array()) {
513+
for (auto& v : fval.as_array())
514+
if (v.is_string()) implied.push_back(v.as_string());
515+
}
516+
m.featuresMap[fname] = std::move(implied);
517+
}
518+
}
519+
504520
auto* targets_table = doc->get_table("targets");
505521
if (targets_table && !targets_table->empty()) {
506522
for (auto& [tname, tval] : *targets_table) {
@@ -600,6 +616,10 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
600616
section, fqName)));
601617
}
602618
}
619+
if (auto it = sub.find("features"); it != sub.end() && it->second.is_array()) {
620+
for (auto& fv : it->second.as_array())
621+
if (fv.is_string()) spec.features.push_back(fv.as_string());
622+
}
603623
if (auto it = sub.find("rev"); it != sub.end() && it->second.is_string()) {
604624
spec.gitRev = it->second.as_string();
605625
spec.gitRefKind = "rev";

src/pm/dep_spec.cppm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ struct DependencySpec {
3939
std::string gitRev; // commit / tag / branch (any one)
4040
std::string gitRefKind; // "rev" / "tag" / "branch" (for clarity)
4141
std::string visibility = "public"; // public / private / interface
42+
std::vector<std::string> features; // requested feature set (long-form dep spec)
4243
std::vector<DependencyCoordinate> candidates; // ordered lookup candidates
4344

4445
bool inheritWorkspace = false; // .workspace = true

0 commit comments

Comments
 (0)