mirror of https://github.com/astral-sh/uv
Add `UV_PYTHON_DOWNLOADS_JSON_URL` to set custom managed python sources (#10939)
## Summary Add an option to overwrite the list of available Python downloads from a local JSON file by using the environment variable `UV_PYTHON_DOWNLOADS_JSON_URL` as an experimental support for providing custom sources for Python distribution binaries #8015 related #10203 I probably should make the JSON to be fetched from a remote URL instead of a local file. please let me know what you think and I will modify the code accordingly. ## Test Plan ### normal run ``` root@75c66494ba8b:/# /code/target/release/uv python list cpython-3.14.0a4+freethreaded-linux-x86_64-gnu <download available> cpython-3.14.0a4-linux-x86_64-gnu <download available> cpython-3.13.1+freethreaded-linux-x86_64-gnu <download available> cpython-3.13.1-linux-x86_64-gnu <download available> cpython-3.12.8-linux-x86_64-gnu <download available> cpython-3.11.11-linux-x86_64-gnu <download available> cpython-3.10.16-linux-x86_64-gnu <download available> cpython-3.9.21-linux-x86_64-gnu <download available> cpython-3.8.20-linux-x86_64-gnu <download available> cpython-3.7.9-linux-x86_64-gnu <download available> pypy-3.10.14-linux-x86_64-gnu <download available> pypy-3.9.19-linux-x86_64-gnu <download available> pypy-3.8.16-linux-x86_64-gnu <download available> pypy-3.7.13-linux-x86_64-gnu <download available> ``` ### empty JSON file ```sh root@75c66494ba8b:/# export UV_PYTHON_DOWNLOADS_JSON_URL=/code/crates/uv-python/my-download-metadata.json root@75c66494ba8b:/# cat $UV_PYTHON_DOWNLOADS_JSON_URL {} root@75c66494ba8b:/# /code/target/release/uv python list root@75c66494ba8b:/# ``` ### JSON file with valid version ```sh root@75c66494ba8b:/# export UV_PYTHON_DOWNLOADS_JSON_URL=/code/crates/uv-python/my-download-metadata.json root@75c66494ba8b:/# cat $UV_PYTHON_DOWNLOADS_JSON_URL { "cpython-3.11.9-linux-x86_64-gnu": { "name": "cpython", "arch": { "family": "x86_64", "variant": null }, "os": "linux", "libc": "gnu", "major": 3, "minor": 11, "patch": 9, "prerelease": "", "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20240814/cpython-3.11.9%2B20240814-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", "sha256": "daa487c7e73005c4426ac393273117cf0e2dc4ab9b2eeda366e04cd00eea00c9", "variant": null } } root@75c66494ba8b:/# /code/target/release/uv python list cpython-3.11.9-linux-x86_64-gnu <download available> root@75c66494ba8b:/# ``` ### Remote Path ```sh root@75c66494ba8b:/# export UV_PYTHON_DOWNLOADS_JSON_URL=http://a.com/file.json root@75c66494ba8b:/# /code/target/release/uv python list error: Remote python downloads JSON is not yet supported, please use a local path (without `file://` prefix) ``` --------- Co-authored-by: Aria Desires <aria.desires@gmail.com>
This commit is contained in:
parent
c0ed5693a7
commit
2b62f73064
|
|
@ -24,7 +24,7 @@ jobs:
|
||||||
- name: Sync Python Releases
|
- name: Sync Python Releases
|
||||||
run: |
|
run: |
|
||||||
uv run -- fetch-download-metadata.py
|
uv run -- fetch-download-metadata.py
|
||||||
uv run -- template-download-metadata.py
|
uv run -- minify-download-metadata.py
|
||||||
working-directory: ./crates/uv-python
|
working-directory: ./crates/uv-python
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
@ -35,7 +35,7 @@ jobs:
|
||||||
commit-message: "Sync latest Python releases"
|
commit-message: "Sync latest Python releases"
|
||||||
add-paths: |
|
add-paths: |
|
||||||
crates/uv-python/download-metadata.json
|
crates/uv-python/download-metadata.json
|
||||||
crates/uv-python/src/downloads.inc
|
crates/uv-python/src/download-metadata-minified.json
|
||||||
branch: "sync-python-releases"
|
branch: "sync-python-releases"
|
||||||
title: "Sync latest Python releases"
|
title: "Sync latest Python releases"
|
||||||
body: "Automated update for Python releases."
|
body: "Automated update for Python releases."
|
||||||
|
|
|
||||||
|
|
@ -5522,6 +5522,7 @@ dependencies = [
|
||||||
"indoc",
|
"indoc",
|
||||||
"insta",
|
"insta",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
"once_cell",
|
||||||
"owo-colors",
|
"owo-colors",
|
||||||
"procfs",
|
"procfs",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ memchr = { version = "2.7.4" }
|
||||||
miette = { version = "7.2.0", features = ["fancy-no-backtrace"] }
|
miette = { version = "7.2.0", features = ["fancy-no-backtrace"] }
|
||||||
nanoid = { version = "0.4.0" }
|
nanoid = { version = "0.4.0" }
|
||||||
nix = { version = "0.29.0" }
|
nix = { version = "0.29.0" }
|
||||||
|
once_cell = { version = "1.20.2" }
|
||||||
owo-colors = { version = "4.1.0" }
|
owo-colors = { version = "4.1.0" }
|
||||||
path-slash = { version = "0.2.1" }
|
path-slash = { version = "0.2.1" }
|
||||||
pathdiff = { version = "0.2.1" }
|
pathdiff = { version = "0.2.1" }
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ tokio-util = { workspace = true, features = ["compat"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
which = { workspace = true }
|
which = { workspace = true }
|
||||||
|
once_cell = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
procfs = { workspace = true }
|
procfs = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.12"
|
||||||
|
# ///
|
||||||
|
"""
|
||||||
|
Generate minified Python version download metadata json to embed in the binary.
|
||||||
|
|
||||||
|
Generates the `download-metadata-minified.json` file from the `download-metadata.json` file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
uv run -- crates/uv-python/minify-download-metadata.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CRATE_ROOT = Path(__file__).parent
|
||||||
|
VERSION_METADATA = CRATE_ROOT / "download-metadata.json"
|
||||||
|
TARGET = CRATE_ROOT / "src" / "download-metadata-minified.json"
|
||||||
|
|
||||||
|
|
||||||
|
def process_json(data: dict) -> dict:
|
||||||
|
out_data = {}
|
||||||
|
|
||||||
|
for key, value in data.items():
|
||||||
|
# Exclude debug variants for now, we don't support them in the Rust side
|
||||||
|
if value["variant"] == "debug":
|
||||||
|
continue
|
||||||
|
|
||||||
|
out_data[key] = value
|
||||||
|
|
||||||
|
return out_data
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
json_data = json.loads(Path(VERSION_METADATA).read_text())
|
||||||
|
json_data = process_json(json_data)
|
||||||
|
json_string = json.dumps(json_data, separators=(",", ":"))
|
||||||
|
TARGET.write_text(json_string)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
|
@ -1,39 +0,0 @@
|
||||||
// Generated with `{{generated_with}}`
|
|
||||||
// From template at `{{generated_from}}`
|
|
||||||
|
|
||||||
use uv_pep440::{Prerelease, PrereleaseKind};
|
|
||||||
use crate::PythonVariant;
|
|
||||||
use crate::platform::ArchVariant;
|
|
||||||
|
|
||||||
pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[
|
|
||||||
{{#versions}}
|
|
||||||
ManagedPythonDownload {
|
|
||||||
key: PythonInstallationKey {
|
|
||||||
major: {{value.major}},
|
|
||||||
minor: {{value.minor}},
|
|
||||||
patch: {{value.patch}},
|
|
||||||
prerelease: {{value.prerelease}},
|
|
||||||
implementation: LenientImplementationName::Known(ImplementationName::{{value.name}}),
|
|
||||||
arch: Arch{
|
|
||||||
family: target_lexicon::Architecture::{{value.arch_family}},
|
|
||||||
variant: {{value.arch_variant}},
|
|
||||||
},
|
|
||||||
os: Os(target_lexicon::OperatingSystem::{{value.os}}),
|
|
||||||
{{#value.libc}}
|
|
||||||
libc: Libc::Some(target_lexicon::Environment::{{.}}),
|
|
||||||
{{/value.libc}}
|
|
||||||
{{^value.libc}}
|
|
||||||
libc: Libc::None,
|
|
||||||
{{/value.libc}}
|
|
||||||
variant: {{value.variant}}
|
|
||||||
},
|
|
||||||
url: "{{value.url}}",
|
|
||||||
{{#value.sha256}}
|
|
||||||
sha256: Some("{{.}}")
|
|
||||||
{{/value.sha256}}
|
|
||||||
{{^value.sha256}}
|
|
||||||
sha256: None
|
|
||||||
{{/value.sha256}}
|
|
||||||
},
|
|
||||||
{{/versions}}
|
|
||||||
];
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -7,8 +9,11 @@ use std::task::{Context, Poll};
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use reqwest_retry::RetryPolicy;
|
use reqwest_retry::RetryPolicy;
|
||||||
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::io::{AsyncRead, ReadBuf};
|
use tokio::io::{AsyncRead, ReadBuf};
|
||||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||||
|
|
@ -30,6 +35,7 @@ use crate::installation::PythonInstallationKey;
|
||||||
use crate::libc::LibcDetectionError;
|
use crate::libc::LibcDetectionError;
|
||||||
use crate::managed::ManagedPythonInstallation;
|
use crate::managed::ManagedPythonInstallation;
|
||||||
use crate::platform::{self, Arch, Libc, Os};
|
use crate::platform::{self, Arch, Libc, Os};
|
||||||
|
use crate::PythonVariant;
|
||||||
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
|
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|
@ -86,9 +92,13 @@ pub enum Error {
|
||||||
Mirror(&'static str, &'static str),
|
Mirror(&'static str, &'static str),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
LibcDetection(#[from] LibcDetectionError),
|
LibcDetection(#[from] LibcDetectionError),
|
||||||
|
#[error("Remote python downloads JSON is not yet supported, please use a local path (without `file://` prefix)")]
|
||||||
|
RemoteJSONNotSupported(),
|
||||||
|
#[error("The json of the python downloads is invalid: {0}")]
|
||||||
|
InvalidPythonDownloadsJSON(String, #[source] serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct ManagedPythonDownload {
|
pub struct ManagedPythonDownload {
|
||||||
key: PythonInstallationKey,
|
key: PythonInstallationKey,
|
||||||
url: &'static str,
|
url: &'static str,
|
||||||
|
|
@ -245,9 +255,11 @@ impl PythonDownloadRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over all [`PythonDownload`]'s that match this request.
|
/// Iterate over all [`PythonDownload`]'s that match this request.
|
||||||
pub fn iter_downloads(&self) -> impl Iterator<Item = &'static ManagedPythonDownload> + '_ {
|
pub fn iter_downloads(
|
||||||
ManagedPythonDownload::iter_all()
|
&self,
|
||||||
.filter(move |download| self.satisfied_by_download(download))
|
) -> Result<impl Iterator<Item = &'static ManagedPythonDownload> + use<'_>, Error> {
|
||||||
|
Ok(ManagedPythonDownload::iter_all()?
|
||||||
|
.filter(move |download| self.satisfied_by_download(download)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this request is satisfied by an installation key.
|
/// Whether this request is satisfied by an installation key.
|
||||||
|
|
@ -445,7 +457,30 @@ impl FromStr for PythonDownloadRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
include!("downloads.inc");
|
const BUILTIN_PYTHON_DOWNLOADS_JSON: &str = include_str!("download-metadata-minified.json");
|
||||||
|
static PYTHON_DOWNLOADS: OnceCell<std::borrow::Cow<'static, [ManagedPythonDownload]>> =
|
||||||
|
OnceCell::new();
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
struct JsonPythonDownload {
|
||||||
|
name: String,
|
||||||
|
arch: JsonArch,
|
||||||
|
os: String,
|
||||||
|
libc: String,
|
||||||
|
major: u8,
|
||||||
|
minor: u8,
|
||||||
|
patch: u8,
|
||||||
|
prerelease: Option<String>,
|
||||||
|
url: String,
|
||||||
|
sha256: Option<String>,
|
||||||
|
variant: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
struct JsonArch {
|
||||||
|
family: String,
|
||||||
|
variant: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum DownloadResult {
|
pub enum DownloadResult {
|
||||||
|
|
@ -459,14 +494,40 @@ impl ManagedPythonDownload {
|
||||||
request: &PythonDownloadRequest,
|
request: &PythonDownloadRequest,
|
||||||
) -> Result<&'static ManagedPythonDownload, Error> {
|
) -> Result<&'static ManagedPythonDownload, Error> {
|
||||||
request
|
request
|
||||||
.iter_downloads()
|
.iter_downloads()?
|
||||||
.next()
|
.next()
|
||||||
.ok_or(Error::NoDownloadFound(request.clone()))
|
.ok_or(Error::NoDownloadFound(request.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over all [`ManagedPythonDownload`]s.
|
/// Iterate over all [`ManagedPythonDownload`]s.
|
||||||
pub fn iter_all() -> impl Iterator<Item = &'static ManagedPythonDownload> {
|
pub fn iter_all() -> Result<impl Iterator<Item = &'static ManagedPythonDownload>, Error> {
|
||||||
PYTHON_DOWNLOADS.iter()
|
let runtime_source = std::env::var(EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL);
|
||||||
|
|
||||||
|
let downloads = PYTHON_DOWNLOADS.get_or_try_init(|| {
|
||||||
|
let json_downloads: HashMap<String, JsonPythonDownload> =
|
||||||
|
if let Ok(json_source) = &runtime_source {
|
||||||
|
if Url::parse(json_source).is_ok() {
|
||||||
|
return Err(Error::RemoteJSONNotSupported());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = match fs_err::File::open(json_source) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(e) => { Err(Error::Io(e)) }?,
|
||||||
|
};
|
||||||
|
|
||||||
|
serde_json::from_reader(file)
|
||||||
|
.map_err(|e| Error::InvalidPythonDownloadsJSON(json_source.clone(), e))?
|
||||||
|
} else {
|
||||||
|
serde_json::from_str(BUILTIN_PYTHON_DOWNLOADS_JSON).map_err(|e| {
|
||||||
|
Error::InvalidPythonDownloadsJSON("EMBEDDED IN THE BINARY".to_string(), e)
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = parse_json_downloads(json_downloads);
|
||||||
|
Ok(Cow::Owned(result))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(downloads.iter())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn url(&self) -> &'static str {
|
pub fn url(&self) -> &'static str {
|
||||||
|
|
@ -702,6 +763,115 @@ impl ManagedPythonDownload {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_json_downloads(
|
||||||
|
json_downloads: HashMap<String, JsonPythonDownload>,
|
||||||
|
) -> Vec<ManagedPythonDownload> {
|
||||||
|
json_downloads
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(key, entry)| {
|
||||||
|
let implementation = match entry.name.as_str() {
|
||||||
|
"cpython" => LenientImplementationName::Known(ImplementationName::CPython),
|
||||||
|
"pypy" => LenientImplementationName::Known(ImplementationName::PyPy),
|
||||||
|
_ => LenientImplementationName::Unknown(entry.name.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let arch_str = match entry.arch.family.as_str() {
|
||||||
|
"armv5tel" => "armv5te".to_string(),
|
||||||
|
// The `gc` variant of riscv64 is the common base instruction set and
|
||||||
|
// is the target in `python-build-standalone`
|
||||||
|
// See https://github.com/astral-sh/python-build-standalone/issues/504
|
||||||
|
"riscv64" => "riscv64gc".to_string(),
|
||||||
|
value => value.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let arch_str = if let Some(variant) = entry.arch.variant {
|
||||||
|
format!("{arch_str}_{variant}")
|
||||||
|
} else {
|
||||||
|
arch_str
|
||||||
|
};
|
||||||
|
|
||||||
|
let arch = match Arch::from_str(&arch_str) {
|
||||||
|
Ok(arch) => arch,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Skipping entry {key}: Invalid arch '{arch_str}' - {e}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let os = match Os::from_str(&entry.os) {
|
||||||
|
Ok(os) => os,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Skipping entry {}: Invalid OS '{}' - {}", key, entry.os, e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let libc = match Libc::from_str(&entry.libc) {
|
||||||
|
Ok(libc) => libc,
|
||||||
|
Err(e) => {
|
||||||
|
debug!(
|
||||||
|
"Skipping entry {}: Invalid libc '{}' - {}",
|
||||||
|
key, entry.libc, e
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let variant = match entry
|
||||||
|
.variant
|
||||||
|
.as_deref()
|
||||||
|
.map(PythonVariant::from_str)
|
||||||
|
.transpose()
|
||||||
|
{
|
||||||
|
Ok(Some(variant)) => variant,
|
||||||
|
Ok(None) => PythonVariant::default(),
|
||||||
|
Err(()) => {
|
||||||
|
debug!(
|
||||||
|
"Skipping entry {key}: Unknown python variant - {}",
|
||||||
|
entry.variant.unwrap_or_default()
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let version_str = format!(
|
||||||
|
"{}.{}.{}{}",
|
||||||
|
entry.major,
|
||||||
|
entry.minor,
|
||||||
|
entry.patch,
|
||||||
|
entry.prerelease.as_deref().unwrap_or_default()
|
||||||
|
);
|
||||||
|
|
||||||
|
let version = match PythonVersion::from_str(&version_str) {
|
||||||
|
Ok(version) => version,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Skipping entry {key}: Invalid version '{version_str}' - {e}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = Box::leak(entry.url.into_boxed_str()) as &'static str;
|
||||||
|
let sha256 = entry
|
||||||
|
.sha256
|
||||||
|
.map(|s| Box::leak(s.into_boxed_str()) as &'static str);
|
||||||
|
|
||||||
|
Some(ManagedPythonDownload {
|
||||||
|
key: PythonInstallationKey::new_from_version(
|
||||||
|
implementation,
|
||||||
|
&version,
|
||||||
|
os,
|
||||||
|
arch,
|
||||||
|
libc,
|
||||||
|
variant,
|
||||||
|
),
|
||||||
|
url,
|
||||||
|
sha256,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.sorted_by(|a, b| Ord::cmp(&b.key, &a.key))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
pub(crate) fn from_reqwest(url: Url, err: reqwest::Error) -> Self {
|
pub(crate) fn from_reqwest(url: Url, err: reqwest::Error) -> Self {
|
||||||
Self::NetworkError(url, WrappedReqwestError::from(err))
|
Self::NetworkError(url, WrappedReqwestError::from(err))
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,7 @@ impl PythonInstallationKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_from_version(
|
pub fn new_from_version(
|
||||||
implementation: LenientImplementationName,
|
implementation: LenientImplementationName,
|
||||||
version: &PythonVersion,
|
version: &PythonVersion,
|
||||||
os: Os,
|
os: Os,
|
||||||
|
|
@ -482,6 +482,6 @@ impl Ord for PythonInstallationKey {
|
||||||
.then_with(|| self.os.to_string().cmp(&other.os.to_string()))
|
.then_with(|| self.os.to_string().cmp(&other.os.to_string()))
|
||||||
.then_with(|| self.arch.to_string().cmp(&other.arch.to_string()))
|
.then_with(|| self.arch.to_string().cmp(&other.arch.to_string()))
|
||||||
.then_with(|| self.libc.to_string().cmp(&other.libc.to_string()))
|
.then_with(|| self.libc.to_string().cmp(&other.libc.to_string()))
|
||||||
.then_with(|| self.variant.cmp(&other.variant))
|
.then_with(|| self.variant.cmp(&other.variant).reverse()) // we want Default to come first
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
# /// script
|
|
||||||
# requires-python = ">=3.12"
|
|
||||||
# dependencies = [
|
|
||||||
# "chevron-blue < 1",
|
|
||||||
# ]
|
|
||||||
# ///
|
|
||||||
"""
|
|
||||||
Generate static Rust code from Python version download metadata.
|
|
||||||
|
|
||||||
Generates the `downloads.inc` file from the `downloads.inc.mustache` template.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
uv run -- crates/uv-python/template-download-metadata.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import chevron_blue
|
|
||||||
|
|
||||||
CRATE_ROOT = Path(__file__).parent
|
|
||||||
WORKSPACE_ROOT = CRATE_ROOT.parent.parent
|
|
||||||
VERSION_METADATA = CRATE_ROOT / "download-metadata.json"
|
|
||||||
TEMPLATE = CRATE_ROOT / "src" / "downloads.inc.mustache"
|
|
||||||
TARGET = TEMPLATE.with_suffix("")
|
|
||||||
PRERELEASE_PATTERN = re.compile(r"(a|b|rc)(\d+)")
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_name(name: str) -> str:
|
|
||||||
match name:
|
|
||||||
case "cpython":
|
|
||||||
return "CPython"
|
|
||||||
case "pypy":
|
|
||||||
return "PyPy"
|
|
||||||
case _:
|
|
||||||
raise ValueError(f"Unknown implementation name: {name}")
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_libc(libc: str) -> str | None:
|
|
||||||
if libc == "none":
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return libc.title()
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_variant(variant: str | None) -> str | None:
|
|
||||||
match variant:
|
|
||||||
case None:
|
|
||||||
return "PythonVariant::Default"
|
|
||||||
case "freethreaded":
|
|
||||||
return "PythonVariant::Freethreaded"
|
|
||||||
case "debug":
|
|
||||||
return "PythonVariant::Debug"
|
|
||||||
case _:
|
|
||||||
raise ValueError(f"Unknown variant: {variant}")
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_arch(arch: dict) -> tuple[str, str]:
|
|
||||||
match arch["family"]:
|
|
||||||
# Special constructors
|
|
||||||
case "i686":
|
|
||||||
family = "X86_32(target_lexicon::X86_32Architecture::I686)"
|
|
||||||
case "aarch64":
|
|
||||||
family = "Aarch64(target_lexicon::Aarch64Architecture::Aarch64)"
|
|
||||||
case "armv5tel":
|
|
||||||
family = "Arm(target_lexicon::ArmArchitecture::Armv5te)"
|
|
||||||
case "armv7":
|
|
||||||
family = "Arm(target_lexicon::ArmArchitecture::Armv7)"
|
|
||||||
case "riscv64":
|
|
||||||
# The `gc` variant of riscv64 is the common base instruction set and
|
|
||||||
# is the target in `python-build-standalone`
|
|
||||||
# See https://github.com/astral-sh/python-build-standalone/issues/504
|
|
||||||
family = "Riscv64(target_lexicon::Riscv64Architecture::Riscv64gc)"
|
|
||||||
case value:
|
|
||||||
family = value.capitalize()
|
|
||||||
variant = (
|
|
||||||
f"Some(ArchVariant::{arch['variant'].capitalize()})"
|
|
||||||
if arch["variant"]
|
|
||||||
else "None"
|
|
||||||
)
|
|
||||||
return family, variant
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_os(os: str) -> str:
|
|
||||||
match os:
|
|
||||||
# Special constructors
|
|
||||||
case "darwin":
|
|
||||||
return "Darwin(None)"
|
|
||||||
|
|
||||||
return os.title()
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_prerelease(prerelease: str) -> str:
|
|
||||||
if not prerelease:
|
|
||||||
return "None"
|
|
||||||
if not (match := PRERELEASE_PATTERN.match(prerelease)):
|
|
||||||
raise ValueError(f"Invalid prerelease: {prerelease!r}")
|
|
||||||
kind, number = match.groups()
|
|
||||||
kind_mapping = {"a": "Alpha", "b": "Beta", "rc": "Rc"}
|
|
||||||
return f"Some(Prerelease {{ kind: PrereleaseKind::{kind_mapping[kind]}, number: {number} }})"
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_value(value: dict) -> dict:
|
|
||||||
value["os"] = prepare_os(value["os"])
|
|
||||||
value["arch_family"], value["arch_variant"] = prepare_arch(value["arch"])
|
|
||||||
value["name"] = prepare_name(value["name"])
|
|
||||||
value["libc"] = prepare_libc(value["libc"])
|
|
||||||
value["prerelease"] = prepare_prerelease(value["prerelease"])
|
|
||||||
value["variant"] = prepare_variant(value["variant"])
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
debug = logging.getLogger().getEffectiveLevel() <= logging.DEBUG
|
|
||||||
|
|
||||||
data: dict[str, Any] = {}
|
|
||||||
data["generated_with"] = Path(__file__).relative_to(WORKSPACE_ROOT).as_posix()
|
|
||||||
data["generated_from"] = TEMPLATE.relative_to(WORKSPACE_ROOT).as_posix()
|
|
||||||
data["versions"] = [
|
|
||||||
{"key": key, "value": prepare_value(value)}
|
|
||||||
for key, value in json.loads(VERSION_METADATA.read_text()).items()
|
|
||||||
# Exclude debug variants for now, we don't support them in the Rust side
|
|
||||||
if value["variant"] != "debug"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Render the template
|
|
||||||
logging.info(f"Rendering `{TEMPLATE.name}`...")
|
|
||||||
output = chevron_blue.render(
|
|
||||||
template=TEMPLATE.read_text(), data=data, no_escape=True, warn=debug
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update the file
|
|
||||||
logging.info(f"Updating `{TARGET}`...")
|
|
||||||
TARGET.write_text("// DO NOT EDIT\n//\n" + output)
|
|
||||||
subprocess.check_call(
|
|
||||||
["rustfmt", str(TARGET)],
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
stdout=sys.stderr if debug else subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
|
|
||||||
logging.info("Done!")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Generates Rust code for Python version metadata.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-v",
|
|
||||||
"--verbose",
|
|
||||||
action="store_true",
|
|
||||||
help="Enable debug logging",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-q",
|
|
||||||
"--quiet",
|
|
||||||
action="store_true",
|
|
||||||
help="Disable logging",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
if args.quiet:
|
|
||||||
log_level = logging.CRITICAL
|
|
||||||
elif args.verbose:
|
|
||||||
log_level = logging.DEBUG
|
|
||||||
else:
|
|
||||||
log_level = logging.INFO
|
|
||||||
|
|
||||||
logging.basicConfig(level=log_level, format="%(message)s")
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|
@ -257,6 +257,14 @@ impl EnvVars {
|
||||||
/// Specifies the directory for storing managed Python installations.
|
/// Specifies the directory for storing managed Python installations.
|
||||||
pub const UV_PYTHON_INSTALL_DIR: &'static str = "UV_PYTHON_INSTALL_DIR";
|
pub const UV_PYTHON_INSTALL_DIR: &'static str = "UV_PYTHON_INSTALL_DIR";
|
||||||
|
|
||||||
|
/// Managed Python installations information is hardcoded in the `uv` binary.
|
||||||
|
///
|
||||||
|
/// This variable can be set to a URL pointing to JSON to use as a list for Python installations.
|
||||||
|
/// This will allow for setting each property of the Python installation, mostly the url part for offline mirror.
|
||||||
|
///
|
||||||
|
/// Note that currently, only local paths are supported.
|
||||||
|
pub const UV_PYTHON_DOWNLOADS_JSON_URL: &'static str = "UV_PYTHON_DOWNLOADS_JSON_URL";
|
||||||
|
|
||||||
/// Managed Python installations are downloaded from the Astral
|
/// Managed Python installations are downloaded from the Astral
|
||||||
/// [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone) project.
|
/// [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone) project.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ pub(crate) async fn list(
|
||||||
let downloads = download_request
|
let downloads = download_request
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(PythonDownloadRequest::iter_downloads)
|
.map(PythonDownloadRequest::iter_downloads)
|
||||||
|
.transpose()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -666,8 +666,8 @@ fn python_install_freethreaded() {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Searching for Python installations
|
Searching for Python installations
|
||||||
Uninstalled 2 versions in [TIME]
|
Uninstalled 2 versions in [TIME]
|
||||||
- cpython-3.13.2-[PLATFORM]
|
|
||||||
- cpython-3.13.2+freethreaded-[PLATFORM] (python3.13t)
|
- cpython-3.13.2+freethreaded-[PLATFORM] (python3.13t)
|
||||||
|
- cpython-3.13.2-[PLATFORM]
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,15 @@ Equivalent to the
|
||||||
[`python-downloads`](../reference/settings.md#python-downloads) setting and, when disabled, the
|
[`python-downloads`](../reference/settings.md#python-downloads) setting and, when disabled, the
|
||||||
`--no-python-downloads` option. Whether uv should allow Python downloads.
|
`--no-python-downloads` option. Whether uv should allow Python downloads.
|
||||||
|
|
||||||
|
### `UV_PYTHON_DOWNLOADS_JSON_URL`
|
||||||
|
|
||||||
|
Managed Python installations information is hardcoded in the `uv` binary.
|
||||||
|
|
||||||
|
This variable can be set to a URL pointing to JSON to use as a list for Python installations.
|
||||||
|
This will allow for setting each property of the Python installation, mostly the url part for offline mirror.
|
||||||
|
|
||||||
|
Note that currently, only local paths are supported.
|
||||||
|
|
||||||
### `UV_PYTHON_INSTALL_DIR`
|
### `UV_PYTHON_INSTALL_DIR`
|
||||||
|
|
||||||
Specifies the directory for storing managed Python installations.
|
Specifies the directory for storing managed Python installations.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue