Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,121 @@
<ItemGroup>
<ReferenceCopyLocalPaths Include="$(MSBuildThisFileDirectory)..\runtimes\win-$(WSLCSDK_Platform)\wslcsdk.dll" />
</ItemGroup>

<!-- ================================================================== -->
<!-- Container Image Build Targets -->
<!-- ================================================================== -->

<!-- Default properties -->
<PropertyGroup>
<WslcImageOutputDir Condition="'$(WslcImageOutputDir)' == ''">$(OutDir)</WslcImageOutputDir>
<WslcCliPath Condition="'$(WslcCliPath)' == '' AND Exists('$(ProgramW6432)\WSL\wslc.exe')">$(ProgramW6432)\WSL\wslc.exe</WslcCliPath>
<WslcCliPath Condition="'$(WslcCliPath)' == ''">wslc</WslcCliPath>
</PropertyGroup>

<!-- Default metadata for WslcImage items -->
<ItemDefinitionGroup>
<WslcImage>
<Tag>latest</Tag>
<OutputDir>$(WslcImageOutputDir)</OutputDir>
</WslcImage>
</ItemDefinitionGroup>

<!-- Auto-detection fallback: if no WslcImage declared, detect container/Dockerfile -->
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should do that. The developer is not required to build their container images here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, this is just fallback, will remove it later.

<ItemGroup Condition="'@(WslcImage)' == '' AND Exists('$(ProjectDir)container\Dockerfile')">
<WslcImage Include="$(MSBuildProjectName)"
Dockerfile="$(ProjectDir)container\Dockerfile"
Context="$(ProjectDir)container"
Sources="$(ProjectDir)container" />
</ItemGroup>

<ItemGroup Condition="'@(WslcImage)' == '' AND Exists('$(ProjectDir)docker\Dockerfile')">
<WslcImage Include="$(MSBuildProjectName)"
Dockerfile="$(ProjectDir)docker\Dockerfile"
Context="$(ProjectDir)docker"
Sources="$(ProjectDir)docker" />
</ItemGroup>

<!-- Check that wslc CLI is installed -->
<Target Name="WslcCheckDependencies"
BeforeTargets="WslcBuildAllImages"
Condition="'@(WslcImage)' != ''">

<Exec Command="&quot;$(WslcCliPath)&quot; --version"
IgnoreExitCode="true"
ConsoleToMSBuild="true"
StandardOutputImportance="low"
StandardErrorImportance="low">
<Output TaskParameter="ExitCode" PropertyName="_WslcExitCode" />
</Exec>

<Error Condition="'$(_WslcExitCode)' != '0'"
Code="WSLC0001"
Text="The WSLC runtime was not found. Install WSL by running: wsl --install --no-distribution" />
</Target>

<!-- Outer target: dispatch each WslcImage to inner target via MSBuild task -->
<Target Name="WslcBuildAllImages"
AfterTargets="Build"
DependsOnTargets="WslcCheckDependencies"
Condition="'@(WslcImage)' != ''">
Comment on lines +64 to +67
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WslcBuildAllImages runs AfterTargets="Build" whenever @(WslcImage) is non-empty. In Visual Studio this will also execute during design-time builds (when $(DesignTimeBuild) is true), which can trigger expensive wslc image build calls and slow the IDE. Add a guard to skip both WslcBuildAllImages (and ideally WslcCheckDependencies) during design-time builds (e.g., require $(DesignTimeBuild) != 'true').

Copilot uses AI. Check for mistakes.

<MSBuild Projects="$(MSBuildProjectFullPath)"
Targets="_WslcBuildSingleImage"
Properties="Configuration=$(Configuration);Platform=$(Platform);_WslcName=%(WslcImage.Identity);_WslcTag=%(WslcImage.Tag);_WslcDockerfile=%(WslcImage.Dockerfile);_WslcContext=%(WslcImage.Context);_WslcSourceDir=%(WslcImage.Sources);_WslcOutput=%(WslcImage.OutputDir)" />
</Target>
Comment on lines +69 to +72
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%(WslcImage.Sources) can be semicolon-delimited (multi-directory Sources), but here it’s passed through the MSBuild task Properties attribute, which itself is semicolon-delimited. Without escaping, values after the first ; will be treated as separate properties and the extra source dirs will be dropped/break parsing. Escape this value (e.g., via MSBuild::Escape) or pass the source directories via an item list instead of a property string.

Copilot uses AI. Check for mistakes.

<!--
Collect source files for incremental check.
Wildcards are expanded here in ItemGroup Include (not in Inputs attribute or MSBuild task Properties,
where they get escaped). Sources can be semicolon separated directory paths;
each is expanded with **\* to collect all files recursively.
-->
<Target Name="_WslcCollectSources" BeforeTargets="_WslcBuildSingleImage">
<ItemGroup>
<_WslcSourceDirs Include="$(_WslcSourceDir.Split(';'))" />
<_WslcSourceFiles Include="$(_WslcDockerfile)" />
<_WslcSourceFiles Include="%(_WslcSourceDirs.Identity)\**\*" Condition="'%(_WslcSourceDirs.Identity)' != ''" />
</ItemGroup>
</Target>

<!-- Inner target: build a single image with incremental check -->
<Target Name="_WslcBuildSingleImage"
DependsOnTargets="_WslcCollectSources"
Inputs="@(_WslcSourceFiles)"
Outputs="$(IntDir)wslc_$(_WslcName).marker">

Comment on lines +91 to +93
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The marker file path uses $(_WslcName) (from %(WslcImage.Identity)) directly: $(IntDir)wslc_$(_WslcName).marker. If an image name contains / (common for registry/repo names like ghcr.io/org/image), this becomes a nested path and MakeDir only creates $(IntDir), so the target can fail to create/touch the marker. Consider deriving a file-system-safe marker name (e.g., replace invalid path chars or use a hash) and use that for marker/tlog filenames.

Copilot uses AI. Check for mistakes.
<MakeDir Directories="$(IntDir)" />

<Message Importance="high"
Text="WSLC: Building image '$(_WslcName):$(_WslcTag)'..." />

<Exec Command="&quot;$(WslcCliPath)&quot; image build -t &quot;$(_WslcName):$(_WslcTag)&quot; -f &quot;$(_WslcDockerfile)&quot; &quot;$(_WslcContext)&quot;"
ConsoleToMSBuild="true" />

<!--
Uncomment when wslc image save is available:
<MakeDir Directories="$(_WslcOutput)" />
<Exec Command="&quot;$(WslcCliPath)&quot; image save -o &quot;$(_WslcOutput)\$(_WslcName).tar&quot; &quot;$(_WslcName):$(_WslcTag)&quot;"
ConsoleToMSBuild="true" />
-->

<Touch Files="$(IntDir)wslc_$(_WslcName).marker" AlwaysCreate="true" />

<Message Importance="high"
Text="WSLC: [$(_WslcName)] Image '$(_WslcName):$(_WslcTag)' built successfully." />
</Target>

<!-- Clean container artifacts -->
<Target Name="WslcClean"
AfterTargets="Clean"
Condition="'@(WslcImage)' != ''">

<Delete Files="$(IntDir)wslc_%(WslcImage.Identity).marker" />
<!--
Uncomment when wslc image save is available:
<Delete Files="%(WslcImage.OutputDir)\%(WslcImage.Identity).tar" />
-->
</Target>

</Project>
89 changes: 89 additions & 0 deletions nuget/Microsoft.WSL.Containers/build/cmake/wslcConfig.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# wslcConfig.cmake - Container image build support for CMake
#
# Provides the wslc_add_image() function for declaring container image
# build targets with incremental rebuild support.
#
# Usage:
# list(APPEND CMAKE_MODULE_PATH "<nuget>/build/cmake")
# find_package(wslc REQUIRED)
#
# wslc_add_image(
# NAME my-server
# DOCKERFILE container/Dockerfile
# CONTEXT container/
# SOURCES container/src/*.cpp container/src/*.h
# TAG latest
# OUTPUT ${CMAKE_BINARY_DIR}/images
# )
#
# # With explicit image registry/name (IMAGE defaults to NAME if omitted):
# wslc_add_image(
# NAME my-server
# IMAGE ghcr.io/myorg/my-server
# TAG v1.2.3
# DOCKERFILE container/Dockerfile
# CONTEXT container/
# )

function(wslc_add_image)
cmake_parse_arguments(
PARSE_ARGV 0 ARG
"" # options (none)
"NAME;IMAGE;TAG;DOCKERFILE;CONTEXT;OUTPUT" # one-value keywords
"SOURCES" # multi-value keywords
)

# Validate required arguments
if(NOT ARG_NAME)
message(FATAL_ERROR "wslc_add_image: NAME is required")
endif()
if(NOT ARG_DOCKERFILE)
message(FATAL_ERROR "wslc_add_image: DOCKERFILE is required")
endif()
if(NOT ARG_CONTEXT)
message(FATAL_ERROR "wslc_add_image: CONTEXT is required")
endif()

# Defaults
if(NOT ARG_IMAGE)
set(ARG_IMAGE "${ARG_NAME}")
endif()
if(NOT ARG_TAG)
set(ARG_TAG "latest")
endif()
if(NOT ARG_OUTPUT)
set(ARG_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}")
endif()

# Find wslc CLI
if(NOT WSLC_CLI_PATH)
find_program(WSLC_CLI_PATH wslc PATHS "$ENV{ProgramW6432}/WSL" "$ENV{ProgramFiles}/WSL")
if(NOT WSLC_CLI_PATH)
message(FATAL_ERROR "wslc CLI not found. Install WSL by running: wsl --install --no-distribution")
endif()
endif()

set(_image_ref "${ARG_IMAGE}:${ARG_TAG}")
set(_marker "${CMAKE_CURRENT_BINARY_DIR}/wslc_${ARG_NAME}.marker")
# Uncomment when wslc image save is available:
# set(_tar_output "${ARG_OUTPUT}/${ARG_NAME}.tar")

# Resolve source globs to file lists
file(GLOB_RECURSE _resolved_sources CONFIGURE_DEPENDS ${ARG_SOURCES})

add_custom_command(
OUTPUT "${_marker}"
COMMAND "${WSLC_CLI_PATH}" image build -t "${_image_ref}" -f "${ARG_DOCKERFILE}" "${ARG_CONTEXT}"
# Uncomment when wslc image save is available:
# COMMAND ${CMAKE_COMMAND} -E make_directory "${ARG_OUTPUT}"
# COMMAND "${WSLC_CLI_PATH}" image save -o "${_tar_output}" "${_image_ref}"
COMMAND ${CMAKE_COMMAND} -E touch "${_marker}"
DEPENDS ${_resolved_sources} "${ARG_DOCKERFILE}"
COMMENT "WSLC: Building image '${_image_ref}'..."
VERBATIM
)

add_custom_target(wslc_image_${ARG_NAME} ALL
DEPENDS "${_marker}"
)
endfunction()
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,63 @@
</ItemDefinitionGroup>

<Import Project="$(MSBuildThisFileDirectory)..\Microsoft.WSL.Containers.common.targets" />

<!-- ================================================================== -->
<!-- C++ specific: Write .tlog files for VS Fast Up-to-Date Check -->
<!-- ================================================================== -->

<Target Name="_WslcWriteAllTlogs"
AfterTargets="WslcBuildAllImages"
Condition="'@(WslcImage)' != '' AND '$(TLogLocation)' != ''">

<MSBuild Projects="$(MSBuildProjectFullPath)"
Targets="_WslcWriteSingleTlog"
Properties="Configuration=$(Configuration);Platform=$(Platform);_WslcName=%(WslcImage.Identity);_WslcDockerfile=%(WslcImage.Dockerfile);_WslcSourceDir=%(WslcImage.Sources);_WslcTlogDir=$([MSBuild]::EnsureTrailingSlash('$(TLogLocation)'))" />
</Target>

<Target Name="_WslcWriteSingleTlog">
<ItemGroup>
<_WslcTlogSourceDirs Include="$(_WslcSourceDir.Split(';'))" />
<_WslcTlogInputs Include="$(_WslcDockerfile)" />
<_WslcTlogInputs Include="%(_WslcTlogSourceDirs.Identity)\**\*" />
</ItemGroup>

<PropertyGroup>
<_WslcDockerfileFullPath>$([System.IO.Path]::GetFullPath('$(_WslcDockerfile)'))</_WslcDockerfileFullPath>
<_WslcMarkerFullPath>$([System.IO.Path]::GetFullPath('$(IntDir)wslc_$(_WslcName).marker'))</_WslcMarkerFullPath>
<!--
Uncomment when wslc image save is available:
<_WslcTarFullPath>$([System.IO.Path]::GetFullPath('$(WslcImageOutputDir)$(_WslcName).tar'))</_WslcTarFullPath>
-->
</PropertyGroup>

<WriteLinesToFile
File="$(_WslcTlogDir)wslc.$(_WslcName).read.1.tlog"
Lines="^$(_WslcDockerfileFullPath);@(_WslcTlogInputs->'%(FullPath)')"
Overwrite="true"
Encoding="Unicode" />

<!--
Uncomment when wslc image save is available, to track tar output:
<WriteLinesToFile
File="$(_WslcTlogDir)wslc.$(_WslcName).write.1.tlog"
Lines="^$(_WslcDockerfileFullPath);$(_WslcMarkerFullPath);$(_WslcTarFullPath)"
Overwrite="true"
Encoding="Unicode" />
-->
<WriteLinesToFile
File="$(_WslcTlogDir)wslc.$(_WslcName).write.1.tlog"
Lines="^$(_WslcDockerfileFullPath);$(_WslcMarkerFullPath)"
Overwrite="true"
Encoding="Unicode" />
</Target>

<!-- Clean tlog files -->
<Target Name="_WslcCleanTlogs"
AfterTargets="WslcClean"
Condition="'@(WslcImage)' != '' AND '$(TLogLocation)' != ''">
<Delete Files="$(TLogLocation)wslc.%(WslcImage.Identity).read.1.tlog" />
<Delete Files="$(TLogLocation)wslc.%(WslcImage.Identity).write.1.tlog" />
</Target>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,23 @@

<Import Project="$(MSBuildThisFileDirectory)..\Microsoft.WSL.Containers.common.targets" />

<!-- ================================================================== -->
<!-- C# specific: CPS Up-to-Date Check for container source files -->
<!-- ================================================================== -->

<!-- Register marker files as build outputs for CPS up-to-date check -->
<ItemGroup Condition="'$(UsingMicrosoftNETSdk)' == 'true'">
<UpToDateCheckBuilt Include="$(IntDir)wslc_%(WslcImage.Identity).marker" />
</ItemGroup>

<!-- Expand multi-directory Sources into UpToDateCheckInput at target time -->
<Target Name="_WslcRegisterUpToDateCheckInputs"
BeforeTargets="CollectUpToDateCheckBuiltDesignTime"
Condition="'$(UsingMicrosoftNETSdk)' == 'true' AND '@(WslcImage)' != ''">
<ItemGroup>
<_WslcUpToDateDirs Include="%(WslcImage.Sources)" Separator=";" />
<UpToDateCheckInput Include="%(_WslcUpToDateDirs.Identity)\**\*" Condition="'%(_WslcUpToDateDirs.Identity)' != ''" />
</ItemGroup>
</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildThisFileDirectory)..\build\Microsoft.WSL.Containers.common.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildThisFileDirectory)..\..\build\native\Microsoft.WSL.Containers.targets" />
</Project>