diff --git a/deployment/cake/apps-uwp-tasks.cake b/deployment/cake/apps-uwp-tasks.cake index 43ce6221..58bc9581 100644 --- a/deployment/cake/apps-uwp-tasks.cake +++ b/deployment/cake/apps-uwp-tasks.cake @@ -1,209 +1,209 @@ -#l "apps-uwp-variables.cake" - -#addin "nuget:?package=Cake.WindowsAppStore&version=2.0.0" - -//------------------------------------------------------------- - -public class UwpProcessor : ProcessorBase -{ - public UwpProcessor(BuildContext buildContext) - : base(buildContext) - { - - } - - public override bool HasItems() - { - return BuildContext.Uwp.Items.Count > 0; - } - - private void UpdateAppxManifestVersion(string path, string version) - { - CakeContext.Information("Updating AppxManifest version @ '{0}' to '{1}'", path, version); - - CakeContext.TransformConfig(path, - new TransformationCollection { - { "Package/Identity/@Version", version } - }); - } - - private string GetArtifactsDirectory(string outputRootDirectory) - { - // 1 directory up since we want to turn "/output/release" into "/output/" - var artifactsDirectoryString = System.IO.Path.Combine(outputRootDirectory, ".."); - var artifactsDirectory = CakeContext.MakeAbsolute(CakeContext.Directory(artifactsDirectoryString)).FullPath; - - return artifactsDirectory; - } - - private string GetAppxUploadFileName(string artifactsDirectory, string solutionName, string versionMajorMinorPatch) - { - var appxUploadSearchPattern = System.IO.Path.Combine(artifactsDirectory, string.Format("{0}_{1}.0_*.appxupload", solutionName, versionMajorMinorPatch)); - - CakeContext.Information("Searching for appxupload using '{0}'", appxUploadSearchPattern); - - var filesToZip = CakeContext.GetFiles(appxUploadSearchPattern); - - CakeContext.Information("Found '{0}' files to upload", filesToZip.Count); - - var appxUploadFile = filesToZip.FirstOrDefault(); - if (appxUploadFile is null) - { - return null; - } - - var appxUploadFileName = appxUploadFile.FullPath; - return appxUploadFileName; - } - - public override async Task PrepareAsync() - { - if (!HasItems()) - { - return; - } - - // Check whether projects should be processed, `.ToList()` - // is required to prevent issues with foreach - foreach (var uwpApp in BuildContext.Uwp.Items.ToList()) - { - if (!ShouldProcessProject(BuildContext, uwpApp)) - { - BuildContext.Uwp.Items.Remove(uwpApp); - } - } - } - - public override async Task UpdateInfoAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var uwpApp in BuildContext.Uwp.Items) - { - var appxManifestFile = System.IO.Path.Combine(".", "src", uwpApp, "Package.appxmanifest"); - UpdateAppxManifestVersion(appxManifestFile, string.Format("{0}.0", BuildContext.General.Version.MajorMinorPatch)); - } - } - - public override async Task BuildAsync() - { - if (!HasItems()) - { - return; - } - - var platforms = new Dictionary(); - //platforms["AnyCPU"] = PlatformTarget.MSIL; - platforms["x86"] = PlatformTarget.x86; - platforms["x64"] = PlatformTarget.x64; - platforms["arm"] = PlatformTarget.ARM; - - // Important note: we only have to build for ARM, it will auto-build x86 / x64 as well - var platform = platforms.First(x => x.Key == "arm"); - - foreach (var uwpApp in BuildContext.Uwp.Items) - { - CakeContext.Information("Building UWP app '{0}'", uwpApp); - - var artifactsDirectory = GetArtifactsDirectory(BuildContext.General.OutputRootDirectory); - var appxUploadFileName = GetAppxUploadFileName(artifactsDirectory, uwpApp, BuildContext.General.Version.MajorMinorPatch); - - // If already exists, skip for store upload debugging - if (appxUploadFileName != null && CakeContext.FileExists(appxUploadFileName)) - { - CakeContext.Information(string.Format("File '{0}' already exists, skipping build", appxUploadFileName)); - continue; - } - - var msBuildSettings = new MSBuildSettings { - Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic - ToolVersion = MSBuildToolVersion.Default, - Configuration = BuildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = platform.Value - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, uwpApp, "build"); - - // Always disable SourceLink - msBuildSettings.WithProperty("EnableSourceLink", "false"); - - // See https://docs.microsoft.com/en-us/windows/uwp/packaging/auto-build-package-uwp-apps for all the details - //msBuildSettings.Properties["UseDotNetNativeToolchain"] = new List(new [] { "false" }); - //msBuildSettings.Properties["UapAppxPackageBuildMode"] = new List(new [] { "StoreUpload" }); - msBuildSettings.Properties["UapAppxPackageBuildMode"] = new List(new [] { "CI" }); - msBuildSettings.Properties["AppxBundlePlatforms"] = new List(new [] { string.Join("|", platforms.Keys) }); - msBuildSettings.Properties["AppxBundle"] = new List(new [] { "Always" }); - msBuildSettings.Properties["AppxPackageDir"] = new List(new [] { artifactsDirectory }); - - CakeContext.Information("Building project for platform {0}, artifacts directory is '{1}'", platform.Key, artifactsDirectory); - - var projectFileName = GetProjectFileName(BuildContext, uwpApp); - - // Note: if csproj doesn't work, use SolutionFileName instead - //var projectFileName = SolutionFileName; - RunMsBuild(BuildContext, uwpApp, projectFileName, msBuildSettings, "build"); - - // Recalculate! - appxUploadFileName = GetAppxUploadFileName(artifactsDirectory, uwpApp, BuildContext.General.Version.MajorMinorPatch); - if (appxUploadFileName is null) - { - throw new Exception(string.Format("Couldn't determine the appxupload file using base directory '{0}'", artifactsDirectory)); - } - - CakeContext.Information("Created appxupload file '{0}'", appxUploadFileName, artifactsDirectory); - } - } - - public override async Task PackageAsync() - { - if (!HasItems()) - { - return; - } - - // No specific implementation required for now, build already wraps it up - } - - public override async Task DeployAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var uwpApp in BuildContext.Uwp.Items) - { - if (!ShouldDeployProject(BuildContext, uwpApp)) - { - CakeContext.Information("UWP app '{0}' should not be deployed", uwpApp); - continue; - } - - BuildContext.CakeContext.LogSeparator("Deploying UWP app '{0}'", uwpApp); - - var artifactsDirectory = GetArtifactsDirectory(BuildContext.General.OutputRootDirectory); - var appxUploadFileName = GetAppxUploadFileName(artifactsDirectory, uwpApp, BuildContext.General.Version.MajorMinorPatch); - - CakeContext.Information("Creating Windows Store app submission"); - - CakeContext.CreateWindowsStoreAppSubmission(appxUploadFileName, new WindowsStoreAppSubmissionSettings - { - ApplicationId = BuildContext.Uwp.WindowsStoreAppId, - ClientId = BuildContext.Uwp.WindowsStoreClientId, - ClientSecret = BuildContext.Uwp.WindowsStoreClientSecret, - TenantId = BuildContext.Uwp.WindowsStoreTenantId - }); - - await BuildContext.Notifications.NotifyAsync(uwpApp, string.Format("Deployed to store"), TargetType.UwpApp); - } - } - - public override async Task FinalizeAsync() - { - - } -} +#l "apps-uwp-variables.cake" + +#addin "nuget:?package=Cake.WindowsAppStore&version=2.0.0" + +//------------------------------------------------------------- + +public class UwpProcessor : ProcessorBase +{ + public UwpProcessor(BuildContext buildContext) + : base(buildContext) + { + + } + + public override bool HasItems() + { + return BuildContext.Uwp.Items.Count > 0; + } + + private void UpdateAppxManifestVersion(string path, string version) + { + CakeContext.Information("Updating AppxManifest version @ '{0}' to '{1}'", path, version); + + CakeContext.TransformConfig(path, + new TransformationCollection { + { "Package/Identity/@Version", version } + }); + } + + private string GetArtifactsDirectory(string outputRootDirectory) + { + // 1 directory up since we want to turn "/output/release" into "/output/" + var artifactsDirectoryString = System.IO.Path.Combine(outputRootDirectory, ".."); + var artifactsDirectory = CakeContext.MakeAbsolute(CakeContext.Directory(artifactsDirectoryString)).FullPath; + + return artifactsDirectory; + } + + private string GetAppxUploadFileName(string artifactsDirectory, string solutionName, string versionMajorMinorPatch) + { + var appxUploadSearchPattern = System.IO.Path.Combine(artifactsDirectory, string.Format("{0}_{1}.0_*.appxupload", solutionName, versionMajorMinorPatch)); + + CakeContext.Information("Searching for appxupload using '{0}'", appxUploadSearchPattern); + + var filesToZip = CakeContext.GetFiles(appxUploadSearchPattern); + + CakeContext.Information("Found '{0}' files to upload", filesToZip.Count); + + var appxUploadFile = filesToZip.FirstOrDefault(); + if (appxUploadFile is null) + { + return null; + } + + var appxUploadFileName = appxUploadFile.FullPath; + return appxUploadFileName; + } + + public override async Task PrepareAsync() + { + if (!HasItems()) + { + return; + } + + // Check whether projects should be processed, `.ToList()` + // is required to prevent issues with foreach + foreach (var uwpApp in BuildContext.Uwp.Items.ToList()) + { + if (!ShouldProcessProject(BuildContext, uwpApp)) + { + BuildContext.Uwp.Items.Remove(uwpApp); + } + } + } + + public override async Task UpdateInfoAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var uwpApp in BuildContext.Uwp.Items) + { + var appxManifestFile = System.IO.Path.Combine(".", "src", uwpApp, "Package.appxmanifest"); + UpdateAppxManifestVersion(appxManifestFile, string.Format("{0}.0", BuildContext.General.Version.MajorMinorPatch)); + } + } + + public override async Task BuildAsync() + { + if (!HasItems()) + { + return; + } + + var platforms = new Dictionary(); + //platforms["AnyCPU"] = PlatformTarget.MSIL; + platforms["x86"] = PlatformTarget.x86; + platforms["x64"] = PlatformTarget.x64; + platforms["arm"] = PlatformTarget.ARM; + + // Important note: we only have to build for ARM, it will auto-build x86 / x64 as well + var platform = platforms.First(x => x.Key == "arm"); + + foreach (var uwpApp in BuildContext.Uwp.Items) + { + CakeContext.Information("Building UWP app '{0}'", uwpApp); + + var artifactsDirectory = GetArtifactsDirectory(BuildContext.General.OutputRootDirectory); + var appxUploadFileName = GetAppxUploadFileName(artifactsDirectory, uwpApp, BuildContext.General.Version.MajorMinorPatch); + + // If already exists, skip for store upload debugging + if (appxUploadFileName != null && CakeContext.FileExists(appxUploadFileName)) + { + CakeContext.Information(string.Format("File '{0}' already exists, skipping build", appxUploadFileName)); + continue; + } + + var msBuildSettings = new MSBuildSettings { + Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic + ToolVersion = MSBuildToolVersion.Default, + Configuration = BuildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = platform.Value + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, uwpApp, "build"); + + // Always disable SourceLink + msBuildSettings.WithProperty("EnableSourceLink", "false"); + + // See https://docs.microsoft.com/en-us/windows/uwp/packaging/auto-build-package-uwp-apps for all the details + //msBuildSettings.Properties["UseDotNetNativeToolchain"] = new List(new [] { "false" }); + //msBuildSettings.Properties["UapAppxPackageBuildMode"] = new List(new [] { "StoreUpload" }); + msBuildSettings.Properties["UapAppxPackageBuildMode"] = new List(new [] { "CI" }); + msBuildSettings.Properties["AppxBundlePlatforms"] = new List(new [] { string.Join("|", platforms.Keys) }); + msBuildSettings.Properties["AppxBundle"] = new List(new [] { "Always" }); + msBuildSettings.Properties["AppxPackageDir"] = new List(new [] { artifactsDirectory }); + + CakeContext.Information("Building project for platform {0}, artifacts directory is '{1}'", platform.Key, artifactsDirectory); + + var projectFileName = GetProjectFileName(BuildContext, uwpApp); + + // Note: if csproj doesn't work, use SolutionFileName instead + //var projectFileName = SolutionFileName; + RunMsBuild(BuildContext, uwpApp, projectFileName, msBuildSettings, "build"); + + // Recalculate! + appxUploadFileName = GetAppxUploadFileName(artifactsDirectory, uwpApp, BuildContext.General.Version.MajorMinorPatch); + if (appxUploadFileName is null) + { + throw new Exception(string.Format("Couldn't determine the appxupload file using base directory '{0}'", artifactsDirectory)); + } + + CakeContext.Information("Created appxupload file '{0}'", appxUploadFileName, artifactsDirectory); + } + } + + public override async Task PackageAsync() + { + if (!HasItems()) + { + return; + } + + // No specific implementation required for now, build already wraps it up + } + + public override async Task DeployAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var uwpApp in BuildContext.Uwp.Items) + { + if (!ShouldDeployProject(BuildContext, uwpApp)) + { + CakeContext.Information("UWP app '{0}' should not be deployed", uwpApp); + continue; + } + + BuildContext.CakeContext.LogSeparator("Deploying UWP app '{0}'", uwpApp); + + var artifactsDirectory = GetArtifactsDirectory(BuildContext.General.OutputRootDirectory); + var appxUploadFileName = GetAppxUploadFileName(artifactsDirectory, uwpApp, BuildContext.General.Version.MajorMinorPatch); + + CakeContext.Information("Creating Windows Store app submission"); + + CakeContext.CreateWindowsStoreAppSubmission(appxUploadFileName, new WindowsStoreAppSubmissionSettings + { + ApplicationId = BuildContext.Uwp.WindowsStoreAppId, + ClientId = BuildContext.Uwp.WindowsStoreClientId, + ClientSecret = BuildContext.Uwp.WindowsStoreClientSecret, + TenantId = BuildContext.Uwp.WindowsStoreTenantId + }); + + await BuildContext.Notifications.NotifyAsync(uwpApp, string.Format("Deployed to store"), TargetType.UwpApp); + } + } + + public override async Task FinalizeAsync() + { + + } +} diff --git a/deployment/cake/apps-uwp-variables.cake b/deployment/cake/apps-uwp-variables.cake index 7b3a9f0c..2a445a6f 100644 --- a/deployment/cake/apps-uwp-variables.cake +++ b/deployment/cake/apps-uwp-variables.cake @@ -1,58 +1,58 @@ -#l "./buildserver.cake" - -//------------------------------------------------------------- - -public class UwpContext : BuildContextWithItemsBase -{ - public UwpContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string WindowsStoreAppId { get; set; } - public string WindowsStoreClientId { get; set; } - public string WindowsStoreClientSecret { get; set; } - public string WindowsStoreTenantId { get; set; } - - protected override void ValidateContext() - { - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' uwp projects"); - } -} - -//------------------------------------------------------------- - -private UwpContext InitializeUwpContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new UwpContext(parentBuildContext) - { - Items = UwpApps ?? new List(), - WindowsStoreAppId = buildContext.BuildServer.GetVariable("WindowsStoreAppId", showValue: true), - WindowsStoreClientId = buildContext.BuildServer.GetVariable("WindowsStoreClientId", showValue: false), - WindowsStoreClientSecret = buildContext.BuildServer.GetVariable("WindowsStoreClientSecret", showValue: false), - WindowsStoreTenantId = buildContext.BuildServer.GetVariable("WindowsStoreTenantId", showValue: false) - }; - - return data; -} - -//------------------------------------------------------------- - -List _uwpApps; - -public List UwpApps -{ - get - { - if (_uwpApps is null) - { - _uwpApps = new List(); - } - - return _uwpApps; - } +#l "./buildserver.cake" + +//------------------------------------------------------------- + +public class UwpContext : BuildContextWithItemsBase +{ + public UwpContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string WindowsStoreAppId { get; set; } + public string WindowsStoreClientId { get; set; } + public string WindowsStoreClientSecret { get; set; } + public string WindowsStoreTenantId { get; set; } + + protected override void ValidateContext() + { + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' uwp projects"); + } +} + +//------------------------------------------------------------- + +private UwpContext InitializeUwpContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new UwpContext(parentBuildContext) + { + Items = UwpApps ?? new List(), + WindowsStoreAppId = buildContext.BuildServer.GetVariable("WindowsStoreAppId", showValue: true), + WindowsStoreClientId = buildContext.BuildServer.GetVariable("WindowsStoreClientId", showValue: false), + WindowsStoreClientSecret = buildContext.BuildServer.GetVariable("WindowsStoreClientSecret", showValue: false), + WindowsStoreTenantId = buildContext.BuildServer.GetVariable("WindowsStoreTenantId", showValue: false) + }; + + return data; +} + +//------------------------------------------------------------- + +List _uwpApps; + +public List UwpApps +{ + get + { + if (_uwpApps is null) + { + _uwpApps = new List(); + } + + return _uwpApps; + } } \ No newline at end of file diff --git a/deployment/cake/apps-web-tasks.cake b/deployment/cake/apps-web-tasks.cake index cd548512..d5a59ad6 100644 --- a/deployment/cake/apps-web-tasks.cake +++ b/deployment/cake/apps-web-tasks.cake @@ -1,207 +1,207 @@ -#l "apps-web-variables.cake" -#l "lib-octopusdeploy.cake" - -#addin "nuget:?package=Microsoft.Azure.KeyVault.Core&version=3.0.5" -#addin "nuget:?package=WindowsAzure.Storage&version=9.3.3" - -//------------------------------------------------------------- - -public class WebProcessor : ProcessorBase -{ - public WebProcessor(BuildContext buildContext) - : base(buildContext) - { - - } - - public override bool HasItems() - { - return BuildContext.Web.Items.Count > 0; - } - - public override async Task PrepareAsync() - { - if (!HasItems()) - { - return; - } - - // Check whether projects should be processed, `.ToList()` - // is required to prevent issues with foreach - foreach (var webApp in BuildContext.Web.Items.ToList()) - { - if (!ShouldProcessProject(BuildContext, webApp)) - { - BuildContext.Web.Items.Remove(webApp); - } - } - } - - public override async Task UpdateInfoAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var webApp in BuildContext.Web.Items) - { - CakeContext.Information("Updating version for web app '{0}'", webApp); - - var projectFileName = GetProjectFileName(BuildContext, webApp); - - CakeContext.TransformConfig(projectFileName, new TransformationCollection - { - { "Project/PropertyGroup/PackageVersion", BuildContext.General.Version.NuGet } - }); - } - } - - public override async Task BuildAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var webApp in BuildContext.Web.Items) - { - BuildContext.CakeContext.LogSeparator("Building web app '{0}'", webApp); - - var projectFileName = GetProjectFileName(BuildContext, webApp); - - var msBuildSettings = new MSBuildSettings - { - Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic - ToolVersion = MSBuildToolVersion.Default, - Configuration = BuildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, webApp, "build"); - - // Always disable SourceLink - msBuildSettings.WithProperty("EnableSourceLink", "false"); - - RunMsBuild(BuildContext, webApp, projectFileName, msBuildSettings, "build"); - } - } - - public override async Task PackageAsync() - { - if (!HasItems()) - { - return; - } - - // For package documentation using Octopus Deploy, see https://octopus.com/docs/deployment-examples/deploying-asp.net-core-web-applications - - foreach (var webApp in BuildContext.Web.Items) - { - if (!ShouldPackageProject(BuildContext, webApp)) - { - CakeContext.Information("Web app '{0}' should not be packaged", webApp); - continue; - } - - BuildContext.CakeContext.LogSeparator("Packaging web app '{0}'", webApp); - - var projectFileName = System.IO.Path.Combine(".", "src", webApp, $"{webApp}.csproj"); - - var outputDirectory = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, webApp); - CakeContext.Information("Output directory: '{0}'", outputDirectory); - - CakeContext.Information("1) Using 'dotnet publish' to package '{0}'", webApp); - - var msBuildSettings = new DotNetMSBuildSettings(); - - msBuildSettings.WithProperty("PackageOutputPath", outputDirectory); - msBuildSettings.WithProperty("ConfigurationName", BuildContext.General.Solution.ConfigurationName); - msBuildSettings.WithProperty("PackageVersion", BuildContext.General.Version.NuGet); - - var publishSettings = new DotNetPublishSettings - { - MSBuildSettings = msBuildSettings, - OutputDirectory = outputDirectory, - Configuration = BuildContext.General.Solution.ConfigurationName - }; - - CakeContext.DotNetPublish(projectFileName, publishSettings); - - CakeContext.Information("2) Using 'octo pack' to package '{0}'", webApp); - - var toolSettings = new DotNetToolSettings - { - }; - - var octoPackCommand = string.Format("--id {0} --version {1} --basePath {0}", webApp, BuildContext.General.Version.NuGet); - CakeContext.DotNetTool(outputDirectory, "octo pack", octoPackCommand, toolSettings); - - BuildContext.CakeContext.LogSeparator(); - } - } - - public override async Task DeployAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var webApp in BuildContext.Web.Items) - { - if (!ShouldDeployProject(BuildContext, webApp)) - { - CakeContext.Information("Web app '{0}' should not be deployed", webApp); - continue; - } - - BuildContext.CakeContext.LogSeparator("Deploying web app '{0}'", webApp); - - var packageToPush = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, string.Format("{0}.{1}.nupkg", webApp, BuildContext.General.Version.NuGet)); - var octopusRepositoryUrl = BuildContext.OctopusDeploy.GetRepositoryUrl(webApp); - var octopusRepositoryApiKey = BuildContext.OctopusDeploy.GetRepositoryApiKey(webApp); - var octopusDeploymentTarget = BuildContext.OctopusDeploy.GetDeploymentTarget(webApp); - - CakeContext.Information("1) Pushing Octopus package"); - - CakeContext.OctoPush(octopusRepositoryUrl, octopusRepositoryApiKey, packageToPush, new OctopusPushSettings - { - ReplaceExisting = true, - }); - - CakeContext.Information("2) Creating release '{0}' in Octopus Deploy", BuildContext.General.Version.NuGet); - - CakeContext.OctoCreateRelease(webApp, new CreateReleaseSettings - { - Server = octopusRepositoryUrl, - ApiKey = octopusRepositoryApiKey, - ReleaseNumber = BuildContext.General.Version.NuGet, - DefaultPackageVersion = BuildContext.General.Version.NuGet, - IgnoreExisting = true - }); - - CakeContext.Information("3) Deploying release '{0}'", BuildContext.General.Version.NuGet); - - CakeContext.OctoDeployRelease(octopusRepositoryUrl, octopusRepositoryApiKey, webApp, octopusDeploymentTarget, - BuildContext.General.Version.NuGet, new OctopusDeployReleaseDeploymentSettings - { - ShowProgress = true, - WaitForDeployment = true, - DeploymentTimeout = TimeSpan.FromMinutes(5), - CancelOnTimeout = true, - GuidedFailure = true, - Force = true, - NoRawLog = true, - }); - - await BuildContext.Notifications.NotifyAsync(webApp, string.Format("Deployed to Octopus Deploy"), TargetType.WebApp); - } - } - - public override async Task FinalizeAsync() - { - - } +#l "apps-web-variables.cake" +#l "lib-octopusdeploy.cake" + +#addin "nuget:?package=Microsoft.Azure.KeyVault.Core&version=3.0.5" +#addin "nuget:?package=WindowsAzure.Storage&version=9.3.3" + +//------------------------------------------------------------- + +public class WebProcessor : ProcessorBase +{ + public WebProcessor(BuildContext buildContext) + : base(buildContext) + { + + } + + public override bool HasItems() + { + return BuildContext.Web.Items.Count > 0; + } + + public override async Task PrepareAsync() + { + if (!HasItems()) + { + return; + } + + // Check whether projects should be processed, `.ToList()` + // is required to prevent issues with foreach + foreach (var webApp in BuildContext.Web.Items.ToList()) + { + if (!ShouldProcessProject(BuildContext, webApp)) + { + BuildContext.Web.Items.Remove(webApp); + } + } + } + + public override async Task UpdateInfoAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var webApp in BuildContext.Web.Items) + { + CakeContext.Information("Updating version for web app '{0}'", webApp); + + var projectFileName = GetProjectFileName(BuildContext, webApp); + + CakeContext.TransformConfig(projectFileName, new TransformationCollection + { + { "Project/PropertyGroup/PackageVersion", BuildContext.General.Version.NuGet } + }); + } + } + + public override async Task BuildAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var webApp in BuildContext.Web.Items) + { + BuildContext.CakeContext.LogSeparator("Building web app '{0}'", webApp); + + var projectFileName = GetProjectFileName(BuildContext, webApp); + + var msBuildSettings = new MSBuildSettings + { + Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic + ToolVersion = MSBuildToolVersion.Default, + Configuration = BuildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, webApp, "build"); + + // Always disable SourceLink + msBuildSettings.WithProperty("EnableSourceLink", "false"); + + RunMsBuild(BuildContext, webApp, projectFileName, msBuildSettings, "build"); + } + } + + public override async Task PackageAsync() + { + if (!HasItems()) + { + return; + } + + // For package documentation using Octopus Deploy, see https://octopus.com/docs/deployment-examples/deploying-asp.net-core-web-applications + + foreach (var webApp in BuildContext.Web.Items) + { + if (!ShouldPackageProject(BuildContext, webApp)) + { + CakeContext.Information("Web app '{0}' should not be packaged", webApp); + continue; + } + + BuildContext.CakeContext.LogSeparator("Packaging web app '{0}'", webApp); + + var projectFileName = System.IO.Path.Combine(".", "src", webApp, $"{webApp}.csproj"); + + var outputDirectory = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, webApp); + CakeContext.Information("Output directory: '{0}'", outputDirectory); + + CakeContext.Information("1) Using 'dotnet publish' to package '{0}'", webApp); + + var msBuildSettings = new DotNetMSBuildSettings(); + + msBuildSettings.WithProperty("PackageOutputPath", outputDirectory); + msBuildSettings.WithProperty("ConfigurationName", BuildContext.General.Solution.ConfigurationName); + msBuildSettings.WithProperty("PackageVersion", BuildContext.General.Version.NuGet); + + var publishSettings = new DotNetPublishSettings + { + MSBuildSettings = msBuildSettings, + OutputDirectory = outputDirectory, + Configuration = BuildContext.General.Solution.ConfigurationName + }; + + CakeContext.DotNetPublish(projectFileName, publishSettings); + + CakeContext.Information("2) Using 'octo pack' to package '{0}'", webApp); + + var toolSettings = new DotNetToolSettings + { + }; + + var octoPackCommand = string.Format("--id {0} --version {1} --basePath {0}", webApp, BuildContext.General.Version.NuGet); + CakeContext.DotNetTool(outputDirectory, "octo pack", octoPackCommand, toolSettings); + + BuildContext.CakeContext.LogSeparator(); + } + } + + public override async Task DeployAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var webApp in BuildContext.Web.Items) + { + if (!ShouldDeployProject(BuildContext, webApp)) + { + CakeContext.Information("Web app '{0}' should not be deployed", webApp); + continue; + } + + BuildContext.CakeContext.LogSeparator("Deploying web app '{0}'", webApp); + + var packageToPush = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, string.Format("{0}.{1}.nupkg", webApp, BuildContext.General.Version.NuGet)); + var octopusRepositoryUrl = BuildContext.OctopusDeploy.GetRepositoryUrl(webApp); + var octopusRepositoryApiKey = BuildContext.OctopusDeploy.GetRepositoryApiKey(webApp); + var octopusDeploymentTarget = BuildContext.OctopusDeploy.GetDeploymentTarget(webApp); + + CakeContext.Information("1) Pushing Octopus package"); + + CakeContext.OctoPush(octopusRepositoryUrl, octopusRepositoryApiKey, packageToPush, new OctopusPushSettings + { + ReplaceExisting = true, + }); + + CakeContext.Information("2) Creating release '{0}' in Octopus Deploy", BuildContext.General.Version.NuGet); + + CakeContext.OctoCreateRelease(webApp, new CreateReleaseSettings + { + Server = octopusRepositoryUrl, + ApiKey = octopusRepositoryApiKey, + ReleaseNumber = BuildContext.General.Version.NuGet, + DefaultPackageVersion = BuildContext.General.Version.NuGet, + IgnoreExisting = true + }); + + CakeContext.Information("3) Deploying release '{0}'", BuildContext.General.Version.NuGet); + + CakeContext.OctoDeployRelease(octopusRepositoryUrl, octopusRepositoryApiKey, webApp, octopusDeploymentTarget, + BuildContext.General.Version.NuGet, new OctopusDeployReleaseDeploymentSettings + { + ShowProgress = true, + WaitForDeployment = true, + DeploymentTimeout = TimeSpan.FromMinutes(5), + CancelOnTimeout = true, + GuidedFailure = true, + Force = true, + NoRawLog = true, + }); + + await BuildContext.Notifications.NotifyAsync(webApp, string.Format("Deployed to Octopus Deploy"), TargetType.WebApp); + } + } + + public override async Task FinalizeAsync() + { + + } } \ No newline at end of file diff --git a/deployment/cake/apps-web-variables.cake b/deployment/cake/apps-web-variables.cake index 2f7b61af..5621d686 100644 --- a/deployment/cake/apps-web-variables.cake +++ b/deployment/cake/apps-web-variables.cake @@ -1,49 +1,49 @@ -#l "./buildserver.cake" - -//------------------------------------------------------------- - -public class WebContext : BuildContextWithItemsBase -{ - public WebContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - protected override void ValidateContext() - { - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' web projects"); - } -} - -//------------------------------------------------------------- - -private WebContext InitializeWebContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new WebContext(parentBuildContext) - { - Items = WebApps ?? new List() - }; - - return data; -} - -//------------------------------------------------------------- - -List _webApps; - -public List WebApps -{ - get - { - if (_webApps is null) - { - _webApps = new List(); - } - - return _webApps; - } +#l "./buildserver.cake" + +//------------------------------------------------------------- + +public class WebContext : BuildContextWithItemsBase +{ + public WebContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + protected override void ValidateContext() + { + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' web projects"); + } +} + +//------------------------------------------------------------- + +private WebContext InitializeWebContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new WebContext(parentBuildContext) + { + Items = WebApps ?? new List() + }; + + return data; +} + +//------------------------------------------------------------- + +List _webApps; + +public List WebApps +{ + get + { + if (_webApps is null) + { + _webApps = new List(); + } + + return _webApps; + } } \ No newline at end of file diff --git a/deployment/cake/apps-wpf-tasks.cake b/deployment/cake/apps-wpf-tasks.cake index 44ea8a45..e5e28f45 100644 --- a/deployment/cake/apps-wpf-tasks.cake +++ b/deployment/cake/apps-wpf-tasks.cake @@ -1,257 +1,257 @@ -#l "apps-wpf-variables.cake" - -#tool "nuget:?package=AzureStorageSync&version=2.0.0-alpha0039&prerelease" - -//------------------------------------------------------------- - -public class WpfProcessor : ProcessorBase -{ - public WpfProcessor(BuildContext buildContext) - : base(buildContext) - { - } - - public override bool HasItems() - { - return BuildContext.Wpf.Items.Count > 0; - } - - public override async Task PrepareAsync() - { - if (!HasItems()) - { - return; - } - - // Check whether projects should be processed, `.ToList()` - // is required to prevent issues with foreach - foreach (var wpfApp in BuildContext.Wpf.Items.ToList()) - { - if (!ShouldProcessProject(BuildContext, wpfApp)) - { - BuildContext.Wpf.Items.Remove(wpfApp); - } - } - } - - public override async Task UpdateInfoAsync() - { - if (!HasItems()) - { - return; - } - - // No specific implementation required for now - } - - public override async Task BuildAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var wpfApp in BuildContext.Wpf.Items) - { - BuildContext.CakeContext.LogSeparator("Building WPF app '{0}'", wpfApp); - - var projectFileName = GetProjectFileName(BuildContext, wpfApp); - - var channelSuffix = BuildContext.Installer.GetDeploymentChannelSuffix(); - - var sourceFileName = System.IO.Path.Combine(".", "design", "logo", $"logo{channelSuffix}.ico"); - if (BuildContext.CakeContext.FileExists(sourceFileName)) - { - CakeContext.Information("Enforcing channel specific icon '{0}'", sourceFileName); - - var projectDirectory = GetProjectDirectory(wpfApp); - var targetFileName = System.IO.Path.Combine(projectDirectory, "Resources", "Icons", "logo.ico"); - - BuildContext.CakeContext.CopyFile(sourceFileName, targetFileName); - } - - var msBuildSettings = new MSBuildSettings - { - Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic - ToolVersion = MSBuildToolVersion.Default, - Configuration = BuildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, wpfApp, "build"); - - // Always disable SourceLink - msBuildSettings.WithProperty("EnableSourceLink", "false"); - - RunMsBuild(BuildContext, wpfApp, projectFileName, msBuildSettings, "build"); - } - } - - public override async Task PackageAsync() - { - if (!HasItems()) - { - return; - } - - if (string.IsNullOrWhiteSpace(BuildContext.Wpf.DeploymentsShare)) - { - CakeContext.Warning("DeploymentsShare variable is not set, cannot package WPF apps"); - return; - } - - var channels = new List(); - - if (BuildContext.General.IsOfficialBuild) - { - // Note: we used to deploy stable to stable, beta and alpha, but want to keep things separated now - channels.Add("stable"); - } - else if (BuildContext.General.IsBetaBuild) - { - // Note: we used to deploy beta to beta and alpha, but want to keep things separated now - channels.Add("beta"); - } - else if (BuildContext.General.IsAlphaBuild) - { - // Single channel - channels.Add("alpha"); - } - else - { - // Unknown build type, just just a single channel - channels.Add(BuildContext.Wpf.Channel); - } - - CakeContext.Information($"Found '{channels.Count}' target channels"); - - foreach (var wpfApp in BuildContext.Wpf.Items) - { - if (!ShouldPackageProject(BuildContext, wpfApp)) - { - CakeContext.Information($"WPF app '{wpfApp}' should not be packaged"); - continue; - } - - var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(wpfApp); - - CakeContext.Information($"Using deployment share '{deploymentShare}' for WPF app '{wpfApp}'"); - - System.IO.Directory.CreateDirectory(deploymentShare); - - CakeContext.Information($"Deleting unnecessary files for WPF app '{wpfApp}'"); - - var outputDirectory = GetProjectOutputDirectory(BuildContext, wpfApp); - var extensionsToDelete = new [] { ".pdb", ".RoslynCA.json" }; - - foreach (var extensionToDelete in extensionsToDelete) - { - var searchPattern = $"{outputDirectory}/**/*{extensionToDelete}"; - var filesToDelete = CakeContext.GetFiles(searchPattern); - - CakeContext.Information("Deleting '{0}' files using search pattern '{1}'", filesToDelete.Count, searchPattern); - - CakeContext.DeleteFiles(filesToDelete); - } - - // We know we *highly likely* need to sign, so try doing this upfront - if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) - { - BuildContext.CakeContext.Information("Searching for packagable files to sign:"); - - var projectFilesToSign = new List(); - - var exeSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{wpfApp}/**/*.exe"; - BuildContext.CakeContext.Information($" - {exeSignFilesSearchPattern}"); - projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(exeSignFilesSearchPattern)); - - var dllSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{wpfApp}/**/*.dll"; - BuildContext.CakeContext.Information($" - {dllSignFilesSearchPattern}"); - projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(dllSignFilesSearchPattern)); - - var signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", BuildContext.General.CodeSign.TimeStampUri, - BuildContext.General.CodeSign.CertificateSubjectName, BuildContext.General.CodeSign.HashAlgorithm); - - SignFiles(BuildContext, signToolCommand, projectFilesToSign); - } - else - { - BuildContext.CakeContext.Warning("No signing certificate subject name provided, not signing any files"); - } - - foreach (var channel in channels) - { - CakeContext.Information("Packaging app '{0}' for channel '{1}'", wpfApp, channel); - - var deploymentShareForChannel = System.IO.Path.Combine(deploymentShare, channel); - System.IO.Directory.CreateDirectory(deploymentShareForChannel); - - await BuildContext.Installer.PackageAsync(wpfApp, channel); - } - } - } - - public override async Task DeployAsync() - { - if (!HasItems()) - { - return; - } - - var azureConnectionString = BuildContext.Wpf.AzureDeploymentsStorageConnectionString; - if (string.IsNullOrWhiteSpace(azureConnectionString)) - { - CakeContext.Warning("Skipping deployments of WPF apps because not Azure deployments storage connection string was specified"); - return; - } - - var azureStorageSyncExes = CakeContext.GetFiles("./tools/AzureStorageSync*/**/AzureStorageSync.exe"); - var azureStorageSyncExe = azureStorageSyncExes.LastOrDefault(); - if (azureStorageSyncExe is null) - { - throw new Exception("Can't find the AzureStorageSync tool that should have been installed via this script"); - } - - foreach (var wpfApp in BuildContext.Wpf.Items) - { - if (!ShouldDeployProject(BuildContext, wpfApp)) - { - CakeContext.Information($"WPF app '{wpfApp}' should not be deployed"); - continue; - } - - BuildContext.CakeContext.LogSeparator($"Deploying WPF app '{wpfApp}'"); - - // TODO: Respect the deploy settings per category, requires changes to AzureStorageSync - if (!BuildContext.Wpf.DeployUpdatesToAlphaChannel || - !BuildContext.Wpf.DeployUpdatesToBetaChannel || - !BuildContext.Wpf.DeployUpdatesToStableChannel || - !BuildContext.Wpf.DeployInstallers) - { - throw new Exception("Not deploying a specific channel is not yet supported, please implement"); - } - - //%DeploymentsShare%\%ProjectName% /%ProjectName% -c %AzureDeploymentsStorageConnectionString% - var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(wpfApp); - var projectSlug = GetProjectSlug(wpfApp, "-"); - - var exitCode = CakeContext.StartProcess(azureStorageSyncExe, new ProcessSettings - { - Arguments = $"{deploymentShare} /{projectSlug} -c {azureConnectionString}" - }); - - if (exitCode != 0) - { - throw new Exception($"Received unexpected exit code '{exitCode}' for WPF app '{wpfApp}'"); - } - - await BuildContext.Notifications.NotifyAsync(wpfApp, string.Format("Deployed to target"), TargetType.WpfApp); - } - } - - public override async Task FinalizeAsync() - { - - } -} +#l "apps-wpf-variables.cake" + +#tool "nuget:?package=AzureStorageSync&version=2.0.0-alpha0039&prerelease" + +//------------------------------------------------------------- + +public class WpfProcessor : ProcessorBase +{ + public WpfProcessor(BuildContext buildContext) + : base(buildContext) + { + } + + public override bool HasItems() + { + return BuildContext.Wpf.Items.Count > 0; + } + + public override async Task PrepareAsync() + { + if (!HasItems()) + { + return; + } + + // Check whether projects should be processed, `.ToList()` + // is required to prevent issues with foreach + foreach (var wpfApp in BuildContext.Wpf.Items.ToList()) + { + if (!ShouldProcessProject(BuildContext, wpfApp)) + { + BuildContext.Wpf.Items.Remove(wpfApp); + } + } + } + + public override async Task UpdateInfoAsync() + { + if (!HasItems()) + { + return; + } + + // No specific implementation required for now + } + + public override async Task BuildAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var wpfApp in BuildContext.Wpf.Items) + { + BuildContext.CakeContext.LogSeparator("Building WPF app '{0}'", wpfApp); + + var projectFileName = GetProjectFileName(BuildContext, wpfApp); + + var channelSuffix = BuildContext.Installer.GetDeploymentChannelSuffix(); + + var sourceFileName = System.IO.Path.Combine(".", "design", "logo", $"logo{channelSuffix}.ico"); + if (BuildContext.CakeContext.FileExists(sourceFileName)) + { + CakeContext.Information("Enforcing channel specific icon '{0}'", sourceFileName); + + var projectDirectory = GetProjectDirectory(wpfApp); + var targetFileName = System.IO.Path.Combine(projectDirectory, "Resources", "Icons", "logo.ico"); + + BuildContext.CakeContext.CopyFile(sourceFileName, targetFileName); + } + + var msBuildSettings = new MSBuildSettings + { + Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic + ToolVersion = MSBuildToolVersion.Default, + Configuration = BuildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, wpfApp, "build"); + + // Always disable SourceLink + msBuildSettings.WithProperty("EnableSourceLink", "false"); + + RunMsBuild(BuildContext, wpfApp, projectFileName, msBuildSettings, "build"); + } + } + + public override async Task PackageAsync() + { + if (!HasItems()) + { + return; + } + + if (string.IsNullOrWhiteSpace(BuildContext.Wpf.DeploymentsShare)) + { + CakeContext.Warning("DeploymentsShare variable is not set, cannot package WPF apps"); + return; + } + + var channels = new List(); + + if (BuildContext.General.IsOfficialBuild) + { + // Note: we used to deploy stable to stable, beta and alpha, but want to keep things separated now + channels.Add("stable"); + } + else if (BuildContext.General.IsBetaBuild) + { + // Note: we used to deploy beta to beta and alpha, but want to keep things separated now + channels.Add("beta"); + } + else if (BuildContext.General.IsAlphaBuild) + { + // Single channel + channels.Add("alpha"); + } + else + { + // Unknown build type, just just a single channel + channels.Add(BuildContext.Wpf.Channel); + } + + CakeContext.Information($"Found '{channels.Count}' target channels"); + + foreach (var wpfApp in BuildContext.Wpf.Items) + { + if (!ShouldPackageProject(BuildContext, wpfApp)) + { + CakeContext.Information($"WPF app '{wpfApp}' should not be packaged"); + continue; + } + + var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(wpfApp); + + CakeContext.Information($"Using deployment share '{deploymentShare}' for WPF app '{wpfApp}'"); + + System.IO.Directory.CreateDirectory(deploymentShare); + + CakeContext.Information($"Deleting unnecessary files for WPF app '{wpfApp}'"); + + var outputDirectory = GetProjectOutputDirectory(BuildContext, wpfApp); + var extensionsToDelete = new [] { ".pdb", ".RoslynCA.json" }; + + foreach (var extensionToDelete in extensionsToDelete) + { + var searchPattern = $"{outputDirectory}/**/*{extensionToDelete}"; + var filesToDelete = CakeContext.GetFiles(searchPattern); + + CakeContext.Information("Deleting '{0}' files using search pattern '{1}'", filesToDelete.Count, searchPattern); + + CakeContext.DeleteFiles(filesToDelete); + } + + // We know we *highly likely* need to sign, so try doing this upfront + if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) + { + BuildContext.CakeContext.Information("Searching for packagable files to sign:"); + + var projectFilesToSign = new List(); + + var exeSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{wpfApp}/**/*.exe"; + BuildContext.CakeContext.Information($" - {exeSignFilesSearchPattern}"); + projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(exeSignFilesSearchPattern)); + + var dllSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{wpfApp}/**/*.dll"; + BuildContext.CakeContext.Information($" - {dllSignFilesSearchPattern}"); + projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(dllSignFilesSearchPattern)); + + var signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", BuildContext.General.CodeSign.TimeStampUri, + BuildContext.General.CodeSign.CertificateSubjectName, BuildContext.General.CodeSign.HashAlgorithm); + + SignFiles(BuildContext, signToolCommand, projectFilesToSign); + } + else + { + BuildContext.CakeContext.Warning("No signing certificate subject name provided, not signing any files"); + } + + foreach (var channel in channels) + { + CakeContext.Information("Packaging app '{0}' for channel '{1}'", wpfApp, channel); + + var deploymentShareForChannel = System.IO.Path.Combine(deploymentShare, channel); + System.IO.Directory.CreateDirectory(deploymentShareForChannel); + + await BuildContext.Installer.PackageAsync(wpfApp, channel); + } + } + } + + public override async Task DeployAsync() + { + if (!HasItems()) + { + return; + } + + var azureConnectionString = BuildContext.Wpf.AzureDeploymentsStorageConnectionString; + if (string.IsNullOrWhiteSpace(azureConnectionString)) + { + CakeContext.Warning("Skipping deployments of WPF apps because not Azure deployments storage connection string was specified"); + return; + } + + var azureStorageSyncExes = CakeContext.GetFiles("./tools/AzureStorageSync*/**/AzureStorageSync.exe"); + var azureStorageSyncExe = azureStorageSyncExes.LastOrDefault(); + if (azureStorageSyncExe is null) + { + throw new Exception("Can't find the AzureStorageSync tool that should have been installed via this script"); + } + + foreach (var wpfApp in BuildContext.Wpf.Items) + { + if (!ShouldDeployProject(BuildContext, wpfApp)) + { + CakeContext.Information($"WPF app '{wpfApp}' should not be deployed"); + continue; + } + + BuildContext.CakeContext.LogSeparator($"Deploying WPF app '{wpfApp}'"); + + // TODO: Respect the deploy settings per category, requires changes to AzureStorageSync + if (!BuildContext.Wpf.DeployUpdatesToAlphaChannel || + !BuildContext.Wpf.DeployUpdatesToBetaChannel || + !BuildContext.Wpf.DeployUpdatesToStableChannel || + !BuildContext.Wpf.DeployInstallers) + { + throw new Exception("Not deploying a specific channel is not yet supported, please implement"); + } + + //%DeploymentsShare%\%ProjectName% /%ProjectName% -c %AzureDeploymentsStorageConnectionString% + var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(wpfApp); + var projectSlug = GetProjectSlug(wpfApp, "-"); + + var exitCode = CakeContext.StartProcess(azureStorageSyncExe, new ProcessSettings + { + Arguments = $"{deploymentShare} /{projectSlug} -c {azureConnectionString}" + }); + + if (exitCode != 0) + { + throw new Exception($"Received unexpected exit code '{exitCode}' for WPF app '{wpfApp}'"); + } + + await BuildContext.Notifications.NotifyAsync(wpfApp, string.Format("Deployed to target"), TargetType.WpfApp); + } + } + + public override async Task FinalizeAsync() + { + + } +} diff --git a/deployment/cake/apps-wpf-variables.cake b/deployment/cake/apps-wpf-variables.cake index 6e8356c3..2a26230d 100644 --- a/deployment/cake/apps-wpf-variables.cake +++ b/deployment/cake/apps-wpf-variables.cake @@ -1,97 +1,97 @@ -#l "buildserver.cake" - -//------------------------------------------------------------- - -public class WpfContext : BuildContextWithItemsBase -{ - public WpfContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - - public string DeploymentsShare { get; set; } - public string Channel { get; set; } - public bool AppendDeploymentChannelSuffix { get; set; } - public bool UpdateDeploymentsShare { get; set; } - public string AzureDeploymentsStorageConnectionString { get; set; } - - public bool GenerateDeploymentCatalog { get; set; } - public bool GroupUpdatesByMajorVersion { get; set; } - public bool DeployUpdatesToAlphaChannel { get; set; } - public bool DeployUpdatesToBetaChannel { get; set; } - public bool DeployUpdatesToStableChannel { get; set; } - public bool DeployInstallers { get; set; } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' wpf projects"); - - CakeContext.Information($"Generate Deployment Catalog: '{GenerateDeploymentCatalog}'"); - CakeContext.Information($"Group updates by major version: '{GroupUpdatesByMajorVersion}'"); - CakeContext.Information($"Deploy updates to alpha channel: '{DeployUpdatesToAlphaChannel}'"); - CakeContext.Information($"Deploy updates to beta channel: '{DeployUpdatesToBetaChannel}'"); - CakeContext.Information($"Deploy updates to stable channel: '{DeployUpdatesToStableChannel}'"); - CakeContext.Information($"Deploy installers: '{DeployInstallers}'"); - } - - public string GetDeploymentShareForProject(string projectName) - { - var projectSlug = GetProjectSlug(projectName, "-"); - var deploymentShare = System.IO.Path.Combine(DeploymentsShare, projectSlug); - - return deploymentShare; - } -} - -//------------------------------------------------------------- - -private WpfContext InitializeWpfContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new WpfContext(parentBuildContext) - { - Items = WpfApps ?? new List(), - DeploymentsShare = buildContext.BuildServer.GetVariable("DeploymentsShare", showValue: true), - Channel = buildContext.BuildServer.GetVariable("Channel", showValue: true), - AppendDeploymentChannelSuffix = buildContext.BuildServer.GetVariableAsBool("AppendDeploymentChannelSuffix", false, showValue: true), - UpdateDeploymentsShare = buildContext.BuildServer.GetVariableAsBool("UpdateDeploymentsShare", true, showValue: true), - AzureDeploymentsStorageConnectionString = buildContext.BuildServer.GetVariable("AzureDeploymentsStorageConnectionString"), - GenerateDeploymentCatalog = buildContext.BuildServer.GetVariableAsBool("WpfGenerateDeploymentCatalog", true, showValue: true), - GroupUpdatesByMajorVersion = buildContext.BuildServer.GetVariableAsBool("WpfGroupUpdatesByMajorVersion", false, showValue: true), - DeployUpdatesToAlphaChannel = buildContext.BuildServer.GetVariableAsBool("WpfDeployUpdatesToAlphaChannel", true, showValue: true), - DeployUpdatesToBetaChannel = buildContext.BuildServer.GetVariableAsBool("WpfDeployUpdatesToBetaChannel", true, showValue: true), - DeployUpdatesToStableChannel = buildContext.BuildServer.GetVariableAsBool("WpfDeployUpdatesToStableChannel", true, showValue: true), - DeployInstallers = buildContext.BuildServer.GetVariableAsBool("WpfDeployInstallers", true, showValue: true), - }; - - if (string.IsNullOrWhiteSpace(data.Channel)) - { - data.Channel = DetermineChannel(buildContext.General); - - data.CakeContext.Information($"Determined channel '{data.Channel}' for wpf projects"); - } - - return data; -} - -//------------------------------------------------------------- - -List _wpfApps; - -public List WpfApps -{ - get - { - if (_wpfApps is null) - { - _wpfApps = new List(); - } - - return _wpfApps; - } +#l "buildserver.cake" + +//------------------------------------------------------------- + +public class WpfContext : BuildContextWithItemsBase +{ + public WpfContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + + public string DeploymentsShare { get; set; } + public string Channel { get; set; } + public bool AppendDeploymentChannelSuffix { get; set; } + public bool UpdateDeploymentsShare { get; set; } + public string AzureDeploymentsStorageConnectionString { get; set; } + + public bool GenerateDeploymentCatalog { get; set; } + public bool GroupUpdatesByMajorVersion { get; set; } + public bool DeployUpdatesToAlphaChannel { get; set; } + public bool DeployUpdatesToBetaChannel { get; set; } + public bool DeployUpdatesToStableChannel { get; set; } + public bool DeployInstallers { get; set; } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' wpf projects"); + + CakeContext.Information($"Generate Deployment Catalog: '{GenerateDeploymentCatalog}'"); + CakeContext.Information($"Group updates by major version: '{GroupUpdatesByMajorVersion}'"); + CakeContext.Information($"Deploy updates to alpha channel: '{DeployUpdatesToAlphaChannel}'"); + CakeContext.Information($"Deploy updates to beta channel: '{DeployUpdatesToBetaChannel}'"); + CakeContext.Information($"Deploy updates to stable channel: '{DeployUpdatesToStableChannel}'"); + CakeContext.Information($"Deploy installers: '{DeployInstallers}'"); + } + + public string GetDeploymentShareForProject(string projectName) + { + var projectSlug = GetProjectSlug(projectName, "-"); + var deploymentShare = System.IO.Path.Combine(DeploymentsShare, projectSlug); + + return deploymentShare; + } +} + +//------------------------------------------------------------- + +private WpfContext InitializeWpfContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new WpfContext(parentBuildContext) + { + Items = WpfApps ?? new List(), + DeploymentsShare = buildContext.BuildServer.GetVariable("DeploymentsShare", showValue: true), + Channel = buildContext.BuildServer.GetVariable("Channel", showValue: true), + AppendDeploymentChannelSuffix = buildContext.BuildServer.GetVariableAsBool("AppendDeploymentChannelSuffix", false, showValue: true), + UpdateDeploymentsShare = buildContext.BuildServer.GetVariableAsBool("UpdateDeploymentsShare", true, showValue: true), + AzureDeploymentsStorageConnectionString = buildContext.BuildServer.GetVariable("AzureDeploymentsStorageConnectionString"), + GenerateDeploymentCatalog = buildContext.BuildServer.GetVariableAsBool("WpfGenerateDeploymentCatalog", true, showValue: true), + GroupUpdatesByMajorVersion = buildContext.BuildServer.GetVariableAsBool("WpfGroupUpdatesByMajorVersion", false, showValue: true), + DeployUpdatesToAlphaChannel = buildContext.BuildServer.GetVariableAsBool("WpfDeployUpdatesToAlphaChannel", true, showValue: true), + DeployUpdatesToBetaChannel = buildContext.BuildServer.GetVariableAsBool("WpfDeployUpdatesToBetaChannel", true, showValue: true), + DeployUpdatesToStableChannel = buildContext.BuildServer.GetVariableAsBool("WpfDeployUpdatesToStableChannel", true, showValue: true), + DeployInstallers = buildContext.BuildServer.GetVariableAsBool("WpfDeployInstallers", true, showValue: true), + }; + + if (string.IsNullOrWhiteSpace(data.Channel)) + { + data.Channel = DetermineChannel(buildContext.General); + + data.CakeContext.Information($"Determined channel '{data.Channel}' for wpf projects"); + } + + return data; +} + +//------------------------------------------------------------- + +List _wpfApps; + +public List WpfApps +{ + get + { + if (_wpfApps is null) + { + _wpfApps = new List(); + } + + return _wpfApps; + } } \ No newline at end of file diff --git a/deployment/cake/buildserver-continuaci.cake b/deployment/cake/buildserver-continuaci.cake index 44d390f8..863ccb18 100644 --- a/deployment/cake/buildserver-continuaci.cake +++ b/deployment/cake/buildserver-continuaci.cake @@ -1,182 +1,182 @@ -public class ContinuaCIBuildServer : BuildServerBase -{ - public ContinuaCIBuildServer(ICakeContext cakeContext) - : base(cakeContext) - { - } - - //------------------------------------------------------------- - - public override async Task OnTestFailedAsync() - { - await ImportUnitTestsAsync(); - } - - //------------------------------------------------------------- - - public override async Task AfterTestAsync() - { - await ImportUnitTestsAsync(); - } - - //------------------------------------------------------------- - - private async Task ImportUnitTestsAsync() - { - foreach (var project in BuildContext.Tests.Items) - { - await ImportTestFilesAsync(project); - } - } - - //------------------------------------------------------------- - - private async Task ImportTestFilesAsync(string projectName) - { - var continuaCIContext = GetContinuaCIContext(); - if (!continuaCIContext.IsRunningOnContinuaCI) - { - return; - } - - CakeContext.Warning($"Importing test results for '{projectName}'"); - - var testResultsDirectory = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, "testresults"); - - if (!CakeContext.DirectoryExists(testResultsDirectory)) - { - CakeContext.Warning("No test results directory"); - return; - } - - var type = string.Empty; - var importType = string.Empty; - - if (IsNUnitTestProject(BuildContext, projectName)) - { - type = "nunit"; - importType = "nunit"; - } - - if (IsXUnitTestProject(BuildContext, projectName)) - { - type = "xunit"; - importType = "mstest"; // Xml type is different - } - - if (string.IsNullOrWhiteSpace(type)) - { - CakeContext.Warning("Could not find test project type"); - return; - } - - CakeContext.Warning($"Determined project type '{type}'"); - - var cakeFilePattern = System.IO.Path.Combine(testResultsDirectory, projectName, "*.xml"); - - CakeContext.Warning($"Using pattern '{cakeFilePattern}'"); - - var testResultsFiles = CakeContext.GetFiles(cakeFilePattern); - if (!testResultsFiles.Any()) - { - CakeContext.Warning($"No test result file found using '{cakeFilePattern}'"); - return; - } - - var continuaCiFilePattern = System.IO.Path.Combine(testResultsDirectory, "**.xml"); - - CakeContext.Information($"Importing test results from using '{continuaCiFilePattern}' using import type '{importType}'"); - - var message = $"@@continua[importUnitTestResults type='{importType}' filePatterns='{cakeFilePattern}']"; - WriteIntegration(message); - } - - //------------------------------------------------------------- - - public override async Task PinBuildAsync(string comment) - { - var continuaCIContext = GetContinuaCIContext(); - if (!continuaCIContext.IsRunningOnContinuaCI) - { - return; - } - - CakeContext.Information("Pinning build in Continua CI"); - - var message = string.Format("@@continua[pinBuild comment='{0}' appendComment='{1}']", - comment, !string.IsNullOrWhiteSpace(comment)); - WriteIntegration(message); - } - - //------------------------------------------------------------- - - public override async Task SetVersionAsync(string version) - { - var continuaCIContext = GetContinuaCIContext(); - if (!continuaCIContext.IsRunningOnContinuaCI) - { - return; - } - - CakeContext.Information("Setting version '{0}' in Continua CI", version); - - var message = string.Format("@@continua[setBuildVersion value='{0}']", version); - WriteIntegration(message); - } - - //------------------------------------------------------------- - - public override async Task SetVariableAsync(string variableName, string value) - { - var continuaCIContext = GetContinuaCIContext(); - if (!continuaCIContext.IsRunningOnContinuaCI) - { - return; - } - - CakeContext.Information("Setting variable '{0}' to '{1}' in Continua CI", variableName, value); - - var message = string.Format("@@continua[setVariable name='{0}' value='{1}' skipIfNotDefined='true']", variableName, value); - WriteIntegration(message); - } - - //------------------------------------------------------------- - - public override Tuple GetVariable(string variableName, string defaultValue) - { - var continuaCIContext = GetContinuaCIContext(); - if (!continuaCIContext.IsRunningOnContinuaCI) - { - return new Tuple(false, string.Empty); - } - - var exists = false; - var value = string.Empty; - - var buildServerVariables = continuaCIContext.Environment.Variable; - if (buildServerVariables.ContainsKey(variableName)) - { - CakeContext.Information("Variable '{0}' is specified via Continua CI", variableName); - - exists = true; - value = buildServerVariables[variableName]; - } - - return new Tuple(exists, value); - } - - //------------------------------------------------------------- - - private IContinuaCIProvider GetContinuaCIContext() - { - return CakeContext.ContinuaCI(); - } - - //------------------------------------------------------------- - - private void WriteIntegration(string message) - { - // Must be Console.WriteLine - CakeContext.Information(message); - } +public class ContinuaCIBuildServer : BuildServerBase +{ + public ContinuaCIBuildServer(ICakeContext cakeContext) + : base(cakeContext) + { + } + + //------------------------------------------------------------- + + public override async Task OnTestFailedAsync() + { + await ImportUnitTestsAsync(); + } + + //------------------------------------------------------------- + + public override async Task AfterTestAsync() + { + await ImportUnitTestsAsync(); + } + + //------------------------------------------------------------- + + private async Task ImportUnitTestsAsync() + { + foreach (var project in BuildContext.Tests.Items) + { + await ImportTestFilesAsync(project); + } + } + + //------------------------------------------------------------- + + private async Task ImportTestFilesAsync(string projectName) + { + var continuaCIContext = GetContinuaCIContext(); + if (!continuaCIContext.IsRunningOnContinuaCI) + { + return; + } + + CakeContext.Warning($"Importing test results for '{projectName}'"); + + var testResultsDirectory = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, "testresults"); + + if (!CakeContext.DirectoryExists(testResultsDirectory)) + { + CakeContext.Warning("No test results directory"); + return; + } + + var type = string.Empty; + var importType = string.Empty; + + if (IsNUnitTestProject(BuildContext, projectName)) + { + type = "nunit"; + importType = "nunit"; + } + + if (IsXUnitTestProject(BuildContext, projectName)) + { + type = "xunit"; + importType = "mstest"; // Xml type is different + } + + if (string.IsNullOrWhiteSpace(type)) + { + CakeContext.Warning("Could not find test project type"); + return; + } + + CakeContext.Warning($"Determined project type '{type}'"); + + var cakeFilePattern = System.IO.Path.Combine(testResultsDirectory, projectName, "*.xml"); + + CakeContext.Warning($"Using pattern '{cakeFilePattern}'"); + + var testResultsFiles = CakeContext.GetFiles(cakeFilePattern); + if (!testResultsFiles.Any()) + { + CakeContext.Warning($"No test result file found using '{cakeFilePattern}'"); + return; + } + + var continuaCiFilePattern = System.IO.Path.Combine(testResultsDirectory, "**.xml"); + + CakeContext.Information($"Importing test results from using '{continuaCiFilePattern}' using import type '{importType}'"); + + var message = $"@@continua[importUnitTestResults type='{importType}' filePatterns='{cakeFilePattern}']"; + WriteIntegration(message); + } + + //------------------------------------------------------------- + + public override async Task PinBuildAsync(string comment) + { + var continuaCIContext = GetContinuaCIContext(); + if (!continuaCIContext.IsRunningOnContinuaCI) + { + return; + } + + CakeContext.Information("Pinning build in Continua CI"); + + var message = string.Format("@@continua[pinBuild comment='{0}' appendComment='{1}']", + comment, !string.IsNullOrWhiteSpace(comment)); + WriteIntegration(message); + } + + //------------------------------------------------------------- + + public override async Task SetVersionAsync(string version) + { + var continuaCIContext = GetContinuaCIContext(); + if (!continuaCIContext.IsRunningOnContinuaCI) + { + return; + } + + CakeContext.Information("Setting version '{0}' in Continua CI", version); + + var message = string.Format("@@continua[setBuildVersion value='{0}']", version); + WriteIntegration(message); + } + + //------------------------------------------------------------- + + public override async Task SetVariableAsync(string variableName, string value) + { + var continuaCIContext = GetContinuaCIContext(); + if (!continuaCIContext.IsRunningOnContinuaCI) + { + return; + } + + CakeContext.Information("Setting variable '{0}' to '{1}' in Continua CI", variableName, value); + + var message = string.Format("@@continua[setVariable name='{0}' value='{1}' skipIfNotDefined='true']", variableName, value); + WriteIntegration(message); + } + + //------------------------------------------------------------- + + public override Tuple GetVariable(string variableName, string defaultValue) + { + var continuaCIContext = GetContinuaCIContext(); + if (!continuaCIContext.IsRunningOnContinuaCI) + { + return new Tuple(false, string.Empty); + } + + var exists = false; + var value = string.Empty; + + var buildServerVariables = continuaCIContext.Environment.Variable; + if (buildServerVariables.ContainsKey(variableName)) + { + CakeContext.Information("Variable '{0}' is specified via Continua CI", variableName); + + exists = true; + value = buildServerVariables[variableName]; + } + + return new Tuple(exists, value); + } + + //------------------------------------------------------------- + + private IContinuaCIProvider GetContinuaCIContext() + { + return CakeContext.ContinuaCI(); + } + + //------------------------------------------------------------- + + private void WriteIntegration(string message) + { + // Must be Console.WriteLine + CakeContext.Information(message); + } } \ No newline at end of file diff --git a/deployment/cake/buildserver.cake b/deployment/cake/buildserver.cake index 053d710e..b2cfc0a0 100644 --- a/deployment/cake/buildserver.cake +++ b/deployment/cake/buildserver.cake @@ -1,511 +1,511 @@ -// Customize this file when using a different build server -#l "buildserver-continuaci.cake" - -using System.Runtime.InteropServices; - -public interface IBuildServer -{ - Task PinBuildAsync(string comment); - Task SetVersionAsync(string version); - Task SetVariableAsync(string name, string value); - Tuple GetVariable(string variableName, string defaultValue); - - void SetBuildContext(BuildContext buildContext); - - Task BeforeInitializeAsync(); - Task AfterInitializeAsync(); - - Task BeforePrepareAsync(); - Task AfterPrepareAsync(); - - Task BeforeUpdateInfoAsync(); - Task AfterUpdateInfoAsync(); - - Task BeforeBuildAsync(); - Task OnBuildFailedAsync(); - Task AfterBuildAsync(); - - Task BeforeTestAsync(); - Task OnTestFailedAsync(); - Task AfterTestAsync(); - - Task BeforePackageAsync(); - Task AfterPackageAsync(); - - Task BeforeDeployAsync(); - Task AfterDeployAsync(); - - Task BeforeFinalizeAsync(); - Task AfterFinalizeAsync(); -} - -public abstract class BuildServerBase : IBuildServer -{ - protected BuildServerBase(ICakeContext cakeContext) - { - CakeContext = cakeContext; - } - - public ICakeContext CakeContext { get; private set; } - - public BuildContext BuildContext { get; private set; } - - public abstract Task PinBuildAsync(string comment); - public abstract Task SetVersionAsync(string version); - public abstract Task SetVariableAsync(string name, string value); - public abstract Tuple GetVariable(string variableName, string defaultValue); - - //------------------------------------------------------------- - - public void SetBuildContext(BuildContext buildContext) - { - BuildContext = buildContext; - } - - //------------------------------------------------------------- - - public virtual async Task BeforeInitializeAsync() - { - } - - public virtual async Task AfterInitializeAsync() - { - } - - //------------------------------------------------------------- - - public virtual async Task BeforePrepareAsync() - { - } - - public virtual async Task AfterPrepareAsync() - { - } - - //------------------------------------------------------------- - - public virtual async Task BeforeUpdateInfoAsync() - { - } - - public virtual async Task AfterUpdateInfoAsync() - { - } - - //------------------------------------------------------------- - - public virtual async Task BeforeBuildAsync() - { - } - - public virtual async Task OnBuildFailedAsync() - { - } - - public virtual async Task AfterBuildAsync() - { - } - - //------------------------------------------------------------- - - public virtual async Task BeforeTestAsync() - { - } - - public virtual async Task OnTestFailedAsync() - { - } - - public virtual async Task AfterTestAsync() - { - } - - //------------------------------------------------------------- - - public virtual async Task BeforePackageAsync() - { - } - - public virtual async Task AfterPackageAsync() - { - } - - //------------------------------------------------------------- - - public virtual async Task BeforeDeployAsync() - { - } - - public virtual async Task AfterDeployAsync() - { - } - - //------------------------------------------------------------- - - public virtual async Task BeforeFinalizeAsync() - { - } - - public virtual async Task AfterFinalizeAsync() - { - } -} - -//------------------------------------------------------------- - -public class BuildServerIntegration : IIntegration -{ - [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] - static extern uint GetPrivateProfileString( - string lpAppName, - string lpKeyName, - string lpDefault, - StringBuilder lpReturnedString, - uint nSize, - string lpFileName); - - private readonly Dictionary _parameters; - private readonly List _buildServers = new List(); - private readonly Dictionary _buildServerVariableCache = new Dictionary(); - - public BuildServerIntegration(ICakeContext cakeContext, Dictionary parameters) - { - CakeContext = cakeContext; - _parameters = parameters; - - _buildServers.Add(new ContinuaCIBuildServer(cakeContext)); - } - - public void SetBuildContext(BuildContext buildContext) - { - BuildContext = buildContext; - - foreach (var buildServer in _buildServers) - { - buildServer.SetBuildContext(buildContext); - } - } - - public BuildContext BuildContext { get; private set; } - - public ICakeContext CakeContext { get; private set; } - - //------------------------------------------------------------- - - public async Task BeforeInitializeAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.BeforeInitializeAsync(); - } - } - - public async Task AfterInitializeAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.AfterInitializeAsync(); - } - } - - //------------------------------------------------------------- - - public async Task BeforePrepareAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.BeforePrepareAsync(); - } - } - - public async Task AfterPrepareAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.AfterPrepareAsync(); - } - } - - //------------------------------------------------------------- - - public async Task BeforeUpdateInfoAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.BeforeUpdateInfoAsync(); - } - } - - public async Task AfterUpdateInfoAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.AfterUpdateInfoAsync(); - } - } - - //------------------------------------------------------------- - - public async Task BeforeBuildAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.BeforeBuildAsync(); - } - } - - public async Task OnBuildFailedAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.OnBuildFailedAsync(); - } - } - - public async Task AfterBuildAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.AfterBuildAsync(); - } - } - - //------------------------------------------------------------- - - public async Task BeforeTestAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.BeforeTestAsync(); - } - } - - public async Task OnTestFailedAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.OnTestFailedAsync(); - } - } - - public async Task AfterTestAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.AfterTestAsync(); - } - } - - //------------------------------------------------------------- - - public async Task BeforePackageAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.BeforePackageAsync(); - } - } - - public async Task AfterPackageAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.AfterPackageAsync(); - } - } - - //------------------------------------------------------------- - - public async Task BeforeDeployAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.BeforeDeployAsync(); - } - } - - public async Task AfterDeployAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.AfterDeployAsync(); - } - } - - //------------------------------------------------------------- - - public async Task BeforeFinalizeAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.BeforeFinalizeAsync(); - } - } - - public async Task AfterFinalizeAsync() - { - foreach (var buildServer in _buildServers) - { - await buildServer.AfterFinalizeAsync(); - } - } - - //------------------------------------------------------------- - - public async Task PinBuildAsync(string comment) - { - foreach (var buildServer in _buildServers) - { - await buildServer.PinBuildAsync(comment); - } - } - - //------------------------------------------------------------- - - public async Task SetVersionAsync(string version) - { - foreach (var buildServer in _buildServers) - { - await buildServer.SetVersionAsync(version); - } - } - - //------------------------------------------------------------- - - public async Task SetVariableAsync(string variableName, string value) - { - foreach (var buildServer in _buildServers) - { - await buildServer.SetVariableAsync(variableName, value); - } - } - - //------------------------------------------------------------- - - public bool GetVariableAsBool(string variableName, bool defaultValue, bool showValue = false) - { - var value = defaultValue; - - if (bool.TryParse(GetVariable(variableName, "unknown", showValue: false), out var retrievedValue)) - { - value = retrievedValue; - } - - if (showValue) - { - PrintVariableValue(variableName, value.ToString()); - } - - return value; - } - - //------------------------------------------------------------- - - public string GetVariable(string variableName, string defaultValue = null, bool showValue = false) - { - var cacheKey = string.Format("{0}__{1}", variableName ?? string.Empty, defaultValue ?? string.Empty); - - if (!_buildServerVariableCache.TryGetValue(cacheKey, out string value)) - { - value = GetVariableForCache(variableName, defaultValue); - - if (showValue) - { - PrintVariableValue(variableName, value); - } - - _buildServerVariableCache[cacheKey] = value; - } - //else - //{ - // Information("Retrieved value for '{0}' from cache", variableName); - //} - - return value; - } - - //------------------------------------------------------------- - - private string GetVariableForCache(string variableName, string defaultValue = null) - { - var argumentValue = CakeContext.Argument(variableName, "non-existing"); - if (argumentValue != "non-existing") - { - CakeContext.Information("Variable '{0}' is specified via an argument", variableName); - - return argumentValue; - } - - // Check each build server - foreach (var buildServer in _buildServers) - { - var buildServerVariable = buildServer.GetVariable(variableName, defaultValue); - if (buildServerVariable.Item1) - { - return buildServerVariable.Item2; - } - } - - var overrideFile = System.IO.Path.Combine(".", "build.cakeoverrides"); - if (System.IO.File.Exists(overrideFile)) - { - var sb = new StringBuilder(string.Empty, 256); - var lengthRead = GetPrivateProfileString("General", variableName, null, sb, (uint)sb.Capacity, overrideFile); - if (lengthRead > 0) - { - CakeContext.Information("Variable '{0}' is specified via build.cakeoverrides", variableName); - - var sbValue = sb.ToString(); - if (sbValue == "[ignore]" || - sbValue == "[empty]") - { - return string.Empty; - } - - return sbValue; - } - } - - if (CakeContext.HasEnvironmentVariable(variableName)) - { - CakeContext.Information("Variable '{0}' is specified via an environment variable", variableName); - - return CakeContext.EnvironmentVariable(variableName); - } - - if (_parameters.TryGetValue(variableName, out var parameter)) - { - CakeContext.Information("Variable '{0}' is specified via the Parameters dictionary", variableName); - - if (parameter is null) - { - return null; - } - - if (parameter is string) - { - return (string)parameter; - } - - if (parameter is Func) - { - return ((Func)parameter).Invoke(); - } - - throw new Exception(string.Format("Parameter is defined as '{0}', but that type is not supported yet...", parameter.GetType().Name)); - } - - CakeContext.Information("Variable '{0}' is not specified, returning default value", variableName); - - return defaultValue ?? string.Empty; - } - - //------------------------------------------------------------- - - private void PrintVariableValue(string variableName, string value, bool isSensitive = false) - { - var valueForLog = isSensitive ? "********" : value; - CakeContext.Information("{0}: '{1}'", variableName, valueForLog); - } -} - +// Customize this file when using a different build server +#l "buildserver-continuaci.cake" + +using System.Runtime.InteropServices; + +public interface IBuildServer +{ + Task PinBuildAsync(string comment); + Task SetVersionAsync(string version); + Task SetVariableAsync(string name, string value); + Tuple GetVariable(string variableName, string defaultValue); + + void SetBuildContext(BuildContext buildContext); + + Task BeforeInitializeAsync(); + Task AfterInitializeAsync(); + + Task BeforePrepareAsync(); + Task AfterPrepareAsync(); + + Task BeforeUpdateInfoAsync(); + Task AfterUpdateInfoAsync(); + + Task BeforeBuildAsync(); + Task OnBuildFailedAsync(); + Task AfterBuildAsync(); + + Task BeforeTestAsync(); + Task OnTestFailedAsync(); + Task AfterTestAsync(); + + Task BeforePackageAsync(); + Task AfterPackageAsync(); + + Task BeforeDeployAsync(); + Task AfterDeployAsync(); + + Task BeforeFinalizeAsync(); + Task AfterFinalizeAsync(); +} + +public abstract class BuildServerBase : IBuildServer +{ + protected BuildServerBase(ICakeContext cakeContext) + { + CakeContext = cakeContext; + } + + public ICakeContext CakeContext { get; private set; } + + public BuildContext BuildContext { get; private set; } + + public abstract Task PinBuildAsync(string comment); + public abstract Task SetVersionAsync(string version); + public abstract Task SetVariableAsync(string name, string value); + public abstract Tuple GetVariable(string variableName, string defaultValue); + + //------------------------------------------------------------- + + public void SetBuildContext(BuildContext buildContext) + { + BuildContext = buildContext; + } + + //------------------------------------------------------------- + + public virtual async Task BeforeInitializeAsync() + { + } + + public virtual async Task AfterInitializeAsync() + { + } + + //------------------------------------------------------------- + + public virtual async Task BeforePrepareAsync() + { + } + + public virtual async Task AfterPrepareAsync() + { + } + + //------------------------------------------------------------- + + public virtual async Task BeforeUpdateInfoAsync() + { + } + + public virtual async Task AfterUpdateInfoAsync() + { + } + + //------------------------------------------------------------- + + public virtual async Task BeforeBuildAsync() + { + } + + public virtual async Task OnBuildFailedAsync() + { + } + + public virtual async Task AfterBuildAsync() + { + } + + //------------------------------------------------------------- + + public virtual async Task BeforeTestAsync() + { + } + + public virtual async Task OnTestFailedAsync() + { + } + + public virtual async Task AfterTestAsync() + { + } + + //------------------------------------------------------------- + + public virtual async Task BeforePackageAsync() + { + } + + public virtual async Task AfterPackageAsync() + { + } + + //------------------------------------------------------------- + + public virtual async Task BeforeDeployAsync() + { + } + + public virtual async Task AfterDeployAsync() + { + } + + //------------------------------------------------------------- + + public virtual async Task BeforeFinalizeAsync() + { + } + + public virtual async Task AfterFinalizeAsync() + { + } +} + +//------------------------------------------------------------- + +public class BuildServerIntegration : IIntegration +{ + [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] + static extern uint GetPrivateProfileString( + string lpAppName, + string lpKeyName, + string lpDefault, + StringBuilder lpReturnedString, + uint nSize, + string lpFileName); + + private readonly Dictionary _parameters; + private readonly List _buildServers = new List(); + private readonly Dictionary _buildServerVariableCache = new Dictionary(); + + public BuildServerIntegration(ICakeContext cakeContext, Dictionary parameters) + { + CakeContext = cakeContext; + _parameters = parameters; + + _buildServers.Add(new ContinuaCIBuildServer(cakeContext)); + } + + public void SetBuildContext(BuildContext buildContext) + { + BuildContext = buildContext; + + foreach (var buildServer in _buildServers) + { + buildServer.SetBuildContext(buildContext); + } + } + + public BuildContext BuildContext { get; private set; } + + public ICakeContext CakeContext { get; private set; } + + //------------------------------------------------------------- + + public async Task BeforeInitializeAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.BeforeInitializeAsync(); + } + } + + public async Task AfterInitializeAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.AfterInitializeAsync(); + } + } + + //------------------------------------------------------------- + + public async Task BeforePrepareAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.BeforePrepareAsync(); + } + } + + public async Task AfterPrepareAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.AfterPrepareAsync(); + } + } + + //------------------------------------------------------------- + + public async Task BeforeUpdateInfoAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.BeforeUpdateInfoAsync(); + } + } + + public async Task AfterUpdateInfoAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.AfterUpdateInfoAsync(); + } + } + + //------------------------------------------------------------- + + public async Task BeforeBuildAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.BeforeBuildAsync(); + } + } + + public async Task OnBuildFailedAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.OnBuildFailedAsync(); + } + } + + public async Task AfterBuildAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.AfterBuildAsync(); + } + } + + //------------------------------------------------------------- + + public async Task BeforeTestAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.BeforeTestAsync(); + } + } + + public async Task OnTestFailedAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.OnTestFailedAsync(); + } + } + + public async Task AfterTestAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.AfterTestAsync(); + } + } + + //------------------------------------------------------------- + + public async Task BeforePackageAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.BeforePackageAsync(); + } + } + + public async Task AfterPackageAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.AfterPackageAsync(); + } + } + + //------------------------------------------------------------- + + public async Task BeforeDeployAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.BeforeDeployAsync(); + } + } + + public async Task AfterDeployAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.AfterDeployAsync(); + } + } + + //------------------------------------------------------------- + + public async Task BeforeFinalizeAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.BeforeFinalizeAsync(); + } + } + + public async Task AfterFinalizeAsync() + { + foreach (var buildServer in _buildServers) + { + await buildServer.AfterFinalizeAsync(); + } + } + + //------------------------------------------------------------- + + public async Task PinBuildAsync(string comment) + { + foreach (var buildServer in _buildServers) + { + await buildServer.PinBuildAsync(comment); + } + } + + //------------------------------------------------------------- + + public async Task SetVersionAsync(string version) + { + foreach (var buildServer in _buildServers) + { + await buildServer.SetVersionAsync(version); + } + } + + //------------------------------------------------------------- + + public async Task SetVariableAsync(string variableName, string value) + { + foreach (var buildServer in _buildServers) + { + await buildServer.SetVariableAsync(variableName, value); + } + } + + //------------------------------------------------------------- + + public bool GetVariableAsBool(string variableName, bool defaultValue, bool showValue = false) + { + var value = defaultValue; + + if (bool.TryParse(GetVariable(variableName, "unknown", showValue: false), out var retrievedValue)) + { + value = retrievedValue; + } + + if (showValue) + { + PrintVariableValue(variableName, value.ToString()); + } + + return value; + } + + //------------------------------------------------------------- + + public string GetVariable(string variableName, string defaultValue = null, bool showValue = false) + { + var cacheKey = string.Format("{0}__{1}", variableName ?? string.Empty, defaultValue ?? string.Empty); + + if (!_buildServerVariableCache.TryGetValue(cacheKey, out string value)) + { + value = GetVariableForCache(variableName, defaultValue); + + if (showValue) + { + PrintVariableValue(variableName, value); + } + + _buildServerVariableCache[cacheKey] = value; + } + //else + //{ + // Information("Retrieved value for '{0}' from cache", variableName); + //} + + return value; + } + + //------------------------------------------------------------- + + private string GetVariableForCache(string variableName, string defaultValue = null) + { + var argumentValue = CakeContext.Argument(variableName, "non-existing"); + if (argumentValue != "non-existing") + { + CakeContext.Information("Variable '{0}' is specified via an argument", variableName); + + return argumentValue; + } + + // Check each build server + foreach (var buildServer in _buildServers) + { + var buildServerVariable = buildServer.GetVariable(variableName, defaultValue); + if (buildServerVariable.Item1) + { + return buildServerVariable.Item2; + } + } + + var overrideFile = System.IO.Path.Combine(".", "build.cakeoverrides"); + if (System.IO.File.Exists(overrideFile)) + { + var sb = new StringBuilder(string.Empty, 256); + var lengthRead = GetPrivateProfileString("General", variableName, null, sb, (uint)sb.Capacity, overrideFile); + if (lengthRead > 0) + { + CakeContext.Information("Variable '{0}' is specified via build.cakeoverrides", variableName); + + var sbValue = sb.ToString(); + if (sbValue == "[ignore]" || + sbValue == "[empty]") + { + return string.Empty; + } + + return sbValue; + } + } + + if (CakeContext.HasEnvironmentVariable(variableName)) + { + CakeContext.Information("Variable '{0}' is specified via an environment variable", variableName); + + return CakeContext.EnvironmentVariable(variableName); + } + + if (_parameters.TryGetValue(variableName, out var parameter)) + { + CakeContext.Information("Variable '{0}' is specified via the Parameters dictionary", variableName); + + if (parameter is null) + { + return null; + } + + if (parameter is string) + { + return (string)parameter; + } + + if (parameter is Func) + { + return ((Func)parameter).Invoke(); + } + + throw new Exception(string.Format("Parameter is defined as '{0}', but that type is not supported yet...", parameter.GetType().Name)); + } + + CakeContext.Information("Variable '{0}' is not specified, returning default value", variableName); + + return defaultValue ?? string.Empty; + } + + //------------------------------------------------------------- + + private void PrintVariableValue(string variableName, string value, bool isSensitive = false) + { + var valueForLog = isSensitive ? "********" : value; + CakeContext.Information("{0}: '{1}'", variableName, valueForLog); + } +} + diff --git a/deployment/cake/codesigning-tasks.cake b/deployment/cake/codesigning-tasks.cake index be849dde..64364f5d 100644 --- a/deployment/cake/codesigning-tasks.cake +++ b/deployment/cake/codesigning-tasks.cake @@ -1,7 +1,7 @@ -#l "codesigning-variables.cake" - -using System.Xml.Linq; - -//------------------------------------------------------------- - -// Empty by design for now +#l "codesigning-variables.cake" + +using System.Xml.Linq; + +//------------------------------------------------------------- + +// Empty by design for now diff --git a/deployment/cake/codesigning-variables.cake b/deployment/cake/codesigning-variables.cake index eefb4444..4bd6e82d 100644 --- a/deployment/cake/codesigning-variables.cake +++ b/deployment/cake/codesigning-variables.cake @@ -1,52 +1,52 @@ -#l "buildserver.cake" - -//------------------------------------------------------------- - -public class CodeSigningContext : BuildContextBase -{ - public CodeSigningContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public List ProjectsToSignImmediately { get; set; } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - //CakeContext.Information($"Found '{Items.Count}' component projects"); - } -} - -//------------------------------------------------------------- - -private CodeSigningContext InitializeCodeSigningContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new CodeSigningContext(parentBuildContext) - { - ProjectsToSignImmediately = CodeSignImmediately ?? new List(), - }; - - return data; -} - -//------------------------------------------------------------- - -List _codeSignImmediately; - -public List CodeSignImmediately -{ - get - { - if (_codeSignImmediately is null) - { - _codeSignImmediately = new List(); - } - - return _codeSignImmediately; - } +#l "buildserver.cake" + +//------------------------------------------------------------- + +public class CodeSigningContext : BuildContextBase +{ + public CodeSigningContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public List ProjectsToSignImmediately { get; set; } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + //CakeContext.Information($"Found '{Items.Count}' component projects"); + } +} + +//------------------------------------------------------------- + +private CodeSigningContext InitializeCodeSigningContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new CodeSigningContext(parentBuildContext) + { + ProjectsToSignImmediately = CodeSignImmediately, + }; + + return data; +} + +//------------------------------------------------------------- + +List _codeSignImmediately; + +public List CodeSignImmediately +{ + get + { + if (_codeSignImmediately is null) + { + _codeSignImmediately = new List(); + } + + return _codeSignImmediately; + } } \ No newline at end of file diff --git a/deployment/cake/components-tasks.cake b/deployment/cake/components-tasks.cake index cf543969..90891315 100644 --- a/deployment/cake/components-tasks.cake +++ b/deployment/cake/components-tasks.cake @@ -1,357 +1,368 @@ -#l "components-variables.cake" - -using System.Xml.Linq; - -//------------------------------------------------------------- - -public class ComponentsProcessor : ProcessorBase -{ - public ComponentsProcessor(BuildContext buildContext) - : base(buildContext) - { - - } - - public override bool HasItems() - { - return BuildContext.Components.Items.Count > 0; - } - - private string GetComponentNuGetRepositoryUrl(string projectName) - { - // Allow per project overrides via "NuGetRepositoryUrlFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "NuGetRepositoryUrlFor", BuildContext.Components.NuGetRepositoryUrl); - } - - private string GetComponentNuGetRepositoryApiKey(string projectName) - { - // Allow per project overrides via "NuGetRepositoryApiKeyFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "NuGetRepositoryApiKeyFor", BuildContext.Components.NuGetRepositoryApiKey); - } - - public override async Task PrepareAsync() - { - if (!HasItems()) - { - return; - } - - // Check whether projects should be processed, `.ToList()` - // is required to prevent issues with foreach - foreach (var component in BuildContext.Components.Items.ToList()) - { - if (!ShouldProcessProject(BuildContext, component)) - { - BuildContext.Components.Items.Remove(component); - } - } - - if (BuildContext.General.IsLocalBuild && BuildContext.General.Target.ToLower().Contains("packagelocal")) - { - foreach (var component in BuildContext.Components.Items) - { - var expandableCacheDirectory = System.IO.Path.Combine("%userprofile%", ".nuget", "packages", component, BuildContext.General.Version.NuGet); - var cacheDirectory = Environment.ExpandEnvironmentVariables(expandableCacheDirectory); - - CakeContext.Information("Checking for existing local NuGet cached version at '{0}'", cacheDirectory); - - var retryCount = 3; - - while (retryCount > 0) - { - if (!CakeContext.DirectoryExists(cacheDirectory)) - { - break; - } - - CakeContext.Information("Deleting already existing NuGet cached version from '{0}'", cacheDirectory); - - CakeContext.DeleteDirectory(cacheDirectory, new DeleteDirectorySettings - { - Force = true, - Recursive = true - }); - - await System.Threading.Tasks.Task.Delay(1000); - - retryCount--; - } - } - } - } - - public override async Task UpdateInfoAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var component in BuildContext.Components.Items) - { - CakeContext.Information("Updating version for component '{0}'", component); - - var projectFileName = GetProjectFileName(BuildContext, component); - - CakeContext.TransformConfig(projectFileName, new TransformationCollection - { - { "Project/PropertyGroup/PackageVersion", BuildContext.General.Version.NuGet } - }); - } - } - - public override async Task BuildAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var component in BuildContext.Components.Items) - { - BuildContext.CakeContext.LogSeparator("Building component '{0}'", component); - - var projectFileName = GetProjectFileName(BuildContext, component); - - var msBuildSettings = new MSBuildSettings - { - Verbosity = Verbosity.Quiet, - //Verbosity = Verbosity.Diagnostic, - ToolVersion = MSBuildToolVersion.Default, - Configuration = BuildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, component, "build"); - - // Note: we need to set OverridableOutputPath because we need to be able to respect - // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which - // are properties passed in using the command line) - var outputDirectory = GetProjectOutputDirectory(BuildContext, component); - CakeContext.Information("Output directory: '{0}'", outputDirectory); - msBuildSettings.WithProperty("OverridableOutputPath", outputDirectory); - - // SourceLink specific stuff - if (IsSourceLinkSupported(BuildContext, component, projectFileName)) - { - var repositoryUrl = BuildContext.General.Repository.Url; - var repositoryCommitId = BuildContext.General.Repository.CommitId; - - CakeContext.Information("Repository url is specified, enabling SourceLink to commit '{0}/commit/{1}'", - repositoryUrl, repositoryCommitId); - - // TODO: For now we are assuming everything is git, we might need to change that in the future - // See why we set the values at https://github.com/dotnet/sourcelink/issues/159#issuecomment-427639278 - msBuildSettings.WithProperty("EnableSourceLink", "true"); - msBuildSettings.WithProperty("EnableSourceControlManagerQueries", "false"); - msBuildSettings.WithProperty("PublishRepositoryUrl", "true"); - msBuildSettings.WithProperty("RepositoryType", "git"); - msBuildSettings.WithProperty("RepositoryUrl", repositoryUrl); - msBuildSettings.WithProperty("RevisionId", repositoryCommitId); - - InjectSourceLinkInProjectFile(BuildContext, component, projectFileName); - } - - RunMsBuild(BuildContext, component, projectFileName, msBuildSettings, "build"); - } - } - - public override async Task PackageAsync() - { - if (!HasItems()) - { - return; - } - - var configurationName = BuildContext.General.Solution.ConfigurationName; - - foreach (var component in BuildContext.Components.Items) - { - // Note: some projects, such as Catel.Fody, require packaging - // of non-deployable projects - if (BuildContext.General.SkipComponentsThatAreNotDeployable && - !ShouldPackageProject(BuildContext, component)) - { - CakeContext.Information("Component '{0}' should not be packaged", component); - continue; - } - - // Special exception for Blazor projects - var isBlazorProject = IsBlazorProject(BuildContext, component); - - BuildContext.CakeContext.LogSeparator("Packaging component '{0}'", component); - - var projectDirectory = GetProjectDirectory(component); - var projectFileName = GetProjectFileName(BuildContext, component); - var outputDirectory = GetProjectOutputDirectory(BuildContext, component); - CakeContext.Information("Output directory: '{0}'", outputDirectory); - - // Step 1: remove intermediate files to ensure we have the same results on the build server, somehow NuGet - // targets tries to find the resource assemblies in [ProjectName]\obj\Release\net46\de\[ProjectName].resources.dll', - // we won't run a clean on the project since it will clean out the actual output (which we still need for packaging) - - CakeContext.Information("Cleaning intermediate files for component '{0}'", component); - - var binFolderPattern = string.Format("{0}/bin/{1}/**.dll", projectDirectory, configurationName); - - CakeContext.Information("Deleting 'bin' directory contents using '{0}'", binFolderPattern); - - var binFiles = CakeContext.GetFiles(binFolderPattern); - CakeContext.DeleteFiles(binFiles); - - if (!isBlazorProject) - { - var objFolderPattern = string.Format("{0}/obj/{1}/**.dll", projectDirectory, configurationName); - - CakeContext.Information("Deleting 'bin' directory contents using '{0}'", objFolderPattern); - - var objFiles = CakeContext.GetFiles(objFolderPattern); - CakeContext.DeleteFiles(objFiles); - } - - CakeContext.Information(string.Empty); - - // Step 2: Go packaging! - CakeContext.Information("Using 'msbuild' to package '{0}'", component); - - var msBuildSettings = new MSBuildSettings - { - Verbosity = Verbosity.Quiet, - //Verbosity = Verbosity.Diagnostic, - ToolVersion = MSBuildToolVersion.Default, - Configuration = configurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, component, "pack"); - - // Note: we need to set OverridableOutputPath because we need to be able to respect - // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which - // are properties passed in using the command line) - msBuildSettings.WithProperty("OverridableOutputPath", outputDirectory); - msBuildSettings.WithProperty("ConfigurationName", configurationName); - msBuildSettings.WithProperty("PackageVersion", BuildContext.General.Version.NuGet); - - // SourceLink specific stuff - var repositoryUrl = BuildContext.General.Repository.Url; - var repositoryCommitId = BuildContext.General.Repository.CommitId; - if (!BuildContext.General.SourceLink.IsDisabled && - !BuildContext.General.IsLocalBuild && - !string.IsNullOrWhiteSpace(repositoryUrl)) - { - CakeContext.Information("Repository url is specified, adding commit specific data to package"); - - // TODO: For now we are assuming everything is git, we might need to change that in the future - // See why we set the values at https://github.com/dotnet/sourcelink/issues/159#issuecomment-427639278 - msBuildSettings.WithProperty("PublishRepositoryUrl", "true"); - msBuildSettings.WithProperty("RepositoryType", "git"); - msBuildSettings.WithProperty("RepositoryUrl", repositoryUrl); - msBuildSettings.WithProperty("RevisionId", repositoryCommitId); - } - - // Disable Multilingual App Toolkit (MAT) during packaging - msBuildSettings.WithProperty("DisableMAT", "true"); - - // Fix for .NET Core 3.0, see https://github.com/dotnet/core-sdk/issues/192, it - // uses obj/release instead of [outputdirectory] - msBuildSettings.WithProperty("DotNetPackIntermediateOutputPath", outputDirectory); - - var noBuild = true; - - if (isBlazorProject) - { - CakeContext.Information("Allowing build and package restore during package phase since this is a Blazor project which requires the 'obj' directory"); - - // Don't use WithProperty since that will concatenate, and we need to overwrite the - // value here - //msBuildSettings.WithProperty("ResolveNuGetPackages", "true"); - msBuildSettings.Properties["ResolveNuGetPackages"] = new List - { - "true" - }; - - msBuildSettings.Restore = true; - noBuild = false; - } - - // As described in the this issue: https://github.com/NuGet/Home/issues/4360 - // we should not use IsTool, but set BuildOutputTargetFolder instead - msBuildSettings.WithProperty("CopyLocalLockFileAssemblies", "true"); - msBuildSettings.WithProperty("IncludeBuildOutput", "true"); - msBuildSettings.WithProperty("NoDefaultExcludes", "true"); - - msBuildSettings.WithProperty("NoBuild", noBuild.ToString()); - msBuildSettings.Targets.Add("Pack"); - - RunMsBuild(BuildContext, component, projectFileName, msBuildSettings, "pack"); - - BuildContext.CakeContext.LogSeparator(); - } - - var codeSign = (!BuildContext.General.IsCiBuild && - !BuildContext.General.IsLocalBuild && - !string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)); - if (codeSign) - { - // For details, see https://docs.microsoft.com/en-us/nuget/create-packages/sign-a-package - // nuget sign MyPackage.nupkg -CertificateSubjectName -Timestamper - var filesToSign = CakeContext.GetFiles($"{BuildContext.General.OutputRootDirectory}/*.nupkg"); - - foreach (var fileToSign in filesToSign) - { - CakeContext.Information($"Signing NuGet package '{fileToSign}' using certificate subject '{BuildContext.General.CodeSign.CertificateSubjectName}'"); - - var exitCode = CakeContext.StartProcess(BuildContext.General.NuGet.Executable, new ProcessSettings - { - Arguments = $"sign \"{fileToSign}\" -CertificateSubjectName \"{BuildContext.General.CodeSign.CertificateSubjectName}\" -Timestamper \"{BuildContext.General.CodeSign.TimeStampUri}\"" - }); - - CakeContext.Information("Signing NuGet package exited with '{0}'", exitCode); - } - } - } - - public override async Task DeployAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var component in BuildContext.Components.Items) - { - if (!ShouldDeployProject(BuildContext, component)) - { - CakeContext.Information("Component '{0}' should not be deployed", component); - continue; - } - - BuildContext.CakeContext.LogSeparator("Deploying component '{0}'", component); - - var packageToPush = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, $"{component}.{BuildContext.General.Version.NuGet}.nupkg"); - var nuGetRepositoryUrl = GetComponentNuGetRepositoryUrl(component); - var nuGetRepositoryApiKey = GetComponentNuGetRepositoryApiKey(component); - - if (string.IsNullOrWhiteSpace(nuGetRepositoryUrl)) - { - throw new Exception("NuGet repository is empty, as a protection mechanism this must *always* be specified to make sure packages aren't accidentally deployed to the default public NuGet feed"); - } - - CakeContext.NuGetPush(packageToPush, new NuGetPushSettings - { - Source = nuGetRepositoryUrl, - ApiKey = nuGetRepositoryApiKey, - ArgumentCustomization = args => args.Append("-SkipDuplicate") - }); - - await BuildContext.Notifications.NotifyAsync(component, string.Format("Deployed to NuGet store"), TargetType.Component); - } - } - - public override async Task FinalizeAsync() - { - - } +#l "components-variables.cake" + +using System.Xml.Linq; + +//------------------------------------------------------------- + +public class ComponentsProcessor : ProcessorBase +{ + public ComponentsProcessor(BuildContext buildContext) + : base(buildContext) + { + + } + + public override bool HasItems() + { + return BuildContext.Components.Items.Count > 0; + } + + private string GetComponentNuGetRepositoryUrl(string projectName) + { + // Allow per project overrides via "NuGetRepositoryUrlFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "NuGetRepositoryUrlFor", BuildContext.Components.NuGetRepositoryUrl); + } + + private string GetComponentNuGetRepositoryApiKey(string projectName) + { + // Allow per project overrides via "NuGetRepositoryApiKeyFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "NuGetRepositoryApiKeyFor", BuildContext.Components.NuGetRepositoryApiKey); + } + + public override async Task PrepareAsync() + { + if (!HasItems()) + { + return; + } + + // Check whether projects should be processed, `.ToList()` + // is required to prevent issues with foreach + foreach (var component in BuildContext.Components.Items.ToList()) + { + if (!ShouldProcessProject(BuildContext, component)) + { + BuildContext.Components.Items.Remove(component); + } + } + + if (BuildContext.General.IsLocalBuild && BuildContext.General.Target.ToLower().Contains("packagelocal")) + { + foreach (var component in BuildContext.Components.Items) + { + var expandableCacheDirectory = System.IO.Path.Combine("%userprofile%", ".nuget", "packages", component, BuildContext.General.Version.NuGet); + var cacheDirectory = Environment.ExpandEnvironmentVariables(expandableCacheDirectory); + + CakeContext.Information("Checking for existing local NuGet cached version at '{0}'", cacheDirectory); + + var retryCount = 3; + + while (retryCount > 0) + { + if (!CakeContext.DirectoryExists(cacheDirectory)) + { + break; + } + + CakeContext.Information("Deleting already existing NuGet cached version from '{0}'", cacheDirectory); + + CakeContext.DeleteDirectory(cacheDirectory, new DeleteDirectorySettings + { + Force = true, + Recursive = true + }); + + await System.Threading.Tasks.Task.Delay(1000); + + retryCount--; + } + } + } + } + + public override async Task UpdateInfoAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var component in BuildContext.Components.Items) + { + CakeContext.Information("Updating version for component '{0}'", component); + + var projectFileName = GetProjectFileName(BuildContext, component); + + CakeContext.TransformConfig(projectFileName, new TransformationCollection + { + { "Project/PropertyGroup/PackageVersion", BuildContext.General.Version.NuGet } + }); + } + } + + public override async Task BuildAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var component in BuildContext.Components.Items) + { + BuildContext.CakeContext.LogSeparator("Building component '{0}'", component); + + var projectFileName = GetProjectFileName(BuildContext, component); + + var msBuildSettings = new MSBuildSettings + { + Verbosity = Verbosity.Quiet, + //Verbosity = Verbosity.Diagnostic, + ToolVersion = MSBuildToolVersion.Default, + Configuration = BuildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, component, "build"); + + // Note: we need to set OverridableOutputPath because we need to be able to respect + // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which + // are properties passed in using the command line) + var outputDirectory = GetProjectOutputDirectory(BuildContext, component); + CakeContext.Information("Output directory: '{0}'", outputDirectory); + msBuildSettings.WithProperty("OverridableOutputPath", outputDirectory); + + // SourceLink specific stuff + if (IsSourceLinkSupported(BuildContext, component, projectFileName)) + { + var repositoryUrl = BuildContext.General.Repository.Url; + var repositoryCommitId = BuildContext.General.Repository.CommitId; + + CakeContext.Information("Repository url is specified, enabling SourceLink to commit '{0}/commit/{1}'", + repositoryUrl, repositoryCommitId); + + // TODO: For now we are assuming everything is git, we might need to change that in the future + // See why we set the values at https://github.com/dotnet/sourcelink/issues/159#issuecomment-427639278 + msBuildSettings.WithProperty("EnableSourceLink", "true"); + msBuildSettings.WithProperty("EnableSourceControlManagerQueries", "false"); + msBuildSettings.WithProperty("PublishRepositoryUrl", "true"); + msBuildSettings.WithProperty("RepositoryType", "git"); + msBuildSettings.WithProperty("RepositoryUrl", repositoryUrl); + msBuildSettings.WithProperty("RevisionId", repositoryCommitId); + + InjectSourceLinkInProjectFile(BuildContext, component, projectFileName); + } + + RunMsBuild(BuildContext, component, projectFileName, msBuildSettings, "build"); + + // Specific code signing, requires the following MSBuild properties: + // * CodeSignEnabled + // * CodeSignCommand + // + // This feature is built to allow projects that have post-build copy + // steps (e.g. for assets) to be signed correctly before being embedded + if (ShouldSignImmediately(BuildContext, component)) + { + SignProjectFiles(BuildContext, component); + } + } + } + + public override async Task PackageAsync() + { + if (!HasItems()) + { + return; + } + + var configurationName = BuildContext.General.Solution.ConfigurationName; + + foreach (var component in BuildContext.Components.Items) + { + // Note: some projects, such as Catel.Fody, require packaging + // of non-deployable projects + if (BuildContext.General.SkipComponentsThatAreNotDeployable && + !ShouldPackageProject(BuildContext, component)) + { + CakeContext.Information("Component '{0}' should not be packaged", component); + continue; + } + + // Special exception for Blazor projects + var isBlazorProject = IsBlazorProject(BuildContext, component); + + BuildContext.CakeContext.LogSeparator("Packaging component '{0}'", component); + + var projectDirectory = GetProjectDirectory(component); + var projectFileName = GetProjectFileName(BuildContext, component); + var outputDirectory = GetProjectOutputDirectory(BuildContext, component); + CakeContext.Information("Output directory: '{0}'", outputDirectory); + + // Step 1: remove intermediate files to ensure we have the same results on the build server, somehow NuGet + // targets tries to find the resource assemblies in [ProjectName]\obj\Release\net46\de\[ProjectName].resources.dll', + // we won't run a clean on the project since it will clean out the actual output (which we still need for packaging) + + CakeContext.Information("Cleaning intermediate files for component '{0}'", component); + + var binFolderPattern = string.Format("{0}/bin/{1}/**.dll", projectDirectory, configurationName); + + CakeContext.Information("Deleting 'bin' directory contents using '{0}'", binFolderPattern); + + var binFiles = CakeContext.GetFiles(binFolderPattern); + CakeContext.DeleteFiles(binFiles); + + if (!isBlazorProject) + { + var objFolderPattern = string.Format("{0}/obj/{1}/**.dll", projectDirectory, configurationName); + + CakeContext.Information("Deleting 'bin' directory contents using '{0}'", objFolderPattern); + + var objFiles = CakeContext.GetFiles(objFolderPattern); + CakeContext.DeleteFiles(objFiles); + } + + CakeContext.Information(string.Empty); + + // Step 2: Go packaging! + CakeContext.Information("Using 'msbuild' to package '{0}'", component); + + var msBuildSettings = new MSBuildSettings + { + Verbosity = Verbosity.Quiet, + //Verbosity = Verbosity.Diagnostic, + ToolVersion = MSBuildToolVersion.Default, + Configuration = configurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, component, "pack"); + + // Note: we need to set OverridableOutputPath because we need to be able to respect + // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which + // are properties passed in using the command line) + msBuildSettings.WithProperty("OverridableOutputPath", outputDirectory); + msBuildSettings.WithProperty("ConfigurationName", configurationName); + msBuildSettings.WithProperty("PackageVersion", BuildContext.General.Version.NuGet); + + // SourceLink specific stuff + var repositoryUrl = BuildContext.General.Repository.Url; + var repositoryCommitId = BuildContext.General.Repository.CommitId; + if (!BuildContext.General.SourceLink.IsDisabled && + !BuildContext.General.IsLocalBuild && + !string.IsNullOrWhiteSpace(repositoryUrl)) + { + CakeContext.Information("Repository url is specified, adding commit specific data to package"); + + // TODO: For now we are assuming everything is git, we might need to change that in the future + // See why we set the values at https://github.com/dotnet/sourcelink/issues/159#issuecomment-427639278 + msBuildSettings.WithProperty("PublishRepositoryUrl", "true"); + msBuildSettings.WithProperty("RepositoryType", "git"); + msBuildSettings.WithProperty("RepositoryUrl", repositoryUrl); + msBuildSettings.WithProperty("RevisionId", repositoryCommitId); + } + + // Disable Multilingual App Toolkit (MAT) during packaging + msBuildSettings.WithProperty("DisableMAT", "true"); + + // Fix for .NET Core 3.0, see https://github.com/dotnet/core-sdk/issues/192, it + // uses obj/release instead of [outputdirectory] + msBuildSettings.WithProperty("DotNetPackIntermediateOutputPath", outputDirectory); + + var noBuild = true; + + if (isBlazorProject) + { + CakeContext.Information("Allowing build and package restore during package phase since this is a Blazor project which requires the 'obj' directory"); + + // Don't use WithProperty since that will concatenate, and we need to overwrite the + // value here + //msBuildSettings.WithProperty("ResolveNuGetPackages", "true"); + msBuildSettings.Properties["ResolveNuGetPackages"] = new List + { + "true" + }; + + msBuildSettings.Restore = true; + noBuild = false; + } + + // As described in the this issue: https://github.com/NuGet/Home/issues/4360 + // we should not use IsTool, but set BuildOutputTargetFolder instead + msBuildSettings.WithProperty("CopyLocalLockFileAssemblies", "true"); + msBuildSettings.WithProperty("IncludeBuildOutput", "true"); + msBuildSettings.WithProperty("NoDefaultExcludes", "true"); + + msBuildSettings.WithProperty("NoBuild", noBuild.ToString()); + msBuildSettings.Targets.Add("Pack"); + + RunMsBuild(BuildContext, component, projectFileName, msBuildSettings, "pack"); + + BuildContext.CakeContext.LogSeparator(); + } + + var codeSign = (!BuildContext.General.IsCiBuild && + !BuildContext.General.IsLocalBuild && + !string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)); + if (codeSign) + { + // For details, see https://docs.microsoft.com/en-us/nuget/create-packages/sign-a-package + // nuget sign MyPackage.nupkg -CertificateSubjectName -Timestamper + var filesToSign = CakeContext.GetFiles($"{BuildContext.General.OutputRootDirectory}/*.nupkg"); + + foreach (var fileToSign in filesToSign) + { + CakeContext.Information($"Signing NuGet package '{fileToSign}' using certificate subject '{BuildContext.General.CodeSign.CertificateSubjectName}'"); + + var exitCode = CakeContext.StartProcess(BuildContext.General.NuGet.Executable, new ProcessSettings + { + Arguments = $"sign \"{fileToSign}\" -CertificateSubjectName \"{BuildContext.General.CodeSign.CertificateSubjectName}\" -Timestamper \"{BuildContext.General.CodeSign.TimeStampUri}\"" + }); + + CakeContext.Information("Signing NuGet package exited with '{0}'", exitCode); + } + } + } + + public override async Task DeployAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var component in BuildContext.Components.Items) + { + if (!ShouldDeployProject(BuildContext, component)) + { + CakeContext.Information("Component '{0}' should not be deployed", component); + continue; + } + + BuildContext.CakeContext.LogSeparator("Deploying component '{0}'", component); + + var packageToPush = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, $"{component}.{BuildContext.General.Version.NuGet}.nupkg"); + var nuGetRepositoryUrl = GetComponentNuGetRepositoryUrl(component); + var nuGetRepositoryApiKey = GetComponentNuGetRepositoryApiKey(component); + + if (string.IsNullOrWhiteSpace(nuGetRepositoryUrl)) + { + throw new Exception("NuGet repository is empty, as a protection mechanism this must *always* be specified to make sure packages aren't accidentally deployed to the default public NuGet feed"); + } + + CakeContext.NuGetPush(packageToPush, new NuGetPushSettings + { + Source = nuGetRepositoryUrl, + ApiKey = nuGetRepositoryApiKey, + ArgumentCustomization = args => args.Append("-SkipDuplicate") + }); + + await BuildContext.Notifications.NotifyAsync(component, string.Format("Deployed to NuGet store"), TargetType.Component); + } + } + + public override async Task FinalizeAsync() + { + + } } \ No newline at end of file diff --git a/deployment/cake/components-variables.cake b/deployment/cake/components-variables.cake index dec12203..244363ef 100644 --- a/deployment/cake/components-variables.cake +++ b/deployment/cake/components-variables.cake @@ -1,55 +1,55 @@ -#l "buildserver.cake" - -//------------------------------------------------------------- - -public class ComponentsContext : BuildContextWithItemsBase -{ - public ComponentsContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string NuGetRepositoryUrl { get; set; } - public string NuGetRepositoryApiKey { get; set; } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' component projects"); - } -} - -//------------------------------------------------------------- - -private ComponentsContext InitializeComponentsContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new ComponentsContext(parentBuildContext) - { - Items = Components ?? new List(), - NuGetRepositoryUrl = buildContext.BuildServer.GetVariable("NuGetRepositoryUrl", showValue: true), - NuGetRepositoryApiKey = buildContext.BuildServer.GetVariable("NuGetRepositoryApiKey", showValue: false) - }; - - return data; -} - -//------------------------------------------------------------- - -List _components; - -public List Components -{ - get - { - if (_components is null) - { - _components = new List(); - } - - return _components; - } +#l "buildserver.cake" + +//------------------------------------------------------------- + +public class ComponentsContext : BuildContextWithItemsBase +{ + public ComponentsContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string NuGetRepositoryUrl { get; set; } + public string NuGetRepositoryApiKey { get; set; } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' component projects"); + } +} + +//------------------------------------------------------------- + +private ComponentsContext InitializeComponentsContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new ComponentsContext(parentBuildContext) + { + Items = Components ?? new List(), + NuGetRepositoryUrl = buildContext.BuildServer.GetVariable("NuGetRepositoryUrl", showValue: true), + NuGetRepositoryApiKey = buildContext.BuildServer.GetVariable("NuGetRepositoryApiKey", showValue: false) + }; + + return data; +} + +//------------------------------------------------------------- + +List _components; + +public List Components +{ + get + { + if (_components is null) + { + _components = new List(); + } + + return _components; + } } \ No newline at end of file diff --git a/deployment/cake/dependencies-tasks.cake b/deployment/cake/dependencies-tasks.cake index b624b09b..f8170045 100644 --- a/deployment/cake/dependencies-tasks.cake +++ b/deployment/cake/dependencies-tasks.cake @@ -1,193 +1,185 @@ -#l "dependencies-variables.cake" - -using System.Xml.Linq; - -//------------------------------------------------------------- - -public class DependenciesProcessor : ProcessorBase -{ - public DependenciesProcessor(BuildContext buildContext) - : base(buildContext) - { - - } - - public override bool HasItems() - { - return BuildContext.Dependencies.Items.Count > 0; - } - - public override async Task PrepareAsync() - { - BuildContext.CakeContext.Information($"Checking '{BuildContext.Dependencies.Items.Count}' dependencies"); - - if (!HasItems()) - { - return; - } - - // We need to go through this twice because a dependency can be a dependency of a dependency - var dependenciesToBuild = new List(); - - // Check whether projects should be processed, `.ToList()` - // is required to prevent issues with foreach - for (int i = 0; i < 3; i++) - { - foreach (var dependency in BuildContext.Dependencies.Items.ToList()) - { - if (dependenciesToBuild.Contains(dependency)) - { - // Already done - continue; - } - - BuildContext.CakeContext.Information($"Checking dependency '{dependency}' using run {i + 1}"); - - if (BuildContext.Dependencies.ShouldBuildDependency(dependency, dependenciesToBuild)) - { - BuildContext.CakeContext.Information($"Dependency '{dependency}' should be included"); - - dependenciesToBuild.Add(dependency); - } - } - } - - // TODO: How to determine the sort order? E.g. dependencies of dependencies? - - foreach (var dependency in BuildContext.Dependencies.Items.ToList()) - { - if (!dependenciesToBuild.Contains(dependency)) - { - BuildContext.CakeContext.Information($"Skipping dependency '{dependency}' because no dependent projects are included"); - - BuildContext.Dependencies.Dependencies.Remove(dependency); - BuildContext.Dependencies.Items.Remove(dependency); - } - } - } - - public override async Task UpdateInfoAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var dependency in BuildContext.Dependencies.Items) - { - CakeContext.Information("Updating version for dependency '{0}'", dependency); - - var projectFileName = GetProjectFileName(BuildContext, dependency); - - CakeContext.TransformConfig(projectFileName, new TransformationCollection - { - { "Project/PropertyGroup/PackageVersion", BuildContext.General.Version.NuGet } - }); - } - } - - public override async Task BuildAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var dependency in BuildContext.Dependencies.Items) - { - BuildContext.CakeContext.LogSeparator("Building dependency '{0}'", dependency); - - var projectFileName = GetProjectFileName(BuildContext, dependency); - - var msBuildSettings = new MSBuildSettings - { - Verbosity = Verbosity.Quiet, - //Verbosity = Verbosity.Diagnostic, - ToolVersion = MSBuildToolVersion.Default, - Configuration = BuildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform, - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, dependency, "build"); - - // Note: we need to set OverridableOutputPath because we need to be able to respect - // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which - // are properties passed in using the command line) - var isCppProject = IsCppProject(projectFileName); - if (isCppProject) - { - // Special C++ exceptions - msBuildSettings.MSBuildPlatform = MSBuildPlatform.Automatic; - msBuildSettings.PlatformTarget = PlatformTarget.Win32; - } - - // SourceLink specific stuff - if (IsSourceLinkSupported(BuildContext, dependency, projectFileName)) - { - var repositoryUrl = BuildContext.General.Repository.Url; - var repositoryCommitId = BuildContext.General.Repository.CommitId; - - CakeContext.Information("Repository url is specified, enabling SourceLink to commit '{0}/commit/{1}'", - repositoryUrl, repositoryCommitId); - - // TODO: For now we are assuming everything is git, we might need to change that in the future - // See why we set the values at https://github.com/dotnet/sourcelink/issues/159#issuecomment-427639278 - msBuildSettings.WithProperty("EnableSourceLink", "true"); - msBuildSettings.WithProperty("EnableSourceControlManagerQueries", "false"); - msBuildSettings.WithProperty("PublishRepositoryUrl", "true"); - msBuildSettings.WithProperty("RepositoryType", "git"); - msBuildSettings.WithProperty("RepositoryUrl", repositoryUrl); - msBuildSettings.WithProperty("RevisionId", repositoryCommitId); - - InjectSourceLinkInProjectFile(BuildContext, dependency, projectFileName); - } - - // Specific code signing, requires the following MSBuild properties: - // * CodeSignEnabled - // * CodeSignCommand - // - // This feature is built to allow projects that have post-build copy - // steps (e.g. for assets) to be signed correctly before being embedded - if (ShouldSignImmediately(BuildContext, dependency)) - { - var codeSignToolFileName = FindSignToolFileName(BuildContext); - var codeSignVerifyCommand = $"verify /pa"; - var codeSignCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", BuildContext.General.CodeSign.TimeStampUri, - BuildContext.General.CodeSign.CertificateSubjectName, BuildContext.General.CodeSign.HashAlgorithm); - - msBuildSettings.WithProperty("CodeSignToolFileName", codeSignToolFileName); - msBuildSettings.WithProperty("CodeSignVerifyCommand", codeSignVerifyCommand); - msBuildSettings.WithProperty("CodeSignCommand", codeSignCommand); - msBuildSettings.WithProperty("CodeSignEnabled", "true"); - } - - RunMsBuild(BuildContext, dependency, projectFileName, msBuildSettings, "build"); - } - } - - public override async Task PackageAsync() - { - if (!HasItems()) - { - return; - } - - // No packaging required for dependencies - } - - public override async Task DeployAsync() - { - if (!HasItems()) - { - return; - } - - // No deployment required for dependencies - } - - public override async Task FinalizeAsync() - { - - } +#l "dependencies-variables.cake" + +using System.Xml.Linq; + +//------------------------------------------------------------- + +public class DependenciesProcessor : ProcessorBase +{ + public DependenciesProcessor(BuildContext buildContext) + : base(buildContext) + { + + } + + public override bool HasItems() + { + return BuildContext.Dependencies.Items.Count > 0; + } + + public override async Task PrepareAsync() + { + BuildContext.CakeContext.Information($"Checking '{BuildContext.Dependencies.Items.Count}' dependencies"); + + if (!HasItems()) + { + return; + } + + // We need to go through this twice because a dependency can be a dependency of a dependency + var dependenciesToBuild = new List(); + + // Check whether projects should be processed, `.ToList()` + // is required to prevent issues with foreach + for (int i = 0; i < 3; i++) + { + foreach (var dependency in BuildContext.Dependencies.Items.ToList()) + { + if (dependenciesToBuild.Contains(dependency)) + { + // Already done + continue; + } + + BuildContext.CakeContext.Information($"Checking dependency '{dependency}' using run {i + 1}"); + + if (BuildContext.Dependencies.ShouldBuildDependency(dependency, dependenciesToBuild)) + { + BuildContext.CakeContext.Information($"Dependency '{dependency}' should be included"); + + dependenciesToBuild.Add(dependency); + } + } + } + + // TODO: How to determine the sort order? E.g. dependencies of dependencies? + + foreach (var dependency in BuildContext.Dependencies.Items.ToList()) + { + if (!dependenciesToBuild.Contains(dependency)) + { + BuildContext.CakeContext.Information($"Skipping dependency '{dependency}' because no dependent projects are included"); + + BuildContext.Dependencies.Dependencies.Remove(dependency); + BuildContext.Dependencies.Items.Remove(dependency); + } + } + } + + public override async Task UpdateInfoAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var dependency in BuildContext.Dependencies.Items) + { + CakeContext.Information("Updating version for dependency '{0}'", dependency); + + var projectFileName = GetProjectFileName(BuildContext, dependency); + + CakeContext.TransformConfig(projectFileName, new TransformationCollection + { + { "Project/PropertyGroup/PackageVersion", BuildContext.General.Version.NuGet } + }); + } + } + + public override async Task BuildAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var dependency in BuildContext.Dependencies.Items) + { + BuildContext.CakeContext.LogSeparator("Building dependency '{0}'", dependency); + + var projectFileName = GetProjectFileName(BuildContext, dependency); + + var msBuildSettings = new MSBuildSettings + { + Verbosity = Verbosity.Quiet, + //Verbosity = Verbosity.Diagnostic, + ToolVersion = MSBuildToolVersion.Default, + Configuration = BuildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform, + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, dependency, "build"); + + // Note: we need to set OverridableOutputPath because we need to be able to respect + // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which + // are properties passed in using the command line) + var isCppProject = IsCppProject(projectFileName); + if (isCppProject) + { + // Special C++ exceptions + msBuildSettings.MSBuildPlatform = MSBuildPlatform.Automatic; + msBuildSettings.PlatformTarget = PlatformTarget.Win32; + } + + // SourceLink specific stuff + if (IsSourceLinkSupported(BuildContext, dependency, projectFileName)) + { + var repositoryUrl = BuildContext.General.Repository.Url; + var repositoryCommitId = BuildContext.General.Repository.CommitId; + + CakeContext.Information("Repository url is specified, enabling SourceLink to commit '{0}/commit/{1}'", + repositoryUrl, repositoryCommitId); + + // TODO: For now we are assuming everything is git, we might need to change that in the future + // See why we set the values at https://github.com/dotnet/sourcelink/issues/159#issuecomment-427639278 + msBuildSettings.WithProperty("EnableSourceLink", "true"); + msBuildSettings.WithProperty("EnableSourceControlManagerQueries", "false"); + msBuildSettings.WithProperty("PublishRepositoryUrl", "true"); + msBuildSettings.WithProperty("RepositoryType", "git"); + msBuildSettings.WithProperty("RepositoryUrl", repositoryUrl); + msBuildSettings.WithProperty("RevisionId", repositoryCommitId); + + InjectSourceLinkInProjectFile(BuildContext, dependency, projectFileName); + } + + RunMsBuild(BuildContext, dependency, projectFileName, msBuildSettings, "build"); + + // Specific code signing, requires the following MSBuild properties: + // * CodeSignEnabled + // * CodeSignCommand + // + // This feature is built to allow projects that have post-build copy + // steps (e.g. for assets) to be signed correctly before being embedded + if (ShouldSignImmediately(BuildContext, dependency)) + { + SignProjectFiles(BuildContext, dependency); + } + } + } + + public override async Task PackageAsync() + { + if (!HasItems()) + { + return; + } + + // No packaging required for dependencies + } + + public override async Task DeployAsync() + { + if (!HasItems()) + { + return; + } + + // No deployment required for dependencies + } + + public override async Task FinalizeAsync() + { + + } } \ No newline at end of file diff --git a/deployment/cake/dependencies-variables.cake b/deployment/cake/dependencies-variables.cake index b6335d86..e7fa8b73 100644 --- a/deployment/cake/dependencies-variables.cake +++ b/deployment/cake/dependencies-variables.cake @@ -1,103 +1,103 @@ -#l "buildserver.cake" - -//------------------------------------------------------------- - -public class DependenciesContext : BuildContextWithItemsBase -{ - public DependenciesContext(IBuildContext parentBuildContext, Dictionary> dependencies) - : base(parentBuildContext) - { - Dependencies = dependencies ?? new Dictionary>(); - Items = Dependencies.Keys.ToList(); - } - - public Dictionary> Dependencies { get; private set; } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' dependency projects"); - } - - public bool ShouldBuildDependency(string dependencyProject) - { - return ShouldBuildDependency(dependencyProject, Array.Empty()); - } - - public bool ShouldBuildDependency(string dependencyProject, IEnumerable knownDependenciesToBeBuilt) - { - if (!Dependencies.TryGetValue(dependencyProject, out var dependencyInfo)) - { - return false; - } - - if (dependencyInfo.Count == 0) - { - // No explicit projects defined, always build dependency - return true; - } - - foreach (var projectRequiringDependency in dependencyInfo) - { - CakeContext.Information($"Checking whether '{projectRequiringDependency}' is in the list to be processed"); - - // Check dependencies of dependencies - if (knownDependenciesToBeBuilt.Any(x => string.Equals(x, projectRequiringDependency, StringComparison.OrdinalIgnoreCase))) - { - CakeContext.Information($"Dependency '{dependencyProject}' is a dependency of dependency project '{projectRequiringDependency}', including this in the build"); - return true; - } - - // Special case: *if* this is the 2nd round we check, and the project requiring this dependency is a test project, - // we should check whether the test project is not already excluded. If so, the Deploy[SomeProject]Tests will return true - // and this logic will still include it, so we need to exclude it explicitly - if (IsTestProject((BuildContext)ParentContext, projectRequiringDependency) && - !knownDependenciesToBeBuilt.Contains(projectRequiringDependency)) - { - CakeContext.Information($"Dependency '{dependencyProject}' is a dependency of '{projectRequiringDependency}', but that is an already excluded test project, not yet including in the build"); - - // Important: don't return, there might be other projects - continue; - } - - // Check if we should build this project - if (ShouldProcessProject((BuildContext)ParentContext, projectRequiringDependency)) - { - CakeContext.Information($"Dependency '{dependencyProject}' is a dependency of '{projectRequiringDependency}', including this in the build"); - return true; - } - } - - return false; - } -} - -//------------------------------------------------------------- - -private DependenciesContext InitializeDependenciesContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new DependenciesContext(parentBuildContext, Dependencies); - - return data; -} - -//------------------------------------------------------------- - -Dictionary> _dependencies; - -public Dictionary> Dependencies -{ - get - { - if (_dependencies is null) - { - _dependencies = new Dictionary>(); - } - - return _dependencies; - } +#l "buildserver.cake" + +//------------------------------------------------------------- + +public class DependenciesContext : BuildContextWithItemsBase +{ + public DependenciesContext(IBuildContext parentBuildContext, Dictionary> dependencies) + : base(parentBuildContext) + { + Dependencies = dependencies ?? new Dictionary>(); + Items = Dependencies.Keys.ToList(); + } + + public Dictionary> Dependencies { get; private set; } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' dependency projects"); + } + + public bool ShouldBuildDependency(string dependencyProject) + { + return ShouldBuildDependency(dependencyProject, Array.Empty()); + } + + public bool ShouldBuildDependency(string dependencyProject, IEnumerable knownDependenciesToBeBuilt) + { + if (!Dependencies.TryGetValue(dependencyProject, out var dependencyInfo)) + { + return false; + } + + if (dependencyInfo.Count == 0) + { + // No explicit projects defined, always build dependency + return true; + } + + foreach (var projectRequiringDependency in dependencyInfo) + { + CakeContext.Information($"Checking whether '{projectRequiringDependency}' is in the list to be processed"); + + // Check dependencies of dependencies + if (knownDependenciesToBeBuilt.Any(x => string.Equals(x, projectRequiringDependency, StringComparison.OrdinalIgnoreCase))) + { + CakeContext.Information($"Dependency '{dependencyProject}' is a dependency of dependency project '{projectRequiringDependency}', including this in the build"); + return true; + } + + // Special case: *if* this is the 2nd round we check, and the project requiring this dependency is a test project, + // we should check whether the test project is not already excluded. If so, the Deploy[SomeProject]Tests will return true + // and this logic will still include it, so we need to exclude it explicitly + if (IsTestProject((BuildContext)ParentContext, projectRequiringDependency) && + !knownDependenciesToBeBuilt.Contains(projectRequiringDependency)) + { + CakeContext.Information($"Dependency '{dependencyProject}' is a dependency of '{projectRequiringDependency}', but that is an already excluded test project, not yet including in the build"); + + // Important: don't return, there might be other projects + continue; + } + + // Check if we should build this project + if (ShouldProcessProject((BuildContext)ParentContext, projectRequiringDependency)) + { + CakeContext.Information($"Dependency '{dependencyProject}' is a dependency of '{projectRequiringDependency}', including this in the build"); + return true; + } + } + + return false; + } +} + +//------------------------------------------------------------- + +private DependenciesContext InitializeDependenciesContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new DependenciesContext(parentBuildContext, Dependencies); + + return data; +} + +//------------------------------------------------------------- + +Dictionary> _dependencies; + +public Dictionary> Dependencies +{ + get + { + if (_dependencies is null) + { + _dependencies = new Dictionary>(); + } + + return _dependencies; + } } \ No newline at end of file diff --git a/deployment/cake/docker-tasks.cake b/deployment/cake/docker-tasks.cake index b0977b45..f6382c79 100644 --- a/deployment/cake/docker-tasks.cake +++ b/deployment/cake/docker-tasks.cake @@ -1,409 +1,409 @@ -#l "docker-variables.cake" -#l "lib-octopusdeploy.cake" - -#addin "nuget:?package=Cake.Docker&version=1.2.2" - -//------------------------------------------------------------- - -public class DockerImagesProcessor : ProcessorBase -{ - public DockerImagesProcessor(BuildContext buildContext) - : base(buildContext) - { - - } - - public override bool HasItems() - { - return BuildContext.DockerImages.Items.Count > 0; - } - - public string GetDockerRegistryUrl(string projectName) - { - // Allow per project overrides via "DockerRegistryUrlFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "DockerRegistryUrlFor", BuildContext.DockerImages.DockerRegistryUrl); - } - - public string GetDockerRegistryUserName(string projectName) - { - // Allow per project overrides via "DockerRegistryUserNameFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "DockerRegistryUserNameFor", BuildContext.DockerImages.DockerRegistryUserName); - } - - public string GetDockerRegistryPassword(string projectName) - { - // Allow per project overrides via "DockerRegistryPasswordFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "DockerRegistryPasswordFor", BuildContext.DockerImages.DockerRegistryPassword); - } - - private string GetDockerImageName(string projectName) - { - var name = projectName.Replace(".", "-"); - return name.ToLower(); - } - - private string GetDockerImageTag(string projectName, string version) - { - var dockerRegistryUrl = GetDockerRegistryUrl(projectName); - - var tag = string.Format("{0}/{1}:{2}", dockerRegistryUrl, GetDockerImageName(projectName), version); - return tag.TrimStart(' ', '/').ToLower(); - } - - private string[] GetDockerImageTags(string projectName) - { - var dockerTags = new List(); - - var versions = new List(); - - versions.Add(BuildContext.General.Version.NuGet); - - foreach (var version in new [] - { - BuildContext.General.Version.MajorMinor, - BuildContext.General.Version.Major - }) - { - var additionalTag = version; - - if (BuildContext.General.IsAlphaBuild) - { - additionalTag += "-alpha"; - } - - if (BuildContext.General.IsBetaBuild) - { - additionalTag += "-beta"; - } - - versions.Add(additionalTag); - } - - foreach (var version in versions) - { - dockerTags.Add(GetDockerImageTag(projectName, version)); - } - - if (BuildContext.General.IsAlphaBuild) - { - dockerTags.Add(GetDockerImageTag(projectName, "latest-alpha")); - } - - if (BuildContext.General.IsBetaBuild) - { - dockerTags.Add(GetDockerImageTag(projectName, "latest-beta")); - } - - if (BuildContext.General.IsOfficialBuild) - { - dockerTags.Add(GetDockerImageTag(projectName, "latest-stable")); - dockerTags.Add(GetDockerImageTag(projectName, "latest")); - } - - return dockerTags.ToArray(); - } - - private void ConfigureDockerSettings(AutoToolSettings dockerSettings) - { - var engineUrl = BuildContext.DockerImages.DockerEngineUrl; - if (!string.IsNullOrWhiteSpace(engineUrl)) - { - CakeContext.Information("Using remote docker engine: '{0}'", engineUrl); - - dockerSettings.ArgumentCustomization = args => args.Prepend($"-H {engineUrl}"); - //dockerSettings.BuildArg = new [] { $"DOCKER_HOST={engineUrl}" }; - } - } - - public override async Task PrepareAsync() - { - if (!HasItems()) - { - return; - } - - // Check whether projects should be processed, `.ToList()` - // is required to prevent issues with foreach - foreach (var dockerImage in BuildContext.DockerImages.Items.ToList()) - { - foreach (var imageTag in GetDockerImageTags(dockerImage)) - { - CakeContext.Information(imageTag); - } - - if (!ShouldProcessProject(BuildContext, dockerImage)) - { - BuildContext.DockerImages.Items.Remove(dockerImage); - } - } - } - - public override async Task UpdateInfoAsync() - { - if (!HasItems()) - { - return; - } - - // Doesn't seem neccessary yet - // foreach (var dockerImage in BuildContext.DockerImages.Items) - // { - // Information("Updating version for docker image '{0}'", dockerImage); - - // var projectFileName = GetProjectFileName(BuildContext, dockerImage); - - // TransformConfig(projectFileName, new TransformationCollection - // { - // { "Project/PropertyGroup/PackageVersion", VersionNuGet } - // }); - // } - } - - public override async Task BuildAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var dockerImage in BuildContext.DockerImages.Items) - { - BuildContext.CakeContext.LogSeparator("Building docker image '{0}'", dockerImage); - - var projectFileName = GetProjectFileName(BuildContext, dockerImage); - - var msBuildSettings = new MSBuildSettings - { - Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic - ToolVersion = MSBuildToolVersion.Default, - Configuration = BuildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, dockerImage, "build"); - - // Always disable SourceLink - msBuildSettings.WithProperty("EnableSourceLink", "false"); - - RunMsBuild(BuildContext, dockerImage, projectFileName, msBuildSettings, "build"); - } - } - - public override async Task PackageAsync() - { - if (!HasItems()) - { - return; - } - - // The following directories are being created, ready for docker images to be used: - // ./output => output of the publish step - // ./config => docker image and config files, in case they need to be packed as well - - foreach (var dockerImage in BuildContext.DockerImages.Items) - { - if (!ShouldPackageProject(BuildContext, dockerImage)) - { - CakeContext.Information("Docker image '{0}' should not be packaged", dockerImage); - continue; - } - - BuildContext.CakeContext.LogSeparator("Packaging docker image '{0}'", dockerImage); - - var projectFileName = GetProjectFileName(BuildContext, dockerImage); - var dockerImageSpecificationDirectory = System.IO.Path.Combine(".", "deployment", "docker", dockerImage); - var dockerImageSpecificationFileName = System.IO.Path.Combine(dockerImageSpecificationDirectory, dockerImage); - - var outputRootDirectory = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, dockerImage, "output"); - - CakeContext.Information("1) Preparing ./config for package '{0}'", dockerImage); - - // ./config - var confTargetDirectory = System.IO.Path.Combine(outputRootDirectory, "conf"); - CakeContext.Information("Conf directory: '{0}'", confTargetDirectory); - - CakeContext.CreateDirectory(confTargetDirectory); - - var confSourceDirectory = string.Format("{0}/*", dockerImageSpecificationDirectory); - CakeContext.Information("Copying files from '{0}' => '{1}'", confSourceDirectory, confTargetDirectory); - - CakeContext.CopyFiles(confSourceDirectory, confTargetDirectory, true); - - BuildContext.CakeContext.LogSeparator(); - - CakeContext.Information("2) Preparing ./output using 'dotnet publish' for package '{0}'", dockerImage); - - // ./output - var outputDirectory = System.IO.Path.Combine(outputRootDirectory, "output"); - CakeContext.Information("Output directory: '{0}'", outputDirectory); - - var msBuildSettings = new DotNetMSBuildSettings(); - - ConfigureMsBuildForDotNet(BuildContext, msBuildSettings, dockerImage, "pack"); - - msBuildSettings.WithProperty("ConfigurationName", BuildContext.General.Solution.ConfigurationName); - msBuildSettings.WithProperty("PackageVersion", BuildContext.General.Version.NuGet); - - // Disable code analyses, we experienced publish issues with mvc .net core projects - msBuildSettings.WithProperty("RunCodeAnalysis", "false"); - - var publishSettings = new DotNetPublishSettings - { - MSBuildSettings = msBuildSettings, - OutputDirectory = outputDirectory, - Configuration = BuildContext.General.Solution.ConfigurationName, - //NoBuild = true - }; - - CakeContext.DotNetPublish(projectFileName, publishSettings); - - BuildContext.CakeContext.LogSeparator(); - - CakeContext.Information("3) Using 'docker build' to package '{0}'", dockerImage); - - // docker build ..\..\output\Release\platform -f .\Dockerfile - - // From the docs (https://docs.microsoft.com/en-us/azure/app-service/containers/tutorial-custom-docker-image#use-a-docker-image-from-any-private-registry-optional), - // we need something like this: - // docker tag .azurecr.io/mydockerimage - var dockerRegistryUrl = GetDockerRegistryUrl(dockerImage); - - // Note: to prevent all output & source files to be copied to the docker context, we will set the - // output directory as context (to keep the footprint as small as possible) - - var dockerSettings = new DockerImageBuildSettings - { - NoCache = true, // Don't use cache, always make sure to fetch the right images - File = dockerImageSpecificationFileName, - //Platform = "linux", - Tag = GetDockerImageTags(dockerImage) - }; - - ConfigureDockerSettings(dockerSettings); - - CakeContext.Information("Docker files source directory: '{0}'", outputRootDirectory); - - CakeContext.DockerBuild(dockerSettings, outputRootDirectory); - - BuildContext.CakeContext.LogSeparator(); - } - } - - public override async Task DeployAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var dockerImage in BuildContext.DockerImages.Items) - { - if (!ShouldDeployProject(BuildContext, dockerImage)) - { - CakeContext.Information("Docker image '{0}' should not be deployed", dockerImage); - continue; - } - - BuildContext.CakeContext.LogSeparator("Deploying docker image '{0}'", dockerImage); - - var dockerRegistryUrl = GetDockerRegistryUrl(dockerImage); - var dockerRegistryUserName = GetDockerRegistryUserName(dockerImage); - var dockerRegistryPassword = GetDockerRegistryPassword(dockerImage); - var dockerImageName = GetDockerImageName(dockerImage); - var octopusRepositoryUrl = BuildContext.OctopusDeploy.GetRepositoryUrl(dockerImage); - var octopusRepositoryApiKey = BuildContext.OctopusDeploy.GetRepositoryApiKey(dockerImage); - var octopusDeploymentTarget = BuildContext.OctopusDeploy.GetDeploymentTarget(dockerImage); - - if (string.IsNullOrWhiteSpace(dockerRegistryUrl)) - { - throw new Exception("Docker registry url is empty, as a protection mechanism this must *always* be specified to make sure packages aren't accidentally deployed to some default public registry"); - } - - // Note: we are logging in each time because the registry might be different per container - CakeContext.Information("Logging in to docker @ '{0}'", dockerRegistryUrl); - - var dockerLoginSettings = new DockerRegistryLoginSettings - { - Username = dockerRegistryUserName, - Password = dockerRegistryPassword - }; - - ConfigureDockerSettings(dockerLoginSettings); - - CakeContext.DockerLogin(dockerLoginSettings, dockerRegistryUrl); - - try - { - foreach (var dockerImageTag in GetDockerImageTags(dockerImage)) - { - CakeContext.Information("Pushing docker images with tag '{0}' to '{1}'", dockerImageTag, dockerRegistryUrl); - - var dockerImagePushSettings = new DockerImagePushSettings - { - }; - - ConfigureDockerSettings(dockerImagePushSettings); - - CakeContext.DockerPush(dockerImagePushSettings, dockerImageTag); - - if (string.IsNullOrWhiteSpace(octopusRepositoryUrl)) - { - CakeContext.Warning("Octopus Deploy url is not specified, skipping deployment to Octopus Deploy"); - continue; - } - - var imageVersion = BuildContext.General.Version.NuGet; - - CakeContext.Information("Creating release '{0}' in Octopus Deploy", imageVersion); - - CakeContext.OctoCreateRelease(dockerImage, new CreateReleaseSettings - { - Server = octopusRepositoryUrl, - ApiKey = octopusRepositoryApiKey, - ReleaseNumber = imageVersion, - DefaultPackageVersion = imageVersion, - IgnoreExisting = true, - Packages = new Dictionary - { - { dockerImageName, imageVersion } - } - }); - - CakeContext.Information("Deploying release '{0}' via Octopus Deploy", imageVersion); - - CakeContext.OctoDeployRelease(octopusRepositoryUrl, octopusRepositoryApiKey, dockerImage, octopusDeploymentTarget, - imageVersion, new OctopusDeployReleaseDeploymentSettings - { - ShowProgress = true, - WaitForDeployment = true, - DeploymentTimeout = TimeSpan.FromMinutes(5), - CancelOnTimeout = true, - GuidedFailure = true, - Force = true, - NoRawLog = true, - }); - - await BuildContext.Notifications.NotifyAsync(dockerImage, string.Format("Deployed to Octopus Deploy"), TargetType.DockerImage); - } - } - finally - { - CakeContext.Information("Logging out of docker @ '{0}'", dockerRegistryUrl); - - var dockerLogoutSettings = new DockerRegistryLogoutSettings - { - }; - - ConfigureDockerSettings(dockerLogoutSettings); - - CakeContext.DockerLogout(dockerLogoutSettings, dockerRegistryUrl); - } - } - } - - public override async Task FinalizeAsync() - { - - } -} +#l "docker-variables.cake" +#l "lib-octopusdeploy.cake" + +#addin "nuget:?package=Cake.Docker&version=1.2.2" + +//------------------------------------------------------------- + +public class DockerImagesProcessor : ProcessorBase +{ + public DockerImagesProcessor(BuildContext buildContext) + : base(buildContext) + { + + } + + public override bool HasItems() + { + return BuildContext.DockerImages.Items.Count > 0; + } + + public string GetDockerRegistryUrl(string projectName) + { + // Allow per project overrides via "DockerRegistryUrlFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "DockerRegistryUrlFor", BuildContext.DockerImages.DockerRegistryUrl); + } + + public string GetDockerRegistryUserName(string projectName) + { + // Allow per project overrides via "DockerRegistryUserNameFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "DockerRegistryUserNameFor", BuildContext.DockerImages.DockerRegistryUserName); + } + + public string GetDockerRegistryPassword(string projectName) + { + // Allow per project overrides via "DockerRegistryPasswordFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "DockerRegistryPasswordFor", BuildContext.DockerImages.DockerRegistryPassword); + } + + private string GetDockerImageName(string projectName) + { + var name = projectName.Replace(".", "-"); + return name.ToLower(); + } + + private string GetDockerImageTag(string projectName, string version) + { + var dockerRegistryUrl = GetDockerRegistryUrl(projectName); + + var tag = string.Format("{0}/{1}:{2}", dockerRegistryUrl, GetDockerImageName(projectName), version); + return tag.TrimStart(' ', '/').ToLower(); + } + + private string[] GetDockerImageTags(string projectName) + { + var dockerTags = new List(); + + var versions = new List(); + + versions.Add(BuildContext.General.Version.NuGet); + + foreach (var version in new [] + { + BuildContext.General.Version.MajorMinor, + BuildContext.General.Version.Major + }) + { + var additionalTag = version; + + if (BuildContext.General.IsAlphaBuild) + { + additionalTag += "-alpha"; + } + + if (BuildContext.General.IsBetaBuild) + { + additionalTag += "-beta"; + } + + versions.Add(additionalTag); + } + + foreach (var version in versions) + { + dockerTags.Add(GetDockerImageTag(projectName, version)); + } + + if (BuildContext.General.IsAlphaBuild) + { + dockerTags.Add(GetDockerImageTag(projectName, "latest-alpha")); + } + + if (BuildContext.General.IsBetaBuild) + { + dockerTags.Add(GetDockerImageTag(projectName, "latest-beta")); + } + + if (BuildContext.General.IsOfficialBuild) + { + dockerTags.Add(GetDockerImageTag(projectName, "latest-stable")); + dockerTags.Add(GetDockerImageTag(projectName, "latest")); + } + + return dockerTags.ToArray(); + } + + private void ConfigureDockerSettings(AutoToolSettings dockerSettings) + { + var engineUrl = BuildContext.DockerImages.DockerEngineUrl; + if (!string.IsNullOrWhiteSpace(engineUrl)) + { + CakeContext.Information("Using remote docker engine: '{0}'", engineUrl); + + dockerSettings.ArgumentCustomization = args => args.Prepend($"-H {engineUrl}"); + //dockerSettings.BuildArg = new [] { $"DOCKER_HOST={engineUrl}" }; + } + } + + public override async Task PrepareAsync() + { + if (!HasItems()) + { + return; + } + + // Check whether projects should be processed, `.ToList()` + // is required to prevent issues with foreach + foreach (var dockerImage in BuildContext.DockerImages.Items.ToList()) + { + foreach (var imageTag in GetDockerImageTags(dockerImage)) + { + CakeContext.Information(imageTag); + } + + if (!ShouldProcessProject(BuildContext, dockerImage)) + { + BuildContext.DockerImages.Items.Remove(dockerImage); + } + } + } + + public override async Task UpdateInfoAsync() + { + if (!HasItems()) + { + return; + } + + // Doesn't seem neccessary yet + // foreach (var dockerImage in BuildContext.DockerImages.Items) + // { + // Information("Updating version for docker image '{0}'", dockerImage); + + // var projectFileName = GetProjectFileName(BuildContext, dockerImage); + + // TransformConfig(projectFileName, new TransformationCollection + // { + // { "Project/PropertyGroup/PackageVersion", VersionNuGet } + // }); + // } + } + + public override async Task BuildAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var dockerImage in BuildContext.DockerImages.Items) + { + BuildContext.CakeContext.LogSeparator("Building docker image '{0}'", dockerImage); + + var projectFileName = GetProjectFileName(BuildContext, dockerImage); + + var msBuildSettings = new MSBuildSettings + { + Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic + ToolVersion = MSBuildToolVersion.Default, + Configuration = BuildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, dockerImage, "build"); + + // Always disable SourceLink + msBuildSettings.WithProperty("EnableSourceLink", "false"); + + RunMsBuild(BuildContext, dockerImage, projectFileName, msBuildSettings, "build"); + } + } + + public override async Task PackageAsync() + { + if (!HasItems()) + { + return; + } + + // The following directories are being created, ready for docker images to be used: + // ./output => output of the publish step + // ./config => docker image and config files, in case they need to be packed as well + + foreach (var dockerImage in BuildContext.DockerImages.Items) + { + if (!ShouldPackageProject(BuildContext, dockerImage)) + { + CakeContext.Information("Docker image '{0}' should not be packaged", dockerImage); + continue; + } + + BuildContext.CakeContext.LogSeparator("Packaging docker image '{0}'", dockerImage); + + var projectFileName = GetProjectFileName(BuildContext, dockerImage); + var dockerImageSpecificationDirectory = System.IO.Path.Combine(".", "deployment", "docker", dockerImage); + var dockerImageSpecificationFileName = System.IO.Path.Combine(dockerImageSpecificationDirectory, dockerImage); + + var outputRootDirectory = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, dockerImage, "output"); + + CakeContext.Information("1) Preparing ./config for package '{0}'", dockerImage); + + // ./config + var confTargetDirectory = System.IO.Path.Combine(outputRootDirectory, "conf"); + CakeContext.Information("Conf directory: '{0}'", confTargetDirectory); + + CakeContext.CreateDirectory(confTargetDirectory); + + var confSourceDirectory = string.Format("{0}/*", dockerImageSpecificationDirectory); + CakeContext.Information("Copying files from '{0}' => '{1}'", confSourceDirectory, confTargetDirectory); + + CakeContext.CopyFiles(confSourceDirectory, confTargetDirectory, true); + + BuildContext.CakeContext.LogSeparator(); + + CakeContext.Information("2) Preparing ./output using 'dotnet publish' for package '{0}'", dockerImage); + + // ./output + var outputDirectory = System.IO.Path.Combine(outputRootDirectory, "output"); + CakeContext.Information("Output directory: '{0}'", outputDirectory); + + var msBuildSettings = new DotNetMSBuildSettings(); + + ConfigureMsBuildForDotNet(BuildContext, msBuildSettings, dockerImage, "pack"); + + msBuildSettings.WithProperty("ConfigurationName", BuildContext.General.Solution.ConfigurationName); + msBuildSettings.WithProperty("PackageVersion", BuildContext.General.Version.NuGet); + + // Disable code analyses, we experienced publish issues with mvc .net core projects + msBuildSettings.WithProperty("RunCodeAnalysis", "false"); + + var publishSettings = new DotNetPublishSettings + { + MSBuildSettings = msBuildSettings, + OutputDirectory = outputDirectory, + Configuration = BuildContext.General.Solution.ConfigurationName, + //NoBuild = true + }; + + CakeContext.DotNetPublish(projectFileName, publishSettings); + + BuildContext.CakeContext.LogSeparator(); + + CakeContext.Information("3) Using 'docker build' to package '{0}'", dockerImage); + + // docker build ..\..\output\Release\platform -f .\Dockerfile + + // From the docs (https://docs.microsoft.com/en-us/azure/app-service/containers/tutorial-custom-docker-image#use-a-docker-image-from-any-private-registry-optional), + // we need something like this: + // docker tag .azurecr.io/mydockerimage + var dockerRegistryUrl = GetDockerRegistryUrl(dockerImage); + + // Note: to prevent all output & source files to be copied to the docker context, we will set the + // output directory as context (to keep the footprint as small as possible) + + var dockerSettings = new DockerImageBuildSettings + { + NoCache = true, // Don't use cache, always make sure to fetch the right images + File = dockerImageSpecificationFileName, + //Platform = "linux", + Tag = GetDockerImageTags(dockerImage) + }; + + ConfigureDockerSettings(dockerSettings); + + CakeContext.Information("Docker files source directory: '{0}'", outputRootDirectory); + + CakeContext.DockerBuild(dockerSettings, outputRootDirectory); + + BuildContext.CakeContext.LogSeparator(); + } + } + + public override async Task DeployAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var dockerImage in BuildContext.DockerImages.Items) + { + if (!ShouldDeployProject(BuildContext, dockerImage)) + { + CakeContext.Information("Docker image '{0}' should not be deployed", dockerImage); + continue; + } + + BuildContext.CakeContext.LogSeparator("Deploying docker image '{0}'", dockerImage); + + var dockerRegistryUrl = GetDockerRegistryUrl(dockerImage); + var dockerRegistryUserName = GetDockerRegistryUserName(dockerImage); + var dockerRegistryPassword = GetDockerRegistryPassword(dockerImage); + var dockerImageName = GetDockerImageName(dockerImage); + var octopusRepositoryUrl = BuildContext.OctopusDeploy.GetRepositoryUrl(dockerImage); + var octopusRepositoryApiKey = BuildContext.OctopusDeploy.GetRepositoryApiKey(dockerImage); + var octopusDeploymentTarget = BuildContext.OctopusDeploy.GetDeploymentTarget(dockerImage); + + if (string.IsNullOrWhiteSpace(dockerRegistryUrl)) + { + throw new Exception("Docker registry url is empty, as a protection mechanism this must *always* be specified to make sure packages aren't accidentally deployed to some default public registry"); + } + + // Note: we are logging in each time because the registry might be different per container + CakeContext.Information("Logging in to docker @ '{0}'", dockerRegistryUrl); + + var dockerLoginSettings = new DockerRegistryLoginSettings + { + Username = dockerRegistryUserName, + Password = dockerRegistryPassword + }; + + ConfigureDockerSettings(dockerLoginSettings); + + CakeContext.DockerLogin(dockerLoginSettings, dockerRegistryUrl); + + try + { + foreach (var dockerImageTag in GetDockerImageTags(dockerImage)) + { + CakeContext.Information("Pushing docker images with tag '{0}' to '{1}'", dockerImageTag, dockerRegistryUrl); + + var dockerImagePushSettings = new DockerImagePushSettings + { + }; + + ConfigureDockerSettings(dockerImagePushSettings); + + CakeContext.DockerPush(dockerImagePushSettings, dockerImageTag); + + if (string.IsNullOrWhiteSpace(octopusRepositoryUrl)) + { + CakeContext.Warning("Octopus Deploy url is not specified, skipping deployment to Octopus Deploy"); + continue; + } + + var imageVersion = BuildContext.General.Version.NuGet; + + CakeContext.Information("Creating release '{0}' in Octopus Deploy", imageVersion); + + CakeContext.OctoCreateRelease(dockerImage, new CreateReleaseSettings + { + Server = octopusRepositoryUrl, + ApiKey = octopusRepositoryApiKey, + ReleaseNumber = imageVersion, + DefaultPackageVersion = imageVersion, + IgnoreExisting = true, + Packages = new Dictionary + { + { dockerImageName, imageVersion } + } + }); + + CakeContext.Information("Deploying release '{0}' via Octopus Deploy", imageVersion); + + CakeContext.OctoDeployRelease(octopusRepositoryUrl, octopusRepositoryApiKey, dockerImage, octopusDeploymentTarget, + imageVersion, new OctopusDeployReleaseDeploymentSettings + { + ShowProgress = true, + WaitForDeployment = true, + DeploymentTimeout = TimeSpan.FromMinutes(5), + CancelOnTimeout = true, + GuidedFailure = true, + Force = true, + NoRawLog = true, + }); + + await BuildContext.Notifications.NotifyAsync(dockerImage, string.Format("Deployed to Octopus Deploy"), TargetType.DockerImage); + } + } + finally + { + CakeContext.Information("Logging out of docker @ '{0}'", dockerRegistryUrl); + + var dockerLogoutSettings = new DockerRegistryLogoutSettings + { + }; + + ConfigureDockerSettings(dockerLogoutSettings); + + CakeContext.DockerLogout(dockerLogoutSettings, dockerRegistryUrl); + } + } + } + + public override async Task FinalizeAsync() + { + + } +} diff --git a/deployment/cake/docker-variables.cake b/deployment/cake/docker-variables.cake index ef23d957..b129cf47 100644 --- a/deployment/cake/docker-variables.cake +++ b/deployment/cake/docker-variables.cake @@ -1,58 +1,58 @@ -#l "buildserver.cake" - -//------------------------------------------------------------- - -public class DockerImagesContext : BuildContextWithItemsBase -{ - public DockerImagesContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string DockerEngineUrl { get; set; } - public string DockerRegistryUrl { get; set; } - public string DockerRegistryUserName { get; set; } - public string DockerRegistryPassword { get; set; } - - protected override void ValidateContext() - { - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' docker image projects"); - } -} - -//------------------------------------------------------------- - -private DockerImagesContext InitializeDockerImagesContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new DockerImagesContext(parentBuildContext) - { - Items = DockerImages ?? new List(), - DockerEngineUrl = buildContext.BuildServer.GetVariable("DockerEngineUrl", showValue: true), - DockerRegistryUrl = buildContext.BuildServer.GetVariable("DockerRegistryUrl", showValue: true), - DockerRegistryUserName = buildContext.BuildServer.GetVariable("DockerRegistryUserName", showValue: false), - DockerRegistryPassword = buildContext.BuildServer.GetVariable("DockerRegistryPassword", showValue: false) - }; - - return data; -} - -//------------------------------------------------------------- - -List _dockerImages; - -public List DockerImages -{ - get - { - if (_dockerImages is null) - { - _dockerImages = new List(); - } - - return _dockerImages; - } +#l "buildserver.cake" + +//------------------------------------------------------------- + +public class DockerImagesContext : BuildContextWithItemsBase +{ + public DockerImagesContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string DockerEngineUrl { get; set; } + public string DockerRegistryUrl { get; set; } + public string DockerRegistryUserName { get; set; } + public string DockerRegistryPassword { get; set; } + + protected override void ValidateContext() + { + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' docker image projects"); + } +} + +//------------------------------------------------------------- + +private DockerImagesContext InitializeDockerImagesContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new DockerImagesContext(parentBuildContext) + { + Items = DockerImages ?? new List(), + DockerEngineUrl = buildContext.BuildServer.GetVariable("DockerEngineUrl", showValue: true), + DockerRegistryUrl = buildContext.BuildServer.GetVariable("DockerRegistryUrl", showValue: true), + DockerRegistryUserName = buildContext.BuildServer.GetVariable("DockerRegistryUserName", showValue: false), + DockerRegistryPassword = buildContext.BuildServer.GetVariable("DockerRegistryPassword", showValue: false) + }; + + return data; +} + +//------------------------------------------------------------- + +List _dockerImages; + +public List DockerImages +{ + get + { + if (_dockerImages is null) + { + _dockerImages = new List(); + } + + return _dockerImages; + } } \ No newline at end of file diff --git a/deployment/cake/generic-tasks.cake b/deployment/cake/generic-tasks.cake index d06bfaa9..41c71396 100644 --- a/deployment/cake/generic-tasks.cake +++ b/deployment/cake/generic-tasks.cake @@ -1,345 +1,297 @@ -#l "generic-variables.cake" - -//#addin "nuget:?package=Cake.DependencyCheck&version=1.2.0" - -//#tool "nuget:?package=DependencyCheck.Runner.Tool&version=3.2.1&include=./**/dependency-check.sh&include=./**/dependency-check.bat" - -//------------------------------------------------------------- - -private void ValidateRequiredInput(string parameterName) -{ - // TODO: Do we want to check the configuration as well? - - if (!Parameters.ContainsKey(parameterName)) - { - throw new Exception(string.Format("Parameter '{0}' is required but not defined", parameterName)); - } -} - -//------------------------------------------------------------- - -private void CleanUpCode(bool failOnChanges) -{ - Information("Cleaning up code using dotnet-format"); - - // --check: return non-0 exit code if changes are needed - // --dry-run: don't save files - - // Note: disabled for now, see: - // * https://github.com/onovotny/MSBuildSdkExtras/issues/164 - // * https://github.com/microsoft/msbuild/issues/4376 - // var arguments = new List(); - - // //arguments.Add("--dry-run"); - - // if (failOnChanges) - // { - // arguments.Add("--check"); - // } - - // DotNetTool(null, "format", string.Join(" ", arguments), - // new DotNetToolSettings - // { - // WorkingDirectory = "./src/" - // }); -} - -//------------------------------------------------------------- - -private void VerifyDependencies(string pathToScan = "./src/**/*.csproj") -{ - Information("Verifying dependencies for security vulnerabilities in '{0}'", pathToScan); - - // Disabled for now - //DependencyCheck(new DependencyCheckSettings - //{ - // Project = SolutionName, - // Scan = pathToScan, - // FailOnCVSS = "0", - // Format = "HTML", - // Data = "%temp%/dependency-check/data" - //}); -} - -//------------------------------------------------------------- - -private void UpdateSolutionAssemblyInfo(BuildContext buildContext) -{ - Information("Updating assembly info to '{0}'", buildContext.General.Version.FullSemVer); - - var assemblyInfoParseResult = ParseAssemblyInfo(buildContext.General.Solution.AssemblyInfoFileName); - - var assemblyInfo = new AssemblyInfoSettings - { - Company = buildContext.General.Copyright.Company, - Version = buildContext.General.Version.MajorMinorPatch, - FileVersion = buildContext.General.Version.MajorMinorPatch, - InformationalVersion = buildContext.General.Version.FullSemVer, - Copyright = string.Format("Copyright © {0} {1} - {2}", - buildContext.General.Copyright.Company, buildContext.General.Copyright.StartYear, DateTime.Now.Year) - }; - - CreateAssemblyInfo(buildContext.General.Solution.AssemblyInfoFileName, assemblyInfo); -} - -//------------------------------------------------------------- - -Task("UpdateNuGet") - .ContinueOnError() - .Does(buildContext => -{ - // DISABLED UNTIL NUGET GETS FIXED: https://github.com/NuGet/Home/issues/10853 - - // Information("Making sure NuGet is using the latest version"); - - // if (buildContext.General.IsLocalBuild && buildContext.General.MaximizePerformance) - // { - // Information("Local build with maximized performance detected, skipping NuGet update check"); - // return; - // } - - // var nuGetExecutable = buildContext.General.NuGet.Executable; - - // var exitCode = StartProcess(nuGetExecutable, new ProcessSettings - // { - // Arguments = "update -self" - // }); - - // var newNuGetVersionInfo = System.Diagnostics.FileVersionInfo.GetVersionInfo(nuGetExecutable); - // var newNuGetVersion = newNuGetVersionInfo.FileVersion; - - // Information("Updating NuGet.exe exited with '{0}', version is '{1}'", exitCode, newNuGetVersion); -}); - -//------------------------------------------------------------- - -Task("RestorePackages") - .IsDependentOn("Prepare") - .IsDependentOn("UpdateNuGet") - .ContinueOnError() - .Does(buildContext => -{ - if (buildContext.General.IsLocalBuild && buildContext.General.MaximizePerformance) - { - Information("Local build with maximized performance detected, skipping package restore"); - return; - } - - //var csharpProjects = GetFiles("./**/*.csproj"); - // var cProjects = GetFiles("./**/*.vcxproj"); - var solutions = GetFiles("./**/*.sln"); - var csharpProjects = new List(); - - foreach (var project in buildContext.AllProjects) - { - // Once a project is in AllProjects, it should always be restored - - var projectFileName = GetProjectFileName(buildContext, project); - if (projectFileName.EndsWith(".csproj")) - { - Information("Adding '{0}' as C# specific project to restore", project); - - csharpProjects.Add(projectFileName); - - // Inject source link *before* package restore - InjectSourceLinkInProjectFile(buildContext, project, projectFileName); - } - } - - var allFiles = new List(); - //allFiles.AddRange(solutions); - allFiles.AddRange(csharpProjects); - // //allFiles.AddRange(cProjects); - - Information($"Found '{allFiles.Count}' projects to restore"); - - foreach (var file in allFiles) - { - RestoreNuGetPackages(buildContext, file); - } - - // C++ files need to be done manually - foreach (var project in buildContext.AllProjects) - { - var projectFileName = GetProjectFileName(buildContext, project); - if (IsCppProject(projectFileName)) - { - buildContext.CakeContext.LogSeparator("'{0}' is a C++ project, restoring NuGet packages separately", project); - - RestoreNuGetPackages(buildContext, projectFileName); - - // For C++ projects, we must clean the project again after a package restore - CleanProject(buildContext, project); - } - } -}); - -//------------------------------------------------------------- - -// Note: it might look weird that this is dependent on restore packages, -// but to clean, the msbuild projects must be able to load. However, they need -// some targets files that come in via packages - -Task("Clean") - //.IsDependentOn("RestorePackages") - .IsDependentOn("Prepare") - .ContinueOnError() - .Does(buildContext => -{ - if (buildContext.General.IsLocalBuild && buildContext.General.MaximizePerformance) - { - Information("Local build with maximized performance detected, skipping solution clean"); - return; - } - - var platforms = new Dictionary(); - platforms["AnyCPU"] = PlatformTarget.MSIL; - platforms["x86"] = PlatformTarget.x86; - platforms["x64"] = PlatformTarget.x64; - platforms["arm"] = PlatformTarget.ARM; - - foreach (var platform in platforms) - { - try - { - Information("Cleaning output for platform '{0}'", platform.Value); - - var msBuildSettings = new MSBuildSettings - { - Verbosity = Verbosity.Minimal, - ToolVersion = MSBuildToolVersion.Default, - Configuration = buildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = platform.Value - }; - - ConfigureMsBuild(buildContext, msBuildSettings, platform.Key, "clean"); - - msBuildSettings.Targets.Add("Clean"); - - MSBuild(buildContext.General.Solution.FileName, msBuildSettings); - } - catch (System.Exception ex) - { - Warning("Failed to clean output for platform '{0}': {1}", platform.Value, ex.Message); - } - } - - // Output directory - DeleteDirectoryWithLogging(buildContext, buildContext.General.OutputRootDirectory); - - // obj directories - foreach (var project in buildContext.AllProjects) - { - CleanProject(buildContext, project); - } -}); - -//------------------------------------------------------------- - -Task("VerifyDependencies") - .IsDependentOn("Prepare") - .Does(async () => -{ - // if (DependencyCheckDisabled) - // { - // Information("Dependency analysis is disabled"); - // return; - // } - - // VerifyDependencies(); -}); - -//------------------------------------------------------------- - -Task("CleanupCode") - .Does(buildContext => -{ - CleanUpCode(true); -}); - -//------------------------------------------------------------- - -Task("CodeSign") - //.ContinueOnError() - .Does(buildContext => -{ - if (buildContext.General.IsCiBuild) - { - Information("Skipping code signing because this is a CI build"); - return; - } - - if (buildContext.General.IsLocalBuild) - { - Information("Local build detected, skipping code signing"); - return; - } - - var certificateSubjectName = buildContext.General.CodeSign.CertificateSubjectName; - if (string.IsNullOrWhiteSpace(certificateSubjectName)) - { - Information("Skipping code signing because the certificate subject name was not specified"); - return; - } - - var filesToSign = new List(); - - // Note: only code-sign components & wpf apps, skip test projects & uwp apps - var projectsToCodeSign = new List(); - projectsToCodeSign.AddRange(buildContext.Components.Items); - projectsToCodeSign.AddRange(buildContext.Wpf.Items); - - foreach (var projectToCodeSign in projectsToCodeSign) - { - var codeSignWildCard = buildContext.General.CodeSign.WildCard; - if (string.IsNullOrWhiteSpace(codeSignWildCard)) - { - // Empty, we need to override with project name for valid default value - codeSignWildCard = projectToCodeSign; - } - - var outputDirectory = string.Format("{0}/{1}", buildContext.General.OutputRootDirectory, projectToCodeSign); - - var projectFilesToSign = new List(); - - var exeSignFilesSearchPattern = string.Format("{0}/**/*{1}*.exe", outputDirectory, codeSignWildCard); - Information(exeSignFilesSearchPattern); - projectFilesToSign.AddRange(GetFiles(exeSignFilesSearchPattern)); - - var dllSignFilesSearchPattern = string.Format("{0}/**/*{1}*.dll", outputDirectory, codeSignWildCard); - Information(dllSignFilesSearchPattern); - projectFilesToSign.AddRange(GetFiles(dllSignFilesSearchPattern)); - - Information("Found '{0}' files to code sign for '{1}'", projectFilesToSign.Count, projectToCodeSign); - - filesToSign.AddRange(projectFilesToSign); - } - - var signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", buildContext.General.CodeSign.TimeStampUri, - certificateSubjectName, buildContext.General.CodeSign.HashAlgorithm); - - SignFiles(buildContext, signToolCommand, filesToSign); - - // var signToolSignSettings = new SignToolSignSettings - // { - // AppendSignature = false, - // TimeStampUri = new Uri(buildContext.General.CodeSign.TimeStampUri), - // CertSubjectName = certificateSubjectName - // }; - - // Sign(filesToSign, signToolSignSettings); - - // Note parallel doesn't seem to be faster in an example repository: - // 1 thread: 1m 30s - // 4 threads: 1m 30s - // 10 threads: 1m 30s - // Parallel.ForEach(filesToSign, new ParallelOptions - // { - // MaxDegreeOfParallelism = 10 - // }, - // fileToSign => - // { - // Sign(fileToSign, signToolSignSettings); - // }); +#l "generic-variables.cake" + +//#addin "nuget:?package=Cake.DependencyCheck&version=1.2.0" + +//#tool "nuget:?package=DependencyCheck.Runner.Tool&version=3.2.1&include=./**/dependency-check.sh&include=./**/dependency-check.bat" + +//------------------------------------------------------------- + +private void ValidateRequiredInput(string parameterName) +{ + // TODO: Do we want to check the configuration as well? + + if (!Parameters.ContainsKey(parameterName)) + { + throw new Exception(string.Format("Parameter '{0}' is required but not defined", parameterName)); + } +} + +//------------------------------------------------------------- + +private void CleanUpCode(bool failOnChanges) +{ + Information("Cleaning up code using dotnet-format"); + + // --check: return non-0 exit code if changes are needed + // --dry-run: don't save files + + // Note: disabled for now, see: + // * https://github.com/onovotny/MSBuildSdkExtras/issues/164 + // * https://github.com/microsoft/msbuild/issues/4376 + // var arguments = new List(); + + // //arguments.Add("--dry-run"); + + // if (failOnChanges) + // { + // arguments.Add("--check"); + // } + + // DotNetTool(null, "format", string.Join(" ", arguments), + // new DotNetToolSettings + // { + // WorkingDirectory = "./src/" + // }); +} + +//------------------------------------------------------------- + +private void VerifyDependencies(string pathToScan = "./src/**/*.csproj") +{ + Information("Verifying dependencies for security vulnerabilities in '{0}'", pathToScan); + + // Disabled for now + //DependencyCheck(new DependencyCheckSettings + //{ + // Project = SolutionName, + // Scan = pathToScan, + // FailOnCVSS = "0", + // Format = "HTML", + // Data = "%temp%/dependency-check/data" + //}); +} + +//------------------------------------------------------------- + +private void UpdateSolutionAssemblyInfo(BuildContext buildContext) +{ + Information("Updating assembly info to '{0}'", buildContext.General.Version.FullSemVer); + + var assemblyInfoParseResult = ParseAssemblyInfo(buildContext.General.Solution.AssemblyInfoFileName); + + var assemblyInfo = new AssemblyInfoSettings + { + Company = buildContext.General.Copyright.Company, + Version = buildContext.General.Version.MajorMinorPatch, + FileVersion = buildContext.General.Version.MajorMinorPatch, + InformationalVersion = buildContext.General.Version.FullSemVer, + Copyright = string.Format("Copyright © {0} {1} - {2}", + buildContext.General.Copyright.Company, buildContext.General.Copyright.StartYear, DateTime.Now.Year) + }; + + CreateAssemblyInfo(buildContext.General.Solution.AssemblyInfoFileName, assemblyInfo); +} + +//------------------------------------------------------------- + +Task("UpdateNuGet") + .ContinueOnError() + .Does(buildContext => +{ + // DISABLED UNTIL NUGET GETS FIXED: https://github.com/NuGet/Home/issues/10853 + + // Information("Making sure NuGet is using the latest version"); + + // if (buildContext.General.IsLocalBuild && buildContext.General.MaximizePerformance) + // { + // Information("Local build with maximized performance detected, skipping NuGet update check"); + // return; + // } + + // var nuGetExecutable = buildContext.General.NuGet.Executable; + + // var exitCode = StartProcess(nuGetExecutable, new ProcessSettings + // { + // Arguments = "update -self" + // }); + + // var newNuGetVersionInfo = System.Diagnostics.FileVersionInfo.GetVersionInfo(nuGetExecutable); + // var newNuGetVersion = newNuGetVersionInfo.FileVersion; + + // Information("Updating NuGet.exe exited with '{0}', version is '{1}'", exitCode, newNuGetVersion); +}); + +//------------------------------------------------------------- + +Task("RestorePackages") + .IsDependentOn("Prepare") + .IsDependentOn("UpdateNuGet") + .ContinueOnError() + .Does(buildContext => +{ + if (buildContext.General.IsLocalBuild && buildContext.General.MaximizePerformance) + { + Information("Local build with maximized performance detected, skipping package restore"); + return; + } + + //var csharpProjects = GetFiles("./**/*.csproj"); + // var cProjects = GetFiles("./**/*.vcxproj"); + var solutions = GetFiles("./**/*.sln"); + var csharpProjects = new List(); + + foreach (var project in buildContext.AllProjects) + { + // Once a project is in AllProjects, it should always be restored + + var projectFileName = GetProjectFileName(buildContext, project); + if (projectFileName.EndsWith(".csproj")) + { + Information("Adding '{0}' as C# specific project to restore", project); + + csharpProjects.Add(projectFileName); + + // Inject source link *before* package restore + InjectSourceLinkInProjectFile(buildContext, project, projectFileName); + } + } + + var allFiles = new List(); + //allFiles.AddRange(solutions); + allFiles.AddRange(csharpProjects); + // //allFiles.AddRange(cProjects); + + Information($"Found '{allFiles.Count}' projects to restore"); + + foreach (var file in allFiles) + { + RestoreNuGetPackages(buildContext, file); + } + + // C++ files need to be done manually + foreach (var project in buildContext.AllProjects) + { + var projectFileName = GetProjectFileName(buildContext, project); + if (IsCppProject(projectFileName)) + { + buildContext.CakeContext.LogSeparator("'{0}' is a C++ project, restoring NuGet packages separately", project); + + RestoreNuGetPackages(buildContext, projectFileName); + + // For C++ projects, we must clean the project again after a package restore + CleanProject(buildContext, project); + } + } +}); + +//------------------------------------------------------------- + +// Note: it might look weird that this is dependent on restore packages, +// but to clean, the msbuild projects must be able to load. However, they need +// some targets files that come in via packages + +Task("Clean") + //.IsDependentOn("RestorePackages") + .IsDependentOn("Prepare") + .ContinueOnError() + .Does(buildContext => +{ + if (buildContext.General.IsLocalBuild && buildContext.General.MaximizePerformance) + { + Information("Local build with maximized performance detected, skipping solution clean"); + return; + } + + var platforms = new Dictionary(); + platforms["AnyCPU"] = PlatformTarget.MSIL; + platforms["x86"] = PlatformTarget.x86; + platforms["x64"] = PlatformTarget.x64; + platforms["arm"] = PlatformTarget.ARM; + + foreach (var platform in platforms) + { + try + { + Information("Cleaning output for platform '{0}'", platform.Value); + + var msBuildSettings = new MSBuildSettings + { + Verbosity = Verbosity.Minimal, + ToolVersion = MSBuildToolVersion.Default, + Configuration = buildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = platform.Value + }; + + ConfigureMsBuild(buildContext, msBuildSettings, platform.Key, "clean"); + + msBuildSettings.Targets.Add("Clean"); + + MSBuild(buildContext.General.Solution.FileName, msBuildSettings); + } + catch (System.Exception ex) + { + Warning("Failed to clean output for platform '{0}': {1}", platform.Value, ex.Message); + } + } + + // Output directory + DeleteDirectoryWithLogging(buildContext, buildContext.General.OutputRootDirectory); + + // obj directories + foreach (var project in buildContext.AllProjects) + { + CleanProject(buildContext, project); + } +}); + +//------------------------------------------------------------- + +Task("VerifyDependencies") + .IsDependentOn("Prepare") + .Does(async () => +{ + // if (DependencyCheckDisabled) + // { + // Information("Dependency analysis is disabled"); + // return; + // } + + // VerifyDependencies(); +}); + +//------------------------------------------------------------- + +Task("CleanupCode") + .Does(buildContext => +{ + CleanUpCode(true); +}); + +//------------------------------------------------------------- + +Task("CodeSign") + //.ContinueOnError() + .Does(buildContext => +{ + if (buildContext.General.IsCiBuild) + { + Information("Skipping code signing because this is a CI build"); + return; + } + + if (buildContext.General.IsLocalBuild) + { + Information("Local build detected, skipping code signing"); + return; + } + + var certificateSubjectName = buildContext.General.CodeSign.CertificateSubjectName; + if (string.IsNullOrWhiteSpace(certificateSubjectName)) + { + Information("Skipping code signing because the certificate subject name was not specified"); + return; + } + + var filesToSign = new List(); + + // Note: only code-sign components & wpf apps, skip test projects & uwp apps + var projectsToCodeSign = new List(); + projectsToCodeSign.AddRange(buildContext.Components.Items); + projectsToCodeSign.AddRange(buildContext.Wpf.Items); + + foreach (var projectToCodeSign in projectsToCodeSign) + { + SignProjectFiles(buildContext, projectToCodeSign); + } }); \ No newline at end of file diff --git a/deployment/cake/generic-variables.cake b/deployment/cake/generic-variables.cake index 167fa654..a3a34d2a 100644 --- a/deployment/cake/generic-variables.cake +++ b/deployment/cake/generic-variables.cake @@ -1,618 +1,618 @@ -#l "buildserver.cake" - -#tool "nuget:?package=GitVersion.CommandLine&version=5.12.0" - -//------------------------------------------------------------- - -public class GeneralContext : BuildContextWithItemsBase -{ - public GeneralContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - SkipComponentsThatAreNotDeployable = true; - EnableMsBuildBinaryLog = true; - EnableMsBuildFileLog = true; - EnableMsBuildXmlLog = true; - } - - public string Target { get; set; } - public string RootDirectory { get; set; } - public string OutputRootDirectory { get; set; } - - public bool IsCiBuild { get; set; } - public bool IsAlphaBuild { get; set; } - public bool IsBetaBuild { get; set; } - public bool IsOfficialBuild { get; set; } - public bool IsLocalBuild { get; set; } - public bool MaximizePerformance { get; set; } - public bool UseVisualStudioPrerelease { get; set; } - public bool VerifyDependencies { get; set; } - public bool SkipComponentsThatAreNotDeployable { get; set; } - - public bool EnableMsBuildBinaryLog { get; set; } - public bool EnableMsBuildFileLog { get; set; } - public bool EnableMsBuildXmlLog { get; set; } - - public VersionContext Version { get; set; } - public CopyrightContext Copyright { get; set; } - public NuGetContext NuGet { get; set; } - public SolutionContext Solution { get; set; } - public SourceLinkContext SourceLink { get; set; } - public CodeSignContext CodeSign { get; set; } - public RepositoryContext Repository { get; set; } - public SonarQubeContext SonarQube { get; set; } - - public List Includes { get; set; } - public List Excludes { get; set; } - - protected override void ValidateContext() - { - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Running target '{Target}'"); - CakeContext.Information($"Using output directory '{OutputRootDirectory}'"); - } -} - -//------------------------------------------------------------- - -public class VersionContext : BuildContextBase -{ - private GitVersion _gitVersionContext; - - public VersionContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public GitVersion GetGitVersionContext(GeneralContext generalContext) - { - if (_gitVersionContext is null) - { - var gitVersionSettings = new GitVersionSettings - { - UpdateAssemblyInfo = false, - Verbosity = GitVersionVerbosity.Verbose - }; - - var gitDirectory = ".git"; - if (!CakeContext.DirectoryExists(gitDirectory)) - { - CakeContext.Information("No local .git directory found, treating as dynamic repository"); - - // Make a *BIG* assumption that the solution name == repository name - var repositoryName = generalContext.Solution.Name; - var dynamicRepositoryPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), repositoryName); - - if (ClearCache) - { - CakeContext.Warning("Cleaning the cloned temp directory, disable by setting 'GitVersion_ClearCache' to 'false'"); - - if (CakeContext.DirectoryExists(dynamicRepositoryPath)) - { - CakeContext.DeleteDirectory(dynamicRepositoryPath, new DeleteDirectorySettings - { - Force = true, - Recursive = true - }); - } - } - - // Validate first - if (string.IsNullOrWhiteSpace(generalContext.Repository.BranchName)) - { - throw new Exception("No local .git directory was found, but repository branch was not specified either. Make sure to specify the branch"); - } - - if (string.IsNullOrWhiteSpace(generalContext.Repository.Url)) - { - throw new Exception("No local .git directory was found, but repository url was not specified either. Make sure to specify the branch"); - } - - CakeContext.Information($"Fetching dynamic repository from url '{generalContext.Repository.Url}' => '{dynamicRepositoryPath}'"); - - // Dynamic repository - gitVersionSettings.UserName = generalContext.Repository.Username; - gitVersionSettings.Password = generalContext.Repository.Password; - gitVersionSettings.Url = generalContext.Repository.Url; - gitVersionSettings.Branch = generalContext.Repository.BranchName; - gitVersionSettings.Commit = generalContext.Repository.CommitId; - gitVersionSettings.NoFetch = false; - gitVersionSettings.WorkingDirectory = generalContext.RootDirectory; - gitVersionSettings.DynamicRepositoryPath = dynamicRepositoryPath; - gitVersionSettings.Verbosity = GitVersionVerbosity.Verbose; - } - - _gitVersionContext = CakeContext.GitVersion(gitVersionSettings); - } - - return _gitVersionContext; - } - - public bool ClearCache { get; set; } - - private string _major; - - public string Major - { - get - { - if (string.IsNullOrWhiteSpace(_major)) - { - _major = GetVersion(MajorMinorPatch, 1); - } - - return _major; - } - } - - private string _majorMinor; - - public string MajorMinor - { - get - { - if (string.IsNullOrWhiteSpace(_majorMinor)) - { - _majorMinor = GetVersion(MajorMinorPatch, 2); - } - - return _majorMinor; - } - } - - public string MajorMinorPatch { get; set; } - public string FullSemVer { get; set; } - public string NuGet { get; set; } - public string CommitsSinceVersionSource { get; set; } - - private string GetVersion(string version, int breakCount) - { - var finalVersion = string.Empty; - - for (int i = 0; i < version.Length; i++) - { - var character = version[i]; - if (!char.IsDigit(character)) - { - breakCount--; - if (breakCount <= 0) - { - break; - } - } - - finalVersion += character.ToString(); - } - - return finalVersion; - } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - - } -} - -//------------------------------------------------------------- - -public class CopyrightContext : BuildContextBase -{ - public CopyrightContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string Company { get; set; } - public string StartYear { get; set; } - - protected override void ValidateContext() - { - if (string.IsNullOrWhiteSpace(Company)) - { - throw new Exception($"Company must be defined"); - } - } - - protected override void LogStateInfoForContext() - { - - } -} - -//------------------------------------------------------------- - -public class NuGetContext : BuildContextBase -{ - public NuGetContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string PackageSources { get; set; } - public string Executable { get; set; } - public string LocalPackagesDirectory { get; set; } - - public bool RestoreUsingNuGet { get; set; } - public bool RestoreUsingDotNetRestore { get; set; } - public bool NoDependencies { get; set; } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Restore using NuGet: '{RestoreUsingNuGet}'"); - CakeContext.Information($"Restore using dotnet restore: '{RestoreUsingDotNetRestore}'"); - } -} - -//------------------------------------------------------------- - -public class SolutionContext : BuildContextBase -{ - public SolutionContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string Name { get; set; } - public string AssemblyInfoFileName { get; set; } - public string FileName { get; set; } - public string Directory - { - get - { - var directory = System.IO.Directory.GetParent(FileName).FullName; - var separator = System.IO.Path.DirectorySeparatorChar.ToString(); - - if (!directory.EndsWith(separator)) - { - directory += separator; - } - - return directory; - } - } - - public bool BuildSolution { get; set; } - public string PublishType { get; set; } - public string ConfigurationName { get; set; } - - protected override void ValidateContext() - { - if (string.IsNullOrWhiteSpace(Name)) - { - throw new Exception($"SolutionName must be defined"); - } - } - - protected override void LogStateInfoForContext() - { - - } -} - -//------------------------------------------------------------- - -public class SourceLinkContext : BuildContextBase -{ - public SourceLinkContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public bool IsDisabled { get; set; } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - - } -} - -//------------------------------------------------------------- - -public class CodeSignContext : BuildContextBase -{ - public CodeSignContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string WildCard { get; set; } - public string CertificateSubjectName { get; set; } - public string TimeStampUri { get; set; } - public string HashAlgorithm { get; set; } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - if (string.IsNullOrWhiteSpace(CertificateSubjectName)) - { - CakeContext.Information($"Code signing is not configured"); - return; - } - - CakeContext.Information($"Code signing subject name: '{CertificateSubjectName}'"); - CakeContext.Information($"Code signing timestamp uri: '{TimeStampUri}'"); - CakeContext.Information($"Code signing hash algorithm: '{HashAlgorithm}'"); - } -} - -//------------------------------------------------------------- - -public class RepositoryContext : BuildContextBase -{ - public RepositoryContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string Url { get; set; } - public string BranchName { get; set; } - public string CommitId { get; set; } - public string Username { get; set; } - public string Password { get; set; } - - protected override void ValidateContext() - { - if (string.IsNullOrWhiteSpace(Url)) - { - throw new Exception($"RepositoryUrl must be defined"); - } - } - - protected override void LogStateInfoForContext() - { - - } -} - -//------------------------------------------------------------- - -public class SonarQubeContext : BuildContextBase -{ - public SonarQubeContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public bool IsDisabled { get; set; } - public bool SupportBranches { get; set; } - public string Url { get; set; } - public string Organization { get; set; } - public string Username { get; set; } - public string Password { get; set; } - public string Project { get; set; } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - - } -} - -//------------------------------------------------------------- - -private GeneralContext InitializeGeneralContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new GeneralContext(parentBuildContext) - { - Target = buildContext.BuildServer.GetVariable("Target", "Default", showValue: true), - }; - - data.Version = new VersionContext(data) - { - ClearCache = buildContext.BuildServer.GetVariableAsBool("GitVersion_ClearCache", false, showValue: true), - MajorMinorPatch = buildContext.BuildServer.GetVariable("GitVersion_MajorMinorPatch", "unknown", showValue: true), - FullSemVer = buildContext.BuildServer.GetVariable("GitVersion_FullSemVer", "unknown", showValue: true), - NuGet = buildContext.BuildServer.GetVariable("GitVersion_NuGetVersion", "unknown", showValue: true), - CommitsSinceVersionSource = buildContext.BuildServer.GetVariable("GitVersion_CommitsSinceVersionSource", "unknown", showValue: true) - }; - - data.Copyright = new CopyrightContext(data) - { - Company = buildContext.BuildServer.GetVariable("Company", showValue: true), - StartYear = buildContext.BuildServer.GetVariable("StartYear", showValue: true) - }; - - data.NuGet = new NuGetContext(data) - { - PackageSources = buildContext.BuildServer.GetVariable("NuGetPackageSources", showValue: true), - Executable = "./tools/nuget.exe", - LocalPackagesDirectory = "c:\\source\\_packages", - RestoreUsingNuGet = buildContext.BuildServer.GetVariableAsBool("NuGet_RestoreUsingNuGet", false, showValue: true), - RestoreUsingDotNetRestore = buildContext.BuildServer.GetVariableAsBool("NuGet_RestoreUsingDotNetRestore", true, showValue: true), - NoDependencies = buildContext.BuildServer.GetVariableAsBool("NuGet_NoDependencies", true, showValue: true) - }; - - var solutionName = buildContext.BuildServer.GetVariable("SolutionName", showValue: true); - - data.Solution = new SolutionContext(data) - { - Name = solutionName, - AssemblyInfoFileName = "./src/SolutionAssemblyInfo.cs", - FileName = string.Format("./src/{0}", string.Format("{0}.sln", solutionName)), - PublishType = buildContext.BuildServer.GetVariable("PublishType", "Unknown", showValue: true), - ConfigurationName = buildContext.BuildServer.GetVariable("ConfigurationName", "Release", showValue: true), - BuildSolution = buildContext.BuildServer.GetVariableAsBool("BuildSolution", false, showValue: true) - }; - - data.IsCiBuild = buildContext.BuildServer.GetVariableAsBool("IsCiBuild", false, showValue: true); - data.IsAlphaBuild = buildContext.BuildServer.GetVariableAsBool("IsAlphaBuild", false, showValue: true); - data.IsBetaBuild = buildContext.BuildServer.GetVariableAsBool("IsBetaBuild", false, showValue: true); - data.IsOfficialBuild = buildContext.BuildServer.GetVariableAsBool("IsOfficialBuild", false, showValue: true); - data.IsLocalBuild = data.Target.ToLower().Contains("local"); - data.MaximizePerformance = buildContext.BuildServer.GetVariableAsBool("MaximizePerformance", true, showValue: true); - data.UseVisualStudioPrerelease = buildContext.BuildServer.GetVariableAsBool("UseVisualStudioPrerelease", false, showValue: true); - data.VerifyDependencies = !buildContext.BuildServer.GetVariableAsBool("DependencyCheckDisabled", false, showValue: true); - data.SkipComponentsThatAreNotDeployable = buildContext.BuildServer.GetVariableAsBool("SkipComponentsThatAreNotDeployable", true, showValue: true); - - data.EnableMsBuildBinaryLog = buildContext.BuildServer.GetVariableAsBool("EnableMsBuildBinaryLog", true, showValue: true); - data.EnableMsBuildFileLog = buildContext.BuildServer.GetVariableAsBool("EnableMsBuildFileLog", true, showValue: true); - data.EnableMsBuildXmlLog = buildContext.BuildServer.GetVariableAsBool("EnableMsBuildXmlLog", true, showValue: true); - - // If local, we want full pdb, so do a debug instead - if (data.IsLocalBuild) - { - parentBuildContext.CakeContext.Warning("Enforcing configuration 'Debug' because this is seems to be a local build, do not publish this package!"); - data.Solution.ConfigurationName = "Debug"; - } - - // Important: do *after* initializing the configuration name - data.RootDirectory = System.IO.Path.GetFullPath("."); - data.OutputRootDirectory = System.IO.Path.GetFullPath(buildContext.BuildServer.GetVariable("OutputRootDirectory", string.Format("./output/{0}", data.Solution.ConfigurationName), showValue: true)); - - data.SourceLink = new SourceLinkContext(data) - { - IsDisabled = buildContext.BuildServer.GetVariableAsBool("SourceLinkDisabled", false, showValue: true) - }; - - data.CodeSign = new CodeSignContext(data) - { - WildCard = buildContext.BuildServer.GetVariable("CodeSignWildcard", showValue: true), - CertificateSubjectName = buildContext.BuildServer.GetVariable("CodeSignCertificateSubjectName", showValue: true), - TimeStampUri = buildContext.BuildServer.GetVariable("CodeSignTimeStampUri", "http://timestamp.digicert.com", showValue: true), - HashAlgorithm = buildContext.BuildServer.GetVariable("CodeSignHashAlgorithm", "SHA256", showValue: true) - }; - - data.Repository = new RepositoryContext(data) - { - Url = buildContext.BuildServer.GetVariable("RepositoryUrl", showValue: true), - BranchName = buildContext.BuildServer.GetVariable("RepositoryBranchName", showValue: true), - CommitId = buildContext.BuildServer.GetVariable("RepositoryCommitId", showValue: true), - Username = buildContext.BuildServer.GetVariable("RepositoryUsername", showValue: false), - Password = buildContext.BuildServer.GetVariable("RepositoryPassword", showValue: false) - }; - - data.SonarQube = new SonarQubeContext(data) - { - IsDisabled = buildContext.BuildServer.GetVariableAsBool("SonarDisabled", false, showValue: true), - SupportBranches = buildContext.BuildServer.GetVariableAsBool("SonarSupportBranches", true, showValue: true), - Url = buildContext.BuildServer.GetVariable("SonarUrl", showValue: true), - Organization = buildContext.BuildServer.GetVariable("SonarOrganization", showValue: true), - Username = buildContext.BuildServer.GetVariable("SonarUsername", showValue: false), - Password = buildContext.BuildServer.GetVariable("SonarPassword", showValue: false), - Project = buildContext.BuildServer.GetVariable("SonarProject", data.Solution.Name, showValue: true) - }; - - data.Includes = SplitCommaSeparatedList(buildContext.BuildServer.GetVariable("Include", string.Empty, showValue: true)); - data.Excludes = SplitCommaSeparatedList(buildContext.BuildServer.GetVariable("Exclude", string.Empty, showValue: true)); - - // Specific overrides, done when we have *all* info - parentBuildContext.CakeContext.Information("Ensuring correct runtime data based on version"); - - var versionContext = data.Version; - if (string.IsNullOrWhiteSpace(versionContext.NuGet) || versionContext.NuGet == "unknown") - { - parentBuildContext.CakeContext.Information("No version info specified, falling back to GitVersion"); - - var gitVersion = versionContext.GetGitVersionContext(data); - - versionContext.MajorMinorPatch = gitVersion.MajorMinorPatch; - versionContext.FullSemVer = gitVersion.FullSemVer; - versionContext.NuGet = gitVersion.NuGetVersionV2; - versionContext.CommitsSinceVersionSource = (gitVersion.CommitsSinceVersionSource ?? 0).ToString(); - } - - parentBuildContext.CakeContext.Information("Defined version: '{0}', commits since version source: '{1}'", versionContext.FullSemVer, versionContext.CommitsSinceVersionSource); - - if (string.IsNullOrWhiteSpace(data.Repository.CommitId)) - { - parentBuildContext.CakeContext.Information("No commit id specified, falling back to GitVersion"); - - var gitVersion = versionContext.GetGitVersionContext(data); - - data.Repository.BranchName = gitVersion.BranchName; - data.Repository.CommitId = gitVersion.Sha; - } - - if (string.IsNullOrWhiteSpace(data.Repository.BranchName)) - { - parentBuildContext.CakeContext.Information("No branch name specified, falling back to GitVersion"); - - var gitVersion = versionContext.GetGitVersionContext(data); - - data.Repository.BranchName = gitVersion.BranchName; - } - - var versionToCheck = versionContext.FullSemVer; - if (versionToCheck.Contains("alpha")) - { - data.IsAlphaBuild = true; - } - else if (versionToCheck.Contains("beta")) - { - data.IsBetaBuild = true; - } - else - { - data.IsOfficialBuild = true; - } - - return data; -} - -//------------------------------------------------------------- - -private static string DetermineChannel(GeneralContext context) -{ - var version = context.Version.FullSemVer; - - var channel = "stable"; - - if (context.IsAlphaBuild) - { - channel = "alpha"; - } - else if (context.IsBetaBuild) - { - channel = "beta"; - } - - return channel; -} - -//------------------------------------------------------------- - -private static string DeterminePublishType(GeneralContext context) -{ - var publishType = "Unknown"; - - if (context.IsOfficialBuild) - { - publishType = "Official"; - } - else if (context.IsBetaBuild) - { - publishType = "Beta"; - } - else if (context.IsAlphaBuild) - { - publishType = "Alpha"; - } - - return publishType; -} +#l "buildserver.cake" + +#tool "nuget:?package=GitVersion.CommandLine&version=5.12.0" + +//------------------------------------------------------------- + +public class GeneralContext : BuildContextWithItemsBase +{ + public GeneralContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + SkipComponentsThatAreNotDeployable = true; + EnableMsBuildBinaryLog = true; + EnableMsBuildFileLog = true; + EnableMsBuildXmlLog = true; + } + + public string Target { get; set; } + public string RootDirectory { get; set; } + public string OutputRootDirectory { get; set; } + + public bool IsCiBuild { get; set; } + public bool IsAlphaBuild { get; set; } + public bool IsBetaBuild { get; set; } + public bool IsOfficialBuild { get; set; } + public bool IsLocalBuild { get; set; } + public bool MaximizePerformance { get; set; } + public bool UseVisualStudioPrerelease { get; set; } + public bool VerifyDependencies { get; set; } + public bool SkipComponentsThatAreNotDeployable { get; set; } + + public bool EnableMsBuildBinaryLog { get; set; } + public bool EnableMsBuildFileLog { get; set; } + public bool EnableMsBuildXmlLog { get; set; } + + public VersionContext Version { get; set; } + public CopyrightContext Copyright { get; set; } + public NuGetContext NuGet { get; set; } + public SolutionContext Solution { get; set; } + public SourceLinkContext SourceLink { get; set; } + public CodeSignContext CodeSign { get; set; } + public RepositoryContext Repository { get; set; } + public SonarQubeContext SonarQube { get; set; } + + public List Includes { get; set; } + public List Excludes { get; set; } + + protected override void ValidateContext() + { + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Running target '{Target}'"); + CakeContext.Information($"Using output directory '{OutputRootDirectory}'"); + } +} + +//------------------------------------------------------------- + +public class VersionContext : BuildContextBase +{ + private GitVersion _gitVersionContext; + + public VersionContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public GitVersion GetGitVersionContext(GeneralContext generalContext) + { + if (_gitVersionContext is null) + { + var gitVersionSettings = new GitVersionSettings + { + UpdateAssemblyInfo = false, + Verbosity = GitVersionVerbosity.Verbose + }; + + var gitDirectory = ".git"; + if (!CakeContext.DirectoryExists(gitDirectory)) + { + CakeContext.Information("No local .git directory found, treating as dynamic repository"); + + // Make a *BIG* assumption that the solution name == repository name + var repositoryName = generalContext.Solution.Name; + var dynamicRepositoryPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), repositoryName); + + if (ClearCache) + { + CakeContext.Warning("Cleaning the cloned temp directory, disable by setting 'GitVersion_ClearCache' to 'false'"); + + if (CakeContext.DirectoryExists(dynamicRepositoryPath)) + { + CakeContext.DeleteDirectory(dynamicRepositoryPath, new DeleteDirectorySettings + { + Force = true, + Recursive = true + }); + } + } + + // Validate first + if (string.IsNullOrWhiteSpace(generalContext.Repository.BranchName)) + { + throw new Exception("No local .git directory was found, but repository branch was not specified either. Make sure to specify the branch"); + } + + if (string.IsNullOrWhiteSpace(generalContext.Repository.Url)) + { + throw new Exception("No local .git directory was found, but repository url was not specified either. Make sure to specify the branch"); + } + + CakeContext.Information($"Fetching dynamic repository from url '{generalContext.Repository.Url}' => '{dynamicRepositoryPath}'"); + + // Dynamic repository + gitVersionSettings.UserName = generalContext.Repository.Username; + gitVersionSettings.Password = generalContext.Repository.Password; + gitVersionSettings.Url = generalContext.Repository.Url; + gitVersionSettings.Branch = generalContext.Repository.BranchName; + gitVersionSettings.Commit = generalContext.Repository.CommitId; + gitVersionSettings.NoFetch = false; + gitVersionSettings.WorkingDirectory = generalContext.RootDirectory; + gitVersionSettings.DynamicRepositoryPath = dynamicRepositoryPath; + gitVersionSettings.Verbosity = GitVersionVerbosity.Verbose; + } + + _gitVersionContext = CakeContext.GitVersion(gitVersionSettings); + } + + return _gitVersionContext; + } + + public bool ClearCache { get; set; } + + private string _major; + + public string Major + { + get + { + if (string.IsNullOrWhiteSpace(_major)) + { + _major = GetVersion(MajorMinorPatch, 1); + } + + return _major; + } + } + + private string _majorMinor; + + public string MajorMinor + { + get + { + if (string.IsNullOrWhiteSpace(_majorMinor)) + { + _majorMinor = GetVersion(MajorMinorPatch, 2); + } + + return _majorMinor; + } + } + + public string MajorMinorPatch { get; set; } + public string FullSemVer { get; set; } + public string NuGet { get; set; } + public string CommitsSinceVersionSource { get; set; } + + private string GetVersion(string version, int breakCount) + { + var finalVersion = string.Empty; + + for (int i = 0; i < version.Length; i++) + { + var character = version[i]; + if (!char.IsDigit(character)) + { + breakCount--; + if (breakCount <= 0) + { + break; + } + } + + finalVersion += character.ToString(); + } + + return finalVersion; + } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + + } +} + +//------------------------------------------------------------- + +public class CopyrightContext : BuildContextBase +{ + public CopyrightContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string Company { get; set; } + public string StartYear { get; set; } + + protected override void ValidateContext() + { + if (string.IsNullOrWhiteSpace(Company)) + { + throw new Exception($"Company must be defined"); + } + } + + protected override void LogStateInfoForContext() + { + + } +} + +//------------------------------------------------------------- + +public class NuGetContext : BuildContextBase +{ + public NuGetContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string PackageSources { get; set; } + public string Executable { get; set; } + public string LocalPackagesDirectory { get; set; } + + public bool RestoreUsingNuGet { get; set; } + public bool RestoreUsingDotNetRestore { get; set; } + public bool NoDependencies { get; set; } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Restore using NuGet: '{RestoreUsingNuGet}'"); + CakeContext.Information($"Restore using dotnet restore: '{RestoreUsingDotNetRestore}'"); + } +} + +//------------------------------------------------------------- + +public class SolutionContext : BuildContextBase +{ + public SolutionContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string Name { get; set; } + public string AssemblyInfoFileName { get; set; } + public string FileName { get; set; } + public string Directory + { + get + { + var directory = System.IO.Directory.GetParent(FileName).FullName; + var separator = System.IO.Path.DirectorySeparatorChar.ToString(); + + if (!directory.EndsWith(separator)) + { + directory += separator; + } + + return directory; + } + } + + public bool BuildSolution { get; set; } + public string PublishType { get; set; } + public string ConfigurationName { get; set; } + + protected override void ValidateContext() + { + if (string.IsNullOrWhiteSpace(Name)) + { + throw new Exception($"SolutionName must be defined"); + } + } + + protected override void LogStateInfoForContext() + { + + } +} + +//------------------------------------------------------------- + +public class SourceLinkContext : BuildContextBase +{ + public SourceLinkContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public bool IsDisabled { get; set; } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + + } +} + +//------------------------------------------------------------- + +public class CodeSignContext : BuildContextBase +{ + public CodeSignContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string WildCard { get; set; } + public string CertificateSubjectName { get; set; } + public string TimeStampUri { get; set; } + public string HashAlgorithm { get; set; } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + if (string.IsNullOrWhiteSpace(CertificateSubjectName)) + { + CakeContext.Information($"Code signing is not configured"); + return; + } + + CakeContext.Information($"Code signing subject name: '{CertificateSubjectName}'"); + CakeContext.Information($"Code signing timestamp uri: '{TimeStampUri}'"); + CakeContext.Information($"Code signing hash algorithm: '{HashAlgorithm}'"); + } +} + +//------------------------------------------------------------- + +public class RepositoryContext : BuildContextBase +{ + public RepositoryContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string Url { get; set; } + public string BranchName { get; set; } + public string CommitId { get; set; } + public string Username { get; set; } + public string Password { get; set; } + + protected override void ValidateContext() + { + if (string.IsNullOrWhiteSpace(Url)) + { + throw new Exception($"RepositoryUrl must be defined"); + } + } + + protected override void LogStateInfoForContext() + { + + } +} + +//------------------------------------------------------------- + +public class SonarQubeContext : BuildContextBase +{ + public SonarQubeContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public bool IsDisabled { get; set; } + public bool SupportBranches { get; set; } + public string Url { get; set; } + public string Organization { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string Project { get; set; } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + + } +} + +//------------------------------------------------------------- + +private GeneralContext InitializeGeneralContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new GeneralContext(parentBuildContext) + { + Target = buildContext.BuildServer.GetVariable("Target", "Default", showValue: true), + }; + + data.Version = new VersionContext(data) + { + ClearCache = buildContext.BuildServer.GetVariableAsBool("GitVersion_ClearCache", false, showValue: true), + MajorMinorPatch = buildContext.BuildServer.GetVariable("GitVersion_MajorMinorPatch", "unknown", showValue: true), + FullSemVer = buildContext.BuildServer.GetVariable("GitVersion_FullSemVer", "unknown", showValue: true), + NuGet = buildContext.BuildServer.GetVariable("GitVersion_NuGetVersion", "unknown", showValue: true), + CommitsSinceVersionSource = buildContext.BuildServer.GetVariable("GitVersion_CommitsSinceVersionSource", "unknown", showValue: true) + }; + + data.Copyright = new CopyrightContext(data) + { + Company = buildContext.BuildServer.GetVariable("Company", showValue: true), + StartYear = buildContext.BuildServer.GetVariable("StartYear", showValue: true) + }; + + data.NuGet = new NuGetContext(data) + { + PackageSources = buildContext.BuildServer.GetVariable("NuGetPackageSources", showValue: true), + Executable = "./tools/nuget.exe", + LocalPackagesDirectory = "c:\\source\\_packages", + RestoreUsingNuGet = buildContext.BuildServer.GetVariableAsBool("NuGet_RestoreUsingNuGet", false, showValue: true), + RestoreUsingDotNetRestore = buildContext.BuildServer.GetVariableAsBool("NuGet_RestoreUsingDotNetRestore", true, showValue: true), + NoDependencies = buildContext.BuildServer.GetVariableAsBool("NuGet_NoDependencies", true, showValue: true) + }; + + var solutionName = buildContext.BuildServer.GetVariable("SolutionName", showValue: true); + + data.Solution = new SolutionContext(data) + { + Name = solutionName, + AssemblyInfoFileName = "./src/SolutionAssemblyInfo.cs", + FileName = string.Format("./src/{0}", string.Format("{0}.sln", solutionName)), + PublishType = buildContext.BuildServer.GetVariable("PublishType", "Unknown", showValue: true), + ConfigurationName = buildContext.BuildServer.GetVariable("ConfigurationName", "Release", showValue: true), + BuildSolution = buildContext.BuildServer.GetVariableAsBool("BuildSolution", false, showValue: true) + }; + + data.IsCiBuild = buildContext.BuildServer.GetVariableAsBool("IsCiBuild", false, showValue: true); + data.IsAlphaBuild = buildContext.BuildServer.GetVariableAsBool("IsAlphaBuild", false, showValue: true); + data.IsBetaBuild = buildContext.BuildServer.GetVariableAsBool("IsBetaBuild", false, showValue: true); + data.IsOfficialBuild = buildContext.BuildServer.GetVariableAsBool("IsOfficialBuild", false, showValue: true); + data.IsLocalBuild = data.Target.ToLower().Contains("local"); + data.MaximizePerformance = buildContext.BuildServer.GetVariableAsBool("MaximizePerformance", true, showValue: true); + data.UseVisualStudioPrerelease = buildContext.BuildServer.GetVariableAsBool("UseVisualStudioPrerelease", false, showValue: true); + data.VerifyDependencies = !buildContext.BuildServer.GetVariableAsBool("DependencyCheckDisabled", false, showValue: true); + data.SkipComponentsThatAreNotDeployable = buildContext.BuildServer.GetVariableAsBool("SkipComponentsThatAreNotDeployable", true, showValue: true); + + data.EnableMsBuildBinaryLog = buildContext.BuildServer.GetVariableAsBool("EnableMsBuildBinaryLog", true, showValue: true); + data.EnableMsBuildFileLog = buildContext.BuildServer.GetVariableAsBool("EnableMsBuildFileLog", true, showValue: true); + data.EnableMsBuildXmlLog = buildContext.BuildServer.GetVariableAsBool("EnableMsBuildXmlLog", true, showValue: true); + + // If local, we want full pdb, so do a debug instead + if (data.IsLocalBuild) + { + parentBuildContext.CakeContext.Warning("Enforcing configuration 'Debug' because this is seems to be a local build, do not publish this package!"); + data.Solution.ConfigurationName = "Debug"; + } + + // Important: do *after* initializing the configuration name + data.RootDirectory = System.IO.Path.GetFullPath("."); + data.OutputRootDirectory = System.IO.Path.GetFullPath(buildContext.BuildServer.GetVariable("OutputRootDirectory", string.Format("./output/{0}", data.Solution.ConfigurationName), showValue: true)); + + data.SourceLink = new SourceLinkContext(data) + { + IsDisabled = buildContext.BuildServer.GetVariableAsBool("SourceLinkDisabled", false, showValue: true) + }; + + data.CodeSign = new CodeSignContext(data) + { + WildCard = buildContext.BuildServer.GetVariable("CodeSignWildcard", showValue: true), + CertificateSubjectName = buildContext.BuildServer.GetVariable("CodeSignCertificateSubjectName", showValue: true), + TimeStampUri = buildContext.BuildServer.GetVariable("CodeSignTimeStampUri", "http://timestamp.digicert.com", showValue: true), + HashAlgorithm = buildContext.BuildServer.GetVariable("CodeSignHashAlgorithm", "SHA256", showValue: true) + }; + + data.Repository = new RepositoryContext(data) + { + Url = buildContext.BuildServer.GetVariable("RepositoryUrl", showValue: true), + BranchName = buildContext.BuildServer.GetVariable("RepositoryBranchName", showValue: true), + CommitId = buildContext.BuildServer.GetVariable("RepositoryCommitId", showValue: true), + Username = buildContext.BuildServer.GetVariable("RepositoryUsername", showValue: false), + Password = buildContext.BuildServer.GetVariable("RepositoryPassword", showValue: false) + }; + + data.SonarQube = new SonarQubeContext(data) + { + IsDisabled = buildContext.BuildServer.GetVariableAsBool("SonarDisabled", false, showValue: true), + SupportBranches = buildContext.BuildServer.GetVariableAsBool("SonarSupportBranches", true, showValue: true), + Url = buildContext.BuildServer.GetVariable("SonarUrl", showValue: true), + Organization = buildContext.BuildServer.GetVariable("SonarOrganization", showValue: true), + Username = buildContext.BuildServer.GetVariable("SonarUsername", showValue: false), + Password = buildContext.BuildServer.GetVariable("SonarPassword", showValue: false), + Project = buildContext.BuildServer.GetVariable("SonarProject", data.Solution.Name, showValue: true) + }; + + data.Includes = SplitCommaSeparatedList(buildContext.BuildServer.GetVariable("Include", string.Empty, showValue: true)); + data.Excludes = SplitCommaSeparatedList(buildContext.BuildServer.GetVariable("Exclude", string.Empty, showValue: true)); + + // Specific overrides, done when we have *all* info + parentBuildContext.CakeContext.Information("Ensuring correct runtime data based on version"); + + var versionContext = data.Version; + if (string.IsNullOrWhiteSpace(versionContext.NuGet) || versionContext.NuGet == "unknown") + { + parentBuildContext.CakeContext.Information("No version info specified, falling back to GitVersion"); + + var gitVersion = versionContext.GetGitVersionContext(data); + + versionContext.MajorMinorPatch = gitVersion.MajorMinorPatch; + versionContext.FullSemVer = gitVersion.FullSemVer; + versionContext.NuGet = gitVersion.NuGetVersionV2; + versionContext.CommitsSinceVersionSource = (gitVersion.CommitsSinceVersionSource ?? 0).ToString(); + } + + parentBuildContext.CakeContext.Information("Defined version: '{0}', commits since version source: '{1}'", versionContext.FullSemVer, versionContext.CommitsSinceVersionSource); + + if (string.IsNullOrWhiteSpace(data.Repository.CommitId)) + { + parentBuildContext.CakeContext.Information("No commit id specified, falling back to GitVersion"); + + var gitVersion = versionContext.GetGitVersionContext(data); + + data.Repository.BranchName = gitVersion.BranchName; + data.Repository.CommitId = gitVersion.Sha; + } + + if (string.IsNullOrWhiteSpace(data.Repository.BranchName)) + { + parentBuildContext.CakeContext.Information("No branch name specified, falling back to GitVersion"); + + var gitVersion = versionContext.GetGitVersionContext(data); + + data.Repository.BranchName = gitVersion.BranchName; + } + + var versionToCheck = versionContext.FullSemVer; + if (versionToCheck.Contains("alpha")) + { + data.IsAlphaBuild = true; + } + else if (versionToCheck.Contains("beta")) + { + data.IsBetaBuild = true; + } + else + { + data.IsOfficialBuild = true; + } + + return data; +} + +//------------------------------------------------------------- + +private static string DetermineChannel(GeneralContext context) +{ + var version = context.Version.FullSemVer; + + var channel = "stable"; + + if (context.IsAlphaBuild) + { + channel = "alpha"; + } + else if (context.IsBetaBuild) + { + channel = "beta"; + } + + return channel; +} + +//------------------------------------------------------------- + +private static string DeterminePublishType(GeneralContext context) +{ + var publishType = "Unknown"; + + if (context.IsOfficialBuild) + { + publishType = "Official"; + } + else if (context.IsBetaBuild) + { + publishType = "Beta"; + } + else if (context.IsAlphaBuild) + { + publishType = "Alpha"; + } + + return publishType; +} diff --git a/deployment/cake/github-pages-tasks.cake b/deployment/cake/github-pages-tasks.cake index 985ba98d..9e008273 100644 --- a/deployment/cake/github-pages-tasks.cake +++ b/deployment/cake/github-pages-tasks.cake @@ -1,223 +1,223 @@ -#l "github-pages-variables.cake" - -#addin "nuget:?package=Cake.Git&version=3.0.0" - -//------------------------------------------------------------- - -public class GitHubPagesProcessor : ProcessorBase -{ - public GitHubPagesProcessor(BuildContext buildContext) - : base(buildContext) - { - - } - - public override bool HasItems() - { - return BuildContext.GitHubPages.Items.Count > 0; - } - - private string GetGitHubPagesRepositoryUrl(string projectName) - { - // Allow per project overrides via "GitHubPagesRepositoryUrlFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "GitHubPagesRepositoryUrlFor", BuildContext.GitHubPages.RepositoryUrl); - } - - private string GetGitHubPagesBranchName(string projectName) - { - // Allow per project overrides via "GitHubPagesBranchNameFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "GitHubPagesBranchNameFor", BuildContext.GitHubPages.BranchName); - } - - private string GetGitHubPagesEmail(string projectName) - { - // Allow per project overrides via "GitHubPagesEmailFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "GitHubPagesEmailFor", BuildContext.GitHubPages.Email); - } - - private string GetGitHubPagesUserName(string projectName) - { - // Allow per project overrides via "GitHubPagesUserNameFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "GitHubPagesUserNameFor", BuildContext.GitHubPages.UserName); - } - - private string GetGitHubPagesApiToken(string projectName) - { - // Allow per project overrides via "GitHubPagesApiTokenFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "GitHubPagesApiTokenFor", BuildContext.GitHubPages.ApiToken); - } - - public override async Task PrepareAsync() - { - if (!HasItems()) - { - return; - } - - // Check whether projects should be processed, `.ToList()` - // is required to prevent issues with foreach - foreach (var gitHubPage in BuildContext.GitHubPages.Items.ToList()) - { - if (!ShouldProcessProject(BuildContext, gitHubPage)) - { - BuildContext.GitHubPages.Items.Remove(gitHubPage); - } - } - } - - public override async Task UpdateInfoAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var gitHubPage in BuildContext.GitHubPages.Items) - { - CakeContext.Information("Updating version for GitHub page '{0}'", gitHubPage); - - var projectFileName = GetProjectFileName(BuildContext, gitHubPage); - - CakeContext.TransformConfig(projectFileName, new TransformationCollection - { - { "Project/PropertyGroup/PackageVersion", BuildContext.General.Version.NuGet } - }); - } - } - - public override async Task BuildAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var gitHubPage in BuildContext.GitHubPages.Items) - { - BuildContext.CakeContext.LogSeparator("Building GitHub page '{0}'", gitHubPage); - - var projectFileName = GetProjectFileName(BuildContext, gitHubPage); - - var msBuildSettings = new MSBuildSettings { - Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic - ToolVersion = MSBuildToolVersion.Default, - Configuration = BuildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, gitHubPage, "build"); - - // Always disable SourceLink - msBuildSettings.WithProperty("EnableSourceLink", "false"); - - RunMsBuild(BuildContext, gitHubPage, projectFileName, msBuildSettings, "build"); - } - } - - public override async Task PackageAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var gitHubPage in BuildContext.GitHubPages.Items) - { - if (!ShouldPackageProject(BuildContext, gitHubPage)) - { - CakeContext.Information("GitHub page '{0}' should not be packaged", gitHubPage); - continue; - } - - BuildContext.CakeContext.LogSeparator("Packaging GitHub pages '{0}'", gitHubPage); - - var projectFileName = GetProjectFileName(BuildContext, gitHubPage); - var outputDirectory = GetProjectOutputDirectory(BuildContext, gitHubPage); - - CakeContext.Information("Output directory: '{0}'", outputDirectory); - - CakeContext.Information("1) Using 'dotnet publish' to package '{0}'", gitHubPage); - - var msBuildSettings = new DotNetMSBuildSettings(); - - ConfigureMsBuildForDotNet(BuildContext, msBuildSettings, gitHubPage, "pack"); - - msBuildSettings.WithProperty("ConfigurationName", BuildContext.General.Solution.ConfigurationName); - msBuildSettings.WithProperty("PackageVersion", BuildContext.General.Version.NuGet); - - var publishSettings = new DotNetPublishSettings - { - MSBuildSettings = msBuildSettings, - OutputDirectory = outputDirectory, - Configuration = BuildContext.General.Solution.ConfigurationName - }; - - CakeContext.DotNetPublish(projectFileName, publishSettings); - } - } - - public override async Task DeployAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var gitHubPage in BuildContext.GitHubPages.Items) - { - if (!ShouldDeployProject(BuildContext, gitHubPage)) - { - CakeContext.Information("GitHub page '{0}' should not be deployed", gitHubPage); - continue; - } - - BuildContext.CakeContext.LogSeparator("Deploying GitHub page '{0}'", gitHubPage); - - CakeContext.Warning("Only Blazor apps are supported as GitHub pages"); - - var temporaryDirectory = GetTempDirectory(BuildContext, "gh-pages", gitHubPage); - - CakeContext.CleanDirectory(temporaryDirectory); - - var repositoryUrl = GetGitHubPagesRepositoryUrl(gitHubPage); - var branchName = GetGitHubPagesBranchName(gitHubPage); - var email = GetGitHubPagesEmail(gitHubPage); - var userName = GetGitHubPagesUserName(gitHubPage); - var apiToken = GetGitHubPagesApiToken(gitHubPage); - - CakeContext.Information("1) Cloning repository '{0}' using branch name '{1}'", repositoryUrl, branchName); - - CakeContext.GitClone(repositoryUrl, temporaryDirectory, userName, apiToken, new GitCloneSettings - { - BranchName = branchName - }); - - CakeContext.Information("2) Updating the GitHub pages branch with latest source"); - - // Special directory we need to distribute (e.g. output\Release\Blazorc.PatternFly.Example\Blazorc.PatternFly.Example\dist) - var sourceDirectory = string.Format("{0}/{1}/wwwroot", BuildContext.General.OutputRootDirectory, gitHubPage); - var sourcePattern = string.Format("{0}/**/*", sourceDirectory); - - CakeContext.Debug("Copying all files from '{0}' => '{1}'", sourcePattern, temporaryDirectory); - - CakeContext.CopyFiles(sourcePattern, temporaryDirectory, true); - - CakeContext.Information("3) Committing latest GitHub pages"); - - CakeContext.GitAddAll(temporaryDirectory); - CakeContext.GitCommit(temporaryDirectory, "Build server", email, string.Format("Auto-update GitHub pages: '{0}'", BuildContext.General.Version.NuGet)); - - CakeContext.Information("4) Pushing code back to repository '{0}'", repositoryUrl); - - CakeContext.GitPush(temporaryDirectory, userName, apiToken); - - await BuildContext.Notifications.NotifyAsync(gitHubPage, string.Format("Deployed to GitHub pages"), TargetType.GitHubPages); - } - } - - public override async Task FinalizeAsync() - { - - } -} +#l "github-pages-variables.cake" + +#addin "nuget:?package=Cake.Git&version=3.0.0" + +//------------------------------------------------------------- + +public class GitHubPagesProcessor : ProcessorBase +{ + public GitHubPagesProcessor(BuildContext buildContext) + : base(buildContext) + { + + } + + public override bool HasItems() + { + return BuildContext.GitHubPages.Items.Count > 0; + } + + private string GetGitHubPagesRepositoryUrl(string projectName) + { + // Allow per project overrides via "GitHubPagesRepositoryUrlFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "GitHubPagesRepositoryUrlFor", BuildContext.GitHubPages.RepositoryUrl); + } + + private string GetGitHubPagesBranchName(string projectName) + { + // Allow per project overrides via "GitHubPagesBranchNameFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "GitHubPagesBranchNameFor", BuildContext.GitHubPages.BranchName); + } + + private string GetGitHubPagesEmail(string projectName) + { + // Allow per project overrides via "GitHubPagesEmailFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "GitHubPagesEmailFor", BuildContext.GitHubPages.Email); + } + + private string GetGitHubPagesUserName(string projectName) + { + // Allow per project overrides via "GitHubPagesUserNameFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "GitHubPagesUserNameFor", BuildContext.GitHubPages.UserName); + } + + private string GetGitHubPagesApiToken(string projectName) + { + // Allow per project overrides via "GitHubPagesApiTokenFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "GitHubPagesApiTokenFor", BuildContext.GitHubPages.ApiToken); + } + + public override async Task PrepareAsync() + { + if (!HasItems()) + { + return; + } + + // Check whether projects should be processed, `.ToList()` + // is required to prevent issues with foreach + foreach (var gitHubPage in BuildContext.GitHubPages.Items.ToList()) + { + if (!ShouldProcessProject(BuildContext, gitHubPage)) + { + BuildContext.GitHubPages.Items.Remove(gitHubPage); + } + } + } + + public override async Task UpdateInfoAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var gitHubPage in BuildContext.GitHubPages.Items) + { + CakeContext.Information("Updating version for GitHub page '{0}'", gitHubPage); + + var projectFileName = GetProjectFileName(BuildContext, gitHubPage); + + CakeContext.TransformConfig(projectFileName, new TransformationCollection + { + { "Project/PropertyGroup/PackageVersion", BuildContext.General.Version.NuGet } + }); + } + } + + public override async Task BuildAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var gitHubPage in BuildContext.GitHubPages.Items) + { + BuildContext.CakeContext.LogSeparator("Building GitHub page '{0}'", gitHubPage); + + var projectFileName = GetProjectFileName(BuildContext, gitHubPage); + + var msBuildSettings = new MSBuildSettings { + Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic + ToolVersion = MSBuildToolVersion.Default, + Configuration = BuildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, gitHubPage, "build"); + + // Always disable SourceLink + msBuildSettings.WithProperty("EnableSourceLink", "false"); + + RunMsBuild(BuildContext, gitHubPage, projectFileName, msBuildSettings, "build"); + } + } + + public override async Task PackageAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var gitHubPage in BuildContext.GitHubPages.Items) + { + if (!ShouldPackageProject(BuildContext, gitHubPage)) + { + CakeContext.Information("GitHub page '{0}' should not be packaged", gitHubPage); + continue; + } + + BuildContext.CakeContext.LogSeparator("Packaging GitHub pages '{0}'", gitHubPage); + + var projectFileName = GetProjectFileName(BuildContext, gitHubPage); + var outputDirectory = GetProjectOutputDirectory(BuildContext, gitHubPage); + + CakeContext.Information("Output directory: '{0}'", outputDirectory); + + CakeContext.Information("1) Using 'dotnet publish' to package '{0}'", gitHubPage); + + var msBuildSettings = new DotNetMSBuildSettings(); + + ConfigureMsBuildForDotNet(BuildContext, msBuildSettings, gitHubPage, "pack"); + + msBuildSettings.WithProperty("ConfigurationName", BuildContext.General.Solution.ConfigurationName); + msBuildSettings.WithProperty("PackageVersion", BuildContext.General.Version.NuGet); + + var publishSettings = new DotNetPublishSettings + { + MSBuildSettings = msBuildSettings, + OutputDirectory = outputDirectory, + Configuration = BuildContext.General.Solution.ConfigurationName + }; + + CakeContext.DotNetPublish(projectFileName, publishSettings); + } + } + + public override async Task DeployAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var gitHubPage in BuildContext.GitHubPages.Items) + { + if (!ShouldDeployProject(BuildContext, gitHubPage)) + { + CakeContext.Information("GitHub page '{0}' should not be deployed", gitHubPage); + continue; + } + + BuildContext.CakeContext.LogSeparator("Deploying GitHub page '{0}'", gitHubPage); + + CakeContext.Warning("Only Blazor apps are supported as GitHub pages"); + + var temporaryDirectory = GetTempDirectory(BuildContext, "gh-pages", gitHubPage); + + CakeContext.CleanDirectory(temporaryDirectory); + + var repositoryUrl = GetGitHubPagesRepositoryUrl(gitHubPage); + var branchName = GetGitHubPagesBranchName(gitHubPage); + var email = GetGitHubPagesEmail(gitHubPage); + var userName = GetGitHubPagesUserName(gitHubPage); + var apiToken = GetGitHubPagesApiToken(gitHubPage); + + CakeContext.Information("1) Cloning repository '{0}' using branch name '{1}'", repositoryUrl, branchName); + + CakeContext.GitClone(repositoryUrl, temporaryDirectory, userName, apiToken, new GitCloneSettings + { + BranchName = branchName + }); + + CakeContext.Information("2) Updating the GitHub pages branch with latest source"); + + // Special directory we need to distribute (e.g. output\Release\Blazorc.PatternFly.Example\Blazorc.PatternFly.Example\dist) + var sourceDirectory = string.Format("{0}/{1}/wwwroot", BuildContext.General.OutputRootDirectory, gitHubPage); + var sourcePattern = string.Format("{0}/**/*", sourceDirectory); + + CakeContext.Debug("Copying all files from '{0}' => '{1}'", sourcePattern, temporaryDirectory); + + CakeContext.CopyFiles(sourcePattern, temporaryDirectory, true); + + CakeContext.Information("3) Committing latest GitHub pages"); + + CakeContext.GitAddAll(temporaryDirectory); + CakeContext.GitCommit(temporaryDirectory, "Build server", email, string.Format("Auto-update GitHub pages: '{0}'", BuildContext.General.Version.NuGet)); + + CakeContext.Information("4) Pushing code back to repository '{0}'", repositoryUrl); + + CakeContext.GitPush(temporaryDirectory, userName, apiToken); + + await BuildContext.Notifications.NotifyAsync(gitHubPage, string.Format("Deployed to GitHub pages"), TargetType.GitHubPages); + } + } + + public override async Task FinalizeAsync() + { + + } +} diff --git a/deployment/cake/github-pages-variables.cake b/deployment/cake/github-pages-variables.cake index b080e381..44652aa3 100644 --- a/deployment/cake/github-pages-variables.cake +++ b/deployment/cake/github-pages-variables.cake @@ -1,89 +1,89 @@ -#l "buildserver.cake" - -//------------------------------------------------------------- - -public class GitHubPagesContext : BuildContextWithItemsBase -{ - public GitHubPagesContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string RepositoryUrl { get; set; } - public string BranchName { get; set; } - public string Email { get; set; } - public string UserName { get; set; } - public string ApiToken { get; set; } - - protected override void ValidateContext() - { - if (Items.Count == 0) - { - return; - } - - if (string.IsNullOrWhiteSpace(RepositoryUrl)) - { - throw new Exception("GitHubPagesRepositoryUrl must be defined"); - } - - if (string.IsNullOrWhiteSpace(BranchName)) - { - throw new Exception("GitHubPagesBranchName must be defined"); - } - - if (string.IsNullOrWhiteSpace(Email)) - { - throw new Exception("GitHubPagesEmail must be defined"); - } - - if (string.IsNullOrWhiteSpace(UserName)) - { - throw new Exception("GitHubPagesUserName must be defined"); - } - - if (string.IsNullOrWhiteSpace(ApiToken)) - { - throw new Exception("GitHubPagesApiToken must be defined"); - } - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' GitHub pages projects"); - } -} - -//------------------------------------------------------------- - -private GitHubPagesContext InitializeGitHubPagesContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new GitHubPagesContext(parentBuildContext) - { - Items = GitHubPages ?? new List(), - RepositoryUrl = buildContext.BuildServer.GetVariable("GitHubPagesRepositoryUrl", ((BuildContext)parentBuildContext).General.Repository.Url, showValue: true), - BranchName = buildContext.BuildServer.GetVariable("GitHubPagesRepositoryUrl", "gh-pages", showValue: true), - Email = buildContext.BuildServer.GetVariable("GitHubPagesEmail", showValue: true), - UserName = buildContext.BuildServer.GetVariable("GitHubPagesUserName", showValue: true), - ApiToken = buildContext.BuildServer.GetVariable("GitHubPagesApiToken", showValue: false), - }; - - return data; -} - -//------------------------------------------------------------- - -List _gitHubPages; - -public List GitHubPages -{ - get - { - if (_gitHubPages is null) - { - _gitHubPages = new List(); - } - - return _gitHubPages; - } +#l "buildserver.cake" + +//------------------------------------------------------------- + +public class GitHubPagesContext : BuildContextWithItemsBase +{ + public GitHubPagesContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string RepositoryUrl { get; set; } + public string BranchName { get; set; } + public string Email { get; set; } + public string UserName { get; set; } + public string ApiToken { get; set; } + + protected override void ValidateContext() + { + if (Items.Count == 0) + { + return; + } + + if (string.IsNullOrWhiteSpace(RepositoryUrl)) + { + throw new Exception("GitHubPagesRepositoryUrl must be defined"); + } + + if (string.IsNullOrWhiteSpace(BranchName)) + { + throw new Exception("GitHubPagesBranchName must be defined"); + } + + if (string.IsNullOrWhiteSpace(Email)) + { + throw new Exception("GitHubPagesEmail must be defined"); + } + + if (string.IsNullOrWhiteSpace(UserName)) + { + throw new Exception("GitHubPagesUserName must be defined"); + } + + if (string.IsNullOrWhiteSpace(ApiToken)) + { + throw new Exception("GitHubPagesApiToken must be defined"); + } + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' GitHub pages projects"); + } +} + +//------------------------------------------------------------- + +private GitHubPagesContext InitializeGitHubPagesContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new GitHubPagesContext(parentBuildContext) + { + Items = GitHubPages ?? new List(), + RepositoryUrl = buildContext.BuildServer.GetVariable("GitHubPagesRepositoryUrl", ((BuildContext)parentBuildContext).General.Repository.Url, showValue: true), + BranchName = buildContext.BuildServer.GetVariable("GitHubPagesRepositoryUrl", "gh-pages", showValue: true), + Email = buildContext.BuildServer.GetVariable("GitHubPagesEmail", showValue: true), + UserName = buildContext.BuildServer.GetVariable("GitHubPagesUserName", showValue: true), + ApiToken = buildContext.BuildServer.GetVariable("GitHubPagesApiToken", showValue: false), + }; + + return data; +} + +//------------------------------------------------------------- + +List _gitHubPages; + +public List GitHubPages +{ + get + { + if (_gitHubPages is null) + { + _gitHubPages = new List(); + } + + return _gitHubPages; + } } \ No newline at end of file diff --git a/deployment/cake/installers-innosetup.cake b/deployment/cake/installers-innosetup.cake index 344a2d1f..c3992be4 100644 --- a/deployment/cake/installers-innosetup.cake +++ b/deployment/cake/installers-innosetup.cake @@ -1,225 +1,225 @@ -//------------------------------------------------------------- - -public class InnoSetupInstaller : IInstaller -{ - public InnoSetupInstaller(BuildContext buildContext) - { - BuildContext = buildContext; - - IsEnabled = BuildContext.BuildServer.GetVariableAsBool("InnoSetupEnabled", true, showValue: true); - - if (IsEnabled) - { - // In the future, check if InnoSetup is installed. Log error if not - IsAvailable = IsEnabled; - } - } - - public BuildContext BuildContext { get; private set; } - - public bool IsEnabled { get; private set; } - - public bool IsAvailable { get; private set; } - - //------------------------------------------------------------- - - public async Task PackageAsync(string projectName, string channel) - { - if (!IsAvailable) - { - BuildContext.CakeContext.Information("Inno Setup is not enabled or available, skipping integration"); - return; - } - - var innoSetupTemplateDirectory = System.IO.Path.Combine(".", "deployment", "innosetup", projectName); - if (!BuildContext.CakeContext.DirectoryExists(innoSetupTemplateDirectory)) - { - BuildContext.CakeContext.Information($"Skip packaging of app '{projectName}' using Inno Setup since no Inno Setup template is present"); - return; - } - - BuildContext.CakeContext.LogSeparator($"Packaging app '{projectName}' using Inno Setup"); - - var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); - - var installersOnDeploymentsShare = System.IO.Path.Combine(deploymentShare, "installer"); - BuildContext.CakeContext.CreateDirectory(installersOnDeploymentsShare); - - var setupSuffix = BuildContext.Installer.GetDeploymentChannelSuffix(); - - var innoSetupOutputRoot = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, "innosetup", projectName); - var innoSetupReleasesRoot = System.IO.Path.Combine(innoSetupOutputRoot, "releases"); - var innoSetupOutputIntermediate = System.IO.Path.Combine(innoSetupOutputRoot, "intermediate"); - - BuildContext.CakeContext.CreateDirectory(innoSetupReleasesRoot); - BuildContext.CakeContext.CreateDirectory(innoSetupOutputIntermediate); - - // Copy all files to the intermediate directory so Inno Setup knows what to do - var appSourceDirectory = string.Format("{0}/{1}/**/*", BuildContext.General.OutputRootDirectory, projectName); - var appTargetDirectory = innoSetupOutputIntermediate; - - BuildContext.CakeContext.Information("Copying files from '{0}' => '{1}'", appSourceDirectory, appTargetDirectory); - - BuildContext.CakeContext.CopyFiles(appSourceDirectory, appTargetDirectory, true); - - // Set up InnoSetup template - BuildContext.CakeContext.CopyDirectory(innoSetupTemplateDirectory, innoSetupOutputIntermediate); - - var innoSetupScriptFileName = System.IO.Path.Combine(innoSetupOutputIntermediate, "setup.iss"); - var fileContents = System.IO.File.ReadAllText(innoSetupScriptFileName); - fileContents = fileContents.Replace("[CHANNEL_SUFFIX]", setupSuffix); - fileContents = fileContents.Replace("[CHANNEL]", BuildContext.Installer.GetDeploymentChannelSuffix(" (", ")")); - fileContents = fileContents.Replace("[VERSION]", BuildContext.General.Version.MajorMinorPatch); - fileContents = fileContents.Replace("[VERSION_DISPLAY]", BuildContext.General.Version.FullSemVer); - fileContents = fileContents.Replace("[WIZARDIMAGEFILE]", string.Format("logo_large{0}", setupSuffix)); - - var signTool = string.Empty; - if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) - { - signTool = string.Format("SignTool={0}", BuildContext.General.CodeSign.CertificateSubjectName); - } - - fileContents = fileContents.Replace("[SIGNTOOL]", signTool); - System.IO.File.WriteAllText(innoSetupScriptFileName, fileContents); - - BuildContext.CakeContext.Information("Generating Inno Setup packages, this can take a while, especially when signing is enabled..."); - - BuildContext.CakeContext.InnoSetup(innoSetupScriptFileName, new InnoSetupSettings - { - OutputDirectory = innoSetupReleasesRoot - }); - - if (BuildContext.Wpf.UpdateDeploymentsShare) - { - BuildContext.CakeContext.Information("Copying Inno Setup files to deployments share at '{0}'", installersOnDeploymentsShare); - - // Copy the following files: - // - Setup.exe => [projectName]-[version].exe - // - Setup.exe => [projectName]-[channel].exe - - var installerSourceFile = System.IO.Path.Combine(innoSetupReleasesRoot, $"{projectName}_{BuildContext.General.Version.FullSemVer}.exe"); - BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}_{BuildContext.General.Version.FullSemVer}.exe")); - BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}{setupSuffix}.exe")); - } - } - - //------------------------------------------------------------- - - public async Task GenerateDeploymentTargetAsync(string projectName) - { - var deploymentTarget = new DeploymentTarget - { - Name = "Inno Setup" - }; - - var channels = new [] - { - "alpha", - "beta", - "stable" - }; - - var deploymentGroupNames = new List(); - var projectDeploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); - - // Just a single group - deploymentGroupNames.Add("all"); - - foreach (var deploymentGroupName in deploymentGroupNames) - { - BuildContext.CakeContext.Information($"Searching for releases for deployment group '{deploymentGroupName}'"); - - var deploymentGroup = new DeploymentGroup - { - Name = deploymentGroupName - }; - - foreach (var channel in channels) - { - BuildContext.CakeContext.Information($"Searching for releases for deployment channel '{deploymentGroupName}/{channel}'"); - - var deploymentChannel = new DeploymentChannel - { - Name = channel - }; - - var targetDirectory = GetDeploymentsShareRootDirectory(projectName, channel); - - BuildContext.CakeContext.Information($"Searching for release files in '{targetDirectory}'"); - - var filter = $"{projectName}_*{channel}*.exe"; - if (channel == "stable") - { - filter = $"{projectName}_*.exe"; - } - - var installationFiles = System.IO.Directory.GetFiles(targetDirectory, filter); - - foreach (var installationFile in installationFiles) - { - var releaseFileInfo = new System.IO.FileInfo(installationFile); - var relativeFileName = new DirectoryPath(projectDeploymentShare).GetRelativePath(new FilePath(releaseFileInfo.FullName)).FullPath.Replace("\\", "/"); - - var releaseVersion = releaseFileInfo.Name - .Replace($"{projectName}", string.Empty) - .Replace($".exe", string.Empty) - .Trim('_'); - - // Either empty or matching a release channel should be ignored - if (string.IsNullOrWhiteSpace(releaseVersion) || - channels.Any(x => x == releaseVersion)) - { - BuildContext.CakeContext.Information($"Ignoring '{installationFile}'"); - continue; - } - - // Special case for stable releases - if (channel == "stable") - { - if (releaseVersion.Contains("-alpha") || - releaseVersion.Contains("-beta")) - { - BuildContext.CakeContext.Information($"Ignoring '{installationFile}'"); - continue; - } - } - - BuildContext.CakeContext.Information($"Applying release based on '{installationFile}'"); - - var release = new DeploymentRelease - { - Name = releaseVersion, - Timestamp = releaseFileInfo.CreationTimeUtc - }; - - // Full release - release.Full = new DeploymentReleasePart - { - RelativeFileName = relativeFileName, - Size = (ulong)releaseFileInfo.Length - }; - - deploymentChannel.Releases.Add(release); - } - - deploymentGroup.Channels.Add(deploymentChannel); - } - - deploymentTarget.Groups.Add(deploymentGroup); - } - - return deploymentTarget; - } - - //------------------------------------------------------------- - - private string GetDeploymentsShareRootDirectory(string projectName, string channel) - { - var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); - - var installersOnDeploymentsShare = System.IO.Path.Combine(deploymentShare, "installer"); - BuildContext.CakeContext.CreateDirectory(installersOnDeploymentsShare); - - return installersOnDeploymentsShare; - } -} +//------------------------------------------------------------- + +public class InnoSetupInstaller : IInstaller +{ + public InnoSetupInstaller(BuildContext buildContext) + { + BuildContext = buildContext; + + IsEnabled = BuildContext.BuildServer.GetVariableAsBool("InnoSetupEnabled", true, showValue: true); + + if (IsEnabled) + { + // In the future, check if InnoSetup is installed. Log error if not + IsAvailable = IsEnabled; + } + } + + public BuildContext BuildContext { get; private set; } + + public bool IsEnabled { get; private set; } + + public bool IsAvailable { get; private set; } + + //------------------------------------------------------------- + + public async Task PackageAsync(string projectName, string channel) + { + if (!IsAvailable) + { + BuildContext.CakeContext.Information("Inno Setup is not enabled or available, skipping integration"); + return; + } + + var innoSetupTemplateDirectory = System.IO.Path.Combine(".", "deployment", "innosetup", projectName); + if (!BuildContext.CakeContext.DirectoryExists(innoSetupTemplateDirectory)) + { + BuildContext.CakeContext.Information($"Skip packaging of app '{projectName}' using Inno Setup since no Inno Setup template is present"); + return; + } + + BuildContext.CakeContext.LogSeparator($"Packaging app '{projectName}' using Inno Setup"); + + var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); + + var installersOnDeploymentsShare = System.IO.Path.Combine(deploymentShare, "installer"); + BuildContext.CakeContext.CreateDirectory(installersOnDeploymentsShare); + + var setupSuffix = BuildContext.Installer.GetDeploymentChannelSuffix(); + + var innoSetupOutputRoot = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, "innosetup", projectName); + var innoSetupReleasesRoot = System.IO.Path.Combine(innoSetupOutputRoot, "releases"); + var innoSetupOutputIntermediate = System.IO.Path.Combine(innoSetupOutputRoot, "intermediate"); + + BuildContext.CakeContext.CreateDirectory(innoSetupReleasesRoot); + BuildContext.CakeContext.CreateDirectory(innoSetupOutputIntermediate); + + // Copy all files to the intermediate directory so Inno Setup knows what to do + var appSourceDirectory = string.Format("{0}/{1}/**/*", BuildContext.General.OutputRootDirectory, projectName); + var appTargetDirectory = innoSetupOutputIntermediate; + + BuildContext.CakeContext.Information("Copying files from '{0}' => '{1}'", appSourceDirectory, appTargetDirectory); + + BuildContext.CakeContext.CopyFiles(appSourceDirectory, appTargetDirectory, true); + + // Set up InnoSetup template + BuildContext.CakeContext.CopyDirectory(innoSetupTemplateDirectory, innoSetupOutputIntermediate); + + var innoSetupScriptFileName = System.IO.Path.Combine(innoSetupOutputIntermediate, "setup.iss"); + var fileContents = System.IO.File.ReadAllText(innoSetupScriptFileName); + fileContents = fileContents.Replace("[CHANNEL_SUFFIX]", setupSuffix); + fileContents = fileContents.Replace("[CHANNEL]", BuildContext.Installer.GetDeploymentChannelSuffix(" (", ")")); + fileContents = fileContents.Replace("[VERSION]", BuildContext.General.Version.MajorMinorPatch); + fileContents = fileContents.Replace("[VERSION_DISPLAY]", BuildContext.General.Version.FullSemVer); + fileContents = fileContents.Replace("[WIZARDIMAGEFILE]", string.Format("logo_large{0}", setupSuffix)); + + var signTool = string.Empty; + if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) + { + signTool = string.Format("SignTool={0}", BuildContext.General.CodeSign.CertificateSubjectName); + } + + fileContents = fileContents.Replace("[SIGNTOOL]", signTool); + System.IO.File.WriteAllText(innoSetupScriptFileName, fileContents); + + BuildContext.CakeContext.Information("Generating Inno Setup packages, this can take a while, especially when signing is enabled..."); + + BuildContext.CakeContext.InnoSetup(innoSetupScriptFileName, new InnoSetupSettings + { + OutputDirectory = innoSetupReleasesRoot + }); + + if (BuildContext.Wpf.UpdateDeploymentsShare) + { + BuildContext.CakeContext.Information("Copying Inno Setup files to deployments share at '{0}'", installersOnDeploymentsShare); + + // Copy the following files: + // - Setup.exe => [projectName]-[version].exe + // - Setup.exe => [projectName]-[channel].exe + + var installerSourceFile = System.IO.Path.Combine(innoSetupReleasesRoot, $"{projectName}_{BuildContext.General.Version.FullSemVer}.exe"); + BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}_{BuildContext.General.Version.FullSemVer}.exe")); + BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}{setupSuffix}.exe")); + } + } + + //------------------------------------------------------------- + + public async Task GenerateDeploymentTargetAsync(string projectName) + { + var deploymentTarget = new DeploymentTarget + { + Name = "Inno Setup" + }; + + var channels = new [] + { + "alpha", + "beta", + "stable" + }; + + var deploymentGroupNames = new List(); + var projectDeploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); + + // Just a single group + deploymentGroupNames.Add("all"); + + foreach (var deploymentGroupName in deploymentGroupNames) + { + BuildContext.CakeContext.Information($"Searching for releases for deployment group '{deploymentGroupName}'"); + + var deploymentGroup = new DeploymentGroup + { + Name = deploymentGroupName + }; + + foreach (var channel in channels) + { + BuildContext.CakeContext.Information($"Searching for releases for deployment channel '{deploymentGroupName}/{channel}'"); + + var deploymentChannel = new DeploymentChannel + { + Name = channel + }; + + var targetDirectory = GetDeploymentsShareRootDirectory(projectName, channel); + + BuildContext.CakeContext.Information($"Searching for release files in '{targetDirectory}'"); + + var filter = $"{projectName}_*{channel}*.exe"; + if (channel == "stable") + { + filter = $"{projectName}_*.exe"; + } + + var installationFiles = System.IO.Directory.GetFiles(targetDirectory, filter); + + foreach (var installationFile in installationFiles) + { + var releaseFileInfo = new System.IO.FileInfo(installationFile); + var relativeFileName = new DirectoryPath(projectDeploymentShare).GetRelativePath(new FilePath(releaseFileInfo.FullName)).FullPath.Replace("\\", "/"); + + var releaseVersion = releaseFileInfo.Name + .Replace($"{projectName}", string.Empty) + .Replace($".exe", string.Empty) + .Trim('_'); + + // Either empty or matching a release channel should be ignored + if (string.IsNullOrWhiteSpace(releaseVersion) || + channels.Any(x => x == releaseVersion)) + { + BuildContext.CakeContext.Information($"Ignoring '{installationFile}'"); + continue; + } + + // Special case for stable releases + if (channel == "stable") + { + if (releaseVersion.Contains("-alpha") || + releaseVersion.Contains("-beta")) + { + BuildContext.CakeContext.Information($"Ignoring '{installationFile}'"); + continue; + } + } + + BuildContext.CakeContext.Information($"Applying release based on '{installationFile}'"); + + var release = new DeploymentRelease + { + Name = releaseVersion, + Timestamp = releaseFileInfo.CreationTimeUtc + }; + + // Full release + release.Full = new DeploymentReleasePart + { + RelativeFileName = relativeFileName, + Size = (ulong)releaseFileInfo.Length + }; + + deploymentChannel.Releases.Add(release); + } + + deploymentGroup.Channels.Add(deploymentChannel); + } + + deploymentTarget.Groups.Add(deploymentGroup); + } + + return deploymentTarget; + } + + //------------------------------------------------------------- + + private string GetDeploymentsShareRootDirectory(string projectName, string channel) + { + var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); + + var installersOnDeploymentsShare = System.IO.Path.Combine(deploymentShare, "installer"); + BuildContext.CakeContext.CreateDirectory(installersOnDeploymentsShare); + + return installersOnDeploymentsShare; + } +} diff --git a/deployment/cake/installers-msix.cake b/deployment/cake/installers-msix.cake index 4aa88974..6e71d638 100644 --- a/deployment/cake/installers-msix.cake +++ b/deployment/cake/installers-msix.cake @@ -1,368 +1,368 @@ -//------------------------------------------------------------- - -public class MsixInstaller : IInstaller -{ - public MsixInstaller(BuildContext buildContext) - { - BuildContext = buildContext; - - Publisher = BuildContext.BuildServer.GetVariable("MsixPublisher", showValue: true); - UpdateUrl = BuildContext.BuildServer.GetVariable("MsixUpdateUrl", showValue: true); - IsEnabled = BuildContext.BuildServer.GetVariableAsBool("MsixEnabled", true, showValue: true); - - if (IsEnabled) - { - // In the future, check if Msix is installed. Log error if not - IsAvailable = IsEnabled; - } - } - - public BuildContext BuildContext { get; private set; } - - public string Publisher { get; private set; } - - public string UpdateUrl { get; private set; } - - public bool IsEnabled { get; private set; } - - public bool IsAvailable { get; private set; } - - //------------------------------------------------------------- - - public async Task PackageAsync(string projectName, string channel) - { - if (!IsAvailable) - { - BuildContext.CakeContext.Information("MSIX is not enabled or available, skipping integration"); - return; - } - - var makeAppxFileName = FindLatestMakeAppxFileName(); - if (!BuildContext.CakeContext.FileExists(makeAppxFileName)) - { - BuildContext.CakeContext.Information("Could not find MakeAppX.exe, skipping MSIX integration"); - return; - } - - var msixTemplateDirectory = System.IO.Path.Combine(".", "deployment", "msix", projectName); - if (!BuildContext.CakeContext.DirectoryExists(msixTemplateDirectory)) - { - BuildContext.CakeContext.Information($"Skip packaging of app '{projectName}' using MSIX since no MSIX template is present"); - return; - } - - var signToolCommand = string.Empty; - if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) - { - signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", BuildContext.General.CodeSign.TimeStampUri, - BuildContext.General.CodeSign.CertificateSubjectName, BuildContext.General.CodeSign.HashAlgorithm); - } - else - { - BuildContext.CakeContext.Warning("No sign tool is defined, MSIX will not be installable to (most or all) users"); - } - - BuildContext.CakeContext.LogSeparator($"Packaging app '{projectName}' using MSIX"); - - var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); - var installersOnDeploymentsShare = GetDeploymentsShareRootDirectory(projectName, channel); - - var setupSuffix = BuildContext.Installer.GetDeploymentChannelSuffix(); - - var msixOutputRoot = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, "msix", projectName); - var msixReleasesRoot = System.IO.Path.Combine(msixOutputRoot, "releases"); - var msixOutputIntermediate = System.IO.Path.Combine(msixOutputRoot, "intermediate"); - - BuildContext.CakeContext.CreateDirectory(msixReleasesRoot); - BuildContext.CakeContext.CreateDirectory(msixOutputIntermediate); - - // Set up MSIX template, all based on the documentation here: https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-manual-conversion - BuildContext.CakeContext.CopyDirectory(msixTemplateDirectory, msixOutputIntermediate); - - var msixInstallerName = $"{projectName}_{BuildContext.General.Version.FullSemVer}.msix"; - var installerSourceFile = System.IO.Path.Combine(msixReleasesRoot, msixInstallerName); - - var variables = new Dictionary(); - variables["[PRODUCT]"] = projectName; - variables["[PRODUCT_WITH_CHANNEL]"] = projectName + BuildContext.Installer.GetDeploymentChannelSuffix(""); - variables["[PRODUCT_WITH_CHANNEL_DISPLAY]"] = projectName + BuildContext.Installer.GetDeploymentChannelSuffix(" (", ")"); - variables["[PUBLISHER]"] = Publisher; - variables["[PUBLISHER_DISPLAY]"] = BuildContext.General.Copyright.Company; - variables["[CHANNEL_SUFFIX]"] = setupSuffix; - variables["[CHANNEL]"] = BuildContext.Installer.GetDeploymentChannelSuffix(" (", ")"); - variables["[VERSION]"] = BuildContext.General.Version.MajorMinorPatch; - variables["[VERSION_WITH_REVISION]"] = $"{BuildContext.General.Version.MajorMinorPatch}.{BuildContext.General.Version.CommitsSinceVersionSource}"; - variables["[VERSION_DISPLAY]"] = BuildContext.General.Version.FullSemVer; - variables["[WIZARDIMAGEFILE]"] = string.Format("logo_large{0}", setupSuffix); - - // Important: urls must be lower case, they are case sensitive in azure blob storage - variables["[URL_APPINSTALLER]"] = $"{UpdateUrl}/{projectName}/{channel}/msix/{projectName}.appinstaller".ToLower(); - variables["[URL_MSIX]"] = $"{UpdateUrl}/{projectName}/{channel}/msix/{msixInstallerName}".ToLower(); - - // Installer file - var msixScriptFileName = System.IO.Path.Combine(msixOutputIntermediate, "AppxManifest.xml"); - - ReplaceVariablesInFile(msixScriptFileName, variables); - - // Update file - var msixUpdateScriptFileName = System.IO.Path.Combine(msixOutputIntermediate, "App.AppInstaller"); - if (BuildContext.CakeContext.FileExists(msixUpdateScriptFileName)) - { - ReplaceVariablesInFile(msixUpdateScriptFileName, variables); - } - - // Copy all files to the intermediate directory so MSIX knows what to do - var appSourceDirectory = string.Format("{0}/{1}/**/*", BuildContext.General.OutputRootDirectory, projectName); - var appTargetDirectory = msixOutputIntermediate; - - BuildContext.CakeContext.Information("Copying files from '{0}' => '{1}'", appSourceDirectory, appTargetDirectory); - - BuildContext.CakeContext.CopyFiles(appSourceDirectory, appTargetDirectory, true); - - BuildContext.CakeContext.Information($"Signing files in '{appTargetDirectory}'"); - - var filesToSign = new List(); - - filesToSign.AddRange(BuildContext.CakeContext.GetFiles($"{appTargetDirectory}/**/*.dll").Select(x => x.FullPath)); - filesToSign.AddRange(BuildContext.CakeContext.GetFiles($"{appTargetDirectory}/**/*.exe").Select(x => x.FullPath)); - - SignFiles(BuildContext, signToolCommand, filesToSign); - - BuildContext.CakeContext.Information("Generating MSIX packages using MakeAppX..."); - - var processSettings = new ProcessSettings - { - WorkingDirectory = appTargetDirectory, - }; - - processSettings.WithArguments(a => a.Append("pack") - .AppendSwitchQuoted("/p", installerSourceFile) - //.AppendSwitchQuoted("/m", msixScriptFileName) // If we specify this one, we *must* provide a mappings file, which we don't want to do - //.AppendSwitchQuoted("/f", msixScriptFileName) - .AppendSwitchQuoted("/d", appTargetDirectory) - //.Append("/v") - .Append("/o")); - - using (var process = BuildContext.CakeContext.StartAndReturnProcess(makeAppxFileName, processSettings)) - { - process.WaitForExit(); - var exitCode = process.GetExitCode(); - - if (exitCode != 0) - { - throw new Exception($"Packaging failed, exit code is '{exitCode}'"); - } - } - - SignFile(BuildContext, signToolCommand, installerSourceFile); - - // Always copy the AppInstaller if available - if (BuildContext.CakeContext.FileExists(msixUpdateScriptFileName)) - { - BuildContext.CakeContext.Information("Copying update manifest to output directory"); - - // - App.AppInstaller => [projectName].AppInstaller - BuildContext.CakeContext.CopyFile(msixUpdateScriptFileName, System.IO.Path.Combine(msixReleasesRoot, $"{projectName}.AppInstaller")); - } - - if (BuildContext.Wpf.UpdateDeploymentsShare) - { - BuildContext.CakeContext.Information("Copying MSIX files to deployments share at '{0}'", installersOnDeploymentsShare); - - // Copy the following files: - // - [ProjectName]_[version].msix => [projectName]_[version].msix - // - [ProjectName]_[version].msix => [projectName]_[channel].msix - - BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, msixInstallerName)); - BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}{setupSuffix}.msix")); - - if (BuildContext.CakeContext.FileExists(msixUpdateScriptFileName)) - { - // - App.AppInstaller => [projectName].AppInstaller - BuildContext.CakeContext.CopyFile(msixUpdateScriptFileName, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}.AppInstaller")); - } - } - } - - //------------------------------------------------------------- - - public async Task GenerateDeploymentTargetAsync(string projectName) - { - var deploymentTarget = new DeploymentTarget - { - Name = "MSIX" - }; - - var channels = new [] - { - "alpha", - "beta", - "stable" - }; - - var deploymentGroupNames = new List(); - var projectDeploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); - - if (BuildContext.Wpf.GroupUpdatesByMajorVersion) - { - // Check every directory that we can parse as number - var directories = System.IO.Directory.GetDirectories(projectDeploymentShare); - - foreach (var directory in directories) - { - var deploymentGroupName = new System.IO.DirectoryInfo(directory).Name; - - if (int.TryParse(deploymentGroupName, out _)) - { - deploymentGroupNames.Add(deploymentGroupName); - } - } - } - else - { - // Just a single group - deploymentGroupNames.Add("all"); - } - - foreach (var deploymentGroupName in deploymentGroupNames) - { - BuildContext.CakeContext.Information($"Searching for releases for deployment group '{deploymentGroupName}'"); - - var deploymentGroup = new DeploymentGroup - { - Name = deploymentGroupName - }; - - var version = deploymentGroupName; - if (version == "all") - { - version = string.Empty; - } - - foreach (var channel in channels) - { - BuildContext.CakeContext.Information($"Searching for releases for deployment channel '{deploymentGroupName}/{channel}'"); - - var deploymentChannel = new DeploymentChannel - { - Name = channel - }; - - var targetDirectory = GetDeploymentsShareRootDirectory(projectName, channel, version); - - BuildContext.CakeContext.Information($"Searching for release files in '{targetDirectory}'"); - - var msixFiles = System.IO.Directory.GetFiles(targetDirectory, "*.msix"); - - foreach (var msixFile in msixFiles) - { - var releaseFileInfo = new System.IO.FileInfo(msixFile); - var relativeFileName = new DirectoryPath(projectDeploymentShare).GetRelativePath(new FilePath(releaseFileInfo.FullName)).FullPath.Replace("\\", "/"); - var releaseVersion = releaseFileInfo.Name - .Replace($"{projectName}_", string.Empty) - .Replace($".msix", string.Empty); - - // Either empty or matching a release channel should be ignored - if (string.IsNullOrWhiteSpace(releaseVersion) || - channels.Any(x => x == releaseVersion)) - { - BuildContext.CakeContext.Information($"Ignoring '{msixFile}'"); - continue; - } - - // Special case for stable releases - if (channel == "stable") - { - if (releaseVersion.Contains("-alpha") || - releaseVersion.Contains("-beta")) - { - BuildContext.CakeContext.Information($"Ignoring '{msixFile}'"); - continue; - } - } - - BuildContext.CakeContext.Information($"Applying release based on '{msixFile}'"); - - var release = new DeploymentRelease - { - Name = releaseVersion, - Timestamp = releaseFileInfo.CreationTimeUtc - }; - - // Only support full versions - release.Full = new DeploymentReleasePart - { - RelativeFileName = relativeFileName, - Size = (ulong)releaseFileInfo.Length - }; - - deploymentChannel.Releases.Add(release); - } - - deploymentGroup.Channels.Add(deploymentChannel); - } - - deploymentTarget.Groups.Add(deploymentGroup); - } - - return deploymentTarget; - } - - //------------------------------------------------------------- - - private string GetDeploymentsShareRootDirectory(string projectName, string channel) - { - var version = string.Empty; - - if (BuildContext.Wpf.GroupUpdatesByMajorVersion) - { - version = BuildContext.General.Version.Major; - } - - return GetDeploymentsShareRootDirectory(projectName, channel, version); - } - - //------------------------------------------------------------- - - private string GetDeploymentsShareRootDirectory(string projectName, string channel, string version) - { - var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); - - if (!string.IsNullOrWhiteSpace(version)) - { - deploymentShare = System.IO.Path.Combine(deploymentShare, version); - } - - var installersOnDeploymentsShare = System.IO.Path.Combine(deploymentShare, channel, "msix"); - BuildContext.CakeContext.CreateDirectory(installersOnDeploymentsShare); - - return installersOnDeploymentsShare; - } - - //------------------------------------------------------------- - - private void ReplaceVariablesInFile(string fileName, Dictionary variables) - { - var fileContents = System.IO.File.ReadAllText(fileName); - - foreach (var keyValuePair in variables) - { - fileContents = fileContents.Replace(keyValuePair.Key, keyValuePair.Value); - } - - System.IO.File.WriteAllText(fileName, fileContents); - } - - //------------------------------------------------------------- - - private string FindLatestMakeAppxFileName() - { - var directory = FindLatestWindowsKitsDirectory(BuildContext); - if (directory != null) - { - return System.IO.Path.Combine(directory, "x64", "makeappx.exe"); - } - - return null; - } +//------------------------------------------------------------- + +public class MsixInstaller : IInstaller +{ + public MsixInstaller(BuildContext buildContext) + { + BuildContext = buildContext; + + Publisher = BuildContext.BuildServer.GetVariable("MsixPublisher", showValue: true); + UpdateUrl = BuildContext.BuildServer.GetVariable("MsixUpdateUrl", showValue: true); + IsEnabled = BuildContext.BuildServer.GetVariableAsBool("MsixEnabled", true, showValue: true); + + if (IsEnabled) + { + // In the future, check if Msix is installed. Log error if not + IsAvailable = IsEnabled; + } + } + + public BuildContext BuildContext { get; private set; } + + public string Publisher { get; private set; } + + public string UpdateUrl { get; private set; } + + public bool IsEnabled { get; private set; } + + public bool IsAvailable { get; private set; } + + //------------------------------------------------------------- + + public async Task PackageAsync(string projectName, string channel) + { + if (!IsAvailable) + { + BuildContext.CakeContext.Information("MSIX is not enabled or available, skipping integration"); + return; + } + + var makeAppxFileName = FindLatestMakeAppxFileName(); + if (!BuildContext.CakeContext.FileExists(makeAppxFileName)) + { + BuildContext.CakeContext.Information("Could not find MakeAppX.exe, skipping MSIX integration"); + return; + } + + var msixTemplateDirectory = System.IO.Path.Combine(".", "deployment", "msix", projectName); + if (!BuildContext.CakeContext.DirectoryExists(msixTemplateDirectory)) + { + BuildContext.CakeContext.Information($"Skip packaging of app '{projectName}' using MSIX since no MSIX template is present"); + return; + } + + var signToolCommand = string.Empty; + if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) + { + signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", BuildContext.General.CodeSign.TimeStampUri, + BuildContext.General.CodeSign.CertificateSubjectName, BuildContext.General.CodeSign.HashAlgorithm); + } + else + { + BuildContext.CakeContext.Warning("No sign tool is defined, MSIX will not be installable to (most or all) users"); + } + + BuildContext.CakeContext.LogSeparator($"Packaging app '{projectName}' using MSIX"); + + var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); + var installersOnDeploymentsShare = GetDeploymentsShareRootDirectory(projectName, channel); + + var setupSuffix = BuildContext.Installer.GetDeploymentChannelSuffix(); + + var msixOutputRoot = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, "msix", projectName); + var msixReleasesRoot = System.IO.Path.Combine(msixOutputRoot, "releases"); + var msixOutputIntermediate = System.IO.Path.Combine(msixOutputRoot, "intermediate"); + + BuildContext.CakeContext.CreateDirectory(msixReleasesRoot); + BuildContext.CakeContext.CreateDirectory(msixOutputIntermediate); + + // Set up MSIX template, all based on the documentation here: https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-manual-conversion + BuildContext.CakeContext.CopyDirectory(msixTemplateDirectory, msixOutputIntermediate); + + var msixInstallerName = $"{projectName}_{BuildContext.General.Version.FullSemVer}.msix"; + var installerSourceFile = System.IO.Path.Combine(msixReleasesRoot, msixInstallerName); + + var variables = new Dictionary(); + variables["[PRODUCT]"] = projectName; + variables["[PRODUCT_WITH_CHANNEL]"] = projectName + BuildContext.Installer.GetDeploymentChannelSuffix(""); + variables["[PRODUCT_WITH_CHANNEL_DISPLAY]"] = projectName + BuildContext.Installer.GetDeploymentChannelSuffix(" (", ")"); + variables["[PUBLISHER]"] = Publisher; + variables["[PUBLISHER_DISPLAY]"] = BuildContext.General.Copyright.Company; + variables["[CHANNEL_SUFFIX]"] = setupSuffix; + variables["[CHANNEL]"] = BuildContext.Installer.GetDeploymentChannelSuffix(" (", ")"); + variables["[VERSION]"] = BuildContext.General.Version.MajorMinorPatch; + variables["[VERSION_WITH_REVISION]"] = $"{BuildContext.General.Version.MajorMinorPatch}.{BuildContext.General.Version.CommitsSinceVersionSource}"; + variables["[VERSION_DISPLAY]"] = BuildContext.General.Version.FullSemVer; + variables["[WIZARDIMAGEFILE]"] = string.Format("logo_large{0}", setupSuffix); + + // Important: urls must be lower case, they are case sensitive in azure blob storage + variables["[URL_APPINSTALLER]"] = $"{UpdateUrl}/{projectName}/{channel}/msix/{projectName}.appinstaller".ToLower(); + variables["[URL_MSIX]"] = $"{UpdateUrl}/{projectName}/{channel}/msix/{msixInstallerName}".ToLower(); + + // Installer file + var msixScriptFileName = System.IO.Path.Combine(msixOutputIntermediate, "AppxManifest.xml"); + + ReplaceVariablesInFile(msixScriptFileName, variables); + + // Update file + var msixUpdateScriptFileName = System.IO.Path.Combine(msixOutputIntermediate, "App.AppInstaller"); + if (BuildContext.CakeContext.FileExists(msixUpdateScriptFileName)) + { + ReplaceVariablesInFile(msixUpdateScriptFileName, variables); + } + + // Copy all files to the intermediate directory so MSIX knows what to do + var appSourceDirectory = string.Format("{0}/{1}/**/*", BuildContext.General.OutputRootDirectory, projectName); + var appTargetDirectory = msixOutputIntermediate; + + BuildContext.CakeContext.Information("Copying files from '{0}' => '{1}'", appSourceDirectory, appTargetDirectory); + + BuildContext.CakeContext.CopyFiles(appSourceDirectory, appTargetDirectory, true); + + BuildContext.CakeContext.Information($"Signing files in '{appTargetDirectory}'"); + + var filesToSign = new List(); + + filesToSign.AddRange(BuildContext.CakeContext.GetFiles($"{appTargetDirectory}/**/*.dll").Select(x => x.FullPath)); + filesToSign.AddRange(BuildContext.CakeContext.GetFiles($"{appTargetDirectory}/**/*.exe").Select(x => x.FullPath)); + + SignFiles(BuildContext, signToolCommand, filesToSign); + + BuildContext.CakeContext.Information("Generating MSIX packages using MakeAppX..."); + + var processSettings = new ProcessSettings + { + WorkingDirectory = appTargetDirectory, + }; + + processSettings.WithArguments(a => a.Append("pack") + .AppendSwitchQuoted("/p", installerSourceFile) + //.AppendSwitchQuoted("/m", msixScriptFileName) // If we specify this one, we *must* provide a mappings file, which we don't want to do + //.AppendSwitchQuoted("/f", msixScriptFileName) + .AppendSwitchQuoted("/d", appTargetDirectory) + //.Append("/v") + .Append("/o")); + + using (var process = BuildContext.CakeContext.StartAndReturnProcess(makeAppxFileName, processSettings)) + { + process.WaitForExit(); + var exitCode = process.GetExitCode(); + + if (exitCode != 0) + { + throw new Exception($"Packaging failed, exit code is '{exitCode}'"); + } + } + + SignFile(BuildContext, signToolCommand, installerSourceFile); + + // Always copy the AppInstaller if available + if (BuildContext.CakeContext.FileExists(msixUpdateScriptFileName)) + { + BuildContext.CakeContext.Information("Copying update manifest to output directory"); + + // - App.AppInstaller => [projectName].AppInstaller + BuildContext.CakeContext.CopyFile(msixUpdateScriptFileName, System.IO.Path.Combine(msixReleasesRoot, $"{projectName}.AppInstaller")); + } + + if (BuildContext.Wpf.UpdateDeploymentsShare) + { + BuildContext.CakeContext.Information("Copying MSIX files to deployments share at '{0}'", installersOnDeploymentsShare); + + // Copy the following files: + // - [ProjectName]_[version].msix => [projectName]_[version].msix + // - [ProjectName]_[version].msix => [projectName]_[channel].msix + + BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, msixInstallerName)); + BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}{setupSuffix}.msix")); + + if (BuildContext.CakeContext.FileExists(msixUpdateScriptFileName)) + { + // - App.AppInstaller => [projectName].AppInstaller + BuildContext.CakeContext.CopyFile(msixUpdateScriptFileName, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}.AppInstaller")); + } + } + } + + //------------------------------------------------------------- + + public async Task GenerateDeploymentTargetAsync(string projectName) + { + var deploymentTarget = new DeploymentTarget + { + Name = "MSIX" + }; + + var channels = new [] + { + "alpha", + "beta", + "stable" + }; + + var deploymentGroupNames = new List(); + var projectDeploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); + + if (BuildContext.Wpf.GroupUpdatesByMajorVersion) + { + // Check every directory that we can parse as number + var directories = System.IO.Directory.GetDirectories(projectDeploymentShare); + + foreach (var directory in directories) + { + var deploymentGroupName = new System.IO.DirectoryInfo(directory).Name; + + if (int.TryParse(deploymentGroupName, out _)) + { + deploymentGroupNames.Add(deploymentGroupName); + } + } + } + else + { + // Just a single group + deploymentGroupNames.Add("all"); + } + + foreach (var deploymentGroupName in deploymentGroupNames) + { + BuildContext.CakeContext.Information($"Searching for releases for deployment group '{deploymentGroupName}'"); + + var deploymentGroup = new DeploymentGroup + { + Name = deploymentGroupName + }; + + var version = deploymentGroupName; + if (version == "all") + { + version = string.Empty; + } + + foreach (var channel in channels) + { + BuildContext.CakeContext.Information($"Searching for releases for deployment channel '{deploymentGroupName}/{channel}'"); + + var deploymentChannel = new DeploymentChannel + { + Name = channel + }; + + var targetDirectory = GetDeploymentsShareRootDirectory(projectName, channel, version); + + BuildContext.CakeContext.Information($"Searching for release files in '{targetDirectory}'"); + + var msixFiles = System.IO.Directory.GetFiles(targetDirectory, "*.msix"); + + foreach (var msixFile in msixFiles) + { + var releaseFileInfo = new System.IO.FileInfo(msixFile); + var relativeFileName = new DirectoryPath(projectDeploymentShare).GetRelativePath(new FilePath(releaseFileInfo.FullName)).FullPath.Replace("\\", "/"); + var releaseVersion = releaseFileInfo.Name + .Replace($"{projectName}_", string.Empty) + .Replace($".msix", string.Empty); + + // Either empty or matching a release channel should be ignored + if (string.IsNullOrWhiteSpace(releaseVersion) || + channels.Any(x => x == releaseVersion)) + { + BuildContext.CakeContext.Information($"Ignoring '{msixFile}'"); + continue; + } + + // Special case for stable releases + if (channel == "stable") + { + if (releaseVersion.Contains("-alpha") || + releaseVersion.Contains("-beta")) + { + BuildContext.CakeContext.Information($"Ignoring '{msixFile}'"); + continue; + } + } + + BuildContext.CakeContext.Information($"Applying release based on '{msixFile}'"); + + var release = new DeploymentRelease + { + Name = releaseVersion, + Timestamp = releaseFileInfo.CreationTimeUtc + }; + + // Only support full versions + release.Full = new DeploymentReleasePart + { + RelativeFileName = relativeFileName, + Size = (ulong)releaseFileInfo.Length + }; + + deploymentChannel.Releases.Add(release); + } + + deploymentGroup.Channels.Add(deploymentChannel); + } + + deploymentTarget.Groups.Add(deploymentGroup); + } + + return deploymentTarget; + } + + //------------------------------------------------------------- + + private string GetDeploymentsShareRootDirectory(string projectName, string channel) + { + var version = string.Empty; + + if (BuildContext.Wpf.GroupUpdatesByMajorVersion) + { + version = BuildContext.General.Version.Major; + } + + return GetDeploymentsShareRootDirectory(projectName, channel, version); + } + + //------------------------------------------------------------- + + private string GetDeploymentsShareRootDirectory(string projectName, string channel, string version) + { + var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); + + if (!string.IsNullOrWhiteSpace(version)) + { + deploymentShare = System.IO.Path.Combine(deploymentShare, version); + } + + var installersOnDeploymentsShare = System.IO.Path.Combine(deploymentShare, channel, "msix"); + BuildContext.CakeContext.CreateDirectory(installersOnDeploymentsShare); + + return installersOnDeploymentsShare; + } + + //------------------------------------------------------------- + + private void ReplaceVariablesInFile(string fileName, Dictionary variables) + { + var fileContents = System.IO.File.ReadAllText(fileName); + + foreach (var keyValuePair in variables) + { + fileContents = fileContents.Replace(keyValuePair.Key, keyValuePair.Value); + } + + System.IO.File.WriteAllText(fileName, fileContents); + } + + //------------------------------------------------------------- + + private string FindLatestMakeAppxFileName() + { + var directory = FindLatestWindowsKitsDirectory(BuildContext); + if (directory != null) + { + return System.IO.Path.Combine(directory, "x64", "makeappx.exe"); + } + + return null; + } } \ No newline at end of file diff --git a/deployment/cake/installers-squirrel.cake b/deployment/cake/installers-squirrel.cake index 10ac3f22..eb544339 100644 --- a/deployment/cake/installers-squirrel.cake +++ b/deployment/cake/installers-squirrel.cake @@ -1,362 +1,362 @@ -#addin "nuget:?package=Cake.Squirrel&version=0.15.2" - -#tool "nuget:?package=Squirrel.Windows&version=2.0.1" - -//------------------------------------------------------------- - -public class SquirrelInstaller : IInstaller -{ - public SquirrelInstaller(BuildContext buildContext) - { - BuildContext = buildContext; - - IsEnabled = BuildContext.BuildServer.GetVariableAsBool("SquirrelEnabled", true, showValue: true); - - if (IsEnabled) - { - // In the future, check if Squirrel is installed. Log error if not - IsAvailable = IsEnabled; - } - } - - public BuildContext BuildContext { get; private set; } - - public bool IsEnabled { get; private set; } - - public bool IsAvailable { get; private set; } - - //------------------------------------------------------------- - - public async Task PackageAsync(string projectName, string channel) - { - if (!IsAvailable) - { - BuildContext.CakeContext.Information("Squirrel is not enabled or available, skipping integration"); - return; - } - - // There are 2 flavors: - // - // 1: Non-grouped: /[app]/[channel] (e.g. /MyApp/alpha) - // Updates will always be applied, even to new major versions - // - // 2: Grouped by major version: /[app]/[major_version]/[channel] (e.g. /MyApp/4/alpha) - // Updates will only be applied to non-major updates. This allows manual migration to - // new major versions, which is very useful when there are dependencies that need to - // be updated before a new major version can be switched to. - var squirrelOutputRoot = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, "squirrel", projectName); - - if (BuildContext.Wpf.GroupUpdatesByMajorVersion) - { - squirrelOutputRoot = System.IO.Path.Combine(squirrelOutputRoot, BuildContext.General.Version.Major); - } - - squirrelOutputRoot = System.IO.Path.Combine(squirrelOutputRoot, channel); - - var squirrelReleasesRoot = System.IO.Path.Combine(squirrelOutputRoot, "releases"); - var squirrelOutputIntermediate = System.IO.Path.Combine(squirrelOutputRoot, "intermediate"); - - var nuSpecTemplateFileName = System.IO.Path.Combine(".", "deployment", "squirrel", "template", $"{projectName}.nuspec"); - var nuSpecFileName = System.IO.Path.Combine(squirrelOutputIntermediate, $"{projectName}.nuspec"); - var nuGetFileName = System.IO.Path.Combine(squirrelOutputIntermediate, $"{projectName}.{BuildContext.General.Version.NuGet}.nupkg"); - - if (!BuildContext.CakeContext.FileExists(nuSpecTemplateFileName)) - { - BuildContext.CakeContext.Information("Skip packaging of WPF app '{0}' using Squirrel since no Squirrel template is present"); - return; - } - - BuildContext.CakeContext.LogSeparator("Packaging WPF app '{0}' using Squirrel", projectName); - - BuildContext.CakeContext.CreateDirectory(squirrelReleasesRoot); - BuildContext.CakeContext.CreateDirectory(squirrelOutputIntermediate); - - // Set up Squirrel nuspec - BuildContext.CakeContext.CopyFile(nuSpecTemplateFileName, nuSpecFileName); - - var setupSuffix = BuildContext.Installer.GetDeploymentChannelSuffix(); - - // Squirrel does not seem to support . in the names - var projectSlug = GetProjectSlug(projectName, "_"); - - BuildContext.CakeContext.TransformConfig(nuSpecFileName, - new TransformationCollection - { - { "package/metadata/id", $"{projectSlug}{setupSuffix}" }, - { "package/metadata/version", BuildContext.General.Version.NuGet }, - { "package/metadata/authors", BuildContext.General.Copyright.Company }, - { "package/metadata/owners", BuildContext.General.Copyright.Company }, - { "package/metadata/copyright", string.Format("Copyright © {0} {1} - {2}", BuildContext.General.Copyright.Company, BuildContext.General.Copyright.StartYear, DateTime.Now.Year) }, - }); - - var fileContents = System.IO.File.ReadAllText(nuSpecFileName); - fileContents = fileContents.Replace("[CHANNEL_SUFFIX]", setupSuffix); - fileContents = fileContents.Replace("[CHANNEL]", BuildContext.Installer.GetDeploymentChannelSuffix(" (", ")")); - System.IO.File.WriteAllText(nuSpecFileName, fileContents); - - // Copy all files to the lib so Squirrel knows what to do - var appSourceDirectory = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, projectName); - var appTargetDirectory = System.IO.Path.Combine(squirrelOutputIntermediate, "lib"); - - BuildContext.CakeContext.Information("Copying files from '{0}' => '{1}'", appSourceDirectory, appTargetDirectory); - - BuildContext.CakeContext.CopyDirectory(appSourceDirectory, appTargetDirectory); - - var squirrelSourceFile = BuildContext.CakeContext.GetFiles("./tools/squirrel.windows.*/tools/Squirrel.exe").Single(); - - // We need to be 1 level deeper, let's just walk each directory in case we can support multi-platform releases - // in the future - foreach (var subDirectory in BuildContext.CakeContext.GetSubDirectories(appTargetDirectory)) - { - var squirrelTargetFile = System.IO.Path.Combine(appTargetDirectory, subDirectory.Segments[subDirectory.Segments.Length - 1], "Squirrel.exe"); - - BuildContext.CakeContext.Information("Copying Squirrel.exe to support self-updates from '{0}' => '{1}'", squirrelSourceFile, squirrelTargetFile); - - BuildContext.CakeContext.CopyFile(squirrelSourceFile, squirrelTargetFile); - } - - // Make sure all files are signed before we package them for Squirrel (saves potential errors occurring later in squirrel releasify) - var signToolCommand = string.Empty; - - if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) - { - // Note: Squirrel uses it's own sign tool, so make sure to follow their specs - signToolCommand = string.Format("/a /t {0} /n {1}", BuildContext.General.CodeSign.TimeStampUri, - BuildContext.General.CodeSign.CertificateSubjectName); - } - - var nuGetSettings = new NuGetPackSettings - { - NoPackageAnalysis = true, - OutputDirectory = squirrelOutputIntermediate, - Verbosity = NuGetVerbosity.Detailed, - }; - - // Fix for target framework issues - nuGetSettings.Properties.Add("TargetPlatformVersion", "7.0"); - - // Create NuGet package - BuildContext.CakeContext.NuGetPack(nuSpecFileName, nuGetSettings); - - // Rename so we have the right nuget package file names (without the channel) - if (!string.IsNullOrWhiteSpace(setupSuffix)) - { - var sourcePackageFileName = System.IO.Path.Combine(squirrelOutputIntermediate, $"{projectSlug}{setupSuffix}.{BuildContext.General.Version.NuGet}.nupkg"); - var targetPackageFileName = System.IO.Path.Combine(squirrelOutputIntermediate, $"{projectName}.{BuildContext.General.Version.NuGet}.nupkg"); - - BuildContext.CakeContext.Information("Moving file from '{0}' => '{1}'", sourcePackageFileName, targetPackageFileName); - - BuildContext.CakeContext.MoveFile(sourcePackageFileName, targetPackageFileName); - } - - // Copy deployments share to the intermediate root so we can locally create the Squirrel releases - - var releasesSourceDirectory = GetDeploymentsShareRootDirectory(projectName, channel); - var releasesTargetDirectory = squirrelReleasesRoot; - - BuildContext.CakeContext.CreateDirectory(releasesSourceDirectory); - BuildContext.CakeContext.CreateDirectory(releasesTargetDirectory); - - BuildContext.CakeContext.Information("Copying releases from '{0}' => '{1}'", releasesSourceDirectory, releasesTargetDirectory); - - BuildContext.CakeContext.CopyDirectory(releasesSourceDirectory, releasesTargetDirectory); - - // Squirrelify! - var squirrelSettings = new SquirrelSettings(); - squirrelSettings.Silent = false; - squirrelSettings.NoMsi = false; - squirrelSettings.ReleaseDirectory = squirrelReleasesRoot; - squirrelSettings.LoadingGif = System.IO.Path.Combine(".", "deployment", "squirrel", "loading.gif"); - - // Note: this is not really generic, but this is where we store our icons file, we can - // always change this in the future - var iconFileName = System.IO.Path.Combine(".", "design", "logo", $"logo{setupSuffix}.ico"); - squirrelSettings.Icon = iconFileName; - squirrelSettings.SetupIcon = iconFileName; - - if (!string.IsNullOrWhiteSpace(signToolCommand)) - { - squirrelSettings.SigningParameters = signToolCommand; - } - - BuildContext.CakeContext.Information("Generating Squirrel packages, this can take a while, especially when signing is enabled..."); - - BuildContext.CakeContext.Squirrel(nuGetFileName, squirrelSettings, true, false); - - if (BuildContext.Wpf.UpdateDeploymentsShare) - { - BuildContext.CakeContext.Information("Copying updated Squirrel files back to deployments share at '{0}'", releasesSourceDirectory); - - // Copy the following files: - // - [version]-full.nupkg - // - [version]-full.nupkg - // - Setup.exe => Setup.exe & WpfApp.exe - // - Setup.msi - // - RELEASES - - var squirrelFiles = BuildContext.CakeContext.GetFiles($"{squirrelReleasesRoot}/{projectSlug}{setupSuffix}-{BuildContext.General.Version.NuGet}*.nupkg"); - BuildContext.CakeContext.CopyFiles(squirrelFiles, releasesSourceDirectory); - BuildContext.CakeContext.CopyFile(System.IO.Path.Combine(squirrelReleasesRoot, "Setup.exe"), System.IO.Path.Combine(releasesSourceDirectory, "Setup.exe")); - BuildContext.CakeContext.CopyFile(System.IO.Path.Combine(squirrelReleasesRoot, "Setup.exe"), System.IO.Path.Combine(releasesSourceDirectory, $"{projectName}.exe")); - BuildContext.CakeContext.CopyFile(System.IO.Path.Combine(squirrelReleasesRoot, "Setup.msi"), System.IO.Path.Combine(releasesSourceDirectory, "Setup.msi")); - BuildContext.CakeContext.CopyFile(System.IO.Path.Combine(squirrelReleasesRoot, "RELEASES"), System.IO.Path.Combine(releasesSourceDirectory, "RELEASES")); - } - } - - //------------------------------------------------------------- - - public async Task GenerateDeploymentTargetAsync(string projectName) - { - var deploymentTarget = new DeploymentTarget - { - Name = "Squirrel" - }; - - var channels = new [] - { - "alpha", - "beta", - "stable" - }; - - var deploymentGroupNames = new List(); - var projectDeploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); - - if (BuildContext.Wpf.GroupUpdatesByMajorVersion) - { - // Check every directory that we can parse as number - var directories = System.IO.Directory.GetDirectories(projectDeploymentShare); - - foreach (var directory in directories) - { - var deploymentGroupName = new System.IO.DirectoryInfo(directory).Name; - - if (int.TryParse(deploymentGroupName, out _)) - { - deploymentGroupNames.Add(deploymentGroupName); - } - } - } - else - { - // Just a single group - deploymentGroupNames.Add("all"); - } - - foreach (var deploymentGroupName in deploymentGroupNames) - { - BuildContext.CakeContext.Information($"Searching for releases for deployment group '{deploymentGroupName}'"); - - var deploymentGroup = new DeploymentGroup - { - Name = deploymentGroupName - }; - - var version = deploymentGroupName; - if (version == "all") - { - version = string.Empty; - } - - foreach (var channel in channels) - { - BuildContext.CakeContext.Information($"Searching for releases for deployment channel '{deploymentGroupName}/{channel}'"); - - var deploymentChannel = new DeploymentChannel - { - Name = channel - }; - - var targetDirectory = GetDeploymentsShareRootDirectory(projectName, channel, version); - - BuildContext.CakeContext.Information($"Searching for release files in '{targetDirectory}'"); - - var fullNupkgFiles = System.IO.Directory.GetFiles(targetDirectory, "*-full.nupkg"); - - foreach (var fullNupkgFile in fullNupkgFiles) - { - BuildContext.CakeContext.Information($"Applying release based on '{fullNupkgFile}'"); - - var fullReleaseFileInfo = new System.IO.FileInfo(fullNupkgFile); - var fullRelativeFileName = new DirectoryPath(projectDeploymentShare).GetRelativePath(new FilePath(fullReleaseFileInfo.FullName)).FullPath.Replace("\\", "/"); - - var releaseVersion = fullReleaseFileInfo.Name - .Replace($"{projectName}_{channel}-", string.Empty) - .Replace($"-full.nupkg", string.Empty); - - // Exception for full releases, they don't contain the channel name - if (channel == "stable") - { - releaseVersion = releaseVersion.Replace($"{projectName}-", string.Empty); - } - - var release = new DeploymentRelease - { - Name = releaseVersion, - Timestamp = fullReleaseFileInfo.CreationTimeUtc - }; - - // Full release - release.Full = new DeploymentReleasePart - { - RelativeFileName = fullRelativeFileName, - Size = (ulong)fullReleaseFileInfo.Length - }; - - // Delta release - var deltaNupkgFile = fullNupkgFile.Replace("-full.nupkg", "-delta.nupkg"); - if (System.IO.File.Exists(deltaNupkgFile)) - { - var deltaReleaseFileInfo = new System.IO.FileInfo(deltaNupkgFile); - var deltafullRelativeFileName = new DirectoryPath(projectDeploymentShare).GetRelativePath(new FilePath(deltaReleaseFileInfo.FullName)).FullPath.Replace("\\", "/"); - - release.Delta = new DeploymentReleasePart - { - RelativeFileName = deltafullRelativeFileName, - Size = (ulong)deltaReleaseFileInfo.Length - }; - } - - deploymentChannel.Releases.Add(release); - } - - deploymentGroup.Channels.Add(deploymentChannel); - } - - deploymentTarget.Groups.Add(deploymentGroup); - } - - return deploymentTarget; - } - - //------------------------------------------------------------- - - private string GetDeploymentsShareRootDirectory(string projectName, string channel) - { - var version = string.Empty; - - if (BuildContext.Wpf.GroupUpdatesByMajorVersion) - { - version = BuildContext.General.Version.Major; - } - - return GetDeploymentsShareRootDirectory(projectName, channel, version); - } - - //------------------------------------------------------------- - - private string GetDeploymentsShareRootDirectory(string projectName, string channel, string version) - { - var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); - - if (!string.IsNullOrWhiteSpace(version)) - { - deploymentShare = System.IO.Path.Combine(deploymentShare, version); - } - - var installersOnDeploymentsShare = System.IO.Path.Combine(deploymentShare, channel); - BuildContext.CakeContext.CreateDirectory(installersOnDeploymentsShare); - - return installersOnDeploymentsShare; - } +#addin "nuget:?package=Cake.Squirrel&version=0.15.2" + +#tool "nuget:?package=Squirrel.Windows&version=2.0.1" + +//------------------------------------------------------------- + +public class SquirrelInstaller : IInstaller +{ + public SquirrelInstaller(BuildContext buildContext) + { + BuildContext = buildContext; + + IsEnabled = BuildContext.BuildServer.GetVariableAsBool("SquirrelEnabled", true, showValue: true); + + if (IsEnabled) + { + // In the future, check if Squirrel is installed. Log error if not + IsAvailable = IsEnabled; + } + } + + public BuildContext BuildContext { get; private set; } + + public bool IsEnabled { get; private set; } + + public bool IsAvailable { get; private set; } + + //------------------------------------------------------------- + + public async Task PackageAsync(string projectName, string channel) + { + if (!IsAvailable) + { + BuildContext.CakeContext.Information("Squirrel is not enabled or available, skipping integration"); + return; + } + + // There are 2 flavors: + // + // 1: Non-grouped: /[app]/[channel] (e.g. /MyApp/alpha) + // Updates will always be applied, even to new major versions + // + // 2: Grouped by major version: /[app]/[major_version]/[channel] (e.g. /MyApp/4/alpha) + // Updates will only be applied to non-major updates. This allows manual migration to + // new major versions, which is very useful when there are dependencies that need to + // be updated before a new major version can be switched to. + var squirrelOutputRoot = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, "squirrel", projectName); + + if (BuildContext.Wpf.GroupUpdatesByMajorVersion) + { + squirrelOutputRoot = System.IO.Path.Combine(squirrelOutputRoot, BuildContext.General.Version.Major); + } + + squirrelOutputRoot = System.IO.Path.Combine(squirrelOutputRoot, channel); + + var squirrelReleasesRoot = System.IO.Path.Combine(squirrelOutputRoot, "releases"); + var squirrelOutputIntermediate = System.IO.Path.Combine(squirrelOutputRoot, "intermediate"); + + var nuSpecTemplateFileName = System.IO.Path.Combine(".", "deployment", "squirrel", "template", $"{projectName}.nuspec"); + var nuSpecFileName = System.IO.Path.Combine(squirrelOutputIntermediate, $"{projectName}.nuspec"); + var nuGetFileName = System.IO.Path.Combine(squirrelOutputIntermediate, $"{projectName}.{BuildContext.General.Version.NuGet}.nupkg"); + + if (!BuildContext.CakeContext.FileExists(nuSpecTemplateFileName)) + { + BuildContext.CakeContext.Information("Skip packaging of WPF app '{0}' using Squirrel since no Squirrel template is present"); + return; + } + + BuildContext.CakeContext.LogSeparator("Packaging WPF app '{0}' using Squirrel", projectName); + + BuildContext.CakeContext.CreateDirectory(squirrelReleasesRoot); + BuildContext.CakeContext.CreateDirectory(squirrelOutputIntermediate); + + // Set up Squirrel nuspec + BuildContext.CakeContext.CopyFile(nuSpecTemplateFileName, nuSpecFileName); + + var setupSuffix = BuildContext.Installer.GetDeploymentChannelSuffix(); + + // Squirrel does not seem to support . in the names + var projectSlug = GetProjectSlug(projectName, "_"); + + BuildContext.CakeContext.TransformConfig(nuSpecFileName, + new TransformationCollection + { + { "package/metadata/id", $"{projectSlug}{setupSuffix}" }, + { "package/metadata/version", BuildContext.General.Version.NuGet }, + { "package/metadata/authors", BuildContext.General.Copyright.Company }, + { "package/metadata/owners", BuildContext.General.Copyright.Company }, + { "package/metadata/copyright", string.Format("Copyright © {0} {1} - {2}", BuildContext.General.Copyright.Company, BuildContext.General.Copyright.StartYear, DateTime.Now.Year) }, + }); + + var fileContents = System.IO.File.ReadAllText(nuSpecFileName); + fileContents = fileContents.Replace("[CHANNEL_SUFFIX]", setupSuffix); + fileContents = fileContents.Replace("[CHANNEL]", BuildContext.Installer.GetDeploymentChannelSuffix(" (", ")")); + System.IO.File.WriteAllText(nuSpecFileName, fileContents); + + // Copy all files to the lib so Squirrel knows what to do + var appSourceDirectory = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, projectName); + var appTargetDirectory = System.IO.Path.Combine(squirrelOutputIntermediate, "lib"); + + BuildContext.CakeContext.Information("Copying files from '{0}' => '{1}'", appSourceDirectory, appTargetDirectory); + + BuildContext.CakeContext.CopyDirectory(appSourceDirectory, appTargetDirectory); + + var squirrelSourceFile = BuildContext.CakeContext.GetFiles("./tools/squirrel.windows.*/tools/Squirrel.exe").Single(); + + // We need to be 1 level deeper, let's just walk each directory in case we can support multi-platform releases + // in the future + foreach (var subDirectory in BuildContext.CakeContext.GetSubDirectories(appTargetDirectory)) + { + var squirrelTargetFile = System.IO.Path.Combine(appTargetDirectory, subDirectory.Segments[subDirectory.Segments.Length - 1], "Squirrel.exe"); + + BuildContext.CakeContext.Information("Copying Squirrel.exe to support self-updates from '{0}' => '{1}'", squirrelSourceFile, squirrelTargetFile); + + BuildContext.CakeContext.CopyFile(squirrelSourceFile, squirrelTargetFile); + } + + // Make sure all files are signed before we package them for Squirrel (saves potential errors occurring later in squirrel releasify) + var signToolCommand = string.Empty; + + if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) + { + // Note: Squirrel uses it's own sign tool, so make sure to follow their specs + signToolCommand = string.Format("/a /t {0} /n {1}", BuildContext.General.CodeSign.TimeStampUri, + BuildContext.General.CodeSign.CertificateSubjectName); + } + + var nuGetSettings = new NuGetPackSettings + { + NoPackageAnalysis = true, + OutputDirectory = squirrelOutputIntermediate, + Verbosity = NuGetVerbosity.Detailed, + }; + + // Fix for target framework issues + nuGetSettings.Properties.Add("TargetPlatformVersion", "7.0"); + + // Create NuGet package + BuildContext.CakeContext.NuGetPack(nuSpecFileName, nuGetSettings); + + // Rename so we have the right nuget package file names (without the channel) + if (!string.IsNullOrWhiteSpace(setupSuffix)) + { + var sourcePackageFileName = System.IO.Path.Combine(squirrelOutputIntermediate, $"{projectSlug}{setupSuffix}.{BuildContext.General.Version.NuGet}.nupkg"); + var targetPackageFileName = System.IO.Path.Combine(squirrelOutputIntermediate, $"{projectName}.{BuildContext.General.Version.NuGet}.nupkg"); + + BuildContext.CakeContext.Information("Moving file from '{0}' => '{1}'", sourcePackageFileName, targetPackageFileName); + + BuildContext.CakeContext.MoveFile(sourcePackageFileName, targetPackageFileName); + } + + // Copy deployments share to the intermediate root so we can locally create the Squirrel releases + + var releasesSourceDirectory = GetDeploymentsShareRootDirectory(projectName, channel); + var releasesTargetDirectory = squirrelReleasesRoot; + + BuildContext.CakeContext.CreateDirectory(releasesSourceDirectory); + BuildContext.CakeContext.CreateDirectory(releasesTargetDirectory); + + BuildContext.CakeContext.Information("Copying releases from '{0}' => '{1}'", releasesSourceDirectory, releasesTargetDirectory); + + BuildContext.CakeContext.CopyDirectory(releasesSourceDirectory, releasesTargetDirectory); + + // Squirrelify! + var squirrelSettings = new SquirrelSettings(); + squirrelSettings.Silent = false; + squirrelSettings.NoMsi = false; + squirrelSettings.ReleaseDirectory = squirrelReleasesRoot; + squirrelSettings.LoadingGif = System.IO.Path.Combine(".", "deployment", "squirrel", "loading.gif"); + + // Note: this is not really generic, but this is where we store our icons file, we can + // always change this in the future + var iconFileName = System.IO.Path.Combine(".", "design", "logo", $"logo{setupSuffix}.ico"); + squirrelSettings.Icon = iconFileName; + squirrelSettings.SetupIcon = iconFileName; + + if (!string.IsNullOrWhiteSpace(signToolCommand)) + { + squirrelSettings.SigningParameters = signToolCommand; + } + + BuildContext.CakeContext.Information("Generating Squirrel packages, this can take a while, especially when signing is enabled..."); + + BuildContext.CakeContext.Squirrel(nuGetFileName, squirrelSettings, true, false); + + if (BuildContext.Wpf.UpdateDeploymentsShare) + { + BuildContext.CakeContext.Information("Copying updated Squirrel files back to deployments share at '{0}'", releasesSourceDirectory); + + // Copy the following files: + // - [version]-full.nupkg + // - [version]-full.nupkg + // - Setup.exe => Setup.exe & WpfApp.exe + // - Setup.msi + // - RELEASES + + var squirrelFiles = BuildContext.CakeContext.GetFiles($"{squirrelReleasesRoot}/{projectSlug}{setupSuffix}-{BuildContext.General.Version.NuGet}*.nupkg"); + BuildContext.CakeContext.CopyFiles(squirrelFiles, releasesSourceDirectory); + BuildContext.CakeContext.CopyFile(System.IO.Path.Combine(squirrelReleasesRoot, "Setup.exe"), System.IO.Path.Combine(releasesSourceDirectory, "Setup.exe")); + BuildContext.CakeContext.CopyFile(System.IO.Path.Combine(squirrelReleasesRoot, "Setup.exe"), System.IO.Path.Combine(releasesSourceDirectory, $"{projectName}.exe")); + BuildContext.CakeContext.CopyFile(System.IO.Path.Combine(squirrelReleasesRoot, "Setup.msi"), System.IO.Path.Combine(releasesSourceDirectory, "Setup.msi")); + BuildContext.CakeContext.CopyFile(System.IO.Path.Combine(squirrelReleasesRoot, "RELEASES"), System.IO.Path.Combine(releasesSourceDirectory, "RELEASES")); + } + } + + //------------------------------------------------------------- + + public async Task GenerateDeploymentTargetAsync(string projectName) + { + var deploymentTarget = new DeploymentTarget + { + Name = "Squirrel" + }; + + var channels = new [] + { + "alpha", + "beta", + "stable" + }; + + var deploymentGroupNames = new List(); + var projectDeploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); + + if (BuildContext.Wpf.GroupUpdatesByMajorVersion) + { + // Check every directory that we can parse as number + var directories = System.IO.Directory.GetDirectories(projectDeploymentShare); + + foreach (var directory in directories) + { + var deploymentGroupName = new System.IO.DirectoryInfo(directory).Name; + + if (int.TryParse(deploymentGroupName, out _)) + { + deploymentGroupNames.Add(deploymentGroupName); + } + } + } + else + { + // Just a single group + deploymentGroupNames.Add("all"); + } + + foreach (var deploymentGroupName in deploymentGroupNames) + { + BuildContext.CakeContext.Information($"Searching for releases for deployment group '{deploymentGroupName}'"); + + var deploymentGroup = new DeploymentGroup + { + Name = deploymentGroupName + }; + + var version = deploymentGroupName; + if (version == "all") + { + version = string.Empty; + } + + foreach (var channel in channels) + { + BuildContext.CakeContext.Information($"Searching for releases for deployment channel '{deploymentGroupName}/{channel}'"); + + var deploymentChannel = new DeploymentChannel + { + Name = channel + }; + + var targetDirectory = GetDeploymentsShareRootDirectory(projectName, channel, version); + + BuildContext.CakeContext.Information($"Searching for release files in '{targetDirectory}'"); + + var fullNupkgFiles = System.IO.Directory.GetFiles(targetDirectory, "*-full.nupkg"); + + foreach (var fullNupkgFile in fullNupkgFiles) + { + BuildContext.CakeContext.Information($"Applying release based on '{fullNupkgFile}'"); + + var fullReleaseFileInfo = new System.IO.FileInfo(fullNupkgFile); + var fullRelativeFileName = new DirectoryPath(projectDeploymentShare).GetRelativePath(new FilePath(fullReleaseFileInfo.FullName)).FullPath.Replace("\\", "/"); + + var releaseVersion = fullReleaseFileInfo.Name + .Replace($"{projectName}_{channel}-", string.Empty) + .Replace($"-full.nupkg", string.Empty); + + // Exception for full releases, they don't contain the channel name + if (channel == "stable") + { + releaseVersion = releaseVersion.Replace($"{projectName}-", string.Empty); + } + + var release = new DeploymentRelease + { + Name = releaseVersion, + Timestamp = fullReleaseFileInfo.CreationTimeUtc + }; + + // Full release + release.Full = new DeploymentReleasePart + { + RelativeFileName = fullRelativeFileName, + Size = (ulong)fullReleaseFileInfo.Length + }; + + // Delta release + var deltaNupkgFile = fullNupkgFile.Replace("-full.nupkg", "-delta.nupkg"); + if (System.IO.File.Exists(deltaNupkgFile)) + { + var deltaReleaseFileInfo = new System.IO.FileInfo(deltaNupkgFile); + var deltafullRelativeFileName = new DirectoryPath(projectDeploymentShare).GetRelativePath(new FilePath(deltaReleaseFileInfo.FullName)).FullPath.Replace("\\", "/"); + + release.Delta = new DeploymentReleasePart + { + RelativeFileName = deltafullRelativeFileName, + Size = (ulong)deltaReleaseFileInfo.Length + }; + } + + deploymentChannel.Releases.Add(release); + } + + deploymentGroup.Channels.Add(deploymentChannel); + } + + deploymentTarget.Groups.Add(deploymentGroup); + } + + return deploymentTarget; + } + + //------------------------------------------------------------- + + private string GetDeploymentsShareRootDirectory(string projectName, string channel) + { + var version = string.Empty; + + if (BuildContext.Wpf.GroupUpdatesByMajorVersion) + { + version = BuildContext.General.Version.Major; + } + + return GetDeploymentsShareRootDirectory(projectName, channel, version); + } + + //------------------------------------------------------------- + + private string GetDeploymentsShareRootDirectory(string projectName, string channel, string version) + { + var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); + + if (!string.IsNullOrWhiteSpace(version)) + { + deploymentShare = System.IO.Path.Combine(deploymentShare, version); + } + + var installersOnDeploymentsShare = System.IO.Path.Combine(deploymentShare, channel); + BuildContext.CakeContext.CreateDirectory(installersOnDeploymentsShare); + + return installersOnDeploymentsShare; + } } \ No newline at end of file diff --git a/deployment/cake/installers.cake b/deployment/cake/installers.cake index 634ff50f..88462d1b 100644 --- a/deployment/cake/installers.cake +++ b/deployment/cake/installers.cake @@ -1,203 +1,203 @@ -// Customize this file when using a different issue tracker -#l "installers-innosetup.cake" -#l "installers-msix.cake" -#l "installers-squirrel.cake" - -using System.Diagnostics; - -//------------------------------------------------------------- - -public interface IInstaller -{ - bool IsAvailable { get; } - - Task PackageAsync(string projectName, string channel); - - Task GenerateDeploymentTargetAsync(string projectName); -} - -//------------------------------------------------------------- - -public class DeploymentCatalog -{ - public DeploymentCatalog() - { - Targets = new List(); - } - - public List Targets { get; private set; } -} - -//------------------------------------------------------------- - -public class DeploymentTarget -{ - public DeploymentTarget() - { - Groups = new List(); - } - - public string Name { get; set; } - - public List Groups { get; private set; } -} - -//------------------------------------------------------------- - -public class DeploymentGroup -{ - public DeploymentGroup() - { - Channels = new List(); - } - - public string Name { get; set; } - - public List Channels { get; private set; } -} - -//------------------------------------------------------------- - -public class DeploymentChannel -{ - public DeploymentChannel() - { - Releases = new List(); - } - - public string Name { get; set; } - - public List Releases { get; private set; } -} - -//------------------------------------------------------------- - -public class DeploymentRelease -{ - public string Name { get; set; } - - public DateTime? Timestamp { get; set;} - - public bool HasFull - { - get { return Full is not null; } - } - - public DeploymentReleasePart Full { get; set; } - - public bool HasDelta - { - get { return Delta is not null; } - } - - public DeploymentReleasePart Delta { get; set; } -} - -//------------------------------------------------------------- - -public class DeploymentReleasePart -{ - public string Hash { get; set; } - - public string RelativeFileName { get; set; } - - public ulong Size { get; set; } -} - -//------------------------------------------------------------- - -public class InstallerIntegration : IntegrationBase -{ - private readonly List _installers = new List(); - - public InstallerIntegration(BuildContext buildContext) - : base(buildContext) - { - _installers.Add(new InnoSetupInstaller(buildContext)); - _installers.Add(new MsixInstaller(buildContext)); - _installers.Add(new SquirrelInstaller(buildContext)); - } - - public string GetDeploymentChannelSuffix(string prefix = "_", string suffix = "") - { - var channelSuffix = string.Empty; - - if (BuildContext.Wpf.AppendDeploymentChannelSuffix) - { - if (BuildContext.General.IsAlphaBuild || - BuildContext.General.IsBetaBuild) - { - channelSuffix = $"{prefix}{BuildContext.Wpf.Channel}{suffix}"; - } - - BuildContext.CakeContext.Information($"Using deployment channel suffix '{channelSuffix}'"); - } - - return channelSuffix; - } - - public async Task PackageAsync(string projectName, string channel) - { - BuildContext.CakeContext.LogSeparator($"Packaging installer for '{projectName}'"); - - foreach (var installer in _installers) - { - if (!installer.IsAvailable) - { - continue; - } - - BuildContext.CakeContext.LogSeparator($"Applying installer '{installer.GetType().Name}' for '{projectName}'"); - - var stopwatch = Stopwatch.StartNew(); - - try - { - await installer.PackageAsync(projectName, channel); - } - finally - { - stopwatch.Stop(); - - BuildContext.CakeContext.Information($"Installer took {stopwatch.Elapsed}"); - } - } - - if (BuildContext.Wpf.GenerateDeploymentCatalog) - { - BuildContext.CakeContext.LogSeparator($"Generating deployment catalog for '{projectName}'"); - - var catalog = new DeploymentCatalog(); - - foreach (var installer in _installers) - { - if (!installer.IsAvailable) - { - continue; - } - - BuildContext.CakeContext.LogSeparator($"Generating deployment target for catalog for installer '{installer.GetType().Name}' for '{projectName}'"); - - var deploymentTarget = await installer.GenerateDeploymentTargetAsync(projectName); - if (deploymentTarget is not null) - { - catalog.Targets.Add(deploymentTarget); - } - } - - var localCatalogDirectory = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, "catalog", projectName); - BuildContext.CakeContext.CreateDirectory(localCatalogDirectory); - - var localCatalogFileName = System.IO.Path.Combine(localCatalogDirectory, "catalog.json"); - var json = Newtonsoft.Json.JsonConvert.SerializeObject(catalog); - - System.IO.File.WriteAllText(localCatalogFileName, json); - - if (BuildContext.Wpf.UpdateDeploymentsShare) - { - var targetFileName = System.IO.Path.Combine(BuildContext.Wpf.GetDeploymentShareForProject(projectName), "catalog.json"); - BuildContext.CakeContext.CopyFile(localCatalogFileName, targetFileName); - } - } - } +// Customize this file when using a different issue tracker +#l "installers-innosetup.cake" +#l "installers-msix.cake" +#l "installers-squirrel.cake" + +using System.Diagnostics; + +//------------------------------------------------------------- + +public interface IInstaller +{ + bool IsAvailable { get; } + + Task PackageAsync(string projectName, string channel); + + Task GenerateDeploymentTargetAsync(string projectName); +} + +//------------------------------------------------------------- + +public class DeploymentCatalog +{ + public DeploymentCatalog() + { + Targets = new List(); + } + + public List Targets { get; private set; } +} + +//------------------------------------------------------------- + +public class DeploymentTarget +{ + public DeploymentTarget() + { + Groups = new List(); + } + + public string Name { get; set; } + + public List Groups { get; private set; } +} + +//------------------------------------------------------------- + +public class DeploymentGroup +{ + public DeploymentGroup() + { + Channels = new List(); + } + + public string Name { get; set; } + + public List Channels { get; private set; } +} + +//------------------------------------------------------------- + +public class DeploymentChannel +{ + public DeploymentChannel() + { + Releases = new List(); + } + + public string Name { get; set; } + + public List Releases { get; private set; } +} + +//------------------------------------------------------------- + +public class DeploymentRelease +{ + public string Name { get; set; } + + public DateTime? Timestamp { get; set;} + + public bool HasFull + { + get { return Full is not null; } + } + + public DeploymentReleasePart Full { get; set; } + + public bool HasDelta + { + get { return Delta is not null; } + } + + public DeploymentReleasePart Delta { get; set; } +} + +//------------------------------------------------------------- + +public class DeploymentReleasePart +{ + public string Hash { get; set; } + + public string RelativeFileName { get; set; } + + public ulong Size { get; set; } +} + +//------------------------------------------------------------- + +public class InstallerIntegration : IntegrationBase +{ + private readonly List _installers = new List(); + + public InstallerIntegration(BuildContext buildContext) + : base(buildContext) + { + _installers.Add(new InnoSetupInstaller(buildContext)); + _installers.Add(new MsixInstaller(buildContext)); + _installers.Add(new SquirrelInstaller(buildContext)); + } + + public string GetDeploymentChannelSuffix(string prefix = "_", string suffix = "") + { + var channelSuffix = string.Empty; + + if (BuildContext.Wpf.AppendDeploymentChannelSuffix) + { + if (BuildContext.General.IsAlphaBuild || + BuildContext.General.IsBetaBuild) + { + channelSuffix = $"{prefix}{BuildContext.Wpf.Channel}{suffix}"; + } + + BuildContext.CakeContext.Information($"Using deployment channel suffix '{channelSuffix}'"); + } + + return channelSuffix; + } + + public async Task PackageAsync(string projectName, string channel) + { + BuildContext.CakeContext.LogSeparator($"Packaging installer for '{projectName}'"); + + foreach (var installer in _installers) + { + if (!installer.IsAvailable) + { + continue; + } + + BuildContext.CakeContext.LogSeparator($"Applying installer '{installer.GetType().Name}' for '{projectName}'"); + + var stopwatch = Stopwatch.StartNew(); + + try + { + await installer.PackageAsync(projectName, channel); + } + finally + { + stopwatch.Stop(); + + BuildContext.CakeContext.Information($"Installer took {stopwatch.Elapsed}"); + } + } + + if (BuildContext.Wpf.GenerateDeploymentCatalog) + { + BuildContext.CakeContext.LogSeparator($"Generating deployment catalog for '{projectName}'"); + + var catalog = new DeploymentCatalog(); + + foreach (var installer in _installers) + { + if (!installer.IsAvailable) + { + continue; + } + + BuildContext.CakeContext.LogSeparator($"Generating deployment target for catalog for installer '{installer.GetType().Name}' for '{projectName}'"); + + var deploymentTarget = await installer.GenerateDeploymentTargetAsync(projectName); + if (deploymentTarget is not null) + { + catalog.Targets.Add(deploymentTarget); + } + } + + var localCatalogDirectory = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, "catalog", projectName); + BuildContext.CakeContext.CreateDirectory(localCatalogDirectory); + + var localCatalogFileName = System.IO.Path.Combine(localCatalogDirectory, "catalog.json"); + var json = Newtonsoft.Json.JsonConvert.SerializeObject(catalog); + + System.IO.File.WriteAllText(localCatalogFileName, json); + + if (BuildContext.Wpf.UpdateDeploymentsShare) + { + var targetFileName = System.IO.Path.Combine(BuildContext.Wpf.GetDeploymentShareForProject(projectName), "catalog.json"); + BuildContext.CakeContext.CopyFile(localCatalogFileName, targetFileName); + } + } + } } \ No newline at end of file diff --git a/deployment/cake/issuetrackers-github.cake b/deployment/cake/issuetrackers-github.cake index d1bce9a2..fcefb366 100644 --- a/deployment/cake/issuetrackers-github.cake +++ b/deployment/cake/issuetrackers-github.cake @@ -1,91 +1,91 @@ -#tool "nuget:?package=gitreleasemanager&version=0.15.0" - -//------------------------------------------------------------- - -public class GitHubIssueTracker : IIssueTracker -{ - public GitHubIssueTracker(BuildContext buildContext) - { - BuildContext = buildContext; - - UserName = buildContext.BuildServer.GetVariable("GitHubUserName", showValue: true); - ApiKey = buildContext.BuildServer.GetVariable("GitHubApiKey", showValue: false); - OwnerName = buildContext.BuildServer.GetVariable("GitHubOwnerName", buildContext.General.Copyright.Company, showValue: true); - ProjectName = buildContext.BuildServer.GetVariable("GitHubProjectName", buildContext.General.Solution.Name, showValue: true); - - if (!string.IsNullOrWhiteSpace(UserName) && - !string.IsNullOrWhiteSpace(ApiKey) && - !string.IsNullOrWhiteSpace(OwnerName) && - !string.IsNullOrWhiteSpace(ProjectName)) - { - IsAvailable = true; - } - } - - public BuildContext BuildContext { get; private set; } - - public string UserName { get; set; } - public string ApiKey { get; set; } - public string OwnerName { get; set; } - public string ProjectName { get; set; } - - public string OwnerAndProjectName - { - get { return $"{OwnerName}/{ProjectName}"; } - } - - public bool IsAvailable { get; private set; } - - public async Task CreateAndReleaseVersionAsync() - { - if (!IsAvailable) - { - BuildContext.CakeContext.Information("GitHub is not available, skipping GitHub integration"); - return; - } - - var version = BuildContext.General.Version.FullSemVer; - - BuildContext.CakeContext.Information("Releasing version '{0}' in GitHub", version); - - // For docs, see https://cakebuild.net/dsl/gitreleasemanager/ - - BuildContext.CakeContext.Information("Step 1 / 4: Creating release"); - - BuildContext.CakeContext.GitReleaseManagerCreate(ApiKey, OwnerName, ProjectName, new GitReleaseManagerCreateSettings - { - TargetDirectory = BuildContext.General.RootDirectory, - Milestone = BuildContext.General.Version.MajorMinorPatch, - Name = version, - Prerelease = !BuildContext.General.IsOfficialBuild, - TargetCommitish = BuildContext.General.Repository.CommitId - }); - - BuildContext.CakeContext.Information("Step 2 / 4: Adding assets to the release (not supported yet)"); - - // Not yet supported - - if (!BuildContext.General.IsOfficialBuild) - { - BuildContext.CakeContext.Information("GitHub release publishing only runs against non-prerelease builds"); - } - else - { - BuildContext.CakeContext.Information("Step 3 / 4: Publishing release"); - - BuildContext.CakeContext.GitReleaseManagerPublish(ApiKey, OwnerName, ProjectName, BuildContext.General.Version.MajorMinorPatch, new GitReleaseManagerPublishSettings - { - TargetDirectory = BuildContext.General.RootDirectory - }); - - BuildContext.CakeContext.Information("Step 4 / 4: Closing the milestone"); - - BuildContext.CakeContext.GitReleaseManagerClose(ApiKey, OwnerName, ProjectName, BuildContext.General.Version.MajorMinorPatch, new GitReleaseManagerCloseMilestoneSettings - { - TargetDirectory = BuildContext.General.RootDirectory - }); - } - - BuildContext.CakeContext.Information("Released version in GitHub"); - } +#tool "nuget:?package=gitreleasemanager&version=0.15.0" + +//------------------------------------------------------------- + +public class GitHubIssueTracker : IIssueTracker +{ + public GitHubIssueTracker(BuildContext buildContext) + { + BuildContext = buildContext; + + UserName = buildContext.BuildServer.GetVariable("GitHubUserName", showValue: true); + ApiKey = buildContext.BuildServer.GetVariable("GitHubApiKey", showValue: false); + OwnerName = buildContext.BuildServer.GetVariable("GitHubOwnerName", buildContext.General.Copyright.Company, showValue: true); + ProjectName = buildContext.BuildServer.GetVariable("GitHubProjectName", buildContext.General.Solution.Name, showValue: true); + + if (!string.IsNullOrWhiteSpace(UserName) && + !string.IsNullOrWhiteSpace(ApiKey) && + !string.IsNullOrWhiteSpace(OwnerName) && + !string.IsNullOrWhiteSpace(ProjectName)) + { + IsAvailable = true; + } + } + + public BuildContext BuildContext { get; private set; } + + public string UserName { get; set; } + public string ApiKey { get; set; } + public string OwnerName { get; set; } + public string ProjectName { get; set; } + + public string OwnerAndProjectName + { + get { return $"{OwnerName}/{ProjectName}"; } + } + + public bool IsAvailable { get; private set; } + + public async Task CreateAndReleaseVersionAsync() + { + if (!IsAvailable) + { + BuildContext.CakeContext.Information("GitHub is not available, skipping GitHub integration"); + return; + } + + var version = BuildContext.General.Version.FullSemVer; + + BuildContext.CakeContext.Information("Releasing version '{0}' in GitHub", version); + + // For docs, see https://cakebuild.net/dsl/gitreleasemanager/ + + BuildContext.CakeContext.Information("Step 1 / 4: Creating release"); + + BuildContext.CakeContext.GitReleaseManagerCreate(ApiKey, OwnerName, ProjectName, new GitReleaseManagerCreateSettings + { + TargetDirectory = BuildContext.General.RootDirectory, + Milestone = BuildContext.General.Version.MajorMinorPatch, + Name = version, + Prerelease = !BuildContext.General.IsOfficialBuild, + TargetCommitish = BuildContext.General.Repository.CommitId + }); + + BuildContext.CakeContext.Information("Step 2 / 4: Adding assets to the release (not supported yet)"); + + // Not yet supported + + if (!BuildContext.General.IsOfficialBuild) + { + BuildContext.CakeContext.Information("GitHub release publishing only runs against non-prerelease builds"); + } + else + { + BuildContext.CakeContext.Information("Step 3 / 4: Publishing release"); + + BuildContext.CakeContext.GitReleaseManagerPublish(ApiKey, OwnerName, ProjectName, BuildContext.General.Version.MajorMinorPatch, new GitReleaseManagerPublishSettings + { + TargetDirectory = BuildContext.General.RootDirectory + }); + + BuildContext.CakeContext.Information("Step 4 / 4: Closing the milestone"); + + BuildContext.CakeContext.GitReleaseManagerClose(ApiKey, OwnerName, ProjectName, BuildContext.General.Version.MajorMinorPatch, new GitReleaseManagerCloseMilestoneSettings + { + TargetDirectory = BuildContext.General.RootDirectory + }); + } + + BuildContext.CakeContext.Information("Released version in GitHub"); + } } \ No newline at end of file diff --git a/deployment/cake/issuetrackers-jira.cake b/deployment/cake/issuetrackers-jira.cake index d467d9ad..9177eb4a 100644 --- a/deployment/cake/issuetrackers-jira.cake +++ b/deployment/cake/issuetrackers-jira.cake @@ -1,62 +1,62 @@ -#tool "nuget:?package=JiraCli&version=1.3.0-alpha0338&prerelease" - -//------------------------------------------------------------- - -public class JiraIssueTracker : IIssueTracker -{ - public JiraIssueTracker(BuildContext buildContext) - { - BuildContext = buildContext; - - Url = buildContext.BuildServer.GetVariable("JiraUrl", showValue: true); - Username = buildContext.BuildServer.GetVariable("JiraUsername", showValue: true); - Password = buildContext.BuildServer.GetVariable("JiraPassword", showValue: false); - ProjectName = buildContext.BuildServer.GetVariable("JiraProjectName", showValue: true); - - if (!string.IsNullOrWhiteSpace(Url) && - !string.IsNullOrWhiteSpace(ProjectName)) - { - IsAvailable = true; - } - } - - public BuildContext BuildContext { get; private set; } - - public string Url { get; set; } - public string Username { get; set; } - public string Password { get; set; } - public string ProjectName { get; set; } - public bool IsAvailable { get; private set; } - - public async Task CreateAndReleaseVersionAsync() - { - if (!IsAvailable) - { - BuildContext.CakeContext.Information("JIRA is not available, skipping JIRA integration"); - return; - } - - var version = BuildContext.General.Version.FullSemVer; - - BuildContext.CakeContext.Information("Releasing version '{0}' in JIRA", version); - - // Example call: - // JiraCli.exe -url %JiraUrl% -user %JiraUsername% -pw %JiraPassword% -action createandreleaseversion - // -project %JiraProjectName% -version %GitVersion_FullSemVer% -merge %IsOfficialBuild% - - var nugetPath = BuildContext.CakeContext.Tools.Resolve("JiraCli.exe"); - BuildContext.CakeContext.StartProcess(nugetPath, new ProcessSettings - { - Arguments = new ProcessArgumentBuilder() - .AppendSwitch("-url", Url) - .AppendSwitch("-user", Username) - .AppendSwitchSecret("-pw", Password) - .AppendSwitch("-action", "createandreleaseversion") - .AppendSwitch("-project", ProjectName) - .AppendSwitch("-version", version) - .AppendSwitch("-merge", BuildContext.General.IsOfficialBuild.ToString()) - }); - - BuildContext.CakeContext.Information("Released version in JIRA"); - } +#tool "nuget:?package=JiraCli&version=1.3.0-alpha0338&prerelease" + +//------------------------------------------------------------- + +public class JiraIssueTracker : IIssueTracker +{ + public JiraIssueTracker(BuildContext buildContext) + { + BuildContext = buildContext; + + Url = buildContext.BuildServer.GetVariable("JiraUrl", showValue: true); + Username = buildContext.BuildServer.GetVariable("JiraUsername", showValue: true); + Password = buildContext.BuildServer.GetVariable("JiraPassword", showValue: false); + ProjectName = buildContext.BuildServer.GetVariable("JiraProjectName", showValue: true); + + if (!string.IsNullOrWhiteSpace(Url) && + !string.IsNullOrWhiteSpace(ProjectName)) + { + IsAvailable = true; + } + } + + public BuildContext BuildContext { get; private set; } + + public string Url { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string ProjectName { get; set; } + public bool IsAvailable { get; private set; } + + public async Task CreateAndReleaseVersionAsync() + { + if (!IsAvailable) + { + BuildContext.CakeContext.Information("JIRA is not available, skipping JIRA integration"); + return; + } + + var version = BuildContext.General.Version.FullSemVer; + + BuildContext.CakeContext.Information("Releasing version '{0}' in JIRA", version); + + // Example call: + // JiraCli.exe -url %JiraUrl% -user %JiraUsername% -pw %JiraPassword% -action createandreleaseversion + // -project %JiraProjectName% -version %GitVersion_FullSemVer% -merge %IsOfficialBuild% + + var nugetPath = BuildContext.CakeContext.Tools.Resolve("JiraCli.exe"); + BuildContext.CakeContext.StartProcess(nugetPath, new ProcessSettings + { + Arguments = new ProcessArgumentBuilder() + .AppendSwitch("-url", Url) + .AppendSwitch("-user", Username) + .AppendSwitchSecret("-pw", Password) + .AppendSwitch("-action", "createandreleaseversion") + .AppendSwitch("-project", ProjectName) + .AppendSwitch("-version", version) + .AppendSwitch("-merge", BuildContext.General.IsOfficialBuild.ToString()) + }); + + BuildContext.CakeContext.Information("Released version in JIRA"); + } } \ No newline at end of file diff --git a/deployment/cake/issuetrackers.cake b/deployment/cake/issuetrackers.cake index 2a2f9c16..8c384163 100644 --- a/deployment/cake/issuetrackers.cake +++ b/deployment/cake/issuetrackers.cake @@ -1,41 +1,41 @@ -// Customize this file when using a different issue tracker -#l "issuetrackers-github.cake" -#l "issuetrackers-jira.cake" - -//------------------------------------------------------------- - -public interface IIssueTracker -{ - Task CreateAndReleaseVersionAsync(); -} - -//------------------------------------------------------------- - -public class IssueTrackerIntegration : IntegrationBase -{ - private readonly List _issueTrackers = new List(); - - public IssueTrackerIntegration(BuildContext buildContext) - : base(buildContext) - { - _issueTrackers.Add(new GitHubIssueTracker(buildContext)); - _issueTrackers.Add(new JiraIssueTracker(buildContext)); - } - - public async Task CreateAndReleaseVersionAsync() - { - BuildContext.CakeContext.LogSeparator("Creating and releasing version"); - - foreach (var issueTracker in _issueTrackers) - { - try - { - await issueTracker.CreateAndReleaseVersionAsync(); - } - catch (Exception ex) - { - BuildContext.CakeContext.Warning(ex.Message); - } - } - } +// Customize this file when using a different issue tracker +#l "issuetrackers-github.cake" +#l "issuetrackers-jira.cake" + +//------------------------------------------------------------- + +public interface IIssueTracker +{ + Task CreateAndReleaseVersionAsync(); +} + +//------------------------------------------------------------- + +public class IssueTrackerIntegration : IntegrationBase +{ + private readonly List _issueTrackers = new List(); + + public IssueTrackerIntegration(BuildContext buildContext) + : base(buildContext) + { + _issueTrackers.Add(new GitHubIssueTracker(buildContext)); + _issueTrackers.Add(new JiraIssueTracker(buildContext)); + } + + public async Task CreateAndReleaseVersionAsync() + { + BuildContext.CakeContext.LogSeparator("Creating and releasing version"); + + foreach (var issueTracker in _issueTrackers) + { + try + { + await issueTracker.CreateAndReleaseVersionAsync(); + } + catch (Exception ex) + { + BuildContext.CakeContext.Warning(ex.Message); + } + } + } } \ No newline at end of file diff --git a/deployment/cake/lib-generic.cake b/deployment/cake/lib-generic.cake index e176d060..d0a5fc98 100644 --- a/deployment/cake/lib-generic.cake +++ b/deployment/cake/lib-generic.cake @@ -1,795 +1,795 @@ -using System.Reflection; - -//------------------------------------------------------------- - -private static readonly Dictionary _dotNetCoreCache = new Dictionary(); -private static readonly Dictionary _blazorCache = new Dictionary(); - -//------------------------------------------------------------- - -public interface IIntegration -{ - -} - -//------------------------------------------------------------- - -public abstract class IntegrationBase : IIntegration -{ - protected IntegrationBase(BuildContext buildContext) - { - BuildContext = buildContext; - } - - public BuildContext BuildContext { get; private set; } -} - -//------------------------------------------------------------- - -public interface IProcessor -{ - bool HasItems(); - - Task PrepareAsync(); - Task UpdateInfoAsync(); - Task BuildAsync(); - Task PackageAsync(); - Task DeployAsync(); - Task FinalizeAsync(); -} - -//------------------------------------------------------------- - -public abstract class ProcessorBase : IProcessor -{ - protected readonly BuildContext BuildContext; - protected readonly ICakeContext CakeContext; - - protected ProcessorBase(BuildContext buildContext) - { - BuildContext = buildContext; - CakeContext = buildContext.CakeContext; - - Name = GetProcessorName(); - } - - public string Name { get; private set; } - - protected virtual string GetProcessorName() - { - var name = GetType().Name.Replace("Processor", string.Empty); - return name; - } - - public abstract bool HasItems(); - - public abstract Task PrepareAsync(); - public abstract Task UpdateInfoAsync(); - public abstract Task BuildAsync(); - public abstract Task PackageAsync(); - public abstract Task DeployAsync(); - public abstract Task FinalizeAsync(); -} - -//------------------------------------------------------------- - -public interface IBuildContext -{ - ICakeContext CakeContext { get; } - IBuildContext ParentContext { get; } - - void Validate(); - void LogStateInfo(); -} - -//------------------------------------------------------------- - -public abstract class BuildContextBase : IBuildContext -{ - private List _childContexts; - private readonly string _contextName; - - protected BuildContextBase(ICakeContext cakeContext) - { - CakeContext = cakeContext; - - _contextName = GetContextName(); - } - - protected BuildContextBase(IBuildContext parentContext) - : this(parentContext.CakeContext) - { - ParentContext = parentContext; - } - - public ICakeContext CakeContext { get; private set; } - - public IBuildContext ParentContext { get; private set; } - - private List GetChildContexts() - { - var items = _childContexts; - if (items is null) - { - items = new List(); - - var properties = GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public); - - foreach (var property in properties) - { - //if (property.Name.EndsWith("Context")) - if (property.PropertyType.GetInterfaces().Any(x => x == (typeof(IBuildContext)))) - { - items.Add((IBuildContext)property.GetValue(this, null)); - } - } - - _childContexts = items; - - CakeContext.Debug($"Found '{items.Count}' child contexts for '{_contextName}' context"); - } - - return items; - } - - protected virtual string GetContextName() - { - var name = GetType().Name.Replace("Context", string.Empty); - return name; - } - - public void Validate() - { - CakeContext.Information($"Validating '{_contextName}' context"); - - ValidateContext(); - - foreach (var childContext in GetChildContexts()) - { - childContext.Validate(); - } - } - - protected abstract void ValidateContext(); - - public void LogStateInfo() - { - LogStateInfoForContext(); - - foreach (var childContext in GetChildContexts()) - { - childContext.LogStateInfo(); - } - } - - protected abstract void LogStateInfoForContext(); -} - -//------------------------------------------------------------- - -public abstract class BuildContextWithItemsBase : BuildContextBase -{ - protected BuildContextWithItemsBase(ICakeContext cakeContext) - : base(cakeContext) - { - } - - protected BuildContextWithItemsBase(IBuildContext parentContext) - : base(parentContext) - { - } - - public List Items { get; set; } -} - -//------------------------------------------------------------- - -public enum TargetType -{ - Unknown, - - Component, - - DockerImage, - - GitHubPages, - - Tool, - - UwpApp, - - VsExtension, - - WebApp, - - WpfApp -} - -//------------------------------------------------------------- - -private static void LogSeparator(this ICakeContext cakeContext, string messageFormat, params object[] args) -{ - cakeContext.Information(""); - cakeContext.Information("--------------------------------------------------------------------------------"); - cakeContext.Information(messageFormat, args); - cakeContext.Information("--------------------------------------------------------------------------------"); - cakeContext.Information(""); -} - -//------------------------------------------------------------- - -private static void LogSeparator(this ICakeContext cakeContext) -{ - cakeContext.Information(""); - cakeContext.Information("--------------------------------------------------------------------------------"); - cakeContext.Information(""); -} - -//------------------------------------------------------------- - -private static string GetTempDirectory(BuildContext buildContext, string section, string projectName) -{ - var tempDirectory = buildContext.CakeContext.Directory(string.Format("./temp/{0}/{1}", section, projectName)); - - buildContext.CakeContext.CreateDirectory(tempDirectory); - - return tempDirectory; -} - -//------------------------------------------------------------- - -private static List SplitCommaSeparatedList(string value) -{ - return SplitSeparatedList(value, ','); -} - -//------------------------------------------------------------- - -private static List SplitSeparatedList(string value, params char[] separators) -{ - var list = new List(); - - if (!string.IsNullOrWhiteSpace(value)) - { - var splitted = value.Split(separators, StringSplitOptions.RemoveEmptyEntries); - - foreach (var split in splitted) - { - list.Add(split.Trim()); - } - } - - return list; -} - -//------------------------------------------------------------- - -private static string GetProjectDirectory(string projectName) -{ - var projectDirectory = System.IO.Path.Combine(".", "src", projectName); - return projectDirectory; -} - -//------------------------------------------------------------- - -private static string GetProjectOutputDirectory(BuildContext buildContext, string projectName) -{ - var projectDirectory = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, projectName); - return projectDirectory; -} - -//------------------------------------------------------------- - -private static string GetProjectFileName(BuildContext buildContext, string projectName) -{ - var allowedExtensions = new [] - { - "csproj", - "vcxproj" - }; - - foreach (var allowedExtension in allowedExtensions) - { - var fileName = System.IO.Path.Combine(GetProjectDirectory(projectName), $"{projectName}.{allowedExtension}"); - - //buildContext.CakeContext.Information(fileName); - - if (buildContext.CakeContext.FileExists(fileName)) - { - return fileName; - } - } - - // Old behavior - var fallbackFileName = System.IO.Path.Combine(GetProjectDirectory(projectName), $"{projectName}.{allowedExtensions[0]}"); - return fallbackFileName; -} - -//------------------------------------------------------------- - -private static string GetProjectSlug(string projectName, string replacement = "") -{ - var slug = projectName.Replace(".", replacement).Replace(" ", replacement); - return slug; -} - -//------------------------------------------------------------- - -private static string[] GetTargetFrameworks(BuildContext buildContext, string projectName) -{ - var targetFrameworks = new List(); - - var projectFileName = GetProjectFileName(buildContext, projectName); - var projectFileContents = System.IO.File.ReadAllText(projectFileName); - - var xmlDocument = XDocument.Parse(projectFileContents); - var projectElement = xmlDocument.Root; - - foreach (var propertyGroupElement in projectElement.Elements("PropertyGroup")) - { - // Step 1: check TargetFramework - var targetFrameworkElement = projectElement.Element("TargetFramework"); - if (targetFrameworkElement != null) - { - targetFrameworks.Add(targetFrameworkElement.Value); - break; - } - - // Step 2: check TargetFrameworks - var targetFrameworksElement = propertyGroupElement.Element("TargetFrameworks"); - if (targetFrameworksElement != null) - { - var value = targetFrameworksElement.Value; - targetFrameworks.AddRange(value.Split(new [] { ';' })); - break; - } - } - - if (targetFrameworks.Count == 0) - { - throw new Exception(string.Format("No target frameworks could be detected for project '{0}'", projectName)); - } - - return targetFrameworks.ToArray(); -} - -//------------------------------------------------------------- - -private static string GetTargetSpecificConfigurationValue(BuildContext buildContext, TargetType targetType, string configurationPrefix, string fallbackValue) -{ - // Allow per project overrides via "[configurationPrefix][targetType]" - var keyToCheck = string.Format("{0}{1}", configurationPrefix, targetType); - - var value = buildContext.BuildServer.GetVariable(keyToCheck, fallbackValue); - return value; -} - -//------------------------------------------------------------- - -private static string GetProjectSpecificConfigurationValue(BuildContext buildContext, string projectName, string configurationPrefix, string fallbackValue) -{ - // Allow per project overrides via "[configurationPrefix][projectName]" - var slug = GetProjectSlug(projectName); - var keyToCheck = string.Format("{0}{1}", configurationPrefix, slug); - - var value = buildContext.BuildServer.GetVariable(keyToCheck, fallbackValue); - return value; -} - -//------------------------------------------------------------- - -private static void CleanProject(BuildContext buildContext, string projectName) -{ - buildContext.CakeContext.LogSeparator("Cleaning project '{0}'", projectName); - - var projectDirectory = GetProjectDirectory(projectName); - - buildContext.CakeContext.Information($"Investigating paths to clean up in '{projectDirectory}'"); - - var directoriesToDelete = new List(); - - var binDirectory = System.IO.Path.Combine(projectDirectory, "bin"); - directoriesToDelete.Add(binDirectory); - - var objDirectory = System.IO.Path.Combine(projectDirectory, "obj"); - directoriesToDelete.Add(objDirectory); - - // Special C++ scenarios - var projectFileName = GetProjectFileName(buildContext, projectName); - if (IsCppProject(projectFileName)) - { - var debugDirectory = System.IO.Path.Combine(projectDirectory, "Debug"); - directoriesToDelete.Add(debugDirectory); - - var releaseDirectory = System.IO.Path.Combine(projectDirectory, "Release"); - directoriesToDelete.Add(releaseDirectory); - - var x64Directory = System.IO.Path.Combine(projectDirectory, "x64"); - directoriesToDelete.Add(x64Directory); - - var x86Directory = System.IO.Path.Combine(projectDirectory, "x86"); - directoriesToDelete.Add(x86Directory); - } - - foreach (var directoryToDelete in directoriesToDelete) - { - DeleteDirectoryWithLogging(buildContext, directoryToDelete); - } -} - -//------------------------------------------------------------- - -private static void DeleteDirectoryWithLogging(BuildContext buildContext, string directoryToDelete) -{ - if (buildContext.CakeContext.DirectoryExists(directoryToDelete)) - { - buildContext.CakeContext.Information($"Cleaning up directory '{directoryToDelete}'"); - - buildContext.CakeContext.DeleteDirectory(directoryToDelete, new DeleteDirectorySettings - { - Force = true, - Recursive = true - }); - } -} - -//------------------------------------------------------------- - -private static bool IsCppProject(string projectName) -{ - return projectName.EndsWith(".vcxproj"); -} - -//------------------------------------------------------------- - -private static bool IsBlazorProject(BuildContext buildContext, string projectName) -{ - var projectFileName = GetProjectFileName(buildContext, projectName); - - if (!_blazorCache.TryGetValue(projectFileName, out var isBlazor)) - { - isBlazor = false; - - var lines = System.IO.File.ReadAllLines(projectFileName); - foreach (var line in lines) - { - // Match both *TargetFramework* and *TargetFrameworks* - var lowerCase = line.ToLower(); - if (lowerCase.Contains(" Excludes - var includes = buildContext.General.Includes; - if (includes.Count > 0) - { - var process = includes.Any(x => string.Equals(x, projectName, StringComparison.OrdinalIgnoreCase)); - - if (!process) - { - buildContext.CakeContext.Warning("Project '{0}' should not be processed, removing from projects to process", projectName); - } - - return process; - } - - var excludes = buildContext.General.Excludes; - if (excludes.Count > 0) - { - var process = !excludes.Any(x => string.Equals(x, projectName, StringComparison.OrdinalIgnoreCase)); - - if (!process) - { - buildContext.CakeContext.Warning("Project '{0}' should not be processed, removing from projects to process", projectName); - } - - return process; - } - - // Is this a known project? - if (!buildContext.RegisteredProjects.Any(x => string.Equals(projectName, x, StringComparison.OrdinalIgnoreCase))) - { - buildContext.CakeContext.Warning("Project '{0}' should not be processed, does not exist as registered project", projectName); - return false; - } - - if (buildContext.General.IsCiBuild) - { - // In CI builds, we always want to include all projects - return true; - } - - // Experimental mode where we ignore projects that are not on the deploy list when not in CI mode, but - // it can only work if they are not part of unit tests (but that should never happen) - // if (buildContext.Tests.Items.Count == 0) - // { - if (checkDeployment && - !ShouldBuildProject(buildContext, projectName) && - !ShouldPackageProject(buildContext, projectName) && - !ShouldDeployProject(buildContext, projectName)) - { - buildContext.CakeContext.Warning("Project '{0}' should not be processed because this is not a CI build, does not contain tests and the project should not be built, packaged or deployed, removing from projects to process", projectName); - return false; - } - //} - - return true; -} - -//------------------------------------------------------------- - -private static List GetProjectRuntimesIdentifiers(BuildContext buildContext, Cake.Core.IO.FilePath solutionOrProjectFileName, List runtimeIdentifiersToInvestigate) -{ - var projectFileContents = System.IO.File.ReadAllText(solutionOrProjectFileName.FullPath)?.ToLower(); - - var supportedRuntimeIdentifiers = new List(); - - foreach (var runtimeIdentifier in runtimeIdentifiersToInvestigate) - { - if (!string.IsNullOrWhiteSpace(runtimeIdentifier)) - { - if (!projectFileContents.Contains(runtimeIdentifier.ToLower())) - { - buildContext.CakeContext.Information("Project '{0}' does not support runtime identifier '{1}', removing from supported runtime identifiers list", solutionOrProjectFileName, runtimeIdentifier); - continue; - } - } - - supportedRuntimeIdentifiers.Add(runtimeIdentifier); - } - - if (supportedRuntimeIdentifiers.Count == 0) - { - buildContext.CakeContext.Information("Project '{0}' does not have any explicit runtime identifiers left, adding empty one as default", solutionOrProjectFileName); - - // Default - supportedRuntimeIdentifiers.Add(string.Empty); - } - - return supportedRuntimeIdentifiers; -} - -//------------------------------------------------------------- - -private static bool ShouldBuildProject(BuildContext buildContext, string projectName) -{ - // Allow the build server to configure this via "Build[ProjectName]" - var slug = GetProjectSlug(projectName); - var keyToCheck = string.Format("Build{0}", slug); - - // No need to build if we don't package - var shouldBuild = ShouldPackageProject(buildContext, projectName); - - // By default, everything should be built. This feature is to explicitly not include - // a project in the build when a solution contains multiple projects / components that - // need to be built / packaged / deployed separately - // - // The default value is "ShouldPackageProject" since we assume it does not need - // to be built if it's not supposed to be packaged - shouldBuild = buildContext.BuildServer.GetVariableAsBool(keyToCheck, shouldBuild); - - buildContext.CakeContext.Information($"Value for '{keyToCheck}': {shouldBuild}"); - - return shouldBuild; -} - -//------------------------------------------------------------- - -private static bool ShouldPackageProject(BuildContext buildContext, string projectName) -{ - // Allow the build server to configure this via "Package[ProjectName]" - var slug = GetProjectSlug(projectName); - var keyToCheck = string.Format("Package{0}", slug); - - // No need to package if we don't deploy - var shouldPackage = ShouldDeployProject(buildContext, projectName); - - // The default value is "ShouldDeployProject" since we assume it does not need - // to be packaged if it's not supposed to be deployed - shouldPackage = buildContext.BuildServer.GetVariableAsBool(keyToCheck, shouldPackage); - - // If this is *only* a dependency, it should never be deployed - if (IsOnlyDependencyProject(buildContext, projectName)) - { - shouldPackage = false; - } - - if (shouldPackage && !ShouldProcessProject(buildContext, projectName, false)) - { - buildContext.CakeContext.Information($"Project '{projectName}' should not be processed, excluding it anyway"); - - shouldPackage = false; - } - - buildContext.CakeContext.Information($"Value for '{keyToCheck}': {shouldPackage}"); - - return shouldPackage; -} - -//------------------------------------------------------------- - -private static bool ShouldDeployProject(BuildContext buildContext, string projectName) -{ - // Allow the build server to configure this via "Deploy[ProjectName]" - var slug = GetProjectSlug(projectName); - var keyToCheck = string.Format("Deploy{0}", slug); - - // By default, deploy - var shouldDeploy = buildContext.BuildServer.GetVariableAsBool(keyToCheck, true); - - // If this is *only* a dependency, it should never be deployed - if (IsOnlyDependencyProject(buildContext, projectName)) - { - shouldDeploy = false; - } - - if (shouldDeploy && !ShouldProcessProject(buildContext, projectName, false)) - { - buildContext.CakeContext.Information($"Project '{projectName}' should not be processed, excluding it anyway"); - - shouldDeploy = false; - } - - buildContext.CakeContext.Information($"Value for '{keyToCheck}': {shouldDeploy}"); - - return shouldDeploy; -} - -//------------------------------------------------------------- - -private static bool IsOnlyDependencyProject(BuildContext buildContext, string projectName) -{ - buildContext.CakeContext.Information($"Checking if project '{projectName}' is a dependency only"); - - // If not in the dependencies list, we can stop checking - if (!buildContext.Dependencies.Items.Contains(projectName)) - { - buildContext.CakeContext.Information($"Project is not in list of dependencies, assuming not dependency only"); - return false; - } - - if (buildContext.Components.Items.Contains(projectName)) - { - buildContext.CakeContext.Information($"Project is list of components, assuming not dependency only"); - return false; - } - - if (buildContext.DockerImages.Items.Contains(projectName)) - { - buildContext.CakeContext.Information($"Project is list of docker images, assuming not dependency only"); - return false; - } - - if (buildContext.GitHubPages.Items.Contains(projectName)) - { - buildContext.CakeContext.Information($"Project is list of GitHub pages, assuming not dependency only"); - return false; - } - - if (buildContext.Templates.Items.Contains(projectName)) - { - buildContext.CakeContext.Information($"Project is list of templates, assuming not dependency only"); - return false; - } - - if (buildContext.Tools.Items.Contains(projectName)) - { - buildContext.CakeContext.Information($"Project is list of tools, assuming not dependency only"); - return false; - } - - if (buildContext.Uwp.Items.Contains(projectName)) - { - buildContext.CakeContext.Information($"Project is list of UWP apps, assuming not dependency only"); - return false; - } - - if (buildContext.VsExtensions.Items.Contains(projectName)) - { - buildContext.CakeContext.Information($"Project is list of VS extensions, assuming not dependency only"); - return false; - } - - if (buildContext.Web.Items.Contains(projectName)) - { - buildContext.CakeContext.Information($"Project is list of web apps, assuming not dependency only"); - return false; - } - - if (buildContext.Wpf.Items.Contains(projectName)) - { - buildContext.CakeContext.Information($"Project is list of WPF apps, assuming not dependency only"); - return false; - } - - buildContext.CakeContext.Information($"Project '{projectName}' is a dependency only"); - - // It's in the dependencies list and not in any other list - return true; -} - -//------------------------------------------------------------- - -public static void Add(this Dictionary> dictionary, string project, params string[] projects) -{ - dictionary.Add(project, new List(projects)); +using System.Reflection; + +//------------------------------------------------------------- + +private static readonly Dictionary _dotNetCoreCache = new Dictionary(); +private static readonly Dictionary _blazorCache = new Dictionary(); + +//------------------------------------------------------------- + +public interface IIntegration +{ + +} + +//------------------------------------------------------------- + +public abstract class IntegrationBase : IIntegration +{ + protected IntegrationBase(BuildContext buildContext) + { + BuildContext = buildContext; + } + + public BuildContext BuildContext { get; private set; } +} + +//------------------------------------------------------------- + +public interface IProcessor +{ + bool HasItems(); + + Task PrepareAsync(); + Task UpdateInfoAsync(); + Task BuildAsync(); + Task PackageAsync(); + Task DeployAsync(); + Task FinalizeAsync(); +} + +//------------------------------------------------------------- + +public abstract class ProcessorBase : IProcessor +{ + protected readonly BuildContext BuildContext; + protected readonly ICakeContext CakeContext; + + protected ProcessorBase(BuildContext buildContext) + { + BuildContext = buildContext; + CakeContext = buildContext.CakeContext; + + Name = GetProcessorName(); + } + + public string Name { get; private set; } + + protected virtual string GetProcessorName() + { + var name = GetType().Name.Replace("Processor", string.Empty); + return name; + } + + public abstract bool HasItems(); + + public abstract Task PrepareAsync(); + public abstract Task UpdateInfoAsync(); + public abstract Task BuildAsync(); + public abstract Task PackageAsync(); + public abstract Task DeployAsync(); + public abstract Task FinalizeAsync(); +} + +//------------------------------------------------------------- + +public interface IBuildContext +{ + ICakeContext CakeContext { get; } + IBuildContext ParentContext { get; } + + void Validate(); + void LogStateInfo(); +} + +//------------------------------------------------------------- + +public abstract class BuildContextBase : IBuildContext +{ + private List _childContexts; + private readonly string _contextName; + + protected BuildContextBase(ICakeContext cakeContext) + { + CakeContext = cakeContext; + + _contextName = GetContextName(); + } + + protected BuildContextBase(IBuildContext parentContext) + : this(parentContext.CakeContext) + { + ParentContext = parentContext; + } + + public ICakeContext CakeContext { get; private set; } + + public IBuildContext ParentContext { get; private set; } + + private List GetChildContexts() + { + var items = _childContexts; + if (items is null) + { + items = new List(); + + var properties = GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public); + + foreach (var property in properties) + { + //if (property.Name.EndsWith("Context")) + if (property.PropertyType.GetInterfaces().Any(x => x == (typeof(IBuildContext)))) + { + items.Add((IBuildContext)property.GetValue(this, null)); + } + } + + _childContexts = items; + + CakeContext.Debug($"Found '{items.Count}' child contexts for '{_contextName}' context"); + } + + return items; + } + + protected virtual string GetContextName() + { + var name = GetType().Name.Replace("Context", string.Empty); + return name; + } + + public void Validate() + { + CakeContext.Information($"Validating '{_contextName}' context"); + + ValidateContext(); + + foreach (var childContext in GetChildContexts()) + { + childContext.Validate(); + } + } + + protected abstract void ValidateContext(); + + public void LogStateInfo() + { + LogStateInfoForContext(); + + foreach (var childContext in GetChildContexts()) + { + childContext.LogStateInfo(); + } + } + + protected abstract void LogStateInfoForContext(); +} + +//------------------------------------------------------------- + +public abstract class BuildContextWithItemsBase : BuildContextBase +{ + protected BuildContextWithItemsBase(ICakeContext cakeContext) + : base(cakeContext) + { + } + + protected BuildContextWithItemsBase(IBuildContext parentContext) + : base(parentContext) + { + } + + public List Items { get; set; } +} + +//------------------------------------------------------------- + +public enum TargetType +{ + Unknown, + + Component, + + DockerImage, + + GitHubPages, + + Tool, + + UwpApp, + + VsExtension, + + WebApp, + + WpfApp +} + +//------------------------------------------------------------- + +private static void LogSeparator(this ICakeContext cakeContext, string messageFormat, params object[] args) +{ + cakeContext.Information(""); + cakeContext.Information("--------------------------------------------------------------------------------"); + cakeContext.Information(messageFormat, args); + cakeContext.Information("--------------------------------------------------------------------------------"); + cakeContext.Information(""); +} + +//------------------------------------------------------------- + +private static void LogSeparator(this ICakeContext cakeContext) +{ + cakeContext.Information(""); + cakeContext.Information("--------------------------------------------------------------------------------"); + cakeContext.Information(""); +} + +//------------------------------------------------------------- + +private static string GetTempDirectory(BuildContext buildContext, string section, string projectName) +{ + var tempDirectory = buildContext.CakeContext.Directory(string.Format("./temp/{0}/{1}", section, projectName)); + + buildContext.CakeContext.CreateDirectory(tempDirectory); + + return tempDirectory; +} + +//------------------------------------------------------------- + +private static List SplitCommaSeparatedList(string value) +{ + return SplitSeparatedList(value, ','); +} + +//------------------------------------------------------------- + +private static List SplitSeparatedList(string value, params char[] separators) +{ + var list = new List(); + + if (!string.IsNullOrWhiteSpace(value)) + { + var splitted = value.Split(separators, StringSplitOptions.RemoveEmptyEntries); + + foreach (var split in splitted) + { + list.Add(split.Trim()); + } + } + + return list; +} + +//------------------------------------------------------------- + +private static string GetProjectDirectory(string projectName) +{ + var projectDirectory = System.IO.Path.Combine(".", "src", projectName); + return projectDirectory; +} + +//------------------------------------------------------------- + +private static string GetProjectOutputDirectory(BuildContext buildContext, string projectName) +{ + var projectDirectory = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, projectName); + return projectDirectory; +} + +//------------------------------------------------------------- + +private static string GetProjectFileName(BuildContext buildContext, string projectName) +{ + var allowedExtensions = new [] + { + "csproj", + "vcxproj" + }; + + foreach (var allowedExtension in allowedExtensions) + { + var fileName = System.IO.Path.Combine(GetProjectDirectory(projectName), $"{projectName}.{allowedExtension}"); + + //buildContext.CakeContext.Information(fileName); + + if (buildContext.CakeContext.FileExists(fileName)) + { + return fileName; + } + } + + // Old behavior + var fallbackFileName = System.IO.Path.Combine(GetProjectDirectory(projectName), $"{projectName}.{allowedExtensions[0]}"); + return fallbackFileName; +} + +//------------------------------------------------------------- + +private static string GetProjectSlug(string projectName, string replacement = "") +{ + var slug = projectName.Replace(".", replacement).Replace(" ", replacement); + return slug; +} + +//------------------------------------------------------------- + +private static string[] GetTargetFrameworks(BuildContext buildContext, string projectName) +{ + var targetFrameworks = new List(); + + var projectFileName = GetProjectFileName(buildContext, projectName); + var projectFileContents = System.IO.File.ReadAllText(projectFileName); + + var xmlDocument = XDocument.Parse(projectFileContents); + var projectElement = xmlDocument.Root; + + foreach (var propertyGroupElement in projectElement.Elements("PropertyGroup")) + { + // Step 1: check TargetFramework + var targetFrameworkElement = projectElement.Element("TargetFramework"); + if (targetFrameworkElement != null) + { + targetFrameworks.Add(targetFrameworkElement.Value); + break; + } + + // Step 2: check TargetFrameworks + var targetFrameworksElement = propertyGroupElement.Element("TargetFrameworks"); + if (targetFrameworksElement != null) + { + var value = targetFrameworksElement.Value; + targetFrameworks.AddRange(value.Split(new [] { ';' })); + break; + } + } + + if (targetFrameworks.Count == 0) + { + throw new Exception(string.Format("No target frameworks could be detected for project '{0}'", projectName)); + } + + return targetFrameworks.ToArray(); +} + +//------------------------------------------------------------- + +private static string GetTargetSpecificConfigurationValue(BuildContext buildContext, TargetType targetType, string configurationPrefix, string fallbackValue) +{ + // Allow per project overrides via "[configurationPrefix][targetType]" + var keyToCheck = string.Format("{0}{1}", configurationPrefix, targetType); + + var value = buildContext.BuildServer.GetVariable(keyToCheck, fallbackValue); + return value; +} + +//------------------------------------------------------------- + +private static string GetProjectSpecificConfigurationValue(BuildContext buildContext, string projectName, string configurationPrefix, string fallbackValue) +{ + // Allow per project overrides via "[configurationPrefix][projectName]" + var slug = GetProjectSlug(projectName); + var keyToCheck = string.Format("{0}{1}", configurationPrefix, slug); + + var value = buildContext.BuildServer.GetVariable(keyToCheck, fallbackValue); + return value; +} + +//------------------------------------------------------------- + +private static void CleanProject(BuildContext buildContext, string projectName) +{ + buildContext.CakeContext.LogSeparator("Cleaning project '{0}'", projectName); + + var projectDirectory = GetProjectDirectory(projectName); + + buildContext.CakeContext.Information($"Investigating paths to clean up in '{projectDirectory}'"); + + var directoriesToDelete = new List(); + + var binDirectory = System.IO.Path.Combine(projectDirectory, "bin"); + directoriesToDelete.Add(binDirectory); + + var objDirectory = System.IO.Path.Combine(projectDirectory, "obj"); + directoriesToDelete.Add(objDirectory); + + // Special C++ scenarios + var projectFileName = GetProjectFileName(buildContext, projectName); + if (IsCppProject(projectFileName)) + { + var debugDirectory = System.IO.Path.Combine(projectDirectory, "Debug"); + directoriesToDelete.Add(debugDirectory); + + var releaseDirectory = System.IO.Path.Combine(projectDirectory, "Release"); + directoriesToDelete.Add(releaseDirectory); + + var x64Directory = System.IO.Path.Combine(projectDirectory, "x64"); + directoriesToDelete.Add(x64Directory); + + var x86Directory = System.IO.Path.Combine(projectDirectory, "x86"); + directoriesToDelete.Add(x86Directory); + } + + foreach (var directoryToDelete in directoriesToDelete) + { + DeleteDirectoryWithLogging(buildContext, directoryToDelete); + } +} + +//------------------------------------------------------------- + +private static void DeleteDirectoryWithLogging(BuildContext buildContext, string directoryToDelete) +{ + if (buildContext.CakeContext.DirectoryExists(directoryToDelete)) + { + buildContext.CakeContext.Information($"Cleaning up directory '{directoryToDelete}'"); + + buildContext.CakeContext.DeleteDirectory(directoryToDelete, new DeleteDirectorySettings + { + Force = true, + Recursive = true + }); + } +} + +//------------------------------------------------------------- + +private static bool IsCppProject(string projectName) +{ + return projectName.EndsWith(".vcxproj"); +} + +//------------------------------------------------------------- + +private static bool IsBlazorProject(BuildContext buildContext, string projectName) +{ + var projectFileName = GetProjectFileName(buildContext, projectName); + + if (!_blazorCache.TryGetValue(projectFileName, out var isBlazor)) + { + isBlazor = false; + + var lines = System.IO.File.ReadAllLines(projectFileName); + foreach (var line in lines) + { + // Match both *TargetFramework* and *TargetFrameworks* + var lowerCase = line.ToLower(); + if (lowerCase.Contains(" Excludes + var includes = buildContext.General.Includes; + if (includes.Count > 0) + { + var process = includes.Any(x => string.Equals(x, projectName, StringComparison.OrdinalIgnoreCase)); + + if (!process) + { + buildContext.CakeContext.Warning("Project '{0}' should not be processed, removing from projects to process", projectName); + } + + return process; + } + + var excludes = buildContext.General.Excludes; + if (excludes.Count > 0) + { + var process = !excludes.Any(x => string.Equals(x, projectName, StringComparison.OrdinalIgnoreCase)); + + if (!process) + { + buildContext.CakeContext.Warning("Project '{0}' should not be processed, removing from projects to process", projectName); + } + + return process; + } + + // Is this a known project? + if (!buildContext.RegisteredProjects.Any(x => string.Equals(projectName, x, StringComparison.OrdinalIgnoreCase))) + { + buildContext.CakeContext.Warning("Project '{0}' should not be processed, does not exist as registered project", projectName); + return false; + } + + if (buildContext.General.IsCiBuild) + { + // In CI builds, we always want to include all projects + return true; + } + + // Experimental mode where we ignore projects that are not on the deploy list when not in CI mode, but + // it can only work if they are not part of unit tests (but that should never happen) + // if (buildContext.Tests.Items.Count == 0) + // { + if (checkDeployment && + !ShouldBuildProject(buildContext, projectName) && + !ShouldPackageProject(buildContext, projectName) && + !ShouldDeployProject(buildContext, projectName)) + { + buildContext.CakeContext.Warning("Project '{0}' should not be processed because this is not a CI build, does not contain tests and the project should not be built, packaged or deployed, removing from projects to process", projectName); + return false; + } + //} + + return true; +} + +//------------------------------------------------------------- + +private static List GetProjectRuntimesIdentifiers(BuildContext buildContext, Cake.Core.IO.FilePath solutionOrProjectFileName, List runtimeIdentifiersToInvestigate) +{ + var projectFileContents = System.IO.File.ReadAllText(solutionOrProjectFileName.FullPath)?.ToLower(); + + var supportedRuntimeIdentifiers = new List(); + + foreach (var runtimeIdentifier in runtimeIdentifiersToInvestigate) + { + if (!string.IsNullOrWhiteSpace(runtimeIdentifier)) + { + if (!projectFileContents.Contains(runtimeIdentifier.ToLower())) + { + buildContext.CakeContext.Information("Project '{0}' does not support runtime identifier '{1}', removing from supported runtime identifiers list", solutionOrProjectFileName, runtimeIdentifier); + continue; + } + } + + supportedRuntimeIdentifiers.Add(runtimeIdentifier); + } + + if (supportedRuntimeIdentifiers.Count == 0) + { + buildContext.CakeContext.Information("Project '{0}' does not have any explicit runtime identifiers left, adding empty one as default", solutionOrProjectFileName); + + // Default + supportedRuntimeIdentifiers.Add(string.Empty); + } + + return supportedRuntimeIdentifiers; +} + +//------------------------------------------------------------- + +private static bool ShouldBuildProject(BuildContext buildContext, string projectName) +{ + // Allow the build server to configure this via "Build[ProjectName]" + var slug = GetProjectSlug(projectName); + var keyToCheck = string.Format("Build{0}", slug); + + // No need to build if we don't package + var shouldBuild = ShouldPackageProject(buildContext, projectName); + + // By default, everything should be built. This feature is to explicitly not include + // a project in the build when a solution contains multiple projects / components that + // need to be built / packaged / deployed separately + // + // The default value is "ShouldPackageProject" since we assume it does not need + // to be built if it's not supposed to be packaged + shouldBuild = buildContext.BuildServer.GetVariableAsBool(keyToCheck, shouldBuild); + + buildContext.CakeContext.Information($"Value for '{keyToCheck}': {shouldBuild}"); + + return shouldBuild; +} + +//------------------------------------------------------------- + +private static bool ShouldPackageProject(BuildContext buildContext, string projectName) +{ + // Allow the build server to configure this via "Package[ProjectName]" + var slug = GetProjectSlug(projectName); + var keyToCheck = string.Format("Package{0}", slug); + + // No need to package if we don't deploy + var shouldPackage = ShouldDeployProject(buildContext, projectName); + + // The default value is "ShouldDeployProject" since we assume it does not need + // to be packaged if it's not supposed to be deployed + shouldPackage = buildContext.BuildServer.GetVariableAsBool(keyToCheck, shouldPackage); + + // If this is *only* a dependency, it should never be deployed + if (IsOnlyDependencyProject(buildContext, projectName)) + { + shouldPackage = false; + } + + if (shouldPackage && !ShouldProcessProject(buildContext, projectName, false)) + { + buildContext.CakeContext.Information($"Project '{projectName}' should not be processed, excluding it anyway"); + + shouldPackage = false; + } + + buildContext.CakeContext.Information($"Value for '{keyToCheck}': {shouldPackage}"); + + return shouldPackage; +} + +//------------------------------------------------------------- + +private static bool ShouldDeployProject(BuildContext buildContext, string projectName) +{ + // Allow the build server to configure this via "Deploy[ProjectName]" + var slug = GetProjectSlug(projectName); + var keyToCheck = string.Format("Deploy{0}", slug); + + // By default, deploy + var shouldDeploy = buildContext.BuildServer.GetVariableAsBool(keyToCheck, true); + + // If this is *only* a dependency, it should never be deployed + if (IsOnlyDependencyProject(buildContext, projectName)) + { + shouldDeploy = false; + } + + if (shouldDeploy && !ShouldProcessProject(buildContext, projectName, false)) + { + buildContext.CakeContext.Information($"Project '{projectName}' should not be processed, excluding it anyway"); + + shouldDeploy = false; + } + + buildContext.CakeContext.Information($"Value for '{keyToCheck}': {shouldDeploy}"); + + return shouldDeploy; +} + +//------------------------------------------------------------- + +private static bool IsOnlyDependencyProject(BuildContext buildContext, string projectName) +{ + buildContext.CakeContext.Information($"Checking if project '{projectName}' is a dependency only"); + + // If not in the dependencies list, we can stop checking + if (!buildContext.Dependencies.Items.Contains(projectName)) + { + buildContext.CakeContext.Information($"Project is not in list of dependencies, assuming not dependency only"); + return false; + } + + if (buildContext.Components.Items.Contains(projectName)) + { + buildContext.CakeContext.Information($"Project is list of components, assuming not dependency only"); + return false; + } + + if (buildContext.DockerImages.Items.Contains(projectName)) + { + buildContext.CakeContext.Information($"Project is list of docker images, assuming not dependency only"); + return false; + } + + if (buildContext.GitHubPages.Items.Contains(projectName)) + { + buildContext.CakeContext.Information($"Project is list of GitHub pages, assuming not dependency only"); + return false; + } + + if (buildContext.Templates.Items.Contains(projectName)) + { + buildContext.CakeContext.Information($"Project is list of templates, assuming not dependency only"); + return false; + } + + if (buildContext.Tools.Items.Contains(projectName)) + { + buildContext.CakeContext.Information($"Project is list of tools, assuming not dependency only"); + return false; + } + + if (buildContext.Uwp.Items.Contains(projectName)) + { + buildContext.CakeContext.Information($"Project is list of UWP apps, assuming not dependency only"); + return false; + } + + if (buildContext.VsExtensions.Items.Contains(projectName)) + { + buildContext.CakeContext.Information($"Project is list of VS extensions, assuming not dependency only"); + return false; + } + + if (buildContext.Web.Items.Contains(projectName)) + { + buildContext.CakeContext.Information($"Project is list of web apps, assuming not dependency only"); + return false; + } + + if (buildContext.Wpf.Items.Contains(projectName)) + { + buildContext.CakeContext.Information($"Project is list of WPF apps, assuming not dependency only"); + return false; + } + + buildContext.CakeContext.Information($"Project '{projectName}' is a dependency only"); + + // It's in the dependencies list and not in any other list + return true; +} + +//------------------------------------------------------------- + +public static void Add(this Dictionary> dictionary, string project, params string[] projects) +{ + dictionary.Add(project, new List(projects)); } \ No newline at end of file diff --git a/deployment/cake/lib-logging.cake b/deployment/cake/lib-logging.cake index da08b224..22587f60 100644 --- a/deployment/cake/lib-logging.cake +++ b/deployment/cake/lib-logging.cake @@ -1,51 +1,51 @@ -// Note: code originally comes from https://stackoverflow.com/questions/50826394/how-to-print-tool-command-line-in-cake - -/// -/// Temporary sets logging verbosity. -/// -/// -/// -/// // Temporary sets logging verbosity to Diagnostic. -/// using(context.UseVerbosity(Verbosity.Diagnostic)) -/// { -/// context.DotNetBuild(project, settings); -/// } -/// -/// -public static VerbosityChanger UseVerbosity(this ICakeContext context, Verbosity newVerbosity) => - new VerbosityChanger(context.Log, newVerbosity); - - -/// -/// Temporary sets logging verbosity to Diagnostic. -/// -/// -/// -/// // Temporary sets logging verbosity to Diagnostic. -/// using(context.UseDiagnosticVerbosity()) -/// { -/// context.DotNetBuild(project, settings); -/// } -/// -/// -public static VerbosityChanger UseDiagnosticVerbosity(this ICakeContext context) => - context.UseVerbosity(Verbosity.Diagnostic); - -/// -/// Cake log verbosity changer. -/// Restores old verbosity on Dispose. -/// -public class VerbosityChanger : IDisposable -{ - ICakeLog _log; - Verbosity _oldVerbosity; - - public VerbosityChanger(ICakeLog log, Verbosity newVerbosity) - { - _log = log; - _oldVerbosity = log.Verbosity; - _log.Verbosity = newVerbosity; - } - - public void Dispose() => _log.Verbosity = _oldVerbosity; +// Note: code originally comes from https://stackoverflow.com/questions/50826394/how-to-print-tool-command-line-in-cake + +/// +/// Temporary sets logging verbosity. +/// +/// +/// +/// // Temporary sets logging verbosity to Diagnostic. +/// using(context.UseVerbosity(Verbosity.Diagnostic)) +/// { +/// context.DotNetBuild(project, settings); +/// } +/// +/// +public static VerbosityChanger UseVerbosity(this ICakeContext context, Verbosity newVerbosity) => + new VerbosityChanger(context.Log, newVerbosity); + + +/// +/// Temporary sets logging verbosity to Diagnostic. +/// +/// +/// +/// // Temporary sets logging verbosity to Diagnostic. +/// using(context.UseDiagnosticVerbosity()) +/// { +/// context.DotNetBuild(project, settings); +/// } +/// +/// +public static VerbosityChanger UseDiagnosticVerbosity(this ICakeContext context) => + context.UseVerbosity(Verbosity.Diagnostic); + +/// +/// Cake log verbosity changer. +/// Restores old verbosity on Dispose. +/// +public class VerbosityChanger : IDisposable +{ + ICakeLog _log; + Verbosity _oldVerbosity; + + public VerbosityChanger(ICakeLog log, Verbosity newVerbosity) + { + _log = log; + _oldVerbosity = log.Verbosity; + _log.Verbosity = newVerbosity; + } + + public void Dispose() => _log.Verbosity = _oldVerbosity; } \ No newline at end of file diff --git a/deployment/cake/lib-msbuild.cake b/deployment/cake/lib-msbuild.cake index 1dacdfd0..984acac6 100644 --- a/deployment/cake/lib-msbuild.cake +++ b/deployment/cake/lib-msbuild.cake @@ -1,486 +1,486 @@ -#addin "nuget:?package=Cake.Issues&version=3.0.0" -#addin "nuget:?package=Cake.Issues.MsBuild&version=3.0.0" - -#tool "nuget:?package=MSBuild.Extension.Pack&version=1.9.1" - -//------------------------------------------------------------- - -private static void BuildSolution(BuildContext buildContext) -{ - var solutionName = buildContext.General.Solution.Name; - var solutionFileName = buildContext.General.Solution.FileName; - - buildContext.CakeContext.LogSeparator("Building solution '{0}'", solutionName); - - var msBuildSettings = new MSBuildSettings - { - Verbosity = Verbosity.Quiet, - //Verbosity = Verbosity.Diagnostic, - ToolVersion = MSBuildToolVersion.Default, - Configuration = buildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform, - PlatformTarget = PlatformTarget.MSIL, - NoLogo = true - }; - - //ConfigureMsBuild(buildContext, msBuildSettings, dependency, "build"); - - RunMsBuild(buildContext, "Solution", solutionFileName, msBuildSettings, "build"); -} - -//------------------------------------------------------------- - -private static void ConfigureMsBuild(BuildContext buildContext, MSBuildSettings msBuildSettings, - string projectName, string action, bool? allowVsPrerelease = null) -{ - var toolPath = GetVisualStudioPath(buildContext, allowVsPrerelease); - if (!string.IsNullOrWhiteSpace(toolPath)) - { - buildContext.CakeContext.Information($"Overriding ms build tool path to '{toolPath}'"); - - msBuildSettings.ToolPath = toolPath; - } - - // Note: we need to set OverridableOutputPath because we need to be able to respect - // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which - // are properties passed in using the command line) - var outputDirectory = GetProjectOutputDirectory(buildContext, projectName); - buildContext.CakeContext.Information("Output directory: '{0}'", outputDirectory); - msBuildSettings.WithProperty("OverridableOutputRootPath", buildContext.General.OutputRootDirectory); - - // GHK: 2022-05-25: Disabled overriding the (whole) output path since this caused all - // reference projects to be re-build again since this override is used for all projects, - // including project references - //msBuildSettings.WithProperty("OverridableOutputPath", outputDirectory); - - msBuildSettings.WithProperty("PackageOutputPath", buildContext.General.OutputRootDirectory); - - // Only optimize in release mode - if (!buildContext.General.IsLocalBuild) - { - buildContext.CakeContext.Information("This is NOT a local build, disabling building of project references"); - - // Don't build project references (should already be built) - msBuildSettings.WithProperty("BuildProjectReferences", "false"); - - //InjectAssemblySearchPathsInProjectFile(buildContext, projectName, GetProjectFileName(buildContext, projectName)); - } - else - { - buildContext.CakeContext.Information("This is a local build, disabling building of project references"); - } - - // Continuous integration build - msBuildSettings.ContinuousIntegrationBuild = true; - //msBuildSettings.WithProperty("ContinuousIntegrationBuild", "true"); - - // No NuGet restore (should already be done) - msBuildSettings.WithProperty("ResolveNuGetPackages", "false"); - msBuildSettings.Restore = false; - - // Solution info - // msBuildSettings.WithProperty("SolutionFileName", System.IO.Path.GetFileName(buildContext.General.Solution.FileName)); - // msBuildSettings.WithProperty("SolutionPath", System.IO.Path.GetFullPath(buildContext.General.Solution.FileName)); - // msBuildSettings.WithProperty("SolutionDir", System.IO.Path.GetFullPath(buildContext.General.Solution.Directory)); - // msBuildSettings.WithProperty("SolutionName", buildContext.General.Solution.Name); - // msBuildSettings.WithProperty("SolutionExt", ".sln"); - // msBuildSettings.WithProperty("DefineExplicitDefaults", "true"); - - // Disable copyright info - msBuildSettings.NoLogo = true; - - // Use as much CPU as possible - msBuildSettings.MaxCpuCount = 0; - - // Enable for file logging - if (buildContext.General.EnableMsBuildFileLog) - { - msBuildSettings.AddFileLogger(new MSBuildFileLogger - { - Verbosity = msBuildSettings.Verbosity, - //Verbosity = Verbosity.Diagnostic, - LogFile = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.log", projectName, action)) - }); - } - - // Enable for bin logging - if (buildContext.General.EnableMsBuildBinaryLog) - { - msBuildSettings.BinaryLogger = new MSBuildBinaryLogSettings - { - Enabled = true, - Imports = MSBuildBinaryLogImports.Embed, - FileName = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.binlog", projectName, action)) - }; - } -} - -//------------------------------------------------------------- - -private static void ConfigureMsBuildForDotNet(BuildContext buildContext, DotNetMSBuildSettings msBuildSettings, - string projectName, string action, bool? allowVsPrerelease = null) -{ - var toolPath = GetVisualStudioPath(buildContext, allowVsPrerelease); - if (!string.IsNullOrWhiteSpace(toolPath)) - { - buildContext.CakeContext.Information($"Overriding ms build tool path to '{toolPath}'"); - - msBuildSettings.ToolPath = toolPath; - } - - // Note: we need to set OverridableOutputPath because we need to be able to respect - // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which - // are properties passed in using the command line) - var outputDirectory = GetProjectOutputDirectory(buildContext, projectName); - buildContext.CakeContext.Information("Output directory: '{0}'", outputDirectory); - msBuildSettings.WithProperty("OverridableOutputRootPath", buildContext.General.OutputRootDirectory); - - // GHK: 2022-05-25: Disabled overriding the (whole) output path since this caused all - // reference projects to be re-build again since this override is used for all projects, - // including project references - //msBuildSettings.WithProperty("OverridableOutputPath", outputDirectory); - - msBuildSettings.WithProperty("PackageOutputPath", buildContext.General.OutputRootDirectory); - - // Only optimize in release mode - if (!buildContext.General.IsLocalBuild) - { - buildContext.CakeContext.Information($"This is NOT a local build, disabling building of project references"); - - // Don't build project references (should already be built) - msBuildSettings.WithProperty("BuildProjectReferences", "false"); - - //InjectAssemblySearchPathsInProjectFile(buildContext, projectName, GetProjectFileName(buildContext, projectName)); - } - else - { - buildContext.CakeContext.Information($"This is a local build, disabling building of project references"); - } - - // Continuous integration build - msBuildSettings.ContinuousIntegrationBuild = true; - //msBuildSettings.WithProperty("ContinuousIntegrationBuild", "true"); - - // No NuGet restore (should already be done) - msBuildSettings.WithProperty("ResolveNuGetPackages", "false"); - //msBuildSettings.Restore = false; - - // Solution info - // msBuildSettings.WithProperty("SolutionFileName", System.IO.Path.GetFileName(buildContext.General.Solution.FileName)); - // msBuildSettings.WithProperty("SolutionPath", System.IO.Path.GetFullPath(buildContext.General.Solution.FileName)); - // msBuildSettings.WithProperty("SolutionDir", System.IO.Path.GetFullPath(buildContext.General.Solution.Directory)); - // msBuildSettings.WithProperty("SolutionName", buildContext.General.Solution.Name); - // msBuildSettings.WithProperty("SolutionExt", ".sln"); - // msBuildSettings.WithProperty("DefineExplicitDefaults", "true"); - - // Disable copyright info - msBuildSettings.NoLogo = true; - - // Use as much CPU as possible - msBuildSettings.MaxCpuCount = 0; - - // Enable for file logging - if (buildContext.General.EnableMsBuildFileLog) - { - msBuildSettings.AddFileLogger(new MSBuildFileLoggerSettings - { - Verbosity = msBuildSettings.Verbosity, - //Verbosity = Verbosity.Diagnostic, - LogFile = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.log", projectName, action)) - }); - } - - // Enable for bin logging - if (buildContext.General.EnableMsBuildBinaryLog) - { - msBuildSettings.BinaryLogger = new MSBuildBinaryLoggerSettings - { - Enabled = true, - Imports = MSBuildBinaryLoggerImports.Embed, - FileName = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}.binlog", projectName, action)) - }; - - // Note: this only works for direct .net core msbuild usage, not when this is - // being wrapped in a tool (such as 'dotnet pack') - var binLogArgs = string.Format("-bl:\"{0}\";ProjectImports=Embed", - System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.binlog", projectName, action))); - - msBuildSettings.ArgumentCustomization = args => args.Append(binLogArgs); - } -} - -//------------------------------------------------------------- - -private static void RunMsBuild(BuildContext buildContext, string projectName, string projectFileName, MSBuildSettings msBuildSettings, string action) -{ - // IMPORTANT NOTE --- READ <============================================= - // - // Note: - // - Binary logger outputs version 9, but the binlog reader only supports up to 8 - // - Xml logger only seems to read warnings - // - // IMPORTANT NOTE --- READ <============================================= - - var totalStopwatch = Stopwatch.StartNew(); - var buildStopwatch = Stopwatch.StartNew(); - - // Enforce additional logging for issues - //var logPath = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.binlog", projectName, action)); - - buildContext.CakeContext.CreateDirectory(buildContext.General.OutputRootDirectory); - - var logPath = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.xml", projectName, action)); - - if (buildContext.General.EnableMsBuildXmlLog) - { - msBuildSettings.WithLogger(buildContext.CakeContext.Tools.Resolve("MSBuild.ExtensionPack.Loggers.dll").FullPath, - "XmlFileLogger", $"logfile=\"{logPath}\";verbosity=Detailed;encoding=UTF-8"); - } - - var failBuild = false; - - try - { - // using (buildContext.CakeContext.UseDiagnosticVerbosity()) - // { - buildContext.CakeContext.MSBuild(projectFileName, msBuildSettings); - //} - } - catch (System.Exception) - { - // Accept for now, we will throw later - failBuild = true; - } - - buildContext.CakeContext.Information(string.Empty); - buildContext.CakeContext.Information($"Done {action}ing project, took '{buildStopwatch.Elapsed}'"); - buildContext.CakeContext.Information(string.Empty); - buildContext.CakeContext.Information($"Investigating potential issues using '{logPath}'"); - buildContext.CakeContext.Information(string.Empty); - - if (System.IO.File.Exists(logPath)) - { - var investigationStopwatch = Stopwatch.StartNew(); - - var issuesContext = buildContext.CakeContext.MsBuildIssuesFromFilePath(logPath, buildContext.CakeContext.MsBuildXmlFileLoggerFormat()); - //var issuesContext = buildContext.CakeContext.MsBuildIssuesFromFilePath(logPath, buildContext.CakeContext.MsBuildBinaryLogFileFormat()); - - buildContext.CakeContext.Debug("Created issue context"); - - var issues = buildContext.CakeContext.ReadIssues(issuesContext, buildContext.General.RootDirectory); - - buildContext.CakeContext.Debug($"Found '{issues.Count()}' potential issues"); - - buildContext.CakeContext.Information(string.Empty); - - var loggedIssues = new HashSet(); - - foreach (var issue in issues) - { - var priority = issue.Priority ?? 0; - - var message = $"{issue.AffectedFileRelativePath}({issue.Line},{issue.Column}): {issue.Rule}: {issue.MessageText}"; - if (loggedIssues.Contains(message)) - { - continue; - } - - //buildContext.CakeContext.Information($"[{issue.Priority}] {message}"); - - if (priority == (int)IssuePriority.Warning) - { - buildContext.CakeContext.Warning($"WARNING: {message}"); - - loggedIssues.Add(message); - } - else if (priority == (int)IssuePriority.Error) - { - buildContext.CakeContext.Error($"ERROR: {message}"); - - loggedIssues.Add(message); - - failBuild = true; - } - } - - buildContext.CakeContext.Information(string.Empty); - buildContext.CakeContext.Information($"Done investigating project, took '{investigationStopwatch.Elapsed}'"); - buildContext.CakeContext.Information($"Total msbuild ({action} + investigation) took '{totalStopwatch.Elapsed}'"); - buildContext.CakeContext.Information(string.Empty); - } - - if (failBuild) - { - buildContext.CakeContext.Information(string.Empty); - - throw new Exception($"{action} failed for project '{projectName}'"); - } -} - -//------------------------------------------------------------- - -private static string FindLatestWindowsKitsDirectory(BuildContext buildContext) -{ - // Find highest number with 10.0, e.g. 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64\makeappx.exe' - var directories = buildContext.CakeContext.GetDirectories($@"{Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)}/Windows Kits/10/bin/10.0.*"); - - //buildContext.CakeContext.Information($"Found '{directories.Count}' potential directories for MakeAppX.exe"); - - var directory = directories.LastOrDefault(); - if (directory != null) - { - return directory.FullPath; - } - - return null; -} - -//------------------------------------------------------------- - -private static string GetVisualStudioDirectory(BuildContext buildContext, bool? allowVsPrerelease = null) -{ - // TODO: Support different editions (e.g. Professional, Enterprise, Community, etc) - - // Force 64-bit, even when running as 32-bit process - var programFilesx64 = Environment.ExpandEnvironmentVariables("%ProgramW6432%"); - var programFilesx86 = Environment.ExpandEnvironmentVariables("%ProgramFiles(x86)%"); - - var prereleasePaths = new List>(new [] - { - new KeyValuePair("Visual Studio 2022 Preview", $@"{programFilesx64}\Microsoft Visual Studio\2022\Preview\"), - new KeyValuePair("Visual Studio 2019 Preview", $@"{programFilesx86}\Microsoft Visual Studio\2019\Preview\"), - }); - - var normalPaths = new List> (new [] - { - new KeyValuePair("Visual Studio 2022 Enterprise", $@"{programFilesx64}\Microsoft Visual Studio\2022\Enterprise\"), - new KeyValuePair("Visual Studio 2022 Professional", $@"{programFilesx64}\Microsoft Visual Studio\2022\Professional\"), - new KeyValuePair("Visual Studio 2022 Community", $@"{programFilesx64}\Microsoft Visual Studio\2022\Community\"), - new KeyValuePair("Visual Studio 2019 Enterprise", $@"{programFilesx86}\Microsoft Visual Studio\2019\Enterprise\"), - new KeyValuePair("Visual Studio 2019 Professional", $@"{programFilesx86}\Microsoft Visual Studio\2019\Professional\"), - new KeyValuePair("Visual Studio 2019 Community", $@"{programFilesx86}\Microsoft Visual Studio\2019\Community\"), - }); - - // Prerelease paths - if ((allowVsPrerelease ?? true) && buildContext.General.UseVisualStudioPrerelease) - { - buildContext.CakeContext.Debug("Checking for installation of Visual Studio (preview)"); - - foreach (var prereleasePath in prereleasePaths) - { - if (System.IO.Directory.Exists(prereleasePath.Value)) - { - buildContext.CakeContext.Debug($"Found {prereleasePath.Key}"); - - return prereleasePath.Value; - } - } - } - - buildContext.CakeContext.Debug("Checking for installation of Visual Studio (non-preview)"); - - // Normal paths - foreach (var normalPath in normalPaths) - { - if (System.IO.Directory.Exists(normalPath.Value)) - { - buildContext.CakeContext.Debug($"Found {normalPath.Key}"); - - return normalPath.Value; - } - } - - // Fallback in case someone *only* has prerelease - foreach (var prereleasePath in prereleasePaths) - { - if (System.IO.Directory.Exists(prereleasePath.Value)) - { - buildContext.CakeContext.Information($"Only Visual Studio preview is available, using {prereleasePath.Key}"); - - return prereleasePath.Value; - } - } - - // Failed - return null; -} - -//------------------------------------------------------------- - -private static string GetVisualStudioPath(BuildContext buildContext, bool? allowVsPrerelease = null) -{ - var potentialPaths = new [] - { - @"MSBuild\Current\Bin\msbuild.exe", - @"MSBuild\17.0\Bin\msbuild.exe", - @"MSBuild\16.0\Bin\msbuild.exe", - @"MSBuild\15.0\Bin\msbuild.exe" - }; - - var directory = GetVisualStudioDirectory(buildContext, allowVsPrerelease); - - foreach (var potentialPath in potentialPaths) - { - var pathToCheck = System.IO.Path.Combine(directory, potentialPath); - if (System.IO.File.Exists(pathToCheck)) - { - return pathToCheck; - } - } - - throw new Exception("Could not find the path to Visual Studio (msbuild.exe)"); -} - -//------------------------------------------------------------- - -private static void InjectAssemblySearchPathsInProjectFile(BuildContext buildContext, string projectName, string projectFileName) -{ - try - { - // Allow this project to find any other projects that we have built (since we disabled - // building of project dependencies) - var assemblySearchPaths = new List(); - var separator = System.IO.Path.DirectorySeparatorChar.ToString(); - - foreach (var project in buildContext.AllProjects) - { - var projectOutputDirectory = GetProjectOutputDirectory(buildContext, project); - assemblySearchPaths.Add(projectOutputDirectory); - } - - if (assemblySearchPaths.Count == 0) - { - buildContext.CakeContext.Information("No assembly search paths found to inject"); - return; - } - - // For SourceLink to work, the .csproj should contain something like this: - // - var projectFileContents = System.IO.File.ReadAllText(projectFileName); - if (projectFileContents.Contains("AssemblySearchPaths")) - { - buildContext.CakeContext.Information("Assembly search paths is already added to the project file"); - return; - } - - buildContext.CakeContext.Information("Injecting assembly search paths into project file"); - - var xmlDocument = XDocument.Parse(projectFileContents); - var projectElement = xmlDocument.Root; - - // Item group with package reference - var propertyGroupElement = new XElement("PropertyGroup"); - var assemblySearchPathsElement = new XElement("AssemblySearchPaths"); - - assemblySearchPathsElement.Value = $"$(AssemblySearchPaths);{string.Join(";", assemblySearchPaths)}"; - - propertyGroupElement.Add(assemblySearchPathsElement); - projectElement.Add(propertyGroupElement); - - xmlDocument.Save(projectFileName); - } - catch (Exception ex) - { - buildContext.CakeContext.Error($"Failed to process assembly search paths for project '{projectFileName}': {ex.Message}"); - } +#addin "nuget:?package=Cake.Issues&version=3.0.0" +#addin "nuget:?package=Cake.Issues.MsBuild&version=3.0.0" + +#tool "nuget:?package=MSBuild.Extension.Pack&version=1.9.1" + +//------------------------------------------------------------- + +private static void BuildSolution(BuildContext buildContext) +{ + var solutionName = buildContext.General.Solution.Name; + var solutionFileName = buildContext.General.Solution.FileName; + + buildContext.CakeContext.LogSeparator("Building solution '{0}'", solutionName); + + var msBuildSettings = new MSBuildSettings + { + Verbosity = Verbosity.Quiet, + //Verbosity = Verbosity.Diagnostic, + ToolVersion = MSBuildToolVersion.Default, + Configuration = buildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform, + PlatformTarget = PlatformTarget.MSIL, + NoLogo = true + }; + + //ConfigureMsBuild(buildContext, msBuildSettings, dependency, "build"); + + RunMsBuild(buildContext, "Solution", solutionFileName, msBuildSettings, "build"); +} + +//------------------------------------------------------------- + +private static void ConfigureMsBuild(BuildContext buildContext, MSBuildSettings msBuildSettings, + string projectName, string action, bool? allowVsPrerelease = null) +{ + var toolPath = GetVisualStudioPath(buildContext, allowVsPrerelease); + if (!string.IsNullOrWhiteSpace(toolPath)) + { + buildContext.CakeContext.Information($"Overriding ms build tool path to '{toolPath}'"); + + msBuildSettings.ToolPath = toolPath; + } + + // Note: we need to set OverridableOutputPath because we need to be able to respect + // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which + // are properties passed in using the command line) + var outputDirectory = GetProjectOutputDirectory(buildContext, projectName); + buildContext.CakeContext.Information("Output directory: '{0}'", outputDirectory); + msBuildSettings.WithProperty("OverridableOutputRootPath", buildContext.General.OutputRootDirectory); + + // GHK: 2022-05-25: Disabled overriding the (whole) output path since this caused all + // reference projects to be re-build again since this override is used for all projects, + // including project references + //msBuildSettings.WithProperty("OverridableOutputPath", outputDirectory); + + msBuildSettings.WithProperty("PackageOutputPath", buildContext.General.OutputRootDirectory); + + // Only optimize in release mode + if (!buildContext.General.IsLocalBuild) + { + buildContext.CakeContext.Information("This is NOT a local build, disabling building of project references"); + + // Don't build project references (should already be built) + msBuildSettings.WithProperty("BuildProjectReferences", "false"); + + //InjectAssemblySearchPathsInProjectFile(buildContext, projectName, GetProjectFileName(buildContext, projectName)); + } + else + { + buildContext.CakeContext.Information("This is a local build, disabling building of project references"); + } + + // Continuous integration build + msBuildSettings.ContinuousIntegrationBuild = true; + //msBuildSettings.WithProperty("ContinuousIntegrationBuild", "true"); + + // No NuGet restore (should already be done) + msBuildSettings.WithProperty("ResolveNuGetPackages", "false"); + msBuildSettings.Restore = false; + + // Solution info + // msBuildSettings.WithProperty("SolutionFileName", System.IO.Path.GetFileName(buildContext.General.Solution.FileName)); + // msBuildSettings.WithProperty("SolutionPath", System.IO.Path.GetFullPath(buildContext.General.Solution.FileName)); + // msBuildSettings.WithProperty("SolutionDir", System.IO.Path.GetFullPath(buildContext.General.Solution.Directory)); + // msBuildSettings.WithProperty("SolutionName", buildContext.General.Solution.Name); + // msBuildSettings.WithProperty("SolutionExt", ".sln"); + // msBuildSettings.WithProperty("DefineExplicitDefaults", "true"); + + // Disable copyright info + msBuildSettings.NoLogo = true; + + // Use as much CPU as possible + msBuildSettings.MaxCpuCount = 0; + + // Enable for file logging + if (buildContext.General.EnableMsBuildFileLog) + { + msBuildSettings.AddFileLogger(new MSBuildFileLogger + { + Verbosity = msBuildSettings.Verbosity, + //Verbosity = Verbosity.Diagnostic, + LogFile = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.log", projectName, action)) + }); + } + + // Enable for bin logging + if (buildContext.General.EnableMsBuildBinaryLog) + { + msBuildSettings.BinaryLogger = new MSBuildBinaryLogSettings + { + Enabled = true, + Imports = MSBuildBinaryLogImports.Embed, + FileName = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.binlog", projectName, action)) + }; + } +} + +//------------------------------------------------------------- + +private static void ConfigureMsBuildForDotNet(BuildContext buildContext, DotNetMSBuildSettings msBuildSettings, + string projectName, string action, bool? allowVsPrerelease = null) +{ + var toolPath = GetVisualStudioPath(buildContext, allowVsPrerelease); + if (!string.IsNullOrWhiteSpace(toolPath)) + { + buildContext.CakeContext.Information($"Overriding ms build tool path to '{toolPath}'"); + + msBuildSettings.ToolPath = toolPath; + } + + // Note: we need to set OverridableOutputPath because we need to be able to respect + // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which + // are properties passed in using the command line) + var outputDirectory = GetProjectOutputDirectory(buildContext, projectName); + buildContext.CakeContext.Information("Output directory: '{0}'", outputDirectory); + msBuildSettings.WithProperty("OverridableOutputRootPath", buildContext.General.OutputRootDirectory); + + // GHK: 2022-05-25: Disabled overriding the (whole) output path since this caused all + // reference projects to be re-build again since this override is used for all projects, + // including project references + //msBuildSettings.WithProperty("OverridableOutputPath", outputDirectory); + + msBuildSettings.WithProperty("PackageOutputPath", buildContext.General.OutputRootDirectory); + + // Only optimize in release mode + if (!buildContext.General.IsLocalBuild) + { + buildContext.CakeContext.Information($"This is NOT a local build, disabling building of project references"); + + // Don't build project references (should already be built) + msBuildSettings.WithProperty("BuildProjectReferences", "false"); + + //InjectAssemblySearchPathsInProjectFile(buildContext, projectName, GetProjectFileName(buildContext, projectName)); + } + else + { + buildContext.CakeContext.Information($"This is a local build, disabling building of project references"); + } + + // Continuous integration build + msBuildSettings.ContinuousIntegrationBuild = true; + //msBuildSettings.WithProperty("ContinuousIntegrationBuild", "true"); + + // No NuGet restore (should already be done) + msBuildSettings.WithProperty("ResolveNuGetPackages", "false"); + //msBuildSettings.Restore = false; + + // Solution info + // msBuildSettings.WithProperty("SolutionFileName", System.IO.Path.GetFileName(buildContext.General.Solution.FileName)); + // msBuildSettings.WithProperty("SolutionPath", System.IO.Path.GetFullPath(buildContext.General.Solution.FileName)); + // msBuildSettings.WithProperty("SolutionDir", System.IO.Path.GetFullPath(buildContext.General.Solution.Directory)); + // msBuildSettings.WithProperty("SolutionName", buildContext.General.Solution.Name); + // msBuildSettings.WithProperty("SolutionExt", ".sln"); + // msBuildSettings.WithProperty("DefineExplicitDefaults", "true"); + + // Disable copyright info + msBuildSettings.NoLogo = true; + + // Use as much CPU as possible + msBuildSettings.MaxCpuCount = 0; + + // Enable for file logging + if (buildContext.General.EnableMsBuildFileLog) + { + msBuildSettings.AddFileLogger(new MSBuildFileLoggerSettings + { + Verbosity = msBuildSettings.Verbosity, + //Verbosity = Verbosity.Diagnostic, + LogFile = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.log", projectName, action)) + }); + } + + // Enable for bin logging + if (buildContext.General.EnableMsBuildBinaryLog) + { + msBuildSettings.BinaryLogger = new MSBuildBinaryLoggerSettings + { + Enabled = true, + Imports = MSBuildBinaryLoggerImports.Embed, + FileName = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}.binlog", projectName, action)) + }; + + // Note: this only works for direct .net core msbuild usage, not when this is + // being wrapped in a tool (such as 'dotnet pack') + var binLogArgs = string.Format("-bl:\"{0}\";ProjectImports=Embed", + System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.binlog", projectName, action))); + + msBuildSettings.ArgumentCustomization = args => args.Append(binLogArgs); + } +} + +//------------------------------------------------------------- + +private static void RunMsBuild(BuildContext buildContext, string projectName, string projectFileName, MSBuildSettings msBuildSettings, string action) +{ + // IMPORTANT NOTE --- READ <============================================= + // + // Note: + // - Binary logger outputs version 9, but the binlog reader only supports up to 8 + // - Xml logger only seems to read warnings + // + // IMPORTANT NOTE --- READ <============================================= + + var totalStopwatch = Stopwatch.StartNew(); + var buildStopwatch = Stopwatch.StartNew(); + + // Enforce additional logging for issues + //var logPath = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.binlog", projectName, action)); + + buildContext.CakeContext.CreateDirectory(buildContext.General.OutputRootDirectory); + + var logPath = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, string.Format(@"MsBuild_{0}_{1}_log.xml", projectName, action)); + + if (buildContext.General.EnableMsBuildXmlLog) + { + msBuildSettings.WithLogger(buildContext.CakeContext.Tools.Resolve("MSBuild.ExtensionPack.Loggers.dll").FullPath, + "XmlFileLogger", $"logfile=\"{logPath}\";verbosity=Detailed;encoding=UTF-8"); + } + + var failBuild = false; + + try + { + // using (buildContext.CakeContext.UseDiagnosticVerbosity()) + // { + buildContext.CakeContext.MSBuild(projectFileName, msBuildSettings); + //} + } + catch (System.Exception) + { + // Accept for now, we will throw later + failBuild = true; + } + + buildContext.CakeContext.Information(string.Empty); + buildContext.CakeContext.Information($"Done {action}ing project, took '{buildStopwatch.Elapsed}'"); + buildContext.CakeContext.Information(string.Empty); + buildContext.CakeContext.Information($"Investigating potential issues using '{logPath}'"); + buildContext.CakeContext.Information(string.Empty); + + if (System.IO.File.Exists(logPath)) + { + var investigationStopwatch = Stopwatch.StartNew(); + + var issuesContext = buildContext.CakeContext.MsBuildIssuesFromFilePath(logPath, buildContext.CakeContext.MsBuildXmlFileLoggerFormat()); + //var issuesContext = buildContext.CakeContext.MsBuildIssuesFromFilePath(logPath, buildContext.CakeContext.MsBuildBinaryLogFileFormat()); + + buildContext.CakeContext.Debug("Created issue context"); + + var issues = buildContext.CakeContext.ReadIssues(issuesContext, buildContext.General.RootDirectory); + + buildContext.CakeContext.Debug($"Found '{issues.Count()}' potential issues"); + + buildContext.CakeContext.Information(string.Empty); + + var loggedIssues = new HashSet(); + + foreach (var issue in issues) + { + var priority = issue.Priority ?? 0; + + var message = $"{issue.AffectedFileRelativePath}({issue.Line},{issue.Column}): {issue.Rule}: {issue.MessageText}"; + if (loggedIssues.Contains(message)) + { + continue; + } + + //buildContext.CakeContext.Information($"[{issue.Priority}] {message}"); + + if (priority == (int)IssuePriority.Warning) + { + buildContext.CakeContext.Warning($"WARNING: {message}"); + + loggedIssues.Add(message); + } + else if (priority == (int)IssuePriority.Error) + { + buildContext.CakeContext.Error($"ERROR: {message}"); + + loggedIssues.Add(message); + + failBuild = true; + } + } + + buildContext.CakeContext.Information(string.Empty); + buildContext.CakeContext.Information($"Done investigating project, took '{investigationStopwatch.Elapsed}'"); + buildContext.CakeContext.Information($"Total msbuild ({action} + investigation) took '{totalStopwatch.Elapsed}'"); + buildContext.CakeContext.Information(string.Empty); + } + + if (failBuild) + { + buildContext.CakeContext.Information(string.Empty); + + throw new Exception($"{action} failed for project '{projectName}'"); + } +} + +//------------------------------------------------------------- + +private static string FindLatestWindowsKitsDirectory(BuildContext buildContext) +{ + // Find highest number with 10.0, e.g. 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64\makeappx.exe' + var directories = buildContext.CakeContext.GetDirectories($@"{Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)}/Windows Kits/10/bin/10.0.*"); + + //buildContext.CakeContext.Information($"Found '{directories.Count}' potential directories for MakeAppX.exe"); + + var directory = directories.LastOrDefault(); + if (directory != null) + { + return directory.FullPath; + } + + return null; +} + +//------------------------------------------------------------- + +private static string GetVisualStudioDirectory(BuildContext buildContext, bool? allowVsPrerelease = null) +{ + // TODO: Support different editions (e.g. Professional, Enterprise, Community, etc) + + // Force 64-bit, even when running as 32-bit process + var programFilesx64 = Environment.ExpandEnvironmentVariables("%ProgramW6432%"); + var programFilesx86 = Environment.ExpandEnvironmentVariables("%ProgramFiles(x86)%"); + + var prereleasePaths = new List>(new [] + { + new KeyValuePair("Visual Studio 2022 Preview", $@"{programFilesx64}\Microsoft Visual Studio\2022\Preview\"), + new KeyValuePair("Visual Studio 2019 Preview", $@"{programFilesx86}\Microsoft Visual Studio\2019\Preview\"), + }); + + var normalPaths = new List> (new [] + { + new KeyValuePair("Visual Studio 2022 Enterprise", $@"{programFilesx64}\Microsoft Visual Studio\2022\Enterprise\"), + new KeyValuePair("Visual Studio 2022 Professional", $@"{programFilesx64}\Microsoft Visual Studio\2022\Professional\"), + new KeyValuePair("Visual Studio 2022 Community", $@"{programFilesx64}\Microsoft Visual Studio\2022\Community\"), + new KeyValuePair("Visual Studio 2019 Enterprise", $@"{programFilesx86}\Microsoft Visual Studio\2019\Enterprise\"), + new KeyValuePair("Visual Studio 2019 Professional", $@"{programFilesx86}\Microsoft Visual Studio\2019\Professional\"), + new KeyValuePair("Visual Studio 2019 Community", $@"{programFilesx86}\Microsoft Visual Studio\2019\Community\"), + }); + + // Prerelease paths + if ((allowVsPrerelease ?? true) && buildContext.General.UseVisualStudioPrerelease) + { + buildContext.CakeContext.Debug("Checking for installation of Visual Studio (preview)"); + + foreach (var prereleasePath in prereleasePaths) + { + if (System.IO.Directory.Exists(prereleasePath.Value)) + { + buildContext.CakeContext.Debug($"Found {prereleasePath.Key}"); + + return prereleasePath.Value; + } + } + } + + buildContext.CakeContext.Debug("Checking for installation of Visual Studio (non-preview)"); + + // Normal paths + foreach (var normalPath in normalPaths) + { + if (System.IO.Directory.Exists(normalPath.Value)) + { + buildContext.CakeContext.Debug($"Found {normalPath.Key}"); + + return normalPath.Value; + } + } + + // Fallback in case someone *only* has prerelease + foreach (var prereleasePath in prereleasePaths) + { + if (System.IO.Directory.Exists(prereleasePath.Value)) + { + buildContext.CakeContext.Information($"Only Visual Studio preview is available, using {prereleasePath.Key}"); + + return prereleasePath.Value; + } + } + + // Failed + return null; +} + +//------------------------------------------------------------- + +private static string GetVisualStudioPath(BuildContext buildContext, bool? allowVsPrerelease = null) +{ + var potentialPaths = new [] + { + @"MSBuild\Current\Bin\msbuild.exe", + @"MSBuild\17.0\Bin\msbuild.exe", + @"MSBuild\16.0\Bin\msbuild.exe", + @"MSBuild\15.0\Bin\msbuild.exe" + }; + + var directory = GetVisualStudioDirectory(buildContext, allowVsPrerelease); + + foreach (var potentialPath in potentialPaths) + { + var pathToCheck = System.IO.Path.Combine(directory, potentialPath); + if (System.IO.File.Exists(pathToCheck)) + { + return pathToCheck; + } + } + + throw new Exception("Could not find the path to Visual Studio (msbuild.exe)"); +} + +//------------------------------------------------------------- + +private static void InjectAssemblySearchPathsInProjectFile(BuildContext buildContext, string projectName, string projectFileName) +{ + try + { + // Allow this project to find any other projects that we have built (since we disabled + // building of project dependencies) + var assemblySearchPaths = new List(); + var separator = System.IO.Path.DirectorySeparatorChar.ToString(); + + foreach (var project in buildContext.AllProjects) + { + var projectOutputDirectory = GetProjectOutputDirectory(buildContext, project); + assemblySearchPaths.Add(projectOutputDirectory); + } + + if (assemblySearchPaths.Count == 0) + { + buildContext.CakeContext.Information("No assembly search paths found to inject"); + return; + } + + // For SourceLink to work, the .csproj should contain something like this: + // + var projectFileContents = System.IO.File.ReadAllText(projectFileName); + if (projectFileContents.Contains("AssemblySearchPaths")) + { + buildContext.CakeContext.Information("Assembly search paths is already added to the project file"); + return; + } + + buildContext.CakeContext.Information("Injecting assembly search paths into project file"); + + var xmlDocument = XDocument.Parse(projectFileContents); + var projectElement = xmlDocument.Root; + + // Item group with package reference + var propertyGroupElement = new XElement("PropertyGroup"); + var assemblySearchPathsElement = new XElement("AssemblySearchPaths"); + + assemblySearchPathsElement.Value = $"$(AssemblySearchPaths);{string.Join(";", assemblySearchPaths)}"; + + propertyGroupElement.Add(assemblySearchPathsElement); + projectElement.Add(propertyGroupElement); + + xmlDocument.Save(projectFileName); + } + catch (Exception ex) + { + buildContext.CakeContext.Error($"Failed to process assembly search paths for project '{projectFileName}': {ex.Message}"); + } } \ No newline at end of file diff --git a/deployment/cake/lib-nuget.cake b/deployment/cake/lib-nuget.cake index 82627022..4f5a0bcd 100644 --- a/deployment/cake/lib-nuget.cake +++ b/deployment/cake/lib-nuget.cake @@ -1,157 +1,157 @@ -public class NuGetServer -{ - public string Url { get;set; } - - public string ApiKey { get;set; } - - public override string ToString() - { - var result = Url; - - result += string.Format(" (ApiKey present: '{0}')", !string.IsNullOrWhiteSpace(ApiKey)); - - return result; - } -} - -//------------------------------------------------------------- - -public static List GetNuGetServers(string urls, string apiKeys) -{ - var splittedUrls = urls.Split(new [] { ";" }, StringSplitOptions.None); - var splittedApiKeys = apiKeys.Split(new [] { ";" }, StringSplitOptions.None); - - if (splittedUrls.Length != splittedApiKeys.Length) - { - throw new Exception("Number of api keys does not match number of urls. Even if an API key is not required, add an empty one"); - } - - var servers = new List(); - - for (int i = 0; i < splittedUrls.Length; i++) - { - var url = splittedUrls[i]; - if (string.IsNullOrWhiteSpace(url)) - { - throw new Exception("Url for NuGet server cannot be empty"); - } - - servers.Add(new NuGetServer - { - Url = url, - ApiKey = splittedApiKeys[i] - }); - } - - return servers; -} - -//------------------------------------------------------------- - -private static void RestoreNuGetPackages(BuildContext buildContext, Cake.Core.IO.FilePath solutionOrProjectFileName) -{ - buildContext.CakeContext.LogSeparator("Restoring packages for '{0}'", solutionOrProjectFileName); - - var sources = SplitSeparatedList(buildContext.General.NuGet.PackageSources, ';'); - - var runtimeIdentifiers = new List(new [] - { - "win-x64", - "browser-wasm" - }); - - var supportedRuntimeIdentifiers = GetProjectRuntimesIdentifiers(buildContext, solutionOrProjectFileName, runtimeIdentifiers); - - RestoreNuGetPackagesUsingNuGet(buildContext, solutionOrProjectFileName, sources, supportedRuntimeIdentifiers); - RestoreNuGetPackagesUsingDotnetRestore(buildContext, solutionOrProjectFileName, sources, supportedRuntimeIdentifiers); -} - -//------------------------------------------------------------- - -private static void RestoreNuGetPackagesUsingNuGet(BuildContext buildContext, Cake.Core.IO.FilePath solutionOrProjectFileName, List sources, List runtimeIdentifiers) -{ - if (!buildContext.General.NuGet.RestoreUsingNuGet) - { - return; - } - - buildContext.CakeContext.LogSeparator("Restoring packages for '{0}' using 'NuGet'", solutionOrProjectFileName); - - // No need to deal with runtime identifiers - - try - { - var nuGetRestoreSettings = new NuGetRestoreSettings - { - DisableParallelProcessing = false, - NoCache = false, - NonInteractive = true, - RequireConsent = false - }; - - if (sources.Count > 0) - { - nuGetRestoreSettings.Source = sources; - } - - buildContext.CakeContext.NuGetRestore(solutionOrProjectFileName, nuGetRestoreSettings); - } - catch (Exception) - { - // Ignore - } -} - -//------------------------------------------------------------- - -private static void RestoreNuGetPackagesUsingDotnetRestore(BuildContext buildContext, Cake.Core.IO.FilePath solutionOrProjectFileName, List sources, List runtimeIdentifiers) -{ - if (!buildContext.General.NuGet.RestoreUsingDotNetRestore) - { - return; - } - - buildContext.CakeContext.LogSeparator("Restoring packages for '{0}' using 'dotnet restore'", solutionOrProjectFileName); - - foreach (var runtimeIdentifier in runtimeIdentifiers) - { - try - { - buildContext.CakeContext.LogSeparator("Restoring packages for '{0}' using 'dotnet restore' using runtime identifier '{1}'", solutionOrProjectFileName, runtimeIdentifier); - - var restoreSettings = new DotNetRestoreSettings - { - DisableParallel = false, - Force = false, - ForceEvaluate = false, - IgnoreFailedSources = true, - NoCache = false, - NoDependencies = buildContext.General.NuGet.NoDependencies, // use true to speed up things - Verbosity = DotNetVerbosity.Normal - }; - - if (!string.IsNullOrWhiteSpace(runtimeIdentifier)) - { - buildContext.CakeContext.Information("Project restore uses explicit runtime identifier, forcing re-evaluation"); - - restoreSettings.Force = true; - restoreSettings.ForceEvaluate = true; - restoreSettings.Runtime = runtimeIdentifier; - } - - if (sources.Count > 0) - { - restoreSettings.Sources = sources; - } - - using (buildContext.CakeContext.UseDiagnosticVerbosity()) - { - buildContext.CakeContext.DotNetRestore(solutionOrProjectFileName.FullPath, restoreSettings); - } - } - catch (Exception) - { - // Ignore - } - } +public class NuGetServer +{ + public string Url { get;set; } + + public string ApiKey { get;set; } + + public override string ToString() + { + var result = Url; + + result += string.Format(" (ApiKey present: '{0}')", !string.IsNullOrWhiteSpace(ApiKey)); + + return result; + } +} + +//------------------------------------------------------------- + +public static List GetNuGetServers(string urls, string apiKeys) +{ + var splittedUrls = urls.Split(new [] { ";" }, StringSplitOptions.None); + var splittedApiKeys = apiKeys.Split(new [] { ";" }, StringSplitOptions.None); + + if (splittedUrls.Length != splittedApiKeys.Length) + { + throw new Exception("Number of api keys does not match number of urls. Even if an API key is not required, add an empty one"); + } + + var servers = new List(); + + for (int i = 0; i < splittedUrls.Length; i++) + { + var url = splittedUrls[i]; + if (string.IsNullOrWhiteSpace(url)) + { + throw new Exception("Url for NuGet server cannot be empty"); + } + + servers.Add(new NuGetServer + { + Url = url, + ApiKey = splittedApiKeys[i] + }); + } + + return servers; +} + +//------------------------------------------------------------- + +private static void RestoreNuGetPackages(BuildContext buildContext, Cake.Core.IO.FilePath solutionOrProjectFileName) +{ + buildContext.CakeContext.LogSeparator("Restoring packages for '{0}'", solutionOrProjectFileName); + + var sources = SplitSeparatedList(buildContext.General.NuGet.PackageSources, ';'); + + var runtimeIdentifiers = new List(new [] + { + "win-x64", + "browser-wasm" + }); + + var supportedRuntimeIdentifiers = GetProjectRuntimesIdentifiers(buildContext, solutionOrProjectFileName, runtimeIdentifiers); + + RestoreNuGetPackagesUsingNuGet(buildContext, solutionOrProjectFileName, sources, supportedRuntimeIdentifiers); + RestoreNuGetPackagesUsingDotnetRestore(buildContext, solutionOrProjectFileName, sources, supportedRuntimeIdentifiers); +} + +//------------------------------------------------------------- + +private static void RestoreNuGetPackagesUsingNuGet(BuildContext buildContext, Cake.Core.IO.FilePath solutionOrProjectFileName, List sources, List runtimeIdentifiers) +{ + if (!buildContext.General.NuGet.RestoreUsingNuGet) + { + return; + } + + buildContext.CakeContext.LogSeparator("Restoring packages for '{0}' using 'NuGet'", solutionOrProjectFileName); + + // No need to deal with runtime identifiers + + try + { + var nuGetRestoreSettings = new NuGetRestoreSettings + { + DisableParallelProcessing = false, + NoCache = false, + NonInteractive = true, + RequireConsent = false + }; + + if (sources.Count > 0) + { + nuGetRestoreSettings.Source = sources; + } + + buildContext.CakeContext.NuGetRestore(solutionOrProjectFileName, nuGetRestoreSettings); + } + catch (Exception) + { + // Ignore + } +} + +//------------------------------------------------------------- + +private static void RestoreNuGetPackagesUsingDotnetRestore(BuildContext buildContext, Cake.Core.IO.FilePath solutionOrProjectFileName, List sources, List runtimeIdentifiers) +{ + if (!buildContext.General.NuGet.RestoreUsingDotNetRestore) + { + return; + } + + buildContext.CakeContext.LogSeparator("Restoring packages for '{0}' using 'dotnet restore'", solutionOrProjectFileName); + + foreach (var runtimeIdentifier in runtimeIdentifiers) + { + try + { + buildContext.CakeContext.LogSeparator("Restoring packages for '{0}' using 'dotnet restore' using runtime identifier '{1}'", solutionOrProjectFileName, runtimeIdentifier); + + var restoreSettings = new DotNetRestoreSettings + { + DisableParallel = false, + Force = false, + ForceEvaluate = false, + IgnoreFailedSources = true, + NoCache = false, + NoDependencies = buildContext.General.NuGet.NoDependencies, // use true to speed up things + Verbosity = DotNetVerbosity.Normal + }; + + if (!string.IsNullOrWhiteSpace(runtimeIdentifier)) + { + buildContext.CakeContext.Information("Project restore uses explicit runtime identifier, forcing re-evaluation"); + + restoreSettings.Force = true; + restoreSettings.ForceEvaluate = true; + restoreSettings.Runtime = runtimeIdentifier; + } + + if (sources.Count > 0) + { + restoreSettings.Sources = sources; + } + + using (buildContext.CakeContext.UseDiagnosticVerbosity()) + { + buildContext.CakeContext.DotNetRestore(solutionOrProjectFileName.FullPath, restoreSettings); + } + } + catch (Exception) + { + // Ignore + } + } } \ No newline at end of file diff --git a/deployment/cake/lib-octopusdeploy.cake b/deployment/cake/lib-octopusdeploy.cake index e1a2fd1f..1cc94168 100644 --- a/deployment/cake/lib-octopusdeploy.cake +++ b/deployment/cake/lib-octopusdeploy.cake @@ -1,40 +1,40 @@ -#tool "nuget:?package=OctopusTools&version=9.1.7" - -public class OctopusDeployIntegration : IntegrationBase -{ - public OctopusDeployIntegration(BuildContext buildContext) - : base(buildContext) - { - OctopusRepositoryUrl = buildContext.BuildServer.GetVariable("OctopusRepositoryUrl", showValue: true); - OctopusRepositoryApiKey = buildContext.BuildServer.GetVariable("OctopusRepositoryApiKey", showValue: false); - OctopusDeploymentTarget = buildContext.BuildServer.GetVariable("OctopusDeploymentTarget", "Staging", showValue: true); - } - - public string OctopusRepositoryUrl { get; set; } - public string OctopusRepositoryApiKey { get; set; } - public string OctopusDeploymentTarget { get; set; } - - //------------------------------------------------------------- - - public string GetRepositoryUrl(string projectName) - { - // Allow per project overrides via "OctopusRepositoryUrlFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "OctopusRepositoryUrlFor", OctopusRepositoryUrl); - } - - //------------------------------------------------------------- - - public string GetRepositoryApiKey(string projectName) - { - // Allow per project overrides via "OctopusRepositoryApiKeyFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "OctopusRepositoryApiKeyFor", OctopusRepositoryApiKey); - } - - //------------------------------------------------------------- - - public string GetDeploymentTarget(string projectName) - { - // Allow per project overrides via "OctopusDeploymentTargetFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "OctopusDeploymentTargetFor", OctopusDeploymentTarget); - } +#tool "nuget:?package=OctopusTools&version=9.1.7" + +public class OctopusDeployIntegration : IntegrationBase +{ + public OctopusDeployIntegration(BuildContext buildContext) + : base(buildContext) + { + OctopusRepositoryUrl = buildContext.BuildServer.GetVariable("OctopusRepositoryUrl", showValue: true); + OctopusRepositoryApiKey = buildContext.BuildServer.GetVariable("OctopusRepositoryApiKey", showValue: false); + OctopusDeploymentTarget = buildContext.BuildServer.GetVariable("OctopusDeploymentTarget", "Staging", showValue: true); + } + + public string OctopusRepositoryUrl { get; set; } + public string OctopusRepositoryApiKey { get; set; } + public string OctopusDeploymentTarget { get; set; } + + //------------------------------------------------------------- + + public string GetRepositoryUrl(string projectName) + { + // Allow per project overrides via "OctopusRepositoryUrlFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "OctopusRepositoryUrlFor", OctopusRepositoryUrl); + } + + //------------------------------------------------------------- + + public string GetRepositoryApiKey(string projectName) + { + // Allow per project overrides via "OctopusRepositoryApiKeyFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "OctopusRepositoryApiKeyFor", OctopusRepositoryApiKey); + } + + //------------------------------------------------------------- + + public string GetDeploymentTarget(string projectName) + { + // Allow per project overrides via "OctopusDeploymentTargetFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "OctopusDeploymentTargetFor", OctopusDeploymentTarget); + } } \ No newline at end of file diff --git a/deployment/cake/lib-signing.cake b/deployment/cake/lib-signing.cake index ba334baf..97dfc736 100644 --- a/deployment/cake/lib-signing.cake +++ b/deployment/cake/lib-signing.cake @@ -1,161 +1,193 @@ -private static string _signToolFileName; - -//------------------------------------------------------------- - -public static bool ShouldSignImmediately(BuildContext buildContext, string projectName) -{ - if (buildContext.General.IsLocalBuild || - buildContext.General.IsCiBuild) - { - // Never code-sign local or ci builds - return false; - } - - if (buildContext.CodeSigning.ProjectsToSignImmediately.Contains(projectName)) - { - buildContext.CakeContext.Information($"Immediately code signing '{projectName}' files"); - return true; - } - - return false; -} - -//------------------------------------------------------------- - -public static void SignFiles(BuildContext buildContext, string signToolCommand, IEnumerable fileNames, string additionalCommandLineArguments = null) -{ - if (fileNames.Any()) - { - buildContext.CakeContext.Information($"Signing '{fileNames.Count()}' files, this could take a while..."); - } - - foreach (var fileName in fileNames) - { - SignFile(buildContext, signToolCommand, fileName.FullPath, additionalCommandLineArguments); - } -} - -//------------------------------------------------------------- - -public static void SignFiles(BuildContext buildContext, string signToolCommand, IEnumerable fileNames, string additionalCommandLineArguments = null) -{ - if (fileNames.Any()) - { - buildContext.CakeContext.Information($"Signing '{fileNames.Count()}' files, this could take a while..."); - } - - foreach (var fileName in fileNames) - { - SignFile(buildContext, signToolCommand, fileName, additionalCommandLineArguments); - } -} - -//------------------------------------------------------------- - -public static void SignFile(BuildContext buildContext, string signToolCommand, string fileName, string additionalCommandLineArguments = null) -{ - // Skip code signing in specific scenarios - if (buildContext.General.IsCiBuild || - buildContext.General.IsLocalBuild) - { - buildContext.CakeContext.Information("Skipping signing because this is a local or CI build"); - return; - } - - if (string.IsNullOrWhiteSpace(signToolCommand)) - { - return; - } - - if (string.IsNullOrWhiteSpace(_signToolFileName)) - { - _signToolFileName = FindSignToolFileName(buildContext); - } - - if (string.IsNullOrWhiteSpace(_signToolFileName)) - { - throw new InvalidOperationException("Cannot find signtool.exe, make sure to install a Windows Development Kit"); - } - - buildContext.CakeContext.Information(string.Empty); - - // Retry mechanism, signing with timestamping is not as reliable as we thought - var safetyCounter = 3; - - while (safetyCounter > 0) - { - buildContext.CakeContext.Information($"Ensuring file '{fileName}' is signed..."); - - // Check - var checkProcessSettings = new ProcessSettings - { - Arguments = $"verify /pa \"{fileName}\"", - Silent = true, - RedirectStandardError = true, - RedirectStandardOutput = true - }; - - using (var checkProcess = buildContext.CakeContext.StartAndReturnProcess(_signToolFileName, checkProcessSettings)) - { - checkProcess.WaitForExit(); - - var exitCode = checkProcess.GetExitCode(); - if (exitCode == 0) - { - buildContext.CakeContext.Information($"File '{fileName}' is already signed, skipping..."); - buildContext.CakeContext.Information(string.Empty); - return; - } - } - - // Sign - if (!string.IsNullOrWhiteSpace(additionalCommandLineArguments)) - { - signToolCommand += $" {additionalCommandLineArguments}"; - } - - var finalCommand = $"{signToolCommand} \"{fileName}\""; - - buildContext.CakeContext.Information($"File '{fileName}' is not signed, signing using '{finalCommand}'"); - - var signProcessSettings = new ProcessSettings - { - Arguments = finalCommand, - Silent = true - }; - - using (var signProcess = buildContext.CakeContext.StartAndReturnProcess(_signToolFileName, signProcessSettings)) - { - signProcess.WaitForExit(); - - var exitCode = signProcess.GetExitCode(); - if (exitCode == 0) - { - return; - } - - buildContext.CakeContext.Warning($"Failed to sign '{fileName}', retries left: '{safetyCounter}'"); - - // Important: add a delay! - System.Threading.Thread.Sleep(5 * 1000); - } - - safetyCounter--; - } - - // If we get here, we failed - throw new Exception($"Signing of '{fileName}' failed"); -} - -//------------------------------------------------------------- - -public static string FindSignToolFileName(BuildContext buildContext) -{ - var directory = FindLatestWindowsKitsDirectory(buildContext); - if (directory != null) - { - return System.IO.Path.Combine(directory, "x64", "signtool.exe"); - } - - return null; -} +private static string _signToolFileName; + +//------------------------------------------------------------- + +public static bool ShouldSignImmediately(BuildContext buildContext, string projectName) +{ + // Sometimes unit tests require signed assemblies, but only sign immediately when it's in the list + if (buildContext.CodeSigning.ProjectsToSignImmediately.Contains(projectName)) + { + buildContext.CakeContext.Information($"Immediately code signing '{projectName}' files"); + return true; + } + + if (buildContext.General.IsLocalBuild || + buildContext.General.IsCiBuild) + { + // Never code-sign local or ci builds + return false; + } + + return false; +} + +//------------------------------------------------------------- + +public static void SignProjectFiles(BuildContext buildContext, string projectName) +{ + var certificateSubjectName = buildContext.General.CodeSign.CertificateSubjectName; + if (string.IsNullOrWhiteSpace(certificateSubjectName)) + { + buildContext.CakeContext.Information("Skipping code signing because the certificate subject name was not specified"); + return; + } + + var codeSignWildCard = buildContext.General.CodeSign.WildCard; + if (string.IsNullOrWhiteSpace(codeSignWildCard)) + { + // Empty, we need to override with project name for valid default value + codeSignWildCard = projectName; + } + + var outputDirectory = string.Format("{0}/{1}", buildContext.General.OutputRootDirectory, projectName); + + var projectFilesToSign = new List(); + + var exeSignFilesSearchPattern = string.Format("{0}/**/*{1}*.exe", outputDirectory, codeSignWildCard); + buildContext.CakeContext.Information(exeSignFilesSearchPattern); + projectFilesToSign.AddRange(buildContext.CakeContext.GetFiles(exeSignFilesSearchPattern)); + + var dllSignFilesSearchPattern = string.Format("{0}/**/*{1}*.dll", outputDirectory, codeSignWildCard); + buildContext.CakeContext.Information(dllSignFilesSearchPattern); + projectFilesToSign.AddRange(buildContext.CakeContext.GetFiles(dllSignFilesSearchPattern)); + + buildContext.CakeContext.Information("Found '{0}' files to code sign for '{1}'", projectFilesToSign.Count, projectName); + + var signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", buildContext.General.CodeSign.TimeStampUri, + certificateSubjectName, buildContext.General.CodeSign.HashAlgorithm); + + SignFiles(buildContext, signToolCommand, projectFilesToSign); +} + +//------------------------------------------------------------- + +public static void SignFiles(BuildContext buildContext, string signToolCommand, IEnumerable fileNames, string additionalCommandLineArguments = null) +{ + if (fileNames.Any()) + { + buildContext.CakeContext.Information($"Signing '{fileNames.Count()}' files, this could take a while..."); + } + + foreach (var fileName in fileNames) + { + SignFile(buildContext, signToolCommand, fileName.FullPath, additionalCommandLineArguments); + } +} + +//------------------------------------------------------------- + +public static void SignFiles(BuildContext buildContext, string signToolCommand, IEnumerable fileNames, string additionalCommandLineArguments = null) +{ + if (fileNames.Any()) + { + buildContext.CakeContext.Information($"Signing '{fileNames.Count()}' files, this could take a while..."); + } + + foreach (var fileName in fileNames) + { + SignFile(buildContext, signToolCommand, fileName, additionalCommandLineArguments); + } +} + +//------------------------------------------------------------- + +public static void SignFile(BuildContext buildContext, string signToolCommand, string fileName, string additionalCommandLineArguments = null) +{ + // Skip code signing in specific scenarios + if (string.IsNullOrWhiteSpace(signToolCommand)) + { + return; + } + + if (string.IsNullOrWhiteSpace(_signToolFileName)) + { + _signToolFileName = FindSignToolFileName(buildContext); + } + + if (string.IsNullOrWhiteSpace(_signToolFileName)) + { + throw new InvalidOperationException("Cannot find signtool.exe, make sure to install a Windows Development Kit"); + } + + buildContext.CakeContext.Information(string.Empty); + + // Retry mechanism, signing with timestamping is not as reliable as we thought + var safetyCounter = 3; + + while (safetyCounter > 0) + { + buildContext.CakeContext.Information($"Ensuring file '{fileName}' is signed..."); + + // Check + var checkProcessSettings = new ProcessSettings + { + Arguments = $"verify /pa \"{fileName}\"", + Silent = true, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + + using (var checkProcess = buildContext.CakeContext.StartAndReturnProcess(_signToolFileName, checkProcessSettings)) + { + checkProcess.WaitForExit(); + + var exitCode = checkProcess.GetExitCode(); + if (exitCode == 0) + { + buildContext.CakeContext.Information($"File '{fileName}' is already signed, skipping..."); + buildContext.CakeContext.Information(string.Empty); + return; + } + } + + // Sign + if (!string.IsNullOrWhiteSpace(additionalCommandLineArguments)) + { + signToolCommand += $" {additionalCommandLineArguments}"; + } + + var finalCommand = $"{signToolCommand} \"{fileName}\""; + + buildContext.CakeContext.Information($"File '{fileName}' is not signed, signing using '{finalCommand}'"); + + var signProcessSettings = new ProcessSettings + { + Arguments = finalCommand, + Silent = true + }; + + using (var signProcess = buildContext.CakeContext.StartAndReturnProcess(_signToolFileName, signProcessSettings)) + { + signProcess.WaitForExit(); + + var exitCode = signProcess.GetExitCode(); + if (exitCode == 0) + { + return; + } + + buildContext.CakeContext.Warning($"Failed to sign '{fileName}', retries left: '{safetyCounter}'"); + + // Important: add a delay! + System.Threading.Thread.Sleep(5 * 1000); + } + + safetyCounter--; + } + + // If we get here, we failed + throw new Exception($"Signing of '{fileName}' failed"); +} + +//------------------------------------------------------------- + +public static string FindSignToolFileName(BuildContext buildContext) +{ + var directory = FindLatestWindowsKitsDirectory(buildContext); + if (directory != null) + { + return System.IO.Path.Combine(directory, "x64", "signtool.exe"); + } + + return null; +} diff --git a/deployment/cake/lib-sourcelink.cake b/deployment/cake/lib-sourcelink.cake index 474c3937..cd7f7135 100644 --- a/deployment/cake/lib-sourcelink.cake +++ b/deployment/cake/lib-sourcelink.cake @@ -1,106 +1,106 @@ -public static bool IsSourceLinkSupported(BuildContext buildContext, string projectName, string projectFileName) -{ - if (buildContext.General.SourceLink.IsDisabled) - { - return false; - } - - if (buildContext.General.IsLocalBuild) - { - return false; - } - - if (string.IsNullOrWhiteSpace(buildContext.General.Repository.Url)) - { - return false; - } - - // Only support C# projects - if (!projectFileName.EndsWith(".csproj")) - { - return false; - } - - // Is this a test project? - if (buildContext.Tests.Items.Contains(projectName)) - { - return false; - } - - // Only support when running a real build, e.g. ot for 'Package' only - if (!buildContext.General.Target.ToLower().Contains("build")) - { - return false; - } - - return true; -} - -//------------------------------------------------------------- - -public static void InjectSourceLinkInProjectFile(BuildContext buildContext, string projectName, string projectFileName) -{ - try - { - // Only support C# projects - if (!IsSourceLinkSupported(buildContext, projectName, projectFileName)) - { - return; - } - - // For SourceLink to work, the .csproj should contain something like this: - // - var projectFileContents = System.IO.File.ReadAllText(projectFileName); - if (projectFileContents.Contains("Microsoft.SourceLink.GitHub")) - { - return; - } - - buildContext.CakeContext.Warning("No SourceLink reference found, automatically injecting SourceLink package reference now"); - - //const string MSBuildNS = (XNamespace) "http://schemas.microsoft.com/developer/msbuild/2003"; - - var xmlDocument = XDocument.Parse(projectFileContents); - var projectElement = xmlDocument.Root; - - // Item group with package reference - var referencesItemGroup = new XElement("ItemGroup"); - var sourceLinkPackageReference = new XElement("PackageReference"); - sourceLinkPackageReference.Add(new XAttribute("Include", "Microsoft.SourceLink.GitHub")); - sourceLinkPackageReference.Add(new XAttribute("Version", "1.1.1")); - sourceLinkPackageReference.Add(new XAttribute("PrivateAssets", "all")); - - referencesItemGroup.Add(sourceLinkPackageReference); - projectElement.Add(referencesItemGroup); - - // Item group with source root - // - var sourceRootItemGroup = new XElement("ItemGroup"); - var sourceRoot = new XElement("SourceRoot"); - - // Required to end with a \ - var sourceRootValue = buildContext.General.RootDirectory; - var directorySeparator = System.IO.Path.DirectorySeparatorChar.ToString(); - if (!sourceRootValue.EndsWith(directorySeparator)) - { - sourceRootValue += directorySeparator; - }; - - sourceRoot.Add(new XAttribute("Include", sourceRootValue)); - sourceRoot.Add(new XAttribute("RepositoryUrl", buildContext.General.Repository.Url)); - - // Note: since we are not allowing source control manager queries (we don't want to require a .git directory), - // we must specify the additional information below - sourceRoot.Add(new XAttribute("SourceControl", "git")); - sourceRoot.Add(new XAttribute("RevisionId", buildContext.General.Repository.CommitId)); - - sourceRootItemGroup.Add(sourceRoot); - projectElement.Add(sourceRootItemGroup); - - xmlDocument.Save(projectFileName); - } - catch (Exception ex) - { - buildContext.CakeContext.Error($"Failed to process source link for project '{projectFileName}': {ex.Message}"); - } +public static bool IsSourceLinkSupported(BuildContext buildContext, string projectName, string projectFileName) +{ + if (buildContext.General.SourceLink.IsDisabled) + { + return false; + } + + if (buildContext.General.IsLocalBuild) + { + return false; + } + + if (string.IsNullOrWhiteSpace(buildContext.General.Repository.Url)) + { + return false; + } + + // Only support C# projects + if (!projectFileName.EndsWith(".csproj")) + { + return false; + } + + // Is this a test project? + if (buildContext.Tests.Items.Contains(projectName)) + { + return false; + } + + // Only support when running a real build, e.g. ot for 'Package' only + if (!buildContext.General.Target.ToLower().Contains("build")) + { + return false; + } + + return true; +} + +//------------------------------------------------------------- + +public static void InjectSourceLinkInProjectFile(BuildContext buildContext, string projectName, string projectFileName) +{ + try + { + // Only support C# projects + if (!IsSourceLinkSupported(buildContext, projectName, projectFileName)) + { + return; + } + + // For SourceLink to work, the .csproj should contain something like this: + // + var projectFileContents = System.IO.File.ReadAllText(projectFileName); + if (projectFileContents.Contains("Microsoft.SourceLink.GitHub")) + { + return; + } + + buildContext.CakeContext.Warning("No SourceLink reference found, automatically injecting SourceLink package reference now"); + + //const string MSBuildNS = (XNamespace) "http://schemas.microsoft.com/developer/msbuild/2003"; + + var xmlDocument = XDocument.Parse(projectFileContents); + var projectElement = xmlDocument.Root; + + // Item group with package reference + var referencesItemGroup = new XElement("ItemGroup"); + var sourceLinkPackageReference = new XElement("PackageReference"); + sourceLinkPackageReference.Add(new XAttribute("Include", "Microsoft.SourceLink.GitHub")); + sourceLinkPackageReference.Add(new XAttribute("Version", "1.1.1")); + sourceLinkPackageReference.Add(new XAttribute("PrivateAssets", "all")); + + referencesItemGroup.Add(sourceLinkPackageReference); + projectElement.Add(referencesItemGroup); + + // Item group with source root + // + var sourceRootItemGroup = new XElement("ItemGroup"); + var sourceRoot = new XElement("SourceRoot"); + + // Required to end with a \ + var sourceRootValue = buildContext.General.RootDirectory; + var directorySeparator = System.IO.Path.DirectorySeparatorChar.ToString(); + if (!sourceRootValue.EndsWith(directorySeparator)) + { + sourceRootValue += directorySeparator; + }; + + sourceRoot.Add(new XAttribute("Include", sourceRootValue)); + sourceRoot.Add(new XAttribute("RepositoryUrl", buildContext.General.Repository.Url)); + + // Note: since we are not allowing source control manager queries (we don't want to require a .git directory), + // we must specify the additional information below + sourceRoot.Add(new XAttribute("SourceControl", "git")); + sourceRoot.Add(new XAttribute("RevisionId", buildContext.General.Repository.CommitId)); + + sourceRootItemGroup.Add(sourceRoot); + projectElement.Add(sourceRootItemGroup); + + xmlDocument.Save(projectFileName); + } + catch (Exception ex) + { + buildContext.CakeContext.Error($"Failed to process source link for project '{projectFileName}': {ex.Message}"); + } } \ No newline at end of file diff --git a/deployment/cake/notifications-msteams.cake b/deployment/cake/notifications-msteams.cake index 0be4310a..d3d911ab 100644 --- a/deployment/cake/notifications-msteams.cake +++ b/deployment/cake/notifications-msteams.cake @@ -1,95 +1,95 @@ -#addin "nuget:?package=Cake.MicrosoftTeams&version=2.0.0" - -//------------------------------------------------------------- - -public class MsTeamsNotifier : INotifier -{ - public MsTeamsNotifier(BuildContext buildContext) - { - BuildContext = buildContext; - - WebhookUrl = buildContext.BuildServer.GetVariable("MsTeamsWebhookUrl", showValue: false); - WebhookUrlForErrors = buildContext.BuildServer.GetVariable("MsTeamsWebhookUrlForErrors", WebhookUrl, showValue: false); - } - - public BuildContext BuildContext { get; private set; } - - public string WebhookUrl { get; private set; } - public string WebhookUrlForErrors { get; private set; } - - public string GetMsTeamsWebhookUrl(string project, TargetType targetType) - { - // Allow per target overrides via "MsTeamsWebhookUrlFor[TargetType]" - var targetTypeUrl = GetTargetSpecificConfigurationValue(BuildContext, targetType, "MsTeamsWebhookUrlFor", string.Empty); - if (!string.IsNullOrEmpty(targetTypeUrl)) - { - return targetTypeUrl; - } - - // Allow per project overrides via "MsTeamsWebhookUrlFor[ProjectName]" - var projectTypeUrl = GetProjectSpecificConfigurationValue(BuildContext, project, "MsTeamsWebhookUrlFor", string.Empty); - if (!string.IsNullOrEmpty(projectTypeUrl)) - { - return projectTypeUrl; - } - - // Return default fallback - return WebhookUrl; - } - - //------------------------------------------------------------- - - private string GetMsTeamsTarget(string project, TargetType targetType, NotificationType notificationType) - { - if (notificationType == NotificationType.Error) - { - return WebhookUrlForErrors; - } - - return GetMsTeamsWebhookUrl(project, targetType); - } - - //------------------------------------------------------------- - - public async Task NotifyAsync(string project, string message, TargetType targetType, NotificationType notificationType) - { - var targetWebhookUrl = GetMsTeamsTarget(project, targetType, notificationType); - if (string.IsNullOrWhiteSpace(targetWebhookUrl)) - { - return; - } - - var messageCard = new MicrosoftTeamsMessageCard - { - title = project, - summary = notificationType.ToString(), - sections = new [] - { - new MicrosoftTeamsMessageSection - { - activityTitle = notificationType.ToString(), - activitySubtitle = message, - activityText = " ", - activityImage = "https://raw.githubusercontent.com/cake-build/graphics/master/png/cake-small.png", - facts = new [] - { - new MicrosoftTeamsMessageFacts { name ="Project", value = project }, - new MicrosoftTeamsMessageFacts { name ="Version", value = BuildContext.General.Version.FullSemVer }, - new MicrosoftTeamsMessageFacts { name ="CakeVersion", value = BuildContext.CakeContext.Environment.Runtime.CakeVersion.ToString() }, - //new MicrosoftTeamsMessageFacts { name ="TargetFramework", value = Context.Environment.Runtime .TargetFramework.ToString() }, - }, - } - } - }; - - var result = BuildContext.CakeContext.MicrosoftTeamsPostMessage(messageCard, new MicrosoftTeamsSettings - { - IncomingWebhookUrl = targetWebhookUrl - }); - - if (result != System.Net.HttpStatusCode.OK) - { - BuildContext.CakeContext.Warning(string.Format("MsTeams result: {0}", result)); - } - } -} +#addin "nuget:?package=Cake.MicrosoftTeams&version=2.0.0" + +//------------------------------------------------------------- + +public class MsTeamsNotifier : INotifier +{ + public MsTeamsNotifier(BuildContext buildContext) + { + BuildContext = buildContext; + + WebhookUrl = buildContext.BuildServer.GetVariable("MsTeamsWebhookUrl", showValue: false); + WebhookUrlForErrors = buildContext.BuildServer.GetVariable("MsTeamsWebhookUrlForErrors", WebhookUrl, showValue: false); + } + + public BuildContext BuildContext { get; private set; } + + public string WebhookUrl { get; private set; } + public string WebhookUrlForErrors { get; private set; } + + public string GetMsTeamsWebhookUrl(string project, TargetType targetType) + { + // Allow per target overrides via "MsTeamsWebhookUrlFor[TargetType]" + var targetTypeUrl = GetTargetSpecificConfigurationValue(BuildContext, targetType, "MsTeamsWebhookUrlFor", string.Empty); + if (!string.IsNullOrEmpty(targetTypeUrl)) + { + return targetTypeUrl; + } + + // Allow per project overrides via "MsTeamsWebhookUrlFor[ProjectName]" + var projectTypeUrl = GetProjectSpecificConfigurationValue(BuildContext, project, "MsTeamsWebhookUrlFor", string.Empty); + if (!string.IsNullOrEmpty(projectTypeUrl)) + { + return projectTypeUrl; + } + + // Return default fallback + return WebhookUrl; + } + + //------------------------------------------------------------- + + private string GetMsTeamsTarget(string project, TargetType targetType, NotificationType notificationType) + { + if (notificationType == NotificationType.Error) + { + return WebhookUrlForErrors; + } + + return GetMsTeamsWebhookUrl(project, targetType); + } + + //------------------------------------------------------------- + + public async Task NotifyAsync(string project, string message, TargetType targetType, NotificationType notificationType) + { + var targetWebhookUrl = GetMsTeamsTarget(project, targetType, notificationType); + if (string.IsNullOrWhiteSpace(targetWebhookUrl)) + { + return; + } + + var messageCard = new MicrosoftTeamsMessageCard + { + title = project, + summary = notificationType.ToString(), + sections = new [] + { + new MicrosoftTeamsMessageSection + { + activityTitle = notificationType.ToString(), + activitySubtitle = message, + activityText = " ", + activityImage = "https://raw.githubusercontent.com/cake-build/graphics/master/png/cake-small.png", + facts = new [] + { + new MicrosoftTeamsMessageFacts { name ="Project", value = project }, + new MicrosoftTeamsMessageFacts { name ="Version", value = BuildContext.General.Version.FullSemVer }, + new MicrosoftTeamsMessageFacts { name ="CakeVersion", value = BuildContext.CakeContext.Environment.Runtime.CakeVersion.ToString() }, + //new MicrosoftTeamsMessageFacts { name ="TargetFramework", value = Context.Environment.Runtime .TargetFramework.ToString() }, + }, + } + } + }; + + var result = BuildContext.CakeContext.MicrosoftTeamsPostMessage(messageCard, new MicrosoftTeamsSettings + { + IncomingWebhookUrl = targetWebhookUrl + }); + + if (result != System.Net.HttpStatusCode.OK) + { + BuildContext.CakeContext.Warning(string.Format("MsTeams result: {0}", result)); + } + } +} diff --git a/deployment/cake/notifications.cake b/deployment/cake/notifications.cake index 42277599..26db97b2 100644 --- a/deployment/cake/notifications.cake +++ b/deployment/cake/notifications.cake @@ -1,53 +1,53 @@ -#l "notifications-msteams.cake" -//#l "notifications-slack.cake" - -//------------------------------------------------------------- - -public enum NotificationType -{ - Info, - - Error -} - -//------------------------------------------------------------- - -public interface INotifier -{ - Task NotifyAsync(string project, string message, TargetType targetType = TargetType.Unknown, NotificationType notificationType = NotificationType.Info); -} - -//------------------------------------------------------------- - -public class NotificationsIntegration : IntegrationBase -{ - private readonly List _notifiers = new List(); - - public NotificationsIntegration(BuildContext buildContext) - : base(buildContext) - { - _notifiers.Add(new MsTeamsNotifier(buildContext)); - } - - public async Task NotifyDefaultAsync(string project, string message, TargetType targetType = TargetType.Unknown) - { - await NotifyAsync(project, message, targetType, NotificationType.Info); - } - - //------------------------------------------------------------- - - public async Task NotifyErrorAsync(string project, string message, TargetType targetType = TargetType.Unknown) - { - await NotifyAsync(project, string.Format("ERROR: {0}", message), targetType, NotificationType.Error); - } - - //------------------------------------------------------------- - - public async Task NotifyAsync(string project, string message, TargetType targetType = TargetType.Unknown, NotificationType notificationType = NotificationType.Info) - { - foreach (var notifier in _notifiers) - { - await notifier.NotifyAsync(project, message, targetType, notificationType); - } - } +#l "notifications-msteams.cake" +//#l "notifications-slack.cake" + +//------------------------------------------------------------- + +public enum NotificationType +{ + Info, + + Error +} + +//------------------------------------------------------------- + +public interface INotifier +{ + Task NotifyAsync(string project, string message, TargetType targetType = TargetType.Unknown, NotificationType notificationType = NotificationType.Info); +} + +//------------------------------------------------------------- + +public class NotificationsIntegration : IntegrationBase +{ + private readonly List _notifiers = new List(); + + public NotificationsIntegration(BuildContext buildContext) + : base(buildContext) + { + _notifiers.Add(new MsTeamsNotifier(buildContext)); + } + + public async Task NotifyDefaultAsync(string project, string message, TargetType targetType = TargetType.Unknown) + { + await NotifyAsync(project, message, targetType, NotificationType.Info); + } + + //------------------------------------------------------------- + + public async Task NotifyErrorAsync(string project, string message, TargetType targetType = TargetType.Unknown) + { + await NotifyAsync(project, string.Format("ERROR: {0}", message), targetType, NotificationType.Error); + } + + //------------------------------------------------------------- + + public async Task NotifyAsync(string project, string message, TargetType targetType = TargetType.Unknown, NotificationType notificationType = NotificationType.Info) + { + foreach (var notifier in _notifiers) + { + await notifier.NotifyAsync(project, message, targetType, notificationType); + } + } } \ No newline at end of file diff --git a/deployment/cake/sourcecontrol-github.cake b/deployment/cake/sourcecontrol-github.cake index b1fb8624..23732ee5 100644 --- a/deployment/cake/sourcecontrol-github.cake +++ b/deployment/cake/sourcecontrol-github.cake @@ -1,77 +1,77 @@ -#addin "nuget:?package=Cake.GitHub&version=0.1.0" -#addin "nuget:?package=Octokit&version=8.0.0" - -//------------------------------------------------------------- - -public class GitHubSourceControl : ISourceControl -{ - public GitHubSourceControl(BuildContext buildContext) - { - BuildContext = buildContext; - - UserName = buildContext.BuildServer.GetVariable("GitHubUserName", buildContext.General.Repository.Username, showValue: true); - ApiKey = buildContext.BuildServer.GetVariable("GitHubApiKey", buildContext.General.Repository.Password, showValue: false); - OwnerName = buildContext.BuildServer.GetVariable("GitHubOwnerName", buildContext.General.Copyright.Company, showValue: true); - ProjectName = buildContext.BuildServer.GetVariable("GitHubProjectName", buildContext.General.Solution.Name, showValue: true); - - if (!string.IsNullOrWhiteSpace(UserName) && - !string.IsNullOrWhiteSpace(ApiKey) && - !string.IsNullOrWhiteSpace(OwnerName) && - !string.IsNullOrWhiteSpace(ProjectName)) - { - IsAvailable = true; - } - } - - public BuildContext BuildContext { get; private set; } - - public string UserName { get; set; } - public string ApiKey { get; set; } - public string OwnerName { get; set; } - public string ProjectName { get; set; } - - public string OwnerAndProjectName - { - get { return $"{OwnerName}/{ProjectName}"; } - } - - public bool IsAvailable { get; private set; } - - public async Task MarkBuildAsPendingAsync(string context, string description) - { - UpdateStatus(GitHubStatusState.Pending, context, description); - } - - public async Task MarkBuildAsFailedAsync(string context, string description) - { - UpdateStatus(GitHubStatusState.Failure, context, description); - } - - public async Task MarkBuildAsSucceededAsync(string context, string description) - { - UpdateStatus(GitHubStatusState.Success, context, description); - } - - private void UpdateStatus(GitHubStatusState state, string context, string description) - { - // Disabled for now - return; - - if (!IsAvailable) - { - return; - } - - BuildContext.CakeContext.Information("Updating GitHub status to '{0}' | '{1}'", state, description); - - var commitSha = BuildContext.General.Repository.CommitId; - - BuildContext.CakeContext.GitHubStatus(UserName, ApiKey, OwnerName, ProjectName, commitSha, new GitHubStatusSettings - { - State = state, - TargetUrl = null,// "url-to-build-server", - Description = description, - Context = $"Cake - {context}" - }); - } +#addin "nuget:?package=Cake.GitHub&version=0.1.0" +#addin "nuget:?package=Octokit&version=8.0.1" + +//------------------------------------------------------------- + +public class GitHubSourceControl : ISourceControl +{ + public GitHubSourceControl(BuildContext buildContext) + { + BuildContext = buildContext; + + UserName = buildContext.BuildServer.GetVariable("GitHubUserName", buildContext.General.Repository.Username, showValue: true); + ApiKey = buildContext.BuildServer.GetVariable("GitHubApiKey", buildContext.General.Repository.Password, showValue: false); + OwnerName = buildContext.BuildServer.GetVariable("GitHubOwnerName", buildContext.General.Copyright.Company, showValue: true); + ProjectName = buildContext.BuildServer.GetVariable("GitHubProjectName", buildContext.General.Solution.Name, showValue: true); + + if (!string.IsNullOrWhiteSpace(UserName) && + !string.IsNullOrWhiteSpace(ApiKey) && + !string.IsNullOrWhiteSpace(OwnerName) && + !string.IsNullOrWhiteSpace(ProjectName)) + { + IsAvailable = true; + } + } + + public BuildContext BuildContext { get; private set; } + + public string UserName { get; set; } + public string ApiKey { get; set; } + public string OwnerName { get; set; } + public string ProjectName { get; set; } + + public string OwnerAndProjectName + { + get { return $"{OwnerName}/{ProjectName}"; } + } + + public bool IsAvailable { get; private set; } + + public async Task MarkBuildAsPendingAsync(string context, string description) + { + UpdateStatus(GitHubStatusState.Pending, context, description); + } + + public async Task MarkBuildAsFailedAsync(string context, string description) + { + UpdateStatus(GitHubStatusState.Failure, context, description); + } + + public async Task MarkBuildAsSucceededAsync(string context, string description) + { + UpdateStatus(GitHubStatusState.Success, context, description); + } + + private void UpdateStatus(GitHubStatusState state, string context, string description) + { + // Disabled for now + return; + + if (!IsAvailable) + { + return; + } + + BuildContext.CakeContext.Information("Updating GitHub status to '{0}' | '{1}'", state, description); + + var commitSha = BuildContext.General.Repository.CommitId; + + BuildContext.CakeContext.GitHubStatus(UserName, ApiKey, OwnerName, ProjectName, commitSha, new GitHubStatusSettings + { + State = state, + TargetUrl = null,// "url-to-build-server", + Description = description, + Context = $"Cake - {context}" + }); + } } \ No newline at end of file diff --git a/deployment/cake/sourcecontrol.cake b/deployment/cake/sourcecontrol.cake index 9c7b5279..49709a38 100644 --- a/deployment/cake/sourcecontrol.cake +++ b/deployment/cake/sourcecontrol.cake @@ -1,84 +1,84 @@ -// Customize this file when using a different source controls -#l "sourcecontrol-github.cake" - -//------------------------------------------------------------- - -public interface ISourceControl -{ - Task MarkBuildAsPendingAsync(string context, string description); - Task MarkBuildAsFailedAsync(string context, string description); - Task MarkBuildAsSucceededAsync(string context, string description); -} - -//------------------------------------------------------------- - -public class SourceControlIntegration : IntegrationBase -{ - private readonly List _sourceControls = new List(); - - public SourceControlIntegration(BuildContext buildContext) - : base(buildContext) - { - _sourceControls.Add(new GitHubSourceControl(buildContext)); - } - - public async Task MarkBuildAsPendingAsync(string context, string description = null) - { - BuildContext.CakeContext.LogSeparator($"Marking build as pending: '{description ?? string.Empty}'"); - - context = context ?? "default"; - description = description ?? "Build pending"; - - foreach (var sourceControl in _sourceControls) - { - try - { - await sourceControl.MarkBuildAsPendingAsync(context, description); - } - catch (Exception ex) - { - BuildContext.CakeContext.Warning($"Failed to update status: {ex.Message}"); - } - } - } - - public async Task MarkBuildAsFailedAsync(string context, string description = null) - { - BuildContext.CakeContext.LogSeparator($"Marking build as failed: '{description ?? string.Empty}'"); - - context = context ?? "default"; - description = description ?? "Build failed"; - - foreach (var sourceControl in _sourceControls) - { - try - { - await sourceControl.MarkBuildAsFailedAsync(context, description); - } - catch (Exception ex) - { - BuildContext.CakeContext.Warning($"Failed to update status: {ex.Message}"); - } - } - } - - public async Task MarkBuildAsSucceededAsync(string context, string description = null) - { - BuildContext.CakeContext.LogSeparator($"Marking build as succeeded: '{description ?? string.Empty}'"); - - context = context ?? "default"; - description = description ?? "Build succeeded"; - - foreach (var sourceControl in _sourceControls) - { - try - { - await sourceControl.MarkBuildAsSucceededAsync(context, description); - } - catch (Exception ex) - { - BuildContext.CakeContext.Warning($"Failed to update status: {ex.Message}"); - } - } - } +// Customize this file when using a different source controls +#l "sourcecontrol-github.cake" + +//------------------------------------------------------------- + +public interface ISourceControl +{ + Task MarkBuildAsPendingAsync(string context, string description); + Task MarkBuildAsFailedAsync(string context, string description); + Task MarkBuildAsSucceededAsync(string context, string description); +} + +//------------------------------------------------------------- + +public class SourceControlIntegration : IntegrationBase +{ + private readonly List _sourceControls = new List(); + + public SourceControlIntegration(BuildContext buildContext) + : base(buildContext) + { + _sourceControls.Add(new GitHubSourceControl(buildContext)); + } + + public async Task MarkBuildAsPendingAsync(string context, string description = null) + { + BuildContext.CakeContext.LogSeparator($"Marking build as pending: '{description ?? string.Empty}'"); + + context = context ?? "default"; + description = description ?? "Build pending"; + + foreach (var sourceControl in _sourceControls) + { + try + { + await sourceControl.MarkBuildAsPendingAsync(context, description); + } + catch (Exception ex) + { + BuildContext.CakeContext.Warning($"Failed to update status: {ex.Message}"); + } + } + } + + public async Task MarkBuildAsFailedAsync(string context, string description = null) + { + BuildContext.CakeContext.LogSeparator($"Marking build as failed: '{description ?? string.Empty}'"); + + context = context ?? "default"; + description = description ?? "Build failed"; + + foreach (var sourceControl in _sourceControls) + { + try + { + await sourceControl.MarkBuildAsFailedAsync(context, description); + } + catch (Exception ex) + { + BuildContext.CakeContext.Warning($"Failed to update status: {ex.Message}"); + } + } + } + + public async Task MarkBuildAsSucceededAsync(string context, string description = null) + { + BuildContext.CakeContext.LogSeparator($"Marking build as succeeded: '{description ?? string.Empty}'"); + + context = context ?? "default"; + description = description ?? "Build succeeded"; + + foreach (var sourceControl in _sourceControls) + { + try + { + await sourceControl.MarkBuildAsSucceededAsync(context, description); + } + catch (Exception ex) + { + BuildContext.CakeContext.Warning($"Failed to update status: {ex.Message}"); + } + } + } } \ No newline at end of file diff --git a/deployment/cake/tasks.cake b/deployment/cake/tasks.cake index 3c7f22d6..987220f7 100644 --- a/deployment/cake/tasks.cake +++ b/deployment/cake/tasks.cake @@ -1,826 +1,826 @@ -#pragma warning disable CS1998 - -#l "lib-generic.cake" -#l "lib-logging.cake" -#l "lib-msbuild.cake" -#l "lib-nuget.cake" -#l "lib-signing.cake" -#l "lib-sourcelink.cake" -#l "issuetrackers.cake" -#l "installers.cake" -#l "sourcecontrol.cake" -#l "notifications.cake" -#l "generic-tasks.cake" -#l "apps-uwp-tasks.cake" -#l "apps-web-tasks.cake" -#l "apps-wpf-tasks.cake" -#l "codesigning-tasks.cake" -#l "components-tasks.cake" -#l "dependencies-tasks.cake" -#l "tools-tasks.cake" -#l "docker-tasks.cake" -#l "github-pages-tasks.cake" -#l "vsextensions-tasks.cake" -#l "tests.cake" -#l "templates-tasks.cake" - -#addin "nuget:?package=Cake.FileHelpers&version=6.1.3" -#addin "nuget:?package=Cake.Sonar&version=1.1.32" -#addin "nuget:?package=MagicChunks&version=2.0.0.119" -#addin "nuget:?package=Newtonsoft.Json&version=13.0.3" - -// Note: the SonarQube tool must be installed as a global .NET tool. If you are getting issues like this: -// -// The SonarScanner for MSBuild integration failed: [...] was unable to collect the required information about your projects. -// -// It probably means the tool is not correctly installed. -// `dotnet tool install --global dotnet-sonarscanner --ignore-failed-sources` -//#tool "nuget:?package=MSBuild.SonarQube.Runner.Tool&version=4.8.0" -#tool "nuget:?package=dotnet-sonarscanner&version=5.13.1" - -//------------------------------------------------------------- -// BACKWARDS COMPATIBILITY CODE - START -//------------------------------------------------------------- - -// Required so we have backwards compatibility, so developers can keep using -// GetBuildServerVariable in build.cake -private BuildServerIntegration _buildServerIntegration = null; - -private BuildServerIntegration GetBuildServerIntegration() -{ - if (_buildServerIntegration is null) - { - _buildServerIntegration = new BuildServerIntegration(Context, Parameters); - } - - return _buildServerIntegration; -} - -public string GetBuildServerVariable(string variableName, string defaultValue = null, bool showValue = false) -{ - var buildServerIntegration = GetBuildServerIntegration(); - return buildServerIntegration.GetVariable(variableName, defaultValue, showValue); -} - -//------------------------------------------------------------- -// BACKWARDS COMPATIBILITY CODE - END -//------------------------------------------------------------- - -//------------------------------------------------------------- -// BUILD CONTEXT -//------------------------------------------------------------- - -public class BuildContext : BuildContextBase -{ - public BuildContext(ICakeContext cakeContext) - : base(cakeContext) - { - Processors = new List(); - AllProjects = new List(); - RegisteredProjects = new List(); - Variables = new Dictionary(); - } - - public List Processors { get; private set; } - public Dictionary Parameters { get; set; } - public Dictionary Variables { get; private set; } - - // Integrations - public BuildServerIntegration BuildServer { get; set; } - public IssueTrackerIntegration IssueTracker { get; set; } - public InstallerIntegration Installer { get; set; } - public NotificationsIntegration Notifications { get; set; } - public SourceControlIntegration SourceControl { get; set; } - public OctopusDeployIntegration OctopusDeploy { get; set; } - - // Contexts - public GeneralContext General { get; set; } - public TestsContext Tests { get; set; } - - public CodeSigningContext CodeSigning { get; set; } - public ComponentsContext Components { get; set; } - public DependenciesContext Dependencies { get; set; } - public DockerImagesContext DockerImages { get; set; } - public GitHubPagesContext GitHubPages { get; set; } - public TemplatesContext Templates { get; set; } - public ToolsContext Tools { get; set; } - public UwpContext Uwp { get; set; } - public VsExtensionsContext VsExtensions { get; set; } - public WebContext Web { get; set; } - public WpfContext Wpf { get; set; } - - public List AllProjects { get; private set; } - public List RegisteredProjects { get; private set; } - - protected override void ValidateContext() - { - } - - protected override void LogStateInfoForContext() - { - } -} - -//------------------------------------------------------------- -// TASKS -//------------------------------------------------------------- - -Setup(setupContext => -{ - setupContext.Information("Running setup of build scripts"); - - var buildContext = new BuildContext(setupContext); - - // Important, set parameters first - buildContext.Parameters = Parameters ?? new Dictionary(); - - setupContext.LogSeparator("Creating integrations"); - - // Important: build server first so other integrations can read values from config - buildContext.BuildServer = GetBuildServerIntegration(); - buildContext.BuildServer.SetBuildContext(buildContext); - - setupContext.LogSeparator("Creating build context"); - - buildContext.General = InitializeGeneralContext(buildContext, buildContext); - buildContext.Tests = InitializeTestsContext(buildContext, buildContext); - - buildContext.CodeSigning = InitializeCodeSigningContext(buildContext, buildContext); - buildContext.Components = InitializeComponentsContext(buildContext, buildContext); - buildContext.Dependencies = InitializeDependenciesContext(buildContext, buildContext); - buildContext.DockerImages = InitializeDockerImagesContext(buildContext, buildContext); - buildContext.GitHubPages = InitializeGitHubPagesContext(buildContext, buildContext); - buildContext.Templates = InitializeTemplatesContext(buildContext, buildContext); - buildContext.Tools = InitializeToolsContext(buildContext, buildContext); - buildContext.Uwp = InitializeUwpContext(buildContext, buildContext); - buildContext.VsExtensions = InitializeVsExtensionsContext(buildContext, buildContext); - buildContext.Web = InitializeWebContext(buildContext, buildContext); - buildContext.Wpf = InitializeWpfContext(buildContext, buildContext); - - // Other integrations last - buildContext.IssueTracker = new IssueTrackerIntegration(buildContext); - buildContext.Installer = new InstallerIntegration(buildContext); - buildContext.Notifications = new NotificationsIntegration(buildContext); - buildContext.OctopusDeploy = new OctopusDeployIntegration(buildContext); - buildContext.SourceControl = new SourceControlIntegration(buildContext); - - setupContext.LogSeparator("Validating build context"); - - buildContext.Validate(); - - setupContext.LogSeparator("Creating processors"); - - // Note: always put templates and dependencies processor first (it's a dependency after all) - buildContext.Processors.Add(new TemplatesProcessor(buildContext)); - buildContext.Processors.Add(new DependenciesProcessor(buildContext)); - buildContext.Processors.Add(new ComponentsProcessor(buildContext)); - buildContext.Processors.Add(new DockerImagesProcessor(buildContext)); - buildContext.Processors.Add(new GitHubPagesProcessor(buildContext)); - buildContext.Processors.Add(new ToolsProcessor(buildContext)); - buildContext.Processors.Add(new UwpProcessor(buildContext)); - buildContext.Processors.Add(new VsExtensionsProcessor(buildContext)); - buildContext.Processors.Add(new WebProcessor(buildContext)); - buildContext.Processors.Add(new WpfProcessor(buildContext)); - // !!! Note: we add test projects *after* preparing all the other processors, see Prepare task !!! - - setupContext.LogSeparator("Registering variables for templates"); - - // Preparing variables for templates - buildContext.Variables["GitVersion_MajorMinorPatch"] = buildContext.General.Version.MajorMinorPatch; - buildContext.Variables["GitVersion_FullSemVer"] = buildContext.General.Version.FullSemVer; - buildContext.Variables["GitVersion_NuGetVersion"] = buildContext.General.Version.NuGet; - - setupContext.LogSeparator("Build context is ready, displaying state info"); - - buildContext.LogStateInfo(); - - return buildContext; -}); - -//------------------------------------------------------------- - -Task("Initialize") - .Does(async buildContext => -{ - await buildContext.BuildServer.BeforeInitializeAsync(); - - buildContext.CakeContext.LogSeparator("Writing special values back to build server"); - - var displayVersion = buildContext.General.Version.FullSemVer; - if (buildContext.General.IsCiBuild) - { - displayVersion += " ci"; - } - - await buildContext.BuildServer.SetVersionAsync(displayVersion); - - var variablesToUpdate = new Dictionary(); - variablesToUpdate["channel"] = buildContext.Wpf.Channel; - variablesToUpdate["publishType"] = buildContext.General.Solution.PublishType.ToString(); - variablesToUpdate["isAlphaBuild"] = buildContext.General.IsAlphaBuild.ToString(); - variablesToUpdate["isBetaBuild"] = buildContext.General.IsBetaBuild.ToString(); - variablesToUpdate["isOfficialBuild"] = buildContext.General.IsOfficialBuild.ToString(); - - // Also write back versioning (then it can be cached), "worst case scenario" it's writing back the same versions - variablesToUpdate["GitVersion_MajorMinorPatch"] = buildContext.General.Version.MajorMinorPatch; - variablesToUpdate["GitVersion_FullSemVer"] = buildContext.General.Version.FullSemVer; - variablesToUpdate["GitVersion_NuGetVersion"] = buildContext.General.Version.NuGet; - variablesToUpdate["GitVersion_CommitsSinceVersionSource"] = buildContext.General.Version.CommitsSinceVersionSource; - - foreach (var variableToUpdate in variablesToUpdate) - { - await buildContext.BuildServer.SetVariableAsync(variableToUpdate.Key, variableToUpdate.Value); - } - - await buildContext.BuildServer.AfterInitializeAsync(); -}); - -//------------------------------------------------------------- - -Task("Prepare") - .Does(async buildContext => -{ - // Add all projects to registered projects - buildContext.RegisteredProjects.AddRange(buildContext.Components.Items); - buildContext.RegisteredProjects.AddRange(buildContext.Dependencies.Items); - buildContext.RegisteredProjects.AddRange(buildContext.DockerImages.Items); - buildContext.RegisteredProjects.AddRange(buildContext.GitHubPages.Items); - buildContext.RegisteredProjects.AddRange(buildContext.Tests.Items); - buildContext.RegisteredProjects.AddRange(buildContext.Tools.Items); - buildContext.RegisteredProjects.AddRange(buildContext.Uwp.Items); - buildContext.RegisteredProjects.AddRange(buildContext.VsExtensions.Items); - buildContext.RegisteredProjects.AddRange(buildContext.Web.Items); - buildContext.RegisteredProjects.AddRange(buildContext.Wpf.Items); - - await buildContext.BuildServer.BeforePrepareAsync(); - - foreach (var processor in buildContext.Processors) - { - if (processor is DependenciesProcessor) - { - // Process later - continue; - } - - await processor.PrepareAsync(); - } - - // Now add all projects, but dependencies first & tests last, which will be added at the end - buildContext.AllProjects.AddRange(buildContext.Components.Items); - buildContext.AllProjects.AddRange(buildContext.DockerImages.Items); - buildContext.AllProjects.AddRange(buildContext.GitHubPages.Items); - buildContext.AllProjects.AddRange(buildContext.Tools.Items); - buildContext.AllProjects.AddRange(buildContext.Uwp.Items); - buildContext.AllProjects.AddRange(buildContext.VsExtensions.Items); - buildContext.AllProjects.AddRange(buildContext.Web.Items); - buildContext.AllProjects.AddRange(buildContext.Wpf.Items); - - buildContext.CakeContext.LogSeparator("Final check which test projects should be included (1/2)"); - - // Once we know all the projects that will be built, we calculate which - // test projects need to be built as well - - var testProcessor = new TestProcessor(buildContext); - await testProcessor.PrepareAsync(); - buildContext.Processors.Add(testProcessor); - - buildContext.CakeContext.Information(string.Empty); - buildContext.CakeContext.Information($"Found '{buildContext.Tests.Items.Count}' test projects"); - - foreach (var test in buildContext.Tests.Items) - { - buildContext.CakeContext.Information($" - {test}"); - } - - buildContext.AllProjects.AddRange(buildContext.Tests.Items); - - buildContext.CakeContext.LogSeparator("Final check which dependencies should be included (2/2)"); - - // Now we really really determined all projects to build, we can check the dependencies - var dependenciesProcessor = (DependenciesProcessor)buildContext.Processors.First(x => x is DependenciesProcessor); - await dependenciesProcessor.PrepareAsync(); - - buildContext.CakeContext.Information(string.Empty); - buildContext.CakeContext.Information($"Found '{buildContext.Dependencies.Items.Count}' dependencies"); - - foreach (var dependency in buildContext.Dependencies.Items) - { - buildContext.CakeContext.Information($" - {dependency}"); - } - - // Add to the front, these are dependencies after all - buildContext.AllProjects.InsertRange(0, buildContext.Dependencies.Items); - - // Now we have the full collection, distinct - var allProjects = buildContext.AllProjects.ToArray(); - - buildContext.AllProjects.Clear(); - buildContext.AllProjects.AddRange(allProjects.Distinct()); - - buildContext.CakeContext.LogSeparator("Final projects to process"); - - foreach (var item in buildContext.AllProjects.ToList()) - { - buildContext.CakeContext.Information($"- {item}"); - } - - await buildContext.BuildServer.AfterPrepareAsync(); -}); - -//------------------------------------------------------------- - -Task("UpdateInfo") - .IsDependentOn("Prepare") - .Does(async buildContext => -{ - await buildContext.BuildServer.BeforeUpdateInfoAsync(); - - UpdateSolutionAssemblyInfo(buildContext); - - foreach (var processor in buildContext.Processors) - { - await processor.UpdateInfoAsync(); - } - - await buildContext.BuildServer.AfterUpdateInfoAsync(); -}); - -//------------------------------------------------------------- - -Task("Build") - .IsDependentOn("Clean") - .IsDependentOn("RestorePackages") - .IsDependentOn("UpdateInfo") - //.IsDependentOn("VerifyDependencies") - .IsDependentOn("CleanupCode") - .Does(async buildContext => -{ - await buildContext.BuildServer.BeforeBuildAsync(); - - await buildContext.SourceControl.MarkBuildAsPendingAsync("Build"); - - var sonarUrl = buildContext.General.SonarQube.Url; - - var enableSonar = !buildContext.General.SonarQube.IsDisabled && - buildContext.General.IsCiBuild && // Only build on CI (all projects need to be included) - !string.IsNullOrWhiteSpace(sonarUrl); - if (enableSonar) - { - var sonarSettings = new SonarBeginSettings - { - // SonarQube info - Url = sonarUrl, - - // Project info - Key = buildContext.General.SonarQube.Project, - Version = buildContext.General.Version.FullSemVer, - - // Use core clr version of SonarQube - UseCoreClr = true, - - // Minimize extreme logging - Verbose = false, - Silent = true, - - // Support waiting for the quality gate - ArgumentCustomization = args => args - .Append("/d:sonar.qualitygate.wait=true") - }; - - if (!string.IsNullOrWhiteSpace(buildContext.General.SonarQube.Organization)) - { - sonarSettings.Organization = buildContext.General.SonarQube.Organization; - } - - if (!string.IsNullOrWhiteSpace(buildContext.General.SonarQube.Username)) - { - sonarSettings.Login = buildContext.General.SonarQube.Username; - } - - if (!string.IsNullOrWhiteSpace(buildContext.General.SonarQube.Password)) - { - sonarSettings.Password = buildContext.General.SonarQube.Password; - } - - // see https://cakebuild.net/api/Cake.Sonar/SonarBeginSettings/ for more information on - // what to set for SonarCloud - - // Branch only works with the branch plugin. Documentation A says it's outdated, but - // B still mentions it: - // A: https://docs.sonarqube.org/latest/branches/overview/ - // B: https://docs.sonarqube.org/latest/analysis/analysis-parameters/ - if (buildContext.General.SonarQube.SupportBranches) - { - // TODO: How to support PR? - sonarSettings.Branch = buildContext.General.Repository.BranchName; - } - - Information("Beginning SonarQube"); - - SonarBegin(sonarSettings); - } - else - { - Information("Skipping Sonar integration since url is not specified or it has been explicitly disabled"); - } - - try - { - if (buildContext.General.Solution.BuildSolution) - { - BuildSolution(buildContext); - } - - foreach (var processor in buildContext.Processors) - { - if (processor is TestProcessor) - { - // Build test projects *after* SonarQube (not part of SQ analysis) - continue; - } - - await processor.BuildAsync(); - } - } - finally - { - if (enableSonar) - { - try - { - await buildContext.SourceControl.MarkBuildAsPendingAsync("SonarQube"); - - var sonarEndSettings = new SonarEndSettings - { - // Use core clr version of SonarQube - UseCoreClr = true - }; - - if (!string.IsNullOrWhiteSpace(buildContext.General.SonarQube.Username)) - { - sonarEndSettings.Login = buildContext.General.SonarQube.Username; - } - - if (!string.IsNullOrWhiteSpace(buildContext.General.SonarQube.Password)) - { - sonarEndSettings.Password = buildContext.General.SonarQube.Password; - } - - Information("Ending SonarQube"); - - SonarEnd(sonarEndSettings); - - await buildContext.SourceControl.MarkBuildAsSucceededAsync("SonarQube"); - } - catch (Exception) - { - var projectSpecificSonarUrl = $"{sonarUrl}/dashboard?id={buildContext.General.SonarQube.Project}"; - - if (buildContext.General.SonarQube.SupportBranches) - { - projectSpecificSonarUrl += $"&branch={buildContext.General.Repository.BranchName}"; - } - - var failedDescription = $"SonarQube failed, please visit '{projectSpecificSonarUrl}' for more details"; - - await buildContext.SourceControl.MarkBuildAsFailedAsync("SonarQube", failedDescription); - - throw; - } - } - } - - var testProcessor = buildContext.Processors.FirstOrDefault(x => x is TestProcessor) as TestProcessor; - if (testProcessor is not null) - { - // Build test projects *after* SonarQube (not part of SQ analysis). Unfortunately, because of this, we cannot yet mark - // the build as succeeded once we end the SQ session. Therefore, if SQ fails, both the SQ *and* build checks - // will be marked as failed if SQ fails. - await testProcessor.BuildAsync(); - } - - await buildContext.SourceControl.MarkBuildAsSucceededAsync("Build"); - - Information("Completed build for version '{0}'", buildContext.General.Version.NuGet); - - await buildContext.BuildServer.AfterBuildAsync(); -}) -.OnError(async (ex, buildContext) => -{ - await buildContext.SourceControl.MarkBuildAsFailedAsync("Build"); - - await buildContext.BuildServer.OnBuildFailedAsync(); - - throw ex; -}); - -//------------------------------------------------------------- - -Task("Test") - .IsDependentOn("Prepare") - // Note: no dependency on 'build' since we might have already built the solution - .Does(async buildContext => -{ - await buildContext.BuildServer.BeforeTestAsync(); - - await buildContext.SourceControl.MarkBuildAsPendingAsync("Test"); - - if (buildContext.Tests.Items.Count > 0) - { - // If docker is involved, login to all registries for the unit / integration tests - var dockerRegistries = new HashSet(); - var dockerProcessor = (DockerImagesProcessor)buildContext.Processors.Single(x => x is DockerImagesProcessor); - - try - { - foreach (var dockerImage in buildContext.DockerImages.Items) - { - var dockerRegistryUrl = dockerProcessor.GetDockerRegistryUrl(dockerImage); - if (dockerRegistries.Contains(dockerRegistryUrl)) - { - continue; - } - - // Note: we are logging in each time because the registry might be different per container - Information($"Logging in to docker @ '{dockerRegistryUrl}'"); - - dockerRegistries.Add(dockerRegistryUrl); - - var dockerRegistryUserName = dockerProcessor.GetDockerRegistryUserName(dockerImage); - var dockerRegistryPassword = dockerProcessor.GetDockerRegistryPassword(dockerImage); - - var dockerLoginSettings = new DockerRegistryLoginSettings - { - Username = dockerRegistryUserName, - Password = dockerRegistryPassword - }; - - DockerLogin(dockerLoginSettings, dockerRegistryUrl); - } - - // Always run all unit test projects before throwing - var failed = false; - - foreach (var testProject in buildContext.Tests.Items) - { - buildContext.CakeContext.LogSeparator("Running tests for '{0}'", testProject); - - try - { - RunUnitTests(buildContext, testProject); - } - catch (Exception ex) - { - failed = true; - - Warning($"Running tests for '{testProject}' caused an exception: {ex.Message}"); - } - } - - if (failed) - { - throw new Exception("At least 1 test project failed execution"); - } - } - finally - { - foreach (var dockerRegistry in dockerRegistries) - { - try - { - Information($"Logging out of docker @ '{dockerRegistry}'"); - - var dockerLogoutSettings = new DockerRegistryLogoutSettings - { - }; - - DockerLogout(dockerLogoutSettings, dockerRegistry); - } - catch (Exception ex) - { - Warning($"Failed to logout from docker: {ex.Message}"); - } - } - } - } - - await buildContext.SourceControl.MarkBuildAsSucceededAsync("Test"); - - Information("Completed tests for version '{0}'", buildContext.General.Version.NuGet); - - await buildContext.BuildServer.AfterTestAsync(); -}) -.OnError(async (ex, buildContext) => -{ - await buildContext.SourceControl.MarkBuildAsFailedAsync("Test"); - - await buildContext.BuildServer.OnTestFailedAsync(); - - throw ex; -}); - -//------------------------------------------------------------- - -Task("Package") - // Make sure to update info so our SolutionAssemblyInfo.cs is up to date - .IsDependentOn("UpdateInfo") - // Note: no dependency on 'build' since we might have already built the solution - // Make sure we have the temporary "project.assets.json" in case we need to package with Visual Studio - .IsDependentOn("RestorePackages") - // Make sure to update if we are running on a new agent so we can sign nuget packages - .IsDependentOn("UpdateNuGet") - .IsDependentOn("CodeSign") - .Does(async buildContext => -{ - await buildContext.BuildServer.BeforePackageAsync(); - - foreach (var processor in buildContext.Processors) - { - await processor.PackageAsync(); - } - - Information("Completed packaging for version '{0}'", buildContext.General.Version.NuGet); - - await buildContext.BuildServer.AfterPackageAsync(); -}); - -//------------------------------------------------------------- - -Task("PackageLocal") - .IsDependentOn("Package") - .Does(buildContext => -{ - // Note: no build server integration calls since this is *local* - - // For now only package components, we might need to move this to components-tasks.cake in the future - if (buildContext.Components.Items.Count == 0 && - buildContext.Tools.Items.Count == 0) - { - return; - } - - var localPackagesDirectory = buildContext.General.NuGet.LocalPackagesDirectory; - - Information("Copying build artifacts to '{0}'", localPackagesDirectory); - - CreateDirectory(localPackagesDirectory); - - foreach (var component in buildContext.Components.Items) - { - try - { - Information("Copying build artifact for '{0}'", component); - - var sourceFile = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, - $"{component}.{buildContext.General.Version.NuGet}.nupkg"); - - CopyFiles(new [] { sourceFile }, localPackagesDirectory); - } - catch (Exception) - { - // Ignore - Warning("Failed to copy build artifacts for '{0}'", component); - } - } - - Information("Copied build artifacts for version '{0}'", buildContext.General.Version.NuGet); -}); - -//------------------------------------------------------------- - -Task("Deploy") - // Note: no dependency on 'package' since we might have already packaged the solution - // Make sure we have the temporary "project.assets.json" in case we need to package with Visual Studio - .IsDependentOn("RestorePackages") - .Does(async buildContext => -{ - await buildContext.BuildServer.BeforeDeployAsync(); - - foreach (var processor in buildContext.Processors) - { - await processor.DeployAsync(); - } - - await buildContext.BuildServer.AfterDeployAsync(); -}); - -//------------------------------------------------------------- - -Task("Finalize") - // Note: no dependency on 'deploy' since we might have already deployed the solution - .Does(async buildContext => -{ - await buildContext.BuildServer.BeforeFinalizeAsync(); - - Information("Finalizing release '{0}'", buildContext.General.Version.FullSemVer); - - foreach (var processor in buildContext.Processors) - { - await processor.FinalizeAsync(); - } - - if (buildContext.General.IsOfficialBuild) - { - await buildContext.BuildServer.PinBuildAsync("Official build"); - } - - await buildContext.IssueTracker.CreateAndReleaseVersionAsync(); - - await buildContext.BuildServer.AfterFinalizeAsync(); -}); - -//------------------------------------------------------------- -// Wrapper tasks since we don't want to add "Build" as a -// dependency to "Package" because we want to run in multiple -// stages -//------------------------------------------------------------- - -Task("BuildAndTest") - .IsDependentOn("Initialize") - .IsDependentOn("Build") - .IsDependentOn("Test"); - -//------------------------------------------------------------- - -Task("BuildAndPackage") - .IsDependentOn("Initialize") - .IsDependentOn("Build") - .IsDependentOn("Test") - .IsDependentOn("Package"); - -//------------------------------------------------------------- - -Task("BuildAndPackageLocal") - .IsDependentOn("Initialize") - .IsDependentOn("Build") - //.IsDependentOn("Test") // Note: don't test for performance on local builds - .IsDependentOn("PackageLocal"); - -//------------------------------------------------------------- - -Task("BuildAndDeploy") - .IsDependentOn("Initialize") - .IsDependentOn("Build") - .IsDependentOn("Test") - .IsDependentOn("Package") - .IsDependentOn("Deploy"); - -//------------------------------------------------------------- - -Task("Default") - .Does(async buildContext => -{ - Error("No target specified, please specify one of the following targets:\n" + - " - Prepare\n" + - " - UpdateInfo\n" + - " - Build\n" + - " - Test\n" + - " - Package\n" + - " - Deploy\n" + - " - Finalize\n\n" + - "or one of the combined ones:\n" + - " - BuildAndTest\n" + - " - BuildAndPackage\n" + - " - BuildAndPackageLocal\n" + - " - BuildAndDeploy\n"); -}); - -//------------------------------------------------------------- -// Test wrappers -//------------------------------------------------------------- - -Task("TestNotifications") - .Does(async buildContext => -{ - await buildContext.Notifications.NotifyAsync("MyProject", "This is a generic test"); - await buildContext.Notifications.NotifyAsync("MyProject", "This is a component test", TargetType.Component); - await buildContext.Notifications.NotifyAsync("MyProject", "This is a docker image test", TargetType.DockerImage); - await buildContext.Notifications.NotifyAsync("MyProject", "This is a web app test", TargetType.WebApp); - await buildContext.Notifications.NotifyAsync("MyProject", "This is a wpf app test", TargetType.WpfApp); - await buildContext.Notifications.NotifyErrorAsync("MyProject", "This is an error"); -}); - -//------------------------------------------------------------- - -Task("TestSourceControl") - .Does(async buildContext => -{ - await buildContext.SourceControl.MarkBuildAsPendingAsync("Build"); - - await System.Threading.Tasks.Task.Delay(5 * 1000); - - await buildContext.SourceControl.MarkBuildAsSucceededAsync("Build"); - - await buildContext.SourceControl.MarkBuildAsPendingAsync("Test"); - - await System.Threading.Tasks.Task.Delay(5 * 1000); - - await buildContext.SourceControl.MarkBuildAsSucceededAsync("Test"); -}); - -//------------------------------------------------------------- -// ACTUAL RUNNER - MUST BE DEFINED AT THE BOTTOM -//------------------------------------------------------------- - -var localTarget = GetBuildServerVariable("Target", "Default", showValue: true); -RunTarget(localTarget); +#pragma warning disable CS1998 + +#l "lib-generic.cake" +#l "lib-logging.cake" +#l "lib-msbuild.cake" +#l "lib-nuget.cake" +#l "lib-signing.cake" +#l "lib-sourcelink.cake" +#l "issuetrackers.cake" +#l "installers.cake" +#l "sourcecontrol.cake" +#l "notifications.cake" +#l "generic-tasks.cake" +#l "apps-uwp-tasks.cake" +#l "apps-web-tasks.cake" +#l "apps-wpf-tasks.cake" +#l "codesigning-tasks.cake" +#l "components-tasks.cake" +#l "dependencies-tasks.cake" +#l "tools-tasks.cake" +#l "docker-tasks.cake" +#l "github-pages-tasks.cake" +#l "vsextensions-tasks.cake" +#l "tests.cake" +#l "templates-tasks.cake" + +#addin "nuget:?package=Cake.FileHelpers&version=6.1.3" +#addin "nuget:?package=Cake.Sonar&version=1.1.32" +#addin "nuget:?package=MagicChunks&version=2.0.0.119" +#addin "nuget:?package=Newtonsoft.Json&version=13.0.3" + +// Note: the SonarQube tool must be installed as a global .NET tool. If you are getting issues like this: +// +// The SonarScanner for MSBuild integration failed: [...] was unable to collect the required information about your projects. +// +// It probably means the tool is not correctly installed. +// `dotnet tool install --global dotnet-sonarscanner --ignore-failed-sources` +//#tool "nuget:?package=MSBuild.SonarQube.Runner.Tool&version=4.8.0" +#tool "nuget:?package=dotnet-sonarscanner&version=5.14.0" + +//------------------------------------------------------------- +// BACKWARDS COMPATIBILITY CODE - START +//------------------------------------------------------------- + +// Required so we have backwards compatibility, so developers can keep using +// GetBuildServerVariable in build.cake +private BuildServerIntegration _buildServerIntegration = null; + +private BuildServerIntegration GetBuildServerIntegration() +{ + if (_buildServerIntegration is null) + { + _buildServerIntegration = new BuildServerIntegration(Context, Parameters); + } + + return _buildServerIntegration; +} + +public string GetBuildServerVariable(string variableName, string defaultValue = null, bool showValue = false) +{ + var buildServerIntegration = GetBuildServerIntegration(); + return buildServerIntegration.GetVariable(variableName, defaultValue, showValue); +} + +//------------------------------------------------------------- +// BACKWARDS COMPATIBILITY CODE - END +//------------------------------------------------------------- + +//------------------------------------------------------------- +// BUILD CONTEXT +//------------------------------------------------------------- + +public class BuildContext : BuildContextBase +{ + public BuildContext(ICakeContext cakeContext) + : base(cakeContext) + { + Processors = new List(); + AllProjects = new List(); + RegisteredProjects = new List(); + Variables = new Dictionary(); + } + + public List Processors { get; private set; } + public Dictionary Parameters { get; set; } + public Dictionary Variables { get; private set; } + + // Integrations + public BuildServerIntegration BuildServer { get; set; } + public IssueTrackerIntegration IssueTracker { get; set; } + public InstallerIntegration Installer { get; set; } + public NotificationsIntegration Notifications { get; set; } + public SourceControlIntegration SourceControl { get; set; } + public OctopusDeployIntegration OctopusDeploy { get; set; } + + // Contexts + public GeneralContext General { get; set; } + public TestsContext Tests { get; set; } + + public CodeSigningContext CodeSigning { get; set; } + public ComponentsContext Components { get; set; } + public DependenciesContext Dependencies { get; set; } + public DockerImagesContext DockerImages { get; set; } + public GitHubPagesContext GitHubPages { get; set; } + public TemplatesContext Templates { get; set; } + public ToolsContext Tools { get; set; } + public UwpContext Uwp { get; set; } + public VsExtensionsContext VsExtensions { get; set; } + public WebContext Web { get; set; } + public WpfContext Wpf { get; set; } + + public List AllProjects { get; private set; } + public List RegisteredProjects { get; private set; } + + protected override void ValidateContext() + { + } + + protected override void LogStateInfoForContext() + { + } +} + +//------------------------------------------------------------- +// TASKS +//------------------------------------------------------------- + +Setup(setupContext => +{ + setupContext.Information("Running setup of build scripts"); + + var buildContext = new BuildContext(setupContext); + + // Important, set parameters first + buildContext.Parameters = Parameters ?? new Dictionary(); + + setupContext.LogSeparator("Creating integrations"); + + // Important: build server first so other integrations can read values from config + buildContext.BuildServer = GetBuildServerIntegration(); + buildContext.BuildServer.SetBuildContext(buildContext); + + setupContext.LogSeparator("Creating build context"); + + buildContext.General = InitializeGeneralContext(buildContext, buildContext); + buildContext.Tests = InitializeTestsContext(buildContext, buildContext); + + buildContext.CodeSigning = InitializeCodeSigningContext(buildContext, buildContext); + buildContext.Components = InitializeComponentsContext(buildContext, buildContext); + buildContext.Dependencies = InitializeDependenciesContext(buildContext, buildContext); + buildContext.DockerImages = InitializeDockerImagesContext(buildContext, buildContext); + buildContext.GitHubPages = InitializeGitHubPagesContext(buildContext, buildContext); + buildContext.Templates = InitializeTemplatesContext(buildContext, buildContext); + buildContext.Tools = InitializeToolsContext(buildContext, buildContext); + buildContext.Uwp = InitializeUwpContext(buildContext, buildContext); + buildContext.VsExtensions = InitializeVsExtensionsContext(buildContext, buildContext); + buildContext.Web = InitializeWebContext(buildContext, buildContext); + buildContext.Wpf = InitializeWpfContext(buildContext, buildContext); + + // Other integrations last + buildContext.IssueTracker = new IssueTrackerIntegration(buildContext); + buildContext.Installer = new InstallerIntegration(buildContext); + buildContext.Notifications = new NotificationsIntegration(buildContext); + buildContext.OctopusDeploy = new OctopusDeployIntegration(buildContext); + buildContext.SourceControl = new SourceControlIntegration(buildContext); + + setupContext.LogSeparator("Validating build context"); + + buildContext.Validate(); + + setupContext.LogSeparator("Creating processors"); + + // Note: always put templates and dependencies processor first (it's a dependency after all) + buildContext.Processors.Add(new TemplatesProcessor(buildContext)); + buildContext.Processors.Add(new DependenciesProcessor(buildContext)); + buildContext.Processors.Add(new ComponentsProcessor(buildContext)); + buildContext.Processors.Add(new DockerImagesProcessor(buildContext)); + buildContext.Processors.Add(new GitHubPagesProcessor(buildContext)); + buildContext.Processors.Add(new ToolsProcessor(buildContext)); + buildContext.Processors.Add(new UwpProcessor(buildContext)); + buildContext.Processors.Add(new VsExtensionsProcessor(buildContext)); + buildContext.Processors.Add(new WebProcessor(buildContext)); + buildContext.Processors.Add(new WpfProcessor(buildContext)); + // !!! Note: we add test projects *after* preparing all the other processors, see Prepare task !!! + + setupContext.LogSeparator("Registering variables for templates"); + + // Preparing variables for templates + buildContext.Variables["GitVersion_MajorMinorPatch"] = buildContext.General.Version.MajorMinorPatch; + buildContext.Variables["GitVersion_FullSemVer"] = buildContext.General.Version.FullSemVer; + buildContext.Variables["GitVersion_NuGetVersion"] = buildContext.General.Version.NuGet; + + setupContext.LogSeparator("Build context is ready, displaying state info"); + + buildContext.LogStateInfo(); + + return buildContext; +}); + +//------------------------------------------------------------- + +Task("Initialize") + .Does(async buildContext => +{ + await buildContext.BuildServer.BeforeInitializeAsync(); + + buildContext.CakeContext.LogSeparator("Writing special values back to build server"); + + var displayVersion = buildContext.General.Version.FullSemVer; + if (buildContext.General.IsCiBuild) + { + displayVersion += " ci"; + } + + await buildContext.BuildServer.SetVersionAsync(displayVersion); + + var variablesToUpdate = new Dictionary(); + variablesToUpdate["channel"] = buildContext.Wpf.Channel; + variablesToUpdate["publishType"] = buildContext.General.Solution.PublishType.ToString(); + variablesToUpdate["isAlphaBuild"] = buildContext.General.IsAlphaBuild.ToString(); + variablesToUpdate["isBetaBuild"] = buildContext.General.IsBetaBuild.ToString(); + variablesToUpdate["isOfficialBuild"] = buildContext.General.IsOfficialBuild.ToString(); + + // Also write back versioning (then it can be cached), "worst case scenario" it's writing back the same versions + variablesToUpdate["GitVersion_MajorMinorPatch"] = buildContext.General.Version.MajorMinorPatch; + variablesToUpdate["GitVersion_FullSemVer"] = buildContext.General.Version.FullSemVer; + variablesToUpdate["GitVersion_NuGetVersion"] = buildContext.General.Version.NuGet; + variablesToUpdate["GitVersion_CommitsSinceVersionSource"] = buildContext.General.Version.CommitsSinceVersionSource; + + foreach (var variableToUpdate in variablesToUpdate) + { + await buildContext.BuildServer.SetVariableAsync(variableToUpdate.Key, variableToUpdate.Value); + } + + await buildContext.BuildServer.AfterInitializeAsync(); +}); + +//------------------------------------------------------------- + +Task("Prepare") + .Does(async buildContext => +{ + // Add all projects to registered projects + buildContext.RegisteredProjects.AddRange(buildContext.Components.Items); + buildContext.RegisteredProjects.AddRange(buildContext.Dependencies.Items); + buildContext.RegisteredProjects.AddRange(buildContext.DockerImages.Items); + buildContext.RegisteredProjects.AddRange(buildContext.GitHubPages.Items); + buildContext.RegisteredProjects.AddRange(buildContext.Tests.Items); + buildContext.RegisteredProjects.AddRange(buildContext.Tools.Items); + buildContext.RegisteredProjects.AddRange(buildContext.Uwp.Items); + buildContext.RegisteredProjects.AddRange(buildContext.VsExtensions.Items); + buildContext.RegisteredProjects.AddRange(buildContext.Web.Items); + buildContext.RegisteredProjects.AddRange(buildContext.Wpf.Items); + + await buildContext.BuildServer.BeforePrepareAsync(); + + foreach (var processor in buildContext.Processors) + { + if (processor is DependenciesProcessor) + { + // Process later + continue; + } + + await processor.PrepareAsync(); + } + + // Now add all projects, but dependencies first & tests last, which will be added at the end + buildContext.AllProjects.AddRange(buildContext.Components.Items); + buildContext.AllProjects.AddRange(buildContext.DockerImages.Items); + buildContext.AllProjects.AddRange(buildContext.GitHubPages.Items); + buildContext.AllProjects.AddRange(buildContext.Tools.Items); + buildContext.AllProjects.AddRange(buildContext.Uwp.Items); + buildContext.AllProjects.AddRange(buildContext.VsExtensions.Items); + buildContext.AllProjects.AddRange(buildContext.Web.Items); + buildContext.AllProjects.AddRange(buildContext.Wpf.Items); + + buildContext.CakeContext.LogSeparator("Final check which test projects should be included (1/2)"); + + // Once we know all the projects that will be built, we calculate which + // test projects need to be built as well + + var testProcessor = new TestProcessor(buildContext); + await testProcessor.PrepareAsync(); + buildContext.Processors.Add(testProcessor); + + buildContext.CakeContext.Information(string.Empty); + buildContext.CakeContext.Information($"Found '{buildContext.Tests.Items.Count}' test projects"); + + foreach (var test in buildContext.Tests.Items) + { + buildContext.CakeContext.Information($" - {test}"); + } + + buildContext.AllProjects.AddRange(buildContext.Tests.Items); + + buildContext.CakeContext.LogSeparator("Final check which dependencies should be included (2/2)"); + + // Now we really really determined all projects to build, we can check the dependencies + var dependenciesProcessor = (DependenciesProcessor)buildContext.Processors.First(x => x is DependenciesProcessor); + await dependenciesProcessor.PrepareAsync(); + + buildContext.CakeContext.Information(string.Empty); + buildContext.CakeContext.Information($"Found '{buildContext.Dependencies.Items.Count}' dependencies"); + + foreach (var dependency in buildContext.Dependencies.Items) + { + buildContext.CakeContext.Information($" - {dependency}"); + } + + // Add to the front, these are dependencies after all + buildContext.AllProjects.InsertRange(0, buildContext.Dependencies.Items); + + // Now we have the full collection, distinct + var allProjects = buildContext.AllProjects.ToArray(); + + buildContext.AllProjects.Clear(); + buildContext.AllProjects.AddRange(allProjects.Distinct()); + + buildContext.CakeContext.LogSeparator("Final projects to process"); + + foreach (var item in buildContext.AllProjects.ToList()) + { + buildContext.CakeContext.Information($"- {item}"); + } + + await buildContext.BuildServer.AfterPrepareAsync(); +}); + +//------------------------------------------------------------- + +Task("UpdateInfo") + .IsDependentOn("Prepare") + .Does(async buildContext => +{ + await buildContext.BuildServer.BeforeUpdateInfoAsync(); + + UpdateSolutionAssemblyInfo(buildContext); + + foreach (var processor in buildContext.Processors) + { + await processor.UpdateInfoAsync(); + } + + await buildContext.BuildServer.AfterUpdateInfoAsync(); +}); + +//------------------------------------------------------------- + +Task("Build") + .IsDependentOn("Clean") + .IsDependentOn("RestorePackages") + .IsDependentOn("UpdateInfo") + //.IsDependentOn("VerifyDependencies") + .IsDependentOn("CleanupCode") + .Does(async buildContext => +{ + await buildContext.BuildServer.BeforeBuildAsync(); + + await buildContext.SourceControl.MarkBuildAsPendingAsync("Build"); + + var sonarUrl = buildContext.General.SonarQube.Url; + + var enableSonar = !buildContext.General.SonarQube.IsDisabled && + buildContext.General.IsCiBuild && // Only build on CI (all projects need to be included) + !string.IsNullOrWhiteSpace(sonarUrl); + if (enableSonar) + { + var sonarSettings = new SonarBeginSettings + { + // SonarQube info + Url = sonarUrl, + + // Project info + Key = buildContext.General.SonarQube.Project, + Version = buildContext.General.Version.FullSemVer, + + // Use core clr version of SonarQube + UseCoreClr = true, + + // Minimize extreme logging + Verbose = false, + Silent = true, + + // Support waiting for the quality gate + ArgumentCustomization = args => args + .Append("/d:sonar.qualitygate.wait=true") + }; + + if (!string.IsNullOrWhiteSpace(buildContext.General.SonarQube.Organization)) + { + sonarSettings.Organization = buildContext.General.SonarQube.Organization; + } + + if (!string.IsNullOrWhiteSpace(buildContext.General.SonarQube.Username)) + { + sonarSettings.Login = buildContext.General.SonarQube.Username; + } + + if (!string.IsNullOrWhiteSpace(buildContext.General.SonarQube.Password)) + { + sonarSettings.Password = buildContext.General.SonarQube.Password; + } + + // see https://cakebuild.net/api/Cake.Sonar/SonarBeginSettings/ for more information on + // what to set for SonarCloud + + // Branch only works with the branch plugin. Documentation A says it's outdated, but + // B still mentions it: + // A: https://docs.sonarqube.org/latest/branches/overview/ + // B: https://docs.sonarqube.org/latest/analysis/analysis-parameters/ + if (buildContext.General.SonarQube.SupportBranches) + { + // TODO: How to support PR? + sonarSettings.Branch = buildContext.General.Repository.BranchName; + } + + Information("Beginning SonarQube"); + + SonarBegin(sonarSettings); + } + else + { + Information("Skipping Sonar integration since url is not specified or it has been explicitly disabled"); + } + + try + { + if (buildContext.General.Solution.BuildSolution) + { + BuildSolution(buildContext); + } + + foreach (var processor in buildContext.Processors) + { + if (processor is TestProcessor) + { + // Build test projects *after* SonarQube (not part of SQ analysis) + continue; + } + + await processor.BuildAsync(); + } + } + finally + { + if (enableSonar) + { + try + { + await buildContext.SourceControl.MarkBuildAsPendingAsync("SonarQube"); + + var sonarEndSettings = new SonarEndSettings + { + // Use core clr version of SonarQube + UseCoreClr = true + }; + + if (!string.IsNullOrWhiteSpace(buildContext.General.SonarQube.Username)) + { + sonarEndSettings.Login = buildContext.General.SonarQube.Username; + } + + if (!string.IsNullOrWhiteSpace(buildContext.General.SonarQube.Password)) + { + sonarEndSettings.Password = buildContext.General.SonarQube.Password; + } + + Information("Ending SonarQube"); + + SonarEnd(sonarEndSettings); + + await buildContext.SourceControl.MarkBuildAsSucceededAsync("SonarQube"); + } + catch (Exception) + { + var projectSpecificSonarUrl = $"{sonarUrl}/dashboard?id={buildContext.General.SonarQube.Project}"; + + if (buildContext.General.SonarQube.SupportBranches) + { + projectSpecificSonarUrl += $"&branch={buildContext.General.Repository.BranchName}"; + } + + var failedDescription = $"SonarQube failed, please visit '{projectSpecificSonarUrl}' for more details"; + + await buildContext.SourceControl.MarkBuildAsFailedAsync("SonarQube", failedDescription); + + throw; + } + } + } + + var testProcessor = buildContext.Processors.FirstOrDefault(x => x is TestProcessor) as TestProcessor; + if (testProcessor is not null) + { + // Build test projects *after* SonarQube (not part of SQ analysis). Unfortunately, because of this, we cannot yet mark + // the build as succeeded once we end the SQ session. Therefore, if SQ fails, both the SQ *and* build checks + // will be marked as failed if SQ fails. + await testProcessor.BuildAsync(); + } + + await buildContext.SourceControl.MarkBuildAsSucceededAsync("Build"); + + Information("Completed build for version '{0}'", buildContext.General.Version.NuGet); + + await buildContext.BuildServer.AfterBuildAsync(); +}) +.OnError(async (ex, buildContext) => +{ + await buildContext.SourceControl.MarkBuildAsFailedAsync("Build"); + + await buildContext.BuildServer.OnBuildFailedAsync(); + + throw ex; +}); + +//------------------------------------------------------------- + +Task("Test") + .IsDependentOn("Prepare") + // Note: no dependency on 'build' since we might have already built the solution + .Does(async buildContext => +{ + await buildContext.BuildServer.BeforeTestAsync(); + + await buildContext.SourceControl.MarkBuildAsPendingAsync("Test"); + + if (buildContext.Tests.Items.Count > 0) + { + // If docker is involved, login to all registries for the unit / integration tests + var dockerRegistries = new HashSet(); + var dockerProcessor = (DockerImagesProcessor)buildContext.Processors.Single(x => x is DockerImagesProcessor); + + try + { + foreach (var dockerImage in buildContext.DockerImages.Items) + { + var dockerRegistryUrl = dockerProcessor.GetDockerRegistryUrl(dockerImage); + if (dockerRegistries.Contains(dockerRegistryUrl)) + { + continue; + } + + // Note: we are logging in each time because the registry might be different per container + Information($"Logging in to docker @ '{dockerRegistryUrl}'"); + + dockerRegistries.Add(dockerRegistryUrl); + + var dockerRegistryUserName = dockerProcessor.GetDockerRegistryUserName(dockerImage); + var dockerRegistryPassword = dockerProcessor.GetDockerRegistryPassword(dockerImage); + + var dockerLoginSettings = new DockerRegistryLoginSettings + { + Username = dockerRegistryUserName, + Password = dockerRegistryPassword + }; + + DockerLogin(dockerLoginSettings, dockerRegistryUrl); + } + + // Always run all unit test projects before throwing + var failed = false; + + foreach (var testProject in buildContext.Tests.Items) + { + buildContext.CakeContext.LogSeparator("Running tests for '{0}'", testProject); + + try + { + RunUnitTests(buildContext, testProject); + } + catch (Exception ex) + { + failed = true; + + Warning($"Running tests for '{testProject}' caused an exception: {ex.Message}"); + } + } + + if (failed) + { + throw new Exception("At least 1 test project failed execution"); + } + } + finally + { + foreach (var dockerRegistry in dockerRegistries) + { + try + { + Information($"Logging out of docker @ '{dockerRegistry}'"); + + var dockerLogoutSettings = new DockerRegistryLogoutSettings + { + }; + + DockerLogout(dockerLogoutSettings, dockerRegistry); + } + catch (Exception ex) + { + Warning($"Failed to logout from docker: {ex.Message}"); + } + } + } + } + + await buildContext.SourceControl.MarkBuildAsSucceededAsync("Test"); + + Information("Completed tests for version '{0}'", buildContext.General.Version.NuGet); + + await buildContext.BuildServer.AfterTestAsync(); +}) +.OnError(async (ex, buildContext) => +{ + await buildContext.SourceControl.MarkBuildAsFailedAsync("Test"); + + await buildContext.BuildServer.OnTestFailedAsync(); + + throw ex; +}); + +//------------------------------------------------------------- + +Task("Package") + // Make sure to update info so our SolutionAssemblyInfo.cs is up to date + .IsDependentOn("UpdateInfo") + // Note: no dependency on 'build' since we might have already built the solution + // Make sure we have the temporary "project.assets.json" in case we need to package with Visual Studio + .IsDependentOn("RestorePackages") + // Make sure to update if we are running on a new agent so we can sign nuget packages + .IsDependentOn("UpdateNuGet") + .IsDependentOn("CodeSign") + .Does(async buildContext => +{ + await buildContext.BuildServer.BeforePackageAsync(); + + foreach (var processor in buildContext.Processors) + { + await processor.PackageAsync(); + } + + Information("Completed packaging for version '{0}'", buildContext.General.Version.NuGet); + + await buildContext.BuildServer.AfterPackageAsync(); +}); + +//------------------------------------------------------------- + +Task("PackageLocal") + .IsDependentOn("Package") + .Does(buildContext => +{ + // Note: no build server integration calls since this is *local* + + // For now only package components, we might need to move this to components-tasks.cake in the future + if (buildContext.Components.Items.Count == 0 && + buildContext.Tools.Items.Count == 0) + { + return; + } + + var localPackagesDirectory = buildContext.General.NuGet.LocalPackagesDirectory; + + Information("Copying build artifacts to '{0}'", localPackagesDirectory); + + CreateDirectory(localPackagesDirectory); + + foreach (var component in buildContext.Components.Items) + { + try + { + Information("Copying build artifact for '{0}'", component); + + var sourceFile = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, + $"{component}.{buildContext.General.Version.NuGet}.nupkg"); + + CopyFiles(new [] { sourceFile }, localPackagesDirectory); + } + catch (Exception) + { + // Ignore + Warning("Failed to copy build artifacts for '{0}'", component); + } + } + + Information("Copied build artifacts for version '{0}'", buildContext.General.Version.NuGet); +}); + +//------------------------------------------------------------- + +Task("Deploy") + // Note: no dependency on 'package' since we might have already packaged the solution + // Make sure we have the temporary "project.assets.json" in case we need to package with Visual Studio + .IsDependentOn("RestorePackages") + .Does(async buildContext => +{ + await buildContext.BuildServer.BeforeDeployAsync(); + + foreach (var processor in buildContext.Processors) + { + await processor.DeployAsync(); + } + + await buildContext.BuildServer.AfterDeployAsync(); +}); + +//------------------------------------------------------------- + +Task("Finalize") + // Note: no dependency on 'deploy' since we might have already deployed the solution + .Does(async buildContext => +{ + await buildContext.BuildServer.BeforeFinalizeAsync(); + + Information("Finalizing release '{0}'", buildContext.General.Version.FullSemVer); + + foreach (var processor in buildContext.Processors) + { + await processor.FinalizeAsync(); + } + + if (buildContext.General.IsOfficialBuild) + { + await buildContext.BuildServer.PinBuildAsync("Official build"); + } + + await buildContext.IssueTracker.CreateAndReleaseVersionAsync(); + + await buildContext.BuildServer.AfterFinalizeAsync(); +}); + +//------------------------------------------------------------- +// Wrapper tasks since we don't want to add "Build" as a +// dependency to "Package" because we want to run in multiple +// stages +//------------------------------------------------------------- + +Task("BuildAndTest") + .IsDependentOn("Initialize") + .IsDependentOn("Build") + .IsDependentOn("Test"); + +//------------------------------------------------------------- + +Task("BuildAndPackage") + .IsDependentOn("Initialize") + .IsDependentOn("Build") + .IsDependentOn("Test") + .IsDependentOn("Package"); + +//------------------------------------------------------------- + +Task("BuildAndPackageLocal") + .IsDependentOn("Initialize") + .IsDependentOn("Build") + //.IsDependentOn("Test") // Note: don't test for performance on local builds + .IsDependentOn("PackageLocal"); + +//------------------------------------------------------------- + +Task("BuildAndDeploy") + .IsDependentOn("Initialize") + .IsDependentOn("Build") + .IsDependentOn("Test") + .IsDependentOn("Package") + .IsDependentOn("Deploy"); + +//------------------------------------------------------------- + +Task("Default") + .Does(async buildContext => +{ + Error("No target specified, please specify one of the following targets:\n" + + " - Prepare\n" + + " - UpdateInfo\n" + + " - Build\n" + + " - Test\n" + + " - Package\n" + + " - Deploy\n" + + " - Finalize\n\n" + + "or one of the combined ones:\n" + + " - BuildAndTest\n" + + " - BuildAndPackage\n" + + " - BuildAndPackageLocal\n" + + " - BuildAndDeploy\n"); +}); + +//------------------------------------------------------------- +// Test wrappers +//------------------------------------------------------------- + +Task("TestNotifications") + .Does(async buildContext => +{ + await buildContext.Notifications.NotifyAsync("MyProject", "This is a generic test"); + await buildContext.Notifications.NotifyAsync("MyProject", "This is a component test", TargetType.Component); + await buildContext.Notifications.NotifyAsync("MyProject", "This is a docker image test", TargetType.DockerImage); + await buildContext.Notifications.NotifyAsync("MyProject", "This is a web app test", TargetType.WebApp); + await buildContext.Notifications.NotifyAsync("MyProject", "This is a wpf app test", TargetType.WpfApp); + await buildContext.Notifications.NotifyErrorAsync("MyProject", "This is an error"); +}); + +//------------------------------------------------------------- + +Task("TestSourceControl") + .Does(async buildContext => +{ + await buildContext.SourceControl.MarkBuildAsPendingAsync("Build"); + + await System.Threading.Tasks.Task.Delay(5 * 1000); + + await buildContext.SourceControl.MarkBuildAsSucceededAsync("Build"); + + await buildContext.SourceControl.MarkBuildAsPendingAsync("Test"); + + await System.Threading.Tasks.Task.Delay(5 * 1000); + + await buildContext.SourceControl.MarkBuildAsSucceededAsync("Test"); +}); + +//------------------------------------------------------------- +// ACTUAL RUNNER - MUST BE DEFINED AT THE BOTTOM +//------------------------------------------------------------- + +var localTarget = GetBuildServerVariable("Target", "Default", showValue: true); +RunTarget(localTarget); diff --git a/deployment/cake/templates-tasks.cake b/deployment/cake/templates-tasks.cake index 6218ab1f..7e69af96 100644 --- a/deployment/cake/templates-tasks.cake +++ b/deployment/cake/templates-tasks.cake @@ -1,103 +1,103 @@ -#l "templates-variables.cake" - -using System.Linq; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using System.IO; - -//------------------------------------------------------------- - -public class TemplatesProcessor : ProcessorBase -{ - public TemplatesProcessor(BuildContext buildContext) - : base(buildContext) - { - var templatesRelativePath = "./deployment/templates"; - - if (CakeContext.DirectoryExists(templatesRelativePath)) - { - var currentDirectoryPath = System.IO.Directory.GetCurrentDirectory(); - var templateAbsolutePath = System.IO.Path.Combine(currentDirectoryPath, templatesRelativePath); - var files = System.IO.Directory.GetFiles(templateAbsolutePath, "*.*", System.IO.SearchOption.AllDirectories); - - CakeContext.Information($"Found '{files.Count()}' template files"); - - foreach (var file in files) - { - BuildContext.Templates.Items.Add(file.Substring(templateAbsolutePath.Length + 1)); - } - } - } - - public override bool HasItems() - { - return BuildContext.Templates.Items.Count > 0; - } - - public override async Task PrepareAsync() - { - - } - - public override async Task UpdateInfoAsync() - { - if (!HasItems()) - { - return; - } - - var variableRegex = new Regex(@"\$\{([^}]+)\}", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled); - - foreach (var template in BuildContext.Templates.Items) - { - CakeContext.Information($"Updating template file '{template}'"); - - var templateSourceFile = $"./deployment/templates/{template}"; - var content = CakeContext.FileReadText(templateSourceFile); - - var matches = variableRegex.Matches(content); - - foreach (var match in matches.Cast()) - { - var variableName = match.Groups[1].Value; - - CakeContext.Information($"Found usage of variable '{variableName}'"); - - if (!BuildContext.Variables.TryGetValue(variableName, out var replacement)) - { - CakeContext.Error($"Could not find value for variable '{variableName}'"); - continue; - } - - content = content.Replace($"${{{variableName}}}", replacement); - } - - CakeContext.FileWriteText($"{template}", content); - } - } - - public override async Task BuildAsync() - { - // Run templates every time - await UpdateInfoAsync(); - } - - public override async Task PackageAsync() - { - // Run templates every time - await UpdateInfoAsync(); - } - - public override async Task DeployAsync() - { - if (!HasItems()) - { - return; - } - } - - public override async Task FinalizeAsync() - { - - } -} +#l "templates-variables.cake" + +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using System.IO; + +//------------------------------------------------------------- + +public class TemplatesProcessor : ProcessorBase +{ + public TemplatesProcessor(BuildContext buildContext) + : base(buildContext) + { + var templatesRelativePath = "./deployment/templates"; + + if (CakeContext.DirectoryExists(templatesRelativePath)) + { + var currentDirectoryPath = System.IO.Directory.GetCurrentDirectory(); + var templateAbsolutePath = System.IO.Path.Combine(currentDirectoryPath, templatesRelativePath); + var files = System.IO.Directory.GetFiles(templateAbsolutePath, "*.*", System.IO.SearchOption.AllDirectories); + + CakeContext.Information($"Found '{files.Count()}' template files"); + + foreach (var file in files) + { + BuildContext.Templates.Items.Add(file.Substring(templateAbsolutePath.Length + 1)); + } + } + } + + public override bool HasItems() + { + return BuildContext.Templates.Items.Count > 0; + } + + public override async Task PrepareAsync() + { + + } + + public override async Task UpdateInfoAsync() + { + if (!HasItems()) + { + return; + } + + var variableRegex = new Regex(@"\$\{([^}]+)\}", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled); + + foreach (var template in BuildContext.Templates.Items) + { + CakeContext.Information($"Updating template file '{template}'"); + + var templateSourceFile = $"./deployment/templates/{template}"; + var content = CakeContext.FileReadText(templateSourceFile); + + var matches = variableRegex.Matches(content); + + foreach (var match in matches.Cast()) + { + var variableName = match.Groups[1].Value; + + CakeContext.Information($"Found usage of variable '{variableName}'"); + + if (!BuildContext.Variables.TryGetValue(variableName, out var replacement)) + { + CakeContext.Error($"Could not find value for variable '{variableName}'"); + continue; + } + + content = content.Replace($"${{{variableName}}}", replacement); + } + + CakeContext.FileWriteText($"{template}", content); + } + } + + public override async Task BuildAsync() + { + // Run templates every time + await UpdateInfoAsync(); + } + + public override async Task PackageAsync() + { + // Run templates every time + await UpdateInfoAsync(); + } + + public override async Task DeployAsync() + { + if (!HasItems()) + { + return; + } + } + + public override async Task FinalizeAsync() + { + + } +} diff --git a/deployment/cake/templates-variables.cake b/deployment/cake/templates-variables.cake index c8cd289a..a493e1d8 100644 --- a/deployment/cake/templates-variables.cake +++ b/deployment/cake/templates-variables.cake @@ -1,50 +1,50 @@ -#l "buildserver.cake" - -//------------------------------------------------------------- - -public class TemplatesContext : BuildContextWithItemsBase -{ - public TemplatesContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' template items"); - } -} - -//------------------------------------------------------------- - -private TemplatesContext InitializeTemplatesContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new TemplatesContext(parentBuildContext) - { - Items = Templates ?? new List(), - }; - - return data; -} - -//------------------------------------------------------------- - -List _templates; - -public List Templates -{ - get - { - if (_templates is null) - { - _templates = new List(); - } - - return _templates; - } -} +#l "buildserver.cake" + +//------------------------------------------------------------- + +public class TemplatesContext : BuildContextWithItemsBase +{ + public TemplatesContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' template items"); + } +} + +//------------------------------------------------------------- + +private TemplatesContext InitializeTemplatesContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new TemplatesContext(parentBuildContext) + { + Items = Templates ?? new List(), + }; + + return data; +} + +//------------------------------------------------------------- + +List _templates; + +public List Templates +{ + get + { + if (_templates is null) + { + _templates = new List(); + } + + return _templates; + } +} diff --git a/deployment/cake/tests-nunit.cake b/deployment/cake/tests-nunit.cake index d525f172..a9216a8e 100644 --- a/deployment/cake/tests-nunit.cake +++ b/deployment/cake/tests-nunit.cake @@ -1,39 +1,39 @@ -#tool "nuget:?package=NUnit.ConsoleRunner&version=3.16.3" - -//------------------------------------------------------------- - -private static void RunTestsUsingNUnit(BuildContext buildContext, string projectName, string testTargetFramework, string testResultsDirectory) -{ - var testFile = System.IO.Path.Combine(GetProjectOutputDirectory(buildContext, projectName), - testTargetFramework, $"{projectName}.dll"); - var resultsFile = System.IO.Path.Combine(testResultsDirectory, "testresults.xml"); - - var nunitSettings = new NUnit3Settings - { - Results = new NUnit3Result[] - { - new NUnit3Result - { - FileName = resultsFile, - Format = "nunit3" - } - }, - NoHeader = true, - NoColor = true, - NoResults = false, - X86 = string.Equals(buildContext.Tests.ProcessBit, "X86", StringComparison.OrdinalIgnoreCase), - Timeout = 60 * 1000, // 60 seconds - Workers = 1 - //Work = testResultsDirectory - }; - - // Note: although the docs say you can use without array initialization, you can't - buildContext.CakeContext.NUnit3(new string[] { testFile }, nunitSettings); - - buildContext.CakeContext.Information("Verifying whether results file '{0}' exists", resultsFile); - - if (!buildContext.CakeContext.FileExists(resultsFile)) - { - throw new Exception(string.Format("Expected results file '{0}' does not exist", resultsFile)); - } +#tool "nuget:?package=NUnit.ConsoleRunner&version=3.16.3" + +//------------------------------------------------------------- + +private static void RunTestsUsingNUnit(BuildContext buildContext, string projectName, string testTargetFramework, string testResultsDirectory) +{ + var testFile = System.IO.Path.Combine(GetProjectOutputDirectory(buildContext, projectName), + testTargetFramework, $"{projectName}.dll"); + var resultsFile = System.IO.Path.Combine(testResultsDirectory, "testresults.xml"); + + var nunitSettings = new NUnit3Settings + { + Results = new NUnit3Result[] + { + new NUnit3Result + { + FileName = resultsFile, + Format = "nunit3" + } + }, + NoHeader = true, + NoColor = true, + NoResults = false, + X86 = string.Equals(buildContext.Tests.ProcessBit, "X86", StringComparison.OrdinalIgnoreCase), + Timeout = 60 * 1000, // 60 seconds + Workers = 1 + //Work = testResultsDirectory + }; + + // Note: although the docs say you can use without array initialization, you can't + buildContext.CakeContext.NUnit3(new string[] { testFile }, nunitSettings); + + buildContext.CakeContext.Information("Verifying whether results file '{0}' exists", resultsFile); + + if (!buildContext.CakeContext.FileExists(resultsFile)) + { + throw new Exception(string.Format("Expected results file '{0}' does not exist", resultsFile)); + } } \ No newline at end of file diff --git a/deployment/cake/tests-variables.cake b/deployment/cake/tests-variables.cake index d56b36dc..fc9f69dc 100644 --- a/deployment/cake/tests-variables.cake +++ b/deployment/cake/tests-variables.cake @@ -1,71 +1,71 @@ -#l "buildserver.cake" - -//------------------------------------------------------------- - -public class TestsContext : BuildContextWithItemsBase -{ - public TestsContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string Framework { get; set; } - public string TargetFramework { get; set; } - public string ProcessBit { get; set; } - - protected override void ValidateContext() - { - if (Items.Count == 0) - { - return; - } - - if (string.IsNullOrWhiteSpace(Framework)) - { - throw new Exception("Framework is required, specify via 'TestFramework'"); - } - - if (string.IsNullOrWhiteSpace(ProcessBit)) - { - throw new Exception("ProcessBit is required, specify via 'TestProcessBit'"); - } - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' test projects"); - } -} - -//------------------------------------------------------------- - -private TestsContext InitializeTestsContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new TestsContext(parentBuildContext) - { - Items = TestProjects, - - Framework = buildContext.BuildServer.GetVariable("TestFramework", "nunit", showValue: true), - TargetFramework = buildContext.BuildServer.GetVariable("TestTargetFramework", "", showValue: true), - ProcessBit = buildContext.BuildServer.GetVariable("TestProcessBit", "X64", showValue: true) - }; - - return data; -} - -//------------------------------------------------------------- - -List _testProjects; - -public List TestProjects -{ - get - { - if (_testProjects is null) - { - _testProjects = new List(); - } - - return _testProjects; - } -} +#l "buildserver.cake" + +//------------------------------------------------------------- + +public class TestsContext : BuildContextWithItemsBase +{ + public TestsContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string Framework { get; set; } + public string TargetFramework { get; set; } + public string ProcessBit { get; set; } + + protected override void ValidateContext() + { + if (Items.Count == 0) + { + return; + } + + if (string.IsNullOrWhiteSpace(Framework)) + { + throw new Exception("Framework is required, specify via 'TestFramework'"); + } + + if (string.IsNullOrWhiteSpace(ProcessBit)) + { + throw new Exception("ProcessBit is required, specify via 'TestProcessBit'"); + } + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' test projects"); + } +} + +//------------------------------------------------------------- + +private TestsContext InitializeTestsContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new TestsContext(parentBuildContext) + { + Items = TestProjects, + + Framework = buildContext.BuildServer.GetVariable("TestFramework", "nunit", showValue: true), + TargetFramework = buildContext.BuildServer.GetVariable("TestTargetFramework", "", showValue: true), + ProcessBit = buildContext.BuildServer.GetVariable("TestProcessBit", "X64", showValue: true) + }; + + return data; +} + +//------------------------------------------------------------- + +List _testProjects; + +public List TestProjects +{ + get + { + if (_testProjects is null) + { + _testProjects = new List(); + } + + return _testProjects; + } +} diff --git a/deployment/cake/tests.cake b/deployment/cake/tests.cake index 36f216c9..a63a5a32 100644 --- a/deployment/cake/tests.cake +++ b/deployment/cake/tests.cake @@ -1,300 +1,300 @@ -// Customize this file when using a different test framework -#l "tests-variables.cake" -#l "tests-nunit.cake" - -public class TestProcessor : ProcessorBase -{ - public TestProcessor(BuildContext buildContext) - : base(buildContext) - { - - } - - public override bool HasItems() - { - return BuildContext.Tests.Items.Count > 0; - } - - public override async Task PrepareAsync() - { - // Check whether projects should be processed, `.ToList()` - // is required to prevent issues with foreach - foreach (var testProject in BuildContext.Tests.Items.ToList()) - { - if (IgnoreTestProject(testProject)) - { - BuildContext.Tests.Items.Remove(testProject); - } - } - } - - public override async Task UpdateInfoAsync() - { - // Not required - } - - public override async Task BuildAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var testProject in BuildContext.Tests.Items) - { - BuildContext.CakeContext.LogSeparator("Building test project '{0}'", testProject); - - var projectFileName = GetProjectFileName(BuildContext, testProject); - - var msBuildSettings = new MSBuildSettings - { - Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic - ToolVersion = MSBuildToolVersion.Default, - Configuration = BuildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, testProject, "build"); - - // Always disable SourceLink - msBuildSettings.WithProperty("EnableSourceLink", "false"); - - // Force disable SonarQube - msBuildSettings.WithProperty("SonarQubeExclude", "true"); - - RunMsBuild(BuildContext, testProject, projectFileName, msBuildSettings, "build"); - } - } - - public override async Task PackageAsync() - { - // Not required - } - - public override async Task DeployAsync() - { - // Not required - } - - public override async Task FinalizeAsync() - { - // Not required - } - - //------------------------------------------------------------- - - private bool IgnoreTestProject(string projectName) - { - if (BuildContext.General.IsLocalBuild && BuildContext.General.MaximizePerformance) - { - BuildContext.CakeContext.Information($"Local build with maximized performance detected, ignoring test project for project '{projectName}'"); - return true; - } - - // In case of a local build and we have included / excluded anything, skip tests - if (BuildContext.General.IsLocalBuild && - (BuildContext.General.Includes.Count > 0 || BuildContext.General.Excludes.Count > 0)) - { - BuildContext.CakeContext.Information($"Skipping test project '{projectName}' because this is a local build with specific includes / excludes"); - return true; - } - - // Special unit test part assuming a few naming conventions: - // 1. [ProjectName].Tests - // 2. [SolutionName].Tests.[ProjectName] - // - // In both cases, we can simply remove ".Tests" and check if that project is being ignored - var expectedProjectName = projectName - .Replace(".Integration.Tests", string.Empty) - .Replace(".IntegrationTests", string.Empty) - .Replace(".Tests", string.Empty); - - // Special case: if this is a "solution wide" test project, it must always run - if (!BuildContext.RegisteredProjects.Any(x => string.Equals(x, expectedProjectName, StringComparison.OrdinalIgnoreCase))) - { - BuildContext.CakeContext.Information($"Including test project '{projectName}' because there are no linked projects, assuming this is a solution wide test project"); - return false; - } - - if (!ShouldProcessProject(BuildContext, expectedProjectName)) - { - BuildContext.CakeContext.Information($"Skipping test project '{projectName}' because project '{expectedProjectName}' should not be processed either"); - return true; - } - - return false; - } -} - -//------------------------------------------------------------- - -private static void RunUnitTests(BuildContext buildContext, string projectName) -{ - var testResultsDirectory = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, "testresults", projectName); - - buildContext.CakeContext.CreateDirectory(testResultsDirectory); - - var ranTests = false; - var failed = false; - var testTargetFramework = GetTestTargetFramework(buildContext, projectName); - - try - { - if (IsDotNetCoreProject(buildContext, projectName)) - { - buildContext.CakeContext.Information($"Project '{projectName}' is a .NET core project, using 'dotnet test' to run the unit tests"); - - var projectFileName = GetProjectFileName(buildContext, projectName); - - var dotNetTestSettings = new DotNetTestSettings - { - Configuration = buildContext.General.Solution.ConfigurationName, - // Loggers = new [] - // { - // "nunit;LogFilePath=test-result.xml" - // }, - NoBuild = true, - NoLogo = true, - NoRestore = true, - OutputDirectory = System.IO.Path.Combine(GetProjectOutputDirectory(buildContext, projectName), testTargetFramework), - ResultsDirectory = testResultsDirectory - }; - - if (IsNUnitTestProject(buildContext, projectName)) - { - dotNetTestSettings.ArgumentCustomization = args => args - .Append($"-- NUnit.TestOutputXml={testResultsDirectory}"); - } - - if (IsXUnitTestProject(buildContext, projectName)) - { - var outputFileName = System.IO.Path.Combine(testResultsDirectory, $"{projectName}.xml"); - - dotNetTestSettings.ArgumentCustomization = args => args - .Append($"-l:trx;LogFileName={outputFileName}"); - } - - var processBit = buildContext.Tests.ProcessBit.ToLower(); - if (!string.IsNullOrWhiteSpace(processBit)) - { - dotNetTestSettings.Runtime = $"win-{processBit}"; - } - - buildContext.CakeContext.DotNetTest(projectFileName, dotNetTestSettings); - - ranTests = true; - } - else - { - buildContext.CakeContext.Information($"Project '{projectName}' is a .NET project, using '{buildContext.Tests.Framework} runner' to run the unit tests"); - - if (IsNUnitTestProject(buildContext, projectName)) - { - RunTestsUsingNUnit(buildContext, projectName, testTargetFramework, testResultsDirectory); - - ranTests = true; - } - } - } - catch (Exception ex) - { - buildContext.CakeContext.Warning($"An exception occurred: {ex.Message}"); - - failed = true; - } - - if (ranTests) - { - buildContext.CakeContext.Information($"Results are available in '{testResultsDirectory}'"); - } - else if (failed) - { - throw new Exception("Unit test execution failed"); - } - else - { - buildContext.CakeContext.Warning("No tests were executed, check whether the used test framework '{0}' is available", buildContext.Tests.Framework); - } -} - -//------------------------------------------------------------- - -private static bool IsTestProject(BuildContext buildContext, string projectName) -{ - if (IsNUnitTestProject(buildContext, projectName)) - { - return true; - } - - if (IsXUnitTestProject(buildContext, projectName)) - { - return true; - } - - return false; -} - -//------------------------------------------------------------- - -private static bool IsNUnitTestProject(BuildContext buildContext, string projectName) -{ - var projectFileName = GetProjectFileName(buildContext, projectName); - var projectFileContents = System.IO.File.ReadAllText(projectFileName); - - if (projectFileContents.ToLower().Contains("nunit")) - { - return true; - } - - return false; - - // Not sure, return framework from config - //return buildContext.Tests.Framework.ToLower().Equals("nunit"); -} - -//------------------------------------------------------------- - -private static bool IsXUnitTestProject(BuildContext buildContext, string projectName) -{ - var projectFileName = GetProjectFileName(buildContext, projectName); - var projectFileContents = System.IO.File.ReadAllText(projectFileName); - - if (projectFileContents.ToLower().Contains("xunit")) - { - return true; - } - - return false; - - // Not sure, return framework from config - //return buildContext.Tests.Framework.ToLower().Equals("xunit"); -} - -//------------------------------------------------------------- - -private static string GetTestTargetFramework(BuildContext buildContext, string projectName) -{ - // Step 1: if defined, use defined value - var testTargetFramework = buildContext.Tests.TargetFramework; - if (!string.IsNullOrWhiteSpace(testTargetFramework)) - { - buildContext.CakeContext.Information("Using test target framework '{0}', specified via the configuration", testTargetFramework); - - return testTargetFramework; - } - - buildContext.CakeContext.Information("Test target framework not specified, auto detecting test target framework"); - - var targetFrameworks = GetTargetFrameworks(buildContext, projectName); - testTargetFramework = targetFrameworks.FirstOrDefault(); - - buildContext.CakeContext.Information("Auto detected test target framework '{0}'", testTargetFramework); - - if (string.IsNullOrWhiteSpace(testTargetFramework)) - { - throw new Exception(string.Format("Test target framework could not automatically be detected for project '{0]'", projectName)); - } - - return testTargetFramework; +// Customize this file when using a different test framework +#l "tests-variables.cake" +#l "tests-nunit.cake" + +public class TestProcessor : ProcessorBase +{ + public TestProcessor(BuildContext buildContext) + : base(buildContext) + { + + } + + public override bool HasItems() + { + return BuildContext.Tests.Items.Count > 0; + } + + public override async Task PrepareAsync() + { + // Check whether projects should be processed, `.ToList()` + // is required to prevent issues with foreach + foreach (var testProject in BuildContext.Tests.Items.ToList()) + { + if (IgnoreTestProject(testProject)) + { + BuildContext.Tests.Items.Remove(testProject); + } + } + } + + public override async Task UpdateInfoAsync() + { + // Not required + } + + public override async Task BuildAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var testProject in BuildContext.Tests.Items) + { + BuildContext.CakeContext.LogSeparator("Building test project '{0}'", testProject); + + var projectFileName = GetProjectFileName(BuildContext, testProject); + + var msBuildSettings = new MSBuildSettings + { + Verbosity = Verbosity.Quiet, // Verbosity.Diagnostic + ToolVersion = MSBuildToolVersion.Default, + Configuration = BuildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, testProject, "build"); + + // Always disable SourceLink + msBuildSettings.WithProperty("EnableSourceLink", "false"); + + // Force disable SonarQube + msBuildSettings.WithProperty("SonarQubeExclude", "true"); + + RunMsBuild(BuildContext, testProject, projectFileName, msBuildSettings, "build"); + } + } + + public override async Task PackageAsync() + { + // Not required + } + + public override async Task DeployAsync() + { + // Not required + } + + public override async Task FinalizeAsync() + { + // Not required + } + + //------------------------------------------------------------- + + private bool IgnoreTestProject(string projectName) + { + if (BuildContext.General.IsLocalBuild && BuildContext.General.MaximizePerformance) + { + BuildContext.CakeContext.Information($"Local build with maximized performance detected, ignoring test project for project '{projectName}'"); + return true; + } + + // In case of a local build and we have included / excluded anything, skip tests + if (BuildContext.General.IsLocalBuild && + (BuildContext.General.Includes.Count > 0 || BuildContext.General.Excludes.Count > 0)) + { + BuildContext.CakeContext.Information($"Skipping test project '{projectName}' because this is a local build with specific includes / excludes"); + return true; + } + + // Special unit test part assuming a few naming conventions: + // 1. [ProjectName].Tests + // 2. [SolutionName].Tests.[ProjectName] + // + // In both cases, we can simply remove ".Tests" and check if that project is being ignored + var expectedProjectName = projectName + .Replace(".Integration.Tests", string.Empty) + .Replace(".IntegrationTests", string.Empty) + .Replace(".Tests", string.Empty); + + // Special case: if this is a "solution wide" test project, it must always run + if (!BuildContext.RegisteredProjects.Any(x => string.Equals(x, expectedProjectName, StringComparison.OrdinalIgnoreCase))) + { + BuildContext.CakeContext.Information($"Including test project '{projectName}' because there are no linked projects, assuming this is a solution wide test project"); + return false; + } + + if (!ShouldProcessProject(BuildContext, expectedProjectName)) + { + BuildContext.CakeContext.Information($"Skipping test project '{projectName}' because project '{expectedProjectName}' should not be processed either"); + return true; + } + + return false; + } +} + +//------------------------------------------------------------- + +private static void RunUnitTests(BuildContext buildContext, string projectName) +{ + var testResultsDirectory = System.IO.Path.Combine(buildContext.General.OutputRootDirectory, "testresults", projectName); + + buildContext.CakeContext.CreateDirectory(testResultsDirectory); + + var ranTests = false; + var failed = false; + var testTargetFramework = GetTestTargetFramework(buildContext, projectName); + + try + { + if (IsDotNetCoreProject(buildContext, projectName)) + { + buildContext.CakeContext.Information($"Project '{projectName}' is a .NET core project, using 'dotnet test' to run the unit tests"); + + var projectFileName = GetProjectFileName(buildContext, projectName); + + var dotNetTestSettings = new DotNetTestSettings + { + Configuration = buildContext.General.Solution.ConfigurationName, + // Loggers = new [] + // { + // "nunit;LogFilePath=test-result.xml" + // }, + NoBuild = true, + NoLogo = true, + NoRestore = true, + OutputDirectory = System.IO.Path.Combine(GetProjectOutputDirectory(buildContext, projectName), testTargetFramework), + ResultsDirectory = testResultsDirectory + }; + + if (IsNUnitTestProject(buildContext, projectName)) + { + dotNetTestSettings.ArgumentCustomization = args => args + .Append($"-- NUnit.TestOutputXml={testResultsDirectory}"); + } + + if (IsXUnitTestProject(buildContext, projectName)) + { + var outputFileName = System.IO.Path.Combine(testResultsDirectory, $"{projectName}.xml"); + + dotNetTestSettings.ArgumentCustomization = args => args + .Append($"-l:trx;LogFileName={outputFileName}"); + } + + var processBit = buildContext.Tests.ProcessBit.ToLower(); + if (!string.IsNullOrWhiteSpace(processBit)) + { + dotNetTestSettings.Runtime = $"win-{processBit}"; + } + + buildContext.CakeContext.DotNetTest(projectFileName, dotNetTestSettings); + + ranTests = true; + } + else + { + buildContext.CakeContext.Information($"Project '{projectName}' is a .NET project, using '{buildContext.Tests.Framework} runner' to run the unit tests"); + + if (IsNUnitTestProject(buildContext, projectName)) + { + RunTestsUsingNUnit(buildContext, projectName, testTargetFramework, testResultsDirectory); + + ranTests = true; + } + } + } + catch (Exception ex) + { + buildContext.CakeContext.Warning($"An exception occurred: {ex.Message}"); + + failed = true; + } + + if (ranTests) + { + buildContext.CakeContext.Information($"Results are available in '{testResultsDirectory}'"); + } + else if (failed) + { + throw new Exception("Unit test execution failed"); + } + else + { + buildContext.CakeContext.Warning("No tests were executed, check whether the used test framework '{0}' is available", buildContext.Tests.Framework); + } +} + +//------------------------------------------------------------- + +private static bool IsTestProject(BuildContext buildContext, string projectName) +{ + if (IsNUnitTestProject(buildContext, projectName)) + { + return true; + } + + if (IsXUnitTestProject(buildContext, projectName)) + { + return true; + } + + return false; +} + +//------------------------------------------------------------- + +private static bool IsNUnitTestProject(BuildContext buildContext, string projectName) +{ + var projectFileName = GetProjectFileName(buildContext, projectName); + var projectFileContents = System.IO.File.ReadAllText(projectFileName); + + if (projectFileContents.ToLower().Contains("nunit")) + { + return true; + } + + return false; + + // Not sure, return framework from config + //return buildContext.Tests.Framework.ToLower().Equals("nunit"); +} + +//------------------------------------------------------------- + +private static bool IsXUnitTestProject(BuildContext buildContext, string projectName) +{ + var projectFileName = GetProjectFileName(buildContext, projectName); + var projectFileContents = System.IO.File.ReadAllText(projectFileName); + + if (projectFileContents.ToLower().Contains("xunit")) + { + return true; + } + + return false; + + // Not sure, return framework from config + //return buildContext.Tests.Framework.ToLower().Equals("xunit"); +} + +//------------------------------------------------------------- + +private static string GetTestTargetFramework(BuildContext buildContext, string projectName) +{ + // Step 1: if defined, use defined value + var testTargetFramework = buildContext.Tests.TargetFramework; + if (!string.IsNullOrWhiteSpace(testTargetFramework)) + { + buildContext.CakeContext.Information("Using test target framework '{0}', specified via the configuration", testTargetFramework); + + return testTargetFramework; + } + + buildContext.CakeContext.Information("Test target framework not specified, auto detecting test target framework"); + + var targetFrameworks = GetTargetFrameworks(buildContext, projectName); + testTargetFramework = targetFrameworks.FirstOrDefault(); + + buildContext.CakeContext.Information("Auto detected test target framework '{0}'", testTargetFramework); + + if (string.IsNullOrWhiteSpace(testTargetFramework)) + { + throw new Exception(string.Format("Test target framework could not automatically be detected for project '{0]'", projectName)); + } + + return testTargetFramework; } \ No newline at end of file diff --git a/deployment/cake/tools-tasks.cake b/deployment/cake/tools-tasks.cake index a481eaad..5ad32069 100644 --- a/deployment/cake/tools-tasks.cake +++ b/deployment/cake/tools-tasks.cake @@ -1,449 +1,449 @@ -#l "tools-variables.cake" - -using System.Xml.Linq; - -//------------------------------------------------------------- - -public class ToolsProcessor : ProcessorBase -{ - public ToolsProcessor(BuildContext buildContext) - : base(buildContext) - { - - } - - private void EnsureChocolateyLicenseFile(string projectName) - { - // Required for Chocolatey - - var projectDirectory = GetProjectDirectory(projectName); - var outputDirectory = GetProjectOutputDirectory(BuildContext, projectName); - - var legalDirectory = System.IO.Path.Combine(outputDirectory, "legal"); - System.IO.Directory.CreateDirectory(legalDirectory); - - // Check if it already exists - var fileName = System.IO.Path.Combine(legalDirectory, "LICENSE.txt"); - if (!CakeContext.FileExists(fileName)) - { - CakeContext.Information("Creating Chocolatey license file for '{0}'", projectName); - - // Option 1: Copy from root - var sourceFile = System.IO.Path.Combine(".", "LICENSE"); - if (CakeContext.FileExists(sourceFile)) - { - CakeContext.Information("Using license file from repository"); - - CakeContext.CopyFile(sourceFile, fileName); - return; - } - - // Option 2: use expression (PackageLicenseExpression) - throw new Exception("Cannot find ./LICENSE, which is required for Chocolatey"); - } - } - - private void EnsureChocolateyVerificationFile(string projectName) - { - // Required for Chocolatey - - var projectDirectory = GetProjectDirectory(projectName); - var outputDirectory = GetProjectOutputDirectory(BuildContext, projectName); - - var legalDirectory = System.IO.Path.Combine(outputDirectory, "legal"); - System.IO.Directory.CreateDirectory(legalDirectory); - - // Check if it already exists - var fileName = System.IO.Path.Combine(legalDirectory, "VERIFICATION.txt"); - if (!CakeContext.FileExists(fileName)) - { - CakeContext.Information("Creating Chocolatey verification file for '{0}'", projectName); - - var verificationBuilder = new StringBuilder(@"VERIFICATION -Verification is intended to assist the Chocolatey moderators and community -in verifying that this package's contents are trustworthy. - -This package is submitted by the software vendor - checksum verification is optional but still included. - -SHA512 CHECKSUMS GENERATED BY BUILD TOOL: -"); - - verificationBuilder.AppendLine(); - - var files = new List(); - - var exePattern = $"{outputDirectory}/**/*.exe"; - files.AddRange(CakeContext.GetFiles(exePattern)); - - var dllPattern = $"{outputDirectory}/**/*.dll"; - files.AddRange(CakeContext.GetFiles(dllPattern)); - - var outputDirectoryPath = new DirectoryPath(outputDirectory); - - foreach (var packageFile in files/*.OrderBy(x => x.FullPath)*/) - { - var relativeFileName = outputDirectoryPath.GetRelativePath(packageFile); - - // Using 'outputDirectory' results in a file like '[ProductName]/netcoreapp3.1/nl/Catel.MVVM.resources.dll', - // so trying to fix the directory to be relative to the output including the target framework by faking - // that the file is 2 directories up - var fixedRelativePathSegments = relativeFileName.Segments.Skip(1).ToArray(); - var fixedRelativePath = new FilePath(System.IO.Path.Combine(fixedRelativePathSegments)); - - var fileHash = CakeContext.CalculateFileHash(packageFile, HashAlgorithm.SHA512); - - verificationBuilder.AppendLine($"* tools/{fixedRelativePath.FullPath} | {fileHash.ToHex()}"); - } - - System.IO.File.WriteAllText(fileName, verificationBuilder.ToString()); - } - } - - private string GetToolsNuGetRepositoryUrls(string projectName) - { - // Allow per project overrides via "NuGetRepositoryUrlFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "ToolsNuGetRepositoryUrlsFor", BuildContext.Tools.NuGetRepositoryUrls); - } - - private string GetToolsNuGetRepositoryApiKeys(string projectName) - { - // Allow per project overrides via "NuGetRepositoryApiKeyFor[ProjectName]" - return GetProjectSpecificConfigurationValue(BuildContext, projectName, "ToolsNuGetRepositoryApiKeysFor", BuildContext.Tools.NuGetRepositoryApiKeys); - } - - public override bool HasItems() - { - return BuildContext.Tools.Items.Count > 0; - } - - public override async Task PrepareAsync() - { - if (!HasItems()) - { - return; - } - - // Check whether projects should be processed, `.ToList()` - // is required to prevent issues with foreach - foreach (var tool in BuildContext.Tools.Items.ToList()) - { - if (!ShouldProcessProject(BuildContext, tool)) - { - BuildContext.Tools.Items.Remove(tool); - } - } - - if (BuildContext.General.IsLocalBuild && BuildContext.General.Target.ToLower().Contains("packagelocal")) - { - foreach (var tool in BuildContext.Tools.Items) - { - var expandableCacheDirectory = System.IO.Path.Combine("%userprofile%", ".nuget", "packages", tool, BuildContext.General.Version.NuGet); - var cacheDirectory = Environment.ExpandEnvironmentVariables(expandableCacheDirectory); - - CakeContext.Information("Checking for existing local NuGet cached version at '{0}'", cacheDirectory); - - var retryCount = 3; - - while (retryCount > 0) - { - if (!CakeContext.DirectoryExists(cacheDirectory)) - { - break; - } - - CakeContext.Information("Deleting already existing NuGet cached version from '{0}'", cacheDirectory); - - CakeContext.DeleteDirectory(cacheDirectory, new DeleteDirectorySettings() - { - Force = true, - Recursive = true - }); - - await System.Threading.Tasks.Task.Delay(1000); - - retryCount--; - } - } - } - } - - public override async Task UpdateInfoAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var tool in BuildContext.Tools.Items) - { - CakeContext.Information("Updating version for tool '{0}'", tool); - - var projectFileName = GetProjectFileName(BuildContext, tool); - - CakeContext.TransformConfig(projectFileName, new TransformationCollection - { - { "Project/PropertyGroup/PackageVersion", BuildContext.General.Version.NuGet } - }); - } - } - - public override async Task BuildAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var tool in BuildContext.Tools.Items) - { - BuildContext.CakeContext.LogSeparator("Building tool '{0}'", tool); - - var projectFileName = GetProjectFileName(BuildContext, tool); - - var msBuildSettings = new MSBuildSettings { - Verbosity = Verbosity.Quiet, - //Verbosity = Verbosity.Diagnostic, - ToolVersion = MSBuildToolVersion.Default, - Configuration = BuildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, tool, "build"); - - // SourceLink specific stuff - var repositoryUrl = BuildContext.General.Repository.Url; - var repositoryCommitId = BuildContext.General.Repository.CommitId; - if (!BuildContext.General.SourceLink.IsDisabled && - !BuildContext.General.IsLocalBuild && - !string.IsNullOrWhiteSpace(repositoryUrl)) - { - CakeContext.Information("Repository url is specified, enabling SourceLink to commit '{0}/commit/{1}'", - repositoryUrl, repositoryCommitId); - - // TODO: For now we are assuming everything is git, we might need to change that in the future - // See why we set the values at https://github.com/dotnet/sourcelink/issues/159#issuecomment-427639278 - msBuildSettings.WithProperty("EnableSourceLink", "true"); - msBuildSettings.WithProperty("EnableSourceControlManagerQueries", "false"); - msBuildSettings.WithProperty("PublishRepositoryUrl", "true"); - msBuildSettings.WithProperty("RepositoryType", "git"); - msBuildSettings.WithProperty("RepositoryUrl", repositoryUrl); - msBuildSettings.WithProperty("RevisionId", repositoryCommitId); - - InjectSourceLinkInProjectFile(BuildContext, tool, projectFileName); - } - - RunMsBuild(BuildContext, tool, projectFileName, msBuildSettings, "build"); - } - } - - public override async Task PackageAsync() - { - if (!HasItems()) - { - return; - } - - var configurationName = BuildContext.General.Solution.ConfigurationName; - var version = BuildContext.General.Version.NuGet; - - foreach (var tool in BuildContext.Tools.Items) - { - if (!ShouldPackageProject(BuildContext, tool)) - { - CakeContext.Information("Tool '{0}' should not be packaged", tool); - continue; - } - - BuildContext.CakeContext.LogSeparator("Packaging tool '{0}'", tool); - - var projectDirectory = System.IO.Path.Combine(".", "src", tool); - var projectFileName = System.IO.Path.Combine(projectDirectory, $"{tool}.csproj"); - var outputDirectory = GetProjectOutputDirectory(BuildContext, tool); - CakeContext.Information("Output directory: '{0}'", outputDirectory); - - // Step 1: remove intermediate files to ensure we have the same results on the build server, somehow NuGet - // targets tries to find the resource assemblies in [ProjectName]\obj\Release\net46\de\[ProjectName].resources.dll', - // we won't run a clean on the project since it will clean out the actual output (which we still need for packaging) - - CakeContext.Information("Cleaning intermediate files for tool '{0}'", tool); - - var binFolderPattern = string.Format("{0}/bin/{1}/**.dll", projectDirectory, configurationName); - - CakeContext.Information("Deleting 'bin' directory contents using '{0}'", binFolderPattern); - - var binFiles = CakeContext.GetFiles(binFolderPattern); - CakeContext.DeleteFiles(binFiles); - - var objFolderPattern = string.Format("{0}/obj/{1}/**.dll", projectDirectory, configurationName); - - CakeContext.Information("Deleting 'bin' directory contents using '{0}'", objFolderPattern); - - var objFiles = CakeContext.GetFiles(objFolderPattern); - CakeContext.DeleteFiles(objFiles); - - // We know we *highly likely* need to sign, so try doing this upfront - if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) - { - BuildContext.CakeContext.Information("Searching for packagable files to sign:"); - - var projectFilesToSign = new List(); - - var exeSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{tool}/**/*.exe"; - BuildContext.CakeContext.Information($" - {exeSignFilesSearchPattern}"); - projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(exeSignFilesSearchPattern)); - - var dllSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{tool}/**/*.dll"; - BuildContext.CakeContext.Information($" - {dllSignFilesSearchPattern}"); - projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(dllSignFilesSearchPattern)); - - var signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", BuildContext.General.CodeSign.TimeStampUri, - BuildContext.General.CodeSign.CertificateSubjectName, BuildContext.General.CodeSign.HashAlgorithm); - - SignFiles(BuildContext, signToolCommand, projectFilesToSign); - } - else - { - BuildContext.CakeContext.Warning("No signing certificate subject name provided, not signing any files"); - } - - CakeContext.Information(string.Empty); - - // Step 2: Ensure chocolatey stuff - EnsureChocolateyLicenseFile(tool); - EnsureChocolateyVerificationFile(tool); - - // Step 3: Go packaging! - CakeContext.Information("Using 'msbuild' to package '{0}'", tool); - - var msBuildSettings = new MSBuildSettings - { - Verbosity = Verbosity.Quiet, - //Verbosity = Verbosity.Diagnostic, - ToolVersion = MSBuildToolVersion.Default, - Configuration = configurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, tool, "pack"); - - msBuildSettings.WithProperty("ConfigurationName", configurationName); - msBuildSettings.WithProperty("PackageVersion", version); - - // SourceLink specific stuff - var repositoryUrl = BuildContext.General.Repository.Url; - var repositoryCommitId = BuildContext.General.Repository.CommitId; - if (!BuildContext.General.SourceLink.IsDisabled && - !BuildContext.General.IsLocalBuild && - !string.IsNullOrWhiteSpace(repositoryUrl)) - { - CakeContext.Information("Repository url is specified, adding commit specific data to package"); - - // TODO: For now we are assuming everything is git, we might need to change that in the future - // See why we set the values at https://github.com/dotnet/sourcelink/issues/159#issuecomment-427639278 - msBuildSettings.WithProperty("PublishRepositoryUrl", "true"); - msBuildSettings.WithProperty("RepositoryType", "git"); - msBuildSettings.WithProperty("RepositoryUrl", repositoryUrl); - msBuildSettings.WithProperty("RevisionId", repositoryCommitId); - } - - // Fix for .NET Core 3.0, see https://github.com/dotnet/core-sdk/issues/192, it - // uses obj/release instead of [outputdirectory] - msBuildSettings.WithProperty("DotNetPackIntermediateOutputPath", outputDirectory); - - // No dependencies for tools - msBuildSettings.WithProperty("SuppressDependenciesWhenPacking", "true"); - - // As described in the this issue: https://github.com/NuGet/Home/issues/4360 - // we should not use IsTool, but set BuildOutputTargetFolder instead - msBuildSettings.WithProperty("CopyLocalLockFileAssemblies", "true"); - msBuildSettings.WithProperty("IncludeBuildOutput", "true"); - msBuildSettings.WithProperty("BuildOutputTargetFolder", "tools"); - msBuildSettings.WithProperty("NoDefaultExcludes", "true"); - - // Ensures that files are written to "tools", not "tools\\netcoreapp3.1" - msBuildSettings.WithProperty("IsTool", "false"); - - msBuildSettings.WithProperty("NoBuild", "true"); - msBuildSettings.Targets.Add("Pack"); - - RunMsBuild(BuildContext, tool, projectFileName, msBuildSettings, "pack"); - - BuildContext.CakeContext.LogSeparator(); - } - - var codeSign = (!BuildContext.General.IsCiBuild && - !BuildContext.General.IsLocalBuild && - !string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)); - if (codeSign) - { - // For details, see https://docs.microsoft.com/en-us/nuget/create-packages/sign-a-package - // nuget sign MyPackage.nupkg -CertificateSubjectName -Timestamper - var filesToSign = CakeContext.GetFiles($"{BuildContext.General.OutputRootDirectory}/*.nupkg"); - - foreach (var fileToSign in filesToSign) - { - CakeContext.Information($"Signing NuGet package '{fileToSign}' using certificate subject '{BuildContext.General.CodeSign.CertificateSubjectName}'"); - - var exitCode = CakeContext.StartProcess(BuildContext.General.NuGet.Executable, new ProcessSettings - { - Arguments = $"sign \"{fileToSign}\" -CertificateSubjectName \"{BuildContext.General.CodeSign.CertificateSubjectName}\" -Timestamper \"{BuildContext.General.CodeSign.TimeStampUri}\"" - }); - - CakeContext.Information("Signing NuGet package exited with '{0}'", exitCode); - } - } - } - - public override async Task DeployAsync() - { - if (!HasItems()) - { - return; - } - - var version = BuildContext.General.Version.NuGet; - - foreach (var tool in BuildContext.Tools.Items) - { - if (!ShouldDeployProject(BuildContext, tool)) - { - CakeContext.Information("Tool '{0}' should not be deployed", tool); - continue; - } - - BuildContext.CakeContext.LogSeparator("Deploying tool '{0}'", tool); - - var packageToPush = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, $"{tool}.{version}.nupkg"); - var nuGetRepositoryUrls = GetToolsNuGetRepositoryUrls(tool); - var nuGetRepositoryApiKeys = GetToolsNuGetRepositoryApiKeys(tool); - - var nuGetServers = GetNuGetServers(nuGetRepositoryUrls, nuGetRepositoryApiKeys); - if (nuGetServers.Count == 0) - { - throw new Exception("No NuGet repositories specified, as a protection mechanism this must *always* be specified to make sure packages aren't accidentally deployed to the default public NuGet feed"); - } - - CakeContext.Information("Found '{0}' target NuGet servers to push tool '{1}'", nuGetServers.Count, tool); - - foreach (var nuGetServer in nuGetServers) - { - CakeContext.Information("Pushing to '{0}'", nuGetServer); - - CakeContext.NuGetPush(packageToPush, new NuGetPushSettings - { - Source = nuGetServer.Url, - ApiKey = nuGetServer.ApiKey - }); - } - - await BuildContext.Notifications.NotifyAsync(tool, string.Format("Deployed to NuGet store(s)"), TargetType.Tool); - } - } - - public override async Task FinalizeAsync() - { - - } +#l "tools-variables.cake" + +using System.Xml.Linq; + +//------------------------------------------------------------- + +public class ToolsProcessor : ProcessorBase +{ + public ToolsProcessor(BuildContext buildContext) + : base(buildContext) + { + + } + + private void EnsureChocolateyLicenseFile(string projectName) + { + // Required for Chocolatey + + var projectDirectory = GetProjectDirectory(projectName); + var outputDirectory = GetProjectOutputDirectory(BuildContext, projectName); + + var legalDirectory = System.IO.Path.Combine(outputDirectory, "legal"); + System.IO.Directory.CreateDirectory(legalDirectory); + + // Check if it already exists + var fileName = System.IO.Path.Combine(legalDirectory, "LICENSE.txt"); + if (!CakeContext.FileExists(fileName)) + { + CakeContext.Information("Creating Chocolatey license file for '{0}'", projectName); + + // Option 1: Copy from root + var sourceFile = System.IO.Path.Combine(".", "LICENSE"); + if (CakeContext.FileExists(sourceFile)) + { + CakeContext.Information("Using license file from repository"); + + CakeContext.CopyFile(sourceFile, fileName); + return; + } + + // Option 2: use expression (PackageLicenseExpression) + throw new Exception("Cannot find ./LICENSE, which is required for Chocolatey"); + } + } + + private void EnsureChocolateyVerificationFile(string projectName) + { + // Required for Chocolatey + + var projectDirectory = GetProjectDirectory(projectName); + var outputDirectory = GetProjectOutputDirectory(BuildContext, projectName); + + var legalDirectory = System.IO.Path.Combine(outputDirectory, "legal"); + System.IO.Directory.CreateDirectory(legalDirectory); + + // Check if it already exists + var fileName = System.IO.Path.Combine(legalDirectory, "VERIFICATION.txt"); + if (!CakeContext.FileExists(fileName)) + { + CakeContext.Information("Creating Chocolatey verification file for '{0}'", projectName); + + var verificationBuilder = new StringBuilder(@"VERIFICATION +Verification is intended to assist the Chocolatey moderators and community +in verifying that this package's contents are trustworthy. + +This package is submitted by the software vendor - checksum verification is optional but still included. + +SHA512 CHECKSUMS GENERATED BY BUILD TOOL: +"); + + verificationBuilder.AppendLine(); + + var files = new List(); + + var exePattern = $"{outputDirectory}/**/*.exe"; + files.AddRange(CakeContext.GetFiles(exePattern)); + + var dllPattern = $"{outputDirectory}/**/*.dll"; + files.AddRange(CakeContext.GetFiles(dllPattern)); + + var outputDirectoryPath = new DirectoryPath(outputDirectory); + + foreach (var packageFile in files/*.OrderBy(x => x.FullPath)*/) + { + var relativeFileName = outputDirectoryPath.GetRelativePath(packageFile); + + // Using 'outputDirectory' results in a file like '[ProductName]/netcoreapp3.1/nl/Catel.MVVM.resources.dll', + // so trying to fix the directory to be relative to the output including the target framework by faking + // that the file is 2 directories up + var fixedRelativePathSegments = relativeFileName.Segments.Skip(1).ToArray(); + var fixedRelativePath = new FilePath(System.IO.Path.Combine(fixedRelativePathSegments)); + + var fileHash = CakeContext.CalculateFileHash(packageFile, HashAlgorithm.SHA512); + + verificationBuilder.AppendLine($"* tools/{fixedRelativePath.FullPath} | {fileHash.ToHex()}"); + } + + System.IO.File.WriteAllText(fileName, verificationBuilder.ToString()); + } + } + + private string GetToolsNuGetRepositoryUrls(string projectName) + { + // Allow per project overrides via "NuGetRepositoryUrlFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "ToolsNuGetRepositoryUrlsFor", BuildContext.Tools.NuGetRepositoryUrls); + } + + private string GetToolsNuGetRepositoryApiKeys(string projectName) + { + // Allow per project overrides via "NuGetRepositoryApiKeyFor[ProjectName]" + return GetProjectSpecificConfigurationValue(BuildContext, projectName, "ToolsNuGetRepositoryApiKeysFor", BuildContext.Tools.NuGetRepositoryApiKeys); + } + + public override bool HasItems() + { + return BuildContext.Tools.Items.Count > 0; + } + + public override async Task PrepareAsync() + { + if (!HasItems()) + { + return; + } + + // Check whether projects should be processed, `.ToList()` + // is required to prevent issues with foreach + foreach (var tool in BuildContext.Tools.Items.ToList()) + { + if (!ShouldProcessProject(BuildContext, tool)) + { + BuildContext.Tools.Items.Remove(tool); + } + } + + if (BuildContext.General.IsLocalBuild && BuildContext.General.Target.ToLower().Contains("packagelocal")) + { + foreach (var tool in BuildContext.Tools.Items) + { + var expandableCacheDirectory = System.IO.Path.Combine("%userprofile%", ".nuget", "packages", tool, BuildContext.General.Version.NuGet); + var cacheDirectory = Environment.ExpandEnvironmentVariables(expandableCacheDirectory); + + CakeContext.Information("Checking for existing local NuGet cached version at '{0}'", cacheDirectory); + + var retryCount = 3; + + while (retryCount > 0) + { + if (!CakeContext.DirectoryExists(cacheDirectory)) + { + break; + } + + CakeContext.Information("Deleting already existing NuGet cached version from '{0}'", cacheDirectory); + + CakeContext.DeleteDirectory(cacheDirectory, new DeleteDirectorySettings() + { + Force = true, + Recursive = true + }); + + await System.Threading.Tasks.Task.Delay(1000); + + retryCount--; + } + } + } + } + + public override async Task UpdateInfoAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var tool in BuildContext.Tools.Items) + { + CakeContext.Information("Updating version for tool '{0}'", tool); + + var projectFileName = GetProjectFileName(BuildContext, tool); + + CakeContext.TransformConfig(projectFileName, new TransformationCollection + { + { "Project/PropertyGroup/PackageVersion", BuildContext.General.Version.NuGet } + }); + } + } + + public override async Task BuildAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var tool in BuildContext.Tools.Items) + { + BuildContext.CakeContext.LogSeparator("Building tool '{0}'", tool); + + var projectFileName = GetProjectFileName(BuildContext, tool); + + var msBuildSettings = new MSBuildSettings { + Verbosity = Verbosity.Quiet, + //Verbosity = Verbosity.Diagnostic, + ToolVersion = MSBuildToolVersion.Default, + Configuration = BuildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, tool, "build"); + + // SourceLink specific stuff + var repositoryUrl = BuildContext.General.Repository.Url; + var repositoryCommitId = BuildContext.General.Repository.CommitId; + if (!BuildContext.General.SourceLink.IsDisabled && + !BuildContext.General.IsLocalBuild && + !string.IsNullOrWhiteSpace(repositoryUrl)) + { + CakeContext.Information("Repository url is specified, enabling SourceLink to commit '{0}/commit/{1}'", + repositoryUrl, repositoryCommitId); + + // TODO: For now we are assuming everything is git, we might need to change that in the future + // See why we set the values at https://github.com/dotnet/sourcelink/issues/159#issuecomment-427639278 + msBuildSettings.WithProperty("EnableSourceLink", "true"); + msBuildSettings.WithProperty("EnableSourceControlManagerQueries", "false"); + msBuildSettings.WithProperty("PublishRepositoryUrl", "true"); + msBuildSettings.WithProperty("RepositoryType", "git"); + msBuildSettings.WithProperty("RepositoryUrl", repositoryUrl); + msBuildSettings.WithProperty("RevisionId", repositoryCommitId); + + InjectSourceLinkInProjectFile(BuildContext, tool, projectFileName); + } + + RunMsBuild(BuildContext, tool, projectFileName, msBuildSettings, "build"); + } + } + + public override async Task PackageAsync() + { + if (!HasItems()) + { + return; + } + + var configurationName = BuildContext.General.Solution.ConfigurationName; + var version = BuildContext.General.Version.NuGet; + + foreach (var tool in BuildContext.Tools.Items) + { + if (!ShouldPackageProject(BuildContext, tool)) + { + CakeContext.Information("Tool '{0}' should not be packaged", tool); + continue; + } + + BuildContext.CakeContext.LogSeparator("Packaging tool '{0}'", tool); + + var projectDirectory = System.IO.Path.Combine(".", "src", tool); + var projectFileName = System.IO.Path.Combine(projectDirectory, $"{tool}.csproj"); + var outputDirectory = GetProjectOutputDirectory(BuildContext, tool); + CakeContext.Information("Output directory: '{0}'", outputDirectory); + + // Step 1: remove intermediate files to ensure we have the same results on the build server, somehow NuGet + // targets tries to find the resource assemblies in [ProjectName]\obj\Release\net46\de\[ProjectName].resources.dll', + // we won't run a clean on the project since it will clean out the actual output (which we still need for packaging) + + CakeContext.Information("Cleaning intermediate files for tool '{0}'", tool); + + var binFolderPattern = string.Format("{0}/bin/{1}/**.dll", projectDirectory, configurationName); + + CakeContext.Information("Deleting 'bin' directory contents using '{0}'", binFolderPattern); + + var binFiles = CakeContext.GetFiles(binFolderPattern); + CakeContext.DeleteFiles(binFiles); + + var objFolderPattern = string.Format("{0}/obj/{1}/**.dll", projectDirectory, configurationName); + + CakeContext.Information("Deleting 'bin' directory contents using '{0}'", objFolderPattern); + + var objFiles = CakeContext.GetFiles(objFolderPattern); + CakeContext.DeleteFiles(objFiles); + + // We know we *highly likely* need to sign, so try doing this upfront + if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) + { + BuildContext.CakeContext.Information("Searching for packagable files to sign:"); + + var projectFilesToSign = new List(); + + var exeSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{tool}/**/*.exe"; + BuildContext.CakeContext.Information($" - {exeSignFilesSearchPattern}"); + projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(exeSignFilesSearchPattern)); + + var dllSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{tool}/**/*.dll"; + BuildContext.CakeContext.Information($" - {dllSignFilesSearchPattern}"); + projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(dllSignFilesSearchPattern)); + + var signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", BuildContext.General.CodeSign.TimeStampUri, + BuildContext.General.CodeSign.CertificateSubjectName, BuildContext.General.CodeSign.HashAlgorithm); + + SignFiles(BuildContext, signToolCommand, projectFilesToSign); + } + else + { + BuildContext.CakeContext.Warning("No signing certificate subject name provided, not signing any files"); + } + + CakeContext.Information(string.Empty); + + // Step 2: Ensure chocolatey stuff + EnsureChocolateyLicenseFile(tool); + EnsureChocolateyVerificationFile(tool); + + // Step 3: Go packaging! + CakeContext.Information("Using 'msbuild' to package '{0}'", tool); + + var msBuildSettings = new MSBuildSettings + { + Verbosity = Verbosity.Quiet, + //Verbosity = Verbosity.Diagnostic, + ToolVersion = MSBuildToolVersion.Default, + Configuration = configurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, tool, "pack"); + + msBuildSettings.WithProperty("ConfigurationName", configurationName); + msBuildSettings.WithProperty("PackageVersion", version); + + // SourceLink specific stuff + var repositoryUrl = BuildContext.General.Repository.Url; + var repositoryCommitId = BuildContext.General.Repository.CommitId; + if (!BuildContext.General.SourceLink.IsDisabled && + !BuildContext.General.IsLocalBuild && + !string.IsNullOrWhiteSpace(repositoryUrl)) + { + CakeContext.Information("Repository url is specified, adding commit specific data to package"); + + // TODO: For now we are assuming everything is git, we might need to change that in the future + // See why we set the values at https://github.com/dotnet/sourcelink/issues/159#issuecomment-427639278 + msBuildSettings.WithProperty("PublishRepositoryUrl", "true"); + msBuildSettings.WithProperty("RepositoryType", "git"); + msBuildSettings.WithProperty("RepositoryUrl", repositoryUrl); + msBuildSettings.WithProperty("RevisionId", repositoryCommitId); + } + + // Fix for .NET Core 3.0, see https://github.com/dotnet/core-sdk/issues/192, it + // uses obj/release instead of [outputdirectory] + msBuildSettings.WithProperty("DotNetPackIntermediateOutputPath", outputDirectory); + + // No dependencies for tools + msBuildSettings.WithProperty("SuppressDependenciesWhenPacking", "true"); + + // As described in the this issue: https://github.com/NuGet/Home/issues/4360 + // we should not use IsTool, but set BuildOutputTargetFolder instead + msBuildSettings.WithProperty("CopyLocalLockFileAssemblies", "true"); + msBuildSettings.WithProperty("IncludeBuildOutput", "true"); + msBuildSettings.WithProperty("BuildOutputTargetFolder", "tools"); + msBuildSettings.WithProperty("NoDefaultExcludes", "true"); + + // Ensures that files are written to "tools", not "tools\\netcoreapp3.1" + msBuildSettings.WithProperty("IsTool", "false"); + + msBuildSettings.WithProperty("NoBuild", "true"); + msBuildSettings.Targets.Add("Pack"); + + RunMsBuild(BuildContext, tool, projectFileName, msBuildSettings, "pack"); + + BuildContext.CakeContext.LogSeparator(); + } + + var codeSign = (!BuildContext.General.IsCiBuild && + !BuildContext.General.IsLocalBuild && + !string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)); + if (codeSign) + { + // For details, see https://docs.microsoft.com/en-us/nuget/create-packages/sign-a-package + // nuget sign MyPackage.nupkg -CertificateSubjectName -Timestamper + var filesToSign = CakeContext.GetFiles($"{BuildContext.General.OutputRootDirectory}/*.nupkg"); + + foreach (var fileToSign in filesToSign) + { + CakeContext.Information($"Signing NuGet package '{fileToSign}' using certificate subject '{BuildContext.General.CodeSign.CertificateSubjectName}'"); + + var exitCode = CakeContext.StartProcess(BuildContext.General.NuGet.Executable, new ProcessSettings + { + Arguments = $"sign \"{fileToSign}\" -CertificateSubjectName \"{BuildContext.General.CodeSign.CertificateSubjectName}\" -Timestamper \"{BuildContext.General.CodeSign.TimeStampUri}\"" + }); + + CakeContext.Information("Signing NuGet package exited with '{0}'", exitCode); + } + } + } + + public override async Task DeployAsync() + { + if (!HasItems()) + { + return; + } + + var version = BuildContext.General.Version.NuGet; + + foreach (var tool in BuildContext.Tools.Items) + { + if (!ShouldDeployProject(BuildContext, tool)) + { + CakeContext.Information("Tool '{0}' should not be deployed", tool); + continue; + } + + BuildContext.CakeContext.LogSeparator("Deploying tool '{0}'", tool); + + var packageToPush = System.IO.Path.Combine(BuildContext.General.OutputRootDirectory, $"{tool}.{version}.nupkg"); + var nuGetRepositoryUrls = GetToolsNuGetRepositoryUrls(tool); + var nuGetRepositoryApiKeys = GetToolsNuGetRepositoryApiKeys(tool); + + var nuGetServers = GetNuGetServers(nuGetRepositoryUrls, nuGetRepositoryApiKeys); + if (nuGetServers.Count == 0) + { + throw new Exception("No NuGet repositories specified, as a protection mechanism this must *always* be specified to make sure packages aren't accidentally deployed to the default public NuGet feed"); + } + + CakeContext.Information("Found '{0}' target NuGet servers to push tool '{1}'", nuGetServers.Count, tool); + + foreach (var nuGetServer in nuGetServers) + { + CakeContext.Information("Pushing to '{0}'", nuGetServer); + + CakeContext.NuGetPush(packageToPush, new NuGetPushSettings + { + Source = nuGetServer.Url, + ApiKey = nuGetServer.ApiKey + }); + } + + await BuildContext.Notifications.NotifyAsync(tool, string.Format("Deployed to NuGet store(s)"), TargetType.Tool); + } + } + + public override async Task FinalizeAsync() + { + + } } \ No newline at end of file diff --git a/deployment/cake/tools-variables.cake b/deployment/cake/tools-variables.cake index a90a4bb1..92b9eca0 100644 --- a/deployment/cake/tools-variables.cake +++ b/deployment/cake/tools-variables.cake @@ -1,55 +1,55 @@ -#l "buildserver.cake" - -//------------------------------------------------------------- - -public class ToolsContext : BuildContextWithItemsBase -{ - public ToolsContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string NuGetRepositoryUrls { get; set; } - public string NuGetRepositoryApiKeys { get; set; } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' tool projects"); - } -} - -//------------------------------------------------------------- - -private ToolsContext InitializeToolsContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new ToolsContext(parentBuildContext) - { - Items = Tools ?? new List(), - NuGetRepositoryUrls = buildContext.BuildServer.GetVariable("ToolsNuGetRepositoryUrls", showValue: true), - NuGetRepositoryApiKeys = buildContext.BuildServer.GetVariable("ToolsNuGetRepositoryApiKeys", showValue: false) - }; - - return data; -} - -//------------------------------------------------------------- - -List _tools; - -public List Tools -{ - get - { - if (_tools is null) - { - _tools = new List(); - } - - return _tools; - } +#l "buildserver.cake" + +//------------------------------------------------------------- + +public class ToolsContext : BuildContextWithItemsBase +{ + public ToolsContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string NuGetRepositoryUrls { get; set; } + public string NuGetRepositoryApiKeys { get; set; } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' tool projects"); + } +} + +//------------------------------------------------------------- + +private ToolsContext InitializeToolsContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new ToolsContext(parentBuildContext) + { + Items = Tools ?? new List(), + NuGetRepositoryUrls = buildContext.BuildServer.GetVariable("ToolsNuGetRepositoryUrls", showValue: true), + NuGetRepositoryApiKeys = buildContext.BuildServer.GetVariable("ToolsNuGetRepositoryApiKeys", showValue: false) + }; + + return data; +} + +//------------------------------------------------------------- + +List _tools; + +public List Tools +{ + get + { + if (_tools is null) + { + _tools = new List(); + } + + return _tools; + } } \ No newline at end of file diff --git a/deployment/cake/vsextensions-tasks.cake b/deployment/cake/vsextensions-tasks.cake index 9903d1dd..5e9c2188 100644 --- a/deployment/cake/vsextensions-tasks.cake +++ b/deployment/cake/vsextensions-tasks.cake @@ -1,171 +1,171 @@ -#l "vsextensions-variables.cake" - -using System.Xml.Linq; - -//------------------------------------------------------------- - -public class VsExtensionsProcessor : ProcessorBase -{ - public VsExtensionsProcessor(BuildContext buildContext) - : base(buildContext) - { - - } - - public override bool HasItems() - { - return BuildContext.VsExtensions.Items.Count > 0; - } - - public override async Task PrepareAsync() - { - if (!HasItems()) - { - return; - } - - // Check whether projects should be processed, `.ToList()` - // is required to prevent issues with foreach - foreach (var vsExtension in BuildContext.VsExtensions.Items.ToList()) - { - if (!ShouldProcessProject(BuildContext, vsExtension)) - { - BuildContext.VsExtensions.Items.Remove(vsExtension); - } - } - } - - public override async Task UpdateInfoAsync() - { - if (!HasItems()) - { - return; - } - - // Note: since we can't use prerelease tags in VSIX, we will use the commit count - // as last part of the version - var version = string.Format("{0}.{1}", BuildContext.General.Version.MajorMinorPatch, BuildContext.General.Version.CommitsSinceVersionSource); - - foreach (var vsExtension in BuildContext.VsExtensions.Items) - { - CakeContext.Information("Updating version for vs extension '{0}'", vsExtension); - - var projectDirectory = GetProjectDirectory(vsExtension); - - // Step 1: update vsix manifest - var vsixManifestFileName = System.IO.Path.Combine(projectDirectory, "source.extension.vsixmanifest"); - - CakeContext.TransformConfig(vsixManifestFileName, new TransformationCollection - { - { "PackageManifest/Metadata/Identity/@Version", version }, - { "PackageManifest/Metadata/Identity/@Publisher", BuildContext.VsExtensions.PublisherName } - }); - } - } - - public override async Task BuildAsync() - { - if (!HasItems()) - { - return; - } - - foreach (var vsExtension in BuildContext.VsExtensions.Items) - { - BuildContext.CakeContext.LogSeparator("Building vs extension '{0}'", vsExtension); - - var projectFileName = GetProjectFileName(BuildContext, vsExtension); - - var msBuildSettings = new MSBuildSettings { - Verbosity = Verbosity.Quiet, - //Verbosity = Verbosity.Diagnostic, - ToolVersion = MSBuildToolVersion.Default, - Configuration = BuildContext.General.Solution.ConfigurationName, - MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform - PlatformTarget = PlatformTarget.MSIL - }; - - ConfigureMsBuild(BuildContext, msBuildSettings, vsExtension, "build"); - - // Note: we need to set OverridableOutputPath because we need to be able to respect - // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which - // are properties passed in using the command line) - var outputDirectory = GetProjectOutputDirectory(BuildContext, vsExtension); - CakeContext.Information("Output directory: '{0}'", outputDirectory); - - // Since vs extensions (for now) use the old csproj style, make sure - // to override the output path as well - msBuildSettings.WithProperty("OutputPath", outputDirectory); - - RunMsBuild(BuildContext, vsExtension, projectFileName, msBuildSettings, "build"); - } - } - - public override async Task PackageAsync() - { - if (!HasItems()) - { - return; - } - - // No packaging required - } - - public override async Task DeployAsync() - { - if (!HasItems()) - { - return; - } - - var vsixPublisherExeDirectory = System.IO.Path.Combine(GetVisualStudioDirectory(BuildContext), "VSSDK", "VisualStudioIntegration", "Tools", "Bin"); - var vsixPublisherExeFileName = System.IO.Path.Combine(vsixPublisherExeDirectory, "VsixPublisher.exe"); - - foreach (var vsExtension in BuildContext.VsExtensions.Items) - { - if (!ShouldDeployProject(BuildContext, vsExtension)) - { - CakeContext.Information("Vs extension '{0}' should not be deployed", vsExtension); - continue; - } - - BuildContext.CakeContext.LogSeparator("Deploying vs extension '{0}'", vsExtension); - - // Step 1: copy the output stuff - var vsExtensionOutputDirectory = GetProjectOutputDirectory(BuildContext, vsExtension); - var payloadFileName = System.IO.Path.Combine(vsExtensionOutputDirectory, $"{vsExtension}.vsix"); - - var overviewSourceFileName = System.IO.Path.Combine("src", vsExtension, "overview.md"); - var overviewTargetFileName = System.IO.Path.Combine(vsExtensionOutputDirectory, "overview.md"); - CakeContext.CopyFile(overviewSourceFileName, overviewTargetFileName); - - var vsGalleryManifestSourceFileName = System.IO.Path.Combine("src", vsExtension, "source.extension.vsgallerymanifest"); - var vsGalleryManifestTargetFileName = System.IO.Path.Combine(vsExtensionOutputDirectory, "source.extension.vsgallerymanifest"); - CakeContext.CopyFile(vsGalleryManifestSourceFileName, vsGalleryManifestTargetFileName); - - // Step 2: update vs gallery manifest - var fileContents = System.IO.File.ReadAllText(vsGalleryManifestTargetFileName); - - fileContents = fileContents.Replace("[PUBLISHERNAME]", BuildContext.VsExtensions.PublisherName); - - System.IO.File.WriteAllText(vsGalleryManifestTargetFileName, fileContents); - - // Step 3: go ahead and publish - CakeContext.StartProcess(vsixPublisherExeFileName, new ProcessSettings - { - Arguments = new ProcessArgumentBuilder() - .Append("publish") - .AppendSwitch("-payload", payloadFileName) - .AppendSwitch("-publishManifest", vsGalleryManifestTargetFileName) - .AppendSwitchSecret("-personalAccessToken", BuildContext.VsExtensions.PersonalAccessToken) - }); - - await BuildContext.Notifications.NotifyAsync(vsExtension, string.Format("Deployed to Visual Studio Gallery"), TargetType.VsExtension); - } - } - - public override async Task FinalizeAsync() - { - - } -} +#l "vsextensions-variables.cake" + +using System.Xml.Linq; + +//------------------------------------------------------------- + +public class VsExtensionsProcessor : ProcessorBase +{ + public VsExtensionsProcessor(BuildContext buildContext) + : base(buildContext) + { + + } + + public override bool HasItems() + { + return BuildContext.VsExtensions.Items.Count > 0; + } + + public override async Task PrepareAsync() + { + if (!HasItems()) + { + return; + } + + // Check whether projects should be processed, `.ToList()` + // is required to prevent issues with foreach + foreach (var vsExtension in BuildContext.VsExtensions.Items.ToList()) + { + if (!ShouldProcessProject(BuildContext, vsExtension)) + { + BuildContext.VsExtensions.Items.Remove(vsExtension); + } + } + } + + public override async Task UpdateInfoAsync() + { + if (!HasItems()) + { + return; + } + + // Note: since we can't use prerelease tags in VSIX, we will use the commit count + // as last part of the version + var version = string.Format("{0}.{1}", BuildContext.General.Version.MajorMinorPatch, BuildContext.General.Version.CommitsSinceVersionSource); + + foreach (var vsExtension in BuildContext.VsExtensions.Items) + { + CakeContext.Information("Updating version for vs extension '{0}'", vsExtension); + + var projectDirectory = GetProjectDirectory(vsExtension); + + // Step 1: update vsix manifest + var vsixManifestFileName = System.IO.Path.Combine(projectDirectory, "source.extension.vsixmanifest"); + + CakeContext.TransformConfig(vsixManifestFileName, new TransformationCollection + { + { "PackageManifest/Metadata/Identity/@Version", version }, + { "PackageManifest/Metadata/Identity/@Publisher", BuildContext.VsExtensions.PublisherName } + }); + } + } + + public override async Task BuildAsync() + { + if (!HasItems()) + { + return; + } + + foreach (var vsExtension in BuildContext.VsExtensions.Items) + { + BuildContext.CakeContext.LogSeparator("Building vs extension '{0}'", vsExtension); + + var projectFileName = GetProjectFileName(BuildContext, vsExtension); + + var msBuildSettings = new MSBuildSettings { + Verbosity = Verbosity.Quiet, + //Verbosity = Verbosity.Diagnostic, + ToolVersion = MSBuildToolVersion.Default, + Configuration = BuildContext.General.Solution.ConfigurationName, + MSBuildPlatform = MSBuildPlatform.x86, // Always require x86, see platform for actual target platform + PlatformTarget = PlatformTarget.MSIL + }; + + ConfigureMsBuild(BuildContext, msBuildSettings, vsExtension, "build"); + + // Note: we need to set OverridableOutputPath because we need to be able to respect + // AppendTargetFrameworkToOutputPath which isn't possible for global properties (which + // are properties passed in using the command line) + var outputDirectory = GetProjectOutputDirectory(BuildContext, vsExtension); + CakeContext.Information("Output directory: '{0}'", outputDirectory); + + // Since vs extensions (for now) use the old csproj style, make sure + // to override the output path as well + msBuildSettings.WithProperty("OutputPath", outputDirectory); + + RunMsBuild(BuildContext, vsExtension, projectFileName, msBuildSettings, "build"); + } + } + + public override async Task PackageAsync() + { + if (!HasItems()) + { + return; + } + + // No packaging required + } + + public override async Task DeployAsync() + { + if (!HasItems()) + { + return; + } + + var vsixPublisherExeDirectory = System.IO.Path.Combine(GetVisualStudioDirectory(BuildContext), "VSSDK", "VisualStudioIntegration", "Tools", "Bin"); + var vsixPublisherExeFileName = System.IO.Path.Combine(vsixPublisherExeDirectory, "VsixPublisher.exe"); + + foreach (var vsExtension in BuildContext.VsExtensions.Items) + { + if (!ShouldDeployProject(BuildContext, vsExtension)) + { + CakeContext.Information("Vs extension '{0}' should not be deployed", vsExtension); + continue; + } + + BuildContext.CakeContext.LogSeparator("Deploying vs extension '{0}'", vsExtension); + + // Step 1: copy the output stuff + var vsExtensionOutputDirectory = GetProjectOutputDirectory(BuildContext, vsExtension); + var payloadFileName = System.IO.Path.Combine(vsExtensionOutputDirectory, $"{vsExtension}.vsix"); + + var overviewSourceFileName = System.IO.Path.Combine("src", vsExtension, "overview.md"); + var overviewTargetFileName = System.IO.Path.Combine(vsExtensionOutputDirectory, "overview.md"); + CakeContext.CopyFile(overviewSourceFileName, overviewTargetFileName); + + var vsGalleryManifestSourceFileName = System.IO.Path.Combine("src", vsExtension, "source.extension.vsgallerymanifest"); + var vsGalleryManifestTargetFileName = System.IO.Path.Combine(vsExtensionOutputDirectory, "source.extension.vsgallerymanifest"); + CakeContext.CopyFile(vsGalleryManifestSourceFileName, vsGalleryManifestTargetFileName); + + // Step 2: update vs gallery manifest + var fileContents = System.IO.File.ReadAllText(vsGalleryManifestTargetFileName); + + fileContents = fileContents.Replace("[PUBLISHERNAME]", BuildContext.VsExtensions.PublisherName); + + System.IO.File.WriteAllText(vsGalleryManifestTargetFileName, fileContents); + + // Step 3: go ahead and publish + CakeContext.StartProcess(vsixPublisherExeFileName, new ProcessSettings + { + Arguments = new ProcessArgumentBuilder() + .Append("publish") + .AppendSwitch("-payload", payloadFileName) + .AppendSwitch("-publishManifest", vsGalleryManifestTargetFileName) + .AppendSwitchSecret("-personalAccessToken", BuildContext.VsExtensions.PersonalAccessToken) + }); + + await BuildContext.Notifications.NotifyAsync(vsExtension, string.Format("Deployed to Visual Studio Gallery"), TargetType.VsExtension); + } + } + + public override async Task FinalizeAsync() + { + + } +} diff --git a/deployment/cake/vsextensions-variables.cake b/deployment/cake/vsextensions-variables.cake index 62618f0f..63e55e90 100644 --- a/deployment/cake/vsextensions-variables.cake +++ b/deployment/cake/vsextensions-variables.cake @@ -1,53 +1,53 @@ -#l "buildserver.cake" - -public class VsExtensionsContext : BuildContextWithItemsBase -{ - public VsExtensionsContext(IBuildContext parentBuildContext) - : base(parentBuildContext) - { - } - - public string PublisherName { get; set; } - public string PersonalAccessToken { get; set; } - - protected override void ValidateContext() - { - - } - - protected override void LogStateInfoForContext() - { - CakeContext.Information($"Found '{Items.Count}' vs extension projects"); - } -} - -//------------------------------------------------------------- - -private VsExtensionsContext InitializeVsExtensionsContext(BuildContext buildContext, IBuildContext parentBuildContext) -{ - var data = new VsExtensionsContext(parentBuildContext) - { - Items = VsExtensions ?? new List(), - PublisherName = buildContext.BuildServer.GetVariable("VsExtensionsPublisherName", showValue: true), - PersonalAccessToken = buildContext.BuildServer.GetVariable("VsExtensionsPersonalAccessToken", showValue: false), - }; - - return data; -} - -//------------------------------------------------------------- - -List _vsExtensions; - -public List VsExtensions -{ - get - { - if (_vsExtensions is null) - { - _vsExtensions = new List(); - } - - return _vsExtensions; - } +#l "buildserver.cake" + +public class VsExtensionsContext : BuildContextWithItemsBase +{ + public VsExtensionsContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string PublisherName { get; set; } + public string PersonalAccessToken { get; set; } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + CakeContext.Information($"Found '{Items.Count}' vs extension projects"); + } +} + +//------------------------------------------------------------- + +private VsExtensionsContext InitializeVsExtensionsContext(BuildContext buildContext, IBuildContext parentBuildContext) +{ + var data = new VsExtensionsContext(parentBuildContext) + { + Items = VsExtensions ?? new List(), + PublisherName = buildContext.BuildServer.GetVariable("VsExtensionsPublisherName", showValue: true), + PersonalAccessToken = buildContext.BuildServer.GetVariable("VsExtensionsPersonalAccessToken", showValue: false), + }; + + return data; +} + +//------------------------------------------------------------- + +List _vsExtensions; + +public List VsExtensions +{ + get + { + if (_vsExtensions is null) + { + _vsExtensions = new List(); + } + + return _vsExtensions; + } } \ No newline at end of file