Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .agents/docs/2026-06-30-target-bare-alias-sugar-design.md
Original file line number Diff line number Diff line change
@@ -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 `<arch>-<os>[-<env>]` (`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.<key>]` and stores a
`ConditionalConfig{ predicate = <key> }` (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.<triple>].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.
42 changes: 42 additions & 0 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<sel>]` table.
The selector `<sel>` 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.<triple>]` (above), not under a bare
alias or `cfg(...)`.

### 2.8 `[features]` — Features (Cargo-style, additive)

```toml
Expand Down
37 changes: 37 additions & 0 deletions docs/zh/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,43 @@ toolchain = "gcc@15.1.0-musl"
linkage = "static"
```

### 2.7.1 `[target.*]` — 平台条件依赖与编译旗标

用 `[target.<sel>]` 表把依赖和编译旗标限定到某平台。选择子 `<sel>` 有三种形式:

| 选择子 | 含义 | 例子 |
|---|---|---|
| **裸 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.<triple>]`(见上)下,而非裸别名或 `cfg(...)` 下。

### 2.8 `[features]` — 特性(Cargo 式,加性)

```toml
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
8 changes: 8 additions & 0 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
72 changes: 72 additions & 0 deletions tests/e2e/91_target_bare_alias.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading