windows-development-sandbox 0.0.0-stage → 1.0.5
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/.sandbox-config.example.json +14 -0
- package/README.md +78 -2
- package/config.example.json +332 -0
- package/dist/api.d.ts +31 -0
- package/dist/api.js +30 -0
- package/dist/bin/agent-sandbox.d.ts +2 -0
- package/dist/bin/agent-sandbox.js +3 -0
- package/dist/bin/claude-host.d.ts +2 -0
- package/dist/bin/claude-host.js +5 -0
- package/dist/bin/claude-sbx.d.ts +2 -0
- package/dist/bin/claude-sbx.js +3 -0
- package/dist/bin/codex-sbx.d.ts +2 -0
- package/dist/bin/codex-sbx.js +3 -0
- package/dist/bin/gemini-sbx.d.ts +2 -0
- package/dist/bin/gemini-sbx.js +3 -0
- package/dist/bin/sandbox.d.ts +2 -0
- package/dist/bin/sandbox.js +4 -0
- package/dist/bin/sbx-hide.d.ts +2 -0
- package/dist/bin/sbx-hide.js +3 -0
- package/dist/bin/sbx-show.d.ts +2 -0
- package/dist/bin/sbx-show.js +3 -0
- package/dist/cli/args.d.ts +32 -0
- package/dist/cli/args.js +111 -0
- package/dist/cli/commands/accounts.d.ts +2 -0
- package/dist/cli/commands/accounts.js +52 -0
- package/dist/cli/commands/context.d.ts +21 -0
- package/dist/cli/commands/context.js +54 -0
- package/dist/cli/commands/data.d.ts +46 -0
- package/dist/cli/commands/data.js +249 -0
- package/dist/cli/commands/index.d.ts +8 -0
- package/dist/cli/commands/index.js +8 -0
- package/dist/cli/commands/launch.d.ts +5 -0
- package/dist/cli/commands/launch.js +152 -0
- package/dist/cli/commands/maintenance.d.ts +8 -0
- package/dist/cli/commands/maintenance.js +125 -0
- package/dist/cli/commands/repair.d.ts +59 -0
- package/dist/cli/commands/repair.js +332 -0
- package/dist/cli/commands/sessions.d.ts +11 -0
- package/dist/cli/commands/sessions.js +113 -0
- package/dist/cli/commands/setup.d.ts +45 -0
- package/dist/cli/commands/setup.js +152 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.js +96 -0
- package/dist/cli/manifest.d.ts +95 -0
- package/dist/cli/manifest.js +87 -0
- package/dist/cli/menu.d.ts +22 -0
- package/dist/cli/menu.js +187 -0
- package/dist/cli/settings.d.ts +11 -0
- package/dist/cli/settings.js +156 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.js +103 -0
- package/dist/core/detect-databases.d.ts +15 -0
- package/dist/core/detect-databases.js +147 -0
- package/dist/core/detect-pwsh.d.ts +18 -0
- package/dist/core/detect-pwsh.js +111 -0
- package/dist/core/detect-toolchains.d.ts +30 -0
- package/dist/core/detect-toolchains.js +58 -0
- package/dist/core/local-config.d.ts +12 -0
- package/dist/core/local-config.js +64 -0
- package/dist/core/schema.d.ts +604 -0
- package/dist/core/schema.js +264 -0
- package/dist/core/types.d.ts +162 -0
- package/dist/core/types.js +1 -0
- package/dist/core/util.d.ts +53 -0
- package/dist/core/util.js +145 -0
- package/dist/core/workspace.d.ts +23 -0
- package/dist/core/workspace.js +162 -0
- package/dist/guest/daemon.d.ts +1 -0
- package/dist/guest/daemon.js +186 -0
- package/dist/guest/db.d.ts +1 -0
- package/dist/guest/db.js +60 -0
- package/dist/guest/env.d.ts +49 -0
- package/dist/guest/env.js +99 -0
- package/dist/guest/gitguard.d.ts +7 -0
- package/dist/guest/gitguard.js +60 -0
- package/dist/guest/gitrules.d.ts +2 -0
- package/dist/guest/gitrules.js +11 -0
- package/dist/guest/guide.d.ts +12 -0
- package/dist/guest/guide.js +68 -0
- package/dist/guest/main.d.ts +1 -0
- package/dist/guest/main.js +120 -0
- package/dist/guest/ptyhost.d.ts +40 -0
- package/dist/guest/ptyhost.js +94 -0
- package/dist/guest/setup/agents.d.ts +8 -0
- package/dist/guest/setup/agents.js +73 -0
- package/dist/guest/setup/claude.d.ts +21 -0
- package/dist/guest/setup/claude.js +138 -0
- package/dist/guest/setup/environment.d.ts +6 -0
- package/dist/guest/setup/environment.js +65 -0
- package/dist/guest/setup/hooks.d.ts +1 -0
- package/dist/guest/setup/hooks.js +36 -0
- package/dist/guest/setup/network.d.ts +10 -0
- package/dist/guest/setup/network.js +99 -0
- package/dist/guest/setup/policy.d.ts +3 -0
- package/dist/guest/setup/policy.js +18 -0
- package/dist/guest/setup/user.d.ts +7 -0
- package/dist/guest/setup/user.js +47 -0
- package/dist/guest/toolchains/binstall.d.ts +7 -0
- package/dist/guest/toolchains/binstall.js +55 -0
- package/dist/guest/toolchains/catalogue.d.ts +51 -0
- package/dist/guest/toolchains/catalogue.js +31 -0
- package/dist/guest/toolchains/cli.d.ts +10 -0
- package/dist/guest/toolchains/cli.js +64 -0
- package/dist/guest/toolchains/common.d.ts +37 -0
- package/dist/guest/toolchains/common.js +124 -0
- package/dist/guest/toolchains/databases.d.ts +7 -0
- package/dist/guest/toolchains/databases.js +226 -0
- package/dist/guest/toolchains/git.d.ts +3 -0
- package/dist/guest/toolchains/git.js +112 -0
- package/dist/guest/toolchains/go.d.ts +3 -0
- package/dist/guest/toolchains/go.js +33 -0
- package/dist/guest/toolchains/index.d.ts +13 -0
- package/dist/guest/toolchains/index.js +122 -0
- package/dist/guest/toolchains/java.d.ts +8 -0
- package/dist/guest/toolchains/java.js +96 -0
- package/dist/guest/toolchains/managers.d.ts +16 -0
- package/dist/guest/toolchains/managers.js +82 -0
- package/dist/guest/toolchains/msvc.d.ts +10 -0
- package/dist/guest/toolchains/msvc.js +216 -0
- package/dist/guest/toolchains/native.d.ts +12 -0
- package/dist/guest/toolchains/native.js +220 -0
- package/dist/guest/toolchains/node.d.ts +3 -0
- package/dist/guest/toolchains/node.js +132 -0
- package/dist/guest/toolchains/pwsh.d.ts +4 -0
- package/dist/guest/toolchains/pwsh.js +14 -0
- package/dist/guest/toolchains/python.d.ts +3 -0
- package/dist/guest/toolchains/python.js +47 -0
- package/dist/guest/toolchains/runtimes.d.ts +17 -0
- package/dist/guest/toolchains/runtimes.js +40 -0
- package/dist/guest/toolchains/scripting.d.ts +10 -0
- package/dist/guest/toolchains/scripting.js +137 -0
- package/dist/guest/toolchains/versions.d.ts +47 -0
- package/dist/guest/toolchains/versions.js +77 -0
- package/dist/guest/toolchains/vscode.d.ts +2 -0
- package/dist/guest/toolchains/vscode.js +53 -0
- package/dist/guest/view.d.ts +1 -0
- package/dist/guest/view.js +22 -0
- package/dist/host/accounts.d.ts +32 -0
- package/dist/host/accounts.js +119 -0
- package/dist/host/channel.d.ts +37 -0
- package/dist/host/channel.js +180 -0
- package/dist/host/claude-profile.d.ts +15 -0
- package/dist/host/claude-profile.js +103 -0
- package/dist/host/cleanup.d.ts +23 -0
- package/dist/host/cleanup.js +66 -0
- package/dist/host/generate.d.ts +15 -0
- package/dist/host/generate.js +185 -0
- package/dist/host/personalize.d.ts +10 -0
- package/dist/host/personalize.js +97 -0
- package/dist/host/platform.d.ts +17 -0
- package/dist/host/platform.js +114 -0
- package/dist/host/project-scan.d.ts +23 -0
- package/dist/host/project-scan.js +173 -0
- package/dist/host/repo-guard.d.ts +23 -0
- package/dist/host/repo-guard.js +128 -0
- package/dist/host/secrets.d.ts +14 -0
- package/dist/host/secrets.js +133 -0
- package/dist/host/terminal.d.ts +32 -0
- package/dist/host/terminal.js +127 -0
- package/dist/host/window.d.ts +4 -0
- package/dist/host/window.js +68 -0
- package/dist/runtime/daemon.js +20291 -0
- package/dist/runtime/db.js +20423 -0
- package/dist/runtime/gitguard.js +84 -0
- package/dist/runtime/main.js +22398 -0
- package/dist/runtime/view.js +19966 -0
- package/dist/selftest/cli.test.d.ts +2 -0
- package/dist/selftest/cli.test.js +30 -0
- package/dist/selftest/config.test.d.ts +3 -0
- package/dist/selftest/config.test.js +74 -0
- package/dist/selftest/data.test.d.ts +2 -0
- package/dist/selftest/data.test.js +73 -0
- package/dist/selftest/detect.test.d.ts +2 -0
- package/dist/selftest/detect.test.js +42 -0
- package/dist/selftest/fixtures.d.ts +232 -0
- package/dist/selftest/fixtures.js +63 -0
- package/dist/selftest/guest.test.d.ts +3 -0
- package/dist/selftest/guest.test.js +61 -0
- package/dist/selftest/harness.d.ts +14 -0
- package/dist/selftest/harness.js +36 -0
- package/dist/selftest/helpers.test.d.ts +2 -0
- package/dist/selftest/helpers.test.js +125 -0
- package/dist/selftest/host.test.d.ts +2 -0
- package/dist/selftest/host.test.js +71 -0
- package/dist/selftest/index.d.ts +1 -0
- package/dist/selftest/index.js +56 -0
- package/dist/selftest/live.test.d.ts +7 -0
- package/dist/selftest/live.test.js +127 -0
- package/dist/selftest/mirror.test.d.ts +3 -0
- package/dist/selftest/mirror.test.js +74 -0
- package/dist/selftest/security.test.d.ts +3 -0
- package/dist/selftest/security.test.js +143 -0
- package/dist/selftest/setup.test.d.ts +2 -0
- package/dist/selftest/setup.test.js +23 -0
- package/dist/selftest/sync.test.d.ts +3 -0
- package/dist/selftest/sync.test.js +82 -0
- package/dist/shared/download.d.ts +58 -0
- package/dist/shared/download.js +200 -0
- package/dist/shared/guest-paths.d.ts +30 -0
- package/dist/shared/guest-paths.js +61 -0
- package/dist/shared/netrules.d.ts +18 -0
- package/dist/shared/netrules.js +98 -0
- package/dist/shared/paths.d.ts +9 -0
- package/dist/shared/paths.js +22 -0
- package/dist/shared/pty-client.d.ts +86 -0
- package/dist/shared/pty-client.js +227 -0
- package/dist/shared/rundir.d.ts +30 -0
- package/dist/shared/rundir.js +86 -0
- package/dist/shared/safe-remove.d.ts +10 -0
- package/dist/shared/safe-remove.js +55 -0
- package/dist/shared/schema.d.ts +84 -0
- package/dist/shared/schema.js +51 -0
- package/dist/shared/system.d.ts +15 -0
- package/dist/shared/system.js +19 -0
- package/dist/sync/claude-mem.d.ts +8 -0
- package/dist/sync/claude-mem.js +77 -0
- package/dist/sync/history.d.ts +30 -0
- package/dist/sync/history.js +129 -0
- package/dist/sync/watcher.d.ts +13 -0
- package/dist/sync/watcher.js +116 -0
- package/hooks/post-agent/README.txt +1 -0
- package/hooks/pre-provision/README.txt +1 -0
- package/package.json +91 -4
- package/sandbox/bootstrap.ps1 +258 -0
- package/sandbox/gitshim.cs +35 -0
- package/sandbox/logon.cmd +16 -0
- package/sandbox/ptyhost.cs +603 -0
- package/sandbox/trust.cmd +35 -0
- package/sandbox/vswhere.cs +83 -0
- package/schemas/config.schema.json +1062 -0
- package/schemas/sandbox-config.schema.json +952 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { dataDir, execAsync, exists, localDir, log, mkdirp, setSandboxStatus, workspace } from '../env.js';
|
|
3
|
+
import { arch, emptyEnv, installOnce, step, tool, wanted } from './common.js';
|
|
4
|
+
import { DATABASE_NAMES, setupDatabase } from './databases.js';
|
|
5
|
+
import { setupGit } from './git.js';
|
|
6
|
+
import { setupGo } from './go.js';
|
|
7
|
+
import { setupJava } from './java.js';
|
|
8
|
+
import { setupMsvc } from './msvc.js';
|
|
9
|
+
import { setupPwsh } from './pwsh.js';
|
|
10
|
+
import { setupCli } from './cli.js';
|
|
11
|
+
import { setupPyenv, setupVolta, wantsPyenv, wantsVolta } from './managers.js';
|
|
12
|
+
import { setupNode } from './node.js';
|
|
13
|
+
import { setupPython } from './python.js';
|
|
14
|
+
import { setupCpp, setupDotnet, setupMesa, setupRust } from './native.js';
|
|
15
|
+
import { setupBun, setupDeno, setupUv, uvVars } from './runtimes.js';
|
|
16
|
+
import { setupPhp, setupRuby, setupVcRuntime } from './scripting.js';
|
|
17
|
+
import { setupVscode } from './vscode.js';
|
|
18
|
+
export { pickNodeVersion, pickPackageVersion } from './versions.js';
|
|
19
|
+
/** How many installers run at once: enough to overlap downloads with installs, few enough not to thrash the disk. */
|
|
20
|
+
const PARALLEL = 4;
|
|
21
|
+
/** Python command-line tools through uv (each in its own environment, commands in uv's bin folder on PATH). */
|
|
22
|
+
const setupPythonTools = async (env, tools) => {
|
|
23
|
+
const uv = tool('uv', 'uv.exe');
|
|
24
|
+
if (!exists(uv))
|
|
25
|
+
throw new Error('toolchains.python.tools needs uv (toolchains.uv.enabled)');
|
|
26
|
+
await installOnce(env, 'uv tool', tools, (pkg) => execAsync(uv, ['tool', 'install', pkg], { env: { ...process.env, ...uvVars() } }));
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Where cargo builds and where sccache keeps its cache. Cargo fingerprints hold absolute paths and the host toolchain,
|
|
30
|
+
* so a target\\ shared with builds on the PC rebuilds every time the side changes: "separate" (the default) keeps a
|
|
31
|
+
* sandbox one next to it. sccache's cache persists unless the project sets SCCACHE_DIR itself.
|
|
32
|
+
*/
|
|
33
|
+
const rustCaches = (rust) => {
|
|
34
|
+
const target = rust.targetDir === 'shared' ? null
|
|
35
|
+
: rust.targetDir === 'separate' ? path.join(workspace, 'target', 'sandbox')
|
|
36
|
+
: rust.targetDir === 'local' ? path.join(localDir, 'cargo-target')
|
|
37
|
+
: rust.targetDir;
|
|
38
|
+
return { ...(target ? { CARGO_TARGET_DIR: target } : {}), ...(process.env.SCCACHE_DIR ? {} : { SCCACHE_DIR: path.join(dataDir, 'cache', 'sccache') }) };
|
|
39
|
+
};
|
|
40
|
+
/** "auto": the msvc host when the MSVC step runs (as on most PCs), else GNU with MinGW. */
|
|
41
|
+
const rustHost = (config) => {
|
|
42
|
+
if (config.rust.host !== 'auto')
|
|
43
|
+
return config.rust.host;
|
|
44
|
+
return (arch === 'arm64' ? 'aarch64' : 'x86_64') + (wanted(config.msvc.enabled, 'msvc') ? '-pc-windows-msvc' : '-pc-windows-gnu');
|
|
45
|
+
};
|
|
46
|
+
/** The toolchains and databases for this sandbox, in the order their PATH entries are listed. */
|
|
47
|
+
const plan = (config, versions, o) => {
|
|
48
|
+
const java = wanted(config.java.enabled, 'java');
|
|
49
|
+
return [
|
|
50
|
+
{ name: 'Git', on: o.gitEnabled, run: (env) => setupGit(env, o.hostGit, o.refresh, o.gitAccess) },
|
|
51
|
+
// Before Node: with package.json pins, Volta's node/npm/pnpm/yarn come first on PATH.
|
|
52
|
+
{ name: 'Volta', on: wantsVolta(config.node.volta), run: (env) => setupVolta(env) },
|
|
53
|
+
{ name: 'Node ' + versions.node.spec, on: true, run: (env) => setupNode(env, versions, config.node) },
|
|
54
|
+
{ name: 'Python', on: wanted(config.python.enabled, 'python'), run: (env) => setupPython(env, config.python, versions) },
|
|
55
|
+
{ name: 'pyenv-win', on: wanted(config.python.enabled, 'python') && wantsPyenv(config.python.pyenv), run: (env) => setupPyenv(env) },
|
|
56
|
+
{ name: 'C/C++', on: wanted(config.cpp.enabled, 'cpp') || wanted(config.rust.enabled, 'rust'), run: (env) => setupCpp(env, config.cpp) },
|
|
57
|
+
{ name: 'MSVC', on: wanted(config.msvc.enabled, 'msvc'), run: (env) => setupMsvc(env, config.msvc) },
|
|
58
|
+
// Rust links with MSVC (msvc host) or MinGW's gcc (GNU host): whichever it uses goes first.
|
|
59
|
+
{ name: 'Rust', on: wanted(config.rust.enabled, 'rust'), after: ['C/C++', 'MSVC'], run: async (env) => { await setupRust(env, { ...config.rust, host: rustHost(config) }, o.refresh); Object.assign(env.vars, rustCaches(config.rust)); } },
|
|
60
|
+
{ name: '.NET', on: wanted(config.dotnet.enabled, 'dotnet'), run: (env) => setupDotnet(env, config.dotnet, o.refresh) },
|
|
61
|
+
{ name: 'Go', on: wanted(config.go.enabled, 'go'), run: (env) => setupGo(env, config.go) },
|
|
62
|
+
{ name: 'Java', on: java, run: (env) => setupJava(env, config.java) },
|
|
63
|
+
{ name: 'Ruby', on: wanted(config.ruby.enabled, 'ruby'), run: (env) => setupRuby(env, config.ruby) },
|
|
64
|
+
// PHP installs the Visual C++ runtime itself: one installer at a time.
|
|
65
|
+
{ name: 'Visual C++ runtime', on: config.vcRuntime.enabled, run: async (env) => { await setupVcRuntime(env); } },
|
|
66
|
+
{ name: 'PHP', on: wanted(config.php.enabled, 'php'), after: ['Visual C++ runtime'], run: (env) => setupPhp(env, config.php) },
|
|
67
|
+
{ name: 'Deno', on: wanted(config.deno.enabled, 'deno'), run: (env) => setupDeno(env) },
|
|
68
|
+
{ name: 'Bun', on: config.bun.enabled, run: (env) => setupBun(env) },
|
|
69
|
+
{ name: 'uv', on: config.uv.enabled, run: (env) => setupUv(env) },
|
|
70
|
+
{ name: 'PowerShell 7', on: wanted(config.pwsh.enabled, 'pwsh'), run: (env) => setupPwsh(env, o.refresh) },
|
|
71
|
+
// The openssl tool comes from Git's install: after Git.
|
|
72
|
+
{ name: 'CLI tools', on: Object.values(config.cli).some((v) => v !== false), after: ['Git'], run: (env) => setupCli(env, config.cli, o.gitAccess, o.refresh) },
|
|
73
|
+
{ name: 'Mesa 3D', on: config.mesa.enabled, run: (env) => setupMesa(env, config.mesa) },
|
|
74
|
+
{ name: 'VS Code', on: config.vscode.enabled, run: (env) => setupVscode(env, o.refresh) },
|
|
75
|
+
{ name: 'Python tools', on: wanted(config.python.enabled, 'python') && config.python.tools.length > 0, after: ['Python', 'uv'], run: (env) => setupPythonTools(env, config.python.tools) },
|
|
76
|
+
// DynamoDB Local runs on Java: the Java step installs the JDK first instead of racing it.
|
|
77
|
+
...o.databases.map((db) => ({ name: DATABASE_NAMES[db.database], on: true, after: db.database === 'dynamodb' && java ? ['Java'] : [], run: (env) => setupDatabase(env, db) })),
|
|
78
|
+
];
|
|
79
|
+
};
|
|
80
|
+
/** Provisions every enabled toolchain and database, and returns the combined environment for the agent. */
|
|
81
|
+
export const setupToolchains = async (config, versions, options) => {
|
|
82
|
+
const steps = plan(config, versions, options).filter((s) => s.on);
|
|
83
|
+
const envs = new Map(steps.map((s) => [s.name, emptyEnv()]));
|
|
84
|
+
const finished = new Map();
|
|
85
|
+
const running = new Set();
|
|
86
|
+
const waiting = [];
|
|
87
|
+
let done = 0;
|
|
88
|
+
const report = () => { if (running.size)
|
|
89
|
+
setSandboxStatus('provisioning', 'Installing ' + [...running].join(', ') + ' (' + done + ' of ' + steps.length + ' done)'); };
|
|
90
|
+
for (const s of steps) {
|
|
91
|
+
// Dependencies first (they are earlier in the plan, so their promises exist), then a free slot.
|
|
92
|
+
finished.set(s.name, (async () => {
|
|
93
|
+
await Promise.all((s.after ?? []).map((name) => finished.get(name)));
|
|
94
|
+
while (running.size >= PARALLEL)
|
|
95
|
+
await new Promise((r) => waiting.push(r));
|
|
96
|
+
running.add(s.name);
|
|
97
|
+
report();
|
|
98
|
+
try {
|
|
99
|
+
await step(s.name, envs.get(s.name), async () => s.run(envs.get(s.name)));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
running.delete(s.name);
|
|
103
|
+
done++;
|
|
104
|
+
report();
|
|
105
|
+
waiting.shift()?.();
|
|
106
|
+
}
|
|
107
|
+
})());
|
|
108
|
+
}
|
|
109
|
+
await Promise.all(finished.values());
|
|
110
|
+
// Merged in plan order, so PATH order never depends on which download finished first.
|
|
111
|
+
const env = emptyEnv();
|
|
112
|
+
env.prepend.push(mkdirp(tool('bin')));
|
|
113
|
+
for (const part of envs.values()) {
|
|
114
|
+
Object.assign(env.vars, part.vars);
|
|
115
|
+
env.prepend.push(...part.prepend);
|
|
116
|
+
env.append.push(...part.append);
|
|
117
|
+
env.summary.push(...part.summary);
|
|
118
|
+
}
|
|
119
|
+
Object.assign(env.vars, config.environment);
|
|
120
|
+
log('Set up ' + steps.length + ' toolchains and databases.');
|
|
121
|
+
return env;
|
|
122
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Toolchains } from '../../core/types.js';
|
|
2
|
+
import { type ToolEnv } from './common.js';
|
|
3
|
+
/**
|
|
4
|
+
* A JDK for tools that run on Java (DynamoDB Local): the newest one already installed, else the newest LTS. Returns its
|
|
5
|
+
* folder; the agent's Java on PATH stays whatever toolchains.java says.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ensureJdk: () => Promise<string>;
|
|
8
|
+
export declare const setupJava: (env: ToolEnv, config: Toolchains["java"]) => Promise<void>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Java: Eclipse Temurin JDK (Adoptium), plus Maven and Gradle. All checksum-verified; the Maven repository and Gradle
|
|
2
|
+
// caches live in persistent storage, so dependencies download once.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fetchJson, fetchText, installArchive } from '../../shared/download.js';
|
|
6
|
+
import { dryRun, errMsg, exists, log, warn } from '../env.js';
|
|
7
|
+
import { arch, resolveCached, tool, versionFromFiles } from './common.js';
|
|
8
|
+
import { javaMajorFrom, pickPackageVersion } from './versions.js';
|
|
9
|
+
const ADOPTIUM = 'https://api.adoptium.net/v3';
|
|
10
|
+
const MAVEN = 'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/';
|
|
11
|
+
const temurin = async (major) => {
|
|
12
|
+
const assets = await fetchJson(ADOPTIUM + '/assets/latest/' + major + '/hotspot?image_type=jdk&os=windows&vendor=eclipse&architecture=' + (arch === 'arm64' ? 'aarch64' : 'x64'));
|
|
13
|
+
return assets.find((a) => a.binary.package.name.endsWith('.zip')) ?? Promise.reject(new Error('No Temurin ' + major + ' JDK zip for Windows'));
|
|
14
|
+
};
|
|
15
|
+
/** A project's .java-version / .sdkmanrc, else the configured version ("lts" = the newest LTS). */
|
|
16
|
+
const javaMajor = async (config) => {
|
|
17
|
+
const fromProject = versionFromFiles(['.java-version', '.sdkmanrc'], (text, name) => javaMajorFrom(text, name === '.sdkmanrc'));
|
|
18
|
+
if (fromProject)
|
|
19
|
+
return { major: fromProject.spec, source: fromProject.source };
|
|
20
|
+
const spec = config.version.trim().toLowerCase();
|
|
21
|
+
if (spec !== 'lts')
|
|
22
|
+
return { major: javaMajorFrom(spec) ?? spec, source: 'config' };
|
|
23
|
+
const lts = await resolveCached('java:lts', async () => String((await fetchJson(ADOPTIUM + '/info/available_releases')).most_recent_lts));
|
|
24
|
+
return { major: lts, source: 'newest LTS' };
|
|
25
|
+
};
|
|
26
|
+
const setupJdk = async (env, config) => {
|
|
27
|
+
const { major, source } = await javaMajor(config);
|
|
28
|
+
const release = await resolveCached('java:' + major, async () => (await temurin(major)).release_name);
|
|
29
|
+
const home = tool('java', release.replace(/[^A-Za-z0-9.-]/g, '_'));
|
|
30
|
+
if (!exists(path.join(home, 'bin', 'java.exe'))) {
|
|
31
|
+
const asset = await temurin(major);
|
|
32
|
+
log('Downloading ' + asset.binary.package.name + ' ...');
|
|
33
|
+
await installArchive(asset.binary.package.link, home, { sha256: asset.binary.package.checksum, locate: 'release', fileName: asset.binary.package.name });
|
|
34
|
+
}
|
|
35
|
+
env.vars.JAVA_HOME = home;
|
|
36
|
+
env.prepend.push(path.join(home, 'bin'));
|
|
37
|
+
env.summary.push('java: ' + release + ' (Temurin, from ' + source + ')');
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* A JDK for tools that run on Java (DynamoDB Local): the newest one already installed, else the newest LTS. Returns its
|
|
41
|
+
* folder; the agent's Java on PATH stays whatever toolchains.java says.
|
|
42
|
+
*/
|
|
43
|
+
export const ensureJdk = async () => {
|
|
44
|
+
const root = tool('java');
|
|
45
|
+
const installed = exists(root) ? fs.readdirSync(root).filter((d) => exists(path.join(root, d, 'bin', 'java.exe'))).sort((a, b) => b.localeCompare(a, undefined, { numeric: true })) : [];
|
|
46
|
+
if (installed.length)
|
|
47
|
+
return path.join(root, installed[0]);
|
|
48
|
+
const scratch = { vars: {}, prepend: [], append: [], summary: [] };
|
|
49
|
+
await setupJdk(scratch, { enabled: true, version: 'lts', maven: false, gradle: false });
|
|
50
|
+
return scratch.vars.JAVA_HOME;
|
|
51
|
+
};
|
|
52
|
+
const setupMaven = async (env) => {
|
|
53
|
+
const version = await resolveCached('maven:3', async () => {
|
|
54
|
+
const versions = [...(await fetchText(MAVEN + 'maven-metadata.xml')).matchAll(/<version>([^<]+)<\/version>/g)].map((m) => m[1]);
|
|
55
|
+
return pickPackageVersion(versions, '3') ?? Promise.reject(new Error('No Maven 3 release found'));
|
|
56
|
+
});
|
|
57
|
+
const home = tool('maven', version);
|
|
58
|
+
if (!exists(path.join(home, 'bin', 'mvn.cmd'))) {
|
|
59
|
+
const url = MAVEN + version + '/apache-maven-' + version + '-bin.zip';
|
|
60
|
+
log('Downloading Maven ' + version + ' ...');
|
|
61
|
+
await installArchive(url, home, { sha512: (await fetchText(url + '.sha512')).trim().split(/\s+/)[0], locate: 'LICENSE' });
|
|
62
|
+
}
|
|
63
|
+
const repo = tool('maven-repository');
|
|
64
|
+
Object.assign(env.vars, { MAVEN_HOME: home, M2_HOME: home, MAVEN_OPTS: '-Dmaven.repo.local=' + repo });
|
|
65
|
+
env.prepend.push(path.join(home, 'bin'));
|
|
66
|
+
env.summary.push('maven: ' + version);
|
|
67
|
+
};
|
|
68
|
+
const setupGradle = async (env) => {
|
|
69
|
+
const current = await fetchJson('https://services.gradle.org/versions/current').catch(() => null);
|
|
70
|
+
const version = await resolveCached('gradle', async () => current?.version ?? Promise.reject(new Error('Gradle version lookup failed')));
|
|
71
|
+
const home = tool('gradle', version);
|
|
72
|
+
if (!exists(path.join(home, 'bin', 'gradle.bat'))) {
|
|
73
|
+
if (!current || current.version !== version)
|
|
74
|
+
throw new Error('Gradle ' + version + ' is not installed and cannot be downloaded right now');
|
|
75
|
+
log('Downloading Gradle ' + version + ' ...');
|
|
76
|
+
await installArchive(current.downloadUrl, home, { sha256: (await fetchText(current.checksumUrl)).trim(), locate: 'LICENSE', fileName: 'gradle-' + version + '.zip' });
|
|
77
|
+
}
|
|
78
|
+
const userHome = tool('gradle-home');
|
|
79
|
+
fs.mkdirSync(userHome, { recursive: true });
|
|
80
|
+
// File-system watching does not work on mapped folders (harmless but noisy "Couldn't add watch" on every build).
|
|
81
|
+
Object.assign(env.vars, { GRADLE_HOME: home, GRADLE_USER_HOME: userHome, GRADLE_OPTS: '-Dorg.gradle.vfs.watch=false' });
|
|
82
|
+
env.prepend.push(path.join(home, 'bin'));
|
|
83
|
+
env.summary.push('gradle: ' + version);
|
|
84
|
+
};
|
|
85
|
+
export const setupJava = async (env, config) => {
|
|
86
|
+
if (dryRun) {
|
|
87
|
+
log('(dry run) would provision Java ' + config.version + (config.maven ? ' + Maven' : '') + (config.gradle ? ' + Gradle' : ''));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await setupJdk(env, config);
|
|
91
|
+
// Maven and Gradle are extras: one failing must not take the JDK down with it.
|
|
92
|
+
for (const [on, name, work] of [[config.maven, 'Maven', setupMaven], [config.gradle, 'Gradle', setupGradle]]) {
|
|
93
|
+
if (on)
|
|
94
|
+
await work(env).catch((e) => { env.summary.push(name.toLowerCase() + ': FAILED'); warn(name + ' setup failed (continuing without it): ' + errMsg(e)); });
|
|
95
|
+
}
|
|
96
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Toolchains } from '../../core/types.js';
|
|
2
|
+
import { type ToolEnv } from './common.js';
|
|
3
|
+
/** The "volta" block of the workspace's package.json ({ node, npm, yarn, pnpm }), if any. */
|
|
4
|
+
export declare const voltaPins: () => Record<string, string>;
|
|
5
|
+
export declare const wantsVolta: (setting: Toolchains["node"]["volta"]) => boolean;
|
|
6
|
+
export declare const wantsPyenv: (setting: Toolchains["python"]["pyenv"]) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Volta from its portable zip. With pins, its shims go first on PATH (the project gets exactly what package.json says,
|
|
9
|
+
* fetched ahead of time); without, Volta is just available and nvm's Node stays the default.
|
|
10
|
+
*/
|
|
11
|
+
export declare const setupVolta: (env: ToolEnv) => Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* pyenv-win from its source archive (it ships no release assets). Its versions persist in tools\pyenv. It comes after
|
|
14
|
+
* the Python step on PATH, so `python` is the sandbox's own until you `pyenv install` / `pyenv local`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const setupPyenv: (env: ToolEnv) => Promise<void>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Version managers people already use: Volta (Node, npm, yarn, pnpm pinned in package.json) and pyenv-win. nvm and
|
|
2
|
+
// the Python step still provide the default versions; these come along when the project or this PC uses them.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { githubAsset, installArchive, latestGithubTag } from '../../shared/download.js';
|
|
6
|
+
import { dryRun, errMsg, execAsync, exists, log, mkdirp, session, warn, workspace } from '../env.js';
|
|
7
|
+
import { arch, resolveCached, tool, versionOf } from './common.js';
|
|
8
|
+
/** The "volta" block of the workspace's package.json ({ node, npm, yarn, pnpm }), if any. */
|
|
9
|
+
export const voltaPins = () => {
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(workspace, 'package.json'), 'utf8').replace(/^/, ''));
|
|
12
|
+
return Object.fromEntries(Object.entries(pkg.volta ?? {}).filter((e) => ['node', 'npm', 'yarn', 'pnpm'].includes(e[0]) && typeof e[1] === 'string'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export const wantsVolta = (setting) => setting === 'auto' ? Object.keys(voltaPins()).length > 0 || Boolean(session.hostTools.volta) : setting;
|
|
19
|
+
export const wantsPyenv = (setting) => setting === 'auto' ? Boolean(session.hostTools.pyenv) : setting;
|
|
20
|
+
/**
|
|
21
|
+
* Volta from its portable zip. With pins, its shims go first on PATH (the project gets exactly what package.json says,
|
|
22
|
+
* fetched ahead of time); without, Volta is just available and nvm's Node stays the default.
|
|
23
|
+
*/
|
|
24
|
+
export const setupVolta = async (env) => {
|
|
25
|
+
if (dryRun) {
|
|
26
|
+
log('(dry run) would provision Volta');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const dir = tool('volta');
|
|
30
|
+
if (!exists(path.join(dir, 'volta.exe'))) {
|
|
31
|
+
const asset = await githubAsset('volta-cli/volta', arch === 'arm64' ? /^volta-[\d.]+-windows-arm64\.zip$/ : /^volta-[\d.]+-windows\.zip$/);
|
|
32
|
+
log('Downloading ' + asset.name + ' ...');
|
|
33
|
+
await installArchive(asset.url, dir, { sha256: asset.sha256, locate: 'volta.exe', fileName: asset.name });
|
|
34
|
+
}
|
|
35
|
+
// The MSI puts these shims (copies of volta-shim.exe) next to volta.exe; the portable zip leaves that to us.
|
|
36
|
+
for (const shim of ['node', 'npm', 'npx', 'pnpm', 'pnpx', 'yarn', 'yarnpkg']) {
|
|
37
|
+
const target = path.join(dir, shim + '.exe');
|
|
38
|
+
if (!exists(target))
|
|
39
|
+
fs.copyFileSync(path.join(dir, 'volta-shim.exe'), target);
|
|
40
|
+
}
|
|
41
|
+
const home = mkdirp(tool('volta-home'));
|
|
42
|
+
const shims = mkdirp(path.join(home, 'bin'));
|
|
43
|
+
const vars = { VOLTA_HOME: home, VOLTA_FEATURE_PNPM: '1' };
|
|
44
|
+
Object.assign(env.vars, vars);
|
|
45
|
+
const pins = voltaPins();
|
|
46
|
+
if (Object.keys(pins).length)
|
|
47
|
+
env.prepend.push(dir, shims);
|
|
48
|
+
else
|
|
49
|
+
env.append.push(dir, shims);
|
|
50
|
+
const volta = path.join(dir, 'volta.exe');
|
|
51
|
+
// `volta install` fetches the pinned versions now (not on the agent's first `node`) and makes them the defaults.
|
|
52
|
+
for (const [toolName, version] of Object.entries(pins)) {
|
|
53
|
+
try {
|
|
54
|
+
await execAsync(volta, ['install', toolName + '@' + version], { env: { ...process.env, ...vars }, cwd: workspace, stdio: 'ignore' });
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
warn('Volta could not install ' + toolName + '@' + version + ': ' + errMsg(e));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
env.summary.push('volta: ' + versionOf(volta) + (Object.keys(pins).length ? ' (pins from package.json: ' + Object.entries(pins).map(([k, v]) => k + '@' + v).join(', ') + ')' : ' (no pins in this project; nvm\'s node is the default)'));
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* pyenv-win from its source archive (it ships no release assets). Its versions persist in tools\pyenv. It comes after
|
|
64
|
+
* the Python step on PATH, so `python` is the sandbox's own until you `pyenv install` / `pyenv local`.
|
|
65
|
+
*/
|
|
66
|
+
export const setupPyenv = async (env) => {
|
|
67
|
+
if (dryRun) {
|
|
68
|
+
log('(dry run) would provision pyenv-win');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const root = tool('pyenv', 'pyenv-win');
|
|
72
|
+
if (!exists(path.join(root, 'bin', 'pyenv.bat'))) {
|
|
73
|
+
const tag = await resolveCached('pyenv-win', () => latestGithubTag('pyenv-win/pyenv-win'));
|
|
74
|
+
log('Downloading pyenv-win ' + tag + ' ...');
|
|
75
|
+
// GitHub's tag archive: no published checksum, but HTTPS from github.com for the project's own tag.
|
|
76
|
+
await installArchive('https://github.com/pyenv-win/pyenv-win/archive/refs/tags/' + encodeURIComponent(tag) + '.zip', root, { locate: 'pyenv.bat', up: 1, fileName: 'pyenv-win.zip' });
|
|
77
|
+
}
|
|
78
|
+
const home = root + path.sep;
|
|
79
|
+
Object.assign(env.vars, { PYENV: home, PYENV_ROOT: home, PYENV_HOME: home });
|
|
80
|
+
env.append.push(path.join(root, 'bin'), path.join(root, 'shims'));
|
|
81
|
+
env.summary.push('pyenv-win: pyenv install <version> / pyenv local <version> (versions persist)');
|
|
82
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Toolchains } from '../../core/types.js';
|
|
2
|
+
import { type ToolEnv } from './common.js';
|
|
3
|
+
/** Variables vcvars64.bat would set for this tree (host x64, target x64). */
|
|
4
|
+
export declare const vcvars: (root: string, msvc: string, sdk: string) => {
|
|
5
|
+
vars: Record<string, string>;
|
|
6
|
+
path: string[];
|
|
7
|
+
};
|
|
8
|
+
/** The vcvars environment of an installed tree (for steps that run before the agent's environment exists). */
|
|
9
|
+
export declare const installedMsvcEnv: () => ReturnType<typeof vcvars> | null;
|
|
10
|
+
export declare const setupMsvc: (env: ToolEnv, config: Toolchains["msvc"]) => Promise<void>;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// MSVC (cl, link) and the Windows SDK without an installer, the way mmozeiko's portable-msvc.py does it: packages
|
|
2
|
+
// from Microsoft's Visual Studio channel manifest (each payload checked against the manifest's sha256), the compiler
|
|
3
|
+
// unpacked from its .vsix, the SDK from its .msi files with `msiexec /a`. Laid out like a Build Tools install, with
|
|
4
|
+
// vcvars64.bat and a vswhere that points at it, so CMake, cargo (msvc host), node-gyp and vcvars-based scripts work.
|
|
5
|
+
// Use is governed by the Visual Studio licence terms: it only installs with toolchains.msvc.acceptLicense.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { download, extractArchive, fetchJson } from '../../shared/download.js';
|
|
9
|
+
import { safeRemove } from '../../shared/safe-remove.js';
|
|
10
|
+
import { system32 } from '../../shared/system.js';
|
|
11
|
+
import { dryRun, execAsync, exists, log, mkdirp } from '../env.js';
|
|
12
|
+
import { ensureCompiled } from '../ptyhost.js';
|
|
13
|
+
import { tool } from './common.js';
|
|
14
|
+
import { msiCabs, pickMsvcVersion, pickWindowsSdk } from './versions.js';
|
|
15
|
+
const CHANNEL = 'https://aka.ms/vs/17/release/channel';
|
|
16
|
+
const ROOT = tool('msvc');
|
|
17
|
+
const MANIFEST_DAY_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
/** Where build tools look for vswhere (local to the sandbox, so it is written each boot). */
|
|
19
|
+
const VSWHERE_DIR = path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Microsoft Visual Studio', 'Installer');
|
|
20
|
+
/** The VS manifest (~18 MB), cached for a day. */
|
|
21
|
+
const manifest = async () => {
|
|
22
|
+
const cached = path.join(ROOT, '.manifest.json');
|
|
23
|
+
if (exists(cached) && Date.now() - fs.statSync(cached).mtimeMs < MANIFEST_DAY_MS)
|
|
24
|
+
return JSON.parse(fs.readFileSync(cached, 'utf8'));
|
|
25
|
+
const channel = await fetchJson(CHANNEL);
|
|
26
|
+
const item = channel.channelItems.find((i) => i.id === 'Microsoft.VisualStudio.Manifests.VisualStudio')?.payloads?.[0];
|
|
27
|
+
if (!item)
|
|
28
|
+
throw new Error('the Visual Studio channel has no package manifest');
|
|
29
|
+
mkdirp(ROOT);
|
|
30
|
+
// The channel's sha256 for the manifest is not the file's hash (portable-msvc.py does not check it either); the
|
|
31
|
+
// manifest comes over HTTPS from Microsoft, and every package payload it lists IS checked against its own sha256.
|
|
32
|
+
await download(item.url, cached);
|
|
33
|
+
return JSON.parse(fs.readFileSync(cached, 'utf8'));
|
|
34
|
+
};
|
|
35
|
+
/** The compiler's packages (host x64, target x64): tools, resources, CRT headers/libs, ASAN/PGO headers, DIA SDK. */
|
|
36
|
+
const MSVC_PARTS = ['tools.hostx64.targetx64.base', 'tools.hostx64.targetx64.res.base', 'crt.headers.base', 'crt.x64.desktop.base', 'crt.x64.store.base', 'crt.source.base', 'asan.headers.base', 'pgo.headers.base'];
|
|
37
|
+
/** The SDK's .msi files (Desktop + UCRT + Store headers/libs/tools for x64), as in portable-msvc.py. */
|
|
38
|
+
const SDK_MSIS = [
|
|
39
|
+
'Windows SDK for Windows Store Apps Tools-x86_en-us.msi', 'Windows SDK for Windows Store Apps Headers-x86_en-us.msi',
|
|
40
|
+
'Windows SDK for Windows Store Apps Headers OnecoreUap-x86_en-us.msi', 'Windows SDK for Windows Store Apps Libs-x86_en-us.msi',
|
|
41
|
+
'Universal CRT Headers Libraries and Sources-x86_en-us.msi', 'Windows SDK Desktop Headers x86-x86_en-us.msi',
|
|
42
|
+
'Windows SDK Desktop Headers x64-x86_en-us.msi', 'Windows SDK Desktop Libs x64-x86_en-us.msi', 'Windows SDK Desktop Tools x64-x86_en-us.msi',
|
|
43
|
+
];
|
|
44
|
+
/** .vsix contents are percent-encoded file names under Contents/; unpack them into the root, names decoded. */
|
|
45
|
+
const unpackVsix = async (vsix, work) => {
|
|
46
|
+
const out = mkdirp(path.join(work, 'vsix-' + path.basename(vsix)));
|
|
47
|
+
await extractArchive(vsix, out, ['Contents']);
|
|
48
|
+
const decode = (dir) => {
|
|
49
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
const from = path.join(dir, entry.name);
|
|
51
|
+
let name = entry.name;
|
|
52
|
+
try {
|
|
53
|
+
if (name.includes('%'))
|
|
54
|
+
name = decodeURIComponent(name);
|
|
55
|
+
}
|
|
56
|
+
catch { /* not percent-encoded after all */ }
|
|
57
|
+
const to = path.join(dir, name);
|
|
58
|
+
if (to !== from)
|
|
59
|
+
fs.renameSync(from, to);
|
|
60
|
+
if (entry.isDirectory())
|
|
61
|
+
decode(to);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
decode(out);
|
|
65
|
+
fs.cpSync(path.join(out, 'Contents'), ROOT, { recursive: true, force: true });
|
|
66
|
+
safeRemove(out, work);
|
|
67
|
+
};
|
|
68
|
+
const install = async (config) => {
|
|
69
|
+
const m = await manifest();
|
|
70
|
+
const byId = new Map();
|
|
71
|
+
for (const p of m.packages)
|
|
72
|
+
byId.set(p.id.toLowerCase(), [...(byId.get(p.id.toLowerCase()) ?? []), p]);
|
|
73
|
+
const pick = (id) => byId.get(id.toLowerCase())?.find((p) => !p.language || p.language === 'en-US');
|
|
74
|
+
const ids = [...byId.keys()];
|
|
75
|
+
const version = pickMsvcVersion(ids, config.msvcVersion);
|
|
76
|
+
if (!version)
|
|
77
|
+
throw new Error('no MSVC ' + config.msvcVersion + ' in the Visual Studio manifest');
|
|
78
|
+
const sdk = pickWindowsSdk(ids, config.sdkVersion);
|
|
79
|
+
if (!sdk)
|
|
80
|
+
throw new Error('no Windows SDK ' + config.sdkVersion + ' in the Visual Studio manifest');
|
|
81
|
+
const work = mkdirp(path.join(ROOT, '.download'));
|
|
82
|
+
const fetchPayload = async (p) => {
|
|
83
|
+
const file = path.join(work, path.basename(p.fileName.replace(/\\/g, '/')));
|
|
84
|
+
if (!exists(file))
|
|
85
|
+
await download(p.url, file, p.sha256);
|
|
86
|
+
return file;
|
|
87
|
+
};
|
|
88
|
+
log('Downloading MSVC ' + version + ' (compiler, CRT) ...');
|
|
89
|
+
for (const part of MSVC_PARTS) {
|
|
90
|
+
const p = pick('microsoft.vc.' + version + '.' + part);
|
|
91
|
+
if (!p) {
|
|
92
|
+
if (part.startsWith('tools') || part.startsWith('crt.headers') || part.startsWith('crt.x64.desktop'))
|
|
93
|
+
throw new Error('MSVC ' + version + ' has no ' + part);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
for (const payload of p.payloads ?? [])
|
|
97
|
+
await unpackVsix(await fetchPayload(payload), work);
|
|
98
|
+
}
|
|
99
|
+
const dia = pick('microsoft.visualcpp.dia.sdk');
|
|
100
|
+
for (const payload of dia?.payloads ?? [])
|
|
101
|
+
await unpackVsix(await fetchPayload(payload), work);
|
|
102
|
+
// The SDK component depends on the package that holds the .msi/.cab payloads (e.g. Win11SDK_10.0.26100).
|
|
103
|
+
const sdkPackage = pick(Object.keys(pick(sdk.componentId)?.dependencies ?? {})[0] ?? '');
|
|
104
|
+
if (!sdkPackage?.payloads)
|
|
105
|
+
throw new Error('the Windows SDK ' + sdk.build + ' package has no payloads');
|
|
106
|
+
log('Downloading the Windows SDK ' + sdk.build + ' (headers, libraries) ...');
|
|
107
|
+
const payload = (name) => sdkPackage.payloads.find((p) => p.fileName.toLowerCase() === ('installers\\' + name).toLowerCase());
|
|
108
|
+
const msis = [];
|
|
109
|
+
for (const name of SDK_MSIS) {
|
|
110
|
+
const p = payload(name);
|
|
111
|
+
if (!p)
|
|
112
|
+
throw new Error('the Windows SDK package has no ' + name);
|
|
113
|
+
const file = await fetchPayload(p);
|
|
114
|
+
msis.push(file);
|
|
115
|
+
for (const cab of msiCabs(fs.readFileSync(file))) {
|
|
116
|
+
const c = payload(cab);
|
|
117
|
+
if (c)
|
|
118
|
+
await fetchPayload(c);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const msi of msis) {
|
|
122
|
+
// An administrative install only unpacks files; its verbose log explains a failure (e.g. a path over 260 characters).
|
|
123
|
+
const logFile = msi + '.log';
|
|
124
|
+
try {
|
|
125
|
+
await execAsync(system32('msiexec.exe'), ['/a', msi, '/quiet', '/qn', 'TARGETDIR=' + ROOT, '/l*v', logFile], { stdio: 'ignore' });
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
const lines = exists(logFile) ? fs.readFileSync(logFile, 'utf16le').split(/\r?\n/).filter((l) => /error|return value 3|failed/i.test(l)).slice(-4).join(' | ') : '';
|
|
129
|
+
throw new Error(path.basename(msi) + ' did not unpack: ' + (e instanceof Error ? e.message : String(e)) + (lines ? ' (' + lines.slice(0, 600) + ')' : ''));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// msiexec /a also copies every .msi into the target; they are not needed.
|
|
133
|
+
for (const name of fs.readdirSync(ROOT).filter((n) => n.toLowerCase().endsWith('.msi')))
|
|
134
|
+
fs.rmSync(path.join(ROOT, name), { force: true });
|
|
135
|
+
safeRemove(work, ROOT);
|
|
136
|
+
const sdkVersion = fs.readdirSync(path.join(ROOT, 'Windows Kits', '10', 'Include')).filter((d) => d.startsWith('10.0.' + sdk.build)).sort().pop();
|
|
137
|
+
if (!sdkVersion)
|
|
138
|
+
throw new Error('the Windows SDK did not unpack (no Include\\10.0.' + sdk.build + '.*)');
|
|
139
|
+
// The tools folder is named after the toolset (14.44.35207), not the package version (14.44.17.14).
|
|
140
|
+
const toolsRoot = path.join(ROOT, 'VC', 'Tools', 'MSVC');
|
|
141
|
+
const tools = fs.readdirSync(toolsRoot).filter((d) => exists(path.join(toolsRoot, d, 'bin', 'Hostx64', 'x64', 'cl.exe'))).sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).pop();
|
|
142
|
+
if (!tools)
|
|
143
|
+
throw new Error('MSVC ' + version + ' did not unpack (no cl.exe under VC\\Tools\\MSVC)');
|
|
144
|
+
return { msvc: tools, sdk: sdkVersion };
|
|
145
|
+
};
|
|
146
|
+
/** Variables vcvars64.bat would set for this tree (host x64, target x64). */
|
|
147
|
+
export const vcvars = (root, msvc, sdk) => {
|
|
148
|
+
const vc = path.join(root, 'VC', 'Tools', 'MSVC', msvc);
|
|
149
|
+
const kits = path.join(root, 'Windows Kits', '10');
|
|
150
|
+
const inc = (...p) => path.join(kits, 'Include', sdk, ...p);
|
|
151
|
+
const lib = (...p) => path.join(kits, 'Lib', sdk, ...p);
|
|
152
|
+
return {
|
|
153
|
+
vars: {
|
|
154
|
+
VSINSTALLDIR: root + '\\', VCINSTALLDIR: path.join(root, 'VC') + '\\', VCToolsInstallDir: vc + '\\', VCToolsVersion: msvc, VisualStudioVersion: '17.0',
|
|
155
|
+
WindowsSdkDir: kits + '\\', WindowsSDKVersion: sdk + '\\', WindowsSDKLibVersion: sdk + '\\', UniversalCRTSdkDir: kits + '\\', UCRTVersion: sdk,
|
|
156
|
+
VSCMD_ARG_HOST_ARCH: 'x64', VSCMD_ARG_TGT_ARCH: 'x64', Platform: 'x64',
|
|
157
|
+
INCLUDE: [path.join(vc, 'include'), inc('ucrt'), inc('shared'), inc('um'), inc('winrt'), inc('cppwinrt')].join(';'),
|
|
158
|
+
LIB: [path.join(vc, 'lib', 'x64'), lib('ucrt', 'x64'), lib('um', 'x64')].join(';'),
|
|
159
|
+
LIBPATH: [path.join(vc, 'lib', 'x64'), path.join(vc, 'lib', 'x86', 'store', 'references')].join(';'),
|
|
160
|
+
},
|
|
161
|
+
path: [path.join(vc, 'bin', 'Hostx64', 'x64'), path.join(kits, 'bin', sdk, 'x64')],
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
/** vcvars64.bat / vcvarsall.bat in the usual place, for scripts that `call` them. */
|
|
165
|
+
const writeVcvars = (env) => {
|
|
166
|
+
const dir = mkdirp(path.join(ROOT, 'VC', 'Auxiliary', 'Build'));
|
|
167
|
+
const lines = ['@echo off', 'rem Written by Windows Development Sandbox for its portable MSVC (host x64, target x64).',
|
|
168
|
+
...Object.entries(env.vars).map(([k, v]) => 'set "' + k + '=' + v + '"'), 'set "PATH=' + env.path.join(';') + ';%PATH%"', 'exit /b 0'];
|
|
169
|
+
fs.writeFileSync(path.join(dir, 'vcvars64.bat'), lines.join('\r\n') + '\r\n');
|
|
170
|
+
fs.writeFileSync(path.join(dir, 'vcvarsall.bat'), ['@echo off', 'rem Only x64 (amd64) is installed.', 'call "%~dp0vcvars64.bat"', 'exit /b %errorlevel%'].join('\r\n') + '\r\n');
|
|
171
|
+
};
|
|
172
|
+
/** vswhere.exe where Visual Studio's installer would put it, answering with this tree. */
|
|
173
|
+
const installVswhere = (msvc) => {
|
|
174
|
+
mkdirp(VSWHERE_DIR);
|
|
175
|
+
fs.copyFileSync(ensureCompiled('vswhere'), path.join(VSWHERE_DIR, 'vswhere.exe'));
|
|
176
|
+
const [major, minor] = msvc.split('.').slice(2);
|
|
177
|
+
fs.writeFileSync(path.join(VSWHERE_DIR, 'vswhere.json'), JSON.stringify({ installationPath: ROOT, installationVersion: '17.' + (minor ?? '0') + '.0.' + (major ?? '0') }));
|
|
178
|
+
};
|
|
179
|
+
/** The vcvars environment of an installed tree (for steps that run before the agent's environment exists). */
|
|
180
|
+
export const installedMsvcEnv = () => {
|
|
181
|
+
try {
|
|
182
|
+
const done = JSON.parse(fs.readFileSync(path.join(ROOT, '.installed.json'), 'utf8'));
|
|
183
|
+
return exists(path.join(ROOT, 'VC', 'Tools', 'MSVC', done.msvc, 'bin', 'Hostx64', 'x64', 'cl.exe')) ? vcvars(ROOT, done.msvc, done.sdk) : null;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
export const setupMsvc = async (env, config) => {
|
|
190
|
+
if (!config.acceptLicense) {
|
|
191
|
+
throw new Error('MSVC is installed only with toolchains.msvc.acceptLicense = true: its use is governed by the Visual Studio licence terms (https://visualstudio.microsoft.com/license-terms/), which need a Visual Studio licence');
|
|
192
|
+
}
|
|
193
|
+
if (dryRun) {
|
|
194
|
+
log('(dry run) would provision MSVC ' + config.msvcVersion + ' and Windows SDK ' + config.sdkVersion);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const marker = path.join(ROOT, '.installed.json');
|
|
198
|
+
const want = config.msvcVersion + '/' + config.sdkVersion;
|
|
199
|
+
let done = exists(marker) ? JSON.parse(fs.readFileSync(marker, 'utf8')) : null;
|
|
200
|
+
if (!done || done.want !== want || !exists(path.join(ROOT, 'VC', 'Tools', 'MSVC', done.msvc, 'bin', 'Hostx64', 'x64', 'cl.exe'))) {
|
|
201
|
+
log('Installing MSVC and the Windows SDK (~1-2 GB, once; licence accepted in toolchains.msvc.acceptLicense) ...');
|
|
202
|
+
// A previous attempt's verified downloads (.download) are kept; everything it unpacked is redone.
|
|
203
|
+
if (exists(ROOT))
|
|
204
|
+
for (const name of fs.readdirSync(ROOT).filter((n) => n !== '.manifest.json' && n !== '.download'))
|
|
205
|
+
safeRemove(path.join(ROOT, name), ROOT);
|
|
206
|
+
const installed = await install(config);
|
|
207
|
+
done = { ...installed, want };
|
|
208
|
+
fs.writeFileSync(marker, JSON.stringify(done, null, 2));
|
|
209
|
+
}
|
|
210
|
+
const vc = vcvars(ROOT, done.msvc, done.sdk);
|
|
211
|
+
writeVcvars(vc);
|
|
212
|
+
installVswhere(done.msvc);
|
|
213
|
+
Object.assign(env.vars, vc.vars);
|
|
214
|
+
env.prepend.push(...vc.path);
|
|
215
|
+
env.summary.push('msvc: ' + done.msvc + ', Windows SDK ' + done.sdk + ' (cl, link; vcvars64.bat and vswhere work)');
|
|
216
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Toolchains } from '../../core/types.js';
|
|
2
|
+
import { type ToolEnv } from './common.js';
|
|
3
|
+
/** MinGW-w64 GCC from WinLibs: gcc/g++/mingw32-make for the agent, and the linker for Rust's GNU host. */
|
|
4
|
+
export declare const setupCpp: (env: ToolEnv, config: Toolchains["cpp"]) => Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* Mesa 3D for Windows (llvmpipe): OpenGL 4.5 on the CPU when the sandbox has no GPU. Windows only offers OpenGL 1.1 in
|
|
7
|
+
* software, and opengl32.dll cannot be replaced system-wide, so programs pick Mesa up from their own folder.
|
|
8
|
+
*/
|
|
9
|
+
export declare const setupMesa: (env: ToolEnv, config: Toolchains["mesa"]) => Promise<void>;
|
|
10
|
+
export declare const setupRust: (env: ToolEnv, config: Toolchains["rust"], refresh: boolean) => Promise<void>;
|
|
11
|
+
/** .NET SDK channels from config plus whatever the workspace's global.json pins. Installs are remembered in a marker. */
|
|
12
|
+
export declare const setupDotnet: (env: ToolEnv, config: Toolchains["dotnet"], refresh: boolean) => Promise<void>;
|