Add missing image build options in nuget (#41037)

* Add --build-arg, --label, --pull, --no-cache image build options to the nuget. * Change the CMake image level PRUNE_AFTER_BUILD to global WSLC_PRUNE_AFTER_BUILD to align with the MSBuild logic.

Feng Wang committed Aug 3, 2026 at 10:29 UTC f62b9dd948177297dc7eab7d46cca9ae419466a0
4 files changed +170 -46
nuget/Microsoft.WSL.Containers/build/Microsoft.WSL.Containers.common.targets
+50 -7
@@ -9,18 +9,23 @@
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 + <WslcImageBuildPull Condition="'$(WslcImageBuildPull)' == ''">false</WslcImageBuildPull>
13 + <WslcImageBuildNoCache Condition="'$(WslcImageBuildNoCache)' == ''">false</WslcImageBuildNoCache>
14 <!-- Unified intermediate directory: $(IntDir) for C++, $(IntermediateOutputPath) for .NET -->
15 <_WslcIntDir Condition="'$(IntDir)' != ''">$(IntDir)</_WslcIntDir>
16 <_WslcIntDir Condition="'$(_WslcIntDir)' == '' AND '$(IntermediateOutputPath)' != ''">$(IntermediateOutputPath)</_WslcIntDir>
17 <_WslcIntDir Condition="'$(_WslcIntDir)' == ''">obj\</_WslcIntDir>
18 </PropertyGroup>
19
18 - <!-- Default metadata for WslcImage items. Image is the full ref;
19 - ':latest' is appended automatically when no tag is given. -->
20 + <!-- Default values for optional WslcImage metadata. Required metadata
21 + (Dockerfile, Context) is intentionally not defaulted here so
22 + _WslcValidateItems reports it as missing. -->
23 <ItemDefinitionGroup>
24 <WslcImage>
25 <Image></Image>
26 <TarLocation></TarLocation>
27 + <BuildArgs></BuildArgs>
28 + <Labels></Labels>
29 </WslcImage>
30 </ItemDefinitionGroup>
31
@@ -75,11 +80,11 @@
80 <MSBuild Projects="$(MSBuildProjectFullPath)"
81 Targets="_WslcBuildAndSaveSingleImage"
82 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)" />
83 + 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);_WslcBuildArgs=$([MSBuild]::Escape('%(WslcImage.BuildArgs)'));_WslcLabels=$([MSBuild]::Escape('%(WslcImage.Labels)'));_WslcPull=$(WslcImageBuildPull);_WslcNoCache=$(WslcImageBuildNoCache)" />
84 </Target>
85
86 <!--
82 - Collect source files for incremental check.
87 + Collect source files and the effective build options for incremental checks.
88 Wildcards are expanded here in ItemGroup Include (not in Inputs attribute or MSBuild task Properties,
89 where they get escaped). Sources can be semicolon separated directory paths;
90 each is expanded with **\* to collect all files recursively.
@@ -92,6 +97,36 @@
97 <_WslcSourceFiles Include="$(_WslcDockerfile)" />
98 <_WslcSourceFiles Include="%(_WslcSourceDirs.Identity)\**\*" Condition="'%(_WslcSourceDirs.Identity)' != ''" />
99 <_WslcSourceFiles Include="$(_WslcContext)\**\*" Condition="'$(_WslcSourceDir)' == '' AND '$(_WslcContext)' != ''" />
100 + <_WslcBuildArgItems Include="$([MSBuild]::Unescape('$(_WslcBuildArgs)').Split(';'))" Condition="'$(_WslcBuildArgs)' != ''" />
101 + <_WslcLabelItems Include="$([MSBuild]::Unescape('$(_WslcLabels)').Split(';'))" Condition="'$(_WslcLabels)' != ''" />
102 + </ItemGroup>
103 + </Target>
104 +
105 + <Target Name="_WslcWriteBuildSignature"
106 + DependsOnTargets="_WslcResolveImageRef;_WslcCollectSources">
107 + <PropertyGroup>
108 + <_WslcBuildSignaturePath>$(_WslcIntDir)wslc.$(_WslcName).options</_WslcBuildSignaturePath>
109 + </PropertyGroup>
110 +
111 + <MakeDir Directories="$(_WslcIntDir)" />
112 +
113 + <ItemGroup>
114 + <_WslcBuildSignature Include="Image=$(_WslcFullRef)" />
115 + <_WslcBuildSignature Include="Dockerfile=$(_WslcDockerfile)" />
116 + <_WslcBuildSignature Include="Context=$(_WslcContext)" />
117 + <_WslcBuildSignature Include="Pull=$(_WslcPull)" />
118 + <_WslcBuildSignature Include="NoCache=$(_WslcNoCache)" />
119 + <_WslcBuildSignature Include="BuildArg=%(_WslcBuildArgItems.Identity)" />
120 + <_WslcBuildSignature Include="Label=%(_WslcLabelItems.Identity)" />
121 + </ItemGroup>
122 +
123 + <WriteLinesToFile File="$(_WslcBuildSignaturePath)"
124 + Lines="@(_WslcBuildSignature)"
125 + Overwrite="true"
126 + WriteOnlyWhenDifferent="true" />
127 +
128 + <ItemGroup>
129 + <_WslcSourceFiles Include="$(_WslcBuildSignaturePath)" />
130 </ItemGroup>
131 </Target>
132
@@ -122,17 +157,25 @@
157 <!-- Build + save one image atomically (per-image, so a mid-list failure
158 leaves a coherent state). Save writes <tar>.tmp and Moves on success. -->
159 <Target Name="_WslcBuildAndSaveSingleImage"
125 - DependsOnTargets="_WslcCollectSources;_WslcResolveImageRef;_WslcResolveSavePath"
160 + DependsOnTargets="_WslcWriteBuildSignature;_WslcResolveSavePath"
161 Inputs="@(_WslcSourceFiles)"
162 Outputs="$(_WslcTarPath)">
163
129 - <MakeDir Directories="$(_WslcIntDir)" />
164 <MakeDir Directories="$(_WslcTarDir)" Condition="'$(_WslcTarDir)' != ''" />
165
166 + <PropertyGroup>
167 + <_WslcBuildOptions>-t &quot;$(_WslcFullRef)&quot;</_WslcBuildOptions>
168 + <_WslcBuildOptions Condition="'$(_WslcNoCache)' == 'true'">$(_WslcBuildOptions) --no-cache</_WslcBuildOptions>
169 + <_WslcBuildOptions Condition="'$(_WslcPull)' == 'true'">$(_WslcBuildOptions) --pull</_WslcBuildOptions>
170 + <_WslcBuildOptions Condition="'@(_WslcBuildArgItems)' != ''">$(_WslcBuildOptions) @(_WslcBuildArgItems->'--build-arg &quot;%(Identity)&quot;', ' ')</_WslcBuildOptions>
171 + <_WslcBuildOptions Condition="'@(_WslcLabelItems)' != ''">$(_WslcBuildOptions) @(_WslcLabelItems->'--label &quot;%(Identity)&quot;', ' ')</_WslcBuildOptions>
172 + <_WslcBuildOptions>$(_WslcBuildOptions) -f &quot;$(_WslcDockerfile)&quot;</_WslcBuildOptions>
173 + </PropertyGroup>
174 +
175 <Message Importance="high"
176 Text="WSLC: Building image '$(_WslcFullRef)'..." />
177
135 - <Exec Command="&quot;$(WslcCliPath)&quot; image build -t &quot;$(_WslcFullRef)&quot; -f &quot;$(_WslcDockerfile)&quot; &quot;$(_WslcContext)&quot;"
178 + <Exec Command="&quot;$(WslcCliPath)&quot; image build $(_WslcBuildOptions) &quot;$(_WslcContext)&quot;"
179 ConsoleToMSBuild="true" />
180
181 <Message Importance="high"
nuget/Microsoft.WSL.Containers/build/native/Microsoft.WSL.Containers.targets
+2 -2
@@ -58,13 +58,13 @@
58
59 <MSBuild Projects="$(MSBuildProjectFullPath)"
60 Targets="_WslcWriteSingleTlog"
61 - 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)'))" />
61 + 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);_WslcBuildArgs=$([MSBuild]::Escape('%(WslcImage.BuildArgs)'));_WslcLabels=$([MSBuild]::Escape('%(WslcImage.Labels)'));_WslcPull=$(WslcImageBuildPull);_WslcNoCache=$(WslcImageBuildNoCache);_WslcTlogDir=$([MSBuild]::EnsureTrailingSlash('$(TLogLocation)'))" />
62 </Target>
63
64 <!-- tlog format requires one path per line; pass ItemGroups to WriteLinesToFile
65 so each entry is its own line (a single ';'-joined string would be one line). -->
66 <Target Name="_WslcWriteSingleTlog"
67 - DependsOnTargets="_WslcCollectSources;_WslcResolveSavePath">
67 + DependsOnTargets="_WslcWriteBuildSignature;_WslcResolveSavePath">
68 <PropertyGroup>
69 <!-- Tool path is just a FUTDC group marker; don't GetFullPath
70 (would mis-resolve bare 'wslc' to <projdir>\wslc). -->
nuget/Microsoft.WSL.Containers/cmake/Microsoft.WSL.ContainersConfig.cmake
+80 -33
@@ -56,39 +56,61 @@ 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.
59 +#[[
60 + wslc_add_image(<target>
61 + IMAGE <ref> DOCKERFILE <path> CONTEXT <dir>
62 + [SOURCES <file>...] [TAR_LOCATION <path>]
63 + [BUILD_ARGS <KEY=VALUE>...] [LABELS <KEY=VALUE>...])
64 +
65 + Adds a target that builds a container image with 'wslc image build' and saves
66 + it to a tarball with 'wslc image save'. The image is rebuilt when the
67 + Dockerfile, a tracked source file, or an image build option changes.
68 +
69 + Required:
70 + <target> Name of the CMake target to create (first, positional).
71 + IMAGE Image reference to tag; ':latest' is appended when the
72 + reference has no tag.
73 + DOCKERFILE Path to the Dockerfile.
74 + CONTEXT Path to the build context directory.
75 +
76 + Optional:
77 + SOURCES Files whose changes trigger a rebuild (globs allowed).
78 + Defaults to every file under CONTEXT.
79 + TAR_LOCATION Output path for the saved tarball.
80 + Defaults to ${CMAKE_CURRENT_BINARY_DIR}/<target>.tar.
81 + BUILD_ARGS Build-time variables (KEY=VALUE), each passed as --build-arg.
82 + LABELS Image labels (KEY=VALUE), each passed as --label.
83 +
84 + Global variables (apply to every target created by wslc_add_image):
85 + WSLC_IMAGE_BUILD_PULL
86 + Always attempt to pull newer base images (--pull).
87 + WSLC_IMAGE_BUILD_NO_CACHE
88 + Build images without the layer cache (--no-cache).
89 + WSLC_PRUNE_AFTER_BUILD
90 + Run 'wslc image prune' after each image is saved.
91 + WSLC_TREAT_PRUNE_FAILURE_AS_ERROR
92 + Fail the build when the post-build prune fails. By default a
93 + prune failure is ignored.
94 +
95 + Example:
96 + find_package(Microsoft.WSL.Containers REQUIRED)
97 +
98 + wslc_add_image(my-server
99 + IMAGE ghcr.io/myorg/my-server:latest
100 + DOCKERFILE container/Dockerfile
101 + CONTEXT container/
102 + BUILD_ARGS VERSION=1.2.3 COMMIT=abcdef
103 + LABELS org.opencontainers.image.source=https://example.com/repo)
104 +
105 + add_dependencies(my_app my-server)
106 +]]
107
108 function(wslc_add_image _target_name)
109 cmake_parse_arguments(
110 PARSE_ARGV 1 ARG
89 - "PRUNE_AFTER_BUILD" # options (boolean flags)
111 + "" # options (boolean flags)
112 "IMAGE;DOCKERFILE;CONTEXT;TAR_LOCATION" # one-value keywords
91 - "SOURCES" # multi-value keywords
113 + "SOURCES;BUILD_ARGS;LABELS" # multi-value keywords
114 )
115
116 # Reject typos / unknown keywords so they can't silently slip through.
@@ -159,11 +181,36 @@ function(wslc_add_image _target_name)
181
182 get_filename_component(_tar_dir "${ARG_TAR_LOCATION}" DIRECTORY)
183
162 - # Prune failure is swallowed by default (housekeeping); set
163 - # WSLC_TREAT_PRUNE_FAILURE_AS_ERROR=ON to fail the build on prune failure.
184 + set(_build_options "")
185 + list(APPEND _build_options -t "${_image_ref}")
186 + foreach(_build_arg IN LISTS ARG_BUILD_ARGS)
187 + if(NOT _build_arg STREQUAL "")
188 + list(APPEND _build_options --build-arg "${_build_arg}")
189 + endif()
190 + endforeach()
191 + foreach(_label IN LISTS ARG_LABELS)
192 + if(NOT _label STREQUAL "")
193 + list(APPEND _build_options --label "${_label}")
194 + endif()
195 + endforeach()
196 + if(WSLC_IMAGE_BUILD_PULL)
197 + list(APPEND _build_options --pull)
198 + endif()
199 + if(WSLC_IMAGE_BUILD_NO_CACHE)
200 + list(APPEND _build_options --no-cache)
201 + endif()
202 + list(APPEND _build_options -f "${_dockerfile_path}")
203 +
204 + # Track the effective build command as an input. file(GENERATE) preserves
205 + # the timestamp when content is unchanged, so only option changes make the
206 + # custom command out of date.
207 + set(_build_signature_file "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${_target_name}-$<CONFIG>.wslc-options")
208 + string(JOIN "\n" _build_signature ${_build_options} "${_context_path}")
209 + file(GENERATE OUTPUT "${_build_signature_file}" CONTENT "${_build_signature}\n")
210 +
211 set(_prune_command "")
212 set(_prune_comment "")
166 - if(ARG_PRUNE_AFTER_BUILD)
213 + if(WSLC_PRUNE_AFTER_BUILD)
214 if(WSLC_TREAT_PRUNE_FAILURE_AS_ERROR)
215 set(_prune_command COMMAND "${WSLC_CLI_PATH}" image prune)
216 else()
@@ -184,11 +231,11 @@ function(wslc_add_image _target_name)
231 add_custom_command(
232 OUTPUT "${ARG_TAR_LOCATION}"
233 COMMAND ${CMAKE_COMMAND} -E make_directory "${_tar_dir}"
187 - COMMAND "${WSLC_CLI_PATH}" image build -t "${_image_ref}" -f "${_dockerfile_path}" "${_context_path}"
234 + COMMAND "${WSLC_CLI_PATH}" image build ${_build_options} "${_context_path}"
235 COMMAND "${WSLC_CLI_PATH}" image save -o "${ARG_TAR_LOCATION}.tmp" "${_image_ref}"
236 COMMAND ${CMAKE_COMMAND} -E rename "${ARG_TAR_LOCATION}.tmp" "${ARG_TAR_LOCATION}"
237 ${_prune_command}
191 - DEPENDS ${_resolved_sources} "${_dockerfile_path}"
238 + DEPENDS ${_resolved_sources} "${_dockerfile_path}" "${_build_signature_file}"
239 COMMENT "WSLC: Building image '${_image_ref}', saving to '${ARG_TAR_LOCATION}'${_prune_comment}..."
240 VERBATIM
241 )
nuget/Microsoft.WSL.Containers/docs/README.MD
+38 -4
@@ -65,7 +65,7 @@ var settings = new SessionSettings("my-session", @"C:\path\to\storage");
65
66 ## Building container images
67
68 -The package integrates the `wslc` CLI into your build system so container images are built and saved as part of your normal project build, with incremental rebuild support (images only rebuild when sources change).
68 +The package integrates the `wslc` CLI into your build system so container images are built and saved as part of your normal project build, with incremental rebuild support (images rebuild when tracked sources or image build options change).
69
70 ### MSBuild
71
@@ -78,8 +78,15 @@ Add `WslcImage` items to your project. Each item builds an image and saves it to
78 Dockerfile="container/Dockerfile"
79 Context="container/"
80 Sources="container/src/**"
81 - TarLocation="$(OutDir)my-server.tar" />
81 + TarLocation="$(OutDir)my-server.tar"
82 + BuildArgs="VERSION=1.2.3;COMMIT=abcdef"
83 + Labels="org.opencontainers.image.source=https://example.com/repo" />
84 </ItemGroup>
85 +
86 +<PropertyGroup>
87 + <WslcImageBuildPull>true</WslcImageBuildPull>
88 + <WslcImageBuildNoCache>false</WslcImageBuildNoCache>
89 +</PropertyGroup>
90 ```
91
92 | Metadata | Required | Description |
@@ -89,33 +96,60 @@ Add `WslcImage` items to your project. Each item builds an image and saves it to
96 | `Context` | Yes | Build context directory |
97 | `Sources` | No | Glob patterns for incremental rebuild tracking (defaults to all files in `Context`) |
98 | `TarLocation` | No | Output path for the saved `.tar` (defaults to `$(OutDir)<name>.tar`) |
99 +| `BuildArgs` | No | Semicolon-separated `KEY=VALUE` build-time variables (each forwarded as `--build-arg`) |
100 +| `Labels` | No | Semicolon-separated `KEY=VALUE` image labels (each forwarded as `--label`) |
101
102 **Optional MSBuild properties:**
103
104 | Property | Default | Description |
105 |---|---|---|
106 | `WslcCliPath` | `wslc` | Path to the `wslc` CLI executable |
107 +| `WslcImageBuildPull` | `false` | Always attempt to pull newer base images (`--pull`) |
108 +| `WslcImageBuildNoCache` | `false` | Build images without the layer cache (`--no-cache`) |
109 | `WslcPruneAfterBuild` | `false` | Run `wslc image prune` after each successful build |
110 | `WslcTreatPruneFailureAsError` | `false` | Fail the build if the post-build prune fails |
100 -| `WslcPlatform` | `$(Platform)` | Override the detected platform (x64 or arm64) |
111
112 ### CMake
113
114 ```cmake
115 find_package(Microsoft.WSL.Containers REQUIRED)
116
117 +set(WSLC_IMAGE_BUILD_PULL ON)
118 +set(WSLC_IMAGE_BUILD_NO_CACHE OFF)
119 +
120 wslc_add_image(my-server
121 IMAGE ghcr.io/myorg/my-server:latest
122 DOCKERFILE container/Dockerfile
123 CONTEXT container/
124 SOURCES container/src/*.cpp container/src/*.h
125 TAR_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/my-server.tar
126 + BUILD_ARGS VERSION=1.2.3 COMMIT=abcdef
127 + LABELS org.opencontainers.image.source=https://example.com/repo
128 )
129
130 add_dependencies(my_app my-server)
131 ```
132
118 -Set `WSLC_CLI_PATH` to override the `wslc` executable location. Pass `PRUNE_AFTER_BUILD` to prune dangling images after each build.
133 +| Argument | Required | Description |
134 +|---|---|---|
135 +| `<target>` | Yes | Name of the CMake target to create (first positional argument) |
136 +| `IMAGE` | Yes | Container image reference (`:latest` appended if no tag) |
137 +| `DOCKERFILE` | Yes | Path to the Dockerfile |
138 +| `CONTEXT` | Yes | Build context directory |
139 +| `SOURCES` | No | Files or glob patterns for incremental rebuild tracking (defaults to all files in `CONTEXT`) |
140 +| `TAR_LOCATION` | No | Output path for the saved `.tar` (defaults to `${CMAKE_CURRENT_BINARY_DIR}/<target>.tar`) |
141 +| `BUILD_ARGS` | No | `KEY=VALUE` build-time variables (each forwarded as `--build-arg`) |
142 +| `LABELS` | No | `KEY=VALUE` image labels (each forwarded as `--label`) |
143 +
144 +**Project-wide CMake variables:**
145 +
146 +| Variable | Default | Description |
147 +|---|---|---|
148 +| `WSLC_CLI_PATH` | Found on `PATH` | Path to the `wslc` CLI executable |
149 +| `WSLC_IMAGE_BUILD_PULL` | `OFF` | Always attempt to pull newer base images (`--pull`) |
150 +| `WSLC_IMAGE_BUILD_NO_CACHE` | `OFF` | Build images without the layer cache (`--no-cache`) |
151 +| `WSLC_PRUNE_AFTER_BUILD` | `OFF` | Run `wslc image prune` after each successful build |
152 +| `WSLC_TREAT_PRUNE_FAILURE_AS_ERROR` | `OFF` | Fail the build if the post-build prune fails |
153
154 ---
155