use cotton::prelude::*;
use super::Project;
use serde_json::Value;
#[derive(Debug, Clone, Copy)]
pub enum CargoMode {
Silent,
Verbose,
}
#[derive(Debug, Clone, Copy)]
pub enum CargoState {
ScriptDiffers,
NoBinary,
BinaryOutdated,
UpToDate,
}
impl CargoState {
pub fn needs_update(&self) -> bool {
match self {
CargoState::ScriptDiffers => true,
_ => false
}
}
pub fn needs_build(&self) -> bool {
match self {
CargoState::ScriptDiffers => true,
CargoState::NoBinary | CargoState::BinaryOutdated => true,
_ => false
}
}
}
#[derive(Debug)]
pub struct Cargo<'p> {
project: &'p Project,
}
impl<'p> Cargo<'p> {
pub fn new(project: &Project) -> PResult<Cargo> {
if !project.home.join("src").exists() {
info!("Initializing cargo project in {}", project.home.display());
collect_errors(run_result!(
"cargo", "init",
"--quiet",
"--vcs", "none",
"--name", &project.name,
"--bin", &project.home
).problem_while("running cargo init")?)
.problem_while("initializing cargo project")?;
}
Ok(Cargo {
project,
})
}
fn built_executable_path(&self) -> PResult<PathBuf> {
let out = collect_output_and_errors(run_result!(
CurrentDir(&self.project.home),
"cargo", "build", "--message-format=json", "--release"
).problem_while("running cargo build")?
).problem_while("getting build metadata")?;
let mut lines = out.lines().collect::<Vec<_>>();
lines.reverse();
let executable = lines.into_iter()
.map(|line| serde_json::from_str(line))
.or_failed_to("parse cargo JSON message")
.find_map(|line: Value| line.get("executable").and_then(Value::as_str).map(ToOwned::to_owned))
.ok_or_problem("Failed to find executable path in cargo JSON output")?;
Ok(executable.into())
}
fn main_path(&self) -> PathBuf {
self.project.home.join("src").join("main.rs")
}
fn manifest_path(&self) -> PathBuf {
self.project.home.join("Cargo.toml")
}
fn script_content(&self) -> PResult<String> {
Ok(read_to_string(&self.project.script).problem_while("reading script contents")?)
}
fn manifest_content(&self) -> PResult<String> {
let manifest = self.script_content()?
.lines()
.map(|l| l.trim())
.skip_while(|l| *l != "/* Cargo.toml")
.skip(1)
.take_while(|l| *l != "*/")
.join("\n");
if manifest.is_empty() {
Err(Problem::from_error("Cargo.toml manifest not found in the script"))
} else {
Ok(manifest)
}
}
pub fn state(&self) -> PResult<CargoState> {
if hex_digest(Some(self.script_content()?.as_str())) != hex_digest_file(&self.main_path())? {
return Ok(CargoState::ScriptDiffers)
}
let binary_path = self.project.binary_path();
if !binary_path.is_file() {
return Ok(CargoState::NoBinary)
}
if metadata(&binary_path)?.modified()? < metadata(&self.project.script)?.modified()? {
return Ok(CargoState::BinaryOutdated)
}
Ok(CargoState::UpToDate)
}
pub fn update(&self) -> PResult<()> {
info!("Updating project");
write(&self.main_path(), self.script_content()?).problem_while("writing new main.rs file")?;
write(&self.manifest_path(), self.manifest_content()?).problem_while("writing new Cargo.toml file")?;
Ok(())
}
pub fn build(&self, mode: CargoMode) -> PResult<()> {
info!("Building release target");
match mode {
CargoMode::Silent => collect_output_and_errors(
run_result!(CurrentDir(&self.project.home), "cargo", "build", "--release")
.problem_while("running cargo build")?)
.map(drop),
CargoMode::Verbose => check_status(
run_result!(CurrentDir(&self.project.home), "cargo", "build", "--color", "always", "--release")
.problem_while("running cargo build")?),
}
.problem_while("building cargo project")?;
rename(self.built_executable_path()?, self.project.binary_path()).problem_while("moving compiled target final location")?;
Ok(())
}
pub fn ensure_updated(&self) -> PResult<()> {
let state = self.state()?;
if state.needs_update() {
self.update()?;
}
Ok(())
}
pub fn ensure_built(&self, mode: CargoMode) -> PResult<()> {
let state = self.state()?;
debug!("State: {:?}", state);
if state.needs_update() {
self.update()?;
}
if state.needs_build() {
self.build(mode)?;
}
Ok(())
}
pub fn check(&self) -> PResult<()> {
self.update()?;
check_status(run_result!(
CurrentDir(&self.project.home),
"cargo", "check", "--color", "always")
.problem_while("running cargo check")?)
.problem_while("running cargo check")?;
Ok(())
}
pub fn test(&self) -> PResult<()> {
self.update()?;
check_status(run_result!(
CurrentDir(&self.project.home),
"cargo", "test", "--color", "always")
.problem_while("running cargo test")?)
.problem_while("running cargo test")?;
Ok(())
}
}