pyfig 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -34
- package/SECURITY.md +1 -1
- package/dist/cjs/index.js +409 -261
- package/dist/cjs/index.js.map +10 -9
- package/dist/cjs/sync-resolver.js +369 -236
- package/dist/cjs/sync-resolver.js.map +9 -8
- package/dist/cli.js +12 -3
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +6 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +31 -21
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +9 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +48 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/package-target.d.ts +3 -3
- package/dist/package-target.d.ts.map +1 -1
- package/dist/package-target.js +6 -6
- package/dist/package-target.js.map +1 -1
- package/dist/provision.d.ts +8 -12
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +156 -158
- package/dist/provision.js.map +1 -1
- package/dist/python-venv.d.ts +12 -0
- package/dist/python-venv.d.ts.map +1 -0
- package/dist/{virtual-environment.js → python-venv.js} +12 -9
- package/dist/python-venv.js.map +1 -0
- package/dist/runners.js +12 -12
- package/dist/runners.js.map +1 -1
- package/dist/sync-resolver.js +2 -2
- package/dist/sync-resolver.js.map +1 -1
- package/dist/venv-identity.d.ts +36 -0
- package/dist/venv-identity.d.ts.map +1 -0
- package/dist/venv-identity.js +80 -0
- package/dist/venv-identity.js.map +1 -0
- package/package.json +2 -2
- package/dist/virtual-environment.d.ts +0 -11
- package/dist/virtual-environment.d.ts.map +0 -1
- package/dist/virtual-environment.js.map +0 -1
|
@@ -63,17 +63,16 @@ var __export = (target, all) => {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
// src/sync-resolver.ts
|
|
66
|
-
var
|
|
66
|
+
var import_node_fs4 = require("node:fs");
|
|
67
67
|
|
|
68
68
|
// src/provision.ts
|
|
69
|
-
var
|
|
69
|
+
var import_node_crypto2 = require("node:crypto");
|
|
70
70
|
var import_node_fs3 = require("node:fs");
|
|
71
|
-
var
|
|
72
|
-
var import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
71
|
+
var import_promises2 = require("node:fs/promises");
|
|
73
72
|
var import_node_os = __toESM(require("node:os"), 1);
|
|
74
|
-
var
|
|
73
|
+
var import_node_path6 = __toESM(require("node:path"), 1);
|
|
75
74
|
var import_node_stream = require("node:stream");
|
|
76
|
-
var
|
|
75
|
+
var import_promises3 = require("node:stream/promises");
|
|
77
76
|
var import_fflate = require("fflate");
|
|
78
77
|
var tar = __toESM(require("tar"), 1);
|
|
79
78
|
var import_undici = require("undici");
|
|
@@ -150,7 +149,11 @@ function resolveOptions(options = {}) {
|
|
|
150
149
|
const configuredProjectRoot = options.projectRoot ?? env["PYFIG_PROJECT_ROOT"];
|
|
151
150
|
const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot(env["INIT_CWD"] ?? process.cwd());
|
|
152
151
|
const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
|
|
152
|
+
const dependencies = normalizeDependencies(options.dependencies ?? []);
|
|
153
|
+
const requirements = normalizeRequirements(options.requirements ?? [], projectRoot);
|
|
153
154
|
return {
|
|
155
|
+
dependencies,
|
|
156
|
+
requirements,
|
|
154
157
|
python: options.python ?? env["PYFIG_PYTHON"] ?? "",
|
|
155
158
|
requiresPython,
|
|
156
159
|
managedPython,
|
|
@@ -170,7 +173,10 @@ function resolveOptions(options = {}) {
|
|
|
170
173
|
env
|
|
171
174
|
};
|
|
172
175
|
}
|
|
173
|
-
function
|
|
176
|
+
function hasRuntimeRequirements(options) {
|
|
177
|
+
return options.dependencies.length > 0 || options.requirements.length > 0;
|
|
178
|
+
}
|
|
179
|
+
function uvProcessEnvironment(options) {
|
|
174
180
|
return {
|
|
175
181
|
...options.env,
|
|
176
182
|
UV_PYTHON_INSTALL_DIR: import_node_path2.default.join(options.cacheDir, "python"),
|
|
@@ -212,46 +218,68 @@ function parseBoolean(value, fallback) {
|
|
|
212
218
|
return false;
|
|
213
219
|
throw new TypeError(`Invalid boolean value: ${value}`);
|
|
214
220
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
|
|
221
|
-
key: options.key,
|
|
222
|
-
name: "Pyfig provisioning",
|
|
223
|
-
namespace: "pyfig",
|
|
224
|
-
timeout: options.timeoutMs,
|
|
225
|
-
predicate: async () => {
|
|
226
|
-
const value = await options.check();
|
|
227
|
-
if (value === null)
|
|
228
|
-
return true;
|
|
229
|
-
existing = { value };
|
|
230
|
-
return false;
|
|
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)}`);
|
|
231
226
|
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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);
|
|
238
258
|
}
|
|
239
259
|
|
|
240
|
-
// src/
|
|
260
|
+
// src/venv-identity.ts
|
|
261
|
+
var import_node_crypto = require("node:crypto");
|
|
262
|
+
var import_promises = require("node:fs/promises");
|
|
263
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
264
|
+
|
|
265
|
+
// src/python-venv.ts
|
|
241
266
|
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
242
267
|
var import_node_path3 = __toESM(require("node:path"), 1);
|
|
243
|
-
var
|
|
244
|
-
function
|
|
245
|
-
return import_node_path3.default.join(projectRoot,
|
|
268
|
+
var VIRTUAL_ENV_ROOT_NAME = import_node_path3.default.join("node_modules", ".pyfig", "venvs");
|
|
269
|
+
function pythonVenvRoot(projectRoot) {
|
|
270
|
+
return import_node_path3.default.join(projectRoot, VIRTUAL_ENV_ROOT_NAME);
|
|
246
271
|
}
|
|
247
|
-
function
|
|
272
|
+
function pythonVenvDirectory(projectRoot, venvDirectory) {
|
|
273
|
+
return import_node_path3.default.join(pythonVenvRoot(projectRoot), venvDirectory);
|
|
274
|
+
}
|
|
275
|
+
function externalPythonVenvDirectories(projectRoot, env) {
|
|
248
276
|
const candidates = [env["VIRTUAL_ENV"], import_node_path3.default.join(projectRoot, ".venv")].filter((value) => Boolean(value)).map((value) => import_node_path3.default.resolve(value));
|
|
249
277
|
return [...new Map(candidates.map((value) => [canonicalPath(value), value])).values()];
|
|
250
278
|
}
|
|
251
|
-
function
|
|
252
|
-
return import_node_path3.default.join(
|
|
279
|
+
function pythonVenvExecutable(pythonVenv) {
|
|
280
|
+
return import_node_path3.default.join(pythonVenv, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
|
|
253
281
|
}
|
|
254
|
-
function
|
|
282
|
+
function activatePythonVenv(env, pythonVenv, pythonExecutable) {
|
|
255
283
|
const activated = { ...env };
|
|
256
284
|
const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
|
|
257
285
|
const existingPath = pathEntry ? activated[pathEntry] : undefined;
|
|
@@ -262,7 +290,7 @@ function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
|
|
|
262
290
|
}
|
|
263
291
|
}
|
|
264
292
|
activated["PATH"] = [import_node_path3.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path3.default.delimiter);
|
|
265
|
-
activated["VIRTUAL_ENV"] =
|
|
293
|
+
activated["VIRTUAL_ENV"] = pythonVenv;
|
|
266
294
|
return activated;
|
|
267
295
|
}
|
|
268
296
|
function pathsEqual(left, right) {
|
|
@@ -277,6 +305,102 @@ function canonicalPath(value) {
|
|
|
277
305
|
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
278
306
|
}
|
|
279
307
|
|
|
308
|
+
// src/venv-identity.ts
|
|
309
|
+
var VENV_SCHEMA = 1;
|
|
310
|
+
var VENV_METADATA = ".pyfig-venv.json";
|
|
311
|
+
function createVenvIdentity(python, requirementsSha256) {
|
|
312
|
+
const contract = {
|
|
313
|
+
schema: VENV_SCHEMA,
|
|
314
|
+
runtime: {
|
|
315
|
+
architecture: process.arch,
|
|
316
|
+
platform: process.platform
|
|
317
|
+
},
|
|
318
|
+
python: {
|
|
319
|
+
architecture: python.architecture,
|
|
320
|
+
baseExecutable: canonicalPath(python.executable),
|
|
321
|
+
basePrefix: canonicalPath(python.basePrefix),
|
|
322
|
+
embeddingHeader: python.embedding ? canonicalPath(python.embedding.headerPath) : null,
|
|
323
|
+
embeddingLibrary: python.embedding ? canonicalPath(python.embedding.libraryPath) : null,
|
|
324
|
+
gilDisabled: python.gilDisabled,
|
|
325
|
+
implementation: python.implementation,
|
|
326
|
+
pointerBits: python.pointerBits,
|
|
327
|
+
version: python.version
|
|
328
|
+
},
|
|
329
|
+
requirementsSha256
|
|
330
|
+
};
|
|
331
|
+
const id = sha256(JSON.stringify(contract));
|
|
332
|
+
const implementation = safePathSegment(python.implementation);
|
|
333
|
+
const version = safePathSegment(python.version);
|
|
334
|
+
return {
|
|
335
|
+
contract,
|
|
336
|
+
directoryName: `${implementation}-${version}-${id.slice(0, 20)}`,
|
|
337
|
+
id
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function requirementsDigest(content) {
|
|
341
|
+
return sha256(content);
|
|
342
|
+
}
|
|
343
|
+
function venvMetadataFile(directory) {
|
|
344
|
+
return import_node_path4.default.join(directory, VENV_METADATA);
|
|
345
|
+
}
|
|
346
|
+
async function venvMatches(directory, venv) {
|
|
347
|
+
try {
|
|
348
|
+
const parsed = JSON.parse(await import_promises.readFile(venvMetadataFile(directory), "utf8"));
|
|
349
|
+
return parsed.venvId === venv.id && JSON.stringify(parsed.contract) === JSON.stringify(venv.contract);
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function writeVenvMetadata(directory, venv) {
|
|
355
|
+
await import_promises.mkdir(directory, { recursive: true });
|
|
356
|
+
const file = venvMetadataFile(directory);
|
|
357
|
+
const staged = `${file}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`;
|
|
358
|
+
const metadata = {
|
|
359
|
+
contract: venv.contract,
|
|
360
|
+
venvId: venv.id
|
|
361
|
+
};
|
|
362
|
+
try {
|
|
363
|
+
await import_promises.writeFile(staged, `${JSON.stringify(metadata, null, 2)}
|
|
364
|
+
`, {
|
|
365
|
+
encoding: "utf8",
|
|
366
|
+
flag: "wx"
|
|
367
|
+
});
|
|
368
|
+
await import_promises.rename(staged, file);
|
|
369
|
+
} finally {
|
|
370
|
+
await import_promises.rm(staged, { force: true });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function safePathSegment(value) {
|
|
374
|
+
return value.toLowerCase().replace(/[^a-z0-9.-]+/gu, "-").replace(/^-+|-+$/gu, "") || "python";
|
|
375
|
+
}
|
|
376
|
+
function sha256(value) {
|
|
377
|
+
return import_node_crypto.createHash("sha256").update(value).digest("hex");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/provision-lock.ts
|
|
381
|
+
var import_loqkit = require("loqkit");
|
|
382
|
+
async function withProvisioningLock(options) {
|
|
383
|
+
let existing;
|
|
384
|
+
const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
|
|
385
|
+
key: options.key,
|
|
386
|
+
name: "Pyfig provisioning",
|
|
387
|
+
namespace: "pyfig",
|
|
388
|
+
timeout: options.timeoutMs,
|
|
389
|
+
predicate: async () => {
|
|
390
|
+
const value = await options.check();
|
|
391
|
+
if (value === null)
|
|
392
|
+
return true;
|
|
393
|
+
existing = { value };
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
if (result.context.state === "executed")
|
|
398
|
+
return result.value;
|
|
399
|
+
if (existing)
|
|
400
|
+
return existing.value;
|
|
401
|
+
throw new Error(`Provisioning check for ${options.key} did not produce a resource.`);
|
|
402
|
+
}
|
|
403
|
+
|
|
280
404
|
// src/package-target.ts
|
|
281
405
|
var TARGET_OPTIONS = new Set([
|
|
282
406
|
"--active",
|
|
@@ -309,7 +433,7 @@ var TARGET_ENVIRONMENT_VARIABLES = new Set([
|
|
|
309
433
|
"UV_SYSTEM_PYTHON",
|
|
310
434
|
"UV_TARGET"
|
|
311
435
|
]);
|
|
312
|
-
function
|
|
436
|
+
function uvCommandNeedsPython(args) {
|
|
313
437
|
return args[0] === "pip" || args[0] === "run";
|
|
314
438
|
}
|
|
315
439
|
function targetUvArguments(args, python) {
|
|
@@ -325,16 +449,16 @@ function targetUvArguments(args, python) {
|
|
|
325
449
|
}
|
|
326
450
|
return [...args];
|
|
327
451
|
}
|
|
328
|
-
function
|
|
329
|
-
const sanitized =
|
|
452
|
+
function uvCommandProcessEnvironment(env, python) {
|
|
453
|
+
const sanitized = sanitizeUvProcessEnvironment(env);
|
|
330
454
|
if (!python?.virtualEnvironment)
|
|
331
455
|
return sanitized;
|
|
332
456
|
return {
|
|
333
|
-
...
|
|
457
|
+
...activatePythonVenv(sanitized, python.virtualEnvironment, python.executable),
|
|
334
458
|
UV_PYTHON: python.executable
|
|
335
459
|
};
|
|
336
460
|
}
|
|
337
|
-
function
|
|
461
|
+
function sanitizeUvProcessEnvironment(env) {
|
|
338
462
|
const sanitized = { ...env };
|
|
339
463
|
for (const key of Object.keys(sanitized)) {
|
|
340
464
|
if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
|
|
@@ -358,12 +482,12 @@ function assertNoTargetOverride(args) {
|
|
|
358
482
|
|
|
359
483
|
// src/process.ts
|
|
360
484
|
var import_node_child_process = require("node:child_process");
|
|
361
|
-
var
|
|
485
|
+
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
362
486
|
var import_which = __toESM(require("which"), 1);
|
|
363
487
|
async function resolveExecutable(value, env) {
|
|
364
488
|
if (!value)
|
|
365
489
|
return null;
|
|
366
|
-
if (
|
|
490
|
+
if (import_node_path5.default.isAbsolute(value)) {
|
|
367
491
|
return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
|
|
368
492
|
}
|
|
369
493
|
if (!/^[A-Za-z0-9_.-]+$/u.test(value))
|
|
@@ -380,7 +504,7 @@ function run(executable, args, env, timeoutMs) {
|
|
|
380
504
|
});
|
|
381
505
|
if (result.status !== 0 || result.error) {
|
|
382
506
|
const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
|
|
383
|
-
throw new Error(`${
|
|
507
|
+
throw new Error(`${import_node_path5.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
|
|
384
508
|
}
|
|
385
509
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
386
510
|
}
|
|
@@ -409,11 +533,11 @@ async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
|
|
|
409
533
|
const abort = () => {
|
|
410
534
|
child.kill();
|
|
411
535
|
const reason = signal?.reason;
|
|
412
|
-
finish(reason instanceof Error ? reason : new Error(`${
|
|
536
|
+
finish(reason instanceof Error ? reason : new Error(`${import_node_path5.default.basename(executable)} was aborted.`));
|
|
413
537
|
};
|
|
414
538
|
const timer = setTimeout(() => {
|
|
415
539
|
child.kill();
|
|
416
|
-
finish(new Error(`${
|
|
540
|
+
finish(new Error(`${import_node_path5.default.basename(executable)} timed out after ${timeoutMs}ms.`));
|
|
417
541
|
}, timeoutMs);
|
|
418
542
|
child.stdout.setEncoding("utf8");
|
|
419
543
|
child.stderr.setEncoding("utf8");
|
|
@@ -421,21 +545,21 @@ async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
|
|
|
421
545
|
stdout += chunk;
|
|
422
546
|
if (stdout.length > 4 * 1024 * 1024) {
|
|
423
547
|
child.kill();
|
|
424
|
-
finish(new Error(`${
|
|
548
|
+
finish(new Error(`${import_node_path5.default.basename(executable)} exceeded the output limit.`));
|
|
425
549
|
}
|
|
426
550
|
});
|
|
427
551
|
child.stderr.on("data", (chunk) => {
|
|
428
552
|
stderr += chunk;
|
|
429
553
|
if (stderr.length > 4 * 1024 * 1024) {
|
|
430
554
|
child.kill();
|
|
431
|
-
finish(new Error(`${
|
|
555
|
+
finish(new Error(`${import_node_path5.default.basename(executable)} exceeded the error-output limit.`));
|
|
432
556
|
}
|
|
433
557
|
});
|
|
434
558
|
child.once("error", (error) => finish(error));
|
|
435
559
|
child.once("close", (code, childSignal) => {
|
|
436
560
|
if (code !== 0) {
|
|
437
561
|
const detail = stderr.trim() || (childSignal ? `signal ${childSignal}` : `exit ${code}`);
|
|
438
|
-
finish(new Error(`${
|
|
562
|
+
finish(new Error(`${import_node_path5.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
|
|
439
563
|
return;
|
|
440
564
|
}
|
|
441
565
|
finish(undefined, { stdout, stderr });
|
|
@@ -589,59 +713,71 @@ function pythonArchitectureMatchesRuntime(python) {
|
|
|
589
713
|
async function ensureUv(options = {}) {
|
|
590
714
|
return ensureUvResolved(resolveOptions(options));
|
|
591
715
|
}
|
|
592
|
-
async function
|
|
716
|
+
async function ensurePython(options = {}) {
|
|
593
717
|
const resolved = resolveOptions(options);
|
|
594
|
-
|
|
595
|
-
const selections = normalizeDependencyOptions(dependencyOptions);
|
|
718
|
+
await validateRequirementsFiles(resolved.requirements);
|
|
596
719
|
const uv = await ensureUvResolved(resolved);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
720
|
+
if (!hasRuntimeRequirements(resolved)) {
|
|
721
|
+
const external = await findReusableExternalPythonVenv(resolved);
|
|
722
|
+
if (external) {
|
|
723
|
+
return { managed: false, python: external, uv, venvId: null };
|
|
724
|
+
}
|
|
600
725
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
726
|
+
const basePython = await ensureBasePythonResolved(uv, resolved);
|
|
727
|
+
const resolution = await resolveRuntimeDependencies(uv, basePython, resolved);
|
|
728
|
+
const venv = createVenvIdentity(basePython, resolution.sha256);
|
|
729
|
+
const python = await ensureManagedPythonVenv(uv, basePython, venv, resolution, resolved);
|
|
730
|
+
return { managed: true, python, uv, venvId: venv.id };
|
|
731
|
+
}
|
|
732
|
+
async function ensureManagedPythonVenv(uv, basePython, venv, resolution, options) {
|
|
733
|
+
const directory = pythonVenvDirectory(options.projectRoot, venv.directoryName);
|
|
734
|
+
const check = async () => {
|
|
735
|
+
if (!await venvMatches(directory, venv))
|
|
736
|
+
return null;
|
|
737
|
+
const candidate = await inspectPythonVenv(directory, options);
|
|
738
|
+
return candidate && pythonSatisfiesPolicy(candidate, options) ? candidate : null;
|
|
605
739
|
};
|
|
606
|
-
|
|
607
|
-
async function ensureVirtualEnvironmentResolved(options, uv) {
|
|
608
|
-
const existing = await findReusableEnvironment(options);
|
|
740
|
+
const existing = await check();
|
|
609
741
|
if (existing)
|
|
610
742
|
return existing;
|
|
611
|
-
const directory = virtualEnvironmentDirectory(options.projectRoot);
|
|
612
|
-
const check = async () => {
|
|
613
|
-
const candidate = await inspectVirtualEnvironment(directory, options);
|
|
614
|
-
return candidate && pythonSatisfiesPolicy(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
|
|
615
|
-
};
|
|
616
743
|
if (!options.createVirtualEnvironment) {
|
|
617
744
|
throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
|
|
618
745
|
}
|
|
619
|
-
const basePython = await ensureBasePythonResolved(uv, options);
|
|
620
746
|
return withProvisioningLock({
|
|
621
747
|
key: `venv:${canonicalPath(directory)}`,
|
|
622
748
|
timeoutMs: options.coordinationTimeoutMs,
|
|
623
749
|
check,
|
|
624
750
|
mutate: async () => {
|
|
625
|
-
await
|
|
626
|
-
await
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
751
|
+
await import_promises2.rm(directory, { recursive: true, force: true });
|
|
752
|
+
await import_promises2.mkdir(import_node_path6.default.dirname(directory), { recursive: true });
|
|
753
|
+
try {
|
|
754
|
+
await runAsync(uv.executable, [
|
|
755
|
+
"venv",
|
|
756
|
+
"--no-project",
|
|
757
|
+
"--no-config",
|
|
758
|
+
"--no-python-downloads",
|
|
759
|
+
"--python",
|
|
760
|
+
basePython.executable,
|
|
761
|
+
directory
|
|
762
|
+
], isolatedUvProcessEnvironment(options), options.installTimeoutMs);
|
|
763
|
+
const created = await inspectPythonVenv(directory, options);
|
|
764
|
+
if (!created || !pythonSatisfiesPolicy(created, options)) {
|
|
765
|
+
throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
|
|
766
|
+
}
|
|
767
|
+
await installResolvedDependencies(uv, created, resolution, options);
|
|
768
|
+
await writeVenvMetadata(directory, venv);
|
|
769
|
+
const installed = await check();
|
|
770
|
+
if (!installed)
|
|
771
|
+
throw new Error(`Pyfig could not validate environment ${directory}.`);
|
|
772
|
+
return installed;
|
|
773
|
+
} catch (error) {
|
|
774
|
+
await import_promises2.rm(directory, { recursive: true, force: true });
|
|
775
|
+
throw error;
|
|
639
776
|
}
|
|
640
|
-
return installed;
|
|
641
777
|
}
|
|
642
778
|
});
|
|
643
779
|
}
|
|
644
|
-
async function
|
|
780
|
+
async function findReusableExternalPythonVenv(options) {
|
|
645
781
|
if (options.python) {
|
|
646
782
|
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
647
783
|
if (!configured || !pythonSatisfiesPolicy(configured, options)) {
|
|
@@ -650,14 +786,10 @@ async function findReusableEnvironment(options) {
|
|
|
650
786
|
if (configured.virtualEnvironment)
|
|
651
787
|
return configured;
|
|
652
788
|
}
|
|
653
|
-
for (const directory of
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
]) {
|
|
657
|
-
const info = await inspectVirtualEnvironment(directory, options);
|
|
658
|
-
if (info && pythonSatisfiesPolicy(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
|
|
789
|
+
for (const directory of externalPythonVenvDirectories(options.projectRoot, options.env)) {
|
|
790
|
+
const info = await inspectPythonVenv(directory, options);
|
|
791
|
+
if (info && pythonSatisfiesPolicy(info, options))
|
|
659
792
|
return info;
|
|
660
|
-
}
|
|
661
793
|
}
|
|
662
794
|
return null;
|
|
663
795
|
}
|
|
@@ -674,6 +806,9 @@ async function ensureBasePythonResolved(uv, options) {
|
|
|
674
806
|
return base;
|
|
675
807
|
throw incompatibleConfiguredPython(options);
|
|
676
808
|
}
|
|
809
|
+
const externalBase = await findExternalBasePython(options);
|
|
810
|
+
if (externalBase)
|
|
811
|
+
return externalBase;
|
|
677
812
|
const system = await findPythonWithUv(uv, options, "system");
|
|
678
813
|
if (system)
|
|
679
814
|
return system;
|
|
@@ -684,11 +819,11 @@ async function ensureBasePythonResolved(uv, options) {
|
|
|
684
819
|
throw new Error(missingPythonMessage(options, "managed Python installation is disabled"));
|
|
685
820
|
}
|
|
686
821
|
return withProvisioningLock({
|
|
687
|
-
key: `python:${canonicalPath(
|
|
822
|
+
key: `python:${canonicalPath(import_node_path6.default.join(options.cacheDir, "python"))}`,
|
|
688
823
|
timeoutMs: options.coordinationTimeoutMs,
|
|
689
824
|
check: () => findPythonWithUv(uv, options, "managed"),
|
|
690
825
|
mutate: async () => {
|
|
691
|
-
await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython],
|
|
826
|
+
await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], isolatedUvProcessEnvironment(options), options.installTimeoutMs);
|
|
692
827
|
const installed = await findPythonWithUv(uv, options, "managed");
|
|
693
828
|
if (!installed) {
|
|
694
829
|
throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
|
|
@@ -697,6 +832,17 @@ async function ensureBasePythonResolved(uv, options) {
|
|
|
697
832
|
}
|
|
698
833
|
});
|
|
699
834
|
}
|
|
835
|
+
async function findExternalBasePython(options) {
|
|
836
|
+
for (const directory of externalPythonVenvDirectories(options.projectRoot, options.env)) {
|
|
837
|
+
const environment = await inspectPythonVenv(directory, options);
|
|
838
|
+
if (!environment || !pythonSatisfiesPolicy(environment, options))
|
|
839
|
+
continue;
|
|
840
|
+
const base = await probePython({ executable: environment.baseExecutable, args: [] }, options);
|
|
841
|
+
if (base && !base.virtualEnvironment && pythonSatisfiesPolicy(base, options))
|
|
842
|
+
return base;
|
|
843
|
+
}
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
700
846
|
async function findPythonWithUv(uv, options, source) {
|
|
701
847
|
for (const request of ["", options.managedPython]) {
|
|
702
848
|
const args = [
|
|
@@ -710,7 +856,7 @@ async function findPythonWithUv(uv, options, source) {
|
|
|
710
856
|
if (request)
|
|
711
857
|
args.push(request);
|
|
712
858
|
try {
|
|
713
|
-
const { stdout } = await runAsync(uv.executable, args,
|
|
859
|
+
const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceProcessEnvironment(options), options.probeTimeoutMs);
|
|
714
860
|
const info = await probePython({ executable: stdout.trim(), args: [] }, options);
|
|
715
861
|
if (info && pythonSatisfiesPolicy(info, options))
|
|
716
862
|
return info;
|
|
@@ -726,7 +872,7 @@ async function findPythonWithUv(uv, options, source) {
|
|
|
726
872
|
"--output-format",
|
|
727
873
|
"json",
|
|
728
874
|
source === "system" ? "--no-managed-python" : "--managed-python"
|
|
729
|
-
],
|
|
875
|
+
], explicitPythonSourceProcessEnvironment(options), options.probeTimeoutMs);
|
|
730
876
|
const candidates = installedPythonPaths(stdout);
|
|
731
877
|
for (const executable of candidates) {
|
|
732
878
|
const info = await probePython({ executable, args: [] }, options);
|
|
@@ -749,59 +895,79 @@ function installedPythonPaths(output) {
|
|
|
749
895
|
});
|
|
750
896
|
return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
|
|
751
897
|
}
|
|
752
|
-
async function
|
|
753
|
-
const executable =
|
|
898
|
+
async function inspectPythonVenv(directory, options) {
|
|
899
|
+
const executable = pythonVenvExecutable(directory);
|
|
754
900
|
const info = await probePython({ executable, args: [] }, options);
|
|
755
901
|
return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
|
|
756
902
|
}
|
|
757
|
-
async function
|
|
758
|
-
if (!
|
|
759
|
-
|
|
903
|
+
async function resolveRuntimeDependencies(uv, python, options) {
|
|
904
|
+
if (!hasRuntimeRequirements(options)) {
|
|
905
|
+
return { content: "", sha256: requirementsDigest("") };
|
|
760
906
|
}
|
|
761
|
-
await
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
const additions = import_node_path5.default.join(temporary, "runtime-dependencies.txt");
|
|
769
|
-
const resolved = import_node_path5.default.join(temporary, "resolved-dependencies.txt");
|
|
770
|
-
const pyproject = import_node_path5.default.join(options.projectRoot, "pyproject.toml");
|
|
771
|
-
const hasPyproject = import_node_fs4.default.existsSync(pyproject);
|
|
772
|
-
if (!hasPyproject && (dependencyOptions.extras.length > 0 || dependencyOptions.groups.length > 0 || dependencyOptions.allExtras)) {
|
|
773
|
-
throw new Error("Runtime extras and groups require an existing pyproject.toml.");
|
|
774
|
-
}
|
|
775
|
-
const sources = hasPyproject ? [pyproject] : [];
|
|
776
|
-
if (dependencies.length > 0) {
|
|
777
|
-
await import_promises.writeFile(additions, `${dependencies.join(`
|
|
907
|
+
const temporary = await import_promises2.mkdtemp(import_node_path6.default.join(import_node_os.default.tmpdir(), "pyfig-resolution-"));
|
|
908
|
+
try {
|
|
909
|
+
const additions = import_node_path6.default.join(temporary, "runtime-dependencies.txt");
|
|
910
|
+
const output = import_node_path6.default.join(temporary, "resolved-dependencies.txt");
|
|
911
|
+
const sources = [...options.requirements];
|
|
912
|
+
if (options.dependencies.length > 0) {
|
|
913
|
+
await import_promises2.writeFile(additions, `${options.dependencies.join(`
|
|
778
914
|
`)}
|
|
779
915
|
`, "utf8");
|
|
780
|
-
|
|
781
|
-
}
|
|
782
|
-
const selectionArgs = [
|
|
783
|
-
...dependencyOptions.extras.flatMap((extra) => ["--extra", extra]),
|
|
784
|
-
...dependencyOptions.groups.flatMap((group) => ["--group", group]),
|
|
785
|
-
...dependencyOptions.allExtras ? ["--all-extras"] : []
|
|
786
|
-
];
|
|
787
|
-
await runAsync(uv.executable, [
|
|
788
|
-
"pip",
|
|
789
|
-
"compile",
|
|
790
|
-
"--no-config",
|
|
791
|
-
"--python",
|
|
792
|
-
python.executable,
|
|
793
|
-
...selectionArgs,
|
|
794
|
-
...sources,
|
|
795
|
-
"--output-file",
|
|
796
|
-
resolved
|
|
797
|
-
], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
798
|
-
await runAsync(uv.executable, ["pip", "install", "--no-config", "--python", python.executable, "-r", resolved], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
799
|
-
} finally {
|
|
800
|
-
await import_promises.rm(temporary, { recursive: true, force: true });
|
|
801
|
-
}
|
|
802
|
-
return true;
|
|
916
|
+
sources.push(additions);
|
|
803
917
|
}
|
|
804
|
-
|
|
918
|
+
await runAsync(uv.executable, [
|
|
919
|
+
"pip",
|
|
920
|
+
"compile",
|
|
921
|
+
"--no-config",
|
|
922
|
+
"--no-header",
|
|
923
|
+
"--no-annotate",
|
|
924
|
+
"--generate-hashes",
|
|
925
|
+
"--python",
|
|
926
|
+
python.executable,
|
|
927
|
+
...sources,
|
|
928
|
+
"--output-file",
|
|
929
|
+
output
|
|
930
|
+
], isolatedUvProcessEnvironment(options), options.installTimeoutMs, undefined, options.projectRoot);
|
|
931
|
+
const content = normalizeResolution(await import_promises2.readFile(output, "utf8"));
|
|
932
|
+
return { content, sha256: requirementsDigest(content) };
|
|
933
|
+
} finally {
|
|
934
|
+
await import_promises2.rm(temporary, { recursive: true, force: true });
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
async function validateRequirementsFiles(requirements) {
|
|
938
|
+
for (const requirement of requirements) {
|
|
939
|
+
let information;
|
|
940
|
+
try {
|
|
941
|
+
information = await import_promises2.stat(requirement);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
throw new Error(`Requirements file is unavailable: ${requirement}`, { cause: error });
|
|
944
|
+
}
|
|
945
|
+
if (!information.isFile()) {
|
|
946
|
+
throw new Error(`Requirements path is not a file: ${requirement}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
async function installResolvedDependencies(uv, python, resolution, options) {
|
|
951
|
+
if (!resolution.content)
|
|
952
|
+
return;
|
|
953
|
+
if (!python.virtualEnvironment) {
|
|
954
|
+
throw new Error("Runtime dependency installation requires a virtual environment.");
|
|
955
|
+
}
|
|
956
|
+
const temporary = await import_promises2.mkdtemp(import_node_path6.default.join(import_node_os.default.tmpdir(), "pyfig-install-"));
|
|
957
|
+
try {
|
|
958
|
+
const requirements = import_node_path6.default.join(temporary, "resolved-dependencies.txt");
|
|
959
|
+
await import_promises2.writeFile(requirements, resolution.content, { encoding: "utf8", flag: "wx" });
|
|
960
|
+
await runAsync(uv.executable, ["pip", "install", "--no-config", "--python", python.executable, "-r", requirements], targetUvProcessEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
961
|
+
await runAsync(uv.executable, ["pip", "check", "--no-config", "--python", python.executable], targetUvProcessEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
962
|
+
} finally {
|
|
963
|
+
await import_promises2.rm(temporary, { recursive: true, force: true });
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function normalizeResolution(content) {
|
|
967
|
+
const normalized = content.replace(/\r\n/gu, `
|
|
968
|
+
`).trimEnd();
|
|
969
|
+
return normalized ? `${normalized}
|
|
970
|
+
` : "";
|
|
805
971
|
}
|
|
806
972
|
async function ensureUvResolved(options) {
|
|
807
973
|
const existing = await findAvailableUv(options);
|
|
@@ -810,7 +976,7 @@ async function ensureUvResolved(options) {
|
|
|
810
976
|
if (!options.installUv) {
|
|
811
977
|
throw new Error(`Compatible uv is unavailable (requires-uv ${options.requiresUv}), and uv installation is disabled.`);
|
|
812
978
|
}
|
|
813
|
-
const policyKey =
|
|
979
|
+
const policyKey = import_node_crypto2.createHash("sha256").update(options.requiresUv).digest("hex");
|
|
814
980
|
return withProvisioningLock({
|
|
815
981
|
key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
|
|
816
982
|
timeoutMs: options.coordinationTimeoutMs,
|
|
@@ -842,7 +1008,7 @@ async function findAvailableUv(options) {
|
|
|
842
1008
|
return findManagedUv(options);
|
|
843
1009
|
}
|
|
844
1010
|
async function findManagedUv(options) {
|
|
845
|
-
const entries = await
|
|
1011
|
+
const entries = await import_promises2.readdir(uvInstallRoot(options), { withFileTypes: true }).catch(() => []);
|
|
846
1012
|
let versions = entries.filter((entry) => entry.isDirectory() && normalizePep440Version(entry.name) === entry.name).map((entry) => entry.name);
|
|
847
1013
|
while (versions.length > 0) {
|
|
848
1014
|
const selected = maxPep440Version(versions, options.requiresUv);
|
|
@@ -855,45 +1021,16 @@ async function findManagedUv(options) {
|
|
|
855
1021
|
}
|
|
856
1022
|
return null;
|
|
857
1023
|
}
|
|
858
|
-
function
|
|
859
|
-
|
|
860
|
-
for (const dependency of normalized) {
|
|
861
|
-
if (!dependency || /[\0\r\n]/u.test(dependency)) {
|
|
862
|
-
throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
return [...new Set(normalized)];
|
|
1024
|
+
function isolatedUvProcessEnvironment(options) {
|
|
1025
|
+
return sanitizeUvProcessEnvironment(uvProcessEnvironment(options));
|
|
866
1026
|
}
|
|
867
|
-
function
|
|
868
|
-
const
|
|
869
|
-
const groups = normalizeNames("dependency group", options.groups ?? []);
|
|
870
|
-
if (options.allExtras && extras.length > 0) {
|
|
871
|
-
throw new TypeError("allExtras cannot be combined with explicit extras.");
|
|
872
|
-
}
|
|
873
|
-
return { extras, groups, allExtras: options.allExtras ?? false };
|
|
874
|
-
}
|
|
875
|
-
function normalizeNames(kind, names) {
|
|
876
|
-
const normalized = names.map((name) => name.trim());
|
|
877
|
-
for (const name of normalized) {
|
|
878
|
-
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
|
|
879
|
-
throw new TypeError(`Invalid ${kind}: ${JSON.stringify(name)}`);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
return [...new Set(normalized)];
|
|
883
|
-
}
|
|
884
|
-
function isManagedEnvironment(python, options) {
|
|
885
|
-
return Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(options.projectRoot));
|
|
886
|
-
}
|
|
887
|
-
function isolatedUvEnvironment(options) {
|
|
888
|
-
return sanitizeUvEnvironment(uvEnvironment(options));
|
|
889
|
-
}
|
|
890
|
-
function explicitPythonSourceEnvironment(options) {
|
|
891
|
-
const env = isolatedUvEnvironment(options);
|
|
1027
|
+
function explicitPythonSourceProcessEnvironment(options) {
|
|
1028
|
+
const env = isolatedUvProcessEnvironment(options);
|
|
892
1029
|
delete env["UV_PYTHON_PREFERENCE"];
|
|
893
1030
|
return env;
|
|
894
1031
|
}
|
|
895
|
-
function
|
|
896
|
-
return
|
|
1032
|
+
function targetUvProcessEnvironment(options, python) {
|
|
1033
|
+
return uvCommandProcessEnvironment(uvProcessEnvironment(options), python);
|
|
897
1034
|
}
|
|
898
1035
|
function incompatibleConfiguredPython(options) {
|
|
899
1036
|
return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `requires-python ${options.requiresPython}` + `${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
|
|
@@ -902,18 +1039,6 @@ function missingPythonMessage(options, reason) {
|
|
|
902
1039
|
const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
|
|
903
1040
|
return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
|
|
904
1041
|
}
|
|
905
|
-
async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
|
|
906
|
-
if (!options.python)
|
|
907
|
-
return true;
|
|
908
|
-
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
909
|
-
if (!configured || !pythonSatisfiesPolicy(configured, options))
|
|
910
|
-
return false;
|
|
911
|
-
if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
|
|
912
|
-
return true;
|
|
913
|
-
}
|
|
914
|
-
const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
|
|
915
|
-
return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
|
|
916
|
-
}
|
|
917
1042
|
var UV_RELEASE_PAGE_SIZE = 100;
|
|
918
1043
|
var UV_RELEASE_MAX_PAGES = 20;
|
|
919
1044
|
var UV_RELEASE_METADATA_TTL_MS = 24 * 60 * 60000;
|
|
@@ -955,11 +1080,7 @@ async function fetchUvReleaseCatalog(options) {
|
|
|
955
1080
|
let exhausted = false;
|
|
956
1081
|
try {
|
|
957
1082
|
for (let page = 1;page <= UV_RELEASE_MAX_PAGES; page += 1) {
|
|
958
|
-
const text = await downloadText(`https://api.github.com/repos/astral-sh/uv/releases?per_page=${UV_RELEASE_PAGE_SIZE}&page=${page}`, options.downloadTimeoutMs, dispatcher,
|
|
959
|
-
Accept: "application/vnd.github+json",
|
|
960
|
-
"User-Agent": "pyfig",
|
|
961
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
962
|
-
});
|
|
1083
|
+
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));
|
|
963
1084
|
const payload = JSON.parse(text);
|
|
964
1085
|
if (!Array.isArray(payload)) {
|
|
965
1086
|
throw new Error("GitHub returned invalid uv release metadata.");
|
|
@@ -992,6 +1113,18 @@ async function fetchUvReleaseCatalog(options) {
|
|
|
992
1113
|
}
|
|
993
1114
|
return { fetchedAt: Date.now(), releases: [...releases.values()] };
|
|
994
1115
|
}
|
|
1116
|
+
function githubApiHeaders(env) {
|
|
1117
|
+
const token = [env["GITHUB_TOKEN"], env["GH_TOKEN"]].find((value) => typeof value === "string" && value.trim().length > 0);
|
|
1118
|
+
if (token && /[\r\n]/u.test(token)) {
|
|
1119
|
+
throw new TypeError("GitHub API token must not contain line breaks.");
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
Accept: "application/vnd.github+json",
|
|
1123
|
+
"User-Agent": "pyfig",
|
|
1124
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
1125
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
995
1128
|
async function readFreshUvReleaseCatalog(file) {
|
|
996
1129
|
const catalog = await readUvReleaseCatalog(file);
|
|
997
1130
|
if (!catalog)
|
|
@@ -1001,7 +1134,7 @@ async function readFreshUvReleaseCatalog(file) {
|
|
|
1001
1134
|
}
|
|
1002
1135
|
async function readUvReleaseCatalog(file) {
|
|
1003
1136
|
try {
|
|
1004
|
-
return parseUvReleaseCatalog(JSON.parse(await
|
|
1137
|
+
return parseUvReleaseCatalog(JSON.parse(await import_promises2.readFile(file, "utf8")));
|
|
1005
1138
|
} catch {
|
|
1006
1139
|
return null;
|
|
1007
1140
|
}
|
|
@@ -1030,20 +1163,20 @@ function parseUvReleaseCatalog(value) {
|
|
|
1030
1163
|
return { fetchedAt: catalog.fetchedAt, releases };
|
|
1031
1164
|
}
|
|
1032
1165
|
async function writeUvReleaseCatalog(file, catalog) {
|
|
1033
|
-
await
|
|
1034
|
-
const staged =
|
|
1166
|
+
await import_promises2.mkdir(import_node_path6.default.dirname(file), { recursive: true });
|
|
1167
|
+
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`);
|
|
1035
1168
|
try {
|
|
1036
|
-
await
|
|
1169
|
+
await import_promises2.writeFile(staged, `${JSON.stringify(catalog)}
|
|
1037
1170
|
`, { encoding: "utf8", flag: "wx" });
|
|
1038
1171
|
if (process.platform === "win32")
|
|
1039
|
-
await
|
|
1040
|
-
await
|
|
1172
|
+
await import_promises2.rm(file, { force: true });
|
|
1173
|
+
await import_promises2.rename(staged, file);
|
|
1041
1174
|
} finally {
|
|
1042
|
-
await
|
|
1175
|
+
await import_promises2.rm(staged, { force: true });
|
|
1043
1176
|
}
|
|
1044
1177
|
}
|
|
1045
1178
|
function uvReleaseMetadataFile(options) {
|
|
1046
|
-
return
|
|
1179
|
+
return import_node_path6.default.join(options.cacheDir, "metadata", "uv-releases.json");
|
|
1047
1180
|
}
|
|
1048
1181
|
function releaseAssetNames(value) {
|
|
1049
1182
|
if (!Array.isArray(value))
|
|
@@ -1056,36 +1189,36 @@ function releaseAssetNames(value) {
|
|
|
1056
1189
|
});
|
|
1057
1190
|
}
|
|
1058
1191
|
function uvInstallRoot(options) {
|
|
1059
|
-
return
|
|
1192
|
+
return import_node_path6.default.join(options.cacheDir, "tools", "uv");
|
|
1060
1193
|
}
|
|
1061
1194
|
function uvInstallDirectory(options, version) {
|
|
1062
|
-
return
|
|
1195
|
+
return import_node_path6.default.join(uvInstallRoot(options), version);
|
|
1063
1196
|
}
|
|
1064
1197
|
function uvExecutable(options, version) {
|
|
1065
|
-
return
|
|
1198
|
+
return import_node_path6.default.join(uvInstallDirectory(options, version), process.platform === "win32" ? "uv.exe" : "uv");
|
|
1066
1199
|
}
|
|
1067
1200
|
async function installUv(installDir, release, options) {
|
|
1068
1201
|
const asset = uvAsset();
|
|
1069
1202
|
const baseUrl = `https://github.com/astral-sh/uv/releases/download/${release.tag}`;
|
|
1070
|
-
const temporary = await
|
|
1203
|
+
const temporary = await import_promises2.mkdtemp(import_node_path6.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
|
|
1071
1204
|
const dispatcher = proxyDispatcher(options.env);
|
|
1072
1205
|
try {
|
|
1073
|
-
const archive =
|
|
1206
|
+
const archive = import_node_path6.default.join(temporary, asset);
|
|
1074
1207
|
await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
|
|
1075
1208
|
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
|
|
1076
1209
|
const actual = await sha256File(archive);
|
|
1077
1210
|
if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
|
|
1078
1211
|
throw new Error(`uv archive checksum mismatch for ${asset}`);
|
|
1079
1212
|
}
|
|
1080
|
-
const extracted =
|
|
1081
|
-
await
|
|
1213
|
+
const extracted = import_node_path6.default.join(temporary, "extracted");
|
|
1214
|
+
await import_promises2.mkdir(extracted, { recursive: true });
|
|
1082
1215
|
const source = await extractUvExecutable(archive, asset, extracted);
|
|
1083
1216
|
if (!source)
|
|
1084
1217
|
throw new Error(`uv archive did not contain the expected executable: ${asset}`);
|
|
1085
1218
|
await publishUvAtomically(source, installDir, release.version, options);
|
|
1086
1219
|
} finally {
|
|
1087
1220
|
await dispatcher?.close();
|
|
1088
|
-
await
|
|
1221
|
+
await import_promises2.rm(temporary, { recursive: true, force: true });
|
|
1089
1222
|
}
|
|
1090
1223
|
}
|
|
1091
1224
|
async function extractUvExecutable(archive, asset, destination) {
|
|
@@ -1094,33 +1227,33 @@ async function extractUvExecutable(archive, asset, destination) {
|
|
|
1094
1227
|
await tar.x({ file: archive, cwd: destination, strict: true });
|
|
1095
1228
|
return findFile(destination, executableName);
|
|
1096
1229
|
}
|
|
1097
|
-
const entries = import_fflate.unzipSync(await
|
|
1098
|
-
filter: ({ name }) =>
|
|
1230
|
+
const entries = import_fflate.unzipSync(await import_promises2.readFile(archive), {
|
|
1231
|
+
filter: ({ name }) => import_node_path6.default.posix.basename(name.replaceAll("\\", "/")) === executableName
|
|
1099
1232
|
});
|
|
1100
|
-
const matches = Object.entries(entries).filter(([name]) =>
|
|
1233
|
+
const matches = Object.entries(entries).filter(([name]) => import_node_path6.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
|
|
1101
1234
|
if (matches.length !== 1 || !matches[0])
|
|
1102
1235
|
return null;
|
|
1103
|
-
const source =
|
|
1104
|
-
await
|
|
1236
|
+
const source = import_node_path6.default.join(destination, executableName);
|
|
1237
|
+
await import_promises2.writeFile(source, matches[0][1], { flag: "wx" });
|
|
1105
1238
|
return source;
|
|
1106
1239
|
}
|
|
1107
1240
|
async function publishUvAtomically(source, installDir, version, options) {
|
|
1108
|
-
await
|
|
1241
|
+
await import_promises2.mkdir(installDir, { recursive: true });
|
|
1109
1242
|
const executable = uvExecutable(options, version);
|
|
1110
|
-
const staged =
|
|
1243
|
+
const staged = import_node_path6.default.join(installDir, `.${import_node_path6.default.basename(executable)}.${process.pid}.${import_node_crypto2.randomUUID()}.tmp`);
|
|
1111
1244
|
try {
|
|
1112
|
-
await
|
|
1245
|
+
await import_promises2.copyFile(source, staged);
|
|
1113
1246
|
if (process.platform !== "win32")
|
|
1114
|
-
await
|
|
1247
|
+
await import_promises2.chmod(staged, 493);
|
|
1115
1248
|
const stagedUv = probeUv(staged, options);
|
|
1116
1249
|
if (!stagedUv || stagedUv.version !== version) {
|
|
1117
1250
|
throw new Error(`Downloaded uv executable is not version ${version}.`);
|
|
1118
1251
|
}
|
|
1119
1252
|
if (process.platform === "win32")
|
|
1120
|
-
await
|
|
1121
|
-
await
|
|
1253
|
+
await import_promises2.rm(executable, { force: true });
|
|
1254
|
+
await import_promises2.rename(staged, executable);
|
|
1122
1255
|
} finally {
|
|
1123
|
-
await
|
|
1256
|
+
await import_promises2.rm(staged, { force: true });
|
|
1124
1257
|
}
|
|
1125
1258
|
}
|
|
1126
1259
|
function uvAsset() {
|
|
@@ -1145,7 +1278,7 @@ async function download(url, destination, timeoutMs, dispatcher) {
|
|
|
1145
1278
|
await response.body?.cancel();
|
|
1146
1279
|
throw new Error(`Could not download ${url}: HTTP ${response.status}`);
|
|
1147
1280
|
}
|
|
1148
|
-
await
|
|
1281
|
+
await import_promises3.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
|
|
1149
1282
|
}
|
|
1150
1283
|
async function downloadText(url, timeoutMs, dispatcher, headers) {
|
|
1151
1284
|
const response = await import_undici.fetch(url, {
|
|
@@ -1161,28 +1294,28 @@ async function downloadText(url, timeoutMs, dispatcher, headers) {
|
|
|
1161
1294
|
return response.text();
|
|
1162
1295
|
}
|
|
1163
1296
|
function proxyDispatcher(env) {
|
|
1164
|
-
const httpProxy =
|
|
1165
|
-
const httpsProxy =
|
|
1297
|
+
const httpProxy = processEnvironmentValue(env, "http_proxy", "HTTP_PROXY");
|
|
1298
|
+
const httpsProxy = processEnvironmentValue(env, "https_proxy", "HTTPS_PROXY");
|
|
1166
1299
|
if (!httpProxy && !httpsProxy)
|
|
1167
1300
|
return;
|
|
1168
1301
|
return new import_undici.EnvHttpProxyAgent({
|
|
1169
1302
|
httpProxy,
|
|
1170
1303
|
httpsProxy,
|
|
1171
|
-
noProxy:
|
|
1304
|
+
noProxy: processEnvironmentValue(env, "no_proxy", "NO_PROXY")
|
|
1172
1305
|
});
|
|
1173
1306
|
}
|
|
1174
|
-
function
|
|
1307
|
+
function processEnvironmentValue(env, lowercase, uppercase) {
|
|
1175
1308
|
return env[lowercase] ?? env[uppercase];
|
|
1176
1309
|
}
|
|
1177
1310
|
async function sha256File(file) {
|
|
1178
|
-
const hash =
|
|
1311
|
+
const hash = import_node_crypto2.createHash("sha256");
|
|
1179
1312
|
for await (const chunk of import_node_fs3.createReadStream(file))
|
|
1180
1313
|
hash.update(chunk);
|
|
1181
1314
|
return hash.digest("hex");
|
|
1182
1315
|
}
|
|
1183
1316
|
async function findFile(directory, name) {
|
|
1184
|
-
for (const entry of await
|
|
1185
|
-
const target =
|
|
1317
|
+
for (const entry of await import_promises2.readdir(directory, { withFileTypes: true })) {
|
|
1318
|
+
const target = import_node_path6.default.join(directory, entry.name);
|
|
1186
1319
|
if (entry.isFile() && entry.name === name)
|
|
1187
1320
|
return target;
|
|
1188
1321
|
if (entry.isDirectory()) {
|
|
@@ -1196,8 +1329,8 @@ async function findFile(directory, name) {
|
|
|
1196
1329
|
|
|
1197
1330
|
// src/sync-resolver.ts
|
|
1198
1331
|
async function main() {
|
|
1199
|
-
const request = JSON.parse(
|
|
1200
|
-
const result = request.target === "uv" ? await ensureUv(request.options) : await
|
|
1332
|
+
const request = JSON.parse(import_node_fs4.readFileSync(0, "utf8"));
|
|
1333
|
+
const result = request.target === "uv" ? await ensureUv(request.options) : await ensurePython(request.options);
|
|
1201
1334
|
process.stdout.write(JSON.stringify(result));
|
|
1202
1335
|
}
|
|
1203
1336
|
main().catch((error) => {
|
|
@@ -1206,4 +1339,4 @@ main().catch((error) => {
|
|
|
1206
1339
|
process.exitCode = 1;
|
|
1207
1340
|
});
|
|
1208
1341
|
|
|
1209
|
-
//# debugId=
|
|
1342
|
+
//# debugId=0726BCDBF58AF5C064756E2164756E21
|