1pub mod container;
14pub mod distrobox;
15pub mod host;
16pub mod toolbx;
17#[cfg(test)]
18use crate::test::prelude::*;
19use futures::TryFutureExt;
20use prelude::*;
21use tracing::Instrument;
22
23#[allow(unused_imports)]
24pub(crate) mod prelude {
25 pub(crate) use crate::{
26 environment::{self, Environment},
27 util::{CommandLine, OutputMatcher, cmd},
28 };
29 pub(crate) use async_trait::async_trait;
30 pub(crate) use displaydoc::Display;
31 pub(crate) use serde_derive::{Deserialize, Serialize};
32 pub(crate) use std::fmt;
33 pub(crate) use thiserror::Error as ThisError;
34 pub(crate) use tokio::process::Command;
35 pub(crate) use tracing::{debug, error, info, span, trace, warn};
36}
37
38#[async_trait]
40pub trait IsEnvironment: fmt::Debug + fmt::Display {
41 type Err;
43
44 async fn exists(&self) -> bool;
50
51 async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err>;
59}
60
61#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Serialize, Deserialize)]
63pub enum Environment {
64 Host(host::Host),
67 Distrobox(distrobox::Distrobox),
69 Toolbx(toolbx::Toolbx),
71 #[cfg(test)]
73 Mock(Mock),
74}
75
76impl Environment {
77 #[cfg(not(test))]
96 pub async fn output_of(&self, cmd: CommandLine) -> Result<String, ExecutionError> {
97 let main_command = cmd.command();
98 let output = self
99 .execute(cmd)
100 .await
101 .map_err(ExecutionError::Environment)?
102 .stdin(std::process::Stdio::null())
105 .output()
106 .await
107 .map_err(|err| match err.kind() {
108 std::io::ErrorKind::NotFound => ExecutionError::NotFound(main_command.clone()),
109 _ => ExecutionError::Unknown(err),
110 })?;
111
112 if output.status.success() {
113 Ok(String::from_utf8_lossy(&output.stdout).to_string())
114 } else {
115 let matcher = OutputMatcher::new(&output);
116 if matcher.starts_with(
117 &format!(
119 "Error: crun: executable file `{}` not found in $PATH: No such file or directory",
120 main_command.clone()
121 )
122 ) ||
123 matcher.starts_with(
125 "Portal call failed: Failed to start command: Failed to execute child process"
126 ) && matcher.ends_with("(No such file or directory)")
127 {
128 Err(ExecutionError::NotFound(main_command))
129 } else {
130 Err(ExecutionError::NonZero {
131 command: main_command,
132 output,
133 })
134 }
135 }
136 }
137
138 #[cfg(test)]
140 pub async fn output_of(&self, _command: CommandLine) -> Result<String, ExecutionError> {
141 match self {
142 Environment::Mock(mock) => mock.pop_raw(),
143 _ => panic!("cannot execute commands in tests with regular envs"),
144 }
145 }
146
147 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, Display)]
252pub struct SerializationError {
253 raw: String,
254}
255
256pub fn current() -> Environment {
258 if toolbx::detect() {
259 Environment::Toolbx(toolbx::Toolbx::current().unwrap())
260 } else if distrobox::detect() {
261 Environment::Distrobox(distrobox::Distrobox::current().unwrap())
262 } else {
263 Environment::Host(host::Host::new())
264 }
265}
266
267pub fn read_env_vars() -> Vec<String> {
273 let exclude = [
274 "HOST",
275 "HOSTNAME",
276 "HOME",
277 "LANG",
278 "LC_CTYPE",
279 "PATH",
280 "PROFILEREAD",
281 "SHELL",
282 ];
283
284 std::env::vars()
285 .filter_map(|(mut key, value)| {
286 if exclude.contains(&&key[..])
287 || key.starts_with('_')
288 || (key.starts_with("XDG_") && key.ends_with("_DIRS"))
289 {
290 None
291 } else {
292 key.push('=');
293 key.push_str(&value);
294 Some(key)
295 }
296 })
297 .collect::<Vec<_>>()
298}
299
300#[derive(Debug, ThisError)]
302pub enum ExecutionError {
303 #[error("command not found: {0}")]
305 NotFound(String),
306
307 #[error(transparent)]
309 Environment(#[from] Error),
310
311 #[error(transparent)]
313 Unknown(#[from] std::io::Error),
314
315 #[error("command '{command}' exited with nonzero code: {output:?}")]
327 NonZero {
328 command: String,
330 output: std::process::Output,
332 },
333}
334
335#[derive(Debug, ThisError)]
337pub enum StartError {
338 #[error(transparent)]
340 Distrobox(#[from] distrobox::StartDistroboxError),
341
342 #[error(transparent)]
344 Toolbx(#[from] toolbx::StartToolbxError),
345}
346
347#[derive(Debug, ThisError, Display)]
349pub enum Error {
350 ExecuteOnHost(#[from] <host::Host as IsEnvironment>::Err),
352
353 ExecuteInToolbx {
355 toolbx: String,
357 source: <toolbx::Toolbx as IsEnvironment>::Err,
359 },
360
361 ExecuteInDistrobox {
363 distrobox: String,
365 source: <distrobox::Distrobox as IsEnvironment>::Err,
367 },
368}