Skip to content

Device Preparation and Signing

M T edited this page Oct 4, 2026 · 1 revision

Device Preparation and Signing

This page covers how an unsigned HaloVision.app becomes a signed app on the user's own Apple Vision Pro, together with the user's privately owned game payload and installation registry seed. There are two supported routes: the guided route, where the setup wizard stages a manifest-verified payload and Xcode signs, installs and launches with automatic signing; and the explicit scripted route, where tools/prepare_engine_vision_device.py validates every signing input, stages the payload into a copy of the app, signs it with codesign, verifies the result, and the user installs it with xcrun devicectl. The page also documents the payload manifest contract and how the app imports that payload on the headset. No personal signing identity, team, bundle identifier or device identifier exists anywhere in the source; every such input is a parameter the user supplies.

Source files

File Role
tools/prepare_engine_vision_device.py Validates app, provisioning profile, identity, entitlements, disk and game; stages GamePayload/ and GamePayloadManifest.json into a copy of the app; signs and verifies; writes a receipt. "This tool never contacts or modifies a device."
tools/setup_halo.py Guided route: stage_payload() writes .setup/GamePayload and its manifest; --stage build copies it into the unsigned app.
native/EngineVision/project.yml Includes the optional payload as app resources; automatic signing.
native/EngineVision/Sources/EngineAssets.swift On-device manifest check, import into Application Support, save preservation and rollback.
native/EngineHost/shims_misc.c Runtime registry seeded from <game root>/halo-vision-registry.txt.
tools/halo-vision-registry.template.txt Placeholder seed for the manual route.
docs/BUILDING.md User instructions for the explicit route.

Route overview

flowchart TD
    subgraph Guided["Guided route (setup.sh)"]
        G1["setup.sh: game/ + halo-vision-registry.txt"] --> G2[".setup/GamePayload + GamePayloadManifest.json"]
        G2 --> G3["Xcode project includes payload as resources"]
        G3 --> G4["User: Team + unique bundle ID + device, press Run"]
        G4 --> G5["Xcode builds, signs, installs, launches"]
    end
    subgraph Explicit["Explicit route"]
        E1["App built with the user's bundle ID"] --> E2["prepare_engine_vision_device.py --preflight-only"]
        E2 --> E3["prepare_engine_vision_device.py: stage payload, codesign, verify"]
        E3 --> E4["output/HaloVision.app + output/preparation.json"]
        E4 --> E5["xcrun devicectl device install app"]
        E5 --> E6["xcrun devicectl device process launch"]
    end
    G5 --> H["On headset: EngineAssetStore imports GamePayload into Application Support"]
    E6 --> H
    H --> R["Engine starts with game root = PackagedGames/payloadID"]
Loading

The game payload contract

Both routes deliver the same two app-bundle resources, and the app refuses anything that does not satisfy this contract.

Layout

HaloVision.app/
  GamePayloadManifest.json
  GamePayload/
    halo.exe
    strings.dll
    halo-vision-registry.txt
    config.txt            (optional)
    *.bik                 (optional movies)
    maps/*.map
    shaders/*.bin

Manifest format

Field Type Rule
formatVersion int must be 1
payloadID 64 lowercase hex inventory_digest() of the file rows (below)
fileCount int number of files entries
totalBytes int sum of file sizes
executableSHA256 64 hex must equal c9acf0c469543283cfed6d7dc04ade976dbdfc7cb4532cf070386de169c19545
files[] {path, bytes, sha256} relative POSIX paths; no absolute paths, .., duplicates (case-insensitive), or the reserved name engine-vision-import.json

payloadID is computed by inventory_digest() (prepare_engine_vision_device.py:97-104): SHA-256 over json.dumps(rows, sort_keys=True, separators=(",", ":")), where rows are {path, bytes, sha256} sorted by (path.lower(), path). Because it hashes destination paths and contents only, two payloads with identical files always have the same ID, and any content change produces a new ID.

Path canonicalisation

Windows file names are case-insensitive, so paths are normalised before hashing. canonical_game_path() (prepare_engine_vision_device.py:107-117) lowercases a first component of maps or shaders, and lowercases top-level file names; nested names keep their case. Its docstring says it must "Match EngineAssetImporter.canonicalRelativePath exactly", which is the Swift twin at EngineAssets.swift:359-368. Two source files mapping to the same destination are rejected on both sides.

Python validation (validate_bundled_game_payload)

prepare_engine_vision_device.py:266-318 is used by setup (for .setup, the staged temp directory, and the app) and by the device tool. It requires:

  1. GamePayloadManifest.json and GamePayload/ exist and neither is a symlink.
  2. formatVersion == 1 and a non-empty files list of objects.
  3. Each path is safe (not absolute, no .., not engine-vision-import.json), unique case-insensitively, exists as a regular non-symlink file, and matches the manifest byte length and SHA-256.
  4. The actual tree under GamePayload/ (via tree_inventory) equals the manifest exactly: "GamePayload contains files absent from its exact manifest".
  5. payloadID equals the recomputed digest; fileCount and totalBytes match.
  6. executableSHA256 equals HALO_SHA256.
  7. Every REQUIRED_GAME_FILES entry is present at its minimum size (halo.exe 2,000,000; maps/ui.map 2,000,000; maps/a10.map 90,000,000; maps/bitmaps.map 300,000,000; maps/sounds.map 200,000,000; shaders/vsh.bin 30,000; shaders/fx.bin 900,000; strings.dll 1,000,000; halo-vision-registry.txt 100).

It returns {manifest, payload_id, file_count, total_bytes, executable_sha256, files}.

The registry seed in the payload

halo-vision-registry.txt is one of the required payload files. At runtime the ADVAPI32 shim reads <game root>/halo-vision-registry.txt into an in-memory table of up to 256 values and writes the file back on RegSetValueExA (shims_misc.c:273-336). On the guided route the wizard produces it from the user's own installation (format and validation). On the manual route the user copies tools/halo-vision-registry.template.txt into the game directory and fills the placeholders with values from their own installation, keeping ExitFlag as clean. The encoded DigitalProductID is not the printed product key. This file contains private product information and must never be published.

Guided route: Xcode automatic signing

After ./setup.sh (default --stage xcode):

  1. .setup/GamePayload and .setup/GamePayloadManifest.json exist and are verified.
  2. project.yml references them as optional resources (optional: true), along with the four visual-pack files, and sets CODE_SIGNING_ALLOWED: YES / CODE_SIGN_STYLE: Automatic. When the payload is present, Xcode copies it into the bundle; when absent (a pure source build), the project still builds.
  3. The user, in Xcode: adds their Apple account if needed; selects the EngineVision target, Signing & Capabilities; selects their Team and replaces the placeholder org.example.halovision with a unique bundle identifier they control; keeps automatic signing; selects the paired, unlocked Vision Pro (Developer Mode enabled); presses Run.
  4. Xcode builds, signs, installs and launches. Provisioning errors, account restrictions, device trust and Developer Mode prompts are resolved by the user in Xcode and on the headset; setup "does not sign in for you, create certificates, or claim the app is installed merely because project generation succeeded" (SETUP.md).

On reruns the wizard keeps the existing Xcode project so these signing choices persist (Setup Wizard, stage 11a).

Explicit route: prepare_engine_vision_device.py

Inputs

Every signing and device input is a required parameter; the module-level defaults for them are None (prepare_engine_vision_device.py:26-34) and are assigned from arguments in main().

Argument Required Default Meaning
--app yes Unsigned (or re-signable) HaloVision.app built with the same bundle identifier as --bundle-id.
--profile-app yes An existing app signed by Xcode for the user's team, bundle ID and headset; its embedded.mobileprovision supplies the provisioning profile.
--game no <repo>/game Owned game folder (symlinked roots are resolved). Must contain halo-vision-registry.txt.
--output no native/EngineVision/.build/device-staging Staging directory. Must not already exist.
--identity-sha1 yes 40-hex SHA-1 fingerprint of the signing certificate (validated by regex, uppercased).
--entitlements yes Entitlements plist for signing, typically exported from the profile app with codesign -d --entitlements :- "$PROFILE_APP".
--team-id yes Apple developer team identifier.
--bundle-id yes The app's bundle identifier.
--device-udid yes Headset UDID that must be listed in the profile's ProvisionedDevices.
--core-device-id yes CoreDevice identifier used by devicectl; recorded in the receipt only.
--preflight-only no off Validate everything and print the receipt JSON without staging or signing.

The user discovers these with read-only commands (from BUILDING.md):

security find-identity -v -p codesigning
xcrun devicectl list devices
codesign -d --entitlements :- "$PROFILE_APP" > "$ENTITLEMENTS"

and then runs:

python tools/prepare_engine_vision_device.py \
  --app "$APP" --profile-app "$PROFILE_APP" --game "$GAME_DIR" \
  --output native/EngineVision/.build/local-device \
  --team-id "$TEAM_ID" --bundle-id "$BUNDLE_ID" \
  --device-udid "$DEVICE_UDID" --core-device-id "$DEVICE_ID" \
  --identity-sha1 "$IDENTITY_SHA1" --entitlements "$ENTITLEMENTS"

All of those shell variables are the user's own values; none are defaulted.

Validation sequence

main() (prepare_engine_vision_device.py:408-459) runs these in order; any failure raises PreparationError (exit 1, device preparation failed: ...).

sequenceDiagram
    participant U as User
    participant T as prepare_engine_vision_device.py
    participant OS as macOS tools
    U->>T: arguments
    T->>OS: file, otool -l (validate_app)
    T->>OS: security cms -D (decode profile)
    T->>OS: security find-identity -v -p codesigning
    T->>OS: openssl x509 -dates (selected certificate)
    T->>T: validate_entitlements()
    T->>T: validate_game() (hashes every file)
    T->>T: disk check, output must not exist
    alt --preflight-only
        T-->>U: receipt JSON (no staging)
    else complete
        T->>T: copy app (clone), stage GamePayload + manifest, validate
        T->>OS: codesign --force --sign SHA1 --entitlements ... --generate-entitlement-der --timestamp=none
        T->>OS: codesign --verify --deep --strict --verbose=4
        T->>OS: codesign -d --entitlements :- (read back)
        T->>T: validate payload again, inventory bundle, atomic rename
        T-->>U: output/HaloVision.app + output/preparation.json
    end
Loading

1. App - validate_app() (lines 140-167):

  • Info.plist exists; CFBundleIdentifier equals --bundle-id (so the direct-route default org.example.halovision must be replaced by building with the user's bundle ID in Xcode, as BUILDING.md instructs).
  • CFBundleSupportedPlatforms contains XROS.
  • /usr/bin/file reports Mach-O 64-bit executable arm64.
  • /usr/bin/otool -l shows platform 11 (visionOS LC_BUILD_VERSION).
  • Returns bundle ID, executable name and SHA-256, bundle size, and MinimumOSVersion.

2. Provisioning profile and identity - validate_profile_and_identity() (lines 170-222):

Check Error
<profile-app>/embedded.mobileprovision exists reference provisioning profile is missing
ExpirationDate is in the future (UTC) provisioning profile expired at ...
--team-id in TeamIdentifier wrong team
--device-udid in ProvisionedDevices does not contain the Vision Pro UDID
Platform contains xros or visionos (case-insensitive) does not include xrOS/visionOS
Entitlements.application-identifier is <team>.* or <team>.<bundle> does not allow the target application identifier
SHA-1 of one of DeveloperCertificates equals --identity-sha1 the selected signing identity is not included in the profile
security find-identity -v -p codesigning lists that SHA-1 the required Apple Development signing identity is unavailable

The exact certificate is then written to a temporary DER file and openssl x509 -dates records its validity window. The code comment explains the choice: "Use the exact selected certificate, not a name shared by multiple accounts." Signing is by SHA-1 fingerprint, never by certificate name.

3. Entitlements - validate_entitlements() (lines 225-231): application-identifier must be exactly <team>.<bundle> and com.apple.developer.team-identifier must equal the team.

4. Game - validate_game() (lines 234-258): inventories every regular file under the resolved --game root, maps each to its canonical destination path, rejects duplicate destinations, checks REQUIRED_GAME_FILES minimum sizes and the halo.exe SHA-256, and returns rows (with source_path and canonical path), total bytes and payloadID. Unlike the setup wizard's check_game(), this function does not reject extra files: everything in the folder is bundled. BUILDING.md therefore says to "Use a clean directory containing your owned game installation".

5. Disk and output - required free space on the output's parent volume is game bytes + app bytes + 2 GiB. The output directory must not exist ("choose a fresh staging directory").

Staging and signing

stage_and_sign_app() (lines 321-372):

  1. Copy the source app to <output>/.HaloVision-staging-<uuid>.app with shutil.copytree(symlinks=False, copy_function=clone_copy) (APFS clones).
  2. Remove any existing GamePayload/ in the copy and recreate it; clone-copy every game file to its canonical path.
  3. Write GamePayloadManifest.json atomically (write_json_atomic: temp name .<name>.<uuid>.tmp, then replace), validate it, and require its payload_id to equal the source inventory hash.
  4. Copy the profile to embedded.mobileprovision.
  5. codesign --force --sign <SHA1> --entitlements <plist> --generate-entitlement-der --timestamp=none <staging app>.
  6. codesign --verify --deep --strict --verbose=4 <staging app>.
  7. Read back the signed entitlements (codesign -d --entitlements :-, parsing from <?xml or <plist) and re-check the application and team identifiers.
  8. Validate the payload again (after signing), inventory the whole bundle, remove any existing destination, and rename the staging app to <output>/HaloVision.app.

After signing, a legacy <output>/app-data directory (from older tooling) is removed if present, and preparation.json is written atomically. The tool prints a short JSON status with the receipt path, signed app path, and payload byte and file counts.

Receipt (preparation.json)

Key Content
prepared_at_utc timestamp
mode preflight-only or complete
source_app path, bundle ID, executable name and SHA-256, bundle bytes, minimum OS
profile profile name, UUID, team, application identifier, created/expires, platforms, provisioned devices, identity SHA-1, certificate dates
entitlements the validated entitlements plist
device provisioned UDID and CoreDevice identifier
game_source path, file count, total bytes, tree SHA-256
disk_free_bytes_before, disk_free_bytes_after free space
device_contacted, device_install_or_launch_performed always false
signed_app path, executable SHA-256, whole-bundle tree SHA-256, bundle bytes, file count, signed entitlements, game_payload report

The receipt and the signed app contain private signing metadata, device identifiers and the user's game files. BUILDING.md: "it is a local installation package, not the public source release." The default output path is under the ignored native/EngineVision/.build/.

Installing and launching

The tool never contacts a device. With the headset unlocked and on the same network, the user runs (BUILDING.md):

xcrun devicectl device install app --device "$DEVICE_ID" \
  native/EngineVision/.build/local-device/HaloVision.app
xcrun devicectl device process launch --device "$DEVICE_ID" "$BUNDLE_ID"

$DEVICE_ID is the CoreDevice identifier shown by xcrun devicectl list devices.

On-device import

When the app starts, EngineAssetStore (EngineAssets.swift:378-470) decides where the game root is:

  • Bundled payload present (GamePayload/ or GamePayloadManifest.json exists in the bundle): prepareBundledGame() (line 404) loads and checks the manifest, then targets Application Support/HaloVision/PackagedGames/<payloadID>. "A distinct version directory preserves earlier game files and saves." If that directory already validates against the manifest (receipt payloadID, count, bytes, and every immutable file's size), it is used immediately; otherwise the payload is imported.
  • No bundled payload: the app validates Application Support/HaloVision/Game and, if missing, lets the user pick a folder (SwiftUI .fileImporter) which is imported without a manifest (required files, minimum sizes and executable hash checked).

Manifest check on the headset

EngineBundledManifest.check() (EngineAssets.swift:80-112) mirrors the Python rules: formatVersion == 1, 64-hex payloadID, positive counts, fileCount == files.count, executableSHA256 equal to the supported digest; every path free of empty, ., .. components, backslashes and NULs, not engine-vision-import.json, unique case-insensitively, with a 64-hex SHA-256; total bytes summed with overflow checking; all EngineAssetCatalog.required files present at minimum size; and the manifest's halo.exe hash equal to executableSHA256.

Import algorithm

EngineAssetImporter.importFolder() (EngineAssets.swift:149-228):

  1. With a manifest, entries come from the manifest's exact relative paths (the code notes this avoids reconstructing paths from /var vs /private/var URLs); each source must be a regular, non-symlink file of the manifest size.
  2. Files are streamed in 1 MiB blocks into a sibling staging directory .Game-import-<uuid>, with progress callbacks and cancellation checks. With a manifest, each file's SHA-256 is computed during the copy and compared.
  3. With a manifest, preserveMutableGameData() copies the player's writable tree Users/Player/Documents/My Games/Halo (profiles, checkpoints, playlist state) from the existing destination into staging, after rejecting symlinks and non-regular items. "The bundled copies are first-launch defaults; after installation this whole subtree belongs to the player."
  4. An engine-vision-import.json receipt (importedAt, fileCount, totalBytes, executableSHA256, payloadID) is written and validate() re-checks required files and the executable hash.
  5. promoteStaging() moves any existing destination to .Game-backup-<uuid>, moves staging into place, and on failure moves the backup straight back ("a failed promotion rolls the original tree, including saves, straight back into place"). A failure to delete the redundant backup is ignored so it cannot turn a successful repair into a launch failure.
  6. Any error removes the staging directory and leaves the previous installation untouched.

validatePreparedFiles() skips size checks for files under the mutable root, so a player's changed save does not force a reinstall, while a missing immutable file does.

The emulated Windows user is named Player; BUILDING.md notes that development builds which used a different emulated user folder need an explicit save migration.

Complete release app

The Complete release ships an unsigned generic Release/HaloVision.app without any registry seed or payload manifest. COMPLETE_RELEASE.md: "it cannot launch correctly until your private registry and payload manifest are added. The guided route above handles that through a fresh local build." The guided route is ./setup.sh --bundled --registry <private export> (or --wine-prefix), which imports Release/HaloVision.app/GamePayload as the game source. No owner profile or device identifier is shipped, and nothing in the archive lets a different Apple account install the owner's signed app.

Failure modes

Message Meaning
input bundle ID must be <id> App was built with a different bundle ID (for example the org.example.halovision placeholder). Build with the intended ID.
input executable has no visionOS LC_BUILD_VERSION (platform 11) Wrong platform build (for example a simulator or macOS binary).
provisioning profile expired at ... Re-run the app from Xcode to refresh the profile app.
provisioning profile does not contain the Vision Pro UDID Register the device and regenerate the profile.
the selected signing identity is not included in the profile / ... is unavailable Wrong fingerprint, or the certificate's private key is not in the keychain.
owned game folder is missing <file> / too small / SHA-256 does not match PC 1.10 Incomplete or unsupported game folder.
owned game folder has two files mapping to <path> Case-colliding files after canonicalisation.
output already exists; choose a fresh staging directory Each package needs a new output path.
insufficient free disk: need at least N bytes Free space.
GamePayload ... differs from manifest / payloadID differs Payload tampering or incomplete copy.
On headset: Bundled game data could not be verified: ... Manifest or payload check failed; EngineAssetStore enters its failed phase with that message rather than reporting the game data as ready.

Testing

Test What it covers Wired into run_source_checks.py
tools/test_setup_halo.py test_payload_manifest_and_resume, test_modified_payload_is_not_overwritten, test_unexpected_files_in_game_not_bundled, test_wrong_executable_rejected use validate_game / validate_bundled_game_payload with patched constants yes
native/EngineVision/Tests/AssetValidationMain.swift CLI that runs EngineAssetImporter.preflight on a real game folder and prints ASSET_PREFLIGHT_OK no (needs owned files)
native/EngineVision/Tests/BundledAssetValidationMain.swift Bundled-payload import with progress monotonicity, Apple path aliases (/var vs /private/var), and expected rejections no
native/EngineVision/Tests/MutableAssetPreservationMain.swift Changed saves accepted, missing immutable files force repair, saves preserved across a repair no

The explicit signing route has no automated test (it requires a real identity, profile and device). VALIDATION.md records that "Required signing/device inputs: Missing inputs rejected; no personal defaults" was checked for 1.0.0, and that the owner Build91 was installed and startup-verified on a headset; the public Build103 is package-verified and unsigned only.

Related pages

Clone this wiki locally