@dbx-tools/projen 0.6.163 → 0.6.168
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -4
- package/index.ts +1 -1
- package/package.json +4 -4
- package/src/project-js.ts +19 -22
- package/src/project-py.ts +10 -7
- package/src/project-rs.ts +159 -39
- package/src/uniffi.ts +8 -0
- package/tasks/rust.ts +17 -3
- package/tasks/uniffi-release.mjs +138 -6
- package/tasks/uniffi.ts +83 -53
package/README.md
CHANGED
|
@@ -96,10 +96,29 @@ derives the crate name and repository from the parent project. A crate containin
|
|
|
96
96
|
automatically wires matching private Node and Python binding packages using the
|
|
97
97
|
same capability name. Repository-specific dependencies and features remain
|
|
98
98
|
declarative options in `.projenrc.ts`; generated bindings are built separately
|
|
99
|
-
from projen synthesis.
|
|
99
|
+
from projen synthesis. Target-independent Node binding TypeScript is committed
|
|
100
|
+
and remains generated/read-only, while native libraries stay ignored. Node
|
|
101
|
+
facades compile to `lib/` and publish JavaScript entry points that plain Node
|
|
102
|
+
can load from `node_modules`. The generated barrel exports the binding API from
|
|
103
|
+
the package root. Do not create a `nodeExports` binding subpath or a
|
|
104
|
+
handwritten type facade.
|
|
105
|
+
|
|
106
|
+
Rust dependencies between binding-enabled workspace crates become Node
|
|
107
|
+
`workspace:*` and Python `internalDependencies` automatically. Python generation
|
|
108
|
+
selects the owning crate and supplies `external_packages` imports. UBRN currently
|
|
109
|
+
emits every linked component; the generation task discards dependency outputs
|
|
110
|
+
and rewrites their imports to the owning package root, including its generated
|
|
111
|
+
`uniffiModule` converter table. It never copies a dependency's records or enums.
|
|
112
|
+
Foreign callback adapters must first be wrapped in an object created by their
|
|
113
|
+
own native library (`createStorageHandle` for auth), because callback registries
|
|
114
|
+
are library-local. Release wheels replace sibling Git requirements with matching
|
|
115
|
+
native-wheel versions, and dependent publishers wait for their dependencies.
|
|
116
|
+
Each crate builds its own `<crate>-uniffi-bindgen` executable so a workspace
|
|
117
|
+
build has no colliding binary outputs. Packaging runs that prebuilt executable.
|
|
100
118
|
|
|
101
119
|
`sync --watch` runs a focused Rust watcher beside the OpenAPI watcher. Changes
|
|
102
|
-
inside an existing UniFFI crate regenerate
|
|
120
|
+
inside an existing UniFFI crate regenerate that crate and its dependent bindings
|
|
121
|
+
in dependency order; adding or
|
|
103
122
|
removing a crate or `setup_scaffolding!()` marker triggers a full synth. Repos
|
|
104
123
|
without Rust crates start no Rust watcher. When Rust projects are detected,
|
|
105
124
|
Cargo is required and the focused task fails immediately if it is unavailable.
|
|
@@ -133,11 +152,17 @@ and the UBRN Cargo build. Release packaging passes the cached executable directl
|
|
|
133
152
|
to the Node binding generator and keeps the package barrel copied from source,
|
|
134
153
|
so it does not need the installed projen dependency graph. A miss saves the
|
|
135
154
|
validated executable immediately, before workspace build and packaging can fail.
|
|
136
|
-
Bun runtime setup still runs
|
|
155
|
+
Bun runtime setup still runs in the single facade row because the binding
|
|
156
|
+
generator and facade bundle are TypeScript. The bundle points its type surface
|
|
157
|
+
at the committed generated source and needs no standalone TypeScript compiler
|
|
158
|
+
or workspace install on a UBRN cache hit. Non-facade rows skip the complete
|
|
159
|
+
Bun/UBRN path.
|
|
160
|
+
Stable Windows rows verify and use the hosted runner's installed Rust toolchain
|
|
161
|
+
and select `rust-lld` for the workspace build. Cargo registry caches
|
|
137
162
|
and the `SCCACHE_GHA_VERSION` namespace stay stable per target/toolchain across
|
|
138
163
|
version tags. Cache keys, restore results, sccache statistics, and phase timings
|
|
139
164
|
are written to each build log. Python generation executes the already-built
|
|
140
|
-
`target/<triple>/release
|
|
165
|
+
`target/<triple>/release/<crate>-uniffi-bindgen` directly. Artifact packaging therefore
|
|
141
166
|
does no Rust compilation after the main workspace build.
|
|
142
167
|
|
|
143
168
|
Artifacts identify their crate, target, and type. Download-only publication
|
package/index.ts
CHANGED
|
@@ -58,7 +58,7 @@ export { DOWNSTREAM_RELEASE_EVENT, RUST_RELEASE_EVENT, RELEASE_TAG, RELEASE_SHA
|
|
|
58
58
|
export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
|
|
59
59
|
export type { PackageTag } from "./src/tags.ts";
|
|
60
60
|
export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
|
|
61
|
-
export { makeDefaultedInterfaceParametersOptional, addExplicitInterfaceReexports } from "./src/uniffi.ts";
|
|
61
|
+
export { makeDefaultedInterfaceParametersOptional, addTypeScriptExtensionsToBindingImports, addExplicitInterfaceReexports } from "./src/uniffi.ts";
|
|
62
62
|
export type { TypeScriptBindingModule } from "./src/uniffi.ts";
|
|
63
63
|
export { DBXToolsVsCode } from "./src/vscode.ts";
|
|
64
64
|
export type { IgnoreGroupOptions } from "./src/watch.ts";
|
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.
|
|
30
|
-
"@dbx-tools/path": "0.6.
|
|
31
|
-
"@dbx-tools/shared-core": "0.6.
|
|
29
|
+
"@dbx-tools/core": "0.6.168",
|
|
30
|
+
"@dbx-tools/path": "0.6.168",
|
|
31
|
+
"@dbx-tools/shared-core": "0.6.168",
|
|
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.
|
|
51
|
+
"version": "0.6.168",
|
|
52
52
|
"types": "index.ts",
|
|
53
53
|
"type": "module",
|
|
54
54
|
"exports": {
|
package/src/project-js.ts
CHANGED
|
@@ -446,13 +446,13 @@ function defaultProjectOptions(
|
|
|
446
446
|
devDeps: ["@types/node@^24.6.0"],
|
|
447
447
|
...(isRoot
|
|
448
448
|
? {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
449
|
+
prettier: true,
|
|
450
|
+
prettierOptions: {
|
|
451
|
+
settings: PRETTIER_SETTINGS,
|
|
452
|
+
ignoreFile: true,
|
|
453
|
+
ignoreFileOptions: { ignorePatterns: [...ignore.ignorePatterns({ test: false })] },
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
456
|
: {}),
|
|
457
457
|
...options,
|
|
458
458
|
...copiedGitIgnoreOptions(options),
|
|
@@ -495,24 +495,23 @@ function defaultTypeScriptProjectOptions(
|
|
|
495
495
|
// ESLint is configured once on the ROOT (see initProject) and lints the whole
|
|
496
496
|
// tree, so packages don't emit their own config. A caller can still override.
|
|
497
497
|
eslint: false,
|
|
498
|
-
devDeps: [...(base.devDeps ?? []), "typescript@^5.9.3",
|
|
498
|
+
devDeps: [...(base.devDeps ?? []), "typescript@^5.9.3", `@types/bun@${BUN_VERSION}`],
|
|
499
499
|
...options,
|
|
500
500
|
...copiedGitIgnoreOptions(options),
|
|
501
501
|
};
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
-
// Pinned to match the subproject defaults so bun resolves
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", "@types/bun@^1.3.14"];
|
|
504
|
+
// Pinned to match the subproject defaults so bun resolves one TypeScript and
|
|
505
|
+
// one runtime-compatible set of Bun globals across the workspace.
|
|
506
|
+
const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", `@types/bun@${BUN_VERSION}`];
|
|
508
507
|
|
|
509
508
|
/** Options for {@link DBXToolsNodeProject} (the monorepo root). */
|
|
510
509
|
export interface DBXToolsJavaScriptProjectOptions
|
|
511
510
|
extends
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
511
|
+
CommonProjectOptions,
|
|
512
|
+
Partial<javascript.NodeProjectOptions>,
|
|
513
|
+
DBXToolsConfigOptions,
|
|
514
|
+
DBXToolsPNPMWorkspaceOptions {
|
|
516
515
|
/**
|
|
517
516
|
* The npm scope for generated package names (`@<scope>/<seg-...>`). Defaults to
|
|
518
517
|
* the (resolved) project name; a leading `@` is optional.
|
|
@@ -593,8 +592,7 @@ export interface DBXToolsTypeScriptProjectOptions
|
|
|
593
592
|
*/
|
|
594
593
|
export class DBXToolsNodeProject
|
|
595
594
|
extends javascript.NodeProject
|
|
596
|
-
implements DBXToolsJavaScriptProject
|
|
597
|
-
{
|
|
595
|
+
implements DBXToolsJavaScriptProject {
|
|
598
596
|
readonly language = "javascript" as const;
|
|
599
597
|
readonly scope: string;
|
|
600
598
|
readonly dbxToolsConfig: DBXToolsConfig;
|
|
@@ -694,8 +692,8 @@ export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
|
|
|
694
692
|
installDependencies(trigger: unknown): void;
|
|
695
693
|
logInstallTrigger(trigger: unknown): void;
|
|
696
694
|
};
|
|
697
|
-
nodePackage.installDependencies = () => {};
|
|
698
|
-
nodePackage.logInstallTrigger = () => {};
|
|
695
|
+
nodePackage.installDependencies = () => { };
|
|
696
|
+
nodePackage.logInstallTrigger = () => { };
|
|
699
697
|
},
|
|
700
698
|
);
|
|
701
699
|
|
|
@@ -708,8 +706,7 @@ export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
|
|
|
708
706
|
*/
|
|
709
707
|
export class DBXToolsTypeScriptProject
|
|
710
708
|
extends typescript.TypeScriptProject
|
|
711
|
-
implements DBXToolsJavaScriptProject
|
|
712
|
-
{
|
|
709
|
+
implements DBXToolsJavaScriptProject {
|
|
713
710
|
readonly language = "javascript" as const;
|
|
714
711
|
readonly scope: string;
|
|
715
712
|
readonly dbxToolsConfig: DBXToolsConfig;
|
package/src/project-py.ts
CHANGED
|
@@ -683,7 +683,10 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
683
683
|
const linesBeforeAuthentication = [
|
|
684
684
|
"# PyPI Trusted Publisher Setup Instructions",
|
|
685
685
|
"",
|
|
686
|
-
"Use
|
|
686
|
+
"Use the system browser to audit and configure the PyPI trusted publishers below.",
|
|
687
|
+
"Do not use an in-app browser or embedded webview.",
|
|
688
|
+
"Do not visit GitHub or use the GitHub API or CLI. Every required GitHub owner, repository, workflow, environment, and branch value is provided below and is authoritative.",
|
|
689
|
+
"Use only PyPI pages for this task.",
|
|
687
690
|
"",
|
|
688
691
|
"## Audit and confirmation",
|
|
689
692
|
"",
|
|
@@ -691,7 +694,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
691
694
|
"",
|
|
692
695
|
"- Confirm that the active PyPI account can administer the listed projects and pending publishers.",
|
|
693
696
|
"- Start at https://pypi.org/manage/projects/ and determine which listed projects exist.",
|
|
694
|
-
"-
|
|
697
|
+
"- On PyPI, inspect every existing GitHub Actions publisher for each project and compare its owner, repository name, workflow name, and environment name with the desired values below.",
|
|
695
698
|
"- Identify duplicate and mismatched trusted publishers that must be replaced by removing the publisher entry and adding the correct one.",
|
|
696
699
|
"- For projects that do not exist, identify the pending publisher that must be created.",
|
|
697
700
|
"- Report the active account and a proposed reconciliation plan grouped by publishers that will be left unchanged, updated, replaced, created, or removed.",
|
|
@@ -700,9 +703,9 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
700
703
|
"",
|
|
701
704
|
"## GitHub environment policy",
|
|
702
705
|
"",
|
|
703
|
-
`-
|
|
704
|
-
"-
|
|
705
|
-
"-
|
|
706
|
+
`- The supplied branch policy value is ${releaseBranch}.`,
|
|
707
|
+
"- Do not inspect or configure GitHub environments during this task.",
|
|
708
|
+
"- GitHub environment administration is a separate task; use the supplied values only when comparing PyPI publisher entries.",
|
|
706
709
|
"",
|
|
707
710
|
"## Authentication",
|
|
708
711
|
"",
|
|
@@ -714,7 +717,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
714
717
|
"",
|
|
715
718
|
"## Efficient browser workflow",
|
|
716
719
|
"",
|
|
717
|
-
"- Reuse
|
|
720
|
+
"- Reuse an existing PyPI tab in the system browser when available.",
|
|
718
721
|
"- For an existing project, open its publisher page directly at https://pypi.org/manage/project/<project-name>/settings/publishing/.",
|
|
719
722
|
"- For a project that does not exist, use the pending-publisher page at https://pypi.org/manage/account/publishing/.",
|
|
720
723
|
"- Treat the publisher table shown after submission as authoritative confirmation of success, even if the navigation header unexpectedly appears signed out.",
|
|
@@ -769,7 +772,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
769
772
|
],
|
|
770
773
|
});
|
|
771
774
|
project.root.addTask("pypiTrustedPublisherInstructions", {
|
|
772
|
-
description: "Print browser
|
|
775
|
+
description: "Print system-browser instructions for PyPI trusted publishers",
|
|
773
776
|
exec: `node ${helper}`,
|
|
774
777
|
receiveArgs: true,
|
|
775
778
|
});
|
package/src/project-rs.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface CargoDependencyOptions {
|
|
|
28
28
|
readonly workspace?: boolean;
|
|
29
29
|
readonly path?: string;
|
|
30
30
|
readonly optional?: boolean;
|
|
31
|
+
readonly package?: string;
|
|
31
32
|
readonly defaultFeatures?: boolean;
|
|
32
33
|
readonly features?: readonly string[];
|
|
33
34
|
}
|
|
@@ -156,6 +157,8 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
|
|
|
156
157
|
] as const;
|
|
157
158
|
|
|
158
159
|
const RUST_CACHE_ENV = {
|
|
160
|
+
CARGO_INCREMENTAL: "0",
|
|
161
|
+
CARGO_TERM_COLOR: "always",
|
|
159
162
|
RUSTC_WRAPPER: "sccache",
|
|
160
163
|
SCCACHE_GHA_ENABLED: "true",
|
|
161
164
|
} as const;
|
|
@@ -203,8 +206,9 @@ function releaseTargets(options: DBXToolsRustWorkspaceOptions): readonly UniFFIR
|
|
|
203
206
|
const target = UNIFFI_RELEASE_TARGETS.find(
|
|
204
207
|
(candidate) => candidate.os === platform.os && candidate.cpu === platform.cpu,
|
|
205
208
|
);
|
|
206
|
-
if (!target)
|
|
209
|
+
if (!target) {
|
|
207
210
|
throw new Error(`Unsupported Rust release platform: ${platform.os}-${platform.cpu}`);
|
|
211
|
+
}
|
|
208
212
|
return target;
|
|
209
213
|
});
|
|
210
214
|
}
|
|
@@ -247,6 +251,7 @@ export interface RustBindingMapping {
|
|
|
247
251
|
readonly pythonPackage?: string;
|
|
248
252
|
readonly pythonModule?: string;
|
|
249
253
|
readonly facadeTarget?: boolean;
|
|
254
|
+
readonly dependencies?: readonly string[];
|
|
250
255
|
}
|
|
251
256
|
|
|
252
257
|
/** Persisted Rust workspace state consumed by `sync --watch`. */
|
|
@@ -257,6 +262,28 @@ export interface RustWorkspaceMapping {
|
|
|
257
262
|
readonly releaseWorkflow?: string;
|
|
258
263
|
}
|
|
259
264
|
|
|
265
|
+
export function orderRustBindings(bindings: readonly RustBindingMapping[]): RustBindingMapping[] {
|
|
266
|
+
const ordered: RustBindingMapping[] = [];
|
|
267
|
+
const visiting = new Set<string>();
|
|
268
|
+
const completed = new Set<string>();
|
|
269
|
+
const visit = (binding: RustBindingMapping): void => {
|
|
270
|
+
if (completed.has(binding.crate)) return;
|
|
271
|
+
if (visiting.has(binding.crate))
|
|
272
|
+
throw new Error(`Cyclic Rust binding dependency: ${binding.crate}`);
|
|
273
|
+
visiting.add(binding.crate);
|
|
274
|
+
for (const name of binding.dependencies ?? []) {
|
|
275
|
+
const dependency = bindings.find((candidate) => candidate.crate === name);
|
|
276
|
+
if (!dependency) throw new Error(`Missing Rust binding dependency: ${name}`);
|
|
277
|
+
visit(dependency);
|
|
278
|
+
}
|
|
279
|
+
visiting.delete(binding.crate);
|
|
280
|
+
completed.add(binding.crate);
|
|
281
|
+
ordered.push(binding);
|
|
282
|
+
};
|
|
283
|
+
for (const binding of bindings) visit(binding);
|
|
284
|
+
return ordered;
|
|
285
|
+
}
|
|
286
|
+
|
|
260
287
|
function rustSources(directory: string): string[] {
|
|
261
288
|
if (!existsSync(directory)) return [];
|
|
262
289
|
const files: string[] = [];
|
|
@@ -294,6 +321,7 @@ function cargoDependency(
|
|
|
294
321
|
...(!value.version && value.path && workspaceVersion ? { version: workspaceVersion } : {}),
|
|
295
322
|
...(value.workspace ? { workspace: true } : {}),
|
|
296
323
|
...(value.path ? { path: value.path } : {}),
|
|
324
|
+
...(value.package ? { package: value.package } : {}),
|
|
297
325
|
...(value.optional ? { optional: true } : {}),
|
|
298
326
|
...(value.defaultFeatures === false ? { "default-features": false } : {}),
|
|
299
327
|
...(value.features?.length ? { features: [...value.features] } : {}),
|
|
@@ -366,7 +394,7 @@ export class DBXToolsRustProject extends Project implements DBXToolsProject {
|
|
|
366
394
|
...(this.uniffi || binary
|
|
367
395
|
? {
|
|
368
396
|
bin: this.uniffi
|
|
369
|
-
? { name:
|
|
397
|
+
? { name: `${crateName}-uniffi-bindgen`, path: "uniffi-bindgen.rs" }
|
|
370
398
|
: { name: binaryName, path: "src/main.rs" },
|
|
371
399
|
}
|
|
372
400
|
: {}),
|
|
@@ -447,27 +475,75 @@ export class DBXToolsRustWorkspace {
|
|
|
447
475
|
);
|
|
448
476
|
|
|
449
477
|
const bindings = this.packages.filter((pkg) => pkg.uniffi);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
478
|
+
const bindingDependencies = (pkg: DBXToolsRustProject, language: "node" | "python") =>
|
|
479
|
+
bindings.filter(
|
|
480
|
+
(dependency) =>
|
|
481
|
+
dependency !== pkg &&
|
|
482
|
+
(dependency.packageOptions.bindings ?? ["node", "python"]).includes(language) &&
|
|
483
|
+
Object.entries(pkg.packageOptions.dependencies ?? {}).some(([name, value]) => {
|
|
484
|
+
const resolved =
|
|
485
|
+
typeof value === "object" && value.workspace
|
|
486
|
+
? (options.workspaceDependencies?.[name] ?? value)
|
|
487
|
+
: value;
|
|
488
|
+
return (
|
|
489
|
+
name === dependency.crateName ||
|
|
490
|
+
(typeof resolved === "object" &&
|
|
491
|
+
(resolved.package === dependency.crateName ||
|
|
492
|
+
(resolved.path !== undefined &&
|
|
493
|
+
resolve(
|
|
494
|
+
typeof value === "object" && value.workspace ? project.outdir : pkg.outdir,
|
|
495
|
+
resolved.path,
|
|
496
|
+
) === dependency.outdir)))
|
|
497
|
+
);
|
|
498
|
+
}),
|
|
499
|
+
);
|
|
500
|
+
this.bindingMappings = orderRustBindings(
|
|
501
|
+
bindings.map((pkg) => {
|
|
502
|
+
const targets = pkg.packageOptions.bindings ?? ["node", "python"];
|
|
503
|
+
const packageName = pkg.packageOptions.directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
504
|
+
const dependencies = [
|
|
505
|
+
...new Set([...bindingDependencies(pkg, "node"), ...bindingDependencies(pkg, "python")]),
|
|
506
|
+
].map((dependency) => dependency.crateName);
|
|
507
|
+
return {
|
|
508
|
+
crate: pkg.crateName,
|
|
509
|
+
...(dependencies.length ? { dependencies } : {}),
|
|
510
|
+
rust: `${root}/${pkg.packageOptions.directory}`,
|
|
511
|
+
...(targets.includes("node")
|
|
512
|
+
? {
|
|
513
|
+
node: `${nodeRoot}/${pkg.packageOptions.directory}`,
|
|
514
|
+
nodePackage: `@${scope}/${packageName}`,
|
|
515
|
+
}
|
|
516
|
+
: {}),
|
|
517
|
+
...(targets.includes("python")
|
|
518
|
+
? {
|
|
519
|
+
python: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`,
|
|
520
|
+
pythonPackage: pkg.crateName,
|
|
521
|
+
pythonModule: pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory),
|
|
522
|
+
}
|
|
523
|
+
: {}),
|
|
524
|
+
};
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
for (const pkg of bindings) {
|
|
528
|
+
const dependencies = bindingDependencies(pkg, "python");
|
|
529
|
+
if (dependencies.length === 0) continue;
|
|
530
|
+
pkg.tryRemoveFile("uniffi.toml");
|
|
531
|
+
new TextFile(pkg, "uniffi.toml", {
|
|
532
|
+
lines: renderToml({
|
|
533
|
+
"bindings.python": { cdylib_name: pkg.crateName.replaceAll("-", "_") },
|
|
534
|
+
"bindings.typescript": { strictTypeChecking: true },
|
|
535
|
+
...pkg.packageOptions.uniffiConfig,
|
|
536
|
+
"bindings.python.external_packages": Object.fromEntries(
|
|
537
|
+
dependencies.map((dependency) => [
|
|
538
|
+
dependency.crateName.replaceAll("-", "_"),
|
|
539
|
+
`${pythonModuleName(pythonModulePrefix, dependency.packageOptions.directory)}.bindings`,
|
|
540
|
+
]),
|
|
541
|
+
),
|
|
542
|
+
})
|
|
543
|
+
.trimEnd()
|
|
544
|
+
.split("\n"),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
471
547
|
const releaseEnabled =
|
|
472
548
|
(options.release ?? true) &&
|
|
473
549
|
(this.bindingMappings.length > 0 ||
|
|
@@ -484,13 +560,7 @@ export class DBXToolsRustWorkspace {
|
|
|
484
560
|
project.gitignore.addPatterns(
|
|
485
561
|
"target/",
|
|
486
562
|
...this.bindingMappings.flatMap((binding) => [
|
|
487
|
-
...(binding.node
|
|
488
|
-
? [
|
|
489
|
-
`${binding.node}/src/bindings.ts`,
|
|
490
|
-
`${binding.node}/src/_bindings*.ts`,
|
|
491
|
-
`${binding.node}/src/*${binding.crate.replaceAll("-", "_")}.*`,
|
|
492
|
-
]
|
|
493
|
-
: []),
|
|
563
|
+
...(binding.node ? [`${binding.node}/src/*${binding.crate.replaceAll("-", "_")}.*`] : []),
|
|
494
564
|
...(binding.python && binding.pythonModule
|
|
495
565
|
? [
|
|
496
566
|
`${binding.python}/src/${binding.pythonModule.replaceAll(".", "/")}/bindings.py`,
|
|
@@ -499,6 +569,13 @@ export class DBXToolsRustWorkspace {
|
|
|
499
569
|
: []),
|
|
500
570
|
]),
|
|
501
571
|
);
|
|
572
|
+
for (const binding of this.bindingMappings) {
|
|
573
|
+
if (!binding.node) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
project.prettier?.addIgnorePattern(`${binding.node}/src/bindings.ts`);
|
|
577
|
+
project.prettier?.addIgnorePattern(`${binding.node}/src/_bindings*.ts`);
|
|
578
|
+
}
|
|
502
579
|
this.pythonPackages = bindings
|
|
503
580
|
.filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("python"))
|
|
504
581
|
.map((pkg) => {
|
|
@@ -509,6 +586,9 @@ export class DBXToolsRustWorkspace {
|
|
|
509
586
|
module,
|
|
510
587
|
description: `Python bindings for ${pkg.crateName}`,
|
|
511
588
|
private: true,
|
|
589
|
+
internalDependencies: bindingDependencies(pkg, "python").map(
|
|
590
|
+
(dependency) => dependency.packageOptions.directory,
|
|
591
|
+
),
|
|
512
592
|
generatedSources: [`src/${module.replaceAll(".", "/")}/bindings.py`],
|
|
513
593
|
trustedPublisher: {
|
|
514
594
|
workflowName: options.releaseWorkflowName ?? "rust-release",
|
|
@@ -544,7 +624,6 @@ export class DBXToolsRustWorkspace {
|
|
|
544
624
|
`@${scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
|
|
545
625
|
);
|
|
546
626
|
node.package.addField("private", true);
|
|
547
|
-
node.package.file.addDeletionOverride("publishConfig");
|
|
548
627
|
node.package.addField("description", `Node bindings for ${binding.crateName}`);
|
|
549
628
|
const nativeTargets = releaseTargets(options);
|
|
550
629
|
if (options.release ?? true) {
|
|
@@ -559,6 +638,11 @@ export class DBXToolsRustWorkspace {
|
|
|
559
638
|
);
|
|
560
639
|
}
|
|
561
640
|
node.addDeps("@ubjs/core@0.31.0-5", "@ubjs/node@0.31.0-5");
|
|
641
|
+
node.addDeps(
|
|
642
|
+
...bindingDependencies(binding, "node").map(
|
|
643
|
+
(dependency) => `@${scope}/${dependency.packageOptions.directory}@workspace:*`,
|
|
644
|
+
),
|
|
645
|
+
);
|
|
562
646
|
if (binding.packageOptions.nodeDependencies?.length) {
|
|
563
647
|
node.addDeps(...binding.packageOptions.nodeDependencies);
|
|
564
648
|
}
|
|
@@ -566,7 +650,6 @@ export class DBXToolsRustWorkspace {
|
|
|
566
650
|
if (binding.packageOptions.nodeDevDependencies?.length) {
|
|
567
651
|
node.addDevDeps(...binding.packageOptions.nodeDevDependencies);
|
|
568
652
|
}
|
|
569
|
-
node.compileTask.reset();
|
|
570
653
|
new TextFile(node, "exports.ts", {
|
|
571
654
|
lines: ['export * from "./src/bindings.ts";', ""],
|
|
572
655
|
});
|
|
@@ -607,10 +690,13 @@ export class DBXToolsRustWorkspace {
|
|
|
607
690
|
project.addTask("rs:lint", { exec: "cargo clippy --workspace --all-targets --all-features" });
|
|
608
691
|
project.addTask("rs:test", { exec: "cargo test --workspace" });
|
|
609
692
|
project.addTask("rs:build", { exec: "cargo build --workspace" });
|
|
610
|
-
project.addTask("rs:bindings", {
|
|
693
|
+
const bindingsTask = project.addTask("rs:bindings", {
|
|
611
694
|
exec: "bun node_modules/@dbx-tools/projen/tasks/rust.ts",
|
|
612
695
|
description: "Generate language bindings for UniFFI-enabled Rust crates",
|
|
613
696
|
});
|
|
697
|
+
if (this.bindingMappings.some((binding) => binding.node)) {
|
|
698
|
+
project.tasks.tryFind("pre-compile")?.spawn(bindingsTask);
|
|
699
|
+
}
|
|
614
700
|
project.addTask("rs:bindings:demo", {
|
|
615
701
|
description: "Generate and run UniFFI Node and Python example CLIs",
|
|
616
702
|
exec: [
|
|
@@ -662,12 +748,20 @@ export class DBXToolsRustWorkspace {
|
|
|
662
748
|
}));
|
|
663
749
|
const publicCrates = this.packages
|
|
664
750
|
.filter((pkg) => !pkg.packageOptions.private)
|
|
751
|
+
.sort((first, second) => {
|
|
752
|
+
const order = this.bindingMappings.map((binding) => binding.crate);
|
|
753
|
+
return order.indexOf(first.crateName) - order.indexOf(second.crateName);
|
|
754
|
+
})
|
|
665
755
|
.map((pkg) => pkg.crateName);
|
|
666
756
|
const targetMatrix = targets.map((target, index) => ({
|
|
667
757
|
target: { ...target, facade: index === 0 },
|
|
668
758
|
}));
|
|
669
759
|
const hasNodeBindings = bindings.some((binding) => binding.node);
|
|
670
760
|
const hasPythonBindings = bindings.some((binding) => binding.python);
|
|
761
|
+
const facadeCondition = "matrix.target.facade";
|
|
762
|
+
const facadeCacheMissCondition =
|
|
763
|
+
"matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true'";
|
|
764
|
+
const usePreinstalledWindowsRust = releaseRustVersion === "stable";
|
|
671
765
|
const hasTargetOutputs =
|
|
672
766
|
bindings.length > 0 || releaseBinaries.length > 0 || publicCrates.length > 0;
|
|
673
767
|
if (hasTargetOutputs && targetMatrix.length === 0) {
|
|
@@ -779,15 +873,32 @@ export class DBXToolsRustWorkspace {
|
|
|
779
873
|
...(hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
|
|
780
874
|
{
|
|
781
875
|
name: "Setup Rust",
|
|
876
|
+
...(usePreinstalledWindowsRust ? { if: "${{ matrix.target.os != 'win32' }}" } : {}),
|
|
782
877
|
uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
|
|
783
878
|
with: { targets: "${{ matrix.target.cargo }}" },
|
|
784
879
|
},
|
|
880
|
+
...(usePreinstalledWindowsRust
|
|
881
|
+
? [
|
|
882
|
+
{
|
|
883
|
+
name: "Verify preinstalled Windows Rust",
|
|
884
|
+
if: "${{ matrix.target.os == 'win32' }}",
|
|
885
|
+
shell: "bash",
|
|
886
|
+
run: [
|
|
887
|
+
"rustc --version --verbose",
|
|
888
|
+
"cargo --version",
|
|
889
|
+
'rustup target list --installed | grep -Fx "${{ matrix.target.cargo }}"',
|
|
890
|
+
'test -f "$(rustc --print sysroot)/lib/rustlib/${{ matrix.target.cargo }}/bin/rust-lld.exe"',
|
|
891
|
+
].join("\n"),
|
|
892
|
+
},
|
|
893
|
+
]
|
|
894
|
+
: []),
|
|
785
895
|
...rustCacheSteps(`release-\${{ matrix.target.cargo }}-rust-${releaseRustVersion}`),
|
|
786
896
|
...(hasNodeBindings
|
|
787
897
|
? [
|
|
788
898
|
{
|
|
789
899
|
name: "Restore UBRN executable",
|
|
790
900
|
id: "ubrn_cache",
|
|
901
|
+
if: "${{ matrix.target.facade }}",
|
|
791
902
|
uses: "actions/cache/restore@v5",
|
|
792
903
|
with: {
|
|
793
904
|
path: ".cache/ubrn",
|
|
@@ -798,7 +909,8 @@ export class DBXToolsRustWorkspace {
|
|
|
798
909
|
: []),
|
|
799
910
|
...(hasNodeBindings
|
|
800
911
|
? bunCacheRestoreSteps(project, {
|
|
801
|
-
|
|
912
|
+
setupCondition: facadeCondition,
|
|
913
|
+
condition: facadeCacheMissCondition,
|
|
802
914
|
})
|
|
803
915
|
: []),
|
|
804
916
|
{
|
|
@@ -827,12 +939,12 @@ export class DBXToolsRustWorkspace {
|
|
|
827
939
|
? [
|
|
828
940
|
{
|
|
829
941
|
name: "Install Node tooling",
|
|
830
|
-
if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
|
|
942
|
+
if: "${{ matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true' }}",
|
|
831
943
|
shell: "bash",
|
|
832
944
|
run: timedBash("node_tooling", "bun install"),
|
|
833
945
|
},
|
|
834
946
|
bunCacheSaveStep({
|
|
835
|
-
condition:
|
|
947
|
+
condition: facadeCacheMissCondition,
|
|
836
948
|
}),
|
|
837
949
|
]
|
|
838
950
|
: []),
|
|
@@ -840,7 +952,7 @@ export class DBXToolsRustWorkspace {
|
|
|
840
952
|
? [
|
|
841
953
|
{
|
|
842
954
|
name: "Prepare UBRN generator",
|
|
843
|
-
if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
|
|
955
|
+
if: "${{ matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true' }}",
|
|
844
956
|
env: {
|
|
845
957
|
CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
|
|
846
958
|
UBRN_EXECUTABLE: ubrnExecutable,
|
|
@@ -858,13 +970,14 @@ export class DBXToolsRustWorkspace {
|
|
|
858
970
|
},
|
|
859
971
|
{
|
|
860
972
|
name: "Verify UBRN executable",
|
|
973
|
+
if: "${{ matrix.target.facade }}",
|
|
861
974
|
env: { UBRN_EXECUTABLE: ubrnExecutable },
|
|
862
975
|
shell: "bash",
|
|
863
976
|
run: 'test -f "$UBRN_EXECUTABLE"',
|
|
864
977
|
},
|
|
865
978
|
{
|
|
866
979
|
name: "Save UBRN executable",
|
|
867
|
-
if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
|
|
980
|
+
if: "${{ matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true' }}",
|
|
868
981
|
uses: "actions/cache/save@v5",
|
|
869
982
|
with: {
|
|
870
983
|
path: ".cache/ubrn",
|
|
@@ -876,6 +989,10 @@ export class DBXToolsRustWorkspace {
|
|
|
876
989
|
{
|
|
877
990
|
name: "Build Rust outputs",
|
|
878
991
|
shell: "bash",
|
|
992
|
+
env: {
|
|
993
|
+
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER:
|
|
994
|
+
"${{ matrix.target.os == 'win32' && 'rust-lld' || '' }}",
|
|
995
|
+
},
|
|
879
996
|
run: timedBash(
|
|
880
997
|
"rust_workspace",
|
|
881
998
|
'cargo build --release --workspace --target "${{ matrix.target.cargo }}"',
|
|
@@ -921,7 +1038,10 @@ export class DBXToolsRustWorkspace {
|
|
|
921
1038
|
`publish-${binding.crate}`,
|
|
922
1039
|
{
|
|
923
1040
|
if: "${{ github.event_name == 'repository_dispatch' }}",
|
|
924
|
-
needs: [
|
|
1041
|
+
needs: [
|
|
1042
|
+
"build",
|
|
1043
|
+
...(binding.dependencies ?? []).map((dependency) => `publish-${dependency}`),
|
|
1044
|
+
],
|
|
925
1045
|
"runs-on": "ubuntu-latest",
|
|
926
1046
|
permissions: {
|
|
927
1047
|
contents: "read",
|
package/src/uniffi.ts
CHANGED
|
@@ -36,6 +36,14 @@ export interface TypeScriptBindingModule {
|
|
|
36
36
|
readonly source: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** Add source extensions to UBRN's generated relative binding imports. */
|
|
40
|
+
export const addTypeScriptExtensionsToBindingImports = (source: string): string =>
|
|
41
|
+
source.replace(
|
|
42
|
+
/(["'])\.\/_bindings(-ffi)?\1/g,
|
|
43
|
+
(_specifier, quote: string, suffix: string | undefined) =>
|
|
44
|
+
`${quote}./_bindings${suffix ?? ""}.ts${quote}`,
|
|
45
|
+
);
|
|
46
|
+
|
|
39
47
|
/** Add explicit type exports for interfaces that TypeScript misses through UBRN's star exports. */
|
|
40
48
|
export const addExplicitInterfaceReexports = (
|
|
41
49
|
facade: string,
|
package/tasks/rust.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
|
8
8
|
import {
|
|
9
9
|
discoverRustCrates,
|
|
10
10
|
hasUniFFIBindings,
|
|
11
|
+
orderRustBindings,
|
|
11
12
|
type RustBindingMapping,
|
|
12
13
|
type RustWorkspaceMapping,
|
|
13
14
|
} from "../src/project-rs.ts";
|
|
@@ -84,6 +85,19 @@ function generate(binding: RustBindingMapping): void {
|
|
|
84
85
|
if (result.status !== 0) throw new Error(`binding generation exited with ${result.status}`);
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
export function affectedRustBindings(
|
|
89
|
+
bindings: readonly RustBindingMapping[],
|
|
90
|
+
changed: ReadonlySet<string>,
|
|
91
|
+
): RustBindingMapping[] {
|
|
92
|
+
const affected = new Set(changed);
|
|
93
|
+
const ordered = orderRustBindings(bindings);
|
|
94
|
+
for (const binding of ordered) {
|
|
95
|
+
if (binding.dependencies?.some((dependency) => affected.has(dependency)))
|
|
96
|
+
affected.add(binding.crate);
|
|
97
|
+
}
|
|
98
|
+
return ordered.filter((binding) => affected.has(binding.crate));
|
|
99
|
+
}
|
|
100
|
+
|
|
87
101
|
const config = rustConfig();
|
|
88
102
|
|
|
89
103
|
async function main(): Promise<void> {
|
|
@@ -92,7 +106,7 @@ async function main(): Promise<void> {
|
|
|
92
106
|
throw new Error("Cargo is required because Rust projects were detected");
|
|
93
107
|
}
|
|
94
108
|
if (!process.argv.includes("--watch")) {
|
|
95
|
-
for (const binding of config.bindings) generate(binding);
|
|
109
|
+
for (const binding of orderRustBindings(config.bindings)) generate(binding);
|
|
96
110
|
return;
|
|
97
111
|
}
|
|
98
112
|
|
|
@@ -109,7 +123,7 @@ async function main(): Promise<void> {
|
|
|
109
123
|
const binding = ownerBinding(path, refreshed.bindings);
|
|
110
124
|
if (binding) targets.set(binding.crate, binding);
|
|
111
125
|
}
|
|
112
|
-
for (const binding of targets.
|
|
126
|
+
for (const binding of affectedRustBindings(refreshed.bindings, new Set(targets.keys()))) {
|
|
113
127
|
logger.start(`generating ${binding.crate} bindings`);
|
|
114
128
|
generate(binding);
|
|
115
129
|
logger.success(`generated ${binding.crate} bindings`);
|
|
@@ -121,7 +135,7 @@ async function main(): Promise<void> {
|
|
|
121
135
|
const binding = ownerBinding(path, latest.bindings);
|
|
122
136
|
if (binding) targets.set(binding.crate, binding);
|
|
123
137
|
}
|
|
124
|
-
for (const binding of targets.
|
|
138
|
+
for (const binding of affectedRustBindings(latest.bindings, new Set(targets.keys()))) {
|
|
125
139
|
logger.start(`generating ${binding.crate} bindings`);
|
|
126
140
|
generate(binding);
|
|
127
141
|
logger.success(`generated ${binding.crate} bindings`);
|
package/tasks/uniffi-release.mjs
CHANGED
|
@@ -65,6 +65,82 @@ const run = (command, args, cwd = root) => {
|
|
|
65
65
|
if (result.status !== 0) throw new Error(`${invocation.command} exited with ${result.status}`);
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
const singlePackage = (directory) => {
|
|
69
|
+
const packages = readdirSync(directory)
|
|
70
|
+
.filter((file) => file.endsWith(".tgz"))
|
|
71
|
+
.map((file) => resolve(directory, file));
|
|
72
|
+
if (packages.length !== 1) {
|
|
73
|
+
throw new Error(`Expected one npm package in ${directory}, found ${packages.length}`);
|
|
74
|
+
}
|
|
75
|
+
return packages[0];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const localWorkspacePackages = () => {
|
|
79
|
+
const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
|
|
80
|
+
const workspaces = Array.isArray(manifest.workspaces) ? manifest.workspaces : [];
|
|
81
|
+
return new Map(
|
|
82
|
+
workspaces.flatMap((directory) => {
|
|
83
|
+
const manifestPath = resolve(root, directory, "package.json");
|
|
84
|
+
if (!existsSync(manifestPath)) return [];
|
|
85
|
+
const workspaceManifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
86
|
+
return workspaceManifest.name ? [[workspaceManifest.name, workspaceManifest.version]] : [];
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const testNodeFacade = ({ facadePackage, manifest, nativePackage, nodePackage }) => {
|
|
92
|
+
const installDirectory = mkdtempSync(join(tmpdir(), "uniffi-facade-install-"));
|
|
93
|
+
try {
|
|
94
|
+
const workspacePackages = localWorkspacePackages();
|
|
95
|
+
const bindings =
|
|
96
|
+
JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dbxToolsConfig?.rust?.bindings ??
|
|
97
|
+
[];
|
|
98
|
+
const localDependencies = Object.keys(manifest.dependencies ?? {}).flatMap((name, index) => {
|
|
99
|
+
const workspaceVersion = workspacePackages.get(name);
|
|
100
|
+
if (!workspaceVersion) return [];
|
|
101
|
+
const binding = bindings.find((binding) => binding.nodePackage === name);
|
|
102
|
+
if (binding) {
|
|
103
|
+
const output = resolve(root, "dist/release", binding.crate, required("node-triple"));
|
|
104
|
+
return [singlePackage(join(output, "npm-facade")), singlePackage(join(output, "npm"))];
|
|
105
|
+
}
|
|
106
|
+
const directory = join(installDirectory, "local-dependencies", String(index));
|
|
107
|
+
mkdirSync(directory, { recursive: true });
|
|
108
|
+
writeFileSync(
|
|
109
|
+
join(directory, "package.json"),
|
|
110
|
+
`${JSON.stringify({
|
|
111
|
+
name,
|
|
112
|
+
version: workspaceVersion,
|
|
113
|
+
type: "module",
|
|
114
|
+
exports: "./index.js",
|
|
115
|
+
})}\n`,
|
|
116
|
+
);
|
|
117
|
+
writeFileSync(join(directory, "index.js"), "export {};\n");
|
|
118
|
+
return [directory];
|
|
119
|
+
});
|
|
120
|
+
writeFileSync(
|
|
121
|
+
join(installDirectory, "package.json"),
|
|
122
|
+
`${JSON.stringify({ private: true, type: "module" })}\n`,
|
|
123
|
+
);
|
|
124
|
+
run(
|
|
125
|
+
"npm",
|
|
126
|
+
[
|
|
127
|
+
"install",
|
|
128
|
+
"--ignore-scripts",
|
|
129
|
+
"--no-audit",
|
|
130
|
+
"--no-fund",
|
|
131
|
+
"--package-lock=false",
|
|
132
|
+
facadePackage,
|
|
133
|
+
nativePackage,
|
|
134
|
+
...localDependencies,
|
|
135
|
+
],
|
|
136
|
+
installDirectory,
|
|
137
|
+
);
|
|
138
|
+
run("node", ["-e", `import(${JSON.stringify(nodePackage)})`], installDirectory);
|
|
139
|
+
} finally {
|
|
140
|
+
rmSync(installDirectory, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
68
144
|
const replaceVersion = (source, version) =>
|
|
69
145
|
source.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
|
|
70
146
|
|
|
@@ -135,8 +211,6 @@ const packageNode = ({
|
|
|
135
211
|
typeof dependency === "string" && dependency.startsWith("workspace:") ? version : dependency,
|
|
136
212
|
]),
|
|
137
213
|
);
|
|
138
|
-
delete manifest.scripts;
|
|
139
|
-
delete manifest.devDependencies;
|
|
140
214
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
141
215
|
run("bun", [
|
|
142
216
|
resolve(root, nodeGenerator),
|
|
@@ -153,8 +227,44 @@ const packageNode = ({
|
|
|
153
227
|
...(parsed.values.ubrn ? ["--ubrn", parsed.values.ubrn, "--skip-barrels"] : []),
|
|
154
228
|
"--skip-build",
|
|
155
229
|
]);
|
|
156
|
-
|
|
157
|
-
|
|
230
|
+
// Node loads the facade from node_modules, so every TypeScript entry point
|
|
231
|
+
// must be emitted and advertised as JavaScript. Bun is always available in
|
|
232
|
+
// the facade row, including when the UBRN cache skips the workspace install.
|
|
233
|
+
rmSync(join(facadeDirectory, "lib"), { recursive: true, force: true });
|
|
234
|
+
run(
|
|
235
|
+
"bun",
|
|
236
|
+
[
|
|
237
|
+
"build",
|
|
238
|
+
"index.ts",
|
|
239
|
+
"--outdir",
|
|
240
|
+
"lib",
|
|
241
|
+
"--target",
|
|
242
|
+
"node",
|
|
243
|
+
"--format",
|
|
244
|
+
"esm",
|
|
245
|
+
"--packages",
|
|
246
|
+
"external",
|
|
247
|
+
],
|
|
248
|
+
facadeDirectory,
|
|
249
|
+
);
|
|
250
|
+
const compiledPublish = { ...(manifest.publishConfig ?? {}) };
|
|
251
|
+
delete compiledPublish.access;
|
|
252
|
+
Object.assign(manifest, compiledPublish);
|
|
253
|
+
manifest.types = "./index.ts";
|
|
254
|
+
manifest.exports["."].types = "./index.ts";
|
|
255
|
+
delete manifest.publishConfig;
|
|
256
|
+
delete manifest.scripts;
|
|
257
|
+
delete manifest.devDependencies;
|
|
258
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
259
|
+
const facadeOutput = resolve(output, "npm-facade");
|
|
260
|
+
mkdirSync(facadeOutput, { recursive: true });
|
|
261
|
+
run("npm", ["pack", "--pack-destination", facadeOutput], facadeDirectory);
|
|
262
|
+
testNodeFacade({
|
|
263
|
+
facadePackage: singlePackage(facadeOutput),
|
|
264
|
+
manifest,
|
|
265
|
+
nativePackage: singlePackage(resolve(output, "npm")),
|
|
266
|
+
nodePackage,
|
|
267
|
+
});
|
|
158
268
|
};
|
|
159
269
|
|
|
160
270
|
const packagePython = ({
|
|
@@ -172,6 +282,19 @@ const packagePython = ({
|
|
|
172
282
|
const pyproject = join(pythonRoot, "pyproject.toml");
|
|
173
283
|
writable(pyproject);
|
|
174
284
|
writeFileSync(pyproject, replaceVersion(readFileSync(pyproject, "utf8"), version));
|
|
285
|
+
const workspaceBindings =
|
|
286
|
+
JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dbxToolsConfig?.rust?.bindings ??
|
|
287
|
+
[];
|
|
288
|
+
let metadata = readFileSync(pyproject, "utf8");
|
|
289
|
+
for (const binding of workspaceBindings) {
|
|
290
|
+
if (!binding.pythonPackage) continue;
|
|
291
|
+
const escaped = binding.pythonPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
292
|
+
metadata = metadata.replace(
|
|
293
|
+
new RegExp(`"${escaped} @ git\\+[^"\\n]+"`, "g"),
|
|
294
|
+
JSON.stringify(`${binding.pythonPackage}==${version}`),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
writeFileSync(pyproject, metadata);
|
|
175
298
|
|
|
176
299
|
const packageName = crate.replace(/^dbx-tools-/, "").replaceAll("-", "_");
|
|
177
300
|
const packageDirectory = resolve(pythonRoot, "src", "dbx_tools", packageName);
|
|
@@ -181,10 +304,19 @@ const packagePython = ({
|
|
|
181
304
|
"target",
|
|
182
305
|
cargoTarget,
|
|
183
306
|
"release",
|
|
184
|
-
|
|
307
|
+
`${crate}-uniffi-bindgen${os === "win32" ? ".exe" : ""}`,
|
|
185
308
|
);
|
|
186
309
|
if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
|
|
187
|
-
run(generator, [
|
|
310
|
+
run(generator, [
|
|
311
|
+
"generate",
|
|
312
|
+
"--language",
|
|
313
|
+
"python",
|
|
314
|
+
"--crate",
|
|
315
|
+
crate.replaceAll("-", "_"),
|
|
316
|
+
"--out-dir",
|
|
317
|
+
generatedDirectory,
|
|
318
|
+
library,
|
|
319
|
+
]);
|
|
188
320
|
const bindings = join(packageDirectory, "bindings.py");
|
|
189
321
|
writable(bindings);
|
|
190
322
|
const body = readFileSync(join(generatedDirectory, `${crate.replaceAll("-", "_")}.py`), "utf8");
|
package/tasks/uniffi.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import {
|
|
3
4
|
cpSync,
|
|
4
5
|
existsSync,
|
|
@@ -14,10 +15,11 @@ import { tmpdir } from "node:os";
|
|
|
14
15
|
import { basename, dirname, join, resolve } from "node:path";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
import { parseArgs } from "node:util";
|
|
17
|
-
import { spawnSync } from "node:child_process";
|
|
18
18
|
import { makeReadonly, makeWritable, stampGenerated } from "../src/generated.ts";
|
|
19
|
+
import type { RustWorkspaceMapping } from "../src/project-rs.ts";
|
|
19
20
|
import {
|
|
20
21
|
addExplicitInterfaceReexports,
|
|
22
|
+
addTypeScriptExtensionsToBindingImports,
|
|
21
23
|
makeDefaultedInterfaceParametersOptional,
|
|
22
24
|
} from "../src/uniffi.ts";
|
|
23
25
|
|
|
@@ -43,6 +45,11 @@ const root = values.root
|
|
|
43
45
|
: resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
44
46
|
const crate = values.crate;
|
|
45
47
|
const libraryName = crate.replaceAll("-", "_");
|
|
48
|
+
const workspace = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dbxToolsConfig
|
|
49
|
+
?.rust as RustWorkspaceMapping | undefined;
|
|
50
|
+
const binding = workspace?.bindings.find((binding) => binding.crate === crate);
|
|
51
|
+
const dependencies =
|
|
52
|
+
workspace?.bindings.filter((candidate) => binding?.dependencies?.includes(candidate.crate)) ?? [];
|
|
46
53
|
const extension =
|
|
47
54
|
process.platform === "darwin" ? "dylib" : process.platform === "win32" ? "dll" : "so";
|
|
48
55
|
const prefix = process.platform === "win32" ? "" : "lib";
|
|
@@ -61,6 +68,9 @@ const run = (command: string, args: string[]) => {
|
|
|
61
68
|
const stderr = normalizedOutput(result.stderr ?? "");
|
|
62
69
|
if (stdout) process.stdout.write(`${stdout}\n`);
|
|
63
70
|
if (stderr) process.stderr.write(`${stderr}\n`);
|
|
71
|
+
if (result.error) {
|
|
72
|
+
throw new Error(`${command} failed: ${result.error.message}`, { cause: result.error });
|
|
73
|
+
}
|
|
64
74
|
if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`);
|
|
65
75
|
};
|
|
66
76
|
|
|
@@ -126,42 +136,60 @@ if (values.node) {
|
|
|
126
136
|
const generatedModules = readdirSync(nodeOutput).filter(
|
|
127
137
|
(file) => file.endsWith(".ts") && file !== "index.ts",
|
|
128
138
|
);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
for (const dependency of dependencies) {
|
|
140
|
+
if (!dependency.nodePackage) throw new Error(`Missing Node binding for ${dependency.crate}`);
|
|
141
|
+
const namespace = dependency.crate.replaceAll("-", "_");
|
|
142
|
+
for (const file of readdirSync(nodeOutput).filter((file) => file.endsWith(".ts"))) {
|
|
143
|
+
const path = join(nodeOutput, file);
|
|
144
|
+
const source = readFileSync(path, "utf8")
|
|
145
|
+
.replace(
|
|
146
|
+
new RegExp(`import (\\w+) from ["']\\./${namespace}["'];`, "g"),
|
|
147
|
+
`import { uniffiModule as $1 } from "${dependency.nodePackage}";`,
|
|
148
|
+
)
|
|
149
|
+
.replaceAll(`'./${namespace}'`, `'${dependency.nodePackage}'`)
|
|
150
|
+
.replaceAll(`"./${namespace}"`, `"${dependency.nodePackage}"`);
|
|
151
|
+
writeFileSync(path, source);
|
|
139
152
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
readFileSync(nodeBindings, "utf8").replaceAll(`./${libraryName}`, "./_bindings"),
|
|
146
|
-
);
|
|
147
|
-
writeFileSync(
|
|
148
|
-
generatedFiles[0],
|
|
149
|
-
readFileSync(generatedFiles[0], "utf8").replaceAll(`./${libraryName}-ffi`, "./_bindings-ffi"),
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
if (linkedComponents) {
|
|
153
|
-
for (const file of [nodeBindings, ...generatedFiles]) {
|
|
154
|
-
let source = readFileSync(file, "utf8");
|
|
155
|
-
for (const [generated, destination] of linkedNames) {
|
|
156
|
-
const from = generated.slice(0, -3);
|
|
157
|
-
const to = destination.slice(0, -3);
|
|
158
|
-
source = source
|
|
159
|
-
.replaceAll(`'./${from}'`, `'./${to}'`)
|
|
160
|
-
.replaceAll(`"./${from}"`, `"./${to}"`);
|
|
161
|
-
}
|
|
162
|
-
writeFileSync(file, source);
|
|
153
|
+
for (const suffix of [".ts", "-ffi.ts"]) {
|
|
154
|
+
const file = `${namespace}${suffix}`;
|
|
155
|
+
rmSync(join(nodeOutput, file), { force: true });
|
|
156
|
+
const index = generatedModules.indexOf(file);
|
|
157
|
+
if (index >= 0) generatedModules.splice(index, 1);
|
|
163
158
|
}
|
|
164
159
|
}
|
|
160
|
+
const ownModules = generatedModules.filter(
|
|
161
|
+
(file) => file === `${libraryName}.ts` || file === `${libraryName}-ffi.ts`,
|
|
162
|
+
);
|
|
163
|
+
if (ownModules.length !== 2 || generatedModules.length !== 2) {
|
|
164
|
+
throw new Error(`Unmapped UniFFI components in ${crate}: ${generatedModules.join(", ")}`);
|
|
165
|
+
}
|
|
166
|
+
writeFileSync(
|
|
167
|
+
join(nodeOutput, "index.ts"),
|
|
168
|
+
[
|
|
169
|
+
`export * from './${libraryName}';`,
|
|
170
|
+
`import uniffiModule from './${libraryName}';`,
|
|
171
|
+
"uniffiModule.initialize();",
|
|
172
|
+
"export { uniffiModule };",
|
|
173
|
+
"export async function uniffiInitAsync() {}",
|
|
174
|
+
"",
|
|
175
|
+
].join("\n"),
|
|
176
|
+
);
|
|
177
|
+
const nodeBindings = join(nodeSource, "bindings.ts");
|
|
178
|
+
const generatedFiles = [join(nodeSource, "_bindings.ts"), join(nodeSource, "_bindings-ffi.ts")];
|
|
179
|
+
replaceGenerated(join(nodeOutput, "index.ts"), nodeBindings);
|
|
180
|
+
replaceGenerated(join(nodeOutput, libraryName + ".ts"), generatedFiles[0]);
|
|
181
|
+
replaceGenerated(join(nodeOutput, libraryName + "-ffi.ts"), generatedFiles[1]);
|
|
182
|
+
writeFileSync(
|
|
183
|
+
nodeBindings,
|
|
184
|
+
readFileSync(nodeBindings, "utf8").replaceAll("./" + libraryName, "./_bindings"),
|
|
185
|
+
);
|
|
186
|
+
writeFileSync(
|
|
187
|
+
generatedFiles[0],
|
|
188
|
+
readFileSync(generatedFiles[0], "utf8").replaceAll(
|
|
189
|
+
"./" + libraryName + "-ffi",
|
|
190
|
+
"./_bindings-ffi",
|
|
191
|
+
),
|
|
192
|
+
);
|
|
165
193
|
for (const file of generatedFiles) {
|
|
166
194
|
writeFileSync(file, makeDefaultedInterfaceParametersOptional(readFileSync(file, "utf8")));
|
|
167
195
|
}
|
|
@@ -176,6 +204,13 @@ if (values.node) {
|
|
|
176
204
|
),
|
|
177
205
|
);
|
|
178
206
|
for (const file of [nodeBindings, ...generatedFiles]) {
|
|
207
|
+
// Consumers type-check these files through workspace source exports, while
|
|
208
|
+
// the generated runtime internals are validated by UniFFI's own build.
|
|
209
|
+
const source = addTypeScriptExtensionsToBindingImports(readFileSync(file, "utf8")).replace(
|
|
210
|
+
/[ \t]+$/gm,
|
|
211
|
+
"",
|
|
212
|
+
);
|
|
213
|
+
writeFileSync(file, `/* eslint-disable */\n// @ts-nocheck\n${source}`);
|
|
179
214
|
stampGenerated(file, {
|
|
180
215
|
tool: "UniFFI binding generation",
|
|
181
216
|
source: `the ${crate} Rust exports`,
|
|
@@ -206,31 +241,26 @@ if (values.python) {
|
|
|
206
241
|
crate.replace(/^dbx-tools-/, "").replaceAll("-", "_"),
|
|
207
242
|
);
|
|
208
243
|
const pythonOutput = mkdtempSync(join(tmpdir(), `${libraryName}-python-`));
|
|
209
|
-
run(
|
|
210
|
-
"
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
library,
|
|
223
|
-
]);
|
|
244
|
+
run(
|
|
245
|
+
join(targetDirectory, `${crate}-uniffi-bindgen${process.platform === "win32" ? ".exe" : ""}`),
|
|
246
|
+
[
|
|
247
|
+
"generate",
|
|
248
|
+
"--language",
|
|
249
|
+
"python",
|
|
250
|
+
"--crate",
|
|
251
|
+
libraryName,
|
|
252
|
+
"--out-dir",
|
|
253
|
+
pythonOutput,
|
|
254
|
+
library,
|
|
255
|
+
],
|
|
256
|
+
);
|
|
224
257
|
const generated = join(pythonOutput, `${libraryName}.py`);
|
|
225
258
|
const pythonBindings = join(pythonPackage, "bindings.py");
|
|
226
259
|
replaceGenerated(generated, pythonBindings);
|
|
227
260
|
stampGeneratedPython(pythonBindings);
|
|
228
261
|
const pythonInit = join(pythonPackage, "__init__.py");
|
|
229
262
|
if (!existsSync(pythonInit)) {
|
|
230
|
-
writeFileSync(
|
|
231
|
-
pythonInit,
|
|
232
|
-
`\"\"\"Python bindings for ${crate}.\"\"\"\n\nfrom .bindings import * # noqa: F403\n`,
|
|
233
|
-
);
|
|
263
|
+
writeFileSync(pythonInit, "");
|
|
234
264
|
}
|
|
235
265
|
const pythonLibrary = join(pythonPackage, basename(library));
|
|
236
266
|
makeWritable(pythonLibrary);
|