@@ -126,6 +126,18 @@ bool is_c_source(const std::filesystem::path& src) {
126126 return src.extension () == " .c" ;
127127}
128128
129+ // Escape a string for JSON (handles " and \).
130+ std::string json_escape (std::string_view s) {
131+ std::string out;
132+ out.reserve (s.size ());
133+ for (char c : s) {
134+ if (c == ' "' ) out += " \\\" " ;
135+ else if (c == ' \\ ' ) out += " \\\\ " ;
136+ else out.push_back (c);
137+ }
138+ return out;
139+ }
140+
129141// Compute the full C++ flags string (everything between compiler binary and -c).
130142// Returns raw strings; escape_ninja_path() is applied by the caller where needed.
131143std::string compute_cxxflags (const BuildPlan& plan) {
@@ -269,6 +281,56 @@ std::string compute_cflags(const BuildPlan& plan) {
269281 b_flag, include_flags, user_cflags);
270282}
271283
284+ // Emit compile_commands.json content as a string.
285+ std::string emit_compile_commands_json (const BuildPlan& plan) {
286+ std::string out;
287+ out.reserve (4096 );
288+ out += " [\n " ;
289+
290+ std::string cxxflags = compute_cxxflags (plan);
291+ std::string cflags = compute_cflags (plan);
292+
293+ for (size_t i = 0 ; i < plan.compileUnits .size (); ++i) {
294+ auto & cu = plan.compileUnits [i];
295+
296+ // Determine compiler and flags based on source type
297+ std::string compiler;
298+ std::string flags;
299+ if (is_c_source (cu.source )) {
300+ compiler = derive_c_compiler (plan.toolchain .binaryPath ).string ();
301+ flags = cflags;
302+ } else {
303+ compiler = plan.toolchain .binaryPath .string ();
304+ flags = cxxflags;
305+ }
306+
307+ // Build the command: compiler + " " + flags + " -c " + source + " -o " + output
308+ auto output_path = (plan.outputDir / cu.object ).string ();
309+ std::string command = compiler + " " + flags + " -c " + cu.source .string () + " -o " + output_path;
310+
311+ // Escape all strings for JSON
312+ std::string dir_escaped = json_escape (plan.projectRoot .string ());
313+ std::string cmd_escaped = json_escape (command);
314+ std::string file_escaped = json_escape (cu.source .string ());
315+ std::string output_escaped = json_escape (output_path);
316+
317+ // Emit entry with 2-space indentation, field order: directory, command, file, output
318+ out += " {\n " ;
319+ out += std::format (" \" directory\" : \" {}\" ,\n " , dir_escaped);
320+ out += std::format (" \" command\" : \" {}\" ,\n " , cmd_escaped);
321+ out += std::format (" \" file\" : \" {}\" ,\n " , file_escaped);
322+ out += std::format (" \" output\" : \" {}\"\n " , output_escaped);
323+ out += " }" ;
324+ if (i + 1 < plan.compileUnits .size ()) {
325+ out += " ," ;
326+ }
327+ out += " \n " ;
328+ }
329+
330+ out += " ]" ;
331+ return out;
332+ }
333+
272334} // namespace
273335
274336std::string emit_ninja_string (const BuildPlan& plan) {
@@ -608,6 +670,9 @@ NinjaBackend::build(const BuildPlan& plan, const BuildOptions& opts)
608670 auto ninja_path = plan.outputDir / " build.ninja" ;
609671 write_file (ninja_path, emit_ninja_string (plan));
610672
673+ auto cc_path = plan.projectRoot / " target" / " compile_commands.json" ;
674+ write_file (cc_path, emit_compile_commands_json (plan));
675+
611676 if (opts.dryRun ) {
612677 BuildResult r;
613678 r.exitCode = 0 ;
0 commit comments