Swap out AutoMapper for Mapperly

This commit is contained in:
Caelan Sayler
2024-01-26 10:35:50 +00:00
parent 90d685bb21
commit 7cc73f584e
7 changed files with 146 additions and 191 deletions

View File

@@ -199,4 +199,8 @@ cpp_space_around_ternary_operator = insert
cpp_wrap_preserve_blocks = one_liners
[*.{csproj,manifest,nuspec,wxs,props}]
indent_size = 2
indent_size = 2
[*.cs]
dotnet_diagnostic.RMG012.severity = error # Unmapped target member
dotnet_diagnostic.RMG020.severity = error # Unmapped source member

View File

@@ -1,166 +0,0 @@
using AutoMapper;
using AutoMapper.Internal;
using Microsoft.Extensions.DependencyInjection;
using Velopack.Deployment;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Commands;
using Velopack.Packaging.Exceptions;
using Velopack.Packaging.Unix.Commands;
using Velopack.Packaging.Windows.Commands;
using Velopack.Vpk.Commands;
using Velopack.Vpk.Updates;
namespace Velopack.Vpk;
public static class CommandMapper
{
private static readonly List<TypePair> RequiredCommandMaps = new();
public static void Validate()
{
var config = GetMapperConfig();
config.AssertConfigurationIsValid();
var rootCommand = new CliRootCommand();
rootCommand.PopulateVelopackCommands(null);
var global = (IGlobalConfiguration) config;
foreach (var pair in RequiredCommandMaps) {
var map = global.FindTypeMapFor(pair);
if (map == null) {
throw new Exception($"Missing map for {pair.SourceType.Name} -> {pair.DestinationType.Name}");
}
}
}
public static void PopulateVelopackCommands(this CliRootCommand rootCommand, IServiceProvider provider)
{
if (VelopackRuntimeInfo.IsWindows) {
rootCommand.AddCommand<WindowsPackCommand, WindowsPackCommandRunner, WindowsPackOptions>(provider);
} else if (VelopackRuntimeInfo.IsOSX) {
rootCommand.AddCommand<OsxBundleCommand, OsxBundleCommandRunner, OsxBundleOptions>(provider);
rootCommand.AddCommand<OsxPackCommand, OsxPackCommandRunner, OsxPackOptions>(provider);
} else if (VelopackRuntimeInfo.IsLinux) {
rootCommand.AddCommand<LinuxPackCommand, LinuxPackCommandRunner, LinuxPackOptions>(provider);
} else {
throw new NotSupportedException("Unsupported OS platform: " + VelopackRuntimeInfo.SystemOs.GetOsLongName());
}
var downloadCommand = new CliCommand("download", "Download's the latest release from a remote update source.");
downloadCommand.AddRepositoryDownload<GitHubDownloadCommand, GitHubRepository, GitHubDownloadOptions>(provider);
downloadCommand.AddRepositoryDownload<S3DownloadCommand, S3Repository, S3DownloadOptions>(provider);
downloadCommand.AddRepositoryDownload<HttpDownloadCommand, HttpRepository, HttpDownloadOptions>(provider);
rootCommand.Add(downloadCommand);
var uploadCommand = new CliCommand("upload", "Upload local package(s) to a remote update source.");
uploadCommand.AddRepositoryUpload<GitHubUploadCommand, GitHubRepository, GitHubUploadOptions>(provider);
uploadCommand.AddRepositoryUpload<S3UploadCommand, S3Repository, S3UploadOptions>(provider);
rootCommand.Add(uploadCommand);
var deltaCommand = new CliCommand("delta", "Utilities for creating or applying delta packages.");
deltaCommand.AddCommand<DeltaGenCommand, DeltaGenCommandRunner, DeltaGenOptions>(provider);
deltaCommand.AddCommand<DeltaPatchCommand, DeltaPatchCommandRunner, DeltaPatchOptions>(provider);
rootCommand.Add(deltaCommand);
}
private static MapperConfiguration GetMapperConfig()
{
return new MapperConfiguration(cfg => {
cfg.CreatePlatformMap<OsxPackCommand, OsxPackOptions>();
cfg.CreatePlatformMap<WindowsPackCommand, WindowsPackOptions>();
cfg.CreatePlatformMap<LinuxPackCommand, LinuxPackOptions>();
cfg.CreateOutputMap<OsxBundleCommand, OsxBundleOptions>();
cfg.CreateOutputMap<GitHubDownloadCommand, GitHubDownloadOptions>();
cfg.CreateOutputMap<GitHubUploadCommand, GitHubUploadOptions>();
cfg.CreateOutputMap<HttpDownloadCommand, HttpDownloadOptions>();
cfg.CreateOutputMap<S3DownloadCommand, S3DownloadOptions>();
cfg.CreateOutputMap<S3UploadCommand, S3UploadOptions>();
cfg.CreateMap<DeltaGenCommand, DeltaGenOptions>();
cfg.CreateMap<DeltaPatchCommand, DeltaPatchOptions>();
});
}
private static CliCommand AddCommand<TCli, TCmd, TOpt>(this CliCommand parent, IServiceProvider provider)
where TCli : BaseCommand, new()
where TCmd : ICommand<TOpt>
where TOpt : class, new()
{
return parent.Add<TCli, TOpt>(provider, (options) => {
var runner = ActivatorUtilities.CreateInstance<TCmd>(provider);
return runner.Run(options);
});
}
private static CliCommand AddRepositoryDownload<TCli, TCmd, TOpt>(this CliCommand parent, IServiceProvider provider)
where TCli : BaseCommand, new()
where TCmd : IRepositoryCanDownload<TOpt>
where TOpt : RepositoryOptions, new()
{
return parent.Add<TCli, TOpt>(provider, (options) => {
var runner = ActivatorUtilities.CreateInstance<TCmd>(provider);
return runner.DownloadLatestFullPackageAsync(options);
});
}
private static CliCommand AddRepositoryUpload<TCli, TCmd, TOpt>(this CliCommand parent, IServiceProvider provider)
where TCli : BaseCommand, new()
where TCmd : IRepositoryCanUpload<TOpt>
where TOpt : RepositoryOptions, new()
{
return parent.Add<TCli, TOpt>(provider, (options) => {
var runner = ActivatorUtilities.CreateInstance<TCmd>(provider);
return runner.UploadMissingAssetsAsync(options);
});
}
private static CliCommand Add<TCli, TOpt>(this CliCommand parent, IServiceProvider provider, Func<TOpt, Task> fn)
where TCli : BaseCommand, new()
where TOpt : class, new()
{
RequiredCommandMaps.Add(new TypePair(typeof(TCli), typeof(TOpt)));
var command = new TCli();
command.SetAction(async (ctx, token) => {
var logger = provider.GetRequiredService<ILogger>();
logger.LogInformation($"[bold]{Program.INTRO}[/]");
var updateCheck = new UpdateChecker(logger);
await updateCheck.CheckForUpdates();
command.SetProperties(ctx);
var mapper = GetMapperConfig().CreateMapper();
var options = mapper.Map<TOpt>(command);
try {
await fn(options);
return 0;
} catch (Exception ex) when (ex is ProcessFailedException or UserInfoException) {
// some exceptions are just user info / user error, so don't need a stack trace.
logger.Fatal($"[bold orange3]{ex.Message}[/]");
return -1;
} catch (Exception ex) {
logger.Fatal(ex, $"Command {typeof(TCli).Name} had an exception.");
return -1;
}
});
parent.Subcommands.Add(command);
return command;
}
private static IMappingExpression<TSource, TDestination> CreatePlatformMap<TSource, TDestination>(
this IMapperConfigurationExpression cfg)
where TSource : PlatformCommand
where TDestination : IPackOptions
{
return cfg.CreateMap<TSource, TDestination>()
.ForMember(x => x.ReleaseDir, x => x.MapFrom(z => z.GetReleaseDirectory()))
.ForMember(x => x.TargetRuntime, x => x.MapFrom(z => z.GetRid()));
}
private static IMappingExpression<TSource, TDestination> CreateOutputMap<TSource, TDestination>(
this IMapperConfigurationExpression cfg)
where TSource : OutputCommand
where TDestination : IOutputOptions
{
return cfg.CreateMap<TSource, TDestination>()
.ForMember(x => x.ReleaseDir, x => x.MapFrom(z => z.GetReleaseDirectory()));
}
}

View File

@@ -4,7 +4,7 @@ namespace Velopack.Vpk.Commands
{
public abstract class OutputCommand : BaseCommand
{
public string ReleaseDirectory { get; private set; }
public string ReleaseDir { get; private set; }
public string Channel { get; private set; }
@@ -15,7 +15,7 @@ namespace Velopack.Vpk.Commands
protected OutputCommand(string name, string description)
: base(name, description)
{
ReleaseDirectoryOption = AddOption<DirectoryInfo>((v) => ReleaseDirectory = v.ToFullNameOrNull(), "-o", "--outputDir")
ReleaseDirectoryOption = AddOption<DirectoryInfo>((v) => ReleaseDir = v.ToFullNameOrNull(), "-o", "--outputDir")
.SetDescription("Output directory for created packages.")
.SetArgumentHelpName("DIR")
.SetDefault(new DirectoryInfo("Releases"));
@@ -29,7 +29,7 @@ namespace Velopack.Vpk.Commands
public DirectoryInfo GetReleaseDirectory()
{
var di = new DirectoryInfo(ReleaseDirectory);
var di = new DirectoryInfo(ReleaseDir);
if (!di.Exists) di.Create();
return di;
}

View File

@@ -0,0 +1,37 @@
using Riok.Mapperly.Abstractions;
using Velopack.Deployment;
using Velopack.Packaging.Commands;
using Velopack.Packaging.Unix.Commands;
using Velopack.Packaging.Windows.Commands;
using Velopack.Vpk.Commands;
namespace Velopack.Vpk;
[Mapper(
RequiredMappingStrategy = RequiredMappingStrategy.Target,
EnabledConversions = MappingConversionType.None)]
public static partial class OptionMapper
{
public static partial TDest Map<TDest>(object source);
public static partial OsxPackOptions ToOptions(this OsxPackCommand cmd);
public static partial WindowsPackOptions ToOptions(this WindowsPackCommand cmd);
public static partial LinuxPackOptions ToOptions(this LinuxPackCommand cmd);
public static partial OsxBundleOptions ToOptions(this OsxBundleCommand cmd);
public static partial GitHubDownloadOptions ToOptions(this GitHubDownloadCommand cmd);
public static partial GitHubUploadOptions ToOptions(this GitHubUploadCommand cmd);
public static partial HttpDownloadOptions ToOptions(this HttpDownloadCommand cmd);
public static partial S3DownloadOptions ToOptions(this S3DownloadCommand cmd);
public static partial S3UploadOptions ToOptions(this S3UploadCommand cmd);
public static partial DeltaGenOptions ToOptions(this DeltaGenCommand cmd);
public static partial DeltaPatchOptions ToOptions(this DeltaPatchCommand cmd);
private static DirectoryInfo StringToDirectoryInfo(string t)
{
var di = new DirectoryInfo(t);
if (!di.Exists) di.Create();
return di;
}
private static RID StringToRID(string t) => RID.Parse(t);
}

View File

@@ -3,9 +3,15 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
using Velopack.Deployment;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Commands;
using Velopack.Packaging.Exceptions;
using Velopack.Packaging.Unix.Commands;
using Velopack.Packaging.Windows.Commands;
using Velopack.Vpk.Commands;
using Velopack.Vpk.Logging;
using Velopack.Vpk.Updates;
namespace Velopack.Vpk;
@@ -22,7 +28,7 @@ public class Program
.SetDescription("Disable console colors and interactive components.");
public static readonly string INTRO
= $"Velopack CLI {VelopackRuntimeInfo.VelopackDisplayVersion} for creating and distributing releases.";
= $"Velopack CLI {VelopackRuntimeInfo.VelopackDisplayVersion}, for distributing applications.";
public static async Task<int> Main(string[] args)
{
@@ -66,15 +72,107 @@ public class Program
var host = builder.Build();
var logger = host.Services.GetRequiredService<Microsoft.Extensions.Logging.ILogger>();
var provider = host.Services;
var rootCommand = new CliRootCommand(INTRO) {
VerboseOption,
LegacyConsole,
};
rootCommand.PopulateVelopackCommands(host.Services);
if (VelopackRuntimeInfo.IsWindows) {
rootCommand.AddCommand<WindowsPackCommand, WindowsPackCommandRunner, WindowsPackOptions>(provider);
} else if (VelopackRuntimeInfo.IsOSX) {
rootCommand.AddCommand<OsxBundleCommand, OsxBundleCommandRunner, OsxBundleOptions>(provider);
rootCommand.AddCommand<OsxPackCommand, OsxPackCommandRunner, OsxPackOptions>(provider);
} else if (VelopackRuntimeInfo.IsLinux) {
rootCommand.AddCommand<LinuxPackCommand, LinuxPackCommandRunner, LinuxPackOptions>(provider);
} else {
throw new NotSupportedException("Unsupported OS platform: " + VelopackRuntimeInfo.SystemOs.GetOsLongName());
}
var downloadCommand = new CliCommand("download", "Download's the latest release from a remote update source.");
downloadCommand.AddRepositoryDownload<GitHubDownloadCommand, GitHubRepository, GitHubDownloadOptions>(provider);
downloadCommand.AddRepositoryDownload<S3DownloadCommand, S3Repository, S3DownloadOptions>(provider);
downloadCommand.AddRepositoryDownload<HttpDownloadCommand, HttpRepository, HttpDownloadOptions>(provider);
rootCommand.Add(downloadCommand);
var uploadCommand = new CliCommand("upload", "Upload local package(s) to a remote update source.");
uploadCommand.AddRepositoryUpload<GitHubUploadCommand, GitHubRepository, GitHubUploadOptions>(provider);
uploadCommand.AddRepositoryUpload<S3UploadCommand, S3Repository, S3UploadOptions>(provider);
rootCommand.Add(uploadCommand);
var deltaCommand = new CliCommand("delta", "Utilities for creating or applying delta packages.");
deltaCommand.AddCommand<DeltaGenCommand, DeltaGenCommandRunner, DeltaGenOptions>(provider);
deltaCommand.AddCommand<DeltaPatchCommand, DeltaPatchCommandRunner, DeltaPatchOptions>(provider);
rootCommand.Add(deltaCommand);
var cli = new CliConfiguration(rootCommand);
return await cli.InvokeAsync(args);
}
}
public static class ProgramCommandExtensions
{
public static CliCommand AddCommand<TCli, TCmd, TOpt>(this CliCommand parent, IServiceProvider provider)
where TCli : BaseCommand, new()
where TCmd : ICommand<TOpt>
where TOpt : class, new()
{
return parent.Add<TCli, TOpt>(provider, (options) => {
var runner = ActivatorUtilities.CreateInstance<TCmd>(provider);
return runner.Run(options);
});
}
public static CliCommand AddRepositoryDownload<TCli, TCmd, TOpt>(this CliCommand parent, IServiceProvider provider)
where TCli : BaseCommand, new()
where TCmd : IRepositoryCanDownload<TOpt>
where TOpt : RepositoryOptions, new()
{
return parent.Add<TCli, TOpt>(provider, (options) => {
var runner = ActivatorUtilities.CreateInstance<TCmd>(provider);
return runner.DownloadLatestFullPackageAsync(options);
});
}
public static CliCommand AddRepositoryUpload<TCli, TCmd, TOpt>(this CliCommand parent, IServiceProvider provider)
where TCli : BaseCommand, new()
where TCmd : IRepositoryCanUpload<TOpt>
where TOpt : RepositoryOptions, new()
{
return parent.Add<TCli, TOpt>(provider, (options) => {
var runner = ActivatorUtilities.CreateInstance<TCmd>(provider);
return runner.UploadMissingAssetsAsync(options);
});
}
private static CliCommand Add<TCli, TOpt>(this CliCommand parent, IServiceProvider provider, Func<TOpt, Task> fn)
where TCli : BaseCommand, new()
where TOpt : class, new()
{
var command = new TCli();
command.SetAction(async (ctx, token) => {
var logger = provider.GetRequiredService<Microsoft.Extensions.Logging.ILogger>();
logger.LogInformation($"[bold]{Program.INTRO}[/]");
var updateCheck = new UpdateChecker(logger);
await updateCheck.CheckForUpdates();
command.SetProperties(ctx);
var options = OptionMapper.Map<TOpt>(command);
try {
await fn(options);
return 0;
} catch (Exception ex) when (ex is ProcessFailedException or UserInfoException) {
// some exceptions are just user info / user error, so don't need a stack trace.
logger.Fatal($"[bold orange3]{ex.Message}[/]");
return -1;
} catch (Exception ex) {
logger.Fatal(ex, $"Command {typeof(TCli).Name} had an exception.");
return -1;
}
});
parent.Subcommands.Add(command);
return command;
}
}

View File

@@ -43,7 +43,7 @@
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.23407.1" />
<PackageReference Include="NuGet.Protocol" Version="6.8.0" />
<PackageReference Include="Spectre.Console" Version="0.48.0" />
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="Riok.Mapperly" Version="3.3.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Velopack.Vpk;
namespace Velopack.CommandLine.Tests
{
public class AutoMapperTests
{
[Fact]
public void AutoMapperConfigIsValid()
{
CommandMapper.Validate();
}
}
}