windows-development-sandbox 1.23.1 → 1.25.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 (85) hide show
  1. package/config.example.json +0 -1
  2. package/dist/bin/agent-sandbox.js +2 -2
  3. package/dist/bin/{chunk-USTRSHIE.js → chunk-IZRGTSQ5.js} +158 -131
  4. package/dist/bin/{chunk-QFSJX2KW.js → chunk-KH4MI2U5.js} +1 -1
  5. package/dist/bin/{chunk-7YPEDAJD.js → chunk-SUYYVIDW.js} +1 -1
  6. package/dist/bin/claude-host.js +2 -2
  7. package/dist/bin/claude-sbx.js +2 -2
  8. package/dist/bin/codex-sbx.js +2 -2
  9. package/dist/bin/gemini-sbx.js +2 -2
  10. package/dist/bin/sandbox.js +2 -2
  11. package/dist/bin/sbx-hide.js +2 -2
  12. package/dist/bin/sbx-show.js +2 -2
  13. package/dist/core/config/defaults.js +0 -1
  14. package/dist/core/detect/toolchains.js +0 -6
  15. package/dist/core/schema/config.d.ts +1 -2
  16. package/dist/core/schema/toolchains.d.ts +1 -2
  17. package/dist/core/schema/toolchains.js +3 -6
  18. package/dist/guest/agents/agent-files.d.ts +11 -7
  19. package/dist/guest/agents/agent-files.js +37 -11
  20. package/dist/guest/agents/local-state.d.ts +2 -1
  21. package/dist/guest/agents/local-state.js +2 -1
  22. package/dist/guest/boot/boot-console.d.ts +28 -0
  23. package/dist/guest/boot/boot-console.js +98 -0
  24. package/dist/guest/boot/boot.d.ts +8 -1
  25. package/dist/guest/boot/boot.js +66 -42
  26. package/dist/guest/boot/graph.d.ts +9 -5
  27. package/dist/guest/boot/graph.js +8 -2
  28. package/dist/guest/boot/progress-view.d.ts +116 -0
  29. package/dist/guest/boot/progress-view.js +456 -0
  30. package/dist/guest/boot/runner.d.ts +20 -10
  31. package/dist/guest/boot/runner.js +39 -47
  32. package/dist/guest/runtime/log-line.d.ts +7 -0
  33. package/dist/guest/runtime/log-line.js +18 -0
  34. package/dist/guest/runtime/log.d.ts +17 -4
  35. package/dist/guest/runtime/log.js +40 -17
  36. package/dist/guest/runtime/process.d.ts +9 -0
  37. package/dist/guest/runtime/process.js +69 -8
  38. package/dist/guest/setup/environment.js +9 -4
  39. package/dist/guest/setup/hooks.d.ts +2 -0
  40. package/dist/guest/setup/hooks.js +4 -0
  41. package/dist/guest/setup/path-plan.d.ts +12 -0
  42. package/dist/guest/setup/path-plan.js +18 -0
  43. package/dist/guest/toolchains/native/unreal.js +4 -6
  44. package/dist/guest/toolchains/native/visual-studio.d.ts +14 -14
  45. package/dist/guest/toolchains/native/visual-studio.js +145 -174
  46. package/dist/guest/toolchains/native/vs-persist.d.ts +189 -0
  47. package/dist/guest/toolchains/native/vs-persist.js +256 -0
  48. package/dist/guest/toolchains/native/vs-request.d.ts +6 -22
  49. package/dist/guest/toolchains/native/vs-request.js +16 -46
  50. package/dist/guest/toolchains/plan.js +11 -14
  51. package/dist/host/migrate/migrations/drop-vs-install-files.d.ts +5 -0
  52. package/dist/host/migrate/migrations/drop-vs-install-files.js +34 -0
  53. package/dist/host/migrate/registry.js +7 -1
  54. package/dist/runtime/agent.js +67 -26
  55. package/dist/runtime/daemon.js +958 -716
  56. package/dist/runtime/db.js +67 -26
  57. package/dist/runtime/main.js +1792 -1038
  58. package/dist/runtime/netproxy.js +3 -6
  59. package/dist/runtime/tool.js +44 -24
  60. package/dist/runtime/view.js +4 -7
  61. package/dist/selftest/agent-paths.test.js +17 -1
  62. package/dist/selftest/agent-settings.test.js +36 -5
  63. package/dist/selftest/boot-console.test.d.ts +2 -0
  64. package/dist/selftest/boot-console.test.js +291 -0
  65. package/dist/selftest/boot-runner.test.js +10 -12
  66. package/dist/selftest/guest-boot.test.js +7 -1
  67. package/dist/selftest/index.js +2 -0
  68. package/dist/selftest/layout.test.js +4 -0
  69. package/dist/selftest/migrate.test.js +21 -0
  70. package/dist/selftest/persisted-installs.test.js +169 -32
  71. package/dist/selftest/support/engine-golden.js +2 -0
  72. package/dist/selftest/support/fixtures.d.ts +1 -2
  73. package/dist/shared/channel/schema.d.ts +3 -6
  74. package/dist/shared/pty/terminal.js +2 -1
  75. package/dist/sync/watcher.js +5 -1
  76. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  77. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  78. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  79. package/package.json +1 -1
  80. package/sandbox/scripts/restore-visual-studio.ps1 +78 -0
  81. package/sandbox/scripts/save-visual-studio.ps1 +106 -0
  82. package/schemas/config.schema.json +3 -7
  83. package/schemas/sandbox-config.schema.json +3 -7
  84. package/dist/guest/toolchains/native/autosdk.d.ts +0 -3
  85. package/dist/guest/toolchains/native/autosdk.js +0 -50
@@ -0,0 +1,256 @@
1
+ // Visual Studio kept on the data disk (visual-studio.ts runs it): what is saved once after the real install, the marker
2
+ // that says the saved copy is complete, and what a later boot puts back (a few copies and registry values) so the Setup
3
+ // API, vswhere and vcvars find it again in seconds. The big folders (Visual Studio, the Windows SDK) stay on the data
4
+ // disk and are registered where they are; the small ones that programs look for at fixed paths are copied back. Pure:
5
+ // no runtime, so the self-test loads it.
6
+ import path from 'node:path';
7
+ import { z } from 'zod';
8
+ const win = path.win32;
9
+ /** The marker file in the saved copy's folder, written last. */
10
+ export const MARKER_FILE = '.complete.json';
11
+ /** The Setup Configuration COM class (vswhere and UnrealBuildTool find Visual Studio through it). */
12
+ export const SETUP_CLSID = '{177F0C4A-1CD3-4DE7-A32C-71DBBB9FA36D}';
13
+ const SETUP_DLL = 'Microsoft.VisualStudio.Setup.Configuration.Native.dll';
14
+ /** A Visual Studio instance ID (8 hex digits in practice); never a path. */
15
+ const INSTANCE_ID = /^[A-Za-z0-9]{1,64}$/;
16
+ export function winPlaces(env) {
17
+ return {
18
+ programFiles: env.ProgramFiles ?? 'C:\\Program Files',
19
+ programFilesX86: env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)',
20
+ programData: env.ProgramData ?? 'C:\\ProgramData',
21
+ };
22
+ }
23
+ export const FOLDER_NAMES = [
24
+ 'Community',
25
+ 'BuildTools',
26
+ 'WindowsKits10',
27
+ 'NETFXSDK',
28
+ 'MicrosoftSDKs-v10.0A',
29
+ 'ReferenceAssemblies',
30
+ 'Installer',
31
+ 'Shared',
32
+ 'Setup',
33
+ 'instance',
34
+ ];
35
+ export function productFolder(product) {
36
+ return product === 'community' ? 'Community' : 'BuildTools';
37
+ }
38
+ /** The instances folder of the Visual Studio installer (one subfolder per instance ID). */
39
+ export function instancesDir(places) {
40
+ return win.join(places.programData, 'Microsoft', 'VisualStudio', 'Packages', '_Instances');
41
+ }
42
+ /** Every folder a Visual Studio install spreads over, and what a boot does with each. */
43
+ export function persistFolders(vs, places) {
44
+ if (!INSTANCE_ID.test(vs.instanceId))
45
+ throw new Error('"' + vs.instanceId + '" is not a Visual Studio instance ID');
46
+ const x86 = places.programFilesX86;
47
+ const one = (name, source, restore, optional = false) => ({
48
+ name,
49
+ source,
50
+ restore,
51
+ optional,
52
+ });
53
+ return [
54
+ one(productFolder(vs.product), vs.installationPath, 'stay'),
55
+ one('WindowsKits10', win.join(x86, 'Windows Kits', '10'), 'stay'),
56
+ one('NETFXSDK', win.join(x86, 'Windows Kits', 'NETFXSDK'), 'stay', true),
57
+ one('MicrosoftSDKs-v10.0A', win.join(x86, 'Microsoft SDKs', 'Windows', 'v10.0A'), 'stay', true),
58
+ // MSBuild looks for the .NET Framework reference assemblies at this fixed path.
59
+ one('ReferenceAssemblies', win.join(x86, 'Reference Assemblies', 'Microsoft', 'Framework', '.NETFramework'), 'copy-missing', true),
60
+ // Scripts and build tools look for vswhere here; the instance's state names setup.exe here too (144 MB).
61
+ one('Installer', win.join(x86, 'Microsoft Visual Studio', 'Installer'), 'copy'),
62
+ one('Shared', win.join(x86, 'Microsoft Visual Studio', 'Shared'), 'copy', true),
63
+ one('Setup', win.join(places.programData, 'Microsoft', 'VisualStudio', 'Setup'), 'copy'),
64
+ one('instance', win.join(instancesDir(places), vs.instanceId), 'copy'),
65
+ ];
66
+ }
67
+ export const RegEntrySchema = z.strictObject({
68
+ view: z.enum(['64', '32']),
69
+ /** Under HKLM, e.g. SOFTWARE\Microsoft\Microsoft SDKs\Windows\v10.0 (the view picks WOW6432Node). */
70
+ key: z.string().min(1).max(400),
71
+ /** '' is the default value. */
72
+ name: z.string().max(200),
73
+ kind: z.enum(['string', 'expand', 'dword']),
74
+ value: z.union([z.string().max(2000), z.number().int()]),
75
+ });
76
+ /** The registry values that point at the saved folders, read in both views at capture time. */
77
+ export const REGISTRY_READS = [
78
+ { key: 'SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots', names: ['KitsRoot10'], recurse: false },
79
+ { key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0', names: null, recurse: false },
80
+ { key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\NETFXSDK', names: null, recurse: true },
81
+ { key: 'SOFTWARE\\Microsoft\\VisualStudio\\Setup', names: null, recurse: false },
82
+ ];
83
+ const VIEWS = ['64', '32'];
84
+ export const VsPersistMarkerSchema = z.strictObject({
85
+ product: z.enum(['community', 'buildtools']),
86
+ /** The installed version (17.14.37827.9). */
87
+ version: z.string().min(1).max(100),
88
+ /** The component IDs it was installed with. */
89
+ components: z.array(z.string().max(200)).max(500),
90
+ instanceId: z.string().regex(INSTANCE_ID),
91
+ /** Where the installer put it (the paths in its state and the registry are rewritten from this). */
92
+ installationPath: z.string().min(3).max(400),
93
+ /** The Windows SDK's ProductVersion (10.0.22621), '' when there is none. */
94
+ sdkVersion: z.string().max(50),
95
+ folders: z.array(z.enum(FOLDER_NAMES)).max(FOLDER_NAMES.length),
96
+ /** The registry values as the installer wrote them (paths on C:). */
97
+ registry: z.array(RegEntrySchema).max(500),
98
+ capturedAt: z.string().max(50),
99
+ });
100
+ /** What save-visual-studio.ps1 writes to its result file. */
101
+ export const SaveResultSchema = z.strictObject({
102
+ copied: z.array(z.enum(FOLDER_NAMES)).max(FOLDER_NAMES.length),
103
+ registry: z.array(RegEntrySchema).max(500),
104
+ });
105
+ /** Without these a saved copy is not usable (the product, its instance, the Setup API, vswhere, the SDK). */
106
+ const needed = (product) => [
107
+ productFolder(product),
108
+ 'instance',
109
+ 'Setup',
110
+ 'Installer',
111
+ 'WindowsKits10',
112
+ ];
113
+ /**
114
+ * Whether the saved copy serves this request: the same product with every component, or Community for a Build Tools
115
+ * request (it holds the same C++ parts; only the workload IDs differ).
116
+ */
117
+ export function persistCovers(marker, req) {
118
+ const same = marker.product === req.product;
119
+ if (!same && !(marker.product === 'community' && req.product === 'buildtools'))
120
+ return false;
121
+ if (!needed(marker.product).every((f) => marker.folders.includes(f)))
122
+ return false;
123
+ const have = new Set(marker.components.map((c) => c.toLowerCase()));
124
+ return req.components.filter((id) => same || !/\.workload\./i.test(id)).every((id) => have.has(id.toLowerCase()));
125
+ }
126
+ /** A string as it appears inside a JSON string (backslashes doubled). */
127
+ const jsonEscaped = (s) => JSON.stringify(s).slice(1, -1);
128
+ /**
129
+ * The instance's state.json with every mention of the install folder (JSON-escaped, any case) moved to `to`. A longer
130
+ * folder name that only starts the same (Community2) is left alone.
131
+ */
132
+ export function rewriteInstanceState(text, from, to) {
133
+ const needle = jsonEscaped(win.normalize(from).replace(/\\+$/, ''));
134
+ const replacement = jsonEscaped(win.normalize(to).replace(/\\+$/, ''));
135
+ const lower = text.toLowerCase();
136
+ const find = needle.toLowerCase();
137
+ let out = '';
138
+ let at = 0;
139
+ for (let i = lower.indexOf(find); i !== -1; i = lower.indexOf(find, i + find.length)) {
140
+ const next = text.slice(i + find.length, i + find.length + 2);
141
+ if (next !== '\\\\' && !next.startsWith('"'))
142
+ continue;
143
+ out += text.slice(at, i) + replacement;
144
+ at = i + find.length;
145
+ }
146
+ return out + text.slice(at);
147
+ }
148
+ /** `value` with a saved folder's old place swapped for its place on the data disk (case-insensitive prefix). */
149
+ export function rebase(value, moves) {
150
+ for (const move of moves) {
151
+ const from = move.from.replace(/\\+$/, '');
152
+ const lower = value.toLowerCase();
153
+ if (lower === from.toLowerCase() || lower.startsWith(from.toLowerCase() + '\\'))
154
+ return move.to.replace(/\\+$/, '') + value.slice(from.length);
155
+ }
156
+ return value;
157
+ }
158
+ /** Whether a registry value from the marker is one the capture reads (a tampered marker writes nothing else). */
159
+ const allowed = (entry) => REGISTRY_READS.some((read) => {
160
+ const key = entry.key.toLowerCase();
161
+ const root = read.key.toLowerCase();
162
+ const under = key === root || (read.recurse && key.startsWith(root + '\\'));
163
+ return under && (read.names === null || read.names.some((n) => n.toLowerCase() === entry.name.toLowerCase()));
164
+ });
165
+ /** What the boot writes under HKLM: the saved values (paths moved to the data disk) in both views, the COM class. */
166
+ export function registryPlan(marker, root, places) {
167
+ const folders = persistFolders(marker, places).filter((f) => marker.folders.includes(f.name));
168
+ const moves = folders
169
+ .filter((f) => f.restore === 'stay')
170
+ .map((f) => ({ from: f.source, to: win.join(root, f.name) }));
171
+ const values = new Map();
172
+ const put = (e) => {
173
+ values.set(e.key.toLowerCase() + '|' + e.name.toLowerCase(), e);
174
+ };
175
+ for (const e of marker.registry.filter(allowed))
176
+ put({
177
+ key: e.key,
178
+ name: e.name,
179
+ kind: e.kind,
180
+ value: typeof e.value === 'string' ? rebase(e.value, moves) : e.value,
181
+ });
182
+ if (marker.folders.includes('WindowsKits10')) {
183
+ const kits = win.join(root, 'WindowsKits10') + '\\';
184
+ const sdk = 'SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0';
185
+ put({ key: 'SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots', name: 'KitsRoot10', kind: 'string', value: kits });
186
+ put({ key: sdk, name: 'InstallationFolder', kind: 'string', value: kits });
187
+ if (marker.sdkVersion)
188
+ put({ key: sdk, name: 'ProductVersion', kind: 'string', value: marker.sdkVersion });
189
+ }
190
+ const both = [...values.values()].flatMap((e) => VIEWS.map((view) => ({ view, ...e })));
191
+ const setup = win.join(places.programData, 'Microsoft', 'VisualStudio', 'Setup');
192
+ const com = (view, arch) => {
193
+ const key = 'SOFTWARE\\Classes\\CLSID\\' + SETUP_CLSID + '\\InprocServer32';
194
+ return [
195
+ { view, key, name: '', kind: 'string', value: win.join(setup, arch, SETUP_DLL) },
196
+ { view, key, name: 'ThreadingModel', kind: 'string', value: 'Both' },
197
+ ];
198
+ };
199
+ return [...both, ...com('64', 'x64'), ...com('32', 'x86')];
200
+ }
201
+ /** Compares dotted versions (17.14.37827.9) part by part, as numbers. */
202
+ function compareVersions(a, b) {
203
+ const x = a.split('.').map((p) => parseInt(p, 10) || 0);
204
+ const y = b.split('.').map((p) => parseInt(p, 10) || 0);
205
+ for (let i = 0; i < Math.max(x.length, y.length); i++)
206
+ if ((x[i] ?? 0) !== (y[i] ?? 0))
207
+ return (x[i] ?? 0) - (y[i] ?? 0);
208
+ return 0;
209
+ }
210
+ /**
211
+ * The newest of the instances vswhere lists. Chosen here, not with vswhere's -latest, which fails outright (0x8000FFFF)
212
+ * when two instances have the same install date, as a copy of an instance does.
213
+ */
214
+ export function newestInstance(instances) {
215
+ return ([...instances].sort((a, b) => compareVersions(b.installationVersion ?? '', a.installationVersion ?? ''))[0] ?? null);
216
+ }
217
+ /** The input of save-visual-studio.ps1: the folders to mirror onto the data disk and the registry values to read. */
218
+ export function saveInput(vs, root, places, resultFile) {
219
+ return {
220
+ copies: persistFolders(vs, places).map((f) => ({
221
+ name: f.name,
222
+ from: f.source,
223
+ to: win.join(root, f.name),
224
+ optional: f.optional,
225
+ })),
226
+ registry: VIEWS.flatMap((view) => REGISTRY_READS.map((r) => ({ view, ...r }))),
227
+ resultFile,
228
+ };
229
+ }
230
+ /** The marker for a finished capture (written after the instance's state is rewritten). */
231
+ export function markerFrom(req, vs, result, capturedAt) {
232
+ const sdk = result.registry.find((e) => /\\Microsoft SDKs\\Windows\\v10\.0$/i.test(e.key) && e.name.toLowerCase() === 'productversion');
233
+ return {
234
+ product: req.product,
235
+ version: vs.version,
236
+ components: [...req.components],
237
+ instanceId: vs.instanceId,
238
+ installationPath: vs.installationPath,
239
+ sdkVersion: typeof sdk?.value === 'string' ? sdk.value : '',
240
+ folders: [...result.copied],
241
+ registry: [...result.registry],
242
+ capturedAt,
243
+ };
244
+ }
245
+ /** The input of restore-visual-studio.ps1: the folders copied back to their fixed places, then the registry values. */
246
+ export function restoreInput(marker, root, places) {
247
+ const copies = persistFolders(marker, places)
248
+ .filter((f) => f.restore !== 'stay' && marker.folders.includes(f.name))
249
+ .map((f) => ({
250
+ name: f.name,
251
+ from: win.join(root, f.name),
252
+ to: f.source,
253
+ mode: f.restore === 'copy-missing' ? 'missing' : 'all',
254
+ }));
255
+ return { copies, registry: registryPlan(marker, root, places) };
256
+ }
@@ -1,12 +1,17 @@
1
1
  import type { Toolchains } from '../../../core/types.js';
2
2
  /**
3
3
  * What Epic lists for Unreal Engine 5 with Visual Studio 2022: the game workload, MSVC v143, a Windows SDK of
4
- * 10.0.18362 or newer, the C++ profiling tools, the .NET Framework targeting packs some engine tools build against,
4
+ * 10.0.22621 or newer, the C++ profiling tools, the .NET Framework targeting packs some engine tools build against,
5
5
  * Visual Studio's Unreal support and the Unreal Engine installer (the Epic Games Launcher).
6
6
  */
7
7
  export declare const UNREAL_COMPONENTS: readonly ["Microsoft.VisualStudio.Workload.NativeGame", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "Microsoft.VisualStudio.Component.Windows11SDK.22621", "Microsoft.VisualStudio.Component.VC.DiagnosticTools", "Microsoft.Net.Component.4.6.2.TargetingPack", "Microsoft.Net.Component.4.8.SDK", "Component.Unreal.Ide", "Component.Unreal"];
8
8
  /** The Build Tools' C++ desktop parts: MSVC v143 and a Windows 11 SDK (Flutter desktop, Unity IL2CPP). */
9
9
  export declare const BUILD_TOOLS_COMPONENTS: readonly ["Microsoft.VisualStudio.Workload.VCTools", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "Microsoft.VisualStudio.Component.Windows11SDK.22621"];
10
+ /**
11
+ * Unreal on the Build Tools: what UnrealBuildTool needs (it finds them through the Setup API like Visual Studio):
12
+ * MSVC v143, the Windows SDK and the .NET Framework parts its tools build against. No IDE and no debugger.
13
+ */
14
+ export declare const UNREAL_BUILD_TOOLS_COMPONENTS: readonly ["Microsoft.VisualStudio.Workload.VCTools", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "Microsoft.VisualStudio.Component.Windows11SDK.22621", "Microsoft.Net.Component.4.6.2.TargetingPack", "Microsoft.Net.Component.4.8.SDK"];
10
15
  export interface VsRequest {
11
16
  readonly product: 'community' | 'buildtools';
12
17
  readonly components: readonly string[];
@@ -17,26 +22,5 @@ export interface VsRequest {
17
22
  readonly accepted: boolean;
18
23
  }
19
24
  export declare function visualStudioWanted(t: Toolchains, on?: (setting: boolean | 'auto', name: string) => boolean): VsRequest | null;
20
- /** Whether Unreal is on and builds from the portable MSVC through AutoSDK instead of Visual Studio. */
21
- export declare function unrealUsesAutosdk(t: Toolchains, on?: (setting: boolean | 'auto', name: string) => boolean): boolean;
22
25
  /** The Visual Studio installer's exit code in plain words (it is a uint32: -1 and 4294967295 are the same). */
23
26
  export declare function vsExitText(code: number): string;
24
- /** Room the staging copy of the layout needs on the sandbox's own disk, on top of the reserve. */
25
- export declare const LAYOUT_STAGING_BYTES: number;
26
- /** Spare room on the sandbox's disk while the layout is built there (before Visual Studio is installed). */
27
- export declare const STAGING_RESERVE_BYTES: number;
28
- /**
29
- * What to do after the layout attempt in the data disk: 'done' when it worked, 'stage' (build it on the sandbox's
30
- * own disk and copy it over) when it failed and that disk has room, 'skip' (download this boot) otherwise.
31
- */
32
- export declare function layoutNextStep(exit: number | null, localFreeBytes: number, reserveBytes: number): 'done' | 'stage' | 'skip';
33
- export interface CopyPair {
34
- readonly from: string;
35
- readonly to: string;
36
- }
37
- /**
38
- * The copies that make an Unreal AutoSDK tree from the portable MSVC (copies, not links: mapped folders refuse
39
- * junctions): MSVC's tools into HostWin64\Win64\VS2022\<msvc> and the Windows SDK's Include, Lib and bin folders into
40
- * HostWin64\Win64\Windows Kits\10. Pure.
41
- */
42
- export declare function autosdkLayout(msvcRoot: string, msvc: string, sdk: string, autosdkRoot: string): CopyPair[];
@@ -1,11 +1,6 @@
1
- // Which Visual Studio this sandbox installs (a real installer: it runs every boot), from every toolchain that needs
2
- // one: Unreal (Community with the C++ game workload), Flutter's Windows desktop builds and Unity's Windows IL2CPP
3
- // builds (the Build Tools' C++ parts). Pure: no runtime, so the self-test loads it. Community holds everything the
4
- // Build Tools would add, so one install serves all of them.
5
- import path from 'node:path';
6
1
  /**
7
2
  * What Epic lists for Unreal Engine 5 with Visual Studio 2022: the game workload, MSVC v143, a Windows SDK of
8
- * 10.0.18362 or newer, the C++ profiling tools, the .NET Framework targeting packs some engine tools build against,
3
+ * 10.0.22621 or newer, the C++ profiling tools, the .NET Framework targeting packs some engine tools build against,
9
4
  * Visual Studio's Unreal support and the Unreal Engine installer (the Epic Games Launcher).
10
5
  */
11
6
  export const UNREAL_COMPONENTS = [
@@ -24,24 +19,34 @@ export const BUILD_TOOLS_COMPONENTS = [
24
19
  'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
25
20
  'Microsoft.VisualStudio.Component.Windows11SDK.22621',
26
21
  ];
22
+ /**
23
+ * Unreal on the Build Tools: what UnrealBuildTool needs (it finds them through the Setup API like Visual Studio):
24
+ * MSVC v143, the Windows SDK and the .NET Framework parts its tools build against. No IDE and no debugger.
25
+ */
26
+ export const UNREAL_BUILD_TOOLS_COMPONENTS = [
27
+ ...BUILD_TOOLS_COMPONENTS,
28
+ 'Microsoft.Net.Component.4.6.2.TargetingPack',
29
+ 'Microsoft.Net.Component.4.8.SDK',
30
+ ];
27
31
  /** Whether a toolchain is on (the host resolved "auto"; a session file that still says "auto" counts as off). */
28
32
  const isOn = (setting) => setting === true;
29
33
  export function visualStudioWanted(t, on = isOn) {
30
- // Unreal with visualStudio "autosdk" needs no Visual Studio: it builds from the portable MSVC (autosdk.ts).
31
- const unreal = on(t.unreal.enabled, 'unreal') && t.unreal.visualStudio === 'full';
34
+ const unreal = on(t.unreal.enabled, 'unreal');
32
35
  const flutter = on(t.flutter.enabled, 'flutter') && t.flutter.windowsDesktop;
33
36
  const unity = on(t.unity.enabled, 'unity') && t.unity.modules.includes('windows-il2cpp');
34
37
  const users = [...(unreal ? ['unreal'] : []), ...(flutter ? ['flutter'] : []), ...(unity ? ['unity'] : [])];
35
38
  if (!users.length)
36
39
  return null;
37
- if (unreal)
40
+ if (unreal) {
41
+ const full = t.unreal.visualStudio === 'full';
38
42
  return {
39
- product: 'community',
40
- components: [...new Set([...UNREAL_COMPONENTS, ...t.unreal.components])],
43
+ product: full ? 'community' : 'buildtools',
44
+ components: [...new Set([...(full ? UNREAL_COMPONENTS : UNREAL_BUILD_TOOLS_COMPONENTS), ...t.unreal.components])],
41
45
  for: users,
42
46
  licence: 'toolchains.unreal.acceptLicense',
43
47
  accepted: t.unreal.acceptLicense,
44
48
  };
49
+ }
45
50
  return {
46
51
  product: 'buildtools',
47
52
  components: [...BUILD_TOOLS_COMPONENTS],
@@ -50,10 +55,6 @@ export function visualStudioWanted(t, on = isOn) {
50
55
  accepted: t.msvc.acceptLicense,
51
56
  };
52
57
  }
53
- /** Whether Unreal is on and builds from the portable MSVC through AutoSDK instead of Visual Studio. */
54
- export function unrealUsesAutosdk(t, on = isOn) {
55
- return on(t.unreal.enabled, 'unreal') && t.unreal.visualStudio === 'autosdk';
56
- }
57
58
  /** The Visual Studio installer's exit code in plain words (it is a uint32: -1 and 4294967295 are the same). */
58
59
  export function vsExitText(code) {
59
60
  const c = code >>> 0;
@@ -74,34 +75,3 @@ export function vsExitText(code) {
74
75
  return 'the installer failed with exit ' + c + ' (0x' + c.toString(16).toUpperCase() + ')';
75
76
  }
76
77
  }
77
- /** Room the staging copy of the layout needs on the sandbox's own disk, on top of the reserve. */
78
- export const LAYOUT_STAGING_BYTES = 12 * 1024 ** 3;
79
- /** Spare room on the sandbox's disk while the layout is built there (before Visual Studio is installed). */
80
- export const STAGING_RESERVE_BYTES = 3 * 1024 ** 3;
81
- /**
82
- * What to do after the layout attempt in the data disk: 'done' when it worked, 'stage' (build it on the sandbox's
83
- * own disk and copy it over) when it failed and that disk has room, 'skip' (download this boot) otherwise.
84
- */
85
- export function layoutNextStep(exit, localFreeBytes, reserveBytes) {
86
- if (exit !== null && (exit === 0 || exit === 3010))
87
- return 'done';
88
- return localFreeBytes >= LAYOUT_STAGING_BYTES + reserveBytes ? 'stage' : 'skip';
89
- }
90
- /**
91
- * The copies that make an Unreal AutoSDK tree from the portable MSVC (copies, not links: mapped folders refuse
92
- * junctions): MSVC's tools into HostWin64\Win64\VS2022\<msvc> and the Windows SDK's Include, Lib and bin folders into
93
- * HostWin64\Win64\Windows Kits\10. Pure.
94
- */
95
- export function autosdkLayout(msvcRoot, msvc, sdk, autosdkRoot) {
96
- const win64 = path.win32.join(autosdkRoot, 'HostWin64', 'Win64');
97
- return [
98
- {
99
- from: path.win32.join(msvcRoot, 'VC', 'Tools', 'MSVC', msvc),
100
- to: path.win32.join(win64, 'VS2022', msvc),
101
- },
102
- ...['Include', 'Lib', 'bin'].map((part) => ({
103
- from: path.win32.join(msvcRoot, 'Windows Kits', '10', part, sdk),
104
- to: path.win32.join(win64, 'Windows Kits', '10', part, sdk),
105
- })),
106
- ];
107
- }
@@ -21,7 +21,7 @@ import { fetchPhp, setupPhp } from './languages/php.js';
21
21
  import { setupRuby } from './languages/ruby.js';
22
22
  import { fetchVcRuntime, setupVcRuntime } from './native/vcruntime.js';
23
23
  import { setupVscode } from './tools/vscode.js';
24
- import { ensureVisualStudio, prefetchVisualStudio, unrealUsesAutosdk, visualStudioWanted, } from './native/visual-studio.js';
24
+ import { ensureVisualStudio, prefetchVisualStudio, visualStudioSaved, visualStudioWanted, } from './native/visual-studio.js';
25
25
  import { duringSession } from '../runtime/project-context.js';
26
26
  import { setupSteam } from './steam/steam.js';
27
27
  import { setupGodot } from './engines/godot.js';
@@ -42,8 +42,6 @@ export function plan(config, versions, o) {
42
42
  const android = wanted(config.android.enabled, 'android');
43
43
  // One Visual Studio for every toolchain that needs the real one (Unreal, Flutter desktop, Unity IL2CPP).
44
44
  const vs = visualStudioWanted(config, (setting, name) => wanted(setting, name));
45
- // Unreal on the portable MSVC (AutoSDK) needs that MSVC even when nothing else asked for it.
46
- const autosdk = unrealUsesAutosdk(config, (setting, name) => wanted(setting, name));
47
45
  const ctx = {
48
46
  refresh: o.refresh,
49
47
  pcInstalls: o.pcInstalls ?? { items: [] },
@@ -69,32 +67,30 @@ export function plan(config, versions, o) {
69
67
  enabled: wanted(config.cpp.enabled, 'cpp') || wanted(config.rust.enabled, 'rust'),
70
68
  install: (env) => setupCpp(env, config.cpp),
71
69
  },
72
- // Visual Studio brings MSVC and the real vswhere: the portable MSVC (and its stand-in vswhere) stays out.
70
+ // Visual Studio brings MSVC and the real vswhere: the portable MSVC (and its stand-in vswhere) stays out. Only the
71
+ // first install goes to the background: a saved copy is back in seconds, before the agent starts.
73
72
  {
74
73
  name: 'Visual Studio',
75
74
  enabled: vs !== null,
76
75
  shared: true,
77
- background: deferred,
76
+ background: deferred && !visualStudioSaved(vs),
78
77
  ...ahead(() => prefetchVisualStudio(vs)),
79
78
  install: async (env) => {
80
- await ensureVisualStudio(env, vs, { keepInstallFiles: config.unreal.keepInstallFiles, lowPriority: deferred });
79
+ await ensureVisualStudio(env, vs, { lowPriority: deferred });
81
80
  },
82
81
  },
83
82
  {
84
83
  name: 'Unreal',
85
84
  enabled: unreal,
86
85
  shared: true,
87
- after: ['Visual Studio', 'MSVC'],
86
+ after: ['Visual Studio'],
88
87
  ...ahead(() => fetchDirectX(config.unreal)),
89
88
  install: (env) => setupUnreal(env, config.unreal, ctx),
90
89
  },
91
90
  {
92
91
  name: 'MSVC',
93
- enabled: (wanted(config.msvc.enabled, 'msvc') || (autosdk && config.msvc.enabled !== false)) && vs === null,
94
- // The same Visual Studio licence terms, which Unreal's acceptLicense already names.
95
- install: (env) => setupMsvc(env, autosdk
96
- ? { ...config.msvc, acceptLicense: config.msvc.acceptLicense || config.unreal.acceptLicense }
97
- : config.msvc),
92
+ enabled: wanted(config.msvc.enabled, 'msvc') && vs === null,
93
+ install: (env) => setupMsvc(env, config.msvc),
98
94
  },
99
95
  // Rust links with MSVC (msvc host) or MinGW's gcc (GNU host): whichever it uses goes first.
100
96
  {
@@ -167,10 +163,11 @@ export function plan(config, versions, o) {
167
163
  },
168
164
  { name: 'Ruby', enabled: wanted(config.ruby.enabled, 'ruby'), install: (env) => setupRuby(env, config.ruby) },
169
165
  // PHP installs the Visual C++ runtime itself: one installer at a time.
170
- // Many apps (bat, delta, Neovim, most GUIs, your own programs) are built against it too.
166
+ // Many apps (bat, delta, Neovim, most GUIs, your own programs) are built against it too, and so is what Visual
167
+ // Studio builds: a Visual Studio put back from the data disk does not bring it, as its installer did.
171
168
  {
172
169
  name: 'Visual C++ runtime',
173
- enabled: config.vcRuntime.enabled || apps,
170
+ enabled: config.vcRuntime.enabled || apps || vs !== null,
174
171
  shared: true,
175
172
  ...ahead(fetchVcRuntime),
176
173
  install: async (env) => {
@@ -0,0 +1,5 @@
1
+ import type { Migration } from '../migration.js';
2
+ import { type Json } from './config-file.js';
3
+ /** The retired values in a config file. */
4
+ export declare function retiredVsSettings(file: Json): string[][];
5
+ export declare const dropVsInstallFiles: Migration;
@@ -0,0 +1,34 @@
1
+ // 1.24 keeps Visual Studio itself on the data disk, so two Unreal settings are gone: toolchains.unreal.keepInstallFiles
2
+ // (the offline install files, which the installer never managed to make in a sandbox) and visualStudio "autosdk" (the
3
+ // portable MSVC instead of Visual Studio, which nothing needs now that Visual Studio is put back in seconds). A config
4
+ // file that still has them would no longer load: they are removed (the rest and the comments stay).
5
+ import _ from 'lodash';
6
+ import { readFileObject, removeValues } from './config-file.js';
7
+ const KEEP_INSTALL_FILES = ['toolchains', 'unreal', 'keepInstallFiles'];
8
+ const VISUAL_STUDIO = ['toolchains', 'unreal', 'visualStudio'];
9
+ /** The retired values in a config file. */
10
+ export function retiredVsSettings(file) {
11
+ return [
12
+ ...(_.has(file, KEEP_INSTALL_FILES) ? [KEEP_INSTALL_FILES] : []),
13
+ ...(_.get(file, VISUAL_STUDIO) === 'autosdk' ? [VISUAL_STUDIO] : []),
14
+ ];
15
+ }
16
+ export const dropVsInstallFiles = {
17
+ id: '2026-11-drop-vs-install-files',
18
+ introducedIn: '1.24.0',
19
+ scope: 'user',
20
+ needsIdleSandbox: false,
21
+ summary: 'Removes toolchains.unreal.keepInstallFiles and visualStudio "autosdk": Visual Studio is now kept on the data disk.',
22
+ applies(ctx) {
23
+ const file = readFileObject(ctx.configPath);
24
+ return file !== null && retiredVsSettings(file).length > 0;
25
+ },
26
+ touches(ctx) {
27
+ return ctx.configPath ? [ctx.configPath] : [];
28
+ },
29
+ async run(ctx, note) {
30
+ const parsed = readFileObject(ctx.configPath);
31
+ if (ctx.configPath && parsed)
32
+ removeValues(ctx.configPath, retiredVsSettings(parsed), 'No longer needed (Visual Studio is kept)', note);
33
+ },
34
+ };
@@ -1,4 +1,10 @@
1
+ import { dropVsInstallFiles } from './migrations/drop-vs-install-files.js';
1
2
  import { followMachineStorage } from './migrations/follow-machine-storage.js';
2
3
  import { pinStorageRoot } from './migrations/pin-storage-root.js';
3
4
  import { unpinConfigDefaults } from './migrations/unpin-config-defaults.js';
4
- export const MIGRATIONS = [unpinConfigDefaults, followMachineStorage, pinStorageRoot];
5
+ export const MIGRATIONS = [
6
+ unpinConfigDefaults,
7
+ followMachineStorage,
8
+ pinStorageRoot,
9
+ dropVsInstallFiles,
10
+ ];