Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4728,6 +4728,12 @@ pub struct PythonListArgs {
/// Select the output format.
#[arg(long, value_enum, default_value_t = PythonListFormat::default())]
pub output_format: PythonListFormat,

/// URL pointing to JSON of custom Python installations.
///
/// Note that currently, only local paths are supported.
#[arg(long, env = EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL)]
pub python_downloads_json_url: Option<String>,
}

#[derive(Args)]
Expand Down Expand Up @@ -4791,6 +4797,12 @@ pub struct PythonInstallArgs {
#[arg(long, env = EnvVars::UV_PYPY_INSTALL_MIRROR)]
pub pypy_mirror: Option<String>,

/// URL pointing to JSON of custom Python installations.
///
/// Note that currently, only local paths are supported.
#[arg(long, env = EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL)]
pub python_downloads_json_url: Option<String>,

/// Reinstall the requested Python version, if it's already installed.
///
/// By default, uv will exit successfully if the version is already
Expand Down
52 changes: 29 additions & 23 deletions crates/uv-python/src/downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,9 @@ impl PythonDownloadRequest {
/// Iterate over all [`PythonDownload`]'s that match this request.
pub fn iter_downloads(
&self,
python_downloads_json_url: Option<&str>,
) -> Result<impl Iterator<Item = &'static ManagedPythonDownload> + use<'_>, Error> {
Ok(ManagedPythonDownload::iter_all()?
Ok(ManagedPythonDownload::iter_all(python_downloads_json_url)?
.filter(move |download| self.satisfied_by_download(download)))
}

Expand Down Expand Up @@ -496,16 +497,17 @@ impl ManagedPythonDownload {
/// be searched for — even if a pre-release was not explicitly requested.
pub fn from_request(
request: &PythonDownloadRequest,
python_downloads_json_url: Option<&str>,
) -> Result<&'static ManagedPythonDownload, Error> {
if let Some(download) = request.iter_downloads()?.next() {
if let Some(download) = request.iter_downloads(python_downloads_json_url)?.next() {
return Ok(download);
}

if !request.allows_prereleases() {
if let Some(download) = request
.clone()
.with_prereleases(true)
.iter_downloads()?
.iter_downloads(python_downloads_json_url)?
.next()
{
return Ok(download);
Expand All @@ -514,32 +516,36 @@ impl ManagedPythonDownload {

Err(Error::NoDownloadFound(request.clone()))
}

//noinspection RsUnresolvedPath - RustRover can't see through the `include!`
/// Iterate over all [`ManagedPythonDownload`]s.
pub fn iter_all() -> Result<impl Iterator<Item = &'static ManagedPythonDownload>, Error> {
let runtime_source = std::env::var(EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL);

/// Iterate over all [`ManagedPythonDownload`]s.
///
/// Note: The list is generated on the first call to this function.
/// so `python_downloads_json_url` is only used in the first call to this function.
pub fn iter_all(
python_downloads_json_url: Option<&str>,
) -> Result<impl Iterator<Item = &'static ManagedPythonDownload>, Error> {
let downloads = PYTHON_DOWNLOADS.get_or_try_init(|| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm it's a bit dubious that we globally cache this result, effectively negating the extra parameter every time but the first time. but. Probably fine?

Copy link
Contributor Author

@MeitarR MeitarR Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, added comment about it 91c736b

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)) }?,
};
let json_downloads: HashMap<String, JsonPythonDownload> = if let Some(json_source) =
python_downloads_json_url
{
if Url::parse(json_source).is_ok() {
return Err(Error::RemoteJSONNotSupported());
}

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 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.to_string(), 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))
})?;
Expand Down
5 changes: 4 additions & 1 deletion crates/uv-python/src/installation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ impl PythonInstallation {
reporter: Option<&dyn Reporter>,
python_install_mirror: Option<&str>,
pypy_install_mirror: Option<&str>,
python_downloads_json_url: Option<&str>,
) -> Result<Self, Error> {
let request = request.unwrap_or(&PythonRequest::Default);

Expand Down Expand Up @@ -127,6 +128,7 @@ impl PythonInstallation {
reporter,
python_install_mirror,
pypy_install_mirror,
python_downloads_json_url,
)
.await
{
Expand All @@ -146,13 +148,14 @@ impl PythonInstallation {
reporter: Option<&dyn Reporter>,
python_install_mirror: Option<&str>,
pypy_install_mirror: Option<&str>,
python_downloads_json_url: Option<&str>,
) -> Result<Self, Error> {
let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
let installations_dir = installations.root();
let scratch_dir = installations.scratch();
let _lock = installations.lock().await?;

let download = ManagedPythonDownload::from_request(&request)?;
let download = ManagedPythonDownload::from_request(&request, python_downloads_json_url)?;
let client = client_builder.build();

info!("Fetching requested Python...");
Expand Down
26 changes: 24 additions & 2 deletions crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -819,21 +819,40 @@ pub struct PythonInstallMirrors {
"#
)]
pub pypy_install_mirror: Option<String>,

/// URL pointing to JSON of custom Python installations.
///
/// Note that currently, only local paths are supported.
#[option(
default = "None",
value_type = "str",
example = r#"
python-downloads-json-url = "/etc/uv/python-downloads.json"
"#
)]
pub python_downloads_json_url: Option<String>,
}

impl Default for PythonInstallMirrors {
fn default() -> Self {
PythonInstallMirrors::resolve(None, None)
PythonInstallMirrors::resolve(None, None, None)
}
}

impl PythonInstallMirrors {
pub fn resolve(python_mirror: Option<String>, pypy_mirror: Option<String>) -> Self {
pub fn resolve(
python_mirror: Option<String>,
pypy_mirror: Option<String>,
python_downloads_json_url: Option<String>,
) -> Self {
let python_mirror_env = std::env::var(EnvVars::UV_PYTHON_INSTALL_MIRROR).ok();
let pypy_mirror_env = std::env::var(EnvVars::UV_PYPY_INSTALL_MIRROR).ok();
let python_downloads_json_url_env =
std::env::var(EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL).ok();
PythonInstallMirrors {
python_install_mirror: python_mirror_env.or(python_mirror),
pypy_install_mirror: pypy_mirror_env.or(pypy_mirror),
python_downloads_json_url: python_downloads_json_url_env.or(python_downloads_json_url),
}
}
}
Expand Down Expand Up @@ -1814,6 +1833,7 @@ pub struct OptionsWire {
// install_mirror: PythonInstallMirrors,
python_install_mirror: Option<String>,
pypy_install_mirror: Option<String>,
python_downloads_json_url: Option<String>,

// #[serde(flatten)]
// publish: PublishOptions
Expand Down Expand Up @@ -1861,6 +1881,7 @@ impl From<OptionsWire> for Options {
python_downloads,
python_install_mirror,
pypy_install_mirror,
python_downloads_json_url,
concurrent_downloads,
concurrent_builds,
concurrent_installs,
Expand Down Expand Up @@ -1967,6 +1988,7 @@ impl From<OptionsWire> for Options {
install_mirrors: PythonInstallMirrors::resolve(
python_install_mirror,
pypy_install_mirror,
python_downloads_json_url,
),
conflicts,
publish: PublishOptions {
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/build_frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ async fn build_package(
Some(&PythonDownloadReporter::single(printer)),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?
.into_interpreter();
Expand Down
4 changes: 4 additions & 0 deletions crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ async fn init_project(
Some(&reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?
.into_interpreter();
Expand All @@ -451,6 +452,7 @@ async fn init_project(
Some(&reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?
.into_interpreter();
Expand Down Expand Up @@ -516,6 +518,7 @@ async fn init_project(
Some(&reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?
.into_interpreter();
Expand All @@ -542,6 +545,7 @@ async fn init_project(
Some(&reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?
.into_interpreter();
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ impl ScriptInterpreter {
Some(&reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?
.into_interpreter();
Expand Down Expand Up @@ -903,6 +904,7 @@ impl ProjectInterpreter {
Some(&reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?;

Expand Down Expand Up @@ -2280,6 +2282,7 @@ pub(crate) async fn init_script_python_requirement(
Some(reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?
.into_interpreter();
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
Some(&download_reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?
.into_interpreter();
Expand Down Expand Up @@ -841,6 +842,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
Some(&download_reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?;

Expand Down
36 changes: 21 additions & 15 deletions crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ struct InstallRequest {
}

impl InstallRequest {
fn new(request: PythonRequest) -> Result<Self> {
fn new(request: PythonRequest, python_downloads_json_url: Option<&str>) -> Result<Self> {
// Make sure the request is a valid download request and fill platform information
let download_request = PythonDownloadRequest::from_request(&request)
.ok_or_else(|| {
Expand All @@ -55,18 +55,20 @@ impl InstallRequest {
.fill()?;

// Find a matching download
let download = match ManagedPythonDownload::from_request(&download_request) {
Ok(download) => download,
Err(downloads::Error::NoDownloadFound(request))
if request.libc().is_some_and(Libc::is_musl)
&& request.arch().is_some_and(Arch::is_arm) =>
let download =
match ManagedPythonDownload::from_request(&download_request, python_downloads_json_url)
{
return Err(anyhow::anyhow!(
"uv does not yet provide musl Python distributions on aarch64."
));
}
Err(err) => return Err(err.into()),
};
Ok(download) => download,
Err(downloads::Error::NoDownloadFound(request))
if request.libc().is_some_and(Libc::is_musl)
&& request.arch().is_some_and(Arch::is_arm) =>
{
return Err(anyhow::anyhow!(
"uv does not yet provide musl Python distributions on aarch64."
));
}
Err(err) => return Err(err.into()),
};

Ok(Self {
request,
Expand Down Expand Up @@ -131,6 +133,7 @@ pub(crate) async fn install(
force: bool,
python_install_mirror: Option<String>,
pypy_install_mirror: Option<String>,
python_downloads_json_url: Option<String>,
network_settings: NetworkSettings,
default: bool,
python_downloads: PythonDownloads,
Expand Down Expand Up @@ -171,13 +174,13 @@ pub(crate) async fn install(
}]
})
.into_iter()
.map(InstallRequest::new)
.map(|a| InstallRequest::new(a, python_downloads_json_url.as_deref()))
.collect::<Result<Vec<_>>>()?
} else {
targets
.iter()
.map(|target| PythonRequest::parse(target.as_str()))
.map(InstallRequest::new)
.map(|a| InstallRequest::new(a, python_downloads_json_url.as_deref()))
.collect::<Result<Vec<_>>>()?
};

Expand Down Expand Up @@ -219,7 +222,10 @@ pub(crate) async fn install(
changelog.existing.insert(installation.key().clone());
if matches!(&request.request, &PythonRequest::Any) {
// Construct an install request matching the existing installation
match InstallRequest::new(PythonRequest::Key(installation.into())) {
match InstallRequest::new(
PythonRequest::Key(installation.into()),
python_downloads_json_url.as_deref(),
) {
Ok(request) => {
debug!("Will reinstall `{}`", installation.key().green());
unsatisfied.push(Cow::Owned(request));
Expand Down
3 changes: 2 additions & 1 deletion crates/uv/src/commands/python/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub(crate) async fn list(
all_arches: bool,
show_urls: bool,
output_format: PythonListFormat,
python_downloads_json_url: Option<String>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
cache: &Cache,
Expand Down Expand Up @@ -101,7 +102,7 @@ pub(crate) async fn list(

let downloads = download_request
.as_ref()
.map(PythonDownloadRequest::iter_downloads)
.map(|a| PythonDownloadRequest::iter_downloads(a, python_downloads_json_url.as_deref()))
.transpose()?
.into_iter()
.flatten();
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/tool/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ pub(crate) async fn refine_interpreter(
Some(reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
)
.await?
.into_interpreter();
Expand Down
Loading
Loading