@dbx-tools/projen 0.6.142 → 0.6.144
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 +37 -0
- package/index.ts +6 -0
- package/package.json +4 -4
- package/src/barrels.ts +33 -16
- package/src/packages.ts +1 -1
- package/src/project-predicate.ts +2 -1
- package/src/project-py.ts +19 -4
- package/src/project-rs.ts +644 -0
- package/src/project.ts +2 -1
- package/src/uniffi.ts +50 -0
- package/tasks/barrels.ts +14 -2
- package/tasks/bump.ts +69 -1
- package/tasks/publish-python.ts +8 -2
- package/tasks/publish-uniffi-local.ts +207 -0
- package/tasks/rust.ts +132 -0
- package/tasks/sync.ts +31 -15
- package/tasks/uniffi-release.ts +168 -0
- package/tasks/uniffi.ts +224 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/** Filesystem-discovered Rust workspaces and UniFFI binding package wiring. */
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
import { string } from "@dbx-tools/shared-core";
|
|
5
|
+
import { Project, TextFile, YamlFile, javascript } from "projen";
|
|
6
|
+
import type { DBXToolsProject } from "./project.ts";
|
|
7
|
+
import { DBXToolsTypeScriptProject } from "./project-js.ts";
|
|
8
|
+
import type { PythonPackageOptions } from "./project-py.ts";
|
|
9
|
+
import { readWorkspaceVersion } from "./workspace-version.ts";
|
|
10
|
+
|
|
11
|
+
export interface CargoDependencyOptions {
|
|
12
|
+
readonly version?: string;
|
|
13
|
+
readonly workspace?: boolean;
|
|
14
|
+
readonly path?: string;
|
|
15
|
+
readonly optional?: boolean;
|
|
16
|
+
readonly defaultFeatures?: boolean;
|
|
17
|
+
readonly features?: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type CargoDependency = string | CargoDependencyOptions;
|
|
21
|
+
|
|
22
|
+
export interface RustPackageOptions {
|
|
23
|
+
readonly directory: string;
|
|
24
|
+
readonly description?: string;
|
|
25
|
+
/** Keep this crate unpublished and out of public docs. */
|
|
26
|
+
readonly private?: boolean;
|
|
27
|
+
readonly dependencies?: Readonly<Record<string, CargoDependency>>;
|
|
28
|
+
readonly devDependencies?: Readonly<Record<string, CargoDependency>>;
|
|
29
|
+
readonly features?: Readonly<Record<string, readonly string[]>>;
|
|
30
|
+
readonly defaultFeatures?: readonly string[];
|
|
31
|
+
readonly binaryName?: string;
|
|
32
|
+
readonly bindings?: readonly ("node" | "python")[];
|
|
33
|
+
readonly nodeDependencies?: readonly string[];
|
|
34
|
+
readonly nodeDevDependencies?: readonly string[];
|
|
35
|
+
readonly uniffiConfig?: Readonly<Record<string, unknown>>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DBXToolsRustWorkspaceOptions {
|
|
39
|
+
readonly root?: string;
|
|
40
|
+
readonly scope: string;
|
|
41
|
+
readonly edition?: string;
|
|
42
|
+
readonly rustVersion?: string;
|
|
43
|
+
readonly license?: string;
|
|
44
|
+
readonly repository?: string;
|
|
45
|
+
readonly workspaceDependencies?: Readonly<Record<string, CargoDependency>>;
|
|
46
|
+
readonly packages?: Readonly<Record<string, Omit<RustPackageOptions, "directory">>>;
|
|
47
|
+
readonly nodeRoot?: string;
|
|
48
|
+
readonly pythonRoot?: string;
|
|
49
|
+
readonly pythonModulePrefix?: string;
|
|
50
|
+
/** Generate tag-driven cross-platform UniFFI package releases. */
|
|
51
|
+
readonly release?: boolean;
|
|
52
|
+
/** Native release targets; defaults to the maintained GitHub-hosted matrix. */
|
|
53
|
+
readonly releaseTargets?: readonly UniFFIReleaseTarget[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UniFFIReleaseTarget {
|
|
57
|
+
readonly runner: string;
|
|
58
|
+
readonly cargo: string;
|
|
59
|
+
readonly node: string;
|
|
60
|
+
readonly python: string;
|
|
61
|
+
readonly os: "darwin" | "linux" | "win32";
|
|
62
|
+
readonly cpu: "arm64" | "x64";
|
|
63
|
+
readonly libc?: "glibc";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Native targets built on matching GitHub-hosted runners. */
|
|
67
|
+
export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
|
|
68
|
+
{
|
|
69
|
+
runner: "ubuntu-22.04",
|
|
70
|
+
cargo: "x86_64-unknown-linux-gnu",
|
|
71
|
+
node: "linux-x64-gnu",
|
|
72
|
+
python: "manylinux_2_35_x86_64",
|
|
73
|
+
os: "linux",
|
|
74
|
+
cpu: "x64",
|
|
75
|
+
libc: "glibc",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
runner: "ubuntu-24.04-arm",
|
|
79
|
+
cargo: "aarch64-unknown-linux-gnu",
|
|
80
|
+
node: "linux-arm64-gnu",
|
|
81
|
+
python: "manylinux_2_39_aarch64",
|
|
82
|
+
os: "linux",
|
|
83
|
+
cpu: "arm64",
|
|
84
|
+
libc: "glibc",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
runner: "macos-15-intel",
|
|
88
|
+
cargo: "x86_64-apple-darwin",
|
|
89
|
+
node: "darwin-x64",
|
|
90
|
+
python: "macosx_13_0_x86_64",
|
|
91
|
+
os: "darwin",
|
|
92
|
+
cpu: "x64",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
runner: "macos-14",
|
|
96
|
+
cargo: "aarch64-apple-darwin",
|
|
97
|
+
node: "darwin-arm64",
|
|
98
|
+
python: "macosx_11_0_arm64",
|
|
99
|
+
os: "darwin",
|
|
100
|
+
cpu: "arm64",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
runner: "windows-latest",
|
|
104
|
+
cargo: "x86_64-pc-windows-msvc",
|
|
105
|
+
node: "win32-x64-msvc",
|
|
106
|
+
python: "win_amd64",
|
|
107
|
+
os: "win32",
|
|
108
|
+
cpu: "x64",
|
|
109
|
+
},
|
|
110
|
+
] as const;
|
|
111
|
+
|
|
112
|
+
/** Persisted mapping consumed by the focused Rust source watcher. */
|
|
113
|
+
export interface RustBindingMapping {
|
|
114
|
+
readonly crate: string;
|
|
115
|
+
readonly rust: string;
|
|
116
|
+
readonly node?: string;
|
|
117
|
+
readonly python?: string;
|
|
118
|
+
readonly nodePackage?: string;
|
|
119
|
+
readonly pythonPackage?: string;
|
|
120
|
+
readonly facadeTarget?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Persisted Rust workspace state consumed by `sync --watch`. */
|
|
124
|
+
export interface RustWorkspaceMapping {
|
|
125
|
+
readonly root: string;
|
|
126
|
+
readonly crates: readonly string[];
|
|
127
|
+
readonly bindings: readonly RustBindingMapping[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function rustSources(directory: string): string[] {
|
|
131
|
+
if (!existsSync(directory)) return [];
|
|
132
|
+
const files: string[] = [];
|
|
133
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
134
|
+
const path = join(directory, entry.name);
|
|
135
|
+
if (entry.isDirectory()) files.push(...rustSources(path));
|
|
136
|
+
else if (entry.isFile() && entry.name.endsWith(".rs")) files.push(path);
|
|
137
|
+
}
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Whether a Rust crate embeds the UniFFI proc-macro scaffolding marker. */
|
|
142
|
+
export function hasUniFFIBindings(directory: string): boolean {
|
|
143
|
+
return rustSources(join(directory, "src")).some((path) =>
|
|
144
|
+
/\buniffi\s*::\s*setup_scaffolding\s*!\s*\(/.test(readFileSync(path, "utf8")),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function discoverRustCrates(root: string): string[] {
|
|
149
|
+
if (!existsSync(root)) return [];
|
|
150
|
+
return readdirSync(root, { withFileTypes: true })
|
|
151
|
+
.filter((entry) => entry.isDirectory())
|
|
152
|
+
.filter((entry) => rustSources(join(root, entry.name, "src")).length > 0)
|
|
153
|
+
.map((entry) => entry.name)
|
|
154
|
+
.sort();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function cargoDependency(
|
|
158
|
+
value: CargoDependency,
|
|
159
|
+
workspaceVersion?: string,
|
|
160
|
+
): string | Record<string, unknown> {
|
|
161
|
+
if (typeof value === "string") return value;
|
|
162
|
+
return {
|
|
163
|
+
...(value.version ? { version: value.version } : {}),
|
|
164
|
+
...(!value.version && value.path && workspaceVersion ? { version: workspaceVersion } : {}),
|
|
165
|
+
...(value.workspace ? { workspace: true } : {}),
|
|
166
|
+
...(value.path ? { path: value.path } : {}),
|
|
167
|
+
...(value.optional ? { optional: true } : {}),
|
|
168
|
+
...(value.defaultFeatures === false ? { "default-features": false } : {}),
|
|
169
|
+
...(value.features?.length ? { features: [...value.features] } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function tomlValue(value: unknown): string {
|
|
174
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
175
|
+
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
176
|
+
if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
|
|
177
|
+
if (value && typeof value === "object") {
|
|
178
|
+
return `{ ${Object.entries(value)
|
|
179
|
+
.map(([key, entry]) => `${key} = ${tomlValue(entry)}`)
|
|
180
|
+
.join(", ")} }`;
|
|
181
|
+
}
|
|
182
|
+
throw new Error(`Unsupported Cargo TOML value: ${String(value)}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderToml(value: Record<string, unknown>): string {
|
|
186
|
+
const blocks: string[] = [];
|
|
187
|
+
for (const [section, entries] of Object.entries(value)) {
|
|
188
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue;
|
|
189
|
+
const rows = Object.entries(entries).map(([key, entry]) => `${key} = ${tomlValue(entry)}`);
|
|
190
|
+
blocks.push(`${section === "bin" ? "[[bin]]" : `[${section}]`}\n${rows.join("\n")}`);
|
|
191
|
+
}
|
|
192
|
+
return `${blocks.join("\n\n")}\n`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** One generated Cargo workspace member. */
|
|
196
|
+
export class DBXToolsRustProject extends Project implements DBXToolsProject {
|
|
197
|
+
readonly language = "rust" as const;
|
|
198
|
+
readonly crateName: string;
|
|
199
|
+
readonly packageOptions: RustPackageOptions;
|
|
200
|
+
readonly uniffi: boolean;
|
|
201
|
+
|
|
202
|
+
constructor(
|
|
203
|
+
parent: javascript.NodeProject,
|
|
204
|
+
root: string,
|
|
205
|
+
scope: string,
|
|
206
|
+
options: RustPackageOptions,
|
|
207
|
+
) {
|
|
208
|
+
const crateName = `${string.toSlug(scope)}-${options.directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`;
|
|
209
|
+
super({ parent, outdir: `${root}/${options.directory}`, name: crateName });
|
|
210
|
+
this.crateName = crateName;
|
|
211
|
+
this.packageOptions = options;
|
|
212
|
+
this.uniffi = hasUniFFIBindings(this.outdir);
|
|
213
|
+
const library = existsSync(join(this.outdir, "src/lib.rs"));
|
|
214
|
+
const manifest: Record<string, unknown> = {
|
|
215
|
+
package: {
|
|
216
|
+
name: crateName,
|
|
217
|
+
version: { workspace: true },
|
|
218
|
+
edition: { workspace: true },
|
|
219
|
+
"rust-version": { workspace: true },
|
|
220
|
+
description: options.description ?? crateName,
|
|
221
|
+
license: { workspace: true },
|
|
222
|
+
repository: { workspace: true },
|
|
223
|
+
...(options.private ? { publish: false } : {}),
|
|
224
|
+
},
|
|
225
|
+
...(library
|
|
226
|
+
? {
|
|
227
|
+
lib: {
|
|
228
|
+
name: crateName.replaceAll("-", "_"),
|
|
229
|
+
path: "src/lib.rs",
|
|
230
|
+
...(this.uniffi ? { "crate-type": ["lib", "cdylib"] } : {}),
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
: {}),
|
|
234
|
+
...(this.uniffi || options.binaryName
|
|
235
|
+
? {
|
|
236
|
+
bin: this.uniffi
|
|
237
|
+
? { name: "uniffi-bindgen", path: "uniffi-bindgen.rs" }
|
|
238
|
+
: { name: options.binaryName, path: "src/main.rs" },
|
|
239
|
+
}
|
|
240
|
+
: {}),
|
|
241
|
+
...(options.features || options.defaultFeatures
|
|
242
|
+
? {
|
|
243
|
+
features: {
|
|
244
|
+
...(options.defaultFeatures ? { default: [...options.defaultFeatures] } : {}),
|
|
245
|
+
...options.features,
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
249
|
+
...(options.dependencies
|
|
250
|
+
? {
|
|
251
|
+
dependencies: Object.fromEntries(
|
|
252
|
+
Object.entries(options.dependencies).map(([name, value]) => [
|
|
253
|
+
name,
|
|
254
|
+
cargoDependency(value, readWorkspaceVersion(parent.outdir)),
|
|
255
|
+
]),
|
|
256
|
+
),
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
259
|
+
...(options.devDependencies
|
|
260
|
+
? {
|
|
261
|
+
"dev-dependencies": Object.fromEntries(
|
|
262
|
+
Object.entries(options.devDependencies).map(([name, value]) => [
|
|
263
|
+
name,
|
|
264
|
+
cargoDependency(value, readWorkspaceVersion(parent.outdir)),
|
|
265
|
+
]),
|
|
266
|
+
),
|
|
267
|
+
}
|
|
268
|
+
: {}),
|
|
269
|
+
};
|
|
270
|
+
new TextFile(this, "Cargo.toml", { lines: renderToml(manifest).trimEnd().split("\n") });
|
|
271
|
+
if (this.uniffi) {
|
|
272
|
+
new TextFile(this, "uniffi-bindgen.rs", {
|
|
273
|
+
lines: ["fn main() {", " uniffi::uniffi_bindgen_main();", "}", ""],
|
|
274
|
+
});
|
|
275
|
+
new TextFile(this, "uniffi.toml", {
|
|
276
|
+
lines: renderToml(
|
|
277
|
+
options.uniffiConfig ?? {
|
|
278
|
+
"bindings.python": { cdylib_name: crateName.replaceAll("-", "_") },
|
|
279
|
+
"bindings.typescript": { strictTypeChecking: true },
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
.trimEnd()
|
|
283
|
+
.split("\n"),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Generated Rust workspace plus convention-derived private UniFFI packages. */
|
|
290
|
+
export class DBXToolsRustWorkspace {
|
|
291
|
+
readonly packages: readonly DBXToolsRustProject[];
|
|
292
|
+
readonly nodePackages: readonly DBXToolsTypeScriptProject[];
|
|
293
|
+
readonly pythonPackages: readonly PythonPackageOptions[];
|
|
294
|
+
readonly bindingMappings: readonly RustBindingMapping[];
|
|
295
|
+
readonly workspaceMapping: RustWorkspaceMapping;
|
|
296
|
+
|
|
297
|
+
constructor(project: javascript.NodeProject, options: DBXToolsRustWorkspaceOptions) {
|
|
298
|
+
const root = options.root ?? "packages/rs";
|
|
299
|
+
const nodeRoot = options.nodeRoot ?? "packages/js/node";
|
|
300
|
+
const packageOptions = options.packages ?? {};
|
|
301
|
+
this.packages = discoverRustCrates(resolve(project.outdir, root)).map(
|
|
302
|
+
(directory) =>
|
|
303
|
+
new DBXToolsRustProject(project, root, options.scope, {
|
|
304
|
+
directory,
|
|
305
|
+
...packageOptions[directory],
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const bindings = this.packages.filter((pkg) => pkg.uniffi);
|
|
310
|
+
this.bindingMappings = bindings.map((pkg) => {
|
|
311
|
+
const targets = pkg.packageOptions.bindings ?? ["node", "python"];
|
|
312
|
+
const packageName = pkg.packageOptions.directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
313
|
+
return {
|
|
314
|
+
crate: pkg.crateName,
|
|
315
|
+
rust: `${root}/${pkg.packageOptions.directory}`,
|
|
316
|
+
...(targets.includes("node")
|
|
317
|
+
? {
|
|
318
|
+
node: `${nodeRoot}/${pkg.packageOptions.directory}`,
|
|
319
|
+
nodePackage: `@${string.toSlug(options.scope)}/${packageName}`,
|
|
320
|
+
}
|
|
321
|
+
: {}),
|
|
322
|
+
...(targets.includes("python")
|
|
323
|
+
? {
|
|
324
|
+
python: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`,
|
|
325
|
+
pythonPackage: pkg.crateName,
|
|
326
|
+
}
|
|
327
|
+
: {}),
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
this.workspaceMapping = {
|
|
331
|
+
root,
|
|
332
|
+
crates: this.packages.map((pkg) => `${root}/${pkg.packageOptions.directory}`),
|
|
333
|
+
bindings: this.bindingMappings,
|
|
334
|
+
};
|
|
335
|
+
project.dbxToolsConfig.rust = this.workspaceMapping;
|
|
336
|
+
this.pythonPackages = bindings
|
|
337
|
+
.filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("python"))
|
|
338
|
+
.map((pkg) => ({
|
|
339
|
+
directory: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`.replace(
|
|
340
|
+
/^packages\/py\//,
|
|
341
|
+
"",
|
|
342
|
+
),
|
|
343
|
+
name: pkg.crateName,
|
|
344
|
+
module: `${options.pythonModulePrefix ?? options.scope.replaceAll("-", "_")}.${pkg.packageOptions.directory.replaceAll("-", "_")}`,
|
|
345
|
+
description: `Python bindings for ${pkg.crateName}`,
|
|
346
|
+
private: true,
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
const existing = new Map(
|
|
350
|
+
project.subprojects.map((child) => [relative(project.outdir, child.outdir), child]),
|
|
351
|
+
);
|
|
352
|
+
const nodePackages: DBXToolsTypeScriptProject[] = [];
|
|
353
|
+
for (const binding of bindings.filter((pkg) =>
|
|
354
|
+
(pkg.packageOptions.bindings ?? ["node", "python"]).includes("node"),
|
|
355
|
+
)) {
|
|
356
|
+
const directory = binding.packageOptions.directory;
|
|
357
|
+
const memberPath = `${nodeRoot}/${directory}`;
|
|
358
|
+
const found = existing.get(memberPath);
|
|
359
|
+
const node =
|
|
360
|
+
found instanceof DBXToolsTypeScriptProject
|
|
361
|
+
? found
|
|
362
|
+
: new DBXToolsTypeScriptProject({
|
|
363
|
+
parent: project,
|
|
364
|
+
outdir: memberPath,
|
|
365
|
+
name: `@${string.toSlug(options.scope)}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
|
|
366
|
+
tags: ["node"],
|
|
367
|
+
});
|
|
368
|
+
node.package.addField(
|
|
369
|
+
"name",
|
|
370
|
+
`@${string.toSlug(options.scope)}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
|
|
371
|
+
);
|
|
372
|
+
node.package.addField("private", true);
|
|
373
|
+
node.package.file.addDeletionOverride("publishConfig");
|
|
374
|
+
node.package.addField("description", `Node bindings for ${binding.crateName}`);
|
|
375
|
+
const releaseTargets = options.releaseTargets ?? UNIFFI_RELEASE_TARGETS;
|
|
376
|
+
if (options.release ?? true) {
|
|
377
|
+
node.package.addField(
|
|
378
|
+
"optionalDependencies",
|
|
379
|
+
Object.fromEntries(
|
|
380
|
+
releaseTargets.map((target) => [
|
|
381
|
+
`@${string.toSlug(options.scope)}/${directory}-${target.node}`,
|
|
382
|
+
readWorkspaceVersion(project.outdir),
|
|
383
|
+
]),
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
node.addDeps("@ubjs/core@0.31.0-5", "@ubjs/node@0.31.0-5");
|
|
388
|
+
if (binding.packageOptions.nodeDependencies?.length) {
|
|
389
|
+
node.addDeps(...binding.packageOptions.nodeDependencies);
|
|
390
|
+
}
|
|
391
|
+
node.addDevDeps("uniffi-bindgen-react-native@0.31.0-5");
|
|
392
|
+
if (binding.packageOptions.nodeDevDependencies?.length) {
|
|
393
|
+
node.addDevDeps(...binding.packageOptions.nodeDevDependencies);
|
|
394
|
+
}
|
|
395
|
+
node.compileTask.reset();
|
|
396
|
+
new TextFile(node, "exports.ts", {
|
|
397
|
+
lines: ['export * from "./src/bindings.ts";', ""],
|
|
398
|
+
});
|
|
399
|
+
nodePackages.push(node);
|
|
400
|
+
}
|
|
401
|
+
this.nodePackages = nodePackages;
|
|
402
|
+
|
|
403
|
+
const generatedWorkspaceManifest: Record<string, unknown> = {
|
|
404
|
+
workspace: {
|
|
405
|
+
members: this.packages.map((pkg) => `${root}/${pkg.packageOptions.directory}`),
|
|
406
|
+
"default-members": this.packages
|
|
407
|
+
.filter((pkg) => !pkg.uniffi)
|
|
408
|
+
.map((pkg) => `${root}/${pkg.packageOptions.directory}`),
|
|
409
|
+
resolver: "2",
|
|
410
|
+
},
|
|
411
|
+
"workspace.package": {
|
|
412
|
+
version: readWorkspaceVersion(project.outdir),
|
|
413
|
+
edition: options.edition ?? "2021",
|
|
414
|
+
"rust-version": options.rustVersion ?? "1.82",
|
|
415
|
+
license: options.license ?? "Apache-2.0",
|
|
416
|
+
repository: options.repository ?? "",
|
|
417
|
+
},
|
|
418
|
+
...(options.workspaceDependencies
|
|
419
|
+
? {
|
|
420
|
+
"workspace.dependencies": Object.fromEntries(
|
|
421
|
+
Object.entries(options.workspaceDependencies).map(([name, value]) => [
|
|
422
|
+
name,
|
|
423
|
+
cargoDependency(value),
|
|
424
|
+
]),
|
|
425
|
+
),
|
|
426
|
+
}
|
|
427
|
+
: {}),
|
|
428
|
+
};
|
|
429
|
+
new TextFile(project, "Cargo.toml", {
|
|
430
|
+
lines: renderToml(generatedWorkspaceManifest).trimEnd().split("\n"),
|
|
431
|
+
});
|
|
432
|
+
project.addTask("rs:format", { exec: "cargo fmt --all" });
|
|
433
|
+
project.addTask("rs:lint", { exec: "cargo clippy --workspace --all-targets --all-features" });
|
|
434
|
+
project.addTask("rs:test", { exec: "cargo test --workspace" });
|
|
435
|
+
project.addTask("rs:build", { exec: "cargo build --workspace" });
|
|
436
|
+
project.addTask("rs:bindings", {
|
|
437
|
+
exec: "bun node_modules/@dbx-tools/projen/tasks/rust.ts",
|
|
438
|
+
description: "Generate language bindings for UniFFI-enabled Rust crates",
|
|
439
|
+
});
|
|
440
|
+
project.addTask("rs:bindings:demo", {
|
|
441
|
+
description: "Generate and run UniFFI Node and Python example CLIs",
|
|
442
|
+
exec: [
|
|
443
|
+
"bun run rs:bindings",
|
|
444
|
+
...this.bindingMappings.flatMap((binding) => [
|
|
445
|
+
...(binding.node ? [`bun ${binding.node}/test/cli.ts`] : []),
|
|
446
|
+
...(binding.python
|
|
447
|
+
? [`uv run --project ${binding.python} ${binding.python}/test/cli.py`]
|
|
448
|
+
: []),
|
|
449
|
+
]),
|
|
450
|
+
].join(" && "),
|
|
451
|
+
});
|
|
452
|
+
if ((options.release ?? true) && this.bindingMappings.length > 0) {
|
|
453
|
+
this.addReleaseWorkflow(project, options.releaseTargets ?? UNIFFI_RELEASE_TARGETS);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private addReleaseWorkflow(
|
|
458
|
+
project: javascript.NodeProject,
|
|
459
|
+
targets: readonly UniFFIReleaseTarget[],
|
|
460
|
+
): void {
|
|
461
|
+
if (!project.github) return;
|
|
462
|
+
const bindings = this.bindingMappings.map((binding) => ({
|
|
463
|
+
...binding,
|
|
464
|
+
node: binding.node ?? "",
|
|
465
|
+
python: binding.python ?? "",
|
|
466
|
+
nodePackage: binding.nodePackage ?? "",
|
|
467
|
+
pythonPackage: binding.pythonPackage ?? "",
|
|
468
|
+
}));
|
|
469
|
+
const matrix = bindings.flatMap((binding) =>
|
|
470
|
+
targets.map((target, index) => ({
|
|
471
|
+
binding,
|
|
472
|
+
target: { ...target, facade: index === 0 },
|
|
473
|
+
})),
|
|
474
|
+
);
|
|
475
|
+
new YamlFile(project, ".github/workflows/rust-release.yml", {
|
|
476
|
+
obj: {
|
|
477
|
+
name: "rust-release",
|
|
478
|
+
on: {
|
|
479
|
+
push: { tags: ["v*"] },
|
|
480
|
+
workflow_dispatch: {
|
|
481
|
+
inputs: {
|
|
482
|
+
version: {
|
|
483
|
+
description: "Version to package during a dry run",
|
|
484
|
+
type: "string",
|
|
485
|
+
default: "0.0.0.dev0",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
permissions: { contents: "read" },
|
|
491
|
+
jobs: {
|
|
492
|
+
build: {
|
|
493
|
+
name: "${{ matrix.binding.crate }} / ${{ matrix.target.node }}",
|
|
494
|
+
"runs-on": "${{ matrix.target.runner }}",
|
|
495
|
+
strategy: {
|
|
496
|
+
"fail-fast": false,
|
|
497
|
+
matrix: { include: matrix },
|
|
498
|
+
},
|
|
499
|
+
steps: [
|
|
500
|
+
{ name: "Checkout", uses: "actions/checkout@v6" },
|
|
501
|
+
{
|
|
502
|
+
name: "Setup Bun",
|
|
503
|
+
uses: "oven-sh/setup-bun@v2",
|
|
504
|
+
with: { "bun-version": "1.3.14" },
|
|
505
|
+
},
|
|
506
|
+
{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
|
|
507
|
+
{
|
|
508
|
+
name: "Setup Rust",
|
|
509
|
+
uses: "dtolnay/rust-toolchain@stable",
|
|
510
|
+
with: { targets: "${{ matrix.target.cargo }}" },
|
|
511
|
+
},
|
|
512
|
+
{ name: "Install", run: "bun install" },
|
|
513
|
+
{
|
|
514
|
+
name: "Build thin native packages",
|
|
515
|
+
shell: "bash",
|
|
516
|
+
env: {
|
|
517
|
+
VERSION:
|
|
518
|
+
"${{ github.event_name == 'push' && github.ref_name || inputs.version }}",
|
|
519
|
+
},
|
|
520
|
+
run: 'bun node_modules/@dbx-tools/projen/tasks/uniffi-release.ts build --crate "${{ matrix.binding.crate }}" --rust "${{ matrix.binding.rust }}" --node "${{ matrix.binding.node }}" --python "${{ matrix.binding.python }}" --node-package "${{ matrix.binding.nodePackage }}" --python-package "${{ matrix.binding.pythonPackage }}" --cargo-target "${{ matrix.target.cargo }}" --node-triple "${{ matrix.target.node }}" --python-tag "${{ matrix.target.python }}" --os "${{ matrix.target.os }}" --cpu "${{ matrix.target.cpu }}" --libc "${{ matrix.target.libc }}" --facade "${{ matrix.target.facade }}" --version "${VERSION#v}"',
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: "Upload native packages",
|
|
524
|
+
uses: "actions/upload-artifact@v7",
|
|
525
|
+
with: {
|
|
526
|
+
name: "${{ matrix.binding.crate }}-${{ matrix.target.node }}",
|
|
527
|
+
path: [
|
|
528
|
+
"dist/uniffi/npm/*.tgz",
|
|
529
|
+
"dist/uniffi/npm-facade/*.tgz",
|
|
530
|
+
"dist/uniffi/python/*.whl",
|
|
531
|
+
].join("\n"),
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
},
|
|
536
|
+
publish: {
|
|
537
|
+
if: "${{ github.event_name == 'push' }}",
|
|
538
|
+
needs: ["build"],
|
|
539
|
+
"runs-on": "ubuntu-latest",
|
|
540
|
+
strategy: { matrix: { binding: bindings } },
|
|
541
|
+
permissions: { contents: "read", "id-token": "write" },
|
|
542
|
+
environment: { name: "native-${{ matrix.binding.crate }}" },
|
|
543
|
+
steps: [
|
|
544
|
+
{ name: "Checkout", uses: "actions/checkout@v6" },
|
|
545
|
+
{
|
|
546
|
+
name: "Setup Node.js",
|
|
547
|
+
uses: "actions/setup-node@v6",
|
|
548
|
+
with: { "registry-url": "https://registry.npmjs.org" },
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
name: "Setup Bun",
|
|
552
|
+
uses: "oven-sh/setup-bun@v2",
|
|
553
|
+
with: { "bun-version": "1.3.14" },
|
|
554
|
+
},
|
|
555
|
+
{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
|
|
556
|
+
{ name: "Install", run: "bun install" },
|
|
557
|
+
{
|
|
558
|
+
name: "Publish workspace npm dependencies",
|
|
559
|
+
if: "${{ matrix.binding.node != '' }}",
|
|
560
|
+
env: {
|
|
561
|
+
NPM_CONFIG_TOKEN: "${{ secrets.NPM_TOKEN }}",
|
|
562
|
+
NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}",
|
|
563
|
+
},
|
|
564
|
+
run: [
|
|
565
|
+
'VERSION="${GITHUB_REF_NAME#v}"',
|
|
566
|
+
'bun node_modules/@dbx-tools/projen/tasks/publish.ts "$VERSION" --exclude projen --exclude "${{ matrix.binding.node }}"',
|
|
567
|
+
].join("\n"),
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "Download packages",
|
|
571
|
+
uses: "actions/download-artifact@v8",
|
|
572
|
+
with: {
|
|
573
|
+
pattern: "${{ matrix.binding.crate }}-*",
|
|
574
|
+
path: "dist/uniffi",
|
|
575
|
+
"merge-multiple": true,
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "Publish npm packages",
|
|
580
|
+
env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" },
|
|
581
|
+
run: 'for package in dist/uniffi/npm/*.tgz; do npm publish "$package" --access public; done',
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: "Publish npm facades",
|
|
585
|
+
if: "${{ matrix.binding.node != '' }}",
|
|
586
|
+
env: { NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}" },
|
|
587
|
+
run: 'for package in dist/uniffi/npm-facade/*.tgz; do npm publish "$package" --access public; done',
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: "Publish Python wheels",
|
|
591
|
+
if: "${{ matrix.binding.python != '' }}",
|
|
592
|
+
run: "uv publish --trusted-publishing always dist/uniffi/python/*.whl",
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
"publish-cargo": {
|
|
597
|
+
if: "${{ github.event_name == 'push' }}",
|
|
598
|
+
needs: ["build"],
|
|
599
|
+
"runs-on": "ubuntu-latest",
|
|
600
|
+
permissions: { contents: "read" },
|
|
601
|
+
steps: [
|
|
602
|
+
{ name: "Checkout", uses: "actions/checkout@v6" },
|
|
603
|
+
{ name: "Setup Rust", uses: "dtolnay/rust-toolchain@stable" },
|
|
604
|
+
{
|
|
605
|
+
name: "Publish public crates",
|
|
606
|
+
env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" },
|
|
607
|
+
run: "cargo publish --workspace --registry crates-io",
|
|
608
|
+
},
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
"publish-local": {
|
|
612
|
+
if: "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}",
|
|
613
|
+
"runs-on": ["self-hosted"],
|
|
614
|
+
permissions: { contents: "read" },
|
|
615
|
+
steps: [
|
|
616
|
+
{ name: "Checkout", uses: "actions/checkout@v6" },
|
|
617
|
+
{
|
|
618
|
+
name: "Setup Bun",
|
|
619
|
+
uses: "oven-sh/setup-bun@v2",
|
|
620
|
+
with: { "bun-version": "1.3.14" },
|
|
621
|
+
},
|
|
622
|
+
{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
|
|
623
|
+
{ name: "Setup Rust", uses: "dtolnay/rust-toolchain@stable" },
|
|
624
|
+
{ name: "Install", run: "bun install" },
|
|
625
|
+
{
|
|
626
|
+
name: "Build and publish host-native packages",
|
|
627
|
+
run: [
|
|
628
|
+
'VERSION="${GITHUB_REF_NAME#v}"',
|
|
629
|
+
'bun node_modules/@dbx-tools/projen/tasks/publish-uniffi-local.ts --version "$VERSION" --registry "${{ vars.LOCAL_NPM_REGISTRY }}" --pypi-publish-url "${{ vars.LOCAL_PYPI_PUBLISH_URL }}" --cargo-registry "${{ vars.LOCAL_CARGO_REGISTRY }}"',
|
|
630
|
+
].join("\n"),
|
|
631
|
+
env: {
|
|
632
|
+
NODE_AUTH_TOKEN: "${{ secrets.LOCAL_NPM_TOKEN }}",
|
|
633
|
+
UV_PUBLISH_USERNAME: "${{ secrets.LOCAL_PYPI_USERNAME }}",
|
|
634
|
+
UV_PUBLISH_PASSWORD: "${{ secrets.LOCAL_PYPI_PASSWORD }}",
|
|
635
|
+
CARGO_REGISTRY_TOKEN: "${{ secrets.LOCAL_CARGO_TOKEN }}",
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
package/src/project.ts
CHANGED
|
@@ -12,9 +12,10 @@ import * as projectPredicate from "./project-predicate.ts";
|
|
|
12
12
|
|
|
13
13
|
export * from "./project-js.ts";
|
|
14
14
|
export * from "./project-py.ts";
|
|
15
|
+
export * from "./project-rs.ts";
|
|
15
16
|
|
|
16
17
|
/** Runtime family implemented by a dbx-tools project. */
|
|
17
|
-
export type DBXToolsProjectLanguage = "javascript" | "python";
|
|
18
|
+
export type DBXToolsProjectLanguage = "javascript" | "python" | "rust";
|
|
18
19
|
|
|
19
20
|
/** Options shared by every dbx-tools project implementation. */
|
|
20
21
|
export interface DBXToolsProjectOptions extends Partial<
|