mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
feat(apple): add direct-download macOS channel (notarized DMG + Sparkle + Homebrew)
Add a second macOS shipping channel alongside the App Store build: - New VniDropDirect target (Release-Direct config) sharing VniDrop's sources via an AppBase target template; links Sparkle behind the DIRECT_DISTRIBUTION flag so the App Store binary never bundles a self-updater. arm64-only (core is arm64). - Sparkle updater (SparkleUpdater.swift) + "Check for Updates" menu, compiled only under DIRECT_DISTRIBUTION; Info.plist SUFeedURL points at the GitHub Release /latest/download/appcast.xml, non-sandboxed entitlements for Developer ID. - build-dmg.sh (archive → Developer ID export → DMG → sign → notarize → staple), generate-appcast.sh, and ExportOptions-DeveloperID.plist. - apple-release.yml: on tag v*.*.*, build/notarize the DMG, publish the GitHub Release with appcast, and push the Homebrew cask to sudosylabs/homebrew-vnidrop. apple.yml gains a PR compile-check of the direct target. - CFBundleVersion is stamped at build time as a UTC YYMMDD.HHMM timestamp for both channels, replacing the hand-maintained build number. - Docs (RELEASE-MACOS.md, README), cask template + tap README, localized updates_check string, Makefile targets, gitignore for dist/ artifacts.
This commit is contained in:
230
.github/workflows/apple-release.yml
vendored
Normal file
230
.github/workflows/apple-release.yml
vendored
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
name: Apple release (macOS DMG)
|
||||||
|
|
||||||
|
# Builds, signs, notarizes, and publishes the direct-download macOS build:
|
||||||
|
# - a Developer ID–signed, notarized VniDrop-<version>.dmg,
|
||||||
|
# - a Sparkle appcast.xml (both attached to the GitHub Release), and
|
||||||
|
# - an updated Homebrew cask pushed to the sudosylabs/homebrew-vnidrop tap.
|
||||||
|
#
|
||||||
|
# The App Store / TestFlight build is NOT produced here — that goes through Xcode
|
||||||
|
# Organizer / App Store Connect. This workflow only covers direct distribution.
|
||||||
|
#
|
||||||
|
# Trigger: push a tag vMAJOR.MINOR.PATCH (must point at a commit on master), or
|
||||||
|
# run manually with an explicit version (produces artifacts, no Release).
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: Release version in MAJOR.MINOR.PATCH form
|
||||||
|
required: true
|
||||||
|
default: "0.1.0"
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: apple-release-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build & notarize DMG
|
||||||
|
runs-on: macos-latest
|
||||||
|
timeout-minutes: 90
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.app }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Verify tag is on master
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
run: |
|
||||||
|
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then
|
||||||
|
echo "Release tags must point to a commit on master" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Resolve version
|
||||||
|
id: version
|
||||||
|
env:
|
||||||
|
REQUESTED_VERSION: ${{ inputs.version || '' }}
|
||||||
|
run: |
|
||||||
|
if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
|
||||||
|
version="${GITHUB_REF_NAME#v}"
|
||||||
|
else
|
||||||
|
version="$REQUESTED_VERSION"
|
||||||
|
fi
|
||||||
|
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "bad version '$version'" >&2; exit 1; }
|
||||||
|
echo "app=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Select Xcode
|
||||||
|
run: sudo xcode-select -s /Applications/Xcode.app
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
targets: aarch64-apple-darwin
|
||||||
|
|
||||||
|
- name: Cache Cargo
|
||||||
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: apple-release-cargo-${{ hashFiles('Cargo.lock') }}
|
||||||
|
restore-keys: apple-release-cargo-
|
||||||
|
|
||||||
|
- name: Install tooling
|
||||||
|
run: brew install xcodegen swiftlint create-dmg
|
||||||
|
|
||||||
|
- name: Install Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
- name: Download Sparkle tools
|
||||||
|
# generate_appcast + sign_update ship in the Sparkle release tarball.
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ver="2.9.4"
|
||||||
|
curl -fsSL -o /tmp/sparkle.tar.xz \
|
||||||
|
"https://github.com/sparkle-project/Sparkle/releases/download/${ver}/Sparkle-${ver}.tar.xz"
|
||||||
|
mkdir -p /tmp/sparkle && tar -xJf /tmp/sparkle.tar.xz -C /tmp/sparkle
|
||||||
|
echo "SPARKLE_BIN=/tmp/sparkle/bin" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Import Developer ID certificate
|
||||||
|
env:
|
||||||
|
CERT_P12_BASE64: ${{ secrets.DEVELOPER_ID_CERT_P12 }}
|
||||||
|
CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
keychain="$RUNNER_TEMP/signing.keychain-db"
|
||||||
|
kpw="$(openssl rand -hex 20)"
|
||||||
|
security create-keychain -p "$kpw" "$keychain"
|
||||||
|
security set-keychain-settings -lut 21600 "$keychain"
|
||||||
|
security unlock-keychain -p "$kpw" "$keychain"
|
||||||
|
echo "$CERT_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
|
||||||
|
security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$CERT_PASSWORD" \
|
||||||
|
-T /usr/bin/codesign -T /usr/bin/security
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$kpw" "$keychain"
|
||||||
|
# Prepend our keychain so codesign/xcodebuild can find the identity.
|
||||||
|
security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"')
|
||||||
|
rm -f "$RUNNER_TEMP/cert.p12"
|
||||||
|
|
||||||
|
- name: Store notarytool credentials
|
||||||
|
env:
|
||||||
|
NOTARY_KEY_P8: ${{ secrets.NOTARY_API_KEY }}
|
||||||
|
NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }}
|
||||||
|
NOTARY_ISSUER: ${{ secrets.NOTARY_ISSUER }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "$NOTARY_KEY_P8" | base64 --decode > "$RUNNER_TEMP/notary.p8"
|
||||||
|
xcrun notarytool store-credentials vnidrop-notary \
|
||||||
|
--key "$RUNNER_TEMP/notary.p8" \
|
||||||
|
--key-id "$NOTARY_KEY_ID" \
|
||||||
|
--issuer "$NOTARY_ISSUER"
|
||||||
|
echo "NOTARY_PROFILE=vnidrop-notary" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Write Sparkle signing key
|
||||||
|
env:
|
||||||
|
SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
printf '%s' "$SPARKLE_ED_PRIVATE_KEY" > "$RUNNER_TEMP/sparkle_ed_private_key"
|
||||||
|
echo "SPARKLE_ED_KEY_FILE=$RUNNER_TEMP/sparkle_ed_private_key" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Build, sign & notarize DMG
|
||||||
|
run: apple/scripts/build-dmg.sh "${{ steps.version.outputs.app }}"
|
||||||
|
|
||||||
|
- name: Generate appcast
|
||||||
|
env:
|
||||||
|
RELEASE_REPO: ${{ github.repository }}
|
||||||
|
run: apple/scripts/generate-appcast.sh "${{ steps.version.outputs.app }}"
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: vnidrop-${{ steps.version.outputs.app }}-macos-dmg
|
||||||
|
path: |
|
||||||
|
apple/dist/VniDrop-*.dmg
|
||||||
|
apple/dist/appcast.xml
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
- name: Publish GitHub Release
|
||||||
|
if: github.event_name == 'push' && github.ref_type == 'tag'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
tag="$GITHUB_REF_NAME"
|
||||||
|
version="${tag#v}"
|
||||||
|
if gh release view "$tag" >/dev/null 2>&1; then
|
||||||
|
echo "Release $tag already exists; refusing to replace assets" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
gh release create "$tag" \
|
||||||
|
"apple/dist/VniDrop-${version}.dmg" \
|
||||||
|
"apple/dist/appcast.xml" \
|
||||||
|
--verify-tag \
|
||||||
|
--title "VniDrop $version" \
|
||||||
|
--generate-notes
|
||||||
|
|
||||||
|
update-cask:
|
||||||
|
name: Update Homebrew cask
|
||||||
|
needs: build
|
||||||
|
if: github.event_name == 'push' && github.ref_type == 'tag'
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Download DMG artifact
|
||||||
|
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||||
|
with:
|
||||||
|
name: vnidrop-${{ needs.build.outputs.version }}-macos-dmg
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Render cask
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.build.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sha="$(sha256sum "dist/VniDrop-${VERSION}.dmg" | cut -d' ' -f1)"
|
||||||
|
sed -e "s/^ version \".*\"/ version \"${VERSION}\"/" \
|
||||||
|
-e "s/^ sha256 \".*\"/ sha256 \"${sha}\"/" \
|
||||||
|
packaging/homebrew/vnidrop.rb > /tmp/vnidrop.rb
|
||||||
|
echo "Rendered cask:"; cat /tmp/vnidrop.rb
|
||||||
|
|
||||||
|
- name: Push to tap
|
||||||
|
env:
|
||||||
|
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||||
|
VERSION: ${{ needs.build.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git clone "https://x-access-token:${TAP_TOKEN}@github.com/sudosylabs/homebrew-vnidrop.git" tap
|
||||||
|
mkdir -p tap/Casks
|
||||||
|
cp /tmp/vnidrop.rb tap/Casks/vnidrop.rb
|
||||||
|
cd tap
|
||||||
|
git config user.name "vnidrop-release-bot"
|
||||||
|
git config user.email "release-bot@users.noreply.github.com"
|
||||||
|
git add Casks/vnidrop.rb
|
||||||
|
git commit -m "vnidrop ${VERSION}" || { echo "no cask changes"; exit 0; }
|
||||||
|
git push
|
||||||
5
.github/workflows/apple.yml
vendored
5
.github/workflows/apple.yml
vendored
@@ -76,3 +76,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and test Apple app
|
- name: Build and test Apple app
|
||||||
run: make check-apple
|
run: make check-apple
|
||||||
|
|
||||||
|
- name: Build direct-download macOS target (Sparkle, unsigned)
|
||||||
|
# Keeps the VniDropDirect (.dmg/Sparkle) target compiling; signing and
|
||||||
|
# notarization happen only in apple-release.yml on a tag.
|
||||||
|
run: make build-apple-macos-direct
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -122,6 +122,12 @@ open-apple-project: apple-project ## Generate and open the native Apple Xcode pr
|
|||||||
build-apple-macos: apple-project ## Build the native macOS app (unsigned by default).
|
build-apple-macos: apple-project ## Build the native macOS app (unsigned by default).
|
||||||
cd $(ROOT)/apple && $(XCODEBUILD) -project VniDrop.xcodeproj -scheme VniDrop -configuration $(APPLE_CONFIGURATION) -derivedDataPath "$(APPLE_DERIVED_DATA)" -destination 'platform=macOS' CODE_SIGNING_ALLOWED=$(APPLE_CODE_SIGNING) CODE_SIGNING_REQUIRED=$(APPLE_CODE_SIGNING) build
|
cd $(ROOT)/apple && $(XCODEBUILD) -project VniDrop.xcodeproj -scheme VniDrop -configuration $(APPLE_CONFIGURATION) -derivedDataPath "$(APPLE_DERIVED_DATA)" -destination 'platform=macOS' CODE_SIGNING_ALLOWED=$(APPLE_CODE_SIGNING) CODE_SIGNING_REQUIRED=$(APPLE_CODE_SIGNING) build
|
||||||
|
|
||||||
|
build-apple-macos-direct: apple-project ## Build the direct-download macOS target (Sparkle, unsigned) — CI compile check.
|
||||||
|
cd $(ROOT)/apple && $(XCODEBUILD) -project VniDrop.xcodeproj -scheme VniDropDirect -configuration Release-Direct -derivedDataPath "$(APPLE_DERIVED_DATA)" -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO build
|
||||||
|
|
||||||
|
build-apple-dmg: ## Build the signed/notarized direct-download .dmg (see apple/RELEASE-MACOS.md for required env).
|
||||||
|
cd $(ROOT) && apple/scripts/build-dmg.sh $(VERSION)
|
||||||
|
|
||||||
open-apple: build-apple-macos ## Build and launch the native macOS app.
|
open-apple: build-apple-macos ## Build and launch the native macOS app.
|
||||||
@test -d "$(APPLE_DERIVED_DATA)/Build/Products/$(APPLE_CONFIGURATION)/VniDrop.app" || { printf 'Built macOS app was not found.\n' >&2; exit 1; }
|
@test -d "$(APPLE_DERIVED_DATA)/Build/Products/$(APPLE_CONFIGURATION)/VniDrop.app" || { printf 'Built macOS app was not found.\n' >&2; exit 1; }
|
||||||
$(OPEN) "$(APPLE_DERIVED_DATA)/Build/Products/$(APPLE_CONFIGURATION)/VniDrop.app"
|
$(OPEN) "$(APPLE_DERIVED_DATA)/Build/Products/$(APPLE_CONFIGURATION)/VniDrop.app"
|
||||||
|
|||||||
4
apple/.gitignore
vendored
4
apple/.gitignore
vendored
@@ -18,3 +18,7 @@ Local.xcconfig
|
|||||||
.swiftpm/
|
.swiftpm/
|
||||||
DerivedData/
|
DerivedData/
|
||||||
*.xcuserstate
|
*.xcuserstate
|
||||||
|
|
||||||
|
# Direct-download (.dmg) build outputs — apple/scripts/build-dmg.sh
|
||||||
|
.build-dmg/
|
||||||
|
dist/
|
||||||
|
|||||||
@@ -34,12 +34,30 @@ Prerequisites: Xcode, Rust with the Apple targets
|
|||||||
make apple-core # Rust core, Swift bindings, and XCFramework
|
make apple-core # Rust core, Swift bindings, and XCFramework
|
||||||
make apple-project # generate apple/VniDrop.xcodeproj
|
make apple-project # generate apple/VniDrop.xcodeproj
|
||||||
make open-apple-project # generate and open the project in Xcode
|
make open-apple-project # generate and open the project in Xcode
|
||||||
make build-apple-macos # unsigned macOS build
|
make build-apple-macos # unsigned macOS build (App Store target)
|
||||||
make open-apple # build and launch the macOS app
|
make open-apple # build and launch the macOS app
|
||||||
make build-apple-ios # unsigned iOS simulator build
|
make build-apple-ios # unsigned iOS simulator app
|
||||||
make check-apple # iOS simulator tests
|
make check-apple # iOS simulator tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### macOS shipping channels
|
||||||
|
|
||||||
|
The macOS app ships through two targets that build identical sources:
|
||||||
|
|
||||||
|
- **`VniDrop`** (`Release`) — Mac App Store / TestFlight. Sandboxed, no
|
||||||
|
self-updater.
|
||||||
|
- **`VniDropDirect`** (`Release-Direct`) — direct-download `.dmg` on GitHub
|
||||||
|
Releases + Homebrew cask. Adds the **Sparkle** auto-updater behind the
|
||||||
|
`DIRECT_DISTRIBUTION` compile flag, so the App Store binary never links Sparkle.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build-apple-macos-direct # unsigned compile-check of the direct target
|
||||||
|
make build-apple-dmg VERSION=x.y.z # signed (+ notarized) .dmg
|
||||||
|
```
|
||||||
|
|
||||||
|
Full signing, notarization, appcast, and cask flow: see
|
||||||
|
[`RELEASE-MACOS.md`](RELEASE-MACOS.md).
|
||||||
|
|
||||||
Use `APPLE_PROFILE=release` to request a release Rust core, or set
|
Use `APPLE_PROFILE=release` to request a release Rust core, or set
|
||||||
`APPLE_DESTINATION` to override the automatically selected iOS simulator.
|
`APPLE_DESTINATION` to override the automatically selected iOS simulator.
|
||||||
Code signing is disabled for the app and test targets; local and CI builds do
|
Code signing is disabled for the app and test targets; local and CI builds do
|
||||||
|
|||||||
155
apple/RELEASE-MACOS.md
Normal file
155
apple/RELEASE-MACOS.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Releasing VniDrop for macOS
|
||||||
|
|
||||||
|
VniDrop ships on macOS through **two independent channels**:
|
||||||
|
|
||||||
|
| Channel | Target / config | Signing | Updates |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Mac App Store / TestFlight** | `VniDrop` / `Release` | Apple Distribution | App Store |
|
||||||
|
| **Direct download (.dmg)** | `VniDropDirect` / `Release-Direct` | Developer ID + notarization | Sparkle |
|
||||||
|
|
||||||
|
The direct build adds the **Sparkle** auto-updater, gated behind the
|
||||||
|
`DIRECT_DISTRIBUTION` compile flag so the App Store binary never links or ships a
|
||||||
|
self-updater (which Apple forbids). Because Swift Package Manager links products
|
||||||
|
per *target*, the isolation is a dedicated `VniDropDirect` target — not just a
|
||||||
|
build configuration.
|
||||||
|
|
||||||
|
This document covers the **direct-download** channel. The App Store channel goes
|
||||||
|
through Xcode Organizer / App Store Connect as before (see
|
||||||
|
`RELEASE-TESTFLIGHT.fr.md`).
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Both channels use Apple's two-field convention, shown as `MARKETING_VERSION (build)`:
|
||||||
|
|
||||||
|
- **`CFBundleShortVersionString`** = `MARKETING_VERSION` — the human `X.Y.Z` version
|
||||||
|
(from the git tag for a release). This is what users, the DMG filename, the
|
||||||
|
GitHub Release, and the Homebrew cask all use.
|
||||||
|
- **`CFBundleVersion`** = a **UTC `YYMMDD.HHMM` timestamp**, stamped into the built
|
||||||
|
Info.plist by the "Stamp build number" build phase (`apple/project.yml`). It fires
|
||||||
|
for every build — Xcode Organizer archive and CLI alike — so each build is
|
||||||
|
monotonic and self-describing. Sparkle compares this field to order updates, and
|
||||||
|
App Store Connect requires each upload's build to exceed the previous one; a
|
||||||
|
timestamp satisfies both automatically. `build-dmg.sh` pins one timestamp per
|
||||||
|
archive (via `VNIDROP_BUILD`) so the app, DMG, and appcast agree.
|
||||||
|
|
||||||
|
No build number is maintained by hand.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## One-time setup
|
||||||
|
|
||||||
|
### 1. Developer ID Application certificate
|
||||||
|
|
||||||
|
Direct distribution requires a **Developer ID Application** certificate (distinct
|
||||||
|
from the *Apple Distribution* cert used for the App Store). In Xcode ▸ Settings ▸
|
||||||
|
Accounts ▸ Manage Certificates ▸ **+** ▸ *Developer ID Application*. Confirm it's
|
||||||
|
in the keychain:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
security find-identity -v -p codesigning | grep "Developer ID Application"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Notarization credentials (App Store Connect API key)
|
||||||
|
|
||||||
|
Create an API key at App Store Connect ▸ Users and Access ▸ Integrations ▸ Keys
|
||||||
|
(role: *Developer*). Download the `AuthKey_XXXX.p8`, and note the **Key ID** and
|
||||||
|
**Issuer ID**. Store a local notarytool profile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun notarytool store-credentials vnidrop-notary \
|
||||||
|
--key /path/to/AuthKey_XXXX.p8 \
|
||||||
|
--key-id <KEY_ID> \
|
||||||
|
--issuer <ISSUER_ID>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Sparkle EdDSA signing key
|
||||||
|
|
||||||
|
Generate the update-signing key once (private key stored in the login keychain):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sparkle's generate_keys, from the downloaded Sparkle release (bin/generate_keys)
|
||||||
|
./bin/generate_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
It prints the **public** key. Put it in `apple/VniDrop/Resources/Info.plist` as
|
||||||
|
`SUPublicEDKey`, replacing `REPLACE_WITH_SPARKLE_ED_PUBLIC_KEY`. Export the
|
||||||
|
**private** key for CI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/generate_keys -x sparkle_ed_private_key # writes the private key file
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Homebrew tap repo
|
||||||
|
|
||||||
|
Create an empty GitHub repo **`sudosylabs/homebrew-vnidrop`** (a `Casks/` folder
|
||||||
|
is enough). The release workflow pushes `Casks/vnidrop.rb` there. Users install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install --cask sudosylabs/vnidrop/vnidrop
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. CI secrets
|
||||||
|
|
||||||
|
Add these to the `sudosylabs/vnidrop` repo (Settings ▸ Secrets ▸ Actions):
|
||||||
|
|
||||||
|
| Secret | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| `DEVELOPER_ID_CERT_P12` | base64 of the exported Developer ID `.p12` |
|
||||||
|
| `DEVELOPER_ID_CERT_PASSWORD` | password for that `.p12` |
|
||||||
|
| `NOTARY_API_KEY` | base64 of `AuthKey_XXXX.p8` |
|
||||||
|
| `NOTARY_KEY_ID` | App Store Connect key ID |
|
||||||
|
| `NOTARY_ISSUER` | App Store Connect issuer ID |
|
||||||
|
| `SPARKLE_ED_PRIVATE_KEY` | contents of the exported Sparkle private key file |
|
||||||
|
| `HOMEBREW_TAP_TOKEN` | PAT with write access to `homebrew-vnidrop` |
|
||||||
|
|
||||||
|
> `SUFeedURL` in Info.plist points at
|
||||||
|
> `https://github.com/sudosylabs/vnidrop/releases/latest/download/appcast.xml`.
|
||||||
|
> GitHub's `/releases/latest/download/` path always redirects to the newest
|
||||||
|
> non-prerelease release's asset, so no GitHub Pages or repo commits are needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Per-release flow
|
||||||
|
|
||||||
|
1. Bump `MARKETING_VERSION` (and `CURRENT_PROJECT_VERSION`) in `apple/project.yml`
|
||||||
|
if needed, commit to `master`.
|
||||||
|
2. Tag and push:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v0.2.0
|
||||||
|
git push origin v0.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
3. `.github/workflows/apple-release.yml` then:
|
||||||
|
- builds the Rust core (release), regenerates the project,
|
||||||
|
- archives + exports the `VniDropDirect` target (Developer ID, hardened runtime),
|
||||||
|
- builds, signs, **notarizes and staples** `VniDrop-<version>.dmg`,
|
||||||
|
- runs `generate_appcast` to produce `appcast.xml` (enclosure → the release DMG),
|
||||||
|
- creates the GitHub Release with both assets, and
|
||||||
|
- renders + pushes the Homebrew cask to the tap.
|
||||||
|
|
||||||
|
Existing installs pick up the new version automatically via Sparkle (feed →
|
||||||
|
latest release's `appcast.xml`); `brew upgrade` respects `auto_updates true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building a DMG locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Signed DMG (skips notarization unless NOTARY_PROFILE is set):
|
||||||
|
make build-apple-dmg VERSION=0.2.0
|
||||||
|
|
||||||
|
# Full signed + notarized DMG:
|
||||||
|
NOTARY_PROFILE=vnidrop-notary make build-apple-dmg VERSION=0.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `apple/dist/VniDrop-<version>.dmg`. See `apple/scripts/build-dmg.sh` for
|
||||||
|
the environment variables (`DEVELOPER_ID_APP`, `DEVELOPMENT_TEAM`,
|
||||||
|
`NOTARY_PROFILE`). Generate the appcast with
|
||||||
|
`apple/scripts/generate-appcast.sh <version>`.
|
||||||
|
|
||||||
|
To compile-check the direct target without signing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build-apple-macos-direct
|
||||||
|
```
|
||||||
@@ -8,6 +8,10 @@ private let mainWindowId = "main"
|
|||||||
@main
|
@main
|
||||||
struct VniDropApp: App {
|
struct VniDropApp: App {
|
||||||
@StateObject private var externalInvitations = ExternalInvitationController()
|
@StateObject private var externalInvitations = ExternalInvitationController()
|
||||||
|
#if DIRECT_DISTRIBUTION && os(macOS)
|
||||||
|
// Sparkle auto-updater, present only in the direct-download (.dmg) build.
|
||||||
|
@StateObject private var updater = SparkleUpdaterController()
|
||||||
|
#endif
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@@ -18,6 +22,11 @@ struct VniDropApp: App {
|
|||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.onOpenURL(perform: openInvitation)
|
.onOpenURL(perform: openInvitation)
|
||||||
}
|
}
|
||||||
|
#if DIRECT_DISTRIBUTION
|
||||||
|
.commands {
|
||||||
|
UpdatesCommands(controller: updater)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#else
|
#else
|
||||||
WindowGroup(id: mainWindowId) {
|
WindowGroup(id: mainWindowId) {
|
||||||
RootView(dependencies: makeAppDependencies(externalInvitations: externalInvitations))
|
RootView(dependencies: makeAppDependencies(externalInvitations: externalInvitations))
|
||||||
|
|||||||
49
apple/VniDrop/Platform/SparkleUpdater.swift
Normal file
49
apple/VniDrop/Platform/SparkleUpdater.swift
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#if DIRECT_DISTRIBUTION && os(macOS)
|
||||||
|
import Combine
|
||||||
|
import Sparkle
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Owns the Sparkle updater for the direct-download (.dmg) build.
|
||||||
|
///
|
||||||
|
/// Compiled only under `DIRECT_DISTRIBUTION`, so the App Store / TestFlight target
|
||||||
|
/// (which must not ship a self-updater) never compiles or links Sparkle. The feed
|
||||||
|
/// URL and public EdDSA key are read from Info.plist (`SUFeedURL`, `SUPublicEDKey`).
|
||||||
|
@MainActor
|
||||||
|
final class SparkleUpdaterController: ObservableObject {
|
||||||
|
private let updaterController: SPUStandardUpdaterController
|
||||||
|
/// Mirrors `SPUUpdater.canCheckForUpdates` so the menu item can disable itself
|
||||||
|
/// while a check is already in flight.
|
||||||
|
@Published private(set) var canCheckForUpdates = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// `startingUpdater: true` begins the automatic background check schedule.
|
||||||
|
updaterController = SPUStandardUpdaterController(
|
||||||
|
startingUpdater: true,
|
||||||
|
updaterDelegate: nil,
|
||||||
|
userDriverDelegate: nil
|
||||||
|
)
|
||||||
|
updaterController.updater
|
||||||
|
.publisher(for: \.canCheckForUpdates)
|
||||||
|
.assign(to: &$canCheckForUpdates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForUpdates() {
|
||||||
|
updaterController.checkForUpdates(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a "Check for Updates…" item to the application menu (right after the
|
||||||
|
/// standard "About VniDrop" item), matching the macOS convention.
|
||||||
|
struct UpdatesCommands: Commands {
|
||||||
|
@ObservedObject var controller: SparkleUpdaterController
|
||||||
|
|
||||||
|
var body: some Commands {
|
||||||
|
CommandGroup(after: .appInfo) {
|
||||||
|
Button(String(localized: L10n.Updates.check)) {
|
||||||
|
controller.checkForUpdates()
|
||||||
|
}
|
||||||
|
.disabled(!controller.canCheckForUpdates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -57,6 +57,20 @@
|
|||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.utilities</string>
|
<string>public.app-category.utilities</string>
|
||||||
|
<!-- Sparkle auto-update (direct-download .dmg build only). These keys are inert
|
||||||
|
in the App Store build, which never loads Sparkle (DIRECT_DISTRIBUTION off).
|
||||||
|
The feed is appcast.xml attached as an asset to each GitHub Release; the
|
||||||
|
/releases/latest/download/ path always redirects to the newest (non-
|
||||||
|
prerelease) release's copy, and its <enclosure> points at that same release's
|
||||||
|
.dmg — no GitHub Pages or repo commits needed. SUPublicEDKey must be the EdDSA
|
||||||
|
public key printed by Sparkle's `generate_keys` — replace the placeholder
|
||||||
|
before shipping (see apple/RELEASE-MACOS.md). -->
|
||||||
|
<key>SUFeedURL</key>
|
||||||
|
<string>https://github.com/sudosylabs/vnidrop/releases/latest/download/appcast.xml</string>
|
||||||
|
<key>SUPublicEDKey</key>
|
||||||
|
<string>/vcOgyrhPi3e58yL8M7hZvDCOgsAKyBsQu/7ChAUk1M=</string>
|
||||||
|
<key>SUEnableAutomaticChecks</key>
|
||||||
|
<true/>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NFCReaderUsageDescription</key>
|
<key>NFCReaderUsageDescription</key>
|
||||||
|
|||||||
15
apple/VniDrop/Resources/VniDropDirect.entitlements
Normal file
15
apple/VniDrop/Resources/VniDropDirect.entitlements
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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">
|
||||||
|
<!-- Entitlements for the direct-download (Developer ID + notarized) macOS build.
|
||||||
|
|
||||||
|
Deliberately NOT sandboxed: a sandboxed app signed for Developer ID requires a
|
||||||
|
provisioning profile, whereas direct-distribution apps run outside the App
|
||||||
|
Store sandbox by convention. Gatekeeper trust here comes from the hardened
|
||||||
|
runtime (ENABLE_HARDENED_RUNTIME) plus notarization, not the sandbox. The App
|
||||||
|
Store target (VniDrop) keeps VniDrop.entitlements with the sandbox enabled.
|
||||||
|
|
||||||
|
Networking and user file access need no entitlements once unsandboxed. -->
|
||||||
|
<dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -12,6 +12,15 @@ options:
|
|||||||
macOS: "15.0"
|
macOS: "15.0"
|
||||||
createIntermediateGroups: true
|
createIntermediateGroups: true
|
||||||
|
|
||||||
|
# Build configurations. Declaring `configs` replaces XcodeGen's Debug/Release
|
||||||
|
# defaults, so both are re-listed here. `Release-Direct` is a release-type config
|
||||||
|
# used only by the VniDropDirect (notarized DMG + Sparkle) target; the App Store
|
||||||
|
# `VniDrop` target ships under plain `Release`.
|
||||||
|
configs:
|
||||||
|
Debug: debug
|
||||||
|
Release: release
|
||||||
|
Release-Direct: release
|
||||||
|
|
||||||
# Project-wide build settings (applied to every target/config).
|
# Project-wide build settings (applied to every target/config).
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
@@ -26,27 +35,44 @@ packages:
|
|||||||
SFSafeSymbols:
|
SFSafeSymbols:
|
||||||
url: https://github.com/SFSafeSymbols/SFSafeSymbols
|
url: https://github.com/SFSafeSymbols/SFSafeSymbols
|
||||||
from: "5.3.0"
|
from: "5.3.0"
|
||||||
|
# Sparkle powers in-app auto-updates for the direct-download (.dmg) build only.
|
||||||
|
# It is linked exclusively by the VniDropDirect target — SwiftPM links products
|
||||||
|
# per target, not per config, so keeping it off the App Store target is what
|
||||||
|
# guarantees the store binary never bundles a self-updater (App Store forbids it).
|
||||||
|
Sparkle:
|
||||||
|
url: https://github.com/sparkle-project/Sparkle
|
||||||
|
from: "2.9.4"
|
||||||
|
|
||||||
targets:
|
# Shared definition for the two shipping app targets. `VniDrop` (App Store /
|
||||||
VniDrop:
|
# TestFlight) and `VniDropDirect` (notarized DMG + Sparkle) build the exact same
|
||||||
|
# sources; only their destinations, extra dependencies, and the DIRECT_DISTRIBUTION
|
||||||
|
# compile flag differ (set per target below).
|
||||||
|
targetTemplates:
|
||||||
|
AppBase:
|
||||||
type: application
|
type: application
|
||||||
supportedDestinations: [iOS, macOS]
|
|
||||||
configFiles:
|
configFiles:
|
||||||
Debug: Signing.xcconfig
|
Debug: Signing.xcconfig
|
||||||
Release: Signing.xcconfig
|
Release: Signing.xcconfig
|
||||||
|
Release-Direct: Signing.xcconfig
|
||||||
sources:
|
sources:
|
||||||
- path: VniDrop
|
- path: VniDrop
|
||||||
excludes:
|
excludes:
|
||||||
- "Resources/Info.plist"
|
- "Resources/Info.plist"
|
||||||
- "Resources/VniDrop.entitlements"
|
- "Resources/VniDrop.entitlements"
|
||||||
|
- "Resources/VniDropDirect.entitlements"
|
||||||
- "Resources/**/.DS_Store"
|
- "Resources/**/.DS_Store"
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
|
PRODUCT_NAME: VniDrop
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: com.vnidrop.app
|
PRODUCT_BUNDLE_IDENTIFIER: com.vnidrop.app
|
||||||
MARKETING_VERSION: "0.1.0"
|
MARKETING_VERSION: "0.1.0"
|
||||||
CURRENT_PROJECT_VERSION: "7"
|
# Placeholder only — the real CFBundleVersion is stamped at build time as a
|
||||||
|
# UTC YYMMDD.HHMM timestamp by the "Stamp build number" phase below, so every
|
||||||
|
# build is monotonic and self-describing (shown as "MARKETING_VERSION (build)").
|
||||||
|
CURRENT_PROJECT_VERSION: "1"
|
||||||
GENERATE_INFOPLIST_FILE: NO
|
GENERATE_INFOPLIST_FILE: NO
|
||||||
INFOPLIST_FILE: VniDrop/Resources/Info.plist
|
INFOPLIST_FILE: VniDrop/Resources/Info.plist
|
||||||
|
CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements
|
||||||
# Mirror the Info.plist identity so Xcode's Identity editor shows it too
|
# Mirror the Info.plist identity so Xcode's Identity editor shows it too
|
||||||
# (the editor reads these build settings, not the manual plist).
|
# (the editor reads these build settings, not the manual plist).
|
||||||
INFOPLIST_KEY_CFBundleDisplayName: VniDrop
|
INFOPLIST_KEY_CFBundleDisplayName: VniDrop
|
||||||
@@ -59,12 +85,11 @@ targets:
|
|||||||
# AccentColor asset mirrors VniDropColors.brandPurple — keep them in sync.
|
# AccentColor asset mirrors VniDropColors.brandPurple — keep them in sync.
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
||||||
configs:
|
configs:
|
||||||
debug:
|
|
||||||
CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements
|
|
||||||
release:
|
release:
|
||||||
CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements
|
|
||||||
# Produce a dSYM in the archive so symbol upload succeeds.
|
# Produce a dSYM in the archive so symbol upload succeeds.
|
||||||
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
|
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
|
||||||
|
release-direct:
|
||||||
|
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
|
||||||
dependencies:
|
dependencies:
|
||||||
- package: VnidropCore
|
- package: VnidropCore
|
||||||
- package: SFSafeSymbols
|
- package: SFSafeSymbols
|
||||||
@@ -86,6 +111,52 @@ targets:
|
|||||||
echo "error: SwiftLint not installed — run 'brew install swiftlint'"
|
echo "error: SwiftLint not installed — run 'brew install swiftlint'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
postBuildScripts:
|
||||||
|
# Stamp CFBundleVersion as a UTC YYMMDD.HHMM timestamp into the built
|
||||||
|
# Info.plist before code signing. Runs for every build (Xcode GUI archive and
|
||||||
|
# CLI alike), so both the App Store and direct-download channels get a
|
||||||
|
# monotonic, meaningful build id. CI/reproducible builds can pin it via the
|
||||||
|
# VNIDROP_BUILD env var. MARKETING_VERSION stays the human X.Y.Z version.
|
||||||
|
- name: Stamp build number (UTC timestamp)
|
||||||
|
basedOnDependencyAnalysis: false
|
||||||
|
script: |
|
||||||
|
build="${VNIDROP_BUILD:-$(date -u +%y%m%d.%H%M)}"
|
||||||
|
plist="${TARGET_BUILD_DIR}/${INFOPLIST_PATH}"
|
||||||
|
if [ -f "$plist" ]; then
|
||||||
|
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $build" "$plist"
|
||||||
|
echo "Stamped CFBundleVersion = $build"
|
||||||
|
else
|
||||||
|
echo "warning: Info.plist not found at $plist; CFBundleVersion not stamped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
targets:
|
||||||
|
# App Store / TestFlight target. iOS + macOS, sandboxed, no self-updater.
|
||||||
|
VniDrop:
|
||||||
|
templates: [AppBase]
|
||||||
|
supportedDestinations: [iOS, macOS]
|
||||||
|
|
||||||
|
# Direct-download macOS target: Developer ID signed, notarized, ships in a .dmg
|
||||||
|
# and self-updates via Sparkle. DIRECT_DISTRIBUTION gates all Sparkle code so the
|
||||||
|
# App Store target above never compiles or links it.
|
||||||
|
VniDropDirect:
|
||||||
|
templates: [AppBase]
|
||||||
|
supportedDestinations: [macOS]
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) DIRECT_DISTRIBUTION"
|
||||||
|
# Notarization requires the hardened runtime.
|
||||||
|
ENABLE_HARDENED_RUNTIME: YES
|
||||||
|
# Non-sandboxed entitlements: a sandboxed Developer ID app needs a
|
||||||
|
# provisioning profile, which direct distribution avoids. (App Store target
|
||||||
|
# keeps VniDrop.entitlements with the sandbox.)
|
||||||
|
CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDropDirect.entitlements
|
||||||
|
# The Rust core's macOS slice (vnidrop.xcframework) is arm64-only
|
||||||
|
# (build-core.sh builds aarch64-apple-darwin only), so the direct build is
|
||||||
|
# Apple-Silicon-only. Pin ARCHS so the Release-Direct (universal-by-default)
|
||||||
|
# link doesn't fail looking for x86_64 symbols.
|
||||||
|
ARCHS: arm64
|
||||||
|
dependencies:
|
||||||
|
- package: Sparkle
|
||||||
|
|
||||||
VniDropTests:
|
VniDropTests:
|
||||||
type: bundle.unit-test
|
type: bundle.unit-test
|
||||||
@@ -93,6 +164,7 @@ targets:
|
|||||||
configFiles:
|
configFiles:
|
||||||
Debug: Signing.xcconfig
|
Debug: Signing.xcconfig
|
||||||
Release: Signing.xcconfig
|
Release: Signing.xcconfig
|
||||||
|
Release-Direct: Signing.xcconfig
|
||||||
sources:
|
sources:
|
||||||
- path: Tests
|
- path: Tests
|
||||||
settings:
|
settings:
|
||||||
@@ -120,3 +192,15 @@ schemes:
|
|||||||
config: Release
|
config: Release
|
||||||
archive:
|
archive:
|
||||||
config: Release
|
config: Release
|
||||||
|
|
||||||
|
# Direct-download build: always the Release-Direct config (Sparkle + notarization).
|
||||||
|
VniDropDirect:
|
||||||
|
build:
|
||||||
|
targets:
|
||||||
|
VniDropDirect: all
|
||||||
|
run:
|
||||||
|
config: Release-Direct
|
||||||
|
profile:
|
||||||
|
config: Release-Direct
|
||||||
|
archive:
|
||||||
|
config: Release-Direct
|
||||||
|
|||||||
19
apple/scripts/ExportOptions-DeveloperID.plist
Normal file
19
apple/scripts/ExportOptions-DeveloperID.plist
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?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">
|
||||||
|
<!-- Export options for the direct-download (Developer ID) macOS build consumed by
|
||||||
|
`xcodebuild -exportArchive` in build-dmg.sh. This produces a Developer
|
||||||
|
ID–signed, hardened-runtime .app suitable for notarization and distribution
|
||||||
|
outside the Mac App Store. The App Store build uses a different flow entirely. -->
|
||||||
|
<dict>
|
||||||
|
<key>method</key>
|
||||||
|
<string>developer-id</string>
|
||||||
|
<key>signingStyle</key>
|
||||||
|
<string>manual</string>
|
||||||
|
<!-- Xcode manages the Developer ID Application certificate lookup from the
|
||||||
|
keychain; the hardened runtime is enabled via ENABLE_HARDENED_RUNTIME in the
|
||||||
|
VniDropDirect target. -->
|
||||||
|
<key>teamID</key>
|
||||||
|
<string>${DEVELOPMENT_TEAM}</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
158
apple/scripts/build-dmg.sh
Executable file
158
apple/scripts/build-dmg.sh
Executable file
@@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Builds the direct-download macOS artifact: a Developer ID–signed, notarized
|
||||||
|
# .dmg of the VniDropDirect target (the Sparkle-enabled build). Produces:
|
||||||
|
# - apple/dist/VniDrop-<version>.dmg (signed + stapled when notarizing)
|
||||||
|
#
|
||||||
|
# This is the direct-distribution counterpart to the App Store archive flow; it
|
||||||
|
# never touches the App Store `VniDrop` target. The Rust crate is not modified.
|
||||||
|
#
|
||||||
|
# Usage: apple/scripts/build-dmg.sh [version]
|
||||||
|
# version MAJOR.MINOR.PATCH; defaults to MARKETING_VERSION / the git tag.
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# DEVELOPER_ID_APP Codesign identity, e.g. "Developer ID Application: … (TEAMID)".
|
||||||
|
# Auto-detected from the keychain when unset.
|
||||||
|
# DEVELOPMENT_TEAM Apple team ID (10 chars). Auto-derived from the identity.
|
||||||
|
# NOTARY_PROFILE Name of a `xcrun notarytool store-credentials` keychain
|
||||||
|
# profile. When set, the DMG is notarized and stapled; when
|
||||||
|
# unset the build still produces a signed DMG and prints the
|
||||||
|
# pending notarization step (useful before creds exist).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
APPLE_DIR="$REPO_ROOT/apple"
|
||||||
|
DIST_DIR="$APPLE_DIR/dist"
|
||||||
|
BUILD_DIR="$APPLE_DIR/.build-dmg"
|
||||||
|
PROJECT="$APPLE_DIR/VniDrop.xcodeproj"
|
||||||
|
SCHEME="VniDropDirect"
|
||||||
|
CONFIG="Release-Direct"
|
||||||
|
APP_NAME="VniDrop"
|
||||||
|
|
||||||
|
# --- Resolve version (arg > git tag > project MARKETING_VERSION) -------------
|
||||||
|
resolve_version() {
|
||||||
|
local v="${1:-}"
|
||||||
|
if [ -z "$v" ] && [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
|
||||||
|
v="${GITHUB_REF_NAME#v}"
|
||||||
|
fi
|
||||||
|
if [ -z "$v" ]; then
|
||||||
|
v="$(sed -nE 's/.*MARKETING_VERSION: "([0-9.]+)".*/\1/p' "$APPLE_DIR/project.yml" | head -1)"
|
||||||
|
fi
|
||||||
|
if [[ ! "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "version must be MAJOR.MINOR.PATCH (got '$v')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s' "$v"
|
||||||
|
}
|
||||||
|
VERSION="$(resolve_version "${1:-}")"
|
||||||
|
|
||||||
|
# CFBundleVersion is a UTC YYMMDD.HHMM timestamp stamped by the target's
|
||||||
|
# "Stamp build number" build phase. Pin it here (one value for the whole archive)
|
||||||
|
# so the app, DMG, and appcast all agree; Sparkle compares it to order updates.
|
||||||
|
BUILD_NUMBER="$(date -u +%y%m%d.%H%M)"
|
||||||
|
export VNIDROP_BUILD="$BUILD_NUMBER"
|
||||||
|
|
||||||
|
# --- Resolve signing identity ------------------------------------------------
|
||||||
|
if [ -z "${DEVELOPER_ID_APP:-}" ]; then
|
||||||
|
DEVELOPER_ID_APP="$(security find-identity -v -p codesigning 2>/dev/null \
|
||||||
|
| sed -nE 's/.*"(Developer ID Application: [^"]+)".*/\1/p' | head -1)"
|
||||||
|
fi
|
||||||
|
if [ -z "${DEVELOPER_ID_APP:-}" ]; then
|
||||||
|
echo "error: no 'Developer ID Application' identity found in the keychain." >&2
|
||||||
|
echo " Create one in Xcode ▸ Settings ▸ Accounts, or set DEVELOPER_ID_APP." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "${DEVELOPMENT_TEAM:-}" ]; then
|
||||||
|
# The team ID is the 10-char code in the trailing parenthesis of the identity.
|
||||||
|
DEVELOPMENT_TEAM="$(printf '%s' "$DEVELOPER_ID_APP" | sed -nE 's/.*\(([A-Z0-9]{10})\)$/\1/p')"
|
||||||
|
fi
|
||||||
|
echo "==> Direct build v$VERSION (CFBundleVersion $BUILD_NUMBER)"
|
||||||
|
echo " identity: $DEVELOPER_ID_APP"
|
||||||
|
echo " team: ${DEVELOPMENT_TEAM:-<unknown>}"
|
||||||
|
|
||||||
|
# --- Build core + regenerate project ----------------------------------------
|
||||||
|
# Release core needs LTO disabled (workspace thin-LTO miscompiles proc-macros).
|
||||||
|
echo "==> Building Rust core (release)"
|
||||||
|
CARGO_PROFILE_RELEASE_LTO=false "$SCRIPT_DIR/build-core.sh" release
|
||||||
|
echo "==> Regenerating Xcode project"
|
||||||
|
( cd "$APPLE_DIR" && xcodegen generate >/dev/null )
|
||||||
|
|
||||||
|
rm -rf "$BUILD_DIR" && mkdir -p "$BUILD_DIR" "$DIST_DIR"
|
||||||
|
ARCHIVE="$BUILD_DIR/$APP_NAME.xcarchive"
|
||||||
|
EXPORT_DIR="$BUILD_DIR/export"
|
||||||
|
|
||||||
|
# --- Archive + export (Developer ID) ----------------------------------------
|
||||||
|
echo "==> Archiving $SCHEME ($CONFIG)"
|
||||||
|
xcodebuild archive \
|
||||||
|
-project "$PROJECT" \
|
||||||
|
-scheme "$SCHEME" \
|
||||||
|
-configuration "$CONFIG" \
|
||||||
|
-destination 'generic/platform=macOS' \
|
||||||
|
-archivePath "$ARCHIVE" \
|
||||||
|
MARKETING_VERSION="$VERSION" \
|
||||||
|
DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \
|
||||||
|
CODE_SIGN_STYLE=Manual \
|
||||||
|
CODE_SIGN_IDENTITY="$DEVELOPER_ID_APP" \
|
||||||
|
| xcbeautify 2>/dev/null || true
|
||||||
|
[ -d "$ARCHIVE" ] || { echo "error: archive failed" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "==> Exporting Developer ID app"
|
||||||
|
EXPORT_OPTS="$BUILD_DIR/ExportOptions.plist"
|
||||||
|
sed "s/\${DEVELOPMENT_TEAM}/$DEVELOPMENT_TEAM/" \
|
||||||
|
"$SCRIPT_DIR/ExportOptions-DeveloperID.plist" > "$EXPORT_OPTS"
|
||||||
|
xcodebuild -exportArchive \
|
||||||
|
-archivePath "$ARCHIVE" \
|
||||||
|
-exportPath "$EXPORT_DIR" \
|
||||||
|
-exportOptionsPlist "$EXPORT_OPTS"
|
||||||
|
APP="$EXPORT_DIR/$APP_NAME.app"
|
||||||
|
[ -d "$APP" ] || { echo "error: export failed" >&2; exit 1; }
|
||||||
|
|
||||||
|
# --- Build the DMG -----------------------------------------------------------
|
||||||
|
DMG="$DIST_DIR/$APP_NAME-$VERSION.dmg"
|
||||||
|
rm -f "$DMG"
|
||||||
|
STAGING="$BUILD_DIR/dmg-staging"
|
||||||
|
rm -rf "$STAGING" && mkdir -p "$STAGING"
|
||||||
|
cp -R "$APP" "$STAGING/"
|
||||||
|
ln -s /Applications "$STAGING/Applications"
|
||||||
|
|
||||||
|
echo "==> Building DMG"
|
||||||
|
if command -v create-dmg >/dev/null 2>&1; then
|
||||||
|
create-dmg \
|
||||||
|
--volname "$APP_NAME" \
|
||||||
|
--app-drop-link 380 205 \
|
||||||
|
--icon "$APP_NAME.app" 130 205 \
|
||||||
|
--window-size 540 380 \
|
||||||
|
--no-internet-enable \
|
||||||
|
"$DMG" "$STAGING" >/dev/null || {
|
||||||
|
# create-dmg exits non-zero if it can't set the fancy layout; fall back.
|
||||||
|
[ -f "$DMG" ] || hdiutil create -volname "$APP_NAME" -srcfolder "$STAGING" \
|
||||||
|
-ov -format UDZO "$DMG" >/dev/null
|
||||||
|
}
|
||||||
|
else
|
||||||
|
hdiutil create -volname "$APP_NAME" -srcfolder "$STAGING" \
|
||||||
|
-ov -format UDZO "$DMG" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Signing DMG"
|
||||||
|
codesign --force --sign "$DEVELOPER_ID_APP" --timestamp "$DMG"
|
||||||
|
|
||||||
|
# --- Notarize + staple -------------------------------------------------------
|
||||||
|
if [ -n "${NOTARY_PROFILE:-}" ]; then
|
||||||
|
echo "==> Notarizing (profile: $NOTARY_PROFILE)"
|
||||||
|
xcrun notarytool submit "$DMG" --keychain-profile "$NOTARY_PROFILE" --wait
|
||||||
|
echo "==> Stapling"
|
||||||
|
xcrun stapler staple "$DMG"
|
||||||
|
xcrun stapler validate "$DMG"
|
||||||
|
spctl -a -vvv --type install "$DMG" || true
|
||||||
|
else
|
||||||
|
echo "==> NOTARY_PROFILE unset — skipping notarization."
|
||||||
|
echo " The DMG is signed but NOT notarized; Gatekeeper will block it until"
|
||||||
|
echo " you run 'xcrun notarytool store-credentials' and re-run with NOTARY_PROFILE set."
|
||||||
|
fi
|
||||||
|
|
||||||
|
SIZE="$(stat -f%z "$DMG")"
|
||||||
|
echo "==> Done."
|
||||||
|
echo " dmg: $DMG"
|
||||||
|
echo " version: $VERSION"
|
||||||
|
echo " size: $SIZE bytes"
|
||||||
68
apple/scripts/generate-appcast.sh
Executable file
68
apple/scripts/generate-appcast.sh
Executable file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Generates/updates the Sparkle appcast for the direct-download build. Runs
|
||||||
|
# Sparkle's `generate_appcast` over the DMGs in apple/dist/, writing:
|
||||||
|
# - apple/dist/appcast.xml
|
||||||
|
#
|
||||||
|
# The <enclosure> URLs point at the matching GitHub Release download assets, and
|
||||||
|
# each item is signed with the project's EdDSA key (from a key file or the
|
||||||
|
# keychain). The resulting appcast.xml is uploaded as a release asset; the app's
|
||||||
|
# SUFeedURL (/releases/latest/download/appcast.xml) always resolves to the newest.
|
||||||
|
#
|
||||||
|
# Usage: apple/scripts/generate-appcast.sh [version]
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# DIST_DIR Folder holding the DMG(s). Default: apple/dist
|
||||||
|
# SPARKLE_BIN Dir containing generate_appcast. Auto-located when unset.
|
||||||
|
# SPARKLE_ED_KEY_FILE Path to the EdDSA private key file. When unset,
|
||||||
|
# generate_appcast reads the key from the login keychain.
|
||||||
|
# RELEASE_REPO owner/repo for enclosure URLs. Default: sudosylabs/vnidrop
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
APPLE_DIR="$REPO_ROOT/apple"
|
||||||
|
DIST_DIR="${DIST_DIR:-$APPLE_DIR/dist}"
|
||||||
|
RELEASE_REPO="${RELEASE_REPO:-sudosylabs/vnidrop}"
|
||||||
|
|
||||||
|
VERSION="${1:-}"
|
||||||
|
if [ -z "$VERSION" ] && [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
|
||||||
|
VERSION="${GITHUB_REF_NAME#v}"
|
||||||
|
fi
|
||||||
|
[ -n "$VERSION" ] || { echo "error: version required (arg or tag)" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Enclosure URLs resolve to the specific release's assets.
|
||||||
|
DOWNLOAD_PREFIX="https://github.com/$RELEASE_REPO/releases/download/v$VERSION"
|
||||||
|
|
||||||
|
# --- Locate generate_appcast -------------------------------------------------
|
||||||
|
find_tool() {
|
||||||
|
local name="$1"
|
||||||
|
if [ -n "${SPARKLE_BIN:-}" ] && [ -x "$SPARKLE_BIN/$name" ]; then
|
||||||
|
printf '%s' "$SPARKLE_BIN/$name"; return 0
|
||||||
|
fi
|
||||||
|
if command -v "$name" >/dev/null 2>&1; then command -v "$name"; return 0; fi
|
||||||
|
# Sparkle SPM artifact bundle lands under DerivedData SourcePackages.
|
||||||
|
local dd="${APPLE_DERIVED_DATA:-$HOME/Library/Developer/Xcode/DerivedData}"
|
||||||
|
local hit
|
||||||
|
hit="$(find "$dd" "$HOME/Library/Caches/org.swift.swiftpm" -type f -name "$name" \
|
||||||
|
-perm -111 2>/dev/null | head -1 || true)"
|
||||||
|
[ -n "$hit" ] && { printf '%s' "$hit"; return 0; }
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
GENERATE_APPCAST="$(find_tool generate_appcast || true)"
|
||||||
|
if [ -z "$GENERATE_APPCAST" ]; then
|
||||||
|
echo "error: generate_appcast not found. Set SPARKLE_BIN to Sparkle's bin/ dir" >&2
|
||||||
|
echo " (download from https://github.com/sparkle-project/Sparkle/releases)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> Using $GENERATE_APPCAST"
|
||||||
|
|
||||||
|
# --- Generate ----------------------------------------------------------------
|
||||||
|
args=( --download-url-prefix "$DOWNLOAD_PREFIX/" -o "$DIST_DIR/appcast.xml" )
|
||||||
|
if [ -n "${SPARKLE_ED_KEY_FILE:-}" ]; then
|
||||||
|
args+=( --ed-key-file "$SPARKLE_ED_KEY_FILE" )
|
||||||
|
fi
|
||||||
|
echo "==> Generating appcast (v$VERSION) → $DIST_DIR/appcast.xml"
|
||||||
|
"$GENERATE_APPCAST" "${args[@]}" "$DIST_DIR"
|
||||||
|
|
||||||
|
echo "==> Done. Enclosure prefix: $DOWNLOAD_PREFIX/"
|
||||||
@@ -4629,6 +4629,23 @@
|
|||||||
"ru": "Поделиться"
|
"ru": "Поделиться"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"updates_check": {
|
||||||
|
"context": "macOS app menu item that checks for a new version via Sparkle. Direct-download (.dmg) build only; never shown in the App Store build.",
|
||||||
|
"targets": [
|
||||||
|
"apple"
|
||||||
|
],
|
||||||
|
"translations": {
|
||||||
|
"en": "Check for Updates…",
|
||||||
|
"fr": "Rechercher les mises à jour…",
|
||||||
|
"es": "Buscar actualizaciones…",
|
||||||
|
"it": "Cerca aggiornamenti…",
|
||||||
|
"de": "Nach Updates suchen…",
|
||||||
|
"pt": "Procurar atualizações…",
|
||||||
|
"pl": "Sprawdź aktualizacje…",
|
||||||
|
"nl": "Zoeken naar updates…",
|
||||||
|
"ru": "Проверить наличие обновлений…"
|
||||||
|
}
|
||||||
|
},
|
||||||
"value_unavailable": {
|
"value_unavailable": {
|
||||||
"context": "Placeholder shown when a device-info or metadata value can't be read.",
|
"context": "Placeholder shown when a device-info or metadata value can't be read.",
|
||||||
"translations": {
|
"translations": {
|
||||||
|
|||||||
54
packaging/homebrew/tap-README.md
Normal file
54
packaging/homebrew/tap-README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# homebrew-vnidrop
|
||||||
|
|
||||||
|
Homebrew tap for [VniDrop](https://github.com/sudosylabs/vnidrop) — direct,
|
||||||
|
private device-to-device file and folder transfer for macOS.
|
||||||
|
|
||||||
|
> This repo only holds the Homebrew **cask**. The app itself lives at
|
||||||
|
> [sudosylabs/vnidrop](https://github.com/sudosylabs/vnidrop). The cask here is
|
||||||
|
> updated automatically by VniDrop's release pipeline on each tagged release.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew tap sudosylabs/vnidrop
|
||||||
|
brew install --cask vnidrop
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in one line:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew install --cask sudosylabs/vnidrop/vnidrop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update
|
||||||
|
|
||||||
|
VniDrop updates itself in-app via [Sparkle](https://sparkle-project.org), so you
|
||||||
|
normally don't need to do anything. To update through Homebrew instead:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew upgrade --cask vnidrop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew uninstall --cask vnidrop
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--zap` to also remove VniDrop's application support, cache, and preference
|
||||||
|
files:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew uninstall --zap --cask vnidrop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- macOS 15 (Sequoia) or later, Apple Silicon.
|
||||||
|
|
||||||
|
## What you get
|
||||||
|
|
||||||
|
The cask installs the Developer ID–signed, notarized `VniDrop.app` from the
|
||||||
|
matching [GitHub Release](https://github.com/sudosylabs/vnidrop/releases). App
|
||||||
|
Store users should install from the Mac App Store instead — that build does not
|
||||||
|
include the Sparkle self-updater.
|
||||||
36
packaging/homebrew/vnidrop.rb
Normal file
36
packaging/homebrew/vnidrop.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Homebrew cask for the direct-download (notarized .dmg) macOS build.
|
||||||
|
#
|
||||||
|
# This file is the source template. The Apple release workflow substitutes the
|
||||||
|
# version + sha256 for each release and pushes the result to the tap repo
|
||||||
|
# (sudosylabs/homebrew-vnidrop, path Casks/vnidrop.rb). Users then install with:
|
||||||
|
# brew install --cask sudosylabs/vnidrop/vnidrop
|
||||||
|
#
|
||||||
|
# `auto_updates true` tells Homebrew that the app updates itself via Sparkle, so
|
||||||
|
# `brew upgrade` won't fight the in-app updater.
|
||||||
|
cask "vnidrop" do
|
||||||
|
version "0.0.0"
|
||||||
|
sha256 "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
|
||||||
|
url "https://github.com/sudosylabs/vnidrop/releases/download/v#{version}/VniDrop-#{version}.dmg",
|
||||||
|
verified: "github.com/sudosylabs/vnidrop/"
|
||||||
|
name "VniDrop"
|
||||||
|
desc "Direct device-to-device file and folder transfer over the network"
|
||||||
|
homepage "https://github.com/sudosylabs/vnidrop"
|
||||||
|
|
||||||
|
livecheck do
|
||||||
|
url :url
|
||||||
|
strategy :github_latest
|
||||||
|
end
|
||||||
|
|
||||||
|
auto_updates true
|
||||||
|
depends_on arch: :arm64
|
||||||
|
depends_on macos: ">= :sequoia"
|
||||||
|
|
||||||
|
app "VniDrop.app"
|
||||||
|
|
||||||
|
zap trash: [
|
||||||
|
"~/Library/Application Support/com.vnidrop.app",
|
||||||
|
"~/Library/Caches/com.vnidrop.app",
|
||||||
|
"~/Library/Preferences/com.vnidrop.app.plist",
|
||||||
|
]
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user