@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.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/mod_artifact/classes.artifactassembler.d.ts +6 -0
- package/dist_ts/mod_artifact/classes.artifactassembler.js +169 -6
- package/dist_ts/mod_artifact/index.d.ts +1 -1
- package/dist_ts/mod_artifact/index.js +2 -2
- package/dist_ts/mod_cargo/classes.cargorunner.js +8 -5
- package/dist_ts/mod_cli/classes.tsrustcli.d.ts +1 -0
- package/dist_ts/mod_cli/classes.tsrustcli.js +30 -3
- package/dist_ts/mod_cli/helpers.targets.d.ts +2 -0
- package/dist_ts/mod_cli/helpers.targets.js +1 -1
- package/dist_ts/mod_matrix/classes.matrixcommandrunner.d.ts +25 -0
- package/dist_ts/mod_matrix/classes.matrixcommandrunner.js +266 -0
- package/dist_ts/mod_matrix/classes.nativematrixbuilder.d.ts +64 -0
- package/dist_ts/mod_matrix/classes.nativematrixbuilder.js +1084 -0
- package/dist_ts/mod_matrix/helpers.matrixconfig.d.ts +30 -0
- package/dist_ts/mod_matrix/helpers.matrixconfig.js +129 -0
- package/dist_ts/mod_matrix/index.d.ts +3 -0
- package/dist_ts/mod_matrix/index.js +4 -0
- package/dist_ts/mod_provenance/classes.gitstate.d.ts +1 -0
- package/dist_ts/mod_provenance/classes.gitstate.js +13 -3
- package/dist_ts/mod_provenance/classes.provenancestore.d.ts +1 -0
- package/dist_ts/mod_provenance/classes.provenancestore.js +5 -1
- package/dist_ts/mod_provenance/index.d.ts +1 -1
- package/dist_ts/mod_provenance/index.js +2 -2
- package/dist_ts/mod_toolchain/classes.toolchainmanager.d.ts +6 -0
- package/dist_ts/mod_toolchain/classes.toolchainmanager.js +211 -40
- package/package.json +2 -2
- package/readme.hints.md +12 -1
- package/readme.md +69 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +1 -0
- package/ts/mod_artifact/classes.artifactassembler.ts +201 -5
- package/ts/mod_artifact/index.ts +2 -0
- package/ts/mod_cargo/classes.cargorunner.ts +9 -4
- package/ts/mod_cli/classes.tsrustcli.ts +34 -2
- package/ts/mod_cli/helpers.targets.ts +3 -0
- package/ts/mod_matrix/classes.matrixcommandrunner.ts +294 -0
- package/ts/mod_matrix/classes.nativematrixbuilder.ts +1472 -0
- package/ts/mod_matrix/helpers.matrixconfig.ts +222 -0
- package/ts/mod_matrix/index.ts +23 -0
- package/ts/mod_provenance/classes.gitstate.ts +13 -2
- package/ts/mod_provenance/classes.provenancestore.ts +4 -0
- package/ts/mod_provenance/index.ts +1 -0
- 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
|
|
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
|
-
|
|
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,
|
package/ts/mod_artifact/index.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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 {
|