Date: 2026-05-13
xlings now provides the Linux LLVM toolchain package MCPP needs to start
supporting Clang as a first-class compiler family. I installed
llvm@20.1.7 into MCPP's isolated registry and verified the package layout.
The main conclusion is that MCPP should not add Clang support as scattered
if clang branches. Current build behavior is tightly coupled to GCC:
bits/std.cc, gcm.cache/*.gcm, -fmodules, GCC P1689 flags,
-static-libstdc++, and binutils ar are all assumed in different modules.
Clang support needs a small toolchain abstraction layer that separates
toolchain resolution, executable launch environment, compiler probing,
standard-library module handling, module BMI dialect, and link/runtime
policy.
Command used from this repository:
target/x86_64-linux-gnu/407914674e90df09/bin/mcpp toolchain install llvm 20.1.7Result:
Installed llvm@20.1.7 -> /home/speak/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7/bin/clang++
MCPP isolated environment:
MCPP_HOME = /home/speak/.mcpp
xlings home = /home/speak/.mcpp/registry
llvm root = /home/speak/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7
mcpp toolchain list now sees the installed toolchain:
Installed:
TOOLCHAIN BINARY
llvm 20.1.7 @mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7/bin/clang++
Before this implementation, MCPP CLI did not list LLVM in the Available
section because cmd_toolchain list explicitly enumerated only gcc and
musl-gcc. Exact install still worked because the install path could resolve
xim:llvm@20.1.7.
The relevant package file is:
/home/speak/.mcpp/registry/data/xim-pkgindex/pkgs/l/llvm.lua
Important package facts:
- Package name:
llvm - Version:
20.1.7 - Linux artifact:
llvm-20.1.7-linux-x86_64.tar.xz - Declared Linux dependencies:
xim:glibc@2.39,xim:linux-headers@5.11.1 - Tools:
clang,clang++,lld,ld.lld,llvm-ar,llvm-ranlib,llvm-nm,llvm-objdump,llvm-readobj,llvm-strip, etc. - Config files:
bin/clang.cfg,bin/clang-20.cfg,bin/clang++.cfg - C++ runtime: bundled libc++/libc++abi/libunwind under
lib/x86_64-unknown-linux-gnu - Compiler runtime: compiler-rt under
lib/clang/20/lib/x86_64-unknown-linux-gnu
clang++.cfg is the key package integration point:
--sysroot=/home/speak/.mcpp/registry/subos/default
-Wl,--dynamic-linker=/home/speak/.mcpp/registry/subos/default/lib/ld-linux-x86-64.so.2
-Wl,--enable-new-dtags,-rpath,/home/speak/.mcpp/registry/subos/default/lib
-Wl,-rpath-link,/home/speak/.mcpp/registry/subos/default/lib
-fuse-ld=lld
--rtlib=compiler-rt
--unwindlib=libunwind
-nostdinc++
-stdlib=libc++
-isystem /home/speak/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7/include/c++/v1
-isystem /home/speak/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7/include/x86_64-unknown-linux-gnu/c++/v1
-L/home/speak/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7/lib/x86_64-unknown-linux-gnu
-Wl,-rpath,/home/speak/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7/lib/x86_64-unknown-linux-gnu
This is good for MCPP: the package already encodes the intended libc++, compiler-rt, lld, sysroot, dynamic linker, and rpath choices. MCPP should not reconstruct all of these manually if the compiler config file is present.
Direct execution currently fails without an additional runtime path:
clang++: error while loading shared libraries: libz.so.1: cannot open shared object file
With host zlib in LD_LIBRARY_PATH, the compiler itself works:
LD_LIBRARY_PATH=/lib/x86_64-linux-gnu \
~/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7/bin/clang++ --versionOutput:
clang version 20.1.7
Target: x86_64-unknown-linux-gnu
Configuration file: .../bin/clang++.cfg
Simple C and C++ builds work with the same launch environment:
LD_LIBRARY_PATH=/lib/x86_64-linux-gnu clang -std=c11 hello.c -o hello
LD_LIBRARY_PATH=/lib/x86_64-linux-gnu clang++ -std=c++23 hello.cpp -o helloThe produced C++ binary may also need libatomic.so.1 at runtime. On this
machine, libatomic.so.1 exists in the host system and in
~/.mcpp/registry/data/xpkgs/xim-x-gcc-runtime/15.1.0/lib64, but not in the
LLVM package rpath or default subos lib path. This means MCPP needs a
toolchain runtime environment concept instead of assuming an absolute compiler
binary is self-sufficient.
The LLVM package includes:
lib/x86_64-unknown-linux-gnu/libc++.modules.json
It declares:
{
"logical-name": "std",
"source-path": "../../share/libc++/v1/std.cppm"
}and:
{
"logical-name": "std.compat",
"source-path": "../../share/libc++/v1/std.compat.cppm"
}However, the referenced files are not present in this installed package:
share/libc++/v1/std.cppm missing
share/libc++/v1/std.compat.cppm missing
As expected, direct import std compilation fails:
fatal error: module 'std' not found
This is an important integration decision:
- MCPP can support Clang for non-
import stdC++ and C sources first. - First-class
import stdsupport requires either the LLVM package to shipstd.cppm/std.compat.cppm, or MCPP must have a separate source provider for libc++ standard module sources. - MCPP should report this as a clear capability failure, not as a generic compiler failure.
Official Clang/libc++ documentation treats standard library modules as compiler/version/configuration-sensitive artifacts. The BMI is not portable across arbitrary compiler or flag changes. Therefore MCPP's fingerprint model must include the Clang resource dir, libc++ module source hash, and relevant module flags.
References:
MCPP resolves the installed binary correctly:
Resolved llvm@20.1.7 -> @mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7/bin/clang++
But then detect() runs the absolute binary directly. Without a launch
environment, clang++ --version fails due libz.so.1.
Implication: a resolved toolchain must carry an execution environment, not only a path.
With LD_LIBRARY_PATH=/lib/x86_64-linux-gnu, detection gets past the loader,
but current detect.cppm can classify Clang as GCC because it searches the
entire --version output for the substring gcc. Clang's version output
contains:
Configuration file: .../bin/clang++.cfg
That suffix includes gcc inside clang++.cfg. The detected label becomes:
gcc 6082 (x86_64-unknown-linux-gnu)
6082 is extracted from the LLVM git commit hash on the first line.
Implication: detection must check Clang before GCC and parse only reliable version fields. It should not substring-match the entire output.
Even when [language].import_std = false, prepare_build() calls
toolchain::ensure_built() unconditionally. That function requires
Toolchain.stdModuleSource, currently populated only for GCC bits/std.cc.
Failure:
toolchain has no std module source (import std unsupported on this compiler)
Implication: standard module preparation must be capability-driven and only
required when the graph imports std or std.compat.
The current Ninja backend assumes:
-fmodules-fdeps-format=p1689r5- BMI suffix
.gcm gcm.cache/<module>.gcm- staged
std.gcmandstd.o - GCC restat behavior around
.gcm
Clang uses a different module dialect:
- BMI suffix
.pcm - explicit module output or precompile flows
-fmodule-file=<name>=<pcm>or prebuilt module pathsclang-scan-depsfor robust dependency scanning- libc++ standard module sources, not GCC
bits/std.cc
- Make LLVM/Clang a first-class MCPP toolchain family.
- Keep GCC and musl-GCC behavior stable.
- Avoid hardcoding one compiler's module model into the generic build plan.
- Preserve xlings isolation: builds should not accidentally depend on system compiler PATH.
- Keep toolchain-specific complexity behind explicit interfaces.
- Support staged delivery: first Clang detection and non-
import stdbuilds, then Clang modules, then Clangimport std.
Normalize user-facing specs before xlings resolution.
struct ToolchainRequest {
std::string family; // "gcc", "llvm", "clang", "musl-gcc", "msvc"
std::string version; // "20.1.7"
std::string libcFlavor; // "", "musl"
std::string xpkgName; // "gcc", "musl-gcc", "llvm"
std::string displaySpec; // "llvm@20.1.7"
};Rules:
llvm@20.1.7->xim:llvm@20.1.7clang@20.1.7->xim:llvm@20.1.7, display asclang/llvmgcc@15.1.0-musl->xim:musl-gcc@15.1.0musl-gcc@15.1.0remains accepted
This removes duplicated parsing in prepare_build, install, default,
remove, and list.
Return more than a compiler path.
struct ToolchainPayload {
std::string displaySpec;
std::string xpkgName;
std::string version;
std::filesystem::path root;
std::filesystem::path binDir;
std::filesystem::path cxx;
std::filesystem::path cc;
std::filesystem::path ar;
std::filesystem::path ranlib;
std::filesystem::path scanner;
std::vector<std::filesystem::path> runtimeLibraryDirs;
std::map<std::string, std::string> env;
};For LLVM:
cxx = bin/clang++cc = bin/clangar = bin/llvm-arranlib = bin/llvm-ranlibscanner = bin/clang-scan-depsif presentruntimeLibraryDirsmust include any dirs needed to launch compiler tools and linked binaries in MCPP's sandbox.
The resolver should inspect package layout and config files, not assume every toolchain has binutils siblings.
Every subprocess that invokes compiler-family tools should go through a launcher helper.
struct ToolInvocation {
std::filesystem::path exe;
std::vector<std::string> args;
std::map<std::string, std::string> env;
std::filesystem::path cwd;
};Why this is needed:
- LLVM's absolute
clang++currently needs a runtime library path forlibz.so.1. - MCPP currently uses raw shell strings in detect, stdmod, scanner, and ninja.
- Ninja build files also need to embed the required environment.
Ninja can express this with rule command prefixes:
rule cxx_object
command = env LD_LIBRARY_PATH=$tool_ld_library_path $cxx $cxxflags -c $in -o $outThe exact env should come from the resolved toolchain profile.
toolchain::detect() should produce a richer profile:
enum class CompilerFamily { GCC, Clang, MSVC };
enum class StdlibFamily { Libstdcxx, Libcxx, MsvcStl, Unknown };
enum class ModuleDialect { GccGcm, ClangPcm, MsvcIfc, None };
struct CompilerProfile {
CompilerFamily family;
std::string version;
std::string targetTriple;
StdlibFamily stdlib;
std::string stdlibVersion;
ModuleDialect moduleDialect;
std::filesystem::path cxx;
std::filesystem::path cc;
std::filesystem::path ar;
std::filesystem::path ranlib;
std::filesystem::path resourceDir;
std::filesystem::path sysroot;
ToolLaunchEnv launchEnv;
StdModuleCapability stdModule;
};Detection rules:
- Detect Clang before GCC.
- Parse
clang version X.Y.Zfrom the first line. - Parse GCC from
g++ (...) X.Y.Zorgcc version X.Y.Z, not arbitrary trailing numbers. - Do not infer
libc++just because family is Clang; inspect config or query include/link search paths when possible.
Replace the single GCC-specific ensure_built() with provider strategies.
class StdModuleProvider {
public:
virtual bool available(const CompilerProfile&) const = 0;
virtual Expected<StdModuleArtifacts, Error>
ensure_built(const CompilerProfile&, std::string_view fp) = 0;
};Providers:
-
GccStdModuleProvider- Source: GCC
bits/std.cc - Output:
std.gcm,std.o - Flags:
-std=c++23 -fmodules
- Source: GCC
-
ClangLibcxxStdModuleProvider- Source:
libc++.modules.json->std.cppm/std.compat.cppm - Output:
std.pcm, optional object if needed by the selected flow - Flags:
-std=c++23 -stdlib=libc++ -fexperimental-librarywhen required - Hard error if
libc++.modules.jsonreferences missing files
- Source:
Standard module building should be conditional:
- If graph imports
stdorstd.compat, require provider availability. - If no unit imports standard modules, skip std module prebuild entirely.
Move compiler-specific BMI naming and flags out of ninja_backend.cppm.
class ModuleDialectRules {
public:
virtual std::string bmi_filename(std::string_view logicalName) const = 0;
virtual std::string cxx_module_flags(const CompileUnit&) const = 0;
virtual std::string scan_rule() const = 0;
virtual std::string compile_rule() const = 0;
};GCC dialect:
- BMI dir:
gcm.cache - BMI suffix:
.gcm - Compile:
-fmodules - Scan:
-fdeps-format=p1689r5 -fdeps-file=...
Clang dialect:
- BMI dir:
pcm.cacheor compiler-neutralbmi.cache - BMI suffix:
.pcm - Compile interface: use
--precompileor-fmodule-output=<pcm> - Compile users: pass
-fmodule-file=<logical>=<pcm>for known imports - Scan: prefer
clang-scan-deps -format=p1689when available
MVP fallback:
- If
clang-scan-depsis unavailable, MCPP can use its existing graph scanner and static dependency edges for project modules. - That fallback is acceptable for initial Clang support, but less precise than compiler-driven scanning.
Move stdlib/runtime link flags out of generic compute_flags().
struct LinkPolicy {
std::vector<std::string> cxxFlags;
std::vector<std::string> cFlags;
std::vector<std::string> ldFlags;
std::filesystem::path linker;
std::filesystem::path ar;
std::filesystem::path ranlib;
};GCC policy:
-static-libstdc++when requested- binutils
arfrom sibling package when available - musl target implies static linkage unless explicitly overridden
LLVM policy:
- Do not add
-static-libstdc++ - Use package config file for libc++/compiler-rt/lld where possible
- Prefer
llvm-arandllvm-ranlib - Preserve or reproduce package rpath behavior for libc++ and subos libs
- Decide separately whether MCPP supports fully static LLVM/libc++ builds
Scope:
- Add data-driven toolchain families:
gcc,musl-gcc,llvm,clang. - Make
clang@<ver>alias tollvm@<ver>. - Include
llvmintoolchain listAvailable output. - Resolve
cc,cxx,ar,ranlibfrom package profile. - Add toolchain launch env and use it in
detect(). - Fix compiler detection order and parsing.
- Stop unconditional std module prebuild.
Acceptance:
mcpp toolchain install llvm 20.1.7mcpp toolchain default llvm@20.1.7mcpp buildfor a project with noimport stdshould reach Ninja.- Clear error if LLVM compiler launch env cannot be formed.
Scope:
- Add Clang flag/link policy.
- Use
clangfor.c,clang++for.cpp/.cppm. - Use
llvm-arfor archives. - Generate Ninja commands with toolchain launch env.
- For projects that do not import
std, skipstd.pcmentirely.
Acceptance:
- Pure C project builds with LLVM.
- Header-based C++ project builds with LLVM.
- Mixed C/C++ project builds with LLVM.
- Static libraries use
llvm-ar. compile_commands.jsonreflects Clang commands.
Scope:
- Add Clang PCM BMI layout.
- Emit module interface build edges producing
.pcmplus objects. - Pass explicit
-fmodule-file=<name>=<path>for imports. - Use existing MCPP graph scanner as initial dependency source.
- Add
clang-scan-depssupport once the tool is available in xlings.
Acceptance:
- Multi-module project without
import stdbuilds with LLVM. - Incremental rebuild works when implementation changes.
- GCC behavior remains unchanged.
Scope:
- Implement
ClangLibcxxStdModuleProvider. - Read
libc++.modules.json. - Validate source paths.
- Prebuild
std.pcmandstd.compat.pcm. - Add std module artifacts to fingerprint inputs.
Dependency:
- The LLVM xpkg must ship
share/libc++/v1/std.cppmandshare/libc++/v1/std.compat.cppm, or MCPP must define another trusted source for them.
Acceptance:
import std; int main(){ std::println("ok"); }builds with LLVM.- Missing libc++ module sources produce a targeted diagnostic.
Scope:
- Teach
mcpp packthat libc++/libc++abi/libunwind are not manylinux system libraries. - Bundle libc++ runtime in
bundle-project, unless explicitly skipped. - Ensure produced binaries do not depend on undeclared host paths.
- Revisit
libatomicandlibzhandling.
Acceptance:
- LLVM-built dynamic package runs after extraction on a clean target host.
bundle-allincludes the correct loader/runtime libraries.
Likely files to touch:
src/cli.cppm- Toolchain spec parsing, install/list/default/remove, prepare_build.
src/toolchain/detect.cppm- Robust family detection, profile fields, launch env.
src/toolchain/stdmod.cppm- Split into provider strategies; make prebuild conditional.
src/build/flags.cppm- Replace GCC-only flag assembly with profile/link policy.
src/build/ninja_backend.cppm- Move module-specific rules into dialect emitters.
src/build/compile_commands.cppm- Preserve Clang command/env assumptions for IDEs.
src/modgraph/p1689.cppm- Rename GCC-specific implementation or add scanner strategy.
src/pack/pack.cppm- Runtime bundling policy for libc++ and LLVM-built binaries.
tests/e2e- Add LLVM install/detect/build tests; gate large downloads where needed.
Unit tests:
- Parse
llvm@20.1.7,clang@20.1.7,gcc@15.1.0-musl. - Detect Clang from version output containing
clang++.cfg. - Verify Clang does not emit
-static-libstdc++. - Verify LLVM archive tool is
llvm-ar. - Verify std module prebuild is skipped when no source imports
std. - Verify missing libc++
std.cppmyields a specific diagnostic.
E2E tests:
mcpp toolchain install llvm 20.1.7in isolatedMCPP_HOME.mcpp toolchain listshows installed LLVM and available LLVM versions.- Pure C project builds with LLVM.
- Header-based C++ project builds with LLVM.
- Mixed C/C++ project builds with LLVM.
- Modular project without
import stdbuilds with LLVM. import stdtest is skipped or expected-fails until std.cppm is available.mcpp packbundle-project includes libc++ runtime for LLVM-built binaries.
These should be resolved with the xlings package owner before implementing Phase 4 and Phase 5:
- Should
llvmdeclare or bundle a provider forlibz.so.1so direct absolutebin/clang++execution works inside MCPP without hostLD_LIBRARY_PATH? - Should
llvmincludeshare/libc++/v1/std.cppmandshare/libc++/v1/std.compat.cppm, sincelibc++.modules.jsonreferences them? - Should
llvmorllvm-toolsincludeclang-scan-deps? It is the natural tool for Clang module dependency scanning. - Should LLVM-built binaries depend on
libatomic.so.1, or should compiler-rt and package rpaths avoid that dependency?
Start with Phase 1 and Phase 2. They establish the architecture and make LLVM
usable for non-import std projects without committing to the full Clang module
pipeline immediately.
Do not wire Clang into the current GCC gcm.cache path. That would create a
fragile hybrid that fails later around BMI format, std module sources,
fingerprints, and incremental rebuilds.
The central design rule is:
BuildPlan stays compiler-neutral.
CompilerProfile + ModuleDialect + StdModuleProvider + LinkPolicy own the
compiler-specific behavior.
This PR implements the first practical slice of the design:
llvm@20.1.7andclang@20.1.7resolve to the xlingsllvmpackage.- Clang detection checks Clang before GCC and only classifies GCC from the
version banner, avoiding false positives from
gcc-runtimepaths inclang++.cfg. - Toolchain metadata now carries compiler launch library paths and output link runtime paths.
- Ninja rules execute compiler, linker, and archive commands through the toolchain launch environment when needed.
- Clang builds use
clang,clang++,llvm-ar, no GCC P1689 dyndep scan, and no-static-libstdc++. import stdprebuild is conditional on the source graph actually importingstdorstd.compat.- If source imports
stdwith LLVM today, MCPP reports that the toolchain has no std module source. This remains blocked on the xlings LLVM package shipping libc++std.cppm/std.compat.cppm.
Validated behavior in this implementation:
- Header-based C++ and mixed C/C++ projects build and run with
llvm@20.1.7. clang@20.1.7works as an alias for the same package.- Existing GCC/musl-GCC unit and e2e coverage remains intact, except that the pack e2e now pins glibc GCC explicitly so it is independent of the user's global default toolchain.