diff --git a/Velopack.sln b/Velopack.sln index 7056d9e1..3e9896d2 100644 --- a/Velopack.sln +++ b/Velopack.sln @@ -51,6 +51,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Divergic.Logging.Xunit", "t EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velopack.Packaging.HostModel", "src\Velopack.Packaging.HostModel\Velopack.Packaging.HostModel.csproj", "{E9A2620C-C638-446C-BA30-F62C05709365}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velopack.Build", "src\Velopack.Build\Velopack.Build.csproj", "{97C9B2CF-877F-4C98-A513-058784A23697}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -117,6 +119,10 @@ Global {E9A2620C-C638-446C-BA30-F62C05709365}.Debug|Any CPU.Build.0 = Debug|Any CPU {E9A2620C-C638-446C-BA30-F62C05709365}.Release|Any CPU.ActiveCfg = Release|Any CPU {E9A2620C-C638-446C-BA30-F62C05709365}.Release|Any CPU.Build.0 = Release|Any CPU + {97C9B2CF-877F-4C98-A513-058784A23697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97C9B2CF-877F-4C98-A513-058784A23697}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97C9B2CF-877F-4C98-A513-058784A23697}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97C9B2CF-877F-4C98-A513-058784A23697}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/AvaloniaCrossPlat/AvaloniaCrossPlat.csproj b/samples/AvaloniaCrossPlat/AvaloniaCrossPlat.csproj index cded3152..cde569b6 100644 --- a/samples/AvaloniaCrossPlat/AvaloniaCrossPlat.csproj +++ b/samples/AvaloniaCrossPlat/AvaloniaCrossPlat.csproj @@ -40,4 +40,6 @@ + + diff --git a/src/Velopack.Build/MSBuildAsyncTask.cs b/src/Velopack.Build/MSBuildAsyncTask.cs new file mode 100644 index 00000000..31432887 --- /dev/null +++ b/src/Velopack.Build/MSBuildAsyncTask.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using MSBuildTask = Microsoft.Build.Utilities.Task; + +namespace Velopack.Build; + +public abstract class MSBuildAsyncTask : MSBuildTask +{ + protected MSBuildLogger Logger { get; } + + protected MSBuildAsyncTask() + { + Logger = new MSBuildLogger(Log); + } + + public sealed override bool Execute() + { + try { + return Task.Run(ExecuteAsync).Result; + } catch (AggregateException ex) { + ex.Flatten().Handle((x) => { + Log.LogError(x.Message); + return true; + }); + return false; + } + } + + protected abstract Task ExecuteAsync(); +} diff --git a/src/Velopack.Build/MSBuildLogger.cs b/src/Velopack.Build/MSBuildLogger.cs new file mode 100644 index 00000000..9d1315a5 --- /dev/null +++ b/src/Velopack.Build/MSBuildLogger.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Extensions.Logging; +using Velopack.Packaging.Abstractions; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using Task = System.Threading.Tasks.Task; + +namespace Velopack.Build; + +public class MSBuildLogger(TaskLoggingHelper loggingHelper) : ILogger, IFancyConsole, IFancyConsoleProgress +{ + private TaskLoggingHelper LoggingHelper { get; } = loggingHelper; + + IDisposable ILogger.BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public async Task ExecuteProgressAsync(Func action) + { + await action(this).ConfigureAwait(false); + } + + public async Task RunTask(string name, Func, Task> fn) + { + try { + await fn(x => { }).ConfigureAwait(false); + } catch (Exception ex) { + this.LogError(ex, "Error running task {0}", name); + } + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel switch { + LogLevel.Trace => LoggingHelper.LogsMessagesOfImportance(MessageImportance.Low), + LogLevel.Debug => LoggingHelper.LogsMessagesOfImportance(MessageImportance.Normal), + LogLevel.Information => LoggingHelper.LogsMessagesOfImportance(MessageImportance.High), + _ => true, + }; + } + + public void Log(LogLevel logLevel, EventId _, TState state, Exception? exception, Func formatter) + { + string message = formatter(state, exception); + if (exception != null) { + message += " " + exception.Message; + } + switch (logLevel) { + case LogLevel.Trace: + LoggingHelper.LogMessage(MessageImportance.Low, message); + break; + case LogLevel.Debug: + LoggingHelper.LogMessage(MessageImportance.Normal, message); + break; + case LogLevel.Information: + LoggingHelper.LogMessage(MessageImportance.High, message); + break; + case LogLevel.Warning: + LoggingHelper.LogWarning(message); + break; + case LogLevel.Error: + case LogLevel.Critical: + LoggingHelper.LogError(message); + break; + } + } + + public void WriteTable(string tableName, IEnumerable> rows, bool hasHeaderRow = true) + { + //Do we need this output for MSBuild? + } + + public System.Threading.Tasks.Task PromptYesNo(string prompt, bool? defaultValue = null, TimeSpan? timeout = null) + { + //TODO: This API is problematic as it assumes interactive. + return Task.FromResult(true); + } + + public void WriteLine(string text = "") + { + Log(LogLevel.Information, 0, null, null, (object? state, Exception? exception) => text); + } +} diff --git a/src/Velopack.Build/PackTask.cs b/src/Velopack.Build/PackTask.cs new file mode 100644 index 00000000..3de19eb4 --- /dev/null +++ b/src/Velopack.Build/PackTask.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Build.Framework; +using Velopack.Packaging; +using Velopack.Packaging.Windows.Commands; + +namespace Velopack.Build; + +public class PackTask : MSBuildAsyncTask +{ + public string? TargetRuntime { get; set; } + + [Required] + public string PackVersion { get; set; } = ""; + + [Required] + public string Runtimes { get; set; } = ""; + + [Required] + public string PackId { get; set; } = ""; + + [Required] + public string PackDirectory { get; set; } = null!; + + [Required] + public string ReleaseDirectory { get; set; } = null!; + + protected override async Task ExecuteAsync() + { + //System.Diagnostics.Debugger.Launch(); + HelperFile.AddSearchPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); + + if (VelopackRuntimeInfo.IsWindows) { + + var targetRuntime = RID.Parse(TargetRuntime ?? VelopackRuntimeInfo.SystemOs.GetOsShortName()); + if (targetRuntime.BaseRID == RuntimeOs.Unknown) { + //TODO: handle this error case + } + + DirectoryInfo releaseDir = new(ReleaseDirectory); + releaseDir.Create(); + + var runner = new WindowsPackCommandRunner(Logger, Logger); + await runner.Run(new WindowsPackOptions() { + PackId = PackId, + ReleaseDir = releaseDir, + PackDirectory = PackDirectory, + Runtimes = Runtimes, + TargetRuntime = targetRuntime, + PackVersion = PackVersion, + }).ConfigureAwait(false); + + Log.LogMessage(MessageImportance.High, $"{PackId} ({PackVersion}) created in {ReleaseDirectory}"); + } else if (VelopackRuntimeInfo.IsOSX) { + //TODO: Implement + + } else if (VelopackRuntimeInfo.IsLinux) { + //TODO: Implement + + } else { + //TODO: Do we really want to fail to pack (effectively failing the user's publish, or should we just warn? + throw new NotSupportedException("Unsupported OS platform: " + VelopackRuntimeInfo.SystemOs.GetOsLongName()); + } + return true; + } +} diff --git a/src/Velopack.Build/PublishTask.cs b/src/Velopack.Build/PublishTask.cs new file mode 100644 index 00000000..78e03c03 --- /dev/null +++ b/src/Velopack.Build/PublishTask.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; +using NuGet.Versioning; +using Velopack.Packaging; +using Velopack.Packaging.Flow; + +namespace Velopack.Build; + +public class PublishTask : MSBuildAsyncTask +{ + private static HttpClient HttpClient { get; } = new(); + + [Required] + public string ReleaseDirectory { get; set; } = ""; + + public string ServiceUrl { get; set; } = VelopackServiceOptions.DefaultBaseUrl; + + public string? Channel { get; set; } + + public string? Version { get; set; } + + protected override async Task ExecuteAsync() + { + //System.Diagnostics.Debugger.Launch(); + VelopackFlowServiceClient client = new(HttpClient, Logger); + if (!await client.LoginAsync(new() { + AllowDeviceCodeFlow = false, + AllowInteractiveLogin = false, + VelopackBaseUrl = ServiceUrl + }).ConfigureAwait(false)) { + Logger.LogWarning("Not logged into Velopack service, skipping publish. Please run vpk login."); + return true; + } + + Channel ??= ReleaseEntryHelper.GetDefaultChannel(VelopackRuntimeInfo.SystemOs); + ReleaseEntryHelper helper = new(ReleaseDirectory, Channel, Logger); + var latestAssets = helper.GetLatestAssets().ToList(); + + List installers = []; + + List files = latestAssets.Select(x => x.FileName).ToList(); + string? packageId = null; + SemanticVersion? version = null; + if (latestAssets.Count > 0) { + packageId = latestAssets[0].PackageId; + version = latestAssets[0].Version; + + if (VelopackRuntimeInfo.IsWindows || VelopackRuntimeInfo.IsOSX) { + var setupName = ReleaseEntryHelper.GetSuggestedSetupName(packageId, Channel); + if (File.Exists(Path.Combine(ReleaseDirectory, setupName))) { + installers.Add(setupName); + } + } + + var portableName = ReleaseEntryHelper.GetSuggestedPortableName(packageId, Channel); + if (File.Exists(Path.Combine(ReleaseDirectory, portableName))) { + installers.Add(portableName); + } + } + + Logger.LogInformation("Preparing to upload {AssetCount} assets to Velopack ({ServiceUrl})", latestAssets.Count + installers.Count, ServiceUrl); + + foreach (var assetFileName in files) { + + var latestPath = Path.Combine(ReleaseDirectory, assetFileName); + + using var fileStream = File.OpenRead(latestPath); + var options = new UploadOptions(fileStream, assetFileName, Channel) { + VelopackBaseUrl = ServiceUrl + }; + + await client.UploadReleaseAssetAsync(options).ConfigureAwait(false); + + Logger.LogInformation("Uploaded {FileName} to Velopack", assetFileName); + } + + foreach(var installerFile in installers) { + var latestPath = Path.Combine(ReleaseDirectory, installerFile); + + using var fileStream = File.OpenRead(latestPath); + var options = new UploadInstallerOptions(packageId!, version!, fileStream, installerFile, Channel) { + VelopackBaseUrl = ServiceUrl + }; + + await client.UploadInstallerAssetAsync(options).ConfigureAwait(false); + + Logger.LogInformation("Uploaded {FileName} installer to Velopack", installerFile); + } + return true; + + } +} diff --git a/src/Velopack.Build/Velopack.Build.csproj b/src/Velopack.Build/Velopack.Build.csproj new file mode 100644 index 00000000..14f5e1d0 --- /dev/null +++ b/src/Velopack.Build/Velopack.Build.csproj @@ -0,0 +1,77 @@ + + + + net472;net6.0 + enable + 12 + + true + + + + + + + + + + + + + ..\Rust\target + UpdateMac + + + + + PreserveNewest + + + + + ..\Rust\target + UpdateNix + + + + + PreserveNewest + + + + + ..\Rust\target\$(Configuration.ToLower()) + update.exe + stub.exe + setup.exe + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + diff --git a/src/Velopack.Build/Velopack.Build.targets b/src/Velopack.Build/Velopack.Build.targets new file mode 100644 index 00000000..66076a25 --- /dev/null +++ b/src/Velopack.Build/Velopack.Build.targets @@ -0,0 +1,73 @@ + + + + true + + + $(VelopackPackOnPublish) + + + net472 + net6.0 + + + + + + + + + $(Version) + $(AssemblyName) + $(PublishDir) + + + net8-x64-desktop + + + releases + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Velopack.Build/dev-scripts/build-win.bat b/src/Velopack.Build/dev-scripts/build-win.bat new file mode 100644 index 00000000..e18a4af5 --- /dev/null +++ b/src/Velopack.Build/dev-scripts/build-win.bat @@ -0,0 +1,16 @@ +@echo off + +setlocal enabledelayedexpansion + +echo. +echo Kill existing MSBuild processes +taskkill /F /IM MSBuild.exe + +echo. +echo Kill existing dotnet processes +taskkill /F /IM dotnet.exe + +echo. +echo Building Velopack.Build +cd %~dp0..\..\..\ +dotnet build -c Debug src/Velopack.Build/Velopack.Build.csproj