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    "wasi",
543    "windows-link",
544    "windows-sys",
545    "windows-targets",
546    "windows_aarch64_gnullvm",
547    "windows_aarch64_msvc",
548    "windows_i686_gnu",
549    "windows_i686_gnullvm",
550    "windows_i686_msvc",
551    "windows_x86_64_gnu",
552    "windows_x86_64_gnullvm",
553    "windows_x86_64_msvc",
554    "wit-bindgen",
555    // tidy-alphabetical-end
556];
557
558const PERMITTED_CRANELIFT_DEPS_LOCATION: ListLocation = location!(+2);
559
560const PERMITTED_CRANELIFT_DEPENDENCIES: &[&str] = &[
561    // tidy-alphabetical-start
562    "allocator-api2",
563    "anyhow",
564    "arbitrary",
565    "bitflags",
566    "bumpalo",
567    "cfg-if",
568    "cranelift-assembler-x64",
569    "cranelift-assembler-x64-meta",
570    "cranelift-bforest",
571    "cranelift-bitset",
572    "cranelift-codegen",
573    "cranelift-codegen-meta",
574    "cranelift-codegen-shared",
575    "cranelift-control",
576    "cranelift-entity",
577    "cranelift-frontend",
578    "cranelift-isle",
579    "cranelift-jit",
580    "cranelift-module",
581    "cranelift-native",
582    "cranelift-object",
583    "cranelift-srcgen",
584    "crc32fast",
585    "equivalent",
586    "fnv",
587    "foldhash",
588    "gimli",
589    "hashbrown",
590    "heck",
591    "indexmap",
592    "libc",
593    "libloading",
594    "libm",
595    "log",
596    "mach2",
597    "memchr",
598    "object",
599    "proc-macro2",
600    "quote",
601    "regalloc2",
602    "region",
603    "rustc-hash",
604    "serde",
605    "serde_core",
606    "serde_derive",
607    "smallvec",
608    "stable_deref_trait",
609    "syn",
610    "target-lexicon",
611    "unicode-ident",
612    "wasmtime-internal-core",
613    "wasmtime-internal-jit-icache-coherence",
614    "windows-link",
615    "windows-sys",
616    "windows-targets",
617    "windows_aarch64_gnullvm",
618    "windows_aarch64_msvc",
619    "windows_i686_gnu",
620    "windows_i686_gnullvm",
621    "windows_i686_msvc",
622    "windows_x86_64_gnu",
623    "windows_x86_64_gnullvm",
624    "windows_x86_64_msvc",
625    // tidy-alphabetical-end
626];
627
628/// Dependency checks.
629///
630/// `root` is path to the directory with the root `Cargo.toml` (for the workspace). `cargo` is path
631/// to the cargo executable.
632pub fn check(root: &Path, cargo: &Path, tidy_ctx: TidyCtx) {
633    let mut check = tidy_ctx.start_check("deps");
634    let bless = tidy_ctx.is_bless_enabled();
635
636    let mut checked_runtime_licenses = false;
637
638    check_proc_macro_dep_list(root, cargo, bless, &mut check);
639
640    for &WorkspaceInfo { path, exceptions, crates_and_deps, submodules } in WORKSPACES {
641        if has_missing_submodule(root, submodules, tidy_ctx.is_running_on_ci()) {
642            continue;
643        }
644
645        if !root.join(path).join("Cargo.lock").exists() {
646            check.error(format!("the `{path}` workspace doesn't have a Cargo.lock"));
647            continue;
648        }
649
650        let mut cmd = cargo_metadata::MetadataCommand::new();
651        cmd.cargo_path(cargo)
652            .manifest_path(root.join(path).join("Cargo.toml"))
653            .features(cargo_metadata::CargoOpt::AllFeatures)
654            .other_options(vec!["--locked".to_owned()]);
655        let metadata = t!(cmd.exec());
656
657        // Check for packages which have been moved into a different workspace and not updated
658        let absolute_root =
659            if path == "." { root.to_path_buf() } else { t!(std::path::absolute(root.join(path))) };
660        let absolute_root_real = t!(std::path::absolute(&metadata.workspace_root));
661        if absolute_root_real != absolute_root {
662            check.error(format!("{path} is part of another workspace ({} != {}), remove from `WORKSPACES` ({WORKSPACE_LOCATION})", absolute_root.display(), absolute_root_real.display()));
663        }
664        check_license_exceptions(&metadata, path, exceptions, &mut check);
665        if let Some((crates, permitted_deps, location)) = crates_and_deps {
666            let descr = crates.get(0).unwrap_or(&path);
667            check_permitted_dependencies(
668                &metadata,
669                descr,
670                permitted_deps,
671                crates,
672                location,
673                &mut check,
674            );
675        }
676
677        if path == "library" {
678            check_runtime_license_exceptions(&metadata, &mut check);
679            check_runtime_no_duplicate_dependencies(&metadata, &mut check);
680            check_runtime_no_proc_macros(&metadata, &mut check);
681            checked_runtime_licenses = true;
682        }
683    }
684
685    // Sanity check to ensure we don't accidentally remove the workspace containing the runtime
686    // crates.
687    assert!(checked_runtime_licenses);
688}
689
690/// Ensure the list of proc-macro crate transitive dependencies is up to date
691fn check_proc_macro_dep_list(root: &Path, cargo: &Path, bless: bool, check: &mut RunningCheck) {
692    let mut cmd = cargo_metadata::MetadataCommand::new();
693    cmd.cargo_path(cargo)
694        .manifest_path(root.join("Cargo.toml"))
695        .features(cargo_metadata::CargoOpt::AllFeatures)
696        .other_options(vec!["--locked".to_owned()]);
697    let metadata = t!(cmd.exec());
698    let is_proc_macro_pkg = |pkg: &Package| pkg.targets.iter().any(|target| target.is_proc_macro());
699
700    let mut proc_macro_deps = HashSet::new();
701    for pkg in metadata.packages.iter().filter(|pkg| is_proc_macro_pkg(pkg)) {
702        deps_of(&metadata, &pkg.id, &mut proc_macro_deps);
703    }
704    // Remove the proc-macro crates themselves
705    proc_macro_deps.retain(|pkg| !is_proc_macro_pkg(&metadata[pkg]));
706
707    let proc_macro_deps: HashSet<_> =
708        proc_macro_deps.into_iter().map(|dep| metadata[dep].name.as_ref()).collect();
709    let expected = proc_macro_deps::CRATES.iter().copied().collect::<HashSet<_>>();
710
711    let needs_blessing = proc_macro_deps.difference(&expected).next().is_some()
712        || expected.difference(&proc_macro_deps).next().is_some();
713
714    if needs_blessing && bless {
715        let mut proc_macro_deps: Vec<_> = proc_macro_deps.into_iter().collect();
716        proc_macro_deps.sort();
717        let mut file = File::create(root.join("src/bootstrap/src/utils/proc_macro_deps.rs"))
718            .expect("`proc_macro_deps` should exist");
719        writeln!(
720            &mut file,
721            "/// Do not update manually - use `./x.py test tidy --bless`
722/// Holds all direct and indirect dependencies of proc-macro crates in tree.
723/// See <https://github.com/rust-lang/rust/issues/134863>
724pub static CRATES: &[&str] = &[
725    // tidy-alphabetical-start"
726        )
727        .unwrap();
728        for dep in proc_macro_deps {
729            writeln!(&mut file, "    {dep:?},").unwrap();
730        }
731        writeln!(
732            &mut file,
733            "    // tidy-alphabetical-end
734];"
735        )
736        .unwrap();
737    } else {
738        let mut error_found = false;
739
740        for missing in proc_macro_deps.difference(&expected) {
741            error_found = true;
742            check.error(format!(
743                "proc-macro crate dependency `{missing}` is not registered in `src/bootstrap/src/utils/proc_macro_deps.rs`",
744            ));
745        }
746        for extra in expected.difference(&proc_macro_deps) {
747            error_found = true;
748            check.error(format!(
749                "`{extra}` is registered in `src/bootstrap/src/utils/proc_macro_deps.rs`, but is not a proc-macro crate dependency",
750            ));
751        }
752        if error_found {
753            check.message("Run `./x.py test tidy --bless` to regenerate the list");
754        }
755    }
756}
757
758/// Used to skip a check if a submodule is not checked out, and not in a CI environment.
759///
760/// This helps prevent enforcing developers to fetch submodules for tidy.
761pub fn has_missing_submodule(root: &Path, submodules: &[&str], is_ci: bool) -> bool {
762    !is_ci
763        && submodules.iter().any(|submodule| {
764            let path = root.join(submodule);
765            !path.exists()
766            // If the directory is empty, we can consider it as an uninitialized submodule.
767            || read_dir(path).unwrap().next().is_none()
768        })
769}
770
771/// Check that all licenses of runtime dependencies are in the valid list in `LICENSES`.
772///
773/// Unlike for tools we don't allow exceptions to the `LICENSES` list for the runtime with the sole
774/// exception of `fortanix-sgx-abi` which is only used on x86_64-fortanix-unknown-sgx.
775fn check_runtime_license_exceptions(metadata: &Metadata, check: &mut RunningCheck) {
776    for pkg in &metadata.packages {
777        if pkg.source.is_none() {
778            // No need to check local packages.
779            continue;
780        }
781        let license = match &pkg.license {
782            Some(license) => license,
783            None => {
784                check
785                    .error(format!("dependency `{}` does not define a license expression", pkg.id));
786                continue;
787            }
788        };
789        if !LICENSES.contains(&license.as_str()) {
790            // This is a specific exception because SGX is considered "third party".
791            // See https://github.com/rust-lang/rust/issues/62620 for more.
792            // In general, these should never be added and this exception
793            // should not be taken as precedent for any new target.
794            if *pkg.name == "fortanix-sgx-abi" && pkg.license.as_deref() == Some("MPL-2.0") {
795                continue;
796            }
797
798            check.error(format!("invalid license `{}` in `{}`", license, pkg.id));
799        }
800    }
801}
802
803/// Check that all licenses of tool dependencies are in the valid list in `LICENSES`.
804///
805/// Packages listed in `exceptions` are allowed for tools.
806fn check_license_exceptions(
807    metadata: &Metadata,
808    workspace: &str,
809    exceptions: &[(&str, &str)],
810    check: &mut RunningCheck,
811) {
812    // Validate the EXCEPTIONS list hasn't changed.
813    for (name, license) in exceptions {
814        // Check that the package actually exists.
815        if !metadata.packages.iter().any(|p| *p.name == *name) {
816            check.error(format!(
817                "could not find exception package `{name}` in workspace `{workspace}`\n\
818                Remove from EXCEPTIONS list if it is no longer used.",
819            ));
820        }
821        // Check that the license hasn't changed.
822        for pkg in metadata.packages.iter().filter(|p| *p.name == *name) {
823            match &pkg.license {
824                None => {
825                    check.error(format!(
826                        "dependency exception `{}` in workspace `{workspace}` does not declare a license expression",
827                        pkg.id
828                    ));
829                }
830                Some(pkg_license) => {
831                    if pkg_license.as_str() != *license {
832                        check.error(format!(r#"dependency exception `{name}` license in workspace `{workspace}` has changed
833    previously `{license}` now `{pkg_license}`
834    update EXCEPTIONS for the new license
835"#));
836                    }
837                }
838            }
839        }
840        if LICENSES.contains(license) || LICENSES_TOOLS.contains(license) {
841            check.error(format!(
842                "dependency exception `{name}` is not necessary. `{license}` is an allowed license"
843            ));
844        }
845    }
846
847    let exception_names: Vec<_> = exceptions.iter().map(|(name, _license)| *name).collect();
848
849    // Check if any package does not have a valid license.
850    for pkg in &metadata.packages {
851        if pkg.source.is_none() {
852            // No need to check local packages.
853            continue;
854        }
855        if exception_names.contains(&pkg.name.as_str()) {
856            continue;
857        }
858        let license = match &pkg.license {
859            Some(license) => license,
860            None => {
861                check.error(format!(
862                    "dependency `{}` in workspace `{workspace}` does not define a license expression",
863                    pkg.id
864                ));
865                continue;
866            }
867        };
868        if !LICENSES.contains(&license.as_str()) && !LICENSES_TOOLS.contains(&license.as_str()) {
869            check.error(format!(
870                "invalid license `{}` for package `{}` in workspace `{workspace}`",
871                license, pkg.id
872            ));
873        }
874    }
875}
876
877fn check_runtime_no_duplicate_dependencies(metadata: &Metadata, check: &mut RunningCheck) {
878    let mut seen_pkgs = HashSet::new();
879    for pkg in &metadata.packages {
880        if pkg.source.is_none() {
881            continue;
882        }
883
884        // Skip the `wasi` crate here which the standard library explicitly
885        // depends on two version of (one for the `wasm32-wasip1` target and
886        // another for the `wasm32-wasip2` target).
887        if pkg.name.to_string() != "wasi" && !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}