Skip to content

User meta-installer: one-download full-stack DisplayXR for end users (new displayxr-installer repo) #284

@dfattal

Description

@dfattal

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:

  1. 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.
  2. 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.
  3. 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:

  1. Bumps versions.json in a PR (pinning each component to a specific tag).
  2. Merges.
  3. Tags vX.Y.Z on the displayxr-installer repo.
  4. 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 infoRun 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
  • macOS bundle: bundled runtime + plug-in dylibs preserve their ad-hoc signatures through the wrapper re-packaging step (regression guard against re-introducing the PR macos: retarget bundled Vulkan to @rpath + ad-hoc re-sign + versioned .pkg filename #279 SIGKILL bug)

Acceptance — follow-up (signed wrapper, zero-prompt UX)

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions