Skip to content

Commit ac4c54f

Browse files
authored
Improve EXE metadata defaults and CLI aliases (#114)
1 parent 7908a04 commit ac4c54f

File tree

3 files changed

+139
-8
lines changed

3 files changed

+139
-8
lines changed

src/DotnetPackaging.Exe/ExePackagingService.cs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Security.Cryptography;
99
using System.Reflection;
1010
using CSharpFunctionalExtensions;
11+
using DotnetPackaging;
1112
using DotnetPackaging.Publish;
1213
using Serilog;
1314
using RuntimeArchitecture = System.Runtime.InteropServices.Architecture;
@@ -55,7 +56,8 @@ public Task<Result<FileInfo>> BuildFromDirectory(
5556
ToMaybe(vendor),
5657
ToMaybe(runtimeIdentifier),
5758
ToMaybe(stubFile),
58-
Maybe<string>.None);
59+
Maybe<string>.None,
60+
Maybe<ProjectMetadata>.None);
5961

6062
return Build(request);
6163
}
@@ -87,22 +89,46 @@ public async Task<Result<FileInfo>> BuildFromProject(
8789
return Result.Failure<FileInfo>(publishResult.Error);
8890
}
8991

92+
var projectMetadata = ReadProjectMetadata(projectFile);
93+
9094
var request = new ExePackagingRequest(
9195
new DirectoryInfo(publishResult.Value.OutputDirectory),
9296
outputFile,
9397
options,
9498
ToMaybe(vendor),
9599
ToMaybe(runtimeIdentifier),
96100
ToMaybe(stubFile),
97-
publishResult.Value.Name);
101+
publishResult.Value.Name,
102+
projectMetadata);
98103

99104
return await Build(request);
100105
}
101106

107+
private Maybe<ProjectMetadata> ReadProjectMetadata(FileInfo projectFile)
108+
{
109+
var metadataResult = ProjectMetadataReader.Read(projectFile);
110+
if (metadataResult.IsFailure)
111+
{
112+
logger.Warning(
113+
"Unable to read project metadata from {ProjectFile}: {Error}",
114+
projectFile.FullName,
115+
metadataResult.Error);
116+
return Maybe<ProjectMetadata>.None;
117+
}
118+
119+
return Maybe<ProjectMetadata>.From(metadataResult.Value);
120+
}
121+
102122
private async Task<Result<FileInfo>> Build(ExePackagingRequest request)
103123
{
104124
var inferredExecutable = InferExecutableName(request.PublishDirectory, request.ProjectName);
105-
var metadata = BuildInstallerMetadata(request.Options, request.PublishDirectory, request.Vendor, inferredExecutable, request.ProjectName);
125+
var metadata = BuildInstallerMetadata(
126+
request.Options,
127+
request.PublishDirectory,
128+
request.Vendor,
129+
inferredExecutable,
130+
request.ProjectName,
131+
request.ProjectMetadata);
106132

107133
if (request.Stub.HasValue)
108134
{
@@ -186,10 +212,17 @@ private static InstallerMetadata BuildInstallerMetadata(
186212
DirectoryInfo contextDir,
187213
Maybe<string> vendor,
188214
Maybe<string> inferredExecutable,
189-
Maybe<string> projectName)
215+
Maybe<string> projectName,
216+
Maybe<ProjectMetadata> projectMetadata)
190217
{
191-
// Prefer explicit --application-name, then project name (when packaging from-project), then publish directory name
218+
var metadataProduct = projectMetadata
219+
.Bind(meta => meta.Product
220+
.Or(() => meta.AssemblyName)
221+
.Or(() => meta.AssemblyTitle));
222+
223+
// Prefer explicit --application-name, then project metadata, then project name (from publish), then publish directory name
192224
var appName = options.Name
225+
.Or(() => metadataProduct)
193226
.Or(() => projectName)
194227
.GetValueOrDefault(contextDir.Name);
195228
var packageName = appName.ToLowerInvariant().Replace(" ", string.Empty).Replace("-", string.Empty);
@@ -199,7 +232,15 @@ private static InstallerMetadata BuildInstallerMetadata(
199232
.Or(() => inferredExecutable)
200233
.Map(NormalizeExecutableRelativePath)
201234
.Match(value => value, () => (string?)null);
202-
var effectiveVendor = vendor.Match(value => value, () => "Unknown");
235+
var vendorFromProject = projectMetadata
236+
.Bind(meta => meta.Company
237+
.Or(() => meta.Product)
238+
.Or(() => meta.AssemblyName)
239+
.Or(() => meta.AssemblyTitle));
240+
241+
var effectiveVendor = vendor
242+
.Or(() => vendorFromProject)
243+
.GetValueOrDefault("Unknown");
203244
var description = options.Comment.Match(value => value, () => (string?)null);
204245

205246
return new InstallerMetadata(appId, appName, version, effectiveVendor, description, executable);
@@ -747,5 +788,6 @@ private sealed record ExePackagingRequest(
747788
Maybe<string> Vendor,
748789
Maybe<string> RuntimeIdentifier,
749790
Maybe<FileInfo> Stub,
750-
Maybe<string> ProjectName);
791+
Maybe<string> ProjectName,
792+
Maybe<ProjectMetadata> ProjectMetadata);
751793
}

src/DotnetPackaging.Tool/Program.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.CommandLine;
34
using System.CommandLine.Parsing;
45
using System.Diagnostics;
@@ -35,6 +36,7 @@ public static async Task<int> Main(string[] args)
3536
Environment.ExitCode = 0;
3637
var verboseEnabled = IsVerboseRequested(args);
3738
SetVerboseEnvironment(verboseEnabled);
39+
var normalizedArgs = NormalizeMetadataAliases(args);
3840

3941
var levelSwitch = new LoggingLevelSwitch(verboseEnabled ? LogEventLevel.Debug : LogEventLevel.Information);
4042

@@ -351,7 +353,7 @@ await ExecuteWithLogging("exe-from-project", extrasOutput.FullName, async logger
351353

352354
rootCommand.Add(exeCommand);
353355

354-
var parseResult = rootCommand.Parse(args, configuration: null);
356+
var parseResult = rootCommand.Parse(normalizedArgs, configuration: null);
355357
var exitCode = await parseResult.InvokeAsync(parseResult.InvocationConfiguration, CancellationToken.None);
356358
var finalExitCode = Environment.ExitCode != 0 ? Environment.ExitCode : exitCode;
357359
Environment.ExitCode = finalExitCode;
@@ -395,6 +397,43 @@ private static void SetVerboseEnvironment(bool verbose)
395397
Environment.SetEnvironmentVariable(VerboseEnvVar, verbose ? "1" : "0");
396398
}
397399

400+
private static string[] NormalizeMetadataAliases(string[] args)
401+
{
402+
if (args.Length == 0)
403+
{
404+
return args;
405+
}
406+
407+
var replacements = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
408+
{
409+
["--productName"] = "--application-name",
410+
["--appName"] = "--application-name",
411+
["--company"] = "--vendor"
412+
};
413+
414+
var normalized = new string[args.Length];
415+
for (var i = 0; i < args.Length; i++)
416+
{
417+
var token = args[i];
418+
if (token.StartsWith("--", StringComparison.Ordinal))
419+
{
420+
var separatorIndex = token.IndexOf('=');
421+
var key = separatorIndex >= 0 ? token[..separatorIndex] : token;
422+
if (replacements.TryGetValue(key, out var replacement))
423+
{
424+
normalized[i] = separatorIndex >= 0
425+
? string.Concat(replacement, token[separatorIndex..])
426+
: replacement;
427+
continue;
428+
}
429+
}
430+
431+
normalized[i] = token;
432+
}
433+
434+
return normalized;
435+
}
436+
398437
private static Command CreateCommand(
399438
string commandName,
400439
string friendlyName,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Xml.Linq;
5+
using CSharpFunctionalExtensions;
6+
7+
namespace DotnetPackaging;
8+
9+
public sealed record ProjectMetadata(
10+
Maybe<string> Product,
11+
Maybe<string> Company,
12+
Maybe<string> AssemblyName,
13+
Maybe<string> AssemblyTitle);
14+
15+
public static class ProjectMetadataReader
16+
{
17+
public static Result<ProjectMetadata> Read(FileInfo projectFile)
18+
{
19+
if (!projectFile.Exists)
20+
{
21+
return Result.Failure<ProjectMetadata>($"Project file not found: {projectFile.FullName}");
22+
}
23+
24+
return Result.Try(() =>
25+
{
26+
var document = XDocument.Load(projectFile.FullName);
27+
var product = ReadProperty(document, "Product");
28+
var company = ReadProperty(document, "Company");
29+
var assemblyName = ReadProperty(document, "AssemblyName");
30+
var assemblyTitle = ReadProperty(document, "AssemblyTitle");
31+
32+
return new ProjectMetadata(product, company, assemblyName, assemblyTitle);
33+
}, ex => $"Failed to read project metadata from {projectFile.FullName}: {ex.Message}");
34+
}
35+
36+
private static Maybe<string> ReadProperty(XDocument document, string propertyName)
37+
{
38+
var element = document
39+
.Descendants()
40+
.FirstOrDefault(x => string.Equals(x.Name.LocalName, propertyName, StringComparison.OrdinalIgnoreCase));
41+
42+
if (element is null)
43+
{
44+
return Maybe<string>.None;
45+
}
46+
47+
var value = element.Value?.Trim();
48+
return string.IsNullOrWhiteSpace(value) ? Maybe<string>.None : Maybe<string>.From(value);
49+
}
50+
}

0 commit comments

Comments
 (0)