Build & Package (Windows, macOS, Linux) #7
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 & Package (Windows, macOS, Linux) | |
| on: | |
| workflow_dispatch: # manual run from Actions tab | |
| env: | |
| BIN_NAME: obamify | |
| # Human-facing display name for the app | |
| APP_DISPLAY_NAME: obamify | |
| # Reverse-DNS bundle identifier | |
| APP_BUNDLE_ID: com.obamify | |
| jobs: | |
| build: | |
| name: Build ${{ matrix.os_tag }} | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - runner: ubuntu-latest | |
| os_tag: linux | |
| target: x86_64-unknown-linux-gnu | |
| ext: "" | |
| - runner: macos-14 | |
| os_tag: macos | |
| target: aarch64-apple-darwin | |
| ext: "" | |
| - runner: windows-latest | |
| os_tag: windows | |
| target: x86_64-pc-windows-msvc | |
| ext: ".exe" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Cargo build (release) | |
| run: cargo build --release --target ${{ matrix.target }} | |
| - name: Package (.app bundle) | |
| if: ${{ startsWith(matrix.runner, 'macos') }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| APP="${{ env.BIN_NAME }}" | |
| DISP="${{ env.APP_DISPLAY_NAME }}" | |
| BUNDLE_ID="${{ env.APP_BUNDLE_ID }}" | |
| BIN="target/${{ matrix.target }}/release/${APP}${{ matrix.ext }}" | |
| test -f "$BIN" || (echo "Missing $BIN" && exit 1) | |
| # Derive version from Cargo metadata (fallback to 0.0.0) | |
| VERSION="$(cargo metadata --format-version 1 --no-deps | python3 -c "import sys, json; m=json.load(sys.stdin); p=[x for x in m['packages'] if x['name']=='${APP}']; print(p[0]['version'] if p else '0.0.0')")" | |
| APPDIR="dist/${DISP}.app" | |
| mkdir -p "${APPDIR}/Contents/MacOS" "${APPDIR}/Contents/Resources" | |
| # Binary | |
| cp "$BIN" "${APPDIR}/Contents/MacOS/${APP}" | |
| chmod +x "${APPDIR}/Contents/MacOS/${APP}" | |
| # Icon (optional) - put your .icns at assets/macos/icon.icns | |
| if [ -f assets/macos/icon.icns ]; then | |
| cp assets/macos/icon.icns "${APPDIR}/Contents/Resources/AppIcon.icns" | |
| ICON_LINE="<key>CFBundleIconFile</key><string>AppIcon</string>" | |
| else | |
| ICON_LINE="" | |
| fi | |
| # Generate Info.plist | |
| cat > "${APPDIR}/Contents/Info.plist" <<PLIST | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>CFBundleName</key><string>${DISP}</string> | |
| <key>CFBundleDisplayName</key><string>${DISP}</string> | |
| <key>CFBundleExecutable</key><string>${APP}</string> | |
| <key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string> | |
| <key>CFBundleVersion</key><string>${VERSION}</string> | |
| <key>CFBundleShortVersionString</key><string>${VERSION}</string> | |
| <key>LSMinimumSystemVersion</key><string>11.0</string> | |
| <key>LSApplicationCategoryType</key><string>public.app-category.utilities</string> | |
| <key>NSHighResolutionCapable</key><true/> | |
| ${ICON_LINE} | |
| </dict> | |
| </plist> | |
| PLIST | |
| # Zip for distribution (zip keeps resource forks properly with ditto flags) | |
| mkdir -p dist | |
| ditto -c -k --sequesterRsrc --keepParent "${APPDIR}" "dist/${APP}-${{ matrix.os_tag }}.zip" | |
| - name: Package (Linux AppImage) | |
| if: ${{ startsWith(matrix.runner, 'ubuntu') }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| APP="${{ env.BIN_NAME }}" | |
| DISP="${{ env.APP_DISPLAY_NAME }}" | |
| BIN="target/${{ matrix.target }}/release/${APP}" | |
| test -f "$BIN" || (echo "Missing $BIN" && exit 1) | |
| mkdir -p dist/AppDir/usr/bin dist/AppDir/usr/share/applications dist/AppDir/usr/share/icons/hicolor/256x256/apps | |
| # Put binary in AppDir | |
| cp "$BIN" dist/AppDir/usr/bin/${APP} | |
| chmod +x dist/AppDir/usr/bin/${APP} | |
| # Icon (optional) - place a 256x256 PNG at assets/linux/icon.png | |
| if [ -f assets/linux/icon.png ]; then | |
| cp assets/linux/icon.png dist/AppDir/usr/share/icons/hicolor/256x256/apps/${APP}.png | |
| ICON_PATH=dist/AppDir/usr/share/icons/hicolor/256x256/apps/${APP}.png | |
| else | |
| ICON_PATH= | |
| fi | |
| # Desktop file | |
| cat > dist/AppDir/usr/share/applications/${APP}.desktop <<DESKTOP | |
| [Desktop Entry] | |
| Type=Application | |
| Name=${DISP} | |
| Exec=${APP} | |
| Icon=${APP} | |
| Categories=Utility; | |
| Terminal=false | |
| DESKTOP | |
| # AppRun launcher | |
| cat > dist/AppDir/AppRun <<'APPRUN' | |
| #!/bin/sh | |
| HERE="$(dirname "$(readlink -f "$0")")" | |
| export PATH="$HERE/usr/bin:$PATH" | |
| exec "${HERE}/usr/bin/${APP_BIN:-${APP}}" "$@" | |
| APPRUN | |
| sed -i "s/\${APP}/${APP}/g" dist/AppDir/AppRun | |
| chmod +x dist/AppDir/AppRun | |
| # Fetch linuxdeploy + appimagetool | |
| wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" -O linuxdeploy | |
| wget -q "https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage" -O appimagetool | |
| chmod +x linuxdeploy appimagetool | |
| # Build AppImage | |
| ./linuxdeploy --appdir dist/AppDir \ | |
| -d dist/AppDir/usr/share/applications/${APP}.desktop \ | |
| $( [ -n "$ICON_PATH" ] && echo "-i $ICON_PATH" ) \ | |
| --output appimage | |
| # Move result(s) to dist with stable name | |
| mkdir -p dist | |
| mv *.AppImage "dist/${APP}-${{ matrix.os_tag }}.AppImage" || true | |
| # Tarball fallback | |
| tar -C "target/${{ matrix.target }}/release" -czf "dist/${APP}-${{ matrix.os_tag }}.tar.gz" "${APP}" | |
| - name: Package (Windows) | |
| if: startsWith(matrix.runner, 'windows') | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $App = "${{ env.BIN_NAME }}" | |
| $Bin = "target/${{ matrix.target }}/release/$App${{ matrix.ext }}" | |
| if (-not (Test-Path $Bin)) { throw "Missing $Bin" } | |
| New-Item -ItemType Directory -Force -Path "dist" | Out-Null | |
| Copy-Item $Bin -Destination "dist/$App-${{ matrix.os_tag }}${{ matrix.ext }}" -Force | |
| - name: Upload artifacts (macOS) | |
| if: ${{ startsWith(matrix.runner, 'macos') }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.BIN_NAME }}-${{ matrix.os_tag }} | |
| path: | | |
| dist/${{ env.APP_DISPLAY_NAME }}.app | |
| dist/${{ env.BIN_NAME }}-${{ matrix.os_tag }}.zip | |
| - name: Upload artifacts (Linux) | |
| if: ${{ startsWith(matrix.runner, 'ubuntu') }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.BIN_NAME }}-${{ matrix.os_tag }} | |
| path: | | |
| dist/${{ env.BIN_NAME }}-${{ matrix.os_tag }}.AppImage | |
| dist/${{ env.BIN_NAME }}-${{ matrix.os_tag }}.tar.gz | |
| - name: Upload artifacts (Windows) | |
| if: startsWith(matrix.runner, 'windows') | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.BIN_NAME }}-${{ matrix.os_tag }} | |
| path: dist/${{ env.BIN_NAME }}-${{ matrix.os_tag }}${{ matrix.ext }} |