@dbx-tools/projen 0.9.12 → 0.9.18

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.
@@ -15,6 +15,7 @@ import * as projectUtils from "@dbx-tools/core/project-utils";
15
15
  import { asyncUtils, json, log, object } from "@dbx-tools/shared-core";
16
16
  import { Command } from "commander";
17
17
  import { publishLocalRelease } from "./local-publish.ts";
18
+ import { hasLocalReleaseTargets, missingLocalReleaseTools } from "./release-assets.ts";
18
19
  import { generateReleaseSummary } from "./release-summary.ts";
19
20
  import {
20
21
  releaseArchitectureOption,
@@ -38,20 +39,57 @@ import {
38
39
  githubAccountSupportsWorkflowChanges,
39
40
  githubAuthenticatedAccounts,
40
41
  githubRepositoryApiPath,
42
+ githubRepositoryCanManage,
41
43
  githubRepositoryIdentity,
44
+ githubRepositoryPermissions,
42
45
  githubRepositorySpecifier,
43
46
  githubTokenArguments,
44
47
  githubTokenEnvironmentName,
45
48
  } from "../src/release-github.ts";
49
+ import { withWorkspaceMutationLock } from "../src/workspace-lock.ts";
46
50
  import {
47
51
  readWorkspaceVersion,
48
52
  resolveBaseVersion,
49
53
  resolveNextVersion,
50
54
  } from "../src/workspace-version.ts";
51
- import { withWorkspaceMutationLock } from "../src/workspace-lock.ts";
52
55
 
53
56
  const logger = log.logger("projen:release");
54
57
 
58
+ export type ReleaseBuildMode = "auto" | "local" | "remote";
59
+
60
+ /** Resolve an explicit or environment-sensitive native release build mode. */
61
+ export function resolveReleaseBuildMode(options: {
62
+ readonly requested: ReleaseBuildMode;
63
+ readonly canManage: boolean;
64
+ readonly localTools: readonly string[];
65
+ readonly hasLocalTargets: boolean;
66
+ readonly approve: boolean;
67
+ readonly wait: boolean;
68
+ readonly hostPlatform?: NodeJS.Platform;
69
+ }): Exclude<ReleaseBuildMode, "auto"> {
70
+ const local =
71
+ options.canManage &&
72
+ options.hasLocalTargets &&
73
+ (options.hostPlatform ?? process.platform) === "darwin" &&
74
+ options.localTools.length === 0 &&
75
+ options.approve &&
76
+ options.wait;
77
+ if (options.requested === "auto") return local ? "local" : "remote";
78
+ if (options.requested === "remote") return "remote";
79
+ if (!options.canManage) throw new Error("--build local requires maintain or admin permission");
80
+ if (!options.hasLocalTargets) throw new Error("--build local requires Rust release targets");
81
+ if ((options.hostPlatform ?? process.platform) !== "darwin") {
82
+ throw new Error("--build local requires macOS");
83
+ }
84
+ if (options.localTools.length > 0) {
85
+ throw new Error(`--build local requires: ${options.localTools.join(", ")}`);
86
+ }
87
+ if (!options.approve || !options.wait) {
88
+ throw new Error("--build local requires automatic merge and waiting");
89
+ }
90
+ return "local";
91
+ }
92
+
55
93
  function git(
56
94
  root: string,
57
95
  args: string[],
@@ -151,12 +189,27 @@ async function waitForReleaseWorkflow(
151
189
  });
152
190
  }
153
191
 
192
+ async function waitForGitHubRelease(
193
+ root: string,
194
+ tag: string,
195
+ env: NodeJS.ProcessEnv,
196
+ timeoutMs: number,
197
+ ): Promise<void> {
198
+ const deadline = Date.now() + timeoutMs;
199
+ while (Date.now() < deadline) {
200
+ if (taskCommandSucceeds(root, "gh", ["release", "view", tag], { env })) return;
201
+ await asyncUtils.sleep(2_000);
202
+ }
203
+ throw new Error(`GitHub Release ${tag} did not appear within ${timeoutMs}ms`);
204
+ }
205
+
154
206
  function githubAccount(root: string): {
155
207
  hostname: string;
156
208
  owner: string;
157
209
  repository: string;
158
210
  login: string;
159
211
  token: string;
212
+ canManage: boolean;
160
213
  } {
161
214
  const repository = projectUtils.repositoryUrl(root);
162
215
  if (!repository) throw new Error("Release preparation requires a GitHub repository");
@@ -199,21 +252,20 @@ function githubAccount(root: string): {
199
252
  });
200
253
  const repositoryData =
201
254
  repositoryResult.exitCode === 0 ? json.parseRecord(repositoryResult.stdout ?? "") : undefined;
202
- const permissions = object.isRecord(repositoryData?.permissions)
203
- ? repositoryData.permissions
204
- : undefined;
205
- if (
206
- permissions?.push !== true &&
207
- permissions?.maintain !== true &&
208
- permissions?.admin !== true
209
- ) {
255
+ const permissions = githubRepositoryPermissions(repositoryData);
256
+ if (!permissions.push && !permissions.maintain && !permissions.admin) {
210
257
  continue;
211
258
  }
212
259
  logger.info("selected GitHub CLI account", {
213
260
  login: account.login,
214
261
  repository: `${identity.owner}/${identity.repository}`,
215
262
  });
216
- return { ...identity, login: account.login, token };
263
+ return {
264
+ ...identity,
265
+ login: account.login,
266
+ token,
267
+ canManage: githubRepositoryCanManage(permissions),
268
+ };
217
269
  }
218
270
  const detected =
219
271
  accounts
@@ -271,6 +323,7 @@ program
271
323
  .option("--no-validate", "skip repository validation tasks, Rust tests, and TypeScript compile")
272
324
  .option("--no-local-publish", "skip local npm, PyPI, and Cargo publication")
273
325
  .option("--no-local-cargo", "skip local Cargo publication")
326
+ .option("--build <mode>", "native release build: auto, local, or remote", "auto")
274
327
  .option("--no-approve", "open the release pull request without enabling automatic merge")
275
328
  .option("--no-wait", "return after enabling automatic merge without watching publication")
276
329
  .option(
@@ -295,6 +348,7 @@ program
295
348
  validate: boolean;
296
349
  localPublish: boolean;
297
350
  localCargo: boolean;
351
+ build: ReleaseBuildMode;
298
352
  approve: boolean;
299
353
  wait: boolean;
300
354
  waitTimeoutMinutes: string;
@@ -316,6 +370,26 @@ program
316
370
  const currentBranch = git(root, ["branch", "--show-current"], { capture: true });
317
371
  if (!currentBranch) throw new Error("Release preparation requires a local branch");
318
372
  const account = githubAccount(root);
373
+ if (!["auto", "local", "remote"].includes(opts.build)) {
374
+ throw new Error("--build must be auto, local, or remote");
375
+ }
376
+ const inspectLocalTools =
377
+ opts.build === "local" || (opts.build === "auto" && account.canManage);
378
+ const localTools = inspectLocalTools ? missingLocalReleaseTools(root) : [];
379
+ const buildMode = resolveReleaseBuildMode({
380
+ requested: opts.build,
381
+ canManage: account.canManage,
382
+ localTools,
383
+ hasLocalTargets: hasLocalReleaseTargets(root),
384
+ approve: opts.approve,
385
+ wait: opts.wait,
386
+ });
387
+ logger.info("selected native release build", {
388
+ requested: opts.build,
389
+ resolved: buildMode,
390
+ manager: account.canManage,
391
+ ...(localTools.length > 0 ? { missing: localTools } : {}),
392
+ });
319
393
  git(root, ["fetch", "--tags", "origin", opts.base]);
320
394
  const comparisonBase = resolveBaseVersion(root, [opts.prefix], {
321
395
  fetch: false,
@@ -455,7 +529,13 @@ program
455
529
  git(releaseRoot, ["add", "-A"]);
456
530
  const staged = git(releaseRoot, ["diff", "--cached", "--name-only"], { capture: true });
457
531
  if (staged) {
458
- git(releaseRoot, ["commit", "-m", `chore(release): ${next.version}`]);
532
+ git(releaseRoot, [
533
+ "commit",
534
+ "-m",
535
+ `chore(release): ${next.version}`,
536
+ "-m",
537
+ `Release-Build: ${buildMode}`,
538
+ ]);
459
539
  } else if (!worktreeExists) {
460
540
  throw new Error("Release preparation produced no changes");
461
541
  }
@@ -471,6 +551,8 @@ program
471
551
  `Source commit: ${git(releaseRoot, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
472
552
  "",
473
553
  "Merging this PR updates VERSION on main and starts the public release workflow.",
554
+ "",
555
+ `Native build: ${buildMode}.`,
474
556
  ...(releaseSummary ? ["", releaseSummary] : []),
475
557
  ].join("\n");
476
558
  const githubEnvironment = {
@@ -511,8 +593,6 @@ program
511
593
  env: githubEnvironment,
512
594
  });
513
595
  }
514
- git(root, ["worktree", "remove", "--force", releaseRoot]);
515
- git(root, ["branch", "--delete", "--force", releaseBranch]);
516
596
  if (opts.approve && opts.wait) {
517
597
  const mergeSha = await waitForPullRequestMerge(
518
598
  root,
@@ -521,10 +601,39 @@ program
521
601
  waitTimeoutMs,
522
602
  );
523
603
  logger.info("release pull request merged", { releaseBranch, mergeSha });
604
+ if (buildMode === "local") {
605
+ git(root, ["fetch", "origin", opts.base]);
606
+ git(releaseRoot, ["switch", "--detach", mergeSha]);
607
+ runTaskCommand(releaseRoot, process.execPath, ["install"]);
608
+ await waitForGitHubRelease(root, releaseTag, githubEnvironment, waitTimeoutMs);
609
+ const releaseAssetsScript = join(
610
+ releaseRoot,
611
+ "node_modules/@dbx-tools/projen/tasks/release-assets.ts",
612
+ );
613
+ runTaskCommand(
614
+ releaseRoot,
615
+ process.execPath,
616
+ [
617
+ releaseAssetsScript,
618
+ "--root",
619
+ releaseRoot,
620
+ "--version",
621
+ next.version,
622
+ "--tag",
623
+ releaseTag,
624
+ "--upload",
625
+ ],
626
+ { env: githubEnvironment },
627
+ );
628
+ }
629
+ git(root, ["worktree", "remove", "--force", releaseRoot]);
630
+ git(root, ["branch", "--delete", "--force", releaseBranch]);
524
631
  await waitForReleaseWorkflow(root, opts.base, mergeSha, githubEnvironment, waitTimeoutMs);
525
632
  logger.success(`published ${releaseTag} from ${mergeSha}`);
526
633
  return;
527
634
  }
635
+ git(root, ["worktree", "remove", "--force", releaseRoot]);
636
+ git(root, ["branch", "--delete", "--force", releaseBranch]);
528
637
  logger.success(
529
638
  `${
530
639
  opts.approve ? "enabled automatic merge for" : "opened"
@@ -534,4 +643,4 @@ program
534
643
  },
535
644
  );
536
645
 
537
- await program.parseAsync();
646
+ if (import.meta.main) await program.parseAsync();
package/tasks/rust.ts CHANGED
@@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
4
  import { dirname, isAbsolute, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { log } from "@dbx-tools/shared-core";
6
+ import { log, object } from "@dbx-tools/shared-core";
7
7
  import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
8
8
  import {
9
9
  discoverRustCrates,
@@ -40,7 +40,7 @@ function currentStructure(config: RustWorkspaceMapping): RustWorkspaceMapping {
40
40
  const binding = recorded.get(rust);
41
41
  return binding ? [binding] : [{ crate: "", rust }];
42
42
  });
43
- return { root: config.root, crates, bindings, binaries: config.binaries ?? [] };
43
+ return { ...config, crates, bindings };
44
44
  }
45
45
 
46
46
  /** Whether discovered crate membership or UniFFI marker membership changed. */
@@ -87,18 +87,47 @@ function generate(binding: RustBindingMapping): void {
87
87
  if (result.status !== 0) throw new Error(`binding generation exited with ${result.status}`);
88
88
  }
89
89
 
90
- export function affectedRustBindings(
90
+ function* iterateAffectedRustBindings(
91
91
  bindings: readonly RustBindingMapping[],
92
92
  changed: ReadonlySet<string>,
93
- ): RustBindingMapping[] {
93
+ ): Generator<RustBindingMapping> {
94
94
  const affected = new Set(changed);
95
- const ordered = orderRustBindings(bindings);
96
- for (const binding of ordered) {
95
+ for (const binding of orderRustBindings(bindings)) {
97
96
  if (binding.dependencies?.some((dependency) => affected.has(dependency))) {
98
97
  affected.add(binding.crate);
99
98
  }
99
+ if (affected.has(binding.crate)) yield binding;
100
100
  }
101
- return ordered.filter((binding) => affected.has(binding.crate));
101
+ }
102
+
103
+ export function affectedRustBindings(
104
+ bindings: readonly RustBindingMapping[],
105
+ changed: ReadonlySet<string>,
106
+ ): RustBindingMapping[] {
107
+ return [...iterateAffectedRustBindings(bindings, changed)];
108
+ }
109
+
110
+ /**
111
+ * Lazily resolve direct binding owners so lock checks stop at the first match.
112
+ */
113
+ export function changedRustOwners(
114
+ config: RustWorkspaceMapping,
115
+ changed: readonly string[],
116
+ ): object.Sequence<RustBindingMapping> {
117
+ return object
118
+ .sequence(changed)
119
+ .map((path) => ownerBinding(path, config.bindings))
120
+ .nonNull()
121
+ .distinct();
122
+ }
123
+
124
+ /** Collect direct owners, then lazily yield them and their dependents in order. */
125
+ export function changedRustBindings(
126
+ config: RustWorkspaceMapping,
127
+ changed: readonly string[],
128
+ ): object.Sequence<RustBindingMapping> {
129
+ const targets = new Set(changedRustOwners(config, changed).map((binding) => binding.crate));
130
+ return object.sequence(iterateAffectedRustBindings(config.bindings, targets));
102
131
  }
103
132
 
104
133
  const config = rustConfig();
@@ -113,37 +142,37 @@ async function main(): Promise<void> {
113
142
  return;
114
143
  }
115
144
 
116
- watchLoop("rust", [resolve(repoRoot, config.root)], (changed) => {
117
- const latest = rustConfig() ?? config;
118
- if (rustStructureChanged(latest)) {
119
- logger.start("Rust project structure changed - re-synthesizing (+install)");
120
- runSynth({ post: true });
121
- logger.success("Rust project structure synchronized");
122
- const refreshed = rustConfig();
123
- if (!refreshed) return;
124
- const targets = new Map<string, RustBindingMapping>();
125
- for (const path of changed) {
126
- const binding = ownerBinding(path, refreshed.bindings);
127
- if (binding) targets.set(binding.crate, binding);
145
+ watchLoop(
146
+ "rust",
147
+ [resolve(repoRoot, config.root)],
148
+ (changed) => {
149
+ const latest = rustConfig() ?? config;
150
+ if (rustStructureChanged(latest)) {
151
+ logger.start("Rust project structure changed - re-synthesizing (+install)");
152
+ runSynth({ post: true });
153
+ logger.success("Rust project structure synchronized");
154
+ const refreshed = rustConfig();
155
+ if (!refreshed) return;
156
+ for (const binding of changedRustBindings(refreshed, changed)) {
157
+ logger.start(`generating ${binding.crate} bindings`);
158
+ generate(binding);
159
+ logger.success(`generated ${binding.crate} bindings`);
160
+ }
161
+ return;
128
162
  }
129
- for (const binding of affectedRustBindings(refreshed.bindings, new Set(targets.keys()))) {
163
+ for (const binding of changedRustBindings(latest, changed)) {
130
164
  logger.start(`generating ${binding.crate} bindings`);
131
165
  generate(binding);
132
166
  logger.success(`generated ${binding.crate} bindings`);
133
167
  }
134
- return;
135
- }
136
- const targets = new Map<string, RustBindingMapping>();
137
- for (const path of changed) {
138
- const binding = ownerBinding(path, latest.bindings);
139
- if (binding) targets.set(binding.crate, binding);
140
- }
141
- for (const binding of affectedRustBindings(latest.bindings, new Set(targets.keys()))) {
142
- logger.start(`generating ${binding.crate} bindings`);
143
- generate(binding);
144
- logger.success(`generated ${binding.crate} bindings`);
145
- }
146
- });
168
+ },
169
+ {
170
+ check: (changed) => {
171
+ const latest = rustConfig() ?? config;
172
+ return changedRustOwners(latest, changed).some(() => true) || rustStructureChanged(latest);
173
+ },
174
+ },
175
+ );
147
176
  }
148
177
 
149
178
  if (import.meta.main) await main();
@@ -5,6 +5,7 @@ import {
5
5
  mkdirSync,
6
6
  mkdtempSync,
7
7
  readFileSync,
8
+ readdirSync,
8
9
  rmSync,
9
10
  writeFileSync,
10
11
  } from "node:fs";
@@ -130,6 +131,7 @@ export const installPythonBindings = ({
130
131
  module,
131
132
  packageRoot,
132
133
  platform,
134
+ generatedSource,
133
135
  readonly = false,
134
136
  run,
135
137
  targetDirectory,
@@ -137,20 +139,25 @@ export const installPythonBindings = ({
137
139
  const packageName = module.split(".").at(-1);
138
140
  if (!packageName) throw new Error(`Invalid Python module ${module}`);
139
141
  const packageDirectory = resolve(packageRoot, "src", ...module.split("."));
140
- const generatedDirectory = mkdtempSync(join(tmpdir(), `${packageName}-python-`));
142
+ const generatedDirectory = generatedSource
143
+ ? undefined
144
+ : mkdtempSync(join(tmpdir(), `${packageName}-python-`));
141
145
  const generator = resolve(targetDirectory, pythonBindingGeneratorName(crate, platform));
142
- if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
143
146
 
144
147
  try {
145
- run(
146
- generator,
147
- pythonBindingGeneratorArgs({
148
- crate,
149
- library,
150
- output: generatedDirectory,
151
- }),
152
- );
153
- const generated = join(generatedDirectory, `${crate.replaceAll("-", "_")}.py`);
148
+ if (!generatedSource) {
149
+ if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
150
+ run(
151
+ generator,
152
+ pythonBindingGeneratorArgs({
153
+ crate,
154
+ library,
155
+ output: generatedDirectory,
156
+ }),
157
+ );
158
+ }
159
+ const generated =
160
+ generatedSource ?? join(generatedDirectory, `${crate.replaceAll("-", "_")}.py`);
154
161
  if (!existsSync(generated)) throw new Error(`Missing generated binding ${generated}`);
155
162
  mkdirSync(packageDirectory, { recursive: true });
156
163
 
@@ -162,6 +169,11 @@ export const installPythonBindings = ({
162
169
  setWritable(init);
163
170
  writeFileSync(init, `${generatedHeader(crate)}from .bindings import *\n`);
164
171
 
172
+ for (const candidate of readdirSync(packageDirectory)) {
173
+ if (/\.(?:dll|dylib|so)$/.test(candidate)) {
174
+ rmSync(join(packageDirectory, candidate), { force: true });
175
+ }
176
+ }
165
177
  const nativeLibrary = join(packageDirectory, basename(library));
166
178
  setWritable(nativeLibrary);
167
179
  cpSync(library, nativeLibrary);
@@ -174,6 +186,6 @@ export const installPythonBindings = ({
174
186
  rmSync(join(packageDirectory, "_generated"), { recursive: true, force: true });
175
187
  return { bindings, init, library: nativeLibrary };
176
188
  } finally {
177
- rmSync(generatedDirectory, { recursive: true, force: true });
189
+ if (generatedDirectory) rmSync(generatedDirectory, { recursive: true, force: true });
178
190
  }
179
191
  };
@@ -17,7 +17,12 @@ import { fileURLToPath } from "node:url";
17
17
  import { createRequire } from "node:module";
18
18
  import { parseArgs } from "node:util";
19
19
  import { spawnSync } from "node:child_process";
20
- import { installPythonBindings, stampPythonProject } from "./uniffi-python.js";
20
+ import {
21
+ installPythonBindings,
22
+ pythonBindingGeneratorArgs,
23
+ pythonBindingGeneratorName,
24
+ stampPythonProject,
25
+ } from "./uniffi-python.js";
21
26
 
22
27
  const require = createRequire(import.meta.url);
23
28
  const bundledToml = fileURLToPath(new URL("./smol-toml.cjs", import.meta.url));
@@ -34,7 +39,9 @@ const parsed = parseArgs({
34
39
  "native-package": { type: "string" },
35
40
  "python-package": { type: "string" },
36
41
  "python-module": { type: "string" },
42
+ "python-bindings": { type: "string" },
37
43
  "cargo-target": { type: "string" },
44
+ "generator-target": { type: "string" },
38
45
  "node-triple": { type: "string" },
39
46
  "python-tag": { type: "string" },
40
47
  os: { type: "string" },
@@ -227,7 +234,9 @@ const facadeDependency = (name, dependency, catalog, workspaceVersions) => {
227
234
  if (dependency.startsWith("workspace:")) {
228
235
  const version = workspaceVersions.get(name);
229
236
  if (!version) throw new Error(`Missing workspace version for ${name}`);
230
- return dependency === "workspace:*" ? version : `${dependency.slice("workspace:".length)}${version}`;
237
+ return dependency === "workspace:*"
238
+ ? version
239
+ : `${dependency.slice("workspace:".length)}${version}`;
231
240
  }
232
241
  if (!dependency.startsWith("catalog:")) return dependency;
233
242
  const resolved = catalog[name];
@@ -412,8 +421,10 @@ const packagePython = ({
412
421
  pythonTag,
413
422
  version,
414
423
  cargoTarget,
424
+ generatorTarget,
415
425
  os,
416
426
  pythonModule,
427
+ pythonBindings,
417
428
  }) => {
418
429
  const pythonRoot = resolve(output, "python-root");
419
430
  cpSync(resolve(root, pythonDirectory), pythonRoot, { recursive: true });
@@ -440,8 +451,9 @@ const packagePython = ({
440
451
  module: pythonModule,
441
452
  packageRoot: pythonRoot,
442
453
  platform: os,
454
+ generatedSource: pythonBindings,
443
455
  run,
444
- targetDirectory: resolve(cargoTargetRoot, cargoTarget, "release"),
456
+ targetDirectory: resolve(cargoTargetRoot, generatorTarget ?? cargoTarget, "release"),
445
457
  });
446
458
 
447
459
  const wheelDirectory = resolve(output, "python");
@@ -461,6 +473,25 @@ const packagePython = ({
461
473
  ]);
462
474
  };
463
475
 
476
+ const generatePython = () => {
477
+ const crate = required("crate");
478
+ const cargoTarget = required("cargo-target");
479
+ const os = required("os");
480
+ const output = resolve(root, required("output"));
481
+ const generator = resolve(
482
+ cargoTargetRoot,
483
+ cargoTarget,
484
+ "release",
485
+ pythonBindingGeneratorName(crate, os),
486
+ );
487
+ const library = libraryPath(crate, cargoTarget, os);
488
+ if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
489
+ if (!existsSync(library)) throw new Error(`Missing native library ${library}`);
490
+ rmSync(output, { recursive: true, force: true });
491
+ mkdirSync(output, { recursive: true });
492
+ run(generator, pythonBindingGeneratorArgs({ crate, library, output }));
493
+ };
494
+
464
495
  const build = () => {
465
496
  const crate = required("crate");
466
497
  const cargoTarget = required("cargo-target");
@@ -481,6 +512,8 @@ const build = () => {
481
512
  ...(existsSync(resolve(root, "Cargo.lock")) ? ["--locked"] : []),
482
513
  "--package",
483
514
  crate,
515
+ "--features",
516
+ "uniffi-bindgen",
484
517
  "--target",
485
518
  cargoTarget,
486
519
  ]);
@@ -513,14 +546,18 @@ const build = () => {
513
546
  pythonTag,
514
547
  version,
515
548
  cargoTarget,
549
+ generatorTarget: parsed.values["generator-target"],
516
550
  os,
517
551
  pythonModule: required("python-module"),
552
+ pythonBindings: parsed.values["python-bindings"],
518
553
  });
519
554
  }
520
555
  };
521
556
 
522
557
  if (parsed.positionals[0] === "build") {
523
558
  build();
559
+ } else if (parsed.positionals[0] === "generate-python") {
560
+ generatePython();
524
561
  } else if (parsed.positionals[0] === "facade") {
525
562
  packageNodeFacade({
526
563
  output: resolve(root, required("output")),
package/tasks/uniffi.ts CHANGED
@@ -119,6 +119,8 @@ if (!values["skip-build"]) {
119
119
  ...(existsSync(join(root, "Cargo.lock")) ? ["--locked"] : []),
120
120
  "--package",
121
121
  crate,
122
+ "--features",
123
+ "uniffi-bindgen",
122
124
  ...(values["cargo-target"] ? ["--target", values["cargo-target"]] : []),
123
125
  ]);
124
126
  }