Skip to content

Commit 14caaf1

Browse files
authored
Merge pull request github#13658 from tamasvajk/cs/standalone/restore-impr
C#: Improve dotnet restore success rate in standalone extraction
2 parents fab231c + d0b8b68 commit 14caaf1

File tree

5 files changed

+241
-141
lines changed

5 files changed

+241
-141
lines changed

csharp/extractor/Semmle.Extraction.CSharp.Standalone/AssemblyCache.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ internal class AssemblyCache
1616
/// Locate all reference files and index them.
1717
/// </summary>
1818
/// <param name="dirs">Directories to search.</param>
19-
/// <param name="progress">Callback for progress.</param>
20-
public AssemblyCache(IEnumerable<string> dirs, IProgressMonitor progress)
19+
/// <param name="progressMonitor">Callback for progress.</param>
20+
public AssemblyCache(IEnumerable<string> dirs, ProgressMonitor progressMonitor)
2121
{
2222
foreach (var dir in dirs)
2323
{
24-
progress.FindingFiles(dir);
24+
progressMonitor.FindingFiles(dir);
2525
AddReferenceDirectory(dir);
2626
}
2727
IndexReferences();

csharp/extractor/Semmle.Extraction.CSharp.Standalone/BuildAnalysis.cs

Lines changed: 147 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -8,89 +8,60 @@
88
using System.Collections.Concurrent;
99
using System.Text;
1010
using System.Security.Cryptography;
11+
using System.Text.RegularExpressions;
1112

1213
namespace Semmle.BuildAnalyser
1314
{
14-
/// <summary>
15-
/// The output of a build analysis.
16-
/// </summary>
17-
internal interface IBuildAnalysis
18-
{
19-
/// <summary>
20-
/// Full filepaths of external references.
21-
/// </summary>
22-
IEnumerable<string> ReferenceFiles { get; }
23-
24-
/// <summary>
25-
/// Full filepaths of C# source files from project files.
26-
/// </summary>
27-
IEnumerable<string> ProjectSourceFiles { get; }
28-
29-
/// <summary>
30-
/// Full filepaths of C# source files in the filesystem.
31-
/// </summary>
32-
IEnumerable<string> AllSourceFiles { get; }
33-
34-
/// <summary>
35-
/// The assembly IDs which could not be resolved.
36-
/// </summary>
37-
IEnumerable<string> UnresolvedReferences { get; }
38-
39-
/// <summary>
40-
/// List of source files referenced by projects but
41-
/// which were not found in the filesystem.
42-
/// </summary>
43-
IEnumerable<string> MissingSourceFiles { get; }
44-
}
45-
4615
/// <summary>
4716
/// Main implementation of the build analysis.
4817
/// </summary>
49-
internal sealed class BuildAnalysis : IBuildAnalysis, IDisposable
18+
internal sealed partial class BuildAnalysis : IDisposable
5019
{
5120
private readonly AssemblyCache assemblyCache;
52-
private readonly IProgressMonitor progressMonitor;
21+
private readonly ProgressMonitor progressMonitor;
5322
private readonly IDictionary<string, bool> usedReferences = new ConcurrentDictionary<string, bool>();
5423
private readonly IDictionary<string, bool> sources = new ConcurrentDictionary<string, bool>();
5524
private readonly IDictionary<string, string> unresolvedReferences = new ConcurrentDictionary<string, string>();
56-
private int failedProjects, succeededProjects;
25+
private int failedProjects;
26+
private int succeededProjects;
5727
private readonly string[] allSources;
5828
private int conflictedReferences = 0;
29+
private readonly Options options;
30+
private readonly DirectoryInfo sourceDir;
31+
private readonly DotNet dotnet;
5932

6033
/// <summary>
6134
/// Performs a C# build analysis.
6235
/// </summary>
6336
/// <param name="options">Analysis options from the command line.</param>
64-
/// <param name="progress">Display of analysis progress.</param>
65-
public BuildAnalysis(Options options, IProgressMonitor progress)
37+
/// <param name="progressMonitor">Display of analysis progress.</param>
38+
public BuildAnalysis(Options options, ProgressMonitor progressMonitor)
6639
{
6740
var startTime = DateTime.Now;
6841

69-
progressMonitor = progress;
70-
var sourceDir = new DirectoryInfo(options.SrcDir);
42+
this.options = options;
43+
this.progressMonitor = progressMonitor;
44+
this.sourceDir = new DirectoryInfo(options.SrcDir);
7145

72-
progressMonitor.FindingFiles(options.SrcDir);
46+
try
47+
{
48+
this.dotnet = new DotNet(progressMonitor);
49+
}
50+
catch
51+
{
52+
progressMonitor.MissingDotNet();
53+
throw;
54+
}
7355

74-
allSources = sourceDir.GetFiles("*.cs", SearchOption.AllDirectories)
75-
.Select(d => d.FullName)
76-
.Where(d => !options.ExcludesFile(d))
77-
.ToArray();
56+
this.progressMonitor.FindingFiles(options.SrcDir);
7857

79-
var dllDirNames = options.DllDirs.Select(Path.GetFullPath).ToList();
80-
packageDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName));
58+
this.allSources = GetFiles("*.cs").ToArray();
59+
var allProjects = GetFiles("*.csproj");
60+
var solutions = options.SolutionFile is not null
61+
? new[] { options.SolutionFile }
62+
: GetFiles("*.sln");
8163

82-
if (options.UseNuGet)
83-
{
84-
try
85-
{
86-
var nuget = new NugetPackages(sourceDir.FullName, packageDirectory);
87-
nuget.InstallPackages(progressMonitor);
88-
}
89-
catch (FileNotFoundException)
90-
{
91-
progressMonitor.MissingNuGet();
92-
}
93-
}
64+
var dllDirNames = options.DllDirs.Select(Path.GetFullPath).ToList();
9465

9566
// Find DLLs in the .Net Framework
9667
if (options.ScanNetFrameworkDlls)
@@ -100,28 +71,41 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
10071
dllDirNames.Add(runtimeLocation);
10172
}
10273

103-
// TODO: remove the below when the required SDK is installed
104-
using (new FileRenamer(sourceDir.GetFiles("global.json", SearchOption.AllDirectories)))
74+
if (options.UseMscorlib)
10575
{
106-
var solutions = options.SolutionFile is not null ?
107-
new[] { options.SolutionFile } :
108-
sourceDir.GetFiles("*.sln", SearchOption.AllDirectories).Select(d => d.FullName);
76+
UseReference(typeof(object).Assembly.Location);
77+
}
78+
79+
packageDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName));
10980

110-
if (options.UseNuGet)
81+
if (options.UseNuGet)
82+
{
83+
dllDirNames.Add(packageDirectory.DirInfo.FullName);
84+
try
11185
{
112-
RestoreSolutions(solutions);
86+
var nuget = new NugetPackages(sourceDir.FullName, packageDirectory, progressMonitor);
87+
nuget.InstallPackages();
88+
}
89+
catch (FileNotFoundException)
90+
{
91+
progressMonitor.MissingNuGet();
11392
}
114-
dllDirNames.Add(packageDirectory.DirInfo.FullName);
115-
assemblyCache = new BuildAnalyser.AssemblyCache(dllDirNames, progress);
116-
AnalyseSolutions(solutions);
11793

118-
foreach (var filename in assemblyCache.AllAssemblies.Select(a => a.Filename))
119-
UseReference(filename);
94+
// TODO: remove the below when the required SDK is installed
95+
using (new FileRenamer(sourceDir.GetFiles("global.json", SearchOption.AllDirectories)))
96+
{
97+
Restore(solutions);
98+
Restore(allProjects);
99+
DownloadMissingPackages(allProjects);
100+
}
120101
}
121102

122-
if (options.UseMscorlib)
103+
assemblyCache = new AssemblyCache(dllDirNames, progressMonitor);
104+
AnalyseSolutions(solutions);
105+
106+
foreach (var filename in assemblyCache.AllAssemblies.Select(a => a.Filename))
123107
{
124-
UseReference(typeof(object).Assembly.Location);
108+
UseReference(filename);
125109
}
126110

127111
ResolveConflicts();
@@ -149,6 +133,13 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
149133
DateTime.Now - startTime);
150134
}
151135

136+
private IEnumerable<string> GetFiles(string pattern)
137+
{
138+
return sourceDir.GetFiles(pattern, SearchOption.AllDirectories)
139+
.Select(d => d.FullName)
140+
.Where(d => !options.ExcludesFile(d));
141+
}
142+
152143
/// <summary>
153144
/// Computes a unique temp directory for the packages associated
154145
/// with this source tree. Use a SHA1 of the directory name.
@@ -158,9 +149,7 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
158149
private static string ComputeTempDirectory(string srcDir)
159150
{
160151
var bytes = Encoding.Unicode.GetBytes(srcDir);
161-
162-
using var sha1 = SHA1.Create();
163-
var sha = sha1.ComputeHash(bytes);
152+
var sha = SHA1.HashData(bytes);
164153
var sb = new StringBuilder();
165154
foreach (var b in sha.Take(8))
166155
sb.AppendFormat("{0:x2}", b);
@@ -195,12 +184,15 @@ private void ResolveConflicts()
195184

196185
// Pick the highest version for each assembly name
197186
foreach (var r in sortedReferences)
187+
{
198188
finalAssemblyList[r.Name] = r;
199-
189+
}
200190
// Update the used references list
201191
usedReferences.Clear();
202192
foreach (var r in finalAssemblyList.Select(r => r.Value.Filename))
193+
{
203194
UseReference(r);
195+
}
204196

205197
// Report the results
206198
foreach (var r in sortedReferences)
@@ -278,7 +270,9 @@ private void UnresolvedReference(string id, string projectFile)
278270
private void AnalyseProjectFiles(IEnumerable<FileInfo> projectFiles)
279271
{
280272
foreach (var proj in projectFiles)
273+
{
281274
AnalyseProject(proj);
275+
}
282276
}
283277

284278
private void AnalyseProject(FileInfo project)
@@ -324,36 +318,90 @@ private void AnalyseProject(FileInfo project)
324318

325319
}
326320

327-
private void Restore(string projectOrSolution)
321+
private bool Restore(string target)
328322
{
329-
int exit;
330-
try
323+
return dotnet.RestoreToDirectory(target, packageDirectory.DirInfo.FullName);
324+
}
325+
326+
private void Restore(IEnumerable<string> targets)
327+
{
328+
foreach (var target in targets)
331329
{
332-
exit = DotNet.RestoreToDirectory(projectOrSolution, packageDirectory.DirInfo.FullName);
330+
Restore(target);
333331
}
334-
catch (FileNotFoundException)
332+
}
333+
334+
private void DownloadMissingPackages(IEnumerable<string> restoreTargets)
335+
{
336+
var alreadyDownloadedPackages = Directory.GetDirectories(packageDirectory.DirInfo.FullName).Select(d => Path.GetFileName(d).ToLowerInvariant()).ToHashSet();
337+
var notYetDownloadedPackages = new HashSet<string>();
338+
339+
var allFiles = GetFiles("*.*").ToArray();
340+
foreach (var file in allFiles)
335341
{
336-
exit = 2;
342+
try
343+
{
344+
using var sr = new StreamReader(file);
345+
ReadOnlySpan<char> line;
346+
while ((line = sr.ReadLine()) != null)
347+
{
348+
foreach (var valueMatch in PackageReference().EnumerateMatches(line))
349+
{
350+
// We can't get the group from the ValueMatch, so doing it manually:
351+
var match = line.Slice(valueMatch.Index, valueMatch.Length);
352+
var includeIndex = match.IndexOf("Include", StringComparison.InvariantCultureIgnoreCase);
353+
if (includeIndex == -1)
354+
{
355+
continue;
356+
}
357+
358+
match = match.Slice(includeIndex + "Include".Length + 1);
359+
360+
var quoteIndex1 = match.IndexOf("\"");
361+
var quoteIndex2 = match.Slice(quoteIndex1 + 1).IndexOf("\"");
362+
363+
var packageName = match.Slice(quoteIndex1 + 1, quoteIndex2).ToString().ToLowerInvariant();
364+
if (!alreadyDownloadedPackages.Contains(packageName))
365+
{
366+
notYetDownloadedPackages.Add(packageName);
367+
}
368+
}
369+
}
370+
}
371+
catch (Exception ex)
372+
{
373+
progressMonitor.FailedToReadFile(file, ex);
374+
continue;
375+
}
337376
}
338377

339-
switch (exit)
378+
foreach (var package in notYetDownloadedPackages)
340379
{
341-
case 0:
342-
case 1:
343-
// No errors
344-
break;
345-
default:
346-
progressMonitor.CommandFailed("dotnet", $"restore \"{projectOrSolution}\"", exit);
347-
break;
348-
}
349-
}
380+
progressMonitor.NugetInstall(package);
381+
using var tempDir = new TemporaryDirectory(ComputeTempDirectory(package));
382+
var success = dotnet.New(tempDir.DirInfo.FullName);
383+
if (!success)
384+
{
385+
continue;
386+
}
387+
success = dotnet.AddPackage(tempDir.DirInfo.FullName, package);
388+
if (!success)
389+
{
390+
continue;
391+
}
350392

351-
public void RestoreSolutions(IEnumerable<string> solutions)
352-
{
353-
Parallel.ForEach(solutions, new ParallelOptions { MaxDegreeOfParallelism = 4 }, Restore);
393+
success = Restore(tempDir.DirInfo.FullName);
394+
395+
// TODO: the restore might fail, we could retry with a prerelease (*-* instead of *) version of the package.
396+
397+
if (!success)
398+
{
399+
progressMonitor.FailedToRestoreNugetPackage(package);
400+
}
401+
}
354402
}
355403

356-
public void AnalyseSolutions(IEnumerable<string> solutions)
404+
private void AnalyseSolutions(IEnumerable<string> solutions)
357405
{
358406
Parallel.ForEach(solutions, new ParallelOptions { MaxDegreeOfParallelism = 4 }, solutionFile =>
359407
{
@@ -374,5 +422,8 @@ public void Dispose()
374422
{
375423
packageDirectory?.Dispose();
376424
}
425+
426+
[GeneratedRegex("<PackageReference .*Include=\"(.*?)\".*/>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
427+
private static partial Regex PackageReference();
377428
}
378429
}

0 commit comments

Comments
 (0)