Skip to content

Commit 92acdb7

Browse files
author
Vladislav Kalugin
committed
Add initial implementation
1 parent f51507d commit 92acdb7

File tree

3 files changed

+376
-0
lines changed

3 files changed

+376
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# MSBuild Logger for export build database
2+
3+
### Usage
4+
5+
`msbuild -logger:/path/to/msbuild-database.dll /t:Rebuild MyProject `
6+
7+
`/t:Rebuild` - to recompile the entire project and get a list of all compilation commands
8+
9+
### Building
10+
11+
`dotnet build`
12+
13+
14+
Based on [Andrew Baumann - MsBuildCompileCommandsJson ](https://github.com/0xabu/MsBuildCompileCommandsJson)

msbuild-database.cs

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
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+
/// <summary>
12+
/// An MsBuild logger that emit compile_commands.json and link_commands.json files from a C++ project build.
13+
/// </summary>
14+
/// <remarks>
15+
/// Based on https://github.com/0xabu/MsBuildCompileCommandsJson
16+
/// </remarks>
17+
public class CompileDatabase : Logger
18+
{
19+
public override void Initialize(IEventSource eventSource)
20+
{
21+
string CompileOutputFilePath = "compile_commands.json";
22+
string LinkOutputFilePath = "link_commands.json";
23+
24+
try
25+
{
26+
const bool append = false;
27+
Encoding utf8WithoutBom = new UTF8Encoding(false);
28+
this.CompileStreamWriter = new StreamWriter(CompileOutputFilePath, append, utf8WithoutBom);
29+
this.firstLine = true;
30+
CompileStreamWriter.WriteLine("[");
31+
32+
this.LinkStreamWriter = new StreamWriter(LinkOutputFilePath, append, utf8WithoutBom);
33+
LinkStreamWriter.WriteLine("[");
34+
}
35+
catch (Exception ex)
36+
{
37+
if (ex is UnauthorizedAccessException
38+
|| ex is ArgumentNullException
39+
|| ex is PathTooLongException
40+
|| ex is DirectoryNotFoundException
41+
|| ex is NotSupportedException
42+
|| ex is ArgumentException
43+
|| ex is SecurityException
44+
|| ex is IOException)
45+
{
46+
throw new LoggerException("Failed to create .json files: " + ex.Message);
47+
}
48+
else
49+
{
50+
// Unexpected failure
51+
throw;
52+
}
53+
}
54+
55+
eventSource.AnyEventRaised += EventSource_AnyEventRaised;
56+
}
57+
58+
private void EventSource_AnyEventRaised(object sender, BuildEventArgs args)
59+
{
60+
if (args is TaskCommandLineEventArgs taskArgs)
61+
{
62+
const string clExe = ".exe ";
63+
int clExeIndex = taskArgs.CommandLine.IndexOf(clExe);
64+
if (clExeIndex == -1)
65+
{
66+
throw new LoggerException("Unexpected lack of executable in " + taskArgs.CommandLine);
67+
}
68+
69+
string exePath = taskArgs.CommandLine.Substring(0, clExeIndex + clExe.Length - 1);
70+
string argsString = taskArgs.CommandLine.Substring(clExeIndex + clExe.Length).TrimStart();
71+
string[] cmdArgs = CommandLineToArgs(argsString);
72+
string dirname = Path.GetDirectoryName(taskArgs.ProjectFile);
73+
String taskName = taskArgs.TaskName.ToLowerInvariant();
74+
75+
if (taskName == "cl")
76+
{
77+
bool isLink = false;
78+
foreach (String arg in cmdArgs)
79+
{
80+
if (arg.Substring(1).ToLowerInvariant() == "link")
81+
{
82+
isLink = true;
83+
break;
84+
}
85+
}
86+
87+
if (isLink)
88+
{
89+
ProcessLinkCommand(exePath, cmdArgs, dirname);
90+
}
91+
else
92+
{
93+
ProcessCompileCommand(exePath, cmdArgs, dirname);
94+
}
95+
}
96+
else if (taskName == "link" || taskArgs.TaskName == "lib")
97+
{
98+
ProcessLinkCommand(exePath, cmdArgs, dirname);
99+
}
100+
}
101+
}
102+
103+
[DllImport("shell32.dll", SetLastError = true)]
104+
static extern IntPtr CommandLineToArgvW(
105+
[MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs);
106+
107+
static string[] CommandLineToArgs(string commandLine)
108+
{
109+
commandLine = commandLine.Replace("\r\n", " ");
110+
int argc;
111+
var argv = CommandLineToArgvW(commandLine, out argc);
112+
if (argv == IntPtr.Zero)
113+
throw new System.ComponentModel.Win32Exception();
114+
try
115+
{
116+
var args = new string[argc];
117+
for (var i = 0; i < args.Length; i++)
118+
{
119+
var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
120+
args[i] = Marshal.PtrToStringUni(p);
121+
}
122+
123+
return args;
124+
}
125+
finally
126+
{
127+
Marshal.FreeHGlobal(argv);
128+
}
129+
}
130+
131+
private void ProcessCompileCommand(string compilerPath, string[] cmdArgs, String dirname)
132+
{
133+
// Options that consume the following argument.
134+
string[] optionsWithParam =
135+
{
136+
"D", "I", "F", "U", "FI", "FU",
137+
"analyze:log", "analyze:stacksize", "analyze:max_paths",
138+
"analyze:ruleset", "analyze:plugin"
139+
};
140+
141+
List<string> maybeFilenames = new List<string>();
142+
List<string> filenames = new List<string>();
143+
bool allFilenamesAreSources = false;
144+
145+
for (int i = 0; i < cmdArgs.Length; i++)
146+
{
147+
bool isOption = cmdArgs[i].StartsWith("/") || cmdArgs[i].StartsWith("-");
148+
string option = isOption ? cmdArgs[i].Substring(1) : "";
149+
150+
if (isOption && Array.Exists(optionsWithParam, e => e == option))
151+
{
152+
i++; // skip next arg
153+
}
154+
else if (option == "Tc" || option == "Tp")
155+
{
156+
// next arg is definitely a source file
157+
if (i + 1 < cmdArgs.Length)
158+
{
159+
filenames.Add(cmdArgs[i + 1]);
160+
}
161+
}
162+
else if (option.StartsWith("Tc") || option.StartsWith("Tp"))
163+
{
164+
// rest of this arg is definitely a source file
165+
filenames.Add(option.Substring(2));
166+
}
167+
else if (option == "TC" || option == "TP")
168+
{
169+
// all inputs are treated as source files
170+
allFilenamesAreSources = true;
171+
}
172+
else if (option == "link")
173+
{
174+
break; // only linker options follow
175+
}
176+
else if (isOption || cmdArgs[i].StartsWith("@"))
177+
{
178+
// other argument, ignore it
179+
}
180+
else
181+
{
182+
// non-argument, add it to our list of potential sources
183+
maybeFilenames.Add(cmdArgs[i]);
184+
}
185+
}
186+
187+
// Iterate over potential sources, and decide (based on the filename)
188+
// whether they are source inputs.
189+
foreach (string filename in maybeFilenames)
190+
{
191+
if (allFilenamesAreSources)
192+
{
193+
filenames.Add(filename);
194+
}
195+
else
196+
{
197+
int suffixPos = filename.LastIndexOf('.');
198+
if (suffixPos != -1)
199+
{
200+
string ext = filename.Substring(suffixPos + 1).ToLowerInvariant();
201+
if (ext == "c" || ext == "cxx" || ext == "cpp")
202+
{
203+
filenames.Add(filename);
204+
}
205+
}
206+
}
207+
}
208+
209+
// simplify the compile command to avoid .. etc.
210+
string compileCommand = '"' + Path.GetFullPath(compilerPath) + "\" " + String.Join(" ", cmdArgs);
211+
212+
WriteCompileCommand(compileCommand, filenames, dirname);
213+
}
214+
215+
private void ProcessLinkCommand(string compilerPath, string[] cmdArgs, String dirname)
216+
{
217+
// Options that consume the following argument.
218+
string[] optionsWithParam =
219+
{
220+
"D", "I", "F", "U", "FI", "FU",
221+
"analyze:log", "analyze:stacksize", "analyze:max_paths",
222+
"analyze:ruleset", "analyze:plugin"
223+
};
224+
225+
List<string> maybeFilenames = new List<string>();
226+
List<string> filenames = new List<string>();
227+
228+
for (int i = 0; i < cmdArgs.Length; i++)
229+
{
230+
bool isOption = cmdArgs[i].StartsWith("/") || cmdArgs[i].StartsWith("-");
231+
string option = isOption ? cmdArgs[i].Substring(1) : "";
232+
233+
if (isOption && Array.Exists(optionsWithParam, e => e == option))
234+
{
235+
i++; // skip next arg
236+
}
237+
else if (isOption || cmdArgs[i].StartsWith("@"))
238+
{
239+
// other argument, ignore it
240+
}
241+
else
242+
{
243+
// non-argument, add it to our list of potential sources
244+
maybeFilenames.Add(cmdArgs[i]);
245+
}
246+
}
247+
248+
// Iterate over potential sources, and decide (based on the filename)
249+
// whether they are source inputs.
250+
foreach (string filename in maybeFilenames)
251+
{
252+
int suffixPos = filename.LastIndexOf('.');
253+
if (suffixPos != -1)
254+
{
255+
string ext = filename.Substring(suffixPos + 1).ToLowerInvariant();
256+
if (ext == "obj" || ext == "lib" || ext == "dll")
257+
{
258+
filenames.Add(filename);
259+
}
260+
}
261+
}
262+
263+
// simplify the compile command to avoid .. etc.
264+
string compileCommand = '"' + Path.GetFullPath(compilerPath) + "\" " + String.Join(" ", cmdArgs);
265+
266+
WriteLinkCommand(compileCommand, filenames, dirname);
267+
}
268+
269+
private void WriteCompileCommand(string compileCommand, List<string> files, string dirname)
270+
{
271+
foreach (string filename in files)
272+
{
273+
// Terminate the preceding entry
274+
if (firstLine)
275+
{
276+
firstLine = false;
277+
}
278+
else
279+
{
280+
CompileStreamWriter.WriteLine(",");
281+
}
282+
283+
// Write one entry
284+
CompileStreamWriter.WriteLine(" {");
285+
CompileStreamWriter.WriteLine(String.Format(
286+
" \"command\": \"{0}\",",
287+
HttpUtility.JavaScriptStringEncode(compileCommand)));
288+
CompileStreamWriter.WriteLine(String.Format(
289+
" \"file\": \"{0}\",",
290+
HttpUtility.JavaScriptStringEncode(filename)));
291+
CompileStreamWriter.WriteLine(String.Format(
292+
" \"directory\": \"{0}\"",
293+
HttpUtility.JavaScriptStringEncode(dirname)));
294+
CompileStreamWriter.Write(" }");
295+
}
296+
}
297+
298+
private void WriteLinkCommand(string linkCommand, List<string> files, string dirname)
299+
{
300+
LinkStreamWriter.WriteLine(" {");
301+
LinkStreamWriter.WriteLine(String.Format(
302+
" \"directory\": \"{0}\",",
303+
HttpUtility.JavaScriptStringEncode(dirname)));
304+
LinkStreamWriter.WriteLine(String.Format(
305+
" \"command\": \"{0}\",",
306+
HttpUtility.JavaScriptStringEncode(linkCommand)));
307+
LinkStreamWriter.WriteLine(" \"files\": [");
308+
bool fl = true;
309+
foreach (string filename in files)
310+
{
311+
if (fl)
312+
{
313+
fl = false;
314+
}
315+
else
316+
{
317+
LinkStreamWriter.WriteLine(",");
318+
}
319+
320+
LinkStreamWriter.Write(String.Format(" \"{0}\"",
321+
HttpUtility.JavaScriptStringEncode(filename)));
322+
}
323+
324+
LinkStreamWriter.WriteLine();
325+
LinkStreamWriter.WriteLine(" ]");
326+
LinkStreamWriter.WriteLine(" }");
327+
}
328+
329+
public override void Shutdown()
330+
{
331+
if (!firstLine)
332+
{
333+
CompileStreamWriter.WriteLine();
334+
}
335+
336+
CompileStreamWriter.WriteLine("]");
337+
CompileStreamWriter.Close();
338+
339+
LinkStreamWriter.WriteLine("]");
340+
LinkStreamWriter.Close();
341+
342+
base.Shutdown();
343+
}
344+
345+
private StreamWriter CompileStreamWriter;
346+
private StreamWriter LinkStreamWriter;
347+
private bool firstLine;
348+
}

msbuild-database.csproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>netstandard2.0</TargetFrameworks>
5+
<Product />
6+
<Description>An MsBuild logger that emit compile_commands.json and link_commands.json files from a C++</Description>
7+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.Build" Version="14.3.0" PrivateAssets="All" />
11+
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="14.3.0" PrivateAssets="All" />
12+
</ItemGroup>
13+
14+
</Project>

0 commit comments

Comments
 (0)