330 lines
10 KiB
Bash
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"
|