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