diff --git a/src/windows/wslc/CMakeLists.txt b/src/windows/wslc/CMakeLists.txt index b1552aa9a..b60cf215b 100644 --- a/src/windows/wslc/CMakeLists.txt +++ b/src/windows/wslc/CMakeLists.txt @@ -6,15 +6,72 @@ list(TRANSFORM WSLC_SUBDIR_PATHS APPEND /*.cpp OUTPUT_VARIABLE SOURCE_PATTERNS) file(GLOB_RECURSE HEADERS CONFIGURE_DEPENDS ${HEADER_PATTERNS}) file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS ${SOURCE_PATTERNS}) +# Build Go template renderer library +set(GO_TEMPLATE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/tools/gotemplate) +set(GO_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/gotemplate") + +find_program(GO_EXECUTABLE go REQUIRED) + +# Reuse TARGET_PLATFORM already normalized by the root CMakeLists.txt +if("${TARGET_PLATFORM}" STREQUAL "arm64") + set(GO_ARCH arm64) + set(GO_LIB_MACHINE ARM64) + find_program(GO_CLANG_EXECUTABLE + NAMES clang + HINTS + "$ENV{VCToolsInstallDir}/Llvm/x64/bin" + "$ENV{VCINSTALLDIR}/Tools/Llvm/x64/bin" + "$ENV{ProgramFiles}/LLVM/bin" + "$ENV{ProgramFiles\(x86\)}/LLVM/bin") + + if(NOT GO_CLANG_EXECUTABLE) + message(FATAL_ERROR "clang is required for arm64 cgo builds. Install the Windows Clang toolchain or add clang to PATH.") + endif() + + set(GO_CGO_ENV + "CC=${GO_CLANG_EXECUTABLE}" + "CGO_CFLAGS=--target=aarch64-pc-windows-msvc" + "CGO_CXXFLAGS=--target=aarch64-pc-windows-msvc" + "CGO_LDFLAGS=--target=aarch64-pc-windows-msvc") +else() + set(GO_ARCH amd64) + set(GO_LIB_MACHINE X64) + set(GO_CGO_ENV) +endif() + +set(GO_OUTPUT_DLL ${GO_OUTPUT_DIR}/render.dll) +set(GO_OUTPUT_LIB ${GO_OUTPUT_DIR}/render.lib) +add_custom_command( + OUTPUT ${GO_OUTPUT_DLL} ${GO_OUTPUT_LIB} + COMMAND ${CMAKE_COMMAND} -E make_directory ${GO_OUTPUT_DIR} + COMMAND ${CMAKE_COMMAND} -E env GOOS=windows GOARCH=${GO_ARCH} CGO_ENABLED=1 ${GO_CGO_ENV} + ${GO_EXECUTABLE} build -o ${GO_OUTPUT_DLL} -buildmode=c-shared -trimpath -ldflags=-s\ -w render.go + COMMAND lib /def:${GO_TEMPLATE_DIR}/render.def /out:${GO_OUTPUT_LIB} /machine:${GO_LIB_MACHINE} + WORKING_DIRECTORY ${GO_TEMPLATE_DIR} + DEPENDS ${GO_TEMPLATE_DIR}/render.go ${GO_TEMPLATE_DIR}/render.def + COMMENT "Building Go template renderer library" + VERBATIM +) + +add_custom_target(gotemplate_lib ALL DEPENDS ${GO_OUTPUT_DLL} ${GO_OUTPUT_LIB}) + +# Include Go source files for browsing in Solution Explorer +set(GO_SOURCES ${GO_TEMPLATE_DIR}/render.go) +set_source_files_properties(${GO_SOURCES} PROPERTIES HEADER_FILE_ONLY TRUE) + # Object library for WSLC components. # Used to build the executable and also unit testing components. -add_library(wslclib OBJECT ${SOURCES} ${HEADERS}) +add_library(wslclib OBJECT ${SOURCES} ${HEADERS} ${GO_SOURCES}) target_include_directories(wslclib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${WSLC_SUBDIR_PATHS}) target_link_libraries(wslclib ${COMMON_LINK_LIBRARIES} yaml-cpp - common) + common + ${GO_OUTPUT_LIB}) + +# Add dependency on Go library +add_dependencies(wslclib gotemplate_lib) target_precompile_headers(wslclib REUSE_FROM common) set_target_properties(wslclib PROPERTIES FOLDER windows) @@ -24,7 +81,11 @@ add_executable(wslc $) target_link_libraries(wslc wslclib) +add_custom_command(TARGET wslc POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${GO_OUTPUT_DLL} $/render.dll +) + set_target_properties(wslc PROPERTIES FOLDER windows) # For prettier source tree browsing -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES} ${HEADERS}) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES} ${HEADERS} ${GO_SOURCES}) diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index ea47a5f55..4505d9a37 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -31,10 +31,6 @@ void Argument::Validate(const ArgMap& execArgs) const { switch (m_argType) { - case ArgType::Format: - validation::ValidateFormatTypeFromString(execArgs.GetAll(), m_name); - break; - case ArgType::Signal: validation::ValidateWSLCSignalFromString(execArgs.GetAll(), m_name); break; @@ -144,29 +140,19 @@ WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring return static_cast(signalValue); } -void ValidateFormatTypeFromString(const std::vector& values, const std::wstring& argName) -{ - for (const auto& value : values) - { - std::ignore = GetFormatTypeFromString(value, argName); - } -} - -FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName) +FormatType GetFormatTypeFromString(const std::wstring& input) { if (IsEqual(input, L"json")) { return FormatType::Json; } - else if (IsEqual(input, L"table")) + + if (IsEqual(input, L"table")) { return FormatType::Table; } - else - { - throw ArgumentException(std::format( - L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input)); - } + + return FormatType::Template; } } // namespace wsl::windows::wslc::validation \ No newline at end of file diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h index 23208699c..08f1dfb59 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.h +++ b/src/windows/wslc/arguments/ArgumentValidation.h @@ -57,8 +57,7 @@ T GetIntegerFromString(const std::wstring& value, const std::wstring& argName = void ValidateWSLCSignalFromString(const std::vector& values, const std::wstring& argName); WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring& argName = {}); -void ValidateFormatTypeFromString(const std::vector& values, const std::wstring& argName); -FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName = {}); +FormatType GetFormatTypeFromString(const std::wstring& input); void ValidateVolumeMount(const std::vector& values); diff --git a/src/windows/wslc/commands/ContainerCommand.h b/src/windows/wslc/commands/ContainerCommand.h index 503618a6d..269084147 100644 --- a/src/windows/wslc/commands/ContainerCommand.h +++ b/src/windows/wslc/commands/ContainerCommand.h @@ -119,7 +119,6 @@ struct ContainerListCommand final : public Command std::wstring LongDescription() const override; protected: - void ValidateArgumentsInternal(const ArgMap& execArgs) const override; void ExecuteInternal(CLIExecutionContext& context) const override; }; diff --git a/src/windows/wslc/commands/ContainerListCommand.cpp b/src/windows/wslc/commands/ContainerListCommand.cpp index 1fb3a6795..7662694d7 100644 --- a/src/windows/wslc/commands/ContainerListCommand.cpp +++ b/src/windows/wslc/commands/ContainerListCommand.cpp @@ -46,18 +46,6 @@ std::wstring ContainerListCommand::LongDescription() const return Localization::WSLCCLI_ContainerListLongDesc(); } -void ContainerListCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const -{ - if (execArgs.Contains(ArgType::Format)) - { - auto format = execArgs.Get(); - if (!IsEqual(format, L"json") && !IsEqual(format, L"table")) - { - throw CommandException(Localization::WSLCCLI_InvalidFormatError()); - } - } -} - // clang-format off void ContainerListCommand::ExecuteInternal(CLIExecutionContext& context) const { diff --git a/src/windows/wslc/core/TemplateRenderer.cpp b/src/windows/wslc/core/TemplateRenderer.cpp new file mode 100644 index 000000000..5f7839487 --- /dev/null +++ b/src/windows/wslc/core/TemplateRenderer.cpp @@ -0,0 +1,68 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + TemplateRenderer.cpp + +Abstract: + + Implementation of the Go template renderer. + +--*/ + +#include "TemplateRenderer.h" +#include + +// Forward-declare the Go template renderer functions (exported from render.dll). +// We declare these directly instead of including the cgo-generated render.h +// to avoid Go boilerplate types that don't compile cleanly with MSVC. +extern "C" { +int TryRenderGoTemplate(const char* templateStr, const char* jsonData, char** output); +void FreeGoString(char* ptr); +} + +namespace wsl::windows::wslc::core { + +using namespace wsl::shared::string; + +TemplateRenderer::RenderResult TemplateRenderer::TryRender(const std::string& templateStr, const std::string& jsonData, std::wstring& output) +{ + try + { + char* rawOutput = nullptr; + auto success = TryRenderGoTemplate(templateStr.c_str(), jsonData.c_str(), &rawOutput); + + std::string result(rawOutput ? rawOutput : ""); + FreeGoString(rawOutput); + + output = MultiByteToWide(result); + return static_cast(success); + } + catch (const std::exception& ex) + { + output = MultiByteToWide(ex.what()); + return RenderResult::Fail_Unknown; + } +} + +void TemplateRenderer::Render(const std::string& templateStr, const std::string& jsonData, std::wstring& output) +{ + switch (TryRender(templateStr, jsonData, output)) + { + case RenderResult::Success: + return; + case RenderResult::Fail_NullPointer: + THROW_HR(E_POINTER); + case RenderResult::Fail_ParseJSON: + case RenderResult::Fail_ParseTemplate: + case RenderResult::Fail_ExecuteTemplate: + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, output); + case RenderResult::Fail_Unknown: + default: + THROW_HR(E_UNEXPECTED); + } +} + +} // namespace wsl::windows::wslc::core diff --git a/src/windows/wslc/core/TemplateRenderer.h b/src/windows/wslc/core/TemplateRenderer.h new file mode 100644 index 000000000..3bc57fb14 --- /dev/null +++ b/src/windows/wslc/core/TemplateRenderer.h @@ -0,0 +1,39 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + TemplateRenderer.h + +Abstract: + + This file contains the interface for rendering Go templates with JSON data. + +--*/ + +#pragma once + +#include + +namespace wsl::windows::wslc::core { + +struct TemplateRenderer +{ + enum class RenderResult + { + Success = 0, + Fail_NullPointer = 1, + Fail_ParseJSON = 2, + Fail_ParseTemplate = 3, + Fail_ExecuteTemplate = 4, + + // All other failures + Fail_Unknown = -1, + }; + + static RenderResult TryRender(const std::string& templateStr, const std::string& jsonData, std::wstring& output); + static void Render(const std::string& templateStr, const std::string& jsonData, std::wstring& output); +}; + +} // namespace wsl::windows::wslc::core \ No newline at end of file diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index c4da81fab..a95c51040 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -25,6 +25,7 @@ enum class FormatType { Table, Json, + Template, }; struct ContainerOptions diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 55e19e478..d38f7ad9e 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -11,6 +11,7 @@ Module Name: Implementation of container command related execution logic. --*/ + #include "Argument.h" #include "ArgumentValidation.h" #include "CLIExecutionContext.h" @@ -20,6 +21,7 @@ Module Name: #include "SessionModel.h" #include "SessionService.h" #include "TableOutput.h" +#include "TemplateRenderer.h" #include #include @@ -29,6 +31,7 @@ using namespace wsl::windows::common::wslutil; using namespace wsl::windows::wslc::execution; using namespace wsl::windows::wslc::models; using namespace wsl::windows::wslc::services; +using namespace wsl::windows::wslc::core; namespace wsl::windows::wslc::task { void AttachContainer::operator()(CLIExecutionContext& context) const @@ -167,6 +170,18 @@ void ListContainers(CLIExecutionContext& context) table.Complete(); break; } + case FormatType::Template: + { + auto templateStr = WideToMultiByte(context.Args.Get()); + for (const auto& container : containers) + { + auto json = ToJson(container); + std::wstring result; + TemplateRenderer::Render(templateStr, json, result); + PrintMessage(result); + } + break; + } default: THROW_HR(E_UNEXPECTED); } diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index 461e00589..6cd0b3173 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -22,6 +22,7 @@ Module Name: #include "PullImageCallback.h" #include "TableOutput.h" #include "Task.h" +#include "TemplateRenderer.h" #include using namespace wsl::shared; @@ -29,6 +30,7 @@ using namespace wsl::windows::common::string; using namespace wsl::windows::common::wslutil; using namespace wsl::windows::wslc::execution; using namespace wsl::windows::wslc::services; +using namespace wsl::windows::wslc::core; namespace wsl::windows::wslc::task { void BuildImage(CLIExecutionContext& context) @@ -124,6 +126,18 @@ void ListImages(CLIExecutionContext& context) table.Complete(); break; } + case FormatType::Template: + { + auto templateStr = WideToMultiByte(context.Args.Get()); + for (const auto& image : images) + { + auto json = ToJson(image); + std::wstring result; + TemplateRenderer::Render(templateStr, json, result); + PrintMessage(result); + } + break; + } default: THROW_HR(E_UNEXPECTED); } diff --git a/src/windows/wslc/tools/gotemplate/render.def b/src/windows/wslc/tools/gotemplate/render.def new file mode 100644 index 000000000..73d74a123 --- /dev/null +++ b/src/windows/wslc/tools/gotemplate/render.def @@ -0,0 +1,5 @@ +LIBRARY render + +EXPORTS + TryRenderGoTemplate + FreeGoString \ No newline at end of file diff --git a/src/windows/wslc/tools/gotemplate/render.go b/src/windows/wslc/tools/gotemplate/render.go new file mode 100644 index 000000000..c0d8cc7df --- /dev/null +++ b/src/windows/wslc/tools/gotemplate/render.go @@ -0,0 +1,80 @@ +//go:build cgo + +package main + +/* +#include +*/ +import "C" + +import ( + "bytes" + "encoding/json" + "text/template" + "unsafe" +) + +func toJSONString(v interface{}) (string, error) { + b, err := json.Marshal(v) + if err != nil { + return "", err + } + + return string(b), nil +} + +// Return codes +// Those errors should be in sync with the ones defined in TemplateRenderer +const ( + Success = 0 + Fail_NullPointer = 1 + Fail_ParseJSON = 2 + Fail_ParseTemplate = 3 + Fail_ExecuteTemplate = 4 +) + +// TryRenderGoTemplate renders a Go template with the provided JSON data. +// Returns 1 on success, 0 on failure. On success, *output contains the rendered result. +// On failure, *output contains the error message. The caller must free *output with FreeGoString. +// +//export TryRenderGoTemplate +func TryRenderGoTemplate(templateStr *C.char, jsonData *C.char, output **C.char) C.int { + if templateStr == nil || jsonData == nil || output == nil { + return Fail_NullPointer + } + + var data interface{} + if err := json.Unmarshal([]byte(C.GoString(jsonData)), &data); err != nil { + *output = C.CString(err.Error()) + return Fail_ParseJSON + } + + funcMap := template.FuncMap{ + "json": toJSONString, + } + + tmpl, err := template.New("gotemplate").Funcs(funcMap).Parse(C.GoString(templateStr)) + if err != nil { + *output = C.CString(err.Error()) + return Fail_ParseTemplate + } + + var result bytes.Buffer + if err = tmpl.Execute(&result, data); err != nil { + *output = C.CString(err.Error()) + return Fail_ExecuteTemplate + } + + *output = C.CString(result.String()) + return Success +} + +// FreeGoString frees a string allocated by TryRenderGoTemplate. +// +//export FreeGoString +func FreeGoString(ptr *C.char) { + C.free(unsafe.Pointer(ptr)) +} + +func main() { +} diff --git a/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp b/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp index 26508f868..b48e2e257 100644 --- a/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp @@ -136,9 +136,10 @@ class WSLCCLIArgumentUnitTests VERIFY_ARE_EQUAL(format, FormatType::Json); format = validation::GetFormatTypeFromString(L"table"); VERIFY_ARE_EQUAL(format, FormatType::Table); - VERIFY_THROWS(validation::GetFormatTypeFromString(L"xml"), ArgumentException); - VERIFY_NO_THROW(validation::ValidateFormatTypeFromString({L"json", L"table"}, L"formatArg")); - VERIFY_THROWS(validation::ValidateFormatTypeFromString({L"JSON", L"TABLE", L"csv"}, L"formatArg"), ArgumentException); + format = validation::GetFormatTypeFromString(L"{{json .}}"); + VERIFY_ARE_EQUAL(format, FormatType::Template); + format = validation::GetFormatTypeFromString(L"template"); + VERIFY_ARE_EQUAL(format, FormatType::Template); } // Test: Verify EnumVariantMap behavior with ArgTypes. diff --git a/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp index 94d4f7679..44ac3a901 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp @@ -154,12 +154,6 @@ class WSLCE2EContainerListTests VERIFY_ARE_EQUAL(containerId, outputLine); } - WSLC_TEST_METHOD(WSLCE2E_Container_List_InvalidFormatOption) - { - const auto result = RunWslc(L"container list --format invalid"); - result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1}); - } - WSLC_TEST_METHOD(WSLCE2E_Container_List_JsonFormat) { VerifyContainerIsNotListed(WslcContainerName); @@ -203,6 +197,23 @@ class WSLCE2EContainerListTests VERIFY_IS_TRUE(std::find(containerIds.begin(), containerIds.end(), containerId2) != containerIds.end()); } + WSLC_TEST_METHOD(WSLCE2E_Container_List_TemplateFormat) + { + // Create a container + RunWslcAndVerify( + std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()), {.Stderr = L"", .ExitCode = 0}); + RunWslcAndVerify( + std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag()), {.Stderr = L"", .ExitCode = 0}); + + // List containers with json format + auto result = RunWslc(L"container list --all --format \"Name = {{.Name}}\""); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto output = result.GetStdoutLines(); + VERIFY_ARE_EQUAL(2U, output.size()); + VERIFY_ARE_EQUAL(std::format(L"Name = {}", WslcContainerName), output[0]); + VERIFY_ARE_EQUAL(std::format(L"Name = {}", WslcContainerName2), output[1]); + } + private: const std::wstring WslcContainerName = L"wslc-test-container"; const std::wstring WslcContainerName2 = L"wslc-test-container-2"; diff --git a/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp index 86df7ad53..216b1d2a4 100644 --- a/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp @@ -79,12 +79,6 @@ class WSLCE2EImageListTests VERIFY_IS_TRUE(imageFound); } - WSLC_TEST_METHOD(WSLCE2E_Image_List_InvalidFormatOption) - { - const auto result = RunWslc(L"image list --format invalid"); - result.Verify({.Stderr = L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.\r\n", .ExitCode = 1}); - } - WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat) { const auto result = RunWslc(L"image list --format json"); @@ -128,6 +122,16 @@ class WSLCE2EImageListTests VERIFY_IS_TRUE(foundHeader, L"Expected table header with REPOSITORY, TAG, IMAGE ID, CREATED, SIZE columns"); } + WSLC_TEST_METHOD(WSLCE2E_Image_List_TemplateFormat) + { + auto result = RunWslc(L"image list --format \"{{.Repository}}:{{.Tag}}\""); + result.Verify({.Stderr = L"", .ExitCode = 0}); + auto output = result.GetStdoutLines(); + VERIFY_ARE_EQUAL(2U, output.size()); + VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), output[0]); + VERIFY_ARE_EQUAL(AlpineImage.NameAndTag(), output[1]); + } + private: const TestImage& DebianImage = DebianTestImage(); const TestImage& AlpineImage = AlpineTestImage();