diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5663695a..b05227d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,12 +15,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false - name: Setup .NET - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: "10.0.x" @@ -77,13 +77,335 @@ jobs: cp publish/linux-arm64/WheelWizard artifacts/WheelWizard_arm64_Linux chmod +x artifacts/WheelWizard_Linux artifacts/WheelWizard_arm64_Linux + - name: Upload Linux + Windows artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: artifacts-linux-windows + path: artifacts/ + retention-days: 1 + + # ────────────────────────────────────────────────────────────── + # macOS builds (native on macOS runner) + # ────────────────────────────────────────────────────────────── + macos-arm64: + name: Build macOS (arm64) + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 + with: + dotnet-version: "10.0.x" + + - name: Build .app bundle for macOS arm64 + run: | + chmod +x macos/release-macos.sh + ./macos/release-macos.sh + env: + BUILD_ARCH: arm64 + + - name: Import signing certificate + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} + run: | + # Write the certificate from the secret + echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 + + # Create a temporary keychain + security create-keychain -p temp build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p temp build.keychain + + # Import the certificate + security import certificate.p12 -k build.keychain \ + -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign + + # Allow codesign to access the keychain + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k temp build.keychain + + # Verify the import + echo "[INFO] Available signing identities:" + security find-identity -v -p basic + rm certificate.p12 + + - name: Sign the .app bundle with Developer ID + run: | + ENTITLEMENTS="${{ github.workspace }}/macos/certs/WheelWizardEntitlements.entitlements" + APP_PATH="release/WheelWizard.app" + SIGNING_IDENTITY="Developer ID Application: Gabriel Magaña (Z4D7MUNZ97)" + + # List available identities for debugging + echo "[INFO] Checking signing identities..." + security find-identity -v -p codesigning + + # Sign all executables inside the bundle first + find "$APP_PATH/Contents/MacOS" -type f | while read fname; do + echo "[INFO] Signing $fname" + codesign --force --timestamp --options=runtime \ + --entitlements "$ENTITLEMENTS" \ + --sign "$SIGNING_IDENTITY" "$fname" + done + + # Sign the .app bundle itself + echo "[INFO] Signing .app bundle" + codesign --force --timestamp --options=runtime \ + --entitlements "$ENTITLEMENTS" \ + --sign "$SIGNING_IDENTITY" "$APP_PATH" + + - name: Create DMG + id: create-dmg + uses: L-Super/create-dmg-actions@8c59360de7ec731bfa03ce3bd2ad5ae650169864 # v1 + with: + dmg_name: WheelWizard-macOSarm64 + src_dir: release/WheelWizard.app + background: macos/backgr.png + window_size: 650 376 + icon_size: 128 + icon_position: 200 150 + app_drop_link: 450 150 + + - name: Notarize and staple the .app + env: + MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }} + MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }} + MACOS_NOTARY_APP_PASSWORD: ${{ secrets.MACOS_NOTARY_APP_PASSWORD }} + run: | + # Notarize the .app bundle + ditto -c -k --sequesterRsrc --keepParent \ + release/WheelWizard.app \ + WheelWizard-app-notarize.zip + xcrun notarytool submit WheelWizard-app-notarize.zip \ + --apple-id "$MACOS_NOTARY_APPLE_ID" \ + --team-id "$MACOS_NOTARY_TEAM_ID" \ + --password "$MACOS_NOTARY_APP_PASSWORD" \ + --wait + rm WheelWizard-app-notarize.zip + xcrun stapler staple release/WheelWizard.app + + - name: Notarize the DMG + env: + MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }} + MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }} + MACOS_NOTARY_APP_PASSWORD: ${{ secrets.MACOS_NOTARY_APP_PASSWORD }} + run: | + xcrun notarytool submit WheelWizard-macOSarm64.dmg \ + --apple-id "$MACOS_NOTARY_APPLE_ID" \ + --team-id "$MACOS_NOTARY_TEAM_ID" \ + --password "$MACOS_NOTARY_APP_PASSWORD" \ + --wait + xcrun stapler staple WheelWizard-macOSarm64.dmg + + - name: Upload macOS DMG + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: artifacts-macos-arm64 + path: WheelWizard-macOSarm64.dmg + retention-days: 1 + + macos-x64: + name: Build macOS (x64) + # Cross-compile x64 on an arm64 runner since macos-13 (Intel) is deprecated. + # .NET handles cross-compilation via the runtime identifier (osx-x64). + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 + with: + dotnet-version: "10.0.x" + + - name: Build .app bundle for macOS x64 + run: | + chmod +x macos/release-macos.sh + ./macos/release-macos.sh + env: + BUILD_ARCH: x64 + + - name: Import signing certificate + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} + run: | + echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 + security create-keychain -p temp build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p temp build.keychain + security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k temp build.keychain + rm certificate.p12 + + - name: Sign the .app bundle with Developer ID + run: | + ENTITLEMENTS="${{ github.workspace }}/macos/certs/WheelWizardEntitlements.entitlements" + APP_PATH="release/WheelWizard.app" + SIGNING_IDENTITY="Developer ID Application: Gabriel Magaña (Z4D7MUNZ97)" + + # Sign all executables inside the bundle first + find "$APP_PATH/Contents/MacOS" -type f | while read fname; do + echo "[INFO] Signing $fname" + codesign --force --timestamp --options=runtime \ + --entitlements "$ENTITLEMENTS" \ + --sign "$SIGNING_IDENTITY" "$fname" + done + + # Sign the .app bundle itself + echo "[INFO] Signing .app bundle" + codesign --force --timestamp --options=runtime \ + --entitlements "$ENTITLEMENTS" \ + --sign "$SIGNING_IDENTITY" "$APP_PATH" + + - name: Create DMG + id: create-dmg + uses: L-Super/create-dmg-actions@8c59360de7ec731bfa03ce3bd2ad5ae650169864 # v1 + with: + dmg_name: WheelWizard-macOSintel + src_dir: release/WheelWizard.app + background: macos/backgr.png + window_size: 650 376 + icon_size: 128 + icon_position: 200 150 + app_drop_link: 450 150 + + - name: Notarize and staple the .app + env: + MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }} + MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }} + MACOS_NOTARY_APP_PASSWORD: ${{ secrets.MACOS_NOTARY_APP_PASSWORD }} + run: | + # Notarize the .app bundle + ditto -c -k --sequesterRsrc --keepParent \ + release/WheelWizard.app \ + WheelWizard-app-notarize.zip + xcrun notarytool submit WheelWizard-app-notarize.zip \ + --apple-id "$MACOS_NOTARY_APPLE_ID" \ + --team-id "$MACOS_NOTARY_TEAM_ID" \ + --password "$MACOS_NOTARY_APP_PASSWORD" \ + --wait + rm WheelWizard-app-notarize.zip + xcrun stapler staple release/WheelWizard.app + + - name: Notarize the DMG + env: + MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }} + MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }} + MACOS_NOTARY_APP_PASSWORD: ${{ secrets.MACOS_NOTARY_APP_PASSWORD }} + run: | + xcrun notarytool submit WheelWizard-macOSintel.dmg \ + --apple-id "$MACOS_NOTARY_APPLE_ID" \ + --team-id "$MACOS_NOTARY_TEAM_ID" \ + --password "$MACOS_NOTARY_APP_PASSWORD" \ + --wait + xcrun stapler staple WheelWizard-macOSintel.dmg + + - name: Upload macOS DMG + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: artifacts-macos-x64 + path: WheelWizard-macOSintel.dmg + retention-days: 1 + + # ────────────────────────────────────────────────────────────── + # Create GitHub release with all assets + # ────────────────────────────────────────────────────────────── + create-release: + name: Create GitHub Release + needs: [release, macos-arm64, macos-x64] + + runs-on: ubuntu-latest + + steps: + - name: Download all artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + + - name: Prepare release assets + run: | + mkdir -p final-artifacts + + # Linux & Windows + cp artifacts-linux-windows/WheelWizardWindows.exe final-artifacts/ + cp artifacts-linux-windows/WheelWizard_Linux final-artifacts/ + cp artifacts-linux-windows/WheelWizard_arm64_Linux final-artifacts/ + + # macOS DMGs + cp artifacts-macos-arm64/WheelWizard-macOSarm64.dmg final-artifacts/ + cp artifacts-macos-x64/WheelWizard-macOSintel.dmg final-artifacts/ + + chmod +x final-artifacts/WheelWizard_Linux final-artifacts/WheelWizard_arm64_Linux + ls -la final-artifacts/ + - name: Create GitHub release env: GH_TOKEN: ${{ github.token }} - run: > - gh release create "${{ github.ref_name }}" - artifacts/WheelWizardWindows.exe - artifacts/WheelWizard_Linux - artifacts/WheelWizard_arm64_Linux - --title "${{ github.ref_name }}" - --generate-notes + run: | + REPO="${{ github.repository }}" + TAG_NAME="${GITHUB_REF_NAME}" + ASSETS=(final-artifacts/*) + gh release create "$TAG_NAME" "${ASSETS[@]}" \ + --repo "$REPO" \ + --title "$TAG_NAME" \ + --generate-notes + + + # ────────────────────────────────────────────────────────────── + # Update Homebrew tap with new version and SHA256 + # (Disabled for now — will be re-enabled later) + # ────────────────────────────────────────────────────────────── + # update-tap: + # name: Update Homebrew Tap + # needs: [create-release, macos-arm64, macos-x64] + # runs-on: ubuntu-latest + # + # steps: + # - name: Download SHA256 artifacts + # uses: actions/download-artifact@v4 + # with: + # pattern: sha256-* + # merge-multiple: true + # + # - name: Read SHA256 values + # id: shas + # run: | + # echo "arm64=$(cat sha256-arm64.txt)" >> "$GITHUB_OUTPUT" + # echo "intel=$(cat sha256-intel.txt)" >> "$GITHUB_OUTPUT" + # + # - name: Checkout homebrew-tap repository + # uses: actions/checkout@v4 + # with: + # repository: TeamWheelWizard/homebrew-tap + # token: ${{ secrets.TAP_TOKEN }} + # path: homebrew-tap + # + # - name: Update cask with real SHA256 values + # run: | + # VERSION="${GITHUB_REF_NAME#v}" + # CASK_FILE="homebrew-tap/Casks/wheelwizard.rb" + # + # # Replace version + # sed -i "s/version \".*\"/version \"$VERSION\"/" "$CASK_FILE" + # + # # Replace SHA256 placeholders with real values + # sed -i "s/REPLACE_ME_ARM64/${{ steps.shas.outputs.arm64 }}/" "$CASK_FILE" + # sed -i "s/REPLACE_ME_INTEL/${{ steps.shas.outputs.intel }}/" "$CASK_FILE" + # + # echo "=== Updated $CASK_FILE ===" + # cat "$CASK_FILE" + # + # - name: Commit and push to homebrew-tap + # run: | + # cd homebrew-tap + # git config user.name "github-actions[bot]" + # git config user.email "github-actions[bot]@users.noreply.github.com" + # git add Casks/wheelwizard.rb + # git commit -m "chore: update wheelwizard to v${GITHUB_REF_NAME#v}" + # git push diff --git a/.gitignore b/.gitignore index 1c87ff33..635b27bb 100644 --- a/.gitignore +++ b/.gitignore @@ -458,6 +458,9 @@ Network Trash Folder Temporary Items .apdisk +# Build output directories +/release/ + # content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore # Windows thumbnail cache files Thumbs.db diff --git a/build-mac.sh b/build-mac.sh deleted file mode 100755 index 56aec8c6..00000000 --- a/build-mac.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -# https://avaloniaui.net/blog/the-definitive-guide-to-building-and-deploying-avalonia-applications-for-macos - -PROJECT_NAME="$1" -OUTPUT_DIR="./WheelWizard/bin/Release/compiled" - -build_for_arch() { - local arch=$1 - echo "Building for $arch..." - dotnet publish -r osx-$arch -c Release /p:PublishSingleFile=true \ - /p:IncludeAllContentForSelfExtract=true /p:IncludeNativeLibrariesForSelfExtract=true \ - /p:EnableCompressionInSingleFile=true /p:PublishReadyToRun=true \ - -p:UseAppHost=true --self-contained true -o "$OUTPUT_DIR/osx-$arch" -} - -build_for_arch "x64" -build_for_arch "arm64" - -echo "Creating universal binary..." -mkdir -p "$OUTPUT_DIR/Universal" -lipo -create \ - "$OUTPUT_DIR/osx-x64/WheelWizard" \ - "$OUTPUT_DIR/osx-arm64/WheelWizard" \ - -output "$OUTPUT_DIR/Universal/WheelWizard" - -echo "Universal binary created" - diff --git a/macos/MacAppTemplate/Contents/CodeResources b/macos/MacAppTemplate/Contents/CodeResources new file mode 100644 index 00000000..6e9f2841 Binary files /dev/null and b/macos/MacAppTemplate/Contents/CodeResources differ diff --git a/macos/MacAppTemplate/Contents/Info.plist b/macos/MacAppTemplate/Contents/Info.plist new file mode 100644 index 00000000..75cd7963 --- /dev/null +++ b/macos/MacAppTemplate/Contents/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleName + WheelWizard + CFBundleIdentifier + ga.gabema.WheelWizard + CFBundleShortVersionString + 2.5.1 + CFBundleVersion + 2.5.1 + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleExecutable + WheelWizard + CFBundleIconFile + WheelWizard.icns + + diff --git a/macos/MacAppTemplate/Contents/Resources/WheelWizard.icns b/macos/MacAppTemplate/Contents/Resources/WheelWizard.icns new file mode 100644 index 00000000..31fbafb2 Binary files /dev/null and b/macos/MacAppTemplate/Contents/Resources/WheelWizard.icns differ diff --git a/macos/backgr.png b/macos/backgr.png new file mode 100644 index 00000000..bbdf116f Binary files /dev/null and b/macos/backgr.png differ diff --git a/macos/certs/WheelWizardEntitlements.entitlements b/macos/certs/WheelWizardEntitlements.entitlements new file mode 100644 index 00000000..0546d84b --- /dev/null +++ b/macos/certs/WheelWizardEntitlements.entitlements @@ -0,0 +1,24 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.automation.apple-events + + + com.apple.security.files.user-selected.read-write + + com.apple.security.files.downloads.read-write + + + com.apple.security.temporary-exception.files.absolute-path.read-write + + /Users/ + + + \ No newline at end of file diff --git a/macos/release-macos.sh b/macos/release-macos.sh new file mode 100755 index 00000000..6bc4b049 --- /dev/null +++ b/macos/release-macos.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# ============================================================================= +# WheelWizard macOS Build Script +# ============================================================================= +# Builds WheelWizard for macOS and creates a .app bundle. +# Designed to run on macOS CI runners (GitHub Actions). +# +# Environment variables: +# BUILD_ARCH - "arm64" or "x64" (default: auto-detect) +# SKIP_BUILD - Set to "true" to skip dotnet build +# OUTPUT_DIR - Output directory (default: ./release) +# +# No codesigning, no notarization, no DMG creation. +# DMG is created by the GitHub Actions workflow using create-dmg action. +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MACOS_DIR="$SCRIPT_DIR" +WW_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MAC_DIRS="$MACOS_DIR/MacAppTemplate" +DEFAULT_OUTPUT="$WW_DIR/release" + +# ---- Detect host architecture ---- +HOST_ARCH="$(uname -m)" +case "$HOST_ARCH" in + arm64|aarch64) HOST_BUILD_ARCH="arm64" ;; + x86_64|amd64) HOST_BUILD_ARCH="x64" ;; + *) HOST_BUILD_ARCH="x64" ;; +esac + +# Use BUILD_ARCH if set, otherwise default to host architecture +BUILD_ARCH="${BUILD_ARCH:-$HOST_BUILD_ARCH}" +RID="osx-$BUILD_ARCH" +OUTPUT_DIR="${OUTPUT_DIR:-$DEFAULT_OUTPUT}" + +echo "[INFO] Host architecture: $HOST_BUILD_ARCH" +echo "[INFO] Building for RID: $RID (arch: $BUILD_ARCH)" +echo "[INFO] Output: $OUTPUT_DIR" + +# If cross-compiling (e.g., building x64 on arm64 host), set the appropriate architecture flag +if [ "$BUILD_ARCH" != "$HOST_BUILD_ARCH" ]; then + echo "[INFO] Cross-compiling: $HOST_BUILD_ARCH -> $BUILD_ARCH" + case "$BUILD_ARCH" in + x64) ARCH_FLAG="-arch x86_64" ;; + arm64) ARCH_FLAG="-arch arm64" ;; + esac +else + ARCH_FLAG="" +fi + +mkdir -p "$OUTPUT_DIR" + +# ============================================================================= +# STEP 1: Build +# ============================================================================= +if [ "${SKIP_BUILD:-}" != "true" ]; then + echo "[INFO] Building WheelWizard for $RID..." + cd "$WW_DIR" + dotnet publish -r "$RID" -c Release-macOS \ + /p:PublishSingleFile=true \ + /p:IncludeAllContentForSelfExtract=true \ + /p:IncludeNativeLibrariesForSelfExtract=true \ + /p:EnableCompressionInSingleFile=true \ + /p:PublishReadyToRun=true \ + -p:UseAppHost=true \ + --self-contained true \ + -o "$OUTPUT_DIR/compiled/$RID" + cd "$SCRIPT_DIR" +else + echo "[INFO] Skipping build (SKIP_BUILD=true)" +fi + +EXE_DIR="$OUTPUT_DIR/compiled/$RID" +if [ ! -f "$EXE_DIR/WheelWizard" ]; then + echo "[ERROR] Built binary not found at $EXE_DIR/WheelWizard" + echo " Make sure the build step succeeded or set SKIP_BUILD=true if pre-built." + exit 1 +fi + +# ============================================================================= +# STEP 2: Create .app bundle +# ============================================================================= +APP_BUNDLE="$OUTPUT_DIR/WheelWizard.app" +echo "[INFO] Creating .app bundle at $APP_BUNDLE" + +# Clean any previous bundle +rm -rf "$APP_BUNDLE" + +# Copy the .app template +if [ -d "$MAC_DIRS" ]; then + cp -R "$MAC_DIRS" "$APP_BUNDLE" +else + echo "[ERROR] Template directory not found: $MAC_DIRS" + echo " The MacAppTemplate directory is required for the .app bundle structure." + exit 1 +fi + +# Place the binary +mkdir -p "$APP_BUNDLE/Contents/MacOS" +cp "$EXE_DIR/WheelWizard" "$APP_BUNDLE/Contents/MacOS/WheelWizard" + +# Copy the icon if present +if [ -f "$MAC_DIRS/Contents/Resources/WheelWizard.icns" ]; then + mkdir -p "$APP_BUNDLE/Contents/Resources" + cp "$MAC_DIRS/Contents/Resources/WheelWizard.icns" "$APP_BUNDLE/Contents/Resources/WheelWizard.icns" +fi + +# Set the bundle version from the release tag (e.g. "v2.5.1" -> "2.5.1"), +# falling back to the version declared in the project file. +if [ -n "${GITHUB_REF_NAME:-}" ]; then + BUNDLE_VERSION="${GITHUB_REF_NAME#v}" +else + BUNDLE_VERSION="$(sed -n 's/.*\(.*\)<\/Version>.*/\1/p' "$WW_DIR/WheelWizard/WheelWizard.csproj" | head -n1)" +fi +if [ -z "$BUNDLE_VERSION" ]; then + echo "[WARN] Could not determine version; leaving Info.plist version unchanged." +else + echo "[INFO] Setting bundle version to $BUNDLE_VERSION" + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUNDLE_VERSION" "$APP_BUNDLE/Contents/Info.plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $BUNDLE_VERSION" "$APP_BUNDLE/Contents/Info.plist" +fi + +echo "[INFO] .app bundle created successfully" + +# ============================================================================= +# STEP 3: Cleanup compiled artifacts +# ============================================================================= +echo "[INFO] Cleaning up intermediate build artifacts..." +rm -rf "$OUTPUT_DIR/compiled" + +echo "" +echo "============================================" +echo " ✅ Build complete!" +echo " .app bundle: $APP_BUNDLE" +echo "============================================"