aboutsummaryrefslogtreecommitdiff
path: root/.run/src/bin
diff options
context:
space:
mode:
Diffstat (limited to '.run/src/bin')
-rwxr-xr-x[-rw-r--r--].run/src/bin/build-all.sh0
-rw-r--r--.run/src/bin/ci.rs138
-rwxr-xr-x[-rw-r--r--].run/src/bin/clippy.sh0
-rw-r--r--.run/src/bin/deploy-api-docs.rs23
-rw-r--r--.run/src/bin/doc-nightly.ps16
-rwxr-xr-x.run/src/bin/doc-nightly.sh8
-rw-r--r--.run/src/bin/doc.ps14
-rwxr-xr-x.run/src/bin/doc.sh4
-rw-r--r--.run/src/bin/install-mling.ps12
-rwxr-xr-x[-rw-r--r--].run/src/bin/install-mling.sh2
-rw-r--r--.run/src/bin/package-all.rs461
-rw-r--r--.run/src/bin/test-all-markdown-code.rs2
-rw-r--r--.run/src/bin/update-version.rs6
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;