Skip to content

C# Change desktop dotnet assembly lookup to fall back to nuget reference assemblies #15600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public static string[] AsListWithExpandedEnvVars(this string? value, IBuildActio
return defaultValue;

return value.
Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).
Split(FileUtils.NewLineCharacters, StringSplitOptions.RemoveEmptyEntries).
Select(s => AsStringWithExpandedEnvVars(s, actions)).ToArray();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public DependencyManager(string srcDir, IDependencyOptions options, ILogger logg
try
{
this.dotnet = DotNet.Make(options, logger, tempWorkingDirectory);
runtimeLazy = new Lazy<Runtime>(() => new Runtime(dotnet));
runtimeLazy = new Lazy<Runtime>(() => new Runtime(dotnet, logger));
}
catch
{
Expand Down Expand Up @@ -111,7 +111,7 @@ public DependencyManager(string srcDir, IDependencyOptions options, ILogger logg
logger.LogInfo($"Unresolved reference {r.Key} in project {r.Value}");
}

var webViewExtractionOption = Environment.GetEnvironmentVariable("CODEQL_EXTRACTOR_CSHARP_STANDALONE_EXTRACT_WEB_VIEWS");
var webViewExtractionOption = Environment.GetEnvironmentVariable(EnvironmentVariableNames.WebViewGeneration);
if (webViewExtractionOption == null ||
bool.TryParse(webViewExtractionOption, out var shouldExtractWebViews) &&
shouldExtractWebViews)
Expand Down Expand Up @@ -158,6 +158,53 @@ private HashSet<string> AddFrameworkDlls(HashSet<string> dllPaths)
{
var frameworkLocations = new HashSet<string>();

var frameworkReferences = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DotnetFrameworkReferences);
var frameworkReferencesUseSubfolders = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DotnetFrameworkReferencesUseSubfolders);
_ = bool.TryParse(frameworkReferencesUseSubfolders, out var useSubfolders);
if (!string.IsNullOrWhiteSpace(frameworkReferences))
{
RemoveFrameworkNugetPackages(dllPaths);
RemoveNugetPackageReference(FrameworkPackageNames.AspNetCoreFramework, dllPaths);
RemoveNugetPackageReference(FrameworkPackageNames.WindowsDesktopFramework, dllPaths);
Comment on lines +166 to +168
Copy link
Contributor Author

@tamasvajk tamasvajk Feb 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this doesn't work on some early .net core variants, where the framework was split up into many nuget packages, and therefore they wouldn't be identified as framework references but as standard nuget packages. But for the moment, I think it's good enough.


var frameworkPaths = frameworkReferences.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);

foreach (var path in frameworkPaths)
{
if (!Directory.Exists(path))
{
logger.LogError($"Specified framework reference path '{path}' does not exist.");
continue;
}

if (useSubfolders)
{
dllPaths.Add(path);
frameworkLocations.Add(path);
continue;
}

try
{
var dlls = Directory.GetFiles(path, "*.dll", new EnumerationOptions { RecurseSubdirectories = false, MatchCasing = MatchCasing.CaseInsensitive });
if (dlls.Length == 0)
{
logger.LogError($"No DLLs found in specified framework reference path '{path}'.");
continue;
}

dllPaths.UnionWith(dlls);
frameworkLocations.UnionWith(dlls);
}
catch (Exception e)
{
logger.LogError($"Error while searching for DLLs in '{path}': {e.Message}");
}
Comment on lines +199 to +202

Check notice

Code scanning / CodeQL

Generic catch clause

Generic catch clause.
}

return frameworkLocations;
}

AddNetFrameworkDlls(dllPaths, frameworkLocations);
AddAspNetCoreFrameworkDlls(dllPaths, frameworkLocations);
AddMicrosoftWindowsDesktopDlls(dllPaths, frameworkLocations);
Expand Down Expand Up @@ -203,9 +250,9 @@ private void RestoreNugetPackages(List<FileInfo> allNonBinaryFiles, IEnumerable<
nugetPackageDllPaths.ExceptWith(excludedPaths);
dllPaths.UnionWith(nugetPackageDllPaths);
}
catch (Exception)
catch (Exception exc)
{
logger.LogError("Failed to restore Nuget packages with nuget.exe");
logger.LogError($"Failed to restore Nuget packages with nuget.exe: {exc.Message}");
}

var restoredProjects = RestoreSolutions(allSolutions, out var assets1);
Expand Down Expand Up @@ -296,14 +343,23 @@ private static DirectoryInfo[] GetPackageVersionSubDirectories(string packagePat
.ToArray();
}

private void RemoveFrameworkNugetPackages(ISet<string> dllPaths, int fromIndex = 0)
{
var packagesInPrioOrder = FrameworkPackageNames.NetFrameworks;
for (var i = fromIndex; i < packagesInPrioOrder.Length; i++)
{
RemoveNugetPackageReference(packagesInPrioOrder[i], dllPaths);
}
}

private void AddNetFrameworkDlls(ISet<string> dllPaths, ISet<string> frameworkLocations)
{
// Multiple dotnet framework packages could be present.
// The order of the packages is important, we're adding the first one that is present in the nuget cache.
var packagesInPrioOrder = FrameworkPackageNames.NetFrameworks;

var frameworkPaths = packagesInPrioOrder
.Select((s, index) => (Index: index, Path: GetPackageDirectory(s)))
.Select((s, index) => (Index: index, Path: GetPackageDirectory(s, packageDirectory)))
.Where(pair => pair.Path is not null)
.ToArray();

Expand All @@ -317,12 +373,7 @@ private void AddNetFrameworkDlls(ISet<string> dllPaths, ISet<string> frameworkLo
}

SelectNewestFrameworkPath(frameworkPath.Path, ".NET Framework", dllPaths, frameworkLocations);

for (var i = frameworkPath.Index + 1; i < packagesInPrioOrder.Length; i++)
{
RemoveNugetPackageReference(packagesInPrioOrder[i], dllPaths);
}

RemoveFrameworkNugetPackages(dllPaths, frameworkPath.Index + 1);
return;
}

Expand All @@ -335,6 +386,16 @@ private void AddNetFrameworkDlls(ISet<string> dllPaths, ISet<string> frameworkLo
else if (fileContent.IsLegacyProjectStructureUsed)
{
runtimeLocation = Runtime.DesktopRuntime;

if (runtimeLocation is null)
{
logger.LogInfo("No .NET Desktop Runtime location found. Attempting to restore the .NET Framework reference assemblies manually.");

if (TryRestorePackageManually(FrameworkPackageNames.LatestNetFrameworkReferenceAssemblies, null))
{
runtimeLocation = GetPackageDirectory(FrameworkPackageNames.LatestNetFrameworkReferenceAssemblies, missingPackageDirectory);
}
Comment on lines +394 to +397
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early commits in this PR tested this behaviour, but that's not the case any longer. Is there an easy way of covering this with an integration test?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the problem that this is an extremely unhappy path that is hard to replicate (as there none of the runners that satisfy the criteria for missing paths)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct. We'd get here if there was no mono installation on linux/macos runners, and no .net install on windows. (I've faced the original issue on a mac with no mono.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yes, then we have reached the limit for what is integration testable unless we introduce some way of adding extra test configuration (we probably shouldn't do this).
Maybe accept that we can't integration test this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When #15650 and this PR are merged, I'll set up some manual testing to see if the extractor is really working without mono.

}
}

runtimeLocation ??= Runtime.ExecutingRuntime;
Expand Down Expand Up @@ -374,7 +435,7 @@ private void AddAspNetCoreFrameworkDlls(ISet<string> dllPaths, ISet<string> fram
}

// First try to find ASP.NET Core assemblies in the NuGet packages
if (GetPackageDirectory(FrameworkPackageNames.AspNetCoreFramework) is string aspNetCorePackage)
if (GetPackageDirectory(FrameworkPackageNames.AspNetCoreFramework, packageDirectory) is string aspNetCorePackage)
{
SelectNewestFrameworkPath(aspNetCorePackage, "ASP.NET Core", dllPaths, frameworkLocations);
return;
Expand All @@ -390,15 +451,15 @@ private void AddAspNetCoreFrameworkDlls(ISet<string> dllPaths, ISet<string> fram

private void AddMicrosoftWindowsDesktopDlls(ISet<string> dllPaths, ISet<string> frameworkLocations)
{
if (GetPackageDirectory(FrameworkPackageNames.WindowsDesktopFramework) is string windowsDesktopApp)
if (GetPackageDirectory(FrameworkPackageNames.WindowsDesktopFramework, packageDirectory) is string windowsDesktopApp)
{
SelectNewestFrameworkPath(windowsDesktopApp, "Windows Desktop App", dllPaths, frameworkLocations);
}
}

private string? GetPackageDirectory(string packagePrefix)
private string? GetPackageDirectory(string packagePrefix, TemporaryDirectory root)
{
return new DirectoryInfo(packageDirectory.DirInfo.FullName)
return new DirectoryInfo(root.DirInfo.FullName)
.EnumerateDirectories(packagePrefix + "*", new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive, RecurseSubdirectories = false })
.FirstOrDefault()?
.FullName;
Expand Down Expand Up @@ -434,19 +495,19 @@ private void GenerateSourceFileFromImplicitUsings()
}

// Hardcoded values from https://learn.microsoft.com/en-us/dotnet/core/project-sdk/overview#implicit-using-directives
usings.UnionWith(new[] { "System", "System.Collections.Generic", "System.IO", "System.Linq", "System.Net.Http", "System.Threading",
"System.Threading.Tasks" });
usings.UnionWith([ "System", "System.Collections.Generic", "System.IO", "System.Linq", "System.Net.Http", "System.Threading",
"System.Threading.Tasks" ]);

if (fileContent.UseAspNetCoreDlls)
{
usings.UnionWith(new[] { "System.Net.Http.Json", "Microsoft.AspNetCore.Builder", "Microsoft.AspNetCore.Hosting",
usings.UnionWith([ "System.Net.Http.Json", "Microsoft.AspNetCore.Builder", "Microsoft.AspNetCore.Hosting",
"Microsoft.AspNetCore.Http", "Microsoft.AspNetCore.Routing", "Microsoft.Extensions.Configuration",
"Microsoft.Extensions.DependencyInjection", "Microsoft.Extensions.Hosting", "Microsoft.Extensions.Logging" });
"Microsoft.Extensions.DependencyInjection", "Microsoft.Extensions.Hosting", "Microsoft.Extensions.Logging" ]);
}

if (fileContent.UseWindowsForms)
{
usings.UnionWith(new[] { "System.Drawing", "System.Windows.Forms" });
usings.UnionWith(["System.Drawing", "System.Windows.Forms"]);
}

usings.UnionWith(fileContent.CustomImplicitUsings);
Expand Down Expand Up @@ -828,47 +889,58 @@ private void DownloadMissingPackages(List<FileInfo> allFiles, ISet<string> dllPa

Parallel.ForEach(notYetDownloadedPackages, new ParallelOptions { MaxDegreeOfParallelism = options.Threads }, package =>
{
logger.LogInfo($"Restoring package {package}...");
using var tempDir = new TemporaryDirectory(ComputeTempDirectory(package, "missingpackages_workingdir"));
var success = dotnet.New(tempDir.DirInfo.FullName);
var success = TryRestorePackageManually(package, nugetConfig);
if (!success)
{
return;
}

success = dotnet.AddPackage(tempDir.DirInfo.FullName, package);
if (!success)
lock (sync)
{
return;
successCount++;
}
});

var res = dotnet.Restore(new(tempDir.DirInfo.FullName, missingPackageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: false, PathToNugetConfig: nugetConfig));
if (!res.Success)
{
if (res.HasNugetPackageSourceError)
{
// Restore could not be completed because the listed source is unavailable. Try without the nuget.config:
res = dotnet.Restore(new(tempDir.DirInfo.FullName, missingPackageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: false, PathToNugetConfig: null, ForceReevaluation: true));
}
CompilationInfos.Add(("Successfully ran fallback nuget restore", successCount.ToString()));

// TODO: the restore might fail, we could retry with a prerelease (*-* instead of *) version of the package.
dllPaths.Add(missingPackageDirectory.DirInfo.FullName);
}

if (!res.Success)
{
logger.LogInfo($"Failed to restore nuget package {package}");
return;
}
}
private bool TryRestorePackageManually(string package, string? nugetConfig)
{
logger.LogInfo($"Restoring package {package}...");
using var tempDir = new TemporaryDirectory(ComputeTempDirectory(package, "missingpackages_workingdir"));
var success = dotnet.New(tempDir.DirInfo.FullName);
if (!success)
{
return false;
}

lock (sync)
success = dotnet.AddPackage(tempDir.DirInfo.FullName, package);
if (!success)
{
return false;
}

var res = dotnet.Restore(new(tempDir.DirInfo.FullName, missingPackageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: false, PathToNugetConfig: nugetConfig));
if (!res.Success)
{
if (res.HasNugetPackageSourceError && nugetConfig is not null)
{
successCount++;
// Restore could not be completed because the listed source is unavailable. Try without the nuget.config:
res = dotnet.Restore(new(tempDir.DirInfo.FullName, missingPackageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: false, PathToNugetConfig: null, ForceReevaluation: true));
}
});

CompilationInfos.Add(("Successfully ran fallback nuget restore", successCount.ToString()));
// TODO: the restore might fail, we could retry with a prerelease (*-* instead of *) version of the package.

dllPaths.Add(missingPackageDirectory.DirInfo.FullName);
if (!res.Success)
{
logger.LogInfo($"Failed to restore nuget package {package}");
return false;
}
}

return true;
}

public void Dispose(TemporaryDirectory? dir, string name)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Semmle.Extraction.CSharp.DependencyFetching
{
internal class EnvironmentVariableNames
{
/// <summary>
/// Controls whether to generate source files from Asp.Net Core views (`.cshtml`, `.razor`).
/// </summary>
public const string WebViewGeneration = "CODEQL_EXTRACTOR_CSHARP_BUILDLESS_EXTRACT_WEB_VIEWS";

/// <summary>
/// Specifies the location of .Net framework references added to the compilation.
/// </summary>
public const string DotnetFrameworkReferences = "CODEQL_EXTRACTOR_CSHARP_BUILDLESS_DOTNET_FRAMEWORK_REFERENCES";

/// <summary>
/// Controls whether to use framework dependencies from subfolders.
/// </summary>
public const string DotnetFrameworkReferencesUseSubfolders = "CODEQL_EXTRACTOR_CSHARP_BUILDLESS_DOTNET_FRAMEWORK_REFERENCES_USE_SUBFOLDERS";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ private record class PathFilter(Regex Regex, bool Include);

public IEnumerable<FileInfo> Filter(IEnumerable<FileInfo> files)
{
var filters = (Environment.GetEnvironmentVariable("LGTM_INDEX_FILTERS") ?? string.Empty).Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var filters = (Environment.GetEnvironmentVariable("LGTM_INDEX_FILTERS") ?? string.Empty).Split(FileUtils.NewLineCharacters, StringSplitOptions.RemoveEmptyEntries);
if (filters.Length == 0)
{
return files;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
using System.Collections.Generic;
using System.Linq;

namespace Semmle.Extraction.CSharp.DependencyFetching
{
internal static class FrameworkPackageNames
{
public static string LatestNetFrameworkReferenceAssemblies { get; } = "microsoft.netframework.referenceassemblies.net481";

public static string AspNetCoreFramework { get; } = "microsoft.aspnetcore.app.ref";

public static string WindowsDesktopFramework { get; } = "microsoft.windowsdesktop.app.ref";

// The order of the packages is important.
public static string[] NetFrameworks { get; } = new string[]
{
public static string[] NetFrameworks { get; } =
[
"microsoft.netcore.app.ref", // net7.0, ... net5.0, netcoreapp3.1, netcoreapp3.0
"microsoft.netframework.referenceassemblies.", // net48, ..., net20
"netstandard.library.ref", // netstandard2.1
"netstandard.library" // netstandard2.0
};
];

public static IEnumerable<string> AllFrameworks { get; } =
NetFrameworks
.Union(new string[] { AspNetCoreFramework, WindowsDesktopFramework });
[.. NetFrameworks, AspNetCoreFramework, WindowsDesktopFramework];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public IEnumerable<string> GenerateFiles(IEnumerable<string> cshtmls, IEnumerabl
var args = new StringBuilder();
args.Append($"/target:exe /generatedfilesout:\"{outputFolder}\" /out:\"{dllPath}\" /analyzerconfig:\"{analyzerConfig}\" ");

foreach (var f in Directory.GetFiles(sourceGeneratorFolder, "*.dll"))
foreach (var f in Directory.GetFiles(sourceGeneratorFolder, "*.dll", new EnumerationOptions { RecurseSubdirectories = false, MatchCasing = MatchCasing.CaseInsensitive }))
{
args.Append($"/analyzer:\"{f}\" ");
}
Expand Down
Loading