| /* |
| * 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 |
| } |