Distribution: signing, notarization, and packaging
vidra build turns a project into something you can hand to someone else: a
macOS .dmg or a self-contained Windows .zip. Producing that file is the easy
half. The other half is convincing the operating system to run it.
An app that arrives over the internet is treated differently from one you built locally. macOS attaches a quarantine flag to downloaded files and asks Gatekeeper for a verdict; Windows shows a SmartScreen warning for unrecognized binaries. Neither cares that the app works on your machine — they care whether it carries a signature they trust.
Nothing here is mandatory to build. With no signing configured,
vidra buildstill produces a working artifact — it warns about what’s missing and carries on. Everything below is opt-in by environment variable.
Default build artifacts
| Target | Artifact | Runs without extra installs? |
|---|---|---|
| macOS | dist/<App>-<version>-macos.dmg (contains the .app and an /Applications symlink) | Yes |
| Windows | dist/<App>-<version>-windows.zip (self-contained, unpackaged) | Yes — except the WebView2 runtime, see below |
Windows builds still produce a ZIP. Extract it and run the host executable inside the app directory.
With --native-update, the public artifacts keep the same names and the build
also writes an update feed:
| Target | Additional output |
|---|---|
| Windows | dist/<App>-<version>-Setup.exe and dist/release/ |
| macOS | dist/release/ |
The Windows -windows.zip is a hand-packaged app directory in a default build
and a renamed Velopack portable archive in a native-update build. Treat both as
portable app ZIPs; do not depend on an identical internal directory layout.
Preview every step, and the exact artifact name, without running anything:
npx vidra build --target macos --plan
The plan reflects your actual environment: it reports whether a Developer ID certificate is present and whether notarization credentials are configured.
macOS
Choosing a signing identity
Vidra picks a certificate based on what the signature is for:
| Context | Preferred certificate | Why |
|---|---|---|
vidra dev / vidra run | Apple Development: | Local launches only. Free with any Apple ID. |
vidra build | Developer ID Application: | The only kind that can be notarized. |
If vidra build can only find a development certificate it will use it and warn
loudly — the result runs locally and will be rejected on every other Mac.
With no certificate at all it falls back to ad-hoc signing (-).
Override the choice with VIDRA_MACOS_CODESIGN_KEY:
export VIDRA_MACOS_CODESIGN_KEY="Developer ID Application: Your Name (TEAMID)"
List available identities with security find-identity -v -p codesigning, or
run npx vidra doctor.
The hardened runtime and entitlements
Notarization requires the hardened runtime (codesign --options runtime).
The hardened runtime, by default, forbids exactly the things .NET needs. Enabling
it without matching entitlements produces an app that signs and notarizes but
dies when it launches.
Scaffolded projects ship src/<App>.Host/Entitlements.plist:
| Entitlement | Why it’s needed |
|---|---|
com.apple.security.cs.allow-jit | .NET’s JIT compiles into executable memory |
com.apple.security.cs.allow-unsigned-executable-memory | the runtime writes then executes that memory |
com.apple.security.cs.disable-library-validation | it loads dylibs signed by a different team |
com.apple.security.network.client | the WebView reaches the Vite dev server |
com.apple.security.files.user-selected.read-write | filePicker returns paths the app then reads |
com.apple.security.app-sandbox = false | Vidra’s unrestricted filesystem API is incompatible with an App Store sandbox |
Both vidra build and MAUI’s signing pass use this file.
Do not add XML comments to
Entitlements.plist. The MacCatalyst SDK’s PList reader rejects comments in the document body.
Projects scaffolded before this file existed must copy it and the
<CodesignEntitlements> property from a new project.
How the bundle is signed
Signing runs inside-out: nested .dylib/.so files and .framework
bundles are signed first, deepest path first, then the .app. Vidra deliberately
does not use codesign --deep, which Apple does not support for submission.
Every signature is timestamped. The .dmg is signed once a real identity exists.
Notarization
Notarization uploads the artifact to Apple, waits for a verdict, and staples the ticket so it validates offline. It requires an Apple Developer Program membership.
Store credentials once:
xcrun notarytool store-credentials vidra-notary \
--apple-id you@example.com --team-id ABCDE12345 --password <app-specific-password>
export VIDRA_NOTARY_PROFILE=vidra-notary
npx vidra build --target macos
Or supply credentials directly for CI:
export VIDRA_APPLE_ID=you@example.com
export VIDRA_TEAM_ID=ABCDE12345
export VIDRA_APP_PASSWORD=abcd-efgh-ijkl-mnop
With no credentials, notarization is skipped. A rejected submission fails the
build, and Vidra fetches and prints the notarytool rejection log automatically.
Verifying the result
After signing, vidra build reports:
codesign --verify --strict— the signature is well-formed and the bundle has not changed. This must pass.spctl --assess— Gatekeeper’s verdict. Rejection is expected until the build is both Developer ID signed and notarized.
Inspect an existing artifact without rebuilding:
npx vidra verify
npx vidra verify dist/MyApp-0.1.0-macos.dmg
npx vidra verify dist/MyApp-0.1.0-windows.zip
npx vidra verify path/to/MyApp.app
To simulate a downloaded macOS artifact, apply quarantine and open it:
xattr -w com.apple.quarantine "0081;$(printf %x $(date +%s));Safari;" dist/*.dmg
open dist/*.dmg
Windows
Authenticode
vidra build --target windows signs the app executable before zipping.
Point Vidra at a certificate file:
$env:VIDRA_WINDOWS_CERT_PATH = "C:\certs\vidra.pfx"
$env:VIDRA_WINDOWS_CERT_PASSWORD = "..."
Or use a certificate already in the store:
$env:VIDRA_WINDOWS_CERT_THUMBPRINT = "4FB0491E0F13CDBDAE24BF16F09E4989F038471B"
Signatures use SHA-256 and are timestamped. Override the default DigiCert
timestamp authority with VIDRA_WINDOWS_TIMESTAMP_URL. Vidra finds
signtool.exe from the installed Windows SDK, or accepts VIDRA_SIGNTOOL_PATH.
With nothing configured, the build is unsigned and still runs. SmartScreen warns users until the binary develops reputation.
The WebView2 runtime
The ZIP bundles .NET and the WindowsAppSDK, but WebView2 is a machine-wide
runtime. It ships with Windows 11 and alongside Edge on most Windows 10
systems. On stripped images, a Vidra app may launch to a blank window.
npx vidra doctor reports whether WebView2 is installed.
Environment variable reference
| Variable | Platform | Effect |
|---|---|---|
VIDRA_MACOS_CODESIGN_KEY | macOS | Override automatic signing identity selection |
VIDRA_NOTARY_PROFILE | macOS | notarytool keychain profile; enables notarization |
VIDRA_APPLE_ID | macOS | Apple ID for notarization |
VIDRA_TEAM_ID | macOS | Apple Developer team identifier |
VIDRA_APP_PASSWORD | macOS | App-specific notarization password |
VIDRA_WINDOWS_CERT_PATH | Windows | Path to a .pfx certificate |
VIDRA_WINDOWS_CERT_PASSWORD | Windows | Password for the .pfx |
VIDRA_WINDOWS_CERT_THUMBPRINT | Windows | Certificate-store thumbprint |
VIDRA_WINDOWS_TIMESTAMP_URL | Windows | Timestamp authority |
VIDRA_SIGNTOOL_PATH | Windows | Explicit path to signtool.exe |
VIDRA_NOTARY_PROFILE takes precedence over the Apple ID credential set.
Troubleshooting
| Symptom | Cause |
|---|---|
| App cannot be opened because the developer cannot be verified | Not notarized, or not Developer ID signed |
| App is killed immediately after signing | Hardened runtime without JIT entitlements |
Failed to parse PList data type: | An XML comment inside Entitlements.plist |
no Entitlements.plist found | Project predates the template file |
| Notarization rejected | Inspect the notarytool log output Vidra prints |
| Blank window on Windows | WebView2 runtime missing |
| Signature later stops validating | The signature was not timestamped |
Updating without a native release
Shipping only a new web UI does not require signing, notarization, or an installer. See Updates. Native changes still use the flow on this page, and contract fingerprints prevent an incompatible web bundle from running against an older binary.