Skip to content

Commit 836daaf

Browse files
committed
C#: Recognize .proj files in autobuilder
When determining the target of `msbuild` or `dotnet build`, first look for `.proj` files, then `.sln` files, and finally `.csproj`/`.vcxproj` files. In all three cases, choose the project/solution file closest to the root.
1 parent b95d7e5 commit 836daaf

File tree

11 files changed

+447
-211
lines changed

11 files changed

+447
-211
lines changed

change-notes/1.19/analysis-csharp.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@
2929

3030
* `getArgument()` on `AccessorCall` has been improved so it now takes tuple assignments into account. For example, the argument for the implicit `value` parameter in the setter of property `P` is `0` in `(P, x) = (0, 1)`. Additionally, the argument for the `value` parameter in compound assignments is now only the expanded value, for example, in `P += 7` the argument is `P + 7` and not `7`.
3131
* The predicate `isInArgument()` has been added to the `AssignableAccess` class. This holds for expressions that are passed as arguments using `in`.
32+
33+
## Changes to the autobuilder
34+
35+
* When determining the target of `msbuild` or `dotnet build`, first look for `.proj` files, then `.sln` files, and finally `.csproj`/`.vcxproj` files. In all three cases, choose the project/solution file closest to the root.

csharp/autobuilder/Semmle.Autobuild.Tests/BuildScripts.cs

Lines changed: 209 additions & 68 deletions
Large diffs are not rendered by default.

csharp/autobuilder/Semmle.Autobuild/Autobuilder.cs

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,32 @@ interface IBuildRule
2929
public class Autobuilder
3030
{
3131
/// <summary>
32-
/// Full file paths of files found in the project directory.
32+
/// Full file paths of files found in the project directory, as well
33+
/// their distance from the project root folder. The list is sorted
34+
/// by distance in ascending order.
3335
/// </summary>
34-
public IEnumerable<string> Paths => pathsLazy.Value;
35-
readonly Lazy<IEnumerable<string>> pathsLazy;
36+
public IEnumerable<(string, int)> Paths => pathsLazy.Value;
37+
readonly Lazy<IEnumerable<(string, int)>> pathsLazy;
3638

3739
/// <summary>
3840
/// Gets a list of paths matching a set of extensions
39-
/// (including the ".").
41+
/// (including the "."), as well their distance from the project root folder.
42+
/// The list is sorted by distance in ascending order.
4043
/// </summary>
4144
/// <param name="extensions">The extensions to find.</param>
4245
/// <returns>The files matching the extension.</returns>
43-
public IEnumerable<string> GetExtensions(params string[] extensions) =>
44-
Paths.Where(p => extensions.Contains(Path.GetExtension(p)));
46+
public IEnumerable<(string, int)> GetExtensions(params string[] extensions) =>
47+
Paths.Where(p => extensions.Contains(Path.GetExtension(p.Item1)));
4548

4649
/// <summary>
47-
/// Gets all paths matching a particular filename.
50+
/// Gets all paths matching a particular filename, as well
51+
/// their distance from the project root folder. The list is sorted
52+
/// by distance in ascending order.
4853
/// </summary>
4954
/// <param name="name">The filename to find.</param>
5055
/// <returns>Possibly empty sequence of paths with the given filename.</returns>
51-
public IEnumerable<string> GetFilename(string name) => Paths.Where(p => Path.GetFileName(p) == name);
56+
public IEnumerable<(string, int)> GetFilename(string name) =>
57+
Paths.Where(p => Path.GetFileName(p.Item1) == name);
5258

5359
/// <summary>
5460
/// Holds if a given path, relative to the root of the source directory
@@ -59,30 +65,30 @@ public IEnumerable<string> GetExtensions(params string[] extensions) =>
5965
public bool HasRelativePath(string path) => HasPath(Actions.PathCombine(RootDirectory, path));
6066

6167
/// <summary>
62-
/// List of solution files to build.
68+
/// List of project/solution files to build.
6369
/// </summary>
64-
public IList<ISolution> SolutionsToBuild => solutionsToBuildLazy.Value;
65-
readonly Lazy<IList<ISolution>> solutionsToBuildLazy;
70+
public IList<IProjectOrSolution> ProjectsOrSolutionsToBuild => projectsOrSolutionsToBuildLazy.Value;
71+
readonly Lazy<IList<IProjectOrSolution>> projectsOrSolutionsToBuildLazy;
6672

6773
/// <summary>
6874
/// Holds if a given path was found.
6975
/// </summary>
7076
/// <param name="path">The path of the file.</param>
7177
/// <returns>True iff the path was found.</returns>
72-
public bool HasPath(string path) => Paths.Any(p => path == p);
78+
public bool HasPath(string path) => Paths.Any(p => path == p.Item1);
7379

74-
void FindFiles(string dir, int depth, IList<string> results)
80+
void FindFiles(string dir, int depth, int maxDepth, IList<(string, int)> results)
7581
{
7682
foreach (var f in Actions.EnumerateFiles(dir))
7783
{
78-
results.Add(f);
84+
results.Add((f, depth));
7985
}
8086

81-
if (depth > 1)
87+
if (depth < maxDepth)
8288
{
8389
foreach (var d in Actions.EnumerateDirectories(dir))
8490
{
85-
FindFiles(d, depth - 1, results);
91+
FindFiles(d, depth + 1, maxDepth, results);
8692
}
8793
}
8894
}
@@ -113,46 +119,75 @@ public Autobuilder(IBuildActions actions, AutobuildOptions options)
113119
Actions = actions;
114120
Options = options;
115121

116-
pathsLazy = new Lazy<IEnumerable<string>>(() =>
122+
pathsLazy = new Lazy<IEnumerable<(string, int)>>(() =>
117123
{
118-
var files = new List<string>();
119-
FindFiles(options.RootDirectory, options.SearchDepth, files);
120-
return files.
121-
OrderBy(s => s.Count(c => c == Path.DirectorySeparatorChar)).
122-
ThenBy(s => Path.GetFileName(s).Length).
123-
ToArray();
124+
var files = new List<(string, int)>();
125+
FindFiles(options.RootDirectory, 0, options.SearchDepth, files);
126+
return files.OrderBy(f => f.Item2).ToArray();
124127
});
125128

126-
solutionsToBuildLazy = new Lazy<IList<ISolution>>(() =>
129+
projectsOrSolutionsToBuildLazy = new Lazy<IList<IProjectOrSolution>>(() =>
127130
{
128131
if (options.Solution.Any())
129132
{
130-
var ret = new List<ISolution>();
133+
var ret = new List<IProjectOrSolution>();
131134
foreach (var solution in options.Solution)
132135
{
133136
if (actions.FileExists(solution))
134137
ret.Add(new Solution(this, solution));
135138
else
136-
Log(Severity.Error, "The specified solution file {0} was not found", solution);
139+
Log(Severity.Error, $"The specified solution file {solution} was not found");
137140
}
138141
return ret;
139142
}
140143

141-
var solutions = GetExtensions(".sln").
142-
Select(s => new Solution(this, s)).
143-
Where(s => s.ProjectCount > 0).
144-
OrderByDescending(s => s.ProjectCount).
145-
ThenBy(s => s.Path.Length).
144+
bool FindFiles(string extension, Func<string, ProjectOrSolution> create, out IEnumerable<IProjectOrSolution> files)
145+
{
146+
var allFiles = GetExtensions(extension).
147+
Select(p => (ProjectOrSolution: create(p.Item1), DistanceFromRoot: p.Item2)).
148+
Where(p => p.ProjectOrSolution.HasLanguage(this.Options.Language)).
146149
ToArray();
147150

148-
foreach (var sln in solutions)
149-
{
150-
Log(Severity.Info, $"Found {sln.Path} with {sln.ProjectCount} {this.Options.Language} projects, version {sln.ToolsVersion}, config {string.Join(" ", sln.Configurations.Select(c => c.FullName))}");
151+
if (allFiles.Length == 0)
152+
{
153+
files = null;
154+
return false;
155+
}
156+
157+
if (options.AllSolutions)
158+
{
159+
files = allFiles.Select(p => p.ProjectOrSolution);
160+
return true;
161+
}
162+
163+
var firstIsClosest = allFiles.Length > 1 && allFiles[0].DistanceFromRoot < allFiles[1].DistanceFromRoot;
164+
if (allFiles.Length == 1 || firstIsClosest)
165+
{
166+
files = allFiles.Select(p => p.ProjectOrSolution).Take(1);
167+
return true;
168+
}
169+
170+
var candidates = allFiles.
171+
Where(f => f.DistanceFromRoot == allFiles[0].DistanceFromRoot).
172+
Select(f => f.ProjectOrSolution);
173+
Log(Severity.Info, $"Found multiple '{extension}' files, giving up: {string.Join(", ", candidates)}.");
174+
files = new IProjectOrSolution[0];
175+
return true;
151176
}
152177

153-
return new List<ISolution>(options.AllSolutions ?
154-
solutions :
155-
solutions.Take(1));
178+
// First look for `.proj` files
179+
if (FindFiles(".proj", f => new Project(this, f), out var ret1))
180+
return ret1.ToList();
181+
182+
// Then look for `.sln` files
183+
if (FindFiles(".sln", f => new Solution(this, f), out var ret2))
184+
return ret2.ToList();
185+
186+
// Finally look for language specific project files, e.g. `.csproj` files
187+
if (FindFiles(this.Options.Language.ProjectExtension, f => new Project(this, f), out var ret3))
188+
return ret3.ToList();
189+
190+
return new List<IProjectOrSolution>();
156191
});
157192

158193
SemmleDist = Actions.GetEnvironmentVariable("SEMMLE_DIST");

csharp/autobuilder/Semmle.Autobuild/BuildActions.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Generic;
44
using System.Diagnostics;
55
using System.IO;
6+
using System.Xml;
67

78
namespace Semmle.Autobuild
89
{
@@ -96,12 +97,27 @@ public interface IBuildActions
9697
/// <returns>The combined path.</returns>
9798
string PathCombine(params string[] parts);
9899

100+
/// <summary>
101+
/// Gets the full path for <paramref name="path"/>, Path.GetFullPath().
102+
/// </summary>
103+
string GetFullPath(string path);
104+
105+
/// <summary>
106+
/// Gets the directory of <paramref name="path"/>, Path.GetDirectoryName().
107+
/// </summary>
108+
string GetDirectoryName(string path);
109+
99110
/// <summary>
100111
/// Writes contents to file, File.WriteAllText().
101112
/// </summary>
102113
/// <param name="filename">The filename.</param>
103114
/// <param name="contents">The text.</param>
104115
void WriteAllText(string filename, string contents);
116+
117+
/// <summary>
118+
/// Loads the XML document from <paramref name="filename"/>.
119+
/// </summary>
120+
XmlDocument LoadXml(string filename);
105121
}
106122

107123
/// <summary>
@@ -167,10 +183,17 @@ int IBuildActions.RunProcess(string cmd, string args, string workingDirectory, I
167183

168184
void IBuildActions.WriteAllText(string filename, string contents) => File.WriteAllText(filename, contents);
169185

170-
private SystemBuildActions()
186+
XmlDocument IBuildActions.LoadXml(string filename)
171187
{
188+
var ret = new XmlDocument();
189+
ret.Load(filename);
190+
return ret;
172191
}
173192

193+
string IBuildActions.GetFullPath(string path) => Path.GetFullPath(path);
194+
195+
string IBuildActions.GetDirectoryName(string path) => Path.GetDirectoryName(path);
196+
174197
public static readonly IBuildActions Instance = new SystemBuildActions();
175198
}
176199
}

csharp/autobuilder/Semmle.Autobuild/BuildCommandAutoRule.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Collections.Generic;
22
using System.IO;
33
using System.Linq;
4-
using Semmle.Util;
54
using Semmle.Util.Logging;
65

76
namespace Semmle.Autobuild
@@ -32,7 +31,7 @@ public BuildScript Analyse(Autobuilder builder)
3231

3332
var extensions = builder.Actions.IsWindows() ? winExtensions : linuxExtensions;
3433
var scripts = buildScripts.SelectMany(s => extensions.Select(e => s + e));
35-
var scriptPath = builder.Paths.Where(p => scripts.Any(p.ToLower().EndsWith)).OrderBy(p => p.Length).FirstOrDefault();
34+
var scriptPath = builder.Paths.Where(p => scripts.Any(p.Item1.ToLower().EndsWith)).OrderBy(p => p.Item2).Select(p => p.Item1).FirstOrDefault();
3635

3736
if (scriptPath == null)
3837
return BuildScript.Failure;
@@ -41,12 +40,12 @@ public BuildScript Analyse(Autobuilder builder)
4140
chmod.RunCommand("/bin/chmod", $"u+x {scriptPath}");
4241
var chmodScript = builder.Actions.IsWindows() ? BuildScript.Success : BuildScript.Try(chmod.Script);
4342

44-
var path = Path.GetDirectoryName(scriptPath);
43+
var dir = builder.Actions.GetDirectoryName(scriptPath);
4544

4645
// A specific .NET Core version may be required
4746
return chmodScript & DotNetRule.WithDotNet(builder, dotNet =>
4847
{
49-
var command = new CommandBuilder(builder.Actions, path, dotNet?.Environment);
48+
var command = new CommandBuilder(builder.Actions, dir, dotNet?.Environment);
5049

5150
// A specific Visual Studio version may be required
5251
var vsTools = MsBuildRule.GetVcVarsBatFile(builder);

csharp/autobuilder/Semmle.Autobuild/DotNetRule.cs

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Newtonsoft.Json.Linq;
55
using System.Collections.Generic;
66
using System.IO;
7+
using Semmle.Util;
78

89
namespace Semmle.Autobuild
910
{
@@ -15,46 +16,36 @@ class DotNetRule : IBuildRule
1516
{
1617
public BuildScript Analyse(Autobuilder builder)
1718
{
18-
builder.Log(Severity.Info, "Attempting to build using .NET Core");
19-
20-
var projects = builder.SolutionsToBuild.Any()
21-
? builder.SolutionsToBuild.SelectMany(s => s.Projects).ToArray()
22-
: builder.GetExtensions(Language.CSharp.ProjectExtension).Select(p => new Project(builder, p)).ToArray();
19+
if (!builder.ProjectsOrSolutionsToBuild.Any())
20+
return BuildScript.Failure;
2321

24-
var notDotNetProject = projects.FirstOrDefault(p => !p.DotNetProject);
22+
var notDotNetProject = builder.ProjectsOrSolutionsToBuild.
23+
SelectMany(p => Enumerators.Singleton(p).Concat(p.IncludedProjects)).
24+
OfType<Project>().
25+
FirstOrDefault(p => !p.DotNetProject);
2526
if (notDotNetProject != null)
2627
{
2728
builder.Log(Severity.Info, "Not using .NET Core because of incompatible project {0}", notDotNetProject);
2829
return BuildScript.Failure;
2930
}
3031

31-
if (!builder.SolutionsToBuild.Any())
32-
// Attempt dotnet build in root folder
33-
return WithDotNet(builder, dotNet =>
34-
{
35-
var info = GetInfoCommand(builder.Actions, dotNet);
36-
var clean = GetCleanCommand(builder.Actions, dotNet).Script;
37-
var restore = GetRestoreCommand(builder.Actions, dotNet).Script;
38-
var build = GetBuildCommand(builder, dotNet).Script;
39-
return info & clean & BuildScript.Try(restore) & build;
40-
});
32+
builder.Log(Severity.Info, "Attempting to build using .NET Core");
4133

42-
// Attempt dotnet build on each solution
4334
return WithDotNet(builder, dotNet =>
4435
{
4536
var ret = GetInfoCommand(builder.Actions, dotNet);
46-
foreach (var solution in builder.SolutionsToBuild)
37+
foreach (var projectOrSolution in builder.ProjectsOrSolutionsToBuild)
4738
{
4839
var cleanCommand = GetCleanCommand(builder.Actions, dotNet);
49-
cleanCommand.QuoteArgument(solution.Path);
40+
cleanCommand.QuoteArgument(projectOrSolution.FullPath);
5041
var clean = cleanCommand.Script;
5142

5243
var restoreCommand = GetRestoreCommand(builder.Actions, dotNet);
53-
restoreCommand.QuoteArgument(solution.Path);
44+
restoreCommand.QuoteArgument(projectOrSolution.FullPath);
5445
var restore = restoreCommand.Script;
5546

5647
var buildCommand = GetBuildCommand(builder, dotNet);
57-
buildCommand.QuoteArgument(solution.Path);
48+
buildCommand.QuoteArgument(projectOrSolution.FullPath);
5849
var build = buildCommand.Script;
5950

6051
ret &= clean & BuildScript.Try(restore) & build;
@@ -110,7 +101,7 @@ static BuildScript DownloadDotNet(Autobuilder builder, string installDir)
110101
// See https://docs.microsoft.com/en-us/dotnet/core/tools/global-json
111102
var installScript = BuildScript.Success;
112103
var validGlobalJson = false;
113-
foreach (var path in builder.Paths.Where(p => p.EndsWith("global.json", StringComparison.Ordinal)))
104+
foreach (var path in builder.Paths.Select(p => p.Item1).Where(p => p.EndsWith("global.json", StringComparison.Ordinal)))
114105
{
115106
string version;
116107
try

csharp/autobuilder/Semmle.Autobuild/Language.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ public sealed class Language
88
public bool ProjectFileHasThisLanguage(string path) =>
99
System.IO.Path.GetExtension(path) == ProjectExtension;
1010

11-
public static bool IsProjectFileForAnySupportedLanguage(string path) =>
12-
Cpp.ProjectFileHasThisLanguage(path) || CSharp.ProjectFileHasThisLanguage(path);
13-
1411
public readonly string ProjectExtension;
1512

1613
private Language(string extension)

0 commit comments

Comments
 (0)