pyfig 0.3.0 → 0.4.0

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.
Files changed (63) hide show
  1. package/README.md +112 -121
  2. package/SECURITY.md +1 -1
  3. package/dist/cjs/index.js +469 -871
  4. package/dist/cjs/index.js.map +14 -15
  5. package/dist/cjs/sync-resolver.js +426 -657
  6. package/dist/cjs/sync-resolver.js.map +12 -12
  7. package/dist/cli.js +5 -8
  8. package/dist/cli.js.map +1 -1
  9. package/dist/client.d.ts +3 -8
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +13 -11
  12. package/dist/client.js.map +1 -1
  13. package/dist/config.d.ts +4 -13
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +7 -51
  16. package/dist/config.js.map +1 -1
  17. package/dist/index.d.ts +5 -6
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/package-target.d.ts +7 -11
  22. package/dist/package-target.d.ts.map +1 -1
  23. package/dist/package-target.js +41 -57
  24. package/dist/package-target.js.map +1 -1
  25. package/dist/probes.d.ts +0 -18
  26. package/dist/probes.d.ts.map +1 -1
  27. package/dist/probes.js +0 -70
  28. package/dist/probes.js.map +1 -1
  29. package/dist/process.d.ts +1 -1
  30. package/dist/process.d.ts.map +1 -1
  31. package/dist/process.js +2 -1
  32. package/dist/process.js.map +1 -1
  33. package/dist/project-root.d.ts +1 -1
  34. package/dist/project-root.d.ts.map +1 -1
  35. package/dist/project-root.js +3 -64
  36. package/dist/project-root.js.map +1 -1
  37. package/dist/provision-lock.d.ts +10 -0
  38. package/dist/provision-lock.d.ts.map +1 -0
  39. package/dist/provision-lock.js +24 -0
  40. package/dist/provision-lock.js.map +1 -0
  41. package/dist/provision.d.ts +18 -6
  42. package/dist/provision.d.ts.map +1 -1
  43. package/dist/provision.js +273 -228
  44. package/dist/provision.js.map +1 -1
  45. package/dist/runners.d.ts +3 -7
  46. package/dist/runners.d.ts.map +1 -1
  47. package/dist/runners.js +35 -115
  48. package/dist/runners.js.map +1 -1
  49. package/dist/sync-resolver.js +4 -13
  50. package/dist/sync-resolver.js.map +1 -1
  51. package/dist/virtual-environment.d.ts +3 -1
  52. package/dist/virtual-environment.d.ts.map +1 -1
  53. package/dist/virtual-environment.js +8 -1
  54. package/dist/virtual-environment.js.map +1 -1
  55. package/package.json +6 -8
  56. package/dist/coordination.d.ts +0 -25
  57. package/dist/coordination.d.ts.map +0 -1
  58. package/dist/coordination.js +0 -187
  59. package/dist/coordination.js.map +0 -1
  60. package/dist/state.d.ts +0 -10
  61. package/dist/state.d.ts.map +0 -1
  62. package/dist/state.js +0 -62
  63. package/dist/state.js.map +0 -1
package/dist/cjs/index.js CHANGED
@@ -66,19 +66,12 @@ var __export = (target, all) => {
66
66
  var exports_src = {};
67
67
  __export(exports_src, {
68
68
  DEFAULT_MANAGED_PYTHON: () => DEFAULT_MANAGED_PYTHON,
69
- DEFAULT_PIP_RANGE: () => DEFAULT_PIP_RANGE,
70
69
  DEFAULT_PYTHON_RANGE: () => DEFAULT_PYTHON_RANGE,
71
70
  DEFAULT_UV_RANGE: () => DEFAULT_UV_RANGE,
72
71
  DEFAULT_UV_VERSION: () => DEFAULT_UV_VERSION,
73
- clearPyfigCaches: () => clearPyfigCaches,
74
72
  createPyfig: () => createPyfig,
75
73
  ensureEnvironment: () => ensureEnvironment,
76
74
  ensureUv: () => ensureUv,
77
- ensureVirtualEnvironment: () => ensureVirtualEnvironment,
78
- pipSpawn: () => pipSpawn,
79
- pipSpawnSync: () => pipSpawnSync,
80
- pythonSpawn: () => pythonSpawn,
81
- pythonSpawnSync: () => pythonSpawnSync,
82
75
  resolveProjectRoot: () => resolveProjectRoot,
83
76
  uvSpawn: () => uvSpawn,
84
77
  uvSpawnSync: () => uvSpawnSync
@@ -86,7 +79,6 @@ __export(exports_src, {
86
79
  module.exports = __toCommonJS(exports_src);
87
80
 
88
81
  // src/config.ts
89
- var import_node_crypto = require("node:crypto");
90
82
  var import_node_path2 = __toESM(require("node:path"), 1);
91
83
  var envPaths = __toESM(require("env-paths"), 1);
92
84
  var import_semver = __toESM(require("semver"), 1);
@@ -94,69 +86,13 @@ var import_semver = __toESM(require("semver"), 1);
94
86
  // src/project-root.ts
95
87
  var import_node_fs = __toESM(require("node:fs"), 1);
96
88
  var import_node_path = __toESM(require("node:path"), 1);
97
- var import_pacwich = require("pacwich");
98
- var PACKAGE_MANAGER_LOCKFILES = [
99
- "bun.lock",
100
- "bun.lockb",
101
- "npm-shrinkwrap.json",
102
- "package-lock.json",
103
- "pnpm-lock.yaml",
104
- "yarn.lock"
105
- ];
106
- var PYTHON_PROJECT_MARKERS = [
107
- "pyproject.toml",
108
- "uv.lock"
109
- ];
110
89
  function resolveProjectRoot(startDirectory = process.cwd()) {
111
90
  const start = import_node_path.default.resolve(startDirectory);
112
- const workspaceRoot = findWorkspaceProjectRoot(start);
113
- if (workspaceRoot) {
114
- try {
115
- return import_node_path.default.resolve(import_pacwich.createFileSystemProject({
116
- rootDirectory: start,
117
- disableExecutableConfigs: true
118
- }).rootDirectory);
119
- } catch {
120
- return workspaceRoot;
121
- }
122
- }
123
- return findConventionalProjectRoot(start);
124
- }
125
- function findWorkspaceProjectRoot(startDirectory) {
126
- for (const directory of ancestors(startDirectory)) {
127
- const packageJson = import_node_path.default.join(directory, "package.json");
128
- if (!import_node_fs.default.existsSync(packageJson))
129
- continue;
130
- if (import_node_fs.default.existsSync(import_node_path.default.join(directory, "pnpm-workspace.yaml")))
131
- return directory;
132
- try {
133
- const manifest = JSON.parse(import_node_fs.default.readFileSync(packageJson, "utf8"));
134
- if (hasWorkspaceDeclaration(manifest.workspaces))
135
- return directory;
136
- } catch {}
137
- }
138
- return null;
139
- }
140
- function findConventionalProjectRoot(startDirectory) {
141
- let nearestPackageDirectory = null;
142
- let nearestPythonDirectory = null;
143
- for (const directory of ancestors(startDirectory)) {
144
- const hasPackage = import_node_fs.default.existsSync(import_node_path.default.join(directory, "package.json"));
145
- if (hasPackage && !nearestPackageDirectory)
146
- nearestPackageDirectory = directory;
147
- if (!nearestPythonDirectory && PYTHON_PROJECT_MARKERS.some((file) => import_node_fs.default.existsSync(import_node_path.default.join(directory, file)))) {
148
- nearestPythonDirectory = directory;
149
- }
150
- if (hasPackage && PACKAGE_MANAGER_LOCKFILES.some((file) => import_node_fs.default.existsSync(import_node_path.default.join(directory, file)))) {
151
- return directory;
152
- }
91
+ for (const directory of ancestors(start)) {
153
92
  if (import_node_fs.default.existsSync(import_node_path.default.join(directory, ".git")))
154
93
  return directory;
155
94
  }
156
- return nearestPackageDirectory ?? nearestPythonDirectory ?? startDirectory;
157
- }
158
- function hasWorkspaceDeclaration(value) {
159
- return Array.isArray(value) || value !== null && typeof value === "object";
95
+ return start;
160
96
  }
161
97
  function* ancestors(startDirectory) {
162
98
  let directory = startDirectory;
@@ -171,14 +107,12 @@ function* ancestors(startDirectory) {
171
107
 
172
108
  // src/config.ts
173
109
  var DEFAULT_PYTHON_RANGE = ">=3.10.0 <4.0.0";
174
- var DEFAULT_PIP_RANGE = ">=20.3.0";
175
110
  var DEFAULT_MANAGED_PYTHON = "3.12";
176
111
  var DEFAULT_UV_VERSION = "0.13.6";
177
- var DEFAULT_UV_RANGE = `>=${DEFAULT_UV_VERSION} <1.0.0`;
112
+ var DEFAULT_UV_RANGE = ">=0.10.0 <1.0.0";
178
113
  var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
179
114
  var DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60000;
180
115
  var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60000;
181
- var DEFAULT_CACHE_TTL_MS = 30000;
182
116
  var envPathsFunction = (() => {
183
117
  const imported = envPaths.default;
184
118
  const implementation = typeof imported === "function" ? imported : imported.default;
@@ -189,10 +123,8 @@ var envPathsFunction = (() => {
189
123
  function resolveOptions(options = {}) {
190
124
  const env = { ...options.env ?? process.env };
191
125
  const pythonRange = options.pythonRange ?? env["PYFIG_PYTHON_RANGE"] ?? DEFAULT_PYTHON_RANGE;
192
- const pipRange = options.pipRange ?? env["PYFIG_PIP_RANGE"] ?? DEFAULT_PIP_RANGE;
193
126
  const uvRange = options.uvRange ?? env["PYFIG_UV_RANGE"] ?? DEFAULT_UV_RANGE;
194
127
  assertRange("Python", pythonRange);
195
- assertRange("pip", pipRange);
196
128
  assertRange("uv", uvRange);
197
129
  const managedPython = options.managedPython ?? env["PYFIG_MANAGED_PYTHON"] ?? DEFAULT_MANAGED_PYTHON;
198
130
  const uvVersion = options.uvVersion ?? env["PYFIG_UV_VERSION"] ?? DEFAULT_UV_VERSION;
@@ -202,16 +134,14 @@ function resolveOptions(options = {}) {
202
134
  const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
203
135
  const downloadTimeoutMs = resolveDuration("downloadTimeoutMs", options.downloadTimeoutMs, env["PYFIG_DOWNLOAD_TIMEOUT_MS"], DEFAULT_DOWNLOAD_TIMEOUT_MS);
204
136
  const coordinationTimeoutMs = resolveDuration("coordinationTimeoutMs", options.coordinationTimeoutMs, env["PYFIG_COORDINATION_TIMEOUT_MS"], installTimeoutMs + downloadTimeoutMs + 60000);
205
- const cacheTtlMs = resolveDuration("cacheTtlMs", options.cacheTtlMs, env["PYFIG_CACHE_TTL_MS"], DEFAULT_CACHE_TTL_MS, true);
206
137
  const configuredHome = options.cacheDir ?? env["PYFIG_HOME"];
207
138
  const cacheDir = configuredHome ? import_node_path2.default.resolve(configuredHome) : envPathsFunction("pyfig", { suffix: "" }).cache;
208
139
  const configuredProjectRoot = options.projectRoot ?? env["PYFIG_PROJECT_ROOT"];
209
- const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot();
140
+ const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot(env["INIT_CWD"] ?? process.cwd());
210
141
  const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
211
142
  return {
212
143
  python: options.python ?? env["PYFIG_PYTHON"] ?? "",
213
144
  pythonRange,
214
- pipRange,
215
145
  managedPython,
216
146
  uvVersion,
217
147
  uvRange,
@@ -227,40 +157,16 @@ function resolveOptions(options = {}) {
227
157
  installTimeoutMs,
228
158
  downloadTimeoutMs,
229
159
  coordinationTimeoutMs,
230
- cacheTtlMs,
231
160
  env
232
161
  };
233
162
  }
234
- function configurationFingerprint(options) {
235
- const policy = [
236
- normalizedPath(options.projectRoot),
237
- normalizedPath(options.cacheDir),
238
- options.python,
239
- options.pythonRange,
240
- options.pipRange,
241
- options.managedPython,
242
- options.uvVersion,
243
- options.uvRange,
244
- options.requireEmbedding,
245
- options.requireGil,
246
- options.autoInstall,
247
- options.installPython,
248
- options.installUv,
249
- options.createVirtualEnvironment,
250
- options.probeTimeoutMs,
251
- options.installTimeoutMs,
252
- options.downloadTimeoutMs,
253
- options.coordinationTimeoutMs,
254
- options.cacheTtlMs,
255
- environmentFingerprint(options.env)
256
- ];
257
- return import_node_crypto.createHash("sha256").update(JSON.stringify(policy)).digest("hex");
258
- }
259
163
  function uvEnvironment(options) {
260
164
  return {
261
165
  ...options.env,
262
166
  UV_PYTHON_INSTALL_DIR: import_node_path2.default.join(options.cacheDir, "python"),
263
- UV_CACHE_DIR: import_node_path2.default.join(options.cacheDir, "uv-cache")
167
+ UV_CACHE_DIR: import_node_path2.default.join(options.cacheDir, "uv-cache"),
168
+ UV_PYTHON_PREFERENCE: "system",
169
+ UV_NO_CONFIG: "1"
264
170
  };
265
171
  }
266
172
  function assertRange(tool, range) {
@@ -304,46 +210,176 @@ function parseBoolean(value, fallback) {
304
210
  return false;
305
211
  throw new TypeError(`Invalid boolean value: ${value}`);
306
212
  }
307
- function environmentFingerprint(env) {
308
- const entries = Object.entries(env).filter((entry) => entry[1] !== undefined).map(([key, value]) => [
309
- process.platform === "win32" ? key.toUpperCase() : key,
310
- value
311
- ]).sort(([left], [right]) => left.localeCompare(right));
312
- return import_node_crypto.createHash("sha256").update(JSON.stringify(entries)).digest("hex");
313
- }
314
- function normalizedPath(value) {
315
- const resolved = import_node_path2.default.resolve(value);
316
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
317
- }
213
+ // src/client.ts
214
+ var import_loqkit2 = require("loqkit");
215
+
318
216
  // src/provision.ts
319
- var import_node_crypto3 = require("node:crypto");
217
+ var import_node_crypto = require("node:crypto");
320
218
  var import_node_fs3 = require("node:fs");
321
- var import_promises2 = require("node:fs/promises");
219
+ var import_promises = require("node:fs/promises");
322
220
  var import_node_fs4 = __toESM(require("node:fs"), 1);
323
221
  var import_node_os = __toESM(require("node:os"), 1);
324
- var import_node_path7 = __toESM(require("node:path"), 1);
222
+ var import_node_path5 = __toESM(require("node:path"), 1);
325
223
  var import_node_stream = require("node:stream");
326
- var import_promises3 = require("node:stream/promises");
224
+ var import_promises2 = require("node:stream/promises");
327
225
  var import_extract_zip = __toESM(require("extract-zip"), 1);
328
226
  var tar = __toESM(require("tar"), 1);
329
227
  var import_undici = require("undici");
330
228
 
331
- // src/coordination.ts
332
- var import_node_crypto2 = require("node:crypto");
333
- var import_node_child_process2 = require("node:child_process");
334
- var import_promises = require("node:fs/promises");
335
- var import_node_path4 = __toESM(require("node:path"), 1);
336
- var workerThreads = __toESM(require("node:worker_threads"), 1);
337
- var import_proper_lockfile = __toESM(require("proper-lockfile"), 1);
229
+ // src/provision-lock.ts
230
+ var import_loqkit = require("loqkit");
231
+ async function withProvisioningLock(options) {
232
+ let existing;
233
+ const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
234
+ key: options.key,
235
+ name: "Pyfig provisioning",
236
+ namespace: "pyfig",
237
+ timeout: options.timeoutMs,
238
+ predicate: async () => {
239
+ const value = await options.check();
240
+ if (value === null)
241
+ return true;
242
+ existing = { value };
243
+ return false;
244
+ }
245
+ });
246
+ if (result.context.state === "executed")
247
+ return result.value;
248
+ if (existing)
249
+ return existing.value;
250
+ throw new Error(`Provisioning check for ${options.key} did not produce a resource.`);
251
+ }
252
+
253
+ // src/virtual-environment.ts
254
+ var import_node_fs2 = __toESM(require("node:fs"), 1);
255
+ var import_node_path3 = __toESM(require("node:path"), 1);
256
+ var VIRTUAL_ENV_DIRECTORY_NAME = import_node_path3.default.join("node_modules", ".pyfig", "venv");
257
+ function virtualEnvironmentDirectory(projectRoot) {
258
+ return import_node_path3.default.join(projectRoot, VIRTUAL_ENV_DIRECTORY_NAME);
259
+ }
260
+ function externalVirtualEnvironmentDirectories(projectRoot, env) {
261
+ const candidates = [env["VIRTUAL_ENV"], import_node_path3.default.join(projectRoot, ".venv")].filter((value) => Boolean(value)).map((value) => import_node_path3.default.resolve(value));
262
+ return [...new Map(candidates.map((value) => [canonicalPath(value), value])).values()];
263
+ }
264
+ function virtualEnvironmentPythonExecutable(virtualEnvironment) {
265
+ return import_node_path3.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
266
+ }
267
+ function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
268
+ const activated = { ...env };
269
+ const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
270
+ const existingPath = pathEntry ? activated[pathEntry] : undefined;
271
+ for (const key of Object.keys(activated)) {
272
+ const normalized = key.toUpperCase();
273
+ if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
274
+ delete activated[key];
275
+ }
276
+ }
277
+ activated["PATH"] = [import_node_path3.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path3.default.delimiter);
278
+ activated["VIRTUAL_ENV"] = virtualEnvironment;
279
+ return activated;
280
+ }
281
+ function pathsEqual(left, right) {
282
+ return canonicalPath(left) === canonicalPath(right);
283
+ }
284
+ function canonicalPath(value) {
285
+ const resolved = import_node_path3.default.resolve(value);
286
+ let canonical = resolved;
287
+ try {
288
+ canonical = import_node_fs2.default.realpathSync.native(resolved);
289
+ } catch {}
290
+ return process.platform === "win32" ? canonical.toLowerCase() : canonical;
291
+ }
292
+
293
+ // src/package-target.ts
294
+ var TARGET_OPTIONS = new Set([
295
+ "--active",
296
+ "--config-file",
297
+ "--directory",
298
+ "--isolated",
299
+ "--prefix",
300
+ "--project",
301
+ "--python",
302
+ "--root",
303
+ "--system",
304
+ "--target",
305
+ "--user"
306
+ ]);
307
+ var TARGET_ENVIRONMENT_VARIABLES = new Set([
308
+ "CONDA_PREFIX",
309
+ "PIP_PREFIX",
310
+ "PIP_PYTHON",
311
+ "PIP_ROOT",
312
+ "PIP_TARGET",
313
+ "PIP_USER",
314
+ "PYTHONHOME",
315
+ "PYTHONUSERBASE",
316
+ "UV_CONFIG_FILE",
317
+ "UV_PROJECT",
318
+ "UV_PROJECT_ENVIRONMENT",
319
+ "UV_PYTHON",
320
+ "UV_PREFIX",
321
+ "UV_ROOT",
322
+ "UV_SYSTEM_PYTHON",
323
+ "UV_TARGET"
324
+ ]);
325
+ function uvCommandNeedsEnvironment(args) {
326
+ return args[0] === "pip" || args[0] === "run";
327
+ }
328
+ function targetUvArguments(args, python) {
329
+ if (!python.virtualEnvironment) {
330
+ throw new Error("Targeted uv commands require a virtual environment.");
331
+ }
332
+ assertNoTargetOverride(args);
333
+ if (args[0] === "run") {
334
+ return ["run", "--no-project", "--python", python.executable, ...args.slice(1)];
335
+ }
336
+ if (args[0] === "pip" && args[1]) {
337
+ return ["pip", args[1], "--python", python.executable, ...args.slice(2)];
338
+ }
339
+ return [...args];
340
+ }
341
+ function uvCommandEnvironment(env, python) {
342
+ const sanitized = sanitizeUvEnvironment(env);
343
+ if (!python?.virtualEnvironment)
344
+ return sanitized;
345
+ return {
346
+ ...activateVirtualEnvironment(sanitized, python.virtualEnvironment, python.executable),
347
+ UV_PYTHON: python.executable
348
+ };
349
+ }
350
+ function sanitizeUvEnvironment(env) {
351
+ const sanitized = { ...env };
352
+ for (const key of Object.keys(sanitized)) {
353
+ if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
354
+ delete sanitized[key];
355
+ }
356
+ return sanitized;
357
+ }
358
+ function assertNoTargetOverride(args) {
359
+ for (const argument of args.slice(1)) {
360
+ if (argument === "--")
361
+ return;
362
+ const [name] = argument.split("=", 1);
363
+ if (name && TARGET_OPTIONS.has(name)) {
364
+ throw new Error(`${name} cannot override the environment selected by Pyfig.`);
365
+ }
366
+ if (argument === "-p" || argument.startsWith("-p=") || /^-p[^-]/u.test(argument)) {
367
+ throw new Error("-p cannot override the environment selected by Pyfig.");
368
+ }
369
+ }
370
+ }
371
+
372
+ // src/probes.ts
373
+ var import_semver2 = __toESM(require("semver"), 1);
338
374
 
339
375
  // src/process.ts
340
376
  var import_node_child_process = require("node:child_process");
341
- var import_node_path3 = __toESM(require("node:path"), 1);
377
+ var import_node_path4 = __toESM(require("node:path"), 1);
342
378
  var import_which = __toESM(require("which"), 1);
343
379
  async function resolveExecutable(value, env) {
344
380
  if (!value)
345
381
  return null;
346
- if (import_node_path3.default.isAbsolute(value)) {
382
+ if (import_node_path4.default.isAbsolute(value)) {
347
383
  return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
348
384
  }
349
385
  if (!/^[A-Za-z0-9_.-]+$/u.test(value))
@@ -360,14 +396,15 @@ function run(executable, args, env, timeoutMs) {
360
396
  });
361
397
  if (result.status !== 0 || result.error) {
362
398
  const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
363
- throw new Error(`${import_node_path3.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
399
+ throw new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
364
400
  }
365
401
  return { stdout: result.stdout, stderr: result.stderr };
366
402
  }
367
- async function runAsync(executable, args, env, timeoutMs, signal) {
403
+ async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
368
404
  return new Promise((resolve, reject) => {
369
405
  const child = import_node_child_process.spawn(executable, args, {
370
406
  env,
407
+ cwd,
371
408
  stdio: ["ignore", "pipe", "pipe"],
372
409
  windowsHide: true
373
410
  });
@@ -388,11 +425,11 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
388
425
  const abort = () => {
389
426
  child.kill();
390
427
  const reason = signal?.reason;
391
- finish(reason instanceof Error ? reason : new Error(`${import_node_path3.default.basename(executable)} was aborted.`));
428
+ finish(reason instanceof Error ? reason : new Error(`${import_node_path4.default.basename(executable)} was aborted.`));
392
429
  };
393
430
  const timer = setTimeout(() => {
394
431
  child.kill();
395
- finish(new Error(`${import_node_path3.default.basename(executable)} timed out after ${timeoutMs}ms.`));
432
+ finish(new Error(`${import_node_path4.default.basename(executable)} timed out after ${timeoutMs}ms.`));
396
433
  }, timeoutMs);
397
434
  child.stdout.setEncoding("utf8");
398
435
  child.stderr.setEncoding("utf8");
@@ -400,21 +437,21 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
400
437
  stdout += chunk;
401
438
  if (stdout.length > 4 * 1024 * 1024) {
402
439
  child.kill();
403
- finish(new Error(`${import_node_path3.default.basename(executable)} exceeded the output limit.`));
440
+ finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the output limit.`));
404
441
  }
405
442
  });
406
443
  child.stderr.on("data", (chunk) => {
407
444
  stderr += chunk;
408
445
  if (stderr.length > 4 * 1024 * 1024) {
409
446
  child.kill();
410
- finish(new Error(`${import_node_path3.default.basename(executable)} exceeded the error-output limit.`));
447
+ finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the error-output limit.`));
411
448
  }
412
449
  });
413
450
  child.once("error", (error) => finish(error));
414
451
  child.once("close", (code, childSignal) => {
415
452
  if (code !== 0) {
416
453
  const detail = stderr.trim() || (childSignal ? `signal ${childSignal}` : `exit ${code}`);
417
- finish(new Error(`${import_node_path3.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
454
+ finish(new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
418
455
  return;
419
456
  }
420
457
  finish(undefined, { stdout, stderr });
@@ -426,213 +463,6 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
426
463
  });
427
464
  }
428
465
 
429
- // src/coordination.ts
430
- var realmMutexTails = new Map;
431
- var flockLookups = new Map;
432
- var FLOCK_READY = "PYFIG_FLOCK_READY";
433
- var FLOCK_HOLDER = [
434
- `process.stdout.write('${FLOCK_READY}\\n')`,
435
- "process.stdin.resume()",
436
- 'process.stdin.once("end",()=>process.exit(0))'
437
- ].join(";");
438
- async function withProvisioningLock(options) {
439
- const existing = await options.check();
440
- if (existing !== null)
441
- return existing;
442
- return withResourceLock(options, async (signal) => {
443
- const rechecked = await options.check();
444
- return rechecked ?? options.mutate(signal);
445
- });
446
- }
447
- async function withResourceLock(options, operation) {
448
- await import_promises.mkdir(options.target, { recursive: true });
449
- return withRealmMutex(options.key, () => withWorkerLock(options, () => withNativeFlock(options, () => withPortableLease(options, operation))));
450
- }
451
- async function withRealmMutex(key, operation) {
452
- const previous = realmMutexTails.get(key) ?? Promise.resolve();
453
- let releaseGate;
454
- const gate = new Promise((resolve) => {
455
- releaseGate = resolve;
456
- });
457
- const tail = previous.catch(() => {
458
- return;
459
- }).then(() => gate);
460
- realmMutexTails.set(key, tail);
461
- await previous.catch(() => {
462
- return;
463
- });
464
- try {
465
- return await operation();
466
- } finally {
467
- releaseGate();
468
- if (realmMutexTails.get(key) === tail)
469
- realmMutexTails.delete(key);
470
- }
471
- }
472
- async function withWorkerLock(options, operation) {
473
- const manager = workerThreads.locks;
474
- if (!manager?.request)
475
- return operation();
476
- const controller = new AbortController;
477
- const timeout = setTimeout(() => controller.abort(new Error(`Timed out waiting for worker lock ${options.key}.`)), options.timeoutMs);
478
- let entered = false;
479
- try {
480
- return await manager.request(workerLockName(options.key), { mode: "exclusive", signal: controller.signal }, async () => {
481
- entered = true;
482
- return operation();
483
- });
484
- } catch (error) {
485
- if (entered)
486
- throw error;
487
- return operation();
488
- } finally {
489
- clearTimeout(timeout);
490
- }
491
- }
492
- async function withNativeFlock(options, operation) {
493
- if (process.platform === "win32")
494
- return operation();
495
- const executable = await findFlock(process.env);
496
- if (!executable)
497
- return operation();
498
- const child = await acquireFlock(executable, `${options.target}.flock`, options.timeoutMs);
499
- if (!child)
500
- return operation();
501
- try {
502
- return await operation();
503
- } finally {
504
- await releaseFlock(child);
505
- }
506
- }
507
- async function findFlock(env) {
508
- const lookupKey = `${env["PATH"] ?? ""}\x00${env["PATHEXT"] ?? ""}`;
509
- let lookup = flockLookups.get(lookupKey);
510
- if (!lookup) {
511
- lookup = resolveExecutable("flock", env);
512
- flockLookups.set(lookupKey, lookup);
513
- }
514
- return lookup;
515
- }
516
- async function acquireFlock(executable, lockPath, timeoutMs) {
517
- const waitSeconds = Math.max(1, Math.ceil(Math.min(timeoutMs, 30000) / 1000));
518
- const child = import_node_child_process2.spawn(executable, ["-x", "-w", String(waitSeconds), lockPath, process.execPath, "-e", FLOCK_HOLDER], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
519
- child.stderr.resume();
520
- let output = "";
521
- const acquired = await new Promise((resolve) => {
522
- let settled = false;
523
- const finish = (value) => {
524
- if (settled)
525
- return;
526
- settled = true;
527
- clearTimeout(timer);
528
- resolve(value);
529
- };
530
- const timer = setTimeout(() => finish(false), Math.min(timeoutMs, 30000) + 5000);
531
- child.once("error", () => finish(false));
532
- child.once("exit", () => finish(false));
533
- child.stdout.setEncoding("utf8");
534
- child.stdout.on("data", (chunk) => {
535
- output += chunk;
536
- if (output.includes(FLOCK_READY))
537
- finish(true);
538
- });
539
- });
540
- if (acquired)
541
- return child;
542
- child.kill();
543
- return null;
544
- }
545
- async function releaseFlock(child) {
546
- if (child.exitCode !== null)
547
- return;
548
- child.stdin.end();
549
- await new Promise((resolve) => {
550
- const timer = setTimeout(() => {
551
- child.kill();
552
- resolve();
553
- }, 5000);
554
- child.once("exit", () => {
555
- clearTimeout(timer);
556
- resolve();
557
- });
558
- });
559
- }
560
- async function withPortableLease(options, operation) {
561
- const controller = new AbortController;
562
- let compromised = null;
563
- const retryIntervalMs = 500;
564
- const release = await import_proper_lockfile.default.lock(options.target, {
565
- realpath: true,
566
- stale: 120000,
567
- update: 15000,
568
- retries: {
569
- retries: Math.ceil(options.timeoutMs / retryIntervalMs),
570
- factor: 1,
571
- minTimeout: retryIntervalMs,
572
- maxTimeout: retryIntervalMs
573
- },
574
- onCompromised: (error) => {
575
- compromised = error;
576
- controller.abort(error);
577
- }
578
- });
579
- try {
580
- const result = await operation(controller.signal);
581
- if (compromised)
582
- throw compromised;
583
- return result;
584
- } finally {
585
- controller.abort();
586
- await release();
587
- }
588
- }
589
- function workerLockName(key) {
590
- return `pyfig:${import_node_crypto2.createHash("sha256").update(key).digest("hex")}`;
591
- }
592
- function cacheLockTarget(cacheDirectory, name) {
593
- return import_node_path4.default.join(cacheDirectory, "locks", name);
594
- }
595
-
596
- // src/probes.ts
597
- var import_node_path6 = __toESM(require("node:path"), 1);
598
- var import_semver2 = __toESM(require("semver"), 1);
599
-
600
- // src/virtual-environment.ts
601
- var import_node_fs2 = __toESM(require("node:fs"), 1);
602
- var import_node_path5 = __toESM(require("node:path"), 1);
603
- var VIRTUAL_ENV_DIRECTORY_NAME = ".venv";
604
- function virtualEnvironmentDirectory(projectRoot) {
605
- return import_node_path5.default.join(projectRoot, VIRTUAL_ENV_DIRECTORY_NAME);
606
- }
607
- function virtualEnvironmentPythonExecutable(virtualEnvironment) {
608
- return import_node_path5.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
609
- }
610
- function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
611
- const activated = { ...env };
612
- const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
613
- const existingPath = pathEntry ? activated[pathEntry] : undefined;
614
- for (const key of Object.keys(activated)) {
615
- const normalized = key.toUpperCase();
616
- if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
617
- delete activated[key];
618
- }
619
- }
620
- activated["PATH"] = [import_node_path5.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path5.default.delimiter);
621
- activated["VIRTUAL_ENV"] = virtualEnvironment;
622
- return activated;
623
- }
624
- function pathsEqual(left, right) {
625
- return canonicalPath(left) === canonicalPath(right);
626
- }
627
- function canonicalPath(value) {
628
- const resolved = import_node_path5.default.resolve(value);
629
- let canonical = resolved;
630
- try {
631
- canonical = import_node_fs2.default.realpathSync.native(resolved);
632
- } catch {}
633
- return process.platform === "win32" ? canonical.toLowerCase() : canonical;
634
- }
635
-
636
466
  // src/probes.ts
637
467
  var PYTHON_PROBE = `
638
468
  import json
@@ -702,22 +532,6 @@ print(json.dumps({
702
532
  "embedding": embedding,
703
533
  }))
704
534
  `;
705
- async function findPython(options) {
706
- const candidates = [];
707
- if (options.python)
708
- candidates.push({ executable: options.python, args: [] });
709
- if (process.platform === "win32") {
710
- candidates.push({ executable: "python", args: [] }, { executable: "python3", args: [] }, { executable: "py", args: ["-3"] });
711
- } else {
712
- candidates.push(...["python3.15", "python3.14", "python3.13", "python3.12", "python3.11", "python3.10"].map((executable) => ({ executable, args: [] })), { executable: "python3", args: [] }, { executable: "python", args: [] });
713
- }
714
- for (const candidate of candidates) {
715
- const info = await probePython(candidate, options);
716
- if (info && pythonSatisfiesRequirements(info, options))
717
- return info;
718
- }
719
- return null;
720
- }
721
535
  function pythonSatisfiesRequirements(python, options) {
722
536
  return import_semver2.default.satisfies(python.version, options.pythonRange) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
723
537
  }
@@ -733,50 +547,6 @@ async function probePython(candidate, options) {
733
547
  return null;
734
548
  }
735
549
  }
736
- async function findPip(python, options) {
737
- if (!python.virtualEnvironment) {
738
- throw new Error("pip discovery requires a virtual-environment Python interpreter.");
739
- }
740
- const activeEnv = activateVirtualEnvironment(options.env, python.virtualEnvironment, python.executable);
741
- try {
742
- const { stdout } = run(python.executable, ["-m", "pip", "--version"], activeEnv, options.probeTimeoutMs);
743
- const details = parsePipVersionOutput(stdout);
744
- const version = normalizeVersion(details?.version);
745
- if (!version || !import_semver2.default.satisfies(version, options.pipRange))
746
- return null;
747
- if (!details || !pipBelongsToPython(details, python))
748
- return null;
749
- return {
750
- kind: "pip",
751
- executable: python.executable,
752
- prefixArgs: ["-m", "pip"],
753
- version,
754
- pythonExecutable: python.executable,
755
- virtualEnvironment: python.virtualEnvironment
756
- };
757
- } catch {
758
- return null;
759
- }
760
- }
761
- function parsePipVersionOutput(output) {
762
- const match = output.trim().match(/^pip\s+(\S+)\s+from\s+(.+?)\s+\(python\s+(\d+)\.(\d+)(?:\.\d+)?\)$/iu);
763
- if (!match?.[1] || !match[2] || !match[3] || !match[4])
764
- return null;
765
- return {
766
- version: match[1],
767
- location: match[2],
768
- pythonMajor: Number.parseInt(match[3], 10),
769
- pythonMinor: Number.parseInt(match[4], 10)
770
- };
771
- }
772
- function pipBelongsToPython(pip, python) {
773
- if (pip.pythonMajor !== python.major || pip.pythonMinor !== python.minor)
774
- return false;
775
- const prefix = normalizePathForComparison(import_node_path6.default.resolve(python.prefix));
776
- const location = normalizePathForComparison(import_node_path6.default.resolve(pip.location));
777
- const relative = import_node_path6.default.relative(prefix, location);
778
- return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path6.default.sep}`) && !import_node_path6.default.isAbsolute(relative);
779
- }
780
550
  async function findUv(options) {
781
551
  const configured = options.env["PYFIG_UV"] ?? "uv";
782
552
  const executable = await resolveExecutable(configured, options.env);
@@ -797,9 +567,6 @@ function normalizeVersion(value) {
797
567
  return null;
798
568
  return import_semver2.default.valid(value) ?? import_semver2.default.coerce(value)?.version ?? null;
799
569
  }
800
- function normalizePathForComparison(value) {
801
- return process.platform === "win32" ? value.toLowerCase() : value;
802
- }
803
570
  function pythonArchitectureMatchesRuntime(python) {
804
571
  const architecture = python.architecture.toLowerCase();
805
572
  if (python.pointerBits !== (process.arch === "ia32" ? 32 : 64))
@@ -814,228 +581,223 @@ function pythonArchitectureMatchesRuntime(python) {
814
581
  return architecture === process.arch;
815
582
  }
816
583
 
817
- // src/state.ts
818
- var completed = new Map;
819
- var inFlight = new Map;
820
- var cacheGeneration = 0;
821
- async function memoizeAsync(namespace, key, ttlMs, validate, load) {
822
- const cacheKey = `${namespace}\x00${key}`;
823
- const cached = completed.get(cacheKey);
824
- if (cached && cached.expiresAt >= Date.now()) {
825
- try {
826
- if (await validate(cached.value))
827
- return cached.value;
828
- } catch {}
829
- completed.delete(cacheKey);
830
- }
831
- const pending = inFlight.get(cacheKey);
832
- if (pending)
833
- return pending;
834
- const generation = cacheGeneration;
835
- const loading = load().then((value) => {
836
- if (ttlMs > 0 && generation === cacheGeneration) {
837
- completed.set(cacheKey, { expiresAt: Date.now() + ttlMs, value });
838
- }
839
- return value;
840
- }).finally(() => {
841
- if (inFlight.get(cacheKey) === loading)
842
- inFlight.delete(cacheKey);
843
- });
844
- inFlight.set(cacheKey, loading);
845
- return loading;
846
- }
847
- function memoizeSync(namespace, key, ttlMs, validate, load) {
848
- const cacheKey = `${namespace}\x00${key}`;
849
- const cached = completed.get(cacheKey);
850
- if (cached && cached.expiresAt >= Date.now()) {
851
- try {
852
- if (validate(cached.value))
853
- return cached.value;
854
- } catch {}
855
- completed.delete(cacheKey);
856
- }
857
- const value = load();
858
- if (ttlMs > 0)
859
- completed.set(cacheKey, { expiresAt: Date.now() + ttlMs, value });
860
- return value;
861
- }
862
- function clearPyfigCaches() {
863
- cacheGeneration += 1;
864
- completed.clear();
865
- inFlight.clear();
866
- }
867
-
868
584
  // src/provision.ts
869
- async function ensurePython(options = {}) {
870
- const resolved = resolveOptions(options);
871
- return memoizeAsync("python", configurationFingerprint(resolved), resolved.cacheTtlMs, pythonIsPresent, () => ensurePythonResolved(resolved));
872
- }
873
- async function ensureVirtualEnvironment(options = {}) {
874
- const resolved = resolveOptions(options);
875
- return memoizeAsync("venv", configurationFingerprint(resolved), resolved.cacheTtlMs, (python) => pythonIsPresent(python) && Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(resolved.projectRoot)), () => ensureVirtualEnvironmentResolved(resolved));
876
- }
877
585
  async function ensureUv(options = {}) {
878
- const resolved = resolveOptions(options);
879
- return memoizeAsync("uv", configurationFingerprint(resolved), resolved.cacheTtlMs, (uv) => import_node_fs4.default.existsSync(uv.executable), () => ensureUvResolved(resolved));
586
+ return ensureUvResolved(resolveOptions(options));
880
587
  }
881
- async function ensureEnvironment(options = {}) {
588
+ async function ensureEnvironment(options = {}, requirements = [], dependencyOptions = {}) {
882
589
  const resolved = resolveOptions(options);
883
- return memoizeAsync("environment", configurationFingerprint(resolved), resolved.cacheTtlMs, environmentIsPresent, async () => {
884
- const python = await ensureVirtualEnvironmentResolved(resolved);
885
- const packageManager = await ensurePipResolved(python, resolved);
886
- return { python, packageManager };
887
- });
590
+ const normalized = normalizeRequirements(requirements);
591
+ const selections = normalizeDependencyOptions(dependencyOptions);
592
+ const uv = await ensureUvResolved(resolved);
593
+ const python = await ensureVirtualEnvironmentResolved(resolved, uv);
594
+ if (normalized.length > 0 || selections.extras.length > 0 || selections.groups.length > 0 || selections.allExtras) {
595
+ await installRuntimeRequirements(uv, python, normalized, selections, resolved);
596
+ }
597
+ return {
598
+ python,
599
+ uv,
600
+ managed: isManagedEnvironment(python, resolved)
601
+ };
888
602
  }
889
- async function ensureVirtualEnvironmentResolved(options) {
603
+ async function ensureVirtualEnvironmentResolved(options, uv) {
604
+ const existing = await findReusableEnvironment(options);
605
+ if (existing)
606
+ return existing;
890
607
  const directory = virtualEnvironmentDirectory(options.projectRoot);
891
608
  const check = async () => {
892
- const existing = await inspectVirtualEnvironment(directory, options);
893
- if (!existing)
894
- return null;
895
- if (pythonSatisfiesRequirements(existing, options) && await virtualEnvironmentMatchesConfiguredPython(existing, options)) {
896
- return existing;
897
- }
898
- throw incompatibleVirtualEnvironmentError(directory, existing, options);
609
+ const candidate = await inspectVirtualEnvironment(directory, options);
610
+ return candidate && pythonSatisfiesRequirements(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
899
611
  };
900
- const existing = await check();
901
- if (existing)
902
- return existing;
903
612
  if (!options.createVirtualEnvironment) {
904
- throw new Error(`No virtual environment at ${directory} satisfies the Python requirements; ` + "virtual-environment creation is disabled.");
613
+ throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
905
614
  }
906
- const basePython = await ensurePythonResolved(options);
907
- const preferredUv = await findAvailableUv(options) ?? (options.installUv ? await ensureUvResolved(options) : null);
615
+ const basePython = await ensureBasePythonResolved(uv, options);
908
616
  return withProvisioningLock({
909
617
  key: `venv:${canonicalPath(directory)}`,
910
- target: directory,
911
618
  timeoutMs: options.coordinationTimeoutMs,
912
- env: options.env,
913
619
  check,
914
- mutate: async (signal) => {
915
- const entries = await import_promises2.readdir(directory);
916
- const hasVenvConfiguration = import_node_fs4.default.existsSync(import_node_path7.default.join(directory, "pyvenv.cfg"));
917
- if (entries.length > 0 && !hasVenvConfiguration) {
918
- throw new Error(`Refusing to replace non-virtual-environment contents at ${directory}.`);
919
- }
920
- await createVirtualEnvironment(directory, basePython, preferredUv, options, signal);
921
- const installed = await inspectVirtualEnvironment(directory, options);
922
- if (!installed || !pythonSatisfiesRequirements(installed, options)) {
923
- throw new Error(`Python created ${directory}, but it does not satisfy the requested runtime capabilities.`);
620
+ mutate: async () => {
621
+ await import_promises.rm(directory, { recursive: true, force: true });
622
+ await import_promises.mkdir(import_node_path5.default.dirname(directory), { recursive: true });
623
+ await runAsync(uv.executable, [
624
+ "venv",
625
+ "--no-project",
626
+ "--no-config",
627
+ "--no-python-downloads",
628
+ "--python",
629
+ basePython.executable,
630
+ directory
631
+ ], isolatedUvEnvironment(options), options.installTimeoutMs);
632
+ const installed = await check();
633
+ if (!installed) {
634
+ throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
924
635
  }
925
636
  return installed;
926
637
  }
927
638
  });
928
639
  }
929
- async function createVirtualEnvironment(directory, basePython, preferredUv, options, signal) {
930
- if (preferredUv) {
931
- try {
932
- await createVirtualEnvironmentWithUv(preferredUv, directory, basePython, options, signal);
933
- return;
934
- } catch {}
640
+ async function findReusableEnvironment(options) {
641
+ if (options.python) {
642
+ const configured = await probePython({ executable: options.python, args: [] }, options);
643
+ if (!configured || !pythonSatisfiesRequirements(configured, options)) {
644
+ throw incompatibleConfiguredPython(options);
645
+ }
646
+ if (configured.virtualEnvironment)
647
+ return configured;
935
648
  }
936
- try {
937
- await runAsync(basePython.executable, ["-m", "venv", directory], withoutActiveVirtualEnvironment(options.env), options.installTimeoutMs, signal);
938
- return;
939
- } catch (systemVenvError) {
940
- if (preferredUv || !options.installUv)
941
- throw systemVenvError;
649
+ for (const directory of [
650
+ ...externalVirtualEnvironmentDirectories(options.projectRoot, options.env),
651
+ virtualEnvironmentDirectory(options.projectRoot)
652
+ ]) {
653
+ const info = await inspectVirtualEnvironment(directory, options);
654
+ if (info && pythonSatisfiesRequirements(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
655
+ return info;
656
+ }
942
657
  }
943
- throw new Error("Python could not create a virtual environment and uv installation is disabled.");
944
- }
945
- async function createVirtualEnvironmentWithUv(uv, directory, basePython, options, signal) {
946
- await runAsync(uv.executable, [
947
- "venv",
948
- "--no-project",
949
- "--no-config",
950
- "--no-python-downloads",
951
- "--allow-existing",
952
- "--python",
953
- basePython.executable,
954
- directory
955
- ], uvEnvironment(options), options.installTimeoutMs, signal);
956
- }
957
- async function inspectVirtualEnvironment(directory, options) {
958
- const executable = virtualEnvironmentPythonExecutable(directory);
959
- const info = await probePython({ executable, args: [] }, options);
960
- return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
658
+ return null;
961
659
  }
962
- async function ensurePythonResolved(options) {
963
- const existing = await discoverPython(options);
964
- if (existing)
965
- return existing;
660
+ async function ensureBasePythonResolved(uv, options) {
661
+ if (options.python) {
662
+ const configured = await probePython({ executable: options.python, args: [] }, options);
663
+ if (!configured || !pythonSatisfiesRequirements(configured, options)) {
664
+ throw incompatibleConfiguredPython(options);
665
+ }
666
+ if (!configured.virtualEnvironment)
667
+ return configured;
668
+ const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
669
+ if (base && pythonSatisfiesRequirements(base, options))
670
+ return base;
671
+ throw incompatibleConfiguredPython(options);
672
+ }
673
+ const system = await findPythonWithUv(uv, options, "system");
674
+ if (system)
675
+ return system;
676
+ const cachedManaged = await findPythonWithUv(uv, options, "managed");
677
+ if (cachedManaged)
678
+ return cachedManaged;
966
679
  if (!options.installPython) {
967
- const capability = options.requireEmbedding ? " with compatible CPython development headers, architecture, and a linkable library" : "";
968
- throw new Error(`No Python interpreter satisfies ${options.pythonRange}${capability}; ` + "managed Python installation is disabled.");
680
+ throw new Error(missingPythonMessage(options, "managed Python installation is disabled"));
969
681
  }
970
- const lockTarget = cacheLockTarget(options.cacheDir, "python");
971
682
  return withProvisioningLock({
972
- key: `python:${canonicalPath(lockTarget)}`,
973
- target: lockTarget,
683
+ key: `python:${canonicalPath(import_node_path5.default.join(options.cacheDir, "python"))}`,
974
684
  timeoutMs: options.coordinationTimeoutMs,
975
- env: options.env,
976
- check: () => discoverPython(options),
977
- mutate: async (signal) => {
978
- const uv = await ensureUvResolved(options);
979
- const uvEnv = uvEnvironment(options);
980
- await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], uvEnv, options.installTimeoutMs, signal);
981
- const installed = await findPythonWithUv(uv, options, signal);
685
+ check: () => findPythonWithUv(uv, options, "managed"),
686
+ mutate: async () => {
687
+ await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], isolatedUvEnvironment(options), options.installTimeoutMs);
688
+ const installed = await findPythonWithUv(uv, options, "managed");
982
689
  if (!installed) {
983
- throw new Error(`uv installed Python ${options.managedPython}, but it does not satisfy the requested ` + "runtime capabilities.");
690
+ throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
984
691
  }
985
692
  return installed;
986
693
  }
987
694
  });
988
695
  }
989
- async function discoverPython(options) {
990
- if (options.python) {
991
- const configured = await probePython({ executable: options.python, args: [] }, options);
992
- if (configured && pythonSatisfiesRequirements(configured, options))
993
- return configured;
994
- throw new Error(`Configured Python ${options.python} is unavailable or does not satisfy ${options.pythonRange}.`);
995
- }
996
- const existingUv = await findAvailableUv(options);
997
- if (existingUv) {
998
- const uvPython = await findPythonWithUv(existingUv, options);
999
- if (uvPython)
1000
- return uvPython;
1001
- }
1002
- return findPython({ ...options, python: "" });
1003
- }
1004
- async function findPythonWithUv(uv, options, signal) {
1005
- const requests = [undefined, options.managedPython];
1006
- for (const requestedPython of requests) {
696
+ async function findPythonWithUv(uv, options, source) {
697
+ for (const request of ["", options.managedPython]) {
1007
698
  const args = [
1008
699
  "python",
1009
700
  "find",
1010
701
  "--no-project",
1011
702
  "--no-config",
1012
- "--managed-python",
1013
- "--no-python-downloads"
703
+ "--no-python-downloads",
704
+ source === "system" ? "--system" : "--managed-python"
1014
705
  ];
1015
- if (requestedPython)
1016
- args.push(requestedPython);
706
+ if (request)
707
+ args.push(request);
1017
708
  try {
1018
- const { stdout } = await runAsync(uv.executable, args, uvEnvironment(options), options.probeTimeoutMs, signal);
709
+ const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
1019
710
  const info = await probePython({ executable: stdout.trim(), args: [] }, options);
1020
711
  if (info && pythonSatisfiesRequirements(info, options))
1021
712
  return info;
1022
- } catch (error) {
1023
- if (signal?.aborted)
1024
- throw signal.reason ?? error;
1025
- }
713
+ } catch {}
1026
714
  }
715
+ try {
716
+ const { stdout } = await runAsync(uv.executable, [
717
+ "python",
718
+ "list",
719
+ "--no-config",
720
+ "--no-python-downloads",
721
+ "--only-installed",
722
+ "--output-format",
723
+ "json",
724
+ source === "system" ? "--no-managed-python" : "--managed-python"
725
+ ], explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
726
+ const candidates = installedPythonPaths(stdout);
727
+ for (const executable of candidates) {
728
+ const info = await probePython({ executable, args: [] }, options);
729
+ if (info && !info.virtualEnvironment && pythonSatisfiesRequirements(info, options)) {
730
+ return info;
731
+ }
732
+ }
733
+ } catch {}
1027
734
  return null;
1028
735
  }
1029
- async function ensurePipResolved(python, options) {
1030
- const existingUv = await findAvailableUv(options);
1031
- if (existingUv)
1032
- return uvPackageManager(existingUv, python);
1033
- if (options.installUv)
1034
- return uvPackageManager(await ensureUvResolved(options), python);
1035
- const existingPip = await findPip(python, options);
1036
- if (existingPip)
1037
- return existingPip;
1038
- throw new Error(`The virtual environment's python -m pip does not satisfy ${options.pipRange}; ` + "uv installation is disabled.");
736
+ function installedPythonPaths(output) {
737
+ const parsed = JSON.parse(output);
738
+ if (!Array.isArray(parsed))
739
+ return [];
740
+ const paths = parsed.flatMap((entry) => {
741
+ if (!entry || typeof entry !== "object" || !("path" in entry))
742
+ return [];
743
+ const candidate = entry.path;
744
+ return typeof candidate === "string" && candidate ? [candidate] : [];
745
+ });
746
+ return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
747
+ }
748
+ async function inspectVirtualEnvironment(directory, options) {
749
+ const executable = virtualEnvironmentPythonExecutable(directory);
750
+ const info = await probePython({ executable, args: [] }, options);
751
+ return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
752
+ }
753
+ async function installRuntimeRequirements(uv, python, requirements, dependencyOptions, options) {
754
+ if (!python.virtualEnvironment) {
755
+ throw new Error("Runtime dependency installation requires a virtual environment.");
756
+ }
757
+ await withProvisioningLock({
758
+ key: `dependencies:${canonicalPath(python.virtualEnvironment)}`,
759
+ timeoutMs: options.coordinationTimeoutMs,
760
+ check: async () => null,
761
+ mutate: async () => {
762
+ const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-requirements-"));
763
+ try {
764
+ const additions = import_node_path5.default.join(temporary, "runtime-requirements.txt");
765
+ const resolved = import_node_path5.default.join(temporary, "resolved-requirements.txt");
766
+ const pyproject = import_node_path5.default.join(options.projectRoot, "pyproject.toml");
767
+ const hasPyproject = import_node_fs4.default.existsSync(pyproject);
768
+ if (!hasPyproject && (dependencyOptions.extras.length > 0 || dependencyOptions.groups.length > 0 || dependencyOptions.allExtras)) {
769
+ throw new Error("Runtime extras and groups require an existing pyproject.toml.");
770
+ }
771
+ const sources = hasPyproject ? [pyproject] : [];
772
+ if (requirements.length > 0) {
773
+ await import_promises.writeFile(additions, `${requirements.join(`
774
+ `)}
775
+ `, "utf8");
776
+ sources.push(additions);
777
+ }
778
+ const selectionArgs = [
779
+ ...dependencyOptions.extras.flatMap((extra) => ["--extra", extra]),
780
+ ...dependencyOptions.groups.flatMap((group) => ["--group", group]),
781
+ ...dependencyOptions.allExtras ? ["--all-extras"] : []
782
+ ];
783
+ await runAsync(uv.executable, [
784
+ "pip",
785
+ "compile",
786
+ "--no-config",
787
+ "--python",
788
+ python.executable,
789
+ ...selectionArgs,
790
+ ...sources,
791
+ "--output-file",
792
+ resolved
793
+ ], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
794
+ await runAsync(uv.executable, ["pip", "install", "--no-config", "--python", python.executable, "-r", resolved], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
795
+ } finally {
796
+ await import_promises.rm(temporary, { recursive: true, force: true });
797
+ }
798
+ return true;
799
+ }
800
+ });
1039
801
  }
1040
802
  async function ensureUvResolved(options) {
1041
803
  const existing = await findAvailableUv(options);
@@ -1046,15 +808,12 @@ async function ensureUvResolved(options) {
1046
808
  }
1047
809
  const installDir = uvInstallDirectory(options);
1048
810
  const executable = uvExecutable(options);
1049
- const lockTarget = cacheLockTarget(options.cacheDir, "uv");
1050
811
  return withProvisioningLock({
1051
- key: `uv:${canonicalPath(lockTarget)}`,
1052
- target: lockTarget,
812
+ key: `uv:${canonicalPath(installDir)}`,
1053
813
  timeoutMs: options.coordinationTimeoutMs,
1054
- env: options.env,
1055
814
  check: () => findAvailableUv(options),
1056
- mutate: async (signal) => {
1057
- await installUv(installDir, options, signal);
815
+ mutate: async () => {
816
+ await installUv(installDir, options);
1058
817
  const result = probeUv(executable, options);
1059
818
  if (!result || result.version !== options.uvVersion) {
1060
819
  throw new Error(`Expected managed uv ${options.uvVersion}, but the installed executable did not match.`);
@@ -1064,88 +823,122 @@ async function ensureUvResolved(options) {
1064
823
  });
1065
824
  }
1066
825
  async function findAvailableUv(options) {
1067
- const system = await findUv(options);
1068
- if (system)
1069
- return system;
826
+ const external = await findUv(options);
827
+ if (external)
828
+ return external;
1070
829
  const managed = probeUv(uvExecutable(options), options);
1071
830
  return managed?.version === options.uvVersion ? managed : null;
1072
831
  }
1073
- function uvInstallDirectory(options) {
1074
- return import_node_path7.default.join(options.cacheDir, "tools", "uv", options.uvVersion);
1075
- }
1076
- function uvExecutable(options) {
1077
- return import_node_path7.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
832
+ function normalizeRequirements(requirements) {
833
+ const normalized = requirements.map((requirement) => requirement.trim());
834
+ for (const requirement of normalized) {
835
+ if (!requirement || /[\0\r\n]/u.test(requirement)) {
836
+ throw new TypeError(`Invalid runtime requirement: ${JSON.stringify(requirement)}`);
837
+ }
838
+ }
839
+ return [...new Set(normalized)];
1078
840
  }
1079
- function uvPackageManager(uv, python) {
1080
- if (!python.virtualEnvironment) {
1081
- throw new Error("uv pip commands require a virtual-environment Python interpreter.");
841
+ function normalizeDependencyOptions(options) {
842
+ const extras = normalizeNames("extra", options.extras ?? []);
843
+ const groups = normalizeNames("dependency group", options.groups ?? []);
844
+ if (options.allExtras && extras.length > 0) {
845
+ throw new TypeError("allExtras cannot be combined with explicit extras.");
1082
846
  }
1083
- return {
1084
- kind: "uv",
1085
- executable: uv.executable,
1086
- prefixArgs: ["pip"],
1087
- version: uv.version,
1088
- pythonExecutable: python.executable,
1089
- virtualEnvironment: python.virtualEnvironment
1090
- };
847
+ return { extras, groups, allExtras: options.allExtras ?? false };
1091
848
  }
1092
- function withoutActiveVirtualEnvironment(env) {
1093
- const result = { ...env };
1094
- for (const key of Object.keys(result)) {
1095
- const normalized = key.toUpperCase();
1096
- if (normalized === "CONDA_PREFIX" || normalized === "VIRTUAL_ENV" || normalized === "PYTHONHOME") {
1097
- delete result[key];
849
+ function normalizeNames(kind, names) {
850
+ const normalized = names.map((name) => name.trim());
851
+ for (const name of normalized) {
852
+ if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
853
+ throw new TypeError(`Invalid ${kind}: ${JSON.stringify(name)}`);
1098
854
  }
1099
855
  }
1100
- return result;
856
+ return [...new Set(normalized)];
857
+ }
858
+ function isManagedEnvironment(python, options) {
859
+ return Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(options.projectRoot));
860
+ }
861
+ function isolatedUvEnvironment(options) {
862
+ return sanitizeUvEnvironment(uvEnvironment(options));
863
+ }
864
+ function explicitPythonSourceEnvironment(options) {
865
+ const env = isolatedUvEnvironment(options);
866
+ delete env["UV_PYTHON_PREFERENCE"];
867
+ return env;
868
+ }
869
+ function targetUvEnvironment(options, python) {
870
+ return uvCommandEnvironment(uvEnvironment(options), python);
871
+ }
872
+ function incompatibleConfiguredPython(options) {
873
+ return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `${options.pythonRange}${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
874
+ }
875
+ function missingPythonMessage(options, reason) {
876
+ const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
877
+ return `No Python satisfies ${options.pythonRange}${embedding}; ${reason}.`;
878
+ }
879
+ async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
880
+ if (!options.python)
881
+ return true;
882
+ const configured = await probePython({ executable: options.python, args: [] }, options);
883
+ if (!configured || !pythonSatisfiesRequirements(configured, options))
884
+ return false;
885
+ if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
886
+ return true;
887
+ }
888
+ const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
889
+ return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
890
+ }
891
+ function uvInstallDirectory(options) {
892
+ return import_node_path5.default.join(options.cacheDir, "tools", "uv", options.uvVersion);
1101
893
  }
1102
- async function installUv(installDir, options, signal) {
894
+ function uvExecutable(options) {
895
+ return import_node_path5.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
896
+ }
897
+ async function installUv(installDir, options) {
1103
898
  const asset = uvAsset();
1104
899
  const baseUrl = `https://github.com/astral-sh/uv/releases/download/${options.uvVersion}`;
1105
- const temporary = await import_promises2.mkdtemp(import_node_path7.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
900
+ const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
1106
901
  const dispatcher = proxyDispatcher(options.env);
1107
902
  try {
1108
- const archive = import_node_path7.default.join(temporary, asset);
1109
- await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, signal, dispatcher);
1110
- const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, signal, dispatcher)).trim().split(/\s+/u)[0];
903
+ const archive = import_node_path5.default.join(temporary, asset);
904
+ await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
905
+ const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
1111
906
  const actual = await sha256File(archive);
1112
907
  if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
1113
908
  throw new Error(`uv archive checksum mismatch for ${asset}`);
1114
909
  }
1115
- const extracted = import_node_path7.default.join(temporary, "extracted");
1116
- await import_promises2.mkdir(extracted, { recursive: true });
1117
- signal.throwIfAborted();
910
+ const extracted = import_node_path5.default.join(temporary, "extracted");
911
+ await import_promises.mkdir(extracted, { recursive: true });
1118
912
  if (asset.endsWith(".zip"))
1119
913
  await import_extract_zip.default(archive, { dir: extracted });
1120
914
  else
1121
915
  await tar.x({ file: archive, cwd: extracted, strict: true });
1122
- signal.throwIfAborted();
1123
916
  const source = await findFile(extracted, process.platform === "win32" ? "uv.exe" : "uv");
1124
917
  if (!source)
1125
918
  throw new Error(`uv archive did not contain the expected executable: ${asset}`);
1126
919
  await publishUvAtomically(source, installDir, options);
1127
920
  } finally {
1128
921
  await dispatcher?.close();
1129
- await import_promises2.rm(temporary, { recursive: true, force: true });
922
+ await import_promises.rm(temporary, { recursive: true, force: true });
1130
923
  }
1131
924
  }
1132
925
  async function publishUvAtomically(source, installDir, options) {
1133
- await import_promises2.mkdir(installDir, { recursive: true });
926
+ await import_promises.mkdir(installDir, { recursive: true });
1134
927
  const executable = uvExecutable(options);
1135
- const staged = import_node_path7.default.join(installDir, `.${import_node_path7.default.basename(executable)}.${process.pid}.${import_node_crypto3.randomUUID()}.tmp`);
928
+ const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
1136
929
  try {
1137
- await import_promises2.copyFile(source, staged);
930
+ await import_promises.copyFile(source, staged);
1138
931
  if (process.platform !== "win32")
1139
- await import_promises2.chmod(staged, 493);
932
+ await import_promises.chmod(staged, 493);
1140
933
  const stagedUv = probeUv(staged, options);
1141
934
  if (!stagedUv || stagedUv.version !== options.uvVersion) {
1142
935
  throw new Error(`Downloaded uv executable is not version ${options.uvVersion}.`);
1143
936
  }
1144
937
  if (process.platform === "win32")
1145
- await import_promises2.rm(executable, { force: true });
1146
- await import_promises2.rename(staged, executable);
938
+ await import_promises.rm(executable, { force: true });
939
+ await import_promises.rename(staged, executable);
1147
940
  } finally {
1148
- await import_promises2.rm(staged, { force: true });
941
+ await import_promises.rm(staged, { force: true });
1149
942
  }
1150
943
  }
1151
944
  function uvAsset() {
@@ -1160,23 +953,23 @@ function uvAsset() {
1160
953
  return `uv-${architecture}-unknown-linux-musl.tar.gz`;
1161
954
  throw new Error(`uv bootstrap does not support platform ${process.platform}.`);
1162
955
  }
1163
- async function download(url, destination, timeoutMs, externalSignal, dispatcher) {
956
+ async function download(url, destination, timeoutMs, dispatcher) {
1164
957
  const response = await import_undici.fetch(url, {
1165
958
  dispatcher,
1166
959
  redirect: "follow",
1167
- signal: combinedSignal(timeoutMs, externalSignal)
960
+ signal: AbortSignal.timeout(timeoutMs)
1168
961
  });
1169
962
  if (!response.ok || !response.body) {
1170
963
  await response.body?.cancel();
1171
964
  throw new Error(`Could not download ${url}: HTTP ${response.status}`);
1172
965
  }
1173
- await import_promises3.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
966
+ await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
1174
967
  }
1175
- async function downloadText(url, timeoutMs, externalSignal, dispatcher) {
968
+ async function downloadText(url, timeoutMs, dispatcher) {
1176
969
  const response = await import_undici.fetch(url, {
1177
970
  dispatcher,
1178
971
  redirect: "follow",
1179
- signal: combinedSignal(timeoutMs, externalSignal)
972
+ signal: AbortSignal.timeout(timeoutMs)
1180
973
  });
1181
974
  if (!response.ok) {
1182
975
  await response.body?.cancel();
@@ -1198,18 +991,15 @@ function proxyDispatcher(env) {
1198
991
  function environmentValue(env, lowercase, uppercase) {
1199
992
  return env[lowercase] ?? env[uppercase];
1200
993
  }
1201
- function combinedSignal(timeoutMs, externalSignal) {
1202
- return AbortSignal.any([externalSignal, AbortSignal.timeout(timeoutMs)]);
1203
- }
1204
994
  async function sha256File(file) {
1205
- const hash = import_node_crypto3.createHash("sha256");
995
+ const hash = import_node_crypto.createHash("sha256");
1206
996
  for await (const chunk of import_node_fs3.createReadStream(file))
1207
997
  hash.update(chunk);
1208
998
  return hash.digest("hex");
1209
999
  }
1210
1000
  async function findFile(directory, name) {
1211
- for (const entry of await import_promises2.readdir(directory, { withFileTypes: true })) {
1212
- const target = import_node_path7.default.join(directory, entry.name);
1001
+ for (const entry of await import_promises.readdir(directory, { withFileTypes: true })) {
1002
+ const target = import_node_path5.default.join(directory, entry.name);
1213
1003
  if (entry.isFile() && entry.name === name)
1214
1004
  return target;
1215
1005
  if (entry.isDirectory()) {
@@ -1220,178 +1010,27 @@ async function findFile(directory, name) {
1220
1010
  }
1221
1011
  return null;
1222
1012
  }
1223
- function incompatibleVirtualEnvironmentError(directory, python, options) {
1224
- const capabilities = options.requireEmbedding ? " or lacks compatible CPython embedding headers, library, or architecture" : "";
1225
- return new Error(`The existing virtual environment at ${directory} uses Python ${python.version}, ` + `which does not satisfy ${options.pythonRange}${capabilities}.`);
1226
- }
1227
- async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
1228
- if (!options.python)
1229
- return true;
1230
- const configured = await probePython({ executable: options.python, args: [] }, options);
1231
- if (!configured || !pythonSatisfiesRequirements(configured, options))
1232
- return false;
1233
- if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
1234
- return true;
1235
- }
1236
- const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
1237
- return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
1238
- }
1239
- function pythonIsPresent(python) {
1240
- return import_node_fs4.default.existsSync(python.executable);
1241
- }
1242
- function environmentIsPresent(environment) {
1243
- return pythonIsPresent(environment.python) && import_node_fs4.default.existsSync(environment.packageManager.executable);
1244
- }
1245
1013
 
1246
1014
  // src/runners.ts
1247
- var import_node_child_process3 = require("node:child_process");
1248
- var import_node_fs6 = __toESM(require("node:fs"), 1);
1249
-
1250
- // src/package-target.ts
1251
- var import_node_os2 = __toESM(require("node:os"), 1);
1252
- var TARGET_CHANGING_OPTIONS = new Set([
1253
- "--config-file",
1254
- "--prefix",
1255
- "--python",
1256
- "--root",
1257
- "--system",
1258
- "--target",
1259
- "--user"
1260
- ]);
1261
- var TARGET_CHANGING_SHORT_OPTIONS = new Set(["-p", "-t"]);
1262
- var TARGET_ENVIRONMENT_VARIABLES = new Set([
1263
- "CONDA_PREFIX",
1264
- "PIP_CONFIG_FILE",
1265
- "PIP_PREFIX",
1266
- "PIP_PYTHON",
1267
- "PIP_ROOT",
1268
- "PIP_TARGET",
1269
- "PIP_USER",
1270
- "PYTHONHOME",
1271
- "PYTHONUSERBASE",
1272
- "UV_CONFIG_FILE",
1273
- "UV_PROJECT_ENVIRONMENT",
1274
- "UV_PREFIX",
1275
- "UV_PYTHON",
1276
- "UV_ROOT",
1277
- "UV_SYSTEM_PYTHON",
1278
- "UV_TARGET"
1279
- ]);
1280
- function assertSafePackageArguments(args) {
1281
- for (const argument of args) {
1282
- if (argument === "--")
1283
- break;
1284
- const [name] = argument.split("=", 1);
1285
- if (name && TARGET_CHANGING_OPTIONS.has(name)) {
1286
- throw new Error(`${name} is not allowed because Pyfig package commands are pinned to the repository .venv.`);
1287
- }
1288
- const shortName = argument.slice(0, 2);
1289
- if (TARGET_CHANGING_SHORT_OPTIONS.has(shortName)) {
1290
- throw new Error(`${shortName} is not allowed because Pyfig package commands are pinned to the repository .venv.`);
1291
- }
1292
- }
1293
- }
1294
- function packageManagerEnvironment(env, packageManager) {
1295
- const active = sanitizedTargetEnvironment(env);
1296
- const activated = activateVirtualEnvironment(active, packageManager.virtualEnvironment, packageManager.pythonExecutable);
1297
- return packageManager.kind === "uv" ? {
1298
- ...activated,
1299
- UV_NO_CONFIG: "1",
1300
- UV_PYTHON: packageManager.pythonExecutable
1301
- } : {
1302
- ...activated,
1303
- PIP_CONFIG_FILE: import_node_os2.default.devNull,
1304
- PIP_REQUIRE_VIRTUALENV: "1",
1305
- PYTHONNOUSERSITE: "1"
1306
- };
1307
- }
1308
- function uvPipEnvironment(env, python) {
1309
- if (!python.virtualEnvironment) {
1310
- throw new Error("uv pip commands require the repository virtual environment.");
1311
- }
1312
- return {
1313
- ...activateVirtualEnvironment(sanitizedTargetEnvironment(env), python.virtualEnvironment, python.executable),
1314
- UV_NO_CONFIG: "1",
1315
- UV_PYTHON: python.executable
1316
- };
1317
- }
1318
- function assertMutationUsesUv(packageManager, args) {
1319
- if (packageManager.kind === "pip" && isPackageMutation(args)) {
1320
- throw new Error("Package mutation requires uv so the repository environment is locked across processes. " + "Enable uv installation or use a read-only pip command.");
1321
- }
1322
- }
1323
- function isPackageMutation(args) {
1324
- return args.some((argument) => argument === "install" || argument === "sync" || argument === "uninstall");
1325
- }
1326
- function isUvPipCommand(args) {
1327
- const separator = args.indexOf("--");
1328
- const commandArguments = separator === -1 ? args : args.slice(0, separator);
1329
- return commandArguments.includes("pip");
1330
- }
1331
- function sanitizedTargetEnvironment(env) {
1332
- const sanitized = { ...env };
1333
- for (const key of Object.keys(sanitized)) {
1334
- if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
1335
- delete sanitized[key];
1336
- }
1337
- return sanitized;
1338
- }
1015
+ var import_node_child_process2 = require("node:child_process");
1339
1016
 
1340
1017
  // src/runtime-files.ts
1341
1018
  var import_node_fs5 = __toESM(require("node:fs"), 1);
1342
- var import_node_path8 = __toESM(require("node:path"), 1);
1343
- var import_node_url = require("node:url");
1344
- var __dirname = "/home/runner/work/pyfig/pyfig/src";
1019
+ var import_node_path6 = __toESM(require("node:path"), 1);
1345
1020
  function runtimeFile(name) {
1346
- const moduleDirectory = typeof __dirname === "string" ? __dirname : import_node_path8.default.dirname(import_node_url.fileURLToPath("file:///home/runner/work/pyfig/pyfig/src/runtime-files.ts"));
1347
- const compiled = import_node_path8.default.join(moduleDirectory, `${name}.js`);
1348
- return import_node_fs5.default.existsSync(compiled) ? compiled : import_node_path8.default.join(moduleDirectory, `${name}.ts`);
1021
+ const moduleDirectory = __dirname;
1022
+ const compiled = import_node_path6.default.join(moduleDirectory, `${name}.js`);
1023
+ return import_node_fs5.default.existsSync(compiled) ? compiled : import_node_path6.default.join(moduleDirectory, `${name}.ts`);
1349
1024
  }
1350
1025
 
1351
1026
  // src/runners.ts
1352
- async function pythonSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
1353
- const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1354
- const python = await ensureVirtualEnvironment(options);
1355
- const command = buildPythonCommand(python, args, resolveOptions(options).env);
1356
- return import_node_child_process3.spawn(command.executable, command.args, {
1357
- ...spawnOptions,
1358
- env: command.env
1359
- });
1360
- }
1361
- function pythonSpawnSync(args = [], spawnOptions = {}, pyfigOptions = {}) {
1362
- const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1363
- const python = resolveSynchronously("venv", options);
1364
- const command = buildPythonCommand(python, args, resolveOptions(options).env);
1365
- return import_node_child_process3.spawnSync(command.executable, command.args, {
1366
- ...spawnOptions,
1367
- env: command.env
1368
- });
1369
- }
1370
- async function pipSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
1371
- const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1372
- const environment = await ensureEnvironment(options);
1373
- const command = buildPipCommand(environment.packageManager, args, resolveOptions(options).env);
1374
- return import_node_child_process3.spawn(command.executable, command.args, {
1375
- ...spawnOptions,
1376
- env: command.env
1377
- });
1378
- }
1379
- function pipSpawnSync(args = [], spawnOptions = {}, pyfigOptions = {}) {
1380
- const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1381
- const environment = resolveSynchronously("environment", options);
1382
- const command = buildPipCommand(environment.packageManager, args, resolveOptions(options).env);
1383
- return import_node_child_process3.spawnSync(command.executable, command.args, {
1384
- ...spawnOptions,
1385
- env: command.env
1386
- });
1387
- }
1388
1027
  async function uvSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
1389
1028
  const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1390
1029
  const resolved = resolveOptions(options);
1391
- const uv = await ensureUv(resolved);
1392
- const python = isUvPipCommand(args) ? await ensureVirtualEnvironment(resolved) : undefined;
1393
- const command = buildUvCommand(uv, args, resolved, python);
1394
- return import_node_child_process3.spawn(command.executable, command.args, {
1030
+ const environment = uvCommandNeedsEnvironment(args) ? await ensureEnvironment(resolved) : undefined;
1031
+ const uv = environment?.uv ?? await ensureUv(resolved);
1032
+ const command = buildUvCommand(uv, args, resolved, environment?.python);
1033
+ return import_node_child_process2.spawn(command.executable, command.args, {
1395
1034
  ...spawnOptions,
1396
1035
  env: command.env
1397
1036
  });
@@ -1399,45 +1038,19 @@ async function uvSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
1399
1038
  function uvSpawnSync(args = [], spawnOptions = {}, pyfigOptions = {}) {
1400
1039
  const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1401
1040
  const resolved = resolveOptions(options);
1402
- const resolution = isUvPipCommand(args) ? resolveSynchronously("uv-pip", resolved) : { uv: resolveSynchronously("uv", resolved), python: undefined };
1403
- const command = buildUvCommand(resolution.uv, args, resolved, resolution.python);
1404
- return import_node_child_process3.spawnSync(command.executable, command.args, {
1041
+ const environment = uvCommandNeedsEnvironment(args) ? resolveSynchronously("environment", resolved) : undefined;
1042
+ const uv = environment?.uv ?? resolveSynchronously("uv", resolved);
1043
+ const command = buildUvCommand(uv, args, resolved, environment?.python);
1044
+ return import_node_child_process2.spawnSync(command.executable, command.args, {
1405
1045
  ...spawnOptions,
1406
1046
  env: command.env
1407
1047
  });
1408
1048
  }
1409
- function buildPythonCommand(python, args, env) {
1410
- const commandEnv = python.virtualEnvironment ? activateVirtualEnvironment(env, python.virtualEnvironment, python.executable) : env;
1411
- return {
1412
- executable: python.executable,
1413
- args: [...args],
1414
- env: commandEnv
1415
- };
1416
- }
1417
- function buildPipCommand(packageManager, args, env) {
1418
- if (!packageManager.virtualEnvironment) {
1419
- throw new Error("pip commands require an active virtual environment.");
1420
- }
1421
- assertSafePackageArguments(args);
1422
- assertMutationUsesUv(packageManager, args);
1423
- return {
1424
- executable: packageManager.executable,
1425
- args: [...packageManager.prefixArgs, ...args],
1426
- env: packageManagerEnvironment(env, packageManager)
1427
- };
1428
- }
1429
1049
  function buildUvCommand(uv, args, options, python) {
1430
- const isPip = isUvPipCommand(args);
1431
- if (isPip && !python?.virtualEnvironment) {
1432
- throw new Error("uv pip commands require the repo virtual-environment Python interpreter.");
1433
- }
1434
- if (isPip)
1435
- assertSafePackageArguments(args);
1436
- const env = isPip && python ? uvPipEnvironment(uvEnvironment(options), python) : uvEnvironment(options);
1437
1050
  return {
1438
1051
  executable: uv.executable,
1439
- args: [...args],
1440
- env
1052
+ args: python ? targetUvArguments(args, python) : [...args],
1053
+ env: uvCommandEnvironment(uvEnvironment(options), python)
1441
1054
  };
1442
1055
  }
1443
1056
  function optionsForInvocation(pyfigOptions, spawnEnv) {
@@ -1445,41 +1058,29 @@ function optionsForInvocation(pyfigOptions, spawnEnv) {
1445
1058
  }
1446
1059
  function resolveSynchronously(target, options) {
1447
1060
  const resolved = resolveOptions(options);
1448
- const cacheKey = `${target}:${configurationFingerprint(resolved)}`;
1449
- return memoizeSync("sync-resolution", cacheKey, resolved.cacheTtlMs, synchronousResolutionIsPresent, () => {
1450
- const request = { target, options: resolved };
1451
- const result = import_node_child_process3.spawnSync(process.execPath, [runtimeFile("sync-resolver")], {
1452
- input: JSON.stringify(request),
1453
- encoding: "utf8",
1454
- env: resolved.env,
1455
- timeout: synchronousResolutionTimeout(resolved),
1456
- maxBuffer: 4 * 1024 * 1024,
1457
- windowsHide: true
1458
- });
1459
- if (result.status !== 0 || result.error) {
1460
- const detail = result.stderr.trim() || result.error?.message || `exit ${result.status}`;
1461
- throw new Error(`Could not resolve ${target} synchronously: ${detail}`);
1462
- }
1463
- try {
1464
- return JSON.parse(result.stdout);
1465
- } catch (error) {
1466
- const detail = error instanceof Error ? error.message : String(error);
1467
- throw new Error(`Could not parse synchronous ${target} resolution: ${detail}`);
1468
- }
1061
+ const request = { target, options: resolved };
1062
+ const result = import_node_child_process2.spawnSync(process.execPath, [runtimeFile("sync-resolver")], {
1063
+ input: JSON.stringify(request),
1064
+ encoding: "utf8",
1065
+ env: resolved.env,
1066
+ timeout: synchronousResolutionTimeout(resolved),
1067
+ maxBuffer: 4 * 1024 * 1024,
1068
+ windowsHide: true
1469
1069
  });
1070
+ if (result.status !== 0 || result.error) {
1071
+ const detail = result.stderr.trim() || result.error?.message || `exit ${result.status}`;
1072
+ throw new Error(`Could not resolve ${target} synchronously: ${detail}`);
1073
+ }
1074
+ try {
1075
+ return JSON.parse(result.stdout);
1076
+ } catch (error) {
1077
+ const detail = error instanceof Error ? error.message : String(error);
1078
+ throw new Error(`Could not parse synchronous ${target} resolution: ${detail}`);
1079
+ }
1470
1080
  }
1471
1081
  function synchronousResolutionTimeout(options) {
1472
1082
  return options.coordinationTimeoutMs + options.downloadTimeoutMs + 2 * options.installTimeoutMs + 4 * options.probeTimeoutMs + 60000;
1473
1083
  }
1474
- function synchronousResolutionIsPresent(resolution) {
1475
- if ("packageManager" in resolution) {
1476
- return import_node_fs6.default.existsSync(resolution.python.executable) && import_node_fs6.default.existsSync(resolution.packageManager.executable);
1477
- }
1478
- if ("uv" in resolution) {
1479
- return import_node_fs6.default.existsSync(resolution.uv.executable) && import_node_fs6.default.existsSync(resolution.python.executable);
1480
- }
1481
- return import_node_fs6.default.existsSync(resolution.executable);
1482
- }
1483
1084
 
1484
1085
  // src/client.ts
1485
1086
  function createPyfig(options = {}) {
@@ -1487,18 +1088,15 @@ function createPyfig(options = {}) {
1487
1088
  ...options,
1488
1089
  env: options.env ? { ...options.env } : undefined
1489
1090
  };
1091
+ const baseEnvironment = import_loqkit2.memoize(() => ensureEnvironment(scopedOptions));
1092
+ const uvInfo = import_loqkit2.memoize(() => ensureUv(scopedOptions));
1490
1093
  return {
1491
- prepare: () => ensureEnvironment(scopedOptions),
1492
- pythonInfo: () => ensureVirtualEnvironment(scopedOptions),
1493
- uvInfo: () => ensureUv(scopedOptions),
1494
- python: (args = [], spawnOptions = {}) => pythonSpawn(args, spawnOptions, scopedOptions),
1495
- pythonSync: (args = [], spawnOptions = {}) => pythonSpawnSync(args, spawnOptions, scopedOptions),
1496
- pip: (args = [], spawnOptions = {}) => pipSpawn(args, spawnOptions, scopedOptions),
1497
- pipSync: (args = [], spawnOptions = {}) => pipSpawnSync(args, spawnOptions, scopedOptions),
1094
+ prepare: (requirements = [], dependencyOptions = {}) => requirements.length === 0 && !dependencyOptions.allExtras && (dependencyOptions.extras?.length ?? 0) === 0 && (dependencyOptions.groups?.length ?? 0) === 0 ? baseEnvironment() : ensureEnvironment(scopedOptions, requirements, dependencyOptions),
1095
+ pythonInfo: async () => (await baseEnvironment()).python,
1096
+ uvInfo,
1498
1097
  uv: (args = [], spawnOptions = {}) => uvSpawn(args, spawnOptions, scopedOptions),
1499
- uvSync: (args = [], spawnOptions = {}) => uvSpawnSync(args, spawnOptions, scopedOptions),
1500
- clearCache: clearPyfigCaches
1098
+ uvSync: (args = [], spawnOptions = {}) => uvSpawnSync(args, spawnOptions, scopedOptions)
1501
1099
  };
1502
1100
  }
1503
1101
 
1504
- //# debugId=B04CEBB9C6624F8464756E2164756E21
1102
+ //# debugId=B84AA2037417535D64756E2164756E21