• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! Checks the licenses of third-party dependencies.
2 
3 use cargo_metadata::{DepKindInfo, Metadata, Package, PackageId};
4 use std::collections::HashSet;
5 use std::path::Path;
6 
7 /// These are licenses that are allowed for all crates, including the runtime,
8 /// rustc, tools, etc.
9 #[rustfmt::skip]
10 const LICENSES: &[&str] = &[
11     // tidy-alphabetical-start
12     "(MIT OR Apache-2.0) AND Unicode-DFS-2016",            // unicode_ident
13     "0BSD OR MIT OR Apache-2.0",                           // adler license
14     "0BSD",
15     "Apache-2.0 / MIT",
16     "Apache-2.0 OR MIT",
17     "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT", // wasi license
18     "Apache-2.0/MIT",
19     "ISC",
20     "MIT / Apache-2.0",
21     "MIT OR Apache-2.0 OR Zlib",                           // tinyvec_macros
22     "MIT OR Apache-2.0",
23     "MIT OR Zlib OR Apache-2.0",                           // miniz_oxide
24     "MIT",
25     "MIT/Apache-2.0",
26     "Unicode-DFS-2016",                                    // tinystr and icu4x
27     "Unlicense OR MIT",
28     "Unlicense/MIT",
29     "Zlib OR Apache-2.0 OR MIT",                           // tinyvec
30     // tidy-alphabetical-end
31 ];
32 
33 /// These are exceptions to Rust's permissive licensing policy, and
34 /// should be considered bugs. Exceptions are only allowed in Rust
35 /// tooling. It is _crucial_ that no exception crates be dependencies
36 /// of the Rust runtime (std/test).
37 #[rustfmt::skip]
38 const EXCEPTIONS: &[(&str, &str)] = &[
39     // tidy-alphabetical-start
40     ("ar_archive_writer", "Apache-2.0 WITH LLVM-exception"), // rustc
41     ("colored", "MPL-2.0"),                                  // rustfmt
42     ("dissimilar", "Apache-2.0"),                            // rustdoc, rustc_lexer (few tests) via expect-test, (dev deps)
43     ("fluent-langneg", "Apache-2.0"),                        // rustc (fluent translations)
44     ("fortanix-sgx-abi", "MPL-2.0"),                         // libstd but only for `sgx` target. FIXME: this dependency violates the documentation comment above.
45     ("instant", "BSD-3-Clause"),                             // rustc_driver/tracing-subscriber/parking_lot
46     ("mdbook", "MPL-2.0"),                                   // mdbook
47     ("ryu", "Apache-2.0 OR BSL-1.0"),                        // cargo/... (because of serde)
48     ("self_cell", "Apache-2.0"),                             // rustc (fluent translations)
49     ("snap", "BSD-3-Clause"),                                // rustc
50     // tidy-alphabetical-end
51 ];
52 
53 const EXCEPTIONS_CARGO: &[(&str, &str)] = &[
54     // tidy-alphabetical-start
55     ("bitmaps", "MPL-2.0+"),
56     ("bytesize", "Apache-2.0"),
57     ("dunce", "CC0-1.0 OR MIT-0 OR Apache-2.0"),
58     ("fiat-crypto", "MIT OR Apache-2.0 OR BSD-1-Clause"),
59     ("im-rc", "MPL-2.0+"),
60     ("imara-diff", "Apache-2.0"),
61     ("instant", "BSD-3-Clause"),
62     ("normalize-line-endings", "Apache-2.0"),
63     ("openssl", "Apache-2.0"),
64     ("ryu", "Apache-2.0 OR BSL-1.0"),
65     ("sha1_smol", "BSD-3-Clause"),
66     ("similar", "Apache-2.0"),
67     ("sized-chunks", "MPL-2.0+"),
68     ("subtle", "BSD-3-Clause"),
69     ("unicode-bom", "Apache-2.0"),
70     // tidy-alphabetical-end
71 ];
72 
73 const EXCEPTIONS_CRANELIFT: &[(&str, &str)] = &[
74     // tidy-alphabetical-start
75     ("cranelift-bforest", "Apache-2.0 WITH LLVM-exception"),
76     ("cranelift-codegen", "Apache-2.0 WITH LLVM-exception"),
77     ("cranelift-codegen-meta", "Apache-2.0 WITH LLVM-exception"),
78     ("cranelift-codegen-shared", "Apache-2.0 WITH LLVM-exception"),
79     ("cranelift-control", "Apache-2.0 WITH LLVM-exception"),
80     ("cranelift-entity", "Apache-2.0 WITH LLVM-exception"),
81     ("cranelift-frontend", "Apache-2.0 WITH LLVM-exception"),
82     ("cranelift-isle", "Apache-2.0 WITH LLVM-exception"),
83     ("cranelift-jit", "Apache-2.0 WITH LLVM-exception"),
84     ("cranelift-module", "Apache-2.0 WITH LLVM-exception"),
85     ("cranelift-native", "Apache-2.0 WITH LLVM-exception"),
86     ("cranelift-object", "Apache-2.0 WITH LLVM-exception"),
87     ("mach", "BSD-2-Clause"),
88     ("regalloc2", "Apache-2.0 WITH LLVM-exception"),
89     ("target-lexicon", "Apache-2.0 WITH LLVM-exception"),
90     ("wasmtime-jit-icache-coherence", "Apache-2.0 WITH LLVM-exception"),
91     // tidy-alphabetical-end
92 ];
93 
94 const EXCEPTIONS_BOOTSTRAP: &[(&str, &str)] = &[
95     ("ryu", "Apache-2.0 OR BSL-1.0"), // through serde
96 ];
97 
98 /// These are the root crates that are part of the runtime. The licenses for
99 /// these and all their dependencies *must not* be in the exception list.
100 const RUNTIME_CRATES: &[&str] = &["std", "core", "alloc", "test", "panic_abort", "panic_unwind"];
101 
102 const PERMITTED_DEPS_LOCATION: &str = concat!(file!(), ":", line!());
103 
104 /// Crates rustc is allowed to depend on. Avoid adding to the list if possible.
105 ///
106 /// This list is here to provide a speed-bump to adding a new dependency to
107 /// rustc. Please check with the compiler team before adding an entry.
108 const PERMITTED_RUSTC_DEPENDENCIES: &[&str] = &[
109     // tidy-alphabetical-start
110     "addr2line",
111     "adler",
112     "ahash",
113     "aho-corasick",
114     "allocator-api2", // FIXME: only appears in Cargo.lock due to https://github.com/rust-lang/cargo/issues/10801
115     "annotate-snippets",
116     "ar_archive_writer",
117     "arrayvec",
118     "atty",
119     "autocfg",
120     "bitflags",
121     "block-buffer",
122     "byteorder", // via ruzstd in object in thorin-dwp
123     "cc",
124     "cfg-if",
125     "compiler_builtins",
126     "convert_case", // dependency of derive_more
127     "cpufeatures",
128     "crc32fast",
129     "crossbeam-channel",
130     "crossbeam-deque",
131     "crossbeam-epoch",
132     "crossbeam-utils",
133     "crypto-common",
134     "cstr",
135     "datafrog",
136     "derive_more",
137     "digest",
138     "displaydoc",
139     "dissimilar",
140     "dlmalloc",
141     "either",
142     "elsa",
143     "ena",
144     "equivalent",
145     "expect-test",
146     "fallible-iterator", // dependency of `thorin`
147     "fastrand",
148     "field-offset",
149     "flate2",
150     "fluent-bundle",
151     "fluent-langneg",
152     "fluent-syntax",
153     "fortanix-sgx-abi",
154     "generic-array",
155     "getopts",
156     "getrandom",
157     "gimli",
158     "gsgdt",
159     "hashbrown",
160     "hermit-abi",
161     "icu_list",
162     "icu_locid",
163     "icu_provider",
164     "icu_provider_adapters",
165     "icu_provider_macros",
166     "indexmap",
167     "instant",
168     "intl-memoizer",
169     "intl_pluralrules",
170     "io-lifetimes",
171     "itertools",
172     "itoa",
173     "jobserver",
174     "lazy_static",
175     "libc",
176     "libloading",
177     "linux-raw-sys",
178     "litemap",
179     "lock_api",
180     "log",
181     "matchers",
182     "md-5",
183     "measureme",
184     "memchr",
185     "memmap2",
186     "memoffset",
187     "miniz_oxide",
188     "nu-ansi-term",
189     "num_cpus",
190     "object",
191     "odht",
192     "once_cell",
193     "overload",
194     "parking_lot",
195     "parking_lot_core",
196     "pathdiff",
197     "perf-event-open-sys",
198     "pin-project-lite",
199     "polonius-engine",
200     "ppv-lite86",
201     "proc-macro-hack",
202     "proc-macro2",
203     "psm",
204     "pulldown-cmark",
205     "punycode",
206     "quote",
207     "rand",
208     "rand_chacha",
209     "rand_core",
210     "rand_xorshift",
211     "rand_xoshiro",
212     "redox_syscall",
213     "regex",
214     "regex-automata",
215     "regex-syntax",
216     "rustc-demangle",
217     "rustc-hash",
218     "rustc-rayon",
219     "rustc-rayon-core",
220     "rustc_version",
221     "rustix",
222     "ruzstd", // via object in thorin-dwp
223     "ryu",
224     "scoped-tls",
225     "scopeguard",
226     "self_cell",
227     "semver",
228     "serde",
229     "serde_derive",
230     "serde_json",
231     "sha1",
232     "sha2",
233     "sharded-slab",
234     "smallvec",
235     "snap",
236     "stable_deref_trait",
237     "stacker",
238     "static_assertions",
239     "syn",
240     "synstructure",
241     "tempfile",
242     "termcolor",
243     "termize",
244     "thin-vec",
245     "thiserror",
246     "thiserror-impl",
247     "thorin-dwp",
248     "thread_local",
249     "tinystr",
250     "tinyvec",
251     "tinyvec_macros",
252     "tracing",
253     "tracing-attributes",
254     "tracing-core",
255     "tracing-log",
256     "tracing-subscriber",
257     "tracing-tree",
258     "twox-hash",
259     "type-map",
260     "typenum",
261     "unic-char-property",
262     "unic-char-range",
263     "unic-common",
264     "unic-emoji-char",
265     "unic-langid",
266     "unic-langid-impl",
267     "unic-langid-macros",
268     "unic-langid-macros-impl",
269     "unic-ucd-version",
270     "unicase",
271     "unicode-ident",
272     "unicode-normalization",
273     "unicode-script",
274     "unicode-security",
275     "unicode-width",
276     "unicode-xid",
277     "valuable",
278     "version_check",
279     "wasi",
280     "winapi",
281     "winapi-i686-pc-windows-gnu",
282     "winapi-util",
283     "winapi-x86_64-pc-windows-gnu",
284     "windows",
285     "windows-sys",
286     "windows-targets",
287     "windows_aarch64_gnullvm",
288     "windows_aarch64_msvc",
289     "windows_i686_gnu",
290     "windows_i686_msvc",
291     "windows_x86_64_gnu",
292     "windows_x86_64_gnullvm",
293     "windows_x86_64_msvc",
294     "writeable",
295     "yansi-term", // this is a false-positive: it's only used by rustfmt, but because it's enabled through a feature, tidy thinks it's used by rustc as well.
296     "yoke",
297     "yoke-derive",
298     "zerofrom",
299     "zerofrom-derive",
300     "zerovec",
301     "zerovec-derive",
302     // tidy-alphabetical-end
303 ];
304 
305 const PERMITTED_CRANELIFT_DEPENDENCIES: &[&str] = &[
306     // tidy-alphabetical-start
307     "ahash",
308     "anyhow",
309     "arbitrary",
310     "autocfg",
311     "bitflags",
312     "bumpalo",
313     "cfg-if",
314     "cranelift-bforest",
315     "cranelift-codegen",
316     "cranelift-codegen-meta",
317     "cranelift-codegen-shared",
318     "cranelift-control",
319     "cranelift-entity",
320     "cranelift-frontend",
321     "cranelift-isle",
322     "cranelift-jit",
323     "cranelift-module",
324     "cranelift-native",
325     "cranelift-object",
326     "crc32fast",
327     "fallible-iterator",
328     "gimli",
329     "hashbrown",
330     "indexmap",
331     "libc",
332     "libloading",
333     "log",
334     "mach",
335     "memchr",
336     "object",
337     "regalloc2",
338     "region",
339     "rustc-hash",
340     "slice-group-by",
341     "smallvec",
342     "stable_deref_trait",
343     "target-lexicon",
344     "version_check",
345     "wasmtime-jit-icache-coherence",
346     "winapi",
347     "winapi-i686-pc-windows-gnu",
348     "winapi-x86_64-pc-windows-gnu",
349     "windows-sys",
350     "windows-targets",
351     "windows_aarch64_gnullvm",
352     "windows_aarch64_msvc",
353     "windows_i686_gnu",
354     "windows_i686_msvc",
355     "windows_x86_64_gnu",
356     "windows_x86_64_gnullvm",
357     "windows_x86_64_msvc",
358     // tidy-alphabetical-end
359 ];
360 
361 /// Dependency checks.
362 ///
363 /// `root` is path to the directory with the root `Cargo.toml` (for the workspace). `cargo` is path
364 /// to the cargo executable.
check(root: &Path, cargo: &Path, bad: &mut bool)365 pub fn check(root: &Path, cargo: &Path, bad: &mut bool) {
366     let mut cmd = cargo_metadata::MetadataCommand::new();
367     cmd.cargo_path(cargo)
368         .manifest_path(root.join("Cargo.toml"))
369         .features(cargo_metadata::CargoOpt::AllFeatures);
370     let metadata = t!(cmd.exec());
371     let runtime_ids = compute_runtime_crates(&metadata);
372     check_license_exceptions(&metadata, EXCEPTIONS, runtime_ids, bad);
373     check_permitted_dependencies(
374         &metadata,
375         "rustc",
376         PERMITTED_RUSTC_DEPENDENCIES,
377         &["rustc_driver", "rustc_codegen_llvm"],
378         bad,
379     );
380 
381     // Check cargo independently as it has it's own workspace.
382     let mut cmd = cargo_metadata::MetadataCommand::new();
383     cmd.cargo_path(cargo)
384         .manifest_path(root.join("src/tools/cargo/Cargo.toml"))
385         .features(cargo_metadata::CargoOpt::AllFeatures);
386     let cargo_metadata = t!(cmd.exec());
387     let runtime_ids = HashSet::new();
388     check_license_exceptions(&cargo_metadata, EXCEPTIONS_CARGO, runtime_ids, bad);
389     check_rustfix(&metadata, &cargo_metadata, bad);
390 
391     // Check rustc_codegen_cranelift independently as it has it's own workspace.
392     let mut cmd = cargo_metadata::MetadataCommand::new();
393     cmd.cargo_path(cargo)
394         .manifest_path(root.join("compiler/rustc_codegen_cranelift/Cargo.toml"))
395         .features(cargo_metadata::CargoOpt::AllFeatures);
396     let metadata = t!(cmd.exec());
397     let runtime_ids = HashSet::new();
398     check_license_exceptions(&metadata, EXCEPTIONS_CRANELIFT, runtime_ids, bad);
399     check_permitted_dependencies(
400         &metadata,
401         "cranelift",
402         PERMITTED_CRANELIFT_DEPENDENCIES,
403         &["rustc_codegen_cranelift"],
404         bad,
405     );
406 
407     let mut cmd = cargo_metadata::MetadataCommand::new();
408     cmd.cargo_path(cargo)
409         .manifest_path(root.join("src/bootstrap/Cargo.toml"))
410         .features(cargo_metadata::CargoOpt::AllFeatures);
411     let metadata = t!(cmd.exec());
412     let runtime_ids = HashSet::new();
413     check_license_exceptions(&metadata, EXCEPTIONS_BOOTSTRAP, runtime_ids, bad);
414 }
415 
416 /// Check that all licenses are in the valid list in `LICENSES`.
417 ///
418 /// Packages listed in `exceptions` are allowed for tools.
check_license_exceptions( metadata: &Metadata, exceptions: &[(&str, &str)], runtime_ids: HashSet<&PackageId>, bad: &mut bool, )419 fn check_license_exceptions(
420     metadata: &Metadata,
421     exceptions: &[(&str, &str)],
422     runtime_ids: HashSet<&PackageId>,
423     bad: &mut bool,
424 ) {
425     // Validate the EXCEPTIONS list hasn't changed.
426     for (name, license) in exceptions {
427         // Check that the package actually exists.
428         if !metadata.packages.iter().any(|p| p.name == *name) {
429             tidy_error!(
430                 bad,
431                 "could not find exception package `{}`\n\
432                 Remove from EXCEPTIONS list if it is no longer used.",
433                 name
434             );
435         }
436         // Check that the license hasn't changed.
437         for pkg in metadata.packages.iter().filter(|p| p.name == *name) {
438             match &pkg.license {
439                 None => {
440                     tidy_error!(
441                         bad,
442                         "dependency exception `{}` does not declare a license expression",
443                         pkg.id
444                     );
445                 }
446                 Some(pkg_license) => {
447                     if pkg_license.as_str() != *license {
448                         println!("dependency exception `{name}` license has changed");
449                         println!("    previously `{license}` now `{pkg_license}`");
450                         println!("    update EXCEPTIONS for the new license");
451                         *bad = true;
452                     }
453                 }
454             }
455         }
456     }
457 
458     let exception_names: Vec<_> = exceptions.iter().map(|(name, _license)| *name).collect();
459 
460     // Check if any package does not have a valid license.
461     for pkg in &metadata.packages {
462         if pkg.source.is_none() {
463             // No need to check local packages.
464             continue;
465         }
466         if !runtime_ids.contains(&pkg.id) && exception_names.contains(&pkg.name.as_str()) {
467             continue;
468         }
469         let license = match &pkg.license {
470             Some(license) => license,
471             None => {
472                 tidy_error!(bad, "dependency `{}` does not define a license expression", pkg.id);
473                 continue;
474             }
475         };
476         if !LICENSES.contains(&license.as_str()) {
477             if pkg.name == "fortanix-sgx-abi" {
478                 // This is a specific exception because SGX is considered
479                 // "third party". See
480                 // https://github.com/rust-lang/rust/issues/62620 for more. In
481                 // general, these should never be added.
482                 continue;
483             }
484             tidy_error!(bad, "invalid license `{}` in `{}`", license, pkg.id);
485         }
486     }
487 }
488 
489 /// Checks the dependency of `restricted_dependency_crates` at the given path. Changes `bad` to
490 /// `true` if a check failed.
491 ///
492 /// Specifically, this checks that the dependencies are on the `permitted_dependencies`.
check_permitted_dependencies( metadata: &Metadata, descr: &str, permitted_dependencies: &[&'static str], restricted_dependency_crates: &[&'static str], bad: &mut bool, )493 fn check_permitted_dependencies(
494     metadata: &Metadata,
495     descr: &str,
496     permitted_dependencies: &[&'static str],
497     restricted_dependency_crates: &[&'static str],
498     bad: &mut bool,
499 ) {
500     let mut has_permitted_dep_error = false;
501     let mut deps = HashSet::new();
502     for to_check in restricted_dependency_crates {
503         let to_check = pkg_from_name(metadata, to_check);
504         use cargo_platform::Cfg;
505         use std::str::FromStr;
506         // We don't expect the compiler to ever run on wasm32, so strip
507         // out those dependencies to avoid polluting the permitted list.
508         deps_of_filtered(metadata, &to_check.id, &mut deps, &|dep_kinds| {
509             dep_kinds.iter().any(|dep_kind| {
510                 dep_kind
511                     .target
512                     .as_ref()
513                     .map(|target| {
514                         !target.matches(
515                             "wasm32-unknown-unknown",
516                             &[
517                                 Cfg::from_str("target_arch=\"wasm32\"").unwrap(),
518                                 Cfg::from_str("target_os=\"unknown\"").unwrap(),
519                             ],
520                         )
521                     })
522                     .unwrap_or(true)
523             })
524         });
525     }
526 
527     // Check that the PERMITTED_DEPENDENCIES does not have unused entries.
528     for permitted in permitted_dependencies {
529         if !deps.iter().any(|dep_id| &pkg_from_id(metadata, dep_id).name == permitted) {
530             tidy_error!(
531                 bad,
532                 "could not find allowed package `{permitted}`\n\
533                 Remove from PERMITTED_DEPENDENCIES list if it is no longer used.",
534             );
535             has_permitted_dep_error = true;
536         }
537     }
538 
539     // Get in a convenient form.
540     let permitted_dependencies: HashSet<_> = permitted_dependencies.iter().cloned().collect();
541 
542     for dep in deps {
543         let dep = pkg_from_id(metadata, dep);
544         // If this path is in-tree, we don't require it to be explicitly permitted.
545         if dep.source.is_some() {
546             if !permitted_dependencies.contains(dep.name.as_str()) {
547                 tidy_error!(bad, "Dependency for {descr} not explicitly permitted: {}", dep.id);
548                 has_permitted_dep_error = true;
549             }
550         }
551     }
552 
553     if has_permitted_dep_error {
554         eprintln!("Go to `{PERMITTED_DEPS_LOCATION}` for the list.");
555     }
556 }
557 
558 /// Finds a package with the given name.
pkg_from_name<'a>(metadata: &'a Metadata, name: &'static str) -> &'a Package559 fn pkg_from_name<'a>(metadata: &'a Metadata, name: &'static str) -> &'a Package {
560     let mut i = metadata.packages.iter().filter(|p| p.name == name);
561     let result =
562         i.next().unwrap_or_else(|| panic!("could not find package `{name}` in package list"));
563     assert!(i.next().is_none(), "more than one package found for `{name}`");
564     result
565 }
566 
pkg_from_id<'a>(metadata: &'a Metadata, id: &PackageId) -> &'a Package567 fn pkg_from_id<'a>(metadata: &'a Metadata, id: &PackageId) -> &'a Package {
568     metadata.packages.iter().find(|p| &p.id == id).unwrap()
569 }
570 
571 /// Finds all the packages that are in the rust runtime.
compute_runtime_crates<'a>(metadata: &'a Metadata) -> HashSet<&'a PackageId>572 fn compute_runtime_crates<'a>(metadata: &'a Metadata) -> HashSet<&'a PackageId> {
573     let mut result = HashSet::new();
574     for name in RUNTIME_CRATES {
575         let id = &pkg_from_name(metadata, name).id;
576         deps_of_filtered(metadata, id, &mut result, &|_| true);
577     }
578     result
579 }
580 
581 /// Recursively find all dependencies.
deps_of_filtered<'a>( metadata: &'a Metadata, pkg_id: &'a PackageId, result: &mut HashSet<&'a PackageId>, filter: &dyn Fn(&[DepKindInfo]) -> bool, )582 fn deps_of_filtered<'a>(
583     metadata: &'a Metadata,
584     pkg_id: &'a PackageId,
585     result: &mut HashSet<&'a PackageId>,
586     filter: &dyn Fn(&[DepKindInfo]) -> bool,
587 ) {
588     if !result.insert(pkg_id) {
589         return;
590     }
591     let node = metadata
592         .resolve
593         .as_ref()
594         .unwrap()
595         .nodes
596         .iter()
597         .find(|n| &n.id == pkg_id)
598         .unwrap_or_else(|| panic!("could not find `{pkg_id}` in resolve"));
599     for dep in &node.deps {
600         if !filter(&dep.dep_kinds) {
601             continue;
602         }
603         deps_of_filtered(metadata, &dep.pkg, result, filter);
604     }
605 }
606 
direct_deps_of<'a>( metadata: &'a Metadata, pkg_id: &'a PackageId, ) -> impl Iterator<Item = &'a Package>607 fn direct_deps_of<'a>(
608     metadata: &'a Metadata,
609     pkg_id: &'a PackageId,
610 ) -> impl Iterator<Item = &'a Package> {
611     let resolve = metadata.resolve.as_ref().unwrap();
612     let node = resolve.nodes.iter().find(|n| &n.id == pkg_id).unwrap();
613     node.deps.iter().map(|dep| pkg_from_id(metadata, &dep.pkg))
614 }
615 
check_rustfix(rust_metadata: &Metadata, cargo_metadata: &Metadata, bad: &mut bool)616 fn check_rustfix(rust_metadata: &Metadata, cargo_metadata: &Metadata, bad: &mut bool) {
617     let cargo = pkg_from_name(cargo_metadata, "cargo");
618     let cargo_rustfix =
619         direct_deps_of(cargo_metadata, &cargo.id).find(|p| p.name == "rustfix").unwrap();
620 
621     let compiletest = pkg_from_name(rust_metadata, "compiletest");
622     let compiletest_rustfix =
623         direct_deps_of(rust_metadata, &compiletest.id).find(|p| p.name == "rustfix").unwrap();
624 
625     if cargo_rustfix.version != compiletest_rustfix.version {
626         tidy_error!(
627             bad,
628             "cargo's rustfix version {} does not match compiletest's rustfix version {}\n\
629              rustfix should be kept in sync, update the cargo side first, and then update \
630              compiletest along with cargo.",
631             cargo_rustfix.version,
632             compiletest_rustfix.version
633         );
634     }
635 }
636