Skip to content

Commit b9acf1a

Browse files
authored
Merge pull request #14111 from michaelnebel/csharp/reduceprojectrestore
C#: Avoid explicitly restoring projects in solution files.
2 parents 111227e + 84ec823 commit b9acf1a

File tree

5 files changed

+117
-27
lines changed

5 files changed

+117
-27
lines changed

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ public DependencyManager(string srcDir, IDependencyOptions options, ILogger logg
103103
progressMonitor.MissingNuGet();
104104
}
105105

106-
Restore(solutions);
107-
Restore(allProjects);
106+
var restoredProjects = RestoreSolutions(solutions);
107+
var projects = allProjects.Except(restoredProjects);
108+
RestoreProjects(projects);
108109
DownloadMissingPackages(allFiles);
109110
}
110111

@@ -351,14 +352,48 @@ private void AnalyseProject(FileInfo project)
351352

352353
}
353354

354-
private bool Restore(string target, string? pathToNugetConfig = null) =>
355-
dotnet.RestoreToDirectory(target, packageDirectory.DirInfo.FullName, pathToNugetConfig);
355+
private bool RestoreProject(string project, out string stdout, string? pathToNugetConfig = null) =>
356+
dotnet.RestoreProjectToDirectory(project, packageDirectory.DirInfo.FullName, out stdout, pathToNugetConfig);
356357

357-
private void Restore(IEnumerable<string> targets, string? pathToNugetConfig = null)
358+
private bool RestoreSolution(string solution, out IEnumerable<string> projects) =>
359+
dotnet.RestoreSolutionToDirectory(solution, packageDirectory.DirInfo.FullName, out projects);
360+
361+
/// <summary>
362+
/// Executes `dotnet restore` on all solution files in solutions.
363+
/// As opposed to RestoreProjects this is not run in parallel using PLINQ
364+
/// as `dotnet restore` on a solution already uses multiple threads for restoring
365+
/// the projects (this can be disabled with the `--disable-parallel` flag).
366+
/// Returns a list of projects that are up to date with respect to restore.
367+
/// </summary>
368+
/// <param name="solutions">A list of paths to solution files.</param>
369+
private IEnumerable<string> RestoreSolutions(IEnumerable<string> solutions) =>
370+
solutions.SelectMany(solution =>
371+
{
372+
RestoreSolution(solution, out var restoredProjects);
373+
return restoredProjects;
374+
});
375+
376+
/// <summary>
377+
/// Executes `dotnet restore` on all projects in projects.
378+
/// This is done in parallel for performance reasons.
379+
/// To ensure that output is not interleaved, the output of each
380+
/// restore is collected and printed.
381+
/// </summary>
382+
/// <param name="projects">A list of paths to project files.</param>
383+
private void RestoreProjects(IEnumerable<string> projects)
358384
{
359-
foreach (var target in targets)
385+
var stdoutLines = projects
386+
.AsParallel()
387+
.WithDegreeOfParallelism(options.Threads)
388+
.Select(project =>
389+
{
390+
RestoreProject(project, out var stdout);
391+
return stdout;
392+
})
393+
.ToList();
394+
foreach (var line in stdoutLines)
360395
{
361-
Restore(target, pathToNugetConfig);
396+
Console.WriteLine(line);
362397
}
363398
}
364399

@@ -401,10 +436,10 @@ private void DownloadMissingPackages(List<FileInfo> allFiles)
401436
continue;
402437
}
403438

404-
success = Restore(tempDir.DirInfo.FullName, nugetConfig);
439+
success = RestoreProject(tempDir.DirInfo.FullName, out var stdout, nugetConfig);
440+
Console.WriteLine(stdout);
405441

406442
// TODO: the restore might fail, we could retry with a prerelease (*-* instead of *) version of the package.
407-
408443
if (!success)
409444
{
410445
progressMonitor.FailedToRestoreNugetPackage(package);

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.IO;
5+
using System.Linq;
6+
using System.Text.RegularExpressions;
57
using Semmle.Util;
68

79
namespace Semmle.Extraction.CSharp.DependencyFetching
810
{
911
/// <summary>
1012
/// Utilities to run the "dotnet" command.
1113
/// </summary>
12-
internal class DotNet : IDotNet
14+
internal partial class DotNet : IDotNet
1315
{
1416
private readonly ProgressMonitor progressMonitor;
1517
private readonly string dotnet;
@@ -31,17 +33,22 @@ private void Info()
3133
}
3234
}
3335

34-
private ProcessStartInfo MakeDotnetStartInfo(string args, bool redirectStandardOutput) =>
35-
new ProcessStartInfo(dotnet, args)
36+
private ProcessStartInfo MakeDotnetStartInfo(string args, bool redirectStandardOutput)
37+
{
38+
var startInfo = new ProcessStartInfo(dotnet, args)
3639
{
3740
UseShellExecute = false,
3841
RedirectStandardOutput = redirectStandardOutput
3942
};
43+
// Set the .NET CLI language to English to avoid localized output.
44+
startInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"] = "en";
45+
return startInfo;
46+
}
4047

4148
private bool RunCommand(string args)
4249
{
4350
progressMonitor.RunningProcess($"{dotnet} {args}");
44-
using var proc = Process.Start(this.MakeDotnetStartInfo(args, redirectStandardOutput: false));
51+
using var proc = Process.Start(MakeDotnetStartInfo(args, redirectStandardOutput: false));
4552
proc?.WaitForExit();
4653
var exitCode = proc?.ExitCode ?? -1;
4754
if (exitCode != 0)
@@ -52,12 +59,50 @@ private bool RunCommand(string args)
5259
return true;
5360
}
5461

55-
public bool RestoreToDirectory(string projectOrSolutionFile, string packageDirectory, string? pathToNugetConfig = null)
62+
private bool RunCommand(string args, out IList<string> output)
63+
{
64+
progressMonitor.RunningProcess($"{dotnet} {args}");
65+
var pi = MakeDotnetStartInfo(args, redirectStandardOutput: true);
66+
var exitCode = pi.ReadOutput(out output);
67+
if (exitCode != 0)
68+
{
69+
progressMonitor.CommandFailed(dotnet, args, exitCode);
70+
return false;
71+
}
72+
return true;
73+
}
74+
75+
private static string GetRestoreArgs(string projectOrSolutionFile, string packageDirectory) =>
76+
$"restore --no-dependencies \"{projectOrSolutionFile}\" --packages \"{packageDirectory}\" /p:DisableImplicitNuGetFallbackFolder=true";
77+
78+
public bool RestoreProjectToDirectory(string projectFile, string packageDirectory, out string stdout, string? pathToNugetConfig = null)
5679
{
57-
var args = $"restore --no-dependencies \"{projectOrSolutionFile}\" --packages \"{packageDirectory}\" /p:DisableImplicitNuGetFallbackFolder=true";
80+
var args = GetRestoreArgs(projectFile, packageDirectory);
5881
if (pathToNugetConfig != null)
82+
{
5983
args += $" --configfile \"{pathToNugetConfig}\"";
60-
return RunCommand(args);
84+
}
85+
var success = RunCommand(args, out var output);
86+
stdout = string.Join("\n", output);
87+
return success;
88+
}
89+
90+
public bool RestoreSolutionToDirectory(string solutionFile, string packageDirectory, out IEnumerable<string> projects)
91+
{
92+
var args = GetRestoreArgs(solutionFile, packageDirectory);
93+
args += " --verbosity normal";
94+
if (RunCommand(args, out var output))
95+
{
96+
var regex = RestoreProjectRegex();
97+
projects = output
98+
.Select(line => regex.Match(line))
99+
.Where(match => match.Success)
100+
.Select(match => match.Groups[1].Value);
101+
return true;
102+
}
103+
104+
projects = Array.Empty<string>();
105+
return false;
61106
}
62107

63108
public bool New(string folder)
@@ -78,22 +123,21 @@ public bool AddPackage(string folder, string package)
78123

79124
private IList<string> GetListed(string args, string artifact)
80125
{
81-
progressMonitor.RunningProcess($"{dotnet} {args}");
82-
var pi = this.MakeDotnetStartInfo(args, redirectStandardOutput: true);
83-
var exitCode = pi.ReadOutput(out var artifacts);
84-
if (exitCode != 0)
126+
if (RunCommand(args, out var artifacts))
85127
{
86-
progressMonitor.CommandFailed(dotnet, args, exitCode);
87-
return new List<string>();
128+
progressMonitor.LogInfo($"Found {artifact}s: {string.Join("\n", artifacts)}");
129+
return artifacts;
88130
}
89-
progressMonitor.LogInfo($"Found {artifact}s: {string.Join("\n", artifacts)}");
90-
return artifacts;
131+
return new List<string>();
91132
}
92133

93134
public bool Exec(string execArgs)
94135
{
95136
var args = $"exec {execArgs}";
96137
return RunCommand(args);
97138
}
139+
140+
[GeneratedRegex("Restored\\s+(.+\\.csproj)", RegexOptions.Compiled)]
141+
private static partial Regex RestoreProjectRegex();
98142
}
99143
}

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNet.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ namespace Semmle.Extraction.CSharp.DependencyFetching
44
{
55
internal interface IDotNet
66
{
7-
bool RestoreToDirectory(string project, string directory, string? pathToNugetConfig = null);
7+
bool RestoreProjectToDirectory(string project, string directory, out string stdout, string? pathToNugetConfig = null);
8+
bool RestoreSolutionToDirectory(string solutionFile, string packageDirectory, out IEnumerable<string> projects);
89
bool New(string folder);
910
bool AddPackage(string folder, string package);
1011
IList<string> GetListedRuntimes();

csharp/extractor/Semmle.Extraction.Tests/FileContent.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using Xunit;
22
using System.Collections.Generic;
3-
using System.Linq;
43
using Semmle.Util.Logging;
54
using Semmle.Extraction.CSharp.DependencyFetching;
65

csharp/extractor/Semmle.Extraction.Tests/Runtime.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Xunit;
2+
using System;
23
using System.Collections.Generic;
34
using Semmle.Extraction.CSharp.DependencyFetching;
45

@@ -18,7 +19,17 @@ public DotNetStub(IList<string> runtimes, IList<string> sdks)
1819

1920
public bool New(string folder) => true;
2021

21-
public bool RestoreToDirectory(string project, string directory, string? pathToNugetConfig = null) => true;
22+
public bool RestoreProjectToDirectory(string project, string directory, out string stdout, string? pathToNugetConfig = null)
23+
{
24+
stdout = "";
25+
return true;
26+
}
27+
28+
public bool RestoreSolutionToDirectory(string solution, string directory, out IEnumerable<string> projects)
29+
{
30+
projects = Array.Empty<string>();
31+
return true;
32+
}
2233

2334
public IList<string> GetListedRuntimes() => runtimes;
2435

0 commit comments

Comments
 (0)