Add --label support to wslc image build (#40837)

John Stephens committed Jun 23, 2026 at 17:40 UTC 2e5a2ae3c6b876d10e885972b02c65e36ccafc31
7 files changed +92 -1
src/windows/service/inc/wslc.idl
+1
@@ -493,6 +493,7 @@ typedef struct _WSLCBuildImageOptions
493 WSLCStringArray BuildArgs; // KEY=VALUE pairs passed as --build-arg to docker.
494 LPCSTR Target; // Target build stage name passed as --target to docker.
495 WSLCBuildImageFlags Flags; // WSLCBuildImageFlags
496 + WSLCStringArray Labels; // KEY=VALUE pairs passed as --label to docker.
497 } WSLCBuildImageOptions;
498
499 typedef struct _WSLCTagImageOptions
src/windows/wslc/commands/ImageBuildCommand.cpp
+1
@@ -32,6 +32,7 @@ std::vector<Argument> ImageBuildCommand::GetArguments() const
32 Argument::Create(ArgType::BuildPull),
33 Argument::Create(ArgType::BuildTarget),
34 Argument::Create(ArgType::File),
35 + Argument::Create(ArgType::Label, false, NO_LIMIT),
36 Argument::Create(ArgType::NoCache),
37 Argument::Create(ArgType::Tag, false, NO_LIMIT),
38 Argument::Create(ArgType::Verbose),
src/windows/wslc/services/ImageService.cpp
+6
@@ -119,6 +119,7 @@ void ImageService::Build(
119 const std::wstring& contextPath,
120 const std::vector<std::wstring>& tags,
121 const std::vector<std::wstring>& buildArgs,
122 + const std::vector<std::wstring>& labels,
123 const std::wstring& dockerfilePath,
124 const std::wstring& target,
125 WSLCBuildImageFlags flags,
@@ -167,6 +168,10 @@ void ImageService::Build(
168 std::vector<LPCSTR> buildArgPointers;
169 toMultiByte(buildArgs, buildArgStrings, buildArgPointers);
170
171 + std::vector<std::string> labelStrings;
172 + std::vector<LPCSTR> labelPointers;
173 + toMultiByte(labels, labelStrings, labelPointers);
174 +
175 auto targetStr = wsl::windows::common::string::WideToMultiByte(target);
176
177 auto contextPathStr = absolutePath.wstring();
@@ -177,6 +182,7 @@ void ImageService::Build(
182 .BuildArgs = {buildArgPointers.data(), static_cast<ULONG>(buildArgPointers.size())},
183 .Target = targetStr.empty() ? nullptr : targetStr.c_str(),
184 .Flags = flags,
185 + .Labels = {labelPointers.data(), static_cast<ULONG>(labelPointers.size())},
186 };
187
188 THROW_IF_FAILED(session.Get()->BuildImage(&options, callback, cancelEvent));
src/windows/wslc/services/ImageService.h
+1
@@ -26,6 +26,7 @@ public:
26 const std::wstring& contextPath,
27 const std::vector<std::wstring>& tags,
28 const std::vector<std::wstring>& buildArgs,
29 + const std::vector<std::wstring>& labels,
30 const std::wstring& dockerfilePath,
31 const std::wstring& target,
32 WSLCBuildImageFlags flags,
src/windows/wslc/tasks/ImageTasks.cpp
+6 -1
@@ -63,6 +63,11 @@ void BuildImage(CLIExecutionContext& context)
63
64 auto tags = context.Args.GetAll<ArgType::Tag>();
65 auto buildArgs = context.Args.GetAll<ArgType::BuildArg>();
66 + auto labels = context.Args.GetAll<ArgType::Label>();
67 + for (const auto& label : labels)
68 + {
69 + validation::ParseLabel(label);
70 + }
71
72 std::wstring dockerfilePath;
73 if (context.Args.Contains(ArgType::File))
@@ -85,7 +90,7 @@ void BuildImage(CLIExecutionContext& context)
90
91 auto cancelEvent = context.CreateCancelEvent();
92 BuildImageCallback callback(cancelEvent, context.Args.Contains(ArgType::Verbose));
88 - services::ImageService::Build(session, contextPath, tags, buildArgs, dockerfilePath, target, flags, &callback, cancelEvent);
93 + services::ImageService::Build(session, contextPath, tags, buildArgs, labels, dockerfilePath, target, flags, &callback, cancelEvent);
94 }
95
96 void GetImages(CLIExecutionContext& context)
src/windows/wslcsession/WSLCSession.cpp
+8
@@ -873,6 +873,7 @@ try
873 RETURN_HR_IF(E_INVALIDARG, *Options->ContextPath == L'\0');
874 RETURN_HR_IF(E_INVALIDARG, Options->Tags.Count > 0 && Options->Tags.Values == nullptr);
875 RETURN_HR_IF(E_INVALIDARG, Options->BuildArgs.Count > 0 && Options->BuildArgs.Values == nullptr);
876 + RETURN_HR_IF(E_INVALIDARG, Options->Labels.Count > 0 && Options->Labels.Values == nullptr);
877 THROW_HR_IF_MSG(
878 E_INVALIDARG,
879 WI_IsAnyFlagSet(static_cast<WSLCBuildImageFlags>(Options->Flags), ~WSLCBuildImageFlagsValid),
@@ -935,6 +936,13 @@ try
936 buildArgs.push_back("--build-arg");
937 buildArgs.push_back(Options->BuildArgs.Values[i]);
938 }
939 + for (ULONG i = 0; i < Options->Labels.Count; i++)
940 + {
941 + RETURN_HR_IF_NULL(E_INVALIDARG, Options->Labels.Values[i]);
942 + RETURN_HR_IF(E_INVALIDARG, Options->Labels.Values[i][0] == '-');
943 + buildArgs.push_back("--label");
944 + buildArgs.push_back(Options->Labels.Values[i]);
945 + }
946
947 buildArgs.push_back("-f");
948 buildArgs.push_back("-");
test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp
+69
@@ -182,6 +182,71 @@ class WSLCE2EImageBuildTests
182 VERIFY_IS_TRUE(!inspectData.Config.value().Cmd.has_value() || inspectData.Config.value().Cmd.value() != finalStageCmd);
183 }
184
185 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_Label_Success)
186 + {
187 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-label";
188 + auto cleanup = SetupTestDirectory(testRoot);
189 +
190 + auto contextDir = testRoot / L"context";
191 + std::error_code ec;
192 + std::filesystem::create_directories(contextDir, ec);
193 + THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
194 +
195 + auto dockerfilePath = testRoot / L"Dockerfile";
196 + WriteTestFileContent(dockerfilePath, "FROM debian:latest\nCMD [\"echo\", \"label-ok\"]\n");
197 +
198 + // Use both the short alias (-l) and long form (--label) to confirm both parse paths.
199 + auto buildResult = RunWslc(std::format(
200 + L"build \"{}\" -f \"{}\" -t {} -l first=one --label second=two",
201 + contextDir.wstring(),
202 + dockerfilePath.wstring(),
203 + BuiltImageLabel.NameAndTag()));
204 + buildResult.Verify({.Stderr = L"", .ExitCode = 0});
205 +
206 + auto inspectData = InspectImage(BuiltImageLabel.NameAndTag());
207 + VERIFY_IS_TRUE(inspectData.Config.has_value());
208 + VERIFY_IS_TRUE(inspectData.Config.value().Labels.has_value());
209 + const auto& labels = inspectData.Config.value().Labels.value();
210 +
211 + auto firstIt = labels.find("first");
212 + VERIFY_IS_TRUE(firstIt != labels.end());
213 + VERIFY_ARE_EQUAL(std::string("one"), firstIt->second);
214 +
215 + auto secondIt = labels.find("second");
216 + VERIFY_IS_TRUE(secondIt != labels.end());
217 + VERIFY_ARE_EQUAL(std::string("two"), secondIt->second);
218 + }
219 +
220 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_LabelOverridesDockerfile_Success)
221 + {
222 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-label-override";
223 + auto cleanup = SetupTestDirectory(testRoot);
224 +
225 + auto contextDir = testRoot / L"context";
226 + std::error_code ec;
227 + std::filesystem::create_directories(contextDir, ec);
228 + THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
229 +
230 + auto dockerfilePath = testRoot / L"Dockerfile";
231 + WriteTestFileContent(
232 + dockerfilePath, "FROM debian:latest\nLABEL conflict=from-dockerfile\nCMD [\"echo\", \"label-override-ok\"]\n");
233 +
234 + auto buildResult = RunWslc(std::format(
235 + L"build \"{}\" -f \"{}\" -t {} --label conflict=from-cli",
236 + contextDir.wstring(),
237 + dockerfilePath.wstring(),
238 + BuiltImageLabelOverride.NameAndTag()));
239 + buildResult.Verify({.Stderr = L"", .ExitCode = 0});
240 +
241 + auto inspectData = InspectImage(BuiltImageLabelOverride.NameAndTag());
242 + VERIFY_IS_TRUE(inspectData.Config.has_value());
243 + VERIFY_IS_TRUE(inspectData.Config.value().Labels.has_value());
244 + const auto& labels = inspectData.Config.value().Labels.value();
245 + auto it = labels.find("conflict");
246 + VERIFY_IS_TRUE(it != labels.end());
247 + VERIFY_ARE_EQUAL(std::string("from-cli"), it->second);
248 + }
249 +
250 WSLC_TEST_METHOD(WSLCE2E_Image_Build_DockerfileInContextDir_Success)
251 {
252 BuildFromContextFile(L"Dockerfile", BuiltImageDockerfile);
@@ -289,6 +354,8 @@ private:
354 const TestImage BuiltImageDockerfile{L"wslc-e2e-build-dockerfile-ctx", L"latest", L""};
355 const TestImage BuiltImageContainerfile{L"wslc-e2e-build-containerfile-ctx", L"latest", L""};
356 const TestImage BuiltImageNoCache{L"wslc-e2e-build-no-cache", L"latest", L""};
357 + const TestImage BuiltImageLabel{L"wslc-e2e-build-label", L"latest", L""};
358 + const TestImage BuiltImageLabelOverride{L"wslc-e2e-build-label-override", L"latest", L""};
359
360 void BuildFromContextFile(const std::wstring& fileName, const TestImage& image)
361 {
@@ -316,6 +383,8 @@ private:
383 EnsureImageIsDeleted(BuiltImageDockerfile);
384 EnsureImageIsDeleted(BuiltImageContainerfile);
385 EnsureImageIsDeleted(BuiltImageNoCache);
386 + EnsureImageIsDeleted(BuiltImageLabel);
387 + EnsureImageIsDeleted(BuiltImageLabelOverride);
388 }
389 };
390 } // namespace WSLCE2ETests