| 1 | if(TARGET Microsoft.WSL.Containers::SDK) |
| 2 | return() |
| 3 | endif() |
| 4 | |
| 5 | if(NOT WIN32) |
| 6 | message(FATAL_ERROR "Microsoft.WSL.Containers: This package only supports Windows.") |
| 7 | endif() |
| 8 | |
| 9 | # Determine target architecture |
| 10 | if(CMAKE_GENERATOR_PLATFORM) |
| 11 | string(TOLOWER "${CMAKE_GENERATOR_PLATFORM}" _wslcsdk_platform) |
| 12 | if(_wslcsdk_platform STREQUAL "x64") |
| 13 | set(_wslcsdk_arch "x64") |
| 14 | elseif(_wslcsdk_platform STREQUAL "arm64") |
| 15 | set(_wslcsdk_arch "arm64") |
| 16 | else() |
| 17 | message(FATAL_ERROR |
| 18 | "Microsoft.WSL.Containers: Unsupported platform '${CMAKE_GENERATOR_PLATFORM}'." |
| 19 | " Supported: x64, ARM64.") |
| 20 | endif() |
| 21 | unset(_wslcsdk_platform) |
| 22 | elseif(CMAKE_SYSTEM_PROCESSOR) |
| 23 | string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _wslcsdk_platform) |
| 24 | if(_wslcsdk_platform MATCHES "amd64|x86_64|x64") |
| 25 | set(_wslcsdk_arch "x64") |
| 26 | elseif(_wslcsdk_platform MATCHES "arm64|aarch64") |
| 27 | set(_wslcsdk_arch "arm64") |
| 28 | else() |
| 29 | message(FATAL_ERROR |
| 30 | "Microsoft.WSL.Containers: Unsupported architecture '${CMAKE_SYSTEM_PROCESSOR}'." |
| 31 | " Supported: x64, ARM64.") |
| 32 | endif() |
| 33 | unset(_wslcsdk_platform) |
| 34 | else() |
| 35 | message(FATAL_ERROR |
| 36 | "Microsoft.WSL.Containers: Could not determine target architecture." |
| 37 | " Set CMAKE_GENERATOR_PLATFORM or CMAKE_SYSTEM_PROCESSOR.") |
| 38 | endif() |
| 39 | |
| 40 | # Compute paths relative to package root (<root>/cmake/ -> <root>/) |
| 41 | get_filename_component(_wslcsdk_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) |
| 42 | set(_wslcsdk_include_dir "${_wslcsdk_root}/include") |
| 43 | set(_wslcsdk_lib_dir "${_wslcsdk_root}/runtimes/win-${_wslcsdk_arch}") |
| 44 | |
| 45 | # Create imported target |
| 46 | add_library(Microsoft.WSL.Containers::SDK SHARED IMPORTED GLOBAL) |
| 47 | set_target_properties(Microsoft.WSL.Containers::SDK PROPERTIES |
| 48 | INTERFACE_INCLUDE_DIRECTORIES "${_wslcsdk_include_dir}" |
| 49 | IMPORTED_IMPLIB "${_wslcsdk_lib_dir}/wslcsdk.lib" |
| 50 | IMPORTED_LOCATION "${_wslcsdk_lib_dir}/native/wslcsdk.dll" |
| 51 | ) |
| 52 | |
| 53 | # Clean up temporary variables |
| 54 | unset(_wslcsdk_arch) |
| 55 | unset(_wslcsdk_root) |
| 56 | unset(_wslcsdk_include_dir) |
| 57 | unset(_wslcsdk_lib_dir) |
| 58 | |
| 59 | #[[ |
| 60 | wslc_add_image(<target> |
| 61 | IMAGE <ref> DOCKERFILE <path> CONTEXT <dir> |
| 62 | [SOURCES <file>...] [TAR_LOCATION <path>] |
| 63 | [BUILD_ARGS <KEY=VALUE>...] [LABELS <KEY=VALUE>...]) |
| 64 | |
| 65 | Adds a target that builds a container image with 'wslc image build' and saves |
| 66 | it to a tarball with 'wslc image save'. The image is rebuilt when the |
| 67 | Dockerfile, a tracked source file, or an image build option changes. |
| 68 | |
| 69 | Required: |
| 70 | <target> Name of the CMake target to create (first, positional). |
| 71 | IMAGE Image reference to tag; ':latest' is appended when the |
| 72 | reference has no tag. |
| 73 | DOCKERFILE Path to the Dockerfile. |
| 74 | CONTEXT Path to the build context directory. |
| 75 | |
| 76 | Optional: |
| 77 | SOURCES Files whose changes trigger a rebuild (globs allowed). |
| 78 | Defaults to every file under CONTEXT. |
| 79 | TAR_LOCATION Output path for the saved tarball. |
| 80 | Defaults to ${CMAKE_CURRENT_BINARY_DIR}/<target>.tar. |
| 81 | BUILD_ARGS Build-time variables (KEY=VALUE), each passed as --build-arg. |
| 82 | LABELS Image labels (KEY=VALUE), each passed as --label. |
| 83 | |
| 84 | Global variables (apply to every target created by wslc_add_image): |
| 85 | WSLC_IMAGE_BUILD_PULL |
| 86 | Always attempt to pull newer base images (--pull). |
| 87 | WSLC_IMAGE_BUILD_NO_CACHE |
| 88 | Build images without the layer cache (--no-cache). |
| 89 | WSLC_PRUNE_AFTER_BUILD |
| 90 | Run 'wslc image prune' after each image is saved. |
| 91 | WSLC_TREAT_PRUNE_FAILURE_AS_ERROR |
| 92 | Fail the build when the post-build prune fails. By default a |
| 93 | prune failure is ignored. |
| 94 | |
| 95 | Example: |
| 96 | find_package(Microsoft.WSL.Containers REQUIRED) |
| 97 | |
| 98 | wslc_add_image(my-server |
| 99 | IMAGE ghcr.io/myorg/my-server:latest |
| 100 | DOCKERFILE container/Dockerfile |
| 101 | CONTEXT container/ |
| 102 | BUILD_ARGS VERSION=1.2.3 COMMIT=abcdef |
| 103 | LABELS org.opencontainers.image.source=https://example.com/repo) |
| 104 | |
| 105 | add_dependencies(my_app my-server) |
| 106 | ]] |
| 107 | |
| 108 | function(wslc_add_image _target_name) |
| 109 | cmake_parse_arguments( |
| 110 | PARSE_ARGV 1 ARG |
| 111 | "" # options (boolean flags) |
| 112 | "IMAGE;DOCKERFILE;CONTEXT;TAR_LOCATION" # one-value keywords |
| 113 | "SOURCES;BUILD_ARGS;LABELS" # multi-value keywords |
| 114 | ) |
| 115 | |
| 116 | # Reject typos / unknown keywords so they can't silently slip through. |
| 117 | if(ARG_UNPARSED_ARGUMENTS) |
| 118 | message(FATAL_ERROR "wslc_add_image: unknown argument(s): ${ARG_UNPARSED_ARGUMENTS}") |
| 119 | endif() |
| 120 | |
| 121 | # Validate required arguments |
| 122 | if(NOT ARG_IMAGE) |
| 123 | message(FATAL_ERROR "wslc_add_image: IMAGE is required") |
| 124 | endif() |
| 125 | if(NOT ARG_DOCKERFILE) |
| 126 | message(FATAL_ERROR "wslc_add_image: DOCKERFILE is required") |
| 127 | endif() |
| 128 | if(NOT ARG_CONTEXT) |
| 129 | message(FATAL_ERROR "wslc_add_image: CONTEXT is required") |
| 130 | endif() |
| 131 | |
| 132 | # Append :latest when IMAGE has no tag. Detect by looking for ':' after the |
| 133 | # last '/', so registry-port refs like localhost:5000/repo aren't misread. |
| 134 | string(FIND "${ARG_IMAGE}" "/" _last_slash_pos REVERSE) |
| 135 | string(FIND "${ARG_IMAGE}" ":" _last_colon_pos REVERSE) |
| 136 | if(_last_colon_pos GREATER _last_slash_pos) |
| 137 | set(_image_ref "${ARG_IMAGE}") |
| 138 | else() |
| 139 | set(_image_ref "${ARG_IMAGE}:latest") |
| 140 | endif() |
| 141 | |
| 142 | # Defaults |
| 143 | if(NOT ARG_TAR_LOCATION) |
| 144 | set(ARG_TAR_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/${_target_name}.tar") |
| 145 | endif() |
| 146 | # Normalize TAR_LOCATION to an absolute path. A bare filename or relative |
| 147 | # path would leave _tar_dir empty below and break `make_directory ""`. |
| 148 | # Skip the normalization when the path contains a generator expression |
| 149 | # (e.g. $<TARGET_FILE_DIR:...>) — those resolve to absolute paths at |
| 150 | # build time and would otherwise get BASE_DIR prepended at configure |
| 151 | # time, producing a doubled path like build/$<...>/foo.tar. |
| 152 | if(NOT ARG_TAR_LOCATION MATCHES "\\$<") |
| 153 | get_filename_component(ARG_TAR_LOCATION "${ARG_TAR_LOCATION}" ABSOLUTE |
| 154 | BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}") |
| 155 | endif() |
| 156 | |
| 157 | # Find wslc CLI on PATH (the WSL MSI puts it there). |
| 158 | if(NOT WSLC_CLI_PATH) |
| 159 | find_program(WSLC_CLI_PATH wslc) |
| 160 | if(NOT WSLC_CLI_PATH) |
| 161 | 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.") |
| 162 | endif() |
| 163 | endif() |
| 164 | |
| 165 | # Validate target name (used as CMake target id and default tar filename). |
| 166 | string(REGEX MATCH "[^a-zA-Z0-9_.+-]" _bad_char "${_target_name}") |
| 167 | if(_bad_char) |
| 168 | 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 '-'.") |
| 169 | endif() |
| 170 | |
| 171 | # Normalize paths to be independent of the build directory |
| 172 | get_filename_component(_dockerfile_path "${ARG_DOCKERFILE}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") |
| 173 | get_filename_component(_context_path "${ARG_CONTEXT}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") |
| 174 | |
| 175 | # Resolve source globs to file lists; default to CONTEXT contents if SOURCES omitted |
| 176 | if(ARG_SOURCES) |
| 177 | file(GLOB_RECURSE _resolved_sources CONFIGURE_DEPENDS ${ARG_SOURCES}) |
| 178 | else() |
| 179 | file(GLOB_RECURSE _resolved_sources CONFIGURE_DEPENDS "${_context_path}/*") |
| 180 | endif() |
| 181 | |
| 182 | get_filename_component(_tar_dir "${ARG_TAR_LOCATION}" DIRECTORY) |
| 183 | |
| 184 | set(_build_options "") |
| 185 | list(APPEND _build_options -t "${_image_ref}") |
| 186 | foreach(_build_arg IN LISTS ARG_BUILD_ARGS) |
| 187 | if(NOT _build_arg STREQUAL "") |
| 188 | list(APPEND _build_options --build-arg "${_build_arg}") |
| 189 | endif() |
| 190 | endforeach() |
| 191 | foreach(_label IN LISTS ARG_LABELS) |
| 192 | if(NOT _label STREQUAL "") |
| 193 | list(APPEND _build_options --label "${_label}") |
| 194 | endif() |
| 195 | endforeach() |
| 196 | if(WSLC_IMAGE_BUILD_PULL) |
| 197 | list(APPEND _build_options --pull) |
| 198 | endif() |
| 199 | if(WSLC_IMAGE_BUILD_NO_CACHE) |
| 200 | list(APPEND _build_options --no-cache) |
| 201 | endif() |
| 202 | list(APPEND _build_options -f "${_dockerfile_path}") |
| 203 | |
| 204 | # Track the effective build command as an input. file(GENERATE) preserves |
| 205 | # the timestamp when content is unchanged, so only option changes make the |
| 206 | # custom command out of date. |
| 207 | set(_build_signature_file "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${_target_name}-$<CONFIG>.wslc-options") |
| 208 | string(JOIN "\n" _build_signature ${_build_options} "${_context_path}") |
| 209 | file(GENERATE OUTPUT "${_build_signature_file}" CONTENT "${_build_signature}\n") |
| 210 | |
| 211 | set(_prune_command "") |
| 212 | set(_prune_comment "") |
| 213 | if(WSLC_PRUNE_AFTER_BUILD) |
| 214 | if(WSLC_TREAT_PRUNE_FAILURE_AS_ERROR) |
| 215 | set(_prune_command COMMAND "${WSLC_CLI_PATH}" image prune) |
| 216 | else() |
| 217 | set(_prune_wrapper "${CMAKE_CURRENT_BINARY_DIR}/wslc_prune_ignore_failure.cmake") |
| 218 | if(NOT EXISTS "${_prune_wrapper}") |
| 219 | file(WRITE "${_prune_wrapper}" |
| 220 | "execute_process(COMMAND \"\${WSLC}\" image prune)\n") |
| 221 | endif() |
| 222 | set(_prune_command COMMAND "${CMAKE_COMMAND}" "-DWSLC=${WSLC_CLI_PATH}" -P "${_prune_wrapper}") |
| 223 | endif() |
| 224 | set(_prune_comment ", and pruning dangling images") |
| 225 | endif() |
| 226 | |
| 227 | # Save to a .tmp and atomically rename on success — wslc image save uses |
| 228 | # CREATE_ALWAYS, which truncates the destination on entry, so a failed |
| 229 | # save would otherwise leave a partial tar with newer mtime than sources |
| 230 | # (and break incremental). The rename only happens if save succeeded. |
| 231 | add_custom_command( |
| 232 | OUTPUT "${ARG_TAR_LOCATION}" |
| 233 | COMMAND ${CMAKE_COMMAND} -E make_directory "${_tar_dir}" |
| 234 | COMMAND "${WSLC_CLI_PATH}" image build ${_build_options} "${_context_path}" |
| 235 | COMMAND "${WSLC_CLI_PATH}" image save -o "${ARG_TAR_LOCATION}.tmp" "${_image_ref}" |
| 236 | COMMAND ${CMAKE_COMMAND} -E rename "${ARG_TAR_LOCATION}.tmp" "${ARG_TAR_LOCATION}" |
| 237 | ${_prune_command} |
| 238 | DEPENDS ${_resolved_sources} "${_dockerfile_path}" "${_build_signature_file}" |
| 239 | COMMENT "WSLC: Building image '${_image_ref}', saving to '${ARG_TAR_LOCATION}'${_prune_comment}..." |
| 240 | VERBATIM |
| 241 | ) |
| 242 | |
| 243 | add_custom_target(${_target_name} DEPENDS "${ARG_TAR_LOCATION}") |
| 244 | endfunction() |