Skip to main content

cargo_msrv/compatibility/
rustup_toolchain_check.rs

1use crate::compatibility::IsCompatible;
2use crate::context::EnvironmentContext;
3use crate::error::{IoError, IoErrorSource, LockfileHandlerError};
4use crate::external_command::cargo_command::CargoCommand;
5use crate::external_command::rustup_command::RustupCommand;
6use crate::lockfile::LockfileHandler;
7use crate::outcome::Incompatible;
8use crate::reporter::event::{CheckMethod, CheckResult, CheckToolchain, Method};
9use crate::rust::Toolchain;
10use crate::rust::setup_toolchain::{SetupRustupToolchain, SetupToolchain};
11use crate::{CargoMSRVError, Compatibility, Reporter, TResult};
12use camino::{Utf8Path, Utf8PathBuf};
13use std::fmt;
14use std::fmt::Formatter;
15
16pub struct RustupToolchainCheck<'reporter, 'env, R: Reporter> {
17    reporter: &'reporter R,
18    settings: Settings<'env>,
19}
20
21impl<'reporter, 'env, R: Reporter> RustupToolchainCheck<'reporter, 'env, R> {
22    pub fn new(
23        reporter: &'reporter R,
24        ignore_lockfile: bool,
25        no_check_feedback: bool,
26        skip_unavailable_toolchains: bool,
27        environment: &'env EnvironmentContext,
28        run_command: RunCommand,
29    ) -> Self {
30        Self {
31            reporter,
32            settings: Settings {
33                ignore_lockfile,
34                no_check_feedback,
35                skip_unavailable_toolchains,
36                environment,
37                check_cmd: run_command,
38            },
39        }
40    }
41}
42
43impl<R: Reporter> IsCompatible for RustupToolchainCheck<'_, '_, R> {
44    fn is_compatible(&self, toolchain: &Toolchain) -> TResult<Compatibility> {
45        let settings = &self.settings;
46
47        self.reporter
48            .run_scoped_event(CheckToolchain::new(toolchain.to_owned()), || {
49                info!(ignore_lockfile_enabled = settings.ignore_lockfile());
50
51                // temporarily move the lockfile if the user opted to ignore it, and it exists
52                let ignore_lockfile = settings.ignore_lockfile();
53                let handle_wrap = create_lockfile_handle(ignore_lockfile, settings.environment)?
54                    .map(|handle| handle.move_lockfile())
55                    .transpose()?;
56
57                // Exit early, while marking this version as incompatible, when `skip_unavailable_toolchains`
58                // is set and the setup failed.
59                match setup_toolchain(self.reporter, toolchain) {
60                    Ok(()) => Ok(()),
61                    Err(err) if settings.skip_unavailable_toolchains() => {
62                        return Ok(Compatibility::Incompatible(Incompatible {
63                            toolchain_spec: toolchain.clone(),
64                            error_message: err.to_string(),
65                        }));
66                    }
67                    Err(err) => Err(err),
68                }?;
69
70                if handle_wrap.is_some() {
71                    remove_lockfile(&settings.lockfile_path())?;
72                }
73
74                let crate_root = settings.crate_root_path();
75                let cmd = &self.settings.check_cmd;
76
77                let outcome = run_check_command_via_rustup(
78                    self.reporter,
79                    toolchain,
80                    crate_root,
81                    cmd.components(),
82                )?;
83
84                // report outcome to UI
85                report_outcome(self.reporter, &outcome, settings.no_check_feedback())?;
86
87                // move the lockfile back - we do this explicitly for clarity
88                if let Some(handle) = handle_wrap {
89                    drop(handle);
90                }
91
92                Ok(outcome)
93            })
94    }
95}
96
97impl<R: Reporter> fmt::Debug for RustupToolchainCheck<'_, '_, R> {
98    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
99        f.write_fmt(format_args!("{:?}", self.settings))
100    }
101}
102
103fn setup_toolchain(reporter: &impl Reporter, toolchain: &Toolchain) -> TResult<()> {
104    let downloader = SetupRustupToolchain::new(reporter);
105    downloader.download(toolchain)?;
106
107    Ok(())
108}
109
110fn run_check_command_via_rustup(
111    reporter: &impl Reporter,
112    toolchain: &Toolchain,
113    dir: &Utf8Path,
114    check: &[String],
115) -> TResult<Compatibility> {
116    let version = format!("{}", toolchain.version());
117    let mut cmd = vec![version.as_str()];
118    cmd.extend(check.iter().map(|s| s.as_str()));
119
120    reporter.report_event(CheckMethod::new(
121        toolchain.to_owned(),
122        Method::rustup_run(&cmd, dir),
123    ))?;
124
125    let rustup_output = RustupCommand::new()
126        .with_args(cmd.iter())
127        .with_dir(dir)
128        .with_stderr()
129        .run()
130        .map_err(|_| CargoMSRVError::UnableToRunCheck {
131            command: cmd[1..].join(" "),
132            cwd: dir.to_path_buf(),
133        })?;
134
135    let status = rustup_output.exit_status();
136
137    if status.success() {
138        Ok(Compatibility::new_success(toolchain.to_owned()))
139    } else {
140        let stderr = rustup_output.stderr();
141        let command = cmd.join(" ");
142
143        info!(
144            ?toolchain,
145            stderr,
146            cmd = command.as_str(),
147            "try_building run failed"
148        );
149
150        Ok(Compatibility::new_failure(
151            toolchain.to_owned(),
152            stderr.to_string(),
153        ))
154    }
155}
156
157fn report_outcome(
158    reporter: &impl Reporter,
159    outcome: &Compatibility,
160    no_error_report: bool,
161) -> TResult<()> {
162    match outcome {
163        Compatibility::Compatible(outcome) => {
164            // report compatibility with this toolchain
165            reporter.report_event(CheckResult::compatible(outcome.toolchain_spec.to_owned()))?
166        }
167        Compatibility::Incompatible(outcome) if no_error_report => {
168            // report incompatibility with this toolchain
169            reporter.report_event(CheckResult::incompatible(
170                outcome.toolchain_spec.to_owned(),
171                None,
172            ))?
173        }
174        Compatibility::Incompatible(outcome) => {
175            // report incompatibility with this toolchain
176            reporter.report_event(CheckResult::incompatible(
177                outcome.toolchain_spec.to_owned(),
178                Some(outcome.error_message.clone()),
179            ))?
180        }
181    };
182
183    Ok(())
184}
185
186/// Creates a lockfile handle, iff the lockfile exists and the user opted
187/// to ignore it.
188fn create_lockfile_handle(
189    ignore_lockfile: bool,
190    env: &EnvironmentContext,
191) -> Result<Option<LockfileHandler>, LockfileHandlerError> {
192    ignore_lockfile
193        .then(|| env.lock())
194        .filter(|lockfile| lockfile.is_file())
195        .map(LockfileHandler::try_new)
196        .transpose()
197}
198
199fn remove_lockfile(lock_file: &Utf8Path) -> TResult<()> {
200    if lock_file.is_file() {
201        std::fs::remove_file(lock_file).map_err(|error| IoError {
202            error,
203            source: IoErrorSource::RemoveFile(lock_file.to_path_buf()),
204        })?;
205    }
206
207    Ok(())
208}
209
210#[derive(Debug)]
211struct Settings<'env> {
212    ignore_lockfile: bool,
213    no_check_feedback: bool,
214    skip_unavailable_toolchains: bool,
215
216    environment: &'env EnvironmentContext,
217    check_cmd: RunCommand,
218}
219
220impl Settings<'_> {
221    pub fn ignore_lockfile(&self) -> bool {
222        self.ignore_lockfile
223    }
224
225    pub fn no_check_feedback(&self) -> bool {
226        self.no_check_feedback
227    }
228
229    pub fn skip_unavailable_toolchains(&self) -> bool {
230        self.skip_unavailable_toolchains
231    }
232
233    pub fn crate_root_path(&self) -> &Utf8Path {
234        self.environment.root()
235    }
236
237    pub fn lockfile_path(&self) -> Utf8PathBuf {
238        self.environment.lock()
239    }
240}
241
242#[derive(Debug)]
243pub struct RunCommand {
244    command: Vec<String>,
245}
246
247impl RunCommand {
248    pub fn from_cargo_command(cargo_command: CargoCommand) -> Self {
249        Self {
250            command: cargo_command.into_args(),
251        }
252    }
253
254    pub fn custom(command: Vec<String>) -> Self {
255        Self { command }
256    }
257
258    pub fn components(&self) -> &[String] {
259        self.command.as_ref()
260    }
261}