pub mod container;
pub mod distrobox;
pub mod host;
pub mod toolbx;
#[cfg(test)]
use crate::test::prelude::*;
use futures::TryFutureExt;
use prelude::*;
use tracing::Instrument;
#[allow(unused_imports)]
pub(crate) mod prelude {
pub(crate) use crate::{
environment::{self, Environment},
util::{CommandLine, OutputMatcher, cmd},
};
pub(crate) use async_trait::async_trait;
pub(crate) use displaydoc::Display;
pub(crate) use serde_derive::{Deserialize, Serialize};
pub(crate) use std::fmt;
pub(crate) use thiserror::Error as ThisError;
pub(crate) use tokio::process::Command;
pub(crate) use tracing::{debug, error, info, span, trace, warn};
}
#[async_trait]
pub trait IsEnvironment: fmt::Debug + fmt::Display {
type Err;
async fn exists(&self) -> bool;
async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err>;
}
#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Environment {
Host(host::Host),
Distrobox(distrobox::Distrobox),
Toolbx(toolbx::Toolbx),
#[cfg(test)]
Mock(Mock),
}
impl Environment {
#[cfg(not(test))]
pub async fn output_of(&self, cmd: CommandLine) -> Result<String, ExecutionError> {
let main_command = cmd.command();
let output = self
.execute(cmd)
.await
.map_err(ExecutionError::Environment)?
.stdin(std::process::Stdio::null())
.output()
.await
.map_err(|err| match err.kind() {
std::io::ErrorKind::NotFound => ExecutionError::NotFound(main_command.clone()),
_ => ExecutionError::Unknown(err),
})?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
let matcher = OutputMatcher::new(&output);
if matcher.starts_with(
&format!(
"Error: crun: executable file `{}` not found in $PATH: No such file or directory",
main_command.clone()
)
) ||
matcher.starts_with(
"Portal call failed: Failed to start command: Failed to execute child process"
) && matcher.ends_with("(No such file or directory)")
{
Err(ExecutionError::NotFound(main_command))
} else {
Err(ExecutionError::NonZero {
command: main_command,
output,
})
}
}
}
#[cfg(test)]
pub async fn output_of(&self, _command: CommandLine) -> Result<String, ExecutionError> {
match self {
Environment::Mock(mock) => mock.pop_raw(),
_ => panic!("cannot execute commands in tests with regular envs"),
}
}
pub fn to_json(&self) -> String {
serde_json::to_string(&self)
.unwrap_or_else(|_| panic!("failed to serialize env '{}' to JSON", self))
}
pub fn start(&self) -> Result<(), anyhow::Error> {
match self {
Self::Distrobox(val) => Ok(val.start()?),
Self::Toolbx(val) => Ok(val.start()?),
_ => Ok(()),
}
}
}
impl fmt::Display for Environment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Host(val) => write!(f, "{}", val),
Self::Distrobox(val) => write!(f, "{}", val),
Self::Toolbx(val) => write!(f, "{}", val),
#[cfg(test)]
Self::Mock(val) => write!(f, "{}", val),
}
}
}
#[async_trait]
impl IsEnvironment for Environment {
type Err = Error;
async fn exists(&self) -> bool {
match self {
Self::Host(val) => val.exists(),
Self::Distrobox(val) => val.exists(),
Self::Toolbx(val) => val.exists(),
#[cfg(test)]
Self::Mock(val) => val.exists(),
}
.await
}
async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err> {
async move {
match self {
Self::Host(val) => val.execute(command).map_err(Error::ExecuteOnHost).await,
Self::Distrobox(val) => {
val.execute(command)
.map_err(|e| Self::Err::ExecuteInDistrobox {
distrobox: val.to_string(),
source: e,
})
.await
}
Self::Toolbx(val) => {
val.execute(command)
.map_err(|e| Self::Err::ExecuteInToolbx {
toolbx: val.to_string(),
source: e,
})
.await
}
#[cfg(test)]
Self::Mock(val) => Ok(val.execute(command).await.unwrap()),
}
}
.in_current_span()
.await
}
}
impl From<host::Host> for Environment {
fn from(value: host::Host) -> Self {
Self::Host(value)
}
}
impl From<distrobox::Distrobox> for Environment {
fn from(value: distrobox::Distrobox) -> Self {
Self::Distrobox(value)
}
}
impl From<toolbx::Toolbx> for Environment {
fn from(value: toolbx::Toolbx) -> Self {
Self::Toolbx(value)
}
}
impl std::str::FromStr for Environment {
type Err = SerializationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let val: Self =
serde_json::from_str(s).map_err(|_| SerializationError { raw: s.to_owned() })?;
Ok(val)
}
}
#[derive(Debug, ThisError, Serialize, Deserialize, Display)]
pub struct SerializationError {
raw: String,
}
pub fn current() -> Environment {
if toolbx::detect() {
Environment::Toolbx(toolbx::Toolbx::current().unwrap())
} else if distrobox::detect() {
Environment::Distrobox(distrobox::Distrobox::current().unwrap())
} else {
Environment::Host(host::Host::new())
}
}
pub fn read_env_vars() -> Vec<String> {
let exclude = [
"HOST",
"HOSTNAME",
"HOME",
"LANG",
"LC_CTYPE",
"PATH",
"PROFILEREAD",
"SHELL",
];
std::env::vars()
.filter_map(|(mut key, value)| {
if exclude.contains(&&key[..])
|| key.starts_with('_')
|| (key.starts_with("XDG_") && key.ends_with("_DIRS"))
{
None
} else {
key.push('=');
key.push_str(&value);
Some(key)
}
})
.collect::<Vec<_>>()
}
#[derive(Debug, ThisError)]
pub enum ExecutionError {
#[error("command not found: {0}")]
NotFound(String),
#[error(transparent)]
Environment(#[from] Error),
#[error(transparent)]
Unknown(#[from] std::io::Error),
#[error("command '{command}' exited with nonzero code: {output:?}")]
NonZero {
command: String,
output: std::process::Output,
},
}
#[derive(Debug, ThisError)]
pub enum StartError {
#[error(transparent)]
Distrobox(#[from] distrobox::StartDistroboxError),
#[error(transparent)]
Toolbx(#[from] toolbx::StartToolbxError),
}
#[derive(Debug, ThisError, Display)]
pub enum Error {
ExecuteOnHost(#[from] <host::Host as IsEnvironment>::Err),
ExecuteInToolbx {
toolbx: String,
source: <toolbx::Toolbx as IsEnvironment>::Err,
},
ExecuteInDistrobox {
distrobox: String,
source: <distrobox::Distrobox as IsEnvironment>::Err,
},
}