You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Today an end user who wants "DisplayXR to just work" has to download + run 4-5 separate installers (runtime, Shell, Leia plug-in, MCP Tools, plus per-demo). That's appropriate for the current developer-facing audience but won't scale to end-user distribution. This issue tracks a single-download meta-installer.
Companion issue: see #283 for the developer-facing orchestrator script. Both share versions.json semantics — the schema is defined once and consumed in both places.
Architectural shape
Both Windows and macOS have native primitives for chained installs:
Windows: NSIS supports ExecWait '$INSTDIR\nested.exe /S' for chaining component installers. The bundle is either an offline .exe that drops + runs each component, or a bootstrapper that downloads them from GitHub Releases at install time. NSIS runs the bundle with RequestExecutionLevel admin, so children inherit elevation and don't re-prompt UAC.
macOS: productbuild --distribution Distribution.xml is literally designed for this — list each component .pkg as a <pkg-ref> and the installer GUI shows checkboxes for optional components. We already use this pattern in installer/macos/Distribution.xml for the runtime + cube test app; extending it to include Shell + plug-ins is the same shape. Embedded child pkgs are read internally by Installer.app and don't quarantine separately.
Where it lives — new DisplayXR/displayxr-installer public repo
NOT in displayxr-runtime/releases. Three reasons:
Branding clarity. A developer integrating DisplayXR into their app wants DisplayXRClient.dll — they should not get a 200 MB bundle of Shell + demos when they gh release download. "Runtime release" and "DisplayXR release" need to be different artifacts on different release pages.
Independent cadence. The bundle ships when the whole stack is ready (launch-driven). The runtime ships when there's a runtime fix worth tagging (engineering-driven). Coupling them forces one team's cadence onto the other.
Compat matrix lives somewhere honest. "runtime v1.4.0 pairs with Shell v0.9.1 pairs with Leia plug-in v1.2.0" gets pinned and tested as a unit. That belongs in a dedicated repo with versions.json + a small CI matrix, not nested inside the runtime's release notes.
Repo layout
DisplayXR/displayxr-installer ← new public repo
├── versions.json ← cross-repo compat matrix (shared schema with #283)
├── installer/
│ ├── windows/DisplayXRBundleInstaller.nsi ← chains component installers via NSIS ExecWait
│ └── macos/Distribution.xml ← wraps child .pkgs via productbuild
├── scripts/
│ └── build-bundle.{sh,bat} ← downloads each component installer + assembles bundle
└── .github/workflows/
└── publish-bundle.yml ← workflow_dispatch on `v*`, attaches .exe + .pkg to release
Outputs
DisplayXRBundle-X.Y.Z.exe (~150 MB Windows offline bundle)
DisplayXRBundle-X.Y.Z.pkg (~100 MB macOS distribution pkg)
One download per OS. User double-clicks → guided install of every component → working DisplayXR.
Release flow
workflow_dispatch (not auto-fired). A release engineer:
Bumps versions.json in a PR (pinning each component to a specific tag).
Merges.
Tags vX.Y.Z on the displayxr-installer repo.
Triggers publish-bundle.yml manually — pulls each component's installer from its source repo's release page, assembles the bundle, attaches to the GitHub Release.
Not auto-triggered on every component release — that would be noisy + brittle (e.g., a runtime patch release shouldn't republish a bundle that wasn't compat-tested against the latest Shell).
displayxr.org integration
The website becomes the download portal with a single button per platform:
displayxr.org/download/macos → 302 to the latest .pkg on displayxr-installer/releases/latest
displayxr.org/download/windows → 302 to the latest .exe
The website doesn't host the binary (GitHub Releases CDN is free + fast); it just brands the URL.
The individual component repos continue to publish their own installers. Developers who want a single component still install it directly from its own release page. End users get the bundle.
Signing — optional, ships fine without it
The bundle can ship completely unsigned in its first release. Gatekeeper / SmartScreen both gate on com.apple.quarantine xattr / Mark of the Web ADS — tags attached only by browsers at download time. The user-facing flow is:
Step
What user sees
Downloads bundle .pkg / .exe from browser → file is quarantined / MOTW-tagged
—
Double-clicks bundle
Single prompt — "Windows protected your PC" / "DisplayXR cannot be verified"
Clicks past it ("More info → Run anyway" on Win; right-click → Open on Mac)
Quarantine bypassed for this execution
Bundle runs elevated, chains child installers via ExecWait /S or embedded <pkg-ref>
Nothing — children don't have MOTW / quarantine because the bundle wrote them, not a browser
Children drop runtime / Shell / plug-in / MCP files into Program Files\ / /Library/Application Support/
Nothing — admin-installed files don't get MOTW
User later launches Shell or a test .app
Nothing — files in install locations have no quarantine
Third-party OpenXR app loads DisplayXRClient.dll via LoadLibrary / dlopen
Nothing — loader doesn't check Gatekeeper for dylib loads
So one user-acknowledged prompt at the bundle level = silent install for everything + silent post-install runs.
Signing is a UX polish on top, not a precondition. When #280 (macOS notarization) and #281 (Windows Authenticode) land, the bundle's CI can reuse the same cert infrastructure to sign its own wrapper artifact — at that point the single warning prompt goes away too. But that's a follow-up, not a blocker.
Caveats of shipping unsigned (bounds, not blockers)
Enterprise-managed machines with AppLocker / WDAC / MDM profiles that forbid unsigned execution will refuse the bundle entirely. Those users need signed artifacts.
Some antivirus heuristics treat unsigned NSIS-wrapped installers as suspicious; variance per vendor. Mitigated somewhat by ad-hoc signing the bundle wrapper (free, no cert).
Trust hygiene — users running signtool verify or codesign --verify on the installed binaries will see "unsigned" even though install succeeded. Cosmetic for hobbyists; deal-breaker for enterprise procurement.
macOS internal ad-hoc signing is NOT optional for the runtime + plug-in + bundled libvulkan. PR macos: retarget bundled Vulkan to @rpath + ad-hoc re-sign + versioned .pkg filename #279 fixed this; the bundle's CI must preserve those ad-hoc signatures (don't strip them when re-packaging into the wrapper). This is separate from Developer ID signing — modified Mach-O binaries without any signature get SIGKILL'd at dlopen by modern macOS.
Documentation requirement at first ship
displayxr.org/download MUST include the one-time bypass copy:
macOS: After downloading, right-click DisplayXRBundle-*.pkg and choose Open. Click Open in the dialog. Apple requires this one-time confirmation for unsigned installers — notarization is in progress (#280).
Windows: After downloading, you may see "Windows protected your PC." Click More info → Run anyway. Microsoft SmartScreen warns about new publishers — Authenticode signing is in progress (#281).
When #280 / #281 close and the bundle adopts wrapper signing, drop this copy.
Effort
~1 week initial scaffolding + ~1 day per release cadence afterward. Most of the recurring work is bumping versions.json and triggering the workflow.
Wrapper signing (when it lands later) is ~40 lines per platform in publish-bundle.yml, parallel to #280 / #281's component-installer signing. Same cert infrastructure, reused.
Acceptance — first ship (unsigned, one-prompt UX)
DisplayXR/displayxr-installer repo created (public)
versions.json + publish-bundle.yml shipping
First v0.1.0 bundle release attaches both .exe and .pkg
Macro test: from a fresh macOS install, downloading the .pkg from a browser and right-click-opening once produces a fully working DisplayXR setup with no further prompts
Macro test: from a fresh Windows install, downloading the .exe and clicking "Run anyway" once produces a fully working setup
Each child installer's /S silent invocation tested in the bundle's CI
Bundle uninstaller cleanly removes every component (chain <componentInstaller> /uninstall /S)
displayxr.org's download page points at the bundle's releases AND carries the one-time-bypass instructions
Today an end user who wants "DisplayXR to just work" has to download + run 4-5 separate installers (runtime, Shell, Leia plug-in, MCP Tools, plus per-demo). That's appropriate for the current developer-facing audience but won't scale to end-user distribution. This issue tracks a single-download meta-installer.
Architectural shape
Both Windows and macOS have native primitives for chained installs:
ExecWait '$INSTDIR\nested.exe /S'for chaining component installers. The bundle is either an offline.exethat drops + runs each component, or a bootstrapper that downloads them from GitHub Releases at install time. NSIS runs the bundle withRequestExecutionLevel admin, so children inherit elevation and don't re-prompt UAC.productbuild --distribution Distribution.xmlis literally designed for this — list each component.pkgas a<pkg-ref>and the installer GUI shows checkboxes for optional components. We already use this pattern ininstaller/macos/Distribution.xmlfor the runtime + cube test app; extending it to include Shell + plug-ins is the same shape. Embedded child pkgs are read internally byInstaller.appand don't quarantine separately.Where it lives — new
DisplayXR/displayxr-installerpublic repoNOT in
displayxr-runtime/releases. Three reasons:DisplayXRClient.dll— they should not get a 200 MB bundle of Shell + demos when theygh release download. "Runtime release" and "DisplayXR release" need to be different artifacts on different release pages.versions.json+ a small CI matrix, not nested inside the runtime's release notes.Repo layout
Outputs
DisplayXRBundle-X.Y.Z.exe(~150 MB Windows offline bundle)DisplayXRBundle-X.Y.Z.pkg(~100 MB macOS distribution pkg)One download per OS. User double-clicks → guided install of every component → working DisplayXR.
Release flow
workflow_dispatch(not auto-fired). A release engineer:versions.jsonin a PR (pinning each component to a specific tag).vX.Y.Zon thedisplayxr-installerrepo.publish-bundle.ymlmanually — pulls each component's installer from its source repo's release page, assembles the bundle, attaches to the GitHub Release.Not auto-triggered on every component release — that would be noisy + brittle (e.g., a runtime patch release shouldn't republish a bundle that wasn't compat-tested against the latest Shell).
displayxr.org integration
The website becomes the download portal with a single button per platform:
displayxr.org/download/macos→ 302 to the latest.pkgondisplayxr-installer/releases/latestdisplayxr.org/download/windows→ 302 to the latest.exeThe website doesn't host the binary (GitHub Releases CDN is free + fast); it just brands the URL.
The individual component repos continue to publish their own installers. Developers who want a single component still install it directly from its own release page. End users get the bundle.
Signing — optional, ships fine without it
The bundle can ship completely unsigned in its first release. Gatekeeper / SmartScreen both gate on
com.apple.quarantinexattr / Mark of the Web ADS — tags attached only by browsers at download time. The user-facing flow is:.pkg/.exefrom browser → file is quarantined / MOTW-taggedExecWait /Sor embedded<pkg-ref>Program Files\//Library/Application Support/DisplayXRClient.dllviaLoadLibrary/ dlopenSo one user-acknowledged prompt at the bundle level = silent install for everything + silent post-install runs.
Signing is a UX polish on top, not a precondition. When #280 (macOS notarization) and #281 (Windows Authenticode) land, the bundle's CI can reuse the same cert infrastructure to sign its own wrapper artifact — at that point the single warning prompt goes away too. But that's a follow-up, not a blocker.
Caveats of shipping unsigned (bounds, not blockers)
signtool verifyorcodesign --verifyon the installed binaries will see "unsigned" even though install succeeded. Cosmetic for hobbyists; deal-breaker for enterprise procurement.Documentation requirement at first ship
displayxr.org/downloadMUST include the one-time bypass copy:When #280 / #281 close and the bundle adopts wrapper signing, drop this copy.
Effort
~1 week initial scaffolding + ~1 day per release cadence afterward. Most of the recurring work is bumping
versions.jsonand triggering the workflow.Wrapper signing (when it lands later) is ~40 lines per platform in
publish-bundle.yml, parallel to #280 / #281's component-installer signing. Same cert infrastructure, reused.Acceptance — first ship (unsigned, one-prompt UX)
DisplayXR/displayxr-installerrepo created (public)versions.json+publish-bundle.ymlshippingv0.1.0bundle release attaches both.exeand.pkg/Ssilent invocation tested in the bundle's CI<componentInstaller> /uninstall /S)Acceptance — follow-up (signed wrapper, zero-prompt UX)
publish-bundle.ymlapplies notarization + Authenticode to the wrapperspctl --assess --type installclean on the.pkgfrom a fresh~/Downloadssigntool verify /paclean on the.exeReferences
versions.jsonschema)installer/macos/Distribution.xml— current single-component distribution example to extendinstaller/DisplayXRInstaller.nsi— current NSIS runtime installer (the bundle will chain into this)