diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-19 05:54:00 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-19 06:15:20 +0800 |
| commit | ec9edc294fd5e7e29977fc7b0e6fb953422bc0e2 (patch) | |
| tree | bbf89ef4457b8de8a8a9bca5668045d612d27572 /mingling_ci/src/res | |
| parent | 06dfc27194c11e1d1033c292c759a1c5d82e780b (diff) | |
Move CI, dev tools, and configs from root-level scattered locations into
a
unified `dev/` directory structure. Update all references across build
scripts, documentation, and editor configurations.
Also consolidate editor config generation into build.rs for automated
synchronization of rust-analyzer settings across VS Code and Zed.
Diffstat (limited to 'mingling_ci/src/res')
| -rw-r--r-- | mingling_ci/src/res/collect_logs.rs | 203 | ||||
| -rw-r--r-- | mingling_ci/src/res/crate_config.rs | 79 | ||||
| -rw-r--r-- | mingling_ci/src/res/features.rs | 47 | ||||
| -rw-r--r-- | mingling_ci/src/res/manifests.rs | 103 | ||||
| -rw-r--r-- | mingling_ci/src/res/print.rs | 174 |
5 files changed, 0 insertions, 606 deletions
diff --git a/mingling_ci/src/res/collect_logs.rs b/mingling_ci/src/res/collect_logs.rs deleted file mode 100644 index 6017168..0000000 --- a/mingling_ci/src/res/collect_logs.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! IO side of the report command: reads the collect directory once and keeps -//! the parsed data in a resource, so chains only do computation. - -use std::collections::BTreeMap; - -use mingling::{Program, macros::program_setup}; - -use crate::ThisProgram; -use crate::reporter::COLLECT_DIR; - -/// Git commit date and short hash for the report. -#[derive(Default, Clone, Debug)] -pub struct GitInfo { - pub date: String, - pub commit_hash: String, -} - -/// Parsed contents of the collect directory. -#[derive(Default, Clone)] -pub struct ResCollectLogs { - /// `(task, item) -> os -> ok` - pub statuses: BTreeMap<(String, String), BTreeMap<String, bool>>, - /// `(task, item) -> location` - pub locations: BTreeMap<(String, String), String>, - /// `(task, os, item) -> stripped error output (location line removed)` - pub err_outputs: BTreeMap<(String, String, String), String>, - pub git: GitInfo, -} - -impl ResCollectLogs { - /// Reads the flat `collect/` directory — aggregate `{task}.{os}.ok` files - /// (`item` or `item = location` per line) and per-item - /// `{task}.{os}.{item}.err` files (first line is the location) — plus the - /// git info. - #[must_use] - pub fn read() -> Self { - let mut logs = Self::default(); - - if let Ok(entries) = std::fs::read_dir(COLLECT_DIR) { - for entry in entries.flatten() { - let file_name = entry.file_name().to_string_lossy().into_owned(); - if let Some((task, os)) = parse_ok_name(&file_name) { - // Aggregate success file: `item` or `item = location` per line. - if let Ok(content) = std::fs::read_to_string(entry.path()) { - for line in content.lines().filter(|l| !l.is_empty()) { - let (item, location) = line - .split_once('=') - .map_or((line, ""), |(name, loc)| (name.trim(), loc.trim())); - logs.statuses - .entry((task.clone(), item.to_string())) - .or_default() - .insert(os.clone(), true); - logs.locations - .insert((task.clone(), item.to_string()), location.to_string()); - } - } - } else if let Some((task, os, item)) = parse_err_name(&file_name) { - let content = std::fs::read_to_string(entry.path()).unwrap_or_default(); - let mut lines = content.splitn(2, '\n'); - let location = lines.next().unwrap_or_default().to_string(); - let output = lines.next().unwrap_or_default().to_string(); - logs.statuses - .entry((task.clone(), item.clone())) - .or_default() - .insert(os.clone(), false); - logs.locations - .insert((task.clone(), item.clone()), location); - logs.err_outputs - .insert((task, os, item), strip_ansi(&output)); - } - } - } - - logs.git = git_info(); - logs - } -} - -/// Parses a `{task}.{os}.ok` file name. -fn parse_ok_name(file_name: &str) -> Option<(String, String)> { - let name = file_name.strip_suffix(".ok")?; - let mut parts = name.rsplitn(2, '.'); - let os = parts.next()?.to_string(); - let task = parts.next()?.to_string(); - Some((task, os)) -} - -/// Parses a `{task}.{os}.{package}.err` file name. -/// -/// Split from the right: package names cannot contain dots (cargo forbids -/// them), while task names may. -fn parse_err_name(file_name: &str) -> Option<(String, String, String)> { - let name = file_name.strip_suffix(".err")?; - let mut parts = name.rsplitn(3, '.'); - let package = parts.next()?.to_string(); - let os = parts.next()?.to_string(); - let task = parts.next()?.to_string(); - Some((task, os, package)) -} - -#[program_setup] -pub fn report_setup(p: &mut Program<ThisProgram>) { - p.with_resource(ResCollectLogs::read()); -} - -/// Strips ANSI escape sequences from `input`. -/// -/// Handles CSI (`ESC [ ...`), OSC (`ESC ] ...` terminated by BEL or `ESC \`) -/// and other single-character escapes, while preserving UTF-8 text. Literal -/// `^[` (caret-bracket, produced by some terminal captures) is normalized to -/// `ESC` first. -fn strip_ansi(input: &str) -> String { - // Normalize literal `^[` (0x5E 0x5B) to a real ESC byte. - let normalized = input.replace("^[", "\u{1b}"); - let mut out = String::with_capacity(normalized.len()); - let mut rest = normalized.as_str(); - while let Some(idx) = rest.find('\u{1b}') { - out.push_str(&rest[..idx]); - rest = &rest[idx..]; - rest = &rest[ansi_len(rest)..]; - } - out.push_str(rest); - out -} - -/// Byte length of the ANSI escape sequence starting at `s[0]` (`s[0]` is `ESC`). -fn ansi_len(s: &str) -> usize { - let b = s.as_bytes(); - match b.get(1) { - Some(b'[') => { - // CSI: `ESC [` params/intermediates (0x20-0x3F) then a final byte (0x40-0x7E). - let mut i = 2; - while i < b.len() { - let byte = b[i]; - i += 1; - if (0x40..=0x7E).contains(&byte) { - break; - } - if !(0x20..=0x3F).contains(&byte) { - break; - } - } - i - } - Some(b']') => { - // OSC: `ESC ]` ... terminated by BEL (0x07) or `ESC \`. - let mut i = 2; - while i < b.len() { - let byte = b[i]; - i += 1; - if byte == 0x07 { - break; - } - if byte == 0x1b { - if b.get(i) == Some(&b'\\') { - i += 1; - } - break; - } - } - i - } - Some(_) => 2.min(b.len()), - None => 1, - } -} - -/// Commit date (`YYYY-MM-DD`) and short commit hash; empty on failure. -fn git_info() -> GitInfo { - let run = |args: &[&str]| { - std::process::Command::new("git") - .args(args) - .output() - .ok() - .filter(|o| o.status.success()) - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .unwrap_or_default() - }; - GitInfo { - date: run(&["log", "-1", "--format=%cs"]), - commit_hash: run(&["rev-parse", "--short", "HEAD"]), - } -} - -#[cfg(test)] -mod tests { - use super::strip_ansi; - - #[test] - fn strips_csi_and_osc_and_literal_caret() { - let input = - "\u{1b}[1m\u{1b}[92mok\u{1b}[0m \u{1b}]8;;https://x\u{1b}\\done\u{1b}]8;;\u{1b}\\\n"; - assert_eq!(strip_ansi(input), "ok done\n"); - - // Literal `^[` (caret-bracket) captured by some terminals. - assert_eq!(strip_ansi("^[[31mred^[[0m"), "red"); - } - - #[test] - fn preserves_utf8() { - assert_eq!(strip_ansi("你好\u{1b}[1m世界!\u{1b}[0m"), "你好世界!"); - } -} diff --git a/mingling_ci/src/res/crate_config.rs b/mingling_ci/src/res/crate_config.rs deleted file mode 100644 index b20e83d..0000000 --- a/mingling_ci/src/res/crate_config.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::collections::HashMap; -use std::path::Path; - -use mingling::{Program, macros::program_setup}; - -use crate::ThisProgram; -use crate::res::{Manifests, ResFeatureList}; - -/// Per-crate CI overrides from `mingling-ci.toml` (optional, crate root). -/// -/// Currently only `[test] command` is read; `clippy.command` / `build.command` -/// will follow the same shape. -#[derive(Default, Clone)] -pub struct ResCrateConfig { - /// Package name -> test command argv (with `<<<features>>>` expanded). - test_commands: HashMap<String, Vec<String>>, -} - -impl ResCrateConfig { - /// The configured `[test] command` for a package, if any. - #[must_use] - pub fn test_command(&self, package: &str) -> Option<&[String]> { - self.test_commands.get(package).map(Vec::as_slice) - } -} - -#[program_setup] -pub fn crate_config_setup(p: &mut Program<ThisProgram>) { - let features = p - .res::<ResFeatureList>() - .map(|f| f.list.clone()) - .unwrap_or_default(); - let joined_features = features.join(","); - - let Some(manifests) = p.res::<Manifests>() else { - return; - }; - - let mut test_commands = HashMap::new(); - for (name, manifest_path) in &manifests.package_dirs { - let config_path = manifest_path - .parent() - .unwrap_or_else(|| Path::new(".")) - .join("mingling-ci.toml"); - - let Ok(content) = std::fs::read_to_string(&config_path) else { - continue; - }; - - let Ok(table) = content.parse::<toml::Value>() else { - continue; - }; - - let Some(command) = table - .get("test") - .and_then(|t| t.get("command")) - .and_then(|c| c.as_array()) - else { - continue; - }; - - let argv: Vec<String> = command - .iter() - .filter_map(|v| v.as_str().map(str::to_string)) - .collect(); - - if argv.is_empty() { - continue; - } - - let argv = argv - .into_iter() - .map(|arg| arg.replace("<<<features>>>", &joined_features)) - .collect(); - test_commands.insert(name.clone(), argv); - } - - p.with_resource(ResCrateConfig { test_commands }); -} diff --git a/mingling_ci/src/res/features.rs b/mingling_ci/src/res/features.rs deleted file mode 100644 index 8009514..0000000 --- a/mingling_ci/src/res/features.rs +++ /dev/null @@ -1,47 +0,0 @@ -use mingling::{Program, macros::program_setup}; - -use crate::ThisProgram; - -/// Manifest that declares the documented feature list. -/// -/// Path is relative to the repo root (the CI's working directory). -const FEATURES_MANIFEST: &str = "./mingling/Cargo.toml"; - -/// The docs.rs feature list of `mingling`, the single source of truth for the -/// feature combinations used by CI checks. -#[derive(Default, Clone)] -pub struct ResFeatureList { - pub list: Vec<String>, -} - -#[program_setup] -pub fn features_setup(p: &mut Program<ThisProgram>) { - p.with_resource(ResFeatureList { - list: docs_rs_features(), - }); -} - -/// Reads `[package.metadata.docs.rs].features` from `mingling/Cargo.toml`. -#[must_use] -fn docs_rs_features() -> Vec<String> { - let Ok(content) = std::fs::read_to_string(FEATURES_MANIFEST) else { - return Vec::new(); - }; - let Ok(toml_value) = content.parse::<toml::Value>() else { - return Vec::new(); - }; - toml_value - .get("package") - .and_then(|p| p.get("metadata")) - .and_then(|m| m.get("docs")) - .and_then(|d| d.get("rs")) - .and_then(|rs| rs.get("features")) - .and_then(|f| f.as_array()) - .map(|features| { - features - .iter() - .filter_map(|v| v.as_str().map(str::to_string)) - .collect() - }) - .unwrap_or_default() -} diff --git a/mingling_ci/src/res/manifests.rs b/mingling_ci/src/res/manifests.rs deleted file mode 100644 index 91836d6..0000000 --- a/mingling_ci/src/res/manifests.rs +++ /dev/null @@ -1,103 +0,0 @@ -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 (`mingling_ci/src/res/`). -const IGNORED_DIRS_FILE: &str = include_str!("../../../.config/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 -/// `.config/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 `.config/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) -} diff --git a/mingling_ci/src/res/print.rs b/mingling_ci/src/res/print.rs deleted file mode 100644 index 9d844a6..0000000 --- a/mingling_ci/src/res/print.rs +++ /dev/null @@ -1,174 +0,0 @@ -use colored::Colorize; -use mingling::config::ErrorOutput; -use mingling::hook::ProgramHook; -use mingling::{Program, macros::program_setup}; -use mingling::{StringVec, this}; - -use crate::ThisProgram; - -#[program_setup] -pub fn print_setup(p: &mut Program<ThisProgram>) { - p.with_resource(CargoError::default()); - p.with_resource(CargoWarn::default()); - p.with_resource(CargoHelp::default()); - p.with_resource(CargoStatus::default()); - - p.with_hook(ProgramHook::empty().on_begin::<_, ()>(move |_| { - let p = this::<ThisProgram>(); - let silence_err = p.stdout_setting.error_output == ErrorOutput::Hide; - - p.modify_res(|r: &mut CargoError| r.silence = silence_err); - p.modify_res(|r: &mut CargoWarn| r.silence = silence_err); - p.modify_res(|r: &mut CargoHelp| r.silence = silence_err); - p.modify_res(|r: &mut CargoStatus| r.silence = silence_err); - })); -} - -#[derive(Default, Clone)] -pub struct CargoError { - silence: bool, -} - -impl MessagePrinter for CargoError { - fn format(&self, msg: impl Into<StringVec>) -> String { - format!("{}: {}", "error".bold().bright_red(), msg.into().join("")) - } - - fn std_mode(&self) -> StandardOutMode { - if self.silence { - StandardOutMode::Silence - } else { - StandardOutMode::Error - } - } -} - -#[derive(Default, Clone)] -pub struct CargoWarn { - silence: bool, -} - -impl MessagePrinter for CargoWarn { - fn format(&self, msg: impl Into<StringVec>) -> String { - format!("{}: {}", "warning".bright_yellow(), msg.into().join("")) - } - - fn std_mode(&self) -> StandardOutMode { - if self.silence { - StandardOutMode::Silence - } else { - StandardOutMode::Error - } - } -} - -#[derive(Default, Clone)] -pub struct CargoHelp { - silence: bool, -} - -impl MessagePrinter for CargoHelp { - fn format(&self, msg: impl Into<StringVec>) -> String { - format!("{}: {}", "help".bright_white(), msg.into().join("")) - } - - fn std_mode(&self) -> StandardOutMode { - if self.silence { - StandardOutMode::Silence - } else { - StandardOutMode::Error - } - } -} - -#[derive(Default, Clone)] -pub struct CargoStatus { - silence: bool, -} - -impl MessagePrinter for CargoStatus { - fn format(&self, msg: impl Into<StringVec>) -> String { - let parts: Vec<String> = msg.into().to_vec(); - let first = if parts.is_empty() { - String::new() - } else { - parts[0].trim().to_string() - }; - - let (prefix, content) = if first.is_empty() { - // Empty: fall back to Info with full message - ("Info".to_string(), parts.join(" ")) - } else if first.chars().count() == 1 { - // Single character: prefix is Info, entire message is content - ("Info".to_string(), parts.join(" ")) - } else if first.chars().count() <= 12 { - // Single part that is a status prefix (no message after it) - if parts.len() == 1 { - ("Info".to_string(), first) - } else { - // First part is a status prefix, remaining parts are the message - let content = parts[1..].join(" ").trim_start().to_string(); - (first, content) - } - } else { - // First part too long: all is message, fall back to Info - ("Info".to_string(), parts.join(" ")) - }; - - let padding = " ".repeat(12usize.saturating_sub(prefix.chars().count())); - - format!( - "{}{} {}", - padding, - prefix.bold().bright_green(), - content.trim() - ) - } - - fn std_mode(&self) -> StandardOutMode { - if self.silence { - StandardOutMode::Silence - } else { - StandardOutMode::Out - } - } -} - -pub trait MessagePrinter { - #[doc(hidden)] - fn println(&self, msg: impl Into<StringVec>) { - match self.std_mode() { - StandardOutMode::Out => println!("{}", self.format(msg)), - StandardOutMode::Error => eprintln!("{}", self.format(msg)), - StandardOutMode::Silence => {} - } - } - - #[doc(hidden)] - fn print(&self, msg: impl Into<StringVec>) { - match self.std_mode() { - StandardOutMode::Out => print!("{}", self.format(msg)), - StandardOutMode::Error => eprint!("{}", self.format(msg)), - StandardOutMode::Silence => {} - } - } - - /// Formats the message string before output. - fn format(&self, msg: impl Into<StringVec>) -> String; - - /// Returns the standard output mode (stdout or stderr). - fn std_mode(&self) -> StandardOutMode; -} - -/// Specifies where standard output messages should be directed. -/// -/// This enum determines whether messages are printed to stdout, stderr, or suppressed entirely. -#[repr(u8)] -pub enum StandardOutMode { - /// Print messages to standard output (stdout). - Out, - /// Print messages to standard error (stderr). - Error, - /// Suppress all output. - Silence, -} |
