From 265b643fc731788b204b081b9e2fe02ee2a61e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Buga?= Date: Wed, 13 Aug 2025 12:25:09 +0200 Subject: [PATCH] Introduce VersionNotFound, avoid accessing github in tests (#525) * Introduce VersionNotFound, avoid accessing github in tests * Accept more version formats --- CHANGELOG.md | 1 + src/error.rs | 6 ++ src/toolchain/mod.rs | 2 +- src/toolchain/rust.rs | 173 ++++++++++++++++++++++++++++-------------- 4 files changed, 123 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a787a91..d2ff2db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/error.rs b/src/error.rs index 02fbead..0ea8f12 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), diff --git a/src/toolchain/mod.rs b/src/toolchain/mod.rs index 735bc48..b268916 100644 --- a/src/toolchain/mod.rs +++ b/src/toolchain/mod.rs @@ -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() } diff --git a/src/toolchain/rust.rs b/src/toolchain/rust.rs index 197eee9..ed705df 100644 --- a/src/toolchain/rust.rs +++ b/src/toolchain/rust.rs @@ -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"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)?$"; -const RE_SEMANTIC_VERSION: &str = - r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)?$"; +pub const RE_EXTENDED_SEMANTIC_VERSION: &str = r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[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 { - 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 { + 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 = 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 = 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 { + 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]