pyfig 0.5.0 → 0.6.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 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` uses the same PEP 440 specifier syntax as `[project].requires-python` in `pyproject.toml`. Comma-separated clauses, compatible releases (`~=`), exclusions (`!=`), wildcard equality (`==3.12.*`), arbitrary equality (`===`), and pre-release versions are supported. For example: `>=3.10,!=3.12.*,<4`.
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
- | `uvVersion` | `PYFIG_UV_VERSION` | `0.13.6` |
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 downloads an exact uv release archive over HTTPS, verifies its adjacent SHA-256 checksum, validates the staged executable version, and atomically publishes it in Pyfig's cache. It does not execute installer scripts or modify shell profiles.
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
- The uv release URL is pinned to an exact validated version and fetched over HTTPS with bounded redirects and timeouts. 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.
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
- DEFAULT_UV_RANGE: () => DEFAULT_UV_RANGE,
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
- var import_semver = __toESM(require("semver"), 1);
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 DEFAULT_UV_VERSION = "0.13.6";
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 uvRange = options.uvRange ?? env["PYFIG_UV_RANGE"] ?? DEFAULT_UV_RANGE;
139
- assertPythonSpecifier(requiresPython);
140
- assertRange("uv", uvRange);
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
- uvVersion,
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 assertRange(tool, range) {
185
- if (!import_semver.default.validRange(range)) {
186
- throw new TypeError(`Invalid ${tool} SemVer range: ${range}`);
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 (!pythonVersionSatisfies(normalized, requiresPython)) {
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 pythonVersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
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 isValidPythonVersion(info.version) ? info : null;
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 && import_semver2.default.satisfies(uv.version, options.uvRange) ? uv : null;
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 import_semver2.default.valid(value) ?? import_semver2.default.coerce(value)?.version ?? null;
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 (required ${options.uvRange}), and uv installation is disabled.`);
813
+ throw new Error(`Compatible uv is unavailable (requires-uv ${options.requiresUv}), and uv installation is disabled.`);
833
814
  }
834
- const installDir = uvInstallDirectory(options);
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(installDir)}`,
817
+ key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
838
818
  timeoutMs: options.coordinationTimeoutMs,
839
819
  check: () => findAvailableUv(options),
840
820
  mutate: async () => {
841
- await installUv(installDir, options);
842
- const result = probeUv(executable, options);
843
- if (!result || result.version !== options.uvVersion) {
844
- throw new Error(`Expected managed uv ${options.uvVersion}, but the installed executable did not match.`);
845
- }
846
- return result;
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
- const managed = probeUv(uvExecutable(options), options);
855
- return managed?.version === options.uvVersion ? managed : null;
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
- function uvInstallDirectory(options) {
917
- return import_node_path5.default.join(options.cacheDir, "tools", "uv", options.uvVersion);
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/${options.uvVersion}`;
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 !== options.uvVersion) {
972
- throw new Error(`Downloaded uv executable is not version ${options.uvVersion}.`);
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=5564427996CDDBA964756E2164756E21
1287
+ //# debugId=25C05D1900A6412264756E2164756E21