mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +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
|
||||
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).
|
||||
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.
|
||||
@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"
|
||||
|
||||
4
apple/.gitignore
vendored
4
apple/.gitignore
vendored
@@ -18,3 +18,7 @@ Local.xcconfig
|
||||
.swiftpm/
|
||||
DerivedData/
|
||||
*.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-project # generate apple/VniDrop.xcodeproj
|
||||
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 build-apple-ios # unsigned iOS simulator build
|
||||
make build-apple-ios # unsigned iOS simulator app
|
||||
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
|
||||
`APPLE_DESTINATION` to override the automatically selected iOS simulator.
|
||||
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
|
||||
struct VniDropApp: App {
|
||||
@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 {
|
||||
#if os(macOS)
|
||||
@@ -18,6 +22,11 @@ struct VniDropApp: App {
|
||||
.ignoresSafeArea()
|
||||
.onOpenURL(perform: openInvitation)
|
||||
}
|
||||
#if DIRECT_DISTRIBUTION
|
||||
.commands {
|
||||
UpdatesCommands(controller: updater)
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
WindowGroup(id: mainWindowId) {
|
||||
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>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<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>
|
||||
<true/>
|
||||
<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"
|
||||
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).
|
||||
settings:
|
||||
base:
|
||||
@@ -26,27 +35,44 @@ packages:
|
||||
SFSafeSymbols:
|
||||
url: https://github.com/SFSafeSymbols/SFSafeSymbols
|
||||
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:
|
||||
VniDrop:
|
||||
# Shared definition for the two shipping app targets. `VniDrop` (App Store /
|
||||
# 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
|
||||
supportedDestinations: [iOS, macOS]
|
||||
configFiles:
|
||||
Debug: Signing.xcconfig
|
||||
Release: Signing.xcconfig
|
||||
Release-Direct: Signing.xcconfig
|
||||
sources:
|
||||
- path: VniDrop
|
||||
excludes:
|
||||
- "Resources/Info.plist"
|
||||
- "Resources/VniDrop.entitlements"
|
||||
- "Resources/VniDropDirect.entitlements"
|
||||
- "Resources/**/.DS_Store"
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_NAME: VniDrop
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.vnidrop.app
|
||||
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
|
||||
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
|
||||
# (the editor reads these build settings, not the manual plist).
|
||||
INFOPLIST_KEY_CFBundleDisplayName: VniDrop
|
||||
@@ -59,12 +85,11 @@ targets:
|
||||
# AccentColor asset mirrors VniDropColors.brandPurple — keep them in sync.
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
||||
configs:
|
||||
debug:
|
||||
CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements
|
||||
release:
|
||||
CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements
|
||||
# Produce a dSYM in the archive so symbol upload succeeds.
|
||||
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
|
||||
release-direct:
|
||||
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
|
||||
dependencies:
|
||||
- package: VnidropCore
|
||||
- package: SFSafeSymbols
|
||||
@@ -86,6 +111,52 @@ targets:
|
||||
echo "error: SwiftLint not installed — run 'brew install swiftlint'"
|
||||
exit 1
|
||||
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:
|
||||
type: bundle.unit-test
|
||||
@@ -93,6 +164,7 @@ targets:
|
||||
configFiles:
|
||||
Debug: Signing.xcconfig
|
||||
Release: Signing.xcconfig
|
||||
Release-Direct: Signing.xcconfig
|
||||
sources:
|
||||
- path: Tests
|
||||
settings:
|
||||
@@ -120,3 +192,15 @@ schemes:
|
||||
config: Release
|
||||
archive:
|
||||
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": "Поделиться"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"context": "Placeholder shown when a device-info or metadata value can't be read.",
|
||||
"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