Files
netdesk/native/docs/build-macos.md
T

13 KiB
Raw Blame History

Building the NetDesk desktop app for macOS

This builds the same egui viewer/host-panel binary the Windows editions ship (crate netdesk-viewer) as a signed, notarized universal macOS app (NetDesk.app) and a distributable .dmg.

  • One binary, two roles: the viewer window and the host panel, identical to the Windows build — only the packaging here is Mac-specific.
  • Decode path: on macOS the host H.264 (Annex-B) stream is decoded on the Apple video engine through VideoToolbox — core/src/decode_vt.rs (VTDecompressionSession, 32-bit BGRA output repacked to RGB8), gated #[cfg(any(target_os = "macos", target_os = "ios"))]. It falls back to the portable openh264 software decoder if the hardware path can't initialize. See cross-platform-decode.md. No VideoToolbox- specific flags are needed at build time: the CoreMedia / CoreVideo / VideoToolbox framework #[link]s resolve against the active SDK.

Where this runs: on the Mac (the M4), not the Windows dev box. Cross- compiling a macOS .app from Windows is not supported — codesign, notarytool, iconutil, lipo and hdiutil are all macOS-only tools.

The script is packaging/build-macos.sh. All outputs land in packaging/dist/.


TL;DR — the Mac build command

Once the one-time Apple Developer setup below is done (Developer ID cert in the keychain + a stored notarytool profile named netdesk), the whole signed + notarized build is:

# on the Mac, from the repo root:
export PATH="$HOME/.cargo/bin:$PATH"
bash packaging/build-macos.sh --notary-profile netdesk

That produces, in packaging/dist/:

Artifact What it is
NetDesk.app signed + notarized + stapled universal app
NetDesk-macOS-universal.dmg signed + notarized + stapled disk image (ship this)

Useful flags: --skip-build (reuse the per-target binaries), --no-sign (local unsigned test build), --no-notarize (sign but skip the Apple round- trip), --identity "…", --icon <png>, --version <x.y.z>, --entitlements <plist>. Run bash packaging/build-macos.sh --help for the full list.


Prerequisites (on the Mac)

  1. Xcode command-line tools (gives clang, the SDKs, lipo, iconutil, sips, hdiutil, codesign, xcrun):

    xcode-select --install
    

    A full Xcode install from the App Store also works.

  2. Rust via rustup. The script adds the two Apple-silicon/Intel std targets for you, but you can pre-add them:

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    rustup target add aarch64-apple-darwin x86_64-apple-darwin
    
  3. An Apple Developer Program membership (needed only for signing + notarization — a --no-sign build needs none). Set that up once, below.


One-time Apple Developer setup

You need a Developer ID Application certificate (this is the identity that lets an app run on any Mac after notarization — distinct from the "Apple Development" / "Apple Distribution" / Mac App Store certs) and a way for notarytool to authenticate.

1. Enrol in the Apple Developer Program

https://developer.apple.com/programs/ — $99/yr. You must be the Account Holder (or have the Admin role) to create a Developer ID certificate.

2. Create the "Developer ID Application" certificate

Easiest, via Xcode:

  1. Xcode → Settings… → Accounts, sign in with the Apple ID on the team.
  2. Select the team → Manage Certificates…
  3. Click + → Developer ID Application.
  4. Xcode creates the private key + cert and installs both into your login keychain. (Back up the private key — it cannot be re-downloaded.)

Or manually at https://developer.apple.com/account/resources/certificates → + → Developer ID → Developer ID Application, upload a CSR made with Keychain Access → Certificate Assistant → Request a Certificate from a Certificate Authority, then double-click the downloaded .cer to install it.

Verify it is present and usable for signing:

security find-identity -v -p codesigning
# look for a line like:
#   1) ABCD…  "Developer ID Application: Akronlogic LLC (TEAMID1234)"

The script auto-detects this identity; override with --identity "Developer ID Application: Akronlogic LLC (TEAMID1234)" if you have more than one.

3. Find your Team ID

https://developer.apple.com/account → Membership details → Team ID (a 10-character string, also shown in the parentheses of the identity name above).

4. Create credentials for notarytool

Two supported options — pick one:

A. App-specific password (simplest). At https://appleid.apple.com → Sign-In and Security → App-Specific Passwords → +, label it e.g. netdesk-notarytool. Then store it once in the keychain as a named profile:

xcrun notarytool store-credentials netdesk \
  --apple-id "you@akronlogic.com" \
  --team-id  "TEAMID1234" \
  --password "abcd-efgh-ijkl-mnop"      # the app-specific password

netdesk is the profile name you pass to --notary-profile netdesk.

B. App Store Connect API key. Create a key at App Store Connect → Users and Access → Integrations → App Store Connect API (role: Developer), download the AuthKey_XXXX.p8, and store it:

xcrun notarytool store-credentials netdesk \
  --key    "/path/AuthKey_XXXXXXXX.p8" \
  --key-id "XXXXXXXXXX" \
  --issuer "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"

Instead of a stored profile you can pass credentials inline each run: --apple-id you@akronlogic.com --team-id TEAMID1234 plus the app-specific password in the environment: export NETDESK_NOTARY_PASSWORD='abcd-efgh-…'.


What the script does (step by step)

packaging/build-macos.sh:

  1. rustup target add aarch64-apple-darwin x86_64-apple-darwin.
  2. cargo build --release -p netdesk-viewer once per target — producing target/aarch64-apple-darwin/release/netdesk-viewer and the x86_64 one. (Cross-building the Intel slice on an Apple-silicon Mac uses the same SDK and needs no extra tooling.)
  3. lipo -create the two into one universal Mach-O at NetDesk.app/Contents/MacOS/NetDesk (lipo -info prints arm64 x86_64).
  4. iconutil builds NetDesk.icns from the largest PNG master it finds (assets/icon-1024.png → icon-512.png → icon.png, or --icon <png>): sips resizes the master into a complete .iconset (16→1024 px, @1x/@2x), then iconutil -c icns packs it into Contents/Resources/NetDesk.icns.
  5. Assembles NetDesk.app — writes Contents/Info.plist with bundle id com.akronlogic.netdesk, CFBundleVersion / CFBundleShortVersionString = the workspace version (read from Cargo.toml, currently 0.1.0), CFBundleExecutable/CFBundleIconFile = NetDesk, LSMinimumSystemVersion 11.0, NSHighResolutionCapable, plus a PkgInfo (APPL????).
  6. codesign the executable and the bundle with the Developer ID Application identity, --options runtime (hardened runtime, required to notarize) and --timestamp (secure timestamp), then codesign --verify --deep --strict.
  7. Notarize + staple the .app: ditto -c -k --keepParent it into a zip, xcrun notarytool submit … --wait, then xcrun stapler staple NetDesk.app so the ticket travels with the app offline. (Skip with --no-staple-app.)
  8. Build the .dmg (hdiutil create, UDZO, with an /Applications symlink for drag-install), sign it, notarize it (notarytool submit … --wait), and staple it.

If no Developer ID identity is found (or --no-sign), it still produces an unsigned .app + .dmg and tells you so — handy for a local smoke test. If signing works but no notarization credentials are present, it signs and prints exactly how to add them, like the Windows installer script does when NSIS is missing.


Exact commands (equivalent to what the script runs)

If you want to drive it by hand, this is the full sequence (VERSION=0.1.0, ID="Developer ID Application: Akronlogic LLC (TEAMID1234)"):

export PATH="$HOME/.cargo/bin:$PATH"

# 1-2. targets + release builds
rustup target add aarch64-apple-darwin x86_64-apple-darwin
cargo build --release -p netdesk-viewer --target aarch64-apple-darwin
cargo build --release -p netdesk-viewer --target x86_64-apple-darwin

# 3. universal binary
mkdir -p packaging/dist/NetDesk.app/Contents/MacOS packaging/dist/NetDesk.app/Contents/Resources
lipo -create -output packaging/dist/NetDesk.app/Contents/MacOS/NetDesk \
  target/aarch64-apple-darwin/release/netdesk-viewer \
  target/x86_64-apple-darwin/release/netdesk-viewer

# 4. icon  (ICONSET fully populated 16..1024 as in the script)
iconutil -c icns NetDesk.iconset -o packaging/dist/NetDesk.app/Contents/Resources/NetDesk.icns

# 5. Info.plist  (the script writes com.akronlogic.netdesk + version 0.1.0)

# 6. sign (hardened runtime + timestamp)
codesign --force --timestamp --options runtime --sign "$ID" packaging/dist/NetDesk.app/Contents/MacOS/NetDesk
codesign --force --timestamp --options runtime --sign "$ID" packaging/dist/NetDesk.app
codesign --verify --deep --strict --verbose=2 packaging/dist/NetDesk.app

# 7. notarize + staple the app
ditto -c -k --keepParent packaging/dist/NetDesk.app packaging/dist/NetDesk.zip
xcrun notarytool submit packaging/dist/NetDesk.zip --keychain-profile netdesk --wait
xcrun stapler staple packaging/dist/NetDesk.app

# 8. dmg -> sign -> notarize -> staple
hdiutil create -volname NetDesk -srcfolder <stage-with-app+Applications-symlink> \
  -fs HFS+ -format UDZO -ov packaging/dist/NetDesk-macOS-universal.dmg
codesign --force --timestamp --sign "$ID" packaging/dist/NetDesk-macOS-universal.dmg
xcrun notarytool submit packaging/dist/NetDesk-macOS-universal.dmg --keychain-profile netdesk --wait
xcrun stapler staple packaging/dist/NetDesk-macOS-universal.dmg

The script wraps all of this (with the full .iconset generation, the plist, auto-detection, and graceful fallbacks) — prefer it over the manual steps.


Verify a finished build

# signature valid + hardened runtime + Developer-ID authority
codesign --verify --deep --strict --verbose=2 packaging/dist/NetDesk.app
codesign -dv --verbose=4 packaging/dist/NetDesk.app 2>&1 | grep -E 'Authority|Timestamp|flags|Identifier'

# Gatekeeper will accept it (Developer ID + notarized)
spctl -a -vvv --type exec packaging/dist/NetDesk.app

# notarization ticket is stapled (works offline)
xcrun stapler validate packaging/dist/NetDesk.app
xcrun stapler validate packaging/dist/NetDesk-macOS-universal.dmg

spctl should print source=Notarized Developer ID and accepted.


Runtime permissions (host mode)

Signing/notarization are not the macOS capture permissions. When the binary runs as the host and first tries to capture the screen or inject input, macOS prompts the user (TCC) to grant, under System Settings → Privacy & Security:

  • Screen Recording — to capture the remote screen.
  • Accessibility — to inject mouse/keyboard input (CGEvent).

These are user-granted at runtime and are not code-signing entitlements, so the default build needs no entitlements file. (If a future host feature drives other apps via Apple Events, add an entitlements plist with the matching keys and pass it via --entitlements.)


Troubleshooting

  • no identity found / unsigned output — the Developer ID Application cert (and its private key) must be in the login keychain of the user running the build; confirm with security find-identity -v -p codesigning.
  • Notarization Invalid / rejected — fetch the log: xcrun notarytool log <submission-id> --keychain-profile netdesk. Almost always a missing --options runtime (hardened runtime) or a missing secure --timestamp; the script sets both.
  • The binary is not signed with a valid Developer ID certificate — an Apple Development cert was used instead of Developer ID Application; only the latter notarizes for distribution outside the App Store.
  • openh264 fails to cross-compile the x86_64 slice — it builds C++ via the cc crate; the Xcode CLT clang handles -arch x86_64 on an arm Mac, so a full xcode-select --install (or Xcode) is enough. If it still fails, build only the native slice with --target aarch64-apple-darwin and drop the lipo step (the app is then arm64-only).
  • Icon looks soft — add a 1024×1024 assets/icon-1024.png master (the repo currently ships 256 and 512); the script prefers the largest it finds.