Skip to content

Commit d088c89

Browse files
authored
Fix support for non-apphost scenarios by providing an array (#67)
Accept arrays for endpoint and args, and bring the targets themselves under tests.
1 parent 6e08590 commit d088c89

File tree

11 files changed

+137
-26
lines changed

11 files changed

+137
-26
lines changed

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
</PropertyGroup>
55
<ItemGroup>
66
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.2.0" />
7+
<PackageVersion Include="Microsoft.Build" Version="17.2.0" />
78
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
89
<PackageVersion Include="Microsoft.VisualStudioEng.MicroBuild.Core" Version="1.0.0" />
910
<PackageVersion Include="MSTest.TestAdapter" Version="2.2.10" />
@@ -12,5 +13,6 @@
1213
<PackageVersion Include="coverlet.collector" Version="3.1.2" />
1314
<PackageVersion Include="GitHubActionsTestLogger" Version="2.0.1" />
1415
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
16+
<PackageVersion Include="Microsoft.Build.Locator" Version="1.4.6-g7150a203a1" />
1517
</ItemGroup>
1618
</Project>

Microsoft.NET.Build.Containers/CreateNewImage.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ public class CreateNewImage : Microsoft.Build.Utilities.Task
5959
/// The entrypoint application of the container.
6060
/// </summary>
6161
[Required]
62-
public string Entrypoint { get; set; }
62+
public ITaskItem[] Entrypoint { get; set; }
6363

6464
/// <summary>
6565
/// Arguments to pass alongside Entrypoint.
6666
/// </summary>
67-
public string EntrypointArgs { get; set; }
67+
public ITaskItem[] EntrypointArgs { get; set; }
6868

6969
public CreateNewImage()
7070
{
@@ -76,8 +76,8 @@ public CreateNewImage()
7676
ImageTag = "";
7777
PublishDirectory = "";
7878
WorkingDirectory = "";
79-
Entrypoint = "";
80-
EntrypointArgs = "";
79+
Entrypoint = Array.Empty<ITaskItem>();
80+
EntrypointArgs = Array.Empty<ITaskItem>();
8181
}
8282

8383

@@ -109,7 +109,7 @@ public override bool Execute()
109109

110110
Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory);
111111
image.AddLayer(newLayer);
112-
image.SetEntrypoint(Entrypoint, EntrypointArgs?.Split(' ').ToArray());
112+
image.SetEntrypoint(Entrypoint.Select(i => i.ItemSpec).ToArray(), EntrypointArgs.Select(i => i.ItemSpec).ToArray());
113113

114114
if (OutputRegistry.StartsWith("docker://"))
115115
{

Microsoft.NET.Build.Containers/Image.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ private void RecalculateDigest() {
5858
manifest["config"]!["digest"] = GetDigest(config);
5959
}
6060

61-
public void SetEntrypoint(string executable, string[]? args = null)
61+
static JsonArray ToJsonArray(string[] items) => new JsonArray(items.Where(s => !string.IsNullOrEmpty(s)).Select(s =>(JsonValue) s).ToArray());
62+
63+
public void SetEntrypoint(string[] executableArgs, string[]? args = null)
6264
{
6365
JsonObject? configObject = config["config"]!.AsObject();
6466

@@ -67,15 +69,15 @@ public void SetEntrypoint(string executable, string[]? args = null)
6769
throw new NotImplementedException("Expected base image to have a config node");
6870
}
6971

70-
configObject["Entrypoint"] = executable;
72+
configObject["Entrypoint"] = ToJsonArray(executableArgs);
7173

7274
if (args is null)
7375
{
7476
configObject.Remove("Cmd");
7577
}
7678
else
7779
{
78-
configObject["Cmd"] = new JsonArray(args.Where(s => !string.IsNullOrEmpty(s)).Select(s =>(JsonObject)s).ToArray());
80+
configObject["Cmd"] = ToJsonArray(args);
7981
}
8082

8183
RecalculateDigest();

Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<!--The folder where the custom task will be present. It points to inside the nuget package. -->
66
<CustomTasksFolder>$(MSBuildThisFileDirectory)..\$(taskForldername)\$(taskFramework)</CustomTasksFolder>
77
<!--Reference to the assembly which contains the MSBuild Task-->
8-
<CustomTasksAssembly>$(CustomTasksFolder)\$(MSBuildThisFileName).dll</CustomTasksAssembly>
8+
<CustomTasksAssembly Condition="'$(CustomTasksAssembly)' == ''">$(CustomTasksFolder)\$(MSBuildThisFileName).dll</CustomTasksAssembly>
99
</PropertyGroup>
1010

1111
<!--Register our custom task-->

Microsoft.NET.Build.Containers/build/Microsoft.NET.Build.Containers.targets

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
<Project>
22
<Target Name="ComputeContainerConfig">
3+
<!-- Reference data about this project -->
4+
<PropertyGroup>
5+
<_IsAspNet Condition="@(ProjectCapability->Count()) > 0 and @(ProjectCapability->AnyHaveMetadataValue('Identity', 'AspNetCore'))">true</_IsAspNet>
6+
<_IsSelfContained Condition="'$(SelfContained)' == 'true' or '$(PublishSelfContained)' == 'true'">true</_IsSelfContained>
7+
</PropertyGroup>
8+
39
<!-- Compute private defaults -->
410
<PropertyGroup Condition="$(ContainerBaseImage) == ''">
5-
<_IsSelfContained Condition="'$(SelfContained)' == 'true' or '$(PublishSelfContained)' == 'true'">true</_IsSelfContained>
611
<_ContainerBaseRegistry>https://mcr.microsoft.com</_ContainerBaseRegistry>
712
<_ContainerBaseImageName Condition="'$(_IsSelfContained)' == 'true'">dotnet/runtime-deps</_ContainerBaseImageName>
8-
<_ContainerBaseImageName Condition="'$(_ContainerBaseImageName)' == '' and @(ProjectCapability->AnyHaveMetadataValue('Identity', 'AspNetCore'))">dotnet/aspnet</_ContainerBaseImageName>
13+
<_ContainerBaseImageName Condition="'$(_ContainerBaseImageName)' == '' and '$(_IsAspNet)' == 'true'">dotnet/aspnet</_ContainerBaseImageName>
914
<_ContainerBaseImageName Condition="'$(_ContainerBaseImageName)' == ''">dotnet/runtime</_ContainerBaseImageName>
1015
<_ContainerBaseImageTag>$(_TargetFrameworkVersionWithoutV)</_ContainerBaseImageTag>
1116
</PropertyGroup>
@@ -18,18 +23,19 @@
1823
<ContainerImageName Condition="'$(ContainerImageName)' == ''">$(AssemblyName)</ContainerImageName>
1924
<ContainerImageTag Condition="'$(ContainerImageTag)' == ''">$(Version)</ContainerImageTag>
2025
<ContainerWorkingDirectory Condition="'$(ContainerWorkingDirectory)' == ''">/app</ContainerWorkingDirectory>
21-
<ContainerEntrypoint Condition="'$(ContainerEntrypoint)' == '' and '$(UseAppHost)' != 'true'">dotnet $(TargetFileName)</ContainerEntrypoint>
22-
<ContainerEntrypoint Condition="'$(ContainerEntrypoint)' == '' and '$(UseAppHost)' == 'true'">$(ContainerWorkingDirectory)/$(AssemblyName)$(_NativeExecutableExtension)</ContainerEntrypoint>
2326
<!-- Could be semicolon-delimited -->
24-
<ContainerEntrypointArgs Condition="'$(ContainerEntrypointArgs)' == ''"></ContainerEntrypointArgs>
2527
</PropertyGroup>
26-
28+
2729
<ItemGroup>
2830
<ContainerLabel Condition="@(ContainerLabel) == ''"></ContainerLabel>
31+
<!-- For non-apphosts, we need to invoke `dotnet` `workingdir/app` as separate args -->
32+
<ContainerEntrypoint Condition="'$(ContainerEntrypoint)' == '' and '$(UseAppHost)' != 'true'" Include="dotnet;$(ContainerWorkingDirectory)/$(TargetFileName)" />
33+
<!-- For apphosts, we need to invoke `workingdir/app` as a single arg -->
34+
<ContainerEntrypoint Condition="'$(ContainerEntrypoint)' == '' and '$(UseAppHost)' == 'true'" Include="$(ContainerWorkingDirectory)/$(AssemblyName)$(_NativeExecutableExtension)" />
2935
</ItemGroup>
3036

3137
<!-- Asp.NET defaults -->
32-
<ItemGroup Condition="@(ProjectCapability->AnyHaveMetadataValue('Identity', 'AspNetCore'))">
38+
<ItemGroup Condition="'$(_IsAspNet)' == 'true'">
3339
<ContainerPort Include="80" Type="tcp" Condition="@(ContainerPort->WithMetadataValue('Identity', '80')->AnyHaveMetadataValue('Type', 'tcp')) == ''" />
3440

3541
<ContainerEnvironmentVariable Include="ASPNETCORE_URLS" Value="http://localhost:5000;https://localhost:5001"

Test.Microsoft.NET.Build.Containers.Filesystem/CreateNewImageTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
using System.Collections;
12
using Microsoft.NET.Build.Containers.Tasks;
23
using System.Diagnostics;
4+
using Microsoft.Build.Framework;
35
using Microsoft.Build.Utilities;
46

57
namespace Test.Microsoft.NET.Build.Containers.Tasks;
@@ -48,8 +50,7 @@ public void CreateNewImage_Baseline()
4850
task.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", "net7.0");
4951
task.ImageName = "dotnet/testimage";
5052
task.WorkingDirectory = "app/";
51-
task.Entrypoint = "dotnet build";
52-
task.EntrypointArgs = "";
53+
task.Entrypoint = new TaskItem[] { new("dotnet"), new("build") };
5354

5455
Assert.IsTrue(task.Execute());
5556
newProjectDir.Delete(true);
@@ -110,8 +111,7 @@ public void ParseContainerProperties_EndToEnd()
110111
cni.OutputRegistry = "http://localhost:5010";
111112
cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", "net7.0");
112113
cni.WorkingDirectory = "app/";
113-
cni.Entrypoint = "ParseContainerProperties_EndToEnd";
114-
cni.EntrypointArgs = "";
114+
cni.Entrypoint = new TaskItem[] { new("ParseContainerProperties_EndToEnd") };
115115

116116
Assert.IsTrue(cni.Execute());
117117
newProjectDir.Delete(true);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Runtime.CompilerServices;
2+
3+
namespace Test.Microsoft.NET.Build.Containers;
4+
5+
public static class CurrentFile
6+
{
7+
public static string Path([CallerFilePath] string file = "") => file;
8+
9+
public static string Relative(string relative, [CallerFilePath] string file = "") {
10+
return global::System.IO.Path.Combine(global::System.IO.Path.GetDirectoryName(file), relative);
11+
}
12+
}

Test.Microsoft.NET.Build.Containers.Filesystem/EndToEnd.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task ApiEndToEndWithRegistryPushAndPull()
2626

2727
x.AddLayer(l);
2828

29-
x.SetEntrypoint("/app/MinimalTestApp");
29+
x.SetEntrypoint(new [] {"/app/MinimalTestApp" });
3030

3131
// Push the image back to the local registry
3232

@@ -64,7 +64,7 @@ public async Task ApiEndToEndWithLocalLoad()
6464

6565
x.AddLayer(l);
6666

67-
x.SetEntrypoint("/app/MinimalTestApp");
67+
x.SetEntrypoint(new [] { "/app/MinimalTestApp" });
6868

6969
// Load the image into the local Docker daemon
7070

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using Microsoft.Build.Evaluation;
2+
using Microsoft.Build.Framework;
3+
using Microsoft.Build.Locator;
4+
5+
namespace Test.Microsoft.NET.Build.Containers.Targets;
6+
7+
[TestClass]
8+
public class TargetsTests
9+
{
10+
private static string CombinedTargetsLocation;
11+
12+
private static string CombineFiles(string propsFile, string targetsFile)
13+
{
14+
var propsContent = File.ReadAllLines(propsFile);
15+
var targetsContent = File.ReadAllLines(targetsFile);
16+
var combinedContent = new List<string>();
17+
combinedContent.AddRange(propsContent[..^1]);
18+
combinedContent.AddRange(targetsContent[1..]);
19+
var tempTargetLocation = Path.Combine(Path.GetTempPath(), "Containers", "Microsoft.NET.Build.Containers.targets");
20+
Directory.CreateDirectory(Path.GetDirectoryName(tempTargetLocation));
21+
File.WriteAllLines(tempTargetLocation, combinedContent);
22+
return tempTargetLocation;
23+
}
24+
25+
[ClassInitialize]
26+
public static void LocateMSBuild(TestContext ctx)
27+
{
28+
var instances = MSBuildLocator.QueryVisualStudioInstances(new() { DiscoveryTypes = DiscoveryType.DotNetSdk, WorkingDirectory = Environment.CurrentDirectory });
29+
MSBuildLocator.RegisterInstance(instances.First());
30+
var relativePath = Path.Combine("..", "Microsoft.NET.Build.Containers", "build", "Microsoft.NET.Build.Containers.targets");
31+
var targetsFile = CurrentFile.Relative(relativePath);
32+
var propsFile = Path.ChangeExtension(targetsFile, ".props");
33+
CombinedTargetsLocation = CombineFiles(propsFile, targetsFile);
34+
}
35+
36+
[ClassCleanup]
37+
public static void Cleanup()
38+
{
39+
if (CombinedTargetsLocation != null) File.Delete(CombinedTargetsLocation);
40+
}
41+
42+
private Project InitProject(Dictionary<string, string> bonusProps)
43+
{
44+
var props = new Dictionary<string, string>();
45+
// required parameters
46+
props["TargetFileName"] = "foo.dll";
47+
props["AssemblyName"] = "foo";
48+
props["_TargetFrameworkVersionWithoutV"] = "7.0";
49+
props["_NativeExecutableExtension"] = ".exe"; //TODO: windows/unix split here
50+
props["Version"] = "1.0.0"; // TODO: need to test non-compliant version strings here
51+
52+
// test setup parameters so that we can load the props/targets/tasks
53+
props["CustomTasksAssembly"] = Path.GetFullPath(Path.Combine(".", "Microsoft.NET.Build.Containers.dll"));
54+
props["_IsTest"] = "true";
55+
56+
var loggers = new List<ILogger>
57+
{
58+
// new Microsoft.Build.Logging.BinaryLogger() {CollectProjectImports = Microsoft.Build.Logging.BinaryLogger.ProjectImportsCollectionMode.Embed, Verbosity = LoggerVerbosity.Diagnostic, Parameters = "LogFile=blah.binlog" },
59+
// new global::Microsoft.Build.Logging.ConsoleLogger(LoggerVerbosity.Detailed)
60+
};
61+
var collection = new ProjectCollection(null, loggers, ToolsetDefinitionLocations.Default);
62+
foreach (var kvp in bonusProps)
63+
{
64+
props[kvp.Key] = kvp.Value;
65+
}
66+
return collection.LoadProject(CombinedTargetsLocation, props, null);
67+
}
68+
69+
[DataRow(true, "/app/foo.exe")]
70+
[DataRow(false, "dotnet", "/app/foo.dll")]
71+
[TestMethod]
72+
public void CanSetEntrypointArgsToUseAppHost(bool useAppHost, params string[] entrypointArgs)
73+
{
74+
var project = InitProject(new()
75+
{
76+
["UseAppHost"] = useAppHost.ToString()
77+
});
78+
Assert.IsTrue(project.Build("ComputeContainerConfig"));
79+
{
80+
var computedEntrypointArgs = project.GetItems("ContainerEntrypoint").Select(i => i.EvaluatedInclude).ToArray();
81+
foreach (var (First, Second) in entrypointArgs.Zip(computedEntrypointArgs))
82+
{
83+
Assert.AreEqual(First, Second);
84+
}
85+
}
86+
}
87+
}

Test.Microsoft.NET.Build.Containers.Filesystem/Test.Microsoft.NET.Build.Containers.Filesystem.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19-
<PackageReference Include="Microsoft.Build.Utilities.Core" />
19+
<PackageReference Include="Microsoft.Build.Utilities.Core" ExcludeAssets="runtime" />
20+
<PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" />
21+
<PackageReference Include="Microsoft.Build.Locator" />
2022
</ItemGroup>
2123

2224
<ItemGroup>

0 commit comments

Comments
 (0)