]>
Commit | Line | Data |
---|---|---|
c2b02b39 EH |
1 | //! Support for future-incompatible warning reporting. |
2 | ||
f57be6f6 AH |
3 | use crate::core::compiler::BuildContext; |
4 | use crate::core::{Dependency, PackageId, Workspace}; | |
5 | use crate::sources::SourceConfigMap; | |
c2b02b39 EH |
6 | use crate::util::{iter_join, CargoResult, Config}; |
7 | use anyhow::{bail, format_err, Context}; | |
6177c658 | 8 | use serde::{Deserialize, Serialize}; |
f57be6f6 AH |
9 | use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; |
10 | use std::fmt::Write as _; | |
c2b02b39 | 11 | use std::io::{Read, Write}; |
82093ad9 | 12 | use std::task::Poll; |
6177c658 | 13 | |
71cb59b6 EH |
14 | pub const REPORT_PREAMBLE: &str = "\ |
15 | The following warnings were discovered during the build. These warnings are an | |
16 | indication that the packages contain code that will become an error in a | |
17 | future release of Rust. These warnings typically cover changes to close | |
18 | soundness problems, unintended or undocumented behavior, or critical problems | |
19 | that cannot be fixed in a backwards-compatible fashion, and are not expected | |
20 | to be in wide use. | |
21 | ||
22 | Each warning should contain a link for more information on what the warning | |
23 | means and how to resolve it. | |
24 | "; | |
25 | ||
7e2b31a6 EH |
26 | /// Current version of the on-disk format. |
27 | const ON_DISK_VERSION: u32 = 0; | |
28 | ||
6177c658 AH |
29 | /// The future incompatibility report, emitted by the compiler as a JSON message. |
30 | #[derive(serde::Deserialize)] | |
31 | pub struct FutureIncompatReport { | |
32 | pub future_incompat_report: Vec<FutureBreakageItem>, | |
33 | } | |
34 | ||
c2b02b39 EH |
35 | /// Structure used for collecting reports in-memory. |
36 | pub struct FutureIncompatReportPackage { | |
37 | pub package_id: PackageId, | |
38 | pub items: Vec<FutureBreakageItem>, | |
39 | } | |
40 | ||
41 | /// A single future-incompatible warning emitted by rustc. | |
6177c658 AH |
42 | #[derive(Serialize, Deserialize)] |
43 | pub struct FutureBreakageItem { | |
44 | /// The date at which this lint will become an error. | |
45 | /// Currently unused | |
46 | pub future_breakage_date: Option<String>, | |
47 | /// The original diagnostic emitted by the compiler | |
48 | pub diagnostic: Diagnostic, | |
49 | } | |
50 | ||
f03d47ce | 51 | /// A diagnostic emitted by the compiler as a JSON message. |
6177c658 AH |
52 | /// We only care about the 'rendered' field |
53 | #[derive(Serialize, Deserialize)] | |
54 | pub struct Diagnostic { | |
55 | pub rendered: String, | |
9c7cc545 | 56 | pub level: String, |
6177c658 AH |
57 | } |
58 | ||
59 | /// The filename in the top-level `target` directory where we store | |
60 | /// the report | |
c2b02b39 EH |
61 | const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json"; |
62 | /// Max number of reports to save on disk. | |
63 | const MAX_REPORTS: usize = 5; | |
64 | ||
65 | /// The structure saved to disk containing the reports. | |
66 | #[derive(Serialize, Deserialize)] | |
67 | pub struct OnDiskReports { | |
68 | /// A schema version number, to handle older cargo's from trying to read | |
69 | /// something that they don't understand. | |
70 | version: u32, | |
71 | /// The report ID to use for the next report to save. | |
72 | next_id: u32, | |
73 | /// Available reports. | |
74 | reports: Vec<OnDiskReport>, | |
75 | } | |
6177c658 | 76 | |
c2b02b39 | 77 | /// A single report for a given compilation session. |
6177c658 | 78 | #[derive(Serialize, Deserialize)] |
c2b02b39 EH |
79 | struct OnDiskReport { |
80 | /// Unique reference to the report for the `--id` CLI flag. | |
81 | id: u32, | |
958f2fc9 AH |
82 | /// A message describing suggestions for fixing the |
83 | /// reported issues | |
84 | suggestion_message: String, | |
c2b02b39 | 85 | /// Report, suitable for printing to the console. |
315d605b | 86 | /// Maps package names to the corresponding report |
d5538c38 AH |
87 | /// We use a `BTreeMap` so that the iteration order |
88 | /// is stable across multiple runs of `cargo` | |
315d605b | 89 | per_package: BTreeMap<String, String>, |
c2b02b39 EH |
90 | } |
91 | ||
92 | impl Default for OnDiskReports { | |
93 | fn default() -> OnDiskReports { | |
94 | OnDiskReports { | |
7e2b31a6 | 95 | version: ON_DISK_VERSION, |
c2b02b39 EH |
96 | next_id: 1, |
97 | reports: Vec::new(), | |
98 | } | |
99 | } | |
100 | } | |
101 | ||
102 | impl OnDiskReports { | |
103 | /// Saves a new report. | |
104 | pub fn save_report( | |
958f2fc9 | 105 | mut self, |
c2b02b39 | 106 | ws: &Workspace<'_>, |
958f2fc9 | 107 | suggestion_message: String, |
c2b02b39 | 108 | per_package_reports: &[FutureIncompatReportPackage], |
958f2fc9 | 109 | ) { |
c2b02b39 | 110 | let report = OnDiskReport { |
958f2fc9 AH |
111 | id: self.next_id, |
112 | suggestion_message, | |
315d605b | 113 | per_package: render_report(per_package_reports), |
c2b02b39 | 114 | }; |
958f2fc9 AH |
115 | self.next_id += 1; |
116 | self.reports.push(report); | |
117 | if self.reports.len() > MAX_REPORTS { | |
118 | self.reports.remove(0); | |
c2b02b39 | 119 | } |
958f2fc9 | 120 | let on_disk = serde_json::to_vec(&self).unwrap(); |
c2b02b39 EH |
121 | if let Err(e) = ws |
122 | .target_dir() | |
123 | .open_rw( | |
124 | FUTURE_INCOMPAT_FILE, | |
125 | ws.config(), | |
126 | "Future incompatibility report", | |
127 | ) | |
128 | .and_then(|file| { | |
129 | let mut file = file.file(); | |
130 | file.set_len(0)?; | |
131 | file.write_all(&on_disk)?; | |
132 | Ok(()) | |
133 | }) | |
134 | { | |
135 | crate::display_warning_with_error( | |
136 | "failed to write on-disk future incompatible report", | |
137 | &e, | |
138 | &mut ws.config().shell(), | |
139 | ); | |
140 | } | |
c2b02b39 EH |
141 | } |
142 | ||
143 | /// Loads the on-disk reports. | |
144 | pub fn load(ws: &Workspace<'_>) -> CargoResult<OnDiskReports> { | |
145 | let report_file = match ws.target_dir().open_ro( | |
146 | FUTURE_INCOMPAT_FILE, | |
147 | ws.config(), | |
148 | "Future incompatible report", | |
149 | ) { | |
150 | Ok(r) => r, | |
151 | Err(e) => { | |
152 | if let Some(io_err) = e.downcast_ref::<std::io::Error>() { | |
153 | if io_err.kind() == std::io::ErrorKind::NotFound { | |
154 | bail!("no reports are currently available"); | |
155 | } | |
156 | } | |
157 | return Err(e); | |
158 | } | |
159 | }; | |
160 | ||
161 | let mut file_contents = String::new(); | |
162 | report_file | |
163 | .file() | |
164 | .read_to_string(&mut file_contents) | |
165 | .with_context(|| "failed to read report")?; | |
166 | let on_disk_reports: OnDiskReports = | |
167 | serde_json::from_str(&file_contents).with_context(|| "failed to load report")?; | |
7e2b31a6 | 168 | if on_disk_reports.version != ON_DISK_VERSION { |
c2b02b39 EH |
169 | bail!("unable to read reports; reports were saved from a future version of Cargo"); |
170 | } | |
171 | Ok(on_disk_reports) | |
172 | } | |
173 | ||
174 | /// Returns the most recent report ID. | |
175 | pub fn last_id(&self) -> u32 { | |
176 | self.reports.last().map(|r| r.id).unwrap() | |
177 | } | |
178 | ||
d5538c38 AH |
179 | pub fn get_report( |
180 | &self, | |
181 | id: u32, | |
182 | config: &Config, | |
183 | package: Option<&str>, | |
184 | ) -> CargoResult<String> { | |
c2b02b39 EH |
185 | let report = self.reports.iter().find(|r| r.id == id).ok_or_else(|| { |
186 | let available = iter_join(self.reports.iter().map(|r| r.id.to_string()), ", "); | |
187 | format_err!( | |
188 | "could not find report with ID {}\n\ | |
189 | Available IDs are: {}", | |
190 | id, | |
191 | available | |
192 | ) | |
193 | })?; | |
6f185070 | 194 | |
958f2fc9 AH |
195 | let mut to_display = report.suggestion_message.clone(); |
196 | to_display += "\n"; | |
6f185070 AH |
197 | |
198 | let package_report = if let Some(package) = package { | |
d5538c38 | 199 | report |
315d605b | 200 | .per_package |
d5538c38 AH |
201 | .get(package) |
202 | .ok_or_else(|| { | |
203 | format_err!( | |
204 | "could not find package with ID `{}`\n | |
205 | Available packages are: {}\n | |
f57be6f6 | 206 | Omit the `--package` flag to display a report for all packages", |
d5538c38 | 207 | package, |
315d605b | 208 | iter_join(report.per_package.keys(), ", ") |
d5538c38 AH |
209 | ) |
210 | })? | |
6f185070 | 211 | .to_string() |
c2b02b39 | 212 | } else { |
d5538c38 | 213 | report |
315d605b | 214 | .per_package |
d5538c38 AH |
215 | .values() |
216 | .cloned() | |
217 | .collect::<Vec<_>>() | |
218 | .join("\n") | |
219 | }; | |
6f185070 AH |
220 | to_display += &package_report; |
221 | ||
622b43aa DSW |
222 | let shell = config.shell(); |
223 | ||
224 | let to_display = if shell.err_supports_color() && shell.out_supports_color() { | |
d5538c38 AH |
225 | to_display |
226 | } else { | |
227 | strip_ansi_escapes::strip(&to_display) | |
c2b02b39 EH |
228 | .map(|v| String::from_utf8(v).expect("utf8")) |
229 | .expect("strip should never fail") | |
230 | }; | |
d5538c38 | 231 | Ok(to_display) |
c2b02b39 EH |
232 | } |
233 | } | |
234 | ||
d5538c38 AH |
235 | fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> BTreeMap<String, String> { |
236 | let mut report: BTreeMap<String, String> = BTreeMap::new(); | |
237 | for per_package in per_package_reports { | |
5afcce6b AH |
238 | let package_spec = format!( |
239 | "{}:{}", | |
240 | per_package.package_id.name(), | |
241 | per_package.package_id.version() | |
242 | ); | |
243 | let rendered = report.entry(package_spec).or_default(); | |
c2b02b39 | 244 | rendered.push_str(&format!( |
7ffba677 | 245 | "The package `{}` currently triggers the following future incompatibility lints:\n", |
c2b02b39 EH |
246 | per_package.package_id |
247 | )); | |
248 | for item in &per_package.items { | |
249 | rendered.extend( | |
250 | item.diagnostic | |
251 | .rendered | |
252 | .lines() | |
253 | .map(|l| format!("> {}\n", l)), | |
254 | ); | |
255 | } | |
71cb59b6 | 256 | } |
d5538c38 | 257 | report |
71cb59b6 | 258 | } |
f57be6f6 | 259 | |
5c3b3808 AH |
260 | /// Returns a user-readable message explaining which of |
261 | /// the packages in `package_ids` have updates available. | |
262 | /// This is best-effort - if an error occurs, `None` will be returned. | |
f57be6f6 AH |
263 | fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> Option<String> { |
264 | // This in general ignores all errors since this is opportunistic. | |
265 | let _lock = ws.config().acquire_package_cache_lock().ok()?; | |
266 | // Create a set of updated registry sources. | |
267 | let map = SourceConfigMap::new(ws.config()).ok()?; | |
82093ad9 | 268 | let mut package_ids: BTreeSet<_> = package_ids |
f57be6f6 AH |
269 | .iter() |
270 | .filter(|pkg_id| pkg_id.source_id().is_registry()) | |
271 | .collect(); | |
272 | let source_ids: HashSet<_> = package_ids | |
273 | .iter() | |
274 | .map(|pkg_id| pkg_id.source_id()) | |
275 | .collect(); | |
276 | let mut sources: HashMap<_, _> = source_ids | |
277 | .into_iter() | |
278 | .filter_map(|sid| { | |
279 | let source = map.load(sid, &HashSet::new()).ok()?; | |
280 | Some((sid, source)) | |
281 | }) | |
282 | .collect(); | |
82093ad9 | 283 | |
f12f0256 | 284 | // Query the sources for new versions, mapping `package_ids` into `summaries`. |
82093ad9 AS |
285 | let mut summaries = Vec::new(); |
286 | while !package_ids.is_empty() { | |
287 | package_ids.retain(|&pkg_id| { | |
288 | let source = match sources.get_mut(&pkg_id.source_id()) { | |
289 | Some(s) => s, | |
290 | None => return false, | |
291 | }; | |
292 | let dep = match Dependency::parse(pkg_id.name(), None, pkg_id.source_id()) { | |
293 | Ok(dep) => dep, | |
294 | Err(_) => return false, | |
295 | }; | |
296 | match source.query_vec(&dep) { | |
297 | Poll::Ready(Ok(sum)) => { | |
298 | summaries.push((pkg_id, sum)); | |
299 | false | |
300 | } | |
301 | Poll::Ready(Err(_)) => false, | |
302 | Poll::Pending => true, | |
303 | } | |
304 | }); | |
305 | for (_, source) in sources.iter_mut() { | |
306 | source.block_until_ready().ok()?; | |
307 | } | |
308 | } | |
309 | ||
f57be6f6 | 310 | let mut updates = String::new(); |
82093ad9 | 311 | for (pkg_id, summaries) in summaries { |
f57be6f6 AH |
312 | let mut updated_versions: Vec<_> = summaries |
313 | .iter() | |
314 | .map(|summary| summary.version()) | |
315 | .filter(|version| *version > pkg_id.version()) | |
316 | .collect(); | |
317 | updated_versions.sort(); | |
318 | ||
319 | let updated_versions = iter_join( | |
320 | updated_versions | |
321 | .into_iter() | |
322 | .map(|version| version.to_string()), | |
323 | ", ", | |
324 | ); | |
325 | ||
326 | if !updated_versions.is_empty() { | |
327 | writeln!( | |
328 | updates, | |
329 | "{} has the following newer versions available: {}", | |
330 | pkg_id, updated_versions | |
331 | ) | |
332 | .unwrap(); | |
333 | } | |
334 | } | |
335 | Some(updates) | |
336 | } | |
337 | ||
5c3b3808 AH |
338 | /// Writes a future-incompat report to disk, using the per-package |
339 | /// reports gathered during the build. If requested by the user, | |
340 | /// a message is also displayed in the build output. | |
341 | pub fn save_and_display_report( | |
f57be6f6 AH |
342 | bcx: &BuildContext<'_, '_>, |
343 | per_package_future_incompat_reports: &[FutureIncompatReportPackage], | |
344 | ) { | |
f57be6f6 AH |
345 | let should_display_message = match bcx.config.future_incompat_config() { |
346 | Ok(config) => config.should_display_message(), | |
347 | Err(e) => { | |
348 | crate::display_warning_with_error( | |
349 | "failed to read future-incompat config from disk", | |
350 | &e, | |
351 | &mut bcx.config.shell(), | |
352 | ); | |
353 | true | |
354 | } | |
355 | }; | |
356 | ||
357 | if per_package_future_incompat_reports.is_empty() { | |
358 | // Explicitly passing a command-line flag overrides | |
359 | // `should_display_message` from the config file | |
360 | if bcx.build_config.future_incompat_report { | |
361 | drop( | |
362 | bcx.config | |
363 | .shell() | |
364 | .note("0 dependencies had future-incompatible warnings"), | |
365 | ); | |
366 | } | |
367 | return; | |
368 | } | |
369 | ||
958f2fc9 AH |
370 | let current_reports = match OnDiskReports::load(bcx.ws) { |
371 | Ok(r) => r, | |
372 | Err(e) => { | |
373 | log::debug!( | |
374 | "saving future-incompatible reports failed to load current reports: {:?}", | |
375 | e | |
376 | ); | |
377 | OnDiskReports::default() | |
378 | } | |
379 | }; | |
380 | let report_id = current_reports.next_id; | |
381 | ||
f57be6f6 AH |
382 | // Get a list of unique and sorted package name/versions. |
383 | let package_ids: BTreeSet<_> = per_package_future_incompat_reports | |
384 | .iter() | |
385 | .map(|r| r.package_id) | |
386 | .collect(); | |
387 | let package_vers: Vec<_> = package_ids.iter().map(|pid| pid.to_string()).collect(); | |
388 | ||
389 | if should_display_message || bcx.build_config.future_incompat_report { | |
390 | drop(bcx.config.shell().warn(&format!( | |
391 | "the following packages contain code that will be rejected by a future \ | |
392 | version of Rust: {}", | |
393 | package_vers.join(", ") | |
394 | ))); | |
395 | } | |
396 | ||
397 | let updated_versions = get_updates(bcx.ws, &package_ids).unwrap_or(String::new()); | |
398 | ||
399 | let update_message = if !updated_versions.is_empty() { | |
400 | format!( | |
401 | " | |
402 | - Some affected dependencies have newer versions available. | |
403 | You may want to consider updating them to a newer version to see if the issue has been fixed. | |
404 | ||
405 | {updated_versions}\n", | |
406 | updated_versions = updated_versions | |
407 | ) | |
408 | } else { | |
409 | String::new() | |
410 | }; | |
411 | ||
958f2fc9 AH |
412 | let upstream_info = package_ids |
413 | .iter() | |
414 | .map(|package_id| { | |
415 | let manifest = bcx.packages.get_one(*package_id).unwrap().manifest(); | |
416 | format!( | |
417 | " | |
f57be6f6 AH |
418 | - {name} |
419 | - Repository: {url} | |
085f04ad | 420 | - Detailed warning command: `cargo report future-incompatibilities --id {id} --package {name}`", |
958f2fc9 AH |
421 | name = format!("{}:{}", package_id.name(), package_id.version()), |
422 | url = manifest | |
423 | .metadata() | |
424 | .repository | |
425 | .as_deref() | |
426 | .unwrap_or("<not found>"), | |
427 | id = report_id, | |
428 | ) | |
429 | }) | |
430 | .collect::<Vec<_>>() | |
7ffba677 | 431 | .join("\n"); |
958f2fc9 AH |
432 | |
433 | let suggestion_message = format!( | |
7ffba677 | 434 | " |
f57be6f6 AH |
435 | To solve this problem, you can try the following approaches: |
436 | ||
437 | {update_message} | |
438 | - If the issue is not solved by updating the dependencies, a fix has to be | |
439 | implemented by those dependencies. You can help with that by notifying the | |
440 | maintainers of this problem (e.g. by creating a bug report) or by proposing a | |
441 | fix to the maintainers (e.g. by creating a pull request): | |
442 | {upstream_info} | |
443 | ||
444 | - If waiting for an upstream fix is not an option, you can use the `[patch]` | |
445 | section in `Cargo.toml` to use your own version of the dependency. For more | |
446 | information, see: | |
447 | https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section | |
448 | ", | |
7ffba677 AH |
449 | upstream_info = upstream_info, |
450 | update_message = update_message, | |
958f2fc9 AH |
451 | ); |
452 | ||
958f2fc9 AH |
453 | current_reports.save_report( |
454 | bcx.ws, | |
455 | suggestion_message.clone(), | |
456 | per_package_future_incompat_reports, | |
457 | ); | |
458 | ||
459 | if bcx.build_config.future_incompat_report { | |
460 | drop(bcx.config.shell().note(&suggestion_message)); | |
f57be6f6 AH |
461 | drop(bcx.config.shell().note(&format!( |
462 | "this report can be shown with `cargo report \ | |
2a43df61 | 463 | future-incompatibilities --id {}`", |
f57be6f6 AH |
464 | report_id |
465 | ))); | |
466 | } else if should_display_message { | |
467 | drop(bcx.config.shell().note(&format!( | |
468 | "to see what the problems were, use the option \ | |
469 | `--future-incompat-report`, or run `cargo report \ | |
470 | future-incompatibilities --id {}`", | |
471 | report_id | |
472 | ))); | |
473 | } | |
474 | } |