From ea673bf06f65a33a2ee958fdb3d641a339f41f05 Mon Sep 17 00:00:00 2001 From: Stas Shevchenko Date: Sat, 12 Sep 2026 08:20:06 +0200 Subject: [PATCH] [2.x] fix: include compileOrder and pipelining in compile cache key (#9749) compileOrder and usePipelining were missing from CompileInputs2, so two builds differing only in those settings shared one cache entry and replayed each other's results. Adds both to the key, with a unit test on the hash and a scripted test that fails without the change. --- main/src/main/scala/sbt/Defaults.scala | 4 ++ .../scala/sbt/internal/CompileInputs2.scala | 10 +++- .../sbt/internal/CompileInputs2HashTest.scala | 53 +++++++++++++++++++ .../cache/compile-order-key/build.sbt | 21 ++++++++ .../src/main/java/example/J.java | 7 +++ .../src/main/scala/example/S.scala | 4 ++ .../src/sbt-test/cache/compile-order-key/test | 11 ++++ 7 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 main/src/test/scala/sbt/internal/CompileInputs2HashTest.scala create mode 100644 sbt-app/src/sbt-test/cache/compile-order-key/build.sbt create mode 100644 sbt-app/src/sbt-test/cache/compile-order-key/src/main/java/example/J.java create mode 100644 sbt-app/src/sbt-test/cache/compile-order-key/src/main/scala/example/S.scala create mode 100644 sbt-app/src/sbt-test/cache/compile-order-key/test diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index b8b493a0b..55f5de7fe 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1005,6 +1005,8 @@ object Defaults extends BuildCommon with DefExtra: c.toVirtualFile(inputs.setup.cacheFile.toPath), extraIncOptions.value.toVector, scalaVersion.value, + compileOrder.value.name, + incOptions.value.pipelining, ) } ) @@ -2570,6 +2572,8 @@ object Defaults extends BuildCommon with DefExtra: c.toVirtualFile(inputs.setup.cacheFile.toPath), incrementalOptions, scalaVersion.value, + compileOrder.value.name, + incOptions.value.pipelining, ) }, bspCompileTask := diff --git a/main/src/main/scala/sbt/internal/CompileInputs2.scala b/main/src/main/scala/sbt/internal/CompileInputs2.scala index d2a0aafef..03982ce28 100644 --- a/main/src/main/scala/sbt/internal/CompileInputs2.scala +++ b/main/src/main/scala/sbt/internal/CompileInputs2.scala @@ -14,6 +14,8 @@ case class CompileInputs2( cachePath: VirtualFileRef, incrementalOptions: Vector[(String, String)], scalaVersion: String, + compileOrder: String, + pipelining: Boolean, ) object CompileInputs2: @@ -23,7 +25,7 @@ object CompileInputs2: CompileInputs2, Vector[HashedVirtualFileRef] :*: Vector[HashedVirtualFileRef] :*: Vector[String] :*: Vector[String] :*: VirtualFileRef :*: VirtualFileRef :*: Vector[(String, String)] :*: - String :*: LNil + String :*: String :*: Boolean :*: LNil ] = LList.iso( { (v: CompileInputs2) => @@ -35,12 +37,14 @@ object CompileInputs2: ("cachePath", v.cachePath) :*: ("incrementalOptions", v.incrementalOptions) :*: ("scalaVersion", v.scalaVersion) :*: + ("compileOrder", v.compileOrder) :*: + ("pipelining", v.pipelining) :*: LNil }, { (in: Vector[HashedVirtualFileRef] :*: Vector[HashedVirtualFileRef] :*: Vector[String] :*: Vector[String] :*: VirtualFileRef :*: VirtualFileRef :*: Vector[(String, String)] :*: - String :*: LNil) => + String :*: String :*: Boolean :*: LNil) => CompileInputs2( in.head, in.tail.head, @@ -50,6 +54,8 @@ object CompileInputs2: in.tail.tail.tail.tail.tail.head, in.tail.tail.tail.tail.tail.tail.head, in.tail.tail.tail.tail.tail.tail.tail.head, + in.tail.tail.tail.tail.tail.tail.tail.tail.head, + in.tail.tail.tail.tail.tail.tail.tail.tail.tail.head, ) } ) diff --git a/main/src/test/scala/sbt/internal/CompileInputs2HashTest.scala b/main/src/test/scala/sbt/internal/CompileInputs2HashTest.scala new file mode 100644 index 000000000..4fe947265 --- /dev/null +++ b/main/src/test/scala/sbt/internal/CompileInputs2HashTest.scala @@ -0,0 +1,53 @@ +/* + * sbt + * Copyright 2023, Scala center + * Copyright 2011 - 2022, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal + +import hedgehog.* +import hedgehog.runner.* +import _root_.sbt.util.CacheImplicits.given +import _root_.sbt.util.DigestHasher +import xsbti.{ HashedVirtualFileRef, VirtualFileRef } + +/** + * The compile action cache is keyed on [[CompileInputs2]], so every setting that changes the + * compile outcome has to be part of it (#9748). + */ +object CompileInputs2HashTest extends Properties: + override def tests: List[Test] = List( + example("compileOrder changes the hash", compileOrderChangesHash), + example("pipelining changes the hash", pipeliningChangesHash), + example("identical inputs hash the same", identicalInputsHashTheSame), + ) + + private def base = CompileInputs2( + classpath = Vector(HashedVirtualFileRef.of("${BASE}/lib/dep.jar", "sha256-abc", 1L)), + sources = Vector(HashedVirtualFileRef.of("${BASE}/src/main/scala/A.scala", "sha256-def", 2L)), + scalacOptions = Vector("-deprecation"), + javacOptions = Vector.empty, + outputPath = VirtualFileRef.of("${BASE}/target/classes"), + cachePath = VirtualFileRef.of("${BASE}/target/inc_compile.zip"), + incrementalOptions = Vector.empty, + scalaVersion = "2.13.18", + compileOrder = "Mixed", + pipelining = false, + ) + + private def compileOrderChangesHash: Result = + val mixed = DigestHasher.hashUnsafe(base) + val javaThenScala = DigestHasher.hashUnsafe(base.copy(compileOrder = "JavaThenScala")) + Result.assert(mixed != javaThenScala) + + private def pipeliningChangesHash: Result = + val off = DigestHasher.hashUnsafe(base) + val on = DigestHasher.hashUnsafe(base.copy(pipelining = true)) + Result.assert(off != on) + + private def identicalInputsHashTheSame: Result = + DigestHasher.hashUnsafe(base) ==== DigestHasher.hashUnsafe(base) +end CompileInputs2HashTest diff --git a/sbt-app/src/sbt-test/cache/compile-order-key/build.sbt b/sbt-app/src/sbt-test/cache/compile-order-key/build.sbt new file mode 100644 index 000000000..743c7c641 --- /dev/null +++ b/sbt-app/src/sbt-test/cache/compile-order-key/build.sbt @@ -0,0 +1,21 @@ +Global / localCacheDirectory := baseDirectory.value / "diskcache" + +val checkClasses = taskKey[Unit]("asserts the Java class was compiled") + +// A distinct project id keeps this fixture's output paths from colliding with same-named +// sibling fixtures in scripted's shared batch directory. +lazy val compileOrderKey = project + .in(file(".")) + .settings( + scalaVersion := "3.9.0", + compileOrder := ( + if ((baseDirectory.value / "mixed.marker").exists) CompileOrder.Mixed + else CompileOrder.JavaThenScala + ), + ) + +checkClasses := Def.uncached { + val dir = (compileOrderKey / Compile / classDirectory).value + val cls = dir / "example" / "J.class" + assert(cls.exists, s"$cls does not exist") +} diff --git a/sbt-app/src/sbt-test/cache/compile-order-key/src/main/java/example/J.java b/sbt-app/src/sbt-test/cache/compile-order-key/src/main/java/example/J.java new file mode 100644 index 000000000..01ba3579f --- /dev/null +++ b/sbt-app/src/sbt-test/cache/compile-order-key/src/main/java/example/J.java @@ -0,0 +1,7 @@ +package example; + +public class J { + public static String go() { + return new S().hello(); + } +} diff --git a/sbt-app/src/sbt-test/cache/compile-order-key/src/main/scala/example/S.scala b/sbt-app/src/sbt-test/cache/compile-order-key/src/main/scala/example/S.scala new file mode 100644 index 000000000..1a94dfea7 --- /dev/null +++ b/sbt-app/src/sbt-test/cache/compile-order-key/src/main/scala/example/S.scala @@ -0,0 +1,4 @@ +package example + +class S: + def hello: String = "hi" diff --git a/sbt-app/src/sbt-test/cache/compile-order-key/test b/sbt-app/src/sbt-test/cache/compile-order-key/test new file mode 100644 index 000000000..ce962cfad --- /dev/null +++ b/sbt-app/src/sbt-test/cache/compile-order-key/test @@ -0,0 +1,11 @@ +# Regression for #9748: compileOrder has to take part in the compile cache key. Under +# JavaThenScala javac runs before the Scala class exists and the build fails. Flipping to +# Mixed has to recompile, not replay that failure from an entry filed under the same key. + +-> compile + +$ touch mixed.marker +> reload + +> compile +> checkClasses