xtask: Add command for checking packages conform to certain standards (#15236)

Marshall Bowers created

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

Change summary

Cargo.lock                                    | 37 +++++++++
Cargo.toml                                    |  1 
tooling/xtask/Cargo.toml                      |  2 
tooling/xtask/src/main.rs                     |  5 +
tooling/xtask/src/tasks.rs                    |  1 
tooling/xtask/src/tasks/licenses.rs           | 15 ++-
tooling/xtask/src/tasks/package_conformity.rs | 77 +++++++++++++++++++++
tooling/xtask/src/workspace.rs                | 20 +---
8 files changed, 136 insertions(+), 22 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1906,6 +1906,15 @@ dependencies = [
  "wayland-client",
 ]
 
+[[package]]
+name = "camino"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "cap-fs-ext"
 version = "3.0.0"
@@ -1983,6 +1992,29 @@ dependencies = [
  "winx",
 ]
 
+[[package]]
+name = "cargo-platform"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
 [[package]]
 name = "cargo_toml"
 version = "0.20.2"
@@ -9401,6 +9433,9 @@ name = "semver"
 version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "serde"
@@ -13429,9 +13464,9 @@ name = "xtask"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "cargo_metadata",
  "cargo_toml",
  "clap",
- "toml 0.8.10",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -304,6 +304,7 @@ bitflags = "2.6.0"
 blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
 blade-macros = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
 blade-util = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
+cargo_metadata = "0.18"
 cargo_toml = "0.20"
 chrono = { version = "0.4", features = ["serde"] }
 clap = { version = "4.4", features = ["derive"] }

tooling/xtask/Cargo.toml 🔗

@@ -10,6 +10,6 @@ workspace = true
 
 [dependencies]
 anyhow.workspace = true
+cargo_metadata.workspace = true
 cargo_toml.workspace = true
 clap = { workspace = true, features = ["derive"] }
-toml.workspace = true

tooling/xtask/src/main.rs 🔗

@@ -16,6 +16,8 @@ enum CliCommand {
     /// Runs `cargo clippy`.
     Clippy(tasks::clippy::ClippyArgs),
     Licenses(tasks::licenses::LicensesArgs),
+    /// Checks that packages conform to a set of standards.
+    PackageConformity(tasks::package_conformity::PackageConformityArgs),
 }
 
 fn main() -> Result<()> {
@@ -24,5 +26,8 @@ fn main() -> Result<()> {
     match args.command {
         CliCommand::Clippy(args) => tasks::clippy::run_clippy(args),
         CliCommand::Licenses(args) => tasks::licenses::run_licenses(args),
+        CliCommand::PackageConformity(args) => {
+            tasks::package_conformity::run_package_conformity(args)
+        }
     }
 }

tooling/xtask/src/tasks/licenses.rs 🔗

@@ -1,6 +1,6 @@
 use std::path::{Path, PathBuf};
 
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 use clap::Parser;
 
 use crate::workspace::load_workspace;
@@ -13,8 +13,11 @@ pub fn run_licenses(_args: LicensesArgs) -> Result<()> {
 
     let workspace = load_workspace()?;
 
-    for member in workspace.members {
-        let crate_dir = PathBuf::from(&member);
+    for package in workspace.workspace_packages() {
+        let crate_dir = package
+            .manifest_path
+            .parent()
+            .ok_or_else(|| anyhow!("no crate directory for {}", package.name))?;
 
         if let Some(license_file) = first_license_file(&crate_dir, &LICENSE_FILES) {
             if !license_file.is_symlink() {
@@ -24,15 +27,15 @@ pub fn run_licenses(_args: LicensesArgs) -> Result<()> {
             continue;
         }
 
-        println!("Missing license: {member}");
+        println!("Missing license: {}", package.name);
     }
 
     Ok(())
 }
 
-fn first_license_file(path: &Path, license_files: &[&str]) -> Option<PathBuf> {
+fn first_license_file(path: impl AsRef<Path>, license_files: &[&str]) -> Option<PathBuf> {
     for license_file in license_files {
-        let path_to_license = path.join(license_file);
+        let path_to_license = path.as_ref().join(license_file);
         if path_to_license.exists() {
             return Some(path_to_license);
         }

tooling/xtask/src/tasks/package_conformity.rs 🔗

@@ -0,0 +1,77 @@
+use std::collections::BTreeMap;
+use std::fs;
+use std::path::Path;
+
+use anyhow::{anyhow, Context, Result};
+use cargo_toml::{Dependency, Manifest};
+use clap::Parser;
+
+use crate::workspace::load_workspace;
+
+#[derive(Parser)]
+pub struct PackageConformityArgs {}
+
+pub fn run_package_conformity(_args: PackageConformityArgs) -> Result<()> {
+    let workspace = load_workspace()?;
+
+    let mut non_workspace_dependencies = BTreeMap::new();
+
+    for package in workspace.workspace_packages() {
+        let is_extension = package
+            .manifest_path
+            .parent()
+            .and_then(|parent| parent.parent())
+            .map_or(false, |grandparent_dir| {
+                grandparent_dir.ends_with("extensions")
+            });
+
+        let cargo_toml = read_cargo_toml(&package.manifest_path)?;
+
+        let is_using_workspace_lints = cargo_toml.lints.map_or(false, |lints| lints.workspace);
+        if !is_using_workspace_lints {
+            eprintln!(
+                "{package:?} is not using workspace lints",
+                package = package.name
+            );
+        }
+
+        // Extensions should not use workspace dependencies.
+        if is_extension {
+            continue;
+        }
+
+        for dependencies in [
+            &cargo_toml.dependencies,
+            &cargo_toml.dev_dependencies,
+            &cargo_toml.build_dependencies,
+        ] {
+            for (name, dependency) in dependencies {
+                if let Dependency::Inherited(_) = dependency {
+                    continue;
+                }
+
+                non_workspace_dependencies
+                    .entry(name.to_owned())
+                    .or_insert_with(Vec::new)
+                    .push(package.name.clone());
+            }
+        }
+    }
+
+    for (dependency, packages) in non_workspace_dependencies {
+        eprintln!(
+            "{dependency} is being used as a non-workspace dependency: {}",
+            packages.join(", ")
+        );
+    }
+
+    Ok(())
+}
+
+/// Returns the contents of the `Cargo.toml` file at the given path.
+fn read_cargo_toml(path: impl AsRef<Path>) -> Result<Manifest> {
+    let path = path.as_ref();
+    let cargo_toml_bytes = fs::read(&path)?;
+    Manifest::from_slice(&cargo_toml_bytes)
+        .with_context(|| anyhow!("failed to read Cargo.toml at {path:?}"))
+}

tooling/xtask/src/workspace.rs 🔗

@@ -1,17 +1,9 @@
-use std::fs;
-
-use anyhow::{anyhow, Result};
-use cargo_toml::{Manifest, Workspace};
-use toml;
+use anyhow::{Context, Result};
+use cargo_metadata::{Metadata, MetadataCommand};
 
 /// Returns the Cargo workspace.
-pub fn load_workspace() -> Result<Workspace> {
-    let workspace_cargo_toml = fs::read_to_string("Cargo.toml")?;
-    let workspace_cargo_toml: Manifest = toml::from_str(&workspace_cargo_toml)?;
-
-    let workspace = workspace_cargo_toml
-        .workspace
-        .ok_or_else(|| anyhow!("top-level Cargo.toml is not a Cargo workspace"))?;
-
-    Ok(workspace)
+pub fn load_workspace() -> Result<Metadata> {
+    MetadataCommand::new()
+        .exec()
+        .context("failed to load cargo metadata")
 }