1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
// Copyright (C) 2023 Andreas Hartmann <hartan@7x.de>
// GNU General Public License v3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
// SPDX-License-Identifier: GPL-3.0-or-later
//! # Command providers
//!
//! Command providers can be anything from where a command could be installed. These differ from
//! the environments in that a provider merely tells whether a program can be found there or not,
//! but it doesn't provide the ability to immediately run the program. On the other hand, an
//! execution environment is something that allows the active execution of commands.
//!
//! Generally speaking, a provider is any sort of package manager (`cargo`, `pip`, `apt`, `dnf`,
//! ...).
//!
//! Execution environments may have zero or more providers available. Containers usually don't have
//! providers, whereas a host operating system will offer some means of installing packages.
//!
//!
//! ## Implementing a new provider
//!
//! The following instructions relate to writing a new provider in Rust, with the goal of inclusion
//! into the application in mind. Of course you don't have to submit your own provider for
//! inclusion if you don't want to. Similarly, you can write a [`mod@custom`] provider first and
//! then open an issue to discuss long-term reimplementation and inclusion in the application.
//!
//! Here are the mandatory steps you must take to write a new provider:
//!
//! - Create a new module file in the `providers` module (here) and name if after the desired
//! provider.
//! - Next, in the `providers` root module:
//! 1. Include your new module (`pub mod ...`)
//! 2. Import the module into the code (`use ...`)
//! 3. Add your new module to the [`Provider`] struct
//! 4. Pass through the [`fmt::Display`] of your new module for [`Provider`]
//! - In your new provider module:
//! 1. Import the prelude: `use crate::provider::prelude::*;`
//! 2. Create a new type (struct) for your provider with any internal state you need (or none
//! at all)
//! 3. Implement [`fmt::Display`], **and please adhere to the projects naming convention**. For
//! example, `dnf` is usually referred to as DNF.
//! 4. Implement [`IsProvider`] for your new provider
//! 5. (Optional) Write some tests, in case your provider can be tested
//! - In `src/lib.rs`:
//! 1. Add your provider into the `providers` vector
//!
//! Following you will find a set of guidelines to help you in the process of writing a robust
//! provider. It's written as a checklist, so feel free to copy it and check the boxes if you want.
//!
//! - Interacting with the provider:
//! - [ ] Test all the different existent queries you can find (i.e. when your provider
//! returns a successful result for existing applications). Some providers change their
//! output format based on what they find, for example by adding optional fields.
//! - [ ] Test a non-existent search query (Personally I like 'asdwasda' for that purpose) and
//! return such errors as [`ProviderError::NotFound`]
//! - [ ] When pre-conditions for your provider aren't fulfilled (i.e. it needs a specific
//! application/package manager), return a [`ProviderError::Requirements`]
//! - [ ] Prefer offline operation and fall back to online operation only when e.g. package
//! manager caches don't exist. Log an info message in such cases so the user can deduce
//! from the log why searching took longer than expected.
//! - [ ] Prefer parsing results from machine-readable output when possible
//! - Error handling:
//! - [ ] **Never cause a panic** by calling e.g. [`unwrap()`](`Result::unwrap()`): Prefer
//! wrapping errors into [`anyhow::Error`] and returning that as
//! [`ProviderError::ApplicationError`] instead. Panics ruin the TUI output and don't add
//! any value for the end-user.
//! - [ ] If you want to recover from errors, consider writing a concrete error type for your
//! provider
//! - [ ] Check if [`ProviderError`] already has the error variant you need and reuse that
//! before reimplementing your own version of it
//! - Other things:
//! - [ ] A [`Candidate`] must have *at least* its' `package` and `actions.execute` fields
//! populated. Try to populate as many as you can.
//! - [ ] Prefer a precise search and returning only few relevant results over generic searches
//! with many results. Built-in providers currently try to stick with less or equal 10
//! results each.
//! - [ ] Try to check whether any of the results you find are already installed. Most package
//! managers will happily report that e.g. `coreutils` provides the `ls` command, but
//! `coreutils` is likely already installed.
//!
//! Feel free to look at some of the existing providers as a reference.
pub mod apt;
pub mod cargo;
pub mod custom;
pub mod cwd;
pub mod dnf;
pub mod flatpak;
pub mod pacman;
pub mod path;
use crate::error::prelude::*;
use async_std::task;
use enum_dispatch::enum_dispatch;
use prelude::*;
use serde_derive::Deserialize;
use apt::Apt;
use cargo::Cargo;
use custom::Custom;
use cwd::Cwd;
use dnf::Dnf;
use flatpak::Flatpak;
use pacman::Pacman;
use path::Path;
pub(crate) mod prelude {
pub use crate::{
environment::{Environment, ExecutionError, IsEnvironment},
provider::{Actions, Candidate, Error as ProviderError, IsProvider, Provider, Query},
util::{cmd, CommandLine, OutputMatcher},
};
pub use anyhow::{anyhow, Context};
pub use async_trait::async_trait;
pub use logerr::LoggableError;
pub use thiserror::Error as ThisError;
pub use std::{fmt, sync::Arc};
pub type ProviderResult<T> = std::result::Result<T, ProviderError>;
}
/// A command provider.
#[async_trait]
#[enum_dispatch]
pub trait IsProvider: std::fmt::Debug + std::fmt::Display {
/// Search for a command with this provider in `env`.
async fn search_internal(
&self,
command: &str,
target_env: Arc<crate::environment::Environment>,
) -> ProviderResult<Vec<Candidate>>;
}
/// Search for `command` inside the given [`IsProvider`], targeting a specific [`Environment`].
///
/// Calls the [`IsProvider`]s search function and wraps the result in a more digestible [`Query`]
/// instance.
pub async fn search_in(
provider: Arc<Provider>,
command: &str,
target_env: Arc<Environment>,
) -> Query {
let results =
match IsProvider::search_internal(provider.as_ref(), command, target_env.clone()).await {
Ok(results) if !results.is_empty() => Ok(results),
// Provider didn't error but turned up no results either, so this is a "not found"
Ok(_) => Err(ProviderError::NotFound(command.to_string())),
Err(error) => Err(error),
};
Query {
env: target_env,
provider,
term: command.to_string(),
results,
}
}
/// Wrapper type for everything implementing [`IsProvider`].
#[enum_dispatch(IsProvider)]
#[derive(Debug, PartialEq)]
pub enum Provider {
Apt,
Cargo,
Custom,
Cwd,
Dnf,
Flatpak,
Pacman,
Path,
}
impl fmt::Display for Provider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Apt(val) => write!(f, "{}", val),
Self::Cargo(val) => write!(f, "{}", val),
Self::Custom(val) => write!(f, "{}", val),
Self::Cwd(val) => write!(f, "{}", val),
Self::Dnf(val) => write!(f, "{}", val),
Self::Flatpak(val) => write!(f, "{}", val),
Self::Pacman(val) => write!(f, "{}", val),
Self::Path(val) => write!(f, "{}", val),
}
}
}
/// A convenient representation of a search query with its' results.
///
/// Refer to [`search_in`] to see how to obtain a `Query` conveniently.
#[derive(Debug)]
pub struct Query {
/// The environment where the query was executed
pub env: Arc<crate::environment::Environment>,
/// The provider that generated this query result
pub provider: Arc<Provider>,
/// The term that was searched for
pub term: String,
/// Result of the query
pub results: ProviderResult<Vec<Candidate>>,
}
impl Query {
/// Install result number `index`.
///
/// Picks the [`Candidate`] at the given `index` if it exists, and attempts to execute their
/// `install` action, if one is provided.
pub fn install(&self, index: usize) -> Result<()> {
let err_context = || {
format!(
"failed to install package '{}' from {} inside {}",
self.term, self.provider, self.env,
)
};
let results = self
.results
.as_ref()
.or_else(|error| {
Err(anyhow::anyhow!("{}", error)).with_context(|| {
format!("provider '{}' doesn't offer results to run", self.provider)
})
})
.with_context(err_context)?;
let candidate = results
.get(index)
.with_context(|| format!("requested candidate {} doesn't exist", index))
.with_context(err_context)?;
if let Some(ref command) = candidate.actions.install {
|| -> anyhow::Result<()> {
let mut command = command.clone();
// Installation is a process triggered by the user and runs interactively. Give the
// user a change to enter passwords etc.
command.is_interactive(true);
task::block_on(self.env.execute(command)?.status())
.map(|_| ())
.map_err(anyhow::Error::new)
}()
.with_context(err_context)
.map_err(CnfError::ApplicationError)?;
}
Ok(())
}
/// Execute result number `index`.
///
/// Picks the [`Candidate`] at the given `index` if it exists, and attempts to execute their
/// `execute` action, if one is provided.
pub fn run(&self, index: usize, args: &[&str]) -> Result<()> {
let err_context = || {
format!(
"failed to run package '{}' from {} inside {}",
self.term, self.provider, self.env,
)
};
let results = self
.results
.as_ref()
.or_else(|error| {
Err(anyhow::anyhow!("{}", error)).with_context(|| {
format!("provider '{}' doesn't offer results to run", self.provider)
})
})
.with_context(err_context)?;
let candidate = results
.get(index)
.with_context(|| format!("requested candidate {} doesn't exist", index))
.with_context(err_context)?;
let command = &candidate.actions.execute;
|| -> anyhow::Result<()> {
let mut cmd = command.clone();
cmd.append(args);
task::block_on(self.env.execute(cmd)?.status())
.map(|_| ())
.map_err(anyhow::Error::new)
}()
.with_context(err_context)
.map_err(CnfError::ApplicationError)
}
}
/// Potential candidate for a command.
#[derive(Debug, Default, Deserialize)]
pub struct Candidate {
/// Name of the package/command that provides "command" to be found
pub package: String,
/// Description of the package/command
#[serde(default)]
pub description: String,
/// Version of the package/command
#[serde(default)]
pub version: String,
/// Origin of the package/command
#[serde(default)]
pub origin: String,
/// Actions available on this candidate
pub actions: Actions,
}
impl fmt::Display for Candidate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
macro_rules! write_format {
($string:expr, $var:expr) => {
writeln!(
f,
"{:>14}: {}",
$string,
if $var.is_empty() { "n/a" } else { $var }
)
};
}
write_format!("Package", &self.package)?;
write_format!("Description", &self.description)?;
write_format!("Version", &self.version)?;
write_format!("Origin", &self.origin)?;
write_format!(
"Needs install",
if self.actions.install.is_some() {
"yes"
} else {
"no"
}
)
}
}
/// Action specification for a [Candidate].
#[derive(Debug, Default, Deserialize)]
pub struct Actions {
/// Action to install a [Candidate]. If this is `None`, the candidate doesn't require
/// installing (most likely because it is already installed/available). A `Some(_)` contains
/// the command to execute in order to install the [Candidate].
#[serde(default)]
pub install: Option<CommandLine>,
/// Action to execute a [Candidate]. Must contain the command required to execute this
/// candidate
pub execute: CommandLine,
}
#[derive(ThisError, Debug)]
pub enum Error {
/// Requested executable couldn't be found.
///
/// Return this error if the executable that `cnf` is queried for cannot be found in a
/// provider. If the provider has other issues (i.e. a certain package manager isn't
/// present), return a [`Requirements`][ProviderError::Requirements] error instead.
#[error("command not found: '{0}'")]
NotFound(String),
/// Provider requirements aren't fulfilled (e.g. some binary/package manager is missing).
///
/// Please provide a human-readable, actionable description of what the respective provider
/// requires.
#[error("requirement not fulfilled: '{0}'")]
Requirements(String),
/// Transparent error from any source.
#[error(transparent)]
ApplicationError(#[from] anyhow::Error),
/// Required feature not implemented yet.
#[error("please implement '{0}' first!")]
NotImplemented(String),
#[error(transparent)]
Execution(ExecutionError),
}
impl From<CnfError> for ProviderError {
fn from(value: CnfError) -> Self {
match value {
CnfError::NotFound(val) => Self::NotFound(val),
_ => Self::ApplicationError(anyhow::Error::new(value)),
}
}
}
impl From<ExecutionError> for ProviderError {
fn from(value: ExecutionError) -> Self {
match value {
ExecutionError::NotFound(val) => ProviderError::Requirements(val),
_ => ProviderError::Execution(value),
}
}
}