@dbx-tools/projen 0.9.17 → 0.9.19

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.
@@ -1,11 +1,12 @@
1
1
  /** Rust release planning and generated GitHub workflow jobs. */
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
- import { dirname, join, relative, resolve } from "node:path";
4
+ import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { stringUtils } from "@dbx-tools/shared-core";
7
7
  import { TextFile, javascript } from "projen";
8
8
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
9
+ import { releaseBinaryAssetName } from "./_release-platform.ts";
9
10
  import { UNIFFI_BINDGEN_FEATURE, type RustProject } from "./_rust-project.ts";
10
11
  import { BUN_VERSION } from "./bun-workflow.ts";
11
12
  import type { DBXToolsJavaScriptProject } from "./project-js.ts";
@@ -16,48 +17,22 @@ import type {
16
17
  UniFFIReleaseTarget,
17
18
  } from "./project-rs.ts";
18
19
  import { defaultReleaseUnitId } from "./release-catalog.ts";
19
- import {
20
- RELEASE_SHA,
21
- RELEASE_SUMMARY_FILE,
22
- RELEASE_TAG,
23
- RELEASE_VERSION,
24
- releaseSourceSteps,
25
- } from "./release-dispatch.ts";
20
+ import { RELEASE_VERSION, releaseSourceSteps } from "./release-dispatch.ts";
26
21
  import {
27
22
  independentReleaseSetupSteps,
28
23
  npmPublishEnvironment,
29
- nodeReleaseSetupSteps,
30
- releaseArtifactSteps,
31
- releaseStageCondition,
24
+ releasePublishCondition,
32
25
  } from "./release.ts";
33
26
 
34
27
  const require = createRequire(import.meta.url);
35
28
 
36
29
  type RustPackageDependencyResolver = (pkg: RustProject) => RustProject[];
37
30
 
38
- const RUST_CACHE_ENV = {
31
+ const RUST_BUILD_ENV = {
39
32
  CARGO_INCREMENTAL: "0",
40
33
  CARGO_TERM_COLOR: "always",
41
34
  } as const;
42
35
 
43
- function rustCacheSteps(sharedKey: string): readonly JobStep[] {
44
- return [
45
- {
46
- name: "Cache Cargo registry and dependencies",
47
- id: "cargo_cache",
48
- uses: "Swatinem/rust-cache@v2.9.2",
49
- with: {
50
- "cache-targets": true,
51
- "cache-workspace-crates": true,
52
- "add-job-id-key": false,
53
- "add-rust-environment-hash-key": true,
54
- "shared-key": sharedKey,
55
- "save-if": "${{ github.event_name == 'push' }}",
56
- },
57
- },
58
- ];
59
- }
60
-
61
36
  function timedBash(phase: string, command: string): string {
62
37
  return [
63
38
  "SECONDS=0",
@@ -66,15 +41,6 @@ function timedBash(phase: string, command: string): string {
66
41
  ].join("\n");
67
42
  }
68
43
 
69
- export function releaseBinaryAssetName(
70
- binaryName: string,
71
- nodeTarget: string,
72
- os: RustReleaseOs | "darwin" | "linux" | "win32",
73
- ): string {
74
- const extension = os === "win32" ? "zip" : "tar.gz";
75
- return `${binaryName}-${nodeTarget}.${extension}`;
76
- }
77
-
78
44
  function taskSource(name: string): string {
79
45
  const sourceDirectory = dirname(fileURLToPath(import.meta.url));
80
46
  const candidates = [
@@ -117,21 +83,21 @@ export function orderRustBindings(bindings: readonly RustBindingMapping[]): Rust
117
83
  return ordered;
118
84
  }
119
85
 
120
- interface RustReleaseBinding extends RustBindingMapping {
86
+ export interface RustReleaseBinding extends RustBindingMapping {
121
87
  readonly node: string;
122
88
  readonly python: string;
123
89
  readonly nodePackage: string;
124
90
  readonly pythonPackage: string;
125
91
  }
126
92
 
127
- interface RustReleaseBinaryPlan {
93
+ export interface RustReleaseBinaryPlan {
128
94
  readonly crate: string;
129
95
  readonly binary: string;
130
96
  readonly excludedOs: readonly RustReleaseOs[];
131
97
  readonly requiredFeatures: readonly string[];
132
98
  }
133
99
 
134
- interface RustReleaseTargetPlan {
100
+ export interface RustReleaseTargetPlan {
135
101
  readonly runner: string;
136
102
  readonly cargo: string;
137
103
  readonly node: string;
@@ -139,56 +105,42 @@ interface RustReleaseTargetPlan {
139
105
  readonly os: RustReleaseOs;
140
106
  readonly cpu: string;
141
107
  readonly libc: string;
108
+ readonly glibcVersion: string;
142
109
  readonly packages: readonly string[];
143
- readonly sources: readonly string[];
144
110
  readonly binaries: readonly string[];
145
111
  readonly features: readonly string[];
146
- readonly fingerprintConfig: string;
112
+ readonly localFeatures: readonly string[];
113
+ }
114
+
115
+ /** Generated release inputs shared by GitHub-hosted and local target builds. */
116
+ export interface RustReleaseConfiguration {
117
+ readonly releaseRustVersion: string;
118
+ readonly usesCargoLock: boolean;
119
+ readonly bindings: readonly RustReleaseBinding[];
120
+ readonly binaries: readonly RustReleaseBinaryPlan[];
121
+ readonly targets: readonly RustReleaseTargetPlan[];
147
122
  }
148
123
 
149
124
  export interface RustReleasePlan {
150
125
  readonly releaseRustVersion: string;
151
126
  readonly releaseTask: string;
152
- readonly releaseHelper: string;
153
127
  readonly bindings: readonly RustReleaseBinding[];
154
128
  readonly nodeBindings: readonly RustReleaseBinding[];
155
129
  readonly releaseBinaries: readonly RustReleaseBinaryPlan[];
156
130
  readonly publicCrates: readonly string[];
157
- readonly uniffiTargets: readonly RustReleaseTargetPlan[];
158
- readonly binaryTargets: readonly RustReleaseTargetPlan[];
131
+ readonly targets: readonly RustReleaseTargetPlan[];
159
132
  readonly hasPythonBindings: boolean;
160
133
  readonly usesCargoLock: boolean;
161
134
  readonly usePreinstalledWindowsRust: boolean;
162
135
  readonly hasTargetOutputs: boolean;
163
136
  }
164
137
 
165
- function packageClosure(
166
- roots: readonly RustProject[],
167
- packageDependencies: RustPackageDependencyResolver,
168
- ): RustProject[] {
169
- const found = new Set<RustProject>();
170
- const visit = (pkg: RustProject): void => {
171
- if (found.has(pkg)) return;
172
- found.add(pkg);
173
- for (const dependency of packageDependencies(pkg)) visit(dependency);
174
- };
175
- for (const pkg of roots) visit(pkg);
176
- return [...found].sort((left, right) => left.crateName.localeCompare(right.crateName));
177
- }
178
-
179
- function packageSources(
180
- project: javascript.NodeProject,
181
- packages: readonly RustProject[],
182
- ): string[] {
183
- return packages.map((pkg) => relative(project.outdir, pkg.outdir).replaceAll("\\", "/")).sort();
184
- }
185
-
186
138
  function releaseTargetPlan(
187
139
  target: UniFFIReleaseTarget,
188
140
  packages: readonly RustProject[],
189
- sources: readonly string[],
190
141
  binaries: readonly string[],
191
142
  features: readonly string[],
143
+ localFeatures: readonly string[],
192
144
  ): RustReleaseTargetPlan {
193
145
  const packageNames = packages.map((pkg) => pkg.crateName).sort();
194
146
  const sortedBinaries = [...new Set(binaries)].sort();
@@ -201,15 +153,11 @@ function releaseTargetPlan(
201
153
  os: target.os,
202
154
  cpu: target.cpu,
203
155
  libc: target.libc ?? "",
156
+ glibcVersion: target.glibcVersion ?? "",
204
157
  packages: packageNames,
205
- sources,
206
158
  binaries: sortedBinaries,
207
159
  features: sortedFeatures,
208
- fingerprintConfig: JSON.stringify({
209
- packages: packageNames,
210
- binaries: sortedBinaries,
211
- features: sortedFeatures,
212
- }),
160
+ localFeatures: [...new Set(localFeatures)].sort(),
213
161
  };
214
162
  }
215
163
 
@@ -261,55 +209,48 @@ export function planRustRelease(
261
209
  const bindingPackages = packages.filter((pkg) =>
262
210
  bindings.some((binding) => binding.crate === pkg.crateName),
263
211
  );
264
- const bindingClosure = packageClosure(bindingPackages, packageDependencies);
265
- const bindingSources = packageSources(project, bindingClosure);
266
- const uniffiTargets = bindings.length
267
- ? targets.map((target) =>
268
- releaseTargetPlan(
269
- target,
270
- bindingPackages,
271
- bindingSources,
272
- [],
273
- bindingPackages.map((pkg) => `${pkg.crateName}/${UNIFFI_BINDGEN_FEATURE}`),
274
- ),
275
- )
276
- : [];
277
- const binaryTargets = targets.flatMap((target) => {
212
+ const releaseTargets = targets.flatMap((target) => {
278
213
  const selectedBinaries = releaseBinaries.filter(
279
214
  (binary) => !binary.excludedOs.includes(target.os),
280
215
  );
281
216
  const binaryPackages = packages.filter((pkg) =>
282
217
  selectedBinaries.some((binary) => binary.crate === pkg.crateName),
283
218
  );
284
- if (!binaryPackages.length) return [];
285
- const binaryClosure = packageClosure(binaryPackages, packageDependencies);
286
- const features = selectedBinaries.flatMap((binary) =>
287
- binary.requiredFeatures.map((feature) => `${binary.crate}/${feature}`),
219
+ const selectedPackages = [...new Set([...bindingPackages, ...binaryPackages])];
220
+ if (!selectedPackages.length) return [];
221
+ const features = [
222
+ ...bindingPackages.map((pkg) => `${pkg.crateName}/${UNIFFI_BINDGEN_FEATURE}`),
223
+ ...selectedBinaries.flatMap((binary) =>
224
+ binary.requiredFeatures.map((feature) => `${binary.crate}/${feature}`),
225
+ ),
226
+ ];
227
+ const localFeatures = selectedPackages.flatMap((pkg) =>
228
+ (pkg.packageOptions.releaseLocalFeatures ?? []).map(
229
+ (feature) => `${pkg.crateName}/${feature}`,
230
+ ),
288
231
  );
289
232
  return [
290
233
  releaseTargetPlan(
291
234
  target,
292
- binaryPackages,
293
- packageSources(project, binaryClosure),
235
+ selectedPackages,
294
236
  selectedBinaries.map((binary) => binary.binary),
295
237
  features,
238
+ localFeatures,
296
239
  ),
297
240
  ];
298
241
  });
299
- const hasTargetOutputs = uniffiTargets.length > 0 || binaryTargets.length > 0;
242
+ const hasTargetOutputs = releaseTargets.length > 0;
300
243
  if (hasTargetOutputs && targets.length === 0) {
301
244
  throw new Error("Rust release requires at least one target");
302
245
  }
303
246
  return {
304
247
  releaseRustVersion,
305
248
  releaseTask: ".projen/uniffi-release.mjs",
306
- releaseHelper: ".projen/rust-release.mjs",
307
249
  bindings,
308
250
  nodeBindings: bindings.filter((binding) => Boolean(binding.node && binding.nodePackage)),
309
251
  releaseBinaries,
310
252
  publicCrates,
311
- uniffiTargets,
312
- binaryTargets,
253
+ targets: releaseTargets,
313
254
  hasPythonBindings: bindings.some((binding) => binding.python),
314
255
  usesCargoLock: existsSync(join(project.outdir, "Cargo.lock")),
315
256
  usePreinstalledWindowsRust: releaseRustVersion === "stable",
@@ -318,10 +259,7 @@ export function planRustRelease(
318
259
  }
319
260
 
320
261
  function rustBuildJobIds(plan: RustReleasePlan): string[] {
321
- return [
322
- ...(plan.uniffiTargets.length ? ["rust-uniffi"] : []),
323
- ...(plan.binaryTargets.length ? ["rust-binaries"] : []),
324
- ];
262
+ return plan.targets.length ? ["rust-build"] : [];
325
263
  }
326
264
 
327
265
  function rustBuildResultCondition(plan: RustReleasePlan): string {
@@ -334,15 +272,25 @@ export function configureRustReleaseTask(
334
272
  project: javascript.NodeProject,
335
273
  plan: RustReleasePlan,
336
274
  ): void {
275
+ new TextFile(project, ".projen/rust-release.json", {
276
+ lines: JSON.stringify(
277
+ {
278
+ releaseRustVersion: plan.releaseRustVersion,
279
+ usesCargoLock: plan.usesCargoLock,
280
+ bindings: plan.bindings,
281
+ binaries: plan.releaseBinaries,
282
+ targets: plan.targets,
283
+ } satisfies RustReleaseConfiguration,
284
+ null,
285
+ 2,
286
+ ).split("\n"),
287
+ });
337
288
  const bindingSupportFiles = [
338
289
  plan.releaseTask,
339
290
  ".projen/uniffi-python.js",
340
291
  ".projen/smol-toml.cjs",
341
292
  ".projen/smol-toml.LICENSE",
342
293
  ] as const;
343
- new TextFile(project, plan.releaseHelper, {
344
- lines: taskSource("rust-release.mjs").trimEnd().split("\n"),
345
- });
346
294
  if (plan.bindings.length) {
347
295
  new TextFile(project, plan.releaseTask, {
348
296
  lines: taskSource("uniffi-release.mjs").trimEnd().split("\n"),
@@ -359,31 +307,7 @@ export function configureRustReleaseTask(
359
307
  } else {
360
308
  for (const path of bindingSupportFiles) project.tryRemoveFile(path);
361
309
  }
362
- const fingerprintTask = project.addTask("rs:release-fingerprint", {
363
- description: "Write the version-independent Rust release build manifest",
364
- });
365
- for (const [namespace, targets] of [
366
- ["uniffi", plan.uniffiTargets],
367
- ["binaries", plan.binaryTargets],
368
- ] as const) {
369
- if (!targets.length) continue;
370
- const sources = [...new Set(targets.flatMap((target) => target.sources))].sort();
371
- fingerprintTask.exec(
372
- [
373
- `node ${plan.releaseHelper} fingerprint`,
374
- "--root .",
375
- `--output ${JSON.stringify(`.release/rust-${namespace}.json`)}`,
376
- `--namespace ${JSON.stringify(namespace)}`,
377
- ...targets.map(
378
- (target) => `--target ${JSON.stringify(`${target.cargo}|${target.fingerprintConfig}`)}`,
379
- ),
380
- `--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
381
- ...sources.map((source) => `--source ${JSON.stringify(source)}`),
382
- "--source-only",
383
- "--portable",
384
- ].join(" "),
385
- );
386
- }
310
+ project.tryRemoveFile(".projen/rust-release.mjs");
387
311
  }
388
312
 
389
313
  function rustBindingCommands(plan: RustReleasePlan, independent: boolean): string[] {
@@ -458,42 +382,38 @@ function rustBinaryCommands(plan: RustReleasePlan, independent: boolean): string
458
382
  });
459
383
  }
460
384
 
461
- type RustBuildKind = "uniffi" | "binaries";
462
-
463
- function rustArtifactSteps(plan: RustReleasePlan, kind: RustBuildKind): JobStep[] {
385
+ function rustArtifactSteps(plan: RustReleasePlan): JobStep[] {
464
386
  const binaryCrates = [...new Set(plan.releaseBinaries.map((binary) => binary.crate))];
465
387
  return [
466
- ...(kind === "uniffi"
467
- ? plan.bindings.flatMap((binding) => [
468
- ...(binding.node
469
- ? [
470
- {
471
- name: `Upload ${binding.crate} native npm package`,
472
- uses: "actions/upload-artifact@v7",
473
- with: {
474
- name: `${binding.crate}-\${{ matrix.node }}-npm`,
475
- path: `dist/release/${binding.crate}/\${{ matrix.node }}/npm/*.tgz`,
476
- "retention-days": 7,
477
- },
478
- },
479
- ]
480
- : []),
481
- ...(binding.python
482
- ? [
483
- {
484
- name: `Upload ${binding.crate} Python wheel`,
485
- uses: "actions/upload-artifact@v7",
486
- with: {
487
- name: `${binding.pythonPackage}--\${{ matrix.python }}--python-wheel`,
488
- path: `dist/release/${binding.crate}/\${{ matrix.node }}/python/*.whl`,
489
- "retention-days": 7,
490
- },
491
- },
492
- ]
493
- : []),
494
- ])
495
- : []),
496
- ...(kind === "binaries" ? binaryCrates : []).map((crate) => {
388
+ ...plan.bindings.flatMap((binding) => [
389
+ ...(binding.node
390
+ ? [
391
+ {
392
+ name: `Upload ${binding.crate} native npm package`,
393
+ uses: "actions/upload-artifact@v7",
394
+ with: {
395
+ name: `${binding.crate}-\${{ matrix.node }}-npm`,
396
+ path: `dist/release/${binding.crate}/\${{ matrix.node }}/npm/*.tgz`,
397
+ "retention-days": 7,
398
+ },
399
+ },
400
+ ]
401
+ : []),
402
+ ...(binding.python
403
+ ? [
404
+ {
405
+ name: `Upload ${binding.crate} Python wheel`,
406
+ uses: "actions/upload-artifact@v7",
407
+ with: {
408
+ name: `${binding.pythonPackage}--\${{ matrix.python }}--python-wheel`,
409
+ path: `dist/release/${binding.crate}/\${{ matrix.node }}/python/*.whl`,
410
+ "retention-days": 7,
411
+ },
412
+ },
413
+ ]
414
+ : []),
415
+ ]),
416
+ ...binaryCrates.map((crate) => {
497
417
  const binaries = plan.releaseBinaries.filter((binary) => binary.crate === crate);
498
418
  const excludedOs =
499
419
  binaries[0]?.excludedOs.filter((os) =>
@@ -520,85 +440,42 @@ function matrixArgumentLoop(variable: string, field: string, flag: string): stri
520
440
  return `while IFS= read -r ${variable}; do ${variable}="\${${variable}%$'\\r'}"; ${variable}_ARGS+=(${flag} "$${variable}"); done < <(jq -r '.[]' <<<'\${{ toJSON(matrix.${field}) }}')`;
521
441
  }
522
442
 
523
- function rustCargoBuildCommand(plan: RustReleasePlan, kind: RustBuildKind): string {
443
+ function rustCargoBuildCommand(plan: RustReleasePlan): string {
524
444
  return [
525
445
  "PACKAGE_ARGS=()",
526
446
  "FEATURE_ARGS=()",
527
447
  matrixArgumentLoop("PACKAGE", "packages", "--package"),
528
448
  matrixArgumentLoop("FEATURE", "features", "--features"),
529
- ...(kind === "binaries"
530
- ? ["BINARY_ARGS=()", matrixArgumentLoop("BINARY", "binaries", "--bin")]
531
- : ["BINARY_ARGS=()"]),
532
- `cargo build --release --timings "\${PACKAGE_ARGS[@]}" "\${BINARY_ARGS[@]}" "\${FEATURE_ARGS[@]}"${
449
+ `cargo build --release --timings "\${PACKAGE_ARGS[@]}" "\${FEATURE_ARGS[@]}"${
533
450
  plan.usesCargoLock ? " --locked" : ""
534
451
  } --target "\${{ matrix.cargo }}"`,
535
452
  ].join("\n");
536
453
  }
537
454
 
538
- export function rustBuildJob(
539
- plan: RustReleasePlan,
540
- kind: RustBuildKind,
541
- independentSetup?: readonly JobStep[],
542
- ): Job {
543
- const independent = Boolean(independentSetup);
544
- const targets = kind === "uniffi" ? plan.uniffiTargets : plan.binaryTargets;
545
- const outputName = kind === "uniffi" ? "rust_uniffi_targets" : "rust_binary_targets";
546
- const title = kind === "uniffi" ? "UniFFI" : "binaries";
547
- const bindingCommands = kind === "uniffi" ? rustBindingCommands(plan, independent) : [];
548
- const binaryCommands = kind === "binaries" ? rustBinaryCommands(plan, independent) : [];
549
- const fingerprintSources = [...new Set(targets.flatMap((target) => target.sources))].sort();
455
+ export function rustBuildJob(plan: RustReleasePlan, setup: readonly JobStep[]): Job {
456
+ const bindingCommands = rustBindingCommands(plan, true);
457
+ const binaryCommands = rustBinaryCommands(plan, true);
550
458
  return {
551
- if: independentSetup
552
- ? `\${{ needs.release-plan.outputs.${outputName} != '[]' && (github.event_name == 'push' || inputs.stage != 'docs') }}`
553
- : "${{ github.event_name == 'push' || inputs.stage == 'all' }}",
554
- name: `${title} / \${{ matrix.node }}`,
555
- needs: [independentSetup ? "release-plan" : "verify-context"],
459
+ if: "${{ needs.release-plan.outputs.rust_targets != '[]' && (github.event_name == 'push' || inputs.stage != 'docs') }}",
460
+ name: "Rust / ${{ matrix.node }}",
461
+ needs: ["release-plan"],
556
462
  runsOn: ["${{ matrix.runner }}"],
557
463
  permissions: { contents: JobPermission.READ },
558
- env: {
559
- ...RUST_CACHE_ENV,
560
- ...(independentSetup ? { BUN_VERSION } : {}),
561
- },
464
+ env: { ...RUST_BUILD_ENV, BUN_VERSION },
562
465
  strategy: {
563
466
  failFast: false,
564
467
  matrix: {
565
- include: independentSetup
566
- ? (`\${{ fromJSON(needs.release-plan.outputs.${outputName}) }}` as never)
567
- : ([...targets] as never),
468
+ include: "${{ fromJSON(needs.release-plan.outputs.rust_targets) }}" as never,
568
469
  },
569
470
  },
570
471
  steps: [
571
- ...(independentSetup ?? releaseSourceSteps()),
572
- ...(kind === "uniffi" && plan.hasPythonBindings
573
- ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }]
574
- : []),
472
+ ...setup,
473
+ ...(plan.hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
575
474
  {
576
475
  name: "Setup Rust",
577
- ...(plan.usePreinstalledWindowsRust ? { if: "${{ matrix.os != 'win32' }}" } : {}),
578
476
  uses: `dtolnay/rust-toolchain@${plan.releaseRustVersion}`,
579
477
  with: { targets: "${{ matrix.cargo }}" },
580
478
  },
581
- ...(plan.usePreinstalledWindowsRust
582
- ? [
583
- {
584
- name: "Verify preinstalled Windows Rust",
585
- if: "${{ matrix.os == 'win32' }}",
586
- shell: "bash",
587
- run: [
588
- "rustc --version --verbose",
589
- "cargo --version",
590
- 'rustup target list --installed | grep -Fx "${{ matrix.cargo }}"',
591
- 'test -f "$(rustc --print sysroot)/lib/rustlib/${{ matrix.cargo }}/bin/rust-lld.exe"',
592
- ].join("\n"),
593
- },
594
- ]
595
- : []),
596
- {
597
- name: "Install LLVM release tools",
598
- shell: "bash",
599
- run: "rustup component add llvm-tools-preview",
600
- },
601
- ...rustCacheSteps(`release-${kind}-\${{ matrix.cargo }}-rust-${plan.releaseRustVersion}`),
602
479
  {
603
480
  name: "Install Linux native dependencies",
604
481
  if: "${{ matrix.os == 'linux' }}",
@@ -614,120 +491,15 @@ export function rustBuildJob(
614
491
  ),
615
492
  },
616
493
  {
617
- name: "Verify Rust build fingerprint",
618
- if: independentSetup
619
- ? undefined
620
- : "${{ github.event_name == 'push' || inputs.stage == 'all' }}",
621
- ...(!independentSetup ? { id: "rust-fingerprint" } : {}),
494
+ name: "Build Rust release outputs",
622
495
  shell: "bash",
623
- run: [
624
- `RUNTIME_MANIFEST="dist/rust-raw/rust-${kind}-build-\${{ matrix.node }}.json"`,
625
- 'mkdir -p "$(dirname "$RUNTIME_MANIFEST")"',
626
- [
627
- `node ${plan.releaseHelper} fingerprint --root .`,
628
- '--output "$RUNTIME_MANIFEST"',
629
- `--namespace ${JSON.stringify(kind)}`,
630
- '--target "${{ matrix.cargo }}|${{ matrix.fingerprintConfig }}"',
631
- `--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
632
- ...fingerprintSources.map((source) => `--source ${JSON.stringify(source)}`),
633
- "--source-only",
634
- ...(independentSetup ? ["--portable"] : []),
635
- ].join(" "),
636
- `test "$(jq -r .namespace "$RUNTIME_MANIFEST")" = "$(jq -r .namespace .release/rust-${kind}.json)"`,
637
- `test "$(jq -r .rustSourceHash "$RUNTIME_MANIFEST")" = "$(jq -r .rustSourceHash .release/rust-${kind}.json)"`,
638
- 'KEY="$(jq -r --arg target "${{ matrix.cargo }}" \'.targets[$target] // empty\' "$RUNTIME_MANIFEST")"',
639
- 'test -n "$KEY"',
640
- 'echo "key=$KEY" >> "$GITHUB_OUTPUT"',
641
- ].join("\n"),
496
+ run: timedBash("rust", rustCargoBuildCommand(plan)),
642
497
  },
643
- ...(!independentSetup
644
- ? [
645
- {
646
- name: "Reuse matching raw Rust outputs",
647
- id: "raw-native",
648
- shell: "bash",
649
- env: { GH_TOKEN: "${{ github.token }}" },
650
- // prettier-ignore
651
- run: stringUtils.dedent(
652
- // ============================================================================
653
- /*bash*/`
654
- KEY="\${{ steps.rust-fingerprint.outputs.key }}"
655
- test -n "$KEY"
656
- ASSET="rust-${kind}-raw-\${{ matrix.node }}-$KEY.tar.gz"
657
- mkdir -p dist/rust-raw
658
- MATCH="$(gh api --paginate "repos/\${{ github.repository }}/releases?per_page=100" --jq '.[] | select(.draft == false) | . as $release | .assets[] | select(.name == "'"$ASSET"'") | [$release.tag_name, .url] | @tsv' | head -n 1)"
659
- if [ -n "$MATCH" ]; then
660
- TAG="\${MATCH%%$'\\t'*}"
661
- URL="\${MATCH#*$'\\t'}"
662
- CHECKSUM_URL="$(gh api "repos/\${{ github.repository }}/releases/tags/$TAG" --jq '.assets[] | select(.name == "'"$ASSET.sha256"'") | .url')"
663
- rm -rf "target/\${{ matrix.cargo }}/release"
664
- if test -n "$CHECKSUM_URL" && gh api "$URL" -H "Accept: application/octet-stream" > "dist/rust-raw/$ASSET" && gh api "$CHECKSUM_URL" -H "Accept: application/octet-stream" > "dist/rust-raw/$ASSET.sha256" && (cd dist/rust-raw && if command -v sha256sum >/dev/null 2>&1; then sha256sum --check "$ASSET.sha256"; else shasum -a 256 --check "$ASSET.sha256"; fi) && tar -xzf "dist/rust-raw/$ASSET"; then
665
- echo "hit=true" >> "$GITHUB_OUTPUT"
666
- else
667
- rm -rf "target/\${{ matrix.cargo }}/release" "dist/rust-raw/$ASSET" "dist/rust-raw/$ASSET.sha256"
668
- echo "::warning::matching raw Rust bundle failed validation; rebuilding"
669
- echo "hit=false" >> "$GITHUB_OUTPUT"
670
- fi
671
- else
672
- echo "hit=false" >> "$GITHUB_OUTPUT"
673
- fi
674
- echo "asset=$ASSET" >> "$GITHUB_OUTPUT"
675
- `
676
- // ============================================================================
677
- ),
678
- },
679
- ]
680
- : []),
681
- {
682
- name: `Build Rust ${title} outputs`,
683
- ...(independentSetup ? {} : { if: "${{ steps.raw-native.outputs.hit != 'true' }}" }),
684
- shell: "bash",
685
- env: {
686
- CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER:
687
- "${{ matrix.os == 'win32' && 'rust-lld' || '' }}",
688
- },
689
- run: timedBash(`rust_${kind}`, rustCargoBuildCommand(plan, kind)),
690
- },
691
- ...(!independentSetup
692
- ? [
693
- {
694
- name: "Upload Cargo timings",
695
- if: "${{ steps.raw-native.outputs.hit != 'true' }}",
696
- uses: "actions/upload-artifact@v7",
697
- with: {
698
- name: `rust-${kind}-\${{ matrix.node }}-cargo-timings`,
699
- path: "target/cargo-timings/*",
700
- "retention-days": 14,
701
- },
702
- },
703
- ]
704
- : []),
705
- ...(!independentSetup
706
- ? [
707
- {
708
- name: "Archive raw Rust outputs",
709
- if: "${{ steps.raw-native.outputs.hit != 'true' }}",
710
- shell: "bash",
711
- run: [
712
- "mkdir -p dist/rust-raw",
713
- 'tar -czf "dist/rust-raw/${{ steps.raw-native.outputs.asset }}" "target/${{ matrix.cargo }}/release"',
714
- '(cd dist/rust-raw && if command -v sha256sum >/dev/null 2>&1; then sha256sum "${{ steps.raw-native.outputs.asset }}"; else shasum -a 256 "${{ steps.raw-native.outputs.asset }}"; fi > "${{ steps.raw-native.outputs.asset }}.sha256")',
715
- ].join("\n"),
716
- },
717
- {
718
- name: "Stamp native release version",
719
- shell: "bash",
720
- env: { VERSION: RELEASE_VERSION },
721
- run: `node ${plan.releaseHelper} stamp-tree --root "target/\${{ matrix.cargo }}/release" --version "$VERSION"`,
722
- },
723
- ]
724
- : []),
725
498
  ...(bindingCommands.length
726
499
  ? [
727
500
  {
728
501
  name: "Package UniFFI outputs",
729
502
  shell: "bash",
730
- ...(independentSetup ? {} : { env: { VERSION: RELEASE_VERSION } }),
731
503
  run: timedBash("uniffi_packaging", bindingCommands.join("\n")),
732
504
  },
733
505
  ]
@@ -741,32 +513,16 @@ export function rustBuildJob(
741
513
  },
742
514
  ]
743
515
  : []),
744
- ...(!independentSetup
745
- ? [
746
- {
747
- name: "Upload reusable raw Rust outputs",
748
- uses: "actions/upload-artifact@v7",
749
- with: {
750
- name: `rust-${kind}-\${{ matrix.node }}-raw`,
751
- path: "dist/rust-raw/*",
752
- "retention-days": 7,
753
- },
754
- },
755
- ]
756
- : []),
757
- ...rustArtifactSteps(plan, kind),
516
+ ...rustArtifactSteps(plan),
758
517
  ],
759
518
  };
760
519
  }
761
520
 
762
- export function rustCargoPublishJob(plan: RustReleasePlan, local: boolean): Job {
763
- const registry = local ? '"${{ vars.LOCAL_CARGO_REGISTRY }}"' : "crates-io";
521
+ export function rustCargoPublishJob(plan: RustReleasePlan): Job {
764
522
  return {
765
- if: local
766
- ? "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}"
767
- : "${{ github.event_name == 'push' }}",
768
- needs: ["verify-context", ...rustBuildJobIds(plan)],
769
- runsOn: [local ? "self-hosted" : "ubuntu-latest"],
523
+ if: releasePublishCondition("cargo", ["needs.verify-context.result == 'success'"]),
524
+ needs: ["verify-context"],
525
+ runsOn: ["ubuntu-latest"],
770
526
  permissions: { contents: JobPermission.READ },
771
527
  steps: [
772
528
  ...releaseSourceSteps(),
@@ -775,14 +531,21 @@ export function rustCargoPublishJob(plan: RustReleasePlan, local: boolean): Job
775
531
  uses: `dtolnay/rust-toolchain@${plan.releaseRustVersion}`,
776
532
  },
777
533
  {
778
- name: local ? "Publish Cargo crates locally" : "Publish public crates",
534
+ name: "Publish public crates",
779
535
  env: {
780
- CARGO_REGISTRY_TOKEN: local
781
- ? "${{ secrets.LOCAL_CARGO_TOKEN }}"
782
- : "${{ secrets.CARGO_REGISTRY_TOKEN }}",
536
+ RELEASE_VERSION,
537
+ CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}",
783
538
  },
784
539
  run: plan.publicCrates
785
- .map((crate) => `cargo publish --package "${crate}" --registry ${registry} --no-verify`)
540
+ .map((crate) =>
541
+ [
542
+ `if cargo info "${crate}@$RELEASE_VERSION" >/dev/null 2>&1; then`,
543
+ ` echo "skip published ${crate}@$RELEASE_VERSION"`,
544
+ "else",
545
+ ` cargo publish --package "${crate}" --registry crates-io --no-verify`,
546
+ "fi",
547
+ ].join("\n"),
548
+ )
786
549
  .join("\n"),
787
550
  },
788
551
  ],
@@ -850,15 +613,6 @@ export function independentRustGitHubReleaseJob(
850
613
  "merge-multiple": true,
851
614
  },
852
615
  },
853
- {
854
- name: "Download reusable raw Rust outputs",
855
- uses: "actions/download-artifact@v8",
856
- with: {
857
- pattern: "*-raw",
858
- path: "dist/rust-raw",
859
- "merge-multiple": true,
860
- },
861
- },
862
616
  {
863
617
  name: "Upload affected GitHub release assets",
864
618
  env: { GH_TOKEN: "${{ github.token }}" },
@@ -879,100 +633,10 @@ export function independentRustGitHubReleaseJob(
879
633
  };
880
634
  }
881
635
 
882
- export function rustGitHubReleaseJob(plan: RustReleasePlan): Job {
883
- return {
884
- if: "${{ github.event_name == 'push' }}",
885
- needs: ["verify-context", ...rustBuildJobIds(plan)],
886
- runsOn: ["ubuntu-latest"],
887
- permissions: { contents: JobPermission.WRITE },
888
- steps: [
889
- ...releaseSourceSteps(),
890
- {
891
- name: "Download release binaries",
892
- uses: "actions/download-artifact@v8",
893
- with: {
894
- pattern: "*-binary",
895
- path: "dist/rust-release",
896
- "merge-multiple": true,
897
- },
898
- },
899
- {
900
- name: "Download reusable raw Rust outputs",
901
- uses: "actions/download-artifact@v8",
902
- with: {
903
- pattern: "*-raw",
904
- path: "dist/rust-raw",
905
- "merge-multiple": true,
906
- },
907
- },
908
- {
909
- name: "Publish GitHub release assets",
910
- uses: "softprops/action-gh-release@v2",
911
- with: {
912
- files: ["dist/rust-release/*", "dist/rust-raw/*"].join("\n"),
913
- body_path: RELEASE_SUMMARY_FILE,
914
- generate_release_notes: true,
915
- tag_name: RELEASE_TAG,
916
- target_commitish: RELEASE_SHA,
917
- },
918
- },
919
- {
920
- name: "Delete superseded raw Rust release assets",
921
- uses: "actions/github-script@v8",
922
- env: { CURRENT_RELEASE_TAG: RELEASE_TAG },
923
- with: {
924
- // prettier-ignore
925
- script: stringUtils.dedent(
926
- // ============================================================================
927
- /*js*/`
928
- const currentTag = process.env.CURRENT_RELEASE_TAG;
929
- if (!currentTag) throw new Error("CURRENT_RELEASE_TAG is required");
930
- const parseVersion = (tag) => {
931
- const match = /^v(\\d+)\\.(\\d+)\\.(\\d+)$/.exec(tag);
932
- return match ? match.slice(1).map(Number) : undefined;
933
- };
934
- const compareVersions = (left, right) => {
935
- for (let index = 0; index < 3; index += 1) {
936
- if (left[index] !== right[index]) return left[index] - right[index];
937
- }
938
- return 0;
939
- };
940
- const currentVersion = parseVersion(currentTag);
941
- if (!currentVersion) throw new Error(\`Unsupported release tag: \${currentTag}\`);
942
- const releases = await github.paginate(github.rest.repos.listReleases, {
943
- ...context.repo,
944
- per_page: 100,
945
- });
946
- const assets = releases.flatMap((release) => {
947
- const version = parseVersion(release.tag_name);
948
- if (release.draft || !version || compareVersions(version, currentVersion) >= 0) {
949
- return [];
950
- }
951
- return release.assets.filter((asset) =>
952
- /^rust-(?:(?:uniffi|binaries)-(?:raw|build)|(?:raw|build))-/.test(asset.name)
953
- );
954
- });
955
- for (const asset of assets) {
956
- core.info(\`Deleting \${asset.name} (\${asset.id})\`);
957
- await github.rest.repos.deleteReleaseAsset({
958
- ...context.repo,
959
- asset_id: asset.id,
960
- });
961
- }
962
- core.info(\`Deleted \${assets.length} superseded raw Rust release assets\`);
963
- `
964
- // ============================================================================
965
- ),
966
- },
967
- },
968
- ],
969
- };
970
- }
971
-
972
636
  export function independentRustNativeNpmPublishJob(project: DBXToolsJavaScriptProject): Job {
973
637
  return {
974
638
  if: "${{ needs.release-plan.outputs.node == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
975
- needs: ["release-plan", "rust-uniffi"],
639
+ needs: ["release-plan", "rust-build"],
976
640
  runsOn: ["ubuntu-latest"],
977
641
  permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
978
642
  timeoutMinutes: 15,
@@ -1032,78 +696,3 @@ export function independentRustNodeFacadePublishJob(
1032
696
  ],
1033
697
  };
1034
698
  }
1035
-
1036
- export function rustNativeNpmPublishJob(project: DBXToolsJavaScriptProject): Job {
1037
- return {
1038
- if: "${{ always() && needs.verify-context.result == 'success' && needs.rust-uniffi.result != 'failure' && needs.rust-uniffi.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
1039
- needs: ["verify-context", "rust-uniffi"],
1040
- runsOn: ["ubuntu-latest"],
1041
- permissions: {
1042
- actions: JobPermission.READ,
1043
- contents: JobPermission.READ,
1044
- idToken: JobPermission.WRITE,
1045
- },
1046
- timeoutMinutes: 15,
1047
- env: { BUN_VERSION, CI: "true" },
1048
- steps: [
1049
- ...nodeReleaseSetupSteps(project),
1050
- ...releaseArtifactSteps({
1051
- currentName: "Download native npm packages",
1052
- recoveredName: "Download recovered native npm packages",
1053
- pattern: "*-npm",
1054
- path: "dist/uniffi/native",
1055
- }),
1056
- {
1057
- name: "Publish native npm packages",
1058
- env: { RELEASE_VERSION, ...npmPublishEnvironment() },
1059
- run: 'bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory dist/uniffi/native --version "$RELEASE_VERSION" $DRY_RUN',
1060
- },
1061
- ],
1062
- };
1063
- }
1064
-
1065
- export function rustNodeFacadePublishJob(
1066
- project: DBXToolsJavaScriptProject,
1067
- bindings: readonly RustReleaseBinding[],
1068
- ): Job {
1069
- return {
1070
- if: releaseStageCondition("node"),
1071
- needs: ["verify-context", "publish-node"],
1072
- runsOn: ["ubuntu-latest"],
1073
- permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
1074
- timeoutMinutes: 30,
1075
- env: { BUN_VERSION, CI: "true" },
1076
- steps: [
1077
- ...nodeReleaseSetupSteps(project),
1078
- {
1079
- name: "Build and publish UniFFI npm facades",
1080
- env: { RELEASE_VERSION, ...npmPublishEnvironment() },
1081
- run: bindings
1082
- .flatMap((binding) => {
1083
- const output = `dist/uniffi/facades/${binding.crate}`;
1084
- return [
1085
- `node .projen/uniffi-release.mjs facade --node "${binding.node}" --node-package "${binding.nodePackage}" --node-triple "linux-x64-gnu" --version "$RELEASE_VERSION" --output "${output}"`,
1086
- `bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory "${output}/npm-facade" --version "$RELEASE_VERSION" $DRY_RUN`,
1087
- ];
1088
- })
1089
- .join("\n"),
1090
- },
1091
- {
1092
- name: "Smoke test published UniFFI npm facades",
1093
- if: "${{ github.event_name == 'push' && vars.UNIFFI_FACADE_SMOKE == 'true' }}",
1094
- continueOnError: true,
1095
- env: { RELEASE_VERSION },
1096
- run: [
1097
- 'SMOKE_DIR="$(mktemp -d)"',
1098
- "trap 'rm -rf \"$SMOKE_DIR\"' EXIT",
1099
- 'cd "$SMOKE_DIR"',
1100
- "npm init --yes >/dev/null",
1101
- ...bindings.flatMap((binding) => [
1102
- `npm install --ignore-scripts --no-audit --no-fund --package-lock=false "${binding.nodePackage}@$RELEASE_VERSION"`,
1103
- `node -e 'import("${binding.nodePackage}")'`,
1104
- ]),
1105
- ].join("\n"),
1106
- },
1107
- ],
1108
- };
1109
- }