commit 48032775acf3e84f4a03a41faf81bb22a9521b5e
parent 5e7ee1b17089ced19dfd1609b8ba281452efa9b0
Author: cowmonk <cowmonk@based.pt>
Date: Sat, 10 Oct 2026 15:27:46 -0700
Add musl cross-toolchain builds
Add target runtimes and wrappers for x86_64 and aarch64.
Pin source checksums and add resumable stages, offline tests and CI.
Diffstat:
22 files changed, 1323 insertions(+), 229 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
@@ -0,0 +1,80 @@
+name: Toolchain checks
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+ inputs:
+ source_llvm:
+ description: Also build LLVM host tools from source
+ type: boolean
+ default: false
+
+permissions:
+ contents: read
+
+jobs:
+ driver:
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+ - run: make check
+
+ cross:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 90
+ strategy:
+ fail-fast: false
+ matrix:
+ arch: [x86_64, aarch64]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install host dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y clang-19 lld-19 llvm-19 llvm-19-dev cmake ninja-build curl xz-utils perl rsync pkg-config qemu-user
+ echo /usr/lib/llvm-19/bin >> "$GITHUB_PATH"
+ - uses: actions/cache@v4
+ with:
+ path: sources
+ key: sources-${{ hashFiles('sources.list') }}
+ - name: Build and run smoke tests
+ env:
+ ARCH: ${{ matrix.arch }}
+ run: |
+ if [ "$ARCH" = aarch64 ]; then
+ make JOBS=2 RUNNER=qemu-aarch64
+ else
+ make JOBS=2
+ fi
+ - name: Check a resumed build
+ env:
+ ARCH: ${{ matrix.arch }}
+ run: make runtimes JOBS=2
+ - name: Save build diagnostics
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: diagnostics-${{ matrix.arch }}
+ path: |
+ build/work/**/CMakeCache.txt
+ build/work/**/CMakeFiles/CMakeConfigureLog.yaml
+ build/work/**/verify/*.header
+ build/work/**/verify/*.segments
+ build/work/**/verify/*.dynamic
+
+ source:
+ if: github.event_name == 'workflow_dispatch' && inputs.source_llvm
+ runs-on: ubuntu-24.04
+ timeout-minutes: 180
+ steps:
+ - uses: actions/checkout@v4
+ - run: |
+ sudo apt-get update
+ sudo apt-get install -y clang-19 lld-19 llvm-19 llvm-19-dev cmake ninja-build curl xz-utils perl rsync
+ echo /usr/lib/llvm-19/bin >> "$GITHUB_PATH"
+ - uses: actions/cache@v4
+ with:
+ path: sources
+ key: sources-${{ hashFiles('sources.list') }}
+ - run: make LLVM_MODE=source JOBS=2 LINK_JOBS=1
diff --git a/Makefile b/Makefile
@@ -1,46 +1,49 @@
include config.mk
+export ARCH TARGET PREFIX BUILD_DIR SOURCES_DIR SOURCES_FILE LLVM_MODE JOBS LINK_JOBS
+export HOST_CC HOST_CXX TARGET_CFLAGS HOST_CFLAGS HOST_CXXFLAGS HOST_LDFLAGS RUNNER
+
all: build
+build:
+ sh scripts/build.sh all
+
pull:
- @if [ ! -d ${PROOT}/sources ]; then \
- echo "Sources Directory Not Found!"; \
- mkdir -p ${PROOT}/sources; \
- echo "Pulling Sources..."; \
- xargs -a ${SOURCES_FILE} -n1 -P4 -- curl -f -L -S -O --output-dir ${PROOT}/sources || { echo "curl failed"; exit 1; }; \
- echo "Pulling cross musl compiler to build toolchain"
- curl -f -L -O https://musl.cc/${ARCH}-linux-musl-cross.tgz --output-dir ${PROOT}/sources
- echo "done"; \
- else \
- . ${PROOT}/scripts/check.sh; \
- fi
-
-build: init
- . ${PROOT}/scripts/musl-cross.sh
- . ${PROOT}/scripts/elftoolchain-cross.sh
-
-init:
- @if [ ! -d ./build/${FROOT} ]; then \
- echo "Making toolchain directory..."; \
- mkdir -p ${PROOT}/build/${FROOT}; \
- fi
- @echo "updating configuration based on config.mk..."
- cp -r config.mk ${PROOT}/scripts/config.sh
- sed -i 's/shell //g' ${PROOT}/scripts/config.sh
+ sh scripts/check.sh pull
+
+verify-sources:
+ sh scripts/check.sh verify
+
+doctor:
+ sh scripts/build.sh doctor
+
+show-config:
+ sh scripts/build.sh config
+
+headers builtins musl atomic runtimes toolchain configure-llvm:
+ sh scripts/build.sh $@
+
+verify:
+ sh scripts/verify.sh
+
+check:
+ sh scripts/test.sh
clean:
- rm -rf ${PROOT}/sources
- rm -rf ${PROOT}/build
+ sh scripts/build.sh clean
help:
- @echo "musllvm"
- @echo "-----------------------"
- @echo "commands:"
- @echo " all - make all (duh)"
- @echo " pull - pull all sources (on first run), second run will check individually"
- @echo " build - build will build to toolchain"
- @echo " clean - clean up all sources and build artifacts"
- @echo " init - sets up everything, run also every time you make changes to config.mk"
- @echo " help - shows this menu"
-
-.PHONY: all pull build clean init help
+ @printf '%s\n' \
+ 'make [ARCH=x86_64|aarch64] [LLVM_MODE=system|source] [JOBS=2]' \
+ ' build build and verify the complete cross toolchain (default)' \
+ ' doctor check host dependencies' \
+ ' show-config print paths and target configuration' \
+ ' pull download and verify pinned source archives' \
+ ' verify-sources check downloaded archives without network access' \
+ ' verify compile, inspect, and optionally run C and C++ programs' \
+ ' check run offline build-driver tests' \
+ ' clean remove the selected toolchain and its work directory' \
+ ' configure-llvm configure the optional LLVM source build'
+
+.PHONY: all build pull verify-sources doctor show-config headers builtins musl
+.PHONY: atomic runtimes toolchain configure-llvm verify check clean help
diff --git a/README.md b/README.md
@@ -1,18 +1,156 @@
-# MUSLLVM
-
-A simple make build system to make a pure musl clang/llvm toolchain.
-Targeting x86_64.
-
-# Dependencies
-Here are some dependencies required to build this toolchain:
-- a C compiler (preferably clang)
-- curl
-- make
-- bmake
-- POSIX Unix utilities (GNU utilities are acceptable)
-
-# Credits
-* [Chimera linux](https://chimera-linux.org) - Majority of patches are sourced from chimera linux.
-* [CMLFS](https://github.com/dslm4515/CMLFS) - Many of the build scripts were taken as reference for the musllvm toolchain itself
-* [Mussel](https://github.com/firasuke/mussel) - Best, shortest, fastest script that builds musl cross-compiler
-* [musl.cc](https://musl.cc) - Prebuilt cross compilers and other stuff
+musllvm
+========
+
+A simple make build system for a musl Clang/LLVM cross toolchain.
+Targets x86_64 and aarch64 Linux.
+
+The toolchain includes musl, compiler-rt, libc++, libc++abi,
+LLVM libunwind and libatomic. Compiler tools run on the build host.
+
+
+dependencies
+============
+
+Build on a Linux host with:
+
+ clang/llvm Clang 19 or newer, clang++, LLD, LLVM utilities and
+ matching llvm-config and LLVM CMake metadata (system mode)
+ make GNU make
+ cmake 3.20 or newer
+ ninja
+ python3
+ perl
+ curl
+ tar gzip and xz support
+ sha256sum
+ rsync
+
+pkg-config is needed for the target pkg-config wrapper. qemu-aarch64 is
+optional for running aarch64 checks on an x86_64 host.
+
+
+building
+========
+
+ make doctor
+ make
+
+Defaults to x86_64 with two build jobs. For aarch64:
+
+ make ARCH=aarch64
+
+LLVM_MODE=system uses the installed host tools and builds the target
+libraries from pinned sources. Keep the host LLVM installation available.
+
+To also build Clang, LLD and the LLVM utilities from source:
+
+ make LLVM_MODE=source ARCH=x86_64 JOBS=2 LINK_JOBS=1 \
+ PREFIX="$PWD/build/x86_64-source" BUILD_DIR="$PWD/build/work/x86_64-source"
+
+Source mode installs host tools under PREFIX/host. These tools use the
+host libc. Both modes produce a cross toolchain.
+
+
+configuration
+=============
+
+Edit [config.mk](config.mk) or override settings through make.
+
+ ARCH x86_64 x86_64 or aarch64
+ TARGET $(ARCH)-linux-musl target triple
+ PREFIX build/$(TARGET) toolchain installation
+ BUILD_DIR build/work/$(TARGET) build files and completed stages
+ LLVM_MODE system system or source
+ JOBS 2 compilation jobs
+ LINK_JOBS 1 parallel links when building LLVM
+ TARGET_CFLAGS -O2 target library flags
+ RUNNER emulator executable (optional)
+
+Each target has its own prefix and work directory. Downloaded and
+extracted sources are shared. Build paths must have no whitespace or
+shell metacharacters.
+
+
+usage
+=====
+
+After building, load the toolchain into your current shell:
+
+ eval "$(build/x86_64-linux-musl/bin/x86_64-linux-musl-env)"
+ "$CC" hello.c -o hello
+ "$CC" -static hello.c -o hello-static
+ "$CXX" -std=c++20 hello.cpp -o hello-cxx
+
+The environment command sets CC, CXX, LLVM binutils, LD, PKG_CONFIG,
+TARGET, MUSLLVM_SYSROOT and CMAKE_TOOLCHAIN_FILE. Use the environment
+command matching your target and installation path.
+
+The compiler wrappers select the target, sysroot, LLD and target runtimes.
+They remove host header and library environment paths. The pkg-config
+wrapper searches the target sysroot.
+
+For CMake:
+
+ cmake -S . -B out -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE="$CMAKE_TOOLCHAIN_FILE"
+ cmake --build out
+
+The sysroot is under PREFIX/sysroot, with headers in usr/include,
+libraries in usr/lib and the musl loader in lib. Compiler-rt is in the
+toolchain's Clang resource directory.
+
+Set MUSLLVM_SYSROOT to use another copy of the sysroot. Set
+MUSLLVM_PKG_CONFIG_PATH to add target pkg-config directories.
+
+
+sources and resuming
+====================
+
+[sources.list](sources.list) pins archive names, URLs and SHA-256 digests.
+Downloads enter the cache after checksum verification.
+
+ make pull
+ make verify-sources
+ make show-config
+
+Run the same command after a failed build to resume completed stages.
+Changes to the target, source pins, compiler, paths, build recipes or
+library flags require make clean with the same ARCH, PREFIX and BUILD_DIR.
+Cleaning removes that toolchain and its work directory, but keeps the
+downloaded and extracted sources. Changing JOBS does not require cleaning.
+
+A running build holds a lock in BUILD_DIR/lock. After a forced termination,
+check that the build process has stopped before removing a leftover lock
+with rmdir.
+
+The older Chimera patches in patches/ are kept as references. They are
+not applied to the pinned LLVM release.
+
+
+checks
+======
+
+ make check
+
+Runs shell syntax checks and driver tests with mocked tools. This does
+no compilation or downloads.
+
+A completed build also runs make verify. This compiles static and dynamic
+C and C++ programs, checks their ELF architecture and runtime dependencies,
+and tests the CMake toolchain file. Programs run when the host architecture
+matches the target or RUNNER names an emulator.
+
+ make verify ARCH=aarch64 RUNNER=qemu-aarch64
+
+[CI](.github/workflows/ci.yml) runs offline tests and both target builds.
+A manual workflow run can also build LLVM from source. Full toolchain
+build verification is still pending.
+
+
+credits
+========
+
+* [Chimera Linux](https://chimera-linux.org) - original patches and libatomic
+* [CMLFS](https://github.com/dslm4515/CMLFS) - build script references
+* [Mussel](https://github.com/firasuke/mussel) - musl cross compiler references
+* [musl.cc](https://musl.cc) - cross compiler resources
diff --git a/config.mk b/config.mk
@@ -1,16 +1,17 @@
-# musllvm default flags
-# --------------------------------
-# feel free to change these however you like
-#
+ARCH ?= x86_64
+TARGET ?= $(ARCH)-linux-musl
+PREFIX ?= $(CURDIR)/build/$(TARGET)
+BUILD_DIR ?= $(CURDIR)/build/work/$(TARGET)
+SOURCES_DIR ?= $(CURDIR)/sources
+SOURCES_FILE ?= $(CURDIR)/sources.list
-#
-# these are shared between the Makefile and scripts
-#
-ARCH=$(shell uname -m)
-PROOT=${PWD}
-SOURCES_FILE=${PROOT}/sources.list
-FROOT="${ARCH}-musllvm"
-CROSS="${PROOT}/build/cross" # cross directory
-
-# MAKEFLAGS to parse the amounto of cores used
-MAKEFLAGS="-j$(shell nproc) -l$(shell nproc)"
+LLVM_MODE ?= system
+JOBS ?= 2
+LINK_JOBS ?= 1
+HOST_CC ?= clang
+HOST_CXX ?= clang++
+TARGET_CFLAGS ?= -O2
+HOST_CFLAGS ?=
+HOST_CXXFLAGS ?=
+HOST_LDFLAGS ?=
+RUNNER ?=
diff --git a/files/toolchain/cc.sh b/files/toolchain/cc.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -eu
+
+prefix=$(CDPATH= cd "$(dirname "$0")/.." && pwd -P)
+sysroot=${MUSLLVM_SYSROOT:-$prefix/sysroot}
+unset CPATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH LIBRARY_PATH
+unset GCC_EXEC_PREFIX COMPILER_PATH
+link=yes
+for argument do
+ case "$argument" in
+ -c|-S|-E|-M|-MM|-fsyntax-only|-nostdlib|-nodefaultlibs|-r) link=no ;;
+ esac
+done
+[ "$link" = no ] || set -- "$@" -latomic
+case "${0##*/}" in
+*++|*-c++)
+ set -- --driver-mode=g++ -stdlib=libc++ -nostdinc++ -isystem "$sysroot/usr/include/c++/v1" "$@" ;;
+esac
+exec "$prefix/libexec/clang" --target=@TARGET@ --sysroot="$sysroot" \
+ --gcc-toolchain="$prefix/share/musllvm/empty-gcc" \
+ -resource-dir="$prefix/lib/clang/@RESOURCE_VERSION@" \
+ --rtlib=compiler-rt --unwindlib=libunwind --ld-path="$prefix/libexec/ld.lld" "$@"
diff --git a/files/toolchain/env.sh b/files/toolchain/env.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+set -eu
+prefix=$(CDPATH= cd "$(dirname "$0")/.." && pwd -P)
+
+quote()
+{
+ printf "'"
+ printf '%s' "$1" | sed "s/'/'\\\\''/g"
+ printf "'"
+}
+
+emit()
+{
+ printf 'export %s=' "$1"
+ quote "$2"
+ printf '\n'
+}
+
+emit MUSLLVM_HOME "$prefix"
+emit MUSLLVM_SYSROOT "$prefix/sysroot"
+emit TARGET @TARGET@
+emit PATH "$prefix/bin:$PATH"
+for pair in CC:cc CXX:c++ AR:ar RANLIB:ranlib NM:nm STRIP:strip \
+ OBJCOPY:objcopy OBJDUMP:objdump READELF:readelf LD:ld PKG_CONFIG:pkg-config; do
+ emit "${pair%%:*}" "$prefix/bin/@TARGET@-${pair#*:}"
+done
+emit CMAKE_TOOLCHAIN_FILE "$prefix/share/musllvm/toolchain.cmake"
diff --git a/files/toolchain/ld.sh b/files/toolchain/ld.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+set -eu
+prefix=$(CDPATH= cd "$(dirname "$0")/.." && pwd -P)
+exec "$prefix/libexec/ld.lld" --sysroot="${MUSLLVM_SYSROOT:-$prefix/sysroot}" "$@"
diff --git a/files/toolchain/pkg-config.sh b/files/toolchain/pkg-config.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+prefix=$(CDPATH= cd "$(dirname "$0")/.." && pwd -P)
+sysroot=${MUSLLVM_SYSROOT:-$prefix/sysroot}
+PKG_CONFIG_SYSROOT_DIR=$sysroot
+PKG_CONFIG_LIBDIR="$sysroot/usr/lib/pkgconfig:$sysroot/usr/share/pkgconfig"
+PKG_CONFIG_PATH=${MUSLLVM_PKG_CONFIG_PATH:-}
+export PKG_CONFIG_SYSROOT_DIR PKG_CONFIG_LIBDIR PKG_CONFIG_PATH
+exec pkg-config "$@"
diff --git a/files/toolchain/toolchain.cmake b/files/toolchain/toolchain.cmake
@@ -0,0 +1,18 @@
+set(CMAKE_SYSTEM_NAME Linux)
+set(CMAKE_SYSTEM_PROCESSOR @ARCH@)
+get_filename_component(MUSLLVM_HOME "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
+set(CMAKE_SYSROOT "${MUSLLVM_HOME}/sysroot")
+if(DEFINED ENV{MUSLLVM_SYSROOT} AND NOT "$ENV{MUSLLVM_SYSROOT}" STREQUAL "")
+ set(CMAKE_SYSROOT "$ENV{MUSLLVM_SYSROOT}")
+endif()
+set(CMAKE_C_COMPILER "${MUSLLVM_HOME}/bin/@TARGET@-cc")
+set(CMAKE_CXX_COMPILER "${MUSLLVM_HOME}/bin/@TARGET@-c++")
+set(CMAKE_ASM_COMPILER "${CMAKE_C_COMPILER}")
+set(CMAKE_AR "${MUSLLVM_HOME}/bin/@TARGET@-ar")
+set(CMAKE_RANLIB "${MUSLLVM_HOME}/bin/@TARGET@-ranlib")
+set(CMAKE_STRIP "${MUSLLVM_HOME}/bin/@TARGET@-strip")
+set(CMAKE_FIND_ROOT_PATH "${CMAKE_SYSROOT}")
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
diff --git a/scripts/build.sh b/scripts/build.sh
@@ -0,0 +1,311 @@
+#!/bin/sh
+set -eu
+
+. "$(dirname "$0")/config.sh"
+
+TOOLS='clang clang++ ld.lld llvm-ar llvm-ranlib llvm-nm llvm-objcopy llvm-objdump llvm-readelf llvm-strip llvm-size llvm-strings llvm-addr2line llvm-cxxfilt'
+
+doctor()
+{
+ for program in make cmake ninja python3 perl tar xz curl sha256sum rsync; do
+ need "$program"
+ done
+ need "$HOST_CC"
+ need "$HOST_CXX"
+ if [ "$LLVM_MODE" = system ]; then
+ "$HOST_CC" --version | head -n 1 | awk '/clang/ { found=1 } END { exit !found }' ||
+ die "LLVM_MODE=system requires Clang; set HOST_CC=clang"
+ clang_major=$("$HOST_CC" -dumpversion)
+ clang_major=${clang_major%%.*}
+ [ "$clang_major" -ge 19 ] 2>/dev/null || die "LLVM_MODE=system requires Clang 19 or newer"
+ for program in $TOOLS llvm-config; do need "$program"; done
+ llvm_cmake=$(llvm-config --cmakedir)
+ [ -f "$llvm_cmake/LLVMConfig.cmake" ] || die "LLVM CMake metadata missing; install the matching LLVM development package"
+ fi
+ printf 'Host dependencies OK (%s LLVM, %s target, %s jobs)\n' "$LLVM_MODE" "$TARGET" "$JOBS"
+}
+
+configure_llvm()
+{
+ cmake -S "$LLVM_SOURCE/llvm" -B "$BUILD_DIR/llvm" -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$PREFIX/host" \
+ -DCMAKE_C_COMPILER="$BOOTSTRAP_CC" -DCMAKE_CXX_COMPILER="$BOOTSTRAP_CXX" \
+ -DCMAKE_C_FLAGS="$HOST_CFLAGS" -DCMAKE_CXX_FLAGS="$HOST_CXXFLAGS" \
+ -DCMAKE_EXE_LINKER_FLAGS="$HOST_LDFLAGS" \
+ -DLLVM_ENABLE_PROJECTS='clang;lld' -DLLVM_TARGETS_TO_BUILD='X86;AArch64' \
+ -DLLVM_DISTRIBUTION_COMPONENTS='clang;clang-resource-headers;lld;llvm-ar;llvm-ranlib;llvm-nm;llvm-objcopy;llvm-objdump;llvm-readelf;llvm-strip;llvm-size;llvm-strings;llvm-addr2line;llvm-cxxfilt' \
+ -DLLVM_PARALLEL_LINK_JOBS="$LINK_JOBS" -DLLVM_ENABLE_ASSERTIONS=OFF \
+ -DLLVM_INCLUDE_TESTS=OFF -DLLVM_INCLUDE_BENCHMARKS=OFF \
+ -DLLVM_INCLUDE_EXAMPLES=OFF -DLLVM_ENABLE_BINDINGS=OFF \
+ -DLLVM_ENABLE_ZLIB=OFF -DLLVM_ENABLE_ZSTD=OFF -DLLVM_ENABLE_LIBXML2=OFF \
+ -DLLVM_ENABLE_LIBEDIT=OFF -DCLANG_ENABLE_ARCMT=OFF -DCLANG_ENABLE_STATIC_ANALYZER=OFF
+}
+
+done_stage()
+{
+ [ -f "$BUILD_DIR/stamps/$1" ]
+}
+
+mark_stage()
+{
+ touch "$BUILD_DIR/stamps/$1"
+}
+
+toolchain()
+{
+ if [ "$LLVM_MODE" = source ]; then
+ if ! done_stage llvm; then
+ configure_llvm
+ cmake --build "$BUILD_DIR/llvm" --parallel "$JOBS" --target distribution
+ cmake --build "$BUILD_DIR/llvm" --target install-distribution
+ mark_stage llvm
+ fi
+ HOST_CLANG="$PREFIX/host/bin/clang"
+ HOST_CLANGXX="$PREFIX/host/bin/clang++"
+ HOST_LLD="$PREFIX/host/bin/ld.lld"
+ LLVM_CMAKE_DIR="$BUILD_DIR/llvm/lib/cmake/llvm"
+ else
+ HOST_CLANG=$BOOTSTRAP_CC
+ HOST_CLANGXX=$BOOTSTRAP_CXX
+ HOST_LLD=$(command -v ld.lld)
+ LLVM_CMAKE_DIR=$(llvm-config --cmakedir)
+ fi
+ HOST_RESOURCE=$("$HOST_CLANG" -print-resource-dir)
+ RESOURCE_VERSION=${HOST_RESOURCE##*/}
+ case "$RESOURCE_VERSION" in
+ *[!0-9.]*|'') die "unexpected Clang resource directory: $HOST_RESOURCE" ;;
+ esac
+ RESOURCE_DIR="$PREFIX/lib/clang/$RESOURCE_VERSION"
+ mkdir -p "$PREFIX/libexec" "$RESOURCE_DIR/lib/linux" "$SYSROOT/usr/lib" "$BUILD_DIR/empty-gcc"
+ for program in $TOOLS; do
+ if [ "$LLVM_MODE" = source ]; then
+ tool_path="../host/bin/$program"
+ else
+ case "$program" in
+ clang) tool_path=$HOST_CLANG ;;
+ clang++) tool_path=$HOST_CLANGXX ;;
+ *) tool_path=$(command -v "$program") ;;
+ esac
+ fi
+ ln -sf "$tool_path" "$PREFIX/libexec/$program"
+ done
+ if ! done_stage toolchain; then
+ cp -R "$HOST_RESOURCE/include" "$RESOURCE_DIR/"
+ mark_stage toolchain
+ fi
+}
+
+configure_musl()
+{
+ mkdir -p "$BUILD_DIR/musl"
+ cat > "$BUILD_DIR/musl-cc" <<EOF
+#!/bin/sh
+exec "$HOST_CLANG" --target="$TARGET" --sysroot="$SYSROOT" \\
+ --gcc-toolchain="$BUILD_DIR/empty-gcc" -resource-dir="$RESOURCE_DIR" \\
+ --ld-path="$HOST_LLD" --rtlib=compiler-rt --unwindlib=none "\$@"
+EOF
+ chmod 755 "$BUILD_DIR/musl-cc"
+ (
+ cd "$BUILD_DIR/musl"
+ CC="$BUILD_DIR/musl-cc" \
+ AR="$PREFIX/libexec/llvm-ar" RANLIB="$PREFIX/libexec/llvm-ranlib" \
+ CFLAGS="$TARGET_CFLAGS" LIBCC="$RESOURCE_DIR/lib/linux/libclang_rt.builtins-$ARCH.a" \
+ "$MUSL_SOURCE/configure" --target="$TARGET" --prefix=/usr --syslibdir=/lib \
+ --disable-wrapper
+ )
+}
+
+headers()
+{
+ if ! done_stage headers; then
+ mkdir -p "$SYSROOT/usr/include" "$BUILD_DIR/linux"
+ make -C "$LINUX_SOURCE" O="$BUILD_DIR/linux" ARCH="$KERNEL_ARCH" \
+ HOSTCC="$BOOTSTRAP_CC" INSTALL_HDR_PATH="$SYSROOT/usr" headers_install
+ configure_musl
+ make -C "$BUILD_DIR/musl" -j "$JOBS" DESTDIR="$SYSROOT" install-headers
+ mark_stage headers
+ fi
+}
+
+target_cmake()
+{
+ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR="$ARCH" \
+ -DCMAKE_C_COMPILER="$HOST_CLANG" -DCMAKE_CXX_COMPILER="$HOST_CLANGXX" \
+ -DCMAKE_ASM_COMPILER="$HOST_CLANG" -DCMAKE_C_COMPILER_TARGET="$TARGET" \
+ -DCMAKE_CXX_COMPILER_TARGET="$TARGET" -DCMAKE_ASM_COMPILER_TARGET="$TARGET" \
+ -DCMAKE_SYSROOT="$SYSROOT" -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \
+ -DCMAKE_C_FLAGS="$TARGET_CFLAGS --gcc-toolchain='$BUILD_DIR/empty-gcc' -resource-dir='$RESOURCE_DIR'" \
+ -DCMAKE_CXX_FLAGS="$TARGET_CFLAGS --gcc-toolchain='$BUILD_DIR/empty-gcc' -resource-dir='$RESOURCE_DIR'" \
+ -DCMAKE_ASM_FLAGS="--gcc-toolchain='$BUILD_DIR/empty-gcc' -resource-dir='$RESOURCE_DIR'" \
+ -DCMAKE_EXE_LINKER_FLAGS="--ld-path='$HOST_LLD' --rtlib=compiler-rt --unwindlib=none -resource-dir='$RESOURCE_DIR'" \
+ -DCMAKE_SHARED_LINKER_FLAGS="--ld-path='$HOST_LLD' --rtlib=compiler-rt --unwindlib=none -resource-dir='$RESOURCE_DIR'" \
+ -DCMAKE_MODULE_LINKER_FLAGS="--ld-path='$HOST_LLD' --rtlib=compiler-rt --unwindlib=none -resource-dir='$RESOURCE_DIR'" \
+ -DCMAKE_AR="$PREFIX/libexec/llvm-ar" -DCMAKE_RANLIB="$PREFIX/libexec/llvm-ranlib" \
+ -DCMAKE_NM="$PREFIX/libexec/llvm-nm" -DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF "$@"
+}
+
+builtins()
+{
+ if ! done_stage builtins; then
+ target_cmake -S "$LLVM_SOURCE/compiler-rt" -B "$BUILD_DIR/builtins" \
+ -DLLVM_CMAKE_DIR="$LLVM_CMAKE_DIR" \
+ -DCMAKE_INSTALL_PREFIX="$RESOURCE_DIR" -DCOMPILER_RT_INSTALL_PATH="$RESOURCE_DIR" \
+ -DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \
+ -DCOMPILER_RT_BUILD_BUILTINS=ON -DCOMPILER_RT_EXCLUDE_ATOMIC_BUILTIN=ON \
+ -DCOMPILER_RT_BUILD_CRT=ON -DCOMPILER_RT_BUILD_SANITIZERS=OFF \
+ -DCOMPILER_RT_BUILD_XRAY=OFF -DCOMPILER_RT_BUILD_LIBFUZZER=OFF \
+ -DCOMPILER_RT_BUILD_PROFILE=OFF -DCOMPILER_RT_BUILD_MEMPROF=OFF \
+ -DCOMPILER_RT_BUILD_CTX_PROFILE=OFF -DCOMPILER_RT_BUILD_ORC=OFF \
+ -DCOMPILER_RT_BUILD_GWP_ASAN=OFF -DCOMPILER_RT_INCLUDE_TESTS=OFF
+ cmake --build "$BUILD_DIR/builtins" --parallel "$JOBS"
+ cmake --install "$BUILD_DIR/builtins"
+ mark_stage builtins
+ fi
+}
+
+musl()
+{
+ if ! done_stage musl; then
+ builtins_archive=$("$HOST_CLANG" --target="$TARGET" -resource-dir="$RESOURCE_DIR" \
+ --rtlib=compiler-rt -print-libgcc-file-name)
+ [ -f "$builtins_archive" ] || die "compiler-rt archive not found: $builtins_archive"
+ make -C "$BUILD_DIR/musl" -j "$JOBS" "LIBCC=$builtins_archive"
+ make -C "$BUILD_DIR/musl" "LIBCC=$builtins_archive" DESTDIR="$SYSROOT" install
+ ln -sf ../usr/lib/libc.so "$SYSROOT/lib/ld-musl-$ARCH.so.1"
+ mark_stage musl
+ fi
+}
+
+atomic()
+{
+ if ! done_stage atomic; then
+ mkdir -p "$BUILD_DIR/atomic"
+ cp "$ATOMIC_SOURCE/atomic.c" "$ATOMIC_SOURCE/Makefile" "$BUILD_DIR/atomic/"
+ make -C "$BUILD_DIR/atomic" -j "$JOBS" CC="$BUILD_DIR/musl-cc" \
+ AR="$PREFIX/libexec/llvm-ar" CFLAGS="$TARGET_CFLAGS"
+ make -C "$BUILD_DIR/atomic" CC="$BUILD_DIR/musl-cc" \
+ AR="$PREFIX/libexec/llvm-ar" CFLAGS="$TARGET_CFLAGS" PREFIX=/usr DESTDIR="$SYSROOT" install
+ mark_stage atomic
+ fi
+}
+
+runtimes()
+{
+ if ! done_stage runtimes; then
+ target_cmake -S "$LLVM_SOURCE/runtimes" -B "$BUILD_DIR/runtimes" \
+ -DCMAKE_INSTALL_PREFIX=/usr -DLLVM_ENABLE_RUNTIMES='libunwind;libcxxabi;libcxx' \
+ -DLIBCXX_HAS_MUSL_LIBC=ON -DLIBCXX_USE_COMPILER_RT=ON \
+ -DLIBCXX_CXX_ABI=libcxxabi -DLIBCXX_ENABLE_SHARED=ON -DLIBCXX_ENABLE_STATIC=ON \
+ -DLIBCXX_STATICALLY_LINK_ABI_IN_STATIC_LIBRARY=ON \
+ -DLIBCXX_STATICALLY_LINK_ABI_IN_SHARED_LIBRARY=OFF -DLIBCXX_ENABLE_ABI_LINKER_SCRIPT=ON \
+ -DLIBCXX_INCLUDE_TESTS=OFF -DLIBCXX_INCLUDE_BENCHMARKS=OFF \
+ -DLIBCXXABI_USE_COMPILER_RT=ON -DLIBCXXABI_USE_LLVM_UNWINDER=ON \
+ -DLIBCXXABI_ENABLE_SHARED=ON -DLIBCXXABI_ENABLE_STATIC=ON \
+ -DLIBCXXABI_INCLUDE_TESTS=OFF -DLIBUNWIND_USE_COMPILER_RT=ON \
+ -DLIBUNWIND_ENABLE_SHARED=ON -DLIBUNWIND_ENABLE_STATIC=ON \
+ -DLIBUNWIND_INCLUDE_TESTS=OFF
+ cmake --build "$BUILD_DIR/runtimes" --parallel "$JOBS"
+ DESTDIR="$SYSROOT" cmake --install "$BUILD_DIR/runtimes"
+ mark_stage runtimes
+ fi
+}
+
+wrappers()
+{
+ mkdir -p "$PREFIX/bin" "$PREFIX/share/musllvm/empty-gcc"
+ for pair in cc:cc.sh c++:cc.sh ld:ld.sh pkg-config:pkg-config.sh env:env.sh; do
+ wrapper="$PREFIX/bin/$TARGET-${pair%%:*}"
+ sed -e "s/@TARGET@/$TARGET/g" -e "s/@RESOURCE_VERSION@/$RESOURCE_VERSION/g" \
+ "$PROOT/files/toolchain/${pair#*:}" > "$wrapper"
+ chmod 755 "$wrapper"
+ done
+ for alias in clang gcc; do ln -sf "$TARGET-cc" "$PREFIX/bin/$TARGET-$alias"; done
+ for alias in clang++ g++; do ln -sf "$TARGET-c++" "$PREFIX/bin/$TARGET-$alias"; done
+ for pair in ar:ar ranlib:ranlib nm:nm objcopy:objcopy objdump:objdump \
+ readelf:readelf strip:strip size:size strings:strings addr2line:addr2line c++filt:cxxfilt; do
+ ln -sf "../libexec/llvm-${pair#*:}" "$PREFIX/bin/$TARGET-${pair%%:*}"
+ done
+ sed -e "s/@TARGET@/$TARGET/g" -e "s/@ARCH@/$ARCH/g" \
+ "$PROOT/files/toolchain/toolchain.cmake" > "$PREFIX/share/musllvm/toolchain.cmake"
+ printf '%s\n' "$RESOURCE_VERSION" > "$PREFIX/share/musllvm/resource-version"
+ printf 'Toolchain ready: %s\n' "$PREFIX"
+}
+
+stage=${1:-all}
+case "$stage" in
+config)
+ printf 'ARCH=%s\nTARGET=%s\nLLVM_MODE=%s\nPREFIX=%s\nSYSROOT=%s\nBUILD_DIR=%s\nJOBS=%s\n' \
+ "$ARCH" "$TARGET" "$LLVM_MODE" "$PREFIX" "$SYSROOT" "$BUILD_DIR" "$JOBS"
+ exit ;;
+doctor) doctor; exit ;;
+clean)
+ [ ! -d "$BUILD_DIR/lock" ] || die "build is running: $BUILD_DIR/lock"
+ [ -f "$PREFIX/.musllvm-config" ] || [ ! -e "$PREFIX" ] || die "refusing to remove an unmarked PREFIX: $PREFIX"
+ [ -f "$BUILD_DIR/config" ] || [ ! -e "$BUILD_DIR" ] || die "refusing to remove an unmarked BUILD_DIR: $BUILD_DIR"
+ rm -rf "$PREFIX" "$BUILD_DIR"
+ exit ;;
+all|headers|builtins|musl|atomic|runtimes|toolchain|configure-llvm) ;;
+*) die "unknown build stage: $stage" ;;
+esac
+
+doctor
+unset CPATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH LIBRARY_PATH
+unset GCC_EXEC_PREFIX COMPILER_PATH CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
+BOOTSTRAP_CC=$(absolute_path "$(command -v "$HOST_CC")")
+BOOTSTRAP_CXX=$(absolute_path "$(command -v "$HOST_CXX")")
+sh "$PROOT/scripts/check.sh" pull
+extract_source llvm
+LLVM_SOURCE=$source_path
+extract_source musl
+MUSL_SOURCE=$source_path
+extract_source linux
+LINUX_SOURCE=$source_path
+extract_source atomic
+ATOMIC_SOURCE=$source_path
+
+config_digest=$({
+ printf '%s\n' "$ARCH" "$TARGET" "$LLVM_MODE" "$PREFIX" "$BUILD_DIR" \
+ "$BOOTSTRAP_CC" "$BOOTSTRAP_CXX" "$TARGET_CFLAGS" "$HOST_CFLAGS" "$HOST_CXXFLAGS" "$HOST_LDFLAGS"
+ "$BOOTSTRAP_CC" --version
+ "$BOOTSTRAP_CXX" --version
+ cat "$SOURCES_FILE"
+ cat "$PROOT/scripts/build.sh" "$PROOT/scripts/config.sh"
+ if [ "$LLVM_MODE" = system ]; then
+ for program in $TOOLS llvm-config; do command -v "$program"; done
+ ld.lld --version
+ llvm-ar --version
+ fi
+} | sha256sum)
+config_digest=${config_digest%% *}
+for config_file in "$PREFIX/.musllvm-config" "$BUILD_DIR/config"; do
+ if [ -f "$config_file" ]; then
+ [ "$(cat "$config_file")" = "$config_digest" ] ||
+ die "configuration changed; run make clean with the same ARCH, PREFIX and BUILD_DIR before rebuilding"
+ fi
+done
+for directory in "$PREFIX" "$BUILD_DIR"; do
+ if [ -d "$directory" ] && [ ! -f "$directory/.musllvm-config" ] && [ ! -f "$directory/config" ]; then
+ [ -z "$(ls -A "$directory")" ] || die "refusing to use an unmarked nonempty directory: $directory"
+ fi
+done
+mkdir -p "$PREFIX" "$BUILD_DIR/stamps"
+mkdir "$BUILD_DIR/lock" 2>/dev/null || die "build is running: $BUILD_DIR/lock"
+trap 'rmdir "$BUILD_DIR/lock"' 0
+trap 'exit 1' HUP INT TERM
+printf '%s\n' "$config_digest" > "$PREFIX/.musllvm-config"
+printf '%s\n' "$config_digest" > "$BUILD_DIR/config"
+if [ "$stage" = configure-llvm ]; then configure_llvm; exit; fi
+toolchain
+if [ "$stage" = toolchain ]; then exit; fi
+headers
+if [ "$stage" = headers ]; then exit; fi
+builtins
+if [ "$stage" = builtins ]; then exit; fi
+musl
+if [ "$stage" = musl ]; then exit; fi
+atomic
+if [ "$stage" = atomic ]; then exit; fi
+runtimes
+wrappers
+if [ "$stage" = all ]; then sh "$PROOT/scripts/verify.sh"; fi
diff --git a/scripts/check.sh b/scripts/check.sh
@@ -1,15 +1,30 @@
#!/bin/sh
set -eu
-. $PWD/scripts/config.sh
+. "$(dirname "$0")/config.sh"
+need sha256sum
+[ -f "$SOURCES_FILE" ] || die "source manifest not found: $SOURCES_FILE"
+case "${1:-pull}" in
+pull) need curl; mkdir -p "$SOURCES_DIR" ;;
+verify) ;;
+*) die "usage: check.sh [pull|verify]" ;;
+esac
-while IFS= read -r url; do \
-[ -z "$url" ] && continue; \
-fname="$(printf '%s' "$url" | sed -E 's/[?#].*$//' | sed -E 's!.*/!!')"; \
-if [ -e "$PROOT/sources/$fname" ]; then \
- printf "SKIP: %s (already exists)\n" "$fname"; \
-else \
- printf "GET: %s -> %s\n" "$url" "$fname"; \
- curl -f -L -O -S --output-dir "$PROOT/sources/" "$url" || { printf "ERROR: failed to download %s\n" "$url"; exit 1; }; \
-fi; \
-done < "$SOURCES_FILE"; \
+for name in musl linux llvm atomic; do
+ source_info "$name"
+ if archive_ok; then
+ printf 'OK %s\n' "$archive"
+ continue
+ fi
+ [ "${1:-pull}" = pull ] || die "missing or corrupt archive: $archive"
+ printf 'Downloading %s\n' "$archive"
+ part="$SOURCES_DIR/$archive.part.$$"
+ trap 'rm -f "$part"' 0
+ trap 'exit 1' HUP INT TERM
+ curl --fail --location --show-error --retry 3 --connect-timeout 20 \
+ --output "$part" "$url"
+ actual_digest=$(sha256sum "$part")
+ [ "${actual_digest%% *}" = "$digest" ] || die "SHA-256 mismatch for $archive"
+ mv "$part" "$SOURCES_DIR/$archive"
+ trap - 0 HUP INT TERM
+done
diff --git a/scripts/config.sh b/scripts/config.sh
@@ -1,16 +1,138 @@
-# musllvm default flags
-# --------------------------------
-# feel free to change these however you like
-#
-
-#
-# these are shared between the Makefile and scripts
-#
-ARCH=$(uname -m)
-PROOT=${PWD}
-SOURCES_FILE=${PROOT}/sources.list
-FROOT="${ARCH}-musllvm"
-CROSS="${PROOT}/build/cross" # cross directory
-
-# MAKEFLAGS to parse the amounto of cores used
-MAKEFLAGS="-j$(nproc) -l$(nproc)"
+#!/bin/sh
+
+die()
+{
+ printf 'musllvm: %s\n' "$*" >&2
+ exit 1
+}
+
+need()
+{
+ command -v "$1" >/dev/null 2>&1 || die "required program not found: $1"
+}
+
+absolute_path()
+{
+ case "$1" in
+ /*) printf '%s\n' "$1" ;;
+ *) printf '%s/%s\n' "$PROOT" "$1" ;;
+ esac
+}
+
+validate_directory()
+{
+ case "$1" in
+ *[!a-zA-Z0-9_./-]*) die "build paths must use letters, digits, slashes, dots, underscores or hyphens: $1" ;;
+ esac
+ case "$1" in
+ /|/usr|/usr/local|/bin|/lib|/lib64|/etc|/home|/tmp|"$PROOT")
+ die "choose a dedicated directory, not $1" ;;
+ */../*|*/..|*/./*|*/.|*/)
+ die "directory must have no dot components or trailing slash: $1" ;;
+ esac
+}
+
+source_info()
+{
+ source_line=$(awk -v component="$1" '$1 == component { print; count++ } END { if (count != 1) exit 1 }' "$SOURCES_FILE") ||
+ die "expected one $1 entry in $SOURCES_FILE"
+ read -r component archive digest url extra <<EOF
+$source_line
+EOF
+ case "$archive" in
+ *[!a-zA-Z0-9._-]*|'') die "invalid archive name: $archive" ;;
+ esac
+ case "$digest" in
+ *[!a-f0-9]*|'') die "invalid SHA-256 for $archive" ;;
+ esac
+ [ "${#digest}" -eq 64 ] && [ -z "$extra" ] || die "invalid source entry for $component"
+ case "$url" in
+ https://*) ;;
+ *) die "source URL must use HTTPS: $url" ;;
+ esac
+ source_name=${archive%.tar.*}
+}
+
+archive_ok()
+{
+ [ -f "$SOURCES_DIR/$archive" ] || return 1
+ actual_digest=$(sha256sum "$SOURCES_DIR/$archive") || return 1
+ [ "${actual_digest%% *}" = "$digest" ]
+}
+
+extract_source()
+{
+ source_info "$1"
+ archive_ok || die "missing or corrupt $archive; run make pull"
+ source_path="$PROOT/build/src/$source_name"
+ if [ -f "$source_path/.musllvm-source" ]; then
+ [ "$(cat "$source_path/.musllvm-source")" = "$digest" ] || die "source digest changed: $source_path"
+ return
+ fi
+ [ ! -e "$source_path" ] || die "incomplete source directory: $source_path"
+ mkdir -p "$PROOT/build/src"
+ source_lock="$source_path.lock"
+ (
+ mkdir "$source_lock" 2>/dev/null || die "source is being extracted: $source_name"
+ trap 'rm -rf "$source_lock"' 0
+ trap 'exit 1' HUP INT TERM
+ source_tmp="$source_lock/tree"
+ mkdir "$source_tmp"
+ printf 'Extracting %s\n' "$archive"
+ tar -xf "$SOURCES_DIR/$archive" --strip-components=1 -C "$source_tmp" || die "could not extract $archive"
+ printf '%s\n' "$digest" > "$source_tmp/.musllvm-source"
+ mv "$source_tmp" "$source_path"
+ )
+}
+
+PROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd -P)
+ARCH=${ARCH:-x86_64}
+TARGET=${TARGET:-$ARCH-linux-musl}
+case "$ARCH" in
+x86_64) KERNEL_ARCH=x86; ELF_MACHINE='Advanced Micro Devices X86-64' ;;
+aarch64) KERNEL_ARCH=arm64; ELF_MACHINE='AArch64' ;;
+*) die "unsupported ARCH=$ARCH; choose x86_64 or aarch64" ;;
+esac
+case "$TARGET" in
+"$ARCH-linux-musl"|"$ARCH-unknown-linux-musl") ;;
+*) die "TARGET=$TARGET does not match ARCH=$ARCH and the musl ABI" ;;
+esac
+LLVM_MODE=${LLVM_MODE:-system}
+case "$LLVM_MODE" in
+system|source) ;;
+*) die "LLVM_MODE must be system or source" ;;
+esac
+JOBS=${JOBS:-2}
+LINK_JOBS=${LINK_JOBS:-1}
+for jobs in "$JOBS" "$LINK_JOBS"; do
+ case "$jobs" in
+ *[!0-9]*|'') die "JOBS and LINK_JOBS must be positive integers" ;;
+ esac
+ [ "$jobs" -gt 0 ] 2>/dev/null || die "JOBS and LINK_JOBS must be positive integers"
+done
+PREFIX=$(absolute_path "${PREFIX:-build/$TARGET}")
+BUILD_DIR=$(absolute_path "${BUILD_DIR:-build/work/$TARGET}")
+SOURCES_DIR=$(absolute_path "${SOURCES_DIR:-sources}")
+SOURCES_FILE=$(absolute_path "${SOURCES_FILE:-sources.list}")
+validate_directory "$PREFIX"
+validate_directory "$BUILD_DIR"
+validate_directory "$SOURCES_DIR"
+case "$PROOT" in
+*[!a-zA-Z0-9_./-]*) die "move the checkout to a path without whitespace or shell metacharacters" ;;
+esac
+case "$PREFIX/" in
+"$BUILD_DIR/"*) die "PREFIX must be separate from BUILD_DIR" ;;
+esac
+case "$BUILD_DIR/" in
+"$PREFIX/"*) die "BUILD_DIR must be separate from PREFIX" ;;
+esac
+SYSROOT="$PREFIX/sysroot"
+HOST_CC=${HOST_CC:-clang}
+HOST_CXX=${HOST_CXX:-clang++}
+TARGET_CFLAGS=${TARGET_CFLAGS:--O2}
+HOST_CFLAGS=${HOST_CFLAGS:-}
+HOST_CXXFLAGS=${HOST_CXXFLAGS:-}
+HOST_LDFLAGS=${HOST_LDFLAGS:-}
+RUNNER=${RUNNER:-}
+export PROOT ARCH TARGET PREFIX BUILD_DIR SOURCES_DIR SOURCES_FILE LLVM_MODE JOBS LINK_JOBS
+export HOST_CC HOST_CXX TARGET_CFLAGS HOST_CFLAGS HOST_CXXFLAGS HOST_LDFLAGS RUNNER
diff --git a/scripts/elftoolchain-cross.sh b/scripts/elftoolchain-cross.sh
@@ -1,42 +0,0 @@
-#!/bin/sh
-set -eu
-
-. $PWD/scripts/config.sh
-
-ELF_TAR=$(ls "$PROOT"/sources/elftoolchain-*.tar.* 2>/dev/null | head -n 1 | xargs basename)
-if [ -z "$ELF_TAR" ]; then
- echo "ERR: No Musl tarball found in sources directory"
- exit 1
-fi
-ELF_SOURCE="${ELF_TAR%%.tar.*}"
-
-if command -v "clang" >/dev/null 2>&1 && command -v "lld" >/dev/null 2>&1; then
- export CC="clang"
-else
- export CC="cc"
-fi
-
-if [ ! -d $PROOT/build/"$ELF_SOURCE" ]; then
- echo "Extracting Elftoolchain tarball to ./build/"
- tar -xpf $PROOT/sources/"$ELF_TAR" -C ./build
-else
- echo "Elftoolchain source already extracted."
-fi
-
-echo "Buliding musl headers"
-if [ -f "$PROOT/build/$ELF_SOURCE" ]; then
- echo "ELFTOOLCHAIN already built, skipping..."
- exit
-fi
-
-cd ./build/"$ELF_SOURCE"
-
-echo "patching elftoolchain-cross..."
-
-patch -Np1 < "$PROOT"/patches/elftoolchain/*
-
-echo "Installing elftoolchain-cross..."
-
-bmake WITH_ADDITIONAL_DOCUMENTATION=no \
- WITH_TESTS=no \
- MANTARGET=man
diff --git a/scripts/headers.sh b/scripts/headers.sh
@@ -1,40 +1,3 @@
#!/bin/sh
set -eu
-
-. $PWD/scripts/config.sh
-
-LINUX_TAR=$(ls "$PROOT"/sources/linux-*.tar.* 2>/dev/null | head -n 1 | xargs basename)
-if [ -z "$LINUX_TAR" ]; then
- echo "ERR: No Linux tarball found in sources directory"
- exit 1
-fi
-LINUX_SOURCE="${LINUX_TAR%%.tar*}"
-
-if command -v "clang" >/dev/null 2>&1 && command -v "lld" >/dev/null 2>&1; then
- export LLVM=1
- export LLVM_IAS=1
-fi
-
-if [ ! -d $PROOT/build/"$LINUX_SOURCE" ]; then
- echo "Extracting Linux tarball to ./build/"
- tar -xpf $PROOT/sources/"$LINUX_TAR" -C $PROOT/build
-else
- echo "Linux source already extracted."
-fi
-
-echo "Creating target include directory"
-mkdir -pv $PROOT/build/"$FROOT"/include
-
-echo "Building Headers"
-if [ -d $PROOT/build/"$FROOT"/include/linux ]; then
- echo "Headers already built, skipping..."
- exit
-fi
-
-cd ./build/"$LINUX_SOURCE"
-
-make mrproper
-make headers
-find usr/include -type f ! -name '*.h' -delete
-cp -rv usr/include/* $PROOT/build/"$FROOT"/include/
-
+exec sh "$(dirname "$0")/build.sh" headers
diff --git a/scripts/musl-cross.sh b/scripts/musl-cross.sh
@@ -1,38 +1,3 @@
#!/bin/sh
set -eu
-
-. $PWD/scripts/config.sh
-
-MUSL_TAR=$(ls "$PROOT"/sources/musl-*.tar.* 2>/dev/null | head -n 1 | xargs basename)
-if [ -z "$MUSL_TAR" ]; then
- echo "ERR: No Musl tarball found in sources directory"
- exit 1
-fi
-MUSL_SOURCE="${MUSL_TAR%%.tar.*}"
-
-if command -v "clang" >/dev/null 2>&1 && command -v "lld" >/dev/null 2>&1; then
- export CC="clang"
-else
- export CC="cc"
-fi
-
-if [ ! -d $PROOT/build/"MUSL_SOURCE" ]; then
- echo "Extracting Musl tarball to ./build/"
- tar -xpf ./sources/"$MUSL_TAR" -C ./build
-else
- echo "Musl source already extracted."
-fi
-
-echo "Buliding musl headers"
-if [ -f "$PROOT/build/$MUSL_SOURCE" ]; then
- echo "Musl headers already built, skipping..."
- exit
-fi
-
-cd $PROOT/build/"$MUSL_SOURCE"
-
-echo "Installing musl headers..."
-./configure \
- --prefix=/
-
-DESTDIR=$CROSS make ARCH=$(uname -m) install-headers
+exec sh "$(dirname "$0")/build.sh" musl
diff --git a/scripts/test.sh b/scripts/test.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -eu
+root=$(CDPATH= cd "$(dirname "$0")/.." && pwd -P)
+for script in "$root"/scripts/*.sh "$root"/files/toolchain/*.sh; do
+ sh -n "$script"
+done
+PYTHONDONTWRITEBYTECODE=1 python3 "$root/tests/test_driver.py"
diff --git a/scripts/verify.sh b/scripts/verify.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+set -eu
+
+. "$(dirname "$0")/config.sh"
+MUSLLVM_SYSROOT=$SYSROOT
+export MUSLLVM_SYSROOT
+cc="$PREFIX/bin/$TARGET-cc"
+cxx="$PREFIX/bin/$TARGET-c++"
+readelf="$PREFIX/bin/$TARGET-readelf"
+work="$BUILD_DIR/verify"
+loader="$SYSROOT/lib/ld-musl-$ARCH.so.1"
+for program in "$cc" "$cxx" "$readelf"; do
+ [ -x "$program" ] || die "toolchain is incomplete: $program"
+done
+mkdir -p "$work"
+"$cc" -std=c99 -O2 -pthread "$PROOT/tests/hello.c" -o "$work/hello-c"
+"$cc" -std=c99 -O2 -pthread -static "$PROOT/tests/hello.c" -o "$work/hello-c-static"
+"$cxx" -std=c++20 -O2 -pthread "$PROOT/tests/hello.cpp" -o "$work/hello-cxx"
+"$cxx" -std=c++20 -O2 -pthread -static "$PROOT/tests/hello.cpp" -o "$work/hello-cxx-static"
+
+for program in hello-c hello-c-static hello-cxx hello-cxx-static; do
+ "$readelf" -h "$work/$program" > "$work/$program.header"
+ grep -F "$ELF_MACHINE" "$work/$program.header" >/dev/null || die "wrong ELF machine: $program"
+ "$readelf" -l "$work/$program" > "$work/$program.segments"
+ "$readelf" -d "$work/$program" > "$work/$program.dynamic"
+ if grep -E 'libc\.so\.6|libstdc\+\+|libgcc_s|ld-linux' "$work/$program.dynamic" "$work/$program.segments"; then
+ die "host runtime dependency in $program"
+ fi
+ case "$program" in
+ *-static)
+ if grep -E 'INTERP|NEEDED' "$work/$program.segments" "$work/$program.dynamic"; then
+ die "static executable has dynamic dependencies: $program"
+ fi ;;
+ *)
+ grep -F "/lib/ld-musl-$ARCH.so.1" "$work/$program.segments" >/dev/null ||
+ die "wrong dynamic loader in $program" ;;
+ esac
+done
+
+for language in c c++; do
+ compiler=$cc
+ [ "$language" = c ] || compiler=$cxx
+ CPATH=/usr/include CPLUS_INCLUDE_PATH=/usr/include LIBRARY_PATH=/usr/lib \
+ "$compiler" -E -v -x "$language" /dev/null > /dev/null 2> "$work/$language.includes"
+ if grep -E '^ /usr/(local/)?include([ /]|$)|^ /usr/.*/c\+\+' "$work/$language.includes"; then
+ die "host headers found in $language search path"
+ fi
+done
+
+cmake -S "$PROOT/tests/cmake" -B "$work/cmake" -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE="$PREFIX/share/musllvm/toolchain.cmake"
+cmake --build "$work/cmake" --parallel "$JOBS"
+
+host_arch=$(uname -m)
+if [ "$host_arch" = "$ARCH" ] || [ -n "$RUNNER" ]; then
+ for program in hello-c-static hello-cxx-static; do
+ if [ -n "$RUNNER" ]; then "$RUNNER" "$work/$program"; else "$work/$program"; fi
+ done
+ for program in hello-c hello-cxx; do
+ if [ -n "$RUNNER" ]; then
+ "$RUNNER" "$loader" --library-path "$SYSROOT/usr/lib:$SYSROOT/lib" "$work/$program"
+ else
+ "$loader" --library-path "$SYSROOT/usr/lib:$SYSROOT/lib" "$work/$program"
+ fi
+ done
+else
+ printf 'Execution skipped for %s on %s; set RUNNER=qemu-%s to run the programs.\n' "$ARCH" "$host_arch" "$ARCH"
+fi
+printf 'Verified %s: static/dynamic C and C++, target ELF, isolated headers, and CMake\n' "$TARGET"
diff --git a/sources.list b/sources.list
@@ -1,16 +1,5 @@
-https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.17.7.tar.xz
-https://musl.libc.org/releases/musl-1.2.5.tar.gz
-https://pub.sortix.org/libz/libz-1.2.8.2025.03.07.tar.gz
-https://github.com/chimera-linux/libatomic-chimera/archive/refs/tags/v0.90.0.tar.gz
-https://github.com/Projeto-Pindorama/musl-compat/archive/refs/tags/20251111.tar.gz
-https://github.com/libarchive/libarchive/releases/download/v3.8.2/libarchive-3.8.2.tar.gz
-https://sourceforge.net/projects/elftoolchain/files/Sources/elftoolchain-0.7.1/elftoolchain-0.7.1.tar.bz2
-https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.2/llvm-21.1.2.src.tar.xz
-https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.2/cmake-21.1.2.src.tar.xz
-https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.2/third-party-21.1.2.src.tar.xz
-https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.2/clang-21.1.2.src.tar.xz
-https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.2/compiler-rt-21.1.2.src.tar.xz
-https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.5/lld-21.1.5.src.tar.xz
-https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.5/libunwind-21.1.5.src.tar.xz
-https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.5/libcxx-21.1.5.src.tar.xz
-https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.5/libcxxabi-21.1.5.src.tar.xz
+# component archive sha256 url
+musl musl-1.2.5.tar.gz a9a118bbe84d8764da0ea0d28b3ab3fae8477fc7e4085d90102b8596fc7c75e4 https://musl.libc.org/releases/musl-1.2.5.tar.gz
+linux linux-6.17.7.tar.xz ddf2ea0d4439e1d57136be3623102af9458f601f5b1cb77e83246e88aea09d0e https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.17.7.tar.xz
+llvm llvm-project-21.1.8.src.tar.xz 4633a23617fa31a3ea51242586ea7fb1da7140e426bd62fc164261fe036aa142 https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.8/llvm-project-21.1.8.src.tar.xz
+atomic libatomic-chimera-0.90.0.tar.gz fccbd8c0ef7fd473275f835b3fca9275fb27a0c196cdcdff1f6d14ab12ed3a53 https://github.com/chimera-linux/libatomic-chimera/archive/refs/tags/v0.90.0.tar.gz
diff --git a/tests/cmake/CMakeLists.txt b/tests/cmake/CMakeLists.txt
@@ -0,0 +1,10 @@
+cmake_minimum_required(VERSION 3.20)
+project(musllvm_smoke LANGUAGES C CXX)
+add_executable(hello-c ../hello.c)
+set_target_properties(hello-c PROPERTIES C_STANDARD 99)
+target_compile_options(hello-c PRIVATE -pthread)
+target_link_options(hello-c PRIVATE -pthread)
+add_executable(hello-cxx ../hello.cpp)
+set_target_properties(hello-cxx PROPERTIES CXX_STANDARD 20)
+target_compile_options(hello-cxx PRIVATE -pthread)
+target_link_options(hello-cxx PRIVATE -pthread)
diff --git a/tests/hello.c b/tests/hello.c
@@ -0,0 +1,32 @@
+#include <pthread.h>
+#include <stdio.h>
+
+static volatile unsigned __int128 numerator = (unsigned __int128)1 << 100;
+static volatile unsigned __int128 denominator = (unsigned __int128)1 << 40;
+
+static void *
+worker(void *argument)
+{
+ return argument;
+}
+
+int
+main(void)
+{
+ pthread_t thread;
+ void *result;
+ int value;
+ unsigned __int128 quotient;
+
+ value = 42;
+ quotient = numerator / denominator;
+ if (quotient != (unsigned __int128)1 << 60)
+ return 1;
+ if (pthread_create(&thread, NULL, worker, &value) != 0)
+ return 1;
+ if (pthread_join(thread, &result) != 0 || result != &value)
+ return 1;
+ if (puts("musllvm C OK") < 0 || fflush(stdout) == EOF)
+ return 1;
+ return 0;
+}
diff --git a/tests/hello.cpp b/tests/hello.cpp
@@ -0,0 +1,29 @@
+#include <atomic>
+#include <iostream>
+#include <numeric>
+#include <stdexcept>
+#include <thread>
+#include <vector>
+
+int
+main()
+{
+ std::vector<int> values(128, 2);
+ std::atomic<int> total{0};
+ std::atomic<unsigned __int128> wide{0};
+ std::thread worker([&]() { total.store(std::accumulate(values.begin(), values.end(), 0)); });
+
+ worker.join();
+ wide.store((unsigned __int128)1 << 100);
+ if (wide.load() != (unsigned __int128)1 << 100)
+ return 1;
+ try {
+ if (total.load() != 256)
+ return 1;
+ throw std::runtime_error("musllvm C++ OK");
+ } catch (const std::runtime_error &error) {
+ std::cout << error.what() << '\n';
+ }
+ std::cout.flush();
+ return std::cout.good() ? 0 : 1;
+}
diff --git a/tests/test_driver.py b/tests/test_driver.py
@@ -0,0 +1,322 @@
+import hashlib
+import io
+import json
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import tarfile
+import tempfile
+import unittest
+
+
+ROOT = Path(__file__).resolve().parent.parent
+MAKE = shutil.which("make")
+TOOLS = "clang clang++ ld.lld llvm-ar llvm-ranlib llvm-nm llvm-objcopy llvm-objdump llvm-readelf llvm-strip llvm-size llvm-strings llvm-addr2line llvm-cxxfilt"
+FAKE_TOOL = r'''#!/usr/bin/env python3
+import json
+import os
+from pathlib import Path
+import shutil
+import sys
+
+name = Path(sys.argv[0]).name
+args = sys.argv[1:]
+with open(os.environ["FAKE_LOG"], "a") as log:
+ log.write(json.dumps({"tool": name, "args": args}) + "\n")
+
+def write(path, value="fixture"):
+ path = Path(path)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(value)
+
+if name in ("clang", "clang++"):
+ if "--version" in args:
+ print("clang version 21.1.8 (fixture)")
+ elif "-dumpversion" in args:
+ print("21.1.8")
+ elif "-print-resource-dir" in args:
+ print(os.environ["FAKE_RESOURCE"])
+ elif "-print-target-triple" in args:
+ print(next(arg.split("=", 1)[1] for arg in args if arg.startswith("--target=")))
+ elif "-print-libgcc-file-name" in args:
+ resource = next(arg.split("=", 1)[1] for arg in args if arg.startswith("-resource-dir="))
+ print(str(Path(resource) / "lib/linux" / ("libclang_rt.builtins-" + os.environ["ARCH"] + ".a")))
+ else:
+ sys.exit("fixture refuses compilation")
+elif name == "llvm-config":
+ print(os.environ["FAKE_LLVM_CMAKE"])
+elif name == "curl":
+ destination = Path(args[args.index("--output") + 1])
+ archive = args[-1].rsplit("/", 1)[-1]
+ if os.environ.get("FAKE_CURL_BAD"):
+ destination.write_text("truncated download")
+ else:
+ shutil.copyfile(Path(os.environ["FAKE_DOWNLOADS"]) / archive, destination)
+ if os.environ.get("FAKE_CURL_FAIL"):
+ sys.exit(22)
+elif name == "make":
+ directory = Path(args[args.index("-C") + 1])
+ if "headers_install" in args:
+ prefix = next(arg.split("=", 1)[1] for arg in args if arg.startswith("INSTALL_HDR_PATH="))
+ write(Path(prefix) / "include/linux/version.h")
+ elif "install-headers" in args:
+ prefix = next(arg.split("=", 1)[1] for arg in args if arg.startswith("DESTDIR="))
+ write(Path(prefix) / "usr/include/stdio.h")
+ elif "install" in args:
+ prefix = next(arg.split("=", 1)[1] for arg in args if arg.startswith("DESTDIR="))
+ write(Path(prefix) / "usr/lib/libc.so")
+ (Path(prefix) / "lib").mkdir(exist_ok=True)
+elif name == "cmake":
+ if "-B" in args:
+ directory = Path(args[args.index("-B") + 1])
+ write(directory / "fixture-config.json", json.dumps(args))
+ elif "--build" in args:
+ directory = Path(args[args.index("--build") + 1])
+ if os.environ.get("FAKE_FAIL_STAGE") == directory.name:
+ sys.exit(1)
+ if "install-distribution" in args:
+ configuration = json.loads((directory / "fixture-config.json").read_text())
+ prefix = next(arg.split("=", 1)[1] for arg in configuration if arg.startswith("-DCMAKE_INSTALL_PREFIX="))
+ for tool in os.environ["FAKE_TOOLS"].split():
+ path = Path(prefix) / "bin" / tool
+ path.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copyfile(os.environ["FAKE_EXECUTABLE"], path)
+ path.chmod(0o755)
+ elif "--install" in args:
+ directory = Path(args[args.index("--install") + 1])
+ configuration = json.loads((directory / "fixture-config.json").read_text())
+ prefix = next(arg.split("=", 1)[1] for arg in configuration if arg.startswith("-DCMAKE_INSTALL_PREFIX="))
+ if directory.name == "builtins":
+ write(Path(prefix) / "lib/linux" / ("libclang_rt.builtins-" + os.environ["ARCH"] + ".a"))
+ else:
+ write(Path(os.environ["DESTDIR"]) / "usr/lib/libc++.a")
+'''
+
+CAPTURE = r'''#!/usr/bin/env python3
+import json
+import os
+import sys
+print(json.dumps({"args": sys.argv[1:], "env": dict(os.environ)}))
+'''
+
+
+class DriverTests(unittest.TestCase):
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory(prefix="musllvm-test-")
+ self.addCleanup(self.temporary.cleanup)
+ self.root = Path(self.temporary.name) / "project"
+ self.root.mkdir()
+ for name in ("scripts", "files/toolchain"):
+ shutil.copytree(ROOT / name, self.root / name)
+ for name in ("Makefile", "config.mk"):
+ shutil.copyfile(ROOT / name, self.root / name)
+ self.tools = Path(self.temporary.name) / "tools"
+ self.tools.mkdir()
+ self.fake = self.tools / "fixture"
+ self.fake.write_text(FAKE_TOOL)
+ self.fake.chmod(0o755)
+ for name in TOOLS.split() + ["llvm-config", "curl", "make", "cmake", "ninja"]:
+ (self.tools / name).symlink_to(self.fake)
+ self.resource = Path(self.temporary.name) / "resource/21"
+ (self.resource / "include").mkdir(parents=True)
+ (self.resource / "include/unwind.h").write_text("fixture")
+ self.llvm_cmake = Path(self.temporary.name) / "llvm-cmake"
+ self.llvm_cmake.mkdir()
+ (self.llvm_cmake / "LLVMConfig.cmake").write_text("fixture")
+ self.log = Path(self.temporary.name) / "commands.jsonl"
+ self.downloads = Path(self.temporary.name) / "downloads"
+ self.downloads.mkdir()
+ manifest = []
+ for component in ("musl", "linux", "llvm", "atomic"):
+ archive = self.downloads / (component + "-test.tar.gz")
+ with tarfile.open(archive, "w:gz") as stream:
+ relative = "configure" if component == "musl" else "placeholder"
+ content = b"#!/bin/sh\nset -eu\n\"$CC\" -print-target-triple\n" if component == "musl" else b"fixture"
+ info = tarfile.TarInfo(component + "-test/" + relative)
+ info.size = len(content)
+ info.mode = 0o755
+ stream.addfile(info, io.BytesIO(content))
+ if component == "atomic":
+ for relative in ("Makefile", "atomic.c"):
+ info = tarfile.TarInfo(component + "-test/" + relative)
+ info.size = 7
+ stream.addfile(info, io.BytesIO(b"fixture"))
+ digest = hashlib.sha256(archive.read_bytes()).hexdigest()
+ manifest.append(f"{component} {archive.name} {digest} https://example.invalid/{archive.name}\n")
+ (self.root / "sources.list").write_text("".join(manifest))
+ self.env = os.environ.copy()
+ for key in ("ARCH", "TARGET", "PREFIX", "BUILD_DIR", "SOURCES_DIR", "SOURCES_FILE", "LLVM_MODE", "JOBS", "LINK_JOBS", "HOST_CC", "HOST_CXX", "TARGET_CFLAGS", "HOST_CFLAGS", "HOST_CXXFLAGS", "HOST_LDFLAGS", "RUNNER", "MAKEFLAGS", "MFLAGS", "MUSLLVM_SYSROOT"):
+ self.env.pop(key, None)
+ self.env.update({
+ "PATH": str(self.tools) + os.pathsep + os.environ["PATH"],
+ "FAKE_LOG": str(self.log), "FAKE_RESOURCE": str(self.resource),
+ "FAKE_LLVM_CMAKE": str(self.llvm_cmake),
+ "FAKE_DOWNLOADS": str(self.downloads), "ARCH": "x86_64",
+ "FAKE_EXECUTABLE": str(self.fake), "FAKE_TOOLS": TOOLS,
+ })
+
+ def run_make(self, *arguments, success=True, env=None):
+ result = subprocess.run([MAKE, *arguments], cwd=self.root, env=env or self.env,
+ text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ if success:
+ self.assertEqual(result.returncode, 0, result.stdout)
+ else:
+ self.assertNotEqual(result.returncode, 0, result.stdout)
+ return result
+
+ def commands(self):
+ return [json.loads(line) for line in self.log.read_text().splitlines()]
+
+ def install_wrapper(self, name, template, prefix=None):
+ prefix = prefix or Path(self.temporary.name) / "relocated tools' path"
+ (prefix / "bin").mkdir(parents=True, exist_ok=True)
+ (prefix / "libexec").mkdir(exist_ok=True)
+ script = (ROOT / "files/toolchain" / template).read_text()
+ path = prefix / "bin" / name
+ path.write_text(script.replace("@TARGET@", "x86_64-linux-musl").replace("@RESOURCE_VERSION@", "21"))
+ path.chmod(0o755)
+ return path
+
+ def test_configuration_and_command_line_overrides(self):
+ result = self.run_make("show-config", "ARCH=aarch64", "JOBS=1")
+ self.assertIn("TARGET=aarch64-linux-musl", result.stdout)
+ self.assertIn("JOBS=1", result.stdout)
+ self.assertEqual((self.root / "scripts/config.sh").read_bytes(), (ROOT / "scripts/config.sh").read_bytes())
+ self.assertFalse((self.root / "build").exists())
+
+ def test_invalid_configuration_fails_before_work(self):
+ for arguments in (("ARCH=mips",), ("JOBS=00",), ("JOBS=-2",), ("LINK_JOBS=1:2",),
+ ("PREFIX=/",), ("BUILD_DIR=/tmp/../home",), ("LLVM_MODE=other",),
+ ("TARGET=aarch64-linux-musl",), ("PREFIX=/tmp/tools with spaces",),
+ ("PREFIX=/tmp/tools$$variable",)):
+ with self.subTest(arguments=arguments):
+ self.run_make("show-config", *arguments, success=False)
+ self.assertFalse((self.root / "build").exists())
+
+ def test_fetch_verifies_and_skips_downloaded_archives(self):
+ self.run_make("pull")
+ self.assertEqual(sum(item["tool"] == "curl" for item in self.commands()), 4)
+ self.run_make("pull", "verify-sources")
+ self.assertEqual(sum(item["tool"] == "curl" for item in self.commands()), 4)
+
+ def test_corrupt_cache_is_repaired(self):
+ self.run_make("pull")
+ (self.root / "sources/musl-test.tar.gz").write_text("corrupt")
+ self.run_make("verify-sources", success=False)
+ self.run_make("pull", "verify-sources")
+ self.assertEqual(sum(item["tool"] == "curl" for item in self.commands()), 5)
+
+ def test_bad_or_failed_download_is_never_published(self):
+ for setting in ("FAKE_CURL_BAD", "FAKE_CURL_FAIL"):
+ with self.subTest(setting=setting):
+ env = {**self.env, setting: "1"}
+ self.run_make("pull", success=False, env=env)
+ self.assertEqual(list((self.root / "sources").iterdir()), [])
+
+ def test_full_driver_resumes_and_rejects_changed_configuration(self):
+ self.run_make("runtimes")
+ first = len([item for item in self.commands() if item["tool"] in ("make", "cmake")])
+ self.run_make("runtimes", "JOBS=1")
+ second = len([item for item in self.commands() if item["tool"] in ("make", "cmake")])
+ self.assertEqual(first, second)
+ self.run_make("runtimes", "TARGET_CFLAGS=-Os", success=False)
+ self.run_make("clean")
+ self.assertFalse((self.root / "build/x86_64-linux-musl").exists())
+ self.assertTrue((self.root / "sources/musl-test.tar.gz").exists())
+
+ def test_failed_stage_resumes_without_rebuilding_headers(self):
+ self.run_make("runtimes", success=False, env={**self.env, "FAKE_FAIL_STAGE": "builtins"})
+ self.assertTrue((self.root / "build/work/x86_64-linux-musl/stamps/headers").exists())
+ self.assertFalse((self.root / "build/work/x86_64-linux-musl/stamps/builtins").exists())
+ self.run_make("runtimes")
+ self.assertEqual(sum("headers_install" in item["args"] for item in self.commands()), 1)
+
+ def test_source_mode_uses_installed_host_tools(self):
+ self.run_make("runtimes", "LLVM_MODE=source")
+ prefix = self.root / "build/x86_64-linux-musl"
+ self.assertEqual(os.readlink(prefix / "libexec/clang"), "../host/bin/clang")
+ self.assertTrue((prefix / "bin/x86_64-linux-musl-c++").exists())
+
+ def test_architectures_use_separate_work_and_output(self):
+ self.run_make("runtimes", "ARCH=x86_64")
+ self.run_make("runtimes", "ARCH=aarch64")
+ for arch in ("x86_64", "aarch64"):
+ self.assertTrue((self.root / f"build/{arch}-linux-musl/bin/{arch}-linux-musl-cc").exists())
+ self.assertTrue((self.root / f"build/work/{arch}-linux-musl/stamps/runtimes").exists())
+
+ def test_clean_preserves_unmarked_directory(self):
+ prefix = self.root / "unrelated"
+ prefix.mkdir()
+ sentinel = prefix / "keep"
+ sentinel.write_text("user data")
+ self.run_make("clean", f"PREFIX={prefix}", success=False)
+ self.assertEqual(sentinel.read_text(), "user data")
+
+ def test_concurrent_build_is_rejected(self):
+ self.run_make("runtimes")
+ (self.root / "build/work/x86_64-linux-musl/lock").mkdir()
+ self.run_make("runtimes", success=False)
+ self.run_make("clean", success=False)
+
+ def test_cxx_wrapper_relocation_and_host_isolation(self):
+ wrapper = self.install_wrapper("x86_64-linux-musl-c++", "cc.sh")
+ capture = wrapper.parent.parent / "libexec/clang"
+ capture.write_text(CAPTURE)
+ capture.chmod(0o755)
+ env = {**self.env, "CPATH": "/host/headers", "LIBRARY_PATH": "/host/libraries"}
+ result = subprocess.check_output([str(wrapper), "source with spaces.cpp", "-static"], env=env, text=True)
+ invocation = json.loads(result)
+ self.assertIn("--driver-mode=g++", invocation["args"])
+ self.assertIn("-stdlib=libc++", invocation["args"])
+ self.assertIn("source with spaces.cpp", invocation["args"])
+ self.assertIn(f"--sysroot={wrapper.parent.parent}/sysroot", invocation["args"])
+ self.assertNotIn("CPATH", invocation["env"])
+ self.assertNotIn("LIBRARY_PATH", invocation["env"])
+
+ def test_wrapper_omits_default_libraries_for_compile_only_and_freestanding(self):
+ wrapper = self.install_wrapper("x86_64-linux-musl-cc", "cc.sh")
+ capture = wrapper.parent.parent / "libexec/clang"
+ capture.write_text(CAPTURE)
+ capture.chmod(0o755)
+ for option in ("-c", "-E", "-nostdlib", "-nodefaultlibs"):
+ with self.subTest(option=option):
+ invocation = json.loads(subprocess.check_output([str(wrapper), option, "source.c"], env=self.env, text=True))
+ self.assertNotIn("-latomic", invocation["args"])
+
+ def test_sysroot_override_applies_to_compiler_and_pkg_config(self):
+ wrapper = self.install_wrapper("x86_64-linux-musl-cc", "cc.sh")
+ capture = wrapper.parent.parent / "libexec/clang"
+ capture.write_text(CAPTURE)
+ capture.chmod(0o755)
+ env = {**self.env, "MUSLLVM_SYSROOT": "/another/sysroot"}
+ invocation = json.loads(subprocess.check_output([str(wrapper), "source.c"], env=env, text=True))
+ self.assertIn("--sysroot=/another/sysroot", invocation["args"])
+ pkg_wrapper = self.install_wrapper("x86_64-linux-musl-pkg-config", "pkg-config.sh")
+ capture = self.tools / "pkg-config"
+ capture.write_text(CAPTURE)
+ capture.chmod(0o755)
+ invocation = json.loads(subprocess.check_output([str(pkg_wrapper), "--libs", "target"], env=env, text=True))
+ self.assertEqual(invocation["env"]["PKG_CONFIG_SYSROOT_DIR"], "/another/sysroot")
+
+ def test_environment_output_is_safe_to_evaluate(self):
+ wrapper = self.install_wrapper("x86_64-linux-musl-env", "env.sh")
+ result = subprocess.check_output(["sh", "-c", 'eval "$("$1")"; printf "%s\\n%s\\n" "$CC" "$CMAKE_TOOLCHAIN_FILE"',
+ "test", str(wrapper)], env=self.env, text=True)
+ self.assertEqual(result.splitlines(), [str(wrapper.parent / "x86_64-linux-musl-cc"),
+ str(wrapper.parent.parent / "share/musllvm/toolchain.cmake")])
+
+ def test_pkg_config_cannot_see_host_packages(self):
+ wrapper = self.install_wrapper("x86_64-linux-musl-pkg-config", "pkg-config.sh")
+ capture = self.tools / "pkg-config"
+ capture.write_text(CAPTURE)
+ capture.chmod(0o755)
+ env = {**self.env, "PKG_CONFIG_PATH": "/host/packages", "PKG_CONFIG_LIBDIR": "/host/packages"}
+ invocation = json.loads(subprocess.check_output([str(wrapper), "--libs", "target"], env=env, text=True))
+ self.assertEqual(invocation["env"]["PKG_CONFIG_PATH"], "")
+ self.assertEqual(invocation["env"]["PKG_CONFIG_SYSROOT_DIR"], str(wrapper.parent.parent / "sysroot"))
+ self.assertNotIn("/host/packages", invocation["env"]["PKG_CONFIG_LIBDIR"])
+
+
+if __name__ == "__main__":
+ unittest.main()