├── .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 --------------------------------------------------------------------------------