@git.zone/tsrust 1.9.1 → 1.10.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 (46) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/index.d.ts +1 -0
  3. package/dist_ts/index.js +2 -1
  4. package/dist_ts/mod_artifact/classes.artifactassembler.d.ts +6 -0
  5. package/dist_ts/mod_artifact/classes.artifactassembler.js +169 -6
  6. package/dist_ts/mod_artifact/index.d.ts +1 -1
  7. package/dist_ts/mod_artifact/index.js +2 -2
  8. package/dist_ts/mod_cargo/classes.cargorunner.js +8 -5
  9. package/dist_ts/mod_cli/classes.tsrustcli.d.ts +1 -0
  10. package/dist_ts/mod_cli/classes.tsrustcli.js +30 -3
  11. package/dist_ts/mod_cli/helpers.targets.d.ts +2 -0
  12. package/dist_ts/mod_cli/helpers.targets.js +1 -1
  13. package/dist_ts/mod_matrix/classes.matrixcommandrunner.d.ts +25 -0
  14. package/dist_ts/mod_matrix/classes.matrixcommandrunner.js +266 -0
  15. package/dist_ts/mod_matrix/classes.nativematrixbuilder.d.ts +64 -0
  16. package/dist_ts/mod_matrix/classes.nativematrixbuilder.js +1084 -0
  17. package/dist_ts/mod_matrix/helpers.matrixconfig.d.ts +30 -0
  18. package/dist_ts/mod_matrix/helpers.matrixconfig.js +129 -0
  19. package/dist_ts/mod_matrix/index.d.ts +3 -0
  20. package/dist_ts/mod_matrix/index.js +4 -0
  21. package/dist_ts/mod_provenance/classes.gitstate.d.ts +1 -0
  22. package/dist_ts/mod_provenance/classes.gitstate.js +13 -3
  23. package/dist_ts/mod_provenance/classes.provenancestore.d.ts +1 -0
  24. package/dist_ts/mod_provenance/classes.provenancestore.js +5 -1
  25. package/dist_ts/mod_provenance/index.d.ts +1 -1
  26. package/dist_ts/mod_provenance/index.js +2 -2
  27. package/dist_ts/mod_toolchain/classes.toolchainmanager.d.ts +6 -0
  28. package/dist_ts/mod_toolchain/classes.toolchainmanager.js +211 -40
  29. package/package.json +2 -2
  30. package/readme.hints.md +12 -1
  31. package/readme.md +69 -2
  32. package/ts/00_commitinfo_data.ts +1 -1
  33. package/ts/index.ts +1 -0
  34. package/ts/mod_artifact/classes.artifactassembler.ts +201 -5
  35. package/ts/mod_artifact/index.ts +2 -0
  36. package/ts/mod_cargo/classes.cargorunner.ts +9 -4
  37. package/ts/mod_cli/classes.tsrustcli.ts +34 -2
  38. package/ts/mod_cli/helpers.targets.ts +3 -0
  39. package/ts/mod_matrix/classes.matrixcommandrunner.ts +294 -0
  40. package/ts/mod_matrix/classes.nativematrixbuilder.ts +1472 -0
  41. package/ts/mod_matrix/helpers.matrixconfig.ts +222 -0
  42. package/ts/mod_matrix/index.ts +23 -0
  43. package/ts/mod_provenance/classes.gitstate.ts +13 -2
  44. package/ts/mod_provenance/classes.provenancestore.ts +4 -0
  45. package/ts/mod_provenance/index.ts +1 -0
  46. package/ts/mod_toolchain/classes.toolchainmanager.ts +236 -43
@@ -7,14 +7,19 @@ import {
7
7
  captureGitSnapshot,
8
8
  getHostIdentity,
9
9
  isLocalProcessRunning,
10
+ MAX_PROVENANCE_SIDECAR_BYTES,
10
11
  PROVENANCE_SIDECAR_SUFFIX,
11
12
  ProvenanceStore,
12
13
  } from '../mod_provenance/index.js';
13
14
 
14
- const TRANSACTION_FORMAT = 'tsrust.artifact-assembly.v1';
15
+ const LEGACY_TRANSACTION_FORMAT = 'tsrust.artifact-assembly.v1';
16
+ const TRANSACTION_FORMAT = 'tsrust.artifact-assembly.v2';
15
17
  const LOCK_FORMAT = 'tsrust.artifact-assembly-lock.v1';
16
18
  const RECOVERY_CLAIM_FORMAT = 'tsrust.artifact-assembly-recovery.v1';
19
+ const MANIFEST_FORMAT = 'tsrust.artifact-assembly-manifest.v1';
17
20
  const TRANSACTION_DIRECTORY = 'tsrust-assembly';
21
+ export const MAX_ARTIFACT_BYTES = 2 * 1024 * 1024 * 1024;
22
+ export const MAX_ASSEMBLY_INPUT_BYTES = 8 * 1024 * 1024 * 1024;
18
23
  const activeOwnerTokens = new Set<string>();
19
24
  const processOwnerTokens = new Set<string>();
20
25
 
@@ -69,13 +74,26 @@ interface IAssemblyPaths {
69
74
  recoveryClaimPath: string;
70
75
  statePath: string;
71
76
  stateTemporaryPath: string;
77
+ manifestPath: string;
78
+ manifestTemporaryPath: string;
72
79
  stagingDirectory: string;
73
80
  backupDirectory: string;
74
81
  failedDirectory: string;
75
82
  }
76
83
 
84
+ interface IAssemblyManifestEntry {
85
+ name: string;
86
+ binarySha256: string;
87
+ sidecarSha256: string;
88
+ }
89
+
90
+ interface IAssemblyManifest {
91
+ format: typeof MANIFEST_FORMAT;
92
+ artifacts: IAssemblyManifestEntry[];
93
+ }
94
+
77
95
  interface IAssemblyTransactionState {
78
- format: typeof TRANSACTION_FORMAT;
96
+ format: typeof TRANSACTION_FORMAT | typeof LEGACY_TRANSACTION_FORMAT;
79
97
  pid: number;
80
98
  hostname: string;
81
99
  hostIdentity: string;
@@ -148,7 +166,7 @@ function isTransactionState(valueArg: unknown): valueArg is IAssemblyTransaction
148
166
  if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) return false;
149
167
  const value = valueArg as Record<string, unknown>;
150
168
  return (
151
- value.format === TRANSACTION_FORMAT &&
169
+ (value.format === TRANSACTION_FORMAT || value.format === LEGACY_TRANSACTION_FORMAT) &&
152
170
  typeof value.pid === 'number' &&
153
171
  typeof value.hostname === 'string' &&
154
172
  typeof value.hostIdentity === 'string' &&
@@ -202,6 +220,28 @@ function isRecoveryClaim(valueArg: unknown): valueArg is IAssemblyRecoveryClaim
202
220
  );
203
221
  }
204
222
 
223
+ function isAssemblyManifest(valueArg: unknown): valueArg is IAssemblyManifest {
224
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) return false;
225
+ const value = valueArg as Record<string, unknown>;
226
+ return (
227
+ value.format === MANIFEST_FORMAT &&
228
+ Array.isArray(value.artifacts) &&
229
+ value.artifacts.length > 0 &&
230
+ value.artifacts.every((entry) => {
231
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
232
+ const candidate = entry as Record<string, unknown>;
233
+ return (
234
+ typeof candidate.name === 'string' &&
235
+ /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(candidate.name) &&
236
+ typeof candidate.binarySha256 === 'string' &&
237
+ /^[a-f0-9]{64}$/.test(candidate.binarySha256) &&
238
+ typeof candidate.sidecarSha256 === 'string' &&
239
+ /^[a-f0-9]{64}$/.test(candidate.sidecarSha256)
240
+ );
241
+ })
242
+ );
243
+ }
244
+
205
245
  export class ArtifactAssembler {
206
246
  private options: IArtifactAssemblerOptions;
207
247
 
@@ -253,6 +293,10 @@ export class ArtifactAssembler {
253
293
 
254
294
  const candidates = await this.collectCandidates(sourceDirectories);
255
295
  await fs.promises.mkdir(paths.stagingDirectory, { mode: 0o700 });
296
+ const manifest: IAssemblyManifest = {
297
+ format: MANIFEST_FORMAT,
298
+ artifacts: [],
299
+ };
256
300
  for (const candidate of candidates) {
257
301
  const destinationBinary = path.join(
258
302
  paths.stagingDirectory,
@@ -275,12 +319,20 @@ export class ArtifactAssembler {
275
319
  if (JSON.stringify(copiedBuildInfo) !== JSON.stringify(candidate.buildInfo)) {
276
320
  throw new Error(`Artifact provenance changed while copying ${candidate.binaryPath}`);
277
321
  }
322
+ manifest.artifacts.push({
323
+ name: candidate.destinationName,
324
+ binarySha256: await ProvenanceStore.sha256(destinationBinary),
325
+ sidecarSha256: await ProvenanceStore.sha256(destinationSidecar),
326
+ });
278
327
  }
328
+ await this.writeManifest(paths, manifest);
329
+ await this.validatePublicationDirectory(paths.stagingDirectory, manifest);
279
330
  await fs.promises.chmod(paths.stagingDirectory, 0o755);
280
331
  await syncDirectory(paths.stagingDirectory);
281
332
  state = { ...state, phase: 'prepared' };
282
333
  await this.writeState(paths, state);
283
334
  await this.assertAssemblySourceUnchanged(paths.workspace);
335
+ await this.validatePublicationDirectory(paths.stagingDirectory, await this.readManifest(paths));
284
336
 
285
337
  if (state.hadExistingOutput) {
286
338
  state = { ...state, phase: 'movingOld' };
@@ -382,6 +434,8 @@ export class ArtifactAssembler {
382
434
  recoveryClaimPath: path.join(transactionParent, 'tsrust-assembly.recovery.lock'),
383
435
  statePath: path.join(transactionDirectory, 'state.json'),
384
436
  stateTemporaryPath: path.join(transactionDirectory, 'state.json.tmp'),
437
+ manifestPath: path.join(transactionDirectory, 'manifest.json'),
438
+ manifestTemporaryPath: path.join(transactionDirectory, 'manifest.json.tmp'),
385
439
  stagingDirectory: path.join(transactionDirectory, 'staging'),
386
440
  backupDirectory: path.join(transactionDirectory, 'backup'),
387
441
  failedDirectory: path.join(transactionDirectory, 'failed'),
@@ -737,7 +791,7 @@ export class ArtifactAssembler {
737
791
  if (!transaction.isDirectory() || transaction.isSymbolicLink()) {
738
792
  throw new Error(`Assembly transaction path is not a regular directory: ${pathsArg.transactionDirectory}`);
739
793
  }
740
- const state = await this.readState(pathsArg);
794
+ let state = await this.readState(pathsArg);
741
795
  if (!state) {
742
796
  await fs.promises.rm(pathsArg.transactionDirectory, { recursive: true });
743
797
  await this.releaseLock(pathsArg, owner.ownerToken);
@@ -757,6 +811,9 @@ export class ArtifactAssembler {
757
811
  state.phase === 'published' ||
758
812
  state.phase === 'committed'
759
813
  ) {
814
+ if (state.format === LEGACY_TRANSACTION_FORMAT) {
815
+ state = await this.migrateLegacyCommittedTransaction(pathsArg, state);
816
+ }
760
817
  const cleanupPending = await this.completeCommittedTransaction(pathsArg, state);
761
818
  if (cleanupPending) {
762
819
  throw new Error(`Unable to clean committed assembly state: ${pathsArg.transactionDirectory}`);
@@ -825,12 +882,95 @@ export class ArtifactAssembler {
825
882
  await syncDirectory(pathsArg.transactionDirectory);
826
883
  }
827
884
 
885
+ private async writeManifest(
886
+ pathsArg: IAssemblyPaths,
887
+ manifestArg: IAssemblyManifest,
888
+ ): Promise<void> {
889
+ const temporary = await fs.promises.open(pathsArg.manifestTemporaryPath, 'wx', 0o600);
890
+ try {
891
+ await temporary.writeFile(`${JSON.stringify(manifestArg, null, 2)}\n`, 'utf8');
892
+ await temporary.sync();
893
+ } finally {
894
+ await temporary.close();
895
+ }
896
+ await fs.promises.rename(pathsArg.manifestTemporaryPath, pathsArg.manifestPath);
897
+ await syncDirectory(pathsArg.transactionDirectory);
898
+ }
899
+
900
+ private async readManifest(pathsArg: IAssemblyPaths): Promise<IAssemblyManifest> {
901
+ const manifest = await fs.promises.lstat(pathsArg.manifestPath);
902
+ if (!manifest.isFile() || manifest.isSymbolicLink() || manifest.size > 16 * 1024 * 1024) {
903
+ throw new Error(`Assembly manifest is not a bounded regular file: ${pathsArg.manifestPath}`);
904
+ }
905
+ let parsed: unknown;
906
+ try {
907
+ parsed = JSON.parse(await fs.promises.readFile(pathsArg.manifestPath, 'utf8'));
908
+ } catch {
909
+ throw new Error(`Assembly manifest is invalid JSON: ${pathsArg.manifestPath}`);
910
+ }
911
+ if (!isAssemblyManifest(parsed)) {
912
+ throw new Error(`Assembly manifest has an invalid shape: ${pathsArg.manifestPath}`);
913
+ }
914
+ const names = parsed.artifacts.map((entry) => entry.name);
915
+ if (new Set(names).size !== names.length) {
916
+ throw new Error(`Assembly manifest contains duplicate artifacts: ${pathsArg.manifestPath}`);
917
+ }
918
+ return parsed;
919
+ }
920
+
921
+ private async validatePublicationDirectory(
922
+ directoryArg: string,
923
+ manifestArg: IAssemblyManifest,
924
+ ): Promise<void> {
925
+ const directory = await fs.promises.lstat(directoryArg);
926
+ if (!directory.isDirectory() || directory.isSymbolicLink()) {
927
+ throw new Error(`Assembly publication path is not a regular directory: ${directoryArg}`);
928
+ }
929
+ const expectedNames = manifestArg.artifacts
930
+ .flatMap((entry) => [entry.name, `${entry.name}${PROVENANCE_SIDECAR_SUFFIX}`])
931
+ .sort();
932
+ const actualNames = (await fs.promises.readdir(directoryArg)).sort();
933
+ if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) {
934
+ throw new Error(`Assembly publication contents do not match the manifest: ${directoryArg}`);
935
+ }
936
+ let aggregateBytes = 0;
937
+ for (const entry of manifestArg.artifacts) {
938
+ const binaryPath = path.join(directoryArg, entry.name);
939
+ const sidecarPath = ProvenanceStore.sidecarPath(binaryPath);
940
+ const binary = await fs.promises.lstat(binaryPath);
941
+ const sidecar = await fs.promises.lstat(sidecarPath);
942
+ if (
943
+ !binary.isFile() ||
944
+ binary.isSymbolicLink() ||
945
+ (binary.mode & 0o111) === 0 ||
946
+ binary.size > MAX_ARTIFACT_BYTES ||
947
+ !sidecar.isFile() ||
948
+ sidecar.isSymbolicLink() ||
949
+ sidecar.size > MAX_PROVENANCE_SIDECAR_BYTES
950
+ ) {
951
+ throw new Error(`Assembly publication artifact is unsafe: ${binaryPath}`);
952
+ }
953
+ aggregateBytes += binary.size + sidecar.size;
954
+ if (aggregateBytes > MAX_ASSEMBLY_INPUT_BYTES) {
955
+ throw new Error('Assembly publication exceeds the aggregate size limit');
956
+ }
957
+ if (
958
+ (await ProvenanceStore.sha256(binaryPath)) !== entry.binarySha256 ||
959
+ (await ProvenanceStore.sha256(sidecarPath)) !== entry.sidecarSha256
960
+ ) {
961
+ throw new Error(`Assembly publication artifact changed: ${binaryPath}`);
962
+ }
963
+ await ProvenanceStore.readSidecar(binaryPath);
964
+ }
965
+ }
966
+
828
967
  private async assertAssemblySourceUnchanged(workspaceArg: string): Promise<void> {
829
968
  const snapshot = await captureGitSnapshot(workspaceArg);
830
969
  if (
831
970
  !snapshot.available ||
832
971
  snapshot.commit !== this.options.expectedGitCommit ||
833
- snapshot.status.length > 0
972
+ snapshot.status.length > 0 ||
973
+ (snapshot.unsafeIndexFlags?.length || 0) > 0
834
974
  ) {
835
975
  throw new Error('Git source state changed before artifact publication');
836
976
  }
@@ -885,16 +1025,62 @@ export class ArtifactAssembler {
885
1025
  await this.releaseLock(pathsArg, stateArg.ownerToken);
886
1026
  }
887
1027
 
1028
+ private async migrateLegacyCommittedTransaction(
1029
+ pathsArg: IAssemblyPaths,
1030
+ stateArg: IAssemblyTransactionState,
1031
+ ): Promise<IAssemblyTransactionState> {
1032
+ let manifest: IAssemblyManifest;
1033
+ if (await lstatOptional(pathsArg.manifestPath)) {
1034
+ manifest = await this.readManifest(pathsArg);
1035
+ } else {
1036
+ const output = await lstatOptional(pathsArg.outputDirectory);
1037
+ const staging = await lstatOptional(pathsArg.stagingDirectory);
1038
+ const publicationDirectory = output
1039
+ ? pathsArg.outputDirectory
1040
+ : staging
1041
+ ? pathsArg.stagingDirectory
1042
+ : undefined;
1043
+ if (!publicationDirectory) {
1044
+ throw new Error(
1045
+ `Legacy committed assembly has no publication directory: ${pathsArg.transactionDirectory}`,
1046
+ );
1047
+ }
1048
+ const candidates = await this.collectCandidates([publicationDirectory]);
1049
+ const artifacts: IAssemblyManifestEntry[] = [];
1050
+ for (const candidate of candidates) {
1051
+ artifacts.push({
1052
+ name: candidate.destinationName,
1053
+ binarySha256: await ProvenanceStore.sha256(candidate.binaryPath),
1054
+ sidecarSha256: await ProvenanceStore.sha256(candidate.sidecarPath),
1055
+ });
1056
+ }
1057
+ manifest = {
1058
+ format: MANIFEST_FORMAT,
1059
+ artifacts,
1060
+ };
1061
+ await fs.promises.rm(pathsArg.manifestTemporaryPath, { force: true });
1062
+ await this.writeManifest(pathsArg, manifest);
1063
+ }
1064
+ const migratedState: IAssemblyTransactionState = {
1065
+ ...stateArg,
1066
+ format: TRANSACTION_FORMAT,
1067
+ };
1068
+ await this.writeState(pathsArg, migratedState);
1069
+ return migratedState;
1070
+ }
1071
+
888
1072
  private async completeCommittedTransaction(
889
1073
  pathsArg: IAssemblyPaths,
890
1074
  stateArg: IAssemblyTransactionState,
891
1075
  ): Promise<boolean> {
1076
+ const manifest = await this.readManifest(pathsArg);
892
1077
  const output = await lstatOptional(pathsArg.outputDirectory);
893
1078
  const staging = await lstatOptional(pathsArg.stagingDirectory);
894
1079
  if (!output) {
895
1080
  if (!staging || !staging.isDirectory() || staging.isSymbolicLink()) {
896
1081
  throw new Error(`Committed artifact publication is incomplete: ${pathsArg.transactionDirectory}`);
897
1082
  }
1083
+ await this.validatePublicationDirectory(pathsArg.stagingDirectory, manifest);
898
1084
  await fs.promises.rename(pathsArg.stagingDirectory, pathsArg.outputDirectory);
899
1085
  await syncDirectory(pathsArg.workspace);
900
1086
  } else {
@@ -904,6 +1090,7 @@ export class ArtifactAssembler {
904
1090
  if (staging) {
905
1091
  throw new Error(`Committed assembly has both staging and output directories: ${pathsArg.transactionDirectory}`);
906
1092
  }
1093
+ await this.validatePublicationDirectory(pathsArg.outputDirectory, manifest);
907
1094
  }
908
1095
  return this.cleanupCommittedTransaction(pathsArg, { ...stateArg, phase: 'published' });
909
1096
  }
@@ -935,6 +1122,7 @@ export class ArtifactAssembler {
935
1122
  );
936
1123
  const expectedBinaries = new Set(this.options.expectedBinaries);
937
1124
  const candidates = new Map<string, IArtifactCandidate>();
1125
+ let aggregateInputBytes = 0;
938
1126
 
939
1127
  for (const sourceDirectory of sourceDirectoriesArg) {
940
1128
  const source = await fs.promises.lstat(sourceDirectory);
@@ -961,6 +1149,9 @@ export class ArtifactAssembler {
961
1149
  if ((binary.mode & 0o111) === 0) {
962
1150
  throw new Error(`Artifact is not executable: ${entryPath}`);
963
1151
  }
1152
+ if (binary.size > MAX_ARTIFACT_BYTES) {
1153
+ throw new Error(`Artifact exceeds the size limit: ${entryPath}`);
1154
+ }
964
1155
 
965
1156
  const buildInfo = await ProvenanceStore.readSidecar(entryPath);
966
1157
  assertArtifactName(buildInfo.binary, 'provenance binary name');
@@ -997,6 +1188,11 @@ export class ArtifactAssembler {
997
1188
  throw new Error(`Artifact inputs contain a duplicate ${destinationName}`);
998
1189
  }
999
1190
  const sidecarPath = path.join(sourceDirectory, sidecarName);
1191
+ const sidecar = await fs.promises.lstat(sidecarPath);
1192
+ aggregateInputBytes += binary.size + sidecar.size;
1193
+ if (aggregateInputBytes > MAX_ASSEMBLY_INPUT_BYTES) {
1194
+ throw new Error('Artifact assembly inputs exceed the aggregate size limit');
1195
+ }
1000
1196
  candidates.set(key, {
1001
1197
  binaryPath: entryPath,
1002
1198
  sidecarPath,
@@ -1,5 +1,7 @@
1
1
  export {
2
2
  ArtifactAssembler,
3
+ MAX_ARTIFACT_BYTES,
4
+ MAX_ASSEMBLY_INPUT_BYTES,
3
5
  type IArtifactAssemblerOptions,
4
6
  type IArtifactAssemblyResult,
5
7
  type IArtifactTarget,
@@ -57,7 +57,7 @@ export class CargoRunner {
57
57
  }
58
58
 
59
59
  const profile = options.debug ? '' : ' --release';
60
- const targetFlag = options.target ? ` --target ${options.target}` : '';
60
+ const targetFlag = options.target ? ` --target ${this.shellQuote(options.target)}` : '';
61
61
  const lockedFlag = options.locked === true ? ' --locked' : '';
62
62
  const rustflags = [...(options.rustflags || [])];
63
63
  if (options.crtStatic) {
@@ -65,7 +65,7 @@ export class CargoRunner {
65
65
  }
66
66
  const rustflagsPrefix = rustflags.length ? `RUSTFLAGS=${this.shellQuote(rustflags.join(' '))} ` : '';
67
67
  const targetDirPrefix = options.targetDir ? `CARGO_TARGET_DIR=${this.shellQuote(options.targetDir)} ` : '';
68
- const command = `${this.envPrefix}cd ${this.rustDir} && ${targetDirPrefix}${rustflagsPrefix}cargo build${profile}${targetFlag}${lockedFlag}`;
68
+ const command = `${this.envPrefix}cd ${this.shellQuote(this.rustDir)} && ${targetDirPrefix}${rustflagsPrefix}cargo build${profile}${targetFlag}${lockedFlag}`;
69
69
 
70
70
  console.log(`Running: ${targetDirPrefix}${rustflagsPrefix}cargo build${profile}${targetFlag}${lockedFlag}`);
71
71
  const result = await this.shell.exec(command);
@@ -85,13 +85,18 @@ export class CargoRunner {
85
85
  * Ensures a rustup target is installed. If not present, installs it via `rustup target add`.
86
86
  */
87
87
  public async ensureTarget(triple: string): Promise<void> {
88
+ if (!triple || triple.length > 255 || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(triple)) {
89
+ throw new Error(`Invalid Rust target: ${JSON.stringify(triple)}`);
90
+ }
88
91
  const listResult = await this.shell.execSilent(`${this.envPrefix}rustup target list --installed`);
89
92
  const installedTargets = listResult.stdout.split('\n').map((l) => l.trim());
90
93
  if (installedTargets.includes(triple)) {
91
94
  return;
92
95
  }
93
96
  console.log(`Installing rustup target: ${triple}`);
94
- const addResult = await this.shell.exec(`${this.envPrefix}rustup target add ${triple}`);
97
+ const addResult = await this.shell.exec(
98
+ `${this.envPrefix}rustup target add ${this.shellQuote(triple)}`,
99
+ );
95
100
  if (addResult.exitCode !== 0) {
96
101
  throw new Error(`Failed to install rustup target ${triple}`);
97
102
  }
@@ -99,7 +104,7 @@ export class CargoRunner {
99
104
 
100
105
  public async clean(options: { targetDir?: string } = {}): Promise<ICargoRunResult> {
101
106
  const targetDirPrefix = options.targetDir ? `CARGO_TARGET_DIR=${this.shellQuote(options.targetDir)} ` : '';
102
- const command = `${this.envPrefix}cd ${this.rustDir} && ${targetDirPrefix}cargo clean`;
107
+ const command = `${this.envPrefix}cd ${this.shellQuote(this.rustDir)} && ${targetDirPrefix}cargo clean`;
103
108
  const result = await this.shell.exec(command);
104
109
 
105
110
  return {
@@ -22,6 +22,7 @@ import {
22
22
  type IGitSnapshot,
23
23
  } from '../mod_provenance/index.js';
24
24
  import { ToolchainManager } from '../mod_toolchain/index.js';
25
+ import { NativeMatrixBuilder } from '../mod_matrix/index.js';
25
26
  import { commitinfo } from '../00_commitinfo_data.js';
26
27
  import {
27
28
  configuredAssemblyTargets,
@@ -52,6 +53,7 @@ export class TsRustCli {
52
53
  constructor(cwd: string = process.cwd()) {
53
54
  this.cwd = cwd;
54
55
  this.cli = new plugins.smartcli.Smartcli();
56
+ this.cli.addVersion(commitinfo.version);
55
57
  const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
56
58
  this.config = smartconfigInstance.dataFor<ITsrustConfig>('@git.zone/tsrust', { targets: [] });
57
59
  this.registerCommands();
@@ -62,6 +64,7 @@ export class TsRustCli {
62
64
  this.registerCleanCommand();
63
65
  this.registerPruneCommand();
64
66
  this.registerAssembleCommand();
67
+ this.registerMatrixCommand();
65
68
  this.registerInspectCommand();
66
69
  }
67
70
 
@@ -93,7 +96,9 @@ export class TsRustCli {
93
96
  projectName,
94
97
  projectVersion,
95
98
  gitCommit: gitSnapshotArg.commit,
96
- gitDirty: gitSnapshotArg.available ? gitSnapshotArg.status.length > 0 : undefined,
99
+ gitDirty: gitSnapshotArg.available
100
+ ? gitSnapshotArg.status.length > 0 || (gitSnapshotArg.unsafeIndexFlags?.length || 0) > 0
101
+ : undefined,
97
102
  builtAt: new Date().toISOString(),
98
103
  tsrustVersion: commitinfo.version,
99
104
  };
@@ -464,7 +469,7 @@ export class TsRustCli {
464
469
  if (!gitSnapshot.available || gitSnapshot.commit === 'unknown') {
465
470
  throw new Error('Artifact assembly requires a Git checkout');
466
471
  }
467
- if (gitSnapshot.status.length > 0) {
472
+ if (gitSnapshot.status.length > 0 || (gitSnapshot.unsafeIndexFlags?.length || 0) > 0) {
468
473
  throw new Error('Artifact assembly requires a clean Git worktree');
469
474
  }
470
475
  const { projectName, projectVersion } = this.readProjectIdentity();
@@ -487,6 +492,33 @@ export class TsRustCli {
487
492
  });
488
493
  }
489
494
 
495
+ private registerMatrixCommand(): void {
496
+ this.cli.addCommand('matrix').subscribe(async (argvArg) => {
497
+ const operation = String((argvArg as any)._?.[1] || '');
498
+ if (operation !== 'check' && operation !== 'build') {
499
+ throw new Error('Usage: tsrust matrix <check|build>');
500
+ }
501
+ const matrixBuilder = new NativeMatrixBuilder({
502
+ workspace: this.cwd,
503
+ config: this.config,
504
+ });
505
+ if (operation === 'check') {
506
+ const result = await matrixBuilder.check();
507
+ console.log(
508
+ `Matrix builders are ready for ${result.targets.join(', ')} on ${result.workers.join(', ')}`,
509
+ );
510
+ return;
511
+ }
512
+ if (operation === 'build') {
513
+ const result = await matrixBuilder.build();
514
+ console.log(
515
+ `Built and assembled ${result.assembly.artifactCount} artifacts for ${result.targets.join(', ')}`,
516
+ );
517
+ return;
518
+ }
519
+ });
520
+ }
521
+
490
522
  private registerCleanCommand(): void {
491
523
  this.cli.addCommand('clean').subscribe(async (_argvArg) => {
492
524
  // Clean cargo build
@@ -1,3 +1,5 @@
1
+ import type { ITsrustMatrixConfig } from '../mod_matrix/helpers.matrixconfig.js';
2
+
1
3
  export const targetAliasMap: Record<string, string> = {
2
4
  linux_amd64: 'x86_64-unknown-linux-gnu',
3
5
  linux_arm64: 'aarch64-unknown-linux-gnu',
@@ -29,6 +31,7 @@ export interface ITsrustConfig {
29
31
  remapLocalPaths?: boolean;
30
32
  targetDir?: string;
31
33
  pruneAfterBuild?: boolean;
34
+ matrix?: ITsrustMatrixConfig;
32
35
  }
33
36
 
34
37
  export interface INormalizedTarget {