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.
- package/README.md +76 -178
- package/SECURITY.md +16 -5
- package/dist/cjs/index.js +482 -1131
- package/dist/cjs/index.js.map +6 -16
- package/dist/index.d.ts +2 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/process.d.ts +6 -12
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +35 -54
- package/dist/process.js.map +1 -1
- package/dist/pyfig.d.ts +31 -0
- package/dist/pyfig.d.ts.map +1 -0
- package/dist/pyfig.js +275 -0
- package/dist/pyfig.js.map +1 -0
- package/dist/uv.d.ts +3 -0
- package/dist/uv.d.ts.map +1 -0
- package/dist/uv.js +423 -0
- package/dist/uv.js.map +1 -0
- package/package.json +6 -12
- package/dist/cjs/sync-resolver.js +0 -1342
- package/dist/cjs/sync-resolver.js.map +0 -20
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -57
- package/dist/cli.js.map +0 -1
- package/dist/client.d.ts +0 -15
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -35
- package/dist/client.js.map +0 -1
- package/dist/config.d.ts +0 -69
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -159
- package/dist/config.js.map +0 -1
- package/dist/package-target.d.ts +0 -8
- package/dist/package-target.d.ts.map +0 -1
- package/dist/package-target.js +0 -81
- package/dist/package-target.js.map +0 -1
- package/dist/pep440.d.ts +0 -12
- package/dist/pep440.d.ts.map +0 -1
- package/dist/pep440.js +0 -21
- package/dist/pep440.js.map +0 -1
- package/dist/probes.d.ts +0 -37
- package/dist/probes.d.ts.map +0 -1
- package/dist/probes.js +0 -145
- package/dist/probes.js.map +0 -1
- package/dist/project-root.d.ts +0 -3
- package/dist/project-root.d.ts.map +0 -1
- package/dist/project-root.js +0 -22
- package/dist/project-root.js.map +0 -1
- package/dist/provision-lock.d.ts +0 -10
- package/dist/provision-lock.d.ts.map +0 -1
- package/dist/provision-lock.js +0 -24
- package/dist/provision-lock.js.map +0 -1
- package/dist/provision.d.ts +0 -20
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -673
- package/dist/provision.js.map +0 -1
- package/dist/python-venv.d.ts +0 -12
- package/dist/python-venv.d.ts.map +0 -1
- package/dist/python-venv.js +0 -52
- package/dist/python-venv.js.map +0 -1
- package/dist/runners.d.ts +0 -14
- package/dist/runners.d.ts.map +0 -1
- package/dist/runners.js +0 -71
- package/dist/runners.js.map +0 -1
- package/dist/runtime-files.d.ts +0 -3
- package/dist/runtime-files.d.ts.map +0 -1
- package/dist/runtime-files.js +0 -12
- package/dist/runtime-files.js.map +0 -1
- package/dist/sync-resolver.d.ts +0 -2
- package/dist/sync-resolver.d.ts.map +0 -1
- package/dist/sync-resolver.js +0 -15
- package/dist/sync-resolver.js.map +0 -1
- package/dist/venv-identity.d.ts +0 -36
- package/dist/venv-identity.d.ts.map +0 -1
- package/dist/venv-identity.js +0 -80
- 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
|
-
|
|
69
|
-
|
|
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/
|
|
81
|
-
var
|
|
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
|
|
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
|
|
80
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
500
81
|
var import_which = __toESM(require("which"), 1);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
545
|
-
resolve(
|
|
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 >
|
|
116
|
+
if (stdout.length > OUTPUT_LIMIT) {
|
|
561
117
|
child.kill();
|
|
562
|
-
finish(new Error(`${
|
|
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 >
|
|
123
|
+
if (stderr.length > OUTPUT_LIMIT) {
|
|
568
124
|
child.kill();
|
|
569
|
-
finish(new Error(`${
|
|
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,
|
|
574
|
-
if (code
|
|
575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
|
172
|
+
const existing = await findAvailableUv(context);
|
|
755
173
|
if (existing)
|
|
756
174
|
return existing;
|
|
757
|
-
|
|
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: `
|
|
762
|
-
|
|
763
|
-
check,
|
|
177
|
+
key: `uv-policy:${canonicalPath(context.cache)}:${policy}`,
|
|
178
|
+
check: () => findAvailableUv(context),
|
|
764
179
|
mutate: async () => {
|
|
765
|
-
|
|
766
|
-
|
|
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(
|
|
1004
|
-
|
|
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(
|
|
1008
|
-
const
|
|
1009
|
-
if (!
|
|
1010
|
-
throw new Error(`
|
|
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
|
|
191
|
+
return installed;
|
|
1013
192
|
}
|
|
1014
193
|
});
|
|
1015
194
|
}
|
|
1016
195
|
});
|
|
1017
196
|
}
|
|
1018
|
-
async function findAvailableUv(
|
|
1019
|
-
const external = await
|
|
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
|
|
201
|
+
return findCachedUv(context);
|
|
1023
202
|
}
|
|
1024
|
-
async function
|
|
1025
|
-
const
|
|
1026
|
-
|
|
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
|
|
1029
|
-
if (!
|
|
208
|
+
const version = import_pep440.maxSatisfying(versions, context.specifier);
|
|
209
|
+
if (!version)
|
|
1030
210
|
return null;
|
|
1031
|
-
const
|
|
1032
|
-
if (
|
|
1033
|
-
return
|
|
1034
|
-
versions = versions.filter((
|
|
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
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
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
|
|
1063
|
-
const version =
|
|
1064
|
-
const release =
|
|
1065
|
-
if (!release)
|
|
1066
|
-
throw new Error(`No published uv release satisfies
|
|
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(
|
|
1071
|
-
const file = uvReleaseMetadataFile(
|
|
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(
|
|
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(
|
|
1092
|
-
const dispatcher = proxyDispatcher(
|
|
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 <=
|
|
1097
|
-
const text = await downloadText(`https://api.github.com/repos/astral-sh/uv/releases?per_page=${
|
|
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
|
|
1106
|
-
if (
|
|
270
|
+
const value = candidate;
|
|
271
|
+
if (value.draft === true || typeof value.tag_name !== "string")
|
|
1107
272
|
continue;
|
|
1108
|
-
const
|
|
1109
|
-
const version = normalizePep440Version(release.tag_name);
|
|
273
|
+
const version = import_pep440.valid(value.tag_name);
|
|
1110
274
|
if (version) {
|
|
1111
|
-
releases.set(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 <
|
|
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 ${
|
|
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
|
|
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 <
|
|
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
|
|
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
|
|
1165
|
-
if (!
|
|
329
|
+
for (const candidate of catalog.releases) {
|
|
330
|
+
if (!candidate || typeof candidate !== "object")
|
|
1166
331
|
return null;
|
|
1167
|
-
const release =
|
|
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" ||
|
|
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
|
|
1181
|
-
const staged =
|
|
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
|
|
347
|
+
await import_promises.writeFile(staged, `${JSON.stringify(catalog)}
|
|
1184
348
|
`, { encoding: "utf8", flag: "wx" });
|
|
1185
349
|
if (process.platform === "win32")
|
|
1186
|
-
await
|
|
1187
|
-
await
|
|
350
|
+
await import_promises.rm(file, { force: true });
|
|
351
|
+
await import_promises.rename(staged, file);
|
|
1188
352
|
} finally {
|
|
1189
|
-
await
|
|
353
|
+
await import_promises.rm(staged, { force: true });
|
|
1190
354
|
}
|
|
1191
355
|
}
|
|
1192
|
-
function
|
|
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
|
|
1218
|
-
const dispatcher = proxyDispatcher(
|
|
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 =
|
|
1221
|
-
await download(`${baseUrl}/${asset}`, archive,
|
|
1222
|
-
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`,
|
|
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 =
|
|
1228
|
-
await
|
|
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
|
|
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
|
|
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
|
|
1245
|
-
filter: ({ name }) =>
|
|
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]) =>
|
|
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 =
|
|
1251
|
-
await
|
|
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
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
const
|
|
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
|
|
402
|
+
await import_promises.copyFile(source, staged);
|
|
1260
403
|
if (process.platform !== "win32")
|
|
1261
|
-
await
|
|
1262
|
-
|
|
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
|
|
1268
|
-
await
|
|
409
|
+
await import_promises.rm(executable, { force: true });
|
|
410
|
+
await import_promises.rename(staged, executable);
|
|
1269
411
|
} finally {
|
|
1270
|
-
await
|
|
412
|
+
await import_promises.rm(staged, { force: true });
|
|
1271
413
|
}
|
|
1272
414
|
}
|
|
1273
|
-
function
|
|
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(
|
|
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
|
|
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 =
|
|
1312
|
-
const httpsProxy =
|
|
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:
|
|
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 =
|
|
1326
|
-
for await (const chunk of
|
|
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
|
|
1332
|
-
const target =
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1387
|
-
|
|
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
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1412
|
-
|
|
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
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
-
|
|
1431
|
-
|
|
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
|
-
|
|
1434
|
-
|
|
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
|
-
|
|
1437
|
-
|
|
744
|
+
if (options.requireGil && python.gilDisabled) {
|
|
745
|
+
throw new Error(`Python ${python.executable} is free-threaded but requireGil is enabled.`);
|
|
1438
746
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
1443
|
-
|
|
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=
|
|
798
|
+
//# debugId=F23B5C2985518F2764756E2164756E21
|