@samitouri / QOSAMI-WSL / commits / 18346688

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="&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
+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()