@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.
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env -S bun
2
+ import {
3
+ chmodSync,
4
+ cpSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ statSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { parseArgs } from "node:util";
15
+ import { spawnSync } from "node:child_process";
16
+
17
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
18
+ const parsed = parseArgs({
19
+ allowPositionals: true,
20
+ options: {
21
+ crate: { type: "string" },
22
+ rust: { type: "string" },
23
+ node: { type: "string" },
24
+ python: { type: "string" },
25
+ "node-package": { type: "string" },
26
+ "python-package": { type: "string" },
27
+ "cargo-target": { type: "string" },
28
+ "node-triple": { type: "string" },
29
+ "python-tag": { type: "string" },
30
+ os: { type: "string" },
31
+ cpu: { type: "string" },
32
+ libc: { type: "string" },
33
+ facade: { type: "string" },
34
+ version: { type: "string" },
35
+ },
36
+ });
37
+
38
+ const required = (name: keyof typeof parsed.values): string => {
39
+ const value = parsed.values[name];
40
+ if (!value) throw new Error(`Missing --${name}`);
41
+ return value;
42
+ };
43
+
44
+ const run = (command: string, args: string[], cwd = root): void => {
45
+ const result = spawnSync(command, args, { cwd, stdio: "inherit" });
46
+ if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`);
47
+ };
48
+
49
+ const replaceVersion = (source: string, version: string): string =>
50
+ source.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
51
+
52
+ function build(): void {
53
+ const crate = required("crate");
54
+ required("rust");
55
+ const cargoTarget = required("cargo-target");
56
+ const nodeTriple = required("node-triple");
57
+ const pythonTag = required("python-tag");
58
+ const version = required("version");
59
+ const os = required("os");
60
+ const cpu = required("cpu");
61
+ const nodeDirectory = parsed.values.node;
62
+ const pythonDirectory = parsed.values.python;
63
+ const nodePackage = parsed.values["node-package"];
64
+ const pythonPackage = parsed.values["python-package"];
65
+ const libraryName = crate.replaceAll("-", "_");
66
+ const extension = os === "darwin" ? "dylib" : os === "win32" ? "dll" : "so";
67
+ const prefix = os === "win32" ? "" : "lib";
68
+ const libraryFile = `${prefix}${libraryName}.${extension}`;
69
+ const library = resolve(root, "target", cargoTarget, "release", libraryFile);
70
+ const output = resolve(root, "dist/uniffi");
71
+
72
+ rmSync(output, { recursive: true, force: true });
73
+ mkdirSync(join(output, "npm"), { recursive: true });
74
+ mkdirSync(join(output, "npm-facade"), { recursive: true });
75
+ mkdirSync(join(output, "python"), { recursive: true });
76
+ run("cargo", ["build", "--release", "--package", crate, "--target", cargoTarget]);
77
+ if (!existsSync(library)) throw new Error(`Missing native library ${library}`);
78
+
79
+ if (nodeDirectory && nodePackage) {
80
+ const nativePackage = resolve(output, "native-node");
81
+ mkdirSync(nativePackage, { recursive: true });
82
+ cpSync(library, join(nativePackage, libraryFile));
83
+ writeFileSync(
84
+ join(nativePackage, "package.json"),
85
+ `${JSON.stringify(
86
+ {
87
+ name: `${nodePackage}-${nodeTriple}`,
88
+ version,
89
+ description: `Native ${nodeTriple} library for ${nodePackage}`,
90
+ license: "Apache-2.0",
91
+ os: [os],
92
+ cpu: [cpu],
93
+ ...(parsed.values.libc ? { libc: [parsed.values.libc] } : {}),
94
+ files: [libraryFile],
95
+ },
96
+ null,
97
+ 2,
98
+ )}\n`,
99
+ );
100
+ run("npm", ["pack", "--pack-destination", resolve(output, "npm")], nativePackage);
101
+
102
+ if (parsed.values.facade === "true") {
103
+ const facade = resolve(output, "facade-node");
104
+ cpSync(resolve(root, nodeDirectory), facade, { recursive: true });
105
+ rmSync(join(facade, "src", libraryFile), { force: true });
106
+ const manifestPath = join(facade, "package.json");
107
+ const manifestMode = statSync(manifestPath).mode;
108
+ chmodSync(manifestPath, manifestMode | 0o200);
109
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
110
+ manifest.version = version;
111
+ manifest.private = false;
112
+ manifest.license = manifest.license === "UNLICENSED" ? "Apache-2.0" : manifest.license;
113
+ manifest.optionalDependencies = Object.fromEntries(
114
+ Object.keys(manifest.optionalDependencies ?? {}).map((name) => [name, version]),
115
+ );
116
+ manifest.dependencies = Object.fromEntries(
117
+ Object.entries(manifest.dependencies ?? {}).map(([name, dependency]) => [
118
+ name,
119
+ typeof dependency === "string" && dependency.startsWith("workspace:")
120
+ ? version
121
+ : dependency,
122
+ ]),
123
+ );
124
+ delete manifest.scripts;
125
+ delete manifest.devDependencies;
126
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
127
+ const generator = resolve(root, "node_modules/@dbx-tools/projen/tasks/uniffi.ts");
128
+ run("bun", [
129
+ generator,
130
+ "--crate",
131
+ crate,
132
+ "--node",
133
+ facade,
134
+ "--cargo-target",
135
+ cargoTarget,
136
+ "--node-package-base",
137
+ `${nodePackage}-`,
138
+ ]);
139
+ run("npm", ["pack", "--pack-destination", resolve(output, "npm-facade")], facade);
140
+ }
141
+ }
142
+
143
+ if (pythonDirectory && pythonPackage) {
144
+ const pythonRoot = resolve(output, "python-root");
145
+ cpSync(resolve(root, pythonDirectory), pythonRoot, { recursive: true });
146
+ const pyproject = join(pythonRoot, "pyproject.toml");
147
+ const mode = statSync(pyproject).mode;
148
+ chmodSync(pyproject, mode | 0o200);
149
+ writeFileSync(pyproject, replaceVersion(readFileSync(pyproject, "utf8"), version));
150
+ const generator = resolve(root, "node_modules/@dbx-tools/projen/tasks/uniffi.ts");
151
+ run("bun", [
152
+ generator,
153
+ "--crate",
154
+ crate,
155
+ "--python",
156
+ pythonRoot,
157
+ "--cargo-target",
158
+ cargoTarget,
159
+ ]);
160
+ run("uv", ["build", "--wheel", "--out-dir", resolve(output, "python")], pythonRoot);
161
+ const wheels = [...new Bun.Glob("*.whl").scanSync(join(output, "python"))];
162
+ if (wheels.length !== 1) throw new Error(`Expected one Python wheel, found ${wheels.length}`);
163
+ run("uvx", ["--from", "wheel", "wheel", "tags", "--remove", "--platform-tag", pythonTag, join(output, "python", wheels[0]!)]);
164
+ }
165
+ }
166
+
167
+ if (parsed.positionals[0] !== "build") throw new Error("Expected build command");
168
+ build();
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env -S bun
2
+ import {
3
+ cpSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ renameSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { basename, dirname, join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { parseArgs } from "node:util";
17
+ import { spawnSync } from "node:child_process";
18
+ import { makeReadonly, makeWritable, stampGenerated } from "../src/generated.ts";
19
+ import {
20
+ addExplicitInterfaceReexports,
21
+ makeDefaultedInterfaceParametersOptional,
22
+ } from "../src/uniffi.ts";
23
+
24
+ const { values } = parseArgs({
25
+ options: {
26
+ crate: { type: "string" },
27
+ node: { type: "string" },
28
+ python: { type: "string" },
29
+ "cargo-target": { type: "string" },
30
+ "node-package-base": { type: "string" },
31
+ },
32
+ });
33
+ if (!values.crate || (!values.node && !values.python)) {
34
+ throw new Error("Expected --crate and at least one of --node or --python");
35
+ }
36
+
37
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
38
+ const crate = values.crate;
39
+ const libraryName = crate.replaceAll("-", "_");
40
+ const extension =
41
+ process.platform === "darwin" ? "dylib" : process.platform === "win32" ? "dll" : "so";
42
+ const prefix = process.platform === "win32" ? "" : "lib";
43
+ const targetDirectory = values["cargo-target"]
44
+ ? join(root, "target", values["cargo-target"], "release")
45
+ : join(root, "target", "release");
46
+ const library = join(targetDirectory, `${prefix}${libraryName}.${extension}`);
47
+ const normalizedOutput = (value: string): string =>
48
+ value
49
+ .replace(/[\p{Extended_Pictographic}\uFE0F]/gu, "")
50
+ .replace(/[ \t]+$/gm, "")
51
+ .trim();
52
+ const run = (command: string, args: string[]) => {
53
+ const result = spawnSync(command, args, { cwd: root, encoding: "utf8" });
54
+ const stdout = normalizedOutput(result.stdout ?? "");
55
+ const stderr = normalizedOutput(result.stderr ?? "");
56
+ if (stdout) process.stdout.write(`${stdout}\n`);
57
+ if (stderr) process.stderr.write(`${stderr}\n`);
58
+ if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`);
59
+ };
60
+
61
+ const replaceGenerated = (source: string, destination: string): void => {
62
+ const deadline = Date.now() + 5_000;
63
+ while (!existsSync(source) && Date.now() < deadline) Bun.sleepSync(25);
64
+ if (!existsSync(source)) throw new Error(`Missing generated binding: ${source}`);
65
+ mkdirSync(dirname(destination), { recursive: true });
66
+ makeWritable(destination);
67
+ rmSync(destination, { force: true });
68
+ renameSync(source, destination);
69
+ };
70
+
71
+ const stampGeneratedPython = (file: string): void => {
72
+ makeWritable(file);
73
+ const body = readFileSync(file, "utf8");
74
+ writeFileSync(
75
+ file,
76
+ [
77
+ "# GENERATED by UniFFI binding generation - DO NOT EDIT.",
78
+ `# Regenerated from the ${crate} Rust exports.`,
79
+ "# Hand edits are overwritten on the next watch; this file is read-only.",
80
+ "",
81
+ body,
82
+ ].join("\n"),
83
+ );
84
+ makeReadonly(file);
85
+ };
86
+
87
+ run("cargo", [
88
+ "build",
89
+ "--release",
90
+ "--package",
91
+ crate,
92
+ ...(values["cargo-target"] ? ["--target", values["cargo-target"]] : []),
93
+ ]);
94
+ if (!existsSync(library)) throw new Error(`Missing compiled UniFFI library: ${library}`);
95
+
96
+ if (values.node) {
97
+ const nodeSource = resolve(root, values.node, "src");
98
+ const nodeOutput = mkdtempSync(join(tmpdir(), `${libraryName}-node-`));
99
+ run(join(root, "node_modules/.bin/ubrn"), [
100
+ "generate",
101
+ "napi",
102
+ "bindings",
103
+ "--library",
104
+ "--ts-dir",
105
+ nodeOutput,
106
+ ...(values["node-package-base"]
107
+ ? ["--lib-package-base", values["node-package-base"], "--lib-node-triple"]
108
+ : ["--lib-colocated"]),
109
+ library,
110
+ ]);
111
+ const generatedModules = readdirSync(nodeOutput).filter(
112
+ (file) => file.endsWith(".ts") && file !== "index.ts",
113
+ );
114
+ const linkedComponents = generatedModules.length > 2;
115
+ const nodeBindings = join(nodeSource, "bindings.ts");
116
+ const linkedNames = new Map(generatedModules.map((file) => [file, `_bindings-${file}`]));
117
+ const generatedFiles = linkedComponents
118
+ ? generatedModules.map((file) => join(nodeSource, linkedNames.get(file)!))
119
+ : [join(nodeSource, "_bindings.ts"), join(nodeSource, "_bindings-ffi.ts")];
120
+ replaceGenerated(join(nodeOutput, "index.ts"), nodeBindings);
121
+ if (linkedComponents) {
122
+ for (const file of generatedModules) {
123
+ replaceGenerated(join(nodeOutput, file), join(nodeSource, linkedNames.get(file)!));
124
+ }
125
+ } else {
126
+ replaceGenerated(join(nodeOutput, `${libraryName}.ts`), generatedFiles[0]);
127
+ replaceGenerated(join(nodeOutput, `${libraryName}-ffi.ts`), generatedFiles[1]);
128
+ writeFileSync(
129
+ nodeBindings,
130
+ readFileSync(nodeBindings, "utf8").replaceAll(`./${libraryName}`, "./_bindings"),
131
+ );
132
+ writeFileSync(
133
+ generatedFiles[0],
134
+ readFileSync(generatedFiles[0], "utf8").replaceAll(`./${libraryName}-ffi`, "./_bindings-ffi"),
135
+ );
136
+ }
137
+ if (linkedComponents) {
138
+ for (const file of [nodeBindings, ...generatedFiles]) {
139
+ let source = readFileSync(file, "utf8");
140
+ for (const [generated, destination] of linkedNames) {
141
+ const from = generated.slice(0, -3);
142
+ const to = destination.slice(0, -3);
143
+ source = source
144
+ .replaceAll(`'./${from}'`, `'./${to}'`)
145
+ .replaceAll(`"./${from}"`, `"./${to}"`);
146
+ }
147
+ writeFileSync(file, source);
148
+ }
149
+ }
150
+ for (const file of generatedFiles) {
151
+ writeFileSync(file, makeDefaultedInterfaceParametersOptional(readFileSync(file, "utf8")));
152
+ }
153
+ writeFileSync(
154
+ nodeBindings,
155
+ addExplicitInterfaceReexports(
156
+ readFileSync(nodeBindings, "utf8"),
157
+ generatedFiles.map((file) => ({
158
+ specifier: `./${basename(file, ".ts")}`,
159
+ source: readFileSync(file, "utf8"),
160
+ })),
161
+ ),
162
+ );
163
+ for (const file of [nodeBindings, ...generatedFiles]) {
164
+ stampGenerated(file, {
165
+ tool: "UniFFI binding generation",
166
+ source: `the ${crate} Rust exports`,
167
+ });
168
+ }
169
+ if (!values["node-package-base"]) {
170
+ const nodeLibrary = join(nodeSource, basename(library));
171
+ makeWritable(nodeLibrary);
172
+ cpSync(library, nodeLibrary);
173
+ makeReadonly(nodeLibrary);
174
+ }
175
+ rmSync(resolve(root, values.node, "src/generated"), { recursive: true, force: true });
176
+ rmSync(nodeOutput, { recursive: true, force: true });
177
+ run(process.execPath, [
178
+ resolve(dirname(fileURLToPath(import.meta.url)), "barrels.ts"),
179
+ "--dir",
180
+ resolve(root, values.node),
181
+ ]);
182
+ }
183
+
184
+ if (values.python) {
185
+ const pythonPackage = resolve(
186
+ root,
187
+ values.python,
188
+ "src/dbx_tools",
189
+ crate.replace(/^dbx-tools-/, "").replaceAll("-", "_"),
190
+ );
191
+ const pythonOutput = mkdtempSync(join(tmpdir(), `${libraryName}-python-`));
192
+ run("cargo", [
193
+ "run",
194
+ "--release",
195
+ "--package",
196
+ crate,
197
+ "--bin",
198
+ "uniffi-bindgen",
199
+ "--",
200
+ "generate",
201
+ "--language",
202
+ "python",
203
+ "--out-dir",
204
+ pythonOutput,
205
+ library,
206
+ ]);
207
+ const generated = join(pythonOutput, `${libraryName}.py`);
208
+ const pythonBindings = join(pythonPackage, "bindings.py");
209
+ replaceGenerated(generated, pythonBindings);
210
+ stampGeneratedPython(pythonBindings);
211
+ const pythonInit = join(pythonPackage, "__init__.py");
212
+ if (!existsSync(pythonInit)) {
213
+ writeFileSync(
214
+ pythonInit,
215
+ `\"\"\"Python bindings for ${crate}.\"\"\"\n\nfrom .bindings import * # noqa: F403\n`,
216
+ );
217
+ }
218
+ const pythonLibrary = join(pythonPackage, basename(library));
219
+ makeWritable(pythonLibrary);
220
+ cpSync(library, pythonLibrary);
221
+ makeReadonly(pythonLibrary);
222
+ rmSync(join(pythonPackage, "_generated"), { recursive: true, force: true });
223
+ rmSync(pythonOutput, { recursive: true, force: true });
224
+ }