-
Notifications
You must be signed in to change notification settings - Fork 150
/
Protobuf.scala
188 lines (167 loc) · 6.69 KB
/
Protobuf.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* license agreements; and to You under the Apache License, version 2.0:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* This file is part of the Apache Pekko project, which was derived from Akka.
*/
/*
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
*/
import java.io.File
import java.io.PrintWriter
import scala.sys.process._
import sbt._
import sbt.util.CacheStoreFactory
import Keys._
import sbtassembly.AssemblyKeys._
object Protobuf {
lazy val paths = SettingKey[Seq[File]]("protobuf-paths", "The paths that contain *.proto files.")
lazy val outputPaths =
SettingKey[Seq[File]]("protobuf-output-paths", "The paths where to save the generated *.java files.")
lazy val importPath = SettingKey[Option[File]](
"protobuf-import-path",
"The path that contain additional *.proto files that can be imported.")
lazy val protoc = SettingKey[String]("protobuf-protoc", "The path and name of the protoc executable.")
lazy val protocVersion = SettingKey[String]("protobuf-protoc-version", "The version of the protoc executable.")
lazy val generate = TaskKey[Unit]("protobuf-generate", "Compile the protobuf sources and do all processing.")
lazy val settings: Seq[Setting[_]] = Seq(
paths := Seq((Compile / sourceDirectory).value, (Test / sourceDirectory).value).map(_ / "protobuf"),
outputPaths := Seq((Compile / sourceDirectory).value, (Test / sourceDirectory).value).map(_ / "java"),
importPath := None,
// this keeps intellij happy for files that use the shaded protobuf
Compile / unmanagedJars += (LocalProject("protobuf-v3") / Compile / packageBin).value,
protoc := "protoc",
protocVersion := Dependencies.Protobuf.protocVersion,
generate := {
val sourceDirs = paths.value
val targetDirs = outputPaths.value
val log = streams.value.log
if (sourceDirs.size != targetDirs.size)
sys.error(
s"Unbalanced number of paths and destination paths!\nPaths: $sourceDirs\nDestination Paths: $targetDirs")
if (sourceDirs.exists(_.exists)) {
val cmd = protoc.value
checkProtocVersion(cmd, protocVersion.value, log)
val base = baseDirectory.value
val sources = base / "src"
val targets = target.value
val cache = targets / "protoc" / "cache"
sourceDirs.zip(targetDirs).map {
case (src, dst) =>
val relative = src
.relativeTo(sources)
.getOrElse(throw new Exception(s"path $src is not a in source tree $sources"))
.toString
val tmp = targets / "protoc" / relative
IO.delete(tmp)
generate(cmd, src, tmp, log, importPath.value)
transformDirectory(
tmp,
dst,
_ => true,
transformFile(
_.replace("com.google.protobuf", "org.apache.pekko.protobufv3.internal")
// this is the one thing that protobufGenerate doesn't fully qualify and causes
// api doc generation to fail
.replace(
"UnusedPrivateParameter",
"org.apache.pekko.protobufv3.internal.GeneratedMessageV3.UnusedPrivateParameter")),
cache,
log)
}
}
})
private def callProtoc[T](protoc: String, args: Seq[String], log: Logger, thunk: (ProcessBuilder, Logger) => T): T =
try {
val proc = Process(protoc, args)
thunk(proc, log)
} catch {
case e: Exception =>
throw new RuntimeException("error while executing '%s' with args: %s".format(protoc, args.mkString(" ")), e)
}
private def checkProtocVersion(protoc: String, protocVersion: String, log: Logger): Unit = {
val res = callProtoc(protoc, Seq("--version"), log,
{ (p, l) =>
p !! l
})
val version = res.split(" ").last.trim
if (version != protocVersion) {
sys.error("Wrong protoc version! Expected %s but got %s".format(protocVersion, version))
}
}
private def generate(protoc: String, srcDir: File, targetDir: File, log: Logger, importPath: Option[File]): Unit = {
val protoFiles = (srcDir ** "*.proto").get
if (srcDir.exists)
if (protoFiles.isEmpty)
log.info("Skipping empty source directory %s".format(srcDir))
else {
targetDir.mkdirs()
log.info("Generating %d protobuf files from %s to %s".format(protoFiles.size, srcDir, targetDir))
protoFiles.foreach { proto =>
log.info("Compiling %s".format(proto))
}
val protoPathArg = importPath match {
case None => Nil
case Some(p) => Seq("--proto_path", p.absolutePath)
}
val exitCode = callProtoc(
protoc,
Seq("-I" + srcDir.absolutePath, "--java_out=%s".format(targetDir.absolutePath)) ++
protoPathArg ++ protoFiles.map(_.absolutePath),
log,
{ (p, l) =>
p ! l
})
if (exitCode != 0)
sys.error("protoc returned exit code: %d".format(exitCode))
}
}
/**
* Create a transformed version of all files in a directory, given a predicate and a transform function for each file. From sbt-site
*/
private def transformDirectory(
sourceDir: File,
targetDir: File,
transformable: File => Boolean,
transform: (File, File) => Unit,
cache: File,
log: Logger): File = {
val runTransform = FileFunction.cached(CacheStoreFactory(cache), FilesInfo.hash, FilesInfo.exists) {
(in: ChangeReport[File], out: ChangeReport[File]) =>
val map = Path.rebase(sourceDir, targetDir)
if (in.removed.nonEmpty || in.modified.nonEmpty) {
log.info("Preprocessing directory %s...".format(sourceDir))
for (source <- in.removed; target <- map(source)) {
IO.delete(target)
}
val updated = for (source <- in.modified; target <- map(source)) yield {
if (source.isFile) {
if (transformable(source)) transform(source, target)
else IO.copyFile(source, target)
}
target
}
log.info("Directory preprocessed: " + targetDir)
updated
} else Set.empty
}
val sources = sourceDir.allPaths.get.toSet
runTransform(sources)
targetDir
}
/**
* Transform a file, line by line.
*/
def transformFile(transform: String => String)(source: File, target: File): Unit = {
IO.reader(source) { reader =>
IO.writer(target, "", IO.defaultCharset) { writer =>
val pw = new PrintWriter(writer)
IO.foreachLine(reader) { line =>
pw.println(transform(line))
}
}
}
}
}