Skip to content

Implement Ninja generator#37

Closed
leynos wants to merge 5 commits intomainfrom
codex/implement-ninja-file-synthesizer
Closed

Implement Ninja generator#37
leynos wants to merge 5 commits intomainfrom
codex/implement-ninja-file-synthesizer

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Jul 31, 2025

Summary

  • implement Ninja file generation logic
  • expose generator via library
  • mark roadmap tasks done and document deterministic output
  • add unit and behaviour tests for generator

Testing

  • make lint
  • make test

https://chatgpt.com/codex/tasks/task_e_688a921dd3f08322b003b278f13cdce5

Summary by Sourcery

Implement a deterministic Ninja build file generator from the IR, expose it in the public API, update documentation to mark completion and describe deterministic output, and add comprehensive unit and behavior tests.

New Features:

  • Add ninja_gen module to generate deterministic Ninja build files from BuildGraph IR
  • Expose the Ninja generator in the public library API

Documentation:

  • Mark Ninja generator tasks as done in roadmap and document deterministic sorting and deduplication in design docs

Tests:

  • Add unit tests for ninja_gen module
  • Add Cucumber step definitions and feature tests for Ninja file generation

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jul 31, 2025

Reviewer's Guide

This PR introduces a new Ninja file generator module with deterministic output, exposes it through the public library API and CLI test harness, updates documentation and roadmap tasks to reflect completion, and adds comprehensive unit and behavior tests.

Class diagram for the new Ninja generator module

classDiagram
    class BuildGraph {
        +HashMap<String, Action> actions
        +HashMap<PathBuf, BuildEdge> targets
        +Vec<PathBuf> default_targets
    }
    class Action {
        +Recipe recipe
        +Option<String> description
        +Option<String> depfile
        +Option<String> deps_format
        +Option<String> pool
        +bool restat
    }
    class BuildEdge {
        +String action_id
        +Vec<PathBuf> inputs
        +Vec<PathBuf> explicit_outputs
        +Vec<PathBuf> implicit_outputs
        +Vec<PathBuf> order_only_deps
        +bool phony
        +bool always
    }
    class Recipe {
        <<enum>>
        Command
        Script
        Rule
    }
    class ninja_gen {
        +generate(graph: &BuildGraph) String
    }
    BuildGraph "1" o-- "many" Action : actions
    BuildGraph "1" o-- "many" BuildEdge : targets
    Action "1" o-- "1" Recipe
    BuildEdge "1" --> "1" Action : action_id
    ninja_gen ..> BuildGraph : uses
    ninja_gen ..> Action : uses
    ninja_gen ..> BuildEdge : uses
    ninja_gen ..> Recipe : uses
Loading

Class diagram for public API exposure of the Ninja generator

classDiagram
    class ninja_gen {
        +generate(graph: &BuildGraph) String
    }
    class lib {
        +pub mod ninja_gen
    }
    lib --> ninja_gen : exposes
Loading

File-Level Changes

Change Details Files
Implement Ninja file generator with deterministic output
  • Provide generate(graph) entry point that serializes rules, edges, and defaults
  • Sort and deduplicate actions and edges to ensure stable output
  • Build helper functions for rules, edges, defaults, and path joining
  • Add unit tests covering simple and phony build scenarios
src/ninja_gen.rs
tests/ninja_gen_tests.rs
Expose Ninja generator in public API and test world
  • Add ninja_gen module export to the library root
  • Extend Cucumber world struct to carry generated Ninja string
  • Update CLI test setup to capture and store ninja output
src/lib.rs
tests/cucumber.rs
Add behavior tests for Ninja file generation
  • Implement Cucumber step definitions for generating and asserting ninja content
  • Register ninja_steps in the steps module
  • Create feature file scenarios verifying rule and build statement presence
tests/steps/ninja_steps.rs
tests/steps/mod.rs
tests/features/ninja.feature
Update documentation and roadmap entries
  • Mark Ninja synthesizer and rule generation tasks as done in roadmap
  • Document deterministic sorting and edge deduplication in design guide
docs/roadmap.md
docs/netsuke-design.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jul 31, 2025

Summary by CodeRabbit

  • New Features

    • Introduced Ninja build file generation, enabling conversion from build graphs to Ninja format.
    • Exposed Ninja generator module in the public API.
    • Added comprehensive documentation detailing Ninja file generation, determinism, and portability.
  • Tests

    • Added unit, integration, and end-to-end tests for Ninja file generation, including snapshot and Cucumber-based scenarios.
    • Validated Ninja file correctness and no-op behaviour with real Ninja builds.
  • Chores

    • Updated CI workflow to display Ninja version.
    • Added new development dependencies for testing and snapshotting.
    • Updated roadmap to reflect completed Ninja generation tasks.

Walkthrough

Introduce a new Ninja build file generator module (ninja_gen.rs) that transforms the build graph IR into Ninja syntax. Expose this module publicly, update documentation and roadmap, and add comprehensive integration and snapshot tests for Ninja file generation. Update CI to show Ninja version and add relevant development dependencies.

Changes

Cohort / File(s) Change Summary
Ninja Generator Module
src/ninja_gen.rs, src/lib.rs
Add ninja_gen module for generating Ninja build files from the IR; expose it publicly.
Documentation Updates
docs/netsuke-design.md, docs/roadmap.md
Document deterministic Ninja generation, new integration tests, and mark roadmap items as completed.
Development Dependencies
Cargo.toml
Add insta (with yaml feature) and tempfile as development dependencies.
CI Workflow
.github/workflows/ci.yml
Add a step to display the installed Ninja version after Rust setup.
Integration and Snapshot Tests
tests/ninja_gen_tests.rs, tests/ninja_snapshot_tests.rs, tests/features/ninja.feature
Add unit, snapshot, and feature tests for Ninja file generation, including end-to-end validation and content checks.
Cucumber Integration Steps
tests/steps/ninja_steps.rs, tests/steps/mod.rs, tests/cucumber.rs
Add Cucumber step definitions and world state for Ninja file generation and verification in integration tests.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Integration Test
    participant World as CliWorld
    participant NinjaGen as ninja_gen::generate
    participant Ninja as Ninja Binary

    Test->>World: Retrieve BuildGraph
    Test->>NinjaGen: Call generate(BuildGraph)
    NinjaGen-->>Test: Return Ninja file content
    Test->>World: Store Ninja file content
    Test->>Ninja: Run Ninja commands with generated file
    Ninja-->>Test: Return build results
    Test->>Test: Assert output and no-op behaviour
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Document phony and always targets #4: Implements and documents new phony and always flags on build targets and their handling in IR and Ninja synthesis, directly related to the phony target logic in Ninja generation.
  • Add intermediate representation structures #32: Introduces the IR data structures (BuildGraph, Action, BuildEdge) that the new Ninja generator module consumes, forming the foundation for these changes.

Suggested reviewers

  • codescene-delta-analysis

Poem

In the forge where Ninja files are spun,
Determinism and testing are finally done.
With snapshot checks and phony delight,
The build graph marches into the night.
CI now shows Ninja’s face,
While tests ensure a flawless race.
🥷✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/implement-ninja-file-synthesizer

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @leynos - I've reviewed your changes - here's some feedback:

  • Instead of ignoring write errors (using let _ = writeln!(…)), consider propagating or unwrapping them to catch unexpected formatting issues during Ninja generation.
  • Edge sorting currently compares Vec<PathBuf> directly; using a joined string representation of the paths as a sort key would ensure consistent ordering across platforms.
  • Deduplication only checks the first explicit output, which may miss duplicate edges with the same later outputs—consider deduplicating based on the full set of outputs or clarifying this behavior in the documentation.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Instead of ignoring write errors (using `let _ = writeln!(…)`), consider propagating or unwrapping them to catch unexpected formatting issues during Ninja generation.
- Edge sorting currently compares `Vec<PathBuf>` directly; using a joined string representation of the paths as a sort key would ensure consistent ordering across platforms.
- Deduplication only checks the first explicit output, which may miss duplicate edges with the same later outputs—consider deduplicating based on the full set of outputs or clarifying this behavior in the documentation.

## Individual Comments

### Comment 1
<location> `src/ninja_gen.rs:64` </location>
<code_context>
+
+fn write_edges(out: &mut String, targets: &HashMap<PathBuf, BuildEdge>) {
+    let mut edges: Vec<&BuildEdge> = targets.values().collect();
+    edges.sort_by(|a, b| a.explicit_outputs.cmp(&b.explicit_outputs));
+    let mut seen = HashSet::new();
+    for edge in edges {
</code_context>

<issue_to_address>
Sorting edges by explicit_outputs may not guarantee a stable order if explicit_outputs contains multiple paths.

If explicit_outputs contains multiple paths, the current sorting may be non-deterministic. To ensure stable output, sort explicit_outputs within each edge before sorting the edges, or implement a more robust comparison method.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
fn write_edges(out: &mut String, targets: &HashMap<PathBuf, BuildEdge>) {
    let mut edges: Vec<&BuildEdge> = targets.values().collect();
    edges.sort_by(|a, b| a.explicit_outputs.cmp(&b.explicit_outputs));
    let mut seen = HashSet::new();
    for edge in edges {
=======
fn write_edges(out: &mut String, targets: &HashMap<PathBuf, BuildEdge>) {
    let mut edges: Vec<&BuildEdge> = targets.values().collect();
    // Sort explicit_outputs within each edge for stable comparison
    for edge in &mut edges {
        // We need to sort the explicit_outputs for each edge.
        // Since edges is Vec<&BuildEdge>, and BuildEdge is likely not mutable here,
        // we need to collect sorted keys for comparison instead.
        // So, instead, sort by a tuple of sorted explicit_outputs.
    }
    edges.sort_by(|a, b| {
        let mut a_outputs = a.explicit_outputs.clone();
        let mut b_outputs = b.explicit_outputs.clone();
        a_outputs.sort();
        b_outputs.sort();
        a_outputs.cmp(&b_outputs)
    });
    let mut seen = HashSet::new();
    for edge in edges {
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `src/ninja_gen.rs:93` </location>
<code_context>
+        let _ = write!(out, " || {}", join(&edge.order_only_deps));
+    }
+    let _ = writeln!(out);
+    if edge.always {
+        let _ = writeln!(out, "  restat = 1");
+    }
+    let _ = writeln!(out);
</code_context>

<issue_to_address>
Setting 'restat = 1' for 'always' may conflict with action-level restat.

Clarify which 'restat' setting takes precedence, or ensure only one source sets it to avoid duplicate lines.

Suggested implementation:

```rust
    let _ = writeln!(out);
    // Only set 'restat = 1' if not already set by the action.
    // Action-level 'restat' takes precedence over 'always'.
    if !edge.action_restat && edge.always {
        let _ = writeln!(out, "  restat = 1");
    }
    let _ = writeln!(out);

```

If there is no `edge.action_restat` boolean or equivalent, you will need to add logic to detect if the action-level restat is set, or adjust the condition to fit your codebase's conventions for action-level restat.
</issue_to_address>

### Comment 3
<location> `src/ninja_gen.rs:16` </location>
<code_context>
+
+/// Generate a Ninja build file as a string.
+#[must_use]
+pub fn generate(graph: &BuildGraph) -> String {
+    let mut out = String::new();
+    write_rules(&mut out, &graph.actions);
</code_context>

<issue_to_address>
Consider replacing the multiple formatting helper functions with `Display` trait implementations for actions and edges to simplify and clarify the code.

Here is one way to collapse all of the little `write_*` helpers and `join` into two `Display` impls.  Once you have these, your `generate` function becomes just “sort + write!(&edge)” or “sort + write!(&NamedAction)”, which is both shorter and more intention-revealing.

```rust
use std::fmt::{self, Display, Formatter};
use itertools::Itertools; // add `itertools = "0.10"` to Cargo.toml

/// Wrapper so we can include the rule‐name (the HashMap key) in the Display impl.
struct NamedAction<'a> {
    id: &'a str,
    action: &'a crate::ir::Action,
}

impl Display for NamedAction<'_> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        writeln!(f, "rule {}", self.id)?;
        match &self.action.recipe {
            Recipe::Command { command } => writeln!(f, "  command = {}", command)?,
            Recipe::Script { script } => {
                writeln!(f, "  command = /bin/sh -e -c \"")?;
                for line in script.lines() {
                    writeln!(f, "    {}", line)?;
                }
                writeln!(f, "  \"")?;
            }
            _ => unreachable!(),
        }
        if let Some(desc) = &self.action.description {
            writeln!(f, "  description = {}", desc)?;
        }
        if let Some(depfile) = &self.action.depfile {
            writeln!(f, "  depfile = {}", depfile)?;
        }
        if let Some(deps) = &self.action.deps_format {
            writeln!(f, "  deps = {}", deps)?;
        }
        if let Some(pool) = &self.action.pool {
            writeln!(f, "  pool = {}", pool)?;
        }
        if self.action.restat {
            writeln!(f, "  restat = 1")?;
        }
        writeln!(f) // blank line
    }
}

impl Display for crate::ir::BuildEdge {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let out = |p: &[std::path::PathBuf]| p.iter().map(|p| p.display()).join(" ");
        write!(f, "build {}", out(&self.explicit_outputs))?;
        if !self.implicit_outputs.is_empty() {
            write!(f, " | {}", out(&self.implicit_outputs))?;
        }
        let rule = if self.phony { "phony" } else { &self.action_id };
        write!(f, ": {}", rule)?;
        if !self.inputs.is_empty() {
            write!(f, " {}", out(&self.inputs))?;
        }
        if !self.order_only_deps.is_empty() {
            write!(f, " || {}", out(&self.order_only_deps))?;
        }
        writeln!(f)?;
        if self.always {
            writeln!(f, "  restat = 1")?;
        }
        writeln!(f)
    }
}
```

And now your `generate` can be shrunk to:

```rust
pub fn generate(graph: &BuildGraph) -> String {
    let mut out = String::new();

    // rules
    let mut acts: Vec<_> = graph.actions.iter().collect();
    acts.sort_by_key(|(id, _)| *id);
    for (id, action) in acts {
        write!(out, "{}", NamedAction { id, action }).unwrap();
    }

    // edges
    let mut edges: Vec<_> = graph.targets.values().collect();
    edges.sort_by(|a, b| a.explicit_outputs.cmp(&b.explicit_outputs));
    let mut seen = std::collections::HashSet::new();
    for e in edges {
        if e.explicit_outputs.first().map_or(false, |p| !seen.insert(p.clone())) {
            continue;
        }
        write!(out, "{}", e).unwrap();
    }

    // defaults
    if !graph.default_targets.is_empty() {
        let defs = graph
            .default_targets
            .iter()
            .sorted()
            .map(|p| p.display())
            .join(" ");
        writeln!(out, "default {}", defs).unwrap();
    }

    out
}
```

This removes all of the `write_rules`, `write_edge(s)`, `join`, `write_defaults` plumbing and puts the formatting where it belongs—in small, focused `Display` impls.
</issue_to_address>

### Comment 4
<location> `tests/features/ninja.feature:1` </location>
<code_context>
+Feature: Ninja file generation
+
+  Scenario: Generate build statements
</code_context>

<issue_to_address>
Consider replacing the Cucumber feature with simple pytest functions to test Ninja file generation.

The Cucumber feature here is adding a whole new DSL layer just to check that your generated Ninja text contains a couple of strings. You can get the same coverage with two tiny pytest functions—no feature files or step-defs required.

For example, under `tests/` create something like:

```python
# tests/test_ninja_generation.py
import pytest
from mypkg.compiler import compile_manifest
from mypkg.ninja import generate_ninja

@pytest.mark.parametrize("yml, substrings", [
    ("tests/data/rules.yml", ["rule", "build hello.o:"]),
    ("tests/data/phony.yml", ["build clean: phony"]),
])
def test_generate_ninja_contains_expected(yml, substrings):
    ir = compile_manifest(yml)
    ninja_text = generate_ninja(ir)
    for sub in substrings:
        assert sub in ninja_text
```

If you prefer “golden files” for full-output comparison, you can do:

```python
def test_ninja_matches_golden(tmp_path):
    ir = compile_manifest("tests/data/rules.yml")
    ninja_text = generate_ninja(ir)

    golden = (tmp_path / "rules.ninja")
    if not golden.exists():
        golden.write_text(ninja_text)   # first run: creates golden
        pytest.skip("Golden file created; re-run to verify")
    assert ninja_text == golden.read_text()
```

This keeps all current assertions, removes the extra Gherkin layer, and is just as readable and maintainable.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/ninja_gen.rs Outdated
Comment thread src/ninja_gen.rs Outdated
Comment thread src/ninja_gen.rs
Comment thread tests/features/ninja.feature
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (4)
src/ninja_gen.rs (3)

62-76: Address sorting stability concern raised in past review.

The sorting by explicit_outputs may be non-deterministic when outputs contain multiple paths, as flagged in the previous review. The deduplication logic using the first output also compounds this issue.

Apply this fix to ensure stable sorting:

 fn write_edges(out: &mut String, targets: &HashMap<PathBuf, BuildEdge>) {
     let mut edges: Vec<&BuildEdge> = targets.values().collect();
-    edges.sort_by(|a, b| a.explicit_outputs.cmp(&b.explicit_outputs));
+    edges.sort_by(|a, b| {
+        let mut a_outputs = a.explicit_outputs.clone();
+        let mut b_outputs = b.explicit_outputs.clone();
+        a_outputs.sort();
+        b_outputs.sort();
+        a_outputs.cmp(&b_outputs)
+    });
     let mut seen = HashSet::new();

93-95: Clarify restat precedence to avoid conflicts.

The edge-level restat = 1 when always is true could conflict with action-level restat settings, as noted in the previous review.

Add logic to prevent duplicate restat declarations:

     let _ = writeln!(out);
-    if edge.always {
+    // Only set restat if not already set by the action
+    if edge.always && !action_has_restat {
         let _ = writeln!(out, "  restat = 1");
     }

16-22: Consider Display trait implementation for cleaner code.

The previous comprehensive review suggested replacing helper functions with Display implementations for actions and edges. This would significantly simplify the code and improve maintainability.

The suggested Display implementation would reduce the generate function to just sorting and writing, making the code more intention-revealing and easier to maintain.

tests/features/ninja.feature (1)

1-13: Consider the complexity trade-off of Cucumber for simple assertions.

The Gherkin scenarios are well-structured and readable, but essentially test string containment. The past review comment correctly identifies that simple unit tests would achieve the same coverage with less complexity overhead.

However, if this aligns with an established BDD testing strategy across the project for consistency, the current approach is acceptable.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd898d5 and 77ca463.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • tests/snapshots/ninja/ninja_snapshot_tests__touch_manifest_ninja.snap is excluded by !**/*.snap
📒 Files selected for processing (12)
  • .github/workflows/ci.yml (1 hunks)
  • Cargo.toml (1 hunks)
  • docs/netsuke-design.md (1 hunks)
  • docs/roadmap.md (1 hunks)
  • src/lib.rs (1 hunks)
  • src/ninja_gen.rs (1 hunks)
  • tests/cucumber.rs (1 hunks)
  • tests/features/ninja.feature (1 hunks)
  • tests/ninja_gen_tests.rs (1 hunks)
  • tests/ninja_snapshot_tests.rs (1 hunks)
  • tests/steps/mod.rs (1 hunks)
  • tests/steps/ninja_steps.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.rs

📄 CodeRabbit Inference Engine (AGENTS.md)

**/*.rs: Clippy warnings MUST be disallowed.
Fix any warnings emitted during tests in the code itself rather than silencing them.
Where a function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a function has too many parameters, group related parameters in meaningfully named structs.
Where a function is returning a large error consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new functionality. Run both before and after making any change.
Every module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings.
Handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code unless absolutely necessary and document any usage clearly.
Place function attributes after doc comments.
Do not use return in single-line functions.
Use predicate functions for conditional criteria with more than two branches.
Lints must not be silenced except as a last resort.
Lint rule suppressions must be tightly scoped and include a clear reason.
Prefer expect over allow.
Prefer .expect() over .unwrap().
Use concat!() to combine long string literals rather than escaping newlines with a backslash.
Prefer semantic error enums: Derive std::error::Error (via the thiserror crate) for any condition the caller might inspect, retry, or map to an HTTP status.
Use an opaque error only at the app boundary: Use eyre::Report for human-readable logs; these should not be exposed in public APIs.
Never export the opaque type from a library: Convert to domain enums at API boundaries, and to eyre only in the main main() entrypoint or top-level async task.

Files:

  • tests/cucumber.rs
  • tests/steps/mod.rs
  • src/lib.rs
  • tests/ninja_gen_tests.rs
  • tests/ninja_snapshot_tests.rs
  • src/ninja_gen.rs
  • tests/steps/ninja_steps.rs

⚙️ CodeRabbit Configuration File

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.

Files:

  • tests/cucumber.rs
  • tests/steps/mod.rs
  • src/lib.rs
  • tests/ninja_gen_tests.rs
  • tests/ninja_snapshot_tests.rs
  • src/ninja_gen.rs
  • tests/steps/ninja_steps.rs
Cargo.toml

📄 CodeRabbit Inference Engine (AGENTS.md)

Cargo.toml: Use explicit version ranges in Cargo.toml and keep dependencies up-to-date.
Mandate caret requirements for all dependencies: All crate versions specified in Cargo.toml must use SemVer-compatible caret requirements (e.g., some-crate = "1.2.3").
Prohibit unstable version specifiers: The use of wildcard (*) or open-ended inequality (>=) version requirements is strictly forbidden. Tilde requirements (~) should only be used where a dependency must be locked to patch-level updates for a specific, documented reason.

Files:

  • Cargo.toml
docs/**/*.md

📄 CodeRabbit Inference Engine (AGENTS.md)

docs/**/*.md: Use the markdown files within the docs/ directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions.
Proactively update the relevant file(s) in the docs/ directory to reflect the latest state when new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve.

Files:

  • docs/roadmap.md
  • docs/netsuke-design.md
**/*.md

📄 CodeRabbit Inference Engine (AGENTS.md)

**/*.md: Documentation must use en-GB-oxendict spelling and grammar, except for the naming of the "LICENSE" file.
Validate Markdown files using make markdownlint.
Run make fmt after any documentation changes to format all Markdown files and fix table markup.
Validate Mermaid diagrams in Markdown files by running make nixie.
Markdown paragraphs and bullet points must be wrapped at 80 columns.
Code blocks in Markdown must be wrapped at 120 columns.
Tables and headings in Markdown must not be wrapped.
Use dashes (-) for list bullets in Markdown.
Use GitHub-flavoured Markdown footnotes ([^1]) for references and footnotes.

Files:

  • docs/roadmap.md
  • docs/netsuke-design.md

⚙️ CodeRabbit Configuration File

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Paragraphs and bullets must be wrapped to 80 columns, except where a long URL would prevent this (in which case, silence MD013 for that line)
  • Code blocks should be wrapped to 120 columns.
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/roadmap.md
  • docs/netsuke-design.md
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: leynos/netsuke#0
File: docs/roadmap.md:0-0
Timestamp: 2025-07-27T17:59:36.506Z
Learning: Applies to docs/src/ninja_gen.rs : Implement the Ninja file synthesizer in src/ninja_gen.rs to traverse the BuildGraph IR.
📚 Learning: applies to docs/src/ninja_gen.rs : implement the ninja file synthesizer in src/ninja_gen.rs to trave...
Learnt from: CR
PR: leynos/netsuke#0
File: docs/roadmap.md:0-0
Timestamp: 2025-07-27T17:59:36.506Z
Learning: Applies to docs/src/ninja_gen.rs : Implement the Ninja file synthesizer in src/ninja_gen.rs to traverse the BuildGraph IR.

Applied to files:

  • docs/roadmap.md
📚 Learning: applies to docs/src/ninja_gen.rs : write logic to generate ninja rule statements from ir::action str...
Learnt from: CR
PR: leynos/netsuke#0
File: docs/roadmap.md:0-0
Timestamp: 2025-07-27T17:59:36.506Z
Learning: Applies to docs/src/ninja_gen.rs : Write logic to generate Ninja rule statements from ir::Action structs and build statements from ir::BuildEdge structs.

Applied to files:

  • docs/roadmap.md
🧬 Code Graph Analysis (3)
tests/ninja_gen_tests.rs (1)
src/ninja_gen.rs (1)
  • generate (16-22)
tests/ninja_snapshot_tests.rs (3)
src/manifest.rs (1)
  • from_str (33-35)
src/ir.rs (1)
  • from_manifest (108-119)
src/ninja_gen.rs (1)
  • generate (16-22)
tests/steps/ninja_steps.rs (1)
src/ninja_gen.rs (1)
  • generate (16-22)
🔇 Additional comments (16)
Cargo.toml (1)

60-61: LGTM! Dependencies follow guidelines correctly.

Both dependencies use proper SemVer-compatible caret requirements and serve legitimate testing purposes for the new Ninja functionality.

src/ninja_gen.rs (2)

1-7: LGTM! Module documentation follows guidelines.

The module-level documentation clearly explains the purpose and notes the deterministic output behaviour.


129-163: LGTM! Comprehensive unit test.

The test covers the main generation path with a realistic example and verifies the expected output format.

tests/cucumber.rs (1)

10-10: LGTM! Clean addition to test infrastructure.

The optional ninja field follows the existing pattern and appropriately supports the new Ninja generation testing.

.github/workflows/ci.yml (1)

21-22: LGTM! Sensible CI enhancement.

Adding the Ninja version check ensures tool availability and aids debugging for the new Ninja-related functionality.

tests/steps/mod.rs (1)

4-4: LGTM! Proper module integration.

The ninja_steps module declaration follows the established pattern and correctly integrates the new Ninja testing capabilities.

src/lib.rs (1)

11-11: LGTM! Clean module export addition.

The new ninja_gen module export follows the established pattern and maintains alphabetical ordering with other public modules.

docs/roadmap.md (1)

58-62: LGTM! Roadmap updates accurately reflect completed work.

The checkboxes and "(done)" annotations properly mark the Ninja synthesizer implementation and rule generation logic as completed, maintaining consistency with other roadmap entries.

docs/netsuke-design.md (1)

1074-1078: LGTM! Valuable design documentation enhancements.

These additions clearly describe the deterministic behaviour and comprehensive testing approach for Ninja file generation, providing important implementation details in the appropriate section.

tests/ninja_gen_tests.rs (2)

1-8: LGTM! Clean test file structure.

The module documentation, imports, and use of rstest follow established patterns and testing best practices.


9-41: LGTM! Comprehensive unit test for Ninja generation.

The test properly constructs IR structures and validates the complete Ninja output format. The use of concat! for the expected string maintains readability whilst following the coding guidelines.

tests/features/ninja.feature (1)

4-10: Confirm test data files exist and contain correct manifests.

  • tests/data/rules.yml defines a compile rule and the hello.o target, satisfying the rule compile and build hello.o: assertions.
  • tests/data/phony.yml defines a phony clean target.

Proceed with tests as-is.

tests/steps/ninja_steps.rs (1)

1-5: LGTM!

The module documentation clearly explains the purpose and the imports are appropriate for the Cucumber step definitions.

tests/ninja_snapshot_tests.rs (3)

1-11: LGTM!

The module documentation is comprehensive and clearly explains the testing approach. The imports are well-organised and appropriate.


13-21: LGTM!

The helper function properly handles command execution with appropriate error messages and follows the coding guideline to use .expect() over .unwrap().


23-42: LGTM!

The test manifest is well-designed with a simple, deterministic touch rule. The error handling follows guidelines with descriptive .expect() calls.

Comment thread src/ninja_gen.rs Outdated
Comment thread tests/ninja_gen_tests.rs
Comment thread tests/ninja_snapshot_tests.rs
Comment thread tests/steps/ninja_steps.rs
Comment thread tests/steps/ninja_steps.rs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/ninja_gen.rs (1)

66-72: Use itertools for cleaner path joining.

Extract path joining logic to avoid duplication and use a more idiomatic approach.

Use itertools::Itertools::join for cleaner code:

 fn join(paths: &[PathBuf]) -> String {
-    paths
-        .iter()
-        .map(|p| p.display().to_string())
-        .collect::<Vec<_>>()
-        .join(" ")
+    use itertools::Itertools;
+    paths.iter().map(|p| p.display()).join(" ")
 }
tests/ninja_snapshot_tests.rs (1)

23-76: Comprehensive end-to-end validation with ninja executable dependency.

The test provides excellent coverage of the entire pipeline from manifest to execution. The Python-based touch command ensures cross-platform compatibility, and the ninja validation is thorough.

Note: The test assumes ninja executable is available in PATH. This dependency is documented in the CI workflow update that runs ninja --version.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77ca463 and 6650139.

⛔ Files ignored due to path filters (1)
  • tests/snapshots/ninja/ninja_snapshot_tests__touch_manifest_ninja.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • docs/netsuke-design.md (1 hunks)
  • src/ninja_gen.rs (1 hunks)
  • tests/ninja_gen_tests.rs (1 hunks)
  • tests/ninja_snapshot_tests.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.rs

📄 CodeRabbit Inference Engine (AGENTS.md)

**/*.rs: Clippy warnings MUST be disallowed.
Fix any warnings emitted during tests in the code itself rather than silencing them.
Where a function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a function has too many parameters, group related parameters in meaningfully named structs.
Where a function is returning a large error consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new functionality. Run both before and after making any change.
Every module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings.
Handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code unless absolutely necessary and document any usage clearly.
Place function attributes after doc comments.
Do not use return in single-line functions.
Use predicate functions for conditional criteria with more than two branches.
Lints must not be silenced except as a last resort.
Lint rule suppressions must be tightly scoped and include a clear reason.
Prefer expect over allow.
Prefer .expect() over .unwrap().
Use concat!() to combine long string literals rather than escaping newlines with a backslash.
Prefer semantic error enums: Derive std::error::Error (via the thiserror crate) for any condition the caller might inspect, retry, or map to an HTTP status.
Use an opaque error only at the app boundary: Use eyre::Report for human-readable logs; these should not be exposed in public APIs.
Never export the opaque type from a library: Convert to domain enums at API boundaries, and to eyre only in the main main() entrypoint or top-level async task.

Files:

  • tests/ninja_gen_tests.rs
  • tests/ninja_snapshot_tests.rs
  • src/ninja_gen.rs

⚙️ CodeRabbit Configuration File

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.

Files:

  • tests/ninja_gen_tests.rs
  • tests/ninja_snapshot_tests.rs
  • src/ninja_gen.rs
docs/**/*.md

📄 CodeRabbit Inference Engine (AGENTS.md)

docs/**/*.md: Use the markdown files within the docs/ directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions.
Proactively update the relevant file(s) in the docs/ directory to reflect the latest state when new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve.

Files:

  • docs/netsuke-design.md
**/*.md

📄 CodeRabbit Inference Engine (AGENTS.md)

**/*.md: Documentation must use en-GB-oxendict spelling and grammar, except for the naming of the "LICENSE" file.
Validate Markdown files using make markdownlint.
Run make fmt after any documentation changes to format all Markdown files and fix table markup.
Validate Mermaid diagrams in Markdown files by running make nixie.
Markdown paragraphs and bullet points must be wrapped at 80 columns.
Code blocks in Markdown must be wrapped at 120 columns.
Tables and headings in Markdown must not be wrapped.
Use dashes (-) for list bullets in Markdown.
Use GitHub-flavoured Markdown footnotes ([^1]) for references and footnotes.

Files:

  • docs/netsuke-design.md

⚙️ CodeRabbit Configuration File

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Paragraphs and bullets must be wrapped to 80 columns, except where a long URL would prevent this (in which case, silence MD013 for that line)
  • Code blocks should be wrapped to 120 columns.
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/netsuke-design.md
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: leynos/netsuke#0
File: docs/roadmap.md:0-0
Timestamp: 2025-07-27T17:59:36.506Z
Learning: Applies to docs/src/ninja_gen.rs : Implement the Ninja file synthesizer in src/ninja_gen.rs to traverse the BuildGraph IR.
📚 Learning: applies to docs/src/ninja_gen.rs : implement the ninja file synthesizer in src/ninja_gen.rs to trave...
Learnt from: CR
PR: leynos/netsuke#0
File: docs/roadmap.md:0-0
Timestamp: 2025-07-27T17:59:36.506Z
Learning: Applies to docs/src/ninja_gen.rs : Implement the Ninja file synthesizer in src/ninja_gen.rs to traverse the BuildGraph IR.

Applied to files:

  • src/ninja_gen.rs
📚 Learning: applies to docs/src/ninja_gen.rs : write logic to generate ninja rule statements from ir::action str...
Learnt from: CR
PR: leynos/netsuke#0
File: docs/roadmap.md:0-0
Timestamp: 2025-07-27T17:59:36.506Z
Learning: Applies to docs/src/ninja_gen.rs : Write logic to generate Ninja rule statements from ir::Action structs and build statements from ir::BuildEdge structs.

Applied to files:

  • src/ninja_gen.rs
🧬 Code Graph Analysis (2)
tests/ninja_gen_tests.rs (1)
src/ninja_gen.rs (1)
  • generate (19-63)
tests/ninja_snapshot_tests.rs (3)
src/manifest.rs (1)
  • from_str (33-35)
src/ir.rs (1)
  • from_manifest (108-119)
src/ninja_gen.rs (1)
  • generate (19-63)
🔇 Additional comments (11)
docs/netsuke-design.md (1)

1074-1083: Excellent documentation of deterministic Ninja generation.

The documentation accurately describes the sorting and deduplication strategies, script formatting, and conditional field inclusion. This provides clear understanding of how the generator ensures deterministic, portable output.

src/ninja_gen.rs (5)

74-79: LGTM! Good key generation strategy.

The use of null separator and sorted paths ensures collision-free, deterministic keys for deduplication.


87-118: Excellent implementation of Ninja rule formatting.

The Display implementation correctly handles both command and script recipes, with proper shell wrapping for scripts and conditional inclusion of optional fields. The formatting matches Ninja syntax requirements.


126-150: Proper Ninja build statement formatting.

The implementation correctly formats all Ninja build statement components. The conditional restat logic (lines 145-147) appropriately avoids conflicts between action-level and edge-level restat settings.


158-193: Basic test coverage is adequate given comprehensive testing elsewhere.

The simple test validates core functionality. More comprehensive scenarios are covered in tests/ninja_gen_tests.rs and tests/ninja_snapshot_tests.rs.


28-46: Confirm sorting stability and deduplication logic
Ensure fn path_key (src/ninja_gen.rs:75–78) sorts and joins the explicit outputs, producing a stable key. Sorting edges by this key at line 29 yields a deterministic order. HashSet deduplication at lines 32–35 removes duplicates correctly. No action required.

tests/ninja_gen_tests.rs (3)

9-41: Good test coverage for phony build rules.

The test correctly validates that phony edges generate the appropriate Ninja build statement with the "phony" rule.


43-85: Excellent comprehensive test coverage.

This test validates script recipe formatting, all optional action fields, complex build edge configurations with implicit outputs and order-only dependencies, and default target generation. The coverage is thorough.


87-122: Excellent parameterised test for restat logic.

The rstest parameterisation effectively validates the conditional restat behaviour across different combinations of action and edge flags. The assertion logic correctly verifies when restat should be included.

tests/ninja_snapshot_tests.rs (2)

13-21: Useful helper function for command execution.

The helper provides consistent command execution with proper error handling and descriptive failure messages.


78-145: Excellent validation of deterministic output and deduplication.

The test effectively validates that ninja generation is deterministic by comparing multiple generations, checks deduplication by counting build statements, and verifies proper ordering of rules and default targets.

Comment thread src/ninja_gen.rs
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jul 31, 2025

@coderabbitai resolve

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jul 31, 2025

@sourcery-ai resolve

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jul 31, 2025

✅ Actions performed

Comments resolved and changes approved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jul 31, 2025

Superseded by #38

@leynos leynos closed this Jul 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant