Skip to content

Commit cf261eb

Browse files
committed
Import compile commands into build graph
2 parents 6aa05b5 + 50549fc commit cf261eb

6 files changed

Lines changed: 680 additions & 32 deletions

File tree

include/vix/cli/build/BuildGraph.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
#include <vix/cli/build/BuildNode.hpp>
2828
#include <vix/cli/build/BuildTask.hpp>
29+
#include <vix/cli/build/CompileCommands.hpp>
2930
#include <vix/cli/build/DependencyFile.hpp>
3031

3132
namespace vix::cli::build
@@ -214,6 +215,18 @@ namespace vix::cli::build
214215
*/
215216
BuildGraphScanResult scan_project();
216217

218+
/**
219+
* @brief Load compile tasks from compile_commands.json.
220+
*
221+
* This imports the real compiler commands generated by CMake/Ninja and
222+
* replaces guessed scan_project() compile commands with exact argv, working
223+
* directory and object output paths.
224+
*
225+
* @param path Path to compile_commands.json
226+
* @return Number of imported compile commands
227+
*/
228+
std::size_t load_compile_commands(const fs::path &path);
229+
217230
/**
218231
* @brief Load dependency files from the object directory and connect nodes.
219232
*
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
*
3+
* @file CompileCommands.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+
* compile_commands.json parser
14+
*
15+
*/
16+
17+
#ifndef VIX_CLI_BUILD_COMPILE_COMMANDS_HPP
18+
#define VIX_CLI_BUILD_COMPILE_COMMANDS_HPP
19+
20+
#include <filesystem>
21+
#include <optional>
22+
#include <string>
23+
#include <vector>
24+
25+
namespace vix::cli::build
26+
{
27+
namespace fs = std::filesystem;
28+
29+
/**
30+
* @brief One entry from compile_commands.json.
31+
*
32+
* CMake/Ninja can generate compile_commands.json with the exact compiler
33+
* command used for every translation unit. Vix imports this data to build
34+
* graph tasks without guessing include paths, defines, flags, object paths,
35+
* or compiler arguments.
36+
*/
37+
struct CompileCommandEntry
38+
{
39+
fs::path directory; ///< Working directory used by the compiler
40+
fs::path source; ///< Source file being compiled
41+
fs::path output; ///< Object file when available
42+
std::vector<std::string> arguments; ///< Compiler argv
43+
std::string rawCommand; ///< Raw command string when provided
44+
45+
/**
46+
* @brief Check whether this entry has enough data to create a compile task.
47+
*
48+
* @return true when directory, source and arguments are present
49+
*/
50+
bool valid() const;
51+
52+
/**
53+
* @brief Check whether the entry has an object output path.
54+
*
55+
* @return true when output is not empty
56+
*/
57+
bool has_output() const;
58+
};
59+
60+
/**
61+
* @brief Return the default compile_commands.json path for a build directory.
62+
*
63+
* @param buildDir Build directory
64+
* @return buildDir / compile_commands.json
65+
*/
66+
fs::path default_compile_commands_path(const fs::path &buildDir);
67+
68+
/**
69+
* @brief Split a compiler command string into argv tokens.
70+
*
71+
* This supports the shell forms usually emitted by CMake:
72+
* - whitespace separated tokens
73+
* - single quotes
74+
* - double quotes
75+
* - backslash escaping
76+
*
77+
* It does not expand environment variables, globs, or shell substitutions.
78+
*
79+
* @param command Raw command string
80+
* @return Parsed argv tokens
81+
*/
82+
std::vector<std::string> split_compile_command(const std::string &command);
83+
84+
/**
85+
* @brief Parse compile_commands.json content.
86+
*
87+
* The parser accepts both formats supported by the compilation database:
88+
* - "command": "c++ ..."
89+
* - "arguments": ["c++", "..."]
90+
*
91+
* @param text JSON content
92+
* @param sourcePath Path of the compile_commands.json file
93+
* @return Parsed entries, or std::nullopt on invalid JSON/schema
94+
*/
95+
std::optional<std::vector<CompileCommandEntry>> parse_compile_commands_text(
96+
const std::string &text,
97+
const fs::path &sourcePath = {});
98+
99+
/**
100+
* @brief Read and parse compile_commands.json from disk.
101+
*
102+
* @param path Path to compile_commands.json
103+
* @return Parsed entries, or std::nullopt when missing/invalid
104+
*/
105+
std::optional<std::vector<CompileCommandEntry>> read_compile_commands(
106+
const fs::path &path);
107+
108+
/**
109+
* @brief Resolve a possibly relative path against a base directory.
110+
*
111+
* @param base Base directory
112+
* @param path Path to resolve
113+
* @return Absolute normalized path when possible
114+
*/
115+
fs::path resolve_compile_command_path(
116+
const fs::path &base,
117+
const fs::path &path);
118+
119+
/**
120+
* @brief Extract the object output path from compiler argv.
121+
*
122+
* Supports:
123+
* - -o file.o
124+
* - -ofile.o
125+
*
126+
* @param arguments Compiler argv
127+
* @param workingDirectory Working directory used to resolve relative paths
128+
* @return Object path, or empty path if not found
129+
*/
130+
fs::path extract_compile_output_path(
131+
const std::vector<std::string> &arguments,
132+
const fs::path &workingDirectory);
133+
134+
} // namespace vix::cli::build
135+
136+
#endif

src/build/BuildGraph.cpp

Lines changed: 143 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
#include <vix/cli/build/BuildGraph.hpp>
18+
#include <vix/cli/build/CompileCommands.hpp>
1819

1920
#include <algorithm>
2021
#include <cstdint>
@@ -390,6 +391,91 @@ namespace vix::cli::build
390391
return 0;
391392
}
392393
}
394+
395+
static std::string compile_task_id_for_source(const fs::path &source)
396+
{
397+
std::uint64_t h = FNV_OFFSET;
398+
399+
const std::string normalized = normalize_path_string(source);
400+
h = fnv_mix_string(h, "compile:");
401+
h = fnv_mix_string(h, normalized);
402+
403+
return "compile:" + hex64(h);
404+
}
405+
406+
static std::vector<std::string> make_dependency_enabled_command(
407+
const std::vector<std::string> &arguments,
408+
const fs::path &dependencyFile)
409+
{
410+
std::vector<std::string> out;
411+
out.reserve(arguments.size() + 6);
412+
413+
bool hasMMD = false;
414+
bool hasMP = false;
415+
bool hasMF = false;
416+
bool hasDependencyMode = false;
417+
418+
for (std::size_t i = 0; i < arguments.size(); ++i)
419+
{
420+
const std::string &arg = arguments[i];
421+
422+
if (arg == "-MMD")
423+
hasMMD = true;
424+
425+
if (arg == "-MP")
426+
hasMP = true;
427+
428+
if (arg == "-MF")
429+
{
430+
hasMF = true;
431+
out.push_back(arg);
432+
433+
if (i + 1 < arguments.size())
434+
{
435+
out.push_back(dependencyFile.string());
436+
++i;
437+
}
438+
else
439+
{
440+
out.push_back(dependencyFile.string());
441+
}
442+
443+
continue;
444+
}
445+
446+
if (arg.rfind("-MF", 0) == 0 && arg.size() > 3)
447+
{
448+
hasMF = true;
449+
out.push_back("-MF");
450+
out.push_back(dependencyFile.string());
451+
continue;
452+
}
453+
454+
if (arg == "-MD" ||
455+
arg == "-MMD" ||
456+
arg == "-M" ||
457+
arg == "-MM")
458+
{
459+
hasDependencyMode = true;
460+
}
461+
462+
out.push_back(arg);
463+
}
464+
465+
if (!hasDependencyMode && !hasMMD)
466+
out.push_back("-MMD");
467+
468+
if (!hasMP)
469+
out.push_back("-MP");
470+
471+
if (!hasMF)
472+
{
473+
out.push_back("-MF");
474+
out.push_back(dependencyFile.string());
475+
}
476+
477+
return out;
478+
}
393479
} // namespace
394480

395481
bool BuildGraphConfig::valid() const
@@ -573,42 +659,11 @@ namespace vix::cli::build
573659
add_node(node);
574660

575661
if (kind == BuildNodeKind::Source)
576-
{
577662
++result.sources;
578-
579-
const fs::path objectPath = object_path_for_source(current);
580-
const fs::path dependencyPath = dependency_path_for_source(current);
581-
582-
BuildNode objectNode =
583-
make_file_build_node(BuildNodeKind::Object, objectPath);
584-
585-
objectNode.id = make_build_node_id(BuildNodeKind::Object, objectPath);
586-
objectNode.hash = hash_file_content(objectPath);
587-
objectNode.add_dependency(node.id);
588-
589-
add_node(objectNode);
590-
591-
const std::vector<std::string> command =
592-
compile_command_for(current, objectPath, dependencyPath);
593-
594-
BuildTask task =
595-
make_compile_task(
596-
node.id,
597-
objectNode.id,
598-
command,
599-
config_.projectDir);
600-
601-
task.logFile = dependencyPath;
602-
add_task(task);
603-
}
604663
else if (kind == BuildNodeKind::Header)
605-
{
606664
++result.headers;
607-
}
608665
else if (kind == BuildNodeKind::Config)
609-
{
610666
++result.configs;
611-
}
612667

613668
++it;
614669
}
@@ -617,6 +672,63 @@ namespace vix::cli::build
617672
return result;
618673
}
619674

675+
std::size_t BuildGraph::load_compile_commands(const fs::path &path)
676+
{
677+
const auto compileCommands = read_compile_commands(path);
678+
679+
if (!compileCommands)
680+
return 0;
681+
682+
std::size_t imported = 0;
683+
684+
for (const CompileCommandEntry &entry : *compileCommands)
685+
{
686+
if (!entry.valid() || !entry.has_output())
687+
continue;
688+
689+
const fs::path sourcePath = entry.source.lexically_normal();
690+
const fs::path objectPath = entry.output.lexically_normal();
691+
const fs::path dependencyPath = dependency_file_for_object(objectPath);
692+
693+
BuildNode sourceNode =
694+
make_file_build_node(BuildNodeKind::Source, sourcePath);
695+
696+
sourceNode.hash = hash_file_content(sourcePath);
697+
add_node(sourceNode);
698+
699+
BuildNode objectNode =
700+
make_file_build_node(BuildNodeKind::Object, objectPath);
701+
702+
objectNode.id = make_build_node_id(BuildNodeKind::Object, objectPath);
703+
objectNode.hash = hash_file_content(objectPath);
704+
objectNode.add_dependency(sourceNode.id);
705+
706+
add_node(objectNode);
707+
708+
std::vector<std::string> command =
709+
make_dependency_enabled_command(
710+
entry.arguments,
711+
dependencyPath);
712+
713+
BuildTask task =
714+
make_compile_task(
715+
sourceNode.id,
716+
objectNode.id,
717+
command,
718+
entry.directory);
719+
720+
task.id = compile_task_id_for_source(sourcePath);
721+
task.workingDirectory = entry.directory;
722+
task.logFile = dependencyPath;
723+
724+
add_task(task);
725+
726+
++imported;
727+
}
728+
729+
return imported;
730+
}
731+
620732
void BuildGraph::load_dependency_files()
621733
{
622734
for (auto &kv : tasks_)

0 commit comments

Comments
 (0)