@git.zone/tsrust 1.7.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 (44) 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_cargo/classes.cargorunner.d.ts +10 -8
  9. package/dist_ts/mod_cargo/classes.cargorunner.js +4 -3
  10. package/dist_ts/mod_cargo/index.d.ts +1 -1
  11. package/dist_ts/mod_cargo/index.js +2 -2
  12. package/dist_ts/mod_cli/classes.tsrustcli.d.ts +5 -2
  13. package/dist_ts/mod_cli/classes.tsrustcli.js +112 -75
  14. package/dist_ts/mod_cli/helpers.targets.d.ts +20 -0
  15. package/dist_ts/mod_cli/helpers.targets.js +106 -0
  16. package/dist_ts/mod_cli/index.d.ts +1 -0
  17. package/dist_ts/mod_cli/index.js +2 -1
  18. package/dist_ts/mod_elf/classes.provenance.d.ts +2 -1
  19. package/dist_ts/mod_elf/classes.provenance.js +0 -0
  20. package/dist_ts/mod_provenance/classes.gitstate.d.ts +7 -0
  21. package/dist_ts/mod_provenance/classes.gitstate.js +34 -0
  22. package/dist_ts/mod_provenance/classes.provenancestore.d.ts +11 -0
  23. package/dist_ts/mod_provenance/classes.provenancestore.js +184 -0
  24. package/dist_ts/mod_provenance/helpers.owneridentity.d.ts +2 -0
  25. package/dist_ts/mod_provenance/helpers.owneridentity.js +57 -0
  26. package/dist_ts/mod_provenance/index.d.ts +3 -0
  27. package/dist_ts/mod_provenance/index.js +4 -0
  28. package/package.json +7 -7
  29. package/readme.hints.md +6 -3
  30. package/readme.md +80 -5
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/index.ts +2 -0
  33. package/ts/mod_artifact/classes.artifactassembler.ts +1028 -0
  34. package/ts/mod_artifact/index.ts +6 -0
  35. package/ts/mod_cargo/classes.cargorunner.ts +14 -3
  36. package/ts/mod_cargo/index.ts +1 -1
  37. package/ts/mod_cli/classes.tsrustcli.ts +164 -94
  38. package/ts/mod_cli/helpers.targets.ts +141 -0
  39. package/ts/mod_cli/index.ts +10 -0
  40. package/ts/mod_elf/classes.provenance.ts +0 -0
  41. package/ts/mod_provenance/classes.gitstate.ts +49 -0
  42. package/ts/mod_provenance/classes.provenancestore.ts +194 -0
  43. package/ts/mod_provenance/helpers.owneridentity.ts +57 -0
  44. 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';
@@ -6,6 +6,16 @@ export interface ICargoRunResult {
6
6
  stdout: string;
7
7
  }
8
8
 
9
+ export interface ICargoBuildOptions {
10
+ debug?: boolean;
11
+ clean?: boolean;
12
+ target?: string;
13
+ crtStatic?: boolean;
14
+ rustflags?: string[];
15
+ targetDir?: string;
16
+ locked?: boolean;
17
+ }
18
+
9
19
  export class CargoRunner {
10
20
  private shell: plugins.smartshell.Smartshell;
11
21
  private rustDir: string;
@@ -29,7 +39,7 @@ export class CargoRunner {
29
39
  return result.stdout.trim();
30
40
  }
31
41
 
32
- public async build(options: { debug?: boolean; clean?: boolean; target?: string; crtStatic?: boolean; rustflags?: string[]; targetDir?: string } = {}): Promise<ICargoRunResult> {
42
+ public async build(options: ICargoBuildOptions = {}): Promise<ICargoRunResult> {
33
43
  if (options.clean) {
34
44
  console.log('Cleaning previous build...');
35
45
  await this.clean({ targetDir: options.targetDir });
@@ -48,15 +58,16 @@ export class CargoRunner {
48
58
 
49
59
  const profile = options.debug ? '' : ' --release';
50
60
  const targetFlag = options.target ? ` --target ${options.target}` : '';
61
+ const lockedFlag = options.locked === true ? ' --locked' : '';
51
62
  const rustflags = [...(options.rustflags || [])];
52
63
  if (options.crtStatic) {
53
64
  rustflags.unshift('-C', 'target-feature=+crt-static');
54
65
  }
55
66
  const rustflagsPrefix = rustflags.length ? `RUSTFLAGS=${this.shellQuote(rustflags.join(' '))} ` : '';
56
67
  const targetDirPrefix = options.targetDir ? `CARGO_TARGET_DIR=${this.shellQuote(options.targetDir)} ` : '';
57
- const command = `${this.envPrefix}cd ${this.rustDir} && ${targetDirPrefix}${rustflagsPrefix}cargo build${profile}${targetFlag}`;
68
+ const command = `${this.envPrefix}cd ${this.rustDir} && ${targetDirPrefix}${rustflagsPrefix}cargo build${profile}${targetFlag}${lockedFlag}`;
58
69
 
59
- console.log(`Running: ${targetDirPrefix}${rustflagsPrefix}cargo build${profile}${targetFlag}`);
70
+ console.log(`Running: ${targetDirPrefix}${rustflagsPrefix}cargo build${profile}${targetFlag}${lockedFlag}`);
60
71
  const result = await this.shell.exec(command);
61
72
 
62
73
  return {
@@ -1,3 +1,3 @@
1
1
  export { CargoConfig } from './classes.cargoconfig.js';
2
- export { CargoRunner } from './classes.cargorunner.js';
2
+ export * from './classes.cargorunner.js';
3
3
  export * from './classes.targetcache.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,55 +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
- static?: boolean;
59
- rustflags?: string[];
60
- remapLocalPaths?: boolean;
61
- targetDir?: string;
62
- pruneAfterBuild?: boolean;
63
- }
26
+ import {
27
+ configuredAssemblyTargets,
28
+ normalizeTargets,
29
+ resolveBuildTargets,
30
+ type ITsrustConfig,
31
+ } from './helpers.targets.js';
64
32
 
65
33
  /** crt-static injection only applies to glibc targets; musl is static by default. */
66
34
  function wantsCrtStatic(triple: string): boolean {
@@ -88,15 +56,11 @@ export class TsRustCli {
88
56
  this.registerStandardCommand();
89
57
  this.registerCleanCommand();
90
58
  this.registerPruneCommand();
59
+ this.registerAssembleCommand();
91
60
  this.registerInspectCommand();
92
61
  }
93
62
 
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'>> {
63
+ private readProjectIdentity(): { projectName: string; projectVersion: string } {
100
64
  let projectName = 'unknown';
101
65
  let projectVersion = 'unknown';
102
66
  try {
@@ -108,25 +72,40 @@ export class TsRustCli {
108
72
  } catch {
109
73
  // package.json optional for pure Rust workspaces
110
74
  }
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
- }
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();
121
87
  return {
122
88
  projectName,
123
89
  projectVersion,
124
- gitCommit,
90
+ gitCommit: gitSnapshotArg.commit,
91
+ gitDirty: gitSnapshotArg.available ? gitSnapshotArg.status.length > 0 : undefined,
125
92
  builtAt: new Date().toISOString(),
126
93
  tsrustVersion: commitinfo.version,
127
94
  };
128
95
  }
129
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
+
130
109
  /**
131
110
  * Resolves the Rust toolchain to use. Tries system cargo first,
132
111
  * then falls back to a bundled toolchain at /tmp/tsrust_toolchain/.
@@ -182,17 +161,19 @@ export class TsRustCli {
182
161
  console.log(`Binary targets: ${workspaceInfo.binTargets.join(', ')}`);
183
162
 
184
163
  const isDebug = !!(argvArg as any).debug;
185
- const shouldClean = !!(argvArg as any).clean;
164
+ let shouldClean = !!(argvArg as any).clean;
186
165
  const distDir = path.join(this.cwd, 'dist_rust');
187
166
  const profile = isDebug ? 'debug' : 'release';
188
167
  const managedTargetDir = resolveManagedTargetDir(this.cwd, this.config.targetDir);
189
168
  await writeTargetCacheMarker(managedTargetDir);
190
169
 
191
- // Parse --target flag (can appear multiple times), fall back to smartconfig.json config
170
+ // CLI targets override host-specific and legacy configured targets.
192
171
  const cliTargets = (argvArg as any).target;
193
- const targets: string[] = cliTargets
172
+ const cliTargetList: string[] | undefined = cliTargets
194
173
  ? (Array.isArray(cliTargets) ? cliTargets : [cliTargets])
195
- : this.config.targets || [];
174
+ : undefined;
175
+ const hostTriple = new ToolchainManager().getHostTriple();
176
+ const resolvedTargets = resolveBuildTargets(this.config, hostTriple, cliTargetList);
196
177
 
197
178
  const useStatic = !!(argvArg as any).static || !!this.config.static;
198
179
  if (useStatic) {
@@ -204,29 +185,29 @@ export class TsRustCli {
204
185
  console.log(`Using additional Rust flags: ${rustflags.join(' ')}`);
205
186
  }
206
187
 
207
- const buildProvenance = await this.collectBuildProvenance();
208
-
209
- if (targets.length > 0) {
188
+ if (resolvedTargets.length > 0) {
210
189
  // Cross-compilation mode
211
- const resolvedTargets = targets.map((t: string) => ({
212
- triple: resolveTargetAlias(t),
213
- friendly: friendlyName(resolveTargetAlias(t)),
214
- }));
215
-
216
- 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
+ );
217
193
 
218
194
  await FsHelpers.ensureEmptyDir(distDir);
219
195
 
220
196
  for (const { triple, friendly } of resolvedTargets) {
221
197
  console.log(`\n--- Building for ${friendly} (${triple}) ---`);
222
198
  if (useStatic && !isLinuxTriple(triple)) {
223
- 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
+ );
224
202
  }
225
203
  const cargoRunner = new CargoRunner(rustDir, envPrefix);
204
+ const gitBefore = await captureGitSnapshot(this.cwd);
205
+ const provenanceBefore = this.collectBuildProvenance(gitBefore);
226
206
  const buildResult = await cargoRunner.build({
227
207
  debug: isDebug,
228
208
  clean: shouldClean,
229
209
  target: triple,
210
+ locked: this.config.locked === true,
230
211
  crtStatic: useStatic && wantsCrtStatic(triple),
231
212
  rustflags,
232
213
  targetDir: managedTargetDir,
@@ -236,6 +217,10 @@ export class TsRustCli {
236
217
  console.error(`Build failed for target ${triple} with exit code ${buildResult.exitCode}`);
237
218
  process.exit(1);
238
219
  }
220
+ const gitAfter = await captureGitSnapshot(this.cwd);
221
+ assertGitSnapshotUnchanged(gitBefore, gitAfter);
222
+ const provenanceAfter = this.collectBuildProvenance(gitAfter);
223
+ this.assertProjectIdentityUnchanged(provenanceBefore, provenanceAfter);
239
224
 
240
225
  const targetDir = getCargoArtifactDir(managedTargetDir, profile, triple);
241
226
 
@@ -253,27 +238,33 @@ export class TsRustCli {
253
238
  await FsHelpers.makeExecutable(destBinary);
254
239
 
255
240
  const size = await FsHelpers.getFileSize(destBinary);
256
- console.log(`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${destName}`);
241
+ console.log(
242
+ `Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${destName}`,
243
+ );
257
244
 
258
245
  if (useStatic && isLinuxTriple(triple)) {
259
246
  if (!(await ElfInspector.isStaticElf(destBinary))) {
260
- 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
+ );
261
250
  process.exit(1);
262
251
  }
263
252
  console.log(`Verified statically linked: dist_rust/${destName}`);
264
253
  }
265
254
 
266
- await ProvenanceStamper.stamp(destBinary, {
267
- ...buildProvenance,
255
+ await ProvenanceStore.write(destBinary, {
256
+ ...provenanceBefore,
268
257
  binary: binName,
269
258
  target: friendly,
270
259
  });
271
- 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
+ );
272
263
  }
273
264
 
274
265
  // Only clean on first iteration
275
266
  if (shouldClean) {
276
- (argvArg as any).clean = false;
267
+ shouldClean = false;
277
268
  }
278
269
  }
279
270
  } else {
@@ -282,17 +273,22 @@ export class TsRustCli {
282
273
  // (proc-macros cannot build with +crt-static on linux-gnu).
283
274
  let nativeTriple: string | undefined;
284
275
  if (useStatic) {
285
- nativeTriple = new ToolchainManager().getHostTriple();
276
+ nativeTriple = hostTriple;
286
277
  if (!isLinuxTriple(nativeTriple)) {
287
- 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
+ );
288
281
  }
289
282
  }
290
283
 
291
284
  const cargoRunner = new CargoRunner(rustDir, envPrefix);
285
+ const gitBefore = await captureGitSnapshot(this.cwd);
286
+ const provenanceBefore = this.collectBuildProvenance(gitBefore);
292
287
  const buildResult = await cargoRunner.build({
293
288
  debug: isDebug,
294
289
  clean: shouldClean,
295
290
  target: nativeTriple,
291
+ locked: this.config.locked === true,
296
292
  crtStatic: !!nativeTriple && wantsCrtStatic(nativeTriple),
297
293
  rustflags,
298
294
  targetDir: managedTargetDir,
@@ -302,6 +298,10 @@ export class TsRustCli {
302
298
  console.error(`Build failed with exit code ${buildResult.exitCode}`);
303
299
  process.exit(1);
304
300
  }
301
+ const gitAfter = await captureGitSnapshot(this.cwd);
302
+ assertGitSnapshotUnchanged(gitBefore, gitAfter);
303
+ const provenanceAfter = this.collectBuildProvenance(gitAfter);
304
+ this.assertProjectIdentityUnchanged(provenanceBefore, provenanceAfter);
305
305
 
306
306
  const targetDir = getCargoArtifactDir(managedTargetDir, profile, nativeTriple);
307
307
 
@@ -320,22 +320,28 @@ export class TsRustCli {
320
320
  await FsHelpers.makeExecutable(destBinary);
321
321
 
322
322
  const size = await FsHelpers.getFileSize(destBinary);
323
- console.log(`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`);
323
+ console.log(
324
+ `Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`,
325
+ );
324
326
 
325
327
  if (useStatic && nativeTriple && isLinuxTriple(nativeTriple)) {
326
328
  if (!(await ElfInspector.isStaticElf(destBinary))) {
327
- 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
+ );
328
332
  process.exit(1);
329
333
  }
330
334
  console.log(`Verified statically linked: dist_rust/${binName}`);
331
335
  }
332
336
 
333
- await ProvenanceStamper.stamp(destBinary, {
334
- ...buildProvenance,
337
+ await ProvenanceStore.write(destBinary, {
338
+ ...provenanceBefore,
335
339
  binary: binName,
336
340
  target: nativeTriple || 'native',
337
341
  });
338
- 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
+ );
339
345
  }
340
346
  }
341
347
 
@@ -356,7 +362,13 @@ export class TsRustCli {
356
362
 
357
363
  private shouldPruneAfterBuild(): boolean {
358
364
  const normalized = process.env.TSRUST_PRUNE_AFTER_BUILD?.toLowerCase();
359
- 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
+ );
360
372
  }
361
373
 
362
374
  private buildRustflags(rustDir: string): string[] {
@@ -378,7 +390,7 @@ export class TsRustCli {
378
390
  }
379
391
 
380
392
  /**
381
- * `tsrust inspect <binary>` — prints the embedded build provenance so any
393
+ * `tsrust inspect <binary>` — verifies and prints build provenance so any
382
394
  * environment can answer "what is this binary built from" definitively.
383
395
  */
384
396
  private registerInspectCommand(): void {
@@ -391,15 +403,73 @@ export class TsRustCli {
391
403
  const resolvedPath = path.isAbsolute(binaryPath)
392
404
  ? binaryPath
393
405
  : path.join(this.cwd, binaryPath);
394
- const info = await ProvenanceStamper.read(resolvedPath);
406
+ const info = await ProvenanceStore.read(resolvedPath);
395
407
  if (!info) {
396
- 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}.`);
397
409
  process.exit(1);
398
410
  }
399
411
  console.log(JSON.stringify(info, null, 2));
400
412
  });
401
413
  }
402
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
+
403
473
  private registerCleanCommand(): void {
404
474
  this.cli.addCommand('clean').subscribe(async (_argvArg) => {
405
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