aboutsummaryrefslogtreecommitdiff
path: root/mingling_ci/src/res
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-18 08:27:36 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-18 08:27:36 +0800
commit5d0918ff5e19ebc67e4698bc308f6562779b7110 (patch)
treea9d7f5fb54afd16056b0a4c24d118db2d95f8d78 /mingling_ci/src/res
parent00cf68abdd2be1cfca8b5a268d087ea2ccedf1dd (diff)
feat: add new Mingling CI system crate (WIP)
Add `mingling_ci` as a standalone CI system built on Mingling 0.4.0 to validate the next version, with report collection and manifest listing commands.
Diffstat (limited to 'mingling_ci/src/res')
-rw-r--r--mingling_ci/src/res/manifests.rs100
-rw-r--r--mingling_ci/src/res/print.rs174
2 files changed, 274 insertions, 0 deletions
diff --git a/mingling_ci/src/res/manifests.rs b/mingling_ci/src/res/manifests.rs
new file mode 100644
index 0000000..56829bb
--- /dev/null
+++ b/mingling_ci/src/res/manifests.rs
@@ -0,0 +1,100 @@
+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>,
+}
+
+#[program_setup]
+pub fn manifests_setup(p: &mut Program<ThisProgram>) {
+ p.with_resource(Manifests {
+ path: cargo_tomls(),
+ });
+}
+
+/// 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.
+pub(crate) 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
new file mode 100644
index 0000000..9d844a6
--- /dev/null
+++ b/mingling_ci/src/res/print.rs
@@ -0,0 +1,174 @@
+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,
+}