From 37a61f67c0b57de8ab5a9e3d1454123d25b5079d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 27 Oct 2025 09:50:47 +0100 Subject: [PATCH 1/5] Add tool for visualizing crate hierarchy --- tools/crate-hierarchy-viz/Cargo.lock | 377 ++++++++++++++++++ tools/crate-hierarchy-viz/Cargo.toml | 14 + .../crate-hierarchy-viz/generate-crate-viz.sh | 37 ++ tools/crate-hierarchy-viz/src/main.rs | 339 ++++++++++++++++ 4 files changed, 767 insertions(+) create mode 100644 tools/crate-hierarchy-viz/Cargo.lock create mode 100644 tools/crate-hierarchy-viz/Cargo.toml create mode 100755 tools/crate-hierarchy-viz/generate-crate-viz.sh create mode 100644 tools/crate-hierarchy-viz/src/main.rs diff --git a/tools/crate-hierarchy-viz/Cargo.lock b/tools/crate-hierarchy-viz/Cargo.lock new file mode 100644 index 0000000000..99daf9dc69 --- /dev/null +++ b/tools/crate-hierarchy-viz/Cargo.lock @@ -0,0 +1,377 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "clap" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "crate-hierarchy-viz" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "toml", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] diff --git a/tools/crate-hierarchy-viz/Cargo.toml b/tools/crate-hierarchy-viz/Cargo.toml new file mode 100644 index 0000000000..90d02d554a --- /dev/null +++ b/tools/crate-hierarchy-viz/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "crate-hierarchy-viz" +version = "0.1.0" +edition = "2024" +description = "Tool to visualize the crate hierarchy in the Graphite workspace" + +[workspace] +members = ["."] + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" +anyhow = "1.0" +clap = { version = "4.5", features = ["derive"] } diff --git a/tools/crate-hierarchy-viz/generate-crate-viz.sh b/tools/crate-hierarchy-viz/generate-crate-viz.sh new file mode 100755 index 0000000000..38e5c0f7cc --- /dev/null +++ b/tools/crate-hierarchy-viz/generate-crate-viz.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Build the visualization tool if it doesn't exist +if [ ! -f "tools/crate-hierarchy-viz/target/debug/crate-hierarchy-viz" ]; then + echo "Building crate hierarchy visualization tool..." + cargo build +fi + +# Generate the DOT file +echo "Generating crate hierarchy graph..." +./target/debug/crate-hierarchy-viz --workspace ../.. --format dot --output crate-hierarchy.dot --exclude-dyn-any + +echo "Generating crate hierarchy graph (excluding dyn-any)..." +./target/debug/crate-hierarchy-viz --workspace ../.. --format dot --exclude-dyn-any --output crate-hierarchy-no-dyn-any.dot + +# Generate visualizations if graphviz is available +if command -v dot &> /dev/null; then + echo "Generating PNG visualizations..." + dot -Tpng crate-hierarchy.dot -o crate-hierarchy.png + dot -Tpng crate-hierarchy-no-dyn-any.dot -o crate-hierarchy-no-dyn-any.png + + echo "Generating SVG visualizations..." + dot -Tsvg crate-hierarchy.dot -o crate-hierarchy.svg + dot -Tsvg crate-hierarchy-no-dyn-any.dot -o crate-hierarchy-no-dyn-any.svg + + echo "Visualizations generated:" + echo " - crate-hierarchy.dot (GraphViz DOT format)" + echo " - crate-hierarchy.png (PNG image)" + echo " - crate-hierarchy.svg (SVG image)" + echo " - crate-hierarchy-no-dyn-any.dot (GraphViz DOT format, dyn-any excluded)" + echo " - crate-hierarchy-no-dyn-any.png (PNG image, dyn-any excluded)" + echo " - crate-hierarchy-no-dyn-any.svg (SVG image, dyn-any excluded)" +else + echo "GraphViz not found. Generated DOT file only:" + echo " - crate-hierarchy.dot" + echo "Install GraphViz to generate PNG/SVG visualizations" +fi diff --git a/tools/crate-hierarchy-viz/src/main.rs b/tools/crate-hierarchy-viz/src/main.rs new file mode 100644 index 0000000000..d2bcd17108 --- /dev/null +++ b/tools/crate-hierarchy-viz/src/main.rs @@ -0,0 +1,339 @@ +use anyhow::{Context, Result}; +use clap::Parser; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Parser)] +#[command(name = "crate-hierarchy-viz")] +#[command(about = "Visualize the crate hierarchy in the Graphite workspace")] +struct Args { + /// Workspace root directory (defaults to current directory) + #[arg(short, long)] + workspace: Option, + + /// Output format: dot, text + #[arg(short, long, default_value = "dot")] + format: String, + + /// Output file (defaults to stdout) + #[arg(short, long)] + output: Option, + + /// Include external dependencies (workspace dependencies) + #[arg(long)] + include_external: bool, + + /// Exclude dyn-any from the graph (it's used everywhere) + #[arg(long)] + exclude_dyn_any: bool, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceToml { + workspace: WorkspaceConfig, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceConfig { + members: Vec, + dependencies: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum WorkspaceDependency { + Simple(String), + Detailed { + path: Option, + version: Option, + workspace: Option, + #[serde(flatten)] + other: HashMap, + }, +} + +#[derive(Debug, Deserialize)] +struct CrateToml { + package: PackageConfig, + dependencies: Option>, +} + +#[derive(Debug, Deserialize)] +struct PackageConfig { + name: String, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum CrateDependency { + Simple(String), + Detailed { + path: Option, + workspace: Option, + version: Option, + optional: Option, + #[serde(flatten)] + other: HashMap, + }, +} + +#[derive(Debug, Clone)] +struct CrateInfo { + name: String, + path: PathBuf, + dependencies: Vec, + external_dependencies: Vec, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + let workspace_root = args.workspace.unwrap_or_else(|| std::env::current_dir().unwrap()); + let workspace_toml_path = workspace_root.join("Cargo.toml"); + + // Parse workspace Cargo.toml + let workspace_content = fs::read_to_string(&workspace_toml_path) + .with_context(|| format!("Failed to read {:?}", workspace_toml_path))?; + let workspace_toml: WorkspaceToml = toml::from_str(&workspace_content) + .with_context(|| "Failed to parse workspace Cargo.toml")?; + + // Get workspace dependencies (external crates defined at workspace level) + let workspace_deps: HashSet = workspace_toml + .workspace + .dependencies + .unwrap_or_default() + .keys() + .cloned() + .collect(); + + // Parse each member crate and build name mapping + let mut crates = Vec::new(); + let mut workspace_crate_names = HashSet::new(); + + // First pass: collect all workspace crate names + for member in &workspace_toml.workspace.members { + let crate_path = workspace_root.join(member); + let cargo_toml_path = crate_path.join("Cargo.toml"); + + if !cargo_toml_path.exists() { + eprintln!("Warning: Cargo.toml not found for member: {}", member); + continue; + } + + let crate_content = fs::read_to_string(&cargo_toml_path) + .with_context(|| format!("Failed to read {:?}", cargo_toml_path))?; + let crate_toml: CrateToml = toml::from_str(&crate_content) + .with_context(|| format!("Failed to parse Cargo.toml for {}", member))?; + + workspace_crate_names.insert(crate_toml.package.name.clone()); + } + + // Second pass: parse dependencies now that we know all workspace crate names + for member in &workspace_toml.workspace.members { + let crate_path = workspace_root.join(member); + let cargo_toml_path = crate_path.join("Cargo.toml"); + + if !cargo_toml_path.exists() { + continue; + } + + let crate_content = fs::read_to_string(&cargo_toml_path) + .with_context(|| format!("Failed to read {:?}", cargo_toml_path))?; + let crate_toml: CrateToml = toml::from_str(&crate_content) + .with_context(|| format!("Failed to parse Cargo.toml for {}", member))?; + + let mut dependencies = Vec::new(); + let mut external_dependencies = Vec::new(); + + if let Some(deps) = &crate_toml.dependencies { + for (dep_name, dep_config) in deps { + let is_workspace_crate = workspace_crate_names.contains(dep_name); + let is_workspace_dep = workspace_deps.contains(dep_name); + + let is_local_dep = match dep_config { + CrateDependency::Detailed { workspace: Some(true), .. } => is_workspace_dep, + CrateDependency::Detailed { path: Some(_), .. } => true, + CrateDependency::Simple(_) => is_workspace_dep, + _ => false, + }; + + // Check if this dependency has a different package name + let actual_dep_name = match dep_config { + CrateDependency::Detailed { other, .. } => { + // Check if there's a "package" field that renames the dependency + if let Some(toml::Value::String(package_name)) = other.get("package") { + package_name.clone() + } else { + dep_name.clone() + } + } + _ => dep_name.clone(), + }; + + let is_actual_workspace_crate = workspace_crate_names.contains(&actual_dep_name); + + if is_workspace_crate || is_actual_workspace_crate || is_local_dep { + dependencies.push(actual_dep_name); + } else { + external_dependencies.push(actual_dep_name); + } + } + } + + crates.push(CrateInfo { + name: crate_toml.package.name.clone(), + path: crate_path, + dependencies, + external_dependencies, + }); + } + + // Filter dependencies to only include workspace crates + for crate_info in &mut crates { + crate_info.dependencies.retain(|dep| workspace_crate_names.contains(dep)); + } + + // Generate output + let output = match args.format.as_str() { + "dot" => generate_dot_format(&crates, args.include_external, args.exclude_dyn_any)?, + "text" => generate_text_format(&crates, args.include_external, args.exclude_dyn_any)?, + _ => anyhow::bail!("Unsupported format: {}", args.format), + }; + + // Write output + if let Some(output_path) = args.output { + fs::write(&output_path, output) + .with_context(|| format!("Failed to write to {:?}", output_path))?; + println!("Output written to: {:?}", output_path); + } else { + print!("{}", output); + } + + Ok(()) +} + +fn generate_dot_format(crates: &[CrateInfo], include_external: bool, exclude_dyn_any: bool) -> Result { + let mut output = String::new(); + output.push_str("digraph CrateHierarchy {\n"); + output.push_str(" rankdir=LR;\n"); + output.push_str(" node [shape=box, style=\"rounded,filled\", fillcolor=lightblue];\n"); + output.push_str(" edge [color=gray];\n\n"); + + // Add subgraphs for different categories + output.push_str(" subgraph cluster_core {\n"); + output.push_str(" label=\"Core Components\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightgray;\n"); + + let core_crates: Vec<_> = crates.iter() + .filter(|c| c.name.starts_with("graphite-") || c.name == "editor") + .collect(); + + for crate_info in &core_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + output.push_str(" subgraph cluster_nodegraph {\n"); + output.push_str(" label=\"Node Graph System\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightyellow;\n"); + + let nodegraph_crates: Vec<_> = crates.iter() + .filter(|c| c.name.starts_with("graphene-") || + c.name == "graph-craft" || + c.name == "interpreted-executor" || + c.name == "wgpu-executor" || + c.name == "node-macro" || + c.name == "preprocessor") + .collect(); + + for crate_info in &nodegraph_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + output.push_str(" subgraph cluster_libraries {\n"); + output.push_str(" label=\"Libraries\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightgreen;\n"); + + let library_crates: Vec<_> = crates.iter() + .filter(|c| !c.name.starts_with("graphite-") && + !c.name.starts_with("graphene-") && + c.name != "graph-craft" && + c.name != "interpreted-executor" && + c.name != "wgpu-executor" && + c.name != "node-macro" && + c.name != "preprocessor" && + c.name != "editor") + .collect(); + + for crate_info in &library_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + // Add dependencies as edges + for crate_info in crates { + for dep in &crate_info.dependencies { + if exclude_dyn_any && dep == "dyn-any" { + continue; + } + output.push_str(&format!(" \"{}\" -> \"{}\";\n", crate_info.name, dep)); + } + + if include_external { + for dep in &crate_info.external_dependencies { + if exclude_dyn_any && dep == "dyn-any" { + continue; + } + output.push_str(&format!(" \"{}\" -> \"{}\" [style=dashed, color=red];\n", crate_info.name, dep)); + } + } + } + + output.push_str("}\n"); + Ok(output) +} + +fn generate_text_format(crates: &[CrateInfo], include_external: bool, exclude_dyn_any: bool) -> Result { + let mut output = String::new(); + output.push_str("Graphite Workspace Crate Hierarchy\n"); + output.push_str("==================================\n\n"); + + for crate_info in crates { + output.push_str(&format!("Crate: {}\n", crate_info.name)); + output.push_str(&format!("Path: {}\n", crate_info.path.display())); + + let filtered_deps: Vec<_> = crate_info.dependencies.iter() + .filter(|dep| !exclude_dyn_any || *dep != "dyn-any") + .collect(); + + if !filtered_deps.is_empty() { + output.push_str("Workspace Dependencies:\n"); + for dep in filtered_deps { + output.push_str(&format!(" - {}\n", dep)); + } + } + + if include_external { + let filtered_external_deps: Vec<_> = crate_info.external_dependencies.iter() + .filter(|dep| !exclude_dyn_any || *dep != "dyn-any") + .collect(); + + if !filtered_external_deps.is_empty() { + output.push_str("External Dependencies:\n"); + for dep in filtered_external_deps { + output.push_str(&format!(" - {}\n", dep)); + } + } + } + + output.push_str("\n"); + } + + Ok(output) +} \ No newline at end of file From 0063411896a7175ce71b4650c4164c4fb7b0a16c Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sun, 16 Nov 2025 12:30:37 +0100 Subject: [PATCH 2/5] Update crate structure --- tools/crate-hierarchy-viz/src/main.rs | 575 +++++++++++++------------- 1 file changed, 289 insertions(+), 286 deletions(-) diff --git a/tools/crate-hierarchy-viz/src/main.rs b/tools/crate-hierarchy-viz/src/main.rs index d2bcd17108..19b2f9a503 100644 --- a/tools/crate-hierarchy-viz/src/main.rs +++ b/tools/crate-hierarchy-viz/src/main.rs @@ -9,331 +9,334 @@ use std::path::{Path, PathBuf}; #[command(name = "crate-hierarchy-viz")] #[command(about = "Visualize the crate hierarchy in the Graphite workspace")] struct Args { - /// Workspace root directory (defaults to current directory) - #[arg(short, long)] - workspace: Option, + /// Workspace root directory (defaults to current directory) + #[arg(short, long)] + workspace: Option, - /// Output format: dot, text - #[arg(short, long, default_value = "dot")] - format: String, + /// Output format: dot, text + #[arg(short, long, default_value = "dot")] + format: String, - /// Output file (defaults to stdout) - #[arg(short, long)] - output: Option, + /// Output file (defaults to stdout) + #[arg(short, long)] + output: Option, - /// Include external dependencies (workspace dependencies) - #[arg(long)] - include_external: bool, + /// Include external dependencies (workspace dependencies) + #[arg(long)] + include_external: bool, - /// Exclude dyn-any from the graph (it's used everywhere) - #[arg(long)] - exclude_dyn_any: bool, + /// Exclude dyn-any from the graph (it's used everywhere) + #[arg(long)] + exclude_dyn_any: bool, } #[derive(Debug, Deserialize)] struct WorkspaceToml { - workspace: WorkspaceConfig, + workspace: WorkspaceConfig, } #[derive(Debug, Deserialize)] struct WorkspaceConfig { - members: Vec, - dependencies: Option>, + members: Vec, + dependencies: Option>, } #[derive(Debug, Deserialize)] #[serde(untagged)] enum WorkspaceDependency { - Simple(String), - Detailed { - path: Option, - version: Option, - workspace: Option, - #[serde(flatten)] - other: HashMap, - }, + Simple(String), + Detailed { + path: Option, + version: Option, + workspace: Option, + #[serde(flatten)] + other: HashMap, + }, } #[derive(Debug, Deserialize)] struct CrateToml { - package: PackageConfig, - dependencies: Option>, + package: PackageConfig, + dependencies: Option>, } #[derive(Debug, Deserialize)] struct PackageConfig { - name: String, + name: String, } #[derive(Debug, Deserialize)] #[serde(untagged)] enum CrateDependency { - Simple(String), - Detailed { - path: Option, - workspace: Option, - version: Option, - optional: Option, - #[serde(flatten)] - other: HashMap, - }, + Simple(String), + Detailed { + path: Option, + workspace: Option, + version: Option, + optional: Option, + #[serde(flatten)] + other: HashMap, + }, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] struct CrateInfo { - name: String, - path: PathBuf, - dependencies: Vec, - external_dependencies: Vec, + name: String, + path: PathBuf, + dependencies: Vec, + external_dependencies: Vec, } fn main() -> Result<()> { - let args = Args::parse(); - - let workspace_root = args.workspace.unwrap_or_else(|| std::env::current_dir().unwrap()); - let workspace_toml_path = workspace_root.join("Cargo.toml"); - - // Parse workspace Cargo.toml - let workspace_content = fs::read_to_string(&workspace_toml_path) - .with_context(|| format!("Failed to read {:?}", workspace_toml_path))?; - let workspace_toml: WorkspaceToml = toml::from_str(&workspace_content) - .with_context(|| "Failed to parse workspace Cargo.toml")?; - - // Get workspace dependencies (external crates defined at workspace level) - let workspace_deps: HashSet = workspace_toml - .workspace - .dependencies - .unwrap_or_default() - .keys() - .cloned() - .collect(); - - // Parse each member crate and build name mapping - let mut crates = Vec::new(); - let mut workspace_crate_names = HashSet::new(); - - // First pass: collect all workspace crate names - for member in &workspace_toml.workspace.members { - let crate_path = workspace_root.join(member); - let cargo_toml_path = crate_path.join("Cargo.toml"); - - if !cargo_toml_path.exists() { - eprintln!("Warning: Cargo.toml not found for member: {}", member); - continue; - } - - let crate_content = fs::read_to_string(&cargo_toml_path) - .with_context(|| format!("Failed to read {:?}", cargo_toml_path))?; - let crate_toml: CrateToml = toml::from_str(&crate_content) - .with_context(|| format!("Failed to parse Cargo.toml for {}", member))?; - - workspace_crate_names.insert(crate_toml.package.name.clone()); - } - - // Second pass: parse dependencies now that we know all workspace crate names - for member in &workspace_toml.workspace.members { - let crate_path = workspace_root.join(member); - let cargo_toml_path = crate_path.join("Cargo.toml"); - - if !cargo_toml_path.exists() { - continue; - } - - let crate_content = fs::read_to_string(&cargo_toml_path) - .with_context(|| format!("Failed to read {:?}", cargo_toml_path))?; - let crate_toml: CrateToml = toml::from_str(&crate_content) - .with_context(|| format!("Failed to parse Cargo.toml for {}", member))?; - - let mut dependencies = Vec::new(); - let mut external_dependencies = Vec::new(); - - if let Some(deps) = &crate_toml.dependencies { - for (dep_name, dep_config) in deps { - let is_workspace_crate = workspace_crate_names.contains(dep_name); - let is_workspace_dep = workspace_deps.contains(dep_name); - - let is_local_dep = match dep_config { - CrateDependency::Detailed { workspace: Some(true), .. } => is_workspace_dep, - CrateDependency::Detailed { path: Some(_), .. } => true, - CrateDependency::Simple(_) => is_workspace_dep, - _ => false, - }; - - // Check if this dependency has a different package name - let actual_dep_name = match dep_config { - CrateDependency::Detailed { other, .. } => { - // Check if there's a "package" field that renames the dependency - if let Some(toml::Value::String(package_name)) = other.get("package") { - package_name.clone() - } else { - dep_name.clone() - } - } - _ => dep_name.clone(), - }; - - let is_actual_workspace_crate = workspace_crate_names.contains(&actual_dep_name); - - if is_workspace_crate || is_actual_workspace_crate || is_local_dep { - dependencies.push(actual_dep_name); - } else { - external_dependencies.push(actual_dep_name); - } - } - } - - crates.push(CrateInfo { - name: crate_toml.package.name.clone(), - path: crate_path, - dependencies, - external_dependencies, - }); - } - - // Filter dependencies to only include workspace crates - for crate_info in &mut crates { - crate_info.dependencies.retain(|dep| workspace_crate_names.contains(dep)); - } - - // Generate output - let output = match args.format.as_str() { - "dot" => generate_dot_format(&crates, args.include_external, args.exclude_dyn_any)?, - "text" => generate_text_format(&crates, args.include_external, args.exclude_dyn_any)?, - _ => anyhow::bail!("Unsupported format: {}", args.format), - }; - - // Write output - if let Some(output_path) = args.output { - fs::write(&output_path, output) - .with_context(|| format!("Failed to write to {:?}", output_path))?; - println!("Output written to: {:?}", output_path); - } else { - print!("{}", output); - } - - Ok(()) + let args = Args::parse(); + + let workspace_root = args.workspace.unwrap_or_else(|| std::env::current_dir().unwrap()); + let workspace_toml_path = workspace_root.join("Cargo.toml"); + + // Parse workspace Cargo.toml + let workspace_content = fs::read_to_string(&workspace_toml_path).with_context(|| format!("Failed to read {:?}", workspace_toml_path))?; + let workspace_toml: WorkspaceToml = toml::from_str(&workspace_content).with_context(|| "Failed to parse workspace Cargo.toml")?; + + // Get workspace dependencies (external crates defined at workspace level) + let workspace_deps: HashSet = workspace_toml.workspace.dependencies.unwrap_or_default().keys().cloned().collect(); + + // Parse each member crate and build name mapping + let mut crates = Vec::new(); + let mut workspace_crate_names = HashSet::new(); + + // First pass: collect all workspace crate names + for member in &workspace_toml.workspace.members { + let crate_path = workspace_root.join(member); + let cargo_toml_path = crate_path.join("Cargo.toml"); + + if !cargo_toml_path.exists() { + eprintln!("Warning: Cargo.toml not found for member: {}", member); + continue; + } + + let crate_content = fs::read_to_string(&cargo_toml_path).with_context(|| format!("Failed to read {:?}", cargo_toml_path))?; + let crate_toml: CrateToml = toml::from_str(&crate_content).with_context(|| format!("Failed to parse Cargo.toml for {}", member))?; + + workspace_crate_names.insert(crate_toml.package.name.clone()); + } + + // Second pass: parse dependencies now that we know all workspace crate names + for member in &workspace_toml.workspace.members { + let crate_path = workspace_root.join(member); + let cargo_toml_path = crate_path.join("Cargo.toml"); + + if !cargo_toml_path.exists() { + continue; + } + + let crate_content = fs::read_to_string(&cargo_toml_path).with_context(|| format!("Failed to read {:?}", cargo_toml_path))?; + let crate_toml: CrateToml = toml::from_str(&crate_content).with_context(|| format!("Failed to parse Cargo.toml for {}", member))?; + + let mut dependencies = Vec::new(); + let mut external_dependencies = Vec::new(); + + if let Some(deps) = &crate_toml.dependencies { + for (dep_name, dep_config) in deps { + let is_workspace_crate = workspace_crate_names.contains(dep_name); + let is_workspace_dep = workspace_deps.contains(dep_name); + + let is_local_dep = match dep_config { + CrateDependency::Detailed { workspace: Some(true), .. } => is_workspace_dep, + CrateDependency::Detailed { path: Some(_), .. } => true, + CrateDependency::Simple(_) => is_workspace_dep, + _ => false, + }; + + // Check if this dependency has a different package name + let actual_dep_name = match dep_config { + CrateDependency::Detailed { other, .. } => { + // Check if there's a "package" field that renames the dependency + if let Some(toml::Value::String(package_name)) = other.get("package") { + package_name.clone() + } else { + dep_name.clone() + } + } + _ => dep_name.clone(), + }; + + let is_actual_workspace_crate = workspace_crate_names.contains(&actual_dep_name); + + if is_workspace_crate || is_actual_workspace_crate || is_local_dep { + dependencies.push(actual_dep_name); + } else { + external_dependencies.push(actual_dep_name); + } + } + } + + crates.push(CrateInfo { + name: crate_toml.package.name.clone(), + path: crate_path, + dependencies, + external_dependencies, + }); + } + + // Filter dependencies to only include workspace crates + for crate_info in &mut crates { + crate_info.dependencies.retain(|dep| workspace_crate_names.contains(dep)); + } + + // Generate output + let output = match args.format.as_str() { + "dot" => generate_dot_format(&crates, args.include_external, args.exclude_dyn_any)?, + "text" => generate_text_format(&crates, args.include_external, args.exclude_dyn_any)?, + _ => anyhow::bail!("Unsupported format: {}", args.format), + }; + + // Write output + if let Some(output_path) = args.output { + fs::write(&output_path, output).with_context(|| format!("Failed to write to {:?}", output_path))?; + println!("Output written to: {:?}", output_path); + } else { + print!("{}", output); + } + + Ok(()) } fn generate_dot_format(crates: &[CrateInfo], include_external: bool, exclude_dyn_any: bool) -> Result { - let mut output = String::new(); - output.push_str("digraph CrateHierarchy {\n"); - output.push_str(" rankdir=LR;\n"); - output.push_str(" node [shape=box, style=\"rounded,filled\", fillcolor=lightblue];\n"); - output.push_str(" edge [color=gray];\n\n"); - - // Add subgraphs for different categories - output.push_str(" subgraph cluster_core {\n"); - output.push_str(" label=\"Core Components\";\n"); - output.push_str(" style=filled;\n"); - output.push_str(" fillcolor=lightgray;\n"); - - let core_crates: Vec<_> = crates.iter() - .filter(|c| c.name.starts_with("graphite-") || c.name == "editor") - .collect(); - - for crate_info in &core_crates { - output.push_str(&format!(" \"{}\";\n", crate_info.name)); - } - output.push_str(" }\n\n"); - - output.push_str(" subgraph cluster_nodegraph {\n"); - output.push_str(" label=\"Node Graph System\";\n"); - output.push_str(" style=filled;\n"); - output.push_str(" fillcolor=lightyellow;\n"); - - let nodegraph_crates: Vec<_> = crates.iter() - .filter(|c| c.name.starts_with("graphene-") || - c.name == "graph-craft" || - c.name == "interpreted-executor" || - c.name == "wgpu-executor" || - c.name == "node-macro" || - c.name == "preprocessor") - .collect(); - - for crate_info in &nodegraph_crates { - output.push_str(&format!(" \"{}\";\n", crate_info.name)); - } - output.push_str(" }\n\n"); - - output.push_str(" subgraph cluster_libraries {\n"); - output.push_str(" label=\"Libraries\";\n"); - output.push_str(" style=filled;\n"); - output.push_str(" fillcolor=lightgreen;\n"); - - let library_crates: Vec<_> = crates.iter() - .filter(|c| !c.name.starts_with("graphite-") && - !c.name.starts_with("graphene-") && - c.name != "graph-craft" && - c.name != "interpreted-executor" && - c.name != "wgpu-executor" && - c.name != "node-macro" && - c.name != "preprocessor" && - c.name != "editor") - .collect(); - - for crate_info in &library_crates { - output.push_str(&format!(" \"{}\";\n", crate_info.name)); - } - output.push_str(" }\n\n"); - - // Add dependencies as edges - for crate_info in crates { - for dep in &crate_info.dependencies { - if exclude_dyn_any && dep == "dyn-any" { - continue; - } - output.push_str(&format!(" \"{}\" -> \"{}\";\n", crate_info.name, dep)); - } - - if include_external { - for dep in &crate_info.external_dependencies { - if exclude_dyn_any && dep == "dyn-any" { - continue; - } - output.push_str(&format!(" \"{}\" -> \"{}\" [style=dashed, color=red];\n", crate_info.name, dep)); - } - } - } - - output.push_str("}\n"); - Ok(output) + let mut output = String::new(); + output.push_str("digraph CrateHierarchy {\n"); + output.push_str(" rankdir=LR;\n"); + output.push_str(" node [shape=box, style=\"rounded,filled\", fillcolor=lightblue];\n"); + output.push_str(" edge [color=gray];\n\n"); + + // Add subgraphs for different categories + output.push_str(" subgraph cluster_core {\n"); + output.push_str(" label=\"Core Components\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightgray;\n"); + + let core_crates: Vec<_> = crates.iter().filter(|c| c.name.starts_with("graphite-") || c.name == "editor").collect(); + + for crate_info in &core_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + output.push_str(" subgraph cluster_nodegraph {\n"); + output.push_str(" label=\"Node Graph System\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightyellow;\n"); + + let nodegraph_crates: Vec<_> = crates + .iter() + .filter(|c| { + c.name == "graph-craft" + || c.name == "interpreted-executor" + || c.name == "wgpu-executor" + || c.name == "node-macro" + || c.name == "preprocessor" + || c.name == "rendering" + || c.name.ends_with("-types") + }) + .collect(); + + for crate_info in &nodegraph_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + output.push_str(" subgraph cluster_nodes {\n"); + output.push_str(" label=\"Nodes\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightblue;\n"); + + let node_crates: Vec<_> = crates + .iter() + .filter(|c| (c.name == "graphene-core" || c.name == "graphene-std" || c.name.contains("-nodes")) && !nodegraph_crates.contains(c)) + .collect(); + + for crate_info in &node_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + output.push_str(" subgraph cluster_libraries {\n"); + output.push_str(" label=\"Libraries\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightgreen;\n"); + + let library_crates: Vec<_> = crates + .iter() + .filter(|c| { + !c.name.starts_with("graphite-") + && !c.name.starts_with("graphene-") + && c.name != "graph-craft" + && c.name != "interpreted-executor" + && c.name != "wgpu-executor" + && c.name != "node-macro" + && c.name != "preprocessor" + && c.name != "editor" + }) + .collect(); + + for crate_info in &library_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + // Add dependencies as edges + for crate_info in crates { + for dep in &crate_info.dependencies { + if exclude_dyn_any && (dep == "dyn-any" || dep == "node-macro") { + continue; + } + output.push_str(&format!(" \"{}\" -> \"{}\";\n", crate_info.name, dep)); + } + + if include_external { + for dep in &crate_info.external_dependencies { + if exclude_dyn_any && dep == "dyn-any" { + continue; + } + output.push_str(&format!(" \"{}\" -> \"{}\" [style=dashed, color=red];\n", crate_info.name, dep)); + } + } + } + + output.push_str("}\n"); + Ok(output) } fn generate_text_format(crates: &[CrateInfo], include_external: bool, exclude_dyn_any: bool) -> Result { - let mut output = String::new(); - output.push_str("Graphite Workspace Crate Hierarchy\n"); - output.push_str("==================================\n\n"); - - for crate_info in crates { - output.push_str(&format!("Crate: {}\n", crate_info.name)); - output.push_str(&format!("Path: {}\n", crate_info.path.display())); - - let filtered_deps: Vec<_> = crate_info.dependencies.iter() - .filter(|dep| !exclude_dyn_any || *dep != "dyn-any") - .collect(); - - if !filtered_deps.is_empty() { - output.push_str("Workspace Dependencies:\n"); - for dep in filtered_deps { - output.push_str(&format!(" - {}\n", dep)); - } - } - - if include_external { - let filtered_external_deps: Vec<_> = crate_info.external_dependencies.iter() - .filter(|dep| !exclude_dyn_any || *dep != "dyn-any") - .collect(); - - if !filtered_external_deps.is_empty() { - output.push_str("External Dependencies:\n"); - for dep in filtered_external_deps { - output.push_str(&format!(" - {}\n", dep)); - } - } - } - - output.push_str("\n"); - } - - Ok(output) -} \ No newline at end of file + let mut output = String::new(); + output.push_str("Graphite Workspace Crate Hierarchy\n"); + output.push_str("==================================\n\n"); + + for crate_info in crates { + output.push_str(&format!("Crate: {}\n", crate_info.name)); + output.push_str(&format!("Path: {}\n", crate_info.path.display())); + + let filtered_deps: Vec<_> = crate_info.dependencies.iter().filter(|dep| !exclude_dyn_any || *dep != "dyn-any").collect(); + + if !filtered_deps.is_empty() { + output.push_str("Workspace Dependencies:\n"); + for dep in filtered_deps { + output.push_str(&format!(" - {}\n", dep)); + } + } + + if include_external { + let filtered_external_deps: Vec<_> = crate_info.external_dependencies.iter().filter(|dep| !exclude_dyn_any || *dep != "dyn-any").collect(); + + if !filtered_external_deps.is_empty() { + output.push_str("External Dependencies:\n"); + for dep in filtered_external_deps { + output.push_str(&format!(" - {}\n", dep)); + } + } + } + + output.push_str("\n"); + } + + Ok(output) +} From aad62eb10adebd43fb8f0c4fbe0fc91d00610674 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 27 Nov 2025 13:30:04 +0100 Subject: [PATCH 3/5] Restructure crate viz and integrate crate into workspace --- Cargo.lock | 10 ++ Cargo.toml | 19 ++- node-graph/graphene-cli/Cargo.toml | 4 +- .../{ => libraries}/wgpu-executor/Cargo.toml | 0 .../wgpu-executor/src/context.rs | 0 .../{ => libraries}/wgpu-executor/src/lib.rs | 0 .../wgpu-executor/src/shader_runtime/mod.rs | 0 .../per_pixel_adjust_runtime.rs | 0 .../wgpu-executor/src/texture_conversion.rs | 0 tools/crate-hierarchy-viz/.gitignore | 3 + tools/crate-hierarchy-viz/Cargo.lock | 109 +++----------- tools/crate-hierarchy-viz/Cargo.toml | 13 +- .../crate-hierarchy-viz/generate-crate-viz.sh | 17 +-- tools/crate-hierarchy-viz/src/main.rs | 137 ++++++------------ 14 files changed, 105 insertions(+), 207 deletions(-) rename node-graph/{ => libraries}/wgpu-executor/Cargo.toml (100%) rename node-graph/{ => libraries}/wgpu-executor/src/context.rs (100%) rename node-graph/{ => libraries}/wgpu-executor/src/lib.rs (100%) rename node-graph/{ => libraries}/wgpu-executor/src/shader_runtime/mod.rs (100%) rename node-graph/{ => libraries}/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs (100%) rename node-graph/{ => libraries}/wgpu-executor/src/texture_conversion.rs (100%) create mode 100644 tools/crate-hierarchy-viz/.gitignore diff --git a/Cargo.lock b/Cargo.lock index a4a58b23d1..f8d6a25aa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1100,6 +1100,16 @@ dependencies = [ "libc", ] +[[package]] +name = "crate-hierarchy-viz" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "toml 0.8.23", +] + [[package]] name = "crc32fast" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index 11ede35b63..78c2261178 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "node-graph/libraries/vector-types", "node-graph/libraries/graphic-types", "node-graph/libraries/rendering", + "node-graph/libraries/wgpu-executor", "node-graph/nodes/blending", "node-graph/nodes/brush", "node-graph/nodes/gcore", @@ -37,8 +38,8 @@ members = [ "node-graph/interpreted-executor", "node-graph/node-macro", "node-graph/preprocessor", - "node-graph/wgpu-executor", "proc-macros", + "tools/crate-hierarchy-viz" ] default-members = [ "editor", @@ -53,6 +54,7 @@ default-members = [ "node-graph/libraries/vector-types", "node-graph/libraries/graphic-types", "node-graph/libraries/rendering", + "node-graph/libraries/wgpu-executor", "node-graph/nodes/blending", "node-graph/nodes/brush", "node-graph/nodes/gcore", @@ -70,12 +72,22 @@ default-members = [ "node-graph/interpreted-executor", "node-graph/node-macro", "node-graph/preprocessor", - "node-graph/wgpu-executor", # blocked by https://github.com/rust-lang/cargo/issues/15890 # "proc-macros", ] resolver = "2" +[workspace.package] +rust-version = "1.88" +edition = "2024" +authors = ["Graphite Authors "] +homepage = "https://graphite.rs" +repository = "https://github.com/GraphiteEditor/Graphite" +license = "Apache-2.0" +version = "0.0.0" +readme = "README.md" +publish = false + [workspace.dependencies] # Local dependencies dyn-any = { path = "libraries/dyn-any", features = [ @@ -109,7 +121,7 @@ raster-nodes = { path = "node-graph/nodes/raster" } graphene-std = { path = "node-graph/nodes/gstd" } interpreted-executor = { path = "node-graph/interpreted-executor" } node-macro = { path = "node-graph/node-macro" } -wgpu-executor = { path = "node-graph/wgpu-executor" } +wgpu-executor = { path = "node-graph/libraries/wgpu-executor" } graphite-proc-macros = { path = "proc-macros" } # Workspace dependencies @@ -258,3 +270,4 @@ debug = true [patch.crates-io] # Force cargo to use only one version of the dpi crate (vendoring breaks without this) dpi = { git = "https://github.com/rust-windowing/winit.git" } + diff --git a/node-graph/graphene-cli/Cargo.toml b/node-graph/graphene-cli/Cargo.toml index f707d164ca..5210adee00 100644 --- a/node-graph/graphene-cli/Cargo.toml +++ b/node-graph/graphene-cli/Cargo.toml @@ -27,9 +27,7 @@ wgpu = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } clap = { workspace = true, features = ["cargo", "derive"] } image = { workspace = true } - -# Optional local dependencies -wgpu-executor = { path = "../wgpu-executor", optional = true } +wgpu-executor = { workspace = true, optional = true } [package.metadata.cargo-shear] ignored = ["wgpu-executor"] diff --git a/node-graph/wgpu-executor/Cargo.toml b/node-graph/libraries/wgpu-executor/Cargo.toml similarity index 100% rename from node-graph/wgpu-executor/Cargo.toml rename to node-graph/libraries/wgpu-executor/Cargo.toml diff --git a/node-graph/wgpu-executor/src/context.rs b/node-graph/libraries/wgpu-executor/src/context.rs similarity index 100% rename from node-graph/wgpu-executor/src/context.rs rename to node-graph/libraries/wgpu-executor/src/context.rs diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs similarity index 100% rename from node-graph/wgpu-executor/src/lib.rs rename to node-graph/libraries/wgpu-executor/src/lib.rs diff --git a/node-graph/wgpu-executor/src/shader_runtime/mod.rs b/node-graph/libraries/wgpu-executor/src/shader_runtime/mod.rs similarity index 100% rename from node-graph/wgpu-executor/src/shader_runtime/mod.rs rename to node-graph/libraries/wgpu-executor/src/shader_runtime/mod.rs diff --git a/node-graph/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs similarity index 100% rename from node-graph/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs rename to node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs diff --git a/node-graph/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs similarity index 100% rename from node-graph/wgpu-executor/src/texture_conversion.rs rename to node-graph/libraries/wgpu-executor/src/texture_conversion.rs diff --git a/tools/crate-hierarchy-viz/.gitignore b/tools/crate-hierarchy-viz/.gitignore new file mode 100644 index 0000000000..15a65bec1e --- /dev/null +++ b/tools/crate-hierarchy-viz/.gitignore @@ -0,0 +1,3 @@ +*.dot +*.png +*.svg diff --git a/tools/crate-hierarchy-viz/Cargo.lock b/tools/crate-hierarchy-viz/Cargo.lock index 99daf9dc69..395e09afcd 100644 --- a/tools/crate-hierarchy-viz/Cargo.lock +++ b/tools/crate-hierarchy-viz/Cargo.lock @@ -34,18 +34,18 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", @@ -60,9 +60,9 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "clap" -version = "4.5.50" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -70,9 +70,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.50" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -122,9 +122,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -134,9 +134,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -225,9 +225,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -277,9 +277,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "utf8parse" @@ -295,83 +295,18 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] diff --git a/tools/crate-hierarchy-viz/Cargo.toml b/tools/crate-hierarchy-viz/Cargo.toml index 90d02d554a..be5ace77e5 100644 --- a/tools/crate-hierarchy-viz/Cargo.toml +++ b/tools/crate-hierarchy-viz/Cargo.toml @@ -1,14 +1,11 @@ [package] name = "crate-hierarchy-viz" -version = "0.1.0" -edition = "2024" description = "Tool to visualize the crate hierarchy in the Graphite workspace" - -[workspace] -members = ["."] +edition.workspace = true +version.workspace = true [dependencies] -serde = { version = "1.0", features = ["derive"] } +serde = { workspace = true } +clap = { workspace = true, features = ["derive"] } toml = "0.8" -anyhow = "1.0" -clap = { version = "4.5", features = ["derive"] } +anyhow = { workspace = true } diff --git a/tools/crate-hierarchy-viz/generate-crate-viz.sh b/tools/crate-hierarchy-viz/generate-crate-viz.sh index 38e5c0f7cc..a36f3c0d53 100755 --- a/tools/crate-hierarchy-viz/generate-crate-viz.sh +++ b/tools/crate-hierarchy-viz/generate-crate-viz.sh @@ -1,35 +1,26 @@ #!/usr/bin/env bash -# Build the visualization tool if it doesn't exist -if [ ! -f "tools/crate-hierarchy-viz/target/debug/crate-hierarchy-viz" ]; then - echo "Building crate hierarchy visualization tool..." - cargo build -fi +# Build the visualization tool first to explain the wait time +echo "Building crate hierarchy visualization tool..." +cargo build # Generate the DOT file echo "Generating crate hierarchy graph..." -./target/debug/crate-hierarchy-viz --workspace ../.. --format dot --output crate-hierarchy.dot --exclude-dyn-any +cargo run -- --workspace ../.. --output crate-hierarchy.dot -echo "Generating crate hierarchy graph (excluding dyn-any)..." -./target/debug/crate-hierarchy-viz --workspace ../.. --format dot --exclude-dyn-any --output crate-hierarchy-no-dyn-any.dot # Generate visualizations if graphviz is available if command -v dot &> /dev/null; then echo "Generating PNG visualizations..." dot -Tpng crate-hierarchy.dot -o crate-hierarchy.png - dot -Tpng crate-hierarchy-no-dyn-any.dot -o crate-hierarchy-no-dyn-any.png echo "Generating SVG visualizations..." dot -Tsvg crate-hierarchy.dot -o crate-hierarchy.svg - dot -Tsvg crate-hierarchy-no-dyn-any.dot -o crate-hierarchy-no-dyn-any.svg echo "Visualizations generated:" echo " - crate-hierarchy.dot (GraphViz DOT format)" echo " - crate-hierarchy.png (PNG image)" echo " - crate-hierarchy.svg (SVG image)" - echo " - crate-hierarchy-no-dyn-any.dot (GraphViz DOT format, dyn-any excluded)" - echo " - crate-hierarchy-no-dyn-any.png (PNG image, dyn-any excluded)" - echo " - crate-hierarchy-no-dyn-any.svg (SVG image, dyn-any excluded)" else echo "GraphViz not found. Generated DOT file only:" echo " - crate-hierarchy.dot" diff --git a/tools/crate-hierarchy-viz/src/main.rs b/tools/crate-hierarchy-viz/src/main.rs index 19b2f9a503..1f5355f4f7 100644 --- a/tools/crate-hierarchy-viz/src/main.rs +++ b/tools/crate-hierarchy-viz/src/main.rs @@ -1,9 +1,9 @@ use anyhow::{Context, Result}; use clap::Parser; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::collections::{HashMap, HashSet}; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; #[derive(Parser)] #[command(name = "crate-hierarchy-viz")] @@ -13,21 +13,9 @@ struct Args { #[arg(short, long)] workspace: Option, - /// Output format: dot, text - #[arg(short, long, default_value = "dot")] - format: String, - /// Output file (defaults to stdout) #[arg(short, long)] output: Option, - - /// Include external dependencies (workspace dependencies) - #[arg(long)] - include_external: bool, - - /// Exclude dyn-any from the graph (it's used everywhere) - #[arg(long)] - exclude_dyn_any: bool, } #[derive(Debug, Deserialize)] @@ -41,16 +29,16 @@ struct WorkspaceConfig { dependencies: Option>, } +/// Represents a workspace-level dependency in Cargo.toml +/// The Simple variant's String is needed for serde deserialization but never read directly #[derive(Debug, Deserialize)] #[serde(untagged)] +#[allow(dead_code)] enum WorkspaceDependency { Simple(String), Detailed { - path: Option, - version: Option, - workspace: Option, #[serde(flatten)] - other: HashMap, + _other: HashMap, }, } @@ -65,15 +53,16 @@ struct PackageConfig { name: String, } +/// Represents a crate-level dependency in Cargo.toml +/// The Simple variant's String is needed for serde deserialization but never read directly #[derive(Debug, Deserialize)] #[serde(untagged)] +#[allow(dead_code)] enum CrateDependency { Simple(String), Detailed { path: Option, workspace: Option, - version: Option, - optional: Option, #[serde(flatten)] other: HashMap, }, @@ -184,11 +173,7 @@ fn main() -> Result<()> { } // Generate output - let output = match args.format.as_str() { - "dot" => generate_dot_format(&crates, args.include_external, args.exclude_dyn_any)?, - "text" => generate_text_format(&crates, args.include_external, args.exclude_dyn_any)?, - _ => anyhow::bail!("Unsupported format: {}", args.format), - }; + let output = generate_dot_format(&crates)?; // Write output if let Some(output_path) = args.output { @@ -201,7 +186,7 @@ fn main() -> Result<()> { Ok(()) } -fn generate_dot_format(crates: &[CrateInfo], include_external: bool, exclude_dyn_any: bool) -> Result { +fn generate_dot_format(crates: &[CrateInfo]) -> Result { let mut output = String::new(); output.push_str("digraph CrateHierarchy {\n"); output.push_str(" rankdir=LR;\n"); @@ -214,7 +199,10 @@ fn generate_dot_format(crates: &[CrateInfo], include_external: bool, exclude_dyn output.push_str(" style=filled;\n"); output.push_str(" fillcolor=lightgray;\n"); - let core_crates: Vec<_> = crates.iter().filter(|c| c.name.starts_with("graphite-") || c.name == "editor").collect(); + let core_crates: Vec<_> = crates + .iter() + .filter(|c| (c.name.starts_with("graphite-") || c.name == "editor" || c.name == "graphene-cli") && !c.name.contains("desktop")) + .collect(); for crate_info in &core_crates { output.push_str(&format!(" \"{}\";\n", crate_info.name)); @@ -227,19 +215,29 @@ fn generate_dot_format(crates: &[CrateInfo], include_external: bool, exclude_dyn output.push_str(" fillcolor=lightyellow;\n"); let nodegraph_crates: Vec<_> = crates + .iter() + .filter(|c| c.name == "graph-craft" || c.name == "interpreted-executor" || c.name == "node-macro" || c.name == "preprocessor" || c.name == "graphene-cli") + .collect(); + + for crate_info in &nodegraph_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + output.push_str(" subgraph cluster_node_libraries {\n"); + output.push_str(" label=\"Node Graph Libraries\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightcyan;\n"); + + let node_library_crates: Vec<_> = crates .iter() .filter(|c| { - c.name == "graph-craft" - || c.name == "interpreted-executor" - || c.name == "wgpu-executor" - || c.name == "node-macro" - || c.name == "preprocessor" - || c.name == "rendering" - || c.name.ends_with("-types") + let path_str = c.path.to_string_lossy(); + path_str.contains("node-graph/libraries") }) .collect(); - for crate_info in &nodegraph_crates { + for crate_info in &node_library_crates { output.push_str(&format!(" \"{}\";\n", crate_info.name)); } output.push_str(" }\n\n"); @@ -251,7 +249,10 @@ fn generate_dot_format(crates: &[CrateInfo], include_external: bool, exclude_dyn let node_crates: Vec<_> = crates .iter() - .filter(|c| (c.name == "graphene-core" || c.name == "graphene-std" || c.name.contains("-nodes")) && !nodegraph_crates.contains(c)) + .filter(|c| { + let path_str = c.path.to_string_lossy(); + path_str.contains("node-graph/nodes") + }) .collect(); for crate_info in &node_crates { @@ -259,26 +260,20 @@ fn generate_dot_format(crates: &[CrateInfo], include_external: bool, exclude_dyn } output.push_str(" }\n\n"); - output.push_str(" subgraph cluster_libraries {\n"); - output.push_str(" label=\"Libraries\";\n"); + output.push_str(" subgraph cluster_desktop{\n"); + output.push_str(" label=\"Desktop\";\n"); output.push_str(" style=filled;\n"); output.push_str(" fillcolor=lightgreen;\n"); - let library_crates: Vec<_> = crates + let desktop_crates: Vec<_> = crates .iter() .filter(|c| { - !c.name.starts_with("graphite-") - && !c.name.starts_with("graphene-") - && c.name != "graph-craft" - && c.name != "interpreted-executor" - && c.name != "wgpu-executor" - && c.name != "node-macro" - && c.name != "preprocessor" - && c.name != "editor" + let path_str = c.path.to_string_lossy(); + path_str.contains("desktop") }) .collect(); - for crate_info in &library_crates { + for crate_info in &desktop_crates { output.push_str(&format!(" \"{}\";\n", crate_info.name)); } output.push_str(" }\n\n"); @@ -286,57 +281,13 @@ fn generate_dot_format(crates: &[CrateInfo], include_external: bool, exclude_dyn // Add dependencies as edges for crate_info in crates { for dep in &crate_info.dependencies { - if exclude_dyn_any && (dep == "dyn-any" || dep == "node-macro") { + if dep == "dyn-any" || dep == "node-macro" { continue; } output.push_str(&format!(" \"{}\" -> \"{}\";\n", crate_info.name, dep)); } - - if include_external { - for dep in &crate_info.external_dependencies { - if exclude_dyn_any && dep == "dyn-any" { - continue; - } - output.push_str(&format!(" \"{}\" -> \"{}\" [style=dashed, color=red];\n", crate_info.name, dep)); - } - } } output.push_str("}\n"); Ok(output) } - -fn generate_text_format(crates: &[CrateInfo], include_external: bool, exclude_dyn_any: bool) -> Result { - let mut output = String::new(); - output.push_str("Graphite Workspace Crate Hierarchy\n"); - output.push_str("==================================\n\n"); - - for crate_info in crates { - output.push_str(&format!("Crate: {}\n", crate_info.name)); - output.push_str(&format!("Path: {}\n", crate_info.path.display())); - - let filtered_deps: Vec<_> = crate_info.dependencies.iter().filter(|dep| !exclude_dyn_any || *dep != "dyn-any").collect(); - - if !filtered_deps.is_empty() { - output.push_str("Workspace Dependencies:\n"); - for dep in filtered_deps { - output.push_str(&format!(" - {}\n", dep)); - } - } - - if include_external { - let filtered_external_deps: Vec<_> = crate_info.external_dependencies.iter().filter(|dep| !exclude_dyn_any || *dep != "dyn-any").collect(); - - if !filtered_external_deps.is_empty() { - output.push_str("External Dependencies:\n"); - for dep in filtered_external_deps { - output.push_str(&format!(" - {}\n", dep)); - } - } - } - - output.push_str("\n"); - } - - Ok(output) -} From 59db59c556ad456dcb53ce5ab9337590a784be74 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Fri, 28 Nov 2025 00:20:16 +0100 Subject: [PATCH 4/5] Remove transitive dependency edges --- tools/crate-hierarchy-viz/Cargo.toml | 2 ++ tools/crate-hierarchy-viz/src/main.rs | 44 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/tools/crate-hierarchy-viz/Cargo.toml b/tools/crate-hierarchy-viz/Cargo.toml index be5ace77e5..d9199f4bcb 100644 --- a/tools/crate-hierarchy-viz/Cargo.toml +++ b/tools/crate-hierarchy-viz/Cargo.toml @@ -3,6 +3,8 @@ name = "crate-hierarchy-viz" description = "Tool to visualize the crate hierarchy in the Graphite workspace" edition.workspace = true version.workspace = true +license.workspace = true +authors.workspace = true [dependencies] serde = { workspace = true } diff --git a/tools/crate-hierarchy-viz/src/main.rs b/tools/crate-hierarchy-viz/src/main.rs index 1f5355f4f7..254b8704c8 100644 --- a/tools/crate-hierarchy-viz/src/main.rs +++ b/tools/crate-hierarchy-viz/src/main.rs @@ -76,6 +76,47 @@ struct CrateInfo { external_dependencies: Vec, } +/// Remove transitive dependencies from the crate list. +/// If A depends on B and C, and B depends on C, then A->C is removed. +fn remove_transitive_dependencies(crates: &mut [CrateInfo]) { + // Build a map from crate name to its dependencies for quick lookup + let dep_map: HashMap> = crates + .iter() + .map(|c| (c.name.clone(), c.dependencies.iter().cloned().collect())) + .collect(); + + // For each crate, compute which dependencies are reachable through other dependencies + for crate_info in crates.iter_mut() { + let mut transitive_deps = HashSet::new(); + + // For each direct dependency, find all its transitive dependencies + for direct_dep in &crate_info.dependencies { + // Recursively collect all transitive dependencies of this direct dependency + let mut visited = HashSet::new(); + collect_all_dependencies(direct_dep, &dep_map, &mut visited); + // Remove the direct dependency itself from the visited set + visited.remove(direct_dep); + transitive_deps.extend(visited); + } + + // Remove dependencies that are transitive + crate_info.dependencies.retain(|dep| !transitive_deps.contains(dep)); + } +} + +/// Recursively collect all dependencies of a crate +fn collect_all_dependencies(crate_name: &str, dep_map: &HashMap>, visited: &mut HashSet) { + if !visited.insert(crate_name.to_string()) { + return; // Already visited, avoid cycles + } + + if let Some(deps) = dep_map.get(crate_name) { + for dep in deps { + collect_all_dependencies(dep, dep_map, visited); + } + } +} + fn main() -> Result<()> { let args = Args::parse(); @@ -172,6 +213,9 @@ fn main() -> Result<()> { crate_info.dependencies.retain(|dep| workspace_crate_names.contains(dep)); } + // Remove transitive dependencies + remove_transitive_dependencies(&mut crates); + // Generate output let output = generate_dot_format(&crates)?; From 282e5e668e1ad5c754b5ed5815931aac8d22dbc7 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Fri, 28 Nov 2025 11:08:21 +0100 Subject: [PATCH 5/5] Move png / svg creation into the rust binary --- .nix/flake.nix | 3 + tools/crate-hierarchy-viz/Cargo.lock | 312 ------------------ .../crate-hierarchy-viz/generate-crate-viz.sh | 28 -- tools/crate-hierarchy-viz/src/main.rs | 94 +++++- 4 files changed, 82 insertions(+), 355 deletions(-) delete mode 100644 tools/crate-hierarchy-viz/Cargo.lock delete mode 100755 tools/crate-hierarchy-viz/generate-crate-viz.sh diff --git a/.nix/flake.nix b/.nix/flake.nix index d2859d0eee..83f8c448b2 100644 --- a/.nix/flake.nix +++ b/.nix/flake.nix @@ -98,6 +98,9 @@ pkgs.gnuplot pkgs.samply pkgs.cargo-flamegraph + + # Plotting tools + pkgs.graphviz ]; all = desktop ++ frontend ++ dev; }; diff --git a/tools/crate-hierarchy-viz/Cargo.lock b/tools/crate-hierarchy-viz/Cargo.lock deleted file mode 100644 index 395e09afcd..0000000000 --- a/tools/crate-hierarchy-viz/Cargo.lock +++ /dev/null @@ -1,312 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "clap" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "crate-hierarchy-viz" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "serde", - "toml", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] diff --git a/tools/crate-hierarchy-viz/generate-crate-viz.sh b/tools/crate-hierarchy-viz/generate-crate-viz.sh deleted file mode 100755 index a36f3c0d53..0000000000 --- a/tools/crate-hierarchy-viz/generate-crate-viz.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -# Build the visualization tool first to explain the wait time -echo "Building crate hierarchy visualization tool..." -cargo build - -# Generate the DOT file -echo "Generating crate hierarchy graph..." -cargo run -- --workspace ../.. --output crate-hierarchy.dot - - -# Generate visualizations if graphviz is available -if command -v dot &> /dev/null; then - echo "Generating PNG visualizations..." - dot -Tpng crate-hierarchy.dot -o crate-hierarchy.png - - echo "Generating SVG visualizations..." - dot -Tsvg crate-hierarchy.dot -o crate-hierarchy.svg - - echo "Visualizations generated:" - echo " - crate-hierarchy.dot (GraphViz DOT format)" - echo " - crate-hierarchy.png (PNG image)" - echo " - crate-hierarchy.svg (SVG image)" -else - echo "GraphViz not found. Generated DOT file only:" - echo " - crate-hierarchy.dot" - echo "Install GraphViz to generate PNG/SVG visualizations" -fi diff --git a/tools/crate-hierarchy-viz/src/main.rs b/tools/crate-hierarchy-viz/src/main.rs index 254b8704c8..f964e8fb27 100644 --- a/tools/crate-hierarchy-viz/src/main.rs +++ b/tools/crate-hierarchy-viz/src/main.rs @@ -1,9 +1,20 @@ -use anyhow::{Context, Result}; -use clap::Parser; +use anyhow::{Context, Result, anyhow}; +use clap::{Parser, ValueEnum}; use serde::Deserialize; use std::collections::{HashMap, HashSet}; use std::fs; use std::path::PathBuf; +use std::process::Command; + +#[derive(Debug, Clone, ValueEnum)] +enum OutputFormat { + /// Output DOT format (GraphViz) + Dot, + /// Output PNG image (requires GraphViz) + Png, + /// Output SVG image (requires GraphViz) + Svg, +} #[derive(Parser)] #[command(name = "crate-hierarchy-viz")] @@ -13,9 +24,13 @@ struct Args { #[arg(short, long)] workspace: Option, - /// Output file (defaults to stdout) + /// Output file (defaults to stdout for DOT format, required for PNG/SVG) #[arg(short, long)] output: Option, + + /// Output format + #[arg(short, long, value_enum, default_value = "dot")] + format: OutputFormat, } #[derive(Debug, Deserialize)] @@ -80,10 +95,7 @@ struct CrateInfo { /// If A depends on B and C, and B depends on C, then A->C is removed. fn remove_transitive_dependencies(crates: &mut [CrateInfo]) { // Build a map from crate name to its dependencies for quick lookup - let dep_map: HashMap> = crates - .iter() - .map(|c| (c.name.clone(), c.dependencies.iter().cloned().collect())) - .collect(); + let dep_map: HashMap> = crates.iter().map(|c| (c.name.clone(), c.dependencies.iter().cloned().collect())).collect(); // For each crate, compute which dependencies are reachable through other dependencies for crate_info in crates.iter_mut() { @@ -216,15 +228,67 @@ fn main() -> Result<()> { // Remove transitive dependencies remove_transitive_dependencies(&mut crates); - // Generate output - let output = generate_dot_format(&crates)?; + // Generate DOT format + let dot_content = generate_dot_format(&crates)?; + + // Handle output based on format + match args.format { + OutputFormat::Dot => { + // Write DOT output + if let Some(output_path) = args.output { + fs::write(&output_path, &dot_content).with_context(|| format!("Failed to write to {:?}", output_path))?; + println!("DOT output written to: {:?}", output_path); + } else { + print!("{}", dot_content); + } + } + OutputFormat::Png | OutputFormat::Svg => { + // Require output file for PNG/SVG + let output_path = args.output.ok_or_else(|| anyhow!("Output file (-o/--output) is required for PNG/SVG formats"))?; + + // Check if dot command is available + let dot_check = Command::new("dot").arg("-V").output(); + if dot_check.is_err() || !dot_check.as_ref().unwrap().status.success() { + return Err(anyhow!( + "GraphViz 'dot' command not found. Please install GraphViz to generate PNG/SVG output.\n\ + On Ubuntu/Debian: sudo apt-get install graphviz\n\ + On macOS: brew install graphviz\n\ + On Windows: Download from https://graphviz.org/download/" + )); + } + + // Determine the format argument for dot + let format_arg = match args.format { + OutputFormat::Png => "png", + OutputFormat::Svg => "svg", + _ => unreachable!(), + }; + + // Run dot command to generate the output + let mut dot_process = Command::new("dot") + .arg(format!("-T{}", format_arg)) + .arg("-o") + .arg(&output_path) + .stdin(std::process::Stdio::piped()) + .spawn() + .with_context(|| "Failed to spawn 'dot' command")?; + + // Write DOT content to stdin + use std::io::Write; + if let Some(mut stdin) = dot_process.stdin.take() { + stdin.write_all(dot_content.as_bytes()).with_context(|| "Failed to write DOT content to 'dot' command")?; + // Close stdin to signal EOF + drop(stdin); + } + + // Wait for the command to complete + let status = dot_process.wait().with_context(|| "Failed to wait for 'dot' command")?; + if !status.success() { + return Err(anyhow!("'dot' command failed with exit code: {:?}", status.code())); + } - // Write output - if let Some(output_path) = args.output { - fs::write(&output_path, output).with_context(|| format!("Failed to write to {:?}", output_path))?; - println!("Output written to: {:?}", output_path); - } else { - print!("{}", output); + println!("{} output written to: {:?}", format_arg.to_uppercase(), output_path); + } } Ok(())