punktf_lib/profile/
hook.rs

1//! Hooks which can be execute by the native os shell.
2
3use std::io::{BufRead as _, BufReader};
4use std::path::Path;
5use std::process::{Command, Stdio};
6
7use color_eyre::eyre::Result;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11/// An enum of errors which can occur during the execution of a [`Hook`].
12#[derive(Error, Debug)]
13pub enum HookError {
14	/// An [`std::io::Error`] which occurred during the execution of a hook.
15	#[error("IO Error")]
16	IoError(#[from] std::io::Error),
17
18	/// The hook failed to execute successfully.
19	#[error("Process failed with status `{0}`")]
20	ExitStatusError(std::process::ExitStatus),
21}
22
23impl From<std::process::ExitStatus> for HookError {
24	fn from(value: std::process::ExitStatus) -> Self {
25		Self::ExitStatusError(value)
26	}
27}
28
29// TODO: Replace once `exit_ok` becomes stable
30/// Maps a value to an Result. This is mainly used as a replacement for
31/// [`std::process::ExitStatus::exit_ok`] until it becomes stable.
32trait ExitOk {
33	/// Error type of the returned result.
34	type Error;
35
36	/// Converts `self` to an result.
37	fn exit_ok(self) -> Result<(), Self::Error>;
38}
39
40impl ExitOk for std::process::ExitStatus {
41	type Error = HookError;
42
43	fn exit_ok(self) -> Result<(), <Self as ExitOk>::Error> {
44		if self.success() {
45			Ok(())
46		} else {
47			Err(self.into())
48		}
49	}
50}
51
52/// Implements the `Hook` trait, which is used to run a command after or before a build.
53#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct Hook(String);
56
57impl Hook {
58	/// Creates a new Hook for the given command. The command must be executable by the native shell.
59	pub fn new<S: Into<String>>(command: S) -> Self {
60		Self(command.into())
61	}
62
63	/// Runs the hook command.
64	pub fn command(&self) -> &str {
65		&self.0
66	}
67
68	/// Executes the hook command.
69	pub fn execute(&self, cwd: &Path) -> Result<()> {
70		let mut child = self
71			.prepare_command()?
72			.current_dir(cwd)
73			.stdout(Stdio::piped())
74			.stderr(Stdio::piped())
75			.spawn()?;
76
77		// No need to call kill here as the program will immediately exit
78		// and thereby kill all spawned children
79		let stdout = child.stdout.take().expect("Failed to get stdout from hook");
80
81		for line in BufReader::new(stdout).lines() {
82			match line {
83				Ok(line) => log::info!("hook::stdout > {}", line),
84				Err(err) => {
85					// Result is explicitly ignored as an error was already
86					// encountered
87					let _ = child.kill();
88					return Err(err.into());
89				}
90			}
91		}
92
93		// No need to call kill here as the program will immediately exit
94		// and thereby kill all spawned children
95		let stderr = child.stderr.take().expect("Failed to get stderr from hook");
96
97		for line in BufReader::new(stderr).lines() {
98			match line {
99				Ok(line) => log::error!("hook::stderr > {}", line),
100				Err(err) => {
101					// Result is explicitly ignored as an error was already
102					// encountered
103					let _ = child.kill();
104					return Err(err.into());
105				}
106			}
107		}
108
109		child
110			.wait_with_output()?
111			.status
112			.exit_ok()
113			.map_err(Into::into)
114	}
115
116	/// Prepares the command for execution depending on the platform.
117	fn prepare_command(&self) -> Result<Command> {
118		cfg_if::cfg_if! {
119			if #[cfg(target_family = "windows")] {
120				let mut cmd = Command::new("cmd");
121				cmd.args(["/C", &self.0]);
122				Ok(cmd)
123			} else if #[cfg(target_family = "unix")] {
124				let mut cmd = Command::new("sh");
125				cmd.args(["-c", &self.0]);
126				Ok(cmd)
127			} else {
128				Err(std::io::Error::new(std::io::ErrorKind::Other, "Hooks are only supported on Windows and Unix-based systems"))
129			}
130		}
131	}
132}