blob: 7d59dd1d1c0ccdc9ffc0faad8ea902f15b654dff [file] [edit]
//! 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::util::data_structures::{HashMap, HashSet};
use crate::workspace::{Dependency, PackageId, Workspace};
use anyhow::{Context, bail, format_err};
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))
}
}