blob: ccf4d4ad79c790a0cf4da74dd401d8b16d3de674 [file] [log] [blame]
// 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(|&&param| !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));
}
}