Skip to content

Commit 21f79d1

Browse files
add support for native executable entrypoint in container publishing (#141)
1 parent bb25035 commit 21f79d1

5 files changed

Lines changed: 176 additions & 0 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ If you also have a `Directory.Build.props` file in your solution, you can remove
3737
</PropertyGroup>
3838
```
3939

40+
- Container publishing: when publishing a container with a single-RID build, the entrypoint is automatically set to the native executable (e.g. `/app/myapp`) instead of `dotnet myapp.dll`. This can be disabled by setting:
41+
42+
```xml
43+
<PropertyGroup>
44+
<ContainerUseNativeCommand>false</ContainerUseNativeCommand>
45+
</PropertyGroup>
46+
```
47+
4048
## What's NOT included
4149

4250
- Enabling a specific or latest C# language version.

src/build/Workleap.DotNet.CodingStandards.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,9 @@
5252
(WarningsAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904
5353
</WarningsAsErrors>
5454
</PropertyGroup>
55+
56+
<!-- Container publishing -->
57+
<PropertyGroup>
58+
<ContainerUseNativeCommand Condition="'$(ContainerUseNativeCommand)' == ''">true</ContainerUseNativeCommand>
59+
</PropertyGroup>
5560
</Project>

src/build/Workleap.DotNet.CodingStandards.targets

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,20 @@
3232
Condition="$(BanNewtonsoftJsonSymbols) == 'true'"
3333
Visible="false" />
3434
</ItemGroup>
35+
36+
<!-- When publishing a container with single-RID builds, use the native executable as the entrypoint (dotnet myapp.dll => myapp) -->
37+
<!-- _ComputeContainerExecutionArgs: https://github.com/dotnet/sdk/blob/d963b10530b152cc453043bb2eaf72a357f6a45c/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets#L191 -->
38+
<!-- ComputeContainerConfig: https://github.com/dotnet/sdk/blob/d963b10530b152cc453043bb2eaf72a357f6a45c/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets#L74 -->
39+
<Target Name="UpdateContainerCommand"
40+
AfterTargets="_ComputeContainerExecutionArgs"
41+
BeforeTargets="ComputeContainerConfig"
42+
Condition="'$(ContainerUseNativeCommand)' == 'true' AND '$(_IsSingleRIDBuild)' == 'true'">
43+
<PropertyGroup>
44+
<_ContainerNativeExecutableExtension Condition="'$(_ContainerIsTargetingWindows)' == 'true'">.exe</_ContainerNativeExecutableExtension>
45+
</PropertyGroup>
46+
<ItemGroup>
47+
<ContainerAppCommand Remove="@(ContainerAppCommand)" />
48+
<ContainerAppCommand Include="$(ContainerWorkingDirectory)$(AssemblyName)$(_ContainerNativeExecutableExtension)" />
49+
</ItemGroup>
50+
</Target>
3551
</Project>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using System.Formats.Tar;
2+
using System.Text.Json;
3+
using Workleap.DotNet.CodingStandards.Tests.Helpers;
4+
using Xunit.Abstractions;
5+
6+
namespace Workleap.DotNet.CodingStandards.Tests;
7+
8+
public sealed class ContainerTests(PackageFixture fixture, ITestOutputHelper testOutputHelper) : IClassFixture<PackageFixture>
9+
{
10+
[Fact]
11+
public async Task PublishContainer_WithContainerUseNativeCommand_EntrypointUsesNativeExecutable()
12+
{
13+
using var project = new ProjectBuilder(fixture, testOutputHelper);
14+
15+
var archivePath = Path.Combine(project.RootFolder, "container");
16+
17+
project.AddFile("test.csproj", $"""
18+
<Project Sdk="Microsoft.NET.Sdk.Web">
19+
<PropertyGroup>
20+
<TargetFramework>net$(NETCoreAppMaximumVersion)</TargetFramework>
21+
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
22+
<ImplicitUsings>enable</ImplicitUsings>
23+
<Nullable>enable</Nullable>
24+
<ContainerArchiveOutputPath>{archivePath}</ContainerArchiveOutputPath>
25+
<ErrorLog>{ProjectBuilder.SarifFileName},version=2.1</ErrorLog>
26+
</PropertyGroup>
27+
28+
<ItemGroup>
29+
<PackageReference Include="Workleap.DotNet.CodingStandards" Version="*" />
30+
</ItemGroup>
31+
</Project>
32+
""");
33+
34+
project.AddFile("Program.cs",
35+
"""
36+
var builder = WebApplication.CreateBuilder(args);
37+
var app = builder.Build();
38+
app.MapGet("/health", () => "ok");
39+
app.Run();
40+
""" + "\n");
41+
42+
var (publishExitCode, _) = await project.ExecuteDotnetCommand(["publish", "/t:PublishContainer"]);
43+
Assert.Equal(0, publishExitCode);
44+
45+
var entrypoint = await GetEntrypointFromArchive(archivePath);
46+
testOutputHelper.WriteLine("Entrypoint: " + JsonSerializer.Serialize(entrypoint));
47+
48+
Assert.Equal(["/app/test"], entrypoint);
49+
}
50+
51+
[Fact]
52+
public async Task PublishContainer_WithContainerUseNativeCommandDisabled_EntrypointUsesDotnet()
53+
{
54+
using var project = new ProjectBuilder(fixture, testOutputHelper);
55+
56+
var archivePath = Path.Combine(project.RootFolder, "container");
57+
58+
project.AddFile("test.csproj", $"""
59+
<Project Sdk="Microsoft.NET.Sdk.Web">
60+
<PropertyGroup>
61+
<TargetFramework>net$(NETCoreAppMaximumVersion)</TargetFramework>
62+
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
63+
<ImplicitUsings>enable</ImplicitUsings>
64+
<Nullable>enable</Nullable>
65+
<ContainerUseNativeCommand>false</ContainerUseNativeCommand>
66+
<ContainerArchiveOutputPath>{archivePath}</ContainerArchiveOutputPath>
67+
<ErrorLog>{ProjectBuilder.SarifFileName},version=2.1</ErrorLog>
68+
</PropertyGroup>
69+
70+
<ItemGroup>
71+
<PackageReference Include="Workleap.DotNet.CodingStandards" Version="*" />
72+
</ItemGroup>
73+
</Project>
74+
""");
75+
76+
project.AddFile("Program.cs",
77+
"""
78+
var builder = WebApplication.CreateBuilder(args);
79+
var app = builder.Build();
80+
app.MapGet("/health", () => "ok");
81+
app.Run();
82+
""" + "\n");
83+
84+
var (publishExitCode, _) = await project.ExecuteDotnetCommand(["publish", "/t:PublishContainer"]);
85+
Assert.Equal(0, publishExitCode);
86+
87+
var entrypoint = await GetEntrypointFromArchive(archivePath);
88+
testOutputHelper.WriteLine("Entrypoint: " + JsonSerializer.Serialize(entrypoint));
89+
90+
Assert.Equal(["dotnet", "/app/test.dll"], entrypoint);
91+
}
92+
93+
private static async Task<string[]> GetEntrypointFromArchive(string archiveDirectory)
94+
{
95+
var tarGzFile = Directory.GetFiles(archiveDirectory, "*.tar.gz").Single();
96+
97+
// Read only the JSON metadata entries from the archive (stop before layer blobs)
98+
var jsonFiles = new Dictionary<string, string>(StringComparer.Ordinal);
99+
await using var fileStream = File.OpenRead(tarGzFile);
100+
await using var tarReader = new TarReader(fileStream);
101+
102+
while (await tarReader.GetNextEntryAsync() is { } entry)
103+
{
104+
if (entry.DataStream == null || !entry.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
105+
{
106+
continue;
107+
}
108+
109+
using var reader = new StreamReader(entry.DataStream, leaveOpen: true);
110+
jsonFiles[entry.Name] = await reader.ReadToEndAsync();
111+
}
112+
113+
// Parse Docker manifest -> config
114+
using var manifest = JsonDocument.Parse(jsonFiles["manifest.json"]);
115+
var configFileName = manifest.RootElement[0].GetProperty("Config").GetString()!;
116+
117+
using var config = JsonDocument.Parse(jsonFiles[configFileName]);
118+
119+
return config.RootElement
120+
.GetProperty("config")
121+
.GetProperty("Entrypoint")
122+
.EnumerateArray()
123+
.Select(e => e.GetString()!)
124+
.ToArray();
125+
}
126+
}

tests/Workleap.DotNet.CodingStandards.Tests/Helpers/ProjectBuilder.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Text;
12
using System.Xml.Linq;
23
using Xunit.Abstractions;
34
using System.Text.Json;
@@ -126,6 +127,26 @@ private async Task<SarifFile> ExecuteDotnetCommandAndGetOutput(string command, s
126127
return sarif;
127128
}
128129

130+
public async Task<(int ExitCode, string Output)> ExecuteDotnetCommand(string[] arguments)
131+
{
132+
var stdOut = new StringBuilder();
133+
var result = await Cli.Wrap("dotnet")
134+
.WithWorkingDirectory(this._directory.FullPath)
135+
.WithArguments(arguments)
136+
.WithEnvironmentVariables(env => env.Set("CI", null).Set("GITHUB_ACTIONS", null))
137+
.WithStandardOutputPipe(PipeTarget.Merge(
138+
PipeTarget.ToDelegate(this._testOutputHelper.WriteLine),
139+
PipeTarget.ToStringBuilder(stdOut)))
140+
.WithStandardErrorPipe(PipeTarget.Merge(
141+
PipeTarget.ToDelegate(this._testOutputHelper.WriteLine),
142+
PipeTarget.ToStringBuilder(stdOut)))
143+
.WithValidation(CommandResultValidation.None)
144+
.ExecuteAsync();
145+
146+
this._testOutputHelper.WriteLine("Process exit code: " + result.ExitCode);
147+
return (result.ExitCode, stdOut.ToString());
148+
}
149+
129150
public void Dispose() => this._directory.Dispose();
130151

131152
private void AppendAdditionalResult(SarifFile sarifFile)

0 commit comments

Comments
 (0)