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