Skip to main content

tidy/
deps.rs

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