cnf_lib/provider/
mod.rs

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