Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ all of it back whenever a device appears.
Requires macOS 27. Download `Hyperpoint-<version>.dmg` from
[Releases](https://github.com/progremir/hyperpoint/releases), open it and drag Hyperpoint into Applications.

The release is signed with an Apple Development certificate and is not notarized, so macOS refuses the first open.
Allow it under System Settings › Privacy & Security › Open Anyway. The installer window says the same.
Releases are signed with Developer ID and notarized by Apple. v0.1.0 predates that: macOS refuses its first open, so
allow it under System Settings › Privacy & Security › Open Anyway.

### From source

Expand Down Expand Up @@ -67,15 +67,28 @@ build under test and reads device state back as proof.

### Releasing

One-time setup: create a **Developer ID Application** certificate (Xcode › Settings › Accounts › Manage Certificates)
and save notarization credentials as the `hyperpoint` keychain profile with an
[app-specific password](https://account.apple.com):

```bash
xcrun notarytool store-credentials hyperpoint --apple-id <apple-id> --team-id ZZ2G9658MN
```

Then:

```bash
scripts/release.sh 0.2.0 --publish
```

This builds `dist/Hyperpoint-0.2.0.dmg`, signed like `bundle.sh`, tags the pushed HEAD `v0.2.0` and publishes the DMG
with its SHA-256 to a GitHub release. It refuses to publish with uncommitted changes or an unpushed HEAD. Drop
`--publish` to only build the DMG. Pull requests that touch the pipeline run `.github/workflows/release.yml`, which
builds an ad-hoc DMG on CI as an artifact. The installer window is drawn by `design/installer/build.py`;
`design/installer/preview.png` shows it mounted.
This builds `dist/Hyperpoint-0.2.0.dmg`, signs the app and the DMG with Developer ID, notarizes and staples the DMG,
tags the pushed HEAD `v0.2.0` and publishes the DMG with its SHA-256 to a GitHub release. It refuses to publish without
Developer ID, with uncommitted changes or with an unpushed HEAD. Drop `--publish` to only build the DMG. Pull requests
that touch the pipeline run `.github/workflows/release.yml`, which builds an ad-hoc DMG on CI as an artifact. The
installer window is drawn by `design/installer/build.py`; `design/installer/preview.png` shows it mounted.

The release's Developer ID signature differs from a `bundle.sh` build's Apple Development one, so switching between
the two asks for the Accessibility and Input Monitoring grants again.

## Design

Expand Down
2 changes: 1 addition & 1 deletion design/installer/background.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified design/installer/background.tiff
Binary file not shown.
2 changes: 1 addition & 1 deletion design/installer/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def text(x, y, s, size, weight, fill, opacity=1):
{arrow()}
{text(W / 2, 290, "Drag Hyperpoint into Applications", 16, 600, INK)}
{text(W / 2, 310, "Then open it from Applications and allow Accessibility and Input Monitoring.", 12, 400, INK, 0.62)}
{text(W / 2, 334, "macOS blocks the first open? System Settings › Privacy &amp; Security › Open Anyway.", 11, 400, ACCENT_DEEP)}
{text(W / 2, 334, "Hyperpoint lives in the menu bar, next to the clock.", 11, 400, ACCENT_DEEP)}
</svg>
"""

Expand Down
Binary file modified design/installer/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions scripts/build-app.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/bin/zsh
# Builds, signs and checks Hyperpoint.app at the path given ($1), stamped with an optional version ($2).
# Builds, signs and checks Hyperpoint.app at the path given ($1), stamped with an optional version ($2). It is signed
# with the hardened runtime notarization needs, and SIGN_TIMESTAMP=1 adds the secure timestamp it also needs.
# TCC keys the Accessibility and Input Monitoring grants to the app's designated requirement. An ad-hoc signature
# makes that a cdhash, which changes on every build and silently revokes both grants, so this refuses one unless
# ALLOW_ADHOC=1 (CI builds that only check the pipeline).
Expand Down Expand Up @@ -28,7 +29,9 @@ cp design/logo/macos/AppIcon.icns design/logo/macos/MenuBarTemplate.png design/l
"$OUT/Contents/Resources/"
plutil -lint "$OUT/Contents/Info.plist" >/dev/null

codesign --force --sign "$IDENTITY" --identifier "$BUNDLE_ID" "$OUT"
TIMESTAMP=(--timestamp=none)
[[ "${SIGN_TIMESTAMP:-0}" == 1 ]] && TIMESTAMP=(--timestamp)
codesign --force --options runtime "${TIMESTAMP[@]}" --sign "$IDENTITY" --identifier "$BUNDLE_ID" "$OUT"
codesign --verify --strict --verbose=2 "$OUT"

REQ="$(codesign -d -r- "$OUT" 2>&1 | grep '^designated' || true)"
Expand Down
44 changes: 36 additions & 8 deletions scripts/release.sh
Original file line number Diff line number Diff line change
@@ -1,36 +1,64 @@
#!/bin/zsh
# Builds dist/Hyperpoint-<version>.dmg, the drag-to-Applications installer, and with --publish uploads it to a
# GitHub release, tagging the pushed HEAD v<version>. Signs with $SIGN_IDENTITY like build-app.sh.
# GitHub release, tagging the pushed HEAD v<version>.
#
# With a Developer ID identity (the default) the app and DMG are signed, notarized with the notarytool keychain
# profile $NOTARY_PROFILE and stapled, so a download opens without a Gatekeeper warning. Any other $SIGN_IDENTITY
# builds a DMG for checking the pipeline, which --publish refuses.
set -euo pipefail
cd "${0:A:h}/.."

VERSION="${1:?usage: release.sh <version> [--publish]}"
PUBLISH="${2:-}"
[[ "$VERSION" =~ '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' ]] || { print -u2 "not a version: $VERSION"; exit 1 }

export SIGN_IDENTITY="${SIGN_IDENTITY:-Developer ID Application: Emir Amanbekov (ZZ2G9658MN)}"
NOTARY_PROFILE="${NOTARY_PROFILE:-hyperpoint}"
NOTARIZE=0
[[ "$SIGN_IDENTITY" == "Developer ID Application:"* ]] && NOTARIZE=1

if [[ "$PUBLISH" == --publish ]]; then
(( NOTARIZE )) || { print -u2 "only a Developer ID build can be published: $SIGN_IDENTITY"; exit 1 }
git diff --quiet HEAD || { print -u2 "commit or stash local changes before publishing"; exit 1 }
[[ -n "$(git branch -r --contains HEAD)" ]] || { print -u2 "push HEAD before publishing"; exit 1 }
fi
if (( NOTARIZE )); then
xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null ||
{ print -u2 "no notarytool profile '$NOTARY_PROFILE': run xcrun notarytool store-credentials $NOTARY_PROFILE"; exit 1 }
fi

DIST="$PWD/dist"
DMG="$DIST/Hyperpoint-$VERSION.dmg"
STAGE="$(mktemp -d)"
trap 'rm -rf "$STAGE"' EXIT

scripts/build-app.sh "$STAGE/Hyperpoint.app" "$VERSION"
SIGN_TIMESTAMP=$NOTARIZE scripts/build-app.sh "$STAGE/Hyperpoint.app" "$VERSION"

VENV=.build/dmgbuild-venv
[[ -x $VENV/bin/dmgbuild ]] || { python3 -m venv $VENV && $VENV/bin/pip install --quiet dmgbuild==1.6.5 }

mkdir -p "$DIST"
rm -f "$DMG"
$VENV/bin/dmgbuild -s scripts/dmg-settings.py \
-D app="$STAGE/Hyperpoint.app" \
-D background="$PWD/design/installer/background.tiff" \
-D volume_icon="$PWD/design/logo/macos/AppIcon.icns" \
Hyperpoint "$DMG"
codesign --force --sign "${SIGN_IDENTITY:-Apple Development: Emir Amanbekov (KGBFPV5MVR)}" "$DMG"
# hdiutil create now and then fails with "Bad file descriptor" on CI runners, so it is retried.
for attempt in 1 2 3; do
$VENV/bin/dmgbuild -s scripts/dmg-settings.py \
-D app="$STAGE/Hyperpoint.app" \
-D background="$PWD/design/installer/background.tiff" \
-D volume_icon="$PWD/design/logo/macos/AppIcon.icns" \
Hyperpoint "$DMG" && break
(( attempt < 3 )) || exit 1
print -u2 "dmgbuild failed, retrying"
sleep 5
done

# Without notarization the image stays unsigned: Gatekeeper refuses to open a downloaded DMG signed with an Apple
# Development certificate, but mounts an unsigned one.
if (( NOTARIZE )); then
codesign --force --timestamp --sign "$SIGN_IDENTITY" "$DMG"
xcrun notarytool submit "$DMG" --keychain-profile "$NOTARY_PROFILE" --wait --timeout 30m
xcrun stapler staple "$DMG"
spctl --assess --type open --context context:primary-signature --verbose=2 "$DMG"
fi
hdiutil verify -quiet "$DMG"
(cd "$DIST" && shasum -a 256 "${DMG:t}" > "${DMG:t}.sha256")
print "built $DMG"
Expand Down
Loading