project/ReflectiveCodeGen.scala (120 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* license agreements; and to You under the Apache License, version 2.0:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* This file is part of the Apache Pekko project, which was derived from Akka.
*/
package org.apache.pekko.grpc.build
import java.io.File
import sbt._
import sbt.Keys._
import sbtprotoc.ProtocPlugin
import ProtocPlugin.autoImport.PB
import protocbridge.Target
import sbt.ProjectRef
import sbt.file
import sbt.internal.inc.classpath.ClasspathUtil
import scala.collection.mutable.ListBuffer
import protocbridge.{ Artifact => BridgeArtifact }
/** A plugin that allows to use a code generator compiled in one subproject to be used in a test project */
object ReflectiveCodeGen extends AutoPlugin {
lazy val generatedLanguages = SettingKey[Seq[String]]("reflectiveGrpcGeneratedLanguages")
lazy val generatedSources = SettingKey[Seq[String]]("reflectiveGrpcGeneratedSources")
lazy val extraGenerators = SettingKey[Seq[String]]("reflectiveGrpcExtraGenerators")
lazy val codeGeneratorSettings = settingKey[Seq[String]]("Code generator settings")
lazy val protocOptions = settingKey[Seq[String]]("Protoc Options.")
// needed to be able to override the PB.generate task reliably
override lazy val requires = ProtocPlugin
override lazy val projectSettings: Seq[Def.Setting[_]] =
inConfig(Compile)(
Seq(
PB.protocOptions := protocOptions.value,
PB.generate :=
// almost the same as `Def.sequential` but will return the "middle" value, ie. the result of the generation
// Defines three steps:
// 1) dynamically load the current code generator and plug it in the mutable generator
// 2) run the generator
// 3) delete the generation cache because it doesn't know that the generator may change
Def.taskDyn {
val _ = setCodeGenerator.value
Def.taskDyn {
val generationResult = generateTaskFromProtocPlugin.value
Def.task {
// path is defined in ProtocPlugin.sourceGeneratorTask
val file = (PB.generate / streams).value.cacheDirectory / s"protobuf_${scalaBinaryVersion.value}"
IO.delete(file)
generationResult
}
}
}.value,
// HACK: make the targets mutable, so we can fill them while running the above PB.generate
PB.targets := scala.collection.mutable.ListBuffer.empty,
// Put an artifact resolver that returns the project's classpath for our generators
PB.artifactResolver := Def.taskDyn {
val cp = (ProjectRef(file("."), "codegen") / Compile / fullClasspath).value.map(_.data)
val oldResolver = PB.artifactResolver.value
Def.task { (artifact: BridgeArtifact) =>
artifact.groupId match {
case "org.apache.pekko" =>
cp
case _ =>
oldResolver(artifact)
}
}
}.value,
setCodeGenerator := loadAndSetGenerator(
// the magic sauce: use the output classpath from the the sbt-plugin project and instantiate generators from there
(ProjectRef(file("."), "sbt-plugin") / Compile / fullClasspath).value,
generatedLanguages.value,
generatedSources.value,
extraGenerators.value,
sourceManaged.value,
codeGeneratorSettings.value,
PB.targets.value.asInstanceOf[ListBuffer[Target]],
scalaBinaryVersion.value),
(Compile / PB.protoSources) := PB.protoSources.value ++ Seq(
PB.externalIncludePath.value,
sourceDirectory.value / "proto"))) ++ Seq(
(Global / codeGeneratorSettings) := Nil,
(Global / generatedLanguages) := Seq("Scala"),
(Global / generatedSources) := Seq("Client", "Server"),
(Global / extraGenerators) := Seq.empty,
(Global / protocOptions) := Seq.empty,
watchSources ++= (ProjectRef(file("."), "codegen") / watchSources).value,
watchSources ++= (ProjectRef(file("."), "sbt-plugin") / watchSources).value)
lazy val setCodeGenerator = taskKey[Unit]("grpc-set-code-generator")
def loadAndSetGenerator(
classpath: Classpath,
languages0: Seq[String],
sources0: Seq[String],
extraGenerators0: Seq[String],
targetPath: File,
generatorSettings: Seq[String],
targets: ListBuffer[Target],
scalaBinaryVersion: String): Unit = {
val languages = languages0.mkString(", ")
val sources = sources0.mkString(", ")
val extraGenerators = extraGenerators0.mkString(", ")
val generatorSettings1 = generatorSettings.mkString("\"", "\", \"", "\"")
val cp = classpath.map(_.data)
// ensure to set right parent classloader, so that protocbridge.ProtocCodeGenerator etc are
// compatible with what is already accessible from this sbt build
val loader = ClasspathUtil.toLoader(cp, classOf[protocbridge.ProtocCodeGenerator].getClassLoader)
import scala.reflect.runtime.universe
import scala.tools.reflect.ToolBox
val tb = universe.runtimeMirror(loader).mkToolBox()
val source =
s"""import org.apache.pekko.grpc.sbt.PekkoGrpcPlugin
|import org.apache.pekko.grpc.sbt.GeneratorBridge
|import PekkoGrpcPlugin.autoImport._
|import PekkoGrpc._
|import org.apache.pekko.grpc.gen.scaladsl._
|import org.apache.pekko.grpc.gen.javadsl._
|import org.apache.pekko.grpc.gen.CodeGenerator.ScalaBinaryVersion
|
|val languages: Seq[PekkoGrpc.Language] = Seq($languages)
|val sources: Seq[PekkoGrpc.GeneratedSource] = Seq($sources)
|val scalaBinaryVersion = ScalaBinaryVersion("$scalaBinaryVersion")
|
|val logger = org.apache.pekko.grpc.gen.StdoutLogger
|
|(targetPath: java.io.File, settings: Seq[String]) => {
| val generators =
| PekkoGrpcPlugin.generatorsFor(sources, languages, scalaBinaryVersion, logger) ++
| Seq($extraGenerators).map(gen => GeneratorBridge.sandboxedGenerator(gen, scalaBinaryVersion, org.apache.pekko.grpc.gen.StdoutLogger))
| PekkoGrpcPlugin.targetsFor(targetPath, settings, generators)
|}
""".stripMargin
val generatorsF = tb.eval(tb.parse(source)).asInstanceOf[(File, Seq[String]) => Seq[Target]]
val generators = generatorsF(targetPath, generatorSettings)
targets.clear()
targets ++= generators.asInstanceOf[Seq[Target]]
}
lazy val generateTaskFromProtocPlugin: Def.Initialize[Task[Seq[File]]] =
// lookup and return `PB.generate := ...` setting from ProtocPlugin
ProtocPlugin.projectSettings
.find(_.key.key == PB.generate.key)
.get
.init
.asInstanceOf[Def.Initialize[Task[Seq[File]]]]
}