@git.zone/tsrust 1.9.1 → 1.10.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/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/mod_artifact/classes.artifactassembler.d.ts +6 -0
- package/dist_ts/mod_artifact/classes.artifactassembler.js +169 -6
- package/dist_ts/mod_artifact/index.d.ts +1 -1
- package/dist_ts/mod_artifact/index.js +2 -2
- package/dist_ts/mod_cargo/classes.cargorunner.js +8 -5
- package/dist_ts/mod_cli/classes.tsrustcli.d.ts +1 -0
- package/dist_ts/mod_cli/classes.tsrustcli.js +30 -3
- package/dist_ts/mod_cli/helpers.targets.d.ts +2 -0
- package/dist_ts/mod_cli/helpers.targets.js +1 -1
- package/dist_ts/mod_matrix/classes.matrixcommandrunner.d.ts +25 -0
- package/dist_ts/mod_matrix/classes.matrixcommandrunner.js +266 -0
- package/dist_ts/mod_matrix/classes.nativematrixbuilder.d.ts +64 -0
- package/dist_ts/mod_matrix/classes.nativematrixbuilder.js +1084 -0
- package/dist_ts/mod_matrix/helpers.matrixconfig.d.ts +30 -0
- package/dist_ts/mod_matrix/helpers.matrixconfig.js +129 -0
- package/dist_ts/mod_matrix/index.d.ts +3 -0
- package/dist_ts/mod_matrix/index.js +4 -0
- package/dist_ts/mod_provenance/classes.gitstate.d.ts +1 -0
- package/dist_ts/mod_provenance/classes.gitstate.js +13 -3
- package/dist_ts/mod_provenance/classes.provenancestore.d.ts +1 -0
- package/dist_ts/mod_provenance/classes.provenancestore.js +5 -1
- package/dist_ts/mod_provenance/index.d.ts +1 -1
- package/dist_ts/mod_provenance/index.js +2 -2
- package/dist_ts/mod_toolchain/classes.toolchainmanager.d.ts +6 -0
- package/dist_ts/mod_toolchain/classes.toolchainmanager.js +211 -40
- package/package.json +2 -2
- package/readme.hints.md +12 -1
- package/readme.md +69 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +1 -0
- package/ts/mod_artifact/classes.artifactassembler.ts +201 -5
- package/ts/mod_artifact/index.ts +2 -0
- package/ts/mod_cargo/classes.cargorunner.ts +9 -4
- package/ts/mod_cli/classes.tsrustcli.ts +34 -2
- package/ts/mod_cli/helpers.targets.ts +3 -0
- package/ts/mod_matrix/classes.matrixcommandrunner.ts +294 -0
- package/ts/mod_matrix/classes.nativematrixbuilder.ts +1472 -0
- package/ts/mod_matrix/helpers.matrixconfig.ts +222 -0
- package/ts/mod_matrix/index.ts +23 -0
- package/ts/mod_provenance/classes.gitstate.ts +13 -2
- package/ts/mod_provenance/classes.provenancestore.ts +4 -0
- package/ts/mod_provenance/index.ts +1 -0
- package/ts/mod_toolchain/classes.toolchainmanager.ts +236 -43
|
@@ -0,0 +1,1472 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
ArtifactAssembler,
|
|
6
|
+
MAX_ARTIFACT_BYTES,
|
|
7
|
+
MAX_ASSEMBLY_INPUT_BYTES,
|
|
8
|
+
type IArtifactAssemblyResult,
|
|
9
|
+
} from '../mod_artifact/index.js';
|
|
10
|
+
import { CargoConfig } from '../mod_cargo/index.js';
|
|
11
|
+
import {
|
|
12
|
+
assertGitSnapshotUnchanged,
|
|
13
|
+
captureGitSnapshot,
|
|
14
|
+
MAX_PROVENANCE_SIDECAR_BYTES,
|
|
15
|
+
PROVENANCE_SIDECAR_SUFFIX,
|
|
16
|
+
ProvenanceStore,
|
|
17
|
+
} from '../mod_provenance/index.js';
|
|
18
|
+
import { ToolchainManager } from '../mod_toolchain/index.js';
|
|
19
|
+
import { friendlyTargetName, type ITsrustConfig } from '../mod_cli/helpers.targets.js';
|
|
20
|
+
import { commitinfo } from '../00_commitinfo_data.js';
|
|
21
|
+
import {
|
|
22
|
+
MatrixCommandRunner,
|
|
23
|
+
type IMatrixCommandOptions,
|
|
24
|
+
type IMatrixCommandRunner,
|
|
25
|
+
} from './classes.matrixcommandrunner.js';
|
|
26
|
+
import {
|
|
27
|
+
resolveMatrixPlan,
|
|
28
|
+
type IResolvedMatrixPlan,
|
|
29
|
+
type IResolvedMatrixWorker,
|
|
30
|
+
type TMatrixHost,
|
|
31
|
+
} from './helpers.matrixconfig.js';
|
|
32
|
+
|
|
33
|
+
const capabilityCommandTimeoutMs = 5 * 60 * 1000;
|
|
34
|
+
const buildCommandTimeoutMs = 60 * 60 * 1000;
|
|
35
|
+
const maximumRetainedDiagnosticRuns = 3;
|
|
36
|
+
const sshConnectionArgs = [
|
|
37
|
+
'-o',
|
|
38
|
+
'ServerAliveInterval=15',
|
|
39
|
+
'-o',
|
|
40
|
+
'ServerAliveCountMax=2',
|
|
41
|
+
];
|
|
42
|
+
const matrixOwnerFileName = '.tsrust-matrix-owner';
|
|
43
|
+
const forwardEnvironmentGrantName = 'TSRUST_MATRIX_ALLOWED_ENVIRONMENT';
|
|
44
|
+
const defaultWorkerEnvironmentNames = [
|
|
45
|
+
'PATH',
|
|
46
|
+
'USER',
|
|
47
|
+
'LOGNAME',
|
|
48
|
+
'SHELL',
|
|
49
|
+
'TMPDIR',
|
|
50
|
+
'TMP',
|
|
51
|
+
'TEMP',
|
|
52
|
+
'LANG',
|
|
53
|
+
'LC_ALL',
|
|
54
|
+
'LC_CTYPE',
|
|
55
|
+
'TERM',
|
|
56
|
+
'COLORTERM',
|
|
57
|
+
'CI',
|
|
58
|
+
'RUSTUP_HOME',
|
|
59
|
+
] as const;
|
|
60
|
+
const transportEnvironmentNames = [
|
|
61
|
+
'PATH',
|
|
62
|
+
'HOME',
|
|
63
|
+
'USER',
|
|
64
|
+
'LOGNAME',
|
|
65
|
+
'SHELL',
|
|
66
|
+
'SSH_AUTH_SOCK',
|
|
67
|
+
'SSH_AGENT_PID',
|
|
68
|
+
'TMPDIR',
|
|
69
|
+
'TMP',
|
|
70
|
+
'TEMP',
|
|
71
|
+
'LANG',
|
|
72
|
+
'LC_ALL',
|
|
73
|
+
'LC_CTYPE',
|
|
74
|
+
'TERM',
|
|
75
|
+
'COLORTERM',
|
|
76
|
+
'CI',
|
|
77
|
+
] as const;
|
|
78
|
+
const bundledToolchainValidationSource = [
|
|
79
|
+
"const fs = require('node:fs');",
|
|
80
|
+
"const path = require('node:path');",
|
|
81
|
+
'const [root, ...executables] = process.argv.slice(1);',
|
|
82
|
+
'const rootEntry = fs.lstatSync(root);',
|
|
83
|
+
'if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink()) process.exit(1);',
|
|
84
|
+
'if (process.getuid && rootEntry.uid !== process.getuid()) process.exit(1);',
|
|
85
|
+
'if ((rootEntry.mode & 0o022) !== 0) process.exit(1);',
|
|
86
|
+
'const physicalRoot = fs.realpathSync(root);',
|
|
87
|
+
'for (const executable of executables) {',
|
|
88
|
+
' const entry = fs.lstatSync(executable);',
|
|
89
|
+
' if (!entry.isFile() && !entry.isSymbolicLink()) process.exit(1);',
|
|
90
|
+
' const resolvedPath = fs.realpathSync(executable);',
|
|
91
|
+
' const relativePath = path.relative(physicalRoot, resolvedPath);',
|
|
92
|
+
" if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath)) process.exit(1);",
|
|
93
|
+
' const resolved = fs.statSync(resolvedPath);',
|
|
94
|
+
' if (!resolved.isFile() || (resolved.mode & 0o111) === 0 || (resolved.mode & 0o022) !== 0) process.exit(1);',
|
|
95
|
+
' if (process.getuid && resolved.uid !== process.getuid()) process.exit(1);',
|
|
96
|
+
'}',
|
|
97
|
+
].join('\n');
|
|
98
|
+
const remoteWatchdogSource = [
|
|
99
|
+
"const childProcess = require('node:child_process');",
|
|
100
|
+
'const shell = process.argv[1];',
|
|
101
|
+
'const script = process.argv[2];',
|
|
102
|
+
"const child = childProcess.spawn(shell, ['-lc', script], { detached: true, stdio: ['ignore', 'inherit', 'inherit'] });",
|
|
103
|
+
'let lastHeartbeat = Date.now();',
|
|
104
|
+
'let terminationDeadline;',
|
|
105
|
+
'let spawnError;',
|
|
106
|
+
'let finished = false;',
|
|
107
|
+
'const killGroup = (signal) => { if (!child.pid) return; try { process.kill(-child.pid, signal); } catch (error) { if (error.code !== "ESRCH") throw error; } };',
|
|
108
|
+
'const groupExists = () => { if (!child.pid) return false; try { process.kill(-child.pid, 0); return true; } catch (error) { if (error.code === "ESRCH") return false; throw error; } };',
|
|
109
|
+
'const terminate = (signal) => { if (terminationDeadline) return; killGroup(signal); terminationDeadline = Date.now() + 10000; };',
|
|
110
|
+
'const enforceEscalation = () => { if (terminationDeadline && Date.now() >= terminationDeadline) killGroup("SIGKILL"); };',
|
|
111
|
+
'const waitForGroupExit = async () => { const deadline = Math.max(Date.now(), terminationDeadline || 0) + 2000; while (groupExists() && Date.now() < deadline) { enforceEscalation(); await new Promise((resolve) => setTimeout(resolve, 100)); } enforceEscalation(); return !groupExists(); };',
|
|
112
|
+
'const signalHandlers = new Map();',
|
|
113
|
+
'for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { const handler = () => terminate(signal); signalHandlers.set(signal, handler); process.on(signal, handler); }',
|
|
114
|
+
'const heartbeatTimer = setInterval(() => { if (Date.now() - lastHeartbeat > 45000) terminate("SIGHUP"); enforceEscalation(); }, 1000);',
|
|
115
|
+
'process.stdin.on("data", () => { lastHeartbeat = Date.now(); });',
|
|
116
|
+
'process.stdin.on("end", () => terminate("SIGHUP"));',
|
|
117
|
+
'process.stdin.on("error", () => terminate("SIGHUP"));',
|
|
118
|
+
'process.stdin.resume();',
|
|
119
|
+
'child.once("error", (error) => { spawnError = error; });',
|
|
120
|
+
'child.once("close", (code, signal) => { void (async () => {',
|
|
121
|
+
' if (finished) return;',
|
|
122
|
+
' finished = true;',
|
|
123
|
+
' const hadSurvivingGroup = groupExists();',
|
|
124
|
+
' if (hadSurvivingGroup) terminate("SIGTERM");',
|
|
125
|
+
' const stopped = await waitForGroupExit();',
|
|
126
|
+
' clearInterval(heartbeatTimer);',
|
|
127
|
+
' for (const [registeredSignal, handler] of signalHandlers) process.off(registeredSignal, handler);',
|
|
128
|
+
' process.stdin.destroy();',
|
|
129
|
+
' if (spawnError) console.error(spawnError);',
|
|
130
|
+
' if (!stopped) console.error("Remote matrix command left a live process group");',
|
|
131
|
+
' process.exitCode = spawnError || !stopped || hadSurvivingGroup ? 1 : (code ?? (signal ? 1 : 0));',
|
|
132
|
+
'})(); });',
|
|
133
|
+
].join('\n');
|
|
134
|
+
|
|
135
|
+
interface IResolvedSshWorker {
|
|
136
|
+
destination: string;
|
|
137
|
+
temporaryRoot: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface IRemoteWorkspace {
|
|
141
|
+
worker: IResolvedMatrixWorker;
|
|
142
|
+
runtime: IResolvedSshWorker;
|
|
143
|
+
directory: string;
|
|
144
|
+
ownerToken: string;
|
|
145
|
+
gitCommit: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface INativeMatrixBuilderOptions {
|
|
149
|
+
workspace: string;
|
|
150
|
+
config: ITsrustConfig;
|
|
151
|
+
environment?: NodeJS.ProcessEnv;
|
|
152
|
+
commandRunner?: IMatrixCommandRunner;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface INativeMatrixCheckResult {
|
|
156
|
+
workers: TMatrixHost[];
|
|
157
|
+
targets: string[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface INativeMatrixBuildResult extends INativeMatrixCheckResult {
|
|
161
|
+
gitCommit: string;
|
|
162
|
+
assembly: IArtifactAssemblyResult;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const shellQuote = (valueArg: string): string => `'${valueArg.replace(/'/g, `'"'"'`)}'`;
|
|
166
|
+
|
|
167
|
+
const expectedHostDetails = (
|
|
168
|
+
hostArg: TMatrixHost,
|
|
169
|
+
): { system: 'Linux' | 'Darwin'; machine: 'x86_64' | 'aarch64' | 'arm64' } => {
|
|
170
|
+
switch (hostArg) {
|
|
171
|
+
case 'linux_amd64':
|
|
172
|
+
return { system: 'Linux', machine: 'x86_64' };
|
|
173
|
+
case 'linux_arm64':
|
|
174
|
+
return { system: 'Linux', machine: 'aarch64' };
|
|
175
|
+
case 'macos_amd64':
|
|
176
|
+
return { system: 'Darwin', machine: 'x86_64' };
|
|
177
|
+
case 'macos_arm64':
|
|
178
|
+
return { system: 'Darwin', machine: 'arm64' };
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const artifactNamesFor = (
|
|
183
|
+
workerArg: IResolvedMatrixWorker,
|
|
184
|
+
binariesArg: string[],
|
|
185
|
+
): string[] =>
|
|
186
|
+
workerArg.targets
|
|
187
|
+
.flatMap((target) =>
|
|
188
|
+
binariesArg.flatMap((binary) => {
|
|
189
|
+
const binaryName = `${binary}_${target.friendly}`;
|
|
190
|
+
return [binaryName, `${binaryName}${PROVENANCE_SIDECAR_SUFFIX}`];
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
193
|
+
.sort();
|
|
194
|
+
|
|
195
|
+
const linuxProbeCompiler = (targetArg: string): string | undefined => {
|
|
196
|
+
switch (targetArg) {
|
|
197
|
+
case 'linux_amd64':
|
|
198
|
+
return 'x86_64-linux-gnu-gcc';
|
|
199
|
+
case 'linux_arm64':
|
|
200
|
+
return 'aarch64-linux-gnu-gcc';
|
|
201
|
+
case 'linux_amd64_musl':
|
|
202
|
+
return 'x86_64-linux-musl-gcc';
|
|
203
|
+
case 'linux_arm64_musl':
|
|
204
|
+
return 'aarch64-linux-musl-gcc';
|
|
205
|
+
default:
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export class NativeMatrixBuilder {
|
|
211
|
+
private readonly workspace: string;
|
|
212
|
+
private readonly environment: NodeJS.ProcessEnv;
|
|
213
|
+
private readonly commandRunner: IMatrixCommandRunner;
|
|
214
|
+
private readonly plan: IResolvedMatrixPlan;
|
|
215
|
+
|
|
216
|
+
constructor(optionsArg: INativeMatrixBuilderOptions) {
|
|
217
|
+
this.workspace = path.resolve(optionsArg.workspace);
|
|
218
|
+
this.environment = { ...process.env, ...(optionsArg.environment || {}) };
|
|
219
|
+
this.commandRunner =
|
|
220
|
+
optionsArg.commandRunner || new MatrixCommandRunner(this.environment);
|
|
221
|
+
this.plan = resolveMatrixPlan(optionsArg.config);
|
|
222
|
+
this.assertForwardEnvironmentGrant();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public async check(): Promise<INativeMatrixCheckResult> {
|
|
226
|
+
await this.assertBundleCompatibleSource();
|
|
227
|
+
await this.commandRunner.execute('git', ['--version'], {
|
|
228
|
+
cwd: this.workspace,
|
|
229
|
+
capture: true,
|
|
230
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
231
|
+
});
|
|
232
|
+
await this.commandRunner.execute('pnpm', ['--version'], {
|
|
233
|
+
cwd: this.workspace,
|
|
234
|
+
capture: true,
|
|
235
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
236
|
+
});
|
|
237
|
+
await this.assertInstalledTsrustVersion(this.workspace);
|
|
238
|
+
for (const worker of this.plan.workers) {
|
|
239
|
+
if (worker.config.transport === 'local') {
|
|
240
|
+
await this.checkLocalWorker(worker);
|
|
241
|
+
} else {
|
|
242
|
+
await this.checkSshWorker(worker, this.resolveSshWorker(worker));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return this.checkResult();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public async build(): Promise<INativeMatrixBuildResult> {
|
|
249
|
+
const gitBefore = await captureGitSnapshot(this.workspace);
|
|
250
|
+
if (!gitBefore.available || gitBefore.commit === 'unknown') {
|
|
251
|
+
throw new Error('tsrust matrix build requires a Git checkout');
|
|
252
|
+
}
|
|
253
|
+
if (gitBefore.status.length > 0 || (gitBefore.unsafeIndexFlags?.length || 0) > 0) {
|
|
254
|
+
throw new Error('tsrust matrix build requires a clean Git worktree');
|
|
255
|
+
}
|
|
256
|
+
await this.assertPlanMatchesCommit(gitBefore.commit);
|
|
257
|
+
await this.check();
|
|
258
|
+
assertGitSnapshotUnchanged(gitBefore, await captureGitSnapshot(this.workspace));
|
|
259
|
+
await this.assertBundleCompatibleSource();
|
|
260
|
+
assertGitSnapshotUnchanged(gitBefore, await captureGitSnapshot(this.workspace));
|
|
261
|
+
const projectIdentity = this.readProjectIdentity();
|
|
262
|
+
const binaries = await this.readBinaryTargets();
|
|
263
|
+
const runParent = path.join(this.workspace, '.nogit', 'tsrust-matrix');
|
|
264
|
+
await this.ensureLocalRunParent(runParent);
|
|
265
|
+
const runDirectory = await fs.promises.mkdtemp(
|
|
266
|
+
path.join(runParent, `${gitBefore.commit.slice(0, 12)}-`),
|
|
267
|
+
);
|
|
268
|
+
const bundlePath = path.join(runDirectory, 'source.bundle');
|
|
269
|
+
const sourceDirectories: string[] = [];
|
|
270
|
+
const remoteWorkspaces: IRemoteWorkspace[] = [];
|
|
271
|
+
let completed = false;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
await this.commandRunner.execute('git', ['bundle', 'create', bundlePath, 'HEAD'], {
|
|
275
|
+
cwd: this.workspace,
|
|
276
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
277
|
+
});
|
|
278
|
+
await this.commandRunner.execute('git', ['bundle', 'verify', bundlePath], {
|
|
279
|
+
cwd: this.workspace,
|
|
280
|
+
capture: true,
|
|
281
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
282
|
+
});
|
|
283
|
+
assertGitSnapshotUnchanged(gitBefore, await captureGitSnapshot(this.workspace));
|
|
284
|
+
|
|
285
|
+
for (const worker of this.plan.workers) {
|
|
286
|
+
if (worker.config.transport === 'local') {
|
|
287
|
+
sourceDirectories.push(
|
|
288
|
+
await this.buildLocalWorker(
|
|
289
|
+
worker,
|
|
290
|
+
bundlePath,
|
|
291
|
+
runDirectory,
|
|
292
|
+
gitBefore.commit,
|
|
293
|
+
projectIdentity,
|
|
294
|
+
binaries,
|
|
295
|
+
),
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
const remoteWorkspace = this.describeRemoteWorkspace(
|
|
299
|
+
worker,
|
|
300
|
+
this.resolveSshWorker(worker),
|
|
301
|
+
gitBefore.commit,
|
|
302
|
+
);
|
|
303
|
+
remoteWorkspaces.push(remoteWorkspace);
|
|
304
|
+
await this.createRemoteWorkspace(remoteWorkspace, gitBefore.commit);
|
|
305
|
+
sourceDirectories.push(
|
|
306
|
+
await this.buildSshWorker(
|
|
307
|
+
remoteWorkspace,
|
|
308
|
+
bundlePath,
|
|
309
|
+
runDirectory,
|
|
310
|
+
gitBefore.commit,
|
|
311
|
+
projectIdentity,
|
|
312
|
+
binaries,
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const gitAfterWorkers = await captureGitSnapshot(this.workspace);
|
|
319
|
+
assertGitSnapshotUnchanged(gitBefore, gitAfterWorkers);
|
|
320
|
+
this.assertProjectIdentityUnchanged(projectIdentity);
|
|
321
|
+
while (remoteWorkspaces.length > 0) {
|
|
322
|
+
const remoteWorkspace = remoteWorkspaces[0];
|
|
323
|
+
await this.cleanupRemoteWorkspace(remoteWorkspace, gitBefore.commit);
|
|
324
|
+
remoteWorkspaces.shift();
|
|
325
|
+
}
|
|
326
|
+
const gitAfterRemoteCleanup = await captureGitSnapshot(this.workspace);
|
|
327
|
+
assertGitSnapshotUnchanged(gitBefore, gitAfterRemoteCleanup);
|
|
328
|
+
this.assertProjectIdentityUnchanged(projectIdentity);
|
|
329
|
+
const assembly = await new ArtifactAssembler({
|
|
330
|
+
workspace: this.workspace,
|
|
331
|
+
sourceDirectories,
|
|
332
|
+
expectedTargets: this.plan.expectedTargets,
|
|
333
|
+
expectedBinaries: binaries,
|
|
334
|
+
expectedProjectName: projectIdentity.projectName,
|
|
335
|
+
expectedProjectVersion: projectIdentity.projectVersion,
|
|
336
|
+
expectedGitCommit: gitBefore.commit,
|
|
337
|
+
expectedTsrustVersion: commitinfo.version,
|
|
338
|
+
}).assemble();
|
|
339
|
+
if (assembly.cleanupPending) {
|
|
340
|
+
console.warn('Matrix assembly committed; stale transaction cleanup will retry next run.');
|
|
341
|
+
}
|
|
342
|
+
completed = true;
|
|
343
|
+
try {
|
|
344
|
+
await fs.promises.rm(runDirectory, { recursive: true });
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.warn(
|
|
347
|
+
`Matrix assembly succeeded but local staging cleanup failed at ${runDirectory}: ${
|
|
348
|
+
error instanceof Error ? error.message : String(error)
|
|
349
|
+
}`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
...this.checkResult(),
|
|
354
|
+
gitCommit: gitBefore.commit,
|
|
355
|
+
assembly,
|
|
356
|
+
};
|
|
357
|
+
} finally {
|
|
358
|
+
if (!completed) {
|
|
359
|
+
console.error(`Retained local tsrust matrix diagnostics at ${runDirectory}`);
|
|
360
|
+
for (const remoteWorkspace of remoteWorkspaces) {
|
|
361
|
+
console.error(
|
|
362
|
+
`${remoteWorkspace.worker.host} matrix diagnostics may remain at ${remoteWorkspace.directory}`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
console.error('Matrix diagnostics are not pruned automatically; inspect before removal.');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private checkResult(): INativeMatrixCheckResult {
|
|
371
|
+
return {
|
|
372
|
+
workers: this.plan.workers.map((worker) => worker.host),
|
|
373
|
+
targets: this.plan.expectedTargets.map((target) => target.friendly),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private assertForwardEnvironmentGrant(): void {
|
|
378
|
+
const granted = new Set(
|
|
379
|
+
(this.environment[forwardEnvironmentGrantName] || '')
|
|
380
|
+
.split(',')
|
|
381
|
+
.map((name) => name.trim())
|
|
382
|
+
.filter(Boolean),
|
|
383
|
+
);
|
|
384
|
+
const reserved = new Set<string>([
|
|
385
|
+
...defaultWorkerEnvironmentNames,
|
|
386
|
+
...transportEnvironmentNames,
|
|
387
|
+
'HOME',
|
|
388
|
+
'CARGO_HOME',
|
|
389
|
+
forwardEnvironmentGrantName,
|
|
390
|
+
]);
|
|
391
|
+
const forbidden = this.plan.forwardEnvironment.filter(
|
|
392
|
+
(name) => reserved.has(name) || !granted.has(name),
|
|
393
|
+
);
|
|
394
|
+
if (forbidden.length > 0) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`matrix.forwardEnvironment is not granted by ${forwardEnvironmentGrantName}: ${forbidden.join(', ')}`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private workerEnvironment(homeArg: string, cargoHomeArg: string): NodeJS.ProcessEnv {
|
|
402
|
+
const environment: NodeJS.ProcessEnv = {};
|
|
403
|
+
for (const name of [
|
|
404
|
+
...defaultWorkerEnvironmentNames,
|
|
405
|
+
...this.plan.forwardEnvironment,
|
|
406
|
+
]) {
|
|
407
|
+
if (this.environment[name] !== undefined) environment[name] = this.environment[name];
|
|
408
|
+
}
|
|
409
|
+
environment.HOME = homeArg;
|
|
410
|
+
environment.CARGO_HOME = cargoHomeArg;
|
|
411
|
+
if (!environment.RUSTUP_HOME && this.environment.HOME) {
|
|
412
|
+
environment.RUSTUP_HOME = path.join(this.environment.HOME, '.rustup');
|
|
413
|
+
}
|
|
414
|
+
return environment;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private transportEnvironment(): NodeJS.ProcessEnv {
|
|
418
|
+
const environment: NodeJS.ProcessEnv = {};
|
|
419
|
+
for (const name of [...transportEnvironmentNames, ...this.plan.forwardEnvironment]) {
|
|
420
|
+
if (this.environment[name] !== undefined) environment[name] = this.environment[name];
|
|
421
|
+
}
|
|
422
|
+
return environment;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private remainingBuildTimeout(deadlineArg: number, workerArg: TMatrixHost): number {
|
|
426
|
+
const remaining = deadlineArg - Date.now();
|
|
427
|
+
if (remaining <= 0) {
|
|
428
|
+
throw new Error(`Matrix worker ${workerArg} exceeded its shared build timeout`);
|
|
429
|
+
}
|
|
430
|
+
return remaining;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private async assertInstalledTsrustVersion(workspaceArg: string): Promise<void> {
|
|
434
|
+
const packagePath = path.join(
|
|
435
|
+
workspaceArg,
|
|
436
|
+
'node_modules',
|
|
437
|
+
'@git.zone',
|
|
438
|
+
'tsrust',
|
|
439
|
+
'package.json',
|
|
440
|
+
);
|
|
441
|
+
const packageJson = JSON.parse(await fs.promises.readFile(packagePath, 'utf8')) as {
|
|
442
|
+
name?: unknown;
|
|
443
|
+
version?: unknown;
|
|
444
|
+
};
|
|
445
|
+
if (packageJson.name !== '@git.zone/tsrust' || packageJson.version !== commitinfo.version) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Project-local tsrust ${String(packageJson.version || 'unknown')} does not match coordinator ${commitinfo.version}`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private async assertPlanMatchesCommit(gitCommitArg: string): Promise<void> {
|
|
453
|
+
const committedFile = await this.commandRunner.execute(
|
|
454
|
+
'git',
|
|
455
|
+
['show', `${gitCommitArg}:.smartconfig.json`],
|
|
456
|
+
{
|
|
457
|
+
cwd: this.workspace,
|
|
458
|
+
capture: true,
|
|
459
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
const committedRoot = JSON.parse(committedFile.stdout) as Record<string, unknown>;
|
|
463
|
+
const committedConfig = committedRoot['@git.zone/tsrust'];
|
|
464
|
+
const committedPlan = resolveMatrixPlan(committedConfig as ITsrustConfig);
|
|
465
|
+
if (JSON.stringify(committedPlan) !== JSON.stringify(this.plan)) {
|
|
466
|
+
throw new Error('Matrix configuration does not match the exact Git commit');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private remoteWorkerEnvironmentFunction(homeArg: string, cargoHomeArg: string): string {
|
|
471
|
+
const names = [
|
|
472
|
+
...new Set([...defaultWorkerEnvironmentNames, ...this.plan.forwardEnvironment]),
|
|
473
|
+
];
|
|
474
|
+
const assignments = names.map((name) => `${name}="\${${name}-}"`).join(' ');
|
|
475
|
+
return `run_matrix_worker() { worker_rustup_home="\${RUSTUP_HOME:-\${HOME}/.rustup}"; env -i ${assignments} HOME=${shellQuote(homeArg)} CARGO_HOME=${shellQuote(cargoHomeArg)} RUSTUP_HOME="$worker_rustup_home" "$@"; }`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private async assertBundleCompatibleSource(): Promise<void> {
|
|
479
|
+
const staged = await this.commandRunner.execute(
|
|
480
|
+
'git',
|
|
481
|
+
['ls-files', '--stage', '-z'],
|
|
482
|
+
{
|
|
483
|
+
cwd: this.workspace,
|
|
484
|
+
capture: true,
|
|
485
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
for (const entry of staged.stdout.split('\0').filter(Boolean)) {
|
|
489
|
+
const mode = entry.slice(0, entry.indexOf(' '));
|
|
490
|
+
if (mode === '160000') {
|
|
491
|
+
throw new Error('tsrust matrix builds do not support Git submodules');
|
|
492
|
+
}
|
|
493
|
+
if (mode === '120000') {
|
|
494
|
+
throw new Error('tsrust matrix builds do not support tracked symbolic links');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const tracked = await this.commandRunner.execute('git', ['ls-files', '-z'], {
|
|
499
|
+
cwd: this.workspace,
|
|
500
|
+
capture: true,
|
|
501
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
502
|
+
});
|
|
503
|
+
if (tracked.stdout.length > 0) {
|
|
504
|
+
const attributes = await this.commandRunner.execute(
|
|
505
|
+
'git',
|
|
506
|
+
['check-attr', '-z', '--stdin', 'filter'],
|
|
507
|
+
{
|
|
508
|
+
cwd: this.workspace,
|
|
509
|
+
capture: true,
|
|
510
|
+
input: tracked.stdout,
|
|
511
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
const values = attributes.stdout.split('\0');
|
|
515
|
+
for (let index = 0; index + 2 < values.length; index += 3) {
|
|
516
|
+
if (!['unspecified', 'unset'].includes(values[index + 2])) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
`tsrust matrix builds do not support checkout filters: ${values[index]}`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const pointerCandidates = await this.commandRunner.execute(
|
|
525
|
+
'git',
|
|
526
|
+
[
|
|
527
|
+
'grep',
|
|
528
|
+
'-I',
|
|
529
|
+
'-l',
|
|
530
|
+
'-z',
|
|
531
|
+
'-e',
|
|
532
|
+
'^version https://',
|
|
533
|
+
'HEAD',
|
|
534
|
+
'--',
|
|
535
|
+
],
|
|
536
|
+
{
|
|
537
|
+
cwd: this.workspace,
|
|
538
|
+
capture: true,
|
|
539
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
540
|
+
allowedExitCodes: [0, 1],
|
|
541
|
+
},
|
|
542
|
+
);
|
|
543
|
+
for (const candidate of pointerCandidates.stdout.split('\0').filter(Boolean)) {
|
|
544
|
+
const filePath = candidate.startsWith('HEAD:') ? candidate.slice(5) : candidate;
|
|
545
|
+
const content = await this.commandRunner.execute('git', ['show', `HEAD:${filePath}`], {
|
|
546
|
+
cwd: this.workspace,
|
|
547
|
+
capture: true,
|
|
548
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
549
|
+
});
|
|
550
|
+
if (
|
|
551
|
+
/^version https:\/\/[^\r\n]+\r?\n(?:ext-[^\r\n]+\r?\n)*oid sha256:[a-f0-9]{64}\r?\nsize [0-9]+(?:\r?\n|$)/.test(
|
|
552
|
+
content.stdout,
|
|
553
|
+
)
|
|
554
|
+
) {
|
|
555
|
+
throw new Error(`tsrust matrix builds do not support Git LFS pointer ${filePath}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private async checkLocalWorker(workerArg: IResolvedMatrixWorker): Promise<void> {
|
|
561
|
+
const actualHost = friendlyTargetName(new ToolchainManager().getHostTriple());
|
|
562
|
+
if (actualHost !== workerArg.host) {
|
|
563
|
+
throw new Error(
|
|
564
|
+
`Local matrix worker expected ${workerArg.host} but coordinator is ${actualHost}`,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
await this.checkLocalToolchain(workerArg.host);
|
|
568
|
+
await this.checkLocalExecution(workerArg);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private async checkLocalToolchain(hostArg: TMatrixHost): Promise<void> {
|
|
572
|
+
let hasSystemCargo = false;
|
|
573
|
+
try {
|
|
574
|
+
await this.commandRunner.execute('cargo', ['--version'], {
|
|
575
|
+
cwd: this.workspace,
|
|
576
|
+
capture: true,
|
|
577
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
578
|
+
});
|
|
579
|
+
hasSystemCargo = true;
|
|
580
|
+
} catch {
|
|
581
|
+
hasSystemCargo = false;
|
|
582
|
+
}
|
|
583
|
+
if (hasSystemCargo) {
|
|
584
|
+
await this.commandRunner.execute('rustup', ['--version'], {
|
|
585
|
+
cwd: this.workspace,
|
|
586
|
+
capture: true,
|
|
587
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
588
|
+
});
|
|
589
|
+
} else {
|
|
590
|
+
const toolchainManager = new ToolchainManager();
|
|
591
|
+
if (await toolchainManager.isInstalled()) {
|
|
592
|
+
await this.commandRunner.execute(
|
|
593
|
+
path.join(ToolchainManager.BIN_DIR, 'cargo'),
|
|
594
|
+
['--version'],
|
|
595
|
+
{
|
|
596
|
+
cwd: this.workspace,
|
|
597
|
+
capture: true,
|
|
598
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
599
|
+
},
|
|
600
|
+
);
|
|
601
|
+
await this.commandRunner.execute(
|
|
602
|
+
path.join(ToolchainManager.BIN_DIR, 'rustup'),
|
|
603
|
+
['--version'],
|
|
604
|
+
{
|
|
605
|
+
cwd: this.workspace,
|
|
606
|
+
capture: true,
|
|
607
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
608
|
+
},
|
|
609
|
+
);
|
|
610
|
+
} else {
|
|
611
|
+
await this.commandRunner.execute('curl', ['--version'], {
|
|
612
|
+
cwd: this.workspace,
|
|
613
|
+
capture: true,
|
|
614
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (hostArg.startsWith('macos_')) {
|
|
619
|
+
await this.commandRunner.execute('xcrun', ['--find', 'clang'], {
|
|
620
|
+
cwd: this.workspace,
|
|
621
|
+
capture: true,
|
|
622
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
623
|
+
});
|
|
624
|
+
} else {
|
|
625
|
+
await this.commandRunner.execute('cc', ['--version'], {
|
|
626
|
+
cwd: this.workspace,
|
|
627
|
+
capture: true,
|
|
628
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private async checkLocalExecution(workerArg: IResolvedMatrixWorker): Promise<void> {
|
|
634
|
+
for (const target of workerArg.targets) {
|
|
635
|
+
if (target.friendly === workerArg.host) continue;
|
|
636
|
+
if (workerArg.host.startsWith('macos_') && target.friendly.startsWith('macos_')) {
|
|
637
|
+
const architecture = target.friendly === 'macos_arm64' ? '-arm64' : '-x86_64';
|
|
638
|
+
await this.commandRunner.execute('/usr/bin/arch', [architecture, '/usr/bin/true'], {
|
|
639
|
+
cwd: this.workspace,
|
|
640
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
641
|
+
});
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (workerArg.host.startsWith('linux_') && target.friendly.startsWith('linux_')) {
|
|
645
|
+
await this.checkLocalLinuxCrossExecution(target.friendly);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
throw new Error(
|
|
649
|
+
`Matrix worker ${workerArg.host} cannot execute smoke tests for ${target.friendly}`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private async checkLocalLinuxCrossExecution(targetArg: string): Promise<void> {
|
|
655
|
+
const compiler = linuxProbeCompiler(targetArg);
|
|
656
|
+
if (!compiler) {
|
|
657
|
+
throw new Error(`No matrix execution probe is available for ${targetArg}`);
|
|
658
|
+
}
|
|
659
|
+
const probeParent = path.join(this.workspace, '.nogit', 'tsrust-matrix-probes');
|
|
660
|
+
await fs.promises.mkdir(probeParent, { recursive: true, mode: 0o700 });
|
|
661
|
+
const probeDirectory = await fs.promises.mkdtemp(path.join(probeParent, `${targetArg}-`));
|
|
662
|
+
try {
|
|
663
|
+
const sourcePath = path.join(probeDirectory, 'probe.c');
|
|
664
|
+
const binaryPath = path.join(probeDirectory, 'probe');
|
|
665
|
+
await fs.promises.writeFile(sourcePath, 'int main(void) { return 0; }\n');
|
|
666
|
+
await this.commandRunner.execute(
|
|
667
|
+
compiler,
|
|
668
|
+
['-static', '-O2', sourcePath, '-o', binaryPath],
|
|
669
|
+
{
|
|
670
|
+
cwd: this.workspace,
|
|
671
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
672
|
+
},
|
|
673
|
+
);
|
|
674
|
+
await this.commandRunner.execute(binaryPath, [], {
|
|
675
|
+
cwd: this.workspace,
|
|
676
|
+
timeoutMs: capabilityCommandTimeoutMs,
|
|
677
|
+
});
|
|
678
|
+
} finally {
|
|
679
|
+
await fs.promises.rm(probeDirectory, { recursive: true, force: true });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private resolveSshWorker(workerArg: IResolvedMatrixWorker): IResolvedSshWorker {
|
|
684
|
+
if (workerArg.config.transport !== 'ssh') {
|
|
685
|
+
throw new Error(`Matrix worker ${workerArg.host} is not configured for SSH`);
|
|
686
|
+
}
|
|
687
|
+
const destination = this.environment[workerArg.config.destinationEnv]?.trim() || '';
|
|
688
|
+
const temporaryRoot = (
|
|
689
|
+
this.environment[workerArg.config.temporaryRootEnv]?.trim() || ''
|
|
690
|
+
).replace(/\/+$/, '');
|
|
691
|
+
if (!/^[A-Za-z0-9._-]+(?:@[A-Za-z0-9._-]+)?$/.test(destination)) {
|
|
692
|
+
throw new Error(
|
|
693
|
+
`${workerArg.config.destinationEnv} must contain one SSH alias or user@host destination without options`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
if (
|
|
697
|
+
!/^\/[A-Za-z0-9._/-]+$/.test(temporaryRoot) ||
|
|
698
|
+
path.posix.normalize(temporaryRoot) !== temporaryRoot ||
|
|
699
|
+
temporaryRoot.split('/').filter(Boolean).length < 2
|
|
700
|
+
) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
`${workerArg.config.temporaryRootEnv} must contain a non-root absolute path without spaces`,
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
return { destination, temporaryRoot };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private async checkSshWorker(
|
|
709
|
+
workerArg: IResolvedMatrixWorker,
|
|
710
|
+
runtimeArg: IResolvedSshWorker,
|
|
711
|
+
): Promise<void> {
|
|
712
|
+
const details = expectedHostDetails(workerArg.host);
|
|
713
|
+
const token = crypto.randomUUID();
|
|
714
|
+
const probeDirectory = `${runtimeArg.temporaryRoot}/tsrust-matrix.check.${token}`;
|
|
715
|
+
const commands = [
|
|
716
|
+
'set -eu',
|
|
717
|
+
'umask 077',
|
|
718
|
+
`test "$(uname -s)" = ${shellQuote(details.system)}`,
|
|
719
|
+
`test "$(uname -m)" = ${shellQuote(details.machine)}`,
|
|
720
|
+
'command -v git >/dev/null',
|
|
721
|
+
'command -v node >/dev/null',
|
|
722
|
+
'command -v pnpm >/dev/null',
|
|
723
|
+
this.remoteToolchainCapabilityCommand(),
|
|
724
|
+
...(details.system === 'Darwin' ? ['command -v codesign >/dev/null'] : []),
|
|
725
|
+
...(details.system === 'Darwin'
|
|
726
|
+
? ['xcrun --find clang >/dev/null']
|
|
727
|
+
: ['command -v cc >/dev/null']),
|
|
728
|
+
...this.remoteRootValidationCommands(runtimeArg.temporaryRoot),
|
|
729
|
+
`test "$(find "$root" -mindepth 1 -maxdepth 1 -type d -name 'tsrust-matrix.*' | wc -l | tr -d ' ')" -lt ${maximumRetainedDiagnosticRuns}`,
|
|
730
|
+
`probe=${shellQuote(probeDirectory)}`,
|
|
731
|
+
`test "$probe" = ${shellQuote(probeDirectory)}`,
|
|
732
|
+
'created=0',
|
|
733
|
+
'cleanup_probe() { if [ "$created" = 1 ]; then rm -rf "$probe"; fi; }',
|
|
734
|
+
'trap cleanup_probe EXIT HUP INT TERM',
|
|
735
|
+
'mkdir -m 700 "$probe"',
|
|
736
|
+
'created=1',
|
|
737
|
+
...this.remoteExecutionProbeCommands(workerArg, probeDirectory),
|
|
738
|
+
'rm -rf "$probe"',
|
|
739
|
+
'created=0',
|
|
740
|
+
'trap - EXIT HUP INT TERM',
|
|
741
|
+
];
|
|
742
|
+
try {
|
|
743
|
+
await this.runRemoteOwnedScript(
|
|
744
|
+
workerArg,
|
|
745
|
+
runtimeArg,
|
|
746
|
+
commands.join('\n'),
|
|
747
|
+
capabilityCommandTimeoutMs,
|
|
748
|
+
);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
throw new Error(
|
|
751
|
+
`${workerArg.host} capability check failed; diagnostics may remain at ${probeDirectory}: ${
|
|
752
|
+
error instanceof Error ? error.message : String(error)
|
|
753
|
+
}`,
|
|
754
|
+
{ cause: error },
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
private remoteToolchainCapabilityCommand(): string {
|
|
760
|
+
const root = ToolchainManager.TOOLCHAIN_DIR;
|
|
761
|
+
const cargo = path.join(ToolchainManager.BIN_DIR, 'cargo');
|
|
762
|
+
const rustup = path.join(ToolchainManager.BIN_DIR, 'rustup');
|
|
763
|
+
const installLock = `${root}.install.lock`;
|
|
764
|
+
return `if command -v cargo >/dev/null && cargo --version >/dev/null 2>&1; then command -v rustup >/dev/null; rustup --version >/dev/null; elif [ -e ${shellQuote(installLock)} ]; then exit 1; elif [ -x ${shellQuote(cargo)} ] && [ -x ${shellQuote(rustup)} ]; then node -e ${shellQuote(bundledToolchainValidationSource)} ${shellQuote(root)} ${shellQuote(cargo)} ${shellQuote(rustup)}; ${shellQuote(cargo)} --version >/dev/null; ${shellQuote(rustup)} --version >/dev/null; elif [ -e ${shellQuote(cargo)} ] || [ -e ${shellQuote(rustup)} ]; then exit 1; else command -v curl >/dev/null; fi`;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private remoteRootValidationCommands(rootArg: string): string[] {
|
|
768
|
+
return [
|
|
769
|
+
`root=${shellQuote(rootArg)}`,
|
|
770
|
+
'if [ -e "$root" ]; then test -d "$root"; test ! -L "$root"; else mkdir -p "$root"; fi',
|
|
771
|
+
'test "$(cd "$root" && pwd -P)" = "$root"',
|
|
772
|
+
'test -z "$(find "$root" -prune -perm -022 -print)"',
|
|
773
|
+
'if [ "$(uname -s)" = Darwin ]; then test "$(stat -f %u "$root")" = "$(id -u)"; else test "$(stat -c %u "$root")" = "$(id -u)"; fi',
|
|
774
|
+
];
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private remoteOwnershipValidationCommands(
|
|
778
|
+
remoteArg: IRemoteWorkspace,
|
|
779
|
+
gitCommitArg: string,
|
|
780
|
+
): string[] {
|
|
781
|
+
const ownerPath = `${remoteArg.directory}/${matrixOwnerFileName}`;
|
|
782
|
+
const ownerContents = `${remoteArg.ownerToken}\n${gitCommitArg}\n${remoteArg.directory}`;
|
|
783
|
+
return [
|
|
784
|
+
`directory=${shellQuote(remoteArg.directory)}`,
|
|
785
|
+
`test "$directory" = ${shellQuote(remoteArg.directory)}`,
|
|
786
|
+
'test -d "$directory"',
|
|
787
|
+
'test ! -L "$directory"',
|
|
788
|
+
'test "$(cd "$directory/.." && pwd -P)" = "$root"',
|
|
789
|
+
'test -z "$(find "$directory" -prune -perm -077 -print)"',
|
|
790
|
+
'if [ "$(uname -s)" = Darwin ]; then test "$(stat -f %u "$directory")" = "$(id -u)"; else test "$(stat -c %u "$directory")" = "$(id -u)"; fi',
|
|
791
|
+
`test -f ${shellQuote(ownerPath)}`,
|
|
792
|
+
`test ! -L ${shellQuote(ownerPath)}`,
|
|
793
|
+
`test -z "$(find ${shellQuote(ownerPath)} -prune -perm -077 -print)"`,
|
|
794
|
+
`if [ "$(uname -s)" = Darwin ]; then test "$(stat -f %u ${shellQuote(ownerPath)})" = "$(id -u)"; else test "$(stat -c %u ${shellQuote(ownerPath)})" = "$(id -u)"; fi`,
|
|
795
|
+
`test "$(cat ${shellQuote(ownerPath)})" = ${shellQuote(ownerContents)}`,
|
|
796
|
+
];
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private remoteExecutionProbeCommands(
|
|
800
|
+
workerArg: IResolvedMatrixWorker,
|
|
801
|
+
probeDirectoryArg: string,
|
|
802
|
+
): string[] {
|
|
803
|
+
const commands: string[] = [];
|
|
804
|
+
for (const target of workerArg.targets) {
|
|
805
|
+
if (target.friendly === workerArg.host) continue;
|
|
806
|
+
if (workerArg.host.startsWith('macos_') && target.friendly.startsWith('macos_')) {
|
|
807
|
+
const architecture = target.friendly === 'macos_arm64' ? '-arm64' : '-x86_64';
|
|
808
|
+
commands.push(`/usr/bin/arch ${architecture} /usr/bin/true`);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (workerArg.host.startsWith('linux_') && target.friendly.startsWith('linux_')) {
|
|
812
|
+
const compiler = linuxProbeCompiler(target.friendly);
|
|
813
|
+
if (!compiler) {
|
|
814
|
+
throw new Error(`No matrix execution probe is available for ${target.friendly}`);
|
|
815
|
+
}
|
|
816
|
+
const sourcePath = `${probeDirectoryArg}/probe-${target.friendly}.c`;
|
|
817
|
+
const binaryPath = `${probeDirectoryArg}/probe-${target.friendly}`;
|
|
818
|
+
commands.push(
|
|
819
|
+
`printf '%s\n' 'int main(void) { return 0; }' > ${shellQuote(sourcePath)}`,
|
|
820
|
+
`${compiler} -static -O2 ${shellQuote(sourcePath)} -o ${shellQuote(binaryPath)}`,
|
|
821
|
+
shellQuote(binaryPath),
|
|
822
|
+
);
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
throw new Error(
|
|
826
|
+
`Matrix worker ${workerArg.host} cannot execute smoke tests for ${target.friendly}`,
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
return commands;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private async readBinaryTargets(): Promise<string[]> {
|
|
833
|
+
const rustDirectory = await this.detectRustDirectory();
|
|
834
|
+
const workspaceInfo = await new CargoConfig(rustDirectory).parse();
|
|
835
|
+
const binaries = [...new Set(workspaceInfo.binTargets)];
|
|
836
|
+
if (binaries.length === 0) {
|
|
837
|
+
throw new Error('tsrust matrix build requires at least one Cargo binary target');
|
|
838
|
+
}
|
|
839
|
+
for (const binary of binaries) {
|
|
840
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(binary)) {
|
|
841
|
+
throw new Error(`Invalid Cargo binary name for matrix build: ${binary}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return binaries;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
private async detectRustDirectory(): Promise<string> {
|
|
848
|
+
for (const directoryName of ['rust', 'ts_rust']) {
|
|
849
|
+
const candidate = path.join(this.workspace, directoryName);
|
|
850
|
+
try {
|
|
851
|
+
const stat = await fs.promises.stat(path.join(candidate, 'Cargo.toml'));
|
|
852
|
+
if (stat.isFile()) return candidate;
|
|
853
|
+
} catch {
|
|
854
|
+
// Try the next conventional Rust source directory.
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
throw new Error('No rust/ or ts_rust/ directory found with a Cargo.toml');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
private readProjectIdentity(): { projectName: string; projectVersion: string } {
|
|
861
|
+
const packageJson = JSON.parse(
|
|
862
|
+
fs.readFileSync(path.join(this.workspace, 'package.json'), 'utf8'),
|
|
863
|
+
) as { name?: unknown; version?: unknown };
|
|
864
|
+
if (typeof packageJson.name !== 'string' || typeof packageJson.version !== 'string') {
|
|
865
|
+
throw new Error('tsrust matrix build requires package.json name and version');
|
|
866
|
+
}
|
|
867
|
+
return { projectName: packageJson.name, projectVersion: packageJson.version };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private assertProjectIdentityUnchanged(identityArg: {
|
|
871
|
+
projectName: string;
|
|
872
|
+
projectVersion: string;
|
|
873
|
+
}): void {
|
|
874
|
+
const current = this.readProjectIdentity();
|
|
875
|
+
if (
|
|
876
|
+
current.projectName !== identityArg.projectName ||
|
|
877
|
+
current.projectVersion !== identityArg.projectVersion
|
|
878
|
+
) {
|
|
879
|
+
throw new Error('Project package identity changed during the matrix build');
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private async ensureLocalRunParent(parentArg: string): Promise<void> {
|
|
884
|
+
await fs.promises.mkdir(parentArg, { recursive: true, mode: 0o700 });
|
|
885
|
+
const stat = await fs.promises.lstat(parentArg);
|
|
886
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
887
|
+
throw new Error(`tsrust matrix state path is not a regular directory: ${parentArg}`);
|
|
888
|
+
}
|
|
889
|
+
const expectedPhysicalPath = path.join(
|
|
890
|
+
await fs.promises.realpath(this.workspace),
|
|
891
|
+
'.nogit',
|
|
892
|
+
'tsrust-matrix',
|
|
893
|
+
);
|
|
894
|
+
if ((await fs.promises.realpath(parentArg)) !== expectedPhysicalPath) {
|
|
895
|
+
throw new Error(`tsrust matrix state path traverses a symbolic link: ${parentArg}`);
|
|
896
|
+
}
|
|
897
|
+
if (
|
|
898
|
+
(typeof process.getuid === 'function' && stat.uid !== process.getuid()) ||
|
|
899
|
+
(stat.mode & 0o077) !== 0
|
|
900
|
+
) {
|
|
901
|
+
throw new Error(`tsrust matrix state path has unsafe ownership or permissions: ${parentArg}`);
|
|
902
|
+
}
|
|
903
|
+
const retainedRuns = (await fs.promises.readdir(parentArg, { withFileTypes: true })).filter(
|
|
904
|
+
(entry) => entry.isDirectory(),
|
|
905
|
+
);
|
|
906
|
+
if (retainedRuns.length >= maximumRetainedDiagnosticRuns) {
|
|
907
|
+
throw new Error(
|
|
908
|
+
`tsrust matrix retains at most ${maximumRetainedDiagnosticRuns} failed local runs; inspect and remove the reported paths before retrying`,
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private async buildLocalWorker(
|
|
914
|
+
workerArg: IResolvedMatrixWorker,
|
|
915
|
+
bundlePathArg: string,
|
|
916
|
+
runDirectoryArg: string,
|
|
917
|
+
gitCommitArg: string,
|
|
918
|
+
identityArg: { projectName: string; projectVersion: string },
|
|
919
|
+
binariesArg: string[],
|
|
920
|
+
): Promise<string> {
|
|
921
|
+
const workerParent = path.join(runDirectoryArg, workerArg.host);
|
|
922
|
+
const workerWorkspace = path.join(workerParent, 'workspace');
|
|
923
|
+
const workerHome = path.join(workerParent, 'home');
|
|
924
|
+
const workerCargoHome = path.join(workerParent, 'cargo-home');
|
|
925
|
+
await fs.promises.mkdir(workerParent, { mode: 0o700 });
|
|
926
|
+
await fs.promises.mkdir(workerHome, { mode: 0o700 });
|
|
927
|
+
await fs.promises.mkdir(workerCargoHome, { mode: 0o700 });
|
|
928
|
+
const workerEnvironment = this.workerEnvironment(workerHome, workerCargoHome);
|
|
929
|
+
const buildDeadline = Date.now() + buildCommandTimeoutMs;
|
|
930
|
+
await this.commandRunner.execute(
|
|
931
|
+
'git',
|
|
932
|
+
['-c', 'advice.detachedHead=false', 'clone', '--quiet', bundlePathArg, workerWorkspace],
|
|
933
|
+
{
|
|
934
|
+
cwd: this.workspace,
|
|
935
|
+
timeoutMs: Math.min(
|
|
936
|
+
capabilityCommandTimeoutMs,
|
|
937
|
+
this.remainingBuildTimeout(buildDeadline, workerArg.host),
|
|
938
|
+
),
|
|
939
|
+
},
|
|
940
|
+
);
|
|
941
|
+
await this.assertWorkerSource(workerWorkspace, gitCommitArg, buildDeadline, workerArg.host);
|
|
942
|
+
await this.commandRunner.execute('pnpm', ['install', '--frozen-lockfile'], {
|
|
943
|
+
cwd: workerWorkspace,
|
|
944
|
+
timeoutMs: this.remainingBuildTimeout(buildDeadline, workerArg.host),
|
|
945
|
+
environment: workerEnvironment,
|
|
946
|
+
});
|
|
947
|
+
await this.assertInstalledTsrustVersion(workerWorkspace);
|
|
948
|
+
this.remainingBuildTimeout(buildDeadline, workerArg.host);
|
|
949
|
+
await this.commandRunner.execute('pnpm', this.workerTsrustArgs(workerArg), {
|
|
950
|
+
cwd: workerWorkspace,
|
|
951
|
+
timeoutMs: this.remainingBuildTimeout(buildDeadline, workerArg.host),
|
|
952
|
+
environment: workerEnvironment,
|
|
953
|
+
});
|
|
954
|
+
await this.assertWorkerSource(workerWorkspace, gitCommitArg, buildDeadline, workerArg.host);
|
|
955
|
+
const artifactDirectory = path.join(workerWorkspace, 'dist_rust');
|
|
956
|
+
await this.assertArtifactDirectory(
|
|
957
|
+
artifactDirectory,
|
|
958
|
+
workerArg,
|
|
959
|
+
binariesArg,
|
|
960
|
+
gitCommitArg,
|
|
961
|
+
identityArg,
|
|
962
|
+
);
|
|
963
|
+
this.remainingBuildTimeout(buildDeadline, workerArg.host);
|
|
964
|
+
await this.smokeLocalArtifacts(
|
|
965
|
+
artifactDirectory,
|
|
966
|
+
workerArg,
|
|
967
|
+
binariesArg,
|
|
968
|
+
workerEnvironment,
|
|
969
|
+
buildDeadline,
|
|
970
|
+
);
|
|
971
|
+
await this.assertWorkerSource(workerWorkspace, gitCommitArg, buildDeadline, workerArg.host);
|
|
972
|
+
return artifactDirectory;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
private workerTsrustArgs(workerArg: IResolvedMatrixWorker): string[] {
|
|
976
|
+
return [
|
|
977
|
+
'exec',
|
|
978
|
+
'tsrust',
|
|
979
|
+
...workerArg.targets.flatMap((target) => ['--target', target.friendly]),
|
|
980
|
+
];
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private async assertWorkerSource(
|
|
984
|
+
workspaceArg: string,
|
|
985
|
+
gitCommitArg: string,
|
|
986
|
+
buildDeadlineArg: number,
|
|
987
|
+
workerArg: TMatrixHost,
|
|
988
|
+
): Promise<void> {
|
|
989
|
+
const commit = await this.commandRunner.execute('git', ['rev-parse', 'HEAD'], {
|
|
990
|
+
cwd: workspaceArg,
|
|
991
|
+
capture: true,
|
|
992
|
+
timeoutMs: Math.min(
|
|
993
|
+
capabilityCommandTimeoutMs,
|
|
994
|
+
this.remainingBuildTimeout(buildDeadlineArg, workerArg),
|
|
995
|
+
),
|
|
996
|
+
});
|
|
997
|
+
if (commit.stdout.trim() !== gitCommitArg) {
|
|
998
|
+
throw new Error(`Matrix worker cloned the wrong Git commit: ${workspaceArg}`);
|
|
999
|
+
}
|
|
1000
|
+
const status = await this.commandRunner.execute(
|
|
1001
|
+
'git',
|
|
1002
|
+
['status', '--porcelain=v1', '--untracked-files=all'],
|
|
1003
|
+
{
|
|
1004
|
+
cwd: workspaceArg,
|
|
1005
|
+
capture: true,
|
|
1006
|
+
timeoutMs: Math.min(
|
|
1007
|
+
capabilityCommandTimeoutMs,
|
|
1008
|
+
this.remainingBuildTimeout(buildDeadlineArg, workerArg),
|
|
1009
|
+
),
|
|
1010
|
+
},
|
|
1011
|
+
);
|
|
1012
|
+
if (status.stdout.length > 0) {
|
|
1013
|
+
throw new Error(`Matrix worker source is dirty: ${workspaceArg}`);
|
|
1014
|
+
}
|
|
1015
|
+
const index = await this.commandRunner.execute('git', ['ls-files', '-v', '-z'], {
|
|
1016
|
+
cwd: workspaceArg,
|
|
1017
|
+
capture: true,
|
|
1018
|
+
timeoutMs: Math.min(
|
|
1019
|
+
capabilityCommandTimeoutMs,
|
|
1020
|
+
this.remainingBuildTimeout(buildDeadlineArg, workerArg),
|
|
1021
|
+
),
|
|
1022
|
+
});
|
|
1023
|
+
if (
|
|
1024
|
+
index.stdout
|
|
1025
|
+
.split('\0')
|
|
1026
|
+
.some((entry) => entry.length > 0 && !entry.startsWith('H '))
|
|
1027
|
+
) {
|
|
1028
|
+
throw new Error(`Matrix worker source uses unsafe Git index flags: ${workspaceArg}`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
private async assertArtifactDirectory(
|
|
1033
|
+
directoryArg: string,
|
|
1034
|
+
workerArg: IResolvedMatrixWorker,
|
|
1035
|
+
binariesArg: string[],
|
|
1036
|
+
gitCommitArg: string,
|
|
1037
|
+
identityArg: { projectName: string; projectVersion: string },
|
|
1038
|
+
): Promise<void> {
|
|
1039
|
+
const expectedNames = artifactNamesFor(workerArg, binariesArg);
|
|
1040
|
+
const actualNames = (await fs.promises.readdir(directoryArg)).sort();
|
|
1041
|
+
if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
`Unexpected ${workerArg.host} matrix artifacts: ${actualNames.join(', ')}`,
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
let aggregateBytes = 0;
|
|
1047
|
+
for (const target of workerArg.targets) {
|
|
1048
|
+
for (const binary of binariesArg) {
|
|
1049
|
+
const binaryPath = path.join(directoryArg, `${binary}_${target.friendly}`);
|
|
1050
|
+
const binaryStat = await fs.promises.lstat(binaryPath);
|
|
1051
|
+
if (!binaryStat.isFile() || binaryStat.isSymbolicLink() || !(binaryStat.mode & 0o111)) {
|
|
1052
|
+
throw new Error(`Matrix artifact is not an executable regular file: ${binaryPath}`);
|
|
1053
|
+
}
|
|
1054
|
+
if (binaryStat.size > MAX_ARTIFACT_BYTES) {
|
|
1055
|
+
throw new Error(`Matrix artifact exceeds the size limit: ${binaryPath}`);
|
|
1056
|
+
}
|
|
1057
|
+
const sidecarStat = await fs.promises.lstat(ProvenanceStore.sidecarPath(binaryPath));
|
|
1058
|
+
if (!sidecarStat.isFile() || sidecarStat.isSymbolicLink()) {
|
|
1059
|
+
throw new Error(`Matrix provenance is not a regular file: ${binaryPath}`);
|
|
1060
|
+
}
|
|
1061
|
+
if (sidecarStat.size > MAX_PROVENANCE_SIDECAR_BYTES) {
|
|
1062
|
+
throw new Error(`Matrix provenance exceeds the size limit: ${binaryPath}`);
|
|
1063
|
+
}
|
|
1064
|
+
aggregateBytes += binaryStat.size + sidecarStat.size;
|
|
1065
|
+
if (aggregateBytes > MAX_ASSEMBLY_INPUT_BYTES) {
|
|
1066
|
+
throw new Error('Matrix artifacts exceed the aggregate size limit');
|
|
1067
|
+
}
|
|
1068
|
+
const buildInfo = await ProvenanceStore.readSidecar(binaryPath);
|
|
1069
|
+
if (
|
|
1070
|
+
buildInfo.binary !== binary ||
|
|
1071
|
+
buildInfo.target !== target.friendly ||
|
|
1072
|
+
buildInfo.projectName !== identityArg.projectName ||
|
|
1073
|
+
buildInfo.projectVersion !== identityArg.projectVersion ||
|
|
1074
|
+
buildInfo.gitCommit !== gitCommitArg ||
|
|
1075
|
+
buildInfo.gitDirty !== false ||
|
|
1076
|
+
buildInfo.tsrustVersion !== commitinfo.version
|
|
1077
|
+
) {
|
|
1078
|
+
throw new Error(`Matrix provenance does not match the build request: ${binaryPath}`);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
private async smokeLocalArtifacts(
|
|
1085
|
+
directoryArg: string,
|
|
1086
|
+
workerArg: IResolvedMatrixWorker,
|
|
1087
|
+
binariesArg: string[],
|
|
1088
|
+
environmentArg: NodeJS.ProcessEnv,
|
|
1089
|
+
buildDeadlineArg: number,
|
|
1090
|
+
): Promise<void> {
|
|
1091
|
+
if (!this.plan.smokeTestArgs) return;
|
|
1092
|
+
for (const target of workerArg.targets) {
|
|
1093
|
+
for (const binary of binariesArg) {
|
|
1094
|
+
const binaryPath = path.join(directoryArg, `${binary}_${target.friendly}`);
|
|
1095
|
+
if (target.friendly.startsWith('macos_')) {
|
|
1096
|
+
const architecture = target.friendly === 'macos_arm64' ? '-arm64' : '-x86_64';
|
|
1097
|
+
await this.commandRunner.execute(
|
|
1098
|
+
'/usr/bin/arch',
|
|
1099
|
+
[architecture, binaryPath, ...this.plan.smokeTestArgs],
|
|
1100
|
+
{
|
|
1101
|
+
cwd: directoryArg,
|
|
1102
|
+
timeoutMs: Math.min(
|
|
1103
|
+
capabilityCommandTimeoutMs,
|
|
1104
|
+
this.remainingBuildTimeout(buildDeadlineArg, workerArg.host),
|
|
1105
|
+
),
|
|
1106
|
+
environment: environmentArg,
|
|
1107
|
+
},
|
|
1108
|
+
);
|
|
1109
|
+
} else {
|
|
1110
|
+
await this.commandRunner.execute(binaryPath, this.plan.smokeTestArgs, {
|
|
1111
|
+
cwd: directoryArg,
|
|
1112
|
+
timeoutMs: Math.min(
|
|
1113
|
+
capabilityCommandTimeoutMs,
|
|
1114
|
+
this.remainingBuildTimeout(buildDeadlineArg, workerArg.host),
|
|
1115
|
+
),
|
|
1116
|
+
environment: environmentArg,
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
private describeRemoteWorkspace(
|
|
1124
|
+
workerArg: IResolvedMatrixWorker,
|
|
1125
|
+
runtimeArg: IResolvedSshWorker,
|
|
1126
|
+
gitCommitArg: string,
|
|
1127
|
+
): IRemoteWorkspace {
|
|
1128
|
+
const ownerToken = crypto.randomUUID();
|
|
1129
|
+
const directory = `${runtimeArg.temporaryRoot}/tsrust-matrix.${gitCommitArg}.${workerArg.host}.${ownerToken}`;
|
|
1130
|
+
return { worker: workerArg, runtime: runtimeArg, directory, ownerToken, gitCommit: gitCommitArg };
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
private async createRemoteWorkspace(
|
|
1134
|
+
remoteArg: IRemoteWorkspace,
|
|
1135
|
+
gitCommitArg: string,
|
|
1136
|
+
): Promise<void> {
|
|
1137
|
+
const { ownerToken, directory } = remoteArg;
|
|
1138
|
+
const ownerContents = `${ownerToken}\n${gitCommitArg}\n${directory}`;
|
|
1139
|
+
const ownerPath = `${directory}/${matrixOwnerFileName}`;
|
|
1140
|
+
const script = [
|
|
1141
|
+
'set -eu',
|
|
1142
|
+
'umask 077',
|
|
1143
|
+
...this.remoteRootValidationCommands(remoteArg.runtime.temporaryRoot),
|
|
1144
|
+
`test "$(find "$root" -mindepth 1 -maxdepth 1 -type d -name 'tsrust-matrix.*' | wc -l | tr -d ' ')" -lt ${maximumRetainedDiagnosticRuns}`,
|
|
1145
|
+
`directory=${shellQuote(directory)}`,
|
|
1146
|
+
`test "$directory" = ${shellQuote(directory)}`,
|
|
1147
|
+
'created=0',
|
|
1148
|
+
`cleanup_unowned() { if [ "$created" = 1 ]; then rm -f ${shellQuote(ownerPath)}; rmdir "$directory" 2>/dev/null || true; fi; }`,
|
|
1149
|
+
'trap cleanup_unowned EXIT HUP INT TERM',
|
|
1150
|
+
'mkdir -m 700 "$directory"',
|
|
1151
|
+
'created=1',
|
|
1152
|
+
`printf '%s\n' ${shellQuote(ownerToken)} ${shellQuote(gitCommitArg)} ${shellQuote(directory)} > ${shellQuote(ownerPath)}`,
|
|
1153
|
+
`chmod 600 ${shellQuote(ownerPath)}`,
|
|
1154
|
+
`test ! -L ${shellQuote(ownerPath)}`,
|
|
1155
|
+
`test "$(cat ${shellQuote(ownerPath)})" = ${shellQuote(ownerContents)}`,
|
|
1156
|
+
'trap - EXIT HUP INT TERM',
|
|
1157
|
+
].join('\n');
|
|
1158
|
+
await this.runRemoteLoginScript(
|
|
1159
|
+
remoteArg.worker,
|
|
1160
|
+
remoteArg.runtime,
|
|
1161
|
+
script,
|
|
1162
|
+
capabilityCommandTimeoutMs,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
private async buildSshWorker(
|
|
1167
|
+
remoteArg: IRemoteWorkspace,
|
|
1168
|
+
bundlePathArg: string,
|
|
1169
|
+
runDirectoryArg: string,
|
|
1170
|
+
gitCommitArg: string,
|
|
1171
|
+
identityArg: { projectName: string; projectVersion: string },
|
|
1172
|
+
binariesArg: string[],
|
|
1173
|
+
): Promise<string> {
|
|
1174
|
+
const buildDeadline = Date.now() + buildCommandTimeoutMs;
|
|
1175
|
+
const remoteBundle = `${remoteArg.directory}/source.bundle`;
|
|
1176
|
+
const remoteWorkspace = `${remoteArg.directory}/workspace`;
|
|
1177
|
+
const remoteBundleTemporary = `${remoteBundle}.${crypto.randomUUID()}.tmp`;
|
|
1178
|
+
await this.runRemoteTransferScript(
|
|
1179
|
+
remoteArg,
|
|
1180
|
+
[
|
|
1181
|
+
`bundle=${shellQuote(remoteBundle)}`,
|
|
1182
|
+
`bundle_temporary=${shellQuote(remoteBundleTemporary)}`,
|
|
1183
|
+
'trap \'rm -f "$bundle_temporary"\' EXIT HUP INT TERM',
|
|
1184
|
+
'cat > "$bundle_temporary"',
|
|
1185
|
+
'chmod 600 "$bundle_temporary"',
|
|
1186
|
+
'mv "$bundle_temporary" "$bundle"',
|
|
1187
|
+
'trap - EXIT HUP INT TERM',
|
|
1188
|
+
].join('\n'),
|
|
1189
|
+
{
|
|
1190
|
+
inputFile: bundlePathArg,
|
|
1191
|
+
timeoutMs: this.remainingBuildTimeout(buildDeadline, remoteArg.worker.host),
|
|
1192
|
+
},
|
|
1193
|
+
);
|
|
1194
|
+
const expectedNames = artifactNamesFor(remoteArg.worker, binariesArg);
|
|
1195
|
+
const remoteDist = `${remoteWorkspace}/dist_rust`;
|
|
1196
|
+
const remoteHome = `${remoteArg.directory}/home`;
|
|
1197
|
+
const remoteCargoHome = `${remoteArg.directory}/cargo-home`;
|
|
1198
|
+
const buildCommands = [
|
|
1199
|
+
'set -eu',
|
|
1200
|
+
...this.remoteRootValidationCommands(remoteArg.runtime.temporaryRoot),
|
|
1201
|
+
...this.remoteOwnershipValidationCommands(remoteArg, gitCommitArg),
|
|
1202
|
+
`bundle=${shellQuote(remoteBundle)}`,
|
|
1203
|
+
`workspace=${shellQuote(remoteWorkspace)}`,
|
|
1204
|
+
`expected_commit=${shellQuote(gitCommitArg)}`,
|
|
1205
|
+
`mkdir -m 700 ${shellQuote(remoteHome)} ${shellQuote(remoteCargoHome)}`,
|
|
1206
|
+
'git -c advice.detachedHead=false clone --quiet "$bundle" "$workspace"',
|
|
1207
|
+
'cd "$workspace"',
|
|
1208
|
+
'test "$(git rev-parse HEAD)" = "$expected_commit"',
|
|
1209
|
+
'test -z "$(git status --porcelain=v1 --untracked-files=all)"',
|
|
1210
|
+
`test -z "$(git ls-files -v | grep -v '^H ' || true)"`,
|
|
1211
|
+
this.remoteWorkerEnvironmentFunction(remoteHome, remoteCargoHome),
|
|
1212
|
+
['run_matrix_worker', 'pnpm', 'install', '--frozen-lockfile']
|
|
1213
|
+
.map(shellQuote)
|
|
1214
|
+
.join(' '),
|
|
1215
|
+
[
|
|
1216
|
+
'run_matrix_worker',
|
|
1217
|
+
'node',
|
|
1218
|
+
'-e',
|
|
1219
|
+
`const value = require('./node_modules/@git.zone/tsrust/package.json'); if (value.name !== '@git.zone/tsrust' || value.version !== ${JSON.stringify(commitinfo.version)}) process.exit(1);`,
|
|
1220
|
+
]
|
|
1221
|
+
.map(shellQuote)
|
|
1222
|
+
.join(' '),
|
|
1223
|
+
['run_matrix_worker', 'pnpm', ...this.workerTsrustArgs(remoteArg.worker)]
|
|
1224
|
+
.map(shellQuote)
|
|
1225
|
+
.join(' '),
|
|
1226
|
+
'test "$(git rev-parse HEAD)" = "$expected_commit"',
|
|
1227
|
+
'test -z "$(git status --porcelain=v1 --untracked-files=all)"',
|
|
1228
|
+
`test -z "$(git ls-files -v | grep -v '^H ' || true)"`,
|
|
1229
|
+
`actual_names="$(find ${shellQuote(remoteDist)} -mindepth 1 -maxdepth 1 -exec basename {} \\; | LC_ALL=C sort)"`,
|
|
1230
|
+
`test "$actual_names" = ${shellQuote(expectedNames.join('\n'))}`,
|
|
1231
|
+
...expectedNames.map(
|
|
1232
|
+
(name) =>
|
|
1233
|
+
`test -f ${shellQuote(`${remoteDist}/${name}`)}; test ! -L ${shellQuote(`${remoteDist}/${name}`)}`,
|
|
1234
|
+
),
|
|
1235
|
+
'artifact_total=0',
|
|
1236
|
+
...expectedNames.map((name) => {
|
|
1237
|
+
const artifactPath = `${remoteDist}/${name}`;
|
|
1238
|
+
const maximumBytes = name.endsWith(PROVENANCE_SIDECAR_SUFFIX)
|
|
1239
|
+
? MAX_PROVENANCE_SIDECAR_BYTES
|
|
1240
|
+
: MAX_ARTIFACT_BYTES;
|
|
1241
|
+
return `if [ "$(uname -s)" = Darwin ]; then artifact_size="$(stat -f %z ${shellQuote(artifactPath)})"; else artifact_size="$(stat -c %s ${shellQuote(artifactPath)})"; fi; test "$artifact_size" -le ${maximumBytes}; artifact_total=$((artifact_total + artifact_size)); test "$artifact_total" -le ${MAX_ASSEMBLY_INPUT_BYTES}`;
|
|
1242
|
+
}),
|
|
1243
|
+
...remoteArg.worker.targets.flatMap((target) =>
|
|
1244
|
+
binariesArg.map(
|
|
1245
|
+
(binary) => `test -x ${shellQuote(`${remoteDist}/${binary}_${target.friendly}`)}`,
|
|
1246
|
+
),
|
|
1247
|
+
),
|
|
1248
|
+
...this.remoteSmokeCommands(remoteArg.worker, remoteDist, binariesArg),
|
|
1249
|
+
'test "$(git rev-parse HEAD)" = "$expected_commit"',
|
|
1250
|
+
'test -z "$(git status --porcelain=v1 --untracked-files=all)"',
|
|
1251
|
+
`test -z "$(git ls-files -v | grep -v '^H ' || true)"`,
|
|
1252
|
+
];
|
|
1253
|
+
await this.runRemoteOwnedScript(
|
|
1254
|
+
remoteArg.worker,
|
|
1255
|
+
remoteArg.runtime,
|
|
1256
|
+
buildCommands.join('\n'),
|
|
1257
|
+
this.remainingBuildTimeout(buildDeadline, remoteArg.worker.host),
|
|
1258
|
+
);
|
|
1259
|
+
|
|
1260
|
+
const localArtifactDirectory = path.join(runDirectoryArg, remoteArg.worker.host, 'artifacts');
|
|
1261
|
+
await fs.promises.mkdir(localArtifactDirectory, { recursive: true, mode: 0o700 });
|
|
1262
|
+
let downloadedBytes = 0;
|
|
1263
|
+
for (const name of expectedNames) {
|
|
1264
|
+
const remoteFile = `${remoteDist}/${name}`;
|
|
1265
|
+
const localFile = path.join(localArtifactDirectory, name);
|
|
1266
|
+
const maximumBytes = name.endsWith(PROVENANCE_SIDECAR_SUFFIX)
|
|
1267
|
+
? MAX_PROVENANCE_SIDECAR_BYTES
|
|
1268
|
+
: MAX_ARTIFACT_BYTES;
|
|
1269
|
+
const blockSize = 1024 * 1024;
|
|
1270
|
+
const blockCount = Math.ceil(maximumBytes / blockSize) + 1;
|
|
1271
|
+
try {
|
|
1272
|
+
await this.runRemoteTransferScript(
|
|
1273
|
+
remoteArg,
|
|
1274
|
+
[
|
|
1275
|
+
`file=${shellQuote(remoteFile)}`,
|
|
1276
|
+
`test "$file" = ${shellQuote(remoteFile)}`,
|
|
1277
|
+
'test -f "$file"',
|
|
1278
|
+
'test ! -L "$file"',
|
|
1279
|
+
'if [ "$(uname -s)" = Darwin ]; then file_size="$(stat -f %z "$file")"; else file_size="$(stat -c %s "$file")"; fi',
|
|
1280
|
+
`test "$file_size" -le ${maximumBytes}`,
|
|
1281
|
+
`dd if="$file" bs=${blockSize} count=${blockCount} 2>/dev/null`,
|
|
1282
|
+
].join('\n'),
|
|
1283
|
+
{
|
|
1284
|
+
outputFile: localFile,
|
|
1285
|
+
maximumOutputBytes: maximumBytes,
|
|
1286
|
+
timeoutMs: this.remainingBuildTimeout(buildDeadline, remoteArg.worker.host),
|
|
1287
|
+
},
|
|
1288
|
+
);
|
|
1289
|
+
const downloaded = await fs.promises.lstat(localFile);
|
|
1290
|
+
if (!downloaded.isFile() || downloaded.isSymbolicLink() || downloaded.size > maximumBytes) {
|
|
1291
|
+
throw new Error(`Remote matrix artifact exceeds its size limit: ${name}`);
|
|
1292
|
+
}
|
|
1293
|
+
downloadedBytes += downloaded.size;
|
|
1294
|
+
if (downloadedBytes > MAX_ASSEMBLY_INPUT_BYTES) {
|
|
1295
|
+
throw new Error('Remote matrix artifacts exceed the aggregate size limit');
|
|
1296
|
+
}
|
|
1297
|
+
await fs.promises.chmod(
|
|
1298
|
+
localFile,
|
|
1299
|
+
name.endsWith(PROVENANCE_SIDECAR_SUFFIX) ? 0o644 : 0o755,
|
|
1300
|
+
);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
await fs.promises.rm(localFile, { force: true });
|
|
1303
|
+
throw error;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
await this.assertArtifactDirectory(
|
|
1307
|
+
localArtifactDirectory,
|
|
1308
|
+
remoteArg.worker,
|
|
1309
|
+
binariesArg,
|
|
1310
|
+
gitCommitArg,
|
|
1311
|
+
identityArg,
|
|
1312
|
+
);
|
|
1313
|
+
this.remainingBuildTimeout(buildDeadline, remoteArg.worker.host);
|
|
1314
|
+
return localArtifactDirectory;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
private remoteSmokeCommands(
|
|
1318
|
+
workerArg: IResolvedMatrixWorker,
|
|
1319
|
+
directoryArg: string,
|
|
1320
|
+
binariesArg: string[],
|
|
1321
|
+
): string[] {
|
|
1322
|
+
if (!this.plan.smokeTestArgs) return [];
|
|
1323
|
+
const commands: string[] = [];
|
|
1324
|
+
for (const target of workerArg.targets) {
|
|
1325
|
+
for (const binary of binariesArg) {
|
|
1326
|
+
const binaryPath = `${directoryArg}/${binary}_${target.friendly}`;
|
|
1327
|
+
if (target.friendly.startsWith('macos_')) {
|
|
1328
|
+
const architecture = target.friendly === 'macos_arm64' ? '-arm64' : '-x86_64';
|
|
1329
|
+
commands.push(
|
|
1330
|
+
[
|
|
1331
|
+
'run_matrix_worker',
|
|
1332
|
+
'/usr/bin/arch',
|
|
1333
|
+
architecture,
|
|
1334
|
+
binaryPath,
|
|
1335
|
+
...this.plan.smokeTestArgs,
|
|
1336
|
+
]
|
|
1337
|
+
.map(shellQuote)
|
|
1338
|
+
.join(' '),
|
|
1339
|
+
);
|
|
1340
|
+
} else {
|
|
1341
|
+
commands.push(
|
|
1342
|
+
['run_matrix_worker', binaryPath, ...this.plan.smokeTestArgs]
|
|
1343
|
+
.map(shellQuote)
|
|
1344
|
+
.join(' '),
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return commands;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
private async cleanupRemoteWorkspace(
|
|
1353
|
+
remoteArg: IRemoteWorkspace,
|
|
1354
|
+
gitCommitArg: string,
|
|
1355
|
+
): Promise<void> {
|
|
1356
|
+
const script = [
|
|
1357
|
+
'set -eu',
|
|
1358
|
+
...this.remoteRootValidationCommands(remoteArg.runtime.temporaryRoot),
|
|
1359
|
+
...this.remoteOwnershipValidationCommands(remoteArg, gitCommitArg),
|
|
1360
|
+
'rm -rf "$directory"',
|
|
1361
|
+
].join('\n');
|
|
1362
|
+
await this.runRemoteLoginScript(
|
|
1363
|
+
remoteArg.worker,
|
|
1364
|
+
remoteArg.runtime,
|
|
1365
|
+
script,
|
|
1366
|
+
capabilityCommandTimeoutMs,
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private async runRemoteTransferScript(
|
|
1371
|
+
remoteArg: IRemoteWorkspace,
|
|
1372
|
+
scriptArg: string,
|
|
1373
|
+
optionsArg: Pick<
|
|
1374
|
+
IMatrixCommandOptions,
|
|
1375
|
+
'inputFile' | 'outputFile' | 'maximumOutputBytes' | 'timeoutMs'
|
|
1376
|
+
>,
|
|
1377
|
+
): Promise<void> {
|
|
1378
|
+
const details = expectedHostDetails(remoteArg.worker.host);
|
|
1379
|
+
const guardedScript = [
|
|
1380
|
+
'set -eu',
|
|
1381
|
+
`test "$(uname -s)" = ${shellQuote(details.system)}`,
|
|
1382
|
+
`test "$(uname -m)" = ${shellQuote(details.machine)}`,
|
|
1383
|
+
...this.remoteRootValidationCommands(remoteArg.runtime.temporaryRoot),
|
|
1384
|
+
...this.remoteOwnershipValidationCommands(remoteArg, remoteArg.gitCommit),
|
|
1385
|
+
scriptArg,
|
|
1386
|
+
].join('\n');
|
|
1387
|
+
await this.commandRunner.execute(
|
|
1388
|
+
'ssh',
|
|
1389
|
+
[
|
|
1390
|
+
'-T',
|
|
1391
|
+
...sshConnectionArgs,
|
|
1392
|
+
'--',
|
|
1393
|
+
remoteArg.runtime.destination,
|
|
1394
|
+
`/bin/sh -c ${shellQuote(guardedScript)}`,
|
|
1395
|
+
],
|
|
1396
|
+
{
|
|
1397
|
+
cwd: this.workspace,
|
|
1398
|
+
timeoutMs: optionsArg.timeoutMs,
|
|
1399
|
+
inputFile: optionsArg.inputFile,
|
|
1400
|
+
outputFile: optionsArg.outputFile,
|
|
1401
|
+
maximumOutputBytes: optionsArg.maximumOutputBytes,
|
|
1402
|
+
environment: this.transportEnvironment(),
|
|
1403
|
+
},
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
private async runRemoteLoginScript(
|
|
1408
|
+
workerArg: IResolvedMatrixWorker,
|
|
1409
|
+
runtimeArg: IResolvedSshWorker,
|
|
1410
|
+
scriptArg: string,
|
|
1411
|
+
timeoutMsArg: number,
|
|
1412
|
+
): Promise<void> {
|
|
1413
|
+
const shell = workerArg.host.startsWith('macos_') ? '/bin/zsh' : '/bin/bash';
|
|
1414
|
+
const shellFlags = workerArg.host.startsWith('macos_') ? '-lic' : '-lc';
|
|
1415
|
+
const details = expectedHostDetails(workerArg.host);
|
|
1416
|
+
const guardedScript = [
|
|
1417
|
+
'set -eu',
|
|
1418
|
+
`test "$(uname -s)" = ${shellQuote(details.system)}`,
|
|
1419
|
+
`test "$(uname -m)" = ${shellQuote(details.machine)}`,
|
|
1420
|
+
scriptArg,
|
|
1421
|
+
].join('\n');
|
|
1422
|
+
await this.commandRunner.execute(
|
|
1423
|
+
'ssh',
|
|
1424
|
+
[
|
|
1425
|
+
...sshConnectionArgs,
|
|
1426
|
+
'--',
|
|
1427
|
+
runtimeArg.destination,
|
|
1428
|
+
`${shell} ${shellFlags} ${shellQuote(guardedScript)}`,
|
|
1429
|
+
],
|
|
1430
|
+
{
|
|
1431
|
+
cwd: this.workspace,
|
|
1432
|
+
timeoutMs: timeoutMsArg,
|
|
1433
|
+
environment: this.transportEnvironment(),
|
|
1434
|
+
},
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
private async runRemoteOwnedScript(
|
|
1439
|
+
workerArg: IResolvedMatrixWorker,
|
|
1440
|
+
runtimeArg: IResolvedSshWorker,
|
|
1441
|
+
scriptArg: string,
|
|
1442
|
+
timeoutMsArg: number,
|
|
1443
|
+
): Promise<void> {
|
|
1444
|
+
const shell = workerArg.host.startsWith('macos_') ? '/bin/zsh' : '/bin/bash';
|
|
1445
|
+
const outerFlags = workerArg.host.startsWith('macos_') ? '-lic' : '-lc';
|
|
1446
|
+
const details = expectedHostDetails(workerArg.host);
|
|
1447
|
+
const guardedScript = [
|
|
1448
|
+
'set -eu',
|
|
1449
|
+
`test "$(uname -s)" = ${shellQuote(details.system)}`,
|
|
1450
|
+
`test "$(uname -m)" = ${shellQuote(details.machine)}`,
|
|
1451
|
+
scriptArg,
|
|
1452
|
+
].join('\n');
|
|
1453
|
+
const watchdogCommand = ['node', '-e', remoteWatchdogSource, shell, guardedScript]
|
|
1454
|
+
.map(shellQuote)
|
|
1455
|
+
.join(' ');
|
|
1456
|
+
await this.commandRunner.execute(
|
|
1457
|
+
'ssh',
|
|
1458
|
+
[
|
|
1459
|
+
...sshConnectionArgs,
|
|
1460
|
+
'--',
|
|
1461
|
+
runtimeArg.destination,
|
|
1462
|
+
`${shell} ${outerFlags} ${shellQuote(watchdogCommand)}`,
|
|
1463
|
+
],
|
|
1464
|
+
{
|
|
1465
|
+
cwd: this.workspace,
|
|
1466
|
+
timeoutMs: timeoutMsArg,
|
|
1467
|
+
heartbeatStdin: true,
|
|
1468
|
+
environment: this.transportEnvironment(),
|
|
1469
|
+
},
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
}
|