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.
package/dist/cjs/index.js CHANGED
@@ -66,9 +66,8 @@ var __export = (target, all) => {
66
66
  var exports_src = {};
67
67
  __export(exports_src, {
68
68
  DEFAULT_MANAGED_PYTHON: () => DEFAULT_MANAGED_PYTHON,
69
- DEFAULT_PYTHON_RANGE: () => DEFAULT_PYTHON_RANGE,
70
- DEFAULT_UV_RANGE: () => DEFAULT_UV_RANGE,
71
- DEFAULT_UV_VERSION: () => DEFAULT_UV_VERSION,
69
+ DEFAULT_REQUIRES_PYTHON: () => DEFAULT_REQUIRES_PYTHON,
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);
@@ -106,10 +119,9 @@ function* ancestors(startDirectory) {
106
119
  }
107
120
 
108
121
  // src/config.ts
109
- var DEFAULT_PYTHON_RANGE = ">=3.10.0 <4.0.0";
122
+ var DEFAULT_REQUIRES_PYTHON = ">=3.10,<4";
110
123
  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";
124
+ var DEFAULT_REQUIRES_UV = ">=0.10,<1";
113
125
  var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
114
126
  var DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60000;
115
127
  var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60000;
@@ -122,14 +134,12 @@ var envPathsFunction = (() => {
122
134
  })();
123
135
  function resolveOptions(options = {}) {
124
136
  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);
137
+ const requiresPython = options.requiresPython ?? env["PYFIG_REQUIRES_PYTHON"] ?? DEFAULT_REQUIRES_PYTHON;
138
+ const requiresUv = options.requiresUv ?? env["PYFIG_REQUIRES_UV"] ?? DEFAULT_REQUIRES_UV;
139
+ assertPep440Specifier("requires-python", requiresPython);
140
+ assertPep440Specifier("requires-uv", requiresUv);
129
141
  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);
142
+ assertManagedPythonRequest(managedPython, requiresPython);
133
143
  const probeTimeoutMs = resolveDuration("probeTimeoutMs", options.probeTimeoutMs, env["PYFIG_PROBE_TIMEOUT_MS"], DEFAULT_PROBE_TIMEOUT_MS);
134
144
  const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
135
145
  const downloadTimeoutMs = resolveDuration("downloadTimeoutMs", options.downloadTimeoutMs, env["PYFIG_DOWNLOAD_TIMEOUT_MS"], DEFAULT_DOWNLOAD_TIMEOUT_MS);
@@ -141,10 +151,9 @@ function resolveOptions(options = {}) {
141
151
  const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
142
152
  return {
143
153
  python: options.python ?? env["PYFIG_PYTHON"] ?? "",
144
- pythonRange,
154
+ requiresPython,
145
155
  managedPython,
146
- uvVersion,
147
- uvRange,
156
+ requiresUv,
148
157
  requireEmbedding: options.requireEmbedding ?? parseBoolean(env["PYFIG_REQUIRE_EMBEDDING"], false),
149
158
  requireGil: options.requireGil ?? parseBoolean(env["PYFIG_REQUIRE_GIL"], false),
150
159
  autoInstall,
@@ -169,27 +178,19 @@ function uvEnvironment(options) {
169
178
  UV_NO_CONFIG: "1"
170
179
  };
171
180
  }
172
- function assertRange(tool, range) {
173
- if (!import_semver.default.validRange(range)) {
174
- throw new TypeError(`Invalid ${tool} SemVer range: ${range}`);
181
+ function assertPep440Specifier(name, specifier) {
182
+ if (!isValidPep440Specifier(specifier)) {
183
+ throw new TypeError(`Invalid ${name} PEP 440 specifier: ${specifier}`);
175
184
  }
176
185
  }
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) {
186
+ function assertManagedPythonRequest(request, requiresPython) {
186
187
  if (!request || /[\\/\0\r\n]/u.test(request) || request.includes("..")) {
187
188
  throw new TypeError(`Invalid managed Python request: ${request}`);
188
189
  }
189
190
  if (/^\d+\.\d+(?:\.\d+)?$/u.test(request)) {
190
191
  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}.`);
192
+ if (!pep440VersionSatisfies(normalized, requiresPython)) {
193
+ throw new TypeError(`Managed Python ${request} cannot satisfy requires-python ${requiresPython}.`);
193
194
  }
194
195
  }
195
196
  }
@@ -222,7 +223,7 @@ var import_node_os = __toESM(require("node:os"), 1);
222
223
  var import_node_path5 = __toESM(require("node:path"), 1);
223
224
  var import_node_stream = require("node:stream");
224
225
  var import_promises2 = require("node:stream/promises");
225
- var import_extract_zip = __toESM(require("extract-zip"), 1);
226
+ var import_fflate = require("fflate");
226
227
  var tar = __toESM(require("tar"), 1);
227
228
  var import_undici = require("undici");
228
229
 
@@ -369,9 +370,6 @@ function assertNoTargetOverride(args) {
369
370
  }
370
371
  }
371
372
 
372
- // src/probes.ts
373
- var import_semver2 = __toESM(require("semver"), 1);
374
-
375
373
  // src/process.ts
376
374
  var import_node_child_process = require("node:child_process");
377
375
  var import_node_path4 = __toESM(require("node:path"), 1);
@@ -512,7 +510,15 @@ if os.path.isfile(header_path) and library_path:
512
510
  }
513
511
 
514
512
  release_level = sys.version_info.releaselevel
515
- release_suffix = "" if release_level == "final" else f"-{release_level}.{sys.version_info.serial}"
513
+ release_suffixes = {
514
+ "alpha": "a",
515
+ "beta": "b",
516
+ "candidate": "rc",
517
+ "final": "",
518
+ }
519
+ release_suffix = release_suffixes[release_level]
520
+ if release_suffix:
521
+ release_suffix += str(sys.version_info.serial)
516
522
 
517
523
  print(json.dumps({
518
524
  "executable": sys.executable,
@@ -532,8 +538,8 @@ print(json.dumps({
532
538
  "embedding": embedding,
533
539
  }))
534
540
  `;
535
- function pythonSatisfiesRequirements(python, options) {
536
- return import_semver2.default.satisfies(python.version, options.pythonRange) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
541
+ function pythonSatisfiesPolicy(python, options) {
542
+ return pep440VersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
537
543
  }
538
544
  async function probePython(candidate, options) {
539
545
  const executable = await resolveExecutable(candidate.executable, options.env);
@@ -542,7 +548,7 @@ async function probePython(candidate, options) {
542
548
  try {
543
549
  const { stdout } = run(executable, [...candidate.args, "-c", PYTHON_PROBE], options.env, options.probeTimeoutMs);
544
550
  const info = JSON.parse(stdout.trim());
545
- return import_semver2.default.valid(info.version) ? info : null;
551
+ return normalizePep440Version(info.version) ? info : null;
546
552
  } catch {
547
553
  return null;
548
554
  }
@@ -551,7 +557,7 @@ async function findUv(options) {
551
557
  const configured = options.env["PYFIG_UV"] ?? "uv";
552
558
  const executable = await resolveExecutable(configured, options.env);
553
559
  const uv = executable ? probeUv(executable, options) : null;
554
- return uv && import_semver2.default.satisfies(uv.version, options.uvRange) ? uv : null;
560
+ return uv && pep440VersionSatisfies(uv.version, options.requiresUv) ? uv : null;
555
561
  }
556
562
  function probeUv(executable, options) {
557
563
  try {
@@ -565,7 +571,7 @@ function probeUv(executable, options) {
565
571
  function normalizeVersion(value) {
566
572
  if (!value)
567
573
  return null;
568
- return import_semver2.default.valid(value) ?? import_semver2.default.coerce(value)?.version ?? null;
574
+ return normalizePep440Version(value);
569
575
  }
570
576
  function pythonArchitectureMatchesRuntime(python) {
571
577
  const architecture = python.architecture.toLowerCase();
@@ -585,14 +591,14 @@ function pythonArchitectureMatchesRuntime(python) {
585
591
  async function ensureUv(options = {}) {
586
592
  return ensureUvResolved(resolveOptions(options));
587
593
  }
588
- async function ensureEnvironment(options = {}, requirements = [], dependencyOptions = {}) {
594
+ async function ensureEnvironment(options = {}, dependencies = [], dependencyOptions = {}) {
589
595
  const resolved = resolveOptions(options);
590
- const normalized = normalizeRequirements(requirements);
596
+ const normalized = normalizeDependencies(dependencies);
591
597
  const selections = normalizeDependencyOptions(dependencyOptions);
592
598
  const uv = await ensureUvResolved(resolved);
593
599
  const python = await ensureVirtualEnvironmentResolved(resolved, uv);
594
600
  if (normalized.length > 0 || selections.extras.length > 0 || selections.groups.length > 0 || selections.allExtras) {
595
- await installRuntimeRequirements(uv, python, normalized, selections, resolved);
601
+ await installRuntimeDependencies(uv, python, normalized, selections, resolved);
596
602
  }
597
603
  return {
598
604
  python,
@@ -607,7 +613,7 @@ async function ensureVirtualEnvironmentResolved(options, uv) {
607
613
  const directory = virtualEnvironmentDirectory(options.projectRoot);
608
614
  const check = async () => {
609
615
  const candidate = await inspectVirtualEnvironment(directory, options);
610
- return candidate && pythonSatisfiesRequirements(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
616
+ return candidate && pythonSatisfiesPolicy(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
611
617
  };
612
618
  if (!options.createVirtualEnvironment) {
613
619
  throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
@@ -640,7 +646,7 @@ async function ensureVirtualEnvironmentResolved(options, uv) {
640
646
  async function findReusableEnvironment(options) {
641
647
  if (options.python) {
642
648
  const configured = await probePython({ executable: options.python, args: [] }, options);
643
- if (!configured || !pythonSatisfiesRequirements(configured, options)) {
649
+ if (!configured || !pythonSatisfiesPolicy(configured, options)) {
644
650
  throw incompatibleConfiguredPython(options);
645
651
  }
646
652
  if (configured.virtualEnvironment)
@@ -651,7 +657,7 @@ async function findReusableEnvironment(options) {
651
657
  virtualEnvironmentDirectory(options.projectRoot)
652
658
  ]) {
653
659
  const info = await inspectVirtualEnvironment(directory, options);
654
- if (info && pythonSatisfiesRequirements(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
660
+ if (info && pythonSatisfiesPolicy(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
655
661
  return info;
656
662
  }
657
663
  }
@@ -660,13 +666,13 @@ async function findReusableEnvironment(options) {
660
666
  async function ensureBasePythonResolved(uv, options) {
661
667
  if (options.python) {
662
668
  const configured = await probePython({ executable: options.python, args: [] }, options);
663
- if (!configured || !pythonSatisfiesRequirements(configured, options)) {
669
+ if (!configured || !pythonSatisfiesPolicy(configured, options)) {
664
670
  throw incompatibleConfiguredPython(options);
665
671
  }
666
672
  if (!configured.virtualEnvironment)
667
673
  return configured;
668
674
  const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
669
- if (base && pythonSatisfiesRequirements(base, options))
675
+ if (base && pythonSatisfiesPolicy(base, options))
670
676
  return base;
671
677
  throw incompatibleConfiguredPython(options);
672
678
  }
@@ -708,7 +714,7 @@ async function findPythonWithUv(uv, options, source) {
708
714
  try {
709
715
  const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
710
716
  const info = await probePython({ executable: stdout.trim(), args: [] }, options);
711
- if (info && pythonSatisfiesRequirements(info, options))
717
+ if (info && pythonSatisfiesPolicy(info, options))
712
718
  return info;
713
719
  } catch {}
714
720
  }
@@ -726,7 +732,7 @@ async function findPythonWithUv(uv, options, source) {
726
732
  const candidates = installedPythonPaths(stdout);
727
733
  for (const executable of candidates) {
728
734
  const info = await probePython({ executable, args: [] }, options);
729
- if (info && !info.virtualEnvironment && pythonSatisfiesRequirements(info, options)) {
735
+ if (info && !info.virtualEnvironment && pythonSatisfiesPolicy(info, options)) {
730
736
  return info;
731
737
  }
732
738
  }
@@ -750,7 +756,7 @@ async function inspectVirtualEnvironment(directory, options) {
750
756
  const info = await probePython({ executable, args: [] }, options);
751
757
  return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
752
758
  }
753
- async function installRuntimeRequirements(uv, python, requirements, dependencyOptions, options) {
759
+ async function installRuntimeDependencies(uv, python, dependencies, dependencyOptions, options) {
754
760
  if (!python.virtualEnvironment) {
755
761
  throw new Error("Runtime dependency installation requires a virtual environment.");
756
762
  }
@@ -759,18 +765,18 @@ async function installRuntimeRequirements(uv, python, requirements, dependencyOp
759
765
  timeoutMs: options.coordinationTimeoutMs,
760
766
  check: async () => null,
761
767
  mutate: async () => {
762
- const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-requirements-"));
768
+ const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-dependencies-"));
763
769
  try {
764
- const additions = import_node_path5.default.join(temporary, "runtime-requirements.txt");
765
- const resolved = import_node_path5.default.join(temporary, "resolved-requirements.txt");
770
+ const additions = import_node_path5.default.join(temporary, "runtime-dependencies.txt");
771
+ const resolved = import_node_path5.default.join(temporary, "resolved-dependencies.txt");
766
772
  const pyproject = import_node_path5.default.join(options.projectRoot, "pyproject.toml");
767
773
  const hasPyproject = import_node_fs4.default.existsSync(pyproject);
768
774
  if (!hasPyproject && (dependencyOptions.extras.length > 0 || dependencyOptions.groups.length > 0 || dependencyOptions.allExtras)) {
769
775
  throw new Error("Runtime extras and groups require an existing pyproject.toml.");
770
776
  }
771
777
  const sources = hasPyproject ? [pyproject] : [];
772
- if (requirements.length > 0) {
773
- await import_promises.writeFile(additions, `${requirements.join(`
778
+ if (dependencies.length > 0) {
779
+ await import_promises.writeFile(additions, `${dependencies.join(`
774
780
  `)}
775
781
  `, "utf8");
776
782
  sources.push(additions);
@@ -804,21 +810,30 @@ async function ensureUvResolved(options) {
804
810
  if (existing)
805
811
  return existing;
806
812
  if (!options.installUv) {
807
- 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.`);
808
814
  }
809
- const installDir = uvInstallDirectory(options);
810
- const executable = uvExecutable(options);
815
+ const policyKey = import_node_crypto.createHash("sha256").update(options.requiresUv).digest("hex");
811
816
  return withProvisioningLock({
812
- key: `uv:${canonicalPath(installDir)}`,
817
+ key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
813
818
  timeoutMs: options.coordinationTimeoutMs,
814
819
  check: () => findAvailableUv(options),
815
820
  mutate: async () => {
816
- await installUv(installDir, options);
817
- const result = probeUv(executable, options);
818
- if (!result || result.version !== options.uvVersion) {
819
- throw new Error(`Expected managed uv ${options.uvVersion}, but the installed executable did not match.`);
820
- }
821
- 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
+ });
822
837
  }
823
838
  });
824
839
  }
@@ -826,14 +841,27 @@ async function findAvailableUv(options) {
826
841
  const external = await findUv(options);
827
842
  if (external)
828
843
  return external;
829
- const managed = probeUv(uvExecutable(options), options);
830
- return managed?.version === options.uvVersion ? managed : null;
831
- }
832
- function normalizeRequirements(requirements) {
833
- const normalized = requirements.map((requirement) => requirement.trim());
834
- for (const requirement of normalized) {
835
- if (!requirement || /[\0\r\n]/u.test(requirement)) {
836
- throw new TypeError(`Invalid runtime requirement: ${JSON.stringify(requirement)}`);
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;
859
+ }
860
+ function normalizeDependencies(dependencies) {
861
+ const normalized = dependencies.map((dependency) => dependency.trim());
862
+ for (const dependency of normalized) {
863
+ if (!dependency || /[\0\r\n]/u.test(dependency)) {
864
+ throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
837
865
  }
838
866
  }
839
867
  return [...new Set(normalized)];
@@ -870,17 +898,17 @@ function targetUvEnvironment(options, python) {
870
898
  return uvCommandEnvironment(uvEnvironment(options), python);
871
899
  }
872
900
  function incompatibleConfiguredPython(options) {
873
- return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `${options.pythonRange}${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
901
+ return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `requires-python ${options.requiresPython}` + `${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
874
902
  }
875
903
  function missingPythonMessage(options, reason) {
876
904
  const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
877
- return `No Python satisfies ${options.pythonRange}${embedding}; ${reason}.`;
905
+ return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
878
906
  }
879
907
  async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
880
908
  if (!options.python)
881
909
  return true;
882
910
  const configured = await probePython({ executable: options.python, args: [] }, options);
883
- if (!configured || !pythonSatisfiesRequirements(configured, options))
911
+ if (!configured || !pythonSatisfiesPolicy(configured, options))
884
912
  return false;
885
913
  if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
886
914
  return true;
@@ -888,15 +916,159 @@ async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, opt
888
916
  const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
889
917
  return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
890
918
  }
891
- function uvInstallDirectory(options) {
892
- 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
+ }
893
1046
  }
894
- function uvExecutable(options) {
895
- return import_node_path5.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
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
+ });
896
1059
  }
897
- async function installUv(installDir, options) {
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);
1065
+ }
1066
+ function uvExecutable(options, version) {
1067
+ return import_node_path5.default.join(uvInstallDirectory(options, version), process.platform === "win32" ? "uv.exe" : "uv");
1068
+ }
1069
+ async function installUv(installDir, release, options) {
898
1070
  const asset = uvAsset();
899
- 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}`;
900
1072
  const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
901
1073
  const dispatcher = proxyDispatcher(options.env);
902
1074
  try {
@@ -909,30 +1081,42 @@ async function installUv(installDir, options) {
909
1081
  }
910
1082
  const extracted = import_node_path5.default.join(temporary, "extracted");
911
1083
  await import_promises.mkdir(extracted, { recursive: true });
912
- if (asset.endsWith(".zip"))
913
- await import_extract_zip.default(archive, { dir: extracted });
914
- else
915
- await tar.x({ file: archive, cwd: extracted, strict: true });
916
- const source = await findFile(extracted, process.platform === "win32" ? "uv.exe" : "uv");
1084
+ const source = await extractUvExecutable(archive, asset, extracted);
917
1085
  if (!source)
918
1086
  throw new Error(`uv archive did not contain the expected executable: ${asset}`);
919
- await publishUvAtomically(source, installDir, options);
1087
+ await publishUvAtomically(source, installDir, release.version, options);
920
1088
  } finally {
921
1089
  await dispatcher?.close();
922
1090
  await import_promises.rm(temporary, { recursive: true, force: true });
923
1091
  }
924
1092
  }
925
- async function publishUvAtomically(source, installDir, options) {
1093
+ async function extractUvExecutable(archive, asset, destination) {
1094
+ const executableName = process.platform === "win32" ? "uv.exe" : "uv";
1095
+ if (!asset.endsWith(".zip")) {
1096
+ await tar.x({ file: archive, cwd: destination, strict: true });
1097
+ return findFile(destination, executableName);
1098
+ }
1099
+ const entries = import_fflate.unzipSync(await import_promises.readFile(archive), {
1100
+ filter: ({ name }) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName
1101
+ });
1102
+ const matches = Object.entries(entries).filter(([name]) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
1103
+ if (matches.length !== 1 || !matches[0])
1104
+ return null;
1105
+ const source = import_node_path5.default.join(destination, executableName);
1106
+ await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
1107
+ return source;
1108
+ }
1109
+ async function publishUvAtomically(source, installDir, version, options) {
926
1110
  await import_promises.mkdir(installDir, { recursive: true });
927
- const executable = uvExecutable(options);
1111
+ const executable = uvExecutable(options, version);
928
1112
  const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
929
1113
  try {
930
1114
  await import_promises.copyFile(source, staged);
931
1115
  if (process.platform !== "win32")
932
1116
  await import_promises.chmod(staged, 493);
933
1117
  const stagedUv = probeUv(staged, options);
934
- if (!stagedUv || stagedUv.version !== options.uvVersion) {
935
- 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}.`);
936
1120
  }
937
1121
  if (process.platform === "win32")
938
1122
  await import_promises.rm(executable, { force: true });
@@ -965,9 +1149,10 @@ async function download(url, destination, timeoutMs, dispatcher) {
965
1149
  }
966
1150
  await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
967
1151
  }
968
- async function downloadText(url, timeoutMs, dispatcher) {
1152
+ async function downloadText(url, timeoutMs, dispatcher, headers) {
969
1153
  const response = await import_undici.fetch(url, {
970
1154
  dispatcher,
1155
+ headers,
971
1156
  redirect: "follow",
972
1157
  signal: AbortSignal.timeout(timeoutMs)
973
1158
  });
@@ -1091,7 +1276,7 @@ function createPyfig(options = {}) {
1091
1276
  const baseEnvironment = import_loqkit2.memoize(() => ensureEnvironment(scopedOptions));
1092
1277
  const uvInfo = import_loqkit2.memoize(() => ensureUv(scopedOptions));
1093
1278
  return {
1094
- prepare: (requirements = [], dependencyOptions = {}) => requirements.length === 0 && !dependencyOptions.allExtras && (dependencyOptions.extras?.length ?? 0) === 0 && (dependencyOptions.groups?.length ?? 0) === 0 ? baseEnvironment() : ensureEnvironment(scopedOptions, requirements, dependencyOptions),
1279
+ prepare: (dependencies = [], dependencyOptions = {}) => dependencies.length === 0 && !dependencyOptions.allExtras && (dependencyOptions.extras?.length ?? 0) === 0 && (dependencyOptions.groups?.length ?? 0) === 0 ? baseEnvironment() : ensureEnvironment(scopedOptions, dependencies, dependencyOptions),
1095
1280
  pythonInfo: async () => (await baseEnvironment()).python,
1096
1281
  uvInfo,
1097
1282
  uv: (args = [], spawnOptions = {}) => uvSpawn(args, spawnOptions, scopedOptions),
@@ -1099,4 +1284,4 @@ function createPyfig(options = {}) {
1099
1284
  };
1100
1285
  }
1101
1286
 
1102
- //# debugId=B84AA2037417535D64756E2164756E21
1287
+ //# debugId=25C05D1900A6412264756E2164756E21