diff --git a/src/shared/inc/stringshared.h b/src/shared/inc/stringshared.h index 978bd9cbd7..7b17bbf677 100644 --- a/src/shared/inc/stringshared.h +++ b/src/shared/inc/stringshared.h @@ -392,9 +392,15 @@ inline std::optional ParseMemorySize(const T* String) std::make_pair(Gigabytes, 1ULL << 30), std::make_pair(Terabytes, 1ULL << 40)}; + // Case-insensitive suffix matching to align with Docker CLI behavior (accepts both 512m and 512M). for (const auto& [Suffix, Factor] : Units) { - if ((Remainder == Suffix.substr(0, 1)) || (Remainder == Suffix)) + if (Remainder.size() == 1 && IsEqual(Remainder, Suffix.substr(0, 1), true)) + { + return Value * Factor; + } + + if (IsEqual(Remainder, Suffix, true)) { return Value * Factor; } diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 10af7310a1..ec2bea889c 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -478,6 +478,9 @@ typedef struct _WSLCBuildImageOptions WSLCStringArray BuildArgs; // KEY=VALUE pairs passed as --build-arg to docker. LPCSTR Target; // Target build stage name passed as --target to docker. WSLCBuildImageFlags Flags; // WSLCBuildImageFlags + LONGLONG ShmSize; // Shared memory size in bytes passed as --shm-size to docker. 0 = default. + [unique, size_is(UlimitsCount)] const WSLCUlimit* Ulimits; + ULONG UlimitsCount; } WSLCBuildImageOptions; typedef struct _WSLCTagImageOptions diff --git a/src/windows/wslc/commands/ImageBuildCommand.cpp b/src/windows/wslc/commands/ImageBuildCommand.cpp index 00128058e0..90837514fd 100644 --- a/src/windows/wslc/commands/ImageBuildCommand.cpp +++ b/src/windows/wslc/commands/ImageBuildCommand.cpp @@ -33,7 +33,9 @@ std::vector ImageBuildCommand::GetArguments() const Argument::Create(ArgType::BuildTarget), Argument::Create(ArgType::File), Argument::Create(ArgType::NoCache), + Argument::Create(ArgType::ShmSize), Argument::Create(ArgType::Tag, false, NO_LIMIT), + Argument::Create(ArgType::Ulimit, false, NO_LIMIT), Argument::Create(ArgType::Verbose), }; } diff --git a/src/windows/wslc/services/ImageService.cpp b/src/windows/wslc/services/ImageService.cpp index e852a7f93d..e877900f22 100644 --- a/src/windows/wslc/services/ImageService.cpp +++ b/src/windows/wslc/services/ImageService.cpp @@ -123,6 +123,8 @@ void ImageService::Build( const std::wstring& target, WSLCBuildImageFlags flags, IProgressCallback* callback, + int64_t shmSize, + const std::vector>& ulimits, HANDLE cancelEvent) { auto absolutePath = std::filesystem::absolute(contextPath); @@ -170,6 +172,14 @@ void ImageService::Build( auto targetStr = wsl::windows::common::string::WideToMultiByte(target); auto contextPathStr = absolutePath.wstring(); + + std::vector ulimitEntries; + ulimitEntries.reserve(ulimits.size()); + for (const auto& [name, soft, hard] : ulimits) + { + ulimitEntries.push_back({name.c_str(), soft, hard}); + } + WSLCBuildImageOptions options{ .ContextPath = contextPathStr.c_str(), .DockerfileHandle = ToCOMInputHandle(dockerfileHandle), @@ -177,6 +187,9 @@ void ImageService::Build( .BuildArgs = {buildArgPointers.data(), static_cast(buildArgPointers.size())}, .Target = targetStr.empty() ? nullptr : targetStr.c_str(), .Flags = flags, + .ShmSize = shmSize, + .Ulimits = ulimitEntries.empty() ? nullptr : ulimitEntries.data(), + .UlimitsCount = static_cast(ulimitEntries.size()), }; THROW_IF_FAILED(session.Get()->BuildImage(&options, callback, cancelEvent)); diff --git a/src/windows/wslc/services/ImageService.h b/src/windows/wslc/services/ImageService.h index a2c6d73609..3e4f82637f 100644 --- a/src/windows/wslc/services/ImageService.h +++ b/src/windows/wslc/services/ImageService.h @@ -30,6 +30,8 @@ class ImageService const std::wstring& target, WSLCBuildImageFlags flags, IProgressCallback* callback, + int64_t shmSize = 0, + const std::vector>& ulimits = {}, HANDLE cancelEvent = nullptr); static std::vector List( diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index b7e0a8693a..cba210ef26 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -85,7 +85,23 @@ void BuildImage(CLIExecutionContext& context) auto cancelEvent = context.CreateCancelEvent(); BuildImageCallback callback(cancelEvent, context.Args.Contains(ArgType::Verbose)); - services::ImageService::Build(session, contextPath, tags, buildArgs, dockerfilePath, target, flags, &callback, cancelEvent); + + int64_t shmSize = 0; + if (context.Args.Contains(ArgType::ShmSize)) + { + shmSize = validation::GetMemorySizeFromString(context.Args.Get()); + } + + std::vector> ulimits; + if (context.Args.Contains(ArgType::Ulimit)) + { + for (const auto& value : context.Args.GetAll()) + { + ulimits.emplace_back(validation::ParseUlimit(value)); + } + } + + services::ImageService::Build(session, contextPath, tags, buildArgs, dockerfilePath, target, flags, &callback, shmSize, ulimits, cancelEvent); } void GetImages(CLIExecutionContext& context) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 17b2a0758b..f8af88866d 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -905,6 +905,27 @@ try buildArgs.push_back(Options->BuildArgs.Values[i]); } + if (Options->ShmSize > 0) + { + buildArgs.push_back("--shm-size"); + buildArgs.push_back(std::to_string(Options->ShmSize)); + } + + for (ULONG i = 0; i < Options->UlimitsCount; i++) + { + RETURN_HR_IF_NULL(E_INVALIDARG, Options->Ulimits); + RETURN_HR_IF_NULL(E_INVALIDARG, Options->Ulimits[i].Name); + buildArgs.push_back("--ulimit"); + if (Options->Ulimits[i].Soft == Options->Ulimits[i].Hard) + { + buildArgs.push_back(std::format("{}={}", Options->Ulimits[i].Name, Options->Ulimits[i].Soft)); + } + else + { + buildArgs.push_back(std::format("{}={}:{}", Options->Ulimits[i].Name, Options->Ulimits[i].Soft, Options->Ulimits[i].Hard)); + } + } + buildArgs.push_back("-f"); buildArgs.push_back("-"); buildArgs.push_back(mountPath); diff --git a/test/windows/wslc/CommandLineTestCases.h b/test/windows/wslc/CommandLineTestCases.h index e7fe302d7a..ef6ea4a715 100644 --- a/test/windows/wslc/CommandLineTestCases.h +++ b/test/windows/wslc/CommandLineTestCases.h @@ -226,6 +226,10 @@ COMMAND_LINE_TEST_CASE(L"image build C:\\context -t test --build-arg KEY=VALUE - COMMAND_LINE_TEST_CASE(L"image build C:\\context --no-cache", L"build", true) COMMAND_LINE_TEST_CASE(L"image build C:\\context --no-cache --verbose", L"build", true) COMMAND_LINE_TEST_CASE(L"image build C:\\context -t test --no-cache", L"build", true) +COMMAND_LINE_TEST_CASE(L"image build C:\\context --shm-size 256m", L"build", true) +COMMAND_LINE_TEST_CASE(L"image build C:\\context --ulimit nofile=1024:2048", L"build", true) +COMMAND_LINE_TEST_CASE(L"image build C:\\context --ulimit nofile=1024:2048 --ulimit nproc=128", L"build", true) +COMMAND_LINE_TEST_CASE(L"image build C:\\context --shm-size 512m --ulimit memlock=67108864", L"build", true) COMMAND_LINE_TEST_CASE(L"image build", L"build", false) COMMAND_LINE_TEST_CASE(L"build C:\\context", L"build", true) COMMAND_LINE_TEST_CASE(L"build C:\\context -t test", L"build", true) diff --git a/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp b/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp index 8ff1e11f20..06e10855d4 100644 --- a/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp @@ -151,6 +151,46 @@ class WSLCCLIArgumentUnitTests VERIFY_THROWS(validation::ValidateGpus({L"0"}, L"gpusArg"), ArgumentException); VERIFY_THROWS(validation::ValidateGpus({L"gpu0"}, L"gpusArg"), ArgumentException); VERIFY_THROWS(validation::ValidateGpus({L""}, L"gpusArg"), ArgumentException); + + // Verify memory size parsing (case-insensitive, matching Docker CLI behavior) + auto memoryBytes = validation::GetMemorySizeFromString(L"512m"); + VERIFY_ARE_EQUAL(memoryBytes, 512LL * 1024 * 1024); + memoryBytes = validation::GetMemorySizeFromString(L"512M"); + VERIFY_ARE_EQUAL(memoryBytes, 512LL * 1024 * 1024); + memoryBytes = validation::GetMemorySizeFromString(L"1g"); + VERIFY_ARE_EQUAL(memoryBytes, 1LL * 1024 * 1024 * 1024); + memoryBytes = validation::GetMemorySizeFromString(L"1G"); + VERIFY_ARE_EQUAL(memoryBytes, 1LL * 1024 * 1024 * 1024); + memoryBytes = validation::GetMemorySizeFromString(L"256k"); + VERIFY_ARE_EQUAL(memoryBytes, 256LL * 1024); + memoryBytes = validation::GetMemorySizeFromString(L"256K"); + VERIFY_ARE_EQUAL(memoryBytes, 256LL * 1024); + memoryBytes = validation::GetMemorySizeFromString(L"1024"); + VERIFY_ARE_EQUAL(memoryBytes, 1024LL); + memoryBytes = validation::GetMemorySizeFromString(L"512mb"); + VERIFY_ARE_EQUAL(memoryBytes, 512LL * 1024 * 1024); + memoryBytes = validation::GetMemorySizeFromString(L"512MB"); + VERIFY_ARE_EQUAL(memoryBytes, 512LL * 1024 * 1024); + VERIFY_THROWS(validation::GetMemorySizeFromString(L"invalidsize"), ArgumentException); + VERIFY_THROWS(validation::GetMemorySizeFromString(L""), ArgumentException); + VERIFY_THROWS(validation::GetMemorySizeFromString(L"abc123"), ArgumentException); + VERIFY_NO_THROW(validation::ValidateMemorySize({L"512m", L"1G", L"256K"}, L"memoryArg")); + VERIFY_THROWS(validation::ValidateMemorySize({L"512m", L"notvalid"}, L"memoryArg"), ArgumentException); + + // Verify CPU (nano-cpus) parsing + auto nanoCpus = validation::GetNanoCpusFromString(L"2.0"); + VERIFY_ARE_EQUAL(nanoCpus, 2'000'000'000LL); + nanoCpus = validation::GetNanoCpusFromString(L"0.5"); + VERIFY_ARE_EQUAL(nanoCpus, 500'000'000LL); + nanoCpus = validation::GetNanoCpusFromString(L"1"); + VERIFY_ARE_EQUAL(nanoCpus, 1'000'000'000LL); + nanoCpus = validation::GetNanoCpusFromString(L"1.5"); + VERIFY_ARE_EQUAL(nanoCpus, 1'500'000'000LL); + VERIFY_THROWS(validation::GetNanoCpusFromString(L"notanumber"), ArgumentException); + VERIFY_THROWS(validation::GetNanoCpusFromString(L""), ArgumentException); + VERIFY_THROWS(validation::GetNanoCpusFromString(L"-1.0"), ArgumentException); + VERIFY_NO_THROW(validation::ValidateNanoCpus({L"1.0", L"2.5", L"0.25"}, L"cpusArg")); + VERIFY_THROWS(validation::ValidateNanoCpus({L"1.0", L"abc"}, L"cpusArg"), ArgumentException); } // Test: Verify EnumVariantMap behavior with ArgTypes. diff --git a/test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp index 05e0ee7ccd..674e7f7548 100644 --- a/test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp @@ -280,6 +280,37 @@ class WSLCE2EImageBuildTests VERIFY_ARE_NOT_EQUAL(firstId, noCacheId, L"--no-cache must rebuild the non-deterministic RUN step"); } + WSLC_TEST_METHOD(WSLCE2E_Image_Build_ShmSizeAndUlimit_Success) + { + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-resource-limits"; + auto cleanup = SetupTestDirectory(testRoot); + + auto contextDir = testRoot / L"context"; + std::error_code ec; + std::filesystem::create_directories(contextDir, ec); + THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir)); + + auto dockerfilePath = testRoot / L"Dockerfile"; +<<<<<<< HEAD + WriteTestFileContent(dockerfilePath, "FROM debian:latest\nCMD [\"echo\", \"resource-limit-ok\"]\n"); +======= + WriteTestFile(dockerfilePath, "FROM debian:latest\nCMD [\"echo\", \"resource-limit-ok\"]\n"); +>>>>>>> 498a328b28c528527d20fddbceabf5b63805c034 + + // Build with --shm-size and --ulimit to verify they are accepted and piped through. + auto buildResult = RunWslc(std::format( + L"build \"{}\" -f \"{}\" -t {} --shm-size 256m --ulimit nofile=1024:2048", + contextDir.wstring(), + dockerfilePath.wstring(), + BuiltImageResourceLimits.NameAndTag())); + buildResult.Verify({.Stderr = L"", .ExitCode = 0}); + + auto inspectData = InspectImage(BuiltImageResourceLimits.NameAndTag()); + VERIFY_IS_TRUE(inspectData.RepoTags.has_value()); + VERIFY_ARE_EQUAL(1u, inspectData.RepoTags.value().size()); + VERIFY_ARE_EQUAL(BuiltImageResourceLimits.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData.RepoTags.value()[0])); + } + private: const TestImage BuiltImage{L"wslc-e2e-build-empty-context", L"latest", L""}; const TestImage BuiltImageTag1{L"wslc-e2e-build-args-tags", L"v1", L""}; @@ -289,6 +320,7 @@ class WSLCE2EImageBuildTests const TestImage BuiltImageDockerfile{L"wslc-e2e-build-dockerfile-ctx", L"latest", L""}; const TestImage BuiltImageContainerfile{L"wslc-e2e-build-containerfile-ctx", L"latest", L""}; const TestImage BuiltImageNoCache{L"wslc-e2e-build-no-cache", L"latest", L""}; + const TestImage BuiltImageResourceLimits{L"wslc-e2e-build-resource-limits", L"latest", L""}; void BuildFromContextFile(const std::wstring& fileName, const TestImage& image) { @@ -316,6 +348,34 @@ class WSLCE2EImageBuildTests EnsureImageIsDeleted(BuiltImageDockerfile); EnsureImageIsDeleted(BuiltImageContainerfile); EnsureImageIsDeleted(BuiltImageNoCache); + EnsureImageIsDeleted(BuiltImageResourceLimits); +<<<<<<< HEAD +======= + } + + static auto SetupTestDirectory(const std::filesystem::path& testRoot) + { + std::error_code ec; + std::filesystem::remove_all(testRoot, ec); + THROW_HR_IF_MSG(E_FAIL, ec.value() != 0 && std::filesystem::exists(testRoot), "%hs", ec.message().c_str()); + + std::filesystem::create_directories(testRoot, ec); + THROW_HR_IF_MSG(E_FAIL, ec.value() != 0 || !std::filesystem::exists(testRoot), "%hs", ec.message().c_str()); + + return wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [testRoot]() { + std::error_code removeError; + std::filesystem::remove_all(testRoot, removeError); + }); + } + + static void WriteTestFile(const std::filesystem::path& path, const std::string& content) + { + std::ofstream file(path); + THROW_HR_IF(E_FAIL, !file.is_open()); + file << content; + THROW_HR_IF(E_FAIL, !file.good()); + file.close(); +>>>>>>> 498a328b28c528527d20fddbceabf5b63805c034 } }; } // namespace WSLCE2ETests