pyfig 0.4.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.
@@ -74,14 +74,28 @@ var import_node_os = __toESM(require("node:os"), 1);
74
74
  var import_node_path5 = __toESM(require("node:path"), 1);
75
75
  var import_node_stream = require("node:stream");
76
76
  var import_promises2 = require("node:stream/promises");
77
- var import_extract_zip = __toESM(require("extract-zip"), 1);
77
+ var import_fflate = require("fflate");
78
78
  var tar = __toESM(require("tar"), 1);
79
79
  var import_undici = require("undici");
80
80
 
81
81
  // src/config.ts
82
82
  var import_node_path2 = __toESM(require("node:path"), 1);
83
83
  var envPaths = __toESM(require("env-paths"), 1);
84
- var import_semver = __toESM(require("semver"), 1);
84
+
85
+ // src/pep440.ts
86
+ var import_pep440 = require("@renovatebot/pep440");
87
+ function isValidPep440Specifier(specifier) {
88
+ return import_pep440.validRange(specifier);
89
+ }
90
+ function normalizePep440Version(version) {
91
+ return import_pep440.valid(version);
92
+ }
93
+ function pep440VersionSatisfies(version, specifier) {
94
+ return import_pep440.filter([version], specifier).length === 1;
95
+ }
96
+ function maxPep440Version(versions, specifier) {
97
+ return import_pep440.maxSatisfying([...versions], specifier);
98
+ }
85
99
 
86
100
  // src/project-root.ts
87
101
  var import_node_fs = __toESM(require("node:fs"), 1);
@@ -106,10 +120,9 @@ function* ancestors(startDirectory) {
106
120
  }
107
121
 
108
122
  // src/config.ts
109
- var DEFAULT_PYTHON_RANGE = ">=3.10.0 <4.0.0";
123
+ var DEFAULT_REQUIRES_PYTHON = ">=3.10,<4";
110
124
  var DEFAULT_MANAGED_PYTHON = "3.12";
111
- var DEFAULT_UV_VERSION = "0.13.6";
112
- var DEFAULT_UV_RANGE = ">=0.10.0 <1.0.0";
125
+ var DEFAULT_REQUIRES_UV = ">=0.10,<1";
113
126
  var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
114
127
  var DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60000;
115
128
  var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60000;
@@ -122,14 +135,12 @@ var envPathsFunction = (() => {
122
135
  })();
123
136
  function resolveOptions(options = {}) {
124
137
  const env = { ...options.env ?? process.env };
125
- const pythonRange = options.pythonRange ?? env["PYFIG_PYTHON_RANGE"] ?? DEFAULT_PYTHON_RANGE;
126
- const uvRange = options.uvRange ?? env["PYFIG_UV_RANGE"] ?? DEFAULT_UV_RANGE;
127
- assertRange("Python", pythonRange);
128
- assertRange("uv", uvRange);
138
+ const requiresPython = options.requiresPython ?? env["PYFIG_REQUIRES_PYTHON"] ?? DEFAULT_REQUIRES_PYTHON;
139
+ const requiresUv = options.requiresUv ?? env["PYFIG_REQUIRES_UV"] ?? DEFAULT_REQUIRES_UV;
140
+ assertPep440Specifier("requires-python", requiresPython);
141
+ assertPep440Specifier("requires-uv", requiresUv);
129
142
  const managedPython = options.managedPython ?? env["PYFIG_MANAGED_PYTHON"] ?? DEFAULT_MANAGED_PYTHON;
130
- const uvVersion = options.uvVersion ?? env["PYFIG_UV_VERSION"] ?? DEFAULT_UV_VERSION;
131
- assertManagedPythonRequest(managedPython, pythonRange);
132
- assertUvVersion(uvVersion, uvRange);
143
+ assertManagedPythonRequest(managedPython, requiresPython);
133
144
  const probeTimeoutMs = resolveDuration("probeTimeoutMs", options.probeTimeoutMs, env["PYFIG_PROBE_TIMEOUT_MS"], DEFAULT_PROBE_TIMEOUT_MS);
134
145
  const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
135
146
  const downloadTimeoutMs = resolveDuration("downloadTimeoutMs", options.downloadTimeoutMs, env["PYFIG_DOWNLOAD_TIMEOUT_MS"], DEFAULT_DOWNLOAD_TIMEOUT_MS);
@@ -141,10 +152,9 @@ function resolveOptions(options = {}) {
141
152
  const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
142
153
  return {
143
154
  python: options.python ?? env["PYFIG_PYTHON"] ?? "",
144
- pythonRange,
155
+ requiresPython,
145
156
  managedPython,
146
- uvVersion,
147
- uvRange,
157
+ requiresUv,
148
158
  requireEmbedding: options.requireEmbedding ?? parseBoolean(env["PYFIG_REQUIRE_EMBEDDING"], false),
149
159
  requireGil: options.requireGil ?? parseBoolean(env["PYFIG_REQUIRE_GIL"], false),
150
160
  autoInstall,
@@ -169,27 +179,19 @@ function uvEnvironment(options) {
169
179
  UV_NO_CONFIG: "1"
170
180
  };
171
181
  }
172
- function assertRange(tool, range) {
173
- if (!import_semver.default.validRange(range)) {
174
- throw new TypeError(`Invalid ${tool} SemVer range: ${range}`);
182
+ function assertPep440Specifier(name, specifier) {
183
+ if (!isValidPep440Specifier(specifier)) {
184
+ throw new TypeError(`Invalid ${name} PEP 440 specifier: ${specifier}`);
175
185
  }
176
186
  }
177
- function assertUvVersion(version, range) {
178
- if (!import_semver.default.valid(version)) {
179
- throw new TypeError(`Invalid exact uv version: ${version}`);
180
- }
181
- if (!import_semver.default.satisfies(version, range)) {
182
- throw new TypeError(`Pinned uv ${version} does not satisfy supported range ${range}.`);
183
- }
184
- }
185
- function assertManagedPythonRequest(request, pythonRange) {
187
+ function assertManagedPythonRequest(request, requiresPython) {
186
188
  if (!request || /[\\/\0\r\n]/u.test(request) || request.includes("..")) {
187
189
  throw new TypeError(`Invalid managed Python request: ${request}`);
188
190
  }
189
191
  if (/^\d+\.\d+(?:\.\d+)?$/u.test(request)) {
190
192
  const normalized = request.split(".").length === 2 ? `${request}.0` : request;
191
- if (!import_semver.default.satisfies(normalized, pythonRange)) {
192
- throw new TypeError(`Managed Python ${request} cannot satisfy configured range ${pythonRange}.`);
193
+ if (!pep440VersionSatisfies(normalized, requiresPython)) {
194
+ throw new TypeError(`Managed Python ${request} cannot satisfy requires-python ${requiresPython}.`);
193
195
  }
194
196
  }
195
197
  }
@@ -354,9 +356,6 @@ function assertNoTargetOverride(args) {
354
356
  }
355
357
  }
356
358
 
357
- // src/probes.ts
358
- var import_semver2 = __toESM(require("semver"), 1);
359
-
360
359
  // src/process.ts
361
360
  var import_node_child_process = require("node:child_process");
362
361
  var import_node_path4 = __toESM(require("node:path"), 1);
@@ -497,7 +496,15 @@ if os.path.isfile(header_path) and library_path:
497
496
  }
498
497
 
499
498
  release_level = sys.version_info.releaselevel
500
- release_suffix = "" if release_level == "final" else f"-{release_level}.{sys.version_info.serial}"
499
+ release_suffixes = {
500
+ "alpha": "a",
501
+ "beta": "b",
502
+ "candidate": "rc",
503
+ "final": "",
504
+ }
505
+ release_suffix = release_suffixes[release_level]
506
+ if release_suffix:
507
+ release_suffix += str(sys.version_info.serial)
501
508
 
502
509
  print(json.dumps({
503
510
  "executable": sys.executable,
@@ -517,8 +524,8 @@ print(json.dumps({
517
524
  "embedding": embedding,
518
525
  }))
519
526
  `;
520
- function pythonSatisfiesRequirements(python, options) {
521
- return import_semver2.default.satisfies(python.version, options.pythonRange) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
527
+ function pythonSatisfiesPolicy(python, options) {
528
+ return pep440VersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
522
529
  }
523
530
  async function probePython(candidate, options) {
524
531
  const executable = await resolveExecutable(candidate.executable, options.env);
@@ -527,7 +534,7 @@ async function probePython(candidate, options) {
527
534
  try {
528
535
  const { stdout } = run(executable, [...candidate.args, "-c", PYTHON_PROBE], options.env, options.probeTimeoutMs);
529
536
  const info = JSON.parse(stdout.trim());
530
- return import_semver2.default.valid(info.version) ? info : null;
537
+ return normalizePep440Version(info.version) ? info : null;
531
538
  } catch {
532
539
  return null;
533
540
  }
@@ -536,7 +543,7 @@ async function findUv(options) {
536
543
  const configured = options.env["PYFIG_UV"] ?? "uv";
537
544
  const executable = await resolveExecutable(configured, options.env);
538
545
  const uv = executable ? probeUv(executable, options) : null;
539
- return uv && import_semver2.default.satisfies(uv.version, options.uvRange) ? uv : null;
546
+ return uv && pep440VersionSatisfies(uv.version, options.requiresUv) ? uv : null;
540
547
  }
541
548
  function probeUv(executable, options) {
542
549
  try {
@@ -550,7 +557,7 @@ function probeUv(executable, options) {
550
557
  function normalizeVersion(value) {
551
558
  if (!value)
552
559
  return null;
553
- return import_semver2.default.valid(value) ?? import_semver2.default.coerce(value)?.version ?? null;
560
+ return normalizePep440Version(value);
554
561
  }
555
562
  function pythonArchitectureMatchesRuntime(python) {
556
563
  const architecture = python.architecture.toLowerCase();
@@ -570,14 +577,14 @@ function pythonArchitectureMatchesRuntime(python) {
570
577
  async function ensureUv(options = {}) {
571
578
  return ensureUvResolved(resolveOptions(options));
572
579
  }
573
- async function ensureEnvironment(options = {}, requirements = [], dependencyOptions = {}) {
580
+ async function ensureEnvironment(options = {}, dependencies = [], dependencyOptions = {}) {
574
581
  const resolved = resolveOptions(options);
575
- const normalized = normalizeRequirements(requirements);
582
+ const normalized = normalizeDependencies(dependencies);
576
583
  const selections = normalizeDependencyOptions(dependencyOptions);
577
584
  const uv = await ensureUvResolved(resolved);
578
585
  const python = await ensureVirtualEnvironmentResolved(resolved, uv);
579
586
  if (normalized.length > 0 || selections.extras.length > 0 || selections.groups.length > 0 || selections.allExtras) {
580
- await installRuntimeRequirements(uv, python, normalized, selections, resolved);
587
+ await installRuntimeDependencies(uv, python, normalized, selections, resolved);
581
588
  }
582
589
  return {
583
590
  python,
@@ -592,7 +599,7 @@ async function ensureVirtualEnvironmentResolved(options, uv) {
592
599
  const directory = virtualEnvironmentDirectory(options.projectRoot);
593
600
  const check = async () => {
594
601
  const candidate = await inspectVirtualEnvironment(directory, options);
595
- return candidate && pythonSatisfiesRequirements(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
602
+ return candidate && pythonSatisfiesPolicy(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
596
603
  };
597
604
  if (!options.createVirtualEnvironment) {
598
605
  throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
@@ -625,7 +632,7 @@ async function ensureVirtualEnvironmentResolved(options, uv) {
625
632
  async function findReusableEnvironment(options) {
626
633
  if (options.python) {
627
634
  const configured = await probePython({ executable: options.python, args: [] }, options);
628
- if (!configured || !pythonSatisfiesRequirements(configured, options)) {
635
+ if (!configured || !pythonSatisfiesPolicy(configured, options)) {
629
636
  throw incompatibleConfiguredPython(options);
630
637
  }
631
638
  if (configured.virtualEnvironment)
@@ -636,7 +643,7 @@ async function findReusableEnvironment(options) {
636
643
  virtualEnvironmentDirectory(options.projectRoot)
637
644
  ]) {
638
645
  const info = await inspectVirtualEnvironment(directory, options);
639
- if (info && pythonSatisfiesRequirements(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
646
+ if (info && pythonSatisfiesPolicy(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
640
647
  return info;
641
648
  }
642
649
  }
@@ -645,13 +652,13 @@ async function findReusableEnvironment(options) {
645
652
  async function ensureBasePythonResolved(uv, options) {
646
653
  if (options.python) {
647
654
  const configured = await probePython({ executable: options.python, args: [] }, options);
648
- if (!configured || !pythonSatisfiesRequirements(configured, options)) {
655
+ if (!configured || !pythonSatisfiesPolicy(configured, options)) {
649
656
  throw incompatibleConfiguredPython(options);
650
657
  }
651
658
  if (!configured.virtualEnvironment)
652
659
  return configured;
653
660
  const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
654
- if (base && pythonSatisfiesRequirements(base, options))
661
+ if (base && pythonSatisfiesPolicy(base, options))
655
662
  return base;
656
663
  throw incompatibleConfiguredPython(options);
657
664
  }
@@ -693,7 +700,7 @@ async function findPythonWithUv(uv, options, source) {
693
700
  try {
694
701
  const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
695
702
  const info = await probePython({ executable: stdout.trim(), args: [] }, options);
696
- if (info && pythonSatisfiesRequirements(info, options))
703
+ if (info && pythonSatisfiesPolicy(info, options))
697
704
  return info;
698
705
  } catch {}
699
706
  }
@@ -711,7 +718,7 @@ async function findPythonWithUv(uv, options, source) {
711
718
  const candidates = installedPythonPaths(stdout);
712
719
  for (const executable of candidates) {
713
720
  const info = await probePython({ executable, args: [] }, options);
714
- if (info && !info.virtualEnvironment && pythonSatisfiesRequirements(info, options)) {
721
+ if (info && !info.virtualEnvironment && pythonSatisfiesPolicy(info, options)) {
715
722
  return info;
716
723
  }
717
724
  }
@@ -735,7 +742,7 @@ async function inspectVirtualEnvironment(directory, options) {
735
742
  const info = await probePython({ executable, args: [] }, options);
736
743
  return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
737
744
  }
738
- async function installRuntimeRequirements(uv, python, requirements, dependencyOptions, options) {
745
+ async function installRuntimeDependencies(uv, python, dependencies, dependencyOptions, options) {
739
746
  if (!python.virtualEnvironment) {
740
747
  throw new Error("Runtime dependency installation requires a virtual environment.");
741
748
  }
@@ -744,18 +751,18 @@ async function installRuntimeRequirements(uv, python, requirements, dependencyOp
744
751
  timeoutMs: options.coordinationTimeoutMs,
745
752
  check: async () => null,
746
753
  mutate: async () => {
747
- const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-requirements-"));
754
+ const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-dependencies-"));
748
755
  try {
749
- const additions = import_node_path5.default.join(temporary, "runtime-requirements.txt");
750
- const resolved = import_node_path5.default.join(temporary, "resolved-requirements.txt");
756
+ const additions = import_node_path5.default.join(temporary, "runtime-dependencies.txt");
757
+ const resolved = import_node_path5.default.join(temporary, "resolved-dependencies.txt");
751
758
  const pyproject = import_node_path5.default.join(options.projectRoot, "pyproject.toml");
752
759
  const hasPyproject = import_node_fs4.default.existsSync(pyproject);
753
760
  if (!hasPyproject && (dependencyOptions.extras.length > 0 || dependencyOptions.groups.length > 0 || dependencyOptions.allExtras)) {
754
761
  throw new Error("Runtime extras and groups require an existing pyproject.toml.");
755
762
  }
756
763
  const sources = hasPyproject ? [pyproject] : [];
757
- if (requirements.length > 0) {
758
- await import_promises.writeFile(additions, `${requirements.join(`
764
+ if (dependencies.length > 0) {
765
+ await import_promises.writeFile(additions, `${dependencies.join(`
759
766
  `)}
760
767
  `, "utf8");
761
768
  sources.push(additions);
@@ -789,21 +796,30 @@ async function ensureUvResolved(options) {
789
796
  if (existing)
790
797
  return existing;
791
798
  if (!options.installUv) {
792
- throw new Error(`Compatible uv is unavailable (required ${options.uvRange}), and uv installation is disabled.`);
799
+ throw new Error(`Compatible uv is unavailable (requires-uv ${options.requiresUv}), and uv installation is disabled.`);
793
800
  }
794
- const installDir = uvInstallDirectory(options);
795
- const executable = uvExecutable(options);
801
+ const policyKey = import_node_crypto.createHash("sha256").update(options.requiresUv).digest("hex");
796
802
  return withProvisioningLock({
797
- key: `uv:${canonicalPath(installDir)}`,
803
+ key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
798
804
  timeoutMs: options.coordinationTimeoutMs,
799
805
  check: () => findAvailableUv(options),
800
806
  mutate: async () => {
801
- await installUv(installDir, options);
802
- const result = probeUv(executable, options);
803
- if (!result || result.version !== options.uvVersion) {
804
- throw new Error(`Expected managed uv ${options.uvVersion}, but the installed executable did not match.`);
805
- }
806
- return result;
807
+ const release = await resolveUvRelease(options);
808
+ const installDir = uvInstallDirectory(options, release.version);
809
+ const executable = uvExecutable(options, release.version);
810
+ return withProvisioningLock({
811
+ key: `uv-release:${canonicalPath(installDir)}`,
812
+ timeoutMs: options.coordinationTimeoutMs,
813
+ check: () => findAvailableUv(options),
814
+ mutate: async () => {
815
+ await installUv(installDir, release, options);
816
+ const result = probeUv(executable, options);
817
+ if (!result || result.version !== release.version) {
818
+ throw new Error(`Expected managed uv ${release.version}, but the installed executable did not match.`);
819
+ }
820
+ return result;
821
+ }
822
+ });
807
823
  }
808
824
  });
809
825
  }
@@ -811,14 +827,27 @@ async function findAvailableUv(options) {
811
827
  const external = await findUv(options);
812
828
  if (external)
813
829
  return external;
814
- const managed = probeUv(uvExecutable(options), options);
815
- return managed?.version === options.uvVersion ? managed : null;
816
- }
817
- function normalizeRequirements(requirements) {
818
- const normalized = requirements.map((requirement) => requirement.trim());
819
- for (const requirement of normalized) {
820
- if (!requirement || /[\0\r\n]/u.test(requirement)) {
821
- throw new TypeError(`Invalid runtime requirement: ${JSON.stringify(requirement)}`);
830
+ return findManagedUv(options);
831
+ }
832
+ async function findManagedUv(options) {
833
+ const entries = await import_promises.readdir(uvInstallRoot(options), { withFileTypes: true }).catch(() => []);
834
+ let versions = entries.filter((entry) => entry.isDirectory() && normalizePep440Version(entry.name) === entry.name).map((entry) => entry.name);
835
+ while (versions.length > 0) {
836
+ const selected = maxPep440Version(versions, options.requiresUv);
837
+ if (!selected)
838
+ return null;
839
+ const candidate = probeUv(uvExecutable(options, selected), options);
840
+ if (candidate?.version === selected)
841
+ return candidate;
842
+ versions = versions.filter((version) => version !== selected);
843
+ }
844
+ return null;
845
+ }
846
+ function normalizeDependencies(dependencies) {
847
+ const normalized = dependencies.map((dependency) => dependency.trim());
848
+ for (const dependency of normalized) {
849
+ if (!dependency || /[\0\r\n]/u.test(dependency)) {
850
+ throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
822
851
  }
823
852
  }
824
853
  return [...new Set(normalized)];
@@ -855,17 +884,17 @@ function targetUvEnvironment(options, python) {
855
884
  return uvCommandEnvironment(uvEnvironment(options), python);
856
885
  }
857
886
  function incompatibleConfiguredPython(options) {
858
- return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `${options.pythonRange}${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
887
+ return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `requires-python ${options.requiresPython}` + `${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
859
888
  }
860
889
  function missingPythonMessage(options, reason) {
861
890
  const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
862
- return `No Python satisfies ${options.pythonRange}${embedding}; ${reason}.`;
891
+ return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
863
892
  }
864
893
  async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
865
894
  if (!options.python)
866
895
  return true;
867
896
  const configured = await probePython({ executable: options.python, args: [] }, options);
868
- if (!configured || !pythonSatisfiesRequirements(configured, options))
897
+ if (!configured || !pythonSatisfiesPolicy(configured, options))
869
898
  return false;
870
899
  if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
871
900
  return true;
@@ -873,15 +902,159 @@ async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, opt
873
902
  const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
874
903
  return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
875
904
  }
876
- function uvInstallDirectory(options) {
877
- return import_node_path5.default.join(options.cacheDir, "tools", "uv", options.uvVersion);
905
+ var UV_RELEASE_PAGE_SIZE = 100;
906
+ var UV_RELEASE_MAX_PAGES = 20;
907
+ var UV_RELEASE_METADATA_TTL_MS = 24 * 60 * 60000;
908
+ async function resolveUvRelease(options) {
909
+ const catalog = await ensureUvReleaseCatalog(options);
910
+ const asset = uvAsset();
911
+ const releases = catalog.releases.filter((release) => release.assets.includes(asset) && release.assets.includes(`${asset}.sha256`));
912
+ const version = maxPep440Version(releases.map((release) => release.version), options.requiresUv);
913
+ const release = releases.find((candidate) => candidate.version === version);
914
+ if (!release) {
915
+ throw new Error(`No published uv release satisfies requires-uv ${options.requiresUv}.`);
916
+ }
917
+ return release;
918
+ }
919
+ async function ensureUvReleaseCatalog(options) {
920
+ const file = uvReleaseMetadataFile(options);
921
+ const stale = await readUvReleaseCatalog(file);
922
+ try {
923
+ return await withProvisioningLock({
924
+ key: `uv-metadata:${canonicalPath(file)}`,
925
+ timeoutMs: options.coordinationTimeoutMs,
926
+ check: () => readFreshUvReleaseCatalog(file),
927
+ mutate: async () => {
928
+ const catalog = await fetchUvReleaseCatalog(options);
929
+ await writeUvReleaseCatalog(file, catalog);
930
+ return catalog;
931
+ }
932
+ });
933
+ } catch (error) {
934
+ const fallback = await readUvReleaseCatalog(file) ?? stale;
935
+ if (fallback)
936
+ return fallback;
937
+ throw error;
938
+ }
878
939
  }
879
- function uvExecutable(options) {
880
- return import_node_path5.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
940
+ async function fetchUvReleaseCatalog(options) {
941
+ const dispatcher = proxyDispatcher(options.env);
942
+ const releases = new Map;
943
+ let exhausted = false;
944
+ try {
945
+ for (let page = 1;page <= UV_RELEASE_MAX_PAGES; page += 1) {
946
+ const text = await downloadText(`https://api.github.com/repos/astral-sh/uv/releases?per_page=${UV_RELEASE_PAGE_SIZE}&page=${page}`, options.downloadTimeoutMs, dispatcher, {
947
+ Accept: "application/vnd.github+json",
948
+ "User-Agent": "pyfig",
949
+ "X-GitHub-Api-Version": "2022-11-28"
950
+ });
951
+ const payload = JSON.parse(text);
952
+ if (!Array.isArray(payload)) {
953
+ throw new Error("GitHub returned invalid uv release metadata.");
954
+ }
955
+ for (const candidate of payload) {
956
+ if (!candidate || typeof candidate !== "object")
957
+ continue;
958
+ const release = candidate;
959
+ if (release.draft === true || typeof release.tag_name !== "string")
960
+ continue;
961
+ const assetNames = releaseAssetNames(release.assets);
962
+ const version = normalizePep440Version(release.tag_name);
963
+ if (version) {
964
+ releases.set(version, { assets: assetNames, tag: release.tag_name, version });
965
+ }
966
+ }
967
+ if (payload.length < UV_RELEASE_PAGE_SIZE) {
968
+ exhausted = true;
969
+ break;
970
+ }
971
+ }
972
+ } finally {
973
+ await dispatcher?.close();
974
+ }
975
+ if (!exhausted) {
976
+ throw new Error(`uv release catalog exceeded ${UV_RELEASE_MAX_PAGES} pages.`);
977
+ }
978
+ if (releases.size === 0) {
979
+ throw new Error("GitHub returned no usable uv releases.");
980
+ }
981
+ return { fetchedAt: Date.now(), releases: [...releases.values()] };
881
982
  }
882
- async function installUv(installDir, options) {
983
+ async function readFreshUvReleaseCatalog(file) {
984
+ const catalog = await readUvReleaseCatalog(file);
985
+ if (!catalog)
986
+ return null;
987
+ const age = Date.now() - catalog.fetchedAt;
988
+ return age >= 0 && age < UV_RELEASE_METADATA_TTL_MS ? catalog : null;
989
+ }
990
+ async function readUvReleaseCatalog(file) {
991
+ try {
992
+ return parseUvReleaseCatalog(JSON.parse(await import_promises.readFile(file, "utf8")));
993
+ } catch {
994
+ return null;
995
+ }
996
+ }
997
+ function parseUvReleaseCatalog(value) {
998
+ if (!value || typeof value !== "object")
999
+ return null;
1000
+ const catalog = value;
1001
+ if (!Number.isSafeInteger(catalog.fetchedAt) || catalog.fetchedAt < 0 || !Array.isArray(catalog.releases)) {
1002
+ return null;
1003
+ }
1004
+ const releases = [];
1005
+ for (const value of catalog.releases) {
1006
+ if (!value || typeof value !== "object")
1007
+ return null;
1008
+ const release = value;
1009
+ 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) {
1010
+ return null;
1011
+ }
1012
+ releases.push({
1013
+ assets: release.assets,
1014
+ tag: release.tag,
1015
+ version: release.version
1016
+ });
1017
+ }
1018
+ return { fetchedAt: catalog.fetchedAt, releases };
1019
+ }
1020
+ async function writeUvReleaseCatalog(file, catalog) {
1021
+ await import_promises.mkdir(import_node_path5.default.dirname(file), { recursive: true });
1022
+ 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`);
1023
+ try {
1024
+ await import_promises.writeFile(staged, `${JSON.stringify(catalog)}
1025
+ `, { encoding: "utf8", flag: "wx" });
1026
+ if (process.platform === "win32")
1027
+ await import_promises.rm(file, { force: true });
1028
+ await import_promises.rename(staged, file);
1029
+ } finally {
1030
+ await import_promises.rm(staged, { force: true });
1031
+ }
1032
+ }
1033
+ function uvReleaseMetadataFile(options) {
1034
+ return import_node_path5.default.join(options.cacheDir, "metadata", "uv-releases.json");
1035
+ }
1036
+ function releaseAssetNames(value) {
1037
+ if (!Array.isArray(value))
1038
+ return [];
1039
+ return value.flatMap((entry) => {
1040
+ if (!entry || typeof entry !== "object")
1041
+ return [];
1042
+ const name = entry.name;
1043
+ return typeof name === "string" ? [name] : [];
1044
+ });
1045
+ }
1046
+ function uvInstallRoot(options) {
1047
+ return import_node_path5.default.join(options.cacheDir, "tools", "uv");
1048
+ }
1049
+ function uvInstallDirectory(options, version) {
1050
+ return import_node_path5.default.join(uvInstallRoot(options), version);
1051
+ }
1052
+ function uvExecutable(options, version) {
1053
+ return import_node_path5.default.join(uvInstallDirectory(options, version), process.platform === "win32" ? "uv.exe" : "uv");
1054
+ }
1055
+ async function installUv(installDir, release, options) {
883
1056
  const asset = uvAsset();
884
- const baseUrl = `https://github.com/astral-sh/uv/releases/download/${options.uvVersion}`;
1057
+ const baseUrl = `https://github.com/astral-sh/uv/releases/download/${release.tag}`;
885
1058
  const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
886
1059
  const dispatcher = proxyDispatcher(options.env);
887
1060
  try {
@@ -894,30 +1067,42 @@ async function installUv(installDir, options) {
894
1067
  }
895
1068
  const extracted = import_node_path5.default.join(temporary, "extracted");
896
1069
  await import_promises.mkdir(extracted, { recursive: true });
897
- if (asset.endsWith(".zip"))
898
- await import_extract_zip.default(archive, { dir: extracted });
899
- else
900
- await tar.x({ file: archive, cwd: extracted, strict: true });
901
- const source = await findFile(extracted, process.platform === "win32" ? "uv.exe" : "uv");
1070
+ const source = await extractUvExecutable(archive, asset, extracted);
902
1071
  if (!source)
903
1072
  throw new Error(`uv archive did not contain the expected executable: ${asset}`);
904
- await publishUvAtomically(source, installDir, options);
1073
+ await publishUvAtomically(source, installDir, release.version, options);
905
1074
  } finally {
906
1075
  await dispatcher?.close();
907
1076
  await import_promises.rm(temporary, { recursive: true, force: true });
908
1077
  }
909
1078
  }
910
- async function publishUvAtomically(source, installDir, options) {
1079
+ async function extractUvExecutable(archive, asset, destination) {
1080
+ const executableName = process.platform === "win32" ? "uv.exe" : "uv";
1081
+ if (!asset.endsWith(".zip")) {
1082
+ await tar.x({ file: archive, cwd: destination, strict: true });
1083
+ return findFile(destination, executableName);
1084
+ }
1085
+ const entries = import_fflate.unzipSync(await import_promises.readFile(archive), {
1086
+ filter: ({ name }) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName
1087
+ });
1088
+ const matches = Object.entries(entries).filter(([name]) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
1089
+ if (matches.length !== 1 || !matches[0])
1090
+ return null;
1091
+ const source = import_node_path5.default.join(destination, executableName);
1092
+ await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
1093
+ return source;
1094
+ }
1095
+ async function publishUvAtomically(source, installDir, version, options) {
911
1096
  await import_promises.mkdir(installDir, { recursive: true });
912
- const executable = uvExecutable(options);
1097
+ const executable = uvExecutable(options, version);
913
1098
  const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
914
1099
  try {
915
1100
  await import_promises.copyFile(source, staged);
916
1101
  if (process.platform !== "win32")
917
1102
  await import_promises.chmod(staged, 493);
918
1103
  const stagedUv = probeUv(staged, options);
919
- if (!stagedUv || stagedUv.version !== options.uvVersion) {
920
- throw new Error(`Downloaded uv executable is not version ${options.uvVersion}.`);
1104
+ if (!stagedUv || stagedUv.version !== version) {
1105
+ throw new Error(`Downloaded uv executable is not version ${version}.`);
921
1106
  }
922
1107
  if (process.platform === "win32")
923
1108
  await import_promises.rm(executable, { force: true });
@@ -950,9 +1135,10 @@ async function download(url, destination, timeoutMs, dispatcher) {
950
1135
  }
951
1136
  await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
952
1137
  }
953
- async function downloadText(url, timeoutMs, dispatcher) {
1138
+ async function downloadText(url, timeoutMs, dispatcher, headers) {
954
1139
  const response = await import_undici.fetch(url, {
955
1140
  dispatcher,
1141
+ headers,
956
1142
  redirect: "follow",
957
1143
  signal: AbortSignal.timeout(timeoutMs)
958
1144
  });
@@ -1008,4 +1194,4 @@ main().catch((error) => {
1008
1194
  process.exitCode = 1;
1009
1195
  });
1010
1196
 
1011
- //# debugId=450BD20294AA111B64756E2164756E21
1197
+ //# debugId=D02BE720BF896D6864756E2164756E21