Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,99 @@
<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)' == ''">wslc</WslcCliPath>
</PropertyGroup>

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

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

<Exec Command="$(WslcCliPath) --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);_WslcSources=%(WslcImage.Sources);_WslcOutput=%(WslcImage.Output)" />
</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 only expand in ItemGroup Include) -->
<Target Name="_WslcCollectSources" BeforeTargets="_WslcBuildSingleImage">
<ItemGroup>
<_WslcSourceFiles Include="$(_WslcDockerfile);$(_WslcSources)" />
</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);$(_WslcOutput)" />

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

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

<Message Importance="high"
Text="WSLC: Saving image '$(_WslcName):$(_WslcTag)' to $(_WslcOutput)\$(_WslcName).tar..." />

<Exec Command="$(WslcCliPath) image save &quot;$(_WslcName):$(_WslcTag)&quot; -o &quot;$(_WslcOutput)\$(_WslcName).tar&quot;"
ConsoleToMSBuild="true" />

<!-- Save image ID for efficient check-then-load at runtime -->
<Exec Command="$(WslcCliPath) image inspect &quot;$(_WslcName):$(_WslcTag)&quot; --format {{.Id}} &gt; &quot;$(_WslcOutput)\$(_WslcName).id&quot;"
IgnoreStandardErrorWarningFormat="true" />

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

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

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

<Delete Files="%(WslcImage.Output)\%(WslcImage.Identity).tar" />
<Delete Files="%(WslcImage.Output)\%(WslcImage.Identity).id" />
<Delete Files="$(IntDir)wslc_%(WslcImage.Identity).marker" />
</Target>

</Project>
67 changes: 67 additions & 0 deletions nuget/Microsoft.WSL.Containers/build/cmake/wslcConfig.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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
# )

function(wslc_add_image)
cmake_parse_arguments(
PARSE_ARGV 0 ARG
"" # options (none)
"NAME;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_TAG)
set(ARG_TAG "latest")
endif()
if(NOT ARG_OUTPUT)
set(ARG_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}")
endif()

set(_image_ref "${ARG_NAME}:${ARG_TAG}")
set(_tar_output "${ARG_OUTPUT}/${ARG_NAME}.tar")
set(_id_output "${ARG_OUTPUT}/${ARG_NAME}.id")
set(_marker "${CMAKE_CURRENT_BINARY_DIR}/wslc_${ARG_NAME}.marker")

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

add_custom_command(
OUTPUT "${_marker}"
COMMAND wslc image build -t "${_image_ref}" -f "${ARG_DOCKERFILE}" "${ARG_CONTEXT}"
COMMAND wslc image save "${_image_ref}" -o "${_tar_output}"
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,49 @@
</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);_WslcSources=%(WslcImage.Sources);_WslcOutput=%(WslcImage.Output);_WslcTlogDir=$(TLogLocation)" />
</Target>

<Target Name="_WslcWriteSingleTlog">
<ItemGroup>
<_WslcTlogInputs Include="$(_WslcDockerfile);$(_WslcSources)" />
</ItemGroup>

<PropertyGroup>
<_WslcDockerfileFullPath>$([System.IO.Path]::GetFullPath('$(_WslcDockerfile)'))</_WslcDockerfileFullPath>
<_WslcMarkerFullPath>$([System.IO.Path]::GetFullPath('$(IntDir)wslc_$(_WslcName).marker'))</_WslcMarkerFullPath>
</PropertyGroup>

<WriteLinesToFile
File="$(_WslcTlogDir)wslc.$(_WslcName).read.1.tlog"
Lines="^$(_WslcDockerfileFullPath);@(_WslcTlogInputs->'%(FullPath)')"
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,13 @@

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

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

<ItemGroup Condition="'$(UsingMicrosoftNETSdk)' == 'true'">
<UpToDateCheckInput Include="%(WslcImage.Sources)" />
<UpToDateCheckBuilt Include="$(IntDir)wslc_%(WslcImage.Identity).marker" />
</ItemGroup>

</Project>
Loading