Skip to content
13 changes: 6 additions & 7 deletions docs/netsuke-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1997,13 +1997,12 @@ the targets listed in the `defaults` section of the manifest are built.
directory. It will invoke the Ninja backend with the appropriate flags, such
as `ninja -t clean`, to remove the outputs of the build rules.

- `Netsuke graph`: This command is an introspection and debugging tool. It will
run the Netsuke pipeline up to Stage 4 (IR Generation) and then invoke Ninja
with the graph tool, `ninja -t graph`. This outputs the complete build
dependency graph in the DOT language. The result can be piped through
`dot -Tsvg` or displayed via `netsuke graph --html` using an embedded
Dagre.js viewer. Visualizing the graph is invaluable for understanding and
debugging complex projects.
- `Netsuke graph`: This command is an introspection and debugging tool. It
runs the Netsuke pipeline through Ninja synthesis (Stage 6) to produce a
temporary `build.ninja`, then invokes Ninja with the graph tool,
`ninja -t graph`, which outputs the complete build dependency graph in the
DOT language. The result can be piped through Graphviz tools such as
`dot -Tsvg`. An optional `--html` renderer is planned for a later milestone.

- `Netsuke manifest FILE`: This command performs the pipeline up to Ninja
synthesis and writes the resulting Ninja file to `FILE` without invoking
Expand Down
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ library, and CLI ergonomics.

- [x] Implement the `clean` subcommand by invoking `ninja -t clean`.

- [ ] Implement the graph subcommand by invoking ninja -t graph to output
a DOT representation of the dependency graph.
- [x] Implement the graph subcommand by invoking `ninja -t graph` to output a
DOT representation of the dependency graph.

- [ ] Refine all CLI output for clarity, ensuring help messages are
descriptive and command feedback is intuitive.
Expand Down
10 changes: 7 additions & 3 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ guiding you.

The `Netsukefile` is a YAML file describing your build process.

Netsuke targets YAML 1.2 and forbids duplicate keys in manifests. If the same
mapping key appears more than once (even if a YAML parser would normally accept
it with “last key wins” behaviour), Netsuke treats this as an error.

### Top-Level Structure

```yaml
Expand Down Expand Up @@ -497,9 +501,9 @@ netsuke [OPTIONS] [COMMAND] [TARGETS...]
rules/targets to be properly configured for cleaning in Ninja (often via
`phony` targets).

- `graph`: Generates the build dependency graph and outputs it in DOT format
(suitable for Graphviz). Future versions may support other formats like
`--html`.
- `graph`: Generates the build dependency graph by running `ninja -t graph` on
the generated `build.ninja`, outputting DOT to stdout (suitable for
Graphviz). Future versions may support other formats like `--html`.

### Exit Codes

Expand Down
45 changes: 34 additions & 11 deletions src/runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,7 @@ pub fn run(cli: &Cli) -> Result<()> {
Ok(())
}
Commands::Clean => handle_clean(cli),
Commands::Graph => {
info!(target: "netsuke::subcommand", subcommand = "graph", "Subcommand requested");
Ok(())
}
Commands::Graph => handle_graph(cli),
}
}

Expand Down Expand Up @@ -156,32 +153,58 @@ fn handle_build(cli: &Cli, args: &BuildArgs) -> Result<()> {
Ok(())
}

/// Remove build artefacts by invoking `ninja -t clean`.
/// Execute a Ninja tool (e.g., `ninja -t clean`) using a temporary build file.
///
/// Generates the Ninja manifest to a temporary file, then invokes Ninja's clean
/// tool to remove all outputs defined by the build graph.
/// Generates the Ninja manifest to a temporary file, then invokes Ninja with
/// `-t <tool>` while preserving the CLI settings (working directory and job
/// count).
///
/// # Errors
///
/// Returns an error if manifest generation or Ninja execution fails.
fn handle_clean(cli: &Cli) -> Result<()> {
info!(target: "netsuke::subcommand", subcommand = "clean", "Subcommand requested");
fn handle_ninja_tool(cli: &Cli, tool: &str) -> Result<()> {
info!(target: "netsuke::subcommand", subcommand = tool, "Subcommand requested");
let ninja = generate_ninja(cli)?;

let tmp = process::create_temp_ninja_file(&ninja)?;
let build_path = tmp.path();

let program = process::resolve_ninja_program();
run_ninja_tool(program.as_path(), cli, build_path, "clean").with_context(|| {
run_ninja_tool(program.as_path(), cli, build_path, tool).with_context(|| {
format!(
"running {} -t clean with build file {}",
"running {} -t {} with build file {}",
program.display(),
tool,
build_path.display()
)
})?;
Ok(())
}

/// Remove build artefacts by invoking `ninja -t clean`.
///
/// Generates the Ninja manifest to a temporary file, then invokes Ninja's clean
/// tool to remove all outputs defined by the build graph.
///
/// # Errors
///
/// Returns an error if manifest generation or Ninja execution fails.
fn handle_clean(cli: &Cli) -> Result<()> {
handle_ninja_tool(cli, "clean")
}

/// Display build dependency graph by invoking `ninja -t graph`.
///
/// Generates the Ninja manifest to a temporary file, then invokes Ninja's graph
/// tool to emit a DOT representation to stdout.
///
/// # Errors
///
/// Returns an error if manifest generation or Ninja execution fails.
fn handle_graph(cli: &Cli) -> Result<()> {
handle_ninja_tool(cli, "graph")
}

/// Generate the Ninja manifest string from the Netsuke manifest referenced by `cli`.
///
/// # Errors
Expand Down
6 changes: 6 additions & 0 deletions test_support/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ pub struct NinjaEnvGuard {
_lock: EnvLock,
}

impl std::fmt::Debug for NinjaEnvGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NinjaEnvGuard").finish_non_exhaustive()
}
}

/// Override the `NINJA_ENV` variable with `path`, returning a guard that resets it.
///
/// In Rust 2024 `std::env::set_var` is `unsafe` because it mutates process-global
Expand Down
7 changes: 7 additions & 0 deletions test_support/src/env_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! synchronised, preventing interference between concurrently running tests.

use std::sync::{Mutex, MutexGuard};
use std::{fmt, fmt::Formatter};

static ENV_LOCK: Mutex<()> = Mutex::new(());

Expand All @@ -12,6 +13,12 @@ pub struct EnvLock {
_guard: MutexGuard<'static, ()>,
}

impl fmt::Debug for EnvLock {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("EnvLock").finish_non_exhaustive()
}
}

impl EnvLock {
/// Acquire the global lock serialising environment mutations.
pub fn acquire() -> Self {
Expand Down
35 changes: 35 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Shared helpers for integration tests.
//!
//! Integration tests under `tests/` compile as independent crates. This module
//! is included via `mod common;` in individual test files to share fixtures and
//! helpers while keeping test modules small and avoiding duplication.

use anyhow::{Context, Result};
use rstest::fixture;
use std::path::PathBuf;
use test_support::{
env::{NinjaEnvGuard, SystemEnv, override_ninja_env},
fake_ninja,
};

/// Create a temporary project with a Netsukefile from `minimal.yml`.
pub fn create_test_manifest() -> Result<(tempfile::TempDir, PathBuf)> {
let temp = tempfile::tempdir().context("create temp dir for test manifest")?;
let manifest_path = temp.path().join("Netsukefile");
std::fs::copy("tests/data/minimal.yml", &manifest_path)
.with_context(|| format!("copy minimal.yml to {}", manifest_path.display()))?;
Ok((temp, manifest_path))
}

/// Fixture: point `NINJA_ENV` at a fake `ninja` with a configurable exit code.
///
/// Returns: (tempdir holding ninja, `NINJA_ENV` guard)
#[fixture]
pub fn ninja_with_exit_code(
#[default(0u8)] exit_code: u8,
) -> Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)> {
let (ninja_dir, ninja_path) = fake_ninja(exit_code)?;
let env = SystemEnv::new();
let guard = override_ninja_env(&env, ninja_path.as_path());
Ok((ninja_dir, ninja_path, guard))
}
9 changes: 8 additions & 1 deletion tests/cucumber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ use netsuke::stdlib::{NetworkPolicy, StdlibState};
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
use std::{collections::HashMap, ffi::OsString, net::TcpListener};
use test_support::{PathGuard, env::restore_many, http};
use test_support::{
PathGuard,
env::{NinjaEnvGuard, restore_many},
http,
};

/// Shared state for Cucumber scenarios.
#[derive(Debug, Default, World)]
Expand Down Expand Up @@ -37,6 +41,8 @@ pub struct CliWorld {
pub temp: Option<tempfile::TempDir>,
/// Guard that restores `PATH` after each scenario.
pub path_guard: Option<PathGuard>,
/// Guard that overrides `NINJA_ENV` for deterministic Ninja resolution.
pub ninja_env_guard: Option<NinjaEnvGuard>,
/// Root directory for stdlib scenarios.
pub stdlib_root: Option<Utf8PathBuf>,
/// Captured output from the last stdlib render.
Expand Down Expand Up @@ -181,6 +187,7 @@ fn block_device_exists() -> bool {
impl Drop for CliWorld {
fn drop(&mut self) {
self.shutdown_http_server();
self.ninja_env_guard.take();
self.restore_environment();
self.stdlib_text = None;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@unix
Feature: Clean subcommand execution

Scenario: Clean invokes ninja with tool flag
Expand Down
29 changes: 29 additions & 0 deletions tests/features_unix/graph.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@unix
Feature: Graph subcommand execution

Scenario: Graph invokes ninja with tool flag
Given a fake ninja executable that expects the graph tool
And the CLI is parsed with "graph"
And the CLI uses the temporary directory
When the graph process is run
Then the command should succeed

Scenario: Graph fails when ninja fails
Given a fake ninja executable that exits with 1
And the CLI is parsed with "graph"
And the CLI uses the temporary directory
When the graph process is run
Then the command should fail with error "ninja exited"

Scenario: Graph respects jobs flag
Given a fake ninja executable that expects graph with 4 jobs
And the CLI is parsed with "-j 4 graph"
And the CLI uses the temporary directory
When the graph process is run
Then the command should succeed

Scenario: Graph fails when ninja is missing
Given no ninja executable is available
And the CLI is parsed with "graph"
When the graph process is run
Then the command should fail with error "No such file or directory"
Comment thread
leynos marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@unix
Feature: Ninja process execution

Scenario: Ninja succeeds
Expand Down
Loading
Loading