1pub mod container;
18pub mod distrobox;
19pub mod host;
20pub mod toolbx;
21#[cfg(test)]
22use crate::test::prelude::*;
23use futures::TryFutureExt;
24use prelude::*;
25use tracing::Instrument;
26
27#[allow(unused_imports)]
28pub(crate) mod prelude {
29 pub use crate::{
30 environment::{self, Environment},
31 util::{cmd, CommandLine, OutputMatcher},
32 };
33 pub use async_trait::async_trait;
34 pub use serde_derive::{Deserialize, Serialize};
35 pub use std::fmt;
36 pub use thiserror::Error as ThisError;
37 pub use tokio::process::Command;
38 pub use tracing::{debug, error, info, span, trace, warn};
39}
40
41#[async_trait]
43pub trait IsEnvironment: fmt::Debug + fmt::Display {
44 type Err;
45
46 async fn exists(&self) -> bool;
52
53 async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err>;
61}
62
63#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Serialize, Deserialize)]
65pub enum Environment {
66 Host(host::Host),
69 Distrobox(distrobox::Distrobox),
71 Toolbx(toolbx::Toolbx),
73 #[cfg(test)]
75 Mock(Mock),
76}
77
78impl Environment {
79 #[cfg(not(test))]
99 pub async fn output_of(&self, cmd: CommandLine) -> Result<String, ExecutionError> {
100 let main_command = cmd.command();
101 let output = self
102 .execute(cmd)
103 .await
104 .map_err(ExecutionError::Environment)?
105 .stdin(std::process::Stdio::null())
108 .output()
109 .await
110 .map_err(|err| match err.kind() {
111 std::io::ErrorKind::NotFound => ExecutionError::NotFound(main_command.clone()),
112 _ => ExecutionError::Unknown(err),
113 })?;
114
115 if output.status.success() {
116 Ok(String::from_utf8_lossy(&output.stdout).to_string())
117 } else {
118 let matcher = OutputMatcher::new(&output);
119 if matcher.starts_with(
120 &format!(
122 "Error: crun: executable file `{}` not found in $PATH: No such file or directory",
123 main_command.clone()
124 )
125 ) ||
126 matcher.starts_with(
128 "Portal call failed: Failed to start command: Failed to execute child process"
129 ) && matcher.ends_with("(No such file or directory)")
130 {
131 Err(ExecutionError::NotFound(main_command))
132 } else {
133 Err(ExecutionError::NonZero {
134 command: main_command,
135 output,
136 })
137 }
138 }
139 }
140 #[cfg(test)]
141 pub async fn output_of(&self, _command: CommandLine) -> Result<String, ExecutionError> {
142 match self {
143 Environment::Mock(ref mock) => mock.pop_raw(),
144 _ => panic!("cannot execute commands in tests with regular envs"),
145 }
146 }
147
148 pub fn to_json(&self) -> String {
149 serde_json::to_string(&self)
150 .unwrap_or_else(|_| panic!("failed to serialize env '{}' to JSON", self))
151 }
152
153 pub fn start(&self) -> Result<(), anyhow::Error> {
158 match self {
159 Self::Distrobox(val) => Ok(val.start()?),
160 Self::Toolbx(val) => Ok(val.start()?),
161 _ => Ok(()),
162 }
163 }
164}
165
166impl fmt::Display for Environment {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 match self {
169 Self::Host(val) => write!(f, "{}", val),
170 Self::Distrobox(val) => write!(f, "{}", val),
171 Self::Toolbx(val) => write!(f, "{}", val),
172 #[cfg(test)]
173 Self::Mock(val) => write!(f, "{}", val),
174 }
175 }
176}
177
178#[async_trait]
179impl IsEnvironment for Environment {
180 type Err = Error;
181
182 async fn exists(&self) -> bool {
183 match self {
184 Self::Host(val) => val.exists(),
185 Self::Distrobox(val) => val.exists(),
186 Self::Toolbx(val) => val.exists(),
187 #[cfg(test)]
188 Self::Mock(val) => val.exists(),
189 }
190 .await
191 }
192
193 async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err> {
194 async move {
195 match self {
196 Self::Host(val) => val.execute(command).map_err(Error::ExecuteOnHost).await,
197 Self::Distrobox(val) => {
198 val.execute(command)
199 .map_err(|e| Self::Err::ExecuteInDistrobox {
200 distrobox: val.to_string(),
201 source: e,
202 })
203 .await
204 }
205 Self::Toolbx(val) => {
206 val.execute(command)
207 .map_err(|e| Self::Err::ExecuteInToolbx {
208 toolbx: val.to_string(),
209 source: e,
210 })
211 .await
212 }
213 #[cfg(test)]
214 Self::Mock(val) => Ok(val.execute(command).await.unwrap()),
215 }
216 }
217 .in_current_span()
218 .await
219 }
220}
221
222impl From<host::Host> for Environment {
223 fn from(value: host::Host) -> Self {
224 Self::Host(value)
225 }
226}
227
228impl From<distrobox::Distrobox> for Environment {
229 fn from(value: distrobox::Distrobox) -> Self {
230 Self::Distrobox(value)
231 }
232}
233
234impl From<toolbx::Toolbx> for Environment {
235 fn from(value: toolbx::Toolbx) -> Self {
236 Self::Toolbx(value)
237 }
238}
239
240impl std::str::FromStr for Environment {
241 type Err = SerializationError;
242
243 fn from_str(s: &str) -> Result<Self, Self::Err> {
244 let val: Self =
245 serde_json::from_str(s).map_err(|_| SerializationError { raw: s.to_owned() })?;
246 Ok(val)
247 }
248}
249
250#[derive(Debug, ThisError, Serialize, Deserialize)]
252#[error("invalid environment specification '{raw}'")]
253pub struct SerializationError {
254 raw: String,
255}
256
257pub fn current() -> Environment {
263 if toolbx::detect() {
264 Environment::Toolbx(toolbx::Toolbx::current().unwrap())
265 } else if distrobox::detect() {
266 Environment::Distrobox(distrobox::Distrobox::current().unwrap())
267 } else {
268 Environment::Host(host::Host::new())
269 }
270}
271
272pub fn read_env_vars() -> Vec<String> {
278 let exclude = [
279 "HOST",
280 "HOSTNAME",
281 "HOME",
282 "LANG",
283 "LC_CTYPE",
284 "PATH",
285 "PROFILEREAD",
286 "SHELL",
287 ];
288
289 std::env::vars()
290 .filter_map(|(mut key, value)| {
291 if exclude.contains(&&key[..])
292 || key.starts_with('_')
293 || (key.starts_with("XDG_") && key.ends_with("_DIRS"))
294 {
295 None
296 } else {
297 key.push('=');
298 key.push_str(&value);
299 Some(key)
300 }
301 })
302 .collect::<Vec<_>>()
303}
304
305#[derive(Debug, ThisError)]
309pub enum ExecutionError {
310 #[error("command not found: {0}")]
312 NotFound(String),
313
314 #[error(transparent)]
316 Environment(#[from] Error),
317
318 #[error(transparent)]
320 Unknown(#[from] std::io::Error),
321
322 #[error("command '{command}' exited with nonzero code")]
334 NonZero {
335 command: String,
336 output: std::process::Output,
337 },
338}
339
340#[derive(Debug, ThisError)]
341pub enum StartError {
342 #[error(transparent)]
343 Distrobox(#[from] distrobox::StartDistroboxError),
344
345 #[error(transparent)]
346 Toolbx(#[from] toolbx::StartToolbxError),
347}
348
349#[derive(Debug, ThisError)]
350pub enum Error {
351 #[error("failed to execute command on host")]
352 ExecuteOnHost(#[from] <host::Host as IsEnvironment>::Err),
353
354 #[error("failed to execute command in '{toolbx}'")]
355 ExecuteInToolbx {
356 toolbx: String,
357 source: <toolbx::Toolbx as IsEnvironment>::Err,
358 },
359
360 #[error("failed to execute command in '{distrobox}'")]
361 ExecuteInDistrobox {
362 distrobox: String,
363 source: <distrobox::Distrobox as IsEnvironment>::Err,
364 },
365}