@dbx-tools/projen 0.9.12 → 0.9.18
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/README.md +33 -25
- package/index.ts +5 -4
- package/package.json +4 -5
- package/src/_release-platform.ts +10 -0
- package/src/_rust-project.ts +40 -13
- package/src/_rust-release-workflow.ts +213 -344
- package/src/openapi.ts +191 -79
- package/src/project-js.ts +8 -13
- package/src/project-py.ts +22 -3
- package/src/project-rs.ts +50 -6
- package/src/release-github.ts +23 -0
- package/src/release-plan.ts +48 -15
- package/src/release.ts +17 -3
- package/src/watch.ts +19 -2
- package/src/workspace-lock.ts +32 -6
- package/tasks/barrels.ts +47 -30
- package/tasks/compile-workspace.ts +121 -0
- package/tasks/openapi.ts +15 -11
- package/tasks/release-assets.ts +476 -0
- package/tasks/release-pr.ts +123 -14
- package/tasks/rust.ts +62 -33
- package/tasks/uniffi-python.js +24 -12
- package/tasks/uniffi-release.mjs +40 -3
- package/tasks/uniffi.ts +2 -0
- package/src/_rust-release.ts +0 -605
- package/tasks/build-rust-release.ts +0 -68
- package/tasks/rust-release.d.ts +0 -41
- package/tasks/rust-release.mjs +0 -1717
package/src/_rust-release.ts
DELETED
|
@@ -1,605 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Version-independent Rust build fingerprints and format-aware release stamping.
|
|
3
|
-
*
|
|
4
|
-
* This is the private owning TypeScript source. `tasks/build-rust-release.ts` bundles it
|
|
5
|
-
* with `smol-toml` and the shared process helper into the standalone Node
|
|
6
|
-
* artifact copied to consumer `.projen/` directories.
|
|
7
|
-
*/
|
|
8
|
-
import { createHash } from "node:crypto";
|
|
9
|
-
import {
|
|
10
|
-
chmodSync,
|
|
11
|
-
closeSync,
|
|
12
|
-
existsSync,
|
|
13
|
-
lstatSync,
|
|
14
|
-
mkdirSync,
|
|
15
|
-
mkdtempSync,
|
|
16
|
-
openSync,
|
|
17
|
-
readFileSync,
|
|
18
|
-
readSync,
|
|
19
|
-
readlinkSync,
|
|
20
|
-
readdirSync,
|
|
21
|
-
rmSync,
|
|
22
|
-
statSync,
|
|
23
|
-
writeFileSync,
|
|
24
|
-
} from "node:fs";
|
|
25
|
-
import { tmpdir } from "node:os";
|
|
26
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
27
|
-
import { fileURLToPath } from "node:url";
|
|
28
|
-
import { parseArgs } from "node:util";
|
|
29
|
-
import * as exec from "@dbx-tools/core/exec";
|
|
30
|
-
import { parse, stringify, type TomlTable } from "smol-toml";
|
|
31
|
-
|
|
32
|
-
export const FINGERPRINT_SCHEMA = 3;
|
|
33
|
-
export const VERSION_SLOT_SCHEMA = 1;
|
|
34
|
-
export const VERSION_CAPACITY = 64;
|
|
35
|
-
|
|
36
|
-
const MAGIC = Buffer.from("DBXVERSION\0\0", "binary");
|
|
37
|
-
const RECORD_SIZE = 128;
|
|
38
|
-
const VERSION_OFFSET = 16;
|
|
39
|
-
const EMPTY_RECORD_OFFSET = 80;
|
|
40
|
-
const MACH_O_MAGICS = new Set([
|
|
41
|
-
"feedface",
|
|
42
|
-
"cefaedfe",
|
|
43
|
-
"feedfacf",
|
|
44
|
-
"cffaedfe",
|
|
45
|
-
"cafebabe",
|
|
46
|
-
"bebafeca",
|
|
47
|
-
"cafebabf",
|
|
48
|
-
"bfbafeca",
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
interface CommandOptions {
|
|
52
|
-
readonly cwd?: string;
|
|
53
|
-
readonly inherit?: boolean;
|
|
54
|
-
readonly check?: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function command(
|
|
58
|
-
commandName: string,
|
|
59
|
-
args: readonly string[],
|
|
60
|
-
options: CommandOptions = {},
|
|
61
|
-
): string {
|
|
62
|
-
const result = exec.spawnSync(commandName, [...args], {
|
|
63
|
-
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
64
|
-
stdout: options.inherit ? "inherit" : "capture",
|
|
65
|
-
stderr: options.inherit ? "inherit" : "capture",
|
|
66
|
-
stdin: "ignore",
|
|
67
|
-
check: options.check ?? true,
|
|
68
|
-
});
|
|
69
|
-
return result.stdout;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function record(value: unknown): value is Record<string, unknown> {
|
|
73
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function removeVersionOnlyFields(value: unknown, key?: string): void {
|
|
77
|
-
if (Array.isArray(value)) {
|
|
78
|
-
for (const item of value) removeVersionOnlyFields(item);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (!record(value)) return;
|
|
82
|
-
if (typeof value.path === "string") delete value.version;
|
|
83
|
-
if (key === "package") delete value.version;
|
|
84
|
-
for (const [childKey, child] of Object.entries(value)) {
|
|
85
|
-
removeVersionOnlyFields(child, childKey);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Canonical TOML representation with workspace-owned release versions removed. */
|
|
90
|
-
export function normalizeManifest(text: string): string {
|
|
91
|
-
const document = parse(text) as TomlTable;
|
|
92
|
-
const packageTable = document.package;
|
|
93
|
-
if (record(packageTable)) delete packageTable.version;
|
|
94
|
-
const workspace = document.workspace;
|
|
95
|
-
if (record(workspace) && record(workspace.package)) delete workspace.package.version;
|
|
96
|
-
removeVersionOnlyFields(document);
|
|
97
|
-
return stringify(document);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Canonical Cargo.lock representation with local package versions normalized. */
|
|
101
|
-
export function normalizeLockfile(
|
|
102
|
-
text: string,
|
|
103
|
-
workspaceVersion: string,
|
|
104
|
-
packageNames: ReadonlySet<string> = new Set(),
|
|
105
|
-
): string {
|
|
106
|
-
const document = parse(text) as TomlTable;
|
|
107
|
-
const packages = document.package;
|
|
108
|
-
if (Array.isArray(packages)) {
|
|
109
|
-
for (const candidate of packages) {
|
|
110
|
-
if (!record(candidate) || candidate.source !== undefined) continue;
|
|
111
|
-
if (
|
|
112
|
-
(typeof candidate.name === "string" && packageNames.has(candidate.name)) ||
|
|
113
|
-
candidate.version === workspaceVersion
|
|
114
|
-
) {
|
|
115
|
-
candidate.version = "<workspace>";
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return stringify(document);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface CargoMetadataPackage {
|
|
123
|
-
readonly id: string;
|
|
124
|
-
readonly name: string;
|
|
125
|
-
readonly manifest_path: string;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
interface CargoMetadata {
|
|
129
|
-
readonly packages: readonly CargoMetadataPackage[];
|
|
130
|
-
readonly workspace_members: readonly string[];
|
|
131
|
-
readonly workspace_root: string;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function cargoMetadata(root: string): CargoMetadata {
|
|
135
|
-
const parsed = JSON.parse(
|
|
136
|
-
command("cargo", ["metadata", "--format-version", "1", "--no-deps"], { cwd: root }),
|
|
137
|
-
) as Partial<CargoMetadata>;
|
|
138
|
-
if (
|
|
139
|
-
!Array.isArray(parsed.packages) ||
|
|
140
|
-
!Array.isArray(parsed.workspace_members) ||
|
|
141
|
-
typeof parsed.workspace_root !== "string"
|
|
142
|
-
) {
|
|
143
|
-
throw new Error("cargo metadata returned an incomplete workspace description");
|
|
144
|
-
}
|
|
145
|
-
return parsed as CargoMetadata;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function workspaceVersion(root: string): string {
|
|
149
|
-
const versionFile = join(root, "VERSION");
|
|
150
|
-
if (existsSync(versionFile)) return readFileSync(versionFile, "utf8").trim();
|
|
151
|
-
const document = parse(readFileSync(join(root, "Cargo.toml"), "utf8")) as TomlTable;
|
|
152
|
-
const workspace = document.workspace;
|
|
153
|
-
return record(workspace) &&
|
|
154
|
-
record(workspace.package) &&
|
|
155
|
-
typeof workspace.package.version === "string"
|
|
156
|
-
? workspace.package.version
|
|
157
|
-
: "";
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function toPosix(path: string): string {
|
|
161
|
-
return path.split(sep).join("/");
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function sourceRoots(root: string, metadata: CargoMetadata, sources: readonly string[]): string[] {
|
|
165
|
-
const members = new Set(metadata.workspace_members);
|
|
166
|
-
const discovered = metadata.packages
|
|
167
|
-
.filter((candidate) => members.has(candidate.id))
|
|
168
|
-
.map((candidate) => dirname(candidate.manifest_path));
|
|
169
|
-
return [...new Set([...discovered, ...sources.map((source) => resolve(root, source))])]
|
|
170
|
-
.map((path) => toPosix(relative(root, path)) || ".")
|
|
171
|
-
.sort();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function ignoredInput(path: string): boolean {
|
|
175
|
-
const segments = path.split("/");
|
|
176
|
-
return segments.some(
|
|
177
|
-
(segment) => segment === ".git" || segment === "node_modules" || segment === "target",
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function walkFiles(root: string, path: string, output: string[]): void {
|
|
182
|
-
const absolute = resolve(root, path);
|
|
183
|
-
if (!existsSync(absolute)) return;
|
|
184
|
-
const stat = lstatSync(absolute);
|
|
185
|
-
if (!stat.isDirectory()) {
|
|
186
|
-
output.push(toPosix(relative(root, absolute)));
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
for (const entry of readdirSync(absolute, { withFileTypes: true })) {
|
|
190
|
-
const child = toPosix(relative(root, join(absolute, entry.name)));
|
|
191
|
-
if (ignoredInput(child)) continue;
|
|
192
|
-
if (entry.isDirectory()) walkFiles(root, child, output);
|
|
193
|
-
else output.push(child);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function inputFiles(root: string, roots: readonly string[]): string[] {
|
|
198
|
-
const pathspecs = [
|
|
199
|
-
"Cargo.toml",
|
|
200
|
-
"Cargo.lock",
|
|
201
|
-
".cargo",
|
|
202
|
-
"rust-toolchain",
|
|
203
|
-
"rust-toolchain.toml",
|
|
204
|
-
...roots,
|
|
205
|
-
];
|
|
206
|
-
const git = exec.spawnSync(
|
|
207
|
-
"git",
|
|
208
|
-
["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", ...pathspecs],
|
|
209
|
-
{
|
|
210
|
-
cwd: root,
|
|
211
|
-
stdout: "capture",
|
|
212
|
-
stderr: "ignore",
|
|
213
|
-
stdin: "ignore",
|
|
214
|
-
check: false,
|
|
215
|
-
trim: false,
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
const files =
|
|
219
|
-
git.exitCode === 0
|
|
220
|
-
? git.stdout.split("\0").filter(Boolean)
|
|
221
|
-
: pathspecs.flatMap((path) => {
|
|
222
|
-
const found: string[] = [];
|
|
223
|
-
walkFiles(root, path, found);
|
|
224
|
-
return found;
|
|
225
|
-
});
|
|
226
|
-
return [...new Set(files.map(toPosix))]
|
|
227
|
-
.filter((file) => !ignoredInput(file) && existsSync(join(root, file)))
|
|
228
|
-
.sort();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function normalizedInput(
|
|
232
|
-
root: string,
|
|
233
|
-
file: string,
|
|
234
|
-
content: Buffer,
|
|
235
|
-
packageNames: ReadonlySet<string>,
|
|
236
|
-
): Buffer {
|
|
237
|
-
if (file.endsWith("Cargo.toml")) return Buffer.from(normalizeManifest(content.toString("utf8")));
|
|
238
|
-
if (file === "Cargo.lock") {
|
|
239
|
-
return Buffer.from(
|
|
240
|
-
normalizeLockfile(content.toString("utf8"), workspaceVersion(root), packageNames),
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
return content;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Hash Cargo-discovered members and every tracked or unignored file beneath them. */
|
|
247
|
-
export function sourceHash(root: string, sources: readonly string[] = []): string {
|
|
248
|
-
const resolvedRoot = resolve(root);
|
|
249
|
-
const metadata = cargoMetadata(resolvedRoot);
|
|
250
|
-
const members = new Set(metadata.workspace_members);
|
|
251
|
-
const packageNames = new Set(
|
|
252
|
-
metadata.packages
|
|
253
|
-
.filter((candidate) => members.has(candidate.id))
|
|
254
|
-
.map((candidate) => candidate.name),
|
|
255
|
-
);
|
|
256
|
-
const hash = createHash("sha256");
|
|
257
|
-
for (const file of inputFiles(resolvedRoot, sourceRoots(resolvedRoot, metadata, sources))) {
|
|
258
|
-
const absolute = join(resolvedRoot, file);
|
|
259
|
-
const stat = lstatSync(absolute);
|
|
260
|
-
hash.update(file);
|
|
261
|
-
hash.update(Buffer.from([0]));
|
|
262
|
-
hash.update(String(stat.mode & 0o111));
|
|
263
|
-
hash.update(Buffer.from([0]));
|
|
264
|
-
hash.update(
|
|
265
|
-
stat.isSymbolicLink()
|
|
266
|
-
? Buffer.from(readlinkSync(absolute))
|
|
267
|
-
: normalizedInput(resolvedRoot, file, readFileSync(absolute), packageNames),
|
|
268
|
-
);
|
|
269
|
-
hash.update(Buffer.from([0]));
|
|
270
|
-
}
|
|
271
|
-
return hash.digest("hex");
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function linkerIdentity(target: string): string {
|
|
275
|
-
if (target.includes("windows-msvc")) return "rust-lld";
|
|
276
|
-
if (target.includes("linux")) return "system-linux-linker";
|
|
277
|
-
if (target.includes("apple-darwin")) return "apple-ld";
|
|
278
|
-
return "system-linker";
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export interface TargetKeyOptions {
|
|
282
|
-
readonly rustSourceHash: string;
|
|
283
|
-
readonly target: string;
|
|
284
|
-
readonly targetConfig?: string;
|
|
285
|
-
readonly toolchain?: string;
|
|
286
|
-
readonly rustc?: string;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function targetKey({
|
|
290
|
-
rustSourceHash,
|
|
291
|
-
target,
|
|
292
|
-
targetConfig = "",
|
|
293
|
-
toolchain = "stable",
|
|
294
|
-
rustc,
|
|
295
|
-
}: TargetKeyOptions): string {
|
|
296
|
-
const hash = createHash("sha256");
|
|
297
|
-
for (const value of [
|
|
298
|
-
String(FINGERPRINT_SCHEMA),
|
|
299
|
-
String(VERSION_SLOT_SCHEMA),
|
|
300
|
-
rustSourceHash,
|
|
301
|
-
target,
|
|
302
|
-
targetConfig,
|
|
303
|
-
toolchain,
|
|
304
|
-
rustc ?? "<portable>",
|
|
305
|
-
linkerIdentity(target),
|
|
306
|
-
"release",
|
|
307
|
-
"raw-target-release-v2",
|
|
308
|
-
]) {
|
|
309
|
-
hash.update(value);
|
|
310
|
-
hash.update(Buffer.from([0]));
|
|
311
|
-
}
|
|
312
|
-
return hash.digest("hex");
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function rustcIdentity(): string {
|
|
316
|
-
return command("rustc", ["--version", "--verbose"])
|
|
317
|
-
.split(/\r?\n/)
|
|
318
|
-
.filter((line) => line && !line.startsWith("host: "))
|
|
319
|
-
.join("\n");
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
export interface FingerprintOptions {
|
|
323
|
-
readonly root?: string;
|
|
324
|
-
readonly output?: string;
|
|
325
|
-
readonly check?: boolean;
|
|
326
|
-
readonly targets: readonly string[];
|
|
327
|
-
readonly toolchain?: string;
|
|
328
|
-
readonly portable?: boolean;
|
|
329
|
-
readonly sources?: readonly string[];
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
export interface RustBuildFingerprint {
|
|
333
|
-
readonly schemaVersion: number;
|
|
334
|
-
readonly versionSlotSchema: number;
|
|
335
|
-
readonly rustSourceHash: string;
|
|
336
|
-
readonly targets: Readonly<Record<string, string>>;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export function fingerprint({
|
|
340
|
-
root = process.cwd(),
|
|
341
|
-
output = ".release/rust-build.json",
|
|
342
|
-
check = false,
|
|
343
|
-
targets,
|
|
344
|
-
toolchain = "stable",
|
|
345
|
-
portable = false,
|
|
346
|
-
sources = [],
|
|
347
|
-
}: FingerprintOptions): RustBuildFingerprint {
|
|
348
|
-
if (!targets.length) throw new Error("at least one --target is required");
|
|
349
|
-
const resolvedRoot = resolve(root);
|
|
350
|
-
const rustSourceHash = sourceHash(resolvedRoot, sources);
|
|
351
|
-
const rustc = portable ? undefined : rustcIdentity();
|
|
352
|
-
const targetEntries = targets
|
|
353
|
-
.map((specification) => {
|
|
354
|
-
const separator = specification.indexOf("|");
|
|
355
|
-
const target = separator < 0 ? specification : specification.slice(0, separator);
|
|
356
|
-
const targetConfig = separator < 0 ? "" : specification.slice(separator + 1);
|
|
357
|
-
return [
|
|
358
|
-
target,
|
|
359
|
-
targetKey({ rustSourceHash, target, targetConfig, toolchain, rustc }),
|
|
360
|
-
] as const;
|
|
361
|
-
})
|
|
362
|
-
.sort(([left], [right]) => left.localeCompare(right));
|
|
363
|
-
const manifest = {
|
|
364
|
-
schemaVersion: FINGERPRINT_SCHEMA,
|
|
365
|
-
versionSlotSchema: VERSION_SLOT_SCHEMA,
|
|
366
|
-
rustSourceHash,
|
|
367
|
-
targets: Object.fromEntries(targetEntries),
|
|
368
|
-
};
|
|
369
|
-
const outputPath = resolve(resolvedRoot, output);
|
|
370
|
-
if (check) {
|
|
371
|
-
const current = JSON.parse(readFileSync(outputPath, "utf8")) as Partial<RustBuildFingerprint>;
|
|
372
|
-
const targetsMatch = targetEntries.every(([target, key]) => current.targets?.[target] === key);
|
|
373
|
-
if (
|
|
374
|
-
current.schemaVersion !== manifest.schemaVersion ||
|
|
375
|
-
current.versionSlotSchema !== manifest.versionSlotSchema ||
|
|
376
|
-
current.rustSourceHash !== manifest.rustSourceHash ||
|
|
377
|
-
!targetsMatch
|
|
378
|
-
) {
|
|
379
|
-
throw new Error(`${outputPath} does not match current Rust inputs`);
|
|
380
|
-
}
|
|
381
|
-
} else {
|
|
382
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
383
|
-
writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
384
|
-
}
|
|
385
|
-
return manifest;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
type BinaryFormat = "elf" | "macho" | "pe";
|
|
389
|
-
|
|
390
|
-
const SECTION_BY_FORMAT: Readonly<Record<BinaryFormat, string>> = {
|
|
391
|
-
elf: ".dbxversion",
|
|
392
|
-
macho: "__DATA,__dbxver",
|
|
393
|
-
pe: ".dbxver",
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
function binaryFormat(data: Buffer): BinaryFormat | undefined {
|
|
397
|
-
if (data.length >= 4 && data.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))) {
|
|
398
|
-
return "elf";
|
|
399
|
-
}
|
|
400
|
-
if (data.length >= 2 && data.subarray(0, 2).toString("ascii") === "MZ") return "pe";
|
|
401
|
-
if (data.length >= 4 && MACH_O_MAGICS.has(data.subarray(0, 4).toString("hex"))) return "macho";
|
|
402
|
-
return undefined;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function binaryHeader(path: string): Buffer {
|
|
406
|
-
const descriptor = openSync(path, "r");
|
|
407
|
-
try {
|
|
408
|
-
const header = Buffer.alloc(4);
|
|
409
|
-
return header.subarray(0, readSync(descriptor, header, 0, header.length, 0));
|
|
410
|
-
} finally {
|
|
411
|
-
closeSync(descriptor);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function executableOnPath(name: string): boolean {
|
|
416
|
-
return (
|
|
417
|
-
exec.spawnSync(process.platform === "win32" ? "where" : "which", [name], {
|
|
418
|
-
stdout: "ignore",
|
|
419
|
-
stderr: "ignore",
|
|
420
|
-
stdin: "ignore",
|
|
421
|
-
check: false,
|
|
422
|
-
}).exitCode === 0
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function llvmObjcopy(explicit?: string): string {
|
|
427
|
-
if (explicit) return explicit;
|
|
428
|
-
if (process.env.LLVM_OBJCOPY) return process.env.LLVM_OBJCOPY;
|
|
429
|
-
if (executableOnPath("llvm-objcopy")) return "llvm-objcopy";
|
|
430
|
-
const verbose = command("rustc", ["--version", "--verbose"]);
|
|
431
|
-
const host = /^host:\s*(.+)$/m.exec(verbose)?.[1];
|
|
432
|
-
const sysroot = command("rustc", ["--print", "sysroot"]);
|
|
433
|
-
if (host) {
|
|
434
|
-
const candidate = join(
|
|
435
|
-
sysroot,
|
|
436
|
-
"lib",
|
|
437
|
-
"rustlib",
|
|
438
|
-
host,
|
|
439
|
-
"bin",
|
|
440
|
-
process.platform === "win32" ? "llvm-objcopy.exe" : "llvm-objcopy",
|
|
441
|
-
);
|
|
442
|
-
if (existsSync(candidate)) return candidate;
|
|
443
|
-
}
|
|
444
|
-
throw new Error("llvm-objcopy is unavailable; install the Rust llvm-tools-preview component");
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function validRecord(data: Buffer, binary: string, section: string): void {
|
|
448
|
-
if (data.length !== RECORD_SIZE) {
|
|
449
|
-
throw new Error(
|
|
450
|
-
`version section ${section} in ${binary} must be ${RECORD_SIZE} bytes, got ${data.length}`,
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
if (!data.subarray(0, MAGIC.length).equals(MAGIC)) {
|
|
454
|
-
throw new Error(`version section ${section} in ${binary} has invalid magic`);
|
|
455
|
-
}
|
|
456
|
-
if (data.readUInt16LE(12) !== VERSION_SLOT_SCHEMA || data.readUInt16LE(14) > VERSION_CAPACITY) {
|
|
457
|
-
throw new Error(`version section ${section} in ${binary} has an unsupported record schema`);
|
|
458
|
-
}
|
|
459
|
-
if (!data.subarray(EMPTY_RECORD_OFFSET).every((byte) => byte === 0)) {
|
|
460
|
-
throw new Error(`version section ${section} in ${binary} has non-zero reserved bytes`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
export interface StampOptions {
|
|
465
|
-
readonly objcopy?: string;
|
|
466
|
-
readonly codesign?: string;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
export function stamp(binary: string, version: string, options: StampOptions = {}): boolean {
|
|
470
|
-
const encoded = Buffer.from(version, "utf8");
|
|
471
|
-
if (!encoded.length || encoded.length > VERSION_CAPACITY) {
|
|
472
|
-
throw new Error(`version must contain 1 to ${VERSION_CAPACITY} bytes`);
|
|
473
|
-
}
|
|
474
|
-
const format = binaryFormat(binaryHeader(binary));
|
|
475
|
-
if (!format) return false;
|
|
476
|
-
const section = SECTION_BY_FORMAT[format];
|
|
477
|
-
const objcopy = llvmObjcopy(options.objcopy);
|
|
478
|
-
const temporary = mkdtempSync(join(tmpdir(), "dbx-rust-version-"));
|
|
479
|
-
const recordPath = join(temporary, "record.bin");
|
|
480
|
-
const mode = statSync(binary).mode;
|
|
481
|
-
try {
|
|
482
|
-
const dumped = exec.spawnSync(objcopy, ["--dump-section", `${section}=${recordPath}`, binary], {
|
|
483
|
-
stdout: "ignore",
|
|
484
|
-
stderr: "ignore",
|
|
485
|
-
stdin: "ignore",
|
|
486
|
-
check: false,
|
|
487
|
-
});
|
|
488
|
-
if (dumped.exitCode !== 0 || !existsSync(recordPath)) return false;
|
|
489
|
-
const data = readFileSync(recordPath);
|
|
490
|
-
validRecord(data, binary, section);
|
|
491
|
-
data.writeUInt16LE(encoded.length, 14);
|
|
492
|
-
data.fill(0, VERSION_OFFSET, VERSION_OFFSET + VERSION_CAPACITY);
|
|
493
|
-
encoded.copy(data, VERSION_OFFSET);
|
|
494
|
-
writeFileSync(recordPath, data);
|
|
495
|
-
command(objcopy, ["--update-section", `${section}=${recordPath}`, binary], { inherit: true });
|
|
496
|
-
chmodSync(binary, mode);
|
|
497
|
-
if (format === "macho") {
|
|
498
|
-
command(options.codesign ?? "codesign", ["--force", "--sign", "-", binary], {
|
|
499
|
-
inherit: true,
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
return true;
|
|
503
|
-
} finally {
|
|
504
|
-
rmSync(temporary, { recursive: true, force: true });
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function files(root: string): string[] {
|
|
509
|
-
const found: string[] = [];
|
|
510
|
-
const pending = [root];
|
|
511
|
-
while (pending.length) {
|
|
512
|
-
const directory = pending.pop()!;
|
|
513
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
514
|
-
const path = join(directory, entry.name);
|
|
515
|
-
if (entry.isDirectory()) pending.push(path);
|
|
516
|
-
else if (entry.isFile()) found.push(path);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return found.sort();
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
export function stampTree(root: string, version: string, options: StampOptions = {}): number {
|
|
523
|
-
let stamped = 0;
|
|
524
|
-
for (const file of files(root)) if (stamp(file, version, options)) stamped += 1;
|
|
525
|
-
if (!stamped) throw new Error(`no structured version sections found under ${root}`);
|
|
526
|
-
process.stdout.write(`stamped ${stamped} native artifact(s)\n`);
|
|
527
|
-
return stamped;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function fingerprintCommand(args: readonly string[]): void {
|
|
531
|
-
const { values } = parseArgs({
|
|
532
|
-
args: [...args],
|
|
533
|
-
options: {
|
|
534
|
-
root: { type: "string", default: process.cwd() },
|
|
535
|
-
output: { type: "string", default: ".release/rust-build.json" },
|
|
536
|
-
check: { type: "boolean", default: false },
|
|
537
|
-
target: { type: "string", multiple: true },
|
|
538
|
-
toolchain: { type: "string", default: "stable" },
|
|
539
|
-
portable: { type: "boolean", default: false },
|
|
540
|
-
source: { type: "string", multiple: true },
|
|
541
|
-
},
|
|
542
|
-
strict: true,
|
|
543
|
-
});
|
|
544
|
-
fingerprint({
|
|
545
|
-
root: values.root,
|
|
546
|
-
output: values.output,
|
|
547
|
-
check: values.check,
|
|
548
|
-
targets: values.target ?? [],
|
|
549
|
-
toolchain: values.toolchain,
|
|
550
|
-
portable: values.portable,
|
|
551
|
-
sources: values.source ?? [],
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function stampCommand(args: readonly string[]): void {
|
|
556
|
-
const { values } = parseArgs({
|
|
557
|
-
args: [...args],
|
|
558
|
-
options: {
|
|
559
|
-
binary: { type: "string" },
|
|
560
|
-
version: { type: "string" },
|
|
561
|
-
objcopy: { type: "string" },
|
|
562
|
-
},
|
|
563
|
-
strict: true,
|
|
564
|
-
});
|
|
565
|
-
if (!values.binary || values.version === undefined) {
|
|
566
|
-
throw new Error("stamp requires --binary and --version");
|
|
567
|
-
}
|
|
568
|
-
if (!stamp(values.binary, values.version, { objcopy: values.objcopy })) {
|
|
569
|
-
throw new Error(`no structured version section found in ${values.binary}`);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function stampTreeCommand(args: readonly string[]): void {
|
|
574
|
-
const { values } = parseArgs({
|
|
575
|
-
args: [...args],
|
|
576
|
-
options: {
|
|
577
|
-
root: { type: "string" },
|
|
578
|
-
version: { type: "string" },
|
|
579
|
-
objcopy: { type: "string" },
|
|
580
|
-
},
|
|
581
|
-
strict: true,
|
|
582
|
-
});
|
|
583
|
-
if (!values.root || values.version === undefined) {
|
|
584
|
-
throw new Error("stamp-tree requires --root and --version");
|
|
585
|
-
}
|
|
586
|
-
stampTree(values.root, values.version, { objcopy: values.objcopy });
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
export function main(args: readonly string[] = process.argv.slice(2)): void {
|
|
590
|
-
const [subcommand, ...commandArgs] = args;
|
|
591
|
-
if (subcommand === "fingerprint") return fingerprintCommand(commandArgs);
|
|
592
|
-
if (subcommand === "stamp") return stampCommand(commandArgs);
|
|
593
|
-
if (subcommand === "stamp-tree") return stampTreeCommand(commandArgs);
|
|
594
|
-
throw new Error("expected fingerprint, stamp, or stamp-tree");
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : "";
|
|
598
|
-
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
599
|
-
try {
|
|
600
|
-
main();
|
|
601
|
-
} catch (error) {
|
|
602
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
603
|
-
process.exitCode = 1;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun
|
|
2
|
-
/** Bundle the owning Rust release helper source into a standalone Node module. */
|
|
3
|
-
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
6
|
-
import * as exec from "@dbx-tools/core/exec";
|
|
7
|
-
|
|
8
|
-
const root = resolve(import.meta.dirname, "..");
|
|
9
|
-
const temporary = mkdtempSync(join(tmpdir(), "dbx-rust-release-bundle-"));
|
|
10
|
-
try {
|
|
11
|
-
const bundled = join(temporary, "rust-release.mjs");
|
|
12
|
-
const declarations = join(temporary, "declarations");
|
|
13
|
-
exec.spawnSync(
|
|
14
|
-
process.execPath,
|
|
15
|
-
[
|
|
16
|
-
"build",
|
|
17
|
-
resolve(root, "src/_rust-release.ts"),
|
|
18
|
-
"--target=node",
|
|
19
|
-
"--format=esm",
|
|
20
|
-
`--outfile=${bundled}`,
|
|
21
|
-
],
|
|
22
|
-
{
|
|
23
|
-
cwd: root,
|
|
24
|
-
stdout: "inherit",
|
|
25
|
-
stderr: "inherit",
|
|
26
|
-
stdin: "ignore",
|
|
27
|
-
check: true,
|
|
28
|
-
},
|
|
29
|
-
);
|
|
30
|
-
const output = resolve(root, "tasks/rust-release.mjs");
|
|
31
|
-
const source = readFileSync(bundled, "utf8");
|
|
32
|
-
writeFileSync(
|
|
33
|
-
output,
|
|
34
|
-
`#!/usr/bin/env node\n// GENERATED from src/_rust-release.ts by tasks/build-rust-release.ts.\n${source.replace(/^#![^\n]*\n/, "")}`,
|
|
35
|
-
);
|
|
36
|
-
chmodSync(output, 0o755);
|
|
37
|
-
exec.spawnSync(
|
|
38
|
-
"bunx",
|
|
39
|
-
[
|
|
40
|
-
"tsc",
|
|
41
|
-
resolve(root, "src/_rust-release.ts"),
|
|
42
|
-
"--declaration",
|
|
43
|
-
"--emitDeclarationOnly",
|
|
44
|
-
"--module",
|
|
45
|
-
"ESNext",
|
|
46
|
-
"--moduleResolution",
|
|
47
|
-
"Bundler",
|
|
48
|
-
"--target",
|
|
49
|
-
"ES2022",
|
|
50
|
-
"--skipLibCheck",
|
|
51
|
-
"--outDir",
|
|
52
|
-
declarations,
|
|
53
|
-
],
|
|
54
|
-
{
|
|
55
|
-
cwd: root,
|
|
56
|
-
stdout: "inherit",
|
|
57
|
-
stderr: "inherit",
|
|
58
|
-
stdin: "ignore",
|
|
59
|
-
check: true,
|
|
60
|
-
},
|
|
61
|
-
);
|
|
62
|
-
writeFileSync(
|
|
63
|
-
resolve(root, "tasks/rust-release.d.ts"),
|
|
64
|
-
readFileSync(join(declarations, "_rust-release.d.ts")),
|
|
65
|
-
);
|
|
66
|
-
} finally {
|
|
67
|
-
rmSync(temporary, { recursive: true, force: true });
|
|
68
|
-
}
|