├── project ├── build.properties ├── pgp.sbt ├── mima.sbt ├── scalafmt.sbt ├── sonatype.sbt └── ci-release.sbt ├── .scalafmt.conf ├── .git-blame-ignore-revs ├── bridge └── src │ ├── test │ ├── resources │ │ ├── error.proto │ │ └── test.proto │ └── scala │ │ └── protocbridge │ │ ├── ProtocCacheCoursier.scala │ │ ├── RunProtoc.scala │ │ ├── frontend │ │ ├── PosixPluginFrontendSpec.scala │ │ ├── MacPluginFrontendSpec.scala │ │ ├── WindowsPluginFrontendSpec.scala │ │ ├── PluginFrontendSpec.scala │ │ └── OsSpecificFrontendSpec.scala │ │ ├── ExtraEnvSpec.scala │ │ ├── codegen │ │ └── CodeGenAppSpec.scala │ │ ├── ProtocCacheSpec.scala │ │ ├── TargetSpec.scala │ │ ├── ProtocIntegrationSpec.scala │ │ └── ProtocBridgeSpec.scala │ └── main │ ├── scala │ └── protocbridge │ │ ├── ProtocCodeGenerator.scala │ │ ├── frontend │ │ ├── WindowsPluginFrontend.scala │ │ ├── MacPluginFrontend.scala │ │ ├── SocketBasedPluginFrontend.scala │ │ ├── PosixPluginFrontend.scala │ │ └── PluginFrontend.scala │ │ ├── Target.scala │ │ ├── codegen │ │ ├── CodeGenResponse.scala │ │ ├── CodeGenRequest.scala │ │ └── CodeGenApp.scala │ │ ├── Artifact.scala │ │ ├── gens.scala │ │ ├── SystemDetector.scala │ │ ├── ProtoUtils.scala │ │ ├── FileCache.scala │ │ ├── ProtocRunner.scala │ │ ├── ExtraEnv.scala │ │ ├── Generator.scala │ │ └── ProtocBridge.scala │ └── java │ └── protocbridge │ └── frontend │ └── BridgeApp.java ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── README.md ├── .gitignore ├── shell.nix ├── .scala-steward.conf ├── sonatype.sbt ├── .mergify.yml ├── protoc-gen └── src │ ├── main │ └── scala │ │ └── protocgen │ │ ├── CodeGenResponse.scala │ │ ├── CodeGenRequest.scala │ │ └── CodeGenApp.scala │ └── test │ └── scala │ └── protocgen │ └── CodeGenAppSpec.scala ├── protoc-cache-coursier └── src │ └── main │ └── scala │ └── protocbridge │ └── ProtocCacheCoursier.scala └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.8 2 | -------------------------------------------------------------------------------- /project/pgp.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") 2 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.10.1" 2 | runner.dialect = "scala213source3" 3 | -------------------------------------------------------------------------------- /project/mima.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") 2 | -------------------------------------------------------------------------------- /project/scalafmt.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") 2 | -------------------------------------------------------------------------------- /project/sonatype.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2") 2 | -------------------------------------------------------------------------------- /project/ci-release.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") 2 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.8.6 2 | 8fd5c1019e6c75614bdde72344258d559dd7eccb 3 | -------------------------------------------------------------------------------- /bridge/src/test/resources/error.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mytest; 4 | 5 | message Error { 6 | string a = 1; 7 | } -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/ProtocCacheCoursier.scala: -------------------------------------------------------------------------------- 1 | ../../../../../protoc-cache-coursier/src/main/scala/protocbridge/ProtocCacheCoursier.scala -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /bridge/src/test/resources/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mytest; 4 | 5 | message TestMsg { 6 | string a = 1; 7 | } 8 | 9 | message AnotherMsg { 10 | string b = 1; 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | protoc-bridge 2 | ------------- 3 | 4 | [![CI](https://github.com/scalapb/protoc-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/scalapb/protoc-bridge/actions/workflows/ci.yml) 5 | 6 | Runs `protoc` in the JVM and routes code generation requests to Scala code. 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bloop/ 2 | .bsp/ 3 | .metals/ 4 | metals.sbt 5 | project/.bloop/ 6 | 7 | *.class 8 | *.log 9 | .idea 10 | 11 | # sbt specific 12 | .cache 13 | .history 14 | .lib/ 15 | dist/* 16 | target/ 17 | lib_managed/ 18 | src_managed/ 19 | project/boot/ 20 | project/plugins/project/ 21 | 22 | # Scala-IDE specific 23 | .scala_dependencies 24 | .worksheet 25 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | {pkgs ? import { 2 | config = { 3 | packageOverrides = pkgs: { 4 | sbt = pkgs.sbt.override { jre = pkgs.openjdk11; }; 5 | }; 6 | }; 7 | }} : 8 | pkgs.mkShell { 9 | buildInputs = [ 10 | pkgs.sbt 11 | pkgs.openjdk11 12 | 13 | # keep this line if you use bash 14 | pkgs.bashInteractive 15 | ]; 16 | } 17 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/ProtocCodeGenerator.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | /** This is the interface that code generators need to implement. */ 4 | trait ProtocCodeGenerator { 5 | def run(request: Array[Byte]): Array[Byte] 6 | 7 | def suggestedDependencies: Seq[Artifact] = Nil 8 | } 9 | 10 | object ProtocCodeGenerator { 11 | import scala.language.implicitConversions 12 | 13 | implicit def toGenerator(p: ProtocCodeGenerator): Generator = 14 | JvmGenerator("jvm", p) 15 | } 16 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | // https://github.com/scalapb/ScalaPB/commit/d3cc69515ea90f1af7eaf2732d22facb6c9e95e3 2 | updates.ignore = [ 3 | { groupId = "com.google.protobuf" } 4 | ] 5 | 6 | updates.pin = [ 7 | # Scala 3.3 is the LTS release 8 | { groupId = "org.scala-lang", artifactId = "scala3-compiler", version = "3.3." } 9 | { groupId = "org.scala-lang", artifactId = "scala3-library", version = "3.3." } 10 | { groupId = "org.scala-lang", artifactId = "scala3-library_sjs1", version = "3.3." } 11 | ] 12 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/RunProtoc.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import sys.process._ 4 | import scala.io.Source 5 | 6 | object RunProtoc extends ProtocRunner[Int] { 7 | def run(args: Seq[String], extraEnv: Seq[(String, String)]): Int = { 8 | CoursierProtocCache.runProtoc("3.24.4", args, extraEnv) 9 | } 10 | 11 | // For backwards binary compatibility 12 | private def run(args: Seq[String]): Int = { 13 | CoursierProtocCache.runProtoc("3.24.4", args, Seq.empty) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/frontend/PosixPluginFrontendSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend 2 | 3 | class PosixPluginFrontendSpec extends OsSpecificFrontendSpec { 4 | if (!PluginFrontend.isWindows && !PluginFrontend.isMac) { 5 | it must "execute a program that forwards input and output to given stream" in { 6 | testSuccess(PosixPluginFrontend) 7 | } 8 | 9 | it must "not hang if there is an OOM in generator" in { 10 | testFailure(PosixPluginFrontend) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sonatype.sbt: -------------------------------------------------------------------------------- 1 | sonatypeProfileName := "com.thesamet" 2 | 3 | pomExtra in Global := { 4 | https://github.com/scalapb/protoc-bridge 5 | 6 | 7 | Apache 2 8 | http://www.apache.org/licenses/LICENSE-2.0.txt 9 | 10 | 11 | 12 | 13 | thesamet 14 | Nadav S. Samet 15 | http://www.thesamet.com/ 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | queue_conditions: 4 | - author=scala-steward 5 | - check-success=ci-passed 6 | merge_conditions: 7 | - check-success=ci-passed 8 | merge_method: squash 9 | 10 | pull_request_rules: 11 | - name: assign and label scala-steward's PRs 12 | conditions: 13 | - author=scala-steward 14 | actions: 15 | assign: 16 | users: [thesamet] 17 | label: 18 | add: [dependency-update] 19 | - name: merge scala-steward's PRs 20 | conditions: [] 21 | actions: 22 | queue: 23 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/frontend/MacPluginFrontendSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend 2 | 3 | class MacPluginFrontendSpec extends OsSpecificFrontendSpec { 4 | if (PluginFrontend.isMac) { 5 | it must "execute a program that forwards input and output to given stream" in { 6 | val state = testSuccess(MacPluginFrontend) 7 | state.serverSocket.isClosed mustBe true 8 | } 9 | 10 | it must "not hang if there is an error in generator" in { 11 | val state = testFailure(MacPluginFrontend) 12 | state.serverSocket.isClosed mustBe true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/frontend/WindowsPluginFrontendSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend 2 | 3 | class WindowsPluginFrontendSpec extends OsSpecificFrontendSpec { 4 | if (PluginFrontend.isWindows) { 5 | it must "execute a program that forwards input and output to given stream" in { 6 | val state = testSuccess(WindowsPluginFrontend) 7 | state.serverSocket.isClosed mustBe true 8 | } 9 | 10 | it must "not hang if there is an OOM in generator" in { 11 | val state = testFailure(WindowsPluginFrontend) 12 | state.serverSocket.isClosed mustBe true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/ExtraEnvSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import org.scalatest._ 4 | 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.must.Matchers 7 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest 8 | import com.google.protobuf.UnknownFieldSet 9 | import com.google.protobuf.ByteString 10 | 11 | class ExtraEnvSpec extends AnyFlatSpec with Matchers { 12 | "ExtraEnv" should "parse and serialize" in { 13 | val env = new ExtraEnv(secondaryOutputDir = "foo") 14 | val bs = ByteString.copyFrom(env.toByteArrayAsField) 15 | val requestWithEnv = CodeGeneratorRequest.parseFrom(env.toByteArrayAsField) 16 | ExtraEnvParser 17 | .fromCodeGeneratorRequest(requestWithEnv) 18 | .secondaryOutputDir must be(env.secondaryOutputDir) 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{matrix.os}} 8 | strategy: 9 | matrix: 10 | os: ["ubuntu-24.04", "windows-2022", "macos-latest"] 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-java@v4 15 | with: 16 | java-version: 11 17 | distribution: temurin 18 | - uses: sbt/setup-sbt@v1 19 | - name: Mount caches 20 | uses: actions/cache@v4 21 | with: 22 | path: | 23 | ~/.sbt 24 | ~/.ivy2/cache 25 | ~/.cache/coursier 26 | key: ${{ runner.os }}-sbt-${{ hashFiles('**/*.sbt') }} 27 | - name: Compile and test 28 | run: | 29 | sbt "+ test" 30 | shell: bash 31 | - name: Format check 32 | if: ${{ runner.os == 'Linux' }} 33 | run: | 34 | sbt scalafmtCheck test:scalafmtCheck scalafmtSbtCheck 35 | # Single final job for mergify. 36 | ci-passed: 37 | runs-on: ubuntu-latest 38 | needs: build 39 | steps: 40 | - run: ':' 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | tags: ["v*"] 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-java@v4 16 | with: 17 | java-version: 8 18 | distribution: temurin 19 | - name: Scala caches 20 | uses: actions/cache@v4 21 | with: 22 | path: | 23 | ~/.sbt 24 | ~/.ivy2/cache 25 | ~/.cache/coursier 26 | key: ${{ runner.os }}-sbt-docs-${{ hashFiles('**/*.sbt') }} 27 | - uses: actions/setup-java@v4 28 | with: 29 | java-version: 11 30 | distribution: temurin 31 | - uses: sbt/setup-sbt@v1 32 | - name: Publish ${{ github.ref }} 33 | run: sbt ci-release 34 | env: 35 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 36 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 37 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 38 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 39 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/frontend/WindowsPluginFrontend.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend 2 | 3 | import java.nio.file.{Path, Paths} 4 | 5 | /** A PluginFrontend that binds a server socket to a local interface. The plugin 6 | * is a batch script that invokes BridgeApp.main() method, in a new JVM with 7 | * the same parameters as the currently running JVM. The plugin will 8 | * communicate its stdin and stdout to this socket. 9 | */ 10 | object WindowsPluginFrontend extends SocketBasedPluginFrontend { 11 | 12 | protected def createShellScript(port: Int): Path = { 13 | val classPath = 14 | Paths.get(getClass.getProtectionDomain.getCodeSource.getLocation.toURI) 15 | val classPathBatchString = classPath.toString.replace("%", "%%") 16 | val batchFile = PluginFrontend.createTempFile( 17 | ".bat", 18 | s"""@echo off 19 | |"${sys 20 | .props( 21 | "java.home" 22 | )}\\bin\\java.exe" -cp "$classPathBatchString" ${classOf[ 23 | BridgeApp 24 | ].getName} $port 25 | """.stripMargin 26 | ) 27 | batchFile 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/Target.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import java.io.File 4 | 5 | /** Target is a generator call and a path to output the generated files */ 6 | case class Target( 7 | generator: Generator, 8 | outputPath: File, 9 | options: Seq[String] = Seq.empty 10 | ) 11 | 12 | object Target { 13 | import scala.language.implicitConversions 14 | def builtin(name: String, options: Seq[String] = Seq.empty) = 15 | (BuiltinGenerator(name), options) 16 | 17 | def apply( 18 | generatorAndOpts: (Generator, Seq[String]), 19 | outputPath: File 20 | ): Target = { 21 | apply(generatorAndOpts._1, outputPath, generatorAndOpts._2) 22 | } 23 | 24 | implicit def generatorOptsFileTupleToTarget( 25 | s: ((Generator, Seq[String]), File) 26 | ): Target = 27 | Target(s._1, s._2) 28 | 29 | implicit def generatorFileTupleToTarget(s: (Generator, File)): Target = 30 | Target(s._1, s._2) 31 | 32 | implicit def protocCodeGeneratorFile(s: (ProtocCodeGenerator, File)): Target = 33 | Target(s._1, s._2) 34 | 35 | implicit def protocCodeGeneratorOptsFile( 36 | s: ((ProtocCodeGenerator, Seq[String]), File) 37 | ): Target = 38 | Target(ProtocCodeGenerator.toGenerator(s._1._1), s._2, s._1._2) 39 | } 40 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/codegen/CodeGenResponse.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.codegen 2 | 3 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse 4 | import scala.collection.JavaConverters._ 5 | 6 | @deprecated( 7 | "Use protocgen.CodeGenResponse from com.thesamet.scalapb:protocgen instead", 8 | "0.9.0" 9 | ) 10 | sealed trait CodeGenResponse { 11 | def toCodeGeneratorResponse: CodeGeneratorResponse = 12 | this match { 13 | case CodeGenResponse.Internal.Success(files) => 14 | val b = CodeGeneratorResponse.newBuilder() 15 | b.addAllFile(files.asJava) 16 | b.build() 17 | case CodeGenResponse.Internal.Failure(msg) => 18 | val b = CodeGeneratorResponse.newBuilder() 19 | b.setError(msg) 20 | b.build() 21 | } 22 | } 23 | 24 | @deprecated( 25 | "Use protocgen.CodeGenResponse from com.thesamet.scalapb:protocgen instead", 26 | "0.9.0" 27 | ) 28 | object CodeGenResponse { 29 | def succeed(files: Seq[CodeGeneratorResponse.File]): CodeGenResponse = 30 | Internal.Success(files) 31 | 32 | def fail(message: String): CodeGenResponse = Internal.Failure(message) 33 | 34 | private object Internal { 35 | final case class Failure(val message: String) extends CodeGenResponse 36 | 37 | final case class Success(val files: Seq[CodeGeneratorResponse.File]) 38 | extends CodeGenResponse 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/Artifact.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | // Simple definition of Maven coordinates. 4 | case class Artifact( 5 | groupId: String, 6 | artifactId: String, 7 | version: String, 8 | crossVersion: Boolean = false, 9 | configuration: Option[String] = None, 10 | extraAttributes: Map[String, String] = Map.empty 11 | ) { 12 | def this(groupId: String, artifactId: String, version: String) = { 13 | this( 14 | groupId, 15 | artifactId, 16 | version, 17 | crossVersion = false, 18 | configuration = None, 19 | extraAttributes = Map.empty 20 | ) 21 | } 22 | 23 | def this( 24 | groupId: String, 25 | artifactId: String, 26 | version: String, 27 | crossVersion: Boolean 28 | ) = { 29 | this( 30 | groupId, 31 | artifactId, 32 | version, 33 | crossVersion, 34 | configuration = None, 35 | extraAttributes = Map.empty 36 | ) 37 | } 38 | 39 | def withExtraAttributes(attrs: (String, String)*): Artifact = 40 | copy(extraAttributes = extraAttributes ++ attrs) 41 | 42 | def asSbtPlugin(scalaVersion: String, sbtVersion: String) = 43 | withExtraAttributes( 44 | "scalaVersion" -> scalaVersion, 45 | "sbtVersion" -> sbtVersion 46 | ) 47 | 48 | override def toString = 49 | s"$groupId:$artifactId:$version(crossVersion=$crossVersion)" 50 | } 51 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/frontend/MacPluginFrontend.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend 2 | 3 | import java.nio.file.attribute.PosixFilePermission 4 | import java.nio.file.{Files, Path} 5 | import java.{util => ju} 6 | 7 | /** PluginFrontend for macOS. 8 | * 9 | * Creates a server socket and uses `nc` to communicate with the socket. We use 10 | * a server socket instead of named pipes because named pipes are unreliable on 11 | * macOS: https://github.com/scalapb/protoc-bridge/issues/366. Since `nc` is 12 | * widely available on macOS, this is the simplest and most reliable solution 13 | * for macOS. 14 | */ 15 | object MacPluginFrontend extends SocketBasedPluginFrontend { 16 | 17 | protected def createShellScript(port: Int): Path = { 18 | val shell = sys.env.getOrElse("PROTOCBRIDGE_SHELL", "/bin/sh") 19 | // We use 127.0.0.1 instead of localhost for the (very unlikely) case that localhost is missing from /etc/hosts. 20 | val scriptName = PluginFrontend.createTempFile( 21 | "", 22 | s"""|#!$shell 23 | |set -e 24 | |nc 127.0.0.1 $port 25 | """.stripMargin 26 | ) 27 | val perms = new ju.HashSet[PosixFilePermission] 28 | perms.add(PosixFilePermission.OWNER_EXECUTE) 29 | perms.add(PosixFilePermission.OWNER_READ) 30 | Files.setPosixFilePermissions( 31 | scriptName, 32 | perms 33 | ) 34 | scriptName 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /protoc-gen/src/main/scala/protocgen/CodeGenResponse.scala: -------------------------------------------------------------------------------- 1 | package protocgen 2 | 3 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse 4 | import scala.collection.JavaConverters._ 5 | 6 | sealed trait CodeGenResponse { 7 | def toCodeGeneratorResponse: CodeGeneratorResponse = 8 | this match { 9 | case CodeGenResponse.Internal.Success(files, features) => 10 | val b = CodeGeneratorResponse.newBuilder() 11 | b.addAllFile(files.asJava) 12 | b.setSupportedFeatures(features.map(_.getNumber()).sum) 13 | b.build() 14 | case CodeGenResponse.Internal.Failure(msg) => 15 | val b = CodeGeneratorResponse.newBuilder() 16 | b.setError(msg) 17 | b.build() 18 | } 19 | } 20 | 21 | object CodeGenResponse { 22 | def succeed(files: Seq[CodeGeneratorResponse.File]): CodeGenResponse = 23 | Internal.Success(files, Set()) 24 | 25 | def succeed( 26 | files: Seq[CodeGeneratorResponse.File], 27 | supportedFeatures: Set[CodeGeneratorResponse.Feature] 28 | ): CodeGenResponse = 29 | Internal.Success(files, supportedFeatures) 30 | 31 | def fail(message: String): CodeGenResponse = Internal.Failure(message) 32 | 33 | private object Internal { 34 | final case class Failure(val message: String) extends CodeGenResponse 35 | 36 | final case class Success( 37 | val files: Seq[CodeGeneratorResponse.File], 38 | supportedFeatures: Set[CodeGeneratorResponse.Feature] 39 | ) extends CodeGenResponse 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /protoc-gen/src/main/scala/protocgen/CodeGenRequest.scala: -------------------------------------------------------------------------------- 1 | package protocgen 2 | 3 | import scala.collection.JavaConverters._ 4 | 5 | import com.google.protobuf.Descriptors.FileDescriptor 6 | import com.google.protobuf.compiler.PluginProtos 7 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest 8 | import com.google.protobuf.DescriptorProtos.FileDescriptorProto 9 | 10 | case class CodeGenRequest( 11 | parameter: String, 12 | filesToGenerate: Seq[FileDescriptor], 13 | allProtos: Seq[FileDescriptor], 14 | compilerVersion: Option[PluginProtos.Version], 15 | asProto: CodeGeneratorRequest 16 | ) 17 | 18 | object CodeGenRequest { 19 | def apply(req: CodeGeneratorRequest) = { 20 | val filesMap = fileDescriptorsByName( 21 | req.getProtoFileList().asScala.toVector 22 | ) 23 | new CodeGenRequest( 24 | parameter = req.getParameter(), 25 | filesToGenerate = 26 | req.getFileToGenerateList().asScala.toVector.map(filesMap), 27 | allProtos = filesMap.values.toVector, 28 | compilerVersion = 29 | if (req.hasCompilerVersion()) Some(req.getCompilerVersion()) else None, 30 | req 31 | ) 32 | } 33 | 34 | def fileDescriptorsByName( 35 | fileProtos: Seq[FileDescriptorProto] 36 | ): Map[String, FileDescriptor] = 37 | fileProtos.foldLeft[Map[String, FileDescriptor]](Map.empty) { 38 | case (acc, fp) => 39 | val deps = fp.getDependencyList.asScala.map(acc) 40 | acc + (fp.getName -> FileDescriptor.buildFrom(fp, deps.toArray)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /bridge/src/main/java/protocbridge/frontend/BridgeApp.java: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.net.Socket; 7 | 8 | public class BridgeApp { 9 | /** 10 | * Simple pure Java application that connects to TCP port (host is 127.0.0.1, port is first argument). 11 | * It redirects stdin and stdout to/from this socket. 12 | */ 13 | public static void main(String[] args) throws IOException { 14 | int port = Integer.parseInt(args[0]); 15 | try (Socket socket = new Socket("127.0.0.1", port)) { 16 | // read stdin and write it to the socket 17 | byte[] input = readInputStreamToByteArray(System.in); 18 | socket.getOutputStream().write(input); 19 | socket.shutdownOutput(); 20 | // read the socket and write bytes to stdout 21 | System.out.write(readInputStreamToByteArray(socket.getInputStream())); 22 | } 23 | } 24 | 25 | private static byte[] readInputStreamToByteArray(InputStream is) throws IOException { 26 | try(ByteArrayOutputStream b = new ByteArrayOutputStream()) { 27 | byte[] buffer = new byte[4096]; 28 | int count = 0; 29 | while (count != -1) { 30 | count = is.read(buffer); 31 | if (count > 0) { 32 | b.write(buffer, 0, count); 33 | } 34 | } 35 | return b.toByteArray(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/gens.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | object gens { 4 | // Prevent the organization name from getting shaded... 5 | // See https://github.com/scalapb/ScalaPB/issues/150 6 | private val JavaProtobufArtifact: String = 7 | "com+google+protobuf".replace('+', '.') 8 | 9 | val cpp = BuiltinGenerator("cpp") 10 | val csharp = BuiltinGenerator("csharp") 11 | val java: BuiltinGenerator = java("3.24.4") 12 | 13 | def java(runtimeVersion: String): BuiltinGenerator = 14 | BuiltinGenerator( 15 | "java", 16 | suggestedDependencies = 17 | Seq(Artifact(JavaProtobufArtifact, "protobuf-java", runtimeVersion)) 18 | ) 19 | 20 | def plugin(name: String): PluginGenerator = PluginGenerator(name, Nil, None) 21 | 22 | def plugin(name: String, path: String): PluginGenerator = 23 | PluginGenerator(name, Nil, Some(path)) 24 | 25 | val javanano = BuiltinGenerator("javanano") 26 | val kotlin: BuiltinGenerator = kotlin("3.24.4") 27 | def kotlin(runtimeVersion: String): BuiltinGenerator = 28 | BuiltinGenerator( 29 | "kotlin", 30 | suggestedDependencies = 31 | Seq(Artifact(JavaProtobufArtifact, "protobuf-kotlin", runtimeVersion)) 32 | ) 33 | 34 | val js = BuiltinGenerator("js") 35 | val objc = BuiltinGenerator("objc") 36 | val python = BuiltinGenerator("python") 37 | val ruby = BuiltinGenerator("ruby") 38 | val go = BuiltinGenerator("go") 39 | val swagger = BuiltinGenerator("swagger") 40 | val gateway = BuiltinGenerator("grpc-gateway") 41 | val descriptorSet = DescriptorSetGenerator() 42 | } 43 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/codegen/CodeGenRequest.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.codegen 2 | 3 | import scala.collection.JavaConverters._ 4 | 5 | import com.google.protobuf.Descriptors.FileDescriptor 6 | import com.google.protobuf.compiler.PluginProtos 7 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest 8 | import com.google.protobuf.DescriptorProtos.FileDescriptorProto 9 | 10 | @deprecated( 11 | "Use protocgen.CodeGenRequest from com.thesamet.scalapb:protocgen instead", 12 | "0.9.0" 13 | ) 14 | case class CodeGenRequest( 15 | parameter: String, 16 | filesToGenerate: Seq[FileDescriptor], 17 | allProtos: Seq[FileDescriptor], 18 | compilerVersion: Option[PluginProtos.Version] 19 | ) 20 | 21 | @deprecated( 22 | "Use protocgen.CodeGenRequest from com.thesamet.scalapb:protocgen instead", 23 | "0.9.0" 24 | ) 25 | object CodeGenRequest { 26 | def apply(req: CodeGeneratorRequest) = { 27 | val filesMap = fileDescriptorsByName( 28 | req.getProtoFileList().asScala.toVector 29 | ) 30 | new CodeGenRequest( 31 | parameter = req.getParameter(), 32 | filesToGenerate = 33 | req.getFileToGenerateList().asScala.toVector.map(filesMap), 34 | allProtos = filesMap.values.toVector, 35 | compilerVersion = 36 | if (req.hasCompilerVersion()) Some(req.getCompilerVersion()) else None 37 | ) 38 | } 39 | 40 | def fileDescriptorsByName( 41 | fileProtos: Seq[FileDescriptorProto] 42 | ): Map[String, FileDescriptor] = 43 | fileProtos.foldLeft[Map[String, FileDescriptor]](Map.empty) { 44 | case (acc, fp) => 45 | val deps = fp.getDependencyList.asScala.map(acc) 46 | acc + (fp.getName -> FileDescriptor.buildFrom(fp, deps.toArray)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/SystemDetector.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import java.{util => ju} 4 | 5 | object SystemDetector { 6 | private val UNKNOWN = "unknown" 7 | private val X86_64_RE = "^(x8664|amd64|ia32e|em64t|x64)$".r 8 | private val X86_32_RE = "^(x8632|x86|i[3-6]86|ia32|x32)$".r 9 | 10 | def normalizedOs(s: String): String = 11 | normalize(s) match { 12 | case p if p.startsWith("aix") => "aix" 13 | case p if p.startsWith("hpux") => "hpux" 14 | case p if p.startsWith("linux") => "linux" 15 | case p if p.startsWith("osx") || p.startsWith("macosx") => "osx" 16 | case p if p.startsWith("windows") => "windows" 17 | case p if p.startsWith("freebsd") => "freebsd" 18 | case p if p.startsWith("openbsd") => "openbsd" 19 | case p if p.startsWith("netbsd") => "netbsd" 20 | case _ => UNKNOWN 21 | } 22 | 23 | def normalizedArch(s: String): String = 24 | normalize(s) match { 25 | case X86_64_RE(_) => "x86_64" 26 | case X86_32_RE(_) => "x86_32" 27 | case "aarch64" => "aarch_64" 28 | case "ppc64le" => "ppcle_64" 29 | case "ppc64" => "ppc_64" 30 | case "s390x" => "s390x" 31 | case _ => UNKNOWN 32 | } 33 | 34 | def detectedClassifier(): String = { 35 | val osName = sys.props.getOrElse("os.name", "") 36 | val osArch = sys.props.getOrElse("os.arch", "") 37 | System.getProperty("os.name") 38 | normalizedOs(osName) + "-" + normalizedArch(osArch) 39 | } 40 | 41 | def normalize(s: String) = 42 | s.toLowerCase(ju.Locale.US).replaceAll("[^a-z0-9]+", "") 43 | } 44 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/frontend/SocketBasedPluginFrontend.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend 2 | 3 | import protocbridge.{ExtraEnv, ProtocCodeGenerator} 4 | 5 | import java.net.{InetAddress, ServerSocket} 6 | import java.nio.file.{Files, Path} 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | import scala.concurrent.{Future, blocking} 9 | 10 | /** PluginFrontend for Windows and macOS where a server socket is used. 11 | */ 12 | abstract class SocketBasedPluginFrontend extends PluginFrontend { 13 | 14 | case class InternalState(serverSocket: ServerSocket, shellScript: Path) 15 | 16 | override def prepare( 17 | plugin: ProtocCodeGenerator, 18 | env: ExtraEnv 19 | ): (Path, InternalState) = { 20 | 21 | /** we need to specify an address, otherwise it uses ANY (*.* in netstat) 22 | * which does not conflict with existing 'localhost' sockets on macos, 23 | * resulting in a conflict later on in MacPluginFrontend 24 | */ 25 | val ss = new ServerSocket(0, 50, InetAddress.getByName("127.0.0.1")) 26 | val sh = createShellScript(ss.getLocalPort) 27 | 28 | Future { 29 | blocking { 30 | // Accept a single client connection from the shell script. 31 | val client = ss.accept() 32 | try { 33 | val response = 34 | PluginFrontend.runWithInputStream( 35 | plugin, 36 | client.getInputStream, 37 | env 38 | ) 39 | client.getOutputStream.write(response) 40 | } finally { 41 | client.close() 42 | } 43 | } 44 | } 45 | 46 | (sh, InternalState(ss, sh)) 47 | } 48 | 49 | override def cleanup(state: InternalState): Unit = { 50 | state.serverSocket.close() 51 | if (sys.props.get("protocbridge.debug") != Some("1")) { 52 | Files.delete(state.shellScript) 53 | } 54 | } 55 | 56 | protected def createShellScript(port: Int): Path 57 | } 58 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/frontend/PluginFrontendSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend 2 | 3 | import java.io.ByteArrayInputStream 4 | 5 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse 6 | import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks 7 | import org.scalatest.flatspec.AnyFlatSpec 8 | import org.scalatest.matchers.must.Matchers 9 | import protocbridge.ExtraEnv 10 | 11 | class PluginFrontendSpec 12 | extends AnyFlatSpec 13 | with Matchers 14 | with ScalaCheckDrivenPropertyChecks { 15 | def expected(error: String) = 16 | CodeGeneratorResponse.newBuilder().setError(error).build() 17 | 18 | def actual(error: String) = 19 | CodeGeneratorResponse.parseFrom( 20 | PluginFrontend.createCodeGeneratorResponseWithError(error) 21 | ) 22 | 23 | "createCodeGeneratorResponseWithError" should "create valid objects" in { 24 | actual("") must be(expected("")) 25 | actual("foo") must be(expected("foo")) 26 | actual("\u2035") must be(expected("\u2035")) 27 | actual("a" * 128) must be(expected("a" * 128)) 28 | actual("a" * 256) must be(expected("a" * 256)) 29 | actual("\u3714\u3715" * 256) must be(expected("\u3714\u3715" * 256)) 30 | actual("abc" * 1000) must be(expected("abc" * 1000)) 31 | forAll(MinSuccessful(1000)) { (s: String) => 32 | actual(s) must be(expected(s)) 33 | } 34 | 35 | } 36 | 37 | "readInputStreamToByteArray" should "read the input stream to a byte array" in { 38 | val env = new ExtraEnv("foo") 39 | def readInput(bs: Array[Byte]) = 40 | PluginFrontend.readInputStreamToByteArrayWithEnv( 41 | new ByteArrayInputStream(bs), 42 | env 43 | ) 44 | 45 | readInput(Array.empty) must be(env.toByteArrayAsField) 46 | readInput(Array[Byte](1, 2, 3, 4)) must be( 47 | Array(1, 2, 3, 4) ++ env.toByteArrayAsField 48 | ) 49 | val special = Array.tabulate[Byte](10000) { n => 50 | (n % 37).toByte 51 | } 52 | readInput(special) must be(special ++ env.toByteArrayAsField) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/codegen/CodeGenApp.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.codegen 2 | 3 | import protocbridge.ProtocCodeGenerator 4 | import com.google.protobuf.ExtensionRegistry 5 | import com.google.protobuf.CodedInputStream 6 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest 7 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse 8 | import com.google.protobuf.Descriptors.FileDescriptor 9 | import com.google.protobuf.compiler.PluginProtos 10 | import com.google.protobuf.DescriptorProtos.FileDescriptorProto 11 | 12 | /** CodeGenApp provides a higher-level Scala API to build protoc code 13 | * generators. 14 | * 15 | * As a code generator author, you need to optionally provide a 16 | * `registerExtensions` function to register any extensions needed for parsing 17 | * the CodeGeneratorRequest. 18 | * 19 | * The implement the function process that takes a CodeGenRequest and returns a 20 | * CodeGenResponse. These classes provides higher-level, idiomatic access to 21 | * the request and response used by protoc. 22 | */ 23 | @deprecated( 24 | "Use protocgen.CodeGenApp from com.thesamet.scalapb:protocgen instead", 25 | "0.9.0" 26 | ) 27 | trait CodeGenApp extends ProtocCodeGenerator { 28 | def registerExtensions(registry: ExtensionRegistry): Unit = {} 29 | 30 | def process(request: CodeGenRequest): CodeGenResponse 31 | 32 | final def main(args: Array[String]): Unit = { 33 | System.out.write(run(CodedInputStream.newInstance(System.in))) 34 | } 35 | 36 | final override def run(req: Array[Byte]): Array[Byte] = 37 | run(CodedInputStream.newInstance(req)) 38 | 39 | final def run(input: CodedInputStream): Array[Byte] = { 40 | try { 41 | val registry = ExtensionRegistry.newInstance() 42 | registerExtensions(registry) 43 | val request = CodeGenRequest( 44 | CodeGeneratorRequest.parseFrom(input, registry) 45 | ) 46 | process(request).toCodeGeneratorResponse.toByteArray() 47 | } catch { 48 | case t: Throwable => 49 | CodeGeneratorResponse 50 | .newBuilder() 51 | .setError(t.toString) 52 | .build() 53 | .toByteArray 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/codegen/CodeGenAppSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.codegen 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.must.Matchers 5 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse 6 | import protocbridge.ProtocBridge 7 | import java.io.File 8 | import java.nio.file.Files 9 | import protocbridge.JvmGenerator 10 | import protocbridge.TestUtils.readLines 11 | import scala.annotation.nowarn 12 | import protocbridge.RunProtoc 13 | 14 | @nowarn("msg=(trait|class|object) CodeGen.*is deprecated") 15 | object TestCodeGenApp extends CodeGenApp { 16 | def process(request: CodeGenRequest): CodeGenResponse = { 17 | if (request.filesToGenerate.exists(_.getName().contains("error"))) 18 | CodeGenResponse.fail("Error!") 19 | else 20 | CodeGenResponse.succeed( 21 | Seq( 22 | CodeGeneratorResponse.File 23 | .newBuilder() 24 | .setName("out.out") 25 | .setContent("out!") 26 | .build() 27 | ) 28 | ) 29 | } 30 | } 31 | 32 | class CodeGenAppSpec extends AnyFlatSpec with Matchers { 33 | "protocbridge.TestCodeGenApp" should "succeed by default" in { 34 | val protoFile = 35 | new File(getClass.getResource("/test.proto").getFile).getAbsolutePath 36 | val protoDir = new File(getClass.getResource("/").getFile).getAbsolutePath 37 | val cgOutDir = Files.createTempDirectory("testout_cg").toFile() 38 | ProtocBridge.execute( 39 | RunProtoc, 40 | Seq( 41 | JvmGenerator("cg", TestCodeGenApp) -> cgOutDir 42 | ), 43 | Seq(protoFile, "-I", protoDir) 44 | ) must be(0) 45 | readLines(new File(cgOutDir, "out.out")) must be(Seq("out!")) 46 | } 47 | 48 | it should "fail on error.proto" in { 49 | val protoFile = 50 | new File(getClass.getResource("/error.proto").getFile).getAbsolutePath 51 | val protoDir = new File(getClass.getResource("/").getFile).getAbsolutePath 52 | val cgOutDir = Files.createTempDirectory("testout_cg").toFile() 53 | ProtocBridge.execute( 54 | RunProtoc, 55 | Seq( 56 | JvmGenerator("cg", TestCodeGenApp) -> cgOutDir 57 | ), 58 | Seq(protoFile, "-I", protoDir) 59 | ) must be(1) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /protoc-gen/src/main/scala/protocgen/CodeGenApp.scala: -------------------------------------------------------------------------------- 1 | package protocgen 2 | 3 | import protocbridge.ProtocCodeGenerator 4 | import com.google.protobuf.ExtensionRegistry 5 | import com.google.protobuf.CodedInputStream 6 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest 7 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse 8 | import com.google.protobuf.Descriptors.FileDescriptor 9 | import com.google.protobuf.compiler.PluginProtos 10 | import com.google.protobuf.DescriptorProtos.FileDescriptorProto 11 | 12 | /** CodeGenApp provides a higher-level Scala API to build protoc code 13 | * generators. 14 | * 15 | * As a code generator author, you need to optionally provide a 16 | * `registerExtensions` function to register any extensions needed for parsing 17 | * the CodeGeneratorRequest. 18 | * 19 | * The implement the function process that takes a CodeGenRequest and returns a 20 | * CodeGenResponse. These classes provides higher-level, idiomatic access to 21 | * the request and response used by protoc. 22 | */ 23 | trait CodeGenApp extends ProtocCodeGenerator { 24 | def registerExtensions(registry: ExtensionRegistry): Unit = {} 25 | 26 | def process(request: CodeGenRequest): CodeGenResponse 27 | 28 | final def main(args: Array[String]): Unit = { 29 | System.out.write(run(CodedInputStream.newInstance(System.in))) 30 | } 31 | 32 | final override def run(req: Array[Byte]): Array[Byte] = 33 | run(CodedInputStream.newInstance(req)) 34 | 35 | private def errorMessage(t: Throwable) = { 36 | val sw = new java.io.StringWriter() 37 | t.printStackTrace(new java.io.PrintWriter(sw, true)) 38 | sw.toString 39 | } 40 | 41 | final def run(input: CodedInputStream): Array[Byte] = { 42 | try { 43 | val registry = ExtensionRegistry.newInstance() 44 | registerExtensions(registry) 45 | val request = CodeGenRequest( 46 | CodeGeneratorRequest.parseFrom(input, registry) 47 | ) 48 | process(request).toCodeGeneratorResponse.toByteArray() 49 | } catch { 50 | case t: Throwable => 51 | CodeGeneratorResponse 52 | .newBuilder() 53 | .setError(errorMessage(t)) 54 | .build() 55 | .toByteArray 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /protoc-cache-coursier/src/main/scala/protocbridge/ProtocCacheCoursier.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import scala.concurrent.Future 4 | import java.io.File 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import scala.concurrent.Await 7 | import scala.concurrent.duration.Duration 8 | import coursier._ 9 | import coursier.core.Extension 10 | 11 | object CoursierProtocCache { 12 | lazy val cache: FileCache[Dependency] = 13 | new FileCache(FileCache.cacheDir, download, filenameFromKey) 14 | 15 | def getProtoc(version: String): File = 16 | Await.result(cache.get(protocDep(version)), Duration.Inf) 17 | 18 | def runProtoc( 19 | version: String, 20 | args: Seq[String], 21 | extraEnv: Seq[(String, String)] 22 | ): Int = { 23 | import sys.process._ 24 | 25 | val protoc = getProtoc(version).getAbsolutePath() 26 | 27 | val cmd = 28 | (ProtocRunner.maybeNixDynamicLinker(protoc).toSeq :+ protoc) ++ args 29 | Process(command = cmd, cwd = None, extraEnv: _*).! 30 | } 31 | 32 | private[this] def download(tmpDir: File, dep: Dependency): Future[File] = { 33 | Fetch() 34 | .addDependencies(dep) 35 | .future() 36 | .map( 37 | _.headOption.getOrElse( 38 | throw new RuntimeException(s"Could not find artifact for $dep") 39 | ) 40 | ) 41 | } 42 | 43 | private[this] def filenameFromKey(dep: Dependency) = { 44 | val ext = 45 | if (dep.publication.classifier.value.startsWith("win")) ".exe" else "" 46 | s"${dep.publication.name}-${dep.publication.classifier.value}-${dep.version}$ext" 47 | } 48 | 49 | private[this] def protocDep(version: String): Dependency = 50 | coursier.core 51 | .Dependency( 52 | coursier.core 53 | .Module( 54 | coursier.core.Organization("com.google.protobuf"), 55 | coursier.core.ModuleName("protoc"), 56 | Map.empty 57 | ), 58 | version 59 | ) 60 | .withPublication( 61 | "protoc", 62 | Type("jar"), 63 | Extension("exe"), 64 | Classifier(SystemDetector.detectedClassifier()) 65 | ) 66 | 67 | // For backwards binary compatibility 68 | private def runProtoc(version: String, args: Seq[String]): Int = 69 | runProtoc(version, args, Seq.empty) 70 | } 71 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/ProtoUtils.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import scala.collection.mutable.ArrayBuilder 4 | import com.google.protobuf.CodedInputStream 5 | import com.google.protobuf.CodedOutputStream 6 | 7 | object ProtoUtils { 8 | def writeRawVarint32(output: ArrayBuilder[Byte], value0: Int): Unit = { 9 | var value = value0 10 | while (true) { 11 | if ((value & ~0x7f) == 0) { 12 | output += value.toByte 13 | return 14 | } else { 15 | output += ((value & 0x7f) | 0x80).toByte 16 | value >>>= 7 17 | } 18 | } 19 | } 20 | 21 | def computeRawVarint32Size(value: Int): Int = { 22 | if ((value & (0xffffffff << 7)) == 0) return 1 23 | if ((value & (0xffffffff << 14)) == 0) return 2 24 | if ((value & (0xffffffff << 21)) == 0) return 3 25 | if ((value & (0xffffffff << 28)) == 0) return 4 26 | 5 27 | } 28 | 29 | def writeTag(b: ArrayBuilder[Byte], fieldNumber: Int, wireType: Int): Unit = { 30 | writeRawVarint32(b, makeTag(fieldNumber, wireType)) 31 | } 32 | 33 | def writeString( 34 | b: ArrayBuilder[Byte], 35 | fieldNumber: Int, 36 | value: String 37 | ): Unit = { 38 | writeTag(b, fieldNumber, WIRETYPE_LENGTH_DELIMITED) 39 | writeStringNoTag(b, value) 40 | } 41 | 42 | def writeBytes( 43 | b: ArrayBuilder[Byte], 44 | fieldNumber: Int, 45 | value: Array[Byte] 46 | ): Unit = { 47 | writeTag(b, fieldNumber, WIRETYPE_LENGTH_DELIMITED) 48 | writeBytesNoTag(b, value) 49 | } 50 | 51 | def writeBytesNoTag(b: ArrayBuilder[Byte], value: Array[Byte]) = { 52 | writeRawVarint32(b, value.length) 53 | b ++= value 54 | } 55 | 56 | def writeStringNoTag(b: ArrayBuilder[Byte], value: String): Unit = { 57 | val bytes = value.getBytes(UTF_8) 58 | writeBytesNoTag(b, bytes) 59 | } 60 | 61 | def computeTagSize(fieldNumber: Int): Int = 62 | computeRawVarint32Size(makeTag(fieldNumber, 0)) 63 | 64 | def computeStringSize(fieldNumber: Int, s: String): Int = { 65 | val sz = s.getBytes(UTF_8).length 66 | computeTagSize(fieldNumber) + computeRawVarint32Size(sz) + sz 67 | } 68 | 69 | val WIRETYPE_LENGTH_DELIMITED = 2 70 | val TAG_TYPE_BITS = 3 71 | 72 | def makeTag(fieldNumber: Int, wireType: Int) = 73 | (fieldNumber << TAG_TYPE_BITS) | wireType 74 | 75 | val UTF_8 = java.nio.charset.Charset.forName("UTF-8") 76 | } 77 | -------------------------------------------------------------------------------- /protoc-gen/src/test/scala/protocgen/CodeGenAppSpec.scala: -------------------------------------------------------------------------------- 1 | package protocgen 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.must.Matchers 5 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse 6 | import protocbridge.ProtocBridge 7 | import java.io.File 8 | import java.nio.file.Files 9 | import protocbridge.JvmGenerator 10 | import protocbridge.TestUtils.readLines 11 | import protocbridge.RunProtoc 12 | import protocbridge.ExtraEnv 13 | import protocbridge.ExtraEnvParser 14 | import protocbridge.frontend.PluginFrontend 15 | 16 | object TestCodeGenApp extends CodeGenApp { 17 | def process(request: CodeGenRequest): CodeGenResponse = { 18 | val env = ExtraEnvParser.fromCodeGeneratorRequest(request.asProto) 19 | assert(new File(env.secondaryOutputDir).isDirectory()) 20 | 21 | if (request.filesToGenerate.exists(_.getName().contains("error"))) 22 | CodeGenResponse.fail("Error!") 23 | else 24 | CodeGenResponse.succeed( 25 | Seq( 26 | CodeGeneratorResponse.File 27 | .newBuilder() 28 | .setName("out.out") 29 | .setContent("out!") 30 | .build(), 31 | CodeGeneratorResponse.File 32 | .newBuilder() 33 | .setName("env") 34 | .setContent(env.secondaryOutputDir.nonEmpty.toString()) 35 | .build() 36 | ) 37 | ) 38 | } 39 | } 40 | 41 | class CodeGenAppSpec extends AnyFlatSpec with Matchers { 42 | "protocgen.TestCodeGenApp" should "succeed by default" in { 43 | val protoFile = 44 | new File(getClass.getResource("/test.proto").getFile).getAbsolutePath 45 | val protoDir = new File(getClass.getResource("/").getFile).getAbsolutePath 46 | val cgOutDir = Files.createTempDirectory("testout_cg").toFile() 47 | ProtocBridge.execute( 48 | RunProtoc, 49 | Seq( 50 | JvmGenerator("cg", TestCodeGenApp) -> cgOutDir 51 | ), 52 | Seq(protoFile, "-I", protoDir), 53 | _ => ??? 54 | ) must be(0) 55 | readLines(new File(cgOutDir, "out.out")) must be(Seq("out!")) 56 | readLines(new File(cgOutDir, "env")) must be(Seq("true")) 57 | } 58 | 59 | "protocgen.TestCodeGenApp" should "fail on error.proto" in { 60 | val protoFile = 61 | new File(getClass.getResource("/error.proto").getFile).getAbsolutePath 62 | val protoDir = new File(getClass.getResource("/").getFile).getAbsolutePath 63 | val cgOutDir = Files.createTempDirectory("testout_cg").toFile() 64 | ProtocBridge.execute( 65 | RunProtoc, 66 | Seq( 67 | JvmGenerator("cg", TestCodeGenApp) -> cgOutDir 68 | ), 69 | Seq(protoFile, "-I", protoDir), 70 | _ => ??? 71 | ) must be(1) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.must.Matchers 5 | import protocbridge.{ExtraEnv, ProtocCodeGenerator} 6 | 7 | import java.io.ByteArrayOutputStream 8 | import scala.sys.process.ProcessIO 9 | import scala.util.Random 10 | 11 | class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers { 12 | 13 | protected def testPluginFrontend( 14 | frontend: PluginFrontend, 15 | generator: ProtocCodeGenerator, 16 | env: ExtraEnv, 17 | request: Array[Byte] 18 | ): (frontend.InternalState, Array[Byte]) = { 19 | val (path, state) = frontend.prepare( 20 | generator, 21 | env 22 | ) 23 | val actualOutput = new ByteArrayOutputStream() 24 | val process = sys.process 25 | .Process(path.toAbsolutePath.toString) 26 | .run( 27 | new ProcessIO( 28 | writeInput => { 29 | writeInput.write(request) 30 | writeInput.close() 31 | }, 32 | processOutput => { 33 | val buffer = new Array[Byte](4096) 34 | var bytesRead = 0 35 | while (bytesRead != -1) { 36 | bytesRead = processOutput.read(buffer) 37 | if (bytesRead != -1) { 38 | actualOutput.write(buffer, 0, bytesRead) 39 | } 40 | } 41 | processOutput.close() 42 | }, 43 | _.close() 44 | ) 45 | ) 46 | process.exitValue() 47 | frontend.cleanup(state) 48 | (state, actualOutput.toByteArray) 49 | } 50 | 51 | protected def testSuccess( 52 | frontend: PluginFrontend 53 | ): frontend.InternalState = { 54 | val random = new Random() 55 | val toSend = Array.fill(123)(random.nextInt(256).toByte) 56 | val toReceive = Array.fill(456)(random.nextInt(256).toByte) 57 | val env = new ExtraEnv(secondaryOutputDir = "tmp") 58 | 59 | val fakeGenerator = new ProtocCodeGenerator { 60 | override def run(request: Array[Byte]): Array[Byte] = { 61 | request mustBe (toSend ++ env.toByteArrayAsField) 62 | toReceive 63 | } 64 | } 65 | val (state, response) = 66 | testPluginFrontend(frontend, fakeGenerator, env, toSend) 67 | response mustBe toReceive 68 | state 69 | } 70 | 71 | protected def testFailure( 72 | frontend: PluginFrontend 73 | ): frontend.InternalState = { 74 | val random = new Random() 75 | val toSend = Array.fill(123)(random.nextInt(256).toByte) 76 | val env = new ExtraEnv(secondaryOutputDir = "tmp") 77 | 78 | val fakeGenerator = new ProtocCodeGenerator { 79 | override def run(request: Array[Byte]): Array[Byte] = { 80 | throw new OutOfMemoryError("test error") 81 | } 82 | } 83 | val (state, response) = 84 | testPluginFrontend(frontend, fakeGenerator, env, toSend) 85 | response.length must be > 0 86 | state 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/ProtocCacheSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.must.Matchers 5 | import scala.concurrent.Future 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import java.io.File 8 | import scala.concurrent.duration.Duration 9 | import java.nio.file.Files 10 | import scala.concurrent.Await 11 | import java.util.concurrent.atomic.AtomicInteger 12 | import org.scalatest.OneInstancePerTest 13 | import scala.concurrent.Promise 14 | 15 | class ProtocCacheSpec 16 | extends AnyFlatSpec 17 | with Matchers 18 | with OneInstancePerTest { 19 | val cacheDir = Files.createTempDirectory("protocache").toFile() 20 | val cache = 21 | new protocbridge.FileCache[String](cacheDir, downloadFile, v => v + ".exe") 22 | val callCount = new AtomicInteger(0) 23 | val p = Promise[Unit]() // used to unsuspend the downloader 24 | 25 | val existing = { 26 | new File(cacheDir, "existing.exe.complete").createNewFile() 27 | val f = new File(cacheDir, "existing.exe") 28 | f.createNewFile() 29 | f 30 | } 31 | 32 | def downloadFile(tmpDir: File, v: String): Future[File] = { 33 | callCount.incrementAndGet() 34 | p.future.map { _ => 35 | if (v == "error") throw new RuntimeException("error!") 36 | else { 37 | val out = new File(tmpDir, v + ".tmp") 38 | out.createNewFile() 39 | out 40 | } 41 | } 42 | } 43 | 44 | "cache" should "immediately return an existing file and make it executable" in { 45 | val c = Await.result(cache.get("existing"), Duration.Inf) 46 | c.canExecute() must be(true) 47 | c must be(existing) 48 | callCount.get must be(0) 49 | } 50 | 51 | "cache" should "make exactly one concurrent request" in { 52 | val fs = (0 to 20).map(_ => cache.get("somever")) 53 | p.success(()) 54 | val res = Await.result(Future.sequence(fs), Duration.Inf) 55 | callCount.get must be(1) 56 | } 57 | 58 | "cache" should "make exactly one concurrent request per key" in { 59 | val fs = (0 to 50).map(i => cache.get(s"somever${i % 3}")) 60 | p.success(()) 61 | val res = Await.result(Future.sequence(fs), Duration.Inf) 62 | callCount.get must be(3) 63 | new File(cacheDir, "somever0.exe").canExecute() must be(true) 64 | new File(cacheDir, "somever1.exe").canExecute() must be(true) 65 | new File(cacheDir, "somever2.exe").canExecute() must be(true) 66 | new File(cacheDir, "somever3.exe").canExecute() must be(false) 67 | Await.result(cache.get("somever1"), Duration.Inf).canExecute() must be(true) 68 | callCount.get must be(3) 69 | Await.result(cache.get("somever3"), Duration.Inf).canExecute() must be(true) 70 | callCount.get must be(4) 71 | } 72 | 73 | "cache" should "make exactly one concurrent request per key on errors" in { 74 | val fs = (0 to 50).map(_ => cache.get(s"error")) 75 | p.success(()) 76 | fs.foreach { f => 77 | intercept[RuntimeException](Await.result(f, Duration.Inf)) 78 | .getMessage() must be("error!") 79 | } 80 | callCount.get must be(1) 81 | // The next call should trigger another download: misses are not remembered. 82 | intercept[RuntimeException](Await.result(cache.get(s"error"), Duration.Inf)) 83 | callCount.get must be(2) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/TargetSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import org.scalatest._ 4 | 5 | import java.io.File 6 | import Target.builtin 7 | import org.scalatest.flatspec.AnyFlatSpec 8 | import org.scalatest.matchers.must.Matchers 9 | 10 | class TargetSpec extends AnyFlatSpec with Matchers { 11 | val TmpPath = new File("/tmp") 12 | 13 | object FoobarGen extends ProtocCodeGenerator { 14 | override def run(request: Array[Byte]): Array[Byte] = new Array[Byte](0) 15 | } 16 | 17 | def foobarGen(opt1: String, opt2: String): (Generator, Seq[String]) = 18 | (JvmGenerator("fff", FoobarGen), Seq(opt1, opt2)) 19 | 20 | "target" should "lift string to BuiltinGeneratorCall" in { 21 | Target(builtin("java"), TmpPath) must matchPattern { 22 | case Target(BuiltinGenerator("java", Nil), TmpPath, Nil) => 23 | } 24 | (builtin("java") -> TmpPath: Target) must matchPattern { 25 | case Target(BuiltinGenerator("java", Nil), TmpPath, Nil) => 26 | } 27 | } 28 | 29 | it should "allow passing options to string generator" in { 30 | Target(builtin("java", Seq("opt1", "opt2")), TmpPath) must matchPattern { 31 | case Target( 32 | BuiltinGenerator("java", Nil), 33 | TmpPath, 34 | Seq("opt1", "opt2") 35 | ) => 36 | } 37 | 38 | (builtin( 39 | "java", 40 | Seq("opt1", "opt2") 41 | ) -> TmpPath: Target) must matchPattern { 42 | case Target( 43 | BuiltinGenerator("java", Nil), 44 | TmpPath, 45 | Seq("opt1", "opt2") 46 | ) => 47 | } 48 | } 49 | 50 | it should "allow predefined builtin constants" in { 51 | Target(gens.java, TmpPath) must matchPattern { 52 | case Target( 53 | BuiltinGenerator( 54 | "java", 55 | List( 56 | Artifact("com.google.protobuf", "protobuf-java", _, false, _, _) 57 | ) 58 | ), 59 | TmpPath, 60 | Nil 61 | ) => 62 | } 63 | } 64 | 65 | it should "allow passing options to predefined plugins" in { 66 | Target(gens.java, TmpPath, Seq("ffx")) must matchPattern { 67 | case Target( 68 | BuiltinGenerator( 69 | "java", 70 | List( 71 | Artifact("com.google.protobuf", "protobuf-java", _, false, _, _) 72 | ) 73 | ), 74 | TmpPath, 75 | Seq("ffx") 76 | ) => 77 | } 78 | 79 | ((gens.java, Seq("ffx")) -> TmpPath: Target) must matchPattern { 80 | case Target( 81 | BuiltinGenerator( 82 | "java", 83 | List( 84 | Artifact("com.google.protobuf", "protobuf-java", _, false, _, _) 85 | ) 86 | ), 87 | TmpPath, 88 | Seq("ffx") 89 | ) => 90 | } 91 | } 92 | 93 | it should "allow using the options syntax" in { 94 | Target(foobarGen("xyz", "wf"), TmpPath) must matchPattern { 95 | case Target(JvmGenerator("fff", FoobarGen), TmpPath, Seq("xyz", "wf")) => 96 | } 97 | 98 | (foobarGen("xyz", "wf") -> TmpPath: Target) must matchPattern { 99 | case Target(JvmGenerator("fff", FoobarGen), TmpPath, Seq("xyz", "wf")) => 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/frontend/PosixPluginFrontend.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend 2 | 3 | import java.nio.file.{Files, Path} 4 | 5 | import protocbridge.ProtocCodeGenerator 6 | import protocbridge.ExtraEnv 7 | import java.nio.file.attribute.PosixFilePermission 8 | 9 | import scala.concurrent.blocking 10 | import scala.concurrent.Future 11 | import scala.concurrent.ExecutionContext.Implicits.global 12 | import scala.sys.process._ 13 | import java.{util => ju} 14 | 15 | /** PluginFrontend for Unix-like systems except macOS (Linux, FreeBSD, 16 | * etc) 17 | * 18 | * Creates a pair of named pipes for input/output and a shell script that 19 | * communicates with them. Compared with `SocketBasedPluginFrontend`, this 20 | * frontend doesn't rely on `nc` that might not be available in some 21 | * distributions. 22 | */ 23 | object PosixPluginFrontend extends PluginFrontend { 24 | case class InternalState( 25 | inputPipe: Path, 26 | outputPipe: Path, 27 | tempDir: Path, 28 | shellScript: Path 29 | ) 30 | 31 | override def prepare( 32 | plugin: ProtocCodeGenerator, 33 | env: ExtraEnv 34 | ): (Path, InternalState) = { 35 | val tempDirPath = Files.createTempDirectory("protopipe-") 36 | val inputPipe = createPipe(tempDirPath, "input") 37 | val outputPipe = createPipe(tempDirPath, "output") 38 | val sh = createShellScript(inputPipe, outputPipe) 39 | 40 | Future { 41 | blocking { 42 | val fsin = Files.newInputStream(inputPipe) 43 | val response = PluginFrontend.runWithInputStream(plugin, fsin, env) 44 | fsin.close() 45 | 46 | // Note that the output pipe must be opened after the input pipe is consumed. 47 | // Otherwise, there might be a deadlock that 48 | // - The shell script is stuck writing to the input pipe (which has a full buffer), 49 | // and doesn't open the write end of the output pipe. 50 | // - This thread is stuck waiting for the write end of the output pipe to be opened. 51 | val fsout = Files.newOutputStream(outputPipe) 52 | fsout.write(response) 53 | fsout.close() 54 | } 55 | } 56 | (sh, InternalState(inputPipe, outputPipe, tempDirPath, sh)) 57 | } 58 | 59 | override def cleanup(state: InternalState): Unit = { 60 | if (sys.props.get("protocbridge.debug") != Some("1")) { 61 | Files.delete(state.inputPipe) 62 | Files.delete(state.outputPipe) 63 | Files.delete(state.tempDir) 64 | Files.delete(state.shellScript) 65 | } 66 | } 67 | 68 | private def createPipe(tempDirPath: Path, name: String): Path = { 69 | val pipeName = tempDirPath.resolve(name) 70 | Seq("mkfifo", "-m", "600", pipeName.toAbsolutePath.toString).!! 71 | pipeName 72 | } 73 | 74 | private def createShellScript(inputPipe: Path, outputPipe: Path): Path = { 75 | val shell = sys.env.getOrElse("PROTOCBRIDGE_SHELL", "/bin/sh") 76 | val scriptName = PluginFrontend.createTempFile( 77 | "", 78 | s"""|#!$shell 79 | |set -e 80 | |cat /dev/stdin > "$inputPipe" 81 | |cat "$outputPipe" 82 | """.stripMargin 83 | ) 84 | val perms = new ju.HashSet[PosixFilePermission] 85 | perms.add(PosixFilePermission.OWNER_EXECUTE) 86 | perms.add(PosixFilePermission.OWNER_READ) 87 | Files.setPosixFilePermissions( 88 | scriptName, 89 | perms 90 | ) 91 | scriptName 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/FileCache.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import scala.concurrent.Future 4 | import dev.dirs.ProjectDirectories 5 | import java.util.concurrent.ConcurrentHashMap 6 | import scala.concurrent.Promise 7 | import java.io.{File, IOException} 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | import scala.util.Try 10 | import java.nio.file.{AccessDeniedException, Files, Path, StandardCopyOption} 11 | 12 | /** Cache for files that performs a single concurrent get per key upon miss. */ 13 | final class FileCache[K]( 14 | cacheDir: File, 15 | doGet: (File, K) => Future[File], 16 | keyAsFileName: K => String 17 | ) { 18 | private[protocbridge] val tasks = new ConcurrentHashMap[K, Promise[File]] 19 | 20 | def get(key: K): Future[File] = { 21 | val f = fileForKey(key) 22 | val cm = completeMarker(key) 23 | if (cm.exists() && f.exists()) { 24 | f.setExecutable(true) 25 | Future.successful(f) 26 | } else { 27 | val p = Promise[File]() 28 | val prev = tasks.putIfAbsent(key, p) 29 | if (prev == null) { 30 | // we are the first 31 | val tmpDir = Files.createTempDirectory("protocbridge").toFile 32 | doGet(tmpDir, key).map(copyToCache(_, f)).onComplete { 33 | (res: Try[File]) => 34 | FileCache.delete(tmpDir) 35 | if (res.isFailure) { tasks.remove(key) } 36 | else { completeMarker(key).createNewFile() } 37 | p.complete(res) 38 | } 39 | p.future 40 | } else { 41 | // discard the promise 42 | p.complete(null) 43 | prev.future 44 | } 45 | } 46 | } 47 | 48 | private[protocbridge] def copyToCache(src: File, dst: File): File = { 49 | val tmp = File.createTempFile("protocbridge", "tmp", dst.getParentFile()) 50 | val dstPath = dst.toPath() 51 | Files.copy(src.toPath(), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING) 52 | tmp.setExecutable(true) 53 | try { 54 | Files.move( 55 | tmp.toPath(), 56 | dstPath, 57 | StandardCopyOption.ATOMIC_MOVE, 58 | StandardCopyOption.REPLACE_EXISTING 59 | ) 60 | } catch { 61 | case e: AccessDeniedException => 62 | // On Windows sometimes atomic moves are impossible when destination 63 | // exists or in use, but (hopefully) we can silently ignore it since the file is 64 | // already there. 65 | if (!Files.isRegularFile(dstPath)) { 66 | throw new IOException( 67 | "File move failed and destination does not exist", 68 | e 69 | ) 70 | } 71 | } 72 | dst 73 | } 74 | 75 | private[protocbridge] def fileForKey(key: K): File = 76 | new File(cacheDir, keyAsFileName(key)) 77 | 78 | private[protocbridge] def completeMarker(key: K): File = 79 | new File(cacheDir, keyAsFileName(key) + ".complete") 80 | } 81 | 82 | object FileCache { 83 | def cacheDir: File = { 84 | val dir = sys.env 85 | .get("PROTOC_CACHE") 86 | .orElse(sys.props.get("protoc.cache")) 87 | .map(new File(_)) 88 | .getOrElse { 89 | new File( 90 | ProjectDirectories 91 | .from("com.thesamet.scalapb", "protocbridge", "protocbridge") 92 | .cacheDir, 93 | "v1" 94 | ) 95 | } 96 | dir.mkdirs() 97 | dir 98 | } 99 | 100 | private[protocbridge] def delete(dir: File): Unit = { 101 | Option(dir.listFiles).foreach(_.foreach(delete)) 102 | dir.delete() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/ProtocRunner.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import java.nio.file.Files 4 | import scala.io.Source 5 | import scala.sys.process.Process 6 | import scala.sys.process.ProcessLogger 7 | 8 | abstract class ProtocRunner[ExitCode] { 9 | self => 10 | def run(args: Seq[String], extraEnv: Seq[(String, String)]): ExitCode 11 | 12 | /** Returns a new ProtocRunner that is executed after this value. The exit 13 | * codes are combined into a tuple. 14 | */ 15 | def zip[E]( 16 | other: ProtocRunner[E] 17 | ): ProtocRunner[(ExitCode, E)] = ProtocRunner.fromFunction { 18 | (args, extraEnv) => (run(args, extraEnv), other.run(args, extraEnv)) 19 | } 20 | 21 | /** Returns a new ProtocRunner that maps the exit code of this runner. */ 22 | def map[E](f: ExitCode => E): ProtocRunner[E] = ProtocRunner.fromFunction { 23 | (args, extraEnv) => f(run(args, extraEnv)) 24 | } 25 | } 26 | 27 | object ProtocRunner { 28 | 29 | /** Makes a ProtocRunner that runs the given command line, but discards the 30 | * extra environment. Exists only for backwards compatility. 31 | */ 32 | private[protocbridge] def apply[A](f: Seq[String] => A): ProtocRunner[A] = 33 | new ProtocRunner[A] { 34 | def run(args: Seq[String], extraEnv: Seq[(String, String)]): A = f(args) 35 | } 36 | 37 | private[this] def detectedOs: String = 38 | SystemDetector.normalizedOs(SystemDetector.detectedClassifier()) 39 | 40 | def fromFunction[A]( 41 | f: (Seq[String], Seq[(String, String)]) => A 42 | ): ProtocRunner[A] = 43 | new ProtocRunner[A] { 44 | def run(args: Seq[String], extraEnv: Seq[(String, String)]): A = 45 | f(args, extraEnv) 46 | } 47 | 48 | def maybeNixDynamicLinker(): Option[String] = 49 | detectedOs match { 50 | case "linux" => 51 | sys.env.get("NIX_CC").map { nixCC => 52 | val source = Source.fromFile(nixCC + "/nix-support/dynamic-linker") 53 | val linker = source.mkString.trim() 54 | source.close() 55 | linker 56 | } 57 | case _ => 58 | None 59 | } 60 | 61 | // This version of maybeNixDynamicLinker() finds ld-linux and also uses it 62 | // to verify that the executable is dynamic. Newer version (>=3.23.0) of 63 | // protoc are static, and thus do not load with ld-linux. 64 | def maybeNixDynamicLinker(executable: String): Option[String] = 65 | maybeNixDynamicLinker().filter { linker => 66 | Process(command = Seq(linker, "--verify", executable)).! == 0 67 | } 68 | 69 | def apply(executable: String): ProtocRunner[Int] = ProtocRunner.fromFunction { 70 | case (args, extraEnv) => 71 | Process( 72 | command = 73 | (maybeNixDynamicLinker(executable).toSeq :+ executable) ++ args, 74 | cwd = None, 75 | extraEnv: _* 76 | ).! 77 | } 78 | 79 | // Transforms the given protoc runner to a new runner that writes the 80 | // options into a temporary file and passes the file to `protoc` as an `@` 81 | // parameter. 82 | def withParametersAsFile[T](underlying: ProtocRunner[T]): ProtocRunner[T] = 83 | fromFunction { (args, extraEnv) => 84 | { 85 | val argumentFile = Files.createTempFile("protoc-args-", ".txt") 86 | try { 87 | val writer = Files.newBufferedWriter(argumentFile) 88 | 89 | try { 90 | args.foreach { arg => 91 | writer.write(arg) 92 | writer.write('\n') 93 | } 94 | } finally { 95 | writer.close() 96 | } 97 | 98 | val fileArgument = s"@${argumentFile.toString}" 99 | underlying.run(Seq(fileArgument), extraEnv) 100 | } finally { 101 | Files.delete(argumentFile) 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/ExtraEnv.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import com.google.protobuf.CodedOutputStream 4 | import com.google.protobuf.WireFormat 5 | import com.google.protobuf.DescriptorProtos.FileDescriptorSet 6 | import com.google.protobuf.TextFormat 7 | import com.google.protobuf.Descriptors.FileDescriptor 8 | import com.google.protobuf.DescriptorProtos.FileDescriptorProto 9 | import com.google.protobuf.DynamicMessage 10 | import com.google.protobuf.Descriptors.Descriptor 11 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest 12 | 13 | /** Ad-hoc message-like class that represents extra environment available for 14 | * protoc code generators running through protocbridge. This message gets 15 | * appended by protocbridge to CodeGeneratorRequest so JVM plugins get access 16 | * to the environment. We do not generate Java or Scala code for this message 17 | * to prevent potential binary compatibility issues between the bridge and the 18 | * plugin. Instead, we serialize directly to bytes. Parsing is done in a 19 | * sandboxed environment, so we rely on DynamicMessage then. 20 | */ 21 | final class ExtraEnv(val secondaryOutputDir: String) { 22 | def toEnvMap: Map[String, String] = Map( 23 | ExtraEnv.ENV_SECONDARY_DIR -> secondaryOutputDir 24 | ) 25 | 26 | // Serializes this message as a field. Used to append it to a CodeGeneratorRequest 27 | private[protocbridge] def toByteArrayAsField: Array[Byte] = { 28 | val out = Array.newBuilder[Byte] 29 | ProtoUtils.writeTag(out, ExtraEnv.EXTRA_ENV_FIELD_NUMBER, 2) 30 | val sz = ProtoUtils.computeStringSize(1, secondaryOutputDir) 31 | ProtoUtils.writeRawVarint32(out, sz) 32 | ProtoUtils.writeString(out, 1, secondaryOutputDir) 33 | 34 | out.result() 35 | } 36 | 37 | override def toString(): String = 38 | s"ExtraEnv(secondaryOutputDir=$secondaryOutputDir)" 39 | } 40 | 41 | object ExtraEnv { 42 | val EXTRA_ENV_FIELD_NUMBER = 1020 // ScalaPB assigned extension number 43 | val ENV_SECONDARY_DIR = "SCALAPB_SECONDARY_OUTPUT_DIR" 44 | } 45 | 46 | // Seperated to a different object since it depends on protobuf-java and expected to be run 47 | // only in sandboxed environment. 48 | object ExtraEnvParser { 49 | def fromDynamicMessage(dm: DynamicMessage): ExtraEnv = { 50 | new ExtraEnv( 51 | dm.getField(extraEnvDescriptor.findFieldByNumber(1)).asInstanceOf[String] 52 | ) 53 | } 54 | 55 | def fromCodeGeneratorRequest(req: CodeGeneratorRequest): ExtraEnv = { 56 | val ll = req 57 | .getUnknownFields() 58 | .getField(ExtraEnv.EXTRA_ENV_FIELD_NUMBER) 59 | .getLengthDelimitedList 60 | if (ll.size() == 0) new ExtraEnv("") 61 | else 62 | fromDynamicMessage( 63 | DynamicMessage.parseFrom( 64 | extraEnvDescriptor, 65 | ll.get(0) 66 | ) 67 | ) 68 | } 69 | 70 | private val (extraEnvDescriptor, request): (Descriptor, Descriptor) = { 71 | val proto = TextFormat.parse( 72 | s""" 73 | |message_type { 74 | | name: "ExtraEnv" 75 | | field { 76 | | name: "secondary_output_dir" 77 | | number: 1 78 | | label: LABEL_OPTIONAL 79 | | type: TYPE_STRING 80 | | json_name: "secondaryOutputDir" 81 | | } 82 | |} 83 | | 84 | |message_type { 85 | | name: "Request" 86 | | field { 87 | | name: "extra_env" 88 | | number: ${ExtraEnv.EXTRA_ENV_FIELD_NUMBER} 89 | | label: LABEL_OPTIONAL 90 | | type: TYPE_MESSAGE 91 | | type_name: ".ExtraEnv" 92 | | json_name: "extraEnv" 93 | | } 94 | |} 95 | |""".stripMargin, 96 | classOf[FileDescriptorProto] 97 | ) 98 | val fd = FileDescriptor 99 | .buildFrom(proto, Array.empty) 100 | 101 | (fd.findMessageTypeByName("ExtraEnv"), fd.findMessageTypeByName("Request")) 102 | } 103 | private val secondaryOutputFieldDescriptor = 104 | extraEnvDescriptor.findFieldByName("secondary_output_dir") 105 | } 106 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/Generator.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | /** Represents a code generator invocation */ 4 | sealed trait Generator { 5 | def name: String 6 | 7 | def suggestedDependencies: Seq[Artifact] 8 | } 9 | 10 | /** Represents a generator built into protoc, to be used with a directory 11 | * target. 12 | */ 13 | final case class BuiltinGenerator( 14 | name: String, 15 | suggestedDependencies: Seq[Artifact] = Nil 16 | ) extends Generator 17 | 18 | final case class PluginGenerator( 19 | name: String, 20 | suggestedDependencies: Seq[Artifact], 21 | path: Option[String] 22 | ) extends Generator 23 | 24 | /** Represents a generator built into protoc, to be used with a file target. */ 25 | final case class DescriptorSetGenerator() extends Generator { 26 | override val name = "descriptor_set" 27 | override val suggestedDependencies = Nil 28 | } 29 | 30 | /** Represents a generator implemented by ProtocCodeGenerator. */ 31 | final case class JvmGenerator(name: String, gen: ProtocCodeGenerator) 32 | extends Generator { 33 | def suggestedDependencies: Seq[Artifact] = gen.suggestedDependencies 34 | } 35 | 36 | /** Represents a JvmGenerator that needs to be dynamically loaded from an 37 | * artifact. This allows to run each JvmGenerator in its own classloader and 38 | * thus avoid dependency conflicts between plugins or between plugins and the 39 | * container (such as sbt). 40 | * 41 | * This mechanism is needed because SBT ships with an old version of 42 | * protobuf-java that is not binary compatible with recent versions. In 43 | * addition, SBT depends on ScalaPB's runtime, so ScalaPB plugins can't use 44 | * ScalaPB itself without running a risk of conflict. 45 | * 46 | * artifact: Artifact containing the generator class. resolver: Using a 47 | * ClassLoader, return a new instance of a ProtocCodeGenerator. 48 | */ 49 | final case class SandboxedJvmGenerator private ( 50 | name: String, 51 | artifact: Artifact, 52 | suggestedDependencies: Seq[Artifact], 53 | resolver: ClassLoader => ProtocCodeGenerator 54 | ) extends Generator { 55 | private[protocbridge] def this( 56 | name: String, 57 | artifact: Artifact, 58 | generatorClass: String, 59 | suggestedDependencies: Seq[Artifact] 60 | ) = 61 | this( 62 | name, 63 | artifact, 64 | suggestedDependencies, 65 | SandboxedJvmGenerator.load(generatorClass, _) 66 | ) 67 | } 68 | 69 | object SandboxedJvmGenerator { 70 | 71 | /** Instantiates a SandboxedJvmGenerator that loads an object named 72 | * generatorClass 73 | */ 74 | def forModule( 75 | name: String, 76 | artifact: Artifact, 77 | generatorClass: String, 78 | suggestedDependencies: Seq[Artifact] 79 | ): SandboxedJvmGenerator = 80 | SandboxedJvmGenerator( 81 | name, 82 | artifact, 83 | suggestedDependencies, 84 | SandboxedJvmGenerator.load(generatorClass, _) 85 | ) 86 | 87 | /** Instantiates a SandboxedJvmGenerator that uses a class loader to load a 88 | * generator 89 | */ 90 | def forResolver( 91 | name: String, 92 | artifact: Artifact, 93 | suggestedDependencies: Seq[Artifact], 94 | resolver: ClassLoader => ProtocCodeGenerator 95 | ): SandboxedJvmGenerator = 96 | SandboxedJvmGenerator( 97 | name: String, 98 | artifact: Artifact, 99 | suggestedDependencies: Seq[Artifact], 100 | resolver: ClassLoader => ProtocCodeGenerator 101 | ) 102 | 103 | // kept for binary compatiblity with 0.9.0-RC1 104 | private[this] def apply( 105 | name: String, 106 | artifact: Artifact, 107 | generatorClass: String, 108 | suggestedDependencies: Seq[Artifact] 109 | ): SandboxedJvmGenerator = 110 | forModule( 111 | name, 112 | artifact, 113 | generatorClass, 114 | suggestedDependencies 115 | ) 116 | 117 | def load( 118 | generatorClass: String, 119 | loader: ClassLoader 120 | ): ProtocCodeGenerator = { 121 | val cls = loader.loadClass(generatorClass) 122 | val module = cls.getField("MODULE$").get(null) 123 | val runMethod = module.getClass().getMethod("run", classOf[Array[Byte]]) 124 | new ProtocCodeGenerator { 125 | def run(request: Array[Byte]): Array[Byte] = 126 | runMethod.invoke(module, request).asInstanceOf[Array[Byte]] 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/frontend/PluginFrontend.scala: -------------------------------------------------------------------------------- 1 | package protocbridge.frontend 2 | 3 | import java.io.{ByteArrayOutputStream, InputStream, PrintWriter, StringWriter} 4 | import java.nio.file.{Files, Path} 5 | 6 | import protocbridge.{ProtocCodeGenerator, ExtraEnv} 7 | 8 | /** A PluginFrontend instance provides a platform-dependent way for protoc to 9 | * communicate with a JVM based ProtocCodeGenerator. 10 | * 11 | * protoc is able to launch plugins. Plugins are executables that take a 12 | * serialized CodeGenerationRequest via stdin and serialize a 13 | * CodeGenerationRequest to stdout. The idea in PluginFrontend is to create a 14 | * minimal plugin that wires its stdin/stdout to this JVM. 15 | * 16 | * The two-way communication always goes as follows: 17 | * 18 | * 1. protoc writes a request to the stdin of a plugin 2. plugin writes the 19 | * data to the channel 3. this JVM reads it, interprets it as 20 | * CodeGenerationRequest and process it. 4. this JVM writes a 21 | * CodeGenerationResponse to the channel 5. this JVM closes the channel. 22 | * 6. the plugin reads the data and writes it to standard out. 7. protoc 23 | * handles the CodeGenerationResponse (creates Scala sources) 24 | */ 25 | trait PluginFrontend { 26 | type InternalState 27 | 28 | // Notifies the frontend to set up a protoc plugin that runs the given generator. It returns 29 | // the system path of the executable and an arbitary internal state object that is passed 30 | // later. Useful for cleanup. 31 | def prepare(plugin: ProtocCodeGenerator, env: ExtraEnv): (Path, InternalState) 32 | 33 | def cleanup(state: InternalState): Unit 34 | } 35 | 36 | object PluginFrontend { 37 | private def getStackTrace(e: Throwable): String = { 38 | val stringWriter = new StringWriter 39 | val printWriter = new PrintWriter(stringWriter) 40 | e.printStackTrace(printWriter) 41 | stringWriter.toString 42 | } 43 | 44 | def runWithBytes( 45 | gen: ProtocCodeGenerator, 46 | request: Array[Byte] 47 | ): Array[Byte] = { 48 | gen.run(request) 49 | } 50 | 51 | def createCodeGeneratorResponseWithError(error: String): Array[Byte] = { 52 | val b = Array.newBuilder[Byte] 53 | 54 | def addRawVarint32(value0: Int): Unit = { 55 | var value = value0 56 | while (true) { 57 | if ((value & ~0x7f) == 0) { 58 | b += value.toByte 59 | return 60 | } else { 61 | b += ((value & 0x7f) | 0x80).toByte 62 | value >>>= 7 63 | } 64 | } 65 | } 66 | 67 | b += 10 68 | val errorBytes = error.getBytes(java.nio.charset.Charset.forName("UTF-8")) 69 | var length = errorBytes.length 70 | addRawVarint32(length) 71 | b ++= errorBytes 72 | b.result() 73 | } 74 | 75 | @deprecated("This method is going to be removed.", "0.9.0") 76 | def readInputStreamToByteArray(fsin: InputStream): Array[Byte] = { 77 | val b = new ByteArrayOutputStream() 78 | val buffer = new Array[Byte](4096) 79 | var count = 0 80 | while (count != -1) { 81 | count = fsin.read(buffer) 82 | if (count > 0) { 83 | b.write(buffer, 0, count) 84 | } 85 | } 86 | b.toByteArray 87 | } 88 | 89 | private[protocbridge] def readInputStreamToByteArrayWithEnv( 90 | fsin: InputStream, 91 | env: ExtraEnv 92 | ): Array[Byte] = { 93 | val b = new ByteArrayOutputStream() 94 | val buffer = new Array[Byte](4096) 95 | var count = 0 96 | while (count != -1) { 97 | count = fsin.read(buffer) 98 | if (count > 0) { 99 | b.write(buffer, 0, count) 100 | } 101 | } 102 | val envBytes = env.toByteArrayAsField 103 | b.write(envBytes, 0, envBytes.length) 104 | b.toByteArray 105 | } 106 | 107 | def runWithInputStream( 108 | gen: ProtocCodeGenerator, 109 | fsin: InputStream, 110 | env: ExtraEnv 111 | ): Array[Byte] = try { 112 | val bytes = readInputStreamToByteArrayWithEnv(fsin, env) 113 | runWithBytes(gen, bytes) 114 | } catch { 115 | // This covers all Throwable including OutOfMemoryError, StackOverflowError, etc. 116 | // We need to make a best effort to return a response to protoc, 117 | // otherwise protoc can hang indefinitely. 118 | case throwable: Throwable => 119 | createCodeGeneratorResponseWithError( 120 | throwable.toString + "\n" + getStackTrace(throwable) 121 | ) 122 | } 123 | 124 | def createTempFile(extension: String, content: String): Path = { 125 | val fileName = Files.createTempFile("protocbridge", extension) 126 | val os = Files.newOutputStream(fileName) 127 | os.write(content.getBytes("UTF-8")) 128 | os.close() 129 | fileName 130 | } 131 | 132 | def isWindows: Boolean = sys.props("os.name").startsWith("Windows") 133 | 134 | def isMac: Boolean = sys.props("os.name").startsWith("Mac") || sys 135 | .props("os.name") 136 | .startsWith("Darwin") 137 | 138 | def newInstance: PluginFrontend = { 139 | if (isWindows) WindowsPluginFrontend 140 | else if (isMac) MacPluginFrontend 141 | else PosixPluginFrontend 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/ProtocIntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import java.io.File 4 | import java.nio.file.Files 5 | import java.util.concurrent.Executors 6 | 7 | import com.google.protobuf.Descriptors.FileDescriptor 8 | import com.google.protobuf.compiler.PluginProtos.{ 9 | CodeGeneratorRequest, 10 | CodeGeneratorResponse 11 | } 12 | 13 | import scala.concurrent.duration.{Duration, SECONDS} 14 | import scala.concurrent.{Await, ExecutionContext, Future, blocking} 15 | import scala.io.Source 16 | import org.scalatest.flatspec.AnyFlatSpec 17 | import org.scalatest.matchers.must.Matchers 18 | import TestUtils.readLines 19 | import protocbridge.frontend.PluginFrontend 20 | 21 | object TestJvmPlugin extends ProtocCodeGenerator { 22 | 23 | import scala.jdk.CollectionConverters._ 24 | 25 | override def run(in: Array[Byte]): Array[Byte] = { 26 | val request = CodeGeneratorRequest.parseFrom(in) 27 | 28 | val filesByName: Map[String, FileDescriptor] = 29 | request.getProtoFileList.asScala 30 | .foldLeft[Map[String, FileDescriptor]](Map.empty) { case (acc, fp) => 31 | val deps = fp.getDependencyList.asScala.map(acc) 32 | acc + (fp.getName -> FileDescriptor.buildFrom(fp, deps.toArray)) 33 | } 34 | 35 | val content = (for { 36 | fileName <- request.getFileToGenerateList.asScala 37 | file = filesByName(fileName) 38 | msg <- file.getMessageTypes().asScala 39 | } yield (file.getName + ":" + msg.getFullName)).mkString("\n") 40 | 41 | val responseBuilder = CodeGeneratorResponse.newBuilder() 42 | responseBuilder 43 | .addFileBuilder() 44 | .setContent(content) 45 | .setName("msglist.txt") 46 | responseBuilder 47 | .addFileBuilder() 48 | .setContent( 49 | if (request.getParameter().isEmpty()) "Empty" 50 | else request.getParameter() 51 | ) 52 | .setName("parameters.txt") 53 | 54 | responseBuilder.build().toByteArray 55 | } 56 | } 57 | 58 | object TestUtils { 59 | def readLines(file: File) = { 60 | val s = Source.fromFile(file) 61 | try { 62 | Source.fromFile(file).getLines().toVector 63 | } finally { 64 | s.close() 65 | } 66 | } 67 | } 68 | 69 | class ProtocIntegrationSpec extends AnyFlatSpec with Matchers { 70 | def invokeProtocProperly(runner: ProtocRunner[Int]) = { 71 | val protoFile = 72 | new File(getClass.getResource("/test.proto").getFile).getAbsolutePath 73 | val protoDir = new File(getClass.getResource("/").getFile).getAbsolutePath 74 | 75 | val javaOutDir = Files.createTempDirectory("javaout").toFile() 76 | val testOutDirs = 77 | (0 to 4).map(i => Files.createTempDirectory(s"testout$i").toFile()) 78 | 79 | ProtocBridge.execute( 80 | runner, 81 | Seq( 82 | protocbridge.gens.java("3.8.0") -> javaOutDir, 83 | TestJvmPlugin -> testOutDirs(0), 84 | TestJvmPlugin -> testOutDirs(1), 85 | JvmGenerator("foo", TestJvmPlugin) -> testOutDirs(2), 86 | ( 87 | JvmGenerator("foo", TestJvmPlugin), 88 | Seq("foo", "bar:value", "baz=qux") 89 | ) -> testOutDirs(3), 90 | JvmGenerator("bar", TestJvmPlugin) -> testOutDirs(4) 91 | ), 92 | Seq(protoFile, "-I", protoDir) 93 | ) must be(0) 94 | 95 | Files.exists( 96 | javaOutDir.toPath.resolve("mytest").resolve("Test.java") 97 | ) must be( 98 | true 99 | ) 100 | 101 | testOutDirs.foreach { testOutDir => 102 | val expected = Seq( 103 | "test.proto:mytest.TestMsg", 104 | "test.proto:mytest.AnotherMsg" 105 | ) 106 | readLines(new File(testOutDir, "msglist.txt")) must be(expected) 107 | } 108 | readLines(new File(testOutDirs(3), "parameters.txt")) must be( 109 | Seq("foo,bar:value,baz=qux") 110 | ) 111 | readLines(new File(testOutDirs(0), "parameters.txt")) must be(Seq("Empty")) 112 | } 113 | 114 | "ProtocBridge.run" should "invoke JVM and Java plugin properly" in { 115 | invokeProtocProperly(RunProtoc) 116 | } 117 | 118 | "ProtocBridge.run" should "invoke JVM and Java plugin properly with options file" in { 119 | invokeProtocProperly(ProtocRunner.withParametersAsFile(RunProtoc)) 120 | } 121 | 122 | it should "not deadlock for highly concurrent invocations" in { 123 | val availableProcessors = Runtime.getRuntime.availableProcessors 124 | assert( 125 | availableProcessors > 1, 126 | "Several vCPUs needed for the test to be relevant" 127 | ) 128 | 129 | val parallelProtocInvocations = availableProcessors * 8 130 | val generatorsByInvocation = availableProcessors * 8 131 | 132 | val protoFile = 133 | new File(getClass.getResource("/test.proto").getFile).getAbsolutePath 134 | val protoDir = new File(getClass.getResource("/").getFile).getAbsolutePath 135 | 136 | implicit val ec = ExecutionContext.fromExecutorService( 137 | Executors.newFixedThreadPool(parallelProtocInvocations) 138 | ) 139 | 140 | val invocations = List.fill(parallelProtocInvocations) { 141 | Future( 142 | blocking( 143 | ProtocBridge.execute( 144 | RunProtoc, 145 | List.fill(generatorsByInvocation)( 146 | Target( 147 | JvmGenerator("foo", TestJvmPlugin), 148 | Files.createTempDirectory(s"foo").toFile 149 | ) 150 | ), 151 | Seq(protoFile, "-I", protoDir) 152 | ) 153 | ) 154 | ) 155 | } 156 | 157 | Await.result( 158 | Future.sequence(invocations), 159 | Duration(60, SECONDS) 160 | ) must be(List.fill(parallelProtocInvocations)(0)) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /bridge/src/test/scala/protocbridge/ProtocBridgeSpec.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import org.scalatest._ 4 | import java.io.File 5 | import java.util.regex.Pattern 6 | import org.scalatest.flatspec.AnyFlatSpec 7 | import org.scalatest.matchers.must.Matchers 8 | 9 | object FoobarGen extends ProtocCodeGenerator { 10 | override def run(request: Array[Byte]): Array[Byte] = new Array[Byte](0) 11 | } 12 | 13 | class ProtocBridgeSpec extends AnyFlatSpec with Matchers { 14 | val TmpPath = new File("/tmp").getAbsoluteFile 15 | val TmpPath1 = new File("/tmp/x").getAbsoluteFile 16 | val TmpPath2 = new File("/tmp/y").getAbsoluteFile 17 | 18 | object TestFrontend extends frontend.PluginFrontend { 19 | type InternalState = Unit 20 | 21 | // Notifies the frontend to set up a protoc plugin that runs the given generator. It returns 22 | // the system path of the executable and an arbitary internal state object that is passed 23 | // later. Useful for cleanup. 24 | def prepare( 25 | plugin: ProtocCodeGenerator, 26 | env: ExtraEnv 27 | ): (java.nio.file.Path, InternalState) = (null, ()) 28 | 29 | def cleanup(state: InternalState): Unit = {} 30 | } 31 | 32 | def foobarGen(opt1: String, opt2: String): (Generator, Seq[String]) = 33 | (JvmGenerator("fff", FoobarGen), Seq(opt1, opt2)) 34 | 35 | def sandboxedGen(opts: String*): (Generator, Seq[String]) = 36 | ( 37 | SandboxedJvmGenerator.forModule( 38 | "sandboxed", 39 | Artifact("group", "id", "version"), 40 | "protocbridge.FoobarGen$", 41 | Nil 42 | ), 43 | opts 44 | ) 45 | 46 | def run(targets: Seq[Target], params: Seq[String] = Seq.empty) = 47 | ProtocBridge.execute( 48 | ProtocRunner(args => args), 49 | targets, 50 | params, 51 | TestFrontend, 52 | _ => getClass.getClassLoader 53 | ) 54 | 55 | "run" should "pass params when there are no targets" in { 56 | run(Seq.empty, Seq.empty) must be(Seq.empty) 57 | run(Seq.empty, Seq("-x", "-y")) must be(Seq("-x", "-y")) 58 | } 59 | 60 | "run" should "allow string args for string generators" in { 61 | run(Seq(Target(Target.builtin("java"), TmpPath))) must be( 62 | Seq(s"--java_out=$TmpPath") 63 | ) 64 | run( 65 | Seq(Target(Target.builtin("java", Seq("x", "y", "z")), TmpPath)) 66 | ) must be( 67 | Seq(s"--java_out=$TmpPath", "--java_opt=x,y,z") 68 | ) 69 | } 70 | 71 | it should "pass builtin targets correctly" in { 72 | run(Seq(Target(gens.java, TmpPath))) must be(Seq(s"--java_out=$TmpPath")) 73 | run(Seq(Target(gens.java, TmpPath, Seq("x", "y")))) must be( 74 | Seq(s"--java_out=$TmpPath", "--java_opt=x,y") 75 | ) 76 | } 77 | 78 | it should "pass external plugins correctly" in { 79 | 80 | // ensure no suffix is added if we have only one native, path-less generator with the same name to maintain backward 81 | // compatibility with <= 0.9.1 clients that assume that this native generator passed once would not be suffixed, 82 | // and therefore could declare that plugin path with the exact name used by the generator passed in the target 83 | // https://github.com/thesamet/sbt-protoc/blob/v1.0.0/src/main/scala/sbtprotoc/ProtocPlugin.scala#L515 84 | run(Seq(Target(gens.plugin("foo"), TmpPath))) must be( 85 | Seq(s"--foo_out=$TmpPath") 86 | ) 87 | 88 | run( 89 | Seq( 90 | Target(gens.plugin("foo"), TmpPath), 91 | Target(gens.plugin("bar"), TmpPath2) 92 | ) 93 | ) must be(Seq(s"--foo_out=$TmpPath", s"--bar_out=$TmpPath2")) 94 | 95 | run( 96 | Seq( 97 | Target(gens.plugin("foo", "/path/to/plugin"), TmpPath), 98 | Target(gens.plugin("bar"), TmpPath2) 99 | ) 100 | ) must be( 101 | Seq( 102 | "--plugin=protoc-gen-foo_0=/path/to/plugin", 103 | s"--foo_0_out=$TmpPath", 104 | s"--bar_out=$TmpPath2" 105 | ) 106 | ) 107 | 108 | run( 109 | Seq( 110 | Target(gens.plugin("foo", "/path/to/plugin"), TmpPath, Seq("w")), 111 | Target(gens.plugin("foo", "/path/to/plugin"), TmpPath, Seq("x")), 112 | Target(gens.plugin("foo", "/otherpath/to/plugin"), TmpPath1, Seq("y")), 113 | Target(gens.plugin("foo", "/otherpath/to/plugin"), TmpPath2, Seq("y")), 114 | Target(gens.plugin("foo"), TmpPath2, Seq("z")), 115 | Target(gens.plugin("bar"), TmpPath) 116 | ) 117 | ) must be( 118 | Seq( 119 | "--plugin=protoc-gen-foo_0=/path/to/plugin", 120 | "--plugin=protoc-gen-foo_1=/path/to/plugin", 121 | "--plugin=protoc-gen-foo_2=/otherpath/to/plugin", 122 | "--plugin=protoc-gen-foo_3=/otherpath/to/plugin", 123 | s"--foo_0_out=$TmpPath", 124 | s"--foo_0_opt=w", 125 | s"--foo_1_out=$TmpPath", 126 | s"--foo_1_opt=x", 127 | s"--foo_2_out=$TmpPath1", 128 | s"--foo_2_opt=y", 129 | s"--foo_3_out=$TmpPath2", 130 | s"--foo_3_opt=y", 131 | s"--foo_4_out=$TmpPath2", 132 | s"--foo_4_opt=z", 133 | s"--bar_out=$TmpPath" 134 | ) 135 | ) 136 | } 137 | 138 | val DefineFlag = "--plugin=protoc-gen-(.*?)=null".r 139 | val UseFlag = s"--(.*?)_out=${Pattern.quote(TmpPath.toString)}".r 140 | val UseFlagParams = s"--(.*?)_opt=x,y".r 141 | 142 | it should "allow using FooBarGen" in { 143 | run(Seq(Target(FoobarGen, TmpPath))) match { 144 | case Seq(DefineFlag(r), UseFlag(s)) if r == s => 145 | } 146 | 147 | run(Seq(Target(FoobarGen, TmpPath, Seq("x", "y")))) match { 148 | case Seq(DefineFlag(r), UseFlag(s), UseFlagParams(o)) 149 | if r == s && r == o => 150 | } 151 | } 152 | 153 | it should "allow using fooBarGen multiple times" in { 154 | run( 155 | Seq( 156 | Target(foobarGen("x", "y"), TmpPath), 157 | FoobarGen -> TmpPath1, 158 | Target(foobarGen("foo", "bar"), TmpPath2) 159 | ) 160 | ) must be( 161 | Seq( 162 | "--plugin=protoc-gen-fff_0=null", 163 | "--plugin=protoc-gen-jvm_1=null", 164 | "--plugin=protoc-gen-fff_2=null", 165 | s"--fff_0_out=$TmpPath", 166 | s"--fff_0_opt=x,y", 167 | s"--jvm_1_out=$TmpPath1", 168 | s"--fff_2_out=$TmpPath2", 169 | s"--fff_2_opt=foo,bar" 170 | ) 171 | ) 172 | } 173 | 174 | it should "allow using sandboxedGen multiple times" in { 175 | run( 176 | Seq( 177 | Target(sandboxedGen("x", "y"), TmpPath1), 178 | Target(sandboxedGen("foo", "bar"), TmpPath2) 179 | ) 180 | ) must be( 181 | Seq( 182 | "--plugin=protoc-gen-jvm_0=null", 183 | "--plugin=protoc-gen-jvm_1=null", 184 | s"--jvm_0_out=$TmpPath1", 185 | s"--jvm_0_opt=x,y", 186 | s"--jvm_1_out=$TmpPath2", 187 | s"--jvm_1_opt=foo,bar" 188 | ) 189 | ) 190 | } 191 | 192 | it should "preserve the order of targets" in { 193 | run( 194 | Seq( 195 | Target(sandboxedGen("sandboxed"), TmpPath1), 196 | Target(foobarGen("foo", "bar"), TmpPath2), 197 | FoobarGen -> TmpPath, 198 | Target(sandboxedGen("x", "y"), TmpPath2) 199 | ) 200 | ) must be( 201 | Seq( 202 | "--plugin=protoc-gen-jvm_0=null", 203 | "--plugin=protoc-gen-fff_1=null", 204 | "--plugin=protoc-gen-jvm_2=null", 205 | "--plugin=protoc-gen-jvm_3=null", 206 | s"--jvm_0_out=$TmpPath1", 207 | s"--jvm_0_opt=sandboxed", 208 | s"--fff_1_out=$TmpPath2", 209 | s"--fff_1_opt=foo,bar", 210 | s"--jvm_2_out=$TmpPath", 211 | s"--jvm_3_out=$TmpPath2", 212 | s"--jvm_3_opt=x,y" 213 | ) 214 | ) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /bridge/src/main/scala/protocbridge/ProtocBridge.scala: -------------------------------------------------------------------------------- 1 | package protocbridge 2 | 3 | import java.io.File 4 | import java.nio.file.Path 5 | 6 | import protocbridge.frontend.PluginFrontend 7 | 8 | import scala.language.implicitConversions 9 | import java.nio.file.Paths 10 | import java.nio.file.Files 11 | 12 | object ProtocBridge { 13 | 14 | /** Runs protoc with a given set of targets. 15 | * 16 | * @param protoc 17 | * a function that runs protoc with the given command line arguments. 18 | * @param targets 19 | * a sequence of generators to invokes 20 | * @param params 21 | * a sequence of additional params to pass to protoc 22 | * @param classLoader 23 | * function that provided a sandboxed ClassLoader for an artifact. 24 | * @tparam A 25 | * @return 26 | * the return value from the protoc function. 27 | */ 28 | def execute[ExitCode]( 29 | protoc: ProtocRunner[ExitCode], 30 | targets: Seq[Target], 31 | params: Seq[String], 32 | classLoader: Artifact => ClassLoader 33 | ): ExitCode = 34 | execute(protoc, targets, params, PluginFrontend.newInstance, classLoader) 35 | 36 | private[protocbridge] def execute[ExitCode]( 37 | protoc: ProtocRunner[ExitCode], 38 | targets: Seq[Target], 39 | params: Seq[String], 40 | pluginFrontend: PluginFrontend, 41 | classLoader: Artifact => ClassLoader 42 | ): ExitCode = { 43 | 44 | // Resolve SandboxedJvmGenerators into JvmGenerators 45 | val targetsResolved = targets.map { 46 | case t @ Target(gen: SandboxedJvmGenerator, _, _) => 47 | val sandboxedGen = gen.resolver(classLoader(gen.artifact)) 48 | t.copy(generator = JvmGenerator(name = sandboxedGen.name, sandboxedGen)) 49 | case t => t 50 | } 51 | 52 | def canSuffix(gen: PluginGenerator): Boolean = 53 | // don't add any suffix if we have only one native, path-less generator with the same name to maintain backward 54 | // compatibility with <= 0.9.1 clients that assume that this native generator passed once would not be suffixed, 55 | // and therefore could declare that plugin path with the exact name used by the generator passed in the target 56 | // https://github.com/thesamet/sbt-protoc/blob/v1.0.0/src/main/scala/sbtprotoc/ProtocPlugin.scala#L515 57 | gen.path.isDefined || 58 | targetsResolved.count(_.generator.name == gen.name) > 1 59 | 60 | // Several targets might use a generator not built-in in protoc (native or bridge) with the same name, 61 | // so we suffix them to avoid collisions when passing them as protoc plugins 62 | val targetsSuffixed = targetsResolved.zipWithIndex.map { 63 | case (t @ Target(gen: JvmGenerator, _, _), i) => 64 | t.copy(generator = gen.copy(name = s"${gen.name}_$i")) 65 | case (t @ Target(gen: PluginGenerator, _, _), i) if canSuffix(gen) => 66 | t.copy(generator = gen.copy(name = s"${gen.name}_$i")) 67 | case (t, _) => t 68 | } 69 | 70 | val bridgeGenerators: Seq[(String, ProtocCodeGenerator)] = 71 | targetsSuffixed.collect { case Target(gen: JvmGenerator, _, _) => 72 | (gen.name, gen.gen) 73 | } 74 | 75 | val cmdLine: Seq[String] = 76 | pluginArgs(targetsSuffixed) ++ targetsSuffixed.flatMap { p => 77 | val maybeOptions = 78 | if (p.options.isEmpty) Nil 79 | else { 80 | s"--${p.generator.name}_opt=${p.options.mkString(",")}" :: Nil 81 | } 82 | s"--${p.generator.name}_out=${p.outputPath.getAbsolutePath}" :: maybeOptions 83 | } ++ params 84 | 85 | runWithGenerators(protoc, bridgeGenerators, cmdLine, pluginFrontend) 86 | } 87 | 88 | private[protocbridge] def execute[ExitCode]( 89 | protoc: ProtocRunner[ExitCode], 90 | targets: Seq[Target], 91 | params: Seq[String], 92 | pluginFrontend: PluginFrontend 93 | ): ExitCode = execute[ExitCode]( 94 | protoc, 95 | targets, 96 | params, 97 | pluginFrontend, 98 | (art: Artifact) => 99 | throw new RuntimeException( 100 | s"Unale to load sandboxed plugin for ${art} since ClassLoader was not provided. If " + 101 | "using sbt-protoc, please update to version 1.0.0-RC5 or later." 102 | ) 103 | ) 104 | 105 | private[protocbridge] def execute[ExitCode]( 106 | protoc: ProtocRunner[ExitCode], 107 | targets: Seq[Target], 108 | params: Seq[String] 109 | ): ExitCode = execute[ExitCode]( 110 | protoc, 111 | targets, 112 | params, 113 | PluginFrontend.newInstance 114 | ) 115 | 116 | private def pluginArgs(targets: Seq[Target]): Seq[String] = 117 | targets.collect { case Target(PluginGenerator(name, _, Some(path)), _, _) => 118 | s"--plugin=protoc-gen-$name=$path" 119 | } 120 | 121 | def runWithGenerators[ExitCode]( 122 | protoc: ProtocRunner[ExitCode], 123 | namedGenerators: Seq[(String, ProtocCodeGenerator)], 124 | params: Seq[String] 125 | ): ExitCode = runWithGenerators( 126 | protoc, 127 | namedGenerators, 128 | params, 129 | PluginFrontend.newInstance 130 | ) 131 | 132 | private def runWithGenerators[ExitCode]( 133 | protoc: ProtocRunner[ExitCode], 134 | bridgeGenerators: Seq[(String, ProtocCodeGenerator)], 135 | params: Seq[String], 136 | pluginFrontend: PluginFrontend 137 | ): ExitCode = { 138 | 139 | import collection.JavaConverters._ 140 | val secondaryOutputDir = Files 141 | .createTempDirectory("protocbridge-secondary") 142 | .toAbsolutePath() 143 | val extraEnv = new ExtraEnv( 144 | secondaryOutputDir = secondaryOutputDir.toString() 145 | ) 146 | 147 | val generatorScriptState 148 | : Seq[(String, (Path, pluginFrontend.InternalState))] = 149 | bridgeGenerators.map { case (name, plugin) => 150 | (name, pluginFrontend.prepare(plugin, extraEnv)) 151 | } 152 | 153 | val cmdLine: Seq[String] = generatorScriptState.map { 154 | case (name, (scriptPath, _)) => 155 | s"--plugin=protoc-gen-${name}=${scriptPath}" 156 | } ++ params 157 | 158 | try { 159 | protoc.run( 160 | cmdLine, 161 | extraEnv.toEnvMap.toSeq 162 | ) 163 | } finally { 164 | if (sys.env.getOrElse("PROTOCBRIDGE_NO_CLEANUP", "0") == "0") { 165 | generatorScriptState.foreach { case (_, (_, state)) => 166 | pluginFrontend.cleanup(state) 167 | } 168 | secondaryOutputDir.toFile().listFiles().foreach(_.delete()) 169 | secondaryOutputDir.toFile.delete() 170 | } 171 | } 172 | } 173 | 174 | // Deprecated methods 175 | @deprecated( 176 | "Please use execute() overload that takes ProtocRunner. Secondary outputs will fail to work.", 177 | "0.9.0" 178 | ) 179 | def run[A]( 180 | protoc: Seq[String] => A, 181 | targets: Seq[Target], 182 | params: Seq[String] 183 | ): A = run(protoc, targets, params, PluginFrontend.newInstance) 184 | 185 | @deprecated( 186 | "Please use execute() overload that takes ProtocRunner. Secondary outputs will fail to work.", 187 | "0.9.0" 188 | ) 189 | def run[ExitCode]( 190 | protoc: Seq[String] => ExitCode, 191 | targets: Seq[Target], 192 | params: Seq[String], 193 | pluginFrontend: PluginFrontend 194 | ): ExitCode = 195 | run( 196 | protoc, 197 | targets, 198 | params, 199 | pluginFrontend, 200 | artifact => 201 | throw new RuntimeException( 202 | s"The version of sbt-protoc you are using is incompatible with '${artifact}' code generator. Please update sbt-protoc to a version >= 0.99.33" 203 | ) 204 | ) 205 | 206 | @deprecated( 207 | "Please use execute(). Secondary outputs will fail to work.", 208 | "0.9.0" 209 | ) 210 | def run[ExitCode]( 211 | protoc: Seq[String] => ExitCode, 212 | targets: Seq[Target], 213 | params: Seq[String], 214 | pluginFrontend: PluginFrontend, 215 | classLoader: Artifact => ClassLoader 216 | ): ExitCode = execute( 217 | ProtocRunner(protoc), 218 | targets, 219 | params, 220 | pluginFrontend, 221 | classLoader 222 | ) 223 | 224 | @deprecated( 225 | "Please use execute(). Secondary outputs will fail to work.", 226 | "0.9.0" 227 | ) 228 | def runWithGenerators[ExitCode]( 229 | protoc: Seq[String] => ExitCode, 230 | namedGenerators: Seq[(String, ProtocCodeGenerator)], 231 | params: Seq[String], 232 | pluginFrontend: PluginFrontend = PluginFrontend.newInstance 233 | ): ExitCode = runWithGenerators( 234 | ProtocRunner(protoc), 235 | namedGenerators, 236 | params, 237 | pluginFrontend 238 | ) 239 | } 240 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------