Skip to content

Commit 1834668

Browse files
shuaiyuanxxCopilot
andauthored
Add MSBuild and CMake build integration for WSLC container images (#14551)
* Add MSBuild and CMake build integration for WSLC container images * resolve comments * rebase to master * save image to tar file * Clean tar file and prune after build * resolve comments * resolve comments * change to incremental save for tar file * resolve comments * resolve comments * update comments * resolve comments * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * resolve comments * refine error message * resolve copilot comments * resolve somments * resolve copilot comments --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 6a462fb commit 1834668

4 files changed

Lines changed: 419 additions & 2 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<!-- ================================================================== -->
4+
<!-- Container Image Build Targets -->
5+
<!-- ================================================================== -->
6+
7+
<!-- Default properties -->
8+
<PropertyGroup>
9+
<!-- Default to bare 'wslc' so cmd.exe resolves it via PATH (the WSL MSI puts wslc.exe there).
10+
Override with WslcCliPath property if needed. -->
11+
<WslcCliPath Condition="'$(WslcCliPath)' == ''">wslc</WslcCliPath>
12+
<!-- Unified intermediate directory: $(IntDir) for C++, $(IntermediateOutputPath) for .NET -->
13+
<_WslcIntDir Condition="'$(IntDir)' != ''">$(IntDir)</_WslcIntDir>
14+
<_WslcIntDir Condition="'$(_WslcIntDir)' == '' AND '$(IntermediateOutputPath)' != ''">$(IntermediateOutputPath)</_WslcIntDir>
15+
<_WslcIntDir Condition="'$(_WslcIntDir)' == ''">obj\</_WslcIntDir>
16+
</PropertyGroup>
17+
18+
<!-- Default metadata for WslcImage items. Image is the full ref;
19+
':latest' is appended automatically when no tag is given. -->
20+
<ItemDefinitionGroup>
21+
<WslcImage>
22+
<Image></Image>
23+
<TarLocation></TarLocation>
24+
</WslcImage>
25+
</ItemDefinitionGroup>
26+
27+
<!-- Check that wslc CLI is installed (invoked via DependsOnTargets from WslcBuildAllImages) -->
28+
<Target Name="WslcCheckDependencies">
29+
30+
<Exec Command="&quot;$(WslcCliPath)&quot; --version"
31+
IgnoreExitCode="true"
32+
ConsoleToMSBuild="true"
33+
StandardOutputImportance="low"
34+
StandardErrorImportance="low">
35+
<Output TaskParameter="ExitCode" PropertyName="_WslcExitCode" />
36+
</Exec>
37+
38+
<Error Condition="'$(_WslcExitCode)' != '0'"
39+
Code="WSLC0001"
40+
Text="The wslc CLI check failed: '$(WslcCliPath) --version' returned exit code $(_WslcExitCode). Install WSL by running 'wsl --install --no-distribution', or set the WslcCliPath property to a specific wslc.exe path." />
41+
</Target>
42+
43+
<!-- Validate required metadata on WslcImage items (invoked via DependsOnTargets from WslcBuildAllImages) -->
44+
<Target Name="_WslcValidateItems">
45+
46+
<Error Condition="'%(WslcImage.Dockerfile)' == ''"
47+
Code="WSLC0002"
48+
Text="WslcImage '%(WslcImage.Identity)' is missing required Dockerfile metadata." />
49+
50+
<Error Condition="'%(WslcImage.Context)' == ''"
51+
Code="WSLC0003"
52+
Text="WslcImage '%(WslcImage.Identity)' is missing required Context metadata." />
53+
54+
<!-- Identity is used as default tar filename and in .tlog filenames; reject filename-invalid chars. -->
55+
<Error Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(WslcImage.Identity)', '[/\\:*?&quot;&lt;&gt;|]'))"
56+
Code="WSLC0004"
57+
Text="WslcImage with Include='%(WslcImage.Identity)' contains a character invalid for filesystem names (one of / \ : * ? &quot; &lt; &gt; |). The Identity is used as the default tar filename and in .tlog filenames. Use a simple identity in Include (e.g. Include=&quot;my-image&quot;) and put the registry/org/tag in the Image metadata." />
58+
</Target>
59+
60+
<!-- Dispatch each WslcImage via MSBuild task instead of batching directly because:
61+
1. Sources may contain semicolons (multi-directory) which conflict with Properties syntax
62+
2. Wildcard expansion in Inputs requires an intermediate ItemGroup (_WslcCollectSources)
63+
that must run per-image in a separate evaluation context
64+
65+
Build + save run together per image, and StopOnFirstFailure halts the
66+
batch on the first failure — so later images aren't attempted before
67+
earlier ones are finished. Within an image, save uses .tmp+Move so a
68+
half-written tar is never visible (a build-success/save-failure does
69+
still leave the image in the daemon without a tar on disk). -->
70+
<Target Name="WslcBuildAllImages"
71+
AfterTargets="Build"
72+
DependsOnTargets="WslcCheckDependencies;_WslcValidateItems"
73+
Condition="'@(WslcImage)' != '' AND '$(DesignTimeBuild)' != 'true' AND '$(IsCrossTargetingBuild)' != 'true'">
74+
75+
<MSBuild Projects="$(MSBuildProjectFullPath)"
76+
Targets="_WslcBuildAndSaveSingleImage"
77+
StopOnFirstFailure="true"
78+
Properties="Configuration=$(Configuration);Platform=$(Platform);_WslcName=%(WslcImage.Identity);_WslcImage=%(WslcImage.Image);_WslcDockerfile=%(WslcImage.Dockerfile);_WslcContext=%(WslcImage.Context);_WslcSourceDir=$([MSBuild]::Escape('%(WslcImage.Sources)'));_WslcIntDir=$(_WslcIntDir);_WslcTarLocation=%(WslcImage.TarLocation);_WslcOutDir=$(OutDir)" />
79+
</Target>
80+
81+
<!--
82+
Collect source files for incremental check.
83+
Wildcards are expanded here in ItemGroup Include (not in Inputs attribute or MSBuild task Properties,
84+
where they get escaped). Sources can be semicolon separated directory paths;
85+
each is expanded with **\* to collect all files recursively.
86+
If Sources is omitted, fall back to tracking the full Context directory so changes to
87+
.dockerignore and other Docker build inputs still invalidate the marker.
88+
-->
89+
<Target Name="_WslcCollectSources">
90+
<ItemGroup>
91+
<_WslcSourceDirs Include="$([MSBuild]::Unescape('$(_WslcSourceDir)').Split(';'))" Condition="'$(_WslcSourceDir)' != ''" />
92+
<_WslcSourceFiles Include="$(_WslcDockerfile)" />
93+
<_WslcSourceFiles Include="%(_WslcSourceDirs.Identity)\**\*" Condition="'%(_WslcSourceDirs.Identity)' != ''" />
94+
<_WslcSourceFiles Include="$(_WslcContext)\**\*" Condition="'$(_WslcSourceDir)' == '' AND '$(_WslcContext)' != ''" />
95+
</ItemGroup>
96+
</Target>
97+
98+
<!-- Resolve _WslcFullRef. Falls back to Identity when Image unset; appends
99+
':latest' when no tag is present (':' after the last '/'). -->
100+
<Target Name="_WslcResolveImageRef">
101+
<PropertyGroup>
102+
<_WslcRawRef Condition="'$(_WslcImage)' != ''">$(_WslcImage)</_WslcRawRef>
103+
<_WslcRawRef Condition="'$(_WslcRawRef)' == ''">$(_WslcName)</_WslcRawRef>
104+
<_WslcLastSlash>$([System.String]::Copy('$(_WslcRawRef)').LastIndexOf('/'))</_WslcLastSlash>
105+
<_WslcLastColon>$([System.String]::Copy('$(_WslcRawRef)').LastIndexOf(':'))</_WslcLastColon>
106+
<_WslcFullRef Condition="$(_WslcLastColon) &gt; $(_WslcLastSlash)">$(_WslcRawRef)</_WslcFullRef>
107+
<_WslcFullRef Condition="'$(_WslcFullRef)' == ''">$(_WslcRawRef):latest</_WslcFullRef>
108+
</PropertyGroup>
109+
</Target>
110+
111+
<!-- Resolve tar path into a property so _WslcBuildAndSaveSingleImage can use it in Inputs/Outputs. -->
112+
<Target Name="_WslcResolveSavePath" DependsOnTargets="_WslcResolveImageRef">
113+
<PropertyGroup>
114+
<!-- Resolve tar path: user-provided TarLocation, else $(OutDir)<name>.tar -->
115+
<_WslcTarPath Condition="'$(_WslcTarLocation)' != ''">$(_WslcTarLocation)</_WslcTarPath>
116+
<_WslcTarPath Condition="'$(_WslcTarPath)' == ''">$(_WslcOutDir)$(_WslcName).tar</_WslcTarPath>
117+
118+
<_WslcTarDir>$([System.IO.Path]::GetDirectoryName('$(_WslcTarPath)'))</_WslcTarDir>
119+
</PropertyGroup>
120+
</Target>
121+
122+
<!-- Build + save one image atomically (per-image, so a mid-list failure
123+
leaves a coherent state). Save writes <tar>.tmp and Moves on success. -->
124+
<Target Name="_WslcBuildAndSaveSingleImage"
125+
DependsOnTargets="_WslcCollectSources;_WslcResolveImageRef;_WslcResolveSavePath"
126+
Inputs="@(_WslcSourceFiles)"
127+
Outputs="$(_WslcTarPath)">
128+
129+
<MakeDir Directories="$(_WslcIntDir)" />
130+
<MakeDir Directories="$(_WslcTarDir)" Condition="'$(_WslcTarDir)' != ''" />
131+
132+
<Message Importance="high"
133+
Text="WSLC: Building image '$(_WslcFullRef)'..." />
134+
135+
<Exec Command="&quot;$(WslcCliPath)&quot; image build -t &quot;$(_WslcFullRef)&quot; -f &quot;$(_WslcDockerfile)&quot; &quot;$(_WslcContext)&quot;"
136+
ConsoleToMSBuild="true" />
137+
138+
<Message Importance="high"
139+
Text="WSLC: Saving image '$(_WslcFullRef)' to '$(_WslcTarPath)'..." />
140+
141+
<Exec Command="&quot;$(WslcCliPath)&quot; image save -o &quot;$(_WslcTarPath).tmp&quot; &quot;$(_WslcFullRef)&quot;"
142+
ConsoleToMSBuild="true" />
143+
144+
<Move SourceFiles="$(_WslcTarPath).tmp" DestinationFiles="$(_WslcTarPath)" />
145+
146+
<!-- Opt-in: WslcPruneAfterBuild=true. Prune failure is a Warning by default;
147+
set WslcTreatPruneFailureAsError=true to fail the build instead. -->
148+
<Message Importance="high"
149+
Text="WSLC: Pruning dangling images..."
150+
Condition="'$(WslcPruneAfterBuild)' == 'true'" />
151+
152+
<Exec Command="&quot;$(WslcCliPath)&quot; image prune"
153+
ConsoleToMSBuild="true"
154+
IgnoreExitCode="true"
155+
Condition="'$(WslcPruneAfterBuild)' == 'true'">
156+
<Output TaskParameter="ExitCode" PropertyName="_WslcPruneExitCode" />
157+
</Exec>
158+
159+
<Error Condition="'$(WslcPruneAfterBuild)' == 'true' AND '$(WslcTreatPruneFailureAsError)' == 'true' AND '$(_WslcPruneExitCode)' != '0'"
160+
Code="WSLC0007"
161+
Text="wslc image prune failed (exit code $(_WslcPruneExitCode)). The image and tar were saved successfully; only the post-build prune failed." />
162+
163+
<Warning Condition="'$(WslcPruneAfterBuild)' == 'true' AND '$(WslcTreatPruneFailureAsError)' != 'true' AND '$(_WslcPruneExitCode)' != '0'"
164+
Code="WSLC0007"
165+
Text="wslc image prune failed (exit code $(_WslcPruneExitCode)). The image and tar were saved successfully; only the post-build prune was skipped. Set WslcTreatPruneFailureAsError=true to fail the build instead." />
166+
</Target>
167+
168+
<!-- Clean container artifacts. ContinueOnError so a transient file lock
169+
(AV scanner, downstream consumer) doesn't block the rest of clean.
170+
.tmp variants are removed too — a failed save can leave one behind. -->
171+
<Target Name="WslcClean"
172+
AfterTargets="Clean"
173+
Condition="'@(WslcImage)' != ''">
174+
175+
<Delete Files="%(WslcImage.TarLocation)"
176+
Condition="'%(WslcImage.TarLocation)' != ''"
177+
ContinueOnError="WarnAndContinue" />
178+
179+
<Delete Files="%(WslcImage.TarLocation).tmp"
180+
Condition="'%(WslcImage.TarLocation)' != ''"
181+
ContinueOnError="WarnAndContinue" />
182+
183+
<Delete Files="$(OutDir)%(WslcImage.Identity).tar"
184+
Condition="'%(WslcImage.TarLocation)' == ''"
185+
ContinueOnError="WarnAndContinue" />
186+
187+
<Delete Files="$(OutDir)%(WslcImage.Identity).tar.tmp"
188+
Condition="'%(WslcImage.TarLocation)' == ''"
189+
ContinueOnError="WarnAndContinue" />
190+
</Target>
191+
192+
</Project>

nuget/Microsoft.WSL.Containers/build/native/Microsoft.WSL.Containers.targets

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,64 @@
3131
<Target Name="WslcValidatePlatform" BeforeTargets="PrepareForBuild" Condition="'$(_wslcIsInvalidPlatform)' == 'true'">
3232
<Error Text="wslcsdk.dll could not be copied because platform '$(WslcPlatform)' is not supported. Only x64 and arm64 platforms are supported. You can override the detected platform by setting the property WslcPlatform." />
3333
</Target>
34-
</Project>
34+
35+
<Import Project="$(MSBuildThisFileDirectory)..\Microsoft.WSL.Containers.common.targets" />
36+
37+
<!-- ================================================================== -->
38+
<!-- C++ specific: Write .tlog files for VS Fast Up-to-Date Check -->
39+
<!-- ================================================================== -->
40+
41+
<Target Name="_WslcWriteAllTlogs"
42+
AfterTargets="WslcBuildAllImages"
43+
Condition="'@(WslcImage)' != '' AND '$(TLogLocation)' != '' AND '$(DesignTimeBuild)' != 'true'">
44+
45+
<MSBuild Projects="$(MSBuildProjectFullPath)"
46+
Targets="_WslcWriteSingleTlog"
47+
Properties="Configuration=$(Configuration);Platform=$(Platform);_WslcName=%(WslcImage.Identity);_WslcDockerfile=%(WslcImage.Dockerfile);_WslcContext=%(WslcImage.Context);_WslcSourceDir=$([MSBuild]::Escape('%(WslcImage.Sources)'));_WslcIntDir=$(_WslcIntDir);_WslcTarLocation=%(WslcImage.TarLocation);_WslcOutDir=$(OutDir);_WslcTlogDir=$([MSBuild]::EnsureTrailingSlash('$(TLogLocation)'))" />
48+
</Target>
49+
50+
<!-- tlog format requires one path per line; pass ItemGroups to WriteLinesToFile
51+
so each entry is its own line (a single ';'-joined string would be one line). -->
52+
<Target Name="_WslcWriteSingleTlog"
53+
DependsOnTargets="_WslcCollectSources;_WslcResolveSavePath">
54+
<PropertyGroup>
55+
<!-- Tool path is just a FUTDC group marker; don't GetFullPath
56+
(would mis-resolve bare 'wslc' to <projdir>\wslc). -->
57+
<_WslcTlogToolPath>$(WslcCliPath)</_WslcTlogToolPath>
58+
<_WslcTarFullPath>$([System.IO.Path]::GetFullPath('$(_WslcTarPath)'))</_WslcTarFullPath>
59+
</PropertyGroup>
60+
61+
<ItemGroup>
62+
<_WslcTlogReadLines Include="^$(_WslcTlogToolPath)" />
63+
<_WslcTlogReadLines Include="@(_WslcSourceFiles->'%(FullPath)')" />
64+
<_WslcTlogWriteLines Include="^$(_WslcTlogToolPath)" />
65+
<_WslcTlogWriteLines Include="$(_WslcTarFullPath)" />
66+
</ItemGroup>
67+
68+
<WriteLinesToFile
69+
File="$(_WslcTlogDir)wslc.$(_WslcName).read.1.tlog"
70+
Lines="@(_WslcTlogReadLines)"
71+
Overwrite="true"
72+
Encoding="Unicode" />
73+
74+
<WriteLinesToFile
75+
File="$(_WslcTlogDir)wslc.$(_WslcName).write.1.tlog"
76+
Lines="@(_WslcTlogWriteLines)"
77+
Overwrite="true"
78+
Encoding="Unicode" />
79+
</Target>
80+
81+
<!-- Clean tlog files -->
82+
<Target Name="_WslcCleanTlogs"
83+
AfterTargets="WslcClean"
84+
Condition="'@(WslcImage)' != '' AND '$(TLogLocation)' != ''">
85+
<PropertyGroup>
86+
<_WslcTlogDir>$([MSBuild]::EnsureTrailingSlash('$(TLogLocation)'))</_WslcTlogDir>
87+
</PropertyGroup>
88+
<Delete Files="$(_WslcTlogDir)wslc.%(WslcImage.Identity).read.1.tlog"
89+
ContinueOnError="WarnAndContinue" />
90+
<Delete Files="$(_WslcTlogDir)wslc.%(WslcImage.Identity).write.1.tlog"
91+
ContinueOnError="WarnAndContinue" />
92+
</Target>
93+
94+
</Project>

nuget/Microsoft.WSL.Containers/build/net/Microsoft.WSL.Containers.targets

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,29 @@
2828
<Target Name="WslcValidatePlatform" BeforeTargets="PrepareForBuild" Condition="'$(_wslcInvalidPlatform)' != ''">
2929
<Error Text="wslcsdk.dll could not be copied because the $(_wslcInvalidPlatformProperty) '$(_wslcInvalidPlatform)' is not supported. Only x64 and arm64 platforms are supported." />
3030
</Target>
31-
</Project>
31+
32+
<Import Project="$(MSBuildThisFileDirectory)..\Microsoft.WSL.Containers.common.targets" />
33+
34+
<!-- ================================================================== -->
35+
<!-- C# specific: CPS Up-to-Date Check for container source files -->
36+
<!-- ================================================================== -->
37+
38+
<!-- Register the saved tar and source files for CPS up-to-date check.
39+
The tar is the sole build output (build is atomic with save — see
40+
_WslcBuildAndSaveSingleImage in common.targets). -->
41+
<Target Name="_WslcRegisterUpToDateCheckInputs"
42+
BeforeTargets="CollectUpToDateCheckBuiltDesignTime"
43+
Condition="'$(UsingMicrosoftNETSdk)' == 'true' AND '@(WslcImage)' != ''">
44+
<ItemGroup>
45+
<UpToDateCheckBuilt Include="%(WslcImage.TarLocation)"
46+
Condition="'%(WslcImage.TarLocation)' != ''" />
47+
<UpToDateCheckBuilt Include="$(OutDir)%(WslcImage.Identity).tar"
48+
Condition="'%(WslcImage.TarLocation)' == ''" />
49+
<UpToDateCheckInput Include="%(WslcImage.Dockerfile)" Condition="'%(WslcImage.Dockerfile)' != ''" />
50+
<_WslcUpToDateDirs Include="@(WslcImage->'%(Sources)')" Condition="'%(WslcImage.Sources)' != ''" />
51+
<UpToDateCheckInput Include="%(_WslcUpToDateDirs.Identity)\**\*" Condition="'%(_WslcUpToDateDirs.Identity)' != ''" />
52+
<UpToDateCheckInput Include="%(WslcImage.Context)\**\*" Condition="'%(WslcImage.Sources)' == '' AND '%(WslcImage.Context)' != ''" />
53+
</ItemGroup>
54+
</Target>
55+
56+
</Project>

0 commit comments

Comments
 (0)