@dbx-tools/projen 0.6.210 → 0.6.212
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/index.ts +2 -2
- package/package.json +4 -4
- package/src/_release-platform.ts +55 -1
- package/src/project-js.ts +62 -1
- package/src/project-py.ts +5 -2
- package/src/project-rs.ts +923 -671
- package/src/release.ts +11 -1
- package/src/tags.ts +7 -2
- package/tasks/bump.ts +12 -29
- package/tasks/release-pr.ts +12 -29
package/src/project-rs.ts
CHANGED
|
@@ -5,9 +5,14 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { exec, project as coreProject } from "@dbx-tools/core";
|
|
6
6
|
import { string } from "@dbx-tools/shared-core";
|
|
7
7
|
import { Component, Project, TextFile, javascript } from "projen";
|
|
8
|
-
import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
|
|
8
|
+
import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
|
|
9
9
|
import { BUN_VERSION } from "./bun-workflow.ts";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
DBX_TOOLS_LICENSE,
|
|
12
|
+
type DBXToolsJavaScriptProject,
|
|
13
|
+
DBXToolsTypeScriptProject,
|
|
14
|
+
projectRepositoryUrl,
|
|
15
|
+
} from "./project-js.ts";
|
|
11
16
|
import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
|
|
12
17
|
import { pythonModuleName, type PythonPackageOptions } from "./project-py.ts";
|
|
13
18
|
import type { DBXToolsProject } from "./project.ts";
|
|
@@ -180,7 +185,7 @@ const RUST_CACHE_ENV = {
|
|
|
180
185
|
CARGO_TERM_COLOR: "always",
|
|
181
186
|
} as const;
|
|
182
187
|
|
|
183
|
-
function rustCacheSteps(sharedKey: string): readonly
|
|
188
|
+
function rustCacheSteps(sharedKey: string): readonly JobStep[] {
|
|
184
189
|
return [
|
|
185
190
|
{
|
|
186
191
|
name: "Cache Cargo registry and dependencies",
|
|
@@ -531,674 +536,960 @@ function rustPackageDependencies(
|
|
|
531
536
|
);
|
|
532
537
|
}
|
|
533
538
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
readonly
|
|
537
|
-
readonly
|
|
538
|
-
readonly
|
|
539
|
-
readonly
|
|
540
|
-
readonly
|
|
541
|
-
readonly
|
|
539
|
+
interface ResolvedRustWorkspaceOptions {
|
|
540
|
+
readonly root: string;
|
|
541
|
+
readonly nodeRoot: string;
|
|
542
|
+
readonly pythonRoot: string;
|
|
543
|
+
readonly scope: string;
|
|
544
|
+
readonly repository: string;
|
|
545
|
+
readonly nativeTargets: readonly UniFFIReleaseTarget[];
|
|
546
|
+
readonly pythonModulePrefix: string;
|
|
547
|
+
readonly packageOptions: Readonly<Record<string, Omit<RustPackageOptions, "directory">>>;
|
|
548
|
+
readonly release: boolean;
|
|
549
|
+
}
|
|
542
550
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
551
|
+
function resolveRustWorkspaceOptions(
|
|
552
|
+
project: javascript.NodeProject,
|
|
553
|
+
options: DBXToolsRustWorkspaceOptions,
|
|
554
|
+
): ResolvedRustWorkspaceOptions {
|
|
555
|
+
const dbxToolsProject = isDBXToolsJavaScriptProject()(project) ? project : undefined;
|
|
556
|
+
const scope = string.toSlug(options.scope ?? dbxToolsProject?.scope ?? project.name);
|
|
557
|
+
return {
|
|
558
|
+
root: options.root ?? "packages/rs",
|
|
559
|
+
nodeRoot: options.nodeRoot ?? "packages/js/node",
|
|
560
|
+
pythonRoot: options.pythonRoot ?? "packages/py",
|
|
561
|
+
scope,
|
|
562
|
+
repository:
|
|
549
563
|
options.repository ??
|
|
550
564
|
projectRepositoryUrl(project) ??
|
|
551
565
|
coreProject.repositoryUrl(project.outdir) ??
|
|
552
|
-
""
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
directory,
|
|
560
|
-
...packageOptions[directory],
|
|
561
|
-
}),
|
|
562
|
-
);
|
|
563
|
-
const commands = new Set<string>();
|
|
564
|
-
this.releaseBinaries = this.packages
|
|
565
|
-
.filter((pkg) => Boolean(pkg.packageOptions.cli))
|
|
566
|
-
.map((pkg) => {
|
|
567
|
-
if (!pkg.packageOptions.release) {
|
|
568
|
-
throw new Error(`${pkg.crateName} must set release when cli is configured`);
|
|
569
|
-
}
|
|
570
|
-
const configured = pkg.packageOptions.cli;
|
|
571
|
-
const command =
|
|
572
|
-
typeof configured === "object" && configured.command
|
|
573
|
-
? configured.command
|
|
574
|
-
: pkg.packageOptions.directory;
|
|
575
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(command)) {
|
|
576
|
-
throw new Error(`Invalid Rust CLI command: ${command}`);
|
|
577
|
-
}
|
|
578
|
-
if (commands.has(command)) {
|
|
579
|
-
throw new Error(`Duplicate Rust CLI command: ${command}`);
|
|
580
|
-
}
|
|
581
|
-
commands.add(command);
|
|
582
|
-
const description =
|
|
583
|
-
(typeof configured === "object" ? configured.description : undefined) ??
|
|
584
|
-
pkg.packageOptions.description ??
|
|
585
|
-
pkg.crateName;
|
|
586
|
-
const excludedOs = new Set(pkg.packageOptions.releaseExcludeOs ?? []);
|
|
587
|
-
const binaryName = pkg.packageOptions.binaryName ?? pkg.crateName;
|
|
588
|
-
return {
|
|
589
|
-
command,
|
|
590
|
-
description,
|
|
591
|
-
binaryName,
|
|
592
|
-
repository,
|
|
593
|
-
assets: nativeTargets
|
|
594
|
-
.filter((target) => !excludedOs.has(target.os))
|
|
595
|
-
.map((target) => ({
|
|
596
|
-
os: target.os,
|
|
597
|
-
cpu: target.cpu,
|
|
598
|
-
name: releaseBinaryAssetName(binaryName, target.node, target.os),
|
|
599
|
-
})),
|
|
600
|
-
};
|
|
601
|
-
});
|
|
602
|
-
if (this.releaseBinaries.length && !repository) {
|
|
603
|
-
throw new Error("A repository URL is required when Rust CLI commands are configured");
|
|
604
|
-
}
|
|
605
|
-
if (this.releaseBinaries.length && !options.cliRegistryPath) {
|
|
606
|
-
throw new Error("cliRegistryPath is required when Rust CLI commands are configured");
|
|
607
|
-
}
|
|
608
|
-
if (options.cliRegistryPath) {
|
|
609
|
-
new TextFile(project, options.cliRegistryPath, {
|
|
610
|
-
lines: rustCliRegistrySource(this.releaseBinaries).trimEnd().split("\n"),
|
|
611
|
-
});
|
|
612
|
-
}
|
|
566
|
+
"",
|
|
567
|
+
nativeTargets: releaseTargets(options),
|
|
568
|
+
pythonModulePrefix: options.pythonModulePrefix ?? scope.replaceAll("-", "_"),
|
|
569
|
+
packageOptions: options.packages ?? {},
|
|
570
|
+
release: options.release ?? true,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
613
573
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
574
|
+
type RustPackageDependencyResolver = (pkg: DBXToolsRustProject) => DBXToolsRustProject[];
|
|
575
|
+
|
|
576
|
+
function createRustPackageDependencyResolver(
|
|
577
|
+
packages: readonly DBXToolsRustProject[],
|
|
578
|
+
project: javascript.NodeProject,
|
|
579
|
+
options: DBXToolsRustWorkspaceOptions,
|
|
580
|
+
): RustPackageDependencyResolver {
|
|
581
|
+
return (pkg) =>
|
|
582
|
+
rustPackageDependencies(packages, pkg, project.outdir, options.workspaceDependencies);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function planRustReleaseBinaries(
|
|
586
|
+
packages: readonly DBXToolsRustProject[],
|
|
587
|
+
resolved: ResolvedRustWorkspaceOptions,
|
|
588
|
+
): RustReleaseBinaryMapping[] {
|
|
589
|
+
const commands = new Set<string>();
|
|
590
|
+
const binaries = packages
|
|
591
|
+
.filter((pkg) => Boolean(pkg.packageOptions.cli))
|
|
592
|
+
.map((pkg) => {
|
|
593
|
+
if (!pkg.packageOptions.release) {
|
|
594
|
+
throw new Error(`${pkg.crateName} must set release when cli is configured`);
|
|
623
595
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
596
|
+
const configured = pkg.packageOptions.cli;
|
|
597
|
+
const command =
|
|
598
|
+
typeof configured === "object" && configured.command
|
|
599
|
+
? configured.command
|
|
600
|
+
: pkg.packageOptions.directory;
|
|
601
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(command)) {
|
|
602
|
+
throw new Error(`Invalid Rust CLI command: ${command}`);
|
|
632
603
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
604
|
+
if (commands.has(command)) {
|
|
605
|
+
throw new Error(`Duplicate Rust CLI command: ${command}`);
|
|
606
|
+
}
|
|
607
|
+
commands.add(command);
|
|
608
|
+
const description =
|
|
609
|
+
(typeof configured === "object" ? configured.description : undefined) ??
|
|
610
|
+
pkg.packageOptions.description ??
|
|
611
|
+
pkg.crateName;
|
|
612
|
+
const excludedOs = new Set(pkg.packageOptions.releaseExcludeOs ?? []);
|
|
613
|
+
const binaryName = pkg.packageOptions.binaryName ?? pkg.crateName;
|
|
614
|
+
return {
|
|
615
|
+
command,
|
|
616
|
+
description,
|
|
617
|
+
binaryName,
|
|
618
|
+
repository: resolved.repository,
|
|
619
|
+
assets: resolved.nativeTargets
|
|
620
|
+
.filter((target) => !excludedOs.has(target.os))
|
|
621
|
+
.map((target) => ({
|
|
622
|
+
os: target.os,
|
|
623
|
+
cpu: target.cpu,
|
|
624
|
+
name: releaseBinaryAssetName(binaryName, target.node, target.os),
|
|
625
|
+
})),
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
if (binaries.length && !resolved.repository) {
|
|
629
|
+
throw new Error("A repository URL is required when Rust CLI commands are configured");
|
|
630
|
+
}
|
|
631
|
+
return binaries;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function validateRustReleaseGraph(
|
|
635
|
+
packages: readonly DBXToolsRustProject[],
|
|
636
|
+
packageDependencies: RustPackageDependencyResolver,
|
|
637
|
+
): void {
|
|
638
|
+
for (const pkg of packages) {
|
|
639
|
+
const excludedOs = new Set(pkg.packageOptions.releaseExcludeOs ?? []);
|
|
640
|
+
if (excludedOs.size && pkg.uniffi) {
|
|
641
|
+
throw new Error(
|
|
642
|
+
`${pkg.crateName} cannot set releaseExcludeOs because UniFFI requires every configured target`,
|
|
639
643
|
);
|
|
640
|
-
this.bindingMappings = orderRustBindings(
|
|
641
|
-
bindings.map((pkg) => {
|
|
642
|
-
const targets = pkg.packageOptions.bindings ?? ["node", "python"];
|
|
643
|
-
const packageDirectory = `${string.toSlug(pkg.packageOptions.directory)}-rs`;
|
|
644
|
-
const dependencies = [
|
|
645
|
-
...new Set([...bindingDependencies(pkg, "node"), ...bindingDependencies(pkg, "python")]),
|
|
646
|
-
].map((dependency) => dependency.crateName);
|
|
647
|
-
return {
|
|
648
|
-
crate: pkg.crateName,
|
|
649
|
-
...(dependencies.length ? { dependencies } : {}),
|
|
650
|
-
rust: `${root}/${pkg.packageOptions.directory}`,
|
|
651
|
-
...(targets.includes("node")
|
|
652
|
-
? {
|
|
653
|
-
node: `${nodeRoot}/${packageDirectory}`,
|
|
654
|
-
nodePackage: `@${scope}/${packageDirectory}`,
|
|
655
|
-
}
|
|
656
|
-
: {}),
|
|
657
|
-
...(targets.includes("python")
|
|
658
|
-
? {
|
|
659
|
-
python: `${options.pythonRoot ?? "packages/py"}/${packageDirectory}`,
|
|
660
|
-
pythonPackage: `${scope}-${packageDirectory}`,
|
|
661
|
-
pythonModule: pythonModuleName(pythonModulePrefix, packageDirectory),
|
|
662
|
-
}
|
|
663
|
-
: {}),
|
|
664
|
-
};
|
|
665
|
-
}),
|
|
666
|
-
);
|
|
667
|
-
for (const pkg of bindings) {
|
|
668
|
-
const dependencies = bindingDependencies(pkg, "python");
|
|
669
|
-
if (dependencies.length === 0) continue;
|
|
670
|
-
pkg.tryRemoveFile("uniffi.toml");
|
|
671
|
-
new TextFile(pkg, "uniffi.toml", {
|
|
672
|
-
lines: renderToml({
|
|
673
|
-
"bindings.python": { cdylib_name: pkg.crateName.replaceAll("-", "_") },
|
|
674
|
-
"bindings.typescript": { strictTypeChecking: true },
|
|
675
|
-
...pkg.packageOptions.uniffiConfig,
|
|
676
|
-
"bindings.python.external_packages": Object.fromEntries(
|
|
677
|
-
dependencies.map((dependency) => [
|
|
678
|
-
dependency.crateName.replaceAll("-", "_"),
|
|
679
|
-
`${pythonModuleName(
|
|
680
|
-
pythonModulePrefix,
|
|
681
|
-
`${string.toSlug(dependency.packageOptions.directory)}-rs`,
|
|
682
|
-
)}.bindings`,
|
|
683
|
-
]),
|
|
684
|
-
),
|
|
685
|
-
})
|
|
686
|
-
.trimEnd()
|
|
687
|
-
.split("\n"),
|
|
688
|
-
});
|
|
689
644
|
}
|
|
690
|
-
const
|
|
691
|
-
(
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
bindings: this.bindingMappings,
|
|
698
|
-
binaries: this.releaseBinaries,
|
|
699
|
-
};
|
|
700
|
-
if (dbxToolsProject) {
|
|
701
|
-
dbxToolsProject.dbxToolsConfig.rust = this.workspaceMapping;
|
|
702
|
-
}
|
|
703
|
-
project.gitignore.addPatterns(
|
|
704
|
-
"!/Cargo.lock",
|
|
705
|
-
"target/",
|
|
706
|
-
...this.bindingMappings.flatMap((binding) => [
|
|
707
|
-
...(binding.node ? [`${binding.node}/src/*${binding.crate.replaceAll("-", "_")}.*`] : []),
|
|
708
|
-
...(binding.python && binding.pythonModule
|
|
709
|
-
? [
|
|
710
|
-
`${binding.python}/src/${binding.pythonModule.replaceAll(".", "/")}/bindings.py`,
|
|
711
|
-
`${binding.python}/src/${binding.pythonModule.replaceAll(".", "/")}/*${binding.crate.replaceAll("-", "_")}.*`,
|
|
712
|
-
]
|
|
713
|
-
: []),
|
|
714
|
-
]),
|
|
715
|
-
);
|
|
716
|
-
for (const binding of this.bindingMappings) {
|
|
717
|
-
if (!binding.node) {
|
|
718
|
-
continue;
|
|
645
|
+
for (const dependency of packageDependencies(pkg)) {
|
|
646
|
+
for (const os of dependency.packageOptions.releaseExcludeOs ?? []) {
|
|
647
|
+
if (!excludedOs.has(os)) {
|
|
648
|
+
throw new Error(
|
|
649
|
+
`${pkg.crateName} must exclude ${os} release builds because it depends on ${dependency.crateName}`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
719
652
|
}
|
|
720
|
-
project.prettier?.addIgnorePattern(`${binding.node}/src/bindings.ts`);
|
|
721
|
-
project.prettier?.addIgnorePattern(`${binding.node}/src/_bindings*.ts`);
|
|
722
653
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
.map((pkg) => {
|
|
726
|
-
const directory = `${string.toSlug(pkg.packageOptions.directory)}-rs`;
|
|
727
|
-
const module = pythonModuleName(pythonModulePrefix, directory);
|
|
728
|
-
const name = `${scope}-${directory}`;
|
|
729
|
-
return {
|
|
730
|
-
directory,
|
|
731
|
-
name,
|
|
732
|
-
module,
|
|
733
|
-
description: `Python bindings for ${pkg.crateName}`,
|
|
734
|
-
uniffi: true,
|
|
735
|
-
internalDependencies: bindingDependencies(pkg, "python").map(
|
|
736
|
-
(dependency) => `${string.toSlug(dependency.packageOptions.directory)}-rs`,
|
|
737
|
-
),
|
|
738
|
-
generatedSources: [
|
|
739
|
-
`src/${module.replaceAll(".", "/")}/bindings.py`,
|
|
740
|
-
`src/${module.replaceAll(".", "/")}/__init__.py`,
|
|
741
|
-
],
|
|
742
|
-
trustedPublisher: {
|
|
743
|
-
environment: `pypi-${name}`,
|
|
744
|
-
artifacts: `platform-specific wheels for ${nativeTargets
|
|
745
|
-
.map((target) => `${target.os}-${target.cpu}`)
|
|
746
|
-
.join(", ")}; all architectures publish to this one PyPI project`,
|
|
747
|
-
},
|
|
748
|
-
};
|
|
749
|
-
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
750
656
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
657
|
+
interface RustBindingPlan {
|
|
658
|
+
readonly packages: readonly DBXToolsRustProject[];
|
|
659
|
+
readonly mappings: readonly RustBindingMapping[];
|
|
660
|
+
readonly dependencies: ReadonlyMap<
|
|
661
|
+
DBXToolsRustProject,
|
|
662
|
+
Readonly<Record<"node" | "python", readonly DBXToolsRustProject[]>>
|
|
663
|
+
>;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function planRustBindings(
|
|
667
|
+
packages: readonly DBXToolsRustProject[],
|
|
668
|
+
packageDependencies: RustPackageDependencyResolver,
|
|
669
|
+
resolved: ResolvedRustWorkspaceOptions,
|
|
670
|
+
): RustBindingPlan {
|
|
671
|
+
const bindings = packages.filter((pkg) => pkg.uniffi);
|
|
672
|
+
const dependencies = new Map<
|
|
673
|
+
DBXToolsRustProject,
|
|
674
|
+
Readonly<Record<"node" | "python", readonly DBXToolsRustProject[]>>
|
|
675
|
+
>();
|
|
676
|
+
for (const pkg of bindings) {
|
|
677
|
+
const direct = packageDependencies(pkg);
|
|
678
|
+
dependencies.set(pkg, {
|
|
679
|
+
node: bindings.filter(
|
|
680
|
+
(dependency) =>
|
|
681
|
+
(dependency.packageOptions.bindings ?? ["node", "python"]).includes("node") &&
|
|
682
|
+
direct.includes(dependency),
|
|
683
|
+
),
|
|
684
|
+
python: bindings.filter(
|
|
685
|
+
(dependency) =>
|
|
686
|
+
(dependency.packageOptions.bindings ?? ["node", "python"]).includes("python") &&
|
|
687
|
+
direct.includes(dependency),
|
|
688
|
+
),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
const mappings = orderRustBindings(
|
|
692
|
+
bindings.map((pkg) => {
|
|
693
|
+
const targets = pkg.packageOptions.bindings ?? ["node", "python"];
|
|
694
|
+
const packageDirectory = `${string.toSlug(pkg.packageOptions.directory)}-rs`;
|
|
695
|
+
const direct = dependencies.get(pkg);
|
|
696
|
+
const dependencyCrates = [
|
|
697
|
+
...new Set([...(direct?.node ?? []), ...(direct?.python ?? [])]),
|
|
698
|
+
].map((dependency) => dependency.crateName);
|
|
699
|
+
return {
|
|
700
|
+
crate: pkg.crateName,
|
|
701
|
+
...(dependencyCrates.length ? { dependencies: dependencyCrates } : {}),
|
|
702
|
+
rust: `${resolved.root}/${pkg.packageOptions.directory}`,
|
|
703
|
+
...(targets.includes("node")
|
|
704
|
+
? {
|
|
705
|
+
node: `${resolved.nodeRoot}/${packageDirectory}`,
|
|
706
|
+
nodePackage: `@${resolved.scope}/${packageDirectory}`,
|
|
707
|
+
}
|
|
708
|
+
: {}),
|
|
709
|
+
...(targets.includes("python")
|
|
710
|
+
? {
|
|
711
|
+
python: `${resolved.pythonRoot}/${packageDirectory}`,
|
|
712
|
+
pythonPackage: `${resolved.scope}-${packageDirectory}`,
|
|
713
|
+
pythonModule: pythonModuleName(resolved.pythonModulePrefix, packageDirectory),
|
|
714
|
+
}
|
|
715
|
+
: {}),
|
|
716
|
+
};
|
|
717
|
+
}),
|
|
718
|
+
);
|
|
719
|
+
return { packages: bindings, mappings, dependencies };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function bindingDependencies(
|
|
723
|
+
plan: RustBindingPlan,
|
|
724
|
+
pkg: DBXToolsRustProject,
|
|
725
|
+
language: "node" | "python",
|
|
726
|
+
): readonly DBXToolsRustProject[] {
|
|
727
|
+
return plan.dependencies.get(pkg)?.[language] ?? [];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function planPythonBindingPackages(
|
|
731
|
+
plan: RustBindingPlan,
|
|
732
|
+
resolved: ResolvedRustWorkspaceOptions,
|
|
733
|
+
): PythonPackageOptions[] {
|
|
734
|
+
return plan.packages
|
|
735
|
+
.filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("python"))
|
|
736
|
+
.map((pkg) => {
|
|
737
|
+
const directory = `${string.toSlug(pkg.packageOptions.directory)}-rs`;
|
|
738
|
+
const module = pythonModuleName(resolved.pythonModulePrefix, directory);
|
|
739
|
+
const name = `${resolved.scope}-${directory}`;
|
|
740
|
+
return {
|
|
741
|
+
directory,
|
|
742
|
+
name,
|
|
743
|
+
module,
|
|
744
|
+
description: `Python bindings for ${pkg.crateName}`,
|
|
745
|
+
uniffi: true,
|
|
746
|
+
internalDependencies: bindingDependencies(plan, pkg, "python").map(
|
|
747
|
+
(dependency) => `${string.toSlug(dependency.packageOptions.directory)}-rs`,
|
|
748
|
+
),
|
|
749
|
+
generatedSources: [
|
|
750
|
+
`src/${module.replaceAll(".", "/")}/bindings.py`,
|
|
751
|
+
`src/${module.replaceAll(".", "/")}/__init__.py`,
|
|
752
|
+
],
|
|
753
|
+
trustedPublisher: {
|
|
754
|
+
environment: `pypi-${name}`,
|
|
755
|
+
artifacts: `platform-specific wheels for ${resolved.nativeTargets
|
|
756
|
+
.map((target) => `${target.os}-${target.cpu}`)
|
|
757
|
+
.join(", ")}; all architectures publish to this one PyPI project`,
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
interface RustNodeBindingPackagePlan {
|
|
764
|
+
readonly binding: DBXToolsRustProject;
|
|
765
|
+
readonly memberPath: string;
|
|
766
|
+
readonly name: string;
|
|
767
|
+
readonly optionalDependencies: Readonly<Record<string, string>>;
|
|
768
|
+
readonly internalDependencies: readonly string[];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function planNodeBindingPackages(
|
|
772
|
+
project: javascript.NodeProject,
|
|
773
|
+
plan: RustBindingPlan,
|
|
774
|
+
resolved: ResolvedRustWorkspaceOptions,
|
|
775
|
+
): RustNodeBindingPackagePlan[] {
|
|
776
|
+
const version = readWorkspaceVersion(project.outdir);
|
|
777
|
+
return plan.packages
|
|
778
|
+
.filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("node"))
|
|
779
|
+
.map((binding) => {
|
|
758
780
|
const directory = `${string.toSlug(binding.packageOptions.directory)}-rs`;
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
"
|
|
772
|
-
`@${scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
|
|
773
|
-
);
|
|
774
|
-
node.dbxToolsConfig.uniffi = true;
|
|
775
|
-
node.package.addField("description", `Node bindings for ${binding.crateName}`);
|
|
776
|
-
if (options.release ?? true) {
|
|
777
|
-
node.package.addField(
|
|
778
|
-
"optionalDependencies",
|
|
779
|
-
Object.fromEntries(
|
|
780
|
-
nativeTargets.map((target) => [
|
|
781
|
-
`@${scope}/${directory}-${target.node}`,
|
|
782
|
-
readWorkspaceVersion(project.outdir),
|
|
783
|
-
]),
|
|
784
|
-
),
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
node.addDeps("@ubjs/core@0.31.0-5", "@ubjs/node@0.31.0-5");
|
|
788
|
-
node.addDeps(
|
|
789
|
-
...bindingDependencies(binding, "node").map(
|
|
781
|
+
return {
|
|
782
|
+
binding,
|
|
783
|
+
memberPath: `${resolved.nodeRoot}/${directory}`,
|
|
784
|
+
name: `@${resolved.scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
|
|
785
|
+
optionalDependencies: resolved.release
|
|
786
|
+
? Object.fromEntries(
|
|
787
|
+
resolved.nativeTargets.map((target) => [
|
|
788
|
+
`@${resolved.scope}/${directory}-${target.node}`,
|
|
789
|
+
version,
|
|
790
|
+
]),
|
|
791
|
+
)
|
|
792
|
+
: {},
|
|
793
|
+
internalDependencies: bindingDependencies(plan, binding, "node").map(
|
|
790
794
|
(dependency) =>
|
|
791
|
-
`@${scope}/${string.toSlug(dependency.packageOptions.directory)}-rs@workspace:*`,
|
|
795
|
+
`@${resolved.scope}/${string.toSlug(dependency.packageOptions.directory)}-rs@workspace:*`,
|
|
792
796
|
),
|
|
793
|
-
|
|
794
|
-
node.addDevDeps(`uniffi-bindgen-react-native@${UBRN_VERSION}`);
|
|
795
|
-
nodePackages.push(node);
|
|
796
|
-
}
|
|
797
|
-
this.nodePackages = nodePackages;
|
|
798
|
-
|
|
799
|
-
const generatedWorkspaceManifest: Record<string, unknown> = {
|
|
800
|
-
workspace: {
|
|
801
|
-
members: this.packages.map((pkg) => `${root}/${pkg.packageOptions.directory}`),
|
|
802
|
-
"default-members": this.packages
|
|
803
|
-
.filter((pkg) => !pkg.uniffi)
|
|
804
|
-
.map((pkg) => `${root}/${pkg.packageOptions.directory}`),
|
|
805
|
-
resolver: "2",
|
|
806
|
-
},
|
|
807
|
-
"workspace.package": {
|
|
808
|
-
version: readWorkspaceVersion(project.outdir),
|
|
809
|
-
edition: options.edition ?? "2021",
|
|
810
|
-
"rust-version": options.rustVersion ?? "1.82",
|
|
811
|
-
license: options.license ?? "Apache-2.0",
|
|
812
|
-
repository,
|
|
813
|
-
},
|
|
814
|
-
...(options.workspaceDependencies
|
|
815
|
-
? {
|
|
816
|
-
"workspace.dependencies": Object.fromEntries(
|
|
817
|
-
Object.entries(options.workspaceDependencies).map(([name, value]) => [
|
|
818
|
-
name,
|
|
819
|
-
cargoDependency(value),
|
|
820
|
-
]),
|
|
821
|
-
),
|
|
822
|
-
}
|
|
823
|
-
: {}),
|
|
824
|
-
};
|
|
825
|
-
new TextFile(project, "Cargo.toml", {
|
|
826
|
-
lines: renderToml(generatedWorkspaceManifest).trimEnd().split("\n"),
|
|
797
|
+
};
|
|
827
798
|
});
|
|
828
|
-
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function createRustPackages(
|
|
802
|
+
project: javascript.NodeProject,
|
|
803
|
+
resolved: ResolvedRustWorkspaceOptions,
|
|
804
|
+
): DBXToolsRustProject[] {
|
|
805
|
+
return discoverRustCrates(resolve(project.outdir, resolved.root)).map(
|
|
806
|
+
(directory) =>
|
|
807
|
+
new DBXToolsRustProject(project, resolved.root, resolved.scope, {
|
|
808
|
+
directory,
|
|
809
|
+
...resolved.packageOptions[directory],
|
|
810
|
+
}),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function configureRustCliRegistry(
|
|
815
|
+
project: javascript.NodeProject,
|
|
816
|
+
path: string | undefined,
|
|
817
|
+
binaries: readonly RustReleaseBinaryMapping[],
|
|
818
|
+
): void {
|
|
819
|
+
if (binaries.length && !path) {
|
|
820
|
+
throw new Error("cliRegistryPath is required when Rust CLI commands are configured");
|
|
821
|
+
}
|
|
822
|
+
if (!path) return;
|
|
823
|
+
new TextFile(project, path, {
|
|
824
|
+
lines: rustCliRegistrySource(binaries).trimEnd().split("\n"),
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function configureRustBindingFiles(
|
|
829
|
+
plan: RustBindingPlan,
|
|
830
|
+
resolved: ResolvedRustWorkspaceOptions,
|
|
831
|
+
): void {
|
|
832
|
+
for (const pkg of plan.packages) {
|
|
833
|
+
const dependencies = bindingDependencies(plan, pkg, "python");
|
|
834
|
+
if (dependencies.length === 0) continue;
|
|
835
|
+
pkg.tryRemoveFile("uniffi.toml");
|
|
836
|
+
new TextFile(pkg, "uniffi.toml", {
|
|
829
837
|
lines: renderToml({
|
|
830
|
-
"
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
"
|
|
834
|
-
|
|
835
|
-
|
|
838
|
+
"bindings.python": { cdylib_name: pkg.crateName.replaceAll("-", "_") },
|
|
839
|
+
"bindings.typescript": { strictTypeChecking: true },
|
|
840
|
+
...pkg.packageOptions.uniffiConfig,
|
|
841
|
+
"bindings.python.external_packages": Object.fromEntries(
|
|
842
|
+
dependencies.map((dependency) => [
|
|
843
|
+
dependency.crateName.replaceAll("-", "_"),
|
|
844
|
+
`${pythonModuleName(
|
|
845
|
+
resolved.pythonModulePrefix,
|
|
846
|
+
`${string.toSlug(dependency.packageOptions.directory)}-rs`,
|
|
847
|
+
)}.bindings`,
|
|
848
|
+
]),
|
|
849
|
+
),
|
|
836
850
|
})
|
|
837
851
|
.trimEnd()
|
|
838
852
|
.split("\n"),
|
|
839
853
|
});
|
|
840
|
-
new RustWorkspaceVersionLock(project);
|
|
841
|
-
project.addTask("rs:format", { exec: "cargo fmt --all" });
|
|
842
|
-
project.addTask("rs:lint", { exec: "cargo clippy --workspace --all-targets --all-features" });
|
|
843
|
-
project.addTask("rs:test", { exec: "cargo test --workspace" });
|
|
844
|
-
project.addTask("rs:build", { exec: "cargo build --workspace" });
|
|
845
|
-
project.addTask("rs:bindings", {
|
|
846
|
-
exec: "bun node_modules/@dbx-tools/projen/tasks/rust.ts",
|
|
847
|
-
description: "Generate language bindings for UniFFI-enabled Rust crates",
|
|
848
|
-
});
|
|
849
|
-
project.removeTask("rs:bindings:demo");
|
|
850
|
-
if (releaseEnabled) {
|
|
851
|
-
this.addReleaseWorkflow(project, options, nativeTargets);
|
|
852
|
-
}
|
|
853
854
|
}
|
|
855
|
+
}
|
|
854
856
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
...binding,
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
857
|
+
function configureRustBindingIgnores(
|
|
858
|
+
project: javascript.NodeProject,
|
|
859
|
+
bindings: readonly RustBindingMapping[],
|
|
860
|
+
): void {
|
|
861
|
+
project.gitignore.addPatterns(
|
|
862
|
+
"!/Cargo.lock",
|
|
863
|
+
"target/",
|
|
864
|
+
...bindings.flatMap((binding) => [
|
|
865
|
+
...(binding.node ? [`${binding.node}/src/*${binding.crate.replaceAll("-", "_")}.*`] : []),
|
|
866
|
+
...(binding.python && binding.pythonModule
|
|
867
|
+
? [
|
|
868
|
+
`${binding.python}/src/${binding.pythonModule.replaceAll(".", "/")}/bindings.py`,
|
|
869
|
+
`${binding.python}/src/${binding.pythonModule.replaceAll(".", "/")}/*${binding.crate.replaceAll("-", "_")}.*`,
|
|
870
|
+
]
|
|
871
|
+
: []),
|
|
872
|
+
]),
|
|
873
|
+
);
|
|
874
|
+
for (const binding of bindings) {
|
|
875
|
+
if (!binding.node) continue;
|
|
876
|
+
project.prettier?.addIgnorePattern(`${binding.node}/src/bindings.ts`);
|
|
877
|
+
project.prettier?.addIgnorePattern(`${binding.node}/src/_bindings*.ts`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function createRustNodeBindingPackages(
|
|
882
|
+
project: javascript.NodeProject,
|
|
883
|
+
plans: readonly RustNodeBindingPackagePlan[],
|
|
884
|
+
): DBXToolsTypeScriptProject[] {
|
|
885
|
+
const existing = new Map(
|
|
886
|
+
project.subprojects.map((child) => [relative(project.outdir, child.outdir), child]),
|
|
887
|
+
);
|
|
888
|
+
return plans.map((plan) => {
|
|
889
|
+
const found = existing.get(plan.memberPath);
|
|
890
|
+
const existingNode = found instanceof DBXToolsTypeScriptProject ? found : undefined;
|
|
891
|
+
const node =
|
|
892
|
+
existingNode ??
|
|
893
|
+
new DBXToolsTypeScriptProject({
|
|
894
|
+
parent: project,
|
|
895
|
+
outdir: plan.memberPath,
|
|
896
|
+
name: plan.name,
|
|
897
|
+
tags: ["node"],
|
|
898
|
+
});
|
|
899
|
+
node.package.addField("name", plan.name);
|
|
900
|
+
node.dbxToolsConfig.uniffi = true;
|
|
901
|
+
node.package.addField("description", `Node bindings for ${plan.binding.crateName}`);
|
|
902
|
+
if (Object.keys(plan.optionalDependencies).length) {
|
|
903
|
+
node.package.addField("optionalDependencies", plan.optionalDependencies);
|
|
904
|
+
}
|
|
905
|
+
node.addDeps("@ubjs/core@0.31.0-5", "@ubjs/node@0.31.0-5");
|
|
906
|
+
node.addDeps(...plan.internalDependencies);
|
|
907
|
+
node.addDevDeps(`uniffi-bindgen-react-native@${UBRN_VERSION}`);
|
|
908
|
+
return node;
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function createRustWorkspaceMapping(
|
|
913
|
+
packages: readonly DBXToolsRustProject[],
|
|
914
|
+
bindings: readonly RustBindingMapping[],
|
|
915
|
+
binaries: readonly RustReleaseBinaryMapping[],
|
|
916
|
+
resolved: ResolvedRustWorkspaceOptions,
|
|
917
|
+
): RustWorkspaceMapping {
|
|
918
|
+
return {
|
|
919
|
+
root: resolved.root,
|
|
920
|
+
crates: packages.map((pkg) => `${resolved.root}/${pkg.packageOptions.directory}`),
|
|
921
|
+
bindings,
|
|
922
|
+
binaries,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function configureRustWorkspaceFiles(
|
|
927
|
+
project: javascript.NodeProject,
|
|
928
|
+
packages: readonly DBXToolsRustProject[],
|
|
929
|
+
options: DBXToolsRustWorkspaceOptions,
|
|
930
|
+
resolved: ResolvedRustWorkspaceOptions,
|
|
931
|
+
): void {
|
|
932
|
+
const manifest: Record<string, unknown> = {
|
|
933
|
+
workspace: {
|
|
934
|
+
members: packages.map((pkg) => `${resolved.root}/${pkg.packageOptions.directory}`),
|
|
935
|
+
"default-members": packages
|
|
936
|
+
.filter((pkg) => !pkg.uniffi)
|
|
937
|
+
.map((pkg) => `${resolved.root}/${pkg.packageOptions.directory}`),
|
|
938
|
+
resolver: "2",
|
|
939
|
+
},
|
|
940
|
+
"workspace.package": {
|
|
941
|
+
version: readWorkspaceVersion(project.outdir),
|
|
942
|
+
edition: options.edition ?? "2021",
|
|
943
|
+
"rust-version": options.rustVersion ?? "1.82",
|
|
944
|
+
license: options.license ?? DBX_TOOLS_LICENSE,
|
|
945
|
+
repository: resolved.repository,
|
|
946
|
+
},
|
|
947
|
+
...(options.workspaceDependencies
|
|
948
|
+
? {
|
|
949
|
+
"workspace.dependencies": Object.fromEntries(
|
|
950
|
+
Object.entries(options.workspaceDependencies).map(([name, value]) => [
|
|
951
|
+
name,
|
|
952
|
+
cargoDependency(value),
|
|
953
|
+
]),
|
|
954
|
+
),
|
|
955
|
+
}
|
|
956
|
+
: {}),
|
|
957
|
+
};
|
|
958
|
+
new TextFile(project, "Cargo.toml", {
|
|
959
|
+
lines: renderToml(manifest).trimEnd().split("\n"),
|
|
960
|
+
});
|
|
961
|
+
new TextFile(project, ".cargo/config.toml", {
|
|
962
|
+
lines: renderToml({
|
|
963
|
+
"target.x86_64-pc-windows-msvc": {
|
|
964
|
+
rustflags: ["-C", "target-feature=+crt-static"],
|
|
965
|
+
},
|
|
966
|
+
"target.aarch64-pc-windows-msvc": {
|
|
967
|
+
rustflags: ["-C", "target-feature=+crt-static"],
|
|
968
|
+
},
|
|
969
|
+
})
|
|
970
|
+
.trimEnd()
|
|
971
|
+
.split("\n"),
|
|
972
|
+
});
|
|
973
|
+
new RustWorkspaceVersionLock(project);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function configureRustWorkspaceTasks(project: javascript.NodeProject): void {
|
|
977
|
+
project.addTask("rs:format", { exec: "cargo fmt --all" });
|
|
978
|
+
project.addTask("rs:lint", { exec: "cargo clippy --workspace --all-targets --all-features" });
|
|
979
|
+
project.addTask("rs:test", { exec: "cargo test --workspace" });
|
|
980
|
+
project.addTask("rs:build", { exec: "cargo build --workspace" });
|
|
981
|
+
project.addTask("rs:bindings", {
|
|
982
|
+
exec: "bun node_modules/@dbx-tools/projen/tasks/rust.ts",
|
|
983
|
+
description: "Generate language bindings for UniFFI-enabled Rust crates",
|
|
984
|
+
});
|
|
985
|
+
project.removeTask("rs:bindings:demo");
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
interface RustReleaseBinding extends RustBindingMapping {
|
|
989
|
+
readonly node: string;
|
|
990
|
+
readonly python: string;
|
|
991
|
+
readonly nodePackage: string;
|
|
992
|
+
readonly pythonPackage: string;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
interface RustReleaseBinaryPlan {
|
|
996
|
+
readonly crate: string;
|
|
997
|
+
readonly binary: string;
|
|
998
|
+
readonly excludedOs: readonly RustReleaseOs[];
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
type RustReleaseTargetPlan = Readonly<Record<string, string | boolean | number>>;
|
|
1002
|
+
|
|
1003
|
+
interface RustReleasePlan {
|
|
1004
|
+
readonly releaseRustVersion: string;
|
|
1005
|
+
readonly releaseTask: string;
|
|
1006
|
+
readonly bindings: readonly RustReleaseBinding[];
|
|
1007
|
+
readonly nodeBindings: readonly RustReleaseBinding[];
|
|
1008
|
+
readonly releaseBinaries: readonly RustReleaseBinaryPlan[];
|
|
1009
|
+
readonly publicCrates: readonly string[];
|
|
1010
|
+
readonly targets: readonly RustReleaseTargetPlan[];
|
|
1011
|
+
readonly hasReleaseExclusions: boolean;
|
|
1012
|
+
readonly hasPythonBindings: boolean;
|
|
1013
|
+
readonly usesCargoLock: boolean;
|
|
1014
|
+
readonly usePreinstalledWindowsRust: boolean;
|
|
1015
|
+
readonly hasTargetOutputs: boolean;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function planRustRelease(
|
|
1019
|
+
project: javascript.NodeProject,
|
|
1020
|
+
options: DBXToolsRustWorkspaceOptions,
|
|
1021
|
+
targets: readonly UniFFIReleaseTarget[],
|
|
1022
|
+
packages: readonly DBXToolsRustProject[],
|
|
1023
|
+
bindingMappings: readonly RustBindingMapping[],
|
|
1024
|
+
): RustReleasePlan {
|
|
1025
|
+
const releaseRustVersion = options.releaseRustVersion ?? "stable";
|
|
1026
|
+
const bindings = bindingMappings.map((binding) => ({
|
|
1027
|
+
...binding,
|
|
1028
|
+
node: binding.node ?? "",
|
|
1029
|
+
python: binding.python ?? "",
|
|
1030
|
+
nodePackage: binding.nodePackage ?? "",
|
|
1031
|
+
pythonPackage: binding.pythonPackage ?? "",
|
|
1032
|
+
}));
|
|
1033
|
+
const releaseBinaries = packages
|
|
1034
|
+
.filter((pkg) => pkg.packageOptions.release)
|
|
1035
|
+
.map((pkg) => ({
|
|
1036
|
+
crate: pkg.crateName,
|
|
1037
|
+
binary: pkg.packageOptions.binaryName ?? pkg.crateName,
|
|
1038
|
+
excludedOs: pkg.packageOptions.releaseExcludeOs ?? [],
|
|
868
1039
|
}));
|
|
869
|
-
|
|
870
|
-
|
|
1040
|
+
const packageDependencies = createRustPackageDependencyResolver(packages, project, options);
|
|
1041
|
+
const publicCrates = orderRustBindings(
|
|
1042
|
+
packages
|
|
1043
|
+
.filter((pkg) => !pkg.packageOptions.private)
|
|
871
1044
|
.map((pkg) => ({
|
|
872
1045
|
crate: pkg.crateName,
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}))
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
(
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1046
|
+
rust: pkg.outdir,
|
|
1047
|
+
dependencies: packageDependencies(pkg).map((dependency) => dependency.crateName),
|
|
1048
|
+
})),
|
|
1049
|
+
).map((pkg) => pkg.crate);
|
|
1050
|
+
const hasReleaseExclusions = packages.some((pkg) => pkg.packageOptions.releaseExcludeOs?.length);
|
|
1051
|
+
const plannedTargets: RustReleaseTargetPlan[] = targets.map((target) => {
|
|
1052
|
+
const cargoExcludes = packages
|
|
1053
|
+
.filter((pkg) => pkg.packageOptions.releaseExcludeOs?.includes(target.os))
|
|
1054
|
+
.map((pkg) => `--exclude ${pkg.crateName}`)
|
|
1055
|
+
.join(" ");
|
|
1056
|
+
return {
|
|
1057
|
+
runner: target.runner,
|
|
1058
|
+
cargo: target.cargo,
|
|
1059
|
+
node: target.node,
|
|
1060
|
+
python: target.python,
|
|
1061
|
+
os: target.os,
|
|
1062
|
+
cpu: target.cpu,
|
|
1063
|
+
...(target.libc ? { libc: target.libc } : {}),
|
|
1064
|
+
...(hasReleaseExclusions ? { cargoExcludes } : {}),
|
|
1065
|
+
};
|
|
1066
|
+
});
|
|
1067
|
+
const hasTargetOutputs =
|
|
1068
|
+
bindings.length > 0 || releaseBinaries.length > 0 || publicCrates.length > 0;
|
|
1069
|
+
if (hasTargetOutputs && plannedTargets.length === 0) {
|
|
1070
|
+
throw new Error("Rust release requires at least one target");
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
releaseRustVersion,
|
|
1074
|
+
releaseTask: ".projen/uniffi-release.mjs",
|
|
1075
|
+
bindings,
|
|
1076
|
+
nodeBindings: bindings.filter((binding) => Boolean(binding.node && binding.nodePackage)),
|
|
1077
|
+
releaseBinaries,
|
|
1078
|
+
publicCrates,
|
|
1079
|
+
targets: plannedTargets,
|
|
1080
|
+
hasReleaseExclusions,
|
|
1081
|
+
hasPythonBindings: bindings.some((binding) => binding.python),
|
|
1082
|
+
usesCargoLock: existsSync(join(project.outdir, "Cargo.lock")),
|
|
1083
|
+
usePreinstalledWindowsRust: releaseRustVersion === "stable",
|
|
1084
|
+
hasTargetOutputs,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function configureRustReleaseTask(project: javascript.NodeProject, plan: RustReleasePlan): void {
|
|
1089
|
+
if (plan.bindings.length) {
|
|
1090
|
+
new TextFile(project, plan.releaseTask, {
|
|
1091
|
+
lines: uniffiReleaseTaskSource().trimEnd().split("\n"),
|
|
899
1092
|
});
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1093
|
+
} else {
|
|
1094
|
+
project.tryRemoveFile(plan.releaseTask);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function rustBindingCommands(plan: RustReleasePlan): string[] {
|
|
1099
|
+
return plan.bindings.map((binding) =>
|
|
1100
|
+
[
|
|
1101
|
+
`node ${plan.releaseTask} build`,
|
|
1102
|
+
`--crate "${binding.crate}"`,
|
|
1103
|
+
`--node "${binding.node}"`,
|
|
1104
|
+
`--python "${binding.python}"`,
|
|
1105
|
+
`--node-package "${binding.nodePackage}"`,
|
|
1106
|
+
`--python-package "${binding.pythonPackage}"`,
|
|
1107
|
+
`--python-module "${binding.pythonModule}"`,
|
|
1108
|
+
'--cargo-target "${{ matrix.cargo }}"',
|
|
1109
|
+
'--node-triple "${{ matrix.node }}"',
|
|
1110
|
+
'--python-tag "${{ matrix.python }}"',
|
|
1111
|
+
'--os "${{ matrix.os }}"',
|
|
1112
|
+
'--cpu "${{ matrix.cpu }}"',
|
|
1113
|
+
'--libc "${{ matrix.libc }}"',
|
|
1114
|
+
'--version "$VERSION"',
|
|
1115
|
+
`--output "dist/release/${binding.crate}/\${{ matrix.node }}"`,
|
|
1116
|
+
"--skip-build",
|
|
1117
|
+
].join(" \\\n "),
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function rustReleaseBinaryCondition(excludedOs: readonly RustReleaseOs[]): string | undefined {
|
|
1122
|
+
return excludedOs.length
|
|
1123
|
+
? `\${{ ${excludedOs.map((os) => `matrix.os != '${os}'`).join(" && ")} }}`
|
|
1124
|
+
: undefined;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function rustBinaryCommands(plan: RustReleasePlan): string[] {
|
|
1128
|
+
return plan.releaseBinaries.flatMap((pkg) => {
|
|
1129
|
+
const windowsAsset = releaseBinaryAssetName(
|
|
1130
|
+
pkg.binary,
|
|
1131
|
+
"${{ matrix.node }}",
|
|
1132
|
+
RustReleaseOs.WINDOWS,
|
|
935
1133
|
);
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
)
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const artifactSteps: JobStep[] = [
|
|
971
|
-
...bindings.flatMap((binding) => [
|
|
972
|
-
...(binding.node
|
|
973
|
-
? [
|
|
974
|
-
{
|
|
975
|
-
name: `Upload ${binding.crate} native npm package`,
|
|
976
|
-
uses: "actions/upload-artifact@v7",
|
|
977
|
-
with: {
|
|
978
|
-
name: `${binding.crate}-\${{ matrix.node }}-npm`,
|
|
979
|
-
path: `dist/release/${binding.crate}/\${{ matrix.node }}/npm/*.tgz`,
|
|
980
|
-
"retention-days": 7,
|
|
981
|
-
},
|
|
1134
|
+
const unixAsset = releaseBinaryAssetName(pkg.binary, "${{ matrix.node }}", RustReleaseOs.LINUX);
|
|
1135
|
+
const commands = [
|
|
1136
|
+
`mkdir -p "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
|
|
1137
|
+
`SOURCE="target/\${{ matrix.cargo }}/release/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
|
|
1138
|
+
`DESTINATION="dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
|
|
1139
|
+
'cp "$SOURCE" "$DESTINATION"',
|
|
1140
|
+
'if [ "${{ matrix.os }}" = "win32" ]; then',
|
|
1141
|
+
` 7z a "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${windowsAsset}" "$DESTINATION"`,
|
|
1142
|
+
"else",
|
|
1143
|
+
` tar -C "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage" -czf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${unixAsset}" "${pkg.binary}"`,
|
|
1144
|
+
"fi",
|
|
1145
|
+
`rm -rf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
|
|
1146
|
+
];
|
|
1147
|
+
const excludedCondition = pkg.excludedOs
|
|
1148
|
+
.map((os) => `[ "\${{ matrix.os }}" != "${os}" ]`)
|
|
1149
|
+
.join(" && ");
|
|
1150
|
+
return excludedCondition
|
|
1151
|
+
? [`if ${excludedCondition}; then`, ...commands.map((command) => ` ${command}`), "fi"]
|
|
1152
|
+
: commands;
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function rustArtifactSteps(plan: RustReleasePlan): JobStep[] {
|
|
1157
|
+
return [
|
|
1158
|
+
...plan.bindings.flatMap((binding) => [
|
|
1159
|
+
...(binding.node
|
|
1160
|
+
? [
|
|
1161
|
+
{
|
|
1162
|
+
name: `Upload ${binding.crate} native npm package`,
|
|
1163
|
+
uses: "actions/upload-artifact@v7",
|
|
1164
|
+
with: {
|
|
1165
|
+
name: `${binding.crate}-\${{ matrix.node }}-npm`,
|
|
1166
|
+
path: `dist/release/${binding.crate}/\${{ matrix.node }}/npm/*.tgz`,
|
|
1167
|
+
"retention-days": 7,
|
|
982
1168
|
},
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1169
|
+
},
|
|
1170
|
+
]
|
|
1171
|
+
: []),
|
|
1172
|
+
...(binding.python
|
|
1173
|
+
? [
|
|
1174
|
+
{
|
|
1175
|
+
name: `Upload ${binding.crate} Python wheel`,
|
|
1176
|
+
uses: "actions/upload-artifact@v7",
|
|
1177
|
+
with: {
|
|
1178
|
+
name: `${binding.pythonPackage}--\${{ matrix.python }}--python-wheel`,
|
|
1179
|
+
path: `dist/release/${binding.crate}/\${{ matrix.node }}/python/*.whl`,
|
|
1180
|
+
"retention-days": 7,
|
|
995
1181
|
},
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1182
|
+
},
|
|
1183
|
+
]
|
|
1184
|
+
: []),
|
|
1185
|
+
]),
|
|
1186
|
+
...plan.releaseBinaries.map((pkg) => ({
|
|
1187
|
+
name: `Upload ${pkg.crate} release binary`,
|
|
1188
|
+
uses: "actions/upload-artifact@v7",
|
|
1189
|
+
...(rustReleaseBinaryCondition(pkg.excludedOs)
|
|
1190
|
+
? { if: rustReleaseBinaryCondition(pkg.excludedOs) }
|
|
1191
|
+
: {}),
|
|
1192
|
+
with: {
|
|
1193
|
+
name: `${pkg.crate}-\${{ matrix.node }}-binary`,
|
|
1194
|
+
path: `dist/release/${pkg.crate}/\${{ matrix.node }}/binary/*`,
|
|
1195
|
+
"retention-days": 7,
|
|
1196
|
+
},
|
|
1197
|
+
})),
|
|
1198
|
+
];
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function rustBuildJob(plan: RustReleasePlan): Job {
|
|
1202
|
+
const bindingCommands = rustBindingCommands(plan);
|
|
1203
|
+
const binaryCommands = rustBinaryCommands(plan);
|
|
1204
|
+
return {
|
|
1205
|
+
if: "${{ github.event_name == 'push' || inputs.stage == 'all' }}",
|
|
1206
|
+
name: "${{ matrix.node }}",
|
|
1207
|
+
needs: ["verify-context"],
|
|
1208
|
+
runsOn: ["${{ matrix.runner }}"],
|
|
1209
|
+
permissions: { contents: JobPermission.READ },
|
|
1210
|
+
env: { ...RUST_CACHE_ENV },
|
|
1211
|
+
strategy: {
|
|
1212
|
+
failFast: false,
|
|
1213
|
+
matrix: { include: [...plan.targets] },
|
|
1214
|
+
},
|
|
1215
|
+
steps: [
|
|
1216
|
+
...releaseSourceSteps(),
|
|
1217
|
+
...(plan.hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
|
|
1218
|
+
{
|
|
1219
|
+
name: "Setup Rust",
|
|
1220
|
+
...(plan.usePreinstalledWindowsRust ? { if: "${{ matrix.os != 'win32' }}" } : {}),
|
|
1221
|
+
uses: `dtolnay/rust-toolchain@${plan.releaseRustVersion}`,
|
|
1222
|
+
with: { targets: "${{ matrix.cargo }}" },
|
|
1223
|
+
},
|
|
1224
|
+
...(plan.usePreinstalledWindowsRust
|
|
1225
|
+
? [
|
|
1226
|
+
{
|
|
1227
|
+
name: "Verify preinstalled Windows Rust",
|
|
1228
|
+
if: "${{ matrix.os == 'win32' }}",
|
|
1229
|
+
shell: "bash",
|
|
1230
|
+
run: [
|
|
1231
|
+
"rustc --version --verbose",
|
|
1232
|
+
"cargo --version",
|
|
1233
|
+
'rustup target list --installed | grep -Fx "${{ matrix.cargo }}"',
|
|
1234
|
+
'test -f "$(rustc --print sysroot)/lib/rustlib/${{ matrix.cargo }}/bin/rust-lld.exe"',
|
|
1235
|
+
].join("\n"),
|
|
1236
|
+
},
|
|
1237
|
+
]
|
|
1238
|
+
: []),
|
|
1239
|
+
...rustCacheSteps(`release-\${{ matrix.cargo }}-rust-${plan.releaseRustVersion}`),
|
|
1240
|
+
{
|
|
1241
|
+
name: "Install Linux native dependencies",
|
|
1242
|
+
if: "${{ matrix.os == 'linux' }}",
|
|
1243
|
+
run: [
|
|
1244
|
+
"sudo rm -f /etc/apt/sources.list.d/google-chrome.list",
|
|
1245
|
+
"sudo apt-get update",
|
|
1246
|
+
"sudo apt-get install --yes libdbus-1-dev pkg-config",
|
|
1247
|
+
].join("\n"),
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
name: "Build Rust outputs",
|
|
1251
|
+
shell: "bash",
|
|
1252
|
+
env: {
|
|
1253
|
+
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER:
|
|
1254
|
+
"${{ matrix.os == 'win32' && 'rust-lld' || '' }}",
|
|
1009
1255
|
},
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
runsOn: ["${{ matrix.runner }}"],
|
|
1017
|
-
permissions: { contents: JobPermission.READ },
|
|
1018
|
-
env: {
|
|
1019
|
-
...RUST_CACHE_ENV,
|
|
1256
|
+
run: timedBash(
|
|
1257
|
+
"rust_workspace",
|
|
1258
|
+
`cargo build --release --workspace${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"${
|
|
1259
|
+
plan.hasReleaseExclusions ? " ${{ matrix.cargoExcludes }}" : ""
|
|
1260
|
+
}`,
|
|
1261
|
+
),
|
|
1020
1262
|
},
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1263
|
+
...(bindingCommands.length
|
|
1264
|
+
? [
|
|
1265
|
+
{
|
|
1266
|
+
name: "Package UniFFI outputs",
|
|
1267
|
+
shell: "bash",
|
|
1268
|
+
env: { VERSION: RELEASE_VERSION },
|
|
1269
|
+
run: timedBash("uniffi_packaging", bindingCommands.join("\n")),
|
|
1270
|
+
},
|
|
1271
|
+
]
|
|
1272
|
+
: []),
|
|
1273
|
+
...(binaryCommands.length
|
|
1274
|
+
? [
|
|
1275
|
+
{
|
|
1276
|
+
name: "Package release binaries",
|
|
1277
|
+
shell: "bash",
|
|
1278
|
+
run: timedBash("binary_packaging", binaryCommands.join("\n")),
|
|
1279
|
+
},
|
|
1280
|
+
]
|
|
1281
|
+
: []),
|
|
1282
|
+
...rustArtifactSteps(plan),
|
|
1283
|
+
],
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function rustCargoPublishJob(plan: RustReleasePlan, local: boolean): Job {
|
|
1288
|
+
const registry = local ? '"${{ vars.LOCAL_CARGO_REGISTRY }}"' : "crates-io";
|
|
1289
|
+
return {
|
|
1290
|
+
if: local
|
|
1291
|
+
? "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}"
|
|
1292
|
+
: "${{ github.event_name == 'push' }}",
|
|
1293
|
+
needs: ["verify-context", "rust-build"],
|
|
1294
|
+
runsOn: [local ? "self-hosted" : "ubuntu-latest"],
|
|
1295
|
+
permissions: { contents: JobPermission.READ },
|
|
1296
|
+
steps: [
|
|
1297
|
+
...releaseSourceSteps(),
|
|
1298
|
+
{
|
|
1299
|
+
name: "Setup Rust",
|
|
1300
|
+
uses: `dtolnay/rust-toolchain@${plan.releaseRustVersion}`,
|
|
1024
1301
|
},
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
|
|
1032
|
-
with: { targets: "${{ matrix.cargo }}" },
|
|
1302
|
+
{
|
|
1303
|
+
name: local ? "Publish Cargo crates locally" : "Publish public crates",
|
|
1304
|
+
env: {
|
|
1305
|
+
CARGO_REGISTRY_TOKEN: local
|
|
1306
|
+
? "${{ secrets.LOCAL_CARGO_TOKEN }}"
|
|
1307
|
+
: "${{ secrets.CARGO_REGISTRY_TOKEN }}",
|
|
1033
1308
|
},
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
"sudo apt-get install --yes libdbus-1-dev pkg-config",
|
|
1057
|
-
].join("\n"),
|
|
1309
|
+
run: plan.publicCrates
|
|
1310
|
+
.map((crate) => `cargo publish --package "${crate}" --registry ${registry} --no-verify`)
|
|
1311
|
+
.join("\n"),
|
|
1312
|
+
},
|
|
1313
|
+
],
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function rustGitHubReleaseJob(): Job {
|
|
1318
|
+
return {
|
|
1319
|
+
if: "${{ github.event_name == 'push' }}",
|
|
1320
|
+
needs: ["verify-context", "rust-build"],
|
|
1321
|
+
runsOn: ["ubuntu-latest"],
|
|
1322
|
+
permissions: { contents: JobPermission.WRITE },
|
|
1323
|
+
steps: [
|
|
1324
|
+
{
|
|
1325
|
+
name: "Download release binaries",
|
|
1326
|
+
uses: "actions/download-artifact@v8",
|
|
1327
|
+
with: {
|
|
1328
|
+
pattern: "*-binary",
|
|
1329
|
+
path: "dist/rust-release",
|
|
1330
|
+
"merge-multiple": true,
|
|
1058
1331
|
},
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
`cargo build --release --workspace${usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"${
|
|
1069
|
-
hasReleaseExclusions ? " ${{ matrix.cargoExcludes }}" : ""
|
|
1070
|
-
}`,
|
|
1071
|
-
),
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
name: "Publish GitHub release assets",
|
|
1335
|
+
uses: "softprops/action-gh-release@v2",
|
|
1336
|
+
with: {
|
|
1337
|
+
files: "dist/rust-release/*",
|
|
1338
|
+
"generate-release-notes": true,
|
|
1339
|
+
tag_name: RELEASE_TAG,
|
|
1340
|
+
target_commitish: RELEASE_SHA,
|
|
1072
1341
|
},
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1342
|
+
},
|
|
1343
|
+
],
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function rustNativeNpmPublishJob(project: DBXToolsJavaScriptProject): Job {
|
|
1348
|
+
return {
|
|
1349
|
+
if: "${{ always() && needs.verify-context.result == 'success' && needs.rust-build.result != 'failure' && needs.rust-build.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
|
|
1350
|
+
needs: ["verify-context", "rust-build"],
|
|
1351
|
+
runsOn: ["ubuntu-latest"],
|
|
1352
|
+
permissions: {
|
|
1353
|
+
actions: JobPermission.READ,
|
|
1354
|
+
contents: JobPermission.READ,
|
|
1355
|
+
idToken: JobPermission.WRITE,
|
|
1356
|
+
},
|
|
1357
|
+
timeoutMinutes: 15,
|
|
1358
|
+
env: { BUN_VERSION, CI: "true" },
|
|
1359
|
+
steps: [
|
|
1360
|
+
...nodeReleaseSetupSteps(project),
|
|
1361
|
+
...releaseArtifactSteps({
|
|
1362
|
+
currentName: "Download native npm packages",
|
|
1363
|
+
recoveredName: "Download recovered native npm packages",
|
|
1364
|
+
pattern: "*-npm",
|
|
1365
|
+
path: "dist/uniffi/native",
|
|
1366
|
+
}),
|
|
1367
|
+
{
|
|
1368
|
+
name: "Publish native npm packages",
|
|
1369
|
+
env: { RELEASE_VERSION, ...npmPublishEnvironment() },
|
|
1370
|
+
run: 'bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory dist/uniffi/native --version "$RELEASE_VERSION" $DRY_RUN',
|
|
1371
|
+
},
|
|
1372
|
+
],
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function rustNodeFacadePublishJob(
|
|
1377
|
+
project: DBXToolsJavaScriptProject,
|
|
1378
|
+
bindings: readonly RustReleaseBinding[],
|
|
1379
|
+
): Job {
|
|
1380
|
+
return {
|
|
1381
|
+
if: releaseStageCondition("node"),
|
|
1382
|
+
needs: ["verify-context", "publish-node"],
|
|
1383
|
+
runsOn: ["ubuntu-latest"],
|
|
1384
|
+
permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
|
|
1385
|
+
timeoutMinutes: 30,
|
|
1386
|
+
env: { BUN_VERSION, CI: "true" },
|
|
1387
|
+
steps: [
|
|
1388
|
+
...nodeReleaseSetupSteps(project),
|
|
1389
|
+
{
|
|
1390
|
+
name: "Build and publish UniFFI npm facades",
|
|
1391
|
+
env: { RELEASE_VERSION, ...npmPublishEnvironment() },
|
|
1392
|
+
run: bindings
|
|
1393
|
+
.flatMap((binding) => {
|
|
1394
|
+
const output = `dist/uniffi/facades/${binding.crate}`;
|
|
1395
|
+
return [
|
|
1396
|
+
`node .projen/uniffi-release.mjs facade --node "${binding.node}" --node-package "${binding.nodePackage}" --node-triple "linux-x64-gnu" --version "$RELEASE_VERSION" --output "${output}"`,
|
|
1397
|
+
`bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory "${output}/npm-facade" --version "$RELEASE_VERSION" $DRY_RUN`,
|
|
1398
|
+
];
|
|
1399
|
+
})
|
|
1400
|
+
.join("\n"),
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
name: "Smoke test published UniFFI npm facades",
|
|
1404
|
+
if: "${{ github.event_name == 'push' && vars.UNIFFI_FACADE_SMOKE == 'true' }}",
|
|
1405
|
+
continueOnError: true,
|
|
1406
|
+
env: { RELEASE_VERSION },
|
|
1407
|
+
run: [
|
|
1408
|
+
'SMOKE_DIR="$(mktemp -d)"',
|
|
1409
|
+
"trap 'rm -rf \"$SMOKE_DIR\"' EXIT",
|
|
1410
|
+
'cd "$SMOKE_DIR"',
|
|
1411
|
+
"npm init --yes >/dev/null",
|
|
1412
|
+
...bindings.flatMap((binding) => [
|
|
1413
|
+
`npm install --ignore-scripts --no-audit --no-fund --package-lock=false "${binding.nodePackage}@$RELEASE_VERSION"`,
|
|
1414
|
+
`node -e 'import("${binding.nodePackage}")'`,
|
|
1415
|
+
]),
|
|
1416
|
+
].join("\n"),
|
|
1417
|
+
},
|
|
1418
|
+
],
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/** Generated Rust workspace plus convention-derived UniFFI binding packages. */
|
|
1423
|
+
export class DBXToolsRustWorkspace {
|
|
1424
|
+
readonly packages: readonly DBXToolsRustProject[];
|
|
1425
|
+
readonly nodePackages: readonly DBXToolsTypeScriptProject[];
|
|
1426
|
+
readonly pythonPackages: readonly PythonPackageOptions[];
|
|
1427
|
+
readonly bindingMappings: readonly RustBindingMapping[];
|
|
1428
|
+
readonly releaseBinaries: readonly RustReleaseBinaryMapping[];
|
|
1429
|
+
readonly workspaceMapping: RustWorkspaceMapping;
|
|
1430
|
+
|
|
1431
|
+
constructor(project: javascript.NodeProject, options: DBXToolsRustWorkspaceOptions) {
|
|
1432
|
+
const resolved = resolveRustWorkspaceOptions(project, options);
|
|
1433
|
+
this.packages = createRustPackages(project, resolved);
|
|
1434
|
+
const packageDependencies = createRustPackageDependencyResolver(
|
|
1435
|
+
this.packages,
|
|
1436
|
+
project,
|
|
1437
|
+
options,
|
|
1438
|
+
);
|
|
1439
|
+
this.releaseBinaries = planRustReleaseBinaries(this.packages, resolved);
|
|
1440
|
+
configureRustCliRegistry(project, options.cliRegistryPath, this.releaseBinaries);
|
|
1441
|
+
|
|
1442
|
+
const bindingPlan = planRustBindings(this.packages, packageDependencies, resolved);
|
|
1443
|
+
validateRustReleaseGraph(this.packages, packageDependencies);
|
|
1444
|
+
this.bindingMappings = bindingPlan.mappings;
|
|
1445
|
+
configureRustBindingFiles(bindingPlan, resolved);
|
|
1446
|
+
const releaseEnabled =
|
|
1447
|
+
resolved.release &&
|
|
1448
|
+
(this.bindingMappings.length > 0 ||
|
|
1449
|
+
this.packages.some((pkg) => pkg.packageOptions.release || !pkg.packageOptions.private));
|
|
1450
|
+
this.workspaceMapping = createRustWorkspaceMapping(
|
|
1451
|
+
this.packages,
|
|
1452
|
+
this.bindingMappings,
|
|
1453
|
+
this.releaseBinaries,
|
|
1454
|
+
resolved,
|
|
1455
|
+
);
|
|
1456
|
+
if (isDBXToolsJavaScriptProject()(project)) {
|
|
1457
|
+
project.dbxToolsConfig.rust = this.workspaceMapping;
|
|
1458
|
+
}
|
|
1459
|
+
configureRustBindingIgnores(project, this.bindingMappings);
|
|
1460
|
+
this.pythonPackages = planPythonBindingPackages(bindingPlan, resolved);
|
|
1461
|
+
this.nodePackages = createRustNodeBindingPackages(
|
|
1462
|
+
project,
|
|
1463
|
+
planNodeBindingPackages(project, bindingPlan, resolved),
|
|
1464
|
+
);
|
|
1465
|
+
configureRustWorkspaceFiles(project, this.packages, options, resolved);
|
|
1466
|
+
configureRustWorkspaceTasks(project);
|
|
1467
|
+
if (releaseEnabled) {
|
|
1468
|
+
this.addReleaseWorkflow(project, options, resolved.nativeTargets);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private addReleaseWorkflow(
|
|
1473
|
+
project: javascript.NodeProject,
|
|
1474
|
+
options: DBXToolsRustWorkspaceOptions,
|
|
1475
|
+
targets: readonly UniFFIReleaseTarget[],
|
|
1476
|
+
): void {
|
|
1477
|
+
if (!project.github || !isDBXToolsJavaScriptProject()(project)) return;
|
|
1478
|
+
const plan = planRustRelease(project, options, targets, this.packages, this.bindingMappings);
|
|
1479
|
+
configureRustReleaseTask(project, plan);
|
|
1095
1480
|
const workflow = releaseWorkflow(project);
|
|
1096
|
-
if (hasTargetOutputs &&
|
|
1097
|
-
workflow.addJob("rust-build",
|
|
1481
|
+
if (plan.hasTargetOutputs && plan.targets.length) {
|
|
1482
|
+
workflow.addJob("rust-build", rustBuildJob(plan));
|
|
1098
1483
|
}
|
|
1099
|
-
if (publicCrates.length) {
|
|
1100
|
-
workflow.addJob("publish-cargo",
|
|
1101
|
-
|
|
1102
|
-
needs: ["verify-context", "rust-build"],
|
|
1103
|
-
runsOn: ["ubuntu-latest"],
|
|
1104
|
-
permissions: { contents: JobPermission.READ },
|
|
1105
|
-
steps: [
|
|
1106
|
-
...releaseSourceSteps(),
|
|
1107
|
-
{
|
|
1108
|
-
name: "Setup Rust",
|
|
1109
|
-
uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
|
|
1110
|
-
},
|
|
1111
|
-
{
|
|
1112
|
-
name: "Publish public crates",
|
|
1113
|
-
env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" },
|
|
1114
|
-
run: publicCrates
|
|
1115
|
-
.map((crate) => `cargo publish --package "${crate}" --registry crates-io --no-verify`)
|
|
1116
|
-
.join("\n"),
|
|
1117
|
-
},
|
|
1118
|
-
],
|
|
1119
|
-
});
|
|
1120
|
-
workflow.addJob("publish-local-cargo", {
|
|
1121
|
-
if: "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}",
|
|
1122
|
-
needs: ["verify-context", "rust-build"],
|
|
1123
|
-
runsOn: ["self-hosted"],
|
|
1124
|
-
permissions: { contents: JobPermission.READ },
|
|
1125
|
-
steps: [
|
|
1126
|
-
...releaseSourceSteps(),
|
|
1127
|
-
{
|
|
1128
|
-
name: "Setup Rust",
|
|
1129
|
-
uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
|
|
1130
|
-
},
|
|
1131
|
-
{
|
|
1132
|
-
name: "Publish Cargo crates locally",
|
|
1133
|
-
env: { CARGO_REGISTRY_TOKEN: "${{ secrets.LOCAL_CARGO_TOKEN }}" },
|
|
1134
|
-
run: publicCrates
|
|
1135
|
-
.map(
|
|
1136
|
-
(crate) =>
|
|
1137
|
-
`cargo publish --package "${crate}" --registry "\${{ vars.LOCAL_CARGO_REGISTRY }}" --no-verify`,
|
|
1138
|
-
)
|
|
1139
|
-
.join("\n"),
|
|
1140
|
-
},
|
|
1141
|
-
],
|
|
1142
|
-
});
|
|
1484
|
+
if (plan.publicCrates.length) {
|
|
1485
|
+
workflow.addJob("publish-cargo", rustCargoPublishJob(plan, false));
|
|
1486
|
+
workflow.addJob("publish-local-cargo", rustCargoPublishJob(plan, true));
|
|
1143
1487
|
}
|
|
1144
|
-
if (releaseBinaries.length) {
|
|
1145
|
-
workflow.addJob("publish-github-release",
|
|
1146
|
-
if: "${{ github.event_name == 'push' }}",
|
|
1147
|
-
needs: ["verify-context", "rust-build"],
|
|
1148
|
-
runsOn: ["ubuntu-latest"],
|
|
1149
|
-
permissions: { contents: JobPermission.WRITE },
|
|
1150
|
-
steps: [
|
|
1151
|
-
{
|
|
1152
|
-
name: "Download release binaries",
|
|
1153
|
-
uses: "actions/download-artifact@v8",
|
|
1154
|
-
with: {
|
|
1155
|
-
pattern: "*-binary",
|
|
1156
|
-
path: "dist/rust-release",
|
|
1157
|
-
"merge-multiple": true,
|
|
1158
|
-
},
|
|
1159
|
-
},
|
|
1160
|
-
{
|
|
1161
|
-
name: "Publish GitHub release assets",
|
|
1162
|
-
uses: "softprops/action-gh-release@v2",
|
|
1163
|
-
with: {
|
|
1164
|
-
files: "dist/rust-release/*",
|
|
1165
|
-
"generate-release-notes": true,
|
|
1166
|
-
tag_name: RELEASE_TAG,
|
|
1167
|
-
target_commitish: RELEASE_SHA,
|
|
1168
|
-
},
|
|
1169
|
-
},
|
|
1170
|
-
],
|
|
1171
|
-
});
|
|
1488
|
+
if (plan.releaseBinaries.length) {
|
|
1489
|
+
workflow.addJob("publish-github-release", rustGitHubReleaseJob());
|
|
1172
1490
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
if (nodeBindings.length && hasNodeRelease(project)) {
|
|
1176
|
-
workflow.addJob("publish-native-npm", {
|
|
1177
|
-
if: "${{ always() && needs.verify-context.result == 'success' && needs.rust-build.result != 'failure' && needs.rust-build.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
|
|
1178
|
-
needs: ["verify-context", "rust-build"],
|
|
1179
|
-
runsOn: ["ubuntu-latest"],
|
|
1180
|
-
permissions: {
|
|
1181
|
-
actions: JobPermission.READ,
|
|
1182
|
-
contents: JobPermission.READ,
|
|
1183
|
-
idToken: JobPermission.WRITE,
|
|
1184
|
-
},
|
|
1185
|
-
timeoutMinutes: 15,
|
|
1186
|
-
env: { BUN_VERSION, CI: "true" },
|
|
1187
|
-
steps: [
|
|
1188
|
-
...nodeReleaseSetupSteps(project),
|
|
1189
|
-
...releaseArtifactSteps({
|
|
1190
|
-
currentName: "Download native npm packages",
|
|
1191
|
-
recoveredName: "Download recovered native npm packages",
|
|
1192
|
-
pattern: "*-npm",
|
|
1193
|
-
path: "dist/uniffi/native",
|
|
1194
|
-
}),
|
|
1195
|
-
{
|
|
1196
|
-
name: "Publish native npm packages",
|
|
1197
|
-
env: { RELEASE_VERSION, ...npmPublishEnvironment() },
|
|
1198
|
-
run: 'bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory dist/uniffi/native --version "$RELEASE_VERSION" $DRY_RUN',
|
|
1199
|
-
},
|
|
1200
|
-
],
|
|
1201
|
-
});
|
|
1491
|
+
if (plan.nodeBindings.length && hasNodeRelease(project)) {
|
|
1492
|
+
workflow.addJob("publish-native-npm", rustNativeNpmPublishJob(project));
|
|
1202
1493
|
const nodeJob = workflow.getJob("publish-node");
|
|
1203
1494
|
if ("uses" in nodeJob) throw new Error("publish-node must be a workflow job");
|
|
1204
1495
|
workflow.updateJob("publish-node", {
|
|
@@ -1206,46 +1497,7 @@ export class DBXToolsRustWorkspace {
|
|
|
1206
1497
|
if: "${{ always() && needs.verify-context.result == 'success' && needs.publish-native-npm.result == 'success' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
|
|
1207
1498
|
needs: ["verify-context", "publish-native-npm"],
|
|
1208
1499
|
});
|
|
1209
|
-
workflow.addJob("publish-node-facades",
|
|
1210
|
-
if: releaseStageCondition("node"),
|
|
1211
|
-
needs: ["verify-context", "publish-node"],
|
|
1212
|
-
runsOn: ["ubuntu-latest"],
|
|
1213
|
-
permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
|
|
1214
|
-
timeoutMinutes: 30,
|
|
1215
|
-
env: { BUN_VERSION, CI: "true" },
|
|
1216
|
-
steps: [
|
|
1217
|
-
...nodeReleaseSetupSteps(project),
|
|
1218
|
-
{
|
|
1219
|
-
name: "Build and publish UniFFI npm facades",
|
|
1220
|
-
env: { RELEASE_VERSION, ...npmPublishEnvironment() },
|
|
1221
|
-
run: nodeBindings
|
|
1222
|
-
.flatMap((binding) => {
|
|
1223
|
-
const output = `dist/uniffi/facades/${binding.crate}`;
|
|
1224
|
-
return [
|
|
1225
|
-
`node .projen/uniffi-release.mjs facade --node "${binding.node}" --node-package "${binding.nodePackage}" --node-triple "linux-x64-gnu" --version "$RELEASE_VERSION" --output "${output}"`,
|
|
1226
|
-
`bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory "${output}/npm-facade" --version "$RELEASE_VERSION" $DRY_RUN`,
|
|
1227
|
-
];
|
|
1228
|
-
})
|
|
1229
|
-
.join("\n"),
|
|
1230
|
-
},
|
|
1231
|
-
{
|
|
1232
|
-
name: "Smoke test published UniFFI npm facades",
|
|
1233
|
-
if: "${{ github.event_name == 'push' && vars.UNIFFI_FACADE_SMOKE == 'true' }}",
|
|
1234
|
-
continueOnError: true,
|
|
1235
|
-
env: { RELEASE_VERSION },
|
|
1236
|
-
run: [
|
|
1237
|
-
'SMOKE_DIR="$(mktemp -d)"',
|
|
1238
|
-
"trap 'rm -rf \"$SMOKE_DIR\"' EXIT",
|
|
1239
|
-
'cd "$SMOKE_DIR"',
|
|
1240
|
-
"npm init --yes >/dev/null",
|
|
1241
|
-
...nodeBindings.flatMap((binding) => [
|
|
1242
|
-
`npm install --ignore-scripts --no-audit --no-fund --package-lock=false "${binding.nodePackage}@$RELEASE_VERSION"`,
|
|
1243
|
-
`node -e 'import("${binding.nodePackage}")'`,
|
|
1244
|
-
]),
|
|
1245
|
-
].join("\n"),
|
|
1246
|
-
},
|
|
1247
|
-
],
|
|
1248
|
-
});
|
|
1500
|
+
workflow.addJob("publish-node-facades", rustNodeFacadePublishJob(project, plan.nodeBindings));
|
|
1249
1501
|
}
|
|
1250
1502
|
}
|
|
1251
1503
|
}
|