blob: e9372c9334896f4f8c9265aa9e29cadbad671257 [file] [log] [blame]
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.generators.protobuf
import java.io.File
import java.util.regex.Pattern
import kotlin.system.exitProcess
// This file generates protobuf classes from formal description.
// To run it, you'll need protoc (protobuf compiler) 2.6.1 installed.
//
// * Windows:
// Download and unpack binaries from here: https://github.com/protocolbuffers/protobuf/releases/tag/v2.6.1
// * Linux:
// Download and unpack sources from here: https://github.com/protocolbuffers/protobuf/releases/tag/v2.6.1
// See README for instructions how to compile the project. Basically something like this should work:
// ./autogen.sh
// ./configure
// make
// make install
// * macOS:
// curl -L https://raw.githubusercontent.com/udalov/protobuf261/master/protobuf261.rb > protobuf261.rb
// brew install protobuf261.rb
//
// You may need to provide custom path to protoc executable, just modify this constant:
private const val PROTOC_EXE = "protoc"
class ProtoPath(val file: String, val generateDebug: Boolean = true) {
val outPath: String = File(file).parent
val packageName: String = findFirst(Pattern.compile("package (.+);"))
val className: String = findFirst(Pattern.compile("option java_outer_classname = \"(.+)\";"))
val debugClassName: String = "Debug$className"
private fun findFirst(pattern: Pattern): String {
for (line in File(file).readLines()) {
val m = pattern.matcher(line)
if (m.find()) return m.group(1)
}
error("Pattern not found in $file: $pattern")
}
}
val PROTO_PATHS: List<ProtoPath> = listOf(
ProtoPath("core/metadata/src/metadata.proto"),
ProtoPath("core/metadata/src/builtins.proto"),
ProtoPath("js/js.serializer/src/js.proto"),
ProtoPath("js/js.serializer/src/js-ast.proto", false),
ProtoPath("core/metadata.jvm/src/jvm_metadata.proto"),
ProtoPath("core/metadata.jvm/src/jvm_module.proto"),
ProtoPath("build-common/src/java_descriptors.proto"),
ProtoPath("compiler/util-klib-metadata/src/KlibMetadataProtoBuf.proto"),
ProtoPath("compiler/ir/serialization.common/src/KotlinIr.proto", false),
ProtoPath("compiler/ir/serialization.jvm/src/JvmIr.proto", false),
ProtoPath("plugins/kotlinx-serialization/kotlinx-serialization.k1/src/class_extensions.proto", generateDebug = false)
)
private val EXT_OPTIONS_PROTO_PATH = ProtoPath("core/metadata/src/ext_options.proto")
private val PROTOBUF_PROTO_PATHS = listOf("./", "core/metadata/src")
fun main() {
try {
checkVersion()
modifyAndExecProtoc(EXT_OPTIONS_PROTO_PATH)
for (protoPath in PROTO_PATHS) {
execProtoc(protoPath.file, protoPath.outPath)
renamePackages(protoPath.file, protoPath.outPath)
modifyAndExecProtoc(protoPath)
}
println()
println("Do not forget to run GenerateProtoBufCompare")
} catch (e: Throwable) {
e.printStackTrace()
} finally {
// Workaround for JVM hanging: IDEA's process handler creates thread pool
exitProcess(0)
}
}
private data class ProcessOutput(val stdout: String, val stderr: String)
private fun execAndGetOutput(vararg args: String): ProcessOutput {
val process = ProcessBuilder().command(*args).redirectErrorStream(true).start()
val stdout = process.inputStream.reader().readText()
val stderr = process.errorStream.reader().readText()
return ProcessOutput(stdout, stderr).also { process.waitFor() }
}
private fun checkVersion() {
val (stdout, stderr) = execAndGetOutput(PROTOC_EXE, "--version")
val version = stdout.trim()
if (version.isEmpty()) {
throw AssertionError("Output is empty, stderr: $stderr")
}
if (version != "libprotoc 2.6.1") {
throw AssertionError("Expected protoc 2.6.1, but was: $version")
}
}
private fun execProtoc(protoPath: String, outPath: String) {
val commandLine =
listOf(PROTOC_EXE, protoPath, "--java_out=$outPath") +
PROTOBUF_PROTO_PATHS.map { "--proto_path=$it" }
println("running ${commandLine.joinToString(" ")}")
val (stdout, stderr) = execAndGetOutput(*commandLine.toTypedArray())
print(stdout)
if (stderr.isNotEmpty()) {
throw AssertionError(stderr)
}
}
private fun renamePackages(protoPath: String, outPath: String) {
fun List<String>.findValue(regex: Regex): String? =
mapNotNull { line ->
regex.find(line)?.groupValues?.get(1)
}.singleOrNull()
val protoFileContents = File(protoPath).readLines()
val packageName = protoFileContents.findValue("package ([\\w.]+);".toRegex())
?: error("No package directive found in $protoPath")
val className = protoFileContents.findValue("option java_outer_classname = \"(\\w+)\";".toRegex())
?: error("No java_outer_classname option found in $protoPath")
val javaMultipleFiles = protoFileContents.findValue("option java_multiple_files = (\\w+);".toRegex()) == "true"
if (javaMultipleFiles) {
val packageDirectory = File(outPath, packageName.replace('.', '/'))
if (!packageDirectory.exists() || !packageDirectory.isDirectory) {
throw AssertionError("$protoPath, java_multiple_files mode: '$packageDirectory' doesn't exist or is not a directory")
}
val javaFiles = packageDirectory.listFiles { f: File -> f.extension == "java" }
?: throw AssertionError("$protoPath, java_multiple_files mode: Can't list directory contents of '$packageDirectory'")
for (javaFile in javaFiles) {
renamePackagesInSingleFile(javaFile)
}
} else {
renamePackagesInSingleFile(File(outPath, "${packageName.replace('.', '/')}/$className.java"))
}
}
private fun renamePackagesInSingleFile(javaFile: File) {
if (!javaFile.exists()) {
throw AssertionError("File does not exist: $javaFile")
}
javaFile.writeText(
javaFile.readLines().joinToString(System.lineSeparator()) { line ->
line.replace("com.google.protobuf", "org.jetbrains.kotlin.protobuf")
// Memory footprint optimizations: do not allocate too big bytes buffers that effectively remain unused
.replace(" unknownFieldsOutput);", " unknownFieldsOutput, 1);")
}
)
}
private fun modifyAndExecProtoc(protoPath: ProtoPath) {
if (protoPath.generateDebug) {
val debugProtoFile = File(protoPath.file.replace(".proto", ".debug.proto"))
debugProtoFile.writeText(modifyForDebug(protoPath))
debugProtoFile.deleteOnExit()
val outPath = "build-common/test"
execProtoc(debugProtoFile.path, outPath)
renamePackages(debugProtoFile.path, outPath)
}
}
private fun modifyForDebug(protoPath: ProtoPath): String {
var text = File(protoPath.file).readText()
.replace(
"option java_outer_classname = \"${protoPath.className}\"",
"option java_outer_classname = \"${protoPath.debugClassName}\""
) // give different name for class
.replace("option optimize_for = LITE_RUNTIME;", "") // using default instead
(listOf(EXT_OPTIONS_PROTO_PATH) + PROTO_PATHS).forEach {
val file = it.file
val newFile = file.replace(".proto", ".debug.proto")
text = text.replace(file, newFile)
}
return text
}