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