cargo_msrv/context/
mod.rs

1//! The `context` is the resolved configuration for the current run of `cargo-msrv`.
2//!
3//! The context is the synthesized user input (opts).
4//! Where the user input deals with presentation, the context consists of only
5//! the relevant data which is necessary for the functioning of the subcommand.
6//!
7//! Unlike the opts, the context is top down, not bottom up.
8
9use crate::cli::rust_releases_opts::RustReleasesOpts;
10use crate::cli::shared_opts::SharedOpts;
11use crate::cli::toolchain_opts::ToolchainOpts;
12
13use crate::error::{CargoMSRVError, InvalidUtf8Error, IoError, IoErrorSource, PathError};
14use crate::manifest::bare_version::BareVersion;
15use camino::{Utf8Path, Utf8PathBuf};
16use clap::ValueEnum;
17use std::convert::{TryFrom, TryInto};
18use std::path::Path;
19use std::str::FromStr;
20use std::{env, fmt};
21
22pub mod find;
23pub mod list;
24pub mod set;
25pub mod show;
26pub mod verify;
27
28use crate::cli::custom_check_opts::CustomCheckOpts;
29use crate::cli::rust_releases_opts::Edition;
30use crate::cli::{CargoMsrvOpts, SubCommand};
31use crate::log_level::LogLevel;
32use crate::reporter::event::SelectedPackage;
33use crate::rust::default_target::default_target;
34pub use find::FindContext;
35pub use list::ListContext;
36pub use set::SetContext;
37pub use show::ShowContext;
38pub use verify::VerifyContext;
39
40/// A `context` in `cargo-msrv`, is a definitive and flattened set of options,
41/// required for the program (and its selected sub-command) to function.
42///
43/// Where various `[...]Opts` structs are used to present an interface to the user,
44/// these contexts are used to present an interface to the program.
45/// These `[...]Opts` structs commonly have a tree structure, whereas the contexts
46/// are intended to be at most 1 level of indirection deep.
47/// In addition, the `[...]Opts` structs are used to present a CLI interface
48/// using `clap` as an argument parser, but may be just one way to provide user
49/// input. Alternative user interfaces may be provided, such as one which parses
50/// environment variables and another which reads inputs from a configuration
51/// file. If multiple inputs are provided, they should be merged with a specified
52/// precedence. The final, flattened result shall be used as the program's internal
53/// interface, i.e. this `context`.
54///
55/// Using sub-contexts allows us to write `TryFrom` implementations,
56/// for each sub-command, where each only contains the relevant portion of
57/// data.
58#[derive(Debug)]
59pub enum Context {
60    Find(FindContext),
61    List(ListContext),
62    Set(SetContext),
63    Show(ShowContext),
64    Verify(VerifyContext),
65}
66
67impl Context {
68    pub fn reporting_name(&self) -> &'static str {
69        match self {
70            Context::Find(_) => "find",
71            Context::List(_) => "list",
72            Context::Set(_) => "set",
73            Context::Show(_) => "show",
74            Context::Verify(_) => "verify",
75        }
76    }
77
78    pub fn environment_context(&self) -> &EnvironmentContext {
79        match self {
80            Context::Find(ctx) => &ctx.environment,
81            Context::List(ctx) => &ctx.environment,
82            Context::Set(ctx) => &ctx.environment,
83            Context::Show(ctx) => &ctx.environment,
84            Context::Verify(ctx) => &ctx.environment,
85        }
86    }
87
88    /// Returns the inner find context, if it was present.
89    pub fn to_find_context(self) -> Option<FindContext> {
90        if let Self::Find(ctx) = self {
91            Some(ctx)
92        } else {
93            None
94        }
95    }
96
97    /// Returns the inner find context, if it was present.
98    pub fn to_verify_context(self) -> Option<VerifyContext> {
99        if let Self::Verify(ctx) = self {
100            Some(ctx)
101        } else {
102            None
103        }
104    }
105}
106
107impl TryFrom<CargoMsrvOpts> for Context {
108    type Error = CargoMSRVError;
109
110    fn try_from(opts: CargoMsrvOpts) -> Result<Self, Self::Error> {
111        let ctx = match opts.subcommand {
112            SubCommand::Find(_) => Self::Find(FindContext::try_from(opts)?),
113            SubCommand::List(_) => Self::List(ListContext::try_from(opts)?),
114            SubCommand::Set(_) => Self::Set(SetContext::try_from(opts)?),
115            SubCommand::Show => Self::Show(ShowContext::try_from(opts)?),
116            SubCommand::Verify(_) => Self::Verify(VerifyContext::try_from(opts)?),
117        };
118
119        Ok(ctx)
120    }
121}
122
123#[derive(Clone, Debug, Default)]
124pub struct RustReleasesContext {
125    /// The minimum Rust version to consider.
126    pub minimum_rust_version: Option<BareVersion>,
127
128    /// The maximum Rust version to consider (inclusive).
129    pub maximum_rust_version: Option<BareVersion>,
130
131    /// Whether to consider patch releases as separate versions.
132    pub consider_patch_releases: bool,
133
134    /// The release source to use.
135    pub release_source: ReleaseSource,
136}
137
138impl From<RustReleasesOpts> for RustReleasesContext {
139    fn from(opts: RustReleasesOpts) -> Self {
140        Self {
141            minimum_rust_version: opts.min.map(|min| min.as_bare_version()),
142            maximum_rust_version: opts.max,
143            consider_patch_releases: opts.include_all_patch_releases,
144            release_source: opts.release_source,
145        }
146    }
147}
148
149impl RustReleasesContext {
150    // This is necessary because we need to fetch the minimum version possibly from the Cargo.toml
151    // via the edition key; but where that file should be located is only after we have an
152    // EnvironmentContext.
153    pub fn resolve_minimum_version(
154        &self,
155        env: &EnvironmentContext,
156    ) -> Result<Option<BareVersion>, CargoMSRVError> {
157        // Precedence 1: Supplied values take precedence over all else.
158        if let Some(min) = &self.minimum_rust_version {
159            return Ok(Some(min.clone()));
160        }
161
162        // Precedence 2: Read from manifest
163        let manifest = env.manifest();
164        let contents = std::fs::read_to_string(&manifest).map_err(|error| IoError {
165            error,
166            source: IoErrorSource::ReadFile(manifest.clone()),
167        })?;
168
169        let document = contents
170            .parse::<toml_edit::DocumentMut>()
171            .map_err(CargoMSRVError::ParseToml)?;
172
173        if let Some(edition) = document
174            .as_table()
175            .get("package")
176            .and_then(toml_edit::Item::as_table)
177            .and_then(|package_table| package_table.get("edition"))
178            .and_then(toml_edit::Item::as_str)
179        {
180            let edition = edition.parse::<Edition>()?;
181
182            return Ok(Some(edition.as_bare_version()));
183        }
184
185        Ok(None)
186    }
187}
188
189#[derive(Debug)]
190pub struct ToolchainContext {
191    /// The target of the toolchain
192    pub target: &'static str,
193
194    /// Components to be installed for the toolchain
195    pub components: &'static [&'static str],
196}
197
198impl TryFrom<ToolchainOpts> for ToolchainContext {
199    type Error = CargoMSRVError;
200
201    fn try_from(opts: ToolchainOpts) -> Result<Self, Self::Error> {
202        let target = if let Some(target) = opts.target {
203            target
204        } else {
205            default_target()?
206        };
207
208        let target: &'static str = String::leak(target);
209
210        let components: &'static [&'static str] = Vec::leak(
211            opts.component
212                .into_iter()
213                .map(|s| {
214                    let s: &'static str = String::leak(s);
215                    s
216                })
217                .collect(),
218        );
219
220        Ok(Self { target, components })
221    }
222}
223
224#[derive(Debug)]
225pub struct CheckCommandContext {
226    pub cargo_features: Option<Vec<String>>,
227
228    pub cargo_all_features: bool,
229
230    pub cargo_no_default_features: bool,
231
232    /// The custom `Rustup` command to invoke for a toolchain.
233    pub rustup_command: Option<Vec<String>>,
234}
235
236impl From<CustomCheckOpts> for CheckCommandContext {
237    fn from(opts: CustomCheckOpts) -> Self {
238        Self {
239            cargo_features: opts.features,
240            cargo_all_features: opts.all_features,
241            cargo_no_default_features: opts.no_default_features,
242            rustup_command: opts.custom_check_opts,
243        }
244    }
245}
246
247#[derive(Clone, Debug)]
248pub struct EnvironmentContext {
249    // TODO: Some parts assume a Cargo crate, but that's not strictly a requirement
250    //  of cargo-msrv (only rustup is). We should fix this.
251    /// The path to the root of a crate.
252    ///
253    /// Does not include a manifest file like Cargo.toml, so it's easy to append
254    /// a file path like `Cargo.toml` or `Cargo.lock`.
255    pub root_crate_path: Utf8PathBuf,
256
257    /// Resolved workspace
258    pub workspace_packages: WorkspacePackages,
259}
260
261impl<'shared_opts> TryFrom<&'shared_opts SharedOpts> for EnvironmentContext {
262    type Error = CargoMSRVError;
263
264    fn try_from(opts: &'shared_opts SharedOpts) -> Result<Self, Self::Error> {
265        let path = if let Some(path) = opts.path.as_ref() {
266            // Use `--path` if specified. This is the oldest supported option.
267            // This option refers to the root of a crate.
268            Ok(path.clone())
269        } else if let Some(path) = opts.manifest_path.as_ref() {
270            // Use `--manifest-path` if specified. This was added later, and can not be specified
271            // together with `--path`. This option refers to the `Cargo.toml` document
272            // of a crate ("manifest").
273            dunce::canonicalize(path)
274                .map_err(|_| CargoMSRVError::Path(PathError::DoesNotExist(path.to_path_buf())))
275                .and_then(|p| {
276                    p.parent().map(Path::to_path_buf).ok_or_else(|| {
277                        CargoMSRVError::Path(PathError::NoParent(path.to_path_buf()))
278                    })
279                })
280        } else {
281            // Otherwise, fall back to the current directory.
282            env::current_dir().map_err(|error| {
283                CargoMSRVError::Io(IoError {
284                    error,
285                    source: IoErrorSource::CurrentDir,
286                })
287            })
288        }?;
289
290        let root_crate_path: Utf8PathBuf = path.try_into().map_err(|err| {
291            CargoMSRVError::Path(PathError::InvalidUtf8(InvalidUtf8Error::from(err)))
292        })?;
293
294        // Only select packages if this is a Cargo project.
295        // For now, to be pragmatic, we'll take a shortcut and say that it is so,
296        // if the cargo metadata command succeeds. If it doesn't, we'll fall
297        // back to just the default package.
298        let workspace_packages = if let Ok(metadata) = cargo_metadata::MetadataCommand::new()
299            .manifest_path(root_crate_path.join("Cargo.toml"))
300            .exec()
301        {
302            let partition = opts.workspace.partition_packages(&metadata);
303            let selected = partition.0.into_iter().cloned().collect();
304            let excluded = partition.1;
305
306            info!(
307                action = "detect_cargo_workspace_packages",
308                method = "cargo_metadata",
309                success = true,
310                ?selected,
311                ?excluded
312            );
313
314            WorkspacePackages::from_vec(selected)
315        } else {
316            info!(
317                action = "detect_cargo_workspace_packages",
318                method = "cargo_metadata",
319                success = false,
320            );
321
322            WorkspacePackages::default()
323        };
324
325        Ok(Self {
326            root_crate_path,
327            workspace_packages,
328        })
329    }
330}
331
332impl EnvironmentContext {
333    /// Path to the crate root
334    pub fn root(&self) -> &Utf8Path {
335        &self.root_crate_path
336    }
337
338    /// The path to the Cargo manifest
339    pub fn manifest(&self) -> Utf8PathBuf {
340        self.root_crate_path.join("Cargo.toml")
341    }
342
343    /// The path to the Cargo lock file
344    pub fn lock(&self) -> Utf8PathBuf {
345        self.root_crate_path.join("Cargo.lock")
346    }
347}
348
349// ---
350
351#[derive(Clone, Debug, Default)]
352pub struct WorkspacePackages {
353    selected: Option<Vec<cargo_metadata::Package>>,
354}
355
356impl WorkspacePackages {
357    pub fn from_vec(selected: Vec<cargo_metadata::Package>) -> Self {
358        Self {
359            selected: Some(selected),
360        }
361    }
362
363    pub fn selected(&self) -> Option<Vec<SelectedPackage>> {
364        self.selected.as_deref().map(|pks| {
365            pks.iter()
366                .map(|pkg| SelectedPackage {
367                    name: pkg.name.to_string(),
368                    path: pkg.manifest_path.to_path_buf(),
369                })
370                .collect()
371        })
372    }
373
374    /// The default package is used when either:
375    /// 1. No packages were selected (e.g. because we are not in a cargo workspace or do not use cargo)
376    /// 2. No workspace flags like --workspace, --package, --all or --exclude are used
377    ///
378    /// See [clap_cargo::Workspace](https://docs.rs/clap-cargo/latest/clap_cargo/struct.Workspace.html) which is
379    /// currently used for the selection.
380    pub fn use_default_package(&self) -> bool {
381        self.selected_packages().is_empty()
382    }
383
384    /// The slice of selected packages.
385    /// If empty, either no workspace selection flag was used, or cargo_metadata failed,
386    /// for example because it wasn't a cargo workspace.
387    pub fn selected_packages(&self) -> &[cargo_metadata::Package] {
388        self.selected.as_deref().unwrap_or_default()
389    }
390}
391
392#[derive(Clone, Copy, Debug, Default, PartialEq, ValueEnum)]
393pub enum OutputFormat {
394    /// Progress bar rendered to stderr
395    #[default]
396    Human,
397    /// Json status updates printed to stdout
398    Json,
399    /// Minimal output, usually just the result, such as the MSRV or whether verify succeeded or failed
400    Minimal,
401    /// No output -- meant to be used for debugging and testing
402    #[value(skip)]
403    None,
404}
405
406impl fmt::Display for OutputFormat {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        match self {
409            Self::Human => write!(f, "human"),
410            Self::Json => write!(f, "json"),
411            Self::Minimal => write!(f, "minimal"),
412            Self::None => write!(f, "none"),
413        }
414    }
415}
416
417impl FromStr for OutputFormat {
418    type Err = CargoMSRVError;
419
420    fn from_str(s: &str) -> Result<Self, Self::Err> {
421        match s {
422            "human" => Ok(Self::Human),
423            "json" => Ok(Self::Json),
424            "minimal" => Ok(Self::Minimal),
425            unknown => Err(CargoMSRVError::InvalidConfig(format!(
426                "Given output format '{}' is not valid",
427                unknown
428            ))),
429        }
430    }
431}
432
433#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, ValueEnum)]
434#[serde(rename_all = "snake_case")]
435pub enum ReleaseSource {
436    #[default]
437    RustChangelog,
438    #[cfg(feature = "rust-releases-dist-source")]
439    RustDist,
440}
441
442impl FromStr for ReleaseSource {
443    type Err = CargoMSRVError;
444
445    fn from_str(s: &str) -> Result<Self, Self::Err> {
446        s.try_into()
447    }
448}
449
450impl From<ReleaseSource> for &'static str {
451    fn from(value: ReleaseSource) -> Self {
452        match value {
453            ReleaseSource::RustChangelog => "rust-changelog",
454            #[cfg(feature = "rust-releases-dist-source")]
455            ReleaseSource::RustDist => "rust-dist",
456        }
457    }
458}
459
460impl TryFrom<&str> for ReleaseSource {
461    type Error = CargoMSRVError;
462
463    fn try_from(source: &str) -> Result<Self, Self::Error> {
464        match source {
465            "rust-changelog" => Ok(Self::RustChangelog),
466            #[cfg(feature = "rust-releases-dist-source")]
467            "rust-dist" => Ok(Self::RustDist),
468            s => Err(CargoMSRVError::RustReleasesSourceParseError(s.to_string())),
469        }
470    }
471}
472
473impl fmt::Display for ReleaseSource {
474    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475        match self {
476            Self::RustChangelog => write!(f, "rust-changelog"),
477            #[cfg(feature = "rust-releases-dist-source")]
478            Self::RustDist => write!(f, "rust-dist"),
479        }
480    }
481}
482
483#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, serde::Serialize)]
484#[serde(rename_all = "snake_case")]
485pub enum SearchMethod {
486    Linear,
487    #[default]
488    Bisect,
489}
490
491impl From<SearchMethod> for &'static str {
492    fn from(method: SearchMethod) -> Self {
493        match method {
494            SearchMethod::Linear => "linear",
495            SearchMethod::Bisect => "bisect",
496        }
497    }
498}
499
500#[derive(Debug, Clone)]
501pub struct TracingOptions {
502    target: TracingTargetOption,
503    level: LogLevel,
504}
505
506impl TracingOptions {
507    pub fn new(target: TracingTargetOption, level: LogLevel) -> Self {
508        Self { target, level }
509    }
510}
511
512impl Default for TracingOptions {
513    fn default() -> Self {
514        Self {
515            target: TracingTargetOption::File,
516            level: LogLevel::default(),
517        }
518    }
519}
520
521impl TracingOptions {
522    pub fn target(&self) -> &TracingTargetOption {
523        &self.target
524    }
525
526    pub fn level(&self) -> &LogLevel {
527        &self.level
528    }
529}
530
531#[derive(Debug, Copy, Clone, ValueEnum)]
532pub enum TracingTargetOption {
533    File,
534    Stdout,
535}
536
537impl Default for TracingTargetOption {
538    fn default() -> Self {
539        Self::File
540    }
541}
542
543impl TracingTargetOption {
544    pub const FILE: &'static str = "file";
545    pub const STDOUT: &'static str = "stdout";
546}
547
548impl FromStr for TracingTargetOption {
549    type Err = CargoMSRVError;
550
551    fn from_str(s: &str) -> Result<Self, Self::Err> {
552        match s {
553            Self::FILE => Ok(Self::File),
554            Self::STDOUT => Ok(Self::Stdout),
555            unknown => Err(CargoMSRVError::InvalidConfig(format!(
556                "Given log target '{}' is not valid",
557                unknown
558            ))),
559        }
560    }
561}