musllvm

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

commit 7838e7676676f4cd0b1dbf2989e6a75c0c562601
parent c59348710ff5127cf7c8eb3d4477cd6a618894ad
Author: cowmonk <cowmonk@based.pt>
Date:   Thu, 15 Oct 2026 08:54:29 -0700

Move driver flags into a clang config file

The wrapper passed link flags on compile-only runs, so -Werror builds
failed on unused argument warnings. Clang does not warn about config
file options, and the config adds the Chimera hardening defaults in both
LLVM modes. Host default configs are ignored.

Build libc++ with typeinfo comparison 2 as Chimera does.

verify now covers -Werror compiles, static-pie, worker thread exceptions,
a static CMake target, hardening flags and the default sysroot.
CI folds the source job into the cross matrix and shares one source
cache with releases. publish.sh only creates a release when gh reports
it missing.

Diffstat:
M.github/workflows/ci.yml | 25++++---------------------
M.github/workflows/release.yml | 10++++------
MREADME.md | 30+++++++++++++++++-------------
Mfiles/toolchain/cc.sh | 8+++-----
Afiles/toolchain/clang.cfg | 13+++++++++++++
Mscripts/build.sh | 4++++
Mscripts/publish.sh | 7+++++--
Mscripts/verify.sh | 49++++++++++++++++++++++++++++++-------------------
Mtests/cmake/CMakeLists.txt | 12+++++++-----
Mtests/hello.cpp | 6+++++-
Mtests/test_driver.py | 39+++++++++++++--------------------------
Mtests/test_release.py | 6+++++-
12 files changed, 110 insertions(+), 99 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml @@ -22,11 +22,14 @@ jobs: cross: runs-on: ubuntu-24.04 - timeout-minutes: 90 + timeout-minutes: ${{ inputs.source_llvm && 360 || 90 }} strategy: fail-fast: false matrix: arch: [x86_64, aarch64] + env: + ARCH: ${{ matrix.arch }} + LLVM_MODE: ${{ inputs.source_llvm && 'source' || 'system' }} steps: - uses: actions/checkout@v4 - name: Install host dependencies @@ -39,8 +42,6 @@ jobs: 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 @@ -48,8 +49,6 @@ jobs: make JOBS=2 fi - name: Check a resumed build - env: - ARCH: ${{ matrix.arch }} run: make runtimes JOBS=2 - name: Save build diagnostics if: failure() @@ -62,19 +61,3 @@ jobs: 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/.github/workflows/release.yml b/.github/workflows/release.yml @@ -8,8 +8,8 @@ on: - config.mk - Makefile - scripts/** + - '!scripts/test.sh' - files/** - - patches/** - tests/hello.* - tests/cmake/** - .github/workflows/release.yml @@ -51,17 +51,15 @@ jobs: echo /usr/lib/llvm-19/bin >> "$GITHUB_PATH" - uses: actions/cache@v4 with: - path: | - sources/*.tar.gz - sources/*.tar.xz - key: release-sources-${{ hashFiles('sources.list') }} + path: sources + key: sources-${{ hashFiles('sources.list') }} - name: Build and package run: | if [ "$ARCH" = aarch64 ]; then echo RUNNER=qemu-aarch64 >> "$GITHUB_ENV" export RUNNER=qemu-aarch64 fi - make JOBS=2 LINK_JOBS=1 + make runtimes JOBS=2 LINK_JOBS=1 make package - name: Verify the unpacked toolchain run: | diff --git a/README.md b/README.md @@ -104,8 +104,14 @@ 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 +The compiler wrappers select the target, sysroot, LLD and target runtimes +through share/musllvm/clang.cfg. They remove host header and library +environment paths and ignore host Clang configuration files. + +The configuration also enables the Chimera Linux hardening defaults: +-fstack-protector-strong, -fno-semantic-interposition, --as-needed and +-z relro -z now. Command-line flags such as -fno-stack-protector, +-Wl,--no-as-needed or -Wl,-z,lazy override them. The pkg-config wrapper searches the target sysroot. For CMake: @@ -142,9 +148,6 @@ 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 ====== @@ -154,23 +157,24 @@ checks 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. +A completed build also runs make verify. This compiles static, static-pie +and dynamic C and C++ programs, directly and through the CMake toolchain +file, and checks their ELF architecture and runtime dependencies. It also +compiles with -Werror. 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. +A manual workflow run can build both targets with LLVM from source instead. publishing ========== [The release workflow](.github/workflows/release.yml) runs on pushes to -main that change sources.list, config.mk, Makefile, scripts/, files/, -patches/ or the compiler smoke tests. This covers source version updates +main that change sources.list, config.mk, Makefile, scripts/, files/ +or the compiler smoke tests. This covers source version updates and new core components added to the build. README edits do not publish a release. The workflow can also be run manually from main. @@ -191,7 +195,7 @@ The first release still needs verification on GitHub Actions. credits ======== -* [Chimera Linux](https://chimera-linux.org) - original patches and libatomic +* [Chimera Linux](https://chimera-linux.org) - libatomic and the libc++ typeinfo setting * [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/files/toolchain/cc.sh b/files/toolchain/cc.sh @@ -14,9 +14,7 @@ done [ "$link" = no ] || set -- "$@" -latomic case "${0##*/}" in *++|*-c++) - set -- --driver-mode=g++ -stdlib=libc++ -nostdinc++ -isystem "$sysroot/usr/include/c++/v1" "$@" ;; + set -- --driver-mode=g++ -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" "$@" +exec "$prefix/libexec/clang" --no-default-config --config="$prefix/share/musllvm/clang.cfg" \ + --sysroot="$sysroot" "$@" diff --git a/files/toolchain/clang.cfg b/files/toolchain/clang.cfg @@ -0,0 +1,13 @@ +# clang never warns about unused config options, so -Werror builds keep working +--target=@TARGET@ +--gcc-toolchain=<CFGDIR>/empty-gcc +-resource-dir=<CFGDIR>/../../lib/clang/@RESOURCE_VERSION@ +--rtlib=compiler-rt +--unwindlib=libunwind +-stdlib=libc++ +--ld-path=<CFGDIR>/../../libexec/ld.lld +# chimera linux hardening defaults +-fstack-protector-strong +-fno-semantic-interposition +-Wl,--as-needed +-Wl,-z,relro,-z,now diff --git a/scripts/build.sh b/scripts/build.sh @@ -195,9 +195,11 @@ runtimes() if ! done_stage runtimes; then # musl lacks __cxa_thread_atexit_impl # compile only probes cannot detect missing symbols + # typeinfo comparison 2 also compares names, as libstdc++ does (chimera) 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_TYPEINFO_COMPARISON_IMPLEMENTATION=2 \ -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 \ @@ -231,6 +233,8 @@ wrappers() done sed -e "s/@TARGET@/$TARGET/g" -e "s/@ARCH@/$ARCH/g" \ "$PROOT/files/toolchain/toolchain.cmake" > "$PREFIX/share/musllvm/toolchain.cmake" + sed -e "s/@TARGET@/$TARGET/g" -e "s/@RESOURCE_VERSION@/$RESOURCE_VERSION/g" \ + "$PROOT/files/toolchain/clang.cfg" > "$PREFIX/share/musllvm/clang.cfg" printf '%s\n' "$RESOURCE_VERSION" > "$PREFIX/share/musllvm/resource-version" printf 'Toolchain ready: %s\n' "$PREFIX" } diff --git a/scripts/publish.sh b/scripts/publish.sh @@ -38,14 +38,17 @@ cp "$root/sources.list" sources.list printf '\n%s\n' 'Verify downloads with sha256sum --ignore-missing -c SHA256SUMS.' } > notes.md -if draft=$(gh release view "$tag" --json isDraft --jq .isDraft); then +if draft=$(gh release view "$tag" --json isDraft --jq .isDraft 2> view.err); then if [ "$draft" = false ]; then printf 'release already published: %s\n' "$tag" exit fi gh release upload "$tag" "$@" SHA256SUMS sources.list --clobber -else +elif grep -F 'release not found' view.err >/dev/null; then gh release create "$tag" "$@" SHA256SUMS sources.list --draft \ --target "$GITHUB_SHA" --title "musllvm $date" --notes-file notes.md +else + cat view.err >&2 + exit 1 fi gh release edit "$tag" --notes-file notes.md --draft=false --latest diff --git a/scripts/verify.sh b/scripts/verify.sh @@ -2,8 +2,7 @@ set -eu . "$(dirname "$0")/config.sh" -MUSLLVM_SYSROOT=$SYSROOT -export MUSLLVM_SYSROOT +unset MUSLLVM_SYSROOT cc="$PREFIX/bin/$TARGET-cc" cxx="$PREFIX/bin/$TARGET-c++" readelf="$PREFIX/bin/$TARGET-readelf" @@ -12,13 +11,28 @@ loader="$SYSROOT/lib/ld-musl-$ARCH.so.1" for program in "$cc" "$cxx" "$readelf"; do [ -x "$program" ] || die "toolchain is incomplete: $program" done + +run() +{ + if [ -n "$RUNNER" ]; then "$RUNNER" "$@"; else "$@"; fi +} + mkdir -p "$work" +"$cc" -std=c99 -Werror -c "$PROOT/tests/hello.c" -o "$work/hello-c.o" +"$cxx" -std=c++20 -Werror -c "$PROOT/tests/hello.cpp" -o "$work/hello-cxx.o" "$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" +"$cxx" -std=c++20 -O2 -pthread -static-pie "$PROOT/tests/hello.cpp" -o "$work/hello-cxx-static-pie" + +cmake -S "$PROOT/tests/cmake" -B "$work/cmake" -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$PREFIX/share/musllvm/toolchain.cmake" +cmake --build "$work/cmake" --parallel "$JOBS" -for program in hello-c hello-c-static hello-cxx hello-cxx-static; do +static='hello-c-static hello-cxx-static hello-cxx-static-pie cmake/hello-cxx-static' +dynamic='hello-c hello-cxx cmake/hello-c cmake/hello-cxx' +for program in $static $dynamic; 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" @@ -26,16 +40,21 @@ for program in hello-c hello-c-static hello-cxx hello-cxx-static; do 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) + case " $static " in + *" $program "*) 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" ;; + die "wrong dynamic loader in $program" + grep -F GNU_RELRO "$work/$program.segments" >/dev/null && + grep -E 'BIND_NOW|FLAGS.* NOW' "$work/$program.dynamic" >/dev/null || + die "hardening defaults missing from $program" ;; esac done +grep -E 'Type: +DYN' "$work/hello-cxx-static-pie.header" >/dev/null || + die "static-pie executable is not position independent" for language in c c++; do compiler=$cc @@ -47,23 +66,15 @@ for language in c c++; do 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 + for program in $static; do + run "$work/$program" 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 + for program in $dynamic; do + run "$loader" --library-path "$SYSROOT/usr/lib:$SYSROOT/lib" "$work/$program" 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" +printf 'Verified %s: static, static-pie and dynamic C and C++, -Werror compiles, target ELF, isolated headers, and CMake\n' "$TARGET" diff --git a/tests/cmake/CMakeLists.txt b/tests/cmake/CMakeLists.txt @@ -2,9 +2,11 @@ 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) +add_executable(hello-cxx-static ../hello.cpp) +set_target_properties(hello-cxx hello-cxx-static PROPERTIES CXX_STANDARD 20) +target_link_options(hello-cxx-static PRIVATE -static) +foreach(target hello-c hello-cxx hello-cxx-static) + target_compile_options(${target} PRIVATE -pthread) + target_link_options(${target} PRIVATE -pthread) +endforeach() diff --git a/tests/hello.cpp b/tests/hello.cpp @@ -24,7 +24,11 @@ main() std::thread worker([&]() { thread_local thread_state state{destroyed}; - total.store(std::accumulate(values.begin(), values.end(), 0)); + try { + throw std::runtime_error("worker"); + } catch (const std::runtime_error &) { + total.store(std::accumulate(values.begin(), values.end(), 0)); + } }); worker.join(); diff --git a/tests/test_driver.py b/tests/test_driver.py @@ -56,7 +56,6 @@ elif name == "curl": 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") @@ -168,6 +167,10 @@ class DriverTests(unittest.TestCase): def commands(self): return [json.loads(line) for line in self.log.read_text().splitlines()] + def install_capture(self, path): + path.write_text(CAPTURE) + path.chmod(0o755) + 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) @@ -182,7 +185,8 @@ class DriverTests(unittest.TestCase): 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.assertIn(f"PREFIX={self.root}/build/aarch64-linux-musl\n", result.stdout) + self.assertIn(f"BUILD_DIR={self.root}/build/work/aarch64-linux-musl\n", result.stdout) self.assertFalse((self.root / "build").exists()) def test_invalid_configuration_fails_before_work(self): @@ -238,13 +242,6 @@ class DriverTests(unittest.TestCase): 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() @@ -261,14 +258,12 @@ class DriverTests(unittest.TestCase): 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) + self.install_capture(wrapper.parent.parent / "libexec/clang") 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) + result = subprocess.check_output([str(wrapper), "source with spaces.cpp"], env=env, text=True) invocation = json.loads(result) self.assertIn("--driver-mode=g++", invocation["args"]) - self.assertIn("-stdlib=libc++", invocation["args"]) + self.assertIn(f"--config={wrapper.parent.parent}/share/musllvm/clang.cfg", 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"]) @@ -276,9 +271,7 @@ class DriverTests(unittest.TestCase): 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) + self.install_capture(wrapper.parent.parent / "libexec/clang") 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)) @@ -286,16 +279,12 @@ class DriverTests(unittest.TestCase): 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) + self.install_capture(wrapper.parent.parent / "libexec/clang") 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) + self.install_capture(self.tools / "pkg-config") 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") @@ -308,9 +297,7 @@ class DriverTests(unittest.TestCase): 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) + self.install_capture(self.tools / "pkg-config") 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"], "") diff --git a/tests/test_release.py b/tests/test_release.py @@ -32,7 +32,7 @@ if action == os.environ.get("FAKE_GH_FAIL"): if action == "view": state = os.environ.get("FAKE_RELEASE", "missing") if state == "missing": - sys.exit(1) + sys.exit("release not found") print("true" if state == "draft" else "false") elif action in ("create", "upload"): for name in ("SHA256SUMS", "sources.list"): @@ -224,6 +224,10 @@ class PublishTests(unittest.TestCase): calls = self.publish(success=False, FAKE_RELEASE=state, FAKE_GH_FAIL=action) self.assertNotIn("edit", [call[1] for call in calls]) + def test_failed_release_lookup_creates_nothing(self): + calls = self.publish(success=False, FAKE_GH_FAIL="view") + self.assertEqual([call[1] for call in calls], ["view"]) + def test_wrong_checkout_prevents_publication(self): self.assertEqual(self.publish(success=False, FAKE_GIT_SHA="2" * 40), [])