pyfig 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -121
- package/SECURITY.md +1 -1
- package/dist/cjs/index.js +469 -871
- package/dist/cjs/index.js.map +14 -15
- package/dist/cjs/sync-resolver.js +426 -657
- package/dist/cjs/sync-resolver.js.map +12 -12
- package/dist/cli.js +5 -8
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +3 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +13 -11
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -51
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +5 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/package-target.d.ts +7 -11
- package/dist/package-target.d.ts.map +1 -1
- package/dist/package-target.js +41 -57
- package/dist/package-target.js.map +1 -1
- package/dist/probes.d.ts +0 -18
- package/dist/probes.d.ts.map +1 -1
- package/dist/probes.js +0 -70
- package/dist/probes.js.map +1 -1
- package/dist/process.d.ts +1 -1
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +2 -1
- package/dist/process.js.map +1 -1
- package/dist/project-root.d.ts +1 -1
- package/dist/project-root.d.ts.map +1 -1
- package/dist/project-root.js +3 -64
- package/dist/project-root.js.map +1 -1
- package/dist/provision-lock.d.ts +10 -0
- package/dist/provision-lock.d.ts.map +1 -0
- package/dist/provision-lock.js +24 -0
- package/dist/provision-lock.js.map +1 -0
- package/dist/provision.d.ts +18 -6
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +273 -228
- package/dist/provision.js.map +1 -1
- package/dist/runners.d.ts +3 -7
- package/dist/runners.d.ts.map +1 -1
- package/dist/runners.js +35 -115
- package/dist/runners.js.map +1 -1
- package/dist/sync-resolver.js +4 -13
- package/dist/sync-resolver.js.map +1 -1
- package/dist/virtual-environment.d.ts +3 -1
- package/dist/virtual-environment.d.ts.map +1 -1
- package/dist/virtual-environment.js +8 -1
- package/dist/virtual-environment.js.map +1 -1
- package/package.json +6 -8
- package/dist/coordination.d.ts +0 -25
- package/dist/coordination.d.ts.map +0 -1
- package/dist/coordination.js +0 -187
- package/dist/coordination.js.map +0 -1
- package/dist/state.d.ts +0 -10
- package/dist/state.d.ts.map +0 -1
- package/dist/state.js +0 -62
- package/dist/state.js.map +0 -1
package/dist/provision.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
3
|
-
import { chmod, copyFile, mkdir, mkdtemp, readdir, rename, rm, } from 'node:fs/promises';
|
|
3
|
+
import { chmod, copyFile, mkdir, mkdtemp, readdir, rename, rm, writeFile, } from 'node:fs/promises';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -9,209 +9,249 @@ import { pipeline } from 'node:stream/promises';
|
|
|
9
9
|
import extractZip from 'extract-zip';
|
|
10
10
|
import * as tar from 'tar';
|
|
11
11
|
import { EnvHttpProxyAgent, fetch } from 'undici';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { resolveOptions, uvEnvironment, } from './config.js';
|
|
13
|
+
import { withProvisioningLock } from './provision-lock.js';
|
|
14
|
+
import { sanitizeUvEnvironment, uvCommandEnvironment } from './package-target.js';
|
|
15
|
+
import { findUv, probePython, probeUv, pythonSatisfiesRequirements, } from './probes.js';
|
|
15
16
|
import { runAsync } from './process.js';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
export async function ensurePython(options = {}) {
|
|
19
|
-
const resolved = resolveOptions(options);
|
|
20
|
-
return memoizeAsync('python', configurationFingerprint(resolved), resolved.cacheTtlMs, pythonIsPresent, () => ensurePythonResolved(resolved));
|
|
21
|
-
}
|
|
22
|
-
export async function ensurePip(options = {}, python) {
|
|
23
|
-
const resolved = resolveOptions(options);
|
|
24
|
-
const selectedPython = python?.virtualEnvironment
|
|
25
|
-
? python
|
|
26
|
-
: await ensureVirtualEnvironmentResolved(resolved);
|
|
27
|
-
return ensurePipResolved(selectedPython, resolved);
|
|
28
|
-
}
|
|
29
|
-
export async function ensureVirtualEnvironment(options = {}) {
|
|
30
|
-
const resolved = resolveOptions(options);
|
|
31
|
-
return memoizeAsync('venv', configurationFingerprint(resolved), resolved.cacheTtlMs, (python) => pythonIsPresent(python) &&
|
|
32
|
-
Boolean(python.virtualEnvironment) &&
|
|
33
|
-
pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(resolved.projectRoot)), () => ensureVirtualEnvironmentResolved(resolved));
|
|
34
|
-
}
|
|
17
|
+
import { canonicalPath, externalVirtualEnvironmentDirectories, pathsEqual, virtualEnvironmentDirectory, virtualEnvironmentPythonExecutable, } from './virtual-environment.js';
|
|
18
|
+
/** Ensure a compatible uv executable, preferring an existing installation. */
|
|
35
19
|
export async function ensureUv(options = {}) {
|
|
36
|
-
|
|
37
|
-
return memoizeAsync('uv', configurationFingerprint(resolved), resolved.cacheTtlMs, (uv) => fs.existsSync(uv.executable), () => ensureUvResolved(resolved));
|
|
20
|
+
return ensureUvResolved(resolveOptions(options));
|
|
38
21
|
}
|
|
39
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Select a compatible environment and optionally add runtime requirements.
|
|
24
|
+
* Project dependency files are inputs only and are never changed.
|
|
25
|
+
*/
|
|
26
|
+
export async function ensureEnvironment(options = {}, requirements = [], dependencyOptions = {}) {
|
|
40
27
|
const resolved = resolveOptions(options);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
28
|
+
const normalized = normalizeRequirements(requirements);
|
|
29
|
+
const selections = normalizeDependencyOptions(dependencyOptions);
|
|
30
|
+
const uv = await ensureUvResolved(resolved);
|
|
31
|
+
const python = await ensureVirtualEnvironmentResolved(resolved, uv);
|
|
32
|
+
if (normalized.length > 0 || selections.extras.length > 0 ||
|
|
33
|
+
selections.groups.length > 0 || selections.allExtras) {
|
|
34
|
+
await installRuntimeRequirements(uv, python, normalized, selections, resolved);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
python,
|
|
38
|
+
uv,
|
|
39
|
+
managed: isManagedEnvironment(python, resolved),
|
|
40
|
+
};
|
|
46
41
|
}
|
|
47
|
-
async function ensureVirtualEnvironmentResolved(options) {
|
|
42
|
+
async function ensureVirtualEnvironmentResolved(options, uv) {
|
|
43
|
+
const existing = await findReusableEnvironment(options);
|
|
44
|
+
if (existing)
|
|
45
|
+
return existing;
|
|
48
46
|
const directory = virtualEnvironmentDirectory(options.projectRoot);
|
|
49
47
|
const check = async () => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return existing;
|
|
56
|
-
}
|
|
57
|
-
throw incompatibleVirtualEnvironmentError(directory, existing, options);
|
|
48
|
+
const candidate = await inspectVirtualEnvironment(directory, options);
|
|
49
|
+
return candidate && pythonSatisfiesRequirements(candidate, options) &&
|
|
50
|
+
await virtualEnvironmentMatchesConfiguredPython(candidate, options)
|
|
51
|
+
? candidate
|
|
52
|
+
: null;
|
|
58
53
|
};
|
|
59
|
-
const existing = await check();
|
|
60
|
-
if (existing)
|
|
61
|
-
return existing;
|
|
62
54
|
if (!options.createVirtualEnvironment) {
|
|
63
|
-
throw new Error(`No
|
|
64
|
-
|
|
55
|
+
throw new Error(`No compatible environment is available and creation is disabled. ` +
|
|
56
|
+
`Pyfig would create ${directory}.`);
|
|
65
57
|
}
|
|
66
|
-
|
|
67
|
-
// order Python -> uv -> venv consistent across callers.
|
|
68
|
-
const basePython = await ensurePythonResolved(options);
|
|
69
|
-
const preferredUv = await findAvailableUv(options) ??
|
|
70
|
-
(options.installUv ? await ensureUvResolved(options) : null);
|
|
58
|
+
const basePython = await ensureBasePythonResolved(uv, options);
|
|
71
59
|
return withProvisioningLock({
|
|
72
60
|
key: `venv:${canonicalPath(directory)}`,
|
|
73
|
-
target: directory,
|
|
74
61
|
timeoutMs: options.coordinationTimeoutMs,
|
|
75
|
-
env: options.env,
|
|
76
62
|
check,
|
|
77
|
-
mutate: async (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
63
|
+
mutate: async () => {
|
|
64
|
+
// Only Pyfig's private path is replaceable. Active and repository-root
|
|
65
|
+
// environments are preserved when incompatible.
|
|
66
|
+
await rm(directory, { recursive: true, force: true });
|
|
67
|
+
await mkdir(path.dirname(directory), { recursive: true });
|
|
68
|
+
await runAsync(uv.executable, [
|
|
69
|
+
'venv',
|
|
70
|
+
'--no-project',
|
|
71
|
+
'--no-config',
|
|
72
|
+
'--no-python-downloads',
|
|
73
|
+
'--python',
|
|
74
|
+
basePython.executable,
|
|
75
|
+
directory,
|
|
76
|
+
], isolatedUvEnvironment(options), options.installTimeoutMs);
|
|
77
|
+
const installed = await check();
|
|
78
|
+
if (!installed) {
|
|
79
|
+
throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
|
|
87
80
|
}
|
|
88
81
|
return installed;
|
|
89
82
|
},
|
|
90
83
|
});
|
|
91
84
|
}
|
|
92
|
-
async function
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// A usable Python may still provide its standard-library venv module.
|
|
85
|
+
async function findReusableEnvironment(options) {
|
|
86
|
+
if (options.python) {
|
|
87
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
88
|
+
if (!configured || !pythonSatisfiesRequirements(configured, options)) {
|
|
89
|
+
throw incompatibleConfiguredPython(options);
|
|
100
90
|
}
|
|
91
|
+
if (configured.virtualEnvironment)
|
|
92
|
+
return configured;
|
|
101
93
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
94
|
+
for (const directory of [
|
|
95
|
+
...externalVirtualEnvironmentDirectories(options.projectRoot, options.env),
|
|
96
|
+
virtualEnvironmentDirectory(options.projectRoot),
|
|
97
|
+
]) {
|
|
98
|
+
const info = await inspectVirtualEnvironment(directory, options);
|
|
99
|
+
if (info && pythonSatisfiesRequirements(info, options) &&
|
|
100
|
+
await virtualEnvironmentMatchesConfiguredPython(info, options)) {
|
|
101
|
+
return info;
|
|
102
|
+
}
|
|
109
103
|
}
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
async function createVirtualEnvironmentWithUv(uv, directory, basePython, options, signal) {
|
|
113
|
-
await runAsync(uv.executable, [
|
|
114
|
-
'venv',
|
|
115
|
-
'--no-project',
|
|
116
|
-
'--no-config',
|
|
117
|
-
'--no-python-downloads',
|
|
118
|
-
'--allow-existing',
|
|
119
|
-
'--python',
|
|
120
|
-
basePython.executable,
|
|
121
|
-
directory,
|
|
122
|
-
], uvEnvironment(options), options.installTimeoutMs, signal);
|
|
123
|
-
}
|
|
124
|
-
async function inspectVirtualEnvironment(directory, options) {
|
|
125
|
-
const executable = virtualEnvironmentPythonExecutable(directory);
|
|
126
|
-
const info = await probePython({ executable, args: [] }, options);
|
|
127
|
-
return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
|
|
104
|
+
return null;
|
|
128
105
|
}
|
|
129
|
-
async function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
106
|
+
async function ensureBasePythonResolved(uv, options) {
|
|
107
|
+
if (options.python) {
|
|
108
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
109
|
+
if (!configured || !pythonSatisfiesRequirements(configured, options)) {
|
|
110
|
+
throw incompatibleConfiguredPython(options);
|
|
111
|
+
}
|
|
112
|
+
if (!configured.virtualEnvironment)
|
|
113
|
+
return configured;
|
|
114
|
+
const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
|
|
115
|
+
if (base && pythonSatisfiesRequirements(base, options))
|
|
116
|
+
return base;
|
|
117
|
+
throw incompatibleConfiguredPython(options);
|
|
118
|
+
}
|
|
119
|
+
// Corporate and offline hosts get a strict system-only pass before Pyfig
|
|
120
|
+
// considers any already-managed interpreter or network installation.
|
|
121
|
+
const system = await findPythonWithUv(uv, options, 'system');
|
|
122
|
+
if (system)
|
|
123
|
+
return system;
|
|
124
|
+
const cachedManaged = await findPythonWithUv(uv, options, 'managed');
|
|
125
|
+
if (cachedManaged)
|
|
126
|
+
return cachedManaged;
|
|
133
127
|
if (!options.installPython) {
|
|
134
|
-
|
|
135
|
-
? ' with compatible CPython development headers, architecture, and a linkable library'
|
|
136
|
-
: '';
|
|
137
|
-
throw new Error(`No Python interpreter satisfies ${options.pythonRange}${capability}; ` +
|
|
138
|
-
'managed Python installation is disabled.');
|
|
128
|
+
throw new Error(missingPythonMessage(options, 'managed Python installation is disabled'));
|
|
139
129
|
}
|
|
140
|
-
const lockTarget = cacheLockTarget(options.cacheDir, 'python');
|
|
141
130
|
return withProvisioningLock({
|
|
142
|
-
key: `python:${canonicalPath(
|
|
143
|
-
target: lockTarget,
|
|
131
|
+
key: `python:${canonicalPath(path.join(options.cacheDir, 'python'))}`,
|
|
144
132
|
timeoutMs: options.coordinationTimeoutMs,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const uvEnv = uvEnvironment(options);
|
|
150
|
-
await runAsync(uv.executable, ['python', 'install', '--no-config', options.managedPython], uvEnv, options.installTimeoutMs, signal);
|
|
151
|
-
const installed = await findPythonWithUv(uv, options, signal);
|
|
133
|
+
check: () => findPythonWithUv(uv, options, 'managed'),
|
|
134
|
+
mutate: async () => {
|
|
135
|
+
await runAsync(uv.executable, ['python', 'install', '--no-config', options.managedPython], isolatedUvEnvironment(options), options.installTimeoutMs);
|
|
136
|
+
const installed = await findPythonWithUv(uv, options, 'managed');
|
|
152
137
|
if (!installed) {
|
|
153
|
-
throw new Error(`uv installed
|
|
154
|
-
'runtime capabilities.');
|
|
138
|
+
throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
|
|
155
139
|
}
|
|
156
140
|
return installed;
|
|
157
141
|
},
|
|
158
142
|
});
|
|
159
143
|
}
|
|
160
|
-
async function
|
|
161
|
-
|
|
162
|
-
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
163
|
-
if (configured && pythonSatisfiesRequirements(configured, options))
|
|
164
|
-
return configured;
|
|
165
|
-
throw new Error(`Configured Python ${options.python} is unavailable or does not satisfy ${options.pythonRange}.`);
|
|
166
|
-
}
|
|
167
|
-
const existingUv = await findAvailableUv(options);
|
|
168
|
-
if (existingUv) {
|
|
169
|
-
const uvPython = await findPythonWithUv(existingUv, options);
|
|
170
|
-
if (uvPython)
|
|
171
|
-
return uvPython;
|
|
172
|
-
}
|
|
173
|
-
return findPython({ ...options, python: '' });
|
|
174
|
-
}
|
|
175
|
-
async function findPythonWithUv(uv, options, signal) {
|
|
176
|
-
const requests = [undefined, options.managedPython];
|
|
177
|
-
for (const requestedPython of requests) {
|
|
144
|
+
async function findPythonWithUv(uv, options, source) {
|
|
145
|
+
for (const request of ['', options.managedPython]) {
|
|
178
146
|
const args = [
|
|
179
147
|
'python',
|
|
180
148
|
'find',
|
|
181
149
|
'--no-project',
|
|
182
150
|
'--no-config',
|
|
183
|
-
'--managed-python',
|
|
184
151
|
'--no-python-downloads',
|
|
152
|
+
source === 'system' ? '--system' : '--managed-python',
|
|
185
153
|
];
|
|
186
|
-
if (
|
|
187
|
-
args.push(
|
|
154
|
+
if (request)
|
|
155
|
+
args.push(request);
|
|
188
156
|
try {
|
|
189
|
-
const { stdout } = await runAsync(uv.executable, args,
|
|
157
|
+
const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
|
|
190
158
|
const info = await probePython({ executable: stdout.trim(), args: [] }, options);
|
|
191
159
|
if (info && pythonSatisfiesRequirements(info, options))
|
|
192
160
|
return info;
|
|
193
161
|
}
|
|
194
|
-
catch
|
|
195
|
-
|
|
196
|
-
throw signal.reason ?? error;
|
|
197
|
-
// Continue through the configured request and then normal system discovery.
|
|
162
|
+
catch {
|
|
163
|
+
// Continue through the generic and configured version requests.
|
|
198
164
|
}
|
|
199
165
|
}
|
|
166
|
+
// `uv python find` returns one match. That match can lack development
|
|
167
|
+
// headers even when another installed interpreter satisfies every requested
|
|
168
|
+
// capability, so inspect uv's complete installed-only inventory before
|
|
169
|
+
// moving from system Python to managed Python or from cache to download.
|
|
170
|
+
try {
|
|
171
|
+
const { stdout } = await runAsync(uv.executable, [
|
|
172
|
+
'python',
|
|
173
|
+
'list',
|
|
174
|
+
'--no-config',
|
|
175
|
+
'--no-python-downloads',
|
|
176
|
+
'--only-installed',
|
|
177
|
+
'--output-format',
|
|
178
|
+
'json',
|
|
179
|
+
source === 'system' ? '--no-managed-python' : '--managed-python',
|
|
180
|
+
], explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
|
|
181
|
+
const candidates = installedPythonPaths(stdout);
|
|
182
|
+
for (const executable of candidates) {
|
|
183
|
+
const info = await probePython({ executable, args: [] }, options);
|
|
184
|
+
if (info && !info.virtualEnvironment && pythonSatisfiesRequirements(info, options)) {
|
|
185
|
+
return info;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Older compatible uv releases can still use the find-based fallback.
|
|
191
|
+
}
|
|
200
192
|
return null;
|
|
201
193
|
}
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
205
|
-
return
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
194
|
+
function installedPythonPaths(output) {
|
|
195
|
+
const parsed = JSON.parse(output);
|
|
196
|
+
if (!Array.isArray(parsed))
|
|
197
|
+
return [];
|
|
198
|
+
const paths = parsed.flatMap((entry) => {
|
|
199
|
+
if (!entry || typeof entry !== 'object' || !('path' in entry))
|
|
200
|
+
return [];
|
|
201
|
+
const candidate = entry.path;
|
|
202
|
+
return typeof candidate === 'string' && candidate ? [candidate] : [];
|
|
203
|
+
});
|
|
204
|
+
return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
|
|
205
|
+
}
|
|
206
|
+
async function inspectVirtualEnvironment(directory, options) {
|
|
207
|
+
const executable = virtualEnvironmentPythonExecutable(directory);
|
|
208
|
+
const info = await probePython({ executable, args: [] }, options);
|
|
209
|
+
return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
|
|
210
|
+
}
|
|
211
|
+
async function installRuntimeRequirements(uv, python, requirements, dependencyOptions, options) {
|
|
212
|
+
if (!python.virtualEnvironment) {
|
|
213
|
+
throw new Error('Runtime dependency installation requires a virtual environment.');
|
|
214
|
+
}
|
|
215
|
+
await withProvisioningLock({
|
|
216
|
+
key: `dependencies:${canonicalPath(python.virtualEnvironment)}`,
|
|
217
|
+
timeoutMs: options.coordinationTimeoutMs,
|
|
218
|
+
check: async () => null,
|
|
219
|
+
mutate: async () => {
|
|
220
|
+
const temporary = await mkdtemp(path.join(os.tmpdir(), 'pyfig-requirements-'));
|
|
221
|
+
try {
|
|
222
|
+
const additions = path.join(temporary, 'runtime-requirements.txt');
|
|
223
|
+
const resolved = path.join(temporary, 'resolved-requirements.txt');
|
|
224
|
+
const pyproject = path.join(options.projectRoot, 'pyproject.toml');
|
|
225
|
+
const hasPyproject = fs.existsSync(pyproject);
|
|
226
|
+
if (!hasPyproject && (dependencyOptions.extras.length > 0 ||
|
|
227
|
+
dependencyOptions.groups.length > 0 ||
|
|
228
|
+
dependencyOptions.allExtras)) {
|
|
229
|
+
throw new Error('Runtime extras and groups require an existing pyproject.toml.');
|
|
230
|
+
}
|
|
231
|
+
const sources = hasPyproject ? [pyproject] : [];
|
|
232
|
+
if (requirements.length > 0) {
|
|
233
|
+
await writeFile(additions, `${requirements.join('\n')}\n`, 'utf8');
|
|
234
|
+
sources.push(additions);
|
|
235
|
+
}
|
|
236
|
+
const selectionArgs = [
|
|
237
|
+
...dependencyOptions.extras.flatMap((extra) => ['--extra', extra]),
|
|
238
|
+
...dependencyOptions.groups.flatMap((group) => ['--group', group]),
|
|
239
|
+
...(dependencyOptions.allExtras ? ['--all-extras'] : []),
|
|
240
|
+
];
|
|
241
|
+
await runAsync(uv.executable, [
|
|
242
|
+
'pip', 'compile', '--no-config', '--python', python.executable, ...selectionArgs,
|
|
243
|
+
...sources, '--output-file', resolved,
|
|
244
|
+
], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
245
|
+
// install preserves unrelated packages; sync would remove anything not
|
|
246
|
+
// represented in this temporary resolution.
|
|
247
|
+
await runAsync(uv.executable, ['pip', 'install', '--no-config', '--python', python.executable, '-r', resolved], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await rm(temporary, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
},
|
|
254
|
+
});
|
|
215
255
|
}
|
|
216
256
|
async function ensureUvResolved(options) {
|
|
217
257
|
const existing = await findAvailableUv(options);
|
|
@@ -222,15 +262,12 @@ async function ensureUvResolved(options) {
|
|
|
222
262
|
}
|
|
223
263
|
const installDir = uvInstallDirectory(options);
|
|
224
264
|
const executable = uvExecutable(options);
|
|
225
|
-
const lockTarget = cacheLockTarget(options.cacheDir, 'uv');
|
|
226
265
|
return withProvisioningLock({
|
|
227
|
-
key: `uv:${canonicalPath(
|
|
228
|
-
target: lockTarget,
|
|
266
|
+
key: `uv:${canonicalPath(installDir)}`,
|
|
229
267
|
timeoutMs: options.coordinationTimeoutMs,
|
|
230
|
-
env: options.env,
|
|
231
268
|
check: () => findAvailableUv(options),
|
|
232
|
-
mutate: async (
|
|
233
|
-
await installUv(installDir, options
|
|
269
|
+
mutate: async () => {
|
|
270
|
+
await installUv(installDir, options);
|
|
234
271
|
const result = probeUv(executable, options);
|
|
235
272
|
if (!result || result.version !== options.uvVersion) {
|
|
236
273
|
throw new Error(`Expected managed uv ${options.uvVersion}, but the installed executable did not match.`);
|
|
@@ -240,63 +277,102 @@ async function ensureUvResolved(options) {
|
|
|
240
277
|
});
|
|
241
278
|
}
|
|
242
279
|
async function findAvailableUv(options) {
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
return
|
|
280
|
+
const external = await findUv(options);
|
|
281
|
+
if (external)
|
|
282
|
+
return external;
|
|
246
283
|
const managed = probeUv(uvExecutable(options), options);
|
|
247
284
|
return managed?.version === options.uvVersion ? managed : null;
|
|
248
285
|
}
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
286
|
+
function normalizeRequirements(requirements) {
|
|
287
|
+
const normalized = requirements.map((requirement) => requirement.trim());
|
|
288
|
+
for (const requirement of normalized) {
|
|
289
|
+
if (!requirement || /[\0\r\n]/u.test(requirement)) {
|
|
290
|
+
throw new TypeError(`Invalid runtime requirement: ${JSON.stringify(requirement)}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return [...new Set(normalized)];
|
|
254
294
|
}
|
|
255
|
-
function
|
|
256
|
-
|
|
257
|
-
|
|
295
|
+
function normalizeDependencyOptions(options) {
|
|
296
|
+
const extras = normalizeNames('extra', options.extras ?? []);
|
|
297
|
+
const groups = normalizeNames('dependency group', options.groups ?? []);
|
|
298
|
+
if (options.allExtras && extras.length > 0) {
|
|
299
|
+
throw new TypeError('allExtras cannot be combined with explicit extras.');
|
|
258
300
|
}
|
|
259
|
-
return {
|
|
260
|
-
kind: 'uv',
|
|
261
|
-
executable: uv.executable,
|
|
262
|
-
prefixArgs: ['pip'],
|
|
263
|
-
version: uv.version,
|
|
264
|
-
pythonExecutable: python.executable,
|
|
265
|
-
virtualEnvironment: python.virtualEnvironment,
|
|
266
|
-
};
|
|
301
|
+
return { extras, groups, allExtras: options.allExtras ?? false };
|
|
267
302
|
}
|
|
268
|
-
function
|
|
269
|
-
const
|
|
270
|
-
for (const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
normalized === 'PYTHONHOME') {
|
|
274
|
-
delete result[key];
|
|
303
|
+
function normalizeNames(kind, names) {
|
|
304
|
+
const normalized = names.map((name) => name.trim());
|
|
305
|
+
for (const name of normalized) {
|
|
306
|
+
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
|
|
307
|
+
throw new TypeError(`Invalid ${kind}: ${JSON.stringify(name)}`);
|
|
275
308
|
}
|
|
276
309
|
}
|
|
277
|
-
return
|
|
310
|
+
return [...new Set(normalized)];
|
|
311
|
+
}
|
|
312
|
+
function isManagedEnvironment(python, options) {
|
|
313
|
+
return Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(options.projectRoot));
|
|
314
|
+
}
|
|
315
|
+
function isolatedUvEnvironment(options) {
|
|
316
|
+
return sanitizeUvEnvironment(uvEnvironment(options));
|
|
317
|
+
}
|
|
318
|
+
/** Explicit --system/--managed-python selectors cannot be combined with uv's preference setting. */
|
|
319
|
+
function explicitPythonSourceEnvironment(options) {
|
|
320
|
+
const env = isolatedUvEnvironment(options);
|
|
321
|
+
delete env['UV_PYTHON_PREFERENCE'];
|
|
322
|
+
return env;
|
|
323
|
+
}
|
|
324
|
+
function targetUvEnvironment(options, python) {
|
|
325
|
+
return uvCommandEnvironment(uvEnvironment(options), python);
|
|
326
|
+
}
|
|
327
|
+
function incompatibleConfiguredPython(options) {
|
|
328
|
+
return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` +
|
|
329
|
+
`${options.pythonRange}${options.requireEmbedding ? ' with CPython embedding files' : ''}.`);
|
|
330
|
+
}
|
|
331
|
+
function missingPythonMessage(options, reason) {
|
|
332
|
+
const embedding = options.requireEmbedding
|
|
333
|
+
? ' with compatible CPython headers, library, architecture, and GIL ABI'
|
|
334
|
+
: '';
|
|
335
|
+
return `No Python satisfies ${options.pythonRange}${embedding}; ${reason}.`;
|
|
278
336
|
}
|
|
279
|
-
async function
|
|
337
|
+
async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
|
|
338
|
+
if (!options.python)
|
|
339
|
+
return true;
|
|
340
|
+
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
341
|
+
if (!configured || !pythonSatisfiesRequirements(configured, options))
|
|
342
|
+
return false;
|
|
343
|
+
if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
const configuredBase = configured.virtualEnvironment
|
|
347
|
+
? configured.baseExecutable
|
|
348
|
+
: configured.executable;
|
|
349
|
+
return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
|
|
350
|
+
}
|
|
351
|
+
function uvInstallDirectory(options) {
|
|
352
|
+
return path.join(options.cacheDir, 'tools', 'uv', options.uvVersion);
|
|
353
|
+
}
|
|
354
|
+
function uvExecutable(options) {
|
|
355
|
+
return path.join(uvInstallDirectory(options), process.platform === 'win32' ? 'uv.exe' : 'uv');
|
|
356
|
+
}
|
|
357
|
+
async function installUv(installDir, options) {
|
|
280
358
|
const asset = uvAsset();
|
|
281
359
|
const baseUrl = `https://github.com/astral-sh/uv/releases/download/${options.uvVersion}`;
|
|
282
360
|
const temporary = await mkdtemp(path.join(os.tmpdir(), 'pyfig-uv-'));
|
|
283
361
|
const dispatcher = proxyDispatcher(options.env);
|
|
284
362
|
try {
|
|
285
363
|
const archive = path.join(temporary, asset);
|
|
286
|
-
await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs,
|
|
287
|
-
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs,
|
|
364
|
+
await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
|
|
365
|
+
const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
|
|
288
366
|
const actual = await sha256File(archive);
|
|
289
367
|
if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
|
|
290
368
|
throw new Error(`uv archive checksum mismatch for ${asset}`);
|
|
291
369
|
}
|
|
292
370
|
const extracted = path.join(temporary, 'extracted');
|
|
293
371
|
await mkdir(extracted, { recursive: true });
|
|
294
|
-
signal.throwIfAborted();
|
|
295
372
|
if (asset.endsWith('.zip'))
|
|
296
373
|
await extractZip(archive, { dir: extracted });
|
|
297
374
|
else
|
|
298
375
|
await tar.x({ file: archive, cwd: extracted, strict: true });
|
|
299
|
-
signal.throwIfAborted();
|
|
300
376
|
const source = await findFile(extracted, process.platform === 'win32' ? 'uv.exe' : 'uv');
|
|
301
377
|
if (!source)
|
|
302
378
|
throw new Error(`uv archive did not contain the expected executable: ${asset}`);
|
|
@@ -339,16 +415,15 @@ function uvAsset() {
|
|
|
339
415
|
return `uv-${architecture}-pc-windows-msvc.zip`;
|
|
340
416
|
if (process.platform === 'darwin')
|
|
341
417
|
return `uv-${architecture}-apple-darwin.tar.gz`;
|
|
342
|
-
// Astral's musl artifacts are fully static and run on both musl and glibc Linux.
|
|
343
418
|
if (process.platform === 'linux')
|
|
344
419
|
return `uv-${architecture}-unknown-linux-musl.tar.gz`;
|
|
345
420
|
throw new Error(`uv bootstrap does not support platform ${process.platform}.`);
|
|
346
421
|
}
|
|
347
|
-
async function download(url, destination, timeoutMs,
|
|
422
|
+
async function download(url, destination, timeoutMs, dispatcher) {
|
|
348
423
|
const response = await fetch(url, {
|
|
349
424
|
dispatcher,
|
|
350
425
|
redirect: 'follow',
|
|
351
|
-
signal:
|
|
426
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
352
427
|
});
|
|
353
428
|
if (!response.ok || !response.body) {
|
|
354
429
|
await response.body?.cancel();
|
|
@@ -356,11 +431,11 @@ async function download(url, destination, timeoutMs, externalSignal, dispatcher)
|
|
|
356
431
|
}
|
|
357
432
|
await pipeline(Readable.fromWeb(response.body), createWriteStream(destination, { flags: 'wx' }));
|
|
358
433
|
}
|
|
359
|
-
async function downloadText(url, timeoutMs,
|
|
434
|
+
async function downloadText(url, timeoutMs, dispatcher) {
|
|
360
435
|
const response = await fetch(url, {
|
|
361
436
|
dispatcher,
|
|
362
437
|
redirect: 'follow',
|
|
363
|
-
signal:
|
|
438
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
364
439
|
});
|
|
365
440
|
if (!response.ok) {
|
|
366
441
|
await response.body?.cancel();
|
|
@@ -382,9 +457,6 @@ function proxyDispatcher(env) {
|
|
|
382
457
|
function environmentValue(env, lowercase, uppercase) {
|
|
383
458
|
return env[lowercase] ?? env[uppercase];
|
|
384
459
|
}
|
|
385
|
-
function combinedSignal(timeoutMs, externalSignal) {
|
|
386
|
-
return AbortSignal.any([externalSignal, AbortSignal.timeout(timeoutMs)]);
|
|
387
|
-
}
|
|
388
460
|
async function sha256File(file) {
|
|
389
461
|
const hash = createHash('sha256');
|
|
390
462
|
for await (const chunk of createReadStream(file))
|
|
@@ -404,31 +476,4 @@ async function findFile(directory, name) {
|
|
|
404
476
|
}
|
|
405
477
|
return null;
|
|
406
478
|
}
|
|
407
|
-
function incompatibleVirtualEnvironmentError(directory, python, options) {
|
|
408
|
-
const capabilities = options.requireEmbedding
|
|
409
|
-
? ' or lacks compatible CPython embedding headers, library, or architecture'
|
|
410
|
-
: '';
|
|
411
|
-
return new Error(`The existing virtual environment at ${directory} uses Python ${python.version}, ` +
|
|
412
|
-
`which does not satisfy ${options.pythonRange}${capabilities}.`);
|
|
413
|
-
}
|
|
414
|
-
async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
|
|
415
|
-
if (!options.python)
|
|
416
|
-
return true;
|
|
417
|
-
const configured = await probePython({ executable: options.python, args: [] }, options);
|
|
418
|
-
if (!configured || !pythonSatisfiesRequirements(configured, options))
|
|
419
|
-
return false;
|
|
420
|
-
if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
const configuredBase = configured.virtualEnvironment
|
|
424
|
-
? configured.baseExecutable
|
|
425
|
-
: configured.executable;
|
|
426
|
-
return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
|
|
427
|
-
}
|
|
428
|
-
function pythonIsPresent(python) {
|
|
429
|
-
return fs.existsSync(python.executable);
|
|
430
|
-
}
|
|
431
|
-
function environmentIsPresent(environment) {
|
|
432
|
-
return pythonIsPresent(environment.python) && fs.existsSync(environment.packageManager.executable);
|
|
433
|
-
}
|
|
434
479
|
//# sourceMappingURL=provision.js.map
|