Skip to content

Commit 9ecd104

Browse files
committed
feat: add LLVM clang toolchain support
1 parent 1f4d5af commit 9ecd104

9 files changed

Lines changed: 1117 additions & 56 deletions

File tree

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

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

src/build/flags.cppm

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

1111
import std;
1212
import mcpp.build.plan;
13+
import mcpp.toolchain.detect;
1314
import mcpp.xlings;
1415

1516
export namespace mcpp::build {
@@ -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
};
@@ -44,8 +46,9 @@ std::filesystem::path derive_c_compiler(const std::filesystem::path& cxxPath) {
4446
std::string cc_stem;
4547
if (stem.ends_with("++")) {
4648
cc_stem = stem.substr(0, stem.size() - 2);
47-
// g++ → gcc; clang++ → clang
48-
if (cc_stem.ends_with("g"))
49+
// g++ → gcc; x86_64-linux-musl-g++ → x86_64-linux-musl-gcc;
50+
// clang++ → clang.
51+
if (cc_stem == "g" || cc_stem.ends_with("-g"))
4952
cc_stem += "cc"; // g → gcc
5053
// else clang++ → clang (already correct after stripping ++)
5154
} else {
@@ -73,6 +76,7 @@ CompileFlags compute_flags(const BuildPlan& plan) {
7376
CompileFlags f;
7477
f.cxxBinary = plan.toolchain.binaryPath;
7578
f.ccBinary = derive_c_compiler(plan.toolchain.binaryPath);
79+
f.toolEnv = mcpp::toolchain::compiler_env_prefix(plan.toolchain);
7680

7781
// PIC?
7882
bool need_pic = false;
@@ -100,8 +104,9 @@ CompileFlags compute_flags(const BuildPlan& plan) {
100104

101105
// Binutils -B flag
102106
bool isMuslTc = plan.toolchain.targetTriple.find("-musl") != std::string::npos;
107+
bool isClang = plan.toolchain.compiler == mcpp::toolchain::CompilerId::Clang;
103108
std::filesystem::path binutilsBin;
104-
if (!isMuslTc) {
109+
if (!isMuslTc && !isClang) {
105110
if (auto ar = mcpp::xlings::paths::find_sibling_binary(
106111
plan.toolchain.binaryPath, "binutils", "bin/ar")) {
107112
binutilsBin = ar->parent_path(); // bin/ar → bin/
@@ -114,7 +119,11 @@ CompileFlags compute_flags(const BuildPlan& plan) {
114119
}
115120

116121
// AR binary
117-
if (!binutilsBin.empty()) {
122+
if (isClang) {
123+
auto llvmAr = plan.toolchain.binaryPath.parent_path() / "llvm-ar";
124+
if (std::filesystem::exists(llvmAr))
125+
f.arBinary = llvmAr;
126+
} else if (!binutilsBin.empty()) {
118127
f.arBinary = binutilsBin / "ar";
119128
} else if (isMuslTc) {
120129
auto muslAr = plan.toolchain.binaryPath.parent_path() / "x86_64-linux-musl-ar";
@@ -142,17 +151,24 @@ CompileFlags compute_flags(const BuildPlan& plan) {
142151
plan.manifest.buildConfig.cStandard.empty() ? "c11" : plan.manifest.buildConfig.cStandard;
143152

144153
// Assemble
145-
f.cxx = std::format("-std=c++23 -fmodules{}{}{}{}{}{}", opt_flag, pic_flag, sysroot_flag,
146-
b_flag, include_flags, user_cxxflags);
154+
std::string module_flag = isClang ? "" : " -fmodules";
155+
f.cxx = std::format("-std=c++23{}{}{}{}{}{}{}", module_flag, opt_flag, pic_flag,
156+
sysroot_flag, b_flag, include_flags, user_cxxflags);
147157
f.cc = std::format("-std={}{}{}{}{}{}{}", c_std, opt_flag, pic_flag, sysroot_flag, b_flag,
148158
include_flags, user_cflags);
149159

150160
// Link flags
151161
f.staticStdlib = plan.manifest.buildConfig.staticStdlib;
152162
f.linkage = plan.manifest.buildConfig.linkage;
153163
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);
164+
std::string static_stdlib = (f.staticStdlib && !isClang) ? " -static-libstdc++" : "";
165+
std::string runtime_dirs;
166+
for (auto& dir : plan.toolchain.linkRuntimeDirs) {
167+
runtime_dirs += " -L" + escape_path(dir);
168+
runtime_dirs += " -Wl,-rpath," + escape_path(dir);
169+
}
170+
f.ld = std::format("{}{}{}{}{}", full_static, static_stdlib, sysroot_flag, b_flag,
171+
runtime_dirs);
156172

157173
return f;
158174
}

src/build/ninja_backend.cppm

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import mcpp.build.plan;
2323
import mcpp.build.flags;
2424
import mcpp.build.compile_commands;
2525
import mcpp.dyndep;
26+
import mcpp.toolchain.detect;
2627
import mcpp.xlings;
2728

2829
export namespace mcpp::build {
@@ -67,6 +68,16 @@ std::string escape_ninja_path(const std::filesystem::path& p) {
6768
return out;
6869
}
6970

71+
std::string escape_ninja_variable_value(std::string_view s) {
72+
std::string out;
73+
out.reserve(s.size());
74+
for (char c : s) {
75+
if (c == '$') out += "$$";
76+
else out.push_back(c);
77+
}
78+
return out;
79+
}
80+
7081
void write_file(const std::filesystem::path& p, std::string_view content) {
7182
std::filesystem::create_directories(p.parent_path());
7283
std::ofstream os(p);
@@ -147,7 +158,8 @@ bool is_c_source(const std::filesystem::path& src) {
147158
} // namespace
148159

149160
std::string emit_ninja_string(const BuildPlan& plan) {
150-
bool dyndep = dyndep_mode_enabled();
161+
bool dyndep = dyndep_mode_enabled()
162+
&& plan.toolchain.compiler == mcpp::toolchain::CompilerId::GCC;
151163
std::string out;
152164
auto append = [&](std::string s) { out += std::move(s); };
153165

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

168180
append(std::format("cxx = {}\n", escape_ninja_path(flags.cxxBinary)));
169181
append(std::format("cxxflags = {}\n", flags.cxx));
182+
append(std::format("toolenv = {}\n", escape_ninja_variable_value(flags.toolEnv)));
170183
if (need_c_rule) {
171184
append(std::format("cc = {}\n", escape_ninja_path(flags.ccBinary)));
172185
append(std::format("cflags = {}\n", flags.cc));
@@ -208,7 +221,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
208221
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then "
209222
"cp -p \"$bmi_out\" \"$bmi_out.bak\"; "
210223
"fi && "
211-
"$cxx $cxxflags -c $in -o $out && "
224+
"$toolenv $cxx $cxxflags -c $in -o $out && "
212225
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && "
213226
"cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then "
214227
"mv \"$bmi_out.bak\" \"$bmi_out\"; "
@@ -221,38 +234,38 @@ std::string emit_ninja_string(const BuildPlan& plan) {
221234
append("\n");
222235

223236
append("rule cxx_object\n");
224-
append(" command = $cxx $cxxflags -c $in -o $out\n");
237+
append(" command = $toolenv $cxx $cxxflags -c $in -o $out\n");
225238
append(" description = OBJ $out\n");
226239
if (dyndep)
227240
append(" restat = 1\n");
228241
append("\n");
229242

230243
if (need_c_rule) {
231244
append("rule c_object\n");
232-
append(" command = $cc $cflags -c $in -o $out\n");
245+
append(" command = $toolenv $cc $cflags -c $in -o $out\n");
233246
append(" description = CC $out\n");
234247
if (dyndep)
235248
append(" restat = 1\n");
236249
append("\n");
237250
}
238251

239252
append("rule cxx_link\n");
240-
append(" command = $cxx $in -o $out $ldflags\n");
253+
append(" command = $toolenv $cxx $in -o $out $ldflags\n");
241254
append(" description = LINK $out\n\n");
242255

243256
append("rule cxx_archive\n");
244-
append(" command = $ar rcs $out $in\n");
257+
append(" command = $toolenv $ar rcs $out $in\n");
245258
append(" description = AR $out\n\n");
246259

247260
append("rule cxx_shared\n");
248-
append(" command = $cxx -shared $in -o $out $ldflags\n");
261+
append(" command = $toolenv $cxx -shared $in -o $out $ldflags\n");
249262
append(" description = SHARED $out\n\n");
250263

251264
if (dyndep) {
252265
// Scan rule: produce P1689 .ddi for one TU.
253266
// -E -M -MM -MF gives us the dep file; -fdeps-* gives us the .ddi.
254267
append("rule cxx_scan\n");
255-
append(" command = $cxx $cxxflags -fdeps-format=p1689r5 "
268+
append(" command = $toolenv $cxx $cxxflags -fdeps-format=p1689r5 "
256269
"-fdeps-file=$out -fdeps-target=$compile_target "
257270
"-M -MM -MF $out.dep -E $in -o $compile_target\n");
258271
append(" description = SCAN $out\n\n");
@@ -268,10 +281,13 @@ std::string emit_ninja_string(const BuildPlan& plan) {
268281
auto std_bmi_dst = std::filesystem::path("gcm.cache") / "std.gcm";
269282
auto std_o_dst = std::filesystem::path("obj") / "std.o";
270283

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)));
284+
bool has_std_artifacts = !plan.stdBmiPath.empty() && !plan.stdObjectPath.empty();
285+
if (has_std_artifacts) {
286+
append(std::format("build {} : cp_bmi {}\n", escape_ninja_path(std_bmi_dst),
287+
escape_ninja_path(plan.stdBmiPath)));
288+
append(std::format("build {} : cp_bmi {}\n\n", escape_ninja_path(std_o_dst),
289+
escape_ninja_path(plan.stdObjectPath)));
290+
}
275291

276292
auto bmi_path = [](std::string_view name) {
277293
std::string s = "gcm.cache/";
@@ -366,7 +382,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
366382
if (rule != "c_object") {
367383
for (auto& imp : cu.imports) {
368384
if (imp == "std" || imp == "std.compat") {
369-
implicit += " gcm.cache/std.gcm";
385+
if (has_std_artifacts)
386+
implicit += " gcm.cache/std.gcm";
370387
continue;
371388
}
372389
implicit += " " + bmi_path(imp);
@@ -397,14 +414,16 @@ std::string emit_ninja_string(const BuildPlan& plan) {
397414
switch (lu.kind) {
398415
case LinkUnit::Binary:
399416
case LinkUnit::TestBinary:
400-
ins += " " + escape_ninja_path(std_o_dst);
417+
if (has_std_artifacts)
418+
ins += " " + escape_ninja_path(std_o_dst);
401419
rule = "cxx_link";
402420
break;
403421
case LinkUnit::StaticLibrary:
404422
rule = "cxx_archive";
405423
break;
406424
case LinkUnit::SharedLibrary:
407-
ins += " " + escape_ninja_path(std_o_dst);
425+
if (has_std_artifacts)
426+
ins += " " + escape_ninja_path(std_o_dst);
408427
rule = "cxx_shared";
409428
break;
410429
}

0 commit comments

Comments
 (0)