@dbx-tools/projen 0.6.77 → 0.6.79
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 +13 -0
- package/index.ts +4 -4
- package/package.json +4 -4
- package/src/pnpm-workspace.ts +1 -0
- package/src/project-js.ts +23 -14
- package/src/project-predicate.ts +17 -6
- package/src/project-py.ts +179 -107
- package/src/project.ts +18 -5
- package/src/publish.ts +2 -2
- package/tasks/bump.ts +52 -5
- package/tasks/publish-python.ts +146 -0
- package/tasks/python-registry.ts +87 -0
package/README.md
CHANGED
|
@@ -260,6 +260,19 @@ a new package is covered without a re-synth. Work from the root:
|
|
|
260
260
|
| `bun run barrels` | regenerate the read-only `index.ts` barrels |
|
|
261
261
|
| `bun run bump` | version, tag, and publish |
|
|
262
262
|
|
|
263
|
+
`bump` also mirrors a release into local registries when the active clients are
|
|
264
|
+
pointed at loopback services. npm uses `npm config get registry` and publishes
|
|
265
|
+
to a local Verdaccio automatically. Python prefers uv's default index and only
|
|
266
|
+
treats a loopback `.../+simple/` URL as writable devpi; a read-only cache such as
|
|
267
|
+
proxpi (`.../index/`) is deliberately ignored. The task stamps every Python
|
|
268
|
+
member and its sibling dependencies to the release version, builds the workspace
|
|
269
|
+
with uv, then runs `devpi upload --from-dir` against the derived writable index.
|
|
270
|
+
Devpi client authentication remains in its normal `~/.devpi` state.
|
|
271
|
+
|
|
272
|
+
Use `--local-registry false` or `--local-pypi false` to disable either local
|
|
273
|
+
publish. An explicit `--local-pypi http://localhost:3141/user/index/` overrides
|
|
274
|
+
auto-detection; `--python-root` defaults to `packages/py`.
|
|
275
|
+
|
|
263
276
|
Members intentionally keep only the tasks that something OTHER than a human
|
|
264
277
|
invokes, so there is no second place to run the same thing:
|
|
265
278
|
|
package/index.ts
CHANGED
|
@@ -36,11 +36,11 @@ export { repoRoot, DEFAULT_PACKAGE_ROOTS, DiscoveredPackage } from "./src/packag
|
|
|
36
36
|
export type { RecordedPackage } from "./src/packages.ts";
|
|
37
37
|
export { PnpmWorkspaceState } from "./src/pnpm-workspace.ts";
|
|
38
38
|
export type { Catalog, AllowBuilds, DBXToolsPNPMWorkspaceOptions } from "./src/pnpm-workspace.ts";
|
|
39
|
-
export type { ApplyToProjectsOptions } from "./src/project.ts";
|
|
39
|
+
export type { DBXToolsProjectLanguage, DBXToolsProjectOptions, DBXToolsProject, ApplyToProjectsOptions } from "./src/project.ts";
|
|
40
40
|
export { PackageIdentifier, PROJEN_VERSION, DBXToolsNodeProject, ROOT_INSTALL_ONLY_MIXIN, DBXToolsTypeScriptProject } from "./src/project-js.ts";
|
|
41
|
-
export type {
|
|
42
|
-
export { DBXToolsPythonWorkspace } from "./src/project-py.ts";
|
|
43
|
-
export type { PythonRepositoryOptions, PythonPackageOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
|
|
41
|
+
export type { DBXToolsJavaScriptProject, DBXToolsJavaScriptProjectOptions, DBXToolsTypeScriptProjectOptions } from "./src/project-js.ts";
|
|
42
|
+
export { DBXToolsPythonProject, DBXToolsPythonWorkspace } from "./src/project-py.ts";
|
|
43
|
+
export type { PythonRepositoryOptions, PythonPackageOptions, DBXToolsPythonProjectOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
|
|
44
44
|
export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
|
|
45
45
|
export { DBXToolsRelease } from "./src/release.ts";
|
|
46
46
|
export type { StandaloneRelease, DBXToolsReleaseOptions } from "./src/release.ts";
|
package/package.json
CHANGED
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@clack/prompts": "^1.7.0",
|
|
29
|
-
"@dbx-tools/core": "0.6.
|
|
30
|
-
"@dbx-tools/path": "0.6.
|
|
31
|
-
"@dbx-tools/shared-core": "0.6.
|
|
29
|
+
"@dbx-tools/core": "0.6.79",
|
|
30
|
+
"@dbx-tools/path": "0.6.79",
|
|
31
|
+
"@dbx-tools/shared-core": "0.6.79",
|
|
32
32
|
"commander": "^15.0.0",
|
|
33
33
|
"concurrently": "^10.0.3",
|
|
34
34
|
"constructs": "^10.6.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"main": "index.ts",
|
|
49
49
|
"license": "Apache-2.0",
|
|
50
|
-
"version": "0.6.
|
|
50
|
+
"version": "0.6.79",
|
|
51
51
|
"types": "index.ts",
|
|
52
52
|
"type": "module",
|
|
53
53
|
"exports": {
|
package/src/pnpm-workspace.ts
CHANGED
|
@@ -250,6 +250,7 @@ export class PnpmWorkspaceState {
|
|
|
250
250
|
*/
|
|
251
251
|
public resolveMembers(project: Project, extraMembers: readonly string[] = []): void {
|
|
252
252
|
const members = project.subprojects
|
|
253
|
+
.filter((sub): sub is javascript.NodeProject => sub instanceof javascript.NodeProject)
|
|
253
254
|
.map((sub) => toPosix(relative(project.outdir, sub.outdir)))
|
|
254
255
|
.filter(Boolean);
|
|
255
256
|
// `extraMembers` are workspace siblings NOT attached as subprojects (e.g. the
|
package/src/project-js.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The dbx-tools project surface plus package tooling: the single
|
|
3
|
-
* {@link
|
|
3
|
+
* {@link DBXToolsJavaScriptProject} interface, the projen Node/TypeScript project classes,
|
|
4
4
|
* naming, guards, manifest fields, and the shared root init.
|
|
5
5
|
*
|
|
6
6
|
* {@link DBXToolsNodeProject} (monorepo root) and {@link DBXToolsTypeScriptProject}
|
|
7
|
-
* (a package, or a standalone compiling root) both implement {@link
|
|
7
|
+
* (a package, or a standalone compiling root) both implement {@link DBXToolsJavaScriptProject}.
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, readdirSync } from "node:fs";
|
|
10
10
|
import { dirname, join, relative, resolve } from "node:path";
|
|
@@ -41,13 +41,14 @@ import { DBXToolsRelease, type StandaloneRelease } from "./release.ts";
|
|
|
41
41
|
import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
|
|
42
42
|
import { DBXToolsRootTsconfig } from "./tsconfig.ts";
|
|
43
43
|
import { DBXToolsVsCode } from "./vscode.ts";
|
|
44
|
+
import type { DBXToolsProject, DBXToolsProjectOptions as CommonProjectOptions } from "./project.ts";
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* The dbx-tools project surface, backed by projen's Node toolchain. A single
|
|
47
48
|
* interface for both the monorepo root and each package: it carries the
|
|
48
49
|
* `dbxToolsConfig` component plus the npm-naming and root-only file components.
|
|
49
50
|
*/
|
|
50
|
-
export interface
|
|
51
|
+
export interface DBXToolsJavaScriptProject extends DBXToolsProject, javascript.NodeProject {
|
|
51
52
|
/** The package's `dbxToolsConfig` component (tags + `package.json` config). */
|
|
52
53
|
readonly dbxToolsConfig: DBXToolsConfig;
|
|
53
54
|
/** npm scope (the `@scope` in `@scope/pkg`), without the leading `@`. */
|
|
@@ -342,7 +343,9 @@ export const PROJEN_VERSION = "^0.101.16";
|
|
|
342
343
|
* inherits the root's config rather than emitting its own. `name`/`defaultReleaseBranch`
|
|
343
344
|
* are resolved/applied by the caller.
|
|
344
345
|
*/
|
|
345
|
-
function defaultProjectOptions(
|
|
346
|
+
function defaultProjectOptions(
|
|
347
|
+
options: DBXToolsJavaScriptProjectOptions,
|
|
348
|
+
): DBXToolsJavaScriptProjectOptions {
|
|
346
349
|
const isRoot = options.parent === undefined;
|
|
347
350
|
return {
|
|
348
351
|
// Bun owns install/run/build/test locally and in CI. projen renders
|
|
@@ -420,7 +423,7 @@ function defaultProjectOptions(options: DBXToolsProjectOptions): DBXToolsProject
|
|
|
420
423
|
* pristine array to seed a child's fresh one. Spread AFTER `...options`.
|
|
421
424
|
*/
|
|
422
425
|
function copiedGitIgnoreOptions(
|
|
423
|
-
options:
|
|
426
|
+
options: DBXToolsJavaScriptProjectOptions,
|
|
424
427
|
): Pick<javascript.NodeProjectOptions, "gitIgnoreOptions"> {
|
|
425
428
|
if (!options.gitIgnoreOptions?.ignorePatterns) return {};
|
|
426
429
|
return {
|
|
@@ -460,8 +463,9 @@ function defaultTypeScriptProjectOptions(
|
|
|
460
463
|
const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", "@types/bun@^1.3.14"];
|
|
461
464
|
|
|
462
465
|
/** Options for {@link DBXToolsNodeProject} (the monorepo root). */
|
|
463
|
-
export interface
|
|
466
|
+
export interface DBXToolsJavaScriptProjectOptions
|
|
464
467
|
extends
|
|
468
|
+
CommonProjectOptions,
|
|
465
469
|
Partial<javascript.NodeProjectOptions>,
|
|
466
470
|
DBXToolsConfigOptions,
|
|
467
471
|
DBXToolsPNPMWorkspaceOptions {
|
|
@@ -529,7 +533,7 @@ export interface DBXToolsProjectOptions
|
|
|
529
533
|
|
|
530
534
|
/** Options for {@link DBXToolsTypeScriptProject} (a package, or a compiling root). */
|
|
531
535
|
export interface DBXToolsTypeScriptProjectOptions
|
|
532
|
-
extends Partial<typescript.TypeScriptProjectOptions>,
|
|
536
|
+
extends Partial<typescript.TypeScriptProjectOptions>, DBXToolsJavaScriptProjectOptions {
|
|
533
537
|
/** Emit the projen-owned bun app scaffolding (`bunfig.toml`/`dev.ts`/`build.ts`). */
|
|
534
538
|
readonly bunApp?: boolean;
|
|
535
539
|
}
|
|
@@ -539,7 +543,11 @@ export interface DBXToolsTypeScriptProjectOptions
|
|
|
539
543
|
* {@link DBXToolsTypeScriptProject} per `src`-bearing folder, then emits the
|
|
540
544
|
* shared config, tasks, `pnpm-workspace.yaml`, and barrels-on-synth.
|
|
541
545
|
*/
|
|
542
|
-
export class DBXToolsNodeProject
|
|
546
|
+
export class DBXToolsNodeProject
|
|
547
|
+
extends javascript.NodeProject
|
|
548
|
+
implements DBXToolsJavaScriptProject
|
|
549
|
+
{
|
|
550
|
+
readonly language = "javascript" as const;
|
|
543
551
|
readonly scope: string;
|
|
544
552
|
readonly dbxToolsConfig: DBXToolsConfig;
|
|
545
553
|
pnpmWorkspace?: PnpmWorkspaceState;
|
|
@@ -548,7 +556,7 @@ export class DBXToolsNodeProject extends javascript.NodeProject implements DBXTo
|
|
|
548
556
|
private readonly extraWorkspaceMembers: readonly string[];
|
|
549
557
|
private readonly rootInstallOnly: boolean;
|
|
550
558
|
|
|
551
|
-
constructor(options:
|
|
559
|
+
constructor(options: DBXToolsJavaScriptProjectOptions = {}) {
|
|
552
560
|
const { name, scope } = resolveIdentity(options);
|
|
553
561
|
const releaseDefaults =
|
|
554
562
|
options.release && options.releaseTrigger === undefined
|
|
@@ -622,8 +630,9 @@ export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
|
|
|
622
630
|
*/
|
|
623
631
|
export class DBXToolsTypeScriptProject
|
|
624
632
|
extends typescript.TypeScriptProject
|
|
625
|
-
implements
|
|
633
|
+
implements DBXToolsJavaScriptProject
|
|
626
634
|
{
|
|
635
|
+
readonly language = "javascript" as const;
|
|
627
636
|
readonly scope: string;
|
|
628
637
|
readonly dbxToolsConfig: DBXToolsConfig;
|
|
629
638
|
pnpmWorkspace?: PnpmWorkspaceState;
|
|
@@ -816,7 +825,7 @@ class PrettierIgnoreGenerated extends Component {
|
|
|
816
825
|
/** Default leading path segment stripped from a package's name (not its tag). */
|
|
817
826
|
const DEFAULT_OMIT_RELATIVE_PREFIX = ["node"];
|
|
818
827
|
|
|
819
|
-
/** Normalize the {@link
|
|
828
|
+
/** Normalize the {@link DBXToolsJavaScriptProjectOptions.omitRelativePrefix} option to a slug list. */
|
|
820
829
|
function resolveOmitRelativePrefix(option: OneOrMany<string> | undefined): string[] {
|
|
821
830
|
const raw = option === undefined ? DEFAULT_OMIT_RELATIVE_PREFIX : option;
|
|
822
831
|
const list = Array.isArray(raw) ? raw : [raw];
|
|
@@ -943,7 +952,7 @@ export function taskScript(_project: javascript.NodeProject, script: string, arg
|
|
|
943
952
|
*/
|
|
944
953
|
function initProject(
|
|
945
954
|
project: DBXToolsNodeProject | DBXToolsTypeScriptProject,
|
|
946
|
-
options:
|
|
955
|
+
options: DBXToolsJavaScriptProjectOptions,
|
|
947
956
|
): void {
|
|
948
957
|
// projen's GithubProject seeds a `# replace this` SampleReadme on every
|
|
949
958
|
// project. READMEs are hand-written and owned outside projen, so drop the
|
|
@@ -1101,7 +1110,7 @@ function initProject(
|
|
|
1101
1110
|
|
|
1102
1111
|
// Already-attached subprojects, keyed by repo-relative member path.
|
|
1103
1112
|
const rootAbs = resolve(project.outdir);
|
|
1104
|
-
const existing = new Map<string,
|
|
1113
|
+
const existing = new Map<string, DBXToolsJavaScriptProject>();
|
|
1105
1114
|
for (const sub of project.subprojects) {
|
|
1106
1115
|
if (sub instanceof DBXToolsNodeProject || sub instanceof DBXToolsTypeScriptProject) {
|
|
1107
1116
|
existing.set(toPosix(relative(rootAbs, sub.outdir)), sub);
|
|
@@ -1182,7 +1191,7 @@ class ChildGitignore extends IgnoreFile {
|
|
|
1182
1191
|
*/
|
|
1183
1192
|
function swapChildGitignore(
|
|
1184
1193
|
project: javascript.NodeProject,
|
|
1185
|
-
options:
|
|
1194
|
+
options: DBXToolsJavaScriptProjectOptions,
|
|
1186
1195
|
): void {
|
|
1187
1196
|
project.tryRemoveFile(".gitignore");
|
|
1188
1197
|
const fresh = new ChildGitignore(project, ".gitignore", {
|
package/src/project-predicate.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { IConstruct } from "constructs";
|
|
|
11
11
|
import { Project } from "projen";
|
|
12
12
|
import { project } from "..";
|
|
13
13
|
import { toPosix } from "./packages.ts";
|
|
14
|
-
import { DBXToolsProject
|
|
14
|
+
import type { DBXToolsProject } from "./project.ts";
|
|
15
|
+
import type { DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Guard: the construct is a projen {@link Project} - the base every builder here
|
|
@@ -27,11 +28,19 @@ export function isProject(): Predicate<IConstruct, Project> {
|
|
|
27
28
|
return predicate.create((c: IConstruct): c is Project => Project.isProject(c));
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/** Guard: the construct
|
|
31
|
+
/** Guard: the construct implements the language-agnostic {@link DBXToolsProject} contract. */
|
|
31
32
|
export function isDBXToolsProject(): Predicate<IConstruct, DBXToolsProject> {
|
|
32
33
|
return isProject().and(
|
|
33
34
|
(project): project is DBXToolsProject =>
|
|
34
|
-
project
|
|
35
|
+
(project as Partial<DBXToolsProject>).language === "javascript" ||
|
|
36
|
+
(project as Partial<DBXToolsProject>).language === "python",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Guard: the construct is a dbx-tools JavaScript/TypeScript project. */
|
|
41
|
+
export function isDBXToolsJavaScriptProject(): Predicate<IConstruct, DBXToolsJavaScriptProject> {
|
|
42
|
+
return isDBXToolsProject().and(
|
|
43
|
+
(project): project is DBXToolsJavaScriptProject => project.language === "javascript",
|
|
35
44
|
);
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -97,14 +106,16 @@ export function hasIdentifierScope(
|
|
|
97
106
|
|
|
98
107
|
/**
|
|
99
108
|
* Matches DBXTools packages carrying every listed tag (`dbxToolsConfig.tags`), narrowing
|
|
100
|
-
* {@link Project} to {@link
|
|
109
|
+
* {@link Project} to {@link DBXToolsJavaScriptProject} (tags live only on JavaScript packages). Also the
|
|
101
110
|
* guard backing each built-in {@link PACKAGE_TAG_MIXINS} entry. Keep it in the SAME `.and(...)`
|
|
102
111
|
* as any name/path filter (or last when chaining) - a later non-tag `.and` re-widens to
|
|
103
112
|
* {@link Project} and drops the narrowing.
|
|
104
113
|
*/
|
|
105
|
-
export function hasTag(
|
|
114
|
+
export function hasTag(
|
|
115
|
+
...tags: OneOrMany<PathMatchInput>
|
|
116
|
+
): Predicate<IConstruct, DBXToolsJavaScriptProject> {
|
|
106
117
|
const matchers = projectMatchers(...tags);
|
|
107
|
-
return
|
|
118
|
+
return isDBXToolsJavaScriptProject().and((project) =>
|
|
108
119
|
matchers.every((matcher) => project.dbxToolsConfig.tags.some((tag) => matcher(tag))),
|
|
109
120
|
);
|
|
110
121
|
}
|
package/src/project-py.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Reusable uv workspace generation for Python packages hosted in a projen tree. */
|
|
2
2
|
import { string } from "@dbx-tools/shared-core";
|
|
3
|
-
import { Component,
|
|
3
|
+
import { Component, type Project, javascript, python, vscode } from "projen";
|
|
4
4
|
import { GithubWorkflow } from "projen/lib/github";
|
|
5
5
|
import { JobPermission } from "projen/lib/github/workflows-model";
|
|
6
|
+
import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
|
|
6
7
|
|
|
7
8
|
/** Git location used by direct `#subdirectory=` package dependencies. */
|
|
8
9
|
export interface PythonRepositoryOptions {
|
|
@@ -12,7 +13,7 @@ export interface PythonRepositoryOptions {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/** One independently installable Python package in the uv workspace. */
|
|
15
|
-
export interface PythonPackageOptions {
|
|
16
|
+
export interface PythonPackageOptions extends DBXToolsProjectOptions {
|
|
16
17
|
readonly directory: string;
|
|
17
18
|
readonly name: string;
|
|
18
19
|
readonly module: string;
|
|
@@ -20,10 +21,19 @@ export interface PythonPackageOptions {
|
|
|
20
21
|
readonly dependencies?: readonly string[];
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/** Options for one projen-native Python workspace member. */
|
|
25
|
+
export interface DBXToolsPythonProjectOptions extends DBXToolsProjectOptions {
|
|
26
|
+
readonly parent: Project;
|
|
27
|
+
readonly package: PythonPackageOptions;
|
|
28
|
+
readonly repository: Required<PythonRepositoryOptions>;
|
|
29
|
+
readonly requiresPython: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
/** Python release workflow configuration. */
|
|
24
33
|
export interface PythonReleaseOptions {
|
|
25
34
|
readonly workflowName?: string;
|
|
26
|
-
|
|
35
|
+
/** GitHub environment by Python distribution name. Defaults to `pypi-<name>`. */
|
|
36
|
+
readonly environments?: Readonly<Record<string, string>>;
|
|
27
37
|
readonly environmentUrl?: string;
|
|
28
38
|
}
|
|
29
39
|
|
|
@@ -69,27 +79,112 @@ function projectVscode(project: Project): vscode.VsCode | undefined {
|
|
|
69
79
|
return (project as Project & { readonly vscode?: vscode.VsCode }).vscode;
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
/** A Python package implemented with projen's `PythonProject` and uv backend. */
|
|
83
|
+
export class DBXToolsPythonProject extends python.PythonProject implements DBXToolsProject {
|
|
84
|
+
readonly language = "python" as const;
|
|
85
|
+
readonly packageOptions: PythonPackageOptions;
|
|
86
|
+
readonly uv: python.Uv;
|
|
87
|
+
|
|
88
|
+
constructor(options: DBXToolsPythonProjectOptions) {
|
|
89
|
+
const pkg = options.package;
|
|
90
|
+
super({
|
|
91
|
+
parent: options.parent,
|
|
92
|
+
outdir: pythonPackagePath(options.repository, pkg.directory),
|
|
93
|
+
name: pkg.name,
|
|
94
|
+
moduleName: pkg.module,
|
|
95
|
+
authorName: "",
|
|
96
|
+
authorEmail: "",
|
|
97
|
+
version: "0.0.0",
|
|
98
|
+
description: pkg.description,
|
|
99
|
+
github: false,
|
|
100
|
+
sample: false,
|
|
101
|
+
pytest: false,
|
|
102
|
+
projenrcPython: false,
|
|
103
|
+
projenrcJs: false,
|
|
104
|
+
projenrcTs: false,
|
|
105
|
+
pip: false,
|
|
106
|
+
venv: false,
|
|
107
|
+
setuptools: false,
|
|
108
|
+
poetry: false,
|
|
109
|
+
uv: true,
|
|
110
|
+
projenCommand: options.parent.projenCommand,
|
|
111
|
+
uvOptions: {
|
|
112
|
+
project: {
|
|
113
|
+
name: pkg.name,
|
|
114
|
+
version: "0.0.0",
|
|
115
|
+
description: pkg.description,
|
|
116
|
+
readme: "README.md",
|
|
117
|
+
requiresPython: options.requiresPython,
|
|
118
|
+
dependencies: [...(pkg.dependencies ?? [])],
|
|
119
|
+
urls: {
|
|
120
|
+
Source: `${options.repository.url.replace(/\.git$/, "")}/tree/${options.repository.ref}/${pythonPackagePath(options.repository, pkg.directory)}`,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
buildSystem: {
|
|
124
|
+
requires: ["uv_build>=0.11.28,<0.12.0"],
|
|
125
|
+
buildBackend: "uv_build",
|
|
126
|
+
},
|
|
127
|
+
uv: {
|
|
128
|
+
buildBackend: {
|
|
129
|
+
moduleName: pkg.module,
|
|
130
|
+
moduleRoot: "src",
|
|
131
|
+
namespace: true,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
this.packageOptions = pkg;
|
|
137
|
+
if (!(this.packagingManager instanceof python.Uv)) {
|
|
138
|
+
throw new Error(`Expected uv packaging for ${pkg.name}`);
|
|
139
|
+
}
|
|
140
|
+
this.uv = this.packagingManager;
|
|
141
|
+
this.uv.file.addDeletionOverride("project.authors");
|
|
142
|
+
this.uv.file.addDeletionOverride("dependency-groups");
|
|
143
|
+
this.uv.file.readonly = true;
|
|
144
|
+
|
|
145
|
+
for (const path of [".gitattributes", ".gitignore"]) {
|
|
146
|
+
this.tryRemoveFile(path);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The root workspace owns dependency installation for every member. */
|
|
151
|
+
public override postSynthesize(): void {}
|
|
152
|
+
}
|
|
153
|
+
|
|
72
154
|
/**
|
|
73
|
-
* Generates a root uv workspace, member
|
|
74
|
-
* interpreter selection, and an optional
|
|
155
|
+
* Generates a root uv workspace, projen-native Python member projects, Python
|
|
156
|
+
* tasks, editor interpreter selection, and an optional publishing workflow.
|
|
75
157
|
*/
|
|
76
158
|
export class DBXToolsPythonWorkspace extends Component {
|
|
77
|
-
readonly packages: readonly
|
|
159
|
+
readonly packages: readonly DBXToolsPythonProject[];
|
|
78
160
|
readonly repository: Required<PythonRepositoryOptions>;
|
|
79
161
|
readonly requiresPython: string;
|
|
162
|
+
readonly file: python.PyprojectTomlFile;
|
|
80
163
|
|
|
81
164
|
constructor(project: javascript.NodeProject, options: DBXToolsPythonWorkspaceOptions) {
|
|
82
165
|
super(project);
|
|
83
|
-
this.packages = options.packages;
|
|
84
166
|
this.repository = {
|
|
85
167
|
url: options.repository.url,
|
|
86
168
|
ref: options.repository.ref ?? "main",
|
|
87
169
|
root: options.repository.root ?? "packages/py",
|
|
88
170
|
};
|
|
89
171
|
this.requiresPython = options.requiresPython ?? ">=3.10";
|
|
90
|
-
|
|
91
|
-
this.
|
|
92
|
-
|
|
172
|
+
this.file = this.emitWorkspace(project, options);
|
|
173
|
+
this.packages = options.packages.map(
|
|
174
|
+
(pkg) =>
|
|
175
|
+
new DBXToolsPythonProject({
|
|
176
|
+
parent: project,
|
|
177
|
+
package: pkg,
|
|
178
|
+
repository: this.repository,
|
|
179
|
+
requiresPython: this.requiresPython,
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
for (const pkg of this.packages) {
|
|
183
|
+
const pyproject = `/${pythonPackagePath(this.repository, pkg.packageOptions.directory)}/pyproject.toml`;
|
|
184
|
+
project.gitignore.include(pyproject);
|
|
185
|
+
project.gitattributes.addAttributes(pyproject, "linguist-generated");
|
|
186
|
+
project.prettier?.addIgnorePattern(pyproject.slice(1));
|
|
187
|
+
}
|
|
93
188
|
this.addTasks(project, options);
|
|
94
189
|
|
|
95
190
|
const interpreterPath = options.interpreterPath ?? "${workspaceFolder}/.venv/bin/python";
|
|
@@ -115,81 +210,45 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
115
210
|
private emitWorkspace(
|
|
116
211
|
project: javascript.NodeProject,
|
|
117
212
|
options: DBXToolsPythonWorkspaceOptions,
|
|
118
|
-
):
|
|
119
|
-
const devDependencies = options.devDependencies ?? DEFAULT_DEV_DEPENDENCIES;
|
|
213
|
+
): python.PyprojectTomlFile {
|
|
120
214
|
const testPaths = options.testPaths ?? [this.repository.root];
|
|
121
|
-
const ruffTarget = options.ruffTarget ?? "py310";
|
|
122
215
|
const perFileIgnores = options.ruffPerFileIgnores ?? {};
|
|
123
|
-
new
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"line-length = 100",
|
|
153
|
-
"",
|
|
154
|
-
"[tool.ruff.lint.per-file-ignores]",
|
|
155
|
-
...Object.entries(perFileIgnores).map(
|
|
156
|
-
([path, rules]) => `${quote(path)} = [${rules.map(quote).join(", ")}]`,
|
|
157
|
-
),
|
|
158
|
-
"",
|
|
159
|
-
],
|
|
216
|
+
const file = new python.PyprojectTomlFile(project, {
|
|
217
|
+
project: {
|
|
218
|
+
name: options.workspaceName ?? `${string.toSlug(project.name)}-python-workspace`,
|
|
219
|
+
version: "0.0.0",
|
|
220
|
+
requiresPython: this.requiresPython,
|
|
221
|
+
dependencies: [],
|
|
222
|
+
},
|
|
223
|
+
dependencyGroups: {
|
|
224
|
+
dev: [...(options.devDependencies ?? DEFAULT_DEV_DEPENDENCIES)],
|
|
225
|
+
},
|
|
226
|
+
tool: {
|
|
227
|
+
uv: python.uvConfig.toJson_UvConfiguration({
|
|
228
|
+
package: false,
|
|
229
|
+
workspace: { members: [`${this.repository.root}/*`] },
|
|
230
|
+
}),
|
|
231
|
+
pytest: {
|
|
232
|
+
ini_options: {
|
|
233
|
+
asyncio_mode: "auto",
|
|
234
|
+
testpaths: testPaths,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
ruff: {
|
|
238
|
+
"target-version": options.ruffTarget ?? "py310",
|
|
239
|
+
"line-length": 100,
|
|
240
|
+
lint: {
|
|
241
|
+
"per-file-ignores": perFileIgnores,
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
160
245
|
});
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
readonly: true,
|
|
168
|
-
lines: [
|
|
169
|
-
'# ~~ Generated by projen. To modify, edit .projenrc.ts and run "bunx projen".',
|
|
170
|
-
"[project]",
|
|
171
|
-
`name = ${quote(pkg.name)}`,
|
|
172
|
-
'version = "0.0.0"',
|
|
173
|
-
`description = ${quote(pkg.description)}`,
|
|
174
|
-
'readme = "README.md"',
|
|
175
|
-
`requires-python = ${quote(this.requiresPython)}`,
|
|
176
|
-
`dependencies = [${(pkg.dependencies ?? []).map(quote).join(", ")}]`,
|
|
177
|
-
"",
|
|
178
|
-
"[project.urls]",
|
|
179
|
-
`Source = ${quote(`${this.repository.url.replace(/\.git$/, "")}/tree/${this.repository.ref}/${this.packagePath(pkg.directory)}`)}`,
|
|
180
|
-
"",
|
|
181
|
-
"[build-system]",
|
|
182
|
-
'requires = ["uv_build>=0.11.28,<0.12.0"]',
|
|
183
|
-
'build-backend = "uv_build"',
|
|
184
|
-
"",
|
|
185
|
-
"[tool.uv.build-backend]",
|
|
186
|
-
`module-name = ${quote(pkg.module)}`,
|
|
187
|
-
'module-root = "src"',
|
|
188
|
-
"namespace = true",
|
|
189
|
-
"",
|
|
190
|
-
],
|
|
191
|
-
});
|
|
192
|
-
}
|
|
246
|
+
file.addOverride(
|
|
247
|
+
"tool.uv.sources",
|
|
248
|
+
Object.fromEntries(options.packages.map((pkg) => [pkg.name, { workspace: true }])),
|
|
249
|
+
);
|
|
250
|
+
file.readonly = true;
|
|
251
|
+
return file;
|
|
193
252
|
}
|
|
194
253
|
|
|
195
254
|
private addTasks(project: javascript.NodeProject, options: DBXToolsPythonWorkspaceOptions): void {
|
|
@@ -247,12 +306,20 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
247
306
|
env: { VERSION: "${{ inputs.version }}" },
|
|
248
307
|
run: this.renderVersionStampScript(),
|
|
249
308
|
},
|
|
250
|
-
{
|
|
309
|
+
{
|
|
310
|
+
name: "Build distributions",
|
|
311
|
+
run: this.packages
|
|
312
|
+
.map(
|
|
313
|
+
(pkg) =>
|
|
314
|
+
`uv build --package ${pkg.packageOptions.name} --out-dir dist/${pkg.packageOptions.directory}`,
|
|
315
|
+
)
|
|
316
|
+
.join("\n"),
|
|
317
|
+
},
|
|
251
318
|
{
|
|
252
319
|
name: "Validate distributions",
|
|
253
320
|
run: [
|
|
254
|
-
`test "$(find dist -
|
|
255
|
-
"uvx twine check dist
|
|
321
|
+
`test "$(find dist -type f \\( -name '*.whl' -o -name '*.tar.gz' \\) | wc -l | tr -d ' ')" -eq ${this.packages.length * 2}`,
|
|
322
|
+
"uvx twine check dist/*/*.whl dist/*/*.tar.gz",
|
|
256
323
|
].join("\n"),
|
|
257
324
|
},
|
|
258
325
|
{
|
|
@@ -262,29 +329,34 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
262
329
|
},
|
|
263
330
|
],
|
|
264
331
|
});
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
steps: [
|
|
276
|
-
{
|
|
277
|
-
name: "Download distributions",
|
|
278
|
-
uses: "actions/download-artifact@v8",
|
|
279
|
-
with: { name: "python-distributions", path: "dist" },
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
name: "Publish to PyPI",
|
|
283
|
-
uses: "pypa/gh-action-pypi-publish@release/v1",
|
|
284
|
-
with: { "packages-dir": "dist" },
|
|
332
|
+
for (const pkg of this.packages) {
|
|
333
|
+
workflow.addJob(`publish-${pkg.packageOptions.directory}`, {
|
|
334
|
+
if: "${{ inputs.publish }}",
|
|
335
|
+
needs: ["build"],
|
|
336
|
+
environment: {
|
|
337
|
+
name:
|
|
338
|
+
options.environments?.[pkg.packageOptions.name] ?? `pypi-${pkg.packageOptions.name}`,
|
|
339
|
+
url:
|
|
340
|
+
options.environmentUrl ??
|
|
341
|
+
`https://pypi.org/project/${pkg.packageOptions.name.replaceAll("_", "-")}/`,
|
|
285
342
|
},
|
|
286
|
-
|
|
287
|
-
|
|
343
|
+
runsOn: ["ubuntu-latest"],
|
|
344
|
+
permissions: { idToken: JobPermission.WRITE },
|
|
345
|
+
timeoutMinutes: 10,
|
|
346
|
+
steps: [
|
|
347
|
+
{
|
|
348
|
+
name: "Download distributions",
|
|
349
|
+
uses: "actions/download-artifact@v8",
|
|
350
|
+
with: { name: "python-distributions", path: "dist" },
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: `Publish ${pkg.packageOptions.name} to PyPI`,
|
|
354
|
+
uses: "pypa/gh-action-pypi-publish@release/v1",
|
|
355
|
+
with: { "packages-dir": `dist/${pkg.packageOptions.directory}` },
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
});
|
|
359
|
+
}
|
|
288
360
|
}
|
|
289
361
|
|
|
290
362
|
private renderVersionStampScript(): string {
|
package/src/project.ts
CHANGED
|
@@ -5,14 +5,27 @@
|
|
|
5
5
|
import { type PathMatchInput } from "@dbx-tools/path";
|
|
6
6
|
import { object, type OneOrMany } from "@dbx-tools/shared-core";
|
|
7
7
|
import { type IConstruct } from "constructs";
|
|
8
|
-
import { Project } from "projen";
|
|
8
|
+
import { Project, type ProjectOptions } from "projen";
|
|
9
9
|
import * as mixin from "./mixin.ts";
|
|
10
10
|
import * as projectPredicate from "./project-predicate.ts";
|
|
11
|
-
import type {
|
|
11
|
+
import type { DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
12
12
|
|
|
13
13
|
export * from "./project-js.ts";
|
|
14
14
|
export * from "./project-py.ts";
|
|
15
15
|
|
|
16
|
+
/** Runtime family implemented by a dbx-tools project. */
|
|
17
|
+
export type DBXToolsProjectLanguage = "javascript" | "python";
|
|
18
|
+
|
|
19
|
+
/** Options shared by every dbx-tools project implementation. */
|
|
20
|
+
export interface DBXToolsProjectOptions extends Partial<
|
|
21
|
+
Pick<ProjectOptions, "name" | "parent" | "outdir">
|
|
22
|
+
> {}
|
|
23
|
+
|
|
24
|
+
/** Minimal language-agnostic project contract. */
|
|
25
|
+
export interface DBXToolsProject extends Project {
|
|
26
|
+
readonly language: DBXToolsProjectLanguage;
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
/** Filters selecting which projects an {@link applyToProjects} call runs against. */
|
|
17
30
|
export interface ApplyToProjectsOptions {
|
|
18
31
|
/** Include plain projen projects. Defaults to DBXTools projects only. */
|
|
@@ -45,8 +58,8 @@ type ApplyToAllProjectsOptions = Omit<ApplyToProjectsOptions, "includeNonDBXTool
|
|
|
45
58
|
export function applyToProjects(
|
|
46
59
|
construct: IConstruct,
|
|
47
60
|
...args:
|
|
48
|
-
| [ApplyToDBXToolsProjectsOptions, ...OneOrMany<(project:
|
|
49
|
-
| OneOrMany<(project:
|
|
61
|
+
| [ApplyToDBXToolsProjectsOptions, ...OneOrMany<(project: DBXToolsJavaScriptProject) => void>]
|
|
62
|
+
| OneOrMany<(project: DBXToolsJavaScriptProject) => void>
|
|
50
63
|
): void;
|
|
51
64
|
|
|
52
65
|
export function applyToProjects(
|
|
@@ -71,7 +84,7 @@ export function applyToProjects<P extends Project>(
|
|
|
71
84
|
const callbacks = (hasOptions ? rest : args) as OneOrMany<(project: Project) => void>;
|
|
72
85
|
let predicate = projectPredicate.isProject();
|
|
73
86
|
if (!options?.includeNonDBXToolsProjects) {
|
|
74
|
-
predicate = predicate.and(projectPredicate.
|
|
87
|
+
predicate = predicate.and(projectPredicate.isDBXToolsJavaScriptProject());
|
|
75
88
|
}
|
|
76
89
|
if (!options?.includeRoots) predicate = predicate.and((project) => project.parent != null);
|
|
77
90
|
if (options?.identifierPackageName) {
|
package/src/publish.ts
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import type { javascript } from "projen";
|
|
31
31
|
import { typescript } from "projen";
|
|
32
|
-
import {
|
|
32
|
+
import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
|
|
33
33
|
import { addPackageFiles, applyCompilerOptions, applyIncludes } from "./project.ts";
|
|
34
34
|
|
|
35
35
|
/** Directory `tsc` emits into, and the root of every published entry point. */
|
|
@@ -63,7 +63,7 @@ export const COMPILED_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions = {
|
|
|
63
63
|
*/
|
|
64
64
|
export function publishesCompiled(pkg: javascript.NodeProject): boolean {
|
|
65
65
|
if (!(pkg instanceof typescript.TypeScriptProject) || !pkg.parent) return false;
|
|
66
|
-
return
|
|
66
|
+
return isDBXToolsJavaScriptProject()(pkg) && !pkg.dbxToolsConfig.tags.includes("ui");
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** The `./lib/...` stem of a source path, or `undefined` if it is not TypeScript. */
|
package/tasks/bump.ts
CHANGED
|
@@ -30,12 +30,11 @@
|
|
|
30
30
|
* `--publish` / `--no-publish` is an alias for `--push` (pushing the tag is
|
|
31
31
|
* what publishes). The tag prefix comes from `--prefix` (default `v`).
|
|
32
32
|
*
|
|
33
|
-
* `--local-registry <value>` publishes
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* local registry. Values:
|
|
33
|
+
* `--local-registry <value>` publishes npm packages to a LOCAL registry (e.g.
|
|
34
|
+
* verdaccio) right after the tag push. `--local-pypi <value>` does the same for
|
|
35
|
+
* Python packages through a writable devpi index. Values for both:
|
|
37
36
|
* - `auto` (default): publish only when `npm config get registry` is a
|
|
38
|
-
* loopback host
|
|
37
|
+
* loopback host, or uv's default index is a loopback devpi `+simple` URL.
|
|
39
38
|
* - `false`: never publish locally.
|
|
40
39
|
* - a URL: always publish to that registry.
|
|
41
40
|
*/
|
|
@@ -45,6 +44,7 @@ import { fileURLToPath } from "node:url";
|
|
|
45
44
|
import { exec, project } from "@dbx-tools/core";
|
|
46
45
|
import { log, net } from "@dbx-tools/shared-core";
|
|
47
46
|
import { Command, Option } from "commander";
|
|
47
|
+
import { activePythonIndex, resolveLocalPypi } from "./python-registry.ts";
|
|
48
48
|
|
|
49
49
|
const logger = log.logger("projen:bump");
|
|
50
50
|
const LEVELS = ["patch", "minor", "major"] as const;
|
|
@@ -188,6 +188,12 @@ program
|
|
|
188
188
|
"publish locally after the tag push: 'auto' (only a loopback npm registry), 'false', or a registry URL",
|
|
189
189
|
"auto",
|
|
190
190
|
)
|
|
191
|
+
.option(
|
|
192
|
+
"--local-pypi <value>",
|
|
193
|
+
"publish Python packages locally: 'auto' (only a loopback devpi +simple index), 'false', or a devpi URL",
|
|
194
|
+
"auto",
|
|
195
|
+
)
|
|
196
|
+
.option("--python-root <path>", "Python workspace package root", "packages/py")
|
|
191
197
|
.action(
|
|
192
198
|
(opts: {
|
|
193
199
|
level: Level;
|
|
@@ -199,7 +205,9 @@ program
|
|
|
199
205
|
tag: boolean;
|
|
200
206
|
push: boolean;
|
|
201
207
|
publish: boolean;
|
|
208
|
+
localPypi: string;
|
|
202
209
|
localRegistry: string;
|
|
210
|
+
pythonRoot: string;
|
|
203
211
|
}) => {
|
|
204
212
|
const pkgPath = resolve(process.cwd(), "package.json");
|
|
205
213
|
if (!existsSync(pkgPath)) throw new Error(`no package.json in ${process.cwd()}`);
|
|
@@ -314,6 +322,45 @@ program
|
|
|
314
322
|
logger.success(`published ${version} to ${localRegistry}`);
|
|
315
323
|
}
|
|
316
324
|
|
|
325
|
+
const activeIndex = activePythonIndex();
|
|
326
|
+
const localPypi = resolveLocalPypi(opts.localPypi, activeIndex);
|
|
327
|
+
const pythonRoot = resolve(opts.pythonRoot);
|
|
328
|
+
if (
|
|
329
|
+
opts.localPypi.toLowerCase() === "auto" &&
|
|
330
|
+
activeIndex &&
|
|
331
|
+
net.isLoopbackHost(new URL(activeIndex)) &&
|
|
332
|
+
!localPypi
|
|
333
|
+
) {
|
|
334
|
+
logger.info(`skipped local Python publish: ${activeIndex} is not a devpi +simple index`);
|
|
335
|
+
}
|
|
336
|
+
if (opts.version === false && localPypi) {
|
|
337
|
+
logger.info("skipped local Python publish (--no-version left packages unstamped)");
|
|
338
|
+
} else if (opts.version && localPypi && existsSync(pythonRoot)) {
|
|
339
|
+
logger.info(`publishing Python ${version} to local devpi ${localPypi.publishUrl}`);
|
|
340
|
+
const publishPythonScript = fileURLToPath(new URL("./publish-python.ts", import.meta.url));
|
|
341
|
+
exec.spawnSync(
|
|
342
|
+
"bun",
|
|
343
|
+
[
|
|
344
|
+
publishPythonScript,
|
|
345
|
+
version,
|
|
346
|
+
"--root",
|
|
347
|
+
pythonRoot,
|
|
348
|
+
"--index-url",
|
|
349
|
+
localPypi.indexUrl,
|
|
350
|
+
"--publish-url",
|
|
351
|
+
localPypi.publishUrl,
|
|
352
|
+
],
|
|
353
|
+
{
|
|
354
|
+
cwd: process.cwd(),
|
|
355
|
+
stdout: "inherit",
|
|
356
|
+
stderr: "inherit",
|
|
357
|
+
stdin: "ignore",
|
|
358
|
+
check: true,
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
logger.success(`published Python ${version} to ${localPypi.publishUrl}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
317
364
|
// Publishing can run package lifecycle hooks, including a standalone
|
|
318
365
|
// project's own projen synth, which rewrites its generated manifest back
|
|
319
366
|
// to 0.0.0. Re-assert the release version last so root and every sibling
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { basename, join, resolve } from "node:path";
|
|
14
|
+
import { exec } from "@dbx-tools/core";
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
|
|
17
|
+
interface PythonProjectFile {
|
|
18
|
+
readonly directory: string;
|
|
19
|
+
readonly mode: number;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly source: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function escapeRegExp(value: string): string {
|
|
26
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function stampPythonProjects(root: string, version: string): () => void {
|
|
30
|
+
const packageFiles = readdirSync(root, { withFileTypes: true })
|
|
31
|
+
.filter((entry) => entry.isDirectory())
|
|
32
|
+
.map((entry) => resolve(root, entry.name, "pyproject.toml"))
|
|
33
|
+
.filter(existsSync)
|
|
34
|
+
.sort();
|
|
35
|
+
const projects: PythonProjectFile[] = packageFiles.map((path) => {
|
|
36
|
+
const source = readFileSync(path, "utf8");
|
|
37
|
+
const name = /^name = "([^"]+)"$/m.exec(source)?.[1];
|
|
38
|
+
if (!name) throw new Error(`Missing project name in ${path}`);
|
|
39
|
+
return {
|
|
40
|
+
directory: basename(resolve(path, "..")),
|
|
41
|
+
mode: statSync(path).mode,
|
|
42
|
+
name,
|
|
43
|
+
path,
|
|
44
|
+
source,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
for (const project of projects) {
|
|
51
|
+
let stamped = project.source.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
|
|
52
|
+
if (stamped === project.source) {
|
|
53
|
+
throw new Error(`Expected one project version in ${project.path}`);
|
|
54
|
+
}
|
|
55
|
+
for (const sibling of projects) {
|
|
56
|
+
stamped = stamped.replace(
|
|
57
|
+
new RegExp(
|
|
58
|
+
`${escapeRegExp(sibling.name)} @ git\\+[^" ]+#subdirectory=[^" ]+/${escapeRegExp(sibling.directory)}`,
|
|
59
|
+
"g",
|
|
60
|
+
),
|
|
61
|
+
`${sibling.name}==${version}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
chmodSync(project.path, project.mode | 0o200);
|
|
65
|
+
writeFileSync(project.path, stamped);
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
for (const project of projects) {
|
|
69
|
+
chmodSync(project.path, project.mode | 0o200);
|
|
70
|
+
writeFileSync(project.path, project.source);
|
|
71
|
+
chmodSync(project.path, project.mode);
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
for (const project of projects) {
|
|
78
|
+
chmodSync(project.path, project.mode | 0o200);
|
|
79
|
+
writeFileSync(project.path, project.source);
|
|
80
|
+
chmodSync(project.path, project.mode);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function publishPythonProjects(options: {
|
|
86
|
+
readonly dryRun?: boolean;
|
|
87
|
+
readonly indexUrl: string;
|
|
88
|
+
readonly publishUrl: string;
|
|
89
|
+
readonly root: string;
|
|
90
|
+
readonly version: string;
|
|
91
|
+
}): void {
|
|
92
|
+
const root = resolve(options.root);
|
|
93
|
+
const output = mkdtempSync(join(tmpdir(), "dbx-tools-python-publish-"));
|
|
94
|
+
const restore = stampPythonProjects(root, options.version);
|
|
95
|
+
try {
|
|
96
|
+
exec.spawnSync("uv", ["build", "--all-packages", "--out-dir", output], {
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
stdout: "inherit",
|
|
99
|
+
stderr: "inherit",
|
|
100
|
+
stdin: "ignore",
|
|
101
|
+
check: true,
|
|
102
|
+
});
|
|
103
|
+
exec.spawnSync(
|
|
104
|
+
"uvx",
|
|
105
|
+
[
|
|
106
|
+
"--from",
|
|
107
|
+
"devpi-client",
|
|
108
|
+
"devpi",
|
|
109
|
+
"upload",
|
|
110
|
+
"--index",
|
|
111
|
+
options.publishUrl,
|
|
112
|
+
"--from-dir",
|
|
113
|
+
...(options.dryRun ? ["--dry-run"] : []),
|
|
114
|
+
output,
|
|
115
|
+
],
|
|
116
|
+
{
|
|
117
|
+
cwd: process.cwd(),
|
|
118
|
+
env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl },
|
|
119
|
+
stdout: "inherit",
|
|
120
|
+
stderr: "inherit",
|
|
121
|
+
stdin: "ignore",
|
|
122
|
+
check: true,
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
} finally {
|
|
126
|
+
restore();
|
|
127
|
+
rmSync(output, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (import.meta.main) {
|
|
132
|
+
const program = new Command();
|
|
133
|
+
program
|
|
134
|
+
.argument("<version>", "Python package version")
|
|
135
|
+
.requiredOption("--index-url <url>", "devpi Simple API URL")
|
|
136
|
+
.requiredOption("--publish-url <url>", "devpi writable index URL")
|
|
137
|
+
.option("--root <path>", "Python workspace package root", "packages/py")
|
|
138
|
+
.option("--dry-run", "build and inspect distributions without uploading")
|
|
139
|
+
.action(
|
|
140
|
+
(
|
|
141
|
+
version: string,
|
|
142
|
+
options: { dryRun?: boolean; indexUrl: string; publishUrl: string; root: string },
|
|
143
|
+
) => publishPythonProjects({ ...options, version }),
|
|
144
|
+
);
|
|
145
|
+
await program.parseAsync();
|
|
146
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { exec } from "@dbx-tools/core";
|
|
5
|
+
import { net } from "@dbx-tools/shared-core";
|
|
6
|
+
|
|
7
|
+
export interface LocalPythonRegistry {
|
|
8
|
+
readonly indexUrl: string;
|
|
9
|
+
readonly publishUrl: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Read the default index URL from uv's TOML configuration. */
|
|
13
|
+
export function parseUvDefaultIndex(source: string): string | undefined {
|
|
14
|
+
const blocks = source.split(/(?=^\[\[index\]\]\s*$)/m);
|
|
15
|
+
for (const block of blocks) {
|
|
16
|
+
if (!/^\[\[index\]\]\s*$/m.test(block) || !/^\s*default\s*=\s*true\s*$/m.test(block)) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const url = /^\s*url\s*=\s*["']([^"']+)["']\s*$/m.exec(block)?.[1];
|
|
20
|
+
if (url) return url;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Convert a devpi Simple API URL into its writable index URL. */
|
|
26
|
+
export function devpiRegistry(index: string): LocalPythonRegistry | undefined {
|
|
27
|
+
let url: URL;
|
|
28
|
+
try {
|
|
29
|
+
url = new URL(index);
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!net.isLoopbackHost(url)) return undefined;
|
|
34
|
+
|
|
35
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
36
|
+
if (!path.endsWith("/+simple")) return undefined;
|
|
37
|
+
url.pathname = `${path.slice(0, -"/+simple".length)}/`;
|
|
38
|
+
url.search = "";
|
|
39
|
+
url.hash = "";
|
|
40
|
+
return {
|
|
41
|
+
indexUrl: new URL("+simple/", url).href,
|
|
42
|
+
publishUrl: url.href,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The active Python package index, preferring uv because Python builds use uv. */
|
|
47
|
+
export function activePythonIndex(): string | undefined {
|
|
48
|
+
for (const value of [process.env.UV_DEFAULT_INDEX, process.env.UV_INDEX_URL]) {
|
|
49
|
+
if (value?.trim()) return value.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const uvConfig = process.env.UV_CONFIG_FILE ?? resolve(homedir(), ".config/uv/uv.toml");
|
|
53
|
+
if (existsSync(uvConfig)) {
|
|
54
|
+
const index = parseUvDefaultIndex(readFileSync(uvConfig, "utf8"));
|
|
55
|
+
if (index) return index;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (process.env.PIP_INDEX_URL?.trim()) return process.env.PIP_INDEX_URL.trim();
|
|
59
|
+
const pip = exec.spawnSync("python", ["-m", "pip", "config", "get", "global.index-url"], {
|
|
60
|
+
cwd: process.cwd(),
|
|
61
|
+
stdout: "capture",
|
|
62
|
+
stderr: "ignore",
|
|
63
|
+
stdin: "ignore",
|
|
64
|
+
check: false,
|
|
65
|
+
});
|
|
66
|
+
return pip.stdout?.trim() || undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resolve `auto`, `false`, or an explicit devpi index/publish URL. */
|
|
70
|
+
export function resolveLocalPypi(
|
|
71
|
+
value: string,
|
|
72
|
+
activeIndex: string | undefined = activePythonIndex(),
|
|
73
|
+
): LocalPythonRegistry | undefined {
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!trimmed || trimmed.toLowerCase() === "false") return undefined;
|
|
76
|
+
if (trimmed.toLowerCase() === "auto") {
|
|
77
|
+
return activeIndex ? devpiRegistry(activeIndex) : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const derived = devpiRegistry(trimmed);
|
|
81
|
+
if (derived) return derived;
|
|
82
|
+
const publishUrl = trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
83
|
+
return {
|
|
84
|
+
publishUrl,
|
|
85
|
+
indexUrl: new URL("+simple/", publishUrl).href,
|
|
86
|
+
};
|
|
87
|
+
}
|