@dbx-tools/projen 0.6.168 → 0.6.174
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 +35 -45
- package/index.ts +1 -0
- package/package.json +4 -5
- package/src/barrels.ts +134 -34
- package/src/project-py.ts +73 -14
- package/src/project-rs.ts +36 -290
- package/src/release.ts +109 -4
- package/src/uniffi.ts +37 -0
- package/tasks/bump.ts +12 -2
- package/tasks/publish-uniffi-local.ts +3 -4
- package/tasks/publish.ts +22 -13
- package/tasks/uniffi-release.mjs +117 -36
- package/tasks/uniffi.ts +19 -3
package/README.md
CHANGED
|
@@ -93,15 +93,20 @@ VS Code component; dbx-tools reuses it rather than constructing a second
|
|
|
93
93
|
configurable `root` (default `packages/rs`), generates its Cargo manifest, and
|
|
94
94
|
derives the crate name and repository from the parent project. A crate containing
|
|
95
95
|
`uniffi::setup_scaffolding!()`
|
|
96
|
-
automatically wires matching
|
|
96
|
+
automatically wires matching public Node and Python binding packages using the
|
|
97
97
|
same capability name. Repository-specific dependencies and features remain
|
|
98
98
|
declarative options in `.projenrc.ts`; generated bindings are built separately
|
|
99
99
|
from projen synthesis. Target-independent Node binding TypeScript is committed
|
|
100
100
|
and remains generated/read-only, while native libraries stay ignored. Node
|
|
101
101
|
facades compile to `lib/` and publish JavaScript entry points that plain Node
|
|
102
|
-
can load from `node_modules`.
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
can load from `node_modules`. A complete `bindings.ts` / `_bindings.ts` /
|
|
103
|
+
`_bindings-ffi.ts` triplet is exported directly from the generated package
|
|
104
|
+
barrel. Python keeps `bindings.py` as the generated implementation and exports
|
|
105
|
+
its public names from a fully generated `__init__.py` with no editable marker
|
|
106
|
+
blocks. Node generation fails when direct binding names conflict with another
|
|
107
|
+
package-root export; Python generation refuses to overwrite a non-generated
|
|
108
|
+
package root. Do not create a `nodeExports` binding subpath or a handwritten
|
|
109
|
+
type facade.
|
|
105
110
|
|
|
106
111
|
Rust dependencies between binding-enabled workspace crates become Node
|
|
107
112
|
`workspace:*` and Python `internalDependencies` automatically. Python generation
|
|
@@ -132,31 +137,13 @@ both the tag target and `HEAD` to equal that SHA before building. Its matrix has
|
|
|
132
137
|
one row per target. Each row installs native dependencies and restores
|
|
133
138
|
Cargo/sccache once,
|
|
134
139
|
builds the Rust workspace once, then packages every discovered output from that
|
|
135
|
-
shared build.
|
|
136
|
-
|
|
140
|
+
shared build. Rust rows package native npm archives and Python wheels; they do
|
|
141
|
+
not install Bun or UBRN and do not build Node facades.
|
|
137
142
|
A new dispatch cancels queued and in-progress release and docs workflows before
|
|
138
143
|
starting, and each workflow's concurrency group also cancels its previous run.
|
|
139
|
-
A binary-only workspace therefore installs
|
|
144
|
+
A binary-only workspace therefore installs no language package tool. `rustVersion` remains the
|
|
140
145
|
MSRV recorded in package manifests, while `releaseRustVersion` independently
|
|
141
|
-
defaults release compilation to `stable`.
|
|
142
|
-
unless `ubrnRustVersion` explicitly selects another one.
|
|
143
|
-
|
|
144
|
-
Generated build and release workflows share the Bun cache helpers from
|
|
145
|
-
`bun-workflow.ts`. One `BUN_VERSION` value drives setup and cache keys, while a
|
|
146
|
-
generated dependency-only fingerprint keeps release version bumps from
|
|
147
|
-
invalidating Bun's global package cache. `node_modules` remains uncached.
|
|
148
|
-
|
|
149
|
-
The final host UBRN executable is cached by pinned UBRN version, Rust toolchain,
|
|
150
|
-
runner OS, and runner architecture. A hit skips both the workspace `bun install`
|
|
151
|
-
and the UBRN Cargo build. Release packaging passes the cached executable directly
|
|
152
|
-
to the Node binding generator and keeps the package barrel copied from source,
|
|
153
|
-
so it does not need the installed projen dependency graph. A miss saves the
|
|
154
|
-
validated executable immediately, before workspace build and packaging can fail.
|
|
155
|
-
Bun runtime setup still runs in the single facade row because the binding
|
|
156
|
-
generator and facade bundle are TypeScript. The bundle points its type surface
|
|
157
|
-
at the committed generated source and needs no standalone TypeScript compiler
|
|
158
|
-
or workspace install on a UBRN cache hit. Non-facade rows skip the complete
|
|
159
|
-
Bun/UBRN path.
|
|
146
|
+
defaults release compilation to `stable`.
|
|
160
147
|
Stable Windows rows verify and use the hosted runner's installed Rust toolchain
|
|
161
148
|
and select `rust-lld` for the workspace build. Cargo registry caches
|
|
162
149
|
and the `SCCACHE_GHA_VERSION` namespace stay stable per target/toolchain across
|
|
@@ -165,17 +152,12 @@ are written to each build log. Python generation executes the already-built
|
|
|
165
152
|
`target/<triple>/release/<crate>-uniffi-bindgen` directly. Artifact packaging therefore
|
|
166
153
|
does no Rust compilation after the main workspace build.
|
|
167
154
|
|
|
168
|
-
Artifacts identify their crate, target, and type.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
Set the repository variable `LOCAL_REPOSITORIES=true` to enable generated
|
|
176
|
-
self-hosted publication from the prebuilt artifacts. Configure `LOCAL_NPM_REGISTRY` and
|
|
177
|
-
`LOCAL_PYPI_PUBLISH_URL` with their matching `LOCAL_*` credentials. Set
|
|
178
|
-
`LOCAL_CARGO_REGISTRY` to a named Cargo registry such as a loopback
|
|
155
|
+
Artifacts identify their crate, target, and type. `rust-release` publishes
|
|
156
|
+
non-private Cargo crates with `cargo publish --no-verify`, uploads requested
|
|
157
|
+
binary assets to the GitHub release, then dispatches the verified Rust run id
|
|
158
|
+
with the tag and SHA. It never publishes npm or PyPI packages.
|
|
159
|
+
|
|
160
|
+
Set `LOCAL_CARGO_REGISTRY` to a named Cargo registry such as a loopback
|
|
179
161
|
[Kellnr](https://kellnr.io/) instance and provide `LOCAL_CARGO_TOKEN`. Public
|
|
180
162
|
Cargo crates also publish directly to crates.io
|
|
181
163
|
with `CARGO_REGISTRY_TOKEN`. Override `releaseTargets` only when a consumer has
|
|
@@ -186,12 +168,19 @@ architecture. Omit both filters to regenerate the complete maintained matrix.
|
|
|
186
168
|
`DBX_TOOLS_RELEASE_PLATFORMS` can select the generated matrix without repeating
|
|
187
169
|
environment parsing in a consumer.
|
|
188
170
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
171
|
+
Public UniFFI facades are marked with `dbxToolsConfig.uniffi = true` in Node
|
|
172
|
+
manifests and `[tool.dbx_tools.config] uniffi = true` in Python manifests. The
|
|
173
|
+
Node release downloads and publishes native archives first, publishes normal
|
|
174
|
+
workspace packages, then builds and publishes facades from the committed
|
|
175
|
+
generated TypeScript in dependency order. The Python release downloads every
|
|
176
|
+
platform wheel and publishes them from one trusted-publisher job per
|
|
177
|
+
distribution. The docs generators include them because they are publicly installable.
|
|
178
|
+
When a conventional Node binding path already belongs to a root subproject,
|
|
179
|
+
Rust mapping reuses that project and adds binding dependencies and metadata to
|
|
180
|
+
its existing manifest.
|
|
181
|
+
After Rust's artifacts finish, it dispatches the downstream `release` event
|
|
182
|
+
with its run id and attempt. Python, Node, and docs consume that event
|
|
183
|
+
independently with the same verified tag and SHA. Without Rust,
|
|
195
184
|
`release-dispatch` sends that event immediately. Configure release GitHub
|
|
196
185
|
environments to permit the generated release branch; the trusted-publisher
|
|
197
186
|
instructions list that branch with the workflow and environment identity PyPI
|
|
@@ -274,8 +263,9 @@ never as a blanket `<package>/src/**`. A codegen package may hold hand-written
|
|
|
274
263
|
modules beside its generated ones, and those must stay linted.
|
|
275
264
|
|
|
276
265
|
`generateBarrels()` writes package-root `index.ts` barrels with module
|
|
277
|
-
namespaces
|
|
278
|
-
|
|
266
|
+
namespaces, flat unique type exports, `PACKAGE_IDENTIFIER`, and
|
|
267
|
+
`PACKAGE_VERSION`, returning the number that actually changed. A name two
|
|
268
|
+
modules both declare is ambiguous and stays namespace-only —
|
|
279
269
|
except when one of them is generated: the hand-written module is the curated view
|
|
280
270
|
of the generated shape (`shared-genie`'s `genie-model.ts` extends its own
|
|
281
271
|
codegen'd `dashboards.ts`), so it owns the name and stays hoisted. A barrel whose export surface is unchanged is left untouched,
|
package/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
|
|
6
|
+
export const PACKAGE_VERSION = "0.6.173";
|
|
6
7
|
export * as barrels from "./src/barrels.ts";
|
|
7
8
|
export * as bunApp from "./src/bun-app.ts";
|
|
8
9
|
export * as bunWorkflow from "./src/bun-workflow.ts";
|
package/package.json
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
"name": "@dbx-tools/projen",
|
|
3
3
|
"scripts": {
|
|
4
4
|
"build": "projen build",
|
|
5
|
-
"bump": "projen bump",
|
|
6
5
|
"clobber": "projen clobber",
|
|
7
6
|
"compile": "projen compile",
|
|
8
7
|
"default": "projen default",
|
|
@@ -26,9 +25,9 @@
|
|
|
26
25
|
},
|
|
27
26
|
"dependencies": {
|
|
28
27
|
"@clack/prompts": "^1.7.0",
|
|
29
|
-
"@dbx-tools/core": "0.6.
|
|
30
|
-
"@dbx-tools/path": "0.6.
|
|
31
|
-
"@dbx-tools/shared-core": "0.6.
|
|
28
|
+
"@dbx-tools/core": "0.6.174",
|
|
29
|
+
"@dbx-tools/path": "0.6.174",
|
|
30
|
+
"@dbx-tools/shared-core": "0.6.174",
|
|
32
31
|
"commander": "^15.0.0",
|
|
33
32
|
"concurrently": "^10.0.3",
|
|
34
33
|
"constructs": "^10.6.0",
|
|
@@ -48,7 +47,7 @@
|
|
|
48
47
|
},
|
|
49
48
|
"main": "index.ts",
|
|
50
49
|
"license": "Apache-2.0",
|
|
51
|
-
"version": "0.6.
|
|
50
|
+
"version": "0.6.174",
|
|
52
51
|
"types": "index.ts",
|
|
53
52
|
"type": "module",
|
|
54
53
|
"exports": {
|
package/src/barrels.ts
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* takes priority. This keeps the barrel auto-generated while letting you add or
|
|
17
17
|
* override individual exports.
|
|
18
18
|
*
|
|
19
|
+
* A UniFFI source triplet (`bindings.ts`, `_bindings.ts`, and
|
|
20
|
+
* `_bindings-ffi.ts`) is one special case: the internal underscore modules stay
|
|
21
|
+
* private, while `bindings.ts` is re-exported directly instead of under a
|
|
22
|
+
* `bindings` namespace. Generation fails when one of those direct binding names
|
|
23
|
+
* conflicts with another top-level package export.
|
|
24
|
+
*
|
|
19
25
|
* Each eligible module becomes `export * as <name> from "./src/x.ts"` (camelCase
|
|
20
26
|
* namespace from its path segments; invalid identifiers suffixed with `Module`),
|
|
21
27
|
* sorted by module path.
|
|
@@ -28,9 +34,10 @@
|
|
|
28
34
|
* `export { ... }`. The module namespaces stay either way, so a namespaced call
|
|
29
35
|
* site keeps working.
|
|
30
36
|
*
|
|
31
|
-
* Every generated barrel also exports `PACKAGE_IDENTIFIER`
|
|
32
|
-
* `package.json`. Runtime helpers can
|
|
33
|
-
* trying to recover it from an ESM namespace
|
|
37
|
+
* Every generated barrel also exports `PACKAGE_IDENTIFIER` and
|
|
38
|
+
* `PACKAGE_VERSION` from the package's own `package.json`. Runtime helpers can
|
|
39
|
+
* retain package identity without trying to recover it from an ESM namespace
|
|
40
|
+
* object or loader function.
|
|
34
41
|
*
|
|
35
42
|
* Uniqueness is tallied over types and values TOGETHER: a name carried by two
|
|
36
43
|
* modules is ambiguous whichever kind it is, and hoisting one module's value
|
|
@@ -107,10 +114,16 @@ const BARREL_HEADER: HeaderOpts = {
|
|
|
107
114
|
source: "the exporting modules in ./src",
|
|
108
115
|
};
|
|
109
116
|
|
|
110
|
-
/** Generated package
|
|
117
|
+
/** Generated package metadata exports, reserved against source-module hoisting. */
|
|
111
118
|
const PACKAGE_IDENTIFIER_EXPORT = "PACKAGE_IDENTIFIER";
|
|
119
|
+
const PACKAGE_VERSION_EXPORT = "PACKAGE_VERSION";
|
|
112
120
|
const PACKAGE_IDENTIFIER_LINE = `export const ${PACKAGE_IDENTIFIER_EXPORT} = "";`;
|
|
121
|
+
const PACKAGE_VERSION_LINE = `export const ${PACKAGE_VERSION_EXPORT} = "";`;
|
|
113
122
|
const PACKAGE_IDENTIFIER_LINE_RE = /^export const PACKAGE_IDENTIFIER = .*;$/m;
|
|
123
|
+
const PACKAGE_VERSION_LINE_RE = /^export const PACKAGE_VERSION = .*;$/m;
|
|
124
|
+
const UNIFFI_BINDINGS_FILE = "bindings.ts";
|
|
125
|
+
const UNIFFI_GENERATED_FILE = "_bindings.ts";
|
|
126
|
+
const UNIFFI_FFI_FILE = "_bindings-ffi.ts";
|
|
114
127
|
|
|
115
128
|
/** `pnpm-workspace` -> `pnpmWorkspace`; `local-fs` -> `localFS` (`fs` -> `FS`). */
|
|
116
129
|
function kebabToCamel(segment: string): string {
|
|
@@ -266,8 +279,9 @@ function customExportNames(file: string): Set<string> {
|
|
|
266
279
|
* `export * as`), then the whole module is re-exported last.
|
|
267
280
|
*/
|
|
268
281
|
function mergeCustomExports(content: string, pkgDir: string): string {
|
|
269
|
-
|
|
270
|
-
|
|
282
|
+
const customPath = join(pkgDir, CUSTOM_EXPORTS_FILE);
|
|
283
|
+
if (!existsSync(customPath) || isLegacyBindingsExport(customPath)) return content;
|
|
284
|
+
const overridden = customExportNames(customPath);
|
|
271
285
|
const kept = content.split("\n").filter((line) => {
|
|
272
286
|
const ns = /^export \* as (\w+) from /.exec(line)?.[1];
|
|
273
287
|
return !(ns && overridden.has(ns));
|
|
@@ -275,29 +289,101 @@ function mergeCustomExports(content: string, pkgDir: string): string {
|
|
|
275
289
|
return `${kept.join("\n").replace(/\n+$/, "")}\nexport * from "./exports.ts";\n`;
|
|
276
290
|
}
|
|
277
291
|
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
function isLegacyBindingsExport(file: string): boolean {
|
|
293
|
+
return readFileSync(file, "utf8").trim() === 'export * from "./src/bindings.ts";';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function hasUniFFIBindings(srcDir: string): boolean {
|
|
297
|
+
return [UNIFFI_BINDINGS_FILE, UNIFFI_GENERATED_FILE, UNIFFI_FFI_FILE].every((file) =>
|
|
298
|
+
existsSync(join(srcDir, file)),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function bindingExportNames(srcDir: string): Set<string> {
|
|
303
|
+
return new Set(
|
|
304
|
+
[UNIFFI_BINDINGS_FILE, UNIFFI_GENERATED_FILE].flatMap((file) =>
|
|
305
|
+
moduleExports(join(srcDir, file)).map((entry) => entry.name),
|
|
306
|
+
),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function barrelExportNames(content: string, customPath: string): Set<string> {
|
|
311
|
+
const names = new Set<string>([PACKAGE_IDENTIFIER_EXPORT, PACKAGE_VERSION_EXPORT]);
|
|
312
|
+
for (const match of content.matchAll(/^export \* as (\w+) from /gm)) {
|
|
313
|
+
names.add(match[1]!);
|
|
314
|
+
}
|
|
315
|
+
for (const match of content.matchAll(/^export(?: type)? \{([^}]+)\} from /gm)) {
|
|
316
|
+
for (const specifier of match[1]!.split(",")) {
|
|
317
|
+
const parts = specifier.trim().split(/\s+as\s+/);
|
|
318
|
+
const name = parts.at(-1);
|
|
319
|
+
if (name) names.add(name);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (existsSync(customPath) && !isLegacyBindingsExport(customPath)) {
|
|
323
|
+
const statements = moduleStatements(customPath) as ReadonlyArray<{
|
|
324
|
+
type: string;
|
|
325
|
+
exported?: unknown;
|
|
326
|
+
}>;
|
|
327
|
+
if (
|
|
328
|
+
statements.some(
|
|
329
|
+
(statement) => statement.type === "ExportAllDeclaration" && !statement.exported,
|
|
330
|
+
)
|
|
331
|
+
) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Cannot verify UniFFI export conflicts through a bare export in ${customPath}`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
for (const name of customExportNames(customPath)) names.add(name);
|
|
337
|
+
}
|
|
338
|
+
return names;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function assertNoBindingExportConflicts(srcDir: string, content: string, customPath: string): void {
|
|
342
|
+
const other = barrelExportNames(content, customPath);
|
|
343
|
+
const conflicts = [...bindingExportNames(srcDir)].filter((name) => other.has(name)).sort();
|
|
344
|
+
if (conflicts.length > 0) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`UniFFI binding exports conflict with package exports in ${srcDir}: ${conflicts.join(", ")}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Read the authoritative npm package name and version emitted by the package project. */
|
|
352
|
+
function packageMetadata(pkgDir: string): { identifier: string; version: string } {
|
|
280
353
|
const manifestPath = join(pkgDir, "package.json");
|
|
281
|
-
const
|
|
282
|
-
? json.parseRecord(readFileSync(manifestPath, "utf8"))
|
|
354
|
+
const manifest = existsSync(manifestPath)
|
|
355
|
+
? json.parseRecord(readFileSync(manifestPath, "utf8"))
|
|
283
356
|
: undefined;
|
|
357
|
+
const name = manifest?.name;
|
|
284
358
|
if (typeof name !== "string" || !name.trim()) {
|
|
285
359
|
throw new Error(`Cannot generate barrel without package.json name: ${manifestPath}`);
|
|
286
360
|
}
|
|
287
|
-
|
|
361
|
+
const version = manifest?.version;
|
|
362
|
+
if (typeof version !== "string" || !version.trim()) {
|
|
363
|
+
throw new Error(`Cannot generate barrel without package.json version: ${manifestPath}`);
|
|
364
|
+
}
|
|
365
|
+
return { identifier: name, version };
|
|
288
366
|
}
|
|
289
367
|
|
|
290
|
-
/** Normalize package
|
|
291
|
-
function
|
|
292
|
-
return content
|
|
368
|
+
/** Normalize package metadata before comparing the barrel's export structure. */
|
|
369
|
+
function withoutPackageMetadata(content: string): string {
|
|
370
|
+
return content
|
|
371
|
+
.replace(PACKAGE_IDENTIFIER_LINE_RE, PACKAGE_IDENTIFIER_LINE)
|
|
372
|
+
.replace(PACKAGE_VERSION_LINE_RE, PACKAGE_VERSION_LINE);
|
|
293
373
|
}
|
|
294
374
|
|
|
295
|
-
/** Resolve and insert package
|
|
296
|
-
function
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
375
|
+
/** Resolve and insert package metadata when a barrel is about to be written. */
|
|
376
|
+
function withPackageMetadata(content: string, pkgDir: string): string {
|
|
377
|
+
const metadata = packageMetadata(pkgDir);
|
|
378
|
+
return content
|
|
379
|
+
.replace(
|
|
380
|
+
PACKAGE_IDENTIFIER_LINE_RE,
|
|
381
|
+
() => `export const ${PACKAGE_IDENTIFIER_EXPORT} = ${JSON.stringify(metadata.identifier)};`,
|
|
382
|
+
)
|
|
383
|
+
.replace(
|
|
384
|
+
PACKAGE_VERSION_LINE_RE,
|
|
385
|
+
() => `export const ${PACKAGE_VERSION_EXPORT} = ${JSON.stringify(metadata.version)};`,
|
|
386
|
+
);
|
|
301
387
|
}
|
|
302
388
|
|
|
303
389
|
/**
|
|
@@ -353,6 +439,10 @@ function generateForPackage(pkgDir: string): number {
|
|
|
353
439
|
if (!existing || (!isSourceExt(existing) && isSourceExt(f))) byModulePath.set(stem, f);
|
|
354
440
|
}
|
|
355
441
|
const modulePaths = [...byModulePath.keys()].sort((a, b) => a.localeCompare(b));
|
|
442
|
+
const uniffiBindings = hasUniFFIBindings(srcDir);
|
|
443
|
+
const namespacedModulePaths = uniffiBindings
|
|
444
|
+
? modulePaths.filter((modulePath) => modulePath !== "bindings")
|
|
445
|
+
: modulePaths;
|
|
356
446
|
|
|
357
447
|
// No eligible modules -> no barrel: drop any stale root barrel and bail.
|
|
358
448
|
if (modulePaths.length === 0) {
|
|
@@ -368,35 +458,45 @@ function generateForPackage(pkgDir: string): number {
|
|
|
368
458
|
// extension is written because `tsc` rewrites it on emit
|
|
369
459
|
// (`rewriteRelativeImportExtensions`) - an extensionless specifier would be
|
|
370
460
|
// copied through verbatim and Node's ESM resolver cannot probe for it.
|
|
371
|
-
const namespaceExports =
|
|
461
|
+
const namespaceExports = namespacedModulePaths
|
|
372
462
|
.map((stem) => {
|
|
373
463
|
const modulePath = `./src/${byModulePath.get(stem)!}`;
|
|
374
464
|
return `export * as ${modulePathToNamespace(modulePath)} from "${modulePath}";`;
|
|
375
465
|
})
|
|
376
466
|
.join("\n");
|
|
377
|
-
let content = `${PACKAGE_IDENTIFIER_LINE}\n${namespaceExports}`;
|
|
467
|
+
let content = `${PACKAGE_IDENTIFIER_LINE}\n${PACKAGE_VERSION_LINE}\n${namespaceExports}`;
|
|
378
468
|
// Hoist package-unique named exports to the top level. Names a hand-authored
|
|
379
469
|
// `exports.ts` declares are suppressed so that file stays authoritative.
|
|
380
470
|
const customPath = join(pkgDir, CUSTOM_EXPORTS_FILE);
|
|
381
|
-
const suppress =
|
|
471
|
+
const suppress =
|
|
472
|
+
existsSync(customPath) && !isLegacyBindingsExport(customPath)
|
|
473
|
+
? customExportNames(customPath)
|
|
474
|
+
: new Set<string>();
|
|
382
475
|
suppress.add(PACKAGE_IDENTIFIER_EXPORT);
|
|
476
|
+
suppress.add(PACKAGE_VERSION_EXPORT);
|
|
383
477
|
content = hoistUniqueExports(content, pkgDir, suppress);
|
|
384
478
|
// A sibling `exports.ts` overrides/extends the generated barrel and wins on conflict.
|
|
385
479
|
content = mergeCustomExports(content, pkgDir);
|
|
480
|
+
if (uniffiBindings) {
|
|
481
|
+
assertNoBindingExportConflicts(srcDir, content, customPath);
|
|
482
|
+
content = content.replace(
|
|
483
|
+
PACKAGE_VERSION_LINE,
|
|
484
|
+
`${PACKAGE_VERSION_LINE}\nexport * from "./src/bindings.ts";`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
386
487
|
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
// identical), and it is also what keeps a no-op cycle off the read-only bit
|
|
394
|
-
// entirely: see {@link writeBarrel} for why unlocking a barrel we are not about
|
|
395
|
-
// to rewrite is what produced spurious EACCES failures.
|
|
488
|
+
// Package metadata is blanked for the structural comparison, then restored
|
|
489
|
+
// from package.json so a version-only change still rewrites the barrel.
|
|
490
|
+
// If the structural result and package metadata match what's on disk, leave
|
|
491
|
+
// the file and its read-only bit untouched and report no change (0). This
|
|
492
|
+
// keeps the watcher quiet on ordinary in-file edits while allowing a version
|
|
493
|
+
// bump to update PACKAGE_VERSION.
|
|
396
494
|
content = `${content.replace(/\n+$/, "")}\n`;
|
|
397
495
|
const template = `${header(BARREL_HEADER)}\n${content}`;
|
|
398
|
-
|
|
399
|
-
|
|
496
|
+
const next = withPackageMetadata(template, pkgDir);
|
|
497
|
+
if (before !== undefined && withoutPackageMetadata(before) === template && before === next) {
|
|
498
|
+
return 0;
|
|
499
|
+
}
|
|
400
500
|
|
|
401
501
|
// Written whole (header included) rather than via `stampGenerated`, which would
|
|
402
502
|
// re-read and rewrite the file to prepend the same header - a second write, and
|
package/src/project-py.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { GithubWorkflow } from "projen/lib/github";
|
|
|
7
7
|
import { JobPermission } from "projen/lib/github/workflows-model";
|
|
8
8
|
import { parse, stringify } from "smol-toml";
|
|
9
9
|
import { projectReleaseBranch, projectRepositoryUrl } from "./project-js.ts";
|
|
10
|
-
import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
|
|
11
10
|
import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
|
|
11
|
+
import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
|
|
12
12
|
import { DOWNSTREAM_RELEASE_EVENT, RELEASE_TAG, releaseSourceSteps } from "./release-dispatch.ts";
|
|
13
13
|
import { readWorkspaceVersion } from "./workspace-version.ts";
|
|
14
14
|
|
|
@@ -29,8 +29,8 @@ export interface PythonPackageOptions extends DBXToolsProjectOptions {
|
|
|
29
29
|
/** Workspace package directories rendered as standalone Git dependencies. */
|
|
30
30
|
readonly internalDependencies?: readonly string[];
|
|
31
31
|
readonly scripts?: Readonly<Record<string, string>>;
|
|
32
|
-
/**
|
|
33
|
-
readonly
|
|
32
|
+
/** Publish this package through the Rust UniFFI release flow instead of the Python workflow. */
|
|
33
|
+
readonly uniffi?: boolean;
|
|
34
34
|
/** Generated source files excluded from strict static analysis. Package-relative. */
|
|
35
35
|
readonly generatedSources?: readonly string[];
|
|
36
36
|
/** Trusted publisher used outside the standard Python release workflow. */
|
|
@@ -75,6 +75,7 @@ interface PythonPublication {
|
|
|
75
75
|
readonly environment: string;
|
|
76
76
|
readonly workflowName: string;
|
|
77
77
|
readonly artifacts?: string;
|
|
78
|
+
readonly dependencies?: readonly string[];
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/** Options for {@link DBXToolsPythonWorkspace}. */
|
|
@@ -214,8 +215,8 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
|
|
|
214
215
|
if (pkg.scripts) {
|
|
215
216
|
this.uv.file.addOverride("project.scripts", pkg.scripts);
|
|
216
217
|
}
|
|
217
|
-
if (pkg.
|
|
218
|
-
this.uv.file.addOverride("tool.
|
|
218
|
+
if (pkg.uniffi !== undefined) {
|
|
219
|
+
this.uv.file.addOverride("tool.dbx_tools.config.uniffi", pkg.uniffi);
|
|
219
220
|
}
|
|
220
221
|
formatPyproject(this.uv.file);
|
|
221
222
|
this.uv.file.readonly = true;
|
|
@@ -319,7 +320,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
319
320
|
projectVscode(project)?.settings.addSetting("python.defaultInterpreterPath", interpreterPath);
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
if (options.release && this.packages.
|
|
323
|
+
if (options.release && this.packages.length > 0) {
|
|
323
324
|
if (isDBXToolsJavaScriptProject()(project)) {
|
|
324
325
|
project.dbxToolsConfig.pythonReleaseWorkflow =
|
|
325
326
|
releaseOptions.workflowName ?? "python-release";
|
|
@@ -428,7 +429,10 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
428
429
|
private addReleaseWorkflow(project: javascript.NodeProject, options: PythonReleaseOptions): void {
|
|
429
430
|
if (!project.github) return;
|
|
430
431
|
const publications = this.publications(options);
|
|
431
|
-
|
|
432
|
+
const uniffiPublications = this.uniffiPublications(options);
|
|
433
|
+
const allPublications = [...publications, ...uniffiPublications];
|
|
434
|
+
if (allPublications.length === 0) return;
|
|
435
|
+
const usesRustArtifacts = uniffiPublications.length > 0;
|
|
432
436
|
const workflowName = options.workflowName ?? "python-release";
|
|
433
437
|
const workflow = new GithubWorkflow(project.github, workflowName, {
|
|
434
438
|
limitConcurrency: true,
|
|
@@ -436,7 +440,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
436
440
|
});
|
|
437
441
|
workflow.file?.addOverride("permissions", {
|
|
438
442
|
contents: "read",
|
|
439
|
-
...(options.upstreamWorkflow ? { actions: "read" } : {}),
|
|
443
|
+
...(options.upstreamWorkflow || usesRustArtifacts ? { actions: "read" } : {}),
|
|
440
444
|
});
|
|
441
445
|
const upstreamWorkflow = options.upstreamWorkflow;
|
|
442
446
|
const branchDispatch = upstreamWorkflow === undefined && isDBXToolsJavaScriptProject()(project);
|
|
@@ -487,7 +491,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
487
491
|
runsOn: ["ubuntu-latest"],
|
|
488
492
|
permissions: {
|
|
489
493
|
contents: JobPermission.READ,
|
|
490
|
-
...(upstreamWorkflow ? { actions: JobPermission.READ } : {}),
|
|
494
|
+
...(upstreamWorkflow || usesRustArtifacts ? { actions: JobPermission.READ } : {}),
|
|
491
495
|
},
|
|
492
496
|
timeoutMinutes: 20,
|
|
493
497
|
steps: [
|
|
@@ -555,6 +559,35 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
555
559
|
]
|
|
556
560
|
: []),
|
|
557
561
|
{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
|
|
562
|
+
...(usesRustArtifacts
|
|
563
|
+
? [
|
|
564
|
+
{
|
|
565
|
+
name: "Require Rust artifact handoff",
|
|
566
|
+
if: "${{ github.event_name == 'repository_dispatch' || github.event_name == 'workflow_run' }}",
|
|
567
|
+
shell: "bash",
|
|
568
|
+
env: {
|
|
569
|
+
RUST_RUN_ID:
|
|
570
|
+
"${{ github.event_name == 'repository_dispatch' && github.event.client_payload.rust_run_id || github.event.workflow_run.id }}",
|
|
571
|
+
RUST_RUN_ATTEMPT:
|
|
572
|
+
"${{ github.event_name == 'repository_dispatch' && github.event.client_payload.rust_run_attempt || github.event.workflow_run.run_attempt }}",
|
|
573
|
+
},
|
|
574
|
+
run: ['test -n "$RUST_RUN_ID"', 'test -n "$RUST_RUN_ATTEMPT"'].join("\n"),
|
|
575
|
+
},
|
|
576
|
+
]
|
|
577
|
+
: []),
|
|
578
|
+
...uniffiPublications.map((publication) => ({
|
|
579
|
+
name: `Download ${publication.distribution} native wheels`,
|
|
580
|
+
if: "${{ github.event_name == 'repository_dispatch' || github.event_name == 'workflow_run' }}",
|
|
581
|
+
uses: "actions/download-artifact@v8",
|
|
582
|
+
with: {
|
|
583
|
+
pattern: `${publication.distribution}--*--python-wheel`,
|
|
584
|
+
path: `dist/${publication.directory}`,
|
|
585
|
+
"merge-multiple": true,
|
|
586
|
+
"run-id":
|
|
587
|
+
"${{ github.event_name == 'repository_dispatch' && github.event.client_payload.rust_run_id || github.event.workflow_run.id }}",
|
|
588
|
+
"github-token": "${{ github.token }}",
|
|
589
|
+
},
|
|
590
|
+
})),
|
|
558
591
|
{
|
|
559
592
|
name: "Stamp workspace versions",
|
|
560
593
|
env: {
|
|
@@ -594,8 +627,15 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
594
627
|
{
|
|
595
628
|
name: "Validate distributions",
|
|
596
629
|
run: [
|
|
597
|
-
|
|
598
|
-
|
|
630
|
+
...publications.map(
|
|
631
|
+
(publication) =>
|
|
632
|
+
`test "$(find dist/${publication.directory} -maxdepth 1 -type f \\( -name '*.whl' -o -name '*.tar.gz' \\) | wc -l | tr -d ' ')" -eq 2`,
|
|
633
|
+
),
|
|
634
|
+
...uniffiPublications.map(
|
|
635
|
+
(publication) =>
|
|
636
|
+
`if [ "$GITHUB_EVENT_NAME" = "repository_dispatch" ] || [ "$GITHUB_EVENT_NAME" = "workflow_run" ]; then find dist/${publication.directory} -maxdepth 1 -name '*.whl' -print -quit | grep -q .; fi`,
|
|
637
|
+
),
|
|
638
|
+
"find dist -type f \\( -name '*.whl' -o -name '*.tar.gz' \\) -print0 | xargs -0 uvx twine check",
|
|
599
639
|
].join("\n"),
|
|
600
640
|
},
|
|
601
641
|
{
|
|
@@ -610,12 +650,15 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
610
650
|
},
|
|
611
651
|
],
|
|
612
652
|
});
|
|
613
|
-
for (const publication of
|
|
653
|
+
for (const publication of allPublications) {
|
|
614
654
|
workflow.addJob(`publish-${publication.directory}`, {
|
|
615
655
|
if: branchDispatch
|
|
616
656
|
? "${{ github.event_name == 'repository_dispatch' }}"
|
|
617
657
|
: "${{ github.event_name == 'push' || github.event_name == 'workflow_run' }}",
|
|
618
|
-
needs: [
|
|
658
|
+
needs: [
|
|
659
|
+
"build",
|
|
660
|
+
...(publication.dependencies ?? []).map((dependency) => `publish-${dependency}`),
|
|
661
|
+
],
|
|
619
662
|
environment: {
|
|
620
663
|
name: publication.environment,
|
|
621
664
|
url:
|
|
@@ -643,7 +686,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
643
686
|
|
|
644
687
|
private publications(options: PythonReleaseOptions): readonly PythonPublication[] {
|
|
645
688
|
return this.packages
|
|
646
|
-
.filter((pkg) =>
|
|
689
|
+
.filter((pkg) => pkg.packageOptions.uniffi !== true)
|
|
647
690
|
.map((pkg) => ({
|
|
648
691
|
directory: pkg.packageOptions.directory,
|
|
649
692
|
distribution: pkg.packageOptions.name,
|
|
@@ -653,6 +696,22 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
653
696
|
}));
|
|
654
697
|
}
|
|
655
698
|
|
|
699
|
+
private uniffiPublications(options: PythonReleaseOptions): readonly PythonPublication[] {
|
|
700
|
+
return this.packages
|
|
701
|
+
.filter((pkg) => pkg.packageOptions.uniffi === true)
|
|
702
|
+
.map((pkg) => ({
|
|
703
|
+
directory: pkg.packageOptions.directory,
|
|
704
|
+
distribution: pkg.packageOptions.name,
|
|
705
|
+
environment:
|
|
706
|
+
pkg.packageOptions.trustedPublisher?.environment ??
|
|
707
|
+
options.environments?.[pkg.packageOptions.name] ??
|
|
708
|
+
`pypi-${pkg.packageOptions.name}`,
|
|
709
|
+
workflowName: options.workflowName ?? "python-release",
|
|
710
|
+
artifacts: pkg.packageOptions.trustedPublisher?.artifacts,
|
|
711
|
+
dependencies: pkg.packageOptions.internalDependencies,
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
714
|
+
|
|
656
715
|
private trustedPublisherPublications(
|
|
657
716
|
options: PythonReleaseOptions,
|
|
658
717
|
): readonly PythonPublication[] {
|