about summary refs log tree commit diff
path: root/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/structure.rs
blob: ea80128e487ac3353d1093ab2477f1c1f8d11ae6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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,
        })
    }
}