Skip to main content

cargo/compiler/
future_incompat.rs

1//! Support for [future-incompatible warning reporting][1].
2//!
3//! Here is an overview of how Cargo handles future-incompatible reports.
4//!
5//! ## Receive reports from the compiler
6//!
7//! When receiving a compiler message during a build, if it is effectively
8//! a [`FutureIncompatReport`], Cargo gathers and forwards it as a
9//! `Message::FutureIncompatReport` to the main thread.
10//!
11//! To have the correct layout of structures for deserializing a report
12//! emitted by the compiler, most of structure definitions, for example
13//! [`FutureIncompatReport`], are copied either partially or entirely from
14//! [compiler/rustc_errors/src/json.rs][2] in rust-lang/rust repository.
15//!
16//! ## Persist reports on disk
17//!
18//! When a build comes to an end, by calling [`save_and_display_report`]
19//! Cargo saves the report on disk, and displays it directly if requested
20//! via command line or configuration. The information of the on-disk file can
21//! be found in [`FUTURE_INCOMPAT_FILE`].
22//!
23//! During the persistent process, Cargo will attempt to query the source of
24//! each package emitting the report, for the sake of providing an upgrade
25//! information as a solution to fix the incompatibility.
26//!
27//! ## Display reports to users
28//!
29//! Users can run `cargo report future-incompat` to retrieve a report. This is
30//! done by [`OnDiskReports::load`]. Cargo simply prints reports to the
31//! standard output.
32//!
33//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/future-incompat-report.html
34//! [2]: https://github.com/rust-lang/rust/blob/9bb6e60d1f1360234aae90c97964c0fa5524f141/compiler/rustc_errors/src/json.rs#L312-L315
35
36use crate::compiler::BuildContext;
37use crate::sources::IndexSummary;
38use crate::sources::SourceConfigMap;
39use crate::sources::source::QueryKind;
40use crate::util::CargoResult;
41use crate::util::cache_lock::CacheLockMode;
42use crate::util::data_structures::{HashMap, HashSet};
43use crate::workspace::{Dependency, PackageId, Workspace};
44use anyhow::{Context, bail, format_err};
45use futures::stream::FuturesUnordered;
46use serde::{Deserialize, Serialize};
47use std::collections::{BTreeMap, BTreeSet};
48use std::fmt::Write as _;
49use std::io::{Read, Write};
50
51pub const REPORT_PREAMBLE: &str = "\
52The following warnings were discovered during the build. These warnings are an
53indication that the packages contain code that will become an error in a
54future release of Rust. These warnings typically cover changes to close
55soundness problems, unintended or undocumented behavior, or critical problems
56that cannot be fixed in a backwards-compatible fashion, and are not expected
57to be in wide use.
58
59Each warning should contain a link for more information on what the warning
60means and how to resolve it.
61";
62
63/// Current version of the on-disk format.
64const ON_DISK_VERSION: u32 = 0;
65
66/// The future incompatibility report, emitted by the compiler as a JSON message.
67#[derive(serde::Deserialize)]
68pub struct FutureIncompatReport {
69    pub future_incompat_report: Vec<FutureBreakageItem>,
70}
71
72/// Structure used for collecting reports in-memory.
73pub struct FutureIncompatReportPackage {
74    pub package_id: PackageId,
75    /// Whether or not this is a local package, or a remote dependency.
76    pub is_local: bool,
77    pub items: Vec<FutureBreakageItem>,
78}
79
80/// A single future-incompatible warning emitted by rustc.
81#[derive(Serialize, Deserialize)]
82pub struct FutureBreakageItem {
83    /// The date at which this lint will become an error.
84    /// Currently unused
85    pub future_breakage_date: Option<String>,
86    /// The original diagnostic emitted by the compiler
87    pub diagnostic: Diagnostic,
88}
89
90/// A diagnostic emitted by the compiler as a JSON message.
91/// We only care about the 'rendered' field
92#[derive(Serialize, Deserialize)]
93pub struct Diagnostic {
94    pub rendered: String,
95    pub level: String,
96}
97
98/// The filename in the top-level `build-dir` directory where we store
99/// the report
100const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
101/// Max number of reports to save on disk.
102const MAX_REPORTS: usize = 5;
103
104/// The structure saved to disk containing the reports.
105#[derive(Serialize, Deserialize)]
106pub struct OnDiskReports {
107    /// A schema version number, to handle older cargo's from trying to read
108    /// something that they don't understand.
109    version: u32,
110    /// The report ID to use for the next report to save.
111    next_id: u32,
112    /// Available reports.
113    reports: Vec<OnDiskReport>,
114}
115
116/// A single report for a given compilation session.
117#[derive(Serialize, Deserialize)]
118struct OnDiskReport {
119    /// Unique reference to the report for the `--id` CLI flag.
120    id: u32,
121    /// A message describing suggestions for fixing the
122    /// reported issues
123    suggestion_message: String,
124    /// Report, suitable for printing to the console.
125    /// Maps package names to the corresponding report
126    /// We use a `BTreeMap` so that the iteration order
127    /// is stable across multiple runs of `cargo`
128    per_package: BTreeMap<String, String>,
129}
130
131impl Default for OnDiskReports {
132    fn default() -> OnDiskReports {
133        OnDiskReports {
134            version: ON_DISK_VERSION,
135            next_id: 1,
136            reports: Vec::new(),
137        }
138    }
139}
140
141impl OnDiskReports {
142    /// Saves a new report returning its id
143    pub fn save_report(
144        mut self,
145        ws: &Workspace<'_>,
146        suggestion_message: String,
147        per_package: BTreeMap<String, String>,
148    ) -> u32 {
149        if let Some(existing_id) = self.has_report(&per_package) {
150            return existing_id;
151        }
152
153        let report = OnDiskReport {
154            id: self.next_id,
155            suggestion_message,
156            per_package,
157        };
158
159        let saved_id = report.id;
160        self.next_id += 1;
161        self.reports.push(report);
162        if self.reports.len() > MAX_REPORTS {
163            self.reports.remove(0);
164        }
165        let on_disk = serde_json::to_vec(&self).unwrap();
166        if let Err(e) = ws
167            .build_dir()
168            .open_rw_exclusive_create(
169                FUTURE_INCOMPAT_FILE,
170                ws.gctx(),
171                "Future incompatibility report",
172            )
173            .and_then(|file| {
174                let mut file = file.file();
175                file.set_len(0)?;
176                file.write_all(&on_disk)?;
177                Ok(())
178            })
179        {
180            crate::display_warning_with_error(
181                "failed to write on-disk future incompatible report",
182                &e,
183                &mut ws.gctx().shell(),
184            );
185        }
186
187        saved_id
188    }
189
190    /// Returns the ID of a report if it is already on disk.
191    fn has_report(&self, rendered_per_package: &BTreeMap<String, String>) -> Option<u32> {
192        self.reports
193            .iter()
194            .find(|existing| &existing.per_package == rendered_per_package)
195            .map(|report| report.id)
196    }
197
198    /// Loads the on-disk reports.
199    pub fn load(ws: &Workspace<'_>) -> CargoResult<OnDiskReports> {
200        let report_file = match ws.build_dir().open_ro_shared(
201            FUTURE_INCOMPAT_FILE,
202            ws.gctx(),
203            "Future incompatible report",
204        ) {
205            Ok(r) => r,
206            Err(e) => {
207                if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
208                    if io_err.kind() == std::io::ErrorKind::NotFound {
209                        bail!("no reports are currently available");
210                    }
211                }
212                return Err(e);
213            }
214        };
215
216        let mut file_contents = String::new();
217        report_file
218            .file()
219            .read_to_string(&mut file_contents)
220            .context("failed to read report")?;
221        let on_disk_reports: OnDiskReports =
222            serde_json::from_str(&file_contents).context("failed to load report")?;
223        if on_disk_reports.version != ON_DISK_VERSION {
224            bail!("unable to read reports; reports were saved from a future version of Cargo");
225        }
226        Ok(on_disk_reports)
227    }
228
229    /// Returns the most recent report ID.
230    pub fn last_id(&self) -> u32 {
231        self.reports.last().map(|r| r.id).unwrap()
232    }
233
234    /// Returns an ANSI-styled report
235    pub fn get_report(&self, id: u32, package: Option<&str>) -> CargoResult<String> {
236        let report = self.reports.iter().find(|r| r.id == id).ok_or_else(|| {
237            let available = itertools::join(self.reports.iter().map(|r| r.id), ", ");
238            format_err!(
239                "could not find report with ID {}\n\
240                 Available IDs are: {}",
241                id,
242                available
243            )
244        })?;
245
246        let mut to_display = report.suggestion_message.clone();
247        to_display += "\n";
248
249        let package_report = if let Some(package) = package {
250            report
251                .per_package
252                .get(package)
253                .ok_or_else(|| {
254                    format_err!(
255                        "could not find package with ID `{}`\n
256                Available packages are: {}\n
257                Omit the `--package` flag to display a report for all packages",
258                        package,
259                        itertools::join(report.per_package.keys(), ", ")
260                    )
261                })?
262                .to_string()
263        } else {
264            report
265                .per_package
266                .values()
267                .cloned()
268                .collect::<Vec<_>>()
269                .join("\n")
270        };
271        to_display += &package_report;
272
273        Ok(to_display)
274    }
275}
276
277fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> BTreeMap<String, String> {
278    let mut report: BTreeMap<String, String> = BTreeMap::new();
279    for per_package in per_package_reports {
280        let package_spec = format!(
281            "{}@{}",
282            per_package.package_id.name(),
283            per_package.package_id.version()
284        );
285        let rendered = report.entry(package_spec).or_default();
286        rendered.push_str(&format!(
287            "The package `{}` currently triggers the following future incompatibility lints:\n",
288            per_package.package_id
289        ));
290        for item in &per_package.items {
291            rendered.extend(
292                item.diagnostic
293                    .rendered
294                    .lines()
295                    .map(|l| format!("> {}\n", l)),
296            );
297        }
298    }
299    report
300}
301
302/// Returns a user-readable message explaining which of
303/// the packages in `package_ids` have updates available.
304/// This is best-effort - if an error occurs, `None` will be returned.
305fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> Option<String> {
306    // This in general ignores all errors since this is opportunistic.
307    let _lock = ws
308        .gctx()
309        .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)
310        .ok()?;
311    // Create a set of updated registry sources.
312    let map = SourceConfigMap::new(ws.gctx()).ok()?;
313    let package_ids: BTreeSet<_> = package_ids
314        .iter()
315        .filter(|pkg_id| pkg_id.source_id().is_registry())
316        .collect();
317    let source_ids: HashSet<_> = package_ids
318        .iter()
319        .map(|pkg_id| pkg_id.source_id())
320        .collect();
321    let sources: HashMap<_, _> = source_ids
322        .into_iter()
323        .filter_map(|sid| {
324            let source = map.load(sid).ok()?;
325            Some((sid, source))
326        })
327        .collect();
328
329    // Query the sources for new versions, mapping `package_ids` into `summaries`.
330    let pending = FuturesUnordered::new();
331    for pkg_id in package_ids {
332        if let Some(source) = sources.get(&pkg_id.source_id())
333            && let Ok(dep) = Dependency::parse(pkg_id.name(), None, pkg_id.source_id())
334        {
335            pending.push(async move {
336                let sum = source.query_vec(&dep, QueryKind::Exact).await.ok()?;
337                Some((pkg_id, sum))
338            });
339        }
340    }
341    let summaries = crate::util::block_on_stream(pending).flatten();
342
343    let mut updates = String::new();
344    for (pkg_id, summaries) in summaries {
345        let mut updated_versions: Vec<_> = summaries
346            .iter()
347            .filter_map(|s| match s {
348                IndexSummary::Candidate(s) => Some(s.version()),
349                _ => None,
350            })
351            .filter(|version| *version > pkg_id.version())
352            .collect();
353        updated_versions.sort();
354
355        if !updated_versions.is_empty() {
356            let updated_versions = itertools::join(updated_versions, ", ");
357            write!(
358                updates,
359                "
360  - {} has the following newer versions available: {}",
361                pkg_id, updated_versions
362            )
363            .unwrap();
364        }
365    }
366    Some(updates)
367}
368
369/// Writes a future-incompat report to disk, using the per-package
370/// reports gathered during the build. If requested by the user,
371/// a message is also displayed in the build output.
372pub fn save_and_display_report(
373    bcx: &BuildContext<'_, '_>,
374    per_package_future_incompat_reports: &[FutureIncompatReportPackage],
375) {
376    let should_display_message = match bcx.gctx.future_incompat_config() {
377        Ok(config) => config.should_display_message(),
378        Err(e) => {
379            crate::display_warning_with_error(
380                "failed to read future-incompat config from disk",
381                &e,
382                &mut bcx.gctx.shell(),
383            );
384            true
385        }
386    };
387
388    if per_package_future_incompat_reports.is_empty() {
389        // Explicitly passing a command-line flag overrides
390        // `should_display_message` from the config file
391        if bcx.build_config.future_incompat_report {
392            drop(
393                bcx.gctx
394                    .shell()
395                    .note("0 dependencies had future-incompatible warnings"),
396            );
397        }
398        return;
399    }
400
401    let current_reports = match OnDiskReports::load(bcx.ws) {
402        Ok(r) => r,
403        Err(e) => {
404            tracing::debug!(
405                "saving future-incompatible reports failed to load current reports: {:?}",
406                e
407            );
408            OnDiskReports::default()
409        }
410    };
411
412    let rendered_report = render_report(per_package_future_incompat_reports);
413
414    // If the report is already on disk, then it will reuse the same ID,
415    // otherwise prepare for the next ID.
416    let report_id = current_reports
417        .has_report(&rendered_report)
418        .unwrap_or(current_reports.next_id);
419
420    // Get a list of unique and sorted package name/versions.
421    let package_ids: BTreeSet<_> = per_package_future_incompat_reports
422        .iter()
423        .map(|r| r.package_id)
424        .collect();
425    let package_vers: Vec<_> = package_ids.iter().map(|pid| pid.to_string()).collect();
426
427    let updated_versions = get_updates(bcx.ws, &package_ids).unwrap_or(String::new());
428
429    let update_message = if !updated_versions.is_empty() {
430        format!(
431            "\
432update to a newer version to see if the issue has been fixed{updated_versions}",
433            updated_versions = updated_versions
434        )
435    } else {
436        String::new()
437    };
438
439    let upstream_info = package_ids
440        .iter()
441        .map(|package_id| {
442            let manifest = bcx.packages.get_one(*package_id).unwrap().manifest();
443            format!(
444                "  - {package_spec}
445  - repository: {url}
446  - detailed warning command: `cargo report future-incompatibilities --id {id} --package {package_spec}`",
447                package_spec = format!("{}@{}", package_id.name(), package_id.version()),
448                url = manifest
449                    .metadata()
450                    .repository
451                    .as_deref()
452                    .unwrap_or("<not found>"),
453                id = report_id,
454            )
455        })
456        .collect::<Vec<_>>()
457        .join("\n\n");
458
459    let all_is_local = per_package_future_incompat_reports
460        .iter()
461        .all(|report| report.is_local);
462
463    let suggestion_header = "to solve this problem, you can try the following approaches:";
464    let mut suggestions = Vec::new();
465    if !all_is_local {
466        if !update_message.is_empty() {
467            suggestions.push(update_message);
468        }
469        suggestions.push(format!(
470            "\
471ensure the maintainers know of this problem (e.g. creating a bug report if needed)
472or even helping with a fix (e.g. by creating a pull request)
473{upstream_info}"
474        ));
475        suggestions.push(
476            "\
477use your own version of the dependency with the `[patch]` section in `Cargo.toml`
478For more information, see:
479https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section"
480                .to_owned(),
481        );
482    }
483
484    let suggestion_message = if suggestions.is_empty() {
485        String::new()
486    } else {
487        let mut suggestion_message = String::new();
488        writeln!(&mut suggestion_message, "{suggestion_header}").unwrap();
489        for suggestion in &suggestions {
490            writeln!(
491                &mut suggestion_message,
492                "
493- {suggestion}"
494            )
495            .unwrap();
496        }
497        suggestion_message
498    };
499    let saved_report_id =
500        current_reports.save_report(bcx.ws, suggestion_message.clone(), rendered_report);
501
502    if should_display_message || bcx.build_config.future_incompat_report {
503        use cargo_util_terminal::report::*;
504        let mut report = vec![Group::with_title(Level::WARNING.secondary_title(format!(
505            "the following packages contain code that will be rejected by a future \
506             version of Rust: {}",
507            package_vers.join(", ")
508        )))];
509        if bcx.build_config.future_incompat_report {
510            for suggestion in &suggestions {
511                report.push(Group::with_title(Level::HELP.secondary_title(suggestion)));
512            }
513            report.push(Group::with_title(Level::NOTE.secondary_title(format!(
514                "this report can be shown with `cargo report \
515             future-incompatibilities --id {}`",
516                saved_report_id
517            ))));
518        } else if should_display_message {
519            report.push(Group::with_title(Level::NOTE.secondary_title(format!(
520                "to see what the problems were, use the option \
521             `--future-incompat-report`, or run `cargo report \
522             future-incompatibilities --id {}`",
523                saved_report_id
524            ))));
525        }
526        drop(bcx.gctx.shell().print_report(&report, false))
527    }
528}