Introduce VersionNotFound, avoid accessing github in tests (#525)

* Introduce VersionNotFound, avoid accessing github in tests

* Accept more version formats
This commit is contained in:
Dániel Buga 2025-08-13 12:25:09 +02:00 committed by GitHub
parent a8fcaac931
commit 265b643fc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 123 additions and 59 deletions

View File

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Updated default GCC / Crosstools version to latest, [`esp-14.2.0_20241119`](https://github.com/espressif/crosstool-NG/releases/tag/esp-14.2.0_20241119) (#508)
- `espup install -v` now accepts version strings with 1-4 parts. (#525)
### Removed

View File

@ -38,6 +38,12 @@ pub enum Error {
)]
InvalidVersion(String),
#[diagnostic(code(espup::toolchain::rust::version_not_found))]
#[error(
"The toolchain version '{0}' was not found. Verify that the release exists in https://github.com/esp-rs/rust-build/releases"
)]
VersionNotFound(String),
#[error(transparent)]
IoError(#[from] std::io::Error),

View File

@ -229,7 +229,7 @@ pub async fn install(args: InstallOpts, install_mode: InstallMode) -> Result<()>
let host_triple = get_host_triple(args.default_host)?;
let xtensa_rust_version = if let Some(toolchain_version) = &args.toolchain_version {
if !args.skip_version_parse {
XtensaRust::parse_version(toolchain_version)?
XtensaRust::find_latest_version_on_github(toolchain_version)?
} else {
toolchain_version.clone()
}

View File

@ -40,9 +40,10 @@ const XTENSA_RUST_API_URL: &str =
"https://api.github.com/repos/esp-rs/rust-build/releases?page=1&per_page=100";
/// Xtensa Rust Toolchain version regex.
pub const RE_EXTENDED_SEMANTIC_VERSION: &str = r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)\.(?P<subpatch>0|[1-9]\d*)?$";
const RE_SEMANTIC_VERSION: &str =
r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)?$";
pub const RE_EXTENDED_SEMANTIC_VERSION: &str = r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)\.(?P<subpatch>0|[1-9]\d*)$";
/// Matches version strings with 1-4 parts.
pub const RE_ANY_SEMANTIC_VERSION: &str =
r"^(0|[1-9]\d*)(\.(0|[1-9]\d*)(\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?)?)?$";
#[derive(Debug, Clone, Default)]
pub struct XtensaRust {
@ -93,9 +94,10 @@ impl XtensaRust {
version.retain(|c| c != 'v' && c != '"');
// Validate the version format - handle both spawning and parsing errors
let parse_task = tokio::task::spawn_blocking(move || Self::parse_version(&version))
.await
.map_err(|_| Error::SerializeJson)?;
let parse_task =
tokio::task::spawn_blocking(move || Self::find_latest_version_on_github(&version))
.await
.map_err(|_| Error::SerializeJson)?;
let validated_version = parse_task?;
@ -136,48 +138,67 @@ impl XtensaRust {
}
}
/// Parses the version of the Xtensa toolchain.
pub fn parse_version(arg: &str) -> Result<String, Error> {
debug!("Parsing Xtensa Rust version: {arg}");
let re_extended = Regex::new(RE_EXTENDED_SEMANTIC_VERSION).unwrap();
let re_semver = Regex::new(RE_SEMANTIC_VERSION).unwrap();
/// Retrieves the latest version of the Xtensa toolchain.
///
/// Note that this function issues a GitHub API request to retrieve the latest version of the Xtensa toolchain.
pub fn find_latest_version_on_github(version: &str) -> Result<String, Error> {
debug!("Parsing Xtensa Rust version: {version}");
let json = github_query(XTENSA_RUST_API_URL)?;
if re_semver.is_match(arg) {
let mut extended_versions: Vec<String> = Vec::new();
for release in json.as_array().unwrap() {
let tag_name = release["tag_name"].to_string().replace(['\"', 'v'], "");
if tag_name.starts_with(arg) {
extended_versions.push(tag_name);
}
}
if extended_versions.is_empty() {
return Err(Error::InvalidVersion(arg.to_string()));
}
let mut max_version = extended_versions.pop().unwrap();
let mut max_subpatch = 0;
for version in extended_versions {
let subpatch: i8 = re_extended
.captures(&version)
.and_then(|cap| {
cap.name("subpatch")
.map(|subpatch| subpatch.as_str().parse().unwrap())
})
.unwrap();
if subpatch > max_subpatch {
max_subpatch = subpatch;
max_version = version;
}
}
return Ok(max_version);
} else if re_extended.is_match(arg) {
for release in json.as_array().unwrap() {
let tag_name = release["tag_name"].to_string().replace(['\"', 'v'], "");
if tag_name.starts_with(arg) {
return Ok(arg.to_string());
}
}
let mut candidates: Vec<String> = Vec::new();
for release in json.as_array().unwrap() {
candidates.push(release["tag_name"].to_string().replace(['\"', 'v'], ""));
}
Self::find_latest_version(version, &candidates)
}
/// Find the latest matching version of the Xtensa toolchain.
///
/// This function takes a version string and a list of candidate versions and returns the latest matching version.
/// If no matching version is found, it returns an error.
///
/// The list of candidate versions is expected to be given in the extended semantic version format.
fn find_latest_version(version: &str, candidates: &[String]) -> Result<String, Error> {
lazy_static::lazy_static! {
static ref RE_EXTENDED: Regex = Regex::new(RE_EXTENDED_SEMANTIC_VERSION).unwrap();
static ref RE_ANY_SEMVER: Regex = Regex::new(RE_ANY_SEMANTIC_VERSION).unwrap();
};
if !RE_ANY_SEMVER.is_match(version) {
return Err(Error::InvalidVersion(version.to_string()));
}
let extract_version_components = |version: &str| -> (u8, u8, u8, u8) {
RE_EXTENDED
.captures(version)
.and_then(|cap| {
let major = cap.name("major").unwrap().as_str().parse().ok()?;
let minor = cap.name("minor").unwrap().as_str().parse().ok()?;
let patch = cap.name("patch").unwrap().as_str().parse().ok()?;
let subpatch = cap.name("subpatch").unwrap().as_str().parse().ok()?;
Some((major, minor, patch, subpatch))
})
.unwrap_or_else(|| panic!("Version {version} is not in the extended semver format"))
};
// Make sure that if we are looking for 1.65.0.x, we don't consider 1.65.1.x or 1.66.0.x
let candidates = candidates.iter().filter(|v| v.starts_with(version));
// Now find the latest
let max_version = candidates
.map(move |candidate| {
let components = extract_version_components(candidate.as_str());
(candidate, components)
})
.max_by_key(|(_, components)| *components)
.map(|(version, _)| version.clone());
match max_version {
Some(version) => Ok(version),
None => Err(Error::VersionNotFound(version.to_string())),
}
Err(Error::InvalidVersion(arg.to_string()))
}
/// Removes the Xtensa Rust toolchain.
@ -474,18 +495,54 @@ mod tests {
#[test]
fn test_xtensa_rust_parse_version() {
initialize_logger("debug");
assert_eq!(XtensaRust::parse_version("1.65.0.0").unwrap(), "1.65.0.0");
assert_eq!(XtensaRust::parse_version("1.65.0.1").unwrap(), "1.65.0.1");
assert_eq!(XtensaRust::parse_version("1.64.0.0").unwrap(), "1.64.0.0");
assert_eq!(XtensaRust::parse_version("1.82.0").unwrap(), "1.82.0.3");
assert_eq!(XtensaRust::parse_version("1.65.0").unwrap(), "1.65.0.1");
assert_eq!(XtensaRust::parse_version("1.64.0").unwrap(), "1.64.0.0");
assert!(XtensaRust::parse_version("422.0.0").is_err());
assert!(XtensaRust::parse_version("422.0.0.0").is_err());
assert!(XtensaRust::parse_version("a.1.1.1").is_err());
assert!(XtensaRust::parse_version("1.1.1.1.1").is_err());
assert!(XtensaRust::parse_version("1..1.1").is_err());
assert!(XtensaRust::parse_version("1._.*.1").is_err());
let candidates = [
String::from("1.64.0.0"),
String::from("1.65.0.0"),
String::from("1.65.0.1"),
String::from("1.65.1.0"),
String::from("1.82.0.3"),
];
assert_eq!(
XtensaRust::find_latest_version("1.65.0.0", &candidates).unwrap(),
"1.65.0.0"
);
assert_eq!(
XtensaRust::find_latest_version("1.65", &candidates).unwrap(),
"1.65.1.0"
);
assert_eq!(
XtensaRust::find_latest_version("1.65.0.1", &candidates).unwrap(),
"1.65.0.1"
);
assert_eq!(
XtensaRust::find_latest_version("1.64.0.0", &candidates).unwrap(),
"1.64.0.0"
);
assert_eq!(
XtensaRust::find_latest_version("1.82.0", &candidates).unwrap(),
"1.82.0.3"
);
assert_eq!(
XtensaRust::find_latest_version("1.65.0", &candidates).unwrap(),
"1.65.0.1"
);
assert_eq!(
XtensaRust::find_latest_version("1.64.0", &candidates).unwrap(),
"1.64.0.0"
);
assert_eq!(
XtensaRust::find_latest_version("1", &candidates).unwrap(),
"1.82.0.3"
);
assert!(XtensaRust::find_latest_version("1.", &candidates).is_err());
assert!(XtensaRust::find_latest_version("1.0.", &candidates).is_err());
assert!(XtensaRust::find_latest_version("1.0.0.", &candidates).is_err());
assert!(XtensaRust::find_latest_version("422.0.0", &candidates).is_err());
assert!(XtensaRust::find_latest_version("422.0.0.0", &candidates).is_err());
assert!(XtensaRust::find_latest_version("a.1.1.1", &candidates).is_err());
assert!(XtensaRust::find_latest_version("1.1.1.1.1", &candidates).is_err());
assert!(XtensaRust::find_latest_version("1..1.1", &candidates).is_err());
assert!(XtensaRust::find_latest_version("1._.*.1", &candidates).is_err());
}
#[test]