Files
netdesk/native/packaging/build-macos.sh
T

365 lines
15 KiB
Bash

#!/usr/bin/env bash
#
# NetDesk by Akronlogic -- macOS DESKTOP build script.
#
# Builds the SAME egui viewer/host-panel binary the Windows editions ship
# (crate `netdesk-viewer`) for macOS, as a signed + notarized universal app:
#
# 1. rustup: add the aarch64-apple-darwin + x86_64-apple-darwin std targets.
# 2. cargo build --release -p netdesk-viewer for BOTH targets.
# 3. lipo the two binaries into one universal (arm64 + x86_64) executable.
# 4. iconutil: turn assets/icon*.png into NetDesk.icns.
# 5. Assemble NetDesk.app (Contents/MacOS + Info.plist + Resources/.icns).
# 6. codesign with a "Developer ID Application" identity (hardened runtime).
# 7. notarize the .app via `xcrun notarytool submit --wait`, then staple it.
# 8. Build a .dmg from the stapled .app, sign + notarize + staple the .dmg.
#
# On macOS the H.264 stream is decoded on the Apple video engine through the
# VideoToolbox path (core/src/decode_vt.rs, VTDecompressionSession); it falls
# back to the portable openh264 software decoder if the hardware path can't
# initialize. Nothing here is Mac-specific beyond packaging -- the viewer code
# is identical to Windows. See docs/build-macos.md.
#
# THIS SCRIPT RUNS ON macOS (the user's M4 Mac), NOT on the Windows dev box.
# All outputs land in packaging/dist/.
#
# Usage (from the repo root or anywhere):
# bash packaging/build-macos.sh --notary-profile netdesk
# bash packaging/build-macos.sh --skip-build # reuse existing targets
# bash packaging/build-macos.sh --no-sign # unsigned .app + .dmg
# bash packaging/build-macos.sh --no-notarize # sign but don't notarize
# bash packaging/build-macos.sh --identity "Developer ID Application: Akronlogic (TEAMID)" \
# --apple-id you@akronlogic.com --team-id TEAMID
#
# Notarization credentials (pick ONE):
# --notary-profile <name> a keychain profile created once with:
# xcrun notarytool store-credentials <name> --apple-id <e> --team-id <t> --password <app-pw>
# --apple-id <email> --team-id <TEAMID> + env NETDESK_NOTARY_PASSWORD=<app-specific-password>
#
set -euo pipefail
# ---------------------------------------------------------------------------
# defaults / args
# ---------------------------------------------------------------------------
BUNDLE_ID="com.akronlogic.netdesk"
APP_NAME="NetDesk"
MIN_MACOS="11.0" # Big Sur: oldest OS the universal app targets
CATEGORY="public.app-category.utilities"
COPYRIGHT="© Akronlogic. All rights reserved."
IDENTITY="" # "Developer ID Application: ... (TEAMID)" (empty = auto-detect)
APPLE_ID=""
TEAM_ID=""
NOTARY_PROFILE=""
ENTITLEMENTS="" # optional path to an entitlements .plist
ICON_SRC="" # optional path to a master PNG (>= 512px, ideally 1024)
VERSION="" # override the Cargo workspace version
SKIP_BUILD=0
NO_SIGN=0
NO_NOTARIZE=0
STAPLE_APP=1 # also notarize+staple the standalone .app (not just the .dmg)
while [ $# -gt 0 ]; do
case "$1" in
--identity) IDENTITY="$2"; shift 2 ;;
--apple-id) APPLE_ID="$2"; shift 2 ;;
--team-id) TEAM_ID="$2"; shift 2 ;;
--notary-profile) NOTARY_PROFILE="$2"; shift 2 ;;
--entitlements) ENTITLEMENTS="$2"; shift 2 ;;
--icon) ICON_SRC="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--skip-build) SKIP_BUILD=1; shift ;;
--no-sign) NO_SIGN=1; shift ;;
--no-notarize) NO_NOTARIZE=1; shift ;;
--no-staple-app) STAPLE_APP=0; shift ;;
-h|--help)
sed -n '2,46p' "$0"; exit 0 ;;
*) echo "unknown option: $1" >&2; exit 2 ;;
esac
done
# ---------------------------------------------------------------------------
# pretty output
# ---------------------------------------------------------------------------
if [ -t 1 ]; then
C_CYAN='\033[36m'; C_YEL='\033[33m'; C_GRN='\033[32m'; C_RED='\033[31m'; C_DIM='\033[2m'; C_OFF='\033[0m'
else
C_CYAN=''; C_YEL=''; C_GRN=''; C_RED=''; C_DIM=''; C_OFF=''
fi
say() { printf "%b%s%b\n" "$C_CYAN" "$*" "$C_OFF"; }
step() { printf "\n%b%s%b\n" "$C_YEL" "$*" "$C_OFF"; }
ok() { printf "%b%s%b\n" "$C_GRN" "$*" "$C_OFF"; }
dim() { printf "%b %s%b\n" "$C_DIM" "$*" "$C_OFF"; }
warn() { printf "%b%s%b\n" "$C_RED" "$*" "$C_OFF"; }
die() { printf "%b%s%b\n" "$C_RED" "$*" "$C_OFF" >&2; exit 1; }
# ---------------------------------------------------------------------------
# paths
# ---------------------------------------------------------------------------
PKG_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$PKG_DIR/.." && pwd)"
DIST_DIR="$PKG_DIR/dist"
ASSETS_DIR="$ROOT_DIR/assets"
APP_DIR="$DIST_DIR/$APP_NAME.app"
MACOS_DIR="$APP_DIR/Contents/MacOS"
RES_DIR="$APP_DIR/Contents/Resources"
PLIST="$APP_DIR/Contents/Info.plist"
EXE_OUT="$MACOS_DIR/$APP_NAME"
TARGET_ARM="aarch64-apple-darwin"
TARGET_X64="x86_64-apple-darwin"
BIN_ARM="$ROOT_DIR/target/$TARGET_ARM/release/netdesk-viewer"
BIN_X64="$ROOT_DIR/target/$TARGET_X64/release/netdesk-viewer"
DMG_STAGE="$DIST_DIR/dmg-stage"
DMG_OUT="$DIST_DIR/$APP_NAME-macOS-universal.dmg"
APP_ZIP="$DIST_DIR/$APP_NAME-macOS-universal.zip"
say "=== NetDesk macOS build (Akronlogic) ==="
dim "root : $ROOT_DIR"
dim "dist : $DIST_DIR"
# ---------------------------------------------------------------------------
# preflight
# ---------------------------------------------------------------------------
[ "$(uname -s)" = "Darwin" ] || die "This script must run on macOS (uname is '$(uname -s)'). Build on the Mac, not the Windows box."
need() { command -v "$1" >/dev/null 2>&1 || die "required tool not found on PATH: $1"; }
need cargo
need rustup
need lipo
need iconutil
need sips
need hdiutil
need codesign
need xcrun
xcode-select -p >/dev/null 2>&1 || die "Xcode command-line tools missing. Run: xcode-select --install"
# version from the Cargo workspace unless overridden
if [ -z "$VERSION" ]; then
VERSION="$(awk -F'"' '/^\[workspace.package\]/{f=1} f&&/^version[[:space:]]*=/{print $2; exit}' "$ROOT_DIR/Cargo.toml")"
[ -n "$VERSION" ] || VERSION="0.0.0"
fi
dim "version : $VERSION"
dim "bundle : $BUNDLE_ID"
mkdir -p "$DIST_DIR"
# ---------------------------------------------------------------------------
# 1 + 2. rust targets + release builds (both arches)
# ---------------------------------------------------------------------------
if [ "$SKIP_BUILD" -eq 0 ]; then
step "[1/8] rustup target add ($TARGET_ARM, $TARGET_X64)"
rustup target add "$TARGET_ARM" "$TARGET_X64"
step "[2/8] cargo build --release -p netdesk-viewer (both arches)"
( cd "$ROOT_DIR" && cargo build --release -p netdesk-viewer --target "$TARGET_ARM" )
( cd "$ROOT_DIR" && cargo build --release -p netdesk-viewer --target "$TARGET_X64" )
else
step "[1-2/8] --skip-build: reusing existing per-target binaries"
fi
[ -f "$BIN_ARM" ] || die "missing arm64 binary: $BIN_ARM"
[ -f "$BIN_X64" ] || die "missing x86_64 binary: $BIN_X64"
# ---------------------------------------------------------------------------
# 3. lipo -> universal binary
# ---------------------------------------------------------------------------
step "[3/8] lipo -> universal (arm64 + x86_64)"
rm -rf "$APP_DIR"
mkdir -p "$MACOS_DIR" "$RES_DIR"
lipo -create -output "$EXE_OUT" "$BIN_ARM" "$BIN_X64"
chmod +x "$EXE_OUT"
dim "$(lipo -info "$EXE_OUT")"
# ---------------------------------------------------------------------------
# 4. icns via iconutil
# ---------------------------------------------------------------------------
step "[4/8] iconutil -> $APP_NAME.icns"
# Prefer the largest master we have: an explicit --icon, else a 1024 PNG if it
# exists, else the 512, else the 256 (assets/icon.png). iconutil needs a full
# .iconset of fixed-named PNGs; sips resizes the master down into each slot.
if [ -z "$ICON_SRC" ]; then
for c in "$ASSETS_DIR/icon-1024.png" "$ASSETS_DIR/icon-512.png" "$ASSETS_DIR/icon.png"; do
if [ -f "$c" ]; then ICON_SRC="$c"; break; fi
done
fi
[ -n "$ICON_SRC" ] && [ -f "$ICON_SRC" ] || die "no icon PNG found (looked for assets/icon-1024.png, icon-512.png, icon.png; or pass --icon)"
dim "icon master: $ICON_SRC"
ICONSET="$DIST_DIR/$APP_NAME.iconset"
rm -rf "$ICONSET"; mkdir -p "$ICONSET"
# name px
gen() { sips -z "$2" "$2" "$ICON_SRC" --out "$ICONSET/$1" >/dev/null; }
gen icon_16x16.png 16
gen icon_16x16@2x.png 32
gen icon_32x32.png 32
gen icon_32x32@2x.png 64
gen icon_128x128.png 128
gen icon_128x128@2x.png 256
gen icon_256x256.png 256
gen icon_256x256@2x.png 512
gen icon_512x512.png 512
gen icon_512x512@2x.png 1024
iconutil -c icns "$ICONSET" -o "$RES_DIR/$APP_NAME.icns"
rm -rf "$ICONSET"
dim "wrote $RES_DIR/$APP_NAME.icns"
# ---------------------------------------------------------------------------
# 5. Info.plist + PkgInfo (assemble the bundle)
# ---------------------------------------------------------------------------
step "[5/8] assemble $APP_NAME.app"
cat > "$PLIST" <<PLIST_EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key> <string>$APP_NAME</string>
<key>CFBundleDisplayName</key> <string>$APP_NAME</string>
<key>CFBundleIdentifier</key> <string>$BUNDLE_ID</string>
<key>CFBundleExecutable</key> <string>$APP_NAME</string>
<key>CFBundleIconFile</key> <string>$APP_NAME</string>
<key>CFBundleVersion</key> <string>$VERSION</string>
<key>CFBundleShortVersionString</key> <string>$VERSION</string>
<key>CFBundlePackageType</key> <string>APPL</string>
<key>CFBundleSignature</key> <string>????</string>
<key>CFBundleInfoDictionaryVersion</key> <string>6.0</string>
<key>LSMinimumSystemVersion</key> <string>$MIN_MACOS</string>
<key>LSApplicationCategoryType</key> <string>$CATEGORY</string>
<key>NSHighResolutionCapable</key> <true/>
<key>NSHumanReadableCopyright</key> <string>$COPYRIGHT</string>
<key>NSSupportsAutomaticGraphicsSwitching</key> <true/>
</dict>
</plist>
PLIST_EOF
printf 'APPL????' > "$APP_DIR/Contents/PkgInfo"
ok "bundle staged: $APP_DIR"
# ---------------------------------------------------------------------------
# resolve signing identity
# ---------------------------------------------------------------------------
if [ "$NO_SIGN" -eq 0 ] && [ -z "$IDENTITY" ]; then
# auto-detect a single "Developer ID Application" identity from the keychain
IDENTITY="$(security find-identity -v -p codesigning 2>/dev/null \
| sed -n 's/.*"\(Developer ID Application: [^"]*\)".*/\1/p' | head -n1 || true)"
fi
CAN_SIGN=0
if [ "$NO_SIGN" -eq 0 ] && [ -n "$IDENTITY" ]; then CAN_SIGN=1; fi
# ---------------------------------------------------------------------------
# 6. codesign (hardened runtime + secure timestamp)
# ---------------------------------------------------------------------------
if [ "$CAN_SIGN" -eq 1 ]; then
step "[6/8] codesign ($IDENTITY)"
CS_ARGS="--force --timestamp --options runtime --sign $IDENTITY"
if [ -n "$ENTITLEMENTS" ]; then
[ -f "$ENTITLEMENTS" ] || die "entitlements file not found: $ENTITLEMENTS"
dim "entitlements: $ENTITLEMENTS"
fi
# single-binary bundle (no nested frameworks/dylibs): sign the executable,
# then the bundle. --options runtime is required for notarization.
if [ -n "$ENTITLEMENTS" ]; then
codesign --force --timestamp --options runtime --entitlements "$ENTITLEMENTS" --sign "$IDENTITY" "$EXE_OUT"
codesign --force --timestamp --options runtime --entitlements "$ENTITLEMENTS" --sign "$IDENTITY" "$APP_DIR"
else
codesign --force --timestamp --options runtime --sign "$IDENTITY" "$EXE_OUT"
codesign --force --timestamp --options runtime --sign "$IDENTITY" "$APP_DIR"
fi
codesign --verify --deep --strict --verbose=2 "$APP_DIR"
dim "signed + verified"
else
step "[6/8] codesign -- SKIPPED"
if [ "$NO_SIGN" -eq 1 ]; then
warn " --no-sign set: the .app and .dmg are UNSIGNED (Gatekeeper will block them on other Macs)."
else
warn " No \"Developer ID Application\" identity found in the keychain -- producing an UNSIGNED bundle."
warn " Install your Developer ID Application cert, or pass --identity \"Developer ID Application: NAME (TEAMID)\"."
fi
fi
# ---------------------------------------------------------------------------
# notarization helper
# ---------------------------------------------------------------------------
notary_auth_ok=0
notary_args=""
if [ -n "$NOTARY_PROFILE" ]; then
notary_args="--keychain-profile $NOTARY_PROFILE"; notary_auth_ok=1
elif [ -n "$APPLE_ID" ] && [ -n "$TEAM_ID" ] && [ -n "${NETDESK_NOTARY_PASSWORD:-}" ]; then
notary_args="--apple-id $APPLE_ID --team-id $TEAM_ID --password $NETDESK_NOTARY_PASSWORD"; notary_auth_ok=1
fi
CAN_NOTARIZE=0
if [ "$CAN_SIGN" -eq 1 ] && [ "$NO_NOTARIZE" -eq 0 ] && [ "$notary_auth_ok" -eq 1 ]; then CAN_NOTARIZE=1; fi
notarize() { # $1 = path to .zip/.dmg to submit
# shellcheck disable=SC2086
xcrun notarytool submit "$1" $notary_args --wait
}
# ---------------------------------------------------------------------------
# 7. notarize + staple the standalone .app (via a zip; stapling needs the app)
# ---------------------------------------------------------------------------
if [ "$CAN_NOTARIZE" -eq 1 ] && [ "$STAPLE_APP" -eq 1 ]; then
step "[7/8] notarize + staple $APP_NAME.app"
rm -f "$APP_ZIP"
/usr/bin/ditto -c -k --keepParent "$APP_DIR" "$APP_ZIP"
notarize "$APP_ZIP"
xcrun stapler staple "$APP_DIR"
xcrun stapler validate "$APP_DIR"
dim "stapled $APP_NAME.app"
else
step "[7/8] notarize + staple .app -- SKIPPED"
if [ "$CAN_SIGN" -eq 0 ]; then
dim "(unsigned bundle cannot be notarized)"
elif [ "$NO_NOTARIZE" -eq 1 ]; then
dim "(--no-notarize set)"
elif [ "$notary_auth_ok" -eq 0 ]; then
warn " No notarization credentials. Pass --notary-profile <name>, or"
warn " --apple-id <e> --team-id <t> with env NETDESK_NOTARY_PASSWORD=<app-pw>."
warn " Create a profile once with: xcrun notarytool store-credentials <name> --apple-id <e> --team-id <t> --password <app-pw>"
fi
fi
# ---------------------------------------------------------------------------
# 8. build the .dmg, then sign + notarize + staple it
# ---------------------------------------------------------------------------
step "[8/8] build $APP_NAME-macOS-universal.dmg"
rm -rf "$DMG_STAGE"; mkdir -p "$DMG_STAGE"
/usr/bin/ditto "$APP_DIR" "$DMG_STAGE/$APP_NAME.app"
ln -s /Applications "$DMG_STAGE/Applications"
rm -f "$DMG_OUT"
hdiutil create -volname "$APP_NAME" -srcfolder "$DMG_STAGE" -fs HFS+ -format UDZO -ov "$DMG_OUT" >/dev/null
rm -rf "$DMG_STAGE"
dim "wrote $DMG_OUT"
if [ "$CAN_SIGN" -eq 1 ]; then
codesign --force --timestamp --sign "$IDENTITY" "$DMG_OUT"
dim "signed the .dmg"
fi
if [ "$CAN_NOTARIZE" -eq 1 ]; then
dim "notarizing the .dmg ..."
notarize "$DMG_OUT"
xcrun stapler staple "$DMG_OUT"
xcrun stapler validate "$DMG_OUT"
dim "stapled the .dmg"
fi
# ---------------------------------------------------------------------------
# summary
# ---------------------------------------------------------------------------
printf "\n"
ok "macOS build complete."
dim "app : $APP_DIR"
[ -f "$DMG_OUT" ] && dim "dmg : $DMG_OUT ($(du -h "$DMG_OUT" | cut -f1))"
if [ "$CAN_SIGN" -eq 0 ]; then
warn "NOTE: artifacts are UNSIGNED -- for distribution, re-run on a Mac with a Developer ID Application cert."
elif [ "$CAN_NOTARIZE" -eq 0 ]; then
warn "NOTE: signed but NOT notarized -- re-run with notarization credentials (see --help) before distributing."
else
ok "Signed, notarized, and stapled -- ready to distribute."
fi