aboutsummaryrefslogtreecommitdiff
path: root/mingling_ci/src/res
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-19 05:54:00 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-19 06:15:20 +0800
commitec9edc294fd5e7e29977fc7b0e6fb953422bc0e2 (patch)
treebbf89ef4457b8de8a8a9bca5668045d612d27572 /mingling_ci/src/res
parent06dfc27194c11e1d1033c292c759a1c5d82e780b (diff)
chore: reorganize dev tools into dev/ directory and consolidate configsHEADmain
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.rs203
-rw-r--r--mingling_ci/src/res/crate_config.rs79
-rw-r--r--mingling_ci/src/res/features.rs47
-rw-r--r--mingling_ci/src/res/manifests.rs103
-rw-r--r--mingling_ci/src/res/print.rs174
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,
-}