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