summary refs log tree commit diff
path: root/pkgs/test/nixpkgs-check-by-name/src/structure.rs
diff options
context:
space:
mode:
authorSilvan Mosberger <silvan.mosberger@tweag.io>2023-08-23 04:22:41 +0200
committerSilvan Mosberger <silvan.mosberger@tweag.io>2023-08-29 16:17:54 +0200
commit271eb0299503892944986eb381b79ec09ea2f2a4 (patch)
tree0548e8ae5c981fc09f15365d95a05e63d790bcb1 /pkgs/test/nixpkgs-check-by-name/src/structure.rs
parent87c5a6a84fcb94fd52507c24bb31c3b844775190 (diff)
downloadnixpkgs-271eb0299503892944986eb381b79ec09ea2f2a4.tar
nixpkgs-271eb0299503892944986eb381b79ec09ea2f2a4.tar.gz
nixpkgs-271eb0299503892944986eb381b79ec09ea2f2a4.tar.bz2
nixpkgs-271eb0299503892944986eb381b79ec09ea2f2a4.tar.lz
nixpkgs-271eb0299503892944986eb381b79ec09ea2f2a4.tar.xz
nixpkgs-271eb0299503892944986eb381b79ec09ea2f2a4.tar.zst
nixpkgs-271eb0299503892944986eb381b79ec09ea2f2a4.zip
pkgs/test/nixpkgs-check-by-name: init
Adds an internal CLI tool to verify Nixpkgs to conform to RFC 140.
See pkgs/test/nixpkgs-check-by-name/README.md for more information.
Diffstat (limited to 'pkgs/test/nixpkgs-check-by-name/src/structure.rs')
-rw-r--r--pkgs/test/nixpkgs-check-by-name/src/structure.rs152
1 files changed, 152 insertions, 0 deletions
diff --git a/pkgs/test/nixpkgs-check-by-name/src/structure.rs b/pkgs/test/nixpkgs-check-by-name/src/structure.rs
new file mode 100644
index 00000000000..ea80128e487
--- /dev/null
+++ b/pkgs/test/nixpkgs-check-by-name/src/structure.rs
@@ -0,0 +1,152 @@
+use crate::utils;
+use crate::utils::ErrorWriter;
+use lazy_static::lazy_static;
+use regex::Regex;
+use std::collections::HashMap;
+use std::io;
+use std::path::{Path, PathBuf};
+
+pub const BASE_SUBPATH: &str = "pkgs/by-name";
+pub const PACKAGE_NIX_FILENAME: &str = "package.nix";
+
+lazy_static! {
+    static ref SHARD_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_-]{1,2}$").unwrap();
+    static ref PACKAGE_NAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap();
+}
+
+/// Contains information about the structure of the pkgs/by-name directory of a Nixpkgs
+pub struct Nixpkgs {
+    /// The path to nixpkgs
+    pub path: PathBuf,
+    /// The names of all packages declared in pkgs/by-name
+    pub package_names: Vec<String>,
+}
+
+impl Nixpkgs {
+    // Some utility functions for the basic structure
+
+    pub fn shard_for_package(package_name: &str) -> String {
+        package_name.to_lowercase().chars().take(2).collect()
+    }
+
+    pub fn relative_dir_for_shard(shard_name: &str) -> PathBuf {
+        PathBuf::from(format!("{BASE_SUBPATH}/{shard_name}"))
+    }
+
+    pub fn relative_dir_for_package(package_name: &str) -> PathBuf {
+        Nixpkgs::relative_dir_for_shard(&Nixpkgs::shard_for_package(package_name))
+            .join(package_name)
+    }
+
+    pub fn relative_file_for_package(package_name: &str) -> PathBuf {
+        Nixpkgs::relative_dir_for_package(package_name).join(PACKAGE_NIX_FILENAME)
+    }
+}
+
+impl Nixpkgs {
+    /// Read the structure of a Nixpkgs directory, displaying errors on the writer.
+    /// May return early with I/O errors.
+    pub fn new<W: io::Write>(
+        path: &Path,
+        error_writer: &mut ErrorWriter<W>,
+    ) -> anyhow::Result<Nixpkgs> {
+        let base_dir = path.join(BASE_SUBPATH);
+
+        let mut package_names = Vec::new();
+
+        for shard_entry in utils::read_dir_sorted(&base_dir)? {
+            let shard_path = shard_entry.path();
+            let shard_name = shard_entry.file_name().to_string_lossy().into_owned();
+            let relative_shard_path = Nixpkgs::relative_dir_for_shard(&shard_name);
+
+            if shard_name == "README.md" {
+                // README.md is allowed to be a file and not checked
+                continue;
+            }
+
+            if !shard_path.is_dir() {
+                error_writer.write(&format!(
+                    "{}: This is a file, but it should be a directory.",
+                    relative_shard_path.display(),
+                ))?;
+                // we can't check for any other errors if it's a file, since there's no subdirectories to check
+                continue;
+            }
+
+            let shard_name_valid = SHARD_NAME_REGEX.is_match(&shard_name);
+            if !shard_name_valid {
+                error_writer.write(&format!(
+                    "{}: Invalid directory name \"{shard_name}\", must be at most 2 ASCII characters consisting of a-z, 0-9, \"-\" or \"_\".",
+                    relative_shard_path.display()
+                ))?;
+            }
+
+            let mut unique_package_names = HashMap::new();
+
+            for package_entry in utils::read_dir_sorted(&shard_path)? {
+                let package_path = package_entry.path();
+                let package_name = package_entry.file_name().to_string_lossy().into_owned();
+                let relative_package_dir =
+                    PathBuf::from(format!("{BASE_SUBPATH}/{shard_name}/{package_name}"));
+
+                if !package_path.is_dir() {
+                    error_writer.write(&format!(
+                        "{}: This path is a file, but it should be a directory.",
+                        relative_package_dir.display(),
+                    ))?;
+                    continue;
+                }
+
+                if let Some(duplicate_package_name) =
+                    unique_package_names.insert(package_name.to_lowercase(), package_name.clone())
+                {
+                    error_writer.write(&format!(
+                        "{}: Duplicate case-sensitive package directories \"{duplicate_package_name}\" and \"{package_name}\".",
+                        relative_shard_path.display(),
+                    ))?;
+                }
+
+                let package_name_valid = PACKAGE_NAME_REGEX.is_match(&package_name);
+                if !package_name_valid {
+                    error_writer.write(&format!(
+                        "{}: Invalid package directory name \"{package_name}\", must be ASCII characters consisting of a-z, A-Z, 0-9, \"-\" or \"_\".",
+                        relative_package_dir.display(),
+                    ))?;
+                }
+
+                let correct_relative_package_dir = Nixpkgs::relative_dir_for_package(&package_name);
+                if relative_package_dir != correct_relative_package_dir {
+                    // Only show this error if we have a valid shard and package name
+                    // Because if one of those is wrong, you should fix that first
+                    if shard_name_valid && package_name_valid {
+                        error_writer.write(&format!(
+                            "{}: Incorrect directory location, should be {} instead.",
+                            relative_package_dir.display(),
+                            correct_relative_package_dir.display(),
+                        ))?;
+                    }
+                }
+
+                let package_nix_path = package_path.join(PACKAGE_NIX_FILENAME);
+                if !package_nix_path.exists() {
+                    error_writer.write(&format!(
+                        "{}: Missing required \"{PACKAGE_NIX_FILENAME}\" file.",
+                        relative_package_dir.display(),
+                    ))?;
+                } else if package_nix_path.is_dir() {
+                    error_writer.write(&format!(
+                        "{}: \"{PACKAGE_NIX_FILENAME}\" must be a file.",
+                        relative_package_dir.display(),
+                    ))?;
+                }
+
+                package_names.push(package_name.clone());
+            }
+        }
+
+        Ok(Nixpkgs {
+            path: path.to_owned(),
+            package_names,
+        })
+    }
+}