@git.zone/tsrust 1.13.0 → 1.13.2
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 +1 -1
- package/dist_ts/mod_cargo/classes.cargorunner.d.ts +1 -0
- package/dist_ts/mod_cargo/classes.cargorunner.js +47 -39
- package/dist_ts/mod_cargo/classes.targetcache.d.ts +10 -0
- package/dist_ts/mod_cargo/classes.targetcache.js +94 -75
- package/dist_ts/mod_cargo/classes.targetguard.d.ts +4 -0
- package/dist_ts/mod_cargo/classes.targetguard.js +83 -0
- package/dist_ts/mod_cli/classes.tsrustcli.js +119 -114
- package/dist_ts/plugins.d.ts +6 -1
- package/dist_ts/plugins.js +7 -2
- package/package.json +4 -4
- package/readme.md +10 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mod_cargo/classes.cargorunner.ts +52 -43
- package/ts/mod_cargo/classes.targetcache.ts +78 -79
- package/ts/mod_cargo/classes.targetguard.ts +63 -0
- package/ts/mod_cli/classes.tsrustcli.ts +141 -136
- package/ts/plugins.ts +6 -0
- package/readme.hints.md +0 -27
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { withTargetCacheLock, assertTargetCacheIdle } from './classes.targetguard.js';
|
|
3
|
+
const fs = plugins.fsPromises;
|
|
4
|
+
const path = plugins.path;
|
|
3
5
|
|
|
4
6
|
export const toolCacheMarkerFile = '.gitzone-tool-cache.json';
|
|
5
7
|
export const tsrustCacheOwner = '@git.zone/tsrust';
|
|
@@ -21,6 +23,9 @@ export interface ITsrustPruneCandidate {
|
|
|
21
23
|
shouldPrune: boolean;
|
|
22
24
|
reason: string;
|
|
23
25
|
truncated?: boolean;
|
|
26
|
+
/** Captured eligibility and identity, revalidated by apply. */
|
|
27
|
+
criteria?: { days?: number; maxSizeBytes?: number };
|
|
28
|
+
identity?: { dev: number; ino: number; markerCreatedAt: string };
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export interface ITsrustPruneOptions {
|
|
@@ -42,7 +47,9 @@ export function createTargetCacheMarker(): IToolCacheMarker {
|
|
|
42
47
|
|
|
43
48
|
export function isValidTargetCacheMarker(markerArg: unknown): markerArg is IToolCacheMarker {
|
|
44
49
|
const marker = markerArg as Partial<IToolCacheMarker>;
|
|
45
|
-
return marker
|
|
50
|
+
return !!marker && typeof marker === 'object'
|
|
51
|
+
&& typeof marker.createdAt === 'string' && Number.isFinite(Date.parse(marker.createdAt))
|
|
52
|
+
&& marker.owner === tsrustCacheOwner
|
|
46
53
|
&& marker.kind === targetCacheKind
|
|
47
54
|
&& marker.safeToPrune === true
|
|
48
55
|
&& marker.schemaVersion === 1;
|
|
@@ -119,7 +126,10 @@ export async function writeTargetCacheMarker(targetDirArg: string): Promise<void
|
|
|
119
126
|
|
|
120
127
|
export async function readTargetCacheMarker(targetDirArg: string): Promise<IToolCacheMarker | undefined> {
|
|
121
128
|
try {
|
|
122
|
-
const
|
|
129
|
+
const markerPath = path.join(targetDirArg, toolCacheMarkerFile);
|
|
130
|
+
const stat = await fs.lstat(markerPath);
|
|
131
|
+
if (!stat.isFile() || stat.size > 65536) return undefined;
|
|
132
|
+
const markerRaw = await fs.readFile(markerPath, 'utf8');
|
|
123
133
|
const marker = JSON.parse(markerRaw);
|
|
124
134
|
return isValidTargetCacheMarker(marker) ? marker : undefined;
|
|
125
135
|
} catch {
|
|
@@ -161,86 +171,56 @@ export async function getDirectorySize(pathArg: string): Promise<number> {
|
|
|
161
171
|
return total;
|
|
162
172
|
}
|
|
163
173
|
|
|
164
|
-
async function getDirectoryUsage(pathArg: string
|
|
174
|
+
async function getDirectoryUsage(pathArg: string): Promise<{ bytes: number; newestMtimeMs: number; truncated: boolean }> {
|
|
165
175
|
let bytes = 0;
|
|
166
176
|
let newestMtimeMs = 0;
|
|
167
177
|
let visitedEntries = 0;
|
|
168
178
|
let truncated = false;
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
visitedEntries++;
|
|
176
|
-
|
|
177
|
-
let stat;
|
|
178
|
-
try {
|
|
179
|
-
stat = await fs.stat(entryPathArg);
|
|
180
|
-
} catch {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
newestMtimeMs = Math.max(newestMtimeMs, stat.mtimeMs);
|
|
184
|
-
if (stat.isFile()) {
|
|
185
|
-
bytes += stat.size;
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (!stat.isDirectory()) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
let entries;
|
|
179
|
+
const deadline = Date.now() + 30_000;
|
|
180
|
+
const root = await fs.lstat(pathArg);
|
|
181
|
+
const visit = async (entryPath: string): Promise<void> => {
|
|
182
|
+
if (++visitedEntries > 100_000 || Date.now() > deadline) { truncated = true; return; }
|
|
193
183
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
for (const entry of entries) {
|
|
199
|
-
if (visitedEntries >= maxEntriesArg) {
|
|
184
|
+
const stat = await fs.lstat(entryPath);
|
|
185
|
+
if (stat.isSymbolicLink() || stat.dev !== root.dev || stat.uid !== root.uid) {
|
|
200
186
|
truncated = true;
|
|
201
187
|
return;
|
|
202
188
|
}
|
|
203
|
-
|
|
204
|
-
|
|
189
|
+
newestMtimeMs = Math.max(newestMtimeMs, stat.mtimeMs);
|
|
190
|
+
if (stat.isFile()) bytes += stat.size;
|
|
191
|
+
else if (stat.isDirectory()) {
|
|
192
|
+
for (const entry of await fs.readdir(entryPath)) {
|
|
193
|
+
await visit(path.join(entryPath, entry));
|
|
194
|
+
if (truncated) break;
|
|
195
|
+
}
|
|
196
|
+
} else truncated = true;
|
|
197
|
+
} catch { truncated = true; }
|
|
205
198
|
};
|
|
206
|
-
|
|
207
199
|
await visit(pathArg);
|
|
208
200
|
return { bytes, newestMtimeMs, truncated };
|
|
209
201
|
}
|
|
210
202
|
|
|
211
|
-
async function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const stat = await fs.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return 0;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
let entries;
|
|
221
|
-
try {
|
|
222
|
-
entries = await fs.readdir(pathArg, { withFileTypes: true });
|
|
223
|
-
} catch {
|
|
224
|
-
return newest;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
for (const entry of entries) {
|
|
228
|
-
const entryPath = path.join(pathArg, entry.name);
|
|
229
|
-
if (entry.isDirectory()) {
|
|
230
|
-
newest = Math.max(newest, await getNewestMtimeMs(entryPath));
|
|
231
|
-
} else if (entry.isFile()) {
|
|
232
|
-
try {
|
|
233
|
-
newest = Math.max(newest, (await fs.stat(entryPath)).mtimeMs);
|
|
234
|
-
} catch {
|
|
235
|
-
// Ignore files that disappear while planning a prune.
|
|
236
|
-
}
|
|
203
|
+
async function assertSafeTarget(workspace: string, target: string): Promise<void> {
|
|
204
|
+
if (!isSafeManagedTargetDir(workspace, target)) throw new Error(`Unsafe target: ${target}`);
|
|
205
|
+
for (const location of [workspace, target]) {
|
|
206
|
+
const stat = await fs.lstat(location);
|
|
207
|
+
if (!stat.isDirectory() || await fs.realpath(location) !== location || stat.uid !== process.getuid?.()) {
|
|
208
|
+
throw new Error(`Target must be an owned directory without symlink ancestors: ${location}`);
|
|
237
209
|
}
|
|
238
210
|
}
|
|
211
|
+
const workspaceStat = await fs.lstat(workspace);
|
|
212
|
+
const targetStat = await fs.lstat(target);
|
|
213
|
+
if (workspaceStat.dev !== targetStat.dev) throw new Error('Target crosses a filesystem boundary');
|
|
214
|
+
}
|
|
239
215
|
|
|
240
|
-
|
|
216
|
+
function validateThresholds(options: { days?: number; maxSizeBytes?: number }): void {
|
|
217
|
+
for (const value of [options.days, options.maxSizeBytes]) {
|
|
218
|
+
if (value !== undefined && (!Number.isFinite(value) || value < 0)) throw new Error('Prune thresholds must be finite non-negative numbers');
|
|
219
|
+
}
|
|
241
220
|
}
|
|
242
221
|
|
|
243
222
|
export async function createTsrustPrunePlan(optionsArg: ITsrustPruneOptions): Promise<ITsrustPruneCandidate[]> {
|
|
223
|
+
validateThresholds(optionsArg);
|
|
244
224
|
const workspace = path.resolve(optionsArg.workspace);
|
|
245
225
|
const managedTargetDir = optionsArg.targetDir
|
|
246
226
|
? resolveManagedTargetDir(workspace, optionsArg.targetDir)
|
|
@@ -264,14 +244,19 @@ export async function createTsrustPrunePlan(optionsArg: ITsrustPruneOptions): Pr
|
|
|
264
244
|
const ageMatches = typeof maxAgeMs === 'number' ? Date.now() - usage.newestMtimeMs >= maxAgeMs : false;
|
|
265
245
|
const bytes = usage.bytes;
|
|
266
246
|
const sizeMatches = typeof optionsArg.maxSizeBytes === 'number' ? bytes >= optionsArg.maxSizeBytes : false;
|
|
267
|
-
|
|
268
|
-
|
|
247
|
+
let safetyError: string | undefined;
|
|
248
|
+
if (isManagedTarget) {
|
|
249
|
+
try { await assertSafeTarget(workspace, candidate); }
|
|
250
|
+
catch (error) { safetyError = (error as Error).message; }
|
|
251
|
+
}
|
|
252
|
+
const shouldPrune = !!marker && isManagedTarget && !usage.truncated && !safetyError && (ageMatches || sizeMatches);
|
|
253
|
+
const reason = safetyError || (usage.truncated ? 'incomplete or unsafe cache scan; report-only' : undefined) || (marker
|
|
269
254
|
? !isManagedTarget
|
|
270
255
|
? 'marked Rust target outside managed tsrust target allowlist; report-only'
|
|
271
256
|
: shouldPrune
|
|
272
257
|
? [ageMatches ? `older than ${optionsArg.days} day(s)` : '', sizeMatches ? `at least ${optionsArg.maxSizeBytes} bytes` : ''].filter(Boolean).join(', ')
|
|
273
258
|
: 'marked tsrust target cache; below prune thresholds'
|
|
274
|
-
: 'unmarked conventional Rust target; report-only';
|
|
259
|
+
: 'unmarked conventional Rust target; report-only');
|
|
275
260
|
|
|
276
261
|
pruneCandidates.push({
|
|
277
262
|
path: candidate,
|
|
@@ -281,6 +266,8 @@ export async function createTsrustPrunePlan(optionsArg: ITsrustPruneOptions): Pr
|
|
|
281
266
|
shouldPrune,
|
|
282
267
|
reason,
|
|
283
268
|
truncated: usage.truncated,
|
|
269
|
+
criteria: { days: optionsArg.days, maxSizeBytes: optionsArg.maxSizeBytes },
|
|
270
|
+
identity: marker ? { dev: (await fs.lstat(candidate)).dev, ino: (await fs.lstat(candidate)).ino, markerCreatedAt: marker.createdAt } : undefined,
|
|
284
271
|
});
|
|
285
272
|
}
|
|
286
273
|
|
|
@@ -289,16 +276,28 @@ export async function createTsrustPrunePlan(optionsArg: ITsrustPruneOptions): Pr
|
|
|
289
276
|
|
|
290
277
|
export async function applyTsrustPrunePlan(planArg: ITsrustPruneCandidate[]): Promise<void> {
|
|
291
278
|
for (const candidate of planArg) {
|
|
292
|
-
if (!candidate.shouldPrune)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
279
|
+
if (!candidate.shouldPrune) continue;
|
|
280
|
+
await withTargetCacheLock(candidate.path, async () => {
|
|
281
|
+
await assertSafeTarget(candidate.workspace, candidate.path);
|
|
282
|
+
if (!candidate.criteria || !candidate.identity) throw new Error('Prune plan lacks eligibility and identity; create a new plan');
|
|
283
|
+
validateThresholds(candidate.criteria);
|
|
284
|
+
const fresh = (await createTsrustPrunePlan({ workspace: candidate.workspace, targetDir: candidate.path, ...candidate.criteria }))
|
|
285
|
+
.find((entry) => entry.path === candidate.path);
|
|
286
|
+
if (!fresh?.shouldPrune || JSON.stringify(fresh.identity) !== JSON.stringify(candidate.identity)) {
|
|
287
|
+
throw new Error(`Rust target cache changed or is no longer eligible: ${candidate.path}`);
|
|
288
|
+
}
|
|
289
|
+
await assertTargetCacheIdle(candidate.path);
|
|
290
|
+
await assertSafeTarget(candidate.workspace, candidate.path);
|
|
291
|
+
const stat = await fs.lstat(candidate.path);
|
|
292
|
+
if (stat.dev !== candidate.identity.dev || stat.ino !== candidate.identity.ino) throw new Error('Target identity changed');
|
|
293
|
+
// Recheck after process inspection as well; it can take time on busy hosts.
|
|
294
|
+
const usage = await getDirectoryUsage(candidate.path);
|
|
295
|
+
const { days, maxSizeBytes } = candidate.criteria;
|
|
296
|
+
if (usage.truncated || !((days !== undefined && Date.now() - usage.newestMtimeMs >= days * 86400000)
|
|
297
|
+
|| (maxSizeBytes !== undefined && usage.bytes >= maxSizeBytes))) throw new Error('Target eligibility changed');
|
|
298
|
+
const marker = await readTargetCacheMarker(candidate.path);
|
|
299
|
+
if (!marker || marker.createdAt !== candidate.identity.markerCreatedAt) throw new Error('Target marker changed');
|
|
300
|
+
await fs.rm(candidate.path, { recursive: true, force: false });
|
|
301
|
+
});
|
|
303
302
|
}
|
|
304
303
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
const heldTargets = new plugins.AsyncLocalStorage<ReadonlySet<string>>();
|
|
4
|
+
|
|
5
|
+
/** A kernel-owned Linux socket serializes builds and pruning without stale lock files. */
|
|
6
|
+
export async function withTargetCacheLock<T>(targetArg: string, action: () => Promise<T>): Promise<T> {
|
|
7
|
+
const target = plugins.path.resolve(targetArg);
|
|
8
|
+
if (process.platform !== 'linux' || heldTargets.getStore()?.has(target)) return action();
|
|
9
|
+
const address = '\0tsrust-target-' + plugins.crypto.createHash('sha256').update(target).digest('hex');
|
|
10
|
+
const deadline = Date.now() + 30_000;
|
|
11
|
+
let server: plugins.net.Server;
|
|
12
|
+
while (true) {
|
|
13
|
+
server = plugins.net.createServer((connection) => connection.destroy());
|
|
14
|
+
const acquired = await new Promise<boolean>((resolve, reject) => {
|
|
15
|
+
const failed = (error: NodeJS.ErrnoException) => {
|
|
16
|
+
if (error.code === 'EADDRINUSE') resolve(false);
|
|
17
|
+
else reject(error);
|
|
18
|
+
};
|
|
19
|
+
server.once('error', failed);
|
|
20
|
+
server.listen(address, () => { server.removeListener('error', failed); resolve(true); });
|
|
21
|
+
});
|
|
22
|
+
if (acquired) break;
|
|
23
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for the Rust target cache guard: ${target}`);
|
|
24
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 100));
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return await heldTargets.run(new Set([...(heldTargets.getStore() || []), target]), action);
|
|
28
|
+
} finally {
|
|
29
|
+
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Fail closed if this host cannot prove that the cache is idle. */
|
|
34
|
+
export async function assertTargetCacheIdle(target: string): Promise<void> {
|
|
35
|
+
if (process.platform !== 'linux') throw new Error('Safe target pruning requires Linux /proc process inspection');
|
|
36
|
+
const fs = plugins.fsPromises;
|
|
37
|
+
const contains = (value: string) => value === target || value.startsWith(target + plugins.path.sep);
|
|
38
|
+
const disappeared = (error: unknown) => ['ENOENT', 'ESRCH'].includes((error as NodeJS.ErrnoException).code || '');
|
|
39
|
+
const deadline = Date.now() + 30_000;
|
|
40
|
+
for (const pid of await fs.readdir('/proc')) {
|
|
41
|
+
if (!/^\d+$/.test(pid) || Number(pid) === process.pid) continue;
|
|
42
|
+
if (Date.now() > deadline) throw new Error('Process inspection timed out; refusing to prune');
|
|
43
|
+
const base = `/proc/${pid}`;
|
|
44
|
+
try {
|
|
45
|
+
const status = await fs.readFile(`${base}/status`, 'utf8');
|
|
46
|
+
const uid = Number(status.match(/^Uid:\s+(\d+)/m)?.[1]);
|
|
47
|
+
if (!Number.isFinite(uid)) throw new Error(`Cannot identify process ${pid}`);
|
|
48
|
+
if (uid !== process.getuid?.()) continue;
|
|
49
|
+
const command = await fs.readFile(`${base}/comm`, 'utf8');
|
|
50
|
+
if (/^(cargo|rustc|rust-analyzer)\n?$/.test(command)) throw new Error(`Rust tool process ${pid} is active; refusing to prune`);
|
|
51
|
+
for (const name of ['cwd', 'exe', ...(await fs.readdir(`${base}/fd`)).map((fd) => `fd/${fd}`)]) {
|
|
52
|
+
try {
|
|
53
|
+
if (contains(await fs.readlink(`${base}/${name}`))) throw new Error(`Process ${pid} is using target cache ${target}`);
|
|
54
|
+
} catch (error) { if (!disappeared(error)) throw error; }
|
|
55
|
+
}
|
|
56
|
+
const maps = await fs.readFile(`${base}/maps`, 'utf8');
|
|
57
|
+
if (maps.split('\n').some((line) => {
|
|
58
|
+
const mappedPath = line.match(/^\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(.*)$/)?.[1];
|
|
59
|
+
return mappedPath !== undefined && contains(mappedPath);
|
|
60
|
+
})) throw new Error(`Process ${pid} maps target cache ${target}`);
|
|
61
|
+
} catch (error) { if (!disappeared(error)) throw error; }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withTargetCacheLock } from '../mod_cargo/classes.targetguard.js';
|
|
1
2
|
import * as fs from 'fs';
|
|
2
3
|
import * as path from 'path';
|
|
3
4
|
import * as os from 'os';
|
|
@@ -175,56 +176,144 @@ export class TsRustCli {
|
|
|
175
176
|
const distDir = path.join(this.cwd, 'dist_rust');
|
|
176
177
|
const profile = isDebug ? 'debug' : 'release';
|
|
177
178
|
const managedTargetDir = resolveManagedTargetDir(this.cwd, this.config.targetDir);
|
|
178
|
-
await
|
|
179
|
+
await withTargetCacheLock(managedTargetDir, async () => {
|
|
180
|
+
await writeTargetCacheMarker(managedTargetDir);
|
|
181
|
+
|
|
182
|
+
// CLI targets override host-specific and legacy configured targets.
|
|
183
|
+
const cliTargets = (argvArg as any).target;
|
|
184
|
+
const cliTargetList: string[] | undefined = cliTargets
|
|
185
|
+
? (Array.isArray(cliTargets) ? cliTargets : [cliTargets])
|
|
186
|
+
: undefined;
|
|
187
|
+
const hostTriple = new ToolchainManager().getHostTriple();
|
|
188
|
+
const resolvedTargets = resolveBuildTargets(this.config, hostTriple, cliTargetList);
|
|
189
|
+
|
|
190
|
+
const useStatic = !!(argvArg as any).static || !!this.config.static;
|
|
191
|
+
if (useStatic) {
|
|
192
|
+
console.log('Static linking enabled (crt-static for linux-gnu targets)');
|
|
193
|
+
}
|
|
179
194
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
: undefined;
|
|
185
|
-
const hostTriple = new ToolchainManager().getHostTriple();
|
|
186
|
-
const resolvedTargets = resolveBuildTargets(this.config, hostTriple, cliTargetList);
|
|
195
|
+
const rustflags = this.buildRustflags(rustDir);
|
|
196
|
+
if (rustflags.length > 0) {
|
|
197
|
+
console.log(`Using additional Rust flags: ${rustflags.join(' ')}`);
|
|
198
|
+
}
|
|
187
199
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
200
|
+
if (resolvedTargets.length > 0) {
|
|
201
|
+
// Cross-compilation mode
|
|
202
|
+
console.log(
|
|
203
|
+
`Cross-compiling for: ${resolvedTargets.map((target) => `${target.friendly} (${target.triple})`).join(', ')}`,
|
|
204
|
+
);
|
|
192
205
|
|
|
193
|
-
|
|
194
|
-
if (rustflags.length > 0) {
|
|
195
|
-
console.log(`Using additional Rust flags: ${rustflags.join(' ')}`);
|
|
196
|
-
}
|
|
206
|
+
await FsHelpers.ensureEmptyDir(distDir);
|
|
197
207
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
for (const { triple, friendly } of resolvedTargets) {
|
|
209
|
+
console.log(`\n--- Building for ${friendly} (${triple}) ---`);
|
|
210
|
+
if (useStatic && !isLinuxTriple(triple)) {
|
|
211
|
+
console.log(
|
|
212
|
+
`Note: static linking is not applicable for ${triple}; building with default linkage.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const cargoRunner = new CargoRunner(rustDir, envPrefix);
|
|
216
|
+
const gitBefore = await captureGitSnapshot(this.cwd);
|
|
217
|
+
const provenanceBefore = this.collectBuildProvenance(gitBefore);
|
|
218
|
+
const buildResult = await cargoRunner.build({
|
|
219
|
+
debug: isDebug,
|
|
220
|
+
clean: shouldClean,
|
|
221
|
+
target: triple,
|
|
222
|
+
locked: this.config.locked === true,
|
|
223
|
+
crtStatic: useStatic && wantsCrtStatic(triple),
|
|
224
|
+
rustflags,
|
|
225
|
+
targetDir: managedTargetDir,
|
|
226
|
+
});
|
|
203
227
|
|
|
204
|
-
|
|
228
|
+
if (!buildResult.success) {
|
|
229
|
+
console.error(`Build failed for target ${triple} with exit code ${buildResult.exitCode}`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
const gitAfter = await captureGitSnapshot(this.cwd);
|
|
233
|
+
assertGitSnapshotUnchanged(gitBefore, gitAfter);
|
|
234
|
+
const provenanceAfter = this.collectBuildProvenance(gitAfter);
|
|
235
|
+
this.assertProjectIdentityUnchanged(provenanceBefore, provenanceAfter);
|
|
205
236
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
237
|
+
const targetDir = getCargoArtifactDir(managedTargetDir, profile, triple);
|
|
238
|
+
|
|
239
|
+
for (const binName of workspaceInfo.binTargets) {
|
|
240
|
+
const srcBinary = path.join(targetDir, binName);
|
|
241
|
+
const destName = `${binName}_${friendly}`;
|
|
242
|
+
const destBinary = path.join(distDir, destName);
|
|
243
|
+
|
|
244
|
+
if (!(await FsHelpers.fileExists(srcBinary))) {
|
|
245
|
+
console.warn(`Warning: Expected binary not found: ${srcBinary}`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (isDarwinTriple(triple)) {
|
|
250
|
+
const signatureResult = await ensureDarwinCodeSignature(srcBinary);
|
|
251
|
+
console.log(
|
|
252
|
+
`${signatureResult === 'applied' ? 'Applied' : 'Verified'} Darwin code signature: ${srcBinary}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
await FsHelpers.copyFile(srcBinary, destBinary);
|
|
256
|
+
await FsHelpers.makeExecutable(destBinary);
|
|
257
|
+
|
|
258
|
+
const size = await FsHelpers.getFileSize(destBinary);
|
|
259
|
+
console.log(
|
|
260
|
+
`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${destName}`,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (useStatic && isLinuxTriple(triple)) {
|
|
264
|
+
if (!(await ElfInspector.isStaticElf(destBinary))) {
|
|
265
|
+
console.error(
|
|
266
|
+
`Error: ${destName} is not statically linked (PT_INTERP present) although static linking was requested.`,
|
|
267
|
+
);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
console.log(`Verified statically linked: dist_rust/${destName}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await ProvenanceStore.write(destBinary, {
|
|
274
|
+
...provenanceBefore,
|
|
275
|
+
binary: binName,
|
|
276
|
+
target: friendly,
|
|
277
|
+
});
|
|
278
|
+
console.log(
|
|
279
|
+
`Wrote provenance: ${provenanceBefore.projectName}@${provenanceBefore.projectVersion} ${provenanceBefore.gitCommit.slice(0, 12)} (${friendly})`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Only clean on first iteration
|
|
284
|
+
if (shouldClean) {
|
|
285
|
+
shouldClean = false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
// Native build. When static linking is requested, build with an
|
|
290
|
+
// explicit host --target so RUSTFLAGS never reach host artifacts
|
|
291
|
+
// (proc-macros cannot build with +crt-static on linux-gnu).
|
|
292
|
+
let nativeTriple: string | undefined;
|
|
293
|
+
if (useStatic) {
|
|
294
|
+
nativeTriple = hostTriple;
|
|
295
|
+
if (!isLinuxTriple(nativeTriple)) {
|
|
296
|
+
console.log(
|
|
297
|
+
`Note: static linking is not applicable for ${nativeTriple}; building with default linkage.`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
212
300
|
}
|
|
301
|
+
|
|
213
302
|
const cargoRunner = new CargoRunner(rustDir, envPrefix);
|
|
214
303
|
const gitBefore = await captureGitSnapshot(this.cwd);
|
|
215
304
|
const provenanceBefore = this.collectBuildProvenance(gitBefore);
|
|
216
305
|
const buildResult = await cargoRunner.build({
|
|
217
306
|
debug: isDebug,
|
|
218
307
|
clean: shouldClean,
|
|
219
|
-
target:
|
|
308
|
+
target: nativeTriple,
|
|
220
309
|
locked: this.config.locked === true,
|
|
221
|
-
crtStatic:
|
|
310
|
+
crtStatic: !!nativeTriple && wantsCrtStatic(nativeTriple),
|
|
222
311
|
rustflags,
|
|
223
312
|
targetDir: managedTargetDir,
|
|
224
313
|
});
|
|
225
314
|
|
|
226
315
|
if (!buildResult.success) {
|
|
227
|
-
console.error(`Build failed
|
|
316
|
+
console.error(`Build failed with exit code ${buildResult.exitCode}`);
|
|
228
317
|
process.exit(1);
|
|
229
318
|
}
|
|
230
319
|
const gitAfter = await captureGitSnapshot(this.cwd);
|
|
@@ -232,19 +321,20 @@ export class TsRustCli {
|
|
|
232
321
|
const provenanceAfter = this.collectBuildProvenance(gitAfter);
|
|
233
322
|
this.assertProjectIdentityUnchanged(provenanceBefore, provenanceAfter);
|
|
234
323
|
|
|
235
|
-
const targetDir = getCargoArtifactDir(managedTargetDir, profile,
|
|
324
|
+
const targetDir = getCargoArtifactDir(managedTargetDir, profile, nativeTriple);
|
|
325
|
+
|
|
326
|
+
await FsHelpers.ensureEmptyDir(distDir);
|
|
236
327
|
|
|
237
328
|
for (const binName of workspaceInfo.binTargets) {
|
|
238
329
|
const srcBinary = path.join(targetDir, binName);
|
|
239
|
-
const
|
|
240
|
-
const destBinary = path.join(distDir, destName);
|
|
330
|
+
const destBinary = path.join(distDir, binName);
|
|
241
331
|
|
|
242
332
|
if (!(await FsHelpers.fileExists(srcBinary))) {
|
|
243
333
|
console.warn(`Warning: Expected binary not found: ${srcBinary}`);
|
|
244
334
|
continue;
|
|
245
335
|
}
|
|
246
336
|
|
|
247
|
-
if (isDarwinTriple(
|
|
337
|
+
if (isDarwinTriple(hostTriple)) {
|
|
248
338
|
const signatureResult = await ensureDarwinCodeSignature(srcBinary);
|
|
249
339
|
console.log(
|
|
250
340
|
`${signatureResult === 'applied' ? 'Applied' : 'Verified'} Darwin code signature: ${srcBinary}`,
|
|
@@ -255,127 +345,41 @@ export class TsRustCli {
|
|
|
255
345
|
|
|
256
346
|
const size = await FsHelpers.getFileSize(destBinary);
|
|
257
347
|
console.log(
|
|
258
|
-
`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${
|
|
348
|
+
`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`,
|
|
259
349
|
);
|
|
260
350
|
|
|
261
|
-
if (useStatic && isLinuxTriple(
|
|
351
|
+
if (useStatic && nativeTriple && isLinuxTriple(nativeTriple)) {
|
|
262
352
|
if (!(await ElfInspector.isStaticElf(destBinary))) {
|
|
263
353
|
console.error(
|
|
264
|
-
`Error: ${
|
|
354
|
+
`Error: ${binName} is not statically linked (PT_INTERP present) although static linking was requested.`,
|
|
265
355
|
);
|
|
266
356
|
process.exit(1);
|
|
267
357
|
}
|
|
268
|
-
console.log(`Verified statically linked: dist_rust/${
|
|
358
|
+
console.log(`Verified statically linked: dist_rust/${binName}`);
|
|
269
359
|
}
|
|
270
360
|
|
|
271
361
|
await ProvenanceStore.write(destBinary, {
|
|
272
362
|
...provenanceBefore,
|
|
273
363
|
binary: binName,
|
|
274
|
-
target:
|
|
364
|
+
target: nativeTriple || 'native',
|
|
275
365
|
});
|
|
276
366
|
console.log(
|
|
277
|
-
`Wrote provenance: ${provenanceBefore.projectName}@${provenanceBefore.projectVersion} ${provenanceBefore.gitCommit.slice(0, 12)} (${
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Only clean on first iteration
|
|
282
|
-
if (shouldClean) {
|
|
283
|
-
shouldClean = false;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
} else {
|
|
287
|
-
// Native build. When static linking is requested, build with an
|
|
288
|
-
// explicit host --target so RUSTFLAGS never reach host artifacts
|
|
289
|
-
// (proc-macros cannot build with +crt-static on linux-gnu).
|
|
290
|
-
let nativeTriple: string | undefined;
|
|
291
|
-
if (useStatic) {
|
|
292
|
-
nativeTriple = hostTriple;
|
|
293
|
-
if (!isLinuxTriple(nativeTriple)) {
|
|
294
|
-
console.log(
|
|
295
|
-
`Note: static linking is not applicable for ${nativeTriple}; building with default linkage.`,
|
|
367
|
+
`Wrote provenance: ${provenanceBefore.projectName}@${provenanceBefore.projectVersion} ${provenanceBefore.gitCommit.slice(0, 12)} (${nativeTriple || 'native'})`,
|
|
296
368
|
);
|
|
297
369
|
}
|
|
298
370
|
}
|
|
299
371
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
clean: shouldClean,
|
|
306
|
-
target: nativeTriple,
|
|
307
|
-
locked: this.config.locked === true,
|
|
308
|
-
crtStatic: !!nativeTriple && wantsCrtStatic(nativeTriple),
|
|
309
|
-
rustflags,
|
|
310
|
-
targetDir: managedTargetDir,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
if (!buildResult.success) {
|
|
314
|
-
console.error(`Build failed with exit code ${buildResult.exitCode}`);
|
|
315
|
-
process.exit(1);
|
|
316
|
-
}
|
|
317
|
-
const gitAfter = await captureGitSnapshot(this.cwd);
|
|
318
|
-
assertGitSnapshotUnchanged(gitBefore, gitAfter);
|
|
319
|
-
const provenanceAfter = this.collectBuildProvenance(gitAfter);
|
|
320
|
-
this.assertProjectIdentityUnchanged(provenanceBefore, provenanceAfter);
|
|
321
|
-
|
|
322
|
-
const targetDir = getCargoArtifactDir(managedTargetDir, profile, nativeTriple);
|
|
323
|
-
|
|
324
|
-
await FsHelpers.ensureEmptyDir(distDir);
|
|
325
|
-
|
|
326
|
-
for (const binName of workspaceInfo.binTargets) {
|
|
327
|
-
const srcBinary = path.join(targetDir, binName);
|
|
328
|
-
const destBinary = path.join(distDir, binName);
|
|
329
|
-
|
|
330
|
-
if (!(await FsHelpers.fileExists(srcBinary))) {
|
|
331
|
-
console.warn(`Warning: Expected binary not found: ${srcBinary}`);
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (isDarwinTriple(hostTriple)) {
|
|
336
|
-
const signatureResult = await ensureDarwinCodeSignature(srcBinary);
|
|
337
|
-
console.log(
|
|
338
|
-
`${signatureResult === 'applied' ? 'Applied' : 'Verified'} Darwin code signature: ${srcBinary}`,
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
await FsHelpers.copyFile(srcBinary, destBinary);
|
|
342
|
-
await FsHelpers.makeExecutable(destBinary);
|
|
343
|
-
|
|
344
|
-
const size = await FsHelpers.getFileSize(destBinary);
|
|
345
|
-
console.log(
|
|
346
|
-
`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`,
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
if (useStatic && nativeTriple && isLinuxTriple(nativeTriple)) {
|
|
350
|
-
if (!(await ElfInspector.isStaticElf(destBinary))) {
|
|
351
|
-
console.error(
|
|
352
|
-
`Error: ${binName} is not statically linked (PT_INTERP present) although static linking was requested.`,
|
|
353
|
-
);
|
|
354
|
-
process.exit(1);
|
|
355
|
-
}
|
|
356
|
-
console.log(`Verified statically linked: dist_rust/${binName}`);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
await ProvenanceStore.write(destBinary, {
|
|
360
|
-
...provenanceBefore,
|
|
361
|
-
binary: binName,
|
|
362
|
-
target: nativeTriple || 'native',
|
|
372
|
+
if (this.shouldPruneAfterBuild()) {
|
|
373
|
+
const plan = await createTsrustPrunePlan({
|
|
374
|
+
workspace: this.cwd,
|
|
375
|
+
targetDir: managedTargetDir,
|
|
376
|
+
days: 0,
|
|
363
377
|
});
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
);
|
|
378
|
+
await applyTsrustPrunePlan(plan);
|
|
379
|
+
console.log('Pruned marked tsrust target cache after successful build.');
|
|
367
380
|
}
|
|
368
|
-
}
|
|
369
381
|
|
|
370
|
-
|
|
371
|
-
const plan = await createTsrustPrunePlan({
|
|
372
|
-
workspace: this.cwd,
|
|
373
|
-
targetDir: managedTargetDir,
|
|
374
|
-
days: 0,
|
|
375
|
-
});
|
|
376
|
-
await applyTsrustPrunePlan(plan);
|
|
377
|
-
console.log('Pruned marked tsrust target cache after successful build.');
|
|
378
|
-
}
|
|
382
|
+
});
|
|
379
383
|
|
|
380
384
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
381
385
|
console.log(`\nDone in ${elapsed}s`);
|
|
@@ -551,11 +555,12 @@ export class TsRustCli {
|
|
|
551
555
|
const daysArg = (argvArg as any).days;
|
|
552
556
|
const days = daysArg === undefined ? 14 : Number(daysArg);
|
|
553
557
|
const maxSizeBytes = parseSizeToBytes((argvArg as any).maxSize as string | undefined);
|
|
558
|
+
if ((argvArg as any).maxSize !== undefined && maxSizeBytes === undefined) throw new Error('Invalid --max-size');
|
|
554
559
|
const apply = !!(argvArg as any).apply;
|
|
555
560
|
const plan = await createTsrustPrunePlan({
|
|
556
561
|
workspace,
|
|
557
562
|
targetDir: resolveManagedTargetDir(workspace, this.config.targetDir),
|
|
558
|
-
days
|
|
563
|
+
days,
|
|
559
564
|
maxSizeBytes,
|
|
560
565
|
});
|
|
561
566
|
|