@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.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +408 -0
- package/bin/drift-package.mjs +82 -0
- package/dist/android.d.ts +18 -0
- package/dist/android.js +207 -0
- package/dist/bootstrap.d.ts +17 -0
- package/dist/bootstrap.js +326 -0
- package/dist/build.d.ts +11 -0
- package/dist/build.js +248 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +343 -0
- package/dist/flags.d.ts +57 -0
- package/dist/flags.js +122 -0
- package/dist/identity.d.ts +51 -0
- package/dist/identity.js +75 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +24 -0
- package/dist/init.d.ts +24 -0
- package/dist/init.js +45 -0
- package/dist/ios.d.ts +27 -0
- package/dist/ios.js +152 -0
- package/dist/macCrossSign.d.ts +34 -0
- package/dist/macCrossSign.js +298 -0
- package/dist/main/consoleForward.d.ts +43 -0
- package/dist/main/consoleForward.js +63 -0
- package/dist/main/csp.d.ts +4 -0
- package/dist/main/csp.js +55 -0
- package/dist/main/entry.d.ts +1 -0
- package/dist/main/entry.js +56 -0
- package/dist/main/ipc.d.ts +28 -0
- package/dist/main/ipc.js +284 -0
- package/dist/main/links.d.ts +1 -0
- package/dist/main/links.js +29 -0
- package/dist/main/menu.d.ts +17 -0
- package/dist/main/menu.js +49 -0
- package/dist/main/serve.d.ts +21 -0
- package/dist/main/serve.js +69 -0
- package/dist/main/shortcuts.d.ts +28 -0
- package/dist/main/shortcuts.js +26 -0
- package/dist/main/splash.d.ts +14 -0
- package/dist/main/splash.js +41 -0
- package/dist/main/splashTiming.d.ts +36 -0
- package/dist/main/splashTiming.js +24 -0
- package/dist/main/window.d.ts +19 -0
- package/dist/main/window.js +297 -0
- package/dist/manifest.d.ts +61 -0
- package/dist/manifest.js +91 -0
- package/dist/mobile/assetCheck.d.ts +1 -0
- package/dist/mobile/assetCheck.js +28 -0
- package/dist/mobile/bridgeShim.d.ts +83 -0
- package/dist/mobile/bridgeShim.js +208 -0
- package/dist/mobile/iosShim.d.ts +42 -0
- package/dist/mobile/iosShim.js +104 -0
- package/dist/mobile/report.d.ts +27 -0
- package/dist/mobile/report.js +122 -0
- package/dist/mobile/versionCode.d.ts +12 -0
- package/dist/mobile/versionCode.js +17 -0
- package/dist/outDir.d.ts +30 -0
- package/dist/outDir.js +32 -0
- package/dist/preload/bridge.d.ts +77 -0
- package/dist/preload/bridge.js +2 -0
- package/dist/preload/entry.d.ts +1 -0
- package/dist/preload/entry.js +77 -0
- package/dist/renderer/fileStore.d.ts +23 -0
- package/dist/renderer/fileStore.js +33 -0
- package/dist/renderer/host.d.ts +45 -0
- package/dist/renderer/host.js +30 -0
- package/dist/renderer/nativeHost.d.ts +83 -0
- package/dist/renderer/nativeHost.js +125 -0
- package/dist/resources.d.ts +71 -0
- package/dist/resources.js +83 -0
- package/dist/runtime.d.ts +38 -0
- package/dist/runtime.js +80 -0
- package/dist/runtimeVersion.d.ts +10 -0
- package/dist/runtimeVersion.js +18 -0
- package/dist/signing.d.ts +49 -0
- package/dist/signing.js +135 -0
- package/dist/stage.d.ts +22 -0
- package/dist/stage.js +138 -0
- package/dist/steam/cloudStore.d.ts +30 -0
- package/dist/steam/cloudStore.js +54 -0
- package/dist/steam/depot.d.ts +29 -0
- package/dist/steam/depot.js +39 -0
- package/dist/steam/main.d.ts +22 -0
- package/dist/steam/main.js +41 -0
- package/dist/templates/buildWindows.d.ts +16 -0
- package/dist/templates/buildWindows.js +161 -0
- package/dist/templates/packageWrapper.d.ts +9 -0
- package/dist/templates/packageWrapper.js +80 -0
- package/dist/toolchain.d.ts +101 -0
- package/dist/toolchain.js +186 -0
- package/package.json +73 -0
- package/src/android.ts +256 -0
- package/src/bootstrap.ts +389 -0
- package/src/build.ts +285 -0
- package/src/cli.ts +367 -0
- package/src/flags.ts +171 -0
- package/src/identity.ts +87 -0
- package/src/index.ts +31 -0
- package/src/init.ts +61 -0
- package/src/ios.ts +185 -0
- package/src/macCrossSign.ts +338 -0
- package/src/main/consoleForward.ts +93 -0
- package/src/main/csp.ts +57 -0
- package/src/main/entry.ts +63 -0
- package/src/main/ipc.ts +311 -0
- package/src/main/links.ts +29 -0
- package/src/main/menu.ts +53 -0
- package/src/main/serve.ts +70 -0
- package/src/main/shortcuts.ts +55 -0
- package/src/main/splash.ts +42 -0
- package/src/main/splashTiming.ts +51 -0
- package/src/main/window.ts +316 -0
- package/src/manifest.ts +157 -0
- package/src/mobile/assetCheck.ts +25 -0
- package/src/mobile/bridgeShim.ts +292 -0
- package/src/mobile/iosShim.ts +148 -0
- package/src/mobile/report.ts +121 -0
- package/src/mobile/versionCode.ts +17 -0
- package/src/outDir.ts +37 -0
- package/src/preload/bridge.ts +73 -0
- package/src/preload/entry.ts +91 -0
- package/src/renderer/fileStore.ts +38 -0
- package/src/renderer/host.ts +96 -0
- package/src/renderer/nativeHost.ts +146 -0
- package/src/resources.ts +150 -0
- package/src/runtime.ts +87 -0
- package/src/runtimeVersion.ts +16 -0
- package/src/signing.ts +206 -0
- package/src/stage.ts +180 -0
- package/src/steam/cloudStore.ts +73 -0
- package/src/steam/depot.ts +69 -0
- package/src/steam/main.ts +69 -0
- package/src/templates/buildWindows.ts +163 -0
- package/src/templates/packageWrapper.ts +82 -0
- package/src/toolchain.ts +210 -0
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { chmod, readdir, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
|
|
8
|
+
import type { Target } from './manifest.ts';
|
|
9
|
+
import {
|
|
10
|
+
PINS,
|
|
11
|
+
ensureDir,
|
|
12
|
+
findAndroidSdk,
|
|
13
|
+
findGradle,
|
|
14
|
+
findJavaHome,
|
|
15
|
+
findRcodesign,
|
|
16
|
+
findXcodeGen,
|
|
17
|
+
needsRcodesign,
|
|
18
|
+
toolchainHome,
|
|
19
|
+
} from './toolchain.ts';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fetch everything a target needs to be built, so nobody has to install seven things by hand.
|
|
23
|
+
*
|
|
24
|
+
* **This is the command that makes the other ones work on a fresh machine.** A packaging toolchain
|
|
25
|
+
* is a JDK, an Android SDK, a Gradle distribution and an Electron runtime, in pinned versions;
|
|
26
|
+
* every one of those is a download and a licence prompt, and a README that lists them is a README
|
|
27
|
+
* somebody follows wrongly at two in the morning.
|
|
28
|
+
*
|
|
29
|
+
* **Nothing is installed system-wide and nothing needs a password.** Everything lands under a
|
|
30
|
+
* cache directory this owns, and a machine that already has a working toolchain — `JAVA_HOME`,
|
|
31
|
+
* `ANDROID_HOME` — is used as it is rather than duplicated.
|
|
32
|
+
*
|
|
33
|
+
* **Only what is actually downloadable is downloaded.** Xcode is not: it is a sign-in and an App
|
|
34
|
+
* Store install on a Mac, so the iOS branch prints what to get rather than pretending.
|
|
35
|
+
*/
|
|
36
|
+
export async function bootstrap(targets: readonly Target[]): Promise<void> {
|
|
37
|
+
const wantsAndroid = targets.includes('android');
|
|
38
|
+
const wantsIos = targets.includes('ios');
|
|
39
|
+
const wantsDesktop = targets.some((target) => target !== 'android' && target !== 'ios');
|
|
40
|
+
|
|
41
|
+
if (wantsDesktop) await bootstrapElectron();
|
|
42
|
+
if (targets.some((target) => needsRcodesign(target))) await bootstrapRcodesign();
|
|
43
|
+
if (wantsAndroid) await bootstrapAndroid();
|
|
44
|
+
if (wantsIos) await bootstrapIos();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `rcodesign`, which signs a Mac target on a machine that has no `codesign`.
|
|
49
|
+
*
|
|
50
|
+
* **Fetched only when a Mac target is asked for off a Mac**, because that is the only situation
|
|
51
|
+
* where the platform's own signer is missing. On a Mac this is never downloaded: `codesign` ships
|
|
52
|
+
* with the command line tools and electron-builder drives it directly.
|
|
53
|
+
*
|
|
54
|
+
* A single static musl binary, so unlike the JDK there is nothing to discover in the archive and
|
|
55
|
+
* nothing that cares which distribution it lands on.
|
|
56
|
+
*/
|
|
57
|
+
async function bootstrapRcodesign(): Promise<void> {
|
|
58
|
+
const existing = await findRcodesign();
|
|
59
|
+
if (existing !== null) {
|
|
60
|
+
console.log(`ok: rcodesign is present at ${existing}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const asset = rcodesignAsset();
|
|
65
|
+
if (asset === null) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`there is no published rcodesign for ${process.platform}/${process.arch}. ` +
|
|
68
|
+
'`cargo install apple-codesign` builds one, and the build will find it in ~/.cargo/bin',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const home = toolchainHome();
|
|
73
|
+
await ensureDir(home);
|
|
74
|
+
console.log(`fetching ${PINS.rcodesign.label}…`);
|
|
75
|
+
const archive = join(home, asset.archive);
|
|
76
|
+
await download(
|
|
77
|
+
'https://github.com/indygreg/apple-platform-rs/releases/download/' +
|
|
78
|
+
`apple-codesign%2F${PINS.rcodesign.version}/${asset.archive}`,
|
|
79
|
+
archive,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const staging = join(home, 'rcodesign-unpack');
|
|
83
|
+
await rm(staging, { recursive: true, force: true });
|
|
84
|
+
await ensureDir(staging);
|
|
85
|
+
await extract(archive, staging);
|
|
86
|
+
|
|
87
|
+
/* The archive unpacks one directory named for the triple, holding the binary and its licences.
|
|
88
|
+
The binary is searched for rather than assumed at that path: the name of the wrapper
|
|
89
|
+
directory is the one part of a release asset that has changed before. */
|
|
90
|
+
const found = await findFile(
|
|
91
|
+
staging,
|
|
92
|
+
process.platform === 'win32' ? 'rcodesign.exe' : 'rcodesign',
|
|
93
|
+
);
|
|
94
|
+
if (found === null) throw new Error('the rcodesign archive did not contain a binary');
|
|
95
|
+
|
|
96
|
+
const target = join(home, `rcodesign-${PINS.rcodesign.version}`);
|
|
97
|
+
await rm(target, { recursive: true, force: true });
|
|
98
|
+
await ensureDir(target);
|
|
99
|
+
const binary = join(target, 'rcodesign');
|
|
100
|
+
await rename(found, binary);
|
|
101
|
+
await chmod(binary, 0o755).catch(() => undefined);
|
|
102
|
+
await rm(staging, { recursive: true, force: true });
|
|
103
|
+
await rm(archive, { force: true });
|
|
104
|
+
console.log(`ok: rcodesign is at ${binary}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Which published rcodesign asset this machine can run, or null if the release page has none. */
|
|
108
|
+
function rcodesignAsset(): { readonly archive: string } | null {
|
|
109
|
+
const version = PINS.rcodesign.version;
|
|
110
|
+
if (process.platform === 'linux') {
|
|
111
|
+
/* musl rather than gnu: the release publishes only the static build, which is the one that
|
|
112
|
+
does not care about the host's glibc. */
|
|
113
|
+
const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64';
|
|
114
|
+
return { archive: `apple-codesign-${version}-${arch}-unknown-linux-musl.tar.gz` };
|
|
115
|
+
}
|
|
116
|
+
if (process.platform === 'win32') {
|
|
117
|
+
return { archive: `apple-codesign-${version}-x86_64-pc-windows-msvc.zip` };
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The first file with this name anywhere under a directory, or null. */
|
|
123
|
+
async function findFile(root: string, name: string): Promise<string | null> {
|
|
124
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
125
|
+
const full = join(root, entry.name);
|
|
126
|
+
if (entry.isDirectory()) {
|
|
127
|
+
const nested = await findFile(full, name);
|
|
128
|
+
if (nested !== null) return nested;
|
|
129
|
+
} else if (entry.name === name) {
|
|
130
|
+
return full;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The Electron runtime, which npm sometimes does not fetch.
|
|
138
|
+
*
|
|
139
|
+
* Its binary arrives through a postinstall script, and an install run with scripts disabled —
|
|
140
|
+
* which several sandboxes, CI images and corporate setups do by default — leaves the package
|
|
141
|
+
* present and the runtime missing. The failure that produces is `Electron failed to install
|
|
142
|
+
* correctly` at the moment somebody first tries to run a game.
|
|
143
|
+
*/
|
|
144
|
+
async function bootstrapElectron(): Promise<void> {
|
|
145
|
+
const require = (await import('node:module')).createRequire(import.meta.url);
|
|
146
|
+
let installer: string;
|
|
147
|
+
try {
|
|
148
|
+
installer = require.resolve('electron/install.js');
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error('electron is not installed; run npm install first');
|
|
151
|
+
}
|
|
152
|
+
const dist = join(installer, '..', 'dist');
|
|
153
|
+
if (await pathExists(dist)) {
|
|
154
|
+
console.log('ok: the Electron runtime is present');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
console.log('fetching the Electron runtime…');
|
|
158
|
+
await run(process.execPath, [installer], {});
|
|
159
|
+
console.log('ok: the Electron runtime is present');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function bootstrapAndroid(): Promise<void> {
|
|
163
|
+
const home = toolchainHome();
|
|
164
|
+
await ensureDir(home);
|
|
165
|
+
|
|
166
|
+
const javaHome = (await findJavaHome()) ?? (await installJdk(home));
|
|
167
|
+
const sdkRoot = (await findAndroidSdk()) ?? (await installAndroidSdk(home, javaHome));
|
|
168
|
+
await installAndroidPackages(sdkRoot, javaHome);
|
|
169
|
+
const gradle = (await findGradle()) ?? (await installGradle(home));
|
|
170
|
+
|
|
171
|
+
console.log(`ok: android toolchain ready`);
|
|
172
|
+
console.log(` JDK: ${javaHome}`);
|
|
173
|
+
console.log(` SDK: ${sdkRoot}`);
|
|
174
|
+
console.log(` Gradle: ${gradle}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function bootstrapIos(): Promise<void> {
|
|
178
|
+
if (process.platform !== 'darwin') {
|
|
179
|
+
console.log('ios: this is not a Mac, and an iOS build needs one. Nothing to fetch here.');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const home = toolchainHome();
|
|
184
|
+
await ensureDir(home);
|
|
185
|
+
if ((await findXcodeGen()) === null) {
|
|
186
|
+
console.log(`fetching ${PINS.xcodegen.label}…`);
|
|
187
|
+
const archive = join(home, 'xcodegen.zip');
|
|
188
|
+
await download(
|
|
189
|
+
`https://github.com/yonaskolb/XcodeGen/releases/download/${PINS.xcodegen.version}/xcodegen.zip`,
|
|
190
|
+
archive,
|
|
191
|
+
);
|
|
192
|
+
const target = join(home, `xcodegen-${PINS.xcodegen.version}`);
|
|
193
|
+
await rm(target, { recursive: true, force: true });
|
|
194
|
+
await ensureDir(target);
|
|
195
|
+
await extract(archive, target);
|
|
196
|
+
/* The archive unpacks a `xcodegen/` directory containing bin and share; flattened so the
|
|
197
|
+
path this file resolves is the path `findXcodeGen` looks for. */
|
|
198
|
+
const [inner] = await readdir(target);
|
|
199
|
+
if (inner !== undefined && inner === 'xcodegen') {
|
|
200
|
+
await rename(join(target, 'xcodegen', 'bin'), join(target, 'bin'));
|
|
201
|
+
await rename(join(target, 'xcodegen', 'share'), join(target, 'share')).catch(() => undefined);
|
|
202
|
+
}
|
|
203
|
+
await chmod(join(target, 'bin', 'xcodegen'), 0o755).catch(() => undefined);
|
|
204
|
+
await rm(archive, { force: true });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Xcode itself cannot be fetched, so it is checked for and named rather than installed. */
|
|
208
|
+
try {
|
|
209
|
+
await run('xcodebuild', ['-version'], { quiet: true });
|
|
210
|
+
console.log('ok: Xcode is here, and so is XcodeGen');
|
|
211
|
+
} catch {
|
|
212
|
+
console.log('ios: XcodeGen is ready; Xcode is not.');
|
|
213
|
+
console.log(' - install Xcode from the App Store, then `xcode-select --install`');
|
|
214
|
+
console.log(' - a free Apple ID installs to your own device for seven days at a time;');
|
|
215
|
+
console.log(' the paid Developer Program is what makes a build last and reach TestFlight');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Temurin, from the Adoptium API, which redirects to the current build of the pinned feature. */
|
|
220
|
+
async function installJdk(home: string): Promise<string> {
|
|
221
|
+
const os =
|
|
222
|
+
process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'mac' : 'linux';
|
|
223
|
+
const arch = process.arch === 'arm64' ? 'aarch64' : 'x64';
|
|
224
|
+
const url =
|
|
225
|
+
`https://api.adoptium.net/v3/binary/latest/${PINS.jdk.feature}/ga/${os}/${arch}` +
|
|
226
|
+
'/jdk/hotspot/normal/eclipse';
|
|
227
|
+
|
|
228
|
+
console.log(`fetching ${PINS.jdk.label}…`);
|
|
229
|
+
const archive = join(home, os === 'windows' ? 'jdk.zip' : 'jdk.tar.gz');
|
|
230
|
+
await download(url, archive);
|
|
231
|
+
|
|
232
|
+
const staging = join(home, 'jdk-unpack');
|
|
233
|
+
await rm(staging, { recursive: true, force: true });
|
|
234
|
+
await ensureDir(staging);
|
|
235
|
+
await extract(archive, staging);
|
|
236
|
+
/* Every JDK archive unpacks into one versioned directory; the pin is on the feature release, so
|
|
237
|
+
the exact name changes with each patch and is discovered rather than assumed. */
|
|
238
|
+
const [inner] = await readdir(staging);
|
|
239
|
+
if (inner === undefined) throw new Error('the JDK archive unpacked into nothing');
|
|
240
|
+
const target = join(home, 'jdk');
|
|
241
|
+
await rm(target, { recursive: true, force: true });
|
|
242
|
+
await rename(join(staging, inner), target);
|
|
243
|
+
await rm(staging, { recursive: true, force: true });
|
|
244
|
+
await rm(archive, { force: true });
|
|
245
|
+
return target;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function installAndroidSdk(home: string, javaHome: string): Promise<string> {
|
|
249
|
+
const os = process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux';
|
|
250
|
+
const url = `https://dl.google.com/android/repository/commandlinetools-${os}-${PINS.androidTools.build}_latest.zip`;
|
|
251
|
+
|
|
252
|
+
console.log(`fetching ${PINS.androidTools.label}…`);
|
|
253
|
+
const archive = join(home, 'cmdline-tools.zip');
|
|
254
|
+
await download(url, archive);
|
|
255
|
+
|
|
256
|
+
const sdkRoot = join(home, 'android-sdk');
|
|
257
|
+
const staging = join(home, 'cmdline-unpack');
|
|
258
|
+
await rm(staging, { recursive: true, force: true });
|
|
259
|
+
await ensureDir(staging);
|
|
260
|
+
await extract(archive, staging);
|
|
261
|
+
/* sdkmanager insists on living at `cmdline-tools/latest/`, and the archive unpacks a directory
|
|
262
|
+
called `cmdline-tools`. Getting this wrong produces a tool that refuses to run and blames the
|
|
263
|
+
SDK root rather than its own location. */
|
|
264
|
+
await ensureDir(join(sdkRoot, 'cmdline-tools'));
|
|
265
|
+
await rm(join(sdkRoot, 'cmdline-tools', 'latest'), { recursive: true, force: true });
|
|
266
|
+
await rename(join(staging, 'cmdline-tools'), join(sdkRoot, 'cmdline-tools', 'latest'));
|
|
267
|
+
await rm(staging, { recursive: true, force: true });
|
|
268
|
+
await rm(archive, { force: true });
|
|
269
|
+
|
|
270
|
+
await acceptLicences(sdkRoot, javaHome);
|
|
271
|
+
return sdkRoot;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Accept the SDK licences, which is a prompt that cannot be skipped and can be answered.
|
|
276
|
+
*
|
|
277
|
+
* Google requires an explicit acceptance per licence before any package installs. Answering it by
|
|
278
|
+
* piping `y` is what every CI image does; it is recorded here rather than hidden because accepting
|
|
279
|
+
* a licence on somebody's behalf is a thing they should be able to see in a diff.
|
|
280
|
+
*/
|
|
281
|
+
async function acceptLicences(sdkRoot: string, javaHome: string): Promise<void> {
|
|
282
|
+
console.log('accepting the Android SDK licences…');
|
|
283
|
+
await run(sdkManager(sdkRoot), [`--sdk_root=${sdkRoot}`, '--licenses'], {
|
|
284
|
+
env: { JAVA_HOME: javaHome },
|
|
285
|
+
stdin: `${'y\n'.repeat(64)}`,
|
|
286
|
+
quiet: true,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function installAndroidPackages(sdkRoot: string, javaHome: string): Promise<void> {
|
|
291
|
+
console.log(`installing ${PINS.androidPackages.join(', ')}…`);
|
|
292
|
+
await run(sdkManager(sdkRoot), [`--sdk_root=${sdkRoot}`, ...PINS.androidPackages], {
|
|
293
|
+
env: { JAVA_HOME: javaHome },
|
|
294
|
+
quiet: true,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function installGradle(home: string): Promise<string> {
|
|
299
|
+
console.log(`fetching ${PINS.gradle.label}…`);
|
|
300
|
+
const archive = join(home, 'gradle.zip');
|
|
301
|
+
await download(
|
|
302
|
+
`https://services.gradle.org/distributions/gradle-${PINS.gradle.version}-bin.zip`,
|
|
303
|
+
archive,
|
|
304
|
+
);
|
|
305
|
+
await extract(archive, home);
|
|
306
|
+
await rm(archive, { force: true });
|
|
307
|
+
const binary = join(
|
|
308
|
+
home,
|
|
309
|
+
`gradle-${PINS.gradle.version}`,
|
|
310
|
+
'bin',
|
|
311
|
+
process.platform === 'win32' ? 'gradle.bat' : 'gradle',
|
|
312
|
+
);
|
|
313
|
+
if (!(await pathExists(binary))) throw new Error('the Gradle archive did not contain a launcher');
|
|
314
|
+
return binary;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function sdkManager(sdkRoot: string): string {
|
|
318
|
+
const name = process.platform === 'win32' ? 'sdkmanager.bat' : 'sdkmanager';
|
|
319
|
+
return join(sdkRoot, 'cmdline-tools', 'latest', 'bin', name);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function download(url: string, to: string): Promise<void> {
|
|
323
|
+
const response = await fetch(url, { redirect: 'follow' });
|
|
324
|
+
if (!response.ok || response.body === null) {
|
|
325
|
+
throw new Error(`could not download ${url}: ${response.status} ${response.statusText}`);
|
|
326
|
+
}
|
|
327
|
+
/* The DOM's `ReadableStream` and Node's are structurally the same and typed apart; the cast is
|
|
328
|
+
at the one line where the two type worlds meet rather than spread through the signature. */
|
|
329
|
+
await pipeline(
|
|
330
|
+
Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]),
|
|
331
|
+
createWriteStream(to),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Unpack an archive with whatever this machine has.
|
|
337
|
+
*
|
|
338
|
+
* `tar` handles both formats on macOS and on Windows, where it is bsdtar; GNU tar on Linux cannot
|
|
339
|
+
* read a zip, so `unzip` is tried first for those. Two attempts rather than a dependency: an
|
|
340
|
+
* archive extractor is a large amount of code to carry for a step that runs once per machine.
|
|
341
|
+
*/
|
|
342
|
+
async function extract(archive: string, into: string): Promise<void> {
|
|
343
|
+
if (archive.endsWith('.zip')) {
|
|
344
|
+
try {
|
|
345
|
+
await run('unzip', ['-q', '-o', archive, '-d', into], { quiet: true });
|
|
346
|
+
return;
|
|
347
|
+
} catch {
|
|
348
|
+
await run('tar', ['-xf', archive, '-C', into], { quiet: true });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
await run('tar', ['-xzf', archive, '-C', into], { quiet: true });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
356
|
+
try {
|
|
357
|
+
await (await import('node:fs/promises')).access(path);
|
|
358
|
+
return true;
|
|
359
|
+
} catch {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
interface RunOptions {
|
|
365
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
366
|
+
readonly stdin?: string;
|
|
367
|
+
readonly quiet?: boolean;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function run(command: string, args: readonly string[], options: RunOptions): Promise<void> {
|
|
371
|
+
await new Promise<void>((resolve, reject) => {
|
|
372
|
+
const child = spawn(command, [...args], {
|
|
373
|
+
env: { ...process.env, ...options.env },
|
|
374
|
+
stdio: [
|
|
375
|
+
options.stdin === undefined ? 'ignore' : 'pipe',
|
|
376
|
+
options.quiet === true ? 'ignore' : 'inherit',
|
|
377
|
+
'inherit',
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
if (options.stdin !== undefined && child.stdin !== null) {
|
|
381
|
+
child.stdin.write(options.stdin);
|
|
382
|
+
child.stdin.end();
|
|
383
|
+
}
|
|
384
|
+
child.on('error', reject);
|
|
385
|
+
child.on('close', (code) =>
|
|
386
|
+
code === 0 ? resolve() : reject(new Error(`${command} exited with ${String(code)}`)),
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
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
|
+
|
|
6
|
+
import { buildAndroid } from './android.ts';
|
|
7
|
+
import { buildIos } from './ios.ts';
|
|
8
|
+
import { crossSignMacApp, zipMacApp } from './macCrossSign.ts';
|
|
9
|
+
import type { PackageManifest, Target } from './manifest.ts';
|
|
10
|
+
import { outFor } from './outDir.ts';
|
|
11
|
+
import { resourceFile } from './resources.ts';
|
|
12
|
+
import { ensureElectronRuntime } from './runtime.ts';
|
|
13
|
+
import type { ResourceOptions } from './resources.ts';
|
|
14
|
+
import { planSigning } from './signing.ts';
|
|
15
|
+
import { consumerVersion, stageApp } from './stage.ts';
|
|
16
|
+
import { findRcodesign } from './toolchain.ts';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* An installable artifact per target.
|
|
20
|
+
*
|
|
21
|
+
* **The builder configuration is derived, not authored.** Every value it needs is already in
|
|
22
|
+
* `drift.package.json`, and a second configuration file would be a second place for the
|
|
23
|
+
* application id to be wrong — which is the failure that is silent until saves land in a directory
|
|
24
|
+
* nobody looks in.
|
|
25
|
+
*
|
|
26
|
+
* **Linux gets a native artifact and that is a Steam Deck decision**, recorded here because it
|
|
27
|
+
* looks like an ordinary target otherwise: a Windows build under Proton is the default assumption
|
|
28
|
+
* and it carries a known regression — gamepads undetected in Electron under Proton after 26.6.10,
|
|
29
|
+
* where the same build works natively. Valve's own guidance points the same way.
|
|
30
|
+
*
|
|
31
|
+
* **A target is refused where this machine cannot honestly produce it**, and the interesting word
|
|
32
|
+
* is *honestly*: the danger was never a build error, it is an artifact that looks finished and
|
|
33
|
+
* will not launch, or one that cannot be signed. `scripts/` carries the Windows and macOS hosts.
|
|
34
|
+
*
|
|
35
|
+
* **macOS is the one target that moved off that rule, and only from Linux.** Nothing in a Mac
|
|
36
|
+
* build is compiled — Electron ships a prebuilt darwin runtime and packing it is file copying and
|
|
37
|
+
* a plist — so the artifact itself was never the problem. The signature was: electron-builder
|
|
38
|
+
* skips signing off darwin, and an unsigned binary is refused by Apple Silicon's loader. With
|
|
39
|
+
* `rcodesign` putting an ad-hoc signature on afterwards, the two things a Linux host still cannot
|
|
40
|
+
* do are named rather than worked around — there is no `.dmg`, because that is `hdiutil`, and
|
|
41
|
+
* there is no proof it launches, because that needs the machine it runs on. See `macCrossSign.ts`.
|
|
42
|
+
*/
|
|
43
|
+
const HOST_FOR: Readonly<Record<Target, NodeJS.Platform | null>> = {
|
|
44
|
+
'win-x64': 'win32',
|
|
45
|
+
'mac-arm64': 'darwin',
|
|
46
|
+
'mac-x64': 'darwin',
|
|
47
|
+
'linux-x64': 'linux',
|
|
48
|
+
/* Android's toolchain is Java and it runs anywhere; iOS needs Xcode, which is a Mac. */
|
|
49
|
+
android: null,
|
|
50
|
+
ios: 'darwin',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Why this machine cannot build that target, or null.
|
|
55
|
+
*
|
|
56
|
+
* A Mac target is allowed from Linux and still refused from Windows — that second refusal is
|
|
57
|
+
* electron-builder's own and would fire a few seconds later anyway, so it is stated here where
|
|
58
|
+
* the reason can be given.
|
|
59
|
+
*/
|
|
60
|
+
function refuseHost(target: Target, platform: NodeJS.Platform): string | null {
|
|
61
|
+
const host = HOST_FOR[target];
|
|
62
|
+
if (host === null || platform === host) return null;
|
|
63
|
+
|
|
64
|
+
if ((target === 'mac-arm64' || target === 'mac-x64') && platform === 'linux') return null;
|
|
65
|
+
|
|
66
|
+
if (target === 'mac-arm64' || target === 'mac-x64') {
|
|
67
|
+
return (
|
|
68
|
+
`${target} is cross-built from Linux and built natively on a Mac, and this is ${platform}. ` +
|
|
69
|
+
'electron-builder refuses a Mac target on Windows outright, so there is nothing to work ' +
|
|
70
|
+
'around here.'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
`${target} is built on ${String(host)}, and this is ${platform}. ` +
|
|
76
|
+
'See packages/package/scripts for the Windows and macOS hosts; cross-building under ' +
|
|
77
|
+
'Wine cannot reach a certificate store, which is the one platform where that matters.'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function build(
|
|
82
|
+
manifest: PackageManifest,
|
|
83
|
+
cwd: string,
|
|
84
|
+
targets: readonly Target[],
|
|
85
|
+
options: ResourceOptions = {},
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const { build: run, Arch, Platform } = await import('electron-builder');
|
|
88
|
+
|
|
89
|
+
/* One artifact per target, and the architecture is named rather than inherited: a build on an
|
|
90
|
+
Apple Silicon machine defaults to arm64, so `mac-x64` asked for on one would quietly produce
|
|
91
|
+
the wrong slice. */
|
|
92
|
+
const targetFor = (name: Target): ReturnType<typeof Platform.LINUX.createTarget> =>
|
|
93
|
+
name === 'win-x64'
|
|
94
|
+
? Platform.WINDOWS.createTarget(undefined, Arch.x64)
|
|
95
|
+
: name === 'linux-x64'
|
|
96
|
+
? Platform.LINUX.createTarget(undefined, Arch.x64)
|
|
97
|
+
: Platform.MAC.createTarget(undefined, name === 'mac-x64' ? Arch.x64 : Arch.arm64);
|
|
98
|
+
const electronVersion = await installedElectronVersion();
|
|
99
|
+
|
|
100
|
+
for (const target of targets) {
|
|
101
|
+
if (target === 'android') {
|
|
102
|
+
await buildAndroid(manifest, cwd, outFor(cwd, target, options), options);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (target === 'ios') {
|
|
106
|
+
await buildIos(manifest, cwd, outFor(cwd, target, options), options);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const refusal = refuseHost(target, process.platform);
|
|
110
|
+
if (refusal !== null) throw new Error(refusal);
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* **The runtime, before anything is staged.** `installedElectronVersion` below reads a version
|
|
114
|
+
* out of `electron/package.json`, which is in the npm tarball — so it answers even when the
|
|
115
|
+
* postinstall that downloads the actual binary never finished, and electron-builder then finds
|
|
116
|
+
* nothing to package several steps and several minutes later. Measured in this repository on
|
|
117
|
+
* 2026-08-29: a version of 43.4.1 against no `dist`. See `runtime.ts`.
|
|
118
|
+
*/
|
|
119
|
+
await ensureElectronRuntime();
|
|
120
|
+
|
|
121
|
+
const signing = planSigning(target, process.env, process.platform);
|
|
122
|
+
if (signing.refusal !== null) throw new Error(`${target}: ${signing.refusal}`);
|
|
123
|
+
console.log(`[drift-package] ${target}: signing ${signing.mode} — ${signing.reason}`);
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* **Looked up before anything is staged.** Signing is the last step of a build that takes
|
|
127
|
+
* minutes, and a missing signer discovered there costs the whole thing — so the tool is
|
|
128
|
+
* resolved while the failure is still cheap.
|
|
129
|
+
*/
|
|
130
|
+
const rcodesign = signing.crossSign ? await findRcodesign() : null;
|
|
131
|
+
if (signing.crossSign && rcodesign === null) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`${target} is signed here by rcodesign, and there is none on this machine. ` +
|
|
134
|
+
`Run \`drift-package bootstrap --target=${target}\`, which fetches it into a cache.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const outDir = outFor(cwd, target, options);
|
|
139
|
+
const stageDir = join(outDir, 'stage');
|
|
140
|
+
await stageApp(manifest, cwd, stageDir, { development: false, ...options });
|
|
141
|
+
|
|
142
|
+
/* Where the signing step found the `.app`, so the archive below does not have to guess at
|
|
143
|
+
electron-builder's own naming for the packed directory. */
|
|
144
|
+
const signed: { app: string | null } = { app: null };
|
|
145
|
+
|
|
146
|
+
await run({
|
|
147
|
+
targets: targetFor(target),
|
|
148
|
+
/*
|
|
149
|
+
* **The staged directory is the project, not just the application.** Pointed at the
|
|
150
|
+
* consumer's own directory instead, electron-builder looks for *their* `package.json`,
|
|
151
|
+
* tries to install *their* dependencies into the artifact, and fails on a project that
|
|
152
|
+
* has neither — which a game built by any other toolchain may well not.
|
|
153
|
+
*/
|
|
154
|
+
projectDir: stageDir,
|
|
155
|
+
config: {
|
|
156
|
+
appId: manifest.id,
|
|
157
|
+
productName: manifest.name,
|
|
158
|
+
electronVersion,
|
|
159
|
+
/* One 1024px PNG for all three platforms: electron-builder derives an `.ico` and an
|
|
160
|
+
`.icns` from it, and a game that supplies its own replaces exactly this line. */
|
|
161
|
+
icon:
|
|
162
|
+
manifest.icon === null
|
|
163
|
+
? await resourceFile('assets', 'icon.png', options.resourceRoot)
|
|
164
|
+
: resolve(cwd, manifest.icon),
|
|
165
|
+
directories: { output: outDir },
|
|
166
|
+
/* The staged directory is the application, whole. Nothing is filtered here because
|
|
167
|
+
nothing that should not ship was put there: see `stage.ts` for what it contains. */
|
|
168
|
+
files: ['**/*'],
|
|
169
|
+
/*
|
|
170
|
+
* **A native module cannot be loaded from inside an asar archive.** `steamworks.js` is a
|
|
171
|
+
* `.node` binary, and one packed into the archive fails at `require` with a message about
|
|
172
|
+
* a path that does not exist — which reads as a missing dependency rather than as a
|
|
173
|
+
* packing decision. Unpacked, and only it.
|
|
174
|
+
*/
|
|
175
|
+
asarUnpack: ['**/node_modules/steamworks.js/**'],
|
|
176
|
+
/*
|
|
177
|
+
* **The signature goes on between packing and archiving, and that ordering is the whole
|
|
178
|
+
* point.** `afterPack` runs once the `.app` is complete and before any target wraps it, so
|
|
179
|
+
* the signed bundle is the one that ends up inside the `.zip`. Signing afterwards would
|
|
180
|
+
* leave the artifact people download carrying the unsigned copy.
|
|
181
|
+
*/
|
|
182
|
+
afterPack: rcodesign === null ? undefined : macSignStep(rcodesign, target, signed),
|
|
183
|
+
mac: {
|
|
184
|
+
hardenedRuntime: signing.mode === 'real',
|
|
185
|
+
category: 'public.app-category.games',
|
|
186
|
+
/*
|
|
187
|
+
* `'-'` is `codesign`'s own spelling of an ad-hoc identity. `null` means *do not sign*,
|
|
188
|
+
* which on Apple Silicon means *does not run* — and is exactly right when this build
|
|
189
|
+
* signs the app itself a few lines below, because electron-builder would otherwise log
|
|
190
|
+
* a warning about skipping a step that is not being skipped.
|
|
191
|
+
*/
|
|
192
|
+
identity: signing.crossSign ? null : signing.mode === 'adhoc' ? '-' : undefined,
|
|
193
|
+
notarize: signing.notarise,
|
|
194
|
+
/*
|
|
195
|
+
* **Both of electron-builder's Mac archives are macOS-only, for different reasons.** A
|
|
196
|
+
* `.dmg` is built by `hdiutil`, which exists nowhere else. Its `.zip` is worse than
|
|
197
|
+
* unavailable: it is produced by 7-Zip off darwin, which dereferences the symlinks every
|
|
198
|
+
* `.framework` is made of, and the result is a corrupt bundle rather than an error.
|
|
199
|
+
*
|
|
200
|
+
* So a cross-build asks for `dir` — pack the `.app` and stop — and the archive is made
|
|
201
|
+
* afterwards by `zipMacApp`, which keeps the links.
|
|
202
|
+
*/
|
|
203
|
+
target: signing.crossSign ? ['dir'] : ['dmg', 'zip'],
|
|
204
|
+
},
|
|
205
|
+
win: {
|
|
206
|
+
target: ['nsis', 'zip'],
|
|
207
|
+
/* Nothing is named here: electron-builder reads `WIN_CSC_LINK`/`CSC_LINK` from the
|
|
208
|
+
environment itself, and repeating them is a second place for them to be wrong. */
|
|
209
|
+
},
|
|
210
|
+
linux: { target: ['AppImage', 'tar.gz'], category: 'Game' },
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* **The archive, for a cross-build only.** electron-builder was asked for `dir` above because
|
|
216
|
+
* neither of its Mac archives can be produced correctly off a Mac, so the artifact somebody
|
|
217
|
+
* downloads is made here — see `zipMacApp` for what 7-Zip does to a framework.
|
|
218
|
+
*
|
|
219
|
+
* The name matches what electron-builder would have written, so a Mac-built release and a
|
|
220
|
+
* Linux-built one are the same file under the same name.
|
|
221
|
+
*/
|
|
222
|
+
if (signed.app !== null) {
|
|
223
|
+
const version = await consumerVersion(cwd);
|
|
224
|
+
const arch = target === 'mac-x64' ? 'x64' : 'arm64';
|
|
225
|
+
await zipMacApp(signed.app, join(outDir, `${manifest.name}-${version}-${arch}-mac.zip`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The `afterPack` hook that ad-hoc signs a cross-built `.app`, or nothing on a Mac.
|
|
232
|
+
*
|
|
233
|
+
* A factory rather than an inline closure so the resolved signer is a `string` inside it: the
|
|
234
|
+
* lookup and the refusal happen once, before a build starts, and this cannot be reached without
|
|
235
|
+
* one. `electronPlatformName` is checked because the hook is called for whatever is being packed.
|
|
236
|
+
*/
|
|
237
|
+
function macSignStep(
|
|
238
|
+
rcodesign: string,
|
|
239
|
+
target: Target,
|
|
240
|
+
signed: { app: string | null },
|
|
241
|
+
): (context: {
|
|
242
|
+
readonly appOutDir: string;
|
|
243
|
+
readonly electronPlatformName: string;
|
|
244
|
+
}) => Promise<void> {
|
|
245
|
+
return async (context) => {
|
|
246
|
+
if (context.electronPlatformName !== 'darwin') return;
|
|
247
|
+
const app = await packedApp(context.appOutDir);
|
|
248
|
+
console.log(`[drift-package] ad-hoc signing ${app} with rcodesign`);
|
|
249
|
+
await crossSignMacApp({
|
|
250
|
+
app,
|
|
251
|
+
rcodesign,
|
|
252
|
+
arch: target === 'mac-x64' ? 'x64' : 'arm64',
|
|
253
|
+
});
|
|
254
|
+
console.log('[drift-package] signed, and every nested bundle checked');
|
|
255
|
+
signed.app = app;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The `.app` electron-builder just wrote, found rather than named after the product. */
|
|
260
|
+
async function packedApp(appOutDir: string): Promise<string> {
|
|
261
|
+
const { readdir } = await import('node:fs/promises');
|
|
262
|
+
const found = (await readdir(appOutDir)).filter((entry) => entry.endsWith('.app'));
|
|
263
|
+
if (found.length !== 1) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`expected one .app in ${appOutDir} and found ${found.length ? found.join(', ') : 'none'}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return join(appOutDir, found[0] as string);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The Electron the artifact will carry, read from what is installed rather than declared.
|
|
273
|
+
*
|
|
274
|
+
* The staged application has no dependencies of its own — both entry points are bundled — so
|
|
275
|
+
* electron-builder has nothing to infer a version from and would refuse. Reading the installed
|
|
276
|
+
* one keeps the artifact and this repository on the same runtime by construction.
|
|
277
|
+
*/
|
|
278
|
+
export async function installedElectronVersion(): Promise<string> {
|
|
279
|
+
const require = createRequire(import.meta.url);
|
|
280
|
+
const manifestPath = require.resolve('electron/package.json');
|
|
281
|
+
const raw: unknown = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
282
|
+
const version = (raw as { version?: unknown }).version;
|
|
283
|
+
if (typeof version !== 'string') throw new Error('electron is installed without a version');
|
|
284
|
+
return version;
|
|
285
|
+
}
|