musl host support: bootstrap and run on Alpine (x86_64-alpine-linux-musl)
All checks were successful
CI / build-test-release (push) Successful in 7m43s
CI / release-musl (push) Successful in 8m56s

The Linux host code paths were gated on the x86_64_pc_linux_gnu target
define alone, and LoadProject hardcoded --target=x86_64-pc-linux-gnu when
compiling project.cpp and the module PCMs. Both break on musl hosts: the
glibc release launcher cannot run there at all (no glibc loader; gcompat
lacks libgcc_s and the __isoc23_* symbols), and a musl-built launcher
would still compile project.cpp for the wrong libc.

- The host gates test clang's __linux__ instead of one triple's define:
  they mean "Linux host", whatever the libc (or, later, the CPU).
- LoadProject derives the host triple from HostTarget() (clang's
  -print-target-triple), which is also more correct on glibc distros
  whose triple isn't x86_64-pc-linux-gnu.
- project.cpp / the cmake libc++ flags treat any *-linux-gnu or
  *-linux-musl target as Linux.
- build.sh targets the host toolchain's triple (CRAFTER_BUILD_TARGET
  overrides) and derives the per-target define from it. On Arch this is
  byte-for-byte the previous behaviour.
- CI: a release-musl job bootstraps on alpine:edge, runs the tests, and
  attaches crafter-build-linux-x86_64-musl-v2.tar.gz to the rolling
  latest release (via the API, so the glibc assets survive). Dynamic
  against musl libc++: the launcher dlopens project.so, so it cannot be
  fully static.

First consumer: imsd's package CI, which cross-compiles for aarch64 from
an Alpine container.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-09-01 23:49:00 +02:00
commit dc239f4257
7 changed files with 131 additions and 22 deletions

View file

@ -8,7 +8,7 @@ module;
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
#ifdef __linux__
#include <unistd.h>
#include <dlfcn.h>
#include <sys/wait.h>
@ -41,7 +41,7 @@ namespace {
// descriptor is close-on-exec / non-inheritable so spawned clang processes
// don't keep the lock alive past our own release.
class CacheLock {
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
#ifdef __linux__
std::int32_t fd_ = -1;
public:
explicit CacheLock(const fs::path& cacheDir) {
@ -765,7 +765,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
}
#endif
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
#ifdef __linux__
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer;
@ -796,7 +796,9 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
std::array<char, 128> buffer;
CommandResult result{};
std::string wrapped = std::format("timeout --kill-after=2 {} {} 2>&1", timeout.count(), cmd);
// -k (not --kill-after=): the short form is what GNU coreutils and busybox
// (Alpine) have in common.
std::string wrapped = std::format("timeout -k 2 {} {} 2>&1", timeout.count(), cmd);
FILE* pipe = popen(wrapped.c_str(), "r");
if (!pipe) throw std::runtime_error("popen() failed!");
@ -968,12 +970,12 @@ namespace {
continue;
}
std::string cmd = std::format(
"clang++ --target=x86_64-pc-linux-gnu -march=native -mtune=native "
"clang++ --target={} -march=native -mtune=native "
"-std=c++26 -stdlib=libc++ -O3 "
"-Wno-reserved-identifier -Wno-reserved-module-identifier "
"-fprebuilt-module-path={} "
"--precompile {} -o {}",
cacheDir.string(), cppmPath.string(), pcmPath.string());
HostTarget(), cacheDir.string(), cppmPath.string(), pcmPath.string());
CommandResult r = Crafter::RunCommandChecked(cmd);
if (r.exitCode != 0) {
throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
@ -998,8 +1000,11 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
fs::path sourceDir = Crafter::GetCrafterBuildHome();
// The host toolchain's own triple (x86_64-pc-linux-gnu on glibc distros,
// x86_64-alpine-linux-musl on Alpine): project.cpp and the module PCMs
// it imports must be built for the libc crafter-build itself runs on.
Configuration hostConfig;
hostConfig.target = "x86_64-pc-linux-gnu";
hostConfig.target = HostTarget();
hostConfig.march = "native";
hostConfig.mtune = "native";
fs::path cacheDir = GetCacheDir() / std::format("{}-{}", hostConfig.target, hostConfig.march);
@ -1015,13 +1020,15 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
const bool stale = !fs::exists(soPath) || fs::last_write_time(soPath) < fs::last_write_time(absProject) || fs::last_write_time(soPath) < fs::last_write_time(hostExe);
if (stale) {
// -fuse-ld=lld like every other link the tool performs: lld is a hard
// dependency, binutils' ld is not (Alpine's clang would default to it).
std::string compileCmd = std::format(
"clang++ --target=x86_64-pc-linux-gnu -march=native -mtune=native "
"-std=c++26 -stdlib=libc++ -shared -fPIC -O3 "
"clang++ --target={} -march=native -mtune=native "
"-std=c++26 -stdlib=libc++ -shared -fPIC -O3 -fuse-ld=lld "
"-Wno-return-type-c-linkage "
"-fprebuilt-module-path={} "
"{} -o {}",
cacheDir.string(),
hostConfig.target, cacheDir.string(),
absProject.string(), soPath.string());
std::string result = RunBuildCommand(compileCmd);