Add `xtask` for finding crates with missing licenses (#11776)

Marshall Bowers created

This PR adds a new `cargo xtask licenses` command for finding crates
with missing license files.

A number of crates were uncovered that were missing a license file, and
have had the appropriate license file added.

Release Notes:

- N/A

Change summary

Cargo.lock                             | 12 ++++
Cargo.toml                             |  1 
crates/dev_server_projects/LICENSE-GPL |  1 
crates/image_viewer/LICENSE-GPL        |  1 
crates/markdown/LICENSE-GPL            |  1 
crates/supermaven/LICENSE-GPL          |  1 
crates/supermaven_api/LICENSE-GPL      |  1 
extensions/ocaml/LICENSE-APACHE        |  1 
tooling/xtask/Cargo.toml               |  2 
tooling/xtask/src/main.rs              | 70 ++-------------------------
tooling/xtask/src/tasks.rs             |  2 
tooling/xtask/src/tasks/clippy.rs      | 63 +++++++++++++++++++++++++
tooling/xtask/src/tasks/licenses.rs    | 39 +++++++++++++++
tooling/xtask/src/workspace.rs         | 17 ++++++
14 files changed, 149 insertions(+), 63 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1887,6 +1887,16 @@ dependencies = [
  "winx",
 ]
 
+[[package]]
+name = "cargo_toml"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8cb1d556b8b8f36e5ca74938008be3ac102f5dcb5b68a0477e4249ae2291cd3"
+dependencies = [
+ "serde",
+ "toml 0.8.10",
+]
+
 [[package]]
 name = "cast"
 version = "0.3.0"
@@ -12810,7 +12820,9 @@ name = "xtask"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "cargo_toml",
  "clap 4.4.4",
+ "toml 0.8.10",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -265,6 +265,7 @@ bitflags = "2.4.2"
 blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
 blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
 cap-std = "3.0"
+cargo_toml = "0.20"
 chrono = { version = "0.4", features = ["serde"] }
 clap = { version = "4.4", features = ["derive"] }
 clickhouse = { version = "0.11.6" }

tooling/xtask/Cargo.toml 🔗

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

tooling/xtask/src/main.rs 🔗

@@ -1,6 +1,7 @@
-use std::process::Command;
+mod tasks;
+mod workspace;
 
-use anyhow::{bail, Context, Result};
+use anyhow::Result;
 use clap::{Parser, Subcommand};
 
 #[derive(Parser)]
@@ -13,72 +14,15 @@ struct Args {
 #[derive(Subcommand)]
 enum CliCommand {
     /// Runs `cargo clippy`.
-    Clippy(ClippyArgs),
+    Clippy(tasks::clippy::ClippyArgs),
+    Licenses(tasks::licenses::LicensesArgs),
 }
 
 fn main() -> Result<()> {
     let args = Args::parse();
 
     match args.command {
-        CliCommand::Clippy(args) => run_clippy(args),
+        CliCommand::Clippy(args) => tasks::clippy::run_clippy(args),
+        CliCommand::Licenses(args) => tasks::licenses::run_licenses(args),
     }
 }
-
-#[derive(Parser)]
-struct ClippyArgs {
-    /// Automatically apply lint suggestions (`clippy --fix`).
-    #[arg(long)]
-    fix: bool,
-
-    /// The package to run Clippy against (`cargo -p <PACKAGE> clippy`).
-    #[arg(long, short)]
-    package: Option<String>,
-}
-
-fn run_clippy(args: ClippyArgs) -> Result<()> {
-    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
-
-    let mut clippy_command = Command::new(&cargo);
-    clippy_command.arg("clippy");
-
-    if let Some(package) = args.package.as_ref() {
-        clippy_command.args(["--package", package]);
-    } else {
-        clippy_command.arg("--workspace");
-    }
-
-    clippy_command
-        .arg("--release")
-        .arg("--all-targets")
-        .arg("--all-features");
-
-    if args.fix {
-        clippy_command.arg("--fix");
-    }
-
-    clippy_command.arg("--");
-
-    // Deny all warnings.
-    clippy_command.args(["--deny", "warnings"]);
-
-    eprintln!(
-        "running: {cargo} {}",
-        clippy_command
-            .get_args()
-            .map(|arg| arg.to_str().unwrap())
-            .collect::<Vec<_>>()
-            .join(" ")
-    );
-
-    let exit_status = clippy_command
-        .spawn()
-        .context("failed to spawn child process")?
-        .wait()
-        .context("failed to wait for child process")?;
-
-    if !exit_status.success() {
-        bail!("clippy failed: {}", exit_status);
-    }
-
-    Ok(())
-}

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

@@ -0,0 +1,63 @@
+use std::process::Command;
+
+use anyhow::{bail, Context, Result};
+use clap::Parser;
+
+#[derive(Parser)]
+pub struct ClippyArgs {
+    /// Automatically apply lint suggestions (`clippy --fix`).
+    #[arg(long)]
+    fix: bool,
+
+    /// The package to run Clippy against (`cargo -p <PACKAGE> clippy`).
+    #[arg(long, short)]
+    package: Option<String>,
+}
+
+pub fn run_clippy(args: ClippyArgs) -> Result<()> {
+    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
+
+    let mut clippy_command = Command::new(&cargo);
+    clippy_command.arg("clippy");
+
+    if let Some(package) = args.package.as_ref() {
+        clippy_command.args(["--package", package]);
+    } else {
+        clippy_command.arg("--workspace");
+    }
+
+    clippy_command
+        .arg("--release")
+        .arg("--all-targets")
+        .arg("--all-features");
+
+    if args.fix {
+        clippy_command.arg("--fix");
+    }
+
+    clippy_command.arg("--");
+
+    // Deny all warnings.
+    clippy_command.args(["--deny", "warnings"]);
+
+    eprintln!(
+        "running: {cargo} {}",
+        clippy_command
+            .get_args()
+            .map(|arg| arg.to_str().unwrap())
+            .collect::<Vec<_>>()
+            .join(" ")
+    );
+
+    let exit_status = clippy_command
+        .spawn()
+        .context("failed to spawn child process")?
+        .wait()
+        .context("failed to wait for child process")?;
+
+    if !exit_status.success() {
+        bail!("clippy failed: {}", exit_status);
+    }
+
+    Ok(())
+}

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

@@ -0,0 +1,39 @@
+use std::path::{Path, PathBuf};
+
+use anyhow::Result;
+use clap::Parser;
+
+use crate::workspace::load_workspace;
+
+#[derive(Parser)]
+pub struct LicensesArgs {}
+
+pub fn run_licenses(_args: LicensesArgs) -> Result<()> {
+    let workspace = load_workspace()?;
+
+    for member in workspace.members {
+        let crate_dir = PathBuf::from(&member);
+
+        if has_any_license_file(
+            &crate_dir,
+            &["LICENSE-APACHE", "LICENSE-GPL", "LICENSE-AGPL"],
+        ) {
+            continue;
+        }
+
+        println!("Missing license: {member}");
+    }
+
+    Ok(())
+}
+
+fn has_any_license_file(path: &Path, license_files: &[&str]) -> bool {
+    for license_file in license_files {
+        let path_to_license = path.join(license_file);
+        if path_to_license.exists() {
+            return true;
+        }
+    }
+
+    false
+}

tooling/xtask/src/workspace.rs 🔗

@@ -0,0 +1,17 @@
+use std::fs;
+
+use anyhow::{anyhow, Result};
+use cargo_toml::{Manifest, Workspace};
+use toml;
+
+/// 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)
+}