@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,222 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configuredAssemblyTargets,
|
|
3
|
+
resolveBuildTargets,
|
|
4
|
+
targetAliasMap,
|
|
5
|
+
type INormalizedTarget,
|
|
6
|
+
type ITsrustConfig,
|
|
7
|
+
} from '../mod_cli/helpers.targets.js';
|
|
8
|
+
|
|
9
|
+
export const matrixHostKeys = [
|
|
10
|
+
'linux_amd64',
|
|
11
|
+
'linux_arm64',
|
|
12
|
+
'macos_amd64',
|
|
13
|
+
'macos_arm64',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type TMatrixHost = (typeof matrixHostKeys)[number];
|
|
17
|
+
|
|
18
|
+
export interface ITsrustLocalMatrixBuilderConfig {
|
|
19
|
+
transport: 'local';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ITsrustSshMatrixBuilderConfig {
|
|
23
|
+
transport: 'ssh';
|
|
24
|
+
destinationEnv: string;
|
|
25
|
+
temporaryRootEnv: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type TTsrustMatrixBuilderConfig =
|
|
29
|
+
| ITsrustLocalMatrixBuilderConfig
|
|
30
|
+
| ITsrustSshMatrixBuilderConfig;
|
|
31
|
+
|
|
32
|
+
export interface ITsrustMatrixConfig {
|
|
33
|
+
builders: Partial<Record<TMatrixHost, TTsrustMatrixBuilderConfig>>;
|
|
34
|
+
smokeTestArgs?: string[];
|
|
35
|
+
forwardEnvironment?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface IResolvedMatrixWorker {
|
|
39
|
+
host: TMatrixHost;
|
|
40
|
+
hostTriple: string;
|
|
41
|
+
config: TTsrustMatrixBuilderConfig;
|
|
42
|
+
targets: INormalizedTarget[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface IResolvedMatrixPlan {
|
|
46
|
+
workers: IResolvedMatrixWorker[];
|
|
47
|
+
expectedTargets: INormalizedTarget[];
|
|
48
|
+
smokeTestArgs?: string[];
|
|
49
|
+
forwardEnvironment: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const environmentNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
53
|
+
const matrixHostKeySet = new Set<string>(matrixHostKeys);
|
|
54
|
+
|
|
55
|
+
const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> =>
|
|
56
|
+
!!valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg);
|
|
57
|
+
|
|
58
|
+
const assertClosedKeys = (
|
|
59
|
+
valueArg: Record<string, unknown>,
|
|
60
|
+
allowedKeysArg: string[],
|
|
61
|
+
labelArg: string,
|
|
62
|
+
): void => {
|
|
63
|
+
const allowedKeys = new Set(allowedKeysArg);
|
|
64
|
+
const unexpectedKeys = Object.keys(valueArg).filter((key) => !allowedKeys.has(key));
|
|
65
|
+
if (unexpectedKeys.length > 0) {
|
|
66
|
+
throw new Error(`${labelArg} contains unsupported keys: ${unexpectedKeys.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const validateBuilder = (
|
|
71
|
+
hostArg: TMatrixHost,
|
|
72
|
+
valueArg: unknown,
|
|
73
|
+
): TTsrustMatrixBuilderConfig => {
|
|
74
|
+
if (!isRecord(valueArg)) {
|
|
75
|
+
throw new Error(`matrix.builders.${hostArg} must be an object`);
|
|
76
|
+
}
|
|
77
|
+
if (valueArg.transport === 'local') {
|
|
78
|
+
assertClosedKeys(valueArg, ['transport'], `matrix.builders.${hostArg}`);
|
|
79
|
+
return { transport: 'local' };
|
|
80
|
+
}
|
|
81
|
+
if (valueArg.transport === 'ssh') {
|
|
82
|
+
assertClosedKeys(
|
|
83
|
+
valueArg,
|
|
84
|
+
['transport', 'destinationEnv', 'temporaryRootEnv'],
|
|
85
|
+
`matrix.builders.${hostArg}`,
|
|
86
|
+
);
|
|
87
|
+
if (
|
|
88
|
+
typeof valueArg.destinationEnv !== 'string' ||
|
|
89
|
+
!environmentNamePattern.test(valueArg.destinationEnv)
|
|
90
|
+
) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`matrix.builders.${hostArg}.destinationEnv must name an environment variable`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (
|
|
96
|
+
typeof valueArg.temporaryRootEnv !== 'string' ||
|
|
97
|
+
!environmentNamePattern.test(valueArg.temporaryRootEnv)
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`matrix.builders.${hostArg}.temporaryRootEnv must name an environment variable`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
transport: 'ssh',
|
|
105
|
+
destinationEnv: valueArg.destinationEnv,
|
|
106
|
+
temporaryRootEnv: valueArg.temporaryRootEnv,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`matrix.builders.${hostArg}.transport must be local or ssh`);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const validateSmokeTestArgs = (valueArg: unknown): string[] | undefined => {
|
|
113
|
+
if (valueArg === undefined) return undefined;
|
|
114
|
+
if (
|
|
115
|
+
!Array.isArray(valueArg) ||
|
|
116
|
+
valueArg.length === 0 ||
|
|
117
|
+
valueArg.some(
|
|
118
|
+
(argument) =>
|
|
119
|
+
typeof argument !== 'string' || argument.includes('\0') || /[\r\n]/.test(argument),
|
|
120
|
+
)
|
|
121
|
+
) {
|
|
122
|
+
throw new Error('matrix.smokeTestArgs must be a non-empty array of single-line strings');
|
|
123
|
+
}
|
|
124
|
+
return [...valueArg];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const validateForwardEnvironment = (valueArg: unknown): string[] => {
|
|
128
|
+
if (valueArg === undefined) return [];
|
|
129
|
+
if (
|
|
130
|
+
!Array.isArray(valueArg) ||
|
|
131
|
+
valueArg.some(
|
|
132
|
+
(environmentName) =>
|
|
133
|
+
typeof environmentName !== 'string' ||
|
|
134
|
+
!environmentNamePattern.test(environmentName),
|
|
135
|
+
)
|
|
136
|
+
) {
|
|
137
|
+
throw new Error('matrix.forwardEnvironment must contain environment variable names');
|
|
138
|
+
}
|
|
139
|
+
return [...new Set(valueArg)];
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const resolveMatrixPlan = (configArg: ITsrustConfig): IResolvedMatrixPlan => {
|
|
143
|
+
const matrixValue = configArg.matrix as unknown;
|
|
144
|
+
if (!isRecord(matrixValue)) {
|
|
145
|
+
throw new Error('tsrust matrix commands require a matrix configuration');
|
|
146
|
+
}
|
|
147
|
+
assertClosedKeys(
|
|
148
|
+
matrixValue,
|
|
149
|
+
['builders', 'smokeTestArgs', 'forwardEnvironment'],
|
|
150
|
+
'matrix',
|
|
151
|
+
);
|
|
152
|
+
if (!isRecord(matrixValue.builders) || Object.keys(matrixValue.builders).length === 0) {
|
|
153
|
+
throw new Error('matrix.builders must define at least one build worker');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const workers: IResolvedMatrixWorker[] = [];
|
|
157
|
+
let localWorkerCount = 0;
|
|
158
|
+
for (const [host, rawBuilder] of Object.entries(matrixValue.builders)) {
|
|
159
|
+
if (!matrixHostKeySet.has(host)) {
|
|
160
|
+
throw new Error(`Unsupported matrix builder host: ${host}`);
|
|
161
|
+
}
|
|
162
|
+
const matrixHost = host as TMatrixHost;
|
|
163
|
+
const builderConfig = validateBuilder(matrixHost, rawBuilder);
|
|
164
|
+
if (builderConfig.transport === 'local') localWorkerCount += 1;
|
|
165
|
+
const hostTriple = targetAliasMap[matrixHost];
|
|
166
|
+
const targets = resolveBuildTargets(configArg, hostTriple);
|
|
167
|
+
if (targets.length === 0) {
|
|
168
|
+
throw new Error(`matrix builder ${matrixHost} has no configured targets`);
|
|
169
|
+
}
|
|
170
|
+
const expectedFamily = matrixHost.startsWith('linux_') ? 'linux' : 'macos';
|
|
171
|
+
const crossFamilyTarget = targets.find(
|
|
172
|
+
(target) => !target.friendly.startsWith(`${expectedFamily}_`),
|
|
173
|
+
);
|
|
174
|
+
if (crossFamilyTarget) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`matrix builder ${matrixHost} cannot own cross-family target ${crossFamilyTarget.friendly}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
workers.push({ host: matrixHost, hostTriple, config: builderConfig, targets });
|
|
180
|
+
}
|
|
181
|
+
if (localWorkerCount > 1) {
|
|
182
|
+
throw new Error('matrix.builders may define at most one local worker');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const expectedTargets = configuredAssemblyTargets(configArg);
|
|
186
|
+
if (expectedTargets.length === 0) {
|
|
187
|
+
throw new Error('tsrust matrix commands require configured targets');
|
|
188
|
+
}
|
|
189
|
+
const expectedByTriple = new Map(expectedTargets.map((target) => [target.triple, target]));
|
|
190
|
+
const ownerByTriple = new Map<string, TMatrixHost>();
|
|
191
|
+
for (const worker of workers) {
|
|
192
|
+
for (const target of worker.targets) {
|
|
193
|
+
if (!expectedByTriple.has(target.triple)) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`matrix builder ${worker.host} selected unexpected target ${target.friendly}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const existingOwner = ownerByTriple.get(target.triple);
|
|
199
|
+
if (existingOwner) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`matrix target ${target.friendly} is assigned to both ${existingOwner} and ${worker.host}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
ownerByTriple.set(target.triple, worker.host);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const missingTargets = expectedTargets.filter((target) => !ownerByTriple.has(target.triple));
|
|
208
|
+
if (missingTargets.length > 0) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`matrix builders do not cover configured targets: ${missingTargets
|
|
211
|
+
.map((target) => target.friendly)
|
|
212
|
+
.join(', ')}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
workers,
|
|
218
|
+
expectedTargets,
|
|
219
|
+
smokeTestArgs: validateSmokeTestArgs(matrixValue.smokeTestArgs),
|
|
220
|
+
forwardEnvironment: validateForwardEnvironment(matrixValue.forwardEnvironment),
|
|
221
|
+
};
|
|
222
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
MatrixCommandRunner,
|
|
3
|
+
type IMatrixCommandOptions,
|
|
4
|
+
type IMatrixCommandResult,
|
|
5
|
+
type IMatrixCommandRunner,
|
|
6
|
+
} from './classes.matrixcommandrunner.js';
|
|
7
|
+
export {
|
|
8
|
+
NativeMatrixBuilder,
|
|
9
|
+
type INativeMatrixBuilderOptions,
|
|
10
|
+
type INativeMatrixBuildResult,
|
|
11
|
+
type INativeMatrixCheckResult,
|
|
12
|
+
} from './classes.nativematrixbuilder.js';
|
|
13
|
+
export {
|
|
14
|
+
matrixHostKeys,
|
|
15
|
+
resolveMatrixPlan,
|
|
16
|
+
type IResolvedMatrixPlan,
|
|
17
|
+
type IResolvedMatrixWorker,
|
|
18
|
+
type ITsrustLocalMatrixBuilderConfig,
|
|
19
|
+
type ITsrustMatrixConfig,
|
|
20
|
+
type ITsrustSshMatrixBuilderConfig,
|
|
21
|
+
type TMatrixHost,
|
|
22
|
+
type TTsrustMatrixBuilderConfig,
|
|
23
|
+
} from './helpers.matrixconfig.js';
|
|
@@ -7,6 +7,7 @@ export interface IGitSnapshot {
|
|
|
7
7
|
available: boolean;
|
|
8
8
|
commit: string;
|
|
9
9
|
status: string;
|
|
10
|
+
unsafeIndexFlags?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export async function captureGitSnapshot(cwdArg: string): Promise<IGitSnapshot> {
|
|
@@ -17,7 +18,7 @@ export async function captureGitSnapshot(cwdArg: string): Promise<IGitSnapshot>
|
|
|
17
18
|
});
|
|
18
19
|
commit = result.stdout.trim();
|
|
19
20
|
} catch {
|
|
20
|
-
return { available: false, commit: 'unknown', status: '' };
|
|
21
|
+
return { available: false, commit: 'unknown', status: '', unsafeIndexFlags: '' };
|
|
21
22
|
}
|
|
22
23
|
try {
|
|
23
24
|
const status = await execFile(
|
|
@@ -25,10 +26,19 @@ export async function captureGitSnapshot(cwdArg: string): Promise<IGitSnapshot>
|
|
|
25
26
|
['-C', cwdArg, 'status', '--porcelain=v1', '--untracked-files=all'],
|
|
26
27
|
{ encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 },
|
|
27
28
|
);
|
|
29
|
+
const index = await execFile('git', ['-C', cwdArg, 'ls-files', '-v', '-z'], {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
32
|
+
});
|
|
33
|
+
const unsafeIndexFlags = index.stdout
|
|
34
|
+
.split('\0')
|
|
35
|
+
.filter((entry) => entry.length > 0 && !entry.startsWith('H '))
|
|
36
|
+
.join('\0');
|
|
28
37
|
return {
|
|
29
38
|
available: true,
|
|
30
39
|
commit,
|
|
31
40
|
status: status.stdout,
|
|
41
|
+
unsafeIndexFlags,
|
|
32
42
|
};
|
|
33
43
|
} catch {
|
|
34
44
|
throw new Error('Failed to inspect Git worktree state');
|
|
@@ -42,7 +52,8 @@ export function assertGitSnapshotUnchanged(
|
|
|
42
52
|
if (
|
|
43
53
|
beforeArg.available !== afterArg.available ||
|
|
44
54
|
beforeArg.commit !== afterArg.commit ||
|
|
45
|
-
beforeArg.status !== afterArg.status
|
|
55
|
+
beforeArg.status !== afterArg.status ||
|
|
56
|
+
(beforeArg.unsafeIndexFlags || '') !== (afterArg.unsafeIndexFlags || '')
|
|
46
57
|
) {
|
|
47
58
|
throw new Error('Git source state changed while Cargo was building');
|
|
48
59
|
}
|
|
@@ -7,6 +7,7 @@ import { getHostIdentity, isLocalProcessRunning } from './helpers.owneridentity.
|
|
|
7
7
|
|
|
8
8
|
const SIDECAR_FORMAT = 'tsrust.build-provenance.v2';
|
|
9
9
|
export const PROVENANCE_SIDECAR_SUFFIX = '.tsrust-build.json';
|
|
10
|
+
export const MAX_PROVENANCE_SIDECAR_BYTES = 1024 * 1024;
|
|
10
11
|
|
|
11
12
|
interface IProvenanceSidecar {
|
|
12
13
|
format: typeof SIDECAR_FORMAT;
|
|
@@ -154,6 +155,9 @@ export class ProvenanceStore {
|
|
|
154
155
|
if (!sidecar.isFile() || sidecar.isSymbolicLink()) {
|
|
155
156
|
throw new Error(`Provenance sidecar is not a regular file: ${sidecarPath}`);
|
|
156
157
|
}
|
|
158
|
+
if (sidecar.size > MAX_PROVENANCE_SIDECAR_BYTES) {
|
|
159
|
+
throw new Error(`Provenance sidecar exceeds the size limit: ${sidecarPath}`);
|
|
160
|
+
}
|
|
157
161
|
let parsed: unknown;
|
|
158
162
|
try {
|
|
159
163
|
parsed = JSON.parse(await fs.promises.readFile(sidecarPath, 'utf8'));
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
1
2
|
import * as fs from 'fs';
|
|
2
3
|
import * as os from 'os';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
import * as plugins from '../plugins.js';
|
|
6
|
+
import { getHostIdentity, isLocalProcessRunning } from '../mod_provenance/index.js';
|
|
7
|
+
|
|
8
|
+
interface IToolchainInstallLock {
|
|
9
|
+
format: 'tsrust.toolchain-install-lock.v1';
|
|
10
|
+
pid: number;
|
|
11
|
+
hostIdentity: string;
|
|
12
|
+
token: string;
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
export class ToolchainManager {
|
|
7
16
|
public static TOOLCHAIN_DIR = '/tmp/tsrust_toolchain';
|
|
@@ -17,6 +26,173 @@ export class ToolchainManager {
|
|
|
17
26
|
});
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
private async ensureSecureToolchainDirectory(): Promise<void> {
|
|
30
|
+
await fs.promises.mkdir(ToolchainManager.TOOLCHAIN_DIR, {
|
|
31
|
+
recursive: true,
|
|
32
|
+
mode: 0o700,
|
|
33
|
+
});
|
|
34
|
+
const directory = await fs.promises.lstat(ToolchainManager.TOOLCHAIN_DIR);
|
|
35
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
36
|
+
throw new Error('Bundled Rust toolchain root is not a regular directory');
|
|
37
|
+
}
|
|
38
|
+
if (typeof process.getuid === 'function' && directory.uid !== process.getuid()) {
|
|
39
|
+
throw new Error('Bundled Rust toolchain root is owned by another user');
|
|
40
|
+
}
|
|
41
|
+
await fs.promises.chmod(ToolchainManager.TOOLCHAIN_DIR, 0o700);
|
|
42
|
+
const physicalParent = await fs.promises.realpath(
|
|
43
|
+
path.dirname(ToolchainManager.TOOLCHAIN_DIR),
|
|
44
|
+
);
|
|
45
|
+
const expectedPhysicalPath = path.join(
|
|
46
|
+
physicalParent,
|
|
47
|
+
path.basename(ToolchainManager.TOOLCHAIN_DIR),
|
|
48
|
+
);
|
|
49
|
+
if ((await fs.promises.realpath(ToolchainManager.TOOLCHAIN_DIR)) !== expectedPhysicalPath) {
|
|
50
|
+
throw new Error('Bundled Rust toolchain root traverses a symbolic link');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async isSecureToolchainExecutable(filePathArg: string): Promise<boolean> {
|
|
55
|
+
let entry: fs.Stats;
|
|
56
|
+
try {
|
|
57
|
+
entry = await fs.promises.lstat(filePathArg);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) {
|
|
63
|
+
throw new Error(`Bundled Rust toolchain entry is not a file: ${filePathArg}`);
|
|
64
|
+
}
|
|
65
|
+
const resolvedPath = await fs.promises.realpath(filePathArg);
|
|
66
|
+
const relativePath = path.relative(
|
|
67
|
+
await fs.promises.realpath(ToolchainManager.TOOLCHAIN_DIR),
|
|
68
|
+
resolvedPath,
|
|
69
|
+
);
|
|
70
|
+
if (
|
|
71
|
+
relativePath === '' ||
|
|
72
|
+
relativePath.startsWith('..') ||
|
|
73
|
+
path.isAbsolute(relativePath)
|
|
74
|
+
) {
|
|
75
|
+
throw new Error(`Bundled Rust toolchain entry escapes its root: ${filePathArg}`);
|
|
76
|
+
}
|
|
77
|
+
const resolved = await fs.promises.stat(resolvedPath);
|
|
78
|
+
if (!resolved.isFile() || (resolved.mode & 0o111) === 0 || (resolved.mode & 0o022) !== 0) {
|
|
79
|
+
throw new Error(`Bundled Rust toolchain executable is unsafe: ${filePathArg}`);
|
|
80
|
+
}
|
|
81
|
+
if (typeof process.getuid === 'function' && resolved.uid !== process.getuid()) {
|
|
82
|
+
throw new Error(`Bundled Rust toolchain executable is owned by another user: ${filePathArg}`);
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async acquireInstallLock(): Promise<IToolchainInstallLock> {
|
|
88
|
+
const lockPath = `${ToolchainManager.TOOLCHAIN_DIR}.install.lock`;
|
|
89
|
+
const lockDirectory = path.dirname(lockPath);
|
|
90
|
+
const temporaryPrefix = `${path.basename(lockPath)}.`;
|
|
91
|
+
const hostIdentity = getHostIdentity();
|
|
92
|
+
for (const entry of await fs.promises.readdir(lockDirectory, { withFileTypes: true })) {
|
|
93
|
+
if (!entry.name.startsWith(temporaryPrefix) || !entry.name.endsWith('.tmp')) continue;
|
|
94
|
+
const suffix = entry.name.slice(temporaryPrefix.length, -'.tmp'.length);
|
|
95
|
+
const [entryHostIdentity, pidText, token, ...unexpected] = suffix.split('.');
|
|
96
|
+
const pid = Number(pidText);
|
|
97
|
+
if (
|
|
98
|
+
unexpected.length > 0 ||
|
|
99
|
+
entryHostIdentity !== hostIdentity ||
|
|
100
|
+
!Number.isSafeInteger(pid) ||
|
|
101
|
+
pid <= 0 ||
|
|
102
|
+
!/^[a-f0-9-]{36}$/.test(token || '') ||
|
|
103
|
+
isLocalProcessRunning(pid)
|
|
104
|
+
) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const temporaryPath = path.join(lockDirectory, entry.name);
|
|
108
|
+
const temporary = await fs.promises.lstat(temporaryPath);
|
|
109
|
+
if (
|
|
110
|
+
temporary.isFile() &&
|
|
111
|
+
!temporary.isSymbolicLink() &&
|
|
112
|
+
(typeof process.getuid !== 'function' || temporary.uid === process.getuid())
|
|
113
|
+
) {
|
|
114
|
+
await fs.promises.rm(temporaryPath, { force: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const deadline = Date.now() + 5 * 60 * 1000;
|
|
118
|
+
while (Date.now() < deadline) {
|
|
119
|
+
const owner: IToolchainInstallLock = {
|
|
120
|
+
format: 'tsrust.toolchain-install-lock.v1',
|
|
121
|
+
pid: process.pid,
|
|
122
|
+
hostIdentity,
|
|
123
|
+
token: crypto.randomUUID(),
|
|
124
|
+
};
|
|
125
|
+
const temporaryPath = `${lockPath}.${owner.hostIdentity}.${owner.pid}.${owner.token}.tmp`;
|
|
126
|
+
try {
|
|
127
|
+
const handle = await fs.promises.open(temporaryPath, 'wx', 0o600);
|
|
128
|
+
try {
|
|
129
|
+
await handle.writeFile(`${JSON.stringify(owner)}\n`, 'utf8');
|
|
130
|
+
await handle.sync();
|
|
131
|
+
} finally {
|
|
132
|
+
await handle.close();
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await fs.promises.link(temporaryPath, lockPath);
|
|
136
|
+
return owner;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
139
|
+
}
|
|
140
|
+
} finally {
|
|
141
|
+
await fs.promises.rm(temporaryPath, { force: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let lock: fs.Stats;
|
|
145
|
+
let contents: string;
|
|
146
|
+
try {
|
|
147
|
+
lock = await fs.promises.lstat(lockPath);
|
|
148
|
+
contents = await fs.promises.readFile(lockPath, 'utf8');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
!lock.isFile() ||
|
|
155
|
+
lock.isSymbolicLink() ||
|
|
156
|
+
(typeof process.getuid === 'function' && lock.uid !== process.getuid()) ||
|
|
157
|
+
(lock.mode & 0o077) !== 0 ||
|
|
158
|
+
lock.size > 4096
|
|
159
|
+
) {
|
|
160
|
+
throw new Error(`Bundled Rust toolchain install lock is unsafe: ${lockPath}`);
|
|
161
|
+
}
|
|
162
|
+
let existing: IToolchainInstallLock;
|
|
163
|
+
try {
|
|
164
|
+
existing = JSON.parse(contents) as IToolchainInstallLock;
|
|
165
|
+
} catch {
|
|
166
|
+
throw new Error(`Bundled Rust toolchain install lock is invalid: ${lockPath}`);
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
existing.format !== 'tsrust.toolchain-install-lock.v1' ||
|
|
170
|
+
!Number.isSafeInteger(existing.pid) ||
|
|
171
|
+
existing.pid <= 0 ||
|
|
172
|
+
!/^[a-f0-9]{64}$/.test(existing.hostIdentity) ||
|
|
173
|
+
!/^[a-f0-9-]{36}$/.test(existing.token)
|
|
174
|
+
) {
|
|
175
|
+
throw new Error(`Bundled Rust toolchain install lock is invalid: ${lockPath}`);
|
|
176
|
+
}
|
|
177
|
+
if (existing.hostIdentity === hostIdentity && !isLocalProcessRunning(existing.pid)) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Bundled Rust toolchain install lock is stale; inspect and remove ${lockPath} before retrying`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
183
|
+
}
|
|
184
|
+
throw new Error('Timed out waiting for the bundled Rust toolchain install lock');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async releaseInstallLock(ownerArg: IToolchainInstallLock): Promise<void> {
|
|
188
|
+
const lockPath = `${ToolchainManager.TOOLCHAIN_DIR}.install.lock`;
|
|
189
|
+
const contents = JSON.parse(await fs.promises.readFile(lockPath, 'utf8')) as IToolchainInstallLock;
|
|
190
|
+
if (contents.token !== ownerArg.token) {
|
|
191
|
+
throw new Error('Bundled Rust toolchain install lock ownership changed');
|
|
192
|
+
}
|
|
193
|
+
await fs.promises.unlink(lockPath);
|
|
194
|
+
}
|
|
195
|
+
|
|
20
196
|
/**
|
|
21
197
|
* Returns the Rust host triple for the current platform.
|
|
22
198
|
*/
|
|
@@ -37,73 +213,90 @@ export class ToolchainManager {
|
|
|
37
213
|
/**
|
|
38
214
|
* Checks if the bundled toolchain is already installed at TOOLCHAIN_DIR.
|
|
39
215
|
*/
|
|
216
|
+
private async isInstalledUnlocked(): Promise<boolean> {
|
|
217
|
+
await this.ensureSecureToolchainDirectory();
|
|
218
|
+
return (
|
|
219
|
+
(await this.isSecureToolchainExecutable(path.join(ToolchainManager.BIN_DIR, 'cargo'))) &&
|
|
220
|
+
(await this.isSecureToolchainExecutable(path.join(ToolchainManager.BIN_DIR, 'rustup')))
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
40
224
|
public async isInstalled(): Promise<boolean> {
|
|
41
|
-
const
|
|
225
|
+
const lockOwner = await this.acquireInstallLock();
|
|
42
226
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return false;
|
|
227
|
+
return await this.isInstalledUnlocked();
|
|
228
|
+
} finally {
|
|
229
|
+
await this.releaseInstallLock(lockOwner);
|
|
47
230
|
}
|
|
48
231
|
}
|
|
49
232
|
|
|
50
233
|
/**
|
|
51
234
|
* Downloads rustup-init and installs a minimal Rust toolchain to TOOLCHAIN_DIR.
|
|
52
235
|
*/
|
|
53
|
-
|
|
236
|
+
private async installUnlocked(): Promise<void> {
|
|
237
|
+
await this.ensureSecureToolchainDirectory();
|
|
54
238
|
const triple = this.getHostTriple();
|
|
55
239
|
const rustupInitUrl = `https://static.rust-lang.org/rustup/dist/${triple}/rustup-init`;
|
|
56
240
|
const rustupInitPath = path.join(ToolchainManager.TOOLCHAIN_DIR, 'rustup-init');
|
|
57
241
|
|
|
58
|
-
|
|
59
|
-
await fs.promises.mkdir(ToolchainManager.TOOLCHAIN_DIR, { recursive: true });
|
|
242
|
+
await fs.promises.rm(rustupInitPath, { force: true });
|
|
60
243
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
244
|
+
try {
|
|
245
|
+
console.log(`Downloading rustup-init for ${triple}...`);
|
|
246
|
+
const downloadResult = await this.shell.exec(
|
|
247
|
+
`curl -sSf -o ${rustupInitPath} ${rustupInitUrl} && chmod +x ${rustupInitPath}`
|
|
248
|
+
);
|
|
249
|
+
if (downloadResult.exitCode !== 0) {
|
|
250
|
+
throw new Error(`Failed to download rustup-init: ${downloadResult.stdout}`);
|
|
251
|
+
}
|
|
68
252
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const installResult = await this.shell.exec(installCmd);
|
|
81
|
-
if (installResult.exitCode !== 0) {
|
|
82
|
-
throw new Error(`Failed to install Rust toolchain: ${installResult.stdout}`);
|
|
83
|
-
}
|
|
253
|
+
console.log('Installing minimal Rust toolchain to /tmp/tsrust_toolchain/...');
|
|
254
|
+
const installCmd = [
|
|
255
|
+
`RUSTUP_HOME="${ToolchainManager.RUSTUP_HOME}"`,
|
|
256
|
+
`CARGO_HOME="${ToolchainManager.CARGO_HOME}"`,
|
|
257
|
+
rustupInitPath,
|
|
258
|
+
'-y',
|
|
259
|
+
'--default-toolchain stable',
|
|
260
|
+
'--profile minimal',
|
|
261
|
+
'--no-modify-path',
|
|
262
|
+
].join(' ');
|
|
84
263
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
264
|
+
const installResult = await this.shell.exec(installCmd);
|
|
265
|
+
if (installResult.exitCode !== 0) {
|
|
266
|
+
throw new Error(`Failed to install Rust toolchain: ${installResult.stdout}`);
|
|
267
|
+
}
|
|
268
|
+
if (!(await this.isInstalledUnlocked())) {
|
|
269
|
+
throw new Error('Installed bundled Rust toolchain failed ownership validation.');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const verifyResult = await this.shell.execSilent(
|
|
273
|
+
`${this.getEnvPrefix()}cargo --version`
|
|
274
|
+
);
|
|
275
|
+
if (verifyResult.exitCode !== 0) {
|
|
276
|
+
throw new Error('Rust toolchain installation verification failed.');
|
|
277
|
+
}
|
|
278
|
+
console.log(`Installed: ${verifyResult.stdout.trim()}`);
|
|
279
|
+
} finally {
|
|
280
|
+
await fs.promises.rm(rustupInitPath, { force: true });
|
|
91
281
|
}
|
|
92
|
-
|
|
282
|
+
}
|
|
93
283
|
|
|
94
|
-
|
|
95
|
-
await
|
|
284
|
+
public async install(): Promise<void> {
|
|
285
|
+
const lockOwner = await this.acquireInstallLock();
|
|
286
|
+
try {
|
|
287
|
+
if (await this.isInstalledUnlocked()) return;
|
|
288
|
+
await this.installUnlocked();
|
|
289
|
+
} finally {
|
|
290
|
+
await this.releaseInstallLock(lockOwner);
|
|
291
|
+
}
|
|
96
292
|
}
|
|
97
293
|
|
|
98
294
|
/**
|
|
99
295
|
* Ensures the bundled toolchain is installed. Downloads if not present.
|
|
100
296
|
*/
|
|
101
297
|
public async ensureInstalled(): Promise<void> {
|
|
102
|
-
if (await this.isInstalled()) {
|
|
103
|
-
console.log('Using bundled Rust toolchain from /tmp/tsrust_toolchain/');
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
298
|
await this.install();
|
|
299
|
+
console.log('Using bundled Rust toolchain from /tmp/tsrust_toolchain/');
|
|
107
300
|
}
|
|
108
301
|
|
|
109
302
|
/**
|