@driftengine/package 3.63.0 → 4.1.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/NOTICE +20 -0
- package/README.md +58 -1
- package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +1 -1
- package/dist/bootstrap.js +7 -1
- package/dist/build.js +23 -10
- package/dist/cli.js +50 -7
- package/dist/flags.js +16 -0
- package/dist/main/ipc.js +5 -19
- package/dist/main/storeFile.d.ts +30 -0
- package/dist/main/storeFile.js +104 -0
- package/dist/main/window.js +6 -3
- package/dist/manifest.d.ts +8 -1
- package/dist/manifest.js +8 -0
- package/dist/native.d.ts +67 -0
- package/dist/native.js +212 -0
- package/dist/nativeBundle.d.ts +53 -0
- package/dist/nativeBundle.js +177 -0
- package/dist/nativeCopy.d.ts +39 -0
- package/dist/nativeCopy.js +110 -0
- package/dist/nativeLicenses.d.ts +42 -0
- package/dist/nativeLicenses.js +131 -0
- package/dist/signing.js +2 -2
- package/dist/toolchain.js +3 -0
- package/package.json +12 -2
- package/src/bootstrap.ts +11 -1
- package/src/build.ts +25 -13
- package/src/cli.ts +54 -7
- package/src/flags.ts +18 -0
- package/src/main/ipc.ts +6 -17
- package/src/main/storeFile.ts +113 -0
- package/src/main/window.ts +6 -3
- package/src/manifest.ts +19 -1
- package/src/native.ts +263 -0
- package/src/nativeBundle.ts +215 -0
- package/src/nativeCopy.ts +133 -0
- package/src/nativeLicenses.ts +163 -0
- package/src/signing.ts +2 -2
- package/src/toolchain.ts +3 -0
package/NOTICE
CHANGED
|
@@ -7,3 +7,23 @@ This product includes software developed at Drift Technologies
|
|
|
7
7
|
Licensed under the Apache License, Version 2.0. Section 4(d) of that licence
|
|
8
8
|
requires this NOTICE to be reproduced in any derivative work you distribute,
|
|
9
9
|
in the source, the documentation, or a display generated by the work.
|
|
10
|
+
|
|
11
|
+
Models run by DriftCapture
|
|
12
|
+
--------------------------
|
|
13
|
+
DriftCapture runs the models below. Their weights are not in this repository
|
|
14
|
+
and are not distributed with it: tools/capture-weights/ fetches each at the
|
|
15
|
+
revision its manifest pins, verifies it, and converts it, and a game that
|
|
16
|
+
ships one ships it under that model's own licence. Each model's definition in
|
|
17
|
+
this engine is written from its project's published code.
|
|
18
|
+
|
|
19
|
+
- Depth Anything 3 Small and Depth Anything 3 Base. Apache License 2.0.
|
|
20
|
+
Copyright (c) 2025 ByteDance Ltd. and/or its affiliates.
|
|
21
|
+
- Depth Anything V2 Small. Apache License 2.0. The University of Hong Kong
|
|
22
|
+
and TikTok; converted to the Transformers layout by The HuggingFace Team.
|
|
23
|
+
- MobileSAM. Apache License 2.0. Its image encoder is TinyViT, Copyright (c)
|
|
24
|
+
2022 Microsoft; its prompt encoder and mask decoder are Segment Anything's,
|
|
25
|
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
26
|
+
- SAM 2.1 tiny. Apache License 2.0. Copyright (c) Meta Platforms, Inc. and
|
|
27
|
+
affiliates.
|
|
28
|
+
- OWLv2 base patch16. Apache License 2.0. Copyright 2023 Google AI and The
|
|
29
|
+
HuggingFace Team.
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ learning what a shell is.
|
|
|
7
7
|
npx drift-package init # writes the two scripts your repository has to own
|
|
8
8
|
npx drift-package doctor # is this project packageable, and how would it be signed
|
|
9
9
|
npx drift-package run # the real shell, from source, no installer
|
|
10
|
-
npx drift-package build --target=linux-x64
|
|
10
|
+
npx drift-package build --target=native-linux-x64
|
|
11
11
|
npx drift-package verify --contains=<a string from your diff>
|
|
12
12
|
```
|
|
13
13
|
|
|
@@ -64,6 +64,8 @@ Beside your `package.json`, in the project that produces the web build:
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
`id`, `name`, `entry` and `targets` are required; every rejection names the field it is about.
|
|
67
|
+
`native.entry` is required when `targets` names `native-linux-x64`, and nowhere else — see
|
|
68
|
+
[the native target](#the-native-target-and-electron-as-the-compatibility-target).
|
|
67
69
|
`icon` is a 1024x1024 PNG and defaults to the engine's own mark, because the alternative default is
|
|
68
70
|
Electron's logo. `window.mode` is what the window is _created_ as — `borderless` makes it
|
|
69
71
|
frameless — and is a different thing from the runtime mode a settings screen changes.
|
|
@@ -327,6 +329,60 @@ which is committed.
|
|
|
327
329
|
mobile build is 20 MB rather than 200, and it is also why WebGL2 is the baseline there: what the
|
|
328
330
|
device offers is what the game gets, and the acceptance probe decides.
|
|
329
331
|
|
|
332
|
+
## The native target, and Electron as the compatibility target
|
|
333
|
+
|
|
334
|
+
**On Linux x64, `native-linux-x64` is the target to ship.** It runs the game on
|
|
335
|
+
[`@driftengine/native-host`](../native-host/README.md) — Node, Dawn and SDL, with no browser — and
|
|
336
|
+
the archive carries everything that runs it:
|
|
337
|
+
|
|
338
|
+
startup/ what the archive holds, and what a Steam depot would upload
|
|
339
|
+
startup the launcher: the Node beside it, running the game
|
|
340
|
+
runtime/node the Node that ran the build
|
|
341
|
+
app/game.mjs the game, the host and the engine, bundled for Node
|
|
342
|
+
app/modules/ each module the bundle starts by URL: workers, the thread bootstrap
|
|
343
|
+
app/node_modules/ the modules that cannot be bundled, and what they load
|
|
344
|
+
app/public/ the game's web build, which its `fetch` of a relative path reads
|
|
345
|
+
licenses/ Node's licence and every package's, with INDEX.txt
|
|
346
|
+
Startup-1.0.0-linux-x64.tar.gz
|
|
347
|
+
|
|
348
|
+
Two things it needs of the game. **The host**, installed beside it at the packager's version —
|
|
349
|
+
`npm install @driftengine/native-host@<version>`; `doctor` names the command when it is missing, and
|
|
350
|
+
a build refuses a host from another release. **A native entry**, `native.entry` in the manifest: a
|
|
351
|
+
module exporting `mount(canvas)`, since there is no page there for a game to find its canvas in.
|
|
352
|
+
The game's page still builds as before, and its files are what the host serves to the game's
|
|
353
|
+
`fetch`. Saves are kept where the desktop target keeps them, under the same name, so a player
|
|
354
|
+
moving between the two builds keeps theirs.
|
|
355
|
+
|
|
356
|
+
**Measured 2026-09-19**, one small program — the starter's lit cube — packaged both ways on one
|
|
357
|
+
machine (an RX 9070 XT under COSMIC), with `npm run native:startup -- --electron` in the engine's
|
|
358
|
+
repository:
|
|
359
|
+
|
|
360
|
+
| | `native-linux-x64` | `linux-x64` (Electron 43.4.1) |
|
|
361
|
+
| -------------------- | ----------------------------- | ------------------------------------- |
|
|
362
|
+
| archive | 61.3 MB | 121.7 MB tarball, 128.5 MB AppImage |
|
|
363
|
+
| unpacked | 181.2 MB, of which Node 124.8 | 330.6 MB |
|
|
364
|
+
| spawn to first frame | 750 ms, WebGPU | 380 ms, WebGL2 — it offered no WebGPU |
|
|
365
|
+
| build | 7 s | 3 min 13 s |
|
|
366
|
+
|
|
367
|
+
The native start is mostly the renderer's pipelines, compiled on every launch because the binding
|
|
368
|
+
exposes Dawn's cache to nothing: 355 ms of the 750, where Node's own start is 17. **On that
|
|
369
|
+
machine's Wayland session the Electron build did not start at all** — its main thread waits on the
|
|
370
|
+
compositor's socket before `ready` — so it was timed under XWayland, where the native host's window
|
|
371
|
+
is too.
|
|
372
|
+
|
|
373
|
+
**The Electron targets are kept, as the compatibility target**, and nothing that shipped stops
|
|
374
|
+
shipping: they are the desktop build on Windows and macOS, where there is no native host yet — it
|
|
375
|
+
reaches both after 4.0.0, and the three bindings already publish binaries for them — and on
|
|
376
|
+
Linux the build for a game with no native entry yet, or one that needs what the host lacks — WebGL2
|
|
377
|
+
beneath WebGPU on a machine with no Vulkan device, a DOM beyond the canvas, the engine's badge, and
|
|
378
|
+
a store (`steam.appId` does nothing on the host, and `doctor` says so).
|
|
379
|
+
|
|
380
|
+
**No copyleft library is bundled.** The Opus decoder's Ogg parser is LGPL-3.0, so it ships as a
|
|
381
|
+
file of its own in `app/node_modules`, which a person can replace, with the GPL's text beside it in
|
|
382
|
+
`licenses/`; any other that would land in the bundle stops the build, naming it. `verify
|
|
383
|
+
--target=native-linux-x64` looks in the native bundle, which is built from the game's source rather
|
|
384
|
+
than copied from its web build.
|
|
385
|
+
|
|
330
386
|
## Where each artifact is built
|
|
331
387
|
|
|
332
388
|
An artifact is built on the platform it targets, with one exception, and `build` refuses the ones
|
|
@@ -334,6 +390,7 @@ this machine cannot honestly produce.
|
|
|
334
390
|
|
|
335
391
|
| target | built on | how |
|
|
336
392
|
| ---------------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
|
|
393
|
+
| `native-linux-x64` | Linux on x64 | `npx drift-package build --target=native-linux-x64` |
|
|
337
394
|
| `linux-x64` | Linux | `npx drift-package build --target=linux-x64` |
|
|
338
395
|
| `win-x64` | Windows, a VM is fine | `drift-package init` writes `scripts/build-windows.ps1` into your project; run it there |
|
|
339
396
|
| `mac-arm64`, `mac-x64` | macOS, **or Linux** | [`scripts/build-macos.sh`](scripts/build-macos.sh), or `build --target=mac-arm64` |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#
|
|
1
|
+
#Sun Sep 20 15:44:54 CEST 2026
|
|
2
2
|
gradle.version=8.9
|
package/dist/bootstrap.js
CHANGED
|
@@ -23,9 +23,15 @@ import { PINS, ensureDir, findAndroidSdk, findGradle, findJavaHome, findRcodesig
|
|
|
23
23
|
export async function bootstrap(targets) {
|
|
24
24
|
const wantsAndroid = targets.includes('android');
|
|
25
25
|
const wantsIos = targets.includes('ios');
|
|
26
|
-
const wantsDesktop = targets.some((target) => target !== 'android' && target !== 'ios');
|
|
26
|
+
const wantsDesktop = targets.some((target) => target !== 'android' && target !== 'ios' && target !== 'native-linux-x64');
|
|
27
27
|
if (wantsDesktop)
|
|
28
28
|
await bootstrapElectron();
|
|
29
|
+
if (targets.includes('native-linux-x64')) {
|
|
30
|
+
/* A machine is ready for it as it is: the artifact carries the Node running the build, and the
|
|
31
|
+
host is a dependency of the game, which `npm install` there fetches. */
|
|
32
|
+
console.log('ok: native-linux-x64 needs nothing fetched for this machine — it ships the Node running the ' +
|
|
33
|
+
"build — and the game needs @driftengine/native-host installed, at the packager's version");
|
|
34
|
+
}
|
|
29
35
|
if (targets.some((target) => needsRcodesign(target)))
|
|
30
36
|
await bootstrapRcodesign();
|
|
31
37
|
if (wantsAndroid)
|
package/dist/build.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { buildAndroid } from './android.js';
|
|
6
6
|
import { buildIos } from './ios.js';
|
|
7
7
|
import { crossSignMacApp, zipMacApp } from './macCrossSign.js';
|
|
8
|
+
import { buildNative } from './native.js';
|
|
8
9
|
import { outFor } from './outDir.js';
|
|
9
10
|
import { resourceFile } from './resources.js';
|
|
10
11
|
import { ensureElectronRuntime } from './runtime.js';
|
|
@@ -41,6 +42,9 @@ const HOST_FOR = {
|
|
|
41
42
|
'mac-arm64': 'darwin',
|
|
42
43
|
'mac-x64': 'darwin',
|
|
43
44
|
'linux-x64': 'linux',
|
|
45
|
+
/* The native host ships the Node that runs the build and the binaries installed beside it;
|
|
46
|
+
`native.ts` refuses the machine itself, architecture included. */
|
|
47
|
+
'native-linux-x64': 'linux',
|
|
44
48
|
/* Android's toolchain is Java and it runs anywhere; iOS needs Xcode, which is a Mac. */
|
|
45
49
|
android: null,
|
|
46
50
|
ios: 'darwin',
|
|
@@ -68,16 +72,6 @@ function refuseHost(target, platform) {
|
|
|
68
72
|
'Wine cannot reach a certificate store, which is the one platform where that matters.');
|
|
69
73
|
}
|
|
70
74
|
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
75
|
for (const target of targets) {
|
|
82
76
|
if (target === 'android') {
|
|
83
77
|
await buildAndroid(manifest, cwd, outFor(cwd, target, options), options);
|
|
@@ -87,9 +81,28 @@ export async function build(manifest, cwd, targets, options = {}) {
|
|
|
87
81
|
await buildIos(manifest, cwd, outFor(cwd, target, options), options);
|
|
88
82
|
continue;
|
|
89
83
|
}
|
|
84
|
+
if (target === 'native-linux-x64') {
|
|
85
|
+
await buildNative(manifest, cwd, outFor(cwd, target, options));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
90
88
|
const refusal = refuseHost(target, process.platform);
|
|
91
89
|
if (refusal !== null)
|
|
92
90
|
throw new Error(refusal);
|
|
91
|
+
/*
|
|
92
|
+
* **Electron's builder and version are read here, for an Electron target**, rather than before
|
|
93
|
+
* the loop: a build of the native target alone needs neither.
|
|
94
|
+
*
|
|
95
|
+
* One artifact per target, and the architecture is named rather than inherited: a build on an
|
|
96
|
+
* Apple Silicon machine defaults to arm64, so `mac-x64` asked for on one would quietly produce
|
|
97
|
+
* the wrong slice.
|
|
98
|
+
*/
|
|
99
|
+
const { build: run, Arch, Platform } = await import('electron-builder');
|
|
100
|
+
const targetFor = (name) => name === 'win-x64'
|
|
101
|
+
? Platform.WINDOWS.createTarget(undefined, Arch.x64)
|
|
102
|
+
: name === 'linux-x64'
|
|
103
|
+
? Platform.LINUX.createTarget(undefined, Arch.x64)
|
|
104
|
+
: Platform.MAC.createTarget(undefined, name === 'mac-x64' ? Arch.x64 : Arch.arm64);
|
|
105
|
+
const electronVersion = await installedElectronVersion();
|
|
93
106
|
/*
|
|
94
107
|
* **The runtime, before anything is staged.** `installedElectronVersion` below reads a version
|
|
95
108
|
* out of `electron/package.json`, which is in the npm tarball — so it answers even when the
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
4
4
|
import { writeFile } from 'node:fs/promises';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -11,6 +11,7 @@ import { refuseLyingFilesystem } from './identity.js';
|
|
|
11
11
|
import { init } from './init.js';
|
|
12
12
|
import { outFor } from './outDir.js';
|
|
13
13
|
import { parseManifest } from './manifest.js';
|
|
14
|
+
import { executableName, nativeHost, nativeNotes, packagerVersion } from './native.js';
|
|
14
15
|
import { planSigning } from './signing.js';
|
|
15
16
|
import { electronMajor } from './runtimeVersion.js';
|
|
16
17
|
import { depotScripts } from './steam/depot.js';
|
|
@@ -35,7 +36,7 @@ delete process.env.ELECTRON_RUN_AS_NODE;
|
|
|
35
36
|
/**
|
|
36
37
|
* `drift-package` — the command a consumer runs to get an installable application.
|
|
37
38
|
*
|
|
38
|
-
*
|
|
39
|
+
* Seven subcommands, and `verify` is the one that is not obvious. An artifact that was never
|
|
39
40
|
* rebuilt is byte-identical in every respect a build log can see, so the only honest check is to
|
|
40
41
|
* look inside the shipped thing for a string from the change. This repository already learned that
|
|
41
42
|
* about deploys: an asset hash proves nothing.
|
|
@@ -121,6 +122,22 @@ async function doctor(cwd, options = {}) {
|
|
|
121
122
|
* arrive twenty minutes later from Gradle.
|
|
122
123
|
*/
|
|
123
124
|
for (const target of manifest.targets) {
|
|
125
|
+
/*
|
|
126
|
+
* **The native target asks nothing of the machine and one thing of the game**: it ships the
|
|
127
|
+
* Node running this, and it needs the host installed beside the game, which `bootstrap` cannot
|
|
128
|
+
* do because it prepares a machine rather than a project.
|
|
129
|
+
*/
|
|
130
|
+
if (target === 'native-linux-x64') {
|
|
131
|
+
for (const note of nativeNotes(manifest))
|
|
132
|
+
console.log(` ${target}: ${note}`);
|
|
133
|
+
try {
|
|
134
|
+
nativeHost(cwd, packagerVersion());
|
|
135
|
+
}
|
|
136
|
+
catch (cause) {
|
|
137
|
+
console.log(` ${target}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
124
141
|
const ready = await toolchainReady(target);
|
|
125
142
|
if (ready)
|
|
126
143
|
continue;
|
|
@@ -129,10 +146,28 @@ async function doctor(cwd, options = {}) {
|
|
|
129
146
|
}
|
|
130
147
|
return manifest;
|
|
131
148
|
}
|
|
132
|
-
|
|
149
|
+
/**
|
|
150
|
+
* **The native target is looked at in its own bundle**, because the build makes that from the game's
|
|
151
|
+
* source rather than copying the web build: the web build inside it, `public/`, and the copied
|
|
152
|
+
* modules are left out, so a fresh web build cannot vouch for a stale native one.
|
|
153
|
+
*/
|
|
154
|
+
function verify(cwd, needle, target, options) {
|
|
133
155
|
const manifest = loadManifest(cwd);
|
|
134
|
-
|
|
135
|
-
|
|
156
|
+
let root = join(cwd, dirname(manifest.entry));
|
|
157
|
+
let files;
|
|
158
|
+
if (target === 'native-linux-x64') {
|
|
159
|
+
root = join(outFor(cwd, target, options), executableName(manifest), 'app');
|
|
160
|
+
if (!existsSync(root)) {
|
|
161
|
+
throw new Error(`verify: there is no native build at ${root}; \`drift-package build ` +
|
|
162
|
+
'--target=native-linux-x64` makes one.');
|
|
163
|
+
}
|
|
164
|
+
const skip = new Set([join(root, 'public'), join(root, 'node_modules')]);
|
|
165
|
+
files = walk(root).filter((file) => ![...skip].some((dir) => file.startsWith(`${dir}/`)));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
files = walk(root);
|
|
169
|
+
}
|
|
170
|
+
const found = files.some((file) => readFileSync(file, 'utf8').includes(needle));
|
|
136
171
|
if (!found) {
|
|
137
172
|
throw new Error(`verify: "${needle}" is in no file under ${root}. That is what a build that did not rerun ` +
|
|
138
173
|
'looks like — the artifact exists and is stale.');
|
|
@@ -223,7 +258,13 @@ async function writeSteamScripts(cwd, args, options) {
|
|
|
223
258
|
const depotId = depotArg === undefined ? manifest.steam.appId + 1 : Number.parseInt(depotArg.slice(8), 10);
|
|
224
259
|
const branchArg = args.find((entry) => entry.startsWith('--branch='));
|
|
225
260
|
const outDir = outFor(cwd, target, options);
|
|
226
|
-
const content = target === '
|
|
261
|
+
const content = target === 'native-linux-x64'
|
|
262
|
+
? executableName(manifest)
|
|
263
|
+
: target === 'linux-x64'
|
|
264
|
+
? 'linux-unpacked'
|
|
265
|
+
: target === 'win-x64'
|
|
266
|
+
? 'win-unpacked'
|
|
267
|
+
: 'mac';
|
|
227
268
|
const scripts = depotScripts({
|
|
228
269
|
appId: manifest.steam.appId,
|
|
229
270
|
depotId,
|
|
@@ -305,7 +346,9 @@ try {
|
|
|
305
346
|
const arg = rest.find((entry) => entry.startsWith('--contains='));
|
|
306
347
|
if (arg === undefined)
|
|
307
348
|
throw new Error('verify needs --contains=<string>');
|
|
308
|
-
|
|
349
|
+
const asked = rest.find((entry) => entry.startsWith('--target='));
|
|
350
|
+
const target = asked === undefined ? null : asked.slice('--target='.length);
|
|
351
|
+
verify(cwd, arg.slice('--contains='.length), target, resources);
|
|
309
352
|
}
|
|
310
353
|
else if (command === 'run') {
|
|
311
354
|
process.exitCode = await run(cwd, resources);
|
package/dist/flags.js
CHANGED
|
@@ -25,6 +25,22 @@ export function planFlags(manifest, target, electronMajor = null) {
|
|
|
25
25
|
*/
|
|
26
26
|
if (target === 'android' || target === 'ios')
|
|
27
27
|
return { switches: [], refusal: null, notes: [] };
|
|
28
|
+
/*
|
|
29
|
+
* **The native host has no Chromium either, and no WebGL2 beneath its WebGPU**: Dawn is its whole
|
|
30
|
+
* renderer. So a switch is nothing there, and a manifest that turns WebGPU off asks for the one
|
|
31
|
+
* backend that target does not have — refused, rather than shipped as a window that cannot draw.
|
|
32
|
+
*/
|
|
33
|
+
if (target === 'native-linux-x64') {
|
|
34
|
+
return {
|
|
35
|
+
switches: [],
|
|
36
|
+
notes: [],
|
|
37
|
+
refusal: manifest.backend.webgpu === 'off'
|
|
38
|
+
? "native-linux-x64 draws with WebGPU alone — Dawn is the host's renderer and there is " +
|
|
39
|
+
'no WebGL2 beneath it — so backend.webgpu "off" cannot be honoured there. Set it to ' +
|
|
40
|
+
'"prefer" or "require", or build a desktop target for WebGL2.'
|
|
41
|
+
: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
28
44
|
const switches = [];
|
|
29
45
|
const notes = [];
|
|
30
46
|
const wantsWebGpu = manifest.backend.webgpu !== 'off';
|
package/dist/main/ipc.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BrowserWindow, app, dialog, ipcMain, screen } from 'electron';
|
|
2
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
import { SteamCloudStore } from '../steam/cloudStore.js';
|
|
5
|
+
import { readStoreFile, writeStoreFile, writeStoreFileSync } from './storeFile.js';
|
|
6
6
|
/**
|
|
7
7
|
* The main-process half of the bridge: a file-backed store, dialogs, focus, and quit.
|
|
8
8
|
*
|
|
@@ -60,22 +60,8 @@ function cloudSnapshot(steam) {
|
|
|
60
60
|
return out;
|
|
61
61
|
}
|
|
62
62
|
function readLocalSnapshot() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (typeof parsed !== 'object' || parsed === null)
|
|
66
|
-
return {};
|
|
67
|
-
const out = {};
|
|
68
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
69
|
-
if (typeof value === 'string')
|
|
70
|
-
out[key] = value;
|
|
71
|
-
}
|
|
72
|
-
return out;
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
/* No file yet, or an unreadable one. A fresh store is the right answer to both: a
|
|
76
|
-
preference is never worth failing a boot for. */
|
|
77
|
-
return {};
|
|
78
|
-
}
|
|
63
|
+
/* No file yet, or an unreadable one, reads as a fresh store: see `storeFile.ts`. */
|
|
64
|
+
return readStoreFile(storeFile());
|
|
79
65
|
}
|
|
80
66
|
export function installIpc(snapshot, windowOf, steam = null) {
|
|
81
67
|
const entries = new Map(Object.entries(snapshot));
|
|
@@ -94,7 +80,7 @@ export function installIpc(snapshot, windowOf, steam = null) {
|
|
|
94
80
|
cloud.write(key, value);
|
|
95
81
|
return;
|
|
96
82
|
}
|
|
97
|
-
void
|
|
83
|
+
void writeStoreFile(storeFile(), entries).catch((cause) => console.error('[driftengine] could not write the store:', cause));
|
|
98
84
|
};
|
|
99
85
|
const schedule = () => {
|
|
100
86
|
if (pending === null)
|
|
@@ -246,7 +232,7 @@ export function installIpc(snapshot, windowOf, steam = null) {
|
|
|
246
232
|
cloud.write(key, value);
|
|
247
233
|
return;
|
|
248
234
|
}
|
|
249
|
-
|
|
235
|
+
writeStoreFileSync(storeFile(), entries);
|
|
250
236
|
}
|
|
251
237
|
catch (cause) {
|
|
252
238
|
console.error('[driftengine] could not write the store on the way out:', cause);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A shell's key-value store as one JSON file, replaced whole so that nothing ever sees half of it.
|
|
3
|
+
*
|
|
4
|
+
* **Written beside the store and renamed over it.** A rename replaces a file in one step, so a
|
|
5
|
+
* reader — or the next boot, after a crash at any instant — finds the previous store or the next
|
|
6
|
+
* one and never a mixture. Written in place, as this was until 2026-09-19, the file is empty from
|
|
7
|
+
* the moment it is opened until the write lands: a test reading it during rewrites found it torn on
|
|
8
|
+
* 72 reads of 119, and a store that does not parse is read as an empty one, so a crash then costs
|
|
9
|
+
* the player every setting they ever made, with nothing said. **Synced before the rename**, so the
|
|
10
|
+
* replacement carries its bytes to the disk before the name moves: without it a power cut can
|
|
11
|
+
* leave the new name on a file whose contents never arrived.
|
|
12
|
+
*
|
|
13
|
+
* **Asynchronous writes land in the order they were asked for.** Each waits for the one before it,
|
|
14
|
+
* so a large early write finishing after a small late one cannot put the older store back.
|
|
15
|
+
*
|
|
16
|
+
* What it gives up: the directory is not synced, so a power cut straight after a rename may bring
|
|
17
|
+
* back the previous store rather than the new one — a lost second of settings, not a lost store.
|
|
18
|
+
* Both shells use this: the desktop shell's main process and the native host.
|
|
19
|
+
*/
|
|
20
|
+
type Entries = Iterable<readonly [string, string]>;
|
|
21
|
+
/**
|
|
22
|
+
* The store, or an empty one when there is no file or the file is not a store. A value that is not a
|
|
23
|
+
* string is dropped. A preference is never worth failing a boot for.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readStoreFile(path: string): Record<string, string>;
|
|
26
|
+
/** Replace the store now, for the way out, where there is no later to wait for. */
|
|
27
|
+
export declare function writeStoreFileSync(path: string, entries: Entries): void;
|
|
28
|
+
/** Replace the store after every write already asked for, with `entries` as they are now. */
|
|
29
|
+
export declare function writeStoreFile(path: string, entries: Entries): Promise<void>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A shell's key-value store as one JSON file, replaced whole so that nothing ever sees half of it.
|
|
3
|
+
*
|
|
4
|
+
* **Written beside the store and renamed over it.** A rename replaces a file in one step, so a
|
|
5
|
+
* reader — or the next boot, after a crash at any instant — finds the previous store or the next
|
|
6
|
+
* one and never a mixture. Written in place, as this was until 2026-09-19, the file is empty from
|
|
7
|
+
* the moment it is opened until the write lands: a test reading it during rewrites found it torn on
|
|
8
|
+
* 72 reads of 119, and a store that does not parse is read as an empty one, so a crash then costs
|
|
9
|
+
* the player every setting they ever made, with nothing said. **Synced before the rename**, so the
|
|
10
|
+
* replacement carries its bytes to the disk before the name moves: without it a power cut can
|
|
11
|
+
* leave the new name on a file whose contents never arrived.
|
|
12
|
+
*
|
|
13
|
+
* **Asynchronous writes land in the order they were asked for.** Each waits for the one before it,
|
|
14
|
+
* so a large early write finishing after a small late one cannot put the older store back.
|
|
15
|
+
*
|
|
16
|
+
* What it gives up: the directory is not synced, so a power cut straight after a rename may bring
|
|
17
|
+
* back the previous store rather than the new one — a lost second of settings, not a lost store.
|
|
18
|
+
* Both shells use this: the desktop shell's main process and the native host.
|
|
19
|
+
*/
|
|
20
|
+
import { closeSync, fsyncSync, openSync, readFileSync, renameSync, rmSync, writeSync, } from 'node:fs';
|
|
21
|
+
import { open, rename, rm } from 'node:fs/promises';
|
|
22
|
+
/**
|
|
23
|
+
* The store, or an empty one when there is no file or the file is not a store. A value that is not a
|
|
24
|
+
* string is dropped. A preference is never worth failing a boot for.
|
|
25
|
+
*/
|
|
26
|
+
export function readStoreFile(path) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
29
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
30
|
+
return {};
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
33
|
+
if (typeof value === 'string')
|
|
34
|
+
out[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let temporaries = 0;
|
|
43
|
+
/** One name per write, so two writes in flight never share a half-written file. */
|
|
44
|
+
function temporaryFor(path) {
|
|
45
|
+
temporaries += 1;
|
|
46
|
+
return `${path}.${process.pid}.${temporaries}.tmp`;
|
|
47
|
+
}
|
|
48
|
+
function textOf(entries) {
|
|
49
|
+
return JSON.stringify(Object.fromEntries(entries));
|
|
50
|
+
}
|
|
51
|
+
/** Replace the store now, for the way out, where there is no later to wait for. */
|
|
52
|
+
export function writeStoreFileSync(path, entries) {
|
|
53
|
+
const temporary = temporaryFor(path);
|
|
54
|
+
try {
|
|
55
|
+
const file = openSync(temporary, 'w');
|
|
56
|
+
try {
|
|
57
|
+
writeSync(file, textOf(entries));
|
|
58
|
+
fsyncSync(file);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
closeSync(file);
|
|
62
|
+
}
|
|
63
|
+
renameSync(temporary, path);
|
|
64
|
+
}
|
|
65
|
+
catch (cause) {
|
|
66
|
+
rmSync(temporary, { force: true });
|
|
67
|
+
throw cause;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const queues = new Map();
|
|
71
|
+
/** Replace the store after every write already asked for, with `entries` as they are now. */
|
|
72
|
+
export function writeStoreFile(path, entries) {
|
|
73
|
+
const text = textOf(entries);
|
|
74
|
+
const before = queues.get(path) ?? Promise.resolve();
|
|
75
|
+
const next = before
|
|
76
|
+
.catch(() => undefined)
|
|
77
|
+
.then(async () => {
|
|
78
|
+
const temporary = temporaryFor(path);
|
|
79
|
+
try {
|
|
80
|
+
const file = await open(temporary, 'w');
|
|
81
|
+
try {
|
|
82
|
+
await file.writeFile(text, 'utf8');
|
|
83
|
+
await file.sync();
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await file.close();
|
|
87
|
+
}
|
|
88
|
+
await rename(temporary, path);
|
|
89
|
+
}
|
|
90
|
+
catch (cause) {
|
|
91
|
+
await rm(temporary, { force: true });
|
|
92
|
+
throw cause;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
queues.set(path, next);
|
|
96
|
+
/* Forgotten once it is the last, so the map holds nothing for a store no longer written. */
|
|
97
|
+
void next
|
|
98
|
+
.catch(() => undefined)
|
|
99
|
+
.then(() => {
|
|
100
|
+
if (queues.get(path) === next)
|
|
101
|
+
queues.delete(path);
|
|
102
|
+
});
|
|
103
|
+
return next;
|
|
104
|
+
}
|
package/dist/main/window.js
CHANGED
|
@@ -151,7 +151,10 @@ export async function startApp(manifest, root, target, options = { development:
|
|
|
151
151
|
window.once('ready-to-show', () => window?.show());
|
|
152
152
|
}
|
|
153
153
|
else {
|
|
154
|
-
|
|
154
|
+
/* Monotonic. Every reading here is subtracted from another and compared against a minimum,
|
|
155
|
+
and a wall clock that steps backwards holds the badge forever while one that steps forwards
|
|
156
|
+
flashes it and shows a white window. */
|
|
157
|
+
const startedAt = performance.now();
|
|
155
158
|
let painted = false;
|
|
156
159
|
let badgeShownAt = null;
|
|
157
160
|
window.once('ready-to-show', () => {
|
|
@@ -163,7 +166,7 @@ export async function startApp(manifest, root, target, options = { development:
|
|
|
163
166
|
* the time it is meant to be readable for — so it is counted from here.
|
|
164
167
|
*/
|
|
165
168
|
splash.once('show', () => {
|
|
166
|
-
badgeShownAt ??=
|
|
169
|
+
badgeShownAt ??= performance.now();
|
|
167
170
|
});
|
|
168
171
|
/*
|
|
169
172
|
* And a badge that cannot load is not worth holding a game for: counting its minimum from
|
|
@@ -174,7 +177,7 @@ export async function startApp(manifest, root, target, options = { development:
|
|
|
174
177
|
badgeShownAt ??= startedAt;
|
|
175
178
|
});
|
|
176
179
|
const poll = setInterval(() => {
|
|
177
|
-
const now =
|
|
180
|
+
const now = performance.now();
|
|
178
181
|
const sinceBadge = badgeShownAt === null ? null : now - badgeShownAt;
|
|
179
182
|
if (splashDecision(now - startedAt, sinceBadge, manifest.splash.minMs, painted) === 'hold')
|
|
180
183
|
return;
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type WebGpuPolicy = 'prefer' | 'require' | 'off';
|
|
2
|
-
export type Target = 'win-x64' | 'mac-arm64' | 'mac-x64' | 'linux-x64' | 'android' | 'ios';
|
|
2
|
+
export type Target = 'win-x64' | 'mac-arm64' | 'mac-x64' | 'linux-x64' | 'native-linux-x64' | 'android' | 'ios';
|
|
3
3
|
/** The two that are a phone rather than a desktop, and differ in almost every way that matters. */
|
|
4
4
|
export declare const MOBILE_TARGETS: readonly Target[];
|
|
5
5
|
export interface PackageManifest {
|
|
@@ -21,6 +21,13 @@ export interface PackageManifest {
|
|
|
21
21
|
readonly gamepad: boolean;
|
|
22
22
|
};
|
|
23
23
|
readonly targets: readonly Target[];
|
|
24
|
+
/**
|
|
25
|
+
* The module a native target mounts: it exports `mount(canvas)`, which the native host calls with
|
|
26
|
+
* its canvas, since there is no page there to find one in. Null unless a native target is built.
|
|
27
|
+
*/
|
|
28
|
+
readonly native: {
|
|
29
|
+
readonly entry: string;
|
|
30
|
+
} | null;
|
|
24
31
|
readonly steam: {
|
|
25
32
|
readonly appId: number | null;
|
|
26
33
|
};
|
package/dist/manifest.js
CHANGED
|
@@ -19,6 +19,7 @@ const TARGETS = [
|
|
|
19
19
|
'mac-arm64',
|
|
20
20
|
'mac-x64',
|
|
21
21
|
'linux-x64',
|
|
22
|
+
'native-linux-x64',
|
|
22
23
|
'android',
|
|
23
24
|
'ios',
|
|
24
25
|
];
|
|
@@ -63,6 +64,12 @@ export function parseManifest(raw) {
|
|
|
63
64
|
throw new Error(`drift.package.json: unknown target "${String(target)}" — expected one of ${TARGETS.join(', ')}`);
|
|
64
65
|
}
|
|
65
66
|
}
|
|
67
|
+
const nativeRaw = (source.native ?? {});
|
|
68
|
+
const nativeEntry = typeof nativeRaw.entry === 'string' && nativeRaw.entry.length > 0 ? nativeRaw.entry : null;
|
|
69
|
+
if (targetsRaw.includes('native-linux-x64') && nativeEntry === null) {
|
|
70
|
+
throw new Error('drift.package.json: the native-linux-x64 target needs "native.entry", the module whose ' +
|
|
71
|
+
'`mount(canvas)` the native host calls — there is no page there to find a canvas in');
|
|
72
|
+
}
|
|
66
73
|
const windowRaw = (source.window ?? {});
|
|
67
74
|
const featuresRaw = (source.features ?? {});
|
|
68
75
|
const steamRaw = (source.steam ?? {});
|
|
@@ -101,6 +108,7 @@ export function parseManifest(raw) {
|
|
|
101
108
|
gamepad: featuresRaw.gamepad !== false,
|
|
102
109
|
},
|
|
103
110
|
targets: targetsRaw,
|
|
111
|
+
native: nativeEntry === null ? null : { entry: nativeEntry },
|
|
104
112
|
steam: { appId: typeof appId === 'number' ? appId : null },
|
|
105
113
|
publisher: typeof source.publisher === 'string' && source.publisher.length > 0
|
|
106
114
|
? source.publisher
|