diff options
Diffstat (limited to '.run/src')
| -rw-r--r-- | .run/src/bin/ci.py | 75 | ||||
| -rw-r--r-- | .run/src/bin/clippy-fix.ps1 | 8 | ||||
| -rwxr-xr-x | .run/src/bin/clippy-fix.sh | 6 | ||||
| -rw-r--r-- | .run/src/bin/cov-test.rs | 571 | ||||
| -rw-r--r-- | .run/src/bin/deploy-api-docs.rs | 118 | ||||
| -rw-r--r-- | .run/src/bin/display-dependency-order.rs | 12 | ||||
| -rw-r--r-- | .run/src/bin/http-page-preview.ps1 | 3 | ||||
| -rwxr-xr-x | .run/src/bin/http-page-preview.sh | 2 | ||||
| -rw-r--r-- | .run/src/bin/install-mling.ps1 | 10 | ||||
| -rwxr-xr-x | .run/src/bin/install-mling.sh | 17 | ||||
| -rw-r--r-- | .run/src/bin/package-all.rs | 736 | ||||
| -rw-r--r-- | .run/src/bin/update-version.rs | 104 | ||||
| -rw-r--r-- | .run/src/bin/windows-folder-hide.ps1 | 115 | ||||
| -rw-r--r-- | .run/src/dependency_order.rs | 196 | ||||
| -rw-r--r-- | .run/src/lib.rs | 459 | ||||
| -rw-r--r-- | .run/src/verify.rs | 506 |
16 files changed, 0 insertions, 2938 deletions
diff --git a/.run/src/bin/ci.py b/.run/src/bin/ci.py deleted file mode 100644 index 6234a19..0000000 --- a/.run/src/bin/ci.py +++ /dev/null @@ -1,75 +0,0 @@ -"""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/clippy-fix.ps1 b/.run/src/bin/clippy-fix.ps1 deleted file mode 100644 index 1d24f92..0000000 --- a/.run/src/bin/clippy-fix.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 --fix --allow-dirty --allow-no-vcs --quiet - Pop-Location -} -Set-Location $starting_dir diff --git a/.run/src/bin/clippy-fix.sh b/.run/src/bin/clippy-fix.sh deleted file mode 100755 index 9771ad4..0000000 --- a/.run/src/bin/clippy-fix.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 --fix --allow-dirty --allow-no-vcs --quiet) -done diff --git a/.run/src/bin/cov-test.rs b/.run/src/bin/cov-test.rs deleted file mode 100644 index f62ff01..0000000 --- a/.run/src/bin/cov-test.rs +++ /dev/null @@ -1,571 +0,0 @@ -//! 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 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"); - - // 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!("Target: {}", cov_target.display()); - - // 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); - }); - - // Move files from <output_path>/html/ to <output_path> - let html_dir = output_path.join("html"); - if html_dir.exists() && html_dir.is_dir() { - println_cargo_style!("Moving files from {}/html/ to {}/", OUTPUT_DIR, OUTPUT_DIR); - - 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(); - let file_name = entry - .file_name() - .to_str() - .expect("Invalid filename") - .to_owned(); - - let dest_path = output_path.join(&file_name); - if dest_path.exists() { - if dest_path.is_dir() { - fs::remove_dir_all(&dest_path).unwrap_or_else(|e| { - eprintln!( - "Warning: could not remove directory {}: {}", - dest_path.display(), - e - ); - }); - } else { - fs::remove_file(&dest_path).unwrap_or_else(|e| { - eprintln!( - "Warning: could not remove file {}: {}", - dest_path.display(), - e - ); - }); - } - } - fs::rename(&entry_path, &dest_path).unwrap_or_else(|e| { - eprintln!("Warning: could not move {}: {}", entry_path.display(), e); - }); - } - - fs::remove_dir(&html_dir).unwrap_or_else(|e| { - eprintln!("Warning: could not remove html directory: {}", e); - }); - - 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()?; - - 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 -} - -#[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 deleted file mode 100644 index 961eb04..0000000 --- a/.run/src/bin/deploy-api-docs.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::path::Path; - -use arg_picker::{Picker, macros::arg}; -use tools::{println_cargo_style, run_cmd}; - -const OUTPUT_DIR: &str = "docs/api-docs"; - -fn main() { - let using_docsrs = Picker::from_args() - .pick_or_default(&arg![docsrs: bool]) - .unwrap(); - - let repo_root = find_git_repo().expect("Failed to find git repository root"); - - // 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 - let output_path = repo_root.join(OUTPUT_DIR); - std::fs::create_dir_all(&output_path).expect("Failed to create output directory"); - - // Build cargo doc command - let cmd = if using_docsrs { - format!( - "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", - features_arg, - output_path.join("target").to_string_lossy() - ) - }; - - println_cargo_style!("Features: {}", features_arg); - println_cargo_style!("Output: {}", output_path.display()); - - // Run cargo doc, then copy generated docs to output directory - println_cargo_style!("Building: docs (cargo doc --no-deps)"); - run_cmd!(&cmd).unwrap_or_else(|code| { - eprintln!("Error: cargo doc failed with exit code {}", code); - std::process::exit(code); - }); - - // Copy generated docs from target/doc to OUTPUT_DIR (top level) - let doc_source = output_path.join("target").join("doc"); - let doc_dest = &output_path; - - if doc_source.exists() { - println_cargo_style!("Copying: docs to output directory"); - // Remove old docs in destination (except target/) - if let Ok(entries) = std::fs::read_dir(doc_dest) { - for entry in entries.flatten() { - let path = entry.path(); - if path.file_name().and_then(|n| n.to_str()) == Some("target") { - continue; - } - if path.is_dir() { - std::fs::remove_dir_all(&path).ok(); - } else { - std::fs::remove_file(&path).ok(); - } - } - } - copy_dir_recursively(&doc_source, doc_dest).expect("Failed to copy documentation"); - } - - // Clean up the intermediate target directory to save space - std::fs::remove_dir_all(output_path.join("target")).ok(); - - println_cargo_style!("Done: API docs deployed to {}", output_path.display()); -} - -fn copy_dir_recursively(src: &Path, dst: &Path) -> std::io::Result<()> { - if !dst.exists() { - std::fs::create_dir_all(dst)?; - } - - for entry in std::fs::read_dir(src)? { - let entry = entry?; - let file_type = entry.file_type()?; - let src_path = entry.path(); - let file_name = src_path.file_name().expect("Failed to get file name"); - let dst_path = dst.join(file_name); - - if file_type.is_dir() { - copy_dir_recursively(&src_path, &dst_path)?; - } else { - std::fs::copy(&src_path, &dst_path)?; - } - } - - Ok(()) -} - -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/display-dependency-order.rs b/.run/src/bin/display-dependency-order.rs deleted file mode 100644 index a31c67a..0000000 --- a/.run/src/bin/display-dependency-order.rs +++ /dev/null @@ -1,12 +0,0 @@ -use tools::{dependency_order::display_dependency_order, eprintln_cargo_style}; - -fn main() { - let order = display_dependency_order(); - if order.is_empty() { - eprintln_cargo_style!("could not find workspace root or mingling crates"); - std::process::exit(1); - } - for path in order { - println!("{}", path.display()); - } -} diff --git a/.run/src/bin/http-page-preview.ps1 b/.run/src/bin/http-page-preview.ps1 deleted file mode 100644 index 8cc3579..0000000 --- a/.run/src/bin/http-page-preview.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -$starting_dir = Get-Location -python -m http.server 3000 -Set-Location $starting_dir diff --git a/.run/src/bin/http-page-preview.sh b/.run/src/bin/http-page-preview.sh deleted file mode 100755 index bed4b1c..0000000 --- a/.run/src/bin/http-page-preview.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -python3 -m http.server 3000 diff --git a/.run/src/bin/install-mling.ps1 b/.run/src/bin/install-mling.ps1 deleted file mode 100644 index 2b55a09..0000000 --- a/.run/src/bin/install-mling.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -$ErrorActionPreference = "Stop" - -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 deleted file mode 100755 index e8cfa18..0000000 --- a/.run/src/bin/install-mling.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -set -e - -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 deleted file mode 100644 index ecdd133..0000000 --- a/.run/src/bin/package-all.rs +++ /dev/null @@ -1,736 +0,0 @@ -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, -}; - -/// A single member from `cargo metadata` output. -#[derive(Deserialize, Debug)] -struct MetadataPackage { - name: String, - version: String, - manifest_path: String, -} - -/// The top-level metadata structure. -#[derive(Deserialize, Debug)] -struct Metadata { - #[allow(dead_code)] - workspace_root: String, - packages: Vec<MetadataPackage>, -} - -fn main() { - // 1. Determine project root - let cwd = std::env::current_dir().expect("failed to get current working directory"); - let workspace_root = find_workspace_root(&cwd).expect("not inside a Cargo workspace"); - println_cargo_style!("Workspace: {}", workspace_root.display()); - - let pre_release_dir = workspace_root.join(".temp/pre-release"); - - // 2. Clean `.temp/pre-release/` - println_cargo_style!("Clean: .temp/pre-release/"); - let _ = std::fs::remove_dir_all(&pre_release_dir); - std::fs::create_dir_all(&pre_release_dir).expect("failed to create .temp/pre-release/"); - - // 3. Run `cargo metadata` to get workspace members info - println_cargo_style!("Metadata: querying workspace members"); - let metadata_json = run_cmd_capture_with_dir( - "cargo metadata --format-version 1 --no-deps", - &workspace_root, - ) - .unwrap_or_else(|(code, _msg)| { - eprintln_cargo_style!(format!("cargo metadata failed (exit {code}):\n{{msg}}")); - std::process::exit(1); - }); - - let metadata: Metadata = serde_json::from_str(&metadata_json).unwrap_or_else(|e| { - eprintln_cargo_style!("failed to parse cargo metadata: {}", e); - std::process::exit(1); - }); - - // Filter workspace members: skip the root virtual manifest - let workspace_root_str = workspace_root.to_string_lossy().replace('\\', "/"); - let members: Vec<&MetadataPackage> = metadata - .packages - .iter() - .filter(|p| { - let mp = p.manifest_path.replace('\\', "/"); - mp.starts_with(&workspace_root_str) - && mp != format!("{}/Cargo.toml", workspace_root_str) - }) - .collect(); - - if members.is_empty() { - eprintln_cargo_style!("No workspace members found!"); - std::process::exit(1); - } - - // Print member info - for m in &members { - println_cargo_style!("Member: {}@{}", m.name, m.version); - } - - // Build version map: crate_name -> version - 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 - let mut member_dirs: Vec<PathBuf> = Vec::new(); - for m in &members { - let dir = Path::new(&m.manifest_path) - .parent() - .expect("manifest_path has no parent"); - let canonical_dir = std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf()); - let canonical_root = - std::fs::canonicalize(&workspace_root).unwrap_or_else(|_| workspace_root.to_path_buf()); - let relative = canonical_dir - .strip_prefix(&canonical_root) - .map(|p| p.to_path_buf()) - .unwrap_or_else(|_| PathBuf::from(dir.file_name().unwrap_or_default())); - if !member_dirs.contains(&relative) { - member_dirs.push(relative); - } - } - - // 4. Copy files to the temp directory, preserving the workspace directory structure - println_cargo_style!("Copy: project structure to .temp/pre-release/"); - - copy_dir( - &workspace_root.join(".cargo"), - &pre_release_dir.join(".cargo"), - ); - - for dir in &member_dirs { - let src = workspace_root.join(dir); - let dst = pre_release_dir.join(dir); - copy_dir(&src, &dst); - } - - copy_file( - &workspace_root.join("Cargo.toml"), - &pre_release_dir.join("Cargo.toml"), - ); - copy_file( - &workspace_root.join("Cargo.lock"), - &pre_release_dir.join("Cargo.lock"), - ); - - // 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"); - - // 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 (ws_package, ws_deps) = parse_workspace_config(&root_content); - - // 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() { - continue; - } - let member_content = std::fs::read_to_string(&member_cargo) - .unwrap_or_else(|e| panic!("failed to read {}: {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 - let package_ok = run_cmd_capture_with_dir( - "cargo package --workspace --no-verify --color always", - &pre_release_dir, - ); - - match &package_ok { - Ok(out) => { - println!("{out}"); - } - Err((code, msg)) => { - // Print output but don't fail yet - eprintln_cargo_style!(format!("cargo package exited with code {code}:")); - println!("{msg}"); - } - } - - // 7. Copy built packages back to .temp/target/package - let temp_package_dir = workspace_root.join(".temp/target/package"); - std::fs::create_dir_all(&temp_package_dir) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", temp_package_dir.display())); - - // cargo package puts .crate files in target/package - let pre_release_target_package = pre_release_dir.join(".temp/target/package"); - if pre_release_target_package.exists() { - println_cargo_style!("Copy: packages to .temp/target/package"); - copy_dir_contents(&pre_release_target_package, &temp_package_dir); - } else { - wprintln_cargo_style!("No packages found in .temp/pre-release/.temp/target/package"); - } - - // 8. Export each crate as a standalone project from the .crate packages. - // The .crate files contain the final publish-ready Cargo.toml with all - // workspace/path deps already resolved by `cargo package`. - let release_dir = workspace_root.join(".temp/release"); - println_cargo_style!("Export: standalone crates to .temp/release/"); - let _ = std::fs::remove_dir_all(&release_dir); - std::fs::create_dir_all(&release_dir) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", release_dir.display())); - - for entry in std::fs::read_dir(&temp_package_dir).expect("failed to read target/package") { - let entry = entry.expect("failed to read entry"); - let path = entry.path(); - if path.extension().is_none_or(|e| e != "crate") { - continue; - } - - // .crate files are gzipped tarballs. Extract to .temp/release/<name>/ - // fname is like "mingling-0.3.0" - let fname = path.file_stem().unwrap().to_string_lossy().to_string(); - - // Derive crate directory name by stripping the version suffix - // mingling-0.3.0 -> mingling, arg-picker-0.1.0 -> arg-picker - let crate_dir_name = fname - .rfind('-') - .and_then(|dash| { - // Check if what follows looks like a semver - let ver_part = &fname[dash + 1..]; - if ver_part.chars().next().is_some_and(|c| c.is_ascii_digit()) { - Some(&fname[..dash]) - } else { - None - } - }) - .unwrap_or(&fname) - .to_string(); - - let target_dir = release_dir.join(&crate_dir_name); - std::fs::create_dir_all(&target_dir) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", target_dir.display())); - - // Extract using flate2 + tar (cross-platform) - let file = match std::fs::File::open(&path) { - Ok(f) => f, - Err(e) => { - eprintln_cargo_style!("Failed to open {}: {e}", path.display()); - continue; - } - }; - let decoder = GzDecoder::new(file); - let mut archive = Archive::new(decoder); - if let Err(e) = archive.unpack(&target_dir) { - eprintln_cargo_style!("Failed to extract {}: {e}", fname); - continue; - } - - // Move the contents from the inner dir up one level - // .crate contains a single top-level dir named after the package - let inner = target_dir.join(&fname); - if inner.exists() { - for inner_entry in std::fs::read_dir(&inner).expect("failed to read inner dir") { - let inner_entry = inner_entry.expect("failed to read entry"); - let inner_path = inner_entry.path(); - let dest = target_dir.join(inner_path.file_name().unwrap()); - if dest.exists() { - let _ = std::fs::remove_dir_all(&dest); - } - std::fs::rename(&inner_path, &dest).unwrap_or_else(|e| { - panic!( - "failed to rename {} -> {}: {e}", - inner_path.display(), - dest.display() - ) - }); - } - let _ = std::fs::remove_dir_all(&inner); - } - - // Clean up: remove .orig file (only the normalized Cargo.toml is needed) - let _ = std::fs::remove_file(target_dir.join("Cargo.toml.orig")); - // 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); - } - - println_cargo_style!("Done: .temp/release/ is ready"); - - // If package failed, report it - if package_ok.is_err() { - eprintln_cargo_style!("cargo package reported errors above"); - std::process::exit(1); - } -} - -/// 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, - 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_package = false; - let mut in_section_with_deps = false; - - for line in content.lines() { - let trimmed = line.trim(); - 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; - } - - // [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 - } - - // 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; - } - } - } - - result.push_str(line); - result.push('\n'); - } - - result -} - -/// 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_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 == "[workspace.dependencies]" { - in_ws_deps = true; - continue; - } - 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'); - } - - result -} - -/// Recursively copy a directory. -fn copy_dir(src: &Path, dst: &Path) { - copy_dir_filtered(src, dst, &|_: &Path| true) -} - -/// Recursively copy a directory with a filter function. -/// The filter receives the source path and returns `true` if the entry should be copied. -fn copy_dir_filtered(src: &Path, dst: &Path, filter: &dyn Fn(&Path) -> bool) { - if !src.exists() { - return; - } - if !filter(src) { - return; - } - std::fs::create_dir_all(dst) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", dst.display())); - - for entry in std::fs::read_dir(src).expect("failed to read directory") { - let entry = entry.expect("failed to read entry"); - let entry_type = entry.file_type().expect("failed to get file type"); - let src_path = entry.path(); - let dst_path = dst.join(entry.file_name()); - - if !filter(&src_path) { - continue; - } - - if entry_type.is_dir() { - copy_dir_filtered(&src_path, &dst_path, filter); - } else if entry_type.is_file() || entry_type.is_symlink() { - copy_file(&src_path, &dst_path); - } - } -} - -/// Copy a file, creating parent directories as needed. -/// If src is a symlink, copies the target content (follow symlinks). -fn copy_file(src: &Path, dst: &Path) { - if let Some(parent) = dst.parent() { - std::fs::create_dir_all(parent) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", parent.display())); - } - - let resolved = if src.is_symlink() { - let target = std::fs::read_link(src) - .unwrap_or_else(|e| panic!("failed to read symlink {}: {e}", src.display())); - if target.is_relative() { - src.parent().unwrap().join(target) - } else { - target - } - } else { - src.to_path_buf() - }; - - std::fs::copy(&resolved, dst).unwrap_or_else(|e| { - panic!( - "failed to copy {} -> {}: {e}", - resolved.display(), - dst.display() - ) - }); -} - -/// Copy all files/directories from one directory into another. -fn copy_dir_contents(src: &Path, dst: &Path) { - if !src.exists() { - return; - } - std::fs::create_dir_all(dst) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", dst.display())); - - for entry in std::fs::read_dir(src).expect("failed to read directory") { - let entry = entry.expect("failed to read entry"); - let entry_type = entry.file_type().expect("failed to get file type"); - let src_path = entry.path(); - let dst_path = dst.join(entry.file_name()); - - if entry_type.is_dir() { - copy_dir(&src_path, &dst_path); - } else if entry_type.is_file() || entry_type.is_symlink() { - copy_file(&src_path, &dst_path); - } - } -} diff --git a/.run/src/bin/update-version.rs b/.run/src/bin/update-version.rs deleted file mode 100644 index 2283662..0000000 --- a/.run/src/bin/update-version.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::io::Write as _; -use std::path::Path; - -use serde::Deserialize; -use tools::println_cargo_style; - -#[derive(Deserialize)] -struct VersionFile { - file: String, - pattern: String, -} - -#[derive(Deserialize)] -struct Config { - #[serde(rename = "file")] - files: Vec<VersionFile>, -} - -fn main() { - let args: Vec<String> = std::env::args().collect(); - - // Get new version - let new_ver = if args.len() > 1 { - args[1].clone() - } else { - print!("Update version to: "); - std::io::stdout().flush().unwrap(); - let mut input = String::new(); - std::io::stdin().read_line(&mut input).unwrap(); - input.trim().to_string() - }; - - if new_ver.is_empty() { - eprintln!("Error: Version cannot be empty."); - std::process::exit(1); - } - - // Read current version from root Cargo.toml's workspace.package.version - let root_cargo_path = "Cargo.toml"; - let root_cargo_content = - std::fs::read_to_string(root_cargo_path).expect("Failed to read Cargo.toml"); - let cargo_value: toml::Value = root_cargo_content - .parse() - .expect("Failed to parse Cargo.toml"); - - let current_ver = cargo_value["workspace"]["package"]["version"] - .as_str() - .expect("workspace.package.version not found in Cargo.toml") - .to_string(); - - if new_ver == current_ver { - println!("Version is already {}. Nothing to do.", current_ver); - return; - } - - println_cargo_style!("Version: {} -> {}", current_ver, new_ver); - - // Read 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 .config/version-files.toml"); - let config: Config = - toml::from_str(&config_str).expect("Failed to parse .config/version-files.toml"); - - let mut updated_count = 0; - let mut skipped_count = 0; - - for vf in &config.files { - let file_path = &vf.file; - let old_pattern = vf.pattern.replace("{VER}", ¤t_ver); - let new_pattern = vf.pattern.replace("{VER}", &new_ver); - - let content = match std::fs::read_to_string(file_path) { - Ok(c) => c, - Err(e) => { - eprintln!("Warning: Could not read {}: {}", file_path, e); - skipped_count += 1; - continue; - } - }; - - let new_content = content.replace(&old_pattern, &new_pattern); - - if new_content == content { - eprintln!( - "Warning: Pattern '{}' not found in {}", - old_pattern, file_path - ); - skipped_count += 1; - continue; - } - - std::fs::write(file_path, &new_content) - .unwrap_or_else(|e| panic!("Failed to write {}: {}", file_path, e)); - println_cargo_style!("Updated: {}", file_path); - updated_count += 1; - } - - println_cargo_style!( - "Done: {} file(s) updated, {} file(s) skipped", - updated_count, - skipped_count - ); -} diff --git a/.run/src/bin/windows-folder-hide.ps1 b/.run/src/bin/windows-folder-hide.ps1 deleted file mode 100644 index ff53202..0000000 --- a/.run/src/bin/windows-folder-hide.ps1 +++ /dev/null @@ -1,115 +0,0 @@ -$skipDirs = @('.git', '.temp', 'target', 'node_modules', '.pnpm') -$selfPath = (Get-Item -LiteralPath $MyInvocation.MyCommand.Path).Directory.FullName - -function Test-InSkipDir { - param( - [object]$Item - ) - $path = if ($Item -is [string]) { - $Item - } elseif ($Item.PSPath) { - $Item.PSPath -replace '^.*::', '' - } else { - $Item.FullName - } - - $parts = $path.Split([System.IO.Path]::DirectorySeparatorChar) - for ($i = 0; $i -lt $parts.Length - 1; $i++) { - if ($parts[$i] -in $skipDirs) { - return $true - } - } - return $false -} - -function Invoke-UnhideRecursive { - param([string]$Path) - Get-ChildItem -LiteralPath $Path -Force | ForEach-Object { - if ($_.PSIsContainer) { - if ($_.Name -in $skipDirs) { - if ($_.Attributes -band [System.IO.FileAttributes]::Hidden) { - Write-Host " -> unhiding skip directory (self only): `"$($_.FullName)`"" - $_.Attributes = $_.Attributes -bxor [System.IO.FileAttributes]::Hidden - } - return - } - Invoke-UnhideRecursive $_.FullName - } else { - if ($_.Attributes -band [System.IO.FileAttributes]::Hidden) { - Write-Host " -> unhiding: `"$($_.FullName)`"" - $_.Attributes = $_.Attributes -bxor [System.IO.FileAttributes]::Hidden - } - } - } -} - -function Test-GitPathSkippable { - param([string]$GitPath) - $parts = $GitPath.Split(@('/', '\')) - for ($i = 0; $i -lt $parts.Length - 1; $i++) { - if ($parts[$i] -in $skipDirs) { - return $true - } - } - return $false -} - -Write-Host "Step 1: Unhiding all files and directories (skipping $($skipDirs -join ', '))..." - -Invoke-UnhideRecursive -Path (Get-Location).Path - -Write-Host "Step 2: Hiding git-ignored items..." - -git ls-files --others --ignored --exclude-standard | Where-Object { - -not (Test-GitPathSkippable $_) -} | ForEach-Object { - $itemPath = $_ - Write-Host "... checking: `"$itemPath`"" - $item = Get-Item $_ -Force -ErrorAction SilentlyContinue - if (-not $item) { return } - - if ($item.FullName -eq $selfPath) { return } - - if (Test-InSkipDir $item) { - Write-Host " -> skipping (inside skip directory)" - return - } - - if ($item.PSIsContainer) { - if (-not ($item.Attributes -band [System.IO.FileAttributes]::Hidden)) { - Write-Host " -> hiding directory (non-recursive)" - $item.Attributes = $item.Attributes -bor [System.IO.FileAttributes]::Hidden - } - } else { - if (-not ($item.Attributes -band [System.IO.FileAttributes]::Hidden)) { - Write-Host " -> hiding" - $item.Attributes = $item.Attributes -bor [System.IO.FileAttributes]::Hidden - } - } -} - -Write-Host "Step 3: Hiding dot-prefixed items..." -Get-ChildItem -Path . -Force -Directory | Where-Object { $_.Name -match '^\.' } | ForEach-Object { - Write-Host "... checking: `"$($_.FullName)`"" - if (Test-InSkipDir $_) { - Write-Host " -> skipping (inside skip directory)" - return - } - if (-not ($_.Attributes -band [System.IO.FileAttributes]::Hidden)) { - Write-Host " -> hiding directory" - $_.Attributes = $_.Attributes -bor [System.IO.FileAttributes]::Hidden - } -} - -Get-ChildItem -Path . -Force -File | Where-Object { $_.Name -match '^\.' } | ForEach-Object { - if ($_.FullName -eq $selfPath) { return } - Write-Host "... checking: `"$($_.FullName)`"" - if (Test-InSkipDir $_) { - Write-Host " -> skipping (inside skip directory)" - return - } - if (-not ($_.Attributes -band [System.IO.FileAttributes]::Hidden)) { - Write-Host " -> hiding file" - $_.Attributes = $_.Attributes -bor [System.IO.FileAttributes]::Hidden - } -} diff --git a/.run/src/dependency_order.rs b/.run/src/dependency_order.rs deleted file mode 100644 index 145bdbd..0000000 --- a/.run/src/dependency_order.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::path::{Path, PathBuf}; - -/// Parse Cargo.toml content and return dependency names that start with `prefix`. -fn parse_mingling_deps(content: &str, prefix: &str) -> Vec<String> { - let value: toml::Value = match content.parse() { - Ok(v) => v, - Err(_) => return Vec::new(), - }; - - let mut names = Vec::new(); - - // Check [dependencies] - if let Some(deps) = value.get("dependencies").and_then(|d| d.as_table()) { - for key in deps.keys() { - if key.starts_with(prefix) { - names.push(key.clone()); - } - } - } - - // Check [build-dependencies] - if let Some(deps) = value.get("build-dependencies").and_then(|d| d.as_table()) { - for key in deps.keys() { - if key.starts_with(prefix) { - names.push(key.clone()); - } - } - } - - names -} - -/// Read workspace members from the root Cargo.toml. -fn get_workspace_members(workspace_root: &std::path::Path) -> Vec<String> { - let cargo_path = workspace_root.join("Cargo.toml"); - let content = match std::fs::read_to_string(&cargo_path) { - Ok(c) => c, - Err(_) => return Vec::new(), - }; - - let value: toml::Value = match content.parse() { - Ok(v) => v, - Err(_) => return Vec::new(), - }; - - value - .get("workspace") - .and_then(|w| w.get("members")) - .and_then(|m| m.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default() -} - -/// Hierarchical topological sort (process layer by layer, sort siblings alphabetically). -/// -/// `dep_map` maps each crate to the list of crates it depends on. -/// Returns the dependency order (dependent crates come first, dependents come later), -/// with crates at the same layer (which can be built in parallel) sorted alphabetically. -fn topological_sort( - all_crates: &HashSet<String>, - dep_map: &HashMap<String, Vec<String>>, -) -> Vec<String> { - // in_degree[crate] = number of remaining mingling_* dependencies not yet processed - let mut in_degree: HashMap<&str, usize> = HashMap::new(); - // reverse[dependency] = list of crates that depend on it - let mut reverse: HashMap<&str, Vec<&str>> = HashMap::new(); - - for name in all_crates { - in_degree.entry(name.as_str()).or_insert(0); - reverse.entry(name.as_str()).or_default(); - } - - for (crate_name, deps) in dep_map { - for dep in deps { - if all_crates.contains(dep.as_str()) { - reverse - .get_mut(dep.as_str()) - .unwrap() - .push(crate_name.as_str()); - *in_degree.get_mut(crate_name.as_str()).unwrap() += 1; - } - } - } - - let mut result: Vec<String> = Vec::new(); - - // Process layer by layer: all crates with in_degree == 0 in one batch form a layer - loop { - let mut current: Vec<&str> = all_crates - .iter() - .filter(|n| in_degree.get(n.as_str()).copied().unwrap_or(0) == 0) - .filter(|n| !result.iter().any(|r| r.as_str() == n.as_str())) - .map(|s| s.as_str()) - .collect(); - - if current.is_empty() { - break; - } - - current.sort(); - result.extend(current.iter().map(|s| s.to_string())); - - for &node in ¤t { - if let Some(dependents) = reverse.get(node) { - for &dependent in dependents { - if let Some(degree) = in_degree.get_mut(dependent) { - *degree -= 1; - } - } - } - } - } - - result -} - -/// Strip the `\\?\` prefix that `std::fs::canonicalize` may add on Windows. -fn strip_verbatim_prefix(p: &Path) -> PathBuf { - let s = p.to_string_lossy(); - let s_ref: &str = &s; - if let Some(rest) = s_ref.strip_prefix("\\\\?\\") { - PathBuf::from(rest) - } else { - p.to_path_buf() - } -} - -/// Find the workspace root by looking for a Cargo.toml with `[workspace]` members. -/// Starts from `start` and walks up the directory tree. -pub fn find_workspace_root(start: &std::path::Path) -> Option<PathBuf> { - let mut current = Some(strip_verbatim_prefix( - &std::fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf()), - )); - while let Some(dir) = current { - let members = get_workspace_members(&dir); - if !members.is_empty() { - return Some(dir); - } - current = dir.parent().map(|p| p.to_path_buf()); - } - None -} - -/// Output all crate paths in dependency order -#[allow(unused)] -pub fn display_dependency_order() -> Vec<PathBuf> { - let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - - let workspace_root = match find_workspace_root(&cwd) { - Some(root) => root, - None => return Vec::new(), - }; - - // Read workspace members from root Cargo.toml - let members = get_workspace_members(&workspace_root); - - // Filter to crates starting with "mingling" or "arg" - let mingling_crates: HashSet<String> = members - .into_iter() - .filter(|m| m.starts_with("mingling") || m.starts_with("arg")) - .collect(); - - if mingling_crates.is_empty() { - return Vec::new(); - } - - // Build dependency graph - let mut dep_map: HashMap<String, Vec<String>> = HashMap::new(); - - for crate_name in &mingling_crates { - let cargo_path = workspace_root.join(crate_name).join("Cargo.toml"); - let content = match std::fs::read_to_string(&cargo_path) { - Ok(c) => c, - Err(_) => { - dep_map.insert(crate_name.clone(), Vec::new()); - continue; - } - }; - let deps = parse_mingling_deps(&content, "mingling"); - // Only keep deps that are actually in our set - let filtered: Vec<String> = deps - .into_iter() - .filter(|d| mingling_crates.contains(d.as_str())) - .collect(); - dep_map.insert(crate_name.clone(), filtered); - } - - let sorted = topological_sort(&mingling_crates, &dep_map); - - sorted.into_iter().map(PathBuf::from).collect() -} diff --git a/.run/src/lib.rs b/.run/src/lib.rs deleted file mode 100644 index b17a61f..0000000 --- a/.run/src/lib.rs +++ /dev/null @@ -1,459 +0,0 @@ -pub mod dependency_order; -pub mod verify; - -use colored::Colorize; - -use std::io::IsTerminal as _; - -#[macro_export] -macro_rules! run_cmd { - ($fmt:literal, $($arg:tt)*) => { - $crate::run_cmd(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::run_cmd($cmd) - }; -} - -/// Run a shell command and capture its combined stdout+stderr output. -/// Returns `Ok(output)` on success, `Err((exit_code, stderr))` on failure. -#[macro_export] -macro_rules! run_cmd_and_capture_stderr { - ($fmt:literal, $($arg:tt)*) => { - $crate::run_cmd_capture(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::run_cmd_capture($cmd) - }; -} - -#[macro_export] -macro_rules! println_cargo_style { - ($fmt:literal, $($arg:tt)*) => { - $crate::println_cargo_style(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::println_cargo_style($cmd) - }; -} - -#[macro_export] -macro_rules! eprintln_cargo_style { - ($fmt:literal, $($arg:tt)*) => { - $crate::eprintln_cargo_style(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::eprintln_cargo_style($cmd) - }; -} - -#[macro_export] -macro_rules! wprintln_cargo_style { - ($fmt:literal, $($arg:tt)*) => { - $crate::wprintln_cargo_style(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::wprintln_cargo_style($cmd) - }; -} - -/// Print a message in cargo style format, with bold green prefix. -/// -/// # Panics -/// -/// Panics if the prefix (text before the first `:`) exceeds 12 characters. -pub fn println_cargo_style(str: impl Into<String>) { - let s = str.into(); - let (prefix, content) = if let Some(pos) = s.find(':') { - ( - s[..pos].trim().to_string(), - s[pos + 1..].trim_start().to_string(), - ) - } else { - (String::new(), s.trim().to_string()) - }; - - assert!( - prefix.len() <= 12, - "prefix length exceeds 12: '{}' has length {}", - prefix, - prefix.len() - ); - - let padding = " ".repeat(12 - prefix.len()); - - println!( - "{}{} {}", - padding, - prefix.bold().bright_green(), - content.trim() - ); -} - -pub fn eprintln_cargo_style(str: impl Into<String>) { - println!("{}: {}", "error".bold().bright_red(), str.into()); -} - -/// Print a message in cargo style format, with bold yellow prefix (warning style). -/// -/// # Panics -/// -/// Panics if the prefix (text before the first `:`) exceeds 12 characters. -pub fn wprintln_cargo_style(str: impl Into<String>) { - let s = str.into(); - let (prefix, content) = if let Some(pos) = s.find(':') { - ( - s[..pos].trim().to_string(), - s[pos + 1..].trim_start().to_string(), - ) - } else { - (String::new(), s.trim().to_string()) - }; - - assert!( - prefix.len() <= 12, - "prefix length exceeds 12: '{}' has length {}", - prefix, - prefix.len() - ); - - let padding = " ".repeat(12 - prefix.len()); - - println!( - "{}{} {}", - padding, - prefix.bold().bright_yellow(), - content.trim() - ); -} - -/// Run a shell command in the current directory and return its exit status. -/// -/// # Panics -/// -/// Panics if the shell command cannot be spawned (e.g. the shell binary is not found). -/// -/// # Errors -/// -/// Returns `Err` with the exit code if the command finishes with a non-zero exit code. -pub fn run_cmd(cmd: impl Into<String>) -> Result<(), i32> { - let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); - run_cmd_with_dir(cmd.into(), &cwd) -} - -/// Run a shell command in the specified directory and return its exit status. -/// -/// # Panics -/// -/// Panics if the shell command cannot be spawned (e.g. the shell binary is not found). -/// -/// # Errors -/// -/// Returns `Err` with the exit code if the command finishes with a non-zero exit code. -pub fn run_cmd_with_dir(cmd: impl Into<String>, dir: &std::path::Path) -> Result<(), i32> { - let shell = if cfg!(target_os = "windows") { - "powershell" - } else { - "sh" - }; - let status = std::process::Command::new(shell) - .arg("-c") - .arg(cmd.into()) - .current_dir(dir) - .status() - .expect("failed to execute command"); - - let exit_code = status.code().unwrap_or(1); - if exit_code == 0 { - Ok(()) - } else { - Err(exit_code) - } -} - -/// Run a shell command and capture its combined stdout+stderr output. -/// -/// On success returns `Ok(combined_output)`. On failure returns `Err((exit_code, stderr))`. -/// Stderr falls back to stdout if stderr is empty. -pub fn run_cmd_capture(cmd: impl Into<String>) -> Result<String, (i32, String)> { - let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); - run_cmd_capture_with_dir(cmd.into(), &cwd) -} - -/// Run a shell command in the specified directory and capture its combined stdout+stderr output. -/// -/// On success returns `Ok(combined_output)`. On failure returns `Err((exit_code, stderr))`. -/// Stderr falls back to stdout if stderr is empty. -pub fn run_cmd_capture_with_dir( - cmd: impl Into<String>, - dir: &std::path::Path, -) -> Result<String, (i32, String)> { - let shell = if cfg!(target_os = "windows") { - "powershell" - } else { - "sh" - }; - let output = std::process::Command::new(shell) - .arg("-c") - .arg(cmd.into()) - .current_dir(dir) - .output() - .expect("failed to execute command"); - - let exit_code = output.status.code().unwrap_or(1); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - // Keep both streams so a failure is never hidden: when stderr carries - // warnings, the real failure details (e.g. the failing test name and - // assertion diff) usually live on stdout and must not be dropped. - let combined = match (stdout.trim().is_empty(), stderr.trim().is_empty()) { - (false, false) => format!("{stdout}\n{stderr}"), - (false, true) => stdout, - (true, false) => stderr, - (true, true) => stdout, - }; - - if exit_code == 0 { - Ok(combined) - } else { - Err((exit_code, combined)) - } -} - -/// Extract a crate-style name from a `Cargo.toml` path. -/// -/// Examples: -/// - `mingling_core/Cargo.toml` → `mingling_core` -/// - `.` → `(root)` -pub fn crate_name_from(path: &std::path::Path) -> String { - path.parent() - .and_then(|p| p.file_name()) - .and_then(|n| n.to_str()) - .unwrap_or("(root)") - .to_string() -} - -/// Run a list of `(label_for_errors, crate_name_for_bar, shell_command)` tuples -/// in parallel with a progress bar. -/// -/// - Success: silent, the bar tracks progress: -/// ` Building [============================] 32/32: mingling_core` -/// - Failure: `pb.println()` prints the error immediately above the bar. -pub fn run_parallel(phase: &str, tasks: Vec<(String, String, String)>) -> Result<(), i32> { - let n = tasks.len(); - if n == 0 { - return Ok(()); - } - - // Cargo-style prefix: right-aligned to 12 chars, bold bright cyan - let padding = " ".repeat(12 - phase.len()); - let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan()); - - let pb = indicatif::ProgressBar::new(n as u64); - pb.set_style( - indicatif::ProgressStyle::default_bar() - .template(&format!( - "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", - styled_prefix - )) - .unwrap() - .progress_chars("=> "), - ); - pb.set_position(0); - - // Pre-extract labels for error messages - let labels: Vec<String> = tasks.iter().map(|(l, _, _)| l.clone()).collect(); - - let (tx, rx) = std::sync::mpsc::channel::<(usize, String, Result<String, (i32, String)>)>(); - - for (i, (_label, crate_name, cmd)) in tasks.into_iter().enumerate() { - let tx = tx.clone(); - std::thread::spawn(move || { - let result = run_cmd_capture(&cmd); - let _ = tx.send((i, crate_name, result)); - }); - } - drop(tx); - - let mut first_exit_code = 0; - - while let Ok((i, crate_name, result)) = rx.recv() { - pb.inc(1); - pb.set_message(crate_name); - - if let Err((code, output)) = result { - if first_exit_code == 0 { - first_exit_code = code; - } - let msg = format!( - "{}: {} failed (exit code {})", - "error".bright_red().bold(), - labels[i], - code, - ); - let mut lines = Vec::new(); - if !output.is_empty() { - lines.extend(output.lines().map(|l| format!(" {l}"))); - } - if std::io::stdout().is_terminal() { - // On a TTY, render errors through the progress bar so they - // appear above it. - pb.println(&msg); - for line in &lines { - pb.println(line); - } - } else { - // On a non-TTY (CI, piped output), `ProgressBar::println` can - // be swallowed, hiding the failure. Emit to plain stdout so the - // failure is always visible. - println!("{msg}"); - for line in &lines { - println!("{line}"); - } - } - } - } - - pb.finish_and_clear(); - - if first_exit_code != 0 { - Err(first_exit_code) - } else { - Ok(()) - } -} - -/// Run a single shell command with a progress bar, capturing its output. -/// -/// - Success: bar clears silently. -/// - Failure: error is printed above the bar, then the bar clears. -pub fn run_cmd_with_progress(phase: &str, label: &str, cmd: String) -> Result<(), i32> { - let padding = " ".repeat(12 - phase.len()); - let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan()); - - let pb = indicatif::ProgressBar::new(1); - pb.set_style( - indicatif::ProgressStyle::default_bar() - .template(&format!( - "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", - styled_prefix - )) - .unwrap() - .progress_chars("=> "), - ); - pb.set_message(label.to_owned()); - - let result = run_cmd_capture(&cmd); - pb.inc(1); - pb.finish_and_clear(); - - match result { - Ok(_) => Ok(()), - Err((code, output)) => { - eprintln_cargo_style(format!("{} failed (exit code {})", label, code)); - if !output.is_empty() { - println!("{}", output.trim_end()); - } - Err(code) - } - } -} - -/// Read `[package.metadata.docs.rs].features` from `mingling/Cargo.toml`. -/// -/// Finds the git repository root, reads `mingling/Cargo.toml`, parses it as TOML, -/// and extracts the feature list under `[package.metadata.docs.rs].features`. -/// -/// # Errors -/// -/// Returns `std::io::Error` if: -/// - The git repository root cannot be found. -/// - The manifest file cannot be read. -/// - The TOML cannot be parsed. -/// - The `[package.metadata.docs.rs].features` key is missing or empty. -pub fn read_features() -> Result<Vec<String>, std::io::Error> { - // Find git repo root - let mut current_dir = std::env::current_dir()?; - let repo_root = loop { - let git_dir = current_dir.join(".git"); - if git_dir.exists() && git_dir.is_dir() { - break Some(current_dir); - } - if !current_dir.pop() { - break None; - } - }; - let repo_root = repo_root.ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::NotFound, - "Failed to find git repository root", - ) - })?; - - let manifest_path = repo_root.join("mingling/Cargo.toml"); - if !manifest_path.exists() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Manifest not found at {}", manifest_path.display()), - )); - } - - let manifest_content = std::fs::read_to_string(&manifest_path)?; - let cargo_toml: toml::Value = manifest_content.parse().map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("Failed to parse Cargo.toml: {}", e), - ) - })?; - - let doc_features = cargo_toml - .get("package") - .and_then(|p| p.get("metadata")) - .and_then(|m| m.get("docs")) - .and_then(|d| d.get("rs")) - .and_then(|rs| rs.get("features")) - .and_then(|f| f.as_array()) - .ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::NotFound, - "[package.metadata.docs.rs] or its 'features' key not found in mingling/Cargo.toml", - ) - })?; - - let features: Vec<String> = doc_features - .iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect(); - - if features.is_empty() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "No features defined in [package.metadata.docs.rs]", - )); - } - - Ok(features) -} - -#[must_use] -pub fn cargo_tomls() -> Vec<std::path::PathBuf> { - let mut cargo_tomls = Vec::new(); - let mut dirs = vec![std::path::PathBuf::from(".")]; - while let Some(dir) = dirs.pop() { - if let Ok(entries) = std::fs::read_dir(&dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - // Skip the .run directory - if path.file_name().and_then(|n| n.to_str()) == Some(".run") { - continue; - } - dirs.push(path); - } else if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") { - cargo_tomls.push(path); - } - } - } - } - cargo_tomls -} diff --git a/.run/src/verify.rs b/.run/src/verify.rs deleted file mode 100644 index b79bb73..0000000 --- a/.run/src/verify.rs +++ /dev/null @@ -1,506 +0,0 @@ -use std::path::Path; - -use crate::println_cargo_style; - -/// Represents a parsed code block from a markdown file -#[derive(Debug, Clone)] -pub struct CodeBlock { - /// Source file path (for reporting) - pub source_file: String, - /// The line number in source file where this block starts - pub line: usize, - /// The raw Rust source code - pub code: String, - /// Feature flags extracted from `// Features: [...]` comment - pub features: Vec<String>, - /// Whether the block had an explicit `// Features:` header - pub has_features_header: bool, - /// Whether the block has `// NOT VERIFIED` to opt out of testing - pub not_verified: bool, - /// External dependencies extracted from `// Dependencies:` comments - pub external_deps: Vec<(String, String)>, - /// Whether this block has a `fn main` entry point - pub has_main: bool, - /// Whether this block has `gen_program!()` call - pub has_gen_program: bool, - /// Whether this block has `// BUILD TIME` annotation (write to build.rs, not main.rs) - pub is_build_time: bool, -} - -/// Parse all ```rust code blocks from markdown content -pub fn parse_code_blocks(content: &str, source_file: &str) -> Vec<CodeBlock> { - let mut blocks = Vec::new(); - let lines: Vec<&str> = content.lines().collect(); - let mut i = 0; - - while i < lines.len() { - if lines[i].trim() == "```rust" { - if let Some(block) = parse_single_block(&lines, i, source_file) { - blocks.push(block); - } - i += 1; - while i < lines.len() && lines[i].trim() != "```" { - i += 1; - } - } - i += 1; - } - - blocks -} - -/// Parse a single code block starting at the ```rust line -fn parse_single_block(lines: &[&str], start: usize, source_file: &str) -> Option<CodeBlock> { - let line_num = start + 1; // 1-based line number - - let mut code_lines: Vec<String> = Vec::new(); - let mut features: Vec<String> = Vec::new(); - let mut has_features_header = false; - let mut not_verified = false; - let mut external_deps: Vec<(String, String)> = Vec::new(); - let mut has_main = false; - let mut has_gen_program = false; - let mut is_build_time = false; - - let mut idx = start + 1; - let mut in_header = true; - - while idx < lines.len() { - let raw_line = lines[idx]; - let trimmed = raw_line.trim(); - - if trimmed == "```" { - break; - } - - // @@@ lines: strip the prefix and treat as regular Rust code - // These lines are hidden in the rendered docs (filtered by a docsify plugin) - // but must still compile. - if let Some(stripped) = trimmed.strip_prefix("@@@") { - in_header = false; - // Strip @@@ and optionally one following space - let code = stripped.trim_start(); - if code.contains("fn main") { - has_main = true; - } - if code.contains("gen_program!") { - has_gen_program = true; - } - code_lines.push(code.to_string()); - idx += 1; - continue; - } - - // Parse header comments - // Check for NOT VERIFIED marker - if in_header && trimmed == "// NOT VERIFIED" { - not_verified = true; - idx += 1; - continue; - } - - if in_header && trimmed == "// BUILD TIME" { - is_build_time = true; - idx += 1; - continue; - } - - if in_header && trimmed.starts_with("// ") { - if trimmed.starts_with("// Features:") { - has_features_header = true; - let feat_str = trimmed.trim_start_matches("// Features:").trim(); - if feat_str.starts_with('[') && feat_str.ends_with(']') { - let inner = &feat_str[1..feat_str.len() - 1]; - if !inner.is_empty() { - features = inner - .split(',') - .map(|s| s.trim().trim_matches('"').to_string()) - .filter(|s| !s.is_empty()) - .collect(); - } - } - idx += 1; - continue; - } - if trimmed == "// Dependencies:" { - idx += 1; - // Collect subsequent `// crate = "version"` lines - while idx < lines.len() { - let next = lines[idx].trim(); - if next == "```" { - break; - } - if next.starts_with("// ") { - let dep_line = next.trim_start_matches("// ").trim(); - if let Some((name, ver)) = dep_line.split_once(" = ") { - external_deps.push(( - name.trim().to_string(), - ver.trim().trim_matches('"').to_string(), - )); - } - idx += 1; - } else { - break; - } - } - continue; - } - } - - in_header = false; - - if raw_line.contains("fn main") { - has_main = true; - } - if raw_line.contains("gen_program!") { - has_gen_program = true; - } - - code_lines.push(raw_line.to_string()); - idx += 1; - } - - if code_lines.is_empty() { - return None; - } - - Some(CodeBlock { - source_file: source_file.to_string(), - line: line_num, - code: code_lines.join("\n"), - features, - has_features_header, - not_verified, - external_deps, - has_main, - has_gen_program, - is_build_time, - }) -} - -/// Generate a Cargo.toml for a block -/// -/// `manifest_path` is the full path to the Cargo.toml file being written; it is used to -/// compute the relative path to the `mingling` crate. -pub fn generate_cargo_toml(block: &CodeBlock, package_name: &str, manifest_path: &Path) -> String { - let features_str = if !block.features.is_empty() { - let feats: Vec<String> = block.features.iter().map(|f| format!("\"{f}\"")).collect(); - format!("features = [{}]", feats.join(", ")) - } else { - String::new() - }; - - let mut extra_deps = String::new(); - for (name, version) in &block.external_deps { - if !version.starts_with('{') { - if name == "serde" || name == "clap" { - extra_deps.push_str(&format!( - "{name} = {{ version = \"{version}\", features = [\"derive\"] }}\n" - )); - } else { - extra_deps.push_str(&format!("{name} = \"{version}\"\n")); - } - } else { - extra_deps.push_str(&format!("{name} = {version}\n")); - } - } - - let mingling_path = find_mingling_relative_path(manifest_path); - - let deps_section = if features_str.is_empty() { - format!("[dependencies]\nmingling = {{ path = \"{mingling_path}\" }}\n{extra_deps}",) - } else { - format!( - "[dependencies]\nmingling = {{ path = \"{mingling_path}\", {features_str} }}\n{extra_deps}", - ) - }; - - // Build-time blocks: mirror the declared features into [build-dependencies] - // so that build.rs can use the same feature set as the crate itself. - let build_deps_section = if block.is_build_time { - let feats_str: Vec<String> = block.features.iter().map(|f| format!("\"{f}\"")).collect(); - let build_feats = if feats_str.is_empty() { - String::new() - } else { - format!("features = [{}]", feats_str.join(", ")) - }; - format!( - "\n[build-dependencies]\nmingling = {{ path = \"{mingling_path}\", {build_feats} }}\n" - ) - } else { - String::new() - }; - - format!( - r#"[package] - name = "{package_name}" - version = "0.0.0" - edition = "2024" - -{deps_section}{build_deps_section} -[workspace] -"# - ) -} - -/// Compute the relative path from a Cargo.toml's parent directory to the `mingling` crate. -/// -/// The process current directory is expected to be the project root (where `mingling/` lives). -/// Returns a forward-slash path safe for embedding in TOML strings. -fn find_mingling_relative_path(manifest_path: &Path) -> String { - let manifest_dir = manifest_path - .parent() - .expect("manifest_path has no parent directory"); - let cwd = std::env::current_dir().expect("failed to get current directory"); - - // Strip cwd prefix to get the relative components of the manifest directory - let relative_to_root = manifest_dir.strip_prefix(&cwd).unwrap_or(manifest_dir); - let depth = relative_to_root.components().count(); - - let mut result = String::new(); - for _ in 0..depth { - result.push_str("../"); - } - result.push_str("mingling"); - result -} - -/// Generate main.rs for a block -/// -/// Automatically prepends `use mingling::prelude::*;` if the block doesn't already have it. -pub fn generate_main_rs(block: &CodeBlock) -> String { - let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n"); - - if !block.code.contains("use mingling::prelude::*;") { - output.push_str("#[allow(unused_imports)]\nuse mingling::prelude::*;\n\n"); - } - - output.push_str(&block.code); - output.push('\n'); - - if !block.has_main { - output.push_str("\nfn main() {}\n"); - } - - if !block.has_gen_program { - output.push_str("\nmingling::macros::gen_program!();\n"); - } - - output -} - -/// Generate build.rs for a build-time block -/// -/// Default: code wrapped in `fn main() { }`. -pub fn generate_build_rs(block: &CodeBlock) -> String { - let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n"); - - if block.has_main { - output.push_str(&block.code); - } else { - output.push_str("fn main() {\n"); - for line in block.code.lines() { - output.push_str(" "); - output.push_str(line); - output.push('\n'); - } - output.push_str("}\n"); - } - - output -} - -/// Build a single code block as a Cargo project. -/// -/// When `is_build_time` is true, `src_content` is written to `build.rs` instead of `src/main.rs`, -/// and a minimal `src/main.rs` stub (`fn main() {}`) is created. -pub fn build_block( - src_dir: &Path, - manifest_path: &Path, - cargo_toml: &str, - src_content: &str, - is_build_time: bool, -) -> (bool, String) { - if let Err(e) = std::fs::create_dir_all(src_dir) { - return (false, format!("mkdir: {e}")); - } - - // Write Cargo.toml - if let Err(e) = std::fs::write(manifest_path, cargo_toml) { - return (false, format!("write Cargo.toml: {e}")); - } - - if is_build_time { - // Write build.rs and a stub main.rs - let crate_dir = manifest_path.parent().unwrap(); - if let Err(e) = std::fs::write(crate_dir.join("build.rs"), src_content) { - return (false, format!("write build.rs: {e}")); - } - if let Err(e) = std::fs::write(src_dir.join("main.rs"), "fn main() {}\n") { - return (false, format!("write main.rs: {e}")); - } - } else { - // Normal: write src/main.rs - if let Err(e) = std::fs::write(src_dir.join("main.rs"), src_content) { - return (false, format!("write main.rs: {e}")); - } - } - - // Check code — inherit stderr so cargo output is real-time and colored - let shell = if cfg!(target_os = "windows") { - "powershell" - } else { - "sh" - }; - let cmd = format!( - "cargo check --color=always --manifest-path {}", - manifest_path.to_string_lossy() - ); - - let mut child = match std::process::Command::new(shell) - .arg("-c") - .arg(&cmd) - .stdout(std::process::Stdio::inherit()) - .stderr(std::process::Stdio::piped()) - .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))) - .spawn() - { - Ok(c) => c, - Err(e) => return (false, format!("spawn: {e}")), - }; - - // Read stderr (buffered, not forwarded — groups print their own output contiguously) - use std::io::BufRead; - let stderr_handle = child.stderr.take().unwrap(); - let reader = std::io::BufReader::new(stderr_handle); - let mut captured = String::new(); - for line in reader.lines() { - match line { - Ok(l) => { - captured.push_str(&l); - captured.push('\n'); - } - Err(_) => break, - } - } - - let status = child.wait().unwrap_or_else(|_| std::process::exit(1)); - let exit_code = status.code().unwrap_or(1); - - if exit_code == 0 { - (true, String::new()) - } else { - let mut last_lines: Vec<&str> = captured.lines().rev().take(20).collect(); - last_lines.reverse(); - let detail = last_lines.join("\n"); - (false, format!("exit code {exit_code}\n{detail}")) - } -} - -/// Compute a stable hash for a code block based on its dependency configuration. -/// -/// Blocks with the same features and external dependencies produce the same hash, -/// allowing them to share a compiled crate and avoid redundant recompilation. -/// -/// Hash input (all sorted for stability): -/// - Sorted mingling feature strings -/// - Sorted external dependency names -/// - Sorted external dependency versions -/// - Sorted external deps as `name=version` pairs -pub fn compute_block_hash(block: &CodeBlock) -> String { - let mut features: Vec<&str> = block.features.iter().map(|s| s.as_str()).collect(); - features.sort(); - let features_str = features.join(","); - - let mut dep_names: Vec<&str> = block - .external_deps - .iter() - .map(|(n, _)| n.as_str()) - .collect(); - dep_names.sort(); - let dep_names_str = dep_names.join(","); - - let mut dep_versions: Vec<&str> = block - .external_deps - .iter() - .map(|(_, v)| v.as_str()) - .collect(); - dep_versions.sort(); - let dep_versions_str = dep_versions.join(","); - - let mut deps: Vec<String> = block - .external_deps - .iter() - .map(|(n, v)| format!("{n}={v}")) - .collect(); - deps.sort(); - let deps_str = deps.join(","); - - let canonical = format!("{features_str}\n{dep_names_str}\n{dep_versions_str}\n{deps_str}"); - - // FNV-1a 64-bit hash — stable across runs (no random seed) - let mut hash: u64 = 0xcbf29ce484222325; - for &byte in canonical.as_bytes() { - hash ^= byte as u64; - hash = hash.wrapping_mul(0x100000001b3); - } - - format!("{:016x}", hash) -} - -/// Determine if a block should be treated as a test candidate. -/// A block is NOT testable only if it has `// NOT VERIFIED` marker. -pub fn is_block_testable(block: &CodeBlock) -> bool { - !block.not_verified -} - -/// Write a summary report -pub fn write_summary_report( - path: &Path, - title: &str, - results: &[(String, usize, bool, String)], - total: usize, - passed: usize, - failed: usize, -) { - let mut content = String::new(); - content.push_str(&format!("# {title}\n\n")); - content.push_str(&format!( - "Tested **{total}** code blocks: **{passed}** passed, **{failed}** failed.\n\n" - )); - content.push_str("## Results\n\n"); - content.push_str("| Block | File | Line | Status |\n"); - content.push_str("|-------|------|------|--------|\n"); - - for (i, (file, line, ok, _)) in results.iter().enumerate() { - let status = if *ok { "PASS" } else { "FAIL" }; - let short_file = file.rsplit('/').next().unwrap_or(file); - content.push_str(&format!( - "| {} | {} | {} | {status} |\n", - i + 1, - short_file, - line - )); - } - - let has_failures = results.iter().any(|(_, _, ok, _)| !ok); - if has_failures { - content.push_str("\n## Failed Blocks\n\n"); - for (i, (file, line, ok, err)) in results.iter().enumerate() { - if !ok { - content.push_str(&format!( - "### Block {} (`{}`, line {})\n\n```\n{err}\n```\n\n", - i + 1, - file, - line - )); - } - } - } - - std::fs::write(path, &content).unwrap_or_else(|e| { - eprintln!("Warning: failed to write {path:?}: {e}"); - }); - - println_cargo_style!("Report: written to {}", path.display()); -} |
