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
package/dist/cjs/index.js
CHANGED
|
@@ -66,19 +66,12 @@ var __export = (target, all) => {
|
|
|
66
66
|
var exports_src = {};
|
|
67
67
|
__export(exports_src, {
|
|
68
68
|
DEFAULT_MANAGED_PYTHON: () => DEFAULT_MANAGED_PYTHON,
|
|
69
|
-
|
|
70
|
-
DEFAULT_PYTHON_RANGE: () => DEFAULT_PYTHON_RANGE,
|
|
69
|
+
DEFAULT_REQUIRES_PYTHON: () => DEFAULT_REQUIRES_PYTHON,
|
|
71
70
|
DEFAULT_UV_RANGE: () => DEFAULT_UV_RANGE,
|
|
72
71
|
DEFAULT_UV_VERSION: () => DEFAULT_UV_VERSION,
|
|
73
|
-
clearPyfigCaches: () => clearPyfigCaches,
|
|
74
72
|
createPyfig: () => createPyfig,
|
|
75
73
|
ensureEnvironment: () => ensureEnvironment,
|
|
76
74
|
ensureUv: () => ensureUv,
|
|
77
|
-
ensureVirtualEnvironment: () => ensureVirtualEnvironment,
|
|
78
|
-
pipSpawn: () => pipSpawn,
|
|
79
|
-
pipSpawnSync: () => pipSpawnSync,
|
|
80
|
-
pythonSpawn: () => pythonSpawn,
|
|
81
|
-
pythonSpawnSync: () => pythonSpawnSync,
|
|
82
75
|
resolveProjectRoot: () => resolveProjectRoot,
|
|
83
76
|
uvSpawn: () => uvSpawn,
|
|
84
77
|
uvSpawnSync: () => uvSpawnSync
|
|
@@ -86,7 +79,6 @@ __export(exports_src, {
|
|
|
86
79
|
module.exports = __toCommonJS(exports_src);
|
|
87
80
|
|
|
88
81
|
// src/config.ts
|
|
89
|
-
var import_node_crypto = require("node:crypto");
|
|
90
82
|
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
91
83
|
var envPaths = __toESM(require("env-paths"), 1);
|
|
92
84
|
var import_semver = __toESM(require("semver"), 1);
|
|
@@ -94,69 +86,13 @@ var import_semver = __toESM(require("semver"), 1);
|
|
|
94
86
|
// src/project-root.ts
|
|
95
87
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
96
88
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
97
|
-
var import_pacwich = require("pacwich");
|
|
98
|
-
var PACKAGE_MANAGER_LOCKFILES = [
|
|
99
|
-
"bun.lock",
|
|
100
|
-
"bun.lockb",
|
|
101
|
-
"npm-shrinkwrap.json",
|
|
102
|
-
"package-lock.json",
|
|
103
|
-
"pnpm-lock.yaml",
|
|
104
|
-
"yarn.lock"
|
|
105
|
-
];
|
|
106
|
-
var PYTHON_PROJECT_MARKERS = [
|
|
107
|
-
"pyproject.toml",
|
|
108
|
-
"uv.lock"
|
|
109
|
-
];
|
|
110
89
|
function resolveProjectRoot(startDirectory = process.cwd()) {
|
|
111
90
|
const start = import_node_path.default.resolve(startDirectory);
|
|
112
|
-
const
|
|
113
|
-
if (workspaceRoot) {
|
|
114
|
-
try {
|
|
115
|
-
return import_node_path.default.resolve(import_pacwich.createFileSystemProject({
|
|
116
|
-
rootDirectory: start,
|
|
117
|
-
disableExecutableConfigs: true
|
|
118
|
-
}).rootDirectory);
|
|
119
|
-
} catch {
|
|
120
|
-
return workspaceRoot;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return findConventionalProjectRoot(start);
|
|
124
|
-
}
|
|
125
|
-
function findWorkspaceProjectRoot(startDirectory) {
|
|
126
|
-
for (const directory of ancestors(startDirectory)) {
|
|
127
|
-
const packageJson = import_node_path.default.join(directory, "package.json");
|
|
128
|
-
if (!import_node_fs.default.existsSync(packageJson))
|
|
129
|
-
continue;
|
|
130
|
-
if (import_node_fs.default.existsSync(import_node_path.default.join(directory, "pnpm-workspace.yaml")))
|
|
131
|
-
return directory;
|
|
132
|
-
try {
|
|
133
|
-
const manifest = JSON.parse(import_node_fs.default.readFileSync(packageJson, "utf8"));
|
|
134
|
-
if (hasWorkspaceDeclaration(manifest.workspaces))
|
|
135
|
-
return directory;
|
|
136
|
-
} catch {}
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
function findConventionalProjectRoot(startDirectory) {
|
|
141
|
-
let nearestPackageDirectory = null;
|
|
142
|
-
let nearestPythonDirectory = null;
|
|
143
|
-
for (const directory of ancestors(startDirectory)) {
|
|
144
|
-
const hasPackage = import_node_fs.default.existsSync(import_node_path.default.join(directory, "package.json"));
|
|
145
|
-
if (hasPackage && !nearestPackageDirectory)
|
|
146
|
-
nearestPackageDirectory = directory;
|
|
147
|
-
if (!nearestPythonDirectory && PYTHON_PROJECT_MARKERS.some((file) => import_node_fs.default.existsSync(import_node_path.default.join(directory, file)))) {
|
|
148
|
-
nearestPythonDirectory = directory;
|
|
149
|
-
}
|
|
150
|
-
if (hasPackage && PACKAGE_MANAGER_LOCKFILES.some((file) => import_node_fs.default.existsSync(import_node_path.default.join(directory, file)))) {
|
|
151
|
-
return directory;
|
|
152
|
-
}
|
|
91
|
+
for (const directory of ancestors(start)) {
|
|
153
92
|
if (import_node_fs.default.existsSync(import_node_path.default.join(directory, ".git")))
|
|
154
93
|
return directory;
|
|
155
94
|
}
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
function hasWorkspaceDeclaration(value) {
|
|
159
|
-
return Array.isArray(value) || value !== null && typeof value === "object";
|
|
95
|
+
return start;
|
|
160
96
|
}
|
|
161
97
|
function* ancestors(startDirectory) {
|
|
162
98
|
let directory = startDirectory;
|
|
@@ -169,16 +105,26 @@ function* ancestors(startDirectory) {
|
|
|
169
105
|
}
|
|
170
106
|
}
|
|
171
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
|
+
|
|
172
120
|
// src/config.ts
|
|
173
|
-
var
|
|
174
|
-
var DEFAULT_PIP_RANGE = ">=20.3.0";
|
|
121
|
+
var DEFAULT_REQUIRES_PYTHON = ">=3.10,<4";
|
|
175
122
|
var DEFAULT_MANAGED_PYTHON = "3.12";
|
|
176
123
|
var DEFAULT_UV_VERSION = "0.13.6";
|
|
177
|
-
var DEFAULT_UV_RANGE =
|
|
124
|
+
var DEFAULT_UV_RANGE = ">=0.10.0 <1.0.0";
|
|
178
125
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
179
126
|
var DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60000;
|
|
180
127
|
var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60000;
|
|
181
|
-
var DEFAULT_CACHE_TTL_MS = 30000;
|
|
182
128
|
var envPathsFunction = (() => {
|
|
183
129
|
const imported = envPaths.default;
|
|
184
130
|
const implementation = typeof imported === "function" ? imported : imported.default;
|
|
@@ -188,30 +134,26 @@ var envPathsFunction = (() => {
|
|
|
188
134
|
})();
|
|
189
135
|
function resolveOptions(options = {}) {
|
|
190
136
|
const env = { ...options.env ?? process.env };
|
|
191
|
-
const
|
|
192
|
-
const pipRange = options.pipRange ?? env["PYFIG_PIP_RANGE"] ?? DEFAULT_PIP_RANGE;
|
|
137
|
+
const requiresPython = options.requiresPython ?? env["PYFIG_REQUIRES_PYTHON"] ?? DEFAULT_REQUIRES_PYTHON;
|
|
193
138
|
const uvRange = options.uvRange ?? env["PYFIG_UV_RANGE"] ?? DEFAULT_UV_RANGE;
|
|
194
|
-
|
|
195
|
-
assertRange("pip", pipRange);
|
|
139
|
+
assertPythonSpecifier(requiresPython);
|
|
196
140
|
assertRange("uv", uvRange);
|
|
197
141
|
const managedPython = options.managedPython ?? env["PYFIG_MANAGED_PYTHON"] ?? DEFAULT_MANAGED_PYTHON;
|
|
198
142
|
const uvVersion = options.uvVersion ?? env["PYFIG_UV_VERSION"] ?? DEFAULT_UV_VERSION;
|
|
199
|
-
assertManagedPythonRequest(managedPython,
|
|
143
|
+
assertManagedPythonRequest(managedPython, requiresPython);
|
|
200
144
|
assertUvVersion(uvVersion, uvRange);
|
|
201
145
|
const probeTimeoutMs = resolveDuration("probeTimeoutMs", options.probeTimeoutMs, env["PYFIG_PROBE_TIMEOUT_MS"], DEFAULT_PROBE_TIMEOUT_MS);
|
|
202
146
|
const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
|
|
203
147
|
const downloadTimeoutMs = resolveDuration("downloadTimeoutMs", options.downloadTimeoutMs, env["PYFIG_DOWNLOAD_TIMEOUT_MS"], DEFAULT_DOWNLOAD_TIMEOUT_MS);
|
|
204
148
|
const coordinationTimeoutMs = resolveDuration("coordinationTimeoutMs", options.coordinationTimeoutMs, env["PYFIG_COORDINATION_TIMEOUT_MS"], installTimeoutMs + downloadTimeoutMs + 60000);
|
|
205
|
-
const cacheTtlMs = resolveDuration("cacheTtlMs", options.cacheTtlMs, env["PYFIG_CACHE_TTL_MS"], DEFAULT_CACHE_TTL_MS, true);
|
|
206
149
|
const configuredHome = options.cacheDir ?? env["PYFIG_HOME"];
|
|
207
150
|
const cacheDir = configuredHome ? import_node_path2.default.resolve(configuredHome) : envPathsFunction("pyfig", { suffix: "" }).cache;
|
|
208
151
|
const configuredProjectRoot = options.projectRoot ?? env["PYFIG_PROJECT_ROOT"];
|
|
209
|
-
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());
|
|
210
153
|
const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
|
|
211
154
|
return {
|
|
212
155
|
python: options.python ?? env["PYFIG_PYTHON"] ?? "",
|
|
213
|
-
|
|
214
|
-
pipRange,
|
|
156
|
+
requiresPython,
|
|
215
157
|
managedPython,
|
|
216
158
|
uvVersion,
|
|
217
159
|
uvRange,
|
|
@@ -227,40 +169,16 @@ function resolveOptions(options = {}) {
|
|
|
227
169
|
installTimeoutMs,
|
|
228
170
|
downloadTimeoutMs,
|
|
229
171
|
coordinationTimeoutMs,
|
|
230
|
-
cacheTtlMs,
|
|
231
172
|
env
|
|
232
173
|
};
|
|
233
174
|
}
|
|
234
|
-
function configurationFingerprint(options) {
|
|
235
|
-
const policy = [
|
|
236
|
-
normalizedPath(options.projectRoot),
|
|
237
|
-
normalizedPath(options.cacheDir),
|
|
238
|
-
options.python,
|
|
239
|
-
options.pythonRange,
|
|
240
|
-
options.pipRange,
|
|
241
|
-
options.managedPython,
|
|
242
|
-
options.uvVersion,
|
|
243
|
-
options.uvRange,
|
|
244
|
-
options.requireEmbedding,
|
|
245
|
-
options.requireGil,
|
|
246
|
-
options.autoInstall,
|
|
247
|
-
options.installPython,
|
|
248
|
-
options.installUv,
|
|
249
|
-
options.createVirtualEnvironment,
|
|
250
|
-
options.probeTimeoutMs,
|
|
251
|
-
options.installTimeoutMs,
|
|
252
|
-
options.downloadTimeoutMs,
|
|
253
|
-
options.coordinationTimeoutMs,
|
|
254
|
-
options.cacheTtlMs,
|
|
255
|
-
environmentFingerprint(options.env)
|
|
256
|
-
];
|
|
257
|
-
return import_node_crypto.createHash("sha256").update(JSON.stringify(policy)).digest("hex");
|
|
258
|
-
}
|
|
259
175
|
function uvEnvironment(options) {
|
|
260
176
|
return {
|
|
261
177
|
...options.env,
|
|
262
178
|
UV_PYTHON_INSTALL_DIR: import_node_path2.default.join(options.cacheDir, "python"),
|
|
263
|
-
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"
|
|
264
182
|
};
|
|
265
183
|
}
|
|
266
184
|
function assertRange(tool, range) {
|
|
@@ -276,14 +194,19 @@ function assertUvVersion(version, range) {
|
|
|
276
194
|
throw new TypeError(`Pinned uv ${version} does not satisfy supported range ${range}.`);
|
|
277
195
|
}
|
|
278
196
|
}
|
|
279
|
-
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) {
|
|
280
203
|
if (!request || /[\\/\0\r\n]/u.test(request) || request.includes("..")) {
|
|
281
204
|
throw new TypeError(`Invalid managed Python request: ${request}`);
|
|
282
205
|
}
|
|
283
206
|
if (/^\d+\.\d+(?:\.\d+)?$/u.test(request)) {
|
|
284
207
|
const normalized = request.split(".").length === 2 ? `${request}.0` : request;
|
|
285
|
-
if (!
|
|
286
|
-
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}.`);
|
|
287
210
|
}
|
|
288
211
|
}
|
|
289
212
|
}
|
|
@@ -304,46 +227,176 @@ function parseBoolean(value, fallback) {
|
|
|
304
227
|
return false;
|
|
305
228
|
throw new TypeError(`Invalid boolean value: ${value}`);
|
|
306
229
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
value
|
|
311
|
-
]).sort(([left], [right]) => left.localeCompare(right));
|
|
312
|
-
return import_node_crypto.createHash("sha256").update(JSON.stringify(entries)).digest("hex");
|
|
313
|
-
}
|
|
314
|
-
function normalizedPath(value) {
|
|
315
|
-
const resolved = import_node_path2.default.resolve(value);
|
|
316
|
-
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
317
|
-
}
|
|
230
|
+
// src/client.ts
|
|
231
|
+
var import_loqkit2 = require("loqkit");
|
|
232
|
+
|
|
318
233
|
// src/provision.ts
|
|
319
|
-
var
|
|
234
|
+
var import_node_crypto = require("node:crypto");
|
|
320
235
|
var import_node_fs3 = require("node:fs");
|
|
321
|
-
var
|
|
236
|
+
var import_promises = require("node:fs/promises");
|
|
322
237
|
var import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
323
238
|
var import_node_os = __toESM(require("node:os"), 1);
|
|
324
|
-
var
|
|
239
|
+
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
325
240
|
var import_node_stream = require("node:stream");
|
|
326
|
-
var
|
|
327
|
-
var
|
|
241
|
+
var import_promises2 = require("node:stream/promises");
|
|
242
|
+
var import_fflate = require("fflate");
|
|
328
243
|
var tar = __toESM(require("tar"), 1);
|
|
329
244
|
var import_undici = require("undici");
|
|
330
245
|
|
|
331
|
-
// src/
|
|
332
|
-
var
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
246
|
+
// src/provision-lock.ts
|
|
247
|
+
var import_loqkit = require("loqkit");
|
|
248
|
+
async function withProvisioningLock(options) {
|
|
249
|
+
let existing;
|
|
250
|
+
const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
|
|
251
|
+
key: options.key,
|
|
252
|
+
name: "Pyfig provisioning",
|
|
253
|
+
namespace: "pyfig",
|
|
254
|
+
timeout: options.timeoutMs,
|
|
255
|
+
predicate: async () => {
|
|
256
|
+
const value = await options.check();
|
|
257
|
+
if (value === null)
|
|
258
|
+
return true;
|
|
259
|
+
existing = { value };
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
if (result.context.state === "executed")
|
|
264
|
+
return result.value;
|
|
265
|
+
if (existing)
|
|
266
|
+
return existing.value;
|
|
267
|
+
throw new Error(`Provisioning check for ${options.key} did not produce a resource.`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/virtual-environment.ts
|
|
271
|
+
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
272
|
+
var import_node_path3 = __toESM(require("node:path"), 1);
|
|
273
|
+
var VIRTUAL_ENV_DIRECTORY_NAME = import_node_path3.default.join("node_modules", ".pyfig", "venv");
|
|
274
|
+
function virtualEnvironmentDirectory(projectRoot) {
|
|
275
|
+
return import_node_path3.default.join(projectRoot, VIRTUAL_ENV_DIRECTORY_NAME);
|
|
276
|
+
}
|
|
277
|
+
function externalVirtualEnvironmentDirectories(projectRoot, env) {
|
|
278
|
+
const candidates = [env["VIRTUAL_ENV"], import_node_path3.default.join(projectRoot, ".venv")].filter((value) => Boolean(value)).map((value) => import_node_path3.default.resolve(value));
|
|
279
|
+
return [...new Map(candidates.map((value) => [canonicalPath(value), value])).values()];
|
|
280
|
+
}
|
|
281
|
+
function virtualEnvironmentPythonExecutable(virtualEnvironment) {
|
|
282
|
+
return import_node_path3.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
|
|
283
|
+
}
|
|
284
|
+
function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
|
|
285
|
+
const activated = { ...env };
|
|
286
|
+
const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
|
|
287
|
+
const existingPath = pathEntry ? activated[pathEntry] : undefined;
|
|
288
|
+
for (const key of Object.keys(activated)) {
|
|
289
|
+
const normalized = key.toUpperCase();
|
|
290
|
+
if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
|
|
291
|
+
delete activated[key];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
activated["PATH"] = [import_node_path3.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path3.default.delimiter);
|
|
295
|
+
activated["VIRTUAL_ENV"] = virtualEnvironment;
|
|
296
|
+
return activated;
|
|
297
|
+
}
|
|
298
|
+
function pathsEqual(left, right) {
|
|
299
|
+
return canonicalPath(left) === canonicalPath(right);
|
|
300
|
+
}
|
|
301
|
+
function canonicalPath(value) {
|
|
302
|
+
const resolved = import_node_path3.default.resolve(value);
|
|
303
|
+
let canonical = resolved;
|
|
304
|
+
try {
|
|
305
|
+
canonical = import_node_fs2.default.realpathSync.native(resolved);
|
|
306
|
+
} catch {}
|
|
307
|
+
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/package-target.ts
|
|
311
|
+
var TARGET_OPTIONS = new Set([
|
|
312
|
+
"--active",
|
|
313
|
+
"--config-file",
|
|
314
|
+
"--directory",
|
|
315
|
+
"--isolated",
|
|
316
|
+
"--prefix",
|
|
317
|
+
"--project",
|
|
318
|
+
"--python",
|
|
319
|
+
"--root",
|
|
320
|
+
"--system",
|
|
321
|
+
"--target",
|
|
322
|
+
"--user"
|
|
323
|
+
]);
|
|
324
|
+
var TARGET_ENVIRONMENT_VARIABLES = new Set([
|
|
325
|
+
"CONDA_PREFIX",
|
|
326
|
+
"PIP_PREFIX",
|
|
327
|
+
"PIP_PYTHON",
|
|
328
|
+
"PIP_ROOT",
|
|
329
|
+
"PIP_TARGET",
|
|
330
|
+
"PIP_USER",
|
|
331
|
+
"PYTHONHOME",
|
|
332
|
+
"PYTHONUSERBASE",
|
|
333
|
+
"UV_CONFIG_FILE",
|
|
334
|
+
"UV_PROJECT",
|
|
335
|
+
"UV_PROJECT_ENVIRONMENT",
|
|
336
|
+
"UV_PYTHON",
|
|
337
|
+
"UV_PREFIX",
|
|
338
|
+
"UV_ROOT",
|
|
339
|
+
"UV_SYSTEM_PYTHON",
|
|
340
|
+
"UV_TARGET"
|
|
341
|
+
]);
|
|
342
|
+
function uvCommandNeedsEnvironment(args) {
|
|
343
|
+
return args[0] === "pip" || args[0] === "run";
|
|
344
|
+
}
|
|
345
|
+
function targetUvArguments(args, python) {
|
|
346
|
+
if (!python.virtualEnvironment) {
|
|
347
|
+
throw new Error("Targeted uv commands require a virtual environment.");
|
|
348
|
+
}
|
|
349
|
+
assertNoTargetOverride(args);
|
|
350
|
+
if (args[0] === "run") {
|
|
351
|
+
return ["run", "--no-project", "--python", python.executable, ...args.slice(1)];
|
|
352
|
+
}
|
|
353
|
+
if (args[0] === "pip" && args[1]) {
|
|
354
|
+
return ["pip", args[1], "--python", python.executable, ...args.slice(2)];
|
|
355
|
+
}
|
|
356
|
+
return [...args];
|
|
357
|
+
}
|
|
358
|
+
function uvCommandEnvironment(env, python) {
|
|
359
|
+
const sanitized = sanitizeUvEnvironment(env);
|
|
360
|
+
if (!python?.virtualEnvironment)
|
|
361
|
+
return sanitized;
|
|
362
|
+
return {
|
|
363
|
+
...activateVirtualEnvironment(sanitized, python.virtualEnvironment, python.executable),
|
|
364
|
+
UV_PYTHON: python.executable
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function sanitizeUvEnvironment(env) {
|
|
368
|
+
const sanitized = { ...env };
|
|
369
|
+
for (const key of Object.keys(sanitized)) {
|
|
370
|
+
if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
|
|
371
|
+
delete sanitized[key];
|
|
372
|
+
}
|
|
373
|
+
return sanitized;
|
|
374
|
+
}
|
|
375
|
+
function assertNoTargetOverride(args) {
|
|
376
|
+
for (const argument of args.slice(1)) {
|
|
377
|
+
if (argument === "--")
|
|
378
|
+
return;
|
|
379
|
+
const [name] = argument.split("=", 1);
|
|
380
|
+
if (name && TARGET_OPTIONS.has(name)) {
|
|
381
|
+
throw new Error(`${name} cannot override the environment selected by Pyfig.`);
|
|
382
|
+
}
|
|
383
|
+
if (argument === "-p" || argument.startsWith("-p=") || /^-p[^-]/u.test(argument)) {
|
|
384
|
+
throw new Error("-p cannot override the environment selected by Pyfig.");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/probes.ts
|
|
390
|
+
var import_semver2 = __toESM(require("semver"), 1);
|
|
338
391
|
|
|
339
392
|
// src/process.ts
|
|
340
393
|
var import_node_child_process = require("node:child_process");
|
|
341
|
-
var
|
|
394
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
342
395
|
var import_which = __toESM(require("which"), 1);
|
|
343
396
|
async function resolveExecutable(value, env) {
|
|
344
397
|
if (!value)
|
|
345
398
|
return null;
|
|
346
|
-
if (
|
|
399
|
+
if (import_node_path4.default.isAbsolute(value)) {
|
|
347
400
|
return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
|
|
348
401
|
}
|
|
349
402
|
if (!/^[A-Za-z0-9_.-]+$/u.test(value))
|
|
@@ -360,14 +413,15 @@ function run(executable, args, env, timeoutMs) {
|
|
|
360
413
|
});
|
|
361
414
|
if (result.status !== 0 || result.error) {
|
|
362
415
|
const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
|
|
363
|
-
throw new Error(`${
|
|
416
|
+
throw new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
|
|
364
417
|
}
|
|
365
418
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
366
419
|
}
|
|
367
|
-
async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
420
|
+
async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
|
|
368
421
|
return new Promise((resolve, reject) => {
|
|
369
422
|
const child = import_node_child_process.spawn(executable, args, {
|
|
370
423
|
env,
|
|
424
|
+
cwd,
|
|
371
425
|
stdio: ["ignore", "pipe", "pipe"],
|
|
372
426
|
windowsHide: true
|
|
373
427
|
});
|
|
@@ -388,11 +442,11 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
|
388
442
|
const abort = () => {
|
|
389
443
|
child.kill();
|
|
390
444
|
const reason = signal?.reason;
|
|
391
|
-
finish(reason instanceof Error ? reason : new Error(`${
|
|
445
|
+
finish(reason instanceof Error ? reason : new Error(`${import_node_path4.default.basename(executable)} was aborted.`));
|
|
392
446
|
};
|
|
393
447
|
const timer = setTimeout(() => {
|
|
394
448
|
child.kill();
|
|
395
|
-
finish(new Error(`${
|
|
449
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} timed out after ${timeoutMs}ms.`));
|
|
396
450
|
}, timeoutMs);
|
|
397
451
|
child.stdout.setEncoding("utf8");
|
|
398
452
|
child.stderr.setEncoding("utf8");
|
|
@@ -400,21 +454,21 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
|
400
454
|
stdout += chunk;
|
|
401
455
|
if (stdout.length > 4 * 1024 * 1024) {
|
|
402
456
|
child.kill();
|
|
403
|
-
finish(new Error(`${
|
|
457
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the output limit.`));
|
|
404
458
|
}
|
|
405
459
|
});
|
|
406
460
|
child.stderr.on("data", (chunk) => {
|
|
407
461
|
stderr += chunk;
|
|
408
462
|
if (stderr.length > 4 * 1024 * 1024) {
|
|
409
463
|
child.kill();
|
|
410
|
-
finish(new Error(`${
|
|
464
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the error-output limit.`));
|
|
411
465
|
}
|
|
412
466
|
});
|
|
413
467
|
child.once("error", (error) => finish(error));
|
|
414
468
|
child.once("close", (code, childSignal) => {
|
|
415
469
|
if (code !== 0) {
|
|
416
470
|
const detail = stderr.trim() || (childSignal ? `signal ${childSignal}` : `exit ${code}`);
|
|
417
|
-
finish(new Error(`${
|
|
471
|
+
finish(new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
|
|
418
472
|
return;
|
|
419
473
|
}
|
|
420
474
|
finish(undefined, { stdout, stderr });
|
|
@@ -426,213 +480,6 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
|
|
|
426
480
|
});
|
|
427
481
|
}
|
|
428
482
|
|
|
429
|
-
// src/coordination.ts
|
|
430
|
-
var realmMutexTails = new Map;
|
|
431
|
-
var flockLookups = new Map;
|
|
432
|
-
var FLOCK_READY = "PYFIG_FLOCK_READY";
|
|
433
|
-
var FLOCK_HOLDER = [
|
|
434
|
-
`process.stdout.write('${FLOCK_READY}\\n')`,
|
|
435
|
-
"process.stdin.resume()",
|
|
436
|
-
'process.stdin.once("end",()=>process.exit(0))'
|
|
437
|
-
].join(";");
|
|
438
|
-
async function withProvisioningLock(options) {
|
|
439
|
-
const existing = await options.check();
|
|
440
|
-
if (existing !== null)
|
|
441
|
-
return existing;
|
|
442
|
-
return withResourceLock(options, async (signal) => {
|
|
443
|
-
const rechecked = await options.check();
|
|
444
|
-
return rechecked ?? options.mutate(signal);
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
async function withResourceLock(options, operation) {
|
|
448
|
-
await import_promises.mkdir(options.target, { recursive: true });
|
|
449
|
-
return withRealmMutex(options.key, () => withWorkerLock(options, () => withNativeFlock(options, () => withPortableLease(options, operation))));
|
|
450
|
-
}
|
|
451
|
-
async function withRealmMutex(key, operation) {
|
|
452
|
-
const previous = realmMutexTails.get(key) ?? Promise.resolve();
|
|
453
|
-
let releaseGate;
|
|
454
|
-
const gate = new Promise((resolve) => {
|
|
455
|
-
releaseGate = resolve;
|
|
456
|
-
});
|
|
457
|
-
const tail = previous.catch(() => {
|
|
458
|
-
return;
|
|
459
|
-
}).then(() => gate);
|
|
460
|
-
realmMutexTails.set(key, tail);
|
|
461
|
-
await previous.catch(() => {
|
|
462
|
-
return;
|
|
463
|
-
});
|
|
464
|
-
try {
|
|
465
|
-
return await operation();
|
|
466
|
-
} finally {
|
|
467
|
-
releaseGate();
|
|
468
|
-
if (realmMutexTails.get(key) === tail)
|
|
469
|
-
realmMutexTails.delete(key);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
async function withWorkerLock(options, operation) {
|
|
473
|
-
const manager = workerThreads.locks;
|
|
474
|
-
if (!manager?.request)
|
|
475
|
-
return operation();
|
|
476
|
-
const controller = new AbortController;
|
|
477
|
-
const timeout = setTimeout(() => controller.abort(new Error(`Timed out waiting for worker lock ${options.key}.`)), options.timeoutMs);
|
|
478
|
-
let entered = false;
|
|
479
|
-
try {
|
|
480
|
-
return await manager.request(workerLockName(options.key), { mode: "exclusive", signal: controller.signal }, async () => {
|
|
481
|
-
entered = true;
|
|
482
|
-
return operation();
|
|
483
|
-
});
|
|
484
|
-
} catch (error) {
|
|
485
|
-
if (entered)
|
|
486
|
-
throw error;
|
|
487
|
-
return operation();
|
|
488
|
-
} finally {
|
|
489
|
-
clearTimeout(timeout);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
async function withNativeFlock(options, operation) {
|
|
493
|
-
if (process.platform === "win32")
|
|
494
|
-
return operation();
|
|
495
|
-
const executable = await findFlock(process.env);
|
|
496
|
-
if (!executable)
|
|
497
|
-
return operation();
|
|
498
|
-
const child = await acquireFlock(executable, `${options.target}.flock`, options.timeoutMs);
|
|
499
|
-
if (!child)
|
|
500
|
-
return operation();
|
|
501
|
-
try {
|
|
502
|
-
return await operation();
|
|
503
|
-
} finally {
|
|
504
|
-
await releaseFlock(child);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
async function findFlock(env) {
|
|
508
|
-
const lookupKey = `${env["PATH"] ?? ""}\x00${env["PATHEXT"] ?? ""}`;
|
|
509
|
-
let lookup = flockLookups.get(lookupKey);
|
|
510
|
-
if (!lookup) {
|
|
511
|
-
lookup = resolveExecutable("flock", env);
|
|
512
|
-
flockLookups.set(lookupKey, lookup);
|
|
513
|
-
}
|
|
514
|
-
return lookup;
|
|
515
|
-
}
|
|
516
|
-
async function acquireFlock(executable, lockPath, timeoutMs) {
|
|
517
|
-
const waitSeconds = Math.max(1, Math.ceil(Math.min(timeoutMs, 30000) / 1000));
|
|
518
|
-
const child = import_node_child_process2.spawn(executable, ["-x", "-w", String(waitSeconds), lockPath, process.execPath, "-e", FLOCK_HOLDER], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
519
|
-
child.stderr.resume();
|
|
520
|
-
let output = "";
|
|
521
|
-
const acquired = await new Promise((resolve) => {
|
|
522
|
-
let settled = false;
|
|
523
|
-
const finish = (value) => {
|
|
524
|
-
if (settled)
|
|
525
|
-
return;
|
|
526
|
-
settled = true;
|
|
527
|
-
clearTimeout(timer);
|
|
528
|
-
resolve(value);
|
|
529
|
-
};
|
|
530
|
-
const timer = setTimeout(() => finish(false), Math.min(timeoutMs, 30000) + 5000);
|
|
531
|
-
child.once("error", () => finish(false));
|
|
532
|
-
child.once("exit", () => finish(false));
|
|
533
|
-
child.stdout.setEncoding("utf8");
|
|
534
|
-
child.stdout.on("data", (chunk) => {
|
|
535
|
-
output += chunk;
|
|
536
|
-
if (output.includes(FLOCK_READY))
|
|
537
|
-
finish(true);
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
if (acquired)
|
|
541
|
-
return child;
|
|
542
|
-
child.kill();
|
|
543
|
-
return null;
|
|
544
|
-
}
|
|
545
|
-
async function releaseFlock(child) {
|
|
546
|
-
if (child.exitCode !== null)
|
|
547
|
-
return;
|
|
548
|
-
child.stdin.end();
|
|
549
|
-
await new Promise((resolve) => {
|
|
550
|
-
const timer = setTimeout(() => {
|
|
551
|
-
child.kill();
|
|
552
|
-
resolve();
|
|
553
|
-
}, 5000);
|
|
554
|
-
child.once("exit", () => {
|
|
555
|
-
clearTimeout(timer);
|
|
556
|
-
resolve();
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
async function withPortableLease(options, operation) {
|
|
561
|
-
const controller = new AbortController;
|
|
562
|
-
let compromised = null;
|
|
563
|
-
const retryIntervalMs = 500;
|
|
564
|
-
const release = await import_proper_lockfile.default.lock(options.target, {
|
|
565
|
-
realpath: true,
|
|
566
|
-
stale: 120000,
|
|
567
|
-
update: 15000,
|
|
568
|
-
retries: {
|
|
569
|
-
retries: Math.ceil(options.timeoutMs / retryIntervalMs),
|
|
570
|
-
factor: 1,
|
|
571
|
-
minTimeout: retryIntervalMs,
|
|
572
|
-
maxTimeout: retryIntervalMs
|
|
573
|
-
},
|
|
574
|
-
onCompromised: (error) => {
|
|
575
|
-
compromised = error;
|
|
576
|
-
controller.abort(error);
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
try {
|
|
580
|
-
const result = await operation(controller.signal);
|
|
581
|
-
if (compromised)
|
|
582
|
-
throw compromised;
|
|
583
|
-
return result;
|
|
584
|
-
} finally {
|
|
585
|
-
controller.abort();
|
|
586
|
-
await release();
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
function workerLockName(key) {
|
|
590
|
-
return `pyfig:${import_node_crypto2.createHash("sha256").update(key).digest("hex")}`;
|
|
591
|
-
}
|
|
592
|
-
function cacheLockTarget(cacheDirectory, name) {
|
|
593
|
-
return import_node_path4.default.join(cacheDirectory, "locks", name);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// src/probes.ts
|
|
597
|
-
var import_node_path6 = __toESM(require("node:path"), 1);
|
|
598
|
-
var import_semver2 = __toESM(require("semver"), 1);
|
|
599
|
-
|
|
600
|
-
// src/virtual-environment.ts
|
|
601
|
-
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
602
|
-
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
603
|
-
var VIRTUAL_ENV_DIRECTORY_NAME = ".venv";
|
|
604
|
-
function virtualEnvironmentDirectory(projectRoot) {
|
|
605
|
-
return import_node_path5.default.join(projectRoot, VIRTUAL_ENV_DIRECTORY_NAME);
|
|
606
|
-
}
|
|
607
|
-
function virtualEnvironmentPythonExecutable(virtualEnvironment) {
|
|
608
|
-
return import_node_path5.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
|
|
609
|
-
}
|
|
610
|
-
function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
|
|
611
|
-
const activated = { ...env };
|
|
612
|
-
const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
|
|
613
|
-
const existingPath = pathEntry ? activated[pathEntry] : undefined;
|
|
614
|
-
for (const key of Object.keys(activated)) {
|
|
615
|
-
const normalized = key.toUpperCase();
|
|
616
|
-
if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
|
|
617
|
-
delete activated[key];
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
activated["PATH"] = [import_node_path5.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path5.default.delimiter);
|
|
621
|
-
activated["VIRTUAL_ENV"] = virtualEnvironment;
|
|
622
|
-
return activated;
|
|
623
|
-
}
|
|
624
|
-
function pathsEqual(left, right) {
|
|
625
|
-
return canonicalPath(left) === canonicalPath(right);
|
|
626
|
-
}
|
|
627
|
-
function canonicalPath(value) {
|
|
628
|
-
const resolved = import_node_path5.default.resolve(value);
|
|
629
|
-
let canonical = resolved;
|
|
630
|
-
try {
|
|
631
|
-
canonical = import_node_fs2.default.realpathSync.native(resolved);
|
|
632
|
-
} catch {}
|
|
633
|
-
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
483
|
// src/probes.ts
|
|
637
484
|
var PYTHON_PROBE = `
|
|
638
485
|
import json
|
|
@@ -682,7 +529,15 @@ if os.path.isfile(header_path) and library_path:
|
|
|
682
529
|
}
|
|
683
530
|
|
|
684
531
|
release_level = sys.version_info.releaselevel
|
|
685
|
-
|
|
532
|
+
release_suffixes = {
|
|
533
|
+
"alpha": "a",
|
|
534
|
+
"beta": "b",
|
|
535
|
+
"candidate": "rc",
|
|
536
|
+
"final": "",
|
|
537
|
+
}
|
|
538
|
+
release_suffix = release_suffixes[release_level]
|
|
539
|
+
if release_suffix:
|
|
540
|
+
release_suffix += str(sys.version_info.serial)
|
|
686
541
|
|
|
687
542
|
print(json.dumps({
|
|
688
543
|
"executable": sys.executable,
|
|
@@ -702,24 +557,8 @@ print(json.dumps({
|
|
|
702
557
|
"embedding": embedding,
|
|
703
558
|
}))
|
|
704
559
|
`;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (options.python)
|
|
708
|
-
candidates.push({ executable: options.python, args: [] });
|
|
709
|
-
if (process.platform === "win32") {
|
|
710
|
-
candidates.push({ executable: "python", args: [] }, { executable: "python3", args: [] }, { executable: "py", args: ["-3"] });
|
|
711
|
-
} else {
|
|
712
|
-
candidates.push(...["python3.15", "python3.14", "python3.13", "python3.12", "python3.11", "python3.10"].map((executable) => ({ executable, args: [] })), { executable: "python3", args: [] }, { executable: "python", args: [] });
|
|
713
|
-
}
|
|
714
|
-
for (const candidate of candidates) {
|
|
715
|
-
const info = await probePython(candidate, options);
|
|
716
|
-
if (info && pythonSatisfiesRequirements(info, options))
|
|
717
|
-
return info;
|
|
718
|
-
}
|
|
719
|
-
return null;
|
|
720
|
-
}
|
|
721
|
-
function pythonSatisfiesRequirements(python, options) {
|
|
722
|
-
return import_semver2.default.satisfies(python.version, options.pythonRange) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
|
|
560
|
+
function pythonSatisfiesPolicy(python, options) {
|
|
561
|
+
return pythonVersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
|
|
723
562
|
}
|
|
724
563
|
async function probePython(candidate, options) {
|
|
725
564
|
const executable = await resolveExecutable(candidate.executable, options.env);
|
|
@@ -728,55 +567,11 @@ async function probePython(candidate, options) {
|
|
|
728
567
|
try {
|
|
729
568
|
const { stdout } = run(executable, [...candidate.args, "-c", PYTHON_PROBE], options.env, options.probeTimeoutMs);
|
|
730
569
|
const info = JSON.parse(stdout.trim());
|
|
731
|
-
return
|
|
570
|
+
return isValidPythonVersion(info.version) ? info : null;
|
|
732
571
|
} catch {
|
|
733
572
|
return null;
|
|
734
573
|
}
|
|
735
574
|
}
|
|
736
|
-
async function findPip(python, options) {
|
|
737
|
-
if (!python.virtualEnvironment) {
|
|
738
|
-
throw new Error("pip discovery requires a virtual-environment Python interpreter.");
|
|
739
|
-
}
|
|
740
|
-
const activeEnv = activateVirtualEnvironment(options.env, python.virtualEnvironment, python.executable);
|
|
741
|
-
try {
|
|
742
|
-
const { stdout } = run(python.executable, ["-m", "pip", "--version"], activeEnv, options.probeTimeoutMs);
|
|
743
|
-
const details = parsePipVersionOutput(stdout);
|
|
744
|
-
const version = normalizeVersion(details?.version);
|
|
745
|
-
if (!version || !import_semver2.default.satisfies(version, options.pipRange))
|
|
746
|
-
return null;
|
|
747
|
-
if (!details || !pipBelongsToPython(details, python))
|
|
748
|
-
return null;
|
|
749
|
-
return {
|
|
750
|
-
kind: "pip",
|
|
751
|
-
executable: python.executable,
|
|
752
|
-
prefixArgs: ["-m", "pip"],
|
|
753
|
-
version,
|
|
754
|
-
pythonExecutable: python.executable,
|
|
755
|
-
virtualEnvironment: python.virtualEnvironment
|
|
756
|
-
};
|
|
757
|
-
} catch {
|
|
758
|
-
return null;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
function parsePipVersionOutput(output) {
|
|
762
|
-
const match = output.trim().match(/^pip\s+(\S+)\s+from\s+(.+?)\s+\(python\s+(\d+)\.(\d+)(?:\.\d+)?\)$/iu);
|
|
763
|
-
if (!match?.[1] || !match[2] || !match[3] || !match[4])
|
|
764
|
-
return null;
|
|
765
|
-
return {
|
|
766
|
-
version: match[1],
|
|
767
|
-
location: match[2],
|
|
768
|
-
pythonMajor: Number.parseInt(match[3], 10),
|
|
769
|
-
pythonMinor: Number.parseInt(match[4], 10)
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
function pipBelongsToPython(pip, python) {
|
|
773
|
-
if (pip.pythonMajor !== python.major || pip.pythonMinor !== python.minor)
|
|
774
|
-
return false;
|
|
775
|
-
const prefix = normalizePathForComparison(import_node_path6.default.resolve(python.prefix));
|
|
776
|
-
const location = normalizePathForComparison(import_node_path6.default.resolve(pip.location));
|
|
777
|
-
const relative = import_node_path6.default.relative(prefix, location);
|
|
778
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path6.default.sep}`) && !import_node_path6.default.isAbsolute(relative);
|
|
779
|
-
}
|
|
780
575
|
async function findUv(options) {
|
|
781
576
|
const configured = options.env["PYFIG_UV"] ?? "uv";
|
|
782
577
|
const executable = await resolveExecutable(configured, options.env);
|
|
@@ -797,9 +592,6 @@ function normalizeVersion(value) {
|
|
|
797
592
|
return null;
|
|
798
593
|
return import_semver2.default.valid(value) ?? import_semver2.default.coerce(value)?.version ?? null;
|
|
799
594
|
}
|
|
800
|
-
function normalizePathForComparison(value) {
|
|
801
|
-
return process.platform === "win32" ? value.toLowerCase() : value;
|
|
802
|
-
}
|
|
803
595
|
function pythonArchitectureMatchesRuntime(python) {
|
|
804
596
|
const architecture = python.architecture.toLowerCase();
|
|
805
597
|
if (python.pointerBits !== (process.arch === "ia32" ? 32 : 64))
|
|
@@ -814,228 +606,223 @@ function pythonArchitectureMatchesRuntime(python) {
|
|
|
814
606
|
return architecture === process.arch;
|
|
815
607
|
}
|
|
816
608
|
|
|
817
|
-
// src/state.ts
|
|
818
|
-
var completed = new Map;
|
|
819
|
-
var inFlight = new Map;
|
|
820
|
-
var cacheGeneration = 0;
|
|
821
|
-
async function memoizeAsync(namespace, key, ttlMs, validate, load) {
|
|
822
|
-
const cacheKey = `${namespace}\x00${key}`;
|
|
823
|
-
const cached = completed.get(cacheKey);
|
|
824
|
-
if (cached && cached.expiresAt >= Date.now()) {
|
|
825
|
-
try {
|
|
826
|
-
if (await validate(cached.value))
|
|
827
|
-
return cached.value;
|
|
828
|
-
} catch {}
|
|
829
|
-
completed.delete(cacheKey);
|
|
830
|
-
}
|
|
831
|
-
const pending = inFlight.get(cacheKey);
|
|
832
|
-
if (pending)
|
|
833
|
-
return pending;
|
|
834
|
-
const generation = cacheGeneration;
|
|
835
|
-
const loading = load().then((value) => {
|
|
836
|
-
if (ttlMs > 0 && generation === cacheGeneration) {
|
|
837
|
-
completed.set(cacheKey, { expiresAt: Date.now() + ttlMs, value });
|
|
838
|
-
}
|
|
839
|
-
return value;
|
|
840
|
-
}).finally(() => {
|
|
841
|
-
if (inFlight.get(cacheKey) === loading)
|
|
842
|
-
inFlight.delete(cacheKey);
|
|
843
|
-
});
|
|
844
|
-
inFlight.set(cacheKey, loading);
|
|
845
|
-
return loading;
|
|
846
|
-
}
|
|
847
|
-
function memoizeSync(namespace, key, ttlMs, validate, load) {
|
|
848
|
-
const cacheKey = `${namespace}\x00${key}`;
|
|
849
|
-
const cached = completed.get(cacheKey);
|
|
850
|
-
if (cached && cached.expiresAt >= Date.now()) {
|
|
851
|
-
try {
|
|
852
|
-
if (validate(cached.value))
|
|
853
|
-
return cached.value;
|
|
854
|
-
} catch {}
|
|
855
|
-
completed.delete(cacheKey);
|
|
856
|
-
}
|
|
857
|
-
const value = load();
|
|
858
|
-
if (ttlMs > 0)
|
|
859
|
-
completed.set(cacheKey, { expiresAt: Date.now() + ttlMs, value });
|
|
860
|
-
return value;
|
|
861
|
-
}
|
|
862
|
-
function clearPyfigCaches() {
|
|
863
|
-
cacheGeneration += 1;
|
|
864
|
-
completed.clear();
|
|
865
|
-
inFlight.clear();
|
|
866
|
-
}
|
|
867
|
-
|
|
868
609
|
// src/provision.ts
|
|
869
|
-
async function ensurePython(options = {}) {
|
|
870
|
-
const resolved = resolveOptions(options);
|
|
871
|
-
return memoizeAsync("python", configurationFingerprint(resolved), resolved.cacheTtlMs, pythonIsPresent, () => ensurePythonResolved(resolved));
|
|
872
|
-
}
|
|
873
|
-
async function ensureVirtualEnvironment(options = {}) {
|
|
874
|
-
const resolved = resolveOptions(options);
|
|
875
|
-
return memoizeAsync("venv", configurationFingerprint(resolved), resolved.cacheTtlMs, (python) => pythonIsPresent(python) && Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(resolved.projectRoot)), () => ensureVirtualEnvironmentResolved(resolved));
|
|
876
|
-
}
|
|
877
610
|
async function ensureUv(options = {}) {
|
|
878
|
-
|
|
879
|
-
return memoizeAsync("uv", configurationFingerprint(resolved), resolved.cacheTtlMs, (uv) => import_node_fs4.default.existsSync(uv.executable), () => ensureUvResolved(resolved));
|
|
611
|
+
return ensureUvResolved(resolveOptions(options));
|
|
880
612
|
}
|
|
881
|
-
async function ensureEnvironment(options = {}) {
|
|
613
|
+
async function ensureEnvironment(options = {}, dependencies = [], dependencyOptions = {}) {
|
|
882
614
|
const resolved = resolveOptions(options);
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
615
|
+
const normalized = normalizeDependencies(dependencies);
|
|
616
|
+
const selections = normalizeDependencyOptions(dependencyOptions);
|
|
617
|
+
const uv = await ensureUvResolved(resolved);
|
|
618
|
+
const python = await ensureVirtualEnvironmentResolved(resolved, uv);
|
|
619
|
+
if (normalized.length > 0 || selections.extras.length > 0 || selections.groups.length > 0 || selections.allExtras) {
|
|
620
|
+
await installRuntimeDependencies(uv, python, normalized, selections, resolved);
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
python,
|
|
624
|
+
uv,
|
|
625
|
+
managed: isManagedEnvironment(python, resolved)
|
|
626
|
+
};
|
|
888
627
|
}
|
|
889
|
-
async function ensureVirtualEnvironmentResolved(options) {
|
|
628
|
+
async function ensureVirtualEnvironmentResolved(options, uv) {
|
|
629
|
+
const existing = await findReusableEnvironment(options);
|
|
630
|
+
if (existing)
|
|
631
|
+
return existing;
|
|
890
632
|
const directory = virtualEnvironmentDirectory(options.projectRoot);
|
|
891
633
|
const check = async () => {
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
return null;
|
|
895
|
-
if (pythonSatisfiesRequirements(existing, options) && await virtualEnvironmentMatchesConfiguredPython(existing, options)) {
|
|
896
|
-
return existing;
|
|
897
|
-
}
|
|
898
|
-
throw incompatibleVirtualEnvironmentError(directory, existing, options);
|
|
634
|
+
const candidate = await inspectVirtualEnvironment(directory, options);
|
|
635
|
+
return candidate && pythonSatisfiesPolicy(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
|
|
899
636
|
};
|
|
900
|
-
const existing = await check();
|
|
901
|
-
if (existing)
|
|
902
|
-
return existing;
|
|
903
637
|
if (!options.createVirtualEnvironment) {
|
|
904
|
-
throw new Error(`No
|
|
638
|
+
throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
|
|
905
639
|
}
|
|
906
|
-
const basePython = await
|
|
907
|
-
const preferredUv = await findAvailableUv(options) ?? (options.installUv ? await ensureUvResolved(options) : null);
|
|
640
|
+
const basePython = await ensureBasePythonResolved(uv, options);
|
|
908
641
|
return withProvisioningLock({
|
|
909
642
|
key: `venv:${canonicalPath(directory)}`,
|
|
910
|
-
target: directory,
|
|
911
643
|
timeoutMs: options.coordinationTimeoutMs,
|
|
912
|
-
env: options.env,
|
|
913
644
|
check,
|
|
914
|
-
mutate: async (
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
645
|
+
mutate: async () => {
|
|
646
|
+
await import_promises.rm(directory, { recursive: true, force: true });
|
|
647
|
+
await import_promises.mkdir(import_node_path5.default.dirname(directory), { recursive: true });
|
|
648
|
+
await runAsync(uv.executable, [
|
|
649
|
+
"venv",
|
|
650
|
+
"--no-project",
|
|
651
|
+
"--no-config",
|
|
652
|
+
"--no-python-downloads",
|
|
653
|
+
"--python",
|
|
654
|
+
basePython.executable,
|
|
655
|
+
directory
|
|
656
|
+
], isolatedUvEnvironment(options), options.installTimeoutMs);
|
|
657
|
+
const installed = await check();
|
|
658
|
+
if (!installed) {
|
|
659
|
+
throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
|
|
924
660
|
}
|
|
925
661
|
return installed;
|
|
926
662
|
}
|
|
927
663
|
});
|
|
928
664
|
}
|
|
929
|
-
async function
|
|
930
|
-
if (
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
665
|
+
async function findReusableEnvironment(options) {
|
|
666
|
+
if (options.python) {
|
|
667
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
668
|
+
if (!configured || !pythonSatisfiesPolicy(configured, options)) {
|
|
669
|
+
throw incompatibleConfiguredPython(options);
|
|
670
|
+
}
|
|
671
|
+
if (configured.virtualEnvironment)
|
|
672
|
+
return configured;
|
|
935
673
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
674
|
+
for (const directory of [
|
|
675
|
+
...externalVirtualEnvironmentDirectories(options.projectRoot, options.env),
|
|
676
|
+
virtualEnvironmentDirectory(options.projectRoot)
|
|
677
|
+
]) {
|
|
678
|
+
const info = await inspectVirtualEnvironment(directory, options);
|
|
679
|
+
if (info && pythonSatisfiesPolicy(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
|
|
680
|
+
return info;
|
|
681
|
+
}
|
|
942
682
|
}
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
async function createVirtualEnvironmentWithUv(uv, directory, basePython, options, signal) {
|
|
946
|
-
await runAsync(uv.executable, [
|
|
947
|
-
"venv",
|
|
948
|
-
"--no-project",
|
|
949
|
-
"--no-config",
|
|
950
|
-
"--no-python-downloads",
|
|
951
|
-
"--allow-existing",
|
|
952
|
-
"--python",
|
|
953
|
-
basePython.executable,
|
|
954
|
-
directory
|
|
955
|
-
], uvEnvironment(options), options.installTimeoutMs, signal);
|
|
956
|
-
}
|
|
957
|
-
async function inspectVirtualEnvironment(directory, options) {
|
|
958
|
-
const executable = virtualEnvironmentPythonExecutable(directory);
|
|
959
|
-
const info = await probePython({ executable, args: [] }, options);
|
|
960
|
-
return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
|
|
683
|
+
return null;
|
|
961
684
|
}
|
|
962
|
-
async function
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
685
|
+
async function ensureBasePythonResolved(uv, options) {
|
|
686
|
+
if (options.python) {
|
|
687
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
688
|
+
if (!configured || !pythonSatisfiesPolicy(configured, options)) {
|
|
689
|
+
throw incompatibleConfiguredPython(options);
|
|
690
|
+
}
|
|
691
|
+
if (!configured.virtualEnvironment)
|
|
692
|
+
return configured;
|
|
693
|
+
const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
|
|
694
|
+
if (base && pythonSatisfiesPolicy(base, options))
|
|
695
|
+
return base;
|
|
696
|
+
throw incompatibleConfiguredPython(options);
|
|
697
|
+
}
|
|
698
|
+
const system = await findPythonWithUv(uv, options, "system");
|
|
699
|
+
if (system)
|
|
700
|
+
return system;
|
|
701
|
+
const cachedManaged = await findPythonWithUv(uv, options, "managed");
|
|
702
|
+
if (cachedManaged)
|
|
703
|
+
return cachedManaged;
|
|
966
704
|
if (!options.installPython) {
|
|
967
|
-
|
|
968
|
-
throw new Error(`No Python interpreter satisfies ${options.pythonRange}${capability}; ` + "managed Python installation is disabled.");
|
|
705
|
+
throw new Error(missingPythonMessage(options, "managed Python installation is disabled"));
|
|
969
706
|
}
|
|
970
|
-
const lockTarget = cacheLockTarget(options.cacheDir, "python");
|
|
971
707
|
return withProvisioningLock({
|
|
972
|
-
key: `python:${canonicalPath(
|
|
973
|
-
target: lockTarget,
|
|
708
|
+
key: `python:${canonicalPath(import_node_path5.default.join(options.cacheDir, "python"))}`,
|
|
974
709
|
timeoutMs: options.coordinationTimeoutMs,
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const
|
|
979
|
-
const uvEnv = uvEnvironment(options);
|
|
980
|
-
await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], uvEnv, options.installTimeoutMs, signal);
|
|
981
|
-
const installed = await findPythonWithUv(uv, options, signal);
|
|
710
|
+
check: () => findPythonWithUv(uv, options, "managed"),
|
|
711
|
+
mutate: async () => {
|
|
712
|
+
await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], isolatedUvEnvironment(options), options.installTimeoutMs);
|
|
713
|
+
const installed = await findPythonWithUv(uv, options, "managed");
|
|
982
714
|
if (!installed) {
|
|
983
|
-
throw new Error(`uv installed
|
|
715
|
+
throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
|
|
984
716
|
}
|
|
985
717
|
return installed;
|
|
986
718
|
}
|
|
987
719
|
});
|
|
988
720
|
}
|
|
989
|
-
async function
|
|
990
|
-
|
|
991
|
-
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
992
|
-
if (configured && pythonSatisfiesRequirements(configured, options))
|
|
993
|
-
return configured;
|
|
994
|
-
throw new Error(`Configured Python ${options.python} is unavailable or does not satisfy ${options.pythonRange}.`);
|
|
995
|
-
}
|
|
996
|
-
const existingUv = await findAvailableUv(options);
|
|
997
|
-
if (existingUv) {
|
|
998
|
-
const uvPython = await findPythonWithUv(existingUv, options);
|
|
999
|
-
if (uvPython)
|
|
1000
|
-
return uvPython;
|
|
1001
|
-
}
|
|
1002
|
-
return findPython({ ...options, python: "" });
|
|
1003
|
-
}
|
|
1004
|
-
async function findPythonWithUv(uv, options, signal) {
|
|
1005
|
-
const requests = [undefined, options.managedPython];
|
|
1006
|
-
for (const requestedPython of requests) {
|
|
721
|
+
async function findPythonWithUv(uv, options, source) {
|
|
722
|
+
for (const request of ["", options.managedPython]) {
|
|
1007
723
|
const args = [
|
|
1008
724
|
"python",
|
|
1009
725
|
"find",
|
|
1010
726
|
"--no-project",
|
|
1011
727
|
"--no-config",
|
|
1012
|
-
"--
|
|
1013
|
-
"--
|
|
728
|
+
"--no-python-downloads",
|
|
729
|
+
source === "system" ? "--system" : "--managed-python"
|
|
1014
730
|
];
|
|
1015
|
-
if (
|
|
1016
|
-
args.push(
|
|
731
|
+
if (request)
|
|
732
|
+
args.push(request);
|
|
1017
733
|
try {
|
|
1018
|
-
const { stdout } = await runAsync(uv.executable, args,
|
|
734
|
+
const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
|
|
1019
735
|
const info = await probePython({ executable: stdout.trim(), args: [] }, options);
|
|
1020
|
-
if (info &&
|
|
736
|
+
if (info && pythonSatisfiesPolicy(info, options))
|
|
1021
737
|
return info;
|
|
1022
|
-
} catch
|
|
1023
|
-
if (signal?.aborted)
|
|
1024
|
-
throw signal.reason ?? error;
|
|
1025
|
-
}
|
|
738
|
+
} catch {}
|
|
1026
739
|
}
|
|
740
|
+
try {
|
|
741
|
+
const { stdout } = await runAsync(uv.executable, [
|
|
742
|
+
"python",
|
|
743
|
+
"list",
|
|
744
|
+
"--no-config",
|
|
745
|
+
"--no-python-downloads",
|
|
746
|
+
"--only-installed",
|
|
747
|
+
"--output-format",
|
|
748
|
+
"json",
|
|
749
|
+
source === "system" ? "--no-managed-python" : "--managed-python"
|
|
750
|
+
], explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
|
|
751
|
+
const candidates = installedPythonPaths(stdout);
|
|
752
|
+
for (const executable of candidates) {
|
|
753
|
+
const info = await probePython({ executable, args: [] }, options);
|
|
754
|
+
if (info && !info.virtualEnvironment && pythonSatisfiesPolicy(info, options)) {
|
|
755
|
+
return info;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
} catch {}
|
|
1027
759
|
return null;
|
|
1028
760
|
}
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1031
|
-
if (
|
|
1032
|
-
return
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
return
|
|
1038
|
-
|
|
761
|
+
function installedPythonPaths(output) {
|
|
762
|
+
const parsed = JSON.parse(output);
|
|
763
|
+
if (!Array.isArray(parsed))
|
|
764
|
+
return [];
|
|
765
|
+
const paths = parsed.flatMap((entry) => {
|
|
766
|
+
if (!entry || typeof entry !== "object" || !("path" in entry))
|
|
767
|
+
return [];
|
|
768
|
+
const candidate = entry.path;
|
|
769
|
+
return typeof candidate === "string" && candidate ? [candidate] : [];
|
|
770
|
+
});
|
|
771
|
+
return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
|
|
772
|
+
}
|
|
773
|
+
async function inspectVirtualEnvironment(directory, options) {
|
|
774
|
+
const executable = virtualEnvironmentPythonExecutable(directory);
|
|
775
|
+
const info = await probePython({ executable, args: [] }, options);
|
|
776
|
+
return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
|
|
777
|
+
}
|
|
778
|
+
async function installRuntimeDependencies(uv, python, dependencies, dependencyOptions, options) {
|
|
779
|
+
if (!python.virtualEnvironment) {
|
|
780
|
+
throw new Error("Runtime dependency installation requires a virtual environment.");
|
|
781
|
+
}
|
|
782
|
+
await withProvisioningLock({
|
|
783
|
+
key: `dependencies:${canonicalPath(python.virtualEnvironment)}`,
|
|
784
|
+
timeoutMs: options.coordinationTimeoutMs,
|
|
785
|
+
check: async () => null,
|
|
786
|
+
mutate: async () => {
|
|
787
|
+
const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-dependencies-"));
|
|
788
|
+
try {
|
|
789
|
+
const additions = import_node_path5.default.join(temporary, "runtime-dependencies.txt");
|
|
790
|
+
const resolved = import_node_path5.default.join(temporary, "resolved-dependencies.txt");
|
|
791
|
+
const pyproject = import_node_path5.default.join(options.projectRoot, "pyproject.toml");
|
|
792
|
+
const hasPyproject = import_node_fs4.default.existsSync(pyproject);
|
|
793
|
+
if (!hasPyproject && (dependencyOptions.extras.length > 0 || dependencyOptions.groups.length > 0 || dependencyOptions.allExtras)) {
|
|
794
|
+
throw new Error("Runtime extras and groups require an existing pyproject.toml.");
|
|
795
|
+
}
|
|
796
|
+
const sources = hasPyproject ? [pyproject] : [];
|
|
797
|
+
if (dependencies.length > 0) {
|
|
798
|
+
await import_promises.writeFile(additions, `${dependencies.join(`
|
|
799
|
+
`)}
|
|
800
|
+
`, "utf8");
|
|
801
|
+
sources.push(additions);
|
|
802
|
+
}
|
|
803
|
+
const selectionArgs = [
|
|
804
|
+
...dependencyOptions.extras.flatMap((extra) => ["--extra", extra]),
|
|
805
|
+
...dependencyOptions.groups.flatMap((group) => ["--group", group]),
|
|
806
|
+
...dependencyOptions.allExtras ? ["--all-extras"] : []
|
|
807
|
+
];
|
|
808
|
+
await runAsync(uv.executable, [
|
|
809
|
+
"pip",
|
|
810
|
+
"compile",
|
|
811
|
+
"--no-config",
|
|
812
|
+
"--python",
|
|
813
|
+
python.executable,
|
|
814
|
+
...selectionArgs,
|
|
815
|
+
...sources,
|
|
816
|
+
"--output-file",
|
|
817
|
+
resolved
|
|
818
|
+
], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
819
|
+
await runAsync(uv.executable, ["pip", "install", "--no-config", "--python", python.executable, "-r", resolved], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
820
|
+
} finally {
|
|
821
|
+
await import_promises.rm(temporary, { recursive: true, force: true });
|
|
822
|
+
}
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
});
|
|
1039
826
|
}
|
|
1040
827
|
async function ensureUvResolved(options) {
|
|
1041
828
|
const existing = await findAvailableUv(options);
|
|
@@ -1046,15 +833,12 @@ async function ensureUvResolved(options) {
|
|
|
1046
833
|
}
|
|
1047
834
|
const installDir = uvInstallDirectory(options);
|
|
1048
835
|
const executable = uvExecutable(options);
|
|
1049
|
-
const lockTarget = cacheLockTarget(options.cacheDir, "uv");
|
|
1050
836
|
return withProvisioningLock({
|
|
1051
|
-
key: `uv:${canonicalPath(
|
|
1052
|
-
target: lockTarget,
|
|
837
|
+
key: `uv:${canonicalPath(installDir)}`,
|
|
1053
838
|
timeoutMs: options.coordinationTimeoutMs,
|
|
1054
|
-
env: options.env,
|
|
1055
839
|
check: () => findAvailableUv(options),
|
|
1056
|
-
mutate: async (
|
|
1057
|
-
await installUv(installDir, options
|
|
840
|
+
mutate: async () => {
|
|
841
|
+
await installUv(installDir, options);
|
|
1058
842
|
const result = probeUv(executable, options);
|
|
1059
843
|
if (!result || result.version !== options.uvVersion) {
|
|
1060
844
|
throw new Error(`Expected managed uv ${options.uvVersion}, but the installed executable did not match.`);
|
|
@@ -1064,88 +848,134 @@ async function ensureUvResolved(options) {
|
|
|
1064
848
|
});
|
|
1065
849
|
}
|
|
1066
850
|
async function findAvailableUv(options) {
|
|
1067
|
-
const
|
|
1068
|
-
if (
|
|
1069
|
-
return
|
|
851
|
+
const external = await findUv(options);
|
|
852
|
+
if (external)
|
|
853
|
+
return external;
|
|
1070
854
|
const managed = probeUv(uvExecutable(options), options);
|
|
1071
855
|
return managed?.version === options.uvVersion ? managed : null;
|
|
1072
856
|
}
|
|
1073
|
-
function
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
857
|
+
function normalizeDependencies(dependencies) {
|
|
858
|
+
const normalized = dependencies.map((dependency) => dependency.trim());
|
|
859
|
+
for (const dependency of normalized) {
|
|
860
|
+
if (!dependency || /[\0\r\n]/u.test(dependency)) {
|
|
861
|
+
throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return [...new Set(normalized)];
|
|
1078
865
|
}
|
|
1079
|
-
function
|
|
1080
|
-
|
|
1081
|
-
|
|
866
|
+
function normalizeDependencyOptions(options) {
|
|
867
|
+
const extras = normalizeNames("extra", options.extras ?? []);
|
|
868
|
+
const groups = normalizeNames("dependency group", options.groups ?? []);
|
|
869
|
+
if (options.allExtras && extras.length > 0) {
|
|
870
|
+
throw new TypeError("allExtras cannot be combined with explicit extras.");
|
|
1082
871
|
}
|
|
1083
|
-
return {
|
|
1084
|
-
kind: "uv",
|
|
1085
|
-
executable: uv.executable,
|
|
1086
|
-
prefixArgs: ["pip"],
|
|
1087
|
-
version: uv.version,
|
|
1088
|
-
pythonExecutable: python.executable,
|
|
1089
|
-
virtualEnvironment: python.virtualEnvironment
|
|
1090
|
-
};
|
|
872
|
+
return { extras, groups, allExtras: options.allExtras ?? false };
|
|
1091
873
|
}
|
|
1092
|
-
function
|
|
1093
|
-
const
|
|
1094
|
-
for (const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
delete result[key];
|
|
874
|
+
function normalizeNames(kind, names) {
|
|
875
|
+
const normalized = names.map((name) => name.trim());
|
|
876
|
+
for (const name of normalized) {
|
|
877
|
+
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
|
|
878
|
+
throw new TypeError(`Invalid ${kind}: ${JSON.stringify(name)}`);
|
|
1098
879
|
}
|
|
1099
880
|
}
|
|
1100
|
-
return
|
|
881
|
+
return [...new Set(normalized)];
|
|
882
|
+
}
|
|
883
|
+
function isManagedEnvironment(python, options) {
|
|
884
|
+
return Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(options.projectRoot));
|
|
885
|
+
}
|
|
886
|
+
function isolatedUvEnvironment(options) {
|
|
887
|
+
return sanitizeUvEnvironment(uvEnvironment(options));
|
|
1101
888
|
}
|
|
1102
|
-
|
|
889
|
+
function explicitPythonSourceEnvironment(options) {
|
|
890
|
+
const env = isolatedUvEnvironment(options);
|
|
891
|
+
delete env["UV_PYTHON_PREFERENCE"];
|
|
892
|
+
return env;
|
|
893
|
+
}
|
|
894
|
+
function targetUvEnvironment(options, python) {
|
|
895
|
+
return uvCommandEnvironment(uvEnvironment(options), python);
|
|
896
|
+
}
|
|
897
|
+
function incompatibleConfiguredPython(options) {
|
|
898
|
+
return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `requires-python ${options.requiresPython}` + `${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
|
|
899
|
+
}
|
|
900
|
+
function missingPythonMessage(options, reason) {
|
|
901
|
+
const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
|
|
902
|
+
return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
|
|
903
|
+
}
|
|
904
|
+
async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
|
|
905
|
+
if (!options.python)
|
|
906
|
+
return true;
|
|
907
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
908
|
+
if (!configured || !pythonSatisfiesPolicy(configured, options))
|
|
909
|
+
return false;
|
|
910
|
+
if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
|
|
914
|
+
return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
|
|
915
|
+
}
|
|
916
|
+
function uvInstallDirectory(options) {
|
|
917
|
+
return import_node_path5.default.join(options.cacheDir, "tools", "uv", options.uvVersion);
|
|
918
|
+
}
|
|
919
|
+
function uvExecutable(options) {
|
|
920
|
+
return import_node_path5.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
|
|
921
|
+
}
|
|
922
|
+
async function installUv(installDir, options) {
|
|
1103
923
|
const asset = uvAsset();
|
|
1104
924
|
const baseUrl = `https://github.com/astral-sh/uv/releases/download/${options.uvVersion}`;
|
|
1105
|
-
const temporary = await
|
|
925
|
+
const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
|
|
1106
926
|
const dispatcher = proxyDispatcher(options.env);
|
|
1107
927
|
try {
|
|
1108
|
-
const archive =
|
|
1109
|
-
await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs,
|
|
1110
|
-
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs,
|
|
928
|
+
const archive = import_node_path5.default.join(temporary, asset);
|
|
929
|
+
await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
|
|
930
|
+
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
|
|
1111
931
|
const actual = await sha256File(archive);
|
|
1112
932
|
if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
|
|
1113
933
|
throw new Error(`uv archive checksum mismatch for ${asset}`);
|
|
1114
934
|
}
|
|
1115
|
-
const extracted =
|
|
1116
|
-
await
|
|
1117
|
-
|
|
1118
|
-
if (asset.endsWith(".zip"))
|
|
1119
|
-
await import_extract_zip.default(archive, { dir: extracted });
|
|
1120
|
-
else
|
|
1121
|
-
await tar.x({ file: archive, cwd: extracted, strict: true });
|
|
1122
|
-
signal.throwIfAborted();
|
|
1123
|
-
const source = await findFile(extracted, process.platform === "win32" ? "uv.exe" : "uv");
|
|
935
|
+
const extracted = import_node_path5.default.join(temporary, "extracted");
|
|
936
|
+
await import_promises.mkdir(extracted, { recursive: true });
|
|
937
|
+
const source = await extractUvExecutable(archive, asset, extracted);
|
|
1124
938
|
if (!source)
|
|
1125
939
|
throw new Error(`uv archive did not contain the expected executable: ${asset}`);
|
|
1126
940
|
await publishUvAtomically(source, installDir, options);
|
|
1127
941
|
} finally {
|
|
1128
942
|
await dispatcher?.close();
|
|
1129
|
-
await
|
|
943
|
+
await import_promises.rm(temporary, { recursive: true, force: true });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
async function extractUvExecutable(archive, asset, destination) {
|
|
947
|
+
const executableName = process.platform === "win32" ? "uv.exe" : "uv";
|
|
948
|
+
if (!asset.endsWith(".zip")) {
|
|
949
|
+
await tar.x({ file: archive, cwd: destination, strict: true });
|
|
950
|
+
return findFile(destination, executableName);
|
|
1130
951
|
}
|
|
952
|
+
const entries = import_fflate.unzipSync(await import_promises.readFile(archive), {
|
|
953
|
+
filter: ({ name }) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName
|
|
954
|
+
});
|
|
955
|
+
const matches = Object.entries(entries).filter(([name]) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
|
|
956
|
+
if (matches.length !== 1 || !matches[0])
|
|
957
|
+
return null;
|
|
958
|
+
const source = import_node_path5.default.join(destination, executableName);
|
|
959
|
+
await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
|
|
960
|
+
return source;
|
|
1131
961
|
}
|
|
1132
962
|
async function publishUvAtomically(source, installDir, options) {
|
|
1133
|
-
await
|
|
963
|
+
await import_promises.mkdir(installDir, { recursive: true });
|
|
1134
964
|
const executable = uvExecutable(options);
|
|
1135
|
-
const staged =
|
|
965
|
+
const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
|
|
1136
966
|
try {
|
|
1137
|
-
await
|
|
967
|
+
await import_promises.copyFile(source, staged);
|
|
1138
968
|
if (process.platform !== "win32")
|
|
1139
|
-
await
|
|
969
|
+
await import_promises.chmod(staged, 493);
|
|
1140
970
|
const stagedUv = probeUv(staged, options);
|
|
1141
971
|
if (!stagedUv || stagedUv.version !== options.uvVersion) {
|
|
1142
972
|
throw new Error(`Downloaded uv executable is not version ${options.uvVersion}.`);
|
|
1143
973
|
}
|
|
1144
974
|
if (process.platform === "win32")
|
|
1145
|
-
await
|
|
1146
|
-
await
|
|
975
|
+
await import_promises.rm(executable, { force: true });
|
|
976
|
+
await import_promises.rename(staged, executable);
|
|
1147
977
|
} finally {
|
|
1148
|
-
await
|
|
978
|
+
await import_promises.rm(staged, { force: true });
|
|
1149
979
|
}
|
|
1150
980
|
}
|
|
1151
981
|
function uvAsset() {
|
|
@@ -1160,23 +990,23 @@ function uvAsset() {
|
|
|
1160
990
|
return `uv-${architecture}-unknown-linux-musl.tar.gz`;
|
|
1161
991
|
throw new Error(`uv bootstrap does not support platform ${process.platform}.`);
|
|
1162
992
|
}
|
|
1163
|
-
async function download(url, destination, timeoutMs,
|
|
993
|
+
async function download(url, destination, timeoutMs, dispatcher) {
|
|
1164
994
|
const response = await import_undici.fetch(url, {
|
|
1165
995
|
dispatcher,
|
|
1166
996
|
redirect: "follow",
|
|
1167
|
-
signal:
|
|
997
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1168
998
|
});
|
|
1169
999
|
if (!response.ok || !response.body) {
|
|
1170
1000
|
await response.body?.cancel();
|
|
1171
1001
|
throw new Error(`Could not download ${url}: HTTP ${response.status}`);
|
|
1172
1002
|
}
|
|
1173
|
-
await
|
|
1003
|
+
await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
|
|
1174
1004
|
}
|
|
1175
|
-
async function downloadText(url, timeoutMs,
|
|
1005
|
+
async function downloadText(url, timeoutMs, dispatcher) {
|
|
1176
1006
|
const response = await import_undici.fetch(url, {
|
|
1177
1007
|
dispatcher,
|
|
1178
1008
|
redirect: "follow",
|
|
1179
|
-
signal:
|
|
1009
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1180
1010
|
});
|
|
1181
1011
|
if (!response.ok) {
|
|
1182
1012
|
await response.body?.cancel();
|
|
@@ -1198,18 +1028,15 @@ function proxyDispatcher(env) {
|
|
|
1198
1028
|
function environmentValue(env, lowercase, uppercase) {
|
|
1199
1029
|
return env[lowercase] ?? env[uppercase];
|
|
1200
1030
|
}
|
|
1201
|
-
function combinedSignal(timeoutMs, externalSignal) {
|
|
1202
|
-
return AbortSignal.any([externalSignal, AbortSignal.timeout(timeoutMs)]);
|
|
1203
|
-
}
|
|
1204
1031
|
async function sha256File(file) {
|
|
1205
|
-
const hash =
|
|
1032
|
+
const hash = import_node_crypto.createHash("sha256");
|
|
1206
1033
|
for await (const chunk of import_node_fs3.createReadStream(file))
|
|
1207
1034
|
hash.update(chunk);
|
|
1208
1035
|
return hash.digest("hex");
|
|
1209
1036
|
}
|
|
1210
1037
|
async function findFile(directory, name) {
|
|
1211
|
-
for (const entry of await
|
|
1212
|
-
const target =
|
|
1038
|
+
for (const entry of await import_promises.readdir(directory, { withFileTypes: true })) {
|
|
1039
|
+
const target = import_node_path5.default.join(directory, entry.name);
|
|
1213
1040
|
if (entry.isFile() && entry.name === name)
|
|
1214
1041
|
return target;
|
|
1215
1042
|
if (entry.isDirectory()) {
|
|
@@ -1220,176 +1047,27 @@ async function findFile(directory, name) {
|
|
|
1220
1047
|
}
|
|
1221
1048
|
return null;
|
|
1222
1049
|
}
|
|
1223
|
-
function incompatibleVirtualEnvironmentError(directory, python, options) {
|
|
1224
|
-
const capabilities = options.requireEmbedding ? " or lacks compatible CPython embedding headers, library, or architecture" : "";
|
|
1225
|
-
return new Error(`The existing virtual environment at ${directory} uses Python ${python.version}, ` + `which does not satisfy ${options.pythonRange}${capabilities}.`);
|
|
1226
|
-
}
|
|
1227
|
-
async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
|
|
1228
|
-
if (!options.python)
|
|
1229
|
-
return true;
|
|
1230
|
-
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
1231
|
-
if (!configured || !pythonSatisfiesRequirements(configured, options))
|
|
1232
|
-
return false;
|
|
1233
|
-
if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
|
|
1234
|
-
return true;
|
|
1235
|
-
}
|
|
1236
|
-
const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
|
|
1237
|
-
return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
|
|
1238
|
-
}
|
|
1239
|
-
function pythonIsPresent(python) {
|
|
1240
|
-
return import_node_fs4.default.existsSync(python.executable);
|
|
1241
|
-
}
|
|
1242
|
-
function environmentIsPresent(environment) {
|
|
1243
|
-
return pythonIsPresent(environment.python) && import_node_fs4.default.existsSync(environment.packageManager.executable);
|
|
1244
|
-
}
|
|
1245
1050
|
|
|
1246
1051
|
// src/runners.ts
|
|
1247
|
-
var
|
|
1248
|
-
var import_node_fs6 = __toESM(require("node:fs"), 1);
|
|
1249
|
-
|
|
1250
|
-
// src/package-target.ts
|
|
1251
|
-
var import_node_os2 = __toESM(require("node:os"), 1);
|
|
1252
|
-
var TARGET_CHANGING_OPTIONS = new Set([
|
|
1253
|
-
"--config-file",
|
|
1254
|
-
"--prefix",
|
|
1255
|
-
"--python",
|
|
1256
|
-
"--root",
|
|
1257
|
-
"--system",
|
|
1258
|
-
"--target",
|
|
1259
|
-
"--user"
|
|
1260
|
-
]);
|
|
1261
|
-
var TARGET_CHANGING_SHORT_OPTIONS = new Set(["-p", "-t"]);
|
|
1262
|
-
var TARGET_ENVIRONMENT_VARIABLES = new Set([
|
|
1263
|
-
"CONDA_PREFIX",
|
|
1264
|
-
"PIP_CONFIG_FILE",
|
|
1265
|
-
"PIP_PREFIX",
|
|
1266
|
-
"PIP_PYTHON",
|
|
1267
|
-
"PIP_ROOT",
|
|
1268
|
-
"PIP_TARGET",
|
|
1269
|
-
"PIP_USER",
|
|
1270
|
-
"PYTHONHOME",
|
|
1271
|
-
"PYTHONUSERBASE",
|
|
1272
|
-
"UV_CONFIG_FILE",
|
|
1273
|
-
"UV_PROJECT_ENVIRONMENT",
|
|
1274
|
-
"UV_PREFIX",
|
|
1275
|
-
"UV_PYTHON",
|
|
1276
|
-
"UV_ROOT",
|
|
1277
|
-
"UV_SYSTEM_PYTHON",
|
|
1278
|
-
"UV_TARGET"
|
|
1279
|
-
]);
|
|
1280
|
-
function assertSafePackageArguments(args) {
|
|
1281
|
-
for (const argument of args) {
|
|
1282
|
-
if (argument === "--")
|
|
1283
|
-
break;
|
|
1284
|
-
const [name] = argument.split("=", 1);
|
|
1285
|
-
if (name && TARGET_CHANGING_OPTIONS.has(name)) {
|
|
1286
|
-
throw new Error(`${name} is not allowed because Pyfig package commands are pinned to the repository .venv.`);
|
|
1287
|
-
}
|
|
1288
|
-
const shortName = argument.slice(0, 2);
|
|
1289
|
-
if (TARGET_CHANGING_SHORT_OPTIONS.has(shortName)) {
|
|
1290
|
-
throw new Error(`${shortName} is not allowed because Pyfig package commands are pinned to the repository .venv.`);
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
function packageManagerEnvironment(env, packageManager) {
|
|
1295
|
-
const active = sanitizedTargetEnvironment(env);
|
|
1296
|
-
const activated = activateVirtualEnvironment(active, packageManager.virtualEnvironment, packageManager.pythonExecutable);
|
|
1297
|
-
return packageManager.kind === "uv" ? {
|
|
1298
|
-
...activated,
|
|
1299
|
-
UV_NO_CONFIG: "1",
|
|
1300
|
-
UV_PYTHON: packageManager.pythonExecutable
|
|
1301
|
-
} : {
|
|
1302
|
-
...activated,
|
|
1303
|
-
PIP_CONFIG_FILE: import_node_os2.default.devNull,
|
|
1304
|
-
PIP_REQUIRE_VIRTUALENV: "1",
|
|
1305
|
-
PYTHONNOUSERSITE: "1"
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
function uvPipEnvironment(env, python) {
|
|
1309
|
-
if (!python.virtualEnvironment) {
|
|
1310
|
-
throw new Error("uv pip commands require the repository virtual environment.");
|
|
1311
|
-
}
|
|
1312
|
-
return {
|
|
1313
|
-
...activateVirtualEnvironment(sanitizedTargetEnvironment(env), python.virtualEnvironment, python.executable),
|
|
1314
|
-
UV_NO_CONFIG: "1",
|
|
1315
|
-
UV_PYTHON: python.executable
|
|
1316
|
-
};
|
|
1317
|
-
}
|
|
1318
|
-
function assertMutationUsesUv(packageManager, args) {
|
|
1319
|
-
if (packageManager.kind === "pip" && isPackageMutation(args)) {
|
|
1320
|
-
throw new Error("Package mutation requires uv so the repository environment is locked across processes. " + "Enable uv installation or use a read-only pip command.");
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
function isPackageMutation(args) {
|
|
1324
|
-
return args.some((argument) => argument === "install" || argument === "sync" || argument === "uninstall");
|
|
1325
|
-
}
|
|
1326
|
-
function isUvPipCommand(args) {
|
|
1327
|
-
const separator = args.indexOf("--");
|
|
1328
|
-
const commandArguments = separator === -1 ? args : args.slice(0, separator);
|
|
1329
|
-
return commandArguments.includes("pip");
|
|
1330
|
-
}
|
|
1331
|
-
function sanitizedTargetEnvironment(env) {
|
|
1332
|
-
const sanitized = { ...env };
|
|
1333
|
-
for (const key of Object.keys(sanitized)) {
|
|
1334
|
-
if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
|
|
1335
|
-
delete sanitized[key];
|
|
1336
|
-
}
|
|
1337
|
-
return sanitized;
|
|
1338
|
-
}
|
|
1052
|
+
var import_node_child_process2 = require("node:child_process");
|
|
1339
1053
|
|
|
1340
1054
|
// src/runtime-files.ts
|
|
1341
1055
|
var import_node_fs5 = __toESM(require("node:fs"), 1);
|
|
1342
|
-
var
|
|
1056
|
+
var import_node_path6 = __toESM(require("node:path"), 1);
|
|
1343
1057
|
function runtimeFile(name) {
|
|
1344
1058
|
const moduleDirectory = __dirname;
|
|
1345
|
-
const compiled =
|
|
1346
|
-
return import_node_fs5.default.existsSync(compiled) ? compiled :
|
|
1059
|
+
const compiled = import_node_path6.default.join(moduleDirectory, `${name}.js`);
|
|
1060
|
+
return import_node_fs5.default.existsSync(compiled) ? compiled : import_node_path6.default.join(moduleDirectory, `${name}.ts`);
|
|
1347
1061
|
}
|
|
1348
1062
|
|
|
1349
1063
|
// src/runners.ts
|
|
1350
|
-
async function pythonSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
|
|
1351
|
-
const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
|
|
1352
|
-
const python = await ensureVirtualEnvironment(options);
|
|
1353
|
-
const command = buildPythonCommand(python, args, resolveOptions(options).env);
|
|
1354
|
-
return import_node_child_process3.spawn(command.executable, command.args, {
|
|
1355
|
-
...spawnOptions,
|
|
1356
|
-
env: command.env
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
function pythonSpawnSync(args = [], spawnOptions = {}, pyfigOptions = {}) {
|
|
1360
|
-
const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
|
|
1361
|
-
const python = resolveSynchronously("venv", options);
|
|
1362
|
-
const command = buildPythonCommand(python, args, resolveOptions(options).env);
|
|
1363
|
-
return import_node_child_process3.spawnSync(command.executable, command.args, {
|
|
1364
|
-
...spawnOptions,
|
|
1365
|
-
env: command.env
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
async function pipSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
|
|
1369
|
-
const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
|
|
1370
|
-
const environment = await ensureEnvironment(options);
|
|
1371
|
-
const command = buildPipCommand(environment.packageManager, args, resolveOptions(options).env);
|
|
1372
|
-
return import_node_child_process3.spawn(command.executable, command.args, {
|
|
1373
|
-
...spawnOptions,
|
|
1374
|
-
env: command.env
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
function pipSpawnSync(args = [], spawnOptions = {}, pyfigOptions = {}) {
|
|
1378
|
-
const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
|
|
1379
|
-
const environment = resolveSynchronously("environment", options);
|
|
1380
|
-
const command = buildPipCommand(environment.packageManager, args, resolveOptions(options).env);
|
|
1381
|
-
return import_node_child_process3.spawnSync(command.executable, command.args, {
|
|
1382
|
-
...spawnOptions,
|
|
1383
|
-
env: command.env
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
1064
|
async function uvSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
|
|
1387
1065
|
const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
|
|
1388
1066
|
const resolved = resolveOptions(options);
|
|
1389
|
-
const
|
|
1390
|
-
const
|
|
1391
|
-
const command = buildUvCommand(uv, args, resolved, python);
|
|
1392
|
-
return
|
|
1067
|
+
const environment = uvCommandNeedsEnvironment(args) ? await ensureEnvironment(resolved) : undefined;
|
|
1068
|
+
const uv = environment?.uv ?? await ensureUv(resolved);
|
|
1069
|
+
const command = buildUvCommand(uv, args, resolved, environment?.python);
|
|
1070
|
+
return import_node_child_process2.spawn(command.executable, command.args, {
|
|
1393
1071
|
...spawnOptions,
|
|
1394
1072
|
env: command.env
|
|
1395
1073
|
});
|
|
@@ -1397,45 +1075,19 @@ async function uvSpawn(args = [], spawnOptions = {}, pyfigOptions = {}) {
|
|
|
1397
1075
|
function uvSpawnSync(args = [], spawnOptions = {}, pyfigOptions = {}) {
|
|
1398
1076
|
const options = optionsForInvocation(pyfigOptions, spawnOptions.env);
|
|
1399
1077
|
const resolved = resolveOptions(options);
|
|
1400
|
-
const
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1078
|
+
const environment = uvCommandNeedsEnvironment(args) ? resolveSynchronously("environment", resolved) : undefined;
|
|
1079
|
+
const uv = environment?.uv ?? resolveSynchronously("uv", resolved);
|
|
1080
|
+
const command = buildUvCommand(uv, args, resolved, environment?.python);
|
|
1081
|
+
return import_node_child_process2.spawnSync(command.executable, command.args, {
|
|
1403
1082
|
...spawnOptions,
|
|
1404
1083
|
env: command.env
|
|
1405
1084
|
});
|
|
1406
1085
|
}
|
|
1407
|
-
function buildPythonCommand(python, args, env) {
|
|
1408
|
-
const commandEnv = python.virtualEnvironment ? activateVirtualEnvironment(env, python.virtualEnvironment, python.executable) : env;
|
|
1409
|
-
return {
|
|
1410
|
-
executable: python.executable,
|
|
1411
|
-
args: [...args],
|
|
1412
|
-
env: commandEnv
|
|
1413
|
-
};
|
|
1414
|
-
}
|
|
1415
|
-
function buildPipCommand(packageManager, args, env) {
|
|
1416
|
-
if (!packageManager.virtualEnvironment) {
|
|
1417
|
-
throw new Error("pip commands require an active virtual environment.");
|
|
1418
|
-
}
|
|
1419
|
-
assertSafePackageArguments(args);
|
|
1420
|
-
assertMutationUsesUv(packageManager, args);
|
|
1421
|
-
return {
|
|
1422
|
-
executable: packageManager.executable,
|
|
1423
|
-
args: [...packageManager.prefixArgs, ...args],
|
|
1424
|
-
env: packageManagerEnvironment(env, packageManager)
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
1086
|
function buildUvCommand(uv, args, options, python) {
|
|
1428
|
-
const isPip = isUvPipCommand(args);
|
|
1429
|
-
if (isPip && !python?.virtualEnvironment) {
|
|
1430
|
-
throw new Error("uv pip commands require the repo virtual-environment Python interpreter.");
|
|
1431
|
-
}
|
|
1432
|
-
if (isPip)
|
|
1433
|
-
assertSafePackageArguments(args);
|
|
1434
|
-
const env = isPip && python ? uvPipEnvironment(uvEnvironment(options), python) : uvEnvironment(options);
|
|
1435
1087
|
return {
|
|
1436
1088
|
executable: uv.executable,
|
|
1437
|
-
args: [...args],
|
|
1438
|
-
env
|
|
1089
|
+
args: python ? targetUvArguments(args, python) : [...args],
|
|
1090
|
+
env: uvCommandEnvironment(uvEnvironment(options), python)
|
|
1439
1091
|
};
|
|
1440
1092
|
}
|
|
1441
1093
|
function optionsForInvocation(pyfigOptions, spawnEnv) {
|
|
@@ -1443,41 +1095,29 @@ function optionsForInvocation(pyfigOptions, spawnEnv) {
|
|
|
1443
1095
|
}
|
|
1444
1096
|
function resolveSynchronously(target, options) {
|
|
1445
1097
|
const resolved = resolveOptions(options);
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
1455
|
-
windowsHide: true
|
|
1456
|
-
});
|
|
1457
|
-
if (result.status !== 0 || result.error) {
|
|
1458
|
-
const detail = result.stderr.trim() || result.error?.message || `exit ${result.status}`;
|
|
1459
|
-
throw new Error(`Could not resolve ${target} synchronously: ${detail}`);
|
|
1460
|
-
}
|
|
1461
|
-
try {
|
|
1462
|
-
return JSON.parse(result.stdout);
|
|
1463
|
-
} catch (error) {
|
|
1464
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1465
|
-
throw new Error(`Could not parse synchronous ${target} resolution: ${detail}`);
|
|
1466
|
-
}
|
|
1098
|
+
const request = { target, options: resolved };
|
|
1099
|
+
const result = import_node_child_process2.spawnSync(process.execPath, [runtimeFile("sync-resolver")], {
|
|
1100
|
+
input: JSON.stringify(request),
|
|
1101
|
+
encoding: "utf8",
|
|
1102
|
+
env: resolved.env,
|
|
1103
|
+
timeout: synchronousResolutionTimeout(resolved),
|
|
1104
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
1105
|
+
windowsHide: true
|
|
1467
1106
|
});
|
|
1107
|
+
if (result.status !== 0 || result.error) {
|
|
1108
|
+
const detail = result.stderr.trim() || result.error?.message || `exit ${result.status}`;
|
|
1109
|
+
throw new Error(`Could not resolve ${target} synchronously: ${detail}`);
|
|
1110
|
+
}
|
|
1111
|
+
try {
|
|
1112
|
+
return JSON.parse(result.stdout);
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1115
|
+
throw new Error(`Could not parse synchronous ${target} resolution: ${detail}`);
|
|
1116
|
+
}
|
|
1468
1117
|
}
|
|
1469
1118
|
function synchronousResolutionTimeout(options) {
|
|
1470
1119
|
return options.coordinationTimeoutMs + options.downloadTimeoutMs + 2 * options.installTimeoutMs + 4 * options.probeTimeoutMs + 60000;
|
|
1471
1120
|
}
|
|
1472
|
-
function synchronousResolutionIsPresent(resolution) {
|
|
1473
|
-
if ("packageManager" in resolution) {
|
|
1474
|
-
return import_node_fs6.default.existsSync(resolution.python.executable) && import_node_fs6.default.existsSync(resolution.packageManager.executable);
|
|
1475
|
-
}
|
|
1476
|
-
if ("uv" in resolution) {
|
|
1477
|
-
return import_node_fs6.default.existsSync(resolution.uv.executable) && import_node_fs6.default.existsSync(resolution.python.executable);
|
|
1478
|
-
}
|
|
1479
|
-
return import_node_fs6.default.existsSync(resolution.executable);
|
|
1480
|
-
}
|
|
1481
1121
|
|
|
1482
1122
|
// src/client.ts
|
|
1483
1123
|
function createPyfig(options = {}) {
|
|
@@ -1485,18 +1125,15 @@ function createPyfig(options = {}) {
|
|
|
1485
1125
|
...options,
|
|
1486
1126
|
env: options.env ? { ...options.env } : undefined
|
|
1487
1127
|
};
|
|
1128
|
+
const baseEnvironment = import_loqkit2.memoize(() => ensureEnvironment(scopedOptions));
|
|
1129
|
+
const uvInfo = import_loqkit2.memoize(() => ensureUv(scopedOptions));
|
|
1488
1130
|
return {
|
|
1489
|
-
prepare: () => ensureEnvironment(scopedOptions),
|
|
1490
|
-
pythonInfo: () =>
|
|
1491
|
-
uvInfo
|
|
1492
|
-
python: (args = [], spawnOptions = {}) => pythonSpawn(args, spawnOptions, scopedOptions),
|
|
1493
|
-
pythonSync: (args = [], spawnOptions = {}) => pythonSpawnSync(args, spawnOptions, scopedOptions),
|
|
1494
|
-
pip: (args = [], spawnOptions = {}) => pipSpawn(args, spawnOptions, scopedOptions),
|
|
1495
|
-
pipSync: (args = [], spawnOptions = {}) => pipSpawnSync(args, spawnOptions, scopedOptions),
|
|
1131
|
+
prepare: (dependencies = [], dependencyOptions = {}) => dependencies.length === 0 && !dependencyOptions.allExtras && (dependencyOptions.extras?.length ?? 0) === 0 && (dependencyOptions.groups?.length ?? 0) === 0 ? baseEnvironment() : ensureEnvironment(scopedOptions, dependencies, dependencyOptions),
|
|
1132
|
+
pythonInfo: async () => (await baseEnvironment()).python,
|
|
1133
|
+
uvInfo,
|
|
1496
1134
|
uv: (args = [], spawnOptions = {}) => uvSpawn(args, spawnOptions, scopedOptions),
|
|
1497
|
-
uvSync: (args = [], spawnOptions = {}) => uvSpawnSync(args, spawnOptions, scopedOptions)
|
|
1498
|
-
clearCache: clearPyfigCaches
|
|
1135
|
+
uvSync: (args = [], spawnOptions = {}) => uvSpawnSync(args, spawnOptions, scopedOptions)
|
|
1499
1136
|
};
|
|
1500
1137
|
}
|
|
1501
1138
|
|
|
1502
|
-
//# debugId=
|
|
1139
|
+
//# debugId=5564427996CDDBA964756E2164756E21
|