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 ### 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) - 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 ### Removed

View File

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