windows-development-sandbox 1.23.2 → 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.
- package/config.example.json +0 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-USTRSHIE.js → chunk-IZRGTSQ5.js} +158 -131
- package/dist/bin/{chunk-QFSJX2KW.js → chunk-KH4MI2U5.js} +1 -1
- package/dist/bin/{chunk-7YPEDAJD.js → chunk-SUYYVIDW.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/core/config/defaults.js +0 -1
- package/dist/core/detect/toolchains.js +0 -6
- package/dist/core/schema/config.d.ts +1 -2
- package/dist/core/schema/toolchains.d.ts +1 -2
- package/dist/core/schema/toolchains.js +3 -6
- package/dist/guest/agents/agent-files.d.ts +11 -7
- package/dist/guest/agents/agent-files.js +37 -11
- package/dist/guest/agents/local-state.d.ts +2 -1
- package/dist/guest/agents/local-state.js +2 -1
- package/dist/guest/boot/boot-console.d.ts +28 -0
- package/dist/guest/boot/boot-console.js +98 -0
- package/dist/guest/boot/boot.d.ts +8 -1
- package/dist/guest/boot/boot.js +66 -42
- package/dist/guest/boot/graph.d.ts +9 -5
- package/dist/guest/boot/graph.js +8 -2
- package/dist/guest/boot/progress-view.d.ts +116 -0
- package/dist/guest/boot/progress-view.js +456 -0
- package/dist/guest/boot/runner.d.ts +20 -10
- package/dist/guest/boot/runner.js +39 -47
- package/dist/guest/runtime/log-line.d.ts +7 -0
- package/dist/guest/runtime/log-line.js +18 -0
- package/dist/guest/runtime/log.d.ts +17 -4
- package/dist/guest/runtime/log.js +40 -17
- package/dist/guest/runtime/process.d.ts +9 -0
- package/dist/guest/runtime/process.js +69 -8
- package/dist/guest/setup/environment.js +9 -4
- package/dist/guest/setup/hooks.d.ts +2 -0
- package/dist/guest/setup/hooks.js +4 -0
- package/dist/guest/setup/path-plan.d.ts +12 -0
- package/dist/guest/setup/path-plan.js +18 -0
- package/dist/guest/toolchains/native/unreal.js +4 -6
- package/dist/guest/toolchains/native/visual-studio.d.ts +14 -14
- package/dist/guest/toolchains/native/visual-studio.js +145 -174
- package/dist/guest/toolchains/native/vs-persist.d.ts +189 -0
- package/dist/guest/toolchains/native/vs-persist.js +256 -0
- package/dist/guest/toolchains/native/vs-request.d.ts +6 -26
- package/dist/guest/toolchains/native/vs-request.js +16 -50
- package/dist/guest/toolchains/plan.js +11 -14
- package/dist/host/migrate/migrations/drop-vs-install-files.d.ts +5 -0
- package/dist/host/migrate/migrations/drop-vs-install-files.js +34 -0
- package/dist/host/migrate/registry.js +7 -1
- package/dist/runtime/agent.js +67 -26
- package/dist/runtime/daemon.js +958 -716
- package/dist/runtime/db.js +67 -26
- package/dist/runtime/main.js +1792 -1038
- package/dist/runtime/netproxy.js +3 -6
- package/dist/runtime/tool.js +44 -24
- package/dist/runtime/view.js +4 -7
- package/dist/selftest/agent-paths.test.js +17 -1
- package/dist/selftest/agent-settings.test.js +36 -5
- package/dist/selftest/boot-console.test.d.ts +2 -0
- package/dist/selftest/boot-console.test.js +291 -0
- package/dist/selftest/boot-runner.test.js +10 -12
- package/dist/selftest/guest-boot.test.js +7 -1
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/layout.test.js +4 -0
- package/dist/selftest/migrate.test.js +21 -0
- package/dist/selftest/persisted-installs.test.js +169 -33
- package/dist/selftest/support/engine-golden.js +2 -0
- package/dist/selftest/support/fixtures.d.ts +1 -2
- package/dist/shared/channel/schema.d.ts +3 -6
- package/dist/shared/pty/terminal.js +2 -1
- package/dist/sync/watcher.js +5 -1
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- package/sandbox/scripts/restore-visual-studio.ps1 +78 -0
- package/sandbox/scripts/save-visual-studio.ps1 +106 -0
- package/schemas/config.schema.json +3 -7
- package/schemas/sandbox-config.schema.json +3 -7
- package/dist/guest/toolchains/native/autosdk.d.ts +0 -3
- package/dist/guest/toolchains/native/autosdk.js +0 -50
|
@@ -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
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// (
|
|
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 {
|
|
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 {
|
|
28
|
-
|
|
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
|
-
|
|
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 *
|
|
44
|
-
buildtools: 5 *
|
|
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
|
|
53
|
-
const
|
|
54
|
-
/**
|
|
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
|
|
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
|
-
'-
|
|
65
|
-
|
|
71
|
+
'-format',
|
|
72
|
+
'json',
|
|
73
|
+
'-utf8',
|
|
66
74
|
]);
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
119
|
-
|
|
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
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
'
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
|
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
|
|
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,
|
|
167
|
+
async function install(req, lowPriority) {
|
|
196
168
|
const boot = BOOTSTRAPPERS[req.product];
|
|
197
169
|
const bootstrapper = await bootstrapperFor(req);
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
/**
|
|
231
|
-
|
|
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],
|
|
249
|
-
await install(req, o.
|
|
227
|
+
assertLocalRoom(INSTALL_BYTES[req.product], name);
|
|
228
|
+
await install(req, o.lowPriority === true);
|
|
250
229
|
const vs = installedVs();
|
|
251
|
-
|
|
252
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { VsRequest } from './vs-request.js';
|
|
3
|
+
/** The marker file in the saved copy's folder, written last. */
|
|
4
|
+
export declare const MARKER_FILE = ".complete.json";
|
|
5
|
+
/** The Setup Configuration COM class (vswhere and UnrealBuildTool find Visual Studio through it). */
|
|
6
|
+
export declare const SETUP_CLSID = "{177F0C4A-1CD3-4DE7-A32C-71DBBB9FA36D}";
|
|
7
|
+
/** The Windows folders an install uses (from the environment; the tests pass their own). */
|
|
8
|
+
export interface WinPlaces {
|
|
9
|
+
readonly programFiles: string;
|
|
10
|
+
readonly programFilesX86: string;
|
|
11
|
+
readonly programData: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function winPlaces(env: NodeJS.ProcessEnv): WinPlaces;
|
|
14
|
+
export declare const FOLDER_NAMES: readonly ["Community", "BuildTools", "WindowsKits10", "NETFXSDK", "MicrosoftSDKs-v10.0A", "ReferenceAssemblies", "Installer", "Shared", "Setup", "instance"];
|
|
15
|
+
export type FolderName = (typeof FOLDER_NAMES)[number];
|
|
16
|
+
/**
|
|
17
|
+
* What a boot does with a saved folder: 'stay' on the data disk (registered there), 'copy' back to where it came from
|
|
18
|
+
* (programs look for it at a fixed path), or 'copy-missing': only the versions not there yet (the reference assemblies,
|
|
19
|
+
* where the .NET step may have put some).
|
|
20
|
+
*/
|
|
21
|
+
export type RestoreMode = 'stay' | 'copy' | 'copy-missing';
|
|
22
|
+
export interface PersistFolder {
|
|
23
|
+
readonly name: FolderName;
|
|
24
|
+
/** Where the install put it. */
|
|
25
|
+
readonly source: string;
|
|
26
|
+
readonly restore: RestoreMode;
|
|
27
|
+
/** Not every install has it (the Build Tools without .NET parts): skipped when missing. */
|
|
28
|
+
readonly optional: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function productFolder(product: VsRequest['product']): 'Community' | 'BuildTools';
|
|
31
|
+
/** The instances folder of the Visual Studio installer (one subfolder per instance ID). */
|
|
32
|
+
export declare function instancesDir(places: WinPlaces): string;
|
|
33
|
+
/** Every folder a Visual Studio install spreads over, and what a boot does with each. */
|
|
34
|
+
export declare function persistFolders(vs: {
|
|
35
|
+
readonly product: VsRequest['product'];
|
|
36
|
+
readonly installationPath: string;
|
|
37
|
+
readonly instanceId: string;
|
|
38
|
+
}, places: WinPlaces): PersistFolder[];
|
|
39
|
+
export declare const RegEntrySchema: z.ZodObject<{
|
|
40
|
+
view: z.ZodEnum<{
|
|
41
|
+
32: "32";
|
|
42
|
+
64: "64";
|
|
43
|
+
}>;
|
|
44
|
+
key: z.ZodString;
|
|
45
|
+
name: z.ZodString;
|
|
46
|
+
kind: z.ZodEnum<{
|
|
47
|
+
string: "string";
|
|
48
|
+
expand: "expand";
|
|
49
|
+
dword: "dword";
|
|
50
|
+
}>;
|
|
51
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
52
|
+
}, z.core.$strict>;
|
|
53
|
+
export type RegEntry = z.infer<typeof RegEntrySchema>;
|
|
54
|
+
export interface RegRead {
|
|
55
|
+
readonly view: '64' | '32';
|
|
56
|
+
readonly key: string;
|
|
57
|
+
/** Only these values (null: every string and DWORD value). */
|
|
58
|
+
readonly names: readonly string[] | null;
|
|
59
|
+
readonly recurse: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** The registry values that point at the saved folders, read in both views at capture time. */
|
|
62
|
+
export declare const REGISTRY_READS: readonly Omit<RegRead, 'view'>[];
|
|
63
|
+
export declare const VsPersistMarkerSchema: z.ZodObject<{
|
|
64
|
+
product: z.ZodEnum<{
|
|
65
|
+
buildtools: "buildtools";
|
|
66
|
+
community: "community";
|
|
67
|
+
}>;
|
|
68
|
+
version: z.ZodString;
|
|
69
|
+
components: z.ZodArray<z.ZodString>;
|
|
70
|
+
instanceId: z.ZodString;
|
|
71
|
+
installationPath: z.ZodString;
|
|
72
|
+
sdkVersion: z.ZodString;
|
|
73
|
+
folders: z.ZodArray<z.ZodEnum<{
|
|
74
|
+
Community: "Community";
|
|
75
|
+
BuildTools: "BuildTools";
|
|
76
|
+
WindowsKits10: "WindowsKits10";
|
|
77
|
+
NETFXSDK: "NETFXSDK";
|
|
78
|
+
"MicrosoftSDKs-v10.0A": "MicrosoftSDKs-v10.0A";
|
|
79
|
+
ReferenceAssemblies: "ReferenceAssemblies";
|
|
80
|
+
Installer: "Installer";
|
|
81
|
+
Shared: "Shared";
|
|
82
|
+
Setup: "Setup";
|
|
83
|
+
instance: "instance";
|
|
84
|
+
}>>;
|
|
85
|
+
registry: z.ZodArray<z.ZodObject<{
|
|
86
|
+
view: z.ZodEnum<{
|
|
87
|
+
32: "32";
|
|
88
|
+
64: "64";
|
|
89
|
+
}>;
|
|
90
|
+
key: z.ZodString;
|
|
91
|
+
name: z.ZodString;
|
|
92
|
+
kind: z.ZodEnum<{
|
|
93
|
+
string: "string";
|
|
94
|
+
expand: "expand";
|
|
95
|
+
dword: "dword";
|
|
96
|
+
}>;
|
|
97
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
98
|
+
}, z.core.$strict>>;
|
|
99
|
+
capturedAt: z.ZodString;
|
|
100
|
+
}, z.core.$strict>;
|
|
101
|
+
export type VsPersistMarker = z.infer<typeof VsPersistMarkerSchema>;
|
|
102
|
+
/** What save-visual-studio.ps1 writes to its result file. */
|
|
103
|
+
export declare const SaveResultSchema: z.ZodObject<{
|
|
104
|
+
copied: z.ZodArray<z.ZodEnum<{
|
|
105
|
+
Community: "Community";
|
|
106
|
+
BuildTools: "BuildTools";
|
|
107
|
+
WindowsKits10: "WindowsKits10";
|
|
108
|
+
NETFXSDK: "NETFXSDK";
|
|
109
|
+
"MicrosoftSDKs-v10.0A": "MicrosoftSDKs-v10.0A";
|
|
110
|
+
ReferenceAssemblies: "ReferenceAssemblies";
|
|
111
|
+
Installer: "Installer";
|
|
112
|
+
Shared: "Shared";
|
|
113
|
+
Setup: "Setup";
|
|
114
|
+
instance: "instance";
|
|
115
|
+
}>>;
|
|
116
|
+
registry: z.ZodArray<z.ZodObject<{
|
|
117
|
+
view: z.ZodEnum<{
|
|
118
|
+
32: "32";
|
|
119
|
+
64: "64";
|
|
120
|
+
}>;
|
|
121
|
+
key: z.ZodString;
|
|
122
|
+
name: z.ZodString;
|
|
123
|
+
kind: z.ZodEnum<{
|
|
124
|
+
string: "string";
|
|
125
|
+
expand: "expand";
|
|
126
|
+
dword: "dword";
|
|
127
|
+
}>;
|
|
128
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
129
|
+
}, z.core.$strict>>;
|
|
130
|
+
}, z.core.$strict>;
|
|
131
|
+
export type SaveResult = z.infer<typeof SaveResultSchema>;
|
|
132
|
+
/**
|
|
133
|
+
* Whether the saved copy serves this request: the same product with every component, or Community for a Build Tools
|
|
134
|
+
* request (it holds the same C++ parts; only the workload IDs differ).
|
|
135
|
+
*/
|
|
136
|
+
export declare function persistCovers(marker: VsPersistMarker, req: Pick<VsRequest, 'product' | 'components'>): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* The instance's state.json with every mention of the install folder (JSON-escaped, any case) moved to `to`. A longer
|
|
139
|
+
* folder name that only starts the same (Community2) is left alone.
|
|
140
|
+
*/
|
|
141
|
+
export declare function rewriteInstanceState(text: string, from: string, to: string): string;
|
|
142
|
+
/** `value` with a saved folder's old place swapped for its place on the data disk (case-insensitive prefix). */
|
|
143
|
+
export declare function rebase(value: string, moves: readonly {
|
|
144
|
+
from: string;
|
|
145
|
+
to: string;
|
|
146
|
+
}[]): string;
|
|
147
|
+
/** What the boot writes under HKLM: the saved values (paths moved to the data disk) in both views, the COM class. */
|
|
148
|
+
export declare function registryPlan(marker: VsPersistMarker, root: string, places: WinPlaces): RegEntry[];
|
|
149
|
+
/**
|
|
150
|
+
* The newest of the instances vswhere lists. Chosen here, not with vswhere's -latest, which fails outright (0x8000FFFF)
|
|
151
|
+
* when two instances have the same install date, as a copy of an instance does.
|
|
152
|
+
*/
|
|
153
|
+
export declare function newestInstance<T extends {
|
|
154
|
+
readonly installationVersion?: string;
|
|
155
|
+
}>(instances: readonly T[]): T | null;
|
|
156
|
+
export interface SaveCopy {
|
|
157
|
+
readonly name: FolderName;
|
|
158
|
+
readonly from: string;
|
|
159
|
+
readonly to: string;
|
|
160
|
+
readonly optional: boolean;
|
|
161
|
+
}
|
|
162
|
+
/** The input of save-visual-studio.ps1: the folders to mirror onto the data disk and the registry values to read. */
|
|
163
|
+
export declare function saveInput(vs: {
|
|
164
|
+
readonly product: VsRequest['product'];
|
|
165
|
+
readonly installationPath: string;
|
|
166
|
+
readonly instanceId: string;
|
|
167
|
+
}, root: string, places: WinPlaces, resultFile: string): {
|
|
168
|
+
copies: SaveCopy[];
|
|
169
|
+
registry: RegRead[];
|
|
170
|
+
resultFile: string;
|
|
171
|
+
};
|
|
172
|
+
/** The marker for a finished capture (written after the instance's state is rewritten). */
|
|
173
|
+
export declare function markerFrom(req: Pick<VsRequest, 'product' | 'components'>, vs: {
|
|
174
|
+
readonly installationPath: string;
|
|
175
|
+
readonly instanceId: string;
|
|
176
|
+
readonly version: string;
|
|
177
|
+
}, result: SaveResult, capturedAt: string): VsPersistMarker;
|
|
178
|
+
export interface RestoreCopy {
|
|
179
|
+
readonly name: FolderName;
|
|
180
|
+
readonly from: string;
|
|
181
|
+
readonly to: string;
|
|
182
|
+
/** 'all': what differs is copied; 'missing': each subfolder only when the target has none. */
|
|
183
|
+
readonly mode: 'all' | 'missing';
|
|
184
|
+
}
|
|
185
|
+
/** The input of restore-visual-studio.ps1: the folders copied back to their fixed places, then the registry values. */
|
|
186
|
+
export declare function restoreInput(marker: VsPersistMarker, root: string, places: WinPlaces): {
|
|
187
|
+
copies: RestoreCopy[];
|
|
188
|
+
registry: RegEntry[];
|
|
189
|
+
};
|