aboutsummaryrefslogtreecommitdiff
path: root/.run/src/bin
diff options
context:
space:
mode:
Diffstat (limited to '.run/src/bin')
-rw-r--r--.run/src/bin/build-all.ps18
-rw-r--r--.run/src/bin/build-all.sh6
-rw-r--r--.run/src/bin/ci.py75
-rw-r--r--.run/src/bin/ci.rs239
-rw-r--r--.run/src/bin/clippy.ps18
-rw-r--r--.run/src/bin/clippy.sh6
-rw-r--r--.run/src/bin/cov-test.rs493
-rw-r--r--.run/src/bin/deploy-api-docs.rs9
-rw-r--r--.run/src/bin/doc-nightly.ps11
-rw-r--r--.run/src/bin/doc-nightly.sh3
-rw-r--r--.run/src/bin/doc.ps11
-rwxr-xr-x.run/src/bin/doc.sh3
-rw-r--r--.run/src/bin/docs-code-box-fix.rs166
-rw-r--r--.run/src/bin/docsify-sidebar-gen.rs262
-rw-r--r--.run/src/bin/install-mling.ps115
-rwxr-xr-x[-rw-r--r--].run/src/bin/install-mling.sh17
-rw-r--r--.run/src/bin/package-all.rs461
-rw-r--r--.run/src/bin/refresh-docs.rs178
-rw-r--r--.run/src/bin/refresh-feature-mod.rs97
-rw-r--r--.run/src/bin/sync-examples.rs131
-rw-r--r--.run/src/bin/test-all-markdown-code.rs261
-rw-r--r--.run/src/bin/test-all.ps18
-rw-r--r--.run/src/bin/test-all.sh6
-rw-r--r--.run/src/bin/test-examples.rs158
-rw-r--r--.run/src/bin/update-version.rs6
25 files changed, 933 insertions, 1685 deletions
diff --git a/.run/src/bin/build-all.ps1 b/.run/src/bin/build-all.ps1
deleted file mode 100644
index 4f35ed8..0000000
--- a/.run/src/bin/build-all.ps1
+++ /dev/null
@@ -1,8 +0,0 @@
-$starting_dir = Get-Location
-Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object {
- $project_dir = $_.DirectoryName
- Push-Location $project_dir
- cargo build
- Pop-Location
-}
-Set-Location $starting_dir
diff --git a/.run/src/bin/build-all.sh b/.run/src/bin/build-all.sh
deleted file mode 100644
index 2036b41..0000000
--- a/.run/src/bin/build-all.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-find . -name "Cargo.toml" -type f | while read -r cargo_file; do
- project_dir=$(dirname "$cargo_file")
- (cd "$project_dir" && cargo build)
-done
diff --git a/.run/src/bin/ci.py b/.run/src/bin/ci.py
new file mode 100644
index 0000000..6234a19
--- /dev/null
+++ b/.run/src/bin/ci.py
@@ -0,0 +1,75 @@
+"""Full CI orchestration for the mingling project.
+
+Runs every `cargo ci` step in order: lock the workspace, run all checks,
+refresh the generated artifacts, then unlock. The final `git-unlock` doubles
+as the idempotency check: it fails with a non-zero exit code when the run
+left the working tree dirty.
+
+The script locates the git repository root and runs with it as the working
+directory, so it can be invoked from anywhere inside the repo.
+"""
+
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+# The pipeline steps, in execution order, as (command, args) pairs.
+STEPS: list[tuple[str, list[str]]] = [
+ ("git-lock", []),
+ ("report-clean", []),
+ ("build-check", []),
+ ("clippy-check", []),
+ ("test-all", []),
+ ("example-check", []),
+ ("docs-check", []),
+ ("example-refresh", []),
+ ("docsify-refresh", []),
+ ("features-refresh", []),
+ # Idempotency check: exits non-zero if CI contaminated the workspace, and
+ # prints the diff of the contamination before restoring.
+ ("git-unlock", ["--show-diff"]),
+]
+
+
+def find_repo_root() -> Path:
+ """Return the nearest ancestor directory containing `.git`."""
+ current = Path.cwd()
+ for directory in (current, *current.parents):
+ if (directory / ".git").is_dir():
+ return directory
+ raise SystemExit("error: not inside a git repository")
+
+
+def main() -> int:
+ root = find_repo_root()
+ os.chdir(root)
+
+ # Signature banner: docs/res/ci_banner.txt, relative to this script
+ # (.run/src/bin -> four levels up is the repo root).
+ banner = (
+ Path(__file__).resolve().parent.parent.parent.parent
+ / "docs"
+ / "res"
+ / "ci_banner.txt"
+ )
+ try:
+ print(banner.read_text(encoding="utf-8"), end="")
+ except OSError:
+ pass
+
+ for command, args in STEPS:
+ print(f"==> cargo ci {' '.join([command, *args])}")
+ result = subprocess.run(["cargo", "ci", command, *args], check=False)
+ if result.returncode != 0:
+ print(
+ f"error: step `{command}` failed with exit code {result.returncode}",
+ file=sys.stderr,
+ )
+ return result.returncode
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs
deleted file mode 100644
index 954034c..0000000
--- a/.run/src/bin/ci.rs
+++ /dev/null
@@ -1,239 +0,0 @@
-use std::io::Write as _;
-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(),
- "mling/res".to_string(),
- "mling\\res".to_string(),
- ]
-}
-
-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)
- --refresh-docs Refresh documentation files
- --test-docs Run documentation tests (build, clippy, test)
- --test-codes Test examples and documentation code blocks
-
-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, test_docs, refresh_docs, test_codes, help) = Picker::from_args()
- .pick_or_default(&arg![yes: bool, 'y'])
- .pick_or_default(&arg![dirty: bool])
- .pick_or_default(&arg![test_docs: bool])
- .pick_or_default(&arg![refresh_docs: bool])
- .pick_or_default(&arg![test_codes: bool])
- .pick_or_default(&arg![help: bool, 'h'])
- .unwrap();
-
- if help {
- print_help();
- return;
- }
-
- let any_specified = test_docs || refresh_docs || test_codes;
- let run_all = !any_specified;
-
- 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(test_docs, test_codes, 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(())
-}
-
-fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> {
- if run_all || test_codes {
- println_cargo_style!("Phase: Scan and build all crates");
- build_all()?;
-
- println_cargo_style!("Phase: Run clippy for all crates");
- clippy_all()?;
-
- println_cargo_style!("Phase: Test all crates");
- test_all()?;
- }
-
- if run_all || test_docs {
- let mut exit_code = 0;
-
- println_cargo_style!("Phase: Verify all *.md document code blocks are compilable");
- if let Err(code) = test_docs_code_blocks() {
- exit_code = exit_code.max(code);
- }
-
- println_cargo_style!("Phase: Test all examples");
- if let Err(code) = test_examples() {
- exit_code = exit_code.max(code);
- }
-
- println_cargo_style!("Phase: Check all documentation is up to date");
- if let Err(code) = docs_refresh() {
- exit_code = exit_code.max(code);
- }
-
- 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"
- )
-}
-
-fn build_all() -> Result<(), i32> {
- let ignore_dirs = get_ignore_dirs();
- let cargo_tomls = cargo_tomls();
- let mut tasks = Vec::new();
- for cargo_toml in cargo_tomls {
- let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
- let path_str = path.to_string_lossy();
- if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
- 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 mut tasks = Vec::new();
- for cargo_toml in cargo_tomls {
- let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
- let path_str = path.to_string_lossy();
- if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
- 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)
-}
-
-fn test_all() -> Result<(), i32> {
- let ignore_dirs = get_ignore_dirs();
- let cargo_tomls = cargo_tomls();
- let mut tasks = Vec::new();
- for cargo_toml in cargo_tomls {
- let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
- let path_str = path.to_string_lossy();
- if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
- continue;
- }
- let label = format!("Testing: {}", 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)
-}
-
-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(())
-}
diff --git a/.run/src/bin/clippy.ps1 b/.run/src/bin/clippy.ps1
deleted file mode 100644
index 1858873..0000000
--- a/.run/src/bin/clippy.ps1
+++ /dev/null
@@ -1,8 +0,0 @@
-$starting_dir = Get-Location
-Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object {
- $project_dir = $_.DirectoryName
- Push-Location $project_dir
- cargo clippy --quiet
- Pop-Location
-}
-Set-Location $starting_dir
diff --git a/.run/src/bin/clippy.sh b/.run/src/bin/clippy.sh
deleted file mode 100644
index b393545..0000000
--- a/.run/src/bin/clippy.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-find . -name "Cargo.toml" -type f | while read -r cargo_file; do
- project_dir=$(dirname "$cargo_file")
- (cd "$project_dir" && cargo clippy --quiet)
-done
diff --git a/.run/src/bin/cov-test.rs b/.run/src/bin/cov-test.rs
index a53f74c..f62ff01 100644
--- a/.run/src/bin/cov-test.rs
+++ b/.run/src/bin/cov-test.rs
@@ -1,35 +1,194 @@
+//! Coverage test generator for mingling.
+//!
+//! This script requires the **fork** of cargo-llvm-cov:
+//! <https://github.com/Weicao-CatilGrass/cargo-llvm-cov>
+//!
+//! The upstream `report` command cannot include binaries of non-workspace
+//! crates (examples and test crates) and unconditionally filters
+//! `tests`/`examples` source files. The fork adds two flags to fix this:
+//!
+//! - `--object <PATH>`: include arbitrary binaries in the report
+//! (upstream issue taiki-e/cargo-llvm-cov#367)
+//! - `--include-examples`: stop filtering source files under the
+//! `examples` directory (upstream issue taiki-e/cargo-llvm-cov#503)
+//!
+//! The script itself does not use `--include-examples`; it passes
+//! `--no-default-ignore-filename-regex` and supplies its own filter so that
+//! `tests`/`benches` directories stay in the report too.
+//!
+//! Install it with:
+//!
+//! ```bash
+//! cargo install --git https://github.com/Weicao-CatilGrass/cargo-llvm-cov cargo-llvm-cov
+//! ```
+
use std::fs;
-use tools::{println_cargo_style, run_cmd};
+use std::path::{Path, PathBuf};
+
+use serde::Deserialize;
+use tools::{eprintln_cargo_style, println_cargo_style, run_cmd};
const OUTPUT_DIR: &str = "docs/cov-test";
+/// Shared target directory for all `cargo llvm-cov` runs.
+///
+/// Pointing every run at the same target dir makes all of them share the
+/// instrumented build cache and, more importantly, accumulate profraw files
+/// in one place so the final `report` can merge everything.
+const COV_TARGET_DIR: &str = ".temp/cov-llvm";
+
+/// An example's `test.toml` (`[[runs]]` entries).
+#[derive(Deserialize)]
+struct TestConfig {
+ runs: Vec<TestCase>,
+}
+
+/// One `[[runs]]` entry of an example's `test.toml`.
+#[derive(Deserialize)]
+struct TestCase {
+ input: Vec<String>,
+}
+
fn main() {
let repo_root = find_git_repo().expect("Failed to find git repository root");
let output_path = repo_root.join(OUTPUT_DIR);
+ let cov_target = repo_root.join(COV_TARGET_DIR);
// Read features from [package.metadata.docs.rs]
let features = tools::read_features().unwrap_or_else(|e| {
eprintln!("Error: {}", e);
std::process::exit(1);
});
-
let features_arg = features.join(",");
// Ensure output directory exists
std::fs::create_dir_all(&output_path).expect("Failed to create output directory");
+ std::fs::create_dir_all(&cov_target).expect("Failed to create cov target directory");
- let cmd = format!(
- "cargo llvm-cov --html --output-dir \"{}\" --workspace --features \"{}\" --color always",
- output_path.to_string_lossy(),
- features_arg,
- );
+ // All `cargo llvm-cov` invocations below share one target dir, so profraw
+ // files accumulate and are merged by the final `report` command.
+ // SAFETY: set before any thread is spawned; this process only shells out
+ // to subcommands via std::process.
+ unsafe {
+ std::env::set_var("CARGO_LLVM_COV_TARGET_DIR", &cov_target);
+ }
+
+ // Drop stale profraw from previous runs (keep the instrumented build cache).
+ clean_old_profraw(&cov_target);
println_cargo_style!("Features: {}", features_arg);
- println_cargo_style!("Coverage: {}", output_path.display());
+ println_cargo_style!("Target: {}", cov_target.display());
- println_cargo_style!("Running: cargo llvm-cov --html");
- run_cmd!(&cmd).unwrap_or_else(|code| {
- eprintln!("Error: cargo llvm-cov failed with exit code {}", code);
+ // 1. Workspace tests
+ println_cargo_style!("Running: cargo llvm-cov test --workspace");
+ run_cmd!(format!(
+ "cargo llvm-cov test --no-report --workspace --features \"{}\" --color always",
+ features_arg
+ ))
+ .unwrap_or_else(|code| {
+ eprintln_cargo_style!("workspace tests failed with exit code {}", code);
+ std::process::exit(code);
+ });
+
+ // 2. Integration test crates under mingling_core/tests (excluded from the
+ // workspace, so they need their own `--manifest-path` runs)
+ for manifest in find_test_crate_manifests(&repo_root) {
+ println_cargo_style!(
+ "Running: cargo llvm-cov test {}",
+ manifest.file_name().unwrap_or_default().to_string_lossy()
+ );
+ run_cmd!(format!(
+ "cargo llvm-cov test --no-report --manifest-path \"{}\" --color always",
+ manifest.display()
+ ))
+ .unwrap_or_else(|code| {
+ eprintln_cargo_style!(
+ "test crate {} failed with exit code {}",
+ manifest.display(),
+ code
+ );
+ std::process::exit(code);
+ });
+ }
+
+ // 3. Examples: build each example with explicit RUSTFLAGS, then execute
+ // every command declared in the example's test.toml directly.
+ //
+ // NOTE: `cargo llvm-cov run` cannot be used here. Its rustc wrapper
+ // only instruments the crates of the *current* cargo project (with
+ // `--manifest-path` that is the example itself), so the mingling
+ // libraries — being dependencies — would not be instrumented and their
+ // coverage would silently be lost (once_exec.rs showed 0%). Building
+ // with plain RUSTFLAGS instruments the whole dependency graph.
+ //
+ // RUSTFLAGS/CARGO_TARGET_DIR are set process-wide here because only the
+ // `report` step (which does not compile) follows. Non-zero exit codes
+ // are expected for some examples (e.g. `--help` exits with 2); profraw
+ // is still written.
+ unsafe {
+ std::env::set_var("RUSTFLAGS", "-Cinstrument-coverage");
+ std::env::set_var("CARGO_TARGET_DIR", &cov_target);
+ }
+ let examples = load_example_commands(&repo_root);
+ let mut built = std::collections::HashSet::new();
+ for (example, input) in &examples {
+ if built.insert(example.clone()) {
+ println_cargo_style!("Building: {}", example);
+ run_cmd!(format!(
+ "cargo build --manifest-path examples/{}/Cargo.toml --color always",
+ example
+ ))
+ .unwrap_or_else(|code| {
+ eprintln_cargo_style!(
+ "build of example {} failed with exit code {}",
+ example,
+ code
+ );
+ std::process::exit(code);
+ });
+ }
+ let binary = cov_target.join("debug").join(get_binary_name(example));
+ let profraw = format!(
+ "{}/example-{}.%p.profraw",
+ cov_target.to_string_lossy(),
+ example
+ );
+ match std::process::Command::new(&binary)
+ .args(input)
+ .env("LLVM_PROFILE_FILE", &profraw)
+ .status()
+ {
+ Ok(status) if status.success() => {}
+ Ok(status) => println_cargo_style!(
+ "Warning: example {} exited with {:?}, profraw still recorded",
+ example,
+ status.code()
+ ),
+ Err(e) => eprintln_cargo_style!("Failed to run example {}: {}", example, e),
+ }
+ }
+
+ // 4. Collect the binaries of non-workspace crates (examples + test crates).
+ // The automatic object-file detection only knows workspace members, so
+ // these must be passed explicitly via --object.
+ let member_names = workspace_member_names(&repo_root);
+ let object_args = collect_object_args(&cov_target, &member_names);
+
+ // 5. Generate the merged HTML report.
+ //
+ // --no-default-ignore-filename-regex: the default regex unconditionally
+ // excludes `examples`/`tests` directories, which is exactly what we want
+ // to include here, so we take over the filter ourselves.
+ let ignore_re = build_ignore_regex(&cov_target);
+ println_cargo_style!("Running: cargo llvm-cov report --html");
+ run_cmd!(format!(
+ "cargo llvm-cov report --html --output-dir \"{}\" --no-default-ignore-filename-regex --ignore-filename-regex \"{}\" {} --color always",
+ output_path.to_string_lossy(),
+ ignore_re,
+ object_args
+ ))
+ .unwrap_or_else(|code| {
+ eprintln_cargo_style!("cargo llvm-cov report failed with exit code {}", code);
std::process::exit(code);
});
@@ -38,7 +197,6 @@ fn main() {
if html_dir.exists() && html_dir.is_dir() {
println_cargo_style!("Moving files from {}/html/ to {}/", OUTPUT_DIR, OUTPUT_DIR);
- // Move each entry in html_dir up one level
for entry in fs::read_dir(&html_dir).expect("Failed to read html directory") {
let entry = entry.expect("Failed to read entry");
let entry_path = entry.path();
@@ -49,7 +207,6 @@ fn main() {
.to_owned();
let dest_path = output_path.join(&file_name);
- // Remove existing file/directory at destination if any
if dest_path.exists() {
if dest_path.is_dir() {
fs::remove_dir_all(&dest_path).unwrap_or_else(|e| {
@@ -74,7 +231,6 @@ fn main() {
});
}
- // Remove the now-empty html directory
fs::remove_dir(&html_dir).unwrap_or_else(|e| {
eprintln!("Warning: could not remove html directory: {}", e);
});
@@ -82,12 +238,297 @@ fn main() {
println_cargo_style!("Files moved successfully.");
}
+ // 6. Recolor the per-file coverage summary with project-specific
+ // thresholds: 0-50% red, 51-80% yellow, 81-100% green. llvm-cov's
+ // built-in thresholds differ, and the color is assigned when the HTML
+ // is generated, so the summary table is rewritten here.
+ let index_path = output_path.join("index.html");
+ if let Err(e) = recolor_report_index(&index_path) {
+ eprintln_cargo_style!("Warning: failed to recolor {}: {}", index_path.display(), e);
+ }
+
println_cargo_style!(
"Done: coverage report generated at {}/index.html",
OUTPUT_DIR
);
}
+/// Remove `*.profraw` from the shared target dir so stale data from previous
+/// runs does not pollute the merged report. The instrumented build cache
+/// (everything else) is kept.
+fn clean_old_profraw(cov_target: &Path) {
+ if let Ok(entries) = fs::read_dir(cov_target) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.extension().is_some_and(|e| e == "profraw") {
+ let _ = fs::remove_file(&path);
+ }
+ }
+ }
+}
+
+/// All `mingling_core/tests/<crate>/Cargo.toml` manifests.
+fn find_test_crate_manifests(repo_root: &Path) -> Vec<PathBuf> {
+ let tests_dir = repo_root.join("mingling_core/tests");
+ let mut manifests = Vec::new();
+ if let Ok(entries) = fs::read_dir(&tests_dir) {
+ for entry in entries.flatten() {
+ let manifest = entry.path().join("Cargo.toml");
+ if manifest.is_file() {
+ manifests.push(manifest);
+ }
+ }
+ }
+ manifests.sort();
+ manifests
+}
+
+/// Parse every `examples/<name>/test.toml` into `(example_name, input)` pairs.
+fn load_example_commands(repo_root: &Path) -> Vec<(String, Vec<String>)> {
+ let examples_dir = repo_root.join("examples");
+ let mut entries: Vec<_> = std::fs::read_dir(&examples_dir)
+ .unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to read {}: {}", examples_dir.display(), e);
+ std::process::exit(1);
+ })
+ .flatten()
+ .collect();
+ entries.sort_by_key(|e| e.file_name());
+
+ let mut pairs = Vec::new();
+ for entry in entries {
+ let path = entry.path();
+ if !path.is_dir() {
+ continue;
+ }
+ let test_toml = path.join("test.toml");
+ if !test_toml.is_file() {
+ continue;
+ }
+ let name = path
+ .file_name()
+ .and_then(|n| n.to_str())
+ .unwrap_or_default()
+ .to_string();
+ let content = fs::read_to_string(&test_toml).unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to read {}: {}", test_toml.display(), e);
+ std::process::exit(1);
+ });
+ let config: TestConfig = toml::from_str(&content).unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to parse {}: {}", test_toml.display(), e);
+ std::process::exit(1);
+ });
+ for case in config.runs {
+ pairs.push((name.clone(), case.input));
+ }
+ }
+ pairs
+}
+
+/// Names of all workspace members, from `cargo metadata --no-deps`.
+fn workspace_member_names(repo_root: &Path) -> Vec<String> {
+ let Ok(output) = tools::run_cmd_capture_with_dir(
+ "cargo metadata --no-deps --format-version 1".to_string(),
+ repo_root,
+ ) 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["name"].as_str().map(str::to_owned))
+ .collect()
+}
+
+/// Collect the binaries of non-workspace crates (examples and test crates)
+/// from the shared target dir, as `--object <path>` arguments.
+///
+/// - `debug/` root: example binaries (built via `cargo llvm-cov run`).
+/// - `debug/deps/`: test crate binaries (e.g. `integration-<hash>`); their
+/// names do not follow a single pattern, so anything that is not a
+/// workspace-member binary and not a proc-macro `.so` is collected.
+///
+/// Workspace member binaries are detected automatically by `report` and must
+/// NOT be passed again (duplicate `-object` entries produce duplicated
+/// output). Hard links to the same file are deduplicated by inode.
+fn collect_object_args(cov_target: &Path, member_names: &[String]) -> String {
+ let debug_dir = cov_target.join("debug");
+ let mut objects = Vec::new();
+ let mut seen = std::collections::HashSet::new();
+
+ for dir in [debug_dir.clone(), debug_dir.join("deps")] {
+ let Ok(entries) = fs::read_dir(&dir) else {
+ continue;
+ };
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if !path.is_file() || !is_executable(&path) {
+ continue;
+ }
+ if !seen.insert(file_id(&path)) {
+ continue;
+ }
+ let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
+ continue;
+ };
+ // Proc-macro shared objects are either workspace members (picked
+ // up automatically) or external deps (excluded from the report
+ // by the ignore regex), so never pass them explicitly.
+ if name.starts_with("lib") && name.ends_with(".so") {
+ continue;
+ }
+ if is_workspace_member_binary(name, member_names) {
+ continue;
+ }
+ objects.push(path);
+ }
+ }
+
+ objects.sort();
+ objects
+ .iter()
+ .map(|p| format!("--object \"{}\"", p.to_string_lossy()))
+ .collect::<Vec<_>>()
+ .join(" ")
+}
+
+/// True if the binary name (e.g. `mingling_core-fea14a01b88afcaa`) belongs to
+/// a workspace member.
+fn is_workspace_member_binary(name: &str, member_names: &[String]) -> bool {
+ let stem = strip_cargo_hash(name);
+ member_names.iter().any(|m| stem == m)
+}
+
+/// Strip the cargo-generated hash suffix: `mingling_core-fea14a01b88afcaa` ->
+/// `mingling_core`. Returns the input unchanged if there is no such suffix.
+fn strip_cargo_hash(name: &str) -> &str {
+ let Some(idx) = name.rfind('-') else {
+ return name;
+ };
+ let (head, tail) = name.split_at(idx);
+ let hash = &tail[1..];
+ if hash.len() == 16 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
+ head
+ } else {
+ name
+ }
+}
+
+/// A stable identity for deduplicating hard links: device+inode on Unix,
+/// canonicalized path elsewhere.
+fn file_id(path: &Path) -> String {
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::MetadataExt as _;
+ if let Ok(metadata) = fs::metadata(path) {
+ return format!("{}:{}", metadata.dev(), metadata.ino());
+ }
+ }
+ fs::canonicalize(path)
+ .unwrap_or_else(|_| path.to_path_buf())
+ .to_string_lossy()
+ .into_owned()
+}
+
+/// Resolve binary filename for the given example.
+///
+/// The binary name matches the package name. On Windows, the `.exe` suffix is
+/// required.
+fn get_binary_name(example_name: &str) -> String {
+ let base = example_name;
+ if cfg!(target_os = "windows") {
+ format!("{base}.exe")
+ } else {
+ base.to_string()
+ }
+}
+
+/// Rewrite the per-file coverage colors in `index.html` with project-specific
+/// thresholds: 0-50% red, 51-80% yellow, 81-100% green.
+fn recolor_report_index(index_path: &Path) -> std::io::Result<()> {
+ let content = fs::read_to_string(index_path)?;
+ fs::write(index_path, recolor_coverage_table(&content))
+}
+
+/// Recolor every `<td class='column-entry-...'><pre>XX% ...</pre></td>` cell
+/// in the coverage summary table according to the new thresholds. Cells with
+/// no data (e.g. branch coverage `- (0/0)`, class `gray`) are left as-is.
+fn recolor_coverage_table(input: &str) -> String {
+ const TD: &str = "<td class='column-entry-";
+ let mut out = String::with_capacity(input.len());
+ let mut rest = input;
+ while let Some(pos) = rest.find(TD) {
+ out.push_str(&rest[..pos + TD.len()]);
+ rest = &rest[pos + TD.len()..];
+ let Some(pre_end) = rest.find("'><pre>") else {
+ out.push_str(rest);
+ return out;
+ };
+ let color = &rest[..pre_end];
+ let tail = &rest[pre_end + "'><pre>".len()..];
+ let pct: String = tail
+ .trim_start()
+ .chars()
+ .take_while(|c| c.is_ascii_digit() || *c == '.')
+ .collect();
+ let new_color = match pct.parse::<f64>() {
+ Ok(v) if v <= 50.0 => "red",
+ Ok(v) if v <= 80.0 => "yellow",
+ Ok(_) => "green",
+ Err(_) => color, // no data (e.g. gray branch column)
+ };
+ out.push_str(new_color);
+ out.push_str("'><pre>");
+ rest = tail;
+ }
+ out.push_str(rest);
+ out
+}
+
+/// True if the file is executable: mode bits on Unix, `.exe` on Windows.
+fn is_executable(path: &Path) -> bool {
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt as _;
+ let Ok(metadata) = std::fs::metadata(path) else {
+ return false;
+ };
+ metadata.permissions().mode() & 0o111 != 0
+ }
+ #[cfg(not(unix))]
+ {
+ path.extension()
+ .is_some_and(|e| e.eq_ignore_ascii_case("exe"))
+ }
+}
+
+/// Regex that keeps only the project's own sources in the report:
+/// excludes the shared llvm-cov target dir, the standard library, and
+/// external dependencies.
+fn build_ignore_regex(cov_target: &Path) -> String {
+ let target = regex_escape_path(cov_target);
+ format!(
+ "^{target}($|/)|/rustc/([0-9a-f]+|[0-9]+\\.[0-9]+\\.[0-9]+)/|/\\.cargo/(registry|git)/|/\\.rustup/toolchains($|/)"
+ )
+}
+
+/// Escape a path for use inside a regular expression (as a literal prefix).
+fn regex_escape_path(path: &Path) -> String {
+ let s = path.to_string_lossy().replace('\\', "/");
+ let mut escaped = String::with_capacity(s.len());
+ for ch in s.chars() {
+ if ch == '.' || ch == '-' {
+ escaped.push('\\');
+ }
+ escaped.push(ch);
+ }
+ escaped
+}
+
fn find_git_repo() -> Option<std::path::PathBuf> {
let mut current_dir = std::env::current_dir().ok()?;
@@ -104,3 +545,27 @@ fn find_git_repo() -> Option<std::path::PathBuf> {
None
}
+
+#[cfg(test)]
+mod tests {
+ use super::recolor_coverage_table;
+
+ #[test]
+ fn recolor_thresholds() {
+ let input = concat!(
+ "<td class='column-entry-red'><pre> 50.00% (2/4)</pre></td>",
+ "<td class='column-entry-yellow'><pre> 51.23% (32/52)</pre></td>",
+ "<td class='column-entry-red'><pre> 80.00% (48/89)</pre></td>",
+ "<td class='column-entry-green'><pre> 81.00% (1/1)</pre></td>",
+ "<td class='column-entry-yellow'><pre> 90.00% (6/7)</pre></td>",
+ "<td class='column-entry-gray'><pre>- (0/0)</pre></td>",
+ );
+ let out = recolor_coverage_table(input);
+ assert!(out.contains("class='column-entry-red'><pre> 50.00%"));
+ assert!(out.contains("class='column-entry-yellow'><pre> 51.23%"));
+ assert!(out.contains("class='column-entry-yellow'><pre> 80.00%"));
+ assert!(out.contains("class='column-entry-green'><pre> 81.00%"));
+ assert!(out.contains("class='column-entry-green'><pre> 90.00%"));
+ assert!(out.contains("class='column-entry-gray'><pre>- (0/0)"));
+ }
+}
diff --git a/.run/src/bin/deploy-api-docs.rs b/.run/src/bin/deploy-api-docs.rs
index ac4486e..961eb04 100644
--- a/.run/src/bin/deploy-api-docs.rs
+++ b/.run/src/bin/deploy-api-docs.rs
@@ -6,9 +6,8 @@ use tools::{println_cargo_style, run_cmd};
const OUTPUT_DIR: &str = "docs/api-docs";
fn main() {
- let (using_docsrs, open) = Picker::from_args()
+ let using_docsrs = Picker::from_args()
.pick_or_default(&arg![docsrs: bool])
- .pick_or_default(&arg![open: bool, 'O'])
.unwrap();
let repo_root = find_git_repo().expect("Failed to find git repository root");
@@ -28,15 +27,13 @@ fn main() {
// Build cargo doc command
let cmd = if using_docsrs {
format!(
- "cargo +nightly rustdoc {} --features \"{}\" -p mingling --target-dir \"{}\" --color always -- --cfg docsrs",
- if open { "--open" } else { "" },
+ "cargo +nightly rustdoc --features \"{}\" -p mingling --target-dir \"{}\" --color always -- --cfg docsrs",
features_arg,
output_path.join("target").to_string_lossy()
)
} else {
format!(
- "cargo doc --no-deps {} --features \"{}\" -p mingling --target-dir \"{}\" --color always",
- if open { "--open" } else { "" },
+ "cargo doc --no-deps --features \"{}\" -p mingling --target-dir \"{}\" --color always",
features_arg,
output_path.join("target").to_string_lossy()
)
diff --git a/.run/src/bin/doc-nightly.ps1 b/.run/src/bin/doc-nightly.ps1
deleted file mode 100644
index 3d8289e..0000000
--- a/.run/src/bin/doc-nightly.ps1
+++ /dev/null
@@ -1 +0,0 @@
-cargo dev_tool deploy-api-docs -- --docsrs --open
diff --git a/.run/src/bin/doc-nightly.sh b/.run/src/bin/doc-nightly.sh
deleted file mode 100644
index 10f9021..0000000
--- a/.run/src/bin/doc-nightly.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-cargo dev_tool deploy-api-docs -- --docsrs --open
diff --git a/.run/src/bin/doc.ps1 b/.run/src/bin/doc.ps1
deleted file mode 100644
index 7c3f222..0000000
--- a/.run/src/bin/doc.ps1
+++ /dev/null
@@ -1 +0,0 @@
-cargo dev_tool deploy-api-docs -- --open
diff --git a/.run/src/bin/doc.sh b/.run/src/bin/doc.sh
deleted file mode 100755
index 8c809b7..0000000
--- a/.run/src/bin/doc.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-cargo dev_tool deploy-api-docs -- --open
diff --git a/.run/src/bin/docs-code-box-fix.rs b/.run/src/bin/docs-code-box-fix.rs
deleted file mode 100644
index 21d2cce..0000000
--- a/.run/src/bin/docs-code-box-fix.rs
+++ /dev/null
@@ -1,166 +0,0 @@
-use std::fs;
-use std::path::Path;
-
-use tools::println_cargo_style;
-
-/// Docsify code blocks require that blank lines before and after code blocks are not completely empty,
-/// but must contain at least one space, otherwise code block rendering will have issues.
-///
-/// This tool scans all `.md` files in the docs directory,
-/// and replaces completely empty lines before and after code blocks with blank lines containing a single space.
-const DOCS_DIR: &str = "./docs";
-
-fn main() {
- println_cargo_style!("Fixing: code box empty lines in docs/**/*.md ...");
- let repo_root = find_git_repo().expect("Cannot find git repo root");
- let docs_dir = repo_root.join(DOCS_DIR);
-
- let mut fixed_count = 0;
- let mut file_count = 0;
-
- collect_md_files(&docs_dir, &mut |path| {
- if let Some(name) = path.file_name() {
- let name = name.to_string_lossy();
- if name.to_lowercase() == "_sidebar.md" {
- return;
- }
- }
-
- let content = fs::read_to_string(path).unwrap_or_default();
- if content.is_empty() {
- return;
- }
-
- let new_content = fix_code_box_empty_lines(&content);
- if new_content != content {
- fs::write(path, &new_content).unwrap();
- println_cargo_style!("Fixed: {}", path.display());
- fixed_count += 1;
- }
- file_count += 1;
- });
-
- println_cargo_style!(
- "Done: Scanned {} files, fixed {} files.",
- file_count,
- fixed_count
- );
-}
-
-fn fix_code_box_empty_lines(content: &str) -> String {
- let mut result = String::new();
- let lines: Vec<&str> = content.lines().collect();
- let len = lines.len();
-
- let mut i = 0;
- while i < len {
- let line = lines[i];
-
- // detect beginning of code block: beginning with ```
- if line.trim_start().starts_with("```") {
- // record the beginning line of the code block
- result.push_str(line);
- result.push('\n');
- i += 1;
-
- // find the end of the code block
- let mut found_end = false;
- let code_start = i; // record starting position of code content
- let mut code_end = len; // index of code block end line
-
- while i < len {
- let cline = lines[i];
- if cline.trim_start().starts_with("```") && cline.trim() != "" {
- // this is the closing marker
- code_end = i;
- found_end = true;
- break;
- }
- i += 1;
- }
-
- // check the blank line before the code block
- // if result ends with \n\n, add a space to turn it into \n \n
- ensure_space_before_code_block(&mut result);
-
- // output code content
- for code_line in lines.iter().take(code_end).skip(code_start) {
- if code_line.is_empty() {
- result.push(' ');
- } else {
- result.push_str(code_line);
- }
- result.push('\n');
- }
-
- if found_end {
- result.push_str(lines[code_end]);
- result.push('\n');
- i += 1;
-
- // check the blank line after the code block
- // if the next line is blank, change it to one with a space
- if i < len && lines[i].trim().is_empty() && lines[i].is_empty() {
- // skip the original blank line, write " \n"
- result.push(' ');
- result.push('\n');
- i += 1;
- }
- }
- } else {
- result.push_str(line);
- result.push('\n');
- i += 1;
- }
- }
-
- // remove trailing newlines
- while result.ends_with('\n') {
- result.pop();
- }
- result.push('\n');
-
- result
-}
-
-/// ensure there is a blank line with a space before the code block
-fn ensure_space_before_code_block(result: &mut String) {
- // if result ends with \n\n,
- // turn it into \n \n
- let len = result.len();
- if len >= 2 && result[len - 2..] == *"\n\n" {
- // insert a space before the last \n
- result.insert(len - 1, ' ');
- }
-}
-
-/// recursively collect all .md files in the docs directory
-fn collect_md_files(dir: &Path, callback: &mut dyn FnMut(&Path)) {
- if let Ok(entries) = fs::read_dir(dir) {
- for entry in entries.flatten() {
- let path = entry.path();
- if path.is_dir() {
- collect_md_files(&path, callback);
- } else if path.extension().is_some_and(|ext| ext == "md") {
- callback(&path);
- }
- }
- }
-}
-
-fn find_git_repo() -> Option<std::path::PathBuf> {
- let mut current_dir = std::env::current_dir().ok()?;
-
- loop {
- let git_dir = current_dir.join(".git");
- if git_dir.exists() && git_dir.is_dir() {
- return Some(current_dir);
- }
-
- if !current_dir.pop() {
- break;
- }
- }
-
- None
-}
diff --git a/.run/src/bin/docsify-sidebar-gen.rs b/.run/src/bin/docsify-sidebar-gen.rs
deleted file mode 100644
index 5beda7f..0000000
--- a/.run/src/bin/docsify-sidebar-gen.rs
+++ /dev/null
@@ -1,262 +0,0 @@
-use std::collections::BTreeMap;
-use std::fmt::Write;
-use std::path::{Path, PathBuf};
-
-use tools::println_cargo_style;
-
-const SIDEBAR_HEAD: &str = "- [Welcome!](README)\n";
-
-fn main() {
- println_cargo_style!("Refresh: _sidebar.md");
- gen_all_sidebars();
-}
-
-/// Find all README.md under docs/, treat each as a site, and generate _sidebar.md for it.
-fn gen_all_sidebars() {
- let repo_root = find_git_repo().unwrap();
- let docs_root = repo_root.join("docs");
-
- let readme_paths = find_all_readmes(&docs_root);
-
- for readme_path in &readme_paths {
- let site_root = readme_path.parent().unwrap();
-
- let content_dir = find_content_dir(site_root);
-
- if let Some(content_dir) = content_dir {
- let lines = build_sidebar_content(site_root, &content_dir, SIDEBAR_HEAD);
-
- let sidebar_path = site_root.join("_sidebar.md");
- std::fs::write(&sidebar_path, lines).unwrap();
- println_cargo_style!("Generated: {}", sidebar_path.display());
- }
- }
-}
-
-/// Recursively find all README.md files under a directory.
-fn find_all_readmes(dir: &Path) -> Vec<PathBuf> {
- let mut results = Vec::new();
- if let Ok(read_dir) = std::fs::read_dir(dir) {
- let mut entries: Vec<_> = read_dir.flatten().collect();
- entries.sort_by_key(|e| e.path());
- for entry in entries {
- let path = entry.path();
- if path.is_dir() {
- results.extend(find_all_readmes(&path));
- } else if path.file_name().is_some_and(|n| n == "README.md") {
- results.push(path);
- }
- }
- }
- results
-}
-
-/// Find the content directory for a site:
-/// 1. Prefer `pages/` if it exists (backward compatible)
-/// 2. Fall back to the first subdirectory that contains .md files
-fn find_content_dir(site_root: &Path) -> Option<PathBuf> {
- // Try pages/ first
- let pages_dir = site_root.join("pages");
- if pages_dir.exists() && pages_dir.is_dir() {
- return Some(pages_dir);
- }
-
- // Fall back to any subdirectory containing .md files
- if let Ok(read_dir) = std::fs::read_dir(site_root) {
- let mut entries: Vec<_> = read_dir.flatten().collect();
- entries.sort_by_key(|e| e.path());
- for entry in entries {
- let path = entry.path();
- if path.is_dir()
- && has_markdown_files(&path) {
- return Some(path);
- }
- }
- }
-
- None
-}
-
-/// Check if a directory (recursively) contains any .md files.
-fn has_markdown_files(dir: &Path) -> bool {
- if let Ok(read_dir) = std::fs::read_dir(dir) {
- for entry in read_dir.flatten() {
- let path = entry.path();
- if path.is_dir() {
- if has_markdown_files(&path) {
- return true;
- }
- } else if path.extension().is_some_and(|ext| ext == "md") {
- return true;
- }
- }
- }
- false
-}
-
-/// Build sidebar content: scan .md files in `pages_dir` and return a formatted sidebar string
-fn build_sidebar_content(base_dir: &Path, pages_dir: &Path, sidebar_head: &str) -> String {
- let mut lines = String::from(sidebar_head);
-
- // Collect and sort entries at root level first
- let mut root_files: Vec<SidebarEntry> = Vec::new();
- // Subdirectory name -> its files
- let mut sub_dirs: BTreeMap<String, Vec<SidebarEntry>> = BTreeMap::new();
-
- if let Ok(read_dir) = std::fs::read_dir(pages_dir) {
- for entry in read_dir.flatten() {
- let path = entry.path();
- if path.is_dir() {
- let dir_name = entry.file_name().to_string_lossy().to_string();
- let entries = collect_markdown_files(&path, base_dir);
- if !entries.is_empty() {
- // Check for .name file to override directory display name
- let display_name = get_directory_display_name(&path, &dir_name);
- sub_dirs.insert(display_name, entries);
- }
- } else if path.extension().is_some_and(|ext| ext == "md") {
- let title = extract_title(&path);
- let relative = path
- .strip_prefix(base_dir)
- .unwrap()
- .to_string_lossy()
- .replace('\\', "/");
- let link = relative
- .strip_suffix(".md")
- .unwrap_or(&relative)
- .to_string();
- root_files.push(SidebarEntry { title, link });
- }
- }
- }
-
- // Sort root files — natural order (1, 2, ..., 10, 11)
- root_files.sort_by(|a, b| natural_cmp(&a.link, &b.link));
-
- // Append root-level files
- for f in &root_files {
- let _ = writeln!(lines, "* [{}]({})", f.title, f.link);
- }
-
- // Append subdirectory groups
- for (dir_name, entries) in &sub_dirs {
- let mut sorted_entries = entries.clone();
- sorted_entries.sort_by(|a, b| natural_cmp(&a.link, &b.link));
-
- // Directory header with 2-space indent
- let _ = writeln!(lines, "* {dir_name}");
- for f in &sorted_entries {
- let _ = writeln!(lines, " * [{}]({})", f.title, f.link);
- }
- }
-
- lines
-}
-
-#[derive(Clone)]
-struct SidebarEntry {
- title: String,
- link: String,
-}
-
-/// Collect all `.md` files directly under `dir`
-fn collect_markdown_files(dir: &Path, base_dir: &Path) -> Vec<SidebarEntry> {
- let mut entries = Vec::new();
-
- if let Ok(read_dir) = std::fs::read_dir(dir) {
- for entry in read_dir.flatten() {
- let path = entry.path();
- if path.extension().is_some_and(|ext| ext == "md") {
- let title = extract_title(&path);
- let relative = path
- .strip_prefix(base_dir)
- .unwrap()
- .to_string_lossy()
- .replace('\\', "/");
- let link = relative
- .strip_suffix(".md")
- .unwrap_or(&relative)
- .to_string();
- entries.push(SidebarEntry { title, link });
- }
- }
- }
-
- entries
-}
-
-/// Extract title from the first line `<h1 align="center">TITLE</h1>`.
-/// Fallback to filename stem.
-fn extract_title(path: &Path) -> String {
- let content = std::fs::read_to_string(path).unwrap_or_default();
- if let Some(first_line) = content.lines().next() {
- let trimmed = first_line.trim();
- // Find `>TITLE<` between `<h1 align="center">` and `</h1>`
- if let Some(start) = trimmed.find('>') {
- let after_start = &trimmed[start + 1..];
- if let Some(end) = after_start.find('<') {
- return after_start[..end].to_string();
- }
- }
- }
- // Fallback: use file stem
- path.file_stem().map_or_else(
- || "Untitled".to_string(),
- |s| s.to_string_lossy().to_string(),
- )
-}
-
-/// Read `.name` file inside a directory to get its display name for the sidebar.
-/// Falls back to the directory name itself if no `.name` file exists.
-fn get_directory_display_name(dir_path: &std::path::Path, fallback: &str) -> String {
- let name_file = dir_path.join(".name");
- if name_file.exists() && name_file.is_file() {
- std::fs::read_to_string(&name_file)
- .ok()
- .map(|s| s.trim().to_string())
- .filter(|s| !s.is_empty())
- .unwrap_or_else(|| fallback.to_string())
- } else {
- fallback.to_string()
- }
-}
-
-fn find_git_repo() -> Option<std::path::PathBuf> {
- let mut current_dir = std::env::current_dir().ok()?;
-
- loop {
- let git_dir = current_dir.join(".git");
- if git_dir.exists() && git_dir.is_dir() {
- return Some(current_dir);
- }
-
- if !current_dir.pop() {
- break;
- }
- }
-
- None
-}
-
-/// Natural (numeric-aware) comparison for sidebar links.
-///
-/// Files prefixed with a number (e.g. `1-getting-started`) are sorted by that number;
-/// files without a numeric prefix fall back to lexicographic order (after numbers).
-fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering {
- let num_a = extract_leading_number(a);
- let num_b = extract_leading_number(b);
- num_a.cmp(&num_b).then_with(|| a.cmp(b))
-}
-
-/// Extract the leading numeric prefix from a sidebar link path.
-///
-/// Looks at the filename stem (after the last `/`) for a number before the first `-`.
-/// Returns `usize::MAX` for entries without a numeric prefix.
-fn extract_leading_number(link: &str) -> usize {
- if let Some(file_stem) = link.rsplit('/').next()
- && let Some(num_end) = file_stem.find('-')
- && let Ok(num) = file_stem[..num_end].parse::<usize>() {
- return num;
- }
- usize::MAX
-}
diff --git a/.run/src/bin/install-mling.ps1 b/.run/src/bin/install-mling.ps1
index bebe9ff..2b55a09 100644
--- a/.run/src/bin/install-mling.ps1
+++ b/.run/src/bin/install-mling.ps1
@@ -1,7 +1,10 @@
-cargo install --path mling
+$ErrorActionPreference = "Stop"
-New-Item -ItemType Directory -Force -Path .temp/comp | Out-Null
-# Copy all files containing _comp from the debug directory
-Get-ChildItem .temp/target/release/*_comp* | ForEach-Object {
- Copy-Item $_.FullName .temp/comp/
-}
+cargo build --release --manifest-path mingling_cli/Cargo.toml
+
+New-Item -ItemType Directory -Force -Path .temp/mling/bin, .temp/mling/scripts | Out-Null
+
+Copy-Item .temp/target/release/mling.exe .temp/mling/bin/
+Copy-Item .temp/target/release/mingling-cli.exe .temp/mling/bin/
+Copy-Item .temp/target/mingling/mling_comp.ps1 .temp/mling/scripts/mling_comp.ps1
+Copy-Item mingling_cli/scripts/load_mling.ps1 .temp/mling/
diff --git a/.run/src/bin/install-mling.sh b/.run/src/bin/install-mling.sh
index 5f2ee7a..e8cfa18 100644..100755
--- a/.run/src/bin/install-mling.sh
+++ b/.run/src/bin/install-mling.sh
@@ -1,6 +1,17 @@
#!/bin/bash
-cargo install --path mling
+set -e
-mkdir -p .temp/comp
-cp .temp/target/release/*_comp.* .temp/comp/ 2>/dev/null || echo "No matching files found"
+cargo build --release --manifest-path mingling_cli/Cargo.toml
+
+mkdir -p .temp/mling/bin .temp/mling/scripts
+
+cp .temp/target/release/mling .temp/mling/bin/
+cp .temp/target/release/mingling-cli .temp/mling/bin/
+
+for comp in zsh sh fish; do
+ cp ".temp/target/mingling/mling_comp.$comp" ".temp/mling/scripts/mling_comp.$comp"
+done
+cp mingling_cli/scripts/load_mling.zsh .temp/mling/
+cp mingling_cli/scripts/load_mling.sh .temp/mling/
+cp mingling_cli/scripts/load_mling.fish .temp/mling/
diff --git a/.run/src/bin/package-all.rs b/.run/src/bin/package-all.rs
index 5d7cbbb..ecdd133 100644
--- a/.run/src/bin/package-all.rs
+++ b/.run/src/bin/package-all.rs
@@ -1,8 +1,10 @@
+use std::collections::HashMap;
use std::path::{Path, PathBuf};
use flate2::read::GzDecoder;
use serde::Deserialize;
use tar::Archive;
+use toml::Table as TomlTable;
use tools::{
dependency_order::find_workspace_root, eprintln_cargo_style, println_cargo_style,
run_cmd_capture_with_dir, wprintln_cargo_style,
@@ -76,14 +78,12 @@ fn main() {
}
// Build version map: crate_name -> version
- let mut version_map: std::collections::HashMap<String, String> =
- std::collections::HashMap::new();
+ let mut version_map: HashMap<String, String> = HashMap::new();
for m in &members {
version_map.insert(m.name.clone(), m.version.clone());
}
// Collect unique member directories that need to be copied
- // Compute relative path by stripping workspace_root prefix
let mut member_dirs: Vec<PathBuf> = Vec::new();
for m in &members {
let dir = Path::new(&m.manifest_path)
@@ -104,20 +104,17 @@ fn main() {
// 4. Copy files to the temp directory, preserving the workspace directory structure
println_cargo_style!("Copy: project structure to .temp/pre-release/");
- // Copy .cargo directory
copy_dir(
&workspace_root.join(".cargo"),
&pre_release_dir.join(".cargo"),
);
- // Copy each member directory
for dir in &member_dirs {
let src = workspace_root.join(dir);
let dst = pre_release_dir.join(dir);
copy_dir(&src, &dst);
}
- // Copy root Cargo.toml and Cargo.lock
copy_file(
&workspace_root.join("Cargo.toml"),
&pre_release_dir.join("Cargo.toml"),
@@ -127,24 +124,20 @@ fn main() {
&pre_release_dir.join("Cargo.lock"),
);
- // 5. Replace workspace dependency paths with version numbers in the root Cargo.toml
- // For workspace-member crates, keep path so cargo can resolve locally during
- // `cargo package --workspace`. `cargo package` automatically converts path deps
- // to version deps in the final .crate manifest.
- println_cargo_style!("Patch: resolve workspace dependency versions");
+ // 5. Fully resolve ALL workspace inheritance in every member's Cargo.toml,
+ // so each crate becomes monomorphic (no `workspace = true` references).
+ // Then strip `[workspace.dependencies]` and `[workspace.package]` from the
+ // root Cargo.toml, since they are no longer needed.
+ println_cargo_style!("Resolve: inline all workspace inheritance");
- let pre_release_cargo = pre_release_dir.join("Cargo.toml");
- let content = std::fs::read_to_string(&pre_release_cargo)
- .unwrap_or_else(|e| panic!("failed to read {}: {e}", pre_release_cargo.display()));
+ // Parse workspace config from the COPIED root Cargo.toml
+ let root_cargo_path = pre_release_dir.join("Cargo.toml");
+ let root_content = std::fs::read_to_string(&root_cargo_path)
+ .unwrap_or_else(|e| panic!("failed to read {}: {e}", root_cargo_path.display()));
- let patched = patch_workspace_deps(&content, &version_map);
+ let (ws_package, ws_deps) = parse_workspace_config(&root_content);
- std::fs::write(&pre_release_cargo, &patched)
- .unwrap_or_else(|e| panic!("failed to write {}: {e}", pre_release_cargo.display()));
-
- // Member cargo.toml files: replace direct `path = "..."` deps (pointing to other
- // workspace members) with version qualification, so `cargo package` can produce
- // a valid .crate without path dependencies.
+ // Resolve each member's Cargo.toml
for dir in &member_dirs {
let member_cargo = pre_release_dir.join(dir).join("Cargo.toml");
if !member_cargo.exists() {
@@ -152,13 +145,18 @@ fn main() {
}
let member_content = std::fs::read_to_string(&member_cargo)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", member_cargo.display()));
- let member_patched = patch_member_path_deps(&member_content, &version_map);
- if member_patched != member_content {
- std::fs::write(&member_cargo, &member_patched)
- .unwrap_or_else(|e| panic!("failed to write {}: {e}", member_cargo.display()));
- }
+ let resolved =
+ resolve_member_manifest(&member_content, dir, &ws_package, &ws_deps, &version_map);
+ std::fs::write(&member_cargo, &resolved)
+ .unwrap_or_else(|e| panic!("failed to write {}: {e}", member_cargo.display()));
}
+ // Strip [workspace.dependencies], [workspace.package], and root [package]
+ // from root Cargo.toml, making it a pure virtual manifest.
+ let stripped = strip_workspace_config(&root_content);
+ std::fs::write(&root_cargo_path, &stripped)
+ .unwrap_or_else(|e| panic!("failed to write {}: {e}", root_cargo_path.display()));
+
println_cargo_style!("Package: running cargo package --workspace --no-verify");
// 6. Run cargo package in the temp directory
@@ -274,6 +272,22 @@ fn main() {
// Also remove Cargo.lock — standalone crate doesn't need it for publish
let _ = std::fs::remove_file(target_dir.join("Cargo.lock"));
+ // Append an empty [workspace] section so each crate is a valid workspace root
+ let cargo_toml_path = target_dir.join("Cargo.toml");
+ if cargo_toml_path.exists() {
+ let mut cargo_content = std::fs::read_to_string(&cargo_toml_path)
+ .unwrap_or_else(|e| panic!("failed to read {}: {e}", cargo_toml_path.display()));
+ // Only append if there isn't already a [workspace] section
+ if !cargo_content.contains("\n[workspace]\n")
+ && !cargo_content.ends_with("\n[workspace]\n")
+ {
+ cargo_content.push_str("\n[workspace]\n");
+ std::fs::write(&cargo_toml_path, &cargo_content).unwrap_or_else(|e| {
+ panic!("failed to write {}: {e}", cargo_toml_path.display())
+ });
+ }
+ }
+
println_cargo_style!("Export: {}", crate_dir_name);
}
@@ -286,57 +300,289 @@ fn main() {
}
}
-/// Replace path-based workspace dependencies with version strings.
-///
-/// Keeps the path form so that `cargo package --workspace` resolves to local workspace
-/// members, but also adds `version = "..."` so the generated .crate has the correct
-/// version dependency.
-fn patch_workspace_deps(
+/// Parse `[workspace.package]` and `[workspace.dependencies]` from the root Cargo.toml.
+/// Returns (package_fields, dep_values).
+fn parse_workspace_config(
+ content: &str,
+) -> (HashMap<String, String>, HashMap<String, toml::Value>) {
+ let table: TomlTable = content.parse().expect("failed to parse root Cargo.toml");
+
+ let mut package_fields = HashMap::new();
+ if let Some(workspace) = table.get("workspace").and_then(|w| w.as_table())
+ && let Some(pkg_table) = workspace.get("package").and_then(|p| p.as_table())
+ {
+ for (k, v) in pkg_table {
+ if let Some(s) = v.as_str() {
+ package_fields.insert(k.clone(), s.to_string());
+ }
+ }
+ }
+
+ let mut dep_values = HashMap::new();
+ if let Some(workspace) = table.get("workspace").and_then(|w| w.as_table())
+ && let Some(deps_table) = workspace.get("dependencies").and_then(|d| d.as_table())
+ {
+ for (k, v) in deps_table {
+ dep_values.insert(k.clone(), v.clone());
+ }
+ }
+
+ (package_fields, dep_values)
+}
+
+/// Serialize a `toml::Value` into Cargo-toml-compatible inline representation.
+fn toml_value_str(v: &toml::Value) -> String {
+ match v {
+ toml::Value::String(s) => format!("\"{}\"", s),
+ toml::Value::Table(t) => {
+ let items: Vec<String> = t
+ .iter()
+ .map(|(k, val)| format!("{} = {}", k, toml_value_str(val)))
+ .collect();
+ format!("{{ {} }}", items.join(", "))
+ }
+ toml::Value::Array(a) => {
+ let items: Vec<String> = a.iter().map(toml_value_str).collect();
+ format!("[{}]", items.join(", "))
+ }
+ toml::Value::Boolean(b) => b.to_string(),
+ toml::Value::Integer(i) => i.to_string(),
+ toml::Value::Float(f) => f.to_string(),
+ toml::Value::Datetime(dt) => format!("\"{}\"", dt),
+ }
+}
+
+/// Compute the relative path from `member_rel_dir` to `target_path`.
+/// Both are relative to workspace root.
+/// e.g. member_rel_dir="mingling", target_path="mingling_core" → "../mingling_core"
+fn make_path_relative_to_member(target_path: &str, member_rel_dir: &Path) -> String {
+ if member_rel_dir.as_os_str().is_empty() || member_rel_dir == Path::new(".") {
+ return target_path.to_string();
+ }
+ let depth = member_rel_dir.components().count();
+ let mut result = PathBuf::new();
+ for _ in 0..depth {
+ result.push("..");
+ }
+ result.push(target_path);
+ result.to_string_lossy().to_string()
+}
+
+/// Resolve a dependency definition from `[workspace.dependencies]` to an inline string.
+/// If the definition contains a path to a workspace member, add `version = "..."`
+/// and adjust the path to be relative to the member's directory.
+fn resolve_dep_def(
+ dep_name: &str,
+ dep_def: &toml::Value,
+ member_rel_dir: &Path,
+ version_map: &HashMap<String, String>,
+) -> String {
+ match dep_def {
+ toml::Value::String(ver) => {
+ format!("\"{}\"", ver)
+ }
+ toml::Value::Table(t) => {
+ let mut resolved = t.clone();
+ let has_path = t.contains_key("path");
+ let is_ws_member = version_map.contains_key(dep_name);
+
+ // Fix path to be relative to member's directory
+ if has_path && let Some(path_val) = t.get("path").and_then(|v| v.as_str()) {
+ let rel = make_path_relative_to_member(path_val, member_rel_dir);
+ resolved.insert("path".to_string(), toml::Value::String(rel));
+ }
+
+ // Add version for workspace member path deps
+ if has_path
+ && is_ws_member
+ && let Some(version) = version_map.get(dep_name)
+ {
+ resolved.insert("version".to_string(), toml::Value::String(version.clone()));
+ }
+
+ let items: Vec<String> = resolved
+ .iter()
+ .map(|(k, val)| format!("{} = {}", k, toml_value_str(val)))
+ .collect();
+ format!("{{ {} }}", items.join(", "))
+ }
+ _ => toml_value_str(dep_def),
+ }
+}
+
+/// Merge an inline `{ workspace = true, optional = true, ... }` with the workspace definition.
+/// Returns the full resolved dependency value string (without the leading `dep_name = `).
+fn merge_inline_dep(
+ inline_rest: &str,
+ dep_name: &str,
+ dep_def: &toml::Value,
+ member_rel_dir: &Path,
+ version_map: &HashMap<String, String>,
+) -> String {
+ // inline_rest is the part after `=`: `{ workspace = true, optional = true }`
+ let inner = inline_rest
+ .trim()
+ .strip_prefix('{')
+ .and_then(|s| s.strip_suffix('}'))
+ .unwrap_or("");
+
+ match dep_def {
+ toml::Value::String(ver) => {
+ // Workspace def is just a version string
+ // Collect extras: everything except `workspace = true`
+ let extras: Vec<&str> = inner
+ .split(',')
+ .map(|s| s.trim())
+ .filter(|s| !s.is_empty() && *s != "workspace = true")
+ .collect();
+
+ if extras.is_empty() {
+ format!("\"{}\"", ver)
+ } else {
+ // Serialize as inline table: version + extras
+ let mut parts = vec![format!("version = \"{}\"", ver)];
+ parts.extend(extras.iter().map(|s| s.to_string()));
+ format!("{{ {} }}", parts.join(", "))
+ }
+ }
+ toml::Value::Table(t) => {
+ // Start from workspace def
+ let mut merged = t.clone();
+
+ // Fix path to be relative to member's directory
+ if let Some(path_val) = t.get("path").and_then(|v| v.as_str()) {
+ let rel = make_path_relative_to_member(path_val, member_rel_dir);
+ merged.insert("path".to_string(), toml::Value::String(rel));
+ }
+
+ // If this dep is a workspace member with a path dep, add version
+ if t.contains_key("path")
+ && version_map.contains_key(dep_name)
+ && let Some(version) = version_map.get(dep_name)
+ {
+ merged.insert("version".to_string(), toml::Value::String(version.clone()));
+ }
+
+ // Apply extra fields from the inline
+ for piece in inner.split(',').map(|s| s.trim()) {
+ let piece = piece.trim();
+ if piece.is_empty() || piece == "workspace = true" {
+ continue;
+ }
+ // Parse `key = value` pairs
+ if let Some((raw_key, raw_val)) = piece.split_once('=') {
+ let k = raw_key.trim();
+ let v = raw_val.trim();
+ if k.is_empty() {
+ continue;
+ }
+ // Try to infer the value type
+ if v == "true" {
+ merged.insert(k.to_string(), toml::Value::Boolean(true));
+ } else if v == "false" {
+ merged.insert(k.to_string(), toml::Value::Boolean(false));
+ } else if v.starts_with('"') && v.ends_with('"') {
+ merged.insert(
+ k.to_string(),
+ toml::Value::String(v[1..v.len() - 1].to_string()),
+ );
+ } else if v.starts_with('[') && v.ends_with(']') {
+ // Simple array parsing: strings only
+ let arr: Vec<toml::Value> = v[1..v.len() - 1]
+ .split(',')
+ .map(|s| {
+ let s = s.trim().trim_matches('"');
+ toml::Value::String(s.to_string())
+ })
+ .collect();
+ merged.insert(k.to_string(), toml::Value::Array(arr));
+ } else if let Ok(n) = v.parse::<i64>() {
+ merged.insert(k.to_string(), toml::Value::Integer(n));
+ } else if let Ok(f) = v.parse::<f64>() {
+ merged.insert(k.to_string(), toml::Value::Float(f));
+ } else {
+ // Treat as string
+ merged.insert(k.to_string(), toml::Value::String(v.to_string()));
+ }
+ }
+ }
+
+ let items: Vec<String> = merged
+ .iter()
+ .map(|(k, val)| format!("{} = {}", k, toml_value_str(val)))
+ .collect();
+ format!("{{ {} }}", items.join(", "))
+ }
+ _ => toml_value_str(dep_def),
+ }
+}
+
+/// Resolve ALL workspace inheritance in a single member crate's Cargo.toml:
+/// - `version.workspace = true` → `version = "0.3.0"`
+/// - `dep.workspace = true` → inline the full definition from ws_deps
+/// - `dep = { workspace = true, ... }` → merge with ws_deps definition
+fn resolve_member_manifest(
content: &str,
- version_map: &std::collections::HashMap<String, String>,
+ member_rel_dir: &Path,
+ ws_package: &HashMap<String, String>,
+ ws_deps: &HashMap<String, toml::Value>,
+ version_map: &HashMap<String, String>,
) -> String {
let mut result = String::new();
- let mut in_workspace_deps = false;
+ let mut in_package = false;
+ let mut in_section_with_deps = false;
for line in content.lines() {
let trimmed = line.trim();
-
- if trimmed == "[workspace.dependencies]" {
- in_workspace_deps = true;
+ let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
+
+ // Track sections
+ if trimmed.starts_with('[') {
+ in_package = trimmed == "[package]";
+ in_section_with_deps = trimmed.starts_with("[dependencies")
+ || trimmed.starts_with("[build-dependencies")
+ || trimmed.starts_with("[dev-dependencies");
result.push_str(line);
result.push('\n');
continue;
}
- // Detect end of workspace.dependencies section
- if in_workspace_deps && trimmed.starts_with('[') {
- in_workspace_deps = false;
+ // [package] section: resolve `field.workspace = true`
+ if in_package && trimmed.ends_with(".workspace = true") {
+ let key = trimmed.strip_suffix(".workspace = true").unwrap().trim();
+ if let Some(value) = ws_package.get(key) {
+ result.push_str(&format!("{indent}{key} = \"{value}\"\n"));
+ continue;
+ }
+ // Also check workspace.dependencies (for fields like `version.workspace`)
+ // when the member has its own version field inherited from workspace.package
}
- if in_workspace_deps
- && let Some(dep_name) = trimmed.split('=').next().map(|s| s.trim())
- && let Some(version) = version_map.get(dep_name)
- {
- let indent = line
- .chars()
- .take_while(|c| c.is_whitespace())
- .collect::<String>();
-
- if trimmed.contains("path =") {
- let path_value = extract_path_value(trimmed);
- let patched_line: String = if let Some(pv) = path_value {
- trimmed.replace(
- &format!("path = \"{}\"", pv),
- &format!("path = \"{}\", version = \"{}\"", pv, version),
- )
- } else {
- trimmed.to_string()
- };
- result.push_str(&format!("{indent}{patched_line}\n"));
- } else {
- result.push_str(&format!("{indent}{dep_name} = \"{version}\"\n"));
+ // Dependency sections
+ if in_section_with_deps {
+ // Shorthand: `foo.workspace = true`
+ if trimmed.ends_with(".workspace = true") {
+ let key = trimmed.strip_suffix(".workspace = true").unwrap().trim();
+ if let Some(dep_def) = ws_deps.get(key) {
+ let resolved = resolve_dep_def(key, dep_def, member_rel_dir, version_map);
+ result.push_str(&format!("{indent}{key} = {resolved}\n"));
+ continue;
+ }
+ }
+
+ // Inline: `foo = { workspace = true, optional = true, ... }`
+ if let Some(eq_pos) = trimmed.find("= {")
+ && trimmed.contains("workspace = true")
+ {
+ let dep_name = trimmed[..eq_pos].trim();
+ if let Some(dep_def) = ws_deps.get(dep_name) {
+ let after_eq = trimmed[eq_pos + 1..].trim();
+ let merged =
+ merge_inline_dep(after_eq, dep_name, dep_def, member_rel_dir, version_map);
+ result.push_str(&format!("{indent}{dep_name} = {merged}\n"));
+ continue;
+ }
}
- continue;
}
result.push_str(line);
@@ -346,62 +592,55 @@ fn patch_workspace_deps(
result
}
-/// Extract the path value from a dependency line like:
-/// `mingling_core = { path = "mingling_core", default-features = false }`
-fn extract_path_value(line: &str) -> Option<String> {
- let line = line.trim();
- if let Some(start) = line.find("path = \"") {
- let after_path = &line[start + 8..];
- if let Some(end) = after_path.find('"') {
- return Some(after_path[..end].to_string());
- }
- }
- None
-}
-
-/// Patch a member crate's Cargo.toml: add `version = "..."` to direct `path = "..."`
-/// dependencies that point to other workspace members.
-fn patch_member_path_deps(
- content: &str,
- version_map: &std::collections::HashMap<String, String>,
-) -> String {
+/// Remove `[workspace.dependencies]`, `[workspace.package]`, and the root `[package]`
+/// section from root Cargo.toml, making it a pure virtual manifest.
+/// Keeps `[workspace]` with `members`, `resolver`, `exclude` so packaging still works.
+fn strip_workspace_config(content: &str) -> String {
let mut result = String::new();
- let mut in_deps = false;
- let mut in_build_deps = false;
+ let mut in_ws_deps = false;
+ let mut in_ws_package = false;
+ let mut in_root_package = false;
for line in content.lines() {
let trimmed = line.trim();
- if trimmed == "[dependencies]" || trimmed.starts_with("[dependencies.") {
- in_deps = true;
- in_build_deps = false;
- } else if trimmed == "[build-dependencies]" || trimmed.starts_with("[build-dependencies.") {
- in_build_deps = true;
- in_deps = false;
- } else if trimmed.starts_with('[') {
- in_deps = false;
- in_build_deps = false;
+ if trimmed == "[workspace.dependencies]" {
+ in_ws_deps = true;
+ continue;
}
-
- if (in_deps || in_build_deps)
- && trimmed.contains("path = \"")
- && !trimmed.contains("workspace = true")
- && let Some(dep_name) = trimmed.split('=').next().map(|s| s.trim())
- && let Some(version) = version_map.get(dep_name.trim_end_matches(".workspace"))
- && !trimmed.contains("version = \"")
- {
- let indent = line
- .chars()
- .take_while(|c| c.is_whitespace())
- .collect::<String>();
- let path_val = extract_path_value(trimmed).unwrap_or_default();
- let patched = trimmed.replace(
- &format!("path = \"{path_val}\""),
- &format!("path = \"{path_val}\", version = \"{version}\""),
- );
- result.push_str(&format!("{indent}{patched}\n"));
+ if trimmed == "[workspace.package]" {
+ in_ws_package = true;
continue;
}
+ if trimmed == "[package]" && !in_ws_deps && !in_ws_package {
+ // Remove the root [package] section entirely (virtual manifest)
+ in_root_package = true;
+ continue;
+ }
+
+ if in_ws_deps {
+ if trimmed.starts_with('[') {
+ in_ws_deps = false;
+ } else {
+ continue;
+ }
+ }
+
+ if in_ws_package {
+ if trimmed.starts_with('[') {
+ in_ws_package = false;
+ } else {
+ continue;
+ }
+ }
+
+ if in_root_package {
+ if trimmed.starts_with('[') {
+ in_root_package = false;
+ } else {
+ continue;
+ }
+ }
result.push_str(line);
result.push('\n');
diff --git a/.run/src/bin/refresh-docs.rs b/.run/src/bin/refresh-docs.rs
deleted file mode 100644
index 82ef906..0000000
--- a/.run/src/bin/refresh-docs.rs
+++ /dev/null
@@ -1,178 +0,0 @@
-use std::path::Path;
-
-use just_fmt::snake_case;
-use just_template::{Template, tmpl};
-use tools::println_cargo_style;
-
-const EXAMPLE_ROOT: &str = "./examples/";
-const OUTPUT_PATH: &str = "./mingling/src/example_docs.rs";
-
-const TEMPLATE_CONTENT: &str = include_str!("../../../mingling/src/example_docs.rs.tmpl");
-
-fn main() {
- gen_example_doc_module();
-}
-
-fn gen_example_doc_module() {
- let mut template = Template::from(TEMPLATE_CONTENT);
- let repo_root = find_git_repo().unwrap();
- let example_root = repo_root.join(EXAMPLE_ROOT);
- let mut examples = Vec::new();
- if let Ok(entries) = std::fs::read_dir(&example_root) {
- for entry in entries.flatten() {
- if let Ok(file_type) = entry.file_type()
- && file_type.is_dir()
- {
- let example_name = entry.file_name().to_string_lossy().to_string();
- // Ignore directories that don't start with "example-"
- if !example_name.starts_with("example-") {
- continue;
- }
- let example_content = ExampleContent::read(&example_name);
- examples.push(example_content);
- }
- }
- }
-
- examples.sort();
-
- for example in examples {
- tmpl!(template += {
- examples {
- (
- example_header = example.header,
- example_import = example.cargo_toml,
- example_code = example.code,
- example_name = snake_case!(&example.name)
- )
- }
- });
- println_cargo_style!("Refresh: {}", example.name);
- }
-
- let template_str = template.to_string();
- let template_str = template_str
- .lines()
- .map(str::trim_end)
- .collect::<Vec<_>>()
- .join("\n")
- + "\n";
- std::fs::write(repo_root.join(OUTPUT_PATH), template_str).unwrap();
-}
-
-struct ExampleContent {
- name: String,
- header: String,
- code: String,
- cargo_toml: String,
-}
-
-impl PartialOrd for ExampleContent {
- fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- Some(self.cmp(other))
- }
-}
-
-impl Ord for ExampleContent {
- fn cmp(&self, other: &Self) -> std::cmp::Ordering {
- self.name.cmp(&other.name)
- }
-}
-
-impl PartialEq for ExampleContent {
- fn eq(&self, other: &Self) -> bool {
- self.name == other.name
- }
-}
-
-impl Eq for ExampleContent {}
-
-impl ExampleContent {
- pub fn read(name: &str) -> Self {
- let repo = find_git_repo().unwrap();
- let cargo_toml = Self::read_cargo_toml(&repo, name);
- let (header, code) = Self::read_header_and_code(&repo, name);
-
- let cargo_toml = cargo_toml
- .lines()
- .map(|line| format!("/// {line}"))
- .collect::<Vec<_>>()
- .join("\n");
-
- let header = header
- .lines()
- .map(|line| format!("/// {line}"))
- .collect::<Vec<_>>()
- .join("\n");
-
- let code = code
- .lines()
- .map(|line| format!("/// {line}"))
- .collect::<Vec<_>>()
- .join("\n");
-
- ExampleContent {
- name: name.to_string(),
- header,
- code,
- cargo_toml,
- }
- }
-
- fn read_header_and_code(repo: &Path, name: &str) -> (String, String) {
- let file_path = repo
- .join(EXAMPLE_ROOT)
- .join(name)
- .join("src")
- .join("main.rs");
- let content = std::fs::read_to_string(&file_path).unwrap_or_default();
- let mut lines = content.lines();
- let mut header = String::new();
- let mut code = String::new();
-
- // Collect header lines (starting with //!)
- for line in lines.by_ref() {
- if line.trim_start().starts_with("//!") {
- let trimmed = line.trim_start_matches("//!");
- header.push_str(trimmed);
- header.push('\n');
- } else {
- // First non-header line found, start collecting code
- code.push_str(line);
- code.push('\n');
- break;
- }
- }
-
- // Collect remaining code lines
- for line in lines {
- code.push_str(line);
- code.push('\n');
- }
-
- (header.trim().to_string(), code.trim().to_string())
- }
-
- fn read_cargo_toml(repo: &Path, name: &str) -> String {
- let file_path = repo.join(EXAMPLE_ROOT).join(name).join("Cargo.toml");
-
- std::fs::read_to_string(&file_path).unwrap_or_default()
- }
-}
-
-fn find_git_repo() -> Option<std::path::PathBuf> {
- let mut current_dir = std::env::current_dir().ok()?;
-
- loop {
- let git_dir = current_dir.join(".git");
- if git_dir.exists() && git_dir.is_dir() {
- return Some(current_dir);
- }
-
- if !current_dir.pop() {
- break;
- }
- }
-
- None
-}
diff --git a/.run/src/bin/refresh-feature-mod.rs b/.run/src/bin/refresh-feature-mod.rs
deleted file mode 100644
index 2255dbc..0000000
--- a/.run/src/bin/refresh-feature-mod.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-use std::collections::BTreeSet;
-use std::path::Path;
-
-use just_fmt::snake_case;
-use just_template::{tmpl, Template};
-use tools::println_cargo_style;
-
-const CARGO_TOML_PATH: &str = "./mingling/Cargo.toml";
-const OUTPUT_PATH: &str = "./mingling/src/features.rs";
-
-const TEMPLATE_CONTENT: &str = include_str!("../../../mingling/src/features.rs.tmpl");
-
-fn main() {
- gen_feature_module();
-}
-
-fn gen_feature_module() {
- let repo_root = find_git_repo().unwrap();
-
- let cargo_toml_path = repo_root.join(CARGO_TOML_PATH);
- let output_path = repo_root.join(OUTPUT_PATH);
-
- let features = parse_features(&cargo_toml_path);
-
- let mut template = Template::from(TEMPLATE_CONTENT);
-
- for feat_name in &features {
- let feat_const_name = snake_case!(feat_name).to_uppercase();
-
- tmpl!(template += {
- features {
- (
- feat_name = feat_name,
- feat_const_name = feat_const_name
- )
- }
- });
- println_cargo_style!("Refresh: feature `{}`", feat_name);
- }
-
- let template_str = template.to_string();
- let template_str = template_str
- .lines()
- .map(str::trim_end)
- .collect::<Vec<_>>()
- .join("\n")
- + "\n";
- std::fs::write(&output_path, template_str).unwrap();
-
- println_cargo_style!("Written: features module to {}", OUTPUT_PATH);
-}
-
-/// Parse all feature names from the `[features]` section of a Cargo.toml.
-fn parse_features(cargo_toml_path: &Path) -> Vec<String> {
- let content = std::fs::read_to_string(cargo_toml_path)
- .unwrap_or_else(|e| panic!("Failed to read {}: {}", cargo_toml_path.display(), e));
-
- let cargo_toml: toml::Value = content
- .parse()
- .unwrap_or_else(|e| panic!("Failed to parse {}: {}", cargo_toml_path.display(), e));
-
- let features_table = cargo_toml
- .get("features")
- .and_then(|v| v.as_table())
- .unwrap_or_else(|| {
- panic!(
- "No [features] section found in {}",
- cargo_toml_path.display()
- )
- });
-
- let mut feature_names: BTreeSet<String> = BTreeSet::new();
- for key in features_table.keys() {
- feature_names.insert(key.clone());
- }
-
- let mut result: Vec<String> = feature_names.into_iter().collect();
- result.sort();
- result
-}
-
-fn find_git_repo() -> Option<std::path::PathBuf> {
- let mut current_dir = std::env::current_dir().ok()?;
-
- loop {
- let git_dir = current_dir.join(".git");
- if git_dir.exists() && git_dir.is_dir() {
- return Some(current_dir);
- }
-
- if !current_dir.pop() {
- break;
- }
- }
-
- None
-}
diff --git a/.run/src/bin/sync-examples.rs b/.run/src/bin/sync-examples.rs
deleted file mode 100644
index 0923b33..0000000
--- a/.run/src/bin/sync-examples.rs
+++ /dev/null
@@ -1,131 +0,0 @@
-use std::fs;
-use std::path::Path;
-
-use serde::{Deserialize, Serialize};
-use tools::println_cargo_style;
-
-#[derive(Serialize)]
-struct ExampleMeta {
- id: String,
- name: String,
- icon: String,
- category: String,
- desc: String,
- tags: Vec<String>,
- files: Vec<String>,
-}
-
-#[derive(Deserialize)]
-struct PageToml {
- example: PageTomlExample,
-}
-
-#[derive(Deserialize)]
-struct PageTomlExample {
- id: String,
- #[serde(default)]
- name: String,
- #[serde(default = "default_icon")]
- icon: String,
- #[serde(default)]
- category: String,
- #[serde(default)]
- desc: String,
- #[serde(default)]
- tags: Vec<String>,
- #[serde(default = "default_files")]
- files: Vec<String>,
-}
-
-fn default_icon() -> String {
- "📦".to_string()
-}
-
-fn default_files() -> Vec<String> {
- vec!["Cargo.toml".to_string(), "src/main.rs".to_string()]
-}
-
-fn main() {
- #[cfg(windows)]
- let _ = colored::control::set_virtual_terminal(true);
-
- let examples_dir = Path::new("examples");
- let output_dir = Path::new("docs/example-pages");
- fs::create_dir_all(output_dir).expect("failed to create docs/example-pages");
-
- let mut examples: Vec<ExampleMeta> = Vec::new();
-
- let entries = fs::read_dir(examples_dir).expect("failed to read examples/");
- for entry in entries.flatten() {
- let path = entry.path();
- if !path.is_dir() {
- continue;
- }
-
- let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
-
- let id = dir_name.to_string();
- let page_toml_path = path.join("page.toml");
-
- let meta = if page_toml_path.exists() {
- match fs::read_to_string(&page_toml_path)
- .map_err(|e| e.to_string())
- .and_then(|content| toml::from_str::<PageToml>(&content).map_err(|e| e.to_string()))
- {
- Ok(page) => {
- let ex = page.example;
- ExampleMeta {
- id: if ex.id.is_empty() { id.clone() } else { ex.id },
- name: if ex.name.is_empty() {
- id.clone()
- } else {
- ex.name
- },
- icon: ex.icon,
- category: ex.category,
- desc: ex.desc,
- tags: ex.tags,
- files: if ex.files.is_empty() {
- default_files()
- } else {
- ex.files
- },
- }
- }
- Err(e) => {
- eprintln!(
- "Warning: failed to parse {}: {}",
- page_toml_path.display(),
- e
- );
- continue;
- }
- }
- } else {
- continue;
- };
-
- examples.push(meta);
- }
-
- // Sort: basic first, then alphabetical
- examples.sort_by(|a, b| {
- if a.id == "example-basic" {
- return std::cmp::Ordering::Less;
- }
- if b.id == "example-basic" {
- return std::cmp::Ordering::Greater;
- }
- a.id.cmp(&b.id)
- });
-
- let json = serde_json::to_string_pretty(&examples).expect("failed to serialize");
- let output_path = output_dir.join("examples.json");
- fs::write(&output_path, &json).expect("failed to write examples.json");
-
- println_cargo_style!(
- "Sync: {} examples -> {}",
- examples.len(),
- output_path.display()
- );
-}
diff --git a/.run/src/bin/test-all-markdown-code.rs b/.run/src/bin/test-all-markdown-code.rs
deleted file mode 100644
index 1c0c9e2..0000000
--- a/.run/src/bin/test-all-markdown-code.rs
+++ /dev/null
@@ -1,261 +0,0 @@
-use std::collections::HashMap;
-use std::env;
-use std::path::{Path, PathBuf};
-
-use colored::Colorize;
-use indicatif::ProgressBar;
-use tools::verify::{
- build_block, compute_block_hash, generate_build_rs, generate_cargo_toml, generate_main_rs,
- is_block_testable, parse_code_blocks, write_summary_report,
-};
-use tools::{eprintln_cargo_style, println_cargo_style};
-
-/// Config from verified-docs.toml
-#[derive(serde::Deserialize)]
-struct Config {
- verified: HashMap<String, String>,
-}
-
-#[tokio::main]
-async fn main() {
- #[cfg(windows)]
- let _ = colored::control::set_virtual_terminal(true);
-
- let config_path = PathBuf::from("verified-docs.toml");
- if !config_path.exists() {
- eprintln_cargo_style!("verified-docs.toml not found in current directory");
- std::process::exit(1);
- }
-
- let config: Config = {
- let content = std::fs::read_to_string(&config_path).unwrap_or_else(|_e| {
- eprintln_cargo_style!("Failed to read verified-docs.toml");
- std::process::exit(1);
- });
- toml::from_str(&content).unwrap_or_else(|_e| {
- eprintln_cargo_style!("Failed to parse verified-docs.toml");
- std::process::exit(1);
- })
- };
-
- // Parse optional path argument from env args
- let single_file: Option<PathBuf> = {
- let args: Vec<String> = env::args().collect();
- if args.len() > 1 {
- let p = PathBuf::from(&args[1]);
- if p.exists() {
- Some(p)
- } else {
- eprintln_cargo_style!("error: specified file '{}' does not exist", args[1]);
- std::process::exit(1);
- }
- } else {
- None
- }
- };
-
- // Collect all markdown files from config
- // Keys are used as labels; values are either single file paths or directory globs
- let mut files: Vec<(String, PathBuf)> = Vec::new();
-
- for (key, value) in &config.verified {
- let candidate = PathBuf::from(value);
- if candidate.is_dir() {
- // Directory — walk it for all .md files, using the key as label
- collect_md_files(&candidate, &mut files, key);
- } else if candidate.exists() && candidate.is_file() {
- // Single file
- files.push((key.to_string(), candidate));
- } else if candidate.extension().is_none() {
- // No extension — treat as a glob like "docs/pages/**", walk the base dir instead
- let base = PathBuf::from(value.trim_end_matches("/**").trim_end_matches('*'));
- if base.is_dir() {
- collect_md_files(&base, &mut files, key);
- }
- }
- }
-
- // Sort for deterministic ordering
- files.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
-
- // If a single file was specified, filter the list to only that file
- if let Some(ref target) = single_file {
- let target_canon = std::fs::canonicalize(target).unwrap_or_else(|_| target.clone());
- files.retain(|(_, path)| {
- std::fs::canonicalize(path)
- .map(|p| p == target_canon)
- .unwrap_or(false)
- });
- if files.is_empty() {
- eprintln_cargo_style!(
- "error: specified file '{}' is not among the configured documentation files",
- target.display()
- );
- std::process::exit(1);
- }
- }
-
- if files.is_empty() {
- eprintln_cargo_style!("No markdown files found to verify");
- std::process::exit(1);
- }
-
- // Parse all code blocks into a flat list with global indices
- let mut flat_blocks: Vec<(usize, tools::verify::CodeBlock)> = Vec::new();
-
- for (label, path) in &files {
- let content = std::fs::read_to_string(path).unwrap_or_else(|e| {
- eprintln_cargo_style!("Failed to read {}: {}", path.display(), e);
- String::new()
- });
- let source_file = format!("{label}/{}", path.file_name().unwrap().to_string_lossy());
- let blocks = parse_code_blocks(&content, &source_file);
- let testable: Vec<_> = blocks.into_iter().filter(is_block_testable).collect();
- for block in testable {
- let idx = flat_blocks.len() + 1; // 1-based global index
- flat_blocks.push((idx, block));
- }
- }
-
- let total_testable = flat_blocks.len();
-
- if total_testable == 0 {
- println_cargo_style!("No testable code blocks found");
- return;
- }
-
- // Create a shared progress bar
- let bar = ProgressBar::new(total_testable as u64);
- bar.set_style(
- indicatif::ProgressStyle::default_bar()
- .template(&format!(
- "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
- " Testing".bold().bright_cyan()
- ))
- .unwrap()
- .progress_chars("=> "),
- );
- bar.set_message("blocks");
-
- // Group blocks by dependency hash
- let mut groups: HashMap<String, Vec<(usize, tools::verify::CodeBlock)>> = HashMap::new();
- for (idx, block) in flat_blocks {
- let hash = compute_block_hash(&block);
- groups.entry(hash).or_default().push((idx, block));
- }
-
- let temp_base = PathBuf::from(".temp/doc-test");
-
- // Sort groups by hash for deterministic output order
- let mut group_vec: Vec<(String, Vec<(usize, tools::verify::CodeBlock)>)> =
- groups.into_iter().collect();
- group_vec.sort_by(|a, b| a.0.cmp(&b.0));
-
- // Spawn a blocking task per group — groups run in parallel, blocks within a group are serial
- let mut handles = Vec::new();
- for (hash, blocks) in group_vec {
- let temp_base = temp_base.clone();
- let bar = bar.clone(); // clone shares the same underlying progress
- let handle = tokio::task::spawn_blocking(move || {
- let crate_dir = temp_base.join(&hash);
- let src_dir = crate_dir.join("src");
- let manifest_path = crate_dir.join("Cargo.toml");
-
- // Generate a single Cargo.toml for the whole group (all blocks share same deps)
- let first_block = &blocks[0].1;
- let cargo_toml = generate_cargo_toml(first_block, "test-doc", &manifest_path);
-
- let mut group_results: Vec<(String, usize, bool, String)> = Vec::new();
- for (block_idx, block) in &blocks {
- let block_label =
- format!("Block {block_idx} ({}:{})", block.source_file, block.line);
-
- bar.set_message(block_label.clone());
-
- let main_rs = if block.is_build_time {
- // For build-time blocks, write a stub main.rs and generate build.rs
- generate_build_rs(block)
- } else {
- generate_main_rs(block)
- };
- let (ok, err) = build_block(
- &src_dir,
- &manifest_path,
- &cargo_toml,
- &main_rs,
- block.is_build_time,
- );
- if ok {
- bar.inc(1);
- } else {
- bar.inc(1);
- bar.println(format!(" {} {block_label}", "failed".bold().bright_red()));
- bar.println(format!(" {block_label} FAILED:\n{err}"));
- }
- group_results.push((block.source_file.clone(), block.line, ok, err));
- }
- group_results
- });
- handles.push(handle);
- }
-
- // Collect results from all groups
- let mut results: Vec<(String, usize, bool, String)> = Vec::new();
- let mut passed = 0usize;
- let mut failed = 0usize;
-
- for handle in handles {
- match handle.await {
- Ok(group_results) => {
- for (file, line, ok, err) in group_results {
- if ok {
- passed += 1;
- } else {
- failed += 1;
- }
- results.push((file, line, ok, err));
- }
- }
- Err(e) => {
- eprintln_cargo_style!("Task panicked: {}", e);
- std::process::exit(1);
- }
- }
- }
-
- bar.finish_and_clear();
-
- let result_msg = format!("Result: {passed}/{total_testable} blocks passed");
- println_cargo_style!(result_msg);
-
- write_summary_report(
- Path::new(".temp/DOCS-TEST-RESULT.md"),
- "Documentation Code Block Test Report",
- &results,
- total_testable,
- passed,
- failed,
- );
-
- if failed > 0 {
- let fail_msg = format!("{failed} block(s) failed to build");
- eprintln_cargo_style!(fail_msg);
- std::process::exit(1);
- }
-
- println_cargo_style!("Done: All verified code blocks build successfully!");
-}
-
-/// Recursively collect all `.md` files under a directory
-fn collect_md_files(dir: &Path, files: &mut Vec<(String, PathBuf)>, lang: &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, lang);
- } else if path.extension().is_some_and(|ext| ext == "md") {
- files.push((lang.to_string(), path));
- }
- }
- }
-}
diff --git a/.run/src/bin/test-all.ps1 b/.run/src/bin/test-all.ps1
deleted file mode 100644
index 231698a..0000000
--- a/.run/src/bin/test-all.ps1
+++ /dev/null
@@ -1,8 +0,0 @@
-$starting_dir = Get-Location
-Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object {
- $project_dir = $_.DirectoryName
- Push-Location $project_dir
- cargo test
- Pop-Location
-}
-Set-Location $starting_dir
diff --git a/.run/src/bin/test-all.sh b/.run/src/bin/test-all.sh
deleted file mode 100644
index b387463..0000000
--- a/.run/src/bin/test-all.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-find . -name "Cargo.toml" -type f | while read -r cargo_file; do
- project_dir=$(dirname "$cargo_file")
- (cd "$project_dir" && cargo test)
-done
diff --git a/.run/src/bin/test-examples.rs b/.run/src/bin/test-examples.rs
deleted file mode 100644
index 539459e..0000000
--- a/.run/src/bin/test-examples.rs
+++ /dev/null
@@ -1,158 +0,0 @@
-use std::collections::HashMap;
-
-use colored::Colorize;
-use indicatif::ProgressBar;
-use serde::Deserialize;
-use tools::{eprintln_cargo_style, println_cargo_style};
-
-#[derive(Deserialize)]
-struct TestConfig {
- test: HashMap<String, Vec<TestCase>>,
-}
-
-#[derive(Deserialize)]
-struct TestCase {
- command: String,
- expect: Expect,
-}
-
-#[derive(Deserialize)]
-struct Expect {
- #[serde(rename = "exit-code")]
- exit_code: i32,
- result: String,
-}
-
-fn main() {
- #[cfg(windows)]
- let _ = colored::control::set_virtual_terminal(true);
-
- let config = load_config();
-
- // Count total test cases upfront
- let total: usize = config.test.values().map(|cases| cases.len()).sum();
- let bar = ProgressBar::new(total as u64);
- bar.set_style(
- indicatif::ProgressStyle::default_bar()
- .template(&format!(
- "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
- " Testing".bold().bright_cyan()
- ))
- .unwrap()
- .progress_chars("=> "),
- );
- bar.set_message("examples");
-
- let passed = run_all_tests(&config, &bar);
-
- bar.finish_and_clear();
-
- println_cargo_style!("Result: {}/{} tests passed", passed, total);
-
- if passed != total {
- eprintln_cargo_style!("{} test(s) failed", total - passed);
- std::process::exit(1);
- }
-}
-
-/// Parse test config from TOML file
-fn load_config() -> TestConfig {
- let content = std::fs::read_to_string("examples/test-examples.toml").unwrap_or_else(|e| {
- eprintln_cargo_style!("Failed to read TOML config file: {}", e);
- std::process::exit(1);
- });
-
- toml::from_str(&content).unwrap_or_else(|e| {
- eprintln_cargo_style!("Failed to parse TOML config: {}", e);
- std::process::exit(1);
- })
-}
-
-/// Run all example test groups, return number passed
-fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize {
- let mut passed = 0;
-
- for (example_name, test_cases) in &config.test {
- bar.set_message(example_name.clone());
-
- if !build_example(example_name) {
- bar.inc(test_cases.len() as u64);
- continue;
- }
-
- for test_case in test_cases {
- if run_single_test(example_name, test_case, bar) {
- passed += 1;
- }
- bar.inc(1);
- }
- }
-
- passed
-}
-
-/// Build the example binary, return true on success
-fn build_example(example_name: &str) -> bool {
- let manifest = format!("examples/{example_name}/Cargo.toml");
- tools::run_cmd_capture(format!(
- "cargo build --manifest-path {manifest} --color always",
- ))
- .is_ok()
-}
-
-/// Run a single test case, return true on pass
-fn run_single_test(example_name: &str, test_case: &TestCase, bar: &ProgressBar) -> bool {
- let binary_path = format!(".temp/target/debug/{}", get_binary_name(example_name));
- let args: Vec<&str> = test_case.command.split_whitespace().collect();
-
- let output = match std::process::Command::new(&binary_path)
- .args(&args)
- .output()
- {
- Ok(o) => o,
- Err(e) => {
- bar.println(format!("'{}' - failed to run: {}", test_case.command, e));
- return false;
- }
- };
-
- let actual_exit_code = output.status.code().unwrap_or(-1);
- let actual_stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
- let actual_stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
-
- let exit_ok = actual_exit_code == test_case.expect.exit_code;
- let result_ok = actual_stdout == test_case.expect.result
- || actual_stdout.contains(&test_case.expect.result);
-
- if exit_ok && result_ok {
- true
- } else {
- bar.println(format!("failed: '{}'", test_case.command));
- if !exit_ok {
- bar.println(format!(
- " Expected exit code: {}, actual: {}",
- test_case.expect.exit_code, actual_exit_code
- ));
- }
- if !result_ok {
- bar.println(format!(" Expected output: {:?}", test_case.expect.result));
- bar.println(format!(" Actual stdout: {:?}", actual_stdout));
- if !actual_stderr.is_empty() {
- bar.println(format!(" Actual stderr: {:?}", actual_stderr));
- }
- }
- false
- }
-}
-
-/// Resolve binary filename for the given example
-///
-/// The binary name matches the package name. On Windows, the `.exe` suffix is required.
-fn get_binary_name(example_name: &str) -> String {
- let base = example_name;
- if cfg!(target_os = "windows") {
- format!("{base}.exe")
- } else {
- base.to_string()
- }
-}
diff --git a/.run/src/bin/update-version.rs b/.run/src/bin/update-version.rs
index 9d595bb..2283662 100644
--- a/.run/src/bin/update-version.rs
+++ b/.run/src/bin/update-version.rs
@@ -56,11 +56,11 @@ fn main() {
println_cargo_style!("Version: {} -> {}", current_ver, new_ver);
// Read version-files.toml
- let config_path = Path::new(".run").join("version-files.toml");
+ let config_path = Path::new(".config").join("version-files.toml");
let config_str =
- std::fs::read_to_string(&config_path).expect("Failed to read .run/version-files.toml");
+ std::fs::read_to_string(&config_path).expect("Failed to read .config/version-files.toml");
let config: Config =
- toml::from_str(&config_str).expect("Failed to parse .run/version-files.toml");
+ toml::from_str(&config_str).expect("Failed to parse .config/version-files.toml");
let mut updated_count = 0;
let mut skipped_count = 0;