# 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`](./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`](../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: ```bash # 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 `, `--version `, `--entitlements `. 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`): ```bash 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: ```bash 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 — \$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 → **+** → *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: ```bash 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 → **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 → *Sign-In and Security → App-Specific Passwords* → **+**, label it e.g. `netdesk-notarytool`. Then store it once in the keychain as a named profile: ```bash 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: ```bash 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 `): `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)"`): ```bash 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 \ -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 ```bash # 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 --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.