pyfig 0.3.1 → 0.5.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 +116 -123
- package/SECURITY.md +2 -2
- package/dist/cjs/index.js +525 -888
- package/dist/cjs/index.js.map +15 -15
- package/dist/cjs/sync-resolver.js +482 -676
- package/dist/cjs/sync-resolver.js.map +13 -12
- package/dist/cli.js +7 -10
- 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 +9 -18
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +22 -59
- 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 +1 -19
- package/dist/probes.d.ts.map +1 -1
- package/dist/probes.js +13 -74
- 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 +297 -235
- package/dist/provision.js.map +1 -1
- package/dist/python-version.d.ts +10 -0
- package/dist/python-version.d.ts.map +1 -0
- package/dist/python-version.js +17 -0
- package/dist/python-version.js.map +1 -0
- 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 +9 -10
- 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
|
|
77
|
-
var
|
|
76
|
+
var import_promises2 = require("node:stream/promises");
|
|
77
|
+
var import_fflate = require("fflate");
|
|
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;
|
|
@@ -162,16 +105,26 @@ function* ancestors(startDirectory) {
|
|
|
162
105
|
}
|
|
163
106
|
}
|
|
164
107
|
|
|
108
|
+
// src/python-version.ts
|
|
109
|
+
var import_pep440 = require("@renovatebot/pep440");
|
|
110
|
+
function isValidPythonSpecifier(specifier) {
|
|
111
|
+
return import_pep440.validRange(specifier);
|
|
112
|
+
}
|
|
113
|
+
function isValidPythonVersion(version) {
|
|
114
|
+
return import_pep440.valid(version) !== null;
|
|
115
|
+
}
|
|
116
|
+
function pythonVersionSatisfies(version, specifier) {
|
|
117
|
+
return import_pep440.filter([version], specifier).length === 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
165
120
|
// src/config.ts
|
|
166
|
-
var
|
|
167
|
-
var DEFAULT_PIP_RANGE = ">=20.3.0";
|
|
121
|
+
var DEFAULT_REQUIRES_PYTHON = ">=3.10,<4";
|
|
168
122
|
var DEFAULT_MANAGED_PYTHON = "3.12";
|
|
169
123
|
var DEFAULT_UV_VERSION = "0.13.6";
|
|
170
|
-
var DEFAULT_UV_RANGE =
|
|
124
|
+
var DEFAULT_UV_RANGE = ">=0.10.0 <1.0.0";
|
|
171
125
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
172
126
|
var DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60000;
|
|
173
127
|
var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60000;
|
|
174
|
-
var DEFAULT_CACHE_TTL_MS = 30000;
|
|
175
128
|
var envPathsFunction = (() => {
|
|
176
129
|
const imported = envPaths.default;
|
|
177
130
|
const implementation = typeof imported === "function" ? imported : imported.default;
|
|
@@ -181,30 +134,26 @@ var envPathsFunction = (() => {
|
|
|
181
134
|
})();
|
|
182
135
|
function resolveOptions(options = {}) {
|
|
183
136
|
const env = { ...options.env ?? process.env };
|
|
184
|
-
const
|
|
185
|
-
const pipRange = options.pipRange ?? env["PYFIG_PIP_RANGE"] ?? DEFAULT_PIP_RANGE;
|
|
137
|
+
const requiresPython = options.requiresPython ?? env["PYFIG_REQUIRES_PYTHON"] ?? DEFAULT_REQUIRES_PYTHON;
|
|
186
138
|
const uvRange = options.uvRange ?? env["PYFIG_UV_RANGE"] ?? DEFAULT_UV_RANGE;
|
|
187
|
-
|
|
188
|
-
assertRange("pip", pipRange);
|
|
139
|
+
assertPythonSpecifier(requiresPython);
|
|
189
140
|
assertRange("uv", uvRange);
|
|
190
141
|
const managedPython = options.managedPython ?? env["PYFIG_MANAGED_PYTHON"] ?? DEFAULT_MANAGED_PYTHON;
|
|
191
142
|
const uvVersion = options.uvVersion ?? env["PYFIG_UV_VERSION"] ?? DEFAULT_UV_VERSION;
|
|
192
|
-
assertManagedPythonRequest(managedPython,
|
|
143
|
+
assertManagedPythonRequest(managedPython, requiresPython);
|
|
193
144
|
assertUvVersion(uvVersion, uvRange);
|
|
194
145
|
const probeTimeoutMs = resolveDuration("probeTimeoutMs", options.probeTimeoutMs, env["PYFIG_PROBE_TIMEOUT_MS"], DEFAULT_PROBE_TIMEOUT_MS);
|
|
195
146
|
const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
|
|
196
147
|
const downloadTimeoutMs = resolveDuration("downloadTimeoutMs", options.downloadTimeoutMs, env["PYFIG_DOWNLOAD_TIMEOUT_MS"], DEFAULT_DOWNLOAD_TIMEOUT_MS);
|
|
197
148
|
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
149
|
const configuredHome = options.cacheDir ?? env["PYFIG_HOME"];
|
|
200
150
|
const cacheDir = configuredHome ? import_node_path2.default.resolve(configuredHome) : envPathsFunction("pyfig", { suffix: "" }).cache;
|
|
201
151
|
const configuredProjectRoot = options.projectRoot ?? env["PYFIG_PROJECT_ROOT"];
|
|
202
|
-
const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot();
|
|
152
|
+
const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot(env["INIT_CWD"] ?? process.cwd());
|
|
203
153
|
const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
|
|
204
154
|
return {
|
|
205
155
|
python: options.python ?? env["PYFIG_PYTHON"] ?? "",
|
|
206
|
-
|
|
207
|
-
pipRange,
|
|
156
|
+
requiresPython,
|
|
208
157
|
managedPython,
|
|
209
158
|
uvVersion,
|
|
210
159
|
uvRange,
|
|
@@ -220,40 +169,16 @@ function resolveOptions(options = {}) {
|
|
|
220
169
|
installTimeoutMs,
|
|
221
170
|
downloadTimeoutMs,
|
|
222
171
|
coordinationTimeoutMs,
|
|
223
|
-
cacheTtlMs,
|
|
224
172
|
env
|
|
225
173
|
};
|
|
226
174
|
}
|
|
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
175
|
function uvEnvironment(options) {
|
|
253
176
|
return {
|
|
254
177
|
...options.env,
|
|
255
178
|
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")
|
|
179
|
+
UV_CACHE_DIR: import_node_path2.default.join(options.cacheDir, "uv-cache"),
|
|
180
|
+
UV_PYTHON_PREFERENCE: "system",
|
|
181
|
+
UV_NO_CONFIG: "1"
|
|
257
182
|
};
|
|
258
183
|
}
|
|
259
184
|
function assertRange(tool, range) {
|
|
@@ -269,14 +194,19 @@ function assertUvVersion(version, range) {
|
|
|
269
194
|
throw new TypeError(`Pinned uv ${version} does not satisfy supported range ${range}.`);
|
|
270
195
|
}
|
|
271
196
|
}
|
|
272
|
-
function
|
|
197
|
+
function assertPythonSpecifier(specifier) {
|
|
198
|
+
if (!isValidPythonSpecifier(specifier)) {
|
|
199
|
+
throw new TypeError(`Invalid requires-python PEP 440 specifier: ${specifier}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function assertManagedPythonRequest(request, requiresPython) {
|
|
273
203
|
if (!request || /[\\/\0\r\n]/u.test(request) || request.includes("..")) {
|
|
274
204
|
throw new TypeError(`Invalid managed Python request: ${request}`);
|
|
275
205
|
}
|
|
276
206
|
if (/^\d+\.\d+(?:\.\d+)?$/u.test(request)) {
|
|
277
207
|
const normalized = request.split(".").length === 2 ? `${request}.0` : request;
|
|
278
|
-
if (!
|
|
279
|
-
throw new TypeError(`Managed Python ${request} cannot satisfy
|
|
208
|
+
if (!pythonVersionSatisfies(normalized, requiresPython)) {
|
|
209
|
+
throw new TypeError(`Managed Python ${request} cannot satisfy requires-python ${requiresPython}.`);
|
|
280
210
|
}
|
|
281
211
|
}
|
|
282
212
|
}
|
|
@@ -297,34 +227,161 @@ function parseBoolean(value, fallback) {
|
|
|
297
227
|
return false;
|
|
298
228
|
throw new TypeError(`Invalid boolean value: ${value}`);
|
|
299
229
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
230
|
+
|
|
231
|
+
// src/provision-lock.ts
|
|
232
|
+
var import_loqkit = require("loqkit");
|
|
233
|
+
async function withProvisioningLock(options) {
|
|
234
|
+
let existing;
|
|
235
|
+
const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
|
|
236
|
+
key: options.key,
|
|
237
|
+
name: "Pyfig provisioning",
|
|
238
|
+
namespace: "pyfig",
|
|
239
|
+
timeout: options.timeoutMs,
|
|
240
|
+
predicate: async () => {
|
|
241
|
+
const value = await options.check();
|
|
242
|
+
if (value === null)
|
|
243
|
+
return true;
|
|
244
|
+
existing = { value };
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
if (result.context.state === "executed")
|
|
249
|
+
return result.value;
|
|
250
|
+
if (existing)
|
|
251
|
+
return existing.value;
|
|
252
|
+
throw new Error(`Provisioning check for ${options.key} did not produce a resource.`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/virtual-environment.ts
|
|
256
|
+
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
257
|
+
var import_node_path3 = __toESM(require("node:path"), 1);
|
|
258
|
+
var VIRTUAL_ENV_DIRECTORY_NAME = import_node_path3.default.join("node_modules", ".pyfig", "venv");
|
|
259
|
+
function virtualEnvironmentDirectory(projectRoot) {
|
|
260
|
+
return import_node_path3.default.join(projectRoot, VIRTUAL_ENV_DIRECTORY_NAME);
|
|
261
|
+
}
|
|
262
|
+
function externalVirtualEnvironmentDirectories(projectRoot, env) {
|
|
263
|
+
const candidates = [env["VIRTUAL_ENV"], import_node_path3.default.join(projectRoot, ".venv")].filter((value) => Boolean(value)).map((value) => import_node_path3.default.resolve(value));
|
|
264
|
+
return [...new Map(candidates.map((value) => [canonicalPath(value), value])).values()];
|
|
306
265
|
}
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
266
|
+
function virtualEnvironmentPythonExecutable(virtualEnvironment) {
|
|
267
|
+
return import_node_path3.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
|
|
268
|
+
}
|
|
269
|
+
function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
|
|
270
|
+
const activated = { ...env };
|
|
271
|
+
const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
|
|
272
|
+
const existingPath = pathEntry ? activated[pathEntry] : undefined;
|
|
273
|
+
for (const key of Object.keys(activated)) {
|
|
274
|
+
const normalized = key.toUpperCase();
|
|
275
|
+
if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
|
|
276
|
+
delete activated[key];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
activated["PATH"] = [import_node_path3.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path3.default.delimiter);
|
|
280
|
+
activated["VIRTUAL_ENV"] = virtualEnvironment;
|
|
281
|
+
return activated;
|
|
282
|
+
}
|
|
283
|
+
function pathsEqual(left, right) {
|
|
284
|
+
return canonicalPath(left) === canonicalPath(right);
|
|
285
|
+
}
|
|
286
|
+
function canonicalPath(value) {
|
|
287
|
+
const resolved = import_node_path3.default.resolve(value);
|
|
288
|
+
let canonical = resolved;
|
|
289
|
+
try {
|
|
290
|
+
canonical = import_node_fs2.default.realpathSync.native(resolved);
|
|
291
|
+
} catch {}
|
|
292
|
+
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
310
293
|
}
|
|
311
294
|
|
|
312
|
-
// src/
|
|
313
|
-
var
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
295
|
+
// src/package-target.ts
|
|
296
|
+
var TARGET_OPTIONS = new Set([
|
|
297
|
+
"--active",
|
|
298
|
+
"--config-file",
|
|
299
|
+
"--directory",
|
|
300
|
+
"--isolated",
|
|
301
|
+
"--prefix",
|
|
302
|
+
"--project",
|
|
303
|
+
"--python",
|
|
304
|
+
"--root",
|
|
305
|
+
"--system",
|
|
306
|
+
"--target",
|
|
307
|
+
"--user"
|
|
308
|
+
]);
|
|
309
|
+
var TARGET_ENVIRONMENT_VARIABLES = new Set([
|
|
310
|
+
"CONDA_PREFIX",
|
|
311
|
+
"PIP_PREFIX",
|
|
312
|
+
"PIP_PYTHON",
|
|
313
|
+
"PIP_ROOT",
|
|
314
|
+
"PIP_TARGET",
|
|
315
|
+
"PIP_USER",
|
|
316
|
+
"PYTHONHOME",
|
|
317
|
+
"PYTHONUSERBASE",
|
|
318
|
+
"UV_CONFIG_FILE",
|
|
319
|
+
"UV_PROJECT",
|
|
320
|
+
"UV_PROJECT_ENVIRONMENT",
|
|
321
|
+
"UV_PYTHON",
|
|
322
|
+
"UV_PREFIX",
|
|
323
|
+
"UV_ROOT",
|
|
324
|
+
"UV_SYSTEM_PYTHON",
|
|
325
|
+
"UV_TARGET"
|
|
326
|
+
]);
|
|
327
|
+
function uvCommandNeedsEnvironment(args) {
|
|
328
|
+
return args[0] === "pip" || args[0] === "run";
|
|
329
|
+
}
|
|
330
|
+
function targetUvArguments(args, python) {
|
|
331
|
+
if (!python.virtualEnvironment) {
|
|
332
|
+
throw new Error("Targeted uv commands require a virtual environment.");
|
|
333
|
+
}
|
|
334
|
+
assertNoTargetOverride(args);
|
|
335
|
+
if (args[0] === "run") {
|
|
336
|
+
return ["run", "--no-project", "--python", python.executable, ...args.slice(1)];
|
|
337
|
+
}
|
|
338
|
+
if (args[0] === "pip" && args[1]) {
|
|
339
|
+
return ["pip", args[1], "--python", python.executable, ...args.slice(2)];
|
|
340
|
+
}
|
|
341
|
+
return [...args];
|
|
342
|
+
}
|
|
343
|
+
function uvCommandEnvironment(env, python) {
|
|
344
|
+
const sanitized = sanitizeUvEnvironment(env);
|
|
345
|
+
if (!python?.virtualEnvironment)
|
|
346
|
+
return sanitized;
|
|
347
|
+
return {
|
|
348
|
+
...activateVirtualEnvironment(sanitized, python.virtualEnvironment, python.executable),
|
|
349
|
+
UV_PYTHON: python.executable
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function sanitizeUvEnvironment(env) {
|
|
353
|
+
const sanitized = { ...env };
|
|
354
|
+
for (const key of Object.keys(sanitized)) {
|
|
355
|
+
if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
|
|
356
|
+
delete sanitized[key];
|
|
357
|
+
}
|
|
358
|
+
return sanitized;
|
|
359
|
+
}
|
|
360
|
+
function assertNoTargetOverride(args) {
|
|
361
|
+
for (const argument of args.slice(1)) {
|
|
362
|
+
if (argument === "--")
|
|
363
|
+
return;
|
|
364
|
+
const [name] = argument.split("=", 1);
|
|
365
|
+
if (name && TARGET_OPTIONS.has(name)) {
|
|
366
|
+
throw new Error(`${name} cannot override the environment selected by Pyfig.`);
|
|
367
|
+
}
|
|
368
|
+
if (argument === "-p" || argument.startsWith("-p=") || /^-p[^-]/u.test(argument)) {
|
|
369
|
+
throw new Error("-p cannot override the environment selected by Pyfig.");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/probes.ts
|
|
375
|
+
var import_semver2 = __toESM(require("semver"), 1);
|
|
319
376
|
|
|
320
377
|
// src/process.ts
|
|
321
378
|
var import_node_child_process = require("node:child_process");
|
|
322
|
-
var
|
|
379
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
323
380
|
var import_which = __toESM(require("which"), 1);
|
|
324
381
|
async function resolveExecutable(value, env) {
|
|
325
382
|
if (!value)
|
|
326
383
|
return null;
|
|
327
|
-
if (
|
|
384
|
+
if (import_node_path4.default.isAbsolute(value)) {
|
|
328
385
|
return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
|
|
329
386
|
}
|
|
330
387
|
if (!/^[A-Za-z0-9_.-]+$/u.test(value))
|
|
@@ -341,14 +398,15 @@ function run(executable, args, env, timeoutMs) {
|
|
|
341
398
|
});
|
|
342
399
|
if (result.status !== 0 || result.error) {
|
|
343
400
|
const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
|
|
344
|
-
throw new Error(`${
|
|
401
|
+
throw new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
|
|
345
402
|
}
|
|
346
403
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
347
404
|
}
|
|
348
|
-
async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
405
|
+
async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
|
|
349
406
|
return new Promise((resolve, reject) => {
|
|
350
407
|
const child = import_node_child_process.spawn(executable, args, {
|
|
351
408
|
env,
|
|
409
|
+
cwd,
|
|
352
410
|
stdio: ["ignore", "pipe", "pipe"],
|
|
353
411
|
windowsHide: true
|
|
354
412
|
});
|
|
@@ -369,11 +427,11 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
|
369
427
|
const abort = () => {
|
|
370
428
|
child.kill();
|
|
371
429
|
const reason = signal?.reason;
|
|
372
|
-
finish(reason instanceof Error ? reason : new Error(`${
|
|
430
|
+
finish(reason instanceof Error ? reason : new Error(`${import_node_path4.default.basename(executable)} was aborted.`));
|
|
373
431
|
};
|
|
374
432
|
const timer = setTimeout(() => {
|
|
375
433
|
child.kill();
|
|
376
|
-
finish(new Error(`${
|
|
434
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} timed out after ${timeoutMs}ms.`));
|
|
377
435
|
}, timeoutMs);
|
|
378
436
|
child.stdout.setEncoding("utf8");
|
|
379
437
|
child.stderr.setEncoding("utf8");
|
|
@@ -381,21 +439,21 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
|
381
439
|
stdout += chunk;
|
|
382
440
|
if (stdout.length > 4 * 1024 * 1024) {
|
|
383
441
|
child.kill();
|
|
384
|
-
finish(new Error(`${
|
|
442
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the output limit.`));
|
|
385
443
|
}
|
|
386
444
|
});
|
|
387
445
|
child.stderr.on("data", (chunk) => {
|
|
388
446
|
stderr += chunk;
|
|
389
447
|
if (stderr.length > 4 * 1024 * 1024) {
|
|
390
448
|
child.kill();
|
|
391
|
-
finish(new Error(`${
|
|
449
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the error-output limit.`));
|
|
392
450
|
}
|
|
393
451
|
});
|
|
394
452
|
child.once("error", (error) => finish(error));
|
|
395
453
|
child.once("close", (code, childSignal) => {
|
|
396
454
|
if (code !== 0) {
|
|
397
455
|
const detail = stderr.trim() || (childSignal ? `signal ${childSignal}` : `exit ${code}`);
|
|
398
|
-
finish(new Error(`${
|
|
456
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
|
|
399
457
|
return;
|
|
400
458
|
}
|
|
401
459
|
finish(undefined, { stdout, stderr });
|
|
@@ -407,213 +465,6 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
|
407
465
|
});
|
|
408
466
|
}
|
|
409
467
|
|
|
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
468
|
// src/probes.ts
|
|
618
469
|
var PYTHON_PROBE = `
|
|
619
470
|
import json
|
|
@@ -663,7 +514,15 @@ if os.path.isfile(header_path) and library_path:
|
|
|
663
514
|
}
|
|
664
515
|
|
|
665
516
|
release_level = sys.version_info.releaselevel
|
|
666
|
-
|
|
517
|
+
release_suffixes = {
|
|
518
|
+
"alpha": "a",
|
|
519
|
+
"beta": "b",
|
|
520
|
+
"candidate": "rc",
|
|
521
|
+
"final": "",
|
|
522
|
+
}
|
|
523
|
+
release_suffix = release_suffixes[release_level]
|
|
524
|
+
if release_suffix:
|
|
525
|
+
release_suffix += str(sys.version_info.serial)
|
|
667
526
|
|
|
668
527
|
print(json.dumps({
|
|
669
528
|
"executable": sys.executable,
|
|
@@ -683,24 +542,8 @@ print(json.dumps({
|
|
|
683
542
|
"embedding": embedding,
|
|
684
543
|
}))
|
|
685
544
|
`;
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
function pythonSatisfiesRequirements(python, options) {
|
|
703
|
-
return import_semver2.default.satisfies(python.version, options.pythonRange) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
|
|
545
|
+
function pythonSatisfiesPolicy(python, options) {
|
|
546
|
+
return pythonVersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
|
|
704
547
|
}
|
|
705
548
|
async function probePython(candidate, options) {
|
|
706
549
|
const executable = await resolveExecutable(candidate.executable, options.env);
|
|
@@ -709,55 +552,11 @@ async function probePython(candidate, options) {
|
|
|
709
552
|
try {
|
|
710
553
|
const { stdout } = run(executable, [...candidate.args, "-c", PYTHON_PROBE], options.env, options.probeTimeoutMs);
|
|
711
554
|
const info = JSON.parse(stdout.trim());
|
|
712
|
-
return
|
|
713
|
-
} catch {
|
|
714
|
-
return null;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
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
|
-
};
|
|
555
|
+
return isValidPythonVersion(info.version) ? info : null;
|
|
738
556
|
} catch {
|
|
739
557
|
return null;
|
|
740
558
|
}
|
|
741
559
|
}
|
|
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
560
|
async function findUv(options) {
|
|
762
561
|
const configured = options.env["PYFIG_UV"] ?? "uv";
|
|
763
562
|
const executable = await resolveExecutable(configured, options.env);
|
|
@@ -778,9 +577,6 @@ function normalizeVersion(value) {
|
|
|
778
577
|
return null;
|
|
779
578
|
return import_semver2.default.valid(value) ?? import_semver2.default.coerce(value)?.version ?? null;
|
|
780
579
|
}
|
|
781
|
-
function normalizePathForComparison(value) {
|
|
782
|
-
return process.platform === "win32" ? value.toLowerCase() : value;
|
|
783
|
-
}
|
|
784
580
|
function pythonArchitectureMatchesRuntime(python) {
|
|
785
581
|
const architecture = python.architecture.toLowerCase();
|
|
786
582
|
if (python.pointerBits !== (process.arch === "ia32" ? 32 : 64))
|
|
@@ -795,228 +591,223 @@ function pythonArchitectureMatchesRuntime(python) {
|
|
|
795
591
|
return architecture === process.arch;
|
|
796
592
|
}
|
|
797
593
|
|
|
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
594
|
// 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
595
|
async function ensureUv(options = {}) {
|
|
859
|
-
|
|
860
|
-
return memoizeAsync("uv", configurationFingerprint(resolved), resolved.cacheTtlMs, (uv) => import_node_fs4.default.existsSync(uv.executable), () => ensureUvResolved(resolved));
|
|
596
|
+
return ensureUvResolved(resolveOptions(options));
|
|
861
597
|
}
|
|
862
|
-
async function ensureEnvironment(options = {}) {
|
|
598
|
+
async function ensureEnvironment(options = {}, dependencies = [], dependencyOptions = {}) {
|
|
863
599
|
const resolved = resolveOptions(options);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
600
|
+
const normalized = normalizeDependencies(dependencies);
|
|
601
|
+
const selections = normalizeDependencyOptions(dependencyOptions);
|
|
602
|
+
const uv = await ensureUvResolved(resolved);
|
|
603
|
+
const python = await ensureVirtualEnvironmentResolved(resolved, uv);
|
|
604
|
+
if (normalized.length > 0 || selections.extras.length > 0 || selections.groups.length > 0 || selections.allExtras) {
|
|
605
|
+
await installRuntimeDependencies(uv, python, normalized, selections, resolved);
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
python,
|
|
609
|
+
uv,
|
|
610
|
+
managed: isManagedEnvironment(python, resolved)
|
|
611
|
+
};
|
|
869
612
|
}
|
|
870
|
-
async function ensureVirtualEnvironmentResolved(options) {
|
|
613
|
+
async function ensureVirtualEnvironmentResolved(options, uv) {
|
|
614
|
+
const existing = await findReusableEnvironment(options);
|
|
615
|
+
if (existing)
|
|
616
|
+
return existing;
|
|
871
617
|
const directory = virtualEnvironmentDirectory(options.projectRoot);
|
|
872
618
|
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);
|
|
619
|
+
const candidate = await inspectVirtualEnvironment(directory, options);
|
|
620
|
+
return candidate && pythonSatisfiesPolicy(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
|
|
880
621
|
};
|
|
881
|
-
const existing = await check();
|
|
882
|
-
if (existing)
|
|
883
|
-
return existing;
|
|
884
622
|
if (!options.createVirtualEnvironment) {
|
|
885
|
-
throw new Error(`No
|
|
623
|
+
throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
|
|
886
624
|
}
|
|
887
|
-
const basePython = await
|
|
888
|
-
const preferredUv = await findAvailableUv(options) ?? (options.installUv ? await ensureUvResolved(options) : null);
|
|
625
|
+
const basePython = await ensureBasePythonResolved(uv, options);
|
|
889
626
|
return withProvisioningLock({
|
|
890
627
|
key: `venv:${canonicalPath(directory)}`,
|
|
891
|
-
target: directory,
|
|
892
628
|
timeoutMs: options.coordinationTimeoutMs,
|
|
893
|
-
env: options.env,
|
|
894
629
|
check,
|
|
895
|
-
mutate: async (
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
630
|
+
mutate: async () => {
|
|
631
|
+
await import_promises.rm(directory, { recursive: true, force: true });
|
|
632
|
+
await import_promises.mkdir(import_node_path5.default.dirname(directory), { recursive: true });
|
|
633
|
+
await runAsync(uv.executable, [
|
|
634
|
+
"venv",
|
|
635
|
+
"--no-project",
|
|
636
|
+
"--no-config",
|
|
637
|
+
"--no-python-downloads",
|
|
638
|
+
"--python",
|
|
639
|
+
basePython.executable,
|
|
640
|
+
directory
|
|
641
|
+
], isolatedUvEnvironment(options), options.installTimeoutMs);
|
|
642
|
+
const installed = await check();
|
|
643
|
+
if (!installed) {
|
|
644
|
+
throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
|
|
905
645
|
}
|
|
906
646
|
return installed;
|
|
907
647
|
}
|
|
908
648
|
});
|
|
909
649
|
}
|
|
910
|
-
async function
|
|
911
|
-
if (
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
650
|
+
async function findReusableEnvironment(options) {
|
|
651
|
+
if (options.python) {
|
|
652
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
653
|
+
if (!configured || !pythonSatisfiesPolicy(configured, options)) {
|
|
654
|
+
throw incompatibleConfiguredPython(options);
|
|
655
|
+
}
|
|
656
|
+
if (configured.virtualEnvironment)
|
|
657
|
+
return configured;
|
|
916
658
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
659
|
+
for (const directory of [
|
|
660
|
+
...externalVirtualEnvironmentDirectories(options.projectRoot, options.env),
|
|
661
|
+
virtualEnvironmentDirectory(options.projectRoot)
|
|
662
|
+
]) {
|
|
663
|
+
const info = await inspectVirtualEnvironment(directory, options);
|
|
664
|
+
if (info && pythonSatisfiesPolicy(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
|
|
665
|
+
return info;
|
|
666
|
+
}
|
|
923
667
|
}
|
|
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;
|
|
668
|
+
return null;
|
|
942
669
|
}
|
|
943
|
-
async function
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
670
|
+
async function ensureBasePythonResolved(uv, options) {
|
|
671
|
+
if (options.python) {
|
|
672
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
673
|
+
if (!configured || !pythonSatisfiesPolicy(configured, options)) {
|
|
674
|
+
throw incompatibleConfiguredPython(options);
|
|
675
|
+
}
|
|
676
|
+
if (!configured.virtualEnvironment)
|
|
677
|
+
return configured;
|
|
678
|
+
const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
|
|
679
|
+
if (base && pythonSatisfiesPolicy(base, options))
|
|
680
|
+
return base;
|
|
681
|
+
throw incompatibleConfiguredPython(options);
|
|
682
|
+
}
|
|
683
|
+
const system = await findPythonWithUv(uv, options, "system");
|
|
684
|
+
if (system)
|
|
685
|
+
return system;
|
|
686
|
+
const cachedManaged = await findPythonWithUv(uv, options, "managed");
|
|
687
|
+
if (cachedManaged)
|
|
688
|
+
return cachedManaged;
|
|
947
689
|
if (!options.installPython) {
|
|
948
|
-
|
|
949
|
-
throw new Error(`No Python interpreter satisfies ${options.pythonRange}${capability}; ` + "managed Python installation is disabled.");
|
|
690
|
+
throw new Error(missingPythonMessage(options, "managed Python installation is disabled"));
|
|
950
691
|
}
|
|
951
|
-
const lockTarget = cacheLockTarget(options.cacheDir, "python");
|
|
952
692
|
return withProvisioningLock({
|
|
953
|
-
key: `python:${canonicalPath(
|
|
954
|
-
target: lockTarget,
|
|
693
|
+
key: `python:${canonicalPath(import_node_path5.default.join(options.cacheDir, "python"))}`,
|
|
955
694
|
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);
|
|
695
|
+
check: () => findPythonWithUv(uv, options, "managed"),
|
|
696
|
+
mutate: async () => {
|
|
697
|
+
await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], isolatedUvEnvironment(options), options.installTimeoutMs);
|
|
698
|
+
const installed = await findPythonWithUv(uv, options, "managed");
|
|
963
699
|
if (!installed) {
|
|
964
|
-
throw new Error(`uv installed
|
|
700
|
+
throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
|
|
965
701
|
}
|
|
966
702
|
return installed;
|
|
967
703
|
}
|
|
968
704
|
});
|
|
969
705
|
}
|
|
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) {
|
|
706
|
+
async function findPythonWithUv(uv, options, source) {
|
|
707
|
+
for (const request of ["", options.managedPython]) {
|
|
988
708
|
const args = [
|
|
989
709
|
"python",
|
|
990
710
|
"find",
|
|
991
711
|
"--no-project",
|
|
992
712
|
"--no-config",
|
|
993
|
-
"--
|
|
994
|
-
"--
|
|
713
|
+
"--no-python-downloads",
|
|
714
|
+
source === "system" ? "--system" : "--managed-python"
|
|
995
715
|
];
|
|
996
|
-
if (
|
|
997
|
-
args.push(
|
|
716
|
+
if (request)
|
|
717
|
+
args.push(request);
|
|
998
718
|
try {
|
|
999
|
-
const { stdout } = await runAsync(uv.executable, args,
|
|
719
|
+
const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
|
|
1000
720
|
const info = await probePython({ executable: stdout.trim(), args: [] }, options);
|
|
1001
|
-
if (info &&
|
|
721
|
+
if (info && pythonSatisfiesPolicy(info, options))
|
|
1002
722
|
return info;
|
|
1003
|
-
} catch
|
|
1004
|
-
if (signal?.aborted)
|
|
1005
|
-
throw signal.reason ?? error;
|
|
1006
|
-
}
|
|
723
|
+
} catch {}
|
|
1007
724
|
}
|
|
725
|
+
try {
|
|
726
|
+
const { stdout } = await runAsync(uv.executable, [
|
|
727
|
+
"python",
|
|
728
|
+
"list",
|
|
729
|
+
"--no-config",
|
|
730
|
+
"--no-python-downloads",
|
|
731
|
+
"--only-installed",
|
|
732
|
+
"--output-format",
|
|
733
|
+
"json",
|
|
734
|
+
source === "system" ? "--no-managed-python" : "--managed-python"
|
|
735
|
+
], explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
|
|
736
|
+
const candidates = installedPythonPaths(stdout);
|
|
737
|
+
for (const executable of candidates) {
|
|
738
|
+
const info = await probePython({ executable, args: [] }, options);
|
|
739
|
+
if (info && !info.virtualEnvironment && pythonSatisfiesPolicy(info, options)) {
|
|
740
|
+
return info;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
} catch {}
|
|
1008
744
|
return null;
|
|
1009
745
|
}
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
if (
|
|
1013
|
-
return
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
return
|
|
1019
|
-
|
|
746
|
+
function installedPythonPaths(output) {
|
|
747
|
+
const parsed = JSON.parse(output);
|
|
748
|
+
if (!Array.isArray(parsed))
|
|
749
|
+
return [];
|
|
750
|
+
const paths = parsed.flatMap((entry) => {
|
|
751
|
+
if (!entry || typeof entry !== "object" || !("path" in entry))
|
|
752
|
+
return [];
|
|
753
|
+
const candidate = entry.path;
|
|
754
|
+
return typeof candidate === "string" && candidate ? [candidate] : [];
|
|
755
|
+
});
|
|
756
|
+
return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
|
|
757
|
+
}
|
|
758
|
+
async function inspectVirtualEnvironment(directory, options) {
|
|
759
|
+
const executable = virtualEnvironmentPythonExecutable(directory);
|
|
760
|
+
const info = await probePython({ executable, args: [] }, options);
|
|
761
|
+
return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
|
|
762
|
+
}
|
|
763
|
+
async function installRuntimeDependencies(uv, python, dependencies, dependencyOptions, options) {
|
|
764
|
+
if (!python.virtualEnvironment) {
|
|
765
|
+
throw new Error("Runtime dependency installation requires a virtual environment.");
|
|
766
|
+
}
|
|
767
|
+
await withProvisioningLock({
|
|
768
|
+
key: `dependencies:${canonicalPath(python.virtualEnvironment)}`,
|
|
769
|
+
timeoutMs: options.coordinationTimeoutMs,
|
|
770
|
+
check: async () => null,
|
|
771
|
+
mutate: async () => {
|
|
772
|
+
const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-dependencies-"));
|
|
773
|
+
try {
|
|
774
|
+
const additions = import_node_path5.default.join(temporary, "runtime-dependencies.txt");
|
|
775
|
+
const resolved = import_node_path5.default.join(temporary, "resolved-dependencies.txt");
|
|
776
|
+
const pyproject = import_node_path5.default.join(options.projectRoot, "pyproject.toml");
|
|
777
|
+
const hasPyproject = import_node_fs4.default.existsSync(pyproject);
|
|
778
|
+
if (!hasPyproject && (dependencyOptions.extras.length > 0 || dependencyOptions.groups.length > 0 || dependencyOptions.allExtras)) {
|
|
779
|
+
throw new Error("Runtime extras and groups require an existing pyproject.toml.");
|
|
780
|
+
}
|
|
781
|
+
const sources = hasPyproject ? [pyproject] : [];
|
|
782
|
+
if (dependencies.length > 0) {
|
|
783
|
+
await import_promises.writeFile(additions, `${dependencies.join(`
|
|
784
|
+
`)}
|
|
785
|
+
`, "utf8");
|
|
786
|
+
sources.push(additions);
|
|
787
|
+
}
|
|
788
|
+
const selectionArgs = [
|
|
789
|
+
...dependencyOptions.extras.flatMap((extra) => ["--extra", extra]),
|
|
790
|
+
...dependencyOptions.groups.flatMap((group) => ["--group", group]),
|
|
791
|
+
...dependencyOptions.allExtras ? ["--all-extras"] : []
|
|
792
|
+
];
|
|
793
|
+
await runAsync(uv.executable, [
|
|
794
|
+
"pip",
|
|
795
|
+
"compile",
|
|
796
|
+
"--no-config",
|
|
797
|
+
"--python",
|
|
798
|
+
python.executable,
|
|
799
|
+
...selectionArgs,
|
|
800
|
+
...sources,
|
|
801
|
+
"--output-file",
|
|
802
|
+
resolved
|
|
803
|
+
], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
804
|
+
await runAsync(uv.executable, ["pip", "install", "--no-config", "--python", python.executable, "-r", resolved], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
805
|
+
} finally {
|
|
806
|
+
await import_promises.rm(temporary, { recursive: true, force: true });
|
|
807
|
+
}
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
});
|
|
1020
811
|
}
|
|
1021
812
|
async function ensureUvResolved(options) {
|
|
1022
813
|
const existing = await findAvailableUv(options);
|
|
@@ -1027,15 +818,12 @@ async function ensureUvResolved(options) {
|
|
|
1027
818
|
}
|
|
1028
819
|
const installDir = uvInstallDirectory(options);
|
|
1029
820
|
const executable = uvExecutable(options);
|
|
1030
|
-
const lockTarget = cacheLockTarget(options.cacheDir, "uv");
|
|
1031
821
|
return withProvisioningLock({
|
|
1032
|
-
key: `uv:${canonicalPath(
|
|
1033
|
-
target: lockTarget,
|
|
822
|
+
key: `uv:${canonicalPath(installDir)}`,
|
|
1034
823
|
timeoutMs: options.coordinationTimeoutMs,
|
|
1035
|
-
env: options.env,
|
|
1036
824
|
check: () => findAvailableUv(options),
|
|
1037
|
-
mutate: async (
|
|
1038
|
-
await installUv(installDir, options
|
|
825
|
+
mutate: async () => {
|
|
826
|
+
await installUv(installDir, options);
|
|
1039
827
|
const result = probeUv(executable, options);
|
|
1040
828
|
if (!result || result.version !== options.uvVersion) {
|
|
1041
829
|
throw new Error(`Expected managed uv ${options.uvVersion}, but the installed executable did not match.`);
|
|
@@ -1045,88 +833,134 @@ async function ensureUvResolved(options) {
|
|
|
1045
833
|
});
|
|
1046
834
|
}
|
|
1047
835
|
async function findAvailableUv(options) {
|
|
1048
|
-
const
|
|
1049
|
-
if (
|
|
1050
|
-
return
|
|
836
|
+
const external = await findUv(options);
|
|
837
|
+
if (external)
|
|
838
|
+
return external;
|
|
1051
839
|
const managed = probeUv(uvExecutable(options), options);
|
|
1052
840
|
return managed?.version === options.uvVersion ? managed : null;
|
|
1053
841
|
}
|
|
1054
|
-
function
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
842
|
+
function normalizeDependencies(dependencies) {
|
|
843
|
+
const normalized = dependencies.map((dependency) => dependency.trim());
|
|
844
|
+
for (const dependency of normalized) {
|
|
845
|
+
if (!dependency || /[\0\r\n]/u.test(dependency)) {
|
|
846
|
+
throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return [...new Set(normalized)];
|
|
1059
850
|
}
|
|
1060
|
-
function
|
|
1061
|
-
|
|
1062
|
-
|
|
851
|
+
function normalizeDependencyOptions(options) {
|
|
852
|
+
const extras = normalizeNames("extra", options.extras ?? []);
|
|
853
|
+
const groups = normalizeNames("dependency group", options.groups ?? []);
|
|
854
|
+
if (options.allExtras && extras.length > 0) {
|
|
855
|
+
throw new TypeError("allExtras cannot be combined with explicit extras.");
|
|
1063
856
|
}
|
|
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
|
-
};
|
|
857
|
+
return { extras, groups, allExtras: options.allExtras ?? false };
|
|
1072
858
|
}
|
|
1073
|
-
function
|
|
1074
|
-
const
|
|
1075
|
-
for (const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
delete result[key];
|
|
859
|
+
function normalizeNames(kind, names) {
|
|
860
|
+
const normalized = names.map((name) => name.trim());
|
|
861
|
+
for (const name of normalized) {
|
|
862
|
+
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
|
|
863
|
+
throw new TypeError(`Invalid ${kind}: ${JSON.stringify(name)}`);
|
|
1079
864
|
}
|
|
1080
865
|
}
|
|
1081
|
-
return
|
|
866
|
+
return [...new Set(normalized)];
|
|
867
|
+
}
|
|
868
|
+
function isManagedEnvironment(python, options) {
|
|
869
|
+
return Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(options.projectRoot));
|
|
1082
870
|
}
|
|
1083
|
-
|
|
871
|
+
function isolatedUvEnvironment(options) {
|
|
872
|
+
return sanitizeUvEnvironment(uvEnvironment(options));
|
|
873
|
+
}
|
|
874
|
+
function explicitPythonSourceEnvironment(options) {
|
|
875
|
+
const env = isolatedUvEnvironment(options);
|
|
876
|
+
delete env["UV_PYTHON_PREFERENCE"];
|
|
877
|
+
return env;
|
|
878
|
+
}
|
|
879
|
+
function targetUvEnvironment(options, python) {
|
|
880
|
+
return uvCommandEnvironment(uvEnvironment(options), python);
|
|
881
|
+
}
|
|
882
|
+
function incompatibleConfiguredPython(options) {
|
|
883
|
+
return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `requires-python ${options.requiresPython}` + `${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
|
|
884
|
+
}
|
|
885
|
+
function missingPythonMessage(options, reason) {
|
|
886
|
+
const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
|
|
887
|
+
return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
|
|
888
|
+
}
|
|
889
|
+
async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
|
|
890
|
+
if (!options.python)
|
|
891
|
+
return true;
|
|
892
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
893
|
+
if (!configured || !pythonSatisfiesPolicy(configured, options))
|
|
894
|
+
return false;
|
|
895
|
+
if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
|
|
899
|
+
return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
|
|
900
|
+
}
|
|
901
|
+
function uvInstallDirectory(options) {
|
|
902
|
+
return import_node_path5.default.join(options.cacheDir, "tools", "uv", options.uvVersion);
|
|
903
|
+
}
|
|
904
|
+
function uvExecutable(options) {
|
|
905
|
+
return import_node_path5.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
|
|
906
|
+
}
|
|
907
|
+
async function installUv(installDir, options) {
|
|
1084
908
|
const asset = uvAsset();
|
|
1085
909
|
const baseUrl = `https://github.com/astral-sh/uv/releases/download/${options.uvVersion}`;
|
|
1086
|
-
const temporary = await
|
|
910
|
+
const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
|
|
1087
911
|
const dispatcher = proxyDispatcher(options.env);
|
|
1088
912
|
try {
|
|
1089
|
-
const archive =
|
|
1090
|
-
await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs,
|
|
1091
|
-
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs,
|
|
913
|
+
const archive = import_node_path5.default.join(temporary, asset);
|
|
914
|
+
await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
|
|
915
|
+
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
|
|
1092
916
|
const actual = await sha256File(archive);
|
|
1093
917
|
if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
|
|
1094
918
|
throw new Error(`uv archive checksum mismatch for ${asset}`);
|
|
1095
919
|
}
|
|
1096
|
-
const extracted =
|
|
1097
|
-
await
|
|
1098
|
-
|
|
1099
|
-
if (asset.endsWith(".zip"))
|
|
1100
|
-
await import_extract_zip.default(archive, { dir: extracted });
|
|
1101
|
-
else
|
|
1102
|
-
await tar.x({ file: archive, cwd: extracted, strict: true });
|
|
1103
|
-
signal.throwIfAborted();
|
|
1104
|
-
const source = await findFile(extracted, process.platform === "win32" ? "uv.exe" : "uv");
|
|
920
|
+
const extracted = import_node_path5.default.join(temporary, "extracted");
|
|
921
|
+
await import_promises.mkdir(extracted, { recursive: true });
|
|
922
|
+
const source = await extractUvExecutable(archive, asset, extracted);
|
|
1105
923
|
if (!source)
|
|
1106
924
|
throw new Error(`uv archive did not contain the expected executable: ${asset}`);
|
|
1107
925
|
await publishUvAtomically(source, installDir, options);
|
|
1108
926
|
} finally {
|
|
1109
927
|
await dispatcher?.close();
|
|
1110
|
-
await
|
|
928
|
+
await import_promises.rm(temporary, { recursive: true, force: true });
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async function extractUvExecutable(archive, asset, destination) {
|
|
932
|
+
const executableName = process.platform === "win32" ? "uv.exe" : "uv";
|
|
933
|
+
if (!asset.endsWith(".zip")) {
|
|
934
|
+
await tar.x({ file: archive, cwd: destination, strict: true });
|
|
935
|
+
return findFile(destination, executableName);
|
|
1111
936
|
}
|
|
937
|
+
const entries = import_fflate.unzipSync(await import_promises.readFile(archive), {
|
|
938
|
+
filter: ({ name }) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName
|
|
939
|
+
});
|
|
940
|
+
const matches = Object.entries(entries).filter(([name]) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
|
|
941
|
+
if (matches.length !== 1 || !matches[0])
|
|
942
|
+
return null;
|
|
943
|
+
const source = import_node_path5.default.join(destination, executableName);
|
|
944
|
+
await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
|
|
945
|
+
return source;
|
|
1112
946
|
}
|
|
1113
947
|
async function publishUvAtomically(source, installDir, options) {
|
|
1114
|
-
await
|
|
948
|
+
await import_promises.mkdir(installDir, { recursive: true });
|
|
1115
949
|
const executable = uvExecutable(options);
|
|
1116
|
-
const staged =
|
|
950
|
+
const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
|
|
1117
951
|
try {
|
|
1118
|
-
await
|
|
952
|
+
await import_promises.copyFile(source, staged);
|
|
1119
953
|
if (process.platform !== "win32")
|
|
1120
|
-
await
|
|
954
|
+
await import_promises.chmod(staged, 493);
|
|
1121
955
|
const stagedUv = probeUv(staged, options);
|
|
1122
956
|
if (!stagedUv || stagedUv.version !== options.uvVersion) {
|
|
1123
957
|
throw new Error(`Downloaded uv executable is not version ${options.uvVersion}.`);
|
|
1124
958
|
}
|
|
1125
959
|
if (process.platform === "win32")
|
|
1126
|
-
await
|
|
1127
|
-
await
|
|
960
|
+
await import_promises.rm(executable, { force: true });
|
|
961
|
+
await import_promises.rename(staged, executable);
|
|
1128
962
|
} finally {
|
|
1129
|
-
await
|
|
963
|
+
await import_promises.rm(staged, { force: true });
|
|
1130
964
|
}
|
|
1131
965
|
}
|
|
1132
966
|
function uvAsset() {
|
|
@@ -1141,23 +975,23 @@ function uvAsset() {
|
|
|
1141
975
|
return `uv-${architecture}-unknown-linux-musl.tar.gz`;
|
|
1142
976
|
throw new Error(`uv bootstrap does not support platform ${process.platform}.`);
|
|
1143
977
|
}
|
|
1144
|
-
async function download(url, destination, timeoutMs,
|
|
978
|
+
async function download(url, destination, timeoutMs, dispatcher) {
|
|
1145
979
|
const response = await import_undici.fetch(url, {
|
|
1146
980
|
dispatcher,
|
|
1147
981
|
redirect: "follow",
|
|
1148
|
-
signal:
|
|
982
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1149
983
|
});
|
|
1150
984
|
if (!response.ok || !response.body) {
|
|
1151
985
|
await response.body?.cancel();
|
|
1152
986
|
throw new Error(`Could not download ${url}: HTTP ${response.status}`);
|
|
1153
987
|
}
|
|
1154
|
-
await
|
|
988
|
+
await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
|
|
1155
989
|
}
|
|
1156
|
-
async function downloadText(url, timeoutMs,
|
|
990
|
+
async function downloadText(url, timeoutMs, dispatcher) {
|
|
1157
991
|
const response = await import_undici.fetch(url, {
|
|
1158
992
|
dispatcher,
|
|
1159
993
|
redirect: "follow",
|
|
1160
|
-
signal:
|
|
994
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1161
995
|
});
|
|
1162
996
|
if (!response.ok) {
|
|
1163
997
|
await response.body?.cancel();
|
|
@@ -1179,18 +1013,15 @@ function proxyDispatcher(env) {
|
|
|
1179
1013
|
function environmentValue(env, lowercase, uppercase) {
|
|
1180
1014
|
return env[lowercase] ?? env[uppercase];
|
|
1181
1015
|
}
|
|
1182
|
-
function combinedSignal(timeoutMs, externalSignal) {
|
|
1183
|
-
return AbortSignal.any([externalSignal, AbortSignal.timeout(timeoutMs)]);
|
|
1184
|
-
}
|
|
1185
1016
|
async function sha256File(file) {
|
|
1186
|
-
const hash =
|
|
1017
|
+
const hash = import_node_crypto.createHash("sha256");
|
|
1187
1018
|
for await (const chunk of import_node_fs3.createReadStream(file))
|
|
1188
1019
|
hash.update(chunk);
|
|
1189
1020
|
return hash.digest("hex");
|
|
1190
1021
|
}
|
|
1191
1022
|
async function findFile(directory, name) {
|
|
1192
|
-
for (const entry of await
|
|
1193
|
-
const target =
|
|
1023
|
+
for (const entry of await import_promises.readdir(directory, { withFileTypes: true })) {
|
|
1024
|
+
const target = import_node_path5.default.join(directory, entry.name);
|
|
1194
1025
|
if (entry.isFile() && entry.name === name)
|
|
1195
1026
|
return target;
|
|
1196
1027
|
if (entry.isDirectory()) {
|
|
@@ -1201,36 +1032,11 @@ async function findFile(directory, name) {
|
|
|
1201
1032
|
}
|
|
1202
1033
|
return null;
|
|
1203
1034
|
}
|
|
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
1035
|
|
|
1227
1036
|
// src/sync-resolver.ts
|
|
1228
1037
|
async function main() {
|
|
1229
1038
|
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);
|
|
1039
|
+
const result = request.target === "uv" ? await ensureUv(request.options) : await ensureEnvironment(request.options);
|
|
1234
1040
|
process.stdout.write(JSON.stringify(result));
|
|
1235
1041
|
}
|
|
1236
1042
|
main().catch((error) => {
|
|
@@ -1239,4 +1045,4 @@ main().catch((error) => {
|
|
|
1239
1045
|
process.exitCode = 1;
|
|
1240
1046
|
});
|
|
1241
1047
|
|
|
1242
|
-
//# debugId=
|
|
1048
|
+
//# debugId=8D6E1E9C1BFA227064756E2164756E21
|