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    "wasip3",
545    "windows-link",
546    "windows-sys",
547    "windows-targets",
548    "windows_aarch64_gnullvm",
549    "windows_aarch64_msvc",
550    "windows_i686_gnu",
551    "windows_i686_gnullvm",
552    "windows_i686_msvc",
553    "windows_x86_64_gnu",
554    "windows_x86_64_gnullvm",
555    "windows_x86_64_msvc",
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    "object",
601    "proc-macro2",
602    "quote",
603    "regalloc2",
604    "region",
605    "rustc-hash",
606    "serde",
607    "serde_core",
608    "serde_derive",
609    "smallvec",
610    "stable_deref_trait",
611    "syn",
612    "target-lexicon",
613    "unicode-ident",
614    "wasmtime-internal-core",
615    "wasmtime-internal-jit-icache-coherence",
616    "windows-link",
617    "windows-sys",
618    "windows-targets",
619    "windows_aarch64_gnullvm",
620    "windows_aarch64_msvc",
621    "windows_i686_gnu",
622    "windows_i686_gnullvm",
623    "windows_i686_msvc",
624    "windows_x86_64_gnu",
625    "windows_x86_64_gnullvm",
626    "windows_x86_64_msvc",
627    // tidy-alphabetical-end
628];
629
630/// Dependency checks.
631///
632/// `root` is path to the directory with the root `Cargo.toml` (for the workspace). `cargo` is path
633/// to the cargo executable.
634pub fn check(root: &Path, cargo: &Path, tidy_ctx: TidyCtx) {
635    let mut check = tidy_ctx.start_check("deps");
636    let bless = tidy_ctx.is_bless_enabled();
637
638    let mut checked_runtime_licenses = false;
639
640    check_proc_macro_dep_list(root, cargo, bless, &mut check);
641
642    for &WorkspaceInfo { path, exceptions, crates_and_deps, submodules } in WORKSPACES {
643        if has_missing_submodule(root, submodules, tidy_ctx.is_running_on_ci()) {
644            continue;
645        }
646
647        if !root.join(path).join("Cargo.lock").exists() {
648            check.error(format!("the `{path}` workspace doesn't have a Cargo.lock"));
649            continue;
650        }
651
652        let mut cmd = cargo_metadata::MetadataCommand::new();
653        cmd.cargo_path(cargo)
654            .manifest_path(root.join(path).join("Cargo.toml"))
655            .features(cargo_metadata::CargoOpt::AllFeatures)
656            .other_options(vec!["--locked".to_owned()]);
657        let metadata = t!(cmd.exec());
658
659        // Check for packages which have been moved into a different workspace and not updated
660        let absolute_root =
661            if path == "." { root.to_path_buf() } else { t!(std::path::absolute(root.join(path))) };
662        let absolute_root_real = t!(std::path::absolute(&metadata.workspace_root));
663        if absolute_root_real != absolute_root {
664            check.error(format!("{path} is part of another workspace ({} != {}), remove from `WORKSPACES` ({WORKSPACE_LOCATION})", absolute_root.display(), absolute_root_real.display()));
665        }
666        check_license_exceptions(&metadata, path, exceptions, &mut check);
667        if let Some((crates, permitted_deps, location)) = crates_and_deps {
668            let descr = crates.get(0).unwrap_or(&path);
669            check_permitted_dependencies(
670                &metadata,
671                descr,
672                permitted_deps,
673                crates,
674                location,
675                &mut check,
676            );
677        }
678
679        if path == "library" {
680            check_runtime_license_exceptions(&metadata, &mut check);
681            check_runtime_no_duplicate_dependencies(&metadata, &mut check);
682            check_runtime_no_proc_macros(&metadata, &mut check);
683            checked_runtime_licenses = true;
684        }
685    }
686
687    // Sanity check to ensure we don't accidentally remove the workspace containing the runtime
688    // crates.
689    assert!(checked_runtime_licenses);
690}
691
692/// Ensure the list of proc-macro crate transitive dependencies is up to date
693fn check_proc_macro_dep_list(root: &Path, cargo: &Path, bless: bool, check: &mut RunningCheck) {
694    let mut cmd = cargo_metadata::MetadataCommand::new();
695    cmd.cargo_path(cargo)
696        .manifest_path(root.join("Cargo.toml"))
697        .features(cargo_metadata::CargoOpt::AllFeatures)
698        .other_options(vec!["--locked".to_owned()]);
699    let metadata = t!(cmd.exec());
700    let is_proc_macro_pkg = |pkg: &Package| pkg.targets.iter().any(|target| target.is_proc_macro());
701
702    let mut proc_macro_deps = HashSet::new();
703    for pkg in metadata.packages.iter().filter(|pkg| is_proc_macro_pkg(pkg)) {
704        deps_of(&metadata, &pkg.id, &mut proc_macro_deps);
705    }
706    // Remove the proc-macro crates themselves
707    proc_macro_deps.retain(|pkg| !is_proc_macro_pkg(&metadata[pkg]));
708
709    let proc_macro_deps: HashSet<_> =
710        proc_macro_deps.into_iter().map(|dep| metadata[dep].name.as_ref()).collect();
711    let expected = proc_macro_deps::CRATES.iter().copied().collect::<HashSet<_>>();
712
713    let needs_blessing = proc_macro_deps.difference(&expected).next().is_some()
714        || expected.difference(&proc_macro_deps).next().is_some();
715
716    if needs_blessing && bless {
717        let mut proc_macro_deps: Vec<_> = proc_macro_deps.into_iter().collect();
718        proc_macro_deps.sort();
719        let mut file = File::create(root.join("src/bootstrap/src/utils/proc_macro_deps.rs"))
720            .expect("`proc_macro_deps` should exist");
721        writeln!(
722            &mut file,
723            "/// Do not update manually - use `./x.py test tidy --bless`
724/// Holds all direct and indirect dependencies of proc-macro crates in tree.
725/// See <https://github.com/rust-lang/rust/issues/134863>
726pub static CRATES: &[&str] = &[
727    // tidy-alphabetical-start"
728        )
729        .unwrap();
730        for dep in proc_macro_deps {
731            writeln!(&mut file, "    {dep:?},").unwrap();
732        }
733        writeln!(
734            &mut file,
735            "    // tidy-alphabetical-end
736];"
737        )
738        .unwrap();
739    } else {
740        let mut error_found = false;
741
742        for missing in proc_macro_deps.difference(&expected) {
743            error_found = true;
744            check.error(format!(
745                "proc-macro crate dependency `{missing}` is not registered in `src/bootstrap/src/utils/proc_macro_deps.rs`",
746            ));
747        }
748        for extra in expected.difference(&proc_macro_deps) {
749            error_found = true;
750            check.error(format!(
751                "`{extra}` is registered in `src/bootstrap/src/utils/proc_macro_deps.rs`, but is not a proc-macro crate dependency",
752            ));
753        }
754        if error_found {
755            check.message("Run `./x.py test tidy --bless` to regenerate the list");
756        }
757    }
758}
759
760/// Used to skip a check if a submodule is not checked out, and not in a CI environment.
761///
762/// This helps prevent enforcing developers to fetch submodules for tidy.
763pub fn has_missing_submodule(root: &Path, submodules: &[&str], is_ci: bool) -> bool {
764    !is_ci
765        && submodules.iter().any(|submodule| {
766            let path = root.join(submodule);
767            !path.exists()
768            // If the directory is empty, we can consider it as an uninitialized submodule.
769            || read_dir(path).unwrap().next().is_none()
770        })
771}
772
773/// Check that all licenses of runtime dependencies are in the valid list in `LICENSES`.
774///
775/// Unlike for tools we don't allow exceptions to the `LICENSES` list for the runtime with the sole
776/// exception of `fortanix-sgx-abi` which is only used on x86_64-fortanix-unknown-sgx.
777fn check_runtime_license_exceptions(metadata: &Metadata, check: &mut RunningCheck) {
778    for pkg in &metadata.packages {
779        if pkg.source.is_none() {
780            // No need to check local packages.
781            continue;
782        }
783        let license = match &pkg.license {
784            Some(license) => license,
785            None => {
786                check
787                    .error(format!("dependency `{}` does not define a license expression", pkg.id));
788                continue;
789            }
790        };
791        if !LICENSES.contains(&license.as_str()) {
792            // This is a specific exception because SGX is considered "third party".
793            // See https://github.com/rust-lang/rust/issues/62620 for more.
794            // In general, these should never be added and this exception
795            // should not be taken as precedent for any new target.
796            if *pkg.name == "fortanix-sgx-abi" && pkg.license.as_deref() == Some("MPL-2.0") {
797                continue;
798            }
799
800            check.error(format!("invalid license `{}` in `{}`", license, pkg.id));
801        }
802    }
803}
804
805/// Check that all licenses of tool dependencies are in the valid list in `LICENSES`.
806///
807/// Packages listed in `exceptions` are allowed for tools.
808fn check_license_exceptions(
809    metadata: &Metadata,
810    workspace: &str,
811    exceptions: &[(&str, &str)],
812    check: &mut RunningCheck,
813) {
814    // Validate the EXCEPTIONS list hasn't changed.
815    for (name, license) in exceptions {
816        // Check that the package actually exists.
817        if !metadata.packages.iter().any(|p| *p.name == *name) {
818            check.error(format!(
819                "could not find exception package `{name}` in workspace `{workspace}`\n\
820                Remove from EXCEPTIONS list if it is no longer used.",
821            ));
822        }
823        // Check that the license hasn't changed.
824        for pkg in metadata.packages.iter().filter(|p| *p.name == *name) {
825            match &pkg.license {
826                None => {
827                    check.error(format!(
828                        "dependency exception `{}` in workspace `{workspace}` does not declare a license expression",
829                        pkg.id
830                    ));
831                }
832                Some(pkg_license) => {
833                    if pkg_license.as_str() != *license {
834                        check.error(format!(r#"dependency exception `{name}` license in workspace `{workspace}` has changed
835    previously `{license}` now `{pkg_license}`
836    update EXCEPTIONS for the new license
837"#));
838                    }
839                }
840            }
841        }
842        if LICENSES.contains(license) || LICENSES_TOOLS.contains(license) {
843            check.error(format!(
844                "dependency exception `{name}` is not necessary. `{license}` is an allowed license"
845            ));
846        }
847    }
848
849    let exception_names: Vec<_> = exceptions.iter().map(|(name, _license)| *name).collect();
850
851    // Check if any package does not have a valid license.
852    for pkg in &metadata.packages {
853        if pkg.source.is_none() {
854            // No need to check local packages.
855            continue;
856        }
857        if exception_names.contains(&pkg.name.as_str()) {
858            continue;
859        }
860        let license = match &pkg.license {
861            Some(license) => license,
862            None => {
863                check.error(format!(
864                    "dependency `{}` in workspace `{workspace}` does not define a license expression",
865                    pkg.id
866                ));
867                continue;
868            }
869        };
870        if !LICENSES.contains(&license.as_str()) && !LICENSES_TOOLS.contains(&license.as_str()) {
871            check.error(format!(
872                "invalid license `{}` for package `{}` in workspace `{workspace}`",
873                license, pkg.id
874            ));
875        }
876    }
877}
878
879fn check_runtime_no_duplicate_dependencies(metadata: &Metadata, check: &mut RunningCheck) {
880    let mut seen_pkgs = HashSet::new();
881    for pkg in &metadata.packages {
882        if pkg.source.is_none() {
883            continue;
884        }
885
886        if !seen_pkgs.insert(&*pkg.name) {
887            check.error(format!(
888                "duplicate package `{}` is not allowed for the standard library",
889                pkg.name
890            ));
891        }
892    }
893}
894
895fn check_runtime_no_proc_macros(metadata: &Metadata, check: &mut RunningCheck) {
896    for pkg in &metadata.packages {
897        if pkg.targets.iter().any(|target| target.is_proc_macro()) {
898            check.error(format!(
899                "proc macro `{}` is not allowed as standard library dependency.\n\
900                Using proc macros in the standard library would break cross-compilation \
901                as proc-macros don't get shipped for the host tuple.",
902                pkg.name
903            ));
904        }
905    }
906}
907
908/// Checks the dependency of `restricted_dependency_crates` at the given path. Changes `bad` to
909/// `true` if a check failed.
910///
911/// Specifically, this checks that the dependencies are on the `permitted_dependencies`.
912fn check_permitted_dependencies(
913    metadata: &Metadata,
914    descr: &str,
915    permitted_dependencies: &[&'static str],
916    restricted_dependency_crates: &[&'static str],
917    permitted_location: ListLocation,
918    check: &mut RunningCheck,
919) {
920    let mut has_permitted_dep_error = false;
921    let mut deps = HashSet::new();
922    for to_check in restricted_dependency_crates {
923        let to_check = pkg_from_name(metadata, to_check);
924        deps_of(metadata, &to_check.id, &mut deps);
925    }
926
927    // Check that the PERMITTED_DEPENDENCIES does not have unused entries.
928    for permitted in permitted_dependencies {
929        fn compare(pkg: &Package, permitted: &str) -> bool {
930            if let Some((name, version)) = permitted.split_once("@") {
931                let Ok(version) = Version::parse(version) else {
932                    return false;
933                };
934                *pkg.name == name && pkg.version == version
935            } else {
936                *pkg.name == permitted
937            }
938        }
939        if !deps.iter().any(|dep_id| compare(pkg_from_id(metadata, dep_id), permitted)) {
940            check.error(format!(
941                "could not find allowed package `{permitted}`\n\
942                Remove from PERMITTED_DEPENDENCIES list if it is no longer used.",
943            ));
944            has_permitted_dep_error = true;
945        }
946    }
947
948    // Get in a convenient form.
949    let permitted_dependencies: HashMap<_, _> = permitted_dependencies
950        .iter()
951        .map(|s| {
952            if let Some((name, version)) = s.split_once('@') {
953                (name, Version::parse(version).ok())
954            } else {
955                (*s, None)
956            }
957        })
958        .collect();
959
960    for dep in deps {
961        let dep = pkg_from_id(metadata, dep);
962        // If this path is in-tree, we don't require it to be explicitly permitted.
963        if dep.source.is_some() {
964            let is_eq = if let Some(version) = permitted_dependencies.get(dep.name.as_str()) {
965                if let Some(version) = version { version == &dep.version } else { true }
966            } else {
967                false
968            };
969            if !is_eq {
970                check.error(format!("Dependency for {descr} not explicitly permitted: {}", dep.id));
971                has_permitted_dep_error = true;
972            }
973        }
974    }
975
976    if has_permitted_dep_error {
977        eprintln!("Go to `{}:{}` for the list.", permitted_location.path, permitted_location.line);
978    }
979}
980
981/// Finds a package with the given name.
982fn pkg_from_name<'a>(metadata: &'a Metadata, name: &'static str) -> &'a Package {
983    let mut i = metadata.packages.iter().filter(|p| *p.name == name);
984    let result =
985        i.next().unwrap_or_else(|| panic!("could not find package `{name}` in package list"));
986    assert!(i.next().is_none(), "more than one package found for `{name}`");
987    result
988}
989
990fn pkg_from_id<'a>(metadata: &'a Metadata, id: &PackageId) -> &'a Package {
991    metadata.packages.iter().find(|p| &p.id == id).unwrap()
992}
993
994/// Recursively find all dependencies.
995fn deps_of<'a>(metadata: &'a Metadata, pkg_id: &'a PackageId, result: &mut HashSet<&'a PackageId>) {
996    if !result.insert(pkg_id) {
997        return;
998    }
999    let node = metadata
1000        .resolve
1001        .as_ref()
1002        .unwrap()
1003        .nodes
1004        .iter()
1005        .find(|n| &n.id == pkg_id)
1006        .unwrap_or_else(|| panic!("could not find `{pkg_id}` in resolve"));
1007    for dep in &node.deps {
1008        deps_of(metadata, &dep.pkg, result);
1009    }
1010}