@dbx-tools/projen 0.6.222 → 0.6.224
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 +41 -7
- package/index.ts +4 -2
- package/package.json +5 -5
- package/src/barrels.ts +10 -4
- package/src/bun-workflow.ts +36 -17
- package/src/clean.ts +3 -3
- package/src/codegen.ts +16 -8
- package/src/packages.ts +14 -13
- package/src/project-js.ts +316 -147
- package/src/project-py.ts +8 -25
- package/src/project-rs.ts +98 -71
- package/src/release-github.ts +37 -0
- package/src/release.ts +25 -36
- package/src/tsconfig.ts +17 -17
- package/tasks/local-publish.ts +3 -3
- package/tasks/publish-npm.ts +75 -4
- package/tasks/publish-python.ts +13 -29
- package/tasks/publish-uniffi-local.ts +12 -9
- package/tasks/publish.ts +2 -2
- package/tasks/release-pr.ts +27 -10
- package/tasks/uniffi-python.d.ts +56 -0
- package/tasks/uniffi-python.js +160 -0
- package/tasks/uniffi-release.mjs +25 -49
- package/tasks/uniffi.ts +11 -45
package/tasks/publish-npm.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
/** Idempotent npm archive publication for release recovery. */
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
readlinkSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join, relative, resolve } from "node:path";
|
|
6
15
|
import { exec } from "@dbx-tools/core";
|
|
7
16
|
import { log } from "@dbx-tools/shared-core";
|
|
8
17
|
import { Command } from "commander";
|
|
@@ -11,6 +20,7 @@ const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
|
11
20
|
const logger = log.logger("dbx-tools:publish-npm");
|
|
12
21
|
|
|
13
22
|
export interface NpmReleaseIdentity {
|
|
23
|
+
readonly contentDigest?: string;
|
|
14
24
|
readonly integrity?: string;
|
|
15
25
|
readonly name: string;
|
|
16
26
|
readonly repository?: unknown;
|
|
@@ -47,7 +57,18 @@ export function npmReleaseMatches(
|
|
|
47
57
|
if (localRepository && publishedRepository && localRepository !== publishedRepository) {
|
|
48
58
|
throw new Error(`Published npm repository does not match ${local.name}@${local.version}`);
|
|
49
59
|
}
|
|
50
|
-
if (
|
|
60
|
+
if (
|
|
61
|
+
local.contentDigest &&
|
|
62
|
+
published.contentDigest &&
|
|
63
|
+
local.contentDigest !== published.contentDigest
|
|
64
|
+
) {
|
|
65
|
+
throw new Error(`Published npm content does not match ${local.name}@${local.version}`);
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
!(local.contentDigest && published.contentDigest) &&
|
|
69
|
+
local.integrity &&
|
|
70
|
+
published.integrity !== local.integrity
|
|
71
|
+
) {
|
|
51
72
|
throw new Error(`Published npm integrity does not match ${local.name}@${local.version}`);
|
|
52
73
|
}
|
|
53
74
|
return true;
|
|
@@ -66,7 +87,7 @@ export async function publishedNpmRelease(
|
|
|
66
87
|
throw new Error(`npm registry lookup failed for ${name}@${version}: ${response.status}`);
|
|
67
88
|
}
|
|
68
89
|
const metadata = (await response.json()) as {
|
|
69
|
-
dist?: { integrity?: string };
|
|
90
|
+
dist?: { integrity?: string; tarball?: string };
|
|
70
91
|
name?: string;
|
|
71
92
|
repository?: unknown;
|
|
72
93
|
version?: string;
|
|
@@ -74,7 +95,23 @@ export async function publishedNpmRelease(
|
|
|
74
95
|
if (!metadata.name || !metadata.version) {
|
|
75
96
|
throw new Error(`npm registry returned an incomplete identity for ${name}@${version}`);
|
|
76
97
|
}
|
|
98
|
+
let contentDigest: string | undefined;
|
|
99
|
+
if (metadata.dist?.tarball) {
|
|
100
|
+
const archive = await fetch(metadata.dist.tarball);
|
|
101
|
+
if (!archive.ok) {
|
|
102
|
+
throw new Error(`npm tarball lookup failed for ${name}@${version}: ${archive.status}`);
|
|
103
|
+
}
|
|
104
|
+
const temp = mkdtempSync(join(tmpdir(), "dbx-tools-published-npm-"));
|
|
105
|
+
const path = join(temp, "package.tgz");
|
|
106
|
+
try {
|
|
107
|
+
writeFileSync(path, Buffer.from(await archive.arrayBuffer()));
|
|
108
|
+
contentDigest = npmArchiveContentDigest(path);
|
|
109
|
+
} finally {
|
|
110
|
+
rmSync(temp, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
77
113
|
return {
|
|
114
|
+
...(contentDigest ? { contentDigest } : {}),
|
|
78
115
|
integrity: metadata.dist?.integrity,
|
|
79
116
|
name: metadata.name,
|
|
80
117
|
repository: metadata.repository,
|
|
@@ -102,6 +139,7 @@ export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
|
|
|
102
139
|
throw new Error(`npm archive has no package name or version: ${path}`);
|
|
103
140
|
}
|
|
104
141
|
return {
|
|
142
|
+
contentDigest: npmArchiveContentDigest(path),
|
|
105
143
|
integrity: `sha512-${createHash("sha512").update(readFileSync(path)).digest("base64")}`,
|
|
106
144
|
name: manifest.name,
|
|
107
145
|
repository: manifest.repository,
|
|
@@ -109,6 +147,39 @@ export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
|
|
|
109
147
|
};
|
|
110
148
|
}
|
|
111
149
|
|
|
150
|
+
/** Hash paths, executable bits, symlink targets, and bytes while ignoring tar metadata. */
|
|
151
|
+
export function npmArchiveContentDigest(path: string): string {
|
|
152
|
+
const temp = mkdtempSync(join(tmpdir(), "dbx-tools-npm-content-"));
|
|
153
|
+
try {
|
|
154
|
+
exec.spawnSync("tar", ["-xzf", path, "-C", temp], {
|
|
155
|
+
cwd: process.cwd(),
|
|
156
|
+
stdout: "ignore",
|
|
157
|
+
stderr: "capture",
|
|
158
|
+
stdin: "ignore",
|
|
159
|
+
check: true,
|
|
160
|
+
});
|
|
161
|
+
const hash = createHash("sha512");
|
|
162
|
+
const visit = (directory: string): void => {
|
|
163
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
|
|
164
|
+
a.name.localeCompare(b.name),
|
|
165
|
+
)) {
|
|
166
|
+
const entryPath = join(directory, entry.name);
|
|
167
|
+
const name = relative(temp, entryPath).split("\\").join("/");
|
|
168
|
+
const stat = lstatSync(entryPath);
|
|
169
|
+
hash.update(`${entry.isDirectory() ? "D" : entry.isSymbolicLink() ? "L" : "F"}\0`);
|
|
170
|
+
hash.update(`${name}\0${stat.mode & 0o111}\0`);
|
|
171
|
+
if (entry.isDirectory()) visit(entryPath);
|
|
172
|
+
else if (entry.isSymbolicLink()) hash.update(readlinkSync(entryPath));
|
|
173
|
+
else hash.update(readFileSync(entryPath));
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
visit(temp);
|
|
177
|
+
return `sha512-${hash.digest("base64")}`;
|
|
178
|
+
} finally {
|
|
179
|
+
rmSync(temp, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
112
183
|
export function packNpmPackage(
|
|
113
184
|
directory: string,
|
|
114
185
|
destination: string,
|
package/tasks/publish-python.ts
CHANGED
|
@@ -13,20 +13,18 @@ import { tmpdir } from "node:os";
|
|
|
13
13
|
import { basename, join, resolve } from "node:path";
|
|
14
14
|
import { exec } from "@dbx-tools/core";
|
|
15
15
|
import { Command } from "commander";
|
|
16
|
+
import { parse, stringify } from "smol-toml";
|
|
17
|
+
import { pythonProjectInfo, stampPythonProject } from "./uniffi-python.js";
|
|
16
18
|
|
|
17
19
|
interface PythonProjectFile {
|
|
18
20
|
readonly directory: string;
|
|
19
21
|
readonly mode: number;
|
|
20
22
|
readonly name: string;
|
|
21
23
|
readonly path: string;
|
|
24
|
+
readonly private: boolean;
|
|
22
25
|
readonly source: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
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
|
-
|
|
30
28
|
export interface StampPythonProjectsOptions {
|
|
31
29
|
readonly rewriteDependencies?: boolean;
|
|
32
30
|
}
|
|
@@ -36,10 +34,6 @@ export interface RestorePythonProjects {
|
|
|
36
34
|
readonly paths: readonly string[];
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
function escapeRegExp(value: string): string {
|
|
40
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
37
|
export function stampPythonProjects(
|
|
44
38
|
root: string,
|
|
45
39
|
version: string,
|
|
@@ -52,37 +46,27 @@ export function stampPythonProjects(
|
|
|
52
46
|
.sort();
|
|
53
47
|
const allProjects: PythonProjectFile[] = packageFiles.map((path) => {
|
|
54
48
|
const source = readFileSync(path, "utf8");
|
|
55
|
-
const
|
|
56
|
-
if (!name) throw new Error(`Missing project name in ${path}`);
|
|
49
|
+
const info = pythonProjectInfo(source, { parse, stringify });
|
|
57
50
|
return {
|
|
58
51
|
directory: basename(resolve(path, "..")),
|
|
59
52
|
mode: statSync(path).mode,
|
|
60
|
-
name,
|
|
53
|
+
name: info.name,
|
|
61
54
|
path,
|
|
55
|
+
private: info.private,
|
|
62
56
|
source,
|
|
63
57
|
};
|
|
64
58
|
});
|
|
65
|
-
const projects = allProjects.filter((project) => !
|
|
59
|
+
const projects = allProjects.filter((project) => !project.private);
|
|
66
60
|
if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
|
|
67
61
|
|
|
68
62
|
try {
|
|
69
63
|
for (const project of projects) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
for (const sibling of allProjects) {
|
|
77
|
-
stamped = stamped.replace(
|
|
78
|
-
new RegExp(
|
|
79
|
-
`${escapeRegExp(sibling.name)} @ git\\+[^" ]+#subdirectory=[^" ]+/${escapeRegExp(sibling.directory)}`,
|
|
80
|
-
"g",
|
|
81
|
-
),
|
|
82
|
-
`${sibling.name}==${version}`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
64
|
+
const stamped = stampPythonProject(project.source, {
|
|
65
|
+
packages: allProjects,
|
|
66
|
+
rewriteDependencies: options.rewriteDependencies,
|
|
67
|
+
toml: { parse, stringify },
|
|
68
|
+
version,
|
|
69
|
+
});
|
|
86
70
|
chmodSync(project.path, project.mode | 0o200);
|
|
87
71
|
writeFileSync(project.path, stamped);
|
|
88
72
|
chmodSync(project.path, project.mode);
|
|
@@ -6,6 +6,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
import { log } from "@dbx-tools/shared-core";
|
|
9
|
+
import { parse, stringify } from "smol-toml";
|
|
9
10
|
import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
10
11
|
import type { RustBindingMapping, RustWorkspaceMapping } from "../src/project-rs.ts";
|
|
11
12
|
|
|
@@ -110,19 +111,21 @@ function publishCargo(config: RustWorkspaceMapping, registry: string, version: s
|
|
|
110
111
|
);
|
|
111
112
|
for (const manifest of manifests) {
|
|
112
113
|
const mode = statSync(manifest).mode;
|
|
113
|
-
|
|
114
|
+
const document = parse(readFileSync(manifest, "utf8")) as Record<string, unknown>;
|
|
114
115
|
for (const crateName of crateNames) {
|
|
115
116
|
if (!crateName) continue;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
for (const sectionName of ["dependencies", "dev-dependencies", "build-dependencies"]) {
|
|
118
|
+
const section = document[sectionName];
|
|
119
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) continue;
|
|
120
|
+
const dependency = (section as Record<string, unknown>)[crateName];
|
|
121
|
+
if (!dependency || typeof dependency !== "object" || Array.isArray(dependency)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
(dependency as Record<string, unknown>).registry = registry;
|
|
125
|
+
}
|
|
123
126
|
}
|
|
124
127
|
chmodSync(manifest, mode | 0o200);
|
|
125
|
-
writeFileSync(manifest,
|
|
128
|
+
writeFileSync(manifest, `${stringify(document).trimEnd()}\n`);
|
|
126
129
|
chmodSync(manifest, mode);
|
|
127
130
|
}
|
|
128
131
|
for (const crateName of crateNames) {
|
package/tasks/publish.ts
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
writeFileSync,
|
|
63
63
|
} from "node:fs";
|
|
64
64
|
import { tmpdir } from "node:os";
|
|
65
|
-
import { dirname, join, resolve } from "node:path";
|
|
65
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
66
66
|
import { exec } from "@dbx-tools/core";
|
|
67
67
|
import { log } from "@dbx-tools/shared-core";
|
|
68
68
|
import ts from "typescript";
|
|
@@ -253,7 +253,7 @@ function applyPublishConfig(pkgPath: string): void {
|
|
|
253
253
|
function enrichedPath(root: string): string {
|
|
254
254
|
const binDir = join(root, "node_modules", ".bin");
|
|
255
255
|
const current = process.env.PATH ?? "";
|
|
256
|
-
return current.split(
|
|
256
|
+
return current.split(delimiter).includes(binDir) ? current : `${binDir}${delimiter}${current}`;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
const [version, ...rest] = process.argv.slice(2);
|
package/tasks/release-pr.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
import { exec, project } from "@dbx-tools/core";
|
|
14
14
|
import { log } from "@dbx-tools/shared-core";
|
|
15
15
|
import { Command } from "commander";
|
|
16
|
+
import { publishLocalRelease } from "./local-publish.ts";
|
|
16
17
|
import {
|
|
17
18
|
releaseArchitectureOption,
|
|
18
19
|
releaseLevelOption,
|
|
@@ -21,7 +22,12 @@ import {
|
|
|
21
22
|
type ReleaseOs,
|
|
22
23
|
type VersionLevel,
|
|
23
24
|
} from "../src/_release-platform.ts";
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
githubRepositoryIdentity,
|
|
27
|
+
githubRepositorySpecifier,
|
|
28
|
+
githubTokenArguments,
|
|
29
|
+
githubTokenEnvironmentName,
|
|
30
|
+
} from "../src/release-github.ts";
|
|
25
31
|
import { readWorkspaceVersion, resolveNextVersion } from "../src/workspace-version.ts";
|
|
26
32
|
|
|
27
33
|
const logger = log.logger("projen:release");
|
|
@@ -100,13 +106,17 @@ function runIgnoringStdout(root: string, command: string, args: string[]): void
|
|
|
100
106
|
});
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
function githubAccount(root: string): {
|
|
109
|
+
function githubAccount(root: string): {
|
|
110
|
+
hostname: string;
|
|
111
|
+
owner: string;
|
|
112
|
+
repository: string;
|
|
113
|
+
token: string;
|
|
114
|
+
} {
|
|
104
115
|
const repository = project.repositoryUrl(root);
|
|
105
116
|
if (!repository) throw new Error("Release preparation requires a GitHub repository");
|
|
106
|
-
const
|
|
107
|
-
if (!owner || !name) throw new Error(`Cannot determine GitHub identity from ${repository}`);
|
|
117
|
+
const identity = githubRepositoryIdentity(repository);
|
|
108
118
|
const token = exec
|
|
109
|
-
.spawnSync("gh",
|
|
119
|
+
.spawnSync("gh", githubTokenArguments(identity.hostname), {
|
|
110
120
|
cwd: root,
|
|
111
121
|
stdout: "capture",
|
|
112
122
|
stderr: "ignore",
|
|
@@ -114,8 +124,10 @@ function githubAccount(root: string): { owner: string; repository: string; token
|
|
|
114
124
|
check: true,
|
|
115
125
|
})
|
|
116
126
|
.stdout?.trim();
|
|
117
|
-
if (!token)
|
|
118
|
-
|
|
127
|
+
if (!token) {
|
|
128
|
+
throw new Error(`No GitHub CLI authentication found for ${identity.hostname}`);
|
|
129
|
+
}
|
|
130
|
+
return { ...identity, token };
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
const program = new Command();
|
|
@@ -129,7 +141,7 @@ program
|
|
|
129
141
|
.addOption(releaseArchitectureOption())
|
|
130
142
|
.option("--local-registry <value>", "local npm registry: auto, false, or an explicit URL", "auto")
|
|
131
143
|
.option("--local-pypi <value>", "local PyPI index: auto, false, or an explicit URL", "auto")
|
|
132
|
-
.option("--python-root <path>", "Python workspace package root"
|
|
144
|
+
.option("--python-root <path>", "Python workspace package root")
|
|
133
145
|
.option("--no-local-cargo", "skip local Cargo publication")
|
|
134
146
|
.option("--approve", "merge the release branch directly into the release base")
|
|
135
147
|
.action(
|
|
@@ -142,7 +154,7 @@ program
|
|
|
142
154
|
arch: ReleaseArch[];
|
|
143
155
|
localRegistry: string;
|
|
144
156
|
localPypi: string;
|
|
145
|
-
pythonRoot
|
|
157
|
+
pythonRoot?: string;
|
|
146
158
|
localCargo: boolean;
|
|
147
159
|
approve: boolean;
|
|
148
160
|
}) => {
|
|
@@ -268,7 +280,12 @@ program
|
|
|
268
280
|
"",
|
|
269
281
|
"Merging this PR updates VERSION on main and starts the public release workflow.",
|
|
270
282
|
].join("\n");
|
|
271
|
-
const githubEnvironment = {
|
|
283
|
+
const githubEnvironment = {
|
|
284
|
+
...process.env,
|
|
285
|
+
GH_HOST: account.hostname,
|
|
286
|
+
GH_REPO: githubRepositorySpecifier(account),
|
|
287
|
+
[githubTokenEnvironmentName(account.hostname)]: account.token,
|
|
288
|
+
};
|
|
272
289
|
const ensurePullRequest = (): void => {
|
|
273
290
|
if (commandSucceeds(root, "gh", ["pr", "view", releaseBranch], githubEnvironment)) return;
|
|
274
291
|
run(
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface TomlCodec {
|
|
2
|
+
parse(source: string): unknown;
|
|
3
|
+
stringify(document: unknown): string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface PythonPackageMapping {
|
|
7
|
+
readonly directory: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PythonProjectInfo {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly private: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Read the project identity and publication flag from parsed TOML metadata. */
|
|
17
|
+
export function pythonProjectInfo(source: string, toml: TomlCodec): PythonProjectInfo;
|
|
18
|
+
|
|
19
|
+
/** Stamp a temporary publication version and exact sibling dependencies. */
|
|
20
|
+
export function stampPythonProject(
|
|
21
|
+
source: string,
|
|
22
|
+
options: {
|
|
23
|
+
readonly packages: readonly PythonPackageMapping[];
|
|
24
|
+
readonly rewriteDependencies?: boolean;
|
|
25
|
+
readonly toml: TomlCodec;
|
|
26
|
+
readonly version: string;
|
|
27
|
+
},
|
|
28
|
+
): string;
|
|
29
|
+
|
|
30
|
+
/** Return the target-specific UniFFI generator executable name. */
|
|
31
|
+
export function pythonBindingGeneratorName(crate: string, platform: string): string;
|
|
32
|
+
|
|
33
|
+
/** Build the canonical UniFFI Python generator argument list. */
|
|
34
|
+
export function pythonBindingGeneratorArgs(options: {
|
|
35
|
+
readonly crate: string;
|
|
36
|
+
readonly library: string;
|
|
37
|
+
readonly output: string;
|
|
38
|
+
}): string[];
|
|
39
|
+
|
|
40
|
+
export interface InstalledPythonBindings {
|
|
41
|
+
readonly bindings: string;
|
|
42
|
+
readonly init: string;
|
|
43
|
+
readonly library: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Generate and place one Python binding module and its native library. */
|
|
47
|
+
export function installPythonBindings(options: {
|
|
48
|
+
readonly crate: string;
|
|
49
|
+
readonly library: string;
|
|
50
|
+
readonly module: string;
|
|
51
|
+
readonly packageRoot: string;
|
|
52
|
+
readonly platform: string;
|
|
53
|
+
readonly readonly?: boolean;
|
|
54
|
+
readonly run: (command: string, args: string[]) => void;
|
|
55
|
+
readonly targetDirectory: string;
|
|
56
|
+
}): InstalledPythonBindings;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { basename, join, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
const READONLY = 0o444;
|
|
15
|
+
const WRITABLE = 0o644;
|
|
16
|
+
|
|
17
|
+
const projectTable = (document) => {
|
|
18
|
+
const project = document?.project;
|
|
19
|
+
if (!project || typeof project !== "object" || Array.isArray(project)) {
|
|
20
|
+
throw new Error("Missing Python project metadata");
|
|
21
|
+
}
|
|
22
|
+
return project;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const directSibling = (dependency, sibling) => {
|
|
26
|
+
const prefix = `${sibling.name} @ git+`;
|
|
27
|
+
if (!dependency.startsWith(prefix)) return false;
|
|
28
|
+
const marker = "#subdirectory=";
|
|
29
|
+
const markerIndex = dependency.indexOf(marker, prefix.length);
|
|
30
|
+
if (markerIndex < 0) return false;
|
|
31
|
+
const directory = dependency.slice(markerIndex + marker.length);
|
|
32
|
+
return directory === sibling.directory || directory.endsWith(`/${sibling.directory}`);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const setWritable = (path) => {
|
|
36
|
+
if (existsSync(path)) chmodSync(path, WRITABLE);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const setReadonly = (path) => {
|
|
40
|
+
if (existsSync(path)) chmodSync(path, READONLY);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const generatedHeader = (crate) =>
|
|
44
|
+
[
|
|
45
|
+
"# GENERATED by UniFFI binding generation - DO NOT EDIT.",
|
|
46
|
+
`# Regenerated from the ${crate} Rust exports.`,
|
|
47
|
+
"# Hand edits are overwritten on the next watch; this file is read-only.",
|
|
48
|
+
"",
|
|
49
|
+
].join("\n");
|
|
50
|
+
|
|
51
|
+
export const pythonProjectInfo = (source, toml) => {
|
|
52
|
+
const document = toml.parse(source);
|
|
53
|
+
const project = projectTable(document);
|
|
54
|
+
if (typeof project.name !== "string" || !project.name) {
|
|
55
|
+
throw new Error("Missing Python project name");
|
|
56
|
+
}
|
|
57
|
+
const config = document?.tool?.["dbx-tools"];
|
|
58
|
+
return {
|
|
59
|
+
name: project.name,
|
|
60
|
+
private: Boolean(config && typeof config === "object" && config.private === true),
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const stampPythonProject = (
|
|
65
|
+
source,
|
|
66
|
+
{ packages, rewriteDependencies = true, toml, version },
|
|
67
|
+
) => {
|
|
68
|
+
const marker = source.startsWith("# ") ? source.slice(0, source.indexOf("\n")) : undefined;
|
|
69
|
+
const document = toml.parse(source);
|
|
70
|
+
const project = projectTable(document);
|
|
71
|
+
if (typeof project.version !== "string") {
|
|
72
|
+
throw new Error("Missing Python project version");
|
|
73
|
+
}
|
|
74
|
+
project.version = version;
|
|
75
|
+
if (rewriteDependencies) {
|
|
76
|
+
const dependencies = project.dependencies;
|
|
77
|
+
if (dependencies !== undefined && !Array.isArray(dependencies)) {
|
|
78
|
+
throw new Error("Python project dependencies must be an array");
|
|
79
|
+
}
|
|
80
|
+
if (dependencies) {
|
|
81
|
+
project.dependencies = dependencies.map((dependency) => {
|
|
82
|
+
if (typeof dependency !== "string") {
|
|
83
|
+
throw new Error("Python project dependencies must be strings");
|
|
84
|
+
}
|
|
85
|
+
const sibling = packages.find((candidate) => directSibling(dependency, candidate));
|
|
86
|
+
return sibling ? `${sibling.name}==${version}` : dependency;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const body = toml.stringify(document).trimEnd();
|
|
91
|
+
return `${marker ? `${marker}\n\n` : ""}${body}\n`;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const pythonBindingGeneratorName = (crate, platform) =>
|
|
95
|
+
`${crate}-uniffi-bindgen${platform === "win32" ? ".exe" : ""}`;
|
|
96
|
+
|
|
97
|
+
export const pythonBindingGeneratorArgs = ({ crate, library, output }) => [
|
|
98
|
+
"generate",
|
|
99
|
+
"--language",
|
|
100
|
+
"python",
|
|
101
|
+
"--crate",
|
|
102
|
+
crate.replaceAll("-", "_"),
|
|
103
|
+
"--out-dir",
|
|
104
|
+
output,
|
|
105
|
+
library,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export const installPythonBindings = ({
|
|
109
|
+
crate,
|
|
110
|
+
library,
|
|
111
|
+
module,
|
|
112
|
+
packageRoot,
|
|
113
|
+
platform,
|
|
114
|
+
readonly = false,
|
|
115
|
+
run,
|
|
116
|
+
targetDirectory,
|
|
117
|
+
}) => {
|
|
118
|
+
const packageName = module.split(".").at(-1);
|
|
119
|
+
if (!packageName) throw new Error(`Invalid Python module ${module}`);
|
|
120
|
+
const packageDirectory = resolve(packageRoot, "src", ...module.split("."));
|
|
121
|
+
const generatedDirectory = mkdtempSync(join(tmpdir(), `${packageName}-python-`));
|
|
122
|
+
const generator = resolve(targetDirectory, pythonBindingGeneratorName(crate, platform));
|
|
123
|
+
if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
run(
|
|
127
|
+
generator,
|
|
128
|
+
pythonBindingGeneratorArgs({
|
|
129
|
+
crate,
|
|
130
|
+
library,
|
|
131
|
+
output: generatedDirectory,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
const generated = join(generatedDirectory, `${crate.replaceAll("-", "_")}.py`);
|
|
135
|
+
if (!existsSync(generated)) throw new Error(`Missing generated binding ${generated}`);
|
|
136
|
+
mkdirSync(packageDirectory, { recursive: true });
|
|
137
|
+
|
|
138
|
+
const bindings = join(packageDirectory, "bindings.py");
|
|
139
|
+
setWritable(bindings);
|
|
140
|
+
writeFileSync(bindings, `${generatedHeader(crate)}${readFileSync(generated, "utf8")}`);
|
|
141
|
+
|
|
142
|
+
const init = join(packageDirectory, "__init__.py");
|
|
143
|
+
setWritable(init);
|
|
144
|
+
writeFileSync(init, "");
|
|
145
|
+
|
|
146
|
+
const nativeLibrary = join(packageDirectory, basename(library));
|
|
147
|
+
setWritable(nativeLibrary);
|
|
148
|
+
cpSync(library, nativeLibrary);
|
|
149
|
+
|
|
150
|
+
if (readonly) {
|
|
151
|
+
setReadonly(bindings);
|
|
152
|
+
setReadonly(init);
|
|
153
|
+
setReadonly(nativeLibrary);
|
|
154
|
+
}
|
|
155
|
+
rmSync(join(packageDirectory, "_generated"), { recursive: true, force: true });
|
|
156
|
+
return { bindings, init, library: nativeLibrary };
|
|
157
|
+
} finally {
|
|
158
|
+
rmSync(generatedDirectory, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
};
|
package/tasks/uniffi-release.mjs
CHANGED
|
@@ -14,8 +14,14 @@ import {
|
|
|
14
14
|
import { basename, dirname, join, resolve } from "node:path";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { createRequire } from "node:module";
|
|
17
18
|
import { parseArgs } from "node:util";
|
|
18
19
|
import { spawnSync } from "node:child_process";
|
|
20
|
+
import { installPythonBindings, stampPythonProject } from "./uniffi-python.js";
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const bundledToml = fileURLToPath(new URL("./smol-toml.cjs", import.meta.url));
|
|
24
|
+
const toml = require(existsSync(bundledToml) ? bundledToml : "smol-toml");
|
|
19
25
|
|
|
20
26
|
const parsed = parseArgs({
|
|
21
27
|
allowPositionals: true,
|
|
@@ -215,9 +221,6 @@ const testNodeFacade = ({
|
|
|
215
221
|
}
|
|
216
222
|
};
|
|
217
223
|
|
|
218
|
-
const replaceVersion = (source, version) =>
|
|
219
|
-
source.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
|
|
220
|
-
|
|
221
224
|
/** Resolve workspace and generated catalog protocols for a publishable facade. */
|
|
222
225
|
const facadeDependency = (name, dependency, version, catalog) => {
|
|
223
226
|
if (typeof dependency !== "string") return dependency;
|
|
@@ -411,57 +414,30 @@ const packagePython = ({
|
|
|
411
414
|
cpSync(resolve(root, pythonDirectory), pythonRoot, { recursive: true });
|
|
412
415
|
const pyproject = join(pythonRoot, "pyproject.toml");
|
|
413
416
|
writable(pyproject);
|
|
414
|
-
writeFileSync(pyproject, replaceVersion(readFileSync(pyproject, "utf8"), version));
|
|
415
417
|
const workspaceBindings =
|
|
416
418
|
JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dbxToolsConfig?.rust?.bindings ??
|
|
417
419
|
[];
|
|
418
|
-
let metadata = readFileSync(pyproject, "utf8");
|
|
419
|
-
for (const binding of workspaceBindings) {
|
|
420
|
-
if (!binding.pythonPackage) continue;
|
|
421
|
-
const escaped = binding.pythonPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
422
|
-
metadata = metadata.replace(
|
|
423
|
-
new RegExp(`"${escaped} @ git\\+[^"\\n]+"`, "g"),
|
|
424
|
-
JSON.stringify(`${binding.pythonPackage}==${version}`),
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
writeFileSync(pyproject, metadata);
|
|
428
|
-
|
|
429
|
-
const packageName = pythonModule.split(".").at(-1);
|
|
430
|
-
if (!packageName) throw new Error(`Invalid Python module ${pythonModule}`);
|
|
431
|
-
const packageDirectory = resolve(pythonRoot, "src", ...pythonModule.split("."));
|
|
432
|
-
const generatedDirectory = mkdtempSync(join(tmpdir(), `${packageName}-python-`));
|
|
433
|
-
const generator = resolve(
|
|
434
|
-
cargoTargetRoot,
|
|
435
|
-
cargoTarget,
|
|
436
|
-
"release",
|
|
437
|
-
`${crate}-uniffi-bindgen${os === "win32" ? ".exe" : ""}`,
|
|
438
|
-
);
|
|
439
|
-
if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
|
|
440
|
-
run(generator, [
|
|
441
|
-
"generate",
|
|
442
|
-
"--language",
|
|
443
|
-
"python",
|
|
444
|
-
"--crate",
|
|
445
|
-
crate.replaceAll("-", "_"),
|
|
446
|
-
"--out-dir",
|
|
447
|
-
generatedDirectory,
|
|
448
|
-
library,
|
|
449
|
-
]);
|
|
450
|
-
const bindings = join(packageDirectory, "bindings.py");
|
|
451
|
-
writable(bindings);
|
|
452
|
-
const body = readFileSync(join(generatedDirectory, `${crate.replaceAll("-", "_")}.py`), "utf8");
|
|
453
420
|
writeFileSync(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
421
|
+
pyproject,
|
|
422
|
+
stampPythonProject(readFileSync(pyproject, "utf8"), {
|
|
423
|
+
packages: workspaceBindings.flatMap((binding) =>
|
|
424
|
+
binding.pythonPackage && binding.python
|
|
425
|
+
? [{ name: binding.pythonPackage, directory: binding.python }]
|
|
426
|
+
: [],
|
|
427
|
+
),
|
|
428
|
+
toml,
|
|
429
|
+
version,
|
|
430
|
+
}),
|
|
462
431
|
);
|
|
463
|
-
|
|
464
|
-
|
|
432
|
+
installPythonBindings({
|
|
433
|
+
crate,
|
|
434
|
+
library,
|
|
435
|
+
module: pythonModule,
|
|
436
|
+
packageRoot: pythonRoot,
|
|
437
|
+
platform: os,
|
|
438
|
+
run,
|
|
439
|
+
targetDirectory: resolve(cargoTargetRoot, cargoTarget, "release"),
|
|
440
|
+
});
|
|
465
441
|
|
|
466
442
|
const wheelDirectory = resolve(output, "python");
|
|
467
443
|
mkdirSync(wheelDirectory, { recursive: true });
|