@git.zone/tsrust 1.8.0 → 1.9.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.
Files changed (38) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/index.d.ts +2 -0
  3. package/dist_ts/index.js +3 -1
  4. package/dist_ts/mod_artifact/classes.artifactassembler.d.ts +48 -0
  5. package/dist_ts/mod_artifact/classes.artifactassembler.js +842 -0
  6. package/dist_ts/mod_artifact/index.d.ts +1 -0
  7. package/dist_ts/mod_artifact/index.js +2 -0
  8. package/dist_ts/mod_cli/classes.tsrustcli.d.ts +5 -2
  9. package/dist_ts/mod_cli/classes.tsrustcli.js +110 -75
  10. package/dist_ts/mod_cli/helpers.targets.d.ts +20 -0
  11. package/dist_ts/mod_cli/helpers.targets.js +106 -0
  12. package/dist_ts/mod_cli/index.d.ts +1 -0
  13. package/dist_ts/mod_cli/index.js +2 -1
  14. package/dist_ts/mod_elf/classes.provenance.d.ts +2 -1
  15. package/dist_ts/mod_elf/classes.provenance.js +0 -0
  16. package/dist_ts/mod_provenance/classes.gitstate.d.ts +7 -0
  17. package/dist_ts/mod_provenance/classes.gitstate.js +34 -0
  18. package/dist_ts/mod_provenance/classes.provenancestore.d.ts +11 -0
  19. package/dist_ts/mod_provenance/classes.provenancestore.js +184 -0
  20. package/dist_ts/mod_provenance/helpers.owneridentity.d.ts +2 -0
  21. package/dist_ts/mod_provenance/helpers.owneridentity.js +57 -0
  22. package/dist_ts/mod_provenance/index.d.ts +3 -0
  23. package/dist_ts/mod_provenance/index.js +4 -0
  24. package/package.json +3 -3
  25. package/readme.hints.md +6 -3
  26. package/readme.md +77 -5
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/index.ts +2 -0
  29. package/ts/mod_artifact/classes.artifactassembler.ts +1028 -0
  30. package/ts/mod_artifact/index.ts +6 -0
  31. package/ts/mod_cli/classes.tsrustcli.ts +162 -95
  32. package/ts/mod_cli/helpers.targets.ts +141 -0
  33. package/ts/mod_cli/index.ts +10 -0
  34. package/ts/mod_elf/classes.provenance.ts +0 -0
  35. package/ts/mod_provenance/classes.gitstate.ts +49 -0
  36. package/ts/mod_provenance/classes.provenancestore.ts +194 -0
  37. package/ts/mod_provenance/helpers.owneridentity.ts +57 -0
  38. package/ts/mod_provenance/index.ts +10 -0
@@ -0,0 +1,6 @@
1
+ export {
2
+ ArtifactAssembler,
3
+ type IArtifactAssemblerOptions,
4
+ type IArtifactAssemblyResult,
5
+ type IArtifactTarget,
6
+ } from './classes.artifactassembler.js';
@@ -2,6 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import * as os from 'os';
4
4
  import * as plugins from '../plugins.js';
5
+ import { ArtifactAssembler } from '../mod_artifact/index.js';
5
6
  import { CargoConfig } from '../mod_cargo/index.js';
6
7
  import { CargoRunner } from '../mod_cargo/index.js';
7
8
  import {
@@ -12,56 +13,22 @@ import {
12
13
  resolveManagedTargetDir,
13
14
  writeTargetCacheMarker,
14
15
  } from '../mod_cargo/index.js';
15
- import { ElfInspector, ProvenanceStamper, type ITsrustBuildInfo } from '../mod_elf/index.js';
16
+ import { ElfInspector, type ITsrustBuildInfo } from '../mod_elf/index.js';
16
17
  import { FsHelpers } from '../mod_fs/index.js';
18
+ import {
19
+ assertGitSnapshotUnchanged,
20
+ captureGitSnapshot,
21
+ ProvenanceStore,
22
+ type IGitSnapshot,
23
+ } from '../mod_provenance/index.js';
17
24
  import { ToolchainManager } from '../mod_toolchain/index.js';
18
25
  import { commitinfo } from '../00_commitinfo_data.js';
19
-
20
- /** Maps friendly target names to Rust target triples */
21
- const targetAliasMap: Record<string, string> = {
22
- linux_amd64: 'x86_64-unknown-linux-gnu',
23
- linux_arm64: 'aarch64-unknown-linux-gnu',
24
- linux_amd64_musl: 'x86_64-unknown-linux-musl',
25
- linux_arm64_musl: 'aarch64-unknown-linux-musl',
26
- macos_amd64: 'x86_64-apple-darwin',
27
- macos_arm64: 'aarch64-apple-darwin',
28
- };
29
-
30
- /** Reverse map: Rust triple → friendly name */
31
- const tripleToFriendlyMap: Record<string, string> = {};
32
- for (const [friendly, triple] of Object.entries(targetAliasMap)) {
33
- tripleToFriendlyMap[triple] = friendly;
34
- }
35
-
36
- /**
37
- * Resolves a user-provided target name to a Rust target triple.
38
- * Accepts both friendly names (linux_arm64) and raw triples (aarch64-unknown-linux-gnu).
39
- */
40
- function resolveTargetAlias(name: string): string {
41
- return targetAliasMap[name] || name;
42
- }
43
-
44
- /**
45
- * Derives a friendly name from a Rust target triple for use in output filenames.
46
- * Falls back to the raw triple with dashes replaced by underscores.
47
- */
48
- function friendlyName(triple: string): string {
49
- if (tripleToFriendlyMap[triple]) {
50
- return tripleToFriendlyMap[triple];
51
- }
52
- // Derive from triple: e.g. x86_64-unknown-linux-gnu → x86_64_unknown_linux_gnu
53
- return triple.replace(/-/g, '_');
54
- }
55
-
56
- interface ITsrustConfig {
57
- targets?: string[];
58
- locked?: boolean;
59
- static?: boolean;
60
- rustflags?: string[];
61
- remapLocalPaths?: boolean;
62
- targetDir?: string;
63
- pruneAfterBuild?: boolean;
64
- }
26
+ import {
27
+ configuredAssemblyTargets,
28
+ normalizeTargets,
29
+ resolveBuildTargets,
30
+ type ITsrustConfig,
31
+ } from './helpers.targets.js';
65
32
 
66
33
  /** crt-static injection only applies to glibc targets; musl is static by default. */
67
34
  function wantsCrtStatic(triple: string): boolean {
@@ -89,15 +56,11 @@ export class TsRustCli {
89
56
  this.registerStandardCommand();
90
57
  this.registerCleanCommand();
91
58
  this.registerPruneCommand();
59
+ this.registerAssembleCommand();
92
60
  this.registerInspectCommand();
93
61
  }
94
62
 
95
- /**
96
- * Collects build provenance once per run: consuming project identity,
97
- * git commit, and the tsrust version doing the build. Tolerant of
98
- * non-git checkouts and missing package metadata.
99
- */
100
- private async collectBuildProvenance(): Promise<Omit<ITsrustBuildInfo, 'binary' | 'target'>> {
63
+ private readProjectIdentity(): { projectName: string; projectVersion: string } {
101
64
  let projectName = 'unknown';
102
65
  let projectVersion = 'unknown';
103
66
  try {
@@ -109,25 +72,40 @@ export class TsRustCli {
109
72
  } catch {
110
73
  // package.json optional for pure Rust workspaces
111
74
  }
112
- let gitCommit = 'unknown';
113
- try {
114
- const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
115
- const result = await shell.execSilent(`git -C ${JSON.stringify(this.cwd)} rev-parse HEAD`);
116
- if (result.exitCode === 0) {
117
- gitCommit = result.stdout.trim();
118
- }
119
- } catch {
120
- // not a git checkout
121
- }
75
+ return { projectName, projectVersion };
76
+ }
77
+
78
+ /**
79
+ * Collects build provenance for one Cargo invocation: consuming project identity,
80
+ * git commit, and the tsrust version doing the build. Tolerant of
81
+ * non-git checkouts and missing package metadata.
82
+ */
83
+ private collectBuildProvenance(
84
+ gitSnapshotArg: IGitSnapshot,
85
+ ): Omit<ITsrustBuildInfo, 'binary' | 'target'> {
86
+ const { projectName, projectVersion } = this.readProjectIdentity();
122
87
  return {
123
88
  projectName,
124
89
  projectVersion,
125
- gitCommit,
90
+ gitCommit: gitSnapshotArg.commit,
91
+ gitDirty: gitSnapshotArg.available ? gitSnapshotArg.status.length > 0 : undefined,
126
92
  builtAt: new Date().toISOString(),
127
93
  tsrustVersion: commitinfo.version,
128
94
  };
129
95
  }
130
96
 
97
+ private assertProjectIdentityUnchanged(
98
+ beforeArg: Omit<ITsrustBuildInfo, 'binary' | 'target'>,
99
+ afterArg: Omit<ITsrustBuildInfo, 'binary' | 'target'>,
100
+ ): void {
101
+ if (
102
+ beforeArg.projectName !== afterArg.projectName ||
103
+ beforeArg.projectVersion !== afterArg.projectVersion
104
+ ) {
105
+ throw new Error('Project package identity changed while Cargo was building');
106
+ }
107
+ }
108
+
131
109
  /**
132
110
  * Resolves the Rust toolchain to use. Tries system cargo first,
133
111
  * then falls back to a bundled toolchain at /tmp/tsrust_toolchain/.
@@ -183,17 +161,19 @@ export class TsRustCli {
183
161
  console.log(`Binary targets: ${workspaceInfo.binTargets.join(', ')}`);
184
162
 
185
163
  const isDebug = !!(argvArg as any).debug;
186
- const shouldClean = !!(argvArg as any).clean;
164
+ let shouldClean = !!(argvArg as any).clean;
187
165
  const distDir = path.join(this.cwd, 'dist_rust');
188
166
  const profile = isDebug ? 'debug' : 'release';
189
167
  const managedTargetDir = resolveManagedTargetDir(this.cwd, this.config.targetDir);
190
168
  await writeTargetCacheMarker(managedTargetDir);
191
169
 
192
- // Parse --target flag (can appear multiple times), fall back to smartconfig.json config
170
+ // CLI targets override host-specific and legacy configured targets.
193
171
  const cliTargets = (argvArg as any).target;
194
- const targets: string[] = cliTargets
172
+ const cliTargetList: string[] | undefined = cliTargets
195
173
  ? (Array.isArray(cliTargets) ? cliTargets : [cliTargets])
196
- : this.config.targets || [];
174
+ : undefined;
175
+ const hostTriple = new ToolchainManager().getHostTriple();
176
+ const resolvedTargets = resolveBuildTargets(this.config, hostTriple, cliTargetList);
197
177
 
198
178
  const useStatic = !!(argvArg as any).static || !!this.config.static;
199
179
  if (useStatic) {
@@ -205,25 +185,24 @@ export class TsRustCli {
205
185
  console.log(`Using additional Rust flags: ${rustflags.join(' ')}`);
206
186
  }
207
187
 
208
- const buildProvenance = await this.collectBuildProvenance();
209
-
210
- if (targets.length > 0) {
188
+ if (resolvedTargets.length > 0) {
211
189
  // Cross-compilation mode
212
- const resolvedTargets = targets.map((t: string) => ({
213
- triple: resolveTargetAlias(t),
214
- friendly: friendlyName(resolveTargetAlias(t)),
215
- }));
216
-
217
- console.log(`Cross-compiling for: ${resolvedTargets.map((t) => `${t.friendly} (${t.triple})`).join(', ')}`);
190
+ console.log(
191
+ `Cross-compiling for: ${resolvedTargets.map((target) => `${target.friendly} (${target.triple})`).join(', ')}`,
192
+ );
218
193
 
219
194
  await FsHelpers.ensureEmptyDir(distDir);
220
195
 
221
196
  for (const { triple, friendly } of resolvedTargets) {
222
197
  console.log(`\n--- Building for ${friendly} (${triple}) ---`);
223
198
  if (useStatic && !isLinuxTriple(triple)) {
224
- console.log(`Note: static linking is not applicable for ${triple}; building with default linkage.`);
199
+ console.log(
200
+ `Note: static linking is not applicable for ${triple}; building with default linkage.`,
201
+ );
225
202
  }
226
203
  const cargoRunner = new CargoRunner(rustDir, envPrefix);
204
+ const gitBefore = await captureGitSnapshot(this.cwd);
205
+ const provenanceBefore = this.collectBuildProvenance(gitBefore);
227
206
  const buildResult = await cargoRunner.build({
228
207
  debug: isDebug,
229
208
  clean: shouldClean,
@@ -238,6 +217,10 @@ export class TsRustCli {
238
217
  console.error(`Build failed for target ${triple} with exit code ${buildResult.exitCode}`);
239
218
  process.exit(1);
240
219
  }
220
+ const gitAfter = await captureGitSnapshot(this.cwd);
221
+ assertGitSnapshotUnchanged(gitBefore, gitAfter);
222
+ const provenanceAfter = this.collectBuildProvenance(gitAfter);
223
+ this.assertProjectIdentityUnchanged(provenanceBefore, provenanceAfter);
241
224
 
242
225
  const targetDir = getCargoArtifactDir(managedTargetDir, profile, triple);
243
226
 
@@ -255,27 +238,33 @@ export class TsRustCli {
255
238
  await FsHelpers.makeExecutable(destBinary);
256
239
 
257
240
  const size = await FsHelpers.getFileSize(destBinary);
258
- console.log(`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${destName}`);
241
+ console.log(
242
+ `Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${destName}`,
243
+ );
259
244
 
260
245
  if (useStatic && isLinuxTriple(triple)) {
261
246
  if (!(await ElfInspector.isStaticElf(destBinary))) {
262
- console.error(`Error: ${destName} is not statically linked (PT_INTERP present) although static linking was requested.`);
247
+ console.error(
248
+ `Error: ${destName} is not statically linked (PT_INTERP present) although static linking was requested.`,
249
+ );
263
250
  process.exit(1);
264
251
  }
265
252
  console.log(`Verified statically linked: dist_rust/${destName}`);
266
253
  }
267
254
 
268
- await ProvenanceStamper.stamp(destBinary, {
269
- ...buildProvenance,
255
+ await ProvenanceStore.write(destBinary, {
256
+ ...provenanceBefore,
270
257
  binary: binName,
271
258
  target: friendly,
272
259
  });
273
- console.log(`Stamped provenance: ${buildProvenance.projectName}@${buildProvenance.projectVersion} ${buildProvenance.gitCommit.slice(0, 12)} (${friendly})`);
260
+ console.log(
261
+ `Wrote provenance: ${provenanceBefore.projectName}@${provenanceBefore.projectVersion} ${provenanceBefore.gitCommit.slice(0, 12)} (${friendly})`,
262
+ );
274
263
  }
275
264
 
276
265
  // Only clean on first iteration
277
266
  if (shouldClean) {
278
- (argvArg as any).clean = false;
267
+ shouldClean = false;
279
268
  }
280
269
  }
281
270
  } else {
@@ -284,13 +273,17 @@ export class TsRustCli {
284
273
  // (proc-macros cannot build with +crt-static on linux-gnu).
285
274
  let nativeTriple: string | undefined;
286
275
  if (useStatic) {
287
- nativeTriple = new ToolchainManager().getHostTriple();
276
+ nativeTriple = hostTriple;
288
277
  if (!isLinuxTriple(nativeTriple)) {
289
- console.log(`Note: static linking is not applicable for ${nativeTriple}; building with default linkage.`);
278
+ console.log(
279
+ `Note: static linking is not applicable for ${nativeTriple}; building with default linkage.`,
280
+ );
290
281
  }
291
282
  }
292
283
 
293
284
  const cargoRunner = new CargoRunner(rustDir, envPrefix);
285
+ const gitBefore = await captureGitSnapshot(this.cwd);
286
+ const provenanceBefore = this.collectBuildProvenance(gitBefore);
294
287
  const buildResult = await cargoRunner.build({
295
288
  debug: isDebug,
296
289
  clean: shouldClean,
@@ -305,6 +298,10 @@ export class TsRustCli {
305
298
  console.error(`Build failed with exit code ${buildResult.exitCode}`);
306
299
  process.exit(1);
307
300
  }
301
+ const gitAfter = await captureGitSnapshot(this.cwd);
302
+ assertGitSnapshotUnchanged(gitBefore, gitAfter);
303
+ const provenanceAfter = this.collectBuildProvenance(gitAfter);
304
+ this.assertProjectIdentityUnchanged(provenanceBefore, provenanceAfter);
308
305
 
309
306
  const targetDir = getCargoArtifactDir(managedTargetDir, profile, nativeTriple);
310
307
 
@@ -323,22 +320,28 @@ export class TsRustCli {
323
320
  await FsHelpers.makeExecutable(destBinary);
324
321
 
325
322
  const size = await FsHelpers.getFileSize(destBinary);
326
- console.log(`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`);
323
+ console.log(
324
+ `Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`,
325
+ );
327
326
 
328
327
  if (useStatic && nativeTriple && isLinuxTriple(nativeTriple)) {
329
328
  if (!(await ElfInspector.isStaticElf(destBinary))) {
330
- console.error(`Error: ${binName} is not statically linked (PT_INTERP present) although static linking was requested.`);
329
+ console.error(
330
+ `Error: ${binName} is not statically linked (PT_INTERP present) although static linking was requested.`,
331
+ );
331
332
  process.exit(1);
332
333
  }
333
334
  console.log(`Verified statically linked: dist_rust/${binName}`);
334
335
  }
335
336
 
336
- await ProvenanceStamper.stamp(destBinary, {
337
- ...buildProvenance,
337
+ await ProvenanceStore.write(destBinary, {
338
+ ...provenanceBefore,
338
339
  binary: binName,
339
340
  target: nativeTriple || 'native',
340
341
  });
341
- console.log(`Stamped provenance: ${buildProvenance.projectName}@${buildProvenance.projectVersion} ${buildProvenance.gitCommit.slice(0, 12)} (${nativeTriple || 'native'})`);
342
+ console.log(
343
+ `Wrote provenance: ${provenanceBefore.projectName}@${provenanceBefore.projectVersion} ${provenanceBefore.gitCommit.slice(0, 12)} (${nativeTriple || 'native'})`,
344
+ );
342
345
  }
343
346
  }
344
347
 
@@ -359,7 +362,13 @@ export class TsRustCli {
359
362
 
360
363
  private shouldPruneAfterBuild(): boolean {
361
364
  const normalized = process.env.TSRUST_PRUNE_AFTER_BUILD?.toLowerCase();
362
- return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'y' || this.config.pruneAfterBuild === true;
365
+ return (
366
+ normalized === '1' ||
367
+ normalized === 'true' ||
368
+ normalized === 'yes' ||
369
+ normalized === 'y' ||
370
+ this.config.pruneAfterBuild === true
371
+ );
363
372
  }
364
373
 
365
374
  private buildRustflags(rustDir: string): string[] {
@@ -381,7 +390,7 @@ export class TsRustCli {
381
390
  }
382
391
 
383
392
  /**
384
- * `tsrust inspect <binary>` — prints the embedded build provenance so any
393
+ * `tsrust inspect <binary>` — verifies and prints build provenance so any
385
394
  * environment can answer "what is this binary built from" definitively.
386
395
  */
387
396
  private registerInspectCommand(): void {
@@ -394,15 +403,73 @@ export class TsRustCli {
394
403
  const resolvedPath = path.isAbsolute(binaryPath)
395
404
  ? binaryPath
396
405
  : path.join(this.cwd, binaryPath);
397
- const info = await ProvenanceStamper.read(resolvedPath);
406
+ const info = await ProvenanceStore.read(resolvedPath);
398
407
  if (!info) {
399
- console.error(`No tsrust build provenance found in ${resolvedPath} (built before tsrust 1.7.0?).`);
408
+ console.error(`No tsrust build provenance found in ${resolvedPath}.`);
400
409
  process.exit(1);
401
410
  }
402
411
  console.log(JSON.stringify(info, null, 2));
403
412
  });
404
413
  }
405
414
 
415
+ private registerAssembleCommand(): void {
416
+ this.cli.addCommand('assemble').subscribe(async (argvArg) => {
417
+ const sourceDirectories = ((argvArg as any)._ as unknown[])
418
+ .slice(1)
419
+ .map((sourceDirectory) => String(sourceDirectory));
420
+ if (sourceDirectories.length === 0) {
421
+ throw new Error('Usage: tsrust assemble <artifact-directory> [...]');
422
+ }
423
+
424
+ const cliTargets = (argvArg as any).target;
425
+ const cliTargetList: string[] | undefined = cliTargets
426
+ ? (Array.isArray(cliTargets) ? cliTargets : [cliTargets])
427
+ : undefined;
428
+ const expectedTargets = cliTargetList
429
+ ? normalizeTargets(cliTargetList, '--target')
430
+ : configuredAssemblyTargets(this.config);
431
+ if (expectedTargets.length === 0) {
432
+ throw new Error(
433
+ 'Artifact assembly requires configured targets or at least one --target',
434
+ );
435
+ }
436
+
437
+ const rustDir = await this.detectRustDir();
438
+ if (!rustDir) {
439
+ throw new Error('No rust/ or ts_rust/ directory found with a Cargo.toml');
440
+ }
441
+ const workspaceInfo = await new CargoConfig(rustDir).parse();
442
+ if (workspaceInfo.binTargets.length === 0) {
443
+ throw new Error('No binary targets found in Cargo.toml');
444
+ }
445
+
446
+ const gitSnapshot = await captureGitSnapshot(this.cwd);
447
+ if (!gitSnapshot.available || gitSnapshot.commit === 'unknown') {
448
+ throw new Error('Artifact assembly requires a Git checkout');
449
+ }
450
+ if (gitSnapshot.status.length > 0) {
451
+ throw new Error('Artifact assembly requires a clean Git worktree');
452
+ }
453
+ const { projectName, projectVersion } = this.readProjectIdentity();
454
+ const result = await new ArtifactAssembler({
455
+ workspace: this.cwd,
456
+ sourceDirectories,
457
+ expectedTargets,
458
+ expectedBinaries: workspaceInfo.binTargets,
459
+ expectedProjectName: projectName,
460
+ expectedProjectVersion: projectVersion,
461
+ expectedGitCommit: gitSnapshot.commit,
462
+ expectedTsrustVersion: commitinfo.version,
463
+ }).assemble();
464
+ console.log(
465
+ `Assembled ${result.artifactCount} artifacts for ${result.targets.join(', ')} into ${result.outputDirectory}`,
466
+ );
467
+ if (result.cleanupPending) {
468
+ console.warn('Artifact assembly committed; stale transaction cleanup will retry next run.');
469
+ }
470
+ });
471
+ }
472
+
406
473
  private registerCleanCommand(): void {
407
474
  this.cli.addCommand('clean').subscribe(async (_argvArg) => {
408
475
  // Clean cargo build
@@ -0,0 +1,141 @@
1
+ export const targetAliasMap: Record<string, string> = {
2
+ linux_amd64: 'x86_64-unknown-linux-gnu',
3
+ linux_arm64: 'aarch64-unknown-linux-gnu',
4
+ linux_amd64_musl: 'x86_64-unknown-linux-musl',
5
+ linux_arm64_musl: 'aarch64-unknown-linux-musl',
6
+ macos_amd64: 'x86_64-apple-darwin',
7
+ macos_arm64: 'aarch64-apple-darwin',
8
+ };
9
+
10
+ const tripleToFriendlyMap = Object.fromEntries(
11
+ Object.entries(targetAliasMap).map(([friendly, triple]) => [triple, friendly]),
12
+ );
13
+
14
+ const supportedHostKeys = new Set([
15
+ 'linux',
16
+ 'macos',
17
+ 'linux_amd64',
18
+ 'linux_arm64',
19
+ 'macos_amd64',
20
+ 'macos_arm64',
21
+ ]);
22
+
23
+ export interface ITsrustConfig {
24
+ targets?: string[];
25
+ targetsByHost?: Record<string, string[]>;
26
+ locked?: boolean;
27
+ static?: boolean;
28
+ rustflags?: string[];
29
+ remapLocalPaths?: boolean;
30
+ targetDir?: string;
31
+ pruneAfterBuild?: boolean;
32
+ }
33
+
34
+ export interface INormalizedTarget {
35
+ triple: string;
36
+ friendly: string;
37
+ }
38
+
39
+ export function resolveTargetAlias(nameArg: string): string {
40
+ return targetAliasMap[nameArg] || nameArg;
41
+ }
42
+
43
+ export function friendlyTargetName(tripleArg: string): string {
44
+ return tripleToFriendlyMap[tripleArg] || tripleArg.replace(/-/g, '_');
45
+ }
46
+
47
+ function validateTargetName(nameArg: string): void {
48
+ if (
49
+ !nameArg ||
50
+ nameArg.length > 255 ||
51
+ !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(nameArg)
52
+ ) {
53
+ throw new Error(`Invalid Rust target name: ${JSON.stringify(nameArg)}`);
54
+ }
55
+ }
56
+
57
+ export function normalizeTargets(targetsArg: string[], labelArg: string): INormalizedTarget[] {
58
+ if (!Array.isArray(targetsArg)) {
59
+ throw new Error(`${labelArg} must be an array of Rust target names`);
60
+ }
61
+ const byTriple = new Map<string, INormalizedTarget>();
62
+ const tripleByFriendly = new Map<string, string>();
63
+ for (const target of targetsArg) {
64
+ if (typeof target !== 'string') {
65
+ throw new Error(`${labelArg} must contain only Rust target names`);
66
+ }
67
+ validateTargetName(target);
68
+ const triple = resolveTargetAlias(target);
69
+ validateTargetName(triple);
70
+ const friendly = friendlyTargetName(triple);
71
+ validateTargetName(friendly);
72
+ const collidingTriple = tripleByFriendly.get(friendly);
73
+ if (collidingTriple && collidingTriple !== triple) {
74
+ throw new Error(
75
+ `${labelArg} targets ${collidingTriple} and ${triple} share output suffix ${friendly}`,
76
+ );
77
+ }
78
+ tripleByFriendly.set(friendly, triple);
79
+ byTriple.set(triple, { triple, friendly });
80
+ }
81
+ return [...byTriple.values()];
82
+ }
83
+
84
+ function validateTargetsByHost(configArg: ITsrustConfig): void {
85
+ if (configArg.targets !== undefined) {
86
+ normalizeTargets(configArg.targets, 'targets');
87
+ }
88
+ if (configArg.targetsByHost === undefined) return;
89
+ if (
90
+ !configArg.targetsByHost ||
91
+ typeof configArg.targetsByHost !== 'object' ||
92
+ Array.isArray(configArg.targetsByHost)
93
+ ) {
94
+ throw new Error('targetsByHost must be an object keyed by supported build hosts');
95
+ }
96
+ for (const [host, targets] of Object.entries(configArg.targetsByHost)) {
97
+ if (!supportedHostKeys.has(host)) {
98
+ throw new Error(`Unsupported targetsByHost key: ${host}`);
99
+ }
100
+ if (!Array.isArray(targets) || targets.length === 0) {
101
+ throw new Error(`targetsByHost.${host} must be a non-empty target array`);
102
+ }
103
+ normalizeTargets(targets, `targetsByHost.${host}`);
104
+ }
105
+ }
106
+
107
+ function hostFamily(hostTripleArg: string): 'linux' | 'macos' {
108
+ if (hostTripleArg.includes('-linux-')) return 'linux';
109
+ if (hostTripleArg.endsWith('-apple-darwin')) return 'macos';
110
+ throw new Error(`Unsupported Rust host triple for targetsByHost: ${hostTripleArg}`);
111
+ }
112
+
113
+ export function resolveBuildTargets(
114
+ configArg: ITsrustConfig,
115
+ hostTripleArg: string,
116
+ cliTargetsArg?: string[],
117
+ ): INormalizedTarget[] {
118
+ validateTargetsByHost(configArg);
119
+ if (cliTargetsArg !== undefined) {
120
+ if (cliTargetsArg.length === 0) {
121
+ throw new Error('--target requires at least one target');
122
+ }
123
+ return normalizeTargets(cliTargetsArg, '--target');
124
+ }
125
+ const exactHost = friendlyTargetName(hostTripleArg);
126
+ const configured =
127
+ configArg.targetsByHost?.[exactHost] ||
128
+ configArg.targetsByHost?.[hostFamily(hostTripleArg)] ||
129
+ configArg.targets ||
130
+ [];
131
+ return normalizeTargets(configured, 'targets');
132
+ }
133
+
134
+ export function configuredAssemblyTargets(configArg: ITsrustConfig): INormalizedTarget[] {
135
+ validateTargetsByHost(configArg);
136
+ const configured = [
137
+ ...(configArg.targets || []),
138
+ ...Object.values(configArg.targetsByHost || {}).flat(),
139
+ ];
140
+ return normalizeTargets(configured, 'configured assembly targets');
141
+ }
@@ -1 +1,11 @@
1
1
  export { TsRustCli, runCli } from './classes.tsrustcli.js';
2
+ export {
3
+ configuredAssemblyTargets,
4
+ friendlyTargetName,
5
+ normalizeTargets,
6
+ resolveBuildTargets,
7
+ resolveTargetAlias,
8
+ targetAliasMap,
9
+ type INormalizedTarget,
10
+ type ITsrustConfig,
11
+ } from './helpers.targets.js';
Binary file
@@ -0,0 +1,49 @@
1
+ import * as childProcess from 'child_process';
2
+ import * as util from 'util';
3
+
4
+ const execFile = util.promisify(childProcess.execFile);
5
+
6
+ export interface IGitSnapshot {
7
+ available: boolean;
8
+ commit: string;
9
+ status: string;
10
+ }
11
+
12
+ export async function captureGitSnapshot(cwdArg: string): Promise<IGitSnapshot> {
13
+ let commit: string;
14
+ try {
15
+ const result = await execFile('git', ['-C', cwdArg, 'rev-parse', 'HEAD'], {
16
+ encoding: 'utf8',
17
+ });
18
+ commit = result.stdout.trim();
19
+ } catch {
20
+ return { available: false, commit: 'unknown', status: '' };
21
+ }
22
+ try {
23
+ const status = await execFile(
24
+ 'git',
25
+ ['-C', cwdArg, 'status', '--porcelain=v1', '--untracked-files=all'],
26
+ { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 },
27
+ );
28
+ return {
29
+ available: true,
30
+ commit,
31
+ status: status.stdout,
32
+ };
33
+ } catch {
34
+ throw new Error('Failed to inspect Git worktree state');
35
+ }
36
+ }
37
+
38
+ export function assertGitSnapshotUnchanged(
39
+ beforeArg: IGitSnapshot,
40
+ afterArg: IGitSnapshot,
41
+ ): void {
42
+ if (
43
+ beforeArg.available !== afterArg.available ||
44
+ beforeArg.commit !== afterArg.commit ||
45
+ beforeArg.status !== afterArg.status
46
+ ) {
47
+ throw new Error('Git source state changed while Cargo was building');
48
+ }
49
+ }