diff --git a/nuget/Microsoft.WSL.Containers/build/Microsoft.WSL.Containers.common.targets b/nuget/Microsoft.WSL.Containers/build/Microsoft.WSL.Containers.common.targets
new file mode 100644
index 000000000..97a443325
--- /dev/null
+++ b/nuget/Microsoft.WSL.Containers/build/Microsoft.WSL.Containers.common.targets
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+
+
+ wslc
+
+ <_WslcIntDir Condition="'$(IntDir)' != ''">$(IntDir)
+ <_WslcIntDir Condition="'$(_WslcIntDir)' == '' AND '$(IntermediateOutputPath)' != ''">$(IntermediateOutputPath)
+ <_WslcIntDir Condition="'$(_WslcIntDir)' == ''">obj\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_WslcSourceDirs Include="$([MSBuild]::Unescape('$(_WslcSourceDir)').Split(';'))" Condition="'$(_WslcSourceDir)' != ''" />
+ <_WslcSourceFiles Include="$(_WslcDockerfile)" />
+ <_WslcSourceFiles Include="%(_WslcSourceDirs.Identity)\**\*" Condition="'%(_WslcSourceDirs.Identity)' != ''" />
+ <_WslcSourceFiles Include="$(_WslcContext)\**\*" Condition="'$(_WslcSourceDir)' == '' AND '$(_WslcContext)' != ''" />
+
+
+
+
+
+
+ <_WslcRawRef Condition="'$(_WslcImage)' != ''">$(_WslcImage)
+ <_WslcRawRef Condition="'$(_WslcRawRef)' == ''">$(_WslcName)
+ <_WslcLastSlash>$([System.String]::Copy('$(_WslcRawRef)').LastIndexOf('/'))
+ <_WslcLastColon>$([System.String]::Copy('$(_WslcRawRef)').LastIndexOf(':'))
+ <_WslcFullRef Condition="$(_WslcLastColon) > $(_WslcLastSlash)">$(_WslcRawRef)
+ <_WslcFullRef Condition="'$(_WslcFullRef)' == ''">$(_WslcRawRef):latest
+
+
+
+
+
+
+
+ <_WslcTarPath Condition="'$(_WslcTarLocation)' != ''">$(_WslcTarLocation)
+ <_WslcTarPath Condition="'$(_WslcTarPath)' == ''">$(_WslcOutDir)$(_WslcName).tar
+
+ <_WslcTarDir>$([System.IO.Path]::GetDirectoryName('$(_WslcTarPath)'))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nuget/Microsoft.WSL.Containers/build/native/Microsoft.WSL.Containers.targets b/nuget/Microsoft.WSL.Containers/build/native/Microsoft.WSL.Containers.targets
index abd559faa..05553336d 100644
--- a/nuget/Microsoft.WSL.Containers/build/native/Microsoft.WSL.Containers.targets
+++ b/nuget/Microsoft.WSL.Containers/build/native/Microsoft.WSL.Containers.targets
@@ -31,4 +31,64 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_WslcTlogToolPath>$(WslcCliPath)
+ <_WslcTarFullPath>$([System.IO.Path]::GetFullPath('$(_WslcTarPath)'))
+
+
+
+ <_WslcTlogReadLines Include="^$(_WslcTlogToolPath)" />
+ <_WslcTlogReadLines Include="@(_WslcSourceFiles->'%(FullPath)')" />
+ <_WslcTlogWriteLines Include="^$(_WslcTlogToolPath)" />
+ <_WslcTlogWriteLines Include="$(_WslcTarFullPath)" />
+
+
+
+
+
+
+
+
+
+
+ <_WslcTlogDir>$([MSBuild]::EnsureTrailingSlash('$(TLogLocation)'))
+
+
+
+
+
+
diff --git a/nuget/Microsoft.WSL.Containers/build/net/Microsoft.WSL.Containers.targets b/nuget/Microsoft.WSL.Containers/build/net/Microsoft.WSL.Containers.targets
index ecc7f902f..d6f501e17 100644
--- a/nuget/Microsoft.WSL.Containers/build/net/Microsoft.WSL.Containers.targets
+++ b/nuget/Microsoft.WSL.Containers/build/net/Microsoft.WSL.Containers.targets
@@ -28,4 +28,29 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_WslcUpToDateDirs Include="@(WslcImage->'%(Sources)')" Condition="'%(WslcImage.Sources)' != ''" />
+
+
+
+
+
+
diff --git a/nuget/Microsoft.WSL.Containers/cmake/Microsoft.WSL.ContainersConfig.cmake b/nuget/Microsoft.WSL.Containers/cmake/Microsoft.WSL.ContainersConfig.cmake
index 4829bed47..090758fb0 100644
--- a/nuget/Microsoft.WSL.Containers/cmake/Microsoft.WSL.ContainersConfig.cmake
+++ b/nuget/Microsoft.WSL.Containers/cmake/Microsoft.WSL.ContainersConfig.cmake
@@ -55,3 +55,143 @@ unset(_wslcsdk_arch)
unset(_wslcsdk_root)
unset(_wslcsdk_include_dir)
unset(_wslcsdk_lib_dir)
+
+# ============================================================================
+# Container Image Build Targets
+# ============================================================================
+#
+# Provides the wslc_add_image() function for declaring container image
+# build targets with incremental rebuild support.
+#
+# Usage:
+# find_package(Microsoft.WSL.Containers REQUIRED)
+#
+# wslc_add_image(my-server
+# IMAGE ghcr.io/myorg/my-server:latest
+# DOCKERFILE container/Dockerfile
+# CONTEXT container/
+# SOURCES container/src/*.cpp container/src/*.h
+# TAR_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/my-server.tar
+# )
+#
+# add_dependencies(my_app my-server)
+#
+# The first positional argument is the CMake target name.
+# IMAGE is the container image reference (required); may include a tag
+# (e.g. 'my-server:v1'). ':latest' is appended automatically when omitted.
+# TAR_LOCATION is the output path for the saved image tarball
+# (optional; defaults to ${CMAKE_CURRENT_BINARY_DIR}/.tar).
+# Pass PRUNE_AFTER_BUILD to also run 'wslc image prune' after save.
+
+function(wslc_add_image _target_name)
+ cmake_parse_arguments(
+ PARSE_ARGV 1 ARG
+ "PRUNE_AFTER_BUILD" # options (boolean flags)
+ "IMAGE;DOCKERFILE;CONTEXT;TAR_LOCATION" # one-value keywords
+ "SOURCES" # multi-value keywords
+ )
+
+ # Reject typos / unknown keywords so they can't silently slip through.
+ if(ARG_UNPARSED_ARGUMENTS)
+ message(FATAL_ERROR "wslc_add_image: unknown argument(s): ${ARG_UNPARSED_ARGUMENTS}")
+ endif()
+
+ # Validate required arguments
+ if(NOT ARG_IMAGE)
+ message(FATAL_ERROR "wslc_add_image: IMAGE 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()
+
+ # Append :latest when IMAGE has no tag. Detect by looking for ':' after the
+ # last '/', so registry-port refs like localhost:5000/repo aren't misread.
+ string(FIND "${ARG_IMAGE}" "/" _last_slash_pos REVERSE)
+ string(FIND "${ARG_IMAGE}" ":" _last_colon_pos REVERSE)
+ if(_last_colon_pos GREATER _last_slash_pos)
+ set(_image_ref "${ARG_IMAGE}")
+ else()
+ set(_image_ref "${ARG_IMAGE}:latest")
+ endif()
+
+ # Defaults
+ if(NOT ARG_TAR_LOCATION)
+ set(ARG_TAR_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/${_target_name}.tar")
+ endif()
+ # Normalize TAR_LOCATION to an absolute path. A bare filename or relative
+ # path would leave _tar_dir empty below and break `make_directory ""`.
+ # Skip the normalization when the path contains a generator expression
+ # (e.g. $) — those resolve to absolute paths at
+ # build time and would otherwise get BASE_DIR prepended at configure
+ # time, producing a doubled path like build/$<...>/foo.tar.
+ if(NOT ARG_TAR_LOCATION MATCHES "\\$<")
+ get_filename_component(ARG_TAR_LOCATION "${ARG_TAR_LOCATION}" ABSOLUTE
+ BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}")
+ endif()
+
+ # Find wslc CLI on PATH (the WSL MSI puts it there).
+ if(NOT WSLC_CLI_PATH)
+ find_program(WSLC_CLI_PATH wslc)
+ if(NOT WSLC_CLI_PATH)
+ message(FATAL_ERROR "wslc CLI not found on PATH. Install WSL by running 'wsl --install --no-distribution', or set the WSLC_CLI_PATH variable to a specific wslc.exe path.")
+ endif()
+ endif()
+
+ # Validate target name (used as CMake target id and default tar filename).
+ string(REGEX MATCH "[^a-zA-Z0-9_.+-]" _bad_char "${_target_name}")
+ if(_bad_char)
+ message(FATAL_ERROR "wslc_add_image: '${_target_name}' contains unsupported character '${_bad_char}'. The target name is used as a CMake target identifier and as the default tar filename, so it must be limited to letters, digits, '_', '.', '+', and '-'.")
+ endif()
+
+ # Normalize paths to be independent of the build directory
+ get_filename_component(_dockerfile_path "${ARG_DOCKERFILE}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+ get_filename_component(_context_path "${ARG_CONTEXT}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+
+ # Resolve source globs to file lists; default to CONTEXT contents if SOURCES omitted
+ if(ARG_SOURCES)
+ file(GLOB_RECURSE _resolved_sources CONFIGURE_DEPENDS ${ARG_SOURCES})
+ else()
+ file(GLOB_RECURSE _resolved_sources CONFIGURE_DEPENDS "${_context_path}/*")
+ endif()
+
+ get_filename_component(_tar_dir "${ARG_TAR_LOCATION}" DIRECTORY)
+
+ # Prune failure is swallowed by default (housekeeping); set
+ # WSLC_TREAT_PRUNE_FAILURE_AS_ERROR=ON to fail the build on prune failure.
+ set(_prune_command "")
+ set(_prune_comment "")
+ if(ARG_PRUNE_AFTER_BUILD)
+ if(WSLC_TREAT_PRUNE_FAILURE_AS_ERROR)
+ set(_prune_command COMMAND "${WSLC_CLI_PATH}" image prune)
+ else()
+ set(_prune_wrapper "${CMAKE_CURRENT_BINARY_DIR}/wslc_prune_ignore_failure.cmake")
+ if(NOT EXISTS "${_prune_wrapper}")
+ file(WRITE "${_prune_wrapper}"
+ "execute_process(COMMAND \"\${WSLC}\" image prune)\n")
+ endif()
+ set(_prune_command COMMAND "${CMAKE_COMMAND}" "-DWSLC=${WSLC_CLI_PATH}" -P "${_prune_wrapper}")
+ endif()
+ set(_prune_comment ", and pruning dangling images")
+ endif()
+
+ # Save to a .tmp and atomically rename on success — wslc image save uses
+ # CREATE_ALWAYS, which truncates the destination on entry, so a failed
+ # save would otherwise leave a partial tar with newer mtime than sources
+ # (and break incremental). The rename only happens if save succeeded.
+ add_custom_command(
+ OUTPUT "${ARG_TAR_LOCATION}"
+ COMMAND ${CMAKE_COMMAND} -E make_directory "${_tar_dir}"
+ COMMAND "${WSLC_CLI_PATH}" image build -t "${_image_ref}" -f "${_dockerfile_path}" "${_context_path}"
+ COMMAND "${WSLC_CLI_PATH}" image save -o "${ARG_TAR_LOCATION}.tmp" "${_image_ref}"
+ COMMAND ${CMAKE_COMMAND} -E rename "${ARG_TAR_LOCATION}.tmp" "${ARG_TAR_LOCATION}"
+ ${_prune_command}
+ DEPENDS ${_resolved_sources} "${_dockerfile_path}"
+ COMMENT "WSLC: Building image '${_image_ref}', saving to '${ARG_TAR_LOCATION}'${_prune_comment}..."
+ VERBATIM
+ )
+
+ add_custom_target(${_target_name} DEPENDS "${ARG_TAR_LOCATION}")
+endfunction()