using System.Collections.Generic; using System.Linq; using Semmle.Util; using Semmle.Util.Logging; namespace Semmle.Autobuild.Shared { internal static class MsBuildCommandExtensions { /// /// Appends a call to msbuild. /// /// public static CommandBuilder MsBuildCommand(this CommandBuilder cmdBuilder, IAutobuilder builder, bool preferDotnet) { // mono doesn't ship with `msbuild` on Arm-based Macs, but we can fall back to // msbuild that ships with `dotnet` which can be invoked with `dotnet msbuild` // perhaps we should do this on all platforms? // Similarly, there's no point in trying to rely on mono if it's not installed. // In which case we can still fall back to `dotnet msbuild`. return preferDotnet ? cmdBuilder.RunCommand("dotnet").Argument("msbuild") : cmdBuilder.RunCommand("msbuild"); } } /// /// A build rule using msbuild. /// public class MsBuildRule : IBuildRule { /// /// A list of solutions or projects which failed to build. /// public List FailedProjectsOrSolutions { get; } = []; public BuildScript Analyse(IAutobuilder builder, bool auto) { if (!builder.ProjectsOrSolutionsToBuild.Any()) return BuildScript.Failure; if (auto) builder.Logger.LogInfo("Attempting to build using MSBuild"); VcVarsBatFile? vsTools = null; if (builder.ProjectsOrSolutionsToBuild.Any()) { var firstSolution = builder.ProjectsOrSolutionsToBuild.OfType().FirstOrDefault(); vsTools = firstSolution is not null ? BuildTools.FindCompatibleVcVars(builder.Actions, firstSolution) : BuildTools.VcVarsAllBatFiles(builder.Actions).OrderByDescending(b => b.ToolsVersion).FirstOrDefault(); } if (vsTools is null && builder.Actions.IsWindows()) { builder.Logger.LogWarning("Could not find a suitable version of VsDevCmd.bat/vcvarsall.bat"); } // Use `nuget.exe` from source code repo, if present, otherwise first attempt with global // `nuget` command, and if that fails, attempt to download `nuget.exe` from nuget.org var nuget = builder.GetFilename("nuget.exe").Select(t => t.Item1).FirstOrDefault() ?? "nuget"; var nugetDownloadPath = builder.Actions.PathCombine(FileUtils.GetTemporaryWorkingDirectory(builder.Actions.GetEnvironmentVariable, builder.Options.Language.UpperCaseName, out _), ".nuget", "nuget.exe"); var nugetDownloaded = false; var ret = BuildScript.Success; foreach (var projectOrSolution in builder.ProjectsOrSolutionsToBuild) { BuildScript GetNugetRestoreScript() => new CommandBuilder(builder.Actions). RunCommand(nuget). Argument("restore"). QuoteArgument(projectOrSolution.FullPath). Argument("-DisableParallelProcessing"). Script; var preferDotnet = builder.Actions.IsRunningOnAppleSilicon() || !builder.Actions.IsWindows() && !builder.Actions.IsMonoInstalled(); var nugetRestore = GetNugetRestoreScript(); var msbuildRestoreCommand = new CommandBuilder(builder.Actions). MsBuildCommand(builder, preferDotnet). Argument("/t:restore"). QuoteArgument(projectOrSolution.FullPath); if (preferDotnet) { // On Apple Silicon, only try package restore with `dotnet msbuild /t:restore` ret &= BuildScript.Try(msbuildRestoreCommand.Script); } else if (nugetDownloaded) { ret &= BuildScript.Try(nugetRestore | msbuildRestoreCommand.Script); } else { // If `nuget restore` fails, and we have not already attempted to download `nuget.exe`, // download it and reattempt `nuget restore`. var nugetDownloadAndRestore = BuildScript.Bind(DownloadNugetExe(builder, nugetDownloadPath), exitCode => { nugetDownloaded = true; if (exitCode != 0) return BuildScript.Failure; nuget = nugetDownloadPath; return GetNugetRestoreScript(); }); ret &= BuildScript.Try(nugetRestore | nugetDownloadAndRestore | msbuildRestoreCommand.Script); } var command = new CommandBuilder(builder.Actions); if (vsTools is not null) { command.CallBatFile(vsTools.Path); // `vcvarsall.bat` sets a default Platform environment variable, // which may not be compatible with the supported platforms of the // given project/solution. Unsetting it means that the default platform // of the project/solution is used instead. command.RunCommand("set Platform=&& type NUL", quoteExe: false); } command.MsBuildCommand(builder, preferDotnet); command.QuoteArgument(projectOrSolution.FullPath); var target = "rebuild"; var platform = projectOrSolution is ISolution s1 ? s1.DefaultPlatformName : null; var configuration = projectOrSolution is ISolution s2 ? s2.DefaultConfigurationName : null; command.Argument($"/t:{target}"); if (platform is not null) command.Argument($"/p:Platform=\"{platform}\""); if (configuration is not null) command.Argument($"/p:Configuration=\"{configuration}\""); // append the build script which invokes msbuild to the overall build script `ret`; // we insert a check that building the current project or solution was successful: // if it was not successful, we add it to `FailedProjectsOrSolutions` ret &= BuildScript.OnFailure(command.Script, ret => { FailedProjectsOrSolutions.Add(projectOrSolution); }); } return ret; } /// /// Returns a script for downloading `nuget.exe` from nuget.org. /// private static BuildScript DownloadNugetExe(IAutobuilder builder, string path) where TAutobuildOptions : AutobuildOptionsShared => BuildScript.Create(_ => { builder.Logger.LogInfo("Attempting to download nuget.exe"); return 0; }) & BuildScript.DownloadFile( FileUtils.NugetExeUrl, path, e => builder.Logger.LogWarning($"Failed to download 'nuget.exe': {e.Message}"), builder.Logger) & BuildScript.Create(_ => { builder.Logger.LogInfo($"Successfully downloaded {path}"); return 0; }); } }