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 strucutures 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::source::QueryKind;
39use crate::sources::SourceConfigMap;
40use crate::util::cache_lock::CacheLockMode;
41use crate::util::CargoResult;
42use anyhow::{bail, format_err, Context};
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    pub items: Vec<FutureBreakageItem>,
74}
75
76/// A single future-incompatible warning emitted by rustc.
77#[derive(Serialize, Deserialize)]
78pub struct FutureBreakageItem {
79    /// The date at which this lint will become an error.
80    /// Currently unused
81    pub future_breakage_date: Option<String>,
82    /// The original diagnostic emitted by the compiler
83    pub diagnostic: Diagnostic,
84}
85
86/// A diagnostic emitted by the compiler as a JSON message.
87/// We only care about the 'rendered' field
88#[derive(Serialize, Deserialize)]
89pub struct Diagnostic {
90    pub rendered: String,
91    pub level: String,
92}
93
94/// The filename in the top-level `target` directory where we store
95/// the report
96const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
97/// Max number of reports to save on disk.
98const MAX_REPORTS: usize = 5;
99
100/// The structure saved to disk containing the reports.
101#[derive(Serialize, Deserialize)]
102pub struct OnDiskReports {
103    /// A schema version number, to handle older cargo's from trying to read
104    /// something that they don't understand.
105    version: u32,
106    /// The report ID to use for the next report to save.
107    next_id: u32,
108    /// Available reports.
109    reports: Vec<OnDiskReport>,
110}
111
112/// A single report for a given compilation session.
113#[derive(Serialize, Deserialize)]
114struct OnDiskReport {
115    /// Unique reference to the report for the `--id` CLI flag.
116    id: u32,
117    /// A message describing suggestions for fixing the
118    /// reported issues
119    suggestion_message: String,
120    /// Report, suitable for printing to the console.
121    /// Maps package names to the corresponding report
122    /// We use a `BTreeMap` so that the iteration order
123    /// is stable across multiple runs of `cargo`
124    per_package: BTreeMap<String, String>,
125}
126
127impl Default for OnDiskReports {
128    fn default() -> OnDiskReports {
129        OnDiskReports {
130            version: ON_DISK_VERSION,
131            next_id: 1,
132            reports: Vec::new(),
133        }
134    }
135}
136
137impl OnDiskReports {
138    /// Saves a new report returning its id
139    pub fn save_report(
140        mut self,
141        ws: &Workspace<'_>,
142        suggestion_message: String,
143        per_package_reports: &[FutureIncompatReportPackage],
144    ) -> u32 {
145        let per_package = render_report(per_package_reports);
146
147        if let Some(existing_report) = self
148            .reports
149            .iter()
150            .find(|existing| existing.per_package == per_package)
151        {
152            return existing_report.id;
153        }
154
155        let report = OnDiskReport {
156            id: self.next_id,
157            suggestion_message,
158            per_package,
159        };
160
161        let saved_id = report.id;
162        self.next_id += 1;
163        self.reports.push(report);
164        if self.reports.len() > MAX_REPORTS {
165            self.reports.remove(0);
166        }
167        let on_disk = serde_json::to_vec(&self).unwrap();
168        if let Err(e) = ws
169            .target_dir()
170            .open_rw_exclusive_create(
171                FUTURE_INCOMPAT_FILE,
172                ws.gctx(),
173                "Future incompatibility report",
174            )
175            .and_then(|file| {
176                let mut file = file.file();
177                file.set_len(0)?;
178                file.write_all(&on_disk)?;
179                Ok(())
180            })
181        {
182            crate::display_warning_with_error(
183                "failed to write on-disk future incompatible report",
184                &e,
185                &mut ws.gctx().shell(),
186            );
187        }
188
189        saved_id
190    }
191
192    /// Loads the on-disk reports.
193    pub fn load(ws: &Workspace<'_>) -> CargoResult<OnDiskReports> {
194        let report_file = match ws.target_dir().open_ro_shared(
195            FUTURE_INCOMPAT_FILE,
196            ws.gctx(),
197            "Future incompatible report",
198        ) {
199            Ok(r) => r,
200            Err(e) => {
201                if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
202                    if io_err.kind() == std::io::ErrorKind::NotFound {
203                        bail!("no reports are currently available");
204                    }
205                }
206                return Err(e);
207            }
208        };
209
210        let mut file_contents = String::new();
211        report_file
212            .file()
213            .read_to_string(&mut file_contents)
214            .context("failed to read report")?;
215        let on_disk_reports: OnDiskReports =
216            serde_json::from_str(&file_contents).context("failed to load report")?;
217        if on_disk_reports.version != ON_DISK_VERSION {
218            bail!("unable to read reports; reports were saved from a future version of Cargo");
219        }
220        Ok(on_disk_reports)
221    }
222
223    /// Returns the most recent report ID.
224    pub fn last_id(&self) -> u32 {
225        self.reports.last().map(|r| r.id).unwrap()
226    }
227
228    /// Returns an ANSI-styled report
229    pub fn get_report(&self, id: u32, package: Option<&str>) -> CargoResult<String> {
230        let report = self.reports.iter().find(|r| r.id == id).ok_or_else(|| {
231            let available = itertools::join(self.reports.iter().map(|r| r.id), ", ");
232            format_err!(
233                "could not find report with ID {}\n\
234                 Available IDs are: {}",
235                id,
236                available
237            )
238        })?;
239
240        let mut to_display = report.suggestion_message.clone();
241        to_display += "\n";
242
243        let package_report = if let Some(package) = package {
244            report
245                .per_package
246                .get(package)
247                .ok_or_else(|| {
248                    format_err!(
249                        "could not find package with ID `{}`\n
250                Available packages are: {}\n
251                Omit the `--package` flag to display a report for all packages",
252                        package,
253                        itertools::join(report.per_package.keys(), ", ")
254                    )
255                })?
256                .to_string()
257        } else {
258            report
259                .per_package
260                .values()
261                .cloned()
262                .collect::<Vec<_>>()
263                .join("\n")
264        };
265        to_display += &package_report;
266
267        Ok(to_display)
268    }
269}
270
271fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> BTreeMap<String, String> {
272    let mut report: BTreeMap<String, String> = BTreeMap::new();
273    for per_package in per_package_reports {
274        let package_spec = format!(
275            "{}@{}",
276            per_package.package_id.name(),
277            per_package.package_id.version()
278        );
279        let rendered = report.entry(package_spec).or_default();
280        rendered.push_str(&format!(
281            "The package `{}` currently triggers the following future incompatibility lints:\n",
282            per_package.package_id
283        ));
284        for item in &per_package.items {
285            rendered.extend(
286                item.diagnostic
287                    .rendered
288                    .lines()
289                    .map(|l| format!("> {}\n", l)),
290            );
291        }
292    }
293    report
294}
295
296/// Returns a user-readable message explaining which of
297/// the packages in `package_ids` have updates available.
298/// This is best-effort - if an error occurs, `None` will be returned.
299fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> Option<String> {
300    // This in general ignores all errors since this is opportunistic.
301    let _lock = ws
302        .gctx()
303        .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)
304        .ok()?;
305    // Create a set of updated registry sources.
306    let map = SourceConfigMap::new(ws.gctx()).ok()?;
307    let mut package_ids: BTreeSet<_> = package_ids
308        .iter()
309        .filter(|pkg_id| pkg_id.source_id().is_registry())
310        .collect();
311    let source_ids: HashSet<_> = package_ids
312        .iter()
313        .map(|pkg_id| pkg_id.source_id())
314        .collect();
315    let mut sources: HashMap<_, _> = source_ids
316        .into_iter()
317        .filter_map(|sid| {
318            let source = map.load(sid, &HashSet::new()).ok()?;
319            Some((sid, source))
320        })
321        .collect();
322
323    // Query the sources for new versions, mapping `package_ids` into `summaries`.
324    let mut summaries = Vec::new();
325    while !package_ids.is_empty() {
326        package_ids.retain(|&pkg_id| {
327            let Some(source) = sources.get_mut(&pkg_id.source_id()) else {
328                return false;
329            };
330            let Ok(dep) = Dependency::parse(pkg_id.name(), None, pkg_id.source_id()) else {
331                return false;
332            };
333            match source.query_vec(&dep, QueryKind::Exact) {
334                Poll::Ready(Ok(sum)) => {
335                    summaries.push((pkg_id, sum));
336                    false
337                }
338                Poll::Ready(Err(_)) => false,
339                Poll::Pending => true,
340            }
341        });
342        for (_, source) in sources.iter_mut() {
343            source.block_until_ready().ok()?;
344        }
345    }
346
347    let mut updates = String::new();
348    for (pkg_id, summaries) in summaries {
349        let mut updated_versions: Vec<_> = summaries
350            .iter()
351            .map(|summary| summary.as_summary().version())
352            .filter(|version| *version > pkg_id.version())
353            .collect();
354        updated_versions.sort();
355
356        if !updated_versions.is_empty() {
357            let updated_versions = itertools::join(updated_versions, ", ");
358            writeln!(
359                updates,
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    let report_id = current_reports.next_id;
412
413    // Get a list of unique and sorted package name/versions.
414    let package_ids: BTreeSet<_> = per_package_future_incompat_reports
415        .iter()
416        .map(|r| r.package_id)
417        .collect();
418    let package_vers: Vec<_> = package_ids.iter().map(|pid| pid.to_string()).collect();
419
420    if should_display_message || bcx.build_config.future_incompat_report {
421        drop(bcx.gctx.shell().warn(&format!(
422            "the following packages contain code that will be rejected by a future \
423             version of Rust: {}",
424            package_vers.join(", ")
425        )));
426    }
427
428    let updated_versions = get_updates(bcx.ws, &package_ids).unwrap_or(String::new());
429
430    let update_message = if !updated_versions.is_empty() {
431        format!(
432            "
433- Some affected dependencies have newer versions available.
434You may want to consider updating them to a newer version to see if the issue has been fixed.
435
436{updated_versions}\n",
437            updated_versions = updated_versions
438        )
439    } else {
440        String::new()
441    };
442
443    let upstream_info = package_ids
444        .iter()
445        .map(|package_id| {
446            let manifest = bcx.packages.get_one(*package_id).unwrap().manifest();
447            format!(
448                "
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");
463
464    let suggestion_message = format!(
465        "
466To solve this problem, you can try the following approaches:
467
468{update_message}
469- If the issue is not solved by updating the dependencies, a fix has to be
470implemented by those dependencies. You can help with that by notifying the
471maintainers of this problem (e.g. by creating a bug report) or by proposing a
472fix to the maintainers (e.g. by creating a pull request):
473{upstream_info}
474
475- If waiting for an upstream fix is not an option, you can use the `[patch]`
476section in `Cargo.toml` to use your own version of the dependency. For more
477information, see:
478https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section
479        ",
480        upstream_info = upstream_info,
481        update_message = update_message,
482    );
483
484    let saved_report_id = current_reports.save_report(
485        bcx.ws,
486        suggestion_message.clone(),
487        per_package_future_incompat_reports,
488    );
489
490    if bcx.build_config.future_incompat_report {
491        drop(bcx.gctx.shell().note(&suggestion_message));
492        drop(bcx.gctx.shell().note(&format!(
493            "this report can be shown with `cargo report \
494             future-incompatibilities --id {}`",
495            saved_report_id
496        )));
497    } else if should_display_message {
498        drop(bcx.gctx.shell().note(&format!(
499            "to see what the problems were, use the option \
500             `--future-incompat-report`, or run `cargo report \
501             future-incompatibilities --id {}`",
502            saved_report_id
503        )));
504    }
505}