eng/scripts/verify-dependencies.rs (207 lines of code) (raw):
#!/usr/bin/env -S cargo +nightly -Zscript
---
[package]
edition = "2021"
[dependencies]
cargo-util-schemas = "0.1.0"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
toml = "0.8.10"
---
use cargo_util_schemas::manifest::{InheritableDependency, TomlDependency, TomlManifest};
use serde::Deserialize;
use std::{
path::{Path, PathBuf},
process::Command,
};
static EXEMPTIONS: &[(&str, &str)] = &[
("azure_core_test", "dotenvy"),
("azure_template", "serde"),
];
fn main() {
let manifest_path = std::env::args()
.nth(1)
.or_else(|| {
find_file(
std::env::current_dir().expect("current directory"),
"Cargo.toml",
)
})
.expect("manifest path");
let package_manifest_path = package_manifest_path(&manifest_path);
let workspace_manifest_path = workspace_manifest_path();
let packages = if package_manifest_path == workspace_manifest_path {
workspace_packages(&workspace_manifest_path)
} else {
vec![package_manifest_path]
};
let mut found = false;
for ref package_manifest_path in packages {
eprintln!("Checking {}", package_manifest_path.display());
let package_manifest_content =
std::fs::read_to_string(package_manifest_path).expect("read package manifest");
let package_manifest: TomlManifest =
toml::from_str(&package_manifest_content).expect("deserialize package manifest");
// Collect all package dependencies including in platform targets.
let mut all_dependencies = vec![
(
"dependencies".to_string(),
package_manifest.dependencies.as_ref(),
),
(
"dev-dependencies".to_string(),
package_manifest.dev_dependencies(),
),
(
"build-dependencies".to_string(),
package_manifest.build_dependencies(),
),
];
if let Some(targets) = package_manifest.target.as_ref() {
for (target, platform) in targets {
all_dependencies.push((
format!("target.'{}'.dependencies", target),
platform.dependencies.as_ref(),
));
all_dependencies.push((
format!("target.'{}'.dev-dependencies", target),
platform.dev_dependencies(),
));
all_dependencies.push((
format!("target.'{}'.build-dependencies", target),
platform.build_dependencies(),
));
}
}
let mut dependencies: Vec<Package> = all_dependencies
.into_iter()
.filter_map(|v| {
if let Some(dependencies) = v.1 {
return Some((v.0, dependencies));
}
None
})
.flat_map(|v| std::iter::repeat(v.0).zip(v.1.iter()))
.filter_map(|v| match v.1 .1 {
InheritableDependency::Value(dep) => match dep {
TomlDependency::Simple(_) => Some(Package {
section: v.0,
name: v.1 .0.to_string(),
}),
TomlDependency::Detailed(details) if details.path.is_none() => Some(Package {
section: v.0,
name: v.1 .0.to_string(),
}),
_ => None,
},
InheritableDependency::Inherit(_) => None,
})
.filter(|v| {
package_manifest
.package
.as_ref()
.is_some_and(|package| !EXEMPTIONS.contains(&(package.name.as_str(), &v.name)))
})
.collect();
if !dependencies.is_empty() {
dependencies.sort();
println!(
"The following `{}` dependencies do not inherit from workspace `{}`:\n",
package_manifest_path.display(),
workspace_manifest_path.display(),
);
println!(
"* {}\n",
dependencies
.into_iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join("\n* ")
);
println!("Add dependencies to workspace and change the package dependency to `{{ workspace = true }}`.");
println!("See <https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace> for more information.");
println!();
found = true;
}
}
if found {
std::process::exit(1);
}
}
fn package_manifest_path(manifest_path: &str) -> PathBuf {
let output = Command::new("cargo")
.args([
"locate-project",
"--message-format",
"plain",
"--manifest-path",
manifest_path,
])
.output()
.expect("executing cargo locate-project");
let path: PathBuf = String::from_utf8(output.stdout)
.expect("valid path")
.trim_end()
.into();
if !path.exists() {
panic!("package manifest not found");
}
path
}
fn workspace_manifest_path() -> PathBuf {
let output = Command::new("cargo")
.args(["locate-project", "--message-format", "plain", "--workspace"])
.output()
.expect("executing cargo locate-project");
let path: PathBuf = String::from_utf8(output.stdout)
.expect("valid path")
.trim_end()
.into();
if !path.exists() {
panic!("workspace manifest not found");
}
path
}
fn workspace_packages(manifest_path: &Path) -> Vec<PathBuf> {
let output = Command::new("cargo")
.args([
"metadata",
"--format-version",
"1",
"--no-deps",
"--manifest-path",
&manifest_path.to_string_lossy(),
])
.output()
.expect("executing cargo metadata");
let manifest: Manifest =
serde_json::from_slice(&output.stdout).expect("bad workspace metadata");
let mut paths = Vec::with_capacity(manifest.packages.len());
for package in manifest.packages {
paths.push(package.manifest_path.into());
}
paths
}
fn find_file(dir: impl AsRef<std::path::Path>, name: &str) -> Option<String> {
for dir in dir.as_ref().ancestors() {
let path = dir.join(name);
if path.exists() {
return Some(path.to_str().unwrap().into());
}
}
None
}
#[derive(Deserialize)]
struct Manifest {
packages: Vec<ManifestPackage>,
}
#[derive(Deserialize)]
struct ManifestPackage {
manifest_path: String,
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Package {
name: String,
section: String,
}
impl std::fmt::Display for Package {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.name, self.section)
}
}