diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5062615ee2..a0ec208c042 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,4 +40,4 @@ jobs: cargo-${{ runner.os }}- - name: Run CI Pipeline - run: cargo run -p gunbc-ci --release + run: cargo run -p gunbc-dag --bin gunbc-ci --release diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d5f0896d3d5..d7d29e1c62a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,4 +18,4 @@ cache: ci: stage: ci script: - - cargo run -p gunbc-ci --release + - cargo run -p gunbc-dag --bin gunbc-ci --release diff --git a/Makefile b/Makefile index f0fd21bf642..9ae8acdb1b1 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,16 @@ # Generated by gunbc-makegen # DO NOT EDIT - regenerate with: make makegen +# Naming convention: +# make - verify only (CI-safe, fails on issues) +# make -fix - auto-fix then verify (for dev) +# +# Dev workflow: make test-fix (fix everything, then test) +# CI verification: make test (verify fmt + lint + test) + .DEFAULT_GOAL := help -.PHONY: help codegen ensure-codegen build clean test check clippy fmt fmt-check ci-yaml gist gist-dry buck2 buck2-dry makegen makegen-dry deps deps-dry ci ci-dry bootstrap bootstrap-dry dag-viz +.PHONY: help codegen ensure-codegen build clean fmt-fix lint-fix test test-fix check check-fix clippy clippy-fix fmt fmt-check ci-yaml gist gist-dry buck2 buck2-dry makegen makegen-dry deps deps-dry ci ci-dry bootstrap bootstrap-dry # Ensure codegen has run (upsert pattern: check stamp -> run if missing) ensure-codegen: @@ -26,6 +33,10 @@ clean: help: @echo "gunbc tools - generated Makefile" @echo "" + @echo "Naming convention:" + @echo " make - verify only (CI-safe)" + @echo " make -fix - auto-fix then verify (for dev)" + @echo "" @echo "Build transactions:" @echo " build - Commit: codegen → cargo build" @echo " clean - Rollback: remove all generated artifacts" @@ -33,8 +44,11 @@ help: @echo "" @echo "Development:" @echo " test - Run all tests" + @echo " test-fix - Run all tests (fmt-fix + lint-fix first)" @echo " check - Type check all targets" + @echo " check-fix - Type check all targets (fmt-fix first)" @echo " clippy - Run clippy linter" + @echo " clippy-fix - Run clippy linter (auto-fix)" @echo " fmt - Format all code" @echo " fmt-check - Format all code (check only)" @echo " ci-yaml - Generate CI workflow YAML (GitHub Actions & GitLab CI)" @@ -53,18 +67,38 @@ help: # Meta Targets - Development workflow commands # ============================================================================ +# fmt-fix: apply formatting (alias for fmt) +fmt-fix: + @cargo fmt + +# lint-fix: auto-fix lint issues where possible +lint-fix: ensure-codegen + @cargo clippy --fix --workspace --allow-dirty --allow-staged -- -D warnings + # test: Run all tests test: build @cargo test +# test-fix: auto-fix then verify +test-fix: fmt-fix lint-fix build + @cargo test + # check: Type check all targets check: ensure-codegen @cargo check --all-targets +# check-fix: auto-fix then verify +check-fix: fmt-fix ensure-codegen + @cargo check --all-targets + # clippy: Run clippy linter clippy: ensure-codegen @cargo clippy --all-targets -- -D warnings +# clippy-fix: auto-fix then verify +clippy-fix: ensure-codegen + @cargo clippy --fix --workspace --allow-dirty --allow-staged -- -D warnings + # fmt: Format all code fmt: @cargo fmt @@ -92,10 +126,10 @@ buck2-dry: ensure-codegen # gunbc-makegen entrypoints: output_path (String) makegen: ensure-codegen - @cargo run -p gunbc-makegen -- $(if $(OUTPUT),--output $(OUTPUT)) + @cargo run -p gunbc-dag --bin gunbc-makegen -- $(if $(OUTPUT),--output $(OUTPUT)) makegen-dry: ensure-codegen - @cargo run -p gunbc-makegen -- --dry-run $(if $(OUTPUT),--output $(OUTPUT)) + @cargo run -p gunbc-dag --bin gunbc-makegen -- --dry-run $(if $(OUTPUT),--output $(OUTPUT)) # gunbc-deps entrypoints: manifest_path (String) deps: ensure-codegen @@ -106,19 +140,15 @@ deps-dry: ensure-codegen # gunbc-ci entrypoints: ci: ensure-codegen - @cargo run -p gunbc-ci -- + @cargo run -p gunbc-dag --bin gunbc-ci -- ci-dry: ensure-codegen - @cargo run -p gunbc-ci -- --dry-run + @cargo run -p gunbc-dag --bin gunbc-ci -- --dry-run # gunbc-bootstrap entrypoints: bootstrap: ensure-codegen - @cargo run -p gunbc-bootstrap -- + @cargo run -p gunbc-dag --bin gunbc-bootstrap -- bootstrap-dry: ensure-codegen - @cargo run -p gunbc-bootstrap -- --dry-run - -# dag-viz: Generate interactive DAG visualization (opens in browser) -dag-viz: - @cargo run -p gunbc-dag --bin gunbc-dag-viz + @cargo run -p gunbc-dag --bin gunbc-bootstrap -- --dry-run diff --git a/core/codegen/src/main.rs b/core/codegen/src/main.rs index 07048ba3d7f..a0b4e19c5c0 100644 --- a/core/codegen/src/main.rs +++ b/core/codegen/src/main.rs @@ -285,7 +285,13 @@ fn cmd_cigen(dry_run: bool) { // Generate CI YAML for gunbc-ci // gunbc-ci is special - it has a handwritten main.rs that handles codegen internally - let config = RenderConfig::new("ci", "gunbc-ci") + let codegen = gunbc_ir::CargoInvocation::standalone("codegen"); + let tool = gunbc_ir::CargoInvocation::composed("ci", "dag"); + let config = RenderConfig::new("ci", tool) + .with_generator( + &codegen.binary, + &format!("{} -- cigen", codegen.command()), + ) .with_runner("ubuntu-latest") .with_env("CARGO_TERM_COLOR", "always") .with_branches(vec!["main"]); @@ -368,10 +374,9 @@ fn cmd_clippy_toml(dry_run: bool) { /// Generate GitHub Actions YAML template. fn generate_github_actions_template(config: &RenderConfig) -> String { let mut yaml = String::new(); - - yaml.push_str("# Generated by gunbc-codegen cigen. Do not edit manually.\n"); - yaml.push_str("# Regenerate with: cargo run -p gunbc-codegen -- cigen\n\n"); - yaml.push_str(&format!("name: {}\n\n", config.workflow_name)); + + yaml.push_str(&config.header("#")); + yaml.push_str(&format!("\n\nname: {}\n\n", config.workflow_name)); // Triggers yaml.push_str("on:\n"); @@ -433,7 +438,7 @@ fn generate_github_actions_template(config: &RenderConfig) -> String { // The prep node uses the resource acquisition (upsert) pattern: check if generated // files exist, generate them if not. This makes CI self-healing. yaml.push_str(" - name: Run CI Pipeline\n"); - yaml.push_str(&format!(" run: cargo run -p {} --release\n", config.tool_binary)); + yaml.push_str(&format!(" run: {} --release\n", config.tool.command())); yaml } @@ -441,9 +446,9 @@ fn generate_github_actions_template(config: &RenderConfig) -> String { /// Generate GitLab CI YAML template. fn generate_gitlab_ci_template(config: &RenderConfig) -> String { let mut yaml = String::new(); - - yaml.push_str("# Generated by gunbc-codegen cigen. Do not edit manually.\n"); - yaml.push_str("# Regenerate with: cargo run -p gunbc-codegen -- cigen\n\n"); + + yaml.push_str(&config.header("#")); + yaml.push_str("\n\n"); // Image yaml.push_str("image: rust:latest\n\n"); @@ -473,7 +478,7 @@ fn generate_gitlab_ci_template(config: &RenderConfig) -> String { yaml.push_str(&format!("{}:\n", config.workflow_name)); yaml.push_str(" stage: ci\n"); yaml.push_str(" script:\n"); - yaml.push_str(&format!(" - cargo run -p {} --release\n", config.tool_binary)); + yaml.push_str(&format!(" - {} --release\n", config.tool.command())); yaml } diff --git a/core/codegen/src/registry.rs b/core/codegen/src/registry.rs index b3fd6d011ec..bbaebdf3ca1 100644 --- a/core/codegen/src/registry.rs +++ b/core/codegen/src/registry.rs @@ -13,6 +13,7 @@ //! Eventually, the entire graph.rs can be generated from these definitions. use crate::cli_gen::{CliBoundary, CliEntrypoint, ToolMeta}; +use gunbc_ir::cargo; // ============================================================================ // DAG Definition Structures @@ -288,7 +289,7 @@ pub fn all_tools() -> Vec { vec![ // gunbc-gist (uses DagBuilder - returns Result) ToolDef::new( - "gunbc-gist", + &cargo::name("gist"), "gist", "Create a GitHub gist from code files", "build_gist_graph", @@ -321,7 +322,7 @@ pub fn all_tools() -> Vec { // gunbc-buck2 (uses DagBuilder - returns Result) ToolDef::new( - "gunbc-buck2", + &cargo::name("buck2"), "buck2", "Generate BUCK file from Cargo.toml", "build_buck2_graph", @@ -352,7 +353,7 @@ pub fn all_tools() -> Vec { // gunbc-makegen (uses DagBuilder - returns Result) ToolDef::new( - "gunbc-makegen", + &cargo::name("makegen"), "makegen", "Generate Makefile from tool registry", "build_makegen_graph", @@ -379,7 +380,7 @@ pub fn all_tools() -> Vec { // gunbc-deps (uses DagBuilder - returns Result) ToolDef::new( - "gunbc-deps", + &cargo::name("deps"), "deps", "Install tool dependencies", "build_deps_graph", @@ -408,7 +409,7 @@ pub fn all_tools() -> Vec { // gunbc-bootstrap (uses DagBuilder - returns Result) ToolDef::new( - "gunbc-bootstrap", + &cargo::name("bootstrap"), "bootstrap", "Generate Makefile and .gitignore", "build_bootstrap_graph", diff --git a/core/exec/src/execute.rs b/core/exec/src/execute.rs index 33f352d57d7..b56f7192f8a 100644 --- a/core/exec/src/execute.rs +++ b/core/exec/src/execute.rs @@ -505,12 +505,19 @@ fn execute_flat( }; node_outputs.insert(node_id.0.clone(), outputs.clone()); - entries.push(LogEntry { + let entry = LogEntry { node_id: node_id.0.clone(), outputs, was_intercepted, - }); - + }; + + // Print node outputs inside the CI group so they appear in the + // collapsible section, not after all groups have closed. + if ci_ctx.is_some() { + print_log_entry(&entry); + } + entries.push(entry); + // End CI group for this node if let Some(ref mut ci) = ci_ctx { ci.end_group(); @@ -520,6 +527,38 @@ fn execute_flat( Ok(ExecutionLog { entries }) } +/// Print a log entry's outputs to stdout. +/// +/// Used inside CI groups so that node outputs appear within the +/// collapsible section rather than in a flat summary after all groups. +fn print_log_entry(entry: &LogEntry) { + for (port, value) in &entry.outputs { + match value { + Value::Str(s) => { + if port.ends_with("stderr") || port.ends_with("stdout") { + if !s.is_empty() { + println!(" {port}: {s}"); + } + } else if s.contains('\n') { + // Multi-line values (reports, etc.) — print in full + println!(" {port}: {s}"); + } else if s.len() < 120 { + println!(" {port}: {s}"); + } else { + println!(" {port}: {}...", &s[..80]); + } + } + Value::Int(i) => println!(" {port}: {i}"), + Value::Bool(b) => println!(" {port}: {b}"), + Value::StrList(list) => println!(" {port}: [{} items]", list.len()), + Value::MapStrStr(map) => println!(" {port}: {{{} entries}}", map.len()), + Value::Json(_) => println!(" {port}: "), + Value::Skipped => {} // Don't print skipped outputs + _ => {} + } + } +} + /// Check whether a node should be skipped based on guard predicates. fn should_skip_node(node: &Node, inputs: &HashMap) -> bool { for port in &node.inputs { diff --git a/core/ir/src/cargo.rs b/core/ir/src/cargo.rs new file mode 100644 index 00000000000..aa57202ff2d --- /dev/null +++ b/core/ir/src/cargo.rs @@ -0,0 +1,230 @@ +//! Cargo workspace binary invocation. +//! +//! Models how to invoke a binary from a cargo workspace. Handles the +//! distinction between standalone packages and binaries that live inside +//! another package: +//! +//! - Standalone: `cargo run -p gunbc-gist` +//! - In-package: `cargo run -p gunbc-dag --bin gunbc-ci` +//! +//! This is the single source of truth for cargo invocation rendering, +//! used by both Makefile generation and CI YAML generation. +//! +//! # Name Composition +//! +//! All binary names follow the `{PREFIX}-{component}` pattern (e.g., `gunbc-ci`). +//! Use [`CargoInvocation::standalone`] and [`CargoInvocation::composed`] to +//! construct invocations from component names, avoiding hardcoded full names. + +/// Workspace binary name prefix. All gunbc binaries follow `{PREFIX}-{component}`. +pub const PREFIX: &str = "gunbc"; + +/// Compose a full binary name from a component: `{PREFIX}-{component}`. +/// +/// # Examples +/// +/// ``` +/// use gunbc_ir::cargo::name; +/// assert_eq!(name("ci"), "gunbc-ci"); +/// assert_eq!(name("gist"), "gunbc-gist"); +/// ``` +pub fn name(component: &str) -> String { + format!("{PREFIX}-{component}") +} + +/// Describes how to invoke a cargo workspace binary. +/// +/// This type is the canonical way to represent "run this binary from +/// the workspace" across all renderers (Makefile, GitHub Actions, GitLab CI). +/// +/// Prefer [`CargoInvocation::standalone`] and [`CargoInvocation::composed`] +/// over [`CargoInvocation::new`] and [`CargoInvocation::in_package`] to +/// ensure names are composed from components rather than hardcoded. +#[derive(Debug, Clone, Default)] +pub struct CargoInvocation { + /// The binary name (e.g., "gunbc-ci"). + pub binary: String, + /// The package name, if different from the binary (e.g., "gunbc-dag"). + /// Set when the binary is a `[[bin]]` entry in another crate's Cargo.toml. + pub package: Option, +} + +impl CargoInvocation { + /// Create an invocation for a binary in its own package. + /// + /// Produces: `cargo run -p ` + /// + /// Prefer [`Self::standalone`] which composes the name from a component. + pub fn new(binary: impl Into) -> Self { + Self { + binary: binary.into(), + package: None, + } + } + + /// Create an invocation for a binary inside another package. + /// + /// Produces: `cargo run -p --bin ` + /// + /// Prefer [`Self::composed`] which composes both names from components. + pub fn in_package(binary: impl Into, package: impl Into) -> Self { + Self { + binary: binary.into(), + package: Some(package.into()), + } + } + + /// Create an invocation for a standalone binary from its component name. + /// + /// The binary name is composed as `{PREFIX}-{component}`. + /// + /// # Examples + /// + /// ``` + /// use gunbc_ir::CargoInvocation; + /// let inv = CargoInvocation::standalone("gist"); + /// assert_eq!(inv.binary, "gunbc-gist"); + /// assert_eq!(inv.command(), "cargo run -p gunbc-gist"); + /// ``` + pub fn standalone(component: &str) -> Self { + Self::new(name(component)) + } + + /// Create an invocation for a binary inside another package, both + /// composed from component names. + /// + /// The binary is `{PREFIX}-{binary_component}`, the package is + /// `{PREFIX}-{package_component}`. + /// + /// # Examples + /// + /// ``` + /// use gunbc_ir::CargoInvocation; + /// let inv = CargoInvocation::composed("ci", "dag"); + /// assert_eq!(inv.binary, "gunbc-ci"); + /// assert_eq!(inv.package, Some("gunbc-dag".to_string())); + /// assert_eq!(inv.command(), "cargo run -p gunbc-dag --bin gunbc-ci"); + /// ``` + pub fn composed(binary_component: &str, package_component: &str) -> Self { + Self::in_package(name(binary_component), name(package_component)) + } + + /// Get the cargo run arguments (without the `cargo run` prefix). + /// + /// Returns `-p --bin ` or `-p `. + pub fn args(&self) -> String { + match &self.package { + Some(pkg) => format!("-p {} --bin {}", pkg, self.binary), + None => format!("-p {}", self.binary), + } + } + + /// Get the full `cargo run` command string. + /// + /// Returns `cargo run -p --bin ` or `cargo run -p `. + pub fn command(&self) -> String { + format!("cargo run {}", self.args()) + } + + /// Get the cargo run command as individual string parts. + /// + /// Returns `["cargo", "run", "-p", package, "--bin", binary]` or + /// `["cargo", "run", "-p", binary]`. + pub fn command_parts(&self) -> Vec { + let mut parts = vec!["cargo".to_string(), "run".to_string()]; + match &self.package { + Some(pkg) => { + parts.push("-p".to_string()); + parts.push(pkg.clone()); + parts.push("--bin".to_string()); + parts.push(self.binary.clone()); + } + None => { + parts.push("-p".to_string()); + parts.push(self.binary.clone()); + } + } + parts + } + + /// Get the cargo run command parts followed by additional arguments. + /// + /// Convenience for building command vectors like + /// `["cargo", "run", "-p", "gunbc-codegen", "--release", "--", "codegen"]`. + pub fn run_with_args(&self, extra: &[&str]) -> Vec { + let mut parts = self.command_parts(); + parts.extend(extra.iter().map(|s| s.to_string())); + parts + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_name_composition() { + assert_eq!(name("ci"), "gunbc-ci"); + assert_eq!(name("gist"), "gunbc-gist"); + assert_eq!(name("dag"), "gunbc-dag"); + } + + #[test] + fn test_standalone() { + let inv = CargoInvocation::standalone("gist"); + assert_eq!(inv.binary, "gunbc-gist"); + assert_eq!(inv.package, None); + assert_eq!(inv.args(), "-p gunbc-gist"); + assert_eq!(inv.command(), "cargo run -p gunbc-gist"); + } + + #[test] + fn test_composed() { + let inv = CargoInvocation::composed("ci", "dag"); + assert_eq!(inv.binary, "gunbc-ci"); + assert_eq!(inv.package, Some("gunbc-dag".to_string())); + assert_eq!(inv.args(), "-p gunbc-dag --bin gunbc-ci"); + assert_eq!(inv.command(), "cargo run -p gunbc-dag --bin gunbc-ci"); + } + + #[test] + fn test_standalone_package() { + let inv = CargoInvocation::new("gunbc-gist"); + assert_eq!(inv.args(), "-p gunbc-gist"); + assert_eq!(inv.command(), "cargo run -p gunbc-gist"); + } + + #[test] + fn test_binary_in_package() { + let inv = CargoInvocation::in_package("gunbc-ci", "gunbc-dag"); + assert_eq!(inv.args(), "-p gunbc-dag --bin gunbc-ci"); + assert_eq!(inv.command(), "cargo run -p gunbc-dag --bin gunbc-ci"); + } + + #[test] + fn test_command_parts_standalone() { + let inv = CargoInvocation::standalone("gist"); + assert_eq!( + inv.command_parts(), + vec!["cargo", "run", "-p", "gunbc-gist"] + ); + } + + #[test] + fn test_command_parts_composed() { + let inv = CargoInvocation::composed("ci", "dag"); + assert_eq!( + inv.command_parts(), + vec!["cargo", "run", "-p", "gunbc-dag", "--bin", "gunbc-ci"] + ); + } + + #[test] + fn test_run_with_args() { + let inv = CargoInvocation::standalone("codegen"); + assert_eq!( + inv.run_with_args(&["--release", "--", "codegen"]), + vec!["cargo", "run", "-p", "gunbc-codegen", "--release", "--", "codegen"] + ); + } +} diff --git a/core/ir/src/lib.rs b/core/ir/src/lib.rs index c4c53d5d5d6..bd2954d9fc6 100644 --- a/core/ir/src/lib.rs +++ b/core/ir/src/lib.rs @@ -47,6 +47,7 @@ pub mod boundary; pub mod builder; +pub mod cargo; pub mod compose; pub mod contract; pub mod dag; @@ -77,6 +78,7 @@ pub use patterns::{ pub use signature::{infer_signature, SignatureError, SignaturePort, WorkflowSignature}; pub use contract::TypeContract; pub use resource::{AccessMode, ResourceAccess, ResourceConflict, ResourceId}; +pub use cargo::{CargoInvocation, PREFIX as CARGO_PREFIX}; pub use transport::{TransportRequest, TransportResponse}; pub use type_op::{BaseType, Coercion, Predicate, PredicateValue, TypeOp, WrapperKind}; pub use type_registry::{TypeNotFoundError, TypeRegistry}; diff --git a/core/ir/src/transport/ci/providers/github.rs b/core/ir/src/transport/ci/providers/github.rs index fbc4b24b9ea..b995b76aa9b 100644 --- a/core/ir/src/transport/ci/providers/github.rs +++ b/core/ir/src/transport/ci/providers/github.rs @@ -14,7 +14,6 @@ //! This module also implements `CiRenderer` for generating GitHub Actions YAML //! from DAGs. Each DAG node becomes a workflow step with proper dependencies. -use crate::language::traits::comment::generated_header; use crate::transport::ci::command::{AnnotationLevel, WorkflowCommand}; use crate::transport::ci::provider::CiProvider; use crate::transport::ci::render::{dag_to_shared_steps, CiRenderer, RenderConfig, SharedStep}; @@ -178,9 +177,9 @@ impl CiRenderer for GitHubActionsProvider { fn render_github_workflow(steps: &[SharedStep], config: &RenderConfig) -> String { let mut yaml = String::new(); - // Header using language module's generated_header for consistency - yaml.push_str(&generated_header("gunbc-codegen", "make ci-yaml", "#")); - yaml.push_str(&format!("\nname: {}\n\n", config.workflow_name)); + // Header from render config — generator name and regen command are set by the caller + yaml.push_str(&config.header("#")); + yaml.push_str(&format!("\n\nname: {}\n\n", config.workflow_name)); // Triggers yaml.push_str("on:\n"); @@ -245,13 +244,13 @@ fn render_github_step(step: &SharedStep, _config: &RenderConfig) -> String { } SharedStep::DagStep { - tool_binary, + tool, node_id, depends_on, } => { let mut yaml = format!( " - name: {}\n id: {}\n run: {} step {}\n", - node_id.0, node_id.0, tool_binary, node_id.0 + node_id.0, node_id.0, tool.command(), node_id.0 ); // Add environment variables for dependencies @@ -271,10 +270,10 @@ fn render_github_step(step: &SharedStep, _config: &RenderConfig) -> String { yaml } - SharedStep::DagRun { tool_binary } => { + SharedStep::DagRun { tool } => { format!( " - name: Run {}\n run: {}\n", - tool_binary, tool_binary + tool.binary, tool.command() ) } } @@ -283,6 +282,7 @@ fn render_github_step(step: &SharedStep, _config: &RenderConfig) -> String { #[cfg(test)] mod tests { use super::*; + use crate::cargo::CargoInvocation; use crate::transport::ci::command::FileLocation; #[test] @@ -368,18 +368,20 @@ mod tests { dag.add_edge(edge("build", "success", "test", "build_success")); let provider = GitHubActionsProvider; - let config = RenderConfig::new("ci", "gunbc-ci") + let tool = CargoInvocation::composed("ci", "dag"); + let config = RenderConfig::new("ci", tool) .with_runner("ubuntu-latest") .with_env("CARGO_TERM_COLOR", "always"); let yaml = provider.render(&dag, &config); + let ci_name = crate::cargo::name("ci"); // Check structure assert!(yaml.contains("name: ci")); assert!(yaml.contains("runs-on: ubuntu-latest")); assert!(yaml.contains("uses: actions/checkout@v4")); - assert!(yaml.contains("gunbc-ci step build")); - assert!(yaml.contains("gunbc-ci step test")); + assert!(yaml.contains(&format!("{ci_name} step build"))); + assert!(yaml.contains(&format!("{ci_name} step test"))); assert!(yaml.contains("CARGO_TERM_COLOR: always")); } diff --git a/core/ir/src/transport/ci/providers/gitlab.rs b/core/ir/src/transport/ci/providers/gitlab.rs index 2c4d7afd819..82f5f2f0deb 100644 --- a/core/ir/src/transport/ci/providers/gitlab.rs +++ b/core/ir/src/transport/ci/providers/gitlab.rs @@ -15,7 +15,6 @@ //! This module also implements `CiRenderer` for generating GitLab CI YAML //! from DAGs. Uses stages for parallelism and `needs` for dependencies. -use crate::language::traits::comment::generated_header; use crate::language::NamingCase; use crate::transport::ci::command::{AnnotationLevel, WorkflowCommand}; use crate::transport::ci::provider::CiProvider; @@ -222,8 +221,8 @@ impl CiRenderer for GitLabCiProvider { fn render_gitlab_ci(steps: &[SharedStep], config: &RenderConfig) -> String { let mut yaml = String::new(); - // Header using language module's generated_header for consistency - yaml.push_str(&generated_header("gunbc-codegen", "make ci-yaml", "#")); + // Header from render config — generator name and regen command are set by the caller + yaml.push_str(&config.header("#")); yaml.push_str("\n\n"); // Default image @@ -288,8 +287,8 @@ fn compute_stages(steps: &[SharedStep]) -> Vec { stages.push(stage_name); } } - SharedStep::DagRun { tool_binary } => { - let stage = format!("{}-run", NamingCase::SnakeCase.apply(tool_binary)); + SharedStep::DagRun { tool } => { + let stage = format!("{}-run", NamingCase::SnakeCase.apply(&tool.binary)); if !stages.contains(&stage) { stages.push(stage); } @@ -324,13 +323,13 @@ fn render_gitlab_job(step: &SharedStep, _config: &RenderConfig) -> String { } SharedStep::DagStep { - tool_binary, + tool, node_id, depends_on, } => { let mut yaml = format!( "{}:\n stage: {}\n script:\n - {} step {}\n", - node_id.0, node_id.0, tool_binary, node_id.0 + node_id.0, node_id.0, tool.command(), node_id.0 ); // Add dependencies using `needs` @@ -351,11 +350,11 @@ fn render_gitlab_job(step: &SharedStep, _config: &RenderConfig) -> String { yaml } - SharedStep::DagRun { tool_binary } => { - let stage_name = format!("{}-run", NamingCase::SnakeCase.apply(tool_binary)); + SharedStep::DagRun { tool } => { + let stage_name = format!("{}-run", NamingCase::SnakeCase.apply(&tool.binary)); format!( "{}:\n stage: {}\n script:\n - {}\n\n", - stage_name, stage_name, tool_binary + stage_name, stage_name, tool.command() ) } } @@ -364,6 +363,7 @@ fn render_gitlab_job(step: &SharedStep, _config: &RenderConfig) -> String { #[cfg(test)] mod tests { use super::*; + use crate::cargo::CargoInvocation; use crate::transport::ci::command::FileLocation; fn test_provider() -> GitLabCiProvider { @@ -474,17 +474,19 @@ mod tests { dag.add_edge(edge("build", "success", "test", "build_success")); let provider = test_provider(); - let config = RenderConfig::new("ci", "gunbc-ci") + let tool = CargoInvocation::composed("ci", "dag"); + let config = RenderConfig::new("ci", tool) .with_runner("rust:latest") .with_env("CARGO_TERM_COLOR", "always"); let yaml = provider.render(&dag, &config); + let ci_name = crate::cargo::name("ci"); // Check structure assert!(yaml.contains("image: rust:latest")); assert!(yaml.contains("stages:")); - assert!(yaml.contains("gunbc-ci step build")); - assert!(yaml.contains("gunbc-ci step test")); + assert!(yaml.contains(&format!("{ci_name} step build"))); + assert!(yaml.contains(&format!("{ci_name} step test"))); assert!(yaml.contains("needs:")); assert!(yaml.contains("CARGO_TERM_COLOR: \"always\"")); } diff --git a/core/ir/src/transport/ci/render.rs b/core/ir/src/transport/ci/render.rs index f840329b819..3d875754b3c 100644 --- a/core/ir/src/transport/ci/render.rs +++ b/core/ir/src/transport/ci/render.rs @@ -28,6 +28,8 @@ //! 3. **Step granularity**: Each DAG node becomes a CI step for visibility //! 4. **Data passing**: Node outputs become step outputs (GitHub) or artifacts (GitLab) +use crate::cargo::CargoInvocation; +use crate::language::traits::comment::generated_header; use crate::{Dag, NodeId}; use std::collections::HashMap; @@ -65,8 +67,16 @@ pub struct RenderConfig { /// Name of the workflow/pipeline. pub workflow_name: String, - /// The CLI tool binary name (e.g., "gunbc-ci"). - pub tool_binary: String, + /// How to invoke the CI tool binary via cargo. + pub tool: CargoInvocation, + + /// Name of the tool that generates this CI config file + /// (e.g., "gunbc-codegen"). Used in the "Generated by" header. + pub generator_name: String, + + /// Command to regenerate this file (e.g., "make ci-yaml"). + /// Used in the "DO NOT EDIT - regenerate with:" header. + pub regenerate_command: String, /// Runner/image to use (e.g., "ubuntu-latest", "saas-linux-small-amd64"). pub runner: String, @@ -89,10 +99,12 @@ pub struct RenderConfig { impl RenderConfig { /// Create a new render config with defaults. - pub fn new(workflow_name: &str, tool_binary: &str) -> Self { + pub fn new(workflow_name: &str, tool: CargoInvocation) -> Self { Self { workflow_name: workflow_name.to_string(), - tool_binary: tool_binary.to_string(), + tool, + generator_name: String::new(), + regenerate_command: String::new(), runner: "ubuntu-latest".to_string(), step_mode: true, env: HashMap::new(), @@ -102,6 +114,27 @@ impl RenderConfig { } } + /// Set the generator name and regeneration command. + /// + /// These appear in the file header: + /// ```text + /// # Generated by {generator_name} + /// # DO NOT EDIT - regenerate with: {regenerate_command} + /// ``` + pub fn with_generator(mut self, name: &str, command: &str) -> Self { + self.generator_name = name.to_string(); + self.regenerate_command = command.to_string(); + self + } + + /// Produce the standard "Generated by / DO NOT EDIT" header. + /// + /// Uses the language module's `generated_header` for consistency + /// with all other generated files in the workspace. + pub fn header(&self, comment_prefix: &str) -> String { + generated_header(&self.generator_name, &self.regenerate_command, comment_prefix) + } + /// Set the runner. pub fn with_runner(mut self, runner: &str) -> Self { self.runner = runner.to_string(); @@ -195,12 +228,12 @@ pub enum SharedStep { Run { name: String, command: String }, /// Run a DAG tool in step mode. DagStep { - tool_binary: String, + tool: CargoInvocation, node_id: NodeId, depends_on: Vec, }, /// Run a DAG tool (full execution). - DagRun { tool_binary: String }, + DagRun { tool: CargoInvocation }, } impl SharedStep { @@ -218,19 +251,17 @@ impl SharedStep { } /// Create a DAG step execution. - pub fn dag_step(tool_binary: &str, node_id: impl Into, depends_on: Vec) -> Self { + pub fn dag_step(tool: CargoInvocation, node_id: impl Into, depends_on: Vec) -> Self { Self::DagStep { - tool_binary: tool_binary.to_string(), + tool, node_id: node_id.into(), depends_on, } } /// Create a full DAG execution. - pub fn dag_run(tool_binary: &str) -> Self { - Self::DagRun { - tool_binary: tool_binary.to_string(), - } + pub fn dag_run(tool: CargoInvocation) -> Self { + Self::DagRun { tool } } } @@ -259,11 +290,11 @@ pub fn dag_to_shared_steps(dag: &Dag, config: &RenderConfig) -> Vec &str { - "gunbc-ci" + CI_GENERATOR_NAME } fn regenerate_command(&self) -> &str { diff --git a/core/testgen/src/codegen.rs b/core/testgen/src/codegen.rs index 933fb22c21d..11e2019d4b0 100644 --- a/core/testgen/src/codegen.rs +++ b/core/testgen/src/codegen.rs @@ -72,7 +72,7 @@ impl<'a, T> TestGenerator<'a, T> { let doc_prefix = RUST_COMMENTS.doc_prefix.unwrap_or("//!"); code.push_str(&format!("{} Generated tests for {} DAG.\n", doc_prefix, module_name)); code.push_str(&format!("{}\n", doc_prefix)); - code.push_str(&generated_header("gunbc-testgen", "make testgen", doc_prefix)); + code.push_str(&generated_header(&gunbc_ir::cargo::name("testgen"), "make testgen", doc_prefix)); code.push_str("\n\n"); // Imports diff --git a/gunbc-dag/src/bin/ci.rs b/gunbc-dag/src/bin/ci.rs index c069495efcf..3e9c0c9f7bb 100644 --- a/gunbc-dag/src/bin/ci.rs +++ b/gunbc-dag/src/bin/ci.rs @@ -16,6 +16,7 @@ use gunbc_dag::build_ci_graph; use gunbc_exec::{execute_with_mode_and_ci, BoundaryMocks, CiContext, ExecutionMode}; +use gunbc_ir::transport::{FileOp, FileResponse, ShellResponse, TransportResponse}; use gunbc_ir::Value; use std::env; use std::process; @@ -42,8 +43,65 @@ fn main() { // Set up execution mode let mode = if dry_run { let mut mocks = BoundaryMocks::new(); - mocks.set_value("report", "overall_success", Value::Bool(true)); - mocks.set_value("report", "report", Value::Str("".to_string())); + + // Transport execution nodes need properly-typed Response mocks. + // The default mock is Value::Str(""), but downstream parse + // nodes call v.as_response() which only matches Value::Response. + + // execute_deps_exists: file exists check for deps.toml + mocks.set_value("execute_deps_exists", "response", Value::Response( + TransportResponse::File(FileResponse { + path: "deps.toml".to_string(), + operation: FileOp::Exists, + success: true, + content: None, + exists: Some(false), + error: None, + }) + )); + + // execute_codegen_exists: file exists check for buck-out/gen/bin + mocks.set_value("execute_codegen_exists", "response", Value::Response( + TransportResponse::File(FileResponse { + path: "buck-out/gen/bin".to_string(), + operation: FileOp::Exists, + success: true, + content: None, + exists: Some(true), // Pretend codegen already exists + error: None, + }) + )); + + // execute_codegen: shell command (skipped when codegen exists) + mocks.set_value("execute_codegen", "response", Value::Response( + TransportResponse::Shell(ShellResponse { + exit_code: 0, + stdout: String::new(), + stderr: String::new(), + }) + )); + mocks.set_value("execute_codegen", "skip", Value::Bool(true)); + + // execute_build: shell command for cargo build + mocks.set_value("execute_build", "response", Value::Response( + TransportResponse::Shell(ShellResponse { + exit_code: 0, + stdout: "".to_string(), + stderr: String::new(), + }) + )); + mocks.set_value("execute_build", "skip", Value::Bool(false)); + + // execute_test: shell command for cargo test + mocks.set_value("execute_test", "response", Value::Response( + TransportResponse::Shell(ShellResponse { + exit_code: 0, + stdout: "".to_string(), + stderr: String::new(), + }) + )); + mocks.set_value("execute_test", "skip", Value::Bool(false)); + ExecutionMode::DryRun(mocks) } else { ExecutionMode::Real @@ -56,7 +114,7 @@ fn main() { let mut ci = CiContext::detect(); // Print header - println!("gunbc-ci"); + println!("{}", gunbc_ir::cargo::name("ci")); println!(" mode: {}", if dry_run { "dry-run" } else { "real" }); // Show CI provider if detected (not "plain") if ci.provider_id() != "plain" { @@ -64,19 +122,11 @@ fn main() { } println!(); - // Execute the CI pipeline with CI context for step visibility + // Execute the CI pipeline with CI context for step visibility. + // Node outputs are printed inside their CI groups by the executor, + // so we only need to check the final result here. match execute_with_mode_and_ci(&dag, mode, &mut ci) { Ok(log) => { - // Print summary (outside of groups) - for entry in &log.entries { - let marker = if entry.was_intercepted { " [DRY-RUN]" } else { "" }; - println!("[{}]{}", entry.node_id, marker); - - for (port, value) in &entry.outputs { - print_value(port, value); - } - } - // Check overall_success and exit with appropriate code for entry in &log.entries { if let Some(Value::Bool(false)) = entry.outputs.get("overall_success") { @@ -91,34 +141,12 @@ fn main() { } } -fn print_value(port: &str, value: &Value) { - match value { - Value::Str(s) => { - if port.ends_with("stderr") || port.ends_with("stdout") { - // Don't truncate stderr/stdout - we want to see full output - if !s.is_empty() { - println!(" {}: {}", port, s); - } - } else if s.len() < 80 { - println!(" {}: {}", port, s); - } else { - println!(" {}: {}...", port, &s[..60.min(s.len())]); - } - } - Value::Int(i) => println!(" {}: {}", port, i), - Value::Bool(b) => println!(" {}: {}", port, b), - Value::StrList(list) => println!(" {}: [{} items]", port, list.len()), - Value::MapStrStr(map) => println!(" {}: {{{} entries}}", port, map.len()), - Value::Json(_) => println!(" {}: ", port), - _ => {} - } -} - fn print_help() { - println!("gunbc-ci - CI orchestration tool"); + let name = gunbc_ir::cargo::name("ci"); + println!("{name} - CI orchestration tool"); println!(); println!("USAGE:"); - println!(" gunbc-ci [OPTIONS]"); + println!(" {name} [OPTIONS]"); println!(); println!("OPTIONS:"); println!(" -n, --dry-run Don't perform actual I/O"); diff --git a/gunbc-dag/src/ci/graph.rs b/gunbc-dag/src/ci/graph.rs index 354e0d38ece..384ec96e097 100644 --- a/gunbc-dag/src/ci/graph.rs +++ b/gunbc-dag/src/ci/graph.rs @@ -147,12 +147,14 @@ pub fn ci_integrations() -> Vec { /// Get the complete workflow configuration for CI. pub fn ci_workflow_config() -> WorkflowConfig { + let codegen_cmd = gunbc_ir::CargoInvocation::standalone("codegen").command(); + let ci_cmd = gunbc_ir::CargoInvocation::composed("ci", "dag").command(); WorkflowConfig::new( "CI", ubuntu_latest(), ci_integrations(), ) - .with_run_command("|\n cargo run -p gunbc-codegen -- codegen\n cargo run -p gunbc-ci -- run") + .with_run_command(format!("|\n {codegen_cmd} -- codegen\n {ci_cmd} -- run")) } /// Get the required permissions for the CI workflow. @@ -504,6 +506,9 @@ pub fn build_ci_graph() -> Result, BuilderError> { port("build_success", "Bool"), port("test_success", "Bool"), port("lint_success", "Bool"), + optional("build_stderr", "String"), + optional("test_stderr", "String"), + optional("lint_stderr", "String"), ], vec![ port("overall_success", "Bool"), @@ -557,10 +562,13 @@ pub fn build_ci_graph() -> Result, BuilderError> { builder.add_edge(clippy_lint.out("skip"), parse_lint.in_port("skip"))?; builder.add_edge(prepare_clippy_lint.out("skip_reason"), parse_lint.in_port("skip_reason"))?; - // Report + // Report - success flags and stderr for failure details builder.add_edge(parse_build.out("build_success"), report.in_port("build_success"))?; builder.add_edge(parse_test.out("test_success"), report.in_port("test_success"))?; builder.add_edge(parse_lint.out("lint_success"), report.in_port("lint_success"))?; + builder.add_edge(parse_build.out("build_stderr"), report.in_port("build_stderr"))?; + builder.add_edge(parse_test.out("test_stderr"), report.in_port("test_stderr"))?; + builder.add_edge(parse_lint.out("lint_stderr"), report.in_port("lint_stderr"))?; Ok(builder.build()) } diff --git a/gunbc-dag/src/ci/ops.rs b/gunbc-dag/src/ci/ops.rs index e82083eaf8a..6f220437e7a 100644 --- a/gunbc-dag/src/ci/ops.rs +++ b/gunbc-dag/src/ci/ops.rs @@ -459,6 +459,10 @@ fn execute_parse_clippy_lint_result(inputs: HashMap) -> Result) -> Result, ExecError> { let build_success = inputs .get("build_success") @@ -477,22 +481,44 @@ fn execute_report(inputs: HashMap) -> Result = [ + ("Build", "build_stderr"), + ("Test", "test_stderr"), + ("Lint", "lint_stderr"), + ] + .iter() + .zip([build_success, test_success, lint_success]) + .filter(|(_, success)| !success) + .map(|((label, key), _)| (*label, *key)) + .collect(); + + for (label, key) in failures { + let stderr = inputs + .get(key) + .and_then(|v| v.as_str()) + .unwrap_or(""); + if !stderr.is_empty() { + report.push_str(&format!("\n--- {label} stderr ---\n{stderr}\n")); + } + } + } + let mut out = HashMap::new(); out.insert("overall_success".to_string(), Value::Bool(overall_success)); out.insert("report".to_string(), Value::Str(report)); diff --git a/gunbc-dag/src/makegen/gitignore.rs b/gunbc-dag/src/makegen/gitignore.rs index c9cf5b138b5..6f2a82dafb2 100644 --- a/gunbc-dag/src/makegen/gitignore.rs +++ b/gunbc-dag/src/makegen/gitignore.rs @@ -12,6 +12,7 @@ //! - `rationale`: Why these files are ignored use crate::makegen::registry::{BuildConfig, BuildSystem}; +use crate::makegen::render::MAKEGEN_NAME; use gunbc_ir::Renderable; // ============================================================================ @@ -186,7 +187,7 @@ impl<'a> GitignoreRenderer<'a> { impl Renderable for GitignoreRenderer<'_> { fn generator_name(&self) -> &str { - "gunbc-makegen" + MAKEGEN_NAME } fn regenerate_command(&self) -> &str { diff --git a/gunbc-dag/src/makegen/graph_mock.rs b/gunbc-dag/src/makegen/graph_mock.rs index ed567b12685..3ee9440ad2b 100644 --- a/gunbc-dag/src/makegen/graph_mock.rs +++ b/gunbc-dag/src/makegen/graph_mock.rs @@ -5,7 +5,7 @@ //! - What input constraints upstream must satisfy //! - Resource simulations for file system operations -use gunbc_ir::Value; +use gunbc_ir::{CargoInvocation, Value}; use gunbc_test::{InputConstraint, MockSpec}; /// Mock specification for the makegen graph. @@ -62,21 +62,24 @@ pub fn makegen_mock_spec_fs_fails() -> MockSpec { /// Generate mock Makefile content. fn mock_makefile_content() -> String { - r#"# Generated by gunbc-makegen -# DO NOT EDIT - -.PHONY: gist deps makegen - -gist: - @cargo run -p gunbc-gist -- $(if $(REPO),--repo $(REPO)) - -deps: - @cargo run -p gunbc-deps -- $(if $(MANIFEST),--manifest $(MANIFEST)) - -makegen: - @cargo run -p gunbc-makegen -- $(if $(OUTPUT),--output $(OUTPUT)) -"# - .to_string() + let gist = CargoInvocation::standalone("gist").command(); + let deps = CargoInvocation::standalone("deps").command(); + let makegen = CargoInvocation::composed("makegen", "dag").command(); + format!( + "# Generated by gunbc-makegen\n\ + # DO NOT EDIT\n\ + \n\ + .PHONY: gist deps makegen\n\ + \n\ + gist:\n\ + \t@{gist} -- $(if $(REPO),--repo $(REPO))\n\ + \n\ + deps:\n\ + \t@{deps} -- $(if $(MANIFEST),--manifest $(MANIFEST))\n\ + \n\ + makegen:\n\ + \t@{makegen} -- $(if $(OUTPUT),--output $(OUTPUT))\n" + ) } #[cfg(test)] diff --git a/gunbc-dag/src/makegen/ops.rs b/gunbc-dag/src/makegen/ops.rs index 00b8bb40c9c..98e0e9a2114 100644 --- a/gunbc-dag/src/makegen/ops.rs +++ b/gunbc-dag/src/makegen/ops.rs @@ -47,7 +47,7 @@ fn execute_load_registry(_inputs: HashMap) -> Result) -> Result { + let gist = CargoInvocation::standalone("gist").command(); + let deps = CargoInvocation::standalone("deps").command(); + let buck2 = CargoInvocation::standalone("buck2").command(); let mut out = HashMap::new(); out.insert( "makefile_content".to_string(), Value::Str( - r#"# Generated Makefile -.PHONY: gist deps buck2 - -gist: - cargo run -p gunbc-gist - -deps: - cargo run -p gunbc-deps - -buck2: - cargo run -p gunbc-buck2 -"# - .to_string(), + format!( + "# Generated Makefile\n\ + .PHONY: gist deps buck2\n\ + \n\ + gist:\n\ + \t{gist}\n\ + \n\ + deps:\n\ + \t{deps}\n\ + \n\ + buck2:\n\ + \t{buck2}\n" + ), ), ); out diff --git a/gunbc-dag/src/makegen/registry.rs b/gunbc-dag/src/makegen/registry.rs index 10032e57fbc..528b1e61e37 100644 --- a/gunbc-dag/src/makegen/registry.rs +++ b/gunbc-dag/src/makegen/registry.rs @@ -9,6 +9,7 @@ //! commands. This eliminates duplicate hardcoded commands across the codebase. use gunbc_deps::DEFAULT_MANIFEST_FILENAME; +use gunbc_ir::CargoInvocation; use gunbc_ir::DEFAULT_MAKEFILE_FILENAME; // ============================================================================ @@ -26,64 +27,74 @@ pub enum BuildSystem { /// Unified build system configuration. /// Single source of truth for build/test/lint operations. +/// +/// Command vectors use `String` to support composed binary names +/// via [`cargo::name`]. #[derive(Debug, Clone)] pub struct BuildConfig { /// Build system (cargo, buck2) pub build_system: BuildSystem, /// Command to run codegen - pub codegen_command: Vec<&'static str>, + pub codegen_command: Vec, /// Command to run daggen - pub daggen_command: Vec<&'static str>, + pub daggen_command: Vec, /// Command to build all targets - pub build_command: Vec<&'static str>, + pub build_command: Vec, /// Command to run tests - pub test_command: Vec<&'static str>, + pub test_command: Vec, /// Command to run linter - pub lint_command: Vec<&'static str>, + pub lint_command: Vec, /// Command to auto-fix lint issues (cargo clippy --fix) - pub lint_fix_command: Vec<&'static str>, + pub lint_fix_command: Vec, /// Command to format code - pub fmt_command: Vec<&'static str>, + pub fmt_command: Vec, /// Command to check formatting - pub fmt_check_command: Vec<&'static str>, + pub fmt_check_command: Vec, /// Command to type-check without full build - pub check_command: Vec<&'static str>, + pub check_command: Vec, /// Command to generate CI YAML - pub ci_yaml_command: Vec<&'static str>, + pub ci_yaml_command: Vec, +} + +/// Helper to convert a list of &str into Vec. +fn strs(parts: &[&str]) -> Vec { + parts.iter().map(|s| s.to_string()).collect() } impl BuildConfig { /// Default cargo-based build config. pub fn cargo() -> Self { + let codegen = CargoInvocation::standalone("codegen"); Self { build_system: BuildSystem::Cargo, - codegen_command: vec!["cargo", "run", "-p", "gunbc-codegen", "--release", "--", "codegen"], - daggen_command: vec!["cargo", "run", "-p", "gunbc-codegen", "--release", "--", "daggen"], - build_command: vec!["cargo", "build", "--all-targets"], - test_command: vec!["cargo", "test"], - lint_command: vec!["cargo", "clippy", "--all-targets", "--", "-D", "warnings"], - lint_fix_command: vec!["cargo", "clippy", "--fix", "--workspace", "--allow-dirty", "--allow-staged", "--", "-D", "warnings"], - fmt_command: vec!["cargo", "fmt"], - fmt_check_command: vec!["cargo", "fmt", "--", "--check"], - check_command: vec!["cargo", "check", "--all-targets"], - ci_yaml_command: vec!["cargo", "run", "-p", "gunbc-codegen", "--release", "--", "cigen"], + codegen_command: codegen.run_with_args(&["--release", "--", "codegen"]), + daggen_command: codegen.run_with_args(&["--release", "--", "daggen"]), + build_command: strs(&["cargo", "build", "--all-targets"]), + test_command: strs(&["cargo", "test"]), + lint_command: strs(&["cargo", "clippy", "--all-targets", "--", "-D", "warnings"]), + lint_fix_command: strs(&["cargo", "clippy", "--fix", "--workspace", "--allow-dirty", "--allow-staged", "--", "-D", "warnings"]), + fmt_command: strs(&["cargo", "fmt"]), + fmt_check_command: strs(&["cargo", "fmt", "--", "--check"]), + check_command: strs(&["cargo", "check", "--all-targets"]), + ci_yaml_command: codegen.run_with_args(&["--release", "--", "cigen"]), } } /// Buck2-based build config (for future use). pub fn buck2() -> Self { + let codegen = CargoInvocation::standalone("codegen"); Self { build_system: BuildSystem::Buck2, - codegen_command: vec!["cargo", "run", "-p", "gunbc-codegen", "--release", "--", "codegen"], - daggen_command: vec!["cargo", "run", "-p", "gunbc-codegen", "--release", "--", "daggen"], - build_command: vec!["buck2", "build", "//..."], - test_command: vec!["buck2", "test", "//..."], - lint_command: vec!["buck2", "run", "//tools:clippy"], - lint_fix_command: vec!["cargo", "clippy", "--fix", "--workspace", "--allow-dirty", "--allow-staged", "--", "-D", "warnings"], - fmt_command: vec!["cargo", "fmt"], // fmt stays cargo - fmt_check_command: vec!["cargo", "fmt", "--", "--check"], - check_command: vec!["buck2", "build", "//..."], // buck2 check is same as build - ci_yaml_command: vec!["cargo", "run", "-p", "gunbc-codegen", "--release", "--", "cigen"], + codegen_command: codegen.run_with_args(&["--release", "--", "codegen"]), + daggen_command: codegen.run_with_args(&["--release", "--", "daggen"]), + build_command: strs(&["buck2", "build", "//..."]), + test_command: strs(&["buck2", "test", "//..."]), + lint_command: strs(&["buck2", "run", "//tools:clippy"]), + lint_fix_command: strs(&["cargo", "clippy", "--fix", "--workspace", "--allow-dirty", "--allow-staged", "--", "-D", "warnings"]), + fmt_command: strs(&["cargo", "fmt"]), // fmt stays cargo + fmt_check_command: strs(&["cargo", "fmt", "--", "--check"]), + check_command: strs(&["buck2", "build", "//..."]), // buck2 check is same as build + ci_yaml_command: codegen.run_with_args(&["--release", "--", "cigen"]), } } @@ -144,8 +155,8 @@ pub fn default_build_config() -> BuildConfig { /// Information about a gunbc tool. #[derive(Debug, Clone)] pub struct ToolInfo { - /// Crate name (e.g., "gunbc-gist") - pub crate_name: String, + /// How to invoke this tool via cargo. + pub invocation: gunbc_ir::CargoInvocation, /// Short name for make target (e.g., "gist") pub short_name: String, /// Description for help text @@ -170,14 +181,16 @@ pub struct ExtraTarget { } impl ToolInfo { - /// Create a new tool info. + /// Create a tool in its own package (e.g., `cargo run -p gunbc-gist`). + /// + /// Prefer [`Self::standalone`] which composes the binary name from a component. pub fn new( - crate_name: impl Into, + binary: impl Into, short_name: impl Into, description: impl Into, ) -> Self { Self { - crate_name: crate_name.into(), + invocation: gunbc_ir::CargoInvocation::new(binary), short_name: short_name.into(), description: description.into(), entrypoints: Vec::new(), @@ -186,6 +199,84 @@ impl ToolInfo { } } + /// Create a tool that's a binary inside another package + /// (e.g., `cargo run -p gunbc-dag --bin gunbc-ci`). + /// + /// Prefer [`Self::composed`] which composes both names from components. + pub fn in_package( + binary: impl Into, + package: impl Into, + short_name: impl Into, + description: impl Into, + ) -> Self { + Self { + invocation: gunbc_ir::CargoInvocation::in_package(binary, package), + short_name: short_name.into(), + description: description.into(), + entrypoints: Vec::new(), + extra_targets: Vec::new(), + has_declarative_dag: false, + } + } + + /// Create a standalone tool from its component name. + /// + /// The binary name is composed as `{PREFIX}-{component}`, and the + /// component is used as the short name for make targets. + /// + /// # Examples + /// + /// ```ignore + /// ToolInfo::standalone("gist", "Create a GitHub gist") + /// // binary: "gunbc-gist", short_name: "gist" + /// ``` + pub fn standalone( + component: &str, + description: impl Into, + ) -> Self { + Self { + invocation: gunbc_ir::CargoInvocation::standalone(component), + short_name: component.to_string(), + description: description.into(), + entrypoints: Vec::new(), + extra_targets: Vec::new(), + has_declarative_dag: false, + } + } + + /// Create a tool that lives inside another package, both composed + /// from component names. + /// + /// The binary is `{PREFIX}-{component}`, the package is + /// `{PREFIX}-{package_component}`, and the component is used as + /// the short name. + /// + /// # Examples + /// + /// ```ignore + /// ToolInfo::composed("ci", "dag", "Run CI pipeline") + /// // binary: "gunbc-ci", package: "gunbc-dag", short_name: "ci" + /// ``` + pub fn composed( + component: &str, + package_component: &str, + description: impl Into, + ) -> Self { + Self { + invocation: gunbc_ir::CargoInvocation::composed(component, package_component), + short_name: component.to_string(), + description: description.into(), + entrypoints: Vec::new(), + extra_targets: Vec::new(), + has_declarative_dag: false, + } + } + + /// The binary name (e.g., "gunbc-ci"). + pub fn binary_name(&self) -> &str { + &self.invocation.binary + } + /// Add an entrypoint parameter. pub fn with_param(mut self, param: EntrypointParam) -> Self { self.entrypoints.push(param); @@ -575,9 +666,9 @@ impl ToolRegistry { meta_targets: default_meta_targets(), }; - // gunbc-gist + // gunbc-gist: standalone package registry.register( - ToolInfo::new("gunbc-gist", "gist", "Create a GitHub gist from code files") + ToolInfo::standalone("gist", "Create a GitHub gist from code files") .with_param( EntrypointParam::new("repo_path", "REPO", "--repo", "String") .with_default("."), @@ -588,9 +679,9 @@ impl ToolRegistry { ), ); - // gunbc-buck2 + // gunbc-buck2: standalone package registry.register( - ToolInfo::new("gunbc-buck2", "buck2", "Generate BUCK file from Cargo.toml") + ToolInfo::standalone("buck2", "Generate BUCK file from Cargo.toml") .with_param( EntrypointParam::new("cargo_toml_path", "INPUT", "--input", "String") .with_default("Cargo.toml"), @@ -601,9 +692,9 @@ impl ToolRegistry { ), ); - // gunbc-makegen (self!) - has declarative DAG + // gunbc-makegen: binary in gunbc-dag package (has declarative DAG) registry.register( - ToolInfo::new("gunbc-makegen", "makegen", "Generate Makefile from tool registry") + ToolInfo::composed("makegen", "dag", "Generate Makefile from tool registry") .with_param( EntrypointParam::new("output_path", "OUTPUT", "--output", "String") .with_default(DEFAULT_MAKEFILE_FILENAME), @@ -611,23 +702,23 @@ impl ToolRegistry { .with_declarative_dag(), ); - // gunbc-deps + // gunbc-deps: standalone package registry.register( - ToolInfo::new("gunbc-deps", "deps", "Install tool dependencies") + ToolInfo::standalone("deps", "Install tool dependencies") .with_param( EntrypointParam::new("manifest_path", "MANIFEST", "--manifest", "String") .with_default(DEFAULT_MANIFEST_FILENAME), ), ); - // gunbc-ci + // gunbc-ci: binary in gunbc-dag package registry.register( - ToolInfo::new("gunbc-ci", "ci", "Run CI pipeline"), + ToolInfo::composed("ci", "dag", "Run CI pipeline"), ); - // gunbc-bootstrap + // gunbc-bootstrap: binary in gunbc-dag package registry.register( - ToolInfo::new("gunbc-bootstrap", "bootstrap", "Generate Makefile and .gitignore"), + ToolInfo::composed("bootstrap", "dag", "Generate Makefile and .gitignore"), ); // NOTE: prep tool has been removed - CI now handles all preparation @@ -649,15 +740,15 @@ mod tests { fn test_build_config_cargo() { let config = BuildConfig::cargo(); assert_eq!(config.build_system, BuildSystem::Cargo); - assert!(config.build_command.contains(&"cargo")); - assert!(config.test_command.contains(&"test")); + assert!(config.build_command.iter().any(|s| s == "cargo")); + assert!(config.test_command.iter().any(|s| s == "test")); } #[test] fn test_build_config_buck2() { let config = BuildConfig::buck2(); assert_eq!(config.build_system, BuildSystem::Buck2); - assert!(config.build_command.contains(&"buck2")); + assert!(config.build_command.iter().any(|s| s == "buck2")); } #[test] diff --git a/gunbc-dag/src/makegen/render.rs b/gunbc-dag/src/makegen/render.rs index 445d9b2bec4..0449f39847c 100644 --- a/gunbc-dag/src/makegen/render.rs +++ b/gunbc-dag/src/makegen/render.rs @@ -8,6 +8,7 @@ use crate::makegen::registry::{ BuildConfig, EntrypointParam, ExtraTarget, MetaTarget, PrepLevel, ToolInfo, ToolRegistry, }; +use gunbc_ir::CargoInvocation; use gunbc_ir::language::MAKEFILE; use gunbc_ir::Renderable; @@ -42,9 +43,13 @@ impl<'a> MakefileRenderer<'a> { } } +/// Composed generator name for the Makefile/gitignore renderer. +/// This must match `cargo::name("makegen")` — verified by test. +pub(crate) const MAKEGEN_NAME: &str = "gunbc-makegen"; + impl Renderable for MakefileRenderer<'_> { fn generator_name(&self) -> &str { - "gunbc-makegen" + MAKEGEN_NAME } fn regenerate_command(&self) -> &str { @@ -168,7 +173,11 @@ fn render_core_targets(config: &BuildConfig) -> String { // Rollback transaction: remove all generated artifacts output.push_str("# Rollback transaction: remove all generated artifacts\n"); output.push_str("clean:\n"); - output.push_str(&format!("{}@cargo run -p gunbc-codegen --release -- rollback\n\n", INDENT)); + output.push_str(&format!( + "{}@{} --release -- rollback\n\n", + INDENT, + CargoInvocation::standalone("codegen").command(), + )); output } @@ -392,13 +401,13 @@ fn render_tool_target(tool: &ToolInfo) -> String { .map(|p| format!("{} ({})", p.port_name, p.type_hint)) .collect::>() .join(", "); - output.push_str(&format!("# {} entrypoints: {}\n", tool.crate_name, port_list)); + output.push_str(&format!("# {} entrypoints: {}\n", tool.binary_name(), port_list)); // Target with ensure-codegen dependency output.push_str(&format!("{}: ensure-codegen\n", tool.short_name)); output.push_str(&format!( - "\t@cargo run -p {} --{}", - tool.crate_name, + "\t@{} --{}", + tool.invocation.command(), render_cli_args(&tool.entrypoints) )); output.push_str("\n\n"); @@ -412,8 +421,8 @@ fn render_dry_run_target(tool: &ToolInfo) -> String { output.push_str(&format!("{}-dry: ensure-codegen\n", tool.short_name)); output.push_str(&format!( - "\t@cargo run -p {} -- --dry-run{}", - tool.crate_name, + "\t@{} -- --dry-run{}", + tool.invocation.command(), render_cli_args(&tool.entrypoints) )); output.push_str("\n\n"); @@ -460,6 +469,7 @@ fn render_cli_args(params: &[EntrypointParam]) -> String { #[cfg(test)] mod tests { use super::*; + use gunbc_ir::cargo; #[test] fn test_render_makefile_has_header() { @@ -568,7 +578,9 @@ mod tests { let registry = ToolRegistry::default_registry(); let renderer = MakefileRenderer::new(®istry); - assert_eq!(renderer.generator_name(), "gunbc-makegen"); + // Verify the const matches the composed name + assert_eq!(MAKEGEN_NAME, cargo::name("makegen")); + assert_eq!(renderer.generator_name(), MAKEGEN_NAME); } #[test] diff --git a/lib/tools/buck2/src/ops.rs b/lib/tools/buck2/src/ops.rs index ae12ec0f8c6..673b736847f 100644 --- a/lib/tools/buck2/src/ops.rs +++ b/lib/tools/buck2/src/ops.rs @@ -224,7 +224,8 @@ fn execute_generate_targets( let mut buck_content = String::new(); // Header using language module's generated_header for consistency - buck_content.push_str(&generated_header("gunbc-buck2", "gunbc-buck2", "#")); + let buck2_name = gunbc_ir::cargo::name("buck2"); + buck_content.push_str(&generated_header(&buck2_name, &buck2_name, "#")); buck_content.push_str("\n\n"); buck_content.push_str("load(\"@prelude//rust:defs.bzl\", \"rust_binary\", \"rust_library\")\n\n"); diff --git a/lib/tools/clippy/src/config.rs b/lib/tools/clippy/src/config.rs index e9534629007..3ab7b2d046b 100644 --- a/lib/tools/clippy/src/config.rs +++ b/lib/tools/clippy/src/config.rs @@ -165,6 +165,7 @@ impl ClippyConfig { "I6: No escape hatches. Use node.requires(&cli::TOOL). Command::new only in transport executor.", ) // Document approved crates (minimal exceptions) + // Crate names follow the {PREFIX}-{component} pattern (see cargo::name) .allow_crate( "gunbc-transport", "IS the I/O boundary - the designated place for I/O", @@ -309,13 +310,20 @@ impl ClippyConfigRenderer { } } +/// Composed generator name for ClippyConfigRenderer. +/// Must match `cargo::name("clippy")` — verified by test. +const CLIPPY_GENERATOR_NAME: &str = "gunbc-clippy"; +/// Regenerate command — must be `&str` for [`Renderable`], verified by test +/// to match `CargoInvocation::standalone("codegen").command() + " -- clippy-toml"`. +const CLIPPY_REGENERATE_CMD: &str = "cargo run -p gunbc-codegen -- clippy-toml"; + impl Renderable for ClippyConfigRenderer { fn generator_name(&self) -> &str { - "gunbc-clippy" + CLIPPY_GENERATOR_NAME } fn regenerate_command(&self) -> &str { - "cargo run -p gunbc-codegen -- clippy-toml" + CLIPPY_REGENERATE_CMD } fn format_id(&self) -> &str { @@ -420,4 +428,13 @@ mod tests { let content = renderer.render_content(); assert!(content.contains("disallowed-methods")); } + + #[test] + fn test_regenerate_command_matches_composed() { + let expected = format!( + "{} -- clippy-toml", + gunbc_ir::CargoInvocation::standalone("codegen").command() + ); + assert_eq!(CLIPPY_REGENERATE_CMD, expected); + } } diff --git a/output b/output deleted file mode 100644 index 7ef2ac41a15..00000000000 --- a/output +++ /dev/null @@ -1,1328 +0,0 @@ -{ - "graphs": [ - { - "id": "gunbc-gist", - "name": "gunbc-gist", - "nodes": [ - { - "id": "list_files", - "label": "list_files", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "repo_path", - "type": "String", - "is_entrypoint": true, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "files", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": true, - "classes": [ - "entrypoint" - ] - }, - { - "id": "filter_files", - "label": "filter_files", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "files", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "files", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": false, - "classes": [] - }, - { - "id": "read_files", - "label": "read_files", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "files", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "repo_path", - "type": "String", - "is_entrypoint": true, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "contents", - "type": "MapStrStr", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": true, - "classes": [ - "entrypoint" - ] - }, - { - "id": "render_markdown", - "label": "render_markdown", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "contents", - "type": "MapStrStr", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "markdown", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": false, - "classes": [] - }, - { - "id": "prepare_gist_request", - "label": "prepare_gist_request", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "markdown", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "request", - "type": "TransportRequest", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": false, - "classes": [] - }, - { - "id": "execute_transport", - "label": "execute_transport", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "request", - "type": "TransportRequest", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "response", - "type": "TransportResponse", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "url", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - } - ], - "edges": [ - { - "id": "e0", - "source": "list_files", - "target": "filter_files", - "source_port": "files", - "target_port": "files", - "data_type": "StrList" - }, - { - "id": "e1", - "source": "filter_files", - "target": "read_files", - "source_port": "files", - "target_port": "files", - "data_type": "StrList" - }, - { - "id": "e2", - "source": "read_files", - "target": "render_markdown", - "source_port": "contents", - "target_port": "contents", - "data_type": "MapStrStr" - }, - { - "id": "e3", - "source": "render_markdown", - "target": "prepare_gist_request", - "source_port": "markdown", - "target_port": "markdown", - "data_type": "String" - }, - { - "id": "e4", - "source": "prepare_gist_request", - "target": "execute_transport", - "source_port": "request", - "target_port": "request", - "data_type": "TransportRequest" - } - ], - "meta": { - "node_count": 6, - "edge_count": 5, - "boundary_count": 1, - "entrypoint_count": 2, - "max_depth": 0 - } - }, - { - "id": "gunbc-buck2", - "name": "gunbc-buck2", - "nodes": [ - { - "id": "parse_cargo_toml", - "label": "parse_cargo_toml", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "cargo_toml_path", - "type": "String", - "is_entrypoint": true, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "cargo_toml", - "type": "Json", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": true, - "classes": [ - "entrypoint" - ] - }, - { - "id": "extract_deps", - "label": "extract_deps", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "cargo_toml", - "type": "Json", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "members", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "deps", - "type": "MapStrStr", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": false, - "classes": [] - }, - { - "id": "generate_targets", - "label": "generate_targets", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "members", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "deps", - "type": "MapStrStr", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "buck_content", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": false, - "classes": [] - }, - { - "id": "prepare_file_write", - "label": "prepare_file_write", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "content", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "output_path", - "type": "String", - "is_entrypoint": true, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "request", - "type": "TransportRequest", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": true, - "classes": [ - "entrypoint" - ] - }, - { - "id": "execute_transport", - "label": "execute_transport", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "request", - "type": "TransportRequest", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "response", - "type": "TransportResponse", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "written_path", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "content", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - } - ], - "edges": [ - { - "id": "e0", - "source": "parse_cargo_toml", - "target": "extract_deps", - "source_port": "cargo_toml", - "target_port": "cargo_toml", - "data_type": "Json" - }, - { - "id": "e1", - "source": "extract_deps", - "target": "generate_targets", - "source_port": "members", - "target_port": "members", - "data_type": "StrList" - }, - { - "id": "e2", - "source": "extract_deps", - "target": "generate_targets", - "source_port": "deps", - "target_port": "deps", - "data_type": "MapStrStr" - }, - { - "id": "e3", - "source": "generate_targets", - "target": "prepare_file_write", - "source_port": "buck_content", - "target_port": "content", - "data_type": "String" - }, - { - "id": "e4", - "source": "prepare_file_write", - "target": "execute_transport", - "source_port": "request", - "target_port": "request", - "data_type": "TransportRequest" - } - ], - "meta": { - "node_count": 5, - "edge_count": 5, - "boundary_count": 1, - "entrypoint_count": 2, - "max_depth": 0 - } - }, - { - "id": "gunbc-makegen", - "name": "gunbc-makegen", - "nodes": [ - { - "id": "load_registry", - "label": "load_registry", - "parent": null, - "type": "opaque", - "inputs": [], - "outputs": [ - { - "name": "tool_count", - "type": "Int", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "tool_names", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "registry", - "type": "Json", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - }, - { - "id": "render_makefile", - "label": "render_makefile", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "registry", - "type": "Json", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "makefile_content", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": false, - "classes": [] - }, - { - "id": "write_makefile", - "label": "write_makefile", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "makefile_content", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "output_path", - "type": "String", - "is_entrypoint": true, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "written_path", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "content", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "changed", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": true, - "classes": [ - "boundary", - "entrypoint" - ] - } - ], - "edges": [ - { - "id": "e0", - "source": "load_registry", - "target": "render_makefile", - "source_port": "registry", - "target_port": "registry", - "data_type": "Json" - }, - { - "id": "e1", - "source": "render_makefile", - "target": "write_makefile", - "source_port": "makefile_content", - "target_port": "makefile_content", - "data_type": "String" - } - ], - "meta": { - "node_count": 3, - "edge_count": 2, - "boundary_count": 2, - "entrypoint_count": 1, - "max_depth": 0 - } - }, - { - "id": "gunbc-deps", - "name": "gunbc-deps", - "nodes": [ - { - "id": "load_manifest", - "label": "load_manifest", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "manifest_path", - "type": "String", - "is_entrypoint": true, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "dep_count", - "type": "Int", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "dep_names", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "manifest_path", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": true, - "is_entrypoint": true, - "classes": [ - "boundary", - "entrypoint" - ] - }, - { - "id": "generate_scripts", - "label": "generate_scripts", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "manifest_path", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "install_script", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "already_installed", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "needs_install", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "platform", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - }, - { - "id": "execute_installs", - "label": "execute_installs", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "install_script", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "executed", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "script", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - } - ], - "edges": [ - { - "id": "e0", - "source": "load_manifest", - "target": "generate_scripts", - "source_port": "manifest_path", - "target_port": "manifest_path", - "data_type": "String" - }, - { - "id": "e1", - "source": "generate_scripts", - "target": "execute_installs", - "source_port": "install_script", - "target_port": "install_script", - "data_type": "String" - } - ], - "meta": { - "node_count": 3, - "edge_count": 2, - "boundary_count": 3, - "entrypoint_count": 1, - "max_depth": 0 - } - }, - { - "id": "gunbc-ci", - "name": "gunbc-ci", - "nodes": [ - { - "id": "setup_deps", - "label": "setup_deps", - "parent": null, - "type": "opaque", - "inputs": [], - "outputs": [ - { - "name": "deps_checked", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "deps_installed", - "type": "Int", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "message", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - }, - { - "id": "build", - "label": "build", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "deps_checked", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "build_success", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "build_stdout", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "build_stderr", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - }, - { - "id": "test", - "label": "test", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "build_success", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "test_success", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "test_skipped", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "test_stdout", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "test_stderr", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - }, - { - "id": "lint", - "label": "lint", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "build_success", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "lint_success", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "lint_skipped", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "lint_stdout", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "lint_stderr", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - }, - { - "id": "report", - "label": "report", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "build_success", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "test_success", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "lint_success", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "overall_success", - "type": "Bool", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "report", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - } - ], - "edges": [ - { - "id": "e0", - "source": "setup_deps", - "target": "build", - "source_port": "deps_checked", - "target_port": "deps_checked", - "data_type": "Bool" - }, - { - "id": "e1", - "source": "build", - "target": "test", - "source_port": "build_success", - "target_port": "build_success", - "data_type": "Bool" - }, - { - "id": "e2", - "source": "build", - "target": "lint", - "source_port": "build_success", - "target_port": "build_success", - "data_type": "Bool" - }, - { - "id": "e3", - "source": "build", - "target": "report", - "source_port": "build_success", - "target_port": "build_success", - "data_type": "Bool" - }, - { - "id": "e4", - "source": "test", - "target": "report", - "source_port": "test_success", - "target_port": "test_success", - "data_type": "Bool" - }, - { - "id": "e5", - "source": "lint", - "target": "report", - "source_port": "lint_success", - "target_port": "lint_success", - "data_type": "Bool" - } - ], - "meta": { - "node_count": 5, - "edge_count": 6, - "boundary_count": 5, - "entrypoint_count": 0, - "max_depth": 0 - } - }, - { - "id": "gunbc-bootstrap", - "name": "gunbc-bootstrap", - "nodes": [ - { - "id": "scan_workspace", - "label": "scan_workspace", - "parent": null, - "type": "opaque", - "inputs": [], - "outputs": [ - { - "name": "crate_count", - "type": "Int", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "crate_names", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - }, - { - "id": "generate_makefile", - "label": "generate_makefile", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "crate_names", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "makefile_content", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": false, - "classes": [] - }, - { - "id": "generate_gitignore", - "label": "generate_gitignore", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "crate_names", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "gitignore_content", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": false, - "classes": [] - }, - { - "id": "write_files", - "label": "write_files", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "makefile_content", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "gitignore_content", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "files_written", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "write_count", - "type": "Int", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - } - ], - "edges": [ - { - "id": "e0", - "source": "scan_workspace", - "target": "generate_makefile", - "source_port": "crate_names", - "target_port": "crate_names", - "data_type": "StrList" - }, - { - "id": "e1", - "source": "scan_workspace", - "target": "generate_gitignore", - "source_port": "crate_names", - "target_port": "crate_names", - "data_type": "StrList" - }, - { - "id": "e2", - "source": "generate_makefile", - "target": "write_files", - "source_port": "makefile_content", - "target_port": "makefile_content", - "data_type": "String" - }, - { - "id": "e3", - "source": "generate_gitignore", - "target": "write_files", - "source_port": "gitignore_content", - "target_port": "gitignore_content", - "data_type": "String" - } - ], - "meta": { - "node_count": 4, - "edge_count": 4, - "boundary_count": 2, - "entrypoint_count": 0, - "max_depth": 0 - } - }, - { - "id": "gunbc-viz", - "name": "gunbc-viz", - "nodes": [ - { - "id": "collect_dags", - "label": "collect_dags", - "parent": null, - "type": "opaque", - "inputs": [], - "outputs": [ - { - "name": "graph_count", - "type": "Int", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "graph_names", - "type": "StrList", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "graphs", - "type": "Json", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - }, - { - "id": "export_json", - "label": "export_json", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "graphs", - "type": "Json", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "json_content", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "content", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - }, - { - "id": "prepare_file_write", - "label": "prepare_file_write", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "content", - "type": "String", - "is_entrypoint": false, - "is_boundary": false - }, - { - "name": "output_path", - "type": "String", - "is_entrypoint": true, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "request", - "type": "TransportRequest", - "is_entrypoint": false, - "is_boundary": false - } - ], - "is_boundary": false, - "is_entrypoint": true, - "classes": [ - "entrypoint" - ] - }, - { - "id": "execute_transport", - "label": "execute_transport", - "parent": null, - "type": "opaque", - "inputs": [ - { - "name": "request", - "type": "TransportRequest", - "is_entrypoint": false, - "is_boundary": false - } - ], - "outputs": [ - { - "name": "response", - "type": "TransportResponse", - "is_entrypoint": false, - "is_boundary": true - }, - { - "name": "written_path", - "type": "String", - "is_entrypoint": false, - "is_boundary": true - } - ], - "is_boundary": true, - "is_entrypoint": false, - "classes": [ - "boundary" - ] - } - ], - "edges": [ - { - "id": "e0", - "source": "collect_dags", - "target": "export_json", - "source_port": "graphs", - "target_port": "graphs", - "data_type": "Json" - }, - { - "id": "e1", - "source": "export_json", - "target": "prepare_file_write", - "source_port": "content", - "target_port": "content", - "data_type": "String" - }, - { - "id": "e2", - "source": "prepare_file_write", - "target": "execute_transport", - "source_port": "request", - "target_port": "request", - "data_type": "TransportRequest" - } - ], - "meta": { - "node_count": 4, - "edge_count": 3, - "boundary_count": 3, - "entrypoint_count": 1, - "max_depth": 0 - } - } - ], - "generated_at": "1769657660" -} \ No newline at end of file