1use 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
61const ON_DISK_VERSION: u32 = 0;
63
64#[derive(serde::Deserialize)]
66pub struct FutureIncompatReport {
67 pub future_incompat_report: Vec<FutureBreakageItem>,
68}
69
70pub struct FutureIncompatReportPackage {
72 pub package_id: PackageId,
73 pub is_local: bool,
75 pub items: Vec<FutureBreakageItem>,
76}
77
78#[derive(Serialize, Deserialize)]
80pub struct FutureBreakageItem {
81 pub future_breakage_date: Option<String>,
84 pub diagnostic: Diagnostic,
86}
87
88#[derive(Serialize, Deserialize)]
91pub struct Diagnostic {
92 pub rendered: String,
93 pub level: String,
94}
95
96const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
99const MAX_REPORTS: usize = 5;
101
102#[derive(Serialize, Deserialize)]
104pub struct OnDiskReports {
105 version: u32,
108 next_id: u32,
110 reports: Vec<OnDiskReport>,
112}
113
114#[derive(Serialize, Deserialize)]
116struct OnDiskReport {
117 id: u32,
119 suggestion_message: String,
122 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 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 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 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 pub fn last_id(&self) -> u32 {
229 self.reports.last().map(|r| r.id).unwrap()
230 }
231
232 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
300fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> Option<String> {
304 let _lock = ws
306 .gctx()
307 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)
308 .ok()?;
309 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 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
374pub 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 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 let report_id = current_reports
422 .has_report(&rendered_report)
423 .unwrap_or(current_reports.next_id);
424
425 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}