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+ }
0 commit comments