#
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}