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