|
| 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 | + <!-- Unified intermediate directory: $(IntDir) for C++, $(IntermediateOutputPath) for .NET --> |
| 13 | + <_WslcIntDir Condition="'$(IntDir)' != ''">$(IntDir)</_WslcIntDir> |
| 14 | + <_WslcIntDir Condition="'$(_WslcIntDir)' == '' AND '$(IntermediateOutputPath)' != ''">$(IntermediateOutputPath)</_WslcIntDir> |
| 15 | + <_WslcIntDir Condition="'$(_WslcIntDir)' == ''">obj\</_WslcIntDir> |
| 16 | + </PropertyGroup> |
| 17 | + |
| 18 | + <!-- Default metadata for WslcImage items. Image is the full ref; |
| 19 | + ':latest' is appended automatically when no tag is given. --> |
| 20 | + <ItemDefinitionGroup> |
| 21 | + <WslcImage> |
| 22 | + <Image></Image> |
| 23 | + <TarLocation></TarLocation> |
| 24 | + </WslcImage> |
| 25 | + </ItemDefinitionGroup> |
| 26 | + |
| 27 | + <!-- Check that wslc CLI is installed (invoked via DependsOnTargets from WslcBuildAllImages) --> |
| 28 | + <Target Name="WslcCheckDependencies"> |
| 29 | + |
| 30 | + <Exec Command=""$(WslcCliPath)" --version" |
| 31 | + IgnoreExitCode="true" |
| 32 | + ConsoleToMSBuild="true" |
| 33 | + StandardOutputImportance="low" |
| 34 | + StandardErrorImportance="low"> |
| 35 | + <Output TaskParameter="ExitCode" PropertyName="_WslcExitCode" /> |
| 36 | + </Exec> |
| 37 | + |
| 38 | + <Error Condition="'$(_WslcExitCode)' != '0'" |
| 39 | + Code="WSLC0001" |
| 40 | + 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." /> |
| 41 | + </Target> |
| 42 | + |
| 43 | + <!-- Validate required metadata on WslcImage items (invoked via DependsOnTargets from WslcBuildAllImages) --> |
| 44 | + <Target Name="_WslcValidateItems"> |
| 45 | + |
| 46 | + <Error Condition="'%(WslcImage.Dockerfile)' == ''" |
| 47 | + Code="WSLC0002" |
| 48 | + Text="WslcImage '%(WslcImage.Identity)' is missing required Dockerfile metadata." /> |
| 49 | + |
| 50 | + <Error Condition="'%(WslcImage.Context)' == ''" |
| 51 | + Code="WSLC0003" |
| 52 | + Text="WslcImage '%(WslcImage.Identity)' is missing required Context metadata." /> |
| 53 | + |
| 54 | + <!-- Identity is used as default tar filename and in .tlog filenames; reject filename-invalid chars. --> |
| 55 | + <Error Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(WslcImage.Identity)', '[/\\:*?"<>|]'))" |
| 56 | + Code="WSLC0004" |
| 57 | + 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." /> |
| 58 | + </Target> |
| 59 | + |
| 60 | + <!-- Dispatch each WslcImage via MSBuild task instead of batching directly because: |
| 61 | + 1. Sources may contain semicolons (multi-directory) which conflict with Properties syntax |
| 62 | + 2. Wildcard expansion in Inputs requires an intermediate ItemGroup (_WslcCollectSources) |
| 63 | + that must run per-image in a separate evaluation context |
| 64 | +
|
| 65 | + Build + save run together per image, and StopOnFirstFailure halts the |
| 66 | + batch on the first failure — so later images aren't attempted before |
| 67 | + earlier ones are finished. Within an image, save uses .tmp+Move so a |
| 68 | + half-written tar is never visible (a build-success/save-failure does |
| 69 | + still leave the image in the daemon without a tar on disk). --> |
| 70 | + <Target Name="WslcBuildAllImages" |
| 71 | + AfterTargets="Build" |
| 72 | + DependsOnTargets="WslcCheckDependencies;_WslcValidateItems" |
| 73 | + Condition="'@(WslcImage)' != '' AND '$(DesignTimeBuild)' != 'true' AND '$(IsCrossTargetingBuild)' != 'true'"> |
| 74 | + |
| 75 | + <MSBuild Projects="$(MSBuildProjectFullPath)" |
| 76 | + Targets="_WslcBuildAndSaveSingleImage" |
| 77 | + StopOnFirstFailure="true" |
| 78 | + 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)" /> |
| 79 | + </Target> |
| 80 | + |
| 81 | + <!-- |
| 82 | + Collect source files for incremental check. |
| 83 | + Wildcards are expanded here in ItemGroup Include (not in Inputs attribute or MSBuild task Properties, |
| 84 | + where they get escaped). Sources can be semicolon separated directory paths; |
| 85 | + each is expanded with **\* to collect all files recursively. |
| 86 | + If Sources is omitted, fall back to tracking the full Context directory so changes to |
| 87 | + .dockerignore and other Docker build inputs still invalidate the marker. |
| 88 | + --> |
| 89 | + <Target Name="_WslcCollectSources"> |
| 90 | + <ItemGroup> |
| 91 | + <_WslcSourceDirs Include="$([MSBuild]::Unescape('$(_WslcSourceDir)').Split(';'))" Condition="'$(_WslcSourceDir)' != ''" /> |
| 92 | + <_WslcSourceFiles Include="$(_WslcDockerfile)" /> |
| 93 | + <_WslcSourceFiles Include="%(_WslcSourceDirs.Identity)\**\*" Condition="'%(_WslcSourceDirs.Identity)' != ''" /> |
| 94 | + <_WslcSourceFiles Include="$(_WslcContext)\**\*" Condition="'$(_WslcSourceDir)' == '' AND '$(_WslcContext)' != ''" /> |
| 95 | + </ItemGroup> |
| 96 | + </Target> |
| 97 | + |
| 98 | + <!-- Resolve _WslcFullRef. Falls back to Identity when Image unset; appends |
| 99 | + ':latest' when no tag is present (':' after the last '/'). --> |
| 100 | + <Target Name="_WslcResolveImageRef"> |
| 101 | + <PropertyGroup> |
| 102 | + <_WslcRawRef Condition="'$(_WslcImage)' != ''">$(_WslcImage)</_WslcRawRef> |
| 103 | + <_WslcRawRef Condition="'$(_WslcRawRef)' == ''">$(_WslcName)</_WslcRawRef> |
| 104 | + <_WslcLastSlash>$([System.String]::Copy('$(_WslcRawRef)').LastIndexOf('/'))</_WslcLastSlash> |
| 105 | + <_WslcLastColon>$([System.String]::Copy('$(_WslcRawRef)').LastIndexOf(':'))</_WslcLastColon> |
| 106 | + <_WslcFullRef Condition="$(_WslcLastColon) > $(_WslcLastSlash)">$(_WslcRawRef)</_WslcFullRef> |
| 107 | + <_WslcFullRef Condition="'$(_WslcFullRef)' == ''">$(_WslcRawRef):latest</_WslcFullRef> |
| 108 | + </PropertyGroup> |
| 109 | + </Target> |
| 110 | + |
| 111 | + <!-- Resolve tar path into a property so _WslcBuildAndSaveSingleImage can use it in Inputs/Outputs. --> |
| 112 | + <Target Name="_WslcResolveSavePath" DependsOnTargets="_WslcResolveImageRef"> |
| 113 | + <PropertyGroup> |
| 114 | + <!-- Resolve tar path: user-provided TarLocation, else $(OutDir)<name>.tar --> |
| 115 | + <_WslcTarPath Condition="'$(_WslcTarLocation)' != ''">$(_WslcTarLocation)</_WslcTarPath> |
| 116 | + <_WslcTarPath Condition="'$(_WslcTarPath)' == ''">$(_WslcOutDir)$(_WslcName).tar</_WslcTarPath> |
| 117 | + |
| 118 | + <_WslcTarDir>$([System.IO.Path]::GetDirectoryName('$(_WslcTarPath)'))</_WslcTarDir> |
| 119 | + </PropertyGroup> |
| 120 | + </Target> |
| 121 | + |
| 122 | + <!-- Build + save one image atomically (per-image, so a mid-list failure |
| 123 | + leaves a coherent state). Save writes <tar>.tmp and Moves on success. --> |
| 124 | + <Target Name="_WslcBuildAndSaveSingleImage" |
| 125 | + DependsOnTargets="_WslcCollectSources;_WslcResolveImageRef;_WslcResolveSavePath" |
| 126 | + Inputs="@(_WslcSourceFiles)" |
| 127 | + Outputs="$(_WslcTarPath)"> |
| 128 | + |
| 129 | + <MakeDir Directories="$(_WslcIntDir)" /> |
| 130 | + <MakeDir Directories="$(_WslcTarDir)" Condition="'$(_WslcTarDir)' != ''" /> |
| 131 | + |
| 132 | + <Message Importance="high" |
| 133 | + Text="WSLC: Building image '$(_WslcFullRef)'..." /> |
| 134 | + |
| 135 | + <Exec Command=""$(WslcCliPath)" image build -t "$(_WslcFullRef)" -f "$(_WslcDockerfile)" "$(_WslcContext)"" |
| 136 | + ConsoleToMSBuild="true" /> |
| 137 | + |
| 138 | + <Message Importance="high" |
| 139 | + Text="WSLC: Saving image '$(_WslcFullRef)' to '$(_WslcTarPath)'..." /> |
| 140 | + |
| 141 | + <Exec Command=""$(WslcCliPath)" image save -o "$(_WslcTarPath).tmp" "$(_WslcFullRef)"" |
| 142 | + ConsoleToMSBuild="true" /> |
| 143 | + |
| 144 | + <Move SourceFiles="$(_WslcTarPath).tmp" DestinationFiles="$(_WslcTarPath)" /> |
| 145 | + |
| 146 | + <!-- Opt-in: WslcPruneAfterBuild=true. Prune failure is a Warning by default; |
| 147 | + set WslcTreatPruneFailureAsError=true to fail the build instead. --> |
| 148 | + <Message Importance="high" |
| 149 | + Text="WSLC: Pruning dangling images..." |
| 150 | + Condition="'$(WslcPruneAfterBuild)' == 'true'" /> |
| 151 | + |
| 152 | + <Exec Command=""$(WslcCliPath)" image prune" |
| 153 | + ConsoleToMSBuild="true" |
| 154 | + IgnoreExitCode="true" |
| 155 | + Condition="'$(WslcPruneAfterBuild)' == 'true'"> |
| 156 | + <Output TaskParameter="ExitCode" PropertyName="_WslcPruneExitCode" /> |
| 157 | + </Exec> |
| 158 | + |
| 159 | + <Error Condition="'$(WslcPruneAfterBuild)' == 'true' AND '$(WslcTreatPruneFailureAsError)' == 'true' AND '$(_WslcPruneExitCode)' != '0'" |
| 160 | + Code="WSLC0007" |
| 161 | + Text="wslc image prune failed (exit code $(_WslcPruneExitCode)). The image and tar were saved successfully; only the post-build prune failed." /> |
| 162 | + |
| 163 | + <Warning Condition="'$(WslcPruneAfterBuild)' == 'true' AND '$(WslcTreatPruneFailureAsError)' != 'true' AND '$(_WslcPruneExitCode)' != '0'" |
| 164 | + Code="WSLC0007" |
| 165 | + 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." /> |
| 166 | + </Target> |
| 167 | + |
| 168 | + <!-- Clean container artifacts. ContinueOnError so a transient file lock |
| 169 | + (AV scanner, downstream consumer) doesn't block the rest of clean. |
| 170 | + .tmp variants are removed too — a failed save can leave one behind. --> |
| 171 | + <Target Name="WslcClean" |
| 172 | + AfterTargets="Clean" |
| 173 | + Condition="'@(WslcImage)' != ''"> |
| 174 | + |
| 175 | + <Delete Files="%(WslcImage.TarLocation)" |
| 176 | + Condition="'%(WslcImage.TarLocation)' != ''" |
| 177 | + ContinueOnError="WarnAndContinue" /> |
| 178 | + |
| 179 | + <Delete Files="%(WslcImage.TarLocation).tmp" |
| 180 | + Condition="'%(WslcImage.TarLocation)' != ''" |
| 181 | + ContinueOnError="WarnAndContinue" /> |
| 182 | + |
| 183 | + <Delete Files="$(OutDir)%(WslcImage.Identity).tar" |
| 184 | + Condition="'%(WslcImage.TarLocation)' == ''" |
| 185 | + ContinueOnError="WarnAndContinue" /> |
| 186 | + |
| 187 | + <Delete Files="$(OutDir)%(WslcImage.Identity).tar.tmp" |
| 188 | + Condition="'%(WslcImage.TarLocation)' == ''" |
| 189 | + ContinueOnError="WarnAndContinue" /> |
| 190 | + </Target> |
| 191 | + |
| 192 | +</Project> |
0 commit comments