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