1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Hooks which can be execute by the native os shell.

use std::io::{BufRead as _, BufReader};
use std::path::Path;
use std::process::{Command, Stdio};

use color_eyre::eyre::Result;
use serde::{Deserialize, Serialize};
use thiserror::Error;

/// An enum of errors which can occur during the execution of a [`Hook`].
#[derive(Error, Debug)]
pub enum HookError {
	/// An [`std::io::Error`] which occurred during the execution of a hook.
	#[error("IO Error")]
	IoError(#[from] std::io::Error),

	/// The hook failed to execute successfully.
	#[error("Process failed with status `{0}`")]
	ExitStatusError(std::process::ExitStatus),
}

impl From<std::process::ExitStatus> for HookError {
	fn from(value: std::process::ExitStatus) -> Self {
		Self::ExitStatusError(value)
	}
}

// TODO: Replace once `exit_ok` becomes stable
/// Maps a value to an Result. This is mainly used as a replacement for
/// [`std::process::ExitStatus::exit_ok`] until it becomes stable.
trait ExitOk {
	/// Error type of the returned result.
	type Error;

	/// Converts `self` to an result.
	fn exit_ok(self) -> Result<(), Self::Error>;
}

impl ExitOk for std::process::ExitStatus {
	type Error = HookError;

	fn exit_ok(self) -> Result<(), <Self as ExitOk>::Error> {
		if self.success() {
			Ok(())
		} else {
			Err(self.into())
		}
	}
}

/// Implements the `Hook` trait, which is used to run a command after or before a build.
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Hook(String);

impl Hook {
	/// Creates a new Hook for the given command. The command must be executable by the native shell.
	pub fn new<S: Into<String>>(command: S) -> Self {
		Self(command.into())
	}

	/// Runs the hook command.
	pub fn command(&self) -> &str {
		&self.0
	}

	/// Executes the hook command.
	pub fn execute(&self, cwd: &Path) -> Result<()> {
		let mut child = self
			.prepare_command()?
			.current_dir(cwd)
			.stdout(Stdio::piped())
			.stderr(Stdio::piped())
			.spawn()?;

		// No need to call kill here as the program will immediately exit
		// and thereby kill all spawned children
		let stdout = child.stdout.take().expect("Failed to get stdout from hook");

		for line in BufReader::new(stdout).lines() {
			match line {
				Ok(line) => log::info!("hook::stdout > {}", line),
				Err(err) => {
					// Result is explicitly ignored as an error was already
					// encountered
					let _ = child.kill();
					return Err(err.into());
				}
			}
		}

		// No need to call kill here as the program will immediately exit
		// and thereby kill all spawned children
		let stderr = child.stderr.take().expect("Failed to get stderr from hook");

		for line in BufReader::new(stderr).lines() {
			match line {
				Ok(line) => log::error!("hook::stderr > {}", line),
				Err(err) => {
					// Result is explicitly ignored as an error was already
					// encountered
					let _ = child.kill();
					return Err(err.into());
				}
			}
		}

		child
			.wait_with_output()?
			.status
			.exit_ok()
			.map_err(Into::into)
	}

	/// Prepares the command for execution depending on the platform.
	fn prepare_command(&self) -> Result<Command> {
		cfg_if::cfg_if! {
			if #[cfg(target_family = "windows")] {
				let mut cmd = Command::new("cmd");
				cmd.args(["/C", &self.0]);
				Ok(cmd)
			} else if #[cfg(target_family = "unix")] {
				let mut cmd = Command::new("sh");
				cmd.args(["-c", &self.0]);
				Ok(cmd)
			} else {
				Err(std::io::Error::new(std::io::ErrorKind::Other, "Hooks are only supported on Windows and Unix-based systems"))
			}
		}
	}
}