diff options
Diffstat (limited to '.run/src/bin')
| -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 |
13 files changed, 0 insertions, 1777 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 - } -} |
