@driftengine/package 3.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +408 -0
  4. package/bin/drift-package.mjs +82 -0
  5. package/dist/android.d.ts +18 -0
  6. package/dist/android.js +207 -0
  7. package/dist/bootstrap.d.ts +17 -0
  8. package/dist/bootstrap.js +326 -0
  9. package/dist/build.d.ts +11 -0
  10. package/dist/build.js +248 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +343 -0
  13. package/dist/flags.d.ts +57 -0
  14. package/dist/flags.js +122 -0
  15. package/dist/identity.d.ts +51 -0
  16. package/dist/identity.js +75 -0
  17. package/dist/index.d.ts +19 -0
  18. package/dist/index.js +24 -0
  19. package/dist/init.d.ts +24 -0
  20. package/dist/init.js +45 -0
  21. package/dist/ios.d.ts +27 -0
  22. package/dist/ios.js +152 -0
  23. package/dist/macCrossSign.d.ts +34 -0
  24. package/dist/macCrossSign.js +298 -0
  25. package/dist/main/consoleForward.d.ts +43 -0
  26. package/dist/main/consoleForward.js +63 -0
  27. package/dist/main/csp.d.ts +4 -0
  28. package/dist/main/csp.js +55 -0
  29. package/dist/main/entry.d.ts +1 -0
  30. package/dist/main/entry.js +56 -0
  31. package/dist/main/ipc.d.ts +28 -0
  32. package/dist/main/ipc.js +284 -0
  33. package/dist/main/links.d.ts +1 -0
  34. package/dist/main/links.js +29 -0
  35. package/dist/main/menu.d.ts +17 -0
  36. package/dist/main/menu.js +49 -0
  37. package/dist/main/serve.d.ts +21 -0
  38. package/dist/main/serve.js +69 -0
  39. package/dist/main/shortcuts.d.ts +28 -0
  40. package/dist/main/shortcuts.js +26 -0
  41. package/dist/main/splash.d.ts +14 -0
  42. package/dist/main/splash.js +41 -0
  43. package/dist/main/splashTiming.d.ts +36 -0
  44. package/dist/main/splashTiming.js +24 -0
  45. package/dist/main/window.d.ts +19 -0
  46. package/dist/main/window.js +297 -0
  47. package/dist/manifest.d.ts +61 -0
  48. package/dist/manifest.js +91 -0
  49. package/dist/mobile/assetCheck.d.ts +1 -0
  50. package/dist/mobile/assetCheck.js +28 -0
  51. package/dist/mobile/bridgeShim.d.ts +83 -0
  52. package/dist/mobile/bridgeShim.js +208 -0
  53. package/dist/mobile/iosShim.d.ts +42 -0
  54. package/dist/mobile/iosShim.js +104 -0
  55. package/dist/mobile/report.d.ts +27 -0
  56. package/dist/mobile/report.js +122 -0
  57. package/dist/mobile/versionCode.d.ts +12 -0
  58. package/dist/mobile/versionCode.js +17 -0
  59. package/dist/outDir.d.ts +30 -0
  60. package/dist/outDir.js +32 -0
  61. package/dist/preload/bridge.d.ts +77 -0
  62. package/dist/preload/bridge.js +2 -0
  63. package/dist/preload/entry.d.ts +1 -0
  64. package/dist/preload/entry.js +77 -0
  65. package/dist/renderer/fileStore.d.ts +23 -0
  66. package/dist/renderer/fileStore.js +33 -0
  67. package/dist/renderer/host.d.ts +45 -0
  68. package/dist/renderer/host.js +30 -0
  69. package/dist/renderer/nativeHost.d.ts +83 -0
  70. package/dist/renderer/nativeHost.js +125 -0
  71. package/dist/resources.d.ts +71 -0
  72. package/dist/resources.js +83 -0
  73. package/dist/runtime.d.ts +38 -0
  74. package/dist/runtime.js +80 -0
  75. package/dist/runtimeVersion.d.ts +10 -0
  76. package/dist/runtimeVersion.js +18 -0
  77. package/dist/signing.d.ts +49 -0
  78. package/dist/signing.js +135 -0
  79. package/dist/stage.d.ts +22 -0
  80. package/dist/stage.js +138 -0
  81. package/dist/steam/cloudStore.d.ts +30 -0
  82. package/dist/steam/cloudStore.js +54 -0
  83. package/dist/steam/depot.d.ts +29 -0
  84. package/dist/steam/depot.js +39 -0
  85. package/dist/steam/main.d.ts +22 -0
  86. package/dist/steam/main.js +41 -0
  87. package/dist/templates/buildWindows.d.ts +16 -0
  88. package/dist/templates/buildWindows.js +161 -0
  89. package/dist/templates/packageWrapper.d.ts +9 -0
  90. package/dist/templates/packageWrapper.js +80 -0
  91. package/dist/toolchain.d.ts +101 -0
  92. package/dist/toolchain.js +186 -0
  93. package/package.json +73 -0
  94. package/src/android.ts +256 -0
  95. package/src/bootstrap.ts +389 -0
  96. package/src/build.ts +285 -0
  97. package/src/cli.ts +367 -0
  98. package/src/flags.ts +171 -0
  99. package/src/identity.ts +87 -0
  100. package/src/index.ts +31 -0
  101. package/src/init.ts +61 -0
  102. package/src/ios.ts +185 -0
  103. package/src/macCrossSign.ts +338 -0
  104. package/src/main/consoleForward.ts +93 -0
  105. package/src/main/csp.ts +57 -0
  106. package/src/main/entry.ts +63 -0
  107. package/src/main/ipc.ts +311 -0
  108. package/src/main/links.ts +29 -0
  109. package/src/main/menu.ts +53 -0
  110. package/src/main/serve.ts +70 -0
  111. package/src/main/shortcuts.ts +55 -0
  112. package/src/main/splash.ts +42 -0
  113. package/src/main/splashTiming.ts +51 -0
  114. package/src/main/window.ts +316 -0
  115. package/src/manifest.ts +157 -0
  116. package/src/mobile/assetCheck.ts +25 -0
  117. package/src/mobile/bridgeShim.ts +292 -0
  118. package/src/mobile/iosShim.ts +148 -0
  119. package/src/mobile/report.ts +121 -0
  120. package/src/mobile/versionCode.ts +17 -0
  121. package/src/outDir.ts +37 -0
  122. package/src/preload/bridge.ts +73 -0
  123. package/src/preload/entry.ts +91 -0
  124. package/src/renderer/fileStore.ts +38 -0
  125. package/src/renderer/host.ts +96 -0
  126. package/src/renderer/nativeHost.ts +146 -0
  127. package/src/resources.ts +150 -0
  128. package/src/runtime.ts +87 -0
  129. package/src/runtimeVersion.ts +16 -0
  130. package/src/signing.ts +206 -0
  131. package/src/stage.ts +180 -0
  132. package/src/steam/cloudStore.ts +73 -0
  133. package/src/steam/depot.ts +69 -0
  134. package/src/steam/main.ts +69 -0
  135. package/src/templates/buildWindows.ts +163 -0
  136. package/src/templates/packageWrapper.ts +82 -0
  137. package/src/toolchain.ts +210 -0
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Drift Technologies
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ DriftEngine
2
+ Copyright 2026 Drift Technologies
3
+
4
+ This product includes software developed at Drift Technologies
5
+ (https://github.com/drftrun/driftengine).
6
+
7
+ Licensed under the Apache License, Version 2.0. Section 4(d) of that licence
8
+ requires this NOTICE to be reproduced in any derivative work you distribute,
9
+ in the source, the documentation, or a display generated by the work.
package/README.md ADDED
@@ -0,0 +1,408 @@
1
+ # `@driftengine/package`
2
+
3
+ Turns a game built on this engine into an installable application, without the engine ever
4
+ learning what a shell is.
5
+
6
+ ```sh
7
+ npx drift-package init # writes the two scripts your repository has to own
8
+ npx drift-package doctor # is this project packageable, and how would it be signed
9
+ npx drift-package run # the real shell, from source, no installer
10
+ npx drift-package build --target=linux-x64
11
+ npx drift-package verify --contains=<a string from your diff>
12
+ ```
13
+
14
+ ## Start here: `drift-package init`
15
+
16
+ It writes two files into your project and nothing else:
17
+
18
+ | | why it cannot live in the packager |
19
+ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | `scripts/build-windows.ps1` | Something has to start Node on the Windows machine, and that something cannot be a Node program |
21
+ | `scripts/package.mjs` | A stable entry point, and the one that works when the engine is vendored instead of installed, where `npx drift-package` resolves nothing |
22
+
23
+ Both refuse to be overwritten without `--force`, because once you have edited one it is yours.
24
+
25
+ **They are short on purpose, and that is the whole design.** The first project to ship on three
26
+ platforms wrote **421 lines** of wrapper before this existed, and almost none of it was about their
27
+ game: it stripped `ELECTRON_RUN_AS_NODE`, launched a binary without `npx`, read an error field
28
+ `spawnSync` does not throw, asked whether the filesystem could tell a file from a directory,
29
+ checked an exit code PowerShell ignores, fetched a runtime npm had declined to fetch, and relocated
30
+ a build off a network drive. **All of that is now in `drift-package` itself**, which is why what it
31
+ writes for you is the part that genuinely has to be a file in your repository.
32
+
33
+ What the generated PowerShell carries that a hand-written one usually does not:
34
+
35
+ - Every native command through a step that checks `$LASTEXITCODE`. `$ErrorActionPreference = 'Stop'`
36
+ governs cmdlets and not `npm`, and PowerShell 5.1 — what a fresh Windows machine runs — has no
37
+ `$PSNativeCommandUseErrorActionPreference`. Unchecked, a failing `npm ci` arrives as a complaint
38
+ about a missing output directory with the real error scrolled off.
39
+ - Relocation off a mapped or network drive, **and the copy back**. Forgetting the second half is a
40
+ fix that creates the next problem: the build succeeds, says so, lists its files, and `out/` in
41
+ your checkout is empty.
42
+ - Pure ASCII, asserted by `scripts/packaging.test.mjs`. PowerShell 5.1 reads a BOM-less `.ps1` in
43
+ the system ANSI codepage, so one typographic character in a comment takes the _parser_ with it.
44
+
45
+ ## `drift.package.json`
46
+
47
+ Beside your `package.json`, in the project that produces the web build:
48
+
49
+ ```json
50
+ {
51
+ "id": "dev.example.title",
52
+ "name": "Title",
53
+ "entry": "dist/index.html",
54
+ "publisher": "Example",
55
+ "icon": "art/icon.png",
56
+ "window": { "width": 1280, "height": 720, "mode": "windowed", "resizable": true },
57
+ "backend": { "webgpu": "prefer", "allowSoftwareRenderer": false },
58
+ "features": { "clipExport": false, "gamepad": true },
59
+ "splash": { "show": true, "minMs": 1400 },
60
+ "targets": ["linux-x64", "win-x64", "mac-arm64"],
61
+ "steam": { "appId": null }
62
+ }
63
+ ```
64
+
65
+ `id`, `name`, `entry` and `targets` are required; every rejection names the field it is about.
66
+ `icon` is a 1024x1024 PNG and defaults to the engine's own mark, because the alternative default is
67
+ Electron's logo. `window.mode` is what the window is _created_ as — `borderless` makes it
68
+ frameless — and is a different thing from the runtime mode a settings screen changes.
69
+
70
+ ## Settings a game drives itself
71
+
72
+ `createHost` hands back every capability the engine takes, picked for wherever the build is
73
+ running. A game writes one settings screen and ships it to both:
74
+
75
+ ```ts
76
+ import { createHost } from '@driftengine/package';
77
+
78
+ const host = createHost(canvas);
79
+
80
+ await host.display.setMode('fullscreen'); // false in a browser outside a gesture
81
+ if (host.display.canSetSize()) {
82
+ // false in a browser, and while fullscreen
83
+ await host.display.setSize(1920, 1080);
84
+ }
85
+ for (const screen of host.display.displays()) {
86
+ console.log(screen.label, screen.width, screen.height, screen.refreshHz);
87
+ }
88
+ host.store.write('settings.volume', '0.8'); // a file in the shell, localStorage in a tab
89
+ ```
90
+
91
+ Every call that a platform cannot perform answers `false` or `null` rather than doing nothing
92
+ quietly, so a settings screen can grey the control instead of offering one that does not work.
93
+ **And it can ask before it draws**: `canSetSize()` answers the same question `setSize` answers by
94
+ performing it, which until 2026-08-28 was the only way to learn it — reported from outside, where a
95
+ screen that wanted to decide whether to draw a resolution control at all had to resize the window to
96
+ find out, with a no-op probe at boot and a paragraph explaining that it is not what it looks like. It
97
+ is read on each call rather than a constant, because a shell that can resize a window cannot resize
98
+ one that is covering a display; in a shell the question and the refusal are decided by one function,
99
+ so a greyed control cannot be one that would have worked.
100
+ **Render resolution is not here**: covering a display never changes the display's mode, so a game
101
+ that wants fewer pixels changes `RenderQuality.resolutionScale`, which is the engine's business.
102
+
103
+ On a phone, `host.screen` carries what a window does not have — orientation, safe-area insets and
104
+ the wake lock — and each of those can be refused too. iOS refuses an orientation lock outright.
105
+
106
+ ## Leaving the game
107
+
108
+ A game decides how it is left, on every platform, through two members of `Lifecycle` and one
109
+ property that says whether an exit button belongs on screen at all.
110
+
111
+ ```ts
112
+ const host = createHost(canvas);
113
+
114
+ // Draw an Exit button only where one would do something: false in a browser tab, false on iOS.
115
+ if (host.lifecycle.canQuit) menu.addExitButton(() => host.lifecycle.requestQuit());
116
+
117
+ // Asked before the shell closes anything — a window's close button, or Android's back gesture.
118
+ host.lifecycle.onQuitRequest(() => {
119
+ pause();
120
+ confirm('Leave the run?').then((yes) => {
121
+ if (yes) host.lifecycle.requestQuit();
122
+ });
123
+ });
124
+ ```
125
+
126
+ **On Android the back gesture goes to the game first**, in this order: a registered
127
+ `onQuitRequest` handler if there is one; failing that, a synthesised **Escape** keydown and keyup,
128
+ because a game with no quit handler almost always has a pause menu bound to that key — and
129
+ `preventDefault` is the page saying the keystroke was its own. Only if neither happens does the
130
+ host consider closing, and then it asks twice: two back presses inside two seconds with a line on
131
+ screen between them. A single gesture never ends a run.
132
+
133
+ So a game that does nothing at all still behaves: back opens its pause menu if Escape does, and
134
+ otherwise takes two presses to leave.
135
+
136
+ ## Signing
137
+
138
+ Chosen from the environment and printed on every build. `doctor` says what a build would do before
139
+ it does it.
140
+
141
+ | mode | when | what the player sees |
142
+ | ------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
143
+ | `none` | no identity, and the target does not require one | Linux: nothing. Windows: SmartScreen warns on first run |
144
+ | `adhoc` | no identity, and the target requires a signature to run at all | macOS: runs locally; from a download, Gatekeeper refuses until the quarantine flag is cleared |
145
+ | `real` | an identity is in the environment | nothing: the artifact is trusted |
146
+
147
+ macOS with no certificate is `adhoc` rather than unsigned because Apple Silicon's **loader**
148
+ refuses an unsigned binary — that is not Gatekeeper, and no user gesture works around it.
149
+
150
+ Signing material is read from the environment and never from `drift.package.json`, which is
151
+ committed. The names are electron-builder's own: `CSC_LINK`, `CSC_KEY_PASSWORD`, `WIN_CSC_LINK`,
152
+ `WIN_CSC_KEY_PASSWORD`, and `APPLE_ID` / `APPLE_APP_SPECIFIC_PASSWORD` / `APPLE_TEAM_ID` for
153
+ notarisation.
154
+
155
+ **A Steam depot needs none of it**: the client delivering it is signed, so an unsigned Windows
156
+ build inside one raises no warning. A certificate buys direct downloads from a website.
157
+
158
+ ## Steam
159
+
160
+ ```jsonc
161
+ "steam": { "appId": 480 }
162
+ ```
163
+
164
+ ```sh
165
+ npm install steamworks.js # in the game, not here: a game that never ships on Steam ships no SDK
166
+ npx drift-package build --target=linux-x64
167
+ npx drift-package steam --target=linux-x64 # writes the two files steamcmd uploads with
168
+ ```
169
+
170
+ Three things happen once the manifest names an app.
171
+
172
+ **Saves go to Steam Cloud** and a game changes nothing to get them. `host.store` is the same
173
+ `KeyValueStore` it always was; whether those bytes land in a file beside the application or in the
174
+ cloud is the main process's decision, and the cloud is read first at boot so a player who changed
175
+ machines finds their settings. Cloud off for the title or the account degrades to the local file.
176
+
177
+ **Achievements and presence arrive** as `host.services`, which is `null` everywhere else — in a
178
+ browser, where the manifest named no app, and when the copy was launched outside Steam. That last
179
+ one is every run during development, so a game checks for null once rather than calling into
180
+ something that quietly does nothing.
181
+
182
+ **The SDK lives in the main process**, and it has to: `steamworks.js` is a native Node module and
183
+ the renderer runs sandboxed with no Node at all. The packager copies it out of the game's own
184
+ `node_modules` into the artifact and unpacks it from the asar archive, because a `.node` binary
185
+ inside one fails to load with a message about a missing path. If it is not installed, the build
186
+ says so and produces an artifact with no store rather than one whose achievements do nothing.
187
+
188
+ `drift-package steam` writes `app_build_<appid>.vdf` and `depot_build_<depotid>.vdf` beside the
189
+ artifacts, with **nothing set live** — an upload waits in Steam's admin for a person, which is
190
+ Valve's own default and the line people delete by accident. `--branch=beta` publishes to a branch;
191
+ `--depot=` overrides the depot, which otherwise defaults to the app id plus one.
192
+
193
+ ## What a device says about itself
194
+
195
+ ```
196
+ …/index.html?report=1
197
+ ```
198
+
199
+ Draws the renderer string, whether WebGPU is offered, the secure-context answer, the platform and
200
+ the WebView build **over** the running game. It is on every platform and it is deliberately not a
201
+ secret gesture: a tester who has to be told one reports the wrong thing.
202
+
203
+ It matters most on a phone. A pinned desktop Chromium cannot see the class of failure that tier
204
+ exists to catch — WebKit enforces a sixteen-byte uniform array stride that Dawn does not, so the
205
+ shaders that draw everything compiled on every desktop browser and on no iPhone. Every value is
206
+ what the device said, and one that could not be read is shown as `—` rather than filled in.
207
+
208
+ ## Getting the toolchain, on any machine
209
+
210
+ ```sh
211
+ npx drift-package bootstrap # for the targets in drift.package.json
212
+ npx drift-package bootstrap --target=android # or one of them
213
+ ```
214
+
215
+ Nothing is installed system-wide and nothing asks for a password. Everything lands under
216
+ `~/.cache/driftengine/toolchain` (`DRIFT_TOOLCHAIN_HOME` moves it), and a machine that already has
217
+ a working `JAVA_HOME` or `ANDROID_HOME` is used as it is rather than duplicated.
218
+
219
+ | target | what bootstrap fetches |
220
+ | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
221
+ | `linux-x64`, `win-x64`, `mac-*` | the Electron runtime, which an `npm install` with scripts disabled leaves out |
222
+ | `android` | Temurin JDK 21, Google's command-line tools, the SDK licences, `platform-tools`, `platforms;android-35`, `build-tools;35.0.0`, and Gradle 8.9 |
223
+ | `ios` | nothing, and it says so: Xcode is an App Store install on a Mac |
224
+
225
+ Versions are pinned in `src/toolchain.ts`. Moving one is a commit with a reason in it, because a
226
+ build that quietly changed its build tools is a build whose output changed for no recorded reason.
227
+
228
+ `doctor` tells you which targets this machine is not ready for, and prints the command that fixes
229
+ each one.
230
+
231
+ ## Android
232
+
233
+ ```sh
234
+ npx drift-package bootstrap --target=android
235
+ npx drift-package build --target=android
236
+ adb install "out/android/<Game>-<version>.apk"
237
+ ```
238
+
239
+ The host is one activity holding a WebView, serving the game from
240
+ `https://appassets.androidplatform.net/` — an https origin, so a secure context, so ES modules,
241
+ workers and `crossOriginIsolated` all work without a loopback server listening inside a game. The
242
+ bridge is injected before the game's own scripts through `addDocumentStartJavaScript`, which is the
243
+ mobile equivalent of the desktop preload, so `createHost` finds the same `__driftHost` it finds on
244
+ the desktop.
245
+
246
+ **Signing is not optional here and there is no unsigned option to offer**: Android refuses to
247
+ install an unsigned APK at all. Without a keystore the packager generates one and says so on every
248
+ build. That APK installs on a device with developer mode on; it cannot go to the Play Store.
249
+
250
+ **The generated key is in the toolchain cache, and it is the application's permanent identity:**
251
+
252
+ ```
253
+ ~/.cache/driftengine/toolchain/keystores/<your app id>.jks alias drift, password driftengine
254
+ ```
255
+
256
+ Not beside the artifact — this page said that until 2026-08-29 and it was wrong, which matters
257
+ because of what the file is. A device refuses an upgrade signed with a _different_ key, reading it
258
+ as a different application wearing the same name, and the only way out on the phone is to
259
+ uninstall, which throws away the player's save. Three things follow, and the first is the one to
260
+ act on today:
261
+
262
+ - **Back it up.** It lives under `~/.cache`, a directory whose entire purpose is being safe to
263
+ delete. Losing it means everybody who installed a build has to uninstall before they can install
264
+ the next one.
265
+
266
+ ```sh
267
+ cp ~/.cache/driftengine/toolchain/keystores/<your app id>.jks ~/wherever-you-keep-things/
268
+ ```
269
+
270
+ - **A second machine makes a second key, silently.** Build somewhere else and the packager finds no
271
+ keystore for that application id and generates a fresh one: a valid APK, signed by a stranger as
272
+ far as any phone holding the first is concerned. A game that will ever be built on two machines
273
+ needs an owned key, not the generated one.
274
+
275
+ - **Check before you send.** `doctor` says which key a build would use before it spends the minutes,
276
+ and the build prints it again on the way past. To verify an APK you are about to hand somebody:
277
+
278
+ ```sh
279
+ ~/.cache/driftengine/toolchain/android-sdk/build-tools/35.0.0/apksigner \
280
+ verify --print-certs out/android/<your game>.apk
281
+ ```
282
+
283
+ If the digest is not the one you expect, **do not send that file** to anybody who already has a
284
+ build installed: it will not install over the top, and the message the phone shows says nothing
285
+ about signing.
286
+
287
+ `DRIFT_ANDROID_KEYSTORE`, `DRIFT_ANDROID_KEYSTORE_PASSWORD` and `DRIFT_ANDROID_KEY_ALIAS` take over
288
+ the moment there is a real one, through the environment and never through `drift.package.json`,
289
+ which is committed.
290
+
291
+ **The renderer is the platform's WebView, not a bundled Chromium.** That is the whole reason a
292
+ mobile build is 20 MB rather than 200, and it is also why WebGL2 is the baseline there: what the
293
+ device offers is what the game gets, and the acceptance probe decides.
294
+
295
+ ## Where each artifact is built
296
+
297
+ An artifact is built on the platform it targets, with one exception, and `build` refuses the ones
298
+ this machine cannot honestly produce.
299
+
300
+ | target | built on | how |
301
+ | ---------------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
302
+ | `linux-x64` | Linux | `npx drift-package build --target=linux-x64` |
303
+ | `win-x64` | Windows, a VM is fine | `drift-package init` writes `scripts/build-windows.ps1` into your project; run it there |
304
+ | `mac-arm64`, `mac-x64` | macOS, **or Linux** | [`scripts/build-macos.sh`](scripts/build-macos.sh), or `build --target=mac-arm64` |
305
+ | `android` | anywhere, the toolchain is Java | `npx drift-package build --target=android` |
306
+ | `ios` | macOS with Xcode | `npx drift-package build --target=ios` |
307
+
308
+ **macOS is the exception, and only from Linux.** Nothing in a Mac build is compiled: Electron ships
309
+ a prebuilt darwin runtime, so packing one is file copying and a plist. What stopped it was never
310
+ the artifact but the signature — electron-builder skips signing anywhere but a Mac, and Apple
311
+ Silicon's loader refuses an unsigned binary outright — so a cross-build applies an ad-hoc signature
312
+ with `rcodesign`, which `bootstrap` fetches. Two things a Linux host cannot do are named rather
313
+ than worked around: there is no `.dmg`, because that is `hdiutil`, so the artifact is a `.zip`; and
314
+ only the machine it runs on can prove it launches, so open one on a Mac before it reaches anybody
315
+ else. A Developer ID in `CSC_LINK` is **refused** here rather than ignored, because a build that
316
+ came out ad-hoc while reporting a certificate is the one that gets sent to strangers by mistake.
317
+
318
+ Windows deliberately does not go through Wine: the NSIS target can be cross-built and the signing
319
+ step cannot reach a Windows certificate store, so the platform whose warnings are hardest to shake
320
+ would be the one built with no way to sign it.
321
+
322
+ ### `--out=<dir>`, for a project on a filesystem that misreports itself
323
+
324
+ Everything lands in `out/<target>/` inside the project unless `--out=` says otherwise:
325
+
326
+ ```sh
327
+ npx drift-package build --target=win-x64 --out=C:\drift-out
328
+ ```
329
+
330
+ The layout under the root is the same, so that gives `C:\drift-out\win-x64\stage`. An absolute
331
+ path is used as it stands; a relative one resolves against wherever you ran the command, which is
332
+ not necessarily `--project=`.
333
+
334
+ **It exists because a staging copy can refuse itself.** Node's `fs.cp` will not copy a thing into
335
+ itself and decides that by walking the destination's ancestors comparing `dev` and `ino` against
336
+ the source. On a normal disk this never fires. Measured by a consumer on a Windows VM with the
337
+ project on a drive mapped to a folder on a Linux machine, `drift.package.json` and `out/` both
338
+ answered `dev=66313 ino=-112686486700016` — one identity for a file and a directory — and the very
339
+ first copy of the build failed with a sentence about copying a file into a subdirectory of itself,
340
+ which is the one thing that was not happening. Point `--out=` at a local disk and the condition
341
+ cannot arise.
342
+
343
+ ## iOS
344
+
345
+ ```sh
346
+ npx drift-package bootstrap --target=ios # fetches XcodeGen; Xcode is an App Store install
347
+ npx drift-package build --target=ios
348
+ ```
349
+
350
+ One view controller holding a `WKWebView`, serving the game over a custom scheme through
351
+ `WKURLSchemeHandler` — `file://` is not an origin, and a game loaded from one fails to import its
352
+ own modules. The renderer is WebKit and there is no choice about that: Apple requires it, which is
353
+ why the engine's WebGL2 baseline matters here and why the acceptance probe decides whether WebGPU
354
+ is among what the device offers.
355
+
356
+ **The synchronous problem is worse than Android's.** A `WKScriptMessageHandler` is one-way and
357
+ there is no synchronous return from native code to JavaScript at all — so everything the engine
358
+ must read synchronously is _injected_ as a literal before the page's first line of script, and only
359
+ the mutations are messages. `WKScriptMessageHandlerWithReply` is what lets a save report whether it
360
+ worked, and it is why the deployment target is iOS 15.
361
+
362
+ Saving goes to the share sheet, because there is no Downloads folder on this platform: a clip is
363
+ written to the temporary directory and handed to `UIActivityViewController`, so the player sends it
364
+ to Photos, Files or a message.
365
+
366
+ **Unsigned unless `APPLE_TEAM_ID` is set.** Apple's free tier signs a build for your own device for
367
+ seven days at a time through Xcode's own interface rather than through anything a script can drive,
368
+ so the default output is an archive and an unsigned `.ipa` — the shape a sideloading tool re-signs.
369
+
370
+ **Not yet run on a Mac.** The Swift, the project spec and the build step are written against
371
+ Apple's documented behaviour and nothing here has compiled them. What is not guessed is the bridge,
372
+ which is shared with Android and tested, and the containment rule in the scheme handler, which
373
+ mirrors the desktop shell's. **One question still gates the tier**: whether WebKit treats a custom
374
+ scheme as a secure context. If it does not, `crossOriginIsolated` is false and the answer is a
375
+ loopback server — `SchemeHandler.swift` is the only file that changes, and
376
+ `drift://localhost/__drift/originProbe.html` answers it in one look on a device. The host must be
377
+ `localhost`, since that is what makes the origin trustworthy at all, and the shell's own assets
378
+ live under `__drift/` rather than behind a second host for the same reason.
379
+
380
+ ## Three things that will bite
381
+
382
+ **A network share refuses the first copy of a build, and blames itself.** `fs.cp` walks the
383
+ destination's parents and compares each one's `dev` and `ino` with the source's, to avoid copying
384
+ something into itself. A redirector that gets no file index from the server invents a constant one,
385
+ and on Windows `dev` is already the volume's serial number — so a file and a folder on the same
386
+ share compare identical and a perfectly legitimate copy is refused, with `cannot copy … to a
387
+ subdirectory of self …` ten minutes into a build, about the thing furthest from the cause.
388
+
389
+ **`drift-package` already refuses this before a build starts, so you do not need to check for it.**
390
+ `doctor` refuses it first, which is where it belongs for a person's time — a build that cannot
391
+ succeed should stop before it downloads a runtime — and `stageApp` refuses it again immediately
392
+ before its first copy, which is where it belongs for correctness. The second one is not redundant:
393
+ this package's `exports` map is `"./*": "./*"`, so staging is importable directly and a route that
394
+ never passes through `doctor` would otherwise get the `cp` message. It also runs before the previous
395
+ stage directory is removed, so a refused build keeps its last good output.
396
+
397
+ What it prints names the volume, both witnesses with their `dev` and `ino`, and the remedy —
398
+ `drift-package build --out=/a/local/disk`, which is cheaper than moving a project with tens of
399
+ gigabytes of assets. If your launcher carries a copy of this check, it can go.
400
+
401
+ **`ELECTRON_RUN_AS_NODE`.** Any Electron-based terminal — an editor's integrated one is the common
402
+ case — exports this for the processes it spawns, and it turns Electron into plain Node. A packaged
403
+ game inheriting it prints a Node banner and dies on `Cannot find module 'electron'`. Both scripts
404
+ above unset it and so does `drift-package run`.
405
+
406
+ **A fresh `npm ci` may not fetch the Electron binary.** The package installs; its postinstall,
407
+ which downloads the runtime, is skipped by some environments. `node node_modules/electron/install.js`
408
+ fetches it.