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    "utf8_iter",
470    "utf8parse",
471    "valuable",
472    "version_check",
473    "wasi",
474    "wasm-encoder",
475    "wasmparser",
476    "windows",
477    "windows-collections",
478    "windows-core",
479    "windows-future",
480    "windows-implement",
481    "windows-interface",
482    "windows-link",
483    "windows-numerics",
484    "windows-result",
485    "windows-strings",
486    "windows-sys",
487    "windows-targets",
488    "windows-threading",
489    "windows_aarch64_gnullvm",
490    "windows_aarch64_msvc",
491    "windows_i686_gnu",
492    "windows_i686_gnullvm",
493    "windows_i686_msvc",
494    "windows_x86_64_gnu",
495    "windows_x86_64_gnullvm",
496    "windows_x86_64_msvc",
497    "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>
498    "writeable",
499    "yoke",
500    "yoke-derive",
501    "zerocopy",
502    "zerocopy-derive",
503    "zerofrom",
504    "zerofrom-derive",
505    "zerotrie",
506    "zerovec",
507    "zerovec-derive",
508    "zlib-rs",
509    // tidy-alphabetical-end
510];
511
512const PERMITTED_STDLIB_DEPS_LOCATION: ListLocation = location!(+2);
513
514const PERMITTED_STDLIB_DEPENDENCIES: &[&str] = &[
515    // tidy-alphabetical-start
516    "addr2line",
517    "adler2",
518    "cc",
519    "cfg-if",
520    "compiler_builtins",
521    "dlmalloc",
522    "foldhash", // FIXME: only appears in Cargo.lock due to https://github.com/rust-lang/cargo/issues/10801
523    "fortanix-sgx-abi",
524    "getopts",
525    "gimli",
526    "hashbrown",
527    "hermit-abi",
528    "libc",
529    "memchr",
530    "miniz_oxide",
531    "moto-rt",
532    "object",
533    "r-efi",
534    "r-efi-alloc",
535    "rand",
536    "rand_core",
537    "rand_xorshift",
538    "rustc-demangle",
539    "rustc-literal-escaper",
540    "shlex",
541    "unwinding",
542    "vex-sdk",
543    "wasip1",
544    "wasip2",
545    "wasip3",
546    "windows-link",
547    "windows-sys@0.61.100", // Enforce the usage of our dummy windows-sys patch. Keep version in sync.
548    "wit-bindgen",
549    // tidy-alphabetical-end
550];
551
552const PERMITTED_CRANELIFT_DEPS_LOCATION: ListLocation = location!(+2);
553
554const PERMITTED_CRANELIFT_DEPENDENCIES: &[&str] = &[
555    // tidy-alphabetical-start
556    "allocator-api2",
557    "anyhow",
558    "arbitrary",
559    "bitflags",
560    "bumpalo",
561    "cfg-if",
562    "cranelift-assembler-x64",
563    "cranelift-assembler-x64-meta",
564    "cranelift-bforest",
565    "cranelift-bitset",
566    "cranelift-codegen",
567    "cranelift-codegen-meta",
568    "cranelift-codegen-shared",
569    "cranelift-control",
570    "cranelift-entity",
571    "cranelift-frontend",
572    "cranelift-isle",
573    "cranelift-jit",
574    "cranelift-module",
575    "cranelift-native",
576    "cranelift-object",
577    "cranelift-srcgen",
578    "crc32fast",
579    "equivalent",
580    "fnv",
581    "foldhash",
582    "gimli",
583    "hashbrown",
584    "heck",
585    "indexmap",
586    "libc",
587    "libloading",
588    "libm",
589    "log",
590    "mach2",
591    "memchr",
592    "object",
593    "proc-macro2",
594    "quote",
595    "regalloc2",
596    "region",
597    "rustc-hash",
598    "serde",
599    "serde_core",
600    "serde_derive",
601    "smallvec",
602    "stable_deref_trait",
603    "syn",
604    "target-lexicon",
605    "unicode-ident",
606    "wasmtime-internal-core",
607    "wasmtime-internal-jit-icache-coherence",
608    "windows-link",
609    "windows-sys",
610    "windows-targets",
611    "windows_aarch64_gnullvm",
612    "windows_aarch64_msvc",
613    "windows_i686_gnu",
614    "windows_i686_gnullvm",
615    "windows_i686_msvc",
616    "windows_x86_64_gnu",
617    "windows_x86_64_gnullvm",
618    "windows_x86_64_msvc",
619    // tidy-alphabetical-end
620];
621
622/// Dependency checks.
623///
624/// `root` is path to the directory with the root `Cargo.toml` (for the workspace). `cargo` is path
625/// to the cargo executable.
626pub fn check(root: &Path, cargo: &Path, tidy_ctx: TidyCtx) {
627    let mut check = tidy_ctx.start_check("deps");
628    let bless = tidy_ctx.is_bless_enabled();
629
630    let mut checked_runtime_licenses = false;
631
632    check_proc_macro_dep_list(root, cargo, bless, &mut check);
633
634    for &WorkspaceInfo { path, exceptions, crates_and_deps, submodules } in WORKSPACES {
635        if has_missing_submodule(root, submodules, tidy_ctx.is_running_on_ci()) {
636            continue;
637        }
638
639        if !root.join(path).join("Cargo.lock").exists() {
640            check.error(format!("the `{path}` workspace doesn't have a Cargo.lock"));
641            continue;
642        }
643
644        let mut cmd = cargo_metadata::MetadataCommand::new();
645        cmd.cargo_path(cargo)
646            .manifest_path(root.join(path).join("Cargo.toml"))
647            .features(cargo_metadata::CargoOpt::AllFeatures)
648            .other_options(vec!["--locked".to_owned()]);
649        let metadata = t!(cmd.exec());
650
651        // Check for packages which have been moved into a different workspace and not updated
652        let absolute_root =
653            if path == "." { root.to_path_buf() } else { t!(std::path::absolute(root.join(path))) };
654        let absolute_root_real = t!(std::path::absolute(&metadata.workspace_root));
655        if absolute_root_real != absolute_root {
656            check.error(format!("{path} is part of another workspace ({} != {}), remove from `WORKSPACES` ({WORKSPACE_LOCATION})", absolute_root.display(), absolute_root_real.display()));
657        }
658        check_license_exceptions(&metadata, path, exceptions, &mut check);
659        if let Some((crates, permitted_deps, location)) = crates_and_deps {
660            let descr = crates.get(0).unwrap_or(&path);
661            check_permitted_dependencies(
662                &metadata,
663                descr,
664                permitted_deps,
665                crates,
666                location,
667                &mut check,
668            );
669        }
670
671        if path == "library" {
672            check_runtime_license_exceptions(&metadata, &mut check);
673            check_runtime_no_duplicate_dependencies(&metadata, &mut check);
674            check_runtime_no_proc_macros(&metadata, &mut check);
675            checked_runtime_licenses = true;
676        }
677    }
678
679    // Sanity check to ensure we don't accidentally remove the workspace containing the runtime
680    // crates.
681    assert!(checked_runtime_licenses);
682}
683
684/// Ensure the list of proc-macro crate transitive dependencies is up to date
685fn check_proc_macro_dep_list(root: &Path, cargo: &Path, bless: bool, check: &mut RunningCheck) {
686    let mut cmd = cargo_metadata::MetadataCommand::new();
687    cmd.cargo_path(cargo)
688        .manifest_path(root.join("Cargo.toml"))
689        .features(cargo_metadata::CargoOpt::AllFeatures)
690        .other_options(vec!["--locked".to_owned()]);
691    let metadata = t!(cmd.exec());
692    let is_proc_macro_pkg = |pkg: &Package| pkg.targets.iter().any(|target| target.is_proc_macro());
693
694    let mut proc_macro_deps = HashSet::new();
695    for pkg in metadata.packages.iter().filter(|pkg| is_proc_macro_pkg(pkg)) {
696        deps_of(&metadata, &pkg.id, &mut proc_macro_deps);
697    }
698    // Remove the proc-macro crates themselves
699    proc_macro_deps.retain(|pkg| !is_proc_macro_pkg(&metadata[pkg]));
700
701    let proc_macro_deps: HashSet<_> =
702        proc_macro_deps.into_iter().map(|dep| metadata[dep].name.as_ref()).collect();
703    let expected = proc_macro_deps::CRATES.iter().copied().collect::<HashSet<_>>();
704
705    let needs_blessing = proc_macro_deps.difference(&expected).next().is_some()
706        || expected.difference(&proc_macro_deps).next().is_some();
707
708    if needs_blessing && bless {
709        let mut proc_macro_deps: Vec<_> = proc_macro_deps.into_iter().collect();
710        proc_macro_deps.sort();
711        let mut file = File::create(root.join("src/bootstrap/src/utils/proc_macro_deps.rs"))
712            .expect("`proc_macro_deps` should exist");
713        writeln!(
714            &mut file,
715            "/// Do not update manually - use `./x.py test tidy --bless`
716/// Holds all direct and indirect dependencies of proc-macro crates in tree.
717/// See <https://github.com/rust-lang/rust/issues/134863>
718pub static CRATES: &[&str] = &[
719    // tidy-alphabetical-start"
720        )
721        .unwrap();
722        for dep in proc_macro_deps {
723            writeln!(&mut file, "    {dep:?},").unwrap();
724        }
725        writeln!(
726            &mut file,
727            "    // tidy-alphabetical-end
728];"
729        )
730        .unwrap();
731    } else {
732        let mut error_found = false;
733
734        for missing in proc_macro_deps.difference(&expected) {
735            error_found = true;
736            check.error(format!(
737                "proc-macro crate dependency `{missing}` is not registered in `src/bootstrap/src/utils/proc_macro_deps.rs`",
738            ));
739        }
740        for extra in expected.difference(&proc_macro_deps) {
741            error_found = true;
742            check.error(format!(
743                "`{extra}` is registered in `src/bootstrap/src/utils/proc_macro_deps.rs`, but is not a proc-macro crate dependency",
744            ));
745        }
746        if error_found {
747            check.message("Run `./x.py test tidy --bless` to regenerate the list");
748        }
749    }
750}
751
752/// Used to skip a check if a submodule is not checked out, and not in a CI environment.
753///
754/// This helps prevent enforcing developers to fetch submodules for tidy.
755pub fn has_missing_submodule(root: &Path, submodules: &[&str], is_ci: bool) -> bool {
756    !is_ci
757        && submodules.iter().any(|submodule| {
758            let path = root.join(submodule);
759            !path.exists()
760            // If the directory is empty, we can consider it as an uninitialized submodule.
761            || read_dir(path).unwrap().next().is_none()
762        })
763}
764
765/// Check that all licenses of runtime dependencies are in the valid list in `LICENSES`.
766///
767/// Unlike for tools we don't allow exceptions to the `LICENSES` list for the runtime with the sole
768/// exception of `fortanix-sgx-abi` which is only used on x86_64-fortanix-unknown-sgx.
769fn check_runtime_license_exceptions(metadata: &Metadata, check: &mut RunningCheck) {
770    for pkg in &metadata.packages {
771        if pkg.source.is_none() {
772            // No need to check local packages.
773            continue;
774        }
775        let license = match &pkg.license {
776            Some(license) => license,
777            None => {
778                check
779                    .error(format!("dependency `{}` does not define a license expression", pkg.id));
780                continue;
781            }
782        };
783        if !LICENSES.contains(&license.as_str()) {
784            // This is a specific exception because SGX is considered "third party".
785            // See https://github.com/rust-lang/rust/issues/62620 for more.
786            // In general, these should never be added and this exception
787            // should not be taken as precedent for any new target.
788            if *pkg.name == "fortanix-sgx-abi" && pkg.license.as_deref() == Some("MPL-2.0") {
789                continue;
790            }
791
792            check.error(format!("invalid license `{}` in `{}`", license, pkg.id));
793        }
794    }
795}
796
797/// Check that all licenses of tool dependencies are in the valid list in `LICENSES`.
798///
799/// Packages listed in `exceptions` are allowed for tools.
800fn check_license_exceptions(
801    metadata: &Metadata,
802    workspace: &str,
803    exceptions: &[(&str, &str)],
804    check: &mut RunningCheck,
805) {
806    // Validate the EXCEPTIONS list hasn't changed.
807    for (name, license) in exceptions {
808        // Check that the package actually exists.
809        if !metadata.packages.iter().any(|p| *p.name == *name) {
810            check.error(format!(
811                "could not find exception package `{name}` in workspace `{workspace}`\n\
812                Remove from EXCEPTIONS list if it is no longer used.",
813            ));
814        }
815        // Check that the license hasn't changed.
816        for pkg in metadata.packages.iter().filter(|p| *p.name == *name) {
817            match &pkg.license {
818                None => {
819                    check.error(format!(
820                        "dependency exception `{}` in workspace `{workspace}` does not declare a license expression",
821                        pkg.id
822                    ));
823                }
824                Some(pkg_license) => {
825                    if pkg_license.as_str() != *license {
826                        check.error(format!(r#"dependency exception `{name}` license in workspace `{workspace}` has changed
827    previously `{license}` now `{pkg_license}`
828    update EXCEPTIONS for the new license
829"#));
830                    }
831                }
832            }
833        }
834        if LICENSES.contains(license) || LICENSES_TOOLS.contains(license) {
835            check.error(format!(
836                "dependency exception `{name}` is not necessary. `{license}` is an allowed license"
837            ));
838        }
839    }
840
841    let exception_names: Vec<_> = exceptions.iter().map(|(name, _license)| *name).collect();
842
843    // Check if any package does not have a valid license.
844    for pkg in &metadata.packages {
845        if pkg.source.is_none() {
846            // No need to check local packages.
847            continue;
848        }
849        if exception_names.contains(&pkg.name.as_str()) {
850            continue;
851        }
852        let license = match &pkg.license {
853            Some(license) => license,
854            None => {
855                check.error(format!(
856                    "dependency `{}` in workspace `{workspace}` does not define a license expression",
857                    pkg.id
858                ));
859                continue;
860            }
861        };
862        if !LICENSES.contains(&license.as_str()) && !LICENSES_TOOLS.contains(&license.as_str()) {
863            check.error(format!(
864                "invalid license `{}` for package `{}` in workspace `{workspace}`",
865                license, pkg.id
866            ));
867        }
868    }
869}
870
871fn check_runtime_no_duplicate_dependencies(metadata: &Metadata, check: &mut RunningCheck) {
872    let mut seen_pkgs = HashSet::new();
873    for pkg in &metadata.packages {
874        if pkg.source.is_none() {
875            continue;
876        }
877
878        if !seen_pkgs.insert(&*pkg.name) {
879            check.error(format!(
880                "duplicate package `{}` is not allowed for the standard library",
881                pkg.name
882            ));
883        }
884    }
885}
886
887fn check_runtime_no_proc_macros(metadata: &Metadata, check: &mut RunningCheck) {
888    for pkg in &metadata.packages {
889        if pkg.targets.iter().any(|target| target.is_proc_macro()) {
890            check.error(format!(
891                "proc macro `{}` is not allowed as standard library dependency.\n\
892                Using proc macros in the standard library would break cross-compilation \
893                as proc-macros don't get shipped for the host tuple.",
894                pkg.name
895            ));
896        }
897    }
898}
899
900/// Checks the dependency of `restricted_dependency_crates` at the given path. Changes `bad` to
901/// `true` if a check failed.
902///
903/// Specifically, this checks that the dependencies are on the `permitted_dependencies`.
904fn check_permitted_dependencies(
905    metadata: &Metadata,
906    descr: &str,
907    permitted_dependencies: &[&'static str],
908    restricted_dependency_crates: &[&'static str],
909    permitted_location: ListLocation,
910    check: &mut RunningCheck,
911) {
912    let mut has_permitted_dep_error = false;
913    let mut deps = HashSet::new();
914    for to_check in restricted_dependency_crates {
915        let to_check = pkg_from_name(metadata, to_check);
916        deps_of(metadata, &to_check.id, &mut deps);
917    }
918
919    // Check that the PERMITTED_DEPENDENCIES does not have unused entries.
920    for permitted in permitted_dependencies {
921        fn compare(pkg: &Package, permitted: &str) -> bool {
922            if let Some((name, version)) = permitted.split_once("@") {
923                let Ok(version) = Version::parse(version) else {
924                    return false;
925                };
926                *pkg.name == name && pkg.version == version
927            } else {
928                *pkg.name == permitted
929            }
930        }
931        if !deps.iter().any(|dep_id| compare(pkg_from_id(metadata, dep_id), permitted)) {
932            check.error(format!(
933                "could not find allowed package `{permitted}`\n\
934                Remove from PERMITTED_DEPENDENCIES list if it is no longer used.",
935            ));
936            has_permitted_dep_error = true;
937        }
938    }
939
940    // Get in a convenient form.
941    let permitted_dependencies: HashMap<_, _> = permitted_dependencies
942        .iter()
943        .map(|s| {
944            if let Some((name, version)) = s.split_once('@') {
945                (name, Version::parse(version).ok())
946            } else {
947                (*s, None)
948            }
949        })
950        .collect();
951
952    for dep in deps {
953        let dep = pkg_from_id(metadata, dep);
954        // If this path is in-tree, we don't require it to be explicitly permitted.
955        if dep.source.is_some() {
956            let is_eq = if let Some(version) = permitted_dependencies.get(dep.name.as_str()) {
957                if let Some(version) = version { version == &dep.version } else { true }
958            } else {
959                false
960            };
961            if !is_eq {
962                check.error(format!("Dependency for {descr} not explicitly permitted: {}", dep.id));
963                has_permitted_dep_error = true;
964            }
965        }
966    }
967
968    if has_permitted_dep_error {
969        eprintln!("Go to `{}:{}` for the list.", permitted_location.path, permitted_location.line);
970    }
971}
972
973/// Finds a package with the given name.
974fn pkg_from_name<'a>(metadata: &'a Metadata, name: &'static str) -> &'a Package {
975    let mut i = metadata.packages.iter().filter(|p| *p.name == name);
976    let result =
977        i.next().unwrap_or_else(|| panic!("could not find package `{name}` in package list"));
978    assert!(i.next().is_none(), "more than one package found for `{name}`");
979    result
980}
981
982fn pkg_from_id<'a>(metadata: &'a Metadata, id: &PackageId) -> &'a Package {
983    metadata.packages.iter().find(|p| &p.id == id).unwrap()
984}
985
986/// Recursively find all dependencies.
987fn deps_of<'a>(metadata: &'a Metadata, pkg_id: &'a PackageId, result: &mut HashSet<&'a PackageId>) {
988    if !result.insert(pkg_id) {
989        return;
990    }
991    let node = metadata
992        .resolve
993        .as_ref()
994        .unwrap()
995        .nodes
996        .iter()
997        .find(|n| &n.id == pkg_id)
998        .unwrap_or_else(|| panic!("could not find `{pkg_id}` in resolve"));
999    for dep in &node.deps {
1000        deps_of(metadata, &dep.pkg, result);
1001    }
1002}