0.6.9 #48
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Tauri App | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| # Prevent concurrent release runs against the same ref (e.g. a re-tag or a | |
| # manual dispatch colliding with a tag push) from racing to create the same | |
| # draft release. Not cancelling in-progress runs: an aborted build can leave | |
| # a half-uploaded release asset behind. | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| CARGO_INCREMENTAL: 0 | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| build-tauri: | |
| permissions: | |
| contents: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: 'macos-latest' | |
| args: '--target universal-apple-darwin' | |
| rust_target: 'aarch64-apple-darwin,x86_64-apple-darwin' | |
| cache_key: 'rust-universal-apple-darwin' | |
| - platform: 'windows-latest' | |
| args: '' | |
| cache_key: 'rust-x86_64-pc-windows-msvc' | |
| # Use Ubuntu 22.04 for broader Linux compatibility (glibc 2.35) | |
| # This supports Debian 12+, Ubuntu 22.04+, Fedora 36+, etc. | |
| - platform: 'ubuntu-22.04' | |
| args: '' | |
| cache_key: 'rust-x86_64-unknown-linux-gnu' | |
| runs-on: ${{ matrix.platform }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| # Install npm deps early so lockfile drift or a registry outage fails | |
| # the job before we spend minutes on Homebrew / lipo / native-lib work. | |
| - name: Install dependencies (root) | |
| run: npm ci | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.rust_target || '' }} | |
| - name: Cache Rust dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: "src-tauri -> target" | |
| shared-key: ${{ matrix.cache_key }} | |
| cache-on-failure: true | |
| - name: Install Linux dependencies | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libvips-dev pkg-config \ | |
| libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev \ | |
| librsvg2-dev patchelf | |
| - name: Cache libvips (Windows) | |
| if: runner.os == 'Windows' | |
| id: cache-vips | |
| uses: actions/cache@v5 | |
| with: | |
| path: vendor/libvips-native | |
| key: libvips-win64-8.18.0 | |
| - name: Install libvips (Windows) | |
| if: runner.os == 'Windows' && steps.cache-vips.outputs.cache-hit != 'true' | |
| shell: pwsh | |
| run: | | |
| $vipsVersion = "8.18.0" | |
| $url = "https://github.com/libvips/build-win64-mxe/releases/download/v${vipsVersion}/vips-dev-w64-web-${vipsVersion}.zip" | |
| Invoke-WebRequest -Uri $url -OutFile vips.zip | |
| Expand-Archive -Path vips.zip -DestinationPath vendor\libvips-extract | |
| Move-Item vendor\libvips-extract\vips-dev-8.18 vendor\libvips-native | |
| Remove-Item vips.zip | |
| Remove-Item -Recurse vendor\libvips-extract | |
| - name: Add libvips to PATH (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: echo "${{ github.workspace }}\vendor\libvips-native\bin" | Out-File -Append -FilePath $env:GITHUB_PATH | |
| - name: Stage native DLLs (Windows) | |
| if: runner.os == 'Windows' | |
| id: stage-windows | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path src-tauri\native-dlls | |
| Copy-Item vendor\libvips-native\bin\*.dll src-tauri\native-dlls\ | |
| $count = (Get-ChildItem src-tauri\native-dlls\*.dll).Count | |
| Write-Host "Staged $count DLLs for bundling" | |
| $json = '{"bundle":{"resources":{"native-dlls/":"native-dlls"}}}' | |
| Set-Content -Path platform-config.json -Value $json | |
| echo "config_args=--config platform-config.json" | Out-File -Append -FilePath $env:GITHUB_OUTPUT | |
| - name: Install libvips ARM64 (macOS) | |
| if: runner.os == 'macOS' | |
| env: | |
| HOMEBREW_NO_AUTO_UPDATE: '1' | |
| HOMEBREW_NO_INSTALL_CLEANUP: '1' | |
| run: | | |
| brew install vips | |
| echo "VIPS_DIR=$(brew --prefix vips)" >> $GITHUB_ENV | |
| # Cache the x86_64 Homebrew installation (Homebrew repo + Cellar) so we don't | |
| # re-run the install script and 'brew install vips' (+ ~40 transitive deps) on | |
| # every run. The cache key is pinned to "vips-v1"; bump to "vips-v2" when vips | |
| # or a major dep updates and a fresh install is needed. | |
| - name: Cache x86_64 Homebrew (macOS) | |
| if: runner.os == 'macOS' | |
| id: cache-x86-brew | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| /usr/local/Homebrew | |
| /usr/local/Cellar | |
| key: macos-x86-homebrew-vips-v1 | |
| - name: Install libvips x86_64 (macOS, for universal build) | |
| if: runner.os == 'macOS' | |
| env: | |
| HOMEBREW_NO_AUTO_UPDATE: '1' | |
| HOMEBREW_NO_INSTALL_CLEANUP: '1' | |
| run: | | |
| if [ "${{ steps.cache-x86-brew.outputs.cache-hit }}" = "true" ]; then | |
| # Recreate /usr/local/bin/brew symlink and re-link all formulae whose | |
| # opt/lib/include symlinks are not preserved by the cache action. | |
| ln -sf /usr/local/Homebrew/bin/brew /usr/local/bin/brew 2>/dev/null || true | |
| arch -x86_64 /usr/local/bin/brew link \ | |
| $(arch -x86_64 /usr/local/bin/brew list --formula) 2>/dev/null || true | |
| else | |
| arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" | |
| arch -x86_64 /usr/local/bin/brew install vips | |
| fi | |
| # Hard-fail if the x86_64 vips prefix is missing — without this the | |
| # collect-universal-dylibs.sh script silently emits ::warning:: and | |
| # produces an ARM64-only DMG that breaks on Intel Macs. | |
| X86_VIPS_PREFIX=$(arch -x86_64 /usr/local/bin/brew --prefix vips 2>/dev/null || true) | |
| if [ -z "$X86_VIPS_PREFIX" ] || [ ! -d "$X86_VIPS_PREFIX/lib" ]; then | |
| echo "::error::x86_64 Homebrew vips not available at expected prefix. Cache may be corrupt — bump the cache key (currently 'macos-x86-homebrew-vips-v1')." | |
| exit 1 | |
| fi | |
| echo "x86_64 vips verified at $X86_VIPS_PREFIX" | |
| - name: Stage universal dylibs (macOS) | |
| if: runner.os == 'macOS' | |
| id: stage-macos | |
| run: | | |
| chmod +x scripts/collect-universal-dylibs.sh | |
| ./scripts/collect-universal-dylibs.sh src-tauri/native-libs | |
| FRAMEWORKS=$(cd src-tauri && find native-libs -name "*.dylib" -maxdepth 1 \ | |
| | sort | sed 's/^/"/' | sed 's/$/"/' | paste -sd, -) | |
| echo "{\"bundle\":{\"macOS\":{\"frameworks\":[$FRAMEWORKS]}}}" \ | |
| > platform-config.json | |
| echo "config_args=--config platform-config.json" >> $GITHUB_OUTPUT | |
| - name: Fix dylib install names for bundling (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| STAGING="src-tauri/native-libs" | |
| # Phase 1 — Change install names in the Homebrew source dylibs before | |
| # compilation. The Rust linker records a dylib's ID verbatim into the | |
| # binary's LC_LOAD_DYLIB; after this, it records @rpath/<name> instead | |
| # of the absolute Homebrew path, so dyld finds the bundled copy at runtime. | |
| for staged in "$STAGING"/*.dylib; do | |
| [ -f "$staged" ] || continue | |
| name=$(basename "$staged") | |
| for brew_lib in \ | |
| /opt/homebrew/opt/*/lib/"$name" \ | |
| /usr/local/opt/*/lib/"$name"; do | |
| [ -f "$brew_lib" ] && install_name_tool -id "@rpath/$name" "$brew_lib" || true | |
| done | |
| done | |
| # Phase 2a — Fix each staged universal dylib's own ID. | |
| for dylib in "$STAGING"/*.dylib; do | |
| [ -f "$dylib" ] || continue | |
| install_name_tool -id "@rpath/$(basename "$dylib")" "$dylib" | |
| done | |
| # Phase 2b — Rewrite references inside each staged dylib that point to | |
| # other bundled dylibs from absolute Homebrew paths to @rpath/<name>. | |
| # This fixes transitive loading (e.g. libvips -> libgobject-2.0). | |
| for dylib in "$STAGING"/*.dylib; do | |
| [ -f "$dylib" ] || continue | |
| while IFS= read -r dep; do | |
| depname=$(basename "$dep") | |
| if [ -f "$STAGING/$depname" ]; then | |
| install_name_tool -change "$dep" "@rpath/$depname" "$dylib" 2>/dev/null || true | |
| fi | |
| done < <(otool -L "$dylib" | awk 'NR>1 {print $1}') | |
| done | |
| - name: Get changelog for release notes | |
| id: changelog | |
| shell: bash | |
| run: | | |
| if [ -f CHANGELOG.md ]; then | |
| DELIMITER="CHANGELOG_$(openssl rand -hex 8)" | |
| { | |
| printf '%s\n' "body<<$DELIMITER" | |
| tr -d '\r' < CHANGELOG.md | |
| printf '\n%s\n' "$DELIMITER" | |
| } >> "$GITHUB_OUTPUT" | |
| else | |
| printf 'body=See the assets to download this version and install.\n' >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Build Tauri app | |
| id: build-tauri | |
| uses: tauri-apps/tauri-action@v0 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| with: | |
| projectPath: . | |
| args: >- | |
| ${{ matrix.args }} | |
| ${{ steps.stage-windows.outputs.config_args || '' }} | |
| ${{ steps.stage-macos.outputs.config_args || '' }} | |
| includeDebug: false | |
| tagName: v__VERSION__ | |
| releaseName: 'Image Optimizer v__VERSION__' | |
| releaseBody: ${{ steps.changelog.outputs.body }} | |
| releaseDraft: true | |
| # Tags containing a hyphen (e.g. v0.7.0-beta.1) are treated as prereleases. | |
| prerelease: ${{ contains(github.ref_name, '-') }} | |
| - name: Verify bundled dylibs (macOS) | |
| if: runner.os == 'macOS' | |
| env: | |
| ARTIFACT_PATHS: ${{ steps.build-tauri.outputs.artifactPaths }} | |
| run: | | |
| # Tauri v2's DMG bundler deletes the .app from disk after | |
| # packaging it (look for "Cleaning ... Image Optimizer.app" | |
| # in the Build Tauri app log). A plain `find src-tauri/target | |
| # -type d -name "*.app"` therefore returns nothing by the | |
| # time this step runs — the only .app that exists lives | |
| # inside the .dmg. Mount the DMG read-only and verify the | |
| # .app inside it. | |
| DMG="" | |
| if [ -n "${ARTIFACT_PATHS:-}" ] && command -v jq >/dev/null 2>&1; then | |
| DMG=$(printf '%s' "$ARTIFACT_PATHS" \ | |
| | jq -r '.[] | select(endswith(".dmg"))' 2>/dev/null \ | |
| | head -1 || true) | |
| fi | |
| if [ -z "$DMG" ] || [ ! -f "$DMG" ]; then | |
| DMG=$(find src-tauri/target -type f -name "*.dmg" 2>/dev/null | head -1 || true) | |
| fi | |
| if [ -z "$DMG" ] || [ ! -f "$DMG" ]; then | |
| echo "::error::No DMG found to verify (searched artifactPaths and src-tauri/target)" | |
| exit 1 | |
| fi | |
| echo "Verifying DMG at: $DMG" | |
| # mktemp -d gives us an empty dir that hdiutil is happy to | |
| # use as an explicit mountpoint. Picking our own mountpoint | |
| # keeps cleanup predictable regardless of volume name. The | |
| # trap detaches on any exit (success, failure, or set -e | |
| # abort) so a failing check never leaves a dangling mount. | |
| MOUNT=$(mktemp -d) | |
| hdiutil attach -nobrowse -readonly -mountpoint "$MOUNT" "$DMG" >/dev/null | |
| trap 'hdiutil detach "$MOUNT" -quiet -force >/dev/null 2>&1 || true' EXIT | |
| APP=$(find "$MOUNT" -maxdepth 2 -type d -name "*.app" 2>/dev/null | head -1 || true) | |
| if [ -z "$APP" ] || [ ! -d "$APP" ]; then | |
| echo "::error::No .app bundle found inside mounted DMG" | |
| ls -la "$MOUNT" || true | |
| exit 1 | |
| fi | |
| echo "Verifying bundle at: $APP" | |
| BIN="$APP/Contents/MacOS/$(basename "$APP" .app)" | |
| FRAMEWORKS="$APP/Contents/Frameworks" | |
| if [ ! -d "$FRAMEWORKS" ]; then | |
| echo "::error::No Frameworks directory in bundle — native-libs were not bundled" | |
| exit 1 | |
| fi | |
| DYLIB_COUNT=$(find "$FRAMEWORKS" -maxdepth 1 -name "*.dylib" | wc -l | tr -d ' ') | |
| echo "Frameworks contains $DYLIB_COUNT dylibs" | |
| if [ "$DYLIB_COUNT" -eq 0 ]; then | |
| echo "::error::No dylibs bundled in Frameworks/" | |
| exit 1 | |
| fi | |
| FAIL=0 | |
| # Check 1: no absolute Homebrew paths in the main binary or any | |
| # bundled dylib — those indicate install_name_tool rewrites were | |
| # missed and the app will fail to launch on machines without | |
| # Homebrew at the exact same path. | |
| if otool -L "$BIN" | grep -E '(/opt/homebrew|/usr/local)/' ; then | |
| echo "::error::Main binary references absolute Homebrew paths; install_name fixup is incomplete" | |
| FAIL=1 | |
| fi | |
| for dylib in "$FRAMEWORKS"/*.dylib; do | |
| if otool -L "$dylib" | tail -n +2 | grep -E '(/opt/homebrew|/usr/local)/' ; then | |
| echo "::error::$(basename "$dylib") references absolute Homebrew paths" | |
| FAIL=1 | |
| fi | |
| done | |
| # Check 2: every @rpath/@loader_path/@executable_path reference | |
| # in the main binary and each bundled dylib must have a matching | |
| # file in Frameworks/. This catches transitive deps that the | |
| # collect script failed to follow (e.g. libwebp -> libsharpyuv). | |
| check_rpath_refs() { | |
| local target="$1" | |
| local target_name | |
| target_name=$(basename "$target") | |
| local local_fail=0 | |
| local dep | |
| while IFS= read -r dep; do | |
| case "$dep" in | |
| @rpath/*|@loader_path/*|@executable_path/*) | |
| local base | |
| base=$(basename "$dep") | |
| # A dylib's own ID shows up as the first otool line; skip | |
| # self-references. | |
| if [ "$base" = "$target_name" ]; then | |
| continue | |
| fi | |
| if [ ! -f "$FRAMEWORKS/$base" ]; then | |
| echo "::error::$target_name references $dep but Frameworks/$base is missing" | |
| local_fail=1 | |
| fi | |
| ;; | |
| esac | |
| done < <(otool -L "$target" | tail -n +2 | awk '{print $1}') | |
| return $local_fail | |
| } | |
| check_rpath_refs "$BIN" || FAIL=1 | |
| for dylib in "$FRAMEWORKS"/*.dylib; do | |
| check_rpath_refs "$dylib" || FAIL=1 | |
| done | |
| if [ "$FAIL" -ne 0 ]; then | |
| echo "::error::Bundle verification failed — see errors above" | |
| exit 1 | |
| fi | |
| echo "OK: all @rpath references resolve inside the bundle and no absolute Homebrew paths remain" | |
| - name: Verify native DLLs in bundle (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $nsis = "src-tauri\target\release\bundle\nsis" | |
| if (Test-Path $nsis) { | |
| $dlls = Get-ChildItem "$nsis\*.dll" -Recurse -ErrorAction SilentlyContinue | |
| Write-Host "Found $($dlls.Count) DLLs in NSIS bundle" | |
| } | |
| $dlls = Get-ChildItem "src-tauri\target\release\native-dlls\*.dll" -ErrorAction SilentlyContinue | |
| if ($dlls) { | |
| Write-Host "OK: $($dlls.Count) native DLLs staged for bundling" | |
| } else { | |
| Write-Host "::warning::No native DLLs found in target — check bundle.resources config" | |
| } | |
| - name: Verify AppImage contains native libs (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| APPIMAGE=$(find src-tauri/target/release/bundle/appimage -name "*.AppImage" 2>/dev/null | head -1) | |
| if [ -z "$APPIMAGE" ]; then | |
| echo "::warning::No AppImage found to verify" | |
| exit 0 | |
| fi | |
| chmod +x "$APPIMAGE" | |
| "$APPIMAGE" --appimage-extract > /dev/null 2>&1 | |
| if find squashfs-root -name "libvips*" | grep -q .; then | |
| echo "OK: libvips found in AppImage" | |
| else | |
| echo "::warning::libvips NOT found in AppImage — users without system libvips will fail" | |
| fi | |
| rm -rf squashfs-root |