@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/dist/build.js ADDED
@@ -0,0 +1,248 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { buildAndroid } from './android.js';
6
+ import { buildIos } from './ios.js';
7
+ import { crossSignMacApp, zipMacApp } from './macCrossSign.js';
8
+ import { outFor } from './outDir.js';
9
+ import { resourceFile } from './resources.js';
10
+ import { ensureElectronRuntime } from './runtime.js';
11
+ import { planSigning } from './signing.js';
12
+ import { consumerVersion, stageApp } from './stage.js';
13
+ import { findRcodesign } from './toolchain.js';
14
+ /**
15
+ * An installable artifact per target.
16
+ *
17
+ * **The builder configuration is derived, not authored.** Every value it needs is already in
18
+ * `drift.package.json`, and a second configuration file would be a second place for the
19
+ * application id to be wrong — which is the failure that is silent until saves land in a directory
20
+ * nobody looks in.
21
+ *
22
+ * **Linux gets a native artifact and that is a Steam Deck decision**, recorded here because it
23
+ * looks like an ordinary target otherwise: a Windows build under Proton is the default assumption
24
+ * and it carries a known regression — gamepads undetected in Electron under Proton after 26.6.10,
25
+ * where the same build works natively. Valve's own guidance points the same way.
26
+ *
27
+ * **A target is refused where this machine cannot honestly produce it**, and the interesting word
28
+ * is *honestly*: the danger was never a build error, it is an artifact that looks finished and
29
+ * will not launch, or one that cannot be signed. `scripts/` carries the Windows and macOS hosts.
30
+ *
31
+ * **macOS is the one target that moved off that rule, and only from Linux.** Nothing in a Mac
32
+ * build is compiled — Electron ships a prebuilt darwin runtime and packing it is file copying and
33
+ * a plist — so the artifact itself was never the problem. The signature was: electron-builder
34
+ * skips signing off darwin, and an unsigned binary is refused by Apple Silicon's loader. With
35
+ * `rcodesign` putting an ad-hoc signature on afterwards, the two things a Linux host still cannot
36
+ * do are named rather than worked around — there is no `.dmg`, because that is `hdiutil`, and
37
+ * there is no proof it launches, because that needs the machine it runs on. See `macCrossSign.ts`.
38
+ */
39
+ const HOST_FOR = {
40
+ 'win-x64': 'win32',
41
+ 'mac-arm64': 'darwin',
42
+ 'mac-x64': 'darwin',
43
+ 'linux-x64': 'linux',
44
+ /* Android's toolchain is Java and it runs anywhere; iOS needs Xcode, which is a Mac. */
45
+ android: null,
46
+ ios: 'darwin',
47
+ };
48
+ /**
49
+ * Why this machine cannot build that target, or null.
50
+ *
51
+ * A Mac target is allowed from Linux and still refused from Windows — that second refusal is
52
+ * electron-builder's own and would fire a few seconds later anyway, so it is stated here where
53
+ * the reason can be given.
54
+ */
55
+ function refuseHost(target, platform) {
56
+ const host = HOST_FOR[target];
57
+ if (host === null || platform === host)
58
+ return null;
59
+ if ((target === 'mac-arm64' || target === 'mac-x64') && platform === 'linux')
60
+ return null;
61
+ if (target === 'mac-arm64' || target === 'mac-x64') {
62
+ return (`${target} is cross-built from Linux and built natively on a Mac, and this is ${platform}. ` +
63
+ 'electron-builder refuses a Mac target on Windows outright, so there is nothing to work ' +
64
+ 'around here.');
65
+ }
66
+ return (`${target} is built on ${String(host)}, and this is ${platform}. ` +
67
+ 'See packages/package/scripts for the Windows and macOS hosts; cross-building under ' +
68
+ 'Wine cannot reach a certificate store, which is the one platform where that matters.');
69
+ }
70
+ export async function build(manifest, cwd, targets, options = {}) {
71
+ const { build: run, Arch, Platform } = await import('electron-builder');
72
+ /* One artifact per target, and the architecture is named rather than inherited: a build on an
73
+ Apple Silicon machine defaults to arm64, so `mac-x64` asked for on one would quietly produce
74
+ the wrong slice. */
75
+ const targetFor = (name) => name === 'win-x64'
76
+ ? Platform.WINDOWS.createTarget(undefined, Arch.x64)
77
+ : name === 'linux-x64'
78
+ ? Platform.LINUX.createTarget(undefined, Arch.x64)
79
+ : Platform.MAC.createTarget(undefined, name === 'mac-x64' ? Arch.x64 : Arch.arm64);
80
+ const electronVersion = await installedElectronVersion();
81
+ for (const target of targets) {
82
+ if (target === 'android') {
83
+ await buildAndroid(manifest, cwd, outFor(cwd, target, options), options);
84
+ continue;
85
+ }
86
+ if (target === 'ios') {
87
+ await buildIos(manifest, cwd, outFor(cwd, target, options), options);
88
+ continue;
89
+ }
90
+ const refusal = refuseHost(target, process.platform);
91
+ if (refusal !== null)
92
+ throw new Error(refusal);
93
+ /*
94
+ * **The runtime, before anything is staged.** `installedElectronVersion` below reads a version
95
+ * out of `electron/package.json`, which is in the npm tarball — so it answers even when the
96
+ * postinstall that downloads the actual binary never finished, and electron-builder then finds
97
+ * nothing to package several steps and several minutes later. Measured in this repository on
98
+ * 2026-08-29: a version of 43.4.1 against no `dist`. See `runtime.ts`.
99
+ */
100
+ await ensureElectronRuntime();
101
+ const signing = planSigning(target, process.env, process.platform);
102
+ if (signing.refusal !== null)
103
+ throw new Error(`${target}: ${signing.refusal}`);
104
+ console.log(`[drift-package] ${target}: signing ${signing.mode} — ${signing.reason}`);
105
+ /*
106
+ * **Looked up before anything is staged.** Signing is the last step of a build that takes
107
+ * minutes, and a missing signer discovered there costs the whole thing — so the tool is
108
+ * resolved while the failure is still cheap.
109
+ */
110
+ const rcodesign = signing.crossSign ? await findRcodesign() : null;
111
+ if (signing.crossSign && rcodesign === null) {
112
+ throw new Error(`${target} is signed here by rcodesign, and there is none on this machine. ` +
113
+ `Run \`drift-package bootstrap --target=${target}\`, which fetches it into a cache.`);
114
+ }
115
+ const outDir = outFor(cwd, target, options);
116
+ const stageDir = join(outDir, 'stage');
117
+ await stageApp(manifest, cwd, stageDir, { development: false, ...options });
118
+ /* Where the signing step found the `.app`, so the archive below does not have to guess at
119
+ electron-builder's own naming for the packed directory. */
120
+ const signed = { app: null };
121
+ await run({
122
+ targets: targetFor(target),
123
+ /*
124
+ * **The staged directory is the project, not just the application.** Pointed at the
125
+ * consumer's own directory instead, electron-builder looks for *their* `package.json`,
126
+ * tries to install *their* dependencies into the artifact, and fails on a project that
127
+ * has neither — which a game built by any other toolchain may well not.
128
+ */
129
+ projectDir: stageDir,
130
+ config: {
131
+ appId: manifest.id,
132
+ productName: manifest.name,
133
+ electronVersion,
134
+ /* One 1024px PNG for all three platforms: electron-builder derives an `.ico` and an
135
+ `.icns` from it, and a game that supplies its own replaces exactly this line. */
136
+ icon: manifest.icon === null
137
+ ? await resourceFile('assets', 'icon.png', options.resourceRoot)
138
+ : resolve(cwd, manifest.icon),
139
+ directories: { output: outDir },
140
+ /* The staged directory is the application, whole. Nothing is filtered here because
141
+ nothing that should not ship was put there: see `stage.ts` for what it contains. */
142
+ files: ['**/*'],
143
+ /*
144
+ * **A native module cannot be loaded from inside an asar archive.** `steamworks.js` is a
145
+ * `.node` binary, and one packed into the archive fails at `require` with a message about
146
+ * a path that does not exist — which reads as a missing dependency rather than as a
147
+ * packing decision. Unpacked, and only it.
148
+ */
149
+ asarUnpack: ['**/node_modules/steamworks.js/**'],
150
+ /*
151
+ * **The signature goes on between packing and archiving, and that ordering is the whole
152
+ * point.** `afterPack` runs once the `.app` is complete and before any target wraps it, so
153
+ * the signed bundle is the one that ends up inside the `.zip`. Signing afterwards would
154
+ * leave the artifact people download carrying the unsigned copy.
155
+ */
156
+ afterPack: rcodesign === null ? undefined : macSignStep(rcodesign, target, signed),
157
+ mac: {
158
+ hardenedRuntime: signing.mode === 'real',
159
+ category: 'public.app-category.games',
160
+ /*
161
+ * `'-'` is `codesign`'s own spelling of an ad-hoc identity. `null` means *do not sign*,
162
+ * which on Apple Silicon means *does not run* — and is exactly right when this build
163
+ * signs the app itself a few lines below, because electron-builder would otherwise log
164
+ * a warning about skipping a step that is not being skipped.
165
+ */
166
+ identity: signing.crossSign ? null : signing.mode === 'adhoc' ? '-' : undefined,
167
+ notarize: signing.notarise,
168
+ /*
169
+ * **Both of electron-builder's Mac archives are macOS-only, for different reasons.** A
170
+ * `.dmg` is built by `hdiutil`, which exists nowhere else. Its `.zip` is worse than
171
+ * unavailable: it is produced by 7-Zip off darwin, which dereferences the symlinks every
172
+ * `.framework` is made of, and the result is a corrupt bundle rather than an error.
173
+ *
174
+ * So a cross-build asks for `dir` — pack the `.app` and stop — and the archive is made
175
+ * afterwards by `zipMacApp`, which keeps the links.
176
+ */
177
+ target: signing.crossSign ? ['dir'] : ['dmg', 'zip'],
178
+ },
179
+ win: {
180
+ target: ['nsis', 'zip'],
181
+ /* Nothing is named here: electron-builder reads `WIN_CSC_LINK`/`CSC_LINK` from the
182
+ environment itself, and repeating them is a second place for them to be wrong. */
183
+ },
184
+ linux: { target: ['AppImage', 'tar.gz'], category: 'Game' },
185
+ },
186
+ });
187
+ /*
188
+ * **The archive, for a cross-build only.** electron-builder was asked for `dir` above because
189
+ * neither of its Mac archives can be produced correctly off a Mac, so the artifact somebody
190
+ * downloads is made here — see `zipMacApp` for what 7-Zip does to a framework.
191
+ *
192
+ * The name matches what electron-builder would have written, so a Mac-built release and a
193
+ * Linux-built one are the same file under the same name.
194
+ */
195
+ if (signed.app !== null) {
196
+ const version = await consumerVersion(cwd);
197
+ const arch = target === 'mac-x64' ? 'x64' : 'arm64';
198
+ await zipMacApp(signed.app, join(outDir, `${manifest.name}-${version}-${arch}-mac.zip`));
199
+ }
200
+ }
201
+ }
202
+ /**
203
+ * The `afterPack` hook that ad-hoc signs a cross-built `.app`, or nothing on a Mac.
204
+ *
205
+ * A factory rather than an inline closure so the resolved signer is a `string` inside it: the
206
+ * lookup and the refusal happen once, before a build starts, and this cannot be reached without
207
+ * one. `electronPlatformName` is checked because the hook is called for whatever is being packed.
208
+ */
209
+ function macSignStep(rcodesign, target, signed) {
210
+ return async (context) => {
211
+ if (context.electronPlatformName !== 'darwin')
212
+ return;
213
+ const app = await packedApp(context.appOutDir);
214
+ console.log(`[drift-package] ad-hoc signing ${app} with rcodesign`);
215
+ await crossSignMacApp({
216
+ app,
217
+ rcodesign,
218
+ arch: target === 'mac-x64' ? 'x64' : 'arm64',
219
+ });
220
+ console.log('[drift-package] signed, and every nested bundle checked');
221
+ signed.app = app;
222
+ };
223
+ }
224
+ /** The `.app` electron-builder just wrote, found rather than named after the product. */
225
+ async function packedApp(appOutDir) {
226
+ const { readdir } = await import('node:fs/promises');
227
+ const found = (await readdir(appOutDir)).filter((entry) => entry.endsWith('.app'));
228
+ if (found.length !== 1) {
229
+ throw new Error(`expected one .app in ${appOutDir} and found ${found.length ? found.join(', ') : 'none'}`);
230
+ }
231
+ return join(appOutDir, found[0]);
232
+ }
233
+ /**
234
+ * The Electron the artifact will carry, read from what is installed rather than declared.
235
+ *
236
+ * The staged application has no dependencies of its own — both entry points are bundled — so
237
+ * electron-builder has nothing to infer a version from and would refuse. Reading the installed
238
+ * one keeps the artifact and this repository on the same runtime by construction.
239
+ */
240
+ export async function installedElectronVersion() {
241
+ const require = createRequire(import.meta.url);
242
+ const manifestPath = require.resolve('electron/package.json');
243
+ const raw = JSON.parse(await readFile(manifestPath, 'utf8'));
244
+ const version = raw.version;
245
+ if (typeof version !== 'string')
246
+ throw new Error('electron is installed without a version');
247
+ return version;
248
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
4
+ import { writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { bootstrap } from './bootstrap.js';
8
+ import { build, installedElectronVersion } from './build.js';
9
+ import { planFlags } from './flags.js';
10
+ import { refuseLyingFilesystem } from './identity.js';
11
+ import { init } from './init.js';
12
+ import { outFor } from './outDir.js';
13
+ import { parseManifest } from './manifest.js';
14
+ import { planSigning } from './signing.js';
15
+ import { electronMajor } from './runtimeVersion.js';
16
+ import { depotScripts } from './steam/depot.js';
17
+ import { androidToolchain, findRcodesign, needsRcodesign, requirementsFor } from './toolchain.js';
18
+ import { stageApp } from './stage.js';
19
+ import { resourceDir } from './resources.js';
20
+ /*
21
+ * **`ELECTRON_RUN_AS_NODE` is removed from this process before anything else happens.**
22
+ *
23
+ * Any terminal that is itself an Electron application — an editor's built-in one is the ordinary
24
+ * case, and a developer's own shell had it set — exports the variable to everything it spawns.
25
+ * Inherited, it turns Electron into plain Node: a packaged build dies on `Cannot find module
26
+ * 'electron'` under a Node banner, which looks nothing like its cause, and in that mode the
27
+ * built-in module genuinely is not there.
28
+ *
29
+ * **It used to be removed for `run` alone**, in `launchEnv` below, which left `build` and every
30
+ * other subcommand inheriting it — and `build` is the one that spends twenty minutes before
31
+ * anything can go wrong. Deleted rather than set empty: Electron treats the variable as set
32
+ * whatever its value.
33
+ */
34
+ delete process.env.ELECTRON_RUN_AS_NODE;
35
+ /**
36
+ * `drift-package` — the command a consumer runs to get an installable application.
37
+ *
38
+ * Four subcommands, and `verify` is the one that is not obvious. An artifact that was never
39
+ * rebuilt is byte-identical in every respect a build log can see, so the only honest check is to
40
+ * look inside the shipped thing for a string from the change. This repository already learned that
41
+ * about deploys: an asset hash proves nothing.
42
+ *
43
+ * Every failure exits non-zero with the reason on stderr. A packaging command that prints a
44
+ * complaint and exits 0 is a command that passes in a script.
45
+ */
46
+ function loadManifest(cwd) {
47
+ const path = join(cwd, 'drift.package.json');
48
+ try {
49
+ return parseManifest(JSON.parse(readFileSync(path, 'utf8')));
50
+ }
51
+ catch (cause) {
52
+ throw new Error(`could not read ${path}: ${String(cause)}`);
53
+ }
54
+ }
55
+ function walk(dir, out = []) {
56
+ for (const entry of readdirSync(dir)) {
57
+ const full = join(dir, entry);
58
+ if (statSync(full).isDirectory())
59
+ walk(full, out);
60
+ else
61
+ out.push(full);
62
+ }
63
+ return out;
64
+ }
65
+ async function doctor(cwd, options = {}) {
66
+ /*
67
+ * **Before anything is read, downloaded or staged**, because the failure it replaces arrives ten
68
+ * minutes in and talks about copying a file into a subdirectory of itself. See `identity.ts`:
69
+ * a share that invents a file index makes Node refuse a copy that is perfectly legitimate.
70
+ */
71
+ refuseLyingFilesystem(cwd, options.outRoot);
72
+ const manifest = loadManifest(cwd);
73
+ /*
74
+ * **The packager's own three directories, checked here rather than met mid-build.** `assets/`
75
+ * becomes every desktop artifact's `drift://shell/` origin, and a copy of the engine that
76
+ * flattened its packages has moved all three out of reach — which used to arrive as `ENOENT` on a
77
+ * path that is not in the project, after esbuild had already run. `doctor` is where this file
78
+ * already refuses what a build cannot do, before anything is downloaded or staged.
79
+ *
80
+ * `assets` alone: it is the one every target needs, and naming `android/` or `ios/` on a machine
81
+ * building for Linux would refuse a build that was never going to touch them. The two mobile
82
+ * builders check their own.
83
+ */
84
+ await resourceDir('assets', options.resourceRoot);
85
+ /*
86
+ * **Against the Electron this machine would actually package**, not the one the package declares.
87
+ * A switch written for one Chromium and applied to another is ignored without a word, so doctor
88
+ * is where a runtime too old for it is named — before a build spends its minutes and ships a
89
+ * game that silently has no WebGPU.
90
+ */
91
+ const electron = electronMajor(await installedElectronVersion().catch(() => null));
92
+ for (const target of manifest.targets) {
93
+ const plan = planFlags(manifest, target, electron);
94
+ if (plan.refusal !== null)
95
+ throw new Error(`${target}: ${plan.refusal}`);
96
+ for (const note of plan.notes)
97
+ console.log(` ${target}: ${note}`);
98
+ }
99
+ console.log(`ok: ${manifest.name} (${manifest.id}) for ${manifest.targets.join(', ')}`);
100
+ for (const target of manifest.targets) {
101
+ const signing = planSigning(target, process.env, process.platform);
102
+ console.log(` ${target}: signing ${signing.mode} — ${signing.reason}`);
103
+ }
104
+ console.log(` splash: ${manifest.splash.show ? `the engine badge, held ${manifest.splash.minMs} ms` : 'off'}`);
105
+ /*
106
+ * **What each target still needs, and the command that fetches it.** A `doctor` that only
107
+ * validated a manifest would pass on a machine with no Java and no SDK, and the failure would
108
+ * arrive twenty minutes later from Gradle.
109
+ */
110
+ for (const target of manifest.targets) {
111
+ const ready = await toolchainReady(target);
112
+ if (ready)
113
+ continue;
114
+ console.log(` ${target}: needs ${requirementsFor(target).join(', ')} — ` +
115
+ `run \`drift-package bootstrap --target=${target}\``);
116
+ }
117
+ return manifest;
118
+ }
119
+ function verify(cwd, needle) {
120
+ const manifest = loadManifest(cwd);
121
+ const root = join(cwd, dirname(manifest.entry));
122
+ const found = walk(root).some((file) => readFileSync(file, 'utf8').includes(needle));
123
+ if (!found) {
124
+ throw new Error(`verify: "${needle}" is in no file under ${root}. That is what a build that did not rerun ` +
125
+ 'looks like — the artifact exists and is stale.');
126
+ }
127
+ console.log(`ok: "${needle}" is in the built bundle`);
128
+ }
129
+ /**
130
+ * The packaged application, run from source without producing an installer.
131
+ *
132
+ * **The whole point is that it is the same code path.** It stages exactly what a build stages and
133
+ * starts Electron on it, so the window, the flags, the `drift://` origin and the badge are the
134
+ * ones that will ship. A `run` that took a shortcut would be a rehearsal of something else.
135
+ */
136
+ async function run(cwd, options) {
137
+ const manifest = await doctor(cwd, options);
138
+ const stageDir = mkdtempSync(join(tmpdir(), 'drift-run-'));
139
+ /* A development run: developer tools open, warnings forwarded. `build` stages the other kind. */
140
+ await stageApp(manifest, cwd, stageDir, { development: true, ...options });
141
+ const electron = await import('electron');
142
+ const binary = electron.default;
143
+ console.log(`[drift-package] running from ${stageDir}`);
144
+ return new Promise((resolve) => {
145
+ const child = spawn(binary, [stageDir], { stdio: 'inherit', env: launchEnv() });
146
+ child.on('close', (code) => {
147
+ /* The staging directory holds a whole copy of the game's build — tens of megabytes for a
148
+ real one — and a `run` per iteration would leave one of each in the temp directory. */
149
+ rmSync(stageDir, { recursive: true, force: true });
150
+ resolve(code ?? 0);
151
+ });
152
+ });
153
+ }
154
+ /**
155
+ * The environment Electron is started in, with one variable removed.
156
+ *
157
+ * **`ELECTRON_RUN_AS_NODE` turns Electron into plain Node**, and any Electron-based terminal sets
158
+ * it for the processes it spawns — an editor's integrated terminal is the common case, and this
159
+ * workspace's own shell had it set. Inherited, it produces a failure that looks nothing like its
160
+ * cause: the binary starts, prints a Node banner and dies on `Cannot find module 'electron'`,
161
+ * because in that mode the built-in module genuinely is not there.
162
+ *
163
+ * Deleted rather than overridden with an empty string: Electron treats the variable as set
164
+ * whatever its value.
165
+ *
166
+ * **This is now the second place it is removed** and it stays: the top of this file clears it for
167
+ * the whole process, and this clears it for the child. The reason is written twice because it is
168
+ * the reason somebody will come here looking for, and a copy of `process.env` taken elsewhere
169
+ * would not inherit the first deletion.
170
+ */
171
+ function launchEnv() {
172
+ const env = { ...process.env };
173
+ delete env.ELECTRON_RUN_AS_NODE;
174
+ return env;
175
+ }
176
+ /**
177
+ * Whether this machine can build that target today.
178
+ *
179
+ * Desktop targets need the Electron runtime, which is present unless an install skipped its
180
+ * postinstall; Android needs a whole toolchain; iOS needs Xcode, which nothing here can check for
181
+ * beyond the platform it would have to be on.
182
+ */
183
+ async function toolchainReady(target) {
184
+ if (target === 'android')
185
+ return (await androidToolchain()) !== null;
186
+ if (target === 'ios')
187
+ return process.platform === 'darwin';
188
+ /* A Mac target built anywhere else is signed by rcodesign, and an unsigned .app will not
189
+ launch — so a machine without it is not ready for that target, however well it packs. */
190
+ if (needsRcodesign(target))
191
+ return (await findRcodesign()) !== null;
192
+ return true;
193
+ }
194
+ /**
195
+ * The two files `steamcmd` uploads a build with, written beside the artifacts they describe.
196
+ *
197
+ * A separate command rather than part of `build`, because uploading is a decision: the scripts
198
+ * name an app id that costs money to get wrong, and a build is often made several times before one
199
+ * of them is the one that ships.
200
+ */
201
+ async function writeSteamScripts(cwd, args, options) {
202
+ const manifest = loadManifest(cwd);
203
+ if (manifest.steam.appId === null) {
204
+ throw new Error('steam: `steam.appId` is null in drift.package.json. Set it to the app id Valve gave you; ' +
205
+ "a depot script with the wrong one uploads into somebody else's game.");
206
+ }
207
+ const target = (args.find((entry) => entry.startsWith('--target=')) ?? '--target=linux-x64').slice('--target='.length);
208
+ const depotArg = args.find((entry) => entry.startsWith('--depot='));
209
+ /* A first depot is conventionally the app id plus one, which is how Valve numbers them. */
210
+ const depotId = depotArg === undefined ? manifest.steam.appId + 1 : Number.parseInt(depotArg.slice(8), 10);
211
+ const branchArg = args.find((entry) => entry.startsWith('--branch='));
212
+ const outDir = outFor(cwd, target, options);
213
+ const content = target === 'linux-x64' ? 'linux-unpacked' : target === 'win-x64' ? 'win-unpacked' : 'mac';
214
+ const scripts = depotScripts({
215
+ appId: manifest.steam.appId,
216
+ depotId,
217
+ description: `${manifest.name} ${target}`,
218
+ contentRoot: content,
219
+ branch: branchArg === undefined ? null : branchArg.slice('--branch='.length),
220
+ });
221
+ await writeFile(join(outDir, scripts.appFileName), scripts.app, 'utf8');
222
+ await writeFile(join(outDir, scripts.depotFileName), scripts.depot, 'utf8');
223
+ console.log(`ok: ${join(outDir, scripts.appFileName)}`);
224
+ console.log(`ok: ${join(outDir, scripts.depotFileName)}`);
225
+ console.log(` upload with: steamcmd +login <user> +run_app_build "${join(outDir, scripts.appFileName)}" +quit`);
226
+ console.log(" nothing is set live: the build waits in Steam's admin for a person");
227
+ }
228
+ /** What this machine builds for itself, used when nothing has said otherwise. */
229
+ function hostTarget() {
230
+ if (process.platform === 'win32')
231
+ return 'win-x64';
232
+ if (process.platform === 'darwin')
233
+ return process.arch === 'x64' ? 'mac-x64' : 'mac-arm64';
234
+ return 'linux-x64';
235
+ }
236
+ function targetsFrom(manifest, args) {
237
+ const asked = args.find((entry) => entry.startsWith('--target='));
238
+ if (asked === undefined)
239
+ return manifest.targets;
240
+ const name = asked.slice('--target='.length);
241
+ const found = manifest.targets.find((target) => target === name);
242
+ if (found === undefined) {
243
+ throw new Error(`--target=${name} is not in this manifest's targets (${manifest.targets.join(', ')})`);
244
+ }
245
+ return [found];
246
+ }
247
+ const [command, ...rest] = process.argv.slice(2);
248
+ /**
249
+ * The project being packaged, which is the working directory unless `--project=` says otherwise.
250
+ *
251
+ * **Because the packager and the game are often not the same repository.** A game that keeps its
252
+ * `drift.package.json` and nothing else — no Electron, no electron-builder, no Android SDK in its
253
+ * own `node_modules` — is packaged by pointing this at it from wherever the tool lives, and that
254
+ * is the arrangement most consumers actually want: half a gigabyte of build tooling does not
255
+ * belong in a game's dependency tree.
256
+ */
257
+ const projectFlag = rest.find((entry) => entry.startsWith('--project='));
258
+ const cwd = projectFlag === undefined
259
+ ? process.cwd()
260
+ : resolve(process.cwd(), projectFlag.slice('--project='.length));
261
+ /**
262
+ * Where the packager's own `assets/`, `android/` and `ios/` are, if not beside its sources.
263
+ *
264
+ * **For a consumer that vendors engine source** rather than installing it: such a vendor copies
265
+ * `packages/<name>/src/**` to `<name>/**` and the flattening moves
266
+ * the three out of reach of anything this package can compute. Reported from outside, where the
267
+ * workaround was a layout exception in the vendor script for this one package. See `resources.ts`
268
+ * for why this is a flag rather than an environment variable.
269
+ */
270
+ const resourcesFlag = rest.find((entry) => entry.startsWith('--resources='));
271
+ /**
272
+ * Where builds write, if not `out/` inside the project.
273
+ *
274
+ * **For a project on a filesystem that misreports file identity**, where the first staging copy
275
+ * of a build refuses itself: `fs.cp` compares `dev` and `ino` up the destination's ancestors, and
276
+ * a shared or network folder can answer the same pair for a file and a directory. `outDir.ts`
277
+ * carries the measurement. Absolute here, or relative to wherever the command was run, which is
278
+ * not necessarily `--project=`.
279
+ */
280
+ const outFlag = rest.find((entry) => entry.startsWith('--out='));
281
+ const resources = {
282
+ ...(resourcesFlag === undefined
283
+ ? {}
284
+ : { resourceRoot: resourcesFlag.slice('--resources='.length) }),
285
+ ...(outFlag === undefined ? {} : { outRoot: outFlag.slice('--out='.length) }),
286
+ };
287
+ try {
288
+ if (command === 'doctor') {
289
+ await doctor(cwd, resources);
290
+ }
291
+ else if (command === 'verify') {
292
+ const arg = rest.find((entry) => entry.startsWith('--contains='));
293
+ if (arg === undefined)
294
+ throw new Error('verify needs --contains=<string>');
295
+ verify(cwd, arg.slice('--contains='.length));
296
+ }
297
+ else if (command === 'run') {
298
+ process.exitCode = await run(cwd, resources);
299
+ }
300
+ else if (command === 'bootstrap') {
301
+ /* Deliberately does not need a manifest: a machine is prepared before a project exists on it,
302
+ and `--target=` names what to prepare for. */
303
+ const asked = rest.find((entry) => entry.startsWith('--target='));
304
+ const targets = asked === undefined
305
+ ? (() => {
306
+ try {
307
+ return loadManifest(cwd).targets;
308
+ }
309
+ catch {
310
+ /* No manifest here: prepare for this desktop, which is the useful default on a
311
+ machine somebody is setting up. */
312
+ return [hostTarget()];
313
+ }
314
+ })()
315
+ : [asked.slice('--target='.length)];
316
+ await bootstrap(targets);
317
+ }
318
+ else if (command === 'init') {
319
+ /*
320
+ * Scaffolds a *project*, where `bootstrap` prepares a *machine*. Two commands because a
321
+ * machine is prepared once and a project once per project, and conflating them would make the
322
+ * one that writes files into your repository the one you run to install a JDK.
323
+ */
324
+ await init(cwd, loadManifest(cwd), rest.includes('--force'));
325
+ }
326
+ else if (command === 'steam') {
327
+ await writeSteamScripts(cwd, rest, resources);
328
+ }
329
+ else if (command === 'build') {
330
+ /* Checked before anything is downloaded or staged: a build that would be refused must be
331
+ refused first, not after twenty minutes of Electron. */
332
+ const manifest = await doctor(cwd, resources);
333
+ await build(manifest, cwd, targetsFrom(manifest, rest), resources);
334
+ }
335
+ else {
336
+ throw new Error('usage: drift-package <init|bootstrap|doctor|build|run|verify|steam> [--project=<dir>] ' +
337
+ `[--target=<target>] [--resources=<dir>] [--out=<dir>] [--force] — got "${String(command)}"`);
338
+ }
339
+ }
340
+ catch (cause) {
341
+ console.error(String(cause instanceof Error ? cause.message : cause));
342
+ process.exit(1);
343
+ }
@@ -0,0 +1,57 @@
1
+ import type { PackageManifest, Target } from './manifest.ts';
2
+ /**
3
+ * The Chromium switches a shipped build launches with.
4
+ *
5
+ * **This is most of why the packager exists.** Every switch below was paid for once already, in
6
+ * this repository or by a consumer, and a game that wraps Electron by hand rediscovers each of
7
+ * them — two of them only by measuring a frame nobody thought to measure.
8
+ *
9
+ * **What this costs**: the flags are ours rather than the platform's defaults, so a Chromium
10
+ * change can invalidate one and nothing will say so. **What would make it wrong**: Electron
11
+ * enabling Vulkan on Linux by default, at which point the first block below becomes a no-op worth
12
+ * deleting rather than a fix worth keeping.
13
+ *
14
+ * **Checked 2026-09-09 and still needed**, on Electron 43.4.1 / Chromium 150.0.7871.224 with an
15
+ * RX 9070 XT: launched with no switches at all, `navigator.gpu` is present and `requestAdapter`
16
+ * answers **null**. With the interop feature it hands back an `amd`/`rdna-4` adapter and a device.
17
+ * So the block below is doing the work rather than riding along, six Chromium majors after it was
18
+ * written.
19
+ *
20
+ * **And a switch is now only asked for on a runtime it has been seen to work on**, because the
21
+ * other half of "nothing will say so" is a name Chromium does not recognise: unknown features in
22
+ * `--enable-features` are dropped in silence, and the build that results is indistinguishable from
23
+ * a machine with no adapter to offer. See `WEBGPU_INTEROP_ELECTRON`.
24
+ */
25
+ export interface FlagPlan {
26
+ readonly switches: readonly (readonly [string, string])[];
27
+ /** Non-null means refuse to build, and this is what to print. */
28
+ readonly refusal: string | null;
29
+ /**
30
+ * What a caller must print, having decided nothing.
31
+ *
32
+ * **A switch that names a feature the runtime has never heard of is worse than no switch**, and
33
+ * that is what these exist for: Chromium ignores an unknown name in `--enable-features` without a
34
+ * word, so a build that cannot deliver WebGPU looks exactly like a machine whose driver had no
35
+ * adapter to offer. Where this plan declines to ask for something, it says so out loud.
36
+ */
37
+ readonly notes: readonly string[];
38
+ }
39
+ /**
40
+ * The Electron the Linux WebGPU switch below is written against, and measured on.
41
+ *
42
+ * **Not the version the feature was introduced in, which nobody here has established.** It is the
43
+ * version this repository has run the switch against and watched WebGPU come up: Electron 43.4.1,
44
+ * Chromium 150.0.7871.224, an RX 9070 XT, where `requestAdapter` answers null without the switch
45
+ * and hands back an `amd`/`rdna-4` adapter with it. Reported from outside against Electron 33.4.11,
46
+ * Chromium 130: the feature name is not in that binary at all, and the switch is inert there.
47
+ *
48
+ * So a runtime older than this is *unverified*, not known-broken — and the plan declines to ship an
49
+ * unverified switch rather than shipping one that may do nothing silently.
50
+ */
51
+ export declare const WEBGPU_INTEROP_ELECTRON = 43;
52
+ /**
53
+ * @param electronMajor The major version of the Electron these switches will be applied to, or
54
+ * `null` where a caller genuinely cannot know. Both callers in this package can: the build reads
55
+ * it out of the installed package, and the main process reads it out of `process.versions`.
56
+ */
57
+ export declare function planFlags(manifest: PackageManifest, target: Target, electronMajor?: number | null): FlagPlan;