Skip to main content

tidy/
deps.rs

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