windows-development-sandbox 1.23.2 → 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 (211) hide show
  1. package/config.example.json +5 -3
  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-7YPEDAJD.js → chunk-3GAXLATK.js} +58 -5
  17. package/dist/bin/{chunk-USTRSHIE.js → chunk-F4PVBN23.js} +2615 -1515
  18. package/dist/bin/{chunk-QFSJX2KW.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 -5
  35. package/dist/core/detect/toolchains.js +0 -6
  36. package/dist/core/schema/config.d.ts +10 -9
  37. package/dist/core/schema/pc-installs.d.ts +3 -1
  38. package/dist/core/schema/pc-installs.js +10 -1
  39. package/dist/core/schema/toolchains.d.ts +1 -2
  40. package/dist/core/schema/toolchains.js +4 -7
  41. package/dist/guest/accounts/kinds/epic.js +18 -13
  42. package/dist/guest/agents/agent-files.d.ts +11 -7
  43. package/dist/guest/agents/agent-files.js +37 -11
  44. package/dist/guest/agents/local-state.d.ts +2 -1
  45. package/dist/guest/agents/local-state.js +2 -1
  46. package/dist/guest/apps/after-install.d.ts +1 -1
  47. package/dist/guest/apps/after-install.js +6 -1
  48. package/dist/guest/apps/app-installer.d.ts +1 -0
  49. package/dist/guest/apps/app-installer.js +13 -3
  50. package/dist/guest/apps/comet-unpack.js +3 -2
  51. package/dist/guest/apps/resolvers-gamedev.js +55 -1
  52. package/dist/guest/boot/boot-console.d.ts +28 -0
  53. package/dist/guest/boot/boot-console.js +98 -0
  54. package/dist/guest/boot/boot.d.ts +8 -1
  55. package/dist/guest/boot/boot.js +80 -43
  56. package/dist/guest/boot/graph.d.ts +9 -5
  57. package/dist/guest/boot/graph.js +8 -2
  58. package/dist/guest/boot/guide.js +24 -0
  59. package/dist/guest/boot/progress-view.d.ts +116 -0
  60. package/dist/guest/boot/progress-view.js +456 -0
  61. package/dist/guest/boot/runner.d.ts +20 -10
  62. package/dist/guest/boot/runner.js +39 -47
  63. package/dist/guest/daemon/daemon.js +3 -0
  64. package/dist/guest/runtime/log-line.d.ts +7 -0
  65. package/dist/guest/runtime/log-line.js +18 -0
  66. package/dist/guest/runtime/log.d.ts +17 -4
  67. package/dist/guest/runtime/log.js +40 -17
  68. package/dist/guest/runtime/process.d.ts +9 -0
  69. package/dist/guest/runtime/process.js +69 -8
  70. package/dist/guest/setup/addon-mcp.d.ts +7 -0
  71. package/dist/guest/setup/addon-mcp.js +19 -4
  72. package/dist/guest/setup/browser-gpu.js +14 -11
  73. package/dist/guest/setup/environment.js +9 -4
  74. package/dist/guest/setup/headroom.js +4 -0
  75. package/dist/guest/setup/hooks.d.ts +2 -0
  76. package/dist/guest/setup/hooks.js +4 -0
  77. package/dist/guest/setup/path-plan.d.ts +12 -0
  78. package/dist/guest/setup/path-plan.js +18 -0
  79. package/dist/guest/toolchains/engines/epic-launcher.d.ts +26 -0
  80. package/dist/guest/toolchains/engines/epic-launcher.js +127 -0
  81. package/dist/guest/toolchains/engines/unreal-engines.d.ts +21 -1
  82. package/dist/guest/toolchains/engines/unreal-engines.js +163 -3
  83. package/dist/guest/toolchains/engines/unreal-status.d.ts +13 -0
  84. package/dist/guest/toolchains/engines/unreal-status.js +41 -0
  85. package/dist/guest/toolchains/native/debuggers.d.ts +6 -0
  86. package/dist/guest/toolchains/native/debuggers.js +57 -0
  87. package/dist/guest/toolchains/native/mesa.js +15 -8
  88. package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
  89. package/dist/guest/toolchains/native/rust-extras.js +23 -1
  90. package/dist/guest/toolchains/native/unreal.d.ts +2 -2
  91. package/dist/guest/toolchains/native/unreal.js +96 -20
  92. package/dist/guest/toolchains/native/visual-studio.d.ts +14 -14
  93. package/dist/guest/toolchains/native/visual-studio.js +145 -174
  94. package/dist/guest/toolchains/native/vs-persist.d.ts +189 -0
  95. package/dist/guest/toolchains/native/vs-persist.js +256 -0
  96. package/dist/guest/toolchains/native/vs-request.d.ts +6 -26
  97. package/dist/guest/toolchains/native/vs-request.js +16 -50
  98. package/dist/guest/toolchains/plan.js +23 -15
  99. package/dist/guest/toolchains/tools/vscode-profile.js +2 -0
  100. package/dist/host/accounts/kinds/epic.js +11 -5
  101. package/dist/host/accounts/source.d.ts +2 -0
  102. package/dist/host/accounts/source.js +1 -0
  103. package/dist/host/migrate/migrations/drop-vs-install-files.d.ts +5 -0
  104. package/dist/host/migrate/migrations/drop-vs-install-files.js +34 -0
  105. package/dist/host/migrate/registry.js +7 -1
  106. package/dist/host/pc-installs/discover.d.ts +2 -0
  107. package/dist/host/pc-installs/discover.js +25 -5
  108. package/dist/host/pc-installs/epic.d.ts +3 -0
  109. package/dist/host/pc-installs/epic.js +33 -2
  110. package/dist/host/pc-installs/source.d.ts +1 -1
  111. package/dist/host/pc-installs/unreal-vault.d.ts +11 -0
  112. package/dist/host/pc-installs/unreal-vault.js +78 -0
  113. package/dist/host/pc-installs/unreal.d.ts +36 -0
  114. package/dist/host/pc-installs/unreal.js +145 -2
  115. package/dist/host/profile/claude-profile.d.ts +8 -0
  116. package/dist/host/profile/claude-profile.js +43 -4
  117. package/dist/host/profile/index.d.ts +1 -1
  118. package/dist/host/profile/index.js +1 -1
  119. package/dist/runtime/agent.js +148 -33
  120. package/dist/runtime/daemon.js +3146 -1546
  121. package/dist/runtime/db.js +148 -33
  122. package/dist/runtime/main.js +3982 -2049
  123. package/dist/runtime/netproxy.js +84 -13
  124. package/dist/runtime/tool.js +125 -31
  125. package/dist/runtime/view.js +85 -14
  126. package/dist/selftest/agent-paths.test.js +18 -1
  127. package/dist/selftest/agent-settings.test.js +36 -5
  128. package/dist/selftest/agents-web.test.js +12 -0
  129. package/dist/selftest/boot-console.test.d.ts +2 -0
  130. package/dist/selftest/boot-console.test.js +291 -0
  131. package/dist/selftest/boot-runner.test.js +10 -12
  132. package/dist/selftest/engines.test.js +602 -3
  133. package/dist/selftest/guest-boot.test.js +7 -1
  134. package/dist/selftest/heavy-load.test.js +3 -1
  135. package/dist/selftest/hostile-input.test.js +11 -1
  136. package/dist/selftest/index.js +4 -0
  137. package/dist/selftest/layout.test.js +4 -0
  138. package/dist/selftest/menu-screens.js +3 -1
  139. package/dist/selftest/migrate.test.js +21 -0
  140. package/dist/selftest/mixamo.test.d.ts +2 -0
  141. package/dist/selftest/mixamo.test.js +60 -0
  142. package/dist/selftest/persisted-installs.test.js +169 -33
  143. package/dist/selftest/secrets.test.js +36 -1
  144. package/dist/selftest/support/engine-golden.js +5 -0
  145. package/dist/selftest/support/fixtures.d.ts +4 -3
  146. package/dist/selftest/support/unreal-sample.d.ts +2 -0
  147. package/dist/selftest/support/unreal-sample.js +83 -0
  148. package/dist/selftest/toolchains.test.js +32 -2
  149. package/dist/shared/accounts/index.d.ts +1 -1
  150. package/dist/shared/accounts/index.js +1 -1
  151. package/dist/shared/accounts/kinds/epic.d.ts +6 -1
  152. package/dist/shared/accounts/kinds/epic.js +11 -1
  153. package/dist/shared/apps/associations.d.ts +7 -0
  154. package/dist/shared/apps/associations.js +26 -0
  155. package/dist/shared/apps/catalogue-agents-web.d.ts +4 -1
  156. package/dist/shared/apps/catalogue-agents-web.js +5 -2
  157. package/dist/shared/apps/catalogue-gamedev.js +71 -1
  158. package/dist/shared/apps/index.d.ts +2 -0
  159. package/dist/shared/apps/index.js +2 -0
  160. package/dist/shared/apps/preview.d.ts +2 -0
  161. package/dist/shared/apps/preview.js +10 -0
  162. package/dist/shared/browser-policy.d.ts +14 -0
  163. package/dist/shared/browser-policy.js +22 -0
  164. package/dist/shared/channel/schema.d.ts +3 -6
  165. package/dist/shared/io/index.d.ts +1 -0
  166. package/dist/shared/io/index.js +1 -0
  167. package/dist/shared/io/rotate-log.d.ts +2 -0
  168. package/dist/shared/io/rotate-log.js +16 -0
  169. package/dist/shared/memory/policy.d.ts +5 -0
  170. package/dist/shared/memory/policy.js +7 -0
  171. package/dist/shared/paths/pc-installs.d.ts +13 -2
  172. package/dist/shared/paths/pc-installs.js +1 -0
  173. package/dist/shared/pty/terminal.js +2 -1
  174. package/dist/shared/readiness.d.ts +0 -1
  175. package/dist/shared/readiness.js +0 -1
  176. package/dist/shared/signer.d.ts +4 -0
  177. package/dist/shared/signer.js +2 -0
  178. package/dist/shared/unreal/engines.d.ts +161 -0
  179. package/dist/shared/unreal/engines.js +308 -0
  180. package/dist/shared/unreal/guide.d.ts +19 -0
  181. package/dist/shared/unreal/guide.js +105 -0
  182. package/dist/shared/unreal/index.d.ts +7 -0
  183. package/dist/shared/unreal/index.js +8 -0
  184. package/dist/shared/unreal/launcher-files.d.ts +88 -0
  185. package/dist/shared/unreal/launcher-files.js +292 -0
  186. package/dist/shared/unreal/mixamo.d.ts +5 -0
  187. package/dist/shared/unreal/mixamo.js +16 -0
  188. package/dist/shared/unreal/room.d.ts +17 -0
  189. package/dist/shared/unreal/room.js +34 -0
  190. package/dist/shared/unreal/status.d.ts +54 -0
  191. package/dist/shared/unreal/status.js +57 -0
  192. package/dist/shared/unreal/vault.d.ts +21 -0
  193. package/dist/shared/unreal/vault.js +67 -0
  194. package/dist/shared/unreal/vs-check.d.ts +39 -0
  195. package/dist/shared/unreal/vs-check.js +79 -0
  196. package/dist/sync/watcher.js +5 -1
  197. package/dist/web/index.html +1 -1
  198. package/dist/web/{main-CCILLX2M.js → main-FNE6XEHA.js} +84 -84
  199. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  200. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  201. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  202. package/package.json +1 -1
  203. package/sandbox/csharp/VsSetup.cs +210 -0
  204. package/sandbox/scripts/get-vs-setup.ps1 +93 -0
  205. package/sandbox/scripts/restore-visual-studio.ps1 +78 -0
  206. package/sandbox/scripts/save-visual-studio.ps1 +106 -0
  207. package/sandbox/scripts/set-registry-values.ps1 +35 -0
  208. package/schemas/config.schema.json +38 -9
  209. package/schemas/sandbox-config.schema.json +11 -8
  210. package/dist/guest/toolchains/native/autosdk.d.ts +0 -3
  211. package/dist/guest/toolchains/native/autosdk.js +0 -50
@@ -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 is its own step
4
- // (visual-studio.ts), which the plan runs first. The engine itself comes from the Epic Games 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';
5
7
  import os from 'node:os';
6
8
  import path from 'node:path';
7
- import { runInstaller } from '../../runtime/pools.js';
9
+ import { errMsg } from '../../../core/util/errors.js';
10
+ import { readValid } from '../../../shared/io/json.js';
8
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';
9
14
  import { dryRun } from '../../runtime/paths.js';
10
15
  import { exists, mkdirp } from '../../runtime/fs.js';
11
- import { log } from '../../runtime/log.js';
12
- 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';
13
20
  import { SIGNERS, signedInstaller } from '../installers.js';
14
21
  import { tool } from '../tool-env.js';
15
- import { setupAutosdk } from './autosdk.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,19 +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
- if (config.visualStudio === 'autosdk')
54
- await setupAutosdk(env);
55
- 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);
56
102
  const vs = installedVs();
57
- env.summary.push('unreal: ' +
58
- (vs ? 'Visual Studio ' + vs.version + ' (C++ game workload, Windows 11 SDK, Unreal support); ' : '') +
59
- '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' : ''));
60
136
  }
@@ -1,18 +1,18 @@
1
1
  import { type ToolEnv } from '../tool-env.js';
2
2
  import { type VsRequest } from './vs-request.js';
3
- export { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, unrealUsesAutosdk, visualStudioWanted, type VsRequest, } from './vs-request.js';
4
- /** The newest Visual Studio (or Build Tools) with the C++ compiler: its folder and version, or null. */
5
- export declare function installedVs(): {
6
- path: string;
7
- version: string;
8
- } | null;
9
- /** Downloads the bootstrapper now, so the install later starts without waiting for it. */
3
+ export { BUILD_TOOLS_COMPONENTS, UNREAL_BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, type VsRequest, } from './vs-request.js';
4
+ export interface InstalledVs {
5
+ readonly path: string;
6
+ readonly version: string;
7
+ readonly instanceId: string;
8
+ }
9
+ /** The newest Visual Studio (or Build Tools) with the C++ compiler, installed or put back: where it is, or null. */
10
+ export declare function installedVs(): InstalledVs | null;
11
+ /** Whether a later boot only puts Visual Studio back (quick enough to wait for) instead of installing it. */
12
+ export declare function visualStudioSaved(req: VsRequest): boolean;
13
+ /** Downloads the bootstrapper now, so the install later starts without waiting (not needed with a saved copy). */
10
14
  export declare function prefetchVisualStudio(req: VsRequest): Promise<void>;
11
- /** Visual Studio as asked for: installed at boot when it is not there yet; its folder and version. */
12
- export declare function ensureVisualStudio(env: ToolEnv, req: VsRequest, o: {
13
- keepInstallFiles: boolean;
15
+ /** Visual Studio as asked for: put back from the data disk, or installed (and saved) the first time. */
16
+ export declare function ensureVisualStudio(env: ToolEnv, req: VsRequest, o?: {
14
17
  lowPriority?: boolean;
15
- }): Promise<{
16
- path: string;
17
- version: string;
18
- }>;
18
+ }): Promise<InstalledVs>;
@@ -1,31 +1,28 @@
1
1
  // Visual Studio for the toolchains that need the real one (vs-request.ts says which): Community with the C++ game
2
2
  // workload for Unreal (its build tool finds Visual Studio through the Setup API, so the portable MSVC does not do), or
3
- // the Build Tools' C++ parts for Flutter desktop and Unity IL2CPP. A real installer: it runs every boot, on the
4
- // sandbox's own disk, and never while sessions run (they would need a restart). The bootstrapper is kept in tools\
5
- // and run only when Microsoft's signature checks out; with keepInstallFiles an offline copy of the install files
6
- // (tools\visualstudio\layout) spares later boots the download.
3
+ // the Build Tools' C++ parts. Installed once with Microsoft's installer (the bootstrapper is kept in tools\ and run
4
+ // only when Microsoft's signature checks out), then saved on the data disk (tools\visualstudio\persist, vs-persist.ts
5
+ // says what goes where). Later boots put the saved copy back in seconds: no download, no install, nothing big on C:.
6
+ // Never installed while sessions run (they would need a restart); putting a saved copy back only adds to the machine.
7
7
  import fs from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import { z } from 'zod';
11
- import { spawnAsync } from '../../../shared/download/index.js';
12
11
  import { heavy, runInstaller } from '../../runtime/pools.js';
13
- import { writeJsonAtomic } from '../../../shared/io/json.js';
14
- import { robocopyFailed } from '../../../shared/io/robocopy-result.js';
12
+ import { readValid, writeJsonAtomic } from '../../../shared/io/json.js';
15
13
  import { safeRemove } from '../../../shared/io/safe-remove.js';
16
- import { SYSTEM } from '../../../shared/system.js';
17
- import { execOutput } from '../../runtime/process.js';
14
+ import { execOutput, runScriptWithJsonAsync } from '../../runtime/process.js';
18
15
  import { exists, mkdirp } from '../../runtime/fs.js';
19
16
  import { log, warn } from '../../runtime/log.js';
20
- import { logsDir } from '../../runtime/paths.js';
21
17
  import { duringSession } from '../../runtime/project-context.js';
22
18
  import { backgroundRunning } from '../../runtime/status.js';
23
19
  import { errMsg } from '../../../core/util/errors.js';
24
20
  import { SIGNERS, signedInstaller } from '../installers.js';
25
21
  import { assertLocalRoom, freeBytes } from '../persisted.js';
26
22
  import { tool } from '../tool-env.js';
27
- import { LAYOUT_STAGING_BYTES, layoutNextStep, STAGING_RESERVE_BYTES, vsExitText, } from './vs-request.js';
28
- export { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, unrealUsesAutosdk, visualStudioWanted, } from './vs-request.js';
23
+ import { vsExitText } from './vs-request.js';
24
+ import { MARKER_FILE, markerFrom, newestInstance, persistCovers, productFolder, restoreInput, rewriteInstanceState, SaveResultSchema, saveInput, VsPersistMarkerSchema, winPlaces, } from './vs-persist.js';
25
+ export { BUILD_TOOLS_COMPONENTS, UNREAL_BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, } from './vs-request.js';
29
26
  const BOOTSTRAPPERS = {
30
27
  community: {
31
28
  url: 'https://aka.ms/vs/17/release/vs_community.exe',
@@ -38,187 +35,140 @@ const BOOTSTRAPPERS = {
38
35
  name: 'Visual Studio Build Tools 2022',
39
36
  },
40
37
  };
41
- /** What each takes on the sandbox's own disk (13 GB more must stay free: assertLocalRoom). */
38
+ const GB = 1024 ** 3;
39
+ /** What each takes on the sandbox's own disk while it is installed (13 GB more must stay free: assertLocalRoom). */
42
40
  const INSTALL_BYTES = {
43
- community: 7 * 1024 ** 3,
44
- buildtools: 5 * 1024 ** 3,
41
+ community: 7 * GB,
42
+ buildtools: 5 * GB,
45
43
  };
44
+ /** Free room on the data disk the saved copy needs (Community with the Windows SDK is about 6.5 GB). */
45
+ const SAVE_ROOM_BYTES = 8 * GB;
46
46
  /** Visual Studio installer exit codes that mean installed: 0, and 3010 (a restart would finish it, not needed here). */
47
47
  const OK_EXIT_CODES = [0, 3010];
48
- /** Free room the offline copy needs before it is made (it grows to about 10 GB). */
49
- const LAYOUT_ROOM_BYTES = 15 * 1024 ** 3;
50
48
  const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
51
49
  const VS_DIR = tool('visualstudio');
52
- const LayoutSchema = z.object({ components: z.array(z.string()) });
53
- const VSWHERE = path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Microsoft Visual Studio', 'Installer', 'vswhere.exe');
54
- /** The newest Visual Studio (or Build Tools) with the C++ compiler: its folder and version, or null. */
50
+ const PERSIST_DIR = path.join(VS_DIR, 'persist');
51
+ const MARKER = path.join(PERSIST_DIR, MARKER_FILE);
52
+ /** What older versions left in tools\visualstudio: the offline install files, which the installer never finished. */
53
+ const LEFTOVERS = ['layout', 'layout-buildtools', 'tmp'];
54
+ const PLACES = winPlaces(process.env);
55
+ const INSTALLER_DIR = path.join(PLACES.programFilesX86, 'Microsoft Visual Studio', 'Installer');
56
+ const VSWHERE = path.join(INSTALLER_DIR, 'vswhere.exe');
57
+ const VswhereSchema = z.array(z.looseObject({
58
+ instanceId: z.string(),
59
+ installationPath: z.string(),
60
+ installationVersion: z.string().optional(),
61
+ }));
62
+ /** The newest Visual Studio (or Build Tools) with the C++ compiler, installed or put back: where it is, or null. */
55
63
  export function installedVs() {
56
64
  if (!exists(VSWHERE))
57
65
  return null;
58
- const ask = (property) => execOutput(VSWHERE, [
59
- '-latest',
66
+ const out = execOutput(VSWHERE, [
60
67
  '-products',
61
68
  '*',
62
69
  '-requires',
63
70
  'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
64
- '-property',
65
- property,
71
+ '-format',
72
+ 'json',
73
+ '-utf8',
66
74
  ]);
67
- const at = ask('installationPath');
68
- return at ? { path: at, version: ask('installationVersion') || 'installed' } : null;
69
- }
70
- function layoutDir(product) {
71
- return path.join(VS_DIR, product === 'community' ? 'layout' : 'layout-buildtools');
72
- }
73
- /** The layout arguments of the bootstrapper (the same for the data disk and the staging folder). */
74
- const layoutArgs = (req, folder) => [
75
- '--layout',
76
- folder,
77
- '--lang',
78
- 'en-US',
79
- '--quiet',
80
- '--wait',
81
- ...req.components.flatMap((id) => ['--add', id]),
82
- ];
83
- /** The bootstrapper's newest log (dd_bootstrapper_*.log) in these folders, or null. */
84
- function newestBootstrapperLog(folders) {
85
- const logs = folders.flatMap((dir) => {
86
- try {
87
- return fs
88
- .readdirSync(dir)
89
- .filter((n) => /^dd_bootstrapper_.*\.log$/i.test(n))
90
- .map((n) => path.join(dir, n));
91
- }
92
- catch {
93
- return []; // The folder is not there: no log in it.
94
- }
95
- });
96
- const stamp = (f) => {
97
- try {
98
- return fs.statSync(f).mtimeMs;
99
- }
100
- catch {
101
- return 0; // Gone since the listing.
102
- }
103
- };
104
- return logs.sort((x, y) => stamp(y) - stamp(x))[0] ?? null;
105
- }
106
- /** Says why the layout failed and keeps the installer's log where the sandbox's logs are. */
107
- function reportLayoutFailure(code, temps) {
108
- warn('Visual Studio: saving the install files failed (exit ' + code + '): ' + vsExitText(code));
109
- const found = newestBootstrapperLog(temps);
110
- if (!found)
111
- return;
112
- const kept = path.join(logsDir, 'vs-layout-' + new Date().toISOString().slice(0, 10) + '.log');
75
+ let found;
113
76
  try {
114
- fs.mkdirSync(logsDir, { recursive: true });
115
- fs.copyFileSync(found, kept);
116
- warn('Visual Studio: the installer log is ' + found + ' (a copy is in ' + kept + ')');
77
+ found = JSON.parse(out || '[]');
117
78
  }
118
- catch (e) {
119
- warn('Visual Studio: the installer log is ' + found + ' (could not copy it: ' + errMsg(e) + ')');
79
+ catch {
80
+ return null; // vswhere printed something else (it failed): no Visual Studio to use.
120
81
  }
82
+ const first = newestInstance(VswhereSchema.safeParse(found).data ?? []);
83
+ return first
84
+ ? { path: first.installationPath, version: first.installationVersion ?? 'installed', instanceId: first.instanceId }
85
+ : null;
121
86
  }
122
- /** Copies a finished layout onto the data disk (never /MOV: mapped folders refuse rename-over). */
123
- async function copyLayout(from, to) {
124
- mkdirp(to);
125
- const code = await heavy.run(() => spawnAsync(SYSTEM.robocopy(), [from, to, '/E', '/COPY:DAT', '/R:1', '/W:1', '/MT:8', '/NP']));
126
- return !robocopyFailed(code);
127
- }
128
- /** The offline copy's installer when it holds every component; made or completed when there is room. */
129
- async function layoutInstaller(req, bootstrapper, lowPriority) {
130
- const layout = layoutDir(req.product);
131
- const marker = path.join(layout, '.complete.json');
132
- const installer = path.join(layout, BOOTSTRAPPERS[req.product].file);
133
- const has = () => {
134
- if (!exists(marker) || !exists(installer))
135
- return false;
136
- try {
137
- const done = LayoutSchema.parse(JSON.parse(fs.readFileSync(marker, 'utf8')));
138
- return req.components.every((id) => done.components.includes(id));
139
- }
140
- catch {
141
- return false;
142
- }
143
- };
144
- const finish = () => {
145
- writeJsonAtomic(marker, { components: [...req.components] });
146
- return installer;
147
- };
148
- if (has())
149
- return installer;
150
- if (freeBytes(mkdirp(VS_DIR)) < LAYOUT_ROOM_BYTES) {
151
- warn('Visual Studio: not keeping the install files (less than 15 GB free in ' + VS_DIR + '); downloading this boot');
87
+ /** The saved copy when it is complete and serves this request, else null. */
88
+ function savedCopy(req) {
89
+ const marker = readValid(VsPersistMarkerSchema, MARKER);
90
+ if (!marker || !persistCovers(marker, req))
152
91
  return null;
92
+ const product = path.join(PERSIST_DIR, productFolder(marker.product));
93
+ const files = [
94
+ path.join(product, 'VC', 'Auxiliary', 'Build', 'vcvars64.bat'),
95
+ path.join(PERSIST_DIR, 'instance', 'state.json'),
96
+ path.join(PERSIST_DIR, 'Installer', 'vswhere.exe'),
97
+ path.join(PERSIST_DIR, 'Setup', 'x64', 'Microsoft.VisualStudio.Setup.Configuration.Native.dll'),
98
+ ];
99
+ return files.every(exists) ? marker : null;
100
+ }
101
+ /** Whether a later boot only puts Visual Studio back (quick enough to wait for) instead of installing it. */
102
+ export function visualStudioSaved(req) {
103
+ return savedCopy(req) !== null;
104
+ }
105
+ /** Puts the saved copy back: its fixed-path folders, the instance, the registry; then vswhere must find it. */
106
+ async function restoreSaved(marker) {
107
+ const started = Date.now();
108
+ log('Visual Studio: putting back the copy saved in ' + PERSIST_DIR + ' ...');
109
+ const plan = restoreInput(marker, PERSIST_DIR, PLACES);
110
+ await heavy.run(() => runScriptWithJsonAsync('restore-visual-studio.ps1', 'PlanFile', plan));
111
+ const vs = installedVs();
112
+ if (!vs)
113
+ throw new Error('vswhere does not find the Visual Studio that was put back');
114
+ log('Visual Studio ' + vs.version + ' is back in ' + Math.round((Date.now() - started) / 1000) + ' s (' + vs.path + ')');
115
+ return vs;
116
+ }
117
+ /**
118
+ * Saves a fresh install on the data disk for later boots (once). The marker goes last, after the instance's state is
119
+ * moved to the saved folder; a failure leaves no marker and only costs the next boot another install.
120
+ */
121
+ async function saveInstall(req, vs, lowPriority) {
122
+ const free = freeBytes(mkdirp(PERSIST_DIR));
123
+ if (free < SAVE_ROOM_BYTES) {
124
+ warn('Visual Studio: not saved for later boots (' +
125
+ (free / GB).toFixed(1) +
126
+ ' GB free in ' +
127
+ PERSIST_DIR +
128
+ ', it needs 8 GB); the next boot installs it again');
129
+ return;
153
130
  }
154
- log('Saving the Visual Studio install files for later boots (~10 GB, once) ...');
155
- fs.rmSync(marker, { force: true });
156
- // The installer crashes (0xE0434352) when it builds the layout in a mapped folder, so it is built on the sandbox's
157
- // own disk and copied over. That runs before Visual Studio is installed and the copy on C: is deleted first, so it
158
- // needs room for the layout only (STAGING_RESERVE_BYTES spare), not for the layout and the install together.
159
- const drive = (process.env.SystemDrive ?? 'C:') + '\\';
160
- if (layoutNextStep(null, freeBytes(drive), STAGING_RESERVE_BYTES) !== 'stage') {
161
- warn('Visual Studio: less than ' +
162
- Math.round((LAYOUT_STAGING_BYTES + STAGING_RESERVE_BYTES) / 1024 ** 3) +
163
- ' GB free on ' +
164
- drive +
165
- ' to build the install files; downloading this boot');
166
- return null;
131
+ fs.rmSync(MARKER, { force: true });
132
+ log('Saving Visual Studio in ' + PERSIST_DIR + ' for later boots (~6.5 GB, once) ...');
133
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sbx-vs-'));
134
+ const resultFile = path.join(dir, 'result.json');
135
+ try {
136
+ const plan = saveInput({ product: req.product, installationPath: vs.path, instanceId: vs.instanceId }, PERSIST_DIR, PLACES, resultFile);
137
+ await heavy.run(() => runScriptWithJsonAsync('save-visual-studio.ps1', 'PlanFile', plan, lowPriority ? ['-LowPriority'] : []));
138
+ const result = readValid(SaveResultSchema, resultFile);
139
+ if (!result)
140
+ throw new Error('the save script left no result');
141
+ const state = path.join(PERSIST_DIR, 'instance', 'state.json');
142
+ const saved = path.join(PERSIST_DIR, productFolder(req.product));
143
+ fs.writeFileSync(state, rewriteInstanceState(fs.readFileSync(state, 'utf8'), vs.path, saved), 'utf8');
144
+ // One product is kept: a Build Tools copy goes when Community replaces it (and the other way round).
145
+ safeRemove(path.join(PERSIST_DIR, productFolder(req.product === 'community' ? 'buildtools' : 'community')), VS_DIR);
146
+ writeJsonAtomic(MARKER, markerFrom(req, { installationPath: vs.path, instanceId: vs.instanceId, version: vs.version }, result, new Date().toISOString()));
147
+ log('Visual Studio is saved: later boots put it back in seconds');
167
148
  }
168
- const staging = path.join(drive, 'vs-layout-staging');
169
- log('Building the install files on ' + drive + ' and copying them to ' + layout + ' ...');
170
- const stagedCode = await runInstaller(bootstrapper, layoutArgs(req, staging), { lowPriority });
171
- const staged = path.join(staging, BOOTSTRAPPERS[req.product].file);
172
- let kept = false;
173
- if (OK_EXIT_CODES.includes(stagedCode) && exists(staged)) {
174
- kept = await copyLayout(staging, layout);
175
- if (!kept)
176
- warn('Visual Studio: copying the install files to ' + layout + ' failed');
149
+ catch (e) {
150
+ warn('Visual Studio: saving it for later boots failed (' + errMsg(e) + '); the next boot installs it again');
177
151
  }
178
- else {
179
- reportLayoutFailure(stagedCode, [os.tmpdir()]);
152
+ finally {
153
+ fs.rmSync(dir, { recursive: true, force: true });
180
154
  }
181
- safeRemove(staging, drive);
182
- return kept && exists(installer) ? finish() : null;
183
155
  }
184
- /** The bootstrapper, downloaded (net pool) and checked; kept in tools\ for later boots. */
156
+ /** The bootstrapper, downloaded (net pool) and checked; kept in tools\ for later installs. */
185
157
  function bootstrapperFor(req) {
186
158
  const boot = BOOTSTRAPPERS[req.product];
187
159
  return signedInstaller(boot.url, path.join(VS_DIR, boot.file), 'the ' + boot.name + ' installer', SIGNERS.microsoft);
188
160
  }
189
- /** Downloads the bootstrapper now, so the install later starts without waiting for it. */
161
+ /** Downloads the bootstrapper now, so the install later starts without waiting (not needed with a saved copy). */
190
162
  export async function prefetchVisualStudio(req) {
191
- if (!req.accepted || installedVs())
163
+ if (!req.accepted || installedVs() || savedCopy(req))
192
164
  return;
193
165
  await bootstrapperFor(req);
194
166
  }
195
- async function install(req, keepInstallFiles, lowPriority) {
167
+ async function install(req, lowPriority) {
196
168
  const boot = BOOTSTRAPPERS[req.product];
197
169
  const bootstrapper = await bootstrapperFor(req);
198
- let installer = null;
199
- if (keepInstallFiles) {
200
- try {
201
- installer = await layoutInstaller(req, bootstrapper, lowPriority);
202
- }
203
- catch (e) {
204
- warn('Visual Studio: the offline copy failed (' + errMsg(e) + '); downloading this boot');
205
- }
206
- }
207
- const run = (from, offline) => runInstaller(from, [
208
- '--quiet',
209
- '--wait',
210
- '--norestart',
211
- '--nocache',
212
- ...(offline ? ['--noWeb'] : []),
213
- ...req.components.flatMap((id) => ['--add', id]),
214
- ], { lowPriority });
215
- log('Installing ' + boot.name + (installer ? ' from the saved install files' : ' (downloads ~5-8 GB)') + ' ...');
216
- let code = await run(installer ?? bootstrapper, installer !== null);
217
- if (installer && (!OK_EXIT_CODES.includes(code) || !installedVs())) {
218
- // The saved copy is on the shared data disk: if installing from it fails, this boot downloads instead.
219
- warn('Visual Studio: installing from the saved install files failed (' + vsExitText(code) + '); downloading');
220
- code = await run(bootstrapper, false);
221
- }
170
+ log('Installing ' + boot.name + ' (downloads ~5-8 GB, once: it is saved on the data disk afterwards) ...');
171
+ const code = await runInstaller(bootstrapper, ['--quiet', '--wait', '--norestart', '--nocache', ...req.components.flatMap((id) => ['--add', id])], { lowPriority });
222
172
  if (!OK_EXIT_CODES.includes(code) || !installedVs())
223
173
  throw new Error('the Visual Studio installer failed (exit ' +
224
174
  code +
@@ -227,8 +177,21 @@ async function install(req, keepInstallFiles, lowPriority) {
227
177
  '); its logs are the dd_*.log files in ' +
228
178
  os.tmpdir());
229
179
  }
230
- /** Visual Studio as asked for: installed at boot when it is not there yet; its folder and version. */
231
- export async function ensureVisualStudio(env, req, o) {
180
+ /** Removes what older versions left in tools\visualstudio (best effort). */
181
+ function removeLeftovers() {
182
+ for (const name of LEFTOVERS) {
183
+ const dir = path.join(VS_DIR, name);
184
+ try {
185
+ if (exists(dir))
186
+ safeRemove(dir, VS_DIR);
187
+ }
188
+ catch (e) {
189
+ warn('Visual Studio: could not remove ' + dir + ' (' + errMsg(e) + ')');
190
+ }
191
+ }
192
+ }
193
+ /** Visual Studio as asked for: put back from the data disk, or installed (and saved) the first time. */
194
+ export async function ensureVisualStudio(env, req, o = {}) {
232
195
  if (!req.accepted)
233
196
  throw new Error('Visual Studio (for ' +
234
197
  req.for.join(', ') +
@@ -238,24 +201,32 @@ export async function ensureVisualStudio(env, req, o) {
238
201
  const bad = req.components.find((id) => !ID.test(id));
239
202
  if (bad)
240
203
  throw new Error('"' + bad + '" is not a Visual Studio component ID');
204
+ removeLeftovers();
205
+ const name = BOOTSTRAPPERS[req.product].name;
206
+ const done = (vs, how) => {
207
+ env.append.push(INSTALLER_DIR);
208
+ env.summary.push('visual studio: ' + name + ' ' + vs.version + ' at ' + vs.path + ' (' + how + '; for ' + req.for.join(', ') + ')');
209
+ return vs;
210
+ };
241
211
  const have = installedVs();
242
212
  if (have)
243
- return have;
213
+ return done(have, 'installed');
214
+ const saved = savedCopy(req);
215
+ if (saved) {
216
+ try {
217
+ return done(await restoreSaved(saved), 'kept on the data disk');
218
+ }
219
+ catch (e) {
220
+ warn('Visual Studio: putting the saved copy back failed (' + errMsg(e) + '); installing it again');
221
+ }
222
+ }
244
223
  if (duringSession())
245
224
  throw new Error(backgroundRunning('Visual Studio')
246
225
  ? 'Visual Studio is still installing in the background; it is ready in a few minutes'
247
226
  : 'Visual Studio is installed when the sandbox starts: restart it (sandbox restart) to get it');
248
- assertLocalRoom(INSTALL_BYTES[req.product], BOOTSTRAPPERS[req.product].name);
249
- await install(req, o.keepInstallFiles, o.lowPriority === true);
227
+ assertLocalRoom(INSTALL_BYTES[req.product], name);
228
+ await install(req, o.lowPriority === true);
250
229
  const vs = installedVs();
251
- env.summary.push('visual studio: ' +
252
- BOOTSTRAPPERS[req.product].name +
253
- ' ' +
254
- vs.version +
255
- ' at ' +
256
- vs.path +
257
- ' (for ' +
258
- req.for.join(', ') +
259
- ')');
260
- return vs;
230
+ await saveInstall(req, vs, o.lowPriority === true);
231
+ return done(vs, 'installed, saved for later boots');
261
232
  }