pyfig 0.6.2 → 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 +47 -34
  2. package/SECURITY.md +1 -1
  3. package/dist/cjs/index.js +409 -261
  4. package/dist/cjs/index.js.map +10 -9
  5. package/dist/cjs/sync-resolver.js +369 -236
  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 +8 -12
  26. package/dist/provision.d.ts.map +1 -1
  27. package/dist/provision.js +156 -158
  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)];
1038
+ function isolatedUvProcessEnvironment(options) {
1039
+ return sanitizeUvProcessEnvironment(uvProcessEnvironment(options));
897
1040
  }
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));
903
- }
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;
@@ -969,11 +1094,7 @@ async function fetchUvReleaseCatalog(options) {
969
1094
  let exhausted = false;
970
1095
  try {
971
1096
  for (let page = 1;page <= UV_RELEASE_MAX_PAGES; page += 1) {
972
- const text = await downloadText(`https://api.github.com/repos/astral-sh/uv/releases?per_page=${UV_RELEASE_PAGE_SIZE}&page=${page}`, options.downloadTimeoutMs, dispatcher, {
973
- Accept: "application/vnd.github+json",
974
- "User-Agent": "pyfig",
975
- "X-GitHub-Api-Version": "2022-11-28"
976
- });
1097
+ const text = await downloadText(`https://api.github.com/repos/astral-sh/uv/releases?per_page=${UV_RELEASE_PAGE_SIZE}&page=${page}`, options.downloadTimeoutMs, dispatcher, githubApiHeaders(options.env));
977
1098
  const payload = JSON.parse(text);
978
1099
  if (!Array.isArray(payload)) {
979
1100
  throw new Error("GitHub returned invalid uv release metadata.");
@@ -1006,6 +1127,18 @@ async function fetchUvReleaseCatalog(options) {
1006
1127
  }
1007
1128
  return { fetchedAt: Date.now(), releases: [...releases.values()] };
1008
1129
  }
1130
+ function githubApiHeaders(env) {
1131
+ const token = [env["GITHUB_TOKEN"], env["GH_TOKEN"]].find((value) => typeof value === "string" && value.trim().length > 0);
1132
+ if (token && /[\r\n]/u.test(token)) {
1133
+ throw new TypeError("GitHub API token must not contain line breaks.");
1134
+ }
1135
+ return {
1136
+ Accept: "application/vnd.github+json",
1137
+ "User-Agent": "pyfig",
1138
+ "X-GitHub-Api-Version": "2022-11-28",
1139
+ ...token ? { Authorization: `Bearer ${token}` } : {}
1140
+ };
1141
+ }
1009
1142
  async function readFreshUvReleaseCatalog(file) {
1010
1143
  const catalog = await readUvReleaseCatalog(file);
1011
1144
  if (!catalog)
@@ -1015,7 +1148,7 @@ async function readFreshUvReleaseCatalog(file) {
1015
1148
  }
1016
1149
  async function readUvReleaseCatalog(file) {
1017
1150
  try {
1018
- return parseUvReleaseCatalog(JSON.parse(await import_promises.readFile(file, "utf8")));
1151
+ return parseUvReleaseCatalog(JSON.parse(await import_promises2.readFile(file, "utf8")));
1019
1152
  } catch {
1020
1153
  return null;
1021
1154
  }
@@ -1044,20 +1177,20 @@ function parseUvReleaseCatalog(value) {
1044
1177
  return { fetchedAt: catalog.fetchedAt, releases };
1045
1178
  }
1046
1179
  async function writeUvReleaseCatalog(file, catalog) {
1047
- await import_promises.mkdir(import_node_path5.default.dirname(file), { recursive: true });
1048
- 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`);
1049
1182
  try {
1050
- await import_promises.writeFile(staged, `${JSON.stringify(catalog)}
1183
+ await import_promises2.writeFile(staged, `${JSON.stringify(catalog)}
1051
1184
  `, { encoding: "utf8", flag: "wx" });
1052
1185
  if (process.platform === "win32")
1053
- await import_promises.rm(file, { force: true });
1054
- await import_promises.rename(staged, file);
1186
+ await import_promises2.rm(file, { force: true });
1187
+ await import_promises2.rename(staged, file);
1055
1188
  } finally {
1056
- await import_promises.rm(staged, { force: true });
1189
+ await import_promises2.rm(staged, { force: true });
1057
1190
  }
1058
1191
  }
1059
1192
  function uvReleaseMetadataFile(options) {
1060
- 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");
1061
1194
  }
1062
1195
  function releaseAssetNames(value) {
1063
1196
  if (!Array.isArray(value))
@@ -1070,36 +1203,36 @@ function releaseAssetNames(value) {
1070
1203
  });
1071
1204
  }
1072
1205
  function uvInstallRoot(options) {
1073
- return import_node_path5.default.join(options.cacheDir, "tools", "uv");
1206
+ return import_node_path6.default.join(options.cacheDir, "tools", "uv");
1074
1207
  }
1075
1208
  function uvInstallDirectory(options, version) {
1076
- return import_node_path5.default.join(uvInstallRoot(options), version);
1209
+ return import_node_path6.default.join(uvInstallRoot(options), version);
1077
1210
  }
1078
1211
  function uvExecutable(options, version) {
1079
- 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");
1080
1213
  }
1081
1214
  async function installUv(installDir, release, options) {
1082
1215
  const asset = uvAsset();
1083
1216
  const baseUrl = `https://github.com/astral-sh/uv/releases/download/${release.tag}`;
1084
- 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-"));
1085
1218
  const dispatcher = proxyDispatcher(options.env);
1086
1219
  try {
1087
- const archive = import_node_path5.default.join(temporary, asset);
1220
+ const archive = import_node_path6.default.join(temporary, asset);
1088
1221
  await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
1089
1222
  const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
1090
1223
  const actual = await sha256File(archive);
1091
1224
  if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
1092
1225
  throw new Error(`uv archive checksum mismatch for ${asset}`);
1093
1226
  }
1094
- const extracted = import_node_path5.default.join(temporary, "extracted");
1095
- 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 });
1096
1229
  const source = await extractUvExecutable(archive, asset, extracted);
1097
1230
  if (!source)
1098
1231
  throw new Error(`uv archive did not contain the expected executable: ${asset}`);
1099
1232
  await publishUvAtomically(source, installDir, release.version, options);
1100
1233
  } finally {
1101
1234
  await dispatcher?.close();
1102
- await import_promises.rm(temporary, { recursive: true, force: true });
1235
+ await import_promises2.rm(temporary, { recursive: true, force: true });
1103
1236
  }
1104
1237
  }
1105
1238
  async function extractUvExecutable(archive, asset, destination) {
@@ -1108,33 +1241,33 @@ async function extractUvExecutable(archive, asset, destination) {
1108
1241
  await tar.x({ file: archive, cwd: destination, strict: true });
1109
1242
  return findFile(destination, executableName);
1110
1243
  }
1111
- const entries = import_fflate.unzipSync(await import_promises.readFile(archive), {
1112
- 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
1113
1246
  });
1114
- 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);
1115
1248
  if (matches.length !== 1 || !matches[0])
1116
1249
  return null;
1117
- const source = import_node_path5.default.join(destination, executableName);
1118
- 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" });
1119
1252
  return source;
1120
1253
  }
1121
1254
  async function publishUvAtomically(source, installDir, version, options) {
1122
- await import_promises.mkdir(installDir, { recursive: true });
1255
+ await import_promises2.mkdir(installDir, { recursive: true });
1123
1256
  const executable = uvExecutable(options, version);
1124
- 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`);
1125
1258
  try {
1126
- await import_promises.copyFile(source, staged);
1259
+ await import_promises2.copyFile(source, staged);
1127
1260
  if (process.platform !== "win32")
1128
- await import_promises.chmod(staged, 493);
1261
+ await import_promises2.chmod(staged, 493);
1129
1262
  const stagedUv = probeUv(staged, options);
1130
1263
  if (!stagedUv || stagedUv.version !== version) {
1131
1264
  throw new Error(`Downloaded uv executable is not version ${version}.`);
1132
1265
  }
1133
1266
  if (process.platform === "win32")
1134
- await import_promises.rm(executable, { force: true });
1135
- await import_promises.rename(staged, executable);
1267
+ await import_promises2.rm(executable, { force: true });
1268
+ await import_promises2.rename(staged, executable);
1136
1269
  } finally {
1137
- await import_promises.rm(staged, { force: true });
1270
+ await import_promises2.rm(staged, { force: true });
1138
1271
  }
1139
1272
  }
1140
1273
  function uvAsset() {
@@ -1159,7 +1292,7 @@ async function download(url, destination, timeoutMs, dispatcher) {
1159
1292
  await response.body?.cancel();
1160
1293
  throw new Error(`Could not download ${url}: HTTP ${response.status}`);
1161
1294
  }
1162
- 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" }));
1163
1296
  }
1164
1297
  async function downloadText(url, timeoutMs, dispatcher, headers) {
1165
1298
  const response = await import_undici.fetch(url, {
@@ -1175,28 +1308,28 @@ async function downloadText(url, timeoutMs, dispatcher, headers) {
1175
1308
  return response.text();
1176
1309
  }
1177
1310
  function proxyDispatcher(env) {
1178
- const httpProxy = environmentValue(env, "http_proxy", "HTTP_PROXY");
1179
- 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");
1180
1313
  if (!httpProxy && !httpsProxy)
1181
1314
  return;
1182
1315
  return new import_undici.EnvHttpProxyAgent({
1183
1316
  httpProxy,
1184
1317
  httpsProxy,
1185
- noProxy: environmentValue(env, "no_proxy", "NO_PROXY")
1318
+ noProxy: processEnvironmentValue(env, "no_proxy", "NO_PROXY")
1186
1319
  });
1187
1320
  }
1188
- function environmentValue(env, lowercase, uppercase) {
1321
+ function processEnvironmentValue(env, lowercase, uppercase) {
1189
1322
  return env[lowercase] ?? env[uppercase];
1190
1323
  }
1191
1324
  async function sha256File(file) {
1192
- const hash = import_node_crypto.createHash("sha256");
1325
+ const hash = import_node_crypto2.createHash("sha256");
1193
1326
  for await (const chunk of import_node_fs3.createReadStream(file))
1194
1327
  hash.update(chunk);
1195
1328
  return hash.digest("hex");
1196
1329
  }
1197
1330
  async function findFile(directory, name) {
1198
- for (const entry of await import_promises.readdir(directory, { withFileTypes: true })) {
1199
- 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);
1200
1333
  if (entry.isFile() && entry.name === name)
1201
1334
  return target;
1202
1335
  if (entry.isDirectory()) {
@@ -1212,21 +1345,21 @@ async function findFile(directory, name) {
1212
1345
  var import_node_child_process2 = require("node:child_process");
1213
1346
 
1214
1347
  // src/runtime-files.ts
1215
- var import_node_fs5 = __toESM(require("node:fs"), 1);
1216
- 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);
1217
1350
  function runtimeFile(name) {
1218
1351
  const moduleDirectory = __dirname;
1219
- const compiled = import_node_path6.default.join(moduleDirectory, `${name}.js`);
1220
- 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`);
1221
1354
  }
1222
1355
 
1223
1356
  // src/runners.ts
1224
1357
  async function uvSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
1225
1358
  const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1226
1359
  const resolved = resolveOptions(options);
1227
- const environment = uvCommandNeedsEnvironment(args) ? await ensureEnvironment(resolved) : undefined;
1228
- const uv = environment?.uv ?? await ensureUv(resolved);
1229
- 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);
1230
1363
  return import_node_child_process2.spawn(command.executable, command.args, {
1231
1364
  ...spawnOptions,
1232
1365
  env: command.env
@@ -1235,9 +1368,9 @@ async function uvSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
1235
1368
  function uvSpawnSync(args = [], spawnOptions = {}, pyfigOptions = {}) {
1236
1369
  const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1237
1370
  const resolved = resolveOptions(options);
1238
- const environment = uvCommandNeedsEnvironment(args) ? resolveSynchronously("environment", resolved) : undefined;
1239
- const uv = environment?.uv ?? resolveSynchronously("uv", resolved);
1240
- 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);
1241
1374
  return import_node_child_process2.spawnSync(command.executable, command.args, {
1242
1375
  ...spawnOptions,
1243
1376
  env: command.env
@@ -1247,7 +1380,7 @@ function buildUvCommand(uv, args, options, python) {
1247
1380
  return {
1248
1381
  executable: uv.executable,
1249
1382
  args: python ? targetUvArguments(args, python) : [...args],
1250
- env: uvCommandEnvironment(uvEnvironment(options), python)
1383
+ env: uvCommandProcessEnvironment(uvProcessEnvironment(options), python)
1251
1384
  };
1252
1385
  }
1253
1386
  function optionsForInvocation(pyfigOptions, spawnEnv) {
@@ -1280,20 +1413,35 @@ function synchronousResolutionTimeout(options) {
1280
1413
  }
1281
1414
 
1282
1415
  // src/client.ts
1283
- function createPyfig(options = {}) {
1284
- const scopedOptions = {
1285
- ...options,
1286
- env: options.env ? { ...options.env } : undefined
1287
- };
1288
- const baseEnvironment = import_loqkit2.memoize(() => ensureEnvironment(scopedOptions));
1289
- const uvInfo = import_loqkit2.memoize(() => ensureUv(scopedOptions));
1290
- return {
1291
- prepare: (dependencies = [], dependencyOptions = {}) => dependencies.length === 0 && !dependencyOptions.allExtras && (dependencyOptions.extras?.length ?? 0) === 0 && (dependencyOptions.groups?.length ?? 0) === 0 ? baseEnvironment() : ensureEnvironment(scopedOptions, dependencies, dependencyOptions),
1292
- pythonInfo: async () => (await baseEnvironment()).python,
1293
- uvInfo,
1294
- uv: (args = [], spawnOptions = {}) => uvSpawn(args, spawnOptions, scopedOptions),
1295
- uvSync: (args = [], spawnOptions = {}) => uvSpawnSync(args, spawnOptions, scopedOptions)
1296
- };
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
+ }
1297
1445
  }
1298
1446
 
1299
- //# debugId=9599078A7E0F681564756E2164756E21
1447
+ //# debugId=653756BC4A56AC4F64756E2164756E21