@dbx-tools/projen 0.9.11 → 0.9.17

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,12 +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, resolve } from "node:path";
4
+ import { dirname, join, relative, 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 type { RustProject } from "./_rust-project.ts";
9
+ import { UNIFFI_BINDGEN_FEATURE, type RustProject } from "./_rust-project.ts";
10
10
  import { BUN_VERSION } from "./bun-workflow.ts";
11
11
  import type { DBXToolsJavaScriptProject } from "./project-js.ts";
12
12
  import type {
@@ -131,25 +131,88 @@ interface RustReleaseBinaryPlan {
131
131
  readonly requiredFeatures: readonly string[];
132
132
  }
133
133
 
134
- type RustReleaseTargetPlan = Readonly<Record<string, string | boolean | number>>;
134
+ interface RustReleaseTargetPlan {
135
+ readonly runner: string;
136
+ readonly cargo: string;
137
+ readonly node: string;
138
+ readonly python: string;
139
+ readonly os: RustReleaseOs;
140
+ readonly cpu: string;
141
+ readonly libc: string;
142
+ readonly packages: readonly string[];
143
+ readonly sources: readonly string[];
144
+ readonly binaries: readonly string[];
145
+ readonly features: readonly string[];
146
+ readonly fingerprintConfig: string;
147
+ }
135
148
 
136
149
  export interface RustReleasePlan {
137
150
  readonly releaseRustVersion: string;
138
151
  readonly releaseTask: string;
139
152
  readonly releaseHelper: string;
140
- readonly rustRoot: string;
141
153
  readonly bindings: readonly RustReleaseBinding[];
142
154
  readonly nodeBindings: readonly RustReleaseBinding[];
143
155
  readonly releaseBinaries: readonly RustReleaseBinaryPlan[];
144
156
  readonly publicCrates: readonly string[];
145
- readonly targets: readonly RustReleaseTargetPlan[];
146
- readonly hasReleaseExclusions: boolean;
157
+ readonly uniffiTargets: readonly RustReleaseTargetPlan[];
158
+ readonly binaryTargets: readonly RustReleaseTargetPlan[];
147
159
  readonly hasPythonBindings: boolean;
148
160
  readonly usesCargoLock: boolean;
149
161
  readonly usePreinstalledWindowsRust: boolean;
150
162
  readonly hasTargetOutputs: boolean;
151
163
  }
152
164
 
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
+ function releaseTargetPlan(
187
+ target: UniFFIReleaseTarget,
188
+ packages: readonly RustProject[],
189
+ sources: readonly string[],
190
+ binaries: readonly string[],
191
+ features: readonly string[],
192
+ ): RustReleaseTargetPlan {
193
+ const packageNames = packages.map((pkg) => pkg.crateName).sort();
194
+ const sortedBinaries = [...new Set(binaries)].sort();
195
+ const sortedFeatures = [...new Set(features)].sort();
196
+ return {
197
+ runner: target.runner,
198
+ cargo: target.cargo,
199
+ node: target.node,
200
+ python: target.python,
201
+ os: target.os,
202
+ cpu: target.cpu,
203
+ libc: target.libc ?? "",
204
+ packages: packageNames,
205
+ sources,
206
+ binaries: sortedBinaries,
207
+ features: sortedFeatures,
208
+ fingerprintConfig: JSON.stringify({
209
+ packages: packageNames,
210
+ binaries: sortedBinaries,
211
+ features: sortedFeatures,
212
+ }),
213
+ };
214
+ }
215
+
153
216
  export function planRustRelease(
154
217
  project: javascript.NodeProject,
155
218
  options: DBXToolsRustWorkspaceOptions,
@@ -195,39 +258,58 @@ export function planRustRelease(
195
258
  dependencies: packageDependencies(pkg).map((dependency) => dependency.crateName),
196
259
  })),
197
260
  ).map((pkg) => pkg.crate);
198
- const hasReleaseExclusions = packages.some((pkg) => pkg.packageOptions.releaseExcludeOs?.length);
199
- const plannedTargets: RustReleaseTargetPlan[] = targets.map((target) => {
200
- const cargoExcludes = packages
201
- .filter((pkg) => pkg.packageOptions.releaseExcludeOs?.includes(target.os))
202
- .map((pkg) => `--exclude ${pkg.crateName}`)
203
- .join(" ");
204
- return {
205
- runner: target.runner,
206
- cargo: target.cargo,
207
- node: target.node,
208
- python: target.python,
209
- os: target.os,
210
- cpu: target.cpu,
211
- ...(target.libc ? { libc: target.libc } : {}),
212
- ...(hasReleaseExclusions ? { cargoExcludes } : {}),
213
- };
261
+ const bindingPackages = packages.filter((pkg) =>
262
+ bindings.some((binding) => binding.crate === pkg.crateName),
263
+ );
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) => {
278
+ const selectedBinaries = releaseBinaries.filter(
279
+ (binary) => !binary.excludedOs.includes(target.os),
280
+ );
281
+ const binaryPackages = packages.filter((pkg) =>
282
+ selectedBinaries.some((binary) => binary.crate === pkg.crateName),
283
+ );
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}`),
288
+ );
289
+ return [
290
+ releaseTargetPlan(
291
+ target,
292
+ binaryPackages,
293
+ packageSources(project, binaryClosure),
294
+ selectedBinaries.map((binary) => binary.binary),
295
+ features,
296
+ ),
297
+ ];
214
298
  });
215
- const hasTargetOutputs =
216
- bindings.length > 0 || releaseBinaries.length > 0 || publicCrates.length > 0;
217
- if (hasTargetOutputs && plannedTargets.length === 0) {
299
+ const hasTargetOutputs = uniffiTargets.length > 0 || binaryTargets.length > 0;
300
+ if (hasTargetOutputs && targets.length === 0) {
218
301
  throw new Error("Rust release requires at least one target");
219
302
  }
220
303
  return {
221
304
  releaseRustVersion,
222
305
  releaseTask: ".projen/uniffi-release.mjs",
223
306
  releaseHelper: ".projen/rust-release.mjs",
224
- rustRoot: options.root ?? "packages/rs",
225
307
  bindings,
226
308
  nodeBindings: bindings.filter((binding) => Boolean(binding.node && binding.nodePackage)),
227
309
  releaseBinaries,
228
310
  publicCrates,
229
- targets: plannedTargets,
230
- hasReleaseExclusions,
311
+ uniffiTargets,
312
+ binaryTargets,
231
313
  hasPythonBindings: bindings.some((binding) => binding.python),
232
314
  usesCargoLock: existsSync(join(project.outdir, "Cargo.lock")),
233
315
  usePreinstalledWindowsRust: releaseRustVersion === "stable",
@@ -235,6 +317,19 @@ export function planRustRelease(
235
317
  };
236
318
  }
237
319
 
320
+ function rustBuildJobIds(plan: RustReleasePlan): string[] {
321
+ return [
322
+ ...(plan.uniffiTargets.length ? ["rust-uniffi"] : []),
323
+ ...(plan.binaryTargets.length ? ["rust-binaries"] : []),
324
+ ];
325
+ }
326
+
327
+ function rustBuildResultCondition(plan: RustReleasePlan): string {
328
+ return rustBuildJobIds(plan)
329
+ .map((job) => `needs.${job}.result != 'failure' && needs.${job}.result != 'cancelled'`)
330
+ .join(" && ");
331
+ }
332
+
238
333
  export function configureRustReleaseTask(
239
334
  project: javascript.NodeProject,
240
335
  plan: RustReleasePlan,
@@ -264,20 +359,31 @@ export function configureRustReleaseTask(
264
359
  } else {
265
360
  for (const path of bindingSupportFiles) project.tryRemoveFile(path);
266
361
  }
267
- project.addTask("rs:release-fingerprint", {
362
+ const fingerprintTask = project.addTask("rs:release-fingerprint", {
268
363
  description: "Write the version-independent Rust release build manifest",
269
- exec: [
270
- `node ${plan.releaseHelper} fingerprint`,
271
- "--root .",
272
- ...plan.targets.map(
273
- (target) =>
274
- `--target ${JSON.stringify(`${String(target.cargo)}|${String(target.cargoExcludes ?? "")}`)}`,
275
- ),
276
- `--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
277
- `--source ${JSON.stringify(plan.rustRoot)}`,
278
- "--portable",
279
- ].join(" "),
280
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
+ }
281
387
  }
282
388
 
283
389
  function rustBindingCommands(plan: RustReleasePlan, independent: boolean): string[] {
@@ -352,61 +458,42 @@ function rustBinaryCommands(plan: RustReleasePlan, independent: boolean): string
352
458
  });
353
459
  }
354
460
 
355
- function rustAdditionalBinaryBuildCommands(plan: RustReleasePlan, independent: boolean): string[] {
356
- return plan.releaseBinaries
357
- .filter((binary) => binary.requiredFeatures.length)
358
- .flatMap((binary) => {
359
- const command = `cargo build --release --package "${binary.crate}" --bin "${binary.binary}" --features "${binary.requiredFeatures.join(",")}"${
360
- plan.usesCargoLock ? " --locked" : ""
361
- } --target "\${{ matrix.cargo }}"`;
362
- const excludedCondition = binary.excludedOs
363
- .map((os) => `[ "\${{ matrix.os }}" != "${os}" ]`)
364
- .join(" && ");
365
- const platformCommands = excludedCondition
366
- ? [`if ${excludedCondition}; then`, ` ${command}`, "fi"]
367
- : [command];
368
- if (!independent) return platformCommands;
369
- const unit = defaultReleaseUnitId("rust", binary.crate);
370
- return [
371
- `if jq -e --arg unit "${unit}" '.units[] | select(.id == $unit)' dist/release-plan.json >/dev/null; then`,
372
- ...platformCommands.map((line) => ` ${line}`),
373
- "fi",
374
- ];
375
- });
376
- }
461
+ type RustBuildKind = "uniffi" | "binaries";
377
462
 
378
- function rustArtifactSteps(plan: RustReleasePlan): JobStep[] {
463
+ function rustArtifactSteps(plan: RustReleasePlan, kind: RustBuildKind): JobStep[] {
379
464
  const binaryCrates = [...new Set(plan.releaseBinaries.map((binary) => binary.crate))];
380
465
  return [
381
- ...plan.bindings.flatMap((binding) => [
382
- ...(binding.node
383
- ? [
384
- {
385
- name: `Upload ${binding.crate} native npm package`,
386
- uses: "actions/upload-artifact@v7",
387
- with: {
388
- name: `${binding.crate}-\${{ matrix.node }}-npm`,
389
- path: `dist/release/${binding.crate}/\${{ matrix.node }}/npm/*.tgz`,
390
- "retention-days": 7,
391
- },
392
- },
393
- ]
394
- : []),
395
- ...(binding.python
396
- ? [
397
- {
398
- name: `Upload ${binding.crate} Python wheel`,
399
- uses: "actions/upload-artifact@v7",
400
- with: {
401
- name: `${binding.pythonPackage}--\${{ matrix.python }}--python-wheel`,
402
- path: `dist/release/${binding.crate}/\${{ matrix.node }}/python/*.whl`,
403
- "retention-days": 7,
404
- },
405
- },
406
- ]
407
- : []),
408
- ]),
409
- ...binaryCrates.map((crate) => {
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) => {
410
497
  const binaries = plan.releaseBinaries.filter((binary) => binary.crate === crate);
411
498
  const excludedOs =
412
499
  binaries[0]?.excludedOs.filter((os) =>
@@ -428,18 +515,43 @@ function rustArtifactSteps(plan: RustReleasePlan): JobStep[] {
428
515
  ];
429
516
  }
430
517
 
431
- export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly JobStep[]): Job {
432
- const bindingCommands = rustBindingCommands(plan, Boolean(independentSetup));
433
- const binaryCommands = rustBinaryCommands(plan, Boolean(independentSetup));
434
- const additionalBinaryBuildCommands = rustAdditionalBinaryBuildCommands(
435
- plan,
436
- Boolean(independentSetup),
437
- );
518
+ /** Build a Bash loop that preserves matrix values across LF and CRLF runners. */
519
+ function matrixArgumentLoop(variable: string, field: string, flag: string): string {
520
+ return `while IFS= read -r ${variable}; do ${variable}="\${${variable}%$'\\r'}"; ${variable}_ARGS+=(${flag} "$${variable}"); done < <(jq -r '.[]' <<<'\${{ toJSON(matrix.${field}) }}')`;
521
+ }
522
+
523
+ function rustCargoBuildCommand(plan: RustReleasePlan, kind: RustBuildKind): string {
524
+ return [
525
+ "PACKAGE_ARGS=()",
526
+ "FEATURE_ARGS=()",
527
+ matrixArgumentLoop("PACKAGE", "packages", "--package"),
528
+ 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[@]}"${
533
+ plan.usesCargoLock ? " --locked" : ""
534
+ } --target "\${{ matrix.cargo }}"`,
535
+ ].join("\n");
536
+ }
537
+
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();
438
550
  return {
439
551
  if: independentSetup
440
- ? "${{ needs.release-plan.outputs.rust_targets != '[]' && (github.event_name == 'push' || inputs.stage != 'docs') }}"
552
+ ? `\${{ needs.release-plan.outputs.${outputName} != '[]' && (github.event_name == 'push' || inputs.stage != 'docs') }}`
441
553
  : "${{ github.event_name == 'push' || inputs.stage == 'all' }}",
442
- name: "${{ matrix.node }}",
554
+ name: `${title} / \${{ matrix.node }}`,
443
555
  needs: [independentSetup ? "release-plan" : "verify-context"],
444
556
  runsOn: ["${{ matrix.runner }}"],
445
557
  permissions: { contents: JobPermission.READ },
@@ -451,13 +563,15 @@ export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly
451
563
  failFast: false,
452
564
  matrix: {
453
565
  include: independentSetup
454
- ? ("${{ fromJSON(needs.release-plan.outputs.rust_targets) }}" as never)
455
- : [...plan.targets],
566
+ ? (`\${{ fromJSON(needs.release-plan.outputs.${outputName}) }}` as never)
567
+ : ([...targets] as never),
456
568
  },
457
569
  },
458
570
  steps: [
459
571
  ...(independentSetup ?? releaseSourceSteps()),
460
- ...(plan.hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
572
+ ...(kind === "uniffi" && plan.hasPythonBindings
573
+ ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }]
574
+ : []),
461
575
  {
462
576
  name: "Setup Rust",
463
577
  ...(plan.usePreinstalledWindowsRust ? { if: "${{ matrix.os != 'win32' }}" } : {}),
@@ -484,15 +598,20 @@ export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly
484
598
  shell: "bash",
485
599
  run: "rustup component add llvm-tools-preview",
486
600
  },
487
- ...rustCacheSteps(`release-\${{ matrix.cargo }}-rust-${plan.releaseRustVersion}`),
601
+ ...rustCacheSteps(`release-${kind}-\${{ matrix.cargo }}-rust-${plan.releaseRustVersion}`),
488
602
  {
489
603
  name: "Install Linux native dependencies",
490
604
  if: "${{ matrix.os == 'linux' }}",
491
- run: [
492
- "sudo rm -f /etc/apt/sources.list.d/google-chrome.list",
493
- "sudo apt-get update",
494
- "sudo apt-get install --yes libdbus-1-dev pkg-config",
495
- ].join("\n"),
605
+ // prettier-ignore
606
+ run: stringUtils.dedent(
607
+ // ============================================================================
608
+ /*bash*/`
609
+ sudo rm -f /etc/apt/sources.list.d/google-chrome.list
610
+ sudo apt-get update
611
+ sudo apt-get install --yes libdbus-1-dev pkg-config
612
+ `
613
+ // ============================================================================
614
+ ),
496
615
  },
497
616
  {
498
617
  name: "Verify Rust build fingerprint",
@@ -501,29 +620,25 @@ export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly
501
620
  : "${{ github.event_name == 'push' || inputs.stage == 'all' }}",
502
621
  ...(!independentSetup ? { id: "rust-fingerprint" } : {}),
503
622
  shell: "bash",
504
- run: independentSetup
505
- ? [
506
- `node ${plan.releaseHelper} fingerprint --check --root .`,
507
- '--target "${{ matrix.cargo }}|${{ matrix.cargoExcludes }}"',
508
- `--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
509
- `--source ${JSON.stringify(plan.rustRoot)}`,
510
- "--portable",
511
- ].join(" ")
512
- : [
513
- 'RUNTIME_MANIFEST="dist/rust-raw/rust-build-${{ matrix.node }}.json"',
514
- 'mkdir -p "$(dirname "$RUNTIME_MANIFEST")"',
515
- [
516
- `node ${plan.releaseHelper} fingerprint --root .`,
517
- '--output "$RUNTIME_MANIFEST"',
518
- '--target "${{ matrix.cargo }}|${{ matrix.cargoExcludes }}"',
519
- `--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
520
- `--source ${JSON.stringify(plan.rustRoot)}`,
521
- ].join(" "),
522
- 'test "$(jq -r .rustSourceHash "$RUNTIME_MANIFEST")" = "$(jq -r .rustSourceHash .release/rust-build.json)"',
523
- 'KEY="$(jq -r --arg target "${{ matrix.cargo }}" \'.targets[$target] // empty\' "$RUNTIME_MANIFEST")"',
524
- 'test -n "$KEY"',
525
- 'echo "key=$KEY" >> "$GITHUB_OUTPUT"',
526
- ].join("\n"),
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"),
527
642
  },
528
643
  ...(!independentSetup
529
644
  ? [
@@ -538,7 +653,7 @@ export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly
538
653
  /*bash*/`
539
654
  KEY="\${{ steps.rust-fingerprint.outputs.key }}"
540
655
  test -n "$KEY"
541
- ASSET="rust-raw-\${{ matrix.node }}-$KEY.tar.gz"
656
+ ASSET="rust-${kind}-raw-\${{ matrix.node }}-$KEY.tar.gz"
542
657
  mkdir -p dist/rust-raw
543
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)"
544
659
  if [ -n "$MATCH" ]; then
@@ -564,29 +679,14 @@ export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly
564
679
  ]
565
680
  : []),
566
681
  {
567
- name: "Build Rust outputs",
682
+ name: `Build Rust ${title} outputs`,
568
683
  ...(independentSetup ? {} : { if: "${{ steps.raw-native.outputs.hit != 'true' }}" }),
569
684
  shell: "bash",
570
685
  env: {
571
686
  CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER:
572
687
  "${{ matrix.os == 'win32' && 'rust-lld' || '' }}",
573
688
  },
574
- run: timedBash(
575
- "rust_workspace",
576
- [
577
- independentSetup
578
- ? [
579
- "mapfile -t PACKAGES < <(jq -r '.[]' <<<'${{ toJSON(matrix.packages) }}')",
580
- "PACKAGE_ARGS=()",
581
- 'for PACKAGE in "${PACKAGES[@]}"; do PACKAGE_ARGS+=(--package "$PACKAGE"); done',
582
- `cargo build --release --timings "\${PACKAGE_ARGS[@]}"${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"`,
583
- ].join("\n")
584
- : `cargo build --release --timings --workspace${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"${
585
- plan.hasReleaseExclusions ? " ${{ matrix.cargoExcludes }}" : ""
586
- }`,
587
- ...additionalBinaryBuildCommands,
588
- ].join("\n"),
589
- ),
689
+ run: timedBash(`rust_${kind}`, rustCargoBuildCommand(plan, kind)),
590
690
  },
591
691
  ...(!independentSetup
592
692
  ? [
@@ -595,7 +695,7 @@ export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly
595
695
  if: "${{ steps.raw-native.outputs.hit != 'true' }}",
596
696
  uses: "actions/upload-artifact@v7",
597
697
  with: {
598
- name: "rust-${{ matrix.node }}-cargo-timings",
698
+ name: `rust-${kind}-\${{ matrix.node }}-cargo-timings`,
599
699
  path: "target/cargo-timings/*",
600
700
  "retention-days": 14,
601
701
  },
@@ -647,14 +747,14 @@ export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly
647
747
  name: "Upload reusable raw Rust outputs",
648
748
  uses: "actions/upload-artifact@v7",
649
749
  with: {
650
- name: "rust-${{ matrix.node }}-raw",
750
+ name: `rust-${kind}-\${{ matrix.node }}-raw`,
651
751
  path: "dist/rust-raw/*",
652
752
  "retention-days": 7,
653
753
  },
654
754
  },
655
755
  ]
656
756
  : []),
657
- ...rustArtifactSteps(plan),
757
+ ...rustArtifactSteps(plan, kind),
658
758
  ],
659
759
  };
660
760
  }
@@ -665,7 +765,7 @@ export function rustCargoPublishJob(plan: RustReleasePlan, local: boolean): Job
665
765
  if: local
666
766
  ? "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}"
667
767
  : "${{ github.event_name == 'push' }}",
668
- needs: ["verify-context", "rust-build"],
768
+ needs: ["verify-context", ...rustBuildJobIds(plan)],
669
769
  runsOn: [local ? "self-hosted" : "ubuntu-latest"],
670
770
  permissions: { contents: JobPermission.READ },
671
771
  steps: [
@@ -705,11 +805,12 @@ export function independentRustCargoPublishJob(
705
805
  "fi",
706
806
  ].join("\n"),
707
807
  );
808
+ const buildCondition = rustBuildResultCondition(plan);
708
809
  return {
709
810
  if: plan.hasTargetOutputs
710
- ? "${{ always() && needs.release-plan.outputs.rust == 'true' && needs.rust-build.result != 'failure' && needs.rust-build.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'rust') }}"
811
+ ? `\${{ always() && needs.release-plan.outputs.rust == 'true' && ${buildCondition} && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'rust') }}`
711
812
  : "${{ needs.release-plan.outputs.rust == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'rust') }}",
712
- needs: ["release-plan", ...(plan.hasTargetOutputs ? ["rust-build"] : [])],
813
+ needs: ["release-plan", ...rustBuildJobIds(plan)],
713
814
  runsOn: ["ubuntu-latest"],
714
815
  permissions: { contents: JobPermission.READ },
715
816
  env: { BUN_VERSION },
@@ -734,7 +835,7 @@ export function independentRustGitHubReleaseJob(
734
835
  ): Job {
735
836
  return {
736
837
  if: "${{ needs.release-plan.outputs.github == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'github') }}",
737
- needs: ["release-plan", "rust-build"],
838
+ needs: ["release-plan", ...rustBuildJobIds(plan)],
738
839
  runsOn: ["ubuntu-latest"],
739
840
  permissions: { contents: JobPermission.WRITE },
740
841
  env: { BUN_VERSION },
@@ -778,10 +879,10 @@ export function independentRustGitHubReleaseJob(
778
879
  };
779
880
  }
780
881
 
781
- export function rustGitHubReleaseJob(): Job {
882
+ export function rustGitHubReleaseJob(plan: RustReleasePlan): Job {
782
883
  return {
783
884
  if: "${{ github.event_name == 'push' }}",
784
- needs: ["verify-context", "rust-build"],
885
+ needs: ["verify-context", ...rustBuildJobIds(plan)],
785
886
  runsOn: ["ubuntu-latest"],
786
887
  permissions: { contents: JobPermission.WRITE },
787
888
  steps: [
@@ -847,7 +948,9 @@ export function rustGitHubReleaseJob(): Job {
847
948
  if (release.draft || !version || compareVersions(version, currentVersion) >= 0) {
848
949
  return [];
849
950
  }
850
- return release.assets.filter((asset) => /^rust-(?:raw|build)-/.test(asset.name));
951
+ return release.assets.filter((asset) =>
952
+ /^rust-(?:(?:uniffi|binaries)-(?:raw|build)|(?:raw|build))-/.test(asset.name)
953
+ );
851
954
  });
852
955
  for (const asset of assets) {
853
956
  core.info(\`Deleting \${asset.name} (\${asset.id})\`);
@@ -869,7 +972,7 @@ export function rustGitHubReleaseJob(): Job {
869
972
  export function independentRustNativeNpmPublishJob(project: DBXToolsJavaScriptProject): Job {
870
973
  return {
871
974
  if: "${{ needs.release-plan.outputs.node == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
872
- needs: ["release-plan", "rust-build"],
975
+ needs: ["release-plan", "rust-uniffi"],
873
976
  runsOn: ["ubuntu-latest"],
874
977
  permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
875
978
  timeoutMinutes: 15,
@@ -932,8 +1035,8 @@ export function independentRustNodeFacadePublishJob(
932
1035
 
933
1036
  export function rustNativeNpmPublishJob(project: DBXToolsJavaScriptProject): Job {
934
1037
  return {
935
- if: "${{ always() && needs.verify-context.result == 'success' && needs.rust-build.result != 'failure' && needs.rust-build.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
936
- needs: ["verify-context", "rust-build"],
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"],
937
1040
  runsOn: ["ubuntu-latest"],
938
1041
  permissions: {
939
1042
  actions: JobPermission.READ,