| //! Support for [future-incompatible warning reporting][1]. | |
| //! | |
| //! Here is an overview of how Cargo handles future-incompatible reports. | |
| //! | |
| //! ## Receive reports from the compiler | |
| //! | |
| //! When receiving a compiler message during a build, if it is effectively | |
| //! a [`FutureIncompatReport`], Cargo gathers and forwards it as a | |
| //! `Message::FutureIncompatReport` to the main thread. | |
| //! | |
| //! To have the correct layout of structures for deserializing a report | |
| //! emitted by the compiler, most of structure definitions, for example | |
| //! [`FutureIncompatReport`], are copied either partially or entirely from | |
| //! [compiler/rustc_errors/src/json.rs][2] in rust-lang/rust repository. | |
| //! | |
| //! ## Persist reports on disk | |
| //! | |
| //! When a build comes to an end, by calling [`save_and_display_report`] | |
| //! Cargo saves the report on disk, and displays it directly if requested | |
| //! via command line or configuration. The information of the on-disk file can | |
| //! be found in [`FUTURE_INCOMPAT_FILE`]. | |
| //! | |
| //! During the persistent process, Cargo will attempt to query the source of | |
| //! each package emitting the report, for the sake of providing an upgrade | |
| //! information as a solution to fix the incompatibility. | |
| //! | |
| //! ## Display reports to users | |
| //! | |
| //! Users can run `cargo report future-incompat` to retrieve a report. This is | |
| //! done by [`OnDiskReports::load`]. Cargo simply prints reports to the | |
| //! standard output. | |
| //! | |
| //! [1]: https://doc.rust-lang.org/nightly/cargo/reference/future-incompat-report.html | |
| //! [2]: https://github.com/rust-lang/rust/blob/9bb6e60d1f1360234aae90c97964c0fa5524f141/compiler/rustc_errors/src/json.rs#L312-L315 | |
| use crate::compiler::BuildContext; | |
| use crate::sources::IndexSummary; | |
| use crate::sources::SourceConfigMap; | |
| use crate::sources::source::QueryKind; | |
| use crate::util::CargoResult; | |
| use crate::util::cache_lock::CacheLockMode; | |
| use crate::workspace::{Dependency, PackageId, Workspace}; | |
| use anyhow::{Context, bail, format_err}; | |
| use cargo_util::collections::{HashMap, HashSet}; | |
| use futures::stream::FuturesUnordered; | |
| use serde::{Deserialize, Serialize}; | |
| use std::collections::{BTreeMap, BTreeSet}; | |
| use std::fmt::Write as _; | |
| use std::io::{Read, Write}; | |
| pub const REPORT_PREAMBLE: &str = "\ | |
| The following warnings were discovered during the build. These warnings are an | |
| indication that the packages contain code that will become an error in a | |
| future release of Rust. These warnings typically cover changes to close | |
| soundness problems, unintended or undocumented behavior, or critical problems | |
| that cannot be fixed in a backwards-compatible fashion, and are not expected | |
| to be in wide use. | |
| Each warning should contain a link for more information on what the warning | |
| means and how to resolve it. | |
| "; | |
| /// Current version of the on-disk format. | |
| const ON_DISK_VERSION: u32 = 0; | |
| /// The future incompatibility report, emitted by the compiler as a JSON message. | |
| #[derive(serde::Deserialize)] | |
| pub struct FutureIncompatReport { | |
| pub future_incompat_report: Vec<FutureBreakageItem>, | |
| } | |
| /// Structure used for collecting reports in-memory. | |
| pub struct FutureIncompatReportPackage { | |
| pub package_id: PackageId, | |
| /// Whether or not this is a local package, or a remote dependency. | |
| pub is_local: bool, | |
| pub items: Vec<FutureBreakageItem>, | |
| } | |
| /// A single future-incompatible warning emitted by rustc. | |
| #[derive(Serialize, Deserialize)] | |
| pub struct FutureBreakageItem { | |
| /// The date at which this lint will become an error. | |
| /// Currently unused | |
| pub future_breakage_date: Option<String>, | |
| /// The original diagnostic emitted by the compiler | |
| pub diagnostic: Diagnostic, | |
| } | |
| /// A diagnostic emitted by the compiler as a JSON message. | |
| /// We only care about the 'rendered' field | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Diagnostic { | |
| pub rendered: String, | |
| pub level: String, | |
| } | |
| /// The filename in the top-level `build-dir` directory where we store | |
| /// the report | |
| const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json"; | |
| /// Max number of reports to save on disk. | |
| const MAX_REPORTS: usize = 5; | |
| /// The structure saved to disk containing the reports. | |
| #[derive(Serialize, Deserialize)] | |
| pub struct OnDiskReports { | |
| /// A schema version number, to handle older cargo's from trying to read | |
| /// something that they don't understand. | |
| version: u32, | |
| /// The report ID to use for the next report to save. | |
| next_id: u32, | |
| /// Available reports. | |
| reports: Vec<OnDiskReport>, | |
| } | |
| /// A single report for a given compilation session. | |
| #[derive(Serialize, Deserialize)] | |
| struct OnDiskReport { | |
| /// Unique reference to the report for the `--id` CLI flag. | |
| id: u32, | |
| /// A message describing suggestions for fixing the | |
| /// reported issues | |
| suggestion_message: String, | |
| /// Report, suitable for printing to the console. | |
| /// Maps package names to the corresponding report | |
| /// We use a `BTreeMap` so that the iteration order | |
| /// is stable across multiple runs of `cargo` | |
| per_package: BTreeMap<String, String>, | |
| } | |
| impl Default for OnDiskReports { | |
| fn default() -> OnDiskReports { | |
| OnDiskReports { | |
| version: ON_DISK_VERSION, | |
| next_id: 1, | |
| reports: Vec::new(), | |
| } | |
| } | |
| } | |
| impl OnDiskReports { | |
| /// Saves a new report returning its id | |
| pub fn save_report( | |
| mut self, | |
| ws: &Workspace<'_>, | |
| suggestion_message: String, | |
| per_package: BTreeMap<String, String>, | |
| ) -> u32 { | |
| if let Some(existing_id) = self.has_report(&per_package) { | |
| return existing_id; | |
| } | |
| let report = OnDiskReport { | |
| id: self.next_id, | |
| suggestion_message, | |
| per_package, | |
| }; | |
| let saved_id = report.id; | |
| self.next_id += 1; | |
| self.reports.push(report); | |
| if self.reports.len() > MAX_REPORTS { | |
| self.reports.remove(0); | |
| } | |
| let on_disk = serde_json::to_vec(&self).unwrap(); | |
| if let Err(e) = ws | |
| .build_dir() | |
| .open_rw_exclusive_create( | |
| FUTURE_INCOMPAT_FILE, | |
| ws.gctx(), | |
| "Future incompatibility report", | |
| ) | |
| .and_then(|file| { | |
| let mut file = file.file(); | |
| file.set_len(0)?; | |
| file.write_all(&on_disk)?; | |
| Ok(()) | |
| }) | |
| { | |
| crate::display_warning_with_error( | |
| "failed to write on-disk future incompatible report", | |
| &e, | |
| &mut ws.gctx().shell(), | |
| ); | |
| } | |
| saved_id | |
| } | |
| /// Returns the ID of a report if it is already on disk. | |
| fn has_report(&self, rendered_per_package: &BTreeMap<String, String>) -> Option<u32> { | |
| self.reports | |
| .iter() | |
| .find(|existing| &existing.per_package == rendered_per_package) | |
| .map(|report| report.id) | |
| } | |
| /// Loads the on-disk reports. | |
| pub fn load(ws: &Workspace<'_>) -> CargoResult<OnDiskReports> { | |
| let report_file = match ws.build_dir().open_ro_shared( | |
| FUTURE_INCOMPAT_FILE, | |
| ws.gctx(), | |
| "Future incompatible report", | |
| ) { | |
| Ok(r) => r, | |
| Err(e) => { | |
| if let Some(io_err) = e.downcast_ref::<std::io::Error>() { | |
| if io_err.kind() == std::io::ErrorKind::NotFound { | |
| bail!("no reports are currently available"); | |
| } | |
| } | |
| return Err(e); | |
| } | |
| }; | |
| let mut file_contents = String::new(); | |
| report_file | |
| .file() | |
| .read_to_string(&mut file_contents) | |
| .context("failed to read report")?; | |
| let on_disk_reports: OnDiskReports = | |
| serde_json::from_str(&file_contents).context("failed to load report")?; | |
| if on_disk_reports.version != ON_DISK_VERSION { | |
| bail!("unable to read reports; reports were saved from a future version of Cargo"); | |
| } | |
| Ok(on_disk_reports) | |
| } | |
| /// Returns the most recent report ID. | |
| pub fn last_id(&self) -> u32 { | |
| self.reports.last().map(|r| r.id).unwrap() | |
| } | |
| /// Returns an ANSI-styled report | |
| pub fn get_report(&self, id: u32, package: Option<&str>) -> CargoResult<String> { | |
| let report = self.reports.iter().find(|r| r.id == id).ok_or_else(|| { | |
| let available = itertools::join(self.reports.iter().map(|r| r.id), ", "); | |
| format_err!( | |
| "could not find report with ID {}\n\ | |
| Available IDs are: {}", | |
| id, | |
| available | |
| ) | |
| })?; | |
| let mut to_display = report.suggestion_message.clone(); | |
| to_display += "\n"; | |
| let package_report = if let Some(package) = package { | |
| report | |
| .per_package | |
| .get(package) | |
| .ok_or_else(|| { | |
| format_err!( | |
| "could not find package with ID `{}`\n | |
| Available packages are: {}\n | |
| Omit the `--package` flag to display a report for all packages", | |
| package, | |
| itertools::join(report.per_package.keys(), ", ") | |
| ) | |
| })? | |
| .to_string() | |
| } else { | |
| report | |
| .per_package | |
| .values() | |
| .cloned() | |
| .collect::<Vec<_>>() | |
| .join("\n") | |
| }; | |
| to_display += &package_report; | |
| Ok(to_display) | |
| } | |
| } | |
| fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> BTreeMap<String, String> { | |
| let mut report: BTreeMap<String, String> = BTreeMap::new(); | |
| for per_package in per_package_reports { | |
| let package_spec = format!( | |
| "{}@{}", | |
| per_package.package_id.name(), | |
| per_package.package_id.version() | |
| ); | |
| let rendered = report.entry(package_spec).or_default(); | |
| rendered.push_str(&format!( | |
| "The package `{}` currently triggers the following future incompatibility lints:\n", | |
| per_package.package_id | |
| )); | |
| for item in &per_package.items { | |
| rendered.extend( | |
| item.diagnostic | |
| .rendered | |
| .lines() | |
| .map(|l| format!("> {}\n", l)), | |
| ); | |
| } | |
| } | |
| report | |
| } | |
| /// Returns a user-readable message explaining which of | |
| /// the packages in `package_ids` have updates available. | |
| /// This is best-effort - if an error occurs, `None` will be returned. | |
| fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> Option<String> { | |
| // This in general ignores all errors since this is opportunistic. | |
| let _lock = ws | |
| .gctx() | |
| .acquire_package_cache_lock(CacheLockMode::DownloadExclusive) | |
| .ok()?; | |
| // Create a set of updated registry sources. | |
| let map = SourceConfigMap::new(ws.gctx()).ok()?; | |
| let package_ids: BTreeSet<_> = package_ids | |
| .iter() | |
| .filter(|pkg_id| pkg_id.source_id().is_registry()) | |
| .collect(); | |
| let source_ids: HashSet<_> = package_ids | |
| .iter() | |
| .map(|pkg_id| pkg_id.source_id()) | |
| .collect(); | |
| let sources: HashMap<_, _> = source_ids | |
| .into_iter() | |
| .filter_map(|sid| { | |
| let source = map.load(sid).ok()?; | |
| Some((sid, source)) | |
| }) | |
| .collect(); | |
| // Query the sources for new versions, mapping `package_ids` into `summaries`. | |
| let pending = FuturesUnordered::new(); | |
| for pkg_id in package_ids { | |
| if let Some(source) = sources.get(&pkg_id.source_id()) | |
| && let Ok(dep) = Dependency::parse(pkg_id.name(), None, pkg_id.source_id()) | |
| { | |
| pending.push(async move { | |
| let sum = source.query_vec(&dep, QueryKind::Exact).await.ok()?; | |
| Some((pkg_id, sum)) | |
| }); | |
| } | |
| } | |
| let summaries = crate::util::block_on_stream(pending).flatten(); | |
| let mut updates = String::new(); | |
| for (pkg_id, summaries) in summaries { | |
| let mut updated_versions: Vec<_> = summaries | |
| .iter() | |
| .filter_map(|s| match s { | |
| IndexSummary::Candidate(s) => Some(s.version()), | |
| _ => None, | |
| }) | |
| .filter(|version| *version > pkg_id.version()) | |
| .collect(); | |
| updated_versions.sort(); | |
| if !updated_versions.is_empty() { | |
| let updated_versions = itertools::join(updated_versions, ", "); | |
| write!( | |
| updates, | |
| " | |
| - {} has the following newer versions available: {}", | |
| pkg_id, updated_versions | |
| ) | |
| .unwrap(); | |
| } | |
| } | |
| Some(updates) | |
| } | |
| /// Writes a future-incompat report to disk, using the per-package | |
| /// reports gathered during the build. If requested by the user, | |
| /// a message is also displayed in the build output. | |
| pub fn save_and_display_report( | |
| bcx: &BuildContext<'_, '_>, | |
| per_package_future_incompat_reports: &[FutureIncompatReportPackage], | |
| ) { | |
| let should_display_message = match bcx.gctx.future_incompat_config() { | |
| Ok(config) => config.should_display_message(), | |
| Err(e) => { | |
| crate::display_warning_with_error( | |
| "failed to read future-incompat config from disk", | |
| &e, | |
| &mut bcx.gctx.shell(), | |
| ); | |
| true | |
| } | |
| }; | |
| if per_package_future_incompat_reports.is_empty() { | |
| // Explicitly passing a command-line flag overrides | |
| // `should_display_message` from the config file | |
| if bcx.build_config.future_incompat_report { | |
| drop( | |
| bcx.gctx | |
| .shell() | |
| .note("0 dependencies had future-incompatible warnings"), | |
| ); | |
| } | |
| return; | |
| } | |
| let current_reports = match OnDiskReports::load(bcx.ws) { | |
| Ok(r) => r, | |
| Err(e) => { | |
| tracing::debug!( | |
| "saving future-incompatible reports failed to load current reports: {:?}", | |
| e | |
| ); | |
| OnDiskReports::default() | |
| } | |
| }; | |
| let rendered_report = render_report(per_package_future_incompat_reports); | |
| // If the report is already on disk, then it will reuse the same ID, | |
| // otherwise prepare for the next ID. | |
| let report_id = current_reports | |
| .has_report(&rendered_report) | |
| .unwrap_or(current_reports.next_id); | |
| // Get a list of unique and sorted package name/versions. | |
| let package_ids: BTreeSet<_> = per_package_future_incompat_reports | |
| .iter() | |
| .map(|r| r.package_id) | |
| .collect(); | |
| let package_vers: Vec<_> = package_ids.iter().map(|pid| pid.to_string()).collect(); | |
| let updated_versions = get_updates(bcx.ws, &package_ids).unwrap_or(String::new()); | |
| let update_message = if !updated_versions.is_empty() { | |
| format!( | |
| "\ | |
| update to a newer version to see if the issue has been fixed{updated_versions}", | |
| updated_versions = updated_versions | |
| ) | |
| } else { | |
| String::new() | |
| }; | |
| let upstream_info = package_ids | |
| .iter() | |
| .map(|package_id| { | |
| let manifest = bcx.packages.get_one(*package_id).unwrap().manifest(); | |
| format!( | |
| " - {package_spec} | |
| - repository: {url} | |
| - detailed warning command: `cargo report future-incompatibilities --id {id} --package {package_spec}`", | |
| package_spec = format!("{}@{}", package_id.name(), package_id.version()), | |
| url = manifest | |
| .metadata() | |
| .repository | |
| .as_deref() | |
| .unwrap_or("<not found>"), | |
| id = report_id, | |
| ) | |
| }) | |
| .collect::<Vec<_>>() | |
| .join("\n\n"); | |
| let all_is_local = per_package_future_incompat_reports | |
| .iter() | |
| .all(|report| report.is_local); | |
| let suggestion_header = "to solve this problem, you can try the following approaches:"; | |
| let mut suggestions = Vec::new(); | |
| if !all_is_local { | |
| if !update_message.is_empty() { | |
| suggestions.push(update_message); | |
| } | |
| suggestions.push(format!( | |
| "\ | |
| ensure the maintainers know of this problem (e.g. creating a bug report if needed) | |
| or even helping with a fix (e.g. by creating a pull request) | |
| {upstream_info}" | |
| )); | |
| suggestions.push( | |
| "\ | |
| use your own version of the dependency with the `[patch]` section in `Cargo.toml` | |
| For more information, see: | |
| https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section" | |
| .to_owned(), | |
| ); | |
| } | |
| let suggestion_message = if suggestions.is_empty() { | |
| String::new() | |
| } else { | |
| let mut suggestion_message = String::new(); | |
| writeln!(&mut suggestion_message, "{suggestion_header}").unwrap(); | |
| for suggestion in &suggestions { | |
| writeln!( | |
| &mut suggestion_message, | |
| " | |
| - {suggestion}" | |
| ) | |
| .unwrap(); | |
| } | |
| suggestion_message | |
| }; | |
| let saved_report_id = | |
| current_reports.save_report(bcx.ws, suggestion_message.clone(), rendered_report); | |
| if should_display_message || bcx.build_config.future_incompat_report { | |
| use cargo_util_terminal::report::*; | |
| let mut report = vec![Group::with_title(Level::WARNING.secondary_title(format!( | |
| "the following packages contain code that will be rejected by a future \ | |
| version of Rust: {}", | |
| package_vers.join(", ") | |
| )))]; | |
| if bcx.build_config.future_incompat_report { | |
| for suggestion in &suggestions { | |
| report.push(Group::with_title(Level::HELP.secondary_title(suggestion))); | |
| } | |
| report.push(Group::with_title(Level::NOTE.secondary_title(format!( | |
| "this report can be shown with `cargo report \ | |
| future-incompatibilities --id {}`", | |
| saved_report_id | |
| )))); | |
| } else if should_display_message { | |
| report.push(Group::with_title(Level::NOTE.secondary_title(format!( | |
| "to see what the problems were, use the option \ | |
| `--future-incompat-report`, or run `cargo report \ | |
| future-incompatibilities --id {}`", | |
| saved_report_id | |
| )))); | |
| } | |
| drop(bcx.gctx.shell().print_report(&report, false)) | |
| } | |
| } |