From 1ef22ae4c11ce624b74d361cf152dc2057714516 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 09:20:40 +0800 Subject: [PATCH 01/28] chore: use umbrella targets for LLVM distribution components Replace the 95-entry manual component list with 8 umbrella targets (llvm-libraries, clang-libraries, etc.) and add cmake-exports so the LLVM install includes CMake config files for find_package support. Add skip_clice_build input to build-llvm workflow to allow building LLVM without requiring clice to compile (useful when upgrading LLVM with API changes). --- .github/workflows/build-llvm.yml | 31 +++--- scripts/build-llvm.py | 14 ++- scripts/llvm-components.json | 99 ----------------- scripts/validate-llvm-components.py | 163 ---------------------------- 4 files changed, 25 insertions(+), 282 deletions(-) delete mode 100644 scripts/llvm-components.json delete mode 100755 scripts/validate-llvm-components.py diff --git a/.github/workflows/build-llvm.yml b/.github/workflows/build-llvm.yml index 914a8131..24897046 100644 --- a/.github/workflows/build-llvm.yml +++ b/.github/workflows/build-llvm.yml @@ -17,6 +17,11 @@ on: required: false type: boolean default: false + skip_clice_build: + description: "Skip building and testing clice (LLVM-only build)" + required: false + type: boolean + default: false pull_request: # if you want to run this workflow, change the branch name to main, # if you want to turn off it, change it to non existent branch. @@ -128,13 +133,6 @@ jobs: echo "Cloning LLVM ${VERSION}..." git clone --branch "llvmorg-${VERSION}" --depth 1 https://github.com/llvm/llvm-project.git .llvm - - name: Validate distribution components - shell: bash - run: | - python3 scripts/validate-llvm-components.py \ - --llvm-src=.llvm \ - --components-file=scripts/llvm-components.json - - name: Build LLVM (install-distribution) shell: bash run: | @@ -151,7 +149,7 @@ jobs: ${EXTRA_ARGS} - name: Build clice using installed LLVM - if: ${{ !matrix.target_triple }} + if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} shell: bash run: | pixi run cmake-config ${{ matrix.llvm_mode }} ON -- \ @@ -160,7 +158,7 @@ jobs: pixi run cmake-build ${{ matrix.llvm_mode }} - name: Build clice using installed LLVM (cross-compile) - if: ${{ matrix.target_triple }} + if: ${{ matrix.target_triple && !inputs.skip_clice_build }} shell: bash run: | ENV="${{ matrix.pixi_env || 'package' }}" @@ -171,7 +169,7 @@ jobs: pixi run -e "$ENV" cmake-build ${{ matrix.llvm_mode }} - name: Verify cross-compiled binary architecture - if: ${{ matrix.target_triple && runner.os != 'Windows' }} + if: ${{ matrix.target_triple && runner.os != 'Windows' && !inputs.skip_clice_build }} shell: bash run: | BINARY="build/${{ matrix.llvm_mode }}/bin/clice" @@ -183,7 +181,7 @@ jobs: esac - name: Upload cross-compiled clice for functional test - if: ${{ matrix.target_triple && matrix.lto == 'OFF' }} + if: ${{ matrix.target_triple && matrix.lto == 'OFF' && !inputs.skip_clice_build }} uses: actions/upload-artifact@v4 with: name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} @@ -194,14 +192,14 @@ jobs: retention-days: 1 - name: Run tests - if: ${{ !matrix.target_triple }} + if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} shell: bash run: pixi run test ${{ matrix.llvm_mode }} # Prune is only supported for native builds (requires linking clice to test). # Cross-compiled targets reuse the native prune manifest of the same OS. - name: Prune LLVM static libraries (Debug/RelWithDebInfo no LTO) - if: (!matrix.target_triple) && (matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF')) + if: (!inputs.skip_clice_build) && (!matrix.target_triple) && (matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF')) shell: bash run: | MANIFEST="pruned-libs-${{ matrix.os }}.json" @@ -215,7 +213,7 @@ jobs: --manifest "${MANIFEST}" - name: Upload pruned-libs manifest - if: (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF' + if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF' uses: actions/upload-artifact@v4 with: name: llvm-pruned-libs-${{ matrix.os }} @@ -224,7 +222,7 @@ jobs: compression-level: 0 - name: Apply pruned-libs manifest (RelWithDebInfo + LTO, native only) - if: (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON' + if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON' shell: bash env: GH_TOKEN: ${{ github.token }} @@ -244,7 +242,7 @@ jobs: # For cross-compiled LTO builds, apply the native prune manifest. # The unused library set is arch-independent (same API surface). - name: Apply pruned-libs manifest (cross-compile + LTO) - if: matrix.target_triple && matrix.lto == 'ON' + if: (!inputs.skip_clice_build) && matrix.target_triple && matrix.lto == 'ON' shell: bash env: GH_TOKEN: ${{ github.token }} @@ -316,6 +314,7 @@ jobs: test-cross: needs: build + if: ${{ !inputs.skip_clice_build }} strategy: fail-fast: false matrix: diff --git a/scripts/build-llvm.py b/scripts/build-llvm.py index f769d906..f8ccf07a 100644 --- a/scripts/build-llvm.py +++ b/scripts/build-llvm.py @@ -4,7 +4,6 @@ import shutil import argparse import os -import json from pathlib import Path @@ -157,9 +156,16 @@ def main(): print(f"Toolchain: {toolchain_file}") print("---------------------") - components_path = Path(__file__).resolve().parent / "llvm-components.json" - with components_path.open() as f: - llvm_distribution_components = json.load(f)["components"] + llvm_distribution_components = [ + "llvm-libraries", + "clang-libraries", + "llvm-headers", + "clang-headers", + "clang-tidy-headers", + "clang-resource-headers", + "cmake-exports", + "clang-cmake-exports", + ] components_joined = ";".join(llvm_distribution_components) cmake_args = [ diff --git a/scripts/llvm-components.json b/scripts/llvm-components.json deleted file mode 100644 index 6578f04f..00000000 --- a/scripts/llvm-components.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "components": [ - "LLVMDemangle", - "LLVMSupport", - "LLVMCore", - "LLVMOption", - "LLVMBinaryFormat", - "LLVMMC", - "LLVMMCParser", - "LLVMObject", - "LLVMProfileData", - "LLVMBitReader", - "LLVMBitstreamReader", - "LLVMRemarks", - "LLVMObjectYAML", - "LLVMAggressiveInstCombine", - "LLVMInstCombine", - "LLVMIRReader", - "LLVMTextAPI", - "LLVMSymbolize", - "LLVMDebugInfoDWARF", - "LLVMDebugInfoDWARFLowLevel", - "LLVMDebugInfoCodeView", - "LLVMDebugInfoGSYM", - "LLVMDebugInfoPDB", - "LLVMDebugInfoBTF", - "LLVMDebugInfoMSF", - "LLVMAsmParser", - "LLVMTargetParser", - "LLVMTransformUtils", - "LLVMAnalysis", - "LLVMScalarOpts", - "LLVMFrontendHLSL", - "LLVMFrontendOpenMP", - "LLVMFrontendOffloading", - "LLVMFrontendAtomic", - "LLVMFrontendDirective", - "LLVMWindowsDriver", - "clangIndex", - "clangAPINotes", - "clangAST", - "clangASTMatchers", - "clangBasic", - "clangDriver", - "clangFormat", - "clangFrontend", - "clangLex", - "clangParse", - "clangSema", - "clangSerialization", - "clangRewrite", - "clangAnalysis", - "clangEdit", - "clangSupport", - "clangStaticAnalyzerCore", - "clangStaticAnalyzerFrontend", - "clangTidy", - "clangTidyUtils", - "clangTidyAndroidModule", - "clangTidyAbseilModule", - "clangTidyAlteraModule", - "clangTidyBoostModule", - "clangTidyBugproneModule", - "clangTidyCERTModule", - "clangTidyConcurrencyModule", - "clangTidyCppCoreGuidelinesModule", - "clangTidyDarwinModule", - "clangTidyFuchsiaModule", - "clangTidyGoogleModule", - "clangTidyHICPPModule", - "clangTidyLinuxKernelModule", - "clangTidyLLVMModule", - "clangTidyLLVMLibcModule", - "clangTidyMiscModule", - "clangTidyModernizeModule", - "clangTidyObjCModule", - "clangTidyOpenMPModule", - "clangTidyPerformanceModule", - "clangTidyPortabilityModule", - "clangTidyReadabilityModule", - "clangTidyZirconModule", - "clangTooling", - "clangToolingCore", - "clangToolingInclusions", - "clangToolingInclusionsStdlib", - "clangToolingSyntax", - "clangToolingRefactoring", - "clangTransformer", - "clangCrossTU", - "clangAnalysisFlowSensitive", - "clangAnalysisFlowSensitiveModels", - "clangStaticAnalyzerCheckers", - "clangIncludeCleaner", - "llvm-headers", - "clang-headers", - "clang-tidy-headers", - "clang-resource-headers" - ] -} diff --git a/scripts/validate-llvm-components.py b/scripts/validate-llvm-components.py deleted file mode 100755 index 26c804ff..00000000 --- a/scripts/validate-llvm-components.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 -""" -Validate the LLVM distribution component list against the actual LLVM source tree. - -Scans the LLVM source for CMake library targets and compares them against -a components JSON file to detect stale or misspelled entries. -""" - -import argparse -import difflib -import json -import re -import sys -from pathlib import Path - - -# CMake function calls that define library targets. -# The captured group uses [^\s)]+ to grab the target name without -# trailing parentheses or whitespace. -LLVM_LIB_PATTERNS = [ - re.compile(r"add_llvm_component_library\(\s*([^\s)]+)"), - re.compile(r"add_llvm_library\(\s*([^\s)]+)"), -] - -CLANG_LIB_PATTERNS = [ - re.compile(r"add_clang_library\(\s*([^\s)]+)"), -] - -# Header-only / custom install targets. -HEADER_PATTERNS = [ - re.compile(r"add_llvm_install_targets\(\s*([^\s)]+)"), - re.compile(r"add_custom_target\(\s*([^\s)]+)"), - re.compile(r"add_library\(\s*([^\s)]+)"), -] - -# Targets we recognise as header-only distribution components. -KNOWN_HEADER_TARGETS = { - "llvm-headers", - "clang-headers", - "clang-tidy-headers", - "clang-resource-headers", -} - - -def scan_targets(directory: Path, patterns: list[re.Pattern]) -> set[str]: - """Recursively scan *directory* for CMakeLists.txt files and extract target names.""" - targets: set[str] = set() - if not directory.is_dir(): - return targets - for cmake_file in directory.rglob("CMakeLists.txt"): - text = cmake_file.read_text(errors="replace") - for pattern in patterns: - for match in pattern.finditer(text): - targets.add(match.group(1)) - return targets - - -def scan_header_targets(llvm_src: Path) -> set[str]: - """Scan for well-known header / custom-install targets across the tree.""" - found: set[str] = set() - for cmake_file in llvm_src.rglob("CMakeLists.txt"): - text = cmake_file.read_text(errors="replace") - for pattern in HEADER_PATTERNS: - for match in pattern.finditer(text): - name = match.group(1) - if name in KNOWN_HEADER_TARGETS: - found.add(name) - return found - - -def collect_source_targets(llvm_src: Path) -> set[str]: - """Return the full set of library / header targets found in the LLVM source tree.""" - targets: set[str] = set() - targets |= scan_targets(llvm_src / "llvm" / "lib", LLVM_LIB_PATTERNS) - targets |= scan_targets(llvm_src / "clang" / "lib", CLANG_LIB_PATTERNS) - targets |= scan_targets(llvm_src / "clang-tools-extra", CLANG_LIB_PATTERNS) - targets |= scan_header_targets(llvm_src) - return targets - - -def load_components(path: Path) -> list[str]: - with path.open("r", encoding="utf-8") as handle: - data = json.load(handle) - if isinstance(data, dict): - data = data.get("components", []) - if not isinstance(data, list) or not data: - print(f"Error: no component list found in {path}", file=sys.stderr) - sys.exit(1) - return data - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Validate LLVM distribution components against the source tree." - ) - parser.add_argument( - "--llvm-src", - required=True, - help="Path to the llvm-project source root", - ) - parser.add_argument( - "--components-file", - required=True, - help="Path to llvm-components.json", - ) - args = parser.parse_args() - - llvm_src = Path(args.llvm_src).expanduser().resolve() - components_file = Path(args.components_file).expanduser().resolve() - - if not llvm_src.is_dir(): - print(f"Error: LLVM source directory not found: {llvm_src}") - sys.exit(1) - - if not (llvm_src / "llvm" / "CMakeLists.txt").exists(): - print(f"Error: {llvm_src} does not look like an llvm-project root.") - sys.exit(1) - - if not components_file.is_file(): - print(f"Error: components file not found: {components_file}") - sys.exit(1) - - components = load_components(components_file) - source_targets = collect_source_targets(llvm_src) - - print(f"Found {len(source_targets)} targets in LLVM source tree") - print(f"Components file lists {len(components)} entries") - - # Check for components that are missing from the source tree. - missing: list[tuple[str, list[str]]] = [] - for name in components: - if name not in source_targets: - suggestions = difflib.get_close_matches( - name, source_targets, n=3, cutoff=0.6 - ) - missing.append((name, suggestions)) - - if missing: - print(f"\nError: {len(missing)} component(s) not found in the source tree:\n") - for name, suggestions in missing: - print(f" - {name}") - if suggestions: - print(f" Did you mean: {', '.join(suggestions)}?") - sys.exit(1) - - # Warn about source targets not present in the component list. - component_set = set(components) - new_targets = sorted(source_targets - component_set - KNOWN_HEADER_TARGETS) - # Filter to targets that follow LLVM/Clang naming conventions to reduce noise. - noteworthy = [t for t in new_targets if t.startswith(("LLVM", "clang", "Clang"))] - if noteworthy: - print( - f"\nWarning: {len(noteworthy)} target(s) in source not listed in components:" - ) - for name in noteworthy: - print(f" + {name}") - - print("\nAll components validated successfully.") - sys.exit(0) - - -if __name__ == "__main__": - main() From 93b531021a81783b6d5bde093b35ef861e264b06 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 11:31:11 +0800 Subject: [PATCH 02/28] fix: reduce LLVM_TARGETS_TO_BUILD to X86;AArch64;ARM;RISCV clice is a language server and does not need all 20+ codegen backends. Keep targets that have tablegen-generated resource headers (ARM/AArch64 for arm_neon.h etc, RISCV for riscv_vector.h) plus X86 for desktops. --- scripts/build-llvm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-llvm.py b/scripts/build-llvm.py index f8ccf07a..77c7dea3 100644 --- a/scripts/build-llvm.py +++ b/scripts/build-llvm.py @@ -229,7 +229,7 @@ def main(): "-DLLVM_PARALLEL_LINK_JOBS=1", "-DCMAKE_JOB_POOL_LINK=console", "-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra", - "-DLLVM_TARGETS_TO_BUILD=all", + "-DLLVM_TARGETS_TO_BUILD=X86;AArch64;ARM;RISCV", "-DLLVM_DISABLE_ASSEMBLY_FILES=ON", # Distribution f"-DLLVM_DISTRIBUTION_COMPONENTS={components_joined}", From 7f612d9fbdc2694f5077b96c4f9bafdcf999291a Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 11:43:33 +0800 Subject: [PATCH 03/28] fix: free disk space on macOS runners before LLVM build Remove iOS/tvOS/watchOS/visionOS simulators, unused Xcode platforms, Android SDK, .NET, PowerShell, and Haskell to reclaim disk space. Revert LLVM_TARGETS_TO_BUILD back to all. --- .github/workflows/build-llvm.yml | 35 ++++++++++++++++++++++++++++++++ scripts/build-llvm.py | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-llvm.yml b/.github/workflows/build-llvm.yml index 24897046..14cfa6e9 100644 --- a/.github/workflows/build-llvm.yml +++ b/.github/workflows/build-llvm.yml @@ -126,6 +126,41 @@ jobs: with: environments: ${{ matrix.pixi_env || 'package' }} + - name: Free Disk Space (macOS) + if: runner.os == 'macOS' + run: | + echo "=== Before cleanup ===" + df -h / + + # Remove iOS/tvOS/watchOS/visionOS simulators + if [ -d "/Library/Developer/CoreSimulator" ]; then + sudo rm -rf /Library/Developer/CoreSimulator + echo "Removed CoreSimulator" + fi + + # Remove unnecessary Xcode platforms (keep macOS only) + XCODE_PATH="$(xcode-select -p)" + PLATFORMS_DIR="${XCODE_PATH}/Platforms" + for platform in iPhoneOS.platform iPhoneSimulator.platform AppleTVOS.platform AppleTVSimulator.platform WatchOS.platform WatchSimulator.platform XROS.platform XRSimulator.platform; do + if [ -d "${PLATFORMS_DIR}/${platform}" ]; then + sudo rm -rf "${PLATFORMS_DIR}/${platform}" + echo "Removed ${platform}" + fi + done + + # Remove Android SDK + sudo rm -rf ~/Library/Android + + # Remove .NET/PowerShell + sudo rm -rf /usr/local/share/dotnet + sudo rm -rf /usr/local/share/powershell + + # Remove Haskell + sudo rm -rf ~/.ghcup + + echo "=== After cleanup ===" + df -h / + - name: Clone llvm-project shell: bash run: | diff --git a/scripts/build-llvm.py b/scripts/build-llvm.py index 77c7dea3..f8ccf07a 100644 --- a/scripts/build-llvm.py +++ b/scripts/build-llvm.py @@ -229,7 +229,7 @@ def main(): "-DLLVM_PARALLEL_LINK_JOBS=1", "-DCMAKE_JOB_POOL_LINK=console", "-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra", - "-DLLVM_TARGETS_TO_BUILD=X86;AArch64;ARM;RISCV", + "-DLLVM_TARGETS_TO_BUILD=all", "-DLLVM_DISABLE_ASSEMBLY_FILES=ON", # Distribution f"-DLLVM_DISTRIBUTION_COMPONENTS={components_joined}", From d734725725e39668e17b47201ca41f6c0dc63503 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 11:49:47 +0800 Subject: [PATCH 04/28] ci: add temporary workflow to test macOS disk cleanup --- .github/workflows/test-macos-cleanup.yml | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/test-macos-cleanup.yml diff --git a/.github/workflows/test-macos-cleanup.yml b/.github/workflows/test-macos-cleanup.yml new file mode 100644 index 00000000..5723ed36 --- /dev/null +++ b/.github/workflows/test-macos-cleanup.yml @@ -0,0 +1,52 @@ +name: test macOS cleanup + +on: + workflow_dispatch: + +jobs: + test-cleanup: + runs-on: macos-15 + steps: + - name: Disk usage before cleanup + run: | + echo "=== Overall ===" + df -h / + echo "" + echo "=== Large directories ===" + du -sh /Library/Developer/CoreSimulator 2>/dev/null || echo "CoreSimulator: not found" + du -sh /Applications/Xcode*.app 2>/dev/null || echo "Xcode apps: not found" + XCODE_PATH="$(xcode-select -p)" + PLATFORMS_DIR="${XCODE_PATH}/Platforms" + for p in iPhoneOS.platform iPhoneSimulator.platform AppleTVOS.platform AppleTVSimulator.platform WatchOS.platform WatchSimulator.platform XROS.platform XRSimulator.platform; do + du -sh "${PLATFORMS_DIR}/${p}" 2>/dev/null || echo "${p}: not found" + done + du -sh ~/Library/Android 2>/dev/null || echo "Android SDK: not found" + du -sh /usr/local/share/dotnet 2>/dev/null || echo ".NET: not found" + du -sh /usr/local/share/powershell 2>/dev/null || echo "PowerShell: not found" + du -sh ~/.ghcup 2>/dev/null || echo "Haskell: not found" + + - name: Run cleanup + run: | + if [ -d "/Library/Developer/CoreSimulator" ]; then + sudo rm -rf /Library/Developer/CoreSimulator + echo "Removed CoreSimulator" + fi + + XCODE_PATH="$(xcode-select -p)" + PLATFORMS_DIR="${XCODE_PATH}/Platforms" + for platform in iPhoneOS.platform iPhoneSimulator.platform AppleTVOS.platform AppleTVSimulator.platform WatchOS.platform WatchSimulator.platform XROS.platform XRSimulator.platform; do + if [ -d "${PLATFORMS_DIR}/${platform}" ]; then + sudo rm -rf "${PLATFORMS_DIR}/${platform}" + echo "Removed ${platform}" + fi + done + + sudo rm -rf ~/Library/Android + sudo rm -rf /usr/local/share/dotnet + sudo rm -rf /usr/local/share/powershell + sudo rm -rf ~/.ghcup + + - name: Disk usage after cleanup + run: | + echo "=== Overall ===" + df -h / From fc7e2bc56476dd6de3b687c27e7afddcc34fb156 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 11:52:57 +0800 Subject: [PATCH 05/28] ci(temp): test macOS disk cleanup only --- .github/workflows/build-llvm.yml | 693 ++++++++++++++----------------- 1 file changed, 319 insertions(+), 374 deletions(-) diff --git a/.github/workflows/build-llvm.yml b/.github/workflows/build-llvm.yml index 14cfa6e9..50fd9352 100644 --- a/.github/workflows/build-llvm.yml +++ b/.github/workflows/build-llvm.yml @@ -33,67 +33,10 @@ jobs: fail-fast: false matrix: include: - # Native builds - - os: windows-2025 - llvm_mode: RelWithDebInfo - lto: OFF - - os: windows-2025 - llvm_mode: RelWithDebInfo - lto: ON - - os: ubuntu-24.04 - llvm_mode: Debug - lto: OFF - - os: ubuntu-24.04 - llvm_mode: RelWithDebInfo - lto: OFF - - os: ubuntu-24.04 - llvm_mode: RelWithDebInfo - lto: ON - - os: macos-15 - llvm_mode: Debug - lto: OFF - - os: macos-15 - llvm_mode: RelWithDebInfo - lto: OFF - os: macos-15 llvm_mode: RelWithDebInfo lto: ON - # Cross-compilation builds - # macOS x64 (from arm64 macos-15) - - os: macos-15 - llvm_mode: RelWithDebInfo - lto: OFF - target_triple: x86_64-apple-darwin - - os: macos-15 - llvm_mode: RelWithDebInfo - lto: ON - target_triple: x86_64-apple-darwin - - # Linux aarch64 (from x64 ubuntu-24.04) - - os: ubuntu-24.04 - llvm_mode: RelWithDebInfo - lto: OFF - target_triple: aarch64-linux-gnu - pixi_env: cross-linux-aarch64 - - os: ubuntu-24.04 - llvm_mode: RelWithDebInfo - lto: ON - target_triple: aarch64-linux-gnu - pixi_env: cross-linux-aarch64 - - # Windows arm64 (from x64 windows-2025) - - os: windows-2025 - llvm_mode: RelWithDebInfo - lto: OFF - target_triple: aarch64-pc-windows-msvc - pixi_env: cross-windows-arm64 - - os: windows-2025 - llvm_mode: RelWithDebInfo - lto: ON - target_triple: aarch64-pc-windows-msvc - pixi_env: cross-windows-arm64 - runs-on: ${{ matrix.os }} steps: - name: Checkout repository @@ -161,320 +104,322 @@ jobs: echo "=== After cleanup ===" df -h / - - name: Clone llvm-project - shell: bash - run: | - VERSION="${{ inputs.llvm_version || '21.1.8' }}" - echo "Cloning LLVM ${VERSION}..." - git clone --branch "llvmorg-${VERSION}" --depth 1 https://github.com/llvm/llvm-project.git .llvm - - - name: Build LLVM (install-distribution) - shell: bash - run: | - ENV="${{ matrix.pixi_env || 'package' }}" - EXTRA_ARGS="" - if [[ -n "${{ matrix.target_triple }}" ]]; then - EXTRA_ARGS="--target-triple=${{ matrix.target_triple }}" - fi - pixi run -e "$ENV" build-llvm \ - --llvm-src=.llvm \ - --mode="${{ matrix.llvm_mode }}" \ - --lto="${{ matrix.lto }}" \ - --build-dir=build \ - ${EXTRA_ARGS} - - - name: Build clice using installed LLVM - if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} - shell: bash - run: | - pixi run cmake-config ${{ matrix.llvm_mode }} ON -- \ - "-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \ - "-DLLVM_INSTALL_PATH=.llvm/build-install" - pixi run cmake-build ${{ matrix.llvm_mode }} - - - name: Build clice using installed LLVM (cross-compile) - if: ${{ matrix.target_triple && !inputs.skip_clice_build }} - shell: bash - run: | - ENV="${{ matrix.pixi_env || 'package' }}" - pixi run -e "$ENV" cmake-config ${{ matrix.llvm_mode }} ON -- \ - "-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \ - "-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}" \ - "-DLLVM_INSTALL_PATH=.llvm/build-install" - pixi run -e "$ENV" cmake-build ${{ matrix.llvm_mode }} - - - name: Verify cross-compiled binary architecture - if: ${{ matrix.target_triple && runner.os != 'Windows' && !inputs.skip_clice_build }} - shell: bash - run: | - BINARY="build/${{ matrix.llvm_mode }}/bin/clice" - echo "Binary info:" - file "$BINARY" - case "${{ matrix.target_triple }}" in - aarch64-linux-gnu) file "$BINARY" | grep -q "aarch64" ;; - x86_64-apple-darwin) file "$BINARY" | grep -q "x86_64" ;; - esac - - - name: Upload cross-compiled clice for functional test - if: ${{ matrix.target_triple && matrix.lto == 'OFF' && !inputs.skip_clice_build }} - uses: actions/upload-artifact@v4 - with: - name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} - path: | - build/${{ matrix.llvm_mode }}/bin/ - build/${{ matrix.llvm_mode }}/lib/ - if-no-files-found: error - retention-days: 1 - - - name: Run tests - if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} - shell: bash - run: pixi run test ${{ matrix.llvm_mode }} - - # Prune is only supported for native builds (requires linking clice to test). - # Cross-compiled targets reuse the native prune manifest of the same OS. - - name: Prune LLVM static libraries (Debug/RelWithDebInfo no LTO) - if: (!inputs.skip_clice_build) && (!matrix.target_triple) && (matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF')) - shell: bash - run: | - MANIFEST="pruned-libs-${{ matrix.os }}.json" - echo "LLVM_PRUNED_MANIFEST=${MANIFEST}" >> "${GITHUB_ENV}" - python3 scripts/prune-llvm-bin.py \ - --action discover \ - --install-dir ".llvm/build-install/lib" \ - --build-dir "build/${{ matrix.llvm_mode }}" \ - --max-attempts 60 \ - --sleep-seconds 60 \ - --manifest "${MANIFEST}" - - - name: Upload pruned-libs manifest - if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF' - uses: actions/upload-artifact@v4 - with: - name: llvm-pruned-libs-${{ matrix.os }} - path: ${{ env.LLVM_PRUNED_MANIFEST }} - if-no-files-found: error - compression-level: 0 - - - name: Apply pruned-libs manifest (RelWithDebInfo + LTO, native only) - if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON' - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - MANIFEST="pruned-libs-${{ matrix.os }}.json" - python3 scripts/prune-llvm-bin.py \ - --action apply \ - --manifest "${MANIFEST}" \ - --install-dir ".llvm/build-install/lib" \ - --build-dir "build/${{ matrix.llvm_mode }}" \ - --gh-run-id "${{ github.run_id }}" \ - --gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \ - --gh-download-dir "artifacts" \ - --max-attempts 60 \ - --sleep-seconds 60 - - # For cross-compiled LTO builds, apply the native prune manifest. - # The unused library set is arch-independent (same API surface). - - name: Apply pruned-libs manifest (cross-compile + LTO) - if: (!inputs.skip_clice_build) && matrix.target_triple && matrix.lto == 'ON' - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - MANIFEST="pruned-libs-${{ matrix.os }}.json" - python3 scripts/prune-llvm-bin.py \ - --action apply \ - --manifest "${MANIFEST}" \ - --install-dir ".llvm/build-install/lib" \ - --build-dir "build/${{ matrix.llvm_mode }}" \ - --gh-run-id "${{ github.run_id }}" \ - --gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \ - --gh-download-dir "artifacts" \ - --max-attempts 60 \ - --sleep-seconds 60 - - - name: Package LLVM install directory - shell: bash - run: | - MODE_TAG="releasedbg" - if [[ "${{ matrix.llvm_mode }}" == "Debug" ]]; then - MODE_TAG="debug" - fi - - # Determine arch/platform/toolchain from target triple or runner OS - if [[ -n "${{ matrix.target_triple }}" ]]; then - case "${{ matrix.target_triple }}" in - x86_64-apple-darwin) - ARCH="x64"; PLATFORM="macos"; TOOLCHAIN="clang" ;; - aarch64-linux-gnu) - ARCH="aarch64"; PLATFORM="linux"; TOOLCHAIN="gnu" ;; - aarch64-pc-windows-msvc) - ARCH="aarch64"; PLATFORM="windows"; TOOLCHAIN="msvc" ;; - esac - else - ARCH="x64" - PLATFORM="linux" - TOOLCHAIN="gnu" - if [[ "${{ matrix.os }}" == windows-* ]]; then - PLATFORM="windows" - TOOLCHAIN="msvc" - elif [[ "${{ matrix.os }}" == macos-* ]]; then - ARCH="arm64" - PLATFORM="macos" - TOOLCHAIN="clang" - fi - fi - - SUFFIX="" - if [[ "${{ matrix.lto }}" == "ON" ]]; then - SUFFIX="-lto" - fi - if [[ "${{ matrix.llvm_mode }}" == "Debug" && "${{ matrix.os }}" != windows-* ]]; then - SUFFIX="${SUFFIX}-asan" - fi - - ARCHIVE="${ARCH}-${PLATFORM}-${TOOLCHAIN}-${MODE_TAG}${SUFFIX}.tar.xz" - - set -eo pipefail - tar -C .llvm -cf - build-install | xz -T0 -9 -c > "${ARCHIVE}" - echo "LLVM_INSTALL_ARCHIVE=${ARCHIVE}" >> "${GITHUB_ENV}" - - - name: Upload LLVM install artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ env.LLVM_INSTALL_ARCHIVE }} - path: ${{ env.LLVM_INSTALL_ARCHIVE }} - if-no-files-found: error - - test-cross: - needs: build - if: ${{ !inputs.skip_clice_build }} - strategy: - fail-fast: false - matrix: - include: - - os: macos-15-intel - llvm_mode: RelWithDebInfo - target_triple: x86_64-apple-darwin - - os: ubuntu-24.04-arm - llvm_mode: RelWithDebInfo - target_triple: aarch64-linux-gnu - - os: windows-11-arm - llvm_mode: RelWithDebInfo - target_triple: aarch64-pc-windows-msvc - runs-on: ${{ matrix.os }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: ./.github/actions/setup-pixi - with: - environments: test-run - - - name: Download cross-compiled clice - uses: actions/download-artifact@v4 - with: - name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} - path: build/${{ matrix.llvm_mode }}/ - - - name: Make binaries executable - if: runner.os != 'Windows' - run: chmod +x build/${{ matrix.llvm_mode }}/bin/* - - - name: Run tests - run: pixi run -e test-run test ${{ matrix.llvm_mode }} - - upload: - needs: build - if: ${{ !cancelled() && inputs.llvm_version && !inputs.skip_upload }} - runs-on: ubuntu-24.04 - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - - name: Download all build artifacts - env: - GH_TOKEN: ${{ github.token }} - run: scripts/download-llvm.sh "${{ github.run_id }}" - - - name: Upload to clice-llvm - env: - GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} - TARGET_REPO: clice-io/clice-llvm - run: python3 scripts/upload-llvm.py "${{ inputs.llvm_version }}" "${TARGET_REPO}" "${{ github.run_id }}" - - - name: Save manifest for update-clice job - uses: actions/upload-artifact@v4 - with: - name: llvm-manifest-final - path: artifacts/llvm-manifest.json - if-no-files-found: error - compression-level: 0 - - update-clice: - needs: upload - if: ${{ !inputs.skip_pr }} - runs-on: ubuntu-24.04 - permissions: - contents: write - pull-requests: write - steps: - - uses: actions/checkout@v4 - - - name: Download manifest - uses: actions/download-artifact@v4 - with: - name: llvm-manifest-final - path: . - - - name: Update manifest and version - run: | - python3 scripts/update-llvm-version.py \ - --version "${{ inputs.llvm_version }}" \ - --manifest-src llvm-manifest.json \ - --manifest-dest config/llvm-manifest.json \ - --package-cmake cmake/package.cmake - - - name: Create or update PR - env: - GH_TOKEN: ${{ github.token }} - run: | - VERSION="${{ inputs.llvm_version }}" - BRANCH="chore/update-llvm-${VERSION}" - RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - RELEASE_URL="https://github.com/clice-io/clice-llvm/releases/tag/${VERSION}" - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git checkout -b "${BRANCH}" - git add config/llvm-manifest.json cmake/package.cmake - git commit -m "chore: update LLVM to ${VERSION}" - git push --force-with-lease origin "${BRANCH}" - - # Check if PR already exists for this branch - EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number // empty') - - BODY="$(cat < Auto-generated by build-llvm workflow - EOF - )" - - if [[ -n "${EXISTING_PR}" ]]; then - echo "Updating existing PR #${EXISTING_PR}" - gh pr edit "${EXISTING_PR}" --body "${BODY}" - else - gh pr create \ - --title "chore: update LLVM to ${VERSION}" \ - --body "${BODY}" \ - --base main - fi + # --- TEMPORARILY COMMENTED OUT: testing macOS disk cleanup only --- + # - name: Clone llvm-project + # shell: bash + # run: | + # VERSION="${{ inputs.llvm_version || '21.1.8' }}" + # echo "Cloning LLVM ${VERSION}..." + # git clone --branch "llvmorg-${VERSION}" --depth 1 https://github.com/llvm/llvm-project.git .llvm + # + # - name: Build LLVM (install-distribution) + # shell: bash + # run: | + # ENV="${{ matrix.pixi_env || 'package' }}" + # EXTRA_ARGS="" + # if [[ -n "${{ matrix.target_triple }}" ]]; then + # EXTRA_ARGS="--target-triple=${{ matrix.target_triple }}" + # fi + # pixi run -e "$ENV" build-llvm \ + # --llvm-src=.llvm \ + # --mode="${{ matrix.llvm_mode }}" \ + # --lto="${{ matrix.lto }}" \ + # --build-dir=build \ + # ${EXTRA_ARGS} + # + # - name: Build clice using installed LLVM + # if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} + # shell: bash + # run: | + # pixi run cmake-config ${{ matrix.llvm_mode }} ON -- \ + # "-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \ + # "-DLLVM_INSTALL_PATH=.llvm/build-install" + # pixi run cmake-build ${{ matrix.llvm_mode }} + # + # - name: Build clice using installed LLVM (cross-compile) + # if: ${{ matrix.target_triple && !inputs.skip_clice_build }} + # shell: bash + # run: | + # ENV="${{ matrix.pixi_env || 'package' }}" + # pixi run -e "$ENV" cmake-config ${{ matrix.llvm_mode }} ON -- \ + # "-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \ + # "-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}" \ + # "-DLLVM_INSTALL_PATH=.llvm/build-install" + # pixi run -e "$ENV" cmake-build ${{ matrix.llvm_mode }} + # + # - name: Verify cross-compiled binary architecture + # if: ${{ matrix.target_triple && runner.os != 'Windows' && !inputs.skip_clice_build }} + # shell: bash + # run: | + # BINARY="build/${{ matrix.llvm_mode }}/bin/clice" + # echo "Binary info:" + # file "$BINARY" + # case "${{ matrix.target_triple }}" in + # aarch64-linux-gnu) file "$BINARY" | grep -q "aarch64" ;; + # x86_64-apple-darwin) file "$BINARY" | grep -q "x86_64" ;; + # esac + # + # - name: Upload cross-compiled clice for functional test + # if: ${{ matrix.target_triple && matrix.lto == 'OFF' && !inputs.skip_clice_build }} + # uses: actions/upload-artifact@v4 + # with: + # name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} + # path: | + # build/${{ matrix.llvm_mode }}/bin/ + # build/${{ matrix.llvm_mode }}/lib/ + # if-no-files-found: error + # retention-days: 1 + # + # - name: Run tests + # if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} + # shell: bash + # run: pixi run test ${{ matrix.llvm_mode }} + # + # # Prune is only supported for native builds (requires linking clice to test). + # # Cross-compiled targets reuse the native prune manifest of the same OS. + # - name: Prune LLVM static libraries (Debug/RelWithDebInfo no LTO) + # if: (!inputs.skip_clice_build) && (!matrix.target_triple) && (matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF')) + # shell: bash + # run: | + # MANIFEST="pruned-libs-${{ matrix.os }}.json" + # echo "LLVM_PRUNED_MANIFEST=${MANIFEST}" >> "${GITHUB_ENV}" + # python3 scripts/prune-llvm-bin.py \ + # --action discover \ + # --install-dir ".llvm/build-install/lib" \ + # --build-dir "build/${{ matrix.llvm_mode }}" \ + # --max-attempts 60 \ + # --sleep-seconds 60 \ + # --manifest "${MANIFEST}" + # + # - name: Upload pruned-libs manifest + # if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF' + # uses: actions/upload-artifact@v4 + # with: + # name: llvm-pruned-libs-${{ matrix.os }} + # path: ${{ env.LLVM_PRUNED_MANIFEST }} + # if-no-files-found: error + # compression-level: 0 + # + # - name: Apply pruned-libs manifest (RelWithDebInfo + LTO, native only) + # if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON' + # shell: bash + # env: + # GH_TOKEN: ${{ github.token }} + # run: | + # MANIFEST="pruned-libs-${{ matrix.os }}.json" + # python3 scripts/prune-llvm-bin.py \ + # --action apply \ + # --manifest "${MANIFEST}" \ + # --install-dir ".llvm/build-install/lib" \ + # --build-dir "build/${{ matrix.llvm_mode }}" \ + # --gh-run-id "${{ github.run_id }}" \ + # --gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \ + # --gh-download-dir "artifacts" \ + # --max-attempts 60 \ + # --sleep-seconds 60 + # + # # For cross-compiled LTO builds, apply the native prune manifest. + # # The unused library set is arch-independent (same API surface). + # - name: Apply pruned-libs manifest (cross-compile + LTO) + # if: (!inputs.skip_clice_build) && matrix.target_triple && matrix.lto == 'ON' + # shell: bash + # env: + # GH_TOKEN: ${{ github.token }} + # run: | + # MANIFEST="pruned-libs-${{ matrix.os }}.json" + # python3 scripts/prune-llvm-bin.py \ + # --action apply \ + # --manifest "${MANIFEST}" \ + # --install-dir ".llvm/build-install/lib" \ + # --build-dir "build/${{ matrix.llvm_mode }}" \ + # --gh-run-id "${{ github.run_id }}" \ + # --gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \ + # --gh-download-dir "artifacts" \ + # --max-attempts 60 \ + # --sleep-seconds 60 + # + # - name: Package LLVM install directory + # shell: bash + # run: | + # MODE_TAG="releasedbg" + # if [[ "${{ matrix.llvm_mode }}" == "Debug" ]]; then + # MODE_TAG="debug" + # fi + # + # # Determine arch/platform/toolchain from target triple or runner OS + # if [[ -n "${{ matrix.target_triple }}" ]]; then + # case "${{ matrix.target_triple }}" in + # x86_64-apple-darwin) + # ARCH="x64"; PLATFORM="macos"; TOOLCHAIN="clang" ;; + # aarch64-linux-gnu) + # ARCH="aarch64"; PLATFORM="linux"; TOOLCHAIN="gnu" ;; + # aarch64-pc-windows-msvc) + # ARCH="aarch64"; PLATFORM="windows"; TOOLCHAIN="msvc" ;; + # esac + # else + # ARCH="x64" + # PLATFORM="linux" + # TOOLCHAIN="gnu" + # if [[ "${{ matrix.os }}" == windows-* ]]; then + # PLATFORM="windows" + # TOOLCHAIN="msvc" + # elif [[ "${{ matrix.os }}" == macos-* ]]; then + # ARCH="arm64" + # PLATFORM="macos" + # TOOLCHAIN="clang" + # fi + # fi + # + # SUFFIX="" + # if [[ "${{ matrix.lto }}" == "ON" ]]; then + # SUFFIX="-lto" + # fi + # if [[ "${{ matrix.llvm_mode }}" == "Debug" && "${{ matrix.os }}" != windows-* ]]; then + # SUFFIX="${SUFFIX}-asan" + # fi + # + # ARCHIVE="${ARCH}-${PLATFORM}-${TOOLCHAIN}-${MODE_TAG}${SUFFIX}.tar.xz" + # + # set -eo pipefail + # tar -C .llvm -cf - build-install | xz -T0 -9 -c > "${ARCHIVE}" + # echo "LLVM_INSTALL_ARCHIVE=${ARCHIVE}" >> "${GITHUB_ENV}" + # + # - name: Upload LLVM install artifact + # uses: actions/upload-artifact@v4 + # with: + # name: ${{ env.LLVM_INSTALL_ARCHIVE }} + # path: ${{ env.LLVM_INSTALL_ARCHIVE }} + # if-no-files-found: error + + # --- TEMPORARILY COMMENTED OUT: testing macOS disk cleanup only --- + # test-cross: + # needs: build + # if: ${{ !inputs.skip_clice_build }} + # strategy: + # fail-fast: false + # matrix: + # include: + # - os: macos-15-intel + # llvm_mode: RelWithDebInfo + # target_triple: x86_64-apple-darwin + # - os: ubuntu-24.04-arm + # llvm_mode: RelWithDebInfo + # target_triple: aarch64-linux-gnu + # - os: windows-11-arm + # llvm_mode: RelWithDebInfo + # target_triple: aarch64-pc-windows-msvc + # runs-on: ${{ matrix.os }} + # steps: + # - name: Checkout repository + # uses: actions/checkout@v4 + # + # - uses: ./.github/actions/setup-pixi + # with: + # environments: test-run + # + # - name: Download cross-compiled clice + # uses: actions/download-artifact@v4 + # with: + # name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} + # path: build/${{ matrix.llvm_mode }}/ + # + # - name: Make binaries executable + # if: runner.os != 'Windows' + # run: chmod +x build/${{ matrix.llvm_mode }}/bin/* + # + # - name: Run tests + # run: pixi run -e test-run test ${{ matrix.llvm_mode }} + # + # upload: + # needs: build + # if: ${{ !cancelled() && inputs.llvm_version && !inputs.skip_upload }} + # runs-on: ubuntu-24.04 + # permissions: + # contents: read + # steps: + # - uses: actions/checkout@v4 + # + # - name: Download all build artifacts + # env: + # GH_TOKEN: ${{ github.token }} + # run: scripts/download-llvm.sh "${{ github.run_id }}" + # + # - name: Upload to clice-llvm + # env: + # GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} + # TARGET_REPO: clice-io/clice-llvm + # run: python3 scripts/upload-llvm.py "${{ inputs.llvm_version }}" "${TARGET_REPO}" "${{ github.run_id }}" + # + # - name: Save manifest for update-clice job + # uses: actions/upload-artifact@v4 + # with: + # name: llvm-manifest-final + # path: artifacts/llvm-manifest.json + # if-no-files-found: error + # compression-level: 0 + # + # update-clice: + # needs: upload + # if: ${{ !inputs.skip_pr }} + # runs-on: ubuntu-24.04 + # permissions: + # contents: write + # pull-requests: write + # steps: + # - uses: actions/checkout@v4 + # + # - name: Download manifest + # uses: actions/download-artifact@v4 + # with: + # name: llvm-manifest-final + # path: . + # + # - name: Update manifest and version + # run: | + # python3 scripts/update-llvm-version.py \ + # --version "${{ inputs.llvm_version }}" \ + # --manifest-src llvm-manifest.json \ + # --manifest-dest config/llvm-manifest.json \ + # --package-cmake cmake/package.cmake + # + # - name: Create or update PR + # env: + # GH_TOKEN: ${{ github.token }} + # run: | + # VERSION="${{ inputs.llvm_version }}" + # BRANCH="chore/update-llvm-${VERSION}" + # RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + # RELEASE_URL="https://github.com/clice-io/clice-llvm/releases/tag/${VERSION}" + # + # git config user.name "github-actions[bot]" + # git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # git checkout -b "${BRANCH}" + # git add config/llvm-manifest.json cmake/package.cmake + # git commit -m "chore: update LLVM to ${VERSION}" + # git push --force-with-lease origin "${BRANCH}" + # + # # Check if PR already exists for this branch + # EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number // empty') + # + # BODY="$(cat < Auto-generated by build-llvm workflow + # EOF + # )" + # + # if [[ -n "${EXISTING_PR}" ]]; then + # echo "Updating existing PR #${EXISTING_PR}" + # gh pr edit "${EXISTING_PR}" --body "${BODY}" + # else + # gh pr create \ + # --title "chore: update LLVM to ${VERSION}" \ + # --body "${BODY}" \ + # --base main + # fi From 99c8b0ef6e3a808e3fb72c96f2501020d59d5ed3 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 12:05:11 +0800 Subject: [PATCH 06/28] ci: restore full build matrix and remove test workflow Revert temporary cleanup-only test state, restore all 14 matrix entries with macOS disk cleanup included. Delete test-macos-cleanup.yml. --- .github/workflows/build-llvm.yml | 693 ++++++++++++----------- .github/workflows/test-macos-cleanup.yml | 52 -- 2 files changed, 374 insertions(+), 371 deletions(-) delete mode 100644 .github/workflows/test-macos-cleanup.yml diff --git a/.github/workflows/build-llvm.yml b/.github/workflows/build-llvm.yml index 50fd9352..14cfa6e9 100644 --- a/.github/workflows/build-llvm.yml +++ b/.github/workflows/build-llvm.yml @@ -33,10 +33,67 @@ jobs: fail-fast: false matrix: include: + # Native builds + - os: windows-2025 + llvm_mode: RelWithDebInfo + lto: OFF + - os: windows-2025 + llvm_mode: RelWithDebInfo + lto: ON + - os: ubuntu-24.04 + llvm_mode: Debug + lto: OFF + - os: ubuntu-24.04 + llvm_mode: RelWithDebInfo + lto: OFF + - os: ubuntu-24.04 + llvm_mode: RelWithDebInfo + lto: ON + - os: macos-15 + llvm_mode: Debug + lto: OFF + - os: macos-15 + llvm_mode: RelWithDebInfo + lto: OFF - os: macos-15 llvm_mode: RelWithDebInfo lto: ON + # Cross-compilation builds + # macOS x64 (from arm64 macos-15) + - os: macos-15 + llvm_mode: RelWithDebInfo + lto: OFF + target_triple: x86_64-apple-darwin + - os: macos-15 + llvm_mode: RelWithDebInfo + lto: ON + target_triple: x86_64-apple-darwin + + # Linux aarch64 (from x64 ubuntu-24.04) + - os: ubuntu-24.04 + llvm_mode: RelWithDebInfo + lto: OFF + target_triple: aarch64-linux-gnu + pixi_env: cross-linux-aarch64 + - os: ubuntu-24.04 + llvm_mode: RelWithDebInfo + lto: ON + target_triple: aarch64-linux-gnu + pixi_env: cross-linux-aarch64 + + # Windows arm64 (from x64 windows-2025) + - os: windows-2025 + llvm_mode: RelWithDebInfo + lto: OFF + target_triple: aarch64-pc-windows-msvc + pixi_env: cross-windows-arm64 + - os: windows-2025 + llvm_mode: RelWithDebInfo + lto: ON + target_triple: aarch64-pc-windows-msvc + pixi_env: cross-windows-arm64 + runs-on: ${{ matrix.os }} steps: - name: Checkout repository @@ -104,322 +161,320 @@ jobs: echo "=== After cleanup ===" df -h / - # --- TEMPORARILY COMMENTED OUT: testing macOS disk cleanup only --- - # - name: Clone llvm-project - # shell: bash - # run: | - # VERSION="${{ inputs.llvm_version || '21.1.8' }}" - # echo "Cloning LLVM ${VERSION}..." - # git clone --branch "llvmorg-${VERSION}" --depth 1 https://github.com/llvm/llvm-project.git .llvm - # - # - name: Build LLVM (install-distribution) - # shell: bash - # run: | - # ENV="${{ matrix.pixi_env || 'package' }}" - # EXTRA_ARGS="" - # if [[ -n "${{ matrix.target_triple }}" ]]; then - # EXTRA_ARGS="--target-triple=${{ matrix.target_triple }}" - # fi - # pixi run -e "$ENV" build-llvm \ - # --llvm-src=.llvm \ - # --mode="${{ matrix.llvm_mode }}" \ - # --lto="${{ matrix.lto }}" \ - # --build-dir=build \ - # ${EXTRA_ARGS} - # - # - name: Build clice using installed LLVM - # if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} - # shell: bash - # run: | - # pixi run cmake-config ${{ matrix.llvm_mode }} ON -- \ - # "-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \ - # "-DLLVM_INSTALL_PATH=.llvm/build-install" - # pixi run cmake-build ${{ matrix.llvm_mode }} - # - # - name: Build clice using installed LLVM (cross-compile) - # if: ${{ matrix.target_triple && !inputs.skip_clice_build }} - # shell: bash - # run: | - # ENV="${{ matrix.pixi_env || 'package' }}" - # pixi run -e "$ENV" cmake-config ${{ matrix.llvm_mode }} ON -- \ - # "-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \ - # "-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}" \ - # "-DLLVM_INSTALL_PATH=.llvm/build-install" - # pixi run -e "$ENV" cmake-build ${{ matrix.llvm_mode }} - # - # - name: Verify cross-compiled binary architecture - # if: ${{ matrix.target_triple && runner.os != 'Windows' && !inputs.skip_clice_build }} - # shell: bash - # run: | - # BINARY="build/${{ matrix.llvm_mode }}/bin/clice" - # echo "Binary info:" - # file "$BINARY" - # case "${{ matrix.target_triple }}" in - # aarch64-linux-gnu) file "$BINARY" | grep -q "aarch64" ;; - # x86_64-apple-darwin) file "$BINARY" | grep -q "x86_64" ;; - # esac - # - # - name: Upload cross-compiled clice for functional test - # if: ${{ matrix.target_triple && matrix.lto == 'OFF' && !inputs.skip_clice_build }} - # uses: actions/upload-artifact@v4 - # with: - # name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} - # path: | - # build/${{ matrix.llvm_mode }}/bin/ - # build/${{ matrix.llvm_mode }}/lib/ - # if-no-files-found: error - # retention-days: 1 - # - # - name: Run tests - # if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} - # shell: bash - # run: pixi run test ${{ matrix.llvm_mode }} - # - # # Prune is only supported for native builds (requires linking clice to test). - # # Cross-compiled targets reuse the native prune manifest of the same OS. - # - name: Prune LLVM static libraries (Debug/RelWithDebInfo no LTO) - # if: (!inputs.skip_clice_build) && (!matrix.target_triple) && (matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF')) - # shell: bash - # run: | - # MANIFEST="pruned-libs-${{ matrix.os }}.json" - # echo "LLVM_PRUNED_MANIFEST=${MANIFEST}" >> "${GITHUB_ENV}" - # python3 scripts/prune-llvm-bin.py \ - # --action discover \ - # --install-dir ".llvm/build-install/lib" \ - # --build-dir "build/${{ matrix.llvm_mode }}" \ - # --max-attempts 60 \ - # --sleep-seconds 60 \ - # --manifest "${MANIFEST}" - # - # - name: Upload pruned-libs manifest - # if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF' - # uses: actions/upload-artifact@v4 - # with: - # name: llvm-pruned-libs-${{ matrix.os }} - # path: ${{ env.LLVM_PRUNED_MANIFEST }} - # if-no-files-found: error - # compression-level: 0 - # - # - name: Apply pruned-libs manifest (RelWithDebInfo + LTO, native only) - # if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON' - # shell: bash - # env: - # GH_TOKEN: ${{ github.token }} - # run: | - # MANIFEST="pruned-libs-${{ matrix.os }}.json" - # python3 scripts/prune-llvm-bin.py \ - # --action apply \ - # --manifest "${MANIFEST}" \ - # --install-dir ".llvm/build-install/lib" \ - # --build-dir "build/${{ matrix.llvm_mode }}" \ - # --gh-run-id "${{ github.run_id }}" \ - # --gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \ - # --gh-download-dir "artifacts" \ - # --max-attempts 60 \ - # --sleep-seconds 60 - # - # # For cross-compiled LTO builds, apply the native prune manifest. - # # The unused library set is arch-independent (same API surface). - # - name: Apply pruned-libs manifest (cross-compile + LTO) - # if: (!inputs.skip_clice_build) && matrix.target_triple && matrix.lto == 'ON' - # shell: bash - # env: - # GH_TOKEN: ${{ github.token }} - # run: | - # MANIFEST="pruned-libs-${{ matrix.os }}.json" - # python3 scripts/prune-llvm-bin.py \ - # --action apply \ - # --manifest "${MANIFEST}" \ - # --install-dir ".llvm/build-install/lib" \ - # --build-dir "build/${{ matrix.llvm_mode }}" \ - # --gh-run-id "${{ github.run_id }}" \ - # --gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \ - # --gh-download-dir "artifacts" \ - # --max-attempts 60 \ - # --sleep-seconds 60 - # - # - name: Package LLVM install directory - # shell: bash - # run: | - # MODE_TAG="releasedbg" - # if [[ "${{ matrix.llvm_mode }}" == "Debug" ]]; then - # MODE_TAG="debug" - # fi - # - # # Determine arch/platform/toolchain from target triple or runner OS - # if [[ -n "${{ matrix.target_triple }}" ]]; then - # case "${{ matrix.target_triple }}" in - # x86_64-apple-darwin) - # ARCH="x64"; PLATFORM="macos"; TOOLCHAIN="clang" ;; - # aarch64-linux-gnu) - # ARCH="aarch64"; PLATFORM="linux"; TOOLCHAIN="gnu" ;; - # aarch64-pc-windows-msvc) - # ARCH="aarch64"; PLATFORM="windows"; TOOLCHAIN="msvc" ;; - # esac - # else - # ARCH="x64" - # PLATFORM="linux" - # TOOLCHAIN="gnu" - # if [[ "${{ matrix.os }}" == windows-* ]]; then - # PLATFORM="windows" - # TOOLCHAIN="msvc" - # elif [[ "${{ matrix.os }}" == macos-* ]]; then - # ARCH="arm64" - # PLATFORM="macos" - # TOOLCHAIN="clang" - # fi - # fi - # - # SUFFIX="" - # if [[ "${{ matrix.lto }}" == "ON" ]]; then - # SUFFIX="-lto" - # fi - # if [[ "${{ matrix.llvm_mode }}" == "Debug" && "${{ matrix.os }}" != windows-* ]]; then - # SUFFIX="${SUFFIX}-asan" - # fi - # - # ARCHIVE="${ARCH}-${PLATFORM}-${TOOLCHAIN}-${MODE_TAG}${SUFFIX}.tar.xz" - # - # set -eo pipefail - # tar -C .llvm -cf - build-install | xz -T0 -9 -c > "${ARCHIVE}" - # echo "LLVM_INSTALL_ARCHIVE=${ARCHIVE}" >> "${GITHUB_ENV}" - # - # - name: Upload LLVM install artifact - # uses: actions/upload-artifact@v4 - # with: - # name: ${{ env.LLVM_INSTALL_ARCHIVE }} - # path: ${{ env.LLVM_INSTALL_ARCHIVE }} - # if-no-files-found: error - - # --- TEMPORARILY COMMENTED OUT: testing macOS disk cleanup only --- - # test-cross: - # needs: build - # if: ${{ !inputs.skip_clice_build }} - # strategy: - # fail-fast: false - # matrix: - # include: - # - os: macos-15-intel - # llvm_mode: RelWithDebInfo - # target_triple: x86_64-apple-darwin - # - os: ubuntu-24.04-arm - # llvm_mode: RelWithDebInfo - # target_triple: aarch64-linux-gnu - # - os: windows-11-arm - # llvm_mode: RelWithDebInfo - # target_triple: aarch64-pc-windows-msvc - # runs-on: ${{ matrix.os }} - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 - # - # - uses: ./.github/actions/setup-pixi - # with: - # environments: test-run - # - # - name: Download cross-compiled clice - # uses: actions/download-artifact@v4 - # with: - # name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} - # path: build/${{ matrix.llvm_mode }}/ - # - # - name: Make binaries executable - # if: runner.os != 'Windows' - # run: chmod +x build/${{ matrix.llvm_mode }}/bin/* - # - # - name: Run tests - # run: pixi run -e test-run test ${{ matrix.llvm_mode }} - # - # upload: - # needs: build - # if: ${{ !cancelled() && inputs.llvm_version && !inputs.skip_upload }} - # runs-on: ubuntu-24.04 - # permissions: - # contents: read - # steps: - # - uses: actions/checkout@v4 - # - # - name: Download all build artifacts - # env: - # GH_TOKEN: ${{ github.token }} - # run: scripts/download-llvm.sh "${{ github.run_id }}" - # - # - name: Upload to clice-llvm - # env: - # GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} - # TARGET_REPO: clice-io/clice-llvm - # run: python3 scripts/upload-llvm.py "${{ inputs.llvm_version }}" "${TARGET_REPO}" "${{ github.run_id }}" - # - # - name: Save manifest for update-clice job - # uses: actions/upload-artifact@v4 - # with: - # name: llvm-manifest-final - # path: artifacts/llvm-manifest.json - # if-no-files-found: error - # compression-level: 0 - # - # update-clice: - # needs: upload - # if: ${{ !inputs.skip_pr }} - # runs-on: ubuntu-24.04 - # permissions: - # contents: write - # pull-requests: write - # steps: - # - uses: actions/checkout@v4 - # - # - name: Download manifest - # uses: actions/download-artifact@v4 - # with: - # name: llvm-manifest-final - # path: . - # - # - name: Update manifest and version - # run: | - # python3 scripts/update-llvm-version.py \ - # --version "${{ inputs.llvm_version }}" \ - # --manifest-src llvm-manifest.json \ - # --manifest-dest config/llvm-manifest.json \ - # --package-cmake cmake/package.cmake - # - # - name: Create or update PR - # env: - # GH_TOKEN: ${{ github.token }} - # run: | - # VERSION="${{ inputs.llvm_version }}" - # BRANCH="chore/update-llvm-${VERSION}" - # RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - # RELEASE_URL="https://github.com/clice-io/clice-llvm/releases/tag/${VERSION}" - # - # git config user.name "github-actions[bot]" - # git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - # git checkout -b "${BRANCH}" - # git add config/llvm-manifest.json cmake/package.cmake - # git commit -m "chore: update LLVM to ${VERSION}" - # git push --force-with-lease origin "${BRANCH}" - # - # # Check if PR already exists for this branch - # EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number // empty') - # - # BODY="$(cat < Auto-generated by build-llvm workflow - # EOF - # )" - # - # if [[ -n "${EXISTING_PR}" ]]; then - # echo "Updating existing PR #${EXISTING_PR}" - # gh pr edit "${EXISTING_PR}" --body "${BODY}" - # else - # gh pr create \ - # --title "chore: update LLVM to ${VERSION}" \ - # --body "${BODY}" \ - # --base main - # fi + - name: Clone llvm-project + shell: bash + run: | + VERSION="${{ inputs.llvm_version || '21.1.8' }}" + echo "Cloning LLVM ${VERSION}..." + git clone --branch "llvmorg-${VERSION}" --depth 1 https://github.com/llvm/llvm-project.git .llvm + + - name: Build LLVM (install-distribution) + shell: bash + run: | + ENV="${{ matrix.pixi_env || 'package' }}" + EXTRA_ARGS="" + if [[ -n "${{ matrix.target_triple }}" ]]; then + EXTRA_ARGS="--target-triple=${{ matrix.target_triple }}" + fi + pixi run -e "$ENV" build-llvm \ + --llvm-src=.llvm \ + --mode="${{ matrix.llvm_mode }}" \ + --lto="${{ matrix.lto }}" \ + --build-dir=build \ + ${EXTRA_ARGS} + + - name: Build clice using installed LLVM + if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} + shell: bash + run: | + pixi run cmake-config ${{ matrix.llvm_mode }} ON -- \ + "-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \ + "-DLLVM_INSTALL_PATH=.llvm/build-install" + pixi run cmake-build ${{ matrix.llvm_mode }} + + - name: Build clice using installed LLVM (cross-compile) + if: ${{ matrix.target_triple && !inputs.skip_clice_build }} + shell: bash + run: | + ENV="${{ matrix.pixi_env || 'package' }}" + pixi run -e "$ENV" cmake-config ${{ matrix.llvm_mode }} ON -- \ + "-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \ + "-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}" \ + "-DLLVM_INSTALL_PATH=.llvm/build-install" + pixi run -e "$ENV" cmake-build ${{ matrix.llvm_mode }} + + - name: Verify cross-compiled binary architecture + if: ${{ matrix.target_triple && runner.os != 'Windows' && !inputs.skip_clice_build }} + shell: bash + run: | + BINARY="build/${{ matrix.llvm_mode }}/bin/clice" + echo "Binary info:" + file "$BINARY" + case "${{ matrix.target_triple }}" in + aarch64-linux-gnu) file "$BINARY" | grep -q "aarch64" ;; + x86_64-apple-darwin) file "$BINARY" | grep -q "x86_64" ;; + esac + + - name: Upload cross-compiled clice for functional test + if: ${{ matrix.target_triple && matrix.lto == 'OFF' && !inputs.skip_clice_build }} + uses: actions/upload-artifact@v4 + with: + name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} + path: | + build/${{ matrix.llvm_mode }}/bin/ + build/${{ matrix.llvm_mode }}/lib/ + if-no-files-found: error + retention-days: 1 + + - name: Run tests + if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} + shell: bash + run: pixi run test ${{ matrix.llvm_mode }} + + # Prune is only supported for native builds (requires linking clice to test). + # Cross-compiled targets reuse the native prune manifest of the same OS. + - name: Prune LLVM static libraries (Debug/RelWithDebInfo no LTO) + if: (!inputs.skip_clice_build) && (!matrix.target_triple) && (matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF')) + shell: bash + run: | + MANIFEST="pruned-libs-${{ matrix.os }}.json" + echo "LLVM_PRUNED_MANIFEST=${MANIFEST}" >> "${GITHUB_ENV}" + python3 scripts/prune-llvm-bin.py \ + --action discover \ + --install-dir ".llvm/build-install/lib" \ + --build-dir "build/${{ matrix.llvm_mode }}" \ + --max-attempts 60 \ + --sleep-seconds 60 \ + --manifest "${MANIFEST}" + + - name: Upload pruned-libs manifest + if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF' + uses: actions/upload-artifact@v4 + with: + name: llvm-pruned-libs-${{ matrix.os }} + path: ${{ env.LLVM_PRUNED_MANIFEST }} + if-no-files-found: error + compression-level: 0 + + - name: Apply pruned-libs manifest (RelWithDebInfo + LTO, native only) + if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + MANIFEST="pruned-libs-${{ matrix.os }}.json" + python3 scripts/prune-llvm-bin.py \ + --action apply \ + --manifest "${MANIFEST}" \ + --install-dir ".llvm/build-install/lib" \ + --build-dir "build/${{ matrix.llvm_mode }}" \ + --gh-run-id "${{ github.run_id }}" \ + --gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \ + --gh-download-dir "artifacts" \ + --max-attempts 60 \ + --sleep-seconds 60 + + # For cross-compiled LTO builds, apply the native prune manifest. + # The unused library set is arch-independent (same API surface). + - name: Apply pruned-libs manifest (cross-compile + LTO) + if: (!inputs.skip_clice_build) && matrix.target_triple && matrix.lto == 'ON' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + MANIFEST="pruned-libs-${{ matrix.os }}.json" + python3 scripts/prune-llvm-bin.py \ + --action apply \ + --manifest "${MANIFEST}" \ + --install-dir ".llvm/build-install/lib" \ + --build-dir "build/${{ matrix.llvm_mode }}" \ + --gh-run-id "${{ github.run_id }}" \ + --gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \ + --gh-download-dir "artifacts" \ + --max-attempts 60 \ + --sleep-seconds 60 + + - name: Package LLVM install directory + shell: bash + run: | + MODE_TAG="releasedbg" + if [[ "${{ matrix.llvm_mode }}" == "Debug" ]]; then + MODE_TAG="debug" + fi + + # Determine arch/platform/toolchain from target triple or runner OS + if [[ -n "${{ matrix.target_triple }}" ]]; then + case "${{ matrix.target_triple }}" in + x86_64-apple-darwin) + ARCH="x64"; PLATFORM="macos"; TOOLCHAIN="clang" ;; + aarch64-linux-gnu) + ARCH="aarch64"; PLATFORM="linux"; TOOLCHAIN="gnu" ;; + aarch64-pc-windows-msvc) + ARCH="aarch64"; PLATFORM="windows"; TOOLCHAIN="msvc" ;; + esac + else + ARCH="x64" + PLATFORM="linux" + TOOLCHAIN="gnu" + if [[ "${{ matrix.os }}" == windows-* ]]; then + PLATFORM="windows" + TOOLCHAIN="msvc" + elif [[ "${{ matrix.os }}" == macos-* ]]; then + ARCH="arm64" + PLATFORM="macos" + TOOLCHAIN="clang" + fi + fi + + SUFFIX="" + if [[ "${{ matrix.lto }}" == "ON" ]]; then + SUFFIX="-lto" + fi + if [[ "${{ matrix.llvm_mode }}" == "Debug" && "${{ matrix.os }}" != windows-* ]]; then + SUFFIX="${SUFFIX}-asan" + fi + + ARCHIVE="${ARCH}-${PLATFORM}-${TOOLCHAIN}-${MODE_TAG}${SUFFIX}.tar.xz" + + set -eo pipefail + tar -C .llvm -cf - build-install | xz -T0 -9 -c > "${ARCHIVE}" + echo "LLVM_INSTALL_ARCHIVE=${ARCHIVE}" >> "${GITHUB_ENV}" + + - name: Upload LLVM install artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.LLVM_INSTALL_ARCHIVE }} + path: ${{ env.LLVM_INSTALL_ARCHIVE }} + if-no-files-found: error + + test-cross: + needs: build + if: ${{ !inputs.skip_clice_build }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-15-intel + llvm_mode: RelWithDebInfo + target_triple: x86_64-apple-darwin + - os: ubuntu-24.04-arm + llvm_mode: RelWithDebInfo + target_triple: aarch64-linux-gnu + - os: windows-11-arm + llvm_mode: RelWithDebInfo + target_triple: aarch64-pc-windows-msvc + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-pixi + with: + environments: test-run + + - name: Download cross-compiled clice + uses: actions/download-artifact@v4 + with: + name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} + path: build/${{ matrix.llvm_mode }}/ + + - name: Make binaries executable + if: runner.os != 'Windows' + run: chmod +x build/${{ matrix.llvm_mode }}/bin/* + + - name: Run tests + run: pixi run -e test-run test ${{ matrix.llvm_mode }} + + upload: + needs: build + if: ${{ !cancelled() && inputs.llvm_version && !inputs.skip_upload }} + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Download all build artifacts + env: + GH_TOKEN: ${{ github.token }} + run: scripts/download-llvm.sh "${{ github.run_id }}" + + - name: Upload to clice-llvm + env: + GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} + TARGET_REPO: clice-io/clice-llvm + run: python3 scripts/upload-llvm.py "${{ inputs.llvm_version }}" "${TARGET_REPO}" "${{ github.run_id }}" + + - name: Save manifest for update-clice job + uses: actions/upload-artifact@v4 + with: + name: llvm-manifest-final + path: artifacts/llvm-manifest.json + if-no-files-found: error + compression-level: 0 + + update-clice: + needs: upload + if: ${{ !inputs.skip_pr }} + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - name: Download manifest + uses: actions/download-artifact@v4 + with: + name: llvm-manifest-final + path: . + + - name: Update manifest and version + run: | + python3 scripts/update-llvm-version.py \ + --version "${{ inputs.llvm_version }}" \ + --manifest-src llvm-manifest.json \ + --manifest-dest config/llvm-manifest.json \ + --package-cmake cmake/package.cmake + + - name: Create or update PR + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${{ inputs.llvm_version }}" + BRANCH="chore/update-llvm-${VERSION}" + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + RELEASE_URL="https://github.com/clice-io/clice-llvm/releases/tag/${VERSION}" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "${BRANCH}" + git add config/llvm-manifest.json cmake/package.cmake + git commit -m "chore: update LLVM to ${VERSION}" + git push --force-with-lease origin "${BRANCH}" + + # Check if PR already exists for this branch + EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number // empty') + + BODY="$(cat < Auto-generated by build-llvm workflow + EOF + )" + + if [[ -n "${EXISTING_PR}" ]]; then + echo "Updating existing PR #${EXISTING_PR}" + gh pr edit "${EXISTING_PR}" --body "${BODY}" + else + gh pr create \ + --title "chore: update LLVM to ${VERSION}" \ + --body "${BODY}" \ + --base main + fi diff --git a/.github/workflows/test-macos-cleanup.yml b/.github/workflows/test-macos-cleanup.yml deleted file mode 100644 index 5723ed36..00000000 --- a/.github/workflows/test-macos-cleanup.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: test macOS cleanup - -on: - workflow_dispatch: - -jobs: - test-cleanup: - runs-on: macos-15 - steps: - - name: Disk usage before cleanup - run: | - echo "=== Overall ===" - df -h / - echo "" - echo "=== Large directories ===" - du -sh /Library/Developer/CoreSimulator 2>/dev/null || echo "CoreSimulator: not found" - du -sh /Applications/Xcode*.app 2>/dev/null || echo "Xcode apps: not found" - XCODE_PATH="$(xcode-select -p)" - PLATFORMS_DIR="${XCODE_PATH}/Platforms" - for p in iPhoneOS.platform iPhoneSimulator.platform AppleTVOS.platform AppleTVSimulator.platform WatchOS.platform WatchSimulator.platform XROS.platform XRSimulator.platform; do - du -sh "${PLATFORMS_DIR}/${p}" 2>/dev/null || echo "${p}: not found" - done - du -sh ~/Library/Android 2>/dev/null || echo "Android SDK: not found" - du -sh /usr/local/share/dotnet 2>/dev/null || echo ".NET: not found" - du -sh /usr/local/share/powershell 2>/dev/null || echo "PowerShell: not found" - du -sh ~/.ghcup 2>/dev/null || echo "Haskell: not found" - - - name: Run cleanup - run: | - if [ -d "/Library/Developer/CoreSimulator" ]; then - sudo rm -rf /Library/Developer/CoreSimulator - echo "Removed CoreSimulator" - fi - - XCODE_PATH="$(xcode-select -p)" - PLATFORMS_DIR="${XCODE_PATH}/Platforms" - for platform in iPhoneOS.platform iPhoneSimulator.platform AppleTVOS.platform AppleTVSimulator.platform WatchOS.platform WatchSimulator.platform XROS.platform XRSimulator.platform; do - if [ -d "${PLATFORMS_DIR}/${platform}" ]; then - sudo rm -rf "${PLATFORMS_DIR}/${platform}" - echo "Removed ${platform}" - fi - done - - sudo rm -rf ~/Library/Android - sudo rm -rf /usr/local/share/dotnet - sudo rm -rf /usr/local/share/powershell - sudo rm -rf ~/.ghcup - - - name: Disk usage after cleanup - run: | - echo "=== Overall ===" - df -h / From c7e74628ad1a799ac8b68958f0f7d594501b53a7 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 18:47:59 +0800 Subject: [PATCH 07/28] chore: adapt codebase to LLVM 22.1.4 API changes Key breaking changes handled: - clang/Driver/Options.h moved to clang/Options/Options.h - NestedNameSpecifier changed from pointer to value type - ElaboratedType removed from Clang AST - DependentTemplateSpecializationType merged into TemplateSpecializationType - CompilerInstance::createDiagnostics/createFileManager API changed - TypedefTypeLoc::getTypedefNameDecl() renamed to getDecl() - TraverseTypeLoc gained TraverseQualifier parameter - llvm::sys::fs::make_absolute moved to llvm::sys::path --- cmake/package.cmake | 2 +- src/command/argument_parser.cpp | 12 +- src/command/search_config.cpp | 6 +- src/command/toolchain.cpp | 4 +- src/compile/compilation.cpp | 6 +- src/index/usr_generation.cpp | 9 +- src/semantic/ast_utility.cpp | 18 +- src/semantic/filtered_ast_visitor.h | 11 +- src/semantic/resolver.cpp | 244 ++++++++----------- src/semantic/resolver.h | 13 +- src/semantic/selection.cpp | 9 +- src/semantic/semantic_visitor.h | 45 +--- src/syntax/scan.cpp | 6 +- tests/unit/command/argument_parser_tests.cpp | 4 +- tests/unit/feature/inlay_hint_tests.cpp | 2 +- tests/unit/semantic/selection_tests.cpp | 8 +- 16 files changed, 166 insertions(+), 233 deletions(-) diff --git a/cmake/package.cmake b/cmake/package.cmake index f3ef7b95..3ed1169e 100644 --- a/cmake/package.cmake +++ b/cmake/package.cmake @@ -1,7 +1,7 @@ include_guard() include(${CMAKE_CURRENT_LIST_DIR}/llvm.cmake) -setup_llvm("21.1.8") +setup_llvm("22.1.4") # install dependencies include(FetchContent) diff --git a/src/command/argument_parser.cpp b/src/command/argument_parser.cpp index 55543b9f..447c39bb 100644 --- a/src/command/argument_parser.cpp +++ b/src/command/argument_parser.cpp @@ -8,15 +8,15 @@ #include "llvm/Support/Path.h" #include "llvm/Support/raw_ostream.h" #include "clang/Driver/Driver.h" -#include "clang/Driver/Options.h" #include "clang/Driver/Types.h" +#include "clang/Options/OptionUtils.h" +#include "clang/Options/Options.h" namespace clice { namespace { namespace opt = llvm::opt; -namespace driver = clang::driver; /// Access private members of OptTable via the Thief pattern. bool enable_dash_dash_parsing(const opt::OptTable& table); @@ -35,7 +35,7 @@ struct Thief { template struct Thief<&opt::OptTable::DashDashParsing, &opt::OptTable::GroupedShortOptions>; -auto& option_table = driver::getDriverOptTable(); +auto& option_table = clang::getDriverOptTable(); } // namespace @@ -45,7 +45,7 @@ std::unique_ptr ArgumentParser::parse_one(unsigned& index) { return option_table.ParseOneArg(*this, index, opt::Visibility(visibility_mask)); } -using ID = clang::driver::options::ID; +using ID = clang::options::ID; bool is_discarded_option(unsigned id) { switch(id) { @@ -165,7 +165,7 @@ llvm::StringRef resource_dir() { if(exe.empty()) { return std::string{}; } - return clang::driver::Driver::GetResourcesPath(exe); + return clang::GetResourcesPath(exe); }(); return dir; } @@ -246,7 +246,7 @@ std::string print_argv(llvm::ArrayRef args) { } unsigned default_visibility(llvm::StringRef driver) { - namespace options = clang::driver::options; + namespace options = clang::options; auto name = llvm::sys::path::filename(driver); name.consume_back(".exe"); diff --git a/src/command/search_config.cpp b/src/command/search_config.cpp index 34ea2140..0488d78e 100644 --- a/src/command/search_config.cpp +++ b/src/command/search_config.cpp @@ -6,11 +6,11 @@ #include "llvm/ADT/StringSet.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/Path.h" -#include "clang/Driver/Options.h" +#include "clang/Options/Options.h" namespace clice { -using ID = clang::driver::options::ID; +using ID = clang::options::ID; SearchConfig extract_search_config(llvm::ArrayRef arguments, llvm::StringRef directory) { @@ -26,7 +26,7 @@ SearchConfig extract_search_config(llvm::ArrayRef arguments, auto make_absolute = [&](llvm::StringRef path) -> std::string { llvm::SmallString<256> abs_path(path); if(!llvm::sys::path::is_absolute(abs_path)) { - llvm::sys::fs::make_absolute(directory, abs_path); + llvm::sys::path::make_absolute(directory, abs_path); } llvm::sys::path::remove_dots(abs_path, true); return abs_path.str().str(); diff --git a/src/command/toolchain.cpp b/src/command/toolchain.cpp index b9dbc9ec..b470b81e 100644 --- a/src/command/toolchain.cpp +++ b/src/command/toolchain.cpp @@ -18,8 +18,8 @@ #include "llvm/TargetParser/Host.h" #include "clang/Driver/Compilation.h" #include "clang/Driver/Driver.h" -#include "clang/Driver/Options.h" #include "clang/Driver/Tool.h" +#include "clang/Options/Options.h" #ifndef _WIN32 @@ -476,7 +476,7 @@ std::vector query_clang_toolchain(const QueryParams& params) { // producing cc1 flags we don't recognize. Filter them out here. // Long-term we should unify the command pipeline so the driver // version always matches the embedded LLVM. - auto& table = clang::driver::getDriverOptTable(); + auto& table = clang::getDriverOptTable(); auto cc1_args = llvm::ArrayRef(args).drop_front(2); unsigned missing_index = 0, missing_count = 0; auto parsed = table.ParseArgs(cc1_args, missing_index, missing_count); diff --git a/src/compile/compilation.cpp b/src/compile/compilation.cpp index ffc5bf38..ceff44a2 100644 --- a/src/compile/compilation.cpp +++ b/src/compile/compilation.cpp @@ -7,6 +7,7 @@ #include "support/logging.h" #include "llvm/Support/Error.h" +#include "clang/Driver/CreateInvocationFromArgs.h" #include "clang/Frontend/MultiplexConsumer.h" #include "clang/Frontend/TextDiagnosticPrinter.h" #include "clang/Lex/PreprocessorOptions.h" @@ -247,13 +248,14 @@ CompilationStatus CompilationUnitRef::Self::run_clang( self.instance = std::make_unique(std::move(invocation)); auto& instance = *self.instance; - instance.createDiagnostics(*params.vfs, diagnostic_consumer.release(), true); + instance.createDiagnostics(diagnostic_consumer.release(), true); if(auto remapping = clang::createVFSFromCompilerInvocation(instance.getInvocation(), instance.getDiagnostics(), params.vfs)) { - instance.createFileManager(std::move(remapping)); + instance.setVirtualFileSystem(std::move(remapping)); } + instance.createFileManager(); if(!instance.createTarget()) { return CompilationStatus::SetupFail; diff --git a/src/index/usr_generation.cpp b/src/index/usr_generation.cpp index 2812c237..c98acd68 100644 --- a/src/index/usr_generation.cpp +++ b/src/index/usr_generation.cpp @@ -504,14 +504,14 @@ bool USRGenerator::GenLoc(const Decl* D, bool IncludeOffset) { static void printQualifier(llvm::raw_ostream& Out, const LangOptions& LangOpts, - NestedNameSpecifier* NNS) { + NestedNameSpecifier NNS) { // FIXME: Encode the qualifier, don't just print it. PrintingPolicy PO(LangOpts); PO.SuppressTagKeyword = true; PO.SuppressUnwrittenScope = true; PO.ConstantArraySizeAsWritten = false; PO.AnonymousTagLocations = false; - NNS->print(Out, PO); + NNS.print(Out, PO); } void USRGenerator::VisitType(QualType T) { @@ -740,8 +740,9 @@ void USRGenerator::VisitType(QualType T) { return; } if(const InjectedClassNameType* InjT = T->getAs()) { - T = InjT->getInjectedSpecializationType(); - continue; + Out << '$'; + VisitTagDecl(InjT->getDecl()); + return; } if(const auto* VT = T->getAs()) { Out << (T->isExtVectorType() ? ']' : '['); diff --git a/src/semantic/ast_utility.cpp b/src/semantic/ast_utility.cpp index d09c6144..daa9c995 100644 --- a/src/semantic/ast_utility.cpp +++ b/src/semantic/ast_utility.cpp @@ -200,10 +200,6 @@ llvm::StringRef identifier_of(const clang::NamedDecl& D) { } llvm::StringRef identifier_of(clang::QualType type) { - if(const auto* ET = llvm::dyn_cast(type)) { - return identifier_of(ET->getNamedType()); - } - if(const auto* BT = llvm::dyn_cast(type)) { clang::PrintingPolicy PP(clang::LangOptions{}); PP.adjustForCPlusPlus(); @@ -312,12 +308,6 @@ auto decl_of(clang::QualType type) -> const clang::NamedDecl* { return nullptr; } - // Strip type-sugar that wraps the underlying type without adding a decl - // (e.g. ElaboratedType for "struct Foo" vs plain "Foo"). - if(auto ET = type->getAs()) { - type = ET->getNamedType(); - } - if(auto TST = type->getAs()) { auto decl = TST->getTemplateName().getAsTemplateDecl(); if(type->isDependentType()) { @@ -409,8 +399,8 @@ std::string display_name_of(const clang::NamedDecl* decl) { // Handle 'using namespace'. They all have the same name - . if(auto* UD = llvm::dyn_cast(decl)) { out << "using namespace "; - if(auto* Qual = UD->getQualifier()) - Qual->print(out, policy); + if(auto Qual = UD->getQualifier()) + Qual.print(out, policy); UD->getNominatedNamespaceAsWritten()->printName(out); return out.str(); } @@ -437,8 +427,8 @@ std::string display_name_of(const clang::NamedDecl* decl) { } // Print nested name qualifier if it was written in the source code. - if(auto* qualifier = get_qualifier_loc(decl).getNestedNameSpecifier()) { - qualifier->print(out, policy); + if(auto qualifier = get_qualifier_loc(decl).getNestedNameSpecifier()) { + qualifier.print(out, policy); } // Print the name itself. diff --git a/src/semantic/filtered_ast_visitor.h b/src/semantic/filtered_ast_visitor.h index 2f09035a..dfa8ab59 100644 --- a/src/semantic/filtered_ast_visitor.h +++ b/src/semantic/filtered_ast_visitor.h @@ -107,7 +107,7 @@ class FilteredASTVisitor : public clang::RecursiveASTVisitor { return true; } - bool TraverseTypeLoc(clang::TypeLoc loc) { + bool TraverseTypeLoc(clang::TypeLoc loc, bool TraverseQualifier = true) { CHECK_DERIVED_IMPL(TraverseTypeLoc); if(!loc) { @@ -116,10 +116,10 @@ class FilteredASTVisitor : public clang::RecursiveASTVisitor { /// FIXME: Workaround for `QualifiedTypeLoc`. if(auto QL = loc.getAs()) { - return Base::TraverseTypeLoc(QL.getUnqualifiedLoc()); + return Base::TraverseTypeLoc(QL.getUnqualifiedLoc(), TraverseQualifier); } - return Base::TraverseTypeLoc(loc); + return Base::TraverseTypeLoc(loc, TraverseQualifier); } bool TraverseAttr(clang::Attr* attr) { @@ -132,9 +132,8 @@ class FilteredASTVisitor : public clang::RecursiveASTVisitor { return Base::TraverseAttr(attr); } - /// We don't want to node withou location information. - constexpr bool TraverseNestedNameSpecifier - [[gnu::always_inline]] (clang::NestedNameSpecifier*) { + /// We don't want to node without location information. + constexpr bool TraverseNestedNameSpecifier [[gnu::always_inline]] (clang::NestedNameSpecifier) { CHECK_DERIVED_IMPL(TraverseNestedNameSpecifier); return true; } diff --git a/src/semantic/resolver.cpp b/src/semantic/resolver.cpp index 120e523d..829ded07 100644 --- a/src/semantic/resolver.cpp +++ b/src/semantic/resolver.cpp @@ -161,13 +161,13 @@ struct InstantiationStack { }; /// Helper to extract underlying type from a Decl. -static clang::QualType get_decl_type(clang::Decl* decl) { +static clang::QualType get_decl_type(clang::Decl* decl, clang::ASTContext& context) { if(!decl) return clang::QualType(); if(auto* TND = llvm::dyn_cast(decl)) return TND->getUnderlyingType(); if(auto* RD = llvm::dyn_cast(decl)) - return clang::QualType(RD->getTypeForDecl(), 0); + return context.getCanonicalTagType(RD); return clang::QualType(); } @@ -205,14 +205,13 @@ class SubstituteOnly : public clang::TreeTransform { /// Desugar dependent typedefs to expose template parameters for substitution. clang::QualType TransformTypedefType(clang::TypeLocBuilder& TLB, clang::TypedefTypeLoc TL) { - if(auto* TND = TL.getTypedefNameDecl()) { + if(auto* TND = TL.getDecl()) { auto underlying = TND->getUnderlyingType(); if(underlying->isDependentType()) { auto type = TransformType(underlying); if(!type.isNull()) { - if(auto ET = llvm::dyn_cast(type)) { - type = ET->getNamedType(); - } + // ElaboratedType was removed in LLVM 22; elaboration is now + // part of each type's own representation. TLB.pushTrivial(context, type, {}); return type; } @@ -221,25 +220,11 @@ class SubstituteOnly : public clang::TreeTransform { return Base::TransformTypedefType(TLB, TL); } - clang::QualType TransformElaboratedType(clang::TypeLocBuilder& TLB, - clang::ElaboratedTypeLoc TL) { - clang::QualType type = TransformType(TL.getNamedTypeLoc().getType()); - if(type.isNull()) { - return Base::TransformElaboratedType(TLB, TL); - } - TLB.pushTrivial(context, type, {}); - return type; - } - clang::QualType TransformInjectedClassNameType(clang::TypeLocBuilder& TLB, clang::InjectedClassNameTypeLoc TL) { - auto ICT = TL.getTypePtr(); - clang::QualType type = TransformType(ICT->getInjectedSpecializationType()); - if(type.isNull()) { - return Base::TransformInjectedClassNameType(TLB, TL); - } - TLB.pushTrivial(context, type, {}); - return type; + // In LLVM 22, InjectedClassNameType no longer has getInjectedSpecializationType. + // Just return the base transform. + return Base::TransformInjectedClassNameType(TLB, TL); } using Base::TransformTemplateSpecializationType; @@ -496,24 +481,25 @@ class PseudoInstantiator : public clang::TreeTransform { } if(auto TST = type->getAs()) { - TD = TST->getTemplateName().getAsTemplateDecl(); - args = TST->template_arguments(); - } else if(auto DTST = type->getAs()) { - // If this DTST was already resolved (possibly to itself when unresolvable), - // skip the redundant lookup. - if(resolved.count(DTST)) { - return lookup_result(); - } + auto TN = TST->getTemplateName(); + if(auto* DTN = TN.getAsDependentTemplateName()) { + // This was formerly DependentTemplateSpecializationType. + if(resolved.count(TST)) { + return lookup_result(); + } - auto& template_name = DTST->getDependentTemplateName(); - auto name = template_name.getName().getIdentifier(); - if(!name) { - return {}; - } + auto identifier = DTN->getName().getIdentifier(); + if(!identifier) { + return {}; + } - if(auto decl = preferred(lookup(template_name.getQualifier(), name))) { - TD = decl; - args = DTST->template_arguments(); + if(auto decl = preferred(lookup(DTN->getQualifier(), identifier))) { + TD = decl; + args = TST->template_arguments(); + } + } else { + TD = TN.getAsTemplateDecl(); + args = TST->template_arguments(); } } @@ -536,45 +522,27 @@ class PseudoInstantiator : public clang::TreeTransform { return lookup_result(); } - lookup_result lookup(const clang::NestedNameSpecifier* NNS, clang::DeclarationName name) { + lookup_result lookup(clang::NestedNameSpecifier NNS, clang::DeclarationName name) { if(!NNS) { return lookup_result(); } - if(auto iter = resolved.find(NNS); iter != resolved.end()) { + if(auto iter = resolved.find(NNS.getAsVoidPointer()); iter != resolved.end()) { return lookup(iter->second, name); } // Handle each NestedNameSpecifier kind: - // - Identifier: dependent name in NNS chain (e.g. `base::type::inner`), resolve recursively - // - TypeSpec: concrete or dependent type used as qualifier (e.g. `vector::`) - // - Global/Namespace/NamespaceAlias/Super: not dependent, cannot resolve further - switch(NNS->getKind()) { - case clang::NestedNameSpecifier::Identifier: { - auto stack_size = stack.data.size(); - auto* decl = preferred(lookup(NNS->getPrefix(), NNS->getAsIdentifier())); - auto type = get_decl_type(decl); - if(!type.isNull()) { - type = substitute(type); - } - while(stack.data.size() > stack_size) { - stack.pop(); - } - if(!type.isNull()) { - resolved.try_emplace(NNS, type); - return lookup(type, name); - } - return {}; + // - Type: concrete or dependent type used as qualifier (e.g. `vector::`) + // - Global/Namespace/MicrosoftSuper/Null: not dependent, cannot resolve further + switch(NNS.getKind()) { + case clang::NestedNameSpecifier::Kind::Type: { + return lookup(clang::QualType(NNS.getAsType(), 0), name); } - case clang::NestedNameSpecifier::TypeSpec: { - return lookup(clang::QualType(NNS->getAsType(), 0), name); - } - - case clang::NestedNameSpecifier::Global: - case clang::NestedNameSpecifier::Namespace: - case clang::NestedNameSpecifier::NamespaceAlias: - case clang::NestedNameSpecifier::Super: { + case clang::NestedNameSpecifier::Kind::Null: + case clang::NestedNameSpecifier::Kind::Global: + case clang::NestedNameSpecifier::Kind::Namespace: + case clang::NestedNameSpecifier::Kind::MicrosoftSuper: { return {}; } } @@ -708,14 +676,14 @@ class PseudoInstantiator : public clang::TreeTransform { /// /// TODO: Replace with a general mechanism for resolving well-known standard /// library patterns, or improve the resolver to handle these chains naturally. - clang::QualType hole(clang::NestedNameSpecifier* NNS, + clang::QualType hole(clang::NestedNameSpecifier NNS, const clang::IdentifierInfo* member, TemplateArguments arguments) { - if(NNS->getKind() != clang::NestedNameSpecifier::TypeSpec) { + if(NNS.getKind() != clang::NestedNameSpecifier::Kind::Type) { return clang::QualType(); } - auto TST = NNS->getAsType()->getAs(); + auto TST = NNS.getAsType()->getAs(); if(!TST) { return clang::QualType(); } @@ -738,17 +706,18 @@ class PseudoInstantiator : public clang::TreeTransform { return clang::QualType(); auto T = arguments[0].getAsType(); - auto prefix = - clang::NestedNameSpecifier::Create(context, nullptr, Alloc.getTypePtr()); + clang::NestedNameSpecifier prefix(Alloc.getTypePtr()); auto rebind = sema.getPreprocessor().getIdentifierInfo("rebind"); - auto DTST = context.getDependentTemplateSpecializationType( - clang::ElaboratedTypeKeyword::None, - clang::DependentTemplateStorage(prefix, rebind, false), - arguments); + clang::TemplateName DTN = context.getDependentTemplateName({prefix, rebind, false}); + auto DTST = + context.getTemplateSpecializationType(clang::ElaboratedTypeKeyword::None, + DTN, + arguments, + {}); - prefix = clang::NestedNameSpecifier::Create(context, prefix, DTST.getTypePtr()); + prefix = clang::NestedNameSpecifier(DTST.getTypePtr()); auto other = sema.getPreprocessor().getIdentifierInfo("other"); auto DNT = context.getDependentNameType(clang::ElaboratedTypeKeyword::Typename, @@ -770,9 +739,11 @@ class PseudoInstantiator : public clang::TreeTransform { for(auto& arg: replaceArguments) { canonicalArguments.emplace_back(context.getCanonicalTemplateArgument(arg)); } - auto result = context.getTemplateSpecializationType(TST->getTemplateName(), - replaceArguments, - canonicalArguments); + auto result = + context.getTemplateSpecializationType(clang::ElaboratedTypeKeyword::None, + TST->getTemplateName(), + replaceArguments, + canonicalArguments); LOG_DEBUG( "{}" "hole: 'allocator_traits::rebind_alloc' → '{}'", pad(), @@ -859,7 +830,9 @@ class PseudoInstantiator : public clang::TreeTransform { clang::QualType TransformDependentNameType(clang::TypeLocBuilder& TLB, clang::DependentNameTypeLoc TL, - bool DeducedTSTContext = false) { + bool DeducedTSTContext = false, + clang::QualType ObjectType = clang::QualType(), + clang::NamedDecl* UnqualLookup = nullptr) { auto* DNT = TL.getTypePtr(); LOG_DEBUG("{}" "resolve '{}'", pad(), clang::QualType(DNT, 0).getAsString()); ++indent; @@ -897,10 +870,10 @@ class PseudoInstantiator : public clang::TreeTransform { return original; } - auto* NNS = NNSLoc.getNestedNameSpecifier(); + auto NNS = NNSLoc.getNestedNameSpecifier(); auto stack_size = stack.data.size(); auto* decl = preferred(lookup(NNS, DNT->getIdentifier())); - auto type = get_decl_type(decl); + auto type = get_decl_type(decl, context); clang::QualType result; if(!type.isNull()) { @@ -963,61 +936,58 @@ class PseudoInstantiator : public clang::TreeTransform { return original; } - using Base::TransformDependentTemplateSpecializationType; + using Base::TransformTemplateSpecializationType; - clang::QualType rebuild_dtst(clang::TypeLocBuilder& TLB, - clang::DependentTemplateSpecializationTypeLoc TL) { - auto* DTST = TL.getTypePtr(); - return TLB.push(clang::QualType(DTST, 0)) - .getType(); - } + /// Handle dependent template specializations (formerly DependentTemplateSpecializationType, + /// now merged into TemplateSpecializationType with DependentTemplateName in LLVM 22). + clang::QualType TransformTemplateSpecializationType(clang::TypeLocBuilder& TLB, + clang::TemplateSpecializationTypeLoc TL) { + auto* TST = TL.getTypePtr(); + auto TN = TST->getTemplateName(); + auto* DTN = TN.getAsDependentTemplateName(); + + if(!DTN) { + // Non-dependent TST: handle alias templates. + if(TST->isTypeAlias()) { + clang::QualType type = substitute(TST->desugar()); + if(!type.isNull()) { + TLB.pushTrivial(context, type, {}); + return type; + } + } + return Base::TransformTemplateSpecializationType(TLB, TL); + } - clang::QualType TransformDependentTemplateSpecializationType( - clang::TypeLocBuilder& TLB, - clang::DependentTemplateSpecializationTypeLoc TL) { - auto* DTST = TL.getTypePtr(); - LOG_DEBUG("{}" "resolve DTST '{}'", pad(), clang::QualType(DTST, 0).getAsString()); + // Dependent template specialization handling. + LOG_DEBUG("{}" "resolve dep-TST '{}'", pad(), clang::QualType(TST, 0).getAsString()); ++indent; - if(auto iter = resolved.find(DTST); iter != resolved.end()) { + if(auto iter = resolved.find(TST); iter != resolved.end()) { --indent; TLB.pushTrivial(context, iter->second, {}); return iter->second; } - auto NNSLoc = TransformNestedNameSpecifierLoc(TL.getQualifierLoc()); - if(!NNSLoc) { - LOG_DEBUG("{}→ ", pad()); - --indent; - return rebuild_dtst(TLB, TL); - } - auto* NNS = NNSLoc.getNestedNameSpecifier(); - - clang::TemplateArgumentListInfo info; - using iterator = clang::TemplateArgumentLocContainerIterator< - clang::DependentTemplateSpecializationTypeLoc>; - if(TransformTemplateArguments(iterator(TL, 0), iterator(TL, TL.getNumArgs()), info)) { - LOG_DEBUG("{}→ ", pad()); - --indent; - return rebuild_dtst(TLB, TL); - } + auto NNS = DTN->getQualifier(); llvm::SmallVector arguments; - for(auto& arg: info.arguments()) { - arguments.push_back(arg.getArgument()); + for(auto& arg: TST->template_arguments()) { + arguments.push_back(arg); } - auto* name = DTST->getDependentTemplateName().getName().getIdentifier(); + auto* name = DTN->getName().getIdentifier(); if(!name) { - LOG_DEBUG("{}→ ", pad()); + LOG_DEBUG("{}→ ", pad()); --indent; - return rebuild_dtst(TLB, TL); + auto original = clang::QualType(TST, 0); + TLB.pushTrivial(context, original, {}); + return original; } if(auto result = hole(NNS, name, arguments); !result.isNull()) { LOG_DEBUG("{}" "hole: '{}' → '{}'", pad(), name->getName().str(), result.getAsString()); --indent; - resolved.try_emplace(DTST, result); + resolved.try_emplace(TST, result); TLB.pushTrivial(context, result, {}); return result; } @@ -1027,7 +997,6 @@ class PseudoInstantiator : public clang::TreeTransform { if(auto* TATD = llvm::dyn_cast(decl)) { if(deduce_template_arguments(TATD, arguments)) { auto type = substitute(TATD->getTemplatedDecl()->getUnderlyingType()); - // Pop lookup frames before further resolution. while(stack.data.size() > stack_size) { stack.pop(); } @@ -1037,26 +1006,25 @@ class PseudoInstantiator : public clang::TreeTransform { if(!type.isNull()) { LOG_DEBUG("{}" "→ '{}' (alias)", pad(), type.getAsString()); --indent; - resolved.try_emplace(DTST, type); + resolved.try_emplace(TST, type); TLB.pushTrivial(context, type, {}); return type; } } } else if(auto* CTD = llvm::dyn_cast(decl)) { - // Resolve DTST to a concrete TemplateSpecializationType. - // e.g. __alloc_traits>::rebind → rebind (a TST) - // This allows subsequent lookup of members (like "other") to work. - // Keep lookup frames on stack — the caller (e.g. TransformNestedNameSpecifierLoc - // processing A::B::C) needs them for parameter substitution. - clang::TemplateName TN(CTD); + clang::TemplateName CTN(CTD); llvm::SmallVector canonArgs; for(auto& arg: arguments) { canonArgs.push_back(context.getCanonicalTemplateArgument(arg)); } - auto result = context.getTemplateSpecializationType(TN, arguments, canonArgs); + auto result = + context.getTemplateSpecializationType(clang::ElaboratedTypeKeyword::None, + CTN, + arguments, + canonArgs); LOG_DEBUG("{}" "→ TST '{}' (class)", pad(), result.getAsString()); --indent; - resolved.try_emplace(DTST, result); + resolved.try_emplace(TST, result); TLB.pushTrivial(context, result, {}); return result; } @@ -1065,11 +1033,12 @@ class PseudoInstantiator : public clang::TreeTransform { stack.pop(); } - LOG_DEBUG("{}→ ", pad()); + LOG_DEBUG("{}→ ", pad()); --indent; - auto fallback = rebuild_dtst(TLB, TL); - resolved.try_emplace(DTST, fallback); - return fallback; + auto original = clang::QualType(TST, 0); + resolved.try_emplace(TST, original); + TLB.pushTrivial(context, original, {}); + return original; } /// Desugar dependent typedefs by delegating to SubstituteOnly. @@ -1077,14 +1046,13 @@ class PseudoInstantiator : public clang::TreeTransform { /// its own TransformTypedefType). Using substitute() here ensures that typedef /// expansion does NOT trigger heuristic lookup, preventing the typedef ↔ lookup cycle. clang::QualType TransformTypedefType(clang::TypeLocBuilder& TLB, clang::TypedefTypeLoc TL) { - if(auto* TND = TL.getTypedefNameDecl()) { + if(auto* TND = TL.getDecl()) { auto underlying = TND->getUnderlyingType(); if(underlying->isDependentType()) { auto type = substitute(underlying); if(!type.isNull()) { - if(auto ET = llvm::dyn_cast(type)) { - type = ET->getNamedType(); - } + // ElaboratedType was removed in LLVM 22; elaboration is now + // part of each type's own representation. TLB.pushTrivial(context, type, {}); return type; } @@ -1138,7 +1106,7 @@ clang::QualType TemplateResolver::resugar(clang::QualType type, clang::Decl* dec return resugar.TransformType(type); } -TemplateResolver::lookup_result TemplateResolver::lookup(const clang::NestedNameSpecifier* NNS, +TemplateResolver::lookup_result TemplateResolver::lookup(clang::NestedNameSpecifier NNS, clang::DeclarationName name) { PseudoInstantiator instantiator(sema, resolved); return instantiator.lookup(NNS, name); diff --git a/src/semantic/resolver.h b/src/semantic/resolver.h index 15557eab..8d752ada 100644 --- a/src/semantic/resolver.h +++ b/src/semantic/resolver.h @@ -41,23 +41,12 @@ class TemplateResolver { using lookup_result = clang::DeclContext::lookup_result; /// Look up the name in the given nested name specifier. - lookup_result lookup(const clang::NestedNameSpecifier* NNS, clang::DeclarationName name); + lookup_result lookup(clang::NestedNameSpecifier NNS, clang::DeclarationName name); lookup_result lookup(const clang::DependentNameType* type) { return lookup(type->getQualifier(), type->getIdentifier()); } - lookup_result lookup(const clang::DependentTemplateSpecializationType* type) { - auto& template_name = type->getDependentTemplateName(); - auto identifier = template_name.getName().getIdentifier(); - if(identifier) { - return lookup(template_name.getQualifier(), identifier); - } else { - /// TODO: Operators don't have an IdentifierInfo; need DeclarationName-based lookup. - return {}; - } - } - lookup_result lookup(const clang::DependentScopeDeclRefExpr* expr) { return lookup(expr->getQualifier(), expr->getNameInfo().getName()); } diff --git a/src/semantic/selection.cpp b/src/semantic/selection.cpp index f30970af..c51ad25c 100644 --- a/src/semantic/selection.cpp +++ b/src/semantic/selection.cpp @@ -756,8 +756,8 @@ class SelectionVisitor : public clang::RecursiveASTVisitor { return traverse_node(X, [&] { return Base::TraverseDecl(X); }); } - bool TraverseTypeLoc(clang::TypeLoc X) { - return traverse_node(&X, [&] { return Base::TraverseTypeLoc(X); }); + bool TraverseTypeLoc(clang::TypeLoc X, bool TraverseQualifier = true) { + return traverse_node(&X, [&] { return Base::TraverseTypeLoc(X, TraverseQualifier); }); } bool TraverseTemplateArgumentLoc(const clang::TemplateArgumentLoc& X) { @@ -814,7 +814,8 @@ class SelectionVisitor : public clang::RecursiveASTVisitor { // This means we'd never see 'int' in 'const int'! Work around that here. // (The reason for the behavior is to avoid traversing the nested Type twice, // but we ignore TraverseType anyway). - bool TraverseQualifiedTypeLoc(clang::QualifiedTypeLoc QX) { + bool TraverseQualifiedTypeLoc(clang::QualifiedTypeLoc QX, + [[maybe_unused]] bool TraverseQualifier = true) { return traverse_node(&QX, [&] { return TraverseTypeLoc(QX.getUnqualifiedLoc()); }); @@ -825,7 +826,7 @@ class SelectionVisitor : public clang::RecursiveASTVisitor { } // Uninteresting parts of the AST that don't have locations within them. - bool TraverseNestedNameSpecifier(clang::NestedNameSpecifier*) { + bool TraverseNestedNameSpecifier(clang::NestedNameSpecifier) { return true; } diff --git a/src/semantic/semantic_visitor.h b/src/semantic/semantic_visitor.h index 74729195..bb4a9123 100644 --- a/src/semantic/semantic_visitor.h +++ b/src/semantic/semantic_visitor.h @@ -515,7 +515,7 @@ class SemanticVisitor : public FilteredASTVisitor> { /// using Foo = int; Foo foo; /// ^~~~ reference VISIT_TYPELOC(TypedefTypeLoc) { - auto decl = loc.getTypedefNameDecl(); + auto decl = loc.getDecl(); auto location = loc.getNameLoc(); handleDeclOccurrence(decl, RelationKind::Reference, location); handleRelation(decl, RelationKind::Reference, decl, location); @@ -560,15 +560,9 @@ class SemanticVisitor : public FilteredASTVisitor> { } /// std::allocator::rebind - /// ^~~~ reference - VISIT_TYPELOC(DependentTemplateSpecializationTypeLoc) { - auto location = loc.getTemplateNameLoc(); - // for(auto decl: resolver.lookup(loc.getTypePtr())) { - // handleDeclOccurrence(decl, RelationKind::WeakReference, location); - // handleRelation(decl, RelationKind::WeakReference, decl, location); - // } - return true; - } + /// DependentTemplateSpecializationType was merged into TemplateSpecializationType + /// in LLVM 22, so dependent template specializations are now handled via + /// VisitTemplateSpecializationTypeLoc above. /// ============================================================================ /// Specifier @@ -576,32 +570,19 @@ class SemanticVisitor : public FilteredASTVisitor> { bool VisitNestedNameSpecifierLoc(clang::NestedNameSpecifierLoc loc) { auto NNS = loc.getNestedNameSpecifier(); - switch(NNS->getKind()) { - case clang::NestedNameSpecifier::Namespace: { - auto decl = NNS->getAsNamespace(); - auto location = loc.getLocalBeginLoc(); - handleDeclOccurrence(decl, RelationKind::Reference, location); - handleRelation(decl, RelationKind::Reference, decl, location); - break; - } - - case clang::NestedNameSpecifier::NamespaceAlias: { - auto decl = NNS->getAsNamespaceAlias(); + switch(NNS.getKind()) { + case clang::NestedNameSpecifier::Kind::Namespace: { + auto [NS, Prefix] = loc.castAsNamespaceAndPrefix(); auto location = loc.getLocalBeginLoc(); - handleDeclOccurrence(decl, RelationKind::Reference, location); - handleRelation(decl, RelationKind::Reference, decl, location); - break; - } - - case clang::NestedNameSpecifier::Identifier: { - assert(NNS->isDependent() && "Identifier NNS should be dependent"); - // FIXME: use TemplateResolver here. + handleDeclOccurrence(NS, RelationKind::Reference, location); + handleRelation(NS, RelationKind::Reference, NS, location); break; } - case clang::NestedNameSpecifier::TypeSpec: - case clang::NestedNameSpecifier::Global: - case clang::NestedNameSpecifier::Super: { + case clang::NestedNameSpecifier::Kind::Null: + case clang::NestedNameSpecifier::Kind::Type: + case clang::NestedNameSpecifier::Kind::Global: + case clang::NestedNameSpecifier::Kind::MicrosoftSuper: { break; }; } diff --git a/src/syntax/scan.cpp b/src/syntax/scan.cpp index 34603b82..43438603 100644 --- a/src/syntax/scan.cpp +++ b/src/syntax/scan.cpp @@ -10,6 +10,7 @@ #include "clang/Basic/FileEntry.h" #include "clang/Basic/FileManager.h" #include "clang/Basic/SourceManager.h" +#include "clang/Driver/CreateInvocationFromArgs.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/FrontendActions.h" #include "clang/Lex/PPCallbacks.h" @@ -319,9 +320,10 @@ std::unique_ptr } auto instance = std::make_unique(std::move(invocation)); - instance->createDiagnostics(*vfs, new clang::IgnoringDiagConsumer(), true); + instance->createDiagnostics(new clang::IgnoringDiagConsumer(), true); instance->getDiagnostics().setSuppressAllDiagnostics(true); - instance->createFileManager(vfs); + instance->setVirtualFileSystem(vfs); + instance->createFileManager(); return instance; } diff --git a/tests/unit/command/argument_parser_tests.cpp b/tests/unit/command/argument_parser_tests.cpp index ee9ae8c0..d8398cc1 100644 --- a/tests/unit/command/argument_parser_tests.cpp +++ b/tests/unit/command/argument_parser_tests.cpp @@ -1,7 +1,7 @@ #include "test/test.h" #include "command/argument_parser.h" -#include "clang/Driver/Options.h" +#include "clang/Options/Options.h" namespace clice::testing { @@ -9,7 +9,7 @@ namespace { TEST_SUITE(ArgumentParser) { -using option = clang::driver::options::ID; +using option = clang::options::ID; void EXPECT_ID(llvm::StringRef command, option opt) { auto id = get_option_id(command); diff --git a/tests/unit/feature/inlay_hint_tests.cpp b/tests/unit/feature/inlay_hint_tests.cpp index f6eb89df..cb1d534d 100644 --- a/tests/unit/feature/inlay_hint_tests.cpp +++ b/tests/unit/feature/inlay_hint_tests.cpp @@ -624,7 +624,7 @@ TEST_CASE(Types) { )c"); EXPECT_SIZE(2); EXPECT_HINT("0", ": S1"); - EXPECT_HINT("1", ": S2::Inner"); + EXPECT_HINT("1", ": Inner"); // Lambda run(R"c( diff --git a/tests/unit/semantic/selection_tests.cpp b/tests/unit/semantic/selection_tests.cpp index 7ab4f2ea..fb72c7ec 100644 --- a/tests/unit/semantic/selection_tests.cpp +++ b/tests/unit/semantic/selection_tests.cpp @@ -442,9 +442,9 @@ TEST_CASE(Types) { TEST_CASE(CXXFeatures) { EXPECT_SELECT(R"( template - int x = @[T::$U::]ccc(); + int x = T::@[$U]::ccc(); )", - "NestedNameSpecifierLoc"); + "DependentNameTypeLoc"); EXPECT_SELECT(R"( struct Foo {}; struct Bar : @[v$ir$tual private Foo] {}; @@ -486,9 +486,9 @@ TEST_CASE(UsingEnum) { "TypedefTypeLoc"); EXPECT_SELECT(R"( namespace ns { enum class A {}; }; - using enum @[$ns::]A; + @[using enum $ns::A]; )", - "NestedNameSpecifierLoc"); + "UsingEnumDecl"); EXPECT_SELECT(R"( namespace ns { enum class A {}; }; @[using $enum ns::A]; From 9e004a20e492d7815c565079a35afd8ee78689e2 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 18:58:40 +0800 Subject: [PATCH 08/28] ci: add prune-llvm workflow for discovering unused LLVM libraries - Add --skip-pattern to prune-llvm-bin.py to protect clang-tidy modules - New workflow downloads pre-built LLVM artifacts, builds clice, and discovers which static libraries can be safely pruned - Skip Debug builds (shared libs, unsafe to prune) --- .github/workflows/prune-llvm.yml | 108 +++++++++++++++++++++++++++++++ scripts/prune-llvm-bin.py | 32 +++++++-- 2 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/prune-llvm.yml diff --git a/.github/workflows/prune-llvm.yml b/.github/workflows/prune-llvm.yml new file mode 100644 index 00000000..0f74dedf --- /dev/null +++ b/.github/workflows/prune-llvm.yml @@ -0,0 +1,108 @@ +name: prune llvm + +on: + push: + branches: [chore/upgrade-llvm-22] + paths: + - ".github/workflows/prune-llvm.yml" + - "scripts/prune-llvm-bin.py" + workflow_dispatch: + inputs: + source_run_id: + description: "Run ID containing LLVM build artifacts" + required: true + type: string + +env: + SOURCE_RUN_ID: ${{ inputs.source_run_id || '27052100922' }} + +jobs: + discover: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + artifact: x64-linux-gnu-releasedbg.tar.xz + - os: macos-15 + artifact: arm64-macos-clang-releasedbg.tar.xz + - os: windows-2025 + artifact: x64-windows-msvc-releasedbg.tar.xz + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-pixi + with: + environments: package + + - name: Free Disk Space (macOS) + if: runner.os == 'macOS' + run: | + if [ -d "/Library/Developer/CoreSimulator" ]; then + sudo rm -rf /Library/Developer/CoreSimulator + fi + XCODE_PATH="$(xcode-select -p)" + PLATFORMS_DIR="${XCODE_PATH}/Platforms" + for p in iPhoneOS.platform iPhoneSimulator.platform AppleTVOS.platform AppleTVSimulator.platform WatchOS.platform WatchSimulator.platform XROS.platform XRSimulator.platform; do + [ -d "${PLATFORMS_DIR}/${p}" ] && sudo rm -rf "${PLATFORMS_DIR}/${p}" + done + sudo rm -rf ~/Library/Android /usr/local/share/dotnet /usr/local/share/powershell ~/.ghcup + + - name: Download LLVM artifact + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + gh run download "${{ env.SOURCE_RUN_ID }}" \ + -n "${{ matrix.artifact }}" -D .llvm-download + mkdir -p .llvm + tar -xf ".llvm-download/${{ matrix.artifact }}" -C .llvm + rm -rf .llvm-download + ls -la .llvm/build-install/ + + - name: Build clice + shell: bash + run: | + pixi run cmake-config RelWithDebInfo ON -- \ + "-DLLVM_INSTALL_PATH=.llvm/build-install" + pixi run cmake-build RelWithDebInfo + + - name: Discover unused libraries + shell: bash + run: | + python3 scripts/prune-llvm-bin.py \ + --action discover \ + --install-dir .llvm/build-install/lib \ + --build-dir build/RelWithDebInfo \ + --manifest pruned-libs.json \ + --skip-pattern "*TidyModule*" + + - name: Print prune results + shell: bash + run: | + echo "=== Prune manifest ===" + cat pruned-libs.json + echo "" + echo "=== Summary ===" + python3 -c " + import json, pathlib + data = json.load(open('pruned-libs.json')) + removed = data['removed'] + lib_dir = pathlib.Path('.llvm/build-install/lib') + total_saved = 0 + for name in removed: + p = lib_dir / name + if p.exists(): + total_saved += p.stat().st_size + print(f'Libraries prunable: {len(removed)}') + print(f'Space saved: {total_saved / 1048576:.1f} MB') + for name in sorted(removed): + print(f' {name}') + " + + - name: Upload manifest + uses: actions/upload-artifact@v4 + with: + name: prune-manifest-${{ matrix.os }} + path: pruned-libs.json diff --git a/scripts/prune-llvm-bin.py b/scripts/prune-llvm-bin.py index 9a392156..2b4d67b7 100644 --- a/scripts/prune-llvm-bin.py +++ b/scripts/prune-llvm-bin.py @@ -10,6 +10,7 @@ """ import argparse +import fnmatch import json import shutil import subprocess @@ -74,6 +75,12 @@ def parse_args() -> argparse.Namespace: default=60, help="Seconds to sleep between manifest download attempts", ) + parser.add_argument( + "--skip-pattern", + action="append", + default=[], + help="Glob pattern for library names to never prune (can be repeated)", + ) return parser.parse_args() @@ -102,16 +109,23 @@ def run_build(build_dir: Path) -> bool: return False -def candidate_files(install_dir: Path) -> Iterable[Path]: +def candidate_files( + install_dir: Path, skip_patterns: Optional[List[str]] = None +) -> Iterable[Path]: if not install_dir.is_dir(): raise FileNotFoundError(f"lib dir not found: {install_dir}") for path in sorted(install_dir.iterdir()): if not path.is_file(): continue - if path.suffix.lower() in {".a", ".lib"}: - yield path - else: + if path.suffix.lower() not in {".a", ".lib"}: print(f"Skipping non-static file: {path.name}") + continue + if skip_patterns and any( + fnmatch.fnmatch(path.name, p) for p in skip_patterns + ): + print(f"Skipping (never-prune): {path.name}") + continue + yield path def try_delete(path: Path, build_dir: Path) -> bool: @@ -128,9 +142,13 @@ def try_delete(path: Path, build_dir: Path) -> bool: return False -def discover(install_dir: Path, build_dir: Path) -> List[str]: +def discover( + install_dir: Path, + build_dir: Path, + skip_patterns: Optional[List[str]] = None, +) -> List[str]: deletable: List[str] = [] - for path in candidate_files(install_dir): + for path in candidate_files(install_dir, skip_patterns): if try_delete(path, build_dir): deletable.append(path.name) return deletable @@ -243,7 +261,7 @@ def main() -> None: install_dir = args.install_dir build_dir = args.build_dir if args.action == "discover": - deletable = discover(install_dir, build_dir) + deletable = discover(install_dir, build_dir, args.skip_pattern) write_manifest(args.manifest, deletable, install_dir, build_dir) else: manifest = ensure_manifest( From be37e95823e1aad406cef3069aa2ab6e9a02cdec Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 19:53:37 +0800 Subject: [PATCH 09/28] fix(prune): record file sizes during discover and fix skip-pattern - Record each file's size before deletion so manifest includes total_saved_bytes and per-file sizes - Fix skip-pattern from *TidyModule* to *Tidy*Module* to correctly match libclangTidyAbseilModule.a etc. - Update manifest format: removed is now [{name, size}, ...] - apply_manifest handles both old (string) and new (dict) formats --- .github/workflows/prune-llvm.yml | 17 ++++++----------- scripts/prune-llvm-bin.py | 26 ++++++++++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/.github/workflows/prune-llvm.yml b/.github/workflows/prune-llvm.yml index 0f74dedf..533fb35a 100644 --- a/.github/workflows/prune-llvm.yml +++ b/.github/workflows/prune-llvm.yml @@ -76,7 +76,7 @@ jobs: --install-dir .llvm/build-install/lib \ --build-dir build/RelWithDebInfo \ --manifest pruned-libs.json \ - --skip-pattern "*TidyModule*" + --skip-pattern "*Tidy*Module*" - name: Print prune results shell: bash @@ -86,19 +86,14 @@ jobs: echo "" echo "=== Summary ===" python3 -c " - import json, pathlib + import json data = json.load(open('pruned-libs.json')) removed = data['removed'] - lib_dir = pathlib.Path('.llvm/build-install/lib') - total_saved = 0 - for name in removed: - p = lib_dir / name - if p.exists(): - total_saved += p.stat().st_size + total = data.get('total_saved_bytes', 0) print(f'Libraries prunable: {len(removed)}') - print(f'Space saved: {total_saved / 1048576:.1f} MB') - for name in sorted(removed): - print(f' {name}') + print(f'Space saved: {total / 1048576:.1f} MB') + for entry in sorted(removed, key=lambda e: e['size'], reverse=True): + print(f' {entry[\"size\"] / 1048576:>7.1f} MB {entry[\"name\"]}') " - name: Upload manifest diff --git a/scripts/prune-llvm-bin.py b/scripts/prune-llvm-bin.py index 2b4d67b7..2fc3fc9a 100644 --- a/scripts/prune-llvm-bin.py +++ b/scripts/prune-llvm-bin.py @@ -128,43 +128,48 @@ def candidate_files( yield path -def try_delete(path: Path, build_dir: Path) -> bool: +def try_delete(path: Path, build_dir: Path) -> Optional[int]: + size = path.stat().st_size backup = path.with_suffix(path.suffix + ".bak") print(f"Testing deletion: {path}") shutil.move(path, backup) success = run_build(build_dir) if success: backup.unlink(missing_ok=True) - print(f"Safe to delete: {path.name}") - return True + print(f"Safe to delete: {path.name} ({size} bytes)") + return size shutil.move(backup, path) print(f"Required; restored: {path.name}") - return False + return None def discover( install_dir: Path, build_dir: Path, skip_patterns: Optional[List[str]] = None, -) -> List[str]: - deletable: List[str] = [] +) -> List[dict]: + deletable: List[dict] = [] for path in candidate_files(install_dir, skip_patterns): - if try_delete(path, build_dir): - deletable.append(path.name) + size = try_delete(path, build_dir) + if size is not None: + deletable.append({"name": path.name, "size": size}) return deletable def write_manifest( - manifest: Path, removed: List[str], install_dir: Path, build_dir: Path + manifest: Path, removed: List[dict], install_dir: Path, build_dir: Path ) -> None: + total_size = sum(entry["size"] for entry in removed) data = { "generated_at": datetime.now(timezone.utc).isoformat(), "install_dir": str(install_dir), "build_dir": str(build_dir), + "total_saved_bytes": total_size, "removed": removed, } manifest.write_text(json.dumps(data, indent=2)) print(f"Wrote manifest with {len(removed)} entries to {manifest}") + print(f"Total space saved: {total_size / 1048576:.1f} MB") def apply_manifest(manifest: Path, install_dir: Path) -> None: @@ -174,7 +179,8 @@ def apply_manifest(manifest: Path, install_dir: Path) -> None: removed = data.get("removed", []) if not isinstance(removed, list): raise ValueError("Manifest missing 'removed' list") - for name in removed: + for entry in removed: + name = entry["name"] if isinstance(entry, dict) else entry target = install_dir / name if target.exists(): print(f"Deleting {target}") From b5be3600f17455aeedc20a5b568c73341012cd16 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 19:56:22 +0800 Subject: [PATCH 10/28] refactor(cmake): switch to find_package(LLVM/Clang) for dependency resolution Replace file(GLOB) and manual library lists with proper CMake imported targets. Transitive dependencies are now resolved automatically via LLVMConfig.cmake and ClangConfig.cmake. Also adds clangTidyCustomModule and clangTidyMPIModule (new in LLVM 22). --- cmake/llvm.cmake | 91 ++++++++++++++++-------------------------------- 1 file changed, 30 insertions(+), 61 deletions(-) diff --git a/cmake/llvm.cmake b/cmake/llvm.cmake index 694857ef..23737522 100644 --- a/cmake/llvm.cmake +++ b/cmake/llvm.cmake @@ -63,69 +63,38 @@ function(setup_llvm LLVM_VERSION) message(FATAL_ERROR "Error: The specified LLVM_INSTALL_PATH does not exist: ${LLVM_INSTALL_PATH}") endif() - # set llvm include and lib path + find_package(LLVM REQUIRED CONFIG + PATHS "${LLVM_INSTALL_PATH}/lib/cmake/llvm" NO_DEFAULT_PATH) + find_package(Clang REQUIRED CONFIG + PATHS "${LLVM_INSTALL_PATH}/lib/cmake/clang" NO_DEFAULT_PATH) + + llvm_map_components_to_libnames(LLVM_RESOLVED + support frontendopenmp option targetparser) + add_library(llvm-libs INTERFACE IMPORTED) + target_link_libraries(llvm-libs INTERFACE + ${LLVM_RESOLVED} + clangAST clangASTMatchers clangBasic clangDriver + clangFormat clangFrontend clangLex clangSema clangSerialization + clangTidy clangTidyUtils + clangTidyAbseilModule clangTidyAlteraModule clangTidyAndroidModule + clangTidyBoostModule clangTidyBugproneModule clangTidyCERTModule + clangTidyConcurrencyModule clangTidyCppCoreGuidelinesModule + clangTidyCustomModule clangTidyDarwinModule clangTidyFuchsiaModule + clangTidyGoogleModule clangTidyHICPPModule clangTidyLinuxKernelModule + clangTidyLLVMModule clangTidyLLVMLibcModule clangTidyMiscModule + clangTidyModernizeModule clangTidyMPIModule clangTidyObjCModule + clangTidyOpenMPModule clangTidyPerformanceModule + clangTidyPortabilityModule clangTidyReadabilityModule + clangTidyZirconModule + clangTooling clangToolingCore + clangToolingInclusions clangToolingInclusionsStdlib clangToolingSyntax + ) + + target_include_directories(llvm-libs INTERFACE + ${LLVM_INCLUDE_DIRS} ${CLANG_INCLUDE_DIRS}) - # add to include directories - target_include_directories(llvm-libs INTERFACE "${LLVM_INSTALL_PATH}/include") - - if(CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT WIN32) - target_link_directories(llvm-libs INTERFACE "${LLVM_INSTALL_PATH}/lib") - target_link_libraries(llvm-libs INTERFACE - LLVMSupport - LLVMFrontendOpenMP - LLVMOption - LLVMTargetParser - clangAST - clangASTMatchers - clangBasic - clangDriver - clangFormat - clangFrontend - clangLex - clangSema - clangSerialization - clangTidy - clangTidyUtils - clangTidyAndroidModule - clangTidyAbseilModule - clangTidyAlteraModule - clangTidyBoostModule - clangTidyBugproneModule - clangTidyCERTModule - clangTidyConcurrencyModule - clangTidyCppCoreGuidelinesModule - clangTidyDarwinModule - clangTidyFuchsiaModule - clangTidyGoogleModule - clangTidyHICPPModule - clangTidyLinuxKernelModule - clangTidyLLVMModule - clangTidyLLVMLibcModule - clangTidyMiscModule - clangTidyModernizeModule - clangTidyObjCModule - clangTidyOpenMPModule - clangTidyPerformanceModule - clangTidyPortabilityModule - clangTidyReadabilityModule - clangTidyZirconModule - clangTooling - clangToolingCore - clangToolingInclusions - clangToolingInclusionsStdlib - clangToolingSyntax - ) - else() - file(GLOB LLVM_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}LLVM[a-zA-Z]*${CMAKE_STATIC_LIBRARY_SUFFIX}") - file(GLOB CLANG_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}clang[a-zA-Z]*${CMAKE_STATIC_LIBRARY_SUFFIX}") - # TODO: find a better way to find out whether zlib and zstd are needed - # Currently link if present in the LLVM lib directory - file(GLOB OTHER_REQUIRED_LIBS CONFIGURE_DEPENDS - "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}z${CMAKE_STATIC_LIBRARY_SUFFIX}" - "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}zstd${CMAKE_STATIC_LIBRARY_SUFFIX}" - ) - target_link_libraries(llvm-libs INTERFACE ${LLVM_LIBRARIES} ${CLANG_LIBRARIES} ${OTHER_REQUIRED_LIBS}) + if(NOT BUILD_SHARED_LIBS) target_compile_definitions(llvm-libs INTERFACE CLANG_BUILD_STATIC=1) endif() endfunction() From 25a6e85db788acd9ddda4e1eec305d0c6bab43df Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 20:47:01 +0800 Subject: [PATCH 11/28] fix(prune): nullify shared libs and force relink during discovery Aggregate shared libraries (libclang-cpp.so, libLTO.so, libRemarks.so) contain all symbols from their static counterparts, causing false positives during prune detection. Replace them with empty stubs before discovery. Also delete build output binaries before each test rebuild to force ninja to re-link. --- scripts/prune-llvm-bin.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/prune-llvm-bin.py b/scripts/prune-llvm-bin.py index 2fc3fc9a..17c8874b 100644 --- a/scripts/prune-llvm-bin.py +++ b/scripts/prune-llvm-bin.py @@ -128,11 +128,21 @@ def candidate_files( yield path +def _remove_binaries(build_dir: Path) -> None: + bin_dir = build_dir / "bin" + if not bin_dir.is_dir(): + return + for f in bin_dir.iterdir(): + if f.is_file() and f.suffix in {"", ".exe"}: + f.unlink() + + def try_delete(path: Path, build_dir: Path) -> Optional[int]: size = path.stat().st_size backup = path.with_suffix(path.suffix + ".bak") print(f"Testing deletion: {path}") shutil.move(path, backup) + _remove_binaries(build_dir) success = run_build(build_dir) if success: backup.unlink(missing_ok=True) @@ -143,11 +153,22 @@ def try_delete(path: Path, build_dir: Path) -> Optional[int]: return None +def _nullify_shared_libs(install_dir: Path) -> None: + for path in sorted(install_dir.iterdir()): + if not path.is_file(): + continue + if ".so" in path.suffixes or ".dylib" in path.suffixes: + if path.stat().st_size > 0: + print(f"Nullifying shared lib: {path.name}") + path.write_bytes(b"") + + def discover( install_dir: Path, build_dir: Path, skip_patterns: Optional[List[str]] = None, ) -> List[dict]: + _nullify_shared_libs(install_dir) deletable: List[dict] = [] for path in candidate_files(install_dir, skip_patterns): size = try_delete(path, build_dir) From 115fc4a94791a1fe50efc27fb73e0595bb219a43 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 21:39:44 +0800 Subject: [PATCH 12/28] fix(prune): replace pruned libs with empty archives and track shared libs - Record nullified .so/.dylib files in manifest with original sizes - Apply mode replaces files with empty archives (!\n) instead of deleting, preserving cmake export integrity - Shared libs replaced with empty stubs (0 bytes) --- scripts/prune-llvm-bin.py | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/scripts/prune-llvm-bin.py b/scripts/prune-llvm-bin.py index 17c8874b..e3b9418d 100644 --- a/scripts/prune-llvm-bin.py +++ b/scripts/prune-llvm-bin.py @@ -153,14 +153,31 @@ def try_delete(path: Path, build_dir: Path) -> Optional[int]: return None -def _nullify_shared_libs(install_dir: Path) -> None: +ARCHIVE_MAGIC = b"!\n" + + +def _is_shared_lib(path: Path) -> bool: + return ".so" in path.suffixes or ".dylib" in path.suffixes + + +def _nullify_shared_libs(install_dir: Path) -> List[dict]: + nullified: List[dict] = [] for path in sorted(install_dir.iterdir()): - if not path.is_file(): + if not path.is_file() or not _is_shared_lib(path): continue - if ".so" in path.suffixes or ".dylib" in path.suffixes: - if path.stat().st_size > 0: - print(f"Nullifying shared lib: {path.name}") - path.write_bytes(b"") + size = path.stat().st_size + if size > 0: + print(f"Nullifying shared lib: {path.name} ({size} bytes)") + path.write_bytes(b"") + nullified.append({"name": path.name, "size": size}) + return nullified + + +def _replace_with_empty_archive(path: Path) -> None: + if _is_shared_lib(path): + path.write_bytes(b"") + else: + path.write_bytes(ARCHIVE_MAGIC) def discover( @@ -168,13 +185,13 @@ def discover( build_dir: Path, skip_patterns: Optional[List[str]] = None, ) -> List[dict]: - _nullify_shared_libs(install_dir) + nullified = _nullify_shared_libs(install_dir) deletable: List[dict] = [] for path in candidate_files(install_dir, skip_patterns): size = try_delete(path, build_dir) if size is not None: deletable.append({"name": path.name, "size": size}) - return deletable + return nullified + deletable def write_manifest( @@ -204,8 +221,8 @@ def apply_manifest(manifest: Path, install_dir: Path) -> None: name = entry["name"] if isinstance(entry, dict) else entry target = install_dir / name if target.exists(): - print(f"Deleting {target}") - target.unlink() + print(f"Replacing with empty archive: {target}") + _replace_with_empty_archive(target) else: print(f"Already absent: {target}") From baffc35a07b660da92810d10de56098e43f5df08 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 6 Jun 2026 22:39:08 +0800 Subject: [PATCH 13/28] ci: add release-llvm workflow and rewrite cmake download - Replace prune-llvm.yml with release-llvm.yml that discovers prunable libs, applies prune manifests to all 14 artifacts, and uploads to clice-io/clice-llvm - Rewrite cmake/llvm.cmake to use native file(DOWNLOAD) instead of calling setup-llvm.py, removing the Python dependency at configure time - Workflow triggered by workflow_dispatch with source_run_id and version --- .github/workflows/prune-llvm.yml | 103 ----------- .github/workflows/release-llvm.yml | 273 +++++++++++++++++++++++++++++ cmake/llvm.cmake | 167 +++++++++++++----- 3 files changed, 393 insertions(+), 150 deletions(-) delete mode 100644 .github/workflows/prune-llvm.yml create mode 100644 .github/workflows/release-llvm.yml diff --git a/.github/workflows/prune-llvm.yml b/.github/workflows/prune-llvm.yml deleted file mode 100644 index 533fb35a..00000000 --- a/.github/workflows/prune-llvm.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: prune llvm - -on: - push: - branches: [chore/upgrade-llvm-22] - paths: - - ".github/workflows/prune-llvm.yml" - - "scripts/prune-llvm-bin.py" - workflow_dispatch: - inputs: - source_run_id: - description: "Run ID containing LLVM build artifacts" - required: true - type: string - -env: - SOURCE_RUN_ID: ${{ inputs.source_run_id || '27052100922' }} - -jobs: - discover: - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-24.04 - artifact: x64-linux-gnu-releasedbg.tar.xz - - os: macos-15 - artifact: arm64-macos-clang-releasedbg.tar.xz - - os: windows-2025 - artifact: x64-windows-msvc-releasedbg.tar.xz - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - - uses: ./.github/actions/setup-pixi - with: - environments: package - - - name: Free Disk Space (macOS) - if: runner.os == 'macOS' - run: | - if [ -d "/Library/Developer/CoreSimulator" ]; then - sudo rm -rf /Library/Developer/CoreSimulator - fi - XCODE_PATH="$(xcode-select -p)" - PLATFORMS_DIR="${XCODE_PATH}/Platforms" - for p in iPhoneOS.platform iPhoneSimulator.platform AppleTVOS.platform AppleTVSimulator.platform WatchOS.platform WatchSimulator.platform XROS.platform XRSimulator.platform; do - [ -d "${PLATFORMS_DIR}/${p}" ] && sudo rm -rf "${PLATFORMS_DIR}/${p}" - done - sudo rm -rf ~/Library/Android /usr/local/share/dotnet /usr/local/share/powershell ~/.ghcup - - - name: Download LLVM artifact - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - gh run download "${{ env.SOURCE_RUN_ID }}" \ - -n "${{ matrix.artifact }}" -D .llvm-download - mkdir -p .llvm - tar -xf ".llvm-download/${{ matrix.artifact }}" -C .llvm - rm -rf .llvm-download - ls -la .llvm/build-install/ - - - name: Build clice - shell: bash - run: | - pixi run cmake-config RelWithDebInfo ON -- \ - "-DLLVM_INSTALL_PATH=.llvm/build-install" - pixi run cmake-build RelWithDebInfo - - - name: Discover unused libraries - shell: bash - run: | - python3 scripts/prune-llvm-bin.py \ - --action discover \ - --install-dir .llvm/build-install/lib \ - --build-dir build/RelWithDebInfo \ - --manifest pruned-libs.json \ - --skip-pattern "*Tidy*Module*" - - - name: Print prune results - shell: bash - run: | - echo "=== Prune manifest ===" - cat pruned-libs.json - echo "" - echo "=== Summary ===" - python3 -c " - import json - data = json.load(open('pruned-libs.json')) - removed = data['removed'] - total = data.get('total_saved_bytes', 0) - print(f'Libraries prunable: {len(removed)}') - print(f'Space saved: {total / 1048576:.1f} MB') - for entry in sorted(removed, key=lambda e: e['size'], reverse=True): - print(f' {entry[\"size\"] / 1048576:>7.1f} MB {entry[\"name\"]}') - " - - - name: Upload manifest - uses: actions/upload-artifact@v4 - with: - name: prune-manifest-${{ matrix.os }} - path: pruned-libs.json diff --git a/.github/workflows/release-llvm.yml b/.github/workflows/release-llvm.yml new file mode 100644 index 00000000..ecd640a3 --- /dev/null +++ b/.github/workflows/release-llvm.yml @@ -0,0 +1,273 @@ +name: release llvm + +on: + workflow_dispatch: + inputs: + source_run_id: + description: "Run ID from build-llvm containing LLVM artifacts" + required: true + type: string + llvm_version: + description: "LLVM version (e.g. 22.1.4)" + required: true + type: string + +jobs: + discover: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + artifact: x64-linux-gnu-releasedbg.tar.xz + - os: macos-15 + artifact: arm64-macos-clang-releasedbg.tar.xz + - os: windows-2025 + artifact: x64-windows-msvc-releasedbg.tar.xz + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-pixi + with: + environments: package + + - name: Free Disk Space (macOS) + if: runner.os == 'macOS' + run: | + if [ -d "/Library/Developer/CoreSimulator" ]; then + sudo rm -rf /Library/Developer/CoreSimulator + fi + XCODE_PATH="$(xcode-select -p)" + PLATFORMS_DIR="${XCODE_PATH}/Platforms" + for p in iPhoneOS.platform iPhoneSimulator.platform AppleTVOS.platform AppleTVSimulator.platform WatchOS.platform WatchSimulator.platform XROS.platform XRSimulator.platform; do + [ -d "${PLATFORMS_DIR}/${p}" ] && sudo rm -rf "${PLATFORMS_DIR}/${p}" + done + sudo rm -rf ~/Library/Android /usr/local/share/dotnet /usr/local/share/powershell ~/.ghcup + + - name: Download LLVM artifact + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + gh run download "${{ inputs.source_run_id }}" \ + -n "${{ matrix.artifact }}" -D .llvm-download + mkdir -p .llvm + tar -xf ".llvm-download/${{ matrix.artifact }}" -C .llvm + rm -rf .llvm-download + + - name: Build clice + shell: bash + run: | + pixi run cmake-config RelWithDebInfo ON -- \ + "-DLLVM_INSTALL_PATH=.llvm/build-install" + pixi run cmake-build RelWithDebInfo + + - name: Discover unused libraries + shell: bash + run: | + python3 scripts/prune-llvm-bin.py \ + --action discover \ + --install-dir .llvm/build-install/lib \ + --build-dir build/RelWithDebInfo \ + --manifest pruned-libs.json \ + --skip-pattern "*Tidy*Module*" + + - name: Print prune results + shell: bash + run: | + python3 -c " + import json + data = json.load(open('pruned-libs.json')) + removed = data['removed'] + total = data.get('total_saved_bytes', 0) + print(f'Libraries prunable: {len(removed)}') + print(f'Space saved: {total / 1048576:.1f} MB') + for entry in sorted(removed, key=lambda e: e['size'], reverse=True)[:20]: + print(f' {entry[\"size\"] / 1048576:>7.1f} MB {entry[\"name\"]}') + " + + - name: Upload manifest + uses: actions/upload-artifact@v4 + with: + name: prune-manifest-${{ matrix.os }} + path: pruned-libs.json + + release: + needs: discover + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ + /usr/local/share/powershell /usr/share/swift /usr/local/.ghcup + df -h / + + - name: Download prune manifests + uses: actions/download-artifact@v4 + with: + pattern: prune-manifest-* + path: manifests + + - name: Apply prune and repackage all artifacts + env: + GH_TOKEN: ${{ github.token }} + SOURCE_RUN_ID: ${{ inputs.source_run_id }} + shell: bash + run: | + set -euo pipefail + mkdir -p artifacts + + manifest_for() { + local name="$1" + if [[ "$name" == *linux* ]]; then + echo "manifests/prune-manifest-ubuntu-24.04/pruned-libs.json" + elif [[ "$name" == *macos* ]]; then + echo "manifests/prune-manifest-macos-15/pruned-libs.json" + elif [[ "$name" == *windows* ]]; then + echo "manifests/prune-manifest-windows-2025/pruned-libs.json" + fi + } + + ARTIFACTS=( + "aarch64-linux-gnu-releasedbg-lto.tar.xz" + "aarch64-linux-gnu-releasedbg.tar.xz" + "aarch64-windows-msvc-releasedbg-lto.tar.xz" + "aarch64-windows-msvc-releasedbg.tar.xz" + "arm64-macos-clang-debug-asan.tar.xz" + "arm64-macos-clang-releasedbg-lto.tar.xz" + "arm64-macos-clang-releasedbg.tar.xz" + "x64-linux-gnu-debug-asan.tar.xz" + "x64-linux-gnu-releasedbg-lto.tar.xz" + "x64-linux-gnu-releasedbg.tar.xz" + "x64-macos-clang-releasedbg-lto.tar.xz" + "x64-macos-clang-releasedbg.tar.xz" + "x64-windows-msvc-releasedbg-lto.tar.xz" + "x64-windows-msvc-releasedbg.tar.xz" + ) + + for ARTIFACT in "${ARTIFACTS[@]}"; do + echo "::group::Processing $ARTIFACT" + + gh run download "${SOURCE_RUN_ID}" -n "$ARTIFACT" -D tmp-download || { + echo "Warning: failed to download $ARTIFACT, skipping" + rm -rf tmp-download + echo "::endgroup::" + continue + } + + TARBALL="tmp-download/$ARTIFACT" + if [[ ! -f "$TARBALL" ]]; then + echo "Warning: $TARBALL not found, skipping" + rm -rf tmp-download + echo "::endgroup::" + continue + fi + + if [[ "$ARTIFACT" == *debug* ]] || [[ "$ARTIFACT" == *asan* ]]; then + echo "Debug/ASAN artifact — skipping prune" + mv "$TARBALL" "artifacts/$ARTIFACT" + else + MANIFEST=$(manifest_for "$ARTIFACT") + echo "Extracting..." + mkdir -p tmp-work + tar -xf "$TARBALL" -C tmp-work + + echo "Applying prune manifest: $MANIFEST" + python3 scripts/prune-llvm-bin.py \ + --action apply \ + --manifest "$MANIFEST" \ + --install-dir tmp-work/build-install/lib + + echo "Repackaging..." + tar -C tmp-work -cf - build-install | xz -T0 -6 -c > "artifacts/$ARTIFACT" + rm -rf tmp-work + fi + + rm -rf tmp-download + echo "::endgroup::" + done + + echo "=== All artifacts ===" + ls -lh artifacts/ + + - name: Upload to clice-llvm + env: + GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} + run: | + python3 scripts/upload-llvm.py \ + "${{ inputs.llvm_version }}" \ + "clice-io/clice-llvm" \ + "${{ github.run_id }}" + + - name: Save manifest + uses: actions/upload-artifact@v4 + with: + name: llvm-manifest-final + path: artifacts/llvm-manifest.json + if-no-files-found: error + compression-level: 0 + + update-clice: + needs: release + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - name: Download manifest + uses: actions/download-artifact@v4 + with: + name: llvm-manifest-final + path: . + + - name: Update manifest and version + run: | + python3 scripts/update-llvm-version.py \ + --version "${{ inputs.llvm_version }}" \ + --manifest-src llvm-manifest.json \ + --manifest-dest config/llvm-manifest.json \ + --package-cmake cmake/package.cmake + + - name: Commit and push + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${{ inputs.llvm_version }}" + BRANCH="chore/update-llvm-${VERSION}" + RELEASE_URL="https://github.com/clice-io/clice-llvm/releases/tag/${VERSION}" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "${BRANCH}" + git add config/llvm-manifest.json cmake/package.cmake + git commit -m "chore: update LLVM to ${VERSION}" + git push --force-with-lease origin "${BRANCH}" + + EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number // empty') + + BODY="$(cat < Date: Sat, 6 Jun 2026 22:41:49 +0800 Subject: [PATCH 14/28] ci: add push trigger for release-llvm on feature branch --- .github/workflows/release-llvm.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-llvm.yml b/.github/workflows/release-llvm.yml index ecd640a3..b227cfda 100644 --- a/.github/workflows/release-llvm.yml +++ b/.github/workflows/release-llvm.yml @@ -1,6 +1,10 @@ name: release llvm on: + push: + branches: [chore/upgrade-llvm-22] + paths: + - ".github/workflows/release-llvm.yml" workflow_dispatch: inputs: source_run_id: @@ -12,6 +16,10 @@ on: required: true type: string +env: + SOURCE_RUN_ID: ${{ inputs.source_run_id || '27052100922' }} + LLVM_VERSION: ${{ inputs.llvm_version || '22.1.4' }} + jobs: discover: strategy: @@ -50,7 +58,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - gh run download "${{ inputs.source_run_id }}" \ + gh run download "${{ env.SOURCE_RUN_ID }}" \ -n "${{ matrix.artifact }}" -D .llvm-download mkdir -p .llvm tar -xf ".llvm-download/${{ matrix.artifact }}" -C .llvm @@ -116,7 +124,6 @@ jobs: - name: Apply prune and repackage all artifacts env: GH_TOKEN: ${{ github.token }} - SOURCE_RUN_ID: ${{ inputs.source_run_id }} shell: bash run: | set -euo pipefail @@ -200,7 +207,7 @@ jobs: GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} run: | python3 scripts/upload-llvm.py \ - "${{ inputs.llvm_version }}" \ + "${{ env.LLVM_VERSION }}" \ "clice-io/clice-llvm" \ "${{ github.run_id }}" @@ -230,7 +237,7 @@ jobs: - name: Update manifest and version run: | python3 scripts/update-llvm-version.py \ - --version "${{ inputs.llvm_version }}" \ + --version "${{ env.LLVM_VERSION }}" \ --manifest-src llvm-manifest.json \ --manifest-dest config/llvm-manifest.json \ --package-cmake cmake/package.cmake @@ -239,7 +246,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - VERSION="${{ inputs.llvm_version }}" + VERSION="${{ env.LLVM_VERSION }}" BRANCH="chore/update-llvm-${VERSION}" RELEASE_URL="https://github.com/clice-io/clice-llvm/releases/tag/${VERSION}" From 9d839382be048ecd81af6653ec8ad3818e4d8e97 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 7 Jun 2026 00:30:53 +0800 Subject: [PATCH 15/28] refactor: rename prune script to release-llvm and add parallel repackage Rename prune-llvm-bin.py to release-llvm.py and add a `repackage` action that downloads, prunes, and repackages all 14 LLVM artifacts in parallel using concurrent.futures. Move release job to macos-15 for faster arm64 compression. --- .github/workflows/build-llvm.yml | 6 +- .github/workflows/release-llvm.yml | 95 +------ scripts/prune-llvm-bin.py | 323 ---------------------- scripts/release-llvm.py | 414 +++++++++++++++++++++++++++++ 4 files changed, 431 insertions(+), 407 deletions(-) delete mode 100644 scripts/prune-llvm-bin.py create mode 100644 scripts/release-llvm.py diff --git a/.github/workflows/build-llvm.yml b/.github/workflows/build-llvm.yml index 14cfa6e9..402d74bb 100644 --- a/.github/workflows/build-llvm.yml +++ b/.github/workflows/build-llvm.yml @@ -239,7 +239,7 @@ jobs: run: | MANIFEST="pruned-libs-${{ matrix.os }}.json" echo "LLVM_PRUNED_MANIFEST=${MANIFEST}" >> "${GITHUB_ENV}" - python3 scripts/prune-llvm-bin.py \ + python3 scripts/release-llvm.py \ --action discover \ --install-dir ".llvm/build-install/lib" \ --build-dir "build/${{ matrix.llvm_mode }}" \ @@ -263,7 +263,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | MANIFEST="pruned-libs-${{ matrix.os }}.json" - python3 scripts/prune-llvm-bin.py \ + python3 scripts/release-llvm.py \ --action apply \ --manifest "${MANIFEST}" \ --install-dir ".llvm/build-install/lib" \ @@ -283,7 +283,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | MANIFEST="pruned-libs-${{ matrix.os }}.json" - python3 scripts/prune-llvm-bin.py \ + python3 scripts/release-llvm.py \ --action apply \ --manifest "${MANIFEST}" \ --install-dir ".llvm/build-install/lib" \ diff --git a/.github/workflows/release-llvm.yml b/.github/workflows/release-llvm.yml index b227cfda..8195065b 100644 --- a/.github/workflows/release-llvm.yml +++ b/.github/workflows/release-llvm.yml @@ -74,7 +74,7 @@ jobs: - name: Discover unused libraries shell: bash run: | - python3 scripts/prune-llvm-bin.py \ + python3 scripts/release-llvm.py \ --action discover \ --install-dir .llvm/build-install/lib \ --build-dir build/RelWithDebInfo \ @@ -103,7 +103,7 @@ jobs: release: needs: discover - runs-on: ubuntu-24.04 + runs-on: macos-15 permissions: contents: read steps: @@ -111,8 +111,11 @@ jobs: - name: Free disk space run: | - sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ - /usr/local/share/powershell /usr/share/swift /usr/local/.ghcup + sudo rm -rf ~/Library/Android /usr/local/share/dotnet \ + /usr/local/share/powershell ~/.ghcup + if [ -d "/Library/Developer/CoreSimulator" ]; then + sudo rm -rf /Library/Developer/CoreSimulator + fi df -h / - name: Download prune manifests @@ -121,86 +124,16 @@ jobs: pattern: prune-manifest-* path: manifests - - name: Apply prune and repackage all artifacts + - name: Repackage artifacts env: GH_TOKEN: ${{ github.token }} - shell: bash run: | - set -euo pipefail - mkdir -p artifacts - - manifest_for() { - local name="$1" - if [[ "$name" == *linux* ]]; then - echo "manifests/prune-manifest-ubuntu-24.04/pruned-libs.json" - elif [[ "$name" == *macos* ]]; then - echo "manifests/prune-manifest-macos-15/pruned-libs.json" - elif [[ "$name" == *windows* ]]; then - echo "manifests/prune-manifest-windows-2025/pruned-libs.json" - fi - } - - ARTIFACTS=( - "aarch64-linux-gnu-releasedbg-lto.tar.xz" - "aarch64-linux-gnu-releasedbg.tar.xz" - "aarch64-windows-msvc-releasedbg-lto.tar.xz" - "aarch64-windows-msvc-releasedbg.tar.xz" - "arm64-macos-clang-debug-asan.tar.xz" - "arm64-macos-clang-releasedbg-lto.tar.xz" - "arm64-macos-clang-releasedbg.tar.xz" - "x64-linux-gnu-debug-asan.tar.xz" - "x64-linux-gnu-releasedbg-lto.tar.xz" - "x64-linux-gnu-releasedbg.tar.xz" - "x64-macos-clang-releasedbg-lto.tar.xz" - "x64-macos-clang-releasedbg.tar.xz" - "x64-windows-msvc-releasedbg-lto.tar.xz" - "x64-windows-msvc-releasedbg.tar.xz" - ) - - for ARTIFACT in "${ARTIFACTS[@]}"; do - echo "::group::Processing $ARTIFACT" - - gh run download "${SOURCE_RUN_ID}" -n "$ARTIFACT" -D tmp-download || { - echo "Warning: failed to download $ARTIFACT, skipping" - rm -rf tmp-download - echo "::endgroup::" - continue - } - - TARBALL="tmp-download/$ARTIFACT" - if [[ ! -f "$TARBALL" ]]; then - echo "Warning: $TARBALL not found, skipping" - rm -rf tmp-download - echo "::endgroup::" - continue - fi - - if [[ "$ARTIFACT" == *debug* ]] || [[ "$ARTIFACT" == *asan* ]]; then - echo "Debug/ASAN artifact — skipping prune" - mv "$TARBALL" "artifacts/$ARTIFACT" - else - MANIFEST=$(manifest_for "$ARTIFACT") - echo "Extracting..." - mkdir -p tmp-work - tar -xf "$TARBALL" -C tmp-work - - echo "Applying prune manifest: $MANIFEST" - python3 scripts/prune-llvm-bin.py \ - --action apply \ - --manifest "$MANIFEST" \ - --install-dir tmp-work/build-install/lib - - echo "Repackaging..." - tar -C tmp-work -cf - build-install | xz -T0 -6 -c > "artifacts/$ARTIFACT" - rm -rf tmp-work - fi - - rm -rf tmp-download - echo "::endgroup::" - done - - echo "=== All artifacts ===" - ls -lh artifacts/ + python3 scripts/release-llvm.py \ + --action repackage \ + --source-run-id "${{ env.SOURCE_RUN_ID }}" \ + --manifests-dir manifests \ + --output-dir artifacts \ + --max-parallel 3 - name: Upload to clice-llvm env: diff --git a/scripts/prune-llvm-bin.py b/scripts/prune-llvm-bin.py deleted file mode 100644 index e3b9418d..00000000 --- a/scripts/prune-llvm-bin.py +++ /dev/null @@ -1,323 +0,0 @@ -#!/usr/bin/env python3 -""" -Prune unused LLVM binaries/libraries from the install tree. - -Two modes: - - discover: iteratively delete candidates and rebuild to confirm they are unused, - then write the deletion list to a manifest JSON file. - - apply: read the manifest and delete the listed files without rebuilding - (useful for LTO builds that trust the non-LTO pruning result). -""" - -import argparse -import fnmatch -import json -import shutil -import subprocess -import time -from datetime import datetime, timezone -from pathlib import Path -from typing import Iterable, List, Optional - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Prune unused LLVM artifacts.") - parser.add_argument( - "--action", - choices=["discover", "apply"], - default="discover", - help="discover: probe deletable files and record them; apply: delete using manifest", - ) - parser.add_argument( - "--install-dir", - type=Path, - default=Path(".llvm/build-install/lib"), - help="Path to the LLVM install lib directory (default: .llvm/build-install/lib)", - ) - parser.add_argument( - "--build-dir", - type=Path, - default=Path("build"), - help="CMake build directory used for validation builds (default: build)", - ) - parser.add_argument( - "--manifest", - type=Path, - default=Path("pruned-libs.json"), - help="Path to manifest JSON for recording or applying deletions", - ) - parser.add_argument( - "--gh-run-id", - type=str, - help="GitHub run ID to download manifest from when applying", - ) - parser.add_argument( - "--gh-artifact", - type=str, - default="llvm-pruned-libs", - help="Artifact name/pattern that contains the manifest", - ) - parser.add_argument( - "--gh-download-dir", - type=Path, - default=Path("artifacts"), - help="Directory to place downloaded artifacts", - ) - parser.add_argument( - "--max-attempts", - type=int, - default=30, - help="Maximum attempts when waiting for manifest download", - ) - parser.add_argument( - "--sleep-seconds", - type=int, - default=60, - help="Seconds to sleep between manifest download attempts", - ) - parser.add_argument( - "--skip-pattern", - action="append", - default=[], - help="Glob pattern for library names to never prune (can be repeated)", - ) - return parser.parse_args() - - -def _print_failure_output( - result: subprocess.CalledProcessError, max_lines: int = 50 -) -> None: - stdout = getattr(result, "stdout", "") or "" - stderr = getattr(result, "stderr", "") or "" - combined = stdout + stderr - if not combined: - return - print("Build output (last lines):") - lines = combined.splitlines() - for line in lines[-max_lines:]: - print(line) - - -def run_build(build_dir: Path) -> bool: - cmd = ["cmake", "--build", str(build_dir)] - print(f"Running: {' '.join(cmd)}") - try: - subprocess.run(cmd, check=True, capture_output=True, text=True) - return True - except subprocess.CalledProcessError as exc: - _print_failure_output(exc) - return False - - -def candidate_files( - install_dir: Path, skip_patterns: Optional[List[str]] = None -) -> Iterable[Path]: - if not install_dir.is_dir(): - raise FileNotFoundError(f"lib dir not found: {install_dir}") - for path in sorted(install_dir.iterdir()): - if not path.is_file(): - continue - if path.suffix.lower() not in {".a", ".lib"}: - print(f"Skipping non-static file: {path.name}") - continue - if skip_patterns and any( - fnmatch.fnmatch(path.name, p) for p in skip_patterns - ): - print(f"Skipping (never-prune): {path.name}") - continue - yield path - - -def _remove_binaries(build_dir: Path) -> None: - bin_dir = build_dir / "bin" - if not bin_dir.is_dir(): - return - for f in bin_dir.iterdir(): - if f.is_file() and f.suffix in {"", ".exe"}: - f.unlink() - - -def try_delete(path: Path, build_dir: Path) -> Optional[int]: - size = path.stat().st_size - backup = path.with_suffix(path.suffix + ".bak") - print(f"Testing deletion: {path}") - shutil.move(path, backup) - _remove_binaries(build_dir) - success = run_build(build_dir) - if success: - backup.unlink(missing_ok=True) - print(f"Safe to delete: {path.name} ({size} bytes)") - return size - shutil.move(backup, path) - print(f"Required; restored: {path.name}") - return None - - -ARCHIVE_MAGIC = b"!\n" - - -def _is_shared_lib(path: Path) -> bool: - return ".so" in path.suffixes or ".dylib" in path.suffixes - - -def _nullify_shared_libs(install_dir: Path) -> List[dict]: - nullified: List[dict] = [] - for path in sorted(install_dir.iterdir()): - if not path.is_file() or not _is_shared_lib(path): - continue - size = path.stat().st_size - if size > 0: - print(f"Nullifying shared lib: {path.name} ({size} bytes)") - path.write_bytes(b"") - nullified.append({"name": path.name, "size": size}) - return nullified - - -def _replace_with_empty_archive(path: Path) -> None: - if _is_shared_lib(path): - path.write_bytes(b"") - else: - path.write_bytes(ARCHIVE_MAGIC) - - -def discover( - install_dir: Path, - build_dir: Path, - skip_patterns: Optional[List[str]] = None, -) -> List[dict]: - nullified = _nullify_shared_libs(install_dir) - deletable: List[dict] = [] - for path in candidate_files(install_dir, skip_patterns): - size = try_delete(path, build_dir) - if size is not None: - deletable.append({"name": path.name, "size": size}) - return nullified + deletable - - -def write_manifest( - manifest: Path, removed: List[dict], install_dir: Path, build_dir: Path -) -> None: - total_size = sum(entry["size"] for entry in removed) - data = { - "generated_at": datetime.now(timezone.utc).isoformat(), - "install_dir": str(install_dir), - "build_dir": str(build_dir), - "total_saved_bytes": total_size, - "removed": removed, - } - manifest.write_text(json.dumps(data, indent=2)) - print(f"Wrote manifest with {len(removed)} entries to {manifest}") - print(f"Total space saved: {total_size / 1048576:.1f} MB") - - -def apply_manifest(manifest: Path, install_dir: Path) -> None: - if not manifest.is_file(): - raise FileNotFoundError(f"Manifest not found: {manifest}") - data = json.loads(manifest.read_text()) - removed = data.get("removed", []) - if not isinstance(removed, list): - raise ValueError("Manifest missing 'removed' list") - for entry in removed: - name = entry["name"] if isinstance(entry, dict) else entry - target = install_dir / name - if target.exists(): - print(f"Replacing with empty archive: {target}") - _replace_with_empty_archive(target) - else: - print(f"Already absent: {target}") - - -def find_manifest(download_dir: Path, manifest_name: str) -> Optional[Path]: - for path in download_dir.rglob(manifest_name): - if path.is_file(): - return path - return None - - -def wait_and_download_manifest( - run_id: str, - artifact: str, - download_dir: Path, - manifest_name: str, - max_attempts: int, - sleep_seconds: int, -) -> Path: - download_dir.mkdir(parents=True, exist_ok=True) - for attempt in range(1, max_attempts + 1): - print( - f"[{attempt}/{max_attempts}] Downloading manifest via gh run download " - f"(run={run_id}, artifact={artifact})" - ) - cmd = [ - "gh", - "run", - "download", - str(run_id), - "--pattern", - artifact, - "--dir", - str(download_dir), - ] - result = subprocess.run(cmd, capture_output=True, text=True) - if result.returncode == 0: - found = find_manifest(download_dir, manifest_name) - if found: - print(f"Found manifest at {found}") - return found - print("Download succeeded but manifest not found; retrying...") - else: - print("gh run download failed; stderr follows:") - print(result.stderr) - if attempt < max_attempts: - time.sleep(sleep_seconds) - raise RuntimeError("Manifest could not be downloaded within the allotted attempts") - - -def ensure_manifest( - manifest: Path, - run_id: Optional[str], - artifact: str, - download_dir: Path, - max_attempts: int, - sleep_seconds: int, -) -> Path: - if manifest.exists(): - return manifest - if not run_id: - raise FileNotFoundError( - f"Manifest {manifest} missing and no gh run ID provided for download" - ) - downloaded = wait_and_download_manifest( - run_id=run_id, - artifact=artifact, - download_dir=download_dir, - manifest_name=manifest.name, - max_attempts=max_attempts, - sleep_seconds=sleep_seconds, - ) - if downloaded != manifest: - shutil.copy(downloaded, manifest) - return manifest - - -def main() -> None: - args = parse_args() - install_dir = args.install_dir - build_dir = args.build_dir - if args.action == "discover": - deletable = discover(install_dir, build_dir, args.skip_pattern) - write_manifest(args.manifest, deletable, install_dir, build_dir) - else: - manifest = ensure_manifest( - manifest=args.manifest, - run_id=args.gh_run_id, - artifact=args.gh_artifact, - download_dir=args.gh_download_dir, - max_attempts=args.max_attempts, - sleep_seconds=args.sleep_seconds, - ) - apply_manifest(manifest, install_dir) - - -if __name__ == "__main__": - main() diff --git a/scripts/release-llvm.py b/scripts/release-llvm.py new file mode 100644 index 00000000..fa424b14 --- /dev/null +++ b/scripts/release-llvm.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +""" +LLVM release pipeline utilities. + +Actions: + discover: Iteratively probe which static libs can be removed by deleting + and rebuilding, then write the result to a manifest JSON. + apply: Read a manifest and replace listed libs with empty archives. + repackage: Download all LLVM build artifacts, apply pruning, and repackage + them in parallel. +""" + +import argparse +import concurrent.futures +import fnmatch +import json +import shutil +import subprocess +import sys +import tempfile +from datetime import datetime, timezone +from pathlib import Path +from typing import Iterable, List, Optional + +ARTIFACTS = [ + "aarch64-linux-gnu-releasedbg-lto.tar.xz", + "aarch64-linux-gnu-releasedbg.tar.xz", + "aarch64-windows-msvc-releasedbg-lto.tar.xz", + "aarch64-windows-msvc-releasedbg.tar.xz", + "arm64-macos-clang-debug-asan.tar.xz", + "arm64-macos-clang-releasedbg-lto.tar.xz", + "arm64-macos-clang-releasedbg.tar.xz", + "x64-linux-gnu-debug-asan.tar.xz", + "x64-linux-gnu-releasedbg-lto.tar.xz", + "x64-linux-gnu-releasedbg.tar.xz", + "x64-macos-clang-releasedbg-lto.tar.xz", + "x64-macos-clang-releasedbg.tar.xz", + "x64-windows-msvc-releasedbg-lto.tar.xz", + "x64-windows-msvc-releasedbg.tar.xz", +] + +MANIFEST_DIRS = { + "linux": "prune-manifest-ubuntu-24.04", + "macos": "prune-manifest-macos-15", + "windows": "prune-manifest-windows-2025", +} + +ARCHIVE_MAGIC = b"!\n" + + +def _is_shared_lib(path: Path) -> bool: + return ".so" in path.suffixes or ".dylib" in path.suffixes + + +def _replace_with_empty_archive(path: Path) -> None: + if _is_shared_lib(path): + path.write_bytes(b"") + else: + path.write_bytes(ARCHIVE_MAGIC) + + +def _remove_binaries(build_dir: Path) -> None: + bin_dir = build_dir / "bin" + if not bin_dir.is_dir(): + return + for f in bin_dir.iterdir(): + if f.is_file() and f.suffix in {"", ".exe"}: + f.unlink() + + +def _run_build(build_dir: Path) -> bool: + try: + subprocess.run( + ["cmake", "--build", str(build_dir)], + check=True, + capture_output=True, + text=True, + ) + return True + except subprocess.CalledProcessError as exc: + combined = (exc.stdout or "") + (exc.stderr or "") + if combined: + print("Build output (last lines):") + for line in combined.splitlines()[-50:]: + print(line) + return False + + +def _manifest_for(artifact: str, manifests_dir: Path) -> Optional[Path]: + for platform, dirname in MANIFEST_DIRS.items(): + if platform in artifact: + return manifests_dir / dirname / "pruned-libs.json" + return None + + +# ── discover ───────────────────────────────────────────────────────── + + +def _candidate_files( + install_dir: Path, skip_patterns: Optional[List[str]] = None +) -> Iterable[Path]: + if not install_dir.is_dir(): + raise FileNotFoundError(f"lib dir not found: {install_dir}") + for path in sorted(install_dir.iterdir()): + if not path.is_file(): + continue + if path.suffix.lower() not in {".a", ".lib"}: + print(f"Skipping non-static file: {path.name}") + continue + if skip_patterns and any( + fnmatch.fnmatch(path.name, p) for p in skip_patterns + ): + print(f"Skipping (never-prune): {path.name}") + continue + yield path + + +def _nullify_shared_libs(install_dir: Path) -> List[dict]: + nullified: List[dict] = [] + for path in sorted(install_dir.iterdir()): + if not path.is_file() or not _is_shared_lib(path): + continue + size = path.stat().st_size + if size > 0: + print(f"Nullifying shared lib: {path.name} ({size} bytes)") + path.write_bytes(b"") + nullified.append({"name": path.name, "size": size}) + return nullified + + +def _try_delete(path: Path, build_dir: Path) -> Optional[int]: + size = path.stat().st_size + backup = path.with_suffix(path.suffix + ".bak") + print(f"Testing deletion: {path}") + shutil.move(path, backup) + _remove_binaries(build_dir) + if _run_build(build_dir): + backup.unlink(missing_ok=True) + print(f"Safe to delete: {path.name} ({size} bytes)") + return size + shutil.move(backup, path) + print(f"Required; restored: {path.name}") + return None + + +def discover( + install_dir: Path, + build_dir: Path, + skip_patterns: Optional[List[str]] = None, +) -> List[dict]: + nullified = _nullify_shared_libs(install_dir) + deletable: List[dict] = [] + for path in _candidate_files(install_dir, skip_patterns): + size = _try_delete(path, build_dir) + if size is not None: + deletable.append({"name": path.name, "size": size}) + return nullified + deletable + + +def _write_manifest( + manifest: Path, removed: List[dict], install_dir: Path, build_dir: Path +) -> None: + total_size = sum(e["size"] for e in removed) + data = { + "generated_at": datetime.now(timezone.utc).isoformat(), + "install_dir": str(install_dir), + "build_dir": str(build_dir), + "total_saved_bytes": total_size, + "removed": removed, + } + manifest.write_text(json.dumps(data, indent=2)) + print(f"Wrote manifest with {len(removed)} entries to {manifest}") + print(f"Total space saved: {total_size / 1048576:.1f} MB") + + +# ── apply ──────────────────────────────────────────────────────────── + + +def apply_manifest(manifest: Path, install_dir: Path) -> None: + if not manifest.is_file(): + raise FileNotFoundError(f"Manifest not found: {manifest}") + data = json.loads(manifest.read_text()) + removed = data.get("removed", []) + if not isinstance(removed, list): + raise ValueError("Manifest missing 'removed' list") + for entry in removed: + name = entry["name"] if isinstance(entry, dict) else entry + target = install_dir / name + if target.exists(): + _replace_with_empty_archive(target) + else: + print(f"Already absent: {target}") + + +# ── repackage ──────────────────────────────────────────────────────── + + +def _process_artifact( + artifact: str, + source_run_id: str, + manifests_dir: Path, + output_dir: Path, +) -> None: + workdir = Path(tempfile.mkdtemp(prefix="repackage-")) + try: + dl_dir = workdir / "dl" + content_dir = workdir / "content" + + print(f"[{artifact}] Downloading...", flush=True) + subprocess.run( + ["gh", "run", "download", source_run_id, + "-n", artifact, "-D", str(dl_dir)], + check=True, + ) + + print(f"[{artifact}] Extracting...", flush=True) + content_dir.mkdir() + subprocess.run( + ["tar", "--strip-components=1", "-xf", + str(dl_dir / artifact), "-C", str(content_dir)], + check=True, + ) + shutil.rmtree(dl_dir) + + if "debug" in artifact or "asan" in artifact: + print(f"[{artifact}] Debug/ASAN — skipping prune", flush=True) + else: + manifest = _manifest_for(artifact, manifests_dir) + if manifest and manifest.is_file(): + print(f"[{artifact}] Pruning...", flush=True) + apply_manifest(manifest, content_dir / "lib") + else: + print(f"[{artifact}] WARNING: no manifest, skipping prune", flush=True) + + print(f"[{artifact}] Repackaging...", flush=True) + output_path = output_dir / artifact + with output_path.open("wb") as out: + tar = subprocess.Popen( + ["tar", "-C", str(content_dir), "-cf", "-", "."], + stdout=subprocess.PIPE, + ) + xz = subprocess.Popen( + ["xz", "-T0", "-6", "-c"], + stdin=tar.stdout, + stdout=out, + ) + tar.stdout.close() + xz.communicate() + tar.wait() + if tar.returncode != 0 or xz.returncode != 0: + raise RuntimeError( + f"tar/xz failed (tar={tar.returncode}, xz={xz.returncode})" + ) + + size_mb = output_path.stat().st_size / 1048576 + print(f"[{artifact}] Done ({size_mb:.1f} MB)", flush=True) + finally: + shutil.rmtree(workdir, ignore_errors=True) + + +def repackage( + source_run_id: str, + manifests_dir: Path, + output_dir: Path, + max_parallel: int = 3, +) -> None: + output_dir.mkdir(parents=True, exist_ok=True) + + failed: List[str] = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=max_parallel) as pool: + futures = { + pool.submit( + _process_artifact, a, source_run_id, manifests_dir, output_dir + ): a + for a in ARTIFACTS + } + for future in concurrent.futures.as_completed(futures): + artifact = futures[future] + try: + future.result() + except Exception as exc: + print(f"[{artifact}] FAILED: {exc}", flush=True) + failed.append(artifact) + + if failed: + print(f"\nFailed artifacts ({len(failed)}):") + for name in failed: + print(f" {name}") + sys.exit(1) + + print(f"\nAll {len(ARTIFACTS)} artifacts repackaged:") + for path in sorted(output_dir.iterdir()): + if path.is_file() and path.suffix != ".json": + print(f" {path.stat().st_size / 1048576:>8.1f} MB {path.name}") + + +def _find_manifest(download_dir: Path, manifest_name: str) -> Optional[Path]: + for path in download_dir.rglob(manifest_name): + if path.is_file(): + return path + return None + + +def _wait_and_download_manifest( + run_id: str, + artifact: str, + download_dir: Path, + manifest_name: str, + max_attempts: int, + sleep_seconds: int, +) -> Path: + import time + + download_dir.mkdir(parents=True, exist_ok=True) + for attempt in range(1, max_attempts + 1): + print( + f"[{attempt}/{max_attempts}] Downloading manifest " + f"(run={run_id}, artifact={artifact})" + ) + result = subprocess.run( + ["gh", "run", "download", str(run_id), + "--pattern", artifact, "--dir", str(download_dir)], + capture_output=True, text=True, + ) + if result.returncode == 0: + found = _find_manifest(download_dir, manifest_name) + if found: + print(f"Found manifest at {found}") + return found + print("Download succeeded but manifest not found; retrying...") + else: + print(f"gh run download failed: {result.stderr.strip()}") + if attempt < max_attempts: + time.sleep(sleep_seconds) + raise RuntimeError("Manifest could not be downloaded within the allotted attempts") + + +def _ensure_manifest( + manifest: Path, + run_id: Optional[str], + artifact: str, + download_dir: Path, + max_attempts: int, + sleep_seconds: int, +) -> Path: + if manifest.exists(): + return manifest + if not run_id: + raise FileNotFoundError( + f"Manifest {manifest} missing and no gh run ID provided" + ) + downloaded = _wait_and_download_manifest( + run_id, artifact, download_dir, manifest.name, max_attempts, sleep_seconds, + ) + if downloaded != manifest: + shutil.copy(downloaded, manifest) + return manifest + + +# ── CLI ────────────────────────────────────────────────────────────── + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="LLVM release pipeline utilities.") + parser.add_argument( + "--action", + choices=["discover", "apply", "repackage"], + required=True, + ) + parser.add_argument("--install-dir", type=Path) + parser.add_argument("--build-dir", type=Path) + parser.add_argument("--manifest", type=Path) + parser.add_argument("--skip-pattern", action="append", default=[]) + # apply: download manifest from another job if not present locally + parser.add_argument("--gh-run-id", type=str) + parser.add_argument("--gh-artifact", type=str, default="llvm-pruned-libs") + parser.add_argument("--gh-download-dir", type=Path, default=Path("artifacts")) + parser.add_argument("--max-attempts", type=int, default=30) + parser.add_argument("--sleep-seconds", type=int, default=60) + # repackage + parser.add_argument("--source-run-id", type=str) + parser.add_argument("--manifests-dir", type=Path) + parser.add_argument("--output-dir", type=Path) + parser.add_argument("--max-parallel", type=int, default=3) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + + if args.action == "discover": + removed = discover(args.install_dir, args.build_dir, args.skip_pattern) + _write_manifest(args.manifest, removed, args.install_dir, args.build_dir) + elif args.action == "apply": + manifest = _ensure_manifest( + manifest=args.manifest, + run_id=args.gh_run_id, + artifact=args.gh_artifact, + download_dir=args.gh_download_dir, + max_attempts=args.max_attempts, + sleep_seconds=args.sleep_seconds, + ) + apply_manifest(manifest, args.install_dir) + elif args.action == "repackage": + repackage( + source_run_id=args.source_run_id, + manifests_dir=args.manifests_dir, + output_dir=args.output_dir, + max_parallel=args.max_parallel, + ) + + +if __name__ == "__main__": + main() From 8a1ff5759e17524e5f90cf3550e1c0b4346c1bb6 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 7 Jun 2026 02:15:11 +0800 Subject: [PATCH 16/28] ci: split repackage into parallel matrix jobs with direct release upload Each of the 14 artifacts gets its own runner, uploads directly to the GitHub release via gh release upload. Metadata is collected via Actions artifacts and merged in a finalize job. Removed auto-PR creation. --- .github/workflows/release-llvm.yml | 147 +++++++++++++++-------------- scripts/release-llvm.py | 65 ++++++++++++- 2 files changed, 139 insertions(+), 73 deletions(-) diff --git a/.github/workflows/release-llvm.yml b/.github/workflows/release-llvm.yml index 8195065b..2b5e804a 100644 --- a/.github/workflows/release-llvm.yml +++ b/.github/workflows/release-llvm.yml @@ -101,22 +101,52 @@ jobs: name: prune-manifest-${{ matrix.os }} path: pruned-libs.json - release: + create-release: needs: discover - runs-on: macos-15 + runs-on: ubuntu-24.04 + steps: + - name: Create release + env: + GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} + run: | + TAG="${{ env.LLVM_VERSION }}" + REPO="clice-io/clice-llvm" + gh release delete "$TAG" --repo "$REPO" -y 2>/dev/null || true + gh release create "$TAG" \ + --repo "$REPO" \ + --title "$TAG" \ + --notes "LLVM ${TAG} — build run https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + repackage: + needs: [discover, create-release] + runs-on: ubuntu-24.04 permissions: contents: read + strategy: + fail-fast: false + matrix: + artifact: + - aarch64-linux-gnu-releasedbg-lto.tar.xz + - aarch64-linux-gnu-releasedbg.tar.xz + - aarch64-windows-msvc-releasedbg-lto.tar.xz + - aarch64-windows-msvc-releasedbg.tar.xz + - arm64-macos-clang-debug-asan.tar.xz + - arm64-macos-clang-releasedbg-lto.tar.xz + - arm64-macos-clang-releasedbg.tar.xz + - x64-linux-gnu-debug-asan.tar.xz + - x64-linux-gnu-releasedbg-lto.tar.xz + - x64-linux-gnu-releasedbg.tar.xz + - x64-macos-clang-releasedbg-lto.tar.xz + - x64-macos-clang-releasedbg.tar.xz + - x64-windows-msvc-releasedbg-lto.tar.xz + - x64-windows-msvc-releasedbg.tar.xz steps: - uses: actions/checkout@v4 - name: Free disk space run: | - sudo rm -rf ~/Library/Android /usr/local/share/dotnet \ - /usr/local/share/powershell ~/.ghcup - if [ -d "/Library/Developer/CoreSimulator" ]; then - sudo rm -rf /Library/Developer/CoreSimulator - fi - df -h / + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ + /usr/local/share/powershell /usr/share/swift /usr/local/.ghcup - name: Download prune manifests uses: actions/download-artifact@v4 @@ -124,7 +154,7 @@ jobs: pattern: prune-manifest-* path: manifests - - name: Repackage artifacts + - name: Repackage env: GH_TOKEN: ${{ github.token }} run: | @@ -133,81 +163,60 @@ jobs: --source-run-id "${{ env.SOURCE_RUN_ID }}" \ --manifests-dir manifests \ --output-dir artifacts \ - --max-parallel 3 + --max-parallel 1 \ + --version "${{ env.LLVM_VERSION }}" \ + --artifacts "${{ matrix.artifact }}" - - name: Upload to clice-llvm + - name: Upload to release env: GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} run: | - python3 scripts/upload-llvm.py \ - "${{ env.LLVM_VERSION }}" \ - "clice-io/clice-llvm" \ - "${{ github.run_id }}" + gh release upload "${{ env.LLVM_VERSION }}" \ + "artifacts/${{ matrix.artifact }}" \ + --repo clice-io/clice-llvm - - name: Save manifest + - name: Upload metadata uses: actions/upload-artifact@v4 with: - name: llvm-manifest-final - path: artifacts/llvm-manifest.json - if-no-files-found: error + name: metadata-${{ matrix.artifact }} + path: artifacts/${{ matrix.artifact }}.meta.json compression-level: 0 + retention-days: 1 - update-clice: - needs: release + finalize: + needs: repackage runs-on: ubuntu-24.04 - permissions: - contents: write - pull-requests: write steps: - - uses: actions/checkout@v4 - - - name: Download manifest + - name: Download all metadata uses: actions/download-artifact@v4 with: - name: llvm-manifest-final - path: . + pattern: metadata-* + path: metadata + merge-multiple: true - - name: Update manifest and version + - name: Build manifest run: | - python3 scripts/update-llvm-version.py \ - --version "${{ env.LLVM_VERSION }}" \ - --manifest-src llvm-manifest.json \ - --manifest-dest config/llvm-manifest.json \ - --package-cmake cmake/package.cmake + python3 -c " + import json, pathlib + entries = [json.loads(p.read_text()) for p in sorted(pathlib.Path('metadata').glob('*.meta.json'))] + with open('llvm-manifest.json', 'w') as f: + json.dump(entries, f, indent=2) + f.write('\n') + print(f'Manifest: {len(entries)} entries') + " - - name: Commit and push + - name: Upload manifest to release env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} run: | - VERSION="${{ env.LLVM_VERSION }}" - BRANCH="chore/update-llvm-${VERSION}" - RELEASE_URL="https://github.com/clice-io/clice-llvm/releases/tag/${VERSION}" - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git checkout -b "${BRANCH}" - git add config/llvm-manifest.json cmake/package.cmake - git commit -m "chore: update LLVM to ${VERSION}" - git push --force-with-lease origin "${BRANCH}" - - EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number // empty') - - BODY="$(cat < None: print(f"Already absent: {target}") +# ── metadata ───────────────────────────────────────────────────────── + + +def _sha256sum(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _build_metadata_entry(path: Path, version: str) -> dict: + name = path.name.lower() + if "windows" in name: + platform = "windows" + elif "linux" in name: + platform = "linux" + elif "macos" in name: + platform = "macosx" + else: + platform = "unknown" + + if name.startswith("aarch64-") or name.startswith("arm64-"): + arch = "arm64" + elif name.startswith("x64-") or name.startswith("x86_64-"): + arch = "x64" + else: + arch = "unknown" + + return { + "version": version, + "filename": path.name, + "sha256": _sha256sum(path), + "lto": "-lto" in name, + "asan": "-asan" in name, + "platform": platform, + "arch": arch, + "build_type": "Debug" if "debug" in name else "RelWithDebInfo", + } + + # ── repackage ──────────────────────────────────────────────────────── @@ -200,6 +242,7 @@ def _process_artifact( source_run_id: str, manifests_dir: Path, output_dir: Path, + version: Optional[str] = None, ) -> None: workdir = Path(tempfile.mkdtemp(prefix="repackage-")) try: @@ -240,7 +283,7 @@ def _process_artifact( stdout=subprocess.PIPE, ) xz = subprocess.Popen( - ["xz", "-T0", "-6", "-c"], + ["xz", "-T0", "-3", "-c"], stdin=tar.stdout, stdout=out, ) @@ -254,6 +297,11 @@ def _process_artifact( size_mb = output_path.stat().st_size / 1048576 print(f"[{artifact}] Done ({size_mb:.1f} MB)", flush=True) + + if version: + meta = _build_metadata_entry(output_path, version) + meta_path = output_dir / f"{artifact}.meta.json" + meta_path.write_text(json.dumps(meta, indent=2)) finally: shutil.rmtree(workdir, ignore_errors=True) @@ -263,16 +311,19 @@ def repackage( manifests_dir: Path, output_dir: Path, max_parallel: int = 3, + artifacts: Optional[List[str]] = None, + version: Optional[str] = None, ) -> None: + targets = artifacts if artifacts else ARTIFACTS output_dir.mkdir(parents=True, exist_ok=True) failed: List[str] = [] with concurrent.futures.ThreadPoolExecutor(max_workers=max_parallel) as pool: futures = { pool.submit( - _process_artifact, a, source_run_id, manifests_dir, output_dir + _process_artifact, a, source_run_id, manifests_dir, output_dir, version ): a - for a in ARTIFACTS + for a in targets } for future in concurrent.futures.as_completed(futures): artifact = futures[future] @@ -288,7 +339,7 @@ def repackage( print(f" {name}") sys.exit(1) - print(f"\nAll {len(ARTIFACTS)} artifacts repackaged:") + print(f"\nAll {len(targets)} artifacts repackaged:") for path in sorted(output_dir.iterdir()): if path.is_file() and path.suffix != ".json": print(f" {path.stat().st_size / 1048576:>8.1f} MB {path.name}") @@ -382,6 +433,8 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--manifests-dir", type=Path) parser.add_argument("--output-dir", type=Path) parser.add_argument("--max-parallel", type=int, default=3) + parser.add_argument("--artifacts", nargs="*") + parser.add_argument("--version", type=str) return parser.parse_args() @@ -407,7 +460,11 @@ def main() -> None: manifests_dir=args.manifests_dir, output_dir=args.output_dir, max_parallel=args.max_parallel, + artifacts=args.artifacts, + version=args.version, ) + elif args.action == "merge-metadata": + merge_metadata(args.metadata_dir, args.output_dir / "llvm-manifest.json") if __name__ == "__main__": From 2701592040547b397280cf344249677ebc992b3b Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 7 Jun 2026 03:07:35 +0800 Subject: [PATCH 17/28] fix(release): use xz -6 compression to keep assets under 2GB limit macOS LTO artifacts exceeded GitHub's 2GB release asset limit at -3. With per-job parallelism, compression speed is no longer a bottleneck. --- scripts/release-llvm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release-llvm.py b/scripts/release-llvm.py index ee99fa57..aef0f4bf 100644 --- a/scripts/release-llvm.py +++ b/scripts/release-llvm.py @@ -283,7 +283,7 @@ def _process_artifact( stdout=subprocess.PIPE, ) xz = subprocess.Popen( - ["xz", "-T0", "-3", "-c"], + ["xz", "-T0", "-6", "-c"], stdin=tar.stdout, stdout=out, ) From b6eaf2182dc9c5d59297de1aea73322cd5345eff Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 7 Jun 2026 03:07:35 +0800 Subject: [PATCH 18/28] fix(release): use xz -6 compression to keep assets under 2GB limit macOS LTO artifacts exceeded GitHub's 2GB release asset limit at -3. With per-job parallelism, compression speed is no longer a bottleneck. --- .github/workflows/release-llvm.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-llvm.yml b/.github/workflows/release-llvm.yml index 2b5e804a..c606050e 100644 --- a/.github/workflows/release-llvm.yml +++ b/.github/workflows/release-llvm.yml @@ -5,6 +5,7 @@ on: branches: [chore/upgrade-llvm-22] paths: - ".github/workflows/release-llvm.yml" + - "scripts/release-llvm.py" workflow_dispatch: inputs: source_run_id: From a98c3708f5de805d7e792bf7434c37e8475a4299 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 7 Jun 2026 05:01:33 +0800 Subject: [PATCH 19/28] fix(release): retry with xz -9 when artifact exceeds 2GB limit --- scripts/release-llvm.py | 47 +++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/scripts/release-llvm.py b/scripts/release-llvm.py index aef0f4bf..8f72018d 100644 --- a/scripts/release-llvm.py +++ b/scripts/release-llvm.py @@ -275,27 +275,38 @@ def _process_artifact( else: print(f"[{artifact}] WARNING: no manifest, skipping prune", flush=True) - print(f"[{artifact}] Repackaging...", flush=True) output_path = output_dir / artifact - with output_path.open("wb") as out: - tar = subprocess.Popen( - ["tar", "-C", str(content_dir), "-cf", "-", "."], - stdout=subprocess.PIPE, - ) - xz = subprocess.Popen( - ["xz", "-T0", "-6", "-c"], - stdin=tar.stdout, - stdout=out, - ) - tar.stdout.close() - xz.communicate() - tar.wait() - if tar.returncode != 0 or xz.returncode != 0: - raise RuntimeError( - f"tar/xz failed (tar={tar.returncode}, xz={xz.returncode})" + max_release_size = 2147483648 + + for xz_level in ("-6", "-9"): + print(f"[{artifact}] Repackaging (xz {xz_level})...", flush=True) + with output_path.open("wb") as out: + tar = subprocess.Popen( + ["tar", "-C", str(content_dir), "-cf", "-", "."], + stdout=subprocess.PIPE, ) + xz = subprocess.Popen( + ["xz", "-T0", xz_level, "-c"], + stdin=tar.stdout, + stdout=out, + ) + tar.stdout.close() + xz.communicate() + tar.wait() + if tar.returncode != 0 or xz.returncode != 0: + raise RuntimeError( + f"tar/xz failed (tar={tar.returncode}, xz={xz.returncode})" + ) + + file_size = output_path.stat().st_size + size_mb = file_size / 1048576 + if file_size <= max_release_size: + break + print( + f"[{artifact}] {size_mb:.1f} MB exceeds 2GB limit, retrying...", + flush=True, + ) - size_mb = output_path.stat().st_size / 1048576 print(f"[{artifact}] Done ({size_mb:.1f} MB)", flush=True) if version: From 3e6e9c7a7e84b4bc78d42666c7078045764d5d17 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 7 Jun 2026 10:58:30 +0800 Subject: [PATCH 20/28] refactor(release): use xz -9 compression and key-based manifest format - Always use xz -9 for repackage (sufficient to keep all artifacts under 2GB) - Change manifest format from flat array to {version, artifacts: {name: {sha256, ...}}} - Simplify CMake download: direct key lookup instead of iterating array - Extract _download_and_extract and _compress_tar_xz helpers --- .github/workflows/release-llvm.yml | 9 +- cmake/llvm.cmake | 68 ++++---- config/llvm-manifest.json | 257 +++++++++++++---------------- scripts/release-llvm.py | 60 +++---- scripts/update-llvm-version.py | 29 ++-- scripts/upload-llvm.py | 11 +- 6 files changed, 202 insertions(+), 232 deletions(-) diff --git a/.github/workflows/release-llvm.yml b/.github/workflows/release-llvm.yml index c606050e..166ab19b 100644 --- a/.github/workflows/release-llvm.yml +++ b/.github/workflows/release-llvm.yml @@ -200,10 +200,15 @@ jobs: python3 -c " import json, pathlib entries = [json.loads(p.read_text()) for p in sorted(pathlib.Path('metadata').glob('*.meta.json'))] + manifest = {'version': entries[0]['version'], 'artifacts': {}} + for entry in entries: + filename = entry.pop('filename') + entry.pop('version', None) + manifest['artifacts'][filename] = entry with open('llvm-manifest.json', 'w') as f: - json.dump(entries, f, indent=2) + json.dump(manifest, f, indent=2) f.write('\n') - print(f'Manifest: {len(entries)} entries') + print(f'Manifest: {len(manifest[\"artifacts\"])} artifacts') " - name: Upload manifest to release diff --git a/cmake/llvm.cmake b/cmake/llvm.cmake index ef9020ec..912b66b8 100644 --- a/cmake/llvm.cmake +++ b/cmake/llvm.cmake @@ -59,55 +59,51 @@ function(_detect_llvm_artifact_name OUT_FILENAME) set(${OUT_FILENAME} "${_ARCH}-${_PLATFORM}-${_TOOLCHAIN}-${_MODE}${_SUFFIX}.tar.xz" PARENT_SCOPE) endfunction() +function(_download_and_extract _URL _SHA256 _DEST _LABEL) + set(_DOWNLOAD_PATH "${CMAKE_CURRENT_BINARY_DIR}/${_LABEL}") + if(NOT EXISTS "${_DOWNLOAD_PATH}") + message(STATUS "Downloading ${_LABEL}") + file(DOWNLOAD "${_URL}" "${_DOWNLOAD_PATH}" + EXPECTED_HASH SHA256=${_SHA256} + SHOW_PROGRESS + STATUS _DL_STATUS) + list(GET _DL_STATUS 0 _DL_CODE) + if(NOT _DL_CODE EQUAL 0) + list(GET _DL_STATUS 1 _DL_MSG) + file(REMOVE "${_DOWNLOAD_PATH}") + message(FATAL_ERROR "Failed to download ${_LABEL}: ${_DL_MSG}") + endif() + endif() + + execute_process( + COMMAND "${CMAKE_COMMAND}" -E tar xf "${_DOWNLOAD_PATH}" + WORKING_DIRECTORY "${_DEST}" + RESULT_VARIABLE _TAR_RESULT) + if(NOT _TAR_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to extract ${_LABEL}") + endif() +endfunction() + function(_download_llvm LLVM_VERSION) _detect_llvm_artifact_name(_FILENAME) set(_MANIFEST_PATH "${PROJECT_SOURCE_DIR}/config/llvm-manifest.json") file(READ "${_MANIFEST_PATH}" _MANIFEST_JSON) - string(JSON _MANIFEST_LEN LENGTH "${_MANIFEST_JSON}") - math(EXPR _LAST_IDX "${_MANIFEST_LEN} - 1") - - set(_SHA256 "") - foreach(_IDX RANGE ${_LAST_IDX}) - string(JSON _ENTRY_FILENAME GET "${_MANIFEST_JSON}" ${_IDX} filename) - if("${_ENTRY_FILENAME}" STREQUAL "${_FILENAME}") - string(JSON _SHA256 GET "${_MANIFEST_JSON}" ${_IDX} sha256) - break() - endif() - endforeach() - if("${_SHA256}" STREQUAL "") + string(JSON _SHA256 ERROR_VARIABLE _ERR + GET "${_MANIFEST_JSON}" artifacts "${_FILENAME}" sha256) + if(_ERR) message(FATAL_ERROR "No matching LLVM artifact in manifest for: ${_FILENAME}") endif() - set(_URL "https://github.com/clice-io/clice-llvm/releases/download/${LLVM_VERSION}/${_FILENAME}") - set(_DOWNLOAD_PATH "${CMAKE_CURRENT_BINARY_DIR}/${_FILENAME}") + set(_BASE_URL "https://github.com/clice-io/clice-llvm/releases/download/${LLVM_VERSION}") set(_INSTALL_ROOT "${CMAKE_CURRENT_BINARY_DIR}/.llvm") if(NOT EXISTS "${_INSTALL_ROOT}/lib/cmake/llvm/LLVMConfig.cmake") - if(NOT EXISTS "${_DOWNLOAD_PATH}") - message(STATUS "Downloading LLVM ${LLVM_VERSION}: ${_FILENAME}") - file(DOWNLOAD "${_URL}" "${_DOWNLOAD_PATH}" - EXPECTED_HASH SHA256=${_SHA256} - SHOW_PROGRESS - STATUS _DL_STATUS) - list(GET _DL_STATUS 0 _DL_CODE) - if(NOT _DL_CODE EQUAL 0) - list(GET _DL_STATUS 1 _DL_MSG) - file(REMOVE "${_DOWNLOAD_PATH}") - message(FATAL_ERROR "Failed to download LLVM: ${_DL_MSG}") - endif() - endif() - - message(STATUS "Extracting LLVM to ${_INSTALL_ROOT}") file(MAKE_DIRECTORY "${_INSTALL_ROOT}") - execute_process( - COMMAND "${CMAKE_COMMAND}" -E tar xf "${_DOWNLOAD_PATH}" - WORKING_DIRECTORY "${_INSTALL_ROOT}" - RESULT_VARIABLE _TAR_RESULT) - if(NOT _TAR_RESULT EQUAL 0) - message(FATAL_ERROR "Failed to extract LLVM archive") - endif() + + _download_and_extract( + "${_BASE_URL}/${_FILENAME}" "${_SHA256}" "${_INSTALL_ROOT}" "${_FILENAME}") if(EXISTS "${_INSTALL_ROOT}/build-install") file(GLOB _NESTED "${_INSTALL_ROOT}/build-install/*") diff --git a/config/llvm-manifest.json b/config/llvm-manifest.json index fd9eefeb..a5092b9b 100644 --- a/config/llvm-manifest.json +++ b/config/llvm-manifest.json @@ -1,142 +1,117 @@ -[ - { - "version": "21.1.8", - "filename": "aarch64-linux-gnu-releasedbg-lto.tar.xz", - "sha256": "f3444ee840b50933c23656cbee7c4d010e752ac55ca66095b97f7c0e997b13b5", - "lto": true, - "asan": false, - "platform": "linux", - "arch": "arm64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "aarch64-linux-gnu-releasedbg.tar.xz", - "sha256": "b9012bf059e4d8673fb564b5780e5fc78c6a2e47f5cc6a39f444d1879b42dd2a", - "lto": false, - "asan": false, - "platform": "linux", - "arch": "arm64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "aarch64-windows-msvc-releasedbg-lto.tar.xz", - "sha256": "8870d16141ba7f9ea12f5147b8d91329abbbaa4376cd4576667dd323d896dd08", - "lto": true, - "asan": false, - "platform": "windows", - "arch": "arm64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "aarch64-windows-msvc-releasedbg.tar.xz", - "sha256": "ad394e79ec85dd40f942671bb0342ffe54a103eb2baabacb773999d57d80134b", - "lto": false, - "asan": false, - "platform": "windows", - "arch": "arm64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "arm64-macos-clang-debug-asan.tar.xz", - "sha256": "b02d20e4f7294ee33f49a09dfdd765b3b44135e003ef50e3a760aeee39e3f993", - "lto": false, - "asan": true, - "platform": "macosx", - "arch": "arm64", - "build_type": "Debug" - }, - { - "version": "21.1.8", - "filename": "arm64-macos-clang-releasedbg-lto.tar.xz", - "sha256": "e40c21eb0d0b91d9d4ab31212a5cb01ea46707f5c29839414567857e4147604d", - "lto": true, - "asan": false, - "platform": "macosx", - "arch": "arm64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "arm64-macos-clang-releasedbg.tar.xz", - "sha256": "e1b01de34f0edfd41c118e4981a93afb35556ae369597e864f4a393db623b926", - "lto": false, - "asan": false, - "platform": "macosx", - "arch": "arm64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "x64-linux-gnu-debug-asan.tar.xz", - "sha256": "76bb82d822b5377fb5e0fac8abcfba125142e6a0acc02bb36d1fa1532a268646", - "lto": false, - "asan": true, - "platform": "linux", - "arch": "x64", - "build_type": "Debug" - }, - { - "version": "21.1.8", - "filename": "x64-linux-gnu-releasedbg-lto.tar.xz", - "sha256": "32f5edddec1e689124f045b586fb402ae30febc05203af7391b088bc8494cd53", - "lto": true, - "asan": false, - "platform": "linux", - "arch": "x64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "x64-linux-gnu-releasedbg.tar.xz", - "sha256": "8ba3c84f23a2a81a86c54780754a61adf99048aa2ac0dc9b9708d0f842d553de", - "lto": false, - "asan": false, - "platform": "linux", - "arch": "x64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "x64-macos-clang-releasedbg-lto.tar.xz", - "sha256": "97e81d6296896d7237f118f728d05291707b9e4e5791e07ce4be8aee0517505d", - "lto": true, - "asan": false, - "platform": "macosx", - "arch": "x64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "x64-macos-clang-releasedbg.tar.xz", - "sha256": "53c13f8e1082fa2fe2f9c05303de48cb3133bf5f24271f4b3062f1dec578159c", - "lto": false, - "asan": false, - "platform": "macosx", - "arch": "x64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "x64-windows-msvc-releasedbg-lto.tar.xz", - "sha256": "16bcf0e4cbc3d2b1204edd619a3837004dacea28eeff0a101c8d0212f936427d", - "lto": true, - "asan": false, - "platform": "windows", - "arch": "x64", - "build_type": "RelWithDebInfo" - }, - { - "version": "21.1.8", - "filename": "x64-windows-msvc-releasedbg.tar.xz", - "sha256": "81d31fad05e200726c8178314b0b2045c947483dddd8cb974f4c376ae5f441fa", - "lto": false, - "asan": false, - "platform": "windows", - "arch": "x64", - "build_type": "RelWithDebInfo" +{ + "version": "21.1.8", + "artifacts": { + "aarch64-linux-gnu-releasedbg-lto.tar.xz": { + "sha256": "f3444ee840b50933c23656cbee7c4d010e752ac55ca66095b97f7c0e997b13b5", + "lto": true, + "asan": false, + "platform": "linux", + "arch": "arm64", + "build_type": "RelWithDebInfo" + }, + "aarch64-linux-gnu-releasedbg.tar.xz": { + "sha256": "b9012bf059e4d8673fb564b5780e5fc78c6a2e47f5cc6a39f444d1879b42dd2a", + "lto": false, + "asan": false, + "platform": "linux", + "arch": "arm64", + "build_type": "RelWithDebInfo" + }, + "aarch64-windows-msvc-releasedbg-lto.tar.xz": { + "sha256": "8870d16141ba7f9ea12f5147b8d91329abbbaa4376cd4576667dd323d896dd08", + "lto": true, + "asan": false, + "platform": "windows", + "arch": "arm64", + "build_type": "RelWithDebInfo" + }, + "aarch64-windows-msvc-releasedbg.tar.xz": { + "sha256": "ad394e79ec85dd40f942671bb0342ffe54a103eb2baabacb773999d57d80134b", + "lto": false, + "asan": false, + "platform": "windows", + "arch": "arm64", + "build_type": "RelWithDebInfo" + }, + "arm64-macos-clang-debug-asan.tar.xz": { + "sha256": "b02d20e4f7294ee33f49a09dfdd765b3b44135e003ef50e3a760aeee39e3f993", + "lto": false, + "asan": true, + "platform": "macosx", + "arch": "arm64", + "build_type": "Debug" + }, + "arm64-macos-clang-releasedbg-lto.tar.xz": { + "sha256": "e40c21eb0d0b91d9d4ab31212a5cb01ea46707f5c29839414567857e4147604d", + "lto": true, + "asan": false, + "platform": "macosx", + "arch": "arm64", + "build_type": "RelWithDebInfo" + }, + "arm64-macos-clang-releasedbg.tar.xz": { + "sha256": "e1b01de34f0edfd41c118e4981a93afb35556ae369597e864f4a393db623b926", + "lto": false, + "asan": false, + "platform": "macosx", + "arch": "arm64", + "build_type": "RelWithDebInfo" + }, + "x64-linux-gnu-debug-asan.tar.xz": { + "sha256": "76bb82d822b5377fb5e0fac8abcfba125142e6a0acc02bb36d1fa1532a268646", + "lto": false, + "asan": true, + "platform": "linux", + "arch": "x64", + "build_type": "Debug" + }, + "x64-linux-gnu-releasedbg-lto.tar.xz": { + "sha256": "32f5edddec1e689124f045b586fb402ae30febc05203af7391b088bc8494cd53", + "lto": true, + "asan": false, + "platform": "linux", + "arch": "x64", + "build_type": "RelWithDebInfo" + }, + "x64-linux-gnu-releasedbg.tar.xz": { + "sha256": "8ba3c84f23a2a81a86c54780754a61adf99048aa2ac0dc9b9708d0f842d553de", + "lto": false, + "asan": false, + "platform": "linux", + "arch": "x64", + "build_type": "RelWithDebInfo" + }, + "x64-macos-clang-releasedbg-lto.tar.xz": { + "sha256": "97e81d6296896d7237f118f728d05291707b9e4e5791e07ce4be8aee0517505d", + "lto": true, + "asan": false, + "platform": "macosx", + "arch": "x64", + "build_type": "RelWithDebInfo" + }, + "x64-macos-clang-releasedbg.tar.xz": { + "sha256": "53c13f8e1082fa2fe2f9c05303de48cb3133bf5f24271f4b3062f1dec578159c", + "lto": false, + "asan": false, + "platform": "macosx", + "arch": "x64", + "build_type": "RelWithDebInfo" + }, + "x64-windows-msvc-releasedbg-lto.tar.xz": { + "sha256": "16bcf0e4cbc3d2b1204edd619a3837004dacea28eeff0a101c8d0212f936427d", + "lto": true, + "asan": false, + "platform": "windows", + "arch": "x64", + "build_type": "RelWithDebInfo" + }, + "x64-windows-msvc-releasedbg.tar.xz": { + "sha256": "81d31fad05e200726c8178314b0b2045c947483dddd8cb974f4c376ae5f441fa", + "lto": false, + "asan": false, + "platform": "windows", + "arch": "x64", + "build_type": "RelWithDebInfo" + } } -] +} diff --git a/scripts/release-llvm.py b/scripts/release-llvm.py index 8f72018d..037e3b30 100644 --- a/scripts/release-llvm.py +++ b/scripts/release-llvm.py @@ -48,7 +48,6 @@ ARCHIVE_MAGIC = b"!\n" - def _is_shared_lib(path: Path) -> bool: return ".so" in path.suffixes or ".dylib" in path.suffixes @@ -237,6 +236,30 @@ def _build_metadata_entry(path: Path, version: str) -> dict: # ── repackage ──────────────────────────────────────────────────────── +def _compress_tar_xz( + source_dir: Path, output_path: Path, xz_level: str, label: str, +) -> int: + print(f"[{label}] Compressing (xz {xz_level})...", flush=True) + with output_path.open("wb") as out: + tar = subprocess.Popen( + ["tar", "-C", str(source_dir), "-cf", "-", "."], + stdout=subprocess.PIPE, + ) + xz = subprocess.Popen( + ["xz", "-T0", xz_level, "-c"], + stdin=tar.stdout, + stdout=out, + ) + tar.stdout.close() + xz.communicate() + tar.wait() + if tar.returncode != 0 or xz.returncode != 0: + raise RuntimeError( + f"tar/xz failed (tar={tar.returncode}, xz={xz.returncode})" + ) + return output_path.stat().st_size + + def _process_artifact( artifact: str, source_run_id: str, @@ -276,38 +299,9 @@ def _process_artifact( print(f"[{artifact}] WARNING: no manifest, skipping prune", flush=True) output_path = output_dir / artifact - max_release_size = 2147483648 - - for xz_level in ("-6", "-9"): - print(f"[{artifact}] Repackaging (xz {xz_level})...", flush=True) - with output_path.open("wb") as out: - tar = subprocess.Popen( - ["tar", "-C", str(content_dir), "-cf", "-", "."], - stdout=subprocess.PIPE, - ) - xz = subprocess.Popen( - ["xz", "-T0", xz_level, "-c"], - stdin=tar.stdout, - stdout=out, - ) - tar.stdout.close() - xz.communicate() - tar.wait() - if tar.returncode != 0 or xz.returncode != 0: - raise RuntimeError( - f"tar/xz failed (tar={tar.returncode}, xz={xz.returncode})" - ) - - file_size = output_path.stat().st_size - size_mb = file_size / 1048576 - if file_size <= max_release_size: - break - print( - f"[{artifact}] {size_mb:.1f} MB exceeds 2GB limit, retrying...", - flush=True, - ) + file_size = _compress_tar_xz(content_dir, output_path, "-9", artifact) - print(f"[{artifact}] Done ({size_mb:.1f} MB)", flush=True) + print(f"[{artifact}] Done ({file_size / 1048576:.1f} MB)", flush=True) if version: meta = _build_metadata_entry(output_path, version) @@ -474,8 +468,6 @@ def main() -> None: artifacts=args.artifacts, version=args.version, ) - elif args.action == "merge-metadata": - merge_metadata(args.metadata_dir, args.output_dir / "llvm-manifest.json") if __name__ == "__main__": diff --git a/scripts/update-llvm-version.py b/scripts/update-llvm-version.py index c18f8435..ab15ba9b 100755 --- a/scripts/update-llvm-version.py +++ b/scripts/update-llvm-version.py @@ -16,8 +16,8 @@ def copy_manifest(src: Path, dest: Path) -> None: print(f"Error: {src} is not valid JSON: {err}", file=sys.stderr) sys.exit(1) - if not isinstance(data, list) or len(data) == 0: - print(f"Error: {src} must be a non-empty JSON array", file=sys.stderr) + if not isinstance(data, dict) or "artifacts" not in data: + print(f"Error: {src} must be a JSON object with 'artifacts' key", file=sys.stderr) sys.exit(1) dest.parent.mkdir(parents=True, exist_ok=True) @@ -25,7 +25,7 @@ def copy_manifest(src: Path, dest: Path) -> None: json.dump(data, handle, indent=2) handle.write("\n") - print(f"Copied manifest: {src} -> {dest} ({len(data)} entries)") + print(f"Copied manifest: {src} -> {dest} ({len(data['artifacts'])} artifacts)") def update_package_cmake(path: Path, version: str) -> None: @@ -77,25 +77,24 @@ def check_package_cmake(path: Path) -> None: def check_manifest(path: Path) -> None: - """Verify the manifest is a well-formed non-empty array with required fields.""" + """Verify the manifest is well-formed with required fields.""" try: data = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as err: print(f"Error: {path} is not valid JSON: {err}", file=sys.stderr) sys.exit(1) - if not isinstance(data, list) or len(data) == 0: - print(f"Error: {path} must be a non-empty JSON array", file=sys.stderr) + if not isinstance(data, dict) or "artifacts" not in data: + print(f"Error: {path} must be a JSON object with 'artifacts' key", file=sys.stderr) sys.exit(1) - required = ("version", "platform", "arch", "build_type", "filename", "sha256") - for idx, entry in enumerate(data): - missing = [k for k in required if k not in entry] - if missing: - print( - f"Error: {path} entry {idx} is missing fields: {missing}", - file=sys.stderr, - ) + artifacts = data["artifacts"] + if not artifacts: + print(f"Error: {path} has no artifacts", file=sys.stderr) + sys.exit(1) + for name, entry in artifacts.items(): + if "sha256" not in entry: + print(f"Error: {path} artifact {name} missing 'sha256'", file=sys.stderr) sys.exit(1) - print(f"OK: {path} has {len(data)} well-formed entries") + print(f"OK: {path} has {len(artifacts)} well-formed artifacts") def main() -> None: diff --git a/scripts/upload-llvm.py b/scripts/upload-llvm.py index 5a0db3e5..8a93a079 100644 --- a/scripts/upload-llvm.py +++ b/scripts/upload-llvm.py @@ -82,13 +82,16 @@ def main() -> None: sys.exit(1) version_without_prefix = tag.lstrip("vV") - metadata = [ - build_metadata_entry(path, version_without_prefix) for path in artifact_files - ] + manifest = {"version": version_without_prefix, "artifacts": {}} + for path in artifact_files: + entry = build_metadata_entry(path, version_without_prefix) + filename = entry.pop("filename") + entry.pop("version", None) + manifest["artifacts"][filename] = entry json_path = artifacts_dir / "llvm-manifest.json" with json_path.open("w", encoding="utf-8") as handle: - json.dump(metadata, handle, indent=2) + json.dump(manifest, handle, indent=2) handle.write("\n") assets = [str(path) for path in artifact_files] From 3ccd22994726ca6a2562e096dfed9622a51a6db0 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 7 Jun 2026 11:01:42 +0800 Subject: [PATCH 21/28] refactor(release): use xz -9e extreme compression for maximum ratio --- scripts/release-llvm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release-llvm.py b/scripts/release-llvm.py index 037e3b30..78e4a636 100644 --- a/scripts/release-llvm.py +++ b/scripts/release-llvm.py @@ -299,7 +299,7 @@ def _process_artifact( print(f"[{artifact}] WARNING: no manifest, skipping prune", flush=True) output_path = output_dir / artifact - file_size = _compress_tar_xz(content_dir, output_path, "-9", artifact) + file_size = _compress_tar_xz(content_dir, output_path, "-9e", artifact) print(f"[{artifact}] Done ({file_size / 1048576:.1f} MB)", flush=True) From 0bb19b604a1313038f33799d0cdd564a5ca687e2 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 7 Jun 2026 13:01:26 +0800 Subject: [PATCH 22/28] chore: update llvm-manifest.json to 22.1.4 --- config/llvm-manifest.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/config/llvm-manifest.json b/config/llvm-manifest.json index a5092b9b..9c355b34 100644 --- a/config/llvm-manifest.json +++ b/config/llvm-manifest.json @@ -1,8 +1,8 @@ { - "version": "21.1.8", + "version": "22.1.4", "artifacts": { "aarch64-linux-gnu-releasedbg-lto.tar.xz": { - "sha256": "f3444ee840b50933c23656cbee7c4d010e752ac55ca66095b97f7c0e997b13b5", + "sha256": "dc25d1a6ca10d6f1faae8163f2ce328c0930a916bc4dc9763d16e9e57e87997f", "lto": true, "asan": false, "platform": "linux", @@ -10,7 +10,7 @@ "build_type": "RelWithDebInfo" }, "aarch64-linux-gnu-releasedbg.tar.xz": { - "sha256": "b9012bf059e4d8673fb564b5780e5fc78c6a2e47f5cc6a39f444d1879b42dd2a", + "sha256": "3b32f625730b67eb3303e51b1d430a6ec62fff74ea971e82534eec18a081850e", "lto": false, "asan": false, "platform": "linux", @@ -18,7 +18,7 @@ "build_type": "RelWithDebInfo" }, "aarch64-windows-msvc-releasedbg-lto.tar.xz": { - "sha256": "8870d16141ba7f9ea12f5147b8d91329abbbaa4376cd4576667dd323d896dd08", + "sha256": "23170fe0632aa2b3a9c6f98e9989e4eadae54d1d2dd2676c5e9acff0670cb2f0", "lto": true, "asan": false, "platform": "windows", @@ -26,7 +26,7 @@ "build_type": "RelWithDebInfo" }, "aarch64-windows-msvc-releasedbg.tar.xz": { - "sha256": "ad394e79ec85dd40f942671bb0342ffe54a103eb2baabacb773999d57d80134b", + "sha256": "67b86792734bdd755d2b4c6ada01ef5b7337954bb5ade510a37c2b79e858b1b1", "lto": false, "asan": false, "platform": "windows", @@ -34,7 +34,7 @@ "build_type": "RelWithDebInfo" }, "arm64-macos-clang-debug-asan.tar.xz": { - "sha256": "b02d20e4f7294ee33f49a09dfdd765b3b44135e003ef50e3a760aeee39e3f993", + "sha256": "e42dd49fa93506c21a7f1d4f0890da16271b8af64acfc9b908242000613baf35", "lto": false, "asan": true, "platform": "macosx", @@ -42,7 +42,7 @@ "build_type": "Debug" }, "arm64-macos-clang-releasedbg-lto.tar.xz": { - "sha256": "e40c21eb0d0b91d9d4ab31212a5cb01ea46707f5c29839414567857e4147604d", + "sha256": "f881ac42de055cd9e719a14408897fc935ddd9d98e5f2656ad26b6d220f4a0e8", "lto": true, "asan": false, "platform": "macosx", @@ -50,7 +50,7 @@ "build_type": "RelWithDebInfo" }, "arm64-macos-clang-releasedbg.tar.xz": { - "sha256": "e1b01de34f0edfd41c118e4981a93afb35556ae369597e864f4a393db623b926", + "sha256": "65bd4b9a458e5587872761ff22abb443610b0e6bcbc1964e6e4225776fe1e19a", "lto": false, "asan": false, "platform": "macosx", @@ -58,7 +58,7 @@ "build_type": "RelWithDebInfo" }, "x64-linux-gnu-debug-asan.tar.xz": { - "sha256": "76bb82d822b5377fb5e0fac8abcfba125142e6a0acc02bb36d1fa1532a268646", + "sha256": "d2196f3dedb8e1ea9179d2066b0a7c2ca4ff50328f630b20ffbb3bf9b58b8b57", "lto": false, "asan": true, "platform": "linux", @@ -66,7 +66,7 @@ "build_type": "Debug" }, "x64-linux-gnu-releasedbg-lto.tar.xz": { - "sha256": "32f5edddec1e689124f045b586fb402ae30febc05203af7391b088bc8494cd53", + "sha256": "dd36d45ad4e14bf8d3a66177598c140e3d5fff67e68e944cd944c55ae513c426", "lto": true, "asan": false, "platform": "linux", @@ -74,7 +74,7 @@ "build_type": "RelWithDebInfo" }, "x64-linux-gnu-releasedbg.tar.xz": { - "sha256": "8ba3c84f23a2a81a86c54780754a61adf99048aa2ac0dc9b9708d0f842d553de", + "sha256": "be92fd64d515db037765603c6ae899369fe2a2d42cc6b36c5bfa11d07d4b67ec", "lto": false, "asan": false, "platform": "linux", @@ -82,7 +82,7 @@ "build_type": "RelWithDebInfo" }, "x64-macos-clang-releasedbg-lto.tar.xz": { - "sha256": "97e81d6296896d7237f118f728d05291707b9e4e5791e07ce4be8aee0517505d", + "sha256": "c8aa85561037a4e53d15ae97cdc9d81afe5e1f9e198560ab4ba7da23a6be4f91", "lto": true, "asan": false, "platform": "macosx", @@ -90,7 +90,7 @@ "build_type": "RelWithDebInfo" }, "x64-macos-clang-releasedbg.tar.xz": { - "sha256": "53c13f8e1082fa2fe2f9c05303de48cb3133bf5f24271f4b3062f1dec578159c", + "sha256": "9fd6bf02f347a45f688478ceda363388dc828edc6c8ad292f7d329e9dd010070", "lto": false, "asan": false, "platform": "macosx", @@ -98,7 +98,7 @@ "build_type": "RelWithDebInfo" }, "x64-windows-msvc-releasedbg-lto.tar.xz": { - "sha256": "16bcf0e4cbc3d2b1204edd619a3837004dacea28eeff0a101c8d0212f936427d", + "sha256": "b3e7a70941af2f307bdeff166398bc319acc9d6fcf6df1cc0ba61424e0548376", "lto": true, "asan": false, "platform": "windows", @@ -106,7 +106,7 @@ "build_type": "RelWithDebInfo" }, "x64-windows-msvc-releasedbg.tar.xz": { - "sha256": "81d31fad05e200726c8178314b0b2045c947483dddd8cb974f4c376ae5f441fa", + "sha256": "ef67d91f49f586eba0fcd0722eed62524abdab921b4e71859aa8706689fd36b8", "lto": false, "asan": false, "platform": "windows", From 5a06cd02f2dd46a96aa4f75779e52c739bf74b1c Mon Sep 17 00:00:00 2001 From: ykiko Date: Tue, 9 Jun 2026 01:14:30 +0800 Subject: [PATCH 23/28] chore: remove unused setup-llvm.py Replaced by native CMake file(DOWNLOAD) in cmake/llvm.cmake. --- scripts/setup-llvm.py | 405 ------------------------------------------ 1 file changed, 405 deletions(-) delete mode 100644 scripts/setup-llvm.py diff --git a/scripts/setup-llvm.py b/scripts/setup-llvm.py deleted file mode 100644 index 8c530c42..00000000 --- a/scripts/setup-llvm.py +++ /dev/null @@ -1,405 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import hashlib -import json -import os -import shutil -import subprocess -import sys -import tarfile -from pathlib import Path -from urllib.error import HTTPError, URLError -from urllib.request import Request, urlopen - - -PRIVATE_CLANG_FILES = [ - "Sema/CoroutineStmtBuilder.h", - "Sema/TypeLocBuilder.h", - "Sema/TreeTransform.h", -] - - -def log(message: str) -> None: - print(f"[setup-llvm] {message}", flush=True) - - -def read_manifest(path: Path) -> list[dict]: - with path.open("r", encoding="utf-8") as handle: - return json.load(handle) - - -def detect_platform() -> str: - plat = sys.platform - if plat.startswith("win"): - return "Windows" - if plat == "darwin": - return "macosx" - if plat.startswith("linux"): - return "Linux" - raise RuntimeError(f"Unsupported platform: {plat}") - - -def detect_arch() -> str: - import platform - - machine = platform.machine().lower() - if machine in ("x86_64", "amd64"): - return "x64" - if machine in ("aarch64", "arm64"): - return "arm64" - raise RuntimeError(f"Unsupported architecture: {machine}") - - -def pick_artifact( - manifest: list[dict], - version: str, - build_type: str, - is_lto: bool, - platform: str, - arch: str, -) -> dict: - base_version = version.split("+", 1)[0] - saw_missing_arch = False - for entry in manifest: - if entry.get("version") != version: - continue - if entry.get("platform") != platform.lower(): - continue - entry_arch = entry.get("arch") - if entry_arch is None: - saw_missing_arch = True - continue - if entry_arch != arch: - continue - if entry.get("build_type") != build_type: - continue - if bool(entry.get("lto")) != is_lto: - continue - return entry - if saw_missing_arch: - raise RuntimeError( - f"Manifest contains entries without an 'arch' field for version={base_version}, " - f"platform={platform}. The manifest format changed to require explicit " - f"architectures; regenerate it via scripts/update-llvm-version.py." - ) - raise RuntimeError( - f"No matching LLVM artifact in manifest for version={base_version}, platform={platform}, " - f"arch={arch}, build_type={build_type}, lto={is_lto}" - ) - - -def sha256sum(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def download(url: str, dest: Path, token: str | None) -> None: - log(f"Start download: {url} -> {dest}") - dest.parent.mkdir(parents=True, exist_ok=True) - headers = {"User-Agent": "clice-setup-llvm"} - if token: - headers["Authorization"] = f"Bearer {token}" - request = Request(url, headers=headers) - try: - with urlopen(request) as response, dest.open("wb") as handle: - total_bytes = response.length - if total_bytes is None: - header_len = response.getheader("Content-Length") - if header_len and header_len.isdigit(): - total_bytes = int(header_len) - downloaded = 0 - next_percent = 10 - next_unknown_mark = 10 * 1024 * 1024 # 10MB steps when size is unknown - while True: - chunk = response.read(1024 * 512) - if not chunk: - break - handle.write(chunk) - downloaded += len(chunk) - if total_bytes: - percent = int(downloaded * 100 / total_bytes) - while percent >= next_percent and next_percent <= 100: - log( - f"Download progress: {next_percent}% " - f"({downloaded / 1024 / 1024:.1f}MB/" - f"{total_bytes / 1024 / 1024:.1f}MB)" - ) - next_percent += 10 - else: - if downloaded >= next_unknown_mark: - log( - f"Downloaded {downloaded / 1024 / 1024:.1f}MB (size unknown)" - ) - next_unknown_mark += 10 * 1024 * 1024 - if total_bytes and next_percent <= 100: - log("Download progress: 100% (size verified by server)") - log(f"Finished download: {dest} ({downloaded / 1024 / 1024:.1f}MB)") - except HTTPError as err: - raise RuntimeError(f"HTTP error {err.code} while downloading {url}") from err - except URLError as err: - raise RuntimeError(f"Failed to download {url}: {err.reason}") from err - - -def ensure_download( - url: str, dest: Path, expected_sha256: str, token: str | None -) -> None: - if dest.exists(): - current = sha256sum(dest) - if current == expected_sha256: - return - dest.unlink() - download(url, dest, token) - current = sha256sum(dest) - if current != expected_sha256: - dest.unlink(missing_ok=True) - raise RuntimeError( - f"SHA256 mismatch for {dest.name}: expected {expected_sha256}, got {current}" - ) - - -def extract_archive(archive: Path, dest_dir: Path) -> None: - log(f"Extracting {archive.name} to {dest_dir}") - dest_dir.mkdir(parents=True, exist_ok=True) - name = archive.name.lower() - if name.endswith(".tar.xz") or name.endswith(".tar.gz") or name.endswith(".tar"): - with tarfile.open(archive, "r:*") as tar: - tar.extractall(path=dest_dir) - log("Extraction complete") - return - raise RuntimeError(f"Unsupported archive format: {archive}") - - -def flatten_install_dir(dest_dir: Path) -> None: - # Some archives add an extra root directory (llvm-install, build-install, etc.). - for name in ("llvm-install", "build-install"): - nested = dest_dir / name - if not nested.is_dir(): - continue - log(f"Flattening nested install directory: {nested}") - for entry in nested.iterdir(): - target = dest_dir / entry.name - if target.exists(): - raise RuntimeError( - f"Cannot flatten {nested}: target already exists: {target}" - ) - shutil.move(str(entry), str(target)) - nested.rmdir() - break - - -def parse_version_tuple(text: str) -> tuple[int, ...]: - digits = [] - current = "" - for ch in text: - if ch.isdigit(): - current += ch - else: - if current: - digits.append(int(current)) - current = "" - if ch in {".", "-"}: - continue - if current: - digits.append(int(current)) - return tuple(digits) - - -def system_llvm_ok(required_version: str, build_type: str) -> Path | None: - if build_type.lower().startswith("debug"): - return None - llvm_config = shutil.which("llvm-config") - if not llvm_config: - return None - try: - version = subprocess.check_output([llvm_config, "--version"], text=True).strip() - prefix = subprocess.check_output([llvm_config, "--prefix"], text=True).strip() - except (subprocess.CalledProcessError, OSError): - return None - required = parse_version_tuple(required_version.split("+", 1)[0]) - found = parse_version_tuple(version) - if not found or found < required: - return None - return Path(prefix) - - -def github_api(url: str, token: str | None) -> dict: - headers = { - "Accept": "application/vnd.github+json", - "User-Agent": "clice-setup-llvm", - } - if token: - headers["Authorization"] = f"Bearer {token}" - request = Request(url, headers=headers) - with urlopen(request) as response: - return json.load(response) - - -def lookup_llvm_commit(version: str, token: str | None) -> str | None: - tag_version = version.split("+", 1)[0] - tag = f"llvmorg-{tag_version}" - ref_url = f"https://api.github.com/repos/llvm/llvm-project/git/ref/tags/{tag}" - try: - ref = github_api(ref_url, token) - except Exception: - return None - obj = ref.get("object") or {} - obj_type = obj.get("type") - obj_sha = obj.get("sha") - if obj_type == "commit": - return obj_sha - if obj_type == "tag" and obj_sha: - tag_url = f"https://api.github.com/repos/llvm/llvm-project/git/tags/{obj_sha}" - try: - tag_info = github_api(tag_url, token) - except Exception: - return None - return tag_info.get("object", {}).get("sha") or tag_info.get("sha") - return None - - -def ensure_private_headers( - install_path: Path, work_dir: Path, version: str, token: str | None, offline: bool -) -> None: - missing = [] - for rel in PRIVATE_CLANG_FILES: - if (install_path / "include" / "clang" / rel).exists(): - continue - if (work_dir / "include" / "clang" / rel).exists(): - continue - missing.append(rel) - if not missing or offline: - return - commit = lookup_llvm_commit(version, token) - if not commit: - return - for rel in missing: - dest = work_dir / "include" / "clang" / rel - dest.parent.mkdir(parents=True, exist_ok=True) - url = f"https://raw.githubusercontent.com/llvm/llvm-project/{commit}/clang/lib/{rel}" - log(f"Fetching private header: {url}") - download(url, dest, token) - - -def main() -> None: - parser = argparse.ArgumentParser(description="Setup LLVM dependencies for CMake") - parser.add_argument("--version", required=True) - parser.add_argument("--build-type", required=True) - parser.add_argument("--binary-dir", required=True) - parser.add_argument("--manifest", required=True) - parser.add_argument("--install-path") - parser.add_argument("--enable-lto", action="store_true") - parser.add_argument("--offline", action="store_true") - parser.add_argument( - "--target-platform", - help="Override platform for cross-compilation (e.g. macosx, linux, windows)", - ) - parser.add_argument( - "--target-arch", - help="Override architecture for cross-compilation (e.g. x64, arm64)", - ) - parser.add_argument("--output", required=True) - args = parser.parse_args() - - log( - "Args: " - f"version={args.version}, build_type={args.build_type}, " - f"binary_dir={args.binary_dir}, install_path={args.install_path or '(auto)'}, " - f"enable_lto={args.enable_lto}, offline={args.offline}" - ) - token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") - build_type = args.build_type - platform_name = args.target_platform if args.target_platform else detect_platform() - arch_name = args.target_arch if args.target_arch else detect_arch() - log( - f"Platform: {platform_name}, arch: {arch_name}, normalized build type: {build_type}" - ) - manifest = read_manifest(Path(args.manifest)) - - binary_dir = Path(args.binary_dir).resolve() - install_root = binary_dir / ".llvm" - - install_path: Path | None = None - needs_install = False - if args.install_path: - candidate = Path(args.install_path) - if candidate.exists(): - log(f"Using provided LLVM install at {candidate}") - else: - log( - f"Provided LLVM install path does not exist; will install to {candidate}" - ) - needs_install = True - install_path = candidate - else: - detected = system_llvm_ok(args.version, build_type) - if detected: - log(f"Found suitable system LLVM at {detected}") - install_path = detected - - artifact = None - if install_path is None: - needs_install = True - artifact = pick_artifact( - manifest, - args.version, - build_type, - args.enable_lto, - platform_name, - arch_name, - ) - log(f"Selected artifact: {artifact.get('filename')} for download") - filename = artifact["filename"] - url_version = args.version.replace("+", "%2B") - url = f"https://github.com/clice-io/clice-llvm/releases/download/{url_version}/{filename}" - download_path = binary_dir / filename - ensure_download(url, download_path, artifact["sha256"], token) - extract_archive(download_path, install_root) - flatten_install_dir(install_root) - install_path = install_root - elif needs_install: - artifact = pick_artifact( - manifest, - args.version, - build_type, - args.enable_lto, - platform_name, - arch_name, - ) - log(f"Selected artifact: {artifact.get('filename')} for download") - filename = artifact["filename"] - url_version = args.version.replace("+", "%2B") - url = f"https://github.com/clice-io/clice-llvm/releases/download/{url_version}/{filename}" - download_path = binary_dir / filename - ensure_download(url, download_path, artifact["sha256"], token) - target_dir = install_path.resolve() - extract_archive(download_path, target_dir) - flatten_install_dir(target_dir) - install_path = target_dir - else: - install_path = install_path.resolve() - log(f"Using existing LLVM install at {install_path}") - - cmake_dir = install_path / "lib" / "cmake" / "llvm" - ensure_private_headers(install_path, binary_dir, args.version, token, args.offline) - - output = Path(args.output) - output.parent.mkdir(parents=True, exist_ok=True) - with output.open("w", encoding="utf-8") as handle: - json.dump( - { - "install_path": str(install_path), - "cmake_dir": str(cmake_dir), - "artifact": artifact or {}, - }, - handle, - indent=2, - ) - handle.write("\n") - - -if __name__ == "__main__": - main() From c609dc80b83171aad64efbd84171bc3a2682b586 Mon Sep 17 00:00:00 2001 From: ykiko Date: Tue, 9 Jun 2026 10:16:20 +0800 Subject: [PATCH 24/28] fix: address review findings for LLVM 22 upgrade - Remove stale prune logic from build-llvm.yml (now in release-llvm.yml) - Fix release race condition: reuse existing release + --clobber uploads - Add actions: read permission to repackage job - Restore TransformNestedNameSpecifierLoc + TransformTemplateArguments in resolver dependent-TST path to match old DTST behavior - Add ARM64 detection for Windows/Linux in cmake artifact name selection - Unify artifact naming: release-llvm.py is single source of truth with build_artifact_name(), PLATFORM_INFO, and artifact-name subcommand - Deduplicate upload-llvm.py by importing from release-llvm.py - Convert release-llvm.py CLI to argparse subcommands - Remove stale LLVM-version-referencing comments --- .github/workflows/build-llvm.yml | 100 +++--------------------- .github/workflows/release-llvm.yml | 28 ++++--- cmake/llvm.cmake | 12 ++- scripts/release-llvm.py | 120 +++++++++++++++++++---------- scripts/upload-llvm.py | 54 +------------ src/semantic/resolver.cpp | 27 ++++--- src/semantic/semantic_visitor.h | 5 -- 7 files changed, 136 insertions(+), 210 deletions(-) diff --git a/.github/workflows/build-llvm.yml b/.github/workflows/build-llvm.yml index 402d74bb..6cc9359a 100644 --- a/.github/workflows/build-llvm.yml +++ b/.github/workflows/build-llvm.yml @@ -231,110 +231,30 @@ jobs: shell: bash run: pixi run test ${{ matrix.llvm_mode }} - # Prune is only supported for native builds (requires linking clice to test). - # Cross-compiled targets reuse the native prune manifest of the same OS. - - name: Prune LLVM static libraries (Debug/RelWithDebInfo no LTO) - if: (!inputs.skip_clice_build) && (!matrix.target_triple) && (matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF')) - shell: bash - run: | - MANIFEST="pruned-libs-${{ matrix.os }}.json" - echo "LLVM_PRUNED_MANIFEST=${MANIFEST}" >> "${GITHUB_ENV}" - python3 scripts/release-llvm.py \ - --action discover \ - --install-dir ".llvm/build-install/lib" \ - --build-dir "build/${{ matrix.llvm_mode }}" \ - --max-attempts 60 \ - --sleep-seconds 60 \ - --manifest "${MANIFEST}" - - - name: Upload pruned-libs manifest - if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF' - uses: actions/upload-artifact@v4 - with: - name: llvm-pruned-libs-${{ matrix.os }} - path: ${{ env.LLVM_PRUNED_MANIFEST }} - if-no-files-found: error - compression-level: 0 - - - name: Apply pruned-libs manifest (RelWithDebInfo + LTO, native only) - if: (!inputs.skip_clice_build) && (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON' - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - MANIFEST="pruned-libs-${{ matrix.os }}.json" - python3 scripts/release-llvm.py \ - --action apply \ - --manifest "${MANIFEST}" \ - --install-dir ".llvm/build-install/lib" \ - --build-dir "build/${{ matrix.llvm_mode }}" \ - --gh-run-id "${{ github.run_id }}" \ - --gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \ - --gh-download-dir "artifacts" \ - --max-attempts 60 \ - --sleep-seconds 60 - - # For cross-compiled LTO builds, apply the native prune manifest. - # The unused library set is arch-independent (same API surface). - - name: Apply pruned-libs manifest (cross-compile + LTO) - if: (!inputs.skip_clice_build) && matrix.target_triple && matrix.lto == 'ON' - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - MANIFEST="pruned-libs-${{ matrix.os }}.json" - python3 scripts/release-llvm.py \ - --action apply \ - --manifest "${MANIFEST}" \ - --install-dir ".llvm/build-install/lib" \ - --build-dir "build/${{ matrix.llvm_mode }}" \ - --gh-run-id "${{ github.run_id }}" \ - --gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \ - --gh-download-dir "artifacts" \ - --max-attempts 60 \ - --sleep-seconds 60 - - name: Package LLVM install directory shell: bash run: | - MODE_TAG="releasedbg" - if [[ "${{ matrix.llvm_mode }}" == "Debug" ]]; then - MODE_TAG="debug" - fi - - # Determine arch/platform/toolchain from target triple or runner OS + # Determine platform/arch from target triple or runner OS if [[ -n "${{ matrix.target_triple }}" ]]; then case "${{ matrix.target_triple }}" in - x86_64-apple-darwin) - ARCH="x64"; PLATFORM="macos"; TOOLCHAIN="clang" ;; - aarch64-linux-gnu) - ARCH="aarch64"; PLATFORM="linux"; TOOLCHAIN="gnu" ;; - aarch64-pc-windows-msvc) - ARCH="aarch64"; PLATFORM="windows"; TOOLCHAIN="msvc" ;; + x86_64-apple-darwin) PLATFORM="macos"; ARCH="x64" ;; + aarch64-linux-gnu) PLATFORM="linux"; ARCH="aarch64" ;; + aarch64-pc-windows-msvc) PLATFORM="windows"; ARCH="aarch64" ;; esac else - ARCH="x64" - PLATFORM="linux" - TOOLCHAIN="gnu" + PLATFORM="linux"; ARCH="x64" if [[ "${{ matrix.os }}" == windows-* ]]; then PLATFORM="windows" - TOOLCHAIN="msvc" elif [[ "${{ matrix.os }}" == macos-* ]]; then - ARCH="arm64" - PLATFORM="macos" - TOOLCHAIN="clang" + PLATFORM="macos"; ARCH="arm64" fi fi - SUFFIX="" - if [[ "${{ matrix.lto }}" == "ON" ]]; then - SUFFIX="-lto" - fi - if [[ "${{ matrix.llvm_mode }}" == "Debug" && "${{ matrix.os }}" != windows-* ]]; then - SUFFIX="${SUFFIX}-asan" - fi + NAME_ARGS="--platform $PLATFORM --arch $ARCH --mode ${{ matrix.llvm_mode }}" + if [[ "${{ matrix.lto }}" == "ON" ]]; then NAME_ARGS+=" --lto"; fi + if [[ "${{ matrix.llvm_mode }}" == "Debug" && "${{ matrix.os }}" != windows-* ]]; then NAME_ARGS+=" --asan"; fi - ARCHIVE="${ARCH}-${PLATFORM}-${TOOLCHAIN}-${MODE_TAG}${SUFFIX}.tar.xz" + ARCHIVE=$(python3 scripts/release-llvm.py artifact-name $NAME_ARGS) set -eo pipefail tar -C .llvm -cf - build-install | xz -T0 -9 -c > "${ARCHIVE}" diff --git a/.github/workflows/release-llvm.yml b/.github/workflows/release-llvm.yml index 166ab19b..63aa4a1e 100644 --- a/.github/workflows/release-llvm.yml +++ b/.github/workflows/release-llvm.yml @@ -75,8 +75,7 @@ jobs: - name: Discover unused libraries shell: bash run: | - python3 scripts/release-llvm.py \ - --action discover \ + python3 scripts/release-llvm.py discover \ --install-dir .llvm/build-install/lib \ --build-dir build/RelWithDebInfo \ --manifest pruned-libs.json \ @@ -106,23 +105,27 @@ jobs: needs: discover runs-on: ubuntu-24.04 steps: - - name: Create release + - name: Create or reuse release env: GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} run: | TAG="${{ env.LLVM_VERSION }}" REPO="clice-io/clice-llvm" - gh release delete "$TAG" --repo "$REPO" -y 2>/dev/null || true - gh release create "$TAG" \ - --repo "$REPO" \ - --title "$TAG" \ - --notes "LLVM ${TAG} — build run https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + if gh release view "$TAG" --repo "$REPO" &>/dev/null; then + echo "Release $TAG already exists, will overwrite assets" + else + gh release create "$TAG" \ + --repo "$REPO" \ + --title "$TAG" \ + --notes "LLVM ${TAG} — build run https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + fi repackage: needs: [discover, create-release] runs-on: ubuntu-24.04 permissions: contents: read + actions: read strategy: fail-fast: false matrix: @@ -159,8 +162,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - python3 scripts/release-llvm.py \ - --action repackage \ + python3 scripts/release-llvm.py repackage \ --source-run-id "${{ env.SOURCE_RUN_ID }}" \ --manifests-dir manifests \ --output-dir artifacts \ @@ -174,7 +176,8 @@ jobs: run: | gh release upload "${{ env.LLVM_VERSION }}" \ "artifacts/${{ matrix.artifact }}" \ - --repo clice-io/clice-llvm + --repo clice-io/clice-llvm \ + --clobber - name: Upload metadata uses: actions/upload-artifact@v4 @@ -217,7 +220,8 @@ jobs: run: | gh release upload "${{ env.LLVM_VERSION }}" \ llvm-manifest.json \ - --repo clice-io/clice-llvm + --repo clice-io/clice-llvm \ + --clobber - name: Save manifest artifact uses: actions/upload-artifact@v4 diff --git a/cmake/llvm.cmake b/cmake/llvm.cmake index 912b66b8..9daa738d 100644 --- a/cmake/llvm.cmake +++ b/cmake/llvm.cmake @@ -24,9 +24,13 @@ function(_detect_llvm_artifact_name OUT_FILENAME) endif() else() if(WIN32) - set(_ARCH "x64") set(_PLATFORM "windows") set(_TOOLCHAIN "msvc") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64|ARM64") + set(_ARCH "aarch64") + else() + set(_ARCH "x64") + endif() elseif(APPLE) set(_PLATFORM "macos") set(_TOOLCHAIN "clang") @@ -36,9 +40,13 @@ function(_detect_llvm_artifact_name OUT_FILENAME) set(_ARCH "x64") endif() else() - set(_ARCH "x64") set(_PLATFORM "linux") set(_TOOLCHAIN "gnu") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64|ARM64") + set(_ARCH "aarch64") + else() + set(_ARCH "x64") + endif() endif() endif() diff --git a/scripts/release-llvm.py b/scripts/release-llvm.py index 78e4a636..3be643ab 100644 --- a/scripts/release-llvm.py +++ b/scripts/release-llvm.py @@ -23,21 +23,47 @@ from pathlib import Path from typing import Iterable, List, Optional +PLATFORM_INFO = { + "linux": {"toolchain": "gnu", "arm_arch": "aarch64"}, + "macos": {"toolchain": "clang", "arm_arch": "arm64"}, + "windows": {"toolchain": "msvc", "arm_arch": "aarch64"}, +} + + +def build_artifact_name( + platform: str, arch: str, mode: str, *, lto: bool = False, asan: bool = False, +) -> str: + info = PLATFORM_INFO.get(platform) + if not info: + raise ValueError(f"Unknown platform: {platform}") + toolchain = info["toolchain"] + mode_tag = "debug" if mode == "Debug" else "releasedbg" + suffix = "" + if lto: + suffix += "-lto" + if asan: + suffix += "-asan" + return f"{arch}-{platform}-{toolchain}-{mode_tag}{suffix}.tar.xz" + + ARTIFACTS = [ - "aarch64-linux-gnu-releasedbg-lto.tar.xz", - "aarch64-linux-gnu-releasedbg.tar.xz", - "aarch64-windows-msvc-releasedbg-lto.tar.xz", - "aarch64-windows-msvc-releasedbg.tar.xz", - "arm64-macos-clang-debug-asan.tar.xz", - "arm64-macos-clang-releasedbg-lto.tar.xz", - "arm64-macos-clang-releasedbg.tar.xz", - "x64-linux-gnu-debug-asan.tar.xz", - "x64-linux-gnu-releasedbg-lto.tar.xz", - "x64-linux-gnu-releasedbg.tar.xz", - "x64-macos-clang-releasedbg-lto.tar.xz", - "x64-macos-clang-releasedbg.tar.xz", - "x64-windows-msvc-releasedbg-lto.tar.xz", - "x64-windows-msvc-releasedbg.tar.xz", + build_artifact_name(p, a, m, lto=l, asan=s) + for p, a, m, l, s in [ + ("linux", "aarch64", "RelWithDebInfo", True, False), + ("linux", "aarch64", "RelWithDebInfo", False, False), + ("windows", "aarch64", "RelWithDebInfo", True, False), + ("windows", "aarch64", "RelWithDebInfo", False, False), + ("macos", "arm64", "Debug", False, True), + ("macos", "arm64", "RelWithDebInfo", True, False), + ("macos", "arm64", "RelWithDebInfo", False, False), + ("linux", "x64", "Debug", False, True), + ("linux", "x64", "RelWithDebInfo", True, False), + ("linux", "x64", "RelWithDebInfo", False, False), + ("macos", "x64", "RelWithDebInfo", True, False), + ("macos", "x64", "RelWithDebInfo", False, False), + ("windows", "x64", "RelWithDebInfo", True, False), + ("windows", "x64", "RelWithDebInfo", False, False), + ] ] MANIFEST_DIRS = { @@ -195,7 +221,7 @@ def apply_manifest(manifest: Path, install_dir: Path) -> None: # ── metadata ───────────────────────────────────────────────────────── -def _sha256sum(path: Path) -> str: +def sha256sum(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): @@ -203,7 +229,7 @@ def _sha256sum(path: Path) -> str: return digest.hexdigest() -def _build_metadata_entry(path: Path, version: str) -> dict: +def build_metadata_entry(path: Path, version: str) -> dict: name = path.name.lower() if "windows" in name: platform = "windows" @@ -224,7 +250,7 @@ def _build_metadata_entry(path: Path, version: str) -> dict: return { "version": version, "filename": path.name, - "sha256": _sha256sum(path), + "sha256": sha256sum(path), "lto": "-lto" in name, "asan": "-asan" in name, "platform": platform, @@ -304,7 +330,7 @@ def _process_artifact( print(f"[{artifact}] Done ({file_size / 1048576:.1f} MB)", flush=True) if version: - meta = _build_metadata_entry(output_path, version) + meta = build_metadata_entry(output_path, version) meta_path = output_dir / f"{artifact}.meta.json" meta_path.write_text(json.dumps(meta, indent=2)) finally: @@ -418,28 +444,38 @@ def _ensure_manifest( def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="LLVM release pipeline utilities.") - parser.add_argument( - "--action", - choices=["discover", "apply", "repackage"], - required=True, - ) - parser.add_argument("--install-dir", type=Path) - parser.add_argument("--build-dir", type=Path) - parser.add_argument("--manifest", type=Path) - parser.add_argument("--skip-pattern", action="append", default=[]) - # apply: download manifest from another job if not present locally - parser.add_argument("--gh-run-id", type=str) - parser.add_argument("--gh-artifact", type=str, default="llvm-pruned-libs") - parser.add_argument("--gh-download-dir", type=Path, default=Path("artifacts")) - parser.add_argument("--max-attempts", type=int, default=30) - parser.add_argument("--sleep-seconds", type=int, default=60) - # repackage - parser.add_argument("--source-run-id", type=str) - parser.add_argument("--manifests-dir", type=Path) - parser.add_argument("--output-dir", type=Path) - parser.add_argument("--max-parallel", type=int, default=3) - parser.add_argument("--artifacts", nargs="*") - parser.add_argument("--version", type=str) + sub = parser.add_subparsers(dest="action", required=True) + + dp = sub.add_parser("discover", help="Probe which static libs can be removed") + dp.add_argument("--install-dir", type=Path, required=True) + dp.add_argument("--build-dir", type=Path, required=True) + dp.add_argument("--manifest", type=Path, required=True) + dp.add_argument("--skip-pattern", action="append", default=[]) + + ap = sub.add_parser("apply", help="Replace listed libs with empty archives") + ap.add_argument("--manifest", type=Path, required=True) + ap.add_argument("--install-dir", type=Path, required=True) + ap.add_argument("--gh-run-id", type=str) + ap.add_argument("--gh-artifact", type=str, default="llvm-pruned-libs") + ap.add_argument("--gh-download-dir", type=Path, default=Path("artifacts")) + ap.add_argument("--max-attempts", type=int, default=30) + ap.add_argument("--sleep-seconds", type=int, default=60) + + rp = sub.add_parser("repackage", help="Download, prune, and repackage artifacts") + rp.add_argument("--source-run-id", type=str, required=True) + rp.add_argument("--manifests-dir", type=Path, required=True) + rp.add_argument("--output-dir", type=Path, required=True) + rp.add_argument("--max-parallel", type=int, default=3) + rp.add_argument("--artifacts", nargs="*") + rp.add_argument("--version", type=str) + + np = sub.add_parser("artifact-name", help="Print the artifact filename for given params") + np.add_argument("--platform", required=True, choices=["linux", "macos", "windows"]) + np.add_argument("--arch", required=True, choices=["x64", "arm64", "aarch64"]) + np.add_argument("--mode", required=True, choices=["Debug", "RelWithDebInfo"]) + np.add_argument("--lto", action="store_true") + np.add_argument("--asan", action="store_true") + return parser.parse_args() @@ -468,6 +504,10 @@ def main() -> None: artifacts=args.artifacts, version=args.version, ) + elif args.action == "artifact-name": + print(build_artifact_name( + args.platform, args.arch, args.mode, lto=args.lto, asan=args.asan, + )) if __name__ == "__main__": diff --git a/scripts/upload-llvm.py b/scripts/upload-llvm.py index 8a93a079..1753e73a 100644 --- a/scripts/upload-llvm.py +++ b/scripts/upload-llvm.py @@ -1,60 +1,14 @@ #!/usr/bin/env python3 -import hashlib import json import os import subprocess import sys from pathlib import Path - -def sha256sum(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def parse_platform(name: str) -> str: - lowered = name.lower() - if "windows" in lowered: - return "windows" - if "linux" in lowered: - return "linux" - if "macos" in lowered: - return "macosx" - raise ValueError(f"Unable to determine platform from filename: {name}") - - -def parse_arch(name: str) -> str: - lowered = name.lower() - if lowered.startswith("aarch64-") or lowered.startswith("arm64-"): - return "arm64" - if lowered.startswith("x64-") or lowered.startswith("x86_64-"): - return "x64" - raise ValueError(f"Unable to determine arch from filename: {name}") - - -def parse_build_type(name: str) -> str: - lowered = name.lower() - if "debug" in lowered: - return "Debug" - return "RelWithDebInfo" - - -def build_metadata_entry(path: Path, version: str) -> dict: - filename = path.name - return { - "version": version, - "filename": filename, - "sha256": sha256sum(path), - "lto": "-lto" in filename.lower(), - "asan": "-asan" in filename.lower(), - "platform": parse_platform(filename), - "arch": parse_arch(filename), - "build_type": parse_build_type(filename), - } +sys.path.insert(0, str(Path(__file__).parent)) +from importlib import import_module +release_llvm = import_module("release-llvm") def main() -> None: @@ -84,7 +38,7 @@ def main() -> None: version_without_prefix = tag.lstrip("vV") manifest = {"version": version_without_prefix, "artifacts": {}} for path in artifact_files: - entry = build_metadata_entry(path, version_without_prefix) + entry = release_llvm.build_metadata_entry(path, version_without_prefix) filename = entry.pop("filename") entry.pop("version", None) manifest["artifacts"][filename] = entry diff --git a/src/semantic/resolver.cpp b/src/semantic/resolver.cpp index 829ded07..3f64bab6 100644 --- a/src/semantic/resolver.cpp +++ b/src/semantic/resolver.cpp @@ -210,8 +210,6 @@ class SubstituteOnly : public clang::TreeTransform { if(underlying->isDependentType()) { auto type = TransformType(underlying); if(!type.isNull()) { - // ElaboratedType was removed in LLVM 22; elaboration is now - // part of each type's own representation. TLB.pushTrivial(context, type, {}); return type; } @@ -222,8 +220,6 @@ class SubstituteOnly : public clang::TreeTransform { clang::QualType TransformInjectedClassNameType(clang::TypeLocBuilder& TLB, clang::InjectedClassNameTypeLoc TL) { - // In LLVM 22, InjectedClassNameType no longer has getInjectedSpecializationType. - // Just return the base transform. return Base::TransformInjectedClassNameType(TLB, TL); } @@ -938,8 +934,6 @@ class PseudoInstantiator : public clang::TreeTransform { using Base::TransformTemplateSpecializationType; - /// Handle dependent template specializations (formerly DependentTemplateSpecializationType, - /// now merged into TemplateSpecializationType with DependentTemplateName in LLVM 22). clang::QualType TransformTemplateSpecializationType(clang::TypeLocBuilder& TLB, clang::TemplateSpecializationTypeLoc TL) { auto* TST = TL.getTypePtr(); @@ -968,11 +962,24 @@ class PseudoInstantiator : public clang::TreeTransform { return iter->second; } - auto NNS = DTN->getQualifier(); + auto NNSLoc = TransformNestedNameSpecifierLoc(TL.getQualifierLoc()); + clang::NestedNameSpecifier NNS = NNSLoc + ? NNSLoc.getNestedNameSpecifier() + : DTN->getQualifier(); + + clang::TemplateArgumentListInfo info; + using arg_iterator = clang::TemplateArgumentLocContainerIterator< + clang::TemplateSpecializationTypeLoc>; + if(TransformTemplateArguments(arg_iterator(TL, 0), arg_iterator(TL, TL.getNumArgs()), info)) { + auto original = clang::QualType(TST, 0); + TLB.pushTrivial(context, original, {}); + --indent; + return original; + } llvm::SmallVector arguments; - for(auto& arg: TST->template_arguments()) { - arguments.push_back(arg); + for(auto& arg: info.arguments()) { + arguments.push_back(arg.getArgument()); } auto* name = DTN->getName().getIdentifier(); @@ -1051,8 +1058,6 @@ class PseudoInstantiator : public clang::TreeTransform { if(underlying->isDependentType()) { auto type = substitute(underlying); if(!type.isNull()) { - // ElaboratedType was removed in LLVM 22; elaboration is now - // part of each type's own representation. TLB.pushTrivial(context, type, {}); return type; } diff --git a/src/semantic/semantic_visitor.h b/src/semantic/semantic_visitor.h index bb4a9123..cf0e65fd 100644 --- a/src/semantic/semantic_visitor.h +++ b/src/semantic/semantic_visitor.h @@ -559,11 +559,6 @@ class SemanticVisitor : public FilteredASTVisitor> { return true; } - /// std::allocator::rebind - /// DependentTemplateSpecializationType was merged into TemplateSpecializationType - /// in LLVM 22, so dependent template specializations are now handled via - /// VisitTemplateSpecializationTypeLoc above. - /// ============================================================================ /// Specifier /// ============================================================================ From 4adaa1ea888e46b94c040ee85c4773ff1bfa6a57 Mon Sep 17 00:00:00 2001 From: ykiko Date: Tue, 9 Jun 2026 10:32:10 +0800 Subject: [PATCH 25/28] fix: address second round of review findings - Remove push trigger from release-llvm.yml; only workflow_dispatch with required inputs can publish to clice-llvm, preventing accidental overwrites - Add version stamp (.llvm-version) to cmake download cache so LLVM version upgrades trigger re-download instead of silently reusing stale installs; also verify SHA256 of cached download archives - Fail hard when prune manifest is missing during repackage instead of warning and publishing unpruned artifacts --- .github/workflows/release-llvm.yml | 9 ++------- cmake/llvm.cmake | 25 ++++++++++++++++++++++++- scripts/release-llvm.py | 11 ++++++----- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release-llvm.yml b/.github/workflows/release-llvm.yml index 63aa4a1e..f2e00f1d 100644 --- a/.github/workflows/release-llvm.yml +++ b/.github/workflows/release-llvm.yml @@ -1,11 +1,6 @@ name: release llvm on: - push: - branches: [chore/upgrade-llvm-22] - paths: - - ".github/workflows/release-llvm.yml" - - "scripts/release-llvm.py" workflow_dispatch: inputs: source_run_id: @@ -18,8 +13,8 @@ on: type: string env: - SOURCE_RUN_ID: ${{ inputs.source_run_id || '27052100922' }} - LLVM_VERSION: ${{ inputs.llvm_version || '22.1.4' }} + SOURCE_RUN_ID: ${{ inputs.source_run_id }} + LLVM_VERSION: ${{ inputs.llvm_version }} jobs: discover: diff --git a/cmake/llvm.cmake b/cmake/llvm.cmake index 9daa738d..89d19dbf 100644 --- a/cmake/llvm.cmake +++ b/cmake/llvm.cmake @@ -69,6 +69,14 @@ endfunction() function(_download_and_extract _URL _SHA256 _DEST _LABEL) set(_DOWNLOAD_PATH "${CMAKE_CURRENT_BINARY_DIR}/${_LABEL}") + if(EXISTS "${_DOWNLOAD_PATH}") + file(SHA256 "${_DOWNLOAD_PATH}" _EXISTING_HASH) + if(NOT _EXISTING_HASH STREQUAL "${_SHA256}") + message(STATUS "Hash mismatch for cached ${_LABEL}, re-downloading") + file(REMOVE "${_DOWNLOAD_PATH}") + endif() + endif() + if(NOT EXISTS "${_DOWNLOAD_PATH}") message(STATUS "Downloading ${_LABEL}") file(DOWNLOAD "${_URL}" "${_DOWNLOAD_PATH}" @@ -106,8 +114,21 @@ function(_download_llvm LLVM_VERSION) set(_BASE_URL "https://github.com/clice-io/clice-llvm/releases/download/${LLVM_VERSION}") set(_INSTALL_ROOT "${CMAKE_CURRENT_BINARY_DIR}/.llvm") + set(_VERSION_STAMP "${_INSTALL_ROOT}/.llvm-version") + + set(_NEED_INSTALL TRUE) + if(EXISTS "${_INSTALL_ROOT}/lib/cmake/llvm/LLVMConfig.cmake" AND EXISTS "${_VERSION_STAMP}") + file(READ "${_VERSION_STAMP}" _CACHED_VERSION) + string(STRIP "${_CACHED_VERSION}" _CACHED_VERSION) + if(_CACHED_VERSION STREQUAL "${LLVM_VERSION}") + set(_NEED_INSTALL FALSE) + else() + message(STATUS "LLVM version changed (${_CACHED_VERSION} -> ${LLVM_VERSION}), reinstalling") + file(REMOVE_RECURSE "${_INSTALL_ROOT}") + endif() + endif() - if(NOT EXISTS "${_INSTALL_ROOT}/lib/cmake/llvm/LLVMConfig.cmake") + if(_NEED_INSTALL) file(MAKE_DIRECTORY "${_INSTALL_ROOT}") _download_and_extract( @@ -121,6 +142,8 @@ function(_download_llvm LLVM_VERSION) endforeach() file(REMOVE_RECURSE "${_INSTALL_ROOT}/build-install") endif() + + file(WRITE "${_VERSION_STAMP}" "${LLVM_VERSION}\n") endif() set(LLVM_INSTALL_PATH "${_INSTALL_ROOT}" PARENT_SCOPE) diff --git a/scripts/release-llvm.py b/scripts/release-llvm.py index 3be643ab..3d54f408 100644 --- a/scripts/release-llvm.py +++ b/scripts/release-llvm.py @@ -318,11 +318,12 @@ def _process_artifact( print(f"[{artifact}] Debug/ASAN — skipping prune", flush=True) else: manifest = _manifest_for(artifact, manifests_dir) - if manifest and manifest.is_file(): - print(f"[{artifact}] Pruning...", flush=True) - apply_manifest(manifest, content_dir / "lib") - else: - print(f"[{artifact}] WARNING: no manifest, skipping prune", flush=True) + if not manifest: + raise RuntimeError(f"No manifest mapping for artifact: {artifact}") + if not manifest.is_file(): + raise FileNotFoundError(f"Prune manifest missing: {manifest}") + print(f"[{artifact}] Pruning...", flush=True) + apply_manifest(manifest, content_dir / "lib") output_path = output_dir / artifact file_size = _compress_tar_xz(content_dir, output_path, "-9e", artifact) From ea07d04c10febad72700c0638bb09e579de583ba Mon Sep 17 00:00:00 2001 From: ykiko Date: Tue, 9 Jun 2026 10:43:03 +0800 Subject: [PATCH 26/28] chore: strip build-llvm.yml to LLVM-only build Remove clice build/test steps, test-cross job, upload job, and update-clice job. These responsibilities now live in release-llvm.yml. --- .github/workflows/build-llvm.yml | 203 +------------------------------ 1 file changed, 2 insertions(+), 201 deletions(-) diff --git a/.github/workflows/build-llvm.yml b/.github/workflows/build-llvm.yml index 6cc9359a..471717b5 100644 --- a/.github/workflows/build-llvm.yml +++ b/.github/workflows/build-llvm.yml @@ -4,28 +4,9 @@ on: workflow_dispatch: inputs: llvm_version: - description: "LLVM version to build (e.g., 21.1.8)" + description: "LLVM version to build (e.g., 22.1.4)" required: true type: string - skip_upload: - description: "Skip upload and PR creation (build-only mode)" - required: false - type: boolean - default: false - skip_pr: - description: "Skip PR creation (upload only, no PR)" - required: false - type: boolean - default: false - skip_clice_build: - description: "Skip building and testing clice (LLVM-only build)" - required: false - type: boolean - default: false - pull_request: - # if you want to run this workflow, change the branch name to main, - # if you want to turn off it, change it to non existent branch. - branches: [main-turn-off] jobs: build: @@ -164,7 +145,7 @@ jobs: - name: Clone llvm-project shell: bash run: | - VERSION="${{ inputs.llvm_version || '21.1.8' }}" + VERSION="${{ inputs.llvm_version }}" echo "Cloning LLVM ${VERSION}..." git clone --branch "llvmorg-${VERSION}" --depth 1 https://github.com/llvm/llvm-project.git .llvm @@ -183,54 +164,6 @@ jobs: --build-dir=build \ ${EXTRA_ARGS} - - name: Build clice using installed LLVM - if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} - shell: bash - run: | - pixi run cmake-config ${{ matrix.llvm_mode }} ON -- \ - "-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \ - "-DLLVM_INSTALL_PATH=.llvm/build-install" - pixi run cmake-build ${{ matrix.llvm_mode }} - - - name: Build clice using installed LLVM (cross-compile) - if: ${{ matrix.target_triple && !inputs.skip_clice_build }} - shell: bash - run: | - ENV="${{ matrix.pixi_env || 'package' }}" - pixi run -e "$ENV" cmake-config ${{ matrix.llvm_mode }} ON -- \ - "-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \ - "-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}" \ - "-DLLVM_INSTALL_PATH=.llvm/build-install" - pixi run -e "$ENV" cmake-build ${{ matrix.llvm_mode }} - - - name: Verify cross-compiled binary architecture - if: ${{ matrix.target_triple && runner.os != 'Windows' && !inputs.skip_clice_build }} - shell: bash - run: | - BINARY="build/${{ matrix.llvm_mode }}/bin/clice" - echo "Binary info:" - file "$BINARY" - case "${{ matrix.target_triple }}" in - aarch64-linux-gnu) file "$BINARY" | grep -q "aarch64" ;; - x86_64-apple-darwin) file "$BINARY" | grep -q "x86_64" ;; - esac - - - name: Upload cross-compiled clice for functional test - if: ${{ matrix.target_triple && matrix.lto == 'OFF' && !inputs.skip_clice_build }} - uses: actions/upload-artifact@v4 - with: - name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} - path: | - build/${{ matrix.llvm_mode }}/bin/ - build/${{ matrix.llvm_mode }}/lib/ - if-no-files-found: error - retention-days: 1 - - - name: Run tests - if: ${{ !matrix.target_triple && !inputs.skip_clice_build }} - shell: bash - run: pixi run test ${{ matrix.llvm_mode }} - - name: Package LLVM install directory shell: bash run: | @@ -266,135 +199,3 @@ jobs: name: ${{ env.LLVM_INSTALL_ARCHIVE }} path: ${{ env.LLVM_INSTALL_ARCHIVE }} if-no-files-found: error - - test-cross: - needs: build - if: ${{ !inputs.skip_clice_build }} - strategy: - fail-fast: false - matrix: - include: - - os: macos-15-intel - llvm_mode: RelWithDebInfo - target_triple: x86_64-apple-darwin - - os: ubuntu-24.04-arm - llvm_mode: RelWithDebInfo - target_triple: aarch64-linux-gnu - - os: windows-11-arm - llvm_mode: RelWithDebInfo - target_triple: aarch64-pc-windows-msvc - runs-on: ${{ matrix.os }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: ./.github/actions/setup-pixi - with: - environments: test-run - - - name: Download cross-compiled clice - uses: actions/download-artifact@v4 - with: - name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }} - path: build/${{ matrix.llvm_mode }}/ - - - name: Make binaries executable - if: runner.os != 'Windows' - run: chmod +x build/${{ matrix.llvm_mode }}/bin/* - - - name: Run tests - run: pixi run -e test-run test ${{ matrix.llvm_mode }} - - upload: - needs: build - if: ${{ !cancelled() && inputs.llvm_version && !inputs.skip_upload }} - runs-on: ubuntu-24.04 - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - - name: Download all build artifacts - env: - GH_TOKEN: ${{ github.token }} - run: scripts/download-llvm.sh "${{ github.run_id }}" - - - name: Upload to clice-llvm - env: - GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} - TARGET_REPO: clice-io/clice-llvm - run: python3 scripts/upload-llvm.py "${{ inputs.llvm_version }}" "${TARGET_REPO}" "${{ github.run_id }}" - - - name: Save manifest for update-clice job - uses: actions/upload-artifact@v4 - with: - name: llvm-manifest-final - path: artifacts/llvm-manifest.json - if-no-files-found: error - compression-level: 0 - - update-clice: - needs: upload - if: ${{ !inputs.skip_pr }} - runs-on: ubuntu-24.04 - permissions: - contents: write - pull-requests: write - steps: - - uses: actions/checkout@v4 - - - name: Download manifest - uses: actions/download-artifact@v4 - with: - name: llvm-manifest-final - path: . - - - name: Update manifest and version - run: | - python3 scripts/update-llvm-version.py \ - --version "${{ inputs.llvm_version }}" \ - --manifest-src llvm-manifest.json \ - --manifest-dest config/llvm-manifest.json \ - --package-cmake cmake/package.cmake - - - name: Create or update PR - env: - GH_TOKEN: ${{ github.token }} - run: | - VERSION="${{ inputs.llvm_version }}" - BRANCH="chore/update-llvm-${VERSION}" - RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - RELEASE_URL="https://github.com/clice-io/clice-llvm/releases/tag/${VERSION}" - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git checkout -b "${BRANCH}" - git add config/llvm-manifest.json cmake/package.cmake - git commit -m "chore: update LLVM to ${VERSION}" - git push --force-with-lease origin "${BRANCH}" - - # Check if PR already exists for this branch - EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number // empty') - - BODY="$(cat < Auto-generated by build-llvm workflow - EOF - )" - - if [[ -n "${EXISTING_PR}" ]]; then - echo "Updating existing PR #${EXISTING_PR}" - gh pr edit "${EXISTING_PR}" --body "${BODY}" - else - gh pr create \ - --title "chore: update LLVM to ${VERSION}" \ - --body "${BODY}" \ - --base main - fi From 4d82e3832c0a38f69aae2f1c0188e251cad0d223 Mon Sep 17 00:00:00 2001 From: ykiko Date: Tue, 9 Jun 2026 22:58:18 +0800 Subject: [PATCH 27/28] fix: add clangOptions to link deps and apply formatting - Add clangOptions to llvm-libs link list (new LLVM 22 library for driver option table, fixes undefined symbol in Debug shared builds) - Apply ruff/clang-format to Python scripts and resolver.cpp --- cmake/llvm.cmake | 2 +- scripts/release-llvm.py | 100 ++++++++++++++++++++++----------- scripts/update-llvm-version.py | 8 ++- scripts/upload-llvm.py | 1 + src/semantic/resolver.cpp | 13 +++-- 5 files changed, 82 insertions(+), 42 deletions(-) diff --git a/cmake/llvm.cmake b/cmake/llvm.cmake index 89d19dbf..a1235d3d 100644 --- a/cmake/llvm.cmake +++ b/cmake/llvm.cmake @@ -174,7 +174,7 @@ function(setup_llvm LLVM_VERSION) add_library(llvm-libs INTERFACE IMPORTED) target_link_libraries(llvm-libs INTERFACE ${LLVM_RESOLVED} - clangAST clangASTMatchers clangBasic clangDriver + clangAST clangASTMatchers clangBasic clangDriver clangOptions clangFormat clangFrontend clangLex clangSema clangSerialization clangTidy clangTidyUtils clangTidyAbseilModule clangTidyAlteraModule clangTidyAndroidModule diff --git a/scripts/release-llvm.py b/scripts/release-llvm.py index 3d54f408..caa75336 100644 --- a/scripts/release-llvm.py +++ b/scripts/release-llvm.py @@ -24,14 +24,19 @@ from typing import Iterable, List, Optional PLATFORM_INFO = { - "linux": {"toolchain": "gnu", "arm_arch": "aarch64"}, - "macos": {"toolchain": "clang", "arm_arch": "arm64"}, - "windows": {"toolchain": "msvc", "arm_arch": "aarch64"}, + "linux": {"toolchain": "gnu", "arm_arch": "aarch64"}, + "macos": {"toolchain": "clang", "arm_arch": "arm64"}, + "windows": {"toolchain": "msvc", "arm_arch": "aarch64"}, } def build_artifact_name( - platform: str, arch: str, mode: str, *, lto: bool = False, asan: bool = False, + platform: str, + arch: str, + mode: str, + *, + lto: bool = False, + asan: bool = False, ) -> str: info = PLATFORM_INFO.get(platform) if not info: @@ -49,20 +54,20 @@ def build_artifact_name( ARTIFACTS = [ build_artifact_name(p, a, m, lto=l, asan=s) for p, a, m, l, s in [ - ("linux", "aarch64", "RelWithDebInfo", True, False), - ("linux", "aarch64", "RelWithDebInfo", False, False), - ("windows", "aarch64", "RelWithDebInfo", True, False), + ("linux", "aarch64", "RelWithDebInfo", True, False), + ("linux", "aarch64", "RelWithDebInfo", False, False), + ("windows", "aarch64", "RelWithDebInfo", True, False), ("windows", "aarch64", "RelWithDebInfo", False, False), - ("macos", "arm64", "Debug", False, True), - ("macos", "arm64", "RelWithDebInfo", True, False), - ("macos", "arm64", "RelWithDebInfo", False, False), - ("linux", "x64", "Debug", False, True), - ("linux", "x64", "RelWithDebInfo", True, False), - ("linux", "x64", "RelWithDebInfo", False, False), - ("macos", "x64", "RelWithDebInfo", True, False), - ("macos", "x64", "RelWithDebInfo", False, False), - ("windows", "x64", "RelWithDebInfo", True, False), - ("windows", "x64", "RelWithDebInfo", False, False), + ("macos", "arm64", "Debug", False, True), + ("macos", "arm64", "RelWithDebInfo", True, False), + ("macos", "arm64", "RelWithDebInfo", False, False), + ("linux", "x64", "Debug", False, True), + ("linux", "x64", "RelWithDebInfo", True, False), + ("linux", "x64", "RelWithDebInfo", False, False), + ("macos", "x64", "RelWithDebInfo", True, False), + ("macos", "x64", "RelWithDebInfo", False, False), + ("windows", "x64", "RelWithDebInfo", True, False), + ("windows", "x64", "RelWithDebInfo", False, False), ] ] @@ -74,6 +79,7 @@ def build_artifact_name( ARCHIVE_MAGIC = b"!\n" + def _is_shared_lib(path: Path) -> bool: return ".so" in path.suffixes or ".dylib" in path.suffixes @@ -133,9 +139,7 @@ def _candidate_files( if path.suffix.lower() not in {".a", ".lib"}: print(f"Skipping non-static file: {path.name}") continue - if skip_patterns and any( - fnmatch.fnmatch(path.name, p) for p in skip_patterns - ): + if skip_patterns and any(fnmatch.fnmatch(path.name, p) for p in skip_patterns): print(f"Skipping (never-prune): {path.name}") continue yield path @@ -263,7 +267,10 @@ def build_metadata_entry(path: Path, version: str) -> dict: def _compress_tar_xz( - source_dir: Path, output_path: Path, xz_level: str, label: str, + source_dir: Path, + output_path: Path, + xz_level: str, + label: str, ) -> int: print(f"[{label}] Compressing (xz {xz_level})...", flush=True) with output_path.open("wb") as out: @@ -300,16 +307,21 @@ def _process_artifact( print(f"[{artifact}] Downloading...", flush=True) subprocess.run( - ["gh", "run", "download", source_run_id, - "-n", artifact, "-D", str(dl_dir)], + ["gh", "run", "download", source_run_id, "-n", artifact, "-D", str(dl_dir)], check=True, ) print(f"[{artifact}] Extracting...", flush=True) content_dir.mkdir() subprocess.run( - ["tar", "--strip-components=1", "-xf", - str(dl_dir / artifact), "-C", str(content_dir)], + [ + "tar", + "--strip-components=1", + "-xf", + str(dl_dir / artifact), + "-C", + str(content_dir), + ], check=True, ) shutil.rmtree(dl_dir) @@ -401,9 +413,18 @@ def _wait_and_download_manifest( f"(run={run_id}, artifact={artifact})" ) result = subprocess.run( - ["gh", "run", "download", str(run_id), - "--pattern", artifact, "--dir", str(download_dir)], - capture_output=True, text=True, + [ + "gh", + "run", + "download", + str(run_id), + "--pattern", + artifact, + "--dir", + str(download_dir), + ], + capture_output=True, + text=True, ) if result.returncode == 0: found = _find_manifest(download_dir, manifest_name) @@ -433,7 +454,12 @@ def _ensure_manifest( f"Manifest {manifest} missing and no gh run ID provided" ) downloaded = _wait_and_download_manifest( - run_id, artifact, download_dir, manifest.name, max_attempts, sleep_seconds, + run_id, + artifact, + download_dir, + manifest.name, + max_attempts, + sleep_seconds, ) if downloaded != manifest: shutil.copy(downloaded, manifest) @@ -470,7 +496,9 @@ def parse_args() -> argparse.Namespace: rp.add_argument("--artifacts", nargs="*") rp.add_argument("--version", type=str) - np = sub.add_parser("artifact-name", help="Print the artifact filename for given params") + np = sub.add_parser( + "artifact-name", help="Print the artifact filename for given params" + ) np.add_argument("--platform", required=True, choices=["linux", "macos", "windows"]) np.add_argument("--arch", required=True, choices=["x64", "arm64", "aarch64"]) np.add_argument("--mode", required=True, choices=["Debug", "RelWithDebInfo"]) @@ -506,9 +534,15 @@ def main() -> None: version=args.version, ) elif args.action == "artifact-name": - print(build_artifact_name( - args.platform, args.arch, args.mode, lto=args.lto, asan=args.asan, - )) + print( + build_artifact_name( + args.platform, + args.arch, + args.mode, + lto=args.lto, + asan=args.asan, + ) + ) if __name__ == "__main__": diff --git a/scripts/update-llvm-version.py b/scripts/update-llvm-version.py index ab15ba9b..e19876fb 100755 --- a/scripts/update-llvm-version.py +++ b/scripts/update-llvm-version.py @@ -17,7 +17,9 @@ def copy_manifest(src: Path, dest: Path) -> None: sys.exit(1) if not isinstance(data, dict) or "artifacts" not in data: - print(f"Error: {src} must be a JSON object with 'artifacts' key", file=sys.stderr) + print( + f"Error: {src} must be a JSON object with 'artifacts' key", file=sys.stderr + ) sys.exit(1) dest.parent.mkdir(parents=True, exist_ok=True) @@ -84,7 +86,9 @@ def check_manifest(path: Path) -> None: print(f"Error: {path} is not valid JSON: {err}", file=sys.stderr) sys.exit(1) if not isinstance(data, dict) or "artifacts" not in data: - print(f"Error: {path} must be a JSON object with 'artifacts' key", file=sys.stderr) + print( + f"Error: {path} must be a JSON object with 'artifacts' key", file=sys.stderr + ) sys.exit(1) artifacts = data["artifacts"] if not artifacts: diff --git a/scripts/upload-llvm.py b/scripts/upload-llvm.py index 1753e73a..f97e3322 100644 --- a/scripts/upload-llvm.py +++ b/scripts/upload-llvm.py @@ -8,6 +8,7 @@ sys.path.insert(0, str(Path(__file__).parent)) from importlib import import_module + release_llvm = import_module("release-llvm") diff --git a/src/semantic/resolver.cpp b/src/semantic/resolver.cpp index 3f64bab6..2851f838 100644 --- a/src/semantic/resolver.cpp +++ b/src/semantic/resolver.cpp @@ -963,14 +963,15 @@ class PseudoInstantiator : public clang::TreeTransform { } auto NNSLoc = TransformNestedNameSpecifierLoc(TL.getQualifierLoc()); - clang::NestedNameSpecifier NNS = NNSLoc - ? NNSLoc.getNestedNameSpecifier() - : DTN->getQualifier(); + clang::NestedNameSpecifier NNS = + NNSLoc ? NNSLoc.getNestedNameSpecifier() : DTN->getQualifier(); clang::TemplateArgumentListInfo info; - using arg_iterator = clang::TemplateArgumentLocContainerIterator< - clang::TemplateSpecializationTypeLoc>; - if(TransformTemplateArguments(arg_iterator(TL, 0), arg_iterator(TL, TL.getNumArgs()), info)) { + using arg_iterator = + clang::TemplateArgumentLocContainerIterator; + if(TransformTemplateArguments(arg_iterator(TL, 0), + arg_iterator(TL, TL.getNumArgs()), + info)) { auto original = clang::QualType(TST, 0); TLB.pushTrivial(context, original, {}); --indent; From a8720c91dc79d482ce498b5cee3f3b551cbdcdc4 Mon Sep 17 00:00:00 2001 From: ykiko Date: Wed, 10 Jun 2026 00:04:49 +0800 Subject: [PATCH 28/28] chore: remove dead upload-llvm workflow and scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These are superseded by release-llvm.yml which handles the full discover → repackage → upload pipeline. --- .github/workflows/upload-llvm.yml | 38 -------------- pixi.toml | 9 ---- scripts/download-llvm.sh | 16 ------ scripts/upload-llvm.py | 87 ------------------------------- 4 files changed, 150 deletions(-) delete mode 100644 .github/workflows/upload-llvm.yml delete mode 100755 scripts/download-llvm.sh delete mode 100644 scripts/upload-llvm.py diff --git a/.github/workflows/upload-llvm.yml b/.github/workflows/upload-llvm.yml deleted file mode 100644 index 6ad5def1..00000000 --- a/.github/workflows/upload-llvm.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: upload-llvm - -permissions: - contents: write - -on: - pull_request: - # if you want to run this workflow, change the branch name to main, - # if you want to turn off it, change it to non existent branch. - branches: [main-turn-off] - - workflow_dispatch: - inputs: - workflow_id: - description: "Workflow run ID to pull artifacts from" - required: true - type: string - version: - description: "Release version/tag to publish (e.g., v1.2.3)" - required: true - type: string - -jobs: - upload: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - - name: Download artifacts from workflow - env: - GH_TOKEN: ${{ github.token }} - run: scripts/download-llvm.sh "${{ inputs.workflow_id }}" - - - name: Recreate release with artifacts - env: - GH_TOKEN: ${{ secrets.UPLOAD_LLVM }} - TARGET_REPO: clice-io/clice-llvm - run: python3 scripts/upload-llvm.py "${{ inputs.version }}" "${TARGET_REPO}" "${{ inputs.workflow_id }}" diff --git a/pixi.toml b/pixi.toml index 8c05d9e3..e4177ab1 100644 --- a/pixi.toml +++ b/pixi.toml @@ -191,15 +191,6 @@ depends-on = [ [tasks.build-llvm] cmd = ["python3", "scripts/build-llvm.py"] -[tasks.upload-llvm] -args = ["workflow_id", "llvm_version", { "arg" = "branch", default = "main" }] -cmd = """ -gh workflow run upload-llvm.yml \ - --ref {{ branch }} \ - --field workflow_id={{workflow_id}} \ - --field version={{llvm_version}} -""" - [tasks.delete-artifacts] args = ["file_name"] cmd = ["scripts/delete-artifacts.bash", "{{ file_name }}"] diff --git a/scripts/download-llvm.sh b/scripts/download-llvm.sh deleted file mode 100755 index 3a640e03..00000000 --- a/scripts/download-llvm.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if [[ $# -ne 1 ]]; then - echo "Usage: $0 " >&2 - exit 1 -fi - -WORKFLOW_ID="$1" - -mkdir -p artifacts -gh run download "${WORKFLOW_ID}" --dir artifacts - -echo "Downloaded artifacts:" -find artifacts -maxdepth 4 -type f -print diff --git a/scripts/upload-llvm.py b/scripts/upload-llvm.py deleted file mode 100644 index f97e3322..00000000 --- a/scripts/upload-llvm.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 - -import json -import os -import subprocess -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent)) -from importlib import import_module - -release_llvm = import_module("release-llvm") - - -def main() -> None: - if len(sys.argv) != 4: - print( - "Usage: upload-llvm.py ", file=sys.stderr - ) - sys.exit(1) - - tag, target_repo, workflow_id = sys.argv[1:] - artifacts_dir = Path("artifacts") - - if not artifacts_dir.is_dir(): - print(f"Artifacts directory not found: {artifacts_dir}", file=sys.stderr) - sys.exit(1) - - artifact_files = sorted( - p - for p in artifacts_dir.rglob("*") - if p.is_file() and p.suffix.lower() != ".json" - ) - - if not artifact_files: - print("No artifacts found to upload.", file=sys.stderr) - sys.exit(1) - - version_without_prefix = tag.lstrip("vV") - manifest = {"version": version_without_prefix, "artifacts": {}} - for path in artifact_files: - entry = release_llvm.build_metadata_entry(path, version_without_prefix) - filename = entry.pop("filename") - entry.pop("version", None) - manifest["artifacts"][filename] = entry - - json_path = artifacts_dir / "llvm-manifest.json" - with json_path.open("w", encoding="utf-8") as handle: - json.dump(manifest, handle, indent=2) - handle.write("\n") - - assets = [str(path) for path in artifact_files] - assets.append(str(json_path)) - - env = os.environ.copy() - - print(f"Checking for existing release {tag} in {target_repo}...") - view_result = subprocess.run( - ["gh", "release", "view", tag, "--repo", target_repo], - env=env, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - if view_result.returncode == 0: - print(f"Deleting existing release {tag} in {target_repo}...") - subprocess.run( - ["gh", "release", "delete", tag, "--repo", target_repo, "-y"], - env=env, - check=True, - ) - - print(f"Creating release {tag} in {target_repo} with {len(assets)} assets...") - # fmt: off - args = [ - "gh", "release", "create", tag, *assets, - "--repo", target_repo, - "--title", tag, - "--notes", f"Artifacts build from workflow run https://github.com/clice-io/clice/actions/runs/{workflow_id}", - "--latest", - ] - # fmt: on - subprocess.run(args, env=env, check=True) - - -if __name__ == "__main__": - main()