| // Copyright 2022 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| use std::collections::{HashMap, HashSet}; |
| use std::path::{Path, PathBuf}; |
| |
| use crate::project::manifest; |
| use crate::util::StringSub; |
| use crate::{download, platform, Error, Result}; |
| |
| /// A source of targets. |
| #[derive(Debug)] |
| pub struct Provider { |
| /// The globally-unique name of the provider. |
| pub name: String, |
| |
| /// Manifest file in which the provider is defined. |
| pub file: PathBuf, |
| |
| /// If true, targets provided by the provider appear in the global namespace |
| /// instead of nested under the provider's name. |
| pub global: bool, |
| } |
| |
| impl Provider { |
| #[must_use] |
| pub(crate) fn new(name: &str, file: &Path, global: bool) -> Self { |
| Self { |
| name: name.into(), |
| file: file.into(), |
| global, |
| } |
| } |
| } |
| |
| /// A buildable `qg` target. |
| #[derive(Debug)] |
| pub struct Target { |
| /// The target's name, without a provider namespace. |
| name: String, |
| |
| /// The name of the target's provider. |
| provider: String, |
| |
| /// Whether the target lives in the global namespace. |
| global: bool, |
| |
| /// List of targets on which this one depends, storing their full namespaced |
| /// paths. |
| dependencies: Vec<String>, |
| |
| /// Information about how to build the target, specific to a type of |
| /// provider. |
| metadata: Metadata, |
| } |
| |
| /// Additional information about how to build a target specific to a type |
| /// of provider. |
| #[derive(Debug)] |
| pub enum Metadata { |
| DepOnly, |
| Cipd(Cipd), |
| Download(Download), |
| } |
| |
| #[derive(Debug)] |
| pub struct Cipd { |
| pub path: String, |
| pub platforms: Vec<String>, |
| pub tags: Vec<String>, |
| pub subdir: Option<PathBuf>, |
| } |
| |
| impl From<manifest::CipdPackage> for Cipd { |
| fn from(value: manifest::CipdPackage) -> Self { |
| // TODO(frolv): This should do some verification. |
| Self { |
| path: value.path, |
| platforms: value.platforms, |
| tags: value.tags, |
| subdir: value.subdir, |
| } |
| } |
| } |
| |
| #[derive(Debug)] |
| pub struct Download { |
| pub format: download::Format, |
| pub url: StringSub, |
| pub url_parameters: HashMap<String, String>, |
| pub variants: Vec<DownloadVariant>, |
| } |
| |
| #[derive(Debug)] |
| pub struct DownloadVariant { |
| pub matches: VariantMatch, |
| url_parameters: HashMap<String, String>, |
| } |
| |
| #[derive(Debug)] |
| pub struct VariantMatch { |
| system: Option<platform::System>, |
| arch: Option<platform::Architecture>, |
| } |
| |
| impl TryFrom<manifest::DownloadablePackage> for Download { |
| type Error = Error; |
| |
| fn try_from(value: manifest::DownloadablePackage) -> std::result::Result<Self, Self::Error> { |
| let url_parameters = value.url_parameters; |
| let mut all_params: HashSet<&str> = url_parameters.keys().map(String::as_str).collect(); |
| |
| let variants: Vec<DownloadVariant> = value |
| .variants |
| .into_iter() |
| .map(|variant| { |
| let mut variant_match = VariantMatch { |
| system: None, |
| arch: None, |
| }; |
| |
| for (var, val) in &variant.match_vars { |
| match var.as_str() { |
| "os" => variant_match.system = Some(val.parse::<platform::System>()?), |
| "arch" => variant_match.arch = Some(val.parse::<platform::Architecture>()?), |
| _ => { |
| // TODO(frolv): Return an error with the invalid variable. |
| return Err(Error::GenericErrorPlaceholder); |
| } |
| } |
| } |
| |
| Ok(DownloadVariant { |
| matches: variant_match, |
| url_parameters: variant.url_parameters, |
| }) |
| }) |
| .collect::<Result<Vec<_>>>()?; |
| |
| for variant in &variants { |
| all_params.extend(variant.url_parameters.keys().map(String::as_str)); |
| } |
| |
| // Check if any of the parameters specified in the `url_parameters` |
| // mapping have invalid names. |
| let invalid_vars: Vec<_> = all_params |
| .iter() |
| .filter(|&¶m| !StringSub::valid_variable_name(param)) |
| .collect(); |
| if !invalid_vars.is_empty() { |
| // TODO(frolv): Return an error containing the invalid variables. |
| println!("invalid URL variable names {invalid_vars:?}"); |
| return Err(Error::GenericErrorPlaceholder); |
| } |
| |
| // Next, check the URL string itself. Each parameter substitution |
| // defined within braces should exist within a provided `url_parameters` |
| // mapping. |
| let url: StringSub = value.url.parse()?; |
| |
| let missing_vars: Vec<_> = url.vars().filter(|&v| !all_params.contains(v)).collect(); |
| if !missing_vars.is_empty() { |
| // TODO(frolv): Return an error containing the missing variables. |
| println!("missing URL parameters {missing_vars:?}"); |
| return Err(Error::GenericErrorPlaceholder); |
| } |
| |
| let format = match value.format.ok_or(Error::GenericErrorPlaceholder)?.as_str() { |
| "bin" => { |
| download::Format::Binary(value.bin_name.ok_or(Error::GenericErrorPlaceholder)?) |
| } |
| _ => return Err(Error::GenericErrorPlaceholder), |
| }; |
| |
| Ok(Self { |
| format, |
| url, |
| url_parameters, |
| variants, |
| }) |
| } |
| } |
| |
| impl TryFrom<manifest::TargetType> for Metadata { |
| type Error = Error; |
| |
| fn try_from(value: manifest::TargetType) -> std::result::Result<Self, Self::Error> { |
| match value { |
| manifest::TargetType::Cipd(data) => Ok(Self::Cipd(data.into())), |
| manifest::TargetType::Download(data) => data.try_into().map(Self::Download), |
| } |
| } |
| } |
| |
| impl Target { |
| pub(crate) fn from_manifest( |
| name: &str, |
| provider: &str, |
| global: bool, |
| target: manifest::Target, |
| ) -> Result<Self> { |
| let target = Self { |
| name: name.to_owned(), |
| provider: provider.to_owned(), |
| global, |
| dependencies: target.deps, |
| metadata: target |
| .desc |
| .map_or(Ok(Metadata::DepOnly), Metadata::try_from)?, |
| }; |
| |
| Ok(target) |
| } |
| |
| /// Returns the name of the package. |
| #[must_use] |
| pub fn name(&self) -> &str { |
| &self.name |
| } |
| |
| /// Returns the name of the package's provider. |
| #[must_use] |
| pub fn provider(&self) -> &str { |
| &self.provider |
| } |
| |
| /// Returns true if the target is defined in the global namespace. |
| #[must_use] |
| pub fn is_global(&self) -> bool { |
| self.global |
| } |
| |
| /// Returns the package's dependencies. |
| pub fn dependencies(&self) -> impl Iterator<Item = &str> { |
| self.dependencies.iter().map(String::as_str) |
| } |
| |
| /// Returns the fully-qualified name of the target. |
| #[must_use] |
| pub fn full_name(&self) -> String { |
| if self.global { |
| self.name.clone() |
| } else { |
| format!("{}:{}", self.provider, self.name) |
| } |
| } |
| |
| /// Returns the type of the package as a string. |
| #[must_use] |
| pub fn type_string(&self) -> &str { |
| match self.metadata { |
| Metadata::DepOnly => "dep_only", |
| Metadata::Cipd(_) => "cipd", |
| Metadata::Download(_) => "download", |
| } |
| } |
| |
| /// Returns the metadata of the target |
| #[must_use] |
| pub fn metadata(&self) -> &Metadata { |
| &self.metadata |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn target_from_deponly() { |
| let manifest_target = manifest::Target { |
| namespace: manifest::ProviderNamespace::Local, |
| deps: vec!["libqg".into(), "cargo".into()], |
| desc: None, |
| }; |
| let target = Target::from_manifest("qg", "qg-provider", true, manifest_target).unwrap(); |
| assert_eq!(target.name(), "qg"); |
| assert_eq!(target.provider(), "qg-provider"); |
| assert_eq!(target.full_name(), "qg"); |
| assert!(target.is_global()); |
| assert_eq!(target.dependencies().count(), 2); |
| assert_eq!(target.type_string(), "dep_only"); |
| } |
| |
| #[test] |
| fn target_from_deponly_local() { |
| let manifest_target = manifest::Target { |
| namespace: manifest::ProviderNamespace::Local, |
| deps: vec!["cargo".into()], |
| desc: None, |
| }; |
| let target = Target::from_manifest("qg", "qg-provider", false, manifest_target).unwrap(); |
| assert_eq!(target.name(), "qg"); |
| assert_eq!(target.provider(), "qg-provider"); |
| assert_eq!(target.full_name(), "qg-provider:qg"); |
| assert!(!target.is_global()); |
| assert_eq!(target.dependencies().count(), 1); |
| assert_eq!(target.type_string(), "dep_only"); |
| } |
| |
| #[test] |
| fn target_from_download_basic_valid() { |
| let manifest_target = manifest::Target { |
| namespace: manifest::ProviderNamespace::Local, |
| deps: vec![], |
| desc: Some(manifest::TargetType::Download( |
| manifest::DownloadablePackage { |
| url: "https://qg.io/client".into(), |
| url_parameters: HashMap::new(), |
| variants: vec![], |
| format: Some("bin".into()), |
| bin_name: Some("qg".into()), |
| }, |
| )), |
| }; |
| let target = Target::from_manifest("qg", "qg-provider", true, manifest_target).unwrap(); |
| assert_eq!(target.name(), "qg"); |
| assert_eq!(target.provider(), "qg-provider"); |
| assert_eq!(target.full_name(), "qg"); |
| assert!(target.is_global()); |
| assert_eq!(target.dependencies().count(), 0); |
| assert_eq!(target.type_string(), "download"); |
| } |
| |
| #[test] |
| fn target_from_download_params_valid() { |
| let manifest_target = manifest::Target { |
| namespace: manifest::ProviderNamespace::Local, |
| deps: vec![], |
| desc: Some(manifest::TargetType::Download( |
| manifest::DownloadablePackage { |
| url: "https://qg.io/client?version={ver}".into(), |
| url_parameters: HashMap::from([("ver".into(), "1.2.4".into())]), |
| variants: vec![], |
| format: Some("bin".into()), |
| bin_name: Some("qg".into()), |
| }, |
| )), |
| }; |
| let target = Target::from_manifest("qg", "qg-provider", true, manifest_target).unwrap(); |
| assert_eq!(target.name(), "qg"); |
| assert_eq!(target.provider(), "qg-provider"); |
| assert_eq!(target.full_name(), "qg"); |
| assert!(target.is_global()); |
| assert_eq!(target.dependencies().count(), 0); |
| assert_eq!(target.type_string(), "download"); |
| } |
| |
| #[test] |
| fn target_from_download_params_missing() { |
| let manifest_target = manifest::Target { |
| namespace: manifest::ProviderNamespace::Local, |
| deps: vec![], |
| desc: Some(manifest::TargetType::Download( |
| manifest::DownloadablePackage { |
| url: "https://qg.io/client?version={ver}".into(), |
| url_parameters: HashMap::new(), // no `ver` defined |
| variants: vec![], |
| format: Some("bin".into()), |
| bin_name: Some("qg".into()), |
| }, |
| )), |
| }; |
| let err = Target::from_manifest("qg", "qg-provider", true, manifest_target).unwrap_err(); |
| assert!(matches!(err, Error::GenericErrorPlaceholder)); |
| } |
| |
| #[test] |
| fn target_from_download_params_invalid_name() { |
| let manifest_target = manifest::Target { |
| namespace: manifest::ProviderNamespace::Local, |
| deps: vec![], |
| desc: Some(manifest::TargetType::Download( |
| manifest::DownloadablePackage { |
| url: "https://qg.io/client?version={0ver}".into(), |
| url_parameters: HashMap::from([("0ver".into(), "01.2.4".into())]), |
| variants: vec![], |
| format: Some("bin".into()), |
| bin_name: Some("qg".into()), |
| }, |
| )), |
| }; |
| let err = Target::from_manifest("qg", "qg-provider", true, manifest_target).unwrap_err(); |
| assert!(matches!(err, Error::GenericErrorPlaceholder)); |
| } |
| |
| #[test] |
| fn target_from_download_params_empty_name() { |
| let manifest_target = manifest::Target { |
| namespace: manifest::ProviderNamespace::Local, |
| deps: vec![], |
| desc: Some(manifest::TargetType::Download( |
| manifest::DownloadablePackage { |
| url: "https://qg.io/client?version={}".into(), |
| url_parameters: HashMap::new(), |
| variants: vec![], |
| format: Some("bin".into()), |
| bin_name: Some("qg".into()), |
| }, |
| )), |
| }; |
| let err = Target::from_manifest("qg", "qg-provider", true, manifest_target).unwrap_err(); |
| assert!(matches!(err, Error::GenericErrorPlaceholder)); |
| } |
| } |