diff --git a/.agents/docs/2026-06-30-target-bare-alias-sugar-design.md b/.agents/docs/2026-06-30-target-bare-alias-sugar-design.md new file mode 100644 index 0000000..60b1218 --- /dev/null +++ b/.agents/docs/2026-06-30-target-bare-alias-sugar-design.md @@ -0,0 +1,95 @@ +# Bare OS-alias sugar for `[target.*]` conditional tables (Design) + +Addresses the ergonomic complaint that `[target.'cfg(linux)'.dependencies.compat]` +is visually noisy. Lets the common single-OS case drop the `cfg(...)` wrapper (and +its mandatory TOML quotes) while keeping the full `cfg(...)` grammar for compound +predicates. Ships in mcpp 0.0.80. + +## Before / after + +```toml +# before — quotes (TOML-mandated around cfg(...)) + ceremony +[target.'cfg(windows)'.dependencies.compat] +openblas = "0.3.33" +[target.'cfg(windows)'.build] +ldflags = ["-Llib", "-llibopenblas"] + +# after — bare alias for the 90% case, no quotes +[target.windows.dependencies.compat] +openblas = "0.3.33" +[target.windows.build] +ldflags = ["-Llib", "-llibopenblas"] + +# compound predicates STILL use cfg(...) (arch / env / all / any / not) +[target.'cfg(all(linux, not(arch = "aarch64")))'.build] +cxxflags = ["-march=x86-64-v2"] +``` + +## Why this is unambiguous + +A mcpp target triple is always `-[-]` (`x86_64-linux-musl`, +`x86_64-pc-windows-msvc`). The bare OS/family aliases **`windows` / `linux` / +`macos` / `unix` are never valid triples** (no dash), so `[target.linux]` can only +mean the `cfg(linux)` predicate — there is no collision with the exact-triple +namespace (`[target.x86_64-linux-musl]`). This is the same alias set already +accepted *inside* `cfg(...)` (`cfgpred::match_alias`), now also accepted as a bare +section key. + +This is a deliberate, small divergence from Cargo (which always requires +`cfg()`/triple) — justified because mcpp's alias set is unambiguous, and the win is +removing the quote-noise from the overwhelmingly common per-OS case. + +## Implementation (one evaluator branch) + +The parser already does the right thing: `manifest.cppm`'s `[target]` loop reads +`build` / `dependencies` / … for **every** `[target.]` and stores a +`ConditionalConfig{ predicate = }` (manifest.cppm:1242-1275). So +`[target.linux.dependencies.compat]` is *already* parsed into a conditional config +with `predicate = "linux"`. The only gap is evaluation. + +`cfgpred::matches()` (`prepare.cppm:139-146`) currently: +```cpp +inline bool matches(const std::string& predicate, const Ctx& c, std::string_view triple) { + std::string_view k = predicate; + if (k.starts_with("cfg(") && k.ends_with(")")) { Parser p{...}; return p.expr(); } + return !triple.empty() && predicate == triple; // bare-triple exact match +} +``` +A bare `"linux"` falls through to the triple branch and never matches (host build → +`triple` empty; cross build → `"linux" != "x86_64-linux-gnu"`). Fix — add the +alias branch **before** the triple fallback: +```cpp + if (predicate == "windows" || predicate == "linux" || + predicate == "macos" || predicate == "unix") { + Parser p{ predicate, 0, c }; // evaluate as the cfg bareword + return p.expr(); + } +``` +That's the whole behavioral change. Evaluation stays **target-resolved** (the +`Ctx` is built from the resolved `--target`, else host) — identical semantics to +`cfg(linux)`. No parser change, no schema change, fully backward-compatible +(`cfg(...)` and exact triples unchanged). + +## Scope boundary (documented) + +The sugar covers the **L1 conditional config**: `.dependencies` / +`.dev-dependencies` / `.build-dependencies` / `.build` (cflags/cxxflags/ldflags). + +`[target.].toolchain` and `.linkage` remain **exact-triple only** — they +describe a *specific* cross target, not an OS family, and are looked up by exact +triple in `prepare_build`. Writing `toolchain`/`linkage` under a bare alias (or a +`cfg(...)` key) has no effect today; a follow-up may add a schema warning to flag +that footgun. Out of scope here. + +## Tests + +`tests/e2e/91_target_bare_alias.sh`: a project with `[target.linux.build] cxxflags` ++ `[target.windows.dependencies]`; assert on Linux the cxxflag define reaches the +TU and the windows-only dep is NOT pulled; assert `[target.'cfg(linux)']` and the +bare `[target.linux]` produce identical results (parity). + +## Docs + +`docs/05-mcpp-toml.md` (+ zh): the `[target.*]` section gains the bare-alias form +as the recommended spelling for single-OS, with `cfg(...)` shown for compound +predicates and the triple form for exact targets. diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index addcec8..58ce224 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -272,6 +272,48 @@ toolchain = "gcc@15.1.0-musl" linkage = "static" ``` +### 2.7.1 `[target.*]` — Platform-Conditional Dependencies & Flags + +Scope dependencies and build flags to a platform with a `[target.]` table. +The selector `` has three forms: + +| Selector | Meaning | Example | +|---|---|---| +| **bare OS alias** | a single OS / family — the concise, common form | `[target.windows]`, `[target.unix]` | +| **`cfg(...)` predicate** | a compound condition (arch / env / combinators) | `[target.'cfg(all(linux, not(arch = "aarch64")))']` | +| **exact triple** | one specific target (also carries `toolchain` / `linkage`) | `[target.x86_64-linux-musl]` | + +Under a selector you may put platform-conditional **dependencies** and **build flags**: + +```toml +# Concise bare-alias form — pull OpenBLAS and link it only on Windows. +[target.windows.dependencies.compat] +openblas = "0.3.33" +[target.windows.build] +ldflags = ["-Llib", "-llibopenblas"] + +# cfg(...) for compound predicates (grammar: all/any/not over os/arch/family/env, +# plus the bare aliases windows/unix/linux/macos). +[target.'cfg(all(linux, not(arch = "aarch64")))'.build] +cxxflags = ["-march=x86-64-v2"] +``` + +`[target.windows]` is exactly equivalent to `[target.'cfg(windows)']` — the bare +aliases `windows` / `linux` / `macos` / `unix` are never valid target triples, so +there is no ambiguity. Use the bare form for a single OS/family; use `cfg(...)` +when you need arch/env conditions or combinators. + +- **Keys**: `dependencies` / `dev-dependencies` / `build-dependencies`, and + `build` with `cflags` / `cxxflags` / `ldflags`. +- **Evaluated against the resolved target** — the `--target` triple for a cross + build, otherwise the host. So a native Linux build never even *downloads* a + `[target.windows]` dependency. +- **Precedence**: an exact-triple table wins over a `cfg`/alias table; multiple + matching predicate tables have their flags concatenated. +- **`toolchain` / `linkage` are exact-triple only** — they describe one specific + cross target, so put them under `[target.]` (above), not under a bare + alias or `cfg(...)`. + ### 2.8 `[features]` — Features (Cargo-style, additive) ```toml diff --git a/docs/zh/05-mcpp-toml.md b/docs/zh/05-mcpp-toml.md index 38d0ff2..56781b6 100644 --- a/docs/zh/05-mcpp-toml.md +++ b/docs/zh/05-mcpp-toml.md @@ -260,6 +260,43 @@ toolchain = "gcc@15.1.0-musl" linkage = "static" ``` +### 2.7.1 `[target.*]` — 平台条件依赖与编译旗标 + +用 `[target.]` 表把依赖和编译旗标限定到某平台。选择子 `` 有三种形式: + +| 选择子 | 含义 | 例子 | +|---|---|---| +| **裸 OS 别名** | 单个 OS / 家族——简洁、最常用 | `[target.windows]`、`[target.unix]` | +| **`cfg(...)` 谓词** | 复合条件(arch / env / 组合子) | `[target.'cfg(all(linux, not(arch = "aarch64")))']` | +| **精确三元组** | 某个具体目标(还承载 `toolchain` / `linkage`) | `[target.x86_64-linux-musl]` | + +任一选择子下都可放平台条件的**依赖**与**编译旗标**: + +```toml +# 简洁的裸别名形式——仅在 Windows 上拉取并链接 OpenBLAS。 +[target.windows.dependencies.compat] +openblas = "0.3.33" +[target.windows.build] +ldflags = ["-Llib", "-llibopenblas"] + +# 复合谓词用 cfg(...)(语法:all/any/not 作用在 os/arch/family/env 上, +# 外加裸别名 windows/unix/linux/macos)。 +[target.'cfg(all(linux, not(arch = "aarch64")))'.build] +cxxflags = ["-march=x86-64-v2"] +``` + +`[target.windows]` 与 `[target.'cfg(windows)']` 完全等价——裸别名 +`windows` / `linux` / `macos` / `unix` 永远不是合法的目标三元组,故无歧义。单个 +OS/家族用裸形式;需要 arch/env 条件或组合子时用 `cfg(...)`。 + +- **可放的键**:`dependencies` / `dev-dependencies` / `build-dependencies`,以及 + `build` 下的 `cflags` / `cxxflags` / `ldflags`。 +- **按解析后的目标求值**——交叉构建时是 `--target` 三元组,否则是 host。所以原生 + Linux 构建**根本不会下载** `[target.windows]` 的依赖。 +- **优先级**:精确三元组表压过 `cfg`/别名表;多个命中的谓词表,其旗标会拼接。 +- **`toolchain` / `linkage` 仅限精确三元组**——它们描述某个具体交叉目标,故应放在 + `[target.]`(见上)下,而非裸别名或 `cfg(...)` 下。 + ### 2.8 `[features]` — 特性(Cargo 式,加性) ```toml diff --git a/mcpp.toml b/mcpp.toml index 8f6a64a..10ff521 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.79" +version = "0.0.80" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index aea57d5..7f659a7 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -142,6 +142,14 @@ inline bool matches(const std::string& predicate, const Ctx& c, std::string_view Parser p{ k.substr(4, k.size() - 5), 0, c }; return p.expr(); } + // Bare OS/family alias sugar: `[target.linux]` ≡ `[target.'cfg(linux)']`. + // These aliases are never valid triples (no dash), so there is no ambiguity + // with the exact-triple namespace. Evaluated as the cfg bareword. + if (predicate == "windows" || predicate == "linux" || + predicate == "macos" || predicate == "unix") { + Parser p{ predicate, 0, c }; + return p.expr(); + } return !triple.empty() && predicate == triple; // bare-triple exact match } diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 8af248c..59e3ff5 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.79"; +inline constexpr std::string_view MCPP_VERSION = "0.0.80"; struct FingerprintInputs { Toolchain toolchain; diff --git a/tests/e2e/91_target_bare_alias.sh b/tests/e2e/91_target_bare_alias.sh new file mode 100755 index 0000000..bcbe05b --- /dev/null +++ b/tests/e2e/91_target_bare_alias.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# 91_target_bare_alias.sh — bare OS-alias sugar for [target.*] conditional tables: +# `[target.linux.build]` ≡ `[target.'cfg(linux)'.build]`. The bare aliases +# windows/linux/macos/unix are never valid triples, so they unambiguously mean the +# cfg predicate. HOST-AWARE: asserts (a) exactly one OS alias applies (the host's) +# and (b) the bare alias agrees with the cfg() form per-OS — so it validates the +# parity on whichever of linux/macos/windows the runner is. +# See .agents/docs/2026-06-30-target-bare-alias-sugar-design.md. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +mkdir -p app/src +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +# Bare-alias forms (the sugar under test). +[target.linux.build] +cxxflags = ["-DBARE_LINUX=1"] +[target.macos.build] +cxxflags = ["-DBARE_MACOS=1"] +[target.windows.build] +cxxflags = ["-DBARE_WIN=1"] +[target.unix.build] +cxxflags = ["-DBARE_UNIX=1"] + +# cfg(...) forms — must produce identical results (parity). +[target.'cfg(linux)'.build] +cxxflags = ["-DCFG_LINUX=1"] +[target.'cfg(macos)'.build] +cxxflags = ["-DCFG_MACOS=1"] +[target.'cfg(windows)'.build] +cxxflags = ["-DCFG_WIN=1"] +[target.'cfg(unix)'.build] +cxxflags = ["-DCFG_UNIX=1"] +EOF +cat > app/src/main.cpp <<'EOF' +// Exactly one OS bare-alias must apply — the host's — on any platform. +#if (defined(BARE_LINUX) + defined(BARE_MACOS) + defined(BARE_WIN)) != 1 +#error "exactly one bare OS alias (linux/macos/windows) must apply on any host" +#endif +// Bare alias and cfg() must agree per OS (parity). +#if defined(BARE_LINUX) != defined(CFG_LINUX) +#error "[target.linux] disagrees with [target.'cfg(linux)']" +#endif +#if defined(BARE_MACOS) != defined(CFG_MACOS) +#error "[target.macos] disagrees with [target.'cfg(macos)']" +#endif +#if defined(BARE_WIN) != defined(CFG_WIN) +#error "[target.windows] disagrees with [target.'cfg(windows)']" +#endif +#if defined(BARE_UNIX) != defined(CFG_UNIX) +#error "[target.unix] disagrees with [target.'cfg(unix)']" +#endif +// unix family applies iff not windows. +#if defined(BARE_WIN) && defined(BARE_UNIX) +#error "unix wrongly applied on a windows host" +#endif +#if !defined(BARE_WIN) && !defined(BARE_UNIX) +#error "unix should apply on a non-windows host" +#endif +int main() { return 0; } +EOF + +cd app +"$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL: build errored (bare alias not honored?)"; exit 1; } + +echo "OK"