diff options
Diffstat (limited to '.run/src/bin')
| -rwxr-xr-x[-rw-r--r--] | .run/src/bin/build-all.sh | 0 | ||||
| -rw-r--r-- | .run/src/bin/ci.rs | 138 | ||||
| -rwxr-xr-x[-rw-r--r--] | .run/src/bin/clippy.sh | 0 | ||||
| -rw-r--r-- | .run/src/bin/deploy-api-docs.rs | 23 | ||||
| -rw-r--r-- | .run/src/bin/doc-nightly.ps1 | 6 | ||||
| -rwxr-xr-x | .run/src/bin/doc-nightly.sh | 8 | ||||
| -rw-r--r-- | .run/src/bin/doc.ps1 | 4 | ||||
| -rwxr-xr-x | .run/src/bin/doc.sh | 4 | ||||
| -rw-r--r-- | .run/src/bin/install-mling.ps1 | 2 | ||||
| -rwxr-xr-x[-rw-r--r--] | .run/src/bin/install-mling.sh | 2 | ||||
| -rw-r--r-- | .run/src/bin/package-all.rs | 461 | ||||
| -rw-r--r-- | .run/src/bin/test-all-markdown-code.rs | 2 | ||||
| -rw-r--r-- | .run/src/bin/update-version.rs | 6 |
13 files changed, 513 insertions, 143 deletions
diff --git a/.run/src/bin/build-all.sh b/.run/src/bin/build-all.sh index 2036b41..2036b41 100644..100755 --- a/.run/src/bin/build-all.sh +++ b/.run/src/bin/build-all.sh diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs index f3ae9e8..39d55eb 100644 --- a/.run/src/bin/ci.rs +++ b/.run/src/bin/ci.rs @@ -1,16 +1,14 @@ use std::io::Write as _; +use std::path::{Path, PathBuf}; use std::process::exit; +use arg_picker::{Picker, macros::arg}; use tools::{ cargo_tomls, crate_name_from, eprintln_cargo_style, println_cargo_style, run_cmd, run_parallel, }; fn get_ignore_dirs() -> Vec<String> { - vec![ - ".temp".to_string(), - "mling/res".to_string(), - "mling\\res".to_string(), - ] + vec![".temp".to_string()] } fn print_help() { @@ -35,19 +33,20 @@ fn main() { let _ = colored::control::set_virtual_terminal(true); println!("{}", include_str!("../../../docs/res/ci_banner.txt")); - let args: Vec<String> = std::env::args().collect(); + let (auto_yes, dirty, test_docs, refresh_docs, test_codes, help) = Picker::from_args() + .pick_or_default(&arg![yes: bool, 'y']) + .pick_or_default(&arg![dirty: bool]) + .pick_or_default(&arg![test_docs: bool]) + .pick_or_default(&arg![refresh_docs: bool]) + .pick_or_default(&arg![test_codes: bool]) + .pick_or_default(&arg![help: bool, 'h']) + .unwrap(); - if args.iter().any(|a| a == "-h" || a == "--help") { + if help { print_help(); return; } - let auto_yes = args.iter().any(|a| a == "-y"); - let dirty = args.iter().any(|a| a == "--dirty"); - - let test_docs = args.iter().any(|a| a == "--test-docs"); - let refresh_docs = args.iter().any(|a| a == "--refresh-docs"); - let test_codes = args.iter().any(|a| a == "--test-codes"); let any_specified = test_docs || refresh_docs || test_codes; let run_all = !any_specified; @@ -120,6 +119,9 @@ fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> { println_cargo_style!("Phase: Test all crates"); test_all()?; + + println_cargo_style!("Phase: Test arg picker"); + test_arg_picker()?; } if run_all || test_docs { @@ -140,6 +142,11 @@ fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> { exit_code = exit_code.max(code); } + println_cargo_style!("Phase: Try Build API docs"); + if let Err(code) = deploy_api_docs() { + exit_code = exit_code.max(code); + } + if exit_code != 0 { return Err(exit_code); } @@ -160,16 +167,56 @@ fn test_docs_code_blocks() -> Result<(), i32> { ) } +/// Returns the manifest paths of all workspace members (via `cargo metadata --no-deps`). +/// +/// These crates are tested/built/clipped together with `--workspace` so that +/// feature-gated code is covered, instead of relying on each crate's default features. +fn workspace_manifests() -> Vec<PathBuf> { + let Ok(output) = tools::run_cmd_capture("cargo metadata --no-deps --format-version 1") else { + return Vec::new(); + }; + let Ok(json) = serde_json::from_str::<serde_json::Value>(&output) else { + return Vec::new(); + }; + json["packages"] + .as_array() + .into_iter() + .flatten() + .filter_map(|p| p["manifest_path"].as_str().map(PathBuf::from)) + .collect() +} + +fn same_path(a: &Path, b: &Path) -> bool { + let norm = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf()); + norm(a) == norm(b) +} + fn build_all() -> Result<(), i32> { let ignore_dirs = get_ignore_dirs(); let cargo_tomls = cargo_tomls(); + let workspace_manifests = workspace_manifests(); let mut tasks = Vec::new(); + + // Workspace members: build with all documented features (same set used by cov-test) + let features_arg = doc_features_arg(); + tasks.push(( + "Build: workspace".to_string(), + "workspace".to_string(), + format!("cargo build --workspace{features_arg} --color always"), + )); + for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); + let path = cargo_toml.parent().unwrap_or(Path::new("")); let path_str = path.to_string_lossy(); if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { continue; } + if workspace_manifests + .iter() + .any(|m| same_path(m, &cargo_toml)) + { + continue; + } let label = format!("Build: {}", cargo_toml.to_string_lossy()); let crate_name = crate_name_from(&cargo_toml); let cmd = format!( @@ -184,13 +231,29 @@ fn build_all() -> Result<(), i32> { fn clippy_all() -> Result<(), i32> { let ignore_dirs = get_ignore_dirs(); let cargo_tomls = cargo_tomls(); + let workspace_manifests = workspace_manifests(); let mut tasks = Vec::new(); + + // Workspace members: clippy with all documented features + let features_arg = doc_features_arg(); + tasks.push(( + "Clippy: workspace".to_string(), + "workspace".to_string(), + format!("cargo clippy --workspace{features_arg} --color always -- -D warnings"), + )); + for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); + let path = cargo_toml.parent().unwrap_or(Path::new("")); let path_str = path.to_string_lossy(); if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { continue; } + if workspace_manifests + .iter() + .any(|m| same_path(m, &cargo_toml)) + { + continue; + } let label = format!("Clippy: {}", cargo_toml.to_string_lossy()); let crate_name = crate_name_from(&cargo_toml); let cmd = format!( @@ -202,17 +265,43 @@ fn clippy_all() -> Result<(), i32> { run_parallel("Clippy", tasks) } +/// ` --features "<docs.rs features>"` (empty string when unavailable) +fn doc_features_arg() -> String { + match tools::read_features() { + Ok(features) if !features.is_empty() => format!(" --features \"{}\"", features.join(",")), + _ => String::new(), + } +} + fn test_all() -> Result<(), i32> { let ignore_dirs = get_ignore_dirs(); let cargo_tomls = cargo_tomls(); + let workspace_manifests = workspace_manifests(); let mut tasks = Vec::new(); + + // Workspace members: test with all documented features so that feature-gated + // tests (comp/repl/picker/structural_renderer/...) are actually executed. + // `arg-picker` is excluded here and tested separately via [`test_arg_picker`]. + let features_arg = doc_features_arg(); + tasks.push(( + "Test: workspace".to_string(), + "workspace".to_string(), + format!("cargo test --workspace{features_arg} --exclude arg-picker --color always"), + )); + for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); + let path = cargo_toml.parent().unwrap_or(Path::new("")); let path_str = path.to_string_lossy(); if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { continue; } - let label = format!("Testing: {}", cargo_toml.to_string_lossy()); + if workspace_manifests + .iter() + .any(|m| same_path(m, &cargo_toml)) + { + continue; + } + let label = format!("Test: {}", cargo_toml.to_string_lossy()); let crate_name = crate_name_from(&cargo_toml); let cmd = format!( "cargo test --manifest-path {} --color always", @@ -223,6 +312,21 @@ fn test_all() -> Result<(), i32> { run_parallel("Testing", tasks) } +/// `arg-picker` is excluded from the workspace test command: when built with +/// `mingling_support` (enabled via `mingling/picker`), its README doctests +/// expand `arg!` to `::mingling::picker::PickerArg`, which is not available +/// inside the arg-picker crate itself. Test it separately with its default +/// features instead. +fn test_arg_picker() -> Result<(), i32> { + run_cmd!("cargo test -p arg-picker --color always") +} + +fn deploy_api_docs() -> Result<(), i32> { + run_cmd!( + "cargo run --manifest-path .run/Cargo.toml --color always --bin deploy-api-docs -- --docsrs" + ) +} + fn docs_refresh() -> Result<(), i32> { println_cargo_style!("Refresh: document at `./docs/`"); diff --git a/.run/src/bin/clippy.sh b/.run/src/bin/clippy.sh index b393545..b393545 100644..100755 --- a/.run/src/bin/clippy.sh +++ b/.run/src/bin/clippy.sh diff --git a/.run/src/bin/deploy-api-docs.rs b/.run/src/bin/deploy-api-docs.rs index 7aa093c..961eb04 100644 --- a/.run/src/bin/deploy-api-docs.rs +++ b/.run/src/bin/deploy-api-docs.rs @@ -1,10 +1,15 @@ 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] @@ -20,11 +25,19 @@ fn main() { std::fs::create_dir_all(&output_path).expect("Failed to create output directory"); // Build cargo doc command - let cmd = format!( - "cargo doc --no-deps --features \"{}\" -p mingling --target-dir \"{}\" --color always", - features_arg, - output_path.join("target").to_string_lossy() - ); + 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()); diff --git a/.run/src/bin/doc-nightly.ps1 b/.run/src/bin/doc-nightly.ps1 new file mode 100644 index 0000000..30d6aaf --- /dev/null +++ b/.run/src/bin/doc-nightly.ps1 @@ -0,0 +1,6 @@ +cargo +nightly rustdoc ` + --manifest-path mingling/Cargo.toml ` + --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros ` + --open ` + -- ` + --cfg docsrs diff --git a/.run/src/bin/doc-nightly.sh b/.run/src/bin/doc-nightly.sh new file mode 100755 index 0000000..944f4b3 --- /dev/null +++ b/.run/src/bin/doc-nightly.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +cargo rustdoc \ + --manifest-path mingling/Cargo.toml \ + --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros \ + --open \ + -- \ + --cfg docsrs diff --git a/.run/src/bin/doc.ps1 b/.run/src/bin/doc.ps1 index 0e55141..731168c 100644 --- a/.run/src/bin/doc.ps1 +++ b/.run/src/bin/doc.ps1 @@ -1,5 +1,5 @@ -cargo doc ` +$env:RUSTDOCFLAGS="--html-in-header mingling/arborium-header.html"; cargo doc ` --manifest-path mingling/Cargo.toml ` --no-deps ` - --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros ` + --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros,pathf ` --open diff --git a/.run/src/bin/doc.sh b/.run/src/bin/doc.sh index 014ea44..d6181d3 100755 --- a/.run/src/bin/doc.sh +++ b/.run/src/bin/doc.sh @@ -1,7 +1,7 @@ #!/bin/bash -cargo doc \ +RUSTDOCFLAGS="--html-in-header mingling/arborium-header.html" cargo doc \ --manifest-path mingling/Cargo.toml \ --no-deps \ - --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros \ + --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros,pathf \ --open diff --git a/.run/src/bin/install-mling.ps1 b/.run/src/bin/install-mling.ps1 index bebe9ff..7fba62e 100644 --- a/.run/src/bin/install-mling.ps1 +++ b/.run/src/bin/install-mling.ps1 @@ -1,4 +1,4 @@ -cargo install --path mling +cargo install --path mingling_cli New-Item -ItemType Directory -Force -Path .temp/comp | Out-Null # Copy all files containing _comp from the debug directory diff --git a/.run/src/bin/install-mling.sh b/.run/src/bin/install-mling.sh index 5f2ee7a..5b5e7b2 100644..100755 --- a/.run/src/bin/install-mling.sh +++ b/.run/src/bin/install-mling.sh @@ -1,6 +1,6 @@ #!/bin/bash -cargo install --path mling +cargo install --path mingling_cli mkdir -p .temp/comp cp .temp/target/release/*_comp.* .temp/comp/ 2>/dev/null || echo "No matching files found" diff --git a/.run/src/bin/package-all.rs b/.run/src/bin/package-all.rs index 5d7cbbb..ecdd133 100644 --- a/.run/src/bin/package-all.rs +++ b/.run/src/bin/package-all.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; use std::path::{Path, PathBuf}; use flate2::read::GzDecoder; use serde::Deserialize; use tar::Archive; +use toml::Table as TomlTable; use tools::{ dependency_order::find_workspace_root, eprintln_cargo_style, println_cargo_style, run_cmd_capture_with_dir, wprintln_cargo_style, @@ -76,14 +78,12 @@ fn main() { } // Build version map: crate_name -> version - let mut version_map: std::collections::HashMap<String, String> = - std::collections::HashMap::new(); + let mut version_map: HashMap<String, String> = HashMap::new(); for m in &members { version_map.insert(m.name.clone(), m.version.clone()); } // Collect unique member directories that need to be copied - // Compute relative path by stripping workspace_root prefix let mut member_dirs: Vec<PathBuf> = Vec::new(); for m in &members { let dir = Path::new(&m.manifest_path) @@ -104,20 +104,17 @@ fn main() { // 4. Copy files to the temp directory, preserving the workspace directory structure println_cargo_style!("Copy: project structure to .temp/pre-release/"); - // Copy .cargo directory copy_dir( &workspace_root.join(".cargo"), &pre_release_dir.join(".cargo"), ); - // Copy each member directory for dir in &member_dirs { let src = workspace_root.join(dir); let dst = pre_release_dir.join(dir); copy_dir(&src, &dst); } - // Copy root Cargo.toml and Cargo.lock copy_file( &workspace_root.join("Cargo.toml"), &pre_release_dir.join("Cargo.toml"), @@ -127,24 +124,20 @@ fn main() { &pre_release_dir.join("Cargo.lock"), ); - // 5. Replace workspace dependency paths with version numbers in the root Cargo.toml - // For workspace-member crates, keep path so cargo can resolve locally during - // `cargo package --workspace`. `cargo package` automatically converts path deps - // to version deps in the final .crate manifest. - println_cargo_style!("Patch: resolve workspace dependency versions"); + // 5. Fully resolve ALL workspace inheritance in every member's Cargo.toml, + // so each crate becomes monomorphic (no `workspace = true` references). + // Then strip `[workspace.dependencies]` and `[workspace.package]` from the + // root Cargo.toml, since they are no longer needed. + println_cargo_style!("Resolve: inline all workspace inheritance"); - let pre_release_cargo = pre_release_dir.join("Cargo.toml"); - let content = std::fs::read_to_string(&pre_release_cargo) - .unwrap_or_else(|e| panic!("failed to read {}: {e}", pre_release_cargo.display())); + // Parse workspace config from the COPIED root Cargo.toml + let root_cargo_path = pre_release_dir.join("Cargo.toml"); + let root_content = std::fs::read_to_string(&root_cargo_path) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", root_cargo_path.display())); - let patched = patch_workspace_deps(&content, &version_map); + let (ws_package, ws_deps) = parse_workspace_config(&root_content); - std::fs::write(&pre_release_cargo, &patched) - .unwrap_or_else(|e| panic!("failed to write {}: {e}", pre_release_cargo.display())); - - // Member cargo.toml files: replace direct `path = "..."` deps (pointing to other - // workspace members) with version qualification, so `cargo package` can produce - // a valid .crate without path dependencies. + // Resolve each member's Cargo.toml for dir in &member_dirs { let member_cargo = pre_release_dir.join(dir).join("Cargo.toml"); if !member_cargo.exists() { @@ -152,13 +145,18 @@ fn main() { } let member_content = std::fs::read_to_string(&member_cargo) .unwrap_or_else(|e| panic!("failed to read {}: {e}", member_cargo.display())); - let member_patched = patch_member_path_deps(&member_content, &version_map); - if member_patched != member_content { - std::fs::write(&member_cargo, &member_patched) - .unwrap_or_else(|e| panic!("failed to write {}: {e}", member_cargo.display())); - } + let resolved = + resolve_member_manifest(&member_content, dir, &ws_package, &ws_deps, &version_map); + std::fs::write(&member_cargo, &resolved) + .unwrap_or_else(|e| panic!("failed to write {}: {e}", member_cargo.display())); } + // Strip [workspace.dependencies], [workspace.package], and root [package] + // from root Cargo.toml, making it a pure virtual manifest. + let stripped = strip_workspace_config(&root_content); + std::fs::write(&root_cargo_path, &stripped) + .unwrap_or_else(|e| panic!("failed to write {}: {e}", root_cargo_path.display())); + println_cargo_style!("Package: running cargo package --workspace --no-verify"); // 6. Run cargo package in the temp directory @@ -274,6 +272,22 @@ fn main() { // Also remove Cargo.lock — standalone crate doesn't need it for publish let _ = std::fs::remove_file(target_dir.join("Cargo.lock")); + // Append an empty [workspace] section so each crate is a valid workspace root + let cargo_toml_path = target_dir.join("Cargo.toml"); + if cargo_toml_path.exists() { + let mut cargo_content = std::fs::read_to_string(&cargo_toml_path) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", cargo_toml_path.display())); + // Only append if there isn't already a [workspace] section + if !cargo_content.contains("\n[workspace]\n") + && !cargo_content.ends_with("\n[workspace]\n") + { + cargo_content.push_str("\n[workspace]\n"); + std::fs::write(&cargo_toml_path, &cargo_content).unwrap_or_else(|e| { + panic!("failed to write {}: {e}", cargo_toml_path.display()) + }); + } + } + println_cargo_style!("Export: {}", crate_dir_name); } @@ -286,57 +300,289 @@ fn main() { } } -/// Replace path-based workspace dependencies with version strings. -/// -/// Keeps the path form so that `cargo package --workspace` resolves to local workspace -/// members, but also adds `version = "..."` so the generated .crate has the correct -/// version dependency. -fn patch_workspace_deps( +/// Parse `[workspace.package]` and `[workspace.dependencies]` from the root Cargo.toml. +/// Returns (package_fields, dep_values). +fn parse_workspace_config( + content: &str, +) -> (HashMap<String, String>, HashMap<String, toml::Value>) { + let table: TomlTable = content.parse().expect("failed to parse root Cargo.toml"); + + let mut package_fields = HashMap::new(); + if let Some(workspace) = table.get("workspace").and_then(|w| w.as_table()) + && let Some(pkg_table) = workspace.get("package").and_then(|p| p.as_table()) + { + for (k, v) in pkg_table { + if let Some(s) = v.as_str() { + package_fields.insert(k.clone(), s.to_string()); + } + } + } + + let mut dep_values = HashMap::new(); + if let Some(workspace) = table.get("workspace").and_then(|w| w.as_table()) + && let Some(deps_table) = workspace.get("dependencies").and_then(|d| d.as_table()) + { + for (k, v) in deps_table { + dep_values.insert(k.clone(), v.clone()); + } + } + + (package_fields, dep_values) +} + +/// Serialize a `toml::Value` into Cargo-toml-compatible inline representation. +fn toml_value_str(v: &toml::Value) -> String { + match v { + toml::Value::String(s) => format!("\"{}\"", s), + toml::Value::Table(t) => { + let items: Vec<String> = t + .iter() + .map(|(k, val)| format!("{} = {}", k, toml_value_str(val))) + .collect(); + format!("{{ {} }}", items.join(", ")) + } + toml::Value::Array(a) => { + let items: Vec<String> = a.iter().map(toml_value_str).collect(); + format!("[{}]", items.join(", ")) + } + toml::Value::Boolean(b) => b.to_string(), + toml::Value::Integer(i) => i.to_string(), + toml::Value::Float(f) => f.to_string(), + toml::Value::Datetime(dt) => format!("\"{}\"", dt), + } +} + +/// Compute the relative path from `member_rel_dir` to `target_path`. +/// Both are relative to workspace root. +/// e.g. member_rel_dir="mingling", target_path="mingling_core" → "../mingling_core" +fn make_path_relative_to_member(target_path: &str, member_rel_dir: &Path) -> String { + if member_rel_dir.as_os_str().is_empty() || member_rel_dir == Path::new(".") { + return target_path.to_string(); + } + let depth = member_rel_dir.components().count(); + let mut result = PathBuf::new(); + for _ in 0..depth { + result.push(".."); + } + result.push(target_path); + result.to_string_lossy().to_string() +} + +/// Resolve a dependency definition from `[workspace.dependencies]` to an inline string. +/// If the definition contains a path to a workspace member, add `version = "..."` +/// and adjust the path to be relative to the member's directory. +fn resolve_dep_def( + dep_name: &str, + dep_def: &toml::Value, + member_rel_dir: &Path, + version_map: &HashMap<String, String>, +) -> String { + match dep_def { + toml::Value::String(ver) => { + format!("\"{}\"", ver) + } + toml::Value::Table(t) => { + let mut resolved = t.clone(); + let has_path = t.contains_key("path"); + let is_ws_member = version_map.contains_key(dep_name); + + // Fix path to be relative to member's directory + if has_path && let Some(path_val) = t.get("path").and_then(|v| v.as_str()) { + let rel = make_path_relative_to_member(path_val, member_rel_dir); + resolved.insert("path".to_string(), toml::Value::String(rel)); + } + + // Add version for workspace member path deps + if has_path + && is_ws_member + && let Some(version) = version_map.get(dep_name) + { + resolved.insert("version".to_string(), toml::Value::String(version.clone())); + } + + let items: Vec<String> = resolved + .iter() + .map(|(k, val)| format!("{} = {}", k, toml_value_str(val))) + .collect(); + format!("{{ {} }}", items.join(", ")) + } + _ => toml_value_str(dep_def), + } +} + +/// Merge an inline `{ workspace = true, optional = true, ... }` with the workspace definition. +/// Returns the full resolved dependency value string (without the leading `dep_name = `). +fn merge_inline_dep( + inline_rest: &str, + dep_name: &str, + dep_def: &toml::Value, + member_rel_dir: &Path, + version_map: &HashMap<String, String>, +) -> String { + // inline_rest is the part after `=`: `{ workspace = true, optional = true }` + let inner = inline_rest + .trim() + .strip_prefix('{') + .and_then(|s| s.strip_suffix('}')) + .unwrap_or(""); + + match dep_def { + toml::Value::String(ver) => { + // Workspace def is just a version string + // Collect extras: everything except `workspace = true` + let extras: Vec<&str> = inner + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty() && *s != "workspace = true") + .collect(); + + if extras.is_empty() { + format!("\"{}\"", ver) + } else { + // Serialize as inline table: version + extras + let mut parts = vec![format!("version = \"{}\"", ver)]; + parts.extend(extras.iter().map(|s| s.to_string())); + format!("{{ {} }}", parts.join(", ")) + } + } + toml::Value::Table(t) => { + // Start from workspace def + let mut merged = t.clone(); + + // Fix path to be relative to member's directory + if let Some(path_val) = t.get("path").and_then(|v| v.as_str()) { + let rel = make_path_relative_to_member(path_val, member_rel_dir); + merged.insert("path".to_string(), toml::Value::String(rel)); + } + + // If this dep is a workspace member with a path dep, add version + if t.contains_key("path") + && version_map.contains_key(dep_name) + && let Some(version) = version_map.get(dep_name) + { + merged.insert("version".to_string(), toml::Value::String(version.clone())); + } + + // Apply extra fields from the inline + for piece in inner.split(',').map(|s| s.trim()) { + let piece = piece.trim(); + if piece.is_empty() || piece == "workspace = true" { + continue; + } + // Parse `key = value` pairs + if let Some((raw_key, raw_val)) = piece.split_once('=') { + let k = raw_key.trim(); + let v = raw_val.trim(); + if k.is_empty() { + continue; + } + // Try to infer the value type + if v == "true" { + merged.insert(k.to_string(), toml::Value::Boolean(true)); + } else if v == "false" { + merged.insert(k.to_string(), toml::Value::Boolean(false)); + } else if v.starts_with('"') && v.ends_with('"') { + merged.insert( + k.to_string(), + toml::Value::String(v[1..v.len() - 1].to_string()), + ); + } else if v.starts_with('[') && v.ends_with(']') { + // Simple array parsing: strings only + let arr: Vec<toml::Value> = v[1..v.len() - 1] + .split(',') + .map(|s| { + let s = s.trim().trim_matches('"'); + toml::Value::String(s.to_string()) + }) + .collect(); + merged.insert(k.to_string(), toml::Value::Array(arr)); + } else if let Ok(n) = v.parse::<i64>() { + merged.insert(k.to_string(), toml::Value::Integer(n)); + } else if let Ok(f) = v.parse::<f64>() { + merged.insert(k.to_string(), toml::Value::Float(f)); + } else { + // Treat as string + merged.insert(k.to_string(), toml::Value::String(v.to_string())); + } + } + } + + let items: Vec<String> = merged + .iter() + .map(|(k, val)| format!("{} = {}", k, toml_value_str(val))) + .collect(); + format!("{{ {} }}", items.join(", ")) + } + _ => toml_value_str(dep_def), + } +} + +/// Resolve ALL workspace inheritance in a single member crate's Cargo.toml: +/// - `version.workspace = true` → `version = "0.3.0"` +/// - `dep.workspace = true` → inline the full definition from ws_deps +/// - `dep = { workspace = true, ... }` → merge with ws_deps definition +fn resolve_member_manifest( content: &str, - version_map: &std::collections::HashMap<String, String>, + member_rel_dir: &Path, + ws_package: &HashMap<String, String>, + ws_deps: &HashMap<String, toml::Value>, + version_map: &HashMap<String, String>, ) -> String { let mut result = String::new(); - let mut in_workspace_deps = false; + let mut in_package = false; + let mut in_section_with_deps = false; for line in content.lines() { let trimmed = line.trim(); - - if trimmed == "[workspace.dependencies]" { - in_workspace_deps = true; + let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect(); + + // Track sections + if trimmed.starts_with('[') { + in_package = trimmed == "[package]"; + in_section_with_deps = trimmed.starts_with("[dependencies") + || trimmed.starts_with("[build-dependencies") + || trimmed.starts_with("[dev-dependencies"); result.push_str(line); result.push('\n'); continue; } - // Detect end of workspace.dependencies section - if in_workspace_deps && trimmed.starts_with('[') { - in_workspace_deps = false; + // [package] section: resolve `field.workspace = true` + if in_package && trimmed.ends_with(".workspace = true") { + let key = trimmed.strip_suffix(".workspace = true").unwrap().trim(); + if let Some(value) = ws_package.get(key) { + result.push_str(&format!("{indent}{key} = \"{value}\"\n")); + continue; + } + // Also check workspace.dependencies (for fields like `version.workspace`) + // when the member has its own version field inherited from workspace.package } - if in_workspace_deps - && let Some(dep_name) = trimmed.split('=').next().map(|s| s.trim()) - && let Some(version) = version_map.get(dep_name) - { - let indent = line - .chars() - .take_while(|c| c.is_whitespace()) - .collect::<String>(); - - if trimmed.contains("path =") { - let path_value = extract_path_value(trimmed); - let patched_line: String = if let Some(pv) = path_value { - trimmed.replace( - &format!("path = \"{}\"", pv), - &format!("path = \"{}\", version = \"{}\"", pv, version), - ) - } else { - trimmed.to_string() - }; - result.push_str(&format!("{indent}{patched_line}\n")); - } else { - result.push_str(&format!("{indent}{dep_name} = \"{version}\"\n")); + // Dependency sections + if in_section_with_deps { + // Shorthand: `foo.workspace = true` + if trimmed.ends_with(".workspace = true") { + let key = trimmed.strip_suffix(".workspace = true").unwrap().trim(); + if let Some(dep_def) = ws_deps.get(key) { + let resolved = resolve_dep_def(key, dep_def, member_rel_dir, version_map); + result.push_str(&format!("{indent}{key} = {resolved}\n")); + continue; + } + } + + // Inline: `foo = { workspace = true, optional = true, ... }` + if let Some(eq_pos) = trimmed.find("= {") + && trimmed.contains("workspace = true") + { + let dep_name = trimmed[..eq_pos].trim(); + if let Some(dep_def) = ws_deps.get(dep_name) { + let after_eq = trimmed[eq_pos + 1..].trim(); + let merged = + merge_inline_dep(after_eq, dep_name, dep_def, member_rel_dir, version_map); + result.push_str(&format!("{indent}{dep_name} = {merged}\n")); + continue; + } } - continue; } result.push_str(line); @@ -346,62 +592,55 @@ fn patch_workspace_deps( result } -/// Extract the path value from a dependency line like: -/// `mingling_core = { path = "mingling_core", default-features = false }` -fn extract_path_value(line: &str) -> Option<String> { - let line = line.trim(); - if let Some(start) = line.find("path = \"") { - let after_path = &line[start + 8..]; - if let Some(end) = after_path.find('"') { - return Some(after_path[..end].to_string()); - } - } - None -} - -/// Patch a member crate's Cargo.toml: add `version = "..."` to direct `path = "..."` -/// dependencies that point to other workspace members. -fn patch_member_path_deps( - content: &str, - version_map: &std::collections::HashMap<String, String>, -) -> String { +/// Remove `[workspace.dependencies]`, `[workspace.package]`, and the root `[package]` +/// section from root Cargo.toml, making it a pure virtual manifest. +/// Keeps `[workspace]` with `members`, `resolver`, `exclude` so packaging still works. +fn strip_workspace_config(content: &str) -> String { let mut result = String::new(); - let mut in_deps = false; - let mut in_build_deps = false; + let mut in_ws_deps = false; + let mut in_ws_package = false; + let mut in_root_package = false; for line in content.lines() { let trimmed = line.trim(); - if trimmed == "[dependencies]" || trimmed.starts_with("[dependencies.") { - in_deps = true; - in_build_deps = false; - } else if trimmed == "[build-dependencies]" || trimmed.starts_with("[build-dependencies.") { - in_build_deps = true; - in_deps = false; - } else if trimmed.starts_with('[') { - in_deps = false; - in_build_deps = false; + if trimmed == "[workspace.dependencies]" { + in_ws_deps = true; + continue; } - - if (in_deps || in_build_deps) - && trimmed.contains("path = \"") - && !trimmed.contains("workspace = true") - && let Some(dep_name) = trimmed.split('=').next().map(|s| s.trim()) - && let Some(version) = version_map.get(dep_name.trim_end_matches(".workspace")) - && !trimmed.contains("version = \"") - { - let indent = line - .chars() - .take_while(|c| c.is_whitespace()) - .collect::<String>(); - let path_val = extract_path_value(trimmed).unwrap_or_default(); - let patched = trimmed.replace( - &format!("path = \"{path_val}\""), - &format!("path = \"{path_val}\", version = \"{version}\""), - ); - result.push_str(&format!("{indent}{patched}\n")); + if trimmed == "[workspace.package]" { + in_ws_package = true; continue; } + if trimmed == "[package]" && !in_ws_deps && !in_ws_package { + // Remove the root [package] section entirely (virtual manifest) + in_root_package = true; + continue; + } + + if in_ws_deps { + if trimmed.starts_with('[') { + in_ws_deps = false; + } else { + continue; + } + } + + if in_ws_package { + if trimmed.starts_with('[') { + in_ws_package = false; + } else { + continue; + } + } + + if in_root_package { + if trimmed.starts_with('[') { + in_root_package = false; + } else { + continue; + } + } result.push_str(line); result.push('\n'); diff --git a/.run/src/bin/test-all-markdown-code.rs b/.run/src/bin/test-all-markdown-code.rs index 1c0c9e2..35c8bbe 100644 --- a/.run/src/bin/test-all-markdown-code.rs +++ b/.run/src/bin/test-all-markdown-code.rs @@ -21,7 +21,7 @@ async fn main() { #[cfg(windows)] let _ = colored::control::set_virtual_terminal(true); - let config_path = PathBuf::from("verified-docs.toml"); + let config_path = PathBuf::from(".config/verified-docs.toml"); if !config_path.exists() { eprintln_cargo_style!("verified-docs.toml not found in current directory"); std::process::exit(1); diff --git a/.run/src/bin/update-version.rs b/.run/src/bin/update-version.rs index 9d595bb..2283662 100644 --- a/.run/src/bin/update-version.rs +++ b/.run/src/bin/update-version.rs @@ -56,11 +56,11 @@ fn main() { println_cargo_style!("Version: {} -> {}", current_ver, new_ver); // Read version-files.toml - let config_path = Path::new(".run").join("version-files.toml"); + let config_path = Path::new(".config").join("version-files.toml"); let config_str = - std::fs::read_to_string(&config_path).expect("Failed to read .run/version-files.toml"); + std::fs::read_to_string(&config_path).expect("Failed to read .config/version-files.toml"); let config: Config = - toml::from_str(&config_str).expect("Failed to parse .run/version-files.toml"); + toml::from_str(&config_str).expect("Failed to parse .config/version-files.toml"); let mut updated_count = 0; let mut skipped_count = 0; |
