1use crate::*;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::io::Write;
5use std::path::PathBuf;
6use std::process::{Command, ExitStatus, Stdio};
7use std::{fmt, fs, io};
8
9#[derive(Debug, Serialize, Deserialize, PartialEq, Copy, Clone)]
10pub enum UpdateSteps {
11 PreInstall,
12 Install,
13 PostInstall,
14}
15
16#[derive(Debug, Serialize, Deserialize)]
18pub struct Updater {
19 pub packagers: BTreeMap<String, Packager>,
20}
21
22#[derive(Debug, Serialize, Deserialize)]
27pub struct Packager {
28 executors: Vec<Executor>,
29 }
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Executor {
35 pub name: String,
36 pre_install: Option<Vec<Cmd>>,
37 install: Cmd,
38 post_install: Option<Vec<Cmd>>,
39 binaries: Option<Vec<String>>, }
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Cmd {
45 exe: String,
46 params: Option<Vec<String>>,
47 current_dir: Option<PathBuf>,
48 env: Option<BTreeMap<String, String>>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ActualCmd {
54 exe: String,
55 params: Vec<String>,
56 current_dir: Option<PathBuf>,
57 env: BTreeMap<String, String>,
58}
59
60impl From<String> for UpdateSteps {
61 fn from(value: String) -> Self {
62 match value.to_lowercase().as_str() {
63 "pre_install" => UpdateSteps::PreInstall,
64 "install" => UpdateSteps::Install,
65 "post_install" => UpdateSteps::PostInstall,
66
67 _ => panic!("Step {} not recognized", value),
68 }
69 }
70}
71
72impl Display for UpdateSteps {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 UpdateSteps::PreInstall => write!(f, "pre_install"),
76 UpdateSteps::Install => write!(f, "install"),
77 UpdateSteps::PostInstall => write!(f, "post_install"),
78 }
79 }
80}
81
82pub fn get_packages_folder(opt: &Opt) -> io::Result<PathBuf> {
83 if let Some(p) = opt.config_folder.clone() {
84 return Ok(p);
85 }
86
87 let config_folder = directories::ProjectDirs::from("net", "ZykiCorp", "System Updater")
88 .ok_or(io::Error::new(
89 io::ErrorKind::NotFound,
90 "System’s configuration folder: for its standard location see https://docs.rs/directories/latest/directories/struct.ProjectDirs.html#method.config_dir",
91 ))?
92 .config_dir()
93 .join("packagers");
94
95 Ok(config_folder)
96}
97
98impl Updater {
99 fn new() -> Updater {
100 Updater {
101 packagers: BTreeMap::default(),
102 }
103 }
104
105 #[doc(hidden)]
107 fn write_config(&self, opt: &Opt) {
108 use std::fs::OpenOptions;
109
110 let config_folder = get_packages_folder(opt).unwrap();
111
112 let mut f = OpenOptions::new()
113 .write(true)
114 .create(true)
115 .truncate(true)
116 .open(config_folder.join("default.yaml"))
117 .unwrap();
118
119 fs::create_dir_all(&config_folder).unwrap();
120
121 f.write_all(serde_yaml::to_string(&self).unwrap().as_bytes())
122 .unwrap();
123 }
124
125 pub fn from_config(opt: &Opt) -> io::Result<Updater> {
127 let mut updater = Updater::new();
128
129 if false {
131 updater
132 .packagers
133 .insert("Test".to_owned(), Packager { executors: vec![] });
134 let sys = updater
135 .packagers
136 .get_mut("Test")
137 .expect("We just created the key");
138
139 sys.executors.push(Executor {
140 name: "Rustup".to_owned(),
141 pre_install: None,
142 install: Cmd {
143 exe: "rustup".to_owned(),
144 params: Some(vec!["self".to_owned(), "update".to_owned()]),
145 current_dir: None,
146 env: None,
147 },
148 post_install: None,
149 binaries: None,
150 });
151
152 sys.executors.push(Executor {
153 name: "Cargo".to_owned(),
154 pre_install: None,
155 install: Cmd {
156 exe: "cargo".to_owned(),
157 params: Some(vec!["install-update".to_owned(), "-a".to_owned()]),
158 current_dir: None,
159 env: None,
160 },
161 post_install: None,
162 binaries: Some(vec!["rustc".to_owned()]),
163 });
164
165 updater.write_config(opt);
166
167 panic!("Wrote a config sample.");
168 }
169
170 let packages_folder = get_packages_folder(opt)?;
171 match packages_folder.try_exists() {
173 Ok(true) => {} Ok(false) => {
175 return Err(io::Error::new(
176 io::ErrorKind::Other,
177 format!(
178 "Configuration folder not accessible at: {}. (broken symlink?)",
179 packages_folder.display()
180 ),
181 ))
182 }
183 Err(e) => return Err(e),
184 }
185
186 for file in packages_folder.read_dir()?.filter(|name| match name {
187 Ok(n) => n.file_name().into_string().unwrap().ends_with(".yaml"),
188 Err(..) => false,
189 }) {
190 let file = file?.path();
191 let sys = std::fs::read_to_string(&file).unwrap();
192 let sys = serde_yaml::from_str(&sys).map_err(|err| {
193 io::Error::new(
194 io::ErrorKind::Other,
195 format!(
196 "Encontered an error while parsing config file {}: {}",
197 file.display(),
198 err
199 ),
200 )
201 })?;
202 updater
203 .packagers
204 .insert(file.file_stem().unwrap().to_str().unwrap().to_owned(), sys);
205 }
206
207 Ok(updater)
210 }
211
212 pub fn update_all(&self, opt: &Opt) -> Summary {
213 let mut status: Vec<_> = vec![];
214
215 for (packager_name, packager) in &self.packagers {
217 let mut record = Record {
219 packager: String::from("No packager"),
220 executor: String::from("No executor"),
221 status: Err(Error::Config {
222 source: which::Error::CannotFindBinaryPath,
223 filename: String::new(),
224 })
225 .into(),
226 };
227
228 assert!(!packager.executors.is_empty());
229
230 for executor in &packager.executors {
231 let mut u = executor.is_usable();
232 let mut is_ok = executor.is_usable().is_ok();
233 if is_ok {
234 u = self.update(executor, opt);
235 is_ok = u.is_ok();
236 }
237
238 record = Record {
239 packager: packager_name.to_string(),
240 executor: executor.name.clone(),
241 status: u.into(),
242 };
243
244 if is_ok {
245 break;
246 }
247 }
248
249 status.push(record);
250 }
251
252 Summary { status }
253 }
254
255 fn update(&self, sys: &Executor, opt: &Opt) -> Result<()> {
256 let steps = &opt.steps;
257 assert!(!steps.is_empty());
258
259 if steps.contains(&UpdateSteps::PreInstall) {
260 sys.pre_install(opt)?;
261 }
262 if steps.contains(&UpdateSteps::Install) {
263 sys.install(opt)?;
264 }
265 if steps.contains(&UpdateSteps::PostInstall) {
266 sys.post_install(opt)?;
267 }
268
269 Ok(())
270 }
271}
272
273fn are_all_binaries_found_string(
274 bins: &[String],
275) -> std::result::Result<(), (which::Error, String)> {
276 let mut outer_b = String::new(); bins.iter()
278 .try_for_each(|b| {
279 outer_b = b.clone();
280 which::which(b)?;
281 Ok::<(), which::Error>(())
282 })
283 .map_err(|s| (s, outer_b))?;
284 Ok(())
285}
286
287fn are_all_binaries_found_cmd(bins: &[Cmd]) -> std::result::Result<(), (which::Error, String)> {
288 let mut outer_b = String::new(); bins.iter()
290 .try_for_each(|b| {
291 outer_b = b.exe.clone();
292 which::which(b.exe.clone())?;
293 Ok::<(), which::Error>(())
294 })
295 .map_err(|s| (s, outer_b))?;
296 Ok(())
297}
298
299impl Executor {
300 pub fn is_usable(&self) -> Result<()> {
302 are_all_binaries_found_cmd(&self.pre_install.clone().unwrap_or_default())
303 .map_err(|(source, filename)| Error::Config { source, filename })?;
304 are_all_binaries_found_cmd(&[self.install.clone()])
305 .map_err(|(source, filename)| Error::Config { source, filename })?;
306
307 are_all_binaries_found_cmd(&self.post_install.clone().unwrap_or_default())
308 .map_err(|(source, filename)| Error::Config { source, filename })?;
309
310 are_all_binaries_found_string(&self.binaries.clone().unwrap_or_default())
311 .map_err(|(source, filename)| Error::Config { source, filename })?;
312
313 Ok(())
314 }
315
316 pub fn pre_install(&self, opt: &Opt) -> Result<()> {
317 if let Some(pre_install) = &self.pre_install {
318 for cmd in pre_install {
319 let cmd = cmd.clone().prepare(opt);
320 let exit_status = cmd.execute(opt).map_err(|err| Error::Execution {
321 source: err,
322 step: UpdateSteps::PreInstall,
323 cmd: cmd.clone(),
324 })?;
325
326 if !exit_status.success() {
327 return Result::Err(Error::Execution {
328 source: io::Error::new(io::ErrorKind::Other, format!("{}", exit_status)),
329 step: UpdateSteps::PreInstall,
330 cmd,
331 });
332 }
333 }
334 }
335 Ok(())
336 }
337
338 pub fn install(&self, opt: &Opt) -> Result<()> {
339 let cmd = self.install.clone().prepare(opt);
340 let exit_status = cmd.execute(opt).map_err(|err| Error::Execution {
341 source: err,
342 step: UpdateSteps::Install,
343 cmd: cmd.clone(),
344 })?;
345
346 if !exit_status.success() {
347 return Err(Error::Execution {
348 source: io::Error::new(io::ErrorKind::Other, format!("{}", exit_status)),
349 step: UpdateSteps::Install,
350 cmd,
351 });
352 }
353 Ok(())
354 }
355
356 pub fn post_install(&self, opt: &Opt) -> Result<()> {
357 if let Some(post_install) = &self.post_install {
358 for cmd in post_install {
359 let cmd = cmd.clone().prepare(opt);
360 let exit_status = cmd.execute(opt).map_err(|err| Error::Execution {
361 source: err,
362 step: UpdateSteps::PostInstall,
363 cmd: cmd.clone(),
364 })?;
365
366 if !exit_status.success() {
367 return Err(Error::Execution {
368 source: io::Error::new(io::ErrorKind::Other, format!("{}", exit_status)),
369 step: UpdateSteps::PostInstall,
370 cmd,
371 });
372 }
373 }
374 }
375 Ok(())
376 }
377}
378
379impl Display for Executor {
380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381 write!(f, "{}", self.name)
382 }
383}
384
385impl Cmd {
386 #[allow(dead_code)] fn new() -> Cmd {
388 Cmd {
389 exe: "".into(),
390 params: None,
391 current_dir: None,
392 env: None,
393 }
394 }
395
396 fn prepare(self, _opt: &Opt) -> ActualCmd {
397 let env = match self.env {
400 Some(env) => env,
401 None => BTreeMap::default(),
402 };
403
404 ActualCmd {
405 exe: self.exe,
406 params: self.params.unwrap_or_default(),
407 current_dir: self.current_dir,
408 env,
409 }
410 }
411}
412
413impl ActualCmd {
414 fn execute(&self, opt: &Opt) -> io::Result<ExitStatus> {
415 let mut cmd = Command::new(&self.exe);
416
417 cmd.args(&self.params).envs(&self.env);
418
419 if let Some(cdir) = &self.current_dir {
420 cmd.current_dir(std::fs::canonicalize(cdir)?);
421 }
422
423 println!();
424 println!("*** Executing: {} ***", self);
425 if opt.quiet {
428 cmd.stdin(Stdio::null())
430 .stdout(Stdio::null())
431 .stderr(Stdio::null());
432 }
433
434 cmd.status()
435 }
436}
437
438impl fmt::Display for ActualCmd {
439 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440 let command = if !self.params.is_empty() {
441 format!("{} {}", &self.exe, &self.params.join(" "))
442 } else {
443 self.exe.clone()
444 };
445 write!(f, "{}", command)?;
446
447 if let Some(cdir) = &self.current_dir {
448 write!(f, " in {:?}", cdir)?;
449 }
450
451 Ok(())
452
453 }
461}