@driftengine/package 3.61.0

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