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
10 changes: 10 additions & 0 deletions .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,16 @@ jobs:
cd hello_win
"$MCPP_SELF" run

# MinGW-w64 GCC via the xlings ecosystem (xim:mingw-gcc → xlings-res
# winlibs mirror): install → default → modules build/run → standalone
# exe. Same flow as e2e 97 but as a visible CI step. Payload is cached
# via the mcpp sandbox cache after the first run.
- name: "Toolchain: MinGW — install → build → run (xim:mingw-gcc)"
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
MCPP="$MCPP_SELF" bash tests/e2e/97_mingw_toolchain.sh

# windows-latest ships VS 2022 Enterprise with the VC workload, so
# msvc@system detection MUST succeed here — a failure is a regression
# in the discovery/identification chain (vswhere → env → paths).
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [0.0.89] — 2026-07-13

### 新增

- **MinGW-w64 工具链入 xlings 生态(Windows 原生 GCC,无需 Visual Studio)**。
`mcpp toolchain install mingw 16.1.0` / `default mingw@16.1.0`:xim 包
`mingw-gcc`(winlibs GCC 16.1.0 + MinGW-w64 14.0.0 UCRT 独立构建,镜像于
xlings-res/mingw-gcc,GitHub+GitCode 双端)。复用既有 GCC 后端
(gcm 模块管线、libstdc++ `bits/std.cc` 的 `import std`);Windows 上
libstdc++/libgcc 默认静态链接(产物免带 DLL,`[build] static_stdlib=false`
可关),`linkage="static"` 升级全静态。e2e 97 覆盖 install→default→
多模块 build/run→独立 exe 验证;ci-windows 新增专项步骤。
连带修复:`toolchain list` 的 Available 段与部分版本解析此前硬编码按
"linux" 平台读取 xpkg 版本(Windows/macOS 上恒空);`toolchain install`
在 Windows/macOS 不再错误安装 glibc/linux-headers 依赖。

### 重构

- **工具链后端抽象层(Part A,对 GCC/Clang 零行为变化,build.ninja 零 diff
实证)**。新增 `mcpp.toolchain.dialect`(命令行拼写 traits:gnu/msvc 两行
数据,`-I/-D/-std=/-c/-o/-O/-g/ar` 及归档命令模板经其发射);`BmiTraits`
并入模块旗标拼写(`compileModulesFlag`/`stdBmiUsePrefix`/
`moduleOutputPrefix`/`bmiSearchPrefix`),flags.cppm 的 is_clang 分支改由
数据驱动;`ProviderCapabilities.has_builtin_p1689_scan` 取代 ninja 后端的
is_gcc 门;`Toolchain::envOverrides`(EnvVar 表,注入 ninja 子进程环境,
为 MSVC 后端 INCLUDE/LIB/PATH 预留);gcc provider 增加与 clang 同形的
`std_module_build_commands`(命令序列);`resolve_link_model` 在 Windows
显式返回 PE 空模型。设计:`.agents/docs/2026-07-13-toolchain-backend-
abstraction-msvc-mingw-design.md` + 同日触点审计文档。

## [0.0.88] — 2026-07-13

### 新增
Expand Down
24 changes: 24 additions & 0 deletions docs/03-toolchains.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,30 @@ Available (run `mcpp toolchain install <compiler> <version>`):

The entry marked with `*` is the current default toolchain. `@mcpp/...` is shorthand for `~/.mcpp/...`, used to keep the output narrower.

## MinGW (Windows-native GCC, no Visual Studio required)

On Windows, `mingw` installs a self-contained MinGW-w64 GCC (winlibs
standalone build, UCRT runtime) into mcpp's sandbox — the same managed-xpkg
model as `gcc`/`llvm`, no Visual Studio needed:

```bash
mcpp toolchain install mingw 16.1.0
mcpp toolchain default mingw@16.1.0
```

It uses the regular GCC module pipeline (`gcm.cache`, `import std` via
libstdc++'s `bits/std.cc`). Produced binaries statically link libstdc++ and
libgcc by default, so they run standalone — no `libstdc++-6.dll` needs to
travel next to your exe (opt out with `[build] static_stdlib = false`).
`[build] linkage = "static"` upgrades that to a fully static link.

In a manifest:

```toml
[toolchain]
windows = "mingw@16.1.0"
```

## MSVC (System Toolchain, Windows)

MSVC is different from every other toolchain mcpp manages: it is a **system
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.88"
version = "0.0.89"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
92 changes: 67 additions & 25 deletions src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import mcpp.build.plan;
import mcpp.platform;
import mcpp.toolchain.clang;
import mcpp.toolchain.detect;
import mcpp.toolchain.dialect;
import mcpp.toolchain.linkmodel;
import mcpp.toolchain.provider;
import mcpp.toolchain.registry;
Expand Down Expand Up @@ -122,10 +123,13 @@ std::string atomic_link_flag(const std::vector<std::filesystem::path>& linkDirs,
CompileFlags compute_flags(const BuildPlan& plan) {
CompileFlags f;

// ProviderCapabilities: centralised query point for per-toolchain decisions.
// Prefer caps.* checks over ad-hoc is_clang()/is_musl_target() calls for
// any new branching added to this function.
// Central query points for per-toolchain decisions — prefer these over
// ad-hoc is_clang()/is_gcc() calls:
// caps — what the toolchain can do (scan-deps, stdlib id, …)
// d — how a flag is SPELT (GNU "-I" vs MSVC "/I")
// traits — BMI mechanics + module-flag spellings
auto caps = mcpp::toolchain::capabilities_for(plan.toolchain);
const auto& d = mcpp::toolchain::dialect_for(plan.toolchain);

// macOS minimum supported OS version for produced binaries.
// Precedence: MACOSX_DEPLOYMENT_TARGET env (explicit per-invocation
Expand All @@ -152,7 +156,7 @@ CompileFlags compute_flags(const BuildPlan& plan) {
std::string include_flags;
for (auto& inc : plan.manifest.buildConfig.includeDirs) {
auto abs = inc.is_absolute() ? inc : (plan.projectRoot / inc);
include_flags += " -I" + escape_path(abs);
include_flags += std::format(" {}{}", d.includePrefix, escape_path(abs));
}

// Sysroot / payload paths — resolved ONCE by the toolchain link model
Expand Down Expand Up @@ -208,11 +212,11 @@ CompileFlags compute_flags(const BuildPlan& plan) {
f.sysroot = link_toolchain_flags;
}

// Binutils -B flag
// Binutils -B flag — a GCC/libstdc++ payload concern (musl bundles its
// own as/ld; Clang and MSVC never take an external binutils).
bool isMuslTc = mcpp::toolchain::is_musl_target(plan.toolchain);
bool isClang = mcpp::toolchain::is_clang(plan.toolchain);
std::filesystem::path binutilsBin;
if (!isMuslTc && !isClang) {
if (!isMuslTc && caps.stdlib_id == "libstdc++") {
auto ar = mcpp::toolchain::archive_tool(plan.toolchain);
if (!ar.empty())
binutilsBin = ar.parent_path();
Expand All @@ -231,8 +235,8 @@ CompileFlags compute_flags(const BuildPlan& plan) {
// unless the profile pins -O0.
auto& prof = plan.manifest.buildConfig;
std::string opt_flag = isMuslTc && prof.optLevel != "0"
? " -Og" : (" -O" + prof.optLevel);
if (prof.debug) opt_flag += " -g";
? " -Og" : std::format(" {}{}", d.optPrefix, prof.optLevel);
if (prof.debug) opt_flag += std::format(" {}", d.debugFlags);
if (prof.lto) opt_flag += " -flto";

// User link flags
Expand All @@ -247,20 +251,25 @@ CompileFlags compute_flags(const BuildPlan& plan) {
plan.manifest.buildConfig.cStandard.empty() ? "c11" : plan.manifest.buildConfig.cStandard;

// Assemble
// -fmodules is a GCC-only flag; Clang uses a different module ABI and does
// not need it. caps.stdlib_id distinguishes GCC (libstdc++) from Clang
// (libc++ / msvc-stl) without an extra is_clang() call.
std::string module_flag = (caps.stdlib_id == "libstdc++") ? " -fmodules" : "";
// Module-flag spellings come from BmiTraits: GCC needs -fmodules on every
// TU (BMIs implicit); Clang/MSVC reference the staged std BMI and a BMI
// search dir explicitly (spelled -fmodule-file=/-fprebuilt-module-path vs
// /reference//ifcSearchDir).
auto traits = mcpp::toolchain::bmi_traits(plan.toolchain);
std::string module_flag{traits.compileModulesFlag};
std::string std_module_flag;
if (isClang && !plan.stdBmiPath.empty()) {
std_module_flag = " -fmodule-file=std=" + escape_path(staged_std_bmi_path(plan));
if (!traits.stdBmiUsePrefix.empty() && !plan.stdBmiPath.empty()) {
std_module_flag = std::string(traits.stdBmiUsePrefix)
+ escape_path(staged_std_bmi_path(plan));
}
std::string std_compat_module_flag;
if (isClang && !plan.stdCompatBmiPath.empty()) {
if (!traits.stdCompatBmiUsePrefix.empty() && !plan.stdCompatBmiPath.empty()) {
// NOTE: staging path is Clang's today; registry-dispatch when the
// MSVC backend lands (std.compat.ixx staging).
auto compatDst = mcpp::toolchain::clang::staged_std_compat_bmi_path(plan.outputDir);
std_compat_module_flag = " -fmodule-file=std.compat=" + escape_path(compatDst);
std_compat_module_flag = std::string(traits.stdCompatBmiUsePrefix)
+ escape_path(compatDst);
}
auto traits = mcpp::toolchain::bmi_traits(plan.toolchain);
std::string prebuilt_module_flag;
if (traits.needsPrebuiltModulePath) {
// Absolute path: a bare `pcm.cache` / `gcm.cache` works at ninja
Expand All @@ -272,22 +281,32 @@ CompileFlags compute_flags(const BuildPlan& plan) {
// resolution fails with `module 'X' not found`. The other
// `-fmodule-file=` flags in this block are already escape_path'd
// (absolute) for the same reason — this one was a leftover.
prebuilt_module_flag = std::format(" -fprebuilt-module-path={}",
escape_path(plan.outputDir / traits.bmiDir));
prebuilt_module_flag = std::string(traits.bmiSearchPrefix)
+ escape_path(plan.outputDir / traits.bmiDir);
}
std::string cxx_std_flag =
plan.cppStandardFlag.empty() ? std::string("-std=c++23") : plan.cppStandardFlag;
plan.cppStandardFlag.empty()
? std::format("{}c++23", d.stdPrefix) : plan.cppStandardFlag;
f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, module_flag, std_module_flag,
std_compat_module_flag, prebuilt_module_flag,
opt_flag, pic_flag, compile_toolchain_flags, b_flag, include_flags);
f.cc = std::format("-std={}{}{}{}{}{}", c_std, opt_flag, pic_flag, compile_toolchain_flags,
b_flag, include_flags);
f.cc = std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag,
compile_toolchain_flags, b_flag, include_flags);

// Link flags
f.staticStdlib = plan.manifest.buildConfig.staticStdlib;
f.linkage = plan.manifest.buildConfig.linkage;
std::string full_static = (mcpp::platform::supports_full_static && f.linkage == "static") ? " -static" : "";
std::string static_stdlib = (f.staticStdlib && !isClang && !mcpp::platform::is_windows) ? " -static-libstdc++" : "";
// Static C++ stdlib: a libstdc++ (GCC/MinGW) concern. On Windows a MinGW
// toolchain also statically links libgcc, so produced binaries don't
// depend on libstdc++-6.dll / libgcc_s DLLs from the toolchain dir
// (portable-by-default, same spirit as the macOS static-libc++ path).
std::string static_stdlib;
if (f.staticStdlib && caps.stdlib_id == "libstdc++") {
static_stdlib = " -static-libstdc++";
if constexpr (mcpp::platform::is_windows)
static_stdlib += " -static-libgcc";
}
std::string runtime_dirs;
if constexpr (mcpp::platform::supports_rpath) {
// Toolchain runtime dirs (glibc/gcc) as before...
Expand Down Expand Up @@ -320,7 +339,30 @@ CompileFlags compute_flags(const BuildPlan& plan) {
if (prof.strip) link_extra += " -s";

if constexpr (mcpp::platform::is_windows) {
f.ld = user_ldflags + link_extra;
// PE link: no rpath/loader/payload model. MSVC-ABI Clang needs
// nothing extra (MSVC STL/SDK via the driver); MinGW adds the static
// libstdc++/libgcc pair (static_stdlib above) and -B so its own
// binutils resolve, plus `-static` for full static when requested
// (MinGW supports it, unlike MSVC-ABI links).
std::string mingw_static;
std::string mingw_stdexp;
if (caps.stdlib_id == "libstdc++") {
// `-static` for the whole link — winlibs' own recommendation for
// standalone exes. The piecemeal recipe (-static-libstdc++ +
// -Wl,-Bstatic -lwinpthread) verifiably loses to the driver's
// implicit closing libs: CI import tables still showed
// libwinpthread-1.dll. System DLLs (KERNEL32/UCRT) still resolve
// via their import libs. Tied to staticStdlib so
// [build] static_stdlib=false opts back into DLL-coupled links.
if (f.staticStdlib || f.linkage == "static")
mingw_static = " -static";
// std::print's terminal probe (__open_terminal /
// __write_to_terminal, bits/print.h) lives in libstdc++exp.a on
// Windows targets — plain -lstdc++ leaves them undefined.
mingw_stdexp = " -lstdc++exp";
}
f.ld = std::format("{}{}{}{}{}{}", mingw_static, static_stdlib, b_flag,
user_ldflags, mingw_stdexp, link_extra);
} else if constexpr (mcpp::platform::needs_explicit_libcxx) {
// macOS. Two min-version concerns (see xlings
// .agents/docs/2026-06-05-macos-min-version-support.md):
Expand Down
39 changes: 30 additions & 9 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import mcpp.build.hermetic;
import mcpp.build.compile_commands;
import mcpp.dyndep;
import mcpp.toolchain.detect;
import mcpp.toolchain.dialect;
import mcpp.toolchain.provider;
import mcpp.toolchain.registry;
import mcpp.xlings;
import mcpp.platform;
Expand Down Expand Up @@ -258,10 +260,12 @@ std::string emit_ninja_string(const BuildPlan& plan) {
// dyndep requires P1689 scanning capability:
// GCC: built-in -fdeps-format=p1689r5
// Clang: external clang-scan-deps tool (same P1689 output format)
bool has_scanner = mcpp::toolchain::is_gcc(plan.toolchain)
|| !plan.scanDepsPath.empty();
// (MSVC /scanDependencies is the future third driver — scanner design §3a)
auto caps = mcpp::toolchain::capabilities_for(plan.toolchain);
bool has_scanner = caps.has_builtin_p1689_scan || !plan.scanDepsPath.empty();
bool dyndep = dyndep_mode_enabled() && has_scanner;
auto traits = mcpp::toolchain::bmi_traits(plan.toolchain);
const auto& dial = mcpp::toolchain::dialect_for(plan.toolchain);
std::string out;
auto append = [&](std::string s) { out += std::move(s); };

Expand Down Expand Up @@ -331,53 +335,65 @@ std::string emit_ninja_string(const BuildPlan& plan) {
// Runtime library paths for private toolchain executables are scoped onto
// the ninja subprocess instead of being emitted into each visible rule.

// Command spellings come from the toolchain's CommandDialect (gnu vs
// msvc); the rule *structure* is shared across compilers.
std::string module_output_flag = traits.needsExplicitModuleOutput
? " -fmodule-output=$bmi_out" : "";
? std::string(traits.moduleOutputPrefix) + "$bmi_out" : "";
const std::string compile_tail = std::format(
"{} $in {}$out", dial.compileOnly, dial.outputObjPrefix);
append("rule cxx_module\n");
if constexpr (mcpp::platform::is_windows) {
// Windows: skip BMI restat optimization (requires POSIX shell).
append(std::format(" command = "
"$cxx $local_includes $cxxflags $unit_cxxflags{} -c $in -o $out\n", module_output_flag));
"$cxx $local_includes $cxxflags $unit_cxxflags{} {}\n",
module_output_flag, compile_tail));
} else {
append(std::format(" command = "
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then "
"cp -p \"$bmi_out\" \"$bmi_out.bak\"; "
"fi && "
"$cxx $local_includes $cxxflags $unit_cxxflags{} -c $in -o $out && "
"$cxx $local_includes $cxxflags $unit_cxxflags{} {} && "
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && "
"cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then "
"mv \"$bmi_out.bak\" \"$bmi_out\"; "
"else "
"rm -f \"$bmi_out.bak\"; "
"fi\n", module_output_flag));
"fi\n", module_output_flag, compile_tail));
}
append(" description = MOD $out\n");
if (dyndep)
append(" restat = 1\n");
append("\n");

append("rule cxx_object\n");
append(" command = $cxx $local_includes $cxxflags $unit_cxxflags -c $in -o $out\n");
append(std::format(
" command = $cxx $local_includes $cxxflags $unit_cxxflags {}\n",
compile_tail));
append(" description = OBJ $out\n");
if (dyndep)
append(" restat = 1\n");
append("\n");

if (need_c_rule) {
append("rule c_object\n");
append(" command = $cc $local_includes $cflags $unit_cflags -c $in -o $out\n");
append(std::format(
" command = $cc $local_includes $cflags $unit_cflags {}\n",
compile_tail));
append(" description = CC $out\n");
if (dyndep)
append(" restat = 1\n");
append("\n");
}

// Link rule: driver-style today (g++/clang++ act as the linker). The
// dialect's LinkStyle::SeparateLinker (link.exe /OUT: + rspfile) is the
// MSVC backend's insertion point — unreachable until that lands.
append("rule cxx_link\n");
append(" command = $cxx $in -o $out $ldflags $unit_ldflags\n");
append(" description = LINK $out\n\n");

append("rule cxx_archive\n");
append(" command = $ar rcs $out $in\n");
append(std::format(" command = {}\n", dial.archiveCmd));
append(" description = AR $out\n\n");

append("rule cxx_shared\n");
Expand Down Expand Up @@ -811,6 +827,11 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
std::vector<std::pair<std::string, std::string>> nenv;
if (r.runtimeEnvKey != "-" && !r.runtimeEnvValue.empty())
nenv.emplace_back(r.runtimeEnvKey, r.runtimeEnvValue);
// Toolchain-declared env (empty for GCC/Clang; MSVC's INCLUDE/LIB/PATH).
// NOTE: not persisted in the fast-path cache yet — revisit when the MSVC
// backend lands (its fast path must re-derive these from detection).
for (auto& ev : plan.toolchain.envOverrides)
nenv.emplace_back(ev.key, ev.value);

auto cap = mcpp::platform::process::capture_exec(nargv, nenv);
std::string out = cap.output;
Expand Down
Loading
Loading