Report cached build steps in wslc build output (#41334)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

ggarzia-MSFT committed Aug 12, 2026 at 17:12 UTC 667d6db2e197d65e9a1407e56c46381dfcd22c82
3 files changed +37 -1
src/windows/inc/docker_schema.h
+3 -1
@@ -702,9 +702,11 @@ struct BuildKitVertex
702 std::string digest;
703 std::string name;
704 std::string started;
705 + std::string completed;
706 std::string error;
707 + bool cached{};
708
707 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(BuildKitVertex, digest, name, started, error);
709 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(BuildKitVertex, digest, name, started, completed, error, cached);
710 };
711
712 struct BuildKitStatus
src/windows/wslcsession/WSLCSession.cpp
+28
@@ -1248,6 +1248,7 @@ try
1248 std::string allOutput;
1249 std::string pendingJson;
1250 std::set<std::string> reportedSteps;
1251 + std::set<std::string> reportedCached;
1252 std::set<std::string> reportedErrors;
1253 std::map<std::string, std::string> digestToStageName;
1254 bool needsNewline = false; // true when the last log chunk didn't end with \n
@@ -1282,6 +1283,23 @@ try
1283 return {};
1284 };
1285
1286 + // Returns the leading step token from a BuildKit vertex name, e.g. "[2/3]" from "[2/3] RUN make".
1287 + // Falls back to the full name when there is no bracketed prefix.
1288 + auto getStepToken = [](const std::string& name) -> std::string {
1289 + if (name.empty() || name[0] != '[')
1290 + {
1291 + return name;
1292 + }
1293 +
1294 + auto close = name.find(']');
1295 + if (close == std::string::npos)
1296 + {
1297 + return name;
1298 + }
1299 +
1300 + return name.substr(0, close + 1);
1301 + };
1302 +
1303 auto logPrefix = [](const std::string& name) -> std::string {
1304 if (name.empty())
1305 {
@@ -1347,6 +1365,16 @@ try
1365 reportProgress(vertex.name + "\n");
1366 }
1367
1368 + if (vertex.cached && reportedCached.insert(vertex.digest).second)
1369 + {
1370 + auto stepToken = getStepToken(vertex.name);
1371 + if (!stepToken.empty())
1372 + {
1373 + flushLine();
1374 + reportProgress(stepToken + " CACHED\n");
1375 + }
1376 + }
1377 +
1378 if (!vertex.error.empty() && reportedErrors.insert(vertex.digest).second)
1379 {
1380 flushLine();
test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp
+6
@@ -1259,12 +1259,18 @@ class WSLCE2EImageBuildTests
1259 cachedBuild.Verify({.Stdout = L"", .ExitCode = 0});
1260 const auto cachedId = InspectImage(BuiltImageNoCache.NameAndTag()).Id;
1261 VERIFY_ARE_EQUAL(firstId, cachedId, L"Repeated build without --no-cache should reuse the cached layer");
1262 + VERIFY_IS_TRUE(
1263 + cachedBuild.StderrContainsSubstring(L"[2/2] CACHED"),
1264 + L"A reused layer must be reported as cached in the build output");
1265
1266 // --no-cache must re-run the non-deterministic step, producing a new id.
1267 auto noCacheBuild = RunWslc(buildCmd + L" --no-cache");
1268 noCacheBuild.Verify({.Stdout = L"", .ExitCode = 0});
1269 const auto noCacheId = InspectImage(BuiltImageNoCache.NameAndTag()).Id;
1270 VERIFY_ARE_NOT_EQUAL(firstId, noCacheId, L"--no-cache must rebuild the non-deterministic RUN step");
1271 + VERIFY_IS_FALSE(
1272 + noCacheBuild.StderrContainsSubstring(L"[2/2] CACHED"),
1273 + L"A step re-run under --no-cache must not be reported as cached");
1274 }
1275
1276 // --iidfile writes the built image's ID to the given host path on success, matching docker build