use super::*;
use crate::{
remote::resolution::{DirectRes, IndexRes},
util::{valid_file, SubPath},
};
use failure::{format_err, Error, ResultExt};
use ignore::gitignore::GitignoreBuilder;
use indexmap::IndexMap;
use semver::Version;
use semver_constraints::Constraint;
use serde::Deserialize;
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use toml;
use url::Url;
use url_serde;
use walkdir::{DirEntry, WalkDir};
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Manifest {
pub package: PackageInfo,
#[serde(default = "IndexMap::new")]
pub dependencies: IndexMap<Name, DepReq>,
#[serde(default = "IndexMap::new")]
pub dev_dependencies: IndexMap<Name, DepReq>,
#[serde(default)]
pub targets: Targets,
#[serde(default)]
pub workspace: IndexMap<Name, SubPath>,
#[serde(default)]
pub scripts: IndexMap<String, String>,
}
impl Manifest {
pub fn workspace(s: &str) -> Option<IndexMap<Name, SubPath>> {
toml::value::Value::try_from(&s)
.ok()?
.get("workspace")?
.clone()
.try_into()
.ok()
}
pub fn version(&self) -> &Version {
&self.package.version
}
pub fn name(&self) -> &Name {
&self.package.name
}
pub fn deps(
&self,
ixmap: &IndexMap<String, IndexRes>,
dev_deps: bool,
) -> Res<IndexMap<PackageId, Constraint>> {
let mut deps = IndexMap::new();
for (n, dep) in &self.dependencies {
let dep = dep.clone();
let (pid, c) = dep.into_dep(&ixmap, n.clone())?;
deps.insert(pid, c);
}
if dev_deps {
for (n, dep) in &self.dev_dependencies {
let dep = dep.clone();
let (pid, c) = dep.into_dep(&ixmap, n.clone())?;
deps.insert(pid, c);
}
}
Ok(deps)
}
pub fn list_files<P>(
&self,
pkg_root: &Path,
search_root: &Path,
mut p: P,
) -> Res<impl Iterator<Item = DirEntry>>
where
P: FnMut(&DirEntry) -> bool,
{
let mut excludes = GitignoreBuilder::new(pkg_root);
if let Some(rs) = self.package.exclude.as_ref() {
for r in rs {
excludes.add_line(None, r)?;
}
}
if pkg_root.join(".gitignore").exists() {
if let Some(e) = excludes.add(pkg_root.join(".gitignore")) {
return Err(e)?;
}
}
let excludes = excludes
.build()
.with_context(|e| format_err!("invalid excludes: {}", e))?;
let walker = WalkDir::new(search_root)
.follow_links(true)
.into_iter()
.filter_entry(move |x| {
!excludes
.matched_path_or_any_parents(x.path(), x.file_type().is_dir())
.is_ignore()
&& p(&x)
})
.filter_map(|x| {
x.ok()
.and_then(|x| if valid_file(&x) { Some(x) } else { None })
});
Ok(walker)
}
}
impl FromStr for Manifest {
type Err = Error;
fn from_str(raw: &str) -> Result<Self, Self::Err> {
let toml: Manifest = toml::from_str(raw)
.with_context(|e| format_err!("invalid manifest file: {}", e))
.map_err(Error::from)?;
Ok(toml)
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct PackageInfo {
pub name: Name,
pub version: Version,
pub authors: Vec<String>,
pub build: Option<SubPath>,
pub description: Option<String>,
#[serde(default = "Vec::new")]
pub keywords: Vec<String>,
pub homepage: Option<String>,
pub repository: Option<String>,
pub readme: Option<SubPath>,
pub license: Option<String>,
pub exclude: Option<Vec<String>>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum DepReq {
Registry(Constraint),
RegLong {
version: Constraint,
index: String,
},
Local {
path: PathBuf,
},
Git {
#[serde(with = "url_serde")]
git: Url,
#[serde(default = "default_tag")]
tag: String,
},
}
fn default_tag() -> String {
"master".to_owned()
}
impl DepReq {
pub fn into_dep(
self,
ixmap: &IndexMap<String, IndexRes>,
n: Name,
) -> Res<(PackageId, Constraint)> {
match self {
DepReq::Registry(c) => {
let def_index = ixmap
.get_index(0)
.ok_or_else(|| format_err!("no default index"))?;
let pi = PackageId::new(n, def_index.1.clone().into());
Ok((pi, c))
}
DepReq::RegLong { version, index } => {
if let Some(mapped) = ixmap.get(&index) {
let pi = PackageId::new(n, mapped.clone().into());
Ok((pi, version))
} else {
let ix = IndexRes::from_str(&index)?;
let pi = PackageId::new(n, ix.into());
Ok((pi, version))
}
}
DepReq::Local { path } => {
let res = DirectRes::Dir { path };
let pi = PackageId::new(n, res.into());
Ok((pi, Constraint::any()))
}
DepReq::Git { git, tag } => {
let res = DirectRes::Git { repo: git, tag };
let pi = PackageId::new(n, res.into());
Ok((pi, Constraint::any()))
}
}
}
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
pub struct Targets {
pub lib: Option<LibTarget>,
#[serde(default = "Vec::new")]
pub bin: Vec<BinTarget>,
#[serde(default = "Vec::new")]
pub test: Vec<TestTarget>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct LibTarget {
#[serde(default = "default_lib_subpath")]
pub path: SubPath,
pub mods: Vec<String>,
#[serde(default)]
pub idris_opts: Vec<String>,
}
fn default_lib_subpath() -> SubPath {
SubPath::from_path(Path::new("src")).unwrap()
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct BinTarget {
pub name: String,
#[serde(default = "default_bin_subpath")]
pub path: SubPath,
pub main: String,
#[serde(default)]
pub idris_opts: Vec<String>,
}
fn default_bin_subpath() -> SubPath {
SubPath::from_path(Path::new("src")).unwrap()
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct TestTarget {
pub name: Option<String>,
#[serde(default = "default_test_subpath")]
pub path: SubPath,
pub main: String,
#[serde(default)]
pub idris_opts: Vec<String>,
}
fn default_test_subpath() -> SubPath {
SubPath::from_path(Path::new("tests")).unwrap()
}
impl From<TestTarget> for BinTarget {
fn from(t: TestTarget) -> Self {
let default_name = format!("test-{}", &t.main)
.trim_end_matches(".idr")
.trim_end_matches(".lidr")
.replace("/", "_")
.replace(".", "_");
BinTarget {
name: t.name.unwrap_or(default_name),
path: t.path,
main: t.main,
idris_opts: t.idris_opts,
}
}
}
impl BinTarget {
pub fn resolve_bin(&self, parent: &Path) -> Option<(PathBuf, PathBuf)> {
let main_path: PathBuf = self.main.clone().into();
if let Ok(s) = SubPath::from_path(&main_path) {
if parent.join(&s.0).with_extension("idr").exists() {
let target_path = if s.0.extension().is_none() {
parent.join(&s.0).with_extension("idr")
} else {
parent.join(&s.0)
};
let src_path = target_path.parent().unwrap();
let target_path: PathBuf = target_path.file_name().unwrap().to_os_string().into();
return Some((src_path.to_path_buf(), target_path));
} else if parent.join(&s.0).with_extension("lidr").exists() {
let target_path = if s.0.extension().is_none() {
parent.join(&s.0).with_extension("lidr")
} else {
parent.join(&s.0)
};
let src_path = target_path.parent().unwrap();
let target_path: PathBuf = target_path.file_name().unwrap().to_os_string().into();
return Some((src_path.to_path_buf(), target_path));
}
}
let src_path = parent.join(&self.path.0);
let mut split = self.main.trim_matches('.').rsplitn(2, '.');
let after = split.next().unwrap();
let (after, before) = if after == "lidr" || after == "idr" {
if let Some(before) = split.next() {
let mut new_split = before.rsplitn(2, '.');
let fpart = new_split.next().unwrap();
(format!("{}.{}", fpart, after), new_split.next())
} else {
(after.to_owned(), None)
}
} else {
(after.to_owned(), split.next())
};
if let Some(before) = before {
let target_path: PathBuf = before.replace(".", "/").into();
if src_path
.join(&target_path)
.join(&after)
.with_extension("idr")
.exists()
{
Some((src_path, target_path.join(after).with_extension("idr")))
} else if src_path
.join(&target_path)
.join(&after)
.with_extension("lidr")
.exists()
{
Some((src_path, target_path.join(after).with_extension("lidr")))
} else if src_path.join(&target_path).with_extension("idr").exists() {
Some((src_path, target_path.with_extension(after)))
} else if src_path.join(&target_path).with_extension("lidr").exists() {
Some((src_path, target_path.with_extension(after)))
} else {
None
}
} else {
let target_path: PathBuf = after.into();
if src_path.join(&target_path).with_extension("idr").exists() {
Some((src_path, target_path.with_extension("idr")))
} else if src_path.join(&target_path).with_extension("lidr").exists() {
Some((src_path, target_path.with_extension("lidr")))
} else {
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manifest_valid() {
let manifest = r#"
[package]
name = 'ring_ding/test'
version = '1.0.0'
authors = ['me']
license = 'MIT'
description = "The best package ever released"
homepage = "https://github.com/elba/elba"
repository = "https://github.com/elba/elba"
readme = "README.md"
keywords = ["package-manager", "packaging"]
exclude = ["*.blah"]
[dependencies]
'awesome/a' = '>= 1.0.0 < 2.0.0'
'cool/b' = { git = 'https://github.com/super/cool', tag = "v1.0.0" }
'great/c' = { path = 'here/right/now' }
[dev_dependencies]
'ayy/x' = '2.0'
[[targets.bin]]
name = 'bin1'
main = 'src/bin/Here'
[targets.lib]
path = "src/lib/"
mods = [
"Control.Monad.Wow",
"Control.Monad.Yeet",
"RingDing.Test"
]
idris_opts = ["--warnpartial", "--warnreach"]
"#;
assert!(Manifest::from_str(manifest).is_ok());
}
#[test]
fn manifest_valid_no_targets() {
let manifest = r#"
[package]
name = 'ring_ding/test'
version = '1.0.0'
authors = ['Me <y@boi.me>']
license = 'MIT'
[dependencies]
'awesome/a' = '>= 1.0.0 < 2.0.0'
'cool/b' = { git = 'https://github.com/super/cool', tag = "v1.0.0" }
'great/c' = { path = 'here/right/now' }
[dev_dependencies]
'ayy/x' = '2.0'
"#;
assert!(Manifest::from_str(manifest).is_ok());
}
#[test]
fn manifest_invalid_target_path() {
let manifest = r#"
[package]
name = 'ring_ding/test'
version = '1.0.0'
description = "a cool package"
authors = ['me']
license = 'MIT'
[dependencies]
'awesome/a' = '>= 1.0.0 < 2.0.0'
'cool/b' = { git = 'https://github.com/super/cool', tag = "v1.0.0" }
'great/c' = { path = 'here/right/now' }
[dev_dependencies]
'ayy/x' = '2.0'
[targets.lib]
path = "../oops"
mods = [
"Right.Here"
]
"#;
assert!(Manifest::from_str(manifest).is_err());
}
}