mirror of https://github.com/sbt/sbt.git
[2.x] fix: Fixes cache invalidation on version change (#9471)
**Problem** packageBin includes version into the file name, which ends up invalidating the cache. **Solution** Define packageInternal, which does not include version in the file name, and used during Compile or Test/compile. Note that publishing and Runtime classpath would continue to use packageBin.
This commit is contained in:
parent
a1114188b4
commit
6b3d7c6301
|
|
@ -112,6 +112,25 @@ private[librarymanagement] abstract class ArtifactFunctions {
|
|||
base + "-" + module.revision + classifierStr + "." + artifact.extension
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `artifactName`, but omits the module's version.
|
||||
*/
|
||||
def internalArtifactName(
|
||||
scalaVersion: ScalaVersion,
|
||||
module: ModuleID,
|
||||
artifact: Artifact
|
||||
): String =
|
||||
import artifact.*
|
||||
val classifierStr = classifier match
|
||||
case None => ""
|
||||
case Some(c) => s"-${c}"
|
||||
val cross = CrossVersion(module.crossVersion, scalaVersion.full, scalaVersion.binary)
|
||||
val withPlatform = module.crossVersion match
|
||||
case _: Disabled => artifact.name
|
||||
case _ => CrossVersion.addPlatformSuffix(artifact.name, module.platformOpt, None)
|
||||
val base = CrossVersion.applyCross(withPlatform, cross)
|
||||
s"${base}${classifierStr}.${artifact.extension}"
|
||||
|
||||
val classifierTypeMap = Map(SourceClassifier -> SourceType, DocClassifier -> DocType)
|
||||
@deprecated("Configuration should not be decided from the classifier.", "1.0")
|
||||
def classifierConf(classifier: String): Configuration =
|
||||
|
|
|
|||
|
|
@ -1812,6 +1812,8 @@ object Defaults extends BuildCommon with DefExtra {
|
|||
packageTaskSettings(packageBin, packageBinMappings) ++
|
||||
packageTaskSettings(packageSrc, packageSrcMappings) ++
|
||||
packageTaskSettings(packageDoc, packageDocMappings) ++
|
||||
packageTaskSettings(packageInternal, packageBin / mappings) ++
|
||||
inTask(packageInternal)(Seq(artifactName :== Artifact.internalArtifactName)) ++
|
||||
Seq(Keys.`package` := packageBin.value)
|
||||
|
||||
def packageBinMappings: Initialize[Task[Seq[(HashedVirtualFileRef, String)]]] =
|
||||
|
|
@ -2884,6 +2886,15 @@ object Classpaths {
|
|||
exportedProductsNoTracking := ClasspathImpl
|
||||
.trackedExportedProducts(TrackLevel.NoTracking)
|
||||
.value,
|
||||
exportedProductsVersioned := Def.uncached(
|
||||
ClasspathImpl.trackedExportedProductsVersioned(TrackLevel.TrackAlways).value
|
||||
),
|
||||
exportedProductsVersionedIfMissing := ClasspathImpl
|
||||
.trackedExportedProductsVersioned(TrackLevel.TrackIfMissing)
|
||||
.value,
|
||||
exportedProductsVersionedNoTracking := ClasspathImpl
|
||||
.trackedExportedProductsVersioned(TrackLevel.NoTracking)
|
||||
.value,
|
||||
exportedProductJars := ClasspathImpl.trackedExportedJarProducts(TrackLevel.TrackAlways).value,
|
||||
exportedProductJarsIfMissing := ClasspathImpl
|
||||
.trackedExportedJarProducts(TrackLevel.TrackIfMissing)
|
||||
|
|
@ -2891,6 +2902,15 @@ object Classpaths {
|
|||
exportedProductJarsNoTracking := ClasspathImpl
|
||||
.trackedExportedJarProducts(TrackLevel.NoTracking)
|
||||
.value,
|
||||
exportedProductJarsVersioned := ClasspathImpl
|
||||
.trackedExportedJarProductsVersioned(TrackLevel.TrackAlways)
|
||||
.value,
|
||||
exportedProductJarsVersionedIfMissing := ClasspathImpl
|
||||
.trackedExportedJarProductsVersioned(TrackLevel.TrackIfMissing)
|
||||
.value,
|
||||
exportedProductJarsVersionedNoTracking := ClasspathImpl
|
||||
.trackedExportedJarProductsVersioned(TrackLevel.NoTracking)
|
||||
.value,
|
||||
internalDependencyAsJars := Def.uncached(internalDependencyJarsTask.value),
|
||||
dependencyClasspathAsJars := Def.uncached(
|
||||
concat(
|
||||
|
|
|
|||
|
|
@ -324,6 +324,7 @@ object Keys {
|
|||
|
||||
// package keys
|
||||
val packageBin = taskKey[HashedVirtualFileRef]("Produces a main artifact, such as a binary jar.").withRank(ATask)
|
||||
val packageInternal = taskKey[HashedVirtualFileRef]("Produces a binary JAR for internal use (inter-project classpaths, BSP).").withRank(DTask)
|
||||
val `package` = taskKey[HashedVirtualFileRef]("Produces the main artifact, such as a binary jar. This is typically an alias for the task that actually does the packaging.").withRank(APlusTask)
|
||||
val packageDoc = taskKey[HashedVirtualFileRef]("Produces a documentation artifact, such as a jar containing API documentation.").withRank(AMinusTask)
|
||||
val packageSrc = taskKey[HashedVirtualFileRef]("Produces a source artifact, such as a jar containing sources and resources.").withRank(AMinusTask)
|
||||
|
|
@ -442,6 +443,9 @@ object Keys {
|
|||
val exportedProducts = taskKey[Classpath]("Build products that go on the exported classpath.").withRank(CTask)
|
||||
val exportedProductsIfMissing = taskKey[Classpath]("Build products that go on the exported classpath if missing.").withRank(CTask)
|
||||
val exportedProductsNoTracking = taskKey[Classpath]("Just the exported classpath without triggering the compilation.").withRank(CTask)
|
||||
val exportedProductsVersioned = taskKey[Classpath]("Build products that go on the exported classpath, packaged with the versioned artifact (used by the Runtime configuration).").withRank(CTask)
|
||||
val exportedProductsVersionedIfMissing = taskKey[Classpath]("Build products that go on the exported classpath, packaged with the versioned artifact, if missing.").withRank(CTask)
|
||||
val exportedProductsVersionedNoTracking = taskKey[Classpath]("Just the exported classpath, packaged with the versioned artifact, without triggering the compilation.").withRank(CTask)
|
||||
val unmanagedClasspath = taskKey[Classpath]("Classpath entries (deep) that are manually managed.").withRank(BPlusTask)
|
||||
val unmanagedJars = taskKey[Classpath]("Classpath entries for the current project (shallow) that are manually managed.").withRank(BPlusTask)
|
||||
val managedClasspath = taskKey[Classpath]("The classpath consisting of external, managed library dependencies.").withRank(BMinusTask)
|
||||
|
|
@ -458,6 +462,9 @@ object Keys {
|
|||
val exportedProductJars = taskKey[Classpath]("Build products that go on the exported classpath as JARs.")
|
||||
val exportedProductJarsIfMissing = taskKey[Classpath]("Build products that go on the exported classpath as JARs if missing.")
|
||||
val exportedProductJarsNoTracking = taskKey[Classpath]("Just the exported classpath as JARs without triggering the compilation.")
|
||||
val exportedProductJarsVersioned = taskKey[Classpath]("Build products that go on the exported classpath as JARs, packaged with the versioned artifact (used by the Runtime configuration).")
|
||||
val exportedProductJarsVersionedIfMissing = taskKey[Classpath]("Build products that go on the exported classpath as JARs, packaged with the versioned artifact, if missing.")
|
||||
val exportedProductJarsVersionedNoTracking = taskKey[Classpath]("Just the exported classpath as JARs, packaged with the versioned artifact, without triggering the compilation.")
|
||||
val exportedPickles = taskKey[Classpath]("Build products that go on the exported compilation classpath as JARs. Note this is promise-blocked.").withRank(DTask)
|
||||
val pickleProducts = taskKey[Seq[VirtualFile]]("Pickle JARs").withRank(DTask)
|
||||
val internalDependencyAsJars = taskKey[Classpath]("The internal (inter-project) classpath as JARs.")
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import sbt.librarymanagement.{
|
|||
TrackLevel,
|
||||
UpdateReport
|
||||
}
|
||||
import sbt.librarymanagement.Configurations.names
|
||||
import sbt.librarymanagement.Configurations.{ names, Runtime }
|
||||
import sbt.SlashSyntax0.*
|
||||
import sbt.std.TaskExtra.*
|
||||
import sbt.util.*
|
||||
|
|
@ -56,13 +56,24 @@ private[sbt] object ClasspathImpl {
|
|||
else exportedProducts.value
|
||||
}
|
||||
|
||||
// Compile/Test: version-free, so a dependency's version bump doesn't bust the cache.
|
||||
def trackedExportedProducts(track: TrackLevel): Initialize[Task[Classpath]] =
|
||||
trackedExportedProductsFor(packageInternal, track)
|
||||
|
||||
// Runtime: versioned, matching what `run` and sbt-native-packager-style tooling expect.
|
||||
def trackedExportedProductsVersioned(track: TrackLevel): Initialize[Task[Classpath]] =
|
||||
trackedExportedProductsFor(packageBin, track)
|
||||
|
||||
private def trackedExportedProductsFor(
|
||||
key: TaskKey[HashedVirtualFileRef],
|
||||
track: TrackLevel
|
||||
): Initialize[Task[Classpath]] =
|
||||
Def.task {
|
||||
val _ = (packageBin / dynamicDependency).value
|
||||
val art = (packageBin / artifact).value
|
||||
val module = projectID.value
|
||||
val config = configuration.value
|
||||
for (f, analysis) <- trackedExportedProductsImplTask(track).value
|
||||
for (f, analysis) <- trackedExportedProductsImplTask(key, track).value
|
||||
yield APIMappings
|
||||
.store(Classpaths.analyzed(f, analysis), apiURL.value)
|
||||
.put(Keys.artifactStr, RemoteCache.artifactToStr(art))
|
||||
|
|
@ -71,12 +82,21 @@ private[sbt] object ClasspathImpl {
|
|||
}
|
||||
|
||||
def trackedExportedJarProducts(track: TrackLevel): Initialize[Task[Classpath]] =
|
||||
trackedExportedJarProductsFor(packageInternal, track)
|
||||
|
||||
def trackedExportedJarProductsVersioned(track: TrackLevel): Initialize[Task[Classpath]] =
|
||||
trackedExportedJarProductsFor(packageBin, track)
|
||||
|
||||
private def trackedExportedJarProductsFor(
|
||||
key: TaskKey[HashedVirtualFileRef],
|
||||
track: TrackLevel
|
||||
): Initialize[Task[Classpath]] =
|
||||
Def.task {
|
||||
val _ = (packageBin / dynamicDependency).value
|
||||
val art = (packageBin / artifact).value
|
||||
val module = projectID.value
|
||||
val config = configuration.value
|
||||
for (f, analysis) <- trackedJarProductsImplTask(track).value
|
||||
for (f, analysis) <- jarProductsForTask(key, track).value
|
||||
yield APIMappings
|
||||
.store(Classpaths.analyzed(f, analysis), apiURL.value)
|
||||
.put(Keys.artifactStr, RemoteCache.artifactToStr(art))
|
||||
|
|
@ -85,13 +105,14 @@ private[sbt] object ClasspathImpl {
|
|||
}
|
||||
|
||||
private def trackedExportedProductsImplTask(
|
||||
key: TaskKey[HashedVirtualFileRef],
|
||||
track: TrackLevel
|
||||
): Initialize[Task[Seq[(HashedVirtualFileRef, VirtualFile)]]] =
|
||||
Def.taskIf {
|
||||
if {
|
||||
val _ = (packageBin / dynamicDependency).value
|
||||
exportJars.value
|
||||
} then trackedJarProductsImplTask(track).value
|
||||
} then jarProductsForTask(key, track).value
|
||||
else trackedNonJarProductsImplTask(track).value
|
||||
}
|
||||
|
||||
|
|
@ -126,13 +147,14 @@ private[sbt] object ClasspathImpl {
|
|||
}
|
||||
}
|
||||
|
||||
private def trackedJarProductsImplTask(
|
||||
private def jarProductsForTask(
|
||||
key: TaskKey[HashedVirtualFileRef],
|
||||
track: TrackLevel
|
||||
): Initialize[Task[Seq[(HashedVirtualFileRef, VirtualFile)]]] =
|
||||
(Def
|
||||
.task {
|
||||
val converter = fileConverter.value
|
||||
val vf = (packageBin / artifactPath).value
|
||||
val vf = (key / artifactPath).value
|
||||
val jar = converter.toPath(vf)
|
||||
(TrackLevel.intersection(track, exportToInternal.value), vf, jar)
|
||||
})
|
||||
|
|
@ -141,13 +163,13 @@ private[sbt] object ClasspathImpl {
|
|||
Def.task {
|
||||
val converter = fileConverter.value
|
||||
val analysisFile = converter.toVirtualFile(compileAnalysisFile.value.toPath)
|
||||
Seq((packageBin.value, analysisFile))
|
||||
Seq((key.value, analysisFile))
|
||||
}
|
||||
case (TrackLevel.TrackIfMissing, _, jar) if !jar.toFile().exists =>
|
||||
Def.task {
|
||||
val converter = fileConverter.value
|
||||
val analysisFile = converter.toVirtualFile(compileAnalysisFile.value.toPath)
|
||||
Seq((packageBin.value, analysisFile))
|
||||
Seq((key.value, analysisFile))
|
||||
}
|
||||
case (_, vf, _) =>
|
||||
Def.task {
|
||||
|
|
@ -164,6 +186,9 @@ private[sbt] object ClasspathImpl {
|
|||
(exportedProductsNoTracking / transitiveClasspathDependency).value,
|
||||
(exportedProductsIfMissing / transitiveClasspathDependency).value,
|
||||
(exportedProducts / transitiveClasspathDependency).value,
|
||||
(exportedProductsVersionedNoTracking / transitiveClasspathDependency).value,
|
||||
(exportedProductsVersionedIfMissing / transitiveClasspathDependency).value,
|
||||
(exportedProductsVersioned / transitiveClasspathDependency).value,
|
||||
(exportedProductJarsNoTracking / transitiveClasspathDependency).value,
|
||||
(exportedProductJarsIfMissing / transitiveClasspathDependency).value,
|
||||
(exportedProductJars / transitiveClasspathDependency).value
|
||||
|
|
@ -194,11 +219,18 @@ private[sbt] object ClasspathImpl {
|
|||
log: Logger
|
||||
): Initialize[Task[Classpath]] =
|
||||
Def.value[Task[Classpath]] {
|
||||
interDependencies(projectRef, deps, conf, self, data, track, false, log)(
|
||||
exportedProductsNoTracking,
|
||||
exportedProductsIfMissing,
|
||||
exportedProducts
|
||||
)
|
||||
if self == Runtime then
|
||||
interDependencies(projectRef, deps, conf, self, data, track, false, log)(
|
||||
exportedProductsVersionedNoTracking,
|
||||
exportedProductsVersionedIfMissing,
|
||||
exportedProductsVersioned
|
||||
)
|
||||
else
|
||||
interDependencies(projectRef, deps, conf, self, data, track, false, log)(
|
||||
exportedProductsNoTracking,
|
||||
exportedProductsIfMissing,
|
||||
exportedProducts
|
||||
)
|
||||
}
|
||||
|
||||
def internalDependencyPicklePathTask: Initialize[Task[Classpath]] = {
|
||||
|
|
@ -258,20 +290,36 @@ private[sbt] object ClasspathImpl {
|
|||
log: Logger
|
||||
): Initialize[Task[Classpath]] =
|
||||
Def.value[Task[Classpath]] {
|
||||
interDependencies[Attributed[HashedVirtualFileRef]](
|
||||
projectRef,
|
||||
deps,
|
||||
conf,
|
||||
self,
|
||||
data,
|
||||
track,
|
||||
false,
|
||||
log,
|
||||
)(
|
||||
exportedProductJarsNoTracking,
|
||||
exportedProductJarsIfMissing,
|
||||
exportedProductJars
|
||||
): Task[Classpath]
|
||||
if self == Runtime then
|
||||
interDependencies[Attributed[HashedVirtualFileRef]](
|
||||
projectRef,
|
||||
deps,
|
||||
conf,
|
||||
self,
|
||||
data,
|
||||
track,
|
||||
false,
|
||||
log,
|
||||
)(
|
||||
exportedProductJarsVersionedNoTracking,
|
||||
exportedProductJarsVersionedIfMissing,
|
||||
exportedProductJarsVersioned
|
||||
): Task[Classpath]
|
||||
else
|
||||
interDependencies[Attributed[HashedVirtualFileRef]](
|
||||
projectRef,
|
||||
deps,
|
||||
conf,
|
||||
self,
|
||||
data,
|
||||
track,
|
||||
false,
|
||||
log,
|
||||
)(
|
||||
exportedProductJarsNoTracking,
|
||||
exportedProductJarsIfMissing,
|
||||
exportedProductJars
|
||||
): Task[Classpath]
|
||||
}
|
||||
|
||||
def unmanagedDependenciesTask: Initialize[Task[Classpath]] =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package example
|
||||
|
||||
@main
|
||||
def main(args: String*): Unit =
|
||||
println(Foo.greeting)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
Global / localCacheDirectory := baseDirectory.value / "diskcache"
|
||||
|
||||
scalaVersion := "3.8.4"
|
||||
|
||||
// packageInternal is used for the Compile/Test-time internal classpath (what app compiles
|
||||
// against), so a dependency's version bump alone doesn't bust downstream compile caches.
|
||||
// packageBin (versioned) must still back the Runtime classpath, since that's what `run`
|
||||
// uses and what tools like sbt-native-packager read to assemble a runnable image.
|
||||
lazy val checkClasspaths = taskKey[Unit]("Assert Compile classpath uses packageInternal, Runtime classpath uses packageBin")
|
||||
|
||||
lazy val foo = project
|
||||
|
||||
lazy val app = project
|
||||
.dependsOn(foo)
|
||||
.settings(
|
||||
checkClasspaths := {
|
||||
val converter = fileConverter.value
|
||||
val binPath = converter.toPath((foo / Compile / packageBin).value)
|
||||
val internalPath = converter.toPath((foo / Compile / packageInternal).value)
|
||||
val s = streams.value
|
||||
|
||||
s.log.info(s"packageBin = $binPath")
|
||||
s.log.info(s"packageInternal = $internalPath")
|
||||
assert(binPath != internalPath, "packageBin and packageInternal unexpectedly produced the same path")
|
||||
|
||||
def check(name: String, cp: Seq[HashedVirtualFileRef], expectVersioned: Boolean): Unit =
|
||||
val paths = cp.map(converter.toPath)
|
||||
s.log.info(s"$name = $paths")
|
||||
val (expected, unexpected) = if expectVersioned then (binPath, internalPath) else (internalPath, binPath)
|
||||
val expectedDesc = if expectVersioned then "packageBin's" else "packageInternal's"
|
||||
val unexpectedDesc = if expectVersioned then "packageInternal's" else "packageBin's"
|
||||
assert(paths.contains(expected), s"$name should contain $expectedDesc jar ($expected), got: $paths")
|
||||
assert(!paths.contains(unexpected), s"$name should NOT contain $unexpectedDesc jar ($unexpected), got: $paths")
|
||||
|
||||
check("Compile/dependencyClasspath", (Compile / dependencyClasspath).value.map(_.data), expectVersioned = false)
|
||||
check("Runtime/dependencyClasspath", (Runtime / dependencyClasspath).value.map(_.data), expectVersioned = true)
|
||||
|
||||
check("Compile/internalDependencyAsJars", (Compile / internalDependencyAsJars).value.map(_.data), expectVersioned = false)
|
||||
check("Runtime/internalDependencyAsJars", (Runtime / internalDependencyAsJars).value.map(_.data), expectVersioned = true)
|
||||
|
||||
check("Compile/dependencyClasspathAsJars", (Compile / dependencyClasspathAsJars).value.map(_.data), expectVersioned = false)
|
||||
check("Runtime/dependencyClasspathAsJars", (Runtime / dependencyClasspathAsJars).value.map(_.data), expectVersioned = true)
|
||||
|
||||
check("Compile/fullClasspathAsJars", (Compile / fullClasspathAsJars).value.map(_.data), expectVersioned = false)
|
||||
check("Runtime/fullClasspathAsJars", (Runtime / fullClasspathAsJars).value.map(_.data), expectVersioned = true)
|
||||
}
|
||||
)
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.aggregate(foo, app)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package example
|
||||
|
||||
object Foo:
|
||||
def greeting: String = "hello"
|
||||
end Foo
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
> app/checkClasspaths
|
||||
> app/run
|
||||
|
||||
# bumping foo's version must not change which task backs each classpath:
|
||||
# Compile still packageInternal (version-free), Runtime still packageBin (versioned)
|
||||
> set foo / version := "0.2.0"
|
||||
> app/checkClasspaths
|
||||
> app/run
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package example
|
||||
|
||||
@main
|
||||
def main(args: String*): Unit =
|
||||
println(Foo.greeting)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import sbt.internal.util.CacheEventSummary
|
||||
import complete.DefaultParsers.*
|
||||
|
||||
Global / localCacheDirectory := baseDirectory.value / "diskcache"
|
||||
|
||||
scalaVersion := "3.8.4"
|
||||
|
||||
lazy val checkMiss = inputKey[Unit]("Assert the exact onsite/miss count of the previous run")
|
||||
|
||||
lazy val foo = project
|
||||
|
||||
lazy val app = project.dependsOn(foo)
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.aggregate(foo, app)
|
||||
.settings(
|
||||
checkMiss := {
|
||||
val expected: Int = (Space ~> NatBasic).parsed
|
||||
val s = streams.value
|
||||
val config = Def.cacheConfiguration.value
|
||||
val prev = config.cacheEventLog.previous match
|
||||
case d: CacheEventSummary.Data => d
|
||||
case CacheEventSummary.Empty => sys.error("empty event log")
|
||||
s.log.info(s"missCount = ${prev.missCount}")
|
||||
assert(prev.missCount == expected, s"prev.missCount = ${prev.missCount} (expected $expected)")
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package example
|
||||
|
||||
object Foo:
|
||||
def greeting: String = "hello"
|
||||
end Foo
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# warm the disk cache
|
||||
> app/compile
|
||||
|
||||
# after a clean, recompiling app (and foo) should fully restore from the disk cache
|
||||
> clean
|
||||
> app/compile
|
||||
> checkMiss 0
|
||||
|
||||
# bumping foo's version alone (e.g. what sbt-dynver would do on a new commit) must not
|
||||
# invalidate app's compile cache entry, since foo's jar content hasn't changed
|
||||
> set foo / version := "0.2.0"
|
||||
> clean
|
||||
> app/compile
|
||||
> checkMiss 0
|
||||
|
|
@ -3,6 +3,7 @@ ThisBuild / scalaVersion := "3.8.4"
|
|||
name := "hello"
|
||||
enablePlugins(JavaAppPackaging)
|
||||
|
||||
@transient
|
||||
lazy val check = taskKey[Unit]("")
|
||||
|
||||
check := {
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ lazy val bazApp = (projectMatrix in file("baz-app"))
|
|||
name := "baz app",
|
||||
check := {
|
||||
val cp = (Compile / fullClasspath).value.map(_.data.id)
|
||||
assert(cp.exists(_.endsWith("baz-core_2.13-0.1.0-SNAPSHOT.jar")), cp)
|
||||
assert(!cp.exists(_.endsWith("baz-core_3.0.0-M1-0.1.0-SNAPSHOT.jar")), cp)
|
||||
assert(cp.exists(_.endsWith("baz-core_2.13.jar")), cp)
|
||||
assert(!cp.exists(_.endsWith("baz-core_3.0.0-M1.jar")), cp)
|
||||
assert(projectMatrixBaseDirectory.value == (ThisBuild / baseDirectory).value / "baz-app",
|
||||
s"projectMatrixBaseDirectory is ${projectMatrixBaseDirectory.value}")
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue