|
1 | | -use crate::ScarbCommand; |
2 | | -use anyhow::{Context, Result}; |
| 1 | +//! Functionality for fetching and parsing `scarb` version information. |
| 2 | +
|
| 3 | +use crate::{ScarbCommand, ScarbUnavailableError, ensure_scarb_available}; |
3 | 4 | use regex::Regex; |
4 | 5 | use semver::Version; |
5 | | -use shared::command::CommandExt; |
6 | | -use std::path::PathBuf; |
7 | | -use std::str::from_utf8; |
| 6 | +use shared::command::{CommandError, CommandExt}; |
| 7 | +use std::collections::HashMap; |
| 8 | +use std::path::Path; |
| 9 | +use std::process::Output; |
| 10 | +use std::sync::LazyLock; |
| 11 | +use thiserror::Error; |
8 | 12 |
|
| 13 | +#[derive(Debug)] |
9 | 14 | pub struct ScarbVersionOutput { |
10 | 15 | pub scarb: Version, |
11 | 16 | pub cairo: Version, |
| 17 | + pub sierra: Version, |
12 | 18 | } |
13 | 19 |
|
14 | | -#[derive(Clone, Debug, Default)] |
15 | | -pub struct VersionCommand { |
16 | | - current_dir: Option<PathBuf>, |
| 20 | +/// Errors that can occur when fetching `scarb` version information. |
| 21 | +#[derive(Error, Debug)] |
| 22 | +pub enum VersionError { |
| 23 | + #[error(transparent)] |
| 24 | + ScarbNotFound(#[from] ScarbUnavailableError), |
| 25 | + |
| 26 | + #[error("Failed to execute `scarb --version`: {0}")] |
| 27 | + CommandExecutionError(#[from] CommandError), |
| 28 | + |
| 29 | + #[error("Could not parse version for tool `{0}`: {1}")] |
| 30 | + VersionParseError(String, #[source] semver::Error), |
| 31 | + |
| 32 | + #[error("Missing version entry for `{0}`")] |
| 33 | + MissingToolVersion(String), |
17 | 34 | } |
18 | 35 |
|
19 | | -impl VersionCommand { |
20 | | - #[must_use] |
21 | | - pub fn new() -> Self { |
22 | | - Self { current_dir: None } |
23 | | - } |
| 36 | +/// Fetches `scarb` version information for the current directory. |
| 37 | +pub fn scarb_version() -> Result<ScarbVersionOutput, VersionError> { |
| 38 | + scarb_version_internal(None) |
| 39 | +} |
| 40 | + |
| 41 | +/// Fetches `scarb` version information for a specific directory. |
| 42 | +pub fn scarb_version_for_dir(dir: impl AsRef<Path>) -> Result<ScarbVersionOutput, VersionError> { |
| 43 | + scarb_version_internal(Some(dir.as_ref())) |
| 44 | +} |
24 | 45 |
|
25 | | - /// Current directory of the `scarb --command` process. |
26 | | - #[must_use] |
27 | | - pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self { |
28 | | - self.current_dir = Some(path.into()); |
29 | | - self |
| 46 | +/// Internal function to fetch `scarb` version information. |
| 47 | +fn scarb_version_internal(dir: Option<&Path>) -> Result<ScarbVersionOutput, VersionError> { |
| 48 | + let output = run_command(dir)?; |
| 49 | + parse_output(&output) |
| 50 | +} |
| 51 | + |
| 52 | +/// Runs the `scarb --version` command in the specified directory. |
| 53 | +fn run_command(dir: Option<&Path>) -> Result<Output, VersionError> { |
| 54 | + ensure_scarb_available()?; |
| 55 | + let mut scarb_version_command = ScarbCommand::new().arg("--version").command(); |
| 56 | + |
| 57 | + if let Some(path) = dir { |
| 58 | + scarb_version_command.current_dir(path); |
30 | 59 | } |
| 60 | + |
| 61 | + Ok(scarb_version_command.output_checked()?) |
31 | 62 | } |
32 | 63 |
|
33 | | -impl VersionCommand { |
34 | | - pub fn run(self) -> Result<ScarbVersionOutput> { |
35 | | - let mut scarb_version_command = ScarbCommand::new().arg("--version").command(); |
| 64 | +/// Parses the output of the `scarb --version` command. |
| 65 | +fn parse_output(output: &Output) -> Result<ScarbVersionOutput, VersionError> { |
| 66 | + let output_str = str::from_utf8(&output.stdout).expect("valid UTF-8 from scarb"); |
36 | 67 |
|
37 | | - if let Some(path) = &self.current_dir { |
38 | | - scarb_version_command.current_dir(path); |
39 | | - } |
| 68 | + let mut versions = extract_versions(output_str)?; |
40 | 69 |
|
41 | | - let scarb_version = scarb_version_command |
42 | | - .output_checked() |
43 | | - .context("Failed to execute `scarb --version`")?; |
| 70 | + let mut get_version = |tool: &str| { |
| 71 | + versions |
| 72 | + .remove(tool) |
| 73 | + .ok_or_else(|| VersionError::MissingToolVersion(tool.into())) |
| 74 | + }; |
44 | 75 |
|
45 | | - let version_output = from_utf8(&scarb_version.stdout) |
46 | | - .context("Failed to parse `scarb --version` output to UTF-8")?; |
| 76 | + Ok(ScarbVersionOutput { |
| 77 | + scarb: get_version("scarb")?, |
| 78 | + cairo: get_version("cairo")?, |
| 79 | + sierra: get_version("sierra")?, |
| 80 | + }) |
| 81 | +} |
47 | 82 |
|
48 | | - Ok(ScarbVersionOutput { |
49 | | - scarb: Self::extract_version(version_output, "scarb")?, |
50 | | - cairo: Self::extract_version(version_output, "cairo:")?, |
51 | | - }) |
52 | | - } |
| 83 | +/// Extracts tool versions from the version output string. |
| 84 | +fn extract_versions(version_output: &str) -> Result<HashMap<String, Version>, VersionError> { |
| 85 | + // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string |
| 86 | + static VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| { |
| 87 | + Regex::new(r"(?P<tool>\w+)[\s:]+(?P<ver>(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?)").expect("this should be a valid regex") |
| 88 | + }); |
53 | 89 |
|
54 | | - fn extract_version(version_output: &str, tool: &str) -> Result<Version> { |
55 | | - // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string |
56 | | - let version_regex = Regex::new( |
57 | | - &format!(r"{tool}?\s*((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)")) |
58 | | - .context("Could not create version matching regex")?; |
| 90 | + VERSION_REGEX |
| 91 | + .captures_iter(version_output) |
| 92 | + .map(|cap| { |
| 93 | + let tool = cap |
| 94 | + .name("tool") |
| 95 | + .expect("regex ensures tool name exists") |
| 96 | + .as_str() |
| 97 | + .to_string(); |
59 | 98 |
|
60 | | - let version_capture = version_regex |
61 | | - .captures(version_output) |
62 | | - .context(format!("Could not find {tool} version"))?; |
| 99 | + let ver_str = cap |
| 100 | + .name("ver") |
| 101 | + .expect("regex ensures version string exists") |
| 102 | + .as_str(); |
63 | 103 |
|
64 | | - let version = version_capture |
65 | | - .get(1) |
66 | | - .context(format!("Could not find {tool} version"))? |
67 | | - .as_str(); |
| 104 | + Version::parse(ver_str) |
| 105 | + .map_err(|e| VersionError::VersionParseError(tool.clone(), e)) |
| 106 | + .map(|version| (tool, version)) |
| 107 | + }) |
| 108 | + .collect() |
| 109 | +} |
| 110 | +#[cfg(test)] |
| 111 | +mod tests { |
| 112 | + use super::*; |
68 | 113 |
|
69 | | - Version::parse(version).context(format!("Failed to parse {tool} version")) |
| 114 | + #[test] |
| 115 | + fn test_happy_case() { |
| 116 | + scarb_version().unwrap(); |
70 | 117 | } |
71 | 118 | } |
0 commit comments