Skip to content

Commit a645787

Browse files
committed
Import Ninja build edges into graph
2 parents cf261eb + b498a7b commit a645787

5 files changed

Lines changed: 988 additions & 1 deletion

File tree

include/vix/cli/build/BuildGraph.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <vix/cli/build/BuildTask.hpp>
2929
#include <vix/cli/build/CompileCommands.hpp>
3030
#include <vix/cli/build/DependencyFile.hpp>
31+
#include <vix/cli/build/BuildNinja.hpp>
3132

3233
namespace vix::cli::build
3334
{
@@ -227,6 +228,19 @@ namespace vix::cli::build
227228
*/
228229
std::size_t load_compile_commands(const fs::path &path);
229230

231+
/**
232+
* @brief Load build tasks from build.ninja.
233+
*
234+
* This imports the real Ninja build edges generated by CMake/Ninja.
235+
* Compile tasks are intentionally ignored here because Vix imports them from
236+
* compile_commands.json. This method imports archive, link, copy, install and
237+
* utility tasks.
238+
*
239+
* @param path Path to build.ninja
240+
* @return Number of imported Ninja tasks
241+
*/
242+
std::size_t load_ninja_build(const fs::path &path);
243+
230244
/**
231245
* @brief Load dependency files from the object directory and connect nodes.
232246
*
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
*
3+
* @file BuildNinja.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2026, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*
13+
* Ninja build graph parser
14+
*
15+
*/
16+
17+
#ifndef VIX_CLI_BUILD_BUILD_NINJA_HPP
18+
#define VIX_CLI_BUILD_BUILD_NINJA_HPP
19+
20+
#include <filesystem>
21+
#include <optional>
22+
#include <string>
23+
#include <unordered_map>
24+
#include <vector>
25+
26+
namespace vix::cli::build
27+
{
28+
namespace fs = std::filesystem;
29+
30+
enum class NinjaEdgeKind
31+
{
32+
Unknown,
33+
Compile,
34+
Archive,
35+
Link,
36+
Copy,
37+
Install,
38+
Utility
39+
};
40+
41+
struct NinjaRule
42+
{
43+
std::string name;
44+
std::unordered_map<std::string, std::string> variables;
45+
46+
bool valid() const;
47+
};
48+
49+
struct NinjaEdge
50+
{
51+
std::vector<fs::path> outputs;
52+
std::vector<fs::path> explicitInputs;
53+
std::vector<fs::path> implicitInputs;
54+
std::vector<fs::path> orderOnlyInputs;
55+
56+
std::string rule;
57+
NinjaEdgeKind kind{NinjaEdgeKind::Unknown};
58+
59+
std::unordered_map<std::string, std::string> variables;
60+
61+
bool valid() const;
62+
fs::path primary_output() const;
63+
};
64+
65+
struct NinjaBuildFile
66+
{
67+
fs::path path;
68+
fs::path directory;
69+
70+
std::unordered_map<std::string, std::string> variables;
71+
std::unordered_map<std::string, NinjaRule> rules;
72+
std::vector<NinjaEdge> edges;
73+
74+
bool valid() const;
75+
};
76+
77+
std::string to_string(NinjaEdgeKind kind);
78+
79+
NinjaEdgeKind classify_ninja_edge(
80+
const NinjaEdge &edge,
81+
const NinjaRule *rule);
82+
83+
fs::path default_build_ninja_path(const fs::path &buildDir);
84+
85+
std::optional<NinjaBuildFile> parse_build_ninja_text(
86+
const std::string &text,
87+
const fs::path &path = {});
88+
89+
std::optional<NinjaBuildFile> read_build_ninja(
90+
const fs::path &path);
91+
92+
fs::path resolve_ninja_path(
93+
const fs::path &base,
94+
const fs::path &path);
95+
96+
} // namespace vix::cli::build
97+
98+
#endif

src/build/BuildGraph.cpp

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include <vix/cli/build/BuildGraph.hpp>
1818
#include <vix/cli/build/CompileCommands.hpp>
19+
#include <vix/cli/build/BuildNinja.hpp>
1920

2021
#include <algorithm>
2122
#include <cstdint>
@@ -476,6 +477,129 @@ namespace vix::cli::build
476477

477478
return out;
478479
}
480+
481+
static BuildNodeKind node_kind_from_ninja_edge_output(
482+
const NinjaEdge &edge,
483+
const fs::path &path)
484+
{
485+
const std::string ext = path.extension().string();
486+
487+
if (edge.kind == NinjaEdgeKind::Archive)
488+
return BuildNodeKind::Library;
489+
490+
if (edge.kind == NinjaEdgeKind::Link)
491+
{
492+
if (ext == ".a" ||
493+
ext == ".so" ||
494+
ext == ".dylib" ||
495+
ext == ".dll" ||
496+
ext == ".lib")
497+
{
498+
return BuildNodeKind::Library;
499+
}
500+
501+
return BuildNodeKind::Executable;
502+
}
503+
504+
if (edge.kind == NinjaEdgeKind::Copy ||
505+
edge.kind == NinjaEdgeKind::Install)
506+
{
507+
return BuildNodeKind::Config;
508+
}
509+
510+
return BuildNodeKind::Unknown;
511+
}
512+
513+
static BuildNodeKind node_kind_from_ninja_input(const fs::path &path)
514+
{
515+
const std::string ext = path.extension().string();
516+
517+
if (ext == ".o" || ext == ".obj")
518+
return BuildNodeKind::Object;
519+
520+
if (ext == ".a" ||
521+
ext == ".so" ||
522+
ext == ".dylib" ||
523+
ext == ".dll" ||
524+
ext == ".lib")
525+
{
526+
return BuildNodeKind::Library;
527+
}
528+
529+
if (is_source_extension(ext))
530+
return BuildNodeKind::Source;
531+
532+
if (is_header_extension(ext))
533+
return BuildNodeKind::Header;
534+
535+
return BuildNodeKind::Config;
536+
}
537+
538+
static BuildTaskKind build_task_kind_from_ninja_edge_kind(NinjaEdgeKind kind)
539+
{
540+
switch (kind)
541+
{
542+
case NinjaEdgeKind::Archive:
543+
return BuildTaskKind::Archive;
544+
case NinjaEdgeKind::Link:
545+
return BuildTaskKind::Link;
546+
case NinjaEdgeKind::Copy:
547+
return BuildTaskKind::Copy;
548+
case NinjaEdgeKind::Install:
549+
return BuildTaskKind::Copy;
550+
case NinjaEdgeKind::Utility:
551+
return BuildTaskKind::Generate;
552+
case NinjaEdgeKind::Compile:
553+
return BuildTaskKind::Compile;
554+
case NinjaEdgeKind::Unknown:
555+
default:
556+
return BuildTaskKind::Unknown;
557+
}
558+
}
559+
560+
static std::string ninja_task_id_for_edge(const NinjaEdge &edge)
561+
{
562+
std::uint64_t h = FNV_OFFSET;
563+
564+
h = fnv_mix_string(h, "ninja:");
565+
h = fnv_mix_string(h, to_string(edge.kind));
566+
h = fnv_mix_string(h, edge.rule);
567+
568+
for (const fs::path &output : edge.outputs)
569+
h = fnv_mix_string(h, normalize_path_string(output));
570+
571+
return "ninja:" + hex64(h);
572+
}
573+
574+
static std::string command_hash_for_argv(
575+
const std::vector<std::string> &command)
576+
{
577+
std::uint64_t h = FNV_OFFSET;
578+
579+
h = fnv_mix_string(h, "command:");
580+
581+
for (const std::string &arg : command)
582+
{
583+
h = fnv_mix_string(h, arg);
584+
h = fnv_mix_string(h, "\0");
585+
}
586+
587+
return hex64(h);
588+
}
589+
590+
static bool should_import_ninja_edge(const NinjaEdge &edge)
591+
{
592+
if (!edge.valid())
593+
return false;
594+
595+
if (edge.kind == NinjaEdgeKind::Compile)
596+
return false;
597+
598+
if (edge.kind == NinjaEdgeKind::Unknown)
599+
return false;
600+
601+
return true;
602+
}
479603
} // namespace
480604

481605
bool BuildGraphConfig::valid() const
@@ -729,6 +853,114 @@ namespace vix::cli::build
729853
return imported;
730854
}
731855

856+
std::size_t BuildGraph::load_ninja_build(const fs::path &path)
857+
{
858+
const auto ninjaBuild = read_build_ninja(path);
859+
860+
if (!ninjaBuild)
861+
return 0;
862+
863+
std::size_t imported = 0;
864+
865+
for (const NinjaEdge &edge : ninjaBuild->edges)
866+
{
867+
if (!should_import_ninja_edge(edge))
868+
continue;
869+
870+
const BuildTaskKind taskKind =
871+
build_task_kind_from_ninja_edge_kind(edge.kind);
872+
873+
if (taskKind == BuildTaskKind::Unknown)
874+
continue;
875+
876+
BuildTask task;
877+
task.id = ninja_task_id_for_edge(edge);
878+
task.kind = taskKind;
879+
task.state = BuildTaskState::Pending;
880+
task.workingDirectory = ninjaBuild->directory;
881+
882+
/*
883+
* Do not expand Ninja rule commands here.
884+
*
885+
* build.ninja is already a complete execution graph. For now we import
886+
* the DAG structure only. Execution remains delegated to Ninja/CMake until
887+
* Vix has a full Ninja variable expander and target-aware executor.
888+
*/
889+
task.command = {
890+
"ninja",
891+
"-C",
892+
ninjaBuild->directory.string(),
893+
edge.primary_output().string()};
894+
895+
task.commandHash = command_hash_for_argv(task.command);
896+
897+
for (const fs::path &input : edge.explicitInputs)
898+
{
899+
const BuildNodeKind inputKind = node_kind_from_ninja_input(input);
900+
901+
BuildNode inputNode = make_file_build_node(inputKind, input);
902+
903+
/*
904+
* Keep Ninja import cheap.
905+
* scan_project() and load_dependency_files() already hash real project
906+
* inputs where needed. Ninja edges may reference many generated files.
907+
*/
908+
inputNode.hash.clear();
909+
910+
add_node(inputNode);
911+
task.add_input(inputNode.id);
912+
}
913+
914+
for (const fs::path &input : edge.implicitInputs)
915+
{
916+
const BuildNodeKind inputKind = node_kind_from_ninja_input(input);
917+
918+
BuildNode inputNode = make_file_build_node(inputKind, input);
919+
inputNode.hash.clear();
920+
921+
add_node(inputNode);
922+
task.add_input(inputNode.id);
923+
}
924+
925+
for (const fs::path &input : edge.orderOnlyInputs)
926+
{
927+
const BuildNodeKind inputKind = node_kind_from_ninja_input(input);
928+
929+
BuildNode inputNode = make_file_build_node(inputKind, input);
930+
inputNode.hash.clear();
931+
932+
add_node(inputNode);
933+
task.add_input(inputNode.id);
934+
}
935+
936+
for (const fs::path &output : edge.outputs)
937+
{
938+
const BuildNodeKind outputKind =
939+
node_kind_from_ninja_edge_output(edge, output);
940+
941+
if (outputKind == BuildNodeKind::Unknown)
942+
continue;
943+
944+
BuildNode outputNode = make_file_build_node(outputKind, output);
945+
outputNode.hash.clear();
946+
947+
for (const auto &inputId : task.inputs)
948+
outputNode.add_dependency(inputId);
949+
950+
add_node(outputNode);
951+
task.add_output(outputNode.id);
952+
}
953+
954+
if (task.outputs.empty())
955+
continue;
956+
957+
add_task(task);
958+
++imported;
959+
}
960+
961+
return imported;
962+
}
963+
732964
void BuildGraph::load_dependency_files()
733965
{
734966
for (auto &kv : tasks_)

0 commit comments

Comments
 (0)