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
9pub const REDO_DATA: &str = ".redo.json";
11
12#[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 pub fn load(target: &str) -> Target {
25 Target::load_from_redo_cache(target)
26 }
27
28 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 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}