@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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the Electron runtime a desktop build packages is actually on this machine.
|
|
3
|
+
*
|
|
4
|
+
* **Because the package being installed proves nothing about the runtime, and `build` could not
|
|
5
|
+
* tell the difference.** `installedElectronVersion()` in `build.ts` resolves
|
|
6
|
+
* `electron/package.json` and reads its `version`. That file is in the npm tarball. The binary is
|
|
7
|
+
* not: a postinstall downloads it into `node_modules/electron/dist` and writes `path.txt` beside
|
|
8
|
+
* it. So a version always resolves, a build always proceeds, and electron-builder fails several
|
|
9
|
+
* steps later with a sentence about packaging rather than about a missing runtime.
|
|
10
|
+
*
|
|
11
|
+
* **Measured in this repository on 2026-08-29**, npm 10.9.8, Node 22.23.2:
|
|
12
|
+
* `installedElectronVersion()` answered `43.4.1` while `dist` did not exist — with
|
|
13
|
+
* `npm config get ignore-scripts` answering `false`. So this is not only npm refusing to run
|
|
14
|
+
* scripts, which is the case a consumer hit and reported: it is the plainer fact that a postinstall
|
|
15
|
+
* which downloads something can fail to finish, for a proxy, a slow link, a sandbox or a CI image,
|
|
16
|
+
* and nothing downstream is told.
|
|
17
|
+
*
|
|
18
|
+
* **Both files, not just the directory.** `path.txt` is what Electron's own resolution reads, and a
|
|
19
|
+
* download interrupted after creating `dist` would otherwise read as present.
|
|
20
|
+
*/
|
|
21
|
+
export declare function electronRuntimeState(packageDir: string): 'present' | 'missing';
|
|
22
|
+
/**
|
|
23
|
+
* Fetch the Electron runtime if it is not here, before a build spends its minutes.
|
|
24
|
+
*
|
|
25
|
+
* **It runs the installer instead of telling somebody to.** The first version of this refusal, in a
|
|
26
|
+
* consumer's own wrapper, printed the command and stopped; they watched a person read it and re-run
|
|
27
|
+
* the script unchanged, three times, and changed it to do the thing. A check that knows the remedy,
|
|
28
|
+
* and whose remedy has no alternatives, should apply it.
|
|
29
|
+
*
|
|
30
|
+
* `install.js` is `__dirname`-based, so the working directory does not matter, and it returns
|
|
31
|
+
* immediately when the binary is already there — so this costs nothing on every run after the
|
|
32
|
+
* first. It is the same script npm would have run.
|
|
33
|
+
*
|
|
34
|
+
* **What would make it wrong** is a consumer who deliberately packages against a runtime they
|
|
35
|
+
* supply themselves, which nothing here supports today and which would want a flag rather than a
|
|
36
|
+
* removal.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ensureElectronRuntime(): Promise<void>;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
/**
|
|
6
|
+
* Whether the Electron runtime a desktop build packages is actually on this machine.
|
|
7
|
+
*
|
|
8
|
+
* **Because the package being installed proves nothing about the runtime, and `build` could not
|
|
9
|
+
* tell the difference.** `installedElectronVersion()` in `build.ts` resolves
|
|
10
|
+
* `electron/package.json` and reads its `version`. That file is in the npm tarball. The binary is
|
|
11
|
+
* not: a postinstall downloads it into `node_modules/electron/dist` and writes `path.txt` beside
|
|
12
|
+
* it. So a version always resolves, a build always proceeds, and electron-builder fails several
|
|
13
|
+
* steps later with a sentence about packaging rather than about a missing runtime.
|
|
14
|
+
*
|
|
15
|
+
* **Measured in this repository on 2026-08-29**, npm 10.9.8, Node 22.23.2:
|
|
16
|
+
* `installedElectronVersion()` answered `43.4.1` while `dist` did not exist — with
|
|
17
|
+
* `npm config get ignore-scripts` answering `false`. So this is not only npm refusing to run
|
|
18
|
+
* scripts, which is the case a consumer hit and reported: it is the plainer fact that a postinstall
|
|
19
|
+
* which downloads something can fail to finish, for a proxy, a slow link, a sandbox or a CI image,
|
|
20
|
+
* and nothing downstream is told.
|
|
21
|
+
*
|
|
22
|
+
* **Both files, not just the directory.** `path.txt` is what Electron's own resolution reads, and a
|
|
23
|
+
* download interrupted after creating `dist` would otherwise read as present.
|
|
24
|
+
*/
|
|
25
|
+
export function electronRuntimeState(packageDir) {
|
|
26
|
+
const hasDist = existsSync(join(packageDir, 'dist'));
|
|
27
|
+
const hasPath = existsSync(join(packageDir, 'path.txt'));
|
|
28
|
+
return hasDist && hasPath ? 'present' : 'missing';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fetch the Electron runtime if it is not here, before a build spends its minutes.
|
|
32
|
+
*
|
|
33
|
+
* **It runs the installer instead of telling somebody to.** The first version of this refusal, in a
|
|
34
|
+
* consumer's own wrapper, printed the command and stopped; they watched a person read it and re-run
|
|
35
|
+
* the script unchanged, three times, and changed it to do the thing. A check that knows the remedy,
|
|
36
|
+
* and whose remedy has no alternatives, should apply it.
|
|
37
|
+
*
|
|
38
|
+
* `install.js` is `__dirname`-based, so the working directory does not matter, and it returns
|
|
39
|
+
* immediately when the binary is already there — so this costs nothing on every run after the
|
|
40
|
+
* first. It is the same script npm would have run.
|
|
41
|
+
*
|
|
42
|
+
* **What would make it wrong** is a consumer who deliberately packages against a runtime they
|
|
43
|
+
* supply themselves, which nothing here supports today and which would want a flag rather than a
|
|
44
|
+
* removal.
|
|
45
|
+
*/
|
|
46
|
+
export async function ensureElectronRuntime() {
|
|
47
|
+
const require = createRequire(import.meta.url);
|
|
48
|
+
let installer;
|
|
49
|
+
try {
|
|
50
|
+
installer = require.resolve('electron/install.js');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error('electron is not installed, so there is no runtime to package a desktop build against. ' +
|
|
54
|
+
'Run `npm install` in the project being packaged.');
|
|
55
|
+
}
|
|
56
|
+
const packageDir = dirname(installer);
|
|
57
|
+
if (electronRuntimeState(packageDir) === 'present')
|
|
58
|
+
return;
|
|
59
|
+
console.log('[drift-package] the Electron package is here and its runtime is not, which is what a ' +
|
|
60
|
+
'postinstall that did not finish leaves behind. Fetching it.');
|
|
61
|
+
await new Promise((resolve, reject) => {
|
|
62
|
+
const child = spawn(process.execPath, [installer], { stdio: 'inherit' });
|
|
63
|
+
child.on('error', reject);
|
|
64
|
+
child.on('close', (code) => {
|
|
65
|
+
if (code === 0)
|
|
66
|
+
resolve();
|
|
67
|
+
else
|
|
68
|
+
reject(new Error(`electron's installer exited ${String(code)}`));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
if (electronRuntimeState(packageDir) === 'missing') {
|
|
72
|
+
throw new Error(`The Electron installer ran and ${join(packageDir, 'path.txt')} is still missing, so there ` +
|
|
73
|
+
'is no runtime to package and no point going on.\n\n' +
|
|
74
|
+
'If it printed a download error, it is the network: a proxy, or the ELECTRON_MIRROR and ' +
|
|
75
|
+
'ELECTRON_CUSTOM_DIR variables if this machine needs them.\n' +
|
|
76
|
+
'If npm is blocking install scripts, let it run this one: ' +
|
|
77
|
+
'`npm install-scripts approve electron`.');
|
|
78
|
+
}
|
|
79
|
+
console.log('[drift-package] ok: the Electron runtime is present');
|
|
80
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The major version of an Electron, from whatever the runtime or the installed package says it is.
|
|
3
|
+
*
|
|
4
|
+
* **A tiny module of its own because both callers need the same answer and neither can be trusted
|
|
5
|
+
* to parse it in passing.** The build reads a version out of `electron/package.json`, which may
|
|
6
|
+
* carry a prerelease suffix; the main process reads `process.versions.electron`, which is a plain
|
|
7
|
+
* triple. `null` for anything that is not a version at all, because a plan that guesses a major it
|
|
8
|
+
* could not read is worse than one that says it does not know.
|
|
9
|
+
*/
|
|
10
|
+
export declare function electronMajor(version: string | undefined | null): number | null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The major version of an Electron, from whatever the runtime or the installed package says it is.
|
|
3
|
+
*
|
|
4
|
+
* **A tiny module of its own because both callers need the same answer and neither can be trusted
|
|
5
|
+
* to parse it in passing.** The build reads a version out of `electron/package.json`, which may
|
|
6
|
+
* carry a prerelease suffix; the main process reads `process.versions.electron`, which is a plain
|
|
7
|
+
* triple. `null` for anything that is not a version at all, because a plan that guesses a major it
|
|
8
|
+
* could not read is worse than one that says it does not know.
|
|
9
|
+
*/
|
|
10
|
+
export function electronMajor(version) {
|
|
11
|
+
if (typeof version !== 'string')
|
|
12
|
+
return null;
|
|
13
|
+
const match = /^(\d+)\./.exec(version.trim());
|
|
14
|
+
if (match === null)
|
|
15
|
+
return null;
|
|
16
|
+
const major = Number(match[1]);
|
|
17
|
+
return Number.isSafeInteger(major) && major > 0 ? major : null;
|
|
18
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Target } from './manifest.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Whether this build is signed, ad-hoc signed, or not signed at all — and why.
|
|
4
|
+
*
|
|
5
|
+
* **Three modes rather than a boolean**, because the middle one is load-bearing: Apple Silicon's
|
|
6
|
+
* loader refuses to execute an unsigned binary outright, which is not Gatekeeper and which no user
|
|
7
|
+
* gesture works around. A macOS build with no certificate must therefore carry *a* signature, and
|
|
8
|
+
* `codesign --sign -` is the one available for free. It is not a substitute for a Developer ID: a
|
|
9
|
+
* downloaded `.dmg` carries `com.apple.quarantine`, an ad-hoc signature does not satisfy
|
|
10
|
+
* Gatekeeper, and the first launch is refused until the flag is cleared.
|
|
11
|
+
*
|
|
12
|
+
* **Everything is read from the environment and nothing from `drift.package.json`.** The manifest
|
|
13
|
+
* is committed; a certificate and its password are not, and a packager that read them from a
|
|
14
|
+
* committed file would be teaching people to commit them. The variable names are
|
|
15
|
+
* `electron-builder`'s own, so there is no translation layer to maintain.
|
|
16
|
+
*
|
|
17
|
+
* **`reason` is printed on every build.** An artifact whose signature status was not stated is the
|
|
18
|
+
* failure this module exists to prevent: the person who built it finds out from a player.
|
|
19
|
+
*/
|
|
20
|
+
export type SigningMode = 'none' | 'adhoc' | 'real';
|
|
21
|
+
export interface SigningPlan {
|
|
22
|
+
readonly mode: SigningMode;
|
|
23
|
+
/** Notarisation is a separate credential from signing, and needs a paid Developer ID. */
|
|
24
|
+
readonly notarise: boolean;
|
|
25
|
+
/** What was decided and what it costs, in words a build log should carry. */
|
|
26
|
+
readonly reason: string;
|
|
27
|
+
/**
|
|
28
|
+
* Whether this build applies the macOS signature itself, because electron-builder will not.
|
|
29
|
+
*
|
|
30
|
+
* True exactly when a Mac target is built somewhere that is not a Mac. `isSignAllowed()` returns
|
|
31
|
+
* false off darwin, and electron-builder then packs on regardless: it logs one warning and
|
|
32
|
+
* produces an unsigned `.app`, which is the artifact Apple Silicon's loader refuses. `rcodesign`
|
|
33
|
+
* puts the signature on afterwards, and this is the flag that says somebody still has to.
|
|
34
|
+
*/
|
|
35
|
+
readonly crossSign: boolean;
|
|
36
|
+
/** Non-null means refuse to build, and this is what to print. */
|
|
37
|
+
readonly refusal: string | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The machine a build is running on, spelled the way `process.platform` spells it.
|
|
41
|
+
*
|
|
42
|
+
* **Passed in rather than read**, and that is this half of the package's rule rather than a
|
|
43
|
+
* preference: `signing.ts` is typechecked with no ambient Node types — see the `exclude` list in
|
|
44
|
+
* `tsconfig.json` — because a consumer bundles this source for a browser, and a `process` reaching
|
|
45
|
+
* one of their bundles is the failure that config exists to prevent. Taking it as an argument also
|
|
46
|
+
* makes every test name the host it means instead of inheriting whichever machine ran it.
|
|
47
|
+
*/
|
|
48
|
+
export type BuildHost = string;
|
|
49
|
+
export declare function planSigning(target: Target, env: Readonly<Record<string, string | undefined>>, platform: BuildHost): SigningPlan;
|
package/dist/signing.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** The ordinary case: the host signs for itself, with its own signer, and nothing is refused. */
|
|
2
|
+
const HOST_SIGNED = { crossSign: false, refusal: null };
|
|
3
|
+
function has(env, name) {
|
|
4
|
+
const value = env[name];
|
|
5
|
+
return typeof value === 'string' && value.length > 0;
|
|
6
|
+
}
|
|
7
|
+
export function planSigning(target, env, platform) {
|
|
8
|
+
if (target === 'linux-x64') {
|
|
9
|
+
return {
|
|
10
|
+
...HOST_SIGNED,
|
|
11
|
+
mode: 'none',
|
|
12
|
+
notarise: false,
|
|
13
|
+
/* No signature is expected of a Linux artifact by anything that runs one. An AppImage can
|
|
14
|
+
carry a detached GPG signature and nothing checks it by default, so producing one would be
|
|
15
|
+
ceremony rather than protection. */
|
|
16
|
+
reason: 'Linux artifacts are not signed; nothing on the platform checks one',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (target === 'android') {
|
|
20
|
+
/*
|
|
21
|
+
* **Android is the one platform with no unsigned option to offer**: it refuses to install an
|
|
22
|
+
* unsigned APK outright, so a build always signs and the only question is with whose key.
|
|
23
|
+
*/
|
|
24
|
+
if (has(env, 'DRIFT_ANDROID_KEYSTORE')) {
|
|
25
|
+
return {
|
|
26
|
+
...HOST_SIGNED,
|
|
27
|
+
mode: 'real',
|
|
28
|
+
notarise: false,
|
|
29
|
+
reason: 'signed with the keystore in DRIFT_ANDROID_KEYSTORE',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
...HOST_SIGNED,
|
|
34
|
+
mode: 'adhoc',
|
|
35
|
+
notarise: false,
|
|
36
|
+
reason: 'a generated throwaway key: installs on a device with developer mode on, cannot be ' +
|
|
37
|
+
'published to the Play Store, and an upgrade signed with a different generated key is ' +
|
|
38
|
+
'refused by the device as an impostor — which is why the generated one is kept',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (target === 'ios') {
|
|
42
|
+
if (has(env, 'APPLE_TEAM_ID')) {
|
|
43
|
+
return {
|
|
44
|
+
...HOST_SIGNED,
|
|
45
|
+
mode: 'real',
|
|
46
|
+
notarise: false,
|
|
47
|
+
reason: 'signed with the Apple team in APPLE_TEAM_ID',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
...HOST_SIGNED,
|
|
52
|
+
mode: 'adhoc',
|
|
53
|
+
notarise: false,
|
|
54
|
+
reason: 'free provisioning: a build installs to your own device and stops running after seven ' +
|
|
55
|
+
'days, which is the Apple ID path rather than the paid Developer Program',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (target === 'win-x64') {
|
|
59
|
+
if (has(env, 'WIN_CSC_LINK') || has(env, 'CSC_LINK')) {
|
|
60
|
+
return {
|
|
61
|
+
...HOST_SIGNED,
|
|
62
|
+
mode: 'real',
|
|
63
|
+
notarise: false,
|
|
64
|
+
reason: 'Authenticode signing from the certificate in the environment',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
...HOST_SIGNED,
|
|
69
|
+
mode: 'none',
|
|
70
|
+
notarise: false,
|
|
71
|
+
reason: 'unsigned: Windows will run it and SmartScreen will warn on first launch. A Steam depot ' +
|
|
72
|
+
'raises no warning, because the client delivering it is signed',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const real = has(env, 'CSC_LINK') || has(env, 'CSC_NAME');
|
|
76
|
+
/*
|
|
77
|
+
* **macOS from somewhere that is not macOS.** electron-builder's own refusal fires on Windows
|
|
78
|
+
* alone, so a Mac target packs here and comes out unsigned unless this build signs it — see
|
|
79
|
+
* `crossSign` above for why that is fatal rather than cosmetic.
|
|
80
|
+
*/
|
|
81
|
+
if (platform !== 'darwin') {
|
|
82
|
+
/*
|
|
83
|
+
* **A certificate in the environment is refused rather than ignored.** `CSC_NAME` names an
|
|
84
|
+
* identity in a keychain and there is no keychain here; `CSC_LINK` is a `.p12`, which
|
|
85
|
+
* rcodesign can use in principle and this build does not wire. Either way the artifact would
|
|
86
|
+
* come out ad-hoc — and an ad-hoc build whose author believes it carries a Developer ID is
|
|
87
|
+
* worse than no build, because that is the one they ship to strangers.
|
|
88
|
+
*/
|
|
89
|
+
if (real) {
|
|
90
|
+
return {
|
|
91
|
+
mode: 'adhoc',
|
|
92
|
+
notarise: false,
|
|
93
|
+
crossSign: true,
|
|
94
|
+
reason: 'refused before signing; see the refusal',
|
|
95
|
+
refusal: `a Developer ID certificate is in the environment and this is ${platform}, which ` +
|
|
96
|
+
'cannot use one: CSC_NAME is a keychain identity and there is no keychain here, and ' +
|
|
97
|
+
'the .p12 route is not wired. The build would be ad-hoc while reporting a certificate. ' +
|
|
98
|
+
'Build it on a Mac, or unset CSC_LINK and CSC_NAME to get an ad-hoc build on purpose',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
mode: 'adhoc',
|
|
103
|
+
notarise: false,
|
|
104
|
+
crossSign: true,
|
|
105
|
+
refusal: null,
|
|
106
|
+
reason: 'ad-hoc signed by rcodesign, because electron-builder skips signing off a Mac and an ' +
|
|
107
|
+
'unsigned binary is refused by the loader on Apple Silicon. The artifact is a .zip: a ' +
|
|
108
|
+
'.dmg is built by hdiutil, which exists only on macOS. A downloaded copy is still ' +
|
|
109
|
+
'refused by Gatekeeper until `xattr -dr com.apple.quarantine` clears the flag, and ' +
|
|
110
|
+
'nothing on this host can prove the result launches — open it on a Mac before it ships',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const notarise = real &&
|
|
114
|
+
has(env, 'APPLE_ID') &&
|
|
115
|
+
has(env, 'APPLE_APP_SPECIFIC_PASSWORD') &&
|
|
116
|
+
has(env, 'APPLE_TEAM_ID');
|
|
117
|
+
if (real) {
|
|
118
|
+
return {
|
|
119
|
+
...HOST_SIGNED,
|
|
120
|
+
mode: 'real',
|
|
121
|
+
notarise,
|
|
122
|
+
reason: notarise
|
|
123
|
+
? 'Developer ID signing and notarisation from the environment'
|
|
124
|
+
: 'Developer ID signing; not notarised, so a download is still refused until it is',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
...HOST_SIGNED,
|
|
129
|
+
mode: 'adhoc',
|
|
130
|
+
notarise: false,
|
|
131
|
+
reason: 'ad-hoc signed: an unsigned binary is refused by the loader on Apple Silicon, so this is ' +
|
|
132
|
+
'the minimum that runs. A downloaded copy is still refused by Gatekeeper until ' +
|
|
133
|
+
'`xattr -dr com.apple.quarantine` clears the flag',
|
|
134
|
+
};
|
|
135
|
+
}
|
package/dist/stage.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Identity } from './identity.ts';
|
|
2
|
+
import type { PackageManifest } from './manifest.ts';
|
|
3
|
+
import type { ResourceOptions } from './resources.ts';
|
|
4
|
+
export declare function stageApp(manifest: PackageManifest, projectDir: string, stageDir: string, options?: {
|
|
5
|
+
readonly development: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* How a path's filesystem identity is read, for the refusal below.
|
|
8
|
+
*
|
|
9
|
+
* The same seam `refuseLyingFilesystem` carries and for the same reason: the condition cannot be
|
|
10
|
+
* reproduced on any filesystem this suite can create, so without it the refusal would ship
|
|
11
|
+
* having never been seen to fire. The default is `statSync` and no caller passes anything else.
|
|
12
|
+
*/
|
|
13
|
+
readonly look?: (path: string) => Identity;
|
|
14
|
+
} & ResourceOptions): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* The version the artifact carries, taken from the consumer's own manifest.
|
|
17
|
+
*
|
|
18
|
+
* A packaged application needs one — installers key upgrades on it — and inventing one here would
|
|
19
|
+
* put a number on a release that matches nothing. Absent, it is `0.0.0`, which is visibly not a
|
|
20
|
+
* release rather than plausibly one.
|
|
21
|
+
*/
|
|
22
|
+
export declare function consumerVersion(projectDir: string): Promise<string>;
|
package/dist/stage.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { build as esbuild } from 'esbuild';
|
|
2
|
+
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { refuseLyingFilesystem } from './identity.js';
|
|
6
|
+
import { resourceDir } from './resources.js';
|
|
7
|
+
/**
|
|
8
|
+
* Everything the shell needs, laid out the way `main/entry.ts` expects to find it.
|
|
9
|
+
*
|
|
10
|
+
* **This exists because the workspace ships TypeScript and Electron runs JavaScript.** There is no
|
|
11
|
+
* build step anywhere else here — a consumer bundles `src/index.ts` themselves — but a main
|
|
12
|
+
* process is not bundled by anybody downstream, so the packager bundles its own two entry points
|
|
13
|
+
* and nothing else. esbuild rather than tsc: it is already a dependency of this repository, it
|
|
14
|
+
* emits CommonJS in one call, and a preload script has to be CommonJS whatever its source is.
|
|
15
|
+
*
|
|
16
|
+
* The layout is the contract between this file, `main/entry.ts` and `main/window.ts`:
|
|
17
|
+
*
|
|
18
|
+
* main.cjs the main process
|
|
19
|
+
* preload.cjs the bridge, loaded before the game
|
|
20
|
+
* drift.package.json the manifest, read at startup
|
|
21
|
+
* build.json whether this is a development run or a shipped artifact
|
|
22
|
+
* shell/ the packager's own assets, served as drift://shell/
|
|
23
|
+
* app/ the game, served as drift://app/
|
|
24
|
+
*
|
|
25
|
+
* **Electron is external to both bundles.** It is resolved from the application's `node_modules`
|
|
26
|
+
* at run time by the runtime itself; bundling it would be bundling a native module.
|
|
27
|
+
*/
|
|
28
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
export async function stageApp(manifest, projectDir, stageDir, options = { development: false }) {
|
|
30
|
+
/*
|
|
31
|
+
* **Before the first copy, and before anything is deleted, because this is where the copy is.**
|
|
32
|
+
*
|
|
33
|
+
* `doctor` refuses this too and does it earlier, which is the right place for a person's time: a
|
|
34
|
+
* build that cannot succeed should stop before it downloads a runtime. But that is a guarantee
|
|
35
|
+
* about the order of two calls, and this package's `exports` map is `"./*": "./*"` — so
|
|
36
|
+
* `stageApp` is importable directly, and a route that never passes through `doctor` is a route
|
|
37
|
+
* where a `cp` explains itself in terms of subdirectories of itself. The check is two `stat`s.
|
|
38
|
+
* It belongs to the copy rather than to the command that usually precedes it.
|
|
39
|
+
*/
|
|
40
|
+
refuseLyingFilesystem(projectDir, stageDir, options.look);
|
|
41
|
+
await rm(stageDir, { recursive: true, force: true });
|
|
42
|
+
await mkdir(stageDir, { recursive: true });
|
|
43
|
+
/* Two calls with explicit outfiles rather than one with two entry points: both entries are
|
|
44
|
+
named `entry.ts`, so a shared `entryNames` would have them collide on the way out — and the
|
|
45
|
+
two names are a contract `main/entry.ts` and `window.ts` both read. */
|
|
46
|
+
for (const [from, to] of [
|
|
47
|
+
[join(HERE, 'main', 'entry.ts'), join(stageDir, 'main.cjs')],
|
|
48
|
+
[join(HERE, 'preload', 'entry.ts'), join(stageDir, 'preload.cjs')],
|
|
49
|
+
]) {
|
|
50
|
+
await esbuild({
|
|
51
|
+
entryPoints: [from],
|
|
52
|
+
outfile: to,
|
|
53
|
+
bundle: true,
|
|
54
|
+
platform: 'node',
|
|
55
|
+
format: 'cjs',
|
|
56
|
+
target: 'node22',
|
|
57
|
+
external: ['electron'],
|
|
58
|
+
logLevel: 'warning',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const version = await consumerVersion(projectDir);
|
|
62
|
+
await writeFile(join(stageDir, 'package.json'), `${JSON.stringify({
|
|
63
|
+
name: manifest.id.split('.').pop() ?? 'app',
|
|
64
|
+
productName: manifest.name,
|
|
65
|
+
version,
|
|
66
|
+
main: 'main.cjs',
|
|
67
|
+
/* Both are read by installers rather than by code: electron-builder warns without them,
|
|
68
|
+
and a Windows installer with no publisher is the one a person is asked to trust. */
|
|
69
|
+
description: manifest.name,
|
|
70
|
+
author: manifest.publisher,
|
|
71
|
+
/* No dependencies: both entry points are bundled, and Electron is supplied by the
|
|
72
|
+
runtime that is executing them. */
|
|
73
|
+
}, null, 2)}\n`);
|
|
74
|
+
await cp(join(projectDir, 'drift.package.json'), join(stageDir, 'drift.package.json'));
|
|
75
|
+
/*
|
|
76
|
+
* **Which kind of build this is, written into the artifact rather than read from an
|
|
77
|
+
* environment variable.** A player's machine has whatever variables it has, and a game that
|
|
78
|
+
* decided whether to expose developer tools from one of them would be a game somebody could
|
|
79
|
+
* turn inside out by exporting a name. The file is written by the packager and is absent from
|
|
80
|
+
* nothing: `entry.ts` treats a missing one as production, which is the safe direction.
|
|
81
|
+
*/
|
|
82
|
+
await writeFile(join(stageDir, 'build.json'), `${JSON.stringify({ development: options.development }, null, 2)}\n`);
|
|
83
|
+
if (manifest.steam.appId !== null) {
|
|
84
|
+
/*
|
|
85
|
+
* **The store SDK is copied in, or the build says it is not there.**
|
|
86
|
+
*
|
|
87
|
+
* The staged application has no `node_modules` — both entry points are bundled — and
|
|
88
|
+
* `steamworks.js` is a native module that cannot be bundled at all. So it is copied from the
|
|
89
|
+
* consumer's own tree, which is also what makes it opt-in: a game that never installs it never
|
|
90
|
+
* ships one, and this says so rather than producing a build whose achievements silently do
|
|
91
|
+
* nothing.
|
|
92
|
+
*/
|
|
93
|
+
const sdk = join(projectDir, 'node_modules', 'steamworks.js');
|
|
94
|
+
try {
|
|
95
|
+
await cp(sdk, join(stageDir, 'node_modules', 'steamworks.js'), { recursive: true });
|
|
96
|
+
await writeFile(join(stageDir, 'node_modules', '.keep'), 'the store SDK lives here; nothing else does\n', 'utf8');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
console.warn(`[drift-package] steam.appId is ${manifest.steam.appId} and steamworks.js is not ` +
|
|
100
|
+
`installed in ${projectDir}. The build will run with no store: achievements, presence ` +
|
|
101
|
+
'and cloud saves are absent. `npm install steamworks.js` in the game to change that.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (manifest.steam.appId !== null) {
|
|
105
|
+
/*
|
|
106
|
+
* **Steam looks for this file beside the executable** when a build is run outside its client,
|
|
107
|
+
* which is every run during development. Without it `SteamAPI_Init` fails and a game that
|
|
108
|
+
* degrades correctly reports no achievements — so the integration looks broken exactly while
|
|
109
|
+
* somebody is building it, and works only after upload.
|
|
110
|
+
*/
|
|
111
|
+
await writeFile(join(stageDir, 'steam_appid.txt'), `${manifest.steam.appId}\n`, 'utf8');
|
|
112
|
+
}
|
|
113
|
+
/* Through `resourceDir`, which is the one place that knows where this package's own three
|
|
114
|
+
directories are and the only place a caller can move them. See `resources.ts`. */
|
|
115
|
+
await cp(await resourceDir('assets', options.resourceRoot), join(stageDir, 'shell'), {
|
|
116
|
+
recursive: true,
|
|
117
|
+
});
|
|
118
|
+
await cp(resolve(projectDir, dirname(manifest.entry)), join(stageDir, 'app'), {
|
|
119
|
+
recursive: true,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The version the artifact carries, taken from the consumer's own manifest.
|
|
124
|
+
*
|
|
125
|
+
* A packaged application needs one — installers key upgrades on it — and inventing one here would
|
|
126
|
+
* put a number on a release that matches nothing. Absent, it is `0.0.0`, which is visibly not a
|
|
127
|
+
* release rather than plausibly one.
|
|
128
|
+
*/
|
|
129
|
+
export async function consumerVersion(projectDir) {
|
|
130
|
+
try {
|
|
131
|
+
const raw = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf8'));
|
|
132
|
+
const version = raw.version;
|
|
133
|
+
return typeof version === 'string' && version.length > 0 ? version : '0.0.0';
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return '0.0.0';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { KeyValueStore } from '@driftengine/core';
|
|
2
|
+
/**
|
|
3
|
+
* Steam Cloud, behind the persistence seam the engine already had.
|
|
4
|
+
*
|
|
5
|
+
* **`KeyValueStore` needed no widening for this**, which is the point of the seam: the engine
|
|
6
|
+
* reads preferences synchronously during boot, Steam's cloud API is synchronous, and the two fit
|
|
7
|
+
* without an adapter. A game that keeps its saves here gets them on the player's other machine for
|
|
8
|
+
* free, and nothing in the engine knows Steam exists.
|
|
9
|
+
*
|
|
10
|
+
* **Every call degrades rather than throws.** Cloud storage can be turned off per title or per
|
|
11
|
+
* account, and the failure that produces is a game that will not start — on the first preference
|
|
12
|
+
* read, before anything is on screen. So a throw falls back to memory, which is exactly what
|
|
13
|
+
* `BrowserStore` does when `localStorage` is blocked: the settings last the session instead of
|
|
14
|
+
* resetting between screens.
|
|
15
|
+
*/
|
|
16
|
+
export interface SteamCloud {
|
|
17
|
+
readFile(name: string): string;
|
|
18
|
+
writeFile(name: string, data: string): boolean;
|
|
19
|
+
deleteFile(name: string): boolean;
|
|
20
|
+
fileExists(name: string): boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare class SteamCloudStore implements KeyValueStore {
|
|
23
|
+
private readonly cloud;
|
|
24
|
+
/** What was read or written this session, so a blocked cloud still behaves like a store. */
|
|
25
|
+
private readonly local;
|
|
26
|
+
constructor(cloud: SteamCloud);
|
|
27
|
+
read(key: string): string | null;
|
|
28
|
+
write(key: string, value: string): void;
|
|
29
|
+
remove(key: string): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A key is a file name on somebody's disk.
|
|
3
|
+
*
|
|
4
|
+
* Slashes, backslashes and dots would make one key reach another game's file, or none at all.
|
|
5
|
+
* Replaced rather than rejected, because a game that has been writing `world.day.23` for a year
|
|
6
|
+
* must not start losing saves the day this lands.
|
|
7
|
+
*/
|
|
8
|
+
function fileNameFor(key) {
|
|
9
|
+
return `${key.replace(/[^A-Za-z0-9._-]/g, '_').replace(/\.\.+/g, '.')}.txt`;
|
|
10
|
+
}
|
|
11
|
+
export class SteamCloudStore {
|
|
12
|
+
cloud;
|
|
13
|
+
/** What was read or written this session, so a blocked cloud still behaves like a store. */
|
|
14
|
+
local = new Map();
|
|
15
|
+
constructor(cloud) {
|
|
16
|
+
this.cloud = cloud;
|
|
17
|
+
}
|
|
18
|
+
read(key) {
|
|
19
|
+
const cached = this.local.get(key);
|
|
20
|
+
if (cached !== undefined)
|
|
21
|
+
return cached;
|
|
22
|
+
try {
|
|
23
|
+
const name = fileNameFor(key);
|
|
24
|
+
if (!this.cloud.fileExists(name))
|
|
25
|
+
return null;
|
|
26
|
+
const value = this.cloud.readFile(name);
|
|
27
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
this.local.set(key, value);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
write(key, value) {
|
|
37
|
+
this.local.set(key, value);
|
|
38
|
+
try {
|
|
39
|
+
this.cloud.writeFile(fileNameFor(key), value);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* Off for this account or this title. The value is in memory and the game carries on. */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
remove(key) {
|
|
46
|
+
this.local.delete(key);
|
|
47
|
+
try {
|
|
48
|
+
this.cloud.deleteFile(fileNameFor(key));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* As above. */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two files `steamcmd` needs to upload a build, generated from the manifest.
|
|
3
|
+
*
|
|
4
|
+
* **Written rather than copied**, because the alternative is a template with somebody else's app
|
|
5
|
+
* id in it. A wrong app id uploads a game into another game's depot, or fails after the whole
|
|
6
|
+
* payload has crossed the network; a wrong depot id ships a build nobody can install.
|
|
7
|
+
*
|
|
8
|
+
* **Nothing is set live.** `setlive` is empty unless a branch is named, so an upload is a build
|
|
9
|
+
* sitting in Steam's admin waiting for a person, not a release. Valve's own template does the same
|
|
10
|
+
* and it is the line people delete by accident.
|
|
11
|
+
*
|
|
12
|
+
* Pure: the strings are the whole of it, and every rule above is a test.
|
|
13
|
+
*/
|
|
14
|
+
export interface DepotOptions {
|
|
15
|
+
readonly appId: number;
|
|
16
|
+
readonly depotId: number;
|
|
17
|
+
readonly description: string;
|
|
18
|
+
/** Relative to where the scripts are written. */
|
|
19
|
+
readonly contentRoot: string;
|
|
20
|
+
/** A branch to publish to, or null to upload and stop. */
|
|
21
|
+
readonly branch: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface DepotScripts {
|
|
24
|
+
readonly appFileName: string;
|
|
25
|
+
readonly app: string;
|
|
26
|
+
readonly depotFileName: string;
|
|
27
|
+
readonly depot: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function depotScripts(options: DepotOptions): DepotScripts;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function depotScripts(options) {
|
|
2
|
+
const depotFileName = `depot_build_${options.depotId}.vdf`;
|
|
3
|
+
return {
|
|
4
|
+
appFileName: `app_build_${options.appId}.vdf`,
|
|
5
|
+
app: `"appbuild"
|
|
6
|
+
{
|
|
7
|
+
\t"appid"\t"${options.appId}"
|
|
8
|
+
\t"desc"\t"${options.description}"
|
|
9
|
+
\t"buildoutput"\t"./steam-build-output"
|
|
10
|
+
\t"contentroot"\t"./${options.contentRoot}"
|
|
11
|
+
\t"setlive"\t"${options.branch ?? ''}"
|
|
12
|
+
\t"preview"\t"0"
|
|
13
|
+
\t"local"\t""
|
|
14
|
+
|
|
15
|
+
\t"depots"
|
|
16
|
+
\t{
|
|
17
|
+
\t\t"${options.depotId}"\t"${depotFileName}"
|
|
18
|
+
\t}
|
|
19
|
+
}
|
|
20
|
+
`,
|
|
21
|
+
depotFileName,
|
|
22
|
+
depot: `"DepotBuildConfig"
|
|
23
|
+
{
|
|
24
|
+
\t"DepotID"\t"${options.depotId}"
|
|
25
|
+
\t"contentroot"\t"./${options.contentRoot}"
|
|
26
|
+
|
|
27
|
+
\t"FileMapping"
|
|
28
|
+
\t{
|
|
29
|
+
\t\t"LocalPath"\t"*"
|
|
30
|
+
\t\t"DepotPath"\t"."
|
|
31
|
+
\t\t"recursive"\t"1"
|
|
32
|
+
\t}
|
|
33
|
+
|
|
34
|
+
\t"FileExclusion"\t"*.pdb"
|
|
35
|
+
\t"FileExclusion"\t"*.log"
|
|
36
|
+
}
|
|
37
|
+
`,
|
|
38
|
+
};
|
|
39
|
+
}
|