pyfig 0.7.0 → 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 (79) hide show
  1. package/README.md +76 -178
  2. package/SECURITY.md +16 -5
  3. package/dist/cjs/index.js +482 -1131
  4. package/dist/cjs/index.js.map +6 -16
  5. package/dist/index.d.ts +2 -8
  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 -1342
  23. package/dist/cjs/sync-resolver.js.map +0 -20
  24. package/dist/cli.d.ts +0 -3
  25. package/dist/cli.d.ts.map +0 -1
  26. package/dist/cli.js +0 -57
  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 -35
  31. package/dist/client.js.map +0 -1
  32. package/dist/config.d.ts +0 -69
  33. package/dist/config.d.ts.map +0 -1
  34. package/dist/config.js +0 -159
  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 -20
  57. package/dist/provision.d.ts.map +0 -1
  58. package/dist/provision.js +0 -673
  59. package/dist/provision.js.map +0 -1
  60. package/dist/python-venv.d.ts +0 -12
  61. package/dist/python-venv.d.ts.map +0 -1
  62. package/dist/python-venv.js +0 -52
  63. package/dist/python-venv.js.map +0 -1
  64. package/dist/runners.d.ts +0 -14
  65. package/dist/runners.d.ts.map +0 -1
  66. package/dist/runners.js +0 -71
  67. package/dist/runners.js.map +0 -1
  68. package/dist/runtime-files.d.ts +0 -3
  69. package/dist/runtime-files.d.ts.map +0 -1
  70. package/dist/runtime-files.js +0 -12
  71. package/dist/runtime-files.js.map +0 -1
  72. package/dist/sync-resolver.d.ts +0 -2
  73. package/dist/sync-resolver.d.ts.map +0 -1
  74. package/dist/sync-resolver.js +0 -15
  75. package/dist/sync-resolver.js.map +0 -1
  76. package/dist/venv-identity.d.ts +0 -36
  77. package/dist/venv-identity.d.ts.map +0 -1
  78. package/dist/venv-identity.js +0 -80
  79. package/dist/venv-identity.js.map +0 -1
package/dist/cjs/index.js CHANGED
@@ -65,1018 +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
- Python: () => Python,
72
- ensurePython: () => ensurePython,
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
- const dependencies = normalizeDependencies(options.dependencies ?? []);
153
- const requirements = normalizeRequirements(options.requirements ?? [], projectRoot);
154
- return {
155
- dependencies,
156
- requirements,
157
- python: options.python ?? env["PYFIG_PYTHON"] ?? "",
158
- requiresPython,
159
- managedPython,
160
- requiresUv,
161
- requireEmbedding: options.requireEmbedding ?? parseBoolean(env["PYFIG_REQUIRE_EMBEDDING"], false),
162
- requireGil: options.requireGil ?? parseBoolean(env["PYFIG_REQUIRE_GIL"], false),
163
- autoInstall,
164
- installPython: options.installPython ?? parseBoolean(env["PYFIG_INSTALL_PYTHON"], autoInstall),
165
- installUv: options.installUv ?? parseBoolean(env["PYFIG_INSTALL_UV"], autoInstall),
166
- createVirtualEnvironment: options.createVirtualEnvironment ?? parseBoolean(env["PYFIG_CREATE_VIRTUAL_ENVIRONMENT"], autoInstall),
167
- cacheDir,
168
- projectRoot,
169
- probeTimeoutMs,
170
- installTimeoutMs,
171
- downloadTimeoutMs,
172
- coordinationTimeoutMs,
173
- env
174
- };
175
- }
176
- function hasRuntimeRequirements(options) {
177
- return options.dependencies.length > 0 || options.requirements.length > 0;
178
- }
179
- function uvProcessEnvironment(options) {
180
- return {
181
- ...options.env,
182
- UV_PYTHON_INSTALL_DIR: import_node_path2.default.join(options.cacheDir, "python"),
183
- UV_CACHE_DIR: import_node_path2.default.join(options.cacheDir, "uv-cache"),
184
- UV_PYTHON_PREFERENCE: "system",
185
- UV_NO_CONFIG: "1"
186
- };
187
- }
188
- function assertPep440Specifier(name, specifier) {
189
- if (!isValidPep440Specifier(specifier)) {
190
- throw new TypeError(`Invalid ${name} PEP 440 specifier: ${specifier}`);
191
- }
192
- }
193
- function assertManagedPythonRequest(request, requiresPython) {
194
- if (!request || /[\\/\0\r\n]/u.test(request) || request.includes("..")) {
195
- throw new TypeError(`Invalid managed Python request: ${request}`);
196
- }
197
- if (/^\d+\.\d+(?:\.\d+)?$/u.test(request)) {
198
- const normalized = request.split(".").length === 2 ? `${request}.0` : request;
199
- if (!pep440VersionSatisfies(normalized, requiresPython)) {
200
- throw new TypeError(`Managed Python ${request} cannot satisfy requires-python ${requiresPython}.`);
201
- }
202
- }
203
- }
204
- function resolveDuration(name, explicit, environment, fallback, allowZero = false) {
205
- const value = explicit ?? (environment === undefined ? fallback : Number(environment));
206
- const minimum = allowZero ? 0 : 1;
207
- if (!Number.isSafeInteger(value) || value < minimum) {
208
- throw new TypeError(`${name} must be ${allowZero ? "a non-negative" : "a positive"} integer.`);
209
- }
210
- return value;
211
- }
212
- function parseBoolean(value, fallback) {
213
- if (value === undefined)
214
- return fallback;
215
- if (/^(1|true|yes|on)$/iu.test(value))
216
- return true;
217
- if (/^(0|false|no|off)$/iu.test(value))
218
- return false;
219
- throw new TypeError(`Invalid boolean value: ${value}`);
220
- }
221
- function normalizeDependencies(dependencies) {
222
- const normalized = dependencies.map((dependency) => dependency.trim());
223
- for (const dependency of normalized) {
224
- if (!dependency || /[\0\r\n]/u.test(dependency)) {
225
- throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
226
- }
227
- }
228
- return normalized;
229
- }
230
- function normalizeRequirements(requirements, projectRoot) {
231
- const normalized = [];
232
- for (const requirement of requirements) {
233
- const candidate = requirement.trim();
234
- if (!candidate || /[\0\r\n]/u.test(candidate)) {
235
- throw new TypeError(`Invalid requirements file path: ${JSON.stringify(candidate)}`);
236
- }
237
- const absolute = import_node_path2.default.resolve(projectRoot, candidate);
238
- const basename = import_node_path2.default.basename(absolute).toLowerCase();
239
- if (isPythonProjectFile(basename)) {
240
- throw new TypeError(`Expected a requirements file, not Python project metadata: ${absolute}`);
241
- }
242
- normalized.push(absolute);
243
- }
244
- return normalized;
245
- }
246
- var PYTHON_PROJECT_FILES = new Set([
247
- "pdm.lock",
248
- "pipfile",
249
- "pipfile.lock",
250
- "poetry.lock",
251
- "pyproject.toml",
252
- "setup.cfg",
253
- "setup.py",
254
- "uv.lock"
255
- ]);
256
- function isPythonProjectFile(basename) {
257
- return PYTHON_PROJECT_FILES.has(basename) || /^pylock(?:\.[^.]+)?\.toml$/u.test(basename);
258
- }
259
- // src/client.ts
260
- var import_loqkit2 = require("loqkit");
261
-
262
- // src/provision.ts
263
- var import_node_crypto2 = require("node:crypto");
264
- var import_node_fs3 = require("node:fs");
265
- var import_promises2 = require("node:fs/promises");
266
- var import_node_os = __toESM(require("node:os"), 1);
267
- var import_node_path6 = __toESM(require("node:path"), 1);
268
- var import_node_stream = require("node:stream");
269
- var import_promises3 = require("node:stream/promises");
270
- var import_fflate = require("fflate");
271
- var tar = __toESM(require("tar"), 1);
272
- var import_undici = require("undici");
273
-
274
- // src/venv-identity.ts
275
- var import_node_crypto = require("node:crypto");
276
- var import_promises = require("node:fs/promises");
277
- var import_node_path4 = __toESM(require("node:path"), 1);
278
-
279
- // src/python-venv.ts
280
- var import_node_fs2 = __toESM(require("node:fs"), 1);
73
+ // src/pyfig.ts
74
+ var import_node_child_process2 = require("node:child_process");
281
75
  var import_node_path3 = __toESM(require("node:path"), 1);
282
- var VIRTUAL_ENV_ROOT_NAME = import_node_path3.default.join("node_modules", ".pyfig", "venvs");
283
- function pythonVenvRoot(projectRoot) {
284
- return import_node_path3.default.join(projectRoot, VIRTUAL_ENV_ROOT_NAME);
285
- }
286
- function pythonVenvDirectory(projectRoot, venvDirectory) {
287
- return import_node_path3.default.join(pythonVenvRoot(projectRoot), venvDirectory);
288
- }
289
- function externalPythonVenvDirectories(projectRoot, env) {
290
- const candidates = [env["VIRTUAL_ENV"], import_node_path3.default.join(projectRoot, ".venv")].filter((value) => Boolean(value)).map((value) => import_node_path3.default.resolve(value));
291
- return [...new Map(candidates.map((value) => [canonicalPath(value), value])).values()];
292
- }
293
- function pythonVenvExecutable(pythonVenv) {
294
- return import_node_path3.default.join(pythonVenv, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
295
- }
296
- function activatePythonVenv(env, pythonVenv, pythonExecutable) {
297
- const activated = { ...env };
298
- const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
299
- const existingPath = pathEntry ? activated[pathEntry] : undefined;
300
- for (const key of Object.keys(activated)) {
301
- const normalized = key.toUpperCase();
302
- if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
303
- delete activated[key];
304
- }
305
- }
306
- activated["PATH"] = [import_node_path3.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path3.default.delimiter);
307
- activated["VIRTUAL_ENV"] = pythonVenv;
308
- return activated;
309
- }
310
- function pathsEqual(left, right) {
311
- return canonicalPath(left) === canonicalPath(right);
312
- }
313
- function canonicalPath(value) {
314
- const resolved = import_node_path3.default.resolve(value);
315
- let canonical = resolved;
316
- try {
317
- canonical = import_node_fs2.default.realpathSync.native(resolved);
318
- } catch {}
319
- return process.platform === "win32" ? canonical.toLowerCase() : canonical;
320
- }
321
-
322
- // src/venv-identity.ts
323
- var VENV_SCHEMA = 1;
324
- var VENV_METADATA = ".pyfig-venv.json";
325
- function createVenvIdentity(python, requirementsSha256) {
326
- const contract = {
327
- schema: VENV_SCHEMA,
328
- runtime: {
329
- architecture: process.arch,
330
- platform: process.platform
331
- },
332
- python: {
333
- architecture: python.architecture,
334
- baseExecutable: canonicalPath(python.executable),
335
- basePrefix: canonicalPath(python.basePrefix),
336
- embeddingHeader: python.embedding ? canonicalPath(python.embedding.headerPath) : null,
337
- embeddingLibrary: python.embedding ? canonicalPath(python.embedding.libraryPath) : null,
338
- gilDisabled: python.gilDisabled,
339
- implementation: python.implementation,
340
- pointerBits: python.pointerBits,
341
- version: python.version
342
- },
343
- requirementsSha256
344
- };
345
- const id = sha256(JSON.stringify(contract));
346
- const implementation = safePathSegment(python.implementation);
347
- const version = safePathSegment(python.version);
348
- return {
349
- contract,
350
- directoryName: `${implementation}-${version}-${id.slice(0, 20)}`,
351
- id
352
- };
353
- }
354
- function requirementsDigest(content) {
355
- return sha256(content);
356
- }
357
- function venvMetadataFile(directory) {
358
- return import_node_path4.default.join(directory, VENV_METADATA);
359
- }
360
- async function venvMatches(directory, venv) {
361
- try {
362
- const parsed = JSON.parse(await import_promises.readFile(venvMetadataFile(directory), "utf8"));
363
- return parsed.venvId === venv.id && JSON.stringify(parsed.contract) === JSON.stringify(venv.contract);
364
- } catch {
365
- return false;
366
- }
367
- }
368
- async function writeVenvMetadata(directory, venv) {
369
- await import_promises.mkdir(directory, { recursive: true });
370
- const file = venvMetadataFile(directory);
371
- const staged = `${file}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`;
372
- const metadata = {
373
- contract: venv.contract,
374
- venvId: venv.id
375
- };
376
- try {
377
- await import_promises.writeFile(staged, `${JSON.stringify(metadata, null, 2)}
378
- `, {
379
- encoding: "utf8",
380
- flag: "wx"
381
- });
382
- await import_promises.rename(staged, file);
383
- } finally {
384
- await import_promises.rm(staged, { force: true });
385
- }
386
- }
387
- function safePathSegment(value) {
388
- return value.toLowerCase().replace(/[^a-z0-9.-]+/gu, "-").replace(/^-+|-+$/gu, "") || "python";
389
- }
390
- function sha256(value) {
391
- return import_node_crypto.createHash("sha256").update(value).digest("hex");
392
- }
393
-
394
- // src/provision-lock.ts
395
- var import_loqkit = require("loqkit");
396
- async function withProvisioningLock(options) {
397
- let existing;
398
- const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
399
- key: options.key,
400
- name: "Pyfig provisioning",
401
- namespace: "pyfig",
402
- timeout: options.timeoutMs,
403
- predicate: async () => {
404
- const value = await options.check();
405
- if (value === null)
406
- return true;
407
- existing = { value };
408
- return false;
409
- }
410
- });
411
- if (result.context.state === "executed")
412
- return result.value;
413
- if (existing)
414
- return existing.value;
415
- throw new Error(`Provisioning check for ${options.key} did not produce a resource.`);
416
- }
417
-
418
- // src/package-target.ts
419
- var TARGET_OPTIONS = new Set([
420
- "--active",
421
- "--config-file",
422
- "--directory",
423
- "--isolated",
424
- "--prefix",
425
- "--project",
426
- "--python",
427
- "--root",
428
- "--system",
429
- "--target",
430
- "--user"
431
- ]);
432
- var TARGET_ENVIRONMENT_VARIABLES = new Set([
433
- "CONDA_PREFIX",
434
- "PIP_PREFIX",
435
- "PIP_PYTHON",
436
- "PIP_ROOT",
437
- "PIP_TARGET",
438
- "PIP_USER",
439
- "PYTHONHOME",
440
- "PYTHONUSERBASE",
441
- "UV_CONFIG_FILE",
442
- "UV_PROJECT",
443
- "UV_PROJECT_ENVIRONMENT",
444
- "UV_PYTHON",
445
- "UV_PREFIX",
446
- "UV_ROOT",
447
- "UV_SYSTEM_PYTHON",
448
- "UV_TARGET"
449
- ]);
450
- function uvCommandNeedsPython(args) {
451
- return args[0] === "pip" || args[0] === "run";
452
- }
453
- function targetUvArguments(args, python) {
454
- if (!python.virtualEnvironment) {
455
- throw new Error("Targeted uv commands require a virtual environment.");
456
- }
457
- assertNoTargetOverride(args);
458
- if (args[0] === "run") {
459
- return ["run", "--no-project", "--python", python.executable, ...args.slice(1)];
460
- }
461
- if (args[0] === "pip" && args[1]) {
462
- return ["pip", args[1], "--python", python.executable, ...args.slice(2)];
463
- }
464
- return [...args];
465
- }
466
- function uvCommandProcessEnvironment(env, python) {
467
- const sanitized = sanitizeUvProcessEnvironment(env);
468
- if (!python?.virtualEnvironment)
469
- return sanitized;
470
- return {
471
- ...activatePythonVenv(sanitized, python.virtualEnvironment, python.executable),
472
- UV_PYTHON: python.executable
473
- };
474
- }
475
- function sanitizeUvProcessEnvironment(env) {
476
- const sanitized = { ...env };
477
- for (const key of Object.keys(sanitized)) {
478
- if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
479
- delete sanitized[key];
480
- }
481
- return sanitized;
482
- }
483
- function assertNoTargetOverride(args) {
484
- for (const argument of args.slice(1)) {
485
- if (argument === "--")
486
- return;
487
- const [name] = argument.split("=", 1);
488
- if (name && TARGET_OPTIONS.has(name)) {
489
- throw new Error(`${name} cannot override the environment selected by Pyfig.`);
490
- }
491
- if (argument === "-p" || argument.startsWith("-p=") || /^-p[^-]/u.test(argument)) {
492
- throw new Error("-p cannot override the environment selected by Pyfig.");
493
- }
494
- }
495
- }
76
+ var import_pep4402 = require("@renovatebot/pep440");
496
77
 
497
78
  // src/process.ts
498
79
  var import_node_child_process = require("node:child_process");
499
- var import_node_path5 = __toESM(require("node:path"), 1);
80
+ var import_node_path = __toESM(require("node:path"), 1);
500
81
  var import_which = __toESM(require("which"), 1);
501
- async function resolveExecutable(value, env) {
502
- if (!value)
503
- return null;
504
- if (import_node_path5.default.isAbsolute(value)) {
505
- return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
506
- }
507
- if (!/^[A-Za-z0-9_.-]+$/u.test(value))
508
- return null;
509
- return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
510
- }
511
- function run(executable, args, env, timeoutMs) {
512
- const result = import_node_child_process.spawnSync(executable, args, {
513
- encoding: "utf8",
514
- env,
515
- timeout: timeoutMs,
516
- maxBuffer: 4 * 1024 * 1024,
517
- 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")
518
89
  });
519
- if (result.status !== 0 || result.error) {
520
- const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
521
- throw new Error(`${import_node_path5.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
522
- }
523
- return { stdout: result.stdout, stderr: result.stderr };
90
+ return resolved ? import_node_path.default.resolve(cwd, resolved) : null;
524
91
  }
525
- async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
92
+ async function capture(executable, args, cwd, env) {
526
93
  return new Promise((resolve, reject) => {
527
94
  const child = import_node_child_process.spawn(executable, args, {
528
- env,
529
95
  cwd,
96
+ env,
530
97
  stdio: ["ignore", "pipe", "pipe"],
531
98
  windowsHide: true
532
99
  });
533
100
  let stdout = "";
534
101
  let stderr = "";
535
102
  let settled = false;
536
- const finish = (error, result) => {
103
+ const finish = (error) => {
537
104
  if (settled)
538
105
  return;
539
106
  settled = true;
540
- clearTimeout(timer);
541
- signal?.removeEventListener("abort", abort);
542
107
  if (error)
543
108
  reject(error);
544
- else if (result)
545
- resolve(result);
546
- };
547
- const abort = () => {
548
- child.kill();
549
- const reason = signal?.reason;
550
- finish(reason instanceof Error ? reason : new Error(`${import_node_path5.default.basename(executable)} was aborted.`));
109
+ else
110
+ resolve(stdout);
551
111
  };
552
- const timer = setTimeout(() => {
553
- child.kill();
554
- finish(new Error(`${import_node_path5.default.basename(executable)} timed out after ${timeoutMs}ms.`));
555
- }, timeoutMs);
556
112
  child.stdout.setEncoding("utf8");
557
113
  child.stderr.setEncoding("utf8");
558
114
  child.stdout.on("data", (chunk) => {
559
115
  stdout += chunk;
560
- if (stdout.length > 4 * 1024 * 1024) {
116
+ if (stdout.length > OUTPUT_LIMIT) {
561
117
  child.kill();
562
- finish(new Error(`${import_node_path5.default.basename(executable)} exceeded the output limit.`));
118
+ finish(new Error(`${import_node_path.default.basename(executable)} exceeded its stdout limit.`));
563
119
  }
564
120
  });
565
121
  child.stderr.on("data", (chunk) => {
566
122
  stderr += chunk;
567
- if (stderr.length > 4 * 1024 * 1024) {
123
+ if (stderr.length > OUTPUT_LIMIT) {
568
124
  child.kill();
569
- finish(new Error(`${import_node_path5.default.basename(executable)} exceeded the error-output limit.`));
125
+ finish(new Error(`${import_node_path.default.basename(executable)} exceeded its stderr limit.`));
570
126
  }
571
127
  });
572
128
  child.once("error", (error) => finish(error));
573
- child.once("close", (code, childSignal) => {
574
- if (code !== 0) {
575
- const detail = stderr.trim() || (childSignal ? `signal ${childSignal}` : `exit ${code}`);
576
- finish(new Error(`${import_node_path5.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
129
+ child.once("close", (code, signal) => {
130
+ if (code === 0) {
131
+ finish();
577
132
  return;
578
133
  }
579
- 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}`));
580
136
  });
581
- if (signal?.aborted)
582
- abort();
583
- else
584
- signal?.addEventListener("abort", abort, { once: true });
585
137
  });
586
138
  }
587
-
588
- // src/probes.ts
589
- var PYTHON_PROBE = `
590
- import json
591
- import os
592
- import platform
593
- import struct
594
- import sys
595
- import sysconfig
596
-
597
- base_prefix = getattr(sys, "base_prefix", sys.prefix)
598
- include_directory = sysconfig.get_path("include") or ""
599
- header_path = os.path.join(include_directory, "Python.h") if include_directory else ""
600
- library_candidates = []
601
- stable_abi_library_candidates = []
602
- if os.name == "nt":
603
- library_directory = sysconfig.get_config_var("LIBDIR") or os.path.join(base_prefix, "libs")
604
- configured_library = sysconfig.get_config_var("LDLIBRARY") or ""
605
- if configured_library:
606
- library_candidates.append(os.path.join(library_directory, os.path.splitext(configured_library)[0] + ".lib"))
607
- library_candidates.append(os.path.join(library_directory, f"python{sys.version_info.major}{sys.version_info.minor}.lib"))
608
- stable_abi_library_candidates.append(os.path.join(library_directory, "python3.lib"))
609
- else:
610
- library_directories = [
611
- sysconfig.get_config_var("LIBDIR"),
612
- sysconfig.get_config_var("LIBPL"),
613
- os.path.join(base_prefix, "lib"),
614
- ]
615
- library_names = [
616
- sysconfig.get_config_var("LDLIBRARY"),
617
- sysconfig.get_config_var("LIBRARY"),
618
- ]
619
- for directory in library_directories:
620
- for name in library_names:
621
- if directory and name:
622
- library_candidates.append(os.path.join(directory, name))
623
- if directory:
624
- stable_abi_library_candidates.extend([
625
- os.path.join(directory, "libpython3.so"),
626
- os.path.join(directory, "libpython3.dylib"),
627
- ])
628
-
629
- library_path = next((candidate for candidate in library_candidates if os.path.isfile(candidate)), "")
630
- stable_abi_library_path = next(
631
- (candidate for candidate in stable_abi_library_candidates if os.path.isfile(candidate)),
632
- None,
633
- )
634
- embedding = None
635
- if os.path.isfile(header_path) and library_path:
636
- library_name = os.path.basename(library_path)
637
- library_kind = "import" if os.name == "nt" else ("static" if library_name.endswith(".a") else "shared")
638
- embedding = {
639
- "includeDirectory": include_directory,
640
- "headerPath": header_path,
641
- "libraryPath": library_path,
642
- "libraryDirectory": os.path.dirname(library_path),
643
- "libraryName": library_name,
644
- "libraryKind": library_kind,
645
- "stableAbiLibraryPath": stable_abi_library_path,
646
- }
647
-
648
- release_level = sys.version_info.releaselevel
649
- release_suffixes = {
650
- "alpha": "a",
651
- "beta": "b",
652
- "candidate": "rc",
653
- "final": "",
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;
654
144
  }
655
- release_suffix = release_suffixes[release_level]
656
- if release_suffix:
657
- release_suffix += str(sys.version_info.serial)
658
145
 
659
- print(json.dumps({
660
- "executable": sys.executable,
661
- "baseExecutable": getattr(sys, "_base_executable", sys.executable),
662
- "version": ".".join(map(str, sys.version_info[:3])) + release_suffix,
663
- "major": sys.version_info.major,
664
- "minor": sys.version_info.minor,
665
- "patch": sys.version_info.micro,
666
- "releaseLevel": release_level,
667
- "implementation": sys.implementation.name,
668
- "architecture": platform.machine().lower(),
669
- "pointerBits": struct.calcsize("P") * 8,
670
- "prefix": sys.prefix,
671
- "basePrefix": base_prefix,
672
- "virtualEnvironment": sys.prefix if sys.prefix != base_prefix else None,
673
- "gilDisabled": bool(sysconfig.get_config_var("Py_GIL_DISABLED")),
674
- "embedding": embedding,
675
- }))
676
- `;
677
- function pythonSatisfiesPolicy(python, options) {
678
- return pep440VersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
679
- }
680
- async function probePython(candidate, options) {
681
- const executable = await resolveExecutable(candidate.executable, options.env);
682
- if (!executable)
683
- return null;
684
- try {
685
- const { stdout } = run(executable, [...candidate.args, "-c", PYTHON_PROBE], options.env, options.probeTimeoutMs);
686
- const info = JSON.parse(stdout.trim());
687
- return normalizePep440Version(info.version) ? info : null;
688
- } catch {
689
- return null;
690
- }
691
- }
692
- async function findUv(options) {
693
- const configured = options.env["PYFIG_UV"] ?? "uv";
694
- const executable = await resolveExecutable(configured, options.env);
695
- const uv = executable ? probeUv(executable, options) : null;
696
- return uv && pep440VersionSatisfies(uv.version, options.requiresUv) ? uv : null;
697
- }
698
- function probeUv(executable, options) {
699
- try {
700
- const { stdout } = run(executable, ["--version"], options.env, options.probeTimeoutMs);
701
- const version = normalizeVersion(stdout.match(/^uv\s+(\S+)/iu)?.[1]);
702
- return version ? { executable, version } : null;
703
- } catch {
704
- return null;
705
- }
706
- }
707
- function normalizeVersion(value) {
708
- if (!value)
709
- return null;
710
- return normalizePep440Version(value);
711
- }
712
- function pythonArchitectureMatchesRuntime(python) {
713
- const architecture = python.architecture.toLowerCase();
714
- if (python.pointerBits !== (process.arch === "ia32" ? 32 : 64))
715
- return false;
716
- if (process.arch === "x64")
717
- return architecture === "x86_64" || architecture === "amd64";
718
- if (process.arch === "arm64")
719
- return architecture === "aarch64" || architecture === "arm64";
720
- if (process.arch === "ia32") {
721
- return architecture === "x86" || architecture === "i386" || architecture === "i686";
722
- }
723
- return architecture === process.arch;
724
- }
725
-
726
- // src/provision.ts
727
- async function ensureUv(options = {}) {
728
- return ensureUvResolved(resolveOptions(options));
729
- }
730
- async function ensurePython(options = {}) {
731
- const resolved = resolveOptions(options);
732
- await validateRequirementsFiles(resolved.requirements);
733
- const uv = await ensureUvResolved(resolved);
734
- if (!hasRuntimeRequirements(resolved)) {
735
- const external = await findReusableExternalPythonVenv(resolved);
736
- if (external) {
737
- return { managed: false, python: external, uv, venvId: null };
738
- }
739
- }
740
- const basePython = await ensureBasePythonResolved(uv, resolved);
741
- const resolution = await resolveRuntimeDependencies(uv, basePython, resolved);
742
- const venv = createVenvIdentity(basePython, resolution.sha256);
743
- const python = await ensureManagedPythonVenv(uv, basePython, venv, resolution, resolved);
744
- return { managed: true, python, uv, venvId: venv.id };
745
- }
746
- async function ensureManagedPythonVenv(uv, basePython, venv, resolution, options) {
747
- const directory = pythonVenvDirectory(options.projectRoot, venv.directoryName);
748
- const check = async () => {
749
- if (!await venvMatches(directory, venv))
750
- return null;
751
- const candidate = await inspectPythonVenv(directory, options);
752
- return candidate && pythonSatisfiesPolicy(candidate, options) ? candidate : null;
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
753
171
  };
754
- const existing = await check();
172
+ const existing = await findAvailableUv(context);
755
173
  if (existing)
756
174
  return existing;
757
- if (!options.createVirtualEnvironment) {
758
- throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
759
- }
175
+ const policy = import_node_crypto.createHash("sha256").update(specifier).digest("hex");
760
176
  return withProvisioningLock({
761
- key: `venv:${canonicalPath(directory)}`,
762
- timeoutMs: options.coordinationTimeoutMs,
763
- check,
177
+ key: `uv-policy:${canonicalPath(context.cache)}:${policy}`,
178
+ check: () => findAvailableUv(context),
764
179
  mutate: async () => {
765
- await import_promises2.rm(directory, { recursive: true, force: true });
766
- await import_promises2.mkdir(import_node_path6.default.dirname(directory), { recursive: true });
767
- try {
768
- await runAsync(uv.executable, [
769
- "venv",
770
- "--no-project",
771
- "--no-config",
772
- "--no-python-downloads",
773
- "--python",
774
- basePython.executable,
775
- directory
776
- ], isolatedUvProcessEnvironment(options), options.installTimeoutMs);
777
- const created = await inspectPythonVenv(directory, options);
778
- if (!created || !pythonSatisfiesPolicy(created, options)) {
779
- throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
780
- }
781
- await installResolvedDependencies(uv, created, resolution, options);
782
- await writeVenvMetadata(directory, venv);
783
- const installed = await check();
784
- if (!installed)
785
- throw new Error(`Pyfig could not validate environment ${directory}.`);
786
- return installed;
787
- } catch (error) {
788
- await import_promises2.rm(directory, { recursive: true, force: true });
789
- throw error;
790
- }
791
- }
792
- });
793
- }
794
- async function findReusableExternalPythonVenv(options) {
795
- if (options.python) {
796
- const configured = await probePython({ executable: options.python, args: [] }, options);
797
- if (!configured || !pythonSatisfiesPolicy(configured, options)) {
798
- throw incompatibleConfiguredPython(options);
799
- }
800
- if (configured.virtualEnvironment)
801
- return configured;
802
- }
803
- for (const directory of externalPythonVenvDirectories(options.projectRoot, options.env)) {
804
- const info = await inspectPythonVenv(directory, options);
805
- if (info && pythonSatisfiesPolicy(info, options))
806
- return info;
807
- }
808
- return null;
809
- }
810
- async function ensureBasePythonResolved(uv, options) {
811
- if (options.python) {
812
- const configured = await probePython({ executable: options.python, args: [] }, options);
813
- if (!configured || !pythonSatisfiesPolicy(configured, options)) {
814
- throw incompatibleConfiguredPython(options);
815
- }
816
- if (!configured.virtualEnvironment)
817
- return configured;
818
- const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
819
- if (base && pythonSatisfiesPolicy(base, options))
820
- return base;
821
- throw incompatibleConfiguredPython(options);
822
- }
823
- const externalBase = await findExternalBasePython(options);
824
- if (externalBase)
825
- return externalBase;
826
- const system = await findPythonWithUv(uv, options, "system");
827
- if (system)
828
- return system;
829
- const cachedManaged = await findPythonWithUv(uv, options, "managed");
830
- if (cachedManaged)
831
- return cachedManaged;
832
- if (!options.installPython) {
833
- throw new Error(missingPythonMessage(options, "managed Python installation is disabled"));
834
- }
835
- return withProvisioningLock({
836
- key: `python:${canonicalPath(import_node_path6.default.join(options.cacheDir, "python"))}`,
837
- timeoutMs: options.coordinationTimeoutMs,
838
- check: () => findPythonWithUv(uv, options, "managed"),
839
- mutate: async () => {
840
- await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], isolatedUvProcessEnvironment(options), options.installTimeoutMs);
841
- const installed = await findPythonWithUv(uv, options, "managed");
842
- if (!installed) {
843
- throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
844
- }
845
- return installed;
846
- }
847
- });
848
- }
849
- async function findExternalBasePython(options) {
850
- for (const directory of externalPythonVenvDirectories(options.projectRoot, options.env)) {
851
- const environment = await inspectPythonVenv(directory, options);
852
- if (!environment || !pythonSatisfiesPolicy(environment, options))
853
- continue;
854
- const base = await probePython({ executable: environment.baseExecutable, args: [] }, options);
855
- if (base && !base.virtualEnvironment && pythonSatisfiesPolicy(base, options))
856
- return base;
857
- }
858
- return null;
859
- }
860
- async function findPythonWithUv(uv, options, source) {
861
- for (const request of ["", options.managedPython]) {
862
- const args = [
863
- "python",
864
- "find",
865
- "--no-project",
866
- "--no-config",
867
- "--no-python-downloads",
868
- source === "system" ? "--system" : "--managed-python"
869
- ];
870
- if (request)
871
- args.push(request);
872
- try {
873
- const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceProcessEnvironment(options), options.probeTimeoutMs);
874
- const info = await probePython({ executable: stdout.trim(), args: [] }, options);
875
- if (info && pythonSatisfiesPolicy(info, options))
876
- return info;
877
- } catch {}
878
- }
879
- try {
880
- const { stdout } = await runAsync(uv.executable, [
881
- "python",
882
- "list",
883
- "--no-config",
884
- "--no-python-downloads",
885
- "--only-installed",
886
- "--output-format",
887
- "json",
888
- source === "system" ? "--no-managed-python" : "--managed-python"
889
- ], explicitPythonSourceProcessEnvironment(options), options.probeTimeoutMs);
890
- const candidates = installedPythonPaths(stdout);
891
- for (const executable of candidates) {
892
- const info = await probePython({ executable, args: [] }, options);
893
- if (info && !info.virtualEnvironment && pythonSatisfiesPolicy(info, options)) {
894
- return info;
895
- }
896
- }
897
- } catch {}
898
- return null;
899
- }
900
- function installedPythonPaths(output) {
901
- const parsed = JSON.parse(output);
902
- if (!Array.isArray(parsed))
903
- return [];
904
- const paths = parsed.flatMap((entry) => {
905
- if (!entry || typeof entry !== "object" || !("path" in entry))
906
- return [];
907
- const candidate = entry.path;
908
- return typeof candidate === "string" && candidate ? [candidate] : [];
909
- });
910
- return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
911
- }
912
- async function inspectPythonVenv(directory, options) {
913
- const executable = pythonVenvExecutable(directory);
914
- const info = await probePython({ executable, args: [] }, options);
915
- return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
916
- }
917
- async function resolveRuntimeDependencies(uv, python, options) {
918
- if (!hasRuntimeRequirements(options)) {
919
- return { content: "", sha256: requirementsDigest("") };
920
- }
921
- const temporary = await import_promises2.mkdtemp(import_node_path6.default.join(import_node_os.default.tmpdir(), "pyfig-resolution-"));
922
- try {
923
- const additions = import_node_path6.default.join(temporary, "runtime-dependencies.txt");
924
- const output = import_node_path6.default.join(temporary, "resolved-dependencies.txt");
925
- const sources = [...options.requirements];
926
- if (options.dependencies.length > 0) {
927
- await import_promises2.writeFile(additions, `${options.dependencies.join(`
928
- `)}
929
- `, "utf8");
930
- sources.push(additions);
931
- }
932
- await runAsync(uv.executable, [
933
- "pip",
934
- "compile",
935
- "--no-config",
936
- "--no-header",
937
- "--no-annotate",
938
- "--generate-hashes",
939
- "--python",
940
- python.executable,
941
- ...sources,
942
- "--output-file",
943
- output
944
- ], isolatedUvProcessEnvironment(options), options.installTimeoutMs, undefined, options.projectRoot);
945
- const content = normalizeResolution(await import_promises2.readFile(output, "utf8"));
946
- return { content, sha256: requirementsDigest(content) };
947
- } finally {
948
- await import_promises2.rm(temporary, { recursive: true, force: true });
949
- }
950
- }
951
- async function validateRequirementsFiles(requirements) {
952
- for (const requirement of requirements) {
953
- let information;
954
- try {
955
- information = await import_promises2.stat(requirement);
956
- } catch (error) {
957
- throw new Error(`Requirements file is unavailable: ${requirement}`, { cause: error });
958
- }
959
- if (!information.isFile()) {
960
- throw new Error(`Requirements path is not a file: ${requirement}`);
961
- }
962
- }
963
- }
964
- async function installResolvedDependencies(uv, python, resolution, options) {
965
- if (!resolution.content)
966
- return;
967
- if (!python.virtualEnvironment) {
968
- throw new Error("Runtime dependency installation requires a virtual environment.");
969
- }
970
- const temporary = await import_promises2.mkdtemp(import_node_path6.default.join(import_node_os.default.tmpdir(), "pyfig-install-"));
971
- try {
972
- const requirements = import_node_path6.default.join(temporary, "resolved-dependencies.txt");
973
- await import_promises2.writeFile(requirements, resolution.content, { encoding: "utf8", flag: "wx" });
974
- await runAsync(uv.executable, ["pip", "install", "--no-config", "--python", python.executable, "-r", requirements], targetUvProcessEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
975
- await runAsync(uv.executable, ["pip", "check", "--no-config", "--python", python.executable], targetUvProcessEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
976
- } finally {
977
- await import_promises2.rm(temporary, { recursive: true, force: true });
978
- }
979
- }
980
- function normalizeResolution(content) {
981
- const normalized = content.replace(/\r\n/gu, `
982
- `).trimEnd();
983
- return normalized ? `${normalized}
984
- ` : "";
985
- }
986
- async function ensureUvResolved(options) {
987
- const existing = await findAvailableUv(options);
988
- if (existing)
989
- return existing;
990
- if (!options.installUv) {
991
- throw new Error(`Compatible uv is unavailable (requires-uv ${options.requiresUv}), and uv installation is disabled.`);
992
- }
993
- const policyKey = import_node_crypto2.createHash("sha256").update(options.requiresUv).digest("hex");
994
- return withProvisioningLock({
995
- key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
996
- timeoutMs: options.coordinationTimeoutMs,
997
- check: () => findAvailableUv(options),
998
- mutate: async () => {
999
- const release = await resolveUvRelease(options);
1000
- const installDir = uvInstallDirectory(options, release.version);
1001
- const executable = uvExecutable(options, release.version);
180
+ const release = await resolveUvRelease(context);
181
+ const destination = uvInstallDirectory(context, release.version);
1002
182
  return withProvisioningLock({
1003
- key: `uv-release:${canonicalPath(installDir)}`,
1004
- timeoutMs: options.coordinationTimeoutMs,
1005
- check: () => findAvailableUv(options),
183
+ key: `uv-release:${canonicalPath(destination)}`,
184
+ check: () => probeUv(uvExecutable(context, release.version), context, release.version),
1006
185
  mutate: async () => {
1007
- await installUv(installDir, release, options);
1008
- const result = probeUv(executable, options);
1009
- if (!result || result.version !== release.version) {
1010
- 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.`);
1011
190
  }
1012
- return result;
191
+ return installed;
1013
192
  }
1014
193
  });
1015
194
  }
1016
195
  });
1017
196
  }
1018
- async function findAvailableUv(options) {
1019
- const external = await findUv(options);
1020
- if (external)
197
+ async function findAvailableUv(context) {
198
+ const external = await findExecutable("uv", context.cwd, context.env);
199
+ if (external && await probeUv(external, context))
1021
200
  return external;
1022
- return findManagedUv(options);
201
+ return findCachedUv(context);
1023
202
  }
1024
- async function findManagedUv(options) {
1025
- const entries = await import_promises2.readdir(uvInstallRoot(options), { withFileTypes: true }).catch(() => []);
1026
- 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);
1027
207
  while (versions.length > 0) {
1028
- const selected = maxPep440Version(versions, options.requiresUv);
1029
- if (!selected)
208
+ const version = import_pep440.maxSatisfying(versions, context.specifier);
209
+ if (!version)
1030
210
  return null;
1031
- const candidate = probeUv(uvExecutable(options, selected), options);
1032
- if (candidate?.version === selected)
1033
- return candidate;
1034
- 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);
1035
215
  }
1036
216
  return null;
1037
217
  }
1038
- function isolatedUvProcessEnvironment(options) {
1039
- return sanitizeUvProcessEnvironment(uvProcessEnvironment(options));
1040
- }
1041
- function explicitPythonSourceProcessEnvironment(options) {
1042
- const env = isolatedUvProcessEnvironment(options);
1043
- delete env["UV_PYTHON_PREFERENCE"];
1044
- return env;
1045
- }
1046
- function targetUvProcessEnvironment(options, python) {
1047
- return uvCommandProcessEnvironment(uvProcessEnvironment(options), python);
1048
- }
1049
- function incompatibleConfiguredPython(options) {
1050
- return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `requires-python ${options.requiresPython}` + `${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
1051
- }
1052
- function missingPythonMessage(options, reason) {
1053
- const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
1054
- return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
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;
225
+ }
1055
226
  }
1056
- var UV_RELEASE_PAGE_SIZE = 100;
1057
- var UV_RELEASE_MAX_PAGES = 20;
1058
- var UV_RELEASE_METADATA_TTL_MS = 24 * 60 * 60000;
1059
- async function resolveUvRelease(options) {
1060
- const catalog = await ensureUvReleaseCatalog(options);
227
+ async function resolveUvRelease(context) {
228
+ const catalog = await ensureUvReleaseCatalog(context);
1061
229
  const asset = uvAsset();
1062
- const releases = catalog.releases.filter((release) => release.assets.includes(asset) && release.assets.includes(`${asset}.sha256`));
1063
- const version = maxPep440Version(releases.map((release) => release.version), options.requiresUv);
1064
- const release = releases.find((candidate) => candidate.version === version);
1065
- if (!release) {
1066
- throw new Error(`No published uv release satisfies requires-uv ${options.requiresUv}.`);
1067
- }
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}.`);
1068
235
  return release;
1069
236
  }
1070
- async function ensureUvReleaseCatalog(options) {
1071
- const file = uvReleaseMetadataFile(options);
237
+ async function ensureUvReleaseCatalog(context) {
238
+ const file = uvReleaseMetadataFile(context);
1072
239
  const stale = await readUvReleaseCatalog(file);
1073
240
  try {
1074
241
  return await withProvisioningLock({
1075
242
  key: `uv-metadata:${canonicalPath(file)}`,
1076
- timeoutMs: options.coordinationTimeoutMs,
1077
243
  check: () => readFreshUvReleaseCatalog(file),
1078
244
  mutate: async () => {
1079
- const catalog = await fetchUvReleaseCatalog(options);
245
+ const catalog = await fetchUvReleaseCatalog(context.env);
1080
246
  await writeUvReleaseCatalog(file, catalog);
1081
247
  return catalog;
1082
248
  }
@@ -1088,30 +254,32 @@ async function ensureUvReleaseCatalog(options) {
1088
254
  throw error;
1089
255
  }
1090
256
  }
1091
- async function fetchUvReleaseCatalog(options) {
1092
- const dispatcher = proxyDispatcher(options.env);
257
+ async function fetchUvReleaseCatalog(env) {
258
+ const dispatcher = proxyDispatcher(env);
1093
259
  const releases = new Map;
1094
260
  let exhausted = false;
1095
261
  try {
1096
- for (let page = 1;page <= UV_RELEASE_MAX_PAGES; page += 1) {
1097
- const text = await downloadText(`https://api.github.com/repos/astral-sh/uv/releases?per_page=${UV_RELEASE_PAGE_SIZE}&page=${page}`, options.downloadTimeoutMs, dispatcher, githubApiHeaders(options.env));
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));
1098
264
  const payload = JSON.parse(text);
1099
- if (!Array.isArray(payload)) {
265
+ if (!Array.isArray(payload))
1100
266
  throw new Error("GitHub returned invalid uv release metadata.");
1101
- }
1102
267
  for (const candidate of payload) {
1103
268
  if (!candidate || typeof candidate !== "object")
1104
269
  continue;
1105
- const release = candidate;
1106
- if (release.draft === true || typeof release.tag_name !== "string")
270
+ const value = candidate;
271
+ if (value.draft === true || typeof value.tag_name !== "string")
1107
272
  continue;
1108
- const assetNames = releaseAssetNames(release.assets);
1109
- const version = normalizePep440Version(release.tag_name);
273
+ const version = import_pep440.valid(value.tag_name);
1110
274
  if (version) {
1111
- 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
+ });
1112
280
  }
1113
281
  }
1114
- if (payload.length < UV_RELEASE_PAGE_SIZE) {
282
+ if (payload.length < RELEASE_PAGE_SIZE) {
1115
283
  exhausted = true;
1116
284
  break;
1117
285
  }
@@ -1119,16 +287,14 @@ async function fetchUvReleaseCatalog(options) {
1119
287
  } finally {
1120
288
  await dispatcher?.close();
1121
289
  }
1122
- if (!exhausted) {
1123
- throw new Error(`uv release catalog exceeded ${UV_RELEASE_MAX_PAGES} pages.`);
1124
- }
1125
- if (releases.size === 0) {
290
+ if (!exhausted)
291
+ throw new Error(`uv release catalog exceeded ${RELEASE_MAX_PAGES} pages.`);
292
+ if (releases.size === 0)
1126
293
  throw new Error("GitHub returned no usable uv releases.");
1127
- }
1128
294
  return { fetchedAt: Date.now(), releases: [...releases.values()] };
1129
295
  }
1130
296
  function githubApiHeaders(env) {
1131
- 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);
1132
298
  if (token && /[\r\n]/u.test(token)) {
1133
299
  throw new TypeError("GitHub API token must not contain line breaks.");
1134
300
  }
@@ -1144,11 +310,11 @@ async function readFreshUvReleaseCatalog(file) {
1144
310
  if (!catalog)
1145
311
  return null;
1146
312
  const age = Date.now() - catalog.fetchedAt;
1147
- return age >= 0 && age < UV_RELEASE_METADATA_TTL_MS ? catalog : null;
313
+ return age >= 0 && age < RELEASE_METADATA_TTL_MS ? catalog : null;
1148
314
  }
1149
315
  async function readUvReleaseCatalog(file) {
1150
316
  try {
1151
- return parseUvReleaseCatalog(JSON.parse(await import_promises2.readFile(file, "utf8")));
317
+ return parseUvReleaseCatalog(JSON.parse(await import_promises.readFile(file, "utf8")));
1152
318
  } catch {
1153
319
  return null;
1154
320
  }
@@ -1157,17 +323,15 @@ function parseUvReleaseCatalog(value) {
1157
323
  if (!value || typeof value !== "object")
1158
324
  return null;
1159
325
  const catalog = value;
1160
- 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))
1161
327
  return null;
1162
- }
1163
328
  const releases = [];
1164
- for (const value of catalog.releases) {
1165
- if (!value || typeof value !== "object")
329
+ for (const candidate of catalog.releases) {
330
+ if (!candidate || typeof candidate !== "object")
1166
331
  return null;
1167
- const release = value;
1168
- 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)
1169
334
  return null;
1170
- }
1171
335
  releases.push({
1172
336
  assets: release.assets,
1173
337
  tag: release.tag,
@@ -1177,62 +341,40 @@ function parseUvReleaseCatalog(value) {
1177
341
  return { fetchedAt: catalog.fetchedAt, releases };
1178
342
  }
1179
343
  async function writeUvReleaseCatalog(file, catalog) {
1180
- await import_promises2.mkdir(import_node_path6.default.dirname(file), { recursive: true });
1181
- const staged = import_node_path6.default.join(import_node_path6.default.dirname(file), `.${import_node_path6.default.basename(file)}.${process.pid}.${import_node_crypto2.randomUUID()}.tmp`);
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`);
1182
346
  try {
1183
- await import_promises2.writeFile(staged, `${JSON.stringify(catalog)}
347
+ await import_promises.writeFile(staged, `${JSON.stringify(catalog)}
1184
348
  `, { encoding: "utf8", flag: "wx" });
1185
349
  if (process.platform === "win32")
1186
- await import_promises2.rm(file, { force: true });
1187
- await import_promises2.rename(staged, file);
350
+ await import_promises.rm(file, { force: true });
351
+ await import_promises.rename(staged, file);
1188
352
  } finally {
1189
- await import_promises2.rm(staged, { force: true });
353
+ await import_promises.rm(staged, { force: true });
1190
354
  }
1191
355
  }
1192
- function uvReleaseMetadataFile(options) {
1193
- return import_node_path6.default.join(options.cacheDir, "metadata", "uv-releases.json");
1194
- }
1195
- function releaseAssetNames(value) {
1196
- if (!Array.isArray(value))
1197
- return [];
1198
- return value.flatMap((entry) => {
1199
- if (!entry || typeof entry !== "object")
1200
- return [];
1201
- const name = entry.name;
1202
- return typeof name === "string" ? [name] : [];
1203
- });
1204
- }
1205
- function uvInstallRoot(options) {
1206
- return import_node_path6.default.join(options.cacheDir, "tools", "uv");
1207
- }
1208
- function uvInstallDirectory(options, version) {
1209
- return import_node_path6.default.join(uvInstallRoot(options), version);
1210
- }
1211
- function uvExecutable(options, version) {
1212
- return import_node_path6.default.join(uvInstallDirectory(options, version), process.platform === "win32" ? "uv.exe" : "uv");
1213
- }
1214
- async function installUv(installDir, release, options) {
356
+ async function installUv(release, context) {
1215
357
  const asset = uvAsset();
1216
358
  const baseUrl = `https://github.com/astral-sh/uv/releases/download/${release.tag}`;
1217
- const temporary = await import_promises2.mkdtemp(import_node_path6.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
1218
- 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);
1219
361
  try {
1220
- const archive = import_node_path6.default.join(temporary, asset);
1221
- await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
1222
- 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];
1223
365
  const actual = await sha256File(archive);
1224
366
  if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
1225
- throw new Error(`uv archive checksum mismatch for ${asset}`);
367
+ throw new Error(`uv archive checksum mismatch for ${asset}.`);
1226
368
  }
1227
- const extracted = import_node_path6.default.join(temporary, "extracted");
1228
- await import_promises2.mkdir(extracted, { recursive: true });
369
+ const extracted = import_node_path2.default.join(temporary, "extracted");
370
+ await import_promises.mkdir(extracted, { recursive: true });
1229
371
  const source = await extractUvExecutable(archive, asset, extracted);
1230
372
  if (!source)
1231
- throw new Error(`uv archive did not contain the expected executable: ${asset}`);
1232
- 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);
1233
375
  } finally {
1234
376
  await dispatcher?.close();
1235
- await import_promises2.rm(temporary, { recursive: true, force: true });
377
+ await import_promises.rm(temporary, { recursive: true, force: true });
1236
378
  }
1237
379
  }
1238
380
  async function extractUvExecutable(archive, asset, destination) {
@@ -1241,58 +383,46 @@ async function extractUvExecutable(archive, asset, destination) {
1241
383
  await tar.x({ file: archive, cwd: destination, strict: true });
1242
384
  return findFile(destination, executableName);
1243
385
  }
1244
- const entries = import_fflate.unzipSync(await import_promises2.readFile(archive), {
1245
- filter: ({ name }) => import_node_path6.default.posix.basename(name.replaceAll("\\", "/")) === executableName
386
+ const entries = import_fflate.unzipSync(await import_promises.readFile(archive), {
387
+ filter: ({ name }) => import_node_path2.default.posix.basename(name.replaceAll("\\", "/")) === executableName
1246
388
  });
1247
- const matches = Object.entries(entries).filter(([name]) => import_node_path6.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
389
+ const matches = Object.entries(entries).filter(([name]) => import_node_path2.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
1248
390
  if (matches.length !== 1 || !matches[0])
1249
391
  return null;
1250
- const source = import_node_path6.default.join(destination, executableName);
1251
- await import_promises2.writeFile(source, matches[0][1], { flag: "wx" });
392
+ const source = import_node_path2.default.join(destination, executableName);
393
+ await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
1252
394
  return source;
1253
395
  }
1254
- async function publishUvAtomically(source, installDir, version, options) {
1255
- await import_promises2.mkdir(installDir, { recursive: true });
1256
- const executable = uvExecutable(options, version);
1257
- const staged = import_node_path6.default.join(installDir, `.${import_node_path6.default.basename(executable)}.${process.pid}.${import_node_crypto2.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`);
1258
401
  try {
1259
- await import_promises2.copyFile(source, staged);
402
+ await import_promises.copyFile(source, staged);
1260
403
  if (process.platform !== "win32")
1261
- await import_promises2.chmod(staged, 493);
1262
- const stagedUv = probeUv(staged, options);
1263
- if (!stagedUv || stagedUv.version !== version) {
404
+ await import_promises.chmod(staged, 493);
405
+ if (!await probeUv(staged, { ...context, specifier: `==${version}` })) {
1264
406
  throw new Error(`Downloaded uv executable is not version ${version}.`);
1265
407
  }
1266
408
  if (process.platform === "win32")
1267
- await import_promises2.rm(executable, { force: true });
1268
- await import_promises2.rename(staged, executable);
409
+ await import_promises.rm(executable, { force: true });
410
+ await import_promises.rename(staged, executable);
1269
411
  } finally {
1270
- await import_promises2.rm(staged, { force: true });
412
+ await import_promises.rm(staged, { force: true });
1271
413
  }
1272
414
  }
1273
- function uvAsset() {
1274
- const architecture = process.arch === "x64" ? "x86_64" : process.arch === "arm64" ? "aarch64" : null;
1275
- if (!architecture)
1276
- throw new Error(`uv bootstrap does not support architecture ${process.arch}.`);
1277
- if (process.platform === "win32")
1278
- return `uv-${architecture}-pc-windows-msvc.zip`;
1279
- if (process.platform === "darwin")
1280
- return `uv-${architecture}-apple-darwin.tar.gz`;
1281
- if (process.platform === "linux")
1282
- return `uv-${architecture}-unknown-linux-musl.tar.gz`;
1283
- throw new Error(`uv bootstrap does not support platform ${process.platform}.`);
1284
- }
1285
- async function download(url, destination, timeoutMs, dispatcher) {
415
+ async function download(url, destination, dispatcher) {
1286
416
  const response = await import_undici.fetch(url, {
1287
417
  dispatcher,
1288
418
  redirect: "follow",
1289
- signal: AbortSignal.timeout(timeoutMs)
419
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
1290
420
  });
1291
421
  if (!response.ok || !response.body) {
1292
422
  await response.body?.cancel();
1293
- throw new Error(`Could not download ${url}: HTTP ${response.status}`);
423
+ throw new Error(`Could not download ${url}: HTTP ${response.status}.`);
1294
424
  }
1295
- await import_promises3.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" }));
1296
426
  }
1297
427
  async function downloadText(url, timeoutMs, dispatcher, headers) {
1298
428
  const response = await import_undici.fetch(url, {
@@ -1303,33 +433,30 @@ async function downloadText(url, timeoutMs, dispatcher, headers) {
1303
433
  });
1304
434
  if (!response.ok) {
1305
435
  await response.body?.cancel();
1306
- throw new Error(`Could not download ${url}: HTTP ${response.status}`);
436
+ throw new Error(`Could not download ${url}: HTTP ${response.status}.`);
1307
437
  }
1308
438
  return response.text();
1309
439
  }
1310
440
  function proxyDispatcher(env) {
1311
- const httpProxy = processEnvironmentValue(env, "http_proxy", "HTTP_PROXY");
1312
- const httpsProxy = processEnvironmentValue(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");
1313
443
  if (!httpProxy && !httpsProxy)
1314
444
  return;
1315
445
  return new import_undici.EnvHttpProxyAgent({
1316
446
  httpProxy,
1317
447
  httpsProxy,
1318
- noProxy: processEnvironmentValue(env, "no_proxy", "NO_PROXY")
448
+ noProxy: environmentValue(env, "no_proxy") ?? environmentValue(env, "NO_PROXY")
1319
449
  });
1320
450
  }
1321
- function processEnvironmentValue(env, lowercase, uppercase) {
1322
- return env[lowercase] ?? env[uppercase];
1323
- }
1324
451
  async function sha256File(file) {
1325
- const hash = import_node_crypto2.createHash("sha256");
1326
- for await (const chunk of import_node_fs3.createReadStream(file))
452
+ const hash = import_node_crypto.createHash("sha256");
453
+ for await (const chunk of import_node_fs.createReadStream(file))
1327
454
  hash.update(chunk);
1328
455
  return hash.digest("hex");
1329
456
  }
1330
457
  async function findFile(directory, name) {
1331
- for (const entry of await import_promises2.readdir(directory, { withFileTypes: true })) {
1332
- const target = import_node_path6.default.join(directory, entry.name);
458
+ for (const entry of await import_promises.readdir(directory, { withFileTypes: true })) {
459
+ const target = import_node_path2.default.join(directory, entry.name);
1333
460
  if (entry.isFile() && entry.name === name)
1334
461
  return target;
1335
462
  if (entry.isDirectory()) {
@@ -1340,108 +467,332 @@ async function findFile(directory, name) {
1340
467
  }
1341
468
  return null;
1342
469
  }
1343
-
1344
- // src/runners.ts
1345
- var import_node_child_process2 = require("node:child_process");
1346
-
1347
- // src/runtime-files.ts
1348
- var import_node_fs4 = __toESM(require("node:fs"), 1);
1349
- var import_node_path7 = __toESM(require("node:path"), 1);
1350
- function runtimeFile(name) {
1351
- const moduleDirectory = __dirname;
1352
- const compiled = import_node_path7.default.join(moduleDirectory, `${name}.js`);
1353
- return import_node_fs4.default.existsSync(compiled) ? compiled : import_node_path7.default.join(moduleDirectory, `${name}.ts`);
1354
- }
1355
-
1356
- // src/runners.ts
1357
- async function uvSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
1358
- const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1359
- const resolved = resolveOptions(options);
1360
- const python = uvCommandNeedsPython(args) ? await ensurePython(resolved) : undefined;
1361
- const uv = python?.uv ?? await ensureUv(resolved);
1362
- const command = buildUvCommand(uv, args, resolved, python?.python);
1363
- return import_node_child_process2.spawn(command.executable, command.args, {
1364
- ...spawnOptions,
1365
- 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
+ }
1366
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");
1367
495
  }
1368
- function uvSpawnSync(args = [], spawnOptions = {}, pyfigOptions = {}) {
1369
- const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
1370
- const resolved = resolveOptions(options);
1371
- const python = uvCommandNeedsPython(args) ? resolveSynchronously("python", resolved) : undefined;
1372
- const uv = python?.uv ?? resolveSynchronously("uv", resolved);
1373
- const command = buildUvCommand(uv, args, resolved, python?.python);
1374
- return import_node_child_process2.spawnSync(command.executable, command.args, {
1375
- ...spawnOptions,
1376
- env: command.env
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);
501
+ }
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] : [];
1377
528
  });
1378
529
  }
1379
- function buildUvCommand(uv, args, options, python) {
1380
- return {
1381
- executable: uv.executable,
1382
- args: python ? targetUvArguments(args, python) : [...args],
1383
- env: uvCommandProcessEnvironment(uvProcessEnvironment(options), python)
1384
- };
530
+ function canonicalPath(value) {
531
+ const normalized = import_node_path2.default.resolve(value);
532
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
533
+ }
534
+ function uniqueSuffix() {
535
+ return `${process.pid}.${import_node_crypto.randomUUID()}`;
1385
536
  }
1386
- function optionsForInvocation(pyfigOptions, spawnEnv) {
1387
- return spawnEnv === undefined ? pyfigOptions : { ...pyfigOptions, env: spawnEnv };
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
+ }
1388
605
  }
1389
- function resolveSynchronously(target, options) {
1390
- const resolved = resolveOptions(options);
1391
- const request = { target, options: resolved };
1392
- const result = import_node_child_process2.spawnSync(process.execPath, [runtimeFile("sync-resolver")], {
1393
- input: JSON.stringify(request),
1394
- encoding: "utf8",
1395
- env: resolved.env,
1396
- timeout: synchronousResolutionTimeout(resolved),
1397
- maxBuffer: 4 * 1024 * 1024,
1398
- windowsHide: true
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
1399
625
  });
1400
- if (result.status !== 0 || result.error) {
1401
- const detail = result.stderr.trim() || result.error?.message || `exit ${result.status}`;
1402
- 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);
670
+ }
671
+ return selectDiscoveredPython(null, options);
672
+ }
673
+ async function selectDiscoveredPython(specifier, options) {
674
+ const request = specifier ? [specifier] : [];
675
+ let systemFailure;
676
+ try {
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;
682
+ } catch (error) {
683
+ systemFailure = error;
1403
684
  }
1404
685
  try {
1405
- return JSON.parse(result.stdout);
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;
1406
691
  } catch (error) {
1407
- const detail = error instanceof Error ? error.message : String(error);
1408
- throw new Error(`Could not parse synchronous ${target} resolution: ${detail}`);
692
+ throw new Error(`No compatible Python was found. System: ${errorMessage(systemFailure)} Managed: ${errorMessage(error)}`, { cause: error });
1409
693
  }
1410
694
  }
1411
- function synchronousResolutionTimeout(options) {
1412
- 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);
1413
702
  }
1414
-
1415
- // src/client.ts
1416
- class Python {
1417
- #options;
1418
- #python;
1419
- #uvInfo;
1420
- constructor(options = {}) {
1421
- this.#options = {
1422
- ...options,
1423
- dependencies: options.dependencies ? [...options.dependencies] : undefined,
1424
- requirements: options.requirements ? [...options.requirements] : undefined,
1425
- env: options.env ? { ...options.env } : undefined
1426
- };
1427
- this.#python = import_loqkit2.memoize(() => ensurePython(this.#options));
1428
- this.#uvInfo = import_loqkit2.memoize(() => ensureUv(this.#options));
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.`);
1429
716
  }
1430
- prepare() {
1431
- return this.#python();
717
+ if (!import_node_path3.default.isAbsolute(value.executable)) {
718
+ throw new Error(`${JSON.stringify(executable)} reported a non-absolute executable path.`);
1432
719
  }
1433
- async pythonInfo() {
1434
- return (await this.#python()).python;
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
+ }
1435
743
  }
1436
- uvInfo() {
1437
- return this.#uvInfo();
744
+ if (options.requireGil && python.gilDisabled) {
745
+ throw new Error(`Python ${python.executable} is free-threaded but requireGil is enabled.`);
1438
746
  }
1439
- uv(args = [], options = {}) {
1440
- return uvSpawn(args, options, this.#options);
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";
1441
757
  }
1442
- uvSync(args, options) {
1443
- return uvSpawnSync(args, options, this.#options);
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;
1444
790
  }
791
+ return environment;
792
+ }
793
+ function assertNonempty(name, value) {
794
+ if (value.trim() === "")
795
+ throw new TypeError(`${name} must not be empty.`);
1445
796
  }
1446
797
 
1447
- //# debugId=653756BC4A56AC4F64756E2164756E21
798
+ //# debugId=F23B5C2985518F2764756E2164756E21