1use crate::compiler::BuildContext;
37use crate::sources::IndexSummary;
38use crate::sources::SourceConfigMap;
39use crate::sources::source::QueryKind;
40use crate::util::CargoResult;
41use crate::util::cache_lock::CacheLockMode;
42use crate::util::data_structures::{HashMap, HashSet};
43use crate::workspace::{Dependency, PackageId, Workspace};
44use anyhow::{Context, bail, format_err};
45use futures::stream::FuturesUnordered;
46use serde::{Deserialize, Serialize};
47use std::collections::{BTreeMap, BTreeSet};
48use std::fmt::Write as _;
49use std::io::{Read, Write};
50
51pub const REPORT_PREAMBLE: &str = "\
52The following warnings were discovered during the build. These warnings are an
53indication that the packages contain code that will become an error in a
54future release of Rust. These warnings typically cover changes to close
55soundness problems, unintended or undocumented behavior, or critical problems
56that cannot be fixed in a backwards-compatible fashion, and are not expected
57to be in wide use.
58
59Each warning should contain a link for more information on what the warning
60means and how to resolve it.
61";
62
63const ON_DISK_VERSION: u32 = 0;
65
66#[derive(serde::Deserialize)]
68pub struct FutureIncompatReport {
69 pub future_incompat_report: Vec<FutureBreakageItem>,
70}
71
72pub struct FutureIncompatReportPackage {
74 pub package_id: PackageId,
75 pub is_local: bool,
77 pub items: Vec<FutureBreakageItem>,
78}
79
80#[derive(Serialize, Deserialize)]
82pub struct FutureBreakageItem {
83 pub future_breakage_date: Option<String>,
86 pub diagnostic: Diagnostic,
88}
89
90#[derive(Serialize, Deserialize)]
93pub struct Diagnostic {
94 pub rendered: String,
95 pub level: String,
96}
97
98const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
101const MAX_REPORTS: usize = 5;
103
104#[derive(Serialize, Deserialize)]
106pub struct OnDiskReports {
107 version: u32,
110 next_id: u32,
112 reports: Vec<OnDiskReport>,
114}
115
116#[derive(Serialize, Deserialize)]
118struct OnDiskReport {
119 id: u32,
121 suggestion_message: String,
124 per_package: BTreeMap<String, String>,
129}
130
131impl Default for OnDiskReports {
132 fn default() -> OnDiskReports {
133 OnDiskReports {
134 version: ON_DISK_VERSION,
135 next_id: 1,
136 reports: Vec::new(),
137 }
138 }
139}
140
141impl OnDiskReports {
142 pub fn save_report(
144 mut self,
145 ws: &Workspace<'_>,
146 suggestion_message: String,
147 per_package: BTreeMap<String, String>,
148 ) -> u32 {
149 if let Some(existing_id) = self.has_report(&per_package) {
150 return existing_id;
151 }
152
153 let report = OnDiskReport {
154 id: self.next_id,
155 suggestion_message,
156 per_package,
157 };
158
159 let saved_id = report.id;
160 self.next_id += 1;
161 self.reports.push(report);
162 if self.reports.len() > MAX_REPORTS {
163 self.reports.remove(0);
164 }
165 let on_disk = serde_json::to_vec(&self).unwrap();
166 if let Err(e) = ws
167 .build_dir()
168 .open_rw_exclusive_create(
169 FUTURE_INCOMPAT_FILE,
170 ws.gctx(),
171 "Future incompatibility report",
172 )
173 .and_then(|file| {
174 let mut file = file.file();
175 file.set_len(0)?;
176 file.write_all(&on_disk)?;
177 Ok(())
178 })
179 {
180 crate::display_warning_with_error(
181 "failed to write on-disk future incompatible report",
182 &e,
183 &mut ws.gctx().shell(),
184 );
185 }
186
187 saved_id
188 }
189
190 fn has_report(&self, rendered_per_package: &BTreeMap<String, String>) -> Option<u32> {
192 self.reports
193 .iter()
194 .find(|existing| &existing.per_package == rendered_per_package)
195 .map(|report| report.id)
196 }
197
198 pub fn load(ws: &Workspace<'_>) -> CargoResult<OnDiskReports> {
200 let report_file = match ws.build_dir().open_ro_shared(
201 FUTURE_INCOMPAT_FILE,
202 ws.gctx(),
203 "Future incompatible report",
204 ) {
205 Ok(r) => r,
206 Err(e) => {
207 if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
208 if io_err.kind() == std::io::ErrorKind::NotFound {
209 bail!("no reports are currently available");
210 }
211 }
212 return Err(e);
213 }
214 };
215
216 let mut file_contents = String::new();
217 report_file
218 .file()
219 .read_to_string(&mut file_contents)
220 .context("failed to read report")?;
221 let on_disk_reports: OnDiskReports =
222 serde_json::from_str(&file_contents).context("failed to load report")?;
223 if on_disk_reports.version != ON_DISK_VERSION {
224 bail!("unable to read reports; reports were saved from a future version of Cargo");
225 }
226 Ok(on_disk_reports)
227 }
228
229 pub fn last_id(&self) -> u32 {
231 self.reports.last().map(|r| r.id).unwrap()
232 }
233
234 pub fn get_report(&self, id: u32, package: Option<&str>) -> CargoResult<String> {
236 let report = self.reports.iter().find(|r| r.id == id).ok_or_else(|| {
237 let available = itertools::join(self.reports.iter().map(|r| r.id), ", ");
238 format_err!(
239 "could not find report with ID {}\n\
240 Available IDs are: {}",
241 id,
242 available
243 )
244 })?;
245
246 let mut to_display = report.suggestion_message.clone();
247 to_display += "\n";
248
249 let package_report = if let Some(package) = package {
250 report
251 .per_package
252 .get(package)
253 .ok_or_else(|| {
254 format_err!(
255 "could not find package with ID `{}`\n
256 Available packages are: {}\n
257 Omit the `--package` flag to display a report for all packages",
258 package,
259 itertools::join(report.per_package.keys(), ", ")
260 )
261 })?
262 .to_string()
263 } else {
264 report
265 .per_package
266 .values()
267 .cloned()
268 .collect::<Vec<_>>()
269 .join("\n")
270 };
271 to_display += &package_report;
272
273 Ok(to_display)
274 }
275}
276
277fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> BTreeMap<String, String> {
278 let mut report: BTreeMap<String, String> = BTreeMap::new();
279 for per_package in per_package_reports {
280 let package_spec = format!(
281 "{}@{}",
282 per_package.package_id.name(),
283 per_package.package_id.version()
284 );
285 let rendered = report.entry(package_spec).or_default();
286 rendered.push_str(&format!(
287 "The package `{}` currently triggers the following future incompatibility lints:\n",
288 per_package.package_id
289 ));
290 for item in &per_package.items {
291 rendered.extend(
292 item.diagnostic
293 .rendered
294 .lines()
295 .map(|l| format!("> {}\n", l)),
296 );
297 }
298 }
299 report
300}
301
302fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> Option<String> {
306 let _lock = ws
308 .gctx()
309 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)
310 .ok()?;
311 let map = SourceConfigMap::new(ws.gctx()).ok()?;
313 let package_ids: BTreeSet<_> = package_ids
314 .iter()
315 .filter(|pkg_id| pkg_id.source_id().is_registry())
316 .collect();
317 let source_ids: HashSet<_> = package_ids
318 .iter()
319 .map(|pkg_id| pkg_id.source_id())
320 .collect();
321 let sources: HashMap<_, _> = source_ids
322 .into_iter()
323 .filter_map(|sid| {
324 let source = map.load(sid).ok()?;
325 Some((sid, source))
326 })
327 .collect();
328
329 let pending = FuturesUnordered::new();
331 for pkg_id in package_ids {
332 if let Some(source) = sources.get(&pkg_id.source_id())
333 && let Ok(dep) = Dependency::parse(pkg_id.name(), None, pkg_id.source_id())
334 {
335 pending.push(async move {
336 let sum = source.query_vec(&dep, QueryKind::Exact).await.ok()?;
337 Some((pkg_id, sum))
338 });
339 }
340 }
341 let summaries = crate::util::block_on_stream(pending).flatten();
342
343 let mut updates = String::new();
344 for (pkg_id, summaries) in summaries {
345 let mut updated_versions: Vec<_> = summaries
346 .iter()
347 .filter_map(|s| match s {
348 IndexSummary::Candidate(s) => Some(s.version()),
349 _ => None,
350 })
351 .filter(|version| *version > pkg_id.version())
352 .collect();
353 updated_versions.sort();
354
355 if !updated_versions.is_empty() {
356 let updated_versions = itertools::join(updated_versions, ", ");
357 write!(
358 updates,
359 "
360 - {} has the following newer versions available: {}",
361 pkg_id, updated_versions
362 )
363 .unwrap();
364 }
365 }
366 Some(updates)
367}
368
369pub fn save_and_display_report(
373 bcx: &BuildContext<'_, '_>,
374 per_package_future_incompat_reports: &[FutureIncompatReportPackage],
375) {
376 let should_display_message = match bcx.gctx.future_incompat_config() {
377 Ok(config) => config.should_display_message(),
378 Err(e) => {
379 crate::display_warning_with_error(
380 "failed to read future-incompat config from disk",
381 &e,
382 &mut bcx.gctx.shell(),
383 );
384 true
385 }
386 };
387
388 if per_package_future_incompat_reports.is_empty() {
389 if bcx.build_config.future_incompat_report {
392 drop(
393 bcx.gctx
394 .shell()
395 .note("0 dependencies had future-incompatible warnings"),
396 );
397 }
398 return;
399 }
400
401 let current_reports = match OnDiskReports::load(bcx.ws) {
402 Ok(r) => r,
403 Err(e) => {
404 tracing::debug!(
405 "saving future-incompatible reports failed to load current reports: {:?}",
406 e
407 );
408 OnDiskReports::default()
409 }
410 };
411
412 let rendered_report = render_report(per_package_future_incompat_reports);
413
414 let report_id = current_reports
417 .has_report(&rendered_report)
418 .unwrap_or(current_reports.next_id);
419
420 let package_ids: BTreeSet<_> = per_package_future_incompat_reports
422 .iter()
423 .map(|r| r.package_id)
424 .collect();
425 let package_vers: Vec<_> = package_ids.iter().map(|pid| pid.to_string()).collect();
426
427 let updated_versions = get_updates(bcx.ws, &package_ids).unwrap_or(String::new());
428
429 let update_message = if !updated_versions.is_empty() {
430 format!(
431 "\
432update to a newer version to see if the issue has been fixed{updated_versions}",
433 updated_versions = updated_versions
434 )
435 } else {
436 String::new()
437 };
438
439 let upstream_info = package_ids
440 .iter()
441 .map(|package_id| {
442 let manifest = bcx.packages.get_one(*package_id).unwrap().manifest();
443 format!(
444 " - {package_spec}
445 - repository: {url}
446 - detailed warning command: `cargo report future-incompatibilities --id {id} --package {package_spec}`",
447 package_spec = format!("{}@{}", package_id.name(), package_id.version()),
448 url = manifest
449 .metadata()
450 .repository
451 .as_deref()
452 .unwrap_or("<not found>"),
453 id = report_id,
454 )
455 })
456 .collect::<Vec<_>>()
457 .join("\n\n");
458
459 let all_is_local = per_package_future_incompat_reports
460 .iter()
461 .all(|report| report.is_local);
462
463 let suggestion_header = "to solve this problem, you can try the following approaches:";
464 let mut suggestions = Vec::new();
465 if !all_is_local {
466 if !update_message.is_empty() {
467 suggestions.push(update_message);
468 }
469 suggestions.push(format!(
470 "\
471ensure the maintainers know of this problem (e.g. creating a bug report if needed)
472or even helping with a fix (e.g. by creating a pull request)
473{upstream_info}"
474 ));
475 suggestions.push(
476 "\
477use your own version of the dependency with the `[patch]` section in `Cargo.toml`
478For more information, see:
479https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section"
480 .to_owned(),
481 );
482 }
483
484 let suggestion_message = if suggestions.is_empty() {
485 String::new()
486 } else {
487 let mut suggestion_message = String::new();
488 writeln!(&mut suggestion_message, "{suggestion_header}").unwrap();
489 for suggestion in &suggestions {
490 writeln!(
491 &mut suggestion_message,
492 "
493- {suggestion}"
494 )
495 .unwrap();
496 }
497 suggestion_message
498 };
499 let saved_report_id =
500 current_reports.save_report(bcx.ws, suggestion_message.clone(), rendered_report);
501
502 if should_display_message || bcx.build_config.future_incompat_report {
503 use cargo_util_terminal::report::*;
504 let mut report = vec![Group::with_title(Level::WARNING.secondary_title(format!(
505 "the following packages contain code that will be rejected by a future \
506 version of Rust: {}",
507 package_vers.join(", ")
508 )))];
509 if bcx.build_config.future_incompat_report {
510 for suggestion in &suggestions {
511 report.push(Group::with_title(Level::HELP.secondary_title(suggestion)));
512 }
513 report.push(Group::with_title(Level::NOTE.secondary_title(format!(
514 "this report can be shown with `cargo report \
515 future-incompatibilities --id {}`",
516 saved_report_id
517 ))));
518 } else if should_display_message {
519 report.push(Group::with_title(Level::NOTE.secondary_title(format!(
520 "to see what the problems were, use the option \
521 `--future-incompat-report`, or run `cargo report \
522 future-incompatibilities --id {}`",
523 saved_report_id
524 ))));
525 }
526 drop(bcx.gctx.shell().print_report(&report, false))
527 }
528}