Skip to main content

tidy/
deps.rs

1//! Checks the licenses of third-party dependencies.
2
3use std::collections::{HashMap, HashSet};
4use std::fmt::{Display, Formatter};
5use std::fs::{File, read_dir};
6use std::io::Write;
7use std::path::Path;
8
9use cargo_metadata::semver::Version;
10use cargo_metadata::{Metadata, Package, PackageId};
11
12use crate::diagnostics::{RunningCheck, TidyCtx};
13
14#[path = "../../../bootstrap/src/utils/proc_macro_deps.rs"]
15mod proc_macro_deps;
16
17#[derive(Clone, Copy)]
18struct ListLocation {
19    path: &'static str,
20    line: u32,
21}
22
23impl Display for ListLocation {
24    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
25        write!(f, "{}:{}", self.path, self.line)
26    }
27}
28
29/// Creates a [`ListLocation`] for the current location (with an additional offset to the actual list start);
30macro_rules! location {
31    (+ $offset:literal) => {
32        ListLocation { path: file!(), line: line!() + $offset }
33    };
34}
35
36/// These are licenses that are allowed for all crates, including the runtime,
37/// rustc, tools, etc.
38#[rustfmt::skip]
39const LICENSES: &[&str] = &[
40    // tidy-alphabetical-start
41    "0BSD OR MIT OR Apache-2.0",                           // adler2 license
42    "Apache-2.0 / MIT",
43    "Apache-2.0 OR ISC OR MIT",
44    "Apache-2.0 OR MIT",
45    "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT", // wasi license
46    "Apache-2.0/MIT",
47    "BSD-2-Clause OR Apache-2.0 OR MIT",                   // zerocopy
48    "BSD-2-Clause OR MIT OR Apache-2.0",
49    "BSD-3-Clause/MIT",
50    "CC0-1.0 OR MIT-0 OR Apache-2.0",
51    "ISC",
52    "MIT / Apache-2.0",
53    "MIT AND (MIT OR Apache-2.0)",
54    "MIT AND Apache-2.0 WITH LLVM-exception AND (MIT OR Apache-2.0)", // compiler-builtins
55    "MIT OR Apache-2.0 OR BSD-1-Clause",
56    "MIT OR Apache-2.0 OR LGPL-2.1-or-later",              // r-efi, r-efi-alloc; LGPL is not acceptable, but we use it under MIT OR Apache-2.0
57    "MIT OR Apache-2.0 OR Zlib",                           // tinyvec_macros
58    "MIT OR Apache-2.0",
59    "MIT OR Zlib OR Apache-2.0",                           // miniz_oxide
60    "MIT",
61    "MIT/Apache-2.0",
62    "Unlicense OR MIT",
63    "Unlicense/MIT",
64    "Zlib",                                                // foldhash (FIXME: see PERMITTED_STDLIB_DEPENDENCIES)
65    // tidy-alphabetical-end
66];
67
68/// These are licenses that are allowed for rustc, tools, etc. But not for the runtime!
69#[rustfmt::skip]
70const LICENSES_TOOLS: &[&str] = &[
71    // tidy-alphabetical-start
72    "(Apache-2.0 OR MIT) AND BSD-3-Clause",
73    "(MIT OR Apache-2.0) AND Unicode-3.0",                 // unicode_ident (1.0.14)
74    "(MIT OR Apache-2.0) AND Unicode-DFS-2016",            // unicode_ident (1.0.12)
75    "0BSD",
76    "Apache-2.0 AND ISC",
77    "Apache-2.0 OR BSL-1.0",  // BSL is not acceptable, but we use it under Apache-2.0
78    "Apache-2.0 OR GPL-2.0-only",
79    "Apache-2.0 WITH LLVM-exception",
80    "Apache-2.0",
81    "BSD-2-Clause",
82    "BSD-3-Clause",
83    "CC0-1.0 OR Apache-2.0 OR Apache-2.0 WITH LLVM-exception",
84    "CC0-1.0",
85    "Unicode-3.0",                                         // icu4x
86    "Unicode-DFS-2016",                                    // tinystr
87    "Zlib OR Apache-2.0 OR MIT",                           // tinyvec
88    "Zlib",
89    // tidy-alphabetical-end
90];
91
92type ExceptionList = &'static [(&'static str, &'static str)];
93
94#[derive(Clone, Copy)]
95pub(crate) struct WorkspaceInfo<'a> {
96    /// Path to the directory containing the workspace root Cargo.toml file.
97    pub(crate) path: &'a str,
98    /// The list of license exceptions.
99    pub(crate) exceptions: ExceptionList,
100    /// Optionally:
101    /// * A list of crates for which dependencies need to be explicitly allowed.
102    /// * The list of allowed dependencies.
103    /// * The source code location of the allowed dependencies list
104    crates_and_deps: Option<(&'a [&'a str], &'a [&'a str], ListLocation)>,
105    /// Submodules required for the workspace
106    pub(crate) submodules: &'a [&'a str],
107}
108
109const WORKSPACE_LOCATION: ListLocation = location!(+4);
110
111/// The workspaces to check for licensing and optionally permitted dependencies.
112// FIXME auto detect all cargo workspaces
113pub(crate) const WORKSPACES: &[WorkspaceInfo<'static>] = &[
114    // The root workspace has to be first for check_rustfix to work.
115    WorkspaceInfo {
116        path: ".",
117        exceptions: EXCEPTIONS,
118        crates_and_deps: Some((
119            &["rustc-main"],
120            PERMITTED_RUSTC_DEPENDENCIES,
121            PERMITTED_RUSTC_DEPS_LOCATION,
122        )),
123        submodules: &[],
124    },
125    WorkspaceInfo {
126        path: "library",
127        exceptions: EXCEPTIONS_STDLIB,
128        crates_and_deps: Some((
129            &["sysroot"],
130            PERMITTED_STDLIB_DEPENDENCIES,
131            PERMITTED_STDLIB_DEPS_LOCATION,
132        )),
133        submodules: &[],
134    },
135    WorkspaceInfo {
136        path: "library/stdarch",
137        exceptions: EXCEPTIONS_STDARCH,
138        crates_and_deps: None,
139        submodules: &[],
140    },
141    WorkspaceInfo {
142        path: "compiler/rustc_codegen_cranelift",
143        exceptions: EXCEPTIONS_CRANELIFT,
144        crates_and_deps: Some((
145            &["rustc_codegen_cranelift"],
146            PERMITTED_CRANELIFT_DEPENDENCIES,
147            PERMITTED_CRANELIFT_DEPS_LOCATION,
148        )),
149        submodules: &[],
150    },
151    WorkspaceInfo {
152        path: "compiler/rustc_codegen_gcc",
153        exceptions: EXCEPTIONS_GCC,
154        crates_and_deps: None,
155        submodules: &[],
156    },
157    WorkspaceInfo {
158        path: "src/bootstrap",
159        exceptions: EXCEPTIONS_BOOTSTRAP,
160        crates_and_deps: None,
161        submodules: &[],
162    },
163    WorkspaceInfo {
164        path: "src/tools/cargo",
165        exceptions: EXCEPTIONS_CARGO,
166        crates_and_deps: None,
167        submodules: &["src/tools/cargo"],
168    },
169    // FIXME uncomment once all deps are vendored
170    //  WorkspaceInfo {
171    //      path: "src/tools/miri/test-cargo-miri",
172    //      crates_and_deps: None
173    //      submodules: &[],
174    //  },
175    // WorkspaceInfo {
176    //      path: "src/tools/miri/test_dependencies",
177    //      crates_and_deps: None,
178    //      submodules: &[],
179    //  }
180    WorkspaceInfo {
181        path: "src/tools/rust-analyzer",
182        exceptions: EXCEPTIONS_RUST_ANALYZER,
183        crates_and_deps: None,
184        submodules: &[],
185    },
186    WorkspaceInfo {
187        path: "src/tools/rustbook",
188        exceptions: EXCEPTIONS_RUSTBOOK,
189        crates_and_deps: None,
190        submodules: &["src/doc/book", "src/doc/reference"],
191    },
192    WorkspaceInfo {
193        path: "src/tools/rustc-perf",
194        exceptions: EXCEPTIONS_RUSTC_PERF,
195        crates_and_deps: None,
196        submodules: &["src/tools/rustc-perf"],
197    },
198    WorkspaceInfo {
199        path: "tests/run-make-cargo/uefi-qemu/uefi_qemu_test",
200        exceptions: EXCEPTIONS_UEFI_QEMU_TEST,
201        crates_and_deps: None,
202        submodules: &[],
203    },
204];
205
206/// These are exceptions to Rust's permissive licensing policy, and
207/// should be considered bugs. Exceptions are only allowed in Rust
208/// tooling. It is _crucial_ that no exception crates be dependencies
209/// of the Rust runtime (std/test).
210#[rustfmt::skip]
211const EXCEPTIONS: ExceptionList = &[
212    // tidy-alphabetical-start
213    ("colored", "MPL-2.0"),                                  // rustfmt
214    ("option-ext", "MPL-2.0"),                               // cargo-miri (via `directories`)
215    // tidy-alphabetical-end
216];
217
218/// These are exceptions to Rust's permissive licensing policy, and
219/// should be considered bugs. Exceptions are only allowed in Rust
220/// tooling. It is _crucial_ that no exception crates be dependencies
221/// of the Rust runtime (std/test).
222#[rustfmt::skip]
223const EXCEPTIONS_STDLIB: ExceptionList = &[
224    // tidy-alphabetical-start
225    ("fortanix-sgx-abi", "MPL-2.0"), // libstd but only for `sgx` target. FIXME: this dependency violates the documentation comment above.
226    // tidy-alphabetical-end
227];
228
229const EXCEPTIONS_CARGO: ExceptionList = &[
230    // tidy-alphabetical-start
231    ("bitmaps", "MPL-2.0+"),
232    ("im-rc", "MPL-2.0+"),
233    ("sized-chunks", "MPL-2.0+"),
234    // tidy-alphabetical-end
235];
236
237const EXCEPTIONS_RUST_ANALYZER: ExceptionList = &[
238    // tidy-alphabetical-start
239    ("option-ext", "MPL-2.0"),
240    // tidy-alphabetical-end
241];
242
243const EXCEPTIONS_RUSTC_PERF: ExceptionList = &[
244    // tidy-alphabetical-start
245    ("inferno", "CDDL-1.0"),
246    ("option-ext", "MPL-2.0"),
247    // tidy-alphabetical-end
248];
249
250const EXCEPTIONS_RUSTBOOK: ExceptionList = &[
251    // tidy-alphabetical-start
252    ("font-awesome-as-a-crate", "CC-BY-4.0 AND MIT"),
253    ("mdbook-core", "MPL-2.0"),
254    ("mdbook-driver", "MPL-2.0"),
255    ("mdbook-html", "MPL-2.0"),
256    ("mdbook-markdown", "MPL-2.0"),
257    ("mdbook-preprocessor", "MPL-2.0"),
258    ("mdbook-renderer", "MPL-2.0"),
259    ("mdbook-summary", "MPL-2.0"),
260    // tidy-alphabetical-end
261];
262
263const EXCEPTIONS_STDARCH: ExceptionList = &[];
264
265const EXCEPTIONS_CRANELIFT: ExceptionList = &[];
266
267const EXCEPTIONS_GCC: ExceptionList = &[
268    // tidy-alphabetical-start
269    ("gccjit", "GPL-3.0"),
270    ("gccjit_sys", "GPL-3.0"),
271    // tidy-alphabetical-end
272];
273
274const EXCEPTIONS_BOOTSTRAP: ExceptionList = &[];
275
276const EXCEPTIONS_UEFI_QEMU_TEST: ExceptionList = &[];
277
278const PERMITTED_RUSTC_DEPS_LOCATION: ListLocation = location!(+6);
279
280/// Crates rustc is allowed to depend on. Avoid adding to the list if possible.
281///
282/// This list is here to provide a speed-bump to adding a new dependency to
283/// rustc. Please check with the compiler team before adding an entry.
284const PERMITTED_RUSTC_DEPENDENCIES: &[&str] = &[
285    // tidy-alphabetical-start
286    "adler2",
287    "aho-corasick",
288    "allocator-api2", // FIXME: only appears in Cargo.lock due to https://github.com/rust-lang/cargo/issues/10801
289    "annotate-snippets",
290    "anstream",
291    "anstyle",
292    "anstyle-parse",
293    "anstyle-query",
294    "anstyle-wincon",
295    "ar_archive_writer",
296    "arrayref",
297    "arrayvec",
298    "bitflags",
299    "blake3",
300    "block-buffer",
301    "block2",
302    "bstr",
303    "cc",
304    "cfg-if",
305    "cfg_aliases",
306    "colorchoice",
307    "constant_time_eq",
308    "cpufeatures",
309    "crc32fast",
310    "crossbeam-deque",
311    "crossbeam-epoch",
312    "crossbeam-utils",
313    "crypto-common",
314    "ctrlc",
315    "darling",
316    "darling_core",
317    "darling_macro",
318    "datafrog",
319    "derive-where",
320    "derive_setters",
321    "digest",
322    "dispatch2",
323    "displaydoc",
324    "dissimilar",
325    "dyn-clone",
326    "either",
327    "elsa",
328    "ena",
329    "equivalent",
330    "errno",
331    "expect-test",
332    "fallible-iterator", // dependency of `thorin`
333    "fastrand",
334    "find-msvc-tools",
335    "flate2",
336    "fluent-bundle",
337    "fluent-langneg",
338    "fluent-syntax",
339    "fnv",
340    "foldhash",
341    "generic-array",
342    "getopts",
343    "getrandom",
344    "gimli",
345    "gsgdt",
346    "hashbrown",
347    "icu_collections",
348    "icu_list",
349    "icu_locale",
350    "icu_locale_core",
351    "icu_locale_data",
352    "icu_provider",
353    "ident_case",
354    "indexmap",
355    "intl-memoizer",
356    "intl_pluralrules",
357    "is_terminal_polyfill",
358    "itertools",
359    "itoa",
360    "jiff",
361    "jiff-static",
362    "jobserver",
363    "lazy_static",
364    "leb128",
365    "libc",
366    "libloading",
367    "linux-raw-sys",
368    "litemap",
369    "lock_api",
370    "log",
371    "matchers",
372    "md-5",
373    "measureme",
374    "memchr",
375    "memmap2",
376    "miniz_oxide",
377    "nix",
378    "nu-ansi-term",
379    "objc2",
380    "objc2-encode",
381    "object",
382    "odht",
383    "once_cell",
384    "once_cell_polyfill",
385    "parking_lot",
386    "parking_lot_core",
387    "pathdiff",
388    "perf-event-open-sys",
389    "pin-project-lite",
390    "polonius-engine",
391    "portable-atomic", // dependency for platforms doesn't support `AtomicU64` in std
392    "portable-atomic-util",
393    "potential_utf",
394    "ppv-lite86",
395    "proc-macro-hack",
396    "proc-macro2",
397    "psm",
398    "pulldown-cmark",
399    "pulldown-cmark-escape",
400    "punycode",
401    "quote",
402    "r-efi",
403    "rand",
404    "rand_chacha",
405    "rand_core",
406    "rand_xorshift", // dependency for doc-tests in rustc_thread_pool
407    "rand_xoshiro",
408    "redox_syscall",
409    "ref-cast",
410    "ref-cast-impl",
411    "regex",
412    "regex-automata",
413    "regex-syntax",
414    "rustc-demangle",
415    "rustc-hash",
416    "rustc-literal-escaper",
417    "rustc-stable-hash",
418    "rustc_apfloat",
419    "rustix",
420    "ruzstd", // via object in thorin-dwp
421    "ryu",
422    "schemars",
423    "schemars_derive",
424    "scoped-tls",
425    "scopeguard",
426    "self_cell",
427    "serde",
428    "serde_core",
429    "serde_derive",
430    "serde_derive_internals",
431    "serde_json",
432    "serde_path_to_error",
433    "sha1",
434    "sha2",
435    "sharded-slab",
436    "shlex",
437    "simd-adler32",
438    "smallvec",
439    "stable_deref_trait",
440    "stacker",
441    "static_assertions",
442    "strsim",
443    "syn",
444    "synstructure",
445    "tempfile",
446    "termize",
447    "thin-vec",
448    "thiserror",
449    "thiserror-impl",
450    "thorin-dwp",
451    "thread_local",
452    "tikv-jemalloc-sys",
453    "tinystr",
454    "tinyvec",
455    "tinyvec_macros",
456    "tracing",
457    "tracing-attributes",
458    "tracing-core",
459    "tracing-log",
460    "tracing-serde",
461    "tracing-subscriber",
462    "tracing-tree",
463    "twox-hash",
464    "type-map",
465    "typenum",
466    "unic-langid",
467    "unic-langid-impl",
468    "unic-langid-macros",
469    "unic-langid-macros-impl",
470    "unicase",
471    "unicode-ident",
472    "unicode-normalization",
473    "unicode-properties",
474    "unicode-script",
475    "unicode-security",
476    "unicode-width",
477    "utf8_iter",
478    "utf8parse",
479    "valuable",
480    "version_check",
481    "wasi",
482    "wasm-encoder",
483    "wasmparser",
484    "windows",
485    "windows-collections",
486    "windows-core",
487    "windows-future",
488    "windows-implement",
489    "windows-interface",
490    "windows-link",
491    "windows-numerics",
492    "windows-result",
493    "windows-strings",
494    "windows-sys",
495    "windows-targets",
496    "windows-threading",
497    "windows_aarch64_gnullvm",
498    "windows_aarch64_msvc",
499    "windows_i686_gnu",
500    "windows_i686_gnullvm",
501    "windows_i686_msvc",
502    "windows_x86_64_gnu",
503    "windows_x86_64_gnullvm",
504    "windows_x86_64_msvc",
505    "wit-bindgen-rt@0.39.0", // pinned to a specific version due to using a binary blob: <https://github.com/rust-lang/rust/pull/136395#issuecomment-2692769062>
506    "writeable",
507    "yoke",
508    "yoke-derive",
509    "zerocopy",
510    "zerocopy-derive",
511    "zerofrom",
512    "zerofrom-derive",
513    "zerotrie",
514    "zerovec",
515    "zerovec-derive",
516    "zlib-rs",
517    // tidy-alphabetical-end
518];
519
520const PERMITTED_STDLIB_DEPS_LOCATION: ListLocation = location!(+2);
521
522const PERMITTED_STDLIB_DEPENDENCIES: &[&str] = &[
523    // tidy-alphabetical-start
524    "addr2line",
525    "adler2",
526    "cc",
527    "cfg-if",
528    "compiler_builtins",
529    "dlmalloc",
530    "foldhash", // FIXME: only appears in Cargo.lock due to https://github.com/rust-lang/cargo/issues/10801
531    "fortanix-sgx-abi",
532    "getopts",
533    "gimli",
534    "hashbrown",
535    "hermit-abi",
536    "libc",
537    "memchr",
538    "miniz_oxide",
539    "moto-rt",
540    "object",
541    "r-efi",
542    "r-efi-alloc",
543    "rand",
544    "rand_core",
545    "rand_xorshift",
546    "rustc-demangle",
547    "rustc-literal-escaper",
548    "shlex",
549    "unwinding",
550    "vex-sdk",
551    "wasip1",
552    "wasip2",
553    "wasip3",
554    "windows-link",
555    "windows-sys@0.61.100", // Enforce the usage of our dummy windows-sys patch. Keep version in sync.
556    "wit-bindgen",
557    // tidy-alphabetical-end
558];
559
560const PERMITTED_CRANELIFT_DEPS_LOCATION: ListLocation = location!(+2);
561
562const PERMITTED_CRANELIFT_DEPENDENCIES: &[&str] = &[
563    // tidy-alphabetical-start
564    "allocator-api2",
565    "anyhow",
566    "arbitrary",
567    "bitflags",
568    "bumpalo",
569    "cfg-if",
570    "cranelift-assembler-x64",
571    "cranelift-assembler-x64-meta",
572    "cranelift-bforest",
573    "cranelift-bitset",
574    "cranelift-codegen",
575    "cranelift-codegen-meta",
576    "cranelift-codegen-shared",
577    "cranelift-control",
578    "cranelift-entity",
579    "cranelift-frontend",
580    "cranelift-isle",
581    "cranelift-jit",
582    "cranelift-module",
583    "cranelift-native",
584    "cranelift-object",
585    "cranelift-srcgen",
586    "crc32fast",
587    "equivalent",
588    "fnv",
589    "foldhash",
590    "gimli",
591    "hashbrown",
592    "heck",
593    "indexmap",
594    "libc",
595    "libloading",
596    "libm",
597    "log",
598    "mach2",
599    "memchr",
600    "memmap2",
601    "object",
602    "proc-macro2",
603    "quote",
604    "regalloc2",
605    "region",
606    "rustc-hash",
607    "serde",
608    "serde_core",
609    "serde_derive",
610    "smallvec",
611    "stable_deref_trait",
612    "syn",
613    "target-lexicon",
614    "unicode-ident",
615    "wasmtime-internal-core",
616    "wasmtime-internal-jit-icache-coherence",
617    "windows-link",
618    "windows-sys",
619    "windows-targets",
620    "windows_aarch64_gnullvm",
621    "windows_aarch64_msvc",
622    "windows_i686_gnu",
623    "windows_i686_gnullvm",
624    "windows_i686_msvc",
625    "windows_x86_64_gnu",
626    "windows_x86_64_gnullvm",
627    "windows_x86_64_msvc",
628    // tidy-alphabetical-end
629];
630
631/// Dependency checks.
632///
633/// `root` is path to the directory with the root `Cargo.toml` (for the workspace). `cargo` is path
634/// to the cargo executable.
635pub fn check(root: &Path, cargo: &Path, tidy_ctx: TidyCtx) {
636    let mut check = tidy_ctx.start_check("deps");
637    let bless = tidy_ctx.is_bless_enabled();
638
639    let mut checked_runtime_licenses = false;
640
641    check_proc_macro_dep_list(root, cargo, bless, &mut check);
642
643    for &WorkspaceInfo { path, exceptions, crates_and_deps, submodules } in WORKSPACES {
644        if has_missing_submodule(root, submodules, tidy_ctx.is_running_on_ci()) {
645            continue;
646        }
647
648        if !root.join(path).join("Cargo.lock").exists() {
649            check.error(format!("the `{path}` workspace doesn't have a Cargo.lock"));
650            continue;
651        }
652
653        let mut cmd = cargo_metadata::MetadataCommand::new();
654        cmd.cargo_path(cargo)
655            .manifest_path(root.join(path).join("Cargo.toml"))
656            .features(cargo_metadata::CargoOpt::AllFeatures)
657            .other_options(vec!["--locked".to_owned()]);
658        let metadata = t!(cmd.exec());
659
660        // Check for packages which have been moved into a different workspace and not updated
661        let absolute_root =
662            if path == "." { root.to_path_buf() } else { t!(std::path::absolute(root.join(path))) };
663        let absolute_root_real = t!(std::path::absolute(&metadata.workspace_root));
664        if absolute_root_real != absolute_root {
665            check.error(format!("{path} is part of another workspace ({} != {}), remove from `WORKSPACES` ({WORKSPACE_LOCATION})", absolute_root.display(), absolute_root_real.display()));
666        }
667        check_license_exceptions(&metadata, path, exceptions, &mut check);
668        if let Some((crates, permitted_deps, location)) = crates_and_deps {
669            let descr = crates.get(0).unwrap_or(&path);
670            check_permitted_dependencies(
671                &metadata,
672                descr,
673                permitted_deps,
674                crates,
675                location,
676                &mut check,
677            );
678        }
679
680        if path == "library" {
681            check_runtime_license_exceptions(&metadata, &mut check);
682            check_runtime_no_duplicate_dependencies(&metadata, &mut check);
683            check_runtime_no_proc_macros(&metadata, &mut check);
684            checked_runtime_licenses = true;
685        }
686    }
687
688    // Sanity check to ensure we don't accidentally remove the workspace containing the runtime
689    // crates.
690    assert!(checked_runtime_licenses);
691}
692
693/// Ensure the list of proc-macro crate transitive dependencies is up to date
694fn check_proc_macro_dep_list(root: &Path, cargo: &Path, bless: bool, check: &mut RunningCheck) {
695    let mut cmd = cargo_metadata::MetadataCommand::new();
696    cmd.cargo_path(cargo)
697        .manifest_path(root.join("Cargo.toml"))
698        .features(cargo_metadata::CargoOpt::AllFeatures)
699        .other_options(vec!["--locked".to_owned()]);
700    let metadata = t!(cmd.exec());
701    let is_proc_macro_pkg = |pkg: &Package| pkg.targets.iter().any(|target| target.is_proc_macro());
702
703    let mut proc_macro_deps = HashSet::new();
704    for pkg in metadata.packages.iter().filter(|pkg| is_proc_macro_pkg(pkg)) {
705        deps_of(&metadata, &pkg.id, &mut proc_macro_deps);
706    }
707    // Remove the proc-macro crates themselves
708    proc_macro_deps.retain(|pkg| !is_proc_macro_pkg(&metadata[pkg]));
709
710    let proc_macro_deps: HashSet<_> =
711        proc_macro_deps.into_iter().map(|dep| metadata[dep].name.as_ref()).collect();
712    let expected = proc_macro_deps::CRATES.iter().copied().collect::<HashSet<_>>();
713
714    let needs_blessing = proc_macro_deps.difference(&expected).next().is_some()
715        || expected.difference(&proc_macro_deps).next().is_some();
716
717    if needs_blessing && bless {
718        let mut proc_macro_deps: Vec<_> = proc_macro_deps.into_iter().collect();
719        proc_macro_deps.sort();
720        let mut file = File::create(root.join("src/bootstrap/src/utils/proc_macro_deps.rs"))
721            .expect("`proc_macro_deps` should exist");
722        writeln!(
723            &mut file,
724            "/// Do not update manually - use `./x.py test tidy --bless`
725/// Holds all direct and indirect dependencies of proc-macro crates in tree.
726/// See <https://github.com/rust-lang/rust/issues/134863>
727pub static CRATES: &[&str] = &[
728    // tidy-alphabetical-start"
729        )
730        .unwrap();
731        for dep in proc_macro_deps {
732            writeln!(&mut file, "    {dep:?},").unwrap();
733        }
734        writeln!(
735            &mut file,
736            "    // tidy-alphabetical-end
737];"
738        )
739        .unwrap();
740    } else {
741        let mut error_found = false;
742
743        for missing in proc_macro_deps.difference(&expected) {
744            error_found = true;
745            check.error(format!(
746                "proc-macro crate dependency `{missing}` is not registered in `src/bootstrap/src/utils/proc_macro_deps.rs`",
747            ));
748        }
749        for extra in expected.difference(&proc_macro_deps) {
750            error_found = true;
751            check.error(format!(
752                "`{extra}` is registered in `src/bootstrap/src/utils/proc_macro_deps.rs`, but is not a proc-macro crate dependency",
753            ));
754        }
755        if error_found {
756            check.message("Run `./x.py test tidy --bless` to regenerate the list");
757        }
758    }
759}
760
761/// Used to skip a check if a submodule is not checked out, and not in a CI environment.
762///
763/// This helps prevent enforcing developers to fetch submodules for tidy.
764pub fn has_missing_submodule(root: &Path, submodules: &[&str], is_ci: bool) -> bool {
765    !is_ci
766        && submodules.iter().any(|submodule| {
767            let path = root.join(submodule);
768            !path.exists()
769            // If the directory is empty, we can consider it as an uninitialized submodule.
770            || read_dir(path).unwrap().next().is_none()
771        })
772}
773
774/// Check that all licenses of runtime dependencies are in the valid list in `LICENSES`.
775///
776/// Unlike for tools we don't allow exceptions to the `LICENSES` list for the runtime with the sole
777/// exception of `fortanix-sgx-abi` which is only used on x86_64-fortanix-unknown-sgx.
778fn check_runtime_license_exceptions(metadata: &Metadata, check: &mut RunningCheck) {
779    for pkg in &metadata.packages {
780        if pkg.source.is_none() {
781            // No need to check local packages.
782            continue;
783        }
784        let license = match &pkg.license {
785            Some(license) => license,
786            None => {
787                check
788                    .error(format!("dependency `{}` does not define a license expression", pkg.id));
789                continue;
790            }
791        };
792        if !LICENSES.contains(&license.as_str()) {
793            // This is a specific exception because SGX is considered "third party".
794            // See https://github.com/rust-lang/rust/issues/62620 for more.
795            // In general, these should never be added and this exception
796            // should not be taken as precedent for any new target.
797            if *pkg.name == "fortanix-sgx-abi" && pkg.license.as_deref() == Some("MPL-2.0") {
798                continue;
799            }
800
801            check.error(format!("invalid license `{}` in `{}`", license, pkg.id));
802        }
803    }
804}
805
806/// Check that all licenses of tool dependencies are in the valid list in `LICENSES`.
807///
808/// Packages listed in `exceptions` are allowed for tools.
809fn check_license_exceptions(
810    metadata: &Metadata,
811    workspace: &str,
812    exceptions: &[(&str, &str)],
813    check: &mut RunningCheck,
814) {
815    // Validate the EXCEPTIONS list hasn't changed.
816    for (name, license) in exceptions {
817        // Check that the package actually exists.
818        if !metadata.packages.iter().any(|p| *p.name == *name) {
819            check.error(format!(
820                "could not find exception package `{name}` in workspace `{workspace}`\n\
821                Remove from EXCEPTIONS list if it is no longer used.",
822            ));
823        }
824        // Check that the license hasn't changed.
825        for pkg in metadata.packages.iter().filter(|p| *p.name == *name) {
826            match &pkg.license {
827                None => {
828                    check.error(format!(
829                        "dependency exception `{}` in workspace `{workspace}` does not declare a license expression",
830                        pkg.id
831                    ));
832                }
833                Some(pkg_license) => {
834                    if pkg_license.as_str() != *license {
835                        check.error(format!(r#"dependency exception `{name}` license in workspace `{workspace}` has changed
836    previously `{license}` now `{pkg_license}`
837    update EXCEPTIONS for the new license
838"#));
839                    }
840                }
841            }
842        }
843        if LICENSES.contains(license) || LICENSES_TOOLS.contains(license) {
844            check.error(format!(
845                "dependency exception `{name}` is not necessary. `{license}` is an allowed license"
846            ));
847        }
848    }
849
850    let exception_names: Vec<_> = exceptions.iter().map(|(name, _license)| *name).collect();
851
852    // Check if any package does not have a valid license.
853    for pkg in &metadata.packages {
854        if pkg.source.is_none() {
855            // No need to check local packages.
856            continue;
857        }
858        if exception_names.contains(&pkg.name.as_str()) {
859            continue;
860        }
861        let license = match &pkg.license {
862            Some(license) => license,
863            None => {
864                check.error(format!(
865                    "dependency `{}` in workspace `{workspace}` does not define a license expression",
866                    pkg.id
867                ));
868                continue;
869            }
870        };
871        if !LICENSES.contains(&license.as_str()) && !LICENSES_TOOLS.contains(&license.as_str()) {
872            check.error(format!(
873                "invalid license `{}` for package `{}` in workspace `{workspace}`",
874                license, pkg.id
875            ));
876        }
877    }
878}
879
880fn check_runtime_no_duplicate_dependencies(metadata: &Metadata, check: &mut RunningCheck) {
881    let mut seen_pkgs = HashSet::new();
882    for pkg in &metadata.packages {
883        if pkg.source.is_none() {
884            continue;
885        }
886
887        if !seen_pkgs.insert(&*pkg.name) {
888            check.error(format!(
889                "duplicate package `{}` is not allowed for the standard library",
890                pkg.name
891            ));
892        }
893    }
894}
895
896fn check_runtime_no_proc_macros(metadata: &Metadata, check: &mut RunningCheck) {
897    for pkg in &metadata.packages {
898        if pkg.targets.iter().any(|target| target.is_proc_macro()) {
899            check.error(format!(
900                "proc macro `{}` is not allowed as standard library dependency.\n\
901                Using proc macros in the standard library would break cross-compilation \
902                as proc-macros don't get shipped for the host tuple.",
903                pkg.name
904            ));
905        }
906    }
907}
908
909/// Checks the dependency of `restricted_dependency_crates` at the given path. Changes `bad` to
910/// `true` if a check failed.
911///
912/// Specifically, this checks that the dependencies are on the `permitted_dependencies`.
913fn check_permitted_dependencies(
914    metadata: &Metadata,
915    descr: &str,
916    permitted_dependencies: &[&'static str],
917    restricted_dependency_crates: &[&'static str],
918    permitted_location: ListLocation,
919    check: &mut RunningCheck,
920) {
921    let mut has_permitted_dep_error = false;
922    let mut deps = HashSet::new();
923    for to_check in restricted_dependency_crates {
924        let to_check = pkg_from_name(metadata, to_check);
925        deps_of(metadata, &to_check.id, &mut deps);
926    }
927
928    // Check that the PERMITTED_DEPENDENCIES does not have unused entries.
929    for permitted in permitted_dependencies {
930        fn compare(pkg: &Package, permitted: &str) -> bool {
931            if let Some((name, version)) = permitted.split_once("@") {
932                let Ok(version) = Version::parse(version) else {
933                    return false;
934                };
935                *pkg.name == name && pkg.version == version
936            } else {
937                *pkg.name == permitted
938            }
939        }
940        if !deps.iter().any(|dep_id| compare(pkg_from_id(metadata, dep_id), permitted)) {
941            check.error(format!(
942                "could not find allowed package `{permitted}`\n\
943                Remove from PERMITTED_DEPENDENCIES list if it is no longer used.",
944            ));
945            has_permitted_dep_error = true;
946        }
947    }
948
949    // Get in a convenient form.
950    let permitted_dependencies: HashMap<_, _> = permitted_dependencies
951        .iter()
952        .map(|s| {
953            if let Some((name, version)) = s.split_once('@') {
954                (name, Version::parse(version).ok())
955            } else {
956                (*s, None)
957            }
958        })
959        .collect();
960
961    for dep in deps {
962        let dep = pkg_from_id(metadata, dep);
963        // If this path is in-tree, we don't require it to be explicitly permitted.
964        if dep.source.is_some() {
965            let is_eq = if let Some(version) = permitted_dependencies.get(dep.name.as_str()) {
966                if let Some(version) = version { version == &dep.version } else { true }
967            } else {
968                false
969            };
970            if !is_eq {
971                check.error(format!("Dependency for {descr} not explicitly permitted: {}", dep.id));
972                has_permitted_dep_error = true;
973            }
974        }
975    }
976
977    if has_permitted_dep_error {
978        eprintln!("Go to `{}:{}` for the list.", permitted_location.path, permitted_location.line);
979    }
980}
981
982/// Finds a package with the given name.
983fn pkg_from_name<'a>(metadata: &'a Metadata, name: &'static str) -> &'a Package {
984    let mut i = metadata.packages.iter().filter(|p| *p.name == name);
985    let result =
986        i.next().unwrap_or_else(|| panic!("could not find package `{name}` in package list"));
987    assert!(i.next().is_none(), "more than one package found for `{name}`");
988    result
989}
990
991fn pkg_from_id<'a>(metadata: &'a Metadata, id: &PackageId) -> &'a Package {
992    metadata.packages.iter().find(|p| &p.id == id).unwrap()
993}
994
995/// Recursively find all dependencies.
996fn deps_of<'a>(metadata: &'a Metadata, pkg_id: &'a PackageId, result: &mut HashSet<&'a PackageId>) {
997    if !result.insert(pkg_id) {
998        return;
999    }
1000    let node = metadata
1001        .resolve
1002        .as_ref()
1003        .unwrap()
1004        .nodes
1005        .iter()
1006        .find(|n| &n.id == pkg_id)
1007        .unwrap_or_else(|| panic!("could not find `{pkg_id}` in resolve"));
1008    for dep in &node.deps {
1009        deps_of(metadata, &dep.pkg, result);
1010    }
1011}