#!/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 a keychain profile created once with: # xcrun notarytool store-credentials --apple-id --team-id --password # --apple-id --team-id + env NETDESK_NOTARY_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" < CFBundleName $APP_NAME CFBundleDisplayName $APP_NAME CFBundleIdentifier $BUNDLE_ID CFBundleExecutable $APP_NAME CFBundleIconFile $APP_NAME CFBundleVersion $VERSION CFBundleShortVersionString $VERSION CFBundlePackageType APPL CFBundleSignature ???? CFBundleInfoDictionaryVersion 6.0 LSMinimumSystemVersion $MIN_MACOS LSApplicationCategoryType $CATEGORY NSHighResolutionCapable NSHumanReadableCopyright $COPYRIGHT NSSupportsAutomaticGraphicsSwitching 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 , or" warn " --apple-id --team-id with env NETDESK_NOTARY_PASSWORD=." warn " Create a profile once with: xcrun notarytool store-credentials --apple-id --team-id --password " 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