@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
package/src/uniffi.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const defaultedParameters = (source: string): Map<string, Set<string>> => {
|
|
2
|
+
const methods = new Map<string, Set<string>>();
|
|
3
|
+
for (const match of source.matchAll(/^\s*(?:async\s+)?(\w+)\(([^)]*)\)/gm)) {
|
|
4
|
+
const parameters = match[2]
|
|
5
|
+
.split(",")
|
|
6
|
+
.map((parameter) => parameter.trim())
|
|
7
|
+
.filter((parameter) => parameter.includes("="))
|
|
8
|
+
.map((parameter) => parameter.match(/^(\w+)\s*:/)?.[1])
|
|
9
|
+
.filter((parameter): parameter is string => parameter !== undefined);
|
|
10
|
+
if (parameters.length > 0) methods.set(match[1], new Set(parameters));
|
|
11
|
+
}
|
|
12
|
+
return methods;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const makeDefaultedInterfaceParametersOptional = (source: string): string => {
|
|
16
|
+
const defaults = defaultedParameters(source);
|
|
17
|
+
return source.replace(/export interface \w+Like \{[\s\S]*?^\}/gm, (block) =>
|
|
18
|
+
block.replace(/^(\s*)(\w+)\(([^)]*)\)/gm, (signature, indent, method, parameters) => {
|
|
19
|
+
const defaulted = defaults.get(method);
|
|
20
|
+
if (!defaulted) return signature;
|
|
21
|
+
const repaired = parameters
|
|
22
|
+
.split(",")
|
|
23
|
+
.map((parameter: string) => {
|
|
24
|
+
const match = parameter.match(/^(\s*)(\w+)(\s*:\s*.*)$/);
|
|
25
|
+
if (!match || !defaulted.has(match[2])) return parameter;
|
|
26
|
+
return `${match[1]}${match[2]}?${match[3]}`;
|
|
27
|
+
})
|
|
28
|
+
.join(",");
|
|
29
|
+
return `${indent}${method}(${repaired})`;
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface TypeScriptBindingModule {
|
|
35
|
+
readonly specifier: string;
|
|
36
|
+
readonly source: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Add explicit type exports for interfaces that TypeScript misses through UBRN's star exports. */
|
|
40
|
+
export const addExplicitInterfaceReexports = (
|
|
41
|
+
facade: string,
|
|
42
|
+
modules: readonly TypeScriptBindingModule[],
|
|
43
|
+
): string => {
|
|
44
|
+
const exports = modules.flatMap(({ specifier, source }) => {
|
|
45
|
+
const names = [...source.matchAll(/^export interface (\w+)/gm)].map((match) => match[1]!);
|
|
46
|
+
return names.length > 0 ? [`export type { ${names.join(", ")} } from '${specifier}';`] : [];
|
|
47
|
+
});
|
|
48
|
+
if (exports.length === 0) return facade;
|
|
49
|
+
return `${facade.trimEnd()}\n${exports.join("\n")}\n`;
|
|
50
|
+
};
|
package/tasks/barrels.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
import { sep } from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
3
4
|
import { log, string } from "@dbx-tools/shared-core";
|
|
4
5
|
import { generateBarrels } from "../src/barrels.ts";
|
|
5
6
|
import { recordedPackages } from "../src/packages.ts";
|
|
6
7
|
import { watchLoop, watchRoots } from "../src/watch.ts";
|
|
7
8
|
|
|
8
9
|
const logger = log.logger("projen:barrels");
|
|
10
|
+
const { values } = parseArgs({
|
|
11
|
+
args: process.argv.slice(2),
|
|
12
|
+
options: {
|
|
13
|
+
watch: { type: "boolean" },
|
|
14
|
+
dir: { type: "string", multiple: true },
|
|
15
|
+
},
|
|
16
|
+
strict: false,
|
|
17
|
+
});
|
|
9
18
|
|
|
10
19
|
/** The recorded package dir that owns `abs`, if any (for a targeted barrel rebuild). */
|
|
11
20
|
function ownerPackageDir(abs: string, pkgDirs: string[]): string | undefined {
|
|
12
21
|
return pkgDirs.find((dir) => abs === dir || abs.startsWith(dir + sep));
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
if (
|
|
24
|
+
if (values.watch) {
|
|
16
25
|
// Watch the package roots; a source edit inside a package rebuilds just that
|
|
17
26
|
// package's `index.ts` barrel (no re-synth - the projenrc watcher owns that).
|
|
18
27
|
// watchLoop already drops generated paths, so a barrel write never re-triggers us.
|
|
@@ -44,7 +53,10 @@ if (process.argv.includes("--watch")) {
|
|
|
44
53
|
if (n) logger.success(`rebuilt ${string.pluralize(n, "barrel")}`);
|
|
45
54
|
});
|
|
46
55
|
} else {
|
|
47
|
-
const
|
|
56
|
+
const dirs = Array.isArray(values.dir)
|
|
57
|
+
? values.dir.filter((value): value is string => typeof value === "string")
|
|
58
|
+
: [];
|
|
59
|
+
const n = generateBarrels(dirs.length ? { dirs } : undefined);
|
|
48
60
|
logger.success(
|
|
49
61
|
n === 0 ? "barrels already up to date" : `updated ${string.pluralize(n, "barrel")}`,
|
|
50
62
|
);
|
package/tasks/bump.ts
CHANGED
|
@@ -48,12 +48,14 @@
|
|
|
48
48
|
* - a URL: always publish to that registry.
|
|
49
49
|
*/
|
|
50
50
|
import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
51
|
-
import {
|
|
51
|
+
import { homedir } from "node:os";
|
|
52
|
+
import { join, resolve } from "node:path";
|
|
52
53
|
import { fileURLToPath } from "node:url";
|
|
53
54
|
import { exec, project } from "@dbx-tools/core";
|
|
54
55
|
import { log, net } from "@dbx-tools/shared-core";
|
|
55
56
|
import { Command, Option } from "commander";
|
|
56
57
|
import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
|
|
58
|
+
import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
57
59
|
import {
|
|
58
60
|
type Semver,
|
|
59
61
|
compareSemver,
|
|
@@ -144,6 +146,21 @@ function resolveLocalRegistry(value: string): string | undefined {
|
|
|
144
146
|
return trimmed;
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
function localCargoRegistry(): string | undefined {
|
|
150
|
+
if (process.env.LOCAL_CARGO_REGISTRY) return process.env.LOCAL_CARGO_REGISTRY;
|
|
151
|
+
const config = join(homedir(), ".cargo", "config.toml");
|
|
152
|
+
if (!existsSync(config)) return undefined;
|
|
153
|
+
const source = readFileSync(config, "utf8");
|
|
154
|
+
const sections = [
|
|
155
|
+
...source.matchAll(/^\[registries\.([^\]]+)\]\s*\n([\s\S]*?)(?=^\[|(?![\s\S]))/gm),
|
|
156
|
+
];
|
|
157
|
+
for (const section of sections) {
|
|
158
|
+
const index = section[2]?.match(/^\s*index\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
159
|
+
if (index && net.isLoopbackHost(index.replace(/^sparse\+/, ""))) return section[1];
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
147
164
|
const program = new Command();
|
|
148
165
|
program
|
|
149
166
|
.description("Bump the release version, then commit, tag, and push it")
|
|
@@ -368,6 +385,57 @@ program
|
|
|
368
385
|
);
|
|
369
386
|
}
|
|
370
387
|
await Promise.all(localPublishes);
|
|
388
|
+
if (opts.version && (publishToLocalRegistry || localPypi)) {
|
|
389
|
+
const publishUniFFIScript = fileURLToPath(
|
|
390
|
+
new URL("./publish-uniffi-local.ts", import.meta.url),
|
|
391
|
+
);
|
|
392
|
+
await exec.spawn(
|
|
393
|
+
"bun",
|
|
394
|
+
[
|
|
395
|
+
publishUniFFIScript,
|
|
396
|
+
"--version",
|
|
397
|
+
version,
|
|
398
|
+
...(publishToLocalRegistry ? ["--registry", localRegistry] : []),
|
|
399
|
+
...(localPypi ? ["--pypi-publish-url", localPypi.publishUrl] : []),
|
|
400
|
+
],
|
|
401
|
+
{
|
|
402
|
+
cwd: process.cwd(),
|
|
403
|
+
stdout: "inherit",
|
|
404
|
+
stderr: "inherit",
|
|
405
|
+
stdin: "ignore",
|
|
406
|
+
check: true,
|
|
407
|
+
},
|
|
408
|
+
);
|
|
409
|
+
logger.success(`published host-native bindings for ${version}`);
|
|
410
|
+
}
|
|
411
|
+
const rust = readDbxToolsConfig(repoRoot)?.rust;
|
|
412
|
+
const hasRustCrates = Boolean(
|
|
413
|
+
rust &&
|
|
414
|
+
typeof rust === "object" &&
|
|
415
|
+
!Array.isArray(rust) &&
|
|
416
|
+
Array.isArray((rust as { crates?: unknown }).crates) &&
|
|
417
|
+
(rust as { crates: unknown[] }).crates.length,
|
|
418
|
+
);
|
|
419
|
+
if (opts.version && hasRustCrates) {
|
|
420
|
+
const cargoRegistry = localCargoRegistry();
|
|
421
|
+
if (cargoRegistry) {
|
|
422
|
+
const publishUniFFIScript = fileURLToPath(
|
|
423
|
+
new URL("./publish-uniffi-local.ts", import.meta.url),
|
|
424
|
+
);
|
|
425
|
+
await exec.spawn(
|
|
426
|
+
"bun",
|
|
427
|
+
[publishUniFFIScript, "--version", version, "--cargo-registry", cargoRegistry],
|
|
428
|
+
{
|
|
429
|
+
cwd: process.cwd(),
|
|
430
|
+
stdout: "inherit",
|
|
431
|
+
stderr: "inherit",
|
|
432
|
+
stdin: "ignore",
|
|
433
|
+
check: true,
|
|
434
|
+
},
|
|
435
|
+
);
|
|
436
|
+
logger.success(`published Rust ${version} to ${cargoRegistry}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
371
439
|
},
|
|
372
440
|
);
|
|
373
441
|
|
package/tasks/publish-python.ts
CHANGED
|
@@ -22,6 +22,11 @@ interface PythonProjectFile {
|
|
|
22
22
|
readonly source: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function isPrivatePythonProject(source: string): boolean {
|
|
26
|
+
const section = source.match(/^\[tool\.dbx-tools\]\s*\n([\s\S]*?)(?=^\[|(?![\s\S]))/m)?.[1] ?? "";
|
|
27
|
+
return /^\s*private\s*=\s*true\s*$/m.test(section);
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
export interface StampPythonProjectsOptions {
|
|
26
31
|
readonly rewriteDependencies?: boolean;
|
|
27
32
|
}
|
|
@@ -45,7 +50,7 @@ export function stampPythonProjects(
|
|
|
45
50
|
.map((entry) => resolve(root, entry.name, "pyproject.toml"))
|
|
46
51
|
.filter(existsSync)
|
|
47
52
|
.sort();
|
|
48
|
-
const
|
|
53
|
+
const allProjects: PythonProjectFile[] = packageFiles.map((path) => {
|
|
49
54
|
const source = readFileSync(path, "utf8");
|
|
50
55
|
const name = /^name = "([^"]+)"$/m.exec(source)?.[1];
|
|
51
56
|
if (!name) throw new Error(`Missing project name in ${path}`);
|
|
@@ -57,6 +62,7 @@ export function stampPythonProjects(
|
|
|
57
62
|
source,
|
|
58
63
|
};
|
|
59
64
|
});
|
|
65
|
+
const projects = allProjects.filter((project) => !isPrivatePythonProject(project.source));
|
|
60
66
|
if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
|
|
61
67
|
|
|
62
68
|
try {
|
|
@@ -67,7 +73,7 @@ export function stampPythonProjects(
|
|
|
67
73
|
}
|
|
68
74
|
let stamped = project.source.replace(versionPattern, `version = "${version}"`);
|
|
69
75
|
if (options.rewriteDependencies ?? true) {
|
|
70
|
-
for (const sibling of
|
|
76
|
+
for (const sibling of allProjects) {
|
|
71
77
|
stamped = stamped.replace(
|
|
72
78
|
new RegExp(
|
|
73
79
|
`${escapeRegExp(sibling.name)} @ git\\+[^" ]+#subdirectory=[^" ]+/${escapeRegExp(sibling.directory)}`,
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
import { chmodSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { arch, platform } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
9
|
+
import type { RustBindingMapping, RustWorkspaceMapping } from "../src/project-rs.ts";
|
|
10
|
+
|
|
11
|
+
const parsed = parseArgs({
|
|
12
|
+
options: {
|
|
13
|
+
version: { type: "string" },
|
|
14
|
+
registry: { type: "string" },
|
|
15
|
+
"pypi-publish-url": { type: "string" },
|
|
16
|
+
"cargo-registry": { type: "string" },
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function run(command: string, args: string[], capture = false): string {
|
|
21
|
+
const result = spawnSync(command, args, {
|
|
22
|
+
cwd: repoRoot,
|
|
23
|
+
encoding: capture ? "utf8" : undefined,
|
|
24
|
+
stdio: capture ? "pipe" : "inherit",
|
|
25
|
+
});
|
|
26
|
+
if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`);
|
|
27
|
+
return capture ? String(result.stdout).trim() : "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function commandAvailable(command: string): boolean {
|
|
31
|
+
return spawnSync(command, ["--version"], { stdio: "ignore" }).status === 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rustHost(): string {
|
|
35
|
+
const host = run("rustc", ["-vV"], true).match(/^host:\s+(.+)$/m)?.[1];
|
|
36
|
+
if (!host) throw new Error("Unable to detect the local Rust target from rustc -vV");
|
|
37
|
+
return host;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pythonTag(): string {
|
|
41
|
+
const value = run(
|
|
42
|
+
"uv",
|
|
43
|
+
[
|
|
44
|
+
"run",
|
|
45
|
+
"python",
|
|
46
|
+
"-c",
|
|
47
|
+
"import sysconfig; print(sysconfig.get_platform().replace('-', '_').replace('.', '_'))",
|
|
48
|
+
],
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
if (!value) throw new Error("Unable to detect the local Python wheel platform tag");
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function nativeTarget(): {
|
|
56
|
+
cpu: "arm64" | "x64";
|
|
57
|
+
libc?: "glibc";
|
|
58
|
+
node: string;
|
|
59
|
+
os: "darwin" | "linux" | "win32";
|
|
60
|
+
} {
|
|
61
|
+
const os = platform();
|
|
62
|
+
const machine = arch();
|
|
63
|
+
const cpu = machine === "arm64" ? "arm64" : machine === "x64" ? "x64" : undefined;
|
|
64
|
+
if (!cpu) throw new Error(`Unsupported local architecture: ${machine}`);
|
|
65
|
+
if (os === "darwin") return { os, cpu, node: `darwin-${cpu}` };
|
|
66
|
+
if (os === "win32") return { os, cpu, node: `win32-${cpu}-msvc` };
|
|
67
|
+
if (os === "linux") {
|
|
68
|
+
const libc = rustHost().includes("musl") ? undefined : "glibc";
|
|
69
|
+
return { os, cpu, node: `linux-${cpu}-${libc ? "gnu" : "musl"}`, ...(libc ? { libc } : {}) };
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Unsupported local platform: ${os}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rustConfig(): RustWorkspaceMapping | undefined {
|
|
75
|
+
const value = readDbxToolsConfig(repoRoot)?.rust;
|
|
76
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
77
|
+
const config = value as Partial<RustWorkspaceMapping>;
|
|
78
|
+
return Array.isArray(config.bindings) ? (config as RustWorkspaceMapping) : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function artifacts(directory: string, suffix: string): string[] {
|
|
82
|
+
if (!existsSync(directory)) return [];
|
|
83
|
+
return readdirSync(directory)
|
|
84
|
+
.filter((name) => name.endsWith(suffix))
|
|
85
|
+
.map((name) => join(directory, name));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function publishCargo(config: RustWorkspaceMapping, registry: string): void {
|
|
89
|
+
const manifests = config.crates
|
|
90
|
+
.map((crate) => resolve(repoRoot, crate, "Cargo.toml"))
|
|
91
|
+
.filter((manifest) => !/^publish = false$/m.test(readFileSync(manifest, "utf8")));
|
|
92
|
+
const originals = new Map(
|
|
93
|
+
manifests.map((manifest) => [manifest, readFileSync(manifest, "utf8")]),
|
|
94
|
+
);
|
|
95
|
+
try {
|
|
96
|
+
const crateNames = manifests.map(
|
|
97
|
+
(manifest) => readFileSync(manifest, "utf8").match(/^name = "([^"]+)"$/m)?.[1],
|
|
98
|
+
);
|
|
99
|
+
for (const manifest of manifests) {
|
|
100
|
+
const mode = statSync(manifest).mode;
|
|
101
|
+
let source = readFileSync(manifest, "utf8");
|
|
102
|
+
for (const crateName of crateNames) {
|
|
103
|
+
if (!crateName) continue;
|
|
104
|
+
source = source.replace(
|
|
105
|
+
new RegExp(
|
|
106
|
+
`(${crateName.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")} = \\{[^}]*)( \\})`,
|
|
107
|
+
"g",
|
|
108
|
+
),
|
|
109
|
+
`$1, registry = "${registry}"$2`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
chmodSync(manifest, mode | 0o200);
|
|
113
|
+
writeFileSync(manifest, source);
|
|
114
|
+
chmodSync(manifest, mode);
|
|
115
|
+
}
|
|
116
|
+
for (const crateName of crateNames) {
|
|
117
|
+
if (!crateName) continue;
|
|
118
|
+
run("cargo", [
|
|
119
|
+
"publish",
|
|
120
|
+
"--package",
|
|
121
|
+
crateName,
|
|
122
|
+
"--registry",
|
|
123
|
+
registry,
|
|
124
|
+
"--allow-dirty",
|
|
125
|
+
"--no-verify",
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
for (const [manifest, source] of originals) {
|
|
130
|
+
const mode = statSync(manifest).mode;
|
|
131
|
+
chmodSync(manifest, mode | 0o200);
|
|
132
|
+
writeFileSync(manifest, source);
|
|
133
|
+
chmodSync(manifest, mode);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildAndPublish(binding: RustBindingMapping, version: string): void {
|
|
139
|
+
const registry = parsed.values.registry;
|
|
140
|
+
const pypiPublishUrl = parsed.values["pypi-publish-url"];
|
|
141
|
+
const includeNode = Boolean(registry && binding.node && binding.nodePackage);
|
|
142
|
+
const includePython = Boolean(pypiPublishUrl && binding.python && binding.pythonPackage);
|
|
143
|
+
if (!includeNode && !includePython) return;
|
|
144
|
+
|
|
145
|
+
const target = nativeTarget();
|
|
146
|
+
const output = resolve(repoRoot, "dist/uniffi");
|
|
147
|
+
run("bun", [
|
|
148
|
+
resolve(dirname(fileURLToPath(import.meta.url)), "uniffi-release.ts"),
|
|
149
|
+
"build",
|
|
150
|
+
"--crate",
|
|
151
|
+
binding.crate,
|
|
152
|
+
"--rust",
|
|
153
|
+
binding.rust,
|
|
154
|
+
"--node",
|
|
155
|
+
includeNode ? binding.node! : "",
|
|
156
|
+
"--python",
|
|
157
|
+
includePython ? binding.python! : "",
|
|
158
|
+
"--node-package",
|
|
159
|
+
includeNode ? binding.nodePackage! : "",
|
|
160
|
+
"--python-package",
|
|
161
|
+
includePython ? binding.pythonPackage! : "",
|
|
162
|
+
"--cargo-target",
|
|
163
|
+
rustHost(),
|
|
164
|
+
"--node-triple",
|
|
165
|
+
target.node,
|
|
166
|
+
"--python-tag",
|
|
167
|
+
pythonTag(),
|
|
168
|
+
"--os",
|
|
169
|
+
target.os,
|
|
170
|
+
"--cpu",
|
|
171
|
+
target.cpu,
|
|
172
|
+
"--libc",
|
|
173
|
+
target.libc ?? "",
|
|
174
|
+
"--facade",
|
|
175
|
+
"true",
|
|
176
|
+
"--version",
|
|
177
|
+
version,
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
if (includeNode) {
|
|
181
|
+
const packages = [
|
|
182
|
+
...artifacts(join(output, "npm"), ".tgz"),
|
|
183
|
+
...artifacts(join(output, "npm-facade"), ".tgz"),
|
|
184
|
+
];
|
|
185
|
+
for (const packageFile of packages)
|
|
186
|
+
run("npm", ["publish", packageFile, "--registry", registry!]);
|
|
187
|
+
}
|
|
188
|
+
if (includePython) {
|
|
189
|
+
const wheels = artifacts(join(output, "python"), ".whl");
|
|
190
|
+
if (wheels.length === 0) throw new Error(`No wheel produced for ${binding.crate}`);
|
|
191
|
+
for (const wheel of wheels) run("uv", ["publish", "--publish-url", pypiPublishUrl!, wheel]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const version = parsed.values.version;
|
|
196
|
+
if (!version) throw new Error("Missing --version");
|
|
197
|
+
const config = rustConfig();
|
|
198
|
+
if (config?.bindings.length) {
|
|
199
|
+
if (!commandAvailable("cargo") || !commandAvailable("rustc")) {
|
|
200
|
+
throw new Error("Cargo and rustc are required because UniFFI Rust projects were detected");
|
|
201
|
+
}
|
|
202
|
+
for (const binding of config.bindings) buildAndPublish(binding, version);
|
|
203
|
+
}
|
|
204
|
+
const cargoRegistry = parsed.values["cargo-registry"];
|
|
205
|
+
if (cargoRegistry && config?.crates.length) {
|
|
206
|
+
publishCargo(config, cargoRegistry);
|
|
207
|
+
}
|
package/tasks/rust.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { log } from "@dbx-tools/shared-core";
|
|
7
|
+
import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
8
|
+
import {
|
|
9
|
+
discoverRustCrates,
|
|
10
|
+
hasUniFFIBindings,
|
|
11
|
+
type RustBindingMapping,
|
|
12
|
+
type RustWorkspaceMapping,
|
|
13
|
+
} from "../src/project-rs.ts";
|
|
14
|
+
import { runSynth } from "../src/scaffold.ts";
|
|
15
|
+
import { watchLoop } from "../src/watch.ts";
|
|
16
|
+
|
|
17
|
+
const logger = log.logger("projen:rust");
|
|
18
|
+
|
|
19
|
+
function rustConfig(): RustWorkspaceMapping | undefined {
|
|
20
|
+
const value = readDbxToolsConfig(repoRoot)?.rust;
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
22
|
+
const candidate = value as Partial<RustWorkspaceMapping>;
|
|
23
|
+
if (typeof candidate.root !== "string") return undefined;
|
|
24
|
+
if (!Array.isArray(candidate.crates) || !Array.isArray(candidate.bindings)) return undefined;
|
|
25
|
+
return candidate as RustWorkspaceMapping;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cargoAvailable(): boolean {
|
|
29
|
+
return spawnSync("cargo", ["--version"], { stdio: "ignore" }).status === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function currentStructure(config: RustWorkspaceMapping): RustWorkspaceMapping {
|
|
33
|
+
const root = resolve(repoRoot, config.root);
|
|
34
|
+
const directories = discoverRustCrates(root);
|
|
35
|
+
const crates = directories.map((directory) => `${config.root}/${directory}`);
|
|
36
|
+
const recorded = new Map(config.bindings.map((binding) => [binding.rust, binding]));
|
|
37
|
+
const bindings = crates.flatMap((rust) => {
|
|
38
|
+
if (!hasUniFFIBindings(resolve(repoRoot, rust))) return [];
|
|
39
|
+
const binding = recorded.get(rust);
|
|
40
|
+
return binding ? [binding] : [{ crate: "", rust }];
|
|
41
|
+
});
|
|
42
|
+
return { root: config.root, crates, bindings };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Whether discovered crate membership or UniFFI marker membership changed. */
|
|
46
|
+
export function rustStructureChanged(config: RustWorkspaceMapping): boolean {
|
|
47
|
+
return !sameStructure(config, currentStructure(config));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sameStructure(left: RustWorkspaceMapping, right: RustWorkspaceMapping): boolean {
|
|
51
|
+
return (
|
|
52
|
+
JSON.stringify(left.crates) === JSON.stringify(right.crates) &&
|
|
53
|
+
JSON.stringify(left.bindings.map((binding) => binding.rust)) ===
|
|
54
|
+
JSON.stringify(right.bindings.map((binding) => binding.rust))
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ownerBinding(
|
|
59
|
+
path: string,
|
|
60
|
+
bindings: readonly RustBindingMapping[],
|
|
61
|
+
): RustBindingMapping | undefined {
|
|
62
|
+
const absolute = isAbsolute(path) ? path : resolve(repoRoot, path);
|
|
63
|
+
return bindings.find((binding) => {
|
|
64
|
+
const directory = resolve(repoRoot, binding.rust);
|
|
65
|
+
return absolute === directory || absolute.startsWith(directory + sep);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function generate(binding: RustBindingMapping): void {
|
|
70
|
+
const targets = [
|
|
71
|
+
...(binding.node ? ["--node", binding.node] : []),
|
|
72
|
+
...(binding.python ? ["--python", binding.python] : []),
|
|
73
|
+
];
|
|
74
|
+
const result = spawnSync(
|
|
75
|
+
process.execPath,
|
|
76
|
+
[
|
|
77
|
+
resolve(dirname(fileURLToPath(import.meta.url)), "uniffi.ts"),
|
|
78
|
+
"--crate",
|
|
79
|
+
binding.crate,
|
|
80
|
+
...targets,
|
|
81
|
+
],
|
|
82
|
+
{ cwd: repoRoot, stdio: "inherit" },
|
|
83
|
+
);
|
|
84
|
+
if (result.status !== 0) throw new Error(`binding generation exited with ${result.status}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const config = rustConfig();
|
|
88
|
+
|
|
89
|
+
async function main(): Promise<void> {
|
|
90
|
+
if (!config || config.crates.length === 0 || !existsSync(resolve(repoRoot, config.root))) return;
|
|
91
|
+
if (!cargoAvailable()) {
|
|
92
|
+
throw new Error("Cargo is required because Rust projects were detected");
|
|
93
|
+
}
|
|
94
|
+
if (!process.argv.includes("--watch")) {
|
|
95
|
+
for (const binding of config.bindings) generate(binding);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
watchLoop("rust", [resolve(repoRoot, config.root)], (changed) => {
|
|
100
|
+
const latest = rustConfig() ?? config;
|
|
101
|
+
if (rustStructureChanged(latest)) {
|
|
102
|
+
logger.start("Rust project structure changed - re-synthesizing (+install)");
|
|
103
|
+
runSynth({ post: true });
|
|
104
|
+
logger.success("Rust project structure synchronized");
|
|
105
|
+
const refreshed = rustConfig();
|
|
106
|
+
if (!refreshed) return;
|
|
107
|
+
const targets = new Map<string, RustBindingMapping>();
|
|
108
|
+
for (const path of changed) {
|
|
109
|
+
const binding = ownerBinding(path, refreshed.bindings);
|
|
110
|
+
if (binding) targets.set(binding.crate, binding);
|
|
111
|
+
}
|
|
112
|
+
for (const binding of targets.values()) {
|
|
113
|
+
logger.start(`generating ${binding.crate} bindings`);
|
|
114
|
+
generate(binding);
|
|
115
|
+
logger.success(`generated ${binding.crate} bindings`);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const targets = new Map<string, RustBindingMapping>();
|
|
120
|
+
for (const path of changed) {
|
|
121
|
+
const binding = ownerBinding(path, latest.bindings);
|
|
122
|
+
if (binding) targets.set(binding.crate, binding);
|
|
123
|
+
}
|
|
124
|
+
for (const binding of targets.values()) {
|
|
125
|
+
logger.start(`generating ${binding.crate} bindings`);
|
|
126
|
+
generate(binding);
|
|
127
|
+
logger.success(`generated ${binding.crate} bindings`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (import.meta.main) await main();
|
package/tasks/sync.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { log } from "@dbx-tools/shared-core";
|
|
4
4
|
import concurrently from "concurrently";
|
|
5
|
+
import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
5
6
|
import { runSynth } from "../src/scaffold.ts";
|
|
6
7
|
|
|
7
8
|
const logger = log.logger("projen:sync");
|
|
@@ -24,6 +25,16 @@ function taskPath(script: string): string {
|
|
|
24
25
|
return fileURLToPath(new URL(`./${script}`, import.meta.url));
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/** Whether synth recorded at least one Rust crate for the focused watcher. */
|
|
29
|
+
function hasRustProjects(): boolean {
|
|
30
|
+
const rust = readDbxToolsConfig(repoRoot)?.rust;
|
|
31
|
+
if (!rust || typeof rust !== "object" || Array.isArray(rust)) return false;
|
|
32
|
+
return (
|
|
33
|
+
Array.isArray((rust as { crates?: unknown }).crates) &&
|
|
34
|
+
(rust as { crates: unknown[] }).crates.length > 0
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
if (!process.argv.includes("--watch")) {
|
|
28
39
|
// One-shot: full synth (+install + barrels via the post-synth component). This is
|
|
29
40
|
// the scriptable path, so a failed synth stays a failed exit code.
|
|
@@ -52,21 +63,26 @@ if (!process.argv.includes("--watch")) {
|
|
|
52
63
|
);
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
const watchers = [
|
|
67
|
+
{ command: `bun "${taskPath("projenrc.ts")}"`, name: "projenrc", prefixColor: "magenta" },
|
|
68
|
+
{ command: `bun "${taskPath("barrels.ts")}" --watch`, name: "barrels", prefixColor: "cyan" },
|
|
69
|
+
{ command: `bun "${taskPath("openapi.ts")}" --watch`, name: "openapi", prefixColor: "green" },
|
|
70
|
+
];
|
|
71
|
+
if (hasRustProjects()) {
|
|
72
|
+
watchers.push({
|
|
73
|
+
command: `bun "${taskPath("rust.ts")}" --watch`,
|
|
74
|
+
name: "rust",
|
|
75
|
+
prefixColor: "yellow",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const { result } = concurrently(watchers, {
|
|
79
|
+
prefix: "name",
|
|
80
|
+
// No `killOthersOn`: one watcher falling over is no reason to tear the other two
|
|
81
|
+
// down. `-1` is concurrently's spelling for "restart forever", so a crashed
|
|
82
|
+
// watcher comes back instead of silently leaving its outputs stale.
|
|
83
|
+
restartTries: -1,
|
|
84
|
+
restartDelay: RESTART_DELAY_MS,
|
|
85
|
+
});
|
|
70
86
|
|
|
71
87
|
let stopping = false;
|
|
72
88
|
|