hbox/
runner.rs

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}