diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..84391c1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,120 @@ +name: Build and test + +on: + push: + branches: [master] + tags: ['v*'] + pull_request: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: macOS ARM64 + os: macos-15 + arch: arm64 + args: --wrap-mode=forcefallback -Dzlib=enabled -Dlibass:asm=enabled + binary: libSubInspector.dylib + artifact: SubInspector-macos-arm64 + - name: macOS x86_64 (cross) + os: macos-15 + arch: x86_64 + args: --wrap-mode=forcefallback -Dzlib=enabled -Dlibass:asm=enabled --cross-file tools/ci/macos-x86_64.ini + binary: libSubInspector.dylib + artifact: SubInspector-macos-x86_64 + - name: Windows x64 + os: windows-2025 + arch: x64 + args: --wrap-mode=forcefallback -Dzlib=enabled -Dlibass:asm=enabled -Db_vscrt=mt + binary: SubInspector.dll + artifact: SubInspector-windows-x64 + - name: Linux (system dependencies) + os: ubuntu-24.04 + args: --wrap-mode=nofallback -Dbuildtype=debugoptimized -Dzlib=enabled + - name: Linux (without zlib) + os: ubuntu-24.04 + args: --wrap-mode=nofallback -Dzlib=disabled + + env: + MACOSX_DEPLOYMENT_TARGET: '11.0' + + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.13' + - name: Install build tools + run: python -m pip install meson==1.12.0 ninja==1.13.0 + + - name: Set up MSVC + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1.13.0 + with: + arch: ${{ matrix.arch }} + - name: Install NASM (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + choco install nasm --yes --no-progress + "C:\Program Files\NASM" >> $env:GITHUB_PATH + - name: Install NASM (macOS) + if: runner.os == 'macOS' + run: brew install nasm + - name: Enable Intel tests through Rosetta + if: runner.os == 'macOS' && matrix.arch == 'x86_64' + run: sudo softwareupdate --install-rosetta --agree-to-license + - name: Install system dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config nasm libass-dev fonts-dejavu-core + + - name: Configure + run: meson setup build ${{ matrix.args }} + - name: Build + run: meson compile -C build + - name: Test + run: meson test -C build --print-errorlogs + - name: Check binary architecture and runtime dependencies + if: matrix.artifact + run: python tools/ci/check-binary.py build/src/${{ matrix.binary }} ${{ matrix.arch }} + - name: Check Linux uses patched libass + if: runner.os == 'Linux' + run: | + ldd build/src/libSubInspector.so + if ldd build/src/libSubInspector.so | grep -q 'libass\.so'; then + echo 'Unexpected runtime dependency on system libass' + exit 1 + fi + - name: Archive sources and package binaries + if: matrix.artifact + run: | + meson dist -C build --include-subprojects --no-tests + python tools/ci/package.py build ${{ matrix.binary }} + - name: Upload binaries and sources + if: matrix.artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.artifact }} + path: build/artifact/ + if-no-files-found: error + - name: Upload build and test logs + if: failure() + uses: actions/upload-artifact@v7 + with: + name: logs-${{ strategy.job-index }} + path: build/meson-logs/ diff --git a/tools/ci/check-binary.py b/tools/ci/check-binary.py new file mode 100644 index 0000000..4e06980 --- /dev/null +++ b/tools/ci/check-binary.py @@ -0,0 +1,41 @@ +"""Reject artifacts with the wrong architecture or non-system DLL dependencies.""" + +import re +import struct +import subprocess +import sys +from pathlib import Path + +binary = Path(sys.argv[1]) +arch = sys.argv[2] + +if sys.platform == 'darwin': + actual = subprocess.check_output(['lipo', '-archs', binary], text=True).strip() + if actual != arch: + sys.exit(f'Expected {arch}, got {actual}') + output = subprocess.check_output(['otool', '-L', binary], text=True) + print(output) + # The first load command is the library's own install name. + dependencies = [line.strip().split(' (', 1)[0] for line in output.splitlines()[2:]] + unexpected = [dep for dep in dependencies + if not dep.startswith(('/usr/lib/', '/System/Library/'))] +elif sys.platform == 'win32': + data = binary.read_bytes() + pe_offset = struct.unpack_from('