Files
openworker/packaging/build_macos_cli.sh
Rohit C Prasad 7a9d2e151a Mac program: resolve links before removing Python.framework
With a framework Python the interpreter is a link into the framework, so the program could not start.
The program is also started once before signing.
2026-10-05 16:55:33 -07:00

118 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build the macOS `openworker` program: one folder with the program and its support files
# (no Python needed on the Mac), packed as openworker-<version>-macos-<arch>.tar.gz.
#
# 1. PyInstaller with packaging/openworker-server.spec, OPENWORKER_BUNDLE=cli: the whole
# command line as the entry point (the same freeze as the Linux program).
# 2. With APPLE_SIGNING_IDENTITY set: sign every Mach-O file (hardened runtime), submit
# the folder to Apple's notary service, and check it the way Gatekeeper does. The
# ticket for a command-line program lives with Apple, not in the files, so there is
# nothing to staple. Without the identity the program is unsigned: fine for your own
# Mac, not for distribution.
# 3. Pack the folder and write its SHA-256 next to it, in packaging/dist/.
#
# Prerequisites: the .venv that build_dmg.sh uses (the package with the bedrock and
# openshell extras, pyinstaller, typer), Xcode command line tools. Notary credentials as
# for build_dmg.sh: NOTARYTOOL_API_KEY_PATH / _KEY_ID / _ISSUER_ID, the APPLE_API_* names
# from CI, or `.ocw-notary.env` one directory above the repo. OCW_SKIP_NOTARIZE=1 signs
# but does not notarize.
#
# Install on a Mac (packaging/install.sh does this):
# tar -xzf openworker-<version>-macos-<arch>.tar.gz -C ~/.local/share
# ln -sf ~/.local/share/openworker/openworker ~/.local/bin/openworker
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(dirname "$HERE")"
OUT="$HERE/dist"
VENV="$ROOT/.venv"
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' "$ROOT/pyproject.toml" | head -1)"
ARCH="$(uname -m)" # arm64 or x86_64
NAME="openworker-$VERSION-macos-$ARCH"
PROGRAM="$OUT/openworker"
[ "$(uname -s)" = "Darwin" ] || { echo "ERROR: this script builds on macOS only (Linux: build_linux.sh)" >&2; exit 1; }
[ -x "$VENV/bin/pyinstaller" ] || { echo "ERROR: $VENV has no pyinstaller; see the VENV PREREQS in build_dmg.sh" >&2; exit 1; }
"$VENV/bin/python" -c "import grpc" 2>/dev/null || { echo "ERROR: grpcio is missing from .venv; install the openshell extra" >&2; exit 1; }
mkdir -p "$OUT"
rm -rf "$PROGRAM" "$OUT/$NAME.tar.gz" "$OUT/$NAME.tar.gz.sha256" "$OUT/$NAME.zip"
echo "==> [1/3] PyInstaller: $NAME"
OPENWORKER_BUNDLE=cli "$VENV/bin/pyinstaller" --noconfirm --clean --log-level WARN \
--distpath "$OUT" --workpath "$HERE/build" "$HERE/openworker-server.spec"
# Resolve every link into a real file first. With a framework Python (python.org, Homebrew,
# the CI runner's) PyInstaller's _internal/Python is a link INTO Python.framework, and the
# framework must then go: codesign and the notary service treat it as a bundle, which this
# flattened layout can never satisfy (the sidecar learned this; see build_dmg.sh). Once the
# links are resolved the framework is only a duplicate of _internal/Python.
cp -RL "$PROGRAM" "$PROGRAM.flat"
rm -rf "$PROGRAM"
mv "$PROGRAM.flat" "$PROGRAM"
rm -rf "$PROGRAM/_internal/Python.framework"
if [ -n "$(find "$PROGRAM" -type d -name "*.framework" | head -1)" ]; then
echo "ERROR: a .framework is in the program folder; it cannot pass notarization" >&2
exit 1
fi
chmod +x "$PROGRAM/openworker"
# The program must start before anything is signed or submitted.
"$PROGRAM/openworker" version
if [ -n "${APPLE_SIGNING_IDENTITY:-}" ]; then
echo "==> [2/3] signing"
# Every Mach-O gets its own file signature with the hardened runtime; the entry point
# also gets the entitlements (disable-library-validation: the bundled Python libraries
# carry python.org's Team ID).
find "$PROGRAM" -type f ! -name "openworker" \
! -name "*.py" ! -name "*.pyc" ! -name "*.txt" ! -name "*.pem" ! -name "*.json" \
-print0 | while IFS= read -r -d '' f; do
file -b "$f" | grep -q "Mach-O" || continue
codesign --force --sign "$APPLE_SIGNING_IDENTITY" --timestamp --options runtime "$f"
done
codesign --force --sign "$APPLE_SIGNING_IDENTITY" --timestamp --options runtime \
--entitlements "$ROOT/surfaces/gui/src-tauri/entitlements.plist" "$PROGRAM/openworker"
if [ "${OCW_SKIP_NOTARIZE:-}" = "1" ]; then
echo " OCW_SKIP_NOTARIZE=1: signed, NOT notarized (do not distribute)"
else
NOTARYTOOL_API_KEY_PATH="${NOTARYTOOL_API_KEY_PATH:-${APPLE_API_KEY_PATH:-}}"
NOTARYTOOL_API_KEY_ID="${NOTARYTOOL_API_KEY_ID:-${APPLE_API_KEY:-}}"
NOTARYTOOL_API_ISSUER_ID="${NOTARYTOOL_API_ISSUER_ID:-${APPLE_API_ISSUER:-}}"
NOTARY_ENV="${OCW_NOTARY_ENV:-$ROOT/../.ocw-notary.env}"
if [ -z "${NOTARYTOOL_API_KEY_PATH:-}" ] && [ -f "$NOTARY_ENV" ]; then
set -a; # shellcheck disable=SC1090
source "$NOTARY_ENV"; set +a
fi
if [ -n "${NOTARYTOOL_API_KEY_PATH:-}" ] && [ -n "${NOTARYTOOL_API_KEY_ID:-}" ] \
&& [ -n "${NOTARYTOOL_API_ISSUER_ID:-}" ]; then
echo " notarizing"
# The notary service takes a zip of the folder. The ticket is recorded with Apple;
# the files themselves do not change, so the tarball packed below is what was checked.
ditto -c -k --keepParent "$PROGRAM" "$OUT/$NAME.zip"
xcrun notarytool submit "$OUT/$NAME.zip" \
--key "$NOTARYTOOL_API_KEY_PATH" \
--key-id "$NOTARYTOOL_API_KEY_ID" \
--issuer "$NOTARYTOOL_API_ISSUER_ID" \
--wait
rm -f "$OUT/$NAME.zip"
# `notarytool submit --wait` exits non-zero when Apple does not accept. spctl cannot
# assess a bare command-line program ("does not seem to be an app"), so the local
# check is the signature itself.
codesign --verify --strict --deep -v "$PROGRAM/openworker"
echo " notarized and accepted"
else
echo " WARNING: signed but NOT notarized. Provide NOTARYTOOL_API_KEY_PATH/_KEY_ID/_ISSUER_ID"
echo " (env, \$OCW_NOTARY_ENV, or $NOTARY_ENV)."
fi
fi
else
echo "==> [2/3] unsigned (set APPLE_SIGNING_IDENTITY for a distributable program)"
fi
echo "==> [3/3] packing"
"$PROGRAM/openworker" version
tar -C "$OUT" -czf "$OUT/$NAME.tar.gz" openworker
(cd "$OUT" && shasum -a 256 "$NAME.tar.gz" > "$NAME.tar.gz.sha256")
ls -la "$OUT/$NAME.tar.gz"
cat "$OUT/$NAME.tar.gz.sha256"