@dbx-tools/projen 0.6.186 → 0.6.191

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,19 +94,20 @@ configurable `root` (default `packages/rs`), generates its Cargo manifest, and
94
94
  derives the crate name and repository from the parent project. A crate containing
95
95
  `uniffi::setup_scaffolding!()`
96
96
  automatically wires matching public Node and Python binding packages using the
97
- same capability name. Repository-specific dependencies and features remain
97
+ `<name>-rs` folder suffix. Node packages are named `@<scope>/<name>-rs`;
98
+ Python distributions are named `<scope>-<name>-rs` and import generated values
99
+ from `<scope>.<name>_rs.bindings`. Binding packages are always dedicated and
100
+ never merge generated code into handwritten Node or Python packages.
101
+ Repository-specific dependencies and features remain
98
102
  declarative options in `.projenrc.ts`; generated bindings are built separately
99
103
  from projen synthesis. Target-independent Node binding TypeScript is committed
100
104
  and remains generated/read-only, while native libraries stay ignored. Node
101
105
  facades compile to `lib/` and publish JavaScript entry points that plain Node
102
106
  can load from `node_modules`. A complete `bindings.ts` / `_bindings.ts` /
103
107
  `_bindings-ffi.ts` triplet is exported directly from the generated package
104
- barrel. Python keeps `bindings.py` as the generated implementation and exports
105
- its public names from a fully generated `__init__.py` with no editable marker
106
- blocks. Node generation fails when direct binding names conflict with another
107
- package-root export; Python generation refuses to overwrite a non-generated
108
- package root. Do not create a `nodeExports` binding subpath or a handwritten
109
- type facade.
108
+ barrel. Python keeps `bindings.py` as the generated implementation and leaves
109
+ `__init__.py` empty. Node generation fails when direct binding names conflict.
110
+ Do not create a `nodeExports` binding subpath or a handwritten type facade.
110
111
 
111
112
  Rust dependencies between binding-enabled workspace crates become Node
112
113
  `workspace:*` and Python `internalDependencies` automatically. Python generation
@@ -158,6 +159,12 @@ release-enabled binary's `releaseExcludeOs` package option to omit it from
158
159
  incompatible rows through Cargo `--exclude`. Release binary packaging and
159
160
  artifact upload are skipped in those rows. UniFFI crates cannot use this option
160
161
  because every configured target must produce their artifacts.
162
+ Set a release binary's `cli` package option and the workspace
163
+ `cliRegistryPath` to generate a typed `dbx` command registry from the same
164
+ selected targets. The generated entries carry only the archives that the
165
+ release matrix publishes, so an excluded operating system fails locally before
166
+ any download is attempted. The runtime command uses `@dbx-tools/core`
167
+ `bin.ensure`; the core installer itself contains no product registry.
161
168
  Rust rows prepare native npm
162
169
  archives and Python wheels; they do not install Bun or UBRN and do not build
163
170
  Node facades. A binary-only row therefore installs no language package tool.
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
- export const PACKAGE_VERSION = "0.6.185";
6
+ export const PACKAGE_VERSION = "0.6.190";
7
7
  export * as barrels from "./src/barrels.ts";
8
8
  export * as bunApp from "./src/bun-app.ts";
9
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
@@ -51,7 +51,7 @@ export type { DBXToolsJavaScriptProject, DBXToolsJavaScriptProjectOptions, DBXTo
51
51
  export { DBXToolsPythonProject, DBXToolsPythonWorkspace } from "./src/project-py.ts";
52
52
  export type { PythonRepositoryOptions, PythonPackageOptions, PythonTrustedPublisherOptions, DBXToolsPythonProjectOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
53
53
  export { RustReleaseOs, RustReleaseCpu, UNIFFI_RELEASE_TARGETS, DBXToolsRustProject, DBXToolsRustWorkspace } from "./src/project-rs.ts";
54
- export type { CargoDependencyOptions, CargoDependency, RustPackageOptions, DBXToolsRustWorkspaceOptions, RustReleasePlatform, UniFFIReleaseTarget, RustBindingMapping, RustWorkspaceMapping } from "./src/project-rs.ts";
54
+ export type { CargoDependencyOptions, CargoDependency, RustCliOptions, RustPackageOptions, DBXToolsRustWorkspaceOptions, RustReleasePlatform, UniFFIReleaseTarget, RustBindingMapping, RustWorkspaceMapping, RustReleaseBinaryAssetMapping, RustReleaseBinaryMapping } from "./src/project-rs.ts";
55
55
  export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
56
56
  export { DBXToolsRelease } from "./src/release.ts";
57
57
  export type { ReleaseStage, ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
package/package.json CHANGED
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@clack/prompts": "^1.7.0",
28
- "@dbx-tools/core": "0.6.186",
29
- "@dbx-tools/path": "0.6.186",
30
- "@dbx-tools/shared-core": "0.6.186",
28
+ "@dbx-tools/core": "0.6.191",
29
+ "@dbx-tools/path": "0.6.191",
30
+ "@dbx-tools/shared-core": "0.6.191",
31
31
  "commander": "^15.0.0",
32
32
  "concurrently": "^10.0.3",
33
33
  "constructs": "^10.6.0",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "main": "index.ts",
49
49
  "license": "Apache-2.0",
50
- "version": "0.6.186",
50
+ "version": "0.6.191",
51
51
  "types": "index.ts",
52
52
  "type": "module",
53
53
  "exports": {
package/src/project-js.ts CHANGED
@@ -446,13 +446,13 @@ function defaultProjectOptions(
446
446
  devDeps: ["@types/node@^24.6.0"],
447
447
  ...(isRoot
448
448
  ? {
449
- prettier: true,
450
- prettierOptions: {
451
- settings: PRETTIER_SETTINGS,
452
- ignoreFile: true,
453
- ignoreFileOptions: { ignorePatterns: [...ignore.ignorePatterns({ test: false })] },
454
- },
455
- }
449
+ prettier: true,
450
+ prettierOptions: {
451
+ settings: PRETTIER_SETTINGS,
452
+ ignoreFile: true,
453
+ ignoreFileOptions: { ignorePatterns: [...ignore.ignorePatterns({ test: false })] },
454
+ },
455
+ }
456
456
  : {}),
457
457
  ...options,
458
458
  ...copiedGitIgnoreOptions(options),
@@ -508,10 +508,10 @@ const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", `@types/bun@${BUN_VERSION}
508
508
  /** Options for {@link DBXToolsNodeProject} (the monorepo root). */
509
509
  export interface DBXToolsJavaScriptProjectOptions
510
510
  extends
511
- CommonProjectOptions,
512
- Partial<javascript.NodeProjectOptions>,
513
- DBXToolsConfigOptions,
514
- DBXToolsPNPMWorkspaceOptions {
511
+ CommonProjectOptions,
512
+ Partial<javascript.NodeProjectOptions>,
513
+ DBXToolsConfigOptions,
514
+ DBXToolsPNPMWorkspaceOptions {
515
515
  /**
516
516
  * The npm scope for generated package names (`@<scope>/<seg-...>`). Defaults to
517
517
  * the (resolved) project name; a leading `@` is optional.
@@ -584,7 +584,8 @@ export interface DBXToolsTypeScriptProjectOptions
584
584
  */
585
585
  export class DBXToolsNodeProject
586
586
  extends javascript.NodeProject
587
- implements DBXToolsJavaScriptProject {
587
+ implements DBXToolsJavaScriptProject
588
+ {
588
589
  readonly language = "javascript" as const;
589
590
  readonly scope: string;
590
591
  readonly dbxToolsConfig: DBXToolsConfig;
@@ -684,8 +685,8 @@ export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
684
685
  installDependencies(trigger: unknown): void;
685
686
  logInstallTrigger(trigger: unknown): void;
686
687
  };
687
- nodePackage.installDependencies = () => { };
688
- nodePackage.logInstallTrigger = () => { };
688
+ nodePackage.installDependencies = () => {};
689
+ nodePackage.logInstallTrigger = () => {};
689
690
  },
690
691
  );
691
692
 
@@ -698,7 +699,8 @@ export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
698
699
  */
699
700
  export class DBXToolsTypeScriptProject
700
701
  extends typescript.TypeScriptProject
701
- implements DBXToolsJavaScriptProject {
702
+ implements DBXToolsJavaScriptProject
703
+ {
702
704
  readonly language = "javascript" as const;
703
705
  readonly scope: string;
704
706
  readonly dbxToolsConfig: DBXToolsConfig;
package/src/project-rs.ts CHANGED
@@ -39,6 +39,13 @@ export interface CargoDependencyOptions {
39
39
 
40
40
  export type CargoDependency = string | CargoDependencyOptions;
41
41
 
42
+ export interface RustCliOptions {
43
+ /** `dbx` subcommand. Defaults to the Rust package directory. */
44
+ readonly command?: string;
45
+ /** Root CLI help text. Defaults to the Rust package description. */
46
+ readonly description?: string;
47
+ }
48
+
42
49
  export interface RustPackageOptions {
43
50
  readonly directory: string;
44
51
  readonly description?: string;
@@ -54,9 +61,9 @@ export interface RustPackageOptions {
54
61
  readonly defaultFeatures?: readonly string[];
55
62
  /** Cargo and release executable name. Defaults to the generated package name. */
56
63
  readonly binaryName?: string;
64
+ /** Publish this release binary through the generated `dbx` command registry. */
65
+ readonly cli?: boolean | RustCliOptions;
57
66
  readonly bindings?: readonly ("node" | "python")[];
58
- readonly nodeDependencies?: readonly string[];
59
- readonly nodeDevDependencies?: readonly string[];
60
67
  readonly uniffiConfig?: Readonly<Record<string, unknown>>;
61
68
  }
62
69
 
@@ -82,6 +89,8 @@ export interface DBXToolsRustWorkspaceOptions {
82
89
  readonly releaseTargets?: readonly UniFFIReleaseTarget[];
83
90
  /** Maintained OS/CPU combinations to release. Defaults to every supported target. */
84
91
  readonly releasePlatforms?: readonly RustReleasePlatform[];
92
+ /** Optional generated TypeScript registry for `cli`-enabled release binaries. */
93
+ readonly cliRegistryPath?: string;
85
94
  }
86
95
 
87
96
  export enum RustReleaseOs {
@@ -154,6 +163,14 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
154
163
  os: RustReleaseOs.WINDOWS,
155
164
  cpu: RustReleaseCpu.X64,
156
165
  },
166
+ {
167
+ runner: "windows-11-vs2026-arm",
168
+ cargo: "aarch64-pc-windows-msvc",
169
+ node: "win32-arm64-msvc",
170
+ python: "win_arm64",
171
+ os: RustReleaseOs.WINDOWS,
172
+ cpu: RustReleaseCpu.ARM64,
173
+ },
157
174
  ] as const;
158
175
 
159
176
  const RUST_CACHE_ENV = {
@@ -230,6 +247,21 @@ function releasePlatformsFromEnvironment(
230
247
  });
231
248
  }
232
249
 
250
+ function releaseBinaryAssetName(binaryName: string, nodeTarget: string, os: RustReleaseOs): string {
251
+ const extension = os === RustReleaseOs.WINDOWS ? "zip" : "tar.gz";
252
+ return `${binaryName}-${nodeTarget}.${extension}`;
253
+ }
254
+
255
+ function rustCliRegistrySource(binaries: readonly RustReleaseBinaryMapping[]): string {
256
+ return [
257
+ "// GENERATED by projen - DO NOT EDIT.",
258
+ "// Rust release package options are the source of truth.",
259
+ "",
260
+ `export const RUST_RELEASE_BINARY_COMMANDS = ${JSON.stringify(binaries, null, 2)} as const;`,
261
+ "",
262
+ ].join("\n");
263
+ }
264
+
233
265
  function uniffiReleaseTaskSource(): string {
234
266
  const sourceDirectory = dirname(fileURLToPath(import.meta.url));
235
267
  const candidates = [
@@ -259,6 +291,23 @@ export interface RustWorkspaceMapping {
259
291
  readonly root: string;
260
292
  readonly crates: readonly string[];
261
293
  readonly bindings: readonly RustBindingMapping[];
294
+ readonly binaries: readonly RustReleaseBinaryMapping[];
295
+ }
296
+
297
+ /** One platform archive published for a Rust CLI binary. */
298
+ export interface RustReleaseBinaryAssetMapping {
299
+ readonly os: RustReleaseOs;
300
+ readonly cpu: RustReleaseCpu;
301
+ readonly name: string;
302
+ }
303
+
304
+ /** Runtime metadata for one lazily installed Rust CLI binary. */
305
+ export interface RustReleaseBinaryMapping {
306
+ readonly command: string;
307
+ readonly description: string;
308
+ readonly binaryName: string;
309
+ readonly repository: string;
310
+ readonly assets: readonly RustReleaseBinaryAssetMapping[];
262
311
  }
263
312
 
264
313
  export function orderRustBindings(bindings: readonly RustBindingMapping[]): RustBindingMapping[] {
@@ -474,12 +523,13 @@ function rustPackageDependencies(
474
523
  );
475
524
  }
476
525
 
477
- /** Generated Rust workspace plus convention-derived UniFFI facade packages. */
526
+ /** Generated Rust workspace plus convention-derived UniFFI binding packages. */
478
527
  export class DBXToolsRustWorkspace {
479
528
  readonly packages: readonly DBXToolsRustProject[];
480
529
  readonly nodePackages: readonly DBXToolsTypeScriptProject[];
481
530
  readonly pythonPackages: readonly PythonPackageOptions[];
482
531
  readonly bindingMappings: readonly RustBindingMapping[];
532
+ readonly releaseBinaries: readonly RustReleaseBinaryMapping[];
483
533
  readonly workspaceMapping: RustWorkspaceMapping;
484
534
 
485
535
  constructor(project: javascript.NodeProject, options: DBXToolsRustWorkspaceOptions) {
@@ -492,6 +542,7 @@ export class DBXToolsRustWorkspace {
492
542
  projectRepositoryUrl(project) ??
493
543
  coreProject.repositoryUrl(project.outdir) ??
494
544
  "";
545
+ const nativeTargets = releaseTargets(options);
495
546
  const pythonModulePrefix = options.pythonModulePrefix ?? scope.replaceAll("-", "_");
496
547
  const packageOptions = options.packages ?? {};
497
548
  this.packages = discoverRustCrates(resolve(project.outdir, root)).map(
@@ -501,6 +552,56 @@ export class DBXToolsRustWorkspace {
501
552
  ...packageOptions[directory],
502
553
  }),
503
554
  );
555
+ const commands = new Set<string>();
556
+ this.releaseBinaries = this.packages
557
+ .filter((pkg) => Boolean(pkg.packageOptions.cli))
558
+ .map((pkg) => {
559
+ if (!pkg.packageOptions.release) {
560
+ throw new Error(`${pkg.crateName} must set release when cli is configured`);
561
+ }
562
+ const configured = pkg.packageOptions.cli;
563
+ const command =
564
+ typeof configured === "object" && configured.command
565
+ ? configured.command
566
+ : pkg.packageOptions.directory;
567
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(command)) {
568
+ throw new Error(`Invalid Rust CLI command: ${command}`);
569
+ }
570
+ if (commands.has(command)) {
571
+ throw new Error(`Duplicate Rust CLI command: ${command}`);
572
+ }
573
+ commands.add(command);
574
+ const description =
575
+ (typeof configured === "object" ? configured.description : undefined) ??
576
+ pkg.packageOptions.description ??
577
+ pkg.crateName;
578
+ const excludedOs = new Set(pkg.packageOptions.releaseExcludeOs ?? []);
579
+ const binaryName = pkg.packageOptions.binaryName ?? pkg.crateName;
580
+ return {
581
+ command,
582
+ description,
583
+ binaryName,
584
+ repository,
585
+ assets: nativeTargets
586
+ .filter((target) => !excludedOs.has(target.os))
587
+ .map((target) => ({
588
+ os: target.os,
589
+ cpu: target.cpu,
590
+ name: releaseBinaryAssetName(binaryName, target.node, target.os),
591
+ })),
592
+ };
593
+ });
594
+ if (this.releaseBinaries.length && !repository) {
595
+ throw new Error("A repository URL is required when Rust CLI commands are configured");
596
+ }
597
+ if (this.releaseBinaries.length && !options.cliRegistryPath) {
598
+ throw new Error("cliRegistryPath is required when Rust CLI commands are configured");
599
+ }
600
+ if (options.cliRegistryPath) {
601
+ new TextFile(project, options.cliRegistryPath, {
602
+ lines: rustCliRegistrySource(this.releaseBinaries).trimEnd().split("\n"),
603
+ });
604
+ }
504
605
 
505
606
  const bindings = this.packages.filter((pkg) => pkg.uniffi);
506
607
  const packageDependencies = (pkg: DBXToolsRustProject) =>
@@ -531,7 +632,7 @@ export class DBXToolsRustWorkspace {
531
632
  this.bindingMappings = orderRustBindings(
532
633
  bindings.map((pkg) => {
533
634
  const targets = pkg.packageOptions.bindings ?? ["node", "python"];
534
- const packageName = pkg.packageOptions.directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
635
+ const packageDirectory = `${string.toSlug(pkg.packageOptions.directory)}-rs`;
535
636
  const dependencies = [
536
637
  ...new Set([...bindingDependencies(pkg, "node"), ...bindingDependencies(pkg, "python")]),
537
638
  ].map((dependency) => dependency.crateName);
@@ -541,15 +642,15 @@ export class DBXToolsRustWorkspace {
541
642
  rust: `${root}/${pkg.packageOptions.directory}`,
542
643
  ...(targets.includes("node")
543
644
  ? {
544
- node: `${nodeRoot}/${pkg.packageOptions.directory}`,
545
- nodePackage: `@${scope}/${packageName}`,
645
+ node: `${nodeRoot}/${packageDirectory}`,
646
+ nodePackage: `@${scope}/${packageDirectory}`,
546
647
  }
547
648
  : {}),
548
649
  ...(targets.includes("python")
549
650
  ? {
550
- python: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`,
551
- pythonPackage: pkg.crateName,
552
- pythonModule: pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory),
651
+ python: `${options.pythonRoot ?? "packages/py"}/${packageDirectory}`,
652
+ pythonPackage: `${scope}-${packageDirectory}`,
653
+ pythonModule: pythonModuleName(pythonModulePrefix, packageDirectory),
553
654
  }
554
655
  : {}),
555
656
  };
@@ -567,7 +668,10 @@ export class DBXToolsRustWorkspace {
567
668
  "bindings.python.external_packages": Object.fromEntries(
568
669
  dependencies.map((dependency) => [
569
670
  dependency.crateName.replaceAll("-", "_"),
570
- `${pythonModuleName(pythonModulePrefix, dependency.packageOptions.directory)}.bindings`,
671
+ `${pythonModuleName(
672
+ pythonModulePrefix,
673
+ `${string.toSlug(dependency.packageOptions.directory)}-rs`,
674
+ )}.bindings`,
571
675
  ]),
572
676
  ),
573
677
  })
@@ -583,6 +687,7 @@ export class DBXToolsRustWorkspace {
583
687
  root,
584
688
  crates: this.packages.map((pkg) => `${root}/${pkg.packageOptions.directory}`),
585
689
  bindings: this.bindingMappings,
690
+ binaries: this.releaseBinaries,
586
691
  };
587
692
  if (dbxToolsProject) {
588
693
  dbxToolsProject.dbxToolsConfig.rust = this.workspaceMapping;
@@ -609,23 +714,25 @@ export class DBXToolsRustWorkspace {
609
714
  this.pythonPackages = bindings
610
715
  .filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("python"))
611
716
  .map((pkg) => {
612
- const module = pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory);
717
+ const directory = `${string.toSlug(pkg.packageOptions.directory)}-rs`;
718
+ const module = pythonModuleName(pythonModulePrefix, directory);
719
+ const name = `${scope}-${directory}`;
613
720
  return {
614
- directory: pkg.packageOptions.directory,
615
- name: pkg.crateName,
721
+ directory,
722
+ name,
616
723
  module,
617
724
  description: `Python bindings for ${pkg.crateName}`,
618
725
  uniffi: true,
619
726
  internalDependencies: bindingDependencies(pkg, "python").map(
620
- (dependency) => dependency.packageOptions.directory,
727
+ (dependency) => `${string.toSlug(dependency.packageOptions.directory)}-rs`,
621
728
  ),
622
729
  generatedSources: [
623
730
  `src/${module.replaceAll(".", "/")}/bindings.py`,
624
731
  `src/${module.replaceAll(".", "/")}/__init__.py`,
625
732
  ],
626
733
  trustedPublisher: {
627
- environment: `pypi-${pkg.crateName}`,
628
- artifacts: `platform-specific wheels for ${releaseTargets(options)
734
+ environment: `pypi-${name}`,
735
+ artifacts: `platform-specific wheels for ${nativeTargets
629
736
  .map((target) => `${target.os}-${target.cpu}`)
630
737
  .join(", ")}; all architectures publish to this one PyPI project`,
631
738
  },
@@ -639,7 +746,7 @@ export class DBXToolsRustWorkspace {
639
746
  for (const binding of bindings.filter((pkg) =>
640
747
  (pkg.packageOptions.bindings ?? ["node", "python"]).includes("node"),
641
748
  )) {
642
- const directory = binding.packageOptions.directory;
749
+ const directory = `${string.toSlug(binding.packageOptions.directory)}-rs`;
643
750
  const memberPath = `${nodeRoot}/${directory}`;
644
751
  const found = existing.get(memberPath);
645
752
  const existingNode = found instanceof DBXToolsTypeScriptProject ? found : undefined;
@@ -656,10 +763,7 @@ export class DBXToolsRustWorkspace {
656
763
  `@${scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
657
764
  );
658
765
  node.dbxToolsConfig.uniffi = true;
659
- if (!existingNode) {
660
- node.package.addField("description", `Node bindings for ${binding.crateName}`);
661
- }
662
- const nativeTargets = releaseTargets(options);
766
+ node.package.addField("description", `Node bindings for ${binding.crateName}`);
663
767
  if (options.release ?? true) {
664
768
  node.package.addField(
665
769
  "optionalDependencies",
@@ -674,16 +778,11 @@ export class DBXToolsRustWorkspace {
674
778
  node.addDeps("@ubjs/core@0.31.0-5", "@ubjs/node@0.31.0-5");
675
779
  node.addDeps(
676
780
  ...bindingDependencies(binding, "node").map(
677
- (dependency) => `@${scope}/${dependency.packageOptions.directory}@workspace:*`,
781
+ (dependency) =>
782
+ `@${scope}/${string.toSlug(dependency.packageOptions.directory)}-rs@workspace:*`,
678
783
  ),
679
784
  );
680
- if (binding.packageOptions.nodeDependencies?.length) {
681
- node.addDeps(...binding.packageOptions.nodeDependencies);
682
- }
683
785
  node.addDevDeps(`uniffi-bindgen-react-native@${UBRN_VERSION}`);
684
- if (binding.packageOptions.nodeDevDependencies?.length) {
685
- node.addDevDeps(...binding.packageOptions.nodeDevDependencies);
686
- }
687
786
  nodePackages.push(node);
688
787
  }
689
788
  this.nodePackages = nodePackages;
@@ -728,20 +827,9 @@ export class DBXToolsRustWorkspace {
728
827
  if (this.bindingMappings.some((binding) => binding.node)) {
729
828
  project.tasks.tryFind("pre-compile")?.spawn(bindingsTask);
730
829
  }
731
- project.addTask("rs:bindings:demo", {
732
- description: "Generate and run UniFFI Node and Python example CLIs",
733
- exec: [
734
- "bun run rs:bindings",
735
- ...this.bindingMappings.flatMap((binding) => [
736
- ...(binding.node ? [`bun ${binding.node}/test/cli.ts`] : []),
737
- ...(binding.python
738
- ? [`uv run --project ${binding.python} ${binding.python}/test/cli.py`]
739
- : []),
740
- ]),
741
- ].join(" && "),
742
- });
830
+ project.removeTask("rs:bindings:demo");
743
831
  if (releaseEnabled) {
744
- this.addReleaseWorkflow(project, options, releaseTargets(options));
832
+ this.addReleaseWorkflow(project, options, nativeTargets);
745
833
  }
746
834
  }
747
835
 
@@ -813,6 +901,7 @@ export class DBXToolsRustWorkspace {
813
901
  `--python "${binding.python}"`,
814
902
  `--node-package "${binding.nodePackage}"`,
815
903
  `--python-package "${binding.pythonPackage}"`,
904
+ `--python-module "${binding.pythonModule}"`,
816
905
  '--cargo-target "${{ matrix.cargo }}"',
817
906
  '--node-triple "${{ matrix.node }}"',
818
907
  '--python-tag "${{ matrix.python }}"',
@@ -829,15 +918,25 @@ export class DBXToolsRustWorkspace {
829
918
  ? `\${{ ${excludedOs.map((os) => `matrix.os != '${os}'`).join(" && ")} }}`
830
919
  : undefined;
831
920
  const binaryCommands = releaseBinaries.flatMap((pkg) => {
921
+ const windowsAsset = releaseBinaryAssetName(
922
+ pkg.binary,
923
+ "${{ matrix.node }}",
924
+ RustReleaseOs.WINDOWS,
925
+ );
926
+ const unixAsset = releaseBinaryAssetName(
927
+ pkg.binary,
928
+ "${{ matrix.node }}",
929
+ RustReleaseOs.LINUX,
930
+ );
832
931
  const commands = [
833
932
  `mkdir -p "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
834
933
  `SOURCE="target/\${{ matrix.cargo }}/release/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
835
934
  `DESTINATION="dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
836
935
  'cp "$SOURCE" "$DESTINATION"',
837
936
  'if [ "${{ matrix.os }}" = "win32" ]; then',
838
- ` 7z a "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${pkg.binary}-\${{ matrix.node }}.zip" "$DESTINATION"`,
937
+ ` 7z a "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${windowsAsset}" "$DESTINATION"`,
839
938
  "else",
840
- ` tar -C "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage" -czf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${pkg.binary}-\${{ matrix.node }}.tar.gz" "${pkg.binary}"`,
939
+ ` tar -C "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage" -czf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${unixAsset}" "${pkg.binary}"`,
841
940
  "fi",
842
941
  `rm -rf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
843
942
  ];
@@ -869,7 +968,7 @@ export class DBXToolsRustWorkspace {
869
968
  name: `Upload ${binding.crate} Python wheel`,
870
969
  uses: "actions/upload-artifact@v7",
871
970
  with: {
872
- name: `${binding.crate}--\${{ matrix.python }}--python-wheel`,
971
+ name: `${binding.pythonPackage}--\${{ matrix.python }}--python-wheel`,
873
972
  path: `dist/release/${binding.crate}/\${{ matrix.node }}/python/*.whl`,
874
973
  "retention-days": 7,
875
974
  },
@@ -942,7 +1041,11 @@ export class DBXToolsRustWorkspace {
942
1041
  {
943
1042
  name: "Install Linux native dependencies",
944
1043
  if: "${{ matrix.os == 'linux' }}",
945
- run: "sudo apt-get update && sudo apt-get install --yes libdbus-1-dev pkg-config",
1044
+ run: [
1045
+ "sudo rm -f /etc/apt/sources.list.d/google-chrome.list",
1046
+ "sudo apt-get update",
1047
+ "sudo apt-get install --yes libdbus-1-dev pkg-config",
1048
+ ].join("\n"),
946
1049
  },
947
1050
  {
948
1051
  name: "Build Rust outputs",
package/src/release.ts CHANGED
@@ -247,16 +247,16 @@ function addDocsJobs(
247
247
  { name: "Configure Pages", uses: "actions/configure-pages@v5" },
248
248
  { name: "Install dependencies", run: "bun install" },
249
249
  { name: "Generate docs from READMEs", run: "bun docs/scripts/sync-readmes.mjs" },
250
- {
251
- name: "Check generated titles",
252
- run: "bun docs/scripts/check-generated-titles.mjs",
253
- },
254
250
  { name: "Install docs dependencies", run: "bun install --cwd .docs-build/site" },
255
251
  bunCacheSaveStep(),
256
252
  {
257
253
  name: "Generate TypeScript API docs",
258
254
  run: "bun docs/scripts/generate-api-docs.mjs",
259
255
  },
256
+ {
257
+ name: "Check generated titles",
258
+ run: "bun docs/scripts/check-generated-titles.mjs",
259
+ },
260
260
  { name: "Build docs", run: "bun run --cwd .docs-build/site build" },
261
261
  {
262
262
  name: "Check generated links",
package/src/tags.ts CHANGED
@@ -182,6 +182,7 @@ export const PACKAGE_TAG_MIXINS = {
182
182
  openapi: create(projectPredicate.hasTag("openapi"), (p) => {
183
183
  p.addDeps("openapi-fetch@catalog:");
184
184
  applyCompilerOptions(p, { target: "ES2022", lib: [...DOM_LIB], types: [] });
185
+ addPackageFiles(p, "openapi.json");
185
186
  }),
186
187
  } satisfies Record<string, ConstructsMixin>;
187
188
 
package/src/uniffi.ts CHANGED
@@ -56,40 +56,3 @@ export const addExplicitInterfaceReexports = (
56
56
  if (exports.length === 0) return facade;
57
57
  return `${facade.trimEnd()}\n${exports.join("\n")}\n`;
58
58
  };
59
-
60
- const GENERATED_BINDINGS_HEADER = "GENERATED by UniFFI binding generation";
61
-
62
- function pythonAllNames(source: string, file: string): string[] {
63
- const body = /^__all__\s*=\s*\[([\s\S]*?)\]/m.exec(source)?.[1];
64
- if (body === undefined) {
65
- throw new Error(`Python exports must define a literal __all__ list: ${file}`);
66
- }
67
- return [...body.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]!);
68
- }
69
-
70
- /** Generate one package root from the binding module's explicit exports. */
71
- export function mergePythonBindingExports(
72
- initSource: string,
73
- bindingsSource: string,
74
- options: { readonly crate: string; readonly file: string },
75
- ): string {
76
- if (initSource.trim() && !initSource.includes(GENERATED_BINDINGS_HEADER)) {
77
- throw new Error(`Cannot replace non-generated Python package exports: ${options.file}`);
78
- }
79
- const bindingNames = pythonAllNames(bindingsSource, options.file);
80
- const publicNames = [...bindingNames].sort((left, right) =>
81
- left < right ? -1 : left > right ? 1 : 0,
82
- );
83
- return [
84
- "# GENERATED by UniFFI binding generation - DO NOT EDIT.",
85
- `# Regenerated from the ${options.crate} Rust exports.`,
86
- "# Hand edits are overwritten on the next watch; this file is read-only.",
87
- "",
88
- "from .bindings import *",
89
- "",
90
- "__all__ = [",
91
- ...publicNames.map((name) => ` ${JSON.stringify(name)},`),
92
- "]",
93
- "",
94
- ].join("\n");
95
- }
package/tasks/bump.ts CHANGED
@@ -55,8 +55,8 @@ import { exec, project } from "@dbx-tools/core";
55
55
  import { log, net } from "@dbx-tools/shared-core";
56
56
  import { Command, Option } from "commander";
57
57
  import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
58
- import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
59
58
  import { releasePlatformFilter } from "../src/_release-platform.ts";
59
+ import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
60
60
  import {
61
61
  type Semver,
62
62
  compareSemver,
@@ -157,6 +157,8 @@ function buildAndPublish(binding: RustBindingMapping, version: string): void {
157
157
  includeNode ? binding.nodePackage! : "",
158
158
  "--python-package",
159
159
  includePython ? binding.pythonPackage! : "",
160
+ "--python-module",
161
+ includePython ? binding.pythonModule! : "",
160
162
  "--cargo-target",
161
163
  rustHost(),
162
164
  "--node-triple",
package/tasks/rust.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env -S bun
2
+ import { spawnSync } from "node:child_process";
2
3
  import { existsSync } from "node:fs";
3
4
  import { dirname, isAbsolute, resolve, sep } from "node:path";
4
- import { spawnSync } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { log } from "@dbx-tools/shared-core";
7
7
  import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
@@ -40,7 +40,7 @@ function currentStructure(config: RustWorkspaceMapping): RustWorkspaceMapping {
40
40
  const binding = recorded.get(rust);
41
41
  return binding ? [binding] : [{ crate: "", rust }];
42
42
  });
43
- return { root: config.root, crates, bindings };
43
+ return { root: config.root, crates, bindings, binaries: config.binaries ?? [] };
44
44
  }
45
45
 
46
46
  /** Whether discovered crate membership or UniFFI marker membership changed. */
@@ -70,7 +70,9 @@ function ownerBinding(
70
70
  function generate(binding: RustBindingMapping): void {
71
71
  const targets = [
72
72
  ...(binding.node ? ["--node", binding.node] : []),
73
- ...(binding.python ? ["--python", binding.python] : []),
73
+ ...(binding.python
74
+ ? ["--python", binding.python, "--python-module", binding.pythonModule ?? ""]
75
+ : []),
74
76
  ];
75
77
  const result = spawnSync(
76
78
  process.execPath,
@@ -92,8 +94,9 @@ export function affectedRustBindings(
92
94
  const affected = new Set(changed);
93
95
  const ordered = orderRustBindings(bindings);
94
96
  for (const binding of ordered) {
95
- if (binding.dependencies?.some((dependency) => affected.has(dependency)))
97
+ if (binding.dependencies?.some((dependency) => affected.has(dependency))) {
96
98
  affected.add(binding.crate);
99
+ }
97
100
  }
98
101
  return ordered.filter((binding) => affected.has(binding.crate));
99
102
  }
@@ -27,6 +27,7 @@ const parsed = parseArgs({
27
27
  "node-package": { type: "string" },
28
28
  "native-package": { type: "string" },
29
29
  "python-package": { type: "string" },
30
+ "python-module": { type: "string" },
30
31
  "cargo-target": { type: "string" },
31
32
  "node-triple": { type: "string" },
32
33
  "python-tag": { type: "string" },
@@ -231,11 +232,28 @@ const writable = (path) => {
231
232
  if (existsSync(path)) chmodSync(path, statSync(path).mode | 0o200);
232
233
  };
233
234
 
235
+ const cargoMetadata = spawnSync("cargo", ["metadata", "--format-version", "1", "--no-deps"], {
236
+ cwd: root,
237
+ encoding: "utf8",
238
+ });
239
+ if (cargoMetadata.error) {
240
+ throw new Error(`cargo metadata failed: ${cargoMetadata.error.message}`, {
241
+ cause: cargoMetadata.error,
242
+ });
243
+ }
244
+ if (cargoMetadata.status !== 0) {
245
+ throw new Error(`cargo metadata exited with ${cargoMetadata.status}`);
246
+ }
247
+ const cargoTargetRoot = JSON.parse(cargoMetadata.stdout).target_directory;
248
+ if (typeof cargoTargetRoot !== "string" || !cargoTargetRoot) {
249
+ throw new Error("cargo metadata returned no target_directory");
250
+ }
251
+
234
252
  const libraryPath = (crate, cargoTarget, os) => {
235
253
  const name = crate.replaceAll("-", "_");
236
254
  const extension = os === "darwin" ? "dylib" : os === "win32" ? "dll" : "so";
237
255
  const prefix = os === "win32" ? "" : "lib";
238
- return resolve(root, "target", cargoTarget, "release", `${prefix}${name}.${extension}`);
256
+ return resolve(cargoTargetRoot, cargoTarget, "release", `${prefix}${name}.${extension}`);
239
257
  };
240
258
 
241
259
  const packageNode = ({
@@ -387,6 +405,7 @@ const packagePython = ({
387
405
  version,
388
406
  cargoTarget,
389
407
  os,
408
+ pythonModule,
390
409
  }) => {
391
410
  const pythonRoot = resolve(output, "python-root");
392
411
  cpSync(resolve(root, pythonDirectory), pythonRoot, { recursive: true });
@@ -407,12 +426,12 @@ const packagePython = ({
407
426
  }
408
427
  writeFileSync(pyproject, metadata);
409
428
 
410
- const packageName = crate.replace(/^dbx-tools-/, "").replaceAll("-", "_");
411
- const packageDirectory = resolve(pythonRoot, "src", "dbx_tools", packageName);
429
+ const packageName = pythonModule.split(".").at(-1);
430
+ if (!packageName) throw new Error(`Invalid Python module ${pythonModule}`);
431
+ const packageDirectory = resolve(pythonRoot, "src", ...pythonModule.split("."));
412
432
  const generatedDirectory = mkdtempSync(join(tmpdir(), `${packageName}-python-`));
413
433
  const generator = resolve(
414
- root,
415
- "target",
434
+ cargoTargetRoot,
416
435
  cargoTarget,
417
436
  "release",
418
437
  `${crate}-uniffi-bindgen${os === "win32" ? ".exe" : ""}`,
@@ -506,6 +525,7 @@ const build = () => {
506
525
  version,
507
526
  cargoTarget,
508
527
  os,
528
+ pythonModule: required("python-module"),
509
529
  });
510
530
  }
511
531
  };
package/tasks/uniffi.ts CHANGED
@@ -21,7 +21,6 @@ import {
21
21
  addExplicitInterfaceReexports,
22
22
  addTypeScriptExtensionsToBindingImports,
23
23
  makeDefaultedInterfaceParametersOptional,
24
- mergePythonBindingExports,
25
24
  } from "../src/uniffi.ts";
26
25
 
27
26
  const { values } = parseArgs({
@@ -30,6 +29,7 @@ const { values } = parseArgs({
30
29
  crate: { type: "string" },
31
30
  node: { type: "string" },
32
31
  python: { type: "string" },
32
+ "python-module": { type: "string" },
33
33
  "cargo-target": { type: "string" },
34
34
  "node-package-base": { type: "string" },
35
35
  ubrn: { type: "string" },
@@ -54,10 +54,6 @@ const dependencies =
54
54
  const extension =
55
55
  process.platform === "darwin" ? "dylib" : process.platform === "win32" ? "dll" : "so";
56
56
  const prefix = process.platform === "win32" ? "" : "lib";
57
- const targetDirectory = values["cargo-target"]
58
- ? join(root, "target", values["cargo-target"], "release")
59
- : join(root, "target", "release");
60
- const library = join(targetDirectory, `${prefix}${libraryName}.${extension}`);
61
57
  const normalizedOutput = (value: string): string =>
62
58
  value
63
59
  .replace(/[\p{Extended_Pictographic}\uFE0F]/gu, "")
@@ -75,6 +71,28 @@ const run = (command: string, args: string[]) => {
75
71
  if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`);
76
72
  };
77
73
 
74
+ const cargoMetadata = spawnSync("cargo", ["metadata", "--format-version", "1", "--no-deps"], {
75
+ cwd: root,
76
+ encoding: "utf8",
77
+ });
78
+ if (cargoMetadata.error) {
79
+ throw new Error(`cargo metadata failed: ${cargoMetadata.error.message}`, {
80
+ cause: cargoMetadata.error,
81
+ });
82
+ }
83
+ if (cargoMetadata.status !== 0) {
84
+ throw new Error(`cargo metadata exited with ${cargoMetadata.status}`);
85
+ }
86
+ const cargoTargetRoot = (JSON.parse(cargoMetadata.stdout) as { target_directory?: unknown })
87
+ .target_directory;
88
+ if (typeof cargoTargetRoot !== "string" || !cargoTargetRoot) {
89
+ throw new Error("cargo metadata returned no target_directory");
90
+ }
91
+ const targetDirectory = values["cargo-target"]
92
+ ? join(cargoTargetRoot, values["cargo-target"], "release")
93
+ : join(cargoTargetRoot, "release");
94
+ const library = join(targetDirectory, `${prefix}${libraryName}.${extension}`);
95
+
78
96
  const replaceGenerated = (source: string, destination: string): void => {
79
97
  const deadline = Date.now() + 5_000;
80
98
  const signal = new Int32Array(new SharedArrayBuffer(4));
@@ -243,12 +261,10 @@ if (values.node) {
243
261
  }
244
262
 
245
263
  if (values.python) {
246
- const pythonPackage = resolve(
247
- root,
248
- values.python,
249
- "src/dbx_tools",
250
- crate.replace(/^dbx-tools-/, "").replaceAll("-", "_"),
251
- );
264
+ if (!values["python-module"]) {
265
+ throw new Error("Expected --python-module with --python");
266
+ }
267
+ const pythonPackage = resolve(root, values.python, "src", ...values["python-module"].split("."));
252
268
  const pythonOutput = mkdtempSync(join(tmpdir(), `${libraryName}-python-`));
253
269
  run(
254
270
  join(targetDirectory, `${crate}-uniffi-bindgen${process.platform === "win32" ? ".exe" : ""}`),
@@ -269,14 +285,7 @@ if (values.python) {
269
285
  stampGeneratedPython(pythonBindings);
270
286
  const pythonInit = join(pythonPackage, "__init__.py");
271
287
  makeWritable(pythonInit);
272
- writeFileSync(
273
- pythonInit,
274
- mergePythonBindingExports(
275
- existsSync(pythonInit) ? readFileSync(pythonInit, "utf8") : "",
276
- readFileSync(pythonBindings, "utf8"),
277
- { crate, file: pythonInit },
278
- ),
279
- );
288
+ writeFileSync(pythonInit, "");
280
289
  makeReadonly(pythonInit);
281
290
  const pythonLibrary = join(pythonPackage, basename(library));
282
291
  makeWritable(pythonLibrary);