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