pyfig 0.6.3 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -166
- package/SECURITY.md +16 -5
- package/dist/cjs/index.js +471 -980
- package/dist/cjs/index.js.map +6 -15
- package/dist/index.d.ts +2 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/process.d.ts +6 -12
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +35 -54
- package/dist/process.js.map +1 -1
- package/dist/pyfig.d.ts +31 -0
- package/dist/pyfig.d.ts.map +1 -0
- package/dist/pyfig.js +275 -0
- package/dist/pyfig.js.map +1 -0
- package/dist/uv.d.ts +3 -0
- package/dist/uv.d.ts.map +1 -0
- package/dist/uv.js +423 -0
- package/dist/uv.js.map +1 -0
- package/package.json +6 -12
- package/dist/cjs/sync-resolver.js +0 -1217
- package/dist/cjs/sync-resolver.js.map +0 -19
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -48
- package/dist/cli.js.map +0 -1
- package/dist/client.d.ts +0 -15
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -25
- package/dist/client.js.map +0 -1
- package/dist/config.d.ts +0 -62
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -112
- package/dist/config.js.map +0 -1
- package/dist/package-target.d.ts +0 -8
- package/dist/package-target.d.ts.map +0 -1
- package/dist/package-target.js +0 -81
- package/dist/package-target.js.map +0 -1
- package/dist/pep440.d.ts +0 -12
- package/dist/pep440.d.ts.map +0 -1
- package/dist/pep440.js +0 -21
- package/dist/pep440.js.map +0 -1
- package/dist/probes.d.ts +0 -37
- package/dist/probes.d.ts.map +0 -1
- package/dist/probes.js +0 -145
- package/dist/probes.js.map +0 -1
- package/dist/project-root.d.ts +0 -3
- package/dist/project-root.d.ts.map +0 -1
- package/dist/project-root.js +0 -22
- package/dist/project-root.js.map +0 -1
- package/dist/provision-lock.d.ts +0 -10
- package/dist/provision-lock.d.ts.map +0 -1
- package/dist/provision-lock.js +0 -24
- package/dist/provision-lock.js.map +0 -1
- package/dist/provision.d.ts +0 -26
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -685
- package/dist/provision.js.map +0 -1
- package/dist/runners.d.ts +0 -14
- package/dist/runners.d.ts.map +0 -1
- package/dist/runners.js +0 -71
- package/dist/runners.js.map +0 -1
- package/dist/runtime-files.d.ts +0 -3
- package/dist/runtime-files.d.ts.map +0 -1
- package/dist/runtime-files.js +0 -12
- package/dist/runtime-files.js.map +0 -1
- package/dist/sync-resolver.d.ts +0 -2
- package/dist/sync-resolver.d.ts.map +0 -1
- package/dist/sync-resolver.js +0 -15
- package/dist/sync-resolver.js.map +0 -1
- package/dist/virtual-environment.d.ts +0 -11
- package/dist/virtual-environment.d.ts.map +0 -1
- package/dist/virtual-environment.js +0 -49
- package/dist/virtual-environment.js.map +0 -1
package/dist/provision.js
DELETED
|
@@ -1,685 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { createReadStream, createWriteStream } from 'node:fs';
|
|
3
|
-
import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile, } from 'node:fs/promises';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { Readable } from 'node:stream';
|
|
8
|
-
import { pipeline } from 'node:stream/promises';
|
|
9
|
-
import { unzipSync } from 'fflate';
|
|
10
|
-
import * as tar from 'tar';
|
|
11
|
-
import { EnvHttpProxyAgent, fetch } from 'undici';
|
|
12
|
-
import { resolveOptions, uvEnvironment, } from './config.js';
|
|
13
|
-
import { withProvisioningLock } from './provision-lock.js';
|
|
14
|
-
import { sanitizeUvEnvironment, uvCommandEnvironment } from './package-target.js';
|
|
15
|
-
import { maxPep440Version, normalizePep440Version, } from './pep440.js';
|
|
16
|
-
import { findUv, probePython, probeUv, pythonSatisfiesPolicy, } from './probes.js';
|
|
17
|
-
import { runAsync } from './process.js';
|
|
18
|
-
import { canonicalPath, externalVirtualEnvironmentDirectories, pathsEqual, virtualEnvironmentDirectory, virtualEnvironmentPythonExecutable, } from './virtual-environment.js';
|
|
19
|
-
/** Ensure a compatible uv executable, preferring an existing installation. */
|
|
20
|
-
export async function ensureUv(options = {}) {
|
|
21
|
-
return ensureUvResolved(resolveOptions(options));
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Select a compatible environment and optionally add runtime dependencies.
|
|
25
|
-
* Project dependency files are inputs only and are never changed.
|
|
26
|
-
*/
|
|
27
|
-
export async function ensureEnvironment(options = {}, dependencies = [], dependencyOptions = {}) {
|
|
28
|
-
const resolved = resolveOptions(options);
|
|
29
|
-
const normalized = normalizeDependencies(dependencies);
|
|
30
|
-
const selections = normalizeDependencyOptions(dependencyOptions);
|
|
31
|
-
const uv = await ensureUvResolved(resolved);
|
|
32
|
-
const python = await ensureVirtualEnvironmentResolved(resolved, uv);
|
|
33
|
-
if (normalized.length > 0 || selections.extras.length > 0 ||
|
|
34
|
-
selections.groups.length > 0 || selections.allExtras) {
|
|
35
|
-
await installRuntimeDependencies(uv, python, normalized, selections, resolved);
|
|
36
|
-
}
|
|
37
|
-
return {
|
|
38
|
-
python,
|
|
39
|
-
uv,
|
|
40
|
-
managed: isManagedEnvironment(python, resolved),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
async function ensureVirtualEnvironmentResolved(options, uv) {
|
|
44
|
-
const existing = await findReusableEnvironment(options);
|
|
45
|
-
if (existing)
|
|
46
|
-
return existing;
|
|
47
|
-
const directory = virtualEnvironmentDirectory(options.projectRoot);
|
|
48
|
-
const check = async () => {
|
|
49
|
-
const candidate = await inspectVirtualEnvironment(directory, options);
|
|
50
|
-
return candidate && pythonSatisfiesPolicy(candidate, options) &&
|
|
51
|
-
await virtualEnvironmentMatchesConfiguredPython(candidate, options)
|
|
52
|
-
? candidate
|
|
53
|
-
: null;
|
|
54
|
-
};
|
|
55
|
-
if (!options.createVirtualEnvironment) {
|
|
56
|
-
throw new Error(`No compatible environment is available and creation is disabled. ` +
|
|
57
|
-
`Pyfig would create ${directory}.`);
|
|
58
|
-
}
|
|
59
|
-
const basePython = await ensureBasePythonResolved(uv, options);
|
|
60
|
-
return withProvisioningLock({
|
|
61
|
-
key: `venv:${canonicalPath(directory)}`,
|
|
62
|
-
timeoutMs: options.coordinationTimeoutMs,
|
|
63
|
-
check,
|
|
64
|
-
mutate: async () => {
|
|
65
|
-
// Only Pyfig's private path is replaceable. Active and repository-root
|
|
66
|
-
// environments are preserved when incompatible.
|
|
67
|
-
await rm(directory, { recursive: true, force: true });
|
|
68
|
-
await mkdir(path.dirname(directory), { recursive: true });
|
|
69
|
-
await runAsync(uv.executable, [
|
|
70
|
-
'venv',
|
|
71
|
-
'--no-project',
|
|
72
|
-
'--no-config',
|
|
73
|
-
'--no-python-downloads',
|
|
74
|
-
'--python',
|
|
75
|
-
basePython.executable,
|
|
76
|
-
directory,
|
|
77
|
-
], isolatedUvEnvironment(options), options.installTimeoutMs);
|
|
78
|
-
const installed = await check();
|
|
79
|
-
if (!installed) {
|
|
80
|
-
throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
|
|
81
|
-
}
|
|
82
|
-
return installed;
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
async function findReusableEnvironment(options) {
|
|
87
|
-
if (options.python) {
|
|
88
|
-
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
89
|
-
if (!configured || !pythonSatisfiesPolicy(configured, options)) {
|
|
90
|
-
throw incompatibleConfiguredPython(options);
|
|
91
|
-
}
|
|
92
|
-
if (configured.virtualEnvironment)
|
|
93
|
-
return configured;
|
|
94
|
-
}
|
|
95
|
-
for (const directory of [
|
|
96
|
-
...externalVirtualEnvironmentDirectories(options.projectRoot, options.env),
|
|
97
|
-
virtualEnvironmentDirectory(options.projectRoot),
|
|
98
|
-
]) {
|
|
99
|
-
const info = await inspectVirtualEnvironment(directory, options);
|
|
100
|
-
if (info && pythonSatisfiesPolicy(info, options) &&
|
|
101
|
-
await virtualEnvironmentMatchesConfiguredPython(info, options)) {
|
|
102
|
-
return info;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
async function ensureBasePythonResolved(uv, options) {
|
|
108
|
-
if (options.python) {
|
|
109
|
-
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
110
|
-
if (!configured || !pythonSatisfiesPolicy(configured, options)) {
|
|
111
|
-
throw incompatibleConfiguredPython(options);
|
|
112
|
-
}
|
|
113
|
-
if (!configured.virtualEnvironment)
|
|
114
|
-
return configured;
|
|
115
|
-
const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
|
|
116
|
-
if (base && pythonSatisfiesPolicy(base, options))
|
|
117
|
-
return base;
|
|
118
|
-
throw incompatibleConfiguredPython(options);
|
|
119
|
-
}
|
|
120
|
-
// Corporate and offline hosts get a strict system-only pass before Pyfig
|
|
121
|
-
// considers any already-managed interpreter or network installation.
|
|
122
|
-
const system = await findPythonWithUv(uv, options, 'system');
|
|
123
|
-
if (system)
|
|
124
|
-
return system;
|
|
125
|
-
const cachedManaged = await findPythonWithUv(uv, options, 'managed');
|
|
126
|
-
if (cachedManaged)
|
|
127
|
-
return cachedManaged;
|
|
128
|
-
if (!options.installPython) {
|
|
129
|
-
throw new Error(missingPythonMessage(options, 'managed Python installation is disabled'));
|
|
130
|
-
}
|
|
131
|
-
return withProvisioningLock({
|
|
132
|
-
key: `python:${canonicalPath(path.join(options.cacheDir, 'python'))}`,
|
|
133
|
-
timeoutMs: options.coordinationTimeoutMs,
|
|
134
|
-
check: () => findPythonWithUv(uv, options, 'managed'),
|
|
135
|
-
mutate: async () => {
|
|
136
|
-
await runAsync(uv.executable, ['python', 'install', '--no-config', options.managedPython], isolatedUvEnvironment(options), options.installTimeoutMs);
|
|
137
|
-
const installed = await findPythonWithUv(uv, options, 'managed');
|
|
138
|
-
if (!installed) {
|
|
139
|
-
throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
|
|
140
|
-
}
|
|
141
|
-
return installed;
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
async function findPythonWithUv(uv, options, source) {
|
|
146
|
-
for (const request of ['', options.managedPython]) {
|
|
147
|
-
const args = [
|
|
148
|
-
'python',
|
|
149
|
-
'find',
|
|
150
|
-
'--no-project',
|
|
151
|
-
'--no-config',
|
|
152
|
-
'--no-python-downloads',
|
|
153
|
-
source === 'system' ? '--system' : '--managed-python',
|
|
154
|
-
];
|
|
155
|
-
if (request)
|
|
156
|
-
args.push(request);
|
|
157
|
-
try {
|
|
158
|
-
const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
|
|
159
|
-
const info = await probePython({ executable: stdout.trim(), args: [] }, options);
|
|
160
|
-
if (info && pythonSatisfiesPolicy(info, options))
|
|
161
|
-
return info;
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// Continue through the generic and configured version requests.
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// `uv python find` returns one match. That match can lack development
|
|
168
|
-
// headers even when another installed interpreter satisfies every requested
|
|
169
|
-
// capability, so inspect uv's complete installed-only inventory before
|
|
170
|
-
// moving from system Python to managed Python or from cache to download.
|
|
171
|
-
try {
|
|
172
|
-
const { stdout } = await runAsync(uv.executable, [
|
|
173
|
-
'python',
|
|
174
|
-
'list',
|
|
175
|
-
'--no-config',
|
|
176
|
-
'--no-python-downloads',
|
|
177
|
-
'--only-installed',
|
|
178
|
-
'--output-format',
|
|
179
|
-
'json',
|
|
180
|
-
source === 'system' ? '--no-managed-python' : '--managed-python',
|
|
181
|
-
], explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
|
|
182
|
-
const candidates = installedPythonPaths(stdout);
|
|
183
|
-
for (const executable of candidates) {
|
|
184
|
-
const info = await probePython({ executable, args: [] }, options);
|
|
185
|
-
if (info && !info.virtualEnvironment && pythonSatisfiesPolicy(info, options)) {
|
|
186
|
-
return info;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
// Older compatible uv releases can still use the find-based fallback.
|
|
192
|
-
}
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
function installedPythonPaths(output) {
|
|
196
|
-
const parsed = JSON.parse(output);
|
|
197
|
-
if (!Array.isArray(parsed))
|
|
198
|
-
return [];
|
|
199
|
-
const paths = parsed.flatMap((entry) => {
|
|
200
|
-
if (!entry || typeof entry !== 'object' || !('path' in entry))
|
|
201
|
-
return [];
|
|
202
|
-
const candidate = entry.path;
|
|
203
|
-
return typeof candidate === 'string' && candidate ? [candidate] : [];
|
|
204
|
-
});
|
|
205
|
-
return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
|
|
206
|
-
}
|
|
207
|
-
async function inspectVirtualEnvironment(directory, options) {
|
|
208
|
-
const executable = virtualEnvironmentPythonExecutable(directory);
|
|
209
|
-
const info = await probePython({ executable, args: [] }, options);
|
|
210
|
-
return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
|
|
211
|
-
}
|
|
212
|
-
async function installRuntimeDependencies(uv, python, dependencies, dependencyOptions, options) {
|
|
213
|
-
if (!python.virtualEnvironment) {
|
|
214
|
-
throw new Error('Runtime dependency installation requires a virtual environment.');
|
|
215
|
-
}
|
|
216
|
-
await withProvisioningLock({
|
|
217
|
-
key: `dependencies:${canonicalPath(python.virtualEnvironment)}`,
|
|
218
|
-
timeoutMs: options.coordinationTimeoutMs,
|
|
219
|
-
check: async () => null,
|
|
220
|
-
mutate: async () => {
|
|
221
|
-
const temporary = await mkdtemp(path.join(os.tmpdir(), 'pyfig-dependencies-'));
|
|
222
|
-
try {
|
|
223
|
-
const additions = path.join(temporary, 'runtime-dependencies.txt');
|
|
224
|
-
const resolved = path.join(temporary, 'resolved-dependencies.txt');
|
|
225
|
-
const pyproject = path.join(options.projectRoot, 'pyproject.toml');
|
|
226
|
-
const hasPyproject = fs.existsSync(pyproject);
|
|
227
|
-
if (!hasPyproject && (dependencyOptions.extras.length > 0 ||
|
|
228
|
-
dependencyOptions.groups.length > 0 ||
|
|
229
|
-
dependencyOptions.allExtras)) {
|
|
230
|
-
throw new Error('Runtime extras and groups require an existing pyproject.toml.');
|
|
231
|
-
}
|
|
232
|
-
const sources = hasPyproject ? [pyproject] : [];
|
|
233
|
-
if (dependencies.length > 0) {
|
|
234
|
-
await writeFile(additions, `${dependencies.join('\n')}\n`, 'utf8');
|
|
235
|
-
sources.push(additions);
|
|
236
|
-
}
|
|
237
|
-
const selectionArgs = [
|
|
238
|
-
...dependencyOptions.extras.flatMap((extra) => ['--extra', extra]),
|
|
239
|
-
...dependencyOptions.groups.flatMap((group) => ['--group', group]),
|
|
240
|
-
...(dependencyOptions.allExtras ? ['--all-extras'] : []),
|
|
241
|
-
];
|
|
242
|
-
await runAsync(uv.executable, [
|
|
243
|
-
'pip', 'compile', '--no-config', '--python', python.executable, ...selectionArgs,
|
|
244
|
-
...sources, '--output-file', resolved,
|
|
245
|
-
], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
246
|
-
// install preserves unrelated packages; sync would remove anything not
|
|
247
|
-
// represented in this temporary resolution.
|
|
248
|
-
await runAsync(uv.executable, ['pip', 'install', '--no-config', '--python', python.executable, '-r', resolved], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
249
|
-
}
|
|
250
|
-
finally {
|
|
251
|
-
await rm(temporary, { recursive: true, force: true });
|
|
252
|
-
}
|
|
253
|
-
return true;
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
async function ensureUvResolved(options) {
|
|
258
|
-
const existing = await findAvailableUv(options);
|
|
259
|
-
if (existing)
|
|
260
|
-
return existing;
|
|
261
|
-
if (!options.installUv) {
|
|
262
|
-
throw new Error(`Compatible uv is unavailable (requires-uv ${options.requiresUv}), and uv installation is disabled.`);
|
|
263
|
-
}
|
|
264
|
-
const policyKey = createHash('sha256').update(options.requiresUv).digest('hex');
|
|
265
|
-
return withProvisioningLock({
|
|
266
|
-
key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
|
|
267
|
-
timeoutMs: options.coordinationTimeoutMs,
|
|
268
|
-
check: () => findAvailableUv(options),
|
|
269
|
-
mutate: async () => {
|
|
270
|
-
const release = await resolveUvRelease(options);
|
|
271
|
-
const installDir = uvInstallDirectory(options, release.version);
|
|
272
|
-
const executable = uvExecutable(options, release.version);
|
|
273
|
-
return withProvisioningLock({
|
|
274
|
-
key: `uv-release:${canonicalPath(installDir)}`,
|
|
275
|
-
timeoutMs: options.coordinationTimeoutMs,
|
|
276
|
-
check: () => findAvailableUv(options),
|
|
277
|
-
mutate: async () => {
|
|
278
|
-
await installUv(installDir, release, options);
|
|
279
|
-
const result = probeUv(executable, options);
|
|
280
|
-
if (!result || result.version !== release.version) {
|
|
281
|
-
throw new Error(`Expected managed uv ${release.version}, but the installed executable did not match.`);
|
|
282
|
-
}
|
|
283
|
-
return result;
|
|
284
|
-
},
|
|
285
|
-
});
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
async function findAvailableUv(options) {
|
|
290
|
-
const external = await findUv(options);
|
|
291
|
-
if (external)
|
|
292
|
-
return external;
|
|
293
|
-
return findManagedUv(options);
|
|
294
|
-
}
|
|
295
|
-
async function findManagedUv(options) {
|
|
296
|
-
const entries = await readdir(uvInstallRoot(options), { withFileTypes: true }).catch(() => []);
|
|
297
|
-
let versions = entries
|
|
298
|
-
.filter(entry => entry.isDirectory() && normalizePep440Version(entry.name) === entry.name)
|
|
299
|
-
.map(entry => entry.name);
|
|
300
|
-
while (versions.length > 0) {
|
|
301
|
-
const selected = maxPep440Version(versions, options.requiresUv);
|
|
302
|
-
if (!selected)
|
|
303
|
-
return null;
|
|
304
|
-
const candidate = probeUv(uvExecutable(options, selected), options);
|
|
305
|
-
if (candidate?.version === selected)
|
|
306
|
-
return candidate;
|
|
307
|
-
versions = versions.filter(version => version !== selected);
|
|
308
|
-
}
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
function normalizeDependencies(dependencies) {
|
|
312
|
-
const normalized = dependencies.map((dependency) => dependency.trim());
|
|
313
|
-
for (const dependency of normalized) {
|
|
314
|
-
if (!dependency || /[\0\r\n]/u.test(dependency)) {
|
|
315
|
-
throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return [...new Set(normalized)];
|
|
319
|
-
}
|
|
320
|
-
function normalizeDependencyOptions(options) {
|
|
321
|
-
const extras = normalizeNames('extra', options.extras ?? []);
|
|
322
|
-
const groups = normalizeNames('dependency group', options.groups ?? []);
|
|
323
|
-
if (options.allExtras && extras.length > 0) {
|
|
324
|
-
throw new TypeError('allExtras cannot be combined with explicit extras.');
|
|
325
|
-
}
|
|
326
|
-
return { extras, groups, allExtras: options.allExtras ?? false };
|
|
327
|
-
}
|
|
328
|
-
function normalizeNames(kind, names) {
|
|
329
|
-
const normalized = names.map((name) => name.trim());
|
|
330
|
-
for (const name of normalized) {
|
|
331
|
-
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
|
|
332
|
-
throw new TypeError(`Invalid ${kind}: ${JSON.stringify(name)}`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return [...new Set(normalized)];
|
|
336
|
-
}
|
|
337
|
-
function isManagedEnvironment(python, options) {
|
|
338
|
-
return Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(options.projectRoot));
|
|
339
|
-
}
|
|
340
|
-
function isolatedUvEnvironment(options) {
|
|
341
|
-
return sanitizeUvEnvironment(uvEnvironment(options));
|
|
342
|
-
}
|
|
343
|
-
/** Explicit --system/--managed-python selectors cannot be combined with uv's preference setting. */
|
|
344
|
-
function explicitPythonSourceEnvironment(options) {
|
|
345
|
-
const env = isolatedUvEnvironment(options);
|
|
346
|
-
delete env['UV_PYTHON_PREFERENCE'];
|
|
347
|
-
return env;
|
|
348
|
-
}
|
|
349
|
-
function targetUvEnvironment(options, python) {
|
|
350
|
-
return uvCommandEnvironment(uvEnvironment(options), python);
|
|
351
|
-
}
|
|
352
|
-
function incompatibleConfiguredPython(options) {
|
|
353
|
-
return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` +
|
|
354
|
-
`requires-python ${options.requiresPython}` +
|
|
355
|
-
`${options.requireEmbedding ? ' with CPython embedding files' : ''}.`);
|
|
356
|
-
}
|
|
357
|
-
function missingPythonMessage(options, reason) {
|
|
358
|
-
const embedding = options.requireEmbedding
|
|
359
|
-
? ' with compatible CPython headers, library, architecture, and GIL ABI'
|
|
360
|
-
: '';
|
|
361
|
-
return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
|
|
362
|
-
}
|
|
363
|
-
async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
|
|
364
|
-
if (!options.python)
|
|
365
|
-
return true;
|
|
366
|
-
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
367
|
-
if (!configured || !pythonSatisfiesPolicy(configured, options))
|
|
368
|
-
return false;
|
|
369
|
-
if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
372
|
-
const configuredBase = configured.virtualEnvironment
|
|
373
|
-
? configured.baseExecutable
|
|
374
|
-
: configured.executable;
|
|
375
|
-
return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
|
|
376
|
-
}
|
|
377
|
-
const UV_RELEASE_PAGE_SIZE = 100;
|
|
378
|
-
const UV_RELEASE_MAX_PAGES = 20;
|
|
379
|
-
const UV_RELEASE_METADATA_TTL_MS = 24 * 60 * 60_000;
|
|
380
|
-
async function resolveUvRelease(options) {
|
|
381
|
-
const catalog = await ensureUvReleaseCatalog(options);
|
|
382
|
-
const asset = uvAsset();
|
|
383
|
-
const releases = catalog.releases.filter(release => release.assets.includes(asset) && release.assets.includes(`${asset}.sha256`));
|
|
384
|
-
const version = maxPep440Version(releases.map(release => release.version), options.requiresUv);
|
|
385
|
-
const release = releases.find(candidate => candidate.version === version);
|
|
386
|
-
if (!release) {
|
|
387
|
-
throw new Error(`No published uv release satisfies requires-uv ${options.requiresUv}.`);
|
|
388
|
-
}
|
|
389
|
-
return release;
|
|
390
|
-
}
|
|
391
|
-
async function ensureUvReleaseCatalog(options) {
|
|
392
|
-
const file = uvReleaseMetadataFile(options);
|
|
393
|
-
const stale = await readUvReleaseCatalog(file);
|
|
394
|
-
try {
|
|
395
|
-
return await withProvisioningLock({
|
|
396
|
-
key: `uv-metadata:${canonicalPath(file)}`,
|
|
397
|
-
timeoutMs: options.coordinationTimeoutMs,
|
|
398
|
-
check: () => readFreshUvReleaseCatalog(file),
|
|
399
|
-
mutate: async () => {
|
|
400
|
-
const catalog = await fetchUvReleaseCatalog(options);
|
|
401
|
-
await writeUvReleaseCatalog(file, catalog);
|
|
402
|
-
return catalog;
|
|
403
|
-
},
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
catch (error) {
|
|
407
|
-
const fallback = await readUvReleaseCatalog(file) ?? stale;
|
|
408
|
-
if (fallback)
|
|
409
|
-
return fallback;
|
|
410
|
-
throw error;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
async function fetchUvReleaseCatalog(options) {
|
|
414
|
-
const dispatcher = proxyDispatcher(options.env);
|
|
415
|
-
const releases = new Map();
|
|
416
|
-
let exhausted = false;
|
|
417
|
-
try {
|
|
418
|
-
for (let page = 1; page <= UV_RELEASE_MAX_PAGES; page += 1) {
|
|
419
|
-
const text = await downloadText(`https://api.github.com/repos/astral-sh/uv/releases?per_page=${UV_RELEASE_PAGE_SIZE}&page=${page}`, options.downloadTimeoutMs, dispatcher, githubApiHeaders(options.env));
|
|
420
|
-
const payload = JSON.parse(text);
|
|
421
|
-
if (!Array.isArray(payload)) {
|
|
422
|
-
throw new Error('GitHub returned invalid uv release metadata.');
|
|
423
|
-
}
|
|
424
|
-
for (const candidate of payload) {
|
|
425
|
-
if (!candidate || typeof candidate !== 'object')
|
|
426
|
-
continue;
|
|
427
|
-
const release = candidate;
|
|
428
|
-
if (release.draft === true || typeof release.tag_name !== 'string')
|
|
429
|
-
continue;
|
|
430
|
-
const assetNames = releaseAssetNames(release.assets);
|
|
431
|
-
const version = normalizePep440Version(release.tag_name);
|
|
432
|
-
if (version) {
|
|
433
|
-
releases.set(version, { assets: assetNames, tag: release.tag_name, version });
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
if (payload.length < UV_RELEASE_PAGE_SIZE) {
|
|
437
|
-
exhausted = true;
|
|
438
|
-
break;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
finally {
|
|
443
|
-
await dispatcher?.close();
|
|
444
|
-
}
|
|
445
|
-
if (!exhausted) {
|
|
446
|
-
throw new Error(`uv release catalog exceeded ${UV_RELEASE_MAX_PAGES} pages.`);
|
|
447
|
-
}
|
|
448
|
-
if (releases.size === 0) {
|
|
449
|
-
throw new Error('GitHub returned no usable uv releases.');
|
|
450
|
-
}
|
|
451
|
-
return { fetchedAt: Date.now(), releases: [...releases.values()] };
|
|
452
|
-
}
|
|
453
|
-
/** Build GitHub API headers without exposing credentials outside the request. */
|
|
454
|
-
export function githubApiHeaders(env) {
|
|
455
|
-
const token = [env['GITHUB_TOKEN'], env['GH_TOKEN']]
|
|
456
|
-
.find(value => typeof value === 'string' && value.trim().length > 0);
|
|
457
|
-
if (token && /[\r\n]/u.test(token)) {
|
|
458
|
-
throw new TypeError('GitHub API token must not contain line breaks.');
|
|
459
|
-
}
|
|
460
|
-
return {
|
|
461
|
-
Accept: 'application/vnd.github+json',
|
|
462
|
-
'User-Agent': 'pyfig',
|
|
463
|
-
'X-GitHub-Api-Version': '2022-11-28',
|
|
464
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
async function readFreshUvReleaseCatalog(file) {
|
|
468
|
-
const catalog = await readUvReleaseCatalog(file);
|
|
469
|
-
if (!catalog)
|
|
470
|
-
return null;
|
|
471
|
-
const age = Date.now() - catalog.fetchedAt;
|
|
472
|
-
return age >= 0 && age < UV_RELEASE_METADATA_TTL_MS ? catalog : null;
|
|
473
|
-
}
|
|
474
|
-
async function readUvReleaseCatalog(file) {
|
|
475
|
-
try {
|
|
476
|
-
return parseUvReleaseCatalog(JSON.parse(await readFile(file, 'utf8')));
|
|
477
|
-
}
|
|
478
|
-
catch {
|
|
479
|
-
return null;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
function parseUvReleaseCatalog(value) {
|
|
483
|
-
if (!value || typeof value !== 'object')
|
|
484
|
-
return null;
|
|
485
|
-
const catalog = value;
|
|
486
|
-
if (!Number.isSafeInteger(catalog.fetchedAt) || catalog.fetchedAt < 0 ||
|
|
487
|
-
!Array.isArray(catalog.releases)) {
|
|
488
|
-
return null;
|
|
489
|
-
}
|
|
490
|
-
const releases = [];
|
|
491
|
-
for (const value of catalog.releases) {
|
|
492
|
-
if (!value || typeof value !== 'object')
|
|
493
|
-
return null;
|
|
494
|
-
const release = value;
|
|
495
|
-
if (!Array.isArray(release.assets) ||
|
|
496
|
-
!release.assets.every(asset => typeof asset === 'string') ||
|
|
497
|
-
typeof release.tag !== 'string' ||
|
|
498
|
-
!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/u.test(release.tag) ||
|
|
499
|
-
typeof release.version !== 'string' ||
|
|
500
|
-
normalizePep440Version(release.version) !== release.version) {
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
releases.push({
|
|
504
|
-
assets: release.assets,
|
|
505
|
-
tag: release.tag,
|
|
506
|
-
version: release.version,
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
return { fetchedAt: catalog.fetchedAt, releases };
|
|
510
|
-
}
|
|
511
|
-
async function writeUvReleaseCatalog(file, catalog) {
|
|
512
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
513
|
-
const staged = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
|
|
514
|
-
try {
|
|
515
|
-
await writeFile(staged, `${JSON.stringify(catalog)}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
516
|
-
if (process.platform === 'win32')
|
|
517
|
-
await rm(file, { force: true });
|
|
518
|
-
await rename(staged, file);
|
|
519
|
-
}
|
|
520
|
-
finally {
|
|
521
|
-
await rm(staged, { force: true });
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
function uvReleaseMetadataFile(options) {
|
|
525
|
-
return path.join(options.cacheDir, 'metadata', 'uv-releases.json');
|
|
526
|
-
}
|
|
527
|
-
function releaseAssetNames(value) {
|
|
528
|
-
if (!Array.isArray(value))
|
|
529
|
-
return [];
|
|
530
|
-
return value.flatMap(entry => {
|
|
531
|
-
if (!entry || typeof entry !== 'object')
|
|
532
|
-
return [];
|
|
533
|
-
const name = entry.name;
|
|
534
|
-
return typeof name === 'string' ? [name] : [];
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
function uvInstallRoot(options) {
|
|
538
|
-
return path.join(options.cacheDir, 'tools', 'uv');
|
|
539
|
-
}
|
|
540
|
-
function uvInstallDirectory(options, version) {
|
|
541
|
-
return path.join(uvInstallRoot(options), version);
|
|
542
|
-
}
|
|
543
|
-
function uvExecutable(options, version) {
|
|
544
|
-
return path.join(uvInstallDirectory(options, version), process.platform === 'win32' ? 'uv.exe' : 'uv');
|
|
545
|
-
}
|
|
546
|
-
async function installUv(installDir, release, options) {
|
|
547
|
-
const asset = uvAsset();
|
|
548
|
-
const baseUrl = `https://github.com/astral-sh/uv/releases/download/${release.tag}`;
|
|
549
|
-
const temporary = await mkdtemp(path.join(os.tmpdir(), 'pyfig-uv-'));
|
|
550
|
-
const dispatcher = proxyDispatcher(options.env);
|
|
551
|
-
try {
|
|
552
|
-
const archive = path.join(temporary, asset);
|
|
553
|
-
await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
|
|
554
|
-
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
|
|
555
|
-
const actual = await sha256File(archive);
|
|
556
|
-
if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
|
|
557
|
-
throw new Error(`uv archive checksum mismatch for ${asset}`);
|
|
558
|
-
}
|
|
559
|
-
const extracted = path.join(temporary, 'extracted');
|
|
560
|
-
await mkdir(extracted, { recursive: true });
|
|
561
|
-
const source = await extractUvExecutable(archive, asset, extracted);
|
|
562
|
-
if (!source)
|
|
563
|
-
throw new Error(`uv archive did not contain the expected executable: ${asset}`);
|
|
564
|
-
await publishUvAtomically(source, installDir, release.version, options);
|
|
565
|
-
}
|
|
566
|
-
finally {
|
|
567
|
-
await dispatcher?.close();
|
|
568
|
-
await rm(temporary, { recursive: true, force: true });
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
async function extractUvExecutable(archive, asset, destination) {
|
|
572
|
-
const executableName = process.platform === 'win32' ? 'uv.exe' : 'uv';
|
|
573
|
-
if (!asset.endsWith('.zip')) {
|
|
574
|
-
await tar.x({ file: archive, cwd: destination, strict: true });
|
|
575
|
-
return findFile(destination, executableName);
|
|
576
|
-
}
|
|
577
|
-
// ZIP entry paths are never written to disk. Extract only uv's bytes and
|
|
578
|
-
// publish them at a path controlled by Pyfig, which prevents Zip Slip and
|
|
579
|
-
// symlink entries from redirecting writes outside the temporary directory.
|
|
580
|
-
const entries = unzipSync(await readFile(archive), {
|
|
581
|
-
filter: ({ name }) => path.posix.basename(name.replaceAll('\\', '/')) === executableName,
|
|
582
|
-
});
|
|
583
|
-
const matches = Object.entries(entries)
|
|
584
|
-
.filter(([name]) => path.posix.basename(name.replaceAll('\\', '/')) === executableName);
|
|
585
|
-
if (matches.length !== 1 || !matches[0])
|
|
586
|
-
return null;
|
|
587
|
-
const source = path.join(destination, executableName);
|
|
588
|
-
await writeFile(source, matches[0][1], { flag: 'wx' });
|
|
589
|
-
return source;
|
|
590
|
-
}
|
|
591
|
-
async function publishUvAtomically(source, installDir, version, options) {
|
|
592
|
-
await mkdir(installDir, { recursive: true });
|
|
593
|
-
const executable = uvExecutable(options, version);
|
|
594
|
-
const staged = path.join(installDir, `.${path.basename(executable)}.${process.pid}.${randomUUID()}.tmp`);
|
|
595
|
-
try {
|
|
596
|
-
await copyFile(source, staged);
|
|
597
|
-
if (process.platform !== 'win32')
|
|
598
|
-
await chmod(staged, 0o755);
|
|
599
|
-
const stagedUv = probeUv(staged, options);
|
|
600
|
-
if (!stagedUv || stagedUv.version !== version) {
|
|
601
|
-
throw new Error(`Downloaded uv executable is not version ${version}.`);
|
|
602
|
-
}
|
|
603
|
-
if (process.platform === 'win32')
|
|
604
|
-
await rm(executable, { force: true });
|
|
605
|
-
await rename(staged, executable);
|
|
606
|
-
}
|
|
607
|
-
finally {
|
|
608
|
-
await rm(staged, { force: true });
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
function uvAsset() {
|
|
612
|
-
const architecture = process.arch === 'x64'
|
|
613
|
-
? 'x86_64'
|
|
614
|
-
: process.arch === 'arm64'
|
|
615
|
-
? 'aarch64'
|
|
616
|
-
: null;
|
|
617
|
-
if (!architecture)
|
|
618
|
-
throw new Error(`uv bootstrap does not support architecture ${process.arch}.`);
|
|
619
|
-
if (process.platform === 'win32')
|
|
620
|
-
return `uv-${architecture}-pc-windows-msvc.zip`;
|
|
621
|
-
if (process.platform === 'darwin')
|
|
622
|
-
return `uv-${architecture}-apple-darwin.tar.gz`;
|
|
623
|
-
if (process.platform === 'linux')
|
|
624
|
-
return `uv-${architecture}-unknown-linux-musl.tar.gz`;
|
|
625
|
-
throw new Error(`uv bootstrap does not support platform ${process.platform}.`);
|
|
626
|
-
}
|
|
627
|
-
async function download(url, destination, timeoutMs, dispatcher) {
|
|
628
|
-
const response = await fetch(url, {
|
|
629
|
-
dispatcher,
|
|
630
|
-
redirect: 'follow',
|
|
631
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
632
|
-
});
|
|
633
|
-
if (!response.ok || !response.body) {
|
|
634
|
-
await response.body?.cancel();
|
|
635
|
-
throw new Error(`Could not download ${url}: HTTP ${response.status}`);
|
|
636
|
-
}
|
|
637
|
-
await pipeline(Readable.fromWeb(response.body), createWriteStream(destination, { flags: 'wx' }));
|
|
638
|
-
}
|
|
639
|
-
async function downloadText(url, timeoutMs, dispatcher, headers) {
|
|
640
|
-
const response = await fetch(url, {
|
|
641
|
-
dispatcher,
|
|
642
|
-
headers,
|
|
643
|
-
redirect: 'follow',
|
|
644
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
645
|
-
});
|
|
646
|
-
if (!response.ok) {
|
|
647
|
-
await response.body?.cancel();
|
|
648
|
-
throw new Error(`Could not download ${url}: HTTP ${response.status}`);
|
|
649
|
-
}
|
|
650
|
-
return response.text();
|
|
651
|
-
}
|
|
652
|
-
function proxyDispatcher(env) {
|
|
653
|
-
const httpProxy = environmentValue(env, 'http_proxy', 'HTTP_PROXY');
|
|
654
|
-
const httpsProxy = environmentValue(env, 'https_proxy', 'HTTPS_PROXY');
|
|
655
|
-
if (!httpProxy && !httpsProxy)
|
|
656
|
-
return undefined;
|
|
657
|
-
return new EnvHttpProxyAgent({
|
|
658
|
-
httpProxy,
|
|
659
|
-
httpsProxy,
|
|
660
|
-
noProxy: environmentValue(env, 'no_proxy', 'NO_PROXY'),
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
function environmentValue(env, lowercase, uppercase) {
|
|
664
|
-
return env[lowercase] ?? env[uppercase];
|
|
665
|
-
}
|
|
666
|
-
async function sha256File(file) {
|
|
667
|
-
const hash = createHash('sha256');
|
|
668
|
-
for await (const chunk of createReadStream(file))
|
|
669
|
-
hash.update(chunk);
|
|
670
|
-
return hash.digest('hex');
|
|
671
|
-
}
|
|
672
|
-
async function findFile(directory, name) {
|
|
673
|
-
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
674
|
-
const target = path.join(directory, entry.name);
|
|
675
|
-
if (entry.isFile() && entry.name === name)
|
|
676
|
-
return target;
|
|
677
|
-
if (entry.isDirectory()) {
|
|
678
|
-
const nested = await findFile(target, name);
|
|
679
|
-
if (nested)
|
|
680
|
-
return nested;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
return null;
|
|
684
|
-
}
|
|
685
|
-
//# sourceMappingURL=provision.js.map
|