| 1 | <?xml version="1.0" encoding="utf-8"?> |
| 2 | <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
| 3 | <!-- ================================================================== --> |
| 4 | <!-- Container Image Build Targets --> |
| 5 | <!-- ================================================================== --> |
| 6 | |
| 7 | <!-- Default properties --> |
| 8 | <PropertyGroup> |
| 9 | <!-- Default to bare 'wslc' so cmd.exe resolves it via PATH (the WSL MSI puts wslc.exe there). |
| 10 | Override with WslcCliPath property if needed. --> |
| 11 | <WslcCliPath Condition="'$(WslcCliPath)' == ''">wslc</WslcCliPath> |
| 12 | <WslcImageBuildPull Condition="'$(WslcImageBuildPull)' == ''">false</WslcImageBuildPull> |
| 13 | <WslcImageBuildNoCache Condition="'$(WslcImageBuildNoCache)' == ''">false</WslcImageBuildNoCache> |
| 14 | <!-- Unified intermediate directory: $(IntDir) for C++, $(IntermediateOutputPath) for .NET --> |
| 15 | <_WslcIntDir Condition="'$(IntDir)' != ''">$(IntDir)</_WslcIntDir> |
| 16 | <_WslcIntDir Condition="'$(_WslcIntDir)' == '' AND '$(IntermediateOutputPath)' != ''">$(IntermediateOutputPath)</_WslcIntDir> |
| 17 | <_WslcIntDir Condition="'$(_WslcIntDir)' == ''">obj\</_WslcIntDir> |
| 18 | </PropertyGroup> |
| 19 | |
| 20 | <!-- Default values for optional WslcImage metadata. Required metadata |
| 21 | (Dockerfile, Context) is intentionally not defaulted here so |
| 22 | _WslcValidateItems reports it as missing. --> |
| 23 | <ItemDefinitionGroup> |
| 24 | <WslcImage> |
| 25 | <Image></Image> |
| 26 | <TarLocation></TarLocation> |
| 27 | <BuildArgs></BuildArgs> |
| 28 | <Labels></Labels> |
| 29 | </WslcImage> |
| 30 | </ItemDefinitionGroup> |
| 31 | |
| 32 | <!-- Check that wslc CLI is installed (invoked via DependsOnTargets from WslcBuildAllImages) --> |
| 33 | <Target Name="WslcCheckDependencies"> |
| 34 | |
| 35 | <Exec Command=""$(WslcCliPath)" --version" |
| 36 | IgnoreExitCode="true" |
| 37 | ConsoleToMSBuild="true" |
| 38 | StandardOutputImportance="low" |
| 39 | StandardErrorImportance="low"> |
| 40 | <Output TaskParameter="ExitCode" PropertyName="_WslcExitCode" /> |
| 41 | </Exec> |
| 42 | |
| 43 | <Error Condition="'$(_WslcExitCode)' != '0'" |
| 44 | Code="WSLC0001" |
| 45 | Text="The wslc CLI check failed: '$(WslcCliPath) --version' returned exit code $(_WslcExitCode). Install WSL by running 'wsl --install --no-distribution', or set the WslcCliPath property to a specific wslc.exe path." /> |
| 46 | </Target> |
| 47 | |
| 48 | <!-- Validate required metadata on WslcImage items (invoked via DependsOnTargets from WslcBuildAllImages) --> |
| 49 | <Target Name="_WslcValidateItems"> |
| 50 | |
| 51 | <Error Condition="'%(WslcImage.Dockerfile)' == ''" |
| 52 | Code="WSLC0002" |
| 53 | Text="WslcImage '%(WslcImage.Identity)' is missing required Dockerfile metadata." /> |
| 54 | |
| 55 | <Error Condition="'%(WslcImage.Context)' == ''" |
| 56 | Code="WSLC0003" |
| 57 | Text="WslcImage '%(WslcImage.Identity)' is missing required Context metadata." /> |
| 58 | |
| 59 | <!-- Identity is used as default tar filename and in .tlog filenames; reject filename-invalid chars. --> |
| 60 | <Error Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(WslcImage.Identity)', '[/\\:*?"<>|]'))" |
| 61 | Code="WSLC0004" |
| 62 | Text="WslcImage with Include='%(WslcImage.Identity)' contains a character invalid for filesystem names (one of / \ : * ? " < > |). The Identity is used as the default tar filename and in .tlog filenames. Use a simple identity in Include (e.g. Include="my-image") and put the registry/org/tag in the Image metadata." /> |
| 63 | </Target> |
| 64 | |
| 65 | <!-- Dispatch each WslcImage via MSBuild task instead of batching directly because: |
| 66 | 1. Sources may contain semicolons (multi-directory) which conflict with Properties syntax |
| 67 | 2. Wildcard expansion in Inputs requires an intermediate ItemGroup (_WslcCollectSources) |
| 68 | that must run per-image in a separate evaluation context |
| 69 | |
| 70 | Build + save run together per image, and StopOnFirstFailure halts the |
| 71 | batch on the first failure — so later images aren't attempted before |
| 72 | earlier ones are finished. Within an image, save uses .tmp+Move so a |
| 73 | half-written tar is never visible (a build-success/save-failure does |
| 74 | still leave the image in the daemon without a tar on disk). --> |
| 75 | <Target Name="WslcBuildAllImages" |
| 76 | AfterTargets="Build" |
| 77 | DependsOnTargets="WslcCheckDependencies;_WslcValidateItems" |
| 78 | Condition="'@(WslcImage)' != '' AND '$(DesignTimeBuild)' != 'true' AND '$(IsCrossTargetingBuild)' != 'true'"> |
| 79 | |
| 80 | <MSBuild Projects="$(MSBuildProjectFullPath)" |
| 81 | Targets="_WslcBuildAndSaveSingleImage" |
| 82 | StopOnFirstFailure="true" |
| 83 | Properties="Configuration=$(Configuration);Platform=$(Platform);_WslcName=%(WslcImage.Identity);_WslcImage=%(WslcImage.Image);_WslcDockerfile=%(WslcImage.Dockerfile);_WslcContext=%(WslcImage.Context);_WslcSourceDir=$([MSBuild]::Escape('%(WslcImage.Sources)'));_WslcIntDir=$(_WslcIntDir);_WslcTarLocation=%(WslcImage.TarLocation);_WslcOutDir=$(OutDir);_WslcBuildArgs=$([MSBuild]::Escape('%(WslcImage.BuildArgs)'));_WslcLabels=$([MSBuild]::Escape('%(WslcImage.Labels)'));_WslcPull=$(WslcImageBuildPull);_WslcNoCache=$(WslcImageBuildNoCache)" /> |
| 84 | </Target> |
| 85 | |
| 86 | <!-- |
| 87 | Collect source files and the effective build options for incremental checks. |
| 88 | Wildcards are expanded here in ItemGroup Include (not in Inputs attribute or MSBuild task Properties, |
| 89 | where they get escaped). Sources can be semicolon separated directory paths; |
| 90 | each is expanded with **\* to collect all files recursively. |
| 91 | If Sources is omitted, fall back to tracking the full Context directory so changes to |
| 92 | .dockerignore and other Docker build inputs still invalidate the marker. |
| 93 | --> |
| 94 | <Target Name="_WslcCollectSources"> |
| 95 | <ItemGroup> |
| 96 | <_WslcSourceDirs Include="$([MSBuild]::Unescape('$(_WslcSourceDir)').Split(';'))" Condition="'$(_WslcSourceDir)' != ''" /> |
| 97 | <_WslcSourceFiles Include="$(_WslcDockerfile)" /> |
| 98 | <_WslcSourceFiles Include="%(_WslcSourceDirs.Identity)\**\*" Condition="'%(_WslcSourceDirs.Identity)' != ''" /> |
| 99 | <_WslcSourceFiles Include="$(_WslcContext)\**\*" Condition="'$(_WslcSourceDir)' == '' AND '$(_WslcContext)' != ''" /> |
| 100 | <_WslcBuildArgItems Include="$([MSBuild]::Unescape('$(_WslcBuildArgs)').Split(';'))" Condition="'$(_WslcBuildArgs)' != ''" /> |
| 101 | <_WslcLabelItems Include="$([MSBuild]::Unescape('$(_WslcLabels)').Split(';'))" Condition="'$(_WslcLabels)' != ''" /> |
| 102 | </ItemGroup> |
| 103 | </Target> |
| 104 | |
| 105 | <Target Name="_WslcWriteBuildSignature" |
| 106 | DependsOnTargets="_WslcResolveImageRef;_WslcCollectSources"> |
| 107 | <PropertyGroup> |
| 108 | <_WslcBuildSignaturePath>$(_WslcIntDir)wslc.$(_WslcName).options</_WslcBuildSignaturePath> |
| 109 | </PropertyGroup> |
| 110 | |
| 111 | <MakeDir Directories="$(_WslcIntDir)" /> |
| 112 | |
| 113 | <ItemGroup> |
| 114 | <_WslcBuildSignature Include="Image=$(_WslcFullRef)" /> |
| 115 | <_WslcBuildSignature Include="Dockerfile=$(_WslcDockerfile)" /> |
| 116 | <_WslcBuildSignature Include="Context=$(_WslcContext)" /> |
| 117 | <_WslcBuildSignature Include="Pull=$(_WslcPull)" /> |
| 118 | <_WslcBuildSignature Include="NoCache=$(_WslcNoCache)" /> |
| 119 | <_WslcBuildSignature Include="BuildArg=%(_WslcBuildArgItems.Identity)" /> |
| 120 | <_WslcBuildSignature Include="Label=%(_WslcLabelItems.Identity)" /> |
| 121 | </ItemGroup> |
| 122 | |
| 123 | <WriteLinesToFile File="$(_WslcBuildSignaturePath)" |
| 124 | Lines="@(_WslcBuildSignature)" |
| 125 | Overwrite="true" |
| 126 | WriteOnlyWhenDifferent="true" /> |
| 127 | |
| 128 | <ItemGroup> |
| 129 | <_WslcSourceFiles Include="$(_WslcBuildSignaturePath)" /> |
| 130 | </ItemGroup> |
| 131 | </Target> |
| 132 | |
| 133 | <!-- Resolve _WslcFullRef. Falls back to Identity when Image unset; appends |
| 134 | ':latest' when no tag is present (':' after the last '/'). --> |
| 135 | <Target Name="_WslcResolveImageRef"> |
| 136 | <PropertyGroup> |
| 137 | <_WslcRawRef Condition="'$(_WslcImage)' != ''">$(_WslcImage)</_WslcRawRef> |
| 138 | <_WslcRawRef Condition="'$(_WslcRawRef)' == ''">$(_WslcName)</_WslcRawRef> |
| 139 | <_WslcLastSlash>$([System.String]::Copy('$(_WslcRawRef)').LastIndexOf('/'))</_WslcLastSlash> |
| 140 | <_WslcLastColon>$([System.String]::Copy('$(_WslcRawRef)').LastIndexOf(':'))</_WslcLastColon> |
| 141 | <_WslcFullRef Condition="$(_WslcLastColon) > $(_WslcLastSlash)">$(_WslcRawRef)</_WslcFullRef> |
| 142 | <_WslcFullRef Condition="'$(_WslcFullRef)' == ''">$(_WslcRawRef):latest</_WslcFullRef> |
| 143 | </PropertyGroup> |
| 144 | </Target> |
| 145 | |
| 146 | <!-- Resolve tar path into a property so _WslcBuildAndSaveSingleImage can use it in Inputs/Outputs. --> |
| 147 | <Target Name="_WslcResolveSavePath" DependsOnTargets="_WslcResolveImageRef"> |
| 148 | <PropertyGroup> |
| 149 | <!-- Resolve tar path: user-provided TarLocation, else $(OutDir)<name>.tar --> |
| 150 | <_WslcTarPath Condition="'$(_WslcTarLocation)' != ''">$(_WslcTarLocation)</_WslcTarPath> |
| 151 | <_WslcTarPath Condition="'$(_WslcTarPath)' == ''">$(_WslcOutDir)$(_WslcName).tar</_WslcTarPath> |
| 152 | |
| 153 | <_WslcTarDir>$([System.IO.Path]::GetDirectoryName('$(_WslcTarPath)'))</_WslcTarDir> |
| 154 | </PropertyGroup> |
| 155 | </Target> |
| 156 | |
| 157 | <!-- Build + save one image atomically (per-image, so a mid-list failure |
| 158 | leaves a coherent state). Save writes <tar>.tmp and Moves on success. --> |
| 159 | <Target Name="_WslcBuildAndSaveSingleImage" |
| 160 | DependsOnTargets="_WslcWriteBuildSignature;_WslcResolveSavePath" |
| 161 | Inputs="@(_WslcSourceFiles)" |
| 162 | Outputs="$(_WslcTarPath)"> |
| 163 | |
| 164 | <MakeDir Directories="$(_WslcTarDir)" Condition="'$(_WslcTarDir)' != ''" /> |
| 165 | |
| 166 | <PropertyGroup> |
| 167 | <_WslcBuildOptions>-t "$(_WslcFullRef)"</_WslcBuildOptions> |
| 168 | <_WslcBuildOptions Condition="'$(_WslcNoCache)' == 'true'">$(_WslcBuildOptions) --no-cache</_WslcBuildOptions> |
| 169 | <_WslcBuildOptions Condition="'$(_WslcPull)' == 'true'">$(_WslcBuildOptions) --pull</_WslcBuildOptions> |
| 170 | <_WslcBuildOptions Condition="'@(_WslcBuildArgItems)' != ''">$(_WslcBuildOptions) @(_WslcBuildArgItems->'--build-arg "%(Identity)"', ' ')</_WslcBuildOptions> |
| 171 | <_WslcBuildOptions Condition="'@(_WslcLabelItems)' != ''">$(_WslcBuildOptions) @(_WslcLabelItems->'--label "%(Identity)"', ' ')</_WslcBuildOptions> |
| 172 | <_WslcBuildOptions>$(_WslcBuildOptions) -f "$(_WslcDockerfile)"</_WslcBuildOptions> |
| 173 | </PropertyGroup> |
| 174 | |
| 175 | <Message Importance="high" |
| 176 | Text="WSLC: Building image '$(_WslcFullRef)'..." /> |
| 177 | |
| 178 | <Exec Command=""$(WslcCliPath)" image build $(_WslcBuildOptions) "$(_WslcContext)"" |
| 179 | ConsoleToMSBuild="true" /> |
| 180 | |
| 181 | <Message Importance="high" |
| 182 | Text="WSLC: Saving image '$(_WslcFullRef)' to '$(_WslcTarPath)'..." /> |
| 183 | |
| 184 | <Exec Command=""$(WslcCliPath)" image save -o "$(_WslcTarPath).tmp" "$(_WslcFullRef)"" |
| 185 | ConsoleToMSBuild="true" /> |
| 186 | |
| 187 | <Move SourceFiles="$(_WslcTarPath).tmp" DestinationFiles="$(_WslcTarPath)" /> |
| 188 | |
| 189 | <!-- Opt-in: WslcPruneAfterBuild=true. Prune failure is a Warning by default; |
| 190 | set WslcTreatPruneFailureAsError=true to fail the build instead. --> |
| 191 | <Message Importance="high" |
| 192 | Text="WSLC: Pruning dangling images..." |
| 193 | Condition="'$(WslcPruneAfterBuild)' == 'true'" /> |
| 194 | |
| 195 | <Exec Command=""$(WslcCliPath)" image prune" |
| 196 | ConsoleToMSBuild="true" |
| 197 | IgnoreExitCode="true" |
| 198 | Condition="'$(WslcPruneAfterBuild)' == 'true'"> |
| 199 | <Output TaskParameter="ExitCode" PropertyName="_WslcPruneExitCode" /> |
| 200 | </Exec> |
| 201 | |
| 202 | <Error Condition="'$(WslcPruneAfterBuild)' == 'true' AND '$(WslcTreatPruneFailureAsError)' == 'true' AND '$(_WslcPruneExitCode)' != '0'" |
| 203 | Code="WSLC0007" |
| 204 | Text="wslc image prune failed (exit code $(_WslcPruneExitCode)). The image and tar were saved successfully; only the post-build prune failed." /> |
| 205 | |
| 206 | <Warning Condition="'$(WslcPruneAfterBuild)' == 'true' AND '$(WslcTreatPruneFailureAsError)' != 'true' AND '$(_WslcPruneExitCode)' != '0'" |
| 207 | Code="WSLC0007" |
| 208 | Text="wslc image prune failed (exit code $(_WslcPruneExitCode)). The image and tar were saved successfully; only the post-build prune was skipped. Set WslcTreatPruneFailureAsError=true to fail the build instead." /> |
| 209 | </Target> |
| 210 | |
| 211 | <!-- Clean container artifacts. ContinueOnError so a transient file lock |
| 212 | (AV scanner, downstream consumer) doesn't block the rest of clean. |
| 213 | .tmp variants are removed too — a failed save can leave one behind. --> |
| 214 | <Target Name="WslcClean" |
| 215 | AfterTargets="Clean" |
| 216 | Condition="'@(WslcImage)' != ''"> |
| 217 | |
| 218 | <Delete Files="%(WslcImage.TarLocation)" |
| 219 | Condition="'%(WslcImage.TarLocation)' != ''" |
| 220 | ContinueOnError="WarnAndContinue" /> |
| 221 | |
| 222 | <Delete Files="%(WslcImage.TarLocation).tmp" |
| 223 | Condition="'%(WslcImage.TarLocation)' != ''" |
| 224 | ContinueOnError="WarnAndContinue" /> |
| 225 | |
| 226 | <Delete Files="$(OutDir)%(WslcImage.Identity).tar" |
| 227 | Condition="'%(WslcImage.TarLocation)' == ''" |
| 228 | ContinueOnError="WarnAndContinue" /> |
| 229 | |
| 230 | <Delete Files="$(OutDir)%(WslcImage.Identity).tar.tmp" |
| 231 | Condition="'%(WslcImage.TarLocation)' == ''" |
| 232 | ContinueOnError="WarnAndContinue" /> |
| 233 | </Target> |
| 234 | |
| 235 | </Project> |