Skip to content

Commit 0d7191c

Browse files
authored
Refactor DotnetPackaging.Tool to use SOLID principles (#125)
* Refactor DotnetPackaging.Tool to use SOLID principles - Extracted command logic from Program.cs to dedicated Command classes in src/DotnetPackaging.Tool/Commands/ - Created CommandFactory for common command setup - Created ExecutionWrapper for logging and timing - Moved helper methods like GetIcon to OptionsBinder - Updated Program.cs to be a lightweight composition root * Use ILogger instead of Console.Error.WriteLine in commands
1 parent e512e41 commit 0d7191c

File tree

14 files changed

+2177
-2000
lines changed

14 files changed

+2177
-2000
lines changed

src/DotnetPackaging.Tool/Commands/AppImageCommand.cs

Lines changed: 329 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System.CommandLine;
2+
using Serilog;
3+
4+
namespace DotnetPackaging.Tool.Commands;
5+
6+
public static class CommandFactory
7+
{
8+
public static Command CreateCommand(
9+
string commandName,
10+
string friendlyName,
11+
string extension,
12+
Func<DirectoryInfo, FileInfo, Options, ILogger, Task> handler,
13+
string? description = null,
14+
params string[] aliases)
15+
{
16+
var buildDir = new Option<DirectoryInfo>("--directory")
17+
{
18+
Description = "Published application directory (for example: bin/Release/<tfm>/publish)",
19+
Required = true
20+
};
21+
var outputFileOption = new Option<FileInfo>("--output")
22+
{
23+
Description = $"Destination path for the generated {extension} file",
24+
Required = true
25+
};
26+
var appName = new Option<string>("--application-name")
27+
{
28+
Description = "Application name",
29+
Required = false
30+
};
31+
var startupWmClass = new Option<string>("--wm-class")
32+
{
33+
Description = "Startup WM Class",
34+
Required = false
35+
};
36+
var mainCategory = new Option<MainCategory?>("--main-category")
37+
{
38+
Description = "Main category",
39+
Required = false,
40+
Arity = ArgumentArity.ZeroOrOne,
41+
};
42+
var additionalCategories = new Option<IEnumerable<AdditionalCategory>>("--additional-categories")
43+
{
44+
Description = "Additional categories",
45+
Required = false,
46+
Arity = ArgumentArity.ZeroOrMore,
47+
AllowMultipleArgumentsPerToken = true
48+
};
49+
var keywords = new Option<IEnumerable<string>>("--keywords")
50+
{
51+
Description = "Keywords",
52+
Required = false,
53+
Arity = ArgumentArity.ZeroOrMore,
54+
AllowMultipleArgumentsPerToken = true
55+
};
56+
var comment = new Option<string>("--comment")
57+
{
58+
Description = "Comment",
59+
Required = false
60+
};
61+
var version = new Option<string>("--version")
62+
{
63+
Description = "Version",
64+
Required = false
65+
};
66+
var homePage = new Option<Uri>("--homepage")
67+
{
68+
Description = "Home page of the application",
69+
Required = false
70+
};
71+
var license = new Option<string>("--license")
72+
{
73+
Description = "License of the application",
74+
Required = false
75+
};
76+
var screenshotUrls = new Option<IEnumerable<Uri>>("--screenshot-urls")
77+
{
78+
Description = "Screenshot URLs",
79+
Required = false
80+
};
81+
var summary = new Option<string>("--summary")
82+
{
83+
Description = "Summary. Short description that should not end in a dot.",
84+
Required = false
85+
};
86+
var appId = new Option<string>("--appId")
87+
{
88+
Description = "Application Id. Usually a Reverse DNS name like com.SomeCompany.SomeApplication",
89+
Required = false
90+
};
91+
var executableName = new Option<string>("--executable-name")
92+
{
93+
Description = "Name of your application's executable",
94+
Required = false
95+
};
96+
var isTerminal = new Option<bool>("--is-terminal")
97+
{
98+
Description = "Indicates whether your application is a terminal application",
99+
Required = false
100+
};
101+
var iconOption = new Option<IIcon?>("--icon")
102+
{
103+
Required = false,
104+
Description = "Path to the application icon"
105+
};
106+
iconOption.CustomParser = OptionsBinder.GetIcon;
107+
108+
var defaultDescription = description ??
109+
$"Create a {friendlyName} from a directory with the published application contents. Everything is inferred. For .NET apps this is usually the 'publish' directory.";
110+
var fromBuildDir = new Command(commandName, defaultDescription);
111+
112+
foreach (var alias in aliases)
113+
{
114+
if (!string.IsNullOrWhiteSpace(alias))
115+
{
116+
fromBuildDir.Aliases.Add(alias);
117+
}
118+
}
119+
120+
fromBuildDir.Add(buildDir);
121+
fromBuildDir.Add(outputFileOption);
122+
fromBuildDir.Add(appName);
123+
fromBuildDir.Add(startupWmClass);
124+
fromBuildDir.Add(mainCategory);
125+
fromBuildDir.Add(keywords);
126+
fromBuildDir.Add(comment);
127+
fromBuildDir.Add(iconOption);
128+
fromBuildDir.Add(additionalCategories);
129+
fromBuildDir.Add(version);
130+
fromBuildDir.Add(homePage);
131+
fromBuildDir.Add(license);
132+
fromBuildDir.Add(screenshotUrls);
133+
fromBuildDir.Add(summary);
134+
fromBuildDir.Add(appId);
135+
fromBuildDir.Add(executableName);
136+
fromBuildDir.Add(isTerminal);
137+
138+
var options = new OptionsBinder(
139+
appName,
140+
startupWmClass,
141+
keywords,
142+
comment,
143+
mainCategory,
144+
additionalCategories,
145+
iconOption,
146+
version,
147+
homePage,
148+
license,
149+
screenshotUrls,
150+
summary,
151+
appId,
152+
executableName,
153+
isTerminal);
154+
155+
fromBuildDir.SetAction(async parseResult =>
156+
{
157+
var directory = parseResult.GetValue(buildDir)!;
158+
var output = parseResult.GetValue(outputFileOption)!;
159+
var opts = options.Bind(parseResult);
160+
await ExecutionWrapper.ExecuteWithLogging(commandName, output.FullName, logger => handler(directory, output, opts, logger));
161+
});
162+
return fromBuildDir;
163+
}
164+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
using System.CommandLine;
2+
using System.Runtime.InteropServices;
3+
using CSharpFunctionalExtensions;
4+
using DotnetPackaging.Deb.Archives.Deb;
5+
using Serilog;
6+
using DotnetPackaging.Tool;
7+
using Zafiro.FileSystem.Core;
8+
using Zafiro.DivineBytes;
9+
10+
namespace DotnetPackaging.Tool.Commands;
11+
12+
public static class DebCommand
13+
{
14+
public static Command GetCommand()
15+
{
16+
var command = CommandFactory.CreateCommand(
17+
"deb",
18+
"Debian package",
19+
".deb",
20+
CreateDeb,
21+
"Create a Debian (.deb) installer for Debian and Ubuntu based distributions.",
22+
"pack-deb",
23+
"debian");
24+
25+
AddFromProjectSubcommand(command);
26+
return command;
27+
}
28+
29+
private static Task CreateDeb(DirectoryInfo inputDir, FileInfo outputFile, Options options, ILogger logger)
30+
{
31+
logger.Debug("Packaging Debian artifact from {Directory}", inputDir.FullName);
32+
var fs = new System.IO.Abstractions.FileSystem();
33+
return new Zafiro.FileSystem.Local.Directory(fs.DirectoryInfo.New(inputDir.FullName))
34+
.ToDirectory()
35+
.Bind(directory => DotnetPackaging.Deb.DebFile.From()
36+
.Directory(directory)
37+
.Configure(configuration => configuration.From(options))
38+
.Build()
39+
.Map(DebMixin.ToData)
40+
.Bind(async data =>
41+
{
42+
await using var fileSystemStream = outputFile.Open(FileMode.Create);
43+
return await data.DumpTo(fileSystemStream);
44+
}))
45+
.WriteResult();
46+
}
47+
48+
private static void AddFromProjectSubcommand(Command debCommand)
49+
{
50+
var project = new Option<FileInfo>("--project") { Description = "Path to the .csproj file", Required = true };
51+
var rid = new Option<string?>("--rid") { Description = "Runtime identifier (e.g. linux-x64, linux-arm64)" };
52+
var selfContained = new Option<bool>("--self-contained") { Description = "Publish self-contained" };
53+
selfContained.DefaultValueFactory = _ => true;
54+
var configuration = new Option<string>("--configuration") { Description = "Build configuration" };
55+
configuration.DefaultValueFactory = _ => "Release";
56+
var singleFile = new Option<bool>("--single-file") { Description = "Publish single-file" };
57+
var trimmed = new Option<bool>("--trimmed") { Description = "Enable trimming" };
58+
var output = new Option<FileInfo>("--output") { Description = "Destination path for the generated .deb", Required = true };
59+
60+
var appName = new Option<string>("--application-name") { Description = "Application name", Required = false };
61+
var startupWmClass = new Option<string>("--wm-class") { Description = "Startup WM Class", Required = false };
62+
var mainCategory = new Option<MainCategory?>("--main-category") { Description = "Main category", Required = false, Arity = ArgumentArity.ZeroOrOne, };
63+
var additionalCategories = new Option<IEnumerable<AdditionalCategory>>("--additional-categories") { Description = "Additional categories", Required = false, Arity = ArgumentArity.ZeroOrMore, AllowMultipleArgumentsPerToken = true };
64+
var keywords = new Option<IEnumerable<string>>("--keywords") { Description = "Keywords", Required = false, Arity = ArgumentArity.ZeroOrMore, AllowMultipleArgumentsPerToken = true };
65+
var comment = new Option<string>("--comment") { Description = "Comment", Required = false };
66+
var version = new Option<string>("--version") { Description = "Version", Required = false };
67+
var homePage = new Option<Uri>("--homepage") { Description = "Home page of the application", Required = false };
68+
var license = new Option<string>("--license") { Description = "License of the application", Required = false };
69+
var screenshotUrls = new Option<IEnumerable<Uri>>("--screenshot-urls") { Description = "Screenshot URLs", Required = false };
70+
var summary = new Option<string>("--summary") { Description = "Summary. Short description that should not end in a dot.", Required = false };
71+
var appId = new Option<string>("--appId") { Description = "Application Id. Usually a Reverse DNS name like com.SomeCompany.SomeApplication", Required = false };
72+
var executableName = new Option<string>("--executable-name") { Description = "Name of your application's executable", Required = false };
73+
var isTerminal = new Option<bool>("--is-terminal") { Description = "Indicates whether your application is a terminal application", Required = false };
74+
var iconOption = new Option<IIcon?>("--icon") { Required = false, Description = "Path to the application icon" };
75+
iconOption.CustomParser = OptionsBinder.GetIcon;
76+
77+
var optionsBinder = new OptionsBinder(appName, startupWmClass, keywords, comment, mainCategory, additionalCategories, iconOption, version, homePage, license, screenshotUrls, summary, appId, executableName, isTerminal);
78+
79+
var fromProject = new Command("from-project") { Description = "Publish a .NET project and build a Debian .deb from the published output." };
80+
fromProject.Add(project);
81+
fromProject.Add(rid);
82+
fromProject.Add(selfContained);
83+
fromProject.Add(configuration);
84+
fromProject.Add(singleFile);
85+
fromProject.Add(trimmed);
86+
fromProject.Add(output);
87+
fromProject.Add(appName);
88+
fromProject.Add(startupWmClass);
89+
fromProject.Add(mainCategory);
90+
fromProject.Add(additionalCategories);
91+
fromProject.Add(keywords);
92+
fromProject.Add(comment);
93+
fromProject.Add(version);
94+
fromProject.Add(homePage);
95+
fromProject.Add(license);
96+
fromProject.Add(screenshotUrls);
97+
fromProject.Add(summary);
98+
fromProject.Add(appId);
99+
fromProject.Add(executableName);
100+
fromProject.Add(isTerminal);
101+
fromProject.Add(iconOption);
102+
103+
fromProject.SetAction(async parseResult =>
104+
{
105+
var prj = parseResult.GetValue(project)!;
106+
var sc = parseResult.GetValue(selfContained);
107+
var cfg = parseResult.GetValue(configuration)!;
108+
var sf = parseResult.GetValue(singleFile);
109+
var tr = parseResult.GetValue(trimmed);
110+
var outFile = parseResult.GetValue(output)!;
111+
var opt = optionsBinder.Bind(parseResult);
112+
var ridVal = parseResult.GetValue(rid);
113+
114+
await ExecutionWrapper.ExecuteWithLogging("deb-from-project", outFile.FullName, async logger =>
115+
{
116+
if (string.IsNullOrWhiteSpace(ridVal) && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
117+
{
118+
logger.Error("--rid is required when building DEB from-project on non-Linux hosts (e.g., linux-x64/linux-arm64).");
119+
Environment.ExitCode = 1;
120+
return;
121+
}
122+
123+
var publisher = new DotnetPackaging.Publish.DotnetPublisher();
124+
var req = new DotnetPackaging.Publish.ProjectPublishRequest(prj.FullName)
125+
{
126+
Rid = string.IsNullOrWhiteSpace(ridVal) ? Maybe<string>.None : Maybe<string>.From(ridVal!),
127+
SelfContained = sc,
128+
Configuration = cfg,
129+
SingleFile = sf,
130+
Trimmed = tr
131+
};
132+
133+
var pub = await publisher.Publish(req);
134+
if (pub.IsFailure)
135+
{
136+
logger.Error("Publish failed: {Error}", pub.Error);
137+
Environment.ExitCode = 1;
138+
return;
139+
}
140+
141+
var container = pub.Value.Container;
142+
var name = pub.Value.Name.Match(value => value, () => (string?)null);
143+
var built = await DotnetPackaging.Deb.DebFile.From().Container(container, name).Configure(o => o.From(opt)).Build();
144+
if (built.IsFailure)
145+
{
146+
logger.Error("Deb creation failed: {Error}", built.Error);
147+
Environment.ExitCode = 1;
148+
return;
149+
}
150+
151+
var data = DebMixin.ToData(built.Value);
152+
await using var fs = outFile.Open(FileMode.Create);
153+
var dumpRes = await data.DumpTo(fs);
154+
if (dumpRes.IsFailure)
155+
{
156+
logger.Error("Failed writing Deb file: {Error}", dumpRes.Error);
157+
Environment.ExitCode = 1;
158+
}
159+
else
160+
{
161+
logger.Information("{OutputFile}", outFile.FullName);
162+
}
163+
});
164+
});
165+
166+
debCommand.Add(fromProject);
167+
}
168+
}

0 commit comments

Comments
 (0)