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