Skip to main content

cnf_lib/provider/
mod.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// SPDX-FileCopyrightText: (C) 2023 Andreas Hartmann <hartan@7x.de>
3// This file is part of cnf-lib, available at <https://gitlab.com/hartang/rust/cnf>
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 returns
51//!       a successful result for existing applications). Some providers change their output format
52//!       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 from
59//!       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 any
65//!       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 results
75//!       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(crate) use crate::{
109        environment::{Environment, ExecutionError, IsEnvironment},
110        provider::{Actions, Candidate, Error as ProviderError, IsProvider, Provider, Query},
111        util::{CommandLine, OutputMatcher, cmd},
112    };
113
114    pub(crate) use anyhow::{Context, anyhow};
115    pub(crate) use async_trait::async_trait;
116    pub(crate) use displaydoc::Display;
117    pub(crate) use logerr::LoggableError;
118    pub(crate) use thiserror::Error as ThisError;
119    pub(crate) use tracing::{Instrument, debug, error, info, trace, warn};
120
121    pub(crate) use std::{fmt, sync::Arc};
122
123    pub(crate) type ProviderResult<T> = std::result::Result<T, ProviderError>;
124}
125
126/// A command provider.
127#[async_trait]
128#[enum_dispatch]
129pub trait IsProvider: std::fmt::Debug + std::fmt::Display {
130    /// Search for a command with this provider in `env`.
131    async fn search_internal(
132        &self,
133        command: &str,
134        target_env: Arc<crate::environment::Environment>,
135    ) -> ProviderResult<Vec<Candidate>>;
136}
137
138/// Search for `command` inside the given [`IsProvider`], targeting a specific [`Environment`].
139///
140/// Calls the [`IsProvider`]s search function and wraps the result in a more digestible [`Query`]
141/// instance.
142// Record provider and env through display impls because it's much more readable
143#[tracing::instrument(level = "debug", skip(provider, target_env), fields(%target_env, %provider))]
144pub async fn search_in(
145    provider: Arc<Provider>,
146    command: &str,
147    target_env: Arc<Environment>,
148) -> Query {
149    let results =
150        match IsProvider::search_internal(provider.as_ref(), command, target_env.clone()).await {
151            Ok(results) if !results.is_empty() => Ok(results),
152            // Provider didn't error but turned up no results either, so this is a "not found"
153            Ok(_) => Err(ProviderError::NotFound(command.to_string())),
154            Err(error) => Err(error),
155        };
156
157    Query {
158        env: target_env,
159        provider,
160        term: command.to_string(),
161        results,
162    }
163}
164
165/// Wrapper type for everything implementing [`IsProvider`].
166#[enum_dispatch(IsProvider)]
167#[derive(Debug, PartialEq)]
168pub enum Provider {
169    /// `apt`, the default package manager on Ubuntu, Debian and other derivations of those.
170    Apt,
171    /// `cargo`, the package manager for the Rust programming language.
172    Cargo,
173    /// Custom provider for user extensions.
174    Custom,
175    /// Pseudo-provider searching for (non-)executable files in the current working directory.
176    Cwd,
177    /// `dnf`, the default package manager on Fedora, RedHat and other derivations ot those.
178    Dnf,
179    /// `flatpak`, the CLI executable for the flatpak application ecosystem.
180    Flatpak,
181    /// `pacman`, the default package manager for Arch Linux and other derivations of that.
182    Pacman,
183    /// Pseudo-provider searching for packages in the directories specified by `$PATH`.
184    Path,
185}
186
187impl fmt::Display for Provider {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        match self {
190            Self::Apt(val) => write!(f, "{}", val),
191            Self::Cargo(val) => write!(f, "{}", val),
192            Self::Custom(val) => write!(f, "{}", val),
193            Self::Cwd(val) => write!(f, "{}", val),
194            Self::Dnf(val) => write!(f, "{}", val),
195            Self::Flatpak(val) => write!(f, "{}", val),
196            Self::Pacman(val) => write!(f, "{}", val),
197            Self::Path(val) => write!(f, "{}", val),
198        }
199    }
200}
201
202/// A convenient representation of a search query with its' results.
203///
204/// Refer to [`search_in`] to see how to obtain a `Query` conveniently.
205#[derive(Debug)]
206pub struct Query {
207    /// The environment where the query was executed
208    pub env: Arc<crate::environment::Environment>,
209    /// The provider that generated this query result
210    pub provider: Arc<Provider>,
211    /// The term that was searched for
212    pub term: String,
213    /// Result of the query
214    pub results: ProviderResult<Vec<Candidate>>,
215}
216
217impl Query {
218    /// Install result number `index`.
219    ///
220    /// Picks the [`Candidate`] at the given `index` if it exists, and attempts to execute their
221    /// `install` action, if one is provided.
222    pub async fn install(&self, index: usize) -> Result<()> {
223        let err_context = || {
224            format!(
225                "failed to install package '{}' from {} inside {}",
226                self.term, self.provider, self.env,
227            )
228        };
229
230        let results = self
231            .results
232            .as_ref()
233            .or_else(|error| {
234                Err(anyhow::anyhow!("{}", error)).with_context(|| {
235                    format!("provider '{}' doesn't offer results to run", self.provider)
236                })
237            })
238            .with_context(err_context)?;
239        let candidate = results
240            .get(index)
241            .with_context(|| format!("requested candidate {} doesn't exist", index))
242            .with_context(err_context)?;
243
244        if let Some(ref command) = candidate.actions.install {
245            let mut command = command.clone();
246            command.is_interactive(true);
247            self.env
248                .execute(command)
249                .map_err(anyhow::Error::new)
250                .and_then(|mut cmd| cmd.status().map_err(anyhow::Error::new))
251                .await
252                .with_context(err_context)
253                .map_err(CnfError::ApplicationError)?;
254        }
255        Ok(())
256    }
257
258    /// Execute result number `index`.
259    ///
260    /// Picks the [`Candidate`] at the given `index` if it exists, and attempts to execute their
261    /// `execute` action, if one is provided.
262    pub async fn run(&self, index: usize, args: &[&str]) -> Result<ExitStatus> {
263        let err_context = || {
264            format!(
265                "failed to run package '{}' from {} inside {}",
266                self.term, self.provider, self.env,
267            )
268        };
269
270        let results = self
271            .results
272            .as_ref()
273            .or_else(|error| {
274                Err(anyhow::anyhow!("{}", error)).with_context(|| {
275                    format!("provider '{}' doesn't offer results to run", self.provider)
276                })
277            })
278            .with_context(err_context)?;
279        let candidate = results
280            .get(index)
281            .with_context(|| format!("requested candidate {} doesn't exist", index))
282            .with_context(err_context)?;
283
284        let mut command = candidate.actions.execute.clone();
285        command.is_interactive(true);
286        command.append(args);
287        self.env
288            .execute(command)
289            .map_err(anyhow::Error::new)
290            .and_then(|mut cmd| cmd.status().map_err(anyhow::Error::new))
291            .await
292            .with_context(err_context)
293            .map_err(CnfError::ApplicationError)
294    }
295}
296
297/// Potential candidate for a command.
298#[derive(Debug, Default, Deserialize)]
299pub struct Candidate {
300    /// Name of the package/command that provides "command" to be found
301    pub package: String,
302    /// Description of the package/command
303    #[serde(default)]
304    pub description: String,
305    /// Version of the package/command
306    #[serde(default)]
307    pub version: String,
308    /// Origin of the package/command
309    #[serde(default)]
310    pub origin: String,
311    /// Actions available on this candidate
312    pub actions: Actions,
313}
314
315impl fmt::Display for Candidate {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        macro_rules! write_format {
318            ($string:expr, $var:expr) => {
319                writeln!(
320                    f,
321                    "{:>14}: {}",
322                    $string,
323                    if $var.is_empty() { "n/a" } else { $var }
324                )
325            };
326        }
327
328        write_format!("Package", &self.package)?;
329        write_format!("Description", &self.description)?;
330        write_format!("Version", &self.version)?;
331        write_format!("Origin", &self.origin)?;
332        write_format!(
333            "Needs install",
334            if self.actions.install.is_some() {
335                "yes"
336            } else {
337                "no"
338            }
339        )
340    }
341}
342
343/// Action specification for a [Candidate].
344#[derive(Debug, Default, Deserialize)]
345pub struct Actions {
346    /// Action to install a [Candidate]. If this is `None`, the candidate doesn't require
347    /// installing (most likely because it is already installed/available). A `Some(_)` contains
348    /// the command to execute in order to install the [Candidate].
349    #[serde(default)]
350    pub install: Option<CommandLine>,
351    /// Action to execute a [Candidate]. Must contain the command required to execute this
352    /// candidate
353    pub execute: CommandLine,
354}
355
356/// Possible errors arising from provider interactions.
357#[derive(ThisError, Debug)]
358pub enum Error {
359    /// Requested executable couldn't be found.
360    ///
361    /// Return this error if the executable that `cnf` is queried for cannot be found in a
362    /// provider. If the provider has other issues (i.e. a certain package manager isn't
363    /// present), return a [`Requirements`][ProviderError::Requirements] error instead.
364    #[error("command not found: '{0}'")]
365    NotFound(String),
366
367    /// Provider requirements aren't fulfilled (e.g. some binary/package manager is missing).
368    ///
369    /// Please provide a human-readable, actionable description of what the respective provider
370    /// requires.
371    #[error("requirement not fulfilled: '{0}'")]
372    Requirements(String),
373
374    /// Transparent error from any source.
375    #[error(transparent)]
376    ApplicationError(#[from] anyhow::Error),
377
378    /// Required feature not implemented yet.
379    #[error("please implement '{0}' first!")]
380    NotImplemented(String),
381
382    /// Unspecified error during command execution.
383    #[error(transparent)]
384    Execution(ExecutionError),
385}
386
387impl From<CnfError> for ProviderError {
388    fn from(value: CnfError) -> Self {
389        match value {
390            CnfError::ApplicationError(val) => Self::ApplicationError(val),
391            _ => Self::ApplicationError(anyhow::Error::new(value)),
392        }
393    }
394}
395
396impl From<ExecutionError> for ProviderError {
397    fn from(value: ExecutionError) -> Self {
398        match value {
399            ExecutionError::NotFound(val) => ProviderError::Requirements(val),
400            _ => ProviderError::Execution(value),
401        }
402    }
403}