隔离插件类路径

Note

文档的配方部分以目标为核心,尽量减少对基础知识的说明。

目标

我想隔离插件类路径,例如以便使用 Scala 2.12 应用程序。

步骤

  1. 以发布到 Maven Central 的命令行应用程序为前提,例如 Coursier CLI 或您自己的应用程序。

  2. 定义一个封装该命令行应用程序的合成子项目:

    package example
    
    import sbt.{ *, given }
    import Keys.*
    
    object BootstrapPlugin extends AutoPlugin:
      override lazy val requires = sbt.plugins.JvmPlugin
    
      lazy val bootstrapCs = project
        .settings(
          scalaVersion := "2.12.21",
          libraryDependencies += "io.get-coursier" %% "coursier-cli" % "2.1.14",
          Compile / run / mainClass := Some("coursier.cli.Coursier"),
          // 仅适用于 sbt 2.x
          clientSide := false,
        )
    
      override lazy val extraProjects = Vector(bootstrapCs)
    
      ....
    end BootstrapPlugin
    
  3. 定义一个表示命令行参数的设置,以及一个在合成子项目上调用 run 任务的动态任务。

project/BootstrapPlugin.scala

package example

import sbt.{ *, given }
import Keys.*

object BootstrapPlugin extends AutoPlugin:
  override lazy val requires = sbt.plugins.JvmPlugin

  object autoImport:
    val packageBootstrap = taskKey[HashedVirtualFileRef]("packageBootstrap")
    val packageBootstrapArgs = settingKey[Seq[String]]("packageBootstrapArgs")
    val packageBootstrapOutput = settingKey[File]("packageBootstrapOutput")
  end autoImport
  import autoImport.*

  lazy val bootstrapCs = project
    .settings(
      scalaVersion := "2.12.21",
      libraryDependencies += "io.get-coursier" %% "coursier-cli" % "2.1.14",
      Compile / run / mainClass := Some("coursier.cli.Coursier"),
      // applicable to sbt 2.x only
      clientSide := false,
    )

  override lazy val extraProjects = Vector(bootstrapCs)

  override lazy val projectSettings = Vector(
    packageBootstrapOutput := target.value / "bootstrap" / s"${moduleName.value}.jar",
    packageBootstrapArgs := {
      val coord =
        s"${organization.value}:${name.value}_${scalaBinaryVersion.value}:${version.value}"
      val sv = scalaVersion.value
      Vector("bootstrap", "--verbose", "--bat=true",
        "--scala-version", sv,
        "-f", coord,
        "-o", packageBootstrapOutput.value.toString)
    },
    packageBootstrap := Def.uncached {
      // to process args before toTask, we need to use dynamic tasks
      (Def.taskDyn {
        // phase 1
        val c = fileConverter.value
        val args = packageBootstrapArgs.value
        val out = packageBootstrapOutput.value
        // phase 2
        val outVf: HashedVirtualFileRef = c.toVirtualFile(out.toPath())
        IO.createDirectory(out.getParentFile())
        // phase 3
        (bootstrapCs / Compile / run)
          .toTask(args.mkString(" ", " ", ""))
          .map(_ => outVf)
      }).value
    },
  )
end BootstrapPlugin

Note

如果命令行应用程序调用了 sys.exit(1),则必须 fork run

// sbtn 的默认值
clientSide := true,
// 用于 sbt --server
Compile / run / fork := true,

测试

$ sbt app/publishLocal
$ sbt app/packageBootstrap
[info] running coursier.cli.Coursier bootstrap --verbose --bat=true 
  --scala-version 3.8.4 -f com.example:hello_3:0.1.0-SNAPSHOT
  -o /.../isolation/target/out/jvm/scala-3.8.4/hello/bootstrap/hello.jar
  Dependencies:
com.example:hello_3:0.1.0-SNAPSHOT:
Wrote target/out/jvm/scala-3.8.4/hello/bootstrap/hello.jar
Wrote target/out/jvm/scala-3.8.4/hello/bootstrap/hello.jar.bat
[success] elapsed time: 3 s, cache 100%, 17 disk cache hits
$ target/out/jvm/scala-3.8.4/hello/bootstrap/hello.jar world
hi world