@dbx-tools/projen 0.6.151 → 0.6.152

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/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.152",
30
+ "@dbx-tools/path": "0.6.152",
31
+ "@dbx-tools/shared-core": "0.6.152",
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.152",
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-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;
@@ -172,6 +172,17 @@ function releaseTargets(options: DBXToolsRustWorkspaceOptions): readonly UniFFIR
172
172
  });
173
173
  }
174
174
 
175
+ function uniffiReleaseTaskSource(): string {
176
+ const sourceDirectory = dirname(fileURLToPath(import.meta.url));
177
+ const candidates = [
178
+ resolve(sourceDirectory, "../tasks/uniffi-release.mjs"),
179
+ resolve(sourceDirectory, "../../tasks/uniffi-release.mjs"),
180
+ ];
181
+ const source = candidates.find(existsSync);
182
+ if (!source) throw new Error("Could not locate tasks/uniffi-release.mjs");
183
+ return readFileSync(source, "utf8");
184
+ }
185
+
175
186
  /** Persisted mapping consumed by the focused Rust source watcher. */
176
187
  export interface RustBindingMapping {
177
188
  readonly crate: string;
@@ -525,7 +536,10 @@ export class DBXToolsRustWorkspace {
525
536
  });
526
537
  if (
527
538
  (options.release ?? true) &&
528
- (this.bindingMappings.length > 0 || this.packages.some((pkg) => pkg.packageOptions.release))
539
+ (this.bindingMappings.length > 0 ||
540
+ this.packages.some(
541
+ (pkg) => pkg.packageOptions.release || !pkg.packageOptions.private,
542
+ ))
529
543
  ) {
530
544
  this.addReleaseWorkflow(project, options, releaseTargets(options));
531
545
  }
@@ -544,37 +558,131 @@ export class DBXToolsRustWorkspace {
544
558
  nodePackage: binding.nodePackage ?? "",
545
559
  pythonPackage: binding.pythonPackage ?? "",
546
560
  }));
547
- const matrix = bindings.flatMap((binding) =>
548
- targets.map((target, index) => ({
549
- binding,
550
- target: { ...target, facade: index === 0 },
551
- })),
552
- );
553
561
  const releaseBinaries = this.packages
554
562
  .filter((pkg) => pkg.packageOptions.release)
555
563
  .map((pkg) => ({
556
564
  crate: pkg.crateName,
557
565
  binary: pkg.packageOptions.binaryName ?? pkg.crateName,
558
566
  }));
559
- const binaryMatrix = releaseBinaries.flatMap((pkg) =>
560
- targets.map((target) => ({ package: pkg, target })),
567
+ const publicCrates = this.packages
568
+ .filter((pkg) => !pkg.packageOptions.private)
569
+ .map((pkg) => pkg.crateName);
570
+ const targetMatrix = targets.map((target, index) => ({
571
+ target: { ...target, facade: index === 0 },
572
+ }));
573
+ const hasNodeBindings = bindings.some((binding) => binding.node);
574
+ const hasPythonBindings = bindings.some((binding) => binding.python);
575
+ const hasTargetOutputs = bindings.length > 0 || releaseBinaries.length > 0;
576
+ const releaseTask = ".projen/uniffi-release.mjs";
577
+ if (bindings.length) {
578
+ new TextFile(project, releaseTask, {
579
+ lines: uniffiReleaseTaskSource().trimEnd().split("\n"),
580
+ });
581
+ } else {
582
+ project.tryRemoveFile(releaseTask);
583
+ }
584
+ const bindingCommands = bindings.map((binding) =>
585
+ [
586
+ `node ${releaseTask} build`,
587
+ `--crate "${binding.crate}"`,
588
+ `--node "${binding.node}"`,
589
+ `--python "${binding.python}"`,
590
+ `--node-package "${binding.nodePackage}"`,
591
+ `--python-package "${binding.pythonPackage}"`,
592
+ ...(binding.node
593
+ ? ['--node-generator "node_modules/@dbx-tools/projen/tasks/uniffi.ts"']
594
+ : []),
595
+ '--cargo-target "${{ matrix.target.cargo }}"',
596
+ '--node-triple "${{ matrix.target.node }}"',
597
+ '--python-tag "${{ matrix.target.python }}"',
598
+ '--os "${{ matrix.target.os }}"',
599
+ '--cpu "${{ matrix.target.cpu }}"',
600
+ '--libc "${{ matrix.target.libc }}"',
601
+ '--facade "${{ matrix.target.facade }}"',
602
+ '--version "${VERSION#v}"',
603
+ `--output "dist/release/${binding.crate}/\${{ matrix.target.node }}"`,
604
+ "--skip-build",
605
+ ].join(" \\\n "),
561
606
  );
607
+ const binaryCommands = releaseBinaries.flatMap((pkg) => [
608
+ `mkdir -p "dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/stage"`,
609
+ `SOURCE="target/\${{ matrix.target.cargo }}/release/${pkg.binary}\${{ matrix.target.os == 'win32' && '.exe' || '' }}"`,
610
+ `DESTINATION="dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/stage/${pkg.binary}\${{ matrix.target.os == 'win32' && '.exe' || '' }}"`,
611
+ 'cp "$SOURCE" "$DESTINATION"',
612
+ 'if [ "${{ matrix.target.os }}" = "win32" ]; then',
613
+ ` 7z a "dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/${pkg.binary}-\${{ matrix.target.node }}.zip" "$DESTINATION"`,
614
+ "else",
615
+ ` 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}"`,
616
+ "fi",
617
+ `rm -rf "dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/stage"`,
618
+ ]);
619
+ const artifactSteps = [
620
+ ...bindings.flatMap((binding) => [
621
+ ...(binding.node
622
+ ? [
623
+ {
624
+ name: `Upload ${binding.crate} native npm package`,
625
+ uses: "actions/upload-artifact@v7",
626
+ with: {
627
+ name: `${binding.crate}-\${{ matrix.target.node }}-npm`,
628
+ path: `dist/release/${binding.crate}/\${{ matrix.target.node }}/npm/*.tgz`,
629
+ },
630
+ },
631
+ {
632
+ name: `Upload ${binding.crate} npm facade`,
633
+ if: "${{ matrix.target.facade }}",
634
+ uses: "actions/upload-artifact@v7",
635
+ with: {
636
+ name: `${binding.crate}-npm-facade`,
637
+ path: `dist/release/${binding.crate}/\${{ matrix.target.node }}/npm-facade/*.tgz`,
638
+ },
639
+ },
640
+ ]
641
+ : []),
642
+ ...(binding.python
643
+ ? [
644
+ {
645
+ name: `Upload ${binding.crate} Python wheel`,
646
+ uses: "actions/upload-artifact@v7",
647
+ with: {
648
+ name: `${binding.crate}-\${{ matrix.target.python }}-python-wheel`,
649
+ path: `dist/release/${binding.crate}/\${{ matrix.target.node }}/python/*.whl`,
650
+ },
651
+ },
652
+ ]
653
+ : []),
654
+ ]),
655
+ ...releaseBinaries.map((pkg) => ({
656
+ name: `Upload ${pkg.crate} release binary`,
657
+ uses: "actions/upload-artifact@v7",
658
+ with: {
659
+ name: `${pkg.crate}-\${{ matrix.target.node }}-binary`,
660
+ path: `dist/release/${pkg.crate}/\${{ matrix.target.node }}/binary/*`,
661
+ },
662
+ })),
663
+ ];
562
664
  const buildJob = {
563
- name: "${{ matrix.binding.crate }} / ${{ matrix.target.node }}",
665
+ name: "${{ matrix.target.node }}",
564
666
  "runs-on": "${{ matrix.target.runner }}",
565
667
  env: RUST_CACHE_ENV,
566
668
  strategy: {
567
669
  "fail-fast": false,
568
- matrix: { include: matrix },
670
+ matrix: { include: targetMatrix },
569
671
  },
570
672
  steps: [
571
673
  { 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" },
674
+ ...(hasNodeBindings
675
+ ? [
676
+ {
677
+ name: "Setup Bun",
678
+ uses: "oven-sh/setup-bun@v2",
679
+ with: { "bun-version": "1.3.14" },
680
+ },
681
+ ]
682
+ : []),
683
+ ...(hasPythonBindings
684
+ ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }]
685
+ : []),
578
686
  {
579
687
  name: "Setup Rust",
580
688
  uses: "dtolnay/rust-toolchain@stable",
@@ -586,76 +694,108 @@ export class DBXToolsRustWorkspace {
586
694
  if: "${{ matrix.target.os == 'linux' }}",
587
695
  run: "sudo apt-get update && sudo apt-get install --yes libdbus-1-dev pkg-config",
588
696
  },
589
- { name: "Install", run: "bun install" },
697
+ ...(hasNodeBindings ? [{ name: "Install Node tooling", run: "bun install" }] : []),
590
698
  {
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
- },
699
+ name: "Build Rust outputs",
700
+ run: 'cargo build --release --workspace --target "${{ matrix.target.cargo }}"',
609
701
  },
702
+ ...(bindingCommands.length
703
+ ? [
704
+ {
705
+ name: "Package UniFFI outputs",
706
+ shell: "bash",
707
+ env: {
708
+ VERSION: "${{ github.event_name == 'push' && github.ref_name || inputs.version }}",
709
+ },
710
+ run: bindingCommands.join("\n"),
711
+ },
712
+ ]
713
+ : []),
714
+ ...(binaryCommands.length
715
+ ? [
716
+ {
717
+ name: "Package release binaries",
718
+ shell: "bash",
719
+ run: binaryCommands.join("\n"),
720
+ },
721
+ ]
722
+ : []),
723
+ ...artifactSteps,
610
724
  ],
611
725
  };
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" },
619
- {
620
- name: "Setup Rust",
621
- uses: "dtolnay/rust-toolchain@stable",
622
- with: { targets: "${{ matrix.target.cargo }}" },
623
- },
624
- ...rustCacheSteps("release-${{ matrix.target.cargo }}"),
726
+ const bindingPublishJobs = Object.fromEntries(
727
+ bindings.map((binding) => [
728
+ `publish-${binding.crate}`,
625
729
  {
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/*",
730
+ if: "${{ github.event_name == 'push' }}",
731
+ needs: ["build"],
732
+ "runs-on": "ubuntu-latest",
733
+ permissions: {
734
+ contents: "read",
735
+ ...(binding.python ? { "id-token": "write" } : {}),
655
736
  },
737
+ ...(binding.python
738
+ ? { environment: { name: `pypi-${binding.crate}` } }
739
+ : {}),
740
+ steps: [
741
+ ...(binding.node
742
+ ? [
743
+ {
744
+ name: "Setup Node.js",
745
+ uses: "actions/setup-node@v6",
746
+ with: { "registry-url": "https://registry.npmjs.org" },
747
+ },
748
+ {
749
+ name: "Download native npm packages",
750
+ uses: "actions/download-artifact@v8",
751
+ with: {
752
+ pattern: `${binding.crate}-*-npm`,
753
+ path: "dist/npm",
754
+ "merge-multiple": true,
755
+ },
756
+ },
757
+ {
758
+ name: "Download npm facade",
759
+ uses: "actions/download-artifact@v8",
760
+ with: {
761
+ name: `${binding.crate}-npm-facade`,
762
+ path: "dist/npm-facade",
763
+ },
764
+ },
765
+ {
766
+ name: "Publish native npm packages",
767
+ env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" },
768
+ run: 'for package in dist/npm/*.tgz; do npm publish "$package" --access public; done',
769
+ },
770
+ {
771
+ name: "Publish npm facade",
772
+ env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" },
773
+ run: 'for package in dist/npm-facade/*.tgz; do npm publish "$package" --access public; done',
774
+ },
775
+ ]
776
+ : []),
777
+ ...(binding.python
778
+ ? [
779
+ { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
780
+ {
781
+ name: "Download Python wheels",
782
+ uses: "actions/download-artifact@v8",
783
+ with: {
784
+ pattern: `${binding.crate}-*-python-wheel`,
785
+ path: "dist/python",
786
+ "merge-multiple": true,
787
+ },
788
+ },
789
+ {
790
+ name: "Publish Python wheels",
791
+ run: "uv publish --trusted-publishing always dist/python/*.whl",
792
+ },
793
+ ]
794
+ : []),
795
+ ],
656
796
  },
657
- ],
658
- };
797
+ ]),
798
+ );
659
799
  const workflowName = options.releaseWorkflowName ?? "rust-release";
660
800
  new YamlFile(project, `.github/workflows/${workflowName}.yml`, {
661
801
  obj: {
@@ -674,81 +814,32 @@ export class DBXToolsRustWorkspace {
674
814
  },
675
815
  permissions: { contents: "read" },
676
816
  jobs: {
677
- ...(matrix.length ? { build: buildJob } : {}),
678
- ...(binaryMatrix.length ? { "build-binaries": binaryBuildJob } : {}),
679
- ...(bindings.length
817
+ ...(hasTargetOutputs && targetMatrix.length ? { build: buildJob } : {}),
818
+ ...bindingPublishJobs,
819
+ ...(publicCrates.length
680
820
  ? {
681
- publish: {
821
+ "publish-cargo": {
682
822
  if: "${{ github.event_name == 'push' }}",
683
- needs: ["build"],
823
+ ...(hasTargetOutputs ? { needs: ["build"] } : {}),
684
824
  "runs-on": "ubuntu-latest",
685
- strategy: { matrix: { binding: bindings } },
686
- permissions: { contents: "read", "id-token": "write" },
687
- environment: { name: "pypi-${{ matrix.binding.crate }}" },
825
+ permissions: { contents: "read" },
688
826
  steps: [
689
827
  { name: "Checkout", uses: "actions/checkout@v6" },
828
+ { name: "Setup Rust", uses: "dtolnay/rust-toolchain@stable" },
690
829
  {
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",
830
+ name: "Publish public crates",
831
+ env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" },
832
+ run: publicCrates
833
+ .map(
834
+ (crate) =>
835
+ `cargo publish --package "${crate}" --registry crates-io --no-verify`,
836
+ )
837
+ .join("\n"),
726
838
  },
727
839
  ],
728
840
  },
729
841
  }
730
842
  : {}),
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
843
  "publish-local": {
753
844
  if: "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}",
754
845
  "runs-on": ["self-hosted"],
@@ -780,11 +871,11 @@ export class DBXToolsRustWorkspace {
780
871
  },
781
872
  ],
782
873
  },
783
- ...(binaryMatrix.length
874
+ ...(releaseBinaries.length
784
875
  ? {
785
876
  "publish-github-release": {
786
877
  if: "${{ github.event_name == 'push' }}",
787
- needs: ["build-binaries"],
878
+ needs: ["build"],
788
879
  "runs-on": "ubuntu-latest",
789
880
  permissions: { contents: "write" },
790
881
  steps: [
@@ -792,7 +883,7 @@ export class DBXToolsRustWorkspace {
792
883
  name: "Download release binaries",
793
884
  uses: "actions/download-artifact@v8",
794
885
  with: {
795
- pattern: "release-*",
886
+ pattern: "*-binary",
796
887
  path: "dist/rust-release",
797
888
  "merge-multiple": true,
798
889
  },
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,267 @@
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:")
123
+ ? version
124
+ : dependency,
125
+ ]),
126
+ );
127
+ delete manifest.scripts;
128
+ delete manifest.devDependencies;
129
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
130
+ run("bun", [
131
+ resolve(root, nodeGenerator),
132
+ "--root",
133
+ root,
134
+ "--crate",
135
+ crate,
136
+ "--node",
137
+ facadeDirectory,
138
+ "--cargo-target",
139
+ required("cargo-target"),
140
+ "--node-package-base",
141
+ `${nodePackage}-`,
142
+ "--skip-build",
143
+ ]);
144
+ mkdirSync(resolve(output, "npm-facade"), { recursive: true });
145
+ run(
146
+ "npm",
147
+ ["pack", "--pack-destination", resolve(output, "npm-facade")],
148
+ facadeDirectory,
149
+ );
150
+ };
151
+
152
+ const packagePython = ({
153
+ crate,
154
+ library,
155
+ output,
156
+ pythonDirectory,
157
+ pythonTag,
158
+ version,
159
+ }) => {
160
+ const pythonRoot = resolve(output, "python-root");
161
+ cpSync(resolve(root, pythonDirectory), pythonRoot, { recursive: true });
162
+ const pyproject = join(pythonRoot, "pyproject.toml");
163
+ writable(pyproject);
164
+ writeFileSync(pyproject, replaceVersion(readFileSync(pyproject, "utf8"), version));
165
+
166
+ const packageName = crate.replace(/^dbx-tools-/, "").replaceAll("-", "_");
167
+ const packageDirectory = resolve(pythonRoot, "src", "dbx_tools", packageName);
168
+ const generatedDirectory = mkdtempSync(join(tmpdir(), `${packageName}-python-`));
169
+ run("cargo", [
170
+ "run",
171
+ "--release",
172
+ "--package",
173
+ crate,
174
+ "--bin",
175
+ "uniffi-bindgen",
176
+ "--",
177
+ "generate",
178
+ "--language",
179
+ "python",
180
+ "--out-dir",
181
+ generatedDirectory,
182
+ library,
183
+ ]);
184
+ const bindings = join(packageDirectory, "bindings.py");
185
+ writable(bindings);
186
+ const body = readFileSync(join(generatedDirectory, `${crate.replaceAll("-", "_")}.py`), "utf8");
187
+ writeFileSync(
188
+ bindings,
189
+ [
190
+ "# GENERATED by UniFFI binding generation - DO NOT EDIT.",
191
+ `# Regenerated from the ${crate} Rust exports.`,
192
+ "# Hand edits are overwritten on the next watch; this file is read-only.",
193
+ "",
194
+ body,
195
+ ].join("\n"),
196
+ );
197
+ cpSync(library, join(packageDirectory, basename(library)));
198
+ rmSync(generatedDirectory, { recursive: true, force: true });
199
+
200
+ const wheelDirectory = resolve(output, "python");
201
+ mkdirSync(wheelDirectory, { recursive: true });
202
+ run("uv", ["build", "--wheel", "--out-dir", wheelDirectory], pythonRoot);
203
+ const wheels = readdirSync(wheelDirectory).filter((file) => file.endsWith(".whl"));
204
+ if (wheels.length !== 1) throw new Error(`Expected one Python wheel, found ${wheels.length}`);
205
+ run("uvx", [
206
+ "--from",
207
+ "wheel",
208
+ "wheel",
209
+ "tags",
210
+ "--remove",
211
+ "--platform-tag",
212
+ pythonTag,
213
+ join(wheelDirectory, wheels[0]),
214
+ ]);
215
+ };
216
+
217
+ const build = () => {
218
+ const crate = required("crate");
219
+ const cargoTarget = required("cargo-target");
220
+ const nodeTriple = required("node-triple");
221
+ const pythonTag = required("python-tag");
222
+ const version = required("version");
223
+ const os = required("os");
224
+ const cpu = required("cpu");
225
+ const output = resolve(root, required("output"));
226
+ const library = libraryPath(crate, cargoTarget, os);
227
+
228
+ rmSync(output, { recursive: true, force: true });
229
+ mkdirSync(output, { recursive: true });
230
+ if (!parsed.values["skip-build"]) {
231
+ run("cargo", ["build", "--release", "--package", crate, "--target", cargoTarget]);
232
+ }
233
+ if (!existsSync(library)) throw new Error(`Missing native library ${library}`);
234
+
235
+ const nodeDirectory = parsed.values.node;
236
+ const nodePackage = parsed.values["node-package"];
237
+ if (nodeDirectory && nodePackage) {
238
+ packageNode({
239
+ crate,
240
+ library,
241
+ output,
242
+ nodeDirectory,
243
+ nodePackage,
244
+ nodeTriple,
245
+ os,
246
+ cpu,
247
+ version,
248
+ facade: parsed.values.facade === "true",
249
+ nodeGenerator: required("node-generator"),
250
+ });
251
+ }
252
+
253
+ const pythonDirectory = parsed.values.python;
254
+ if (pythonDirectory) {
255
+ packagePython({
256
+ crate,
257
+ library,
258
+ output,
259
+ pythonDirectory,
260
+ pythonTag,
261
+ version,
262
+ });
263
+ }
264
+ };
265
+
266
+ if (parsed.positionals[0] !== "build") throw new Error("Expected build command");
267
+ 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 =
@@ -90,13 +94,15 @@ const stampGeneratedPython = (file: string): void => {
90
94
  makeReadonly(file);
91
95
  };
92
96
 
93
- run("cargo", [
94
- "build",
95
- "--release",
96
- "--package",
97
- crate,
98
- ...(values["cargo-target"] ? ["--target", values["cargo-target"]] : []),
99
- ]);
97
+ if (!values["skip-build"]) {
98
+ run("cargo", [
99
+ "build",
100
+ "--release",
101
+ "--package",
102
+ crate,
103
+ ...(values["cargo-target"] ? ["--target", values["cargo-target"]] : []),
104
+ ]);
105
+ }
100
106
  if (!existsSync(library)) throw new Error(`Missing compiled UniFFI library: ${library}`);
101
107
 
102
108
  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();