Skip to content

Instantly share code, notes, and snippets.

@heaths
Last active February 27, 2024 09:27
Show Gist options
  • Save heaths/c1a9f3dcfba9d762b34f33346c6d8b0e to your computer and use it in GitHub Desktop.
Save heaths/c1a9f3dcfba9d762b34f33346c6d8b0e to your computer and use it in GitHub Desktop.
Rust script to verify that package dependencies are inherited from single workspace.
#!/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::{
io::Read,
path::PathBuf,
process::{Command, Stdio},
};
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(&manifest_path);
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.into_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,
})
.collect();
if !dependencies.is_empty() {
dependencies.sort();
println!(
"The following dependencies do not inherit from workspace `{}`:\n",
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.");
std::process::exit(1);
}
}
fn package_manifest_path(manifest_path: &str) -> PathBuf {
let mut cmd = Command::new("cargo")
.args(&["read-manifest", "--manifest-path", manifest_path])
.stdout(Stdio::piped())
.spawn()
.expect("executing cargo read-manifest");
let path: PathBuf = read_manifest(&mut cmd)
.manifest_path
.expect("manifest_path")
.into();
if !path.exists() {
panic!("package manifest not found");
}
path
}
fn workspace_manifest_path(manifest_path: &str) -> PathBuf {
let mut cmd = Command::new("cargo")
.args(&[
"metadata",
"--format-version",
"1",
"--all-features",
"--manifest-path",
manifest_path,
])
.stdout(Stdio::piped())
.spawn()
.expect("executing cargo metadata");
let path: PathBuf = read_manifest(&mut cmd)
.workspace_root
.expect("workspace_root")
.into();
let path = path.join("Cargo.toml");
if !path.exists() {
panic!("workspace manifest not found");
}
path
}
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
}
fn read_manifest(cmd: &mut std::process::Child) -> CargoManifest {
let mut reader = std::io::BufReader::new(cmd.stdout.take().expect("buffering stdout"));
let mut content: String = Default::default();
reader.read_to_string(&mut content).expect("reading stdout");
serde_json::from_str::<CargoManifest>(&content).expect("deserializing manifest")
}
#[derive(Deserialize)]
struct CargoManifest {
pub manifest_path: Option<String>,
pub workspace_root: Option<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)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment