Skip to main content

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