365 lines
15 KiB
Bash
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
|