1use 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
62const ON_DISK_VERSION: u32 = 0;
64
65#[derive(serde::Deserialize)]
67pub struct FutureIncompatReport {
68 pub future_incompat_report: Vec<FutureBreakageItem>,
69}
70
71pub struct FutureIncompatReportPackage {
73 pub package_id: PackageId,
74 pub is_local: bool,
76 pub items: Vec<FutureBreakageItem>,
77}
78
79#[derive(Serialize, Deserialize)]
81pub struct FutureBreakageItem {
82 pub future_breakage_date: Option<String>,
85 pub diagnostic: Diagnostic,
87}
88
89#[derive(Serialize, Deserialize)]
92pub struct Diagnostic {
93 pub rendered: String,
94 pub level: String,
95}
96
97const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
100const MAX_REPORTS: usize = 5;
102
103#[derive(Serialize, Deserialize)]
105pub struct OnDiskReports {
106 version: u32,
109 next_id: u32,
111 reports: Vec<OnDiskReport>,
113}
114
115#[derive(Serialize, Deserialize)]
117struct OnDiskReport {
118 id: u32,
120 suggestion_message: String,
123 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 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 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 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 pub fn last_id(&self) -> u32 {
230 self.reports.last().map(|r| r.id).unwrap()
231 }
232
233 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
301fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> Option<String> {
305 let _lock = ws
307 .gctx()
308 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)
309 .ok()?;
310 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 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
368pub 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 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 let report_id = current_reports
416 .has_report(&rendered_report)
417 .unwrap_or(current_reports.next_id);
418
419 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}