1use crate::configs::context::Context;
2use crate::configs::index::Binary;
3use crate::configs::user::UserConfig;
4use crate::packages::Package;
5use log::{debug, error, info, warn};
6use rand::{distributions::Alphanumeric, thread_rng, Rng};
7use std::io::{stdin, BufRead, BufReader, IsTerminal, Read, Write};
8use std::path::Path;
9use std::process::{Command, Stdio};
10use std::thread;
11
12pub fn build(package: &Package) -> bool {
13 let config = UserConfig::load().unwrap_or_default();
14 let context = Context::from(package);
15 let image_name = context.apply(package.index.image.name.clone());
16 let image = format!("{}:{}", image_name, package.versions.current);
17
18 let mut args = vec!["build".to_string(), "-t".to_string(), image];
19 if let Some(build) = &package.index.image.build {
20 args.push("-f".to_string());
21 args.push(build.dockerfile.clone());
22 if let Some(build_args) = &build.args {
23 for (key, value) in build_args {
24 args.push("--build-arg".to_string());
25 args.push(format!("{}={}", key, context.apply(value.clone())));
26 }
27 }
28 args.push(build.context.clone());
29 }
30
31 run_command_with_args(config.engine.as_str(), &args, None)
32}
33
34pub fn pull(package: &Package) -> bool {
35 let config = UserConfig::load().unwrap_or_default();
36 let image_name = Context::from(package).apply(package.index.image.name.clone());
37 let image = format!("{}:{}", image_name, package.versions.current);
38 run_command_with_args(config.engine.as_str(), &["pull".to_string(), image], None)
39}
40
41pub fn run(package: &Package, binary: Option<String>, params: &Vec<String>) -> bool {
42 let config = UserConfig::load().unwrap_or_default();
43
44 let interactive = !stdin().is_terminal();
45 let mut buffer = Vec::new();
46 if interactive {
47 stdin()
48 .read_to_end(&mut buffer)
49 .expect("Failed to read stdin");
50 }
51
52 let mut args = vec!["run".to_string()];
53 args.push(if interactive {
54 "-i".to_string()
55 } else {
56 "-it".to_string()
57 });
58
59 let binary = get_binary(package, &binary);
60
61 add_default_flags(package, &mut args);
62 add_ports(package, &mut args);
63 add_volumes(package, &mut args);
64 add_current_directory(package, &mut args);
65 add_environment_variables(package, &mut args);
66 add_binary_entrypoint(binary, &mut args);
67 add_container_image(package, &mut args);
68 add_binary_cmd(binary, &mut args);
69
70 if should_wrap_args(binary) {
71 debug!("Wrapping params in quotes");
72 let escaped_params: Vec<String> = params
73 .iter()
74 .map(|param| param.replace("\"", "\\\""))
75 .collect();
76 args.push(escaped_params.join(" "));
77 } else {
78 args.extend(params.iter().cloned());
79 }
80
81 run_command_with_args(config.engine.as_str(), &args, Some(buffer))
82}
83
84fn should_wrap_args(binary: Option<&Binary>) -> bool {
85 binary.map_or(false, |bin| bin.wrap_args)
86}
87
88fn generate_random_name(package: &Package) -> String {
89 let id: String = thread_rng()
90 .sample_iter(&Alphanumeric)
91 .take(10)
92 .map(char::from)
93 .collect();
94 format!("hbox-{}-{}-{}", package.name, package.versions.current, id)
95}
96
97fn add_default_flags(package: &Package, args: &mut Vec<String>) {
98 args.push("--rm".to_string());
99 args.push("--name".to_string());
100 args.push(generate_random_name(package));
101}
102
103fn add_container_image(package: &Package, args: &mut Vec<String>) {
104 let context = Context::from(&package);
105 args.push(format!(
106 "{}:{}",
107 context.apply(package.index.image.name.clone()),
108 package.versions.current
109 ));
110}
111
112fn add_volumes(package: &Package, args: &mut Vec<String>) {
113 if let Some(volumes) = &package.index.volumes {
114 for volume in volumes {
115 let source = shellexpand::full(&volume.source).unwrap();
116 if Path::new(&source.to_string()).exists() {
117 args.push("-v".to_string());
118 args.push(format!("{}:{}", &source, volume.target));
119 } else {
120 warn!("Volume source '{}' not found. Skipping.", source);
121 }
122 }
123 }
124}
125
126fn add_ports(package: &Package, args: &mut Vec<String>) {
127 if let Some(ports) = &package.index.ports {
128 for port in ports {
129 args.push("-p".to_string());
130 args.push(format!("{}:{}", &port.host, port.container));
131 }
132 }
133}
134
135fn add_current_directory(package: &Package, args: &mut Vec<String>) {
136 if let Some(current_directory) = &package.index.current_directory {
137 args.push("-w".to_string());
138 args.push(current_directory.clone());
139 }
140}
141
142fn add_environment_variables(package: &Package, args: &mut Vec<String>) {
143 if let Some(environment_variables) = &package.index.environment_variables {
144 for env_var in environment_variables {
145 let expanded_value = shellexpand::full(&env_var.value).unwrap_or_default();
146 args.push("-e".to_string());
147 args.push(format!("{}={}", env_var.name, expanded_value));
148 }
149 }
150}
151
152fn add_binary_entrypoint(binary: Option<&Binary>, args: &mut Vec<String>) {
153 if let Some(binary) = binary {
154 args.push("--entrypoint".to_string());
155 args.push(binary.path.to_string());
156 }
157}
158
159fn add_binary_cmd(binary: Option<&Binary>, args: &mut Vec<String>) {
160 if let Some(binary) = binary {
161 if let Some(cmd) = &binary.cmd {
162 args.extend(cmd.iter().cloned());
163 }
164 }
165}
166
167fn get_binary<'a>(package: &'a Package, binary: &Option<String>) -> Option<&'a Binary> {
168 binary.as_ref().and_then(|b| {
169 package
170 .index
171 .binaries
172 .as_ref()
173 .and_then(|binaries| binaries.iter().find(|binary| binary.name == *b))
174 })
175}
176
177fn get_stdio(
178 config: &crate::configs::user::Root,
179 stdin_buffer: &Option<Vec<u8>>,
180) -> (Stdio, Stdio, Stdio) {
181 let stdin = if let Some(b) = stdin_buffer {
182 if b.is_empty() {
183 Stdio::inherit()
184 } else {
185 Stdio::piped()
186 }
187 } else {
188 Stdio::inherit()
189 };
190
191 let stdout = if config.experimental.capture_stdout {
192 Stdio::piped()
193 } else {
194 Stdio::inherit()
195 };
196 let stderr = if config.experimental.capture_stderr {
197 Stdio::piped()
198 } else {
199 Stdio::inherit()
200 };
201
202 (stdin, stdout, stderr)
203}
204
205fn run_command_with_args(command: &str, args: &[String], stdin_buffer: Option<Vec<u8>>) -> bool {
206 debug!("Running command: {} {}", command, args.join(" "));
207
208 let config = UserConfig::load().unwrap_or_default();
209 let (stdin, stdout, stderr) = get_stdio(&config, &stdin_buffer);
210
211 let mut child = Command::new(command)
212 .args(args)
213 .stdout(stdout)
214 .stderr(stderr)
215 .stdin(stdin)
216 .spawn()
217 .expect("Failed to spawn command");
218
219 if let Some(buffer) = stdin_buffer {
220 if !buffer.is_empty() {
221 let child_stdin = child.stdin.as_mut().expect("Failed to open stdin");
222 child_stdin
223 .write_all(&buffer)
224 .expect("Failed to write to stdin");
225 }
226 }
227
228 let stdout_thread = spawn_log_thread(
229 child.stdout.take(),
230 |line| info!("{}", line),
231 config.experimental.capture_stdout,
232 );
233 let stderr_thread = spawn_log_thread(
234 child.stderr.take(),
235 |line| error!("{}", line),
236 config.experimental.capture_stderr,
237 );
238
239 let status = child.wait().expect("Failed to wait on child process");
240
241 if let Some(thread) = stdout_thread {
242 let _ = thread.join();
243 }
244
245 if let Some(thread) = stderr_thread {
246 let _ = thread.join();
247 }
248
249 status.success()
250}
251
252fn spawn_log_thread<R: Read + Send + 'static>(
253 reader: Option<R>,
254 log_fn: impl Fn(&str) + Send + 'static,
255 capture: bool,
256) -> Option<thread::JoinHandle<()>> {
257 if !capture {
258 return None;
259 }
260 let reader = reader.expect("Failed to open reader");
261 Some(thread::spawn(move || {
262 let reader = BufReader::new(reader);
263 for line in reader.split(b'\n') {
264 match line {
265 Ok(line) => match std::str::from_utf8(&line) {
266 Ok(line) => log_fn(line),
267 Err(_) => error!(
268 "Failed to read line from output: stream did not contain valid UTF-8"
269 ),
270 },
271 Err(e) => error!("Failed to read line from output: {}", e),
272 }
273 }
274 }))
275}