pyfig 0.6.3 → 0.7.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 (44) hide show
  1. package/README.md +46 -33
  2. package/SECURITY.md +1 -1
  3. package/dist/cjs/index.js +396 -256
  4. package/dist/cjs/index.js.map +10 -9
  5. package/dist/cjs/sync-resolver.js +356 -231
  6. package/dist/cjs/sync-resolver.js.map +9 -8
  7. package/dist/cli.js +12 -3
  8. package/dist/cli.js.map +1 -1
  9. package/dist/client.d.ts +6 -6
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +31 -21
  12. package/dist/client.js.map +1 -1
  13. package/dist/config.d.ts +9 -2
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +48 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/index.d.ts +3 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/package-target.d.ts +3 -3
  22. package/dist/package-target.d.ts.map +1 -1
  23. package/dist/package-target.js +6 -6
  24. package/dist/package-target.js.map +1 -1
  25. package/dist/provision.d.ts +6 -12
  26. package/dist/provision.d.ts.map +1 -1
  27. package/dist/provision.js +141 -153
  28. package/dist/provision.js.map +1 -1
  29. package/dist/python-venv.d.ts +12 -0
  30. package/dist/python-venv.d.ts.map +1 -0
  31. package/dist/{virtual-environment.js → python-venv.js} +12 -9
  32. package/dist/python-venv.js.map +1 -0
  33. package/dist/runners.js +12 -12
  34. package/dist/runners.js.map +1 -1
  35. package/dist/sync-resolver.js +2 -2
  36. package/dist/sync-resolver.js.map +1 -1
  37. package/dist/venv-identity.d.ts +36 -0
  38. package/dist/venv-identity.d.ts.map +1 -0
  39. package/dist/venv-identity.js +80 -0
  40. package/dist/venv-identity.js.map +1 -0
  41. package/package.json +2 -2
  42. package/dist/virtual-environment.d.ts +0 -11
  43. package/dist/virtual-environment.d.ts.map +0 -1
  44. package/dist/virtual-environment.js.map +0 -1
package/dist/cjs/index.js CHANGED
@@ -68,8 +68,8 @@ __export(exports_src, {
68
68
  DEFAULT_MANAGED_PYTHON: () => DEFAULT_MANAGED_PYTHON,
69
69
  DEFAULT_REQUIRES_PYTHON: () => DEFAULT_REQUIRES_PYTHON,
70
70
  DEFAULT_REQUIRES_UV: () => DEFAULT_REQUIRES_UV,
71
- createPyfig: () => createPyfig,
72
- ensureEnvironment: () => ensureEnvironment,
71
+ Python: () => Python,
72
+ ensurePython: () => ensurePython,
73
73
  ensureUv: () => ensureUv,
74
74
  resolveProjectRoot: () => resolveProjectRoot,
75
75
  uvSpawn: () => uvSpawn,
@@ -149,7 +149,11 @@ function resolveOptions(options = {}) {
149
149
  const configuredProjectRoot = options.projectRoot ?? env["PYFIG_PROJECT_ROOT"];
150
150
  const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot(env["INIT_CWD"] ?? process.cwd());
151
151
  const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
152
+ const dependencies = normalizeDependencies(options.dependencies ?? []);
153
+ const requirements = normalizeRequirements(options.requirements ?? [], projectRoot);
152
154
  return {
155
+ dependencies,
156
+ requirements,
153
157
  python: options.python ?? env["PYFIG_PYTHON"] ?? "",
154
158
  requiresPython,
155
159
  managedPython,
@@ -169,7 +173,10 @@ function resolveOptions(options = {}) {
169
173
  env
170
174
  };
171
175
  }
172
- function uvEnvironment(options) {
176
+ function hasRuntimeRequirements(options) {
177
+ return options.dependencies.length > 0 || options.requirements.length > 0;
178
+ }
179
+ function uvProcessEnvironment(options) {
173
180
  return {
174
181
  ...options.env,
175
182
  UV_PYTHON_INSTALL_DIR: import_node_path2.default.join(options.cacheDir, "python"),
@@ -211,61 +218,82 @@ function parseBoolean(value, fallback) {
211
218
  return false;
212
219
  throw new TypeError(`Invalid boolean value: ${value}`);
213
220
  }
221
+ function normalizeDependencies(dependencies) {
222
+ const normalized = dependencies.map((dependency) => dependency.trim());
223
+ for (const dependency of normalized) {
224
+ if (!dependency || /[\0\r\n]/u.test(dependency)) {
225
+ throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
226
+ }
227
+ }
228
+ return normalized;
229
+ }
230
+ function normalizeRequirements(requirements, projectRoot) {
231
+ const normalized = [];
232
+ for (const requirement of requirements) {
233
+ const candidate = requirement.trim();
234
+ if (!candidate || /[\0\r\n]/u.test(candidate)) {
235
+ throw new TypeError(`Invalid requirements file path: ${JSON.stringify(candidate)}`);
236
+ }
237
+ const absolute = import_node_path2.default.resolve(projectRoot, candidate);
238
+ const basename = import_node_path2.default.basename(absolute).toLowerCase();
239
+ if (isPythonProjectFile(basename)) {
240
+ throw new TypeError(`Expected a requirements file, not Python project metadata: ${absolute}`);
241
+ }
242
+ normalized.push(absolute);
243
+ }
244
+ return normalized;
245
+ }
246
+ var PYTHON_PROJECT_FILES = new Set([
247
+ "pdm.lock",
248
+ "pipfile",
249
+ "pipfile.lock",
250
+ "poetry.lock",
251
+ "pyproject.toml",
252
+ "setup.cfg",
253
+ "setup.py",
254
+ "uv.lock"
255
+ ]);
256
+ function isPythonProjectFile(basename) {
257
+ return PYTHON_PROJECT_FILES.has(basename) || /^pylock(?:\.[^.]+)?\.toml$/u.test(basename);
258
+ }
214
259
  // src/client.ts
215
260
  var import_loqkit2 = require("loqkit");
216
261
 
217
262
  // src/provision.ts
218
- var import_node_crypto = require("node:crypto");
263
+ var import_node_crypto2 = require("node:crypto");
219
264
  var import_node_fs3 = require("node:fs");
220
- var import_promises = require("node:fs/promises");
221
- var import_node_fs4 = __toESM(require("node:fs"), 1);
265
+ var import_promises2 = require("node:fs/promises");
222
266
  var import_node_os = __toESM(require("node:os"), 1);
223
- var import_node_path5 = __toESM(require("node:path"), 1);
267
+ var import_node_path6 = __toESM(require("node:path"), 1);
224
268
  var import_node_stream = require("node:stream");
225
- var import_promises2 = require("node:stream/promises");
269
+ var import_promises3 = require("node:stream/promises");
226
270
  var import_fflate = require("fflate");
227
271
  var tar = __toESM(require("tar"), 1);
228
272
  var import_undici = require("undici");
229
273
 
230
- // src/provision-lock.ts
231
- var import_loqkit = require("loqkit");
232
- async function withProvisioningLock(options) {
233
- let existing;
234
- const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
235
- key: options.key,
236
- name: "Pyfig provisioning",
237
- namespace: "pyfig",
238
- timeout: options.timeoutMs,
239
- predicate: async () => {
240
- const value = await options.check();
241
- if (value === null)
242
- return true;
243
- existing = { value };
244
- return false;
245
- }
246
- });
247
- if (result.context.state === "executed")
248
- return result.value;
249
- if (existing)
250
- return existing.value;
251
- throw new Error(`Provisioning check for ${options.key} did not produce a resource.`);
252
- }
274
+ // src/venv-identity.ts
275
+ var import_node_crypto = require("node:crypto");
276
+ var import_promises = require("node:fs/promises");
277
+ var import_node_path4 = __toESM(require("node:path"), 1);
253
278
 
254
- // src/virtual-environment.ts
279
+ // src/python-venv.ts
255
280
  var import_node_fs2 = __toESM(require("node:fs"), 1);
256
281
  var import_node_path3 = __toESM(require("node:path"), 1);
257
- var VIRTUAL_ENV_DIRECTORY_NAME = import_node_path3.default.join("node_modules", ".pyfig", "venv");
258
- function virtualEnvironmentDirectory(projectRoot) {
259
- return import_node_path3.default.join(projectRoot, VIRTUAL_ENV_DIRECTORY_NAME);
282
+ var VIRTUAL_ENV_ROOT_NAME = import_node_path3.default.join("node_modules", ".pyfig", "venvs");
283
+ function pythonVenvRoot(projectRoot) {
284
+ return import_node_path3.default.join(projectRoot, VIRTUAL_ENV_ROOT_NAME);
260
285
  }
261
- function externalVirtualEnvironmentDirectories(projectRoot, env) {
286
+ function pythonVenvDirectory(projectRoot, venvDirectory) {
287
+ return import_node_path3.default.join(pythonVenvRoot(projectRoot), venvDirectory);
288
+ }
289
+ function externalPythonVenvDirectories(projectRoot, env) {
262
290
  const candidates = [env["VIRTUAL_ENV"], import_node_path3.default.join(projectRoot, ".venv")].filter((value) => Boolean(value)).map((value) => import_node_path3.default.resolve(value));
263
291
  return [...new Map(candidates.map((value) => [canonicalPath(value), value])).values()];
264
292
  }
265
- function virtualEnvironmentPythonExecutable(virtualEnvironment) {
266
- return import_node_path3.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
293
+ function pythonVenvExecutable(pythonVenv) {
294
+ return import_node_path3.default.join(pythonVenv, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
267
295
  }
268
- function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
296
+ function activatePythonVenv(env, pythonVenv, pythonExecutable) {
269
297
  const activated = { ...env };
270
298
  const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
271
299
  const existingPath = pathEntry ? activated[pathEntry] : undefined;
@@ -276,7 +304,7 @@ function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
276
304
  }
277
305
  }
278
306
  activated["PATH"] = [import_node_path3.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path3.default.delimiter);
279
- activated["VIRTUAL_ENV"] = virtualEnvironment;
307
+ activated["VIRTUAL_ENV"] = pythonVenv;
280
308
  return activated;
281
309
  }
282
310
  function pathsEqual(left, right) {
@@ -291,6 +319,102 @@ function canonicalPath(value) {
291
319
  return process.platform === "win32" ? canonical.toLowerCase() : canonical;
292
320
  }
293
321
 
322
+ // src/venv-identity.ts
323
+ var VENV_SCHEMA = 1;
324
+ var VENV_METADATA = ".pyfig-venv.json";
325
+ function createVenvIdentity(python, requirementsSha256) {
326
+ const contract = {
327
+ schema: VENV_SCHEMA,
328
+ runtime: {
329
+ architecture: process.arch,
330
+ platform: process.platform
331
+ },
332
+ python: {
333
+ architecture: python.architecture,
334
+ baseExecutable: canonicalPath(python.executable),
335
+ basePrefix: canonicalPath(python.basePrefix),
336
+ embeddingHeader: python.embedding ? canonicalPath(python.embedding.headerPath) : null,
337
+ embeddingLibrary: python.embedding ? canonicalPath(python.embedding.libraryPath) : null,
338
+ gilDisabled: python.gilDisabled,
339
+ implementation: python.implementation,
340
+ pointerBits: python.pointerBits,
341
+ version: python.version
342
+ },
343
+ requirementsSha256
344
+ };
345
+ const id = sha256(JSON.stringify(contract));
346
+ const implementation = safePathSegment(python.implementation);
347
+ const version = safePathSegment(python.version);
348
+ return {
349
+ contract,
350
+ directoryName: `${implementation}-${version}-${id.slice(0, 20)}`,
351
+ id
352
+ };
353
+ }
354
+ function requirementsDigest(content) {
355
+ return sha256(content);
356
+ }
357
+ function venvMetadataFile(directory) {
358
+ return import_node_path4.default.join(directory, VENV_METADATA);
359
+ }
360
+ async function venvMatches(directory, venv) {
361
+ try {
362
+ const parsed = JSON.parse(await import_promises.readFile(venvMetadataFile(directory), "utf8"));
363
+ return parsed.venvId === venv.id && JSON.stringify(parsed.contract) === JSON.stringify(venv.contract);
364
+ } catch {
365
+ return false;
366
+ }
367
+ }
368
+ async function writeVenvMetadata(directory, venv) {
369
+ await import_promises.mkdir(directory, { recursive: true });
370
+ const file = venvMetadataFile(directory);
371
+ const staged = `${file}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`;
372
+ const metadata = {
373
+ contract: venv.contract,
374
+ venvId: venv.id
375
+ };
376
+ try {
377
+ await import_promises.writeFile(staged, `${JSON.stringify(metadata, null, 2)}
378
+ `, {
379
+ encoding: "utf8",
380
+ flag: "wx"
381
+ });
382
+ await import_promises.rename(staged, file);
383
+ } finally {
384
+ await import_promises.rm(staged, { force: true });
385
+ }
386
+ }
387
+ function safePathSegment(value) {
388
+ return value.toLowerCase().replace(/[^a-z0-9.-]+/gu, "-").replace(/^-+|-+$/gu, "") || "python";
389
+ }
390
+ function sha256(value) {
391
+ return import_node_crypto.createHash("sha256").update(value).digest("hex");
392
+ }
393
+
394
+ // src/provision-lock.ts
395
+ var import_loqkit = require("loqkit");
396
+ async function withProvisioningLock(options) {
397
+ let existing;
398
+ const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
399
+ key: options.key,
400
+ name: "Pyfig provisioning",
401
+ namespace: "pyfig",
402
+ timeout: options.timeoutMs,
403
+ predicate: async () => {
404
+ const value = await options.check();
405
+ if (value === null)
406
+ return true;
407
+ existing = { value };
408
+ return false;
409
+ }
410
+ });
411
+ if (result.context.state === "executed")
412
+ return result.value;
413
+ if (existing)
414
+ return existing.value;
415
+ throw new Error(`Provisioning check for ${options.key} did not produce a resource.`);
416
+ }
417
+
294
418
  // src/package-target.ts
295
419
  var TARGET_OPTIONS = new Set([
296
420
  "--active",
@@ -323,7 +447,7 @@ var TARGET_ENVIRONMENT_VARIABLES = new Set([
323
447
  "UV_SYSTEM_PYTHON",
324
448
  "UV_TARGET"
325
449
  ]);
326
- function uvCommandNeedsEnvironment(args) {
450
+ function uvCommandNeedsPython(args) {
327
451
  return args[0] === "pip" || args[0] === "run";
328
452
  }
329
453
  function targetUvArguments(args, python) {
@@ -339,16 +463,16 @@ function targetUvArguments(args, python) {
339
463
  }
340
464
  return [...args];
341
465
  }
342
- function uvCommandEnvironment(env, python) {
343
- const sanitized = sanitizeUvEnvironment(env);
466
+ function uvCommandProcessEnvironment(env, python) {
467
+ const sanitized = sanitizeUvProcessEnvironment(env);
344
468
  if (!python?.virtualEnvironment)
345
469
  return sanitized;
346
470
  return {
347
- ...activateVirtualEnvironment(sanitized, python.virtualEnvironment, python.executable),
471
+ ...activatePythonVenv(sanitized, python.virtualEnvironment, python.executable),
348
472
  UV_PYTHON: python.executable
349
473
  };
350
474
  }
351
- function sanitizeUvEnvironment(env) {
475
+ function sanitizeUvProcessEnvironment(env) {
352
476
  const sanitized = { ...env };
353
477
  for (const key of Object.keys(sanitized)) {
354
478
  if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
@@ -372,12 +496,12 @@ function assertNoTargetOverride(args) {
372
496
 
373
497
  // src/process.ts
374
498
  var import_node_child_process = require("node:child_process");
375
- var import_node_path4 = __toESM(require("node:path"), 1);
499
+ var import_node_path5 = __toESM(require("node:path"), 1);
376
500
  var import_which = __toESM(require("which"), 1);
377
501
  async function resolveExecutable(value, env) {
378
502
  if (!value)
379
503
  return null;
380
- if (import_node_path4.default.isAbsolute(value)) {
504
+ if (import_node_path5.default.isAbsolute(value)) {
381
505
  return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
382
506
  }
383
507
  if (!/^[A-Za-z0-9_.-]+$/u.test(value))
@@ -394,7 +518,7 @@ function run(executable, args, env, timeoutMs) {
394
518
  });
395
519
  if (result.status !== 0 || result.error) {
396
520
  const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
397
- throw new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
521
+ throw new Error(`${import_node_path5.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
398
522
  }
399
523
  return { stdout: result.stdout, stderr: result.stderr };
400
524
  }
@@ -423,11 +547,11 @@ async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
423
547
  const abort = () => {
424
548
  child.kill();
425
549
  const reason = signal?.reason;
426
- finish(reason instanceof Error ? reason : new Error(`${import_node_path4.default.basename(executable)} was aborted.`));
550
+ finish(reason instanceof Error ? reason : new Error(`${import_node_path5.default.basename(executable)} was aborted.`));
427
551
  };
428
552
  const timer = setTimeout(() => {
429
553
  child.kill();
430
- finish(new Error(`${import_node_path4.default.basename(executable)} timed out after ${timeoutMs}ms.`));
554
+ finish(new Error(`${import_node_path5.default.basename(executable)} timed out after ${timeoutMs}ms.`));
431
555
  }, timeoutMs);
432
556
  child.stdout.setEncoding("utf8");
433
557
  child.stderr.setEncoding("utf8");
@@ -435,21 +559,21 @@ async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
435
559
  stdout += chunk;
436
560
  if (stdout.length > 4 * 1024 * 1024) {
437
561
  child.kill();
438
- finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the output limit.`));
562
+ finish(new Error(`${import_node_path5.default.basename(executable)} exceeded the output limit.`));
439
563
  }
440
564
  });
441
565
  child.stderr.on("data", (chunk) => {
442
566
  stderr += chunk;
443
567
  if (stderr.length > 4 * 1024 * 1024) {
444
568
  child.kill();
445
- finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the error-output limit.`));
569
+ finish(new Error(`${import_node_path5.default.basename(executable)} exceeded the error-output limit.`));
446
570
  }
447
571
  });
448
572
  child.once("error", (error) => finish(error));
449
573
  child.once("close", (code, childSignal) => {
450
574
  if (code !== 0) {
451
575
  const detail = stderr.trim() || (childSignal ? `signal ${childSignal}` : `exit ${code}`);
452
- finish(new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
576
+ finish(new Error(`${import_node_path5.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
453
577
  return;
454
578
  }
455
579
  finish(undefined, { stdout, stderr });
@@ -603,59 +727,71 @@ function pythonArchitectureMatchesRuntime(python) {
603
727
  async function ensureUv(options = {}) {
604
728
  return ensureUvResolved(resolveOptions(options));
605
729
  }
606
- async function ensureEnvironment(options = {}, dependencies = [], dependencyOptions = {}) {
730
+ async function ensurePython(options = {}) {
607
731
  const resolved = resolveOptions(options);
608
- const normalized = normalizeDependencies(dependencies);
609
- const selections = normalizeDependencyOptions(dependencyOptions);
732
+ await validateRequirementsFiles(resolved.requirements);
610
733
  const uv = await ensureUvResolved(resolved);
611
- const python = await ensureVirtualEnvironmentResolved(resolved, uv);
612
- if (normalized.length > 0 || selections.extras.length > 0 || selections.groups.length > 0 || selections.allExtras) {
613
- await installRuntimeDependencies(uv, python, normalized, selections, resolved);
734
+ if (!hasRuntimeRequirements(resolved)) {
735
+ const external = await findReusableExternalPythonVenv(resolved);
736
+ if (external) {
737
+ return { managed: false, python: external, uv, venvId: null };
738
+ }
614
739
  }
615
- return {
616
- python,
617
- uv,
618
- managed: isManagedEnvironment(python, resolved)
740
+ const basePython = await ensureBasePythonResolved(uv, resolved);
741
+ const resolution = await resolveRuntimeDependencies(uv, basePython, resolved);
742
+ const venv = createVenvIdentity(basePython, resolution.sha256);
743
+ const python = await ensureManagedPythonVenv(uv, basePython, venv, resolution, resolved);
744
+ return { managed: true, python, uv, venvId: venv.id };
745
+ }
746
+ async function ensureManagedPythonVenv(uv, basePython, venv, resolution, options) {
747
+ const directory = pythonVenvDirectory(options.projectRoot, venv.directoryName);
748
+ const check = async () => {
749
+ if (!await venvMatches(directory, venv))
750
+ return null;
751
+ const candidate = await inspectPythonVenv(directory, options);
752
+ return candidate && pythonSatisfiesPolicy(candidate, options) ? candidate : null;
619
753
  };
620
- }
621
- async function ensureVirtualEnvironmentResolved(options, uv) {
622
- const existing = await findReusableEnvironment(options);
754
+ const existing = await check();
623
755
  if (existing)
624
756
  return existing;
625
- const directory = virtualEnvironmentDirectory(options.projectRoot);
626
- const check = async () => {
627
- const candidate = await inspectVirtualEnvironment(directory, options);
628
- return candidate && pythonSatisfiesPolicy(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
629
- };
630
757
  if (!options.createVirtualEnvironment) {
631
758
  throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
632
759
  }
633
- const basePython = await ensureBasePythonResolved(uv, options);
634
760
  return withProvisioningLock({
635
761
  key: `venv:${canonicalPath(directory)}`,
636
762
  timeoutMs: options.coordinationTimeoutMs,
637
763
  check,
638
764
  mutate: async () => {
639
- await import_promises.rm(directory, { recursive: true, force: true });
640
- await import_promises.mkdir(import_node_path5.default.dirname(directory), { recursive: true });
641
- await runAsync(uv.executable, [
642
- "venv",
643
- "--no-project",
644
- "--no-config",
645
- "--no-python-downloads",
646
- "--python",
647
- basePython.executable,
648
- directory
649
- ], isolatedUvEnvironment(options), options.installTimeoutMs);
650
- const installed = await check();
651
- if (!installed) {
652
- throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
765
+ await import_promises2.rm(directory, { recursive: true, force: true });
766
+ await import_promises2.mkdir(import_node_path6.default.dirname(directory), { recursive: true });
767
+ try {
768
+ await runAsync(uv.executable, [
769
+ "venv",
770
+ "--no-project",
771
+ "--no-config",
772
+ "--no-python-downloads",
773
+ "--python",
774
+ basePython.executable,
775
+ directory
776
+ ], isolatedUvProcessEnvironment(options), options.installTimeoutMs);
777
+ const created = await inspectPythonVenv(directory, options);
778
+ if (!created || !pythonSatisfiesPolicy(created, options)) {
779
+ throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
780
+ }
781
+ await installResolvedDependencies(uv, created, resolution, options);
782
+ await writeVenvMetadata(directory, venv);
783
+ const installed = await check();
784
+ if (!installed)
785
+ throw new Error(`Pyfig could not validate environment ${directory}.`);
786
+ return installed;
787
+ } catch (error) {
788
+ await import_promises2.rm(directory, { recursive: true, force: true });
789
+ throw error;
653
790
  }
654
- return installed;
655
791
  }
656
792
  });
657
793
  }
658
- async function findReusableEnvironment(options) {
794
+ async function findReusableExternalPythonVenv(options) {
659
795
  if (options.python) {
660
796
  const configured = await probePython({ executable: options.python, args: [] }, options);
661
797
  if (!configured || !pythonSatisfiesPolicy(configured, options)) {
@@ -664,14 +800,10 @@ async function findReusableEnvironment(options) {
664
800
  if (configured.virtualEnvironment)
665
801
  return configured;
666
802
  }
667
- for (const directory of [
668
- ...externalVirtualEnvironmentDirectories(options.projectRoot, options.env),
669
- virtualEnvironmentDirectory(options.projectRoot)
670
- ]) {
671
- const info = await inspectVirtualEnvironment(directory, options);
672
- if (info && pythonSatisfiesPolicy(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
803
+ for (const directory of externalPythonVenvDirectories(options.projectRoot, options.env)) {
804
+ const info = await inspectPythonVenv(directory, options);
805
+ if (info && pythonSatisfiesPolicy(info, options))
673
806
  return info;
674
- }
675
807
  }
676
808
  return null;
677
809
  }
@@ -688,6 +820,9 @@ async function ensureBasePythonResolved(uv, options) {
688
820
  return base;
689
821
  throw incompatibleConfiguredPython(options);
690
822
  }
823
+ const externalBase = await findExternalBasePython(options);
824
+ if (externalBase)
825
+ return externalBase;
691
826
  const system = await findPythonWithUv(uv, options, "system");
692
827
  if (system)
693
828
  return system;
@@ -698,11 +833,11 @@ async function ensureBasePythonResolved(uv, options) {
698
833
  throw new Error(missingPythonMessage(options, "managed Python installation is disabled"));
699
834
  }
700
835
  return withProvisioningLock({
701
- key: `python:${canonicalPath(import_node_path5.default.join(options.cacheDir, "python"))}`,
836
+ key: `python:${canonicalPath(import_node_path6.default.join(options.cacheDir, "python"))}`,
702
837
  timeoutMs: options.coordinationTimeoutMs,
703
838
  check: () => findPythonWithUv(uv, options, "managed"),
704
839
  mutate: async () => {
705
- await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], isolatedUvEnvironment(options), options.installTimeoutMs);
840
+ await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], isolatedUvProcessEnvironment(options), options.installTimeoutMs);
706
841
  const installed = await findPythonWithUv(uv, options, "managed");
707
842
  if (!installed) {
708
843
  throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
@@ -711,6 +846,17 @@ async function ensureBasePythonResolved(uv, options) {
711
846
  }
712
847
  });
713
848
  }
849
+ async function findExternalBasePython(options) {
850
+ for (const directory of externalPythonVenvDirectories(options.projectRoot, options.env)) {
851
+ const environment = await inspectPythonVenv(directory, options);
852
+ if (!environment || !pythonSatisfiesPolicy(environment, options))
853
+ continue;
854
+ const base = await probePython({ executable: environment.baseExecutable, args: [] }, options);
855
+ if (base && !base.virtualEnvironment && pythonSatisfiesPolicy(base, options))
856
+ return base;
857
+ }
858
+ return null;
859
+ }
714
860
  async function findPythonWithUv(uv, options, source) {
715
861
  for (const request of ["", options.managedPython]) {
716
862
  const args = [
@@ -724,7 +870,7 @@ async function findPythonWithUv(uv, options, source) {
724
870
  if (request)
725
871
  args.push(request);
726
872
  try {
727
- const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
873
+ const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceProcessEnvironment(options), options.probeTimeoutMs);
728
874
  const info = await probePython({ executable: stdout.trim(), args: [] }, options);
729
875
  if (info && pythonSatisfiesPolicy(info, options))
730
876
  return info;
@@ -740,7 +886,7 @@ async function findPythonWithUv(uv, options, source) {
740
886
  "--output-format",
741
887
  "json",
742
888
  source === "system" ? "--no-managed-python" : "--managed-python"
743
- ], explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
889
+ ], explicitPythonSourceProcessEnvironment(options), options.probeTimeoutMs);
744
890
  const candidates = installedPythonPaths(stdout);
745
891
  for (const executable of candidates) {
746
892
  const info = await probePython({ executable, args: [] }, options);
@@ -763,59 +909,79 @@ function installedPythonPaths(output) {
763
909
  });
764
910
  return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
765
911
  }
766
- async function inspectVirtualEnvironment(directory, options) {
767
- const executable = virtualEnvironmentPythonExecutable(directory);
912
+ async function inspectPythonVenv(directory, options) {
913
+ const executable = pythonVenvExecutable(directory);
768
914
  const info = await probePython({ executable, args: [] }, options);
769
915
  return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
770
916
  }
771
- async function installRuntimeDependencies(uv, python, dependencies, dependencyOptions, options) {
772
- if (!python.virtualEnvironment) {
773
- throw new Error("Runtime dependency installation requires a virtual environment.");
917
+ async function resolveRuntimeDependencies(uv, python, options) {
918
+ if (!hasRuntimeRequirements(options)) {
919
+ return { content: "", sha256: requirementsDigest("") };
774
920
  }
775
- await withProvisioningLock({
776
- key: `dependencies:${canonicalPath(python.virtualEnvironment)}`,
777
- timeoutMs: options.coordinationTimeoutMs,
778
- check: async () => null,
779
- mutate: async () => {
780
- const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-dependencies-"));
781
- try {
782
- const additions = import_node_path5.default.join(temporary, "runtime-dependencies.txt");
783
- const resolved = import_node_path5.default.join(temporary, "resolved-dependencies.txt");
784
- const pyproject = import_node_path5.default.join(options.projectRoot, "pyproject.toml");
785
- const hasPyproject = import_node_fs4.default.existsSync(pyproject);
786
- if (!hasPyproject && (dependencyOptions.extras.length > 0 || dependencyOptions.groups.length > 0 || dependencyOptions.allExtras)) {
787
- throw new Error("Runtime extras and groups require an existing pyproject.toml.");
788
- }
789
- const sources = hasPyproject ? [pyproject] : [];
790
- if (dependencies.length > 0) {
791
- await import_promises.writeFile(additions, `${dependencies.join(`
921
+ const temporary = await import_promises2.mkdtemp(import_node_path6.default.join(import_node_os.default.tmpdir(), "pyfig-resolution-"));
922
+ try {
923
+ const additions = import_node_path6.default.join(temporary, "runtime-dependencies.txt");
924
+ const output = import_node_path6.default.join(temporary, "resolved-dependencies.txt");
925
+ const sources = [...options.requirements];
926
+ if (options.dependencies.length > 0) {
927
+ await import_promises2.writeFile(additions, `${options.dependencies.join(`
792
928
  `)}
793
929
  `, "utf8");
794
- sources.push(additions);
795
- }
796
- const selectionArgs = [
797
- ...dependencyOptions.extras.flatMap((extra) => ["--extra", extra]),
798
- ...dependencyOptions.groups.flatMap((group) => ["--group", group]),
799
- ...dependencyOptions.allExtras ? ["--all-extras"] : []
800
- ];
801
- await runAsync(uv.executable, [
802
- "pip",
803
- "compile",
804
- "--no-config",
805
- "--python",
806
- python.executable,
807
- ...selectionArgs,
808
- ...sources,
809
- "--output-file",
810
- resolved
811
- ], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
812
- await runAsync(uv.executable, ["pip", "install", "--no-config", "--python", python.executable, "-r", resolved], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
813
- } finally {
814
- await import_promises.rm(temporary, { recursive: true, force: true });
815
- }
816
- return true;
930
+ sources.push(additions);
817
931
  }
818
- });
932
+ await runAsync(uv.executable, [
933
+ "pip",
934
+ "compile",
935
+ "--no-config",
936
+ "--no-header",
937
+ "--no-annotate",
938
+ "--generate-hashes",
939
+ "--python",
940
+ python.executable,
941
+ ...sources,
942
+ "--output-file",
943
+ output
944
+ ], isolatedUvProcessEnvironment(options), options.installTimeoutMs, undefined, options.projectRoot);
945
+ const content = normalizeResolution(await import_promises2.readFile(output, "utf8"));
946
+ return { content, sha256: requirementsDigest(content) };
947
+ } finally {
948
+ await import_promises2.rm(temporary, { recursive: true, force: true });
949
+ }
950
+ }
951
+ async function validateRequirementsFiles(requirements) {
952
+ for (const requirement of requirements) {
953
+ let information;
954
+ try {
955
+ information = await import_promises2.stat(requirement);
956
+ } catch (error) {
957
+ throw new Error(`Requirements file is unavailable: ${requirement}`, { cause: error });
958
+ }
959
+ if (!information.isFile()) {
960
+ throw new Error(`Requirements path is not a file: ${requirement}`);
961
+ }
962
+ }
963
+ }
964
+ async function installResolvedDependencies(uv, python, resolution, options) {
965
+ if (!resolution.content)
966
+ return;
967
+ if (!python.virtualEnvironment) {
968
+ throw new Error("Runtime dependency installation requires a virtual environment.");
969
+ }
970
+ const temporary = await import_promises2.mkdtemp(import_node_path6.default.join(import_node_os.default.tmpdir(), "pyfig-install-"));
971
+ try {
972
+ const requirements = import_node_path6.default.join(temporary, "resolved-dependencies.txt");
973
+ await import_promises2.writeFile(requirements, resolution.content, { encoding: "utf8", flag: "wx" });
974
+ await runAsync(uv.executable, ["pip", "install", "--no-config", "--python", python.executable, "-r", requirements], targetUvProcessEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
975
+ await runAsync(uv.executable, ["pip", "check", "--no-config", "--python", python.executable], targetUvProcessEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
976
+ } finally {
977
+ await import_promises2.rm(temporary, { recursive: true, force: true });
978
+ }
979
+ }
980
+ function normalizeResolution(content) {
981
+ const normalized = content.replace(/\r\n/gu, `
982
+ `).trimEnd();
983
+ return normalized ? `${normalized}
984
+ ` : "";
819
985
  }
820
986
  async function ensureUvResolved(options) {
821
987
  const existing = await findAvailableUv(options);
@@ -824,7 +990,7 @@ async function ensureUvResolved(options) {
824
990
  if (!options.installUv) {
825
991
  throw new Error(`Compatible uv is unavailable (requires-uv ${options.requiresUv}), and uv installation is disabled.`);
826
992
  }
827
- const policyKey = import_node_crypto.createHash("sha256").update(options.requiresUv).digest("hex");
993
+ const policyKey = import_node_crypto2.createHash("sha256").update(options.requiresUv).digest("hex");
828
994
  return withProvisioningLock({
829
995
  key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
830
996
  timeoutMs: options.coordinationTimeoutMs,
@@ -856,7 +1022,7 @@ async function findAvailableUv(options) {
856
1022
  return findManagedUv(options);
857
1023
  }
858
1024
  async function findManagedUv(options) {
859
- const entries = await import_promises.readdir(uvInstallRoot(options), { withFileTypes: true }).catch(() => []);
1025
+ const entries = await import_promises2.readdir(uvInstallRoot(options), { withFileTypes: true }).catch(() => []);
860
1026
  let versions = entries.filter((entry) => entry.isDirectory() && normalizePep440Version(entry.name) === entry.name).map((entry) => entry.name);
861
1027
  while (versions.length > 0) {
862
1028
  const selected = maxPep440Version(versions, options.requiresUv);
@@ -869,45 +1035,16 @@ async function findManagedUv(options) {
869
1035
  }
870
1036
  return null;
871
1037
  }
872
- function normalizeDependencies(dependencies) {
873
- const normalized = dependencies.map((dependency) => dependency.trim());
874
- for (const dependency of normalized) {
875
- if (!dependency || /[\0\r\n]/u.test(dependency)) {
876
- throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
877
- }
878
- }
879
- return [...new Set(normalized)];
880
- }
881
- function normalizeDependencyOptions(options) {
882
- const extras = normalizeNames("extra", options.extras ?? []);
883
- const groups = normalizeNames("dependency group", options.groups ?? []);
884
- if (options.allExtras && extras.length > 0) {
885
- throw new TypeError("allExtras cannot be combined with explicit extras.");
886
- }
887
- return { extras, groups, allExtras: options.allExtras ?? false };
888
- }
889
- function normalizeNames(kind, names) {
890
- const normalized = names.map((name) => name.trim());
891
- for (const name of normalized) {
892
- if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
893
- throw new TypeError(`Invalid ${kind}: ${JSON.stringify(name)}`);
894
- }
895
- }
896
- return [...new Set(normalized)];
897
- }
898
- function isManagedEnvironment(python, options) {
899
- return Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(options.projectRoot));
900
- }
901
- function isolatedUvEnvironment(options) {
902
- return sanitizeUvEnvironment(uvEnvironment(options));
1038
+ function isolatedUvProcessEnvironment(options) {
1039
+ return sanitizeUvProcessEnvironment(uvProcessEnvironment(options));
903
1040
  }
904
- function explicitPythonSourceEnvironment(options) {
905
- const env = isolatedUvEnvironment(options);
1041
+ function explicitPythonSourceProcessEnvironment(options) {
1042
+ const env = isolatedUvProcessEnvironment(options);
906
1043
  delete env["UV_PYTHON_PREFERENCE"];
907
1044
  return env;
908
1045
  }
909
- function targetUvEnvironment(options, python) {
910
- return uvCommandEnvironment(uvEnvironment(options), python);
1046
+ function targetUvProcessEnvironment(options, python) {
1047
+ return uvCommandProcessEnvironment(uvProcessEnvironment(options), python);
911
1048
  }
912
1049
  function incompatibleConfiguredPython(options) {
913
1050
  return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `requires-python ${options.requiresPython}` + `${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
@@ -916,18 +1053,6 @@ function missingPythonMessage(options, reason) {
916
1053
  const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
917
1054
  return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
918
1055
  }
919
- async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
920
- if (!options.python)
921
- return true;
922
- const configured = await probePython({ executable: options.python, args: [] }, options);
923
- if (!configured || !pythonSatisfiesPolicy(configured, options))
924
- return false;
925
- if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
926
- return true;
927
- }
928
- const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
929
- return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
930
- }
931
1056
  var UV_RELEASE_PAGE_SIZE = 100;
932
1057
  var UV_RELEASE_MAX_PAGES = 20;
933
1058
  var UV_RELEASE_METADATA_TTL_MS = 24 * 60 * 60000;
@@ -1023,7 +1148,7 @@ async function readFreshUvReleaseCatalog(file) {
1023
1148
  }
1024
1149
  async function readUvReleaseCatalog(file) {
1025
1150
  try {
1026
- return parseUvReleaseCatalog(JSON.parse(await import_promises.readFile(file, "utf8")));
1151
+ return parseUvReleaseCatalog(JSON.parse(await import_promises2.readFile(file, "utf8")));
1027
1152
  } catch {
1028
1153
  return null;
1029
1154
  }
@@ -1052,20 +1177,20 @@ function parseUvReleaseCatalog(value) {
1052
1177
  return { fetchedAt: catalog.fetchedAt, releases };
1053
1178
  }
1054
1179
  async function writeUvReleaseCatalog(file, catalog) {
1055
- await import_promises.mkdir(import_node_path5.default.dirname(file), { recursive: true });
1056
- const staged = import_node_path5.default.join(import_node_path5.default.dirname(file), `.${import_node_path5.default.basename(file)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
1180
+ await import_promises2.mkdir(import_node_path6.default.dirname(file), { recursive: true });
1181
+ const staged = import_node_path6.default.join(import_node_path6.default.dirname(file), `.${import_node_path6.default.basename(file)}.${process.pid}.${import_node_crypto2.randomUUID()}.tmp`);
1057
1182
  try {
1058
- await import_promises.writeFile(staged, `${JSON.stringify(catalog)}
1183
+ await import_promises2.writeFile(staged, `${JSON.stringify(catalog)}
1059
1184
  `, { encoding: "utf8", flag: "wx" });
1060
1185
  if (process.platform === "win32")
1061
- await import_promises.rm(file, { force: true });
1062
- await import_promises.rename(staged, file);
1186
+ await import_promises2.rm(file, { force: true });
1187
+ await import_promises2.rename(staged, file);
1063
1188
  } finally {
1064
- await import_promises.rm(staged, { force: true });
1189
+ await import_promises2.rm(staged, { force: true });
1065
1190
  }
1066
1191
  }
1067
1192
  function uvReleaseMetadataFile(options) {
1068
- return import_node_path5.default.join(options.cacheDir, "metadata", "uv-releases.json");
1193
+ return import_node_path6.default.join(options.cacheDir, "metadata", "uv-releases.json");
1069
1194
  }
1070
1195
  function releaseAssetNames(value) {
1071
1196
  if (!Array.isArray(value))
@@ -1078,36 +1203,36 @@ function releaseAssetNames(value) {
1078
1203
  });
1079
1204
  }
1080
1205
  function uvInstallRoot(options) {
1081
- return import_node_path5.default.join(options.cacheDir, "tools", "uv");
1206
+ return import_node_path6.default.join(options.cacheDir, "tools", "uv");
1082
1207
  }
1083
1208
  function uvInstallDirectory(options, version) {
1084
- return import_node_path5.default.join(uvInstallRoot(options), version);
1209
+ return import_node_path6.default.join(uvInstallRoot(options), version);
1085
1210
  }
1086
1211
  function uvExecutable(options, version) {
1087
- return import_node_path5.default.join(uvInstallDirectory(options, version), process.platform === "win32" ? "uv.exe" : "uv");
1212
+ return import_node_path6.default.join(uvInstallDirectory(options, version), process.platform === "win32" ? "uv.exe" : "uv");
1088
1213
  }
1089
1214
  async function installUv(installDir, release, options) {
1090
1215
  const asset = uvAsset();
1091
1216
  const baseUrl = `https://github.com/astral-sh/uv/releases/download/${release.tag}`;
1092
- const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
1217
+ const temporary = await import_promises2.mkdtemp(import_node_path6.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
1093
1218
  const dispatcher = proxyDispatcher(options.env);
1094
1219
  try {
1095
- const archive = import_node_path5.default.join(temporary, asset);
1220
+ const archive = import_node_path6.default.join(temporary, asset);
1096
1221
  await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
1097
1222
  const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
1098
1223
  const actual = await sha256File(archive);
1099
1224
  if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
1100
1225
  throw new Error(`uv archive checksum mismatch for ${asset}`);
1101
1226
  }
1102
- const extracted = import_node_path5.default.join(temporary, "extracted");
1103
- await import_promises.mkdir(extracted, { recursive: true });
1227
+ const extracted = import_node_path6.default.join(temporary, "extracted");
1228
+ await import_promises2.mkdir(extracted, { recursive: true });
1104
1229
  const source = await extractUvExecutable(archive, asset, extracted);
1105
1230
  if (!source)
1106
1231
  throw new Error(`uv archive did not contain the expected executable: ${asset}`);
1107
1232
  await publishUvAtomically(source, installDir, release.version, options);
1108
1233
  } finally {
1109
1234
  await dispatcher?.close();
1110
- await import_promises.rm(temporary, { recursive: true, force: true });
1235
+ await import_promises2.rm(temporary, { recursive: true, force: true });
1111
1236
  }
1112
1237
  }
1113
1238
  async function extractUvExecutable(archive, asset, destination) {
@@ -1116,33 +1241,33 @@ async function extractUvExecutable(archive, asset, destination) {
1116
1241
  await tar.x({ file: archive, cwd: destination, strict: true });
1117
1242
  return findFile(destination, executableName);
1118
1243
  }
1119
- const entries = import_fflate.unzipSync(await import_promises.readFile(archive), {
1120
- filter: ({ name }) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName
1244
+ const entries = import_fflate.unzipSync(await import_promises2.readFile(archive), {
1245
+ filter: ({ name }) => import_node_path6.default.posix.basename(name.replaceAll("\\", "/")) === executableName
1121
1246
  });
1122
- const matches = Object.entries(entries).filter(([name]) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
1247
+ const matches = Object.entries(entries).filter(([name]) => import_node_path6.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
1123
1248
  if (matches.length !== 1 || !matches[0])
1124
1249
  return null;
1125
- const source = import_node_path5.default.join(destination, executableName);
1126
- await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
1250
+ const source = import_node_path6.default.join(destination, executableName);
1251
+ await import_promises2.writeFile(source, matches[0][1], { flag: "wx" });
1127
1252
  return source;
1128
1253
  }
1129
1254
  async function publishUvAtomically(source, installDir, version, options) {
1130
- await import_promises.mkdir(installDir, { recursive: true });
1255
+ await import_promises2.mkdir(installDir, { recursive: true });
1131
1256
  const executable = uvExecutable(options, version);
1132
- const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
1257
+ const staged = import_node_path6.default.join(installDir, `.${import_node_path6.default.basename(executable)}.${process.pid}.${import_node_crypto2.randomUUID()}.tmp`);
1133
1258
  try {
1134
- await import_promises.copyFile(source, staged);
1259
+ await import_promises2.copyFile(source, staged);
1135
1260
  if (process.platform !== "win32")
1136
- await import_promises.chmod(staged, 493);
1261
+ await import_promises2.chmod(staged, 493);
1137
1262
  const stagedUv = probeUv(staged, options);
1138
1263
  if (!stagedUv || stagedUv.version !== version) {
1139
1264
  throw new Error(`Downloaded uv executable is not version ${version}.`);
1140
1265
  }
1141
1266
  if (process.platform === "win32")
1142
- await import_promises.rm(executable, { force: true });
1143
- await import_promises.rename(staged, executable);
1267
+ await import_promises2.rm(executable, { force: true });
1268
+ await import_promises2.rename(staged, executable);
1144
1269
  } finally {
1145
- await import_promises.rm(staged, { force: true });
1270
+ await import_promises2.rm(staged, { force: true });
1146
1271
  }
1147
1272
  }
1148
1273
  function uvAsset() {
@@ -1167,7 +1292,7 @@ async function download(url, destination, timeoutMs, dispatcher) {
1167
1292
  await response.body?.cancel();
1168
1293
  throw new Error(`Could not download ${url}: HTTP ${response.status}`);
1169
1294
  }
1170
- await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
1295
+ await import_promises3.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
1171
1296
  }
1172
1297
  async function downloadText(url, timeoutMs, dispatcher, headers) {
1173
1298
  const response = await import_undici.fetch(url, {
@@ -1183,28 +1308,28 @@ async function downloadText(url, timeoutMs, dispatcher, headers) {
1183
1308
  return response.text();
1184
1309
  }
1185
1310
  function proxyDispatcher(env) {
1186
- const httpProxy = environmentValue(env, "http_proxy", "HTTP_PROXY");
1187
- const httpsProxy = environmentValue(env, "https_proxy", "HTTPS_PROXY");
1311
+ const httpProxy = processEnvironmentValue(env, "http_proxy", "HTTP_PROXY");
1312
+ const httpsProxy = processEnvironmentValue(env, "https_proxy", "HTTPS_PROXY");
1188
1313
  if (!httpProxy && !httpsProxy)
1189
1314
  return;
1190
1315
  return new import_undici.EnvHttpProxyAgent({
1191
1316
  httpProxy,
1192
1317
  httpsProxy,
1193
- noProxy: environmentValue(env, "no_proxy", "NO_PROXY")
1318
+ noProxy: processEnvironmentValue(env, "no_proxy", "NO_PROXY")
1194
1319
  });
1195
1320
  }
1196
- function environmentValue(env, lowercase, uppercase) {
1321
+ function processEnvironmentValue(env, lowercase, uppercase) {
1197
1322
  return env[lowercase] ?? env[uppercase];
1198
1323
  }
1199
1324
  async function sha256File(file) {
1200
- const hash = import_node_crypto.createHash("sha256");
1325
+ const hash = import_node_crypto2.createHash("sha256");
1201
1326
  for await (const chunk of import_node_fs3.createReadStream(file))
1202
1327
  hash.update(chunk);
1203
1328
  return hash.digest("hex");
1204
1329
  }
1205
1330
  async function findFile(directory, name) {
1206
- for (const entry of await import_promises.readdir(directory, { withFileTypes: true })) {
1207
- const target = import_node_path5.default.join(directory, entry.name);
1331
+ for (const entry of await import_promises2.readdir(directory, { withFileTypes: true })) {
1332
+ const target = import_node_path6.default.join(directory, entry.name);
1208
1333
  if (entry.isFile() && entry.name === name)
1209
1334
  return target;
1210
1335
  if (entry.isDirectory()) {
@@ -1220,21 +1345,21 @@ async function findFile(directory, name) {
1220
1345
  var import_node_child_process2 = require("node:child_process");
1221
1346
 
1222
1347
  // src/runtime-files.ts
1223
- var import_node_fs5 = __toESM(require("node:fs"), 1);
1224
- var import_node_path6 = __toESM(require("node:path"), 1);
1348
+ var import_node_fs4 = __toESM(require("node:fs"), 1);
1349
+ var import_node_path7 = __toESM(require("node:path"), 1);
1225
1350
  function runtimeFile(name) {
1226
1351
  const moduleDirectory = __dirname;
1227
- const compiled = import_node_path6.default.join(moduleDirectory, `${name}.js`);
1228
- return import_node_fs5.default.existsSync(compiled) ? compiled : import_node_path6.default.join(moduleDirectory, `${name}.ts`);
1352
+ const compiled = import_node_path7.default.join(moduleDirectory, `${name}.js`);
1353
+ return import_node_fs4.default.existsSync(compiled) ? compiled : import_node_path7.default.join(moduleDirectory, `${name}.ts`);
1229
1354
  }
1230
1355
 
1231
1356
  // src/runners.ts
1232
1357
  async function uvSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
1233
1358
  const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1234
1359
  const resolved = resolveOptions(options);
1235
- const environment = uvCommandNeedsEnvironment(args) ? await ensureEnvironment(resolved) : undefined;
1236
- const uv = environment?.uv ?? await ensureUv(resolved);
1237
- const command = buildUvCommand(uv, args, resolved, environment?.python);
1360
+ const python = uvCommandNeedsPython(args) ? await ensurePython(resolved) : undefined;
1361
+ const uv = python?.uv ?? await ensureUv(resolved);
1362
+ const command = buildUvCommand(uv, args, resolved, python?.python);
1238
1363
  return import_node_child_process2.spawn(command.executable, command.args, {
1239
1364
  ...spawnOptions,
1240
1365
  env: command.env
@@ -1243,9 +1368,9 @@ async function uvSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
1243
1368
  function uvSpawnSync(args = [], spawnOptions = {}, pyfigOptions = {}) {
1244
1369
  const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1245
1370
  const resolved = resolveOptions(options);
1246
- const environment = uvCommandNeedsEnvironment(args) ? resolveSynchronously("environment", resolved) : undefined;
1247
- const uv = environment?.uv ?? resolveSynchronously("uv", resolved);
1248
- const command = buildUvCommand(uv, args, resolved, environment?.python);
1371
+ const python = uvCommandNeedsPython(args) ? resolveSynchronously("python", resolved) : undefined;
1372
+ const uv = python?.uv ?? resolveSynchronously("uv", resolved);
1373
+ const command = buildUvCommand(uv, args, resolved, python?.python);
1249
1374
  return import_node_child_process2.spawnSync(command.executable, command.args, {
1250
1375
  ...spawnOptions,
1251
1376
  env: command.env
@@ -1255,7 +1380,7 @@ function buildUvCommand(uv, args, options, python) {
1255
1380
  return {
1256
1381
  executable: uv.executable,
1257
1382
  args: python ? targetUvArguments(args, python) : [...args],
1258
- env: uvCommandEnvironment(uvEnvironment(options), python)
1383
+ env: uvCommandProcessEnvironment(uvProcessEnvironment(options), python)
1259
1384
  };
1260
1385
  }
1261
1386
  function optionsForInvocation(pyfigOptions, spawnEnv) {
@@ -1288,20 +1413,35 @@ function synchronousResolutionTimeout(options) {
1288
1413
  }
1289
1414
 
1290
1415
  // src/client.ts
1291
- function createPyfig(options = {}) {
1292
- const scopedOptions = {
1293
- ...options,
1294
- env: options.env ? { ...options.env } : undefined
1295
- };
1296
- const baseEnvironment = import_loqkit2.memoize(() => ensureEnvironment(scopedOptions));
1297
- const uvInfo = import_loqkit2.memoize(() => ensureUv(scopedOptions));
1298
- return {
1299
- prepare: (dependencies = [], dependencyOptions = {}) => dependencies.length === 0 && !dependencyOptions.allExtras && (dependencyOptions.extras?.length ?? 0) === 0 && (dependencyOptions.groups?.length ?? 0) === 0 ? baseEnvironment() : ensureEnvironment(scopedOptions, dependencies, dependencyOptions),
1300
- pythonInfo: async () => (await baseEnvironment()).python,
1301
- uvInfo,
1302
- uv: (args = [], spawnOptions = {}) => uvSpawn(args, spawnOptions, scopedOptions),
1303
- uvSync: (args = [], spawnOptions = {}) => uvSpawnSync(args, spawnOptions, scopedOptions)
1304
- };
1416
+ class Python {
1417
+ #options;
1418
+ #python;
1419
+ #uvInfo;
1420
+ constructor(options = {}) {
1421
+ this.#options = {
1422
+ ...options,
1423
+ dependencies: options.dependencies ? [...options.dependencies] : undefined,
1424
+ requirements: options.requirements ? [...options.requirements] : undefined,
1425
+ env: options.env ? { ...options.env } : undefined
1426
+ };
1427
+ this.#python = import_loqkit2.memoize(() => ensurePython(this.#options));
1428
+ this.#uvInfo = import_loqkit2.memoize(() => ensureUv(this.#options));
1429
+ }
1430
+ prepare() {
1431
+ return this.#python();
1432
+ }
1433
+ async pythonInfo() {
1434
+ return (await this.#python()).python;
1435
+ }
1436
+ uvInfo() {
1437
+ return this.#uvInfo();
1438
+ }
1439
+ uv(args = [], options = {}) {
1440
+ return uvSpawn(args, options, this.#options);
1441
+ }
1442
+ uvSync(args, options) {
1443
+ return uvSpawnSync(args, options, this.#options);
1444
+ }
1305
1445
  }
1306
1446
 
1307
- //# debugId=0CA78C4B6148614B64756E2164756E21
1447
+ //# debugId=653756BC4A56AC4F64756E2164756E21