Repository navigation
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.
| 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. |
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"]
Both routes deliver the same two app-bundle resources, and the app refuses anything that does not satisfy this contract.
HaloVision.app/
GamePayloadManifest.json
GamePayload/
halo.exe
strings.dll
halo-vision-registry.txt
config.txt (optional)
*.bik (optional movies)
maps/*.map
shaders/*.bin
| 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.
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.
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:
-
GamePayloadManifest.jsonandGamePayload/exist and neither is a symlink. -
formatVersion == 1and a non-emptyfileslist of objects. - Each path is safe (not absolute, no
.., notengine-vision-import.json), unique case-insensitively, exists as a regular non-symlink file, and matches the manifest byte length and SHA-256. - The actual tree under
GamePayload/(viatree_inventory) equals the manifest exactly: "GamePayload contains files absent from its exact manifest". -
payloadIDequals the recomputed digest;fileCountandtotalBytesmatch. -
executableSHA256equalsHALO_SHA256. - Every
REQUIRED_GAME_FILESentry is present at its minimum size (halo.exe2,000,000;maps/ui.map2,000,000;maps/a10.map90,000,000;maps/bitmaps.map300,000,000;maps/sounds.map200,000,000;shaders/vsh.bin30,000;shaders/fx.bin900,000;strings.dll1,000,000;halo-vision-registry.txt100).
It returns {manifest, payload_id, file_count, total_bytes, executable_sha256, files}.
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.
After ./setup.sh (default --stage xcode):
-
.setup/GamePayloadand.setup/GamePayloadManifest.jsonexist and are verified. -
project.ymlreferences them as optional resources (optional: true), along with the four visual-pack files, and setsCODE_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. - 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.halovisionwith a unique bundle identifier they control; keeps automatic signing; selects the paired, unlocked Vision Pro (Developer Mode enabled); presses Run. - 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).
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.
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
1. App - validate_app() (lines 140-167):
-
Info.plistexists;CFBundleIdentifierequals--bundle-id(so the direct-route defaultorg.example.halovisionmust be replaced by building with the user's bundle ID in Xcode, as BUILDING.md instructs). -
CFBundleSupportedPlatformscontainsXROS. -
/usr/bin/filereportsMach-O 64-bit executable arm64. -
/usr/bin/otool -lshowsplatform 11(visionOSLC_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").
stage_and_sign_app() (lines 321-372):
- Copy the source app to
<output>/.HaloVision-staging-<uuid>.appwithshutil.copytree(symlinks=False, copy_function=clone_copy)(APFS clones). - Remove any existing
GamePayload/in the copy and recreate it; clone-copy every game file to its canonical path. - Write
GamePayloadManifest.jsonatomically (write_json_atomic: temp name.<name>.<uuid>.tmp, thenreplace), validate it, and require itspayload_idto equal the source inventory hash. - Copy the profile to
embedded.mobileprovision. -
codesign --force --sign <SHA1> --entitlements <plist> --generate-entitlement-der --timestamp=none <staging app>. -
codesign --verify --deep --strict --verbose=4 <staging app>. - Read back the signed entitlements (
codesign -d --entitlements :-, parsing from<?xmlor<plist) and re-check the application and team identifiers. - 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.
| 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/.
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.
When the app starts, EngineAssetStore (EngineAssets.swift:378-470) decides where the game root is:
-
Bundled payload present (
GamePayload/orGamePayloadManifest.jsonexists in the bundle):prepareBundledGame()(line 404) loads and checks the manifest, then targetsApplication Support/HaloVision/PackagedGames/<payloadID>. "A distinct version directory preserves earlier game files and saves." If that directory already validates against the manifest (receiptpayloadID, 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/Gameand, if missing, lets the user pick a folder (SwiftUI.fileImporter) which is imported without a manifest (required files, minimum sizes and executable hash checked).
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.
EngineAssetImporter.importFolder() (EngineAssets.swift:149-228):
- With a manifest, entries come from the manifest's exact relative paths (the code notes this avoids reconstructing paths from
/varvs/private/varURLs); each source must be a regular, non-symlink file of the manifest size. - 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. - With a manifest,
preserveMutableGameData()copies the player's writable treeUsers/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." - An
engine-vision-import.jsonreceipt (importedAt,fileCount,totalBytes,executableSHA256,payloadID) is written andvalidate()re-checks required files and the executable hash. -
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. - 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.
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.
| 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. |
| 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.
Documents master-chef at commit 9f915af (v1.0.3). Unofficial project, not affiliated with Microsoft, Bungie, Gearbox or Apple. Original code is MIT licensed; game content is not included.
Overview
- Architecture Overview
- Repository Layout
- Glossary
- Environment Variables
- Contributing Guide
- Open Questions
Translation
- Static Translation Pipeline
- XWA Decoder and Lifter
- Function Address Lists
- EngineReuse Runtime
- x87 Floating Point
Host runtime
- EngineHost Overview
- Win32 Compatibility Layer
- Threading and Synchronization
- Guest Memory and Heap
- Engine Overrides and Hooks
- Runtime Settings
Graphics
- Direct3D9 Bridge
- Metal Renderer
- Shader Translation
- Textures and Texture Packs
- Geometry Fast Paths
- Radial Fog
Panorama and presentation
- Panorama System
- Panorama Budget and LOD
- Frame Pacing
- visionOS App
- Immersive Presenter
- Layer Alignment
Audio and input
Tooling and process