/// <summary> /// Adds the standard .NET targets to the build. /// </summary> /// <param name="build">The build to which to add targets.</param> /// <param name="settings">The build settings.</param> public static void AddDotNetTargets(this BuildApp build, DotNetBuildSettings?settings = null) { settings ??= new DotNetBuildSettings(); var buildOptions = settings.BuildOptions ?? (settings.BuildOptions = new DotNetBuildOptions()); var configurationOption = buildOptions.ConfigurationOption ?? (buildOptions.ConfigurationOption = build.AddOption("-c|--configuration <name>", "The configuration to build (default Release)", "Release")); var platformOption = buildOptions.PlatformOption ?? (buildOptions.PlatformOption = build.AddOption("-p|--platform <name>", "The solution platform to build")); var verbosityOption = buildOptions.VerbosityOption ?? (buildOptions.VerbosityOption = build.AddOption("-v|--verbosity <level>", "The build verbosity (q[uiet], m[inimal], n[ormal], d[etailed])")); var versionSuffixOption = buildOptions.VersionSuffixOption ?? (buildOptions.VersionSuffixOption = build.AddOption("--version-suffix <suffix>", "Generates a prerelease package")); var nugetOutputOption = buildOptions.NuGetOutputOption ?? (buildOptions.NuGetOutputOption = build.AddOption("--nuget-output <path>", "Directory for generated package (default release)", "release")); var triggerOption = buildOptions.TriggerOption ?? (buildOptions.TriggerOption = build.AddOption("--trigger <name>", "The git branch or tag that triggered the build")); var buildNumberOption = buildOptions.BuildNumberOption ?? (buildOptions.BuildNumberOption = build.AddOption("--build-number <number>", "The automated build number")); var noTestFlag = buildOptions.NoTestFlag ?? (buildOptions.NoTestFlag = build.AddFlag("--no-test", "Skip the unit tests")); var solutionName = settings.SolutionName; var nugetSource = settings.NuGetSource ?? "https://api.nuget.org/v3/index.json"; var msbuildSettings = settings.MSBuildSettings; var packagePaths = new List <string>(); string?trigger = null; var ignoreIfAlreadyPushed = false; build.Target("clean") .Describe("Deletes all build output") .Does(() => { var findDirectoriesToDelete = settings.CleanSettings?.FindDirectoriesToDelete ?? (() => FindDirectories("{src,tests,tools}/**/{bin,obj}").Except(FindDirectories("tools/bin")).ToList()); foreach (var directoryToDelete in findDirectoriesToDelete()) { DeleteDirectory(directoryToDelete); } var verbosity = GetVerbosity(); var extraProperties = GetExtraProperties("clean"); if (msbuildSettings == null) { RunDotNet(new[] { "clean", solutionName, "-c", configurationOption.Value, GetPlatformArg(), "--verbosity", verbosity, GetMaxCpuCountArg() }.Concat(extraProperties)); } else { MSBuild(new[] { solutionName, "-t:Clean", $"-p:Configuration={configurationOption.Value}", GetPlatformArg(), $"-v:{verbosity}", GetMaxCpuCountArg() }.Concat(extraProperties)); } }); build.Target("restore") .Describe("Restores NuGet packages") .Does(() => { var verbosity = GetVerbosity(); var extraProperties = GetExtraProperties("restore"); if (msbuildSettings == null) { RunDotNet(new[] { "restore", solutionName, GetPlatformArg(), "--verbosity", verbosity, GetMaxCpuCountArg() }.Concat(extraProperties)); } else { MSBuild(new[] { solutionName, "-t:Restore", $"-p:Configuration={configurationOption.Value}", GetPlatformArg(), $"-v:{verbosity}", GetMaxCpuCountArg() }.Concat(extraProperties)); } if (DotNetLocalTool.Any()) { RunDotNet("tool", "restore"); } }); build.Target("build") .DependsOn("restore") .Describe("Builds the solution") .Does(() => { var buildNumberArg = GetBuildNumberArg(); var verbosity = GetVerbosity(); var extraProperties = GetExtraProperties("build"); if (msbuildSettings == null) { RunDotNet(new[] { "build", solutionName, "-c", configurationOption.Value, GetPlatformArg(), buildNumberArg, "--no-restore", "--verbosity", verbosity, GetMaxCpuCountArg() }.Concat(extraProperties)); } else { MSBuild(new[] { solutionName, $"-p:Configuration={configurationOption.Value}", GetPlatformArg(), buildNumberArg, $"-v:{verbosity}", GetMaxCpuCountArg() }.Concat(extraProperties)); } }); build.Target("test") .DependsOn("build") .Describe("Runs the unit tests") .Does(() => { if (noTestFlag.Value) { Console.WriteLine("Skipping unit tests due to --no-test."); } else { var extraProperties = GetExtraProperties("test").ToList(); var findTestAssemblies = settings.TestSettings?.FindTestAssemblies; if (findTestAssemblies != null) { foreach (var testAssembly in findTestAssemblies()) { if (settings.TestSettings?.RunTests != null) { settings.TestSettings.RunTests(testAssembly); } else { RunDotNet(new AppRunnerSettings { Arguments = new[] { "vstest", Path.GetFileName(testAssembly) }.Concat(extraProperties), WorkingDirectory = Path.GetDirectoryName(testAssembly) }); } } } else { var testProjects = new List <string?>(); var findTestProjects = settings.TestSettings?.FindProjects; if (findTestProjects != null) { testProjects.AddRange(findTestProjects()); } else { testProjects.Add(solutionName); } foreach (var testProject in testProjects) { if (settings.TestSettings?.RunTests != null) { settings.TestSettings.RunTests(testProject); } else { RunDotNet(new[] { "test", testProject, "-c", configurationOption.Value, GetPlatformArg(), "--no-build", GetMaxCpuCountArg() }.Concat(extraProperties)); } } } } }); build.Target("package") .DependsOn("clean", "test") .Describe("Builds NuGet packages") .Does(() => { trigger = triggerOption.Value; if (trigger == "detect") { using var repository = new Repository("."); var headSha = repository.Head.Tip.Sha; var autoTrigger = GetBestTriggerFromTags(repository.Tags.Where(x => x.Target.Sha == headSha).Select(x => x.FriendlyName).ToList()); if (autoTrigger != null) { trigger = autoTrigger; ignoreIfAlreadyPushed = true; Console.WriteLine($"Detected trigger: {trigger}"); } } var versionSuffix = versionSuffixOption.Value; if (versionSuffix == null && trigger != null) { versionSuffix = GetVersionFromTrigger(trigger) is string triggerVersion ? SplitVersion(triggerVersion).Suffix : null; } var nugetOutputPath = Path.GetFullPath(nugetOutputOption.Value !); var tempOutputPath = Path.Combine(nugetOutputPath, Path.GetRandomFileName()); var packageProjects = new List <string?>(); var findPackageProjects = settings.PackageSettings?.FindProjects; if (findPackageProjects != null) { packageProjects.AddRange(findPackageProjects()); } else { packageProjects.Add(solutionName); } var extraProperties = GetExtraProperties("package").ToList(); foreach (var packageProject in packageProjects) { if (msbuildSettings == null) { RunDotNet(new[] { "pack", packageProject, "-c", configurationOption.Value, GetPlatformArg(), "--no-build", "--output", tempOutputPath, versionSuffix != null ? "--version-suffix" : null, versionSuffix, GetMaxCpuCountArg(), }.Concat(extraProperties)); } else { MSBuild(new[] { packageProject, "-t:Pack", $"-p:Configuration={configurationOption.Value}", GetPlatformArg(), "-p:NoBuild=true", $"-p:PackageOutputPath={tempOutputPath}", versionSuffix != null ? $"-p:VersionSuffix={versionSuffix}" : null, $"-v:{GetVerbosity()}", GetMaxCpuCountArg(), }.Concat(extraProperties)); } } var tempPackagePaths = FindFilesFrom(tempOutputPath, "*.nupkg"); foreach (var tempPackagePath in tempPackagePaths) { var packagePath = Path.Combine(nugetOutputPath, Path.GetFileName(tempPackagePath) ?? throw new InvalidOperationException()); if (File.Exists(packagePath)) { File.Delete(packagePath); } File.Move(tempPackagePath, packagePath); packagePaths.Add(packagePath); Console.WriteLine($"NuGet package: {packagePath}"); } DeleteDirectory(tempOutputPath); if (packagePaths.Count == 0) { throw new BuildException("No NuGet packages created."); } }); build.Target("publish") .Describe("Publishes NuGet packages and documentation") .DependsOn("package") .Does(() => { if (packagePaths.Count == 0) { throw new BuildException("No NuGet packages found."); } if (trigger is null) { if (packagePaths.Any(x => GetPackageInfo(x).Version == "0.0.0")) { Console.WriteLine("Not publishing package with version 0.0.0. Change package version to publish."); return; } trigger = "publish-all"; } var triggerParts = trigger.Split('-'); var publishTrigger = triggerParts.Length >= 2 && triggerParts[0] == "publish" ? triggerParts[1] : null; var shouldPublishPackages = publishTrigger == "package" || publishTrigger == "packages" || publishTrigger == "all"; var shouldPublishDocs = publishTrigger == "docs" || publishTrigger == "all"; var shouldSkipDuplicates = publishTrigger == "all"; var triggerVersion = GetVersionFromTrigger(trigger); if (triggerVersion != null) { var mismatches = packagePaths.Where(x => GetPackageInfo(x).Version != triggerVersion).ToList(); if (mismatches.Count != 0) { throw new BuildException($"Trigger '{trigger}' doesn't match package version: {string.Join(", ", mismatches.Select(Path.GetFileName))}"); } shouldPublishPackages = true; shouldPublishDocs = triggerVersion.IndexOf('-') == -1; } if (shouldPublishPackages || shouldPublishDocs) { var docsSettings = settings.DocsSettings; var shouldPushDocs = false; string?cloneDirectory = null; string?repoDirectory = null; string?gitBranchName = null; if (shouldPublishDocs && docsSettings != null) { if (docsSettings.GitLogin == null || docsSettings.GitAuthor == null) { throw new BuildException("GitLogin and GitAuthor must be set on DocsSettings."); } var gitRepositoryUrl = docsSettings.GitRepositoryUrl; gitBranchName = docsSettings.GitBranchName; if (gitRepositoryUrl != null) { cloneDirectory = "docs_repo_" + Path.GetRandomFileName(); Repository.Clone(sourceUrl: gitRepositoryUrl, workdirPath: cloneDirectory, options: new CloneOptions { BranchName = gitBranchName, CredentialsProvider = ProvideCredentials }); repoDirectory = cloneDirectory; } else { repoDirectory = "."; } using var repository = new Repository(repoDirectory); if (gitRepositoryUrl != null) { gitBranchName ??= repository.Head.FriendlyName; } else if (gitBranchName != null) { if (gitBranchName != repository.Head.FriendlyName) { var branch = repository.Branches[gitBranchName] ?? repository.CreateBranch(gitBranchName); Commands.Checkout(repository, branch); } } else { var branch = repository.Branches.FirstOrDefault(x => x.IsCurrentRepositoryHead); if (branch == null) { var autoBranchName = Environment.GetEnvironmentVariable("APPVEYOR_REPO_BRANCH"); if (autoBranchName == null) { var gitRef = Environment.GetEnvironmentVariable("GITHUB_REF"); const string prefix = "refs/heads/"; if (gitRef != null && gitRef.StartsWith(prefix, StringComparison.Ordinal)) { autoBranchName = gitRef.Substring(prefix.Length); } } if (autoBranchName != null) { branch = repository.Branches[autoBranchName] ?? repository.CreateBranch(autoBranchName); } else { branch = repository.Branches.FirstOrDefault(x => x.Tip.Sha == repository.Head.Tip.Sha); } if (branch != null) { Commands.Checkout(repository, branch); } } if (branch == null) { throw new BuildException("Could not determine repository branch for publishing docs."); } gitBranchName = branch.FriendlyName; } var docsPath = Path.Combine(repoDirectory, docsSettings.TargetDirectory ?? "docs"); string?xmlDocGenPath = null; var xmlDocGenProject = FindFiles("tools/XmlDocGen/XmlDocGen.csproj").FirstOrDefault(); if (xmlDocGenProject != null) { RunDotNet("publish", xmlDocGenProject, "--output", Path.Combine("tools", "bin", "XmlDocGen"), "--nologo", "--verbosity", "quiet"); xmlDocGenPath = Path.Combine("tools", "bin", "XmlDocGen", "XmlDocGen.dll"); } var projectHasDocs = docsSettings.ProjectHasDocs ?? (_ => true); foreach (var projectName in packagePaths.Select(x => GetPackageInfo(x).Name).Where(projectHasDocs)) { if (xmlDocGenPath != null) { RunDotNet(new[] { xmlDocGenPath }.Concat(GetXmlDocArgs(projectName))); } else { var assemblyPaths = new List <string>(); if (docsSettings.FindAssemblies != null) { assemblyPaths.AddRange(docsSettings.FindAssemblies(projectName)); } else { var assemblyPath = (docsSettings.FindAssembly ?? FindAssembly)(projectName); if (assemblyPath != null) { assemblyPaths.Add(assemblyPath); } } if (assemblyPaths.Count != 0) { if (DotNetLocalTool.TryCreate("xmldocmd") is DotNetLocalTool xmldocmd) { foreach (var assemblyPath in assemblyPaths) { xmldocmd.Run(GetXmlDocArgs(assemblyPath)); } } else { var dotNetTools = settings.DotNetTools ?? new DotNetTools(Path.Combine("tools", "bin")); var xmlDocMarkdownVersion = settings.DocsSettings?.ToolVersion ?? "2.0.1"; foreach (var assemblyPath in assemblyPaths) { RunApp(dotNetTools.GetToolPath($"xmldocmd/{xmlDocMarkdownVersion}"), GetXmlDocArgs(assemblyPath)); } } } else { Console.WriteLine($"Documentation not generated for {projectName}; assembly not found."); }
/// <summary> /// Adds the standard .NET targets to the build. /// </summary> /// <param name="build">The build to which to add targets.</param> /// <param name="settings">The build settings.</param> public static void AddDotNetTargets(this BuildApp build, DotNetBuildSettings?settings = null) { settings ??= new DotNetBuildSettings(); var buildOptions = settings.BuildOptions ?? (settings.BuildOptions = new DotNetBuildOptions()); var configurationOption = buildOptions.ConfigurationOption ?? (buildOptions.ConfigurationOption = build.AddOption("-c|--configuration <name>", "The configuration to build (default Release)", "Release")); var platformOption = buildOptions.PlatformOption ?? (buildOptions.PlatformOption = build.AddOption("-p|--platform <name>", "The solution platform to build")); var versionSuffixOption = buildOptions.VersionSuffixOption ?? (buildOptions.VersionSuffixOption = build.AddOption("--version-suffix <suffix>", "Generates a prerelease package")); var nugetOutputOption = buildOptions.NuGetOutputOption ?? (buildOptions.NuGetOutputOption = build.AddOption("--nuget-output <path>", "Directory for generated package (default release)", "release")); var triggerOption = buildOptions.TriggerOption ?? (buildOptions.TriggerOption = build.AddOption("--trigger <name>", "The git branch or tag that triggered the build")); var buildNumberOption = buildOptions.BuildNumberOption ?? (buildOptions.BuildNumberOption = build.AddOption("--build-number <number>", "The automated build number")); var noTestFlag = buildOptions.NoTestFlag ?? (buildOptions.NoTestFlag = build.AddFlag("--no-test", "Skip the unit tests")); var solutionName = settings.SolutionName; var nugetSource = settings.NuGetSource ?? "https://api.nuget.org/v3/index.json"; var msbuildSettings = settings.MSBuildSettings; var dotNetTools = settings.DotNetTools ?? new DotNetTools(Path.Combine("tools", "bin")); var xmlDocMarkdownVersion = settings.DocsSettings?.ToolVersion ?? "2.0.1"; var packagePaths = new List <string>(); string?trigger = null; var ignoreIfAlreadyPushed = false; build.Target("clean") .Describe("Deletes all build output") .Does(() => { var findDirectoriesToDelete = settings.CleanSettings?.FindDirectoriesToDelete ?? (() => FindDirectories("{src,tests}/**/{bin,obj}", "tools/XmlDocTarget/{bin,obj}")); foreach (var directoryToDelete in findDirectoriesToDelete()) { deleteDirectory(directoryToDelete); } var extraProperties = getExtraProperties("clean"); if (msbuildSettings == null) { RunDotNet(new[] { "clean", solutionName, "-c", configurationOption.Value, getPlatformArg(), "--verbosity", "normal", getMaxCpuCountArg() }.Concat(extraProperties)); } else { runMSBuild(new[] { solutionName, "-t:Clean", $"-p:Configuration={configurationOption.Value}", getPlatformArg(), "-v:normal", getMaxCpuCountArg() }.Concat(extraProperties)); } }); build.Target("restore") .Describe("Restores NuGet packages") .Does(() => { var extraProperties = getExtraProperties("restore"); if (msbuildSettings == null) { RunDotNet(new[] { "restore", solutionName, getPlatformArg(), "--verbosity", "normal", getMaxCpuCountArg() }.Concat(extraProperties)); } else { runMSBuild(new[] { solutionName, "-t:Restore", $"-p:Configuration={configurationOption.Value}", getPlatformArg(), "-v:normal", getMaxCpuCountArg() }.Concat(extraProperties)); } }); build.Target("build") .DependsOn("restore") .Describe("Builds the solution") .Does(() => { var buildNumberArg = buildNumberOption.Value == null ? null : $"-p:BuildNumber={buildNumberOption.Value}"; var extraProperties = getExtraProperties("build"); if (msbuildSettings == null) { RunDotNet(new[] { "build", solutionName, "-c", configurationOption.Value, getPlatformArg(), buildNumberArg, "--no-restore", "--verbosity", "normal", getMaxCpuCountArg() }.Concat(extraProperties)); } else { runMSBuild(new[] { solutionName, $"-p:Configuration={configurationOption.Value}", getPlatformArg(), buildNumberArg, "-v:normal", getMaxCpuCountArg() }.Concat(extraProperties)); } }); build.Target("test") .DependsOn("build") .Describe("Runs the unit tests") .Does(() => { if (noTestFlag.Value) { Console.WriteLine("Skipping unit tests due to --no-test."); } else { var extraProperties = getExtraProperties("test").ToList(); var findTestAssemblies = settings.TestSettings?.FindTestAssemblies; if (findTestAssemblies != null) { foreach (var testAssembly in findTestAssemblies()) { if (settings.TestSettings?.RunTests != null) { settings.TestSettings.RunTests(testAssembly); } else { RunDotNet(new AppRunnerSettings { Arguments = new[] { "vstest", Path.GetFileName(testAssembly) }.Concat(extraProperties), WorkingDirectory = Path.GetDirectoryName(testAssembly) }); } } } else { var testProjects = new List <string?>(); var findTestProjects = settings.TestSettings?.FindProjects; if (findTestProjects != null) { testProjects.AddRange(findTestProjects()); } else { testProjects.Add(solutionName); } foreach (var testProject in testProjects) { if (settings.TestSettings?.RunTests != null) { settings.TestSettings.RunTests(testProject); } else { RunDotNet(new[] { "test", testProject, "-c", configurationOption.Value, getPlatformArg(), "--no-build", getMaxCpuCountArg() }.Concat(extraProperties)); } } } } }); build.Target("package") .DependsOn("clean", "test") .Describe("Builds NuGet packages") .Does(() => { trigger = triggerOption.Value; if (trigger == "detect") { using var repository = new Repository("."); var headSha = repository.Head.Tip.Sha; var autoTrigger = GetBestTriggerFromTags(repository.Tags.Where(x => x.Target.Sha == headSha).Select(x => x.FriendlyName).ToList()); if (autoTrigger != null) { trigger = autoTrigger; ignoreIfAlreadyPushed = true; Console.WriteLine($"Detected trigger: {trigger}"); } } var versionSuffix = versionSuffixOption.Value; if (versionSuffix == null && trigger != null) { versionSuffix = GetVersionFromTrigger(trigger) is string triggerVersion ? SplitVersion(triggerVersion).Suffix : null; } var nugetOutputPath = Path.GetFullPath(nugetOutputOption.Value); var tempOutputPath = Path.Combine(nugetOutputPath, $"temp_{Guid.NewGuid():N}"); var packageProjects = new List <string?>(); var findPackageProjects = settings.PackageSettings?.FindProjects; if (findPackageProjects != null) { packageProjects.AddRange(findPackageProjects()); } else { packageProjects.Add(solutionName); } var extraProperties = getExtraProperties("package").ToList(); foreach (var packageProject in packageProjects) { if (msbuildSettings == null) { RunDotNet(new[] { "pack", packageProject, "-c", configurationOption.Value, getPlatformArg(), "--no-build", "--output", tempOutputPath, versionSuffix != null ? "--version-suffix" : null, versionSuffix, getMaxCpuCountArg() }.Concat(extraProperties)); } else { runMSBuild(new[] { packageProject, "-t:Pack", $"-p:Configuration={configurationOption.Value}", getPlatformArg(), "-p:NoBuild=true", $"-p:PackageOutputPath={tempOutputPath}", versionSuffix != null ? $"-p:VersionSuffix={versionSuffix}" : null, "-v:normal", getMaxCpuCountArg() }.Concat(extraProperties)); } } var tempPackagePaths = FindFilesFrom(tempOutputPath, "*.nupkg"); foreach (var tempPackagePath in tempPackagePaths) { var packagePath = Path.Combine(nugetOutputPath, Path.GetFileName(tempPackagePath) ?? throw new InvalidOperationException()); if (File.Exists(packagePath)) { File.Delete(packagePath); } File.Move(tempPackagePath, packagePath); packagePaths.Add(packagePath); } deleteDirectory(tempOutputPath); if (packagePaths.Count == 0) { throw new ApplicationException("No NuGet packages created."); } }); build.Target("publish") .Describe("Publishes NuGet packages and documentation") .DependsOn("package") .Does(() => { if (packagePaths.Count == 0) { throw new ApplicationException("No NuGet packages found."); } if (trigger == null) { throw new ApplicationException("--trigger option required."); } var triggerParts = trigger.Split('-'); var publishTrigger = triggerParts.Length >= 2 && triggerParts[0] == "publish" ? triggerParts[1] : null; var shouldPublishPackages = publishTrigger == "package" || publishTrigger == "packages" || publishTrigger == "all"; var shouldPublishDocs = publishTrigger == "docs" || publishTrigger == "all"; var triggerVersion = GetVersionFromTrigger(trigger); if (triggerVersion != null) { var mismatches = packagePaths.Where(x => GetPackageInfo(x).Version != triggerVersion).ToList(); if (mismatches.Count != 0) { throw new ApplicationException($"Trigger '{trigger}' doesn't match package version: {string.Join(", ", mismatches.Select(Path.GetFileName))}"); } shouldPublishPackages = true; shouldPublishDocs = triggerVersion.IndexOf('-') == -1; } if (shouldPublishPackages || shouldPublishDocs) { var docsSettings = settings.DocsSettings; var shouldPushDocs = false; string?cloneDirectory = null; string?repoDirectory = null; string?gitBranchName = null; Credentials provideCredentials(string url, string usernameFromUrl, SupportedCredentialTypes types) => new UsernamePasswordCredentials { Username = docsSettings?.GitLogin?.Username ?? throw new ApplicationException("GitLogin has a null Username."), Password = docsSettings?.GitLogin?.Password ?? throw new ApplicationException("GitLogin has a null Password."), }; if (shouldPublishDocs && docsSettings != null) { if (docsSettings.GitLogin == null || docsSettings.GitAuthor == null) { throw new ApplicationException("GitLogin and GitAuthor must be set on DocsSettings."); } var gitRepositoryUrl = docsSettings.GitRepositoryUrl; gitBranchName = docsSettings.GitBranchName; if (gitRepositoryUrl != null) { cloneDirectory = "docs_repo_" + Path.GetRandomFileName(); Repository.Clone(sourceUrl: gitRepositoryUrl, workdirPath: cloneDirectory, options: new CloneOptions { BranchName = gitBranchName, CredentialsProvider = provideCredentials }); repoDirectory = cloneDirectory; } else { repoDirectory = "."; } using var repository = new Repository(repoDirectory); if (gitRepositoryUrl != null) { if (gitBranchName == null) { gitBranchName = repository.Head.FriendlyName; } } else if (gitBranchName != null) { if (gitBranchName != repository.Head.FriendlyName) { var branch = repository.Branches[gitBranchName] ?? repository.CreateBranch(gitBranchName); Commands.Checkout(repository, branch); } } else { var branch = repository.Branches.FirstOrDefault(x => x.IsCurrentRepositoryHead); if (branch == null) { var autoBranchName = Environment.GetEnvironmentVariable("APPVEYOR_REPO_BRANCH"); if (autoBranchName != null) { branch = repository.Branches[autoBranchName] ?? repository.CreateBranch(autoBranchName); } else { branch = repository.Branches.FirstOrDefault(x => x.Tip.Sha == repository.Head.Tip.Sha); } if (branch != null) { Commands.Checkout(repository, branch); } } if (branch == null) { throw new ArgumentException("Could not determine repository branch for publishing docs."); } gitBranchName = branch.FriendlyName; } var projectHasDocs = docsSettings.ProjectHasDocs ?? (_ => true); foreach (var projectName in packagePaths.Select(x => GetPackageInfo(x).Name).Where(projectHasDocs)) { string findAssembly(string name) => FindFiles($"tools/XmlDocTarget/bin/**/{name}.dll").OrderByDescending(File.GetLastWriteTime).FirstOrDefault() ?? FindFiles($"src/{name}/bin/**/{name}.dll").OrderByDescending(File.GetLastWriteTime).FirstOrDefault(); var assemblyPaths = new List <string>(); if (docsSettings.FindAssemblies != null) { assemblyPaths.AddRange(docsSettings.FindAssemblies(projectName)); } else { var assemblyPath = (docsSettings.FindAssembly ?? findAssembly)(projectName); if (assemblyPath != null) { assemblyPaths.Add(assemblyPath); } } if (assemblyPaths.Count != 0) { foreach (var assemblyPath in assemblyPaths) { RunApp(dotNetTools.GetToolPath($"xmldocmd/{xmlDocMarkdownVersion}"), assemblyPath, Path.Combine(repoDirectory, docsSettings.TargetDirectory ?? "docs"), "--source", $"{docsSettings.SourceCodeUrl}/{projectName}", "--newline", "lf", "--clean"); } } else { Console.WriteLine($"Documentation not generated for {projectName}; assembly not found."); } } shouldPushDocs = repository.RetrieveStatus().IsDirty; } if (shouldPublishPackages) { var nugetApiKey = settings.NuGetApiKey; if (string.IsNullOrEmpty(nugetApiKey)) { throw new ApplicationException("NuGetApiKey required to publish."); } if (ignoreIfAlreadyPushed) { var nugetSettings = NuGet.Configuration.Settings.LoadDefaultSettings(root: null); var packageSourceProvider = new PackageSourceProvider(nugetSettings); var sourceRepositoryProvider = new SourceRepositoryProvider(packageSourceProvider, NuGet.Protocol.Core.Types.Repository.Provider.GetCoreV3()); using var sourceCacheContext = new SourceCacheContext(); var nugetRepositories = sourceRepositoryProvider.GetRepositories() .Select(x => x.GetResourceAsync <DependencyInfoResource>().GetAwaiter().GetResult()) .ToList(); foreach (var packagePath in packagePaths.ToList()) { var packageInfo = GetPackageInfo(packagePath); var package = new PackageIdentity(packageInfo.Name, NuGetVersion.Parse(packageInfo.Version)); foreach (var nugetRepository in nugetRepositories) { var dependencyInfo = nugetRepository.ResolvePackage(package, NuGetFramework.AnyFramework, sourceCacheContext, NullLogger.Instance, CancellationToken.None).GetAwaiter().GetResult(); if (dependencyInfo != null) { Console.WriteLine($"Package already pushed: {packageInfo.Name} {packageInfo.Version}"); packagePaths.Remove(packagePath); break; } } } } foreach (var packagePath in packagePaths) { RunDotNet("nuget", "push", packagePath, "--source", nugetSource, "--api-key", nugetApiKey); } } if (shouldPushDocs) { using var repository = new Repository(repoDirectory); Console.WriteLine("Publishing documentation changes."); Commands.Stage(repository, "*"); var author = new Signature(docsSettings !.GitAuthor !.Name, docsSettings !.GitAuthor !.Email, DateTimeOffset.Now); repository.Commit("Documentation updated.", author, author, new CommitOptions()); repository.Network.Push(repository.Network.Remotes["origin"], $"refs/heads/{gitBranchName}", new PushOptions { CredentialsProvider = provideCredentials }); } if (cloneDirectory != null) { // delete the cloned directory foreach (var fileInfo in FindFiles(cloneDirectory, "**").Select(x => new FileInfo(x)).Where(x => x.IsReadOnly)) { fileInfo.IsReadOnly = false; } deleteDirectory(cloneDirectory); } } else { Console.WriteLine("To publish to NuGet, push this tag: v" + GetPackageInfo(packagePaths[0]).Version); } }); string?getPlatformArg() { var platformValue = platformOption?.Value ?? settings?.SolutionPlatform; return(platformValue == null ? null : $"-p:Platform={platformValue}"); } string?getMaxCpuCountArg() { if (settings !.MaxCpuCount != null) { return($"-maxcpucount:{settings.MaxCpuCount}"); }
/// <summary> /// Adds the standard .NET targets to the build. /// </summary> /// <param name="build">The build to which to add targets.</param> /// <param name="settings">The build settings.</param> public static void AddDotNetTargets(this BuildApp build, DotNetBuildSettings settings = null) { settings = settings ?? new DotNetBuildSettings(); var buildOptions = settings.BuildOptions ?? (settings.BuildOptions = new DotNetBuildOptions()); var configurationOption = buildOptions.ConfigurationOption ?? (buildOptions.ConfigurationOption = build.AddOption("-c|--configuration <name>", "The configuration to build (default Release)", "Release")); var platformOption = buildOptions.PlatformOption ?? (buildOptions.PlatformOption = build.AddOption("-p|--platform <name>", "The solution platform to build")); var versionSuffixOption = buildOptions.VersionSuffixOption ?? (buildOptions.VersionSuffixOption = build.AddOption("--version-suffix <suffix>", "Generates a prerelease package")); var nugetOutputOption = buildOptions.NuGetOutputOption ?? (buildOptions.NuGetOutputOption = build.AddOption("--nuget-output <path>", "Directory for generated package (default release)", "release")); var triggerOption = buildOptions.TriggerOption ?? (buildOptions.TriggerOption = build.AddOption("--trigger <name>", "The git branch or tag that triggered the build")); var buildNumberOption = buildOptions.BuildNumberOption ?? (buildOptions.BuildNumberOption = build.AddOption("--build-number <number>", "The automated build number")); var solutionName = settings.SolutionName; var nugetSource = settings.NuGetSource ?? "https://api.nuget.org/v3/index.json"; var msbuildSettings = settings.MSBuildSettings; var dotNetTools = settings.DotNetTools ?? new DotNetTools(Path.Combine("tools", "bin")); var sourceLinkVersion = settings.SourceLinkToolVersion ?? "3.1.1"; var xmlDocMarkdownVersion = settings.DocsSettings?.ToolVersion ?? "1.5.1"; var packageDiffVersion = settings.PackageDiffToolVersion ?? "0.2.1"; var packagePaths = new List <string>(); bool hasBadPackageVersion = false; build.Target("clean") .Describe("Deletes all build output") .Does(() => { foreach (var directory in FindDirectories("{src,tests}/**/{bin,obj}", "tools/bin", "release")) { Directory.Delete(directory, recursive: true); } var extraProperties = getExtraProperties("clean"); if (msbuildSettings == null) { RunDotNet(new[] { "clean", solutionName, "-c", configurationOption.Value, getPlatformArg(), "--verbosity", "normal", getMaxCpuCountArg() }.Concat(extraProperties)); } else { runMSBuild(new[] { solutionName, "-t:Clean", $"-p:Configuration={configurationOption.Value}", getPlatformArg(), "-v:normal", getMaxCpuCountArg() }.Concat(extraProperties)); } }); build.Target("restore") .Describe("Restores NuGet packages") .Does(() => { var extraProperties = getExtraProperties("restore"); if (msbuildSettings == null) { RunDotNet(new[] { "restore", solutionName, getPlatformArg(), "--verbosity", "normal", getMaxCpuCountArg() }.Concat(extraProperties)); } else { runMSBuild(new[] { solutionName, "-t:Restore", $"-p:Configuration={configurationOption.Value}", getPlatformArg(), "-v:normal", getMaxCpuCountArg() }.Concat(extraProperties)); } }); build.Target("build") .DependsOn("restore") .Describe("Builds the solution") .Does(() => { var buildNumberArg = buildNumberOption.Value == null ? null : $"-p:BuildNumber={buildNumberOption.Value}"; var extraProperties = getExtraProperties("build"); if (msbuildSettings == null) { RunDotNet(new[] { "build", solutionName, "-c", configurationOption.Value, getPlatformArg(), buildNumberArg, "--no-restore", "--verbosity", "normal", getMaxCpuCountArg() }.Concat(extraProperties)); } else { runMSBuild(new[] { solutionName, $"-p:Configuration={configurationOption.Value}", getPlatformArg(), buildNumberArg, "-v:normal", getMaxCpuCountArg() }.Concat(extraProperties)); } }); build.Target("test") .DependsOn("build") .Describe("Runs the unit tests") .Does(() => { var extraProperties = getExtraProperties("test"); var findTestAssemblies = settings.TestSettings?.FindTestAssemblies; if (findTestAssemblies != null) { foreach (var testAssembly in findTestAssemblies()) { RunDotNet(new AppRunnerSettings { Arguments = new[] { "vstest", Path.GetFileName(testAssembly) }.Concat(extraProperties), WorkingDirectory = Path.GetDirectoryName(testAssembly) }); } } else { RunDotNet(new[] { "test", solutionName, "-c", configurationOption.Value, getPlatformArg(), "--no-build", getMaxCpuCountArg() }.Concat(extraProperties)); } }); build.Target("package") .DependsOn("test") .Describe("Builds NuGet packages") .Does(() => { string versionSuffix = versionSuffixOption.Value; string trigger = triggerOption.Value; if (versionSuffix == null && trigger != null) { var group = s_triggerRegex.Match(trigger).Groups["suffix"]; if (group.Success) { versionSuffix = group.ToString(); } } var nugetOutputPath = Path.GetFullPath(nugetOutputOption.Value); var tempOutputPath = Path.Combine(nugetOutputPath, $"temp_{Guid.NewGuid():N}"); var extraProperties = getExtraProperties("package"); if (msbuildSettings == null) { RunDotNet(new[] { "pack", solutionName, "-c", configurationOption.Value, getPlatformArg(), "--no-build", "--output", tempOutputPath, versionSuffix != null ? "--version-suffix" : null, versionSuffix, getMaxCpuCountArg() }.Concat(extraProperties)); } else { runMSBuild(new[] { solutionName, "-t:Pack", $"-p:Configuration={configurationOption.Value}", getPlatformArg(), "-p:NoBuild=true", $"-p:PackageOutputPath={tempOutputPath}", versionSuffix != null ? $"-p:VersionSuffix={versionSuffix}" : null, "-v:normal", getMaxCpuCountArg() }.Concat(extraProperties)); } var tempPackagePaths = FindFilesFrom(tempOutputPath, "*.nupkg"); foreach (var tempPackagePath in tempPackagePaths) { var packagePath = Path.Combine(nugetOutputPath, Path.GetFileName(tempPackagePath)); if (File.Exists(packagePath)) { File.Delete(packagePath); } File.Move(tempPackagePath, packagePath); packagePaths.Add(packagePath); } Directory.Delete(tempOutputPath); if (packagePaths.Count == 0) { throw new ApplicationException("No NuGet packages created."); } var projectUsesSemVer = settings.ProjectUsesSemVer; foreach (var packagePath in packagePaths) { var packageInfo = GetPackageInfo(packagePath); if (projectUsesSemVer == null || projectUsesSemVer(packageInfo.Name)) { int exitCode = RunApp(dotNetTools.GetToolPath($"Faithlife.PackageDiffTool.Tool/{packageDiffVersion}", "packagediff"), new AppRunnerSettings { Arguments = new[] { "--verifyversion", "--verbose", packagePath }, IsExitCodeSuccess = x => x <= 2, // don't fail on crash }); hasBadPackageVersion = exitCode == 2; } } }); build.Target("publish") .Describe("Publishes NuGet packages and documentation") .DependsOn("clean", "package") .Does(() => { if (packagePaths.Count == 0) { throw new ApplicationException("No NuGet packages found."); } var trigger = triggerOption.Value; if (trigger == null) { throw new ApplicationException("--trigger option required."); } bool shouldPublishPackages = trigger == "publish-package" || trigger == "publish-packages" || trigger == "publish-all"; bool shouldPublishDocs = trigger == "publish-docs" || trigger == "publish-all"; var triggerMatch = s_triggerRegex.Match(trigger); if (triggerMatch.Success) { var triggerName = triggerMatch.Groups["name"].Value; var triggerVersion = triggerMatch.Groups["version"].Value; if (triggerName.Length == 0) { var mismatches = packagePaths.Where(x => GetPackageInfo(x).Version != triggerVersion).ToList(); if (mismatches.Count != 0) { throw new ApplicationException($"Trigger '{trigger}' doesn't match package version: {string.Join(", ", mismatches.Select(Path.GetFileName))}"); } } else { var matches = packagePaths.Where(x => $".{GetPackageInfo(x).Name}".EndsWith($".{triggerName}", StringComparison.OrdinalIgnoreCase)).ToList(); if (matches.Count == 0) { throw new ApplicationException($"Trigger '{trigger}' does not match any packages: {string.Join(", ", packagePaths.Select(Path.GetFileName))}"); } if (matches.Count > 1) { throw new ApplicationException($"Trigger '{trigger}' matches multiple package(s): {string.Join(", ", matches.Select(Path.GetFileName))}"); } if (GetPackageInfo(matches[0]).Version != triggerVersion) { throw new ApplicationException($"Trigger '{trigger}' doesn't match package version: {Path.GetFileName(matches[0])}"); } packagePaths = matches; } shouldPublishPackages = true; shouldPublishDocs = !triggerMatch.Groups["suffix"].Success; } if (shouldPublishPackages && hasBadPackageVersion) { throw new ApplicationException("Use suggested package version to publish."); } if (shouldPublishPackages || shouldPublishDocs) { var docsSettings = settings.DocsSettings; bool shouldPushDocs = false; string cloneDirectory = null; string repoDirectory = null; string gitBranchName = null; Credentials provideCredentials(string url, string usernameFromUrl, SupportedCredentialTypes types) => new UsernamePasswordCredentials { Username = docsSettings.GitLogin.Username ?? throw new ApplicationException("GitLogin has a null Username."), Password = docsSettings.GitLogin.Password ?? throw new ApplicationException("GitLogin has a null Password."), }; if (shouldPublishDocs && docsSettings != null) { if (docsSettings.GitLogin == null || docsSettings.GitAuthor == null) { throw new ApplicationException("GitLogin and GitAuthor must be set on DocsSettings."); } var gitRepositoryUrl = docsSettings.GitRepositoryUrl; gitBranchName = docsSettings.GitBranchName; if (gitRepositoryUrl != null) { cloneDirectory = "docs_repo_" + Path.GetRandomFileName(); Repository.Clone(sourceUrl: gitRepositoryUrl, workdirPath: cloneDirectory, options: new CloneOptions { BranchName = gitBranchName, CredentialsProvider = provideCredentials }); repoDirectory = cloneDirectory; } else { repoDirectory = "."; } using (var repository = new Repository(repoDirectory)) { if (gitRepositoryUrl != null) { if (gitBranchName == null) { gitBranchName = repository.Head.FriendlyName; } } else if (gitBranchName != null) { if (gitBranchName != repository.Head.FriendlyName) { var branch = repository.Branches[gitBranchName] ?? repository.CreateBranch(gitBranchName); Commands.Checkout(repository, branch); } } else { var branch = repository.Branches.FirstOrDefault(x => x.IsCurrentRepositoryHead); if (branch == null) { var autoBranchName = Environment.GetEnvironmentVariable("APPVEYOR_REPO_BRANCH"); if (autoBranchName == null) { throw new ArgumentException("Could not determine repository branch."); } branch = repository.Branches[autoBranchName] ?? repository.CreateBranch(autoBranchName); Commands.Checkout(repository, branch); } gitBranchName = branch.FriendlyName; } foreach (var projectName in packagePaths.Select(x => GetPackageInfo(x).Name)) { string findAssembly(string name) => FindFiles($"tools/XmlDocTarget/bin/**/{name}.dll").OrderByDescending(File.GetLastWriteTime).FirstOrDefault() ?? FindFiles($"src/{name}/bin/**/{name}.dll").OrderByDescending(File.GetLastWriteTime).FirstOrDefault(); var assemblyPath = (docsSettings.FindAssembly ?? findAssembly)(projectName); if (assemblyPath != null) { RunApp(dotNetTools.GetToolPath($"xmldocmd/{xmlDocMarkdownVersion}"), assemblyPath, Path.Combine(repoDirectory, docsSettings.TargetDirectory ?? "docs"), "--source", $"{docsSettings.SourceCodeUrl}/{projectName}", "--newline", "lf", "--clean"); } else { Console.WriteLine($"Documentation not generated for {projectName}; assembly not found."); } } shouldPushDocs = repository.RetrieveStatus().IsDirty; } } if (shouldPublishPackages) { var projectUsesSourceLink = settings.ProjectUsesSourceLink; foreach (var packagePath in packagePaths) { if (projectUsesSourceLink == null || projectUsesSourceLink(GetPackageInfo(packagePath).Name)) { RunApp(dotNetTools.GetToolPath($"sourcelink/{sourceLinkVersion}"), "test", packagePath); } } var nugetApiKey = settings.NuGetApiKey; if (string.IsNullOrEmpty(nugetApiKey)) { throw new ApplicationException("NuGetApiKey required to publish."); } foreach (var packagePath in packagePaths) { RunDotNet("nuget", "push", packagePath, "--source", nugetSource, "--api-key", nugetApiKey); } } if (shouldPushDocs) { using (var repository = new Repository(repoDirectory)) { Console.WriteLine("Publishing documentation changes."); Commands.Stage(repository, "*"); var author = new Signature(docsSettings.GitAuthor.Name, docsSettings.GitAuthor.Email, DateTimeOffset.Now); repository.Commit("Documentation updated.", author, author, new CommitOptions()); repository.Network.Push(repository.Network.Remotes["origin"], $"refs/heads/{gitBranchName}", new PushOptions { CredentialsProvider = provideCredentials }); } } if (cloneDirectory != null) { // delete the cloned directory foreach (var fileInfo in FindFiles(cloneDirectory, "**").Select(x => new FileInfo(x)).Where(x => x.IsReadOnly)) { fileInfo.IsReadOnly = false; } Directory.Delete(cloneDirectory, recursive: true); } } else { Console.WriteLine("To publish to NuGet, push a matching git tag for the release."); } }); string getPlatformArg() { string platformValue = platformOption.Value ?? settings?.SolutionPlatform; return(platformValue == null ? null : $"-p:Platform={platformValue}"); } string getMaxCpuCountArg() { if (settings.MaxCpuCount != null) { return($"-maxcpucount:{settings.MaxCpuCount}"); } else if (msbuildSettings != null) { return("-maxcpucount"); } else { return(null); } } IEnumerable <string> getExtraProperties(string target) { var pairs = settings.ExtraProperties?.Invoke(target); if (pairs != null) { foreach (var pair in pairs) { yield return($"-p:{pair.Key}={pair.Value}"); } } } void runMSBuild(IEnumerable <string> arguments) => RunMSBuild(msbuildSettings, arguments); }