Skip to content

Commit f2060cc

Browse files
authored
xtask: Add command for checking packages conform to certain standards (zed-industries#15236)
This PR adds a new `xtask` command for checking that packages conform to certain standards. Still a work-in-progress, but right now it checks: - If `[lints] workspace = true` is set - If packages are using non-workspace dependencies Release Notes: - N/A
1 parent 13693ff commit f2060cc

File tree

8 files changed

+136
-22
lines changed

8 files changed

+136
-22
lines changed

Cargo.lock

+36-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ bitflags = "2.6.0"
304304
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
305305
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
306306
blade-util = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
307+
cargo_metadata = "0.18"
307308
cargo_toml = "0.20"
308309
chrono = { version = "0.4", features = ["serde"] }
309310
clap = { version = "4.4", features = ["derive"] }

tooling/xtask/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ workspace = true
1010

1111
[dependencies]
1212
anyhow.workspace = true
13+
cargo_metadata.workspace = true
1314
cargo_toml.workspace = true
1415
clap = { workspace = true, features = ["derive"] }
15-
toml.workspace = true

tooling/xtask/src/main.rs

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ enum CliCommand {
1616
/// Runs `cargo clippy`.
1717
Clippy(tasks::clippy::ClippyArgs),
1818
Licenses(tasks::licenses::LicensesArgs),
19+
/// Checks that packages conform to a set of standards.
20+
PackageConformity(tasks::package_conformity::PackageConformityArgs),
1921
}
2022

2123
fn main() -> Result<()> {
@@ -24,5 +26,8 @@ fn main() -> Result<()> {
2426
match args.command {
2527
CliCommand::Clippy(args) => tasks::clippy::run_clippy(args),
2628
CliCommand::Licenses(args) => tasks::licenses::run_licenses(args),
29+
CliCommand::PackageConformity(args) => {
30+
tasks::package_conformity::run_package_conformity(args)
31+
}
2732
}
2833
}

tooling/xtask/src/tasks.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod clippy;
22
pub mod licenses;
3+
pub mod package_conformity;

tooling/xtask/src/tasks/licenses.rs

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::{Path, PathBuf};
22

3-
use anyhow::Result;
3+
use anyhow::{anyhow, Result};
44
use clap::Parser;
55

66
use crate::workspace::load_workspace;
@@ -13,8 +13,11 @@ pub fn run_licenses(_args: LicensesArgs) -> Result<()> {
1313

1414
let workspace = load_workspace()?;
1515

16-
for member in workspace.members {
17-
let crate_dir = PathBuf::from(&member);
16+
for package in workspace.workspace_packages() {
17+
let crate_dir = package
18+
.manifest_path
19+
.parent()
20+
.ok_or_else(|| anyhow!("no crate directory for {}", package.name))?;
1821

1922
if let Some(license_file) = first_license_file(&crate_dir, &LICENSE_FILES) {
2023
if !license_file.is_symlink() {
@@ -24,15 +27,15 @@ pub fn run_licenses(_args: LicensesArgs) -> Result<()> {
2427
continue;
2528
}
2629

27-
println!("Missing license: {member}");
30+
println!("Missing license: {}", package.name);
2831
}
2932

3033
Ok(())
3134
}
3235

33-
fn first_license_file(path: &Path, license_files: &[&str]) -> Option<PathBuf> {
36+
fn first_license_file(path: impl AsRef<Path>, license_files: &[&str]) -> Option<PathBuf> {
3437
for license_file in license_files {
35-
let path_to_license = path.join(license_file);
38+
let path_to_license = path.as_ref().join(license_file);
3639
if path_to_license.exists() {
3740
return Some(path_to_license);
3841
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use std::collections::BTreeMap;
2+
use std::fs;
3+
use std::path::Path;
4+
5+
use anyhow::{anyhow, Context, Result};
6+
use cargo_toml::{Dependency, Manifest};
7+
use clap::Parser;
8+
9+
use crate::workspace::load_workspace;
10+
11+
#[derive(Parser)]
12+
pub struct PackageConformityArgs {}
13+
14+
pub fn run_package_conformity(_args: PackageConformityArgs) -> Result<()> {
15+
let workspace = load_workspace()?;
16+
17+
let mut non_workspace_dependencies = BTreeMap::new();
18+
19+
for package in workspace.workspace_packages() {
20+
let is_extension = package
21+
.manifest_path
22+
.parent()
23+
.and_then(|parent| parent.parent())
24+
.map_or(false, |grandparent_dir| {
25+
grandparent_dir.ends_with("extensions")
26+
});
27+
28+
let cargo_toml = read_cargo_toml(&package.manifest_path)?;
29+
30+
let is_using_workspace_lints = cargo_toml.lints.map_or(false, |lints| lints.workspace);
31+
if !is_using_workspace_lints {
32+
eprintln!(
33+
"{package:?} is not using workspace lints",
34+
package = package.name
35+
);
36+
}
37+
38+
// Extensions should not use workspace dependencies.
39+
if is_extension {
40+
continue;
41+
}
42+
43+
for dependencies in [
44+
&cargo_toml.dependencies,
45+
&cargo_toml.dev_dependencies,
46+
&cargo_toml.build_dependencies,
47+
] {
48+
for (name, dependency) in dependencies {
49+
if let Dependency::Inherited(_) = dependency {
50+
continue;
51+
}
52+
53+
non_workspace_dependencies
54+
.entry(name.to_owned())
55+
.or_insert_with(Vec::new)
56+
.push(package.name.clone());
57+
}
58+
}
59+
}
60+
61+
for (dependency, packages) in non_workspace_dependencies {
62+
eprintln!(
63+
"{dependency} is being used as a non-workspace dependency: {}",
64+
packages.join(", ")
65+
);
66+
}
67+
68+
Ok(())
69+
}
70+
71+
/// Returns the contents of the `Cargo.toml` file at the given path.
72+
fn read_cargo_toml(path: impl AsRef<Path>) -> Result<Manifest> {
73+
let path = path.as_ref();
74+
let cargo_toml_bytes = fs::read(&path)?;
75+
Manifest::from_slice(&cargo_toml_bytes)
76+
.with_context(|| anyhow!("failed to read Cargo.toml at {path:?}"))
77+
}

tooling/xtask/src/workspace.rs

+6-14
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
1-
use std::fs;
2-
3-
use anyhow::{anyhow, Result};
4-
use cargo_toml::{Manifest, Workspace};
5-
use toml;
1+
use anyhow::{Context, Result};
2+
use cargo_metadata::{Metadata, MetadataCommand};
63

74
/// Returns the Cargo workspace.
8-
pub fn load_workspace() -> Result<Workspace> {
9-
let workspace_cargo_toml = fs::read_to_string("Cargo.toml")?;
10-
let workspace_cargo_toml: Manifest = toml::from_str(&workspace_cargo_toml)?;
11-
12-
let workspace = workspace_cargo_toml
13-
.workspace
14-
.ok_or_else(|| anyhow!("top-level Cargo.toml is not a Cargo workspace"))?;
15-
16-
Ok(workspace)
5+
pub fn load_workspace() -> Result<Metadata> {
6+
MetadataCommand::new()
7+
.exec()
8+
.context("failed to load cargo metadata")
179
}

0 commit comments

Comments
 (0)