@dbx-tools/projen 0.9.19 → 0.9.28
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 +83 -229
- package/index.ts +11 -18
- package/package.json +9 -5
- package/src/_task-command.ts +75 -0
- package/src/barrels.ts +1 -74
- package/src/module-exports.ts +175 -1
- package/src/openapi.ts +5 -72
- package/src/packages.ts +45 -0
- package/src/project-js.ts +5 -48
- package/src/project-predicate.ts +1 -2
- package/src/project-py.ts +87 -199
- package/src/project.ts +2 -2
- package/src/python-node-bindings.ts +138 -0
- package/src/python-node-bundle.ts +92 -0
- package/src/release-catalog.ts +40 -208
- package/src/{release-dispatch.ts → release-context.ts} +0 -11
- package/src/release.ts +74 -778
- package/src/scaffold.ts +2 -6
- package/src/workspace-version.ts +9 -121
- package/tasks/bump.ts +17 -34
- package/tasks/databricks-cli-assets.ts +129 -0
- package/tasks/local-publish.ts +61 -138
- package/tasks/publish-npm.ts +71 -30
- package/tasks/publish-python.ts +61 -148
- package/tasks/publish.ts +32 -182
- package/tasks/python-node-bindings-watch.ts +47 -0
- package/tasks/python-node-bindings.ts +1017 -0
- package/tasks/python-registry.ts +2 -9
- package/tasks/python-release.ts +74 -0
- package/tasks/release-version.ts +23 -2
- package/tasks/release.ts +99 -0
- package/tasks/sync.ts +13 -18
- package/tasks/version-check.ts +13 -72
- package/src/_rust-project.ts +0 -429
- package/src/_rust-release-workflow.ts +0 -698
- package/src/project-rs.ts +0 -1127
- package/src/release-plan.ts +0 -269
- package/src/release-please.ts +0 -242
- package/src/uniffi.ts +0 -65
- package/tasks/publish-uniffi-local.ts +0 -314
- package/tasks/release-assets.ts +0 -476
- package/tasks/release-bootstrap.ts +0 -59
- package/tasks/release-candidate.ts +0 -383
- package/tasks/release-manifest.ts +0 -273
- package/tasks/release-plan.ts +0 -89
- package/tasks/release-please.ts +0 -162
- package/tasks/release-pr.ts +0 -563
- package/tasks/release-request.ts +0 -166
- package/tasks/release-summary-units.ts +0 -75
- package/tasks/release-summary.ts +0 -381
- package/tasks/rust.ts +0 -178
- package/tasks/stamp-python.ts +0 -13
- package/tasks/uniffi-python.d.ts +0 -61
- package/tasks/uniffi-python.js +0 -191
- package/tasks/uniffi-release.mjs +0 -573
- package/tasks/uniffi.ts +0 -271
package/src/project-py.ts
CHANGED
|
@@ -6,20 +6,16 @@ import { JobPermission } from "projen/lib/github/workflows-model";
|
|
|
6
6
|
import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
|
|
7
7
|
import { DBX_TOOLS_LICENSE, projectRepositoryUrl } from "./project-js.ts";
|
|
8
8
|
import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
|
|
9
|
+
import { hasWorkspaceNodePackage } from "./python-node-bindings.ts";
|
|
10
|
+
import { PythonNodeBundle, type PythonNodeBindingsOptions } from "./python-node-bundle.ts";
|
|
9
11
|
import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
|
|
12
|
+
import { RELEASE_VERSION, releaseSourceSteps } from "./release-context.ts";
|
|
10
13
|
import {
|
|
11
|
-
|
|
12
|
-
RELEASE_TAG,
|
|
13
|
-
RELEASE_VERSION,
|
|
14
|
-
releaseSourceSteps,
|
|
15
|
-
} from "./release-dispatch.ts";
|
|
16
|
-
import {
|
|
17
|
-
independentReleaseSetupSteps,
|
|
18
|
-
registerIndependentPublicationJob,
|
|
14
|
+
refreshReleaseDocsDependencies,
|
|
19
15
|
releaseTagPattern,
|
|
20
16
|
releasePublishCondition,
|
|
21
|
-
releaseStageCondition,
|
|
22
17
|
tryReleaseWorkflow,
|
|
18
|
+
uvSetupStep,
|
|
23
19
|
} from "./release.ts";
|
|
24
20
|
import { readWorkspaceVersion } from "./workspace-version.ts";
|
|
25
21
|
|
|
@@ -35,13 +31,15 @@ export interface PythonPackageOptions extends DBXToolsProjectOptions {
|
|
|
35
31
|
readonly directory: string;
|
|
36
32
|
readonly name?: string;
|
|
37
33
|
readonly module?: string;
|
|
34
|
+
/** Python import root. Defaults to `generated-src` for generated packages, otherwise `src`. */
|
|
35
|
+
readonly moduleRoot?: string;
|
|
38
36
|
readonly description: string;
|
|
39
37
|
readonly dependencies?: readonly string[];
|
|
40
38
|
/** Workspace package directories rendered as standalone Git dependencies. */
|
|
41
39
|
readonly internalDependencies?: readonly string[];
|
|
42
40
|
readonly scripts?: Readonly<Record<string, string>>;
|
|
43
|
-
/**
|
|
44
|
-
readonly
|
|
41
|
+
/** Build-time Node package embedded through PythonMonkey. */
|
|
42
|
+
readonly nodeBindings?: PythonNodeBindingsOptions;
|
|
45
43
|
/** Generated source files excluded from strict static analysis. Package-relative. */
|
|
46
44
|
readonly generatedSources?: readonly string[];
|
|
47
45
|
/** Trusted publisher used outside the standard Python release workflow. */
|
|
@@ -114,8 +112,6 @@ const DEFAULT_DEV_DEPENDENCIES = [
|
|
|
114
112
|
"ruff>=0.12,<1",
|
|
115
113
|
] as const;
|
|
116
114
|
|
|
117
|
-
const quote = (value: string): string => JSON.stringify(value);
|
|
118
|
-
|
|
119
115
|
/** Repository-relative path for a Python package directory. */
|
|
120
116
|
export function pythonPackagePath(repository: PythonRepositoryOptions, directory: string): string {
|
|
121
117
|
return `${repository.root ?? "packages/py"}/${directory}`;
|
|
@@ -197,7 +193,8 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
|
|
|
197
193
|
uv: {
|
|
198
194
|
buildBackend: {
|
|
199
195
|
moduleName: pkg.module,
|
|
200
|
-
moduleRoot:
|
|
196
|
+
moduleRoot:
|
|
197
|
+
pkg.moduleRoot ?? (pkg.nodeBindings?.layout === "package" ? "generated-src" : "src"),
|
|
201
198
|
namespace: true,
|
|
202
199
|
},
|
|
203
200
|
},
|
|
@@ -214,8 +211,24 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
|
|
|
214
211
|
if (pkg.scripts) {
|
|
215
212
|
this.uv.file.addOverride("project.scripts", pkg.scripts);
|
|
216
213
|
}
|
|
217
|
-
if (pkg.
|
|
218
|
-
this.uv.file.addOverride("tool.dbx_tools.
|
|
214
|
+
if (pkg.nodeBindings) {
|
|
215
|
+
this.uv.file.addOverride("tool.dbx_tools.node_bindings", {
|
|
216
|
+
package: pkg.nodeBindings.package,
|
|
217
|
+
...(pkg.nodeBindings.entrypoint ? { entrypoint: pkg.nodeBindings.entrypoint } : {}),
|
|
218
|
+
...(pkg.nodeBindings.layout ? { layout: pkg.nodeBindings.layout } : {}),
|
|
219
|
+
...(pkg.nodeBindings.private ? { private: true } : {}),
|
|
220
|
+
...(pkg.nodeBindings.shimRoot ? { shim_root: pkg.nodeBindings.shimRoot } : {}),
|
|
221
|
+
...(pkg.nodeBindings.functionOverrides?.length
|
|
222
|
+
? {
|
|
223
|
+
function_overrides: pkg.nodeBindings.functionOverrides.map((override) => ({
|
|
224
|
+
module: override.module,
|
|
225
|
+
export: override.export,
|
|
226
|
+
handler: override.handler,
|
|
227
|
+
...(override.handlerExport ? { handler_export: override.handlerExport } : {}),
|
|
228
|
+
})),
|
|
229
|
+
}
|
|
230
|
+
: {}),
|
|
231
|
+
});
|
|
219
232
|
}
|
|
220
233
|
this.uv.file.readonly = true;
|
|
221
234
|
|
|
@@ -279,10 +292,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
279
292
|
}));
|
|
280
293
|
const resolvedOptions = { ...options, packages };
|
|
281
294
|
this.requiresPython = options.requiresPython ?? ">=3.10";
|
|
282
|
-
this.version =
|
|
283
|
-
isDBXToolsJavaScriptProject()(project) && project.releaseCatalog.mode === "independent"
|
|
284
|
-
? "0.0.0"
|
|
285
|
-
: readWorkspaceVersion(project.outdir);
|
|
295
|
+
this.version = readWorkspaceVersion(project.outdir);
|
|
286
296
|
this.file = this.emitWorkspace(project, resolvedOptions, scope);
|
|
287
297
|
this.packages = packages.map(
|
|
288
298
|
(pkg) =>
|
|
@@ -300,6 +310,14 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
300
310
|
: this.version,
|
|
301
311
|
}),
|
|
302
312
|
);
|
|
313
|
+
for (const pkg of this.packages) {
|
|
314
|
+
if (!pkg.packageOptions.nodeBindings) continue;
|
|
315
|
+
new PythonNodeBundle(project, {
|
|
316
|
+
name: pkg.packageOptions.directory,
|
|
317
|
+
projectDirectory: pythonPackagePath(this.repository, pkg.packageOptions.directory),
|
|
318
|
+
watch: hasWorkspaceNodePackage(project, pkg.packageOptions.nodeBindings.package),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
303
321
|
if (isDBXToolsJavaScriptProject()(project)) {
|
|
304
322
|
for (const pkg of this.packages) {
|
|
305
323
|
project.releaseCatalog.registerProject(pkg, {
|
|
@@ -321,7 +339,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
321
339
|
return {
|
|
322
340
|
target: dependency.name,
|
|
323
341
|
kind: "runtime" as const,
|
|
324
|
-
requirement: pythonReleaseRequirement(version
|
|
342
|
+
requirement: pythonReleaseRequirement(version),
|
|
325
343
|
propagation: "outside-range" as const,
|
|
326
344
|
publishOrder: true,
|
|
327
345
|
internal: true,
|
|
@@ -372,7 +390,9 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
372
390
|
|
|
373
391
|
private emitWorkspace(
|
|
374
392
|
project: javascript.NodeProject,
|
|
375
|
-
options: DBXToolsPythonWorkspaceOptions,
|
|
393
|
+
options: Omit<DBXToolsPythonWorkspaceOptions, "packages"> & {
|
|
394
|
+
readonly packages: readonly ResolvedPythonPackageOptions[];
|
|
395
|
+
},
|
|
376
396
|
scope: string,
|
|
377
397
|
): python.PyprojectTomlFile {
|
|
378
398
|
const testPaths = options.testPaths ?? [this.repository.root];
|
|
@@ -416,7 +436,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
416
436
|
const projectExcludes = [
|
|
417
437
|
...(options.pyreflyProjectExcludes ?? []),
|
|
418
438
|
...options.packages.flatMap((pkg) =>
|
|
419
|
-
(pkg.generatedSources ?? []).map(
|
|
439
|
+
[...(pkg.generatedSources ?? []), ...pythonNodeGeneratedSources(pkg)].map(
|
|
420
440
|
(source) => `${this.repository.root}/${pkg.directory}/${source}`,
|
|
421
441
|
),
|
|
422
442
|
),
|
|
@@ -459,154 +479,12 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
459
479
|
private addReleaseWorkflow(project: javascript.NodeProject, options: PythonReleaseOptions): void {
|
|
460
480
|
if (!project.github || !isDBXToolsJavaScriptProject()(project)) return;
|
|
461
481
|
const publications = this.publications(options);
|
|
462
|
-
const
|
|
463
|
-
const allPublications = [...publications, ...uniffiPublications];
|
|
482
|
+
const allPublications = publications;
|
|
464
483
|
if (allPublications.length === 0) return;
|
|
465
|
-
const usesRustArtifacts = uniffiPublications.length > 0;
|
|
466
484
|
const workflow = tryReleaseWorkflow(project);
|
|
467
485
|
if (!workflow) {
|
|
468
486
|
throw new Error("Python release requires the root dbx-tools release mode");
|
|
469
487
|
}
|
|
470
|
-
if (project.releaseCatalog.mode === "independent") {
|
|
471
|
-
workflow.addJob("build-python", {
|
|
472
|
-
if: usesRustArtifacts
|
|
473
|
-
? "${{ always() && needs.release-plan.outputs.python == 'true' && needs.rust-build.result != 'failure' && needs.rust-build.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}"
|
|
474
|
-
: "${{ needs.release-plan.outputs.python == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}",
|
|
475
|
-
needs: ["release-plan", ...(usesRustArtifacts ? ["rust-build"] : [])],
|
|
476
|
-
runsOn: ["ubuntu-latest"],
|
|
477
|
-
permissions: { actions: JobPermission.READ, contents: JobPermission.READ },
|
|
478
|
-
timeoutMinutes: 20,
|
|
479
|
-
env: { BUN_VERSION },
|
|
480
|
-
steps: [
|
|
481
|
-
...independentReleaseSetupSteps(project),
|
|
482
|
-
{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
|
|
483
|
-
...uniffiPublications.map((publication) => ({
|
|
484
|
-
name: `Download ${publication.distribution} native wheels`,
|
|
485
|
-
uses: "actions/download-artifact@v8",
|
|
486
|
-
with: {
|
|
487
|
-
pattern: `${publication.distribution}--*--python-wheel`,
|
|
488
|
-
path: `dist/${publication.directory}`,
|
|
489
|
-
"merge-multiple": true,
|
|
490
|
-
},
|
|
491
|
-
})),
|
|
492
|
-
{
|
|
493
|
-
name: "Build affected distributions",
|
|
494
|
-
shell: "bash",
|
|
495
|
-
run: publications
|
|
496
|
-
.map(
|
|
497
|
-
(publication) =>
|
|
498
|
-
`if jq -e --arg package ${quote(publication.distribution)} '.pythonPackages[] | select(.identity == $package)' dist/release-plan.json >/dev/null; then uv build --package ${quote(publication.distribution)} --out-dir dist/${publication.directory}; fi`,
|
|
499
|
-
)
|
|
500
|
-
.join("\n"),
|
|
501
|
-
},
|
|
502
|
-
{
|
|
503
|
-
name: "Validate affected distributions",
|
|
504
|
-
run: "find dist -type f \\( -name '*.whl' -o -name '*.tar.gz' \\) -print0 | xargs -0 uvx twine check",
|
|
505
|
-
},
|
|
506
|
-
{
|
|
507
|
-
name: "Upload distributions",
|
|
508
|
-
uses: "actions/upload-artifact@v7",
|
|
509
|
-
with: { name: "python-distributions", path: "dist", "retention-days": 7 },
|
|
510
|
-
},
|
|
511
|
-
],
|
|
512
|
-
});
|
|
513
|
-
for (const publication of allPublications) {
|
|
514
|
-
const dependencyJobs = (publication.dependencies ?? []).map(
|
|
515
|
-
(dependency) => `publish-pypi-${dependency}`,
|
|
516
|
-
);
|
|
517
|
-
const dependencyCondition = dependencyJobs
|
|
518
|
-
.map((job) => `needs.${job}.result != 'failure' && needs.${job}.result != 'cancelled'`)
|
|
519
|
-
.join(" && ");
|
|
520
|
-
workflow.addJob(`publish-pypi-${publication.directory}`, {
|
|
521
|
-
if: `\${{ always() && needs.build-python.result == 'success' && contains(needs.release-plan.outputs.python_packages, '"identity":"${publication.distribution}"') && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python')${dependencyCondition ? ` && ${dependencyCondition}` : ""} }}`,
|
|
522
|
-
needs: ["release-plan", "build-python", ...dependencyJobs],
|
|
523
|
-
environment: {
|
|
524
|
-
name: publication.environment,
|
|
525
|
-
url:
|
|
526
|
-
options.environmentUrl ??
|
|
527
|
-
`https://pypi.org/project/${publication.distribution.replaceAll("_", "-")}/`,
|
|
528
|
-
},
|
|
529
|
-
runsOn: ["ubuntu-latest"],
|
|
530
|
-
permissions: { idToken: JobPermission.WRITE },
|
|
531
|
-
timeoutMinutes: 10,
|
|
532
|
-
steps: [
|
|
533
|
-
{
|
|
534
|
-
name: "Download distributions",
|
|
535
|
-
uses: "actions/download-artifact@v8",
|
|
536
|
-
with: { name: "python-distributions", path: "dist" },
|
|
537
|
-
},
|
|
538
|
-
{
|
|
539
|
-
name: `Publish ${publication.distribution} to PyPI`,
|
|
540
|
-
uses: "pypa/gh-action-pypi-publish@release/v1",
|
|
541
|
-
with: {
|
|
542
|
-
"packages-dir": `dist/${publication.directory}`,
|
|
543
|
-
"skip-existing": true,
|
|
544
|
-
},
|
|
545
|
-
},
|
|
546
|
-
],
|
|
547
|
-
});
|
|
548
|
-
registerIndependentPublicationJob(workflow, `publish-pypi-${publication.directory}`);
|
|
549
|
-
}
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
workflow.addJob("build-python", {
|
|
553
|
-
if: releaseStageCondition("python", ["needs.verify-context.result == 'success'"]),
|
|
554
|
-
needs: ["verify-context"],
|
|
555
|
-
runsOn: ["ubuntu-latest"],
|
|
556
|
-
permissions: { contents: JobPermission.READ },
|
|
557
|
-
timeoutMinutes: 20,
|
|
558
|
-
env: { BUN_VERSION },
|
|
559
|
-
steps: [
|
|
560
|
-
...releaseSourceSteps(),
|
|
561
|
-
...bunCacheRestoreSteps(project, {
|
|
562
|
-
ignorePaths: project.workflowCacheIgnorePaths,
|
|
563
|
-
}),
|
|
564
|
-
{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
|
|
565
|
-
{ name: "Install release helpers", run: "bun install" },
|
|
566
|
-
bunCacheSaveStep(),
|
|
567
|
-
{
|
|
568
|
-
name: "Download approved Python distributions",
|
|
569
|
-
env: { GH_TOKEN: "${{ github.token }}", RELEASE_TAG },
|
|
570
|
-
run: [
|
|
571
|
-
"rm -rf dist/release-download",
|
|
572
|
-
"mkdir -p dist/release-download",
|
|
573
|
-
'gh release download "$RELEASE_TAG" --pattern release-manifest.json --pattern SHA256SUMS --pattern "*.whl" --pattern "*.tar.gz" --dir dist/release-download',
|
|
574
|
-
].join("\n"),
|
|
575
|
-
},
|
|
576
|
-
...allPublications.map((publication) => ({
|
|
577
|
-
name: `Select ${publication.distribution} distributions`,
|
|
578
|
-
env: {
|
|
579
|
-
PACKAGE_NAME: publication.distribution.replaceAll("_", "-").toLowerCase(),
|
|
580
|
-
RELEASE_SHA,
|
|
581
|
-
RELEASE_TAG,
|
|
582
|
-
RELEASE_VERSION,
|
|
583
|
-
},
|
|
584
|
-
run: [
|
|
585
|
-
"bun node_modules/@dbx-tools/projen/tasks/release-manifest.ts verify \\",
|
|
586
|
-
" --directory dist/release-download \\",
|
|
587
|
-
' --tag "$RELEASE_TAG" \\',
|
|
588
|
-
' --sha "$RELEASE_SHA" \\',
|
|
589
|
-
' --version "$RELEASE_VERSION" \\',
|
|
590
|
-
" --kind pypi \\",
|
|
591
|
-
' --package "$PACKAGE_NAME" \\',
|
|
592
|
-
` --output dist/${publication.directory}`,
|
|
593
|
-
].join("\n"),
|
|
594
|
-
})),
|
|
595
|
-
{
|
|
596
|
-
name: "Validate approved distributions",
|
|
597
|
-
run: `find ${allPublications.map((publication) => `dist/${publication.directory}`).join(" ")} -type f \\( -name '*.whl' -o -name '*.tar.gz' \\) -print0 | xargs -0 uvx twine check`,
|
|
598
|
-
},
|
|
599
|
-
{
|
|
600
|
-
name: "Upload distributions",
|
|
601
|
-
uses: "actions/upload-artifact@v7",
|
|
602
|
-
with: {
|
|
603
|
-
name: "python-distributions",
|
|
604
|
-
path: allPublications.map((publication) => `dist/${publication.directory}`).join("\n"),
|
|
605
|
-
"retention-days": 7,
|
|
606
|
-
},
|
|
607
|
-
},
|
|
608
|
-
],
|
|
609
|
-
});
|
|
610
488
|
for (const publication of allPublications) {
|
|
611
489
|
const dependencyJobs = (publication.dependencies ?? []).map(
|
|
612
490
|
(dependency) => `publish-pypi-${dependency}`,
|
|
@@ -614,10 +492,9 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
614
492
|
workflow.addJob(`publish-pypi-${publication.directory}`, {
|
|
615
493
|
if: releasePublishCondition("python", [
|
|
616
494
|
"needs.verify-context.result == 'success'",
|
|
617
|
-
"needs.build-python.result == 'success'",
|
|
618
495
|
...dependencyJobs.map((job) => `needs['${job}'].result == 'success'`),
|
|
619
496
|
]),
|
|
620
|
-
needs: ["verify-context",
|
|
497
|
+
needs: ["verify-context", ...dependencyJobs],
|
|
621
498
|
environment: {
|
|
622
499
|
name: publication.environment,
|
|
623
500
|
url:
|
|
@@ -627,11 +504,24 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
627
504
|
runsOn: ["ubuntu-latest"],
|
|
628
505
|
permissions: { idToken: JobPermission.WRITE },
|
|
629
506
|
timeoutMinutes: 10,
|
|
507
|
+
env: { BUN_VERSION },
|
|
630
508
|
steps: [
|
|
509
|
+
...releaseSourceSteps(),
|
|
510
|
+
...bunCacheRestoreSteps(project, {
|
|
511
|
+
ignorePaths: project.workflowCacheIgnorePaths,
|
|
512
|
+
}),
|
|
513
|
+
uvSetupStep(),
|
|
514
|
+
{ name: "Install release helpers", run: "bun install --frozen-lockfile" },
|
|
515
|
+
bunCacheSaveStep(),
|
|
631
516
|
{
|
|
632
|
-
name:
|
|
633
|
-
|
|
634
|
-
|
|
517
|
+
name: `Build ${publication.distribution} distributions`,
|
|
518
|
+
env: { RELEASE_VERSION },
|
|
519
|
+
run: [
|
|
520
|
+
'bun node_modules/@dbx-tools/projen/tasks/publish-python.ts "$RELEASE_VERSION"',
|
|
521
|
+
`--root ${JSON.stringify(this.repository.root)}`,
|
|
522
|
+
`--package ${JSON.stringify(publication.directory)}`,
|
|
523
|
+
`--output dist/${publication.directory}`,
|
|
524
|
+
].join(" \\\n "),
|
|
635
525
|
},
|
|
636
526
|
{
|
|
637
527
|
name: `Publish ${publication.distribution} to PyPI`,
|
|
@@ -644,32 +534,17 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
644
534
|
],
|
|
645
535
|
});
|
|
646
536
|
}
|
|
537
|
+
refreshReleaseDocsDependencies(project);
|
|
647
538
|
}
|
|
648
539
|
|
|
649
540
|
private publications(options: PythonReleaseOptions): readonly PythonPublication[] {
|
|
650
|
-
return this.packages
|
|
651
|
-
|
|
652
|
-
.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}));
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
private uniffiPublications(options: PythonReleaseOptions): readonly PythonPublication[] {
|
|
661
|
-
return this.packages
|
|
662
|
-
.filter((pkg) => pkg.packageOptions.uniffi === true)
|
|
663
|
-
.map((pkg) => ({
|
|
664
|
-
directory: pkg.packageOptions.directory,
|
|
665
|
-
distribution: pkg.packageOptions.name,
|
|
666
|
-
environment:
|
|
667
|
-
pkg.packageOptions.trustedPublisher?.environment ??
|
|
668
|
-
options.environments?.[pkg.packageOptions.name] ??
|
|
669
|
-
`pypi-${pkg.packageOptions.name}`,
|
|
670
|
-
artifacts: pkg.packageOptions.trustedPublisher?.artifacts,
|
|
671
|
-
dependencies: pkg.packageOptions.internalDependencies,
|
|
672
|
-
}));
|
|
541
|
+
return this.packages.map((pkg) => ({
|
|
542
|
+
directory: pkg.packageOptions.directory,
|
|
543
|
+
distribution: pkg.packageOptions.name,
|
|
544
|
+
environment:
|
|
545
|
+
options.environments?.[pkg.packageOptions.name] ?? `pypi-${pkg.packageOptions.name}`,
|
|
546
|
+
dependencies: pkg.packageOptions.internalDependencies,
|
|
547
|
+
}));
|
|
673
548
|
}
|
|
674
549
|
|
|
675
550
|
private trustedPublisherPublications(
|
|
@@ -697,11 +572,10 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
697
572
|
): void {
|
|
698
573
|
const repository = this.githubRepository();
|
|
699
574
|
const publications = this.trustedPublisherPublications(options);
|
|
700
|
-
const releaseTag =
|
|
701
|
-
|
|
702
|
-
?
|
|
703
|
-
:
|
|
704
|
-
: "v*";
|
|
575
|
+
const releaseTag =
|
|
576
|
+
isDBXToolsJavaScriptProject()(project) && tryReleaseWorkflow(project)
|
|
577
|
+
? releaseTagPattern(project)
|
|
578
|
+
: "v*";
|
|
705
579
|
const linesBeforeAuthentication = [
|
|
706
580
|
"# PyPI Trusted Publisher Setup Instructions",
|
|
707
581
|
"",
|
|
@@ -814,3 +688,17 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
814
688
|
return { owner: match[1], name: match[2] };
|
|
815
689
|
}
|
|
816
690
|
}
|
|
691
|
+
|
|
692
|
+
function pythonNodeGeneratedSources(pkg: ResolvedPythonPackageOptions): string[] {
|
|
693
|
+
if (!pkg.nodeBindings) return [];
|
|
694
|
+
const moduleRoot =
|
|
695
|
+
pkg.moduleRoot ?? (pkg.nodeBindings.layout === "package" ? "generated-src" : "src");
|
|
696
|
+
const moduleDirectory = `${moduleRoot}/${pkg.module.replaceAll(".", "/")}`;
|
|
697
|
+
const generatedDirectory =
|
|
698
|
+
pkg.nodeBindings.layout === "package" ? moduleDirectory : `${moduleDirectory}/_generated`;
|
|
699
|
+
return [
|
|
700
|
+
`${generatedDirectory}/_runtime.js`,
|
|
701
|
+
`${generatedDirectory}/node_bindings.py`,
|
|
702
|
+
`${generatedDirectory}/__init__.py`,
|
|
703
|
+
];
|
|
704
|
+
}
|
package/src/project.ts
CHANGED
|
@@ -12,10 +12,10 @@ import * as projectPredicate from "./project-predicate.ts";
|
|
|
12
12
|
|
|
13
13
|
export * from "./project-js.ts";
|
|
14
14
|
export * from "./project-py.ts";
|
|
15
|
-
export * from "./
|
|
15
|
+
export * from "./python-node-bundle.ts";
|
|
16
16
|
|
|
17
17
|
/** Runtime family implemented by a dbx-tools project. */
|
|
18
|
-
export type DBXToolsProjectLanguage = "javascript" | "python"
|
|
18
|
+
export type DBXToolsProjectLanguage = "javascript" | "python";
|
|
19
19
|
|
|
20
20
|
/** Options shared by every dbx-tools project implementation. */
|
|
21
21
|
export interface DBXToolsProjectOptions extends Partial<
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** Shared pyproject-backed configuration for PythonMonkey Node bindings. */
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { object } from "@dbx-tools/shared-core";
|
|
5
|
+
import type { Project } from "projen";
|
|
6
|
+
import { parse } from "smol-toml";
|
|
7
|
+
import { workspaceDependencyDirectories } from "./packages.ts";
|
|
8
|
+
|
|
9
|
+
export interface ResolvedPythonNodeFunctionOverride {
|
|
10
|
+
readonly handlerExport: string;
|
|
11
|
+
readonly handlerFile: string;
|
|
12
|
+
readonly targetExport: string;
|
|
13
|
+
readonly targetModule: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ResolvedPythonNodeBindings {
|
|
17
|
+
readonly bindingsPackageOutput: string;
|
|
18
|
+
readonly bindingsOutput: string;
|
|
19
|
+
readonly functionOverrides: readonly ResolvedPythonNodeFunctionOverride[];
|
|
20
|
+
readonly entrypoint: string;
|
|
21
|
+
readonly moduleDirectory: string;
|
|
22
|
+
readonly layout: "package" | "submodule";
|
|
23
|
+
readonly private: boolean;
|
|
24
|
+
readonly package: string;
|
|
25
|
+
readonly project: string;
|
|
26
|
+
readonly projectDirectory: string;
|
|
27
|
+
readonly pyproject: string;
|
|
28
|
+
readonly runtimeOutput: string;
|
|
29
|
+
readonly shimRoot?: string;
|
|
30
|
+
readonly workspaceDirectories: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolve one Python package's Node binding configuration and conventional outputs. */
|
|
34
|
+
export function resolvePythonNodeBindings(
|
|
35
|
+
root: string,
|
|
36
|
+
project: string,
|
|
37
|
+
): ResolvedPythonNodeBindings {
|
|
38
|
+
const projectDirectory = resolve(root, project);
|
|
39
|
+
const pyproject = resolve(projectDirectory, "pyproject.toml");
|
|
40
|
+
const manifest = record(parse(readFileSync(pyproject, "utf8")), "pyproject.toml");
|
|
41
|
+
const tool = record(manifest.tool, "tool");
|
|
42
|
+
const dbxTools = record(tool.dbx_tools, "tool.dbx_tools");
|
|
43
|
+
const bindings = record(dbxTools.node_bindings, "tool.dbx_tools.node_bindings");
|
|
44
|
+
const uv = record(tool.uv, "tool.uv");
|
|
45
|
+
const backend = record(uv["build-backend"], "tool.uv.build-backend");
|
|
46
|
+
const packageName = requiredString(bindings.package, "tool.dbx_tools.node_bindings.package");
|
|
47
|
+
const entrypoint =
|
|
48
|
+
optionalString(bindings.entrypoint, "tool.dbx_tools.node_bindings.entrypoint") ?? packageName;
|
|
49
|
+
const layout = nodeBindingsLayout(bindings.layout);
|
|
50
|
+
const moduleName = requiredString(backend["module-name"], "tool.uv.build-backend.module-name");
|
|
51
|
+
const moduleRoot = requiredString(backend["module-root"], "tool.uv.build-backend.module-root");
|
|
52
|
+
const configuredOverrides = bindings.function_overrides ?? [];
|
|
53
|
+
if (!Array.isArray(configuredOverrides)) {
|
|
54
|
+
throw new Error("tool.dbx_tools.node_bindings.function_overrides must be an array");
|
|
55
|
+
}
|
|
56
|
+
const moduleDirectory = resolve(projectDirectory, moduleRoot, ...moduleName.split("."));
|
|
57
|
+
const generatedDirectory =
|
|
58
|
+
layout === "package" ? moduleDirectory : join(moduleDirectory, "_generated");
|
|
59
|
+
const shimRoot = optionalString(bindings.shim_root, "tool.dbx_tools.node_bindings.shim_root");
|
|
60
|
+
return {
|
|
61
|
+
project,
|
|
62
|
+
projectDirectory,
|
|
63
|
+
pyproject,
|
|
64
|
+
package: packageName,
|
|
65
|
+
entrypoint,
|
|
66
|
+
layout,
|
|
67
|
+
private: optionalBoolean(bindings.private, "tool.dbx_tools.node_bindings.private") ?? false,
|
|
68
|
+
moduleDirectory,
|
|
69
|
+
runtimeOutput: join(generatedDirectory, "_runtime.js"),
|
|
70
|
+
bindingsOutput: join(generatedDirectory, "node_bindings.py"),
|
|
71
|
+
bindingsPackageOutput: join(generatedDirectory, "__init__.py"),
|
|
72
|
+
...(shimRoot ? { shimRoot: resolve(root, shimRoot) } : {}),
|
|
73
|
+
functionOverrides: configuredOverrides.map((candidate, index) => {
|
|
74
|
+
const path = `tool.dbx_tools.node_bindings.function_overrides[${index}]`;
|
|
75
|
+
const override = record(candidate, path);
|
|
76
|
+
const targetExport = requiredString(override.export, `${path}.export`);
|
|
77
|
+
return {
|
|
78
|
+
targetModule: requiredString(override.module, `${path}.module`),
|
|
79
|
+
targetExport,
|
|
80
|
+
handlerFile: resolve(root, requiredString(override.handler, `${path}.handler`)),
|
|
81
|
+
handlerExport:
|
|
82
|
+
optionalString(override.handler_export, `${path}.handler_export`) ?? targetExport,
|
|
83
|
+
};
|
|
84
|
+
}),
|
|
85
|
+
workspaceDirectories: workspaceDependencyDirectories(packageName, root),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function nodeBindingsLayout(value: unknown): "package" | "submodule" {
|
|
90
|
+
if (value === undefined) return "submodule";
|
|
91
|
+
if (value === "package" || value === "submodule") return value;
|
|
92
|
+
throw new Error("tool.dbx_tools.node_bindings.layout must be package or submodule");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** All paths that can change a workspace-backed generated Node runtime. */
|
|
96
|
+
export function pythonNodeBindingWatchInputs(config: ResolvedPythonNodeBindings): string[] {
|
|
97
|
+
if (config.workspaceDirectories.length === 0) {
|
|
98
|
+
throw new Error(`${config.package} is not a workspace package and cannot be watched`);
|
|
99
|
+
}
|
|
100
|
+
return [
|
|
101
|
+
config.pyproject,
|
|
102
|
+
...config.workspaceDirectories,
|
|
103
|
+
...(config.shimRoot ? [config.shimRoot] : []),
|
|
104
|
+
...config.functionOverrides.map(({ handlerFile }) => handlerFile),
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Whether a Projen tree already contains the configured Node package. */
|
|
109
|
+
export function hasWorkspaceNodePackage(project: Project, packageName: string): boolean {
|
|
110
|
+
return (
|
|
111
|
+
project.name === packageName || project.subprojects.some((child) => child.name === packageName)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function record(value: unknown, path: string): Record<string, unknown> {
|
|
116
|
+
if (!object.isRecord(value)) throw new Error(`${path} must be a table`);
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function requiredString(value: unknown, path: string): string {
|
|
121
|
+
const parsed = optionalString(value, path);
|
|
122
|
+
if (!parsed) throw new Error(`${path} must be a non-empty string`);
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function optionalString(value: unknown, path: string): string | undefined {
|
|
127
|
+
if (value === undefined) return undefined;
|
|
128
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
129
|
+
throw new Error(`${path} must be a non-empty string`);
|
|
130
|
+
}
|
|
131
|
+
return value.trim();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function optionalBoolean(value: unknown, path: string): boolean | undefined {
|
|
135
|
+
if (value === undefined) return undefined;
|
|
136
|
+
if (typeof value !== "boolean") throw new Error(`${path} must be a boolean`);
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Component, type Project, type Task } from "projen";
|
|
2
|
+
import { taskScript } from "./project-js.ts";
|
|
3
|
+
import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
|
|
4
|
+
|
|
5
|
+
/** One function replaced only in a generated PythonMonkey bundle. */
|
|
6
|
+
export interface PythonNodeFunctionOverride {
|
|
7
|
+
/** Module specifier imported by the ordinary Node source. */
|
|
8
|
+
readonly module: string;
|
|
9
|
+
/** Named export to replace. */
|
|
10
|
+
readonly export: string;
|
|
11
|
+
/** Repository-relative TypeScript file containing the replacement. */
|
|
12
|
+
readonly handler: string;
|
|
13
|
+
/** Named export in {@link handler}. Defaults to {@link export}. */
|
|
14
|
+
readonly handlerExport?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Node package embedded into one Python distribution through PythonMonkey. */
|
|
18
|
+
export interface PythonNodeBindingsOptions {
|
|
19
|
+
/** Public Node package specifier, such as `@dbx-tools/auth`. */
|
|
20
|
+
readonly package: string;
|
|
21
|
+
/** Optional portable package subpath to bind. Defaults to {@link package}. */
|
|
22
|
+
readonly entrypoint?: string;
|
|
23
|
+
/** Generated bindings own the package or live under `_generated`. Defaults to `submodule`. */
|
|
24
|
+
readonly layout?: "package" | "submodule";
|
|
25
|
+
/** Keep generated symbols out of the generated package `__init__`. Defaults to false. */
|
|
26
|
+
readonly private?: boolean;
|
|
27
|
+
/** Repository-relative directory containing Node built-in shims. */
|
|
28
|
+
readonly shimRoot?: string;
|
|
29
|
+
/** Functions replaced only while generating the Python runtime. */
|
|
30
|
+
readonly functionOverrides?: readonly PythonNodeFunctionOverride[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Options for pyproject-driven PythonMonkey generation tasks. */
|
|
34
|
+
export interface PythonNodeBundleOptions {
|
|
35
|
+
/** Stable task-name prefix, such as `auth`. */
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/** Repository-relative Python project containing `pyproject.toml`. */
|
|
38
|
+
readonly projectDirectory: string;
|
|
39
|
+
/** Add the freshness check to the project's test task. Defaults to true. */
|
|
40
|
+
readonly test?: boolean;
|
|
41
|
+
/** Regenerate during the root workspace watch loop. Defaults to false. */
|
|
42
|
+
readonly watch?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Owns paired generation and freshness-check tasks for one Python package. */
|
|
46
|
+
export class PythonNodeBundle extends Component {
|
|
47
|
+
readonly buildTask: Task;
|
|
48
|
+
readonly checkTask: Task;
|
|
49
|
+
readonly watchTask?: Task;
|
|
50
|
+
|
|
51
|
+
constructor(project: Project, options: PythonNodeBundleOptions) {
|
|
52
|
+
super(project);
|
|
53
|
+
const command = taskScript(
|
|
54
|
+
project,
|
|
55
|
+
"python-node-bindings.ts",
|
|
56
|
+
`--project ${shellQuote(options.projectDirectory)}`,
|
|
57
|
+
);
|
|
58
|
+
this.buildTask = project.addTask(`${options.name}:python-runtime`, {
|
|
59
|
+
description: `Generate Node bindings for Python package ${options.name}`,
|
|
60
|
+
exec: command,
|
|
61
|
+
});
|
|
62
|
+
this.checkTask = project.addTask(`${options.name}:python-runtime:check`, {
|
|
63
|
+
description: `Verify generated Node bindings for Python package ${options.name}`,
|
|
64
|
+
exec: `${command} --check`,
|
|
65
|
+
});
|
|
66
|
+
if (options.test ?? true) project.testTask.spawn(this.checkTask);
|
|
67
|
+
if (options.watch) {
|
|
68
|
+
this.watchTask = project.addTask(`${options.name}:python-runtime:watch`, {
|
|
69
|
+
description: `Regenerate Node bindings for Python package ${options.name} on changes`,
|
|
70
|
+
exec: taskScript(
|
|
71
|
+
project,
|
|
72
|
+
"python-node-bindings-watch.ts",
|
|
73
|
+
`--project ${shellQuote(options.projectDirectory)}`,
|
|
74
|
+
),
|
|
75
|
+
});
|
|
76
|
+
if (isDBXToolsJavaScriptProject()(project)) {
|
|
77
|
+
const configured = project.dbxToolsConfig.pythonNodeBindings;
|
|
78
|
+
const projects = Array.isArray(configured)
|
|
79
|
+
? configured.filter((value): value is string => typeof value === "string")
|
|
80
|
+
: [];
|
|
81
|
+
project.dbxToolsConfig.pythonNodeBindings = [
|
|
82
|
+
...new Set([...projects, options.projectDirectory]),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function shellQuote(value: string): string {
|
|
90
|
+
if (/^[A-Za-z0-9_./:@=+#-]+$/.test(value)) return value;
|
|
91
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
92
|
+
}
|