redo/
target.rs

1use serde::{Deserialize, Serialize};
2use std::io::Result;
3use std::path::PathBuf;
4use std::process::Command;
5use tempfile::*;
6
7pub use crate::dependency::Dependency;
8
9/// Cache file for dependencies hashes for targets (one per directory)
10pub const REDO_DATA: &str = ".redo.json";
11
12/// Target describes `redo` target that supposed to be build
13#[derive(Serialize, Deserialize, Debug, Clone)]
14pub struct Target {
15    #[serde(alias = "target")]
16    pub path: PathBuf,
17    pub dependencies: Vec<Dependency>,
18}
19
20type Targets = Vec<Target>;
21
22impl Target {
23    /// Load target from cache
24    pub fn load(target: &str) -> Target {
25        Target::load_from_redo_cache(target)
26    }
27
28    /// Determines if Target needs to be updated based on dependencies
29    pub fn needs_update(&self) -> bool {
30        let mut at_least_one_iteration = false;
31        for dep in self.dependencies.iter() {
32            at_least_one_iteration = true;
33            if dep.needs_update() {
34                return true;
35            }
36        }
37        !at_least_one_iteration
38    }
39
40    /// Executes `redo` mechanics according to specification
41    ///
42    /// - If file is up to date then inform user and return
43    /// - Otherwise:
44    ///   - Update dependencies and their hashes
45    ///   - Save dependencies to cache file
46    ///   - run associated `.do` file to produce target
47    pub fn redo(mut self) -> Result<()> {
48        if !self.needs_update() {
49            println!("{} is up to date", self.path.to_str().unwrap());
50            return Ok(());
51        }
52
53        for dependency in &mut self.dependencies {
54            dependency.update_hash();
55        }
56
57        let do_file = self.do_file_path();
58        write(&self)?;
59        println!("redo {}", self.path.to_str().unwrap());
60
61        let tmp = NamedTempFile::new()?;
62        let tmp_path = tmp.into_temp_path();
63
64        let status = Command::new("sh")
65            .args([
66                "-e",
67                &to_str(do_file),
68                "",
69                &to_str(PathBuf::from(self.path.file_stem().unwrap())),
70                tmp_path.to_str().unwrap(),
71            ])
72            .status();
73
74        let exit_code = status?.code().unwrap_or(1);
75        if exit_code != 0 {
76            eprintln!(
77                "[REDO ERROR] Do script ended with non-zero exit code: {}",
78                exit_code
79            );
80            std::process::exit(1);
81        }
82
83        std::fs::rename(tmp_path, self.path)
84    }
85
86    fn load_from_redo_cache(target: &str) -> Target {
87        let target_path = std::path::PathBuf::from(target);
88        let mut target = read()
89            .unwrap()
90            .into_iter()
91            .find(|cached| cached.path == target_path)
92            .unwrap_or_else(|| Target {
93                path: PathBuf::from(target),
94                dependencies: vec![],
95            });
96
97        target.ensure_do_dependency_exists();
98        target
99    }
100
101    fn do_file_path(&self) -> PathBuf {
102        self.path.with_extension("do")
103    }
104
105    fn ensure_do_dependency_exists(&mut self) {
106        let do_path = self.do_file_path();
107        if let None = self.dependencies.iter().find(|dep| dep.name == do_path) {
108            self.dependencies.push(Dependency {
109                name: do_path,
110                hash: String::new(),
111            })
112        }
113    }
114}
115
116fn into_io_result<T, E>(result: std::result::Result<T, E>) -> std::io::Result<T>
117where
118    E: Into<Box<dyn std::error::Error + Send + Sync>>,
119{
120    use std::io::*;
121    match result {
122        Ok(value) => Ok(value),
123        Err(error) => Err(Error::new(ErrorKind::Other, error)),
124    }
125}
126
127fn read() -> Result<Targets> {
128    let cache = std::fs::read_to_string(REDO_DATA)?;
129    into_io_result(serde_json::from_str::<Targets>(&cache))
130}
131
132fn write(target: &Target) -> Result<()> {
133    let mut cache = read()?;
134
135    if let Some(entry) = cache.iter_mut().find(|entry| target.path == entry.path) {
136        *entry = target.clone();
137    } else {
138        cache.push(target.clone());
139    }
140
141    let cache = into_io_result(serde_json::to_string_pretty(&cache))?;
142    std::fs::write(REDO_DATA, cache)
143}
144
145fn to_str(path: PathBuf) -> String {
146    path.into_os_string().into_string().unwrap()
147}