Skip to content

Commit 4e0cc03

Browse files
authored
Add LLVM toolchain support and mirror config
Adds LLVM/Clang toolchain provider support, import std handling, self mirror config, xlings index migration, and bumps release metadata to 0.0.14.
1 parent 1f4d5af commit 4e0cc03

27 files changed

Lines changed: 2441 additions & 506 deletions

.agents/docs/2026-05-13-llvm-clang-toolchain-support-design.md

Lines changed: 696 additions & 0 deletions
Large diffs are not rendered by default.

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ jobs:
109109
# above so they don't have to reinstall the sandbox.
110110
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
111111
test -x "$MCPP_VENDORED_XLINGS"
112+
# GitHub-hosted runners are outside CN; keep CI toolchain downloads on
113+
# the global mirror while mcpp's default remains CN for fresh local
114+
# sandboxes. E2E tests with their own MCPP_HOME read this variable.
115+
export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL
116+
"$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR"
117+
"$MCPP" self config
112118
# Pin the global default so test 28 (which exercises the
113119
# default-toolchain path) gets a deterministic GNU answer
114120
# instead of whatever auto-install picks on a fresh sandbox.

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.14] — 2026-05-13
7+
8+
LLVM / Clang 工具链支持与 xlings 镜像配置完善。
9+
10+
### 新增
11+
12+
-**LLVM / Clang 工具链支持** —— 新增基于 `clang++``clang-scan-deps`
13+
`llvm-ar``lld` 的工具链探测与构建路径,支持 xlings `llvm` 包提供的
14+
自包含 Linux LLVM 工具链。
15+
-**`import std` 支持** —— LLVM libc++ 模块标准库可用时,自动发现
16+
`std.cppm` / `std.compat.cppm`,并接入标准库 BMI 预构建流程。
17+
-**`mcpp self config --mirror`** —— 通过 xlings 抽象层配置 sandbox
18+
镜像,默认初始化为 `CN`,CI 可显式切换为 `GLOBAL`
19+
20+
### 改进
21+
22+
- 🔧 **工具链 provider 拆分** —— 将通用模型、探测逻辑、GCC、Clang、LLVM
23+
provider 与 registry 分离到独立模块,为后续更多工具链扩展预留入口。
24+
- 🔧 **xlings 索引兼容迁移** —— 自动将历史 `mcpp-index` 索引名迁移到
25+
`mcpplibs`,避免旧 sandbox 状态影响新流程。
26+
627
## [0.0.4] — 2026-05-10
728

829
构建 / 环境体验优化三件套。

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.13"
3+
version = "0.0.14"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/flags.cppm

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export module mcpp.build.flags;
1010

1111
import std;
1212
import mcpp.build.plan;
13-
import mcpp.xlings;
13+
import mcpp.toolchain.detect;
14+
import mcpp.toolchain.registry;
1415

1516
export namespace mcpp::build {
1617

@@ -23,6 +24,7 @@ struct CompileFlags {
2324
std::filesystem::path arBinary; // ar path (may be empty → use PATH)
2425
std::string sysroot; // --sysroot=... (for ninja ldflags)
2526
std::string bFlag; // -B<binutils> (for ninja ldflags)
27+
std::string toolEnv; // env prefix for private toolchain executables
2628
bool staticStdlib = true;
2729
std::string linkage; // "static" or ""
2830
};
@@ -35,23 +37,8 @@ namespace mcpp::build {
3537

3638
namespace {
3739

38-
std::filesystem::path derive_c_compiler(const std::filesystem::path& cxxPath) {
39-
auto stem = cxxPath.stem().string();
40-
auto parent = cxxPath.parent_path();
41-
auto ext = cxxPath.extension();
42-
43-
// g++ → gcc, clang++ → clang, x86_64-linux-musl-g++ → x86_64-linux-musl-gcc
44-
std::string cc_stem;
45-
if (stem.ends_with("++")) {
46-
cc_stem = stem.substr(0, stem.size() - 2);
47-
// g++ → gcc; clang++ → clang
48-
if (cc_stem.ends_with("g"))
49-
cc_stem += "cc"; // g → gcc
50-
// else clang++ → clang (already correct after stripping ++)
51-
} else {
52-
cc_stem = stem; // fallback: same as cxx
53-
}
54-
return parent / (cc_stem + ext.string());
40+
std::filesystem::path staged_std_bmi_path(const BuildPlan& plan) {
41+
return mcpp::toolchain::staged_std_bmi_path(plan.toolchain, plan.outputDir);
5542
}
5643

5744
// Escape a path for embedding in ninja rule strings.
@@ -72,7 +59,8 @@ std::string escape_path(const std::filesystem::path& p) {
7259
CompileFlags compute_flags(const BuildPlan& plan) {
7360
CompileFlags f;
7461
f.cxxBinary = plan.toolchain.binaryPath;
75-
f.ccBinary = derive_c_compiler(plan.toolchain.binaryPath);
62+
f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain);
63+
f.toolEnv = mcpp::toolchain::compiler_env_prefix(plan.toolchain);
7664

7765
// PIC?
7866
bool need_pic = false;
@@ -99,13 +87,13 @@ CompileFlags compute_flags(const BuildPlan& plan) {
9987
}
10088

10189
// Binutils -B flag
102-
bool isMuslTc = plan.toolchain.targetTriple.find("-musl") != std::string::npos;
90+
bool isMuslTc = mcpp::toolchain::is_musl_target(plan.toolchain);
91+
bool isClang = mcpp::toolchain::is_clang(plan.toolchain);
10392
std::filesystem::path binutilsBin;
104-
if (!isMuslTc) {
105-
if (auto ar = mcpp::xlings::paths::find_sibling_binary(
106-
plan.toolchain.binaryPath, "binutils", "bin/ar")) {
107-
binutilsBin = ar->parent_path(); // bin/ar → bin/
108-
}
93+
if (!isMuslTc && !isClang) {
94+
auto ar = mcpp::toolchain::archive_tool(plan.toolchain);
95+
if (!ar.empty())
96+
binutilsBin = ar.parent_path();
10997
}
11098
std::string b_flag;
11199
if (!binutilsBin.empty()) {
@@ -114,13 +102,7 @@ CompileFlags compute_flags(const BuildPlan& plan) {
114102
}
115103

116104
// AR binary
117-
if (!binutilsBin.empty()) {
118-
f.arBinary = binutilsBin / "ar";
119-
} else if (isMuslTc) {
120-
auto muslAr = plan.toolchain.binaryPath.parent_path() / "x86_64-linux-musl-ar";
121-
if (std::filesystem::exists(muslAr))
122-
f.arBinary = muslAr;
123-
}
105+
f.arBinary = mcpp::toolchain::archive_tool(plan.toolchain);
124106

125107
// Opt level (musl ICE workaround)
126108
std::string opt_flag = isMuslTc ? " -Og" : " -O2";
@@ -142,17 +124,28 @@ CompileFlags compute_flags(const BuildPlan& plan) {
142124
plan.manifest.buildConfig.cStandard.empty() ? "c11" : plan.manifest.buildConfig.cStandard;
143125

144126
// Assemble
145-
f.cxx = std::format("-std=c++23 -fmodules{}{}{}{}{}{}", opt_flag, pic_flag, sysroot_flag,
146-
b_flag, include_flags, user_cxxflags);
127+
std::string module_flag = isClang ? "" : " -fmodules";
128+
std::string std_module_flag;
129+
if (isClang && !plan.stdBmiPath.empty()) {
130+
std_module_flag = " -fmodule-file=std=" + escape_path(staged_std_bmi_path(plan));
131+
}
132+
f.cxx = std::format("-std=c++23{}{}{}{}{}{}{}{}", module_flag, std_module_flag,
133+
opt_flag, pic_flag, sysroot_flag, b_flag, include_flags, user_cxxflags);
147134
f.cc = std::format("-std={}{}{}{}{}{}{}", c_std, opt_flag, pic_flag, sysroot_flag, b_flag,
148135
include_flags, user_cflags);
149136

150137
// Link flags
151138
f.staticStdlib = plan.manifest.buildConfig.staticStdlib;
152139
f.linkage = plan.manifest.buildConfig.linkage;
153140
std::string full_static = (f.linkage == "static") ? " -static" : "";
154-
std::string static_stdlib = f.staticStdlib ? " -static-libstdc++" : "";
155-
f.ld = std::format("{}{}{}{}", full_static, static_stdlib, sysroot_flag, b_flag);
141+
std::string static_stdlib = (f.staticStdlib && !isClang) ? " -static-libstdc++" : "";
142+
std::string runtime_dirs;
143+
for (auto& dir : plan.toolchain.linkRuntimeDirs) {
144+
runtime_dirs += " -L" + escape_path(dir);
145+
runtime_dirs += " -Wl,-rpath," + escape_path(dir);
146+
}
147+
f.ld = std::format("{}{}{}{}{}", full_static, static_stdlib, sysroot_flag, b_flag,
148+
runtime_dirs);
156149

157150
return f;
158151
}

src/build/ninja_backend.cppm

Lines changed: 37 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import mcpp.build.plan;
2323
import mcpp.build.flags;
2424
import mcpp.build.compile_commands;
2525
import mcpp.dyndep;
26+
import mcpp.toolchain.detect;
27+
import mcpp.toolchain.registry;
2628
import mcpp.xlings;
2729

2830
export namespace mcpp::build {
@@ -67,6 +69,16 @@ std::string escape_ninja_path(const std::filesystem::path& p) {
6769
return out;
6870
}
6971

72+
std::string escape_ninja_variable_value(std::string_view s) {
73+
std::string out;
74+
out.reserve(s.size());
75+
for (char c : s) {
76+
if (c == '$') out += "$$";
77+
else out.push_back(c);
78+
}
79+
return out;
80+
}
81+
7082
void write_file(const std::filesystem::path& p, std::string_view content) {
7183
std::filesystem::create_directories(p.parent_path());
7284
std::ofstream os(p);
@@ -107,47 +119,15 @@ std::filesystem::path mcpp_exe_path() {
107119
return "mcpp"; // fall back to PATH lookup
108120
}
109121

110-
// Derive a sibling C compiler from a C++ compiler binary path. Used so .c
111-
// sources can be compiled by the actual C frontend (cc1), not g++ which
112-
// rejects implicit `void*` conversions and `restrict` etc.
113-
// .../bin/g++ → .../bin/gcc
114-
// .../bin/x86_64-linux-musl-g++ → .../bin/x86_64-linux-musl-gcc
115-
// .../bin/clang++ → .../bin/clang
116-
// .../bin/c++ → .../bin/cc
117-
// If no sibling exists, return "gcc" so PATH lookup is the final fallback
118-
// (this also keeps unit tests that don't touch a real toolchain happy).
119-
std::filesystem::path derive_c_compiler(const std::filesystem::path& cxx) {
120-
auto fname = cxx.filename().string();
121-
auto try_replace = [&](std::string_view from,
122-
std::string_view to) -> std::optional<std::filesystem::path> {
123-
auto pos = fname.rfind(from);
124-
if (pos == std::string::npos)
125-
return std::nullopt;
126-
std::string repl = fname;
127-
repl.replace(pos, from.size(), to);
128-
auto p = cxx.parent_path() / repl;
129-
std::error_code ec;
130-
if (std::filesystem::exists(p, ec))
131-
return p;
132-
return std::nullopt;
133-
};
134-
if (auto p = try_replace("clang++", "clang"))
135-
return *p;
136-
if (auto p = try_replace("g++", "gcc"))
137-
return *p;
138-
if (auto p = try_replace("c++", "cc"))
139-
return *p;
140-
return "gcc";
141-
}
142-
143122
bool is_c_source(const std::filesystem::path& src) {
144123
return src.extension() == ".c";
145124
}
146125

147126
} // namespace
148127

149128
std::string emit_ninja_string(const BuildPlan& plan) {
150-
bool dyndep = dyndep_mode_enabled();
129+
bool dyndep = dyndep_mode_enabled()
130+
&& mcpp::toolchain::is_gcc(plan.toolchain);
151131
std::string out;
152132
auto append = [&](std::string s) { out += std::move(s); };
153133

@@ -167,6 +147,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
167147

168148
append(std::format("cxx = {}\n", escape_ninja_path(flags.cxxBinary)));
169149
append(std::format("cxxflags = {}\n", flags.cxx));
150+
append(std::format("toolenv = {}\n", escape_ninja_variable_value(flags.toolEnv)));
170151
if (need_c_rule) {
171152
append(std::format("cc = {}\n", escape_ninja_path(flags.ccBinary)));
172153
append(std::format("cflags = {}\n", flags.cc));
@@ -208,7 +189,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
208189
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then "
209190
"cp -p \"$bmi_out\" \"$bmi_out.bak\"; "
210191
"fi && "
211-
"$cxx $cxxflags -c $in -o $out && "
192+
"$toolenv $cxx $cxxflags -c $in -o $out && "
212193
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && "
213194
"cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then "
214195
"mv \"$bmi_out.bak\" \"$bmi_out\"; "
@@ -221,38 +202,38 @@ std::string emit_ninja_string(const BuildPlan& plan) {
221202
append("\n");
222203

223204
append("rule cxx_object\n");
224-
append(" command = $cxx $cxxflags -c $in -o $out\n");
205+
append(" command = $toolenv $cxx $cxxflags -c $in -o $out\n");
225206
append(" description = OBJ $out\n");
226207
if (dyndep)
227208
append(" restat = 1\n");
228209
append("\n");
229210

230211
if (need_c_rule) {
231212
append("rule c_object\n");
232-
append(" command = $cc $cflags -c $in -o $out\n");
213+
append(" command = $toolenv $cc $cflags -c $in -o $out\n");
233214
append(" description = CC $out\n");
234215
if (dyndep)
235216
append(" restat = 1\n");
236217
append("\n");
237218
}
238219

239220
append("rule cxx_link\n");
240-
append(" command = $cxx $in -o $out $ldflags\n");
221+
append(" command = $toolenv $cxx $in -o $out $ldflags\n");
241222
append(" description = LINK $out\n\n");
242223

243224
append("rule cxx_archive\n");
244-
append(" command = $ar rcs $out $in\n");
225+
append(" command = $toolenv $ar rcs $out $in\n");
245226
append(" description = AR $out\n\n");
246227

247228
append("rule cxx_shared\n");
248-
append(" command = $cxx -shared $in -o $out $ldflags\n");
229+
append(" command = $toolenv $cxx -shared $in -o $out $ldflags\n");
249230
append(" description = SHARED $out\n\n");
250231

251232
if (dyndep) {
252233
// Scan rule: produce P1689 .ddi for one TU.
253234
// -E -M -MM -MF gives us the dep file; -fdeps-* gives us the .ddi.
254235
append("rule cxx_scan\n");
255-
append(" command = $cxx $cxxflags -fdeps-format=p1689r5 "
236+
append(" command = $toolenv $cxx $cxxflags -fdeps-format=p1689r5 "
256237
"-fdeps-file=$out -fdeps-target=$compile_target "
257238
"-M -MM -MF $out.dep -E $in -o $compile_target\n");
258239
append(" description = SCAN $out\n\n");
@@ -264,14 +245,17 @@ std::string emit_ninja_string(const BuildPlan& plan) {
264245
append(" restat = 1\n\n");
265246
}
266247

267-
// Stage prebuilt std artifacts into our gcm.cache/
268-
auto std_bmi_dst = std::filesystem::path("gcm.cache") / "std.gcm";
248+
// Stage prebuilt std artifacts into the compiler-specific BMI cache.
249+
auto std_bmi_dst = mcpp::toolchain::staged_std_bmi_path(plan.toolchain, {});
269250
auto std_o_dst = std::filesystem::path("obj") / "std.o";
270251

271-
append(std::format("build {} : cp_bmi {}\n", escape_ninja_path(std_bmi_dst),
272-
escape_ninja_path(plan.stdBmiPath)));
273-
append(std::format("build {} : cp_bmi {}\n\n", escape_ninja_path(std_o_dst),
274-
escape_ninja_path(plan.stdObjectPath)));
252+
bool has_std_artifacts = !plan.stdBmiPath.empty() && !plan.stdObjectPath.empty();
253+
if (has_std_artifacts) {
254+
append(std::format("build {} : cp_bmi {}\n", escape_ninja_path(std_bmi_dst),
255+
escape_ninja_path(plan.stdBmiPath)));
256+
append(std::format("build {} : cp_bmi {}\n\n", escape_ninja_path(std_o_dst),
257+
escape_ninja_path(plan.stdObjectPath)));
258+
}
275259

276260
auto bmi_path = [](std::string_view name) {
277261
std::string s = "gcm.cache/";
@@ -366,7 +350,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
366350
if (rule != "c_object") {
367351
for (auto& imp : cu.imports) {
368352
if (imp == "std" || imp == "std.compat") {
369-
implicit += " gcm.cache/std.gcm";
353+
if (has_std_artifacts)
354+
implicit += " " + escape_ninja_path(std_bmi_dst);
370355
continue;
371356
}
372357
implicit += " " + bmi_path(imp);
@@ -397,14 +382,16 @@ std::string emit_ninja_string(const BuildPlan& plan) {
397382
switch (lu.kind) {
398383
case LinkUnit::Binary:
399384
case LinkUnit::TestBinary:
400-
ins += " " + escape_ninja_path(std_o_dst);
385+
if (has_std_artifacts)
386+
ins += " " + escape_ninja_path(std_o_dst);
401387
rule = "cxx_link";
402388
break;
403389
case LinkUnit::StaticLibrary:
404390
rule = "cxx_archive";
405391
break;
406392
case LinkUnit::SharedLibrary:
407-
ins += " " + escape_ninja_path(std_o_dst);
393+
if (has_std_artifacts)
394+
ins += " " + escape_ninja_path(std_o_dst);
408395
rule = "cxx_shared";
409396
break;
410397
}

0 commit comments

Comments
 (0)