mirror of
https://github.com/github/codeql.git
synced 2025-12-16 16:53:25 +01:00
402 lines
17 KiB
C#
402 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
using Semmle.Util;
|
|
using Semmle.Util.Logging;
|
|
|
|
namespace Semmle.Extraction.CSharp.DependencyFetching
|
|
{
|
|
/// <summary>
|
|
/// Utilities to run the "dotnet" command.
|
|
/// </summary>
|
|
public partial class DotNet : IDotNet
|
|
{
|
|
private readonly IDotNetCliInvoker dotnetCliInvoker;
|
|
private readonly ILogger logger;
|
|
private readonly TemporaryDirectory? tempWorkingDirectory;
|
|
|
|
private DotNet(IDotNetCliInvoker dotnetCliInvoker, ILogger logger, bool runDotnetInfo, TemporaryDirectory? tempWorkingDirectory = null)
|
|
{
|
|
this.tempWorkingDirectory = tempWorkingDirectory;
|
|
this.dotnetCliInvoker = dotnetCliInvoker;
|
|
this.logger = logger;
|
|
if (runDotnetInfo)
|
|
{
|
|
Info();
|
|
}
|
|
}
|
|
|
|
private DotNet(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory, DependabotProxy? dependabotProxy) : this(new DotNetCliInvoker(logger, Path.Combine(dotNetPath ?? string.Empty, "dotnet"), dependabotProxy), logger, dotNetPath is null, tempWorkingDirectory) { }
|
|
|
|
internal static IDotNet Make(IDotNetCliInvoker dotnetCliInvoker, ILogger logger, bool runDotnetInfo) => new DotNet(dotnetCliInvoker, logger, runDotnetInfo);
|
|
|
|
public static IDotNet Make(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory, DependabotProxy? dependabotProxy) => new DotNet(logger, dotNetPath, tempWorkingDirectory, dependabotProxy);
|
|
|
|
private static void HandleRetryExitCode143(string dotnet, int attempt, ILogger logger)
|
|
{
|
|
logger.LogWarning($"Running '{dotnet} --info' failed with exit code 143. Retrying...");
|
|
var sleep = Math.Pow(2, attempt) * 1000;
|
|
Thread.Sleep((int)sleep);
|
|
}
|
|
|
|
private void Info()
|
|
{
|
|
// Allow up to four attempts (with up to three retries) to run `dotnet --info`, to mitigate transient issues
|
|
for (int attempt = 0; attempt < 4; attempt++)
|
|
{
|
|
var exitCode = dotnetCliInvoker.RunCommandExitCode("--info", silent: false);
|
|
switch (exitCode)
|
|
{
|
|
case 0:
|
|
return;
|
|
case 143 when attempt < 3:
|
|
HandleRetryExitCode143(dotnetCliInvoker.Exec, attempt, logger);
|
|
continue;
|
|
default:
|
|
throw new Exception($"{dotnetCliInvoker.Exec} --info failed with exit code {exitCode}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private string GetRestoreArgs(RestoreSettings restoreSettings)
|
|
{
|
|
var args = $"restore --no-dependencies \"{restoreSettings.File}\" --packages \"{restoreSettings.PackageDirectory}\" /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal";
|
|
|
|
if (restoreSettings.ForceDotnetRefAssemblyFetching)
|
|
{
|
|
// Ugly hack: we set the TargetFrameworkRootPath and NetCoreTargetingPackRoot properties to an empty folder:
|
|
var path = ".empty";
|
|
if (tempWorkingDirectory != null)
|
|
{
|
|
path = Path.Combine(tempWorkingDirectory.ToString(), "emptyFakeDotnetRoot");
|
|
Directory.CreateDirectory(path);
|
|
}
|
|
|
|
args += $" /p:TargetFrameworkRootPath=\"{path}\" /p:NetCoreTargetingPackRoot=\"{path}\"";
|
|
}
|
|
|
|
if (restoreSettings.PathToNugetConfig != null)
|
|
{
|
|
args += $" --configfile \"{restoreSettings.PathToNugetConfig}\"";
|
|
}
|
|
|
|
if (restoreSettings.ForceReevaluation)
|
|
{
|
|
args += " --force";
|
|
}
|
|
|
|
if (restoreSettings.TargetWindows)
|
|
{
|
|
args += " /p:EnableWindowsTargeting=true";
|
|
}
|
|
|
|
if (restoreSettings.ExtraArgs is not null)
|
|
{
|
|
args += $" {restoreSettings.ExtraArgs}";
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
public RestoreResult Restore(RestoreSettings restoreSettings)
|
|
{
|
|
var args = GetRestoreArgs(restoreSettings);
|
|
var success = dotnetCliInvoker.RunCommand(args, out var output);
|
|
return new(success, output);
|
|
}
|
|
|
|
public bool New(string folder)
|
|
{
|
|
var args = $"new console --no-restore --output \"{folder}\"";
|
|
return dotnetCliInvoker.RunCommand(args);
|
|
}
|
|
|
|
public bool AddPackage(string folder, string package)
|
|
{
|
|
var args = $"add \"{folder}\" package \"{package}\" --no-restore";
|
|
return dotnetCliInvoker.RunCommand(args);
|
|
}
|
|
|
|
public IList<string> GetListedRuntimes() => GetResultList("--list-runtimes");
|
|
|
|
public IList<string> GetListedSdks() => GetResultList("--list-sdks");
|
|
|
|
private IList<string> GetResultList(string args, string? workingDirectory = null, bool silent = true)
|
|
{
|
|
if (dotnetCliInvoker.RunCommand(args, workingDirectory, out var results, silent))
|
|
{
|
|
return results;
|
|
}
|
|
logger.LogWarning($"Running 'dotnet {args}' failed.");
|
|
return [];
|
|
}
|
|
|
|
public bool Exec(string execArgs)
|
|
{
|
|
var args = $"exec {execArgs}";
|
|
return dotnetCliInvoker.RunCommand(args);
|
|
}
|
|
|
|
private const string nugetListSourceCommand = "nuget list source --format Short";
|
|
|
|
public IList<string> GetNugetFeeds(string nugetConfig)
|
|
{
|
|
logger.LogInfo($"Getting NuGet feeds from '{nugetConfig}'...");
|
|
return GetResultList($"{nugetListSourceCommand} --configfile \"{nugetConfig}\"");
|
|
}
|
|
|
|
public IList<string> GetNugetFeedsFromFolder(string folderPath)
|
|
{
|
|
logger.LogInfo($"Getting NuGet feeds in folder '{folderPath}'...");
|
|
return GetResultList(nugetListSourceCommand, folderPath);
|
|
}
|
|
|
|
// The version number should be kept in sync with the version .NET version used for building the application.
|
|
public const string LatestDotNetSdkVersion = "9.0.300";
|
|
|
|
public static ReadOnlyDictionary<string, string> MinimalEnvironment => IDotNetCliInvoker.MinimalEnvironment;
|
|
|
|
/// <summary>
|
|
/// Returns a script for downloading relevant versions of the
|
|
/// .NET SDK. The SDK(s) will be installed at <code>installDir</code>
|
|
/// (provided that the script succeeds).
|
|
/// </summary>
|
|
private static BuildScript DownloadDotNet(IBuildActions actions, ILogger logger, IEnumerable<string> files, string tempWorkingDirectory, bool shouldCleanUp, string installDir, string? version, bool ensureDotNetAvailable)
|
|
{
|
|
if (!string.IsNullOrEmpty(version))
|
|
// Specific version requested
|
|
return DownloadDotNetVersion(actions, logger, tempWorkingDirectory, shouldCleanUp, installDir, [version]);
|
|
|
|
// Download versions mentioned in `global.json` files
|
|
// See https://docs.microsoft.com/en-us/dotnet/core/tools/global-json
|
|
var versions = new List<string>();
|
|
|
|
foreach (var path in files.Where(p => string.Equals(FileUtils.SafeGetFileName(p, logger), "global.json", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
try
|
|
{
|
|
var o = JObject.Parse(File.ReadAllText(path));
|
|
var v = (string?)o?["sdk"]?["version"];
|
|
if (v is not null)
|
|
{
|
|
versions.Add(v);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// not a valid `global.json` file
|
|
logger.LogInfo($"Couldn't find .NET SDK version in '{path}'.");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (versions.Count > 0)
|
|
{
|
|
return
|
|
DownloadDotNetVersion(actions, logger, tempWorkingDirectory, shouldCleanUp, installDir, versions) |
|
|
// if neither of the versions succeed, try the latest version
|
|
DownloadDotNetVersion(actions, logger, tempWorkingDirectory, shouldCleanUp, installDir, [LatestDotNetSdkVersion], needExactVersion: false);
|
|
}
|
|
|
|
if (ensureDotNetAvailable)
|
|
{
|
|
return DownloadDotNetVersion(actions, logger, tempWorkingDirectory, shouldCleanUp, installDir, [LatestDotNetSdkVersion], needExactVersion: false);
|
|
}
|
|
|
|
return BuildScript.Failure;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a script for running `dotnet --info`, with retries on exit code 143.
|
|
/// </summary>
|
|
public static BuildScript InfoScript(IBuildActions actions, string dotnet, IDictionary<string, string>? environment, ILogger logger)
|
|
{
|
|
var info = new CommandBuilder(actions, null, environment).
|
|
RunCommand(dotnet).
|
|
Argument("--info");
|
|
var script = info.Script;
|
|
for (var attempt = 0; attempt < 4; attempt++)
|
|
{
|
|
var attemptCopy = attempt; // Capture in local variable
|
|
script = BuildScript.Bind(script, ret =>
|
|
{
|
|
switch (ret)
|
|
{
|
|
case 0:
|
|
return BuildScript.Success;
|
|
case 143 when attemptCopy < 3:
|
|
HandleRetryExitCode143(dotnet, attemptCopy, logger);
|
|
return info.Script;
|
|
default:
|
|
return BuildScript.Failure;
|
|
}
|
|
});
|
|
}
|
|
return script;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a script for downloading specific .NET SDK versions, if the
|
|
/// versions are not already installed.
|
|
///
|
|
/// See https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script.
|
|
/// </summary>
|
|
private static BuildScript DownloadDotNetVersion(IBuildActions actions, ILogger logger, string tempWorkingDirectory, bool shouldCleanUp, string path, IEnumerable<string> versions, bool needExactVersion = true)
|
|
{
|
|
if (!versions.Any())
|
|
{
|
|
logger.LogInfo("No .NET SDK versions requested.");
|
|
return BuildScript.Failure;
|
|
}
|
|
|
|
return BuildScript.Bind(GetInstalledSdksScript(actions), (sdks, sdksRet) =>
|
|
{
|
|
if (
|
|
needExactVersion &&
|
|
sdksRet == 0 &&
|
|
// quadratic; should be OK, given that both `version` and `sdks` are expected to be small
|
|
versions.All(version => sdks.Any(sdk => sdk.StartsWith(version + " ", StringComparison.Ordinal))))
|
|
{
|
|
// The requested SDKs are already installed, so no need to reinstall
|
|
return BuildScript.Failure;
|
|
}
|
|
else if (!needExactVersion && sdksRet == 0 && sdks.Count > 0)
|
|
{
|
|
// there's at least one SDK installed, so no need to reinstall
|
|
return BuildScript.Failure;
|
|
}
|
|
else if (!needExactVersion && sdksRet != 0)
|
|
{
|
|
logger.LogInfo("No .NET SDK found.");
|
|
}
|
|
|
|
BuildScript prelude;
|
|
BuildScript postlude;
|
|
Func<string, BuildScript> getInstall;
|
|
|
|
if (actions.IsWindows())
|
|
{
|
|
prelude = BuildScript.Success;
|
|
postlude = BuildScript.Success;
|
|
|
|
getInstall = version =>
|
|
{
|
|
var psCommand = $"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; &([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Version {version} -InstallDir {path}";
|
|
|
|
BuildScript GetInstall(string pwsh) =>
|
|
new CommandBuilder(actions).
|
|
RunCommand(pwsh).
|
|
Argument("-NoProfile").
|
|
Argument("-ExecutionPolicy").
|
|
Argument("unrestricted").
|
|
Argument("-Command").
|
|
Argument($"\"{psCommand}\"").
|
|
Script;
|
|
|
|
return GetInstall("pwsh") | GetInstall("powershell");
|
|
};
|
|
}
|
|
else
|
|
{
|
|
var dotnetInstallPath = actions.PathCombine(tempWorkingDirectory, ".dotnet", "dotnet-install.sh");
|
|
var downloadDotNetInstallSh = BuildScript.DownloadFile(
|
|
"https://dot.net/v1/dotnet-install.sh",
|
|
dotnetInstallPath,
|
|
e => logger.LogWarning($"Failed to download 'dotnet-install.sh': {e.Message}"),
|
|
logger);
|
|
|
|
var chmod = new CommandBuilder(actions).
|
|
RunCommand("chmod").
|
|
Argument("u+x").
|
|
Argument(dotnetInstallPath);
|
|
|
|
prelude = downloadDotNetInstallSh & chmod.Script;
|
|
postlude = shouldCleanUp ? BuildScript.DeleteFile(dotnetInstallPath) : BuildScript.Success;
|
|
|
|
getInstall = version =>
|
|
{
|
|
var cb = new CommandBuilder(actions).
|
|
RunCommand(dotnetInstallPath).
|
|
Argument("--channel").
|
|
Argument("release").
|
|
Argument("--version").
|
|
Argument(version);
|
|
|
|
// Request ARM64 architecture on Apple Silicon machines
|
|
if (actions.IsRunningOnAppleSilicon())
|
|
{
|
|
cb.Argument("--architecture").
|
|
Argument("arm64");
|
|
}
|
|
|
|
return cb.Argument("--install-dir").
|
|
Argument(path).Script;
|
|
};
|
|
}
|
|
|
|
var dotnetInfo = InfoScript(actions, actions.PathCombine(path, "dotnet"), MinimalEnvironment.ToDictionary(), logger);
|
|
|
|
Func<string, BuildScript> getInstallAndVerify = version =>
|
|
// run `dotnet --info` after install, to check that it executes successfully
|
|
getInstall(version) & dotnetInfo;
|
|
|
|
var installScript = prelude & BuildScript.Failure;
|
|
|
|
var attempted = new HashSet<string>();
|
|
foreach (var version in versions)
|
|
{
|
|
if (!attempted.Add(version))
|
|
continue;
|
|
|
|
installScript = BuildScript.Bind(installScript, combinedExit =>
|
|
{
|
|
logger.LogInfo($"Attempting to download .NET {version}");
|
|
|
|
// When there are multiple versions requested, we want to try to fetch them all, reporting
|
|
// a successful exit code when at least one of them succeeds
|
|
return combinedExit != 0 ? getInstallAndVerify(version) : BuildScript.Bind(getInstallAndVerify(version), _ => BuildScript.Success);
|
|
});
|
|
}
|
|
|
|
return installScript & postlude;
|
|
});
|
|
}
|
|
|
|
private static BuildScript GetInstalledSdksScript(IBuildActions actions)
|
|
{
|
|
var listSdks = new CommandBuilder(actions, silent: true, environment: MinimalEnvironment).
|
|
RunCommand("dotnet").
|
|
Argument("--list-sdks");
|
|
return listSdks.Script;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a script that attempts to download relevant version(s) of the
|
|
/// .NET SDK, followed by running the script generated by <paramref name="f"/>.
|
|
///
|
|
/// The argument to <paramref name="f"/> is the path to the directory in which the
|
|
/// .NET SDK(s) were installed.
|
|
/// </summary>
|
|
public static BuildScript WithDotNet(IBuildActions actions, ILogger logger, IEnumerable<string> files, string tempWorkingDirectory, bool shouldCleanUp, bool ensureDotNetAvailable, string? version, Func<string?, BuildScript> f)
|
|
{
|
|
var installDir = actions.PathCombine(tempWorkingDirectory, ".dotnet");
|
|
var installScript = DownloadDotNet(actions, logger, files, tempWorkingDirectory, shouldCleanUp, installDir, version, ensureDotNetAvailable);
|
|
return BuildScript.Bind(installScript, installed =>
|
|
{
|
|
if (installed != 0)
|
|
{
|
|
// The .NET SDK was not installed, either because the installation failed or because it was already installed.
|
|
installDir = null;
|
|
}
|
|
|
|
return f(installDir);
|
|
});
|
|
}
|
|
}
|
|
}
|