use std::error::Error;
use std::ffi::OsStr;
use std::fmt::{Display,Formatter};
use std::fs::{read_to_string};
use std::path::{Path,PathBuf};
use std::mem;
pub struct Dependencies {
build: Vec<String>,
run: Vec<String>,
build_pre: Vec<String>,
}
pub struct Recipe {
pub name: String,
pub version: String,
pub revision: String,
pub path: String,
pub category: String,
pub provide: Vec<String>,
pub depend: Dependencies,
}
fn field_from_recipe(path: PathBuf, field: String) -> Result<String, Box<dyn Error>> {
let mut captured_quote_count = 0;
let mut captured: Vec<String> = Vec::new();
for line in read_to_string(&path)?.lines() {
if line.starts_with("#") {
continue;
}
if captured_quote_count > 1 {
return Ok(captured.join("\n"));
}
if line.starts_with(field.as_str()) {
let fields: Vec<&str> = line.split("=").collect();
if fields.len() >= 2 {
for f in fields.iter() {
captured_quote_count += f.matches("\"").count();
}
if captured_quote_count == 0 {
return Ok(fields[1].to_string());
}
let buffer = fields[1].replace("\"", "").replace("\t", "");
if buffer == "" {
continue;
}
captured.push(buffer);
if captured_quote_count > 1 {
return Ok(captured.join("\n"));
}
}
continue
}
if captured_quote_count > 0 {
captured_quote_count += line.matches("\"").count();
let buffer = line.replace("\"", "").replace("\t", "");
if buffer == "" {
continue;
}
captured.push(buffer);
}
}
return Err(From::from(format!("Unknown field {} in recipe file", field)));
}
impl Recipe {
pub fn new() -> Recipe {
Recipe {
name: String::new(),
version: String::new(),
revision: String::new(),
path: String::new(),
category: String::new(),
provide: Vec::new(),
depend: Dependencies {
run: Vec::new(),
build: Vec::new(),
build_pre: Vec::new(),
},
}
}
pub fn new_from_file<P: AsRef<Path>>(path: P, category: String) -> Result<Recipe, Box<dyn Error>> {
let recipe_name_parts: Vec<&str> = path.as_ref().file_stem()
.and_then(OsStr::to_str).unwrap()
.split("-").collect();
if recipe_name_parts.len() != 2 {
return Err(From::from(format!("Unknown recipe name")));
}
let revision = field_from_recipe(path.as_ref().to_path_buf(), "REVISION".to_string())?;
let provide = field_from_recipe(path.as_ref().to_path_buf(), "PROVIDES".to_string())?;
let dep_run = field_from_recipe(path.as_ref().to_path_buf(), "REQUIRES".to_string()).unwrap_or(String::new());
let dep_build = field_from_recipe(path.as_ref().to_path_buf(), "BUILD_REQUIRES".to_string()).unwrap_or(String::new());
let dep_build_pre = field_from_recipe(path.as_ref().to_path_buf(), "BUILD_PREREQUIRES".to_string()).unwrap_or(String::new());
Ok(Recipe {
name: recipe_name_parts[0].to_string(),
version: recipe_name_parts[1].to_string(),
revision: revision,
path: path.as_ref().display().to_string(),
category: category,
provide: provide.split("\n").map(str::to_string).collect(),
depend: Dependencies {
run: dep_run.split("\n").filter(|&x| !x.is_empty()).map(str::to_string).collect(),
build: dep_build.split("\n").filter(|&x| !x.is_empty()).map(str::to_string).collect(),
build_pre: dep_build_pre.split("\n").filter(|&x| !x.is_empty()).map(str::to_string).collect(),
},
})
}
pub fn set_vars(&mut self, key: String, new_value: String) {
let templates = vec![format!("${}", key), format!("${{{}}}", key)];
for template in templates.iter() {
for value in self.provide.iter_mut() {
if !value.contains(template) {
continue
}
let new_string = value.replace(template, &new_value);
let _ = mem::replace(value, new_string);
}
for value in self.depend.run.iter_mut() {
if !value.contains(template) {
continue
}
let new_string = value.replace(template, &new_value);
let _ = mem::replace(value, new_string);
}
for value in self.depend.build.iter_mut() {
if !value.contains(template) {
continue
}
let new_string = value.replace(template, &new_value);
let _ = mem::replace(value, new_string);
}
for value in self.depend.build_pre.iter_mut() {
if !value.contains(template) {
continue
}
let new_string = value.replace(template, &new_value);
let _ = mem::replace(value, new_string);
}
}
}
}
impl Display for Recipe {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "Recipe {} / {}-{}-{}\n", self.category, self.name, self.version, self.revision)?;
if self.provide.len() > 0 {
write!(f, " Specifies {} provides:\n", self.provide.len())?;
for i in self.provide.iter() {
write!(f, " + {}\n", i)?;
}
}
if self.depend.run.len() > 0 {
write!(f, " Specifies {} runtime dependencies:\n", self.depend.run.len())?;
for i in self.depend.run.iter() {
write!(f, " + {}\n", i)?;
}
}
if self.depend.build.len() > 0 {
write!(f, " Specifies {} build dependencies:\n", self.depend.build.len())?;
for i in self.depend.build.iter() {
write!(f, " + {}\n", i)?;
}
}
if self.depend.build_pre.len() > 0 {
write!(f, " Specifies {} pre-build dependencies:\n", self.depend.build_pre.len())?;
for i in self.depend.build_pre.iter() {
write!(f, " + {}\n", i)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
static SAMPLE_RECIPE_A: &'static str = "sample/ports/games-emulation/dosbox/dosbox-0.74.3.recipe";
static SAMPLE_RECIPE_B: &'static str = "sample/ports/media-fonts/anonymous_pro/anonymous_pro-1.002.001.recipe";
#[test]
fn test_recipe_new_from_file() {
let _recipeA = Recipe::new_from_file(SAMPLE_RECIPE_A, "games-emulation".to_string());
let _recipeB = Recipe::new_from_file(SAMPLE_RECIPE_B, "media-fonts".to_string());
}
#[test]
fn test_recipe_simple_field() {
let revision = field_from_recipe(SAMPLE_RECIPE_A.into(), "REVISION".to_string());
assert!(revision.unwrap() == "1", "Incorrect REVISION from sample recipe!");
}
}