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/README.md +19 -16
- package/SECURITY.md +2 -2
- package/dist/cjs/index.js +282 -97
- package/dist/cjs/index.js.map +8 -7
- package/dist/cjs/sync-resolver.js +279 -93
- package/dist/cjs/sync-resolver.js.map +8 -7
- package/dist/cli.js +4 -6
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +2 -2
- package/dist/config.d.ts +10 -14
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -29
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pep440.d.ts +12 -0
- package/dist/pep440.d.ts.map +1 -0
- package/dist/pep440.js +21 -0
- package/dist/pep440.js.map +1 -0
- package/dist/probes.d.ts +1 -1
- package/dist/probes.d.ts.map +1 -1
- package/dist/probes.js +15 -7
- package/dist/probes.js.map +1 -1
- package/dist/provision.d.ts +2 -2
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +254 -58
- package/dist/provision.js.map +1 -1
- package/package.json +7 -6
|
@@ -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
|
|
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
|
-
|
|
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
|
|
123
|
+
var DEFAULT_REQUIRES_PYTHON = ">=3.10,<4";
|
|
110
124
|
var DEFAULT_MANAGED_PYTHON = "3.12";
|
|
111
|
-
var
|
|
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
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
+
requiresPython,
|
|
145
156
|
managedPython,
|
|
146
|
-
|
|
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
|
|
173
|
-
if (!
|
|
174
|
-
throw new TypeError(`Invalid ${
|
|
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
|
|
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 (!
|
|
192
|
-
throw new TypeError(`Managed Python ${request} cannot satisfy
|
|
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
|
-
|
|
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
|
|
521
|
-
return
|
|
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
|
|
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 &&
|
|
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
|
|
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 = {},
|
|
580
|
+
async function ensureEnvironment(options = {}, dependencies = [], dependencyOptions = {}) {
|
|
574
581
|
const resolved = resolveOptions(options);
|
|
575
|
-
const normalized =
|
|
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
|
|
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 &&
|
|
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 || !
|
|
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 &&
|
|
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 || !
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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
|
|
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-
|
|
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-
|
|
750
|
-
const resolved = import_node_path5.default.join(temporary, "resolved-
|
|
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 (
|
|
758
|
-
await import_promises.writeFile(additions, `${
|
|
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 (
|
|
799
|
+
throw new Error(`Compatible uv is unavailable (requires-uv ${options.requiresUv}), and uv installation is disabled.`);
|
|
793
800
|
}
|
|
794
|
-
const
|
|
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(
|
|
803
|
+
key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
|
|
798
804
|
timeoutMs: options.coordinationTimeoutMs,
|
|
799
805
|
check: () => findAvailableUv(options),
|
|
800
806
|
mutate: async () => {
|
|
801
|
-
await
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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 ` +
|
|
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.
|
|
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 || !
|
|
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
|
-
|
|
877
|
-
|
|
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
|
|
880
|
-
|
|
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
|
|
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/${
|
|
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
|
-
|
|
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
|
|
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 !==
|
|
920
|
-
throw new Error(`Downloaded uv executable is not version ${
|
|
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=
|
|
1197
|
+
//# debugId=D02BE720BF896D6864756E2164756E21
|