1
- using System ;
1
+ using Semmle . Util ;
2
+ using System ;
2
3
using System . Collections . Generic ;
3
4
using System . IO ;
4
5
using System . Linq ;
5
- using System . Runtime . InteropServices ;
6
- using Semmle . Util ;
7
6
using Semmle . Extraction . CSharp . Standalone ;
7
+ using System . Threading . Tasks ;
8
+ using System . Collections . Concurrent ;
9
+ using System . Text ;
10
+ using System . Security . Cryptography ;
8
11
9
12
namespace Semmle . BuildAnalyser
10
13
{
@@ -43,19 +46,18 @@ interface IBuildAnalysis
43
46
/// <summary>
44
47
/// Main implementation of the build analysis.
45
48
/// </summary>
46
- class BuildAnalysis : IBuildAnalysis
49
+ class BuildAnalysis : IBuildAnalysis , IDisposable
47
50
{
48
- readonly AssemblyCache assemblyCache ;
49
- readonly NugetPackages nuget ;
50
- readonly IProgressMonitor progressMonitor ;
51
- HashSet < string > usedReferences = new HashSet < string > ( ) ;
52
- readonly HashSet < string > usedSources = new HashSet < string > ( ) ;
53
- readonly HashSet < string > missingSources = new HashSet < string > ( ) ;
54
- readonly Dictionary < string , string > unresolvedReferences = new Dictionary < string , string > ( ) ;
55
- readonly DirectoryInfo sourceDir ;
56
- int failedProjects , succeededProjects ;
57
- readonly string [ ] allSources ;
58
- int conflictedReferences = 0 ;
51
+ private readonly AssemblyCache assemblyCache ;
52
+ private readonly NugetPackages nuget ;
53
+ private readonly IProgressMonitor progressMonitor ;
54
+ private readonly IDictionary < string , bool > usedReferences = new ConcurrentDictionary < string , bool > ( ) ;
55
+ private readonly IDictionary < string , bool > sources = new ConcurrentDictionary < string , bool > ( ) ;
56
+ private readonly IDictionary < string , string > unresolvedReferences = new ConcurrentDictionary < string , string > ( ) ;
57
+ private readonly DirectoryInfo sourceDir ;
58
+ private int failedProjects , succeededProjects ;
59
+ private readonly string [ ] allSources ;
60
+ private int conflictedReferences = 0 ;
59
61
60
62
/// <summary>
61
63
/// Performs a C# build analysis.
@@ -64,6 +66,8 @@ class BuildAnalysis : IBuildAnalysis
64
66
/// <param name="progress">Display of analysis progress.</param>
65
67
public BuildAnalysis ( Options options , IProgressMonitor progress )
66
68
{
69
+ var startTime = DateTime . Now ;
70
+
67
71
progressMonitor = progress ;
68
72
sourceDir = new DirectoryInfo ( options . SrcDir ) ;
69
73
@@ -74,36 +78,43 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
74
78
Where ( d => ! options . ExcludesFile ( d ) ) .
75
79
ToArray ( ) ;
76
80
77
- var dllDirNames = options . DllDirs . Select ( Path . GetFullPath ) ;
81
+ var dllDirNames = options . DllDirs . Select ( Path . GetFullPath ) . ToList ( ) ;
82
+ PackageDirectory = new TemporaryDirectory ( ComputeTempDirectory ( sourceDir . FullName ) ) ;
78
83
79
84
if ( options . UseNuGet )
80
85
{
81
- nuget = new NugetPackages ( sourceDir . FullName ) ;
82
- ReadNugetFiles ( ) ;
83
- dllDirNames = dllDirNames . Concat ( Enumerators . Singleton ( nuget . PackageDirectory ) ) ;
86
+ try
87
+ {
88
+ nuget = new NugetPackages ( sourceDir . FullName , PackageDirectory ) ;
89
+ ReadNugetFiles ( ) ;
90
+ }
91
+ catch ( FileNotFoundException )
92
+ {
93
+ progressMonitor . MissingNuGet ( ) ;
94
+ }
84
95
}
85
96
86
97
// Find DLLs in the .Net Framework
87
98
if ( options . ScanNetFrameworkDlls )
88
99
{
89
- dllDirNames = dllDirNames . Concat ( Runtime . Runtimes . Take ( 1 ) ) ;
100
+ dllDirNames . Add ( Runtime . Runtimes . First ( ) ) ;
90
101
}
91
102
92
- assemblyCache = new BuildAnalyser . AssemblyCache ( dllDirNames , progress ) ;
93
-
94
- // Analyse all .csproj files in the source tree.
95
- if ( options . SolutionFile != null )
96
- {
97
- AnalyseSolution ( options . SolutionFile ) ;
98
- }
99
- else if ( options . AnalyseCsProjFiles )
103
+ // These files can sometimes prevent `dotnet restore` from working correctly.
104
+ using ( new FileRenamer ( sourceDir . GetFiles ( "global.json" , SearchOption . AllDirectories ) ) )
105
+ using ( new FileRenamer ( sourceDir . GetFiles ( "Directory.Build.props" , SearchOption . AllDirectories ) ) )
100
106
{
101
- AnalyseProjectFiles ( ) ;
102
- }
107
+ var solutions = options . SolutionFile != null ?
108
+ new [ ] { options . SolutionFile } :
109
+ sourceDir . GetFiles ( "*.sln" , SearchOption . AllDirectories ) . Select ( d => d . FullName ) ;
103
110
104
- if ( ! options . AnalyseCsProjFiles )
105
- {
106
- usedReferences = new HashSet < string > ( assemblyCache . AllAssemblies . Select ( a => a . Filename ) ) ;
111
+ RestoreSolutions ( solutions ) ;
112
+ dllDirNames . Add ( PackageDirectory . DirInfo . FullName ) ;
113
+ assemblyCache = new BuildAnalyser . AssemblyCache ( dllDirNames , progress ) ;
114
+ AnalyseSolutions ( solutions ) ;
115
+
116
+ foreach ( var filename in assemblyCache . AllAssemblies . Select ( a => a . Filename ) )
117
+ UseReference ( filename ) ;
107
118
}
108
119
109
120
ResolveConflicts ( ) ;
@@ -114,7 +125,7 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
114
125
}
115
126
116
127
// Output the findings
117
- foreach ( var r in usedReferences )
128
+ foreach ( var r in usedReferences . Keys )
118
129
{
119
130
progressMonitor . ResolvedReference ( r ) ;
120
131
}
@@ -132,7 +143,27 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
132
143
UnresolvedReferences . Count ( ) ,
133
144
conflictedReferences ,
134
145
succeededProjects + failedProjects ,
135
- failedProjects ) ;
146
+ failedProjects ,
147
+ DateTime . Now - startTime ) ;
148
+ }
149
+
150
+ /// <summary>
151
+ /// Computes a unique temp directory for the packages associated
152
+ /// with this source tree. Use a SHA1 of the directory name.
153
+ /// </summary>
154
+ /// <param name="srcDir"></param>
155
+ /// <returns>The full path of the temp directory.</returns>
156
+ private static string ComputeTempDirectory ( string srcDir )
157
+ {
158
+ var bytes = Encoding . Unicode . GetBytes ( srcDir ) ;
159
+
160
+ using var sha1 = new SHA1CryptoServiceProvider ( ) ;
161
+ var sha = sha1 . ComputeHash ( bytes ) ;
162
+ var sb = new StringBuilder ( ) ;
163
+ foreach ( var b in sha . Take ( 8 ) )
164
+ sb . AppendFormat ( "{0:x2}" , b ) ;
165
+
166
+ return Path . Combine ( Path . GetTempPath ( ) , "GitHub" , "packages" , sb . ToString ( ) ) ;
136
167
}
137
168
138
169
/// <summary>
@@ -143,7 +174,7 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
143
174
void ResolveConflicts ( )
144
175
{
145
176
var sortedReferences = usedReferences .
146
- Select ( r => assemblyCache . GetAssemblyInfo ( r ) ) .
177
+ Select ( r => assemblyCache . GetAssemblyInfo ( r . Key ) ) .
147
178
OrderBy ( r => r . Version ) .
148
179
ToArray ( ) ;
149
180
@@ -154,7 +185,9 @@ void ResolveConflicts()
154
185
finalAssemblyList [ r . Name ] = r ;
155
186
156
187
// Update the used references list
157
- usedReferences = new HashSet < string > ( finalAssemblyList . Select ( r => r . Value . Filename ) ) ;
188
+ usedReferences . Clear ( ) ;
189
+ foreach ( var r in finalAssemblyList . Select ( r => r . Value . Filename ) )
190
+ UseReference ( r ) ;
158
191
159
192
// Report the results
160
193
foreach ( var r in sortedReferences )
@@ -183,7 +216,7 @@ void ReadNugetFiles()
183
216
/// <param name="reference">The filename of the reference.</param>
184
217
void UseReference ( string reference )
185
218
{
186
- usedReferences . Add ( reference ) ;
219
+ usedReferences [ reference ] = true ;
187
220
}
188
221
189
222
/// <summary>
@@ -192,25 +225,18 @@ void UseReference(string reference)
192
225
/// <param name="sourceFile">The source file.</param>
193
226
void UseSource ( FileInfo sourceFile )
194
227
{
195
- if ( sourceFile . Exists )
196
- {
197
- usedSources . Add ( sourceFile . FullName ) ;
198
- }
199
- else
200
- {
201
- missingSources . Add ( sourceFile . FullName ) ;
202
- }
228
+ sources [ sourceFile . FullName ] = sourceFile . Exists ;
203
229
}
204
230
205
231
/// <summary>
206
232
/// The list of resolved reference files.
207
233
/// </summary>
208
- public IEnumerable < string > ReferenceFiles => this . usedReferences ;
234
+ public IEnumerable < string > ReferenceFiles => this . usedReferences . Keys ;
209
235
210
236
/// <summary>
211
237
/// The list of source files used in projects.
212
238
/// </summary>
213
- public IEnumerable < string > ProjectSourceFiles => usedSources ;
239
+ public IEnumerable < string > ProjectSourceFiles => sources . Where ( s => s . Value ) . Select ( s => s . Key ) ;
214
240
215
241
/// <summary>
216
242
/// All of the source files in the source directory.
@@ -226,7 +252,7 @@ void UseSource(FileInfo sourceFile)
226
252
/// List of source files which were mentioned in project files but
227
253
/// do not exist on the file system.
228
254
/// </summary>
229
- public IEnumerable < string > MissingSourceFiles => missingSources ;
255
+ public IEnumerable < string > MissingSourceFiles => sources . Where ( s => ! s . Value ) . Select ( s => s . Key ) ;
230
256
231
257
/// <summary>
232
258
/// Record that a particular reference couldn't be resolved.
@@ -239,74 +265,101 @@ void UnresolvedReference(string id, string projectFile)
239
265
unresolvedReferences [ id ] = projectFile ;
240
266
}
241
267
242
- /// <summary>
243
- /// Performs an analysis of all .csproj files.
244
- /// </summary>
245
- void AnalyseProjectFiles ( )
246
- {
247
- AnalyseProjectFiles ( sourceDir . GetFiles ( "*.csproj" , SearchOption . AllDirectories ) ) ;
248
- }
268
+ readonly TemporaryDirectory PackageDirectory ;
249
269
250
270
/// <summary>
251
271
/// Reads all the source files and references from the given list of projects.
252
272
/// </summary>
253
273
/// <param name="projectFiles">The list of projects to analyse.</param>
254
- void AnalyseProjectFiles ( FileInfo [ ] projectFiles )
274
+ void AnalyseProjectFiles ( IEnumerable < FileInfo > projectFiles )
255
275
{
256
- progressMonitor . AnalysingProjectFiles ( projectFiles . Count ( ) ) ;
257
-
258
276
foreach ( var proj in projectFiles )
277
+ AnalyseProject ( proj ) ;
278
+ }
279
+
280
+ void AnalyseProject ( FileInfo project )
281
+ {
282
+ if ( ! project . Exists )
259
283
{
260
- try
261
- {
262
- var csProj = new CsProjFile ( proj ) ;
284
+ progressMonitor . MissingProject ( project . FullName ) ;
285
+ return ;
286
+ }
287
+
288
+ try
289
+ {
290
+ var csProj = new CsProjFile ( project ) ;
263
291
264
- foreach ( var @ref in csProj . References )
292
+ foreach ( var @ref in csProj . References )
293
+ {
294
+ AssemblyInfo resolved = assemblyCache . ResolveReference ( @ref ) ;
295
+ if ( ! resolved . Valid )
265
296
{
266
- AssemblyInfo resolved = assemblyCache . ResolveReference ( @ref ) ;
267
- if ( ! resolved . Valid )
268
- {
269
- UnresolvedReference ( @ref , proj . FullName ) ;
270
- }
271
- else
272
- {
273
- UseReference ( resolved . Filename ) ;
274
- }
297
+ UnresolvedReference ( @ref , project . FullName ) ;
275
298
}
276
-
277
- foreach ( var src in csProj . Sources )
299
+ else
278
300
{
279
- // Make a note of which source files the projects use.
280
- // This information doesn't affect the build but is dumped
281
- // as diagnostic output.
282
- UseSource ( new FileInfo ( src ) ) ;
301
+ UseReference ( resolved . Filename ) ;
283
302
}
284
- ++ succeededProjects ;
285
303
}
286
- catch ( Exception ex ) // lgtm[cs/catch-of-all-exceptions]
304
+
305
+ foreach ( var src in csProj . Sources )
287
306
{
288
- ++ failedProjects ;
289
- progressMonitor . FailedProjectFile ( proj . FullName , ex . Message ) ;
307
+ // Make a note of which source files the projects use.
308
+ // This information doesn't affect the build but is dumped
309
+ // as diagnostic output.
310
+ UseSource ( new FileInfo ( src ) ) ;
290
311
}
312
+
313
+ ++ succeededProjects ;
314
+ }
315
+ catch ( Exception ex ) // lgtm[cs/catch-of-all-exceptions]
316
+ {
317
+ ++ failedProjects ;
318
+ progressMonitor . FailedProjectFile ( project . FullName , ex . Message ) ;
291
319
}
320
+
292
321
}
293
322
294
- /// <summary>
295
- /// Delete packages directory.
296
- /// </summary>
297
- public void Cleanup ( )
323
+ void Restore ( string projectOrSolution )
324
+ {
325
+ int exit = DotNet . RestoreToDirectory ( projectOrSolution , PackageDirectory . DirInfo . FullName ) ;
326
+ switch ( exit )
327
+ {
328
+ case 0 :
329
+ case 1 :
330
+ // No errors
331
+ break ;
332
+ default :
333
+ progressMonitor . CommandFailed ( "dotnet" , $ "restore \" { projectOrSolution } \" ", exit ) ;
334
+ break ;
335
+ }
336
+ }
337
+
338
+ public void RestoreSolutions ( IEnumerable < string > solutions )
298
339
{
299
- if ( nuget != null ) nuget . Cleanup ( progressMonitor ) ;
340
+ Parallel . ForEach ( solutions , new ParallelOptions { MaxDegreeOfParallelism = 4 } , Restore ) ;
300
341
}
301
342
302
- /// <summary>
303
- /// Analyse all project files in a given solution only.
304
- /// </summary>
305
- /// <param name="solutionFile">The filename of the solution.</param>
306
- public void AnalyseSolution ( string solutionFile )
343
+ public void AnalyseSolutions ( IEnumerable < string > solutions )
344
+ {
345
+ Parallel . ForEach ( solutions , new ParallelOptions { MaxDegreeOfParallelism = 4 } , solutionFile =>
346
+ {
347
+ try
348
+ {
349
+ var sln = new SolutionFile ( solutionFile ) ;
350
+ progressMonitor . AnalysingSolution ( solutionFile ) ;
351
+ AnalyseProjectFiles ( sln . Projects . Select ( p => new FileInfo ( p ) ) . Where ( p => p . Exists ) ) ;
352
+ }
353
+ catch ( Microsoft . Build . Exceptions . InvalidProjectFileException ex )
354
+ {
355
+ progressMonitor . FailedProjectFile ( solutionFile , ex . BaseMessage ) ;
356
+ }
357
+ } ) ;
358
+ }
359
+
360
+ public void Dispose ( )
307
361
{
308
- var sln = new SolutionFile ( solutionFile ) ;
309
- AnalyseProjectFiles ( sln . Projects . Select ( p => new FileInfo ( p ) ) . ToArray ( ) ) ;
362
+ PackageDirectory ? . Dispose ( ) ;
310
363
}
311
364
}
312
365
}
0 commit comments