Add a version backwards incompatibility scheme for the JSON Python download list

If the JSON does not deserialize, check to see if any key is named
"version" and print a different error message hinting at upgrading uv.

The intention is that we will add this field on backwards-incompatible
changes (and set it to an integer or string or something else that does
not deserialize into a JsonPythonDownload).
This commit is contained in:
Geoffrey Thomas 2025-07-18 00:45:48 -04:00
parent ecebc914a6
commit 25dd584a27
1 changed files with 24 additions and 8 deletions

View File

@ -98,8 +98,10 @@ pub enum Error {
Mirror(&'static str, &'static str), Mirror(&'static str, &'static str),
#[error("Failed to determine the libc used on the current platform")] #[error("Failed to determine the libc used on the current platform")]
LibcDetection(#[from] LibcDetectionError), LibcDetection(#[from] LibcDetectionError),
#[error("The JSON of the python downloads is invalid: {0}")] #[error("Unable to parse the JSON Python download list at {0}")]
InvalidPythonDownloadsJSON(String, #[source] serde_json::Error), InvalidPythonDownloadsJSON(String, #[source] serde_json::Error),
#[error("This version of uv is too old to support the JSON Python download list at {0}")]
UnsupportedPythonDownloadsJSON(String),
#[error("An offline Python installation was requested, but {file} (from {url}) is missing in {}", python_builds_dir.user_display())] #[error("An offline Python installation was requested, but {file} (from {url}) is missing in {}", python_builds_dir.user_display())]
OfflinePythonMissing { OfflinePythonMissing {
file: Box<PythonInstallationKey>, file: Box<PythonInstallationKey>,
@ -701,16 +703,30 @@ impl ManagedPythonDownloadList {
} }
}; };
let json_downloads: HashMap<String, JsonPythonDownload> = serde_json::from_slice(&buf) let json_downloads: HashMap<String, JsonPythonDownload> = serde_json::from_slice(&buf)
.map_err(|e| { .map_err(
Error::InvalidPythonDownloadsJSON( // As an explicit compatibility mechanism, if there's a top-level "version" key, it
match json_source { // means it's a newer format than we know how to deal with. Before reporting a
// parse error about the format of JsonPythonDownload, check for that key. We can do
// this by parsing into a Map<String, IgnoredAny> which allows any valid JSON on the
// value side. (Because it's zero-sized, Clippy suggests Set<String>, but that won't
// have the same parsing effect.)
#[allow(clippy::zero_sized_map_values)]
|e| {
let source = match json_source {
Source::BuiltIn => "EMBEDDED IN THE BINARY".to_owned(), Source::BuiltIn => "EMBEDDED IN THE BINARY".to_owned(),
Source::Path(path) => path.to_string_lossy().to_string(), Source::Path(path) => path.to_string_lossy().to_string(),
Source::Http(url) => url.to_string(), Source::Http(url) => url.to_string(),
}, };
e, if let Ok(keys) =
) serde_json::from_slice::<HashMap<String, serde::de::IgnoredAny>>(&buf)
})?; && keys.contains_key("version")
{
Error::UnsupportedPythonDownloadsJSON(source)
} else {
Error::InvalidPythonDownloadsJSON(source, e)
}
},
)?;
let result = parse_json_downloads(json_downloads); let result = parse_json_downloads(json_downloads);
Ok(Self { downloads: result }) Ok(Self { downloads: result })