@dbx-tools/projen 0.6.151 → 0.6.153

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
@@ -104,11 +104,26 @@ removing a crate or `setup_scaffolding!()` marker triggers a full synth. Repos
104
104
  without Rust crates start no Rust watcher. When Rust projects are detected,
105
105
  Cargo is required and the focused task fails immediately if it is unavailable.
106
106
 
107
- The workspace also generates a `rust-release` workflow from those discovered
108
- UniFFI crates. It builds Linux x64/arm64, macOS x64/arm64, and Windows x64.
109
- Node publishes a small facade plus optional OS/CPU packages, so npm installs
110
- only the matching native library. Python publishes one platform-tagged wheel
111
- per target, so pip follows the same thin-install model.
107
+ The workspace also generates a `rust-release` workflow from discovered crates,
108
+ UniFFI bindings, and release-enabled binaries. Its matrix has one row per
109
+ target. Each row installs native dependencies and restores Cargo/sccache once,
110
+ builds the Rust workspace once, then packages every discovered output from that
111
+ shared build. Bun and the workspace install are present only when a Node binding
112
+ needs TypeScript generation; uv is present only when a Python wheel is needed.
113
+ A binary-only workspace therefore installs neither.
114
+
115
+ The Node generator's Rust CLI is cached under one target directory keyed by its
116
+ pinned UBRN version and runner architecture, then prepared before the workspace
117
+ build. Python generation executes the already-built
118
+ `target/<triple>/release/uniffi-bindgen` directly. Artifact packaging therefore
119
+ does no Rust compilation after the main workspace build.
120
+
121
+ Artifacts identify their crate, target, and type. Download-only publication
122
+ jobs publish native npm platform archives before the facade and publish
123
+ platform-tagged Python wheels without checkout, Bun, or `bun install`.
124
+ Non-private Cargo crates publish from a source-only job with
125
+ `cargo publish --no-verify`; GitHub Release uploads likewise consume prebuilt
126
+ binary artifacts without reinstalling a toolchain.
112
127
 
113
128
  Set the repository variable `LOCAL_REPOSITORIES=true` to enable the generated
114
129
  self-hosted mirror job. Configure `LOCAL_NPM_REGISTRY` and
@@ -119,11 +134,13 @@ detects its OS, architecture, libc, Rust target, and Python wheel tag and builds
119
134
  only that native target. Public Cargo crates also publish directly to crates.io
120
135
  with `CARGO_REGISTRY_TOKEN`. Override `releaseTargets` only when a consumer has
121
136
  additional native runners; ordinary projects inherit the maintained matrix
122
- automatically.
137
+ automatically. `bun run bump` also accepts repeatable `--os` and `--arch`
138
+ selectors; every selected operating system is crossed with every selected
139
+ architecture. Omit both filters to regenerate the complete maintained matrix.
123
140
 
124
- Private Python projects are marked with `[tool.dbx-tools] private = true`. They
125
- are excluded from the uv workspace, documentation, and Python release workflow
126
- until their native artifact publishing matrix is enabled.
141
+ Private Python binding projects are marked with `[tool.dbx-tools] private =
142
+ true`. They stay out of the standard uv/Python release and docs surfaces;
143
+ `rust-release` publishes their prebuilt native wheels directly.
127
144
 
128
145
  ## Customize Packages With Mixins
129
146
 
@@ -318,10 +335,12 @@ Use `--local-registry false` or `--local-pypi false` to disable either local
318
335
  publish. An explicit `--local-pypi http://localhost:3141/user/index/` overrides
319
336
  auto-detection; `--python-root` defaults to `packages/py`.
320
337
 
321
- The pushed `v*` tag is also the public release boundary: it triggers npm
322
- publishing, stamps and publishes every Python distribution to PyPI, and rebuilds
323
- and deploys the documentation site. Ordinary pushes to `main` publish none of
324
- those surfaces.
338
+ The pushed `v*` tag is also the public release boundary. Available workflow
339
+ stages form the generated chain Rust -> Python -> Node -> docs: Rust builds and
340
+ publishes native artifacts and Cargo crates, Python publishes standard
341
+ distributions, Node publishes standard workspace packages, and docs deploys
342
+ after publication. A stage with no corresponding outputs is omitted. Ordinary
343
+ pushes to `main` publish none of those surfaces.
325
344
 
326
345
  Members intentionally keep only the tasks that something OTHER than a human
327
346
  invokes, so there is no second place to run the same thing:
package/package.json CHANGED
@@ -26,9 +26,9 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@clack/prompts": "^1.7.0",
29
- "@dbx-tools/core": "0.6.151",
30
- "@dbx-tools/path": "0.6.151",
31
- "@dbx-tools/shared-core": "0.6.151",
29
+ "@dbx-tools/core": "0.6.153",
30
+ "@dbx-tools/path": "0.6.153",
31
+ "@dbx-tools/shared-core": "0.6.153",
32
32
  "commander": "^15.0.0",
33
33
  "concurrently": "^10.0.3",
34
34
  "constructs": "^10.6.0",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "main": "index.ts",
50
50
  "license": "Apache-2.0",
51
- "version": "0.6.151",
51
+ "version": "0.6.153",
52
52
  "types": "index.ts",
53
53
  "type": "module",
54
54
  "exports": {
@@ -0,0 +1,14 @@
1
+ /** Cartesian-product release target filter shared by the bump task. */
2
+ export function releasePlatformFilter(
3
+ operatingSystems: readonly string[],
4
+ architectures: readonly string[],
5
+ ): string {
6
+ if ((operatingSystems.length === 0) !== (architectures.length === 0)) {
7
+ throw new Error("--os and --arch must be used together");
8
+ }
9
+ return operatingSystems
10
+ .flatMap((operatingSystem) =>
11
+ architectures.map((architecture) => `${operatingSystem}:${architecture}`),
12
+ )
13
+ .join(",");
14
+ }
package/src/project-js.ts CHANGED
@@ -379,6 +379,7 @@ function defaultProjectOptions(
379
379
  // provider. See {@link DBXToolsRelease}.
380
380
  ...(isRoot ? {} : { npmAccess: javascript.NpmAccess.PUBLIC }),
381
381
  buildWorkflow: false,
382
+ workflowPackageCache: false,
382
383
  release: false,
383
384
  // The root build validates the whole workspace and must not also pack every
384
385
  // member into unused `dist/js` tarballs. Child projects keep projen's package
@@ -518,7 +519,7 @@ export interface DBXToolsJavaScriptProjectOptions
518
519
  /** Workflow that must finish successfully before the main Node release runs. */
519
520
  readonly releaseUpstreamWorkflow?: string;
520
521
  /** Main Node release workflow name. Defaults to `node-release`. */
521
- readonly releaseWorkflowName?: string | false;
522
+ readonly nodeReleaseWorkflowName?: string | false;
522
523
  /**
523
524
  * Extra workspace member paths (repo-relative, POSIX) to list in the workspace
524
525
  * config ALONGSIDE the discovered `packageRoots` members - for a package that
@@ -1188,7 +1189,7 @@ function initProject(
1188
1189
  tagPrefix: options.releaseTagPrefix,
1189
1190
  standaloneReleases: options.standaloneReleases,
1190
1191
  upstreamWorkflow: options.releaseUpstreamWorkflow,
1191
- workflowName: options.releaseWorkflowName,
1192
+ workflowName: options.nodeReleaseWorkflowName ?? options.releaseWorkflowName,
1192
1193
  });
1193
1194
  }
1194
1195
 
package/src/project-rs.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  /** Filesystem-discovered Rust workspaces and UniFFI binding package wiring. */
2
2
  import { existsSync, readFileSync, readdirSync } from "node:fs";
3
- import { join, relative, resolve } from "node:path";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { string } from "@dbx-tools/shared-core";
5
6
  import { Project, TextFile, YamlFile, javascript } from "projen";
6
7
  import type { DBXToolsProject } from "./project.ts";
7
8
  import { DBXToolsTypeScriptProject } from "./project-js.ts";
8
9
  import type { PythonPackageOptions } from "./project-py.ts";
9
10
  import { readWorkspaceVersion } from "./workspace-version.ts";
10
- import { mixin } from "../index.ts";
11
- import { isDBXToolsJavaScriptProject, isDBXToolsProject } from "./project-predicate.ts";
11
+ import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
12
12
 
13
13
  export interface CargoDependencyOptions {
14
14
  readonly version?: string;
@@ -139,6 +139,7 @@ const RUST_CACHE_ENV = {
139
139
  RUSTC_WRAPPER: "sccache",
140
140
  SCCACHE_GHA_ENABLED: "true",
141
141
  } as const;
142
+ const UBRN_VERSION = "0.31.0-5";
142
143
 
143
144
  function rustCacheSteps(sharedKey: string): readonly Record<string, unknown>[] {
144
145
  return [
@@ -172,6 +173,17 @@ function releaseTargets(options: DBXToolsRustWorkspaceOptions): readonly UniFFIR
172
173
  });
173
174
  }
174
175
 
176
+ function uniffiReleaseTaskSource(): string {
177
+ const sourceDirectory = dirname(fileURLToPath(import.meta.url));
178
+ const candidates = [
179
+ resolve(sourceDirectory, "../tasks/uniffi-release.mjs"),
180
+ resolve(sourceDirectory, "../../tasks/uniffi-release.mjs"),
181
+ ];
182
+ const source = candidates.find(existsSync);
183
+ if (!source) throw new Error("Could not locate tasks/uniffi-release.mjs");
184
+ return readFileSync(source, "utf8");
185
+ }
186
+
175
187
  /** Persisted mapping consumed by the focused Rust source watcher. */
176
188
  export interface RustBindingMapping {
177
189
  readonly crate: string;
@@ -462,7 +474,7 @@ export class DBXToolsRustWorkspace {
462
474
  if (binding.packageOptions.nodeDependencies?.length) {
463
475
  node.addDeps(...binding.packageOptions.nodeDependencies);
464
476
  }
465
- node.addDevDeps("uniffi-bindgen-react-native@0.31.0-5");
477
+ node.addDevDeps(`uniffi-bindgen-react-native@${UBRN_VERSION}`);
466
478
  if (binding.packageOptions.nodeDevDependencies?.length) {
467
479
  node.addDevDeps(...binding.packageOptions.nodeDevDependencies);
468
480
  }
@@ -525,7 +537,8 @@ export class DBXToolsRustWorkspace {
525
537
  });
526
538
  if (
527
539
  (options.release ?? true) &&
528
- (this.bindingMappings.length > 0 || this.packages.some((pkg) => pkg.packageOptions.release))
540
+ (this.bindingMappings.length > 0 ||
541
+ this.packages.some((pkg) => pkg.packageOptions.release || !pkg.packageOptions.private))
529
542
  ) {
530
543
  this.addReleaseWorkflow(project, options, releaseTargets(options));
531
544
  }
@@ -544,118 +557,269 @@ export class DBXToolsRustWorkspace {
544
557
  nodePackage: binding.nodePackage ?? "",
545
558
  pythonPackage: binding.pythonPackage ?? "",
546
559
  }));
547
- const matrix = bindings.flatMap((binding) =>
548
- targets.map((target, index) => ({
549
- binding,
550
- target: { ...target, facade: index === 0 },
551
- })),
552
- );
553
560
  const releaseBinaries = this.packages
554
561
  .filter((pkg) => pkg.packageOptions.release)
555
562
  .map((pkg) => ({
556
563
  crate: pkg.crateName,
557
564
  binary: pkg.packageOptions.binaryName ?? pkg.crateName,
558
565
  }));
559
- const binaryMatrix = releaseBinaries.flatMap((pkg) =>
560
- targets.map((target) => ({ package: pkg, target })),
566
+ const publicCrates = this.packages
567
+ .filter((pkg) => !pkg.packageOptions.private)
568
+ .map((pkg) => pkg.crateName);
569
+ const targetMatrix = targets.map((target, index) => ({
570
+ target: { ...target, facade: index === 0 },
571
+ }));
572
+ const hasNodeBindings = bindings.some((binding) => binding.node);
573
+ const hasPythonBindings = bindings.some((binding) => binding.python);
574
+ const hasTargetOutputs = bindings.length > 0 || releaseBinaries.length > 0;
575
+ const releaseTask = ".projen/uniffi-release.mjs";
576
+ if (bindings.length) {
577
+ new TextFile(project, releaseTask, {
578
+ lines: uniffiReleaseTaskSource().trimEnd().split("\n"),
579
+ });
580
+ } else {
581
+ project.tryRemoveFile(releaseTask);
582
+ }
583
+ const bindingCommands = bindings.map((binding) =>
584
+ [
585
+ `node ${releaseTask} build`,
586
+ `--crate "${binding.crate}"`,
587
+ `--node "${binding.node}"`,
588
+ `--python "${binding.python}"`,
589
+ `--node-package "${binding.nodePackage}"`,
590
+ `--python-package "${binding.pythonPackage}"`,
591
+ ...(binding.node
592
+ ? ['--node-generator "node_modules/@dbx-tools/projen/tasks/uniffi.ts"']
593
+ : []),
594
+ '--cargo-target "${{ matrix.target.cargo }}"',
595
+ '--node-triple "${{ matrix.target.node }}"',
596
+ '--python-tag "${{ matrix.target.python }}"',
597
+ '--os "${{ matrix.target.os }}"',
598
+ '--cpu "${{ matrix.target.cpu }}"',
599
+ '--libc "${{ matrix.target.libc }}"',
600
+ '--facade "${{ matrix.target.facade }}"',
601
+ '--version "${VERSION#v}"',
602
+ `--output "dist/release/${binding.crate}/\${{ matrix.target.node }}"`,
603
+ "--skip-build",
604
+ ].join(" \\\n "),
561
605
  );
606
+ const binaryCommands = releaseBinaries.flatMap((pkg) => [
607
+ `mkdir -p "dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/stage"`,
608
+ `SOURCE="target/\${{ matrix.target.cargo }}/release/${pkg.binary}\${{ matrix.target.os == 'win32' && '.exe' || '' }}"`,
609
+ `DESTINATION="dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/stage/${pkg.binary}\${{ matrix.target.os == 'win32' && '.exe' || '' }}"`,
610
+ 'cp "$SOURCE" "$DESTINATION"',
611
+ 'if [ "${{ matrix.target.os }}" = "win32" ]; then',
612
+ ` 7z a "dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/${pkg.binary}-\${{ matrix.target.node }}.zip" "$DESTINATION"`,
613
+ "else",
614
+ ` tar -C "dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/stage" -czf "dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/${pkg.binary}-\${{ matrix.target.node }}.tar.gz" "${pkg.binary}"`,
615
+ "fi",
616
+ `rm -rf "dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/stage"`,
617
+ ]);
618
+ const artifactSteps = [
619
+ ...bindings.flatMap((binding) => [
620
+ ...(binding.node
621
+ ? [
622
+ {
623
+ name: `Upload ${binding.crate} native npm package`,
624
+ uses: "actions/upload-artifact@v7",
625
+ with: {
626
+ name: `${binding.crate}-\${{ matrix.target.node }}-npm`,
627
+ path: `dist/release/${binding.crate}/\${{ matrix.target.node }}/npm/*.tgz`,
628
+ },
629
+ },
630
+ {
631
+ name: `Upload ${binding.crate} npm facade`,
632
+ if: "${{ matrix.target.facade }}",
633
+ uses: "actions/upload-artifact@v7",
634
+ with: {
635
+ name: `${binding.crate}-npm-facade`,
636
+ path: `dist/release/${binding.crate}/\${{ matrix.target.node }}/npm-facade/*.tgz`,
637
+ },
638
+ },
639
+ ]
640
+ : []),
641
+ ...(binding.python
642
+ ? [
643
+ {
644
+ name: `Upload ${binding.crate} Python wheel`,
645
+ uses: "actions/upload-artifact@v7",
646
+ with: {
647
+ name: `${binding.crate}-\${{ matrix.target.python }}-python-wheel`,
648
+ path: `dist/release/${binding.crate}/\${{ matrix.target.node }}/python/*.whl`,
649
+ },
650
+ },
651
+ ]
652
+ : []),
653
+ ]),
654
+ ...releaseBinaries.map((pkg) => ({
655
+ name: `Upload ${pkg.crate} release binary`,
656
+ uses: "actions/upload-artifact@v7",
657
+ with: {
658
+ name: `${pkg.crate}-\${{ matrix.target.node }}-binary`,
659
+ path: `dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/*`,
660
+ },
661
+ })),
662
+ ];
562
663
  const buildJob = {
563
- name: "${{ matrix.binding.crate }} / ${{ matrix.target.node }}",
664
+ name: "${{ matrix.target.node }}",
564
665
  "runs-on": "${{ matrix.target.runner }}",
565
666
  env: RUST_CACHE_ENV,
566
667
  strategy: {
567
668
  "fail-fast": false,
568
- matrix: { include: matrix },
669
+ matrix: { include: targetMatrix },
569
670
  },
570
671
  steps: [
571
672
  { name: "Checkout", uses: "actions/checkout@v6" },
572
- {
573
- name: "Setup Bun",
574
- uses: "oven-sh/setup-bun@v2",
575
- with: { "bun-version": "1.3.14" },
576
- },
577
- { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
673
+ ...(hasNodeBindings
674
+ ? [
675
+ {
676
+ name: "Setup Bun",
677
+ uses: "oven-sh/setup-bun@v2",
678
+ with: { "bun-version": "1.3.14" },
679
+ },
680
+ ]
681
+ : []),
682
+ ...(hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
578
683
  {
579
684
  name: "Setup Rust",
580
685
  uses: "dtolnay/rust-toolchain@stable",
581
686
  with: { targets: "${{ matrix.target.cargo }}" },
582
687
  },
583
688
  ...rustCacheSteps("release-${{ matrix.target.cargo }}"),
689
+ ...(hasNodeBindings
690
+ ? [
691
+ {
692
+ name: "Cache UBRN generator",
693
+ uses: "actions/cache@v5",
694
+ with: {
695
+ path: "target/ubrn",
696
+ key: `ubrn-\${{ runner.os }}-\${{ runner.arch }}-${UBRN_VERSION}`,
697
+ },
698
+ },
699
+ ]
700
+ : []),
584
701
  {
585
702
  name: "Install Linux native dependencies",
586
703
  if: "${{ matrix.target.os == 'linux' }}",
587
704
  run: "sudo apt-get update && sudo apt-get install --yes libdbus-1-dev pkg-config",
588
705
  },
589
- { name: "Install", run: "bun install" },
706
+ ...(hasNodeBindings ? [{ name: "Install Node tooling", run: "bun install" }] : []),
707
+ ...(hasNodeBindings
708
+ ? [
709
+ {
710
+ name: "Prepare UBRN generator",
711
+ env: {
712
+ CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
713
+ },
714
+ run: "cargo build --manifest-path node_modules/uniffi-bindgen-react-native/crates/ubrn_cli/Cargo.toml",
715
+ },
716
+ ]
717
+ : []),
590
718
  {
591
- name: "Build thin native packages",
592
- shell: "bash",
593
- env: {
594
- VERSION: "${{ github.event_name == 'push' && github.ref_name || inputs.version }}",
595
- },
596
- run: 'bun node_modules/@dbx-tools/projen/tasks/uniffi-release.ts build --crate "${{ matrix.binding.crate }}" --rust "${{ matrix.binding.rust }}" --node "${{ matrix.binding.node }}" --python "${{ matrix.binding.python }}" --node-package "${{ matrix.binding.nodePackage }}" --python-package "${{ matrix.binding.pythonPackage }}" --cargo-target "${{ matrix.target.cargo }}" --node-triple "${{ matrix.target.node }}" --python-tag "${{ matrix.target.python }}" --os "${{ matrix.target.os }}" --cpu "${{ matrix.target.cpu }}" --libc "${{ matrix.target.libc }}" --facade "${{ matrix.target.facade }}" --version "${VERSION#v}"',
597
- },
598
- {
599
- name: "Upload native packages",
600
- uses: "actions/upload-artifact@v7",
601
- with: {
602
- name: "${{ matrix.binding.crate }}-${{ matrix.target.node }}",
603
- path: [
604
- "dist/uniffi/npm/*.tgz",
605
- "dist/uniffi/npm-facade/*.tgz",
606
- "dist/uniffi/python/*.whl",
607
- ].join("\n"),
608
- },
719
+ name: "Build Rust outputs",
720
+ run: 'cargo build --release --workspace --target "${{ matrix.target.cargo }}"',
609
721
  },
722
+ ...(bindingCommands.length
723
+ ? [
724
+ {
725
+ name: "Package UniFFI outputs",
726
+ shell: "bash",
727
+ env: {
728
+ VERSION:
729
+ "${{ github.event_name == 'push' && github.ref_name || inputs.version }}",
730
+ ...(hasNodeBindings
731
+ ? {
732
+ CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
733
+ }
734
+ : {}),
735
+ },
736
+ run: bindingCommands.join("\n"),
737
+ },
738
+ ]
739
+ : []),
740
+ ...(binaryCommands.length
741
+ ? [
742
+ {
743
+ name: "Package release binaries",
744
+ shell: "bash",
745
+ run: binaryCommands.join("\n"),
746
+ },
747
+ ]
748
+ : []),
749
+ ...artifactSteps,
610
750
  ],
611
751
  };
612
- const binaryBuildJob = {
613
- name: "${{ matrix.package.crate }} / ${{ matrix.target.node }}",
614
- "runs-on": "${{ matrix.target.runner }}",
615
- env: RUST_CACHE_ENV,
616
- strategy: { "fail-fast": false, matrix: { include: binaryMatrix } },
617
- steps: [
618
- { name: "Checkout", uses: "actions/checkout@v6" },
752
+ const bindingPublishJobs = Object.fromEntries(
753
+ bindings.map((binding) => [
754
+ `publish-${binding.crate}`,
619
755
  {
620
- name: "Setup Rust",
621
- uses: "dtolnay/rust-toolchain@stable",
622
- with: { targets: "${{ matrix.target.cargo }}" },
623
- },
624
- ...rustCacheSteps("release-${{ matrix.target.cargo }}"),
625
- {
626
- name: "Install Linux native dependencies",
627
- if: "${{ matrix.target.os == 'linux' }}",
628
- run: "sudo apt-get update && sudo apt-get install --yes libdbus-1-dev pkg-config",
629
- },
630
- {
631
- name: "Build release binary",
632
- shell: "bash",
633
- run: [
634
- 'cargo build --release --package "${{ matrix.package.crate }}" --bin "${{ matrix.package.binary }}" --target "${{ matrix.target.cargo }}"',
635
- "mkdir -p dist/rust-release/stage",
636
- "SOURCE=\"target/${{ matrix.target.cargo }}/release/${{ matrix.package.binary }}${{ matrix.target.os == 'win32' && '.exe' || '' }}\"",
637
- "DESTINATION=\"dist/rust-release/stage/${{ matrix.package.binary }}${{ matrix.target.os == 'win32' && '.exe' || '' }}\"",
638
- 'cp "$SOURCE" "$DESTINATION"',
639
- 'if [ "${{ matrix.target.os }}" = "win32" ]; then',
640
- ' ARCHIVE="dist/rust-release/${{ matrix.package.binary }}-${{ matrix.target.node }}.zip"',
641
- ' 7z a "$ARCHIVE" "$DESTINATION"',
642
- "else",
643
- ' ARCHIVE="dist/rust-release/${{ matrix.package.binary }}-${{ matrix.target.node }}.tar.gz"',
644
- ' tar -C dist/rust-release/stage -czf "$ARCHIVE" "${{ matrix.package.binary }}"',
645
- "fi",
646
- "rm -rf dist/rust-release/stage",
647
- ].join("\n"),
648
- },
649
- {
650
- name: "Upload release binary",
651
- uses: "actions/upload-artifact@v7",
652
- with: {
653
- name: "release-${{ matrix.package.crate }}-${{ matrix.target.node }}",
654
- path: "dist/rust-release/*",
756
+ if: "${{ github.event_name == 'push' }}",
757
+ needs: ["build"],
758
+ "runs-on": "ubuntu-latest",
759
+ permissions: {
760
+ contents: "read",
761
+ ...(binding.python ? { "id-token": "write" } : {}),
655
762
  },
763
+ ...(binding.python ? { environment: { name: `pypi-${binding.crate}` } } : {}),
764
+ steps: [
765
+ ...(binding.node
766
+ ? [
767
+ {
768
+ name: "Setup Node.js",
769
+ uses: "actions/setup-node@v6",
770
+ with: { "registry-url": "https://registry.npmjs.org" },
771
+ },
772
+ {
773
+ name: "Download native npm packages",
774
+ uses: "actions/download-artifact@v8",
775
+ with: {
776
+ pattern: `${binding.crate}-*-npm`,
777
+ path: "dist/npm",
778
+ "merge-multiple": true,
779
+ },
780
+ },
781
+ {
782
+ name: "Download npm facade",
783
+ uses: "actions/download-artifact@v8",
784
+ with: {
785
+ name: `${binding.crate}-npm-facade`,
786
+ path: "dist/npm-facade",
787
+ },
788
+ },
789
+ {
790
+ name: "Publish native npm packages",
791
+ env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" },
792
+ run: 'for package in dist/npm/*.tgz; do npm publish "$package" --access public; done',
793
+ },
794
+ {
795
+ name: "Publish npm facade",
796
+ env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" },
797
+ run: 'for package in dist/npm-facade/*.tgz; do npm publish "$package" --access public; done',
798
+ },
799
+ ]
800
+ : []),
801
+ ...(binding.python
802
+ ? [
803
+ { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
804
+ {
805
+ name: "Download Python wheels",
806
+ uses: "actions/download-artifact@v8",
807
+ with: {
808
+ pattern: `${binding.crate}-*-python-wheel`,
809
+ path: "dist/python",
810
+ "merge-multiple": true,
811
+ },
812
+ },
813
+ {
814
+ name: "Publish Python wheels",
815
+ run: "uv publish --trusted-publishing always dist/python/*.whl",
816
+ },
817
+ ]
818
+ : []),
819
+ ],
656
820
  },
657
- ],
658
- };
821
+ ]),
822
+ );
659
823
  const workflowName = options.releaseWorkflowName ?? "rust-release";
660
824
  new YamlFile(project, `.github/workflows/${workflowName}.yml`, {
661
825
  obj: {
@@ -674,81 +838,32 @@ export class DBXToolsRustWorkspace {
674
838
  },
675
839
  permissions: { contents: "read" },
676
840
  jobs: {
677
- ...(matrix.length ? { build: buildJob } : {}),
678
- ...(binaryMatrix.length ? { "build-binaries": binaryBuildJob } : {}),
679
- ...(bindings.length
841
+ ...(hasTargetOutputs && targetMatrix.length ? { build: buildJob } : {}),
842
+ ...bindingPublishJobs,
843
+ ...(publicCrates.length
680
844
  ? {
681
- publish: {
845
+ "publish-cargo": {
682
846
  if: "${{ github.event_name == 'push' }}",
683
- needs: ["build"],
847
+ ...(hasTargetOutputs ? { needs: ["build"] } : {}),
684
848
  "runs-on": "ubuntu-latest",
685
- strategy: { matrix: { binding: bindings } },
686
- permissions: { contents: "read", "id-token": "write" },
687
- environment: { name: "pypi-${{ matrix.binding.crate }}" },
849
+ permissions: { contents: "read" },
688
850
  steps: [
689
851
  { name: "Checkout", uses: "actions/checkout@v6" },
852
+ { name: "Setup Rust", uses: "dtolnay/rust-toolchain@stable" },
690
853
  {
691
- name: "Setup Node.js",
692
- uses: "actions/setup-node@v6",
693
- with: { "registry-url": "https://registry.npmjs.org" },
694
- },
695
- {
696
- name: "Setup Bun",
697
- uses: "oven-sh/setup-bun@v2",
698
- with: { "bun-version": "1.3.14" },
699
- },
700
- { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
701
- { name: "Install", run: "bun install" },
702
- {
703
- name: "Download packages",
704
- uses: "actions/download-artifact@v8",
705
- with: {
706
- pattern: "${{ matrix.binding.crate }}-*",
707
- path: "dist/uniffi",
708
- "merge-multiple": true,
709
- },
710
- },
711
- {
712
- name: "Publish npm packages",
713
- env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" },
714
- run: 'for package in dist/uniffi/npm/*.tgz; do npm publish "$package" --access public; done',
715
- },
716
- {
717
- name: "Publish npm facades",
718
- if: "${{ matrix.binding.node != '' }}",
719
- env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" },
720
- run: 'for package in dist/uniffi/npm-facade/*.tgz; do npm publish "$package" --access public; done',
721
- },
722
- {
723
- name: "Publish Python wheels",
724
- if: "${{ matrix.binding.python != '' }}",
725
- run: "uv publish --trusted-publishing always dist/uniffi/python/*.whl",
854
+ name: "Publish public crates",
855
+ env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" },
856
+ run: publicCrates
857
+ .map(
858
+ (crate) =>
859
+ `cargo publish --package "${crate}" --registry crates-io --no-verify`,
860
+ )
861
+ .join("\n"),
726
862
  },
727
863
  ],
728
864
  },
729
865
  }
730
866
  : {}),
731
- "publish-cargo": {
732
- if: "${{ github.event_name == 'push' }}",
733
- needs: [matrix.length ? "build" : "build-binaries"],
734
- "runs-on": "ubuntu-latest",
735
- permissions: { contents: "read" },
736
- env: RUST_CACHE_ENV,
737
- steps: [
738
- { name: "Checkout", uses: "actions/checkout@v6" },
739
- { name: "Setup Rust", uses: "dtolnay/rust-toolchain@stable" },
740
- ...rustCacheSteps("cargo-publish"),
741
- {
742
- name: "Install Linux native dependencies",
743
- run: "sudo apt-get update && sudo apt-get install --yes libdbus-1-dev pkg-config",
744
- },
745
- {
746
- name: "Publish public crates",
747
- env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" },
748
- run: "cargo publish --workspace --registry crates-io",
749
- },
750
- ],
751
- },
752
867
  "publish-local": {
753
868
  if: "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}",
754
869
  "runs-on": ["self-hosted"],
@@ -780,11 +895,11 @@ export class DBXToolsRustWorkspace {
780
895
  },
781
896
  ],
782
897
  },
783
- ...(binaryMatrix.length
898
+ ...(releaseBinaries.length
784
899
  ? {
785
900
  "publish-github-release": {
786
901
  if: "${{ github.event_name == 'push' }}",
787
- needs: ["build-binaries"],
902
+ needs: ["build"],
788
903
  "runs-on": "ubuntu-latest",
789
904
  permissions: { contents: "write" },
790
905
  steps: [
@@ -792,7 +907,7 @@ export class DBXToolsRustWorkspace {
792
907
  name: "Download release binaries",
793
908
  uses: "actions/download-artifact@v8",
794
909
  with: {
795
- pattern: "release-*",
910
+ pattern: "*-binary",
796
911
  path: "dist/rust-release",
797
912
  "merge-multiple": true,
798
913
  },
package/tasks/bump.ts CHANGED
@@ -56,6 +56,7 @@ 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
58
  import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
59
+ import { releasePlatformFilter } from "../src/_release-platform.ts";
59
60
  import {
60
61
  type Semver,
61
62
  compareSemver,
@@ -234,9 +235,7 @@ program
234
235
  }) => {
235
236
  const pkgPath = resolve(process.cwd(), "package.json");
236
237
  if (!existsSync(pkgPath)) throw new Error(`no package.json in ${process.cwd()}`);
237
- if ((opts.os.length === 0) !== (opts.arch.length === 0)) {
238
- throw new Error("--os and --arch must be used together");
239
- }
238
+ const releasePlatforms = releasePlatformFilter(opts.os, opts.arch);
240
239
 
241
240
  const siblings = opts.sibling.map((s) => ({ ...s, pkgPath: resolve(s.dir, "package.json") }));
242
241
  for (const s of siblings) {
@@ -285,9 +284,6 @@ program
285
284
 
286
285
  if (opts.synth) {
287
286
  logger.info("synthesizing (projen)");
288
- const releasePlatforms = opts.os
289
- .flatMap((os) => opts.arch.map((arch) => `${os}:${arch}`))
290
- .join(",");
291
287
  exec.spawnSync("bun", [".projenrc.ts"], {
292
288
  cwd: process.cwd(),
293
289
  stdout: "inherit",
@@ -144,19 +144,19 @@ function buildAndPublish(binding: RustBindingMapping, version: string): void {
144
144
 
145
145
  const target = nativeTarget();
146
146
  const output = resolve(repoRoot, "dist/uniffi");
147
- run("bun", [
148
- resolve(dirname(fileURLToPath(import.meta.url)), "uniffi-release.ts"),
147
+ run("node", [
148
+ resolve(dirname(fileURLToPath(import.meta.url)), "uniffi-release.mjs"),
149
149
  "build",
150
150
  "--crate",
151
151
  binding.crate,
152
- "--rust",
153
- binding.rust,
154
152
  "--node",
155
153
  includeNode ? binding.node! : "",
156
154
  "--python",
157
155
  includePython ? binding.python! : "",
158
156
  "--node-package",
159
157
  includeNode ? binding.nodePackage! : "",
158
+ "--node-generator",
159
+ "projen/tasks/uniffi.ts",
160
160
  "--python-package",
161
161
  includePython ? binding.pythonPackage! : "",
162
162
  "--cargo-target",
@@ -175,6 +175,8 @@ function buildAndPublish(binding: RustBindingMapping, version: string): void {
175
175
  "true",
176
176
  "--version",
177
177
  version,
178
+ "--output",
179
+ "dist/uniffi",
178
180
  ]);
179
181
 
180
182
  if (includeNode) {
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ chmodSync,
4
+ cpSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ rmSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { basename, dirname, join, resolve } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+ import { fileURLToPath } from "node:url";
17
+ import { parseArgs } from "node:util";
18
+ import { spawnSync } from "node:child_process";
19
+
20
+ const parsed = parseArgs({
21
+ allowPositionals: true,
22
+ options: {
23
+ root: { type: "string" },
24
+ crate: { type: "string" },
25
+ node: { type: "string" },
26
+ python: { type: "string" },
27
+ "node-package": { type: "string" },
28
+ "node-generator": { type: "string" },
29
+ "python-package": { type: "string" },
30
+ "cargo-target": { type: "string" },
31
+ "node-triple": { type: "string" },
32
+ "python-tag": { type: "string" },
33
+ os: { type: "string" },
34
+ cpu: { type: "string" },
35
+ libc: { type: "string" },
36
+ facade: { type: "string" },
37
+ version: { type: "string" },
38
+ output: { type: "string" },
39
+ "skip-build": { type: "boolean" },
40
+ },
41
+ });
42
+
43
+ const required = (name) => {
44
+ const value = parsed.values[name];
45
+ if (!value) throw new Error(`Missing --${name}`);
46
+ return value;
47
+ };
48
+
49
+ const root = resolve(parsed.values.root ?? process.cwd());
50
+ const run = (command, args, cwd = root) => {
51
+ const result = spawnSync(command, args, { cwd, stdio: "inherit" });
52
+ if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`);
53
+ };
54
+
55
+ const replaceVersion = (source, version) =>
56
+ source.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
57
+
58
+ const writable = (path) => {
59
+ if (existsSync(path)) chmodSync(path, statSync(path).mode | 0o200);
60
+ };
61
+
62
+ const libraryPath = (crate, cargoTarget, os) => {
63
+ const name = crate.replaceAll("-", "_");
64
+ const extension = os === "darwin" ? "dylib" : os === "win32" ? "dll" : "so";
65
+ const prefix = os === "win32" ? "" : "lib";
66
+ return resolve(root, "target", cargoTarget, "release", `${prefix}${name}.${extension}`);
67
+ };
68
+
69
+ const packageNode = ({
70
+ crate,
71
+ library,
72
+ output,
73
+ nodeDirectory,
74
+ nodePackage,
75
+ nodeTriple,
76
+ os,
77
+ cpu,
78
+ version,
79
+ facade,
80
+ nodeGenerator,
81
+ }) => {
82
+ const libraryFile = basename(library);
83
+ const nativePackage = resolve(output, "native-node");
84
+ mkdirSync(nativePackage, { recursive: true });
85
+ cpSync(library, join(nativePackage, libraryFile));
86
+ writeFileSync(
87
+ join(nativePackage, "package.json"),
88
+ `${JSON.stringify(
89
+ {
90
+ name: `${nodePackage}-${nodeTriple}`,
91
+ version,
92
+ description: `Native ${nodeTriple} library for ${nodePackage}`,
93
+ license: "Apache-2.0",
94
+ os: [os],
95
+ cpu: [cpu],
96
+ ...(parsed.values.libc ? { libc: [parsed.values.libc] } : {}),
97
+ files: [libraryFile],
98
+ },
99
+ null,
100
+ 2,
101
+ )}\n`,
102
+ );
103
+ mkdirSync(resolve(output, "npm"), { recursive: true });
104
+ run("npm", ["pack", "--pack-destination", resolve(output, "npm")], nativePackage);
105
+
106
+ if (!facade) return;
107
+ const facadeDirectory = resolve(output, "facade-node");
108
+ cpSync(resolve(root, nodeDirectory), facadeDirectory, { recursive: true });
109
+ rmSync(join(facadeDirectory, "src", libraryFile), { force: true });
110
+ const manifestPath = join(facadeDirectory, "package.json");
111
+ writable(manifestPath);
112
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
113
+ manifest.version = version;
114
+ manifest.private = false;
115
+ manifest.license = manifest.license === "UNLICENSED" ? "Apache-2.0" : manifest.license;
116
+ manifest.optionalDependencies = Object.fromEntries(
117
+ Object.keys(manifest.optionalDependencies ?? {}).map((name) => [name, version]),
118
+ );
119
+ manifest.dependencies = Object.fromEntries(
120
+ Object.entries(manifest.dependencies ?? {}).map(([name, dependency]) => [
121
+ name,
122
+ typeof dependency === "string" && dependency.startsWith("workspace:") ? version : dependency,
123
+ ]),
124
+ );
125
+ delete manifest.scripts;
126
+ delete manifest.devDependencies;
127
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
128
+ run("bun", [
129
+ resolve(root, nodeGenerator),
130
+ "--root",
131
+ root,
132
+ "--crate",
133
+ crate,
134
+ "--node",
135
+ facadeDirectory,
136
+ "--cargo-target",
137
+ required("cargo-target"),
138
+ "--node-package-base",
139
+ `${nodePackage}-`,
140
+ "--skip-build",
141
+ ]);
142
+ mkdirSync(resolve(output, "npm-facade"), { recursive: true });
143
+ run("npm", ["pack", "--pack-destination", resolve(output, "npm-facade")], facadeDirectory);
144
+ };
145
+
146
+ const packagePython = ({
147
+ crate,
148
+ library,
149
+ output,
150
+ pythonDirectory,
151
+ pythonTag,
152
+ version,
153
+ cargoTarget,
154
+ os,
155
+ }) => {
156
+ const pythonRoot = resolve(output, "python-root");
157
+ cpSync(resolve(root, pythonDirectory), pythonRoot, { recursive: true });
158
+ const pyproject = join(pythonRoot, "pyproject.toml");
159
+ writable(pyproject);
160
+ writeFileSync(pyproject, replaceVersion(readFileSync(pyproject, "utf8"), version));
161
+
162
+ const packageName = crate.replace(/^dbx-tools-/, "").replaceAll("-", "_");
163
+ const packageDirectory = resolve(pythonRoot, "src", "dbx_tools", packageName);
164
+ const generatedDirectory = mkdtempSync(join(tmpdir(), `${packageName}-python-`));
165
+ const generator = resolve(
166
+ root,
167
+ "target",
168
+ cargoTarget,
169
+ "release",
170
+ `uniffi-bindgen${os === "win32" ? ".exe" : ""}`,
171
+ );
172
+ if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
173
+ run(generator, ["generate", "--language", "python", "--out-dir", generatedDirectory, library]);
174
+ const bindings = join(packageDirectory, "bindings.py");
175
+ writable(bindings);
176
+ const body = readFileSync(join(generatedDirectory, `${crate.replaceAll("-", "_")}.py`), "utf8");
177
+ writeFileSync(
178
+ bindings,
179
+ [
180
+ "# GENERATED by UniFFI binding generation - DO NOT EDIT.",
181
+ `# Regenerated from the ${crate} Rust exports.`,
182
+ "# Hand edits are overwritten on the next watch; this file is read-only.",
183
+ "",
184
+ body,
185
+ ].join("\n"),
186
+ );
187
+ cpSync(library, join(packageDirectory, basename(library)));
188
+ rmSync(generatedDirectory, { recursive: true, force: true });
189
+
190
+ const wheelDirectory = resolve(output, "python");
191
+ mkdirSync(wheelDirectory, { recursive: true });
192
+ run("uv", ["build", "--wheel", "--out-dir", wheelDirectory], pythonRoot);
193
+ const wheels = readdirSync(wheelDirectory).filter((file) => file.endsWith(".whl"));
194
+ if (wheels.length !== 1) throw new Error(`Expected one Python wheel, found ${wheels.length}`);
195
+ run("uvx", [
196
+ "--from",
197
+ "wheel",
198
+ "wheel",
199
+ "tags",
200
+ "--remove",
201
+ "--platform-tag",
202
+ pythonTag,
203
+ join(wheelDirectory, wheels[0]),
204
+ ]);
205
+ };
206
+
207
+ const build = () => {
208
+ const crate = required("crate");
209
+ const cargoTarget = required("cargo-target");
210
+ const nodeTriple = required("node-triple");
211
+ const pythonTag = required("python-tag");
212
+ const version = required("version");
213
+ const os = required("os");
214
+ const cpu = required("cpu");
215
+ const output = resolve(root, required("output"));
216
+ const library = libraryPath(crate, cargoTarget, os);
217
+
218
+ rmSync(output, { recursive: true, force: true });
219
+ mkdirSync(output, { recursive: true });
220
+ if (!parsed.values["skip-build"]) {
221
+ run("cargo", ["build", "--release", "--package", crate, "--target", cargoTarget]);
222
+ }
223
+ if (!existsSync(library)) throw new Error(`Missing native library ${library}`);
224
+
225
+ const nodeDirectory = parsed.values.node;
226
+ const nodePackage = parsed.values["node-package"];
227
+ if (nodeDirectory && nodePackage) {
228
+ packageNode({
229
+ crate,
230
+ library,
231
+ output,
232
+ nodeDirectory,
233
+ nodePackage,
234
+ nodeTriple,
235
+ os,
236
+ cpu,
237
+ version,
238
+ facade: parsed.values.facade === "true",
239
+ nodeGenerator: required("node-generator"),
240
+ });
241
+ }
242
+
243
+ const pythonDirectory = parsed.values.python;
244
+ if (pythonDirectory) {
245
+ packagePython({
246
+ crate,
247
+ library,
248
+ output,
249
+ pythonDirectory,
250
+ pythonTag,
251
+ version,
252
+ cargoTarget,
253
+ os,
254
+ });
255
+ }
256
+ };
257
+
258
+ if (parsed.positionals[0] !== "build") throw new Error("Expected build command");
259
+ build();
package/tasks/uniffi.ts CHANGED
@@ -23,18 +23,22 @@ import {
23
23
 
24
24
  const { values } = parseArgs({
25
25
  options: {
26
+ root: { type: "string" },
26
27
  crate: { type: "string" },
27
28
  node: { type: "string" },
28
29
  python: { type: "string" },
29
30
  "cargo-target": { type: "string" },
30
31
  "node-package-base": { type: "string" },
32
+ "skip-build": { type: "boolean" },
31
33
  },
32
34
  });
33
35
  if (!values.crate || (!values.node && !values.python)) {
34
36
  throw new Error("Expected --crate and at least one of --node or --python");
35
37
  }
36
38
 
37
- const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
39
+ const root = values.root
40
+ ? resolve(values.root)
41
+ : resolve(dirname(fileURLToPath(import.meta.url)), "../..");
38
42
  const crate = values.crate;
39
43
  const libraryName = crate.replaceAll("-", "_");
40
44
  const extension =
@@ -60,7 +64,8 @@ const run = (command: string, args: string[]) => {
60
64
 
61
65
  const replaceGenerated = (source: string, destination: string): void => {
62
66
  const deadline = Date.now() + 5_000;
63
- while (!existsSync(source) && Date.now() < deadline) Bun.sleepSync(25);
67
+ const signal = new Int32Array(new SharedArrayBuffer(4));
68
+ while (!existsSync(source) && Date.now() < deadline) Atomics.wait(signal, 0, 0, 25);
64
69
  if (!existsSync(source)) throw new Error(`Missing generated binding: ${source}`);
65
70
  mkdirSync(dirname(destination), { recursive: true });
66
71
  makeWritable(destination);
@@ -90,13 +95,15 @@ const stampGeneratedPython = (file: string): void => {
90
95
  makeReadonly(file);
91
96
  };
92
97
 
93
- run("cargo", [
94
- "build",
95
- "--release",
96
- "--package",
97
- crate,
98
- ...(values["cargo-target"] ? ["--target", values["cargo-target"]] : []),
99
- ]);
98
+ if (!values["skip-build"]) {
99
+ run("cargo", [
100
+ "build",
101
+ "--release",
102
+ "--package",
103
+ crate,
104
+ ...(values["cargo-target"] ? ["--target", values["cargo-target"]] : []),
105
+ ]);
106
+ }
100
107
  if (!existsSync(library)) throw new Error(`Missing compiled UniFFI library: ${library}`);
101
108
 
102
109
  if (values.node) {
@@ -1,168 +0,0 @@
1
- #!/usr/bin/env -S bun
2
- import {
3
- chmodSync,
4
- cpSync,
5
- existsSync,
6
- mkdirSync,
7
- readFileSync,
8
- rmSync,
9
- statSync,
10
- writeFileSync,
11
- } from "node:fs";
12
- import { dirname, join, resolve } from "node:path";
13
- import { fileURLToPath } from "node:url";
14
- import { parseArgs } from "node:util";
15
- import { spawnSync } from "node:child_process";
16
-
17
- const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
18
- const parsed = parseArgs({
19
- allowPositionals: true,
20
- options: {
21
- crate: { type: "string" },
22
- rust: { type: "string" },
23
- node: { type: "string" },
24
- python: { type: "string" },
25
- "node-package": { type: "string" },
26
- "python-package": { type: "string" },
27
- "cargo-target": { type: "string" },
28
- "node-triple": { type: "string" },
29
- "python-tag": { type: "string" },
30
- os: { type: "string" },
31
- cpu: { type: "string" },
32
- libc: { type: "string" },
33
- facade: { type: "string" },
34
- version: { type: "string" },
35
- },
36
- });
37
-
38
- const required = (name: keyof typeof parsed.values): string => {
39
- const value = parsed.values[name];
40
- if (!value) throw new Error(`Missing --${name}`);
41
- return value;
42
- };
43
-
44
- const run = (command: string, args: string[], cwd = root): void => {
45
- const result = spawnSync(command, args, { cwd, stdio: "inherit" });
46
- if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`);
47
- };
48
-
49
- const replaceVersion = (source: string, version: string): string =>
50
- source.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
51
-
52
- function build(): void {
53
- const crate = required("crate");
54
- required("rust");
55
- const cargoTarget = required("cargo-target");
56
- const nodeTriple = required("node-triple");
57
- const pythonTag = required("python-tag");
58
- const version = required("version");
59
- const os = required("os");
60
- const cpu = required("cpu");
61
- const nodeDirectory = parsed.values.node;
62
- const pythonDirectory = parsed.values.python;
63
- const nodePackage = parsed.values["node-package"];
64
- const pythonPackage = parsed.values["python-package"];
65
- const libraryName = crate.replaceAll("-", "_");
66
- const extension = os === "darwin" ? "dylib" : os === "win32" ? "dll" : "so";
67
- const prefix = os === "win32" ? "" : "lib";
68
- const libraryFile = `${prefix}${libraryName}.${extension}`;
69
- const library = resolve(root, "target", cargoTarget, "release", libraryFile);
70
- const output = resolve(root, "dist/uniffi");
71
-
72
- rmSync(output, { recursive: true, force: true });
73
- mkdirSync(join(output, "npm"), { recursive: true });
74
- mkdirSync(join(output, "npm-facade"), { recursive: true });
75
- mkdirSync(join(output, "python"), { recursive: true });
76
- run("cargo", ["build", "--release", "--package", crate, "--target", cargoTarget]);
77
- if (!existsSync(library)) throw new Error(`Missing native library ${library}`);
78
-
79
- if (nodeDirectory && nodePackage) {
80
- const nativePackage = resolve(output, "native-node");
81
- mkdirSync(nativePackage, { recursive: true });
82
- cpSync(library, join(nativePackage, libraryFile));
83
- writeFileSync(
84
- join(nativePackage, "package.json"),
85
- `${JSON.stringify(
86
- {
87
- name: `${nodePackage}-${nodeTriple}`,
88
- version,
89
- description: `Native ${nodeTriple} library for ${nodePackage}`,
90
- license: "Apache-2.0",
91
- os: [os],
92
- cpu: [cpu],
93
- ...(parsed.values.libc ? { libc: [parsed.values.libc] } : {}),
94
- files: [libraryFile],
95
- },
96
- null,
97
- 2,
98
- )}\n`,
99
- );
100
- run("npm", ["pack", "--pack-destination", resolve(output, "npm")], nativePackage);
101
-
102
- if (parsed.values.facade === "true") {
103
- const facade = resolve(output, "facade-node");
104
- cpSync(resolve(root, nodeDirectory), facade, { recursive: true });
105
- rmSync(join(facade, "src", libraryFile), { force: true });
106
- const manifestPath = join(facade, "package.json");
107
- const manifestMode = statSync(manifestPath).mode;
108
- chmodSync(manifestPath, manifestMode | 0o200);
109
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
110
- manifest.version = version;
111
- manifest.private = false;
112
- manifest.license = manifest.license === "UNLICENSED" ? "Apache-2.0" : manifest.license;
113
- manifest.optionalDependencies = Object.fromEntries(
114
- Object.keys(manifest.optionalDependencies ?? {}).map((name) => [name, version]),
115
- );
116
- manifest.dependencies = Object.fromEntries(
117
- Object.entries(manifest.dependencies ?? {}).map(([name, dependency]) => [
118
- name,
119
- typeof dependency === "string" && dependency.startsWith("workspace:")
120
- ? version
121
- : dependency,
122
- ]),
123
- );
124
- delete manifest.scripts;
125
- delete manifest.devDependencies;
126
- writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
127
- const generator = resolve(root, "node_modules/@dbx-tools/projen/tasks/uniffi.ts");
128
- run("bun", [
129
- generator,
130
- "--crate",
131
- crate,
132
- "--node",
133
- facade,
134
- "--cargo-target",
135
- cargoTarget,
136
- "--node-package-base",
137
- `${nodePackage}-`,
138
- ]);
139
- run("npm", ["pack", "--pack-destination", resolve(output, "npm-facade")], facade);
140
- }
141
- }
142
-
143
- if (pythonDirectory && pythonPackage) {
144
- const pythonRoot = resolve(output, "python-root");
145
- cpSync(resolve(root, pythonDirectory), pythonRoot, { recursive: true });
146
- const pyproject = join(pythonRoot, "pyproject.toml");
147
- const mode = statSync(pyproject).mode;
148
- chmodSync(pyproject, mode | 0o200);
149
- writeFileSync(pyproject, replaceVersion(readFileSync(pyproject, "utf8"), version));
150
- const generator = resolve(root, "node_modules/@dbx-tools/projen/tasks/uniffi.ts");
151
- run("bun", [
152
- generator,
153
- "--crate",
154
- crate,
155
- "--python",
156
- pythonRoot,
157
- "--cargo-target",
158
- cargoTarget,
159
- ]);
160
- run("uv", ["build", "--wheel", "--out-dir", resolve(output, "python")], pythonRoot);
161
- const wheels = [...new Bun.Glob("*.whl").scanSync(join(output, "python"))];
162
- if (wheels.length !== 1) throw new Error(`Expected one Python wheel, found ${wheels.length}`);
163
- run("uvx", ["--from", "wheel", "wheel", "tags", "--remove", "--platform-tag", pythonTag, join(output, "python", wheels[0]!)]);
164
- }
165
- }
166
-
167
- if (parsed.positionals[0] !== "build") throw new Error("Expected build command");
168
- build();