From ec9edc294fd5e7e29977fc7b0e6fb953422bc0e2 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 19 Aug 2026 05:54:00 +0800 Subject: chore: reorganize dev tools into dev/ directory and consolidate configs 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. --- mingling_ci/src/task/cmd_build_check.rs | 47 ------ mingling_ci/src/task/cmd_clippy_check.rs | 50 ------ mingling_ci/src/task/cmd_docs_check.rs | 44 ------ mingling_ci/src/task/cmd_example_check.rs | 69 --------- mingling_ci/src/task/cmd_markdown_check.rs | 192 ----------------------- mingling_ci/src/task/cmd_markdown_compare.rs | 221 --------------------------- mingling_ci/src/task/cmd_test.rs | 54 ------- mingling_ci/src/task/run.rs | 114 -------------- 8 files changed, 791 deletions(-) delete mode 100644 mingling_ci/src/task/cmd_build_check.rs delete mode 100644 mingling_ci/src/task/cmd_clippy_check.rs delete mode 100644 mingling_ci/src/task/cmd_docs_check.rs delete mode 100644 mingling_ci/src/task/cmd_example_check.rs delete mode 100644 mingling_ci/src/task/cmd_markdown_check.rs delete mode 100644 mingling_ci/src/task/cmd_markdown_compare.rs delete mode 100644 mingling_ci/src/task/cmd_test.rs delete mode 100644 mingling_ci/src/task/run.rs (limited to 'mingling_ci/src/task') diff --git a/mingling_ci/src/task/cmd_build_check.rs b/mingling_ci/src/task/cmd_build_check.rs deleted file mode 100644 index f67fe2e..0000000 --- a/mingling_ci/src/task/cmd_build_check.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::ffi::OsString; -use std::path::Path; - -use mingling::{ - Grouped, Routable, - macros::{buffer, command, renderer}, - res::ResExitCode, -}; - -use crate::Next; -use crate::res::Manifests; -use crate::task::run::{location, run_parallel_checks}; - -#[command(node = "build-check")] -pub async fn build_check(manifests: &Manifests) -> Next { - let tasks = manifests - .package_dirs - .iter() - .map(|(name, path)| (name.clone(), location(path), build_args(path))) - .collect(); - let fail_count = run_parallel_checks("Build-Check", "Building", tasks).await; - ResultBuildCheck { fail_count }.to_chain() -} - -/// `cargo build --manifest-path ` -fn build_args(path: &Path) -> Vec { - vec![ - "cargo".into(), - "build".into(), - "--manifest-path".into(), - path.as_os_str().to_os_string(), - ] -} - -/// Number of packages that failed to build. -#[derive(Grouped)] -pub struct ResultBuildCheck { - pub fail_count: usize, -} - -/// Silently sets a non-zero exit code when any build failed. -#[renderer(buffer)] -pub fn render_build_check(r: ResultBuildCheck, exit_code: &mut ResExitCode) { - if r.fail_count > 0 { - exit_code.exit_code = 1; - } -} diff --git a/mingling_ci/src/task/cmd_clippy_check.rs b/mingling_ci/src/task/cmd_clippy_check.rs deleted file mode 100644 index a0dd46e..0000000 --- a/mingling_ci/src/task/cmd_clippy_check.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::ffi::OsString; -use std::path::Path; - -use mingling::{ - Grouped, Routable, - macros::{buffer, command, renderer}, - res::ResExitCode, -}; - -use crate::Next; -use crate::res::Manifests; -use crate::task::run::{location, run_parallel_checks}; - -#[command(node = "clippy-check")] -pub async fn clippy_check(manifests: &Manifests) -> Next { - let tasks = manifests - .package_dirs - .iter() - .map(|(name, path)| (name.clone(), location(path), clippy_args(path))) - .collect(); - let fail_count = run_parallel_checks("Clippy-Check", "Clippy", tasks).await; - ResultClippyCheck { fail_count }.to_chain() -} - -/// `cargo clippy --manifest-path -- -D warnings` -fn clippy_args(path: &Path) -> Vec { - vec![ - "cargo".into(), - "clippy".into(), - "--manifest-path".into(), - path.as_os_str().to_os_string(), - "--".into(), - "-D".into(), - "warnings".into(), - ] -} - -/// Number of packages that failed clippy. -#[derive(Grouped)] -pub struct ResultClippyCheck { - pub fail_count: usize, -} - -/// Silently sets a non-zero exit code when any clippy check failed. -#[renderer(buffer)] -pub fn render_clippy_check(r: ResultClippyCheck, exit_code: &mut ResExitCode) { - if r.fail_count > 0 { - exit_code.exit_code = 1; - } -} diff --git a/mingling_ci/src/task/cmd_docs_check.rs b/mingling_ci/src/task/cmd_docs_check.rs deleted file mode 100644 index 3a77d4d..0000000 --- a/mingling_ci/src/task/cmd_docs_check.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::ffi::OsString; - -use mingling::{ - Grouped, Routable, - macros::{buffer, command, renderer}, - res::ResExitCode, -}; - -use crate::Next; -use crate::res::ResFeatureList; -use crate::task::run::run_parallel_checks; - -#[command(node = "docs-check")] -pub async fn docs_check(features: &ResFeatureList) -> Next { - let args = vec![ - OsString::from("cargo"), - OsString::from("rustdoc"), - OsString::from("--features"), - OsString::from(features.list.join(",")), - OsString::from("-p"), - OsString::from("mingling"), - OsString::from("--"), - OsString::from("-D"), - OsString::from("warnings"), - ]; - let tasks = vec![("mingling".to_string(), "./mingling".to_string(), args)]; - let fail_count = run_parallel_checks("Docs-Check", "Docs", tasks).await; - - ResultDocsCheck { fail_count }.to_chain() -} - -/// Number of failed doc builds (0 or 1). -#[derive(Grouped)] -pub struct ResultDocsCheck { - pub fail_count: usize, -} - -/// Silently sets a non-zero exit code when the doc build failed. -#[renderer(buffer)] -pub fn render_docs_check(r: ResultDocsCheck, exit_code: &mut ResExitCode) { - if r.fail_count > 0 { - exit_code.exit_code = 1; - } -} diff --git a/mingling_ci/src/task/cmd_example_check.rs b/mingling_ci/src/task/cmd_example_check.rs deleted file mode 100644 index 1b9f440..0000000 --- a/mingling_ci/src/task/cmd_example_check.rs +++ /dev/null @@ -1,69 +0,0 @@ -use colored::Colorize; -use mingling::{ - Grouped, Routable, - macros::{buffer, command, renderer}, - res::ResExitCode, -}; - -use crate::Next; -use crate::examples::{check_example, load_test_configs}; -use crate::progress::task_progress_bar; -use crate::reporter::{self, ReportResult}; - -#[command(node = "example-check")] -pub async fn example_check() -> Next { - reporter::set_task("Example-Check"); - - let configs = load_test_configs(); - let total = configs.len(); - let pb = task_progress_bar(total, "Testing"); - pb.set_message("examples"); - - // One blocking task per example: build + run its test cases. - let mut handles = Vec::new(); - for example in configs { - handles.push(tokio::task::spawn_blocking(move || check_example(example))); - } - - let mut fail_count = 0; - for handle in handles { - let Ok(outcome) = handle.await else { - continue; - }; - pb.set_message(outcome.name.clone()); - pb.inc(1); - - if outcome.ok { - reporter::export(&outcome.name, &outcome.location, ReportResult::Ok); - } else { - fail_count += 1; - // Plain stderr: `pb.println` is swallowed on non-TTY (CI). - eprintln!(" {} {}", "failed".bright_red(), outcome.name); - eprintln!(" {}", outcome.output); - reporter::export( - &outcome.name, - &outcome.location, - ReportResult::Error(outcome.output), - ); - } - } - - pb.finish_and_clear(); - reporter::flush(); - - ResultExampleCheck { fail_count }.to_chain() -} - -/// Number of examples that failed to build or pass their tests. -#[derive(Grouped)] -pub struct ResultExampleCheck { - pub fail_count: usize, -} - -/// Silently sets a non-zero exit code when any example failed. -#[renderer(buffer)] -pub fn render_example_check(r: ResultExampleCheck, exit_code: &mut ResExitCode) { - if r.fail_count > 0 { - exit_code.exit_code = 1; - } -} diff --git a/mingling_ci/src/task/cmd_markdown_check.rs b/mingling_ci/src/task/cmd_markdown_check.rs deleted file mode 100644 index 2408636..0000000 --- a/mingling_ci/src/task/cmd_markdown_check.rs +++ /dev/null @@ -1,192 +0,0 @@ -use std::collections::HashMap; -use std::path::{Path, PathBuf}; - -use just_fmt::snake_case; -use mingling::{ - Grouped, RenderResult, Routable, - macros::{buffer, command, renderer}, - res::ResExitCode, -}; - -use crate::Next; -use crate::markdown::project::parse_markdown; -use crate::markdown::test::{MarkdownBlockOutcome, try_test_markdown_project}; -use crate::reporter::{self, ReportResult}; -use crate::res::{CargoError, MessagePrinter}; - -const VERIFIED_DOCS: &str = ".config/verified-docs.toml"; - -#[command(node = "markdown-check")] -pub async fn markdown_check(args: Vec) -> Next { - let Some(path_str) = args.first() else { - return ErrorMarkdownArgs("missing argument".to_string()).to_chain(); - }; - let path = - std::env::current_dir().map_or_else(|_| PathBuf::from(path_str), |cwd| cwd.join(path_str)); - if !path.is_file() { - return ErrorMarkdownArgs(format!("{} is not a file", path.display())).to_chain(); - } - let Ok(content) = std::fs::read_to_string(&path) else { - return ErrorMarkdownArgs(format!("failed to read {}", path.display())).to_chain(); - }; - - let location = path.to_string_lossy().into_owned(); - let item = format!("doc-{}", snake_case!(&stem_of(&path))); - reporter::set_task("Markdown-Check"); - - let projects = parse_markdown(&content, &location); - let outcomes = try_test_markdown_project(projects).await; - let file_info = HashMap::from([(location.clone(), (item, location))]); - let fail_count = report_files(&outcomes, &file_info); - reporter::flush(); - - ResultMarkdownCheck { fail_count }.to_chain() -} - -#[command(node = "markdown-check-all")] -pub async fn markdown_check_all() -> Next { - let Some(files) = verified_md_files() else { - return ErrorMarkdownConfig.to_chain(); - }; - reporter::set_task("Markdown-Check-All"); - - // Collect all projects; remember each file's report identity - // (`{key}-{snake_case(file_stem)}` -> location). - let mut projects = Vec::new(); - let mut file_info: HashMap = HashMap::new(); - for (label, path) in files { - let Ok(content) = std::fs::read_to_string(&path) else { - continue; - }; - let file_name = path.file_name().unwrap().to_string_lossy(); - let source_file = format!("{label}/{file_name}"); - let item = format!("{label}-{}", snake_case!(&stem_of(&path))); - let location = path.to_string_lossy().into_owned(); - file_info.insert(source_file.clone(), (item, location)); - projects.extend(parse_markdown(&content, &source_file)); - } - - let outcomes = try_test_markdown_project(projects).await; - let fail_count = report_files(&outcomes, &file_info); - reporter::flush(); - - ResultMarkdownCheck { fail_count }.to_chain() -} - -/// The file name without extension, e.g. `README.md` → `README`. -pub(crate) fn stem_of(path: &Path) -> String { - path.file_stem() - .unwrap_or_default() - .to_string_lossy() - .into_owned() -} - -/// Exports one report entry per source file: `ok` when every block passed, -/// otherwise an error carrying the failed blocks' details. -fn report_files( - outcomes: &[MarkdownBlockOutcome], - file_info: &HashMap, -) -> usize { - let mut by_file: HashMap<&str, (bool, Vec)> = HashMap::new(); - for outcome in outcomes { - let (ok, outputs) = by_file - .entry(outcome.source_file.as_str()) - .or_insert((true, Vec::new())); - if !outcome.ok { - *ok = false; - outputs.push(format!( - "{}:{}:\n{}", - outcome.source_file, outcome.line, outcome.output - )); - } - } - - let mut fail_count = 0; - for (source_file, (ok, outputs)) in by_file { - let Some((item, location)) = file_info.get(source_file) else { - continue; - }; - if ok { - reporter::export(item, location, ReportResult::Ok); - } else { - fail_count += outputs.len(); - reporter::export(item, location, ReportResult::Error(outputs.join("\n\n"))); - } - } - fail_count -} - -/// Reads `verified-docs.toml` and collects all `.md` files: single files, -/// directories, or `**` globs (walked from the base directory). -fn verified_md_files() -> Option> { - let content = std::fs::read_to_string(VERIFIED_DOCS).ok()?; - let table: toml::Table = content.parse().ok()?; - - let mut files: Vec<(String, PathBuf)> = Vec::new(); - for (label, value) in table.get("verified")?.as_table()? { - let value_str = value.as_str()?; - let candidate = PathBuf::from(value_str); - if candidate.is_dir() { - collect_md_files(&candidate, &mut files, label); - } else if candidate.is_file() { - files.push((label.clone(), candidate)); - } else if candidate.extension().is_none() { - // Glob like "docs/pages/**": walk the base directory. - let base = PathBuf::from(value_str.trim_end_matches("/**").trim_end_matches('*')); - if base.is_dir() { - collect_md_files(&base, &mut files, label); - } - } - } - - files.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); - Some(files) -} - -/// Recursively collects all `.md` files under a directory. -fn collect_md_files(dir: &Path, files: &mut Vec<(String, PathBuf)>, label: &str) { - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - collect_md_files(&path, files, label); - } else if path.extension().is_some_and(|ext| ext == "md") { - files.push((label.to_string(), path)); - } - } - } -} - -/// Number of code blocks that failed to build. -#[derive(Grouped)] -pub struct ResultMarkdownCheck { - pub fail_count: usize, -} - -#[derive(Grouped, Default)] -pub struct ErrorMarkdownArgs(pub String); - -#[derive(Grouped, Default)] -pub struct ErrorMarkdownConfig; - -/// Silently sets a non-zero exit code when any block failed. -#[renderer(buffer)] -pub fn render_markdown_check(r: ResultMarkdownCheck, exit_code: &mut ResExitCode) { - if r.fail_count > 0 { - exit_code.exit_code = 1; - } -} - -#[renderer] -pub fn render_error_markdown_args(e: ErrorMarkdownArgs, error: &CargoError) -> RenderResult { - let render_result = RenderResult::new(); - error.println(vec![e.0]); - render_result -} - -#[renderer] -pub fn render_error_markdown_config(_: ErrorMarkdownConfig, error: &CargoError) -> RenderResult { - let render_result = RenderResult::new(); - error.println(vec![format!("failed to read {VERIFIED_DOCS}")]); - render_result -} diff --git a/mingling_ci/src/task/cmd_markdown_compare.rs b/mingling_ci/src/task/cmd_markdown_compare.rs deleted file mode 100644 index b014f1e..0000000 --- a/mingling_ci/src/task/cmd_markdown_compare.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::collections::BTreeSet; -use std::path::{Path, PathBuf}; - -use colored::Colorize; -use just_fmt::snake_case; -use mingling::{ - Grouped, Routable, - macros::{buffer, command, renderer}, - res::ResExitCode, -}; - -use crate::Next; -use crate::markdown::compare::{collect_md_files, compare_signature}; -use crate::reporter::{self, ReportResult}; -use crate::task::cmd_markdown_check::{ErrorMarkdownArgs, ErrorMarkdownConfig, stem_of}; - -const DOCS_DIR: &str = "./docs"; -const LANG_CONFIG: &str = ".config/docs-lang.txt"; - -/// One file-pair outcome of a structure comparison. -struct CompareOutcome { - item: String, - location: String, - ok: bool, - output: String, -} - -#[command(node = "markdown-compare")] -// `#[command]` rewrites an owned first param into the entry type, so the args -// must be passed by value even though the body only reads them. -#[allow(clippy::needless_pass_by_value)] -pub fn markdown_compare(args: Vec) -> Next { - let [ref_arg, trans_arg] = args.as_slice() else { - return ErrorMarkdownArgs("missing and arguments".to_string()) - .to_chain(); - }; - let ref_path = cwd().join(ref_arg); - let trans_path = cwd().join(trans_arg); - - reporter::set_task("Markdown-Compare"); - let outcomes = if ref_path.is_dir() && trans_path.is_dir() { - compare_dirs(&ref_path, &trans_path, "doc") - } else if ref_path.is_file() && trans_path.is_file() { - compare_files(&ref_path, &trans_path, "doc") - } else { - return ErrorMarkdownArgs( - "both arguments must be files or both must be directories".to_string(), - ) - .to_chain(); - }; - let fail_count = export_outcomes(&outcomes); - reporter::flush(); - - ResultMarkdownCompare { fail_count }.to_chain() -} - -#[command(node = "markdown-compare-all")] -pub fn markdown_compare_all() -> Next { - let Some(langs) = lang_config() else { - return ErrorMarkdownConfig.to_chain(); - }; - let Some(reference) = langs.first() else { - return ErrorMarkdownConfig.to_chain(); - }; - let ref_dir = PathBuf::from(DOCS_DIR).join(reference); - if !ref_dir.is_dir() { - return ErrorMarkdownArgs(format!( - "reference docs directory `{}` does not exist", - ref_dir.display() - )) - .to_chain(); - } - - reporter::set_task("Markdown-Compare-All"); - let mut fail_count = 0; - for lang in &langs[1..] { - let lang_dir = PathBuf::from(DOCS_DIR).join(lang); - if !lang_dir.is_dir() { - eprintln!( - " {}: `{}` does not exist", - "ERROR".bright_red(), - lang_dir.display() - ); - fail_count += 1; - continue; - } - let outcomes = compare_dirs(&ref_dir, &lang_dir, &lang_key(lang)); - fail_count += export_outcomes(&outcomes); - } - reporter::flush(); - - ResultMarkdownCompare { fail_count }.to_chain() -} - -/// Compares one file pair (reference vs translation). -fn compare_files(ref_path: &Path, trans_path: &Path, prefix: &str) -> Vec { - let item = format!("{prefix}-{}", snake_case!(&stem_of(ref_path))); - let location = trans_path.to_string_lossy().into_owned(); - match compare_signature(ref_path, trans_path) { - Ok(()) => vec![CompareOutcome { - item, - location, - ok: true, - output: String::new(), - }], - Err(diffs) => vec![CompareOutcome { - item, - location, - ok: false, - output: diffs.join("\n"), - }], - } -} - -/// Compares two directories: every `.md` file in the reference must exist in -/// the translation with the same structural signature; extra files are errors. -fn compare_dirs(ref_dir: &Path, trans_dir: &Path, prefix: &str) -> Vec { - let ref_files = collect_md_files(ref_dir); - let ref_set: BTreeSet = ref_files.iter().cloned().collect(); - let trans_set: BTreeSet = collect_md_files(trans_dir).into_iter().collect(); - - let mut outcomes = Vec::new(); - for file in ref_files { - let item = format!("{prefix}-{}", snake_case!(&stem_of(&file))); - let trans_path = trans_dir.join(&file); - let location = trans_path.to_string_lossy().into_owned(); - if !trans_set.contains(&file) { - outcomes.push(CompareOutcome { - item, - location, - ok: false, - output: "missing in translation".to_string(), - }); - continue; - } - outcomes.push(match compare_signature(&ref_dir.join(&file), &trans_path) { - Ok(()) => CompareOutcome { - item, - location, - ok: true, - output: String::new(), - }, - Err(diffs) => CompareOutcome { - item, - location, - ok: false, - output: diffs.join("\n"), - }, - }); - } - - for file in trans_set.difference(&ref_set) { - let item = format!("{prefix}-{}", snake_case!(&stem_of(file))); - let trans_path = trans_dir.join(file); - outcomes.push(CompareOutcome { - item, - location: trans_path.to_string_lossy().into_owned(), - ok: false, - output: "extra file, not in reference".to_string(), - }); - } - outcomes -} - -/// Exports the outcomes via `reporter`; failures also print to stderr. -fn export_outcomes(outcomes: &[CompareOutcome]) -> usize { - let mut fail_count = 0; - for outcome in outcomes { - if outcome.ok { - reporter::export(&outcome.item, &outcome.location, ReportResult::Ok); - } else { - fail_count += 1; - eprintln!(" {} {}", "failed".bright_red(), outcome.item); - eprintln!(" {}\n{}", outcome.location, outcome.output); - reporter::export( - &outcome.item, - &outcome.location, - ReportResult::Error(outcome.output.clone()), - ); - } - } - fail_count -} - -/// Reads `.config/docs-lang.txt`: the first line is the reference directory -/// (relative to `./docs/`), the rest are translations that must mirror it. -fn lang_config() -> Option> { - let content = std::fs::read_to_string(LANG_CONFIG).ok()?; - Some( - content - .lines() - .map(str::trim) - .filter(|l| !l.is_empty() && !l.starts_with('#')) - .map(|l| l.trim_start_matches("./").to_string()) - .collect(), - ) -} - -/// Turns a lang directory path into a report-item key, e.g. -/// `./_zh_CN/pages/` → `_zh_CN_pages`. -fn lang_key(lang: &str) -> String { - lang.trim_matches('/').replace('/', "_") -} - -fn cwd() -> PathBuf { - std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) -} - -/// Number of files that failed the structure comparison. -#[derive(Grouped)] -pub struct ResultMarkdownCompare { - pub fail_count: usize, -} - -/// Silently sets a non-zero exit code when any comparison failed. -#[renderer(buffer)] -pub fn render_markdown_compare(r: ResultMarkdownCompare, exit_code: &mut ResExitCode) { - if r.fail_count > 0 { - exit_code.exit_code = 1; - } -} diff --git a/mingling_ci/src/task/cmd_test.rs b/mingling_ci/src/task/cmd_test.rs deleted file mode 100644 index 5b9f55a..0000000 --- a/mingling_ci/src/task/cmd_test.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::ffi::OsString; -use std::path::Path; - -use mingling::{ - Grouped, Routable, - macros::{buffer, command, renderer}, - res::ResExitCode, -}; - -use crate::Next; -use crate::res::{Manifests, ResCrateConfig}; -use crate::task::run::{location, run_parallel_checks}; - -#[command(node = "test-all")] -pub async fn test_all(manifests: &Manifests, config: &ResCrateConfig) -> Next { - let tasks = manifests - .package_dirs - .iter() - .map(|(name, path)| { - let args = config.test_command(name).map_or_else( - || test_args(path), - |cmd| cmd.iter().map(|s| OsString::from(s.as_str())).collect(), - ); - (name.clone(), location(path), args) - }) - .collect(); - let fail_count = run_parallel_checks("Test-All", "Testing", tasks).await; - ResultTestAll { fail_count }.to_chain() -} - -/// Default: `cargo test --manifest-path ` (crates without a -/// `mingling-ci.toml` override). -fn test_args(path: &Path) -> Vec { - vec![ - "cargo".into(), - "test".into(), - "--manifest-path".into(), - path.as_os_str().to_os_string(), - ] -} - -/// Number of packages that failed tests. -#[derive(Grouped)] -pub struct ResultTestAll { - pub fail_count: usize, -} - -/// Silently sets a non-zero exit code when any test failed. -#[renderer(buffer)] -pub fn render_test_all(r: ResultTestAll, exit_code: &mut ResExitCode) { - if r.fail_count > 0 { - exit_code.exit_code = 1; - } -} diff --git a/mingling_ci/src/task/run.rs b/mingling_ci/src/task/run.rs deleted file mode 100644 index ba752dd..0000000 --- a/mingling_ci/src/task/run.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::ffi::OsString; -use std::path::Path; - -use colored::Colorize; - -use crate::progress::task_progress_bar; -use crate::reporter::{self, ReportResult}; - -/// The manifest's parent directory, e.g. `./mingling` — the report location -/// for a crate-based item. -pub(crate) fn location(path: &Path) -> String { - path.parent() - .map_or_else(|| ".".to_string(), |d| d.to_string_lossy().into_owned()) -} - -/// Outcome of a `cargo` subcommand. -struct CargoResult { - ok: bool, - exit_code: Option, - output: String, -} - -/// Runs the given cargo task list in parallel. -/// -/// Each task is an `(item, location, argv)` triple; progress and failures go -/// to stderr: a failing task prints its output immediately and writes its -/// report entry at the same time. Returns the number of failing tasks. -pub(crate) async fn run_parallel_checks( - task: &str, - phase: &str, - tasks: Vec<(String, String, Vec)>, -) -> usize { - reporter::set_task(task); - - let n = tasks.len(); - let pb = task_progress_bar(n, phase); - pb.set_message("tasks"); - - // Run each task in parallel. - let mut set = tokio::task::JoinSet::new(); - for (item, location, args) in tasks { - set.spawn(async move { (item, location, run_cargo(args).await) }); - } - - let mut fail_count = 0; - while let Some(joined) = set.join_next().await { - let Ok((item, location, result)) = joined else { - continue; - }; - pb.inc(1); - pb.set_message(item.clone()); - - if result.ok { - reporter::export(&item, &location, ReportResult::Ok); - } else { - fail_count += 1; - // Failures print to stderr immediately (bar suspended to avoid - // interleaving) and write their report entry at the same time. - pb.suspend(|| { - eprintln!( - "{}: {} failed{}", - phase.bold().bright_cyan(), - item, - result - .exit_code - .map_or_else(String::new, |c| format!(" (exit code {c})")) - ); - for line in result.output.lines() { - eprintln!(" {line}"); - } - }); - reporter::export(&item, &location, ReportResult::Error(result.output)); - } - } - - pb.finish_and_clear(); - reporter::flush(); - fail_count -} - -/// Runs a `cargo` subcommand, capturing its output. -/// Runs a cargo subcommand (`argv[0]` is the program), capturing its output. -async fn run_cargo(argv: Vec) -> CargoResult { - let mut argv = argv.into_iter(); - let Some(program) = argv.next() else { - return CargoResult { - ok: false, - exit_code: None, - output: "empty command".to_string(), - }; - }; - - let output = tokio::process::Command::new(program) - .args(argv) - .output() - .await; - - match output { - Ok(output) => { - let mut log = String::from_utf8_lossy(&output.stdout).into_owned(); - log.push_str(&String::from_utf8_lossy(&output.stderr)); - CargoResult { - ok: output.status.success(), - exit_code: output.status.code(), - output: log, - } - } - Err(e) => CargoResult { - ok: false, - exit_code: None, - output: format!("failed to run cargo: {e}"), - }, - } -} -- cgit