@dbx-tools/projen 0.6.185 → 0.6.188

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
@@ -153,10 +154,17 @@ skips matching files and fails when an existing filename has different content.
153
154
 
154
155
  The Rust matrix has one row per target. Each row installs native dependencies,
155
156
  restores Cargo and sccache once, builds the Cargo workspace once, then packages
156
- every discovered output from that shared build. Set a source-only crate's
157
- `releaseExcludeOs` package option to omit it from incompatible rows through
158
- Cargo `--exclude`; crates that produce UniFFI or release-binary artifacts cannot
159
- use this option because every configured target must produce their artifacts.
157
+ every discovered output from that shared build. Set a source-only crate's or
158
+ release-enabled binary's `releaseExcludeOs` package option to omit it from
159
+ incompatible rows through Cargo `--exclude`. Release binary packaging and
160
+ artifact upload are skipped in those rows. UniFFI crates cannot use this option
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.
160
168
  Rust rows prepare native npm
161
169
  archives and Python wheels; they do not install Bun or UBRN and do not build
162
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.184";
6
+ export const PACKAGE_VERSION = "0.6.187";
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.185",
29
- "@dbx-tools/path": "0.6.185",
30
- "@dbx-tools/shared-core": "0.6.185",
28
+ "@dbx-tools/core": "0.6.188",
29
+ "@dbx-tools/path": "0.6.188",
30
+ "@dbx-tools/shared-core": "0.6.188",
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.185",
50
+ "version": "0.6.188",
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;
@@ -46,7 +53,7 @@ export interface RustPackageOptions {
46
53
  readonly private?: boolean;
47
54
  /** Build this crate's binary for each target and attach it to the GitHub release. */
48
55
  readonly release?: boolean;
49
- /** Omit this source-only crate from release workspace builds on these operating systems. */
56
+ /** Omit this crate and any release binary artifact from these operating systems. */
50
57
  readonly releaseExcludeOs?: readonly RustReleaseOs[];
51
58
  readonly dependencies?: Readonly<Record<string, CargoDependency>>;
52
59
  readonly devDependencies?: Readonly<Record<string, CargoDependency>>;
@@ -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 {
@@ -230,6 +239,21 @@ function releasePlatformsFromEnvironment(
230
239
  });
231
240
  }
232
241
 
242
+ function releaseBinaryAssetName(binaryName: string, nodeTarget: string, os: RustReleaseOs): string {
243
+ const extension = os === RustReleaseOs.WINDOWS ? "zip" : "tar.gz";
244
+ return `${binaryName}-${nodeTarget}.${extension}`;
245
+ }
246
+
247
+ function rustCliRegistrySource(binaries: readonly RustReleaseBinaryMapping[]): string {
248
+ return [
249
+ "// GENERATED by projen - DO NOT EDIT.",
250
+ "// Rust release package options are the source of truth.",
251
+ "",
252
+ `export const RUST_RELEASE_BINARY_COMMANDS = ${JSON.stringify(binaries, null, 2)} as const;`,
253
+ "",
254
+ ].join("\n");
255
+ }
256
+
233
257
  function uniffiReleaseTaskSource(): string {
234
258
  const sourceDirectory = dirname(fileURLToPath(import.meta.url));
235
259
  const candidates = [
@@ -259,6 +283,23 @@ export interface RustWorkspaceMapping {
259
283
  readonly root: string;
260
284
  readonly crates: readonly string[];
261
285
  readonly bindings: readonly RustBindingMapping[];
286
+ readonly binaries: readonly RustReleaseBinaryMapping[];
287
+ }
288
+
289
+ /** One platform archive published for a Rust CLI binary. */
290
+ export interface RustReleaseBinaryAssetMapping {
291
+ readonly os: RustReleaseOs;
292
+ readonly cpu: RustReleaseCpu;
293
+ readonly name: string;
294
+ }
295
+
296
+ /** Runtime metadata for one lazily installed Rust CLI binary. */
297
+ export interface RustReleaseBinaryMapping {
298
+ readonly command: string;
299
+ readonly description: string;
300
+ readonly binaryName: string;
301
+ readonly repository: string;
302
+ readonly assets: readonly RustReleaseBinaryAssetMapping[];
262
303
  }
263
304
 
264
305
  export function orderRustBindings(bindings: readonly RustBindingMapping[]): RustBindingMapping[] {
@@ -474,12 +515,13 @@ function rustPackageDependencies(
474
515
  );
475
516
  }
476
517
 
477
- /** Generated Rust workspace plus convention-derived UniFFI facade packages. */
518
+ /** Generated Rust workspace plus convention-derived UniFFI binding packages. */
478
519
  export class DBXToolsRustWorkspace {
479
520
  readonly packages: readonly DBXToolsRustProject[];
480
521
  readonly nodePackages: readonly DBXToolsTypeScriptProject[];
481
522
  readonly pythonPackages: readonly PythonPackageOptions[];
482
523
  readonly bindingMappings: readonly RustBindingMapping[];
524
+ readonly releaseBinaries: readonly RustReleaseBinaryMapping[];
483
525
  readonly workspaceMapping: RustWorkspaceMapping;
484
526
 
485
527
  constructor(project: javascript.NodeProject, options: DBXToolsRustWorkspaceOptions) {
@@ -492,6 +534,7 @@ export class DBXToolsRustWorkspace {
492
534
  projectRepositoryUrl(project) ??
493
535
  coreProject.repositoryUrl(project.outdir) ??
494
536
  "";
537
+ const nativeTargets = releaseTargets(options);
495
538
  const pythonModulePrefix = options.pythonModulePrefix ?? scope.replaceAll("-", "_");
496
539
  const packageOptions = options.packages ?? {};
497
540
  this.packages = discoverRustCrates(resolve(project.outdir, root)).map(
@@ -501,15 +544,65 @@ export class DBXToolsRustWorkspace {
501
544
  ...packageOptions[directory],
502
545
  }),
503
546
  );
547
+ const commands = new Set<string>();
548
+ this.releaseBinaries = this.packages
549
+ .filter((pkg) => Boolean(pkg.packageOptions.cli))
550
+ .map((pkg) => {
551
+ if (!pkg.packageOptions.release) {
552
+ throw new Error(`${pkg.crateName} must set release when cli is configured`);
553
+ }
554
+ const configured = pkg.packageOptions.cli;
555
+ const command =
556
+ typeof configured === "object" && configured.command
557
+ ? configured.command
558
+ : pkg.packageOptions.directory;
559
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(command)) {
560
+ throw new Error(`Invalid Rust CLI command: ${command}`);
561
+ }
562
+ if (commands.has(command)) {
563
+ throw new Error(`Duplicate Rust CLI command: ${command}`);
564
+ }
565
+ commands.add(command);
566
+ const description =
567
+ (typeof configured === "object" ? configured.description : undefined) ??
568
+ pkg.packageOptions.description ??
569
+ pkg.crateName;
570
+ const excludedOs = new Set(pkg.packageOptions.releaseExcludeOs ?? []);
571
+ const binaryName = pkg.packageOptions.binaryName ?? pkg.crateName;
572
+ return {
573
+ command,
574
+ description,
575
+ binaryName,
576
+ repository,
577
+ assets: nativeTargets
578
+ .filter((target) => !excludedOs.has(target.os))
579
+ .map((target) => ({
580
+ os: target.os,
581
+ cpu: target.cpu,
582
+ name: releaseBinaryAssetName(binaryName, target.node, target.os),
583
+ })),
584
+ };
585
+ });
586
+ if (this.releaseBinaries.length && !repository) {
587
+ throw new Error("A repository URL is required when Rust CLI commands are configured");
588
+ }
589
+ if (this.releaseBinaries.length && !options.cliRegistryPath) {
590
+ throw new Error("cliRegistryPath is required when Rust CLI commands are configured");
591
+ }
592
+ if (options.cliRegistryPath) {
593
+ new TextFile(project, options.cliRegistryPath, {
594
+ lines: rustCliRegistrySource(this.releaseBinaries).trimEnd().split("\n"),
595
+ });
596
+ }
504
597
 
505
598
  const bindings = this.packages.filter((pkg) => pkg.uniffi);
506
599
  const packageDependencies = (pkg: DBXToolsRustProject) =>
507
600
  rustPackageDependencies(this.packages, pkg, project.outdir, options.workspaceDependencies);
508
601
  for (const pkg of this.packages) {
509
602
  const excludedOs = new Set(pkg.packageOptions.releaseExcludeOs ?? []);
510
- if (excludedOs.size && (pkg.uniffi || pkg.packageOptions.release)) {
603
+ if (excludedOs.size && pkg.uniffi) {
511
604
  throw new Error(
512
- `${pkg.crateName} cannot set releaseExcludeOs because it produces target-specific release artifacts`,
605
+ `${pkg.crateName} cannot set releaseExcludeOs because UniFFI requires every configured target`,
513
606
  );
514
607
  }
515
608
  for (const dependency of packageDependencies(pkg)) {
@@ -531,7 +624,7 @@ export class DBXToolsRustWorkspace {
531
624
  this.bindingMappings = orderRustBindings(
532
625
  bindings.map((pkg) => {
533
626
  const targets = pkg.packageOptions.bindings ?? ["node", "python"];
534
- const packageName = pkg.packageOptions.directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
627
+ const packageDirectory = `${string.toSlug(pkg.packageOptions.directory)}-rs`;
535
628
  const dependencies = [
536
629
  ...new Set([...bindingDependencies(pkg, "node"), ...bindingDependencies(pkg, "python")]),
537
630
  ].map((dependency) => dependency.crateName);
@@ -541,15 +634,15 @@ export class DBXToolsRustWorkspace {
541
634
  rust: `${root}/${pkg.packageOptions.directory}`,
542
635
  ...(targets.includes("node")
543
636
  ? {
544
- node: `${nodeRoot}/${pkg.packageOptions.directory}`,
545
- nodePackage: `@${scope}/${packageName}`,
637
+ node: `${nodeRoot}/${packageDirectory}`,
638
+ nodePackage: `@${scope}/${packageDirectory}`,
546
639
  }
547
640
  : {}),
548
641
  ...(targets.includes("python")
549
642
  ? {
550
- python: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`,
551
- pythonPackage: pkg.crateName,
552
- pythonModule: pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory),
643
+ python: `${options.pythonRoot ?? "packages/py"}/${packageDirectory}`,
644
+ pythonPackage: `${scope}-${packageDirectory}`,
645
+ pythonModule: pythonModuleName(pythonModulePrefix, packageDirectory),
553
646
  }
554
647
  : {}),
555
648
  };
@@ -567,7 +660,10 @@ export class DBXToolsRustWorkspace {
567
660
  "bindings.python.external_packages": Object.fromEntries(
568
661
  dependencies.map((dependency) => [
569
662
  dependency.crateName.replaceAll("-", "_"),
570
- `${pythonModuleName(pythonModulePrefix, dependency.packageOptions.directory)}.bindings`,
663
+ `${pythonModuleName(
664
+ pythonModulePrefix,
665
+ `${string.toSlug(dependency.packageOptions.directory)}-rs`,
666
+ )}.bindings`,
571
667
  ]),
572
668
  ),
573
669
  })
@@ -583,6 +679,7 @@ export class DBXToolsRustWorkspace {
583
679
  root,
584
680
  crates: this.packages.map((pkg) => `${root}/${pkg.packageOptions.directory}`),
585
681
  bindings: this.bindingMappings,
682
+ binaries: this.releaseBinaries,
586
683
  };
587
684
  if (dbxToolsProject) {
588
685
  dbxToolsProject.dbxToolsConfig.rust = this.workspaceMapping;
@@ -609,23 +706,25 @@ export class DBXToolsRustWorkspace {
609
706
  this.pythonPackages = bindings
610
707
  .filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("python"))
611
708
  .map((pkg) => {
612
- const module = pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory);
709
+ const directory = `${string.toSlug(pkg.packageOptions.directory)}-rs`;
710
+ const module = pythonModuleName(pythonModulePrefix, directory);
711
+ const name = `${scope}-${directory}`;
613
712
  return {
614
- directory: pkg.packageOptions.directory,
615
- name: pkg.crateName,
713
+ directory,
714
+ name,
616
715
  module,
617
716
  description: `Python bindings for ${pkg.crateName}`,
618
717
  uniffi: true,
619
718
  internalDependencies: bindingDependencies(pkg, "python").map(
620
- (dependency) => dependency.packageOptions.directory,
719
+ (dependency) => `${string.toSlug(dependency.packageOptions.directory)}-rs`,
621
720
  ),
622
721
  generatedSources: [
623
722
  `src/${module.replaceAll(".", "/")}/bindings.py`,
624
723
  `src/${module.replaceAll(".", "/")}/__init__.py`,
625
724
  ],
626
725
  trustedPublisher: {
627
- environment: `pypi-${pkg.crateName}`,
628
- artifacts: `platform-specific wheels for ${releaseTargets(options)
726
+ environment: `pypi-${name}`,
727
+ artifacts: `platform-specific wheels for ${nativeTargets
629
728
  .map((target) => `${target.os}-${target.cpu}`)
630
729
  .join(", ")}; all architectures publish to this one PyPI project`,
631
730
  },
@@ -639,7 +738,7 @@ export class DBXToolsRustWorkspace {
639
738
  for (const binding of bindings.filter((pkg) =>
640
739
  (pkg.packageOptions.bindings ?? ["node", "python"]).includes("node"),
641
740
  )) {
642
- const directory = binding.packageOptions.directory;
741
+ const directory = `${string.toSlug(binding.packageOptions.directory)}-rs`;
643
742
  const memberPath = `${nodeRoot}/${directory}`;
644
743
  const found = existing.get(memberPath);
645
744
  const existingNode = found instanceof DBXToolsTypeScriptProject ? found : undefined;
@@ -656,10 +755,7 @@ export class DBXToolsRustWorkspace {
656
755
  `@${scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
657
756
  );
658
757
  node.dbxToolsConfig.uniffi = true;
659
- if (!existingNode) {
660
- node.package.addField("description", `Node bindings for ${binding.crateName}`);
661
- }
662
- const nativeTargets = releaseTargets(options);
758
+ node.package.addField("description", `Node bindings for ${binding.crateName}`);
663
759
  if (options.release ?? true) {
664
760
  node.package.addField(
665
761
  "optionalDependencies",
@@ -674,16 +770,11 @@ export class DBXToolsRustWorkspace {
674
770
  node.addDeps("@ubjs/core@0.31.0-5", "@ubjs/node@0.31.0-5");
675
771
  node.addDeps(
676
772
  ...bindingDependencies(binding, "node").map(
677
- (dependency) => `@${scope}/${dependency.packageOptions.directory}@workspace:*`,
773
+ (dependency) =>
774
+ `@${scope}/${string.toSlug(dependency.packageOptions.directory)}-rs@workspace:*`,
678
775
  ),
679
776
  );
680
- if (binding.packageOptions.nodeDependencies?.length) {
681
- node.addDeps(...binding.packageOptions.nodeDependencies);
682
- }
683
777
  node.addDevDeps(`uniffi-bindgen-react-native@${UBRN_VERSION}`);
684
- if (binding.packageOptions.nodeDevDependencies?.length) {
685
- node.addDevDeps(...binding.packageOptions.nodeDevDependencies);
686
- }
687
778
  nodePackages.push(node);
688
779
  }
689
780
  this.nodePackages = nodePackages;
@@ -728,20 +819,9 @@ export class DBXToolsRustWorkspace {
728
819
  if (this.bindingMappings.some((binding) => binding.node)) {
729
820
  project.tasks.tryFind("pre-compile")?.spawn(bindingsTask);
730
821
  }
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
- });
822
+ project.removeTask("rs:bindings:demo");
743
823
  if (releaseEnabled) {
744
- this.addReleaseWorkflow(project, options, releaseTargets(options));
824
+ this.addReleaseWorkflow(project, options, nativeTargets);
745
825
  }
746
826
  }
747
827
 
@@ -764,6 +844,7 @@ export class DBXToolsRustWorkspace {
764
844
  .map((pkg) => ({
765
845
  crate: pkg.crateName,
766
846
  binary: pkg.packageOptions.binaryName ?? pkg.crateName,
847
+ excludedOs: pkg.packageOptions.releaseExcludeOs ?? [],
767
848
  }));
768
849
  const publicCrates = orderRustBindings(
769
850
  this.packages
@@ -812,6 +893,7 @@ export class DBXToolsRustWorkspace {
812
893
  `--python "${binding.python}"`,
813
894
  `--node-package "${binding.nodePackage}"`,
814
895
  `--python-package "${binding.pythonPackage}"`,
896
+ `--python-module "${binding.pythonModule}"`,
815
897
  '--cargo-target "${{ matrix.cargo }}"',
816
898
  '--node-triple "${{ matrix.node }}"',
817
899
  '--python-tag "${{ matrix.python }}"',
@@ -823,18 +905,40 @@ export class DBXToolsRustWorkspace {
823
905
  "--skip-build",
824
906
  ].join(" \\\n "),
825
907
  );
826
- const binaryCommands = releaseBinaries.flatMap((pkg) => [
827
- `mkdir -p "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
828
- `SOURCE="target/\${{ matrix.cargo }}/release/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
829
- `DESTINATION="dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
830
- 'cp "$SOURCE" "$DESTINATION"',
831
- 'if [ "${{ matrix.os }}" = "win32" ]; then',
832
- ` 7z a "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${pkg.binary}-\${{ matrix.node }}.zip" "$DESTINATION"`,
833
- "else",
834
- ` 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}"`,
835
- "fi",
836
- `rm -rf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
837
- ]);
908
+ const releaseBinaryCondition = (excludedOs: readonly RustReleaseOs[]) =>
909
+ excludedOs.length
910
+ ? `\${{ ${excludedOs.map((os) => `matrix.os != '${os}'`).join(" && ")} }}`
911
+ : undefined;
912
+ const binaryCommands = releaseBinaries.flatMap((pkg) => {
913
+ const windowsAsset = releaseBinaryAssetName(
914
+ pkg.binary,
915
+ "${{ matrix.node }}",
916
+ RustReleaseOs.WINDOWS,
917
+ );
918
+ const unixAsset = releaseBinaryAssetName(
919
+ pkg.binary,
920
+ "${{ matrix.node }}",
921
+ RustReleaseOs.LINUX,
922
+ );
923
+ const commands = [
924
+ `mkdir -p "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
925
+ `SOURCE="target/\${{ matrix.cargo }}/release/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
926
+ `DESTINATION="dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
927
+ 'cp "$SOURCE" "$DESTINATION"',
928
+ 'if [ "${{ matrix.os }}" = "win32" ]; then',
929
+ ` 7z a "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${windowsAsset}" "$DESTINATION"`,
930
+ "else",
931
+ ` tar -C "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage" -czf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${unixAsset}" "${pkg.binary}"`,
932
+ "fi",
933
+ `rm -rf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
934
+ ];
935
+ const excludedCondition = pkg.excludedOs
936
+ .map((os) => `[ "\${{ matrix.os }}" != "${os}" ]`)
937
+ .join(" && ");
938
+ return excludedCondition
939
+ ? [`if ${excludedCondition}; then`, ...commands.map((command) => ` ${command}`), "fi"]
940
+ : commands;
941
+ });
838
942
  const artifactSteps: JobStep[] = [
839
943
  ...bindings.flatMap((binding) => [
840
944
  ...(binding.node
@@ -856,7 +960,7 @@ export class DBXToolsRustWorkspace {
856
960
  name: `Upload ${binding.crate} Python wheel`,
857
961
  uses: "actions/upload-artifact@v7",
858
962
  with: {
859
- name: `${binding.crate}--\${{ matrix.python }}--python-wheel`,
963
+ name: `${binding.pythonPackage}--\${{ matrix.python }}--python-wheel`,
860
964
  path: `dist/release/${binding.crate}/\${{ matrix.node }}/python/*.whl`,
861
965
  "retention-days": 7,
862
966
  },
@@ -867,6 +971,9 @@ export class DBXToolsRustWorkspace {
867
971
  ...releaseBinaries.map((pkg) => ({
868
972
  name: `Upload ${pkg.crate} release binary`,
869
973
  uses: "actions/upload-artifact@v7",
974
+ ...(releaseBinaryCondition(pkg.excludedOs)
975
+ ? { if: releaseBinaryCondition(pkg.excludedOs) }
976
+ : {}),
870
977
  with: {
871
978
  name: `${pkg.crate}-\${{ matrix.node }}-binary`,
872
979
  path: `dist/release/${pkg.crate}/\${{ matrix.node }}/binary/*`,
@@ -926,7 +1033,11 @@ export class DBXToolsRustWorkspace {
926
1033
  {
927
1034
  name: "Install Linux native dependencies",
928
1035
  if: "${{ matrix.os == 'linux' }}",
929
- run: "sudo apt-get update && sudo apt-get install --yes libdbus-1-dev pkg-config",
1036
+ run: [
1037
+ "sudo rm -f /etc/apt/sources.list.d/google-chrome.list",
1038
+ "sudo apt-get update",
1039
+ "sudo apt-get install --yes libdbus-1-dev pkg-config",
1040
+ ].join("\n"),
930
1041
  },
931
1042
  {
932
1043
  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,
@@ -330,7 +330,7 @@ program
330
330
  "-m",
331
331
  "🌸 Shipped with Kanna — https://kanna.sh",
332
332
  "-m",
333
- "Co-Authored-By: Kanna <noreply@kanna.sh>\nKanna-Agent: codex/gpt",
333
+ "Co-Authored-By: Kanna <noreply@kanna.sh>\nKanna-Agent: codex/databricks-gpt-5-6-sol",
334
334
  ]);
335
335
  } else {
336
336
  logger.info("nothing to commit");
@@ -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);