Meta-build dependency isolation in SBT

I’ve been hit by an problem in the Kinetic Merge CI build in the context of upgrading to SBT 2…

What’s going on is that the build.sbt for the shipped deliverables invokes Coursier to package up the jar files into a standalone executable, so there is a stanza:

    packageExecutable := {
      val packagingVersion = (ThisBuild / version).value

      println(s"Packaging executable with version: $packagingVersion")

      val localArtifactCoordinates =
        s"${organization.value}:${name.value}_${scalaBinaryVersion.value}:$packagingVersion"

      val executablePath = s"${target.value}${Path.sep}${name.value}"

      coursier.cli.Coursier.main(
        s"bootstrap --verbose --bat=true --scala-version ${scalaBinaryVersion.value} -f $localArtifactCoordinates -o $executablePath"
          .split("\\s+")
      )

      name.value
    },

Observe the dependency on Coursier within the build itself.

This has always been a bit problematic, because there are SBT plugins that also depend on bits of Coursier, and that leads to dependency hell, so in the past I came up with a workaround by writing another build.sbt for the meta-build; that file goes in project/build.sbt, and it had a dependency definition:

ThisBuild / libraryDependencies += "io.get-coursier" %% "coursier-cli" % "2.1.14"

That applies to the top-level build, and all is well on the main branch.

Where it’s going wrong is that with SBT 2, the version of Scala that SBT runs with has bumped up from 2.12 all the way to 3.8.* - and Coursier isn’t published for Scala 3.* yet.

Not to worry, I’ll jemmy the dependencies as per the Coursier folks advice:

ThisBuild / libraryDependencies += ("io.get-coursier" %% "coursier-cli" % "2.1.24")
  .cross(CrossVersion.for3Use2_13)

(I also took the opportunity to upgrade the dependency at long last now that the build is off Scala 2.12).

That works in itself, but leads to problems where other ordinary dependencies in the top-level build.sbt are conflicting with transitive dependencies of Coursier, because Kinetic Merge is built for Scala 3.3 and the Coursier dependencies are forced to 2.13.

What I really want to do is just get a build that ships an executable, but I’d like to not have to rewrite everything in the project’s build and GitHub CI actions.

So the likes of Mill and maybe just scala-cli are on the table, but not my first choice.

Is there some way I can persuade SBT to not pollute the dependency scope used to build the shipped stuff with the meta-build dependencies?

It seems that ThisBuild is too global here (I tried Global too, but that didn’t work either).