windows-development-sandbox 1.25.0 → 1.25.1

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 (158) hide show
  1. package/config.example.json +5 -2
  2. package/dist/app/features/data/data.def.d.ts +4 -4
  3. package/dist/app/features/data/data.run.d.ts +1 -1
  4. package/dist/app/features/defs.d.ts +161 -6
  5. package/dist/app/features/defs.js +7 -0
  6. package/dist/app/features/projects/projects.def.d.ts +1 -1
  7. package/dist/app/features/projects/projects.run.d.ts +1 -1
  8. package/dist/app/features/unreal/index.d.ts +2 -0
  9. package/dist/app/features/unreal/index.js +3 -0
  10. package/dist/app/features/unreal/unreal.def.d.ts +236 -0
  11. package/dist/app/features/unreal/unreal.def.js +379 -0
  12. package/dist/app/features/unreal/unreal.run.d.ts +96 -0
  13. package/dist/app/features/unreal/unreal.run.js +217 -0
  14. package/dist/app/registry.js +5 -0
  15. package/dist/bin/agent-sandbox.js +2 -2
  16. package/dist/bin/{chunk-SUYYVIDW.js → chunk-3GAXLATK.js} +58 -5
  17. package/dist/bin/{chunk-IZRGTSQ5.js → chunk-F4PVBN23.js} +2465 -1392
  18. package/dist/bin/{chunk-KH4MI2U5.js → chunk-G3TG4ZT7.js} +1 -1
  19. package/dist/bin/claude-host.js +2 -2
  20. package/dist/bin/claude-sbx.js +2 -2
  21. package/dist/bin/codex-sbx.js +2 -2
  22. package/dist/bin/gemini-sbx.js +2 -2
  23. package/dist/bin/sandbox.js +2 -2
  24. package/dist/bin/sbx-hide.js +2 -2
  25. package/dist/bin/sbx-show.js +2 -2
  26. package/dist/cli/commands/launch/launch.js +5 -2
  27. package/dist/cli/commands/registry.js +3 -0
  28. package/dist/cli/manifest.d.ts +18 -0
  29. package/dist/cli/manifest.js +26 -0
  30. package/dist/cli/menu/coverage-exempt.js +1 -0
  31. package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
  32. package/dist/cli/menu/pages/sandbox-page.js +34 -4
  33. package/dist/cli/menu/views.js +2 -0
  34. package/dist/core/config/defaults.js +18 -4
  35. package/dist/core/schema/config.d.ts +9 -7
  36. package/dist/core/schema/pc-installs.d.ts +3 -1
  37. package/dist/core/schema/pc-installs.js +10 -1
  38. package/dist/core/schema/toolchains.js +1 -1
  39. package/dist/guest/accounts/kinds/epic.js +18 -13
  40. package/dist/guest/apps/after-install.d.ts +1 -1
  41. package/dist/guest/apps/after-install.js +6 -1
  42. package/dist/guest/apps/app-installer.d.ts +1 -0
  43. package/dist/guest/apps/app-installer.js +13 -3
  44. package/dist/guest/apps/comet-unpack.js +3 -2
  45. package/dist/guest/apps/resolvers-gamedev.js +55 -1
  46. package/dist/guest/boot/boot.js +14 -1
  47. package/dist/guest/boot/guide.js +24 -0
  48. package/dist/guest/daemon/daemon.js +3 -0
  49. package/dist/guest/setup/addon-mcp.d.ts +7 -0
  50. package/dist/guest/setup/addon-mcp.js +19 -4
  51. package/dist/guest/setup/browser-gpu.js +14 -11
  52. package/dist/guest/setup/headroom.js +4 -0
  53. package/dist/guest/toolchains/engines/epic-launcher.d.ts +26 -0
  54. package/dist/guest/toolchains/engines/epic-launcher.js +127 -0
  55. package/dist/guest/toolchains/engines/unreal-engines.d.ts +21 -1
  56. package/dist/guest/toolchains/engines/unreal-engines.js +163 -3
  57. package/dist/guest/toolchains/engines/unreal-status.d.ts +13 -0
  58. package/dist/guest/toolchains/engines/unreal-status.js +41 -0
  59. package/dist/guest/toolchains/native/debuggers.d.ts +6 -0
  60. package/dist/guest/toolchains/native/debuggers.js +57 -0
  61. package/dist/guest/toolchains/native/mesa.js +15 -8
  62. package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
  63. package/dist/guest/toolchains/native/rust-extras.js +23 -1
  64. package/dist/guest/toolchains/native/unreal.d.ts +2 -2
  65. package/dist/guest/toolchains/native/unreal.js +96 -18
  66. package/dist/guest/toolchains/plan.js +12 -1
  67. package/dist/guest/toolchains/tools/vscode-profile.js +2 -0
  68. package/dist/host/accounts/kinds/epic.js +11 -5
  69. package/dist/host/accounts/source.d.ts +2 -0
  70. package/dist/host/accounts/source.js +1 -0
  71. package/dist/host/pc-installs/discover.d.ts +2 -0
  72. package/dist/host/pc-installs/discover.js +25 -5
  73. package/dist/host/pc-installs/epic.d.ts +3 -0
  74. package/dist/host/pc-installs/epic.js +33 -2
  75. package/dist/host/pc-installs/source.d.ts +1 -1
  76. package/dist/host/pc-installs/unreal-vault.d.ts +11 -0
  77. package/dist/host/pc-installs/unreal-vault.js +78 -0
  78. package/dist/host/pc-installs/unreal.d.ts +36 -0
  79. package/dist/host/pc-installs/unreal.js +145 -2
  80. package/dist/host/profile/claude-profile.d.ts +8 -0
  81. package/dist/host/profile/claude-profile.js +43 -4
  82. package/dist/host/profile/index.d.ts +1 -1
  83. package/dist/host/profile/index.js +1 -1
  84. package/dist/runtime/agent.js +81 -7
  85. package/dist/runtime/daemon.js +2645 -1287
  86. package/dist/runtime/db.js +81 -7
  87. package/dist/runtime/main.js +2901 -1722
  88. package/dist/runtime/netproxy.js +81 -7
  89. package/dist/runtime/tool.js +81 -7
  90. package/dist/runtime/view.js +81 -7
  91. package/dist/selftest/agent-paths.test.js +1 -0
  92. package/dist/selftest/agents-web.test.js +12 -0
  93. package/dist/selftest/engines.test.js +602 -3
  94. package/dist/selftest/heavy-load.test.js +3 -1
  95. package/dist/selftest/hostile-input.test.js +11 -1
  96. package/dist/selftest/index.js +2 -0
  97. package/dist/selftest/menu-screens.js +3 -1
  98. package/dist/selftest/mixamo.test.d.ts +2 -0
  99. package/dist/selftest/mixamo.test.js +60 -0
  100. package/dist/selftest/secrets.test.js +36 -1
  101. package/dist/selftest/support/engine-golden.js +3 -0
  102. package/dist/selftest/support/fixtures.d.ts +3 -1
  103. package/dist/selftest/support/unreal-sample.d.ts +2 -0
  104. package/dist/selftest/support/unreal-sample.js +83 -0
  105. package/dist/selftest/toolchains.test.js +32 -2
  106. package/dist/shared/accounts/index.d.ts +1 -1
  107. package/dist/shared/accounts/index.js +1 -1
  108. package/dist/shared/accounts/kinds/epic.d.ts +6 -1
  109. package/dist/shared/accounts/kinds/epic.js +11 -1
  110. package/dist/shared/apps/associations.d.ts +7 -0
  111. package/dist/shared/apps/associations.js +26 -0
  112. package/dist/shared/apps/catalogue-agents-web.d.ts +4 -1
  113. package/dist/shared/apps/catalogue-agents-web.js +5 -2
  114. package/dist/shared/apps/catalogue-gamedev.js +71 -1
  115. package/dist/shared/apps/index.d.ts +2 -0
  116. package/dist/shared/apps/index.js +2 -0
  117. package/dist/shared/apps/preview.d.ts +2 -0
  118. package/dist/shared/apps/preview.js +10 -0
  119. package/dist/shared/browser-policy.d.ts +14 -0
  120. package/dist/shared/browser-policy.js +22 -0
  121. package/dist/shared/io/index.d.ts +1 -0
  122. package/dist/shared/io/index.js +1 -0
  123. package/dist/shared/io/rotate-log.d.ts +2 -0
  124. package/dist/shared/io/rotate-log.js +16 -0
  125. package/dist/shared/memory/policy.d.ts +5 -0
  126. package/dist/shared/memory/policy.js +7 -0
  127. package/dist/shared/paths/pc-installs.d.ts +13 -2
  128. package/dist/shared/paths/pc-installs.js +1 -0
  129. package/dist/shared/readiness.d.ts +0 -1
  130. package/dist/shared/readiness.js +0 -1
  131. package/dist/shared/signer.d.ts +4 -0
  132. package/dist/shared/signer.js +2 -0
  133. package/dist/shared/unreal/engines.d.ts +161 -0
  134. package/dist/shared/unreal/engines.js +308 -0
  135. package/dist/shared/unreal/guide.d.ts +19 -0
  136. package/dist/shared/unreal/guide.js +105 -0
  137. package/dist/shared/unreal/index.d.ts +7 -0
  138. package/dist/shared/unreal/index.js +8 -0
  139. package/dist/shared/unreal/launcher-files.d.ts +88 -0
  140. package/dist/shared/unreal/launcher-files.js +292 -0
  141. package/dist/shared/unreal/mixamo.d.ts +5 -0
  142. package/dist/shared/unreal/mixamo.js +16 -0
  143. package/dist/shared/unreal/room.d.ts +17 -0
  144. package/dist/shared/unreal/room.js +34 -0
  145. package/dist/shared/unreal/status.d.ts +54 -0
  146. package/dist/shared/unreal/status.js +57 -0
  147. package/dist/shared/unreal/vault.d.ts +21 -0
  148. package/dist/shared/unreal/vault.js +67 -0
  149. package/dist/shared/unreal/vs-check.d.ts +39 -0
  150. package/dist/shared/unreal/vs-check.js +79 -0
  151. package/dist/web/index.html +1 -1
  152. package/dist/web/{main-CCILLX2M.js → main-FNE6XEHA.js} +84 -84
  153. package/package.json +1 -1
  154. package/sandbox/csharp/VsSetup.cs +210 -0
  155. package/sandbox/scripts/get-vs-setup.ps1 +93 -0
  156. package/sandbox/scripts/set-registry-values.ps1 +35 -0
  157. package/schemas/config.schema.json +35 -2
  158. package/schemas/sandbox-config.schema.json +8 -1
@@ -0,0 +1,41 @@
1
+ // tools\unreal\status.json: what the sandbox found for Unreal, for the guide and the Unreal page on the PC. The boot's
2
+ // machine-wide step writes the launcher, Visual Studio and debugger parts, each project's step its engine; every write
3
+ // keeps what the other parts wrote.
4
+ import path from 'node:path';
5
+ import { readValid, writeJsonAtomic } from '../../../shared/io/json.js';
6
+ import { UNREAL_STATUS_FILE, UnrealStatusSchema } from '../../../shared/unreal/index.js';
7
+ import { mkdirp } from '../../runtime/fs.js';
8
+ import { tool } from '../tool-env.js';
9
+ export const UNREAL_DIR = tool('unreal');
10
+ /** Where the Epic Games Launcher installs engines (persistEngines). */
11
+ export const ENGINES_DIR = path.join(UNREAL_DIR, 'engines');
12
+ /** The engines' local derived data cache (derivedDataCache "persisted"). */
13
+ export const DDC_DIR = path.join(UNREAL_DIR, 'ddc');
14
+ /** The launcher's manifests and entries of the engines in ENGINES_DIR, kept for later boots. */
15
+ export const SAVED_DIR = path.join(UNREAL_DIR, 'launcher');
16
+ const FILE = path.join(UNREAL_DIR, UNREAL_STATUS_FILE);
17
+ const EMPTY = {
18
+ updatedAt: '',
19
+ engines: [],
20
+ projects: [],
21
+ launcher: { installed: false, installLocation: null, problem: null, freeBytes: null, needBytes: 0 },
22
+ visualStudio: { version: '', path: '', how: '', check: '', ok: false },
23
+ debugger: null,
24
+ ddc: { mode: 'local', dir: null },
25
+ vaults: [],
26
+ mcpUrl: null,
27
+ };
28
+ export function readUnrealStatus() {
29
+ return readValid(UnrealStatusSchema, FILE) ?? EMPTY;
30
+ }
31
+ /** Changes the status with `change` and writes it (whole, atomically). */
32
+ export function updateUnrealStatus(change) {
33
+ const next = { ...change(readUnrealStatus()), updatedAt: new Date().toISOString() };
34
+ mkdirp(UNREAL_DIR);
35
+ writeJsonAtomic(FILE, next);
36
+ return next;
37
+ }
38
+ /** A fresh status at boot: the machine parts start over, the engines and projects are found again. */
39
+ export function resetUnrealStatus() {
40
+ updateUnrealStatus(() => EMPTY);
41
+ }
@@ -0,0 +1,6 @@
1
+ /** KitsRoot10 from the registry (64-bit view), or null. */
2
+ export declare function kitsRoot(): string | null;
3
+ /** The debuggers' x64 folder when cdb.exe is in it, else null. */
4
+ export declare function debuggersDir(root?: string | null): string | null;
5
+ /** The debuggers' folder, installed with the SDK installer when missing; null when there is no SDK to add them to. */
6
+ export declare function ensureDebuggers(): Promise<string | null>;
@@ -0,0 +1,57 @@
1
+ // The Windows SDK's Debugging Tools (cdb, windbg, kd) for reading Unreal crash dumps: not part of the SDK Visual Studio
2
+ // installs, so the Windows SDK installer (downloaded once into tools\visualstudio, Microsoft's signature checked) adds
3
+ // only that feature into the SDK the registry names (KitsRoot10). Visual Studio kept on the data disk has its SDK
4
+ // there, so the debuggers are installed once and come back with it (~45 s, 370 MB).
5
+ import path from 'node:path';
6
+ import { SYSTEM } from '../../../shared/system.js';
7
+ import { runInstaller } from '../../runtime/pools.js';
8
+ import { execOutput } from '../../runtime/process.js';
9
+ import { exists } from '../../runtime/fs.js';
10
+ import { log } from '../../runtime/log.js';
11
+ import { SIGNERS, signedInstaller } from '../installers.js';
12
+ import { tool } from '../tool-env.js';
13
+ /** The Windows SDK 10.0.22621 installer (the SDK version Visual Studio's Unreal components install). */
14
+ const SDK_SETUP_URL = 'https://go.microsoft.com/fwlink/?linkid=2311806';
15
+ const SDK_SETUP = tool('visualstudio', 'winsdksetup-22621.exe');
16
+ /** KitsRoot10 from the registry (64-bit view), or null. */
17
+ export function kitsRoot() {
18
+ const out = execOutput(SYSTEM.reg(), [
19
+ 'query',
20
+ 'HKLM\\SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots',
21
+ '/v',
22
+ 'KitsRoot10',
23
+ '/reg:64',
24
+ ]);
25
+ const m = /KitsRoot10\s+REG_SZ\s+(.+)$/im.exec(out);
26
+ return m ? m[1].trim() : null;
27
+ }
28
+ /** The debuggers' x64 folder when cdb.exe is in it, else null. */
29
+ export function debuggersDir(root = kitsRoot()) {
30
+ if (!root)
31
+ return null;
32
+ const dir = path.join(root, 'Debuggers', 'x64');
33
+ return exists(path.join(dir, 'cdb.exe')) ? dir : null;
34
+ }
35
+ /** The debuggers' folder, installed with the SDK installer when missing; null when there is no SDK to add them to. */
36
+ export async function ensureDebuggers() {
37
+ const root = kitsRoot();
38
+ if (!root)
39
+ return null;
40
+ const have = debuggersDir(root);
41
+ if (have)
42
+ return have;
43
+ const setup = await signedInstaller(SDK_SETUP_URL, SDK_SETUP, 'the Windows SDK installer, for its debuggers (~1.5 MB, then ~100 MB)', SIGNERS.microsoft);
44
+ log('Installing the Windows SDK debuggers (cdb, windbg) into ' + root + ' ...');
45
+ const code = await runInstaller(setup, [
46
+ '/features',
47
+ 'OptionId.WindowsDesktopDebuggers',
48
+ '/quiet',
49
+ '/norestart',
50
+ '/ceip',
51
+ 'off',
52
+ ]);
53
+ const dir = debuggersDir(root);
54
+ if (!dir)
55
+ throw new Error('the Windows SDK installer did not add the debuggers (exit ' + code + ')');
56
+ return dir;
57
+ }
@@ -5,7 +5,8 @@ import { SYSTEM } from '../../../shared/system.js';
5
5
  import { dryRun } from '../../runtime/paths.js';
6
6
  import { execAsync, scriptOutput } from '../../runtime/process.js';
7
7
  import { exists } from '../../runtime/fs.js';
8
- import { log } from '../../runtime/log.js';
8
+ import { log, warn } from '../../runtime/log.js';
9
+ import { errMsg } from '../../../core/util/errors.js';
9
10
  import { arch, tool } from '../tool-env.js';
10
11
  import { installFromGithub } from '../install.js';
11
12
  /**
@@ -33,6 +34,8 @@ export async function setupMesa(env, config) {
33
34
  await setupLavapipe(env, dir);
34
35
  }
35
36
  const VULKAN_RUNTIME = 'https://sdk.lunarg.com/sdk/download/latest/windows/vulkan-runtime-components.zip';
37
+ /** Used when "latest" is not published yet (LunarG announces a version before its runtime zip is up: a 404). */
38
+ const VULKAN_RUNTIME_PINNED = 'https://sdk.lunarg.com/sdk/download/1.4.350.0/windows/vulkan-runtime-components.zip';
36
39
  /**
37
40
  * Software Vulkan: Mesa's lavapipe plus the Khronos loader (vulkan-1.dll, from LunarG's runtime components; Windows
38
41
  * Sandbox has no Vulkan loader). The loader ignores VK_DRIVER_FILES in elevated processes, and everything in the
@@ -43,15 +46,19 @@ const setupLavapipe = async (env, mesaDir) => {
43
46
  if (!exists(icd))
44
47
  throw new Error('this Mesa build has no lavapipe (' + path.basename(icd) + ')');
45
48
  const dir = tool('vulkan');
46
- if (!exists(path.join(dir, 'vulkan-1.dll'))) {
47
- log('Downloading the Vulkan loader (~10 MB once) ...');
49
+ // vulkaninfo.exe too: a copy kept from an older runtime zip may lack it.
50
+ if (!exists(path.join(dir, 'vulkan-1.dll')) || !exists(path.join(dir, 'vulkaninfo.exe'))) {
51
+ log('Downloading the Vulkan loader (~20 MB once) ...');
48
52
  // LunarG publishes no checksum for this zip; it comes over HTTPS from LunarG, the Khronos SDK's maintainer.
49
53
  // The zip has x86 and x64 folders: only the 64-bit loader (and vulkaninfo) is extracted.
50
- await downloader.archive(VULKAN_RUNTIME, dir, {
51
- locate: 'vulkan-1.dll',
52
- fileName: 'vulkan-runtime.zip',
53
- members: ['*/x64/*'],
54
- });
54
+ const fetchRuntime = (url) => downloader.archive(url, dir, { locate: 'vulkan-1.dll', fileName: 'vulkan-runtime.zip', members: ['*/x64/*'] });
55
+ try {
56
+ await fetchRuntime(VULKAN_RUNTIME);
57
+ }
58
+ catch (e) {
59
+ warn('The latest Vulkan runtime could not be downloaded (' + errMsg(e) + '); using version 1.4.350.0.');
60
+ await fetchRuntime(VULKAN_RUNTIME_PINNED);
61
+ }
55
62
  }
56
63
  const register = (file) => execAsync(SYSTEM.reg(), ['add', 'HKLM\\SOFTWARE\\Khronos\\Vulkan\\Drivers', '/v', file, '/t', 'REG_DWORD', '/d', '0', '/f'], { stdio: 'ignore' });
57
64
  await register(icd);
@@ -14,6 +14,13 @@ export declare function cargoShim(env: ToolEnv, rust: Toolchains['rust']): void;
14
14
  * run at once in the whole sandbox and, with linker "lld", has MinGW's gcc use Rust's own lld instead of GNU ld.
15
15
  */
16
16
  export declare function linkGate(env: ToolEnv, rust: Toolchains['rust'], host: string): void;
17
+ /**
18
+ * MSVC host: link.exe writes an executable into a mapped folder (the workspace, C:\AgentData) as all zeros, so a
19
+ * fresh build script or test program fails with "The file or directory is corrupted and unreadable" (os error 1392).
20
+ * Rust's own lld (a copy of rust-lld.exe named lld-link.exe, which is how lld picks its mode) writes them whole.
21
+ * `rust.linker` "gnu" keeps link.exe; a linker the user set for the target wins.
22
+ */
23
+ export declare function lldLink(env: ToolEnv, rust: Toolchains['rust'], host: string): void;
17
24
  /**
18
25
  * Cargo defaults that fit Windows, written only when there is no $CARGO_HOME\config.toml yet: fewer parallel jobs
19
26
  * (parallel rustc ran out of paging file) and, for the GNU host, only line tables as debug info (full debug info made
@@ -7,7 +7,7 @@ import { execOutput } from '../../runtime/process.js';
7
7
  import { ensureCompiled } from '../../runtime/csc.js';
8
8
  import { arch, tool } from '../tool-env.js';
9
9
  import { wanted } from '../detect.js';
10
- import { cargoJobs, chooseTargetDir } from '../../../shared/memory/index.js';
10
+ import { cargoJobs, cargoLinkerVar, chooseTargetDir } from '../../../shared/memory/index.js';
11
11
  import { linkerSlots } from '../../governor/decide.js';
12
12
  import { warn } from '../../runtime/log.js';
13
13
  import { freeBytes } from '../persisted.js';
@@ -81,6 +81,28 @@ export function linkGate(env, rust, host) {
81
81
  env.vars.AGENT_LINK_SLOTS +
82
82
  ' at once');
83
83
  }
84
+ /**
85
+ * MSVC host: link.exe writes an executable into a mapped folder (the workspace, C:\AgentData) as all zeros, so a
86
+ * fresh build script or test program fails with "The file or directory is corrupted and unreadable" (os error 1392).
87
+ * Rust's own lld (a copy of rust-lld.exe named lld-link.exe, which is how lld picks its mode) writes them whole.
88
+ * `rust.linker` "gnu" keeps link.exe; a linker the user set for the target wins.
89
+ */
90
+ export function lldLink(env, rust, host) {
91
+ if (!host.endsWith('-pc-windows-msvc') || rust.linker !== 'lld')
92
+ return;
93
+ const variable = cargoLinkerVar(host);
94
+ if (process.env[variable])
95
+ return;
96
+ const rustEnv = { ...process.env, RUSTUP_HOME: tool('rust', 'rustup'), CARGO_HOME: tool('rust', 'cargo') };
97
+ const sysroot = execOutput(tool('rust', 'cargo', 'bin', 'rustc.exe'), ['--print', 'sysroot'], { env: rustEnv });
98
+ const lld = sysroot ? path.join(sysroot, 'lib', 'rustlib', host, 'bin', 'rust-lld.exe') : '';
99
+ if (!lld || !exists(lld))
100
+ return;
101
+ const shim = path.join(mkdirp(tool('shims')), 'lld-link.exe');
102
+ fs.copyFileSync(lld, shim);
103
+ env.vars[variable] = shim;
104
+ env.summary.push('rust linking: lld (link.exe writes empty files into mapped folders)');
105
+ }
84
106
  /**
85
107
  * Cargo defaults that fit Windows, written only when there is no $CARGO_HOME\config.toml yet: fewer parallel jobs
86
108
  * (parallel rustc ran out of paging file) and, for the GNU host, only line tables as debug info (full debug info made
@@ -2,6 +2,6 @@ import type { Toolchains } from '../../../core/types.js';
2
2
  import type { ToolCtx } from '../step.js';
3
3
  import { type ToolEnv } from '../tool-env.js';
4
4
  export { UNREAL_COMPONENTS } from './vs-request.js';
5
- /** Downloads the DirectX redistributable ahead of the install (only once the licence is accepted). */
5
+ /** Downloads the DirectX redistributable and the launcher ahead of the install (only once the licence is accepted). */
6
6
  export declare function fetchDirectX(config: Toolchains['unreal']): Promise<void>;
7
- export declare function setupUnreal(env: ToolEnv, config: Toolchains['unreal'], ctx: ToolCtx): Promise<void>;
7
+ export declare function setupUnreal(env: ToolEnv, config: Toolchains['unreal'], _ctx: ToolCtx): Promise<void>;
@@ -1,18 +1,25 @@
1
- // Unreal Engine development (toolchains.unreal): the DirectX End-User Runtime the engine lists (a real installer: it
2
- // runs every boot), then the engines (engines/unreal-engines.ts: the launcher's install folder on the data disk, PC
3
- // engines from pcInstalls, the derived data cache). Visual Studio (Community with the C++ game workload, or the Build
4
- // Tools) is its own step (visual-studio.ts), which the plan runs first. The engine itself comes from the Epic Games
5
- // Launcher or its source.
1
+ // Unreal Engine development, machine-wide (toolchains.unreal, once per boot): the DirectX End-User Runtime the engine
2
+ // lists and the Epic Games Launcher (real installers: they run every boot), the launcher pointed at the data disk for
3
+ // engines and given back the engines it installed there, the Windows SDK debuggers for crash dumps, and a check that
4
+ // UnrealBuildTool finds Visual Studio the way it looks (the Setup Configuration API). Visual Studio itself is its own
5
+ // step (visual-studio.ts), which the plan runs first; each project's engine is set up by engines/unreal-engines.ts.
6
+ import fs from 'node:fs';
6
7
  import os from 'node:os';
7
8
  import path from 'node:path';
8
- import { runInstaller } from '../../runtime/pools.js';
9
+ import { errMsg } from '../../../core/util/errors.js';
10
+ import { readValid } from '../../../shared/io/json.js';
9
11
  import { safeRemove } from '../../../shared/io/safe-remove.js';
12
+ import { VsSetupReportSchema, vsVerdict } from '../../../shared/unreal/index.js';
13
+ import { runInstaller } from '../../runtime/pools.js';
10
14
  import { dryRun } from '../../runtime/paths.js';
11
15
  import { exists, mkdirp } from '../../runtime/fs.js';
12
- import { log } from '../../runtime/log.js';
13
- import { setupUnrealEngines } from '../engines/unreal-engines.js';
16
+ import { log, warn } from '../../runtime/log.js';
17
+ import { runScriptAsync } from '../../runtime/process.js';
18
+ import { ensureLauncher, fetchLauncher, placeEngines, restoreLauncherItems } from '../engines/epic-launcher.js';
19
+ import { resetUnrealStatus, updateUnrealStatus } from '../engines/unreal-status.js';
14
20
  import { SIGNERS, signedInstaller } from '../installers.js';
15
21
  import { tool } from '../tool-env.js';
22
+ import { ensureDebuggers } from './debuggers.js';
16
23
  import { installedVs } from './visual-studio.js';
17
24
  export { UNREAL_COMPONENTS } from './vs-request.js';
18
25
  const DIRECTX_URL = 'https://download.microsoft.com/download/8/4/A/84A35BF1-DAFE-4AE8-82AF-AD2AE20B6B14/directx_Jun2010_redist.exe';
@@ -23,10 +30,13 @@ const directXInstalled = () => {
23
30
  };
24
31
  /** The redistributable (downloaded once, signature checked). */
25
32
  const directXRedist = () => signedInstaller(DIRECTX_URL, path.join(tool('visualstudio'), 'directx_Jun2010_redist.exe'), 'the DirectX End-User Runtime (~95 MB once)', SIGNERS.microsoft);
26
- /** Downloads the DirectX redistributable ahead of the install (only once the licence is accepted). */
33
+ /** Downloads the DirectX redistributable and the launcher ahead of the install (only once the licence is accepted). */
27
34
  export async function fetchDirectX(config) {
28
- if (config.acceptLicense && !directXInstalled())
35
+ if (!config.acceptLicense)
36
+ return;
37
+ if (!directXInstalled())
29
38
  await directXRedist();
39
+ await fetchLauncher();
30
40
  }
31
41
  async function installDirectX() {
32
42
  if (directXInstalled())
@@ -42,17 +52,85 @@ async function installDirectX() {
42
52
  if (code !== 0)
43
53
  throw new Error('the DirectX End-User Runtime installer failed (exit ' + code + ')');
44
54
  }
45
- export async function setupUnreal(env, config, ctx) {
55
+ /** What UnrealBuildTool's way of finding Visual Studio finds (scripts\get-vs-setup.ps1 calls the Setup API). */
56
+ async function checkVisualStudio() {
57
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sbx-vs-check-'));
58
+ const file = path.join(dir, 'result.json');
59
+ try {
60
+ await runScriptAsync('get-vs-setup.ps1', ['-ResultFile', file]);
61
+ const report = readValid(VsSetupReportSchema, file);
62
+ return report ? vsVerdict(report) : { ok: false, text: 'the check left no result' };
63
+ }
64
+ catch (e) {
65
+ return { ok: false, text: 'the check failed: ' + errMsg(e) };
66
+ }
67
+ finally {
68
+ fs.rmSync(dir, { recursive: true, force: true });
69
+ }
70
+ }
71
+ /** Runs one part, warning (not failing the step) when it does not work. */
72
+ async function part(what, work, fallback) {
73
+ try {
74
+ return await work();
75
+ }
76
+ catch (e) {
77
+ warn('Unreal: ' + what + ' failed (' + errMsg(e) + ')');
78
+ return fallback;
79
+ }
80
+ }
81
+ export async function setupUnreal(env, config, _ctx) {
46
82
  if (!config.acceptLicense)
47
- throw new Error('Unreal tools are installed only with toolchains.unreal.acceptLicense = true: Visual Studio Community and the DirectX End-User Runtime come under Microsoft licence terms (https://visualstudio.microsoft.com/license-terms/)');
83
+ throw new Error("Unreal tools are installed only with toolchains.unreal.acceptLicense = true: Visual Studio Community and the DirectX End-User Runtime come under Microsoft licence terms (https://visualstudio.microsoft.com/license-terms/), the Epic Games Launcher under Epic's (https://www.epicgames.com/site/eula)");
48
84
  if (dryRun) {
49
- log('(dry run) would install the DirectX End-User Runtime and set up the Unreal engines');
85
+ log('(dry run) would install the DirectX End-User Runtime and the Epic Games Launcher');
50
86
  return;
51
87
  }
52
- await installDirectX();
53
- await setupUnrealEngines(env, config, ctx);
88
+ resetUnrealStatus();
89
+ await part('the DirectX End-User Runtime', installDirectX, undefined);
90
+ const launcher = await part('the Epic Games Launcher', ensureLauncher, null);
91
+ // Before anyone opens the launcher: engines go to the data disk, and it knows the ones it installed there before.
92
+ const place = placeEngines(config.persistEngines);
93
+ if (config.persistEngines)
94
+ await part("restoring the launcher's engines", async () => restoreLauncherItems(), []);
95
+ if (place.problem) {
96
+ warn('Unreal: ' + place.problem);
97
+ env.summary.push('UNREAL: ' + place.problem);
98
+ }
99
+ const debuggers = await part('the Windows SDK debuggers', ensureDebuggers, null);
100
+ if (debuggers)
101
+ env.append.push(debuggers);
54
102
  const vs = installedVs();
55
- env.summary.push('unreal: ' +
56
- (vs ? 'Visual Studio ' + vs.version + ' at ' + vs.path + '; ' : '') +
57
- 'DirectX End-User Runtime; get the engine from the Epic Games Launcher (Start menu)');
103
+ const check = vs ? await checkVisualStudio() : { ok: false, text: 'no Visual Studio with the C++ tools' };
104
+ if (!check.ok)
105
+ warn('Unreal: UnrealBuildTool would not find a compiler: ' + check.text);
106
+ else
107
+ log('Unreal: UnrealBuildTool finds ' + check.text);
108
+ const kept = Boolean(vs && /\\visualstudio\\persist\\/i.test(vs.path + '\\'));
109
+ updateUnrealStatus((s) => ({
110
+ ...s,
111
+ launcher: {
112
+ installed: launcher !== null,
113
+ installLocation: place.installLocation,
114
+ problem: place.problem,
115
+ freeBytes: place.freeBytes,
116
+ needBytes: place.needBytes,
117
+ },
118
+ visualStudio: {
119
+ version: vs?.version ?? '',
120
+ path: vs?.path ?? '',
121
+ how: vs ? (kept ? 'kept on the data disk' : 'installed in this sandbox') : '',
122
+ check: check.text,
123
+ ok: check.ok,
124
+ },
125
+ debugger: debuggers ? path.join(debuggers, 'cdb.exe') : null,
126
+ }));
127
+ env.summary.push('unreal tools: ' +
128
+ (vs
129
+ ? 'Visual Studio ' +
130
+ vs.version +
131
+ (check.ok ? ' (UnrealBuildTool finds it)' : ' (UnrealBuildTool would NOT find it)')
132
+ : 'no Visual Studio') +
133
+ ', DirectX End-User Runtime' +
134
+ (launcher ? ', Epic Games Launcher' : '') +
135
+ (debuggers ? ', cdb/windbg' : ''));
58
136
  }
@@ -12,6 +12,7 @@ import { setupDotnet } from './native/dotnet.js';
12
12
  import { setupMesa } from './native/mesa.js';
13
13
  import { setupMsvc } from './native/msvc/index.js';
14
14
  import { fetchDirectX, setupUnreal } from './native/unreal.js';
15
+ import { setupUnrealEngines } from './engines/unreal-engines.js';
15
16
  import { fetchRustupInit, setupRust } from './native/rust.js';
16
17
  import { setupNode } from './languages/node.js';
17
18
  import { setupPwsh } from './tools/pwsh.js';
@@ -32,7 +33,7 @@ import { setupFlutter } from './mobile/flutter.js';
32
33
  import { setupDart } from './mobile/dart.js';
33
34
  import { setupReactNative } from './mobile/react-native.js';
34
35
  import { setupKotlin } from './languages/kotlin.js';
35
- import { rustHost, rustCaches, cargoDefaults, cargoShim, linkGate, resolveTargetDir, targetSummary, } from './native/rust-extras.js';
36
+ import { rustHost, rustCaches, cargoDefaults, cargoShim, linkGate, lldLink, resolveTargetDir, targetSummary, } from './native/rust-extras.js';
36
37
  import { setupPythonTools } from './languages/python-tools.js';
37
38
  export function plan(config, versions, o) {
38
39
  const apps = Boolean(o.apps && (o.apps.install.length || Object.keys(o.apps.custom).length));
@@ -87,6 +88,14 @@ export function plan(config, versions, o) {
87
88
  ...ahead(() => fetchDirectX(config.unreal)),
88
89
  install: (env) => setupUnreal(env, config.unreal, ctx),
89
90
  },
91
+ // Each project's engine (its .uproject's EngineAssociation) on its PATH: not shared, a project that joins later
92
+ // gets its own. It registers every engine too, so this PC's engines work without the licence step above.
93
+ {
94
+ name: 'Unreal engines',
95
+ enabled: unreal,
96
+ after: ['Unreal'],
97
+ install: (env) => setupUnrealEngines(env, config.unreal, ctx),
98
+ },
90
99
  {
91
100
  name: 'MSVC',
92
101
  enabled: wanted(config.msvc.enabled, 'msvc') && vs === null,
@@ -108,6 +117,8 @@ export function plan(config, versions, o) {
108
117
  cargoDefaults(rustHost(config));
109
118
  cargoShim(env, rust);
110
119
  linkGate(env, rust, rustHost(config));
120
+ lldLink(env, rust, rustHost(config));
121
+ lldLink(env, rust, rustHost(config));
111
122
  }
112
123
  },
113
124
  },
@@ -54,6 +54,8 @@ const BY_TOOLCHAIN = {
54
54
  rust: ['rust-lang.rust-analyzer', 'vadimcn.vscode-lldb'],
55
55
  cpp: ['ms-vscode.cpptools', 'ms-vscode.cmake-tools'],
56
56
  msvc: ['ms-vscode.cpptools', 'ms-vscode.cmake-tools'],
57
+ // Unreal C++ (IntelliSense from the generated .code-workspace) and the C# of the *.Build.cs / *.Target.cs rules.
58
+ unreal: ['ms-vscode.cpptools', 'ms-dotnettools.csharp'],
57
59
  dotnet: ['ms-dotnettools.csharp'],
58
60
  ruby: ['shopify.ruby-lsp'],
59
61
  php: ['bmewburn.vscode-intelephense-client', 'xdebug.php-debug'],
@@ -1,15 +1,21 @@
1
- // Epic Games: this PC's launcher sign-in ("Remember me" in its GameUserSettings.ini, often UTF-16). Taking it signs
2
- // the launcher on this PC out the first time a sandbox uses it (a sign-in works in one place at a time).
1
+ // Epic Games: this PC's launcher sign-in ("Remember me" in its GameUserSettings.ini, often UTF-16; launcher 17+ keeps
2
+ // it in Config\WindowsEditor, older ones in Config\Windows). Taking it signs the launcher on this PC out the first time
3
+ // a sandbox uses it (a sign-in works in one place at a time).
3
4
  import path from 'node:path';
4
- import { epicSettingsFile, MAX_EPIC_INI_BYTES, readEpicLogin, } from '../../../shared/accounts/index.js';
5
+ import { epicSettingsFiles, MAX_EPIC_INI_BYTES, readEpicLogin, } from '../../../shared/accounts/index.js';
5
6
  import { decodeText, readBoundedFile } from '../../../shared/io/index.js';
6
7
  export const epicSource = {
7
8
  fromPc(pc, from) {
8
9
  const localAppData = from ? path.resolve(from) : pc.localAppData;
9
10
  if (!localAppData)
10
11
  return null;
11
- const bytes = readBoundedFile(epicSettingsFile(localAppData), MAX_EPIC_INI_BYTES);
12
- return bytes ? readEpicLogin(decodeText(bytes).text) : null;
12
+ for (const file of epicSettingsFiles(localAppData)) {
13
+ const bytes = readBoundedFile(file, MAX_EPIC_INI_BYTES);
14
+ const login = bytes ? readEpicLogin(decodeText(bytes).text) : null;
15
+ if (login)
16
+ return login;
17
+ }
18
+ return null;
13
19
  },
14
20
  summary: () => null,
15
21
  };
@@ -4,6 +4,8 @@ export interface PcEnv {
4
4
  readonly localAppData: string;
5
5
  readonly appData: string;
6
6
  readonly programData: string;
7
+ /** C:\Program Files (where the Epic Games Launcher installs engines by default). */
8
+ readonly programFiles?: string;
7
9
  /** The environment the command runs with (a variable set in this terminal). */
8
10
  readonly env?: Readonly<Record<string, string | undefined>>;
9
11
  /** A variable of this Windows user's saved environment (HKCU\Environment), or null when it is not set. */
@@ -32,6 +32,7 @@ export function pcEnv() {
32
32
  localAppData: env.LOCALAPPDATA ?? '',
33
33
  appData: env.APPDATA ?? '',
34
34
  programData: env.ProgramData ?? env.PROGRAMDATA ?? 'C:\\ProgramData',
35
+ programFiles: env.ProgramFiles ?? 'C:\\Program Files',
35
36
  env,
36
37
  userVariable: savedUserVariable,
37
38
  };
@@ -10,6 +10,8 @@ export interface DiscoverOptions {
10
10
  readonly packageRoot: string;
11
11
  readonly storageRoot: string;
12
12
  readonly sources?: readonly PcInstallSource[];
13
+ /** This launch sets the sandbox up for Unreal ("auto" maps this PC's engines and vault cache then). */
14
+ readonly unrealOn?: boolean;
13
15
  }
14
16
  export interface DiscoveredInstalls {
15
17
  readonly mappings: ResolvedMapping[];
@@ -5,21 +5,29 @@ import { pcEnv } from '../accounts/index.js';
5
5
  import { epicInstalls } from './epic.js';
6
6
  import { steamInstalls } from './steam.js';
7
7
  import { unityInstalls } from './unity.js';
8
- import { unrealInstalls } from './unreal.js';
8
+ import { unrealFolders, unrealInstalls } from './unreal.js';
9
+ import { unrealVault } from './unreal-vault.js';
9
10
  /** At most this many installs are mapped (each one is a mapping in the .wsb). */
10
11
  export const MAX_PC_INSTALLS = 40;
11
12
  export const PC_INSTALL_SOURCES = [
12
13
  steamInstalls,
13
14
  epicInstalls,
15
+ unrealFolders,
14
16
  unrealInstalls,
17
+ unrealVault,
15
18
  unityInstalls,
16
19
  ];
20
+ /** What a setting asks for: false (nothing), true (everything) or a list; "auto" follows the Unreal toolchain. */
21
+ function wantedOf(s, setting, unrealOn) {
22
+ const value = s[setting];
23
+ return value === 'auto' ? unrealOn : value;
24
+ }
17
25
  export function discoverPcInstalls(s, o, pc = pcEnv()) {
18
26
  const mappings = [];
19
27
  const items = [];
20
28
  const notes = [];
21
29
  for (const source of o.sources ?? PC_INSTALL_SOURCES) {
22
- const wanted = s[source.setting];
30
+ const wanted = wantedOf(s, source.setting, o.unrealOn === true);
23
31
  if (wanted === false || (Array.isArray(wanted) && !wanted.length))
24
32
  continue;
25
33
  let found = [];
@@ -37,14 +45,22 @@ export function discoverPcInstalls(s, o, pc = pcEnv()) {
37
45
  }
38
46
  try {
39
47
  const guest = pcInstallGuest(install.kind, install.id);
40
- if (mappings.some((m) => m.guest.toLowerCase() === guest.toLowerCase()))
48
+ // Once each: a folder you registered (writable, listed first) is not mapped again read-only.
49
+ const same = (m) => m.guest.toLowerCase() === guest.toLowerCase() || m.host.toLowerCase() === install.host.toLowerCase();
50
+ if (mappings.some(same))
41
51
  continue;
42
52
  if (!dirExists(install.host)) {
43
53
  notes.push('pcInstalls: ' + install.name + ' is not there any more (' + install.host + ').');
44
54
  continue;
45
55
  }
46
56
  assertSafeMapping(install.host, o.packageRoot, o.storageRoot);
47
- mappings.push({ host: install.host, guest, readOnly: true, label: 'this PC, read-only' });
57
+ const writable = install.writable === true;
58
+ mappings.push({
59
+ host: install.host,
60
+ guest,
61
+ readOnly: !writable,
62
+ label: writable ? 'this PC, writable' : 'this PC, read-only',
63
+ });
48
64
  const { host: _host, ...shown } = install;
49
65
  items.push({ ...shown, guest });
50
66
  }
@@ -53,7 +69,11 @@ export function discoverPcInstalls(s, o, pc = pcEnv()) {
53
69
  }
54
70
  }
55
71
  if (Array.isArray(wanted) && found.length < wanted.length)
56
- notes.push('pcInstalls.' + source.setting + ': some of the listed ones are not installed on this PC.');
72
+ notes.push('pcInstalls.' +
73
+ source.setting +
74
+ (source.setting === 'unrealEngineFolders'
75
+ ? ': some of the listed folders are missing or hold no engine (Engine\\Build\\Build.version).'
76
+ : ': some of the listed ones are not installed on this PC.'));
57
77
  }
58
78
  return { mappings, manifest: { items }, notes };
59
79
  }
@@ -1,2 +1,5 @@
1
+ import type { PcInstall } from '../../shared/paths/index.js';
1
2
  import type { PcInstallSource } from './source.js';
3
+ /** The games a LauncherInstalled.dat lists that `wanted` maps (by app name), each once. */
4
+ export declare function findEpicGames(text: string | null, wanted: true | readonly (string | number)[]): PcInstall[];
2
5
  export declare const epicInstalls: PcInstallSource;
@@ -1,7 +1,38 @@
1
+ // Epic Games Launcher games of this PC (pcInstalls.epicGames): the InstallationList of its LauncherInstalled.dat
2
+ // (%ProgramData%\Epic\UnrealEngineLauncher), engines (UE_x.y) left to unreal.ts. C:\PcInstalls\epic\<AppName>.
3
+ import path from 'node:path';
4
+ import { readBoundedFile } from '../../shared/io/index.js';
5
+ import { launcherInstallations, launcherInstalledFile, MAX_LAUNCHER_FILE_BYTES } from '../../shared/unreal/index.js';
6
+ const APP = /^[A-Za-z0-9._-]{1,100}$/;
7
+ /** The games a LauncherInstalled.dat lists that `wanted` maps (by app name), each once. */
8
+ export function findEpicGames(text, wanted) {
9
+ const names = wanted === true ? null : new Set(wanted.map((w) => String(w).toLowerCase()));
10
+ const found = [];
11
+ for (const i of text ? launcherInstallations(text) : []) {
12
+ if (!APP.test(i.appName) || /^\.+$/.test(i.appName) || /^UE_\d/.test(i.appName))
13
+ continue;
14
+ if (names && !names.has(i.appName.toLowerCase()))
15
+ continue;
16
+ if (found.some((f) => f.id.toLowerCase() === i.appName.toLowerCase()))
17
+ continue;
18
+ found.push({
19
+ kind: 'epic-game',
20
+ id: i.appName,
21
+ name: path.win32.basename(i.installLocation).slice(0, 100) || i.appName,
22
+ ...(i.ids ? { version: i.ids.appVersion.slice(0, 60), launcher: i.ids } : {}),
23
+ host: path.win32.normalize(i.installLocation),
24
+ guest: '',
25
+ });
26
+ }
27
+ return found;
28
+ }
1
29
  export const epicInstalls = {
2
30
  kind: 'epic-game',
3
31
  setting: 'epicGames',
4
- find(_wanted, _pc) {
5
- return [];
32
+ find(wanted, pc) {
33
+ const bytes = readBoundedFile(launcherInstalledFile(pc.programData), MAX_LAUNCHER_FILE_BYTES);
34
+ if (!bytes)
35
+ throw new Error('the Epic Games Launcher has no LauncherInstalled.dat here');
36
+ return findEpicGames(bytes.toString('utf8'), wanted);
6
37
  },
7
38
  };
@@ -3,7 +3,7 @@ import type { PcEnv } from '../accounts/index.js';
3
3
  export interface PcInstallSource {
4
4
  readonly kind: PcInstall['kind'];
5
5
  /** The settings key that turns it on (pcInstalls.<setting>). */
6
- readonly setting: 'steamGames' | 'epicGames' | 'unrealEngines' | 'unityEditors';
6
+ readonly setting: 'steamGames' | 'epicGames' | 'unrealEngines' | 'unrealEngineFolders' | 'unrealAssets' | 'unityEditors';
7
7
  /** What is installed: everything (true) or the ones listed. Bounded reads only (registry, manifests). */
8
8
  find(wanted: true | readonly (string | number)[], pc: PcEnv): PcInstall[];
9
9
  }
@@ -0,0 +1,11 @@
1
+ import type { PcInstall } from '../../shared/paths/index.js';
2
+ import { type VaultFs } from '../../shared/unreal/index.js';
3
+ import type { PcEnv } from '../accounts/index.js';
4
+ import type { PcInstallSource } from './source.js';
5
+ /** The vault cache folders of this PC: the ones its launcher settings name, else the default. */
6
+ export declare function vaultDirs(pc: Pick<PcEnv, 'localAppData' | 'programData'>, isDir: (dir: string) => boolean): string[];
7
+ /** The vault folders as installs to map (read-only). */
8
+ export declare function vaultInstalls(dirs: readonly string[]): PcInstall[];
9
+ /** This PC's disk for vaultPacks (bounded lists). */
10
+ export declare const PC_VAULT_FS: VaultFs;
11
+ export declare const unrealVault: PcInstallSource;