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