@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/mod_artifact/classes.artifactassembler.d.ts +48 -0
- package/dist_ts/mod_artifact/classes.artifactassembler.js +842 -0
- package/dist_ts/mod_artifact/index.d.ts +1 -0
- package/dist_ts/mod_artifact/index.js +2 -0
- package/dist_ts/mod_cargo/classes.cargorunner.d.ts +10 -8
- package/dist_ts/mod_cargo/classes.cargorunner.js +4 -3
- package/dist_ts/mod_cargo/index.d.ts +1 -1
- package/dist_ts/mod_cargo/index.js +2 -2
- package/dist_ts/mod_cli/classes.tsrustcli.d.ts +5 -2
- package/dist_ts/mod_cli/classes.tsrustcli.js +112 -75
- package/dist_ts/mod_cli/helpers.targets.d.ts +20 -0
- package/dist_ts/mod_cli/helpers.targets.js +106 -0
- package/dist_ts/mod_cli/index.d.ts +1 -0
- package/dist_ts/mod_cli/index.js +2 -1
- package/dist_ts/mod_elf/classes.provenance.d.ts +2 -1
- package/dist_ts/mod_elf/classes.provenance.js +0 -0
- package/dist_ts/mod_provenance/classes.gitstate.d.ts +7 -0
- package/dist_ts/mod_provenance/classes.gitstate.js +34 -0
- package/dist_ts/mod_provenance/classes.provenancestore.d.ts +11 -0
- package/dist_ts/mod_provenance/classes.provenancestore.js +184 -0
- package/dist_ts/mod_provenance/helpers.owneridentity.d.ts +2 -0
- package/dist_ts/mod_provenance/helpers.owneridentity.js +57 -0
- package/dist_ts/mod_provenance/index.d.ts +3 -0
- package/dist_ts/mod_provenance/index.js +4 -0
- package/package.json +7 -7
- package/readme.hints.md +6 -3
- package/readme.md +80 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +2 -0
- package/ts/mod_artifact/classes.artifactassembler.ts +1028 -0
- package/ts/mod_artifact/index.ts +6 -0
- package/ts/mod_cargo/classes.cargorunner.ts +14 -3
- package/ts/mod_cargo/index.ts +1 -1
- package/ts/mod_cli/classes.tsrustcli.ts +164 -94
- package/ts/mod_cli/helpers.targets.ts +141 -0
- package/ts/mod_cli/index.ts +10 -0
- package/ts/mod_elf/classes.provenance.ts +0 -0
- package/ts/mod_provenance/classes.gitstate.ts +49 -0
- package/ts/mod_provenance/classes.provenancestore.ts +194 -0
- package/ts/mod_provenance/helpers.owneridentity.ts +57 -0
- package/ts/mod_provenance/index.ts +10 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as childProcess from 'child_process';
|
|
2
|
+
import * as util from 'util';
|
|
3
|
+
|
|
4
|
+
const execFile = util.promisify(childProcess.execFile);
|
|
5
|
+
|
|
6
|
+
export interface IGitSnapshot {
|
|
7
|
+
available: boolean;
|
|
8
|
+
commit: string;
|
|
9
|
+
status: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function captureGitSnapshot(cwdArg: string): Promise<IGitSnapshot> {
|
|
13
|
+
let commit: string;
|
|
14
|
+
try {
|
|
15
|
+
const result = await execFile('git', ['-C', cwdArg, 'rev-parse', 'HEAD'], {
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
});
|
|
18
|
+
commit = result.stdout.trim();
|
|
19
|
+
} catch {
|
|
20
|
+
return { available: false, commit: 'unknown', status: '' };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const status = await execFile(
|
|
24
|
+
'git',
|
|
25
|
+
['-C', cwdArg, 'status', '--porcelain=v1', '--untracked-files=all'],
|
|
26
|
+
{ encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 },
|
|
27
|
+
);
|
|
28
|
+
return {
|
|
29
|
+
available: true,
|
|
30
|
+
commit,
|
|
31
|
+
status: status.stdout,
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error('Failed to inspect Git worktree state');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function assertGitSnapshotUnchanged(
|
|
39
|
+
beforeArg: IGitSnapshot,
|
|
40
|
+
afterArg: IGitSnapshot,
|
|
41
|
+
): void {
|
|
42
|
+
if (
|
|
43
|
+
beforeArg.available !== afterArg.available ||
|
|
44
|
+
beforeArg.commit !== afterArg.commit ||
|
|
45
|
+
beforeArg.status !== afterArg.status
|
|
46
|
+
) {
|
|
47
|
+
throw new Error('Git source state changed while Cargo was building');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import type { ITsrustBuildInfo } from '../mod_elf/index.js';
|
|
5
|
+
import { ProvenanceStamper } from '../mod_elf/index.js';
|
|
6
|
+
import { getHostIdentity, isLocalProcessRunning } from './helpers.owneridentity.js';
|
|
7
|
+
|
|
8
|
+
const SIDECAR_FORMAT = 'tsrust.build-provenance.v2';
|
|
9
|
+
export const PROVENANCE_SIDECAR_SUFFIX = '.tsrust-build.json';
|
|
10
|
+
|
|
11
|
+
interface IProvenanceSidecar {
|
|
12
|
+
format: typeof SIDECAR_FORMAT;
|
|
13
|
+
binarySha256: string;
|
|
14
|
+
buildInfo: ITsrustBuildInfo;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isBuildInfo(valueArg: unknown): valueArg is ITsrustBuildInfo {
|
|
18
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) return false;
|
|
19
|
+
const value = valueArg as Record<string, unknown>;
|
|
20
|
+
return (
|
|
21
|
+
typeof value.binary === 'string' &&
|
|
22
|
+
typeof value.target === 'string' &&
|
|
23
|
+
typeof value.projectName === 'string' &&
|
|
24
|
+
typeof value.projectVersion === 'string' &&
|
|
25
|
+
typeof value.gitCommit === 'string' &&
|
|
26
|
+
typeof value.builtAt === 'string' &&
|
|
27
|
+
typeof value.tsrustVersion === 'string' &&
|
|
28
|
+
(value.gitDirty === undefined || typeof value.gitDirty === 'boolean')
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ProvenanceStore {
|
|
33
|
+
public static sidecarPath(binaryPathArg: string): string {
|
|
34
|
+
return `${binaryPathArg}${PROVENANCE_SIDECAR_SUFFIX}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public static async sha256(binaryPathArg: string): Promise<string> {
|
|
38
|
+
const binary = await fs.promises.lstat(binaryPathArg);
|
|
39
|
+
if (!binary.isFile() || binary.isSymbolicLink()) {
|
|
40
|
+
throw new Error(`Provenance binary is not a regular file: ${binaryPathArg}`);
|
|
41
|
+
}
|
|
42
|
+
const digest = crypto.createHash('sha256');
|
|
43
|
+
await new Promise<void>((resolve, reject) => {
|
|
44
|
+
const stream = fs.createReadStream(binaryPathArg);
|
|
45
|
+
stream.on('data', (chunk) => digest.update(chunk));
|
|
46
|
+
stream.on('error', reject);
|
|
47
|
+
stream.on('end', resolve);
|
|
48
|
+
});
|
|
49
|
+
return digest.digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static async write(binaryPathArg: string, infoArg: ITsrustBuildInfo): Promise<string> {
|
|
53
|
+
if (!isBuildInfo(infoArg)) {
|
|
54
|
+
throw new Error('Provenance build information has an invalid shape');
|
|
55
|
+
}
|
|
56
|
+
const binary = await fs.promises.lstat(binaryPathArg);
|
|
57
|
+
if (!binary.isFile() || binary.isSymbolicLink()) {
|
|
58
|
+
throw new Error(`Provenance binary is not a regular file: ${binaryPathArg}`);
|
|
59
|
+
}
|
|
60
|
+
const sidecarPath = ProvenanceStore.sidecarPath(binaryPathArg);
|
|
61
|
+
await ProvenanceStore.recoverTemporaryFiles(sidecarPath);
|
|
62
|
+
const existing = await ProvenanceStore.readExistingSidecar(binaryPathArg);
|
|
63
|
+
if (existing) {
|
|
64
|
+
if (JSON.stringify(existing) === JSON.stringify(infoArg)) return sidecarPath;
|
|
65
|
+
throw new Error(`Provenance sidecar already exists with different build information: ${sidecarPath}`);
|
|
66
|
+
}
|
|
67
|
+
const record: IProvenanceSidecar = {
|
|
68
|
+
format: SIDECAR_FORMAT,
|
|
69
|
+
binarySha256: await ProvenanceStore.sha256(binaryPathArg),
|
|
70
|
+
buildInfo: infoArg,
|
|
71
|
+
};
|
|
72
|
+
const temporaryPath = `${sidecarPath}.${getHostIdentity()}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
73
|
+
let temporary: fs.promises.FileHandle | undefined;
|
|
74
|
+
try {
|
|
75
|
+
temporary = await fs.promises.open(temporaryPath, 'wx', 0o600);
|
|
76
|
+
await temporary.writeFile(`${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
77
|
+
await temporary.chmod(0o644);
|
|
78
|
+
await temporary.sync();
|
|
79
|
+
await temporary.close();
|
|
80
|
+
temporary = undefined;
|
|
81
|
+
try {
|
|
82
|
+
await fs.promises.link(temporaryPath, sidecarPath);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
85
|
+
const concurrentlyWritten = await ProvenanceStore.readSidecar(binaryPathArg);
|
|
86
|
+
if (JSON.stringify(concurrentlyWritten) !== JSON.stringify(infoArg)) {
|
|
87
|
+
throw new Error(`Concurrent provenance write disagreed for ${binaryPathArg}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
await fs.promises.unlink(temporaryPath);
|
|
91
|
+
const directory = await fs.promises.open(path.dirname(sidecarPath), 'r');
|
|
92
|
+
try {
|
|
93
|
+
await directory.sync();
|
|
94
|
+
} finally {
|
|
95
|
+
await directory.close();
|
|
96
|
+
}
|
|
97
|
+
return sidecarPath;
|
|
98
|
+
} finally {
|
|
99
|
+
await Promise.allSettled([
|
|
100
|
+
temporary?.close() || Promise.resolve(),
|
|
101
|
+
fs.promises.rm(temporaryPath, { force: true }),
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private static async readExistingSidecar(
|
|
107
|
+
binaryPathArg: string,
|
|
108
|
+
): Promise<ITsrustBuildInfo | undefined> {
|
|
109
|
+
try {
|
|
110
|
+
return await ProvenanceStore.readSidecar(binaryPathArg);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private static async recoverTemporaryFiles(sidecarPathArg: string): Promise<void> {
|
|
118
|
+
const directoryPath = path.dirname(sidecarPathArg);
|
|
119
|
+
const temporaryPrefix = `${path.basename(sidecarPathArg)}.`;
|
|
120
|
+
const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true });
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (!entry.name.startsWith(temporaryPrefix) || !entry.name.endsWith('.tmp')) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!entry.isFile()) continue;
|
|
126
|
+
const suffix = entry.name.slice(temporaryPrefix.length, -'.tmp'.length);
|
|
127
|
+
const [hostIdentity, pidText, identifier, ...unexpected] = suffix.split('.');
|
|
128
|
+
const pid = Number(pidText);
|
|
129
|
+
if (
|
|
130
|
+
unexpected.length > 0 ||
|
|
131
|
+
!/^[a-f0-9]{64}$/.test(hostIdentity || '') ||
|
|
132
|
+
!Number.isSafeInteger(pid) ||
|
|
133
|
+
!/^[a-f0-9-]{36}$/.test(identifier || '') ||
|
|
134
|
+
hostIdentity !== getHostIdentity() ||
|
|
135
|
+
isLocalProcessRunning(pid)
|
|
136
|
+
) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const temporaryPath = path.join(directoryPath, entry.name);
|
|
140
|
+
try {
|
|
141
|
+
const temporary = await fs.promises.lstat(temporaryPath);
|
|
142
|
+
if (temporary.isFile() && !temporary.isSymbolicLink()) {
|
|
143
|
+
await fs.promises.rm(temporaryPath, { force: true });
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public static async readSidecar(binaryPathArg: string): Promise<ITsrustBuildInfo> {
|
|
152
|
+
const sidecarPath = ProvenanceStore.sidecarPath(binaryPathArg);
|
|
153
|
+
const sidecar = await fs.promises.lstat(sidecarPath);
|
|
154
|
+
if (!sidecar.isFile() || sidecar.isSymbolicLink()) {
|
|
155
|
+
throw new Error(`Provenance sidecar is not a regular file: ${sidecarPath}`);
|
|
156
|
+
}
|
|
157
|
+
let parsed: unknown;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(await fs.promises.readFile(sidecarPath, 'utf8'));
|
|
160
|
+
} catch {
|
|
161
|
+
throw new Error(`Provenance sidecar is invalid JSON: ${sidecarPath}`);
|
|
162
|
+
}
|
|
163
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
164
|
+
throw new Error(`Provenance sidecar has an invalid shape: ${sidecarPath}`);
|
|
165
|
+
}
|
|
166
|
+
const record = parsed as Partial<IProvenanceSidecar>;
|
|
167
|
+
if (
|
|
168
|
+
record.format !== SIDECAR_FORMAT ||
|
|
169
|
+
typeof record.binarySha256 !== 'string' ||
|
|
170
|
+
!/^[a-f0-9]{64}$/.test(record.binarySha256) ||
|
|
171
|
+
!isBuildInfo(record.buildInfo)
|
|
172
|
+
) {
|
|
173
|
+
throw new Error(`Provenance sidecar has an unsupported shape: ${sidecarPath}`);
|
|
174
|
+
}
|
|
175
|
+
const actualSha256 = await ProvenanceStore.sha256(binaryPathArg);
|
|
176
|
+
if (actualSha256 !== record.binarySha256) {
|
|
177
|
+
throw new Error(`Provenance sidecar hash does not match ${binaryPathArg}`);
|
|
178
|
+
}
|
|
179
|
+
return record.buildInfo;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public static async read(binaryPathArg: string): Promise<ITsrustBuildInfo | undefined> {
|
|
183
|
+
const sidecarPath = ProvenanceStore.sidecarPath(binaryPathArg);
|
|
184
|
+
try {
|
|
185
|
+
await fs.promises.lstat(sidecarPath);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
188
|
+
return ProvenanceStamper.read(binaryPathArg);
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
return ProvenanceStore.readSidecar(binaryPathArg);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as childProcess from 'child_process';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
|
|
6
|
+
let cachedHostIdentity: string | undefined;
|
|
7
|
+
|
|
8
|
+
function readBootIdentity(): string {
|
|
9
|
+
if (process.platform === 'linux') {
|
|
10
|
+
try {
|
|
11
|
+
return fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
|
|
12
|
+
} catch {
|
|
13
|
+
// Fall through to the portable host fingerprint.
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (process.platform === 'darwin') {
|
|
17
|
+
try {
|
|
18
|
+
return childProcess.execFileSync('/usr/sbin/sysctl', ['-n', 'kern.boottime'], {
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
}).trim();
|
|
21
|
+
} catch {
|
|
22
|
+
// Fall through to the portable host fingerprint.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return String(Math.round((Date.now() - os.uptime() * 1000) / 60_000));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getHostIdentity(): string {
|
|
29
|
+
if (cachedHostIdentity) return cachedHostIdentity;
|
|
30
|
+
const macAddresses = Object.values(os.networkInterfaces())
|
|
31
|
+
.flat()
|
|
32
|
+
.map((address) => address?.mac.toLowerCase())
|
|
33
|
+
.filter((address): address is string => !!address && address !== '00:00:00:00:00:00')
|
|
34
|
+
.sort();
|
|
35
|
+
cachedHostIdentity = crypto
|
|
36
|
+
.createHash('sha256')
|
|
37
|
+
.update(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
platform: process.platform,
|
|
40
|
+
hostname: os.hostname(),
|
|
41
|
+
bootIdentity: readBootIdentity(),
|
|
42
|
+
macAddresses,
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
.digest('hex');
|
|
46
|
+
return cachedHostIdentity;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isLocalProcessRunning(pidArg: number): boolean {
|
|
50
|
+
if (!Number.isSafeInteger(pidArg) || pidArg <= 0) return false;
|
|
51
|
+
try {
|
|
52
|
+
process.kill(pidArg, 0);
|
|
53
|
+
return true;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return (error as NodeJS.ErrnoException).code !== 'ESRCH';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export {
|
|
2
|
+
captureGitSnapshot,
|
|
3
|
+
assertGitSnapshotUnchanged,
|
|
4
|
+
type IGitSnapshot,
|
|
5
|
+
} from './classes.gitstate.js';
|
|
6
|
+
export {
|
|
7
|
+
ProvenanceStore,
|
|
8
|
+
PROVENANCE_SIDECAR_SUFFIX,
|
|
9
|
+
} from './classes.provenancestore.js';
|
|
10
|
+
export { getHostIdentity, isLocalProcessRunning } from './helpers.owneridentity.js';
|