@git.zone/tsrust 1.5.0 → 1.7.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/.smartconfig.json +10 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mod_cargo/classes.cargorunner.d.ts +4 -1
- package/dist_ts/mod_cargo/classes.cargorunner.js +8 -6
- package/dist_ts/mod_cargo/classes.targetcache.d.ts +37 -0
- package/dist_ts/mod_cargo/classes.targetcache.js +260 -0
- package/dist_ts/mod_cargo/index.d.ts +1 -0
- package/dist_ts/mod_cargo/index.js +2 -1
- package/dist_ts/mod_cli/classes.tsrustcli.d.ts +13 -0
- package/dist_ts/mod_cli/classes.tsrustcli.js +126 -8
- package/dist_ts/mod_elf/classes.provenance.d.ts +24 -0
- package/dist_ts/mod_elf/classes.provenance.js +0 -0
- package/dist_ts/mod_elf/index.d.ts +1 -0
- package/dist_ts/mod_elf/index.js +2 -1
- package/package.json +2 -2
- package/readme.md +39 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mod_cargo/classes.cargorunner.ts +8 -6
- package/ts/mod_cargo/classes.targetcache.ts +304 -0
- package/ts/mod_cargo/index.ts +1 -0
- package/ts/mod_cli/classes.tsrustcli.ts +142 -7
- package/ts/mod_elf/classes.provenance.ts +0 -0
- package/ts/mod_elf/index.ts +1 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const toolCacheMarkerFile = '.gitzone-tool-cache.json';
|
|
5
|
+
export const tsrustCacheOwner = '@git.zone/tsrust';
|
|
6
|
+
export const targetCacheKind = 'rust-target-cache';
|
|
7
|
+
|
|
8
|
+
export interface IToolCacheMarker {
|
|
9
|
+
owner: string;
|
|
10
|
+
kind: string;
|
|
11
|
+
safeToPrune: boolean;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
schemaVersion: 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ITsrustPruneCandidate {
|
|
17
|
+
path: string;
|
|
18
|
+
workspace: string;
|
|
19
|
+
bytes: number;
|
|
20
|
+
marked: boolean;
|
|
21
|
+
shouldPrune: boolean;
|
|
22
|
+
reason: string;
|
|
23
|
+
truncated?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ITsrustPruneOptions {
|
|
27
|
+
workspace: string;
|
|
28
|
+
targetDir?: string;
|
|
29
|
+
days?: number;
|
|
30
|
+
maxSizeBytes?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createTargetCacheMarker(): IToolCacheMarker {
|
|
34
|
+
return {
|
|
35
|
+
owner: tsrustCacheOwner,
|
|
36
|
+
kind: targetCacheKind,
|
|
37
|
+
safeToPrune: true,
|
|
38
|
+
createdAt: new Date().toISOString(),
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isValidTargetCacheMarker(markerArg: unknown): markerArg is IToolCacheMarker {
|
|
44
|
+
const marker = markerArg as Partial<IToolCacheMarker>;
|
|
45
|
+
return marker.owner === tsrustCacheOwner
|
|
46
|
+
&& marker.kind === targetCacheKind
|
|
47
|
+
&& marker.safeToPrune === true
|
|
48
|
+
&& marker.schemaVersion === 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getDefaultManagedTargetDir(workspaceArg: string): string {
|
|
52
|
+
return path.resolve(workspaceArg, '.nogit', 'tsrust-target');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isSafeManagedTargetDir(workspaceArg: string, targetDirArg: string): boolean {
|
|
56
|
+
const workspace = path.resolve(workspaceArg);
|
|
57
|
+
const targetDir = path.resolve(targetDirArg);
|
|
58
|
+
const relative = path.relative(workspace, targetDir);
|
|
59
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return relative === path.join('.nogit', 'tsrust-target')
|
|
63
|
+
|| relative.startsWith(`${path.join('.nogit', 'tsrust-')}`)
|
|
64
|
+
|| relative.startsWith(`${path.join('.nogit', 'tsrust')}${path.sep}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveManagedTargetDir(workspaceArg: string, configTargetDirArg?: string): string {
|
|
68
|
+
const configured = process.env.TSRUST_TARGET_DIR || configTargetDirArg;
|
|
69
|
+
const targetDir = configured
|
|
70
|
+
? path.isAbsolute(configured) ? configured : path.resolve(workspaceArg, configured)
|
|
71
|
+
: getDefaultManagedTargetDir(workspaceArg);
|
|
72
|
+
if (!isSafeManagedTargetDir(workspaceArg, targetDir)) {
|
|
73
|
+
throw new Error(`Refusing unsafe tsrust target dir outside workspace .nogit/: ${targetDir}`);
|
|
74
|
+
}
|
|
75
|
+
return targetDir;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getCargoArtifactDir(targetDirArg: string, profileArg: string, targetTripleArg?: string): string {
|
|
79
|
+
return targetTripleArg
|
|
80
|
+
? path.join(targetDirArg, targetTripleArg, profileArg)
|
|
81
|
+
: path.join(targetDirArg, profileArg);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseSizeToBytes(valueArg: string | undefined): number | undefined {
|
|
85
|
+
if (!valueArg) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const match = valueArg.trim().match(/^(\d+(?:\.\d+)?)(b|kb|kib|mb|mib|gb|gib)?$/i);
|
|
89
|
+
if (!match) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const value = Number(match[1]);
|
|
93
|
+
const unit = (match[2] || 'b').toLowerCase();
|
|
94
|
+
const multiplier = unit === 'gb' || unit === 'gib'
|
|
95
|
+
? 1024 ** 3
|
|
96
|
+
: unit === 'mb' || unit === 'mib'
|
|
97
|
+
? 1024 ** 2
|
|
98
|
+
: unit === 'kb' || unit === 'kib'
|
|
99
|
+
? 1024
|
|
100
|
+
: 1;
|
|
101
|
+
return Math.round(value * multiplier);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function writeTargetCacheMarker(targetDirArg: string): Promise<void> {
|
|
105
|
+
await fs.mkdir(targetDirArg, { recursive: true });
|
|
106
|
+
const existingMarker = await readTargetCacheMarker(targetDirArg);
|
|
107
|
+
if (existingMarker) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const entries = await fs.readdir(targetDirArg);
|
|
111
|
+
if (entries.length > 0) {
|
|
112
|
+
throw new Error(`Refusing to mark pre-existing non-empty tsrust target cache: ${targetDirArg}`);
|
|
113
|
+
}
|
|
114
|
+
await fs.writeFile(
|
|
115
|
+
path.join(targetDirArg, toolCacheMarkerFile),
|
|
116
|
+
`${JSON.stringify(createTargetCacheMarker(), null, 2)}\n`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function readTargetCacheMarker(targetDirArg: string): Promise<IToolCacheMarker | undefined> {
|
|
121
|
+
try {
|
|
122
|
+
const markerRaw = await fs.readFile(path.join(targetDirArg, toolCacheMarkerFile), 'utf8');
|
|
123
|
+
const marker = JSON.parse(markerRaw);
|
|
124
|
+
return isValidTargetCacheMarker(marker) ? marker : undefined;
|
|
125
|
+
} catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function pathExists(pathArg: string): Promise<boolean> {
|
|
131
|
+
try {
|
|
132
|
+
await fs.stat(pathArg);
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function getDirectorySize(pathArg: string): Promise<number> {
|
|
140
|
+
let total = 0;
|
|
141
|
+
let entries;
|
|
142
|
+
try {
|
|
143
|
+
entries = await fs.readdir(pathArg, { withFileTypes: true });
|
|
144
|
+
} catch {
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const entryPath = path.join(pathArg, entry.name);
|
|
150
|
+
if (entry.isDirectory()) {
|
|
151
|
+
total += await getDirectorySize(entryPath);
|
|
152
|
+
} else if (entry.isFile()) {
|
|
153
|
+
try {
|
|
154
|
+
total += (await fs.stat(entryPath)).size;
|
|
155
|
+
} catch {
|
|
156
|
+
// Ignore files that disappear while planning a prune.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return total;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function getDirectoryUsage(pathArg: string, maxEntriesArg = 100_000): Promise<{ bytes: number; newestMtimeMs: number; truncated: boolean }> {
|
|
165
|
+
let bytes = 0;
|
|
166
|
+
let newestMtimeMs = 0;
|
|
167
|
+
let visitedEntries = 0;
|
|
168
|
+
let truncated = false;
|
|
169
|
+
|
|
170
|
+
const visit = async (entryPathArg: string): Promise<void> => {
|
|
171
|
+
if (visitedEntries >= maxEntriesArg) {
|
|
172
|
+
truncated = true;
|
|
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;
|
|
193
|
+
try {
|
|
194
|
+
entries = await fs.readdir(entryPathArg, { withFileTypes: true });
|
|
195
|
+
} catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
if (visitedEntries >= maxEntriesArg) {
|
|
200
|
+
truncated = true;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
await visit(path.join(entryPathArg, entry.name));
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
await visit(pathArg);
|
|
208
|
+
return { bytes, newestMtimeMs, truncated };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function getNewestMtimeMs(pathArg: string): Promise<number> {
|
|
212
|
+
let newest = 0;
|
|
213
|
+
try {
|
|
214
|
+
const stat = await fs.stat(pathArg);
|
|
215
|
+
newest = stat.mtimeMs;
|
|
216
|
+
} catch {
|
|
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
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return newest;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function createTsrustPrunePlan(optionsArg: ITsrustPruneOptions): Promise<ITsrustPruneCandidate[]> {
|
|
244
|
+
const workspace = path.resolve(optionsArg.workspace);
|
|
245
|
+
const managedTargetDir = optionsArg.targetDir
|
|
246
|
+
? resolveManagedTargetDir(workspace, optionsArg.targetDir)
|
|
247
|
+
: resolveManagedTargetDir(workspace);
|
|
248
|
+
const candidates = [
|
|
249
|
+
managedTargetDir,
|
|
250
|
+
path.join(workspace, 'rust', 'target'),
|
|
251
|
+
path.join(workspace, 'ts_rust', 'target'),
|
|
252
|
+
];
|
|
253
|
+
const uniqueCandidates = [...new Set(candidates.map((candidate) => path.resolve(candidate)))];
|
|
254
|
+
const pruneCandidates: ITsrustPruneCandidate[] = [];
|
|
255
|
+
const maxAgeMs = typeof optionsArg.days === 'number' ? optionsArg.days * 24 * 60 * 60 * 1000 : undefined;
|
|
256
|
+
|
|
257
|
+
for (const candidate of uniqueCandidates) {
|
|
258
|
+
if (!(await pathExists(candidate))) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const marker = await readTargetCacheMarker(candidate);
|
|
262
|
+
const usage = await getDirectoryUsage(candidate);
|
|
263
|
+
const isManagedTarget = isSafeManagedTargetDir(workspace, candidate);
|
|
264
|
+
const ageMatches = typeof maxAgeMs === 'number' ? Date.now() - usage.newestMtimeMs >= maxAgeMs : false;
|
|
265
|
+
const bytes = usage.bytes;
|
|
266
|
+
const sizeMatches = typeof optionsArg.maxSizeBytes === 'number' ? bytes >= optionsArg.maxSizeBytes : false;
|
|
267
|
+
const shouldPrune = !!marker && isManagedTarget && (ageMatches || sizeMatches);
|
|
268
|
+
const reason = marker
|
|
269
|
+
? !isManagedTarget
|
|
270
|
+
? 'marked Rust target outside managed tsrust target allowlist; report-only'
|
|
271
|
+
: shouldPrune
|
|
272
|
+
? [ageMatches ? `older than ${optionsArg.days} day(s)` : '', sizeMatches ? `at least ${optionsArg.maxSizeBytes} bytes` : ''].filter(Boolean).join(', ')
|
|
273
|
+
: 'marked tsrust target cache; below prune thresholds'
|
|
274
|
+
: 'unmarked conventional Rust target; report-only';
|
|
275
|
+
|
|
276
|
+
pruneCandidates.push({
|
|
277
|
+
path: candidate,
|
|
278
|
+
workspace,
|
|
279
|
+
bytes,
|
|
280
|
+
marked: !!marker,
|
|
281
|
+
shouldPrune,
|
|
282
|
+
reason,
|
|
283
|
+
truncated: usage.truncated,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return pruneCandidates;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function applyTsrustPrunePlan(planArg: ITsrustPruneCandidate[]): Promise<void> {
|
|
291
|
+
for (const candidate of planArg) {
|
|
292
|
+
if (!candidate.shouldPrune) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (!isSafeManagedTargetDir(candidate.workspace, candidate.path)) {
|
|
296
|
+
throw new Error(`Refusing unsafe Rust target cache path: ${candidate.path}`);
|
|
297
|
+
}
|
|
298
|
+
const marker = await readTargetCacheMarker(candidate.path);
|
|
299
|
+
if (!marker) {
|
|
300
|
+
throw new Error(`Refusing to remove unmarked Rust target cache: ${candidate.path}`);
|
|
301
|
+
}
|
|
302
|
+
await fs.rm(candidate.path, { recursive: true, force: true });
|
|
303
|
+
}
|
|
304
|
+
}
|
package/ts/mod_cargo/index.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
1
2
|
import * as path from 'path';
|
|
2
3
|
import * as os from 'os';
|
|
3
4
|
import * as plugins from '../plugins.js';
|
|
4
5
|
import { CargoConfig } from '../mod_cargo/index.js';
|
|
5
6
|
import { CargoRunner } from '../mod_cargo/index.js';
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
applyTsrustPrunePlan,
|
|
9
|
+
createTsrustPrunePlan,
|
|
10
|
+
getCargoArtifactDir,
|
|
11
|
+
parseSizeToBytes,
|
|
12
|
+
resolveManagedTargetDir,
|
|
13
|
+
writeTargetCacheMarker,
|
|
14
|
+
} from '../mod_cargo/index.js';
|
|
15
|
+
import { ElfInspector, ProvenanceStamper, type ITsrustBuildInfo } from '../mod_elf/index.js';
|
|
7
16
|
import { FsHelpers } from '../mod_fs/index.js';
|
|
8
17
|
import { ToolchainManager } from '../mod_toolchain/index.js';
|
|
18
|
+
import { commitinfo } from '../00_commitinfo_data.js';
|
|
9
19
|
|
|
10
20
|
/** Maps friendly target names to Rust target triples */
|
|
11
21
|
const targetAliasMap: Record<string, string> = {
|
|
@@ -48,6 +58,8 @@ interface ITsrustConfig {
|
|
|
48
58
|
static?: boolean;
|
|
49
59
|
rustflags?: string[];
|
|
50
60
|
remapLocalPaths?: boolean;
|
|
61
|
+
targetDir?: string;
|
|
62
|
+
pruneAfterBuild?: boolean;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
/** crt-static injection only applies to glibc targets; musl is static by default. */
|
|
@@ -75,6 +87,44 @@ export class TsRustCli {
|
|
|
75
87
|
private registerCommands(): void {
|
|
76
88
|
this.registerStandardCommand();
|
|
77
89
|
this.registerCleanCommand();
|
|
90
|
+
this.registerPruneCommand();
|
|
91
|
+
this.registerInspectCommand();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Collects build provenance once per run: consuming project identity,
|
|
96
|
+
* git commit, and the tsrust version doing the build. Tolerant of
|
|
97
|
+
* non-git checkouts and missing package metadata.
|
|
98
|
+
*/
|
|
99
|
+
private async collectBuildProvenance(): Promise<Omit<ITsrustBuildInfo, 'binary' | 'target'>> {
|
|
100
|
+
let projectName = 'unknown';
|
|
101
|
+
let projectVersion = 'unknown';
|
|
102
|
+
try {
|
|
103
|
+
const packageJson = JSON.parse(
|
|
104
|
+
fs.readFileSync(path.join(this.cwd, 'package.json'), 'utf8'),
|
|
105
|
+
);
|
|
106
|
+
projectName = packageJson.name || projectName;
|
|
107
|
+
projectVersion = packageJson.version || projectVersion;
|
|
108
|
+
} catch {
|
|
109
|
+
// package.json optional for pure Rust workspaces
|
|
110
|
+
}
|
|
111
|
+
let gitCommit = 'unknown';
|
|
112
|
+
try {
|
|
113
|
+
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
|
|
114
|
+
const result = await shell.execSilent(`git -C ${JSON.stringify(this.cwd)} rev-parse HEAD`);
|
|
115
|
+
if (result.exitCode === 0) {
|
|
116
|
+
gitCommit = result.stdout.trim();
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// not a git checkout
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
projectName,
|
|
123
|
+
projectVersion,
|
|
124
|
+
gitCommit,
|
|
125
|
+
builtAt: new Date().toISOString(),
|
|
126
|
+
tsrustVersion: commitinfo.version,
|
|
127
|
+
};
|
|
78
128
|
}
|
|
79
129
|
|
|
80
130
|
/**
|
|
@@ -135,6 +185,8 @@ export class TsRustCli {
|
|
|
135
185
|
const shouldClean = !!(argvArg as any).clean;
|
|
136
186
|
const distDir = path.join(this.cwd, 'dist_rust');
|
|
137
187
|
const profile = isDebug ? 'debug' : 'release';
|
|
188
|
+
const managedTargetDir = resolveManagedTargetDir(this.cwd, this.config.targetDir);
|
|
189
|
+
await writeTargetCacheMarker(managedTargetDir);
|
|
138
190
|
|
|
139
191
|
// Parse --target flag (can appear multiple times), fall back to smartconfig.json config
|
|
140
192
|
const cliTargets = (argvArg as any).target;
|
|
@@ -152,6 +204,8 @@ export class TsRustCli {
|
|
|
152
204
|
console.log(`Using additional Rust flags: ${rustflags.join(' ')}`);
|
|
153
205
|
}
|
|
154
206
|
|
|
207
|
+
const buildProvenance = await this.collectBuildProvenance();
|
|
208
|
+
|
|
155
209
|
if (targets.length > 0) {
|
|
156
210
|
// Cross-compilation mode
|
|
157
211
|
const resolvedTargets = targets.map((t: string) => ({
|
|
@@ -175,6 +229,7 @@ export class TsRustCli {
|
|
|
175
229
|
target: triple,
|
|
176
230
|
crtStatic: useStatic && wantsCrtStatic(triple),
|
|
177
231
|
rustflags,
|
|
232
|
+
targetDir: managedTargetDir,
|
|
178
233
|
});
|
|
179
234
|
|
|
180
235
|
if (!buildResult.success) {
|
|
@@ -182,8 +237,7 @@ export class TsRustCli {
|
|
|
182
237
|
process.exit(1);
|
|
183
238
|
}
|
|
184
239
|
|
|
185
|
-
|
|
186
|
-
const targetDir = path.join(rustDir, 'target', triple, profile);
|
|
240
|
+
const targetDir = getCargoArtifactDir(managedTargetDir, profile, triple);
|
|
187
241
|
|
|
188
242
|
for (const binName of workspaceInfo.binTargets) {
|
|
189
243
|
const srcBinary = path.join(targetDir, binName);
|
|
@@ -208,6 +262,13 @@ export class TsRustCli {
|
|
|
208
262
|
}
|
|
209
263
|
console.log(`Verified statically linked: dist_rust/${destName}`);
|
|
210
264
|
}
|
|
265
|
+
|
|
266
|
+
await ProvenanceStamper.stamp(destBinary, {
|
|
267
|
+
...buildProvenance,
|
|
268
|
+
binary: binName,
|
|
269
|
+
target: friendly,
|
|
270
|
+
});
|
|
271
|
+
console.log(`Stamped provenance: ${buildProvenance.projectName}@${buildProvenance.projectVersion} ${buildProvenance.gitCommit.slice(0, 12)} (${friendly})`);
|
|
211
272
|
}
|
|
212
273
|
|
|
213
274
|
// Only clean on first iteration
|
|
@@ -234,6 +295,7 @@ export class TsRustCli {
|
|
|
234
295
|
target: nativeTriple,
|
|
235
296
|
crtStatic: !!nativeTriple && wantsCrtStatic(nativeTriple),
|
|
236
297
|
rustflags,
|
|
298
|
+
targetDir: managedTargetDir,
|
|
237
299
|
});
|
|
238
300
|
|
|
239
301
|
if (!buildResult.success) {
|
|
@@ -241,9 +303,7 @@ export class TsRustCli {
|
|
|
241
303
|
process.exit(1);
|
|
242
304
|
}
|
|
243
305
|
|
|
244
|
-
const targetDir = nativeTriple
|
|
245
|
-
? path.join(rustDir, 'target', nativeTriple, profile)
|
|
246
|
-
: path.join(rustDir, 'target', profile);
|
|
306
|
+
const targetDir = getCargoArtifactDir(managedTargetDir, profile, nativeTriple);
|
|
247
307
|
|
|
248
308
|
await FsHelpers.ensureEmptyDir(distDir);
|
|
249
309
|
|
|
@@ -269,14 +329,36 @@ export class TsRustCli {
|
|
|
269
329
|
}
|
|
270
330
|
console.log(`Verified statically linked: dist_rust/${binName}`);
|
|
271
331
|
}
|
|
332
|
+
|
|
333
|
+
await ProvenanceStamper.stamp(destBinary, {
|
|
334
|
+
...buildProvenance,
|
|
335
|
+
binary: binName,
|
|
336
|
+
target: nativeTriple || 'native',
|
|
337
|
+
});
|
|
338
|
+
console.log(`Stamped provenance: ${buildProvenance.projectName}@${buildProvenance.projectVersion} ${buildProvenance.gitCommit.slice(0, 12)} (${nativeTriple || 'native'})`);
|
|
272
339
|
}
|
|
273
340
|
}
|
|
274
341
|
|
|
342
|
+
if (this.shouldPruneAfterBuild()) {
|
|
343
|
+
const plan = await createTsrustPrunePlan({
|
|
344
|
+
workspace: this.cwd,
|
|
345
|
+
targetDir: managedTargetDir,
|
|
346
|
+
days: 0,
|
|
347
|
+
});
|
|
348
|
+
await applyTsrustPrunePlan(plan);
|
|
349
|
+
console.log('Pruned marked tsrust target cache after successful build.');
|
|
350
|
+
}
|
|
351
|
+
|
|
275
352
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
276
353
|
console.log(`\nDone in ${elapsed}s`);
|
|
277
354
|
});
|
|
278
355
|
}
|
|
279
356
|
|
|
357
|
+
private shouldPruneAfterBuild(): boolean {
|
|
358
|
+
const normalized = process.env.TSRUST_PRUNE_AFTER_BUILD?.toLowerCase();
|
|
359
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'y' || this.config.pruneAfterBuild === true;
|
|
360
|
+
}
|
|
361
|
+
|
|
280
362
|
private buildRustflags(rustDir: string): string[] {
|
|
281
363
|
const rustflags = [...(this.config.rustflags || [])];
|
|
282
364
|
if (this.config.remapLocalPaths) {
|
|
@@ -295,6 +377,29 @@ export class TsRustCli {
|
|
|
295
377
|
return rustflags;
|
|
296
378
|
}
|
|
297
379
|
|
|
380
|
+
/**
|
|
381
|
+
* `tsrust inspect <binary>` — prints the embedded build provenance so any
|
|
382
|
+
* environment can answer "what is this binary built from" definitively.
|
|
383
|
+
*/
|
|
384
|
+
private registerInspectCommand(): void {
|
|
385
|
+
this.cli.addCommand('inspect').subscribe(async (argvArg) => {
|
|
386
|
+
const binaryPath = (argvArg as any)._?.[1];
|
|
387
|
+
if (!binaryPath) {
|
|
388
|
+
console.error('Usage: tsrust inspect <path-to-binary>');
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
const resolvedPath = path.isAbsolute(binaryPath)
|
|
392
|
+
? binaryPath
|
|
393
|
+
: path.join(this.cwd, binaryPath);
|
|
394
|
+
const info = await ProvenanceStamper.read(resolvedPath);
|
|
395
|
+
if (!info) {
|
|
396
|
+
console.error(`No tsrust build provenance found in ${resolvedPath} (built before tsrust 1.7.0?).`);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
console.log(JSON.stringify(info, null, 2));
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
298
403
|
private registerCleanCommand(): void {
|
|
299
404
|
this.cli.addCommand('clean').subscribe(async (_argvArg) => {
|
|
300
405
|
// Clean cargo build
|
|
@@ -303,7 +408,7 @@ export class TsRustCli {
|
|
|
303
408
|
const envPrefix = await this.resolveToolchain();
|
|
304
409
|
console.log('Running cargo clean...');
|
|
305
410
|
const runner = new CargoRunner(rustDir, envPrefix);
|
|
306
|
-
await runner.clean();
|
|
411
|
+
await runner.clean({ targetDir: resolveManagedTargetDir(this.cwd, this.config.targetDir) });
|
|
307
412
|
console.log('Cargo clean complete.');
|
|
308
413
|
}
|
|
309
414
|
|
|
@@ -318,6 +423,36 @@ export class TsRustCli {
|
|
|
318
423
|
});
|
|
319
424
|
}
|
|
320
425
|
|
|
426
|
+
private registerPruneCommand(): void {
|
|
427
|
+
this.cli.addCommand('prune').subscribe(async (argvArg) => {
|
|
428
|
+
const workspace = ((argvArg as any).workspace as string | undefined) || this.cwd;
|
|
429
|
+
const daysArg = (argvArg as any).days;
|
|
430
|
+
const days = daysArg === undefined ? 14 : Number(daysArg);
|
|
431
|
+
const maxSizeBytes = parseSizeToBytes((argvArg as any).maxSize as string | undefined);
|
|
432
|
+
const apply = !!(argvArg as any).apply;
|
|
433
|
+
const plan = await createTsrustPrunePlan({
|
|
434
|
+
workspace,
|
|
435
|
+
targetDir: resolveManagedTargetDir(workspace, this.config.targetDir),
|
|
436
|
+
days: Number.isFinite(days) ? days : 14,
|
|
437
|
+
maxSizeBytes,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
console.log(apply ? 'tsrust prune apply plan:' : 'tsrust prune dry-run plan:');
|
|
441
|
+
if (plan.length === 0) {
|
|
442
|
+
console.log('No Rust target caches found.');
|
|
443
|
+
}
|
|
444
|
+
for (const candidate of plan) {
|
|
445
|
+
const action = candidate.shouldPrune ? (apply ? 'remove' : 'would remove') : 'report';
|
|
446
|
+
console.log(`${action}: ${candidate.path} (${FsHelpers.formatFileSize(candidate.bytes)}) - ${candidate.reason}`);
|
|
447
|
+
}
|
|
448
|
+
if (apply) {
|
|
449
|
+
await applyTsrustPrunePlan(plan);
|
|
450
|
+
} else {
|
|
451
|
+
console.log('Dry-run only. Re-run with --apply to remove marked tsrust target caches.');
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
321
456
|
private async detectRustDir(): Promise<string | null> {
|
|
322
457
|
// Check rust/ first
|
|
323
458
|
const rustDir = path.join(this.cwd, 'rust');
|
|
Binary file
|
package/ts/mod_elf/index.ts
CHANGED