Skip to content

Commit 314ca79

Browse files
authored
Merge pull request github#13667 from michaelnebel/csharp/standalonescan
C#: Use dotnet --list-runtimes to find runtime locations.
2 parents a850a48 + e4aaa43 commit 314ca79

File tree

6 files changed

+272
-40
lines changed

6 files changed

+272
-40
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public BuildAnalysis(Options options, ProgressMonitor progressMonitor)
6666
// Find DLLs in the .Net Framework
6767
if (options.ScanNetFrameworkDlls)
6868
{
69-
var runtimeLocation = Runtime.GetRuntime(options.UseSelfContainedDotnet);
69+
var runtimeLocation = new Runtime(dotnet).GetRuntime(options.UseSelfContainedDotnet);
7070
progressMonitor.Log(Util.Logging.Severity.Debug, $"Runtime location selected: {runtimeLocation}");
7171
dllDirNames.Add(runtimeLocation);
7272
}

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

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
1-
using System;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
4+
using Semmle.Util;
35

46
namespace Semmle.BuildAnalyser
57
{
8+
internal interface IDotNet
9+
{
10+
bool RestoreToDirectory(string project, string directory);
11+
bool New(string folder);
12+
bool AddPackage(string folder, string package);
13+
public IList<string> GetListedRuntimes();
14+
}
15+
616
/// <summary>
717
/// Utilities to run the "dotnet" command.
818
/// </summary>
9-
internal class DotNet
19+
internal class DotNet : IDotNet
1020
{
21+
private const string dotnet = "dotnet";
1122
private readonly ProgressMonitor progressMonitor;
1223

1324
public DotNet(ProgressMonitor progressMonitor)
@@ -19,26 +30,26 @@ public DotNet(ProgressMonitor progressMonitor)
1930
private void Info()
2031
{
2132
// TODO: make sure the below `dotnet` version is matching the one specified in global.json
22-
progressMonitor.RunningProcess("dotnet --info");
23-
using var proc = Process.Start("dotnet", "--info");
33+
progressMonitor.RunningProcess($"{dotnet} --info");
34+
using var proc = Process.Start(dotnet, "--info");
2435
proc.WaitForExit();
2536
var ret = proc.ExitCode;
2637

2738
if (ret != 0)
2839
{
29-
progressMonitor.CommandFailed("dotnet", "--info", ret);
30-
throw new Exception($"dotnet --info failed with exit code {ret}.");
40+
progressMonitor.CommandFailed(dotnet, "--info", ret);
41+
throw new Exception($"{dotnet} --info failed with exit code {ret}.");
3142
}
3243
}
3344

3445
private bool RunCommand(string args)
3546
{
36-
progressMonitor.RunningProcess($"dotnet {args}");
37-
using var proc = Process.Start("dotnet", args);
47+
progressMonitor.RunningProcess($"{dotnet} {args}");
48+
using var proc = Process.Start(dotnet, args);
3849
proc.WaitForExit();
3950
if (proc.ExitCode != 0)
4051
{
41-
progressMonitor.CommandFailed("dotnet", args, proc.ExitCode);
52+
progressMonitor.CommandFailed(dotnet, args, proc.ExitCode);
4253
return false;
4354
}
4455

@@ -62,5 +73,22 @@ public bool AddPackage(string folder, string package)
6273
var args = $"add \"{folder}\" package \"{package}\" --no-restore";
6374
return RunCommand(args);
6475
}
76+
77+
public IList<string> GetListedRuntimes()
78+
{
79+
var args = "--list-runtimes";
80+
var pi = new ProcessStartInfo(dotnet, args)
81+
{
82+
RedirectStandardOutput = true,
83+
UseShellExecute = false
84+
};
85+
var exitCode = pi.ReadOutput(out var runtimes);
86+
if (exitCode != 0)
87+
{
88+
progressMonitor.CommandFailed(dotnet, args, exitCode);
89+
return new List<string>();
90+
}
91+
return runtimes;
92+
}
6593
}
6694
}

csharp/extractor/Semmle.Extraction.CSharp.Standalone/Properties/AssemblyInfo.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Reflection;
2+
using System.Runtime.CompilerServices;
23
using System.Runtime.InteropServices;
34

45
// General Information about an assembly is controlled through the following
@@ -13,6 +14,9 @@
1314
[assembly: AssemblyTrademark("")]
1415
[assembly: AssemblyCulture("")]
1516

17+
// Expose internals for testing purposes.
18+
[assembly: InternalsVisibleTo("Semmle.Extraction.Tests")]
19+
1620
// Setting ComVisible to false makes the types in this assembly not visible
1721
// to COM components. If you need to access a type in this assembly from
1822
// COM, set the ComVisible attribute to true on that type.

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

Lines changed: 114 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,114 @@
33
using System.Runtime.InteropServices;
44
using System.IO;
55
using System.Linq;
6+
using System.Text.RegularExpressions;
7+
using Semmle.BuildAnalyser;
68
using Semmle.Util;
79

810
namespace Semmle.Extraction.CSharp.Standalone
911
{
1012
/// <summary>
1113
/// Locates .NET Runtimes.
1214
/// </summary>
13-
internal static class Runtime
15+
internal partial class Runtime
1416
{
17+
private const string netCoreApp = "Microsoft.NETCore.App";
18+
private const string aspNetCoreApp = "Microsoft.AspNetCore.App";
19+
20+
private readonly IDotNet dotNet;
1521
private static string ExecutingRuntime => RuntimeEnvironment.GetRuntimeDirectory();
1622

17-
/// <summary>
18-
/// Locates .NET Core Runtimes.
19-
/// </summary>
20-
private static IEnumerable<string> CoreRuntimes
23+
public Runtime(IDotNet dotNet) => this.dotNet = dotNet;
24+
25+
internal sealed class RuntimeVersion : IComparable<RuntimeVersion>
2126
{
22-
get
27+
private readonly string dir;
28+
private readonly Version version;
29+
private readonly Version? preReleaseVersion;
30+
private readonly string? preReleaseVersionType;
31+
private bool IsPreRelease => preReleaseVersionType is not null && preReleaseVersion is not null;
32+
public string FullPath
2333
{
24-
var dotnetPath = FileUtils.FindProgramOnPath(Win32.IsWindows() ? "dotnet.exe" : "dotnet");
25-
var dotnetDirs = dotnetPath is not null
26-
? new[] { dotnetPath }
27-
: new[] { "/usr/share/dotnet", @"C:\Program Files\dotnet" };
28-
var coreDirs = dotnetDirs.Select(d => Path.Combine(d, "shared", "Microsoft.NETCore.App"));
34+
get
35+
{
36+
var preRelease = IsPreRelease ? $"-{preReleaseVersionType}.{preReleaseVersion}" : "";
37+
var version = this.version + preRelease;
38+
return Path.Combine(dir, version);
39+
}
40+
}
2941

30-
var dir = coreDirs.FirstOrDefault(Directory.Exists);
31-
if (dir is not null)
42+
public RuntimeVersion(string dir, string version, string preReleaseVersionType, string preReleaseVersion)
43+
{
44+
this.dir = dir;
45+
this.version = Version.Parse(version);
46+
if (!string.IsNullOrEmpty(preReleaseVersion) && !string.IsNullOrEmpty(preReleaseVersionType))
3247
{
33-
return Directory.EnumerateDirectories(dir).OrderByDescending(Path.GetFileName);
48+
this.preReleaseVersionType = preReleaseVersionType;
49+
this.preReleaseVersion = Version.Parse(preReleaseVersion);
3450
}
51+
}
3552

36-
return Enumerable.Empty<string>();
53+
public int CompareTo(RuntimeVersion? other)
54+
{
55+
var c = version.CompareTo(other?.version);
56+
if (c == 0 && IsPreRelease)
57+
{
58+
if (!other!.IsPreRelease)
59+
{
60+
return -1;
61+
}
62+
63+
// Both are pre-release like runtime versions.
64+
// The pre-release version types are sorted alphabetically (e.g. alpha, beta, preview, rc)
65+
// and the pre-release version types are more important that the pre-release version numbers.
66+
return preReleaseVersionType != other!.preReleaseVersionType
67+
? preReleaseVersionType!.CompareTo(other!.preReleaseVersionType)
68+
: preReleaseVersion!.CompareTo(other!.preReleaseVersion);
69+
}
70+
71+
return c;
3772
}
73+
74+
public override bool Equals(object? obj) =>
75+
obj is not null && obj is RuntimeVersion other && other.FullPath == FullPath;
76+
77+
public override int GetHashCode() => FullPath.GetHashCode();
78+
79+
public override string ToString() => FullPath;
80+
}
81+
82+
[GeneratedRegex(@"^(\S+)\s(\d+\.\d+\.\d+)(-([a-z]+)\.(\d+\.\d+\.\d+))?\s\[(\S+)\]$")]
83+
private static partial Regex RuntimeRegex();
84+
85+
/// <summary>
86+
/// Parses the output of `dotnet --list-runtimes` to get a map from a runtime to the location of
87+
/// the newest version of the runtime.
88+
/// It is assume that the format of a listed runtime is something like:
89+
/// Microsoft.NETCore.App 7.0.2 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
90+
/// </summary>
91+
private static Dictionary<string, RuntimeVersion> ParseRuntimes(IList<string> listed)
92+
{
93+
// Parse listed runtimes.
94+
var runtimes = new Dictionary<string, RuntimeVersion>();
95+
listed.ForEach(r =>
96+
{
97+
var match = RuntimeRegex().Match(r);
98+
if (match.Success)
99+
{
100+
runtimes.AddOrUpdate(match.Groups[1].Value, new RuntimeVersion(match.Groups[6].Value, match.Groups[2].Value, match.Groups[4].Value, match.Groups[5].Value));
101+
}
102+
});
103+
104+
return runtimes;
105+
}
106+
107+
/// <summary>
108+
/// Returns a dictionary mapping runtimes to their newest version.
109+
/// </summary>
110+
internal Dictionary<string, RuntimeVersion> GetNewestRuntimes()
111+
{
112+
var listed = dotNet.GetListedRuntimes();
113+
return ParseRuntimes(listed);
38114
}
39115

40116
/// <summary>
@@ -69,24 +145,33 @@ private static IEnumerable<string> DesktopRuntimes
69145
}
70146
}
71147

72-
/// <summary>
73-
/// Gets the .NET runtime location to use for extraction
74-
/// </summary>
75-
public static string GetRuntime(bool useSelfContained) => useSelfContained ? ExecutingRuntime : Runtimes.First();
76-
77-
private static IEnumerable<string> Runtimes
148+
private IEnumerable<string> GetRuntimes()
78149
{
79-
get
80-
{
81-
foreach (var r in CoreRuntimes)
82-
yield return r;
150+
// Gets the newest version of the installed runtimes.
151+
var newestRuntimes = GetNewestRuntimes();
83152

84-
foreach (var r in DesktopRuntimes)
85-
yield return r;
153+
// Location of the newest .NET Core Runtime.
154+
if (newestRuntimes.TryGetValue(netCoreApp, out var netCoreVersion))
155+
{
156+
yield return netCoreVersion.FullPath;
157+
}
86158

87-
// A bad choice if it's the self-contained runtime distributed in codeql dist.
88-
yield return ExecutingRuntime;
159+
// Location of the newest ASP.NET Core Runtime.
160+
if (newestRuntimes.TryGetValue(aspNetCoreApp, out var aspNetCoreVersion))
161+
{
162+
yield return aspNetCoreVersion.FullPath;
89163
}
164+
165+
foreach (var r in DesktopRuntimes)
166+
yield return r;
167+
168+
// A bad choice if it's the self-contained runtime distributed in codeql dist.
169+
yield return ExecutingRuntime;
90170
}
171+
172+
/// <summary>
173+
/// Gets the .NET runtime location to use for extraction
174+
/// </summary>
175+
public string GetRuntime(bool useSelfContained) => useSelfContained ? ExecutingRuntime : GetRuntimes().First();
91176
}
92177
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using Xunit;
2+
using System.Collections.Generic;
3+
using Semmle.BuildAnalyser;
4+
using Semmle.Extraction.CSharp.Standalone;
5+
6+
namespace Semmle.Extraction.Tests
7+
{
8+
internal class DotNetStub : IDotNet
9+
{
10+
private readonly IList<string> runtimes;
11+
12+
public DotNetStub(IList<string> runtimes) => this.runtimes = runtimes;
13+
14+
public bool AddPackage(string folder, string package) => true;
15+
16+
public bool New(string folder) => true;
17+
18+
public bool RestoreToDirectory(string project, string directory) => true;
19+
20+
public IList<string> GetListedRuntimes() => runtimes;
21+
}
22+
23+
public class RuntimeTests
24+
{
25+
[Fact]
26+
public void TestRuntime1()
27+
{
28+
// Setup
29+
var listedRuntimes = new List<string> {
30+
"Microsoft.AspNetCore.App 5.0.12 [/path/dotnet/shared/Microsoft.AspNetCore.App]",
31+
"Microsoft.AspNetCore.App 6.0.4 [/path/dotnet/shared/Microsoft.AspNetCore.App]",
32+
"Microsoft.AspNetCore.App 7.0.0 [/path/dotnet/shared/Microsoft.AspNetCore.App]",
33+
"Microsoft.AspNetCore.App 7.0.2 [/path/dotnet/shared/Microsoft.AspNetCore.App]",
34+
"Microsoft.NETCore.App 5.0.12 [/path/dotnet/shared/Microsoft.NETCore.App]",
35+
"Microsoft.NETCore.App 6.0.4 [/path/dotnet/shared/Microsoft.NETCore.App]",
36+
"Microsoft.NETCore.App 7.0.0 [/path/dotnet/shared/Microsoft.NETCore.App]",
37+
"Microsoft.NETCore.App 7.0.2 [/path/dotnet/shared/Microsoft.NETCore.App]"
38+
};
39+
var dotnet = new DotNetStub(listedRuntimes);
40+
var runtime = new Runtime(dotnet);
41+
42+
// Execute
43+
var runtimes = runtime.GetNewestRuntimes();
44+
45+
// Verify
46+
Assert.Equal(2, runtimes.Count);
47+
48+
Assert.True(runtimes.TryGetValue("Microsoft.AspNetCore.App", out var aspNetCoreApp));
49+
Assert.Equal("/path/dotnet/shared/Microsoft.AspNetCore.App/7.0.2", aspNetCoreApp.FullPath);
50+
51+
Assert.True(runtimes.TryGetValue("Microsoft.NETCore.App", out var netCoreApp));
52+
Assert.Equal("/path/dotnet/shared/Microsoft.NETCore.App/7.0.2", netCoreApp.FullPath);
53+
}
54+
55+
[Fact]
56+
public void TestRuntime2()
57+
{
58+
// Setup
59+
var listedRuntimes = new List<string>
60+
{
61+
"Microsoft.NETCore.App 7.0.2 [/path/dotnet/shared/Microsoft.NETCore.App]",
62+
"Microsoft.NETCore.App 8.0.0-preview.5.43280.8 [/path/dotnet/shared/Microsoft.NETCore.App]",
63+
"Microsoft.NETCore.App 8.0.0-preview.5.23280.8 [/path/dotnet/shared/Microsoft.NETCore.App]"
64+
};
65+
var dotnet = new DotNetStub(listedRuntimes);
66+
var runtime = new Runtime(dotnet);
67+
68+
// Execute
69+
var runtimes = runtime.GetNewestRuntimes();
70+
71+
// Verify
72+
Assert.Single(runtimes);
73+
74+
Assert.True(runtimes.TryGetValue("Microsoft.NETCore.App", out var netCoreApp));
75+
Assert.Equal("/path/dotnet/shared/Microsoft.NETCore.App/8.0.0-preview.5.43280.8", netCoreApp.FullPath);
76+
}
77+
78+
[Fact]
79+
public void TestRuntime3()
80+
{
81+
// Setup
82+
var listedRuntimes = new List<string>
83+
{
84+
"Microsoft.NETCore.App 7.0.2 [/path/dotnet/shared/Microsoft.NETCore.App]",
85+
"Microsoft.NETCore.App 8.0.0-rc.4.43280.8 [/path/dotnet/shared/Microsoft.NETCore.App]",
86+
"Microsoft.NETCore.App 8.0.0-preview.5.23280.8 [/path/dotnet/shared/Microsoft.NETCore.App]"
87+
};
88+
var dotnet = new DotNetStub(listedRuntimes);
89+
var runtime = new Runtime(dotnet);
90+
91+
// Execute
92+
var runtimes = runtime.GetNewestRuntimes();
93+
94+
// Verify
95+
Assert.Single(runtimes);
96+
97+
Assert.True(runtimes.TryGetValue("Microsoft.NETCore.App", out var netCoreApp));
98+
Assert.Equal("/path/dotnet/shared/Microsoft.NETCore.App/8.0.0-rc.4.43280.8", netCoreApp.FullPath);
99+
}
100+
101+
}
102+
}

0 commit comments

Comments
 (0)