├── .github
└── workflows
│ └── dotnet.yml
├── .gitignore
├── CompileCommandsJson.cs
├── CompileCommandsJson.csproj
├── LICENSE
└── README.md
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: .NET
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Setup .NET
17 | uses: actions/setup-dotnet@v1
18 | with:
19 | dotnet-version: 5.0.x
20 | - name: Restore dependencies
21 | run: dotnet restore
22 | - name: Build
23 | run: dotnet build --no-restore
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/
2 | /obj/
3 |
--------------------------------------------------------------------------------
/CompileCommandsJson.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Runtime.InteropServices;
5 | using System.Security;
6 | using System.Text;
7 | using System.Web;
8 | using Microsoft.Build.Framework;
9 | using Microsoft.Build.Utilities;
10 |
11 | ///
12 | /// MSBuild logger to emit a compile_commands.json file from a C++ project build.
13 | ///
14 | ///
15 | /// Based on the work of:
16 | /// * Kirill Osenkov and the MSBuildStructuredLog project.
17 | /// * Dave Glick's MsBuildPipeLogger.
18 | ///
19 | /// Ref for MSBuild Logger API:
20 | /// https://docs.microsoft.com/en-us/visualstudio/msbuild/build-loggers
21 | /// Format spec:
22 | /// https://clang.llvm.org/docs/JSONCompilationDatabase.html
23 | ///
24 | public class CompileCommandsJson : Logger
25 | {
26 | public override void Initialize(IEventSource eventSource)
27 | {
28 | // Default to writing compile_commands.json in the current directory,
29 | // but permit it to be overridden by a parameter.
30 | //
31 | string outputFilePath = String.IsNullOrEmpty(Parameters) ? "compile_commands.json" : Parameters;
32 |
33 | try
34 | {
35 | const bool append = false;
36 | Encoding utf8WithoutBom = new UTF8Encoding(false);
37 | this.streamWriter = new StreamWriter(outputFilePath, append, utf8WithoutBom);
38 | this.firstLine = true;
39 | streamWriter.WriteLine("[");
40 | }
41 | catch (Exception ex)
42 | {
43 | if (ex is UnauthorizedAccessException
44 | || ex is ArgumentNullException
45 | || ex is PathTooLongException
46 | || ex is DirectoryNotFoundException
47 | || ex is NotSupportedException
48 | || ex is ArgumentException
49 | || ex is SecurityException
50 | || ex is IOException)
51 | {
52 | throw new LoggerException("Failed to create " + outputFilePath + ": " + ex.Message);
53 | }
54 | else
55 | {
56 | // Unexpected failure
57 | throw;
58 | }
59 | }
60 |
61 | eventSource.AnyEventRaised += EventSource_AnyEventRaised;
62 | }
63 |
64 | private void EventSource_AnyEventRaised(object sender, BuildEventArgs args)
65 | {
66 | if (args is TaskCommandLineEventArgs taskArgs && taskArgs.TaskName == "CL")
67 | {
68 | // taskArgs.CommandLine begins with the full path to the compiler, but that path is
69 | // *not* escaped/quoted for a shell, and may contain spaces, such as C:\Program Files
70 | // (x86)\Microsoft Visual Studio\... As a workaround for this misfeature, find the
71 | // end of the path by searching for CL.exe. (This will fail if a user renames the
72 | // compiler binary, or installs their tools to a path that includes "CL.exe ".)
73 | const string clExe = "cl.exe ";
74 | int clExeIndex = taskArgs.CommandLine.ToLower().IndexOf(clExe);
75 | if (clExeIndex == -1)
76 | {
77 | throw new LoggerException("Unexpected lack of CL.exe in " + taskArgs.CommandLine);
78 | }
79 |
80 | string compilerPath = taskArgs.CommandLine.Substring(0, clExeIndex + clExe.Length - 1);
81 | string argsString = taskArgs.CommandLine.Substring(clExeIndex + clExe.Length).TrimStart();
82 | string[] cmdArgs = CommandLineToArgs(argsString);
83 |
84 | // Options that consume the following argument.
85 | string[] optionsWithParam = {
86 | "D", "I", "F", "U", "FI", "FU",
87 | "analyze:log", "analyze:stacksize", "analyze:max_paths",
88 | "analyze:ruleset", "analyze:plugin"};
89 |
90 | List maybeFilenames = new List();
91 | List filenames = new List();
92 | bool allFilenamesAreSources = false;
93 |
94 | for (int i = 0; i < cmdArgs.Length; i++)
95 | {
96 | bool isOption = cmdArgs[i].StartsWith("/") || cmdArgs[i].StartsWith("-");
97 | string option = isOption ? cmdArgs[i].Substring(1) : "";
98 |
99 | if (isOption && Array.Exists(optionsWithParam, e => e == option))
100 | {
101 | i++; // skip next arg
102 | }
103 | else if (option == "Tc" || option == "Tp")
104 | {
105 | // next arg is definitely a source file
106 | if (i + 1 < cmdArgs.Length)
107 | {
108 | filenames.Add(cmdArgs[i + 1]);
109 | }
110 | }
111 | else if (option.StartsWith("Tc") || option.StartsWith("Tp"))
112 | {
113 | // rest of this arg is definitely a source file
114 | filenames.Add(option.Substring(2));
115 | }
116 | else if (option == "TC" || option == "TP")
117 | {
118 | // all inputs are treated as source files
119 | allFilenamesAreSources = true;
120 | }
121 | else if (option == "link")
122 | {
123 | break; // only linker options follow
124 | }
125 | else if (isOption || cmdArgs[i].StartsWith("@"))
126 | {
127 | // other argument, ignore it
128 | }
129 | else
130 | {
131 | // non-argument, add it to our list of potential sources
132 | maybeFilenames.Add(cmdArgs[i]);
133 | }
134 | }
135 |
136 | // Iterate over potential sources, and decide (based on the filename)
137 | // whether they are source inputs.
138 | foreach (string filename in maybeFilenames)
139 | {
140 | if (allFilenamesAreSources)
141 | {
142 | filenames.Add(filename);
143 | }
144 | else
145 | {
146 | int suffixPos = filename.LastIndexOf('.');
147 | if (suffixPos != -1)
148 | {
149 | string ext = filename.Substring(suffixPos + 1).ToLowerInvariant();
150 | if (ext == "c" || ext == "cxx" || ext == "cpp")
151 | {
152 | filenames.Add(filename);
153 | }
154 | }
155 | }
156 | }
157 |
158 | // simplify the compile command to avoid .. etc.
159 | string compileCommand = '"' + Path.GetFullPath(compilerPath) + "\" " + argsString;
160 | string dirname = Path.GetDirectoryName(taskArgs.ProjectFile);
161 |
162 | // For each source file, emit a JSON entry
163 | foreach (string filename in filenames)
164 | {
165 | // Terminate the preceding entry
166 | if (firstLine)
167 | {
168 | firstLine = false;
169 | }
170 | else
171 | {
172 | streamWriter.WriteLine(",");
173 | }
174 |
175 | // Write one entry
176 | streamWriter.WriteLine(String.Format(
177 | "{{\"directory\": \"{0}\",",
178 | HttpUtility.JavaScriptStringEncode(dirname)));
179 | streamWriter.WriteLine(String.Format(
180 | " \"command\": \"{0}\",",
181 | HttpUtility.JavaScriptStringEncode(compileCommand)));
182 | streamWriter.Write(String.Format(
183 | " \"file\": \"{0}\"}}",
184 | HttpUtility.JavaScriptStringEncode(filename)));
185 | }
186 | }
187 | }
188 |
189 | [DllImport("shell32.dll", SetLastError = true)]
190 | static extern IntPtr CommandLineToArgvW(
191 | [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs);
192 |
193 | static string[] CommandLineToArgs(string commandLine)
194 | {
195 | int argc;
196 | var argv = CommandLineToArgvW(commandLine, out argc);
197 | if (argv == IntPtr.Zero)
198 | throw new System.ComponentModel.Win32Exception();
199 | try
200 | {
201 | var args = new string[argc];
202 | for (var i = 0; i < args.Length; i++)
203 | {
204 | var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
205 | args[i] = Marshal.PtrToStringUni(p);
206 | }
207 |
208 | return args;
209 | }
210 | finally
211 | {
212 | Marshal.FreeHGlobal(argv);
213 | }
214 | }
215 |
216 | public override void Shutdown()
217 | {
218 | if (!firstLine)
219 | {
220 | streamWriter.WriteLine();
221 | }
222 | streamWriter.WriteLine("]");
223 | streamWriter.Close();
224 | base.Shutdown();
225 | }
226 |
227 | private StreamWriter streamWriter;
228 | private bool firstLine;
229 | }
230 |
--------------------------------------------------------------------------------
/CompileCommandsJson.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | CompileCommandsJson
6 |
7 | A logger for MSBuild that emits a compile_commands.json file.
8 | MIT
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MSBuild `compile_commands.json` logger
2 |
3 | This is a simple
4 | [MSBuild logger](https://docs.microsoft.com/en-us/visualstudio/msbuild/build-loggers)
5 | that emits a
6 | [Clang-style `compile_commands.json` file](https://clang.llvm.org/docs/JSONCompilationDatabase.html)
7 | by observing the MSVC compiler invocations when a C++ project is built. It is particularly useful
8 | with [Visual Studio Code's C/C++ extension](https://code.visualstudio.com/docs/cpp/), which
9 | [can be configured](https://code.visualstudio.com/docs/cpp/c-cpp-properties-schema-reference#_configuration-properties)
10 | to use `compile_commands.json` to determine the compiler options (include paths,
11 | defines, etc.) for accurate IntelliSense.
12 |
13 | ## Usage
14 |
15 | Building the project is straightforward:
16 | ```
17 | dotnet build
18 | ```
19 |
20 | Then, invoke MSBuild with [the `-logger` option](https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-command-line-reference).
21 | For example:
22 | ```
23 | msbuild -logger:/path/to/CompileCommandsJson.dll MyProject
24 | ```
25 |
26 | By default, `compile_commands.json` is written in the current directory. You can
27 | control the output path using a parameter, e.g.:
28 | ```
29 | msbuild -logger:/path/to/CompileCommandsJson.dll;my_new_compile_commands.json MyProject
30 | ```
31 |
32 | ## Limitations
33 |
34 | There are two significant design limitations:
35 |
36 | 1. The logger will only emit entries for compiler invocations that it observes
37 | during a build; in particular, for an incremental build, there will be no
38 | output for any targets that are considered up to date.
39 |
40 | 2. The logger truncates the JSON file at startup, and writes to it
41 | incrementally throughout the build.
42 |
43 | Thus, for an accurate result you should use this logger only on a completely
44 | clean build, and to avoid confusing tools (such as VSCode) that may observe the
45 | file as it is written, you should probably write the output to a temporary file
46 | and rename it only after the build succeeds. Typical usage is roughly:
47 |
48 | ```
49 | rm -r out
50 | msbuild -logger:CompileCommandsLogger.dll;cctmp.json
51 | mv cctmp.json compile_commands.json
52 | ```
53 |
54 | ## Author
55 |
56 | Andrew Baumann
--------------------------------------------------------------------------------