Repository navigation
feat(macos): ship Bridge as a .app in a styled DMG; fix the updater for real release archives #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,6 +72,15 @@ jobs: | |
| macos: | ||
| name: macOS universal | ||
| runs-on: macos-14 | ||
| # Signing and notarization are automatic when these secrets exist; | ||
| # without them the app is ad-hoc signed. | ||
| env: | ||
| MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }} | ||
| MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | ||
| MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} | ||
| APPLE_ID: ${{ secrets.APPLE_ID }} | ||
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | ||
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: dtolnay/rust-toolchain@stable | ||
|
|
@@ -105,7 +114,7 @@ jobs: | |
| "node-$NATIVE_HID_NODE_VERSION-darwin-x64/bin/node" \ | ||
| -output native-hid/node/bin/node | ||
| chmod +x native-hid/node/bin/node | ||
| - name: Package universal binary | ||
| - name: Package universal binary (existing bare-binary installs) | ||
| run: | | ||
| lipo -create \ | ||
| target/aarch64-apple-darwin/release/openmouse-bridge \ | ||
|
|
@@ -115,12 +124,62 @@ jobs: | |
| cp assets/fonts/DM-Sans-OFL.txt ./DM-Sans-OFL.txt | ||
| zip -r openmouse-bridge-macos-universal.zip openmouse-bridge README.md DM-Sans-OFL.txt native-hid | ||
| shasum -a 256 openmouse-bridge-macos-universal.zip > openmouse-bridge-macos-universal.zip.sha256 | ||
| - name: Import signing certificate | ||
| if: env.MACOS_CERTIFICATE_BASE64 != '' | ||
| run: | | ||
| set -euo pipefail | ||
| keychain="$RUNNER_TEMP/openmouse-signing.keychain-db" | ||
| keychain_password="$(uuidgen)" | ||
| echo "$MACOS_CERTIFICATE_BASE64" | base64 --decode > "$RUNNER_TEMP/certificate.p12" | ||
| security create-keychain -p "$keychain_password" "$keychain" | ||
| security set-keychain-settings -lut 21600 "$keychain" | ||
| security unlock-keychain -p "$keychain_password" "$keychain" | ||
| security import "$RUNNER_TEMP/certificate.p12" -P "$MACOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain" | ||
| security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" | ||
| security list-keychains -d user -s "$keychain" login.keychain-db | ||
| echo "MACOS_SIGN_IDENTITY=$MACOS_SIGN_IDENTITY" >> "$GITHUB_ENV" | ||
| - name: Build OpenMouse Bridge.app | ||
| run: | | ||
| version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)" | ||
| mkdir -p dist | ||
| packaging/macos/build-app.sh openmouse-bridge native-hid "$version" dist | ||
| - name: Notarize app | ||
| if: env.MACOS_CERTIFICATE_BASE64 != '' && env.APPLE_ID != '' | ||
| run: | | ||
| set -euo pipefail | ||
| ditto -c -k --keepParent "dist/OpenMouse Bridge.app" notarize-app.zip | ||
| xcrun notarytool submit notarize-app.zip --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_PASSWORD" --wait | ||
| xcrun stapler staple "dist/OpenMouse Bridge.app" | ||
| - name: Package DMG and app update archive | ||
| run: | | ||
| set -euo pipefail | ||
| python3 -m pip install --quiet dmgbuild | ||
| packaging/macos/build-dmg.sh "dist/OpenMouse Bridge.app" OpenMouse-Bridge-macos-universal.dmg | ||
| # Plain Info-ZIP (not ditto): the updater's strict extractor expects ordinary zip entries. | ||
| (cd dist && zip -r -X -q "../openmouse-bridge-macos-app.zip" "OpenMouse Bridge.app") | ||
| shasum -a 256 openmouse-bridge-macos-app.zip > openmouse-bridge-macos-app.zip.sha256 | ||
| - name: Notarize DMG | ||
| if: env.MACOS_CERTIFICATE_BASE64 != '' && env.APPLE_ID != '' | ||
| run: | | ||
| set -euo pipefail | ||
| xcrun notarytool submit OpenMouse-Bridge-macos-universal.dmg --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_PASSWORD" --wait | ||
| xcrun stapler staple OpenMouse-Bridge-macos-universal.dmg | ||
| - name: Verify DMG | ||
| run: | | ||
| set -euo pipefail | ||
| hdiutil attach OpenMouse-Bridge-macos-universal.dmg -nobrowse -readonly -mountpoint "$RUNNER_TEMP/dmg" | ||
| test -d "$RUNNER_TEMP/dmg/OpenMouse Bridge.app" | ||
| codesign --verify --deep --strict "$RUNNER_TEMP/dmg/OpenMouse Bridge.app" | ||
| hdiutil detach "$RUNNER_TEMP/dmg" | ||
| - uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: bridge-macos | ||
| path: | | ||
| openmouse-bridge-macos-universal.zip | ||
| openmouse-bridge-macos-universal.zip.sha256 | ||
| openmouse-bridge-macos-app.zip | ||
| openmouse-bridge-macos-app.zip.sha256 | ||
| OpenMouse-Bridge-macos-universal.dmg | ||
|
Comment on lines
+180
to
+182
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 Supported by static analysis
Document the ad-hoc macOS fallback. The workflow intentionally publishes the DMG when signing secrets are absent. It does not fail or warn; it builds an ad-hoc-signed, non-notarized app. The README documents only the signed path, so users are not told that macOS may require a manual Gatekeeper override. Suggested documentation
|
||
| linux: | ||
| name: Linux x64 | ||
| runs-on: ubuntu-24.04 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| /target | ||
| native-hid/node_modules | ||
| .DS_Store | ||
| __pycache__/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -247,6 +247,15 @@ cargo test | |
| cargo clippy --all-targets -- -D warnings | ||
| ``` | ||
|
|
||
| ## Install on macOS | ||
|
|
||
| Download `OpenMouse-Bridge-macos-universal.dmg` from the latest release, open | ||
| it, and drag OpenMouse Bridge to Applications. It runs as a menu bar app with | ||
| no Terminal window. `packaging/macos/` holds the scripts that build the app | ||
| bundle and DMG; the release workflow signs and notarizes them when the | ||
| `MACOS_CERTIFICATE_BASE64`, `MACOS_CERTIFICATE_PASSWORD`, `MACOS_SIGN_IDENTITY`, | ||
| `APPLE_ID`, `APPLE_TEAM_ID` and `APPLE_APP_PASSWORD` secrets are set. | ||
|
Comment on lines
+252
to
+257
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Document the one-time upgrade for existing macOS installs. If a user installed an earlier release, the old updater cannot install this app-bundle release automatically. Add the required manual-install step here so existing users know how to move to the DMG. The PR objectives explicitly identify this migration requirement.
|
||
|
|
||
| ## Install on Linux | ||
|
|
||
| One-line install from the latest stable release (detects missing system | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| <?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>CFBundleDevelopmentRegion</key> | ||
| <string>en</string> | ||
| <key>CFBundleExecutable</key> | ||
| <string>openmouse-bridge</string> | ||
| <key>CFBundleIconFile</key> | ||
| <string>OpenMouse</string> | ||
| <key>CFBundleIdentifier</key> | ||
| <string>io.OpenMouse.OpenMouse-Bridge</string> | ||
| <key>CFBundleInfoDictionaryVersion</key> | ||
| <string>6.0</string> | ||
| <key>CFBundleName</key> | ||
| <string>OpenMouse Bridge</string> | ||
| <key>CFBundleDisplayName</key> | ||
| <string>OpenMouse Bridge</string> | ||
| <key>CFBundlePackageType</key> | ||
| <string>APPL</string> | ||
| <key>CFBundleShortVersionString</key> | ||
| <string>@VERSION@</string> | ||
| <key>CFBundleVersion</key> | ||
| <string>@BUILD@</string> | ||
| <key>LSMinimumSystemVersion</key> | ||
| <string>11.0</string> | ||
| <key>LSUIElement</key> | ||
| <true/> | ||
| <key>NSHighResolutionCapable</key> | ||
| <true/> | ||
| <key>NSHumanReadableCopyright</key> | ||
| <string>MIT License</string> | ||
| </dict> | ||
| </plist> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| #!/bin/bash | ||
| # Assembles "OpenMouse Bridge.app" from a built Bridge binary and a prepared | ||
| # native-hid directory (deps + bundled Node.js, see release.yml). | ||
| # | ||
| # build-app.sh <binary> <native-hid-dir> <version> <out-dir> | ||
| # | ||
| # Signs ad-hoc by default. Set MACOS_SIGN_IDENTITY to a Developer ID | ||
| # Application identity for a distributable (notarizable) build. | ||
| set -euo pipefail | ||
|
|
||
| binary="$1" | ||
| native_hid="$2" | ||
| version="$3" | ||
| out_dir="$4" | ||
| identity="${MACOS_SIGN_IDENTITY:--}" | ||
|
|
||
| here="$(cd "$(dirname "$0")" && pwd)" | ||
| repo="$(cd "$here/../.." && pwd)" | ||
| app="$out_dir/OpenMouse Bridge.app" | ||
|
|
||
| rm -rf "$app" | ||
| mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" | ||
|
|
||
| cp "$binary" "$app/Contents/MacOS/openmouse-bridge" | ||
| chmod +x "$app/Contents/MacOS/openmouse-bridge" | ||
|
|
||
| ditto "$native_hid" "$app/Contents/Resources/native-hid" | ||
| # Bridge's updater refuses packages that omit the bundled font's license. | ||
| cp "$repo/assets/fonts/DM-Sans-OFL.txt" "$app/Contents/Resources/DM-Sans-OFL.txt" | ||
| # npm's .bin entries are symlinks, which the update zip cannot carry. | ||
| find "$app/Contents/Resources/native-hid" -name .bin -type d -prune -exec rm -rf {} + | ||
| if [ -n "$(find "$app" -type l)" ]; then | ||
| echo "bundle contains symlinks:" >&2 | ||
| find "$app" -type l >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # 1.0.0-beta.19 -> short version as-is, build number numeric. | ||
| build="${version%%-*}" | ||
| sed -e "s/@VERSION@/$version/" -e "s/@BUILD@/$build/" \ | ||
| "$here/Info.plist" > "$app/Contents/Info.plist" | ||
|
|
||
| iconset="$(mktemp -d)/OpenMouse.iconset" | ||
| mkdir -p "$iconset" | ||
| icon="$here/icon-1024.png" # regenerate with make-assets.py | ||
| for size in 16 32 128 256 512; do | ||
| sips -z "$size" "$size" "$icon" --out "$iconset/icon_${size}x${size}.png" >/dev/null | ||
| double=$((size * 2)) | ||
| sips -z "$double" "$double" "$icon" --out "$iconset/icon_${size}x${size}@2x.png" >/dev/null | ||
| done | ||
| iconutil -c icns "$iconset" -o "$app/Contents/Resources/OpenMouse.icns" | ||
|
|
||
| sign_flags=(--force --sign "$identity" --entitlements "$here/entitlements.plist") | ||
| if [ "$identity" != "-" ]; then | ||
| sign_flags+=(--options runtime --timestamp) | ||
| fi | ||
|
|
||
| # Inside-out: nested Mach-O (Node, node-hid addon) first, then the app. | ||
| while IFS= read -r -d '' file; do | ||
| if file -b "$file" | grep -q "Mach-O"; then | ||
| codesign "${sign_flags[@]}" "$file" | ||
| fi | ||
| done < <(find "$app/Contents/Resources" -type f -print0) | ||
| codesign "${sign_flags[@]}" "$app" | ||
|
|
||
| codesign --verify --deep --strict "$app" | ||
| echo "$app" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| #!/bin/bash | ||
| # Wraps the app in a styled drag-to-Applications disk image. Needs dmgbuild | ||
| # (pip install dmgbuild). | ||
| # | ||
| # build-dmg.sh <app> <output.dmg> | ||
| set -euo pipefail | ||
|
|
||
| app="$1" | ||
| dmg="$2" | ||
| identity="${MACOS_SIGN_IDENTITY:--}" | ||
| here="$(cd "$(dirname "$0")" && pwd)" | ||
|
|
||
| rm -f "$dmg" | ||
| dmgbuild -s "$here/dmg-settings.py" \ | ||
| -D app="$app" \ | ||
| -D icon="$app/Contents/Resources/OpenMouse.icns" \ | ||
| -D background="$here/dmg-background.tiff" \ | ||
| "OpenMouse Bridge" "$dmg" | ||
|
|
||
| if [ "$identity" != "-" ]; then | ||
| codesign --force --sign "$identity" --timestamp "$dmg" | ||
| fi | ||
| echo "$dmg" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # dmgbuild settings for the installer window; see build-dmg.sh. | ||
| # defines: app (path to the .app), icon (volume icon .icns), background (.tiff) | ||
| import os | ||
|
|
||
| app = defines["app"] | ||
| app_name = os.path.basename(app) | ||
|
|
||
| format = "UDZO" | ||
| files = [app] | ||
| symlinks = {"Applications": "/Applications"} | ||
| background = defines["background"] | ||
| icon = defines["icon"] | ||
|
|
||
| show_status_bar = False | ||
| show_tab_view = False | ||
| show_toolbar = False | ||
| show_pathbar = False | ||
| show_sidebar = False | ||
| default_view = "icon-view" | ||
|
|
||
| # Frame = 32pt title bar + 400pt content + 28pt path bar (bg is 660x428 so it | ||
| # also fills the frame when the path bar is hidden). Icon y matches make-assets.py. | ||
| window_rect = ((200, 120), (660, 460)) | ||
| icon_size = 112 | ||
| text_size = 13 | ||
| arrange_by = None | ||
| icon_locations = {app_name: (170, 185), "Applications": (490, 185)} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?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> | ||
| <!-- The bundled Node.js runtime needs V8's JIT, and loads node-hid's | ||
| prebuilt addon, which carries a different signature than the host. --> | ||
| <key>com.apple.security.cs.allow-jit</key> | ||
| <true/> | ||
| <key>com.apple.security.cs.allow-unsigned-executable-memory</key> | ||
| <true/> | ||
| <key>com.apple.security.cs.disable-library-validation</key> | ||
| <true/> | ||
| </dict> | ||
| </plist> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 High
workflows/release.yml:156The macOS release job aborts before packaging artifacts because
python3 -m pip install dmgbuildinstalls into the externally managed base Python onmacos-14. Installdmgbuildin a virtual environment and add that environment’sbindirectory toPATHbefore runningbuild-dmg.sh.🚀 Reply "fix it for me" or copy this AI Prompt for your agent: