Skip to main content

tidy/
deps.rs

1//! Checks the licenses of third-party dependencies.
2
3use std::collections::{BTreeSet, HashMap, HashSet};
4use std::fmt::{Display, Formatter};
5use std::fs::{self, read_dir};
6use std::io;
7use std::path::Path;
8
9use cargo_metadata::semver::Version;
10use cargo_metadata::{Metadata, Package, PackageId};
11
12use crate::diagnostics::{RunningCheck, TidyCtx};
13
14#[derive(Clone, Copy)]
15struct ListLocation {
16    path: &'static str,
17    line: u32,
18}
19
20impl Display for ListLocation {
21    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
22        write!(f, "{}:{}", self.path, self.line)
23    }
24}
25
26/// Creates a [`ListLocation`] for the current location (with an additional offset to the actual list start);
27macro_rules! location {
28    (+ $offset:literal) => {
29        ListLocation { path: file!(), line: line!() + $offset }
30    };
31}
32
33/// These are licenses that are allowed for all crates, including the runtime,
34/// rustc, tools, etc.
35#[rustfmt::skip]
36const LICENSES: &[&str] = &[
37    // tidy-alphabetical-start
38    "(MIT OR Apache-2.0) AND MIT",
39    "0BSD OR MIT OR Apache-2.0",                           // adler2 license
40    "Apache-2.0 / MIT",
41    "Apache-2.0 OR ISC OR MIT",
42    "Apache-2.0 OR MIT",
43    "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT", // wasi license
44    "Apache-2.0/MIT",
45    "BSD-2-Clause OR Apache-2.0 OR MIT",                   // zerocopy
46    "BSD-2-Clause OR MIT OR Apache-2.0",
47    "BSD-3-Clause/MIT",
48    "CC0-1.0 OR MIT-0 OR Apache-2.0",
49    "ISC",
50    "MIT / Apache-2.0",
51    "MIT AND (MIT OR Apache-2.0)",
52    "MIT AND Apache-2.0 WITH LLVM-exception AND (MIT OR Apache-2.0)", // compiler-builtins
53    "MIT OR Apache-2.0 OR BSD-1-Clause",
54    "MIT OR Apache-2.0 OR LGPL-2.1-or-later",              // r-efi, r-efi-alloc; LGPL is not acceptable, but we use it under MIT OR Apache-2.0
55    "MIT OR Apache-2.0 OR Zlib",                           // tinyvec_macros
56    "MIT OR Apache-2.0",
57    "MIT OR Zlib OR Apache-2.0",                           // miniz_oxide
58    "MIT",
59    "MIT/Apache-2.0",
60    "Unlicense OR MIT",
61    "Unlicense/MIT",
62    "Zlib",                                                // foldhash (FIXME: see PERMITTED_STDLIB_DEPENDENCIES)
63    // tidy-alphabetical-end
64];
65
66/// These are licenses that are allowed for rustc, tools, etc. But not for the runtime!
67#[rustfmt::skip]
68const LICENSES_TOOLS: &[&str] = &[
69    // tidy-alphabetical-start
70    "(Apache-2.0 OR MIT) AND BSD-3-Clause",
71    "(MIT OR Apache-2.0) AND Unicode-3.0",                 // unicode_ident (1.0.14)
72    "(MIT OR Apache-2.0) AND Unicode-DFS-2016",            // unicode_ident (1.0.12)
73    "0BSD",
74    "Apache-2.0 AND ISC",
75    "Apache-2.0 OR BSL-1.0",  // BSL is not acceptable, but we use it under Apache-2.0
76    "Apache-2.0 OR GPL-2.0-only",
77    "Apache-2.0 WITH LLVM-exception",
78    "Apache-2.0",
79    "BSD-2-Clause",
80    "BSD-3-Clause",
81    "CC0-1.0 OR Apache-2.0 OR Apache-2.0 WITH LLVM-exception",
82    "CC0-1.0",
83    "Unicode-3.0",                                         // icu4x
84    "Unicode-DFS-2016",                                    // tinystr
85    "Zlib OR Apache-2.0 OR MIT",                           // tinyvec
86    "Zlib",
87    // tidy-alphabetical-end
88];
89
90type ExceptionList = &'static [(&'static str, &'static str)];
91
92#[derive(Clone, Copy)]
93pub(crate) struct WorkspaceInfo<'a> {
94    /// Path to the directory containing the workspace root Cargo.toml file.
95    pub(crate) path: &'a str,
96    /// The list of license exceptions.
97    pub(crate) exceptions: ExceptionList,
98    /// Optionally:
99    /// * A list of crates for which dependencies need to be explicitly allowed.
100    /// * The list of allowed dependencies.
101    /// * The source code location of the allowed dependencies list
102    crates_and_deps: Option<(&'a [&'a str], &'a [&'a str], ListLocation)>,
103    /// Submodules required for the workspace
104    pub(crate) submodules: &'a [&'a str],
105}
106
107const WORKSPACE_LOCATION: ListLocation = location!(+4);
108
109/// The workspaces to check for licensing and optionally permitted dependencies.
110// FIXME auto detect all cargo workspaces
111pub(crate) const WORKSPACES: &[WorkspaceInfo<'static>] = &[
112    // The root workspace has to be first for check_rustfix to work.
113    WorkspaceInfo {
114        path: ".",
115        exceptions: EXCEPTIONS,
116        crates_and_deps: Some((
117            &["rustc-main"],
118            PERMITTED_RUSTC_DEPENDENCIES,
119            PERMITTED_RUSTC_DEPS_LOCATION,
120        )),
121        submodules: &[],
122    },
123    WorkspaceInfo {
124        path: "library",
125        exceptions: EXCEPTIONS_STDLIB,
126        crates_and_deps: Some((
127            &["sysroot"],
128            PERMITTED_STDLIB_DEPENDENCIES,
129            PERMITTED_STDLIB_DEPS_LOCATION,
130        )),
131        submodules: &[],
132    },
133    WorkspaceInfo {
134        path: "library/stdarch",
135        exceptions: EXCEPTIONS_STDARCH,
136        crates_and_deps: None,
137        submodules: &[],
138    },
139    WorkspaceInfo {
140        path: "compiler/rustc_codegen_cranelift",
141        exceptions: EXCEPTIONS_CRANELIFT,
142        crates_and_deps: Some((
143            &["rustc_codegen_cranelift"],
144            PERMITTED_CRANELIFT_DEPENDENCIES,
145            PERMITTED_CRANELIFT_DEPS_LOCATION,
146        )),
147        submodules: &[],
148    },
149    WorkspaceInfo {
150        path: "compiler/rustc_codegen_gcc",
151        exceptions: EXCEPTIONS_GCC,
152        crates_and_deps: None,
153        submodules: &[],
154    },
155    WorkspaceInfo {
156        path: "src/bootstrap",
157        exceptions: EXCEPTIONS_BOOTSTRAP,
158        crates_and_deps: None,
159        submodules: &[],
160    },
161    WorkspaceInfo {
162        path: "src/tools/cargo",
163        exceptions: EXCEPTIONS_CARGO,
164        crates_and_deps: None,
165        submodules: &["src/tools/cargo"],
166    },
167    // FIXME uncomment once all deps are vendored
168    //  WorkspaceInfo {
169    //      path: "src/tools/miri/test-cargo-miri",
170    //      crates_and_deps: None
171    //      submodules: &[],
172    //  },
173    // WorkspaceInfo {
174    //      path: "src/tools/miri/test_dependencies",
175    //      crates_and_deps: None,
176    //      submodules: &[],
177    //  }
178    WorkspaceInfo {
179        path: "src/tools/rust-analyzer",
180        exceptions: EXCEPTIONS_RUST_ANALYZER,
181        crates_and_deps: None,
182        submodules: &[],
183    },
184    WorkspaceInfo {
185        path: "src/tools/rustbook",
186        exceptions: EXCEPTIONS_RUSTBOOK,
187        crates_and_deps: None,
188        submodules: &["src/doc/book", "src/doc/reference"],
189    },
190    WorkspaceInfo {
191        path: "src/tools/rustc-perf",
192        exceptions: EXCEPTIONS_RUSTC_PERF,
193        crates_and_deps: None,
194        submodules: &["src/tools/rustc-perf"],
195    },
196    WorkspaceInfo {
197        path: "tests/run-make-cargo/uefi-qemu/uefi_qemu_test",
198        exceptions: EXCEPTIONS_UEFI_QEMU_TEST,
199        crates_and_deps: None,
200        submodules: &[],
201    },
202];
203
204/// These are exceptions to Rust's permissive licensing policy, and
205/// should be considered bugs. Exceptions are only allowed in Rust
206/// tooling. It is _crucial_ that no exception crates be dependencies
207/// of the Rust runtime (std/test).
208#[rustfmt::skip]
209const EXCEPTIONS: ExceptionList = &[
210    // tidy-alphabetical-start
211    ("colored", "MPL-2.0"),                                  // rustfmt
212    ("option-ext", "MPL-2.0"),                               // cargo-miri (via `directories`)
213    // tidy-alphabetical-end
214];
215
216/// These are exceptions to Rust's permissive licensing policy, and
217/// should be considered bugs. Exceptions are only allowed in Rust
218/// tooling. It is _crucial_ that no exception crates be dependencies
219/// of the Rust runtime (std/test).
220#[rustfmt::skip]
221const EXCEPTIONS_STDLIB: ExceptionList = &[
222    // tidy-alphabetical-start
223    ("fortanix-sgx-abi", "MPL-2.0"), // libstd but only for `sgx` target. FIXME: this dependency violates the documentation comment above.
224    // tidy-alphabetical-end
225];
226
227const EXCEPTIONS_CARGO: ExceptionList = &[
228    // tidy-alphabetical-start
229    ("bitmaps", "MPL-2.0+"),
230    ("im-rc", "MPL-2.0+"),
231    ("sized-chunks", "MPL-2.0+"),
232    // tidy-alphabetical-end
233];
234
235const EXCEPTIONS_RUST_ANALYZER: ExceptionList = &[
236    // tidy-alphabetical-start
237    ("option-ext", "MPL-2.0"),
238    // tidy-alphabetical-end
239];
240
241const EXCEPTIONS_RUSTC_PERF: ExceptionList = &[
242    // tidy-alphabetical-start
243    ("aws-lc-rs", "ISC AND (Apache-2.0 OR ISC)"),
244    (
245        "aws-lc-sys",
246        "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)",
247    ),
248    ("brotli", "BSD-3-Clause AND MIT"),
249    ("fast-srgb8", "MIT OR Apache-2.0 OR CC0-1.0"),
250    ("inferno", "CDDL-1.0"),
251    ("option-ext", "MPL-2.0"),
252    ("wasite", "Apache-2.0 OR BSL-1.0 OR MIT"),
253    ("webpki-root-certs", "CDLA-Permissive-2.0"),
254    ("whoami", "Apache-2.0 OR BSL-1.0 OR MIT"),
255    // tidy-alphabetical-end
256];
257
258const EXCEPTIONS_RUSTBOOK: ExceptionList = &[
259    // tidy-alphabetical-start
260    ("font-awesome-as-a-crate", "CC-BY-4.0 AND MIT"),
261    ("mdbook-core", "MPL-2.0"),
262    ("mdbook-driver", "MPL-2.0"),
263    ("mdbook-html", "MPL-2.0"),
264    ("mdbook-markdown", "MPL-2.0"),
265    ("mdbook-preprocessor", "MPL-2.0"),
266    ("mdbook-renderer", "MPL-2.0"),
267    ("mdbook-summary", "MPL-2.0"),
268    // tidy-alphabetical-end
269];
270
271const EXCEPTIONS_STDARCH: ExceptionList = &[];
272
273const EXCEPTIONS_CRANELIFT: ExceptionList = &[];
274
275const EXCEPTIONS_GCC: ExceptionList = &[
276    // tidy-alphabetical-start
277    ("gccjit", "GPL-3.0"),
278    ("gccjit_sys", "GPL-3.0"),
279    // tidy-alphabetical-end
280];
281
282const EXCEPTIONS_BOOTSTRAP: ExceptionList = &[];
283
284const EXCEPTIONS_UEFI_QEMU_TEST: ExceptionList = &[];
285
286const PERMITTED_RUSTC_DEPS_LOCATION: ListLocation = location!(+6);
287
288/// Crates rustc is allowed to depend on. Avoid adding to the list if possible.
289///
290/// This list is here to provide a speed-bump to adding a new dependency to
291/// rustc. Please check with the compiler team before adding an entry.
292const PERMITTED_RUSTC_DEPENDENCIES: &[&str] = &[
293    // tidy-alphabetical-start
294    "adler2",
295    "aho-corasick",
296    "allocator-api2", // FIXME: only appears in Cargo.lock due to https://github.com/rust-lang/cargo/issues/10801
297    "annotate-snippets",
298    "anstream",
299    "anstyle",
300    "anstyle-parse",
301    "anstyle-query",
302    "anstyle-wincon",
303    "ar_archive_writer",
304    "arrayref",
305    "arrayvec",
306    "bitflags",
307    "blake3",
308    "block-buffer",
309    "block2",
310    "bstr",
311    "cc",
312    "cfg-if",
313    "cfg_aliases",
314    "colorchoice",
315    "constant_time_eq",
316    "cpufeatures",
317    "crc32fast",
318    "crossbeam-deque",
319    "crossbeam-epoch",
320    "crossbeam-utils",
321    "crypto-common",
322    "ctrlc",
323    "darling",
324    "darling_core",
325    "darling_macro",
326    "datafrog",
327    "derive-where",
328    "derive_setters",
329    "digest",
330    "dispatch2",
331    "displaydoc",
332    "dissimilar",
333    "dyn-clone",
334    "either",
335    "elsa",
336    "ena",
337    "equivalent",
338    "errno",
339    "expect-test",
340    "fastrand",
341    "find-msvc-tools",
342    "flate2",
343    "fluent-bundle",
344    "fluent-langneg",
345    "fluent-syntax",
346    "fnv",
347    "foldhash",
348    "generic-array",
349    "getopts",
350    "getrandom",
351    "gimli",
352    "gsgdt",
353    "hashbrown",
354    "icu_collections",
355    "icu_list",
356    "icu_locale",
357    "icu_locale_core",
358    "icu_locale_data",
359    "icu_provider",
360    "ident_case",
361    "indexmap",
362    "intl-memoizer",
363    "intl_pluralrules",
364    "is_terminal_polyfill",
365    "itertools",
366    "itoa",
367    "jiff",
368    "jiff-static",
369    "jiff-tzdb",
370    "jiff-tzdb-platform",
371    "jobserver",
372    "lazy_static",
373    "leb128fmt",
374    "libc",
375    "libloading",
376    "linux-raw-sys",
377    "litemap",
378    "lock_api",
379    "log",
380    "matchers",
381    "md-5",
382    "measureme",
383    "memchr",
384    "memmap2",
385    "miniz_oxide",
386    "nix",
387    "nu-ansi-term",
388    "objc2",
389    "objc2-encode",
390    "object",
391    "odht",
392    "once_cell",
393    "once_cell_polyfill",
394    "parking_lot",
395    "parking_lot_core",
396    "pathdiff",
397    "perf-event-open-sys",
398    "pin-project-lite",
399    "polonius-engine",
400    "portable-atomic", // dependency for platforms doesn't support `AtomicU64` in std
401    "portable-atomic-util",
402    "potential_utf",
403    "ppv-lite86",
404    "proc-macro-hack",
405    "proc-macro2",
406    "pulldown-cmark",
407    "pulldown-cmark-escape",
408    "punycode",
409    "quote",
410    "r-efi",
411    "rand",
412    "rand_chacha",
413    "rand_core",
414    "rand_xorshift", // dependency for doc-tests in rustc_thread_pool
415    "rand_xoshiro",
416    "redox_syscall",
417    "ref-cast",
418    "ref-cast-impl",
419    "regex",
420    "regex-automata",
421    "regex-syntax",
422    "rustc-demangle",
423    "rustc-hash",
424    "rustc-literal-escaper",
425    "rustc-stable-hash",
426    "rustc_apfloat",
427    "rustix",
428    "ruzstd", // via object in thorin-dwp
429    "ryu",
430    "schemars",
431    "schemars_derive",
432    "scoped-tls",
433    "scopeguard",
434    "self_cell",
435    "semver",
436    "serde",
437    "serde_core",
438    "serde_derive",
439    "serde_derive_internals",
440    "serde_json",
441    "serde_path_to_error",
442    "sha1",
443    "sha2",
444    "sharded-slab",
445    "shlex",
446    "simd-adler32",
447    "smallvec",
448    "stable_deref_trait",
449    "static_assertions",
450    "strsim",
451    "syn",
452    "synstructure",
453    "tempfile",
454    "termize",
455    "thin-vec",
456    "thiserror",
457    "thiserror-impl",
458    "thorin-dwp",
459    "thread_local",
460    "tikv-jemalloc-sys",
461    "tinystr",
462    "tinyvec",
463    "tinyvec_macros",
464    "tracing",
465    "tracing-attributes",
466    "tracing-core",
467    "tracing-log",
468    "tracing-serde",
469    "tracing-subscriber",
470    "tracing-tree",
471    "twox-hash",
472    "type-map",
473    "typenum",
474    "unic-langid",
475    "unic-langid-impl",
476    "unic-langid-macros",
477    "unic-langid-macros-impl",
478    "unicase",
479    "unicode-ident",
480    "unicode-normalization",
481    "unicode-properties",
482    "unicode-script",
483    "unicode-security",
484    "unicode-width",
485    "utf8_iter",
486    "utf8parse",
487    "valuable",
488    "version_check",
489    "wasi",
490    "wasm-encoder",
491    "wasmparser",
492    "windows",
493    "windows-collections",
494    "windows-core",
495    "windows-future",
496    "windows-implement",
497    "windows-interface",
498    "windows-link",
499    "windows-numerics",
500    "windows-result",
501    "windows-strings",
502    "windows-sys",
503    "windows-threading",
504    "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>
505    "writeable",
506    "yoke",
507    "yoke-derive",
508    "zerocopy",
509    "zerocopy-derive",
510    "zerofrom",
511    "zerofrom-derive",
512    "zerotrie",
513    "zerovec",
514    "zerovec-derive",
515    "zlib-rs",
516    // tidy-alphabetical-end
517];
518
519const PERMITTED_STDLIB_DEPS_LOCATION: ListLocation = location!(+2);
520
521const PERMITTED_STDLIB_DEPENDENCIES: &[&str] = &[
522    // tidy-alphabetical-start
523    "addr2line",
524    "adler2",
525    "cc",
526    "cfg-if",
527    "compiler_builtins",
528    "dlmalloc",
529    "find-msvc-tools", // via cc
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    // Sort and deduplicate the crate names.
713    // Cargo package names may contain `-`, but will normalize these to `_` before passing to rustc.
714    // As bootstrap parses the `--crate-name` flag, use the name of the actual lib target which has
715    // been normalized.
716    let proc_macro_deps = proc_macro_deps
717        .into_iter()
718        .filter_map(|dep| {
719            metadata[dep].targets.iter().find_map(|target| target.is_lib().then_some(&target.name))
720        })
721        .collect::<BTreeSet<_>>();
722
723    let expected = {
724        use std::fmt::Write;
725
726        const HEADER: &str = "\
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        const FOOTER: &str = "    // tidy-alphabetical-end
734];
735";
736
737        let mut buf = String::with_capacity(4096);
738        buf.push_str(HEADER);
739        for dep in proc_macro_deps {
740            writeln!(buf, "    {dep:?},").unwrap();
741        }
742        buf.push_str(FOOTER);
743        buf
744    };
745
746    const PROC_MACRO_DEPS_RS: &str = "src/bootstrap/src/utils/proc_macro_deps.rs";
747    let proc_macro_deps_rs_path = &root.join(PROC_MACRO_DEPS_RS);
748    let actual = match fs::read_to_string(proc_macro_deps_rs_path) {
749        Ok(actual) => actual,
750        Err(e) => {
751            if e.kind() == io::ErrorKind::NotFound {
752                check.error(format!(
753                    "`{PROC_MACRO_DEPS_RS}` not found; has it been moved or renamed?"
754                ));
755            } else {
756                check.error(format!("`{PROC_MACRO_DEPS_RS}` could not be read: {e:?}"));
757            }
758            return;
759        }
760    };
761
762    if actual != expected {
763        if bless {
764            fs::write(proc_macro_deps_rs_path, &expected).unwrap();
765        } else {
766            let diff = similar::TextDiff::from_lines(&actual, &expected);
767            let mut unified = diff.unified_diff();
768            unified.header(PROC_MACRO_DEPS_RS, "(expected)");
769
770            check.error(format!("`{PROC_MACRO_DEPS_RS}` is not up-to-date:\n{unified}"));
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}