diff --git a/Cargo.lock b/Cargo.lock index b4d87bb..1e3ed22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,6 +257,7 @@ dependencies = [ "globwalk", "home", "log", + "regex", "remove_dir_all", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index dfc1428..595b83c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,10 @@ members = ["cargo-pio", "ldproxy"] [package] name = "embuild" version = "0.31.4" -authors = ["Ivan Markov ", "Dominik Gschwind "] +authors = [ + "Ivan Markov ", + "Dominik Gschwind ", +] edition = "2021" rust-version = "1.58" categories = ["embedded", "development-tools::build-utils"] @@ -16,14 +19,22 @@ readme = "README.md" [package.metadata.docs.rs] all-features = true -rustc-args = ["--cfg", "docsrs"] +rustc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"] [features] default = [] # Platformio support -pio = ["ureq", "bindgen", "tempfile", "which", "manifest", "serde", "serde_json"] +pio = [ + "ureq", + "bindgen", + "tempfile", + "which", + "manifest", + "serde", + "serde_json", +] # cmake file-api & utilities cmake = ["dep-cmake", "tempfile", "bindgen", "serde", "serde_json", "strum"] # glob utilities @@ -31,7 +42,16 @@ glob = ["globwalk"] # Cargo.toml and config.toml utilities manifest = ["cargo_toml", "toml"] # esp-idf installer -espidf = ["tempfile", "which", "git", "serde", "serde_json", "strum", "home"] +espidf = [ + "tempfile", + "which", + "git", + "serde", + "serde_json", + "strum", + "home", + "regex", +] # git utilities git = ["remove_dir_all"] # kconfig utilities @@ -61,3 +81,6 @@ tempfile = { version = "3", optional = true } ureq = { version = "2", optional = true } bindgen = { version = "0.69.4", optional = true } dep-cmake = { package = "cmake", version = "0.1", optional = true } +regex = { version = "1.5", optional = true, default-features = false, features = [ + "std", +] } diff --git a/src/espidf.rs b/src/espidf.rs index f6c5f70..3ca64de 100644 --- a/src/espidf.rs +++ b/src/espidf.rs @@ -9,21 +9,26 @@ //! //! - **`~/.espressif`**, if `install_dir` is None -use std::collections::HashSet; use std::ffi::{OsStr, OsString}; -use std::io::Write; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; +use std::process::Command; use std::sync::Arc; use std::{env, fs}; use anyhow::{anyhow, Context, Error, Result}; +use serde::{Deserialize, Serialize}; use crate::python::PYTHON; use crate::{cmd, git, path_buf, python}; +use self::tools_schema::{PlatformDownloadInfo, ToolInfo, VersionInfo}; + #[cfg(feature = "elf")] pub mod ulp_fsm; +mod tools_schema; + pub const DEFAULT_ESP_IDF_REPOSITORY: &str = "https://github.com/espressif/esp-idf.git"; pub const MANAGED_ESP_IDF_REPOS_DIR_BASE: &str = "esp-idf"; @@ -102,6 +107,180 @@ impl Tools { } } +/// A tool instance describing its properties. +#[derive(Debug, Default)] +struct Tool { + name: String, + /// url to obtain the Tool as an compressed binary + url: String, + /// version of the tool in no particular format + version: String, + /// hash of the compressed file + sha256: String, + /// size of the compressed file + size: i64, + /// Base absolute install dir as absolute Path + install_dir: PathBuf, + /// Path relative to install dir + export_path: PathBuf, + /// Command path and args that printout the current version of the tool + /// - First element is the relative path to the command + /// - Every other element represents a arg given to the cmd + version_cmd_args: Vec, + /// regex to extract the version returned by the version_cmd + version_regex: String, +} + +impl Tool { + /// Test if the tool is installed correctly + fn test(&self) -> bool { + let tool_path = self.abs_export_path(); + + // if path does not exist -> tool is not installed + if !tool_path.exists() { + return false; + } + log::debug!( + "Run cmd: {:?} to get current tool version", + self.test_command(), + ); + + let output = self + .test_command() + .output() + .unwrap_or_else(|_| panic!("Failed to run command: {:?}", self.test_command())); + + let regex = regex::Regex::new(&self.version_regex).expect("Invalid regex pattern provided"); + + if let Some(capture) = regex.captures(&String::from_utf8_lossy(&output.stdout)) { + if let Some(var) = capture.get(0) { + log::debug!("Match: {:?}, Version: {:?}", &var.as_str(), &self.version); + return true; + } + } + + false + } + + /// get the absolute PATH + fn abs_export_path(&self) -> PathBuf { + self.install_dir.join(self.export_path.as_path()) + } + + /// Creates a Command that will echo back the current version of the tool + /// + /// Since Command is non clonable this helper is provided + fn test_command(&self) -> Command { + let cmd_abs_path = self + .abs_export_path() + .join(self.version_cmd_args[0].clone()); + + let mut version_cmd = std::process::Command::new(cmd_abs_path); + version_cmd.args(self.version_cmd_args[1..].iter().cloned()); + version_cmd + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct ToolsInfo { + tools: Vec, + version: u32, +} + +fn parse_tools( + tools_wanted: Vec<&str>, + tools_json_file: PathBuf, + install_dir: PathBuf, +) -> anyhow::Result> { + let mut tools_string = String::new(); + let mut tools_file = std::fs::File::open(tools_json_file)?; + + tools_file.read_to_string(&mut tools_string)?; + + let tools_info = serde_json::from_str::(&tools_string)?; + + let tools = tools_info.tools; + + let tools = tools.iter().filter(|tool_info|{ + // tools_json schema contract marks name not as required ;( + tools_wanted.contains(&tool_info.name.as_ref().unwrap().as_str()) + }).map(|tool_info| { + let mut tool = Tool { + name: tool_info.name.as_ref().unwrap().clone(), + install_dir: install_dir.clone(), + version_cmd_args: tool_info.version_cmd.to_vec(), + version_regex: tool_info.version_regex.to_string(), + ..Default::default() + }; + + tool_info.versions.iter().filter(|version| { + version.status == Some(tools_schema::VersionInfoStatus::Recommended) + }).for_each(|version| { + + let os_matcher = |info: &VersionInfo| -> Option { + let os = std::env::consts::OS; + let arch = std::env::consts::ARCH; + + match os { + "linux" => match arch { + "x86_64" => info.linux_amd64.clone(), + // raspberryPI 2-4 ? + "armv7" => info.linux_armel.clone(), + // rPI 5 ? + "armv8" => info.linux_arm64.clone(), + _ => None, + }, + "windows" => match arch { + "x86" => info.win32.clone(), + "x86_64" => info.win64.clone(), + _ => None, + }, + "macos" => match arch { + "aarch64" => info.macos.clone(), + "x86_64" => info.macos_arm64.clone(), + _ => None, + }, + _ => None, + } + + }; + + // either a any key is provided or only platform specific keys + let info = if let Some(plaform_dll_info) = version.any.clone() { + plaform_dll_info + } else if let Some(plaform_dll_info) = os_matcher(version) { + plaform_dll_info + } else { + panic!("Neither any or platform specifc match found. Please create an issue on https://github.com/esp-rs/embuild and report your operating system"); + }; + + tool.url = info.url; + tool.sha256 = info.sha256; + tool.size = info.size; + tool.version.clone_from(version.name.as_ref().unwrap()); + + tool.export_path = PathBuf::new().join("tools").join(&tool.name).join(&tool.version); + + // export_path has two layers if indirection... + // it seams only the first array is ever used + let first_path = tool_info.export_paths.first(); + + if let Some(path) = first_path { + for element in path.iter() { + if !element.is_empty() { + tool.export_path = tool.export_path.join(element); + } + } + } + }); + log::debug!("{tool:?}"); + tool + } + ).collect(); + + Ok(tools) +} + /// The error returned by [`EspIdf::try_from_env`]. #[derive(Debug, thiserror::Error)] pub enum FromEnvError { @@ -379,93 +558,117 @@ impl Installer { ), EspIdfOrigin::Custom(repository) => (repository, false), }; - let version = EspIdfVersion::try_from(&repository); - let path_var_sep = if cfg!(windows) { ';' } else { ':' }; + // Reading the version out of a cmake build file + let esp_version = EspIdfVersion::try_from(&repository)?; // Create python virtualenv or use a previously installed one. - // TODO: also install python - python::check_python_at_least(3, 6)?; + // The systems minimal python version for bootstrepping the virtuelenv + // - By "system python" we refer to the current python executable that is provided to this processs that is + // first found in the env PATH + // - This will also be the python version used inside the virtualenv + let python_version = python::check_python_at_least(3, 6)?; + // Using the idf_tools.py script version that comes with the esp-idf git repository let idf_tools_py = path_buf![repository.worktree(), "tools", "idf_tools.py"]; - let get_python_env_dir = || -> Result { - cmd!(PYTHON, &idf_tools_py, "--idf-path", repository.worktree(), "--quiet", "export", "--format=key-value"; - ignore_exitcode=(), env=(IDF_TOOLS_PATH_VAR, &install_dir), - env_remove=(IDF_PYTHON_ENV_PATH_VAR), env_remove=("MSYSTEM")) - .stdout()? - .lines() - .find_map({ - let python_env_var_prefix = format!("{IDF_PYTHON_ENV_PATH_VAR}="); - move |s| s.trim().strip_prefix(&python_env_var_prefix).map(str::to_string) - }) - .ok_or_else(|| anyhow!( - "`idf_tools.py export` result contains no `{IDF_PYTHON_ENV_PATH_VAR}` item" - )) - }; + // TODO: add virtual_env check to skip install-python-env + // running the command cost 2-3 seconds but always makes sure that everything is installed correctly and is up-to-date - let python_env_dir: PathBuf = match get_python_env_dir() { - Ok(dir) if Path::new(&dir).exists() => dir, - _ => { - cmd!(PYTHON, &idf_tools_py, "--idf-path", repository.worktree(), "--non-interactive", "install-python-env"; - env=(IDF_TOOLS_PATH_VAR, &install_dir), env_remove=("MSYSTEM"), env_remove=(IDF_PYTHON_ENV_PATH_VAR)).run()?; - get_python_env_dir()? - } - }.into(); - - // TODO: better way to get the virtualenv python executable - let python = which::which_in( - "python", - #[cfg(windows)] - Some(&python_env_dir.join("Scripts")), - #[cfg(not(windows))] - Some(&python_env_dir.join("bin")), - "", - )?; + // assumes that the command can be run repeatedly + // whenalready installed -> checks for updates and a working state + cmd!(PYTHON, &idf_tools_py, "--idf-path", repository.worktree(), "--non-interactive", "install-python-env"; + env=(IDF_TOOLS_PATH_VAR, &install_dir), env_remove=("MSYSTEM"), env_remove=(IDF_PYTHON_ENV_PATH_VAR)).run()?; + + // since the above command exited sucessfully -> there should be a virt_env dir + + // the idf_tools.py templating name according to https://github.com/espressif/esp-idf/blob/master/tools/idf_tools.py#L99 + // uses always the systems python version -> idf{ESP_IDF_MAJOR_MINOR_VERSION}_py{SYSTEM_PYTHON_MAJOR_MINOR}_env, + + // with above knowladge -> construct the python_env_dir implicitly + let idf_major_minor = format!("{}.{}", esp_version.major, esp_version.minor); + let python_major_minor = format!("{}.{}", python_version.major, python_version.minor); + + let python_env_dir_template = format!("idf{idf_major_minor}_py{python_major_minor}_env"); + + let python_env_dir = path_buf![&install_dir, "python_env", python_env_dir_template]; + + let esp_version = Ok(esp_version); + + #[cfg(windows)] + let venv_python = PathBuf::from(python_env_dir).join("Scripts/python"); + + #[cfg(not(windows))] + let venv_python = python_env_dir.join("bin/python"); + + log::debug!("Start installing tools"); + + // End: Install virt_env + // Section: Install tools. - // Install tools. let tools = self .tools_provider - .map(|p| p(&repository, &version)) + .map(|p| p(&repository, &esp_version)) .unwrap_or(Ok(Vec::new()))?; - let mut exported_paths = HashSet::new(); - for tool in tools { - let tools_json = tool - .index - .as_ref() - .map(|tools_json| [OsStr::new("--tools-json"), tools_json.as_os_str()].into_iter()) - .into_iter() - .flatten(); - - // Install the tools. - cmd!(&python, &idf_tools_py, "--idf-path", repository.worktree(), @tools_json.clone(), "install"; - env=(IDF_TOOLS_PATH_VAR, &install_dir), args=(tool.tools)).run()?; - - // Get the paths to the tools. - // - // Note: `idf_tools.py` queries the environment - // variable `MSYSTEM` to determine if it should convert the paths to its shell - // equivalent on windows - // (https://github.com/espressif/esp-idf/blob/bcbef9a8db54d2deef83402f6e4403ccf298803a/tools/idf_tools.py#L243) - // (for example to unix paths when using msys or cygwin), but we need Windows - // native paths in rust. So we remove that environment variable when calling - // idf_tools.py. - exported_paths.extend( - cmd!(&python, &idf_tools_py, "--idf-path", repository.worktree(), @tools_json, "--quiet", "export", "--format=key-value"; - ignore_exitcode=(), env=(IDF_TOOLS_PATH_VAR, &install_dir), env_remove=("MSYSTEM")).stdout()? - .lines() - .find(|s| s.trim_start().starts_with("PATH=")) - .unwrap_or_default().trim() // `idf_tools.py export` result contains no `PATH` item if all tools are already in $PATH - .strip_prefix("PATH=").unwrap_or_default() - .rsplit_once(path_var_sep).unwrap_or_default().0 // remove $PATH, %PATH% - .split(path_var_sep) - .map(|s| s.to_owned()) - ); + + let tools_wanted = tools.clone(); + let tools_wanted: Vec<&str> = tools_wanted + .iter() + .flat_map(|tool| tool.tools.iter().map(|s| s.as_str())) + .collect(); + + let tools_json = repository.worktree().join("tools/tools.json"); + + let tools_vec = parse_tools( + tools_wanted.clone(), + tools_json.clone(), + install_dir.clone(), + ) + .unwrap(); + + //let tools_vec = parse_into_tools(tools_wanted, tools_json, install_dir.clone())?; + + let all_tools_installed = tools_vec.iter().all(|tool| tool.test()); + + if !all_tools_installed { + for tool_set in tools { + let tools_json = tool_set + .index + .as_ref() + .map(|tools_json| { + [OsStr::new("--tools-json"), tools_json.as_os_str()].into_iter() + }) + .into_iter() + .flatten(); + + cmd!(&venv_python, &idf_tools_py, "--idf-path", repository.worktree(), @tools_json.clone(), "install"; + env=(IDF_TOOLS_PATH_VAR, &install_dir), args=(tool_set.tools)).run()?; + } + + // Test again if all tools are now installed correctly + let all_tools_installed = tools_vec.iter().all(|tool| tool.test()); + if !all_tools_installed { + return Err(anyhow::Error::msg("Could not install all requested Tools")); + } } + // End Tools install + // Create PATH + + // All tools are installed -> infer there PATH variable by using the information out of tools.json + let mut tools_path: Vec = tools_vec + .iter() + .map(|tool| tool.abs_export_path()) + .collect(); + + // add the python virtual env to the export path + let mut python_path = venv_python.clone(); + python_path.pop(); + tools_path.push(python_path); + let paths = env::join_paths( - exported_paths + tools_path .into_iter() .map(PathBuf::from) .chain(env::split_paths(&env::var_os("PATH").unwrap_or_default())), @@ -476,8 +679,8 @@ impl Installer { Ok(EspIdf { repository, exported_path: paths, - venv_python: python, - version, + venv_python, + version: esp_version, is_managed_espidf: managed_repo, }) } diff --git a/src/espidf/tools_schema.rs b/src/espidf/tools_schema.rs new file mode 100644 index 0000000..1e332a6 --- /dev/null +++ b/src/espidf/tools_schema.rs @@ -0,0 +1,800 @@ +#![allow(clippy::redundant_closure_call)] +#![allow(clippy::needless_lifetimes)] +#![allow(clippy::match_single_binding)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::to_string_trait_impl)] + +/// This is an autogenerated file and includes small manual fixes +/// +/// generated with [cargo-typify](https://github.com/oxidecomputer/typify) v0.1 +/// using command "cargo typify -B tools_schema.json" +/// and the [tools_schema.json]( https://github.com/espressif/esp-idf/blob/master/tools/tools_schema.json ) file +/// +/// manual adjustments: +/// - made everything pub into pub (crate) +/// - manual adding missing macos-arm64 prob to VersionInfo not included in schema -> https://github.com/espressif/esp-idf/issues/13853 +/// - Switched out EnvVars with serde_json Value, cause the json schema is using a patternPropertie to "specify" the field key **sick** +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[doc = r" Error types."] +pub(crate) mod error { + #[doc = r" Error from a TryFrom or FromStr implementation."] + pub(crate) struct ConversionError(std::borrow::Cow<'static, str>); + impl std::error::Error for ConversionError {} + impl std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + std::fmt::Display::fmt(&self.0, f) + } + } + impl std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + std::fmt::Debug::fmt(&self.0, f) + } + } + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } +} +#[doc = "Array of strings. Used to represent paths (split into components) and command lines (split into arguments)"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"description\": \"Array of strings. Used to represent paths (split into components) and command lines (split into arguments)\","] +#[doc = " \"type\": \"array\","] +#[doc = " \"items\": {"] +#[doc = " \"type\": \"string\""] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct ArrayOfStrings(pub(crate) Vec); +impl std::ops::Deref for ArrayOfStrings { + type Target = Vec; + fn deref(&self) -> &Vec { + &self.0 + } +} +impl From for Vec { + fn from(value: ArrayOfStrings) -> Self { + value.0 + } +} +impl From<&ArrayOfStrings> for ArrayOfStrings { + fn from(value: &ArrayOfStrings) -> Self { + value.clone() + } +} +impl From> for ArrayOfStrings { + fn from(value: Vec) -> Self { + Self(value) + } +} +/* #[doc = "Collection of environment variables. Keys and values are the environment variable names and values, respectively."] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"description\": \"Collection of environment variables. Keys and values are the environment variable names and values, respectively.\","] +#[doc = " \"type\": \"object\","] +#[doc = " \"patternProperties\": {"] +#[doc = " \"^([A-Z_0-9]+)+$\": {"] +#[doc = " \"type\": \"string\""] +#[doc = " }"] +#[doc = " },"] +#[doc = " \"additionalProperties\": false"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Debug, Deserialize, Serialize)] +//#[serde(deny_unknown_fields)] +pub(crate) struct EnvVars {} +impl From<&EnvVars> for EnvVars { + fn from(value: &EnvVars) -> Self { + value.clone() + } +} */ +#[doc = "Array of paths to be exported (added to PATH). Each item in the array is relative to the directory where the tool will be installed."] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"description\": \"Array of paths to be exported (added to PATH). Each item in the array is relative to the directory where the tool will be installed.\","] +#[doc = " \"type\": \"array\","] +#[doc = " \"items\": {"] +#[doc = " \"$ref\": \"#/definitions/arrayOfStrings\""] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct ExportPaths(pub(crate) Vec); +impl std::ops::Deref for ExportPaths { + type Target = Vec; + fn deref(&self) -> &Vec { + &self.0 + } +} +impl From for Vec { + fn from(value: ExportPaths) -> Self { + value.0 + } +} +impl From<&ExportPaths> for ExportPaths { + fn from(value: &ExportPaths) -> Self { + value.clone() + } +} +impl From> for ExportPaths { + fn from(value: Vec) -> Self { + Self(value) + } +} +#[doc = "If 'always', the tool will be installed by default. If 'on_request', tool will be installed when specifically requested. If 'never', tool will not be considered for installation."] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"description\": \"If 'always', the tool will be installed by default. If 'on_request', tool will be installed when specifically requested. If 'never', tool will not be considered for installation.\","] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"always\","] +#[doc = " \"on_request\","] +#[doc = " \"never\""] +#[doc = " ]"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub(crate) enum InstallRequirementInfo { + #[serde(rename = "always")] + Always, + #[serde(rename = "on_request")] + OnRequest, + #[serde(rename = "never")] + Never, +} +impl From<&InstallRequirementInfo> for InstallRequirementInfo { + fn from(value: &InstallRequirementInfo) -> Self { + value.clone() + } +} +impl ToString for InstallRequirementInfo { + fn to_string(&self) -> String { + match *self { + Self::Always => "always".to_string(), + Self::OnRequest => "on_request".to_string(), + Self::Never => "never".to_string(), + } + } +} +impl std::str::FromStr for InstallRequirementInfo { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> Result { + match value { + "always" => Ok(Self::Always), + "on_request" => Ok(Self::OnRequest), + "never" => Ok(Self::Never), + _ => Err("invalid value".into()), + } + } +} +impl std::convert::TryFrom<&str> for InstallRequirementInfo { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> Result { + value.parse() + } +} +impl std::convert::TryFrom<&String> for InstallRequirementInfo { + type Error = self::error::ConversionError; + fn try_from(value: &String) -> Result { + value.parse() + } +} +impl std::convert::TryFrom for InstallRequirementInfo { + type Error = self::error::ConversionError; + fn try_from(value: String) -> Result { + value.parse() + } +} +#[doc = "Information about download artifact for one platform"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"description\": \"Information about download artifact for one platform\","] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"sha256\","] +#[doc = " \"size\","] +#[doc = " \"url\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"rename_dist\": {"] +#[doc = " \"description\": \"Filename under which the archive should be downloaded\","] +#[doc = " \"type\": \"string\""] +#[doc = " },"] +#[doc = " \"sha256\": {"] +#[doc = " \"description\": \"SHA256 sum of the file\","] +#[doc = " \"type\": \"string\""] +#[doc = " },"] +#[doc = " \"size\": {"] +#[doc = " \"description\": \"Size of the file, in bytes\","] +#[doc = " \"type\": \"integer\""] +#[doc = " },"] +#[doc = " \"url\": {"] +#[doc = " \"description\": \"Download URL\","] +#[doc = " \"type\": \"string\""] +#[doc = " }"] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct PlatformDownloadInfo { + #[doc = "Filename under which the archive should be downloaded"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) rename_dist: Option, + #[doc = "SHA256 sum of the file"] + pub(crate) sha256: String, + #[doc = "Size of the file, in bytes"] + pub(crate) size: i64, + #[doc = "Download URL"] + pub(crate) url: String, +} +impl From<&PlatformDownloadInfo> for PlatformDownloadInfo { + fn from(value: &PlatformDownloadInfo) -> Self { + value.clone() + } +} +#[doc = "Platform-specific values which override the defaults"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"description\": \"Platform-specific values which override the defaults\","] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"platforms\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"export_paths\": {"] +#[doc = " \"description\": \"Platform-specific replacement for toolInfo/export_paths\","] +#[doc = " \"$ref\": \"#/definitions/exportPaths\""] +#[doc = " },"] +#[doc = " \"export_vars\": {"] +#[doc = " \"description\": \"Platform-specific replacement for toolInfo/export_vars\","] +#[doc = " \"$ref\": \"#/definitions/envVars\""] +#[doc = " },"] +#[doc = " \"install\": {"] +#[doc = " \"description\": \"Platform-specific replacement for toolInfo/install\","] +#[doc = " \"$ref\": \"#/definitions/installRequirementInfo\""] +#[doc = " },"] +#[doc = " \"platforms\": {"] +#[doc = " \"description\": \"List of platforms to which this override applies\","] +#[doc = " \"type\": \"array\","] +#[doc = " \"items\": {"] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"linux-i686\","] +#[doc = " \"linux-amd64\","] +#[doc = " \"linux-armel\","] +#[doc = " \"linux-arm64\","] +#[doc = " \"macos\","] +#[doc = " \"macos-arm64\","] +#[doc = " \"win32\","] +#[doc = " \"win64\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " },"] +#[doc = " \"strip_container_dirs\": {"] +#[doc = " \"description\": \"Platform-specific replacement for toolInfo/strip_container_dirs\","] +#[doc = " \"type\": \"string\""] +#[doc = " },"] +#[doc = " \"supported_targets\": {"] +#[doc = " \"description\": \"Platform-specific replacement for toolInfo/supported_targets\","] +#[doc = " \"$ref\": \"#/definitions/arrayOfStrings\""] +#[doc = " },"] +#[doc = " \"version_cmd\": {"] +#[doc = " \"description\": \"Platform-specific replacement for toolInfo/version_cmd\","] +#[doc = " \"$ref\": \"#/definitions/arrayOfStrings\""] +#[doc = " },"] +#[doc = " \"version_regex\": {"] +#[doc = " \"description\": \"Platform-specific replacement for toolInfo/version_regex\","] +#[doc = " \"$ref\": \"#/definitions/regex\""] +#[doc = " },"] +#[doc = " \"version_regex_replace\": {"] +#[doc = " \"description\": \"Platform-specific replacement for toolInfo/version_regex_replace\","] +#[doc = " \"type\": \"string\""] +#[doc = " }"] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct PlatformOverrideInfo { + #[doc = "Platform-specific replacement for toolInfo/export_paths"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) export_paths: Option, + #[doc = "Platform-specific replacement for toolInfo/export_vars"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) export_vars: Option, + #[doc = "Platform-specific replacement for toolInfo/install"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) install: Option, + #[doc = "List of platforms to which this override applies"] + pub(crate) platforms: Vec, + #[doc = "Platform-specific replacement for toolInfo/strip_container_dirs"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) strip_container_dirs: Option, + #[doc = "Platform-specific replacement for toolInfo/supported_targets"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) supported_targets: Option, + #[doc = "Platform-specific replacement for toolInfo/version_cmd"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) version_cmd: Option, + #[doc = "Platform-specific replacement for toolInfo/version_regex"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) version_regex: Option, + #[doc = "Platform-specific replacement for toolInfo/version_regex_replace"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) version_regex_replace: Option, +} +impl From<&PlatformOverrideInfo> for PlatformOverrideInfo { + fn from(value: &PlatformOverrideInfo) -> Self { + value.clone() + } +} +#[doc = "PlatformOverrideInfoPlatformsItem"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"linux-i686\","] +#[doc = " \"linux-amd64\","] +#[doc = " \"linux-armel\","] +#[doc = " \"linux-arm64\","] +#[doc = " \"macos\","] +#[doc = " \"macos-arm64\","] +#[doc = " \"win32\","] +#[doc = " \"win64\""] +#[doc = " ]"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub(crate) enum PlatformOverrideInfoPlatformsItem { + #[serde(rename = "linux-i686")] + LinuxI686, + #[serde(rename = "linux-amd64")] + LinuxAmd64, + #[serde(rename = "linux-armel")] + LinuxArmel, + #[serde(rename = "linux-arm64")] + LinuxArm64, + #[serde(rename = "macos")] + Macos, + #[serde(rename = "macos-arm64")] + MacosArm64, + #[serde(rename = "win32")] + Win32, + #[serde(rename = "win64")] + Win64, +} +impl From<&PlatformOverrideInfoPlatformsItem> for PlatformOverrideInfoPlatformsItem { + fn from(value: &PlatformOverrideInfoPlatformsItem) -> Self { + value.clone() + } +} +impl ToString for PlatformOverrideInfoPlatformsItem { + fn to_string(&self) -> String { + match *self { + Self::LinuxI686 => "linux-i686".to_string(), + Self::LinuxAmd64 => "linux-amd64".to_string(), + Self::LinuxArmel => "linux-armel".to_string(), + Self::LinuxArm64 => "linux-arm64".to_string(), + Self::Macos => "macos".to_string(), + Self::MacosArm64 => "macos-arm64".to_string(), + Self::Win32 => "win32".to_string(), + Self::Win64 => "win64".to_string(), + } + } +} +impl std::str::FromStr for PlatformOverrideInfoPlatformsItem { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> Result { + match value { + "linux-i686" => Ok(Self::LinuxI686), + "linux-amd64" => Ok(Self::LinuxAmd64), + "linux-armel" => Ok(Self::LinuxArmel), + "linux-arm64" => Ok(Self::LinuxArm64), + "macos" => Ok(Self::Macos), + "macos-arm64" => Ok(Self::MacosArm64), + "win32" => Ok(Self::Win32), + "win64" => Ok(Self::Win64), + _ => Err("invalid value".into()), + } + } +} +impl std::convert::TryFrom<&str> for PlatformOverrideInfoPlatformsItem { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> Result { + value.parse() + } +} +impl std::convert::TryFrom<&String> for PlatformOverrideInfoPlatformsItem { + type Error = self::error::ConversionError; + fn try_from(value: &String) -> Result { + value.parse() + } +} +impl std::convert::TryFrom for PlatformOverrideInfoPlatformsItem { + type Error = self::error::ConversionError; + fn try_from(value: String) -> Result { + value.parse() + } +} +#[doc = "A regular expression"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"description\": \"A regular expression\","] +#[doc = " \"type\": \"string\""] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub(crate) struct Regex(pub(crate) String); +impl std::ops::Deref for Regex { + type Target = String; + fn deref(&self) -> &String { + &self.0 + } +} +impl From for String { + fn from(value: Regex) -> Self { + value.0 + } +} +impl From<&Regex> for Regex { + fn from(value: &Regex) -> Self { + value.clone() + } +} +impl From for Regex { + fn from(value: String) -> Self { + Self(value) + } +} +impl std::str::FromStr for Regex { + type Err = std::convert::Infallible; + fn from_str(value: &str) -> Result { + Ok(Self(value.to_string())) + } +} +impl ToString for Regex { + fn to_string(&self) -> String { + self.0.to_string() + } +} +#[doc = "Information about one tool"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"description\": \"Information about one tool\","] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"description\","] +#[doc = " \"export_paths\","] +#[doc = " \"info_url\","] +#[doc = " \"install\","] +#[doc = " \"license\","] +#[doc = " \"version_cmd\","] +#[doc = " \"version_regex\","] +#[doc = " \"versions\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"description\": {"] +#[doc = " \"description\": \"A short (one sentence) description of the tool.\","] +#[doc = " \"type\": \"string\""] +#[doc = " },"] +#[doc = " \"export_paths\": {"] +#[doc = " \"$ref\": \"#/definitions/exportPaths\""] +#[doc = " },"] +#[doc = " \"export_vars\": {"] +#[doc = " \"description\": \"Some variable expansions are done on the values. 1) ${TOOL_PATH} is replaced with the directory where the tool is installed.\","] +#[doc = " \"$ref\": \"#/definitions/envVars\""] +#[doc = " },"] +#[doc = " \"info_url\": {"] +#[doc = " \"description\": \"URL of the page with information about the tool\","] +#[doc = " \"type\": \"string\""] +#[doc = " },"] +#[doc = " \"install\": {"] +#[doc = " \"description\": \"If 'always', the tool will be installed by default. If 'on_request', tool will be installed when specifically requested. If 'never', tool will not be considered for installation.\","] +#[doc = " \"$ref\": \"#/definitions/installRequirementInfo\""] +#[doc = " },"] +#[doc = " \"is_executable\": {"] +#[doc = " \"description\": \"If false - tool does not contain executables. The version will not be checked but export_vars applied.\","] +#[doc = " \"type\": \"boolean\""] +#[doc = " },"] +#[doc = " \"license\": {"] +#[doc = " \"description\": \"License name. Use SPDX license identifier if it exists, short name of the license otherwise.\","] +#[doc = " \"type\": \"string\""] +#[doc = " },"] +#[doc = " \"name\": {"] +#[doc = " \"description\": \"Tool name (used as a directory name)\","] +#[doc = " \"type\": \"string\""] +#[doc = " },"] +#[doc = " \"platform_overrides\": {"] +#[doc = " \"description\": \"List of platform-specific overrides\","] +#[doc = " \"type\": \"array\","] +#[doc = " \"items\": {"] +#[doc = " \"$ref\": \"#/definitions/platformOverrideInfo\""] +#[doc = " }"] +#[doc = " },"] +#[doc = " \"strip_container_dirs\": {"] +#[doc = " \"description\": \"If specified, this number of top directory levels will removed when extracting. E.g. if strip_container_dirs=2, archive path a/b/c/d.txt will be extracted as c/d.txt\","] +#[doc = " \"type\": \"integer\""] +#[doc = " },"] +#[doc = " \"supported_targets\": {"] +#[doc = " \"description\": \"Array of esp_targets that this tool is needed for.\","] +#[doc = " \"$ref\": \"#/definitions/arrayOfStrings\""] +#[doc = " },"] +#[doc = " \"version_cmd\": {"] +#[doc = " \"description\": \"Command to be executed (along with any extra arguments). The executable be present in one of the export_paths.\","] +#[doc = " \"$ref\": \"#/definitions/arrayOfStrings\""] +#[doc = " },"] +#[doc = " \"version_regex\": {"] +#[doc = " \"description\": \"Regex which is to be applied to version_cmd output to extract the version. By default, the version will be the first capture group of the expression. If version_regex_replace is specified, version will be obtained by doing a substitution using version_regex_replace instead.\","] +#[doc = " \"$ref\": \"#/definitions/regex\""] +#[doc = " },"] +#[doc = " \"version_regex_replace\": {"] +#[doc = " \"description\": \"If given, this will be used as substitute expression for the regex defined in version_regex, to obtain the version string. Not specifying this is equivalent to setting it to '\\\\1' (i.e. return the first capture group).\","] +#[doc = " \"type\": \"string\""] +#[doc = " },"] +#[doc = " \"versions\": {"] +#[doc = " \"description\": \"List of versions\","] +#[doc = " \"type\": \"array\","] +#[doc = " \"items\": {"] +#[doc = " \"$ref\": \"#/definitions/versionInfo\""] +#[doc = " }"] +#[doc = " }"] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct ToolInfo { + #[doc = "A short (one sentence) description of the tool."] + pub(crate) description: String, + pub(crate) export_paths: ExportPaths, + #[doc = "Some variable expansions are done on the values. 1) ${TOOL_PATH} is replaced with the directory where the tool is installed."] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) export_vars: Option, + #[doc = "URL of the page with information about the tool"] + pub(crate) info_url: String, + #[doc = "If 'always', the tool will be installed by default. If 'on_request', tool will be installed when specifically requested. If 'never', tool will not be considered for installation."] + pub(crate) install: InstallRequirementInfo, + #[doc = "If false - tool does not contain executables. The version will not be checked but export_vars applied."] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) is_executable: Option, + #[doc = "License name. Use SPDX license identifier if it exists, short name of the license otherwise."] + pub(crate) license: String, + #[doc = "Tool name (used as a directory name)"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) name: Option, + #[doc = "List of platform-specific overrides"] + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub(crate) platform_overrides: Vec, + #[doc = "If specified, this number of top directory levels will removed when extracting. E.g. if strip_container_dirs=2, archive path a/b/c/d.txt will be extracted as c/d.txt"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) strip_container_dirs: Option, + #[doc = "Array of esp_targets that this tool is needed for."] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) supported_targets: Option, + #[doc = "Command to be executed (along with any extra arguments). The executable be present in one of the export_paths."] + pub(crate) version_cmd: ArrayOfStrings, + #[doc = "Regex which is to be applied to version_cmd output to extract the version. By default, the version will be the first capture group of the expression. If version_regex_replace is specified, version will be obtained by doing a substitution using version_regex_replace instead."] + pub(crate) version_regex: Regex, + #[doc = "If given, this will be used as substitute expression for the regex defined in version_regex, to obtain the version string. Not specifying this is equivalent to setting it to '\\1' (i.e. return the first capture group)."] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) version_regex_replace: Option, + #[doc = "List of versions"] + pub(crate) versions: Vec, +} +impl From<&ToolInfo> for ToolInfo { + fn from(value: &ToolInfo) -> Self { + value.clone() + } +} +#[doc = "VersionInfo"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"object\","] +#[doc = " \"properties\": {"] +#[doc = " \"any\": {"] +#[doc = " \"$ref\": \"#/definitions/platformDownloadInfo\""] +#[doc = " },"] +#[doc = " \"linux-amd64\": {"] +#[doc = " \"$ref\": \"#/definitions/platformDownloadInfo\""] +#[doc = " },"] +#[doc = " \"linux-arm64\": {"] +#[doc = " \"$ref\": \"#/definitions/platformDownloadInfo\""] +#[doc = " },"] +#[doc = " \"linux-armel\": {"] +#[doc = " \"$ref\": \"#/definitions/platformDownloadInfo\""] +#[doc = " },"] +#[doc = " \"linux-i686\": {"] +#[doc = " \"$ref\": \"#/definitions/platformDownloadInfo\""] +#[doc = " },"] +#[doc = " \"macos\": {"] +#[doc = " \"$ref\": \"#/definitions/platformDownloadInfo\""] +#[doc = " },"] +#[doc = " \"name\": {"] +#[doc = " \"description\": \"Version name (used as a directory name)\","] +#[doc = " \"type\": \"string\""] +#[doc = " },"] +#[doc = " \"status\": {"] +#[doc = " \"description\": \"Determines whether the version is recommended/supported/deprecated\","] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"recommended\","] +#[doc = " \"supported\","] +#[doc = " \"deprecated\""] +#[doc = " ]"] +#[doc = " },"] +#[doc = " \"win32\": {"] +#[doc = " \"$ref\": \"#/definitions/platformDownloadInfo\""] +#[doc = " },"] +#[doc = " \"win64\": {"] +#[doc = " \"$ref\": \"#/definitions/platformDownloadInfo\""] +#[doc = " }"] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct VersionInfo { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) any: Option, + #[serde( + rename = "linux-amd64", + default, + skip_serializing_if = "Option::is_none" + )] + pub(crate) linux_amd64: Option, + #[serde( + rename = "linux-arm64", + default, + skip_serializing_if = "Option::is_none" + )] + pub(crate) linux_arm64: Option, + #[serde( + rename = "linux-armel", + default, + skip_serializing_if = "Option::is_none" + )] + pub(crate) linux_armel: Option, + #[serde( + rename = "linux-i686", + default, + skip_serializing_if = "Option::is_none" + )] + pub(crate) linux_i686: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) macos: Option, + #[serde( + rename = "macos-arm64", + default, + skip_serializing_if = "Option::is_none" + )] + pub(crate) macos_arm64: Option, + #[doc = "Version name (used as a directory name)"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) name: Option, + #[doc = "Determines whether the version is recommended/supported/deprecated"] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) status: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) win32: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) win64: Option, +} +impl From<&VersionInfo> for VersionInfo { + fn from(value: &VersionInfo) -> Self { + value.clone() + } +} +#[doc = "Determines whether the version is recommended/supported/deprecated"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"description\": \"Determines whether the version is recommended/supported/deprecated\","] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"recommended\","] +#[doc = " \"supported\","] +#[doc = " \"deprecated\""] +#[doc = " ]"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub(crate) enum VersionInfoStatus { + #[serde(rename = "recommended")] + Recommended, + #[serde(rename = "supported")] + Supported, + #[serde(rename = "deprecated")] + Deprecated, +} +impl From<&VersionInfoStatus> for VersionInfoStatus { + fn from(value: &VersionInfoStatus) -> Self { + value.clone() + } +} +impl ToString for VersionInfoStatus { + fn to_string(&self) -> String { + match *self { + Self::Recommended => "recommended".to_string(), + Self::Supported => "supported".to_string(), + Self::Deprecated => "deprecated".to_string(), + } + } +} +impl std::str::FromStr for VersionInfoStatus { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> Result { + match value { + "recommended" => Ok(Self::Recommended), + "supported" => Ok(Self::Supported), + "deprecated" => Ok(Self::Deprecated), + _ => Err("invalid value".into()), + } + } +} +impl std::convert::TryFrom<&str> for VersionInfoStatus { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> Result { + value.parse() + } +} +impl std::convert::TryFrom<&String> for VersionInfoStatus { + type Error = self::error::ConversionError; + fn try_from(value: &String) -> Result { + value.parse() + } +} +impl std::convert::TryFrom for VersionInfoStatus { + type Error = self::error::ConversionError; + fn try_from(value: String) -> Result { + value.parse() + } +} diff --git a/src/python.rs b/src/python.rs index c6ecd3a..61b1394 100644 --- a/src/python.rs +++ b/src/python.rs @@ -16,8 +16,14 @@ pub const PYTHON: &str = { } }; +/// The Version of a Python Binary +pub struct PythonVersion { + pub major: u32, + pub minor: u32, +} + /// Check that python is at least `major.minor`. -pub fn check_python_at_least(major: u32, minor: u32) -> Result<()> { +pub fn check_python_at_least(major: u32, minor: u32) -> Result { let version_str = cmd!(PYTHON, "--version") .stdout() .context("Failed to locate python. Is python installed and in your $PATH?")?; @@ -52,6 +58,9 @@ pub fn check_python_at_least(major: u32, minor: u32) -> Result<()> { ) .context(format!("When running '{PYTHON} --version'"))) } else { - Ok(()) + Ok(PythonVersion { + major: python_major, + minor: python_minor, + }) } }