// mcpp.cli top-level command dispatch (and nothing else).
//
// The cli layer only parses arguments and routes:
// mcpp.cli.cmd_build / cmd_new / cmd_registry / cmd_cache /
// mcpp.cli.cmd_toolchain / cmd_publish / cmd_self (parse + route)
// mcpp.pm.commands (add / remove / update)
// Domain logic lives in its owning subsystem: mcpp.build.{prepare,execute},
// mcpp.pm.index_management, mcpp.toolchain.lifecycle, mcpp.scaffold.create,
// mcpp.publish.pipeline, mcpp.pack.pipeline, mcpp.bmi_cache.maintenance, mcpp.doctor,
// mcpp.project, mcpp.fetcher.progress.
// See .agents/docs/2026-06-10-cli-modularization.md for the architecture.
module;
#include
#include
export module mcpp.cli;
import std;
import mcpplibs.cmdline;
import mcpp.cli.cmd_build;
import mcpp.cli.cmd_cache;
import mcpp.cli.cmd_new;
import mcpp.cli.cmd_publish;
import mcpp.cli.cmd_xpkg;
import mcpp.cli.cmd_registry;
import mcpp.cli.cmd_self;
import mcpp.cli.cmd_toolchain;
import mcpp.pm.commands;
import mcpp.toolchain.fingerprint; // MCPP_VERSION
import mcpp.wire;
import mcpp.platform.env; // --offline MCPP_OFFLINE
import mcpp.platform.runtime_search; // linker-wrapper path-injection opt-out
import mcpp.ui;
import mcpp.log;
export namespace mcpp::cli {
int run(int argc, char** argv);
} // namespace mcpp::cli
namespace mcpp::cli {
// Custom top-level help. cmdline's auto-generated `print_help` is a fine
// default but its layout (`USAGE:`, no command-specific blurbs) doesn't
// match what the e2e tests assert against they check for `Usage:`
// (mixed case) plus `mcpp new` / `mcpp build` literals. We keep the
// canonical printer here so the docs/CHANGELOG examples don't drift
// every time cmdline tweaks its formatting.
void print_usage() {
std::println("mcpp v{} - modern C++23 build tool", mcpp::toolchain::MCPP_VERSION);
std::println("");
std::println("Usage:");
std::println("Project commands:");
std::println(" mcpp new Create a new package skeleton");
std::println(" mcpp build [options] Build the current package");
std::println(" mcpp run [target] [-- args...] Build + run a binary target");
std::println(" mcpp test [pattern] [-- args...] Build + run tests/**/*.cpp (--list, --timeout, --build-timeout, --message-format json)");
std::println(" mcpp clean [--bmi-cache] Remove target/ (and optionally the build cache)");
std::println(" mcpp add [ns.]pkg@ver Add an exact dependency to mcpp.toml");
std::println(" mcpp remove [ns.]pkg Remove an exact dependency from mcpp.toml");
std::println(" mcpp update [pkg] Re-resolve deps and rewrite mcpp.lock");
std::println(" mcpp search Search packages in registries");
std::println(" mcpp publish [--dry-run] Publish package to default registry");
std::println(" mcpp pack [target] Build + package (program: bundle; library: interface + binaries)");
std::println(" mcpp emit xpkg [-V VER] [-o FILE] Generate xpkg Lua entry");
std::println(" mcpp xpkg parse [--json] Validate an xpkg descriptor (resolver grammar)");
std::println("");
std::println("Resource management:");
std::println(" mcpp toolchain install|list|default Manage mcpp's private toolchains");
std::println(" mcpp cache dir|list|info|gc|... Inspect/manage the global build cache");
std::println(" mcpp index list|add|remove|update Manage package registries");
std::println("");
std::println("About mcpp itself:");
std::println(" mcpp self doctor Diagnose mcpp environment health");
std::println(" mcpp self env Print mcpp paths and toolchain");
std::println(" mcpp self config [--mirror CN|GLOBAL] Show or modify mcpp's xlings config");
std::println(" mcpp self version Show mcpp version");
std::println(" mcpp self explain Show extended description for an error code");
std::println(" mcpp --help / --version Help / version");
std::println("");
std::println("Build options:");
std::println(" --verbose, -v Verbose compiler output");
std::println(" --quiet, -q Suppress status output");
std::println(" --print-fingerprint Show toolchain fingerprint and 11 inputs");
std::println(" --configure-only Generate CDB without compiling or linking");
std::println(" --cache Dependency cache: global (default) | local | off");
std::println(" --no-cache Deprecated alias for --cache=off (clears the build dir)");
std::println(" --no-color Disable colored output");
std::println(" --offline Never touch the network (also: MCPP_OFFLINE=1)");
std::println(" --jobs N|auto, -j Concurrent compiles ('auto' = cores + free RAM)");
std::println(" --toolchain SPEC Use this toolchain for one build (e.g. llvm@22.1.8)");
std::println("");
std::println("Docs: https://github.com/mcpp-community/mcpp/tree/main/docs");
}
int run(int argc, char** argv) {
namespace cl = mcpplibs::cmdline;
// --quiet / --no-color: pre-scan
// The cmdline lib propagates global options into nested subcommand
// ParsedArgs, but we set ui:: state up-front so that *every* line
// emitted from the action lambdas (including the very first
// "Resolving toolchain" banner) honours the user's intent. This is
// a side-channel only the global options are still declared on
// the App below so they show up in --help and pass schema checks.
for (int i = 1; i < argc; ++i) {
std::string_view a = argv[i];
// Everything after a bare `--` belongs to the program being run or the
// test binary being invoked, not to mcpp. Without this, `mcpp run -- -j 4`
// reads the child's flag as mcpp's own concurrency setting `-j` is a
// common enough flag that this is a matter of when, not whether.
if (a == "--") break;
if (a == "--quiet" || a == "-q") mcpp::ui::set_quiet(true);
else if (a == "--no-color") mcpp::ui::disable_color();
else if (a == "--verbose" || a == "-v") mcpp::log::set_verbose(true);
// --offline is published as the env var rather than plumbed through
// BuildOverrides: its consumers are index refresh, package install and
// toolchain auto-install, which sit in three subsystems and would each
// need a parameter threaded down. Same shape as MCPP_VERBOSE above, and
// it makes `MCPP_OFFLINE=1` and `--offline` literally the same switch.
else if (a == "--offline") mcpp::platform::env::set("MCPP_OFFLINE", "1");
// --jobs rides the same side channel as --offline, for the same reason
// recorded there: its consumer is deep in mcpp.build.execute and
// threading a parameter down would touch every caller in between.
// Accepts `--jobs N`, `--jobs=N`, `-j N` and `-jN`.
else if (a == "--jobs" || a == "-j") {
if (i + 1 < argc) mcpp::platform::env::set("MCPP_JOBS", argv[++i]);
}
else if (a.starts_with("--jobs=")) mcpp::platform::env::set("MCPP_JOBS", std::string(a.substr(7)));
// --toolchain rides the same channel, for the same reason: its consumer
// is deep inside prepare's resolution and threading a parameter down
// would touch every caller in between.
else if (a == "--toolchain") {
if (i + 1 < argc) mcpp::platform::env::set("MCPP_TOOLCHAIN", argv[++i]);
}
else if (a.starts_with("--toolchain=")) mcpp::platform::env::set("MCPP_TOOLCHAIN", std::string(a.substr(12)));
else if (a.starts_with("-j") && a.size() > 2)
mcpp::platform::env::set("MCPP_JOBS", std::string(a.substr(2)));
}
// Decline xlings' linker-wrapper path injection, for this process and
// everything it spawns (openxlings/xlings#540).
//
// That wrapper appends `-rpath "$XLINGS_SUBOS_LIB"` to every link it sees.
// mcpp wants the TAG half of what it does and must refuse the PATH half:
// `$XLINGS_SUBOS_LIB` names the ACTIVE SHELL's SubOS, which is measurably
// not the one mcpp resolved mcpp keeps its own xlings home under
// `/registry`, so on an ordinary developer machine the variable
// points at a different farm backed by a DIFFERENT PHYSICAL glibc payload.
// Inheriting it would put a second libc on the artifact's search path,
// which is the one thing rule B exists to prevent. mcpp emits its own farm
// entry, derived from the binding it actually selected.
//
// Set here rather than per link command: the link line has a hard 128KiB
// ceiling that real workspaces already spend 43% of, and children inherit
// the environment for free. Declared BEFORE the wrapper ships, because
// "the exit must be declared, not inferred" is the rule that whole
// negotiation established today this is a no-op.
mcpp::platform::env::set(
std::string(mcpp::platform::search::kLinkerPathInjectionOptOut),
std::string(mcpp::platform::search::kLinkerPathInjectionOptOutValue));
// Env override (observability, esp. CI): MCPP_VERBOSE=
// turns on verbose logging for EVERY mcpp invocation including the ones
// nested inside e2e test scripts that call $MCPP without flags. Lets a
// workflow flip on diagnostics globally with one env var. An explicit
// --quiet still wins (it is processed above and gates the verbose sinks).
if (const char* v = std::getenv("MCPP_VERBOSE");
v && *v && std::string_view(v) != "0")
mcpp::log::set_verbose(true);
// top-level --help / -h / --version intercept
// cmdline auto-handles these but its formatter doesn't match the
// mixed-case "Usage:" + per-command blurbs that documentation +
// e2e tests pin. Print our canonical screen and `mcpp X.Y.Z`
// version line up-front so the App never sees these tokens.
if (argc >= 2) {
std::string_view a = argv[1];
if (a == "--help" || a == "-h") { print_usage(); return 0; }
if (a == "--version" || a == "-V") {
std::println("mcpp {}", mcpp::toolchain::MCPP_VERSION);
return 0;
}
}
// action_rc plumbing
// cmdline's action callback returns void. Capture int return codes
// via a shared local; `wrap_rc` adapts an `int(ParsedArgs&)` lambda
// into the void-returning shape cmdline expects.
int action_rc = 0;
auto wrap_rc = [&action_rc](auto&& fn) {
return [fn = std::forward(fn), &action_rc]
(const cl::ParsedArgs& args) {
action_rc = fn(args);
};
};
// nested-subcommand dispatcher
// cmdline's run() only dispatches one level when a parent subcommand
// (e.g. `self`, `cache`, `index`, `emit`) has its own children but no
// parent action, the matched leaf never gets invoked. We give every
// such parent an action that switches on the parsed child name and
// forwards to the right cmd_* directly.
using cmd_fn = int(*)(const cl::ParsedArgs&);
auto dispatch_sub = [](std::string_view parent,
const cl::ParsedArgs& parsed,
std::initializer_list table) -> int {
if (!parsed.has_subcommand()) {
std::string usage = std::format("`mcpp {}` requires a subcommand: ", parent);
bool first = true;
for (auto& [n, _] : table) {
if (!first) usage += " / ";
usage += n;
first = false;
}
mcpp::ui::error(usage);
return 2;
}
auto name = parsed.subcommand_name();
auto sub_ref = parsed.subcommand();
if (!sub_ref) return 2;
for (auto& [n, fn] : table) {
if (n == name) return fn(sub_ref->get());
}
mcpp::ui::error(std::format("unknown `mcpp {} {}` subcommand", parent, name));
return 2;
};
// `--` passthrough for `mcpp run` / `mcpp test`
// cmdline natively recognises `--` and dumps everything after it
// into `parsed.positionals` (with the bare `--` token dropped). For
// `mcpp run [target] -- args...` we need to distinguish the
// optional `target` (a real positional) from the passthrough
// tokens (which must reach the executed binary verbatim, even when
// they look like `-x` / `--foo`). We split ourselves at the first
// `--` and present cmdline only the pre-`--` slice; post-args go
// into `passthrough` and are handed to the action helper directly.
std::vector passthrough;
std::vector trimmed_argv(argv, argv + argc);
{
for (std::size_t i = 1; i < trimmed_argv.size(); ++i) {
if (std::string_view(trimmed_argv[i]) != "--") continue;
for (std::size_t j = i + 1; j < trimmed_argv.size(); ++j)
passthrough.emplace_back(trimmed_argv[j]);
trimmed_argv.resize(i); // drop `--` and everything after
break;
}
}
int trimmed_argc = static_cast(trimmed_argv.size());
char** trimmed_argp = trimmed_argv.empty() ? nullptr : trimmed_argv.data();
// Build the top-level App
auto app = cl::App("mcpp")
.version(std::string{mcpp::toolchain::MCPP_VERSION})
.description("modern C++ build tool")
.option(cl::Option("quiet").short_name('q')
.help("Suppress status output").global())
.option(cl::Option("verbose").short_name('v')
.help("Show detailed progress on stderr").global())
.option(cl::Option("no-color")
.help("Disable colored output").global())
.option(cl::Option("offline")
.help("Never touch the network (index refresh, downloads, toolchain install)")
.global())
// Answers "what do you speak" without spawning a command that might
// fail. An optimisation, NOT the client's detection rule: on any mcpp
// predating it this is itself an unknown option, so a client must
// still detect the protocol by parsing stdout for schemaVersion+kind.
.option(cl::Option("protocol-version")
.help("Print the machine-output protocol this build speaks (JSON)"))
// project commands
.subcommand(cl::App("new")
.description("Create a new mcpp package skeleton")
// not .required(): `--list-templates` runs without a name
// (cmd_new validates presence for project creation itself).
.arg(cl::Arg("name").help("Package directory name"))
.option(cl::Option("template").short_name('t').takes_value().value_name("SPEC")
.help("bin (default) | [ns.]pkg[@ver][:template] exact package template"))
.option(cl::Option("list-templates").takes_value().value_name("PKG")
.help("List templates from exact [ns.]pkg[@ver]"))
.action(wrap_rc(cmd_new)))
.subcommand(cl::App("build")
.description("Build the current package")
.option(cl::Option("configure-only")
.help("Generate compile_commands.json without compiling or linking"))
.option(cl::Option("print-fingerprint")
.help("Show toolchain fingerprint and 11 inputs"))
.option(cl::Option("cache").takes_value().value_name("MODE")
.help("Global dependency cache: global (default) | local | off"))
.option(cl::Option("jobs").short_name('j').takes_value().value_name("N")
.help("Concurrent compiles: a number, or 'auto' to size from cores + free RAM"))
.option(cl::Option("toolchain").takes_value().value_name("SPEC")
.help("Build with this toolchain for one build, e.g. llvm@22.1.8"))
.option(cl::Option("no-cache")
.help("Deprecated alias for --cache=off (also clears the build dir)"))
.option(cl::Option("target").takes_value().help(
"Build for (e.g. x86_64-linux-musl); looks up [target.] in mcpp.toml"))
.option(cl::Option("static").help(
"Force static linking (-static). On Linux, prefer pairing with --target -linux-musl"))
.option(cl::Option("package").short_name('p').takes_value().value_name("NAME")
.help("Build only the named workspace member"))
.option(cl::Option("profile").takes_value().value_name("NAME")
.help("Build profile: release (default) | dev | dist | "))
.option(cl::Option("release").help("Shorthand for --profile release"))
.option(cl::Option("dev").help("Shorthand for --profile dev (-O0 -g)"))
.option(cl::Option("features").takes_value().value_name("LIST")
.help("Activate root-package features (comma-separated)"))
.option(cl::Option("cap").takes_value().value_name("LIST")
.help("Pin capability providers (e.g. blas=openblas,lapack=mkl)"))
.option(cl::Option("strict")
.help("Treat manifest schema warnings (unknown feature/platform) as errors"))
.option(cl::Option("workspace")
.help("Build all workspace members"))
.action(wrap_rc(cmd_build)))
.subcommand(cl::App("run")
.description("Build + run a binary target (after `--`, args are passed to it)")
// Named `bin`, NOT `target`, and the rename is load-bearing.
//
// This positional is a BINARY NAME from [[bin]]/src layout. It was
// called `target` the same word as the cross-target axis and
// ParsedArgs::value() falls back from an unset option to a
// positional OF THE SAME NAME, so adding `--target` here made
// every ordinary invocation read the binary name as a triple:
//
// $ mcpp run q
// error: unknown target 'q'
//
// The name is never read back (cmd_run takes positional(0) by
// index); it only labels this slot and shows up in --help, where
// `bin` is the more accurate word anyway. So renaming it is what
// lets `run` spell the flag `--target` like every other
// subcommand, instead of being the one command that cannot.
.arg(cl::Arg("bin").help("Binary name (optional)"))
.option(cl::Option("target").takes_value().value_name("TRIPLE")
.help("Cross target triple (same axis as `mcpp build --target`)"))
// Kept as an alias: it shipped in 2026.8.19.1 as the only spelling
// `run` accepted, and scripts written against it must keep working.
.option(cl::Option("target-triple").takes_value().value_name("TRIPLE")
.help("Alias for --target"))
.option(cl::Option("package").short_name('p').takes_value().value_name("NAME")
.help("Run only the named workspace member (single-member; no --workspace fan-out)"))
.option(cl::Option("cache").takes_value().value_name("MODE")
.help("Global dependency cache: global (default) | local | off"))
.option(cl::Option("no-cache")
.help("Deprecated alias for --cache=off (also clears the build dir)"))
.action(wrap_rc([&passthrough](const cl::ParsedArgs& p) {
return cmd_run(p, std::span(passthrough));
})))
.subcommand(cl::App("test")
.description("Build + run all tests/**/*.cpp (after `--`, args go to each test binary)")
.arg(cl::Arg("pattern")
.help("Run only tests whose name contains PATTERN (optional)"))
.option(cl::Option("target").takes_value().value_name("TRIPLE")
.help("Cross target triple (same axis as `mcpp build --target`)"))
.option(cl::Option("message-format").takes_value().value_name("FMT")
.help("Output format: human (default) | json (NDJSON, one record per test)"))
.option(cl::Option("list")
.help("List (filtered) tests without building or running them"))
.option(cl::Option("timeout").takes_value().value_name("SECS")
.help("Kill a test still RUNNING after SECS seconds (default 300; 0 = no limit)"))
.option(cl::Option("build-timeout").takes_value().value_name("SECS")
.help("Kill a compile/link drive still running after SECS seconds (default 0 = no limit; POSIX only)"))
.option(cl::Option("workspace-timeout").takes_value().value_name("SECS")
.help("Stop the --workspace fan-out after SECS seconds and report what did run (default 0 = no limit)"))
.option(cl::Option("profile").takes_value().value_name("NAME")
.help("Build profile for the test build: release (default) | dev | dist | "))
.option(cl::Option("features").takes_value().value_name("LIST")
.help("Activate root-package features for the test build (comma-separated)"))
.option(cl::Option("cap").takes_value().value_name("LIST")
.help("Pin capability providers (e.g. blas=openblas,lapack=mkl)"))
.option(cl::Option("strict")
.help("Treat manifest schema warnings (unknown feature/platform) as errors"))
.option(cl::Option("package").short_name('p').takes_value().value_name("NAME")
.help("Run tests only for the named workspace member"))
.option(cl::Option("cache").takes_value().value_name("MODE")
.help("Global dependency cache: global (default) | local | off"))
.option(cl::Option("no-cache")
.help("Deprecated alias for --cache=off (also clears the build dir)"))
.option(cl::Option("workspace")
.help("Run tests for all workspace members"))
.action(wrap_rc([&passthrough](const cl::ParsedArgs& p) {
return cmd_test(p, std::span(passthrough));
})))
.subcommand(cl::App("clean")
.description("Remove target/ (and optionally the global build cache)")
.option(cl::Option("bmi-cache").help("Also wipe the global build cache (see `mcpp cache clean`)"))
.action(wrap_rc(cmd_clean)))
.subcommand(cl::App("why")
.description("Explain how the toolchain / runtime / deps were resolved")
.arg(cl::Arg("topic").help("toolchain | runtime | deps (default: all)"))
.action(wrap_rc(cmd_why)))
.subcommand(cl::App("resolve")
.description("Re-resolve the build plan and explain it")
.option(cl::Option("explain").help("Print resolved toolchain / runtime / deps"))
.action(wrap_rc(cmd_why)))
.subcommand(cl::App("add")
.description("Add a dependency to mcpp.toml")
.arg(cl::Arg("pkg").help(
"Exact package spec, e.g. foo@1.0.0 or compat.gtest@1.15.2")
.required())
.option(cl::Option("dev").help(
"Add to [dev-dependencies] (test-only, e.g. compat.gtest)"))
.action(wrap_rc(mcpp::pm::commands::cmd_add)))
.subcommand(cl::App("remove")
.description("Remove a dependency from mcpp.toml")
.arg(cl::Arg("pkg").help("Exact package selector [ns.]name").required())
.action(wrap_rc(mcpp::pm::commands::cmd_remove)))
.subcommand(cl::App("update")
.description("Re-resolve dependencies and rewrite mcpp.lock")
.arg(cl::Arg("pkg").help("If given, update only that package"))
.action(wrap_rc(mcpp::pm::commands::cmd_update)))
.subcommand(cl::App("search")
.description("Search packages in configured registries")
.arg(cl::Arg("keyword").help("Search keyword (substring match)").required())
.action(wrap_rc(cmd_search)))
.subcommand(cl::App("publish")
.description("Publish package to default registry")
.option(cl::Option("dry-run").help("Print xpkg.lua without uploading"))
.option(cl::Option("allow-dirty").help("Allow uncommitted changes"))
.action(wrap_rc(cmd_publish)))
.subcommand(cl::App("pack")
// "archive", not "tarball": a Windows target produces a .zip, and
// the help said tarball while the code had already stopped
// agreeing. `--format tar` likewise selects "an archive rather
// than a plain directory" WHICH archive follows the artifact,
// because a .tar.gz full of DLLs is a package most Windows users
// cannot open without installing something first.
// Says both shapes, because `[targets.].kind` picks between them
// and the one-line help is where a reader finds that out. "Bundle
// into a self-contained archive" described only the program case,
// which is now half of what this command does.
.description("Build + package: a program becomes a self-contained "
"bundle, a library an interface + prebuilt binaries")
// NB: a target NAME from [targets.*], not a triple the same
// split `mcpp run [target]` has. Its `kind` decides what is
// packed, so there is no --lib and no --artifact: a program
// becomes an application bundle, a library becomes a library
// package. Omit it and mcpp picks the only packable target.
.arg(cl::Arg("target").help("Target name from [targets.*] (optional)"))
.option(cl::Option("mode").takes_value()
.help("system | vendored (default) | self-contained | static"))
.option(cl::Option("target").takes_value().multiple()
.help("Triple, e.g. x86_64-linux-musl (repeatable: one leg per triple)"))
.option(cl::Option("format").takes_value()
.help("tar (default; .zip for a Windows target) | dir"))
.option(cl::Option("output").short_name('o').takes_value()
.help("Override output path"))
// Packaging builds RELEASE by default the artifact leaves this
// machine. `[build] default-profile` still wins when it is set;
// this only replaces the "dev" fallback every other command uses.
.option(cl::Option("profile").takes_value()
.help("Build profile (default: [build] default-profile, else release)"))
.option(cl::Option("no-strip")
.help("Ship the artifacts as built (default: strip debug info)"))
.option(cl::Option("debug-symbols").takes_value().value_name("DIR")
.help("Write the separated *.debug files here (default: discard)"))
.action(wrap_rc(cmd_pack)))
// emit (one nested subcommand: xpkg)
.subcommand(cl::App("emit")
.description("Generate package descriptor (xpkg)")
.subcommand(cl::App("xpkg")
.description("Generate xpkg Lua entry")
.option(cl::Option("version").short_name('V').takes_value().value_name("VER")
.help("Override package version"))
.option(cl::Option("output").short_name('o').takes_value().value_name("FILE")
.help("Write to file instead of stdout"))
.option(cl::Option("namespace").takes_value().value_name("NS")
.help("Package namespace for the emitted descriptor "
"(overrides [package] namespace). Emits both "
"`namespace` and the fully-qualified `name`")))
.action(wrap_rc([&dispatch_sub](const cl::ParsedArgs& p) {
return dispatch_sub("emit", p, {{"xpkg", cmd_emit_xpkg}});
})))
// xpkg (descriptor tooling: parse)
.subcommand(cl::App("xpkg")
.description("Inspect / validate xpkg descriptors")
.subcommand(cl::App("parse")
.description("Parse a descriptor's mcpp segment exactly as the resolver would (strict: unknown keys are errors)")
.option(cl::Option("json")
.help("Emit machine-readable JSON (legacy payload, kept for ever)"))
.option(cl::Option("format").takes_value().value_name("json")
.help("Machine-readable output (enveloped; see docs/11-machine-output.md)"))
.option(cl::Option("allow-unknown")
.help("Downgrade unknown mcpp-segment keys from error to warning"))
.option(cl::Option("all-os")
.help("Validate every per-OS section (linux/macosx/windows), "
"not just the running host's"))
.option(cl::Option("allow-split-name")
.help("OBSOLETE (kept accepted so 0.0.105-era index CI keeps "
"working): skip the package.name form check. Since "
"0.0.106 the canonical form IS the short name, so "
"xlings-native descriptors pass without this flag")))
.action(wrap_rc([&dispatch_sub](const cl::ParsedArgs& p) {
return dispatch_sub("xpkg", p, {{"parse", cmd_xpkg_parse}});
})))
// resource management
.subcommand(cl::App("toolchain")
.description("Install / list / select / remove C++ toolchains")
.subcommand(cl::App("list").description("List installed toolchains"))
.subcommand(cl::App("install")
.description("Install a toolchain via mcpp's xlings")
// Both `mcpp toolchain install gcc 16.1.0` and `mcpp toolchain
// install gcc@16.1.0` are accepted, and the version may be
// partial (`15`, `15.1`) mcpp resolves to the highest match.
// With --target the family may be omitted entirely (taken from
// the target's convention pin):
// mcpp toolchain install --target x86_64-windows-gnu
.arg(cl::Arg("compiler").help("gcc | llvm | msvc (or gcc@16.1.0; legacy aliases accepted)"))
.arg(cl::Arg("version").help("e.g. 16.1.0, 15, 15.1"))
.option(cl::Option("target").takes_value().help(
"Install the toolchain payload for (e.g. x86_64-windows-gnu)")))
.subcommand(cl::App("default")
.description("Set the default toolchain (and optionally the default target)")
// Same dual-form as `install`: `gcc@16.1.0` or `gcc 16.1.0`,
// partial versions allowed.
.arg(cl::Arg("spec").help("[@] (version may be partial)").required())
.arg(cl::Arg("version").help("(optional, alternative to @-form)"))
.option(cl::Option("target").takes_value().help(
"Default build target (omit = host)")))
.subcommand(cl::App("remove")
.description("Uninstall a toolchain")
.arg(cl::Arg("spec").help("@").required())
.option(cl::Option("target").takes_value().help(
"Remove the payload for instead of the host one")))
.action(wrap_rc(cmd_toolchain)))
.subcommand(cl::App("cache")
.description("Inspect and manage the global build cache")
.subcommand(cl::App("dir")
.description("Print the cache root (and any pre-v1 cache)"))
.subcommand(cl::App("list")
.description("List cache entries with size + last-use")
.option(cl::Option("json")
.help("Emit machine-readable JSON (legacy payload, kept for ever)"))
.option(cl::Option("format").takes_value().value_name("json")
.help("Machine-readable output (enveloped; see docs/11-machine-output.md)")))
.subcommand(cl::App("info")
.description("Show details (incl. key inputs) for a cached package")
.arg(cl::Arg("pkg").help("@").required()))
.subcommand(cl::App("prune")
.description("Drop entries not used within a threshold")
.option(cl::Option("older-than").takes_value().value_name("N{s|m|h|d}")
.help("Age threshold (e.g. 30d)")))
.subcommand(cl::App("gc")
.description("LRU-collect package entries to a size and/or age budget")
.option(cl::Option("max-size").takes_value().value_name("N{MiB|GiB}")
.help("Keep the package cache under this size (e.g. 5GiB)"))
.option(cl::Option("older-than").takes_value().value_name("N{s|m|h|d}")
.help("Also drop entries unused for longer than this")))
.subcommand(cl::App("clean")
.description("Drop cache entries (default: package entries only)")
.option(cl::Option("deps").help("Drop package entries (default)"))
.option(cl::Option("std").help("Drop std module entries"))
.option(cl::Option("all").help("Drop both"))
.option(cl::Option("legacy")
.help("Remove the unused pre-v1 cache at $MCPP_HOME/bmi")))
.subcommand(cl::App("verify")
.description("Check every entry's manifest against the files on disk"))
.action(wrap_rc([&dispatch_sub](const cl::ParsedArgs& p) {
return dispatch_sub("cache", p, {
{"dir", cmd_cache_dir},
{"list", cmd_cache_list},
{"info", cmd_cache_info},
{"prune", cmd_cache_prune},
{"gc", cmd_cache_gc},
{"clean", cmd_cache_clean},
{"verify", cmd_cache_verify},
});
})))
.subcommand(cl::App("index")
.description("Manage configured package registries")
.subcommand(cl::App("list")
.description("List configured registries"))
.subcommand(cl::App("add")
.description("Add a custom registry")
.arg(cl::Arg("name").help("Registry name").required())
.arg(cl::Arg("url").help("Registry URL").required()))
.subcommand(cl::App("remove")
.description("Remove a registry")
.arg(cl::Arg("name").help("Registry name").required()))
.subcommand(cl::App("update")
.description("Refresh local registry clones")
.arg(cl::Arg("name").help("If given, update only this index")))
.subcommand(cl::App("status")
.description("Show local index presence/freshness (offline)"))
.subcommand(cl::App("pin")
.description("Pin a custom index to a commit rev in mcpp.toml")
.arg(cl::Arg("name").help("Index name").required())
.arg(cl::Arg("rev").help("Commit sha (defaults to current lock rev)")))
.subcommand(cl::App("unpin")
.description("Remove rev pin from a custom index in mcpp.toml")
.arg(cl::Arg("name").help("Index name").required()))
.action(wrap_rc([&dispatch_sub](const cl::ParsedArgs& p) {
return dispatch_sub("index", p, {
{"list", cmd_index_list},
{"add", cmd_index_add},
{"remove", cmd_index_remove},
{"update", cmd_index_update},
{"status", cmd_index_status},
{"pin", cmd_index_pin},
{"unpin", cmd_index_unpin},
});
})))
// about mcpp itself
.subcommand(cl::App("self")
.description("Inspect and manage mcpp itself")
.subcommand(cl::App("init")
.description("Initialize or repair mcpp sandbox")
.option(cl::Option("force")
.help("Delete registry and re-initialize from scratch")))
.subcommand(cl::App("doctor")
.description("Diagnose mcpp environment health"))
.subcommand(cl::App("env")
.description("Print mcpp paths and configuration")
.option(cl::Option("format").takes_value().value_name("json")
.help("Machine-readable output (enveloped; see docs/11-machine-output.md)")))
.subcommand(cl::App("config")
.description("Show or modify mcpp's private xlings configuration")
.option(cl::Option("mirror").takes_value().value_name("CN|GLOBAL")
.help("Set xlings mirror for mcpp's private registry")))
.subcommand(cl::App("version")
.description("Show mcpp version"))
.subcommand(cl::App("explain")
.description("Show extended description for an error code")
.arg(cl::Arg("code").help("Error code such as E0001").required()))
.action(wrap_rc([&dispatch_sub](const cl::ParsedArgs& p) {
return dispatch_sub("self", p, {
{"init", cmd_self_init},
{"doctor", cmd_doctor},
{"env", cmd_env},
{"config", cmd_self_config},
{"version", cmd_self_version},
{"explain", cmd_explain_action},
});
})))
// top-level explain alias
// Preserves `mcpp explain E0001` as a shortcut for
// `mcpp self explain E0001`.
.subcommand(cl::App("explain")
.description("Show extended description for an error code")
.arg(cl::Arg("code").help("Error code such as E0001").required())
.action(wrap_rc(cmd_explain_action)))
// bareword `version` alias
// cmdline natively handles `--help`/`--version`/`-h`; the bareword
// `mcpp help` is intercepted by the pre-scan above (prints the
// canonical usage screen). `mcpp version` is wired through the
// App so it shows up in the auto-generated subcommand list.
.subcommand(cl::App("version")
.description("Show mcpp version")
.action(wrap_rc(cmd_self_version)))
// hidden / internal
.subcommand(cl::App("dyndep")
.description("(internal: invoked by ninja) Emit ninja dyndep file from .ddi inputs")
.option(cl::Option("output").short_name('o').takes_value().value_name("PATH")
.help("Path to write dyndep file"))
.option(cl::Option("single").help("Single-file mode: one .ddi one .dd"))
.option(cl::Option("bmi-dir").takes_value().value_name("DIR")
.help("BMI cache directory name (default: gcm.cache)"))
.option(cl::Option("bmi-ext").takes_value().value_name("EXT")
.help("BMI file extension (default: .gcm)"))
.option(cl::Option("split-module")
.help("Also emit a record for the provided BMI (two-phase "
"schedule: BMI and object are separate edges)"))
.option(cl::Option("expect-provides").takes_value().value_name("NAME")
.help("(verification) planned provided module for this TU"))
.option(cl::Option("expect-imports").takes_value().value_name("CSV")
.help("(verification) planned imports for this TU, comma-separated"))
.option(cl::Option("expect-none")
.help("(verification) planner assumed no provides/imports"))
.action(wrap_rc(cmd_dyndep)))
.subcommand(cl::App("stage")
.description("(internal: invoked by ninja) Stage a cached artifact into the build dir")
.option(cl::Option("output").short_name('o').takes_value().value_name("PATH")
.help("Destination path inside the build directory"))
.option(cl::Option("verify").takes_value().value_name("MODE")
.help("Already-staged check: size (default) | content"))
.action(wrap_rc(cmd_stage)))
.subcommand(cl::App("coff-def")
.description("(internal: invoked by ninja) Write a .def of every exportable symbol in the given COFF objects")
.option(cl::Option("output").takes_value().value_name("PATH").help("the .def to write"))
.option(cl::Option("name").takes_value().value_name("DLL").help("LIBRARY name recorded in the .def"))
.action(wrap_rc(cmd_coff_def)))
.subcommand(cl::App("bmi-equal")
.description("(internal: invoked by ninja) Compare two BMIs ignoring the compiler's embedded timestamp")
.action(wrap_rc(cmd_bmi_equal)))
// The three edges of the detach-codegen schedule. Internal, and named as
// such: they are only ever invoked by a generated build.ninja.
.subcommand(cl::App("bmi-compile")
.description("(internal) Compile a module interface and return when its BMI is published")
.option(cl::Option("bmi").takes_value().value_name("PATH").help("BMI this unit publishes"))
.option(cl::Option("slot").takes_value().value_name("PATH").help("where .log/.rc are kept"))
.option(cl::Option("self").takes_value().value_name("PATH").help("path to mcpp, re-invoked as supervisor"))
.option(cl::Option("sem").takes_value().value_name("DIR").help("concurrency token directory"))
.option(cl::Option("cap").takes_value().value_name("N").help("max concurrent compilers"))
.option(cl::Option("command-file").takes_value().value_name("PATH").help("file holding the compiler command line"))
.option(cl::Option("dep-from").takes_value().value_name("PATH").help("scanner depfile to adopt"))
.option(cl::Option("dep-to").takes_value().value_name("PATH").help("where ninja expects this edge's depfile"))
.action(wrap_rc(cmd_bmi_compile)))
.subcommand(cl::App("bmi-supervise")
.description("(internal) Run a compiler to completion and record its status")
.option(cl::Option("slot").takes_value().value_name("PATH"))
.option(cl::Option("token").takes_value().value_name("PATH"))
.option(cl::Option("command-file").takes_value().value_name("PATH"))
.action(wrap_rc(cmd_bmi_supervise)))
.subcommand(cl::App("bmi-await")
.description("(internal) Join a detached compiler and replay its diagnostics")
.option(cl::Option("slot").takes_value().value_name("PATH"))
.option(cl::Option("object").takes_value().value_name("PATH"))
.action(wrap_rc(cmd_bmi_await)))
;
// The bareword `mcpp help` and `mcpp` (no args) both print the
// canonical help screen.
if (argc