musllvm

musl and LLVM cross-toolchain build system
git clone https://git.daat.foo/repos/musllvm.git
Log | Files | Refs | README | LICENSE

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:
A.github/workflows/ci.yml | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MMakefile | 77++++++++++++++++++++++++++++++++++++++++-------------------------------------
MREADME.md | 174++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mconfig.mk | 31++++++++++++++++---------------
Afiles/toolchain/cc.sh | 22++++++++++++++++++++++
Afiles/toolchain/env.sh | 27+++++++++++++++++++++++++++
Afiles/toolchain/ld.sh | 4++++
Afiles/toolchain/pkg-config.sh | 9+++++++++
Afiles/toolchain/toolchain.cmake | 18++++++++++++++++++
Ascripts/build.sh | 311+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/check.sh | 37++++++++++++++++++++++++++-----------
Mscripts/config.sh | 154++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Dscripts/elftoolchain-cross.sh | 42------------------------------------------
Mscripts/headers.sh | 39+--------------------------------------
Mscripts/musl-cross.sh | 37+------------------------------------
Ascripts/test.sh | 7+++++++
Ascripts/verify.sh | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msources.list | 21+++++----------------
Atests/cmake/CMakeLists.txt | 10++++++++++
Atests/hello.c | 32++++++++++++++++++++++++++++++++
Atests/hello.cpp | 29+++++++++++++++++++++++++++++
Atests/test_driver.py | 322+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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()