pyfig 0.6.3 → 0.7.1

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 (75) hide show
  1. package/README.md +77 -166
  2. package/SECURITY.md +16 -5
  3. package/dist/cjs/index.js +471 -980
  4. package/dist/cjs/index.js.map +6 -15
  5. package/dist/index.d.ts +2 -9
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -5
  8. package/dist/index.js.map +1 -1
  9. package/dist/process.d.ts +6 -12
  10. package/dist/process.d.ts.map +1 -1
  11. package/dist/process.js +35 -54
  12. package/dist/process.js.map +1 -1
  13. package/dist/pyfig.d.ts +31 -0
  14. package/dist/pyfig.d.ts.map +1 -0
  15. package/dist/pyfig.js +275 -0
  16. package/dist/pyfig.js.map +1 -0
  17. package/dist/uv.d.ts +3 -0
  18. package/dist/uv.d.ts.map +1 -0
  19. package/dist/uv.js +423 -0
  20. package/dist/uv.js.map +1 -0
  21. package/package.json +6 -12
  22. package/dist/cjs/sync-resolver.js +0 -1217
  23. package/dist/cjs/sync-resolver.js.map +0 -19
  24. package/dist/cli.d.ts +0 -3
  25. package/dist/cli.d.ts.map +0 -1
  26. package/dist/cli.js +0 -48
  27. package/dist/cli.js.map +0 -1
  28. package/dist/client.d.ts +0 -15
  29. package/dist/client.d.ts.map +0 -1
  30. package/dist/client.js +0 -25
  31. package/dist/client.js.map +0 -1
  32. package/dist/config.d.ts +0 -62
  33. package/dist/config.d.ts.map +0 -1
  34. package/dist/config.js +0 -112
  35. package/dist/config.js.map +0 -1
  36. package/dist/package-target.d.ts +0 -8
  37. package/dist/package-target.d.ts.map +0 -1
  38. package/dist/package-target.js +0 -81
  39. package/dist/package-target.js.map +0 -1
  40. package/dist/pep440.d.ts +0 -12
  41. package/dist/pep440.d.ts.map +0 -1
  42. package/dist/pep440.js +0 -21
  43. package/dist/pep440.js.map +0 -1
  44. package/dist/probes.d.ts +0 -37
  45. package/dist/probes.d.ts.map +0 -1
  46. package/dist/probes.js +0 -145
  47. package/dist/probes.js.map +0 -1
  48. package/dist/project-root.d.ts +0 -3
  49. package/dist/project-root.d.ts.map +0 -1
  50. package/dist/project-root.js +0 -22
  51. package/dist/project-root.js.map +0 -1
  52. package/dist/provision-lock.d.ts +0 -10
  53. package/dist/provision-lock.d.ts.map +0 -1
  54. package/dist/provision-lock.js +0 -24
  55. package/dist/provision-lock.js.map +0 -1
  56. package/dist/provision.d.ts +0 -26
  57. package/dist/provision.d.ts.map +0 -1
  58. package/dist/provision.js +0 -685
  59. package/dist/provision.js.map +0 -1
  60. package/dist/runners.d.ts +0 -14
  61. package/dist/runners.d.ts.map +0 -1
  62. package/dist/runners.js +0 -71
  63. package/dist/runners.js.map +0 -1
  64. package/dist/runtime-files.d.ts +0 -3
  65. package/dist/runtime-files.d.ts.map +0 -1
  66. package/dist/runtime-files.js +0 -12
  67. package/dist/runtime-files.js.map +0 -1
  68. package/dist/sync-resolver.d.ts +0 -2
  69. package/dist/sync-resolver.d.ts.map +0 -1
  70. package/dist/sync-resolver.js +0 -15
  71. package/dist/sync-resolver.js.map +0 -1
  72. package/dist/virtual-environment.d.ts +0 -11
  73. package/dist/virtual-environment.d.ts.map +0 -1
  74. package/dist/virtual-environment.js +0 -49
  75. package/dist/virtual-environment.js.map +0 -1
package/dist/cjs/index.js CHANGED
@@ -65,893 +65,184 @@ var __export = (target, all) => {
65
65
  // src/index.ts
66
66
  var exports_src = {};
67
67
  __export(exports_src, {
68
- DEFAULT_MANAGED_PYTHON: () => DEFAULT_MANAGED_PYTHON,
69
- DEFAULT_REQUIRES_PYTHON: () => DEFAULT_REQUIRES_PYTHON,
70
- DEFAULT_REQUIRES_UV: () => DEFAULT_REQUIRES_UV,
71
- createPyfig: () => createPyfig,
72
- ensureEnvironment: () => ensureEnvironment,
73
- ensureUv: () => ensureUv,
74
- resolveProjectRoot: () => resolveProjectRoot,
75
- uvSpawn: () => uvSpawn,
76
- uvSpawnSync: () => uvSpawnSync
68
+ PythonContext: () => PythonContext,
69
+ pyfig: () => pyfig
77
70
  });
78
71
  module.exports = __toCommonJS(exports_src);
79
72
 
80
- // src/config.ts
81
- var import_node_path2 = __toESM(require("node:path"), 1);
82
- var envPaths = __toESM(require("env-paths"), 1);
83
-
84
- // src/pep440.ts
85
- var import_pep440 = require("@renovatebot/pep440");
86
- function isValidPep440Specifier(specifier) {
87
- return import_pep440.validRange(specifier);
88
- }
89
- function normalizePep440Version(version) {
90
- return import_pep440.valid(version);
91
- }
92
- function pep440VersionSatisfies(version, specifier) {
93
- return import_pep440.filter([version], specifier).length === 1;
94
- }
95
- function maxPep440Version(versions, specifier) {
96
- return import_pep440.maxSatisfying([...versions], specifier);
97
- }
98
-
99
- // src/project-root.ts
100
- var import_node_fs = __toESM(require("node:fs"), 1);
101
- var import_node_path = __toESM(require("node:path"), 1);
102
- function resolveProjectRoot(startDirectory = process.cwd()) {
103
- const start = import_node_path.default.resolve(startDirectory);
104
- for (const directory of ancestors(start)) {
105
- if (import_node_fs.default.existsSync(import_node_path.default.join(directory, ".git")))
106
- return directory;
107
- }
108
- return start;
109
- }
110
- function* ancestors(startDirectory) {
111
- let directory = startDirectory;
112
- while (true) {
113
- yield directory;
114
- const parent = import_node_path.default.dirname(directory);
115
- if (parent === directory)
116
- return;
117
- directory = parent;
118
- }
119
- }
120
-
121
- // src/config.ts
122
- var DEFAULT_REQUIRES_PYTHON = ">=3.10,<4";
123
- var DEFAULT_MANAGED_PYTHON = "3.12";
124
- var DEFAULT_REQUIRES_UV = ">=0.10,<1";
125
- var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
126
- var DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60000;
127
- var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60000;
128
- var envPathsFunction = (() => {
129
- const imported = envPaths.default;
130
- const implementation = typeof imported === "function" ? imported : imported.default;
131
- if (!implementation)
132
- throw new TypeError("env-paths did not expose its default function.");
133
- return implementation;
134
- })();
135
- function resolveOptions(options = {}) {
136
- const env = { ...options.env ?? process.env };
137
- const requiresPython = options.requiresPython ?? env["PYFIG_REQUIRES_PYTHON"] ?? DEFAULT_REQUIRES_PYTHON;
138
- const requiresUv = options.requiresUv ?? env["PYFIG_REQUIRES_UV"] ?? DEFAULT_REQUIRES_UV;
139
- assertPep440Specifier("requires-python", requiresPython);
140
- assertPep440Specifier("requires-uv", requiresUv);
141
- const managedPython = options.managedPython ?? env["PYFIG_MANAGED_PYTHON"] ?? DEFAULT_MANAGED_PYTHON;
142
- assertManagedPythonRequest(managedPython, requiresPython);
143
- const probeTimeoutMs = resolveDuration("probeTimeoutMs", options.probeTimeoutMs, env["PYFIG_PROBE_TIMEOUT_MS"], DEFAULT_PROBE_TIMEOUT_MS);
144
- const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
145
- const downloadTimeoutMs = resolveDuration("downloadTimeoutMs", options.downloadTimeoutMs, env["PYFIG_DOWNLOAD_TIMEOUT_MS"], DEFAULT_DOWNLOAD_TIMEOUT_MS);
146
- const coordinationTimeoutMs = resolveDuration("coordinationTimeoutMs", options.coordinationTimeoutMs, env["PYFIG_COORDINATION_TIMEOUT_MS"], installTimeoutMs + downloadTimeoutMs + 60000);
147
- const configuredHome = options.cacheDir ?? env["PYFIG_HOME"];
148
- const cacheDir = configuredHome ? import_node_path2.default.resolve(configuredHome) : envPathsFunction("pyfig", { suffix: "" }).cache;
149
- const configuredProjectRoot = options.projectRoot ?? env["PYFIG_PROJECT_ROOT"];
150
- const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot(env["INIT_CWD"] ?? process.cwd());
151
- const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
152
- return {
153
- python: options.python ?? env["PYFIG_PYTHON"] ?? "",
154
- requiresPython,
155
- managedPython,
156
- requiresUv,
157
- requireEmbedding: options.requireEmbedding ?? parseBoolean(env["PYFIG_REQUIRE_EMBEDDING"], false),
158
- requireGil: options.requireGil ?? parseBoolean(env["PYFIG_REQUIRE_GIL"], false),
159
- autoInstall,
160
- installPython: options.installPython ?? parseBoolean(env["PYFIG_INSTALL_PYTHON"], autoInstall),
161
- installUv: options.installUv ?? parseBoolean(env["PYFIG_INSTALL_UV"], autoInstall),
162
- createVirtualEnvironment: options.createVirtualEnvironment ?? parseBoolean(env["PYFIG_CREATE_VIRTUAL_ENVIRONMENT"], autoInstall),
163
- cacheDir,
164
- projectRoot,
165
- probeTimeoutMs,
166
- installTimeoutMs,
167
- downloadTimeoutMs,
168
- coordinationTimeoutMs,
169
- env
170
- };
171
- }
172
- function uvEnvironment(options) {
173
- return {
174
- ...options.env,
175
- UV_PYTHON_INSTALL_DIR: import_node_path2.default.join(options.cacheDir, "python"),
176
- UV_CACHE_DIR: import_node_path2.default.join(options.cacheDir, "uv-cache"),
177
- UV_PYTHON_PREFERENCE: "system",
178
- UV_NO_CONFIG: "1"
179
- };
180
- }
181
- function assertPep440Specifier(name, specifier) {
182
- if (!isValidPep440Specifier(specifier)) {
183
- throw new TypeError(`Invalid ${name} PEP 440 specifier: ${specifier}`);
184
- }
185
- }
186
- function assertManagedPythonRequest(request, requiresPython) {
187
- if (!request || /[\\/\0\r\n]/u.test(request) || request.includes("..")) {
188
- throw new TypeError(`Invalid managed Python request: ${request}`);
189
- }
190
- if (/^\d+\.\d+(?:\.\d+)?$/u.test(request)) {
191
- const normalized = request.split(".").length === 2 ? `${request}.0` : request;
192
- if (!pep440VersionSatisfies(normalized, requiresPython)) {
193
- throw new TypeError(`Managed Python ${request} cannot satisfy requires-python ${requiresPython}.`);
194
- }
195
- }
196
- }
197
- function resolveDuration(name, explicit, environment, fallback, allowZero = false) {
198
- const value = explicit ?? (environment === undefined ? fallback : Number(environment));
199
- const minimum = allowZero ? 0 : 1;
200
- if (!Number.isSafeInteger(value) || value < minimum) {
201
- throw new TypeError(`${name} must be ${allowZero ? "a non-negative" : "a positive"} integer.`);
202
- }
203
- return value;
204
- }
205
- function parseBoolean(value, fallback) {
206
- if (value === undefined)
207
- return fallback;
208
- if (/^(1|true|yes|on)$/iu.test(value))
209
- return true;
210
- if (/^(0|false|no|off)$/iu.test(value))
211
- return false;
212
- throw new TypeError(`Invalid boolean value: ${value}`);
213
- }
214
- // src/client.ts
215
- var import_loqkit2 = require("loqkit");
216
-
217
- // src/provision.ts
218
- var import_node_crypto = require("node:crypto");
219
- 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);
222
- var import_node_os = __toESM(require("node:os"), 1);
223
- var import_node_path5 = __toESM(require("node:path"), 1);
224
- var import_node_stream = require("node:stream");
225
- var import_promises2 = require("node:stream/promises");
226
- var import_fflate = require("fflate");
227
- var tar = __toESM(require("tar"), 1);
228
- var import_undici = require("undici");
229
-
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
- }
253
-
254
- // src/virtual-environment.ts
255
- var import_node_fs2 = __toESM(require("node:fs"), 1);
73
+ // src/pyfig.ts
74
+ var import_node_child_process2 = require("node:child_process");
256
75
  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);
260
- }
261
- function externalVirtualEnvironmentDirectories(projectRoot, env) {
262
- 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
- return [...new Map(candidates.map((value) => [canonicalPath(value), value])).values()];
264
- }
265
- function virtualEnvironmentPythonExecutable(virtualEnvironment) {
266
- return import_node_path3.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
267
- }
268
- function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
269
- const activated = { ...env };
270
- const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
271
- const existingPath = pathEntry ? activated[pathEntry] : undefined;
272
- for (const key of Object.keys(activated)) {
273
- const normalized = key.toUpperCase();
274
- if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
275
- delete activated[key];
276
- }
277
- }
278
- activated["PATH"] = [import_node_path3.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path3.default.delimiter);
279
- activated["VIRTUAL_ENV"] = virtualEnvironment;
280
- return activated;
281
- }
282
- function pathsEqual(left, right) {
283
- return canonicalPath(left) === canonicalPath(right);
284
- }
285
- function canonicalPath(value) {
286
- const resolved = import_node_path3.default.resolve(value);
287
- let canonical = resolved;
288
- try {
289
- canonical = import_node_fs2.default.realpathSync.native(resolved);
290
- } catch {}
291
- return process.platform === "win32" ? canonical.toLowerCase() : canonical;
292
- }
293
-
294
- // src/package-target.ts
295
- var TARGET_OPTIONS = new Set([
296
- "--active",
297
- "--config-file",
298
- "--directory",
299
- "--isolated",
300
- "--prefix",
301
- "--project",
302
- "--python",
303
- "--root",
304
- "--system",
305
- "--target",
306
- "--user"
307
- ]);
308
- var TARGET_ENVIRONMENT_VARIABLES = new Set([
309
- "CONDA_PREFIX",
310
- "PIP_PREFIX",
311
- "PIP_PYTHON",
312
- "PIP_ROOT",
313
- "PIP_TARGET",
314
- "PIP_USER",
315
- "PYTHONHOME",
316
- "PYTHONUSERBASE",
317
- "UV_CONFIG_FILE",
318
- "UV_PROJECT",
319
- "UV_PROJECT_ENVIRONMENT",
320
- "UV_PYTHON",
321
- "UV_PREFIX",
322
- "UV_ROOT",
323
- "UV_SYSTEM_PYTHON",
324
- "UV_TARGET"
325
- ]);
326
- function uvCommandNeedsEnvironment(args) {
327
- return args[0] === "pip" || args[0] === "run";
328
- }
329
- function targetUvArguments(args, python) {
330
- if (!python.virtualEnvironment) {
331
- throw new Error("Targeted uv commands require a virtual environment.");
332
- }
333
- assertNoTargetOverride(args);
334
- if (args[0] === "run") {
335
- return ["run", "--no-project", "--python", python.executable, ...args.slice(1)];
336
- }
337
- if (args[0] === "pip" && args[1]) {
338
- return ["pip", args[1], "--python", python.executable, ...args.slice(2)];
339
- }
340
- return [...args];
341
- }
342
- function uvCommandEnvironment(env, python) {
343
- const sanitized = sanitizeUvEnvironment(env);
344
- if (!python?.virtualEnvironment)
345
- return sanitized;
346
- return {
347
- ...activateVirtualEnvironment(sanitized, python.virtualEnvironment, python.executable),
348
- UV_PYTHON: python.executable
349
- };
350
- }
351
- function sanitizeUvEnvironment(env) {
352
- const sanitized = { ...env };
353
- for (const key of Object.keys(sanitized)) {
354
- if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
355
- delete sanitized[key];
356
- }
357
- return sanitized;
358
- }
359
- function assertNoTargetOverride(args) {
360
- for (const argument of args.slice(1)) {
361
- if (argument === "--")
362
- return;
363
- const [name] = argument.split("=", 1);
364
- if (name && TARGET_OPTIONS.has(name)) {
365
- throw new Error(`${name} cannot override the environment selected by Pyfig.`);
366
- }
367
- if (argument === "-p" || argument.startsWith("-p=") || /^-p[^-]/u.test(argument)) {
368
- throw new Error("-p cannot override the environment selected by Pyfig.");
369
- }
370
- }
371
- }
76
+ var import_pep4402 = require("@renovatebot/pep440");
372
77
 
373
78
  // src/process.ts
374
79
  var import_node_child_process = require("node:child_process");
375
- var import_node_path4 = __toESM(require("node:path"), 1);
80
+ var import_node_path = __toESM(require("node:path"), 1);
376
81
  var import_which = __toESM(require("which"), 1);
377
- async function resolveExecutable(value, env) {
378
- if (!value)
379
- return null;
380
- if (import_node_path4.default.isAbsolute(value)) {
381
- return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
382
- }
383
- if (!/^[A-Za-z0-9_.-]+$/u.test(value))
384
- return null;
385
- return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
386
- }
387
- function run(executable, args, env, timeoutMs) {
388
- const result = import_node_child_process.spawnSync(executable, args, {
389
- encoding: "utf8",
390
- env,
391
- timeout: timeoutMs,
392
- maxBuffer: 4 * 1024 * 1024,
393
- windowsHide: true
82
+ var OUTPUT_LIMIT = 1024 * 1024;
83
+ async function findExecutable(requested, cwd, env) {
84
+ const candidate = import_node_path.default.isAbsolute(requested) || /[\\/]/u.test(requested) ? import_node_path.default.resolve(cwd, requested) : requested;
85
+ const resolved = await import_which.default(candidate, {
86
+ nothrow: true,
87
+ path: environmentValue(env, "PATH"),
88
+ pathExt: environmentValue(env, "PATHEXT")
394
89
  });
395
- if (result.status !== 0 || result.error) {
396
- 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}`);
398
- }
399
- return { stdout: result.stdout, stderr: result.stderr };
90
+ return resolved ? import_node_path.default.resolve(cwd, resolved) : null;
400
91
  }
401
- async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
92
+ async function capture(executable, args, cwd, env) {
402
93
  return new Promise((resolve, reject) => {
403
94
  const child = import_node_child_process.spawn(executable, args, {
404
- env,
405
95
  cwd,
96
+ env,
406
97
  stdio: ["ignore", "pipe", "pipe"],
407
98
  windowsHide: true
408
99
  });
409
100
  let stdout = "";
410
101
  let stderr = "";
411
102
  let settled = false;
412
- const finish = (error, result) => {
103
+ const finish = (error) => {
413
104
  if (settled)
414
105
  return;
415
106
  settled = true;
416
- clearTimeout(timer);
417
- signal?.removeEventListener("abort", abort);
418
107
  if (error)
419
108
  reject(error);
420
- else if (result)
421
- resolve(result);
422
- };
423
- const abort = () => {
424
- child.kill();
425
- const reason = signal?.reason;
426
- finish(reason instanceof Error ? reason : new Error(`${import_node_path4.default.basename(executable)} was aborted.`));
109
+ else
110
+ resolve(stdout);
427
111
  };
428
- const timer = setTimeout(() => {
429
- child.kill();
430
- finish(new Error(`${import_node_path4.default.basename(executable)} timed out after ${timeoutMs}ms.`));
431
- }, timeoutMs);
432
112
  child.stdout.setEncoding("utf8");
433
113
  child.stderr.setEncoding("utf8");
434
114
  child.stdout.on("data", (chunk) => {
435
115
  stdout += chunk;
436
- if (stdout.length > 4 * 1024 * 1024) {
116
+ if (stdout.length > OUTPUT_LIMIT) {
437
117
  child.kill();
438
- finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the output limit.`));
118
+ finish(new Error(`${import_node_path.default.basename(executable)} exceeded its stdout limit.`));
439
119
  }
440
120
  });
441
121
  child.stderr.on("data", (chunk) => {
442
122
  stderr += chunk;
443
- if (stderr.length > 4 * 1024 * 1024) {
123
+ if (stderr.length > OUTPUT_LIMIT) {
444
124
  child.kill();
445
- finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the error-output limit.`));
125
+ finish(new Error(`${import_node_path.default.basename(executable)} exceeded its stderr limit.`));
446
126
  }
447
127
  });
448
128
  child.once("error", (error) => finish(error));
449
- child.once("close", (code, childSignal) => {
450
- if (code !== 0) {
451
- 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}`));
129
+ child.once("close", (code, signal) => {
130
+ if (code === 0) {
131
+ finish();
453
132
  return;
454
133
  }
455
- finish(undefined, { stdout, stderr });
134
+ const detail = stderr.trim() || (signal ? `signal ${signal}` : `exit ${code}`);
135
+ finish(new Error(`${import_node_path.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
456
136
  });
457
- if (signal?.aborted)
458
- abort();
459
- else
460
- signal?.addEventListener("abort", abort, { once: true });
461
137
  });
462
138
  }
463
-
464
- // src/probes.ts
465
- var PYTHON_PROBE = `
466
- import json
467
- import os
468
- import platform
469
- import struct
470
- import sys
471
- import sysconfig
472
-
473
- base_prefix = getattr(sys, "base_prefix", sys.prefix)
474
- include_directory = sysconfig.get_path("include") or ""
475
- header_path = os.path.join(include_directory, "Python.h") if include_directory else ""
476
- library_candidates = []
477
- stable_abi_library_candidates = []
478
- if os.name == "nt":
479
- library_directory = sysconfig.get_config_var("LIBDIR") or os.path.join(base_prefix, "libs")
480
- configured_library = sysconfig.get_config_var("LDLIBRARY") or ""
481
- if configured_library:
482
- library_candidates.append(os.path.join(library_directory, os.path.splitext(configured_library)[0] + ".lib"))
483
- library_candidates.append(os.path.join(library_directory, f"python{sys.version_info.major}{sys.version_info.minor}.lib"))
484
- stable_abi_library_candidates.append(os.path.join(library_directory, "python3.lib"))
485
- else:
486
- library_directories = [
487
- sysconfig.get_config_var("LIBDIR"),
488
- sysconfig.get_config_var("LIBPL"),
489
- os.path.join(base_prefix, "lib"),
490
- ]
491
- library_names = [
492
- sysconfig.get_config_var("LDLIBRARY"),
493
- sysconfig.get_config_var("LIBRARY"),
494
- ]
495
- for directory in library_directories:
496
- for name in library_names:
497
- if directory and name:
498
- library_candidates.append(os.path.join(directory, name))
499
- if directory:
500
- stable_abi_library_candidates.extend([
501
- os.path.join(directory, "libpython3.so"),
502
- os.path.join(directory, "libpython3.dylib"),
503
- ])
504
-
505
- library_path = next((candidate for candidate in library_candidates if os.path.isfile(candidate)), "")
506
- stable_abi_library_path = next(
507
- (candidate for candidate in stable_abi_library_candidates if os.path.isfile(candidate)),
508
- None,
509
- )
510
- embedding = None
511
- if os.path.isfile(header_path) and library_path:
512
- library_name = os.path.basename(library_path)
513
- library_kind = "import" if os.name == "nt" else ("static" if library_name.endswith(".a") else "shared")
514
- embedding = {
515
- "includeDirectory": include_directory,
516
- "headerPath": header_path,
517
- "libraryPath": library_path,
518
- "libraryDirectory": os.path.dirname(library_path),
519
- "libraryName": library_name,
520
- "libraryKind": library_kind,
521
- "stableAbiLibraryPath": stable_abi_library_path,
522
- }
523
-
524
- release_level = sys.version_info.releaselevel
525
- release_suffixes = {
526
- "alpha": "a",
527
- "beta": "b",
528
- "candidate": "rc",
529
- "final": "",
530
- }
531
- release_suffix = release_suffixes[release_level]
532
- if release_suffix:
533
- release_suffix += str(sys.version_info.serial)
534
-
535
- print(json.dumps({
536
- "executable": sys.executable,
537
- "baseExecutable": getattr(sys, "_base_executable", sys.executable),
538
- "version": ".".join(map(str, sys.version_info[:3])) + release_suffix,
539
- "major": sys.version_info.major,
540
- "minor": sys.version_info.minor,
541
- "patch": sys.version_info.micro,
542
- "releaseLevel": release_level,
543
- "implementation": sys.implementation.name,
544
- "architecture": platform.machine().lower(),
545
- "pointerBits": struct.calcsize("P") * 8,
546
- "prefix": sys.prefix,
547
- "basePrefix": base_prefix,
548
- "virtualEnvironment": sys.prefix if sys.prefix != base_prefix else None,
549
- "gilDisabled": bool(sysconfig.get_config_var("Py_GIL_DISABLED")),
550
- "embedding": embedding,
551
- }))
552
- `;
553
- function pythonSatisfiesPolicy(python, options) {
554
- return pep440VersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
555
- }
556
- async function probePython(candidate, options) {
557
- const executable = await resolveExecutable(candidate.executable, options.env);
558
- if (!executable)
559
- return null;
560
- try {
561
- const { stdout } = run(executable, [...candidate.args, "-c", PYTHON_PROBE], options.env, options.probeTimeoutMs);
562
- const info = JSON.parse(stdout.trim());
563
- return normalizePep440Version(info.version) ? info : null;
564
- } catch {
565
- return null;
566
- }
567
- }
568
- async function findUv(options) {
569
- const configured = options.env["PYFIG_UV"] ?? "uv";
570
- const executable = await resolveExecutable(configured, options.env);
571
- const uv = executable ? probeUv(executable, options) : null;
572
- return uv && pep440VersionSatisfies(uv.version, options.requiresUv) ? uv : null;
573
- }
574
- function probeUv(executable, options) {
575
- try {
576
- const { stdout } = run(executable, ["--version"], options.env, options.probeTimeoutMs);
577
- const version = normalizeVersion(stdout.match(/^uv\s+(\S+)/iu)?.[1]);
578
- return version ? { executable, version } : null;
579
- } catch {
580
- return null;
581
- }
582
- }
583
- function normalizeVersion(value) {
584
- if (!value)
585
- return null;
586
- return normalizePep440Version(value);
587
- }
588
- function pythonArchitectureMatchesRuntime(python) {
589
- const architecture = python.architecture.toLowerCase();
590
- if (python.pointerBits !== (process.arch === "ia32" ? 32 : 64))
591
- return false;
592
- if (process.arch === "x64")
593
- return architecture === "x86_64" || architecture === "amd64";
594
- if (process.arch === "arm64")
595
- return architecture === "aarch64" || architecture === "arm64";
596
- if (process.arch === "ia32") {
597
- return architecture === "x86" || architecture === "i386" || architecture === "i686";
598
- }
599
- return architecture === process.arch;
139
+ function environmentValue(env, name) {
140
+ if (env[name] !== undefined || process.platform !== "win32")
141
+ return env[name];
142
+ const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
143
+ return key ? env[key] : undefined;
600
144
  }
601
145
 
602
- // src/provision.ts
603
- async function ensureUv(options = {}) {
604
- return ensureUvResolved(resolveOptions(options));
605
- }
606
- async function ensureEnvironment(options = {}, dependencies = [], dependencyOptions = {}) {
607
- const resolved = resolveOptions(options);
608
- const normalized = normalizeDependencies(dependencies);
609
- const selections = normalizeDependencyOptions(dependencyOptions);
610
- 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);
614
- }
615
- return {
616
- python,
617
- uv,
618
- managed: isManagedEnvironment(python, resolved)
146
+ // src/uv.ts
147
+ var import_node_crypto = require("node:crypto");
148
+ var import_node_fs = require("node:fs");
149
+ var import_promises = require("node:fs/promises");
150
+ var import_node_os = __toESM(require("node:os"), 1);
151
+ var import_node_path2 = __toESM(require("node:path"), 1);
152
+ var import_node_stream = require("node:stream");
153
+ var import_promises2 = require("node:stream/promises");
154
+ var import_pep440 = require("@renovatebot/pep440");
155
+ var import_fflate = require("fflate");
156
+ var import_loqkit = require("loqkit");
157
+ var tar = __toESM(require("tar"), 1);
158
+ var import_undici = require("undici");
159
+ var RELEASE_PAGE_SIZE = 100;
160
+ var RELEASE_MAX_PAGES = 20;
161
+ var RELEASE_METADATA_TTL_MS = 24 * 60 * 60000;
162
+ var METADATA_TIMEOUT_MS = 30000;
163
+ var DOWNLOAD_TIMEOUT_MS = 5 * 60000;
164
+ var COORDINATION_TIMEOUT_MS = 15 * 60000;
165
+ async function resolveUv(specifier, cwd, env) {
166
+ const context = {
167
+ cache: uvCacheDirectory(env),
168
+ cwd,
169
+ env,
170
+ specifier
619
171
  };
620
- }
621
- async function ensureVirtualEnvironmentResolved(options, uv) {
622
- const existing = await findReusableEnvironment(options);
172
+ const existing = await findAvailableUv(context);
623
173
  if (existing)
624
174
  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
- if (!options.createVirtualEnvironment) {
631
- throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
632
- }
633
- const basePython = await ensureBasePythonResolved(uv, options);
175
+ const policy = import_node_crypto.createHash("sha256").update(specifier).digest("hex");
634
176
  return withProvisioningLock({
635
- key: `venv:${canonicalPath(directory)}`,
636
- timeoutMs: options.coordinationTimeoutMs,
637
- check,
177
+ key: `uv-policy:${canonicalPath(context.cache)}:${policy}`,
178
+ check: () => findAvailableUv(context),
638
179
  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.`);
653
- }
654
- return installed;
655
- }
656
- });
657
- }
658
- async function findReusableEnvironment(options) {
659
- if (options.python) {
660
- const configured = await probePython({ executable: options.python, args: [] }, options);
661
- if (!configured || !pythonSatisfiesPolicy(configured, options)) {
662
- throw incompatibleConfiguredPython(options);
663
- }
664
- if (configured.virtualEnvironment)
665
- return configured;
666
- }
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)) {
673
- return info;
674
- }
675
- }
676
- return null;
677
- }
678
- async function ensureBasePythonResolved(uv, options) {
679
- if (options.python) {
680
- const configured = await probePython({ executable: options.python, args: [] }, options);
681
- if (!configured || !pythonSatisfiesPolicy(configured, options)) {
682
- throw incompatibleConfiguredPython(options);
683
- }
684
- if (!configured.virtualEnvironment)
685
- return configured;
686
- const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
687
- if (base && pythonSatisfiesPolicy(base, options))
688
- return base;
689
- throw incompatibleConfiguredPython(options);
690
- }
691
- const system = await findPythonWithUv(uv, options, "system");
692
- if (system)
693
- return system;
694
- const cachedManaged = await findPythonWithUv(uv, options, "managed");
695
- if (cachedManaged)
696
- return cachedManaged;
697
- if (!options.installPython) {
698
- throw new Error(missingPythonMessage(options, "managed Python installation is disabled"));
699
- }
700
- return withProvisioningLock({
701
- key: `python:${canonicalPath(import_node_path5.default.join(options.cacheDir, "python"))}`,
702
- timeoutMs: options.coordinationTimeoutMs,
703
- check: () => findPythonWithUv(uv, options, "managed"),
704
- mutate: async () => {
705
- await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], isolatedUvEnvironment(options), options.installTimeoutMs);
706
- const installed = await findPythonWithUv(uv, options, "managed");
707
- if (!installed) {
708
- throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
709
- }
710
- return installed;
711
- }
712
- });
713
- }
714
- async function findPythonWithUv(uv, options, source) {
715
- for (const request of ["", options.managedPython]) {
716
- const args = [
717
- "python",
718
- "find",
719
- "--no-project",
720
- "--no-config",
721
- "--no-python-downloads",
722
- source === "system" ? "--system" : "--managed-python"
723
- ];
724
- if (request)
725
- args.push(request);
726
- try {
727
- const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
728
- const info = await probePython({ executable: stdout.trim(), args: [] }, options);
729
- if (info && pythonSatisfiesPolicy(info, options))
730
- return info;
731
- } catch {}
732
- }
733
- try {
734
- const { stdout } = await runAsync(uv.executable, [
735
- "python",
736
- "list",
737
- "--no-config",
738
- "--no-python-downloads",
739
- "--only-installed",
740
- "--output-format",
741
- "json",
742
- source === "system" ? "--no-managed-python" : "--managed-python"
743
- ], explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
744
- const candidates = installedPythonPaths(stdout);
745
- for (const executable of candidates) {
746
- const info = await probePython({ executable, args: [] }, options);
747
- if (info && !info.virtualEnvironment && pythonSatisfiesPolicy(info, options)) {
748
- return info;
749
- }
750
- }
751
- } catch {}
752
- return null;
753
- }
754
- function installedPythonPaths(output) {
755
- const parsed = JSON.parse(output);
756
- if (!Array.isArray(parsed))
757
- return [];
758
- const paths = parsed.flatMap((entry) => {
759
- if (!entry || typeof entry !== "object" || !("path" in entry))
760
- return [];
761
- const candidate = entry.path;
762
- return typeof candidate === "string" && candidate ? [candidate] : [];
763
- });
764
- return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
765
- }
766
- async function inspectVirtualEnvironment(directory, options) {
767
- const executable = virtualEnvironmentPythonExecutable(directory);
768
- const info = await probePython({ executable, args: [] }, options);
769
- return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
770
- }
771
- async function installRuntimeDependencies(uv, python, dependencies, dependencyOptions, options) {
772
- if (!python.virtualEnvironment) {
773
- throw new Error("Runtime dependency installation requires a virtual environment.");
774
- }
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(`
792
- `)}
793
- `, "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;
817
- }
818
- });
819
- }
820
- async function ensureUvResolved(options) {
821
- const existing = await findAvailableUv(options);
822
- if (existing)
823
- return existing;
824
- if (!options.installUv) {
825
- throw new Error(`Compatible uv is unavailable (requires-uv ${options.requiresUv}), and uv installation is disabled.`);
826
- }
827
- const policyKey = import_node_crypto.createHash("sha256").update(options.requiresUv).digest("hex");
828
- return withProvisioningLock({
829
- key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
830
- timeoutMs: options.coordinationTimeoutMs,
831
- check: () => findAvailableUv(options),
832
- mutate: async () => {
833
- const release = await resolveUvRelease(options);
834
- const installDir = uvInstallDirectory(options, release.version);
835
- const executable = uvExecutable(options, release.version);
180
+ const release = await resolveUvRelease(context);
181
+ const destination = uvInstallDirectory(context, release.version);
836
182
  return withProvisioningLock({
837
- key: `uv-release:${canonicalPath(installDir)}`,
838
- timeoutMs: options.coordinationTimeoutMs,
839
- check: () => findAvailableUv(options),
183
+ key: `uv-release:${canonicalPath(destination)}`,
184
+ check: () => probeUv(uvExecutable(context, release.version), context, release.version),
840
185
  mutate: async () => {
841
- await installUv(installDir, release, options);
842
- const result = probeUv(executable, options);
843
- if (!result || result.version !== release.version) {
844
- throw new Error(`Expected managed uv ${release.version}, but the installed executable did not match.`);
186
+ await installUv(release, context);
187
+ const installed = await probeUv(uvExecutable(context, release.version), context, release.version);
188
+ if (!installed) {
189
+ throw new Error(`Managed uv ${release.version} failed its post-install probe.`);
845
190
  }
846
- return result;
191
+ return installed;
847
192
  }
848
193
  });
849
194
  }
850
195
  });
851
196
  }
852
- async function findAvailableUv(options) {
853
- const external = await findUv(options);
854
- if (external)
197
+ async function findAvailableUv(context) {
198
+ const external = await findExecutable("uv", context.cwd, context.env);
199
+ if (external && await probeUv(external, context))
855
200
  return external;
856
- return findManagedUv(options);
201
+ return findCachedUv(context);
857
202
  }
858
- async function findManagedUv(options) {
859
- const entries = await import_promises.readdir(uvInstallRoot(options), { withFileTypes: true }).catch(() => []);
860
- let versions = entries.filter((entry) => entry.isDirectory() && normalizePep440Version(entry.name) === entry.name).map((entry) => entry.name);
203
+ async function findCachedUv(context) {
204
+ const root = uvInstallRoot(context);
205
+ const entries = await import_promises.readdir(root, { withFileTypes: true }).catch(() => []);
206
+ let versions = entries.filter((entry) => entry.isDirectory() && import_pep440.valid(entry.name) === entry.name).map((entry) => entry.name);
861
207
  while (versions.length > 0) {
862
- const selected = maxPep440Version(versions, options.requiresUv);
863
- if (!selected)
208
+ const version = import_pep440.maxSatisfying(versions, context.specifier);
209
+ if (!version)
864
210
  return null;
865
- const candidate = probeUv(uvExecutable(options, selected), options);
866
- if (candidate?.version === selected)
867
- return candidate;
868
- versions = versions.filter((version) => version !== selected);
211
+ const executable = await probeUv(uvExecutable(context, version), context, version);
212
+ if (executable)
213
+ return executable;
214
+ versions = versions.filter((candidate) => candidate !== version);
869
215
  }
870
216
  return null;
871
217
  }
872
- function normalizeDependencies(dependencies) {
873
- const normalized = dependencies.map((dependency) => dependency.trim());
874
- for (const dependency of normalized) {
875
- if (!dependency || /[\0\r\n]/u.test(dependency)) {
876
- throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
877
- }
878
- }
879
- return [...new Set(normalized)];
880
- }
881
- function normalizeDependencyOptions(options) {
882
- const extras = normalizeNames("extra", options.extras ?? []);
883
- const groups = normalizeNames("dependency group", options.groups ?? []);
884
- if (options.allExtras && extras.length > 0) {
885
- throw new TypeError("allExtras cannot be combined with explicit extras.");
886
- }
887
- return { extras, groups, allExtras: options.allExtras ?? false };
888
- }
889
- function normalizeNames(kind, names) {
890
- const normalized = names.map((name) => name.trim());
891
- for (const name of normalized) {
892
- if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
893
- throw new TypeError(`Invalid ${kind}: ${JSON.stringify(name)}`);
894
- }
895
- }
896
- return [...new Set(normalized)];
897
- }
898
- function isManagedEnvironment(python, options) {
899
- return Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(options.projectRoot));
900
- }
901
- function isolatedUvEnvironment(options) {
902
- return sanitizeUvEnvironment(uvEnvironment(options));
903
- }
904
- function explicitPythonSourceEnvironment(options) {
905
- const env = isolatedUvEnvironment(options);
906
- delete env["UV_PYTHON_PREFERENCE"];
907
- return env;
908
- }
909
- function targetUvEnvironment(options, python) {
910
- return uvCommandEnvironment(uvEnvironment(options), python);
911
- }
912
- function incompatibleConfiguredPython(options) {
913
- return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `requires-python ${options.requiresPython}` + `${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
914
- }
915
- function missingPythonMessage(options, reason) {
916
- const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
917
- return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
918
- }
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;
218
+ async function probeUv(executable, context, expectedVersion) {
219
+ try {
220
+ const output = await capture(executable, ["--version"], context.cwd, context.env);
221
+ const version = import_pep440.valid(output.match(/^uv\s+(\S+)/iu)?.[1] ?? "");
222
+ return version && (!expectedVersion || version === expectedVersion) && import_pep440.maxSatisfying([version], context.specifier) ? executable : null;
223
+ } catch {
224
+ return null;
927
225
  }
928
- const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
929
- return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
930
226
  }
931
- var UV_RELEASE_PAGE_SIZE = 100;
932
- var UV_RELEASE_MAX_PAGES = 20;
933
- var UV_RELEASE_METADATA_TTL_MS = 24 * 60 * 60000;
934
- async function resolveUvRelease(options) {
935
- const catalog = await ensureUvReleaseCatalog(options);
227
+ async function resolveUvRelease(context) {
228
+ const catalog = await ensureUvReleaseCatalog(context);
936
229
  const asset = uvAsset();
937
- const releases = catalog.releases.filter((release) => release.assets.includes(asset) && release.assets.includes(`${asset}.sha256`));
938
- const version = maxPep440Version(releases.map((release) => release.version), options.requiresUv);
939
- const release = releases.find((candidate) => candidate.version === version);
940
- if (!release) {
941
- throw new Error(`No published uv release satisfies requires-uv ${options.requiresUv}.`);
942
- }
230
+ const candidates = catalog.releases.filter((release) => release.assets.includes(asset) && release.assets.includes(`${asset}.sha256`));
231
+ const version = import_pep440.maxSatisfying(candidates.map((release) => release.version), context.specifier);
232
+ const release = candidates.find((candidate) => candidate.version === version);
233
+ if (!release)
234
+ throw new Error(`No published uv release satisfies ${context.specifier}.`);
943
235
  return release;
944
236
  }
945
- async function ensureUvReleaseCatalog(options) {
946
- const file = uvReleaseMetadataFile(options);
237
+ async function ensureUvReleaseCatalog(context) {
238
+ const file = uvReleaseMetadataFile(context);
947
239
  const stale = await readUvReleaseCatalog(file);
948
240
  try {
949
241
  return await withProvisioningLock({
950
242
  key: `uv-metadata:${canonicalPath(file)}`,
951
- timeoutMs: options.coordinationTimeoutMs,
952
243
  check: () => readFreshUvReleaseCatalog(file),
953
244
  mutate: async () => {
954
- const catalog = await fetchUvReleaseCatalog(options);
245
+ const catalog = await fetchUvReleaseCatalog(context.env);
955
246
  await writeUvReleaseCatalog(file, catalog);
956
247
  return catalog;
957
248
  }
@@ -963,30 +254,32 @@ async function ensureUvReleaseCatalog(options) {
963
254
  throw error;
964
255
  }
965
256
  }
966
- async function fetchUvReleaseCatalog(options) {
967
- const dispatcher = proxyDispatcher(options.env);
257
+ async function fetchUvReleaseCatalog(env) {
258
+ const dispatcher = proxyDispatcher(env);
968
259
  const releases = new Map;
969
260
  let exhausted = false;
970
261
  try {
971
- 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, githubApiHeaders(options.env));
262
+ for (let page = 1;page <= RELEASE_MAX_PAGES; page += 1) {
263
+ const text = await downloadText(`https://api.github.com/repos/astral-sh/uv/releases?per_page=${RELEASE_PAGE_SIZE}&page=${page}`, METADATA_TIMEOUT_MS, dispatcher, githubApiHeaders(env));
973
264
  const payload = JSON.parse(text);
974
- if (!Array.isArray(payload)) {
265
+ if (!Array.isArray(payload))
975
266
  throw new Error("GitHub returned invalid uv release metadata.");
976
- }
977
267
  for (const candidate of payload) {
978
268
  if (!candidate || typeof candidate !== "object")
979
269
  continue;
980
- const release = candidate;
981
- if (release.draft === true || typeof release.tag_name !== "string")
270
+ const value = candidate;
271
+ if (value.draft === true || typeof value.tag_name !== "string")
982
272
  continue;
983
- const assetNames = releaseAssetNames(release.assets);
984
- const version = normalizePep440Version(release.tag_name);
273
+ const version = import_pep440.valid(value.tag_name);
985
274
  if (version) {
986
- releases.set(version, { assets: assetNames, tag: release.tag_name, version });
275
+ releases.set(version, {
276
+ assets: releaseAssetNames(value.assets),
277
+ tag: value.tag_name,
278
+ version
279
+ });
987
280
  }
988
281
  }
989
- if (payload.length < UV_RELEASE_PAGE_SIZE) {
282
+ if (payload.length < RELEASE_PAGE_SIZE) {
990
283
  exhausted = true;
991
284
  break;
992
285
  }
@@ -994,16 +287,14 @@ async function fetchUvReleaseCatalog(options) {
994
287
  } finally {
995
288
  await dispatcher?.close();
996
289
  }
997
- if (!exhausted) {
998
- throw new Error(`uv release catalog exceeded ${UV_RELEASE_MAX_PAGES} pages.`);
999
- }
1000
- if (releases.size === 0) {
290
+ if (!exhausted)
291
+ throw new Error(`uv release catalog exceeded ${RELEASE_MAX_PAGES} pages.`);
292
+ if (releases.size === 0)
1001
293
  throw new Error("GitHub returned no usable uv releases.");
1002
- }
1003
294
  return { fetchedAt: Date.now(), releases: [...releases.values()] };
1004
295
  }
1005
296
  function githubApiHeaders(env) {
1006
- const token = [env["GITHUB_TOKEN"], env["GH_TOKEN"]].find((value) => typeof value === "string" && value.trim().length > 0);
297
+ const token = [environmentValue(env, "GITHUB_TOKEN"), environmentValue(env, "GH_TOKEN")].find((value) => typeof value === "string" && value.trim().length > 0);
1007
298
  if (token && /[\r\n]/u.test(token)) {
1008
299
  throw new TypeError("GitHub API token must not contain line breaks.");
1009
300
  }
@@ -1019,7 +310,7 @@ async function readFreshUvReleaseCatalog(file) {
1019
310
  if (!catalog)
1020
311
  return null;
1021
312
  const age = Date.now() - catalog.fetchedAt;
1022
- return age >= 0 && age < UV_RELEASE_METADATA_TTL_MS ? catalog : null;
313
+ return age >= 0 && age < RELEASE_METADATA_TTL_MS ? catalog : null;
1023
314
  }
1024
315
  async function readUvReleaseCatalog(file) {
1025
316
  try {
@@ -1032,17 +323,15 @@ function parseUvReleaseCatalog(value) {
1032
323
  if (!value || typeof value !== "object")
1033
324
  return null;
1034
325
  const catalog = value;
1035
- if (!Number.isSafeInteger(catalog.fetchedAt) || catalog.fetchedAt < 0 || !Array.isArray(catalog.releases)) {
326
+ if (!Number.isSafeInteger(catalog.fetchedAt) || catalog.fetchedAt < 0 || !Array.isArray(catalog.releases))
1036
327
  return null;
1037
- }
1038
328
  const releases = [];
1039
- for (const value of catalog.releases) {
1040
- if (!value || typeof value !== "object")
329
+ for (const candidate of catalog.releases) {
330
+ if (!candidate || typeof candidate !== "object")
1041
331
  return null;
1042
- const release = value;
1043
- if (!Array.isArray(release.assets) || !release.assets.every((asset) => typeof asset === "string") || typeof release.tag !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/u.test(release.tag) || typeof release.version !== "string" || normalizePep440Version(release.version) !== release.version) {
332
+ const release = candidate;
333
+ if (!Array.isArray(release.assets) || !release.assets.every((asset) => typeof asset === "string") || typeof release.tag !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/u.test(release.tag) || typeof release.version !== "string" || import_pep440.valid(release.version) !== release.version)
1044
334
  return null;
1045
- }
1046
335
  releases.push({
1047
336
  assets: release.assets,
1048
337
  tag: release.tag,
@@ -1052,8 +341,8 @@ function parseUvReleaseCatalog(value) {
1052
341
  return { fetchedAt: catalog.fetchedAt, releases };
1053
342
  }
1054
343
  async function writeUvReleaseCatalog(file, catalog) {
1055
- await import_promises.mkdir(import_node_path5.default.dirname(file), { recursive: true });
1056
- const staged = import_node_path5.default.join(import_node_path5.default.dirname(file), `.${import_node_path5.default.basename(file)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
344
+ await import_promises.mkdir(import_node_path2.default.dirname(file), { recursive: true });
345
+ const staged = import_node_path2.default.join(import_node_path2.default.dirname(file), `.${import_node_path2.default.basename(file)}.${uniqueSuffix()}.tmp`);
1057
346
  try {
1058
347
  await import_promises.writeFile(staged, `${JSON.stringify(catalog)}
1059
348
  `, { encoding: "utf8", flag: "wx" });
@@ -1064,47 +353,25 @@ async function writeUvReleaseCatalog(file, catalog) {
1064
353
  await import_promises.rm(staged, { force: true });
1065
354
  }
1066
355
  }
1067
- function uvReleaseMetadataFile(options) {
1068
- return import_node_path5.default.join(options.cacheDir, "metadata", "uv-releases.json");
1069
- }
1070
- function releaseAssetNames(value) {
1071
- if (!Array.isArray(value))
1072
- return [];
1073
- return value.flatMap((entry) => {
1074
- if (!entry || typeof entry !== "object")
1075
- return [];
1076
- const name = entry.name;
1077
- return typeof name === "string" ? [name] : [];
1078
- });
1079
- }
1080
- function uvInstallRoot(options) {
1081
- return import_node_path5.default.join(options.cacheDir, "tools", "uv");
1082
- }
1083
- function uvInstallDirectory(options, version) {
1084
- return import_node_path5.default.join(uvInstallRoot(options), version);
1085
- }
1086
- function uvExecutable(options, version) {
1087
- return import_node_path5.default.join(uvInstallDirectory(options, version), process.platform === "win32" ? "uv.exe" : "uv");
1088
- }
1089
- async function installUv(installDir, release, options) {
356
+ async function installUv(release, context) {
1090
357
  const asset = uvAsset();
1091
358
  const baseUrl = `https://github.com/astral-sh/uv/releases/download/${release.tag}`;
1092
- const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
1093
- const dispatcher = proxyDispatcher(options.env);
359
+ const temporary = await import_promises.mkdtemp(import_node_path2.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
360
+ const dispatcher = proxyDispatcher(context.env);
1094
361
  try {
1095
- const archive = import_node_path5.default.join(temporary, asset);
1096
- await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
1097
- const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
362
+ const archive = import_node_path2.default.join(temporary, asset);
363
+ await download(`${baseUrl}/${asset}`, archive, dispatcher);
364
+ const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, DOWNLOAD_TIMEOUT_MS, dispatcher)).trim().split(/\s+/u)[0];
1098
365
  const actual = await sha256File(archive);
1099
366
  if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
1100
- throw new Error(`uv archive checksum mismatch for ${asset}`);
367
+ throw new Error(`uv archive checksum mismatch for ${asset}.`);
1101
368
  }
1102
- const extracted = import_node_path5.default.join(temporary, "extracted");
369
+ const extracted = import_node_path2.default.join(temporary, "extracted");
1103
370
  await import_promises.mkdir(extracted, { recursive: true });
1104
371
  const source = await extractUvExecutable(archive, asset, extracted);
1105
372
  if (!source)
1106
- throw new Error(`uv archive did not contain the expected executable: ${asset}`);
1107
- await publishUvAtomically(source, installDir, release.version, options);
373
+ throw new Error(`uv archive did not contain the expected executable: ${asset}.`);
374
+ await publishUv(source, release.version, context);
1108
375
  } finally {
1109
376
  await dispatcher?.close();
1110
377
  await import_promises.rm(temporary, { recursive: true, force: true });
@@ -1117,25 +384,25 @@ async function extractUvExecutable(archive, asset, destination) {
1117
384
  return findFile(destination, executableName);
1118
385
  }
1119
386
  const entries = import_fflate.unzipSync(await import_promises.readFile(archive), {
1120
- filter: ({ name }) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName
387
+ filter: ({ name }) => import_node_path2.default.posix.basename(name.replaceAll("\\", "/")) === executableName
1121
388
  });
1122
- const matches = Object.entries(entries).filter(([name]) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
389
+ const matches = Object.entries(entries).filter(([name]) => import_node_path2.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
1123
390
  if (matches.length !== 1 || !matches[0])
1124
391
  return null;
1125
- const source = import_node_path5.default.join(destination, executableName);
392
+ const source = import_node_path2.default.join(destination, executableName);
1126
393
  await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
1127
394
  return source;
1128
395
  }
1129
- async function publishUvAtomically(source, installDir, version, options) {
1130
- await import_promises.mkdir(installDir, { recursive: true });
1131
- const executable = uvExecutable(options, version);
1132
- const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
396
+ async function publishUv(source, version, context) {
397
+ const installDirectory = uvInstallDirectory(context, version);
398
+ await import_promises.mkdir(installDirectory, { recursive: true });
399
+ const executable = uvExecutable(context, version);
400
+ const staged = import_node_path2.default.join(installDirectory, `.${import_node_path2.default.basename(executable)}.${uniqueSuffix()}.tmp`);
1133
401
  try {
1134
402
  await import_promises.copyFile(source, staged);
1135
403
  if (process.platform !== "win32")
1136
404
  await import_promises.chmod(staged, 493);
1137
- const stagedUv = probeUv(staged, options);
1138
- if (!stagedUv || stagedUv.version !== version) {
405
+ if (!await probeUv(staged, { ...context, specifier: `==${version}` })) {
1139
406
  throw new Error(`Downloaded uv executable is not version ${version}.`);
1140
407
  }
1141
408
  if (process.platform === "win32")
@@ -1145,29 +412,17 @@ async function publishUvAtomically(source, installDir, version, options) {
1145
412
  await import_promises.rm(staged, { force: true });
1146
413
  }
1147
414
  }
1148
- function uvAsset() {
1149
- const architecture = process.arch === "x64" ? "x86_64" : process.arch === "arm64" ? "aarch64" : null;
1150
- if (!architecture)
1151
- throw new Error(`uv bootstrap does not support architecture ${process.arch}.`);
1152
- if (process.platform === "win32")
1153
- return `uv-${architecture}-pc-windows-msvc.zip`;
1154
- if (process.platform === "darwin")
1155
- return `uv-${architecture}-apple-darwin.tar.gz`;
1156
- if (process.platform === "linux")
1157
- return `uv-${architecture}-unknown-linux-musl.tar.gz`;
1158
- throw new Error(`uv bootstrap does not support platform ${process.platform}.`);
1159
- }
1160
- async function download(url, destination, timeoutMs, dispatcher) {
415
+ async function download(url, destination, dispatcher) {
1161
416
  const response = await import_undici.fetch(url, {
1162
417
  dispatcher,
1163
418
  redirect: "follow",
1164
- signal: AbortSignal.timeout(timeoutMs)
419
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
1165
420
  });
1166
421
  if (!response.ok || !response.body) {
1167
422
  await response.body?.cancel();
1168
- throw new Error(`Could not download ${url}: HTTP ${response.status}`);
423
+ throw new Error(`Could not download ${url}: HTTP ${response.status}.`);
1169
424
  }
1170
- await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
425
+ await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs.createWriteStream(destination, { flags: "wx" }));
1171
426
  }
1172
427
  async function downloadText(url, timeoutMs, dispatcher, headers) {
1173
428
  const response = await import_undici.fetch(url, {
@@ -1178,33 +433,30 @@ async function downloadText(url, timeoutMs, dispatcher, headers) {
1178
433
  });
1179
434
  if (!response.ok) {
1180
435
  await response.body?.cancel();
1181
- throw new Error(`Could not download ${url}: HTTP ${response.status}`);
436
+ throw new Error(`Could not download ${url}: HTTP ${response.status}.`);
1182
437
  }
1183
438
  return response.text();
1184
439
  }
1185
440
  function proxyDispatcher(env) {
1186
- const httpProxy = environmentValue(env, "http_proxy", "HTTP_PROXY");
1187
- const httpsProxy = environmentValue(env, "https_proxy", "HTTPS_PROXY");
441
+ const httpProxy = environmentValue(env, "http_proxy") ?? environmentValue(env, "HTTP_PROXY");
442
+ const httpsProxy = environmentValue(env, "https_proxy") ?? environmentValue(env, "HTTPS_PROXY");
1188
443
  if (!httpProxy && !httpsProxy)
1189
444
  return;
1190
445
  return new import_undici.EnvHttpProxyAgent({
1191
446
  httpProxy,
1192
447
  httpsProxy,
1193
- noProxy: environmentValue(env, "no_proxy", "NO_PROXY")
448
+ noProxy: environmentValue(env, "no_proxy") ?? environmentValue(env, "NO_PROXY")
1194
449
  });
1195
450
  }
1196
- function environmentValue(env, lowercase, uppercase) {
1197
- return env[lowercase] ?? env[uppercase];
1198
- }
1199
451
  async function sha256File(file) {
1200
452
  const hash = import_node_crypto.createHash("sha256");
1201
- for await (const chunk of import_node_fs3.createReadStream(file))
453
+ for await (const chunk of import_node_fs.createReadStream(file))
1202
454
  hash.update(chunk);
1203
455
  return hash.digest("hex");
1204
456
  }
1205
457
  async function findFile(directory, name) {
1206
458
  for (const entry of await import_promises.readdir(directory, { withFileTypes: true })) {
1207
- const target = import_node_path5.default.join(directory, entry.name);
459
+ const target = import_node_path2.default.join(directory, entry.name);
1208
460
  if (entry.isFile() && entry.name === name)
1209
461
  return target;
1210
462
  if (entry.isDirectory()) {
@@ -1215,93 +467,332 @@ async function findFile(directory, name) {
1215
467
  }
1216
468
  return null;
1217
469
  }
1218
-
1219
- // src/runners.ts
1220
- var import_node_child_process2 = require("node:child_process");
1221
-
1222
- // src/runtime-files.ts
1223
- var import_node_fs5 = __toESM(require("node:fs"), 1);
1224
- var import_node_path6 = __toESM(require("node:path"), 1);
1225
- function runtimeFile(name) {
1226
- const moduleDirectory = __dirname;
1227
- const compiled = import_node_path6.default.join(moduleDirectory, `${name}.js`);
1228
- return import_node_fs5.default.existsSync(compiled) ? compiled : import_node_path6.default.join(moduleDirectory, `${name}.ts`);
1229
- }
1230
-
1231
- // src/runners.ts
1232
- async function uvSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
1233
- const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1234
- const resolved = resolveOptions(options);
1235
- const environment = uvCommandNeedsEnvironment(args) ? await ensureEnvironment(resolved) : undefined;
1236
- const uv = environment?.uv ?? await ensureUv(resolved);
1237
- const command = buildUvCommand(uv, args, resolved, environment?.python);
1238
- return import_node_child_process2.spawn(command.executable, command.args, {
1239
- ...spawnOptions,
1240
- env: command.env
470
+ async function withProvisioningLock(options) {
471
+ let existing;
472
+ const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
473
+ key: options.key,
474
+ name: "Pyfig uv setup",
475
+ namespace: "pyfig",
476
+ timeout: COORDINATION_TIMEOUT_MS,
477
+ predicate: async () => {
478
+ const value = await options.check();
479
+ if (value === null)
480
+ return true;
481
+ existing = { value };
482
+ return false;
483
+ }
1241
484
  });
485
+ if (result.context.state === "executed")
486
+ return result.value;
487
+ if (existing)
488
+ return existing.value;
489
+ throw new Error(`Provisioning check for ${options.key} did not produce uv.`);
490
+ }
491
+ function uvCacheDirectory(env) {
492
+ const home = environmentValue(env, process.platform === "win32" ? "USERPROFILE" : "HOME") ?? import_node_os.default.homedir();
493
+ const root = process.platform === "win32" ? environmentValue(env, "LOCALAPPDATA") ?? import_node_path2.default.join(home, "AppData", "Local") : process.platform === "darwin" ? import_node_path2.default.join(home, "Library", "Caches") : environmentValue(env, "XDG_CACHE_HOME") ?? import_node_path2.default.join(home, ".cache");
494
+ return import_node_path2.default.resolve(root, "pyfig");
495
+ }
496
+ function uvInstallRoot(context) {
497
+ return import_node_path2.default.join(context.cache, "tools", "uv");
498
+ }
499
+ function uvInstallDirectory(context, version) {
500
+ return import_node_path2.default.join(uvInstallRoot(context), version);
1242
501
  }
1243
- function uvSpawnSync(args = [], spawnOptions = {}, pyfigOptions = {}) {
1244
- const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1245
- const resolved = resolveOptions(options);
1246
- const environment = uvCommandNeedsEnvironment(args) ? resolveSynchronously("environment", resolved) : undefined;
1247
- const uv = environment?.uv ?? resolveSynchronously("uv", resolved);
1248
- const command = buildUvCommand(uv, args, resolved, environment?.python);
1249
- return import_node_child_process2.spawnSync(command.executable, command.args, {
1250
- ...spawnOptions,
1251
- env: command.env
502
+ function uvExecutable(context, version) {
503
+ return import_node_path2.default.join(uvInstallDirectory(context, version), process.platform === "win32" ? "uv.exe" : "uv");
504
+ }
505
+ function uvReleaseMetadataFile(context) {
506
+ return import_node_path2.default.join(context.cache, "metadata", "uv-releases.json");
507
+ }
508
+ function uvAsset() {
509
+ const architecture = process.arch === "x64" ? "x86_64" : process.arch === "arm64" ? "aarch64" : null;
510
+ if (!architecture)
511
+ throw new Error(`uv setup does not support architecture ${process.arch}.`);
512
+ if (process.platform === "win32")
513
+ return `uv-${architecture}-pc-windows-msvc.zip`;
514
+ if (process.platform === "darwin")
515
+ return `uv-${architecture}-apple-darwin.tar.gz`;
516
+ if (process.platform === "linux")
517
+ return `uv-${architecture}-unknown-linux-musl.tar.gz`;
518
+ throw new Error(`uv setup does not support platform ${process.platform}.`);
519
+ }
520
+ function releaseAssetNames(value) {
521
+ if (!Array.isArray(value))
522
+ return [];
523
+ return value.flatMap((entry) => {
524
+ if (!entry || typeof entry !== "object")
525
+ return [];
526
+ const name = entry.name;
527
+ return typeof name === "string" ? [name] : [];
1252
528
  });
1253
529
  }
1254
- function buildUvCommand(uv, args, options, python) {
1255
- return {
1256
- executable: uv.executable,
1257
- args: python ? targetUvArguments(args, python) : [...args],
1258
- env: uvCommandEnvironment(uvEnvironment(options), python)
1259
- };
530
+ function canonicalPath(value) {
531
+ const normalized = import_node_path2.default.resolve(value);
532
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
1260
533
  }
1261
- function optionsForInvocation(pyfigOptions, spawnEnv) {
1262
- return spawnEnv === undefined ? pyfigOptions : { ...pyfigOptions, env: spawnEnv };
534
+ function uniqueSuffix() {
535
+ return `${process.pid}.${import_node_crypto.randomUUID()}`;
1263
536
  }
1264
- function resolveSynchronously(target, options) {
1265
- const resolved = resolveOptions(options);
1266
- const request = { target, options: resolved };
1267
- const result = import_node_child_process2.spawnSync(process.execPath, [runtimeFile("sync-resolver")], {
1268
- input: JSON.stringify(request),
1269
- encoding: "utf8",
1270
- env: resolved.env,
1271
- timeout: synchronousResolutionTimeout(resolved),
1272
- maxBuffer: 4 * 1024 * 1024,
1273
- windowsHide: true
537
+
538
+ // src/pyfig.ts
539
+ var DEFAULT_UV = ">=0.10,<1";
540
+ var PYTHON_PROBE = String.raw`
541
+ import json
542
+ import os
543
+ import platform
544
+ import struct
545
+ import sys
546
+ import sysconfig
547
+
548
+ base_prefix = getattr(sys, "base_prefix", sys.prefix)
549
+ include_directory = sysconfig.get_path("include") or ""
550
+ header_path = os.path.join(include_directory, "Python.h") if include_directory else ""
551
+ library_candidates = []
552
+ if os.name == "nt":
553
+ library_directory = sysconfig.get_config_var("LIBDIR") or os.path.join(base_prefix, "libs")
554
+ configured_library = sysconfig.get_config_var("LDLIBRARY") or ""
555
+ if configured_library:
556
+ library_candidates.append(os.path.join(library_directory, os.path.splitext(configured_library)[0] + ".lib"))
557
+ library_candidates.append(os.path.join(library_directory, f"python{sys.version_info.major}{sys.version_info.minor}.lib"))
558
+ else:
559
+ for directory in [
560
+ sysconfig.get_config_var("LIBDIR"),
561
+ sysconfig.get_config_var("LIBPL"),
562
+ os.path.join(base_prefix, "lib"),
563
+ ]:
564
+ for name in [sysconfig.get_config_var("LDLIBRARY"), sysconfig.get_config_var("LIBRARY")]:
565
+ if directory and name:
566
+ library_candidates.append(os.path.join(directory, name))
567
+
568
+ release_suffix = {"alpha": "a", "beta": "b", "candidate": "rc", "final": ""}[sys.version_info.releaselevel]
569
+ if release_suffix:
570
+ release_suffix += str(sys.version_info.serial)
571
+
572
+ print(json.dumps({
573
+ "executable": sys.executable,
574
+ "version": ".".join(map(str, sys.version_info[:3])) + release_suffix,
575
+ "implementation": sys.implementation.name,
576
+ "architecture": platform.machine().lower(),
577
+ "pointerBits": struct.calcsize("P") * 8,
578
+ "gilDisabled": bool(sysconfig.get_config_var("Py_GIL_DISABLED")),
579
+ "embedding": os.path.isfile(header_path) and any(os.path.isfile(path) for path in library_candidates),
580
+ }))
581
+ `;
582
+
583
+ class PythonContext {
584
+ #cwd;
585
+ #env;
586
+ uv;
587
+ python;
588
+ constructor(uvPath, pythonPath, cwd = process.cwd(), env = process.env) {
589
+ this.#cwd = cwd;
590
+ this.#env = { ...env };
591
+ this.uv = uvPath;
592
+ this.python = pythonPath;
593
+ Object.freeze(this);
594
+ }
595
+ run(...input) {
596
+ const { args, spawnOptions } = splitRunInput(input);
597
+ return import_node_child_process2.spawn(this.uv, buildRunArgs(this.python, args), {
598
+ cwd: this.#cwd,
599
+ env: this.#env,
600
+ stdio: "inherit",
601
+ windowsHide: true,
602
+ ...spawnOptions
603
+ });
604
+ }
605
+ }
606
+ async function pyfig(options = {}) {
607
+ const {
608
+ uv: uvRequest = DEFAULT_UV,
609
+ python: pythonRequest,
610
+ requireEmbedding = false,
611
+ requireGil = false,
612
+ cwd = process.cwd(),
613
+ env: suppliedEnvironment
614
+ } = options;
615
+ const env = mergeEnvironment(suppliedEnvironment);
616
+ const workingDirectory = import_node_path3.default.resolve(cwd);
617
+ const uvPath = await selectUv(uvRequest, workingDirectory, env);
618
+ const selectedPythonPath = await selectPython({
619
+ cwd: workingDirectory,
620
+ env,
621
+ pythonRequest,
622
+ requireEmbedding,
623
+ requireGil,
624
+ uvPath
1274
625
  });
1275
- if (result.status !== 0 || result.error) {
1276
- const detail = result.stderr.trim() || result.error?.message || `exit ${result.status}`;
1277
- throw new Error(`Could not resolve ${target} synchronously: ${detail}`);
626
+ return new PythonContext(uvPath, selectedPythonPath, workingDirectory, env);
627
+ }
628
+ function buildRunArgs(pythonPath, args) {
629
+ const separator = args.indexOf("--");
630
+ if (separator < 0) {
631
+ return ["run", "--python", pythonPath, "--", ...args];
632
+ }
633
+ const uvArgs = args.slice(0, separator);
634
+ const command = args.slice(separator + 1);
635
+ if (uvArgs.some((argument) => argument === "--python" || argument.startsWith("--python="))) {
636
+ throw new TypeError("run() does not accept --python before --; Python is selected by pyfig().");
637
+ }
638
+ uvArgs.push("--python", pythonPath);
639
+ return ["run", ...uvArgs, "--", ...command];
640
+ }
641
+ function splitRunInput(input) {
642
+ const values = [...input];
643
+ const last = values.at(-1);
644
+ const spawnOptions = typeof last === "object" && last !== null && !Array.isArray(last) ? values.pop() : {};
645
+ if (!values.every((value) => typeof value === "string")) {
646
+ throw new TypeError("uv run arguments must be strings with an optional SpawnOptions object last.");
647
+ }
648
+ return { args: values, spawnOptions };
649
+ }
650
+ async function selectUv(request, cwd, env) {
651
+ const selection = await executableOrSpecifier("uv", request, cwd, env);
652
+ if (selection.executable)
653
+ return selection.executable;
654
+ const specifier = selection.specifier;
655
+ if (!specifier)
656
+ throw new Error("uv selection did not contain a version specifier.");
657
+ return resolveUv(specifier, cwd, env);
658
+ }
659
+ async function selectPython(options) {
660
+ if (options.pythonRequest !== undefined) {
661
+ const selection = await executableOrSpecifier("python", options.pythonRequest, options.cwd, options.env);
662
+ if (selection.executable) {
663
+ const explicit = await probePython(selection.executable, options.cwd, options.env);
664
+ validatePythonCapabilities(explicit, options);
665
+ return explicit.executable;
666
+ }
667
+ if (!selection.specifier)
668
+ throw new Error("Python selection did not contain a version specifier.");
669
+ return selectDiscoveredPython(selection.specifier, options);
1278
670
  }
671
+ return selectDiscoveredPython(null, options);
672
+ }
673
+ async function selectDiscoveredPython(specifier, options) {
674
+ const request = specifier ? [specifier] : [];
675
+ let systemFailure;
1279
676
  try {
1280
- return JSON.parse(result.stdout);
677
+ const systemPath = await findPython(options.uvPath, ["--system", "--no-managed-python", ...request], options);
678
+ const system = await probePython(systemPath, options.cwd, options.env);
679
+ validatePythonVersion(system, specifier);
680
+ validatePythonCapabilities(system, options);
681
+ return system.executable;
1281
682
  } catch (error) {
1282
- const detail = error instanceof Error ? error.message : String(error);
1283
- throw new Error(`Could not parse synchronous ${target} resolution: ${detail}`);
683
+ systemFailure = error;
684
+ }
685
+ try {
686
+ const managedPath = await findPython(options.uvPath, ["--managed-python", ...request], options);
687
+ const managed = await probePython(managedPath, options.cwd, options.env);
688
+ validatePythonVersion(managed, specifier);
689
+ validatePythonCapabilities(managed, options);
690
+ return managed.executable;
691
+ } catch (error) {
692
+ throw new Error(`No compatible Python was found. System: ${errorMessage(systemFailure)} Managed: ${errorMessage(error)}`, { cause: error });
1284
693
  }
1285
694
  }
1286
- function synchronousResolutionTimeout(options) {
1287
- return options.coordinationTimeoutMs + options.downloadTimeoutMs + 2 * options.installTimeoutMs + 4 * options.probeTimeoutMs + 60000;
695
+ async function findPython(uvPath, args, options) {
696
+ const output = await capture(uvPath, ["python", "find", ...args], options.cwd, options.env);
697
+ const executable = output.trim().split(/\r?\n/u).at(-1) ?? "";
698
+ if (!import_node_path3.default.isAbsolute(executable)) {
699
+ throw new Error(`uv did not report an absolute Python path: ${executable || "<empty>"}`);
700
+ }
701
+ return import_node_path3.default.normalize(executable);
1288
702
  }
1289
-
1290
- // src/client.ts
1291
- function createPyfig(options = {}) {
1292
- const scopedOptions = {
1293
- ...options,
1294
- env: options.env ? { ...options.env } : undefined
1295
- };
1296
- const baseEnvironment = import_loqkit2.memoize(() => ensureEnvironment(scopedOptions));
1297
- const uvInfo = import_loqkit2.memoize(() => ensureUv(scopedOptions));
1298
- return {
1299
- prepare: (dependencies = [], dependencyOptions = {}) => dependencies.length === 0 && !dependencyOptions.allExtras && (dependencyOptions.extras?.length ?? 0) === 0 && (dependencyOptions.groups?.length ?? 0) === 0 ? baseEnvironment() : ensureEnvironment(scopedOptions, dependencies, dependencyOptions),
1300
- pythonInfo: async () => (await baseEnvironment()).python,
1301
- uvInfo,
1302
- uv: (args = [], spawnOptions = {}) => uvSpawn(args, spawnOptions, scopedOptions),
1303
- uvSync: (args = [], spawnOptions = {}) => uvSpawnSync(args, spawnOptions, scopedOptions)
1304
- };
703
+ async function probePython(executable, cwd, env) {
704
+ const output = await capture(executable, ["-I", "-S", "-c", PYTHON_PROBE], cwd, env);
705
+ const line = output.trim().split(/\r?\n/u).at(-1) ?? "";
706
+ let value;
707
+ try {
708
+ value = JSON.parse(line);
709
+ } catch (error) {
710
+ throw new Error(`${JSON.stringify(executable)} did not return valid Python metadata.`, {
711
+ cause: error
712
+ });
713
+ }
714
+ if (!isPythonProbe(value)) {
715
+ throw new Error(`${JSON.stringify(executable)} returned incomplete Python metadata.`);
716
+ }
717
+ if (!import_node_path3.default.isAbsolute(value.executable)) {
718
+ throw new Error(`${JSON.stringify(executable)} reported a non-absolute executable path.`);
719
+ }
720
+ return { ...value, executable: import_node_path3.default.normalize(value.executable) };
721
+ }
722
+ function validatePythonVersion(python, specifier) {
723
+ if (!specifier)
724
+ return;
725
+ const version = import_pep4402.valid(python.version);
726
+ if (!version)
727
+ throw new Error(`Python ${python.executable} reported invalid version ${python.version}.`);
728
+ if (import_pep4402.filter([version], specifier).length !== 1) {
729
+ throw new Error(`Python ${version} does not satisfy ${specifier}.`);
730
+ }
731
+ }
732
+ function validatePythonCapabilities(python, options) {
733
+ if (options.requireEmbedding) {
734
+ if (python.implementation !== "cpython") {
735
+ throw new Error(`Python embedding requires CPython, received ${python.implementation}.`);
736
+ }
737
+ if (!python.embedding) {
738
+ throw new Error(`Python ${python.executable} does not provide embedding headers and a library.`);
739
+ }
740
+ if (!pythonArchitectureMatchesRuntime(python)) {
741
+ throw new Error(`Python ${python.executable} does not match the JavaScript runtime architecture.`);
742
+ }
743
+ }
744
+ if (options.requireGil && python.gilDisabled) {
745
+ throw new Error(`Python ${python.executable} is free-threaded but requireGil is enabled.`);
746
+ }
747
+ }
748
+ function pythonArchitectureMatchesRuntime(python) {
749
+ if (python.pointerBits !== (process.arch === "ia32" ? 32 : 64))
750
+ return false;
751
+ if (process.arch === "x64")
752
+ return python.architecture === "x86_64" || python.architecture === "amd64";
753
+ if (process.arch === "arm64")
754
+ return python.architecture === "aarch64" || python.architecture === "arm64";
755
+ if (process.arch === "ia32") {
756
+ return python.architecture === "x86" || python.architecture === "i386" || python.architecture === "i686";
757
+ }
758
+ return python.architecture === process.arch;
759
+ }
760
+ async function executableOrSpecifier(name, value, cwd, env) {
761
+ assertNonempty(name, value);
762
+ const executable = await findExecutable(value, cwd, env);
763
+ if (executable)
764
+ return { executable, specifier: null };
765
+ if (import_pep4402.validRange(value))
766
+ return { executable: null, specifier: value };
767
+ throw new TypeError(`${name} must resolve to an executable or be a valid PEP 440 specifier, received ${JSON.stringify(value)}.`);
768
+ }
769
+ function isPythonProbe(value) {
770
+ if (!value || typeof value !== "object" || Array.isArray(value))
771
+ return false;
772
+ const probe = value;
773
+ return typeof probe["architecture"] === "string" && typeof probe["embedding"] === "boolean" && typeof probe["executable"] === "string" && typeof probe["gilDisabled"] === "boolean" && typeof probe["implementation"] === "string" && typeof probe["pointerBits"] === "number" && typeof probe["version"] === "string";
774
+ }
775
+ function errorMessage(error) {
776
+ return error instanceof Error ? error.message : String(error);
777
+ }
778
+ function mergeEnvironment(supplied) {
779
+ const environment = { ...process.env };
780
+ for (const [key, value] of Object.entries(supplied ?? {})) {
781
+ if (process.platform === "win32") {
782
+ const existing = Object.keys(environment).find((candidate) => candidate.toLowerCase() === key.toLowerCase());
783
+ if (existing && existing !== key)
784
+ delete environment[existing];
785
+ }
786
+ if (value === undefined)
787
+ delete environment[key];
788
+ else
789
+ environment[key] = value;
790
+ }
791
+ return environment;
792
+ }
793
+ function assertNonempty(name, value) {
794
+ if (value.trim() === "")
795
+ throw new TypeError(`${name} must not be empty.`);
1305
796
  }
1306
797
 
1307
- //# debugId=0CA78C4B6148614B64756E2164756E21
798
+ //# debugId=F23B5C2985518F2764756E2164756E21