pyfig 0.3.0 → 0.4.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 +112 -121
- package/SECURITY.md +1 -1
- package/dist/cjs/index.js +469 -871
- package/dist/cjs/index.js.map +14 -15
- package/dist/cjs/sync-resolver.js +426 -657
- package/dist/cjs/sync-resolver.js.map +12 -12
- package/dist/cli.js +5 -8
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +3 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +13 -11
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -51
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +5 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/package-target.d.ts +7 -11
- package/dist/package-target.d.ts.map +1 -1
- package/dist/package-target.js +41 -57
- package/dist/package-target.js.map +1 -1
- package/dist/probes.d.ts +0 -18
- package/dist/probes.d.ts.map +1 -1
- package/dist/probes.js +0 -70
- package/dist/probes.js.map +1 -1
- package/dist/process.d.ts +1 -1
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +2 -1
- package/dist/process.js.map +1 -1
- package/dist/project-root.d.ts +1 -1
- package/dist/project-root.d.ts.map +1 -1
- package/dist/project-root.js +3 -64
- package/dist/project-root.js.map +1 -1
- package/dist/provision-lock.d.ts +10 -0
- package/dist/provision-lock.d.ts.map +1 -0
- package/dist/provision-lock.js +24 -0
- package/dist/provision-lock.js.map +1 -0
- package/dist/provision.d.ts +18 -6
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +273 -228
- package/dist/provision.js.map +1 -1
- package/dist/runners.d.ts +3 -7
- package/dist/runners.d.ts.map +1 -1
- package/dist/runners.js +35 -115
- package/dist/runners.js.map +1 -1
- package/dist/sync-resolver.js +4 -13
- package/dist/sync-resolver.js.map +1 -1
- package/dist/virtual-environment.d.ts +3 -1
- package/dist/virtual-environment.d.ts.map +1 -1
- package/dist/virtual-environment.js +8 -1
- package/dist/virtual-environment.js.map +1 -1
- package/package.json +6 -8
- package/dist/coordination.d.ts +0 -25
- package/dist/coordination.d.ts.map +0 -1
- package/dist/coordination.js +0 -187
- package/dist/coordination.js.map +0 -1
- package/dist/state.d.ts +0 -10
- package/dist/state.d.ts.map +0 -1
- package/dist/state.js +0 -62
- package/dist/state.js.map +0 -1
|
@@ -66,20 +66,19 @@ var __export = (target, all) => {
|
|
|
66
66
|
var import_node_fs5 = require("node:fs");
|
|
67
67
|
|
|
68
68
|
// src/provision.ts
|
|
69
|
-
var
|
|
69
|
+
var import_node_crypto = require("node:crypto");
|
|
70
70
|
var import_node_fs3 = require("node:fs");
|
|
71
|
-
var
|
|
71
|
+
var import_promises = require("node:fs/promises");
|
|
72
72
|
var import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
73
73
|
var import_node_os = __toESM(require("node:os"), 1);
|
|
74
|
-
var
|
|
74
|
+
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
75
75
|
var import_node_stream = require("node:stream");
|
|
76
|
-
var
|
|
76
|
+
var import_promises2 = require("node:stream/promises");
|
|
77
77
|
var import_extract_zip = __toESM(require("extract-zip"), 1);
|
|
78
78
|
var tar = __toESM(require("tar"), 1);
|
|
79
79
|
var import_undici = require("undici");
|
|
80
80
|
|
|
81
81
|
// src/config.ts
|
|
82
|
-
var import_node_crypto = require("node:crypto");
|
|
83
82
|
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
84
83
|
var envPaths = __toESM(require("env-paths"), 1);
|
|
85
84
|
var import_semver = __toESM(require("semver"), 1);
|
|
@@ -87,69 +86,13 @@ var import_semver = __toESM(require("semver"), 1);
|
|
|
87
86
|
// src/project-root.ts
|
|
88
87
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
89
88
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
90
|
-
var import_pacwich = require("pacwich");
|
|
91
|
-
var PACKAGE_MANAGER_LOCKFILES = [
|
|
92
|
-
"bun.lock",
|
|
93
|
-
"bun.lockb",
|
|
94
|
-
"npm-shrinkwrap.json",
|
|
95
|
-
"package-lock.json",
|
|
96
|
-
"pnpm-lock.yaml",
|
|
97
|
-
"yarn.lock"
|
|
98
|
-
];
|
|
99
|
-
var PYTHON_PROJECT_MARKERS = [
|
|
100
|
-
"pyproject.toml",
|
|
101
|
-
"uv.lock"
|
|
102
|
-
];
|
|
103
89
|
function resolveProjectRoot(startDirectory = process.cwd()) {
|
|
104
90
|
const start = import_node_path.default.resolve(startDirectory);
|
|
105
|
-
const
|
|
106
|
-
if (workspaceRoot) {
|
|
107
|
-
try {
|
|
108
|
-
return import_node_path.default.resolve(import_pacwich.createFileSystemProject({
|
|
109
|
-
rootDirectory: start,
|
|
110
|
-
disableExecutableConfigs: true
|
|
111
|
-
}).rootDirectory);
|
|
112
|
-
} catch {
|
|
113
|
-
return workspaceRoot;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return findConventionalProjectRoot(start);
|
|
117
|
-
}
|
|
118
|
-
function findWorkspaceProjectRoot(startDirectory) {
|
|
119
|
-
for (const directory of ancestors(startDirectory)) {
|
|
120
|
-
const packageJson = import_node_path.default.join(directory, "package.json");
|
|
121
|
-
if (!import_node_fs.default.existsSync(packageJson))
|
|
122
|
-
continue;
|
|
123
|
-
if (import_node_fs.default.existsSync(import_node_path.default.join(directory, "pnpm-workspace.yaml")))
|
|
124
|
-
return directory;
|
|
125
|
-
try {
|
|
126
|
-
const manifest = JSON.parse(import_node_fs.default.readFileSync(packageJson, "utf8"));
|
|
127
|
-
if (hasWorkspaceDeclaration(manifest.workspaces))
|
|
128
|
-
return directory;
|
|
129
|
-
} catch {}
|
|
130
|
-
}
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
function findConventionalProjectRoot(startDirectory) {
|
|
134
|
-
let nearestPackageDirectory = null;
|
|
135
|
-
let nearestPythonDirectory = null;
|
|
136
|
-
for (const directory of ancestors(startDirectory)) {
|
|
137
|
-
const hasPackage = import_node_fs.default.existsSync(import_node_path.default.join(directory, "package.json"));
|
|
138
|
-
if (hasPackage && !nearestPackageDirectory)
|
|
139
|
-
nearestPackageDirectory = directory;
|
|
140
|
-
if (!nearestPythonDirectory && PYTHON_PROJECT_MARKERS.some((file) => import_node_fs.default.existsSync(import_node_path.default.join(directory, file)))) {
|
|
141
|
-
nearestPythonDirectory = directory;
|
|
142
|
-
}
|
|
143
|
-
if (hasPackage && PACKAGE_MANAGER_LOCKFILES.some((file) => import_node_fs.default.existsSync(import_node_path.default.join(directory, file)))) {
|
|
144
|
-
return directory;
|
|
145
|
-
}
|
|
91
|
+
for (const directory of ancestors(start)) {
|
|
146
92
|
if (import_node_fs.default.existsSync(import_node_path.default.join(directory, ".git")))
|
|
147
93
|
return directory;
|
|
148
94
|
}
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
function hasWorkspaceDeclaration(value) {
|
|
152
|
-
return Array.isArray(value) || value !== null && typeof value === "object";
|
|
95
|
+
return start;
|
|
153
96
|
}
|
|
154
97
|
function* ancestors(startDirectory) {
|
|
155
98
|
let directory = startDirectory;
|
|
@@ -164,14 +107,12 @@ function* ancestors(startDirectory) {
|
|
|
164
107
|
|
|
165
108
|
// src/config.ts
|
|
166
109
|
var DEFAULT_PYTHON_RANGE = ">=3.10.0 <4.0.0";
|
|
167
|
-
var DEFAULT_PIP_RANGE = ">=20.3.0";
|
|
168
110
|
var DEFAULT_MANAGED_PYTHON = "3.12";
|
|
169
111
|
var DEFAULT_UV_VERSION = "0.13.6";
|
|
170
|
-
var DEFAULT_UV_RANGE =
|
|
112
|
+
var DEFAULT_UV_RANGE = ">=0.10.0 <1.0.0";
|
|
171
113
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
172
114
|
var DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60000;
|
|
173
115
|
var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60000;
|
|
174
|
-
var DEFAULT_CACHE_TTL_MS = 30000;
|
|
175
116
|
var envPathsFunction = (() => {
|
|
176
117
|
const imported = envPaths.default;
|
|
177
118
|
const implementation = typeof imported === "function" ? imported : imported.default;
|
|
@@ -182,10 +123,8 @@ var envPathsFunction = (() => {
|
|
|
182
123
|
function resolveOptions(options = {}) {
|
|
183
124
|
const env = { ...options.env ?? process.env };
|
|
184
125
|
const pythonRange = options.pythonRange ?? env["PYFIG_PYTHON_RANGE"] ?? DEFAULT_PYTHON_RANGE;
|
|
185
|
-
const pipRange = options.pipRange ?? env["PYFIG_PIP_RANGE"] ?? DEFAULT_PIP_RANGE;
|
|
186
126
|
const uvRange = options.uvRange ?? env["PYFIG_UV_RANGE"] ?? DEFAULT_UV_RANGE;
|
|
187
127
|
assertRange("Python", pythonRange);
|
|
188
|
-
assertRange("pip", pipRange);
|
|
189
128
|
assertRange("uv", uvRange);
|
|
190
129
|
const managedPython = options.managedPython ?? env["PYFIG_MANAGED_PYTHON"] ?? DEFAULT_MANAGED_PYTHON;
|
|
191
130
|
const uvVersion = options.uvVersion ?? env["PYFIG_UV_VERSION"] ?? DEFAULT_UV_VERSION;
|
|
@@ -195,16 +134,14 @@ function resolveOptions(options = {}) {
|
|
|
195
134
|
const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
|
|
196
135
|
const downloadTimeoutMs = resolveDuration("downloadTimeoutMs", options.downloadTimeoutMs, env["PYFIG_DOWNLOAD_TIMEOUT_MS"], DEFAULT_DOWNLOAD_TIMEOUT_MS);
|
|
197
136
|
const coordinationTimeoutMs = resolveDuration("coordinationTimeoutMs", options.coordinationTimeoutMs, env["PYFIG_COORDINATION_TIMEOUT_MS"], installTimeoutMs + downloadTimeoutMs + 60000);
|
|
198
|
-
const cacheTtlMs = resolveDuration("cacheTtlMs", options.cacheTtlMs, env["PYFIG_CACHE_TTL_MS"], DEFAULT_CACHE_TTL_MS, true);
|
|
199
137
|
const configuredHome = options.cacheDir ?? env["PYFIG_HOME"];
|
|
200
138
|
const cacheDir = configuredHome ? import_node_path2.default.resolve(configuredHome) : envPathsFunction("pyfig", { suffix: "" }).cache;
|
|
201
139
|
const configuredProjectRoot = options.projectRoot ?? env["PYFIG_PROJECT_ROOT"];
|
|
202
|
-
const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot();
|
|
140
|
+
const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot(env["INIT_CWD"] ?? process.cwd());
|
|
203
141
|
const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
|
|
204
142
|
return {
|
|
205
143
|
python: options.python ?? env["PYFIG_PYTHON"] ?? "",
|
|
206
144
|
pythonRange,
|
|
207
|
-
pipRange,
|
|
208
145
|
managedPython,
|
|
209
146
|
uvVersion,
|
|
210
147
|
uvRange,
|
|
@@ -220,40 +157,16 @@ function resolveOptions(options = {}) {
|
|
|
220
157
|
installTimeoutMs,
|
|
221
158
|
downloadTimeoutMs,
|
|
222
159
|
coordinationTimeoutMs,
|
|
223
|
-
cacheTtlMs,
|
|
224
160
|
env
|
|
225
161
|
};
|
|
226
162
|
}
|
|
227
|
-
function configurationFingerprint(options) {
|
|
228
|
-
const policy = [
|
|
229
|
-
normalizedPath(options.projectRoot),
|
|
230
|
-
normalizedPath(options.cacheDir),
|
|
231
|
-
options.python,
|
|
232
|
-
options.pythonRange,
|
|
233
|
-
options.pipRange,
|
|
234
|
-
options.managedPython,
|
|
235
|
-
options.uvVersion,
|
|
236
|
-
options.uvRange,
|
|
237
|
-
options.requireEmbedding,
|
|
238
|
-
options.requireGil,
|
|
239
|
-
options.autoInstall,
|
|
240
|
-
options.installPython,
|
|
241
|
-
options.installUv,
|
|
242
|
-
options.createVirtualEnvironment,
|
|
243
|
-
options.probeTimeoutMs,
|
|
244
|
-
options.installTimeoutMs,
|
|
245
|
-
options.downloadTimeoutMs,
|
|
246
|
-
options.coordinationTimeoutMs,
|
|
247
|
-
options.cacheTtlMs,
|
|
248
|
-
environmentFingerprint(options.env)
|
|
249
|
-
];
|
|
250
|
-
return import_node_crypto.createHash("sha256").update(JSON.stringify(policy)).digest("hex");
|
|
251
|
-
}
|
|
252
163
|
function uvEnvironment(options) {
|
|
253
164
|
return {
|
|
254
165
|
...options.env,
|
|
255
166
|
UV_PYTHON_INSTALL_DIR: import_node_path2.default.join(options.cacheDir, "python"),
|
|
256
|
-
UV_CACHE_DIR: import_node_path2.default.join(options.cacheDir, "uv-cache")
|
|
167
|
+
UV_CACHE_DIR: import_node_path2.default.join(options.cacheDir, "uv-cache"),
|
|
168
|
+
UV_PYTHON_PREFERENCE: "system",
|
|
169
|
+
UV_NO_CONFIG: "1"
|
|
257
170
|
};
|
|
258
171
|
}
|
|
259
172
|
function assertRange(tool, range) {
|
|
@@ -297,34 +210,161 @@ function parseBoolean(value, fallback) {
|
|
|
297
210
|
return false;
|
|
298
211
|
throw new TypeError(`Invalid boolean value: ${value}`);
|
|
299
212
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
213
|
+
|
|
214
|
+
// src/provision-lock.ts
|
|
215
|
+
var import_loqkit = require("loqkit");
|
|
216
|
+
async function withProvisioningLock(options) {
|
|
217
|
+
let existing;
|
|
218
|
+
const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
|
|
219
|
+
key: options.key,
|
|
220
|
+
name: "Pyfig provisioning",
|
|
221
|
+
namespace: "pyfig",
|
|
222
|
+
timeout: options.timeoutMs,
|
|
223
|
+
predicate: async () => {
|
|
224
|
+
const value = await options.check();
|
|
225
|
+
if (value === null)
|
|
226
|
+
return true;
|
|
227
|
+
existing = { value };
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
if (result.context.state === "executed")
|
|
232
|
+
return result.value;
|
|
233
|
+
if (existing)
|
|
234
|
+
return existing.value;
|
|
235
|
+
throw new Error(`Provisioning check for ${options.key} did not produce a resource.`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/virtual-environment.ts
|
|
239
|
+
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
240
|
+
var import_node_path3 = __toESM(require("node:path"), 1);
|
|
241
|
+
var VIRTUAL_ENV_DIRECTORY_NAME = import_node_path3.default.join("node_modules", ".pyfig", "venv");
|
|
242
|
+
function virtualEnvironmentDirectory(projectRoot) {
|
|
243
|
+
return import_node_path3.default.join(projectRoot, VIRTUAL_ENV_DIRECTORY_NAME);
|
|
244
|
+
}
|
|
245
|
+
function externalVirtualEnvironmentDirectories(projectRoot, env) {
|
|
246
|
+
const candidates = [env["VIRTUAL_ENV"], import_node_path3.default.join(projectRoot, ".venv")].filter((value) => Boolean(value)).map((value) => import_node_path3.default.resolve(value));
|
|
247
|
+
return [...new Map(candidates.map((value) => [canonicalPath(value), value])).values()];
|
|
306
248
|
}
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
249
|
+
function virtualEnvironmentPythonExecutable(virtualEnvironment) {
|
|
250
|
+
return import_node_path3.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
|
|
251
|
+
}
|
|
252
|
+
function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
|
|
253
|
+
const activated = { ...env };
|
|
254
|
+
const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
|
|
255
|
+
const existingPath = pathEntry ? activated[pathEntry] : undefined;
|
|
256
|
+
for (const key of Object.keys(activated)) {
|
|
257
|
+
const normalized = key.toUpperCase();
|
|
258
|
+
if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
|
|
259
|
+
delete activated[key];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
activated["PATH"] = [import_node_path3.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path3.default.delimiter);
|
|
263
|
+
activated["VIRTUAL_ENV"] = virtualEnvironment;
|
|
264
|
+
return activated;
|
|
265
|
+
}
|
|
266
|
+
function pathsEqual(left, right) {
|
|
267
|
+
return canonicalPath(left) === canonicalPath(right);
|
|
268
|
+
}
|
|
269
|
+
function canonicalPath(value) {
|
|
270
|
+
const resolved = import_node_path3.default.resolve(value);
|
|
271
|
+
let canonical = resolved;
|
|
272
|
+
try {
|
|
273
|
+
canonical = import_node_fs2.default.realpathSync.native(resolved);
|
|
274
|
+
} catch {}
|
|
275
|
+
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
310
276
|
}
|
|
311
277
|
|
|
312
|
-
// src/
|
|
313
|
-
var
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
278
|
+
// src/package-target.ts
|
|
279
|
+
var TARGET_OPTIONS = new Set([
|
|
280
|
+
"--active",
|
|
281
|
+
"--config-file",
|
|
282
|
+
"--directory",
|
|
283
|
+
"--isolated",
|
|
284
|
+
"--prefix",
|
|
285
|
+
"--project",
|
|
286
|
+
"--python",
|
|
287
|
+
"--root",
|
|
288
|
+
"--system",
|
|
289
|
+
"--target",
|
|
290
|
+
"--user"
|
|
291
|
+
]);
|
|
292
|
+
var TARGET_ENVIRONMENT_VARIABLES = new Set([
|
|
293
|
+
"CONDA_PREFIX",
|
|
294
|
+
"PIP_PREFIX",
|
|
295
|
+
"PIP_PYTHON",
|
|
296
|
+
"PIP_ROOT",
|
|
297
|
+
"PIP_TARGET",
|
|
298
|
+
"PIP_USER",
|
|
299
|
+
"PYTHONHOME",
|
|
300
|
+
"PYTHONUSERBASE",
|
|
301
|
+
"UV_CONFIG_FILE",
|
|
302
|
+
"UV_PROJECT",
|
|
303
|
+
"UV_PROJECT_ENVIRONMENT",
|
|
304
|
+
"UV_PYTHON",
|
|
305
|
+
"UV_PREFIX",
|
|
306
|
+
"UV_ROOT",
|
|
307
|
+
"UV_SYSTEM_PYTHON",
|
|
308
|
+
"UV_TARGET"
|
|
309
|
+
]);
|
|
310
|
+
function uvCommandNeedsEnvironment(args) {
|
|
311
|
+
return args[0] === "pip" || args[0] === "run";
|
|
312
|
+
}
|
|
313
|
+
function targetUvArguments(args, python) {
|
|
314
|
+
if (!python.virtualEnvironment) {
|
|
315
|
+
throw new Error("Targeted uv commands require a virtual environment.");
|
|
316
|
+
}
|
|
317
|
+
assertNoTargetOverride(args);
|
|
318
|
+
if (args[0] === "run") {
|
|
319
|
+
return ["run", "--no-project", "--python", python.executable, ...args.slice(1)];
|
|
320
|
+
}
|
|
321
|
+
if (args[0] === "pip" && args[1]) {
|
|
322
|
+
return ["pip", args[1], "--python", python.executable, ...args.slice(2)];
|
|
323
|
+
}
|
|
324
|
+
return [...args];
|
|
325
|
+
}
|
|
326
|
+
function uvCommandEnvironment(env, python) {
|
|
327
|
+
const sanitized = sanitizeUvEnvironment(env);
|
|
328
|
+
if (!python?.virtualEnvironment)
|
|
329
|
+
return sanitized;
|
|
330
|
+
return {
|
|
331
|
+
...activateVirtualEnvironment(sanitized, python.virtualEnvironment, python.executable),
|
|
332
|
+
UV_PYTHON: python.executable
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function sanitizeUvEnvironment(env) {
|
|
336
|
+
const sanitized = { ...env };
|
|
337
|
+
for (const key of Object.keys(sanitized)) {
|
|
338
|
+
if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
|
|
339
|
+
delete sanitized[key];
|
|
340
|
+
}
|
|
341
|
+
return sanitized;
|
|
342
|
+
}
|
|
343
|
+
function assertNoTargetOverride(args) {
|
|
344
|
+
for (const argument of args.slice(1)) {
|
|
345
|
+
if (argument === "--")
|
|
346
|
+
return;
|
|
347
|
+
const [name] = argument.split("=", 1);
|
|
348
|
+
if (name && TARGET_OPTIONS.has(name)) {
|
|
349
|
+
throw new Error(`${name} cannot override the environment selected by Pyfig.`);
|
|
350
|
+
}
|
|
351
|
+
if (argument === "-p" || argument.startsWith("-p=") || /^-p[^-]/u.test(argument)) {
|
|
352
|
+
throw new Error("-p cannot override the environment selected by Pyfig.");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/probes.ts
|
|
358
|
+
var import_semver2 = __toESM(require("semver"), 1);
|
|
319
359
|
|
|
320
360
|
// src/process.ts
|
|
321
361
|
var import_node_child_process = require("node:child_process");
|
|
322
|
-
var
|
|
362
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
323
363
|
var import_which = __toESM(require("which"), 1);
|
|
324
364
|
async function resolveExecutable(value, env) {
|
|
325
365
|
if (!value)
|
|
326
366
|
return null;
|
|
327
|
-
if (
|
|
367
|
+
if (import_node_path4.default.isAbsolute(value)) {
|
|
328
368
|
return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
|
|
329
369
|
}
|
|
330
370
|
if (!/^[A-Za-z0-9_.-]+$/u.test(value))
|
|
@@ -341,14 +381,15 @@ function run(executable, args, env, timeoutMs) {
|
|
|
341
381
|
});
|
|
342
382
|
if (result.status !== 0 || result.error) {
|
|
343
383
|
const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
|
|
344
|
-
throw new Error(`${
|
|
384
|
+
throw new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
|
|
345
385
|
}
|
|
346
386
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
347
387
|
}
|
|
348
|
-
async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
388
|
+
async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
|
|
349
389
|
return new Promise((resolve, reject) => {
|
|
350
390
|
const child = import_node_child_process.spawn(executable, args, {
|
|
351
391
|
env,
|
|
392
|
+
cwd,
|
|
352
393
|
stdio: ["ignore", "pipe", "pipe"],
|
|
353
394
|
windowsHide: true
|
|
354
395
|
});
|
|
@@ -369,11 +410,11 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
|
369
410
|
const abort = () => {
|
|
370
411
|
child.kill();
|
|
371
412
|
const reason = signal?.reason;
|
|
372
|
-
finish(reason instanceof Error ? reason : new Error(`${
|
|
413
|
+
finish(reason instanceof Error ? reason : new Error(`${import_node_path4.default.basename(executable)} was aborted.`));
|
|
373
414
|
};
|
|
374
415
|
const timer = setTimeout(() => {
|
|
375
416
|
child.kill();
|
|
376
|
-
finish(new Error(`${
|
|
417
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} timed out after ${timeoutMs}ms.`));
|
|
377
418
|
}, timeoutMs);
|
|
378
419
|
child.stdout.setEncoding("utf8");
|
|
379
420
|
child.stderr.setEncoding("utf8");
|
|
@@ -381,21 +422,21 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
|
381
422
|
stdout += chunk;
|
|
382
423
|
if (stdout.length > 4 * 1024 * 1024) {
|
|
383
424
|
child.kill();
|
|
384
|
-
finish(new Error(`${
|
|
425
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the output limit.`));
|
|
385
426
|
}
|
|
386
427
|
});
|
|
387
428
|
child.stderr.on("data", (chunk) => {
|
|
388
429
|
stderr += chunk;
|
|
389
430
|
if (stderr.length > 4 * 1024 * 1024) {
|
|
390
431
|
child.kill();
|
|
391
|
-
finish(new Error(`${
|
|
432
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the error-output limit.`));
|
|
392
433
|
}
|
|
393
434
|
});
|
|
394
435
|
child.once("error", (error) => finish(error));
|
|
395
436
|
child.once("close", (code, childSignal) => {
|
|
396
437
|
if (code !== 0) {
|
|
397
438
|
const detail = stderr.trim() || (childSignal ? `signal ${childSignal}` : `exit ${code}`);
|
|
398
|
-
finish(new Error(`${
|
|
439
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
|
|
399
440
|
return;
|
|
400
441
|
}
|
|
401
442
|
finish(undefined, { stdout, stderr });
|
|
@@ -407,213 +448,6 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
|
407
448
|
});
|
|
408
449
|
}
|
|
409
450
|
|
|
410
|
-
// src/coordination.ts
|
|
411
|
-
var realmMutexTails = new Map;
|
|
412
|
-
var flockLookups = new Map;
|
|
413
|
-
var FLOCK_READY = "PYFIG_FLOCK_READY";
|
|
414
|
-
var FLOCK_HOLDER = [
|
|
415
|
-
`process.stdout.write('${FLOCK_READY}\\n')`,
|
|
416
|
-
"process.stdin.resume()",
|
|
417
|
-
'process.stdin.once("end",()=>process.exit(0))'
|
|
418
|
-
].join(";");
|
|
419
|
-
async function withProvisioningLock(options) {
|
|
420
|
-
const existing = await options.check();
|
|
421
|
-
if (existing !== null)
|
|
422
|
-
return existing;
|
|
423
|
-
return withResourceLock(options, async (signal) => {
|
|
424
|
-
const rechecked = await options.check();
|
|
425
|
-
return rechecked ?? options.mutate(signal);
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
async function withResourceLock(options, operation) {
|
|
429
|
-
await import_promises.mkdir(options.target, { recursive: true });
|
|
430
|
-
return withRealmMutex(options.key, () => withWorkerLock(options, () => withNativeFlock(options, () => withPortableLease(options, operation))));
|
|
431
|
-
}
|
|
432
|
-
async function withRealmMutex(key, operation) {
|
|
433
|
-
const previous = realmMutexTails.get(key) ?? Promise.resolve();
|
|
434
|
-
let releaseGate;
|
|
435
|
-
const gate = new Promise((resolve) => {
|
|
436
|
-
releaseGate = resolve;
|
|
437
|
-
});
|
|
438
|
-
const tail = previous.catch(() => {
|
|
439
|
-
return;
|
|
440
|
-
}).then(() => gate);
|
|
441
|
-
realmMutexTails.set(key, tail);
|
|
442
|
-
await previous.catch(() => {
|
|
443
|
-
return;
|
|
444
|
-
});
|
|
445
|
-
try {
|
|
446
|
-
return await operation();
|
|
447
|
-
} finally {
|
|
448
|
-
releaseGate();
|
|
449
|
-
if (realmMutexTails.get(key) === tail)
|
|
450
|
-
realmMutexTails.delete(key);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
async function withWorkerLock(options, operation) {
|
|
454
|
-
const manager = workerThreads.locks;
|
|
455
|
-
if (!manager?.request)
|
|
456
|
-
return operation();
|
|
457
|
-
const controller = new AbortController;
|
|
458
|
-
const timeout = setTimeout(() => controller.abort(new Error(`Timed out waiting for worker lock ${options.key}.`)), options.timeoutMs);
|
|
459
|
-
let entered = false;
|
|
460
|
-
try {
|
|
461
|
-
return await manager.request(workerLockName(options.key), { mode: "exclusive", signal: controller.signal }, async () => {
|
|
462
|
-
entered = true;
|
|
463
|
-
return operation();
|
|
464
|
-
});
|
|
465
|
-
} catch (error) {
|
|
466
|
-
if (entered)
|
|
467
|
-
throw error;
|
|
468
|
-
return operation();
|
|
469
|
-
} finally {
|
|
470
|
-
clearTimeout(timeout);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
async function withNativeFlock(options, operation) {
|
|
474
|
-
if (process.platform === "win32")
|
|
475
|
-
return operation();
|
|
476
|
-
const executable = await findFlock(process.env);
|
|
477
|
-
if (!executable)
|
|
478
|
-
return operation();
|
|
479
|
-
const child = await acquireFlock(executable, `${options.target}.flock`, options.timeoutMs);
|
|
480
|
-
if (!child)
|
|
481
|
-
return operation();
|
|
482
|
-
try {
|
|
483
|
-
return await operation();
|
|
484
|
-
} finally {
|
|
485
|
-
await releaseFlock(child);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
async function findFlock(env) {
|
|
489
|
-
const lookupKey = `${env["PATH"] ?? ""}\x00${env["PATHEXT"] ?? ""}`;
|
|
490
|
-
let lookup = flockLookups.get(lookupKey);
|
|
491
|
-
if (!lookup) {
|
|
492
|
-
lookup = resolveExecutable("flock", env);
|
|
493
|
-
flockLookups.set(lookupKey, lookup);
|
|
494
|
-
}
|
|
495
|
-
return lookup;
|
|
496
|
-
}
|
|
497
|
-
async function acquireFlock(executable, lockPath, timeoutMs) {
|
|
498
|
-
const waitSeconds = Math.max(1, Math.ceil(Math.min(timeoutMs, 30000) / 1000));
|
|
499
|
-
const child = import_node_child_process2.spawn(executable, ["-x", "-w", String(waitSeconds), lockPath, process.execPath, "-e", FLOCK_HOLDER], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
500
|
-
child.stderr.resume();
|
|
501
|
-
let output = "";
|
|
502
|
-
const acquired = await new Promise((resolve) => {
|
|
503
|
-
let settled = false;
|
|
504
|
-
const finish = (value) => {
|
|
505
|
-
if (settled)
|
|
506
|
-
return;
|
|
507
|
-
settled = true;
|
|
508
|
-
clearTimeout(timer);
|
|
509
|
-
resolve(value);
|
|
510
|
-
};
|
|
511
|
-
const timer = setTimeout(() => finish(false), Math.min(timeoutMs, 30000) + 5000);
|
|
512
|
-
child.once("error", () => finish(false));
|
|
513
|
-
child.once("exit", () => finish(false));
|
|
514
|
-
child.stdout.setEncoding("utf8");
|
|
515
|
-
child.stdout.on("data", (chunk) => {
|
|
516
|
-
output += chunk;
|
|
517
|
-
if (output.includes(FLOCK_READY))
|
|
518
|
-
finish(true);
|
|
519
|
-
});
|
|
520
|
-
});
|
|
521
|
-
if (acquired)
|
|
522
|
-
return child;
|
|
523
|
-
child.kill();
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
async function releaseFlock(child) {
|
|
527
|
-
if (child.exitCode !== null)
|
|
528
|
-
return;
|
|
529
|
-
child.stdin.end();
|
|
530
|
-
await new Promise((resolve) => {
|
|
531
|
-
const timer = setTimeout(() => {
|
|
532
|
-
child.kill();
|
|
533
|
-
resolve();
|
|
534
|
-
}, 5000);
|
|
535
|
-
child.once("exit", () => {
|
|
536
|
-
clearTimeout(timer);
|
|
537
|
-
resolve();
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
async function withPortableLease(options, operation) {
|
|
542
|
-
const controller = new AbortController;
|
|
543
|
-
let compromised = null;
|
|
544
|
-
const retryIntervalMs = 500;
|
|
545
|
-
const release = await import_proper_lockfile.default.lock(options.target, {
|
|
546
|
-
realpath: true,
|
|
547
|
-
stale: 120000,
|
|
548
|
-
update: 15000,
|
|
549
|
-
retries: {
|
|
550
|
-
retries: Math.ceil(options.timeoutMs / retryIntervalMs),
|
|
551
|
-
factor: 1,
|
|
552
|
-
minTimeout: retryIntervalMs,
|
|
553
|
-
maxTimeout: retryIntervalMs
|
|
554
|
-
},
|
|
555
|
-
onCompromised: (error) => {
|
|
556
|
-
compromised = error;
|
|
557
|
-
controller.abort(error);
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
|
-
try {
|
|
561
|
-
const result = await operation(controller.signal);
|
|
562
|
-
if (compromised)
|
|
563
|
-
throw compromised;
|
|
564
|
-
return result;
|
|
565
|
-
} finally {
|
|
566
|
-
controller.abort();
|
|
567
|
-
await release();
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
function workerLockName(key) {
|
|
571
|
-
return `pyfig:${import_node_crypto2.createHash("sha256").update(key).digest("hex")}`;
|
|
572
|
-
}
|
|
573
|
-
function cacheLockTarget(cacheDirectory, name) {
|
|
574
|
-
return import_node_path4.default.join(cacheDirectory, "locks", name);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// src/probes.ts
|
|
578
|
-
var import_node_path6 = __toESM(require("node:path"), 1);
|
|
579
|
-
var import_semver2 = __toESM(require("semver"), 1);
|
|
580
|
-
|
|
581
|
-
// src/virtual-environment.ts
|
|
582
|
-
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
583
|
-
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
584
|
-
var VIRTUAL_ENV_DIRECTORY_NAME = ".venv";
|
|
585
|
-
function virtualEnvironmentDirectory(projectRoot) {
|
|
586
|
-
return import_node_path5.default.join(projectRoot, VIRTUAL_ENV_DIRECTORY_NAME);
|
|
587
|
-
}
|
|
588
|
-
function virtualEnvironmentPythonExecutable(virtualEnvironment) {
|
|
589
|
-
return import_node_path5.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
|
|
590
|
-
}
|
|
591
|
-
function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
|
|
592
|
-
const activated = { ...env };
|
|
593
|
-
const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
|
|
594
|
-
const existingPath = pathEntry ? activated[pathEntry] : undefined;
|
|
595
|
-
for (const key of Object.keys(activated)) {
|
|
596
|
-
const normalized = key.toUpperCase();
|
|
597
|
-
if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
|
|
598
|
-
delete activated[key];
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
activated["PATH"] = [import_node_path5.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path5.default.delimiter);
|
|
602
|
-
activated["VIRTUAL_ENV"] = virtualEnvironment;
|
|
603
|
-
return activated;
|
|
604
|
-
}
|
|
605
|
-
function pathsEqual(left, right) {
|
|
606
|
-
return canonicalPath(left) === canonicalPath(right);
|
|
607
|
-
}
|
|
608
|
-
function canonicalPath(value) {
|
|
609
|
-
const resolved = import_node_path5.default.resolve(value);
|
|
610
|
-
let canonical = resolved;
|
|
611
|
-
try {
|
|
612
|
-
canonical = import_node_fs2.default.realpathSync.native(resolved);
|
|
613
|
-
} catch {}
|
|
614
|
-
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
451
|
// src/probes.ts
|
|
618
452
|
var PYTHON_PROBE = `
|
|
619
453
|
import json
|
|
@@ -683,22 +517,6 @@ print(json.dumps({
|
|
|
683
517
|
"embedding": embedding,
|
|
684
518
|
}))
|
|
685
519
|
`;
|
|
686
|
-
async function findPython(options) {
|
|
687
|
-
const candidates = [];
|
|
688
|
-
if (options.python)
|
|
689
|
-
candidates.push({ executable: options.python, args: [] });
|
|
690
|
-
if (process.platform === "win32") {
|
|
691
|
-
candidates.push({ executable: "python", args: [] }, { executable: "python3", args: [] }, { executable: "py", args: ["-3"] });
|
|
692
|
-
} else {
|
|
693
|
-
candidates.push(...["python3.15", "python3.14", "python3.13", "python3.12", "python3.11", "python3.10"].map((executable) => ({ executable, args: [] })), { executable: "python3", args: [] }, { executable: "python", args: [] });
|
|
694
|
-
}
|
|
695
|
-
for (const candidate of candidates) {
|
|
696
|
-
const info = await probePython(candidate, options);
|
|
697
|
-
if (info && pythonSatisfiesRequirements(info, options))
|
|
698
|
-
return info;
|
|
699
|
-
}
|
|
700
|
-
return null;
|
|
701
|
-
}
|
|
702
520
|
function pythonSatisfiesRequirements(python, options) {
|
|
703
521
|
return import_semver2.default.satisfies(python.version, options.pythonRange) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
|
|
704
522
|
}
|
|
@@ -714,50 +532,6 @@ async function probePython(candidate, options) {
|
|
|
714
532
|
return null;
|
|
715
533
|
}
|
|
716
534
|
}
|
|
717
|
-
async function findPip(python, options) {
|
|
718
|
-
if (!python.virtualEnvironment) {
|
|
719
|
-
throw new Error("pip discovery requires a virtual-environment Python interpreter.");
|
|
720
|
-
}
|
|
721
|
-
const activeEnv = activateVirtualEnvironment(options.env, python.virtualEnvironment, python.executable);
|
|
722
|
-
try {
|
|
723
|
-
const { stdout } = run(python.executable, ["-m", "pip", "--version"], activeEnv, options.probeTimeoutMs);
|
|
724
|
-
const details = parsePipVersionOutput(stdout);
|
|
725
|
-
const version = normalizeVersion(details?.version);
|
|
726
|
-
if (!version || !import_semver2.default.satisfies(version, options.pipRange))
|
|
727
|
-
return null;
|
|
728
|
-
if (!details || !pipBelongsToPython(details, python))
|
|
729
|
-
return null;
|
|
730
|
-
return {
|
|
731
|
-
kind: "pip",
|
|
732
|
-
executable: python.executable,
|
|
733
|
-
prefixArgs: ["-m", "pip"],
|
|
734
|
-
version,
|
|
735
|
-
pythonExecutable: python.executable,
|
|
736
|
-
virtualEnvironment: python.virtualEnvironment
|
|
737
|
-
};
|
|
738
|
-
} catch {
|
|
739
|
-
return null;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
function parsePipVersionOutput(output) {
|
|
743
|
-
const match = output.trim().match(/^pip\s+(\S+)\s+from\s+(.+?)\s+\(python\s+(\d+)\.(\d+)(?:\.\d+)?\)$/iu);
|
|
744
|
-
if (!match?.[1] || !match[2] || !match[3] || !match[4])
|
|
745
|
-
return null;
|
|
746
|
-
return {
|
|
747
|
-
version: match[1],
|
|
748
|
-
location: match[2],
|
|
749
|
-
pythonMajor: Number.parseInt(match[3], 10),
|
|
750
|
-
pythonMinor: Number.parseInt(match[4], 10)
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
function pipBelongsToPython(pip, python) {
|
|
754
|
-
if (pip.pythonMajor !== python.major || pip.pythonMinor !== python.minor)
|
|
755
|
-
return false;
|
|
756
|
-
const prefix = normalizePathForComparison(import_node_path6.default.resolve(python.prefix));
|
|
757
|
-
const location = normalizePathForComparison(import_node_path6.default.resolve(pip.location));
|
|
758
|
-
const relative = import_node_path6.default.relative(prefix, location);
|
|
759
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path6.default.sep}`) && !import_node_path6.default.isAbsolute(relative);
|
|
760
|
-
}
|
|
761
535
|
async function findUv(options) {
|
|
762
536
|
const configured = options.env["PYFIG_UV"] ?? "uv";
|
|
763
537
|
const executable = await resolveExecutable(configured, options.env);
|
|
@@ -778,9 +552,6 @@ function normalizeVersion(value) {
|
|
|
778
552
|
return null;
|
|
779
553
|
return import_semver2.default.valid(value) ?? import_semver2.default.coerce(value)?.version ?? null;
|
|
780
554
|
}
|
|
781
|
-
function normalizePathForComparison(value) {
|
|
782
|
-
return process.platform === "win32" ? value.toLowerCase() : value;
|
|
783
|
-
}
|
|
784
555
|
function pythonArchitectureMatchesRuntime(python) {
|
|
785
556
|
const architecture = python.architecture.toLowerCase();
|
|
786
557
|
if (python.pointerBits !== (process.arch === "ia32" ? 32 : 64))
|
|
@@ -795,228 +566,223 @@ function pythonArchitectureMatchesRuntime(python) {
|
|
|
795
566
|
return architecture === process.arch;
|
|
796
567
|
}
|
|
797
568
|
|
|
798
|
-
// src/state.ts
|
|
799
|
-
var completed = new Map;
|
|
800
|
-
var inFlight = new Map;
|
|
801
|
-
var cacheGeneration = 0;
|
|
802
|
-
async function memoizeAsync(namespace, key, ttlMs, validate, load) {
|
|
803
|
-
const cacheKey = `${namespace}\x00${key}`;
|
|
804
|
-
const cached = completed.get(cacheKey);
|
|
805
|
-
if (cached && cached.expiresAt >= Date.now()) {
|
|
806
|
-
try {
|
|
807
|
-
if (await validate(cached.value))
|
|
808
|
-
return cached.value;
|
|
809
|
-
} catch {}
|
|
810
|
-
completed.delete(cacheKey);
|
|
811
|
-
}
|
|
812
|
-
const pending = inFlight.get(cacheKey);
|
|
813
|
-
if (pending)
|
|
814
|
-
return pending;
|
|
815
|
-
const generation = cacheGeneration;
|
|
816
|
-
const loading = load().then((value) => {
|
|
817
|
-
if (ttlMs > 0 && generation === cacheGeneration) {
|
|
818
|
-
completed.set(cacheKey, { expiresAt: Date.now() + ttlMs, value });
|
|
819
|
-
}
|
|
820
|
-
return value;
|
|
821
|
-
}).finally(() => {
|
|
822
|
-
if (inFlight.get(cacheKey) === loading)
|
|
823
|
-
inFlight.delete(cacheKey);
|
|
824
|
-
});
|
|
825
|
-
inFlight.set(cacheKey, loading);
|
|
826
|
-
return loading;
|
|
827
|
-
}
|
|
828
|
-
function memoizeSync(namespace, key, ttlMs, validate, load) {
|
|
829
|
-
const cacheKey = `${namespace}\x00${key}`;
|
|
830
|
-
const cached = completed.get(cacheKey);
|
|
831
|
-
if (cached && cached.expiresAt >= Date.now()) {
|
|
832
|
-
try {
|
|
833
|
-
if (validate(cached.value))
|
|
834
|
-
return cached.value;
|
|
835
|
-
} catch {}
|
|
836
|
-
completed.delete(cacheKey);
|
|
837
|
-
}
|
|
838
|
-
const value = load();
|
|
839
|
-
if (ttlMs > 0)
|
|
840
|
-
completed.set(cacheKey, { expiresAt: Date.now() + ttlMs, value });
|
|
841
|
-
return value;
|
|
842
|
-
}
|
|
843
|
-
function clearPyfigCaches() {
|
|
844
|
-
cacheGeneration += 1;
|
|
845
|
-
completed.clear();
|
|
846
|
-
inFlight.clear();
|
|
847
|
-
}
|
|
848
|
-
|
|
849
569
|
// src/provision.ts
|
|
850
|
-
async function ensurePython(options = {}) {
|
|
851
|
-
const resolved = resolveOptions(options);
|
|
852
|
-
return memoizeAsync("python", configurationFingerprint(resolved), resolved.cacheTtlMs, pythonIsPresent, () => ensurePythonResolved(resolved));
|
|
853
|
-
}
|
|
854
|
-
async function ensureVirtualEnvironment(options = {}) {
|
|
855
|
-
const resolved = resolveOptions(options);
|
|
856
|
-
return memoizeAsync("venv", configurationFingerprint(resolved), resolved.cacheTtlMs, (python) => pythonIsPresent(python) && Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(resolved.projectRoot)), () => ensureVirtualEnvironmentResolved(resolved));
|
|
857
|
-
}
|
|
858
570
|
async function ensureUv(options = {}) {
|
|
859
|
-
|
|
860
|
-
return memoizeAsync("uv", configurationFingerprint(resolved), resolved.cacheTtlMs, (uv) => import_node_fs4.default.existsSync(uv.executable), () => ensureUvResolved(resolved));
|
|
571
|
+
return ensureUvResolved(resolveOptions(options));
|
|
861
572
|
}
|
|
862
|
-
async function ensureEnvironment(options = {}) {
|
|
573
|
+
async function ensureEnvironment(options = {}, requirements = [], dependencyOptions = {}) {
|
|
863
574
|
const resolved = resolveOptions(options);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
575
|
+
const normalized = normalizeRequirements(requirements);
|
|
576
|
+
const selections = normalizeDependencyOptions(dependencyOptions);
|
|
577
|
+
const uv = await ensureUvResolved(resolved);
|
|
578
|
+
const python = await ensureVirtualEnvironmentResolved(resolved, uv);
|
|
579
|
+
if (normalized.length > 0 || selections.extras.length > 0 || selections.groups.length > 0 || selections.allExtras) {
|
|
580
|
+
await installRuntimeRequirements(uv, python, normalized, selections, resolved);
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
python,
|
|
584
|
+
uv,
|
|
585
|
+
managed: isManagedEnvironment(python, resolved)
|
|
586
|
+
};
|
|
869
587
|
}
|
|
870
|
-
async function ensureVirtualEnvironmentResolved(options) {
|
|
588
|
+
async function ensureVirtualEnvironmentResolved(options, uv) {
|
|
589
|
+
const existing = await findReusableEnvironment(options);
|
|
590
|
+
if (existing)
|
|
591
|
+
return existing;
|
|
871
592
|
const directory = virtualEnvironmentDirectory(options.projectRoot);
|
|
872
593
|
const check = async () => {
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
return null;
|
|
876
|
-
if (pythonSatisfiesRequirements(existing, options) && await virtualEnvironmentMatchesConfiguredPython(existing, options)) {
|
|
877
|
-
return existing;
|
|
878
|
-
}
|
|
879
|
-
throw incompatibleVirtualEnvironmentError(directory, existing, options);
|
|
594
|
+
const candidate = await inspectVirtualEnvironment(directory, options);
|
|
595
|
+
return candidate && pythonSatisfiesRequirements(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
|
|
880
596
|
};
|
|
881
|
-
const existing = await check();
|
|
882
|
-
if (existing)
|
|
883
|
-
return existing;
|
|
884
597
|
if (!options.createVirtualEnvironment) {
|
|
885
|
-
throw new Error(`No
|
|
598
|
+
throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
|
|
886
599
|
}
|
|
887
|
-
const basePython = await
|
|
888
|
-
const preferredUv = await findAvailableUv(options) ?? (options.installUv ? await ensureUvResolved(options) : null);
|
|
600
|
+
const basePython = await ensureBasePythonResolved(uv, options);
|
|
889
601
|
return withProvisioningLock({
|
|
890
602
|
key: `venv:${canonicalPath(directory)}`,
|
|
891
|
-
target: directory,
|
|
892
603
|
timeoutMs: options.coordinationTimeoutMs,
|
|
893
|
-
env: options.env,
|
|
894
604
|
check,
|
|
895
|
-
mutate: async (
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
605
|
+
mutate: async () => {
|
|
606
|
+
await import_promises.rm(directory, { recursive: true, force: true });
|
|
607
|
+
await import_promises.mkdir(import_node_path5.default.dirname(directory), { recursive: true });
|
|
608
|
+
await runAsync(uv.executable, [
|
|
609
|
+
"venv",
|
|
610
|
+
"--no-project",
|
|
611
|
+
"--no-config",
|
|
612
|
+
"--no-python-downloads",
|
|
613
|
+
"--python",
|
|
614
|
+
basePython.executable,
|
|
615
|
+
directory
|
|
616
|
+
], isolatedUvEnvironment(options), options.installTimeoutMs);
|
|
617
|
+
const installed = await check();
|
|
618
|
+
if (!installed) {
|
|
619
|
+
throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
|
|
905
620
|
}
|
|
906
621
|
return installed;
|
|
907
622
|
}
|
|
908
623
|
});
|
|
909
624
|
}
|
|
910
|
-
async function
|
|
911
|
-
if (
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
625
|
+
async function findReusableEnvironment(options) {
|
|
626
|
+
if (options.python) {
|
|
627
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
628
|
+
if (!configured || !pythonSatisfiesRequirements(configured, options)) {
|
|
629
|
+
throw incompatibleConfiguredPython(options);
|
|
630
|
+
}
|
|
631
|
+
if (configured.virtualEnvironment)
|
|
632
|
+
return configured;
|
|
916
633
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
634
|
+
for (const directory of [
|
|
635
|
+
...externalVirtualEnvironmentDirectories(options.projectRoot, options.env),
|
|
636
|
+
virtualEnvironmentDirectory(options.projectRoot)
|
|
637
|
+
]) {
|
|
638
|
+
const info = await inspectVirtualEnvironment(directory, options);
|
|
639
|
+
if (info && pythonSatisfiesRequirements(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
|
|
640
|
+
return info;
|
|
641
|
+
}
|
|
923
642
|
}
|
|
924
|
-
|
|
925
|
-
}
|
|
926
|
-
async function createVirtualEnvironmentWithUv(uv, directory, basePython, options, signal) {
|
|
927
|
-
await runAsync(uv.executable, [
|
|
928
|
-
"venv",
|
|
929
|
-
"--no-project",
|
|
930
|
-
"--no-config",
|
|
931
|
-
"--no-python-downloads",
|
|
932
|
-
"--allow-existing",
|
|
933
|
-
"--python",
|
|
934
|
-
basePython.executable,
|
|
935
|
-
directory
|
|
936
|
-
], uvEnvironment(options), options.installTimeoutMs, signal);
|
|
937
|
-
}
|
|
938
|
-
async function inspectVirtualEnvironment(directory, options) {
|
|
939
|
-
const executable = virtualEnvironmentPythonExecutable(directory);
|
|
940
|
-
const info = await probePython({ executable, args: [] }, options);
|
|
941
|
-
return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
|
|
643
|
+
return null;
|
|
942
644
|
}
|
|
943
|
-
async function
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
645
|
+
async function ensureBasePythonResolved(uv, options) {
|
|
646
|
+
if (options.python) {
|
|
647
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
648
|
+
if (!configured || !pythonSatisfiesRequirements(configured, options)) {
|
|
649
|
+
throw incompatibleConfiguredPython(options);
|
|
650
|
+
}
|
|
651
|
+
if (!configured.virtualEnvironment)
|
|
652
|
+
return configured;
|
|
653
|
+
const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
|
|
654
|
+
if (base && pythonSatisfiesRequirements(base, options))
|
|
655
|
+
return base;
|
|
656
|
+
throw incompatibleConfiguredPython(options);
|
|
657
|
+
}
|
|
658
|
+
const system = await findPythonWithUv(uv, options, "system");
|
|
659
|
+
if (system)
|
|
660
|
+
return system;
|
|
661
|
+
const cachedManaged = await findPythonWithUv(uv, options, "managed");
|
|
662
|
+
if (cachedManaged)
|
|
663
|
+
return cachedManaged;
|
|
947
664
|
if (!options.installPython) {
|
|
948
|
-
|
|
949
|
-
throw new Error(`No Python interpreter satisfies ${options.pythonRange}${capability}; ` + "managed Python installation is disabled.");
|
|
665
|
+
throw new Error(missingPythonMessage(options, "managed Python installation is disabled"));
|
|
950
666
|
}
|
|
951
|
-
const lockTarget = cacheLockTarget(options.cacheDir, "python");
|
|
952
667
|
return withProvisioningLock({
|
|
953
|
-
key: `python:${canonicalPath(
|
|
954
|
-
target: lockTarget,
|
|
668
|
+
key: `python:${canonicalPath(import_node_path5.default.join(options.cacheDir, "python"))}`,
|
|
955
669
|
timeoutMs: options.coordinationTimeoutMs,
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const
|
|
960
|
-
const uvEnv = uvEnvironment(options);
|
|
961
|
-
await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], uvEnv, options.installTimeoutMs, signal);
|
|
962
|
-
const installed = await findPythonWithUv(uv, options, signal);
|
|
670
|
+
check: () => findPythonWithUv(uv, options, "managed"),
|
|
671
|
+
mutate: async () => {
|
|
672
|
+
await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], isolatedUvEnvironment(options), options.installTimeoutMs);
|
|
673
|
+
const installed = await findPythonWithUv(uv, options, "managed");
|
|
963
674
|
if (!installed) {
|
|
964
|
-
throw new Error(`uv installed
|
|
675
|
+
throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
|
|
965
676
|
}
|
|
966
677
|
return installed;
|
|
967
678
|
}
|
|
968
679
|
});
|
|
969
680
|
}
|
|
970
|
-
async function
|
|
971
|
-
|
|
972
|
-
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
973
|
-
if (configured && pythonSatisfiesRequirements(configured, options))
|
|
974
|
-
return configured;
|
|
975
|
-
throw new Error(`Configured Python ${options.python} is unavailable or does not satisfy ${options.pythonRange}.`);
|
|
976
|
-
}
|
|
977
|
-
const existingUv = await findAvailableUv(options);
|
|
978
|
-
if (existingUv) {
|
|
979
|
-
const uvPython = await findPythonWithUv(existingUv, options);
|
|
980
|
-
if (uvPython)
|
|
981
|
-
return uvPython;
|
|
982
|
-
}
|
|
983
|
-
return findPython({ ...options, python: "" });
|
|
984
|
-
}
|
|
985
|
-
async function findPythonWithUv(uv, options, signal) {
|
|
986
|
-
const requests = [undefined, options.managedPython];
|
|
987
|
-
for (const requestedPython of requests) {
|
|
681
|
+
async function findPythonWithUv(uv, options, source) {
|
|
682
|
+
for (const request of ["", options.managedPython]) {
|
|
988
683
|
const args = [
|
|
989
684
|
"python",
|
|
990
685
|
"find",
|
|
991
686
|
"--no-project",
|
|
992
687
|
"--no-config",
|
|
993
|
-
"--
|
|
994
|
-
"--
|
|
688
|
+
"--no-python-downloads",
|
|
689
|
+
source === "system" ? "--system" : "--managed-python"
|
|
995
690
|
];
|
|
996
|
-
if (
|
|
997
|
-
args.push(
|
|
691
|
+
if (request)
|
|
692
|
+
args.push(request);
|
|
998
693
|
try {
|
|
999
|
-
const { stdout } = await runAsync(uv.executable, args,
|
|
694
|
+
const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
|
|
1000
695
|
const info = await probePython({ executable: stdout.trim(), args: [] }, options);
|
|
1001
696
|
if (info && pythonSatisfiesRequirements(info, options))
|
|
1002
697
|
return info;
|
|
1003
|
-
} catch
|
|
1004
|
-
if (signal?.aborted)
|
|
1005
|
-
throw signal.reason ?? error;
|
|
1006
|
-
}
|
|
698
|
+
} catch {}
|
|
1007
699
|
}
|
|
700
|
+
try {
|
|
701
|
+
const { stdout } = await runAsync(uv.executable, [
|
|
702
|
+
"python",
|
|
703
|
+
"list",
|
|
704
|
+
"--no-config",
|
|
705
|
+
"--no-python-downloads",
|
|
706
|
+
"--only-installed",
|
|
707
|
+
"--output-format",
|
|
708
|
+
"json",
|
|
709
|
+
source === "system" ? "--no-managed-python" : "--managed-python"
|
|
710
|
+
], explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
|
|
711
|
+
const candidates = installedPythonPaths(stdout);
|
|
712
|
+
for (const executable of candidates) {
|
|
713
|
+
const info = await probePython({ executable, args: [] }, options);
|
|
714
|
+
if (info && !info.virtualEnvironment && pythonSatisfiesRequirements(info, options)) {
|
|
715
|
+
return info;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} catch {}
|
|
1008
719
|
return null;
|
|
1009
720
|
}
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
if (
|
|
1013
|
-
return
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
return
|
|
1019
|
-
|
|
721
|
+
function installedPythonPaths(output) {
|
|
722
|
+
const parsed = JSON.parse(output);
|
|
723
|
+
if (!Array.isArray(parsed))
|
|
724
|
+
return [];
|
|
725
|
+
const paths = parsed.flatMap((entry) => {
|
|
726
|
+
if (!entry || typeof entry !== "object" || !("path" in entry))
|
|
727
|
+
return [];
|
|
728
|
+
const candidate = entry.path;
|
|
729
|
+
return typeof candidate === "string" && candidate ? [candidate] : [];
|
|
730
|
+
});
|
|
731
|
+
return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
|
|
732
|
+
}
|
|
733
|
+
async function inspectVirtualEnvironment(directory, options) {
|
|
734
|
+
const executable = virtualEnvironmentPythonExecutable(directory);
|
|
735
|
+
const info = await probePython({ executable, args: [] }, options);
|
|
736
|
+
return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
|
|
737
|
+
}
|
|
738
|
+
async function installRuntimeRequirements(uv, python, requirements, dependencyOptions, options) {
|
|
739
|
+
if (!python.virtualEnvironment) {
|
|
740
|
+
throw new Error("Runtime dependency installation requires a virtual environment.");
|
|
741
|
+
}
|
|
742
|
+
await withProvisioningLock({
|
|
743
|
+
key: `dependencies:${canonicalPath(python.virtualEnvironment)}`,
|
|
744
|
+
timeoutMs: options.coordinationTimeoutMs,
|
|
745
|
+
check: async () => null,
|
|
746
|
+
mutate: async () => {
|
|
747
|
+
const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-requirements-"));
|
|
748
|
+
try {
|
|
749
|
+
const additions = import_node_path5.default.join(temporary, "runtime-requirements.txt");
|
|
750
|
+
const resolved = import_node_path5.default.join(temporary, "resolved-requirements.txt");
|
|
751
|
+
const pyproject = import_node_path5.default.join(options.projectRoot, "pyproject.toml");
|
|
752
|
+
const hasPyproject = import_node_fs4.default.existsSync(pyproject);
|
|
753
|
+
if (!hasPyproject && (dependencyOptions.extras.length > 0 || dependencyOptions.groups.length > 0 || dependencyOptions.allExtras)) {
|
|
754
|
+
throw new Error("Runtime extras and groups require an existing pyproject.toml.");
|
|
755
|
+
}
|
|
756
|
+
const sources = hasPyproject ? [pyproject] : [];
|
|
757
|
+
if (requirements.length > 0) {
|
|
758
|
+
await import_promises.writeFile(additions, `${requirements.join(`
|
|
759
|
+
`)}
|
|
760
|
+
`, "utf8");
|
|
761
|
+
sources.push(additions);
|
|
762
|
+
}
|
|
763
|
+
const selectionArgs = [
|
|
764
|
+
...dependencyOptions.extras.flatMap((extra) => ["--extra", extra]),
|
|
765
|
+
...dependencyOptions.groups.flatMap((group) => ["--group", group]),
|
|
766
|
+
...dependencyOptions.allExtras ? ["--all-extras"] : []
|
|
767
|
+
];
|
|
768
|
+
await runAsync(uv.executable, [
|
|
769
|
+
"pip",
|
|
770
|
+
"compile",
|
|
771
|
+
"--no-config",
|
|
772
|
+
"--python",
|
|
773
|
+
python.executable,
|
|
774
|
+
...selectionArgs,
|
|
775
|
+
...sources,
|
|
776
|
+
"--output-file",
|
|
777
|
+
resolved
|
|
778
|
+
], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
779
|
+
await runAsync(uv.executable, ["pip", "install", "--no-config", "--python", python.executable, "-r", resolved], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
780
|
+
} finally {
|
|
781
|
+
await import_promises.rm(temporary, { recursive: true, force: true });
|
|
782
|
+
}
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
});
|
|
1020
786
|
}
|
|
1021
787
|
async function ensureUvResolved(options) {
|
|
1022
788
|
const existing = await findAvailableUv(options);
|
|
@@ -1027,15 +793,12 @@ async function ensureUvResolved(options) {
|
|
|
1027
793
|
}
|
|
1028
794
|
const installDir = uvInstallDirectory(options);
|
|
1029
795
|
const executable = uvExecutable(options);
|
|
1030
|
-
const lockTarget = cacheLockTarget(options.cacheDir, "uv");
|
|
1031
796
|
return withProvisioningLock({
|
|
1032
|
-
key: `uv:${canonicalPath(
|
|
1033
|
-
target: lockTarget,
|
|
797
|
+
key: `uv:${canonicalPath(installDir)}`,
|
|
1034
798
|
timeoutMs: options.coordinationTimeoutMs,
|
|
1035
|
-
env: options.env,
|
|
1036
799
|
check: () => findAvailableUv(options),
|
|
1037
|
-
mutate: async (
|
|
1038
|
-
await installUv(installDir, options
|
|
800
|
+
mutate: async () => {
|
|
801
|
+
await installUv(installDir, options);
|
|
1039
802
|
const result = probeUv(executable, options);
|
|
1040
803
|
if (!result || result.version !== options.uvVersion) {
|
|
1041
804
|
throw new Error(`Expected managed uv ${options.uvVersion}, but the installed executable did not match.`);
|
|
@@ -1045,88 +808,122 @@ async function ensureUvResolved(options) {
|
|
|
1045
808
|
});
|
|
1046
809
|
}
|
|
1047
810
|
async function findAvailableUv(options) {
|
|
1048
|
-
const
|
|
1049
|
-
if (
|
|
1050
|
-
return
|
|
811
|
+
const external = await findUv(options);
|
|
812
|
+
if (external)
|
|
813
|
+
return external;
|
|
1051
814
|
const managed = probeUv(uvExecutable(options), options);
|
|
1052
815
|
return managed?.version === options.uvVersion ? managed : null;
|
|
1053
816
|
}
|
|
1054
|
-
function
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
817
|
+
function normalizeRequirements(requirements) {
|
|
818
|
+
const normalized = requirements.map((requirement) => requirement.trim());
|
|
819
|
+
for (const requirement of normalized) {
|
|
820
|
+
if (!requirement || /[\0\r\n]/u.test(requirement)) {
|
|
821
|
+
throw new TypeError(`Invalid runtime requirement: ${JSON.stringify(requirement)}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return [...new Set(normalized)];
|
|
1059
825
|
}
|
|
1060
|
-
function
|
|
1061
|
-
|
|
1062
|
-
|
|
826
|
+
function normalizeDependencyOptions(options) {
|
|
827
|
+
const extras = normalizeNames("extra", options.extras ?? []);
|
|
828
|
+
const groups = normalizeNames("dependency group", options.groups ?? []);
|
|
829
|
+
if (options.allExtras && extras.length > 0) {
|
|
830
|
+
throw new TypeError("allExtras cannot be combined with explicit extras.");
|
|
1063
831
|
}
|
|
1064
|
-
return {
|
|
1065
|
-
kind: "uv",
|
|
1066
|
-
executable: uv.executable,
|
|
1067
|
-
prefixArgs: ["pip"],
|
|
1068
|
-
version: uv.version,
|
|
1069
|
-
pythonExecutable: python.executable,
|
|
1070
|
-
virtualEnvironment: python.virtualEnvironment
|
|
1071
|
-
};
|
|
832
|
+
return { extras, groups, allExtras: options.allExtras ?? false };
|
|
1072
833
|
}
|
|
1073
|
-
function
|
|
1074
|
-
const
|
|
1075
|
-
for (const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
delete result[key];
|
|
834
|
+
function normalizeNames(kind, names) {
|
|
835
|
+
const normalized = names.map((name) => name.trim());
|
|
836
|
+
for (const name of normalized) {
|
|
837
|
+
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
|
|
838
|
+
throw new TypeError(`Invalid ${kind}: ${JSON.stringify(name)}`);
|
|
1079
839
|
}
|
|
1080
840
|
}
|
|
1081
|
-
return
|
|
841
|
+
return [...new Set(normalized)];
|
|
842
|
+
}
|
|
843
|
+
function isManagedEnvironment(python, options) {
|
|
844
|
+
return Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(options.projectRoot));
|
|
845
|
+
}
|
|
846
|
+
function isolatedUvEnvironment(options) {
|
|
847
|
+
return sanitizeUvEnvironment(uvEnvironment(options));
|
|
848
|
+
}
|
|
849
|
+
function explicitPythonSourceEnvironment(options) {
|
|
850
|
+
const env = isolatedUvEnvironment(options);
|
|
851
|
+
delete env["UV_PYTHON_PREFERENCE"];
|
|
852
|
+
return env;
|
|
853
|
+
}
|
|
854
|
+
function targetUvEnvironment(options, python) {
|
|
855
|
+
return uvCommandEnvironment(uvEnvironment(options), python);
|
|
856
|
+
}
|
|
857
|
+
function incompatibleConfiguredPython(options) {
|
|
858
|
+
return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `${options.pythonRange}${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
|
|
859
|
+
}
|
|
860
|
+
function missingPythonMessage(options, reason) {
|
|
861
|
+
const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
|
|
862
|
+
return `No Python satisfies ${options.pythonRange}${embedding}; ${reason}.`;
|
|
863
|
+
}
|
|
864
|
+
async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
|
|
865
|
+
if (!options.python)
|
|
866
|
+
return true;
|
|
867
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
868
|
+
if (!configured || !pythonSatisfiesRequirements(configured, options))
|
|
869
|
+
return false;
|
|
870
|
+
if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
|
|
874
|
+
return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
|
|
1082
875
|
}
|
|
1083
|
-
|
|
876
|
+
function uvInstallDirectory(options) {
|
|
877
|
+
return import_node_path5.default.join(options.cacheDir, "tools", "uv", options.uvVersion);
|
|
878
|
+
}
|
|
879
|
+
function uvExecutable(options) {
|
|
880
|
+
return import_node_path5.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
|
|
881
|
+
}
|
|
882
|
+
async function installUv(installDir, options) {
|
|
1084
883
|
const asset = uvAsset();
|
|
1085
884
|
const baseUrl = `https://github.com/astral-sh/uv/releases/download/${options.uvVersion}`;
|
|
1086
|
-
const temporary = await
|
|
885
|
+
const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
|
|
1087
886
|
const dispatcher = proxyDispatcher(options.env);
|
|
1088
887
|
try {
|
|
1089
|
-
const archive =
|
|
1090
|
-
await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs,
|
|
1091
|
-
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs,
|
|
888
|
+
const archive = import_node_path5.default.join(temporary, asset);
|
|
889
|
+
await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
|
|
890
|
+
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
|
|
1092
891
|
const actual = await sha256File(archive);
|
|
1093
892
|
if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
|
|
1094
893
|
throw new Error(`uv archive checksum mismatch for ${asset}`);
|
|
1095
894
|
}
|
|
1096
|
-
const extracted =
|
|
1097
|
-
await
|
|
1098
|
-
signal.throwIfAborted();
|
|
895
|
+
const extracted = import_node_path5.default.join(temporary, "extracted");
|
|
896
|
+
await import_promises.mkdir(extracted, { recursive: true });
|
|
1099
897
|
if (asset.endsWith(".zip"))
|
|
1100
898
|
await import_extract_zip.default(archive, { dir: extracted });
|
|
1101
899
|
else
|
|
1102
900
|
await tar.x({ file: archive, cwd: extracted, strict: true });
|
|
1103
|
-
signal.throwIfAborted();
|
|
1104
901
|
const source = await findFile(extracted, process.platform === "win32" ? "uv.exe" : "uv");
|
|
1105
902
|
if (!source)
|
|
1106
903
|
throw new Error(`uv archive did not contain the expected executable: ${asset}`);
|
|
1107
904
|
await publishUvAtomically(source, installDir, options);
|
|
1108
905
|
} finally {
|
|
1109
906
|
await dispatcher?.close();
|
|
1110
|
-
await
|
|
907
|
+
await import_promises.rm(temporary, { recursive: true, force: true });
|
|
1111
908
|
}
|
|
1112
909
|
}
|
|
1113
910
|
async function publishUvAtomically(source, installDir, options) {
|
|
1114
|
-
await
|
|
911
|
+
await import_promises.mkdir(installDir, { recursive: true });
|
|
1115
912
|
const executable = uvExecutable(options);
|
|
1116
|
-
const staged =
|
|
913
|
+
const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
|
|
1117
914
|
try {
|
|
1118
|
-
await
|
|
915
|
+
await import_promises.copyFile(source, staged);
|
|
1119
916
|
if (process.platform !== "win32")
|
|
1120
|
-
await
|
|
917
|
+
await import_promises.chmod(staged, 493);
|
|
1121
918
|
const stagedUv = probeUv(staged, options);
|
|
1122
919
|
if (!stagedUv || stagedUv.version !== options.uvVersion) {
|
|
1123
920
|
throw new Error(`Downloaded uv executable is not version ${options.uvVersion}.`);
|
|
1124
921
|
}
|
|
1125
922
|
if (process.platform === "win32")
|
|
1126
|
-
await
|
|
1127
|
-
await
|
|
923
|
+
await import_promises.rm(executable, { force: true });
|
|
924
|
+
await import_promises.rename(staged, executable);
|
|
1128
925
|
} finally {
|
|
1129
|
-
await
|
|
926
|
+
await import_promises.rm(staged, { force: true });
|
|
1130
927
|
}
|
|
1131
928
|
}
|
|
1132
929
|
function uvAsset() {
|
|
@@ -1141,23 +938,23 @@ function uvAsset() {
|
|
|
1141
938
|
return `uv-${architecture}-unknown-linux-musl.tar.gz`;
|
|
1142
939
|
throw new Error(`uv bootstrap does not support platform ${process.platform}.`);
|
|
1143
940
|
}
|
|
1144
|
-
async function download(url, destination, timeoutMs,
|
|
941
|
+
async function download(url, destination, timeoutMs, dispatcher) {
|
|
1145
942
|
const response = await import_undici.fetch(url, {
|
|
1146
943
|
dispatcher,
|
|
1147
944
|
redirect: "follow",
|
|
1148
|
-
signal:
|
|
945
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1149
946
|
});
|
|
1150
947
|
if (!response.ok || !response.body) {
|
|
1151
948
|
await response.body?.cancel();
|
|
1152
949
|
throw new Error(`Could not download ${url}: HTTP ${response.status}`);
|
|
1153
950
|
}
|
|
1154
|
-
await
|
|
951
|
+
await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
|
|
1155
952
|
}
|
|
1156
|
-
async function downloadText(url, timeoutMs,
|
|
953
|
+
async function downloadText(url, timeoutMs, dispatcher) {
|
|
1157
954
|
const response = await import_undici.fetch(url, {
|
|
1158
955
|
dispatcher,
|
|
1159
956
|
redirect: "follow",
|
|
1160
|
-
signal:
|
|
957
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1161
958
|
});
|
|
1162
959
|
if (!response.ok) {
|
|
1163
960
|
await response.body?.cancel();
|
|
@@ -1179,18 +976,15 @@ function proxyDispatcher(env) {
|
|
|
1179
976
|
function environmentValue(env, lowercase, uppercase) {
|
|
1180
977
|
return env[lowercase] ?? env[uppercase];
|
|
1181
978
|
}
|
|
1182
|
-
function combinedSignal(timeoutMs, externalSignal) {
|
|
1183
|
-
return AbortSignal.any([externalSignal, AbortSignal.timeout(timeoutMs)]);
|
|
1184
|
-
}
|
|
1185
979
|
async function sha256File(file) {
|
|
1186
|
-
const hash =
|
|
980
|
+
const hash = import_node_crypto.createHash("sha256");
|
|
1187
981
|
for await (const chunk of import_node_fs3.createReadStream(file))
|
|
1188
982
|
hash.update(chunk);
|
|
1189
983
|
return hash.digest("hex");
|
|
1190
984
|
}
|
|
1191
985
|
async function findFile(directory, name) {
|
|
1192
|
-
for (const entry of await
|
|
1193
|
-
const target =
|
|
986
|
+
for (const entry of await import_promises.readdir(directory, { withFileTypes: true })) {
|
|
987
|
+
const target = import_node_path5.default.join(directory, entry.name);
|
|
1194
988
|
if (entry.isFile() && entry.name === name)
|
|
1195
989
|
return target;
|
|
1196
990
|
if (entry.isDirectory()) {
|
|
@@ -1201,36 +995,11 @@ async function findFile(directory, name) {
|
|
|
1201
995
|
}
|
|
1202
996
|
return null;
|
|
1203
997
|
}
|
|
1204
|
-
function incompatibleVirtualEnvironmentError(directory, python, options) {
|
|
1205
|
-
const capabilities = options.requireEmbedding ? " or lacks compatible CPython embedding headers, library, or architecture" : "";
|
|
1206
|
-
return new Error(`The existing virtual environment at ${directory} uses Python ${python.version}, ` + `which does not satisfy ${options.pythonRange}${capabilities}.`);
|
|
1207
|
-
}
|
|
1208
|
-
async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
|
|
1209
|
-
if (!options.python)
|
|
1210
|
-
return true;
|
|
1211
|
-
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
1212
|
-
if (!configured || !pythonSatisfiesRequirements(configured, options))
|
|
1213
|
-
return false;
|
|
1214
|
-
if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
|
|
1215
|
-
return true;
|
|
1216
|
-
}
|
|
1217
|
-
const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
|
|
1218
|
-
return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
|
|
1219
|
-
}
|
|
1220
|
-
function pythonIsPresent(python) {
|
|
1221
|
-
return import_node_fs4.default.existsSync(python.executable);
|
|
1222
|
-
}
|
|
1223
|
-
function environmentIsPresent(environment) {
|
|
1224
|
-
return pythonIsPresent(environment.python) && import_node_fs4.default.existsSync(environment.packageManager.executable);
|
|
1225
|
-
}
|
|
1226
998
|
|
|
1227
999
|
// src/sync-resolver.ts
|
|
1228
1000
|
async function main() {
|
|
1229
1001
|
const request = JSON.parse(import_node_fs5.readFileSync(0, "utf8"));
|
|
1230
|
-
const result = request.target === "
|
|
1231
|
-
uv: await ensureUv(request.options),
|
|
1232
|
-
python: await ensureVirtualEnvironment(request.options)
|
|
1233
|
-
} : await ensureEnvironment(request.options);
|
|
1002
|
+
const result = request.target === "uv" ? await ensureUv(request.options) : await ensureEnvironment(request.options);
|
|
1234
1003
|
process.stdout.write(JSON.stringify(result));
|
|
1235
1004
|
}
|
|
1236
1005
|
main().catch((error) => {
|
|
@@ -1239,4 +1008,4 @@ main().catch((error) => {
|
|
|
1239
1008
|
process.exitCode = 1;
|
|
1240
1009
|
});
|
|
1241
1010
|
|
|
1242
|
-
//# debugId=
|
|
1011
|
+
//# debugId=450BD20294AA111B64756E2164756E21
|