aboutsummaryrefslogtreecommitdiff
path: root/dev/ci/src/res/manifests.rs
blob: 7c72146bbb54357581657b5e69dced74cc72545a (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
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use mingling::{Program, macros::program_setup};

use crate::ThisProgram;

/// Directories whose manifests are excluded from CI checks.
///
/// Path is relative to the crate source file (`dev/ci/src/res/`).
const IGNORED_DIRS_FILE: &str = include_str!("../../../configs/ci-ignored-dirs.txt");

/// All `Cargo.toml` manifests the CI will check.
#[derive(Default, Clone)]
pub struct Manifests {
    pub path: Vec<PathBuf>,
    /// Package name -> its manifest path.
    pub package_dirs: HashMap<String, PathBuf>,
}

#[program_setup]
pub fn manifests_setup(p: &mut Program<ThisProgram>) {
    let path = cargo_tomls();
    let package_dirs = path.iter().map(|p| (package_name(p), p.clone())).collect();
    p.with_resource(Manifests { path, package_dirs });
}

/// Recursively collects every `Cargo.toml` under the current directory,
/// skipping the legacy `.run` CI directory and any directory listed in
/// `dev/configs/ci-ignored-dirs.txt`.
#[must_use]
fn cargo_tomls() -> Vec<PathBuf> {
    let ignored = ignored_dirs();
    let mut cargo_tomls = Vec::new();
    let mut dirs = vec![PathBuf::from(".")];
    while let Some(dir) = dirs.pop() {
        if is_ignored(&dir.to_string_lossy(), &ignored) {
            continue;
        }
        if let Ok(entries) = std::fs::read_dir(&dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_dir() {
                    // Skip the legacy `.run` CI directory
                    if path.file_name().and_then(|n| n.to_str()) == Some(".run") {
                        continue;
                    }
                    dirs.push(path);
                } else if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
                    cargo_tomls.push(path);
                }
            }
        }
    }
    cargo_tomls
}

/// Parses `dev/configs/ci-ignored-dirs.txt` into directory prefixes:
/// non-empty lines that do not start with `#`, with the trailing `/` stripped
/// (e.g. `./.temp/` → `./.temp`).
fn ignored_dirs() -> Vec<String> {
    IGNORED_DIRS_FILE
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty() && !line.starts_with('#'))
        .map(|line| line.trim_end_matches('/').to_string())
        .collect()
}

/// Whether `path` (a walk directory, e.g. `./.temp` or `./examples`) is inside
/// one of the ignored directories.
fn is_ignored(path: &str, ignored: &[String]) -> bool {
    ignored.iter().any(|dir| {
        path.strip_prefix(dir.as_str())
            .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
    })
}

/// Extracts the package name from a `Cargo.toml`.
///
/// Falls back to the parent directory name (e.g. `mingling_core/Cargo.toml` →
/// `mingling_core`, workspace root → `(root)`), matching the legacy CI.
fn package_name(path: &Path) -> String {
    let fallback = || {
        path.parent()
            .and_then(|p| p.file_name())
            .and_then(|n| n.to_str())
            .unwrap_or("(root)")
            .to_string()
    };

    let Ok(content) = std::fs::read_to_string(path) else {
        return fallback();
    };
    let Ok(toml_value) = content.parse::<toml::Table>() else {
        return fallback();
    };
    toml_value
        .get("package")
        .and_then(|p| p.get("name"))
        .and_then(|n| n.as_str())
        .map_or_else(fallback, str::to_string)
}