@git.zone/tsrust 1.8.0 → 1.9.1

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 (41) 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 +122 -75
  10. package/dist_ts/mod_cli/helpers.codesign.d.ts +2 -0
  11. package/dist_ts/mod_cli/helpers.codesign.js +48 -0
  12. package/dist_ts/mod_cli/helpers.targets.d.ts +20 -0
  13. package/dist_ts/mod_cli/helpers.targets.js +106 -0
  14. package/dist_ts/mod_cli/index.d.ts +1 -0
  15. package/dist_ts/mod_cli/index.js +2 -1
  16. package/dist_ts/mod_elf/classes.provenance.d.ts +2 -1
  17. package/dist_ts/mod_elf/classes.provenance.js +0 -0
  18. package/dist_ts/mod_provenance/classes.gitstate.d.ts +7 -0
  19. package/dist_ts/mod_provenance/classes.gitstate.js +34 -0
  20. package/dist_ts/mod_provenance/classes.provenancestore.d.ts +11 -0
  21. package/dist_ts/mod_provenance/classes.provenancestore.js +184 -0
  22. package/dist_ts/mod_provenance/helpers.owneridentity.d.ts +2 -0
  23. package/dist_ts/mod_provenance/helpers.owneridentity.js +57 -0
  24. package/dist_ts/mod_provenance/index.d.ts +3 -0
  25. package/dist_ts/mod_provenance/index.js +4 -0
  26. package/package.json +3 -3
  27. package/readme.hints.md +6 -3
  28. package/readme.md +77 -5
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/index.ts +2 -0
  31. package/ts/mod_artifact/classes.artifactassembler.ts +1028 -0
  32. package/ts/mod_artifact/index.ts +6 -0
  33. package/ts/mod_cli/classes.tsrustcli.ts +179 -95
  34. package/ts/mod_cli/helpers.codesign.ts +71 -0
  35. package/ts/mod_cli/helpers.targets.ts +141 -0
  36. package/ts/mod_cli/index.ts +10 -0
  37. package/ts/mod_elf/classes.provenance.ts +0 -0
  38. package/ts/mod_provenance/classes.gitstate.ts +49 -0
  39. package/ts/mod_provenance/classes.provenancestore.ts +194 -0
  40. package/ts/mod_provenance/helpers.owneridentity.ts +57 -0
  41. 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,23 @@ 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';
32
+ import { ensureDarwinCodeSignature } from './helpers.codesign.js';
65
33
 
66
34
  /** crt-static injection only applies to glibc targets; musl is static by default. */
67
35
  function wantsCrtStatic(triple: string): boolean {
@@ -72,6 +40,10 @@ function isLinuxTriple(triple: string): boolean {
72
40
  return triple.includes('-linux-');
73
41
  }
74
42
 
43
+ function isDarwinTriple(triple: string): boolean {
44
+ return triple.endsWith('-apple-darwin');
45
+ }
46
+
75
47
  export class TsRustCli {
76
48
  private cli: plugins.smartcli.Smartcli;
77
49
  private cwd: string;
@@ -89,15 +61,11 @@ export class TsRustCli {
89
61
  this.registerStandardCommand();
90
62
  this.registerCleanCommand();
91
63
  this.registerPruneCommand();
64
+ this.registerAssembleCommand();
92
65
  this.registerInspectCommand();
93
66
  }
94
67
 
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'>> {
68
+ private readProjectIdentity(): { projectName: string; projectVersion: string } {
101
69
  let projectName = 'unknown';
102
70
  let projectVersion = 'unknown';
103
71
  try {
@@ -109,25 +77,40 @@ export class TsRustCli {
109
77
  } catch {
110
78
  // package.json optional for pure Rust workspaces
111
79
  }
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
- }
80
+ return { projectName, projectVersion };
81
+ }
82
+
83
+ /**
84
+ * Collects build provenance for one Cargo invocation: consuming project identity,
85
+ * git commit, and the tsrust version doing the build. Tolerant of
86
+ * non-git checkouts and missing package metadata.
87
+ */
88
+ private collectBuildProvenance(
89
+ gitSnapshotArg: IGitSnapshot,
90
+ ): Omit<ITsrustBuildInfo, 'binary' | 'target'> {
91
+ const { projectName, projectVersion } = this.readProjectIdentity();
122
92
  return {
123
93
  projectName,
124
94
  projectVersion,
125
- gitCommit,
95
+ gitCommit: gitSnapshotArg.commit,
96
+ gitDirty: gitSnapshotArg.available ? gitSnapshotArg.status.length > 0 : undefined,
126
97
  builtAt: new Date().toISOString(),
127
98
  tsrustVersion: commitinfo.version,
128
99
  };
129
100
  }
130
101
 
102
+ private assertProjectIdentityUnchanged(
103
+ beforeArg: Omit<ITsrustBuildInfo, 'binary' | 'target'>,
104
+ afterArg: Omit<ITsrustBuildInfo, 'binary' | 'target'>,
105
+ ): void {
106
+ if (
107
+ beforeArg.projectName !== afterArg.projectName ||
108
+ beforeArg.projectVersion !== afterArg.projectVersion
109
+ ) {
110
+ throw new Error('Project package identity changed while Cargo was building');
111
+ }
112
+ }
113
+
131
114
  /**
132
115
  * Resolves the Rust toolchain to use. Tries system cargo first,
133
116
  * then falls back to a bundled toolchain at /tmp/tsrust_toolchain/.
@@ -183,17 +166,19 @@ export class TsRustCli {
183
166
  console.log(`Binary targets: ${workspaceInfo.binTargets.join(', ')}`);
184
167
 
185
168
  const isDebug = !!(argvArg as any).debug;
186
- const shouldClean = !!(argvArg as any).clean;
169
+ let shouldClean = !!(argvArg as any).clean;
187
170
  const distDir = path.join(this.cwd, 'dist_rust');
188
171
  const profile = isDebug ? 'debug' : 'release';
189
172
  const managedTargetDir = resolveManagedTargetDir(this.cwd, this.config.targetDir);
190
173
  await writeTargetCacheMarker(managedTargetDir);
191
174
 
192
- // Parse --target flag (can appear multiple times), fall back to smartconfig.json config
175
+ // CLI targets override host-specific and legacy configured targets.
193
176
  const cliTargets = (argvArg as any).target;
194
- const targets: string[] = cliTargets
177
+ const cliTargetList: string[] | undefined = cliTargets
195
178
  ? (Array.isArray(cliTargets) ? cliTargets : [cliTargets])
196
- : this.config.targets || [];
179
+ : undefined;
180
+ const hostTriple = new ToolchainManager().getHostTriple();
181
+ const resolvedTargets = resolveBuildTargets(this.config, hostTriple, cliTargetList);
197
182
 
198
183
  const useStatic = !!(argvArg as any).static || !!this.config.static;
199
184
  if (useStatic) {
@@ -205,25 +190,24 @@ export class TsRustCli {
205
190
  console.log(`Using additional Rust flags: ${rustflags.join(' ')}`);
206
191
  }
207
192
 
208
- const buildProvenance = await this.collectBuildProvenance();
209
-
210
- if (targets.length > 0) {
193
+ if (resolvedTargets.length > 0) {
211
194
  // 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(', ')}`);
195
+ console.log(
196
+ `Cross-compiling for: ${resolvedTargets.map((target) => `${target.friendly} (${target.triple})`).join(', ')}`,
197
+ );
218
198
 
219
199
  await FsHelpers.ensureEmptyDir(distDir);
220
200
 
221
201
  for (const { triple, friendly } of resolvedTargets) {
222
202
  console.log(`\n--- Building for ${friendly} (${triple}) ---`);
223
203
  if (useStatic && !isLinuxTriple(triple)) {
224
- console.log(`Note: static linking is not applicable for ${triple}; building with default linkage.`);
204
+ console.log(
205
+ `Note: static linking is not applicable for ${triple}; building with default linkage.`,
206
+ );
225
207
  }
226
208
  const cargoRunner = new CargoRunner(rustDir, envPrefix);
209
+ const gitBefore = await captureGitSnapshot(this.cwd);
210
+ const provenanceBefore = this.collectBuildProvenance(gitBefore);
227
211
  const buildResult = await cargoRunner.build({
228
212
  debug: isDebug,
229
213
  clean: shouldClean,
@@ -238,6 +222,10 @@ export class TsRustCli {
238
222
  console.error(`Build failed for target ${triple} with exit code ${buildResult.exitCode}`);
239
223
  process.exit(1);
240
224
  }
225
+ const gitAfter = await captureGitSnapshot(this.cwd);
226
+ assertGitSnapshotUnchanged(gitBefore, gitAfter);
227
+ const provenanceAfter = this.collectBuildProvenance(gitAfter);
228
+ this.assertProjectIdentityUnchanged(provenanceBefore, provenanceAfter);
241
229
 
242
230
  const targetDir = getCargoArtifactDir(managedTargetDir, profile, triple);
243
231
 
@@ -251,31 +239,43 @@ export class TsRustCli {
251
239
  continue;
252
240
  }
253
241
 
242
+ if (isDarwinTriple(triple)) {
243
+ const signatureResult = await ensureDarwinCodeSignature(srcBinary);
244
+ console.log(
245
+ `${signatureResult === 'applied' ? 'Applied' : 'Verified'} Darwin code signature: ${srcBinary}`,
246
+ );
247
+ }
254
248
  await FsHelpers.copyFile(srcBinary, destBinary);
255
249
  await FsHelpers.makeExecutable(destBinary);
256
250
 
257
251
  const size = await FsHelpers.getFileSize(destBinary);
258
- console.log(`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${destName}`);
252
+ console.log(
253
+ `Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${destName}`,
254
+ );
259
255
 
260
256
  if (useStatic && isLinuxTriple(triple)) {
261
257
  if (!(await ElfInspector.isStaticElf(destBinary))) {
262
- console.error(`Error: ${destName} is not statically linked (PT_INTERP present) although static linking was requested.`);
258
+ console.error(
259
+ `Error: ${destName} is not statically linked (PT_INTERP present) although static linking was requested.`,
260
+ );
263
261
  process.exit(1);
264
262
  }
265
263
  console.log(`Verified statically linked: dist_rust/${destName}`);
266
264
  }
267
265
 
268
- await ProvenanceStamper.stamp(destBinary, {
269
- ...buildProvenance,
266
+ await ProvenanceStore.write(destBinary, {
267
+ ...provenanceBefore,
270
268
  binary: binName,
271
269
  target: friendly,
272
270
  });
273
- console.log(`Stamped provenance: ${buildProvenance.projectName}@${buildProvenance.projectVersion} ${buildProvenance.gitCommit.slice(0, 12)} (${friendly})`);
271
+ console.log(
272
+ `Wrote provenance: ${provenanceBefore.projectName}@${provenanceBefore.projectVersion} ${provenanceBefore.gitCommit.slice(0, 12)} (${friendly})`,
273
+ );
274
274
  }
275
275
 
276
276
  // Only clean on first iteration
277
277
  if (shouldClean) {
278
- (argvArg as any).clean = false;
278
+ shouldClean = false;
279
279
  }
280
280
  }
281
281
  } else {
@@ -284,13 +284,17 @@ export class TsRustCli {
284
284
  // (proc-macros cannot build with +crt-static on linux-gnu).
285
285
  let nativeTriple: string | undefined;
286
286
  if (useStatic) {
287
- nativeTriple = new ToolchainManager().getHostTriple();
287
+ nativeTriple = hostTriple;
288
288
  if (!isLinuxTriple(nativeTriple)) {
289
- console.log(`Note: static linking is not applicable for ${nativeTriple}; building with default linkage.`);
289
+ console.log(
290
+ `Note: static linking is not applicable for ${nativeTriple}; building with default linkage.`,
291
+ );
290
292
  }
291
293
  }
292
294
 
293
295
  const cargoRunner = new CargoRunner(rustDir, envPrefix);
296
+ const gitBefore = await captureGitSnapshot(this.cwd);
297
+ const provenanceBefore = this.collectBuildProvenance(gitBefore);
294
298
  const buildResult = await cargoRunner.build({
295
299
  debug: isDebug,
296
300
  clean: shouldClean,
@@ -305,6 +309,10 @@ export class TsRustCli {
305
309
  console.error(`Build failed with exit code ${buildResult.exitCode}`);
306
310
  process.exit(1);
307
311
  }
312
+ const gitAfter = await captureGitSnapshot(this.cwd);
313
+ assertGitSnapshotUnchanged(gitBefore, gitAfter);
314
+ const provenanceAfter = this.collectBuildProvenance(gitAfter);
315
+ this.assertProjectIdentityUnchanged(provenanceBefore, provenanceAfter);
308
316
 
309
317
  const targetDir = getCargoArtifactDir(managedTargetDir, profile, nativeTriple);
310
318
 
@@ -319,26 +327,38 @@ export class TsRustCli {
319
327
  continue;
320
328
  }
321
329
 
330
+ if (isDarwinTriple(hostTriple)) {
331
+ const signatureResult = await ensureDarwinCodeSignature(srcBinary);
332
+ console.log(
333
+ `${signatureResult === 'applied' ? 'Applied' : 'Verified'} Darwin code signature: ${srcBinary}`,
334
+ );
335
+ }
322
336
  await FsHelpers.copyFile(srcBinary, destBinary);
323
337
  await FsHelpers.makeExecutable(destBinary);
324
338
 
325
339
  const size = await FsHelpers.getFileSize(destBinary);
326
- console.log(`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`);
340
+ console.log(
341
+ `Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`,
342
+ );
327
343
 
328
344
  if (useStatic && nativeTriple && isLinuxTriple(nativeTriple)) {
329
345
  if (!(await ElfInspector.isStaticElf(destBinary))) {
330
- console.error(`Error: ${binName} is not statically linked (PT_INTERP present) although static linking was requested.`);
346
+ console.error(
347
+ `Error: ${binName} is not statically linked (PT_INTERP present) although static linking was requested.`,
348
+ );
331
349
  process.exit(1);
332
350
  }
333
351
  console.log(`Verified statically linked: dist_rust/${binName}`);
334
352
  }
335
353
 
336
- await ProvenanceStamper.stamp(destBinary, {
337
- ...buildProvenance,
354
+ await ProvenanceStore.write(destBinary, {
355
+ ...provenanceBefore,
338
356
  binary: binName,
339
357
  target: nativeTriple || 'native',
340
358
  });
341
- console.log(`Stamped provenance: ${buildProvenance.projectName}@${buildProvenance.projectVersion} ${buildProvenance.gitCommit.slice(0, 12)} (${nativeTriple || 'native'})`);
359
+ console.log(
360
+ `Wrote provenance: ${provenanceBefore.projectName}@${provenanceBefore.projectVersion} ${provenanceBefore.gitCommit.slice(0, 12)} (${nativeTriple || 'native'})`,
361
+ );
342
362
  }
343
363
  }
344
364
 
@@ -359,7 +379,13 @@ export class TsRustCli {
359
379
 
360
380
  private shouldPruneAfterBuild(): boolean {
361
381
  const normalized = process.env.TSRUST_PRUNE_AFTER_BUILD?.toLowerCase();
362
- return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'y' || this.config.pruneAfterBuild === true;
382
+ return (
383
+ normalized === '1' ||
384
+ normalized === 'true' ||
385
+ normalized === 'yes' ||
386
+ normalized === 'y' ||
387
+ this.config.pruneAfterBuild === true
388
+ );
363
389
  }
364
390
 
365
391
  private buildRustflags(rustDir: string): string[] {
@@ -381,7 +407,7 @@ export class TsRustCli {
381
407
  }
382
408
 
383
409
  /**
384
- * `tsrust inspect <binary>` — prints the embedded build provenance so any
410
+ * `tsrust inspect <binary>` — verifies and prints build provenance so any
385
411
  * environment can answer "what is this binary built from" definitively.
386
412
  */
387
413
  private registerInspectCommand(): void {
@@ -394,15 +420,73 @@ export class TsRustCli {
394
420
  const resolvedPath = path.isAbsolute(binaryPath)
395
421
  ? binaryPath
396
422
  : path.join(this.cwd, binaryPath);
397
- const info = await ProvenanceStamper.read(resolvedPath);
423
+ const info = await ProvenanceStore.read(resolvedPath);
398
424
  if (!info) {
399
- console.error(`No tsrust build provenance found in ${resolvedPath} (built before tsrust 1.7.0?).`);
425
+ console.error(`No tsrust build provenance found in ${resolvedPath}.`);
400
426
  process.exit(1);
401
427
  }
402
428
  console.log(JSON.stringify(info, null, 2));
403
429
  });
404
430
  }
405
431
 
432
+ private registerAssembleCommand(): void {
433
+ this.cli.addCommand('assemble').subscribe(async (argvArg) => {
434
+ const sourceDirectories = ((argvArg as any)._ as unknown[])
435
+ .slice(1)
436
+ .map((sourceDirectory) => String(sourceDirectory));
437
+ if (sourceDirectories.length === 0) {
438
+ throw new Error('Usage: tsrust assemble <artifact-directory> [...]');
439
+ }
440
+
441
+ const cliTargets = (argvArg as any).target;
442
+ const cliTargetList: string[] | undefined = cliTargets
443
+ ? (Array.isArray(cliTargets) ? cliTargets : [cliTargets])
444
+ : undefined;
445
+ const expectedTargets = cliTargetList
446
+ ? normalizeTargets(cliTargetList, '--target')
447
+ : configuredAssemblyTargets(this.config);
448
+ if (expectedTargets.length === 0) {
449
+ throw new Error(
450
+ 'Artifact assembly requires configured targets or at least one --target',
451
+ );
452
+ }
453
+
454
+ const rustDir = await this.detectRustDir();
455
+ if (!rustDir) {
456
+ throw new Error('No rust/ or ts_rust/ directory found with a Cargo.toml');
457
+ }
458
+ const workspaceInfo = await new CargoConfig(rustDir).parse();
459
+ if (workspaceInfo.binTargets.length === 0) {
460
+ throw new Error('No binary targets found in Cargo.toml');
461
+ }
462
+
463
+ const gitSnapshot = await captureGitSnapshot(this.cwd);
464
+ if (!gitSnapshot.available || gitSnapshot.commit === 'unknown') {
465
+ throw new Error('Artifact assembly requires a Git checkout');
466
+ }
467
+ if (gitSnapshot.status.length > 0) {
468
+ throw new Error('Artifact assembly requires a clean Git worktree');
469
+ }
470
+ const { projectName, projectVersion } = this.readProjectIdentity();
471
+ const result = await new ArtifactAssembler({
472
+ workspace: this.cwd,
473
+ sourceDirectories,
474
+ expectedTargets,
475
+ expectedBinaries: workspaceInfo.binTargets,
476
+ expectedProjectName: projectName,
477
+ expectedProjectVersion: projectVersion,
478
+ expectedGitCommit: gitSnapshot.commit,
479
+ expectedTsrustVersion: commitinfo.version,
480
+ }).assemble();
481
+ console.log(
482
+ `Assembled ${result.artifactCount} artifacts for ${result.targets.join(', ')} into ${result.outputDirectory}`,
483
+ );
484
+ if (result.cleanupPending) {
485
+ console.warn('Artifact assembly committed; stale transaction cleanup will retry next run.');
486
+ }
487
+ });
488
+ }
489
+
406
490
  private registerCleanCommand(): void {
407
491
  this.cli.addCommand('clean').subscribe(async (_argvArg) => {
408
492
  // Clean cargo build
@@ -0,0 +1,71 @@
1
+ import * as childProcess from 'child_process';
2
+ import * as util from 'util';
3
+
4
+ const execFile = util.promisify(childProcess.execFile);
5
+
6
+ export type TDarwinCodeSignatureResult = 'existing' | 'applied';
7
+
8
+ interface ICodeSignError extends Error {
9
+ stderr?: string | Buffer;
10
+ }
11
+
12
+ const codeSignEnvironment = {
13
+ ...process.env,
14
+ LANG: 'C',
15
+ LC_ALL: 'C',
16
+ };
17
+
18
+ const errorStderr = (errorArg: unknown): string => {
19
+ const stderr = (errorArg as ICodeSignError | undefined)?.stderr;
20
+ return Buffer.isBuffer(stderr) ? stderr.toString('utf8') : stderr || '';
21
+ };
22
+
23
+ export const ensureDarwinCodeSignature = async (
24
+ binaryPathArg: string,
25
+ codeSignCommandArg: string = '/usr/bin/codesign',
26
+ ): Promise<TDarwinCodeSignatureResult> => {
27
+ let verificationError: unknown;
28
+ try {
29
+ await execFile(
30
+ codeSignCommandArg,
31
+ ['--verify', '--deep', '--strict', binaryPathArg],
32
+ { env: codeSignEnvironment },
33
+ );
34
+ return 'existing';
35
+ } catch (error) {
36
+ verificationError = error;
37
+ }
38
+
39
+ try {
40
+ await execFile(codeSignCommandArg, ['-d', binaryPathArg], {
41
+ env: codeSignEnvironment,
42
+ });
43
+ } catch (error) {
44
+ if (!errorStderr(error).includes('code object is not signed at all')) {
45
+ throw new Error(`Unable to classify Darwin code signature for ${binaryPathArg}`, {
46
+ cause: error,
47
+ });
48
+ }
49
+ try {
50
+ await execFile(
51
+ codeSignCommandArg,
52
+ ['--force', '--sign', '-', '--timestamp=none', binaryPathArg],
53
+ { env: codeSignEnvironment },
54
+ );
55
+ await execFile(
56
+ codeSignCommandArg,
57
+ ['--verify', '--deep', '--strict', binaryPathArg],
58
+ { env: codeSignEnvironment },
59
+ );
60
+ return 'applied';
61
+ } catch (signingError) {
62
+ throw new Error(`Failed to apply a valid Darwin code signature to ${binaryPathArg}`, {
63
+ cause: signingError,
64
+ });
65
+ }
66
+ }
67
+
68
+ throw new Error(`Darwin binary has a present but invalid code signature: ${binaryPathArg}`, {
69
+ cause: verificationError,
70
+ });
71
+ };
@@ -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