Files
pebrel/scripts/package-macos.sh
T

330 lines
10 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=en_US.UTF-8
usage() {
cat <<'EOF'
Usage: scripts/package-macos.sh --binary PATH --version VERSION
[--channel preview --preview-id ID|--channel stable]
--architecture aarch64|x86_64 --build-number NUMBER
[--output-directory DIR] [--force]
[--sign-identity ID --notary-profile PROFILE [--signing-keychain PATH]]
Builds an ad-hoc-signed Pebrel .app and packages it in a DMG. Preview
packages require --preview-id; stable packages use formal bundle metadata and
arm64/x64 asset names.
Developer ID mode requires both signing and notarization; failures never
silently downgrade to an ad-hoc signature.
EOF
}
binary=""
version=""
preview_id=""
channel="preview"
architecture=""
build_number=""
output_directory="dist"
force=0
sign_identity=""
notary_profile=""
signing_keychain=""
while [[ $# -gt 0 ]]; do
case "$1" in
--binary)
binary="${2:-}"
shift 2
;;
--version)
version="${2:-}"
shift 2
;;
--preview-id)
preview_id="${2:-}"
shift 2
;;
--channel)
channel="${2:-}"
shift 2
;;
--architecture)
architecture="${2:-}"
shift 2
;;
--build-number)
build_number="${2:-}"
shift 2
;;
--output-directory)
output_directory="${2:-}"
shift 2
;;
--force)
force=1
shift
;;
--sign-identity) sign_identity="${2:-}"; shift 2 ;;
--notary-profile) notary_profile="${2:-}"; shift 2 ;;
--signing-keychain) signing_keychain="${2:-}"; shift 2 ;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -n "$sign_identity" || -n "$notary_profile" || -n "$signing_keychain" ]]; then
if [[ "$sign_identity" != "Developer ID Application: "* || -z "$notary_profile" ]]; then
echo "Developer ID mode requires a Developer ID Application identity and notary profile" >&2
exit 2
fi
fi
if [[ "$channel" != "preview" && "$channel" != "stable" ]]; then
echo "unsupported package channel: $channel (expected preview or stable)" >&2
exit 2
fi
if [[ "$channel" == "preview" && -z "$preview_id" ]]; then
echo "Preview packages require --preview-id" >&2
exit 2
fi
if [[ "$channel" == "stable" && -n "$preview_id" ]]; then
echo "stable packages must not include --preview-id" >&2
exit 2
fi
if [[ -z "$binary" || -z "$version" || \
-z "$architecture" || -z "$build_number" ]]; then
usage >&2
exit 2
fi
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
echo "invalid Cargo package version: $version" >&2
exit 2
fi
if [[ -n "$preview_id" && ! "$preview_id" =~ ^[0-9A-Za-z][0-9A-Za-z.-]{0,31}$ ]]; then
echo "invalid Preview id: $preview_id" >&2
exit 2
fi
if [[ "$architecture" != "aarch64" && "$architecture" != "x86_64" ]]; then
echo "unsupported macOS package architecture: $architecture" >&2
exit 2
fi
if [[ ! "$build_number" =~ ^[1-9][0-9]*$ ]]; then
echo "CFBundleVersion build number must be a positive integer" >&2
exit 2
fi
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "macOS packages must be built on a native macOS runner" >&2
exit 1
fi
expected_uname="arm64"
if [[ "$architecture" == "x86_64" ]]; then
expected_uname="x86_64"
fi
if [[ "$(uname -m)" != "$expected_uname" ]]; then
echo "runner architecture $(uname -m) does not match requested $architecture" >&2
exit 1
fi
for command in codesign hdiutil iconutil lipo otool plutil shasum sips; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "required packaging command is missing: $command" >&2
exit 1
fi
done
if [[ ! -f "$binary" || ! -x "$binary" ]]; then
echo "Pebrel binary is missing or not executable: $binary" >&2
exit 1
fi
script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
repo="$(cd "$script_directory/.." && pwd -P)"
binary="$(cd "$(dirname "$binary")" && pwd -P)/$(basename "$binary")"
python3 "$script_directory/preview_release.py" --check-binary "$binary"
mkdir -p "$output_directory"
output_directory="$(cd "$output_directory" && pwd -P)"
plist_source="$repo/packaging/macos/Info.plist"
icon_source="$repo/extra/logo/nebula.png"
for required in "$plist_source" "$icon_source" "$repo/README.md" \
"$repo/README.zh-CN.md" \
"$repo/CHANGELOG.md" "$repo/INSTALL.md" "$repo/LICENSE" \
"$repo/THIRD-PARTY-NOTICES" "$repo/licenses/LICENSE-LUA" \
"$repo/licenses/LICENSE-MLUA" "$repo/licenses/LICENSE-LATIN-MODERN-MATH"; do
if [[ ! -f "$required" ]]; then
echo "required package input is missing: $required" >&2
exit 1
fi
done
help_text="$("$binary" --help 2>&1)"
if [[ "$help_text" != *"--gpui"* ]]; then
echo "refusing to package a non-GPUI Pebrel binary" >&2
exit 1
fi
version_text="$("$binary" --version 2>&1)"
if [[ "$version_text" != *"$version"* ]]; then
echo "binary version does not match package version $version: $version_text" >&2
exit 1
fi
binary_architectures="$(lipo -archs "$binary")"
if [[ "$binary_architectures" != "$expected_uname" ]]; then
echo "binary architecture is $binary_architectures; expected only $expected_uname" >&2
exit 1
fi
while IFS= read -r dependency; do
case "$dependency" in
/usr/lib/*|/System/Library/*) ;;
*) echo "non-system Mach-O dependency is not bundled: $dependency" >&2; exit 1 ;;
esac
done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}')
if [[ "$channel" == "stable" ]]; then
release="$version"
bundle_name="Pebrel"
bundle_id="io.github.kuddev.pebrel"
if [[ "$architecture" == "aarch64" ]]; then
asset_architecture="arm64"
else
asset_architecture="x64"
fi
volume_name="Pebrel"
else
release="$version-preview.$preview_id"
bundle_name="Pebrel Preview"
bundle_id="io.github.kuddev.pebrel.preview"
asset_architecture="$architecture"
volume_name="Pebrel Preview"
fi
dmg_path="$output_directory/Pebrel-v$release-macos-$asset_architecture.dmg"
if [[ -e "$dmg_path" && $force -ne 1 ]]; then
echo "package already exists: $dmg_path (pass --force to replace it)" >&2
exit 1
fi
if [[ $force -eq 1 ]]; then
rm -f -- "$dmg_path"
fi
work="$(mktemp -d "${TMPDIR:-/tmp}/pebrel-macos-package.XXXXXX")"
cleanup() {
if [[ -n "${work:-}" && -d "$work" ]]; then
rm -rf -- "$work"
fi
}
trap cleanup EXIT
stage="$work/dmg-root"
app="$stage/$bundle_name.app"
contents="$app/Contents"
resources="$contents/Resources"
mkdir -p "$contents/MacOS" "$resources/docs" "$resources/licenses"
install -m 0755 "$binary" "$contents/MacOS/pebrel"
install -m 0644 "$plist_source" "$contents/Info.plist"
plutil -replace CFBundleShortVersionString -string "$version" "$contents/Info.plist"
plutil -replace CFBundleVersion -string "$build_number" "$contents/Info.plist"
plutil -replace CFBundleGetInfoString -string \
"$bundle_name $version" "$contents/Info.plist"
plutil -replace CFBundleDisplayName -string "$bundle_name" "$contents/Info.plist"
plutil -replace CFBundleName -string "$bundle_name" "$contents/Info.plist"
plutil -replace CFBundleIdentifier -string "$bundle_id" "$contents/Info.plist"
plutil -lint "$contents/Info.plist"
iconset="$work/pebrel.iconset"
mkdir -p "$iconset"
while read -r pixels filename; do
sips -z "$pixels" "$pixels" "$icon_source" --out "$iconset/$filename" >/dev/null
done <<'EOF'
16 icon_16x16.png
32 icon_16x16@2x.png
32 icon_32x32.png
64 icon_32x32@2x.png
128 icon_128x128.png
256 icon_128x128@2x.png
256 icon_256x256.png
512 icon_256x256@2x.png
512 icon_512x512.png
1024 icon_512x512@2x.png
EOF
iconutil -c icns "$iconset" -o "$resources/pebrel.icns"
install -m 0644 "$repo/README.md" "$resources/docs/README.md"
install -m 0644 "$repo/README.zh-CN.md" "$resources/docs/README.zh-CN.md"
install -m 0644 "$repo/CHANGELOG.md" "$resources/docs/CHANGELOG.md"
install -m 0644 "$repo/INSTALL.md" "$resources/docs/INSTALL.md"
install -m 0644 "$repo/LICENSE" "$resources/licenses/LICENSE"
install -m 0644 "$repo/THIRD-PARTY-NOTICES" "$resources/licenses/THIRD-PARTY-NOTICES"
install -m 0644 "$repo/licenses/LICENSE-LUA" "$resources/licenses/LICENSE-LUA"
install -m 0644 "$repo/licenses/LICENSE-MLUA" "$resources/licenses/LICENSE-MLUA"
install -m 0644 "$repo/licenses/LICENSE-LATIN-MODERN-MATH" \
"$resources/licenses/LICENSE-LATIN-MODERN-MATH"
keychain_args=()
if [[ -n "$signing_keychain" ]]; then
keychain_args=(--keychain "$signing_keychain")
fi
if [[ -n "$sign_identity" ]]; then
codesign --force --sign "$sign_identity" --options runtime --timestamp \
${keychain_args[@]+"${keychain_args[@]}"} "$app"
else
codesign --force --sign - --timestamp=none "$app"
fi
codesign --verify --deep --strict --verbose=2 "$app"
if [[ "$(plutil -extract CFBundleIdentifier raw -o - "$contents/Info.plist")" != \
"$bundle_id" ]]; then
echo "application bundle identifier verification failed" >&2
exit 1
fi
if [[ "$(lipo -archs "$contents/MacOS/pebrel")" != "$expected_uname" ]]; then
echo "packaged Mach-O architecture verification failed" >&2
exit 1
fi
ln -s /Applications "$stage/Applications"
df -h "$output_directory" "$work"
du -sh "$stage"
size_mib="$(python3 "$script_directory/macos_dmg.py" "$stage")"
echo "DMG filesystem capacity: ${size_mib} MiB"
if hdiutil create \
-volname "$volume_name" \
-fs HFS+ \
-size "${size_mib}m" \
-srcfolder "$stage" \
-ov \
-format UDZO \
"$dmg_path"; then
:
else
create_status=$?
df -h "$output_directory" "$work"
exit "$create_status"
fi
hdiutil verify "$dmg_path"
if [[ -n "$sign_identity" ]]; then
codesign --force --sign "$sign_identity" --timestamp ${keychain_args[@]+"${keychain_args[@]}"} "$dmg_path"
xcrun notarytool submit "$dmg_path" --keychain-profile "$notary_profile" \
${keychain_args[@]+"${keychain_args[@]}"} --wait --output-format json >"$work/notarization.json"
python3 - "$work/notarization.json" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as stream:
result = json.load(stream)
if result.get("status") != "Accepted":
raise SystemExit(f"Apple notarization did not accept the DMG: {result.get('status')}")
PY
xcrun stapler staple "$dmg_path"
xcrun stapler validate "$dmg_path"
spctl --assess --type open --context context:primary-signature --verbose=2 "$dmg_path"
fi
if [[ ! -s "$dmg_path" ]]; then
echo "DMG output is missing or empty: $dmg_path" >&2
exit 1
fi
shasum -a 256 "$dmg_path"