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}