diff options
Diffstat (limited to '.run/src/bin/ci.rs')
| -rw-r--r-- | .run/src/bin/ci.rs | 444 |
1 files changed, 0 insertions, 444 deletions
diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs deleted file mode 100644 index c090eaa..0000000 --- a/.run/src/bin/ci.rs +++ /dev/null @@ -1,444 +0,0 @@ -use std::io::Write as _; -use std::path::{Path, PathBuf}; -use std::process::exit; - -use arg_picker::{Picker, macros::arg}; -use tools::{ - cargo_tomls, crate_name_from, eprintln_cargo_style, println_cargo_style, run_cmd, run_parallel, -}; - -fn get_ignore_dirs() -> Vec<String> { - vec![".temp".to_string()] -} - -/// A single CI step, each individually toggleable via `--check-*`. -struct Checks { - build: bool, - clippy: bool, - test: bool, - arg_picker: bool, - markdown_code: bool, - examples: bool, - docs_refresh: bool, - api_docs: bool, -} - -impl Checks { - fn any(&self) -> bool { - self.build - || self.clippy - || self.test - || self.arg_picker - || self.markdown_code - || self.examples - || self.docs_refresh - || self.api_docs - } -} - -fn print_help() { - println!( - r" -Usage: ci [options] -Options: - -h, --help Print this help message - -y Auto-confirm temporary commits - --dirty Run CI on dirty workspace (skip temp commit & clean check) - --check-build Build all crates - --check-clippy Run clippy on all crates (-D warnings) - --check-test Run unit tests for all crates - --check-arg-picker Test the arg-picker crate - --check-markdown-code Verify all *.md code blocks compile - --check-examples Test all examples - --check-docs-refresh Refresh docs and fail if the tree is contaminated - --check-api-docs Build API docs with docs.rs features - -If no specific options are given, all checks are run. - " - ); -} - -fn main() { - #[cfg(windows)] - let _ = colored::control::set_virtual_terminal(true); - println!("{}", include_str!("../../../docs/res/ci_banner.txt")); - - let ( - auto_yes, - dirty, - check_build, - check_clippy, - check_test, - check_arg_picker, - check_markdown_code, - check_examples, - check_docs_refresh, - check_api_docs, - help, - ) = Picker::from_args() - .pick_or_default(&arg![yes: bool, 'y']) - .pick_or_default(&arg![dirty: bool]) - .pick_or_default(&arg![check_build: bool]) - .pick_or_default(&arg![check_clippy: bool]) - .pick_or_default(&arg![check_test: bool]) - .pick_or_default(&arg![check_arg_picker: bool]) - .pick_or_default(&arg![check_markdown_code: bool]) - .pick_or_default(&arg![check_examples: bool]) - .pick_or_default(&arg![check_docs_refresh: bool]) - .pick_or_default(&arg![check_api_docs: bool]) - .pick_or_default(&arg![help: bool, 'h']) - .unwrap(); - - if help { - print_help(); - return; - } - - let checks = Checks { - build: check_build, - clippy: check_clippy, - test: check_test, - arg_picker: check_arg_picker, - markdown_code: check_markdown_code, - examples: check_examples, - docs_refresh: check_docs_refresh, - api_docs: check_api_docs, - }; - let run_all = !checks.any(); - - let needs_commit_temp = !dirty && !{ run_cmd!("git diff-index --quiet HEAD --").is_ok() }; - - if needs_commit_temp { - if auto_yes { - run_cmd!("git add .").unwrap(); - run_cmd!("git commit -m \"[DO NOT PUSH] CI TEMP [DO NOT PUSH]\"").unwrap(); - } else { - print!("Working tree is not clean, temporarily commit? [y/N]:"); - std::io::stdout().flush().unwrap(); - let mut input = String::new(); - std::io::stdin().read_line(&mut input).unwrap(); - let input = input.trim(); - if input == "y" || input == "Y" || input == "yes" || input == "Yes" { - run_cmd!("git add .").unwrap(); - run_cmd!("git commit -m \"[DO NOT PUSH] CI TEMP [DO NOT PUSH]\"").unwrap(); - } else { - eprintln_cargo_style!("Aborting."); - exit(2) - } - } - } - - if let Err(exit_code) = ci(&checks, run_all) { - restore_workspace(needs_commit_temp).unwrap(); - exit(exit_code) - } - - if !dirty { - let is_worktree_clean = run_cmd!("git diff-index --quiet HEAD --").is_ok(); - if !is_worktree_clean { - eprintln_cargo_style!("The repository was contaminated during CI, failing!"); - - // Print git status - println!(); - let _ = run_cmd!("git status"); - - if needs_commit_temp { - restore_workspace(true).unwrap(); - } - exit(1) - } - } - - println_cargo_style!("Done: All check passed!"); - - if needs_commit_temp { - restore_workspace(true).unwrap(); - } -} - -fn restore_workspace(undo_commit: bool) -> Result<(), i32> { - run_cmd!("git reset --hard --quiet")?; - if undo_commit { - run_cmd!("git reset --soft HEAD~1 --quiet")?; - run_cmd!("git reset --quiet")?; - } - Ok(()) -} - -/// Run one CI step. -/// -/// When `continue_on_error` is set (used for the documentation steps in -/// "run all" mode), a failing step is recorded and the remaining steps still -/// execute, so every problem is reported in a single run. -fn run_step( - exit_code: &mut i32, - phase: &str, - step: fn() -> Result<(), i32>, - continue_on_error: bool, -) -> Result<(), i32> { - println_cargo_style!(phase); - match step() { - Ok(()) => Ok(()), - Err(code) if continue_on_error => { - *exit_code = (*exit_code).max(code); - Ok(()) - } - Err(code) => Err(code), - } -} - -fn ci(checks: &Checks, run_all: bool) -> Result<(), i32> { - let mut exit_code = 0; - - if run_all || checks.build { - run_step( - &mut exit_code, - "Phase: Scan and build all crates", - build_all, - false, - )?; - } - if run_all || checks.clippy { - run_step( - &mut exit_code, - "Phase: Run clippy for all crates", - clippy_all, - false, - )?; - } - if run_all || checks.test { - run_step(&mut exit_code, "Phase: Test all crates", test_all, false)?; - } - if run_all || checks.arg_picker { - run_step( - &mut exit_code, - "Phase: Test arg picker", - test_arg_picker, - false, - )?; - } - - if run_all || checks.markdown_code { - run_step( - &mut exit_code, - "Phase: Verify all *.md document code blocks are compilable", - test_docs_code_blocks, - run_all, - )?; - } - if run_all || checks.examples { - run_step( - &mut exit_code, - "Phase: Test all examples", - test_examples, - run_all, - )?; - } - if run_all || checks.docs_refresh { - run_step( - &mut exit_code, - "Phase: Check all documentation is up to date", - docs_refresh, - run_all, - )?; - } - if run_all || checks.api_docs { - run_step( - &mut exit_code, - "Phase: Try Build API docs", - deploy_api_docs, - run_all, - )?; - } - - if exit_code != 0 { - return Err(exit_code); - } - - run_cmd!("git add --renormalize .")?; - - Ok(()) -} - -fn test_examples() -> Result<(), i32> { - run_cmd!("cargo run --manifest-path .run/Cargo.toml --color always --bin test-examples") -} - -fn test_docs_code_blocks() -> Result<(), i32> { - run_cmd!( - "cargo run --manifest-path .run/Cargo.toml --color always --bin test-all-markdown-code" - ) -} - -/// Returns the manifest paths of all workspace members (via `cargo metadata --no-deps`). -/// -/// These crates are tested/built/clipped together with `--workspace` so that -/// feature-gated code is covered, instead of relying on each crate's default features. -fn workspace_manifests() -> Vec<PathBuf> { - let Ok(output) = tools::run_cmd_capture("cargo metadata --no-deps --format-version 1") else { - return Vec::new(); - }; - let Ok(json) = serde_json::from_str::<serde_json::Value>(&output) else { - return Vec::new(); - }; - json["packages"] - .as_array() - .into_iter() - .flatten() - .filter_map(|p| p["manifest_path"].as_str().map(PathBuf::from)) - .collect() -} - -fn same_path(a: &Path, b: &Path) -> bool { - let norm = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf()); - norm(a) == norm(b) -} - -fn build_all() -> Result<(), i32> { - let ignore_dirs = get_ignore_dirs(); - let cargo_tomls = cargo_tomls(); - let workspace_manifests = workspace_manifests(); - let mut tasks = Vec::new(); - - // Workspace members: build with all documented features (same set used by cov-test) - let features_arg = doc_features_arg(); - tasks.push(( - "Build: workspace".to_string(), - "workspace".to_string(), - format!("cargo build --workspace{features_arg} --color always"), - )); - - for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(Path::new("")); - let path_str = path.to_string_lossy(); - if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { - continue; - } - if workspace_manifests - .iter() - .any(|m| same_path(m, &cargo_toml)) - { - continue; - } - let label = format!("Build: {}", cargo_toml.to_string_lossy()); - let crate_name = crate_name_from(&cargo_toml); - let cmd = format!( - "cargo build --manifest-path {} --color always", - cargo_toml.to_string_lossy() - ); - tasks.push((label, crate_name, cmd)); - } - run_parallel("Building", tasks) -} - -fn clippy_all() -> Result<(), i32> { - let ignore_dirs = get_ignore_dirs(); - let cargo_tomls = cargo_tomls(); - let workspace_manifests = workspace_manifests(); - let mut tasks = Vec::new(); - - // Workspace members: clippy with all documented features - let features_arg = doc_features_arg(); - tasks.push(( - "Clippy: workspace".to_string(), - "workspace".to_string(), - format!("cargo clippy --workspace{features_arg} --color always -- -D warnings"), - )); - - for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(Path::new("")); - let path_str = path.to_string_lossy(); - if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { - continue; - } - if workspace_manifests - .iter() - .any(|m| same_path(m, &cargo_toml)) - { - continue; - } - let label = format!("Clippy: {}", cargo_toml.to_string_lossy()); - let crate_name = crate_name_from(&cargo_toml); - let cmd = format!( - "cargo clippy --manifest-path {} --color always -- -D warnings", - cargo_toml.to_string_lossy() - ); - tasks.push((label, crate_name, cmd)); - } - run_parallel("Clippy", tasks) -} - -/// ` --features "<docs.rs features>"` (empty string when unavailable) -fn doc_features_arg() -> String { - match tools::read_features() { - Ok(features) if !features.is_empty() => format!(" --features \"{}\"", features.join(",")), - _ => String::new(), - } -} - -fn test_all() -> Result<(), i32> { - let ignore_dirs = get_ignore_dirs(); - let cargo_tomls = cargo_tomls(); - let workspace_manifests = workspace_manifests(); - let mut tasks = Vec::new(); - - // Workspace members: test with all documented features so that feature-gated - // tests (comp/repl/picker/structural_renderer/...) are actually executed. - // `arg-picker` is excluded here and tested separately via [`test_arg_picker`]. - let features_arg = doc_features_arg(); - tasks.push(( - "Test: workspace".to_string(), - "workspace".to_string(), - format!("cargo test --workspace{features_arg} --exclude arg-picker --color always"), - )); - - for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(Path::new("")); - let path_str = path.to_string_lossy(); - if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { - continue; - } - if workspace_manifests - .iter() - .any(|m| same_path(m, &cargo_toml)) - { - continue; - } - let label = format!("Test: {}", cargo_toml.to_string_lossy()); - let crate_name = crate_name_from(&cargo_toml); - let cmd = format!( - "cargo test --manifest-path {} --color always", - cargo_toml.to_string_lossy() - ); - tasks.push((label, crate_name, cmd)); - } - run_parallel("Testing", tasks) -} - -/// `arg-picker` is excluded from the workspace test command: when built with -/// `mingling_support` (enabled via `mingling/picker`), its README doctests -/// expand `arg!` to `::mingling::picker::PickerArg`, which is not available -/// inside the arg-picker crate itself. Test it separately with its default -/// features instead. -fn test_arg_picker() -> Result<(), i32> { - run_cmd!("cargo test -p arg-picker --color always") -} - -fn deploy_api_docs() -> Result<(), i32> { - run_cmd!( - "cargo run --manifest-path .run/Cargo.toml --color always --bin deploy-api-docs -- --docsrs" - ) -} - -fn docs_refresh() -> Result<(), i32> { - println_cargo_style!("Refresh: document at `./docs/`"); - - run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin docs-code-box-fix")?; - run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin docsify-sidebar-gen")?; - run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin refresh-docs")?; - run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin refresh-feature-mod")?; - run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin sync-examples")?; - run_cmd!("cargo fmt")?; - - Ok(()) -} |
