Skip to main content

tidy/
deps.rs

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