1use crate::core::compiler::BuildContext;
37use crate::core::{Dependency, PackageId, Workspace};
38use crate::sources::source::QueryKind;
39use crate::sources::SourceConfigMap;
40use crate::util::cache_lock::CacheLockMode;
41use crate::util::CargoResult;
42use anyhow::{bail, format_err, Context};
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 items: Vec<FutureBreakageItem>,
74}
75
76#[derive(Serialize, Deserialize)]
78pub struct FutureBreakageItem {
79 pub future_breakage_date: Option<String>,
82 pub diagnostic: Diagnostic,
84}
85
86#[derive(Serialize, Deserialize)]
89pub struct Diagnostic {
90 pub rendered: String,
91 pub level: String,
92}
93
94const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
97const MAX_REPORTS: usize = 5;
99
100#[derive(Serialize, Deserialize)]
102pub struct OnDiskReports {
103 version: u32,
106 next_id: u32,
108 reports: Vec<OnDiskReport>,
110}
111
112#[derive(Serialize, Deserialize)]
114struct OnDiskReport {
115 id: u32,
117 suggestion_message: String,
120 per_package: BTreeMap<String, String>,
125}
126
127impl Default for OnDiskReports {
128 fn default() -> OnDiskReports {
129 OnDiskReports {
130 version: ON_DISK_VERSION,
131 next_id: 1,
132 reports: Vec::new(),
133 }
134 }
135}
136
137impl OnDiskReports {
138 pub fn save_report(
140 mut self,
141 ws: &Workspace<'_>,
142 suggestion_message: String,
143 per_package_reports: &[FutureIncompatReportPackage],
144 ) -> u32 {
145 let per_package = render_report(per_package_reports);
146
147 if let Some(existing_report) = self
148 .reports
149 .iter()
150 .find(|existing| existing.per_package == per_package)
151 {
152 return existing_report.id;
153 }
154
155 let report = OnDiskReport {
156 id: self.next_id,
157 suggestion_message,
158 per_package,
159 };
160
161 let saved_id = report.id;
162 self.next_id += 1;
163 self.reports.push(report);
164 if self.reports.len() > MAX_REPORTS {
165 self.reports.remove(0);
166 }
167 let on_disk = serde_json::to_vec(&self).unwrap();
168 if let Err(e) = ws
169 .target_dir()
170 .open_rw_exclusive_create(
171 FUTURE_INCOMPAT_FILE,
172 ws.gctx(),
173 "Future incompatibility report",
174 )
175 .and_then(|file| {
176 let mut file = file.file();
177 file.set_len(0)?;
178 file.write_all(&on_disk)?;
179 Ok(())
180 })
181 {
182 crate::display_warning_with_error(
183 "failed to write on-disk future incompatible report",
184 &e,
185 &mut ws.gctx().shell(),
186 );
187 }
188
189 saved_id
190 }
191
192 pub fn load(ws: &Workspace<'_>) -> CargoResult<OnDiskReports> {
194 let report_file = match ws.target_dir().open_ro_shared(
195 FUTURE_INCOMPAT_FILE,
196 ws.gctx(),
197 "Future incompatible report",
198 ) {
199 Ok(r) => r,
200 Err(e) => {
201 if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
202 if io_err.kind() == std::io::ErrorKind::NotFound {
203 bail!("no reports are currently available");
204 }
205 }
206 return Err(e);
207 }
208 };
209
210 let mut file_contents = String::new();
211 report_file
212 .file()
213 .read_to_string(&mut file_contents)
214 .context("failed to read report")?;
215 let on_disk_reports: OnDiskReports =
216 serde_json::from_str(&file_contents).context("failed to load report")?;
217 if on_disk_reports.version != ON_DISK_VERSION {
218 bail!("unable to read reports; reports were saved from a future version of Cargo");
219 }
220 Ok(on_disk_reports)
221 }
222
223 pub fn last_id(&self) -> u32 {
225 self.reports.last().map(|r| r.id).unwrap()
226 }
227
228 pub fn get_report(&self, id: u32, package: Option<&str>) -> CargoResult<String> {
230 let report = self.reports.iter().find(|r| r.id == id).ok_or_else(|| {
231 let available = itertools::join(self.reports.iter().map(|r| r.id), ", ");
232 format_err!(
233 "could not find report with ID {}\n\
234 Available IDs are: {}",
235 id,
236 available
237 )
238 })?;
239
240 let mut to_display = report.suggestion_message.clone();
241 to_display += "\n";
242
243 let package_report = if let Some(package) = package {
244 report
245 .per_package
246 .get(package)
247 .ok_or_else(|| {
248 format_err!(
249 "could not find package with ID `{}`\n
250 Available packages are: {}\n
251 Omit the `--package` flag to display a report for all packages",
252 package,
253 itertools::join(report.per_package.keys(), ", ")
254 )
255 })?
256 .to_string()
257 } else {
258 report
259 .per_package
260 .values()
261 .cloned()
262 .collect::<Vec<_>>()
263 .join("\n")
264 };
265 to_display += &package_report;
266
267 Ok(to_display)
268 }
269}
270
271fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> BTreeMap<String, String> {
272 let mut report: BTreeMap<String, String> = BTreeMap::new();
273 for per_package in per_package_reports {
274 let package_spec = format!(
275 "{}@{}",
276 per_package.package_id.name(),
277 per_package.package_id.version()
278 );
279 let rendered = report.entry(package_spec).or_default();
280 rendered.push_str(&format!(
281 "The package `{}` currently triggers the following future incompatibility lints:\n",
282 per_package.package_id
283 ));
284 for item in &per_package.items {
285 rendered.extend(
286 item.diagnostic
287 .rendered
288 .lines()
289 .map(|l| format!("> {}\n", l)),
290 );
291 }
292 }
293 report
294}
295
296fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> Option<String> {
300 let _lock = ws
302 .gctx()
303 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)
304 .ok()?;
305 let map = SourceConfigMap::new(ws.gctx()).ok()?;
307 let mut package_ids: BTreeSet<_> = package_ids
308 .iter()
309 .filter(|pkg_id| pkg_id.source_id().is_registry())
310 .collect();
311 let source_ids: HashSet<_> = package_ids
312 .iter()
313 .map(|pkg_id| pkg_id.source_id())
314 .collect();
315 let mut sources: HashMap<_, _> = source_ids
316 .into_iter()
317 .filter_map(|sid| {
318 let source = map.load(sid, &HashSet::new()).ok()?;
319 Some((sid, source))
320 })
321 .collect();
322
323 let mut summaries = Vec::new();
325 while !package_ids.is_empty() {
326 package_ids.retain(|&pkg_id| {
327 let Some(source) = sources.get_mut(&pkg_id.source_id()) else {
328 return false;
329 };
330 let Ok(dep) = Dependency::parse(pkg_id.name(), None, pkg_id.source_id()) else {
331 return false;
332 };
333 match source.query_vec(&dep, QueryKind::Exact) {
334 Poll::Ready(Ok(sum)) => {
335 summaries.push((pkg_id, sum));
336 false
337 }
338 Poll::Ready(Err(_)) => false,
339 Poll::Pending => true,
340 }
341 });
342 for (_, source) in sources.iter_mut() {
343 source.block_until_ready().ok()?;
344 }
345 }
346
347 let mut updates = String::new();
348 for (pkg_id, summaries) in summaries {
349 let mut updated_versions: Vec<_> = summaries
350 .iter()
351 .map(|summary| summary.as_summary().version())
352 .filter(|version| *version > pkg_id.version())
353 .collect();
354 updated_versions.sort();
355
356 if !updated_versions.is_empty() {
357 let updated_versions = itertools::join(updated_versions, ", ");
358 writeln!(
359 updates,
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 let report_id = current_reports.next_id;
412
413 let package_ids: BTreeSet<_> = per_package_future_incompat_reports
415 .iter()
416 .map(|r| r.package_id)
417 .collect();
418 let package_vers: Vec<_> = package_ids.iter().map(|pid| pid.to_string()).collect();
419
420 if should_display_message || bcx.build_config.future_incompat_report {
421 drop(bcx.gctx.shell().warn(&format!(
422 "the following packages contain code that will be rejected by a future \
423 version of Rust: {}",
424 package_vers.join(", ")
425 )));
426 }
427
428 let updated_versions = get_updates(bcx.ws, &package_ids).unwrap_or(String::new());
429
430 let update_message = if !updated_versions.is_empty() {
431 format!(
432 "
433- Some affected dependencies have newer versions available.
434You may want to consider updating them to a newer version to see if the issue has been fixed.
435
436{updated_versions}\n",
437 updated_versions = updated_versions
438 )
439 } else {
440 String::new()
441 };
442
443 let upstream_info = package_ids
444 .iter()
445 .map(|package_id| {
446 let manifest = bcx.packages.get_one(*package_id).unwrap().manifest();
447 format!(
448 "
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");
463
464 let suggestion_message = format!(
465 "
466To solve this problem, you can try the following approaches:
467
468{update_message}
469- If the issue is not solved by updating the dependencies, a fix has to be
470implemented by those dependencies. You can help with that by notifying the
471maintainers of this problem (e.g. by creating a bug report) or by proposing a
472fix to the maintainers (e.g. by creating a pull request):
473{upstream_info}
474
475- If waiting for an upstream fix is not an option, you can use the `[patch]`
476section in `Cargo.toml` to use your own version of the dependency. For more
477information, see:
478https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section
479 ",
480 upstream_info = upstream_info,
481 update_message = update_message,
482 );
483
484 let saved_report_id = current_reports.save_report(
485 bcx.ws,
486 suggestion_message.clone(),
487 per_package_future_incompat_reports,
488 );
489
490 if bcx.build_config.future_incompat_report {
491 drop(bcx.gctx.shell().note(&suggestion_message));
492 drop(bcx.gctx.shell().note(&format!(
493 "this report can be shown with `cargo report \
494 future-incompatibilities --id {}`",
495 saved_report_id
496 )));
497 } else if should_display_message {
498 drop(bcx.gctx.shell().note(&format!(
499 "to see what the problems were, use the option \
500 `--future-incompat-report`, or run `cargo report \
501 future-incompatibilities --id {}`",
502 saved_report_id
503 )));
504 }
505}