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 match self {
117 Self::Toolbx(t) if matcher.ends_with(&format!("Error: command {main_command} not found in container {}", t.name())) => Err(ExecutionError::NotFound(main_command)),
118 Self::Host(h) if (matcher.starts_with(
119 "Portal call failed: Failed to start command: Failed to execute child process"
120 ) && matcher.ends_with("(No such file or directory)")) => Err(ExecutionError::NotFound(main_command)),
121 _ => Err(ExecutionError::NonZero { command: main_command, output }),
122 }
123 }
124 }
125
126 #[cfg(test)]
128 pub async fn output_of(&self, _command: CommandLine) -> Result<String, ExecutionError> {
129 match self {
130 Environment::Mock(mock) => mock.pop_raw(),
131 _ => panic!("cannot execute commands in tests with regular envs"),
132 }
133 }
134
135 pub fn to_json(&self) -> String {
137 serde_json::to_string(&self)
138 .unwrap_or_else(|_| panic!("failed to serialize env '{}' to JSON", self))
139 }
140
141 pub fn start(&self) -> Result<(), anyhow::Error> {
146 match self {
147 Self::Distrobox(val) => Ok(val.start()?),
148 Self::Toolbx(val) => Ok(val.start()?),
149 _ => Ok(()),
150 }
151 }
152}
153
154impl fmt::Display for Environment {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 match self {
157 Self::Host(val) => write!(f, "{}", val),
158 Self::Distrobox(val) => write!(f, "{}", val),
159 Self::Toolbx(val) => write!(f, "{}", val),
160 #[cfg(test)]
161 Self::Mock(val) => write!(f, "{}", val),
162 }
163 }
164}
165
166#[async_trait]
167impl IsEnvironment for Environment {
168 type Err = Error;
169
170 async fn exists(&self) -> bool {
171 match self {
172 Self::Host(val) => val.exists(),
173 Self::Distrobox(val) => val.exists(),
174 Self::Toolbx(val) => val.exists(),
175 #[cfg(test)]
176 Self::Mock(val) => val.exists(),
177 }
178 .await
179 }
180
181 async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err> {
182 async move {
183 match self {
184 Self::Host(val) => val.execute(command).map_err(Error::ExecuteOnHost).await,
185 Self::Distrobox(val) => {
186 val.execute(command)
187 .map_err(|e| Self::Err::ExecuteInDistrobox {
188 distrobox: val.to_string(),
189 source: e,
190 })
191 .await
192 }
193 Self::Toolbx(val) => {
194 val.execute(command)
195 .map_err(|e| Self::Err::ExecuteInToolbx {
196 toolbx: val.to_string(),
197 source: e,
198 })
199 .await
200 }
201 #[cfg(test)]
202 Self::Mock(val) => Ok(val.execute(command).await.unwrap()),
203 }
204 }
205 .in_current_span()
206 .await
207 }
208}
209
210impl From<host::Host> for Environment {
211 fn from(value: host::Host) -> Self {
212 Self::Host(value)
213 }
214}
215
216impl From<distrobox::Distrobox> for Environment {
217 fn from(value: distrobox::Distrobox) -> Self {
218 Self::Distrobox(value)
219 }
220}
221
222impl From<toolbx::Toolbx> for Environment {
223 fn from(value: toolbx::Toolbx) -> Self {
224 Self::Toolbx(value)
225 }
226}
227
228impl std::str::FromStr for Environment {
229 type Err = SerializationError;
230
231 fn from_str(s: &str) -> Result<Self, Self::Err> {
232 let val: Self =
233 serde_json::from_str(s).map_err(|_| SerializationError { raw: s.to_owned() })?;
234 Ok(val)
235 }
236}
237
238#[derive(Debug, ThisError, Serialize, Deserialize, Display)]
240pub struct SerializationError {
241 raw: String,
242}
243
244pub fn current() -> Environment {
246 if toolbx::detect() {
247 Environment::Toolbx(toolbx::Toolbx::current().unwrap())
248 } else if distrobox::detect() {
249 Environment::Distrobox(distrobox::Distrobox::current().unwrap())
250 } else {
251 Environment::Host(host::Host::new())
252 }
253}
254
255pub fn read_env_vars() -> Vec<String> {
261 let exclude = [
262 "HOST",
263 "HOSTNAME",
264 "HOME",
265 "LANG",
266 "LC_CTYPE",
267 "PATH",
268 "PROFILEREAD",
269 "SHELL",
270 ];
271
272 std::env::vars()
273 .filter_map(|(mut key, value)| {
274 if exclude.contains(&&key[..])
275 || key.starts_with('_')
276 || (key.starts_with("XDG_") && key.ends_with("_DIRS"))
277 {
278 None
279 } else {
280 key.push('=');
281 key.push_str(&value);
282 Some(key)
283 }
284 })
285 .collect::<Vec<_>>()
286}
287
288#[derive(Debug, ThisError)]
290pub enum ExecutionError {
291 #[error("command not found: {0}")]
293 NotFound(String),
294
295 #[error(transparent)]
297 Environment(#[from] Error),
298
299 #[error(transparent)]
301 Unknown(#[from] std::io::Error),
302
303 #[error("command '{command}' exited with nonzero code: {code:?}", code = output.status)]
305 NonZero {
306 command: String,
308 output: std::process::Output,
310 },
311}
312
313#[derive(Debug, ThisError)]
315pub enum StartError {
316 #[error(transparent)]
318 Distrobox(#[from] distrobox::StartDistroboxError),
319
320 #[error(transparent)]
322 Toolbx(#[from] toolbx::StartToolbxError),
323}
324
325#[derive(Debug, ThisError, Display)]
327pub enum Error {
328 ExecuteOnHost(#[from] <host::Host as IsEnvironment>::Err),
330
331 ExecuteInToolbx {
333 toolbx: String,
335 source: <toolbx::Toolbx as IsEnvironment>::Err,
337 },
338
339 ExecuteInDistrobox {
341 distrobox: String,
343 source: <distrobox::Distrobox as IsEnvironment>::Err,
345 },
346}