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/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.toml b/tools/crate-hierarchy-viz/Cargo.toml new file mode 100644 index 0000000000..d9199f4bcb --- /dev/null +++ b/tools/crate-hierarchy-viz/Cargo.toml @@ -0,0 +1,13 @@ +[package] +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 } +clap = { workspace = true, features = ["derive"] } +toml = "0.8" +anyhow = { workspace = true } diff --git a/tools/crate-hierarchy-viz/src/main.rs b/tools/crate-hierarchy-viz/src/main.rs new file mode 100644 index 0000000000..f964e8fb27 --- /dev/null +++ b/tools/crate-hierarchy-viz/src/main.rs @@ -0,0 +1,401 @@ +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")] +#[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 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)] +struct WorkspaceToml { + workspace: WorkspaceConfig, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceConfig { + members: Vec, + 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 { + #[serde(flatten)] + _other: HashMap, + }, +} + +#[derive(Debug, Deserialize)] +struct CrateToml { + package: PackageConfig, + dependencies: Option>, +} + +#[derive(Debug, Deserialize)] +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, + #[serde(flatten)] + other: HashMap, + }, +} + +#[derive(Debug, Clone, PartialEq)] +struct CrateInfo { + name: String, + path: PathBuf, + dependencies: Vec, + 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(); + + 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)); + } + + // Remove transitive dependencies + remove_transitive_dependencies(&mut 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())); + } + + println!("{} output written to: {:?}", format_arg.to_uppercase(), output_path); + } + } + + Ok(()) +} + +fn generate_dot_format(crates: &[CrateInfo]) -> 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" || c.name == "graphene-cli") && !c.name.contains("desktop")) + .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 == "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| { + let path_str = c.path.to_string_lossy(); + path_str.contains("node-graph/libraries") + }) + .collect(); + + for crate_info in &node_library_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| { + let path_str = c.path.to_string_lossy(); + path_str.contains("node-graph/nodes") + }) + .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_desktop{\n"); + output.push_str(" label=\"Desktop\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightgreen;\n"); + + let desktop_crates: Vec<_> = crates + .iter() + .filter(|c| { + let path_str = c.path.to_string_lossy(); + path_str.contains("desktop") + }) + .collect(); + + for crate_info in &desktop_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 dep == "dyn-any" || dep == "node-macro" { + continue; + } + output.push_str(&format!(" \"{}\" -> \"{}\";\n", crate_info.name, dep)); + } + } + + output.push_str("}\n"); + Ok(output) +}