@git.zone/tsrust 1.13.0 → 1.14.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 +4 -0
- package/assets/notices/gcc-gpl-3.0.txt +674 -0
- package/assets/notices/gcc-runtime-library-exception-3.1.txt +73 -0
- package/assets/notices/glibc-lgpl-2.1.txt +502 -0
- package/assets/notices/musl-copyright.txt +193 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -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.d.ts +10 -0
- package/dist_ts/mod_cli/classes.tsrustcli.js +191 -106
- 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.nativematrixbuilder.d.ts +6 -0
- package/dist_ts/mod_matrix/classes.nativematrixbuilder.js +44 -1
- package/dist_ts/mod_notices/classes.nativenoticesgenerator.d.ts +96 -0
- package/dist_ts/mod_notices/classes.nativenoticesgenerator.js +912 -0
- package/dist_ts/mod_notices/classes.noticetoolchain.d.ts +30 -0
- package/dist_ts/mod_notices/classes.noticetoolchain.js +69 -0
- package/dist_ts/mod_notices/helpers.licensetext.d.ts +12 -0
- package/dist_ts/mod_notices/helpers.licensetext.js +81 -0
- package/dist_ts/mod_notices/helpers.muslversion.d.ts +6 -0
- package/dist_ts/mod_notices/helpers.muslversion.js +47 -0
- package/dist_ts/mod_notices/helpers.noticesconfig.d.ts +51 -0
- package/dist_ts/mod_notices/helpers.noticesconfig.js +113 -0
- package/dist_ts/mod_notices/helpers.runtimetexts.d.ts +9 -0
- package/dist_ts/mod_notices/helpers.runtimetexts.js +45 -0
- package/dist_ts/mod_notices/helpers.spdx.d.ts +21 -0
- package/dist_ts/mod_notices/helpers.spdx.js +88 -0
- package/dist_ts/mod_notices/helpers.staticglibc.d.ts +26 -0
- package/dist_ts/mod_notices/helpers.staticglibc.js +45 -0
- package/dist_ts/mod_notices/index.d.ts +8 -0
- package/dist_ts/mod_notices/index.js +9 -0
- package/dist_ts/plugins.d.ts +6 -1
- package/dist_ts/plugins.js +7 -2
- package/package.json +5 -5
- package/readme.md +115 -13
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +1 -0
- 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 +227 -128
- package/ts/mod_cli/helpers.targets.ts +2 -0
- package/ts/mod_matrix/classes.nativematrixbuilder.ts +44 -0
- package/ts/mod_notices/classes.nativenoticesgenerator.ts +1151 -0
- package/ts/mod_notices/classes.noticetoolchain.ts +92 -0
- package/ts/mod_notices/helpers.licensetext.ts +111 -0
- package/ts/mod_notices/helpers.muslversion.ts +46 -0
- package/ts/mod_notices/helpers.noticesconfig.ts +190 -0
- package/ts/mod_notices/helpers.runtimetexts.ts +63 -0
- package/ts/mod_notices/helpers.spdx.ts +107 -0
- package/ts/mod_notices/helpers.staticglibc.ts +79 -0
- package/ts/mod_notices/index.ts +8 -0
- 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
|
+
}
|