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>
Shawn Yuan committed
May 12, 2026 at 09:41 UTC
18346688ef17e3216309efca3d8b49faa52e917a
4 files changed
+419
-2
nuget/Microsoft.WSL.Containers/build/Microsoft.WSL.Containers.common.targets
new
+192
@@ -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=""$(WslcCliPath)" --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)', '[/\\:*?"<>|]'))"
56
+ Code="WSLC0004"
57
+ Text="WslcImage with Include='%(WslcImage.Identity)' contains a character invalid for filesystem names (one of / \ : * ? " < > |). The Identity is used as the default tar filename and in .tlog filenames. Use a simple identity in Include (e.g. Include="my-image") 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) > $(_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=""$(WslcCliPath)" image build -t "$(_WslcFullRef)" -f "$(_WslcDockerfile)" "$(_WslcContext)""
136
+ ConsoleToMSBuild="true" />
137
+
138
+ <Message Importance="high"
139
+ Text="WSLC: Saving image '$(_WslcFullRef)' to '$(_WslcTarPath)'..." />
140
+
141
+ <Exec Command=""$(WslcCliPath)" image save -o "$(_WslcTarPath).tmp" "$(_WslcFullRef)""
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=""$(WslcCliPath)" 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
+61
-1
@@ -31,4 +31,64 @@
31
<Target Name="WslcValidatePlatform" BeforeTargets="PrepareForBuild" Condition="'$(_wslcIsInvalidPlatform)' == 'true'">
32
<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." />
33
</Target>
34
-</Project>
\ No newline at end of file
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
+26
-1
@@ -28,4 +28,29 @@
28
<Target Name="WslcValidatePlatform" BeforeTargets="PrepareForBuild" Condition="'$(_wslcInvalidPlatform)' != ''">
29
<Error Text="wslcsdk.dll could not be copied because the $(_wslcInvalidPlatformProperty) '$(_wslcInvalidPlatform)' is not supported. Only x64 and arm64 platforms are supported." />
30
</Target>
31
-</Project>
\ No newline at end of file
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>
nuget/Microsoft.WSL.Containers/cmake/Microsoft.WSL.ContainersConfig.cmake
+140
@@ -55,3 +55,143 @@ unset(_wslcsdk_arch)
55
unset(_wslcsdk_root)
56
unset(_wslcsdk_include_dir)
57
unset(_wslcsdk_lib_dir)
58
+
59
+# ============================================================================
60
+# Container Image Build Targets
61
+# ============================================================================
62
+#
63
+# Provides the wslc_add_image() function for declaring container image
64
+# build targets with incremental rebuild support.
65
+#
66
+# Usage:
67
+# find_package(Microsoft.WSL.Containers REQUIRED)
68
+#
69
+# wslc_add_image(my-server
70
+# IMAGE ghcr.io/myorg/my-server:latest
71
+# DOCKERFILE container/Dockerfile
72
+# CONTEXT container/
73
+# SOURCES container/src/*.cpp container/src/*.h
74
+# TAR_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/my-server.tar
75
+# )
76
+#
77
+# add_dependencies(my_app my-server)
78
+#
79
+# The first positional argument is the CMake target name.
80
+# IMAGE is the container image reference (required); may include a tag
81
+# (e.g. 'my-server:v1'). ':latest' is appended automatically when omitted.
82
+# TAR_LOCATION is the output path for the saved image tarball
83
+# (optional; defaults to ${CMAKE_CURRENT_BINARY_DIR}/<target>.tar).
84
+# Pass PRUNE_AFTER_BUILD to also run 'wslc image prune' after save.
85
+
86
+function(wslc_add_image _target_name)
87
+ cmake_parse_arguments(
88
+ PARSE_ARGV 1 ARG
89
+ "PRUNE_AFTER_BUILD" # options (boolean flags)
90
+ "IMAGE;DOCKERFILE;CONTEXT;TAR_LOCATION" # one-value keywords
91
+ "SOURCES" # multi-value keywords
92
+ )
93
+
94
+ # Reject typos / unknown keywords so they can't silently slip through.
95
+ if(ARG_UNPARSED_ARGUMENTS)
96
+ message(FATAL_ERROR "wslc_add_image: unknown argument(s): ${ARG_UNPARSED_ARGUMENTS}")
97
+ endif()
98
+
99
+ # Validate required arguments
100
+ if(NOT ARG_IMAGE)
101
+ message(FATAL_ERROR "wslc_add_image: IMAGE is required")
102
+ endif()
103
+ if(NOT ARG_DOCKERFILE)
104
+ message(FATAL_ERROR "wslc_add_image: DOCKERFILE is required")
105
+ endif()
106
+ if(NOT ARG_CONTEXT)
107
+ message(FATAL_ERROR "wslc_add_image: CONTEXT is required")
108
+ endif()
109
+
110
+ # Append :latest when IMAGE has no tag. Detect by looking for ':' after the
111
+ # last '/', so registry-port refs like localhost:5000/repo aren't misread.
112
+ string(FIND "${ARG_IMAGE}" "/" _last_slash_pos REVERSE)
113
+ string(FIND "${ARG_IMAGE}" ":" _last_colon_pos REVERSE)
114
+ if(_last_colon_pos GREATER _last_slash_pos)
115
+ set(_image_ref "${ARG_IMAGE}")
116
+ else()
117
+ set(_image_ref "${ARG_IMAGE}:latest")
118
+ endif()
119
+
120
+ # Defaults
121
+ if(NOT ARG_TAR_LOCATION)
122
+ set(ARG_TAR_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/${_target_name}.tar")
123
+ endif()
124
+ # Normalize TAR_LOCATION to an absolute path. A bare filename or relative
125
+ # path would leave _tar_dir empty below and break `make_directory ""`.
126
+ # Skip the normalization when the path contains a generator expression
127
+ # (e.g. $<TARGET_FILE_DIR:...>) — those resolve to absolute paths at
128
+ # build time and would otherwise get BASE_DIR prepended at configure
129
+ # time, producing a doubled path like build/$<...>/foo.tar.
130
+ if(NOT ARG_TAR_LOCATION MATCHES "\\$<")
131
+ get_filename_component(ARG_TAR_LOCATION "${ARG_TAR_LOCATION}" ABSOLUTE
132
+ BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}")
133
+ endif()
134
+
135
+ # Find wslc CLI on PATH (the WSL MSI puts it there).
136
+ if(NOT WSLC_CLI_PATH)
137
+ find_program(WSLC_CLI_PATH wslc)
138
+ if(NOT WSLC_CLI_PATH)
139
+ message(FATAL_ERROR "wslc CLI not found on PATH. Install WSL by running 'wsl --install --no-distribution', or set the WSLC_CLI_PATH variable to a specific wslc.exe path.")
140
+ endif()
141
+ endif()
142
+
143
+ # Validate target name (used as CMake target id and default tar filename).
144
+ string(REGEX MATCH "[^a-zA-Z0-9_.+-]" _bad_char "${_target_name}")
145
+ if(_bad_char)
146
+ message(FATAL_ERROR "wslc_add_image: '${_target_name}' contains unsupported character '${_bad_char}'. The target name is used as a CMake target identifier and as the default tar filename, so it must be limited to letters, digits, '_', '.', '+', and '-'.")
147
+ endif()
148
+
149
+ # Normalize paths to be independent of the build directory
150
+ get_filename_component(_dockerfile_path "${ARG_DOCKERFILE}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
151
+ get_filename_component(_context_path "${ARG_CONTEXT}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
152
+
153
+ # Resolve source globs to file lists; default to CONTEXT contents if SOURCES omitted
154
+ if(ARG_SOURCES)
155
+ file(GLOB_RECURSE _resolved_sources CONFIGURE_DEPENDS ${ARG_SOURCES})
156
+ else()
157
+ file(GLOB_RECURSE _resolved_sources CONFIGURE_DEPENDS "${_context_path}/*")
158
+ endif()
159
+
160
+ get_filename_component(_tar_dir "${ARG_TAR_LOCATION}" DIRECTORY)
161
+
162
+ # Prune failure is swallowed by default (housekeeping); set
163
+ # WSLC_TREAT_PRUNE_FAILURE_AS_ERROR=ON to fail the build on prune failure.
164
+ set(_prune_command "")
165
+ set(_prune_comment "")
166
+ if(ARG_PRUNE_AFTER_BUILD)
167
+ if(WSLC_TREAT_PRUNE_FAILURE_AS_ERROR)
168
+ set(_prune_command COMMAND "${WSLC_CLI_PATH}" image prune)
169
+ else()
170
+ set(_prune_wrapper "${CMAKE_CURRENT_BINARY_DIR}/wslc_prune_ignore_failure.cmake")
171
+ if(NOT EXISTS "${_prune_wrapper}")
172
+ file(WRITE "${_prune_wrapper}"
173
+ "execute_process(COMMAND \"\${WSLC}\" image prune)\n")
174
+ endif()
175
+ set(_prune_command COMMAND "${CMAKE_COMMAND}" "-DWSLC=${WSLC_CLI_PATH}" -P "${_prune_wrapper}")
176
+ endif()
177
+ set(_prune_comment ", and pruning dangling images")
178
+ endif()
179
+
180
+ # Save to a .tmp and atomically rename on success — wslc image save uses
181
+ # CREATE_ALWAYS, which truncates the destination on entry, so a failed
182
+ # save would otherwise leave a partial tar with newer mtime than sources
183
+ # (and break incremental). The rename only happens if save succeeded.
184
+ add_custom_command(
185
+ OUTPUT "${ARG_TAR_LOCATION}"
186
+ COMMAND ${CMAKE_COMMAND} -E make_directory "${_tar_dir}"
187
+ COMMAND "${WSLC_CLI_PATH}" image build -t "${_image_ref}" -f "${_dockerfile_path}" "${_context_path}"
188
+ COMMAND "${WSLC_CLI_PATH}" image save -o "${ARG_TAR_LOCATION}.tmp" "${_image_ref}"
189
+ COMMAND ${CMAKE_COMMAND} -E rename "${ARG_TAR_LOCATION}.tmp" "${ARG_TAR_LOCATION}"
190
+ ${_prune_command}
191
+ DEPENDS ${_resolved_sources} "${_dockerfile_path}"
192
+ COMMENT "WSLC: Building image '${_image_ref}', saving to '${ARG_TAR_LOCATION}'${_prune_comment}..."
193
+ VERBATIM
194
+ )
195
+
196
+ add_custom_target(${_target_name} DEPENDS "${ARG_TAR_LOCATION}")
197
+endfunction()