@dbx-tools/projen 0.9.4 → 0.9.6

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.
Files changed (47) hide show
  1. package/README.md +64 -16
  2. package/index.ts +7 -5
  3. package/package.json +9 -6
  4. package/src/_rust-project.ts +356 -0
  5. package/src/_rust-release-workflow.ts +901 -0
  6. package/src/_rust-release.ts +605 -0
  7. package/src/_task-command.ts +89 -0
  8. package/src/barrels.ts +9 -7
  9. package/src/engine-root.ts +5 -5
  10. package/src/openapi.ts +1 -1
  11. package/src/packages.ts +9 -9
  12. package/src/pnpm-workspace.ts +1 -1
  13. package/src/project-js.ts +19 -25
  14. package/src/project-py.ts +6 -6
  15. package/src/project-rs.ts +100 -1098
  16. package/src/projen-version.ts +9 -0
  17. package/src/publish.ts +3 -4
  18. package/src/release-catalog.ts +1 -1
  19. package/src/release.ts +11 -12
  20. package/src/scaffold.ts +1 -1
  21. package/src/uniffi.ts +7 -0
  22. package/src/watch.ts +2 -2
  23. package/src/workspace-version.ts +20 -15
  24. package/tasks/barrels.ts +4 -4
  25. package/tasks/build-rust-release.ts +68 -0
  26. package/tasks/bump.ts +3 -2
  27. package/tasks/clean.ts +3 -3
  28. package/tasks/local-publish.ts +21 -3
  29. package/tasks/openapi.ts +2 -2
  30. package/tasks/publish-npm.ts +12 -6
  31. package/tasks/publish-python.ts +2 -2
  32. package/tasks/publish-uniffi-local.ts +11 -15
  33. package/tasks/publish.ts +88 -88
  34. package/tasks/python-registry.ts +1 -1
  35. package/tasks/release-bootstrap.ts +2 -2
  36. package/tasks/release-plan.ts +1 -1
  37. package/tasks/release-please.ts +1 -1
  38. package/tasks/release-pr.ts +60 -115
  39. package/tasks/release-request.ts +8 -29
  40. package/tasks/release-summary-units.ts +2 -9
  41. package/tasks/release-summary.ts +35 -24
  42. package/tasks/release-version.ts +44 -0
  43. package/tasks/rust-release.d.ts +41 -0
  44. package/tasks/rust-release.mjs +1717 -0
  45. package/tasks/uniffi-python.js +1 -1
  46. package/tasks/uniffi.ts +7 -1
  47. package/tasks/version-check.ts +6 -5
package/src/project-rs.ts CHANGED
@@ -1,13 +1,37 @@
1
1
  /** Filesystem-discovered Rust workspaces and UniFFI binding package wiring. */
2
- import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
- import { createRequire } from "node:module";
4
- import { dirname, join, relative, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { project as coreProject } from "@dbx-tools/core";
7
- import { string } from "@dbx-tools/shared-core";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join, relative, resolve } from "node:path";
4
+ import * as projectUtils from "@dbx-tools/core/project-utils";
5
+ import { stringUtils } from "@dbx-tools/shared-core";
8
6
  import { Component, Project, TextFile, TomlFile, javascript } from "projen";
9
- import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
10
- import { BUN_VERSION } from "./bun-workflow.ts";
7
+ import {
8
+ RustProject,
9
+ type CargoExampleOptions as RustCargoExampleOptions,
10
+ cargoDependency,
11
+ discoverRustCrates,
12
+ structuredTomlSections,
13
+ type CargoDependency as RustCargoDependency,
14
+ type CargoDependencyOptions as RustCargoDependencyOptions,
15
+ type DBXToolsRustProjectOptions as RustProjectOptions,
16
+ type RustCrateOptions as RustCrateConfiguration,
17
+ type RustCliOptions as RustCliConfiguration,
18
+ } from "./_rust-project.ts";
19
+ import {
20
+ configureRustReleaseTask,
21
+ independentRustCargoPublishJob,
22
+ independentRustGitHubReleaseJob,
23
+ independentRustNativeNpmPublishJob,
24
+ independentRustNodeFacadePublishJob,
25
+ orderRustBindings,
26
+ planRustRelease,
27
+ releaseBinaryAssetName,
28
+ rustBuildJob,
29
+ rustCargoPublishJob,
30
+ rustGitHubReleaseJob,
31
+ rustNativeNpmPublishJob,
32
+ rustNodeFacadePublishJob,
33
+ } from "./_rust-release-workflow.ts";
34
+ import { toPosix } from "./packages.ts";
11
35
  import {
12
36
  DBX_TOOLS_LICENSE,
13
37
  type DBXToolsJavaScriptProject,
@@ -16,65 +40,29 @@ import {
16
40
  } from "./project-js.ts";
17
41
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
18
42
  import { pythonModuleName, type PythonPackageOptions } from "./project-py.ts";
19
- import type { DBXToolsProject } from "./project.ts";
20
43
  import { defaultReleaseUnitId, type ReleaseDependencyInput } from "./release-catalog.ts";
21
- import {
22
- RELEASE_SHA,
23
- RELEASE_SUMMARY_FILE,
24
- RELEASE_TAG,
25
- RELEASE_VERSION,
26
- releaseSourceSteps,
27
- } from "./release-dispatch.ts";
28
44
  import {
29
45
  hasNodeRelease,
30
46
  independentReleaseSetupSteps,
31
- npmPublishEnvironment,
32
- nodeReleaseSetupSteps,
33
47
  registerIndependentPublicationJob,
34
- releaseArtifactSteps,
35
- releaseStageCondition,
36
48
  tryReleaseWorkflow,
37
49
  } from "./release.ts";
38
50
  import { readWorkspaceVersion } from "./workspace-version.ts";
39
-
40
- export interface CargoDependencyOptions {
41
- readonly version?: string;
42
- readonly workspace?: boolean;
43
- readonly path?: string;
44
- readonly optional?: boolean;
45
- readonly package?: string;
46
- readonly defaultFeatures?: boolean;
47
- readonly features?: readonly string[];
48
- }
49
-
50
- export type CargoDependency = string | CargoDependencyOptions;
51
-
52
- export interface RustCliOptions {
53
- /** `dbx` subcommand. Defaults to the Rust package directory. */
54
- readonly command?: string;
55
- /** Root CLI help text. Defaults to the Rust package description. */
56
- readonly description?: string;
57
- }
58
-
59
- export interface RustPackageOptions {
60
- readonly directory: string;
61
- readonly description?: string;
62
- /** Keep this crate unpublished and out of public docs. */
63
- readonly private?: boolean;
64
- /** Build this crate's binary for each target and attach it to the GitHub release. */
65
- readonly release?: boolean;
66
- /** Omit this crate and any release binary artifact from these operating systems. */
67
- readonly releaseExcludeOs?: readonly RustReleaseOs[];
68
- readonly dependencies?: Readonly<Record<string, CargoDependency>>;
69
- readonly devDependencies?: Readonly<Record<string, CargoDependency>>;
70
- readonly features?: Readonly<Record<string, readonly string[]>>;
71
- readonly defaultFeatures?: readonly string[];
72
- /** Cargo and release executable name. Defaults to the generated package name. */
73
- readonly binaryName?: string;
74
- /** Publish this release binary through the generated `dbx` command registry. */
75
- readonly cli?: boolean | RustCliOptions;
76
- readonly bindings?: readonly ("node" | "python")[];
77
- readonly uniffiConfig?: Readonly<Record<string, unknown>>;
51
+ export { discoverRustCrates, hasUniFFIBindings } from "./_rust-project.ts";
52
+ export { orderRustBindings } from "./_rust-release-workflow.ts";
53
+
54
+ export type CargoDependency = RustCargoDependency;
55
+ export type CargoDependencyOptions = RustCargoDependencyOptions;
56
+ export type CargoExampleOptions = RustCargoExampleOptions;
57
+ export type DBXToolsRustProjectOptions = RustProjectOptions;
58
+ export type RustCrateOptions = RustCrateConfiguration;
59
+ export type RustCliOptions = RustCliConfiguration;
60
+
61
+ /** Public Rust project facade; implementation lives apart from workspace/release coordination. */
62
+ export class DBXToolsRustProject extends RustProject {
63
+ constructor(options: DBXToolsRustProjectOptions) {
64
+ super(options);
65
+ }
78
66
  }
79
67
 
80
68
  export interface DBXToolsRustWorkspaceOptions {
@@ -88,10 +76,11 @@ export interface DBXToolsRustWorkspaceOptions {
88
76
  readonly license?: string;
89
77
  readonly repository?: string;
90
78
  readonly workspaceDependencies?: Readonly<Record<string, CargoDependency>>;
91
- readonly packages?: Readonly<Record<string, Omit<RustPackageOptions, "directory">>>;
79
+ readonly packages?: Readonly<Record<string, RustCrateOptions>>;
92
80
  readonly nodeRoot?: string;
93
81
  readonly pythonRoot?: string;
94
82
  readonly pythonModulePrefix?: string;
83
+ /** Default publication policy for discovered crates. Individual packages can override it. */
95
84
  readonly private?: boolean;
96
85
  /** Generate release-branch-driven cross-platform UniFFI package releases. */
97
86
  readonly release?: boolean;
@@ -185,38 +174,6 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
185
174
 
186
175
  const UBRN_VERSION = "0.31.0-5";
187
176
  const RELEASE_PLATFORMS_ENV = "DBX_TOOLS_RELEASE_PLATFORMS";
188
- const require = createRequire(import.meta.url);
189
- const RUST_CACHE_ENV = {
190
- CARGO_INCREMENTAL: "0",
191
- CARGO_TERM_COLOR: "always",
192
- } as const;
193
-
194
- function rustCacheSteps(sharedKey: string): readonly JobStep[] {
195
- return [
196
- {
197
- name: "Cache Cargo registry and dependencies",
198
- id: "cargo_cache",
199
- uses: "Swatinem/rust-cache@v2.9.2",
200
- with: {
201
- "cache-targets": true,
202
- "cache-workspace-crates": true,
203
- "add-job-id-key": false,
204
- "add-rust-environment-hash-key": true,
205
- "shared-key": sharedKey,
206
- "save-if": "${{ github.event_name == 'push' }}",
207
- },
208
- },
209
- ];
210
- }
211
-
212
- function timedBash(phase: string, command: string): string {
213
- return [
214
- "SECONDS=0",
215
- `trap 'status=$?; echo "phase=${phase} duration_seconds=$SECONDS status=$status"; exit "$status"' EXIT`,
216
- command,
217
- ].join("\n");
218
- }
219
-
220
177
  function releaseTargets(options: DBXToolsRustWorkspaceOptions): readonly UniFFIReleaseTarget[] {
221
178
  if (options.releaseTargets && options.releasePlatforms) {
222
179
  throw new Error("releaseTargets and releasePlatforms are mutually exclusive");
@@ -253,40 +210,16 @@ function releasePlatformsFromEnvironment(
253
210
  });
254
211
  }
255
212
 
256
- function releaseBinaryAssetName(binaryName: string, nodeTarget: string, os: RustReleaseOs): string {
257
- const extension = os === RustReleaseOs.WINDOWS ? "zip" : "tar.gz";
258
- return `${binaryName}-${nodeTarget}.${extension}`;
259
- }
260
-
261
213
  function rustCliRegistrySource(binaries: readonly RustReleaseBinaryMapping[]): string {
262
214
  return [
263
215
  "// GENERATED by projen - DO NOT EDIT.",
264
216
  "// Rust release package options are the source of truth.",
265
217
  "",
266
- `export const RUST_RELEASE_BINARY_COMMANDS = ${JSON.stringify(binaries, null, 2)} as const;`,
218
+ `export const RELEASE_BINARY_COMMANDS = ${JSON.stringify(binaries, null, 2)} as const;`,
267
219
  "",
268
220
  ].join("\n");
269
221
  }
270
222
 
271
- function taskSource(name: string): string {
272
- const sourceDirectory = dirname(fileURLToPath(import.meta.url));
273
- const candidates = [
274
- resolve(sourceDirectory, `../tasks/${name}`),
275
- resolve(sourceDirectory, `../../tasks/${name}`),
276
- ];
277
- const source = candidates.find(existsSync);
278
- if (!source) throw new Error(`Could not locate tasks/${name}`);
279
- return readFileSync(source, "utf8");
280
- }
281
-
282
- function smolTomlSource(name: "dist/index.cjs" | "LICENSE"): string {
283
- const entry = require.resolve("smol-toml");
284
- return readFileSync(
285
- resolve(dirname(entry), name === "LICENSE" ? "../LICENSE" : "index.cjs"),
286
- "utf8",
287
- );
288
- }
289
-
290
223
  /** Update only source-less workspace package entries in Cargo.lock. */
291
224
  export function synchronizeCargoLockVersions(
292
225
  content: string,
@@ -364,198 +297,6 @@ export interface RustReleaseBinaryMapping {
364
297
  readonly assets: readonly RustReleaseBinaryAssetMapping[];
365
298
  }
366
299
 
367
- export function orderRustBindings(bindings: readonly RustBindingMapping[]): RustBindingMapping[] {
368
- const ordered: RustBindingMapping[] = [];
369
- const visiting = new Set<string>();
370
- const completed = new Set<string>();
371
- const visit = (binding: RustBindingMapping): void => {
372
- if (completed.has(binding.crate)) return;
373
- if (visiting.has(binding.crate)) {
374
- throw new Error(`Cyclic Rust binding dependency: ${binding.crate}`);
375
- }
376
- visiting.add(binding.crate);
377
- for (const name of binding.dependencies ?? []) {
378
- const dependency = bindings.find((candidate) => candidate.crate === name);
379
- if (!dependency) throw new Error(`Missing Rust binding dependency: ${name}`);
380
- visit(dependency);
381
- }
382
- visiting.delete(binding.crate);
383
- completed.add(binding.crate);
384
- ordered.push(binding);
385
- };
386
- for (const binding of bindings) visit(binding);
387
- return ordered;
388
- }
389
-
390
- function rustSources(directory: string): string[] {
391
- if (!existsSync(directory)) return [];
392
- const files: string[] = [];
393
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
394
- const path = join(directory, entry.name);
395
- if (entry.isDirectory()) files.push(...rustSources(path));
396
- else if (entry.isFile() && entry.name.endsWith(".rs")) files.push(path);
397
- }
398
- return files;
399
- }
400
-
401
- /** Whether a Rust crate embeds the UniFFI proc-macro scaffolding marker. */
402
- export function hasUniFFIBindings(directory: string): boolean {
403
- return rustSources(join(directory, "src")).some((path) =>
404
- /\buniffi\s*::\s*setup_scaffolding\s*!\s*\(/.test(readFileSync(path, "utf8")),
405
- );
406
- }
407
-
408
- export function discoverRustCrates(root: string): string[] {
409
- if (!existsSync(root)) return [];
410
- return readdirSync(root, { withFileTypes: true })
411
- .filter((entry) => entry.isDirectory())
412
- .filter((entry) => rustSources(join(root, entry.name, "src")).length > 0)
413
- .map((entry) => entry.name)
414
- .sort();
415
- }
416
-
417
- function cargoDependency(
418
- value: CargoDependency,
419
- workspaceVersion?: string,
420
- ): string | Record<string, unknown> {
421
- if (typeof value === "string") return value;
422
- return {
423
- ...(value.version ? { version: value.version } : {}),
424
- ...(!value.version && value.path && workspaceVersion ? { version: workspaceVersion } : {}),
425
- ...(value.workspace ? { workspace: true } : {}),
426
- ...(value.path ? { path: value.path } : {}),
427
- ...(value.package ? { package: value.package } : {}),
428
- ...(value.optional ? { optional: true } : {}),
429
- ...(value.defaultFeatures === false ? { "default-features": false } : {}),
430
- ...(value.features?.length ? { features: [...value.features] } : {}),
431
- };
432
- }
433
-
434
- function structuredTomlSections(value: Readonly<Record<string, unknown>>): Record<string, unknown> {
435
- const result: Record<string, unknown> = {};
436
- for (const [section, contents] of Object.entries(value)) {
437
- const path = section.split(".");
438
- let target = result;
439
- for (const part of path.slice(0, -1)) {
440
- const current = target[part];
441
- if (current === undefined) {
442
- target[part] = {};
443
- } else if (!current || typeof current !== "object" || Array.isArray(current)) {
444
- throw new Error(`Conflicting TOML section ${section}`);
445
- }
446
- target = target[part] as Record<string, unknown>;
447
- }
448
- target[path.at(-1)!] = contents;
449
- }
450
- return result;
451
- }
452
-
453
- /** One generated Cargo workspace member. */
454
- export class DBXToolsRustProject extends Project implements DBXToolsProject {
455
- readonly language = "rust" as const;
456
- readonly crateName: string;
457
- readonly packageOptions: RustPackageOptions;
458
- readonly uniffi: boolean;
459
- readonly manifestFile: TomlFile;
460
-
461
- constructor(
462
- parent: javascript.NodeProject,
463
- root: string,
464
- scope: string,
465
- options: RustPackageOptions,
466
- ) {
467
- const crateName = `${string.toSlug(scope)}-${options.directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`;
468
- super({ parent, outdir: `${root}/${options.directory}`, name: crateName });
469
- this.crateName = crateName;
470
- this.packageOptions = options;
471
- this.uniffi = hasUniFFIBindings(this.outdir);
472
- const library = existsSync(join(this.outdir, "src/lib.rs"));
473
- const binary = existsSync(join(this.outdir, "src/main.rs"));
474
- const binaryName = options.binaryName ?? crateName;
475
- const packageVersion =
476
- isDBXToolsJavaScriptProject()(parent) && parent.releaseCatalog.mode === "independent"
477
- ? parent.releaseCatalog.versionForRegistration(
478
- "rust",
479
- crateName,
480
- `${root}/${options.directory}`,
481
- )
482
- : { workspace: true };
483
- const manifest: Record<string, unknown> = {
484
- package: {
485
- name: crateName,
486
- version: packageVersion,
487
- edition: { workspace: true },
488
- "rust-version": { workspace: true },
489
- description: options.description ?? crateName,
490
- license: { workspace: true },
491
- repository: { workspace: true },
492
- ...(options.private ? { publish: false } : {}),
493
- },
494
- ...(library
495
- ? {
496
- lib: {
497
- name: crateName.replaceAll("-", "_"),
498
- path: "src/lib.rs",
499
- ...(this.uniffi ? { "crate-type": ["lib", "cdylib"] } : {}),
500
- },
501
- }
502
- : {}),
503
- ...(this.uniffi || binary
504
- ? {
505
- bin: [
506
- this.uniffi
507
- ? { name: `${crateName}-uniffi-bindgen`, path: "uniffi-bindgen.rs" }
508
- : { name: binaryName, path: "src/main.rs" },
509
- ],
510
- }
511
- : {}),
512
- ...(options.features || options.defaultFeatures
513
- ? {
514
- features: {
515
- ...(options.defaultFeatures ? { default: [...options.defaultFeatures] } : {}),
516
- ...options.features,
517
- },
518
- }
519
- : {}),
520
- ...(options.dependencies
521
- ? {
522
- dependencies: Object.fromEntries(
523
- Object.entries(options.dependencies).map(([name, value]) => [
524
- name,
525
- cargoDependency(value, readWorkspaceVersion(parent.outdir)),
526
- ]),
527
- ),
528
- }
529
- : {}),
530
- ...(options.devDependencies
531
- ? {
532
- "dev-dependencies": Object.fromEntries(
533
- Object.entries(options.devDependencies).map(([name, value]) => [
534
- name,
535
- cargoDependency(value, readWorkspaceVersion(parent.outdir)),
536
- ]),
537
- ),
538
- }
539
- : {}),
540
- };
541
- this.manifestFile = new TomlFile(this, "Cargo.toml", { marker: false, obj: manifest });
542
- if (this.uniffi) {
543
- new TextFile(this, "uniffi-bindgen.rs", {
544
- lines: ["fn main() {", " uniffi::uniffi_bindgen_main();", "}", ""],
545
- });
546
- new TomlFile(this, "uniffi.toml", {
547
- marker: false,
548
- obj: structuredTomlSections(
549
- options.uniffiConfig ?? {
550
- "bindings.python": { cdylib_name: crateName.replaceAll("-", "_") },
551
- "bindings.typescript": { strictTypeChecking: true },
552
- },
553
- ),
554
- });
555
- }
556
- }
557
- }
558
-
559
300
  function rustPackageDependencies(
560
301
  packages: readonly DBXToolsRustProject[],
561
302
  pkg: DBXToolsRustProject,
@@ -592,7 +333,8 @@ interface ResolvedRustWorkspaceOptions {
592
333
  readonly repository: string;
593
334
  readonly nativeTargets: readonly UniFFIReleaseTarget[];
594
335
  readonly pythonModulePrefix: string;
595
- readonly packageOptions: Readonly<Record<string, Omit<RustPackageOptions, "directory">>>;
336
+ readonly packageOptions: Readonly<Record<string, RustCrateOptions>>;
337
+ readonly private?: boolean;
596
338
  readonly release: boolean;
597
339
  }
598
340
 
@@ -601,7 +343,7 @@ function resolveRustWorkspaceOptions(
601
343
  options: DBXToolsRustWorkspaceOptions,
602
344
  ): ResolvedRustWorkspaceOptions {
603
345
  const dbxToolsProject = isDBXToolsJavaScriptProject()(project) ? project : undefined;
604
- const scope = string.toSlug(options.scope ?? dbxToolsProject?.scope ?? project.name);
346
+ const scope = stringUtils.toSlug(options.scope ?? dbxToolsProject?.scope ?? project.name);
605
347
  return {
606
348
  root: options.root ?? "packages/rs",
607
349
  nodeRoot: options.nodeRoot ?? "packages/js/node",
@@ -610,11 +352,12 @@ function resolveRustWorkspaceOptions(
610
352
  repository:
611
353
  options.repository ??
612
354
  projectRepositoryUrl(project) ??
613
- coreProject.repositoryUrl(project.outdir) ??
355
+ projectUtils.repositoryUrl(project.outdir) ??
614
356
  "",
615
357
  nativeTargets: releaseTargets(options),
616
358
  pythonModulePrefix: options.pythonModulePrefix ?? scope.replaceAll("-", "_"),
617
359
  packageOptions: options.packages ?? {},
360
+ private: options.private,
618
361
  release: options.release ?? true,
619
362
  };
620
363
  }
@@ -750,7 +493,7 @@ function planRustBindings(
750
493
  const mappings = orderRustBindings(
751
494
  bindings.map((pkg) => {
752
495
  const targets = pkg.packageOptions.bindings ?? ["node", "python"];
753
- const packageDirectory = `${string.toSlug(pkg.packageOptions.directory)}-rs`;
496
+ const packageDirectory = `${stringUtils.toSlug(pkg.packageOptions.directory)}-rs`;
754
497
  const direct = dependencies.get(pkg);
755
498
  const dependencyCrates = [
756
499
  ...new Set([...(direct?.node ?? []), ...(direct?.python ?? [])]),
@@ -793,7 +536,7 @@ function planPythonBindingPackages(
793
536
  return plan.packages
794
537
  .filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("python"))
795
538
  .map((pkg) => {
796
- const directory = `${string.toSlug(pkg.packageOptions.directory)}-rs`;
539
+ const directory = `${stringUtils.toSlug(pkg.packageOptions.directory)}-rs`;
797
540
  const module = pythonModuleName(resolved.pythonModulePrefix, directory);
798
541
  const name = `${resolved.scope}-${directory}`;
799
542
  return {
@@ -803,7 +546,7 @@ function planPythonBindingPackages(
803
546
  description: `Python bindings for ${pkg.crateName}`,
804
547
  uniffi: true,
805
548
  internalDependencies: bindingDependencies(plan, pkg, "python").map(
806
- (dependency) => `${string.toSlug(dependency.packageOptions.directory)}-rs`,
549
+ (dependency) => `${stringUtils.toSlug(dependency.packageOptions.directory)}-rs`,
807
550
  ),
808
551
  generatedSources: [
809
552
  `src/${module.replaceAll(".", "/")}/bindings.py`,
@@ -835,7 +578,7 @@ function planNodeBindingPackages(
835
578
  return plan.packages
836
579
  .filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("node"))
837
580
  .map((binding) => {
838
- const directory = `${string.toSlug(binding.packageOptions.directory)}-rs`;
581
+ const directory = `${stringUtils.toSlug(binding.packageOptions.directory)}-rs`;
839
582
  const version = isDBXToolsJavaScriptProject()(project)
840
583
  ? project.releaseCatalog.versionFor(binding)
841
584
  : readWorkspaceVersion(project.outdir);
@@ -853,7 +596,7 @@ function planNodeBindingPackages(
853
596
  : {},
854
597
  internalDependencies: bindingDependencies(plan, binding, "node").map(
855
598
  (dependency) =>
856
- `@${resolved.scope}/${string.toSlug(dependency.packageOptions.directory)}-rs@workspace:*`,
599
+ `@${resolved.scope}/${stringUtils.toSlug(dependency.packageOptions.directory)}-rs@workspace:*`,
857
600
  ),
858
601
  };
859
602
  });
@@ -863,13 +606,32 @@ function createRustPackages(
863
606
  project: javascript.NodeProject,
864
607
  resolved: ResolvedRustWorkspaceOptions,
865
608
  ): DBXToolsRustProject[] {
866
- return discoverRustCrates(resolve(project.outdir, resolved.root)).map(
867
- (directory) =>
868
- new DBXToolsRustProject(project, resolved.root, resolved.scope, {
869
- directory,
870
- ...resolved.packageOptions[directory],
871
- }),
872
- );
609
+ return discoverRustCrates(resolve(project.outdir, resolved.root)).map((directory) => {
610
+ const configured = {
611
+ private: resolved.private,
612
+ ...resolved.packageOptions[directory],
613
+ };
614
+ const name = `${resolved.scope}-${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`;
615
+ const version =
616
+ isDBXToolsJavaScriptProject()(project) && project.releaseCatalog.mode === "independent"
617
+ ? project.releaseCatalog.versionForRegistration(
618
+ "rust",
619
+ name,
620
+ `${resolved.root}/${directory}`,
621
+ )
622
+ : undefined;
623
+ return new DBXToolsRustProject({
624
+ parent: project,
625
+ outdir: `${resolved.root}/${directory}`,
626
+ name,
627
+ directory,
628
+ workspaceRoot: resolved.root,
629
+ workspace: true,
630
+ scope: resolved.scope,
631
+ ...(version ? { version } : {}),
632
+ ...configured,
633
+ });
634
+ });
873
635
  }
874
636
 
875
637
  function configureRustCliRegistry(
@@ -905,7 +667,7 @@ function configureRustBindingFiles(
905
667
  dependency.crateName.replaceAll("-", "_"),
906
668
  `${pythonModuleName(
907
669
  resolved.pythonModulePrefix,
908
- `${string.toSlug(dependency.packageOptions.directory)}-rs`,
670
+ `${stringUtils.toSlug(dependency.packageOptions.directory)}-rs`,
909
671
  )}.bindings`,
910
672
  ]),
911
673
  ),
@@ -1053,775 +815,6 @@ function configureRustWorkspaceTasks(project: javascript.NodeProject): void {
1053
815
  project.removeTask("rs:bindings:demo");
1054
816
  }
1055
817
 
1056
- interface RustReleaseBinding extends RustBindingMapping {
1057
- readonly node: string;
1058
- readonly python: string;
1059
- readonly nodePackage: string;
1060
- readonly pythonPackage: string;
1061
- }
1062
-
1063
- interface RustReleaseBinaryPlan {
1064
- readonly crate: string;
1065
- readonly binary: string;
1066
- readonly excludedOs: readonly RustReleaseOs[];
1067
- }
1068
-
1069
- type RustReleaseTargetPlan = Readonly<Record<string, string | boolean | number>>;
1070
-
1071
- interface RustReleasePlan {
1072
- readonly releaseRustVersion: string;
1073
- readonly releaseTask: string;
1074
- readonly bindings: readonly RustReleaseBinding[];
1075
- readonly nodeBindings: readonly RustReleaseBinding[];
1076
- readonly releaseBinaries: readonly RustReleaseBinaryPlan[];
1077
- readonly publicCrates: readonly string[];
1078
- readonly targets: readonly RustReleaseTargetPlan[];
1079
- readonly hasReleaseExclusions: boolean;
1080
- readonly hasPythonBindings: boolean;
1081
- readonly usesCargoLock: boolean;
1082
- readonly usePreinstalledWindowsRust: boolean;
1083
- readonly hasTargetOutputs: boolean;
1084
- }
1085
-
1086
- function planRustRelease(
1087
- project: javascript.NodeProject,
1088
- options: DBXToolsRustWorkspaceOptions,
1089
- targets: readonly UniFFIReleaseTarget[],
1090
- packages: readonly DBXToolsRustProject[],
1091
- bindingMappings: readonly RustBindingMapping[],
1092
- ): RustReleasePlan {
1093
- const releaseRustVersion = options.releaseRustVersion ?? "stable";
1094
- const bindings = bindingMappings.map((binding) => ({
1095
- ...binding,
1096
- node: binding.node ?? "",
1097
- python: binding.python ?? "",
1098
- nodePackage: binding.nodePackage ?? "",
1099
- pythonPackage: binding.pythonPackage ?? "",
1100
- }));
1101
- const releaseBinaries = packages
1102
- .filter((pkg) => pkg.packageOptions.release)
1103
- .map((pkg) => ({
1104
- crate: pkg.crateName,
1105
- binary: pkg.packageOptions.binaryName ?? pkg.crateName,
1106
- excludedOs: pkg.packageOptions.releaseExcludeOs ?? [],
1107
- }));
1108
- const packageDependencies = createRustPackageDependencyResolver(packages, project, options);
1109
- const publicCrates = orderRustBindings(
1110
- packages
1111
- .filter((pkg) => !pkg.packageOptions.private)
1112
- .map((pkg) => ({
1113
- crate: pkg.crateName,
1114
- rust: pkg.outdir,
1115
- dependencies: packageDependencies(pkg).map((dependency) => dependency.crateName),
1116
- })),
1117
- ).map((pkg) => pkg.crate);
1118
- const hasReleaseExclusions = packages.some((pkg) => pkg.packageOptions.releaseExcludeOs?.length);
1119
- const plannedTargets: RustReleaseTargetPlan[] = targets.map((target) => {
1120
- const cargoExcludes = packages
1121
- .filter((pkg) => pkg.packageOptions.releaseExcludeOs?.includes(target.os))
1122
- .map((pkg) => `--exclude ${pkg.crateName}`)
1123
- .join(" ");
1124
- return {
1125
- runner: target.runner,
1126
- cargo: target.cargo,
1127
- node: target.node,
1128
- python: target.python,
1129
- os: target.os,
1130
- cpu: target.cpu,
1131
- ...(target.libc ? { libc: target.libc } : {}),
1132
- ...(hasReleaseExclusions ? { cargoExcludes } : {}),
1133
- };
1134
- });
1135
- const hasTargetOutputs =
1136
- bindings.length > 0 || releaseBinaries.length > 0 || publicCrates.length > 0;
1137
- if (hasTargetOutputs && plannedTargets.length === 0) {
1138
- throw new Error("Rust release requires at least one target");
1139
- }
1140
- return {
1141
- releaseRustVersion,
1142
- releaseTask: ".projen/uniffi-release.mjs",
1143
- bindings,
1144
- nodeBindings: bindings.filter((binding) => Boolean(binding.node && binding.nodePackage)),
1145
- releaseBinaries,
1146
- publicCrates,
1147
- targets: plannedTargets,
1148
- hasReleaseExclusions,
1149
- hasPythonBindings: bindings.some((binding) => binding.python),
1150
- usesCargoLock: existsSync(join(project.outdir, "Cargo.lock")),
1151
- usePreinstalledWindowsRust: releaseRustVersion === "stable",
1152
- hasTargetOutputs,
1153
- };
1154
- }
1155
-
1156
- function configureRustReleaseTask(project: javascript.NodeProject, plan: RustReleasePlan): void {
1157
- const supportFiles = [
1158
- plan.releaseTask,
1159
- ".projen/uniffi-python.js",
1160
- ".projen/smol-toml.cjs",
1161
- ".projen/smol-toml.LICENSE",
1162
- ] as const;
1163
- if (plan.bindings.length) {
1164
- new TextFile(project, plan.releaseTask, {
1165
- lines: taskSource("uniffi-release.mjs").trimEnd().split("\n"),
1166
- });
1167
- new TextFile(project, supportFiles[1], {
1168
- lines: taskSource("uniffi-python.js").trimEnd().split("\n"),
1169
- });
1170
- new TextFile(project, supportFiles[2], {
1171
- lines: smolTomlSource("dist/index.cjs").trimEnd().split("\n"),
1172
- });
1173
- new TextFile(project, supportFiles[3], {
1174
- lines: smolTomlSource("LICENSE").trimEnd().split("\n"),
1175
- });
1176
- } else {
1177
- for (const path of supportFiles) project.tryRemoveFile(path);
1178
- }
1179
- project.addTask("rs:release-fingerprint", {
1180
- description: "Write the version-independent Rust release build manifest",
1181
- exec: [
1182
- "cargo run --quiet --package dbx-tools-release-tools -- fingerprint",
1183
- ...plan.targets.map(
1184
- (target) =>
1185
- `--target ${JSON.stringify(`${String(target.cargo)}|${String(target.cargoExcludes ?? "")}`)}`,
1186
- ),
1187
- `--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
1188
- "--portable",
1189
- ].join(" "),
1190
- });
1191
- }
1192
-
1193
- function rustBindingCommands(plan: RustReleasePlan, independent: boolean): string[] {
1194
- return plan.bindings.map((binding) => {
1195
- const unit = defaultReleaseUnitId("rust", binding.crate);
1196
- const command = [
1197
- `node ${plan.releaseTask} build`,
1198
- `--crate "${binding.crate}"`,
1199
- `--node "${binding.node}"`,
1200
- `--python "${binding.python}"`,
1201
- `--node-package "${binding.nodePackage}"`,
1202
- `--python-package "${binding.pythonPackage}"`,
1203
- `--python-module "${binding.pythonModule}"`,
1204
- '--cargo-target "${{ matrix.cargo }}"',
1205
- '--node-triple "${{ matrix.node }}"',
1206
- '--python-tag "${{ matrix.python }}"',
1207
- '--os "${{ matrix.os }}"',
1208
- '--cpu "${{ matrix.cpu }}"',
1209
- '--libc "${{ matrix.libc }}"',
1210
- independent
1211
- ? `--version "$(jq -r --arg unit "${unit}" '.units[] | select(.id == $unit) | .newVersion' dist/release-plan.json)"`
1212
- : '--version "$VERSION"',
1213
- `--output "dist/release/${binding.crate}/\${{ matrix.node }}"`,
1214
- "--skip-build",
1215
- ].join(" \\\n ");
1216
- return independent
1217
- ? [
1218
- `if jq -e --arg unit "${unit}" '.units[] | select(.id == $unit)' dist/release-plan.json >/dev/null; then`,
1219
- ` ${command.replaceAll("\n", "\n ")}`,
1220
- "fi",
1221
- ].join("\n")
1222
- : command;
1223
- });
1224
- }
1225
-
1226
- function rustReleaseBinaryCondition(excludedOs: readonly RustReleaseOs[]): string | undefined {
1227
- return excludedOs.length
1228
- ? `\${{ ${excludedOs.map((os) => `matrix.os != '${os}'`).join(" && ")} }}`
1229
- : undefined;
1230
- }
1231
-
1232
- function rustBinaryCommands(plan: RustReleasePlan, independent: boolean): string[] {
1233
- return plan.releaseBinaries.flatMap((pkg) => {
1234
- const unit = defaultReleaseUnitId("rust", pkg.crate);
1235
- const windowsAsset = releaseBinaryAssetName(
1236
- pkg.binary,
1237
- "${{ matrix.node }}",
1238
- RustReleaseOs.WINDOWS,
1239
- );
1240
- const unixAsset = releaseBinaryAssetName(pkg.binary, "${{ matrix.node }}", RustReleaseOs.LINUX);
1241
- const commands = [
1242
- `mkdir -p "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
1243
- `SOURCE="target/\${{ matrix.cargo }}/release/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
1244
- `DESTINATION="dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
1245
- 'cp "$SOURCE" "$DESTINATION"',
1246
- 'if [ "${{ matrix.os }}" = "win32" ]; then',
1247
- ` 7z a "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${windowsAsset}" "$DESTINATION"`,
1248
- "else",
1249
- ` tar -C "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage" -czf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${unixAsset}" "${pkg.binary}"`,
1250
- "fi",
1251
- `rm -rf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
1252
- ];
1253
- const excludedCondition = pkg.excludedOs
1254
- .map((os) => `[ "\${{ matrix.os }}" != "${os}" ]`)
1255
- .join(" && ");
1256
- const platformCommands = excludedCondition
1257
- ? [`if ${excludedCondition}; then`, ...commands.map((command) => ` ${command}`), "fi"]
1258
- : commands;
1259
- return independent
1260
- ? [
1261
- `if jq -e --arg unit "${unit}" '.units[] | select(.id == $unit)' dist/release-plan.json >/dev/null; then`,
1262
- ...platformCommands.map((command) => ` ${command}`),
1263
- "fi",
1264
- ]
1265
- : platformCommands;
1266
- });
1267
- }
1268
-
1269
- function rustArtifactSteps(plan: RustReleasePlan): JobStep[] {
1270
- return [
1271
- ...plan.bindings.flatMap((binding) => [
1272
- ...(binding.node
1273
- ? [
1274
- {
1275
- name: `Upload ${binding.crate} native npm package`,
1276
- uses: "actions/upload-artifact@v7",
1277
- with: {
1278
- name: `${binding.crate}-\${{ matrix.node }}-npm`,
1279
- path: `dist/release/${binding.crate}/\${{ matrix.node }}/npm/*.tgz`,
1280
- "retention-days": 7,
1281
- },
1282
- },
1283
- ]
1284
- : []),
1285
- ...(binding.python
1286
- ? [
1287
- {
1288
- name: `Upload ${binding.crate} Python wheel`,
1289
- uses: "actions/upload-artifact@v7",
1290
- with: {
1291
- name: `${binding.pythonPackage}--\${{ matrix.python }}--python-wheel`,
1292
- path: `dist/release/${binding.crate}/\${{ matrix.node }}/python/*.whl`,
1293
- "retention-days": 7,
1294
- },
1295
- },
1296
- ]
1297
- : []),
1298
- ]),
1299
- ...plan.releaseBinaries.map((pkg) => ({
1300
- name: `Upload ${pkg.crate} release binary`,
1301
- uses: "actions/upload-artifact@v7",
1302
- ...(rustReleaseBinaryCondition(pkg.excludedOs)
1303
- ? { if: rustReleaseBinaryCondition(pkg.excludedOs) }
1304
- : {}),
1305
- with: {
1306
- name: `${pkg.crate}-\${{ matrix.node }}-binary`,
1307
- path: `dist/release/${pkg.crate}/\${{ matrix.node }}/binary/*`,
1308
- "retention-days": 7,
1309
- },
1310
- })),
1311
- ];
1312
- }
1313
-
1314
- function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly JobStep[]): Job {
1315
- const bindingCommands = rustBindingCommands(plan, Boolean(independentSetup));
1316
- const binaryCommands = rustBinaryCommands(plan, Boolean(independentSetup));
1317
- return {
1318
- if: independentSetup
1319
- ? "${{ needs.release-plan.outputs.rust_targets != '[]' && (github.event_name == 'push' || inputs.stage != 'docs') }}"
1320
- : "${{ github.event_name == 'push' || inputs.stage == 'all' }}",
1321
- name: "${{ matrix.node }}",
1322
- needs: [independentSetup ? "release-plan" : "verify-context"],
1323
- runsOn: ["${{ matrix.runner }}"],
1324
- permissions: { contents: JobPermission.READ },
1325
- env: {
1326
- ...RUST_CACHE_ENV,
1327
- ...(independentSetup ? { BUN_VERSION } : {}),
1328
- },
1329
- strategy: {
1330
- failFast: false,
1331
- matrix: {
1332
- include: independentSetup
1333
- ? ("${{ fromJSON(needs.release-plan.outputs.rust_targets) }}" as never)
1334
- : [...plan.targets],
1335
- },
1336
- },
1337
- steps: [
1338
- ...(independentSetup ?? releaseSourceSteps()),
1339
- ...(plan.hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
1340
- {
1341
- name: "Setup Rust",
1342
- ...(plan.usePreinstalledWindowsRust ? { if: "${{ matrix.os != 'win32' }}" } : {}),
1343
- uses: `dtolnay/rust-toolchain@${plan.releaseRustVersion}`,
1344
- with: { targets: "${{ matrix.cargo }}" },
1345
- },
1346
- ...(plan.usePreinstalledWindowsRust
1347
- ? [
1348
- {
1349
- name: "Verify preinstalled Windows Rust",
1350
- if: "${{ matrix.os == 'win32' }}",
1351
- shell: "bash",
1352
- run: [
1353
- "rustc --version --verbose",
1354
- "cargo --version",
1355
- 'rustup target list --installed | grep -Fx "${{ matrix.cargo }}"',
1356
- 'test -f "$(rustc --print sysroot)/lib/rustlib/${{ matrix.cargo }}/bin/rust-lld.exe"',
1357
- ].join("\n"),
1358
- },
1359
- ]
1360
- : []),
1361
- ...rustCacheSteps(`release-\${{ matrix.cargo }}-rust-${plan.releaseRustVersion}`),
1362
- {
1363
- name: "Install Linux native dependencies",
1364
- if: "${{ matrix.os == 'linux' }}",
1365
- run: [
1366
- "sudo rm -f /etc/apt/sources.list.d/google-chrome.list",
1367
- "sudo apt-get update",
1368
- "sudo apt-get install --yes libdbus-1-dev pkg-config",
1369
- ].join("\n"),
1370
- },
1371
- {
1372
- name: "Verify Rust build fingerprint",
1373
- if: independentSetup
1374
- ? undefined
1375
- : "${{ github.event_name == 'push' || inputs.stage == 'all' }}",
1376
- ...(!independentSetup ? { id: "rust-fingerprint" } : {}),
1377
- shell: "bash",
1378
- run: independentSetup
1379
- ? [
1380
- "cargo run --quiet --package dbx-tools-release-tools -- fingerprint --check",
1381
- '--target "${{ matrix.cargo }}|${{ matrix.cargoExcludes }}"',
1382
- `--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
1383
- "--portable",
1384
- ].join(" ")
1385
- : [
1386
- 'RUNTIME_MANIFEST="dist/rust-raw/rust-build-${{ matrix.node }}.json"',
1387
- 'mkdir -p "$(dirname "$RUNTIME_MANIFEST")"',
1388
- [
1389
- "cargo run --quiet --package dbx-tools-release-tools -- fingerprint",
1390
- '--output "$RUNTIME_MANIFEST"',
1391
- '--target "${{ matrix.cargo }}|${{ matrix.cargoExcludes }}"',
1392
- `--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
1393
- ].join(" "),
1394
- 'test "$(jq -r .rustSourceHash "$RUNTIME_MANIFEST")" = "$(jq -r .rustSourceHash .release/rust-build.json)"',
1395
- 'KEY="$(jq -r --arg target "${{ matrix.cargo }}" \'.targets[$target] // empty\' "$RUNTIME_MANIFEST")"',
1396
- 'test -n "$KEY"',
1397
- 'echo "key=$KEY" >> "$GITHUB_OUTPUT"',
1398
- ].join("\n"),
1399
- },
1400
- ...(!independentSetup
1401
- ? [
1402
- {
1403
- name: "Reuse matching raw Rust outputs",
1404
- id: "raw-native",
1405
- shell: "bash",
1406
- env: { GH_TOKEN: "${{ github.token }}" },
1407
- run: [
1408
- 'KEY="${{ steps.rust-fingerprint.outputs.key }}"',
1409
- 'test -n "$KEY"',
1410
- 'ASSET="rust-raw-${{ matrix.node }}-$KEY.tar.gz"',
1411
- "mkdir -p dist/rust-raw",
1412
- '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)"',
1413
- 'if [ -n "$MATCH" ]; then',
1414
- " TAG=\"${MATCH%%$'\\t'*}\"",
1415
- " URL=\"${MATCH#*$'\\t'}\"",
1416
- ' CHECKSUM_URL="$(gh api "repos/${{ github.repository }}/releases/tags/$TAG" --jq \'.assets[] | select(.name == "\'"$ASSET.sha256"\'") | .url\')"',
1417
- ' rm -rf "target/${{ matrix.cargo }}/release"',
1418
- ' 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',
1419
- ' echo "hit=true" >> "$GITHUB_OUTPUT"',
1420
- " else",
1421
- ' rm -rf "target/${{ matrix.cargo }}/release" "dist/rust-raw/$ASSET" "dist/rust-raw/$ASSET.sha256"',
1422
- ' echo "::warning::matching raw Rust bundle failed validation; rebuilding"',
1423
- ' echo "hit=false" >> "$GITHUB_OUTPUT"',
1424
- " fi",
1425
- "else",
1426
- ' echo "hit=false" >> "$GITHUB_OUTPUT"',
1427
- "fi",
1428
- 'echo "asset=$ASSET" >> "$GITHUB_OUTPUT"',
1429
- ].join("\n"),
1430
- },
1431
- ]
1432
- : []),
1433
- {
1434
- name: "Build Rust outputs",
1435
- ...(independentSetup ? {} : { if: "${{ steps.raw-native.outputs.hit != 'true' }}" }),
1436
- shell: "bash",
1437
- env: {
1438
- CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER:
1439
- "${{ matrix.os == 'win32' && 'rust-lld' || '' }}",
1440
- },
1441
- run: timedBash(
1442
- "rust_workspace",
1443
- independentSetup
1444
- ? [
1445
- "mapfile -t PACKAGES < <(jq -r '.[]' <<<'${{ toJSON(matrix.packages) }}')",
1446
- "PACKAGE_ARGS=()",
1447
- 'for PACKAGE in "${PACKAGES[@]}"; do PACKAGE_ARGS+=(--package "$PACKAGE"); done',
1448
- `cargo build --release --timings "\${PACKAGE_ARGS[@]}"${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"`,
1449
- ].join("\n")
1450
- : `cargo build --release --timings --workspace${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"${
1451
- plan.hasReleaseExclusions ? " ${{ matrix.cargoExcludes }}" : ""
1452
- }`,
1453
- ),
1454
- },
1455
- ...(!independentSetup
1456
- ? [
1457
- {
1458
- name: "Upload Cargo timings",
1459
- if: "${{ steps.raw-native.outputs.hit != 'true' }}",
1460
- uses: "actions/upload-artifact@v7",
1461
- with: {
1462
- name: "rust-${{ matrix.node }}-cargo-timings",
1463
- path: "target/cargo-timings/*",
1464
- "retention-days": 14,
1465
- },
1466
- },
1467
- ]
1468
- : []),
1469
- ...(!independentSetup
1470
- ? [
1471
- {
1472
- name: "Archive raw Rust outputs",
1473
- if: "${{ steps.raw-native.outputs.hit != 'true' }}",
1474
- shell: "bash",
1475
- run: [
1476
- "mkdir -p dist/rust-raw",
1477
- 'tar -czf "dist/rust-raw/${{ steps.raw-native.outputs.asset }}" "target/${{ matrix.cargo }}/release"',
1478
- '(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")',
1479
- ].join("\n"),
1480
- },
1481
- {
1482
- name: "Stamp native release version",
1483
- shell: "bash",
1484
- env: { VERSION: RELEASE_VERSION },
1485
- run: 'cargo run --quiet --package dbx-tools-release-tools -- stamp-tree --root "target/${{ matrix.cargo }}/release" --version "$VERSION"',
1486
- },
1487
- ]
1488
- : []),
1489
- ...(bindingCommands.length
1490
- ? [
1491
- {
1492
- name: "Package UniFFI outputs",
1493
- shell: "bash",
1494
- ...(independentSetup ? {} : { env: { VERSION: RELEASE_VERSION } }),
1495
- run: timedBash("uniffi_packaging", bindingCommands.join("\n")),
1496
- },
1497
- ]
1498
- : []),
1499
- ...(binaryCommands.length
1500
- ? [
1501
- {
1502
- name: "Package release binaries",
1503
- shell: "bash",
1504
- run: timedBash("binary_packaging", binaryCommands.join("\n")),
1505
- },
1506
- ]
1507
- : []),
1508
- ...(!independentSetup
1509
- ? [
1510
- {
1511
- name: "Upload reusable raw Rust outputs",
1512
- uses: "actions/upload-artifact@v7",
1513
- with: {
1514
- name: "rust-${{ matrix.node }}-raw",
1515
- path: "dist/rust-raw/*",
1516
- "retention-days": 7,
1517
- },
1518
- },
1519
- ]
1520
- : []),
1521
- ...rustArtifactSteps(plan),
1522
- ],
1523
- };
1524
- }
1525
-
1526
- function rustCargoPublishJob(plan: RustReleasePlan, local: boolean): Job {
1527
- const registry = local ? '"${{ vars.LOCAL_CARGO_REGISTRY }}"' : "crates-io";
1528
- return {
1529
- if: local
1530
- ? "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}"
1531
- : "${{ github.event_name == 'push' }}",
1532
- needs: ["verify-context", "rust-build"],
1533
- runsOn: [local ? "self-hosted" : "ubuntu-latest"],
1534
- permissions: { contents: JobPermission.READ },
1535
- steps: [
1536
- ...releaseSourceSteps(),
1537
- {
1538
- name: "Setup Rust",
1539
- uses: `dtolnay/rust-toolchain@${plan.releaseRustVersion}`,
1540
- },
1541
- {
1542
- name: local ? "Publish Cargo crates locally" : "Publish public crates",
1543
- env: {
1544
- CARGO_REGISTRY_TOKEN: local
1545
- ? "${{ secrets.LOCAL_CARGO_TOKEN }}"
1546
- : "${{ secrets.CARGO_REGISTRY_TOKEN }}",
1547
- },
1548
- run: plan.publicCrates
1549
- .map((crate) => `cargo publish --package "${crate}" --registry ${registry} --no-verify`)
1550
- .join("\n"),
1551
- },
1552
- ],
1553
- };
1554
- }
1555
-
1556
- function independentRustCargoPublishJob(
1557
- project: DBXToolsJavaScriptProject,
1558
- plan: RustReleasePlan,
1559
- ): Job {
1560
- const commands = plan.publicCrates.map((crate) =>
1561
- [
1562
- `VERSION="$(jq -r --arg package "${crate}" '.rustPackages[] | select(.identity == $package) | .version' dist/release-plan.json)"`,
1563
- 'if [ -n "$VERSION" ]; then',
1564
- ` if cargo info "${crate}@$VERSION" >/dev/null 2>&1; then`,
1565
- ` echo "skip published ${crate}@$VERSION"`,
1566
- " else",
1567
- ` cargo publish --package "${crate}" --registry crates-io --no-verify`,
1568
- " fi",
1569
- "fi",
1570
- ].join("\n"),
1571
- );
1572
- return {
1573
- if: plan.hasTargetOutputs
1574
- ? "${{ 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') }}"
1575
- : "${{ needs.release-plan.outputs.rust == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'rust') }}",
1576
- needs: ["release-plan", ...(plan.hasTargetOutputs ? ["rust-build"] : [])],
1577
- runsOn: ["ubuntu-latest"],
1578
- permissions: { contents: JobPermission.READ },
1579
- env: { BUN_VERSION },
1580
- steps: [
1581
- ...independentReleaseSetupSteps(project),
1582
- {
1583
- name: "Setup Rust",
1584
- uses: `dtolnay/rust-toolchain@${plan.releaseRustVersion}`,
1585
- },
1586
- {
1587
- name: "Publish affected Cargo crates",
1588
- env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" },
1589
- run: commands.join("\n"),
1590
- },
1591
- ],
1592
- };
1593
- }
1594
-
1595
- function independentRustGitHubReleaseJob(
1596
- project: DBXToolsJavaScriptProject,
1597
- plan: RustReleasePlan,
1598
- ): Job {
1599
- return {
1600
- if: "${{ needs.release-plan.outputs.github == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'github') }}",
1601
- needs: ["release-plan", "rust-build"],
1602
- runsOn: ["ubuntu-latest"],
1603
- permissions: { contents: JobPermission.WRITE },
1604
- env: { BUN_VERSION },
1605
- steps: [
1606
- ...independentReleaseSetupSteps(project),
1607
- {
1608
- name: "Download release binaries",
1609
- uses: "actions/download-artifact@v8",
1610
- with: {
1611
- pattern: "*-binary",
1612
- path: "dist/rust-release",
1613
- "merge-multiple": true,
1614
- },
1615
- },
1616
- {
1617
- name: "Download reusable raw Rust outputs",
1618
- uses: "actions/download-artifact@v8",
1619
- with: {
1620
- pattern: "*-raw",
1621
- path: "dist/rust-raw",
1622
- "merge-multiple": true,
1623
- },
1624
- },
1625
- {
1626
- name: "Upload affected GitHub release assets",
1627
- env: { GH_TOKEN: "${{ github.token }}" },
1628
- shell: "bash",
1629
- run: plan.releaseBinaries
1630
- .map((binary) => {
1631
- const unit = defaultReleaseUnitId("rust", binary.crate);
1632
- return [
1633
- `VERSION="$(jq -r --arg unit "${unit}" '.units[] | select(.id == $unit) | .newVersion' dist/release-plan.json)"`,
1634
- 'if [ -n "$VERSION" ]; then',
1635
- ` gh release upload "${unit}-v$VERSION" dist/rust-release/${binary.binary}-* --clobber`,
1636
- "fi",
1637
- ].join("\n");
1638
- })
1639
- .join("\n"),
1640
- },
1641
- ],
1642
- };
1643
- }
1644
-
1645
- function rustGitHubReleaseJob(): Job {
1646
- return {
1647
- if: "${{ github.event_name == 'push' }}",
1648
- needs: ["verify-context", "rust-build"],
1649
- runsOn: ["ubuntu-latest"],
1650
- permissions: { contents: JobPermission.WRITE },
1651
- steps: [
1652
- ...releaseSourceSteps(),
1653
- {
1654
- name: "Download release binaries",
1655
- uses: "actions/download-artifact@v8",
1656
- with: {
1657
- pattern: "*-binary",
1658
- path: "dist/rust-release",
1659
- "merge-multiple": true,
1660
- },
1661
- },
1662
- {
1663
- name: "Download reusable raw Rust outputs",
1664
- uses: "actions/download-artifact@v8",
1665
- with: {
1666
- pattern: "*-raw",
1667
- path: "dist/rust-raw",
1668
- "merge-multiple": true,
1669
- },
1670
- },
1671
- {
1672
- name: "Publish GitHub release assets",
1673
- uses: "softprops/action-gh-release@v2",
1674
- with: {
1675
- files: ["dist/rust-release/*", "dist/rust-raw/*"].join("\n"),
1676
- body_path: RELEASE_SUMMARY_FILE,
1677
- generate_release_notes: true,
1678
- tag_name: RELEASE_TAG,
1679
- target_commitish: RELEASE_SHA,
1680
- },
1681
- },
1682
- ],
1683
- };
1684
- }
1685
-
1686
- function independentRustNativeNpmPublishJob(project: DBXToolsJavaScriptProject): Job {
1687
- return {
1688
- if: "${{ needs.release-plan.outputs.node == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
1689
- needs: ["release-plan", "rust-build"],
1690
- runsOn: ["ubuntu-latest"],
1691
- permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
1692
- timeoutMinutes: 15,
1693
- env: { BUN_VERSION, CI: "true" },
1694
- steps: [
1695
- ...independentReleaseSetupSteps(project),
1696
- {
1697
- name: "Download native npm packages",
1698
- uses: "actions/download-artifact@v8",
1699
- with: {
1700
- pattern: "*-npm",
1701
- path: "dist/uniffi/native",
1702
- "merge-multiple": true,
1703
- },
1704
- },
1705
- {
1706
- name: "Publish affected native npm packages",
1707
- env: npmPublishEnvironment(),
1708
- run: "bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory dist/uniffi/native",
1709
- },
1710
- ],
1711
- };
1712
- }
1713
-
1714
- function independentRustNodeFacadePublishJob(
1715
- project: DBXToolsJavaScriptProject,
1716
- bindings: readonly RustBindingMapping[],
1717
- ): Job {
1718
- return {
1719
- if: "${{ needs.release-plan.outputs.node == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
1720
- needs: ["release-plan", "publish-node", "publish-native-npm"],
1721
- runsOn: ["ubuntu-latest"],
1722
- permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
1723
- timeoutMinutes: 30,
1724
- env: { BUN_VERSION, CI: "true" },
1725
- steps: [
1726
- ...independentReleaseSetupSteps(project),
1727
- {
1728
- name: "Build and publish affected UniFFI npm facades",
1729
- env: npmPublishEnvironment(),
1730
- shell: "bash",
1731
- run: bindings
1732
- .flatMap((binding) => {
1733
- if (!binding.node || !binding.nodePackage) return [];
1734
- const unit = defaultReleaseUnitId("rust", binding.crate);
1735
- const output = `dist/uniffi/facades/${binding.crate}`;
1736
- return [
1737
- `VERSION="$(jq -r --arg unit "${unit}" '.units[] | select(.id == $unit) | .newVersion' dist/release-plan.json)"`,
1738
- 'if [ -n "$VERSION" ]; then',
1739
- ` node .projen/uniffi-release.mjs facade --node "${binding.node}" --node-package "${binding.nodePackage}" --node-triple "linux-x64-gnu" --version "$VERSION" --output "${output}"`,
1740
- ` bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory "${output}/npm-facade" --version "$VERSION"`,
1741
- "fi",
1742
- ];
1743
- })
1744
- .join("\n"),
1745
- },
1746
- ],
1747
- };
1748
- }
1749
-
1750
- function rustNativeNpmPublishJob(project: DBXToolsJavaScriptProject): Job {
1751
- return {
1752
- 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') }}",
1753
- needs: ["verify-context", "rust-build"],
1754
- runsOn: ["ubuntu-latest"],
1755
- permissions: {
1756
- actions: JobPermission.READ,
1757
- contents: JobPermission.READ,
1758
- idToken: JobPermission.WRITE,
1759
- },
1760
- timeoutMinutes: 15,
1761
- env: { BUN_VERSION, CI: "true" },
1762
- steps: [
1763
- ...nodeReleaseSetupSteps(project),
1764
- ...releaseArtifactSteps({
1765
- currentName: "Download native npm packages",
1766
- recoveredName: "Download recovered native npm packages",
1767
- pattern: "*-npm",
1768
- path: "dist/uniffi/native",
1769
- }),
1770
- {
1771
- name: "Publish native npm packages",
1772
- env: { RELEASE_VERSION, ...npmPublishEnvironment() },
1773
- run: 'bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory dist/uniffi/native --version "$RELEASE_VERSION" $DRY_RUN',
1774
- },
1775
- ],
1776
- };
1777
- }
1778
-
1779
- function rustNodeFacadePublishJob(
1780
- project: DBXToolsJavaScriptProject,
1781
- bindings: readonly RustReleaseBinding[],
1782
- ): Job {
1783
- return {
1784
- if: releaseStageCondition("node"),
1785
- needs: ["verify-context", "publish-node"],
1786
- runsOn: ["ubuntu-latest"],
1787
- permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
1788
- timeoutMinutes: 30,
1789
- env: { BUN_VERSION, CI: "true" },
1790
- steps: [
1791
- ...nodeReleaseSetupSteps(project),
1792
- {
1793
- name: "Build and publish UniFFI npm facades",
1794
- env: { RELEASE_VERSION, ...npmPublishEnvironment() },
1795
- run: bindings
1796
- .flatMap((binding) => {
1797
- const output = `dist/uniffi/facades/${binding.crate}`;
1798
- return [
1799
- `node .projen/uniffi-release.mjs facade --node "${binding.node}" --node-package "${binding.nodePackage}" --node-triple "linux-x64-gnu" --version "$RELEASE_VERSION" --output "${output}"`,
1800
- `bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory "${output}/npm-facade" --version "$RELEASE_VERSION" $DRY_RUN`,
1801
- ];
1802
- })
1803
- .join("\n"),
1804
- },
1805
- {
1806
- name: "Smoke test published UniFFI npm facades",
1807
- if: "${{ github.event_name == 'push' && vars.UNIFFI_FACADE_SMOKE == 'true' }}",
1808
- continueOnError: true,
1809
- env: { RELEASE_VERSION },
1810
- run: [
1811
- 'SMOKE_DIR="$(mktemp -d)"',
1812
- "trap 'rm -rf \"$SMOKE_DIR\"' EXIT",
1813
- 'cd "$SMOKE_DIR"',
1814
- "npm init --yes >/dev/null",
1815
- ...bindings.flatMap((binding) => [
1816
- `npm install --ignore-scripts --no-audit --no-fund --package-lock=false "${binding.nodePackage}@$RELEASE_VERSION"`,
1817
- `node -e 'import("${binding.nodePackage}")'`,
1818
- ]),
1819
- ].join("\n"),
1820
- },
1821
- ],
1822
- };
1823
- }
1824
-
1825
818
  function rustReleaseDependencies(
1826
819
  root: DBXToolsJavaScriptProject,
1827
820
  project: DBXToolsRustProject,
@@ -1877,7 +870,7 @@ function configureIndependentRustVersions(
1877
870
  }
1878
871
 
1879
872
  /** Generated Rust workspace plus convention-derived UniFFI binding packages. */
1880
- export class DBXToolsRustWorkspace {
873
+ export class DBXToolsRustWorkspace extends Component {
1881
874
  readonly packages: readonly DBXToolsRustProject[];
1882
875
  readonly nodePackages: readonly DBXToolsTypeScriptProject[];
1883
876
  readonly pythonPackages: readonly PythonPackageOptions[];
@@ -1886,6 +879,7 @@ export class DBXToolsRustWorkspace {
1886
879
  readonly workspaceMapping: RustWorkspaceMapping;
1887
880
 
1888
881
  constructor(project: javascript.NodeProject, options: DBXToolsRustWorkspaceOptions) {
882
+ super(project);
1889
883
  const resolved = resolveRustWorkspaceOptions(project, options);
1890
884
  this.packages = createRustPackages(project, resolved);
1891
885
  const packageDependencies = createRustPackageDependencyResolver(
@@ -1899,7 +893,7 @@ export class DBXToolsRustWorkspace {
1899
893
  language: "rust",
1900
894
  identity: pkg.crateName,
1901
895
  publish: !pkg.packageOptions.private || pkg.packageOptions.release === true,
1902
- sourcePaths: [`${relative(project.outdir, pkg.outdir).replaceAll("\\", "/")}/src`],
896
+ sourcePaths: [`${toPosix(relative(project.outdir, pkg.outdir))}/src`],
1903
897
  dependencies: [
1904
898
  ...rustReleaseDependencies(
1905
899
  project,
@@ -2003,7 +997,7 @@ export class DBXToolsRustWorkspace {
2003
997
  configureRustWorkspaceFiles(project, this.packages, options, resolved);
2004
998
  configureRustWorkspaceTasks(project);
2005
999
  if (releaseEnabled) {
2006
- this.addReleaseWorkflow(project, options, resolved.nativeTargets);
1000
+ this.addReleaseWorkflow(project, options, resolved.nativeTargets, packageDependencies);
2007
1001
  }
2008
1002
  }
2009
1003
 
@@ -2011,9 +1005,17 @@ export class DBXToolsRustWorkspace {
2011
1005
  project: javascript.NodeProject,
2012
1006
  options: DBXToolsRustWorkspaceOptions,
2013
1007
  targets: readonly UniFFIReleaseTarget[],
1008
+ packageDependencies: RustPackageDependencyResolver,
2014
1009
  ): void {
2015
1010
  if (!project.github || !isDBXToolsJavaScriptProject()(project)) return;
2016
- const plan = planRustRelease(project, options, targets, this.packages, this.bindingMappings);
1011
+ const plan = planRustRelease(
1012
+ project,
1013
+ options,
1014
+ targets,
1015
+ this.packages,
1016
+ this.bindingMappings,
1017
+ packageDependencies,
1018
+ );
2017
1019
  const workflow = tryReleaseWorkflow(project);
2018
1020
  if (!workflow) {
2019
1021
  throw new Error("Rust release requires the root dbx-tools release mode");