1use std::ffi::{OsStr, OsString};
4use std::path::{Path, PathBuf};
5use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, Output, Stdio};
6
7pub use error::OroScriptError;
8use error::{IoContext, Result};
9use oro_common::BuildManifest;
10use regex::Regex;
11
12mod error;
13
14#[derive(Debug)]
15pub struct OroScript<'a> {
16 manifest: Option<&'a BuildManifest>,
17 event: String,
18 package_path: PathBuf,
19 paths: Vec<PathBuf>,
20 cmd: Command,
21 workspace_path: Option<PathBuf>,
22}
23
24impl<'a> OroScript<'a> {
25 pub fn new(package_path: impl AsRef<Path>, event: impl AsRef<str>) -> Result<Self> {
26 let package_path = package_path.as_ref();
27 let package_path = dunce::canonicalize(package_path).io_context(|| format!("Failed to canonicalize package path at {} while preparing to run a package script.", package_path.display()))?;
28 let shell = if cfg!(target_os = "windows") {
29 if let Some(com_spec) = std::env::var_os("ComSpec") {
30 com_spec
31 } else {
32 OsString::from("cmd")
33 }
34 } else {
35 OsString::from("sh")
36 };
37 let shell_str = shell.to_string_lossy();
38 let shell_is_cmd = Regex::new(r"(?:^|\\)cmd(?:\.exe)?$")
39 .unwrap()
40 .is_match(&shell_str);
41 let mut cmd = Command::new(&shell);
42 if shell_is_cmd {
43 cmd.arg("/d");
44 cmd.arg("/s");
45 cmd.arg("/c");
46 } else {
47 cmd.arg("-c");
48 }
49 cmd.current_dir(&package_path);
50 cmd.stdin(Stdio::null());
51 cmd.stdout(Stdio::piped());
52 cmd.stderr(Stdio::piped());
53 Ok(Self {
54 event: event.as_ref().into(),
55 manifest: None,
56 package_path,
57 paths: Self::get_existing_paths(),
58 workspace_path: None,
59 cmd,
60 })
61 }
62
63 pub fn workspace_path(mut self, path: impl AsRef<Path>) -> Self {
66 self.workspace_path = Some(path.as_ref().to_path_buf());
67 self
68 }
69
70 pub fn env(mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self {
72 self.cmd.env(key.as_ref(), value.as_ref());
73 self
74 }
75
76 pub fn stdout(mut self, stdout: impl Into<Stdio>) -> Self {
79 self.cmd.stdout(stdout.into());
80 self
81 }
82
83 pub fn stderr(mut self, stderr: impl Into<Stdio>) -> Self {
86 self.cmd.stderr(stderr.into());
87 self
88 }
89
90 pub fn stdin(mut self, stdin: impl Into<Stdio>) -> Self {
97 self.cmd.stdin(stdin.into());
98 self
99 }
100
101 pub fn output(self) -> Result<Output> {
103 self.set_all_paths()?
104 .set_script()?
105 .cmd
106 .output()
107 .map_err(OroScriptError::ScriptProcessError)
108 .and_then(|out| {
109 if out.status.success() {
110 Ok(out)
111 } else {
112 Err(OroScriptError::ScriptError(
113 out.status,
114 Some(out.stdout),
115 Some(out.stderr),
116 ))
117 }
118 })
119 }
120
121 pub fn spawn(self) -> Result<ScriptChild> {
123 self.set_all_paths()?
124 .set_script()?
125 .cmd
126 .spawn()
127 .map(ScriptChild::new)
128 .map_err(OroScriptError::SpawnError)
129 }
130
131 fn set_script(mut self) -> Result<Self> {
132 let event = &self.event;
133 if let Some(pkg) = self.manifest {
134 let script = pkg
135 .scripts
136 .get(event)
137 .ok_or_else(|| OroScriptError::MissingEvent(event.to_string()))?;
138 tracing::trace!(
139 "Executing script for event '{event}' for package at {}: {script}",
140 self.package_path.display()
141 );
142 #[cfg(windows)]
143 {
144 use std::os::windows::process::CommandExt;
145 self.cmd.raw_arg(script);
146 }
147 #[cfg(not(windows))]
148 self.cmd.arg(script);
149 } else {
150 let package_path = &self.package_path;
151 let json = package_path.join("package.json");
152 let pkg = BuildManifest::from_path(&json).io_context(|| {
153 format!(
154 "Failed to read BuildManifest from path at {} while running package script.",
155 json.display()
156 )
157 })?;
158 let script = pkg
159 .scripts
160 .get(event)
161 .ok_or_else(|| OroScriptError::MissingEvent(event.to_string()))?;
162 tracing::trace!(
163 "Executing script for event '{event}' for package at {}: {script}",
164 self.package_path.display()
165 );
166 #[cfg(windows)]
167 {
168 use std::os::windows::process::CommandExt;
169 self.cmd.raw_arg(script);
170 }
171 #[cfg(not(windows))]
172 self.cmd.arg(script);
173 }
174 Ok(self)
175 }
176
177 fn set_all_paths(mut self) -> Result<Self> {
178 for dir in self.package_path.ancestors() {
179 self.paths
180 .push(dir.join("node_modules").join(".bin").to_path_buf());
181 if let Some(workspace_path) = &self.workspace_path {
182 if dir == workspace_path {
183 break;
184 }
185 }
186 }
187 let paths = format!("{}", std::env::join_paths(&self.paths)?.to_string_lossy());
188 for (var, _) in Self::current_paths() {
189 self = self.env(format!("{}", var.to_string_lossy()), paths.clone());
190 }
191 Ok(self)
192 }
193
194 fn current_paths() -> impl Iterator<Item = (OsString, Vec<PathBuf>)> {
195 std::env::vars_os().filter_map(|(var, val)| {
196 if var.to_string_lossy().to_lowercase() == "path" {
197 Some((var, std::env::split_paths(&val).collect::<Vec<PathBuf>>()))
198 } else {
199 None
200 }
201 })
202 }
203
204 fn get_existing_paths() -> Vec<PathBuf> {
205 Self::current_paths()
206 .map(|(_, paths)| paths)
207 .reduce(|mut a, mut b| {
208 a.append(&mut b);
209 a
210 })
211 .unwrap_or_default()
212 }
213}
214
215pub struct ScriptChild {
217 child: Child,
218 pub stdin: Option<ChildStdin>,
219 pub stdout: Option<ChildStdout>,
220 pub stderr: Option<ChildStderr>,
221}
222
223impl ScriptChild {
224 fn new(mut child: Child) -> Self {
225 Self {
226 stdin: child.stdin.take(),
227 stdout: child.stdout.take(),
228 stderr: child.stderr.take(),
229 child,
230 }
231 }
232
233 pub fn id(&self) -> u32 {
235 self.child.id()
236 }
237
238 pub fn kill(mut self) -> Result<()> {
240 self.child
241 .kill()
242 .map_err(OroScriptError::ScriptProcessError)
243 }
244
245 pub fn wait(mut self) -> Result<()> {
248 self.child
249 .wait()
250 .map_err(OroScriptError::ScriptProcessError)
251 .and_then(|status| {
252 if status.success() {
253 Ok(())
254 } else {
255 Err(OroScriptError::ScriptError(status, None, None))
256 }
257 })
258 }
259}