pyfig 0.5.0 → 0.6.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 +6 -5
- package/SECURITY.md +1 -1
- package/dist/cjs/index.js +221 -73
- package/dist/cjs/index.js.map +7 -7
- package/dist/cjs/sync-resolver.js +220 -71
- package/dist/cjs/sync-resolver.js.map +8 -8
- package/dist/cli.js +2 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +5 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -29
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pep440.d.ts +12 -0
- package/dist/pep440.d.ts.map +1 -0
- package/dist/pep440.js +21 -0
- package/dist/pep440.js.map +1 -0
- package/dist/probes.d.ts.map +1 -1
- package/dist/probes.js +5 -6
- package/dist/probes.js.map +1 -1
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +203 -24
- package/dist/provision.js.map +1 -1
- package/package.json +4 -4
- package/dist/python-version.d.ts +0 -10
- package/dist/python-version.d.ts.map +0 -1
- package/dist/python-version.js +0 -17
- package/dist/python-version.js.map +0 -1
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ import { createPyfig } from 'pyfig';
|
|
|
17
17
|
|
|
18
18
|
const pyfig = createPyfig({
|
|
19
19
|
requiresPython: '>=3.11,<4',
|
|
20
|
+
requiresUv: '>=0.10,<1',
|
|
20
21
|
requireEmbedding: true,
|
|
21
22
|
requireGil: true,
|
|
22
23
|
});
|
|
@@ -136,15 +137,14 @@ The package exposes matching ESM and CommonJS entry points. Probe, download, loc
|
|
|
136
137
|
|
|
137
138
|
Precedence is API or CLI option, environment variable, then default.
|
|
138
139
|
|
|
139
|
-
`requiresPython`
|
|
140
|
+
`requiresPython` and `requiresUv` use PEP 440 specifiers. Comma-separated clauses, compatible releases (`~=`), exclusions (`!=`), wildcard equality, arbitrary equality (`===`), and pre-release versions are supported. For example: `requiresPython: '>=3.10,!=3.12.*,<4'` and `requiresUv: '==0.13.*'`. Pyfig reuses a compatible installed or cached uv first. When installation is necessary, it resolves the greatest published uv release satisfying `requiresUv` and containing the required platform assets.
|
|
140
141
|
|
|
141
142
|
| API option | Environment variable | Default |
|
|
142
143
|
| --- | --- | --- |
|
|
143
144
|
| `python` | `PYFIG_PYTHON` | uv discovery |
|
|
144
145
|
| `requiresPython` | `PYFIG_REQUIRES_PYTHON` | `>=3.10,<4` |
|
|
145
146
|
| `managedPython` | `PYFIG_MANAGED_PYTHON` | `3.12` |
|
|
146
|
-
| `
|
|
147
|
-
| `uvRange` | `PYFIG_UV_RANGE` | `>=0.10.0 <1.0.0` |
|
|
147
|
+
| `requiresUv` | `PYFIG_REQUIRES_UV` | `>=0.10,<1` |
|
|
148
148
|
| `cacheDir` | `PYFIG_HOME` | OS cache directory |
|
|
149
149
|
| `projectRoot` | `PYFIG_PROJECT_ROOT` | `INIT_CWD`, Git root, or current directory |
|
|
150
150
|
| `requireEmbedding` | `PYFIG_REQUIRE_EMBEDDING` | `false` |
|
|
@@ -166,6 +166,7 @@ Set `PYFIG_UV` to choose an explicit uv executable. Set `PYFIG_AUTO_INSTALL=0` o
|
|
|
166
166
|
bunx --bun pyfig
|
|
167
167
|
bunx --bun pyfig --check
|
|
168
168
|
bunx --bun pyfig --requires-python '>=3.12,<3.14'
|
|
169
|
+
bunx --bun pyfig --requires-uv '>=0.12,<1'
|
|
169
170
|
bunx --bun pyfig --require-embedding --require-gil
|
|
170
171
|
bunx --bun pyfig --project-root /path/to/repository
|
|
171
172
|
bunx --bun pyfig --json
|
|
@@ -175,9 +176,9 @@ bunx --bun pyfig --json
|
|
|
175
176
|
|
|
176
177
|
## Concurrency and installation safety
|
|
177
178
|
|
|
178
|
-
Pyfig
|
|
179
|
+
Pyfig resolves a compatible uv release from Astral's GitHub releases, downloads that exact archive over HTTPS, verifies its adjacent SHA-256 checksum, validates the staged executable version, and atomically publishes it in Pyfig's cache. A compatible cached executable is probed locally without a network request. Release metadata is cached on disk for 24 hours; stale metadata is refreshed under a Loqkit lock and remains available as a fallback when GitHub's API is temporarily unavailable. Pyfig does not execute installer scripts or modify shell profiles.
|
|
179
180
|
|
|
180
|
-
Provisioning mutations use Loqkit's layered in-memory, worker, native-file-lock-when-available, and portable-file coordination with capability checks before and after acquisition. uv supplies its own target-environment lock for installs. Base client preparation is memoized; dependency preparation is intentionally rerun so uv can cheaply verify current inputs through its shared cache.
|
|
181
|
+
Provisioning mutations use Loqkit's layered in-memory, worker, native-file-lock-when-available, and portable-file coordination with capability checks before and after acquisition. Compatible managed uv executables are cached by resolved version; Loqkit coordinates both the `requiresUv` policy lookup and exact release publication. uv supplies its own target-environment lock for installs. Base client preparation is memoized; dependency preparation is intentionally rerun so uv can cheaply verify current inputs through its shared cache.
|
|
181
182
|
|
|
182
183
|
## Platform support
|
|
183
184
|
|
package/SECURITY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Pyfig can download Astral uv release archives and can ask uv to download Python distributions. Automatic installation is enabled by default; set `autoInstall: false`, pass `--check`, or set `PYFIG_AUTO_INSTALL=0` when downloads are not allowed.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Pyfig resolves a uv release allowed by `requiresUv`, then fetches that exact release URL over HTTPS with bounded redirects and timeouts. GitHub release metadata is cached for 24 hours under Loqkit coordination, with validated stale metadata available if refresh fails. Pyfig verifies the adjacent SHA-256 checksum, validates the staged executable's reported version, and atomically publishes it into its cache. It never executes an installer script or modifies shell profiles or the host `PATH`. Proxy variables are honored through a per-request dispatcher without logging proxy credentials.
|
|
6
6
|
|
|
7
7
|
Provisioning uses in-process, worker, optional native advisory, and portable filesystem coordination followed by a post-lock capability check. Runtime dependency mutation uses uv's target-environment lock. Pyfig pins uv commands to the selected venv and removes configuration values that could redirect them. It reads an existing `pyproject.toml` only as a resolver input and never writes project manifests or requirements files.
|
|
8
8
|
|
package/dist/cjs/index.js
CHANGED
|
@@ -67,8 +67,7 @@ var exports_src = {};
|
|
|
67
67
|
__export(exports_src, {
|
|
68
68
|
DEFAULT_MANAGED_PYTHON: () => DEFAULT_MANAGED_PYTHON,
|
|
69
69
|
DEFAULT_REQUIRES_PYTHON: () => DEFAULT_REQUIRES_PYTHON,
|
|
70
|
-
|
|
71
|
-
DEFAULT_UV_VERSION: () => DEFAULT_UV_VERSION,
|
|
70
|
+
DEFAULT_REQUIRES_UV: () => DEFAULT_REQUIRES_UV,
|
|
72
71
|
createPyfig: () => createPyfig,
|
|
73
72
|
ensureEnvironment: () => ensureEnvironment,
|
|
74
73
|
ensureUv: () => ensureUv,
|
|
@@ -81,7 +80,21 @@ module.exports = __toCommonJS(exports_src);
|
|
|
81
80
|
// src/config.ts
|
|
82
81
|
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
83
82
|
var envPaths = __toESM(require("env-paths"), 1);
|
|
84
|
-
|
|
83
|
+
|
|
84
|
+
// src/pep440.ts
|
|
85
|
+
var import_pep440 = require("@renovatebot/pep440");
|
|
86
|
+
function isValidPep440Specifier(specifier) {
|
|
87
|
+
return import_pep440.validRange(specifier);
|
|
88
|
+
}
|
|
89
|
+
function normalizePep440Version(version) {
|
|
90
|
+
return import_pep440.valid(version);
|
|
91
|
+
}
|
|
92
|
+
function pep440VersionSatisfies(version, specifier) {
|
|
93
|
+
return import_pep440.filter([version], specifier).length === 1;
|
|
94
|
+
}
|
|
95
|
+
function maxPep440Version(versions, specifier) {
|
|
96
|
+
return import_pep440.maxSatisfying([...versions], specifier);
|
|
97
|
+
}
|
|
85
98
|
|
|
86
99
|
// src/project-root.ts
|
|
87
100
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
@@ -105,23 +118,10 @@ function* ancestors(startDirectory) {
|
|
|
105
118
|
}
|
|
106
119
|
}
|
|
107
120
|
|
|
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
|
-
|
|
120
121
|
// src/config.ts
|
|
121
122
|
var DEFAULT_REQUIRES_PYTHON = ">=3.10,<4";
|
|
122
123
|
var DEFAULT_MANAGED_PYTHON = "3.12";
|
|
123
|
-
var
|
|
124
|
-
var DEFAULT_UV_RANGE = ">=0.10.0 <1.0.0";
|
|
124
|
+
var DEFAULT_REQUIRES_UV = ">=0.10,<1";
|
|
125
125
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
126
126
|
var DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60000;
|
|
127
127
|
var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60000;
|
|
@@ -135,13 +135,11 @@ var envPathsFunction = (() => {
|
|
|
135
135
|
function resolveOptions(options = {}) {
|
|
136
136
|
const env = { ...options.env ?? process.env };
|
|
137
137
|
const requiresPython = options.requiresPython ?? env["PYFIG_REQUIRES_PYTHON"] ?? DEFAULT_REQUIRES_PYTHON;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
const requiresUv = options.requiresUv ?? env["PYFIG_REQUIRES_UV"] ?? DEFAULT_REQUIRES_UV;
|
|
139
|
+
assertPep440Specifier("requires-python", requiresPython);
|
|
140
|
+
assertPep440Specifier("requires-uv", requiresUv);
|
|
141
141
|
const managedPython = options.managedPython ?? env["PYFIG_MANAGED_PYTHON"] ?? DEFAULT_MANAGED_PYTHON;
|
|
142
|
-
const uvVersion = options.uvVersion ?? env["PYFIG_UV_VERSION"] ?? DEFAULT_UV_VERSION;
|
|
143
142
|
assertManagedPythonRequest(managedPython, requiresPython);
|
|
144
|
-
assertUvVersion(uvVersion, uvRange);
|
|
145
143
|
const probeTimeoutMs = resolveDuration("probeTimeoutMs", options.probeTimeoutMs, env["PYFIG_PROBE_TIMEOUT_MS"], DEFAULT_PROBE_TIMEOUT_MS);
|
|
146
144
|
const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
|
|
147
145
|
const downloadTimeoutMs = resolveDuration("downloadTimeoutMs", options.downloadTimeoutMs, env["PYFIG_DOWNLOAD_TIMEOUT_MS"], DEFAULT_DOWNLOAD_TIMEOUT_MS);
|
|
@@ -155,8 +153,7 @@ function resolveOptions(options = {}) {
|
|
|
155
153
|
python: options.python ?? env["PYFIG_PYTHON"] ?? "",
|
|
156
154
|
requiresPython,
|
|
157
155
|
managedPython,
|
|
158
|
-
|
|
159
|
-
uvRange,
|
|
156
|
+
requiresUv,
|
|
160
157
|
requireEmbedding: options.requireEmbedding ?? parseBoolean(env["PYFIG_REQUIRE_EMBEDDING"], false),
|
|
161
158
|
requireGil: options.requireGil ?? parseBoolean(env["PYFIG_REQUIRE_GIL"], false),
|
|
162
159
|
autoInstall,
|
|
@@ -181,22 +178,9 @@ function uvEnvironment(options) {
|
|
|
181
178
|
UV_NO_CONFIG: "1"
|
|
182
179
|
};
|
|
183
180
|
}
|
|
184
|
-
function
|
|
185
|
-
if (!
|
|
186
|
-
throw new TypeError(`Invalid ${
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
function assertUvVersion(version, range) {
|
|
190
|
-
if (!import_semver.default.valid(version)) {
|
|
191
|
-
throw new TypeError(`Invalid exact uv version: ${version}`);
|
|
192
|
-
}
|
|
193
|
-
if (!import_semver.default.satisfies(version, range)) {
|
|
194
|
-
throw new TypeError(`Pinned uv ${version} does not satisfy supported range ${range}.`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
function assertPythonSpecifier(specifier) {
|
|
198
|
-
if (!isValidPythonSpecifier(specifier)) {
|
|
199
|
-
throw new TypeError(`Invalid requires-python PEP 440 specifier: ${specifier}`);
|
|
181
|
+
function assertPep440Specifier(name, specifier) {
|
|
182
|
+
if (!isValidPep440Specifier(specifier)) {
|
|
183
|
+
throw new TypeError(`Invalid ${name} PEP 440 specifier: ${specifier}`);
|
|
200
184
|
}
|
|
201
185
|
}
|
|
202
186
|
function assertManagedPythonRequest(request, requiresPython) {
|
|
@@ -205,7 +189,7 @@ function assertManagedPythonRequest(request, requiresPython) {
|
|
|
205
189
|
}
|
|
206
190
|
if (/^\d+\.\d+(?:\.\d+)?$/u.test(request)) {
|
|
207
191
|
const normalized = request.split(".").length === 2 ? `${request}.0` : request;
|
|
208
|
-
if (!
|
|
192
|
+
if (!pep440VersionSatisfies(normalized, requiresPython)) {
|
|
209
193
|
throw new TypeError(`Managed Python ${request} cannot satisfy requires-python ${requiresPython}.`);
|
|
210
194
|
}
|
|
211
195
|
}
|
|
@@ -386,9 +370,6 @@ function assertNoTargetOverride(args) {
|
|
|
386
370
|
}
|
|
387
371
|
}
|
|
388
372
|
|
|
389
|
-
// src/probes.ts
|
|
390
|
-
var import_semver2 = __toESM(require("semver"), 1);
|
|
391
|
-
|
|
392
373
|
// src/process.ts
|
|
393
374
|
var import_node_child_process = require("node:child_process");
|
|
394
375
|
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
@@ -558,7 +539,7 @@ print(json.dumps({
|
|
|
558
539
|
}))
|
|
559
540
|
`;
|
|
560
541
|
function pythonSatisfiesPolicy(python, options) {
|
|
561
|
-
return
|
|
542
|
+
return pep440VersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
|
|
562
543
|
}
|
|
563
544
|
async function probePython(candidate, options) {
|
|
564
545
|
const executable = await resolveExecutable(candidate.executable, options.env);
|
|
@@ -567,7 +548,7 @@ async function probePython(candidate, options) {
|
|
|
567
548
|
try {
|
|
568
549
|
const { stdout } = run(executable, [...candidate.args, "-c", PYTHON_PROBE], options.env, options.probeTimeoutMs);
|
|
569
550
|
const info = JSON.parse(stdout.trim());
|
|
570
|
-
return
|
|
551
|
+
return normalizePep440Version(info.version) ? info : null;
|
|
571
552
|
} catch {
|
|
572
553
|
return null;
|
|
573
554
|
}
|
|
@@ -576,7 +557,7 @@ async function findUv(options) {
|
|
|
576
557
|
const configured = options.env["PYFIG_UV"] ?? "uv";
|
|
577
558
|
const executable = await resolveExecutable(configured, options.env);
|
|
578
559
|
const uv = executable ? probeUv(executable, options) : null;
|
|
579
|
-
return uv &&
|
|
560
|
+
return uv && pep440VersionSatisfies(uv.version, options.requiresUv) ? uv : null;
|
|
580
561
|
}
|
|
581
562
|
function probeUv(executable, options) {
|
|
582
563
|
try {
|
|
@@ -590,7 +571,7 @@ function probeUv(executable, options) {
|
|
|
590
571
|
function normalizeVersion(value) {
|
|
591
572
|
if (!value)
|
|
592
573
|
return null;
|
|
593
|
-
return
|
|
574
|
+
return normalizePep440Version(value);
|
|
594
575
|
}
|
|
595
576
|
function pythonArchitectureMatchesRuntime(python) {
|
|
596
577
|
const architecture = python.architecture.toLowerCase();
|
|
@@ -829,21 +810,30 @@ async function ensureUvResolved(options) {
|
|
|
829
810
|
if (existing)
|
|
830
811
|
return existing;
|
|
831
812
|
if (!options.installUv) {
|
|
832
|
-
throw new Error(`Compatible uv is unavailable (
|
|
813
|
+
throw new Error(`Compatible uv is unavailable (requires-uv ${options.requiresUv}), and uv installation is disabled.`);
|
|
833
814
|
}
|
|
834
|
-
const
|
|
835
|
-
const executable = uvExecutable(options);
|
|
815
|
+
const policyKey = import_node_crypto.createHash("sha256").update(options.requiresUv).digest("hex");
|
|
836
816
|
return withProvisioningLock({
|
|
837
|
-
key: `uv:${canonicalPath(
|
|
817
|
+
key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
|
|
838
818
|
timeoutMs: options.coordinationTimeoutMs,
|
|
839
819
|
check: () => findAvailableUv(options),
|
|
840
820
|
mutate: async () => {
|
|
841
|
-
await
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
821
|
+
const release = await resolveUvRelease(options);
|
|
822
|
+
const installDir = uvInstallDirectory(options, release.version);
|
|
823
|
+
const executable = uvExecutable(options, release.version);
|
|
824
|
+
return withProvisioningLock({
|
|
825
|
+
key: `uv-release:${canonicalPath(installDir)}`,
|
|
826
|
+
timeoutMs: options.coordinationTimeoutMs,
|
|
827
|
+
check: () => findAvailableUv(options),
|
|
828
|
+
mutate: async () => {
|
|
829
|
+
await installUv(installDir, release, options);
|
|
830
|
+
const result = probeUv(executable, options);
|
|
831
|
+
if (!result || result.version !== release.version) {
|
|
832
|
+
throw new Error(`Expected managed uv ${release.version}, but the installed executable did not match.`);
|
|
833
|
+
}
|
|
834
|
+
return result;
|
|
835
|
+
}
|
|
836
|
+
});
|
|
847
837
|
}
|
|
848
838
|
});
|
|
849
839
|
}
|
|
@@ -851,8 +841,21 @@ async function findAvailableUv(options) {
|
|
|
851
841
|
const external = await findUv(options);
|
|
852
842
|
if (external)
|
|
853
843
|
return external;
|
|
854
|
-
|
|
855
|
-
|
|
844
|
+
return findManagedUv(options);
|
|
845
|
+
}
|
|
846
|
+
async function findManagedUv(options) {
|
|
847
|
+
const entries = await import_promises.readdir(uvInstallRoot(options), { withFileTypes: true }).catch(() => []);
|
|
848
|
+
let versions = entries.filter((entry) => entry.isDirectory() && normalizePep440Version(entry.name) === entry.name).map((entry) => entry.name);
|
|
849
|
+
while (versions.length > 0) {
|
|
850
|
+
const selected = maxPep440Version(versions, options.requiresUv);
|
|
851
|
+
if (!selected)
|
|
852
|
+
return null;
|
|
853
|
+
const candidate = probeUv(uvExecutable(options, selected), options);
|
|
854
|
+
if (candidate?.version === selected)
|
|
855
|
+
return candidate;
|
|
856
|
+
versions = versions.filter((version) => version !== selected);
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
856
859
|
}
|
|
857
860
|
function normalizeDependencies(dependencies) {
|
|
858
861
|
const normalized = dependencies.map((dependency) => dependency.trim());
|
|
@@ -913,15 +916,159 @@ async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, opt
|
|
|
913
916
|
const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
|
|
914
917
|
return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
|
|
915
918
|
}
|
|
916
|
-
|
|
917
|
-
|
|
919
|
+
var UV_RELEASE_PAGE_SIZE = 100;
|
|
920
|
+
var UV_RELEASE_MAX_PAGES = 20;
|
|
921
|
+
var UV_RELEASE_METADATA_TTL_MS = 24 * 60 * 60000;
|
|
922
|
+
async function resolveUvRelease(options) {
|
|
923
|
+
const catalog = await ensureUvReleaseCatalog(options);
|
|
924
|
+
const asset = uvAsset();
|
|
925
|
+
const releases = catalog.releases.filter((release) => release.assets.includes(asset) && release.assets.includes(`${asset}.sha256`));
|
|
926
|
+
const version = maxPep440Version(releases.map((release) => release.version), options.requiresUv);
|
|
927
|
+
const release = releases.find((candidate) => candidate.version === version);
|
|
928
|
+
if (!release) {
|
|
929
|
+
throw new Error(`No published uv release satisfies requires-uv ${options.requiresUv}.`);
|
|
930
|
+
}
|
|
931
|
+
return release;
|
|
932
|
+
}
|
|
933
|
+
async function ensureUvReleaseCatalog(options) {
|
|
934
|
+
const file = uvReleaseMetadataFile(options);
|
|
935
|
+
const stale = await readUvReleaseCatalog(file);
|
|
936
|
+
try {
|
|
937
|
+
return await withProvisioningLock({
|
|
938
|
+
key: `uv-metadata:${canonicalPath(file)}`,
|
|
939
|
+
timeoutMs: options.coordinationTimeoutMs,
|
|
940
|
+
check: () => readFreshUvReleaseCatalog(file),
|
|
941
|
+
mutate: async () => {
|
|
942
|
+
const catalog = await fetchUvReleaseCatalog(options);
|
|
943
|
+
await writeUvReleaseCatalog(file, catalog);
|
|
944
|
+
return catalog;
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
} catch (error) {
|
|
948
|
+
const fallback = await readUvReleaseCatalog(file) ?? stale;
|
|
949
|
+
if (fallback)
|
|
950
|
+
return fallback;
|
|
951
|
+
throw error;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async function fetchUvReleaseCatalog(options) {
|
|
955
|
+
const dispatcher = proxyDispatcher(options.env);
|
|
956
|
+
const releases = new Map;
|
|
957
|
+
let exhausted = false;
|
|
958
|
+
try {
|
|
959
|
+
for (let page = 1;page <= UV_RELEASE_MAX_PAGES; page += 1) {
|
|
960
|
+
const text = await downloadText(`https://api.github.com/repos/astral-sh/uv/releases?per_page=${UV_RELEASE_PAGE_SIZE}&page=${page}`, options.downloadTimeoutMs, dispatcher, {
|
|
961
|
+
Accept: "application/vnd.github+json",
|
|
962
|
+
"User-Agent": "pyfig",
|
|
963
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
964
|
+
});
|
|
965
|
+
const payload = JSON.parse(text);
|
|
966
|
+
if (!Array.isArray(payload)) {
|
|
967
|
+
throw new Error("GitHub returned invalid uv release metadata.");
|
|
968
|
+
}
|
|
969
|
+
for (const candidate of payload) {
|
|
970
|
+
if (!candidate || typeof candidate !== "object")
|
|
971
|
+
continue;
|
|
972
|
+
const release = candidate;
|
|
973
|
+
if (release.draft === true || typeof release.tag_name !== "string")
|
|
974
|
+
continue;
|
|
975
|
+
const assetNames = releaseAssetNames(release.assets);
|
|
976
|
+
const version = normalizePep440Version(release.tag_name);
|
|
977
|
+
if (version) {
|
|
978
|
+
releases.set(version, { assets: assetNames, tag: release.tag_name, version });
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (payload.length < UV_RELEASE_PAGE_SIZE) {
|
|
982
|
+
exhausted = true;
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
} finally {
|
|
987
|
+
await dispatcher?.close();
|
|
988
|
+
}
|
|
989
|
+
if (!exhausted) {
|
|
990
|
+
throw new Error(`uv release catalog exceeded ${UV_RELEASE_MAX_PAGES} pages.`);
|
|
991
|
+
}
|
|
992
|
+
if (releases.size === 0) {
|
|
993
|
+
throw new Error("GitHub returned no usable uv releases.");
|
|
994
|
+
}
|
|
995
|
+
return { fetchedAt: Date.now(), releases: [...releases.values()] };
|
|
996
|
+
}
|
|
997
|
+
async function readFreshUvReleaseCatalog(file) {
|
|
998
|
+
const catalog = await readUvReleaseCatalog(file);
|
|
999
|
+
if (!catalog)
|
|
1000
|
+
return null;
|
|
1001
|
+
const age = Date.now() - catalog.fetchedAt;
|
|
1002
|
+
return age >= 0 && age < UV_RELEASE_METADATA_TTL_MS ? catalog : null;
|
|
1003
|
+
}
|
|
1004
|
+
async function readUvReleaseCatalog(file) {
|
|
1005
|
+
try {
|
|
1006
|
+
return parseUvReleaseCatalog(JSON.parse(await import_promises.readFile(file, "utf8")));
|
|
1007
|
+
} catch {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function parseUvReleaseCatalog(value) {
|
|
1012
|
+
if (!value || typeof value !== "object")
|
|
1013
|
+
return null;
|
|
1014
|
+
const catalog = value;
|
|
1015
|
+
if (!Number.isSafeInteger(catalog.fetchedAt) || catalog.fetchedAt < 0 || !Array.isArray(catalog.releases)) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
const releases = [];
|
|
1019
|
+
for (const value of catalog.releases) {
|
|
1020
|
+
if (!value || typeof value !== "object")
|
|
1021
|
+
return null;
|
|
1022
|
+
const release = value;
|
|
1023
|
+
if (!Array.isArray(release.assets) || !release.assets.every((asset) => typeof asset === "string") || typeof release.tag !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/u.test(release.tag) || typeof release.version !== "string" || normalizePep440Version(release.version) !== release.version) {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
releases.push({
|
|
1027
|
+
assets: release.assets,
|
|
1028
|
+
tag: release.tag,
|
|
1029
|
+
version: release.version
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
return { fetchedAt: catalog.fetchedAt, releases };
|
|
1033
|
+
}
|
|
1034
|
+
async function writeUvReleaseCatalog(file, catalog) {
|
|
1035
|
+
await import_promises.mkdir(import_node_path5.default.dirname(file), { recursive: true });
|
|
1036
|
+
const staged = import_node_path5.default.join(import_node_path5.default.dirname(file), `.${import_node_path5.default.basename(file)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
|
|
1037
|
+
try {
|
|
1038
|
+
await import_promises.writeFile(staged, `${JSON.stringify(catalog)}
|
|
1039
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
1040
|
+
if (process.platform === "win32")
|
|
1041
|
+
await import_promises.rm(file, { force: true });
|
|
1042
|
+
await import_promises.rename(staged, file);
|
|
1043
|
+
} finally {
|
|
1044
|
+
await import_promises.rm(staged, { force: true });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function uvReleaseMetadataFile(options) {
|
|
1048
|
+
return import_node_path5.default.join(options.cacheDir, "metadata", "uv-releases.json");
|
|
1049
|
+
}
|
|
1050
|
+
function releaseAssetNames(value) {
|
|
1051
|
+
if (!Array.isArray(value))
|
|
1052
|
+
return [];
|
|
1053
|
+
return value.flatMap((entry) => {
|
|
1054
|
+
if (!entry || typeof entry !== "object")
|
|
1055
|
+
return [];
|
|
1056
|
+
const name = entry.name;
|
|
1057
|
+
return typeof name === "string" ? [name] : [];
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
function uvInstallRoot(options) {
|
|
1061
|
+
return import_node_path5.default.join(options.cacheDir, "tools", "uv");
|
|
1062
|
+
}
|
|
1063
|
+
function uvInstallDirectory(options, version) {
|
|
1064
|
+
return import_node_path5.default.join(uvInstallRoot(options), version);
|
|
918
1065
|
}
|
|
919
|
-
function uvExecutable(options) {
|
|
920
|
-
return import_node_path5.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
|
|
1066
|
+
function uvExecutable(options, version) {
|
|
1067
|
+
return import_node_path5.default.join(uvInstallDirectory(options, version), process.platform === "win32" ? "uv.exe" : "uv");
|
|
921
1068
|
}
|
|
922
|
-
async function installUv(installDir, options) {
|
|
1069
|
+
async function installUv(installDir, release, options) {
|
|
923
1070
|
const asset = uvAsset();
|
|
924
|
-
const baseUrl = `https://github.com/astral-sh/uv/releases/download/${
|
|
1071
|
+
const baseUrl = `https://github.com/astral-sh/uv/releases/download/${release.tag}`;
|
|
925
1072
|
const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
|
|
926
1073
|
const dispatcher = proxyDispatcher(options.env);
|
|
927
1074
|
try {
|
|
@@ -937,7 +1084,7 @@ async function installUv(installDir, options) {
|
|
|
937
1084
|
const source = await extractUvExecutable(archive, asset, extracted);
|
|
938
1085
|
if (!source)
|
|
939
1086
|
throw new Error(`uv archive did not contain the expected executable: ${asset}`);
|
|
940
|
-
await publishUvAtomically(source, installDir, options);
|
|
1087
|
+
await publishUvAtomically(source, installDir, release.version, options);
|
|
941
1088
|
} finally {
|
|
942
1089
|
await dispatcher?.close();
|
|
943
1090
|
await import_promises.rm(temporary, { recursive: true, force: true });
|
|
@@ -959,17 +1106,17 @@ async function extractUvExecutable(archive, asset, destination) {
|
|
|
959
1106
|
await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
|
|
960
1107
|
return source;
|
|
961
1108
|
}
|
|
962
|
-
async function publishUvAtomically(source, installDir, options) {
|
|
1109
|
+
async function publishUvAtomically(source, installDir, version, options) {
|
|
963
1110
|
await import_promises.mkdir(installDir, { recursive: true });
|
|
964
|
-
const executable = uvExecutable(options);
|
|
1111
|
+
const executable = uvExecutable(options, version);
|
|
965
1112
|
const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
|
|
966
1113
|
try {
|
|
967
1114
|
await import_promises.copyFile(source, staged);
|
|
968
1115
|
if (process.platform !== "win32")
|
|
969
1116
|
await import_promises.chmod(staged, 493);
|
|
970
1117
|
const stagedUv = probeUv(staged, options);
|
|
971
|
-
if (!stagedUv || stagedUv.version !==
|
|
972
|
-
throw new Error(`Downloaded uv executable is not version ${
|
|
1118
|
+
if (!stagedUv || stagedUv.version !== version) {
|
|
1119
|
+
throw new Error(`Downloaded uv executable is not version ${version}.`);
|
|
973
1120
|
}
|
|
974
1121
|
if (process.platform === "win32")
|
|
975
1122
|
await import_promises.rm(executable, { force: true });
|
|
@@ -1002,9 +1149,10 @@ async function download(url, destination, timeoutMs, dispatcher) {
|
|
|
1002
1149
|
}
|
|
1003
1150
|
await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
|
|
1004
1151
|
}
|
|
1005
|
-
async function downloadText(url, timeoutMs, dispatcher) {
|
|
1152
|
+
async function downloadText(url, timeoutMs, dispatcher, headers) {
|
|
1006
1153
|
const response = await import_undici.fetch(url, {
|
|
1007
1154
|
dispatcher,
|
|
1155
|
+
headers,
|
|
1008
1156
|
redirect: "follow",
|
|
1009
1157
|
signal: AbortSignal.timeout(timeoutMs)
|
|
1010
1158
|
});
|
|
@@ -1136,4 +1284,4 @@ function createPyfig(options = {}) {
|
|
|
1136
1284
|
};
|
|
1137
1285
|
}
|
|
1138
1286
|
|
|
1139
|
-
//# debugId=
|
|
1287
|
+
//# debugId=25C05D1900A6412264756E2164756E21
|