From 4fc4dc16109b9465939d70c97f0f892a390b545c Mon Sep 17 00:00:00 2001 From: Adron Hall Date: Fri, 23 Oct 2026 21:40:05 -0700 Subject: [PATCH] Add dist-local.sh: build a distributable .pkg on this Mac Wraps build-pkg.sh for a person at a Mac: finds the Developer ID Application and Installer certificates in the Keychain, notarizes with a notarytool Keychain profile (substratal-notary by default) when one exists, then reports the package signature, the Gatekeeper verdict and the SHA-256. Without certificates it builds an unsigned test package and says so. --unsigned skips signing, --open reveals the .pkg in Finder. build-pkg.sh also accepts NOTARY_PROFILE as an alternative to an App Store Connect key. make dist; README and CHANGELOG updated. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + Makefile | 5 ++- README.md | 11 ++++++ scripts/build-pkg.sh | 15 ++++++-- scripts/dist-local.sh | 87 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100755 scripts/dist-local.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ff3ef..b2c4079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Every release of Substratal Hub for macOS. The Hub help center's What's new page ### Build and release - `scripts/run-local.sh`: build and launch the app for testing, against the production API or a local devserver. +- `scripts/dist-local.sh` (`make dist`): build a distributable `.pkg` on your Mac, signed and notarized with the Developer ID certificates and `notarytool` profile in your Keychain, with signature and Gatekeeper checks. - `scripts/build-pkg.sh` and the **Release** workflow: build the `.pkg` installer and publish it as a GitHub Release asset. Signed and notarized when the Developer ID secrets are configured; an unsigned pre-release otherwise. ### Platform API diff --git a/Makefile b/Makefile index 45486f4..2456189 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,9 @@ run-local: ## Build and launch for testing; API=… to choose the API (default p pkg: ## Build the installer into build/pkg (signs/notarizes when APP_IDENTITY etc. are set) scripts/build-pkg.sh +dist: ## Build a distributable .pkg on this Mac, signing/notarizing with what's in your Keychain + scripts/dist-local.sh --open + test: ## SubstratalKit unit tests cd Packages/SubstratalKit && swift test @@ -44,4 +47,4 @@ help-check: ## Check the app's Help links against a fresh build of the Hub help check: test build help-check ## Everything CI runs -.PHONY: help project build run run-local pkg test ui-test sync-api sync-screens help-check check +.PHONY: help project build run run-local pkg dist test ui-test sync-api sync-screens help-check check diff --git a/README.md b/README.md index 153461e..9625fb9 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,17 @@ or choose **Settings → Connection → Local devserver** in the app. Each envir ### Building the installer (.pkg) +On your Mac, for distribution: + +```bash +scripts/dist-local.sh # or: make dist (also shows it in Finder) +scripts/dist-local.sh 0.2.0 # a specific version +``` + +It finds the **Developer ID Application** and **Developer ID Installer** certificates in your Keychain and signs with them. With a `notarytool` profile (create it once: `xcrun notarytool store-credentials substratal-notary --team-id --apple-id `) it also notarizes and staples, so the `.pkg` opens on any Mac without warnings. Then it checks the signature and Gatekeeper and prints the path and SHA-256. Without certificates it builds an unsigned test package and says so. + +The underlying build, used by CI as well: + ```bash scripts/build-pkg.sh # → build/pkg/SubstratalHub-.pkg (+ .sha256) VERSION=0.2.0 BUILD_NUMBER=7 scripts/build-pkg.sh diff --git a/scripts/build-pkg.sh b/scripts/build-pkg.sh index d92b48f..6bd690a 100755 --- a/scripts/build-pkg.sh +++ b/scripts/build-pkg.sh @@ -12,8 +12,10 @@ # Unset → ad-hoc signed: runs locally, Gatekeeper warns elsewhere. # INSTALLER_IDENTITY "Developer ID Installer: …" signs the .pkg. Unset → unsigned .pkg. # NOTARY_KEY_PATH, NOTARY_KEY_ID, NOTARY_ISSUER_ID -# App Store Connect API key → notarize and staple the .pkg. -# Needs both identities. Unset → not notarized. +# App Store Connect API key → notarize and staple the .pkg (CI). +# NOTARY_PROFILE Or a notarytool Keychain profile (on a Mac: +# xcrun notarytool store-credentials ). +# Either needs both identities. Neither → not notarized. # # Prints the .pkg path last, and writes .sha256 next to it. set -euo pipefail @@ -88,10 +90,17 @@ sign=() [[ -n "$INSTALLER_IDENTITY" ]] && sign=(--sign "$INSTALLER_IDENTITY" --timestamp) productbuild --distribution "$dist" --package-path "$OUT" ${sign[@]+"${sign[@]}"} "$PKG" >/dev/null +notary=() if [[ -n "${NOTARY_KEY_PATH:-}" && -n "${NOTARY_KEY_ID:-}" && -n "${NOTARY_ISSUER_ID:-}" ]]; then + notary=(--key "$NOTARY_KEY_PATH" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER_ID") +elif [[ -n "${NOTARY_PROFILE:-}" ]]; then + notary=(--keychain-profile "$NOTARY_PROFILE") +fi + +if [[ ${#notary[@]} -gt 0 ]]; then [[ -n "$APP_IDENTITY" && -n "$INSTALLER_IDENTITY" ]] || { echo "build-pkg: notarizing needs APP_IDENTITY and INSTALLER_IDENTITY" >&2; exit 1; } step "Notarizing (this waits for Apple)" - xcrun notarytool submit "$PKG" --key "$NOTARY_KEY_PATH" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER_ID" --wait + xcrun notarytool submit "$PKG" "${notary[@]}" --wait xcrun stapler staple "$PKG" spctl --assess --type install --verbose "$PKG" status="signed, notarized and stapled" diff --git a/scripts/dist-local.sh b/scripts/dist-local.sh new file mode 100755 index 0000000..3b7b75a --- /dev/null +++ b/scripts/dist-local.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Builds a distributable Substratal Hub installer on this Mac. +# +# scripts/dist-local.sh # version from project.yml +# scripts/dist-local.sh 0.2.0 # a specific version (build number: BUILD_NUMBER or a timestamp) +# scripts/dist-local.sh --unsigned # skip signing even if certificates are installed +# scripts/dist-local.sh --open # show the .pkg in Finder when done +# +# Finds your signing setup and uses as much of it as is there: +# - "Developer ID Application" and "Developer ID Installer" certificates in your +# Keychain sign the app and the .pkg. Override with APP_IDENTITY / INSTALLER_IDENTITY. +# - A notarytool Keychain profile notarizes and staples it, so it opens on any Mac +# without warnings. Create one once with: +# xcrun notarytool store-credentials substratal-notary --team-id --apple-id +# The profile name defaults to substratal-notary; override with NOTARY_PROFILE. +# With none of that, you get an unsigned .pkg that's fine for testing on Macs you control. +# +# The build itself is scripts/build-pkg.sh; this wraps it for a person at a Mac. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +unsigned=false +reveal=false +version="" +for arg in "$@"; do + case "$arg" in + --unsigned) unsigned=true ;; + --open) reveal=true ;; + -h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + -*) echo "dist-local: unknown option $arg (try --help)" >&2; exit 1 ;; + *) version="$arg" ;; + esac +done + +command -v xcodegen >/dev/null || { echo "dist-local: XcodeGen is required: brew install xcodegen" >&2; exit 1; } + +[[ -n "$version" ]] && export VERSION="$version" +export BUILD_NUMBER="${BUILD_NUMBER:-$(date +%Y%m%d%H%M)}" + +find_identity() { # $1 = certificate kind, e.g. "Developer ID Application" + security find-identity -v 2>/dev/null | sed -n "s/.*\"\($1: .*\)\"/\1/p" | head -1 +} + +if $unsigned; then + export APP_IDENTITY="" INSTALLER_IDENTITY="" +else + export APP_IDENTITY="${APP_IDENTITY:-$(find_identity "Developer ID Application")}" + export INSTALLER_IDENTITY="${INSTALLER_IDENTITY:-$(find_identity "Developer ID Installer")}" +fi + +profile="${NOTARY_PROFILE:-substratal-notary}" +unset NOTARY_PROFILE +if [[ -n "$APP_IDENTITY" && -n "$INSTALLER_IDENTITY" ]]; then + # Only notarize with a profile that actually exists. + if xcrun notarytool history --keychain-profile "$profile" >/dev/null 2>&1; then + export NOTARY_PROFILE="$profile" + fi +fi + +echo "Substratal Hub installer" +echo " App signing: ${APP_IDENTITY:-none (ad-hoc)}" +echo " Installer signing: ${INSTALLER_IDENTITY:-none (unsigned .pkg)}" +echo " Notarization: ${NOTARY_PROFILE:-none}" +if [[ -z "$APP_IDENTITY" || -z "$INSTALLER_IDENTITY" ]] && ! $unsigned; then + echo " (No Developer ID certificates found in your Keychain, so this build is for testing only.)" +fi +echo + +pkg="$(scripts/build-pkg.sh | tee /dev/stderr | tail -1)" + +echo +echo "Checks" +echo " Package signature: $(pkgutil --check-signature "$pkg" 2>/dev/null | sed -n 's/^ Status: //p' | head -1 || true)" +if spctl --assess --type install "$pkg" >/dev/null 2>&1; then + echo " Gatekeeper: accepted (opens on any Mac)" +else + echo " Gatekeeper: not accepted; people must right-click → Open, or allow it in System Settings → Privacy & Security" +fi +echo " SHA-256: $(cut -d' ' -f1 "$pkg.sha256")" +echo +echo "Installer: $pkg" +echo "Install: sudo installer -pkg \"$pkg\" -target / (or double-click it)" + +$reveal && open -R "$pkg" +exit 0