@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,22 @@
|
|
|
1
|
+
import type { SteamCloud } from './cloudStore.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Steam, opened in the only process that can hold it.
|
|
4
|
+
*
|
|
5
|
+
* **`steamworks.js` is a native Node module and the renderer has no Node.** It runs sandboxed with
|
|
6
|
+
* `contextIsolation` on, which is what keeps a game — and any third-party code, mod or shader it
|
|
7
|
+
* loads — away from the filesystem. So the SDK lives here, in the main process, and reaches a game
|
|
8
|
+
* through the same audited bridge as every other capability.
|
|
9
|
+
*
|
|
10
|
+
* **Every failure is `null`, and there are three ordinary ones**: the manifest named no app, the
|
|
11
|
+
* module is not installed, or the copy was launched outside Steam. A game must run in all three —
|
|
12
|
+
* that last one is every run during development — so none of them throws.
|
|
13
|
+
*
|
|
14
|
+
* `steamworks.js` rather than `greenworks`, whose own README describes itself as maintained on a
|
|
15
|
+
* best-effort basis. A store integration on a release's critical path is the wrong place for that.
|
|
16
|
+
*/
|
|
17
|
+
export interface SteamRuntime {
|
|
18
|
+
readonly cloud: SteamCloud;
|
|
19
|
+
unlockAchievement(id: string): void;
|
|
20
|
+
setRichPresence(text: string): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function openSteam(appId: number | null, appRoot: string): SteamRuntime | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function openSteam(appId, appRoot) {
|
|
4
|
+
if (appId === null)
|
|
5
|
+
return null;
|
|
6
|
+
let client;
|
|
7
|
+
try {
|
|
8
|
+
/*
|
|
9
|
+
* Resolved from the packaged application's own `node_modules`, which is where `stage.ts` puts
|
|
10
|
+
* it — not from this file's location, which at run time is inside a bundle.
|
|
11
|
+
*/
|
|
12
|
+
const require = createRequire(join(appRoot, 'package.json'));
|
|
13
|
+
const steamworks = require('steamworks.js');
|
|
14
|
+
client = steamworks.init(appId);
|
|
15
|
+
}
|
|
16
|
+
catch (cause) {
|
|
17
|
+
/* Named rather than silent: a developer who expected achievements and got none should be able
|
|
18
|
+
to see which of the three ordinary reasons it was. */
|
|
19
|
+
console.log(`[driftengine] Steam is not available: ${String(cause)}`);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
cloud: client.cloud,
|
|
24
|
+
unlockAchievement: (id) => {
|
|
25
|
+
try {
|
|
26
|
+
client.achievement.activate(id);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* An achievement that fails to unlock is not a reason for a frame to stop. */
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
setRichPresence: (text) => {
|
|
33
|
+
try {
|
|
34
|
+
client.localplayer.setRichPresence('steam_display', text);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* As above. */
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PackageManifest } from '../manifest.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The PowerShell a consumer needs on the machine that builds their Windows artifacts.
|
|
4
|
+
*
|
|
5
|
+
* **This is a template and not a shipped script because it has to be a file in their repository**:
|
|
6
|
+
* something has to start Node on that machine, and that something cannot live inside a Node
|
|
7
|
+
* program. Everything in it that could have lived in `drift-package` already does — see
|
|
8
|
+
* `runtime.ts` and `identity.ts`.
|
|
9
|
+
*
|
|
10
|
+
* **Pure ASCII, and `scripts/packaging.test.mjs` asserts it.** Windows PowerShell 5.1, which is
|
|
11
|
+
* what a fresh Windows machine runs, reads a BOM-less `.ps1` in the system ANSI codepage rather
|
|
12
|
+
* than as UTF-8, so one typographic character in a comment arrives as mojibake and takes the
|
|
13
|
+
* *parser* with it — four errors deep, pointing at lines whose content is fine, and invisible from
|
|
14
|
+
* Linux.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildWindowsScript(manifest: PackageManifest): string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PowerShell a consumer needs on the machine that builds their Windows artifacts.
|
|
3
|
+
*
|
|
4
|
+
* **This is a template and not a shipped script because it has to be a file in their repository**:
|
|
5
|
+
* something has to start Node on that machine, and that something cannot live inside a Node
|
|
6
|
+
* program. Everything in it that could have lived in `drift-package` already does — see
|
|
7
|
+
* `runtime.ts` and `identity.ts`.
|
|
8
|
+
*
|
|
9
|
+
* **Pure ASCII, and `scripts/packaging.test.mjs` asserts it.** Windows PowerShell 5.1, which is
|
|
10
|
+
* what a fresh Windows machine runs, reads a BOM-less `.ps1` in the system ANSI codepage rather
|
|
11
|
+
* than as UTF-8, so one typographic character in a comment arrives as mojibake and takes the
|
|
12
|
+
* *parser* with it — four errors deep, pointing at lines whose content is fine, and invisible from
|
|
13
|
+
* Linux.
|
|
14
|
+
*/
|
|
15
|
+
export function buildWindowsScript(manifest) {
|
|
16
|
+
const name = manifest.name;
|
|
17
|
+
const local = `${manifest.id.split('.').pop() ?? 'drift'}-build`;
|
|
18
|
+
return `# Build the Windows artifacts for ${name}, on Windows, because they are not cross-built.
|
|
19
|
+
#
|
|
20
|
+
# KEEP THIS FILE PURE ASCII. Windows PowerShell 5.1 -- what a fresh Windows machine runs -- reads a
|
|
21
|
+
# .ps1 with no byte-order mark in the system ANSI codepage, not as UTF-8. A single typographic
|
|
22
|
+
# character in a comment arrives as mojibake and takes the parser with it, four errors deep,
|
|
23
|
+
# pointing at lines whose real content is fine. Nothing on Linux would ever show it.
|
|
24
|
+
#
|
|
25
|
+
# Wine can produce an NSIS installer from Linux and cannot reach a Windows certificate store, so
|
|
26
|
+
# the one platform whose warnings are hardest to shake would be the one built with no way to sign
|
|
27
|
+
# it properly.
|
|
28
|
+
#
|
|
29
|
+
# .\\scripts\\build-windows.ps1
|
|
30
|
+
# $env:WIN_CSC_LINK = 'C:\\certs\\code.pfx'; $env:WIN_CSC_KEY_PASSWORD = '...' # when there is one
|
|
31
|
+
#
|
|
32
|
+
# With no certificate the build succeeds and is unsigned: SmartScreen warns on first launch.
|
|
33
|
+
#
|
|
34
|
+
# Written by \`drift-package init\`. Edit it and it is yours; init will not overwrite it without
|
|
35
|
+
# --force.
|
|
36
|
+
|
|
37
|
+
$ErrorActionPreference = 'Stop'
|
|
38
|
+
$ProjectDir = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
|
39
|
+
$SourceDir = $ProjectDir
|
|
40
|
+
|
|
41
|
+
function Require-Command($name, $hint) {
|
|
42
|
+
if ($null -eq (Get-Command $name -ErrorAction SilentlyContinue)) {
|
|
43
|
+
throw "$name is not on PATH. $hint"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Require-Command node 'Install Node 22.12 or newer from https://nodejs.org'
|
|
48
|
+
Require-Command npm 'It ships with Node'
|
|
49
|
+
|
|
50
|
+
$version = (node --version).TrimStart('v')
|
|
51
|
+
if ([version]($version -split '-')[0] -lt [version]'22.12.0') {
|
|
52
|
+
throw "Node $version is too old; this project needs 22.12.0 or newer"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (-not (Test-Path (Join-Path $ProjectDir 'drift.package.json'))) {
|
|
56
|
+
throw "No drift.package.json in $ProjectDir. That file is what a build is configured from."
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ELECTRON_RUN_AS_NODE turns Electron into plain Node, and an Electron-based terminal sets it for
|
|
60
|
+
# everything it spawns. Inherited, the build produces an artifact that dies on 'Cannot find module
|
|
61
|
+
# electron' with a Node banner, which looks nothing like its cause.
|
|
62
|
+
Remove-Item Env:\\ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue
|
|
63
|
+
|
|
64
|
+
# A native command's exit code does not trip $ErrorActionPreference.
|
|
65
|
+
#
|
|
66
|
+
# That preference governs PowerShell cmdlets. npm is an external program: a non-zero exit sets
|
|
67
|
+
# $LASTEXITCODE and otherwise does nothing. So a failing step could print a real, useful error, the
|
|
68
|
+
# script would carry straight on, and the only thing left to read was a complaint about a directory
|
|
69
|
+
# that was never going to exist. PowerShell 7.3 has $PSNativeCommandUseErrorActionPreference and
|
|
70
|
+
# 5.1 does not, so it is checked by hand and each step says which step it was.
|
|
71
|
+
function Invoke-Step($what, [scriptblock] $step) {
|
|
72
|
+
Write-Host "\`n== $what"
|
|
73
|
+
& $step
|
|
74
|
+
if ($LASTEXITCODE -ne 0) {
|
|
75
|
+
throw "$what failed with exit code $LASTEXITCODE. The reason is in the output above this line."
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# A build on a mapped or network drive relocates itself to a local disk first.
|
|
80
|
+
#
|
|
81
|
+
# Node's fs.cp refuses to copy a thing into itself and decides that by comparing dev and ino of the
|
|
82
|
+
# destination's parents against the source. A redirector that gets no file index from the server
|
|
83
|
+
# invents one, and on Windows dev is already the volume serial number -- so a file and a directory
|
|
84
|
+
# come back identical and the first staging copy fails, talking about subdirectories of itself.
|
|
85
|
+
# \`drift-package doctor\` refuses that up front and names --out=<dir>; this relocates instead,
|
|
86
|
+
# because the artifacts have to end up back in the checkout either way.
|
|
87
|
+
#
|
|
88
|
+
# node_modules is deliberately not copied: it is reinstalled locally, which is also what stops a
|
|
89
|
+
# Windows npm ci from overwriting a Linux checkout's tree when the share is one in use.
|
|
90
|
+
$root = [System.IO.Path]::GetPathRoot($ProjectDir)
|
|
91
|
+
$driveType = $null
|
|
92
|
+
try {
|
|
93
|
+
$driveType = (New-Object System.IO.DriveInfo($root)).DriveType
|
|
94
|
+
} catch {
|
|
95
|
+
# An unusual root is not worth failing a build over; treat it as fixed and let the build speak.
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if ($driveType -ne $null -and $driveType -ne 'Fixed') {
|
|
99
|
+
$local = Join-Path $env:LOCALAPPDATA '${local}'
|
|
100
|
+
Write-Warning ("$ProjectDir is on a $driveType drive, which cannot hold this build. Copying to " +
|
|
101
|
+
"$local and building there instead.")
|
|
102
|
+
Write-Host ''
|
|
103
|
+
Write-Host "== robocopy to $local"
|
|
104
|
+
robocopy $ProjectDir $local /E /XD node_modules out dist .git /NFL /NDL /NJH /NJS /NP
|
|
105
|
+
# robocopy's exit code is a bitmask, not a status: under 8 means it did its job, and 1 is "files
|
|
106
|
+
# were copied", which is the ordinary success and reads as failure to every other tool.
|
|
107
|
+
if ($LASTEXITCODE -ge 8) {
|
|
108
|
+
throw "robocopy failed with exit code $LASTEXITCODE while copying $ProjectDir to $local."
|
|
109
|
+
}
|
|
110
|
+
$global:LASTEXITCODE = 0
|
|
111
|
+
$ProjectDir = $local
|
|
112
|
+
Write-Host "Building from $ProjectDir"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Push-Location $ProjectDir
|
|
116
|
+
try {
|
|
117
|
+
Invoke-Step 'npm ci' { npm ci }
|
|
118
|
+
Invoke-Step 'drift-package doctor' { node scripts/package.mjs doctor }
|
|
119
|
+
Invoke-Step 'drift-package build' { node scripts/package.mjs build --target=win-x64 }
|
|
120
|
+
|
|
121
|
+
# Built, said it succeeded, and produced nothing: worth its own sentence rather than an ENOENT
|
|
122
|
+
# from Get-ChildItem, which is what this looks like otherwise.
|
|
123
|
+
$out = Join-Path $ProjectDir 'out\\win-x64'
|
|
124
|
+
if (-not (Test-Path $out)) {
|
|
125
|
+
throw ('The build reported success and ' + $out + ' does not exist. Nothing was packaged. ' +
|
|
126
|
+
'Run the build step on its own and read what it prints.')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# The artifacts go back to the checkout this was run from.
|
|
130
|
+
#
|
|
131
|
+
# Relocating the build makes this necessary, and it is easy to miss: the build succeeds, says so,
|
|
132
|
+
# lists its files, and out\\ in the project is empty because the build happened somewhere else.
|
|
133
|
+
# robocopy and not fs.cp: this writes back onto the share, and fs.cp is exactly what will not
|
|
134
|
+
# cross it.
|
|
135
|
+
if ($SourceDir -ne $ProjectDir) {
|
|
136
|
+
$back = Join-Path $SourceDir 'out\\win-x64'
|
|
137
|
+
Write-Host ''
|
|
138
|
+
Write-Host "== copying the artifacts back to $back"
|
|
139
|
+
robocopy $out $back /E /NFL /NDL /NJH /NJS /NP
|
|
140
|
+
if ($LASTEXITCODE -ge 8) {
|
|
141
|
+
throw ("The build succeeded but copying the artifacts from $out back to $back failed with " +
|
|
142
|
+
"robocopy exit code $LASTEXITCODE. They are still in $out -- nothing is lost.")
|
|
143
|
+
}
|
|
144
|
+
$global:LASTEXITCODE = 0
|
|
145
|
+
$out = $back
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Write-Host "\`nArtifacts:"
|
|
149
|
+
Get-ChildItem -Path $out -File | ForEach-Object { $_.FullName }
|
|
150
|
+
|
|
151
|
+
if (-not ($env:WIN_CSC_LINK -or $env:CSC_LINK)) {
|
|
152
|
+
Write-Host ""
|
|
153
|
+
Write-Host "This build is unsigned. Windows will run it; SmartScreen warns on first launch."
|
|
154
|
+
Write-Host "A Steam depot raises no warning, because the client delivering it is signed."
|
|
155
|
+
Write-Host "An OV certificate in WIN_CSC_LINK removes the warning for direct downloads."
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
Pop-Location
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PackageManifest } from '../manifest.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The Node launcher a consumer puts in their own repository, and why it cannot be a line in
|
|
4
|
+
* `package.json`.
|
|
5
|
+
*
|
|
6
|
+
* Every line below is a failure somebody has already had on a Windows machine, where the person
|
|
7
|
+
* who wrote the script is not the person watching it fail.
|
|
8
|
+
*/
|
|
9
|
+
export declare function packageWrapperScript(manifest: PackageManifest): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node launcher a consumer puts in their own repository, and why it cannot be a line in
|
|
3
|
+
* `package.json`.
|
|
4
|
+
*
|
|
5
|
+
* Every line below is a failure somebody has already had on a Windows machine, where the person
|
|
6
|
+
* who wrote the script is not the person watching it fail.
|
|
7
|
+
*/
|
|
8
|
+
export function packageWrapperScript(manifest) {
|
|
9
|
+
return `#!/usr/bin/env node
|
|
10
|
+
/**
|
|
11
|
+
* \`drift-package\` for ${manifest.name}.
|
|
12
|
+
*
|
|
13
|
+
* node scripts/package.mjs doctor
|
|
14
|
+
* node scripts/package.mjs build --target=win-x64
|
|
15
|
+
*
|
|
16
|
+
* Written by \`drift-package init\`. Edit it and it is yours; init will not overwrite it without
|
|
17
|
+
* --force.
|
|
18
|
+
*
|
|
19
|
+
* **The CLI is launched with this Node, never through npx.** On Windows \`npx\` is \`npx.cmd\`, and a
|
|
20
|
+
* \`.cmd\` is not something CreateProcess knows how to launch: it needs cmd.exe, that is
|
|
21
|
+
* \`shell: true\`, which reopens quoting on a platform where paths contain spaces. Launching the
|
|
22
|
+
* Node that is already running avoids the question, and uses the copy of the tool this project
|
|
23
|
+
* locked rather than whichever one npx decides to find.
|
|
24
|
+
*/
|
|
25
|
+
import { spawnSync } from 'node:child_process';
|
|
26
|
+
import { readFileSync } from 'node:fs';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* Where the packager's executable is.
|
|
36
|
+
*
|
|
37
|
+
* \`@driftengine/package\` declares no \`exports\`, so its \`package.json\` resolves by the old rules and
|
|
38
|
+
* the \`bin\` it names is read from there rather than guessed at. A copy of the engine that was
|
|
39
|
+
* vendored instead of installed will not resolve at all, and the message says which of the two
|
|
40
|
+
* situations you are in instead of a module-not-found.
|
|
41
|
+
*/
|
|
42
|
+
let bin;
|
|
43
|
+
try {
|
|
44
|
+
const manifest = require.resolve('@driftengine/package/package.json');
|
|
45
|
+
const declared = JSON.parse(readFileSync(manifest, 'utf8')).bin;
|
|
46
|
+
const relative = typeof declared === 'string' ? declared : declared?.['drift-package'];
|
|
47
|
+
bin = path.join(path.dirname(manifest), relative);
|
|
48
|
+
} catch {
|
|
49
|
+
console.error(
|
|
50
|
+
'Could not resolve @driftengine/package. If the engine is installed, run npm ci. If it is ' +
|
|
51
|
+
'vendored, point \`bin\` below at the vendored bin/drift-package.mjs and keep the rest of ' +
|
|
52
|
+
'this file as it is: what the rest of it does is report failures npm and Windows would ' +
|
|
53
|
+
'otherwise swallow.',
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = spawnSync(process.execPath, [bin, ...process.argv.slice(2)], {
|
|
59
|
+
cwd: REPO_ROOT,
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* **spawnSync does not throw**, and this is the half that is easy to leave out. If the process
|
|
65
|
+
* never starts it puts the reason in \`result.error\` and leaves \`status\` at null, so a bare
|
|
66
|
+
* \`process.exit(status ?? 1)\` turns "I could not launch anything" into "it failed with 1" — the
|
|
67
|
+
* same exit as a real failure, with nothing above it to read. On a Windows machine that looked like
|
|
68
|
+
* a bare "failed with exit code 1" and cost half a day.
|
|
69
|
+
*/
|
|
70
|
+
if (result.error !== undefined) {
|
|
71
|
+
console.error(\`Could not launch \${process.execPath}: \${String(result.error)}\`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (result.signal !== null && result.signal !== undefined) {
|
|
75
|
+
console.error(\`drift-package was terminated by signal \${result.signal}.\`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
process.exit(result.status ?? 1);
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Target } from './manifest.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Everything a build needs that is not a Node package, and where it goes.
|
|
4
|
+
*
|
|
5
|
+
* **Pinned, downloaded, and kept outside the repository.** A packaging toolchain is gigabytes of
|
|
6
|
+
* Java, an Android SDK and a Gradle distribution; none of it belongs in a git tree, and none of it
|
|
7
|
+
* should be a paragraph in a README telling somebody to install seven things by hand. `bootstrap`
|
|
8
|
+
* fetches exactly these versions into a cache, and a machine that already has a working toolchain
|
|
9
|
+
* is used as it is rather than duplicated.
|
|
10
|
+
*
|
|
11
|
+
* **Versions are pinned rather than tracked.** A build that quietly moved to a new Android build
|
|
12
|
+
* tools release is a build whose output changed for a reason nobody recorded. Moving one of these
|
|
13
|
+
* is a commit with a reason in it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PINS: {
|
|
16
|
+
/** Temurin 21. AGP 8.7 needs 17 or newer; 21 is the current long-term release. */
|
|
17
|
+
readonly jdk: {
|
|
18
|
+
readonly feature: '21';
|
|
19
|
+
readonly label: 'Temurin JDK 21';
|
|
20
|
+
};
|
|
21
|
+
/** Google's command-line tools bundle, which is what installs the rest of the SDK. */
|
|
22
|
+
readonly androidTools: {
|
|
23
|
+
readonly build: '13114758';
|
|
24
|
+
readonly label: 'Android command-line tools';
|
|
25
|
+
};
|
|
26
|
+
/** What an APK is actually compiled against. */
|
|
27
|
+
readonly androidPackages: readonly ['platform-tools', 'platforms;android-35', 'build-tools;35.0.0'];
|
|
28
|
+
/** AGP 8.7.3 requires Gradle 8.9 or newer. */
|
|
29
|
+
readonly gradle: {
|
|
30
|
+
readonly version: '8.9';
|
|
31
|
+
readonly label: 'Gradle 8.9';
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* XcodeGen, which turns a spec into an `.xcodeproj`.
|
|
35
|
+
*
|
|
36
|
+
* A `.xcodeproj` is a generated artefact full of absolute paths and a merge conflict in every
|
|
37
|
+
* pull request. Generating it means the repository holds a readable spec instead — and unlike
|
|
38
|
+
* Xcode itself, this is a single binary that can be downloaded.
|
|
39
|
+
*/
|
|
40
|
+
readonly xcodegen: {
|
|
41
|
+
readonly version: '2.42.0';
|
|
42
|
+
readonly label: 'XcodeGen 2.42.0';
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* `rcodesign`, which is what signs a Mac target built anywhere but a Mac.
|
|
46
|
+
*
|
|
47
|
+
* Apple's `codesign` is macOS only and electron-builder silently skips signing without it, so
|
|
48
|
+
* without this a cross-built `.app` is unsigned and the loader on Apple Silicon refuses it.
|
|
49
|
+
* This is the one implementation of Apple's code signature format that runs on Linux.
|
|
50
|
+
*
|
|
51
|
+
* The Linux asset is linked against musl and is a single static binary, so it does not care
|
|
52
|
+
* which distribution it lands on and installs nothing system-wide.
|
|
53
|
+
*/
|
|
54
|
+
readonly rcodesign: {
|
|
55
|
+
readonly version: '0.29.0';
|
|
56
|
+
readonly label: 'rcodesign 0.29.0 (apple-codesign)';
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Where downloaded tools live.
|
|
61
|
+
*
|
|
62
|
+
* Outside the project on purpose: two games on one machine share one Android SDK, and a `git
|
|
63
|
+
* clean` must not cost half a gigabyte of downloads.
|
|
64
|
+
*/
|
|
65
|
+
export declare function toolchainHome(): string;
|
|
66
|
+
export interface AndroidToolchain {
|
|
67
|
+
readonly javaHome: string;
|
|
68
|
+
readonly sdkRoot: string;
|
|
69
|
+
/** The `gradle` executable to invoke. */
|
|
70
|
+
readonly gradle: string;
|
|
71
|
+
/** Whether these came from the machine rather than from the cache. */
|
|
72
|
+
readonly system: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** The JDK this machine will use, preferring one it already has. */
|
|
75
|
+
export declare function findJavaHome(): Promise<string | null>;
|
|
76
|
+
/** The Android SDK this machine will use, preferring one it already has. */
|
|
77
|
+
export declare function findAndroidSdk(): Promise<string | null>;
|
|
78
|
+
/** The Gradle this machine will use. A system one is taken at whatever version it is. */
|
|
79
|
+
export declare function findGradle(): Promise<string | null>;
|
|
80
|
+
/** XcodeGen, from the cache or from the machine. */
|
|
81
|
+
export declare function findXcodeGen(): Promise<string | null>;
|
|
82
|
+
/**
|
|
83
|
+
* The `rcodesign` this machine will use, from the cache or from the machine.
|
|
84
|
+
*
|
|
85
|
+
* A system one is taken at whatever version it is, the way Gradle and XcodeGen are: this signs
|
|
86
|
+
* with a format Apple has not changed in years, and a person who installed it deliberately has a
|
|
87
|
+
* reason. `cargo install apple-codesign` puts one in `~/.cargo/bin`, which is why that is looked
|
|
88
|
+
* at — it is the route on an architecture the release page has no asset for.
|
|
89
|
+
*/
|
|
90
|
+
export declare function findRcodesign(): Promise<string | null>;
|
|
91
|
+
/**
|
|
92
|
+
* Whether a target signs with `rcodesign` rather than with the host's own signer.
|
|
93
|
+
*
|
|
94
|
+
* The one condition is a Mac target that is not being built on a Mac. Kept here beside the tool
|
|
95
|
+
* it decides about, so `doctor`, `bootstrap` and `build` cannot disagree about when it is needed.
|
|
96
|
+
*/
|
|
97
|
+
export declare function needsRcodesign(target: Target, platform?: NodeJS.Platform): boolean;
|
|
98
|
+
export declare function androidToolchain(): Promise<AndroidToolchain | null>;
|
|
99
|
+
/** Which of the pinned tools a target needs, in the words `doctor` prints. */
|
|
100
|
+
export declare function requirementsFor(target: Target): readonly string[];
|
|
101
|
+
export declare function ensureDir(path: string): Promise<void>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { access, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Everything a build needs that is not a Node package, and where it goes.
|
|
6
|
+
*
|
|
7
|
+
* **Pinned, downloaded, and kept outside the repository.** A packaging toolchain is gigabytes of
|
|
8
|
+
* Java, an Android SDK and a Gradle distribution; none of it belongs in a git tree, and none of it
|
|
9
|
+
* should be a paragraph in a README telling somebody to install seven things by hand. `bootstrap`
|
|
10
|
+
* fetches exactly these versions into a cache, and a machine that already has a working toolchain
|
|
11
|
+
* is used as it is rather than duplicated.
|
|
12
|
+
*
|
|
13
|
+
* **Versions are pinned rather than tracked.** A build that quietly moved to a new Android build
|
|
14
|
+
* tools release is a build whose output changed for a reason nobody recorded. Moving one of these
|
|
15
|
+
* is a commit with a reason in it.
|
|
16
|
+
*/
|
|
17
|
+
export const PINS = {
|
|
18
|
+
/** Temurin 21. AGP 8.7 needs 17 or newer; 21 is the current long-term release. */
|
|
19
|
+
jdk: { feature: '21', label: 'Temurin JDK 21' },
|
|
20
|
+
/** Google's command-line tools bundle, which is what installs the rest of the SDK. */
|
|
21
|
+
androidTools: { build: '13114758', label: 'Android command-line tools' },
|
|
22
|
+
/** What an APK is actually compiled against. */
|
|
23
|
+
androidPackages: ['platform-tools', 'platforms;android-35', 'build-tools;35.0.0'],
|
|
24
|
+
/** AGP 8.7.3 requires Gradle 8.9 or newer. */
|
|
25
|
+
gradle: { version: '8.9', label: 'Gradle 8.9' },
|
|
26
|
+
/**
|
|
27
|
+
* XcodeGen, which turns a spec into an `.xcodeproj`.
|
|
28
|
+
*
|
|
29
|
+
* A `.xcodeproj` is a generated artefact full of absolute paths and a merge conflict in every
|
|
30
|
+
* pull request. Generating it means the repository holds a readable spec instead — and unlike
|
|
31
|
+
* Xcode itself, this is a single binary that can be downloaded.
|
|
32
|
+
*/
|
|
33
|
+
xcodegen: { version: '2.42.0', label: 'XcodeGen 2.42.0' },
|
|
34
|
+
/**
|
|
35
|
+
* `rcodesign`, which is what signs a Mac target built anywhere but a Mac.
|
|
36
|
+
*
|
|
37
|
+
* Apple's `codesign` is macOS only and electron-builder silently skips signing without it, so
|
|
38
|
+
* without this a cross-built `.app` is unsigned and the loader on Apple Silicon refuses it.
|
|
39
|
+
* This is the one implementation of Apple's code signature format that runs on Linux.
|
|
40
|
+
*
|
|
41
|
+
* The Linux asset is linked against musl and is a single static binary, so it does not care
|
|
42
|
+
* which distribution it lands on and installs nothing system-wide.
|
|
43
|
+
*/
|
|
44
|
+
rcodesign: { version: '0.29.0', label: 'rcodesign 0.29.0 (apple-codesign)' },
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Where downloaded tools live.
|
|
48
|
+
*
|
|
49
|
+
* Outside the project on purpose: two games on one machine share one Android SDK, and a `git
|
|
50
|
+
* clean` must not cost half a gigabyte of downloads.
|
|
51
|
+
*/
|
|
52
|
+
export function toolchainHome() {
|
|
53
|
+
const override = process.env.DRIFT_TOOLCHAIN_HOME;
|
|
54
|
+
if (typeof override === 'string' && override.length > 0)
|
|
55
|
+
return override;
|
|
56
|
+
const cache = process.env.XDG_CACHE_HOME;
|
|
57
|
+
const base = typeof cache === 'string' && cache.length > 0 ? cache : join(homedir(), '.cache');
|
|
58
|
+
return join(base, 'driftengine', 'toolchain');
|
|
59
|
+
}
|
|
60
|
+
async function exists(path) {
|
|
61
|
+
try {
|
|
62
|
+
await access(path);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** The JDK this machine will use, preferring one it already has. */
|
|
70
|
+
export async function findJavaHome() {
|
|
71
|
+
const fromEnv = process.env.JAVA_HOME;
|
|
72
|
+
if (typeof fromEnv === 'string' &&
|
|
73
|
+
fromEnv.length > 0 &&
|
|
74
|
+
(await exists(join(fromEnv, 'bin', javac())))) {
|
|
75
|
+
return fromEnv;
|
|
76
|
+
}
|
|
77
|
+
const cached = join(toolchainHome(), 'jdk');
|
|
78
|
+
return (await exists(join(cached, 'bin', javac()))) ? cached : null;
|
|
79
|
+
}
|
|
80
|
+
/** The Android SDK this machine will use, preferring one it already has. */
|
|
81
|
+
export async function findAndroidSdk() {
|
|
82
|
+
for (const name of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
|
|
83
|
+
const root = process.env[name];
|
|
84
|
+
if (typeof root === 'string' &&
|
|
85
|
+
root.length > 0 &&
|
|
86
|
+
(await exists(join(root, 'platform-tools')))) {
|
|
87
|
+
return root;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const cached = join(toolchainHome(), 'android-sdk');
|
|
91
|
+
return (await exists(join(cached, 'platform-tools'))) ? cached : null;
|
|
92
|
+
}
|
|
93
|
+
/** The Gradle this machine will use. A system one is taken at whatever version it is. */
|
|
94
|
+
export async function findGradle() {
|
|
95
|
+
const cached = join(toolchainHome(), `gradle-${PINS.gradle.version}`, 'bin', gradleBinary());
|
|
96
|
+
if (await exists(cached))
|
|
97
|
+
return cached;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/** XcodeGen, from the cache or from the machine. */
|
|
101
|
+
export async function findXcodeGen() {
|
|
102
|
+
const cached = join(toolchainHome(), `xcodegen-${PINS.xcodegen.version}`, 'bin', 'xcodegen');
|
|
103
|
+
if (await exists(cached))
|
|
104
|
+
return cached;
|
|
105
|
+
/* Installed by Homebrew or Mint, which many Macs already have. */
|
|
106
|
+
for (const path of ['/opt/homebrew/bin/xcodegen', '/usr/local/bin/xcodegen']) {
|
|
107
|
+
if (await exists(path))
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The `rcodesign` this machine will use, from the cache or from the machine.
|
|
114
|
+
*
|
|
115
|
+
* A system one is taken at whatever version it is, the way Gradle and XcodeGen are: this signs
|
|
116
|
+
* with a format Apple has not changed in years, and a person who installed it deliberately has a
|
|
117
|
+
* reason. `cargo install apple-codesign` puts one in `~/.cargo/bin`, which is why that is looked
|
|
118
|
+
* at — it is the route on an architecture the release page has no asset for.
|
|
119
|
+
*/
|
|
120
|
+
export async function findRcodesign() {
|
|
121
|
+
const cached = join(toolchainHome(), `rcodesign-${PINS.rcodesign.version}`, 'rcodesign');
|
|
122
|
+
if (await exists(cached))
|
|
123
|
+
return cached;
|
|
124
|
+
const home = process.env.HOME;
|
|
125
|
+
const candidates = [
|
|
126
|
+
'/usr/local/bin/rcodesign',
|
|
127
|
+
'/usr/bin/rcodesign',
|
|
128
|
+
'/opt/homebrew/bin/rcodesign',
|
|
129
|
+
...(typeof home === 'string' && home.length > 0
|
|
130
|
+
? [join(home, '.cargo', 'bin', 'rcodesign')]
|
|
131
|
+
: []),
|
|
132
|
+
];
|
|
133
|
+
for (const path of candidates) {
|
|
134
|
+
if (await exists(path))
|
|
135
|
+
return path;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Whether a target signs with `rcodesign` rather than with the host's own signer.
|
|
141
|
+
*
|
|
142
|
+
* The one condition is a Mac target that is not being built on a Mac. Kept here beside the tool
|
|
143
|
+
* it decides about, so `doctor`, `bootstrap` and `build` cannot disagree about when it is needed.
|
|
144
|
+
*/
|
|
145
|
+
export function needsRcodesign(target, platform = process.platform) {
|
|
146
|
+
return (target === 'mac-arm64' || target === 'mac-x64') && platform !== 'darwin';
|
|
147
|
+
}
|
|
148
|
+
export async function androidToolchain() {
|
|
149
|
+
const javaHome = await findJavaHome();
|
|
150
|
+
const sdkRoot = await findAndroidSdk();
|
|
151
|
+
const gradle = await findGradle();
|
|
152
|
+
if (javaHome === null || sdkRoot === null || gradle === null)
|
|
153
|
+
return null;
|
|
154
|
+
return {
|
|
155
|
+
javaHome,
|
|
156
|
+
sdkRoot,
|
|
157
|
+
gradle,
|
|
158
|
+
system: javaHome === process.env.JAVA_HOME || sdkRoot === process.env.ANDROID_HOME,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** Which of the pinned tools a target needs, in the words `doctor` prints. */
|
|
162
|
+
export function requirementsFor(target) {
|
|
163
|
+
switch (target) {
|
|
164
|
+
case 'android':
|
|
165
|
+
return [PINS.jdk.label, PINS.androidTools.label, ...PINS.androidPackages, PINS.gradle.label];
|
|
166
|
+
case 'ios':
|
|
167
|
+
/* XcodeGen is downloadable; Xcode is not, and pretending otherwise would be the lie this
|
|
168
|
+
file exists to avoid — it is a sign-in and an App Store download, and only on a Mac. */
|
|
169
|
+
return ['macOS', 'Xcode with the iOS SDK', PINS.xcodegen.label];
|
|
170
|
+
default:
|
|
171
|
+
return [
|
|
172
|
+
'the Electron runtime for this platform',
|
|
173
|
+
/* Only off a Mac, where the platform's own `codesign` is missing. */
|
|
174
|
+
...(needsRcodesign(target) ? [PINS.rcodesign.label] : []),
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export async function ensureDir(path) {
|
|
179
|
+
await mkdir(path, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
function javac() {
|
|
182
|
+
return process.platform === 'win32' ? 'javac.exe' : 'javac';
|
|
183
|
+
}
|
|
184
|
+
function gradleBinary() {
|
|
185
|
+
return process.platform === 'win32' ? 'gradle.bat' : 'gradle';
|
|
186
|
+
}
|