13 KiB
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. Seecross-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
.appfrom 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)
-
Xcode command-line tools (gives
clang, the SDKs,lipo,iconutil,sips,hdiutil,codesign,xcrun):xcode-select --installA full Xcode install from the App Store also works.
-
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 -
An Apple Developer Program membership (needed only for signing + notarization — a
--no-signbuild 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:
- Xcode → Settings… → Accounts, sign in with the Apple ID on the team.
- Select the team → Manage Certificates…
- Click + → Developer ID Application.
- 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 TEAMID1234plus the app-specific password in the environment:export NETDESK_NOTARY_PASSWORD='abcd-efgh-…'.
What the script does (step by step)
packaging/build-macos.sh:
- rustup target add
aarch64-apple-darwinx86_64-apple-darwin. - cargo build --release -p netdesk-viewer once per target — producing
target/aarch64-apple-darwin/release/netdesk-viewerand thex86_64one. (Cross-building the Intel slice on an Apple-silicon Mac uses the same SDK and needs no extra tooling.) - lipo -create the two into one universal Mach-O at
NetDesk.app/Contents/MacOS/NetDesk(lipo -infoprintsarm64 x86_64). - iconutil builds
NetDesk.icnsfrom the largest PNG master it finds (assets/icon-1024.png→icon-512.png→icon.png, or--icon <png>):sipsresizes the master into a complete.iconset(16→1024 px, @1x/@2x), theniconutil -c icnspacks it intoContents/Resources/NetDesk.icns. - Assembles
NetDesk.app— writesContents/Info.plistwith bundle idcom.akronlogic.netdesk,CFBundleVersion/CFBundleShortVersionString= the workspace version (read fromCargo.toml, currently 0.1.0),CFBundleExecutable/CFBundleIconFile=NetDesk,LSMinimumSystemVersion11.0,NSHighResolutionCapable, plus aPkgInfo(APPL????). - codesign the executable and the bundle with the Developer ID Application
identity,
--options runtime(hardened runtime, required to notarize) and--timestamp(secure timestamp), thencodesign --verify --deep --strict. - Notarize + staple the
.app:ditto -c -k --keepParentit into a zip,xcrun notarytool submit … --wait, thenxcrun stapler staple NetDesk.appso the ticket travels with the app offline. (Skip with--no-staple-app.) - Build the
.dmg(hdiutil create, UDZO, with an/Applicationssymlink 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 withsecurity 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
cccrate; the Xcode CLT clang handles-arch x86_64on an arm Mac, so a fullxcode-select --install(or Xcode) is enough. If it still fails, build only the native slice with--target aarch64-apple-darwinand drop the lipo step (the app is then arm64-only). - Icon looks soft — add a 1024×1024
assets/icon-1024.pngmaster (the repo currently ships 256 and 512); the script prefers the largest it finds.