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}