@dbx-tools/projen 0.9.17 → 0.9.19

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.
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  chmodSync,
4
4
  existsSync,
5
+ mkdirSync,
5
6
  mkdtempSync,
6
7
  readFileSync,
7
8
  readdirSync,
@@ -29,10 +30,19 @@ interface PythonProjectFile {
29
30
  }
30
31
 
31
32
  export interface StampPythonProjectsOptions {
33
+ readonly excludeUniFFI?: boolean;
32
34
  readonly rewriteDependencies?: boolean;
33
35
  readonly versions?: ReadonlyMap<string, string>;
34
36
  }
35
37
 
38
+ /** Python wheel and source archives accepted by Twine and package indexes. */
39
+ export function pythonDistributionPaths(directory: string): string[] {
40
+ return readdirSync(directory)
41
+ .filter((file) => file.endsWith(".whl") || file.endsWith(".tar.gz"))
42
+ .sort()
43
+ .map((file) => join(directory, file));
44
+ }
45
+
36
46
  export interface RestorePythonProjects {
37
47
  (): void;
38
48
  readonly paths: readonly string[];
@@ -63,7 +73,10 @@ export function stampPythonProjects(
63
73
  };
64
74
  });
65
75
  const projects = allProjects.filter(
66
- (project) => !project.private && (!options.versions || options.versions.has(project.directory)),
76
+ (project) =>
77
+ !project.private &&
78
+ (!options.excludeUniFFI || !project.uniffi) &&
79
+ (!options.versions || options.versions.has(project.directory)),
67
80
  );
68
81
  if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
69
82
 
@@ -110,12 +123,65 @@ export function publishPythonProjects(options: {
110
123
  readonly version: string;
111
124
  readonly plan?: ReleasePlan;
112
125
  }): void {
113
- const root = resolve(options.root);
114
126
  const output = mkdtempSync(join(tmpdir(), "projen-python-publish-"));
127
+ try {
128
+ buildPythonProjects({ ...options, output });
129
+ exec.spawnSync(
130
+ "uvx",
131
+ [
132
+ "--from",
133
+ "devpi-client",
134
+ "devpi",
135
+ "upload",
136
+ "--index",
137
+ options.publishUrl,
138
+ "--from-dir",
139
+ ...(options.dryRun ? ["--dry-run"] : []),
140
+ output,
141
+ ],
142
+ {
143
+ cwd: process.cwd(),
144
+ env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl },
145
+ stdout: "inherit",
146
+ stderr: "inherit",
147
+ stdin: "ignore",
148
+ check: true,
149
+ },
150
+ );
151
+ } finally {
152
+ rmSync(output, { recursive: true, force: true });
153
+ }
154
+ }
155
+
156
+ /** Build the exact Python release distributions without publishing them. */
157
+ export function buildPythonProjects(options: {
158
+ readonly allowEmpty?: boolean;
159
+ readonly excludeUniFFI?: boolean;
160
+ readonly output: string;
161
+ readonly root: string;
162
+ readonly version: string;
163
+ readonly plan?: ReleasePlan;
164
+ }): void {
165
+ const root = resolve(options.root);
166
+ const output = resolve(options.output);
167
+ rmSync(output, { recursive: true, force: true });
168
+ const standardProjects = options.excludeUniFFI
169
+ ? readdirSync(root, { withFileTypes: true })
170
+ .filter((entry) => entry.isDirectory())
171
+ .map((entry) => resolve(root, entry.name, "pyproject.toml"))
172
+ .filter(existsSync)
173
+ .map((path) => pythonProjectInfo(readFileSync(path, "utf8"), { parse, stringify }))
174
+ .filter((project) => !project.private && !project.uniffi)
175
+ : undefined;
176
+ if (standardProjects?.length === 0 && options.allowEmpty) {
177
+ mkdirSync(output, { recursive: true });
178
+ return;
179
+ }
115
180
  const planned = new Map(
116
181
  options.plan?.pythonPackages.map((pkg) => [pkg.path.replace(/^.*\//, ""), pkg.version]) ?? [],
117
182
  );
118
183
  const stamp = stampPythonProjects(root, options.version, {
184
+ excludeUniFFI: options.excludeUniFFI,
119
185
  ...(options.plan ? { versions: planned } : {}),
120
186
  });
121
187
  try {
@@ -130,6 +196,16 @@ export function publishPythonProjects(options: {
130
196
  check: true,
131
197
  });
132
198
  }
199
+ } else if (options.excludeUniFFI) {
200
+ for (const project of standardProjects ?? []) {
201
+ exec.spawnSync("uv", ["build", "--package", project.name, "--out-dir", output], {
202
+ cwd: process.cwd(),
203
+ stdout: "inherit",
204
+ stderr: "inherit",
205
+ stdin: "ignore",
206
+ check: true,
207
+ });
208
+ }
133
209
  } else {
134
210
  exec.spawnSync("uv", ["build", "--all-packages", "--out-dir", output], {
135
211
  cwd: process.cwd(),
@@ -139,31 +215,19 @@ export function publishPythonProjects(options: {
139
215
  check: true,
140
216
  });
141
217
  }
142
- exec.spawnSync(
143
- "uvx",
144
- [
145
- "--from",
146
- "devpi-client",
147
- "devpi",
148
- "upload",
149
- "--index",
150
- options.publishUrl,
151
- "--from-dir",
152
- ...(options.dryRun ? ["--dry-run"] : []),
153
- output,
154
- ],
155
- {
156
- cwd: process.cwd(),
157
- env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl },
158
- stdout: "inherit",
159
- stderr: "inherit",
160
- stdin: "ignore",
161
- check: true,
162
- },
163
- );
218
+ const distributions = pythonDistributionPaths(output);
219
+ if (distributions.length === 0) {
220
+ throw new Error(`No Python distributions found in ${output}`);
221
+ }
222
+ exec.spawnSync("uvx", ["twine", "check", ...distributions], {
223
+ cwd: process.cwd(),
224
+ stdout: "inherit",
225
+ stderr: "inherit",
226
+ stdin: "ignore",
227
+ check: true,
228
+ });
164
229
  } finally {
165
230
  stamp();
166
- rmSync(output, { recursive: true, force: true });
167
231
  }
168
232
  }
169
233
 
@@ -175,6 +239,7 @@ if (import.meta.main) {
175
239
  .requiredOption("--publish-url <url>", "devpi writable index URL")
176
240
  .option("--root <path>", "Python workspace package root", "packages/py")
177
241
  .option("--plan <path>", "affected release plan")
242
+ .option("--output <path>", "Build distributions into a directory without publishing")
178
243
  .option("--dry-run", "build and inspect distributions without uploading")
179
244
  .action(
180
245
  (
@@ -185,6 +250,7 @@ if (import.meta.main) {
185
250
  publishUrl: string;
186
251
  root: string;
187
252
  plan?: string;
253
+ output?: string;
188
254
  },
189
255
  ) => {
190
256
  const plan = options.plan
@@ -192,7 +258,16 @@ if (import.meta.main) {
192
258
  : undefined;
193
259
  const fallbackVersion = version ?? plan?.pythonPackages[0]?.version;
194
260
  if (!fallbackVersion) throw new Error("Python publication requires a version or plan");
195
- publishPythonProjects({ ...options, version: fallbackVersion, plan });
261
+ if (options.output) {
262
+ buildPythonProjects({
263
+ output: options.output,
264
+ root: options.root,
265
+ version: fallbackVersion,
266
+ plan,
267
+ });
268
+ } else {
269
+ publishPythonProjects({ ...options, version: fallbackVersion, plan });
270
+ }
196
271
  },
197
272
  );
198
273
  await program.parseAsync();
package/tasks/publish.ts CHANGED
@@ -55,7 +55,9 @@
55
55
  */
56
56
  import {
57
57
  chmodSync,
58
+ copyFileSync,
58
59
  existsSync,
60
+ mkdirSync,
59
61
  mkdtempSync,
60
62
  readFileSync,
61
63
  rmSync,
@@ -234,12 +236,14 @@ const plan =
234
236
  : undefined;
235
237
  if (!version && !plan) {
236
238
  logger.error(
237
- "usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--exclude <dir>] [--dry-run] [--skip-compile]",
239
+ "usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--output <dir>] [--exclude <dir>] [--dry-run] [--skip-compile]",
238
240
  );
239
241
  process.exit(1);
240
242
  }
241
243
  const registryIdx = rest.indexOf("--registry");
242
244
  const registry = registryIdx >= 0 ? rest[registryIdx + 1] : undefined;
245
+ const outputIdx = rest.indexOf("--output");
246
+ const output = outputIdx >= 0 ? resolve(root, rest[outputIdx + 1]) : undefined;
243
247
  const dryRun = rest.includes("--dry-run");
244
248
  const skipCompile = rest.includes("--skip-compile");
245
249
  const concurrencyIdx = rest.indexOf("--concurrency");
@@ -371,8 +375,12 @@ for (const { dir } of publishable) {
371
375
  }
372
376
 
373
377
  logger.info(
374
- `${dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
378
+ `${output ? "packing" : dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
375
379
  );
380
+ if (output) {
381
+ rmSync(output, { recursive: true, force: true });
382
+ mkdirSync(output, { recursive: true });
383
+ }
376
384
  await asyncUtils.mapConcurrent(
377
385
  publishable,
378
386
  async ({ dir, name, version: packageVersion, access }) => {
@@ -390,6 +398,15 @@ await asyncUtils.mapConcurrent(
390
398
  `Packed npm access ${String(local.access)} does not match ${String(access)} for ${name}`,
391
399
  );
392
400
  }
401
+ if (output) {
402
+ const destination = join(output, archive.split(/[\\/]/).at(-1)!);
403
+ if (existsSync(destination)) {
404
+ throw new Error(`Duplicate npm archive name: ${destination}`);
405
+ }
406
+ copyFileSync(archive, destination);
407
+ logger.info(`packed ${name} @ ${packageVersion}`);
408
+ return;
409
+ }
393
410
  if (!dryRun) {
394
411
  const published = await publishedNpmRelease(local.name, local.version, registry);
395
412
  if (npmReleaseMatches(local, published)) {
@@ -410,4 +427,6 @@ await asyncUtils.mapConcurrent(
410
427
  },
411
428
  { concurrency, errorMode: "settle" },
412
429
  );
413
- logger.success(`${dryRun ? "dry-run: packed" : "published"} ${publishable.length} packages`);
430
+ logger.success(
431
+ `${output || dryRun ? "packed" : "published"} ${publishable.length} packages${output ? ` to ${output}` : ""}`,
432
+ );