| /* |
| * * Copyright 2022 Google LLC. All rights reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the License); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.devtools.kotlin |
| |
| import java.io.BufferedInputStream |
| import java.io.BufferedOutputStream |
| import java.nio.file.Files |
| import java.nio.file.Path |
| import java.nio.file.Paths |
| import java.nio.file.StandardCopyOption |
| import java.util.zip.ZipEntry |
| import java.util.zip.ZipInputStream |
| import java.util.zip.ZipOutputStream |
| import kotlin.system.exitProcess |
| import picocli.CommandLine |
| import picocli.CommandLine.Command |
| import picocli.CommandLine.Model.CommandSpec |
| import picocli.CommandLine.Option |
| import picocli.CommandLine.ParameterException |
| import picocli.CommandLine.Parameters |
| import picocli.CommandLine.Spec |
| |
| @Command( |
| name = "source-jar-zipper", |
| subcommands = [Unzip::class, Zip::class, ZipResources::class], |
| description = ["A tool to pack and unpack srcjar files, and to zip resource files"], |
| ) |
| class SourceJarZipper : Runnable { |
| @Spec private lateinit var spec: CommandSpec |
| override fun run() { |
| throw ParameterException(spec.commandLine(), "Specify a command: zip, zip_resources or unzip") |
| } |
| } |
| |
| fun main(args: Array<String>) { |
| val exitCode = CommandLine(SourceJarZipper()).execute(*args) |
| exitProcess(exitCode) |
| } |
| |
| /** |
| * Checks for duplicates and add an entry into [errors] if found any, otherwise adds a pair |
| * of [zipPath] and [sourcePath] to the receiver |
| * @receiver a mutable map of path to path, where keys are relative paths of files inside the |
| * resulting .jar, and values are full paths of files |
| * @param[zipPath] relative path inside the jar, built either from package name |
| * (e.g. package com.google.foo -> com/google/foo/FileName.kt) or by resolving the file name |
| * relatively the directory it came from (e.g. foo/bar/1/2.txt came from foo/bar -> 1/2.txt) |
| * @param[sourcePath] full path of file into its file system |
| * @param[errors] list of strings describing catched errors |
| */ |
| fun MutableMap<Path, Path>.checkForDuplicatesAndSetFilePathToPathInsideJar( |
| zipPath: Path, |
| sourcePath: Path, |
| errors: MutableList<String>, |
| ) { |
| val duplicatedSourcePath: Path? = this[zipPath] |
| if (duplicatedSourcePath == null) { |
| this[zipPath] = sourcePath |
| } else { |
| errors.add( |
| "${sourcePath} has the same path inside .jar as ${duplicatedSourcePath}! " + |
| "If it is intended behavior rename one or both of them." |
| ) |
| } |
| } |
| |
| fun MutableMap<Path, Path>.writeToStream( |
| zipper: ZipOutputStream, |
| prefix: String = "", |
| ) { |
| for ((zipPath, sourcePath) in this) { |
| BufferedInputStream(Files.newInputStream(sourcePath)).use { inputStream -> |
| val entry = ZipEntry(Paths.get(prefix).resolve(zipPath).toString()) |
| entry.time = 0 |
| zipper.putNextEntry(entry) |
| inputStream.copyTo(zipper, bufferSize = 1024) |
| } |
| } |
| } |
| |
| @Command(name = "zip", description = ["Zip source files into a source jar file"]) |
| class Zip : Runnable { |
| |
| @Parameters(index = "0", paramLabel = "outputJar", description = ["Output jar"]) |
| lateinit var outputJar: Path |
| |
| @Option( |
| names = ["-i", "--ignore_not_allowed_files"], |
| description = ["Ignore not .kt, .java or invalid file paths without raising an exception"], |
| ) |
| var ignoreNotAllowedFiles = false |
| |
| @Option( |
| names = ["--kotlin_srcs"], |
| split = ",", |
| description = ["Kotlin source files"], |
| ) |
| val kotlinSrcs = mutableListOf<Path>() |
| |
| @Option( |
| names = ["--common_srcs"], |
| split = ",", |
| description = ["Common source files"], |
| ) |
| val commonSrcs = mutableListOf<Path>() |
| |
| companion object { |
| const val PACKAGE_SPACE = "package " |
| val PACKAGE_NAME_REGEX = "[a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z0-9_]+)*".toRegex() |
| } |
| |
| override fun run() { |
| check(kotlinSrcs.isNotEmpty() or commonSrcs.isNotEmpty()) { |
| "Expected at least one source file." |
| } |
| |
| // Validating files and getting paths for resulting .jar in one cycle |
| // for each _srcs list |
| val ktZipPathToSourcePath = mutableMapOf<Path, Path>() |
| val commonZipPathToSourcePath = mutableMapOf<Path, Path>() |
| val errors = mutableListOf<String>() |
| |
| fun Path.getPackagePath(): Path { |
| this.toFile().bufferedReader().use { stream -> |
| while (true) { |
| val line = stream.readLine() ?: return this.fileName |
| |
| if (line.startsWith(PACKAGE_SPACE)) { |
| // Kotlin allows usage of reserved words in package names framing them |
| // with backquote symbol "`" |
| val packageName = |
| line.substring(PACKAGE_SPACE.length).trim().replace(";", "").replace("`", "") |
| if (!PACKAGE_NAME_REGEX.matches(packageName)) { |
| errors.add("${this} contains an invalid package name") |
| return this.fileName |
| } |
| return Paths.get(packageName.replace(".", "/")).resolve(this.fileName) |
| } |
| } |
| } |
| } |
| |
| fun Path.validateFile(): Boolean { |
| when { |
| !Files.isRegularFile(this) -> { |
| if (!ignoreNotAllowedFiles) errors.add("${this} is not a file") |
| return false |
| } |
| !this.toString().endsWith(".kt") && !this.toString().endsWith(".java") -> { |
| if (!ignoreNotAllowedFiles) errors.add("${this} is not a Kotlin file") |
| return false |
| } |
| else -> return true |
| } |
| } |
| |
| for (sourcePath in kotlinSrcs) { |
| if (sourcePath.validateFile()) { |
| ktZipPathToSourcePath.checkForDuplicatesAndSetFilePathToPathInsideJar( |
| sourcePath.getPackagePath(), |
| sourcePath, |
| errors, |
| ) |
| } |
| } |
| |
| for (sourcePath in commonSrcs) { |
| if (sourcePath.validateFile()) { |
| commonZipPathToSourcePath.checkForDuplicatesAndSetFilePathToPathInsideJar( |
| sourcePath.getPackagePath(), |
| sourcePath, |
| errors, |
| ) |
| } |
| } |
| |
| if (ktZipPathToSourcePath.isEmpty() && commonZipPathToSourcePath.isEmpty()) { |
| errors.add("Expected at least one valid source file .kt or .java") |
| } |
| check(errors.isEmpty()) { errors.joinToString("\n") } |
| |
| ZipOutputStream(BufferedOutputStream(Files.newOutputStream(outputJar))).use { zipper -> |
| commonZipPathToSourcePath.writeToStream(zipper, "common-srcs") |
| ktZipPathToSourcePath.writeToStream(zipper) |
| } |
| } |
| } |
| |
| @Command(name = "unzip", description = ["Unzip a jar archive into a specified directory"]) |
| class Unzip : Runnable { |
| |
| @Parameters(index = "0", paramLabel = "inputJar", description = ["Jar archive to unzip"]) |
| lateinit var inputJar: Path |
| |
| @Parameters(index = "1", paramLabel = "outputDir", description = ["Output directory"]) |
| lateinit var outputDir: Path |
| |
| override fun run() { |
| ZipInputStream(Files.newInputStream(inputJar)).use { unzipper -> |
| while (true) { |
| val zipEntry: ZipEntry? = unzipper.nextEntry |
| if (zipEntry == null) return |
| |
| val entryName = zipEntry.name |
| check(!entryName.contains("./")) { "Cannot unpack srcjar with relative path ${entryName}" } |
| |
| if (!entryName.endsWith(".kt") && !entryName.endsWith(".java")) continue |
| |
| val entryPath = outputDir.resolve(entryName) |
| if (!Files.exists(entryPath.parent)) Files.createDirectories(entryPath.parent) |
| Files.copy(unzipper, entryPath, StandardCopyOption.REPLACE_EXISTING) |
| } |
| } |
| } |
| } |
| |
| @Command(name = "zip_resources", description = ["Zip resources"]) |
| class ZipResources : Runnable { |
| |
| @Parameters(index = "0", paramLabel = "outputJar", description = ["Output jar"]) |
| lateinit var outputJar: Path |
| |
| @Option( |
| names = ["--input_dirs"], |
| split = ",", |
| description = ["Input files directories"], |
| required = true, |
| ) |
| val inputDirs = mutableListOf<Path>() |
| |
| override fun run() { |
| val filePathToOutputPath = mutableMapOf<Path, Path>() |
| val errors = mutableListOf<String>() |
| |
| // inputDirs has filter checking if the dir exists, because some empty dirs generated by blaze |
| // may not exist from Kotlin compiler's side. It turned out to be safer to apply a filter then |
| // to rely that generated directories are always directories, not just path names |
| for (dirPath in inputDirs.filter { curDirPath -> Files.exists(curDirPath) }) { |
| if (!Files.isDirectory(dirPath)) { |
| errors.add("${dirPath} is not a directory") |
| } else { |
| Files.walk(dirPath) |
| .filter { fileOrDir -> !Files.isDirectory(fileOrDir) } |
| .forEach { filePath -> |
| filePathToOutputPath.checkForDuplicatesAndSetFilePathToPathInsideJar( |
| dirPath.relativize(filePath), |
| filePath, |
| errors |
| ) |
| } |
| } |
| } |
| |
| check(errors.isEmpty()) { errors.joinToString("\n") } |
| |
| ZipOutputStream(BufferedOutputStream(Files.newOutputStream(outputJar))).use { zipper -> |
| filePathToOutputPath.writeToStream(zipper) |
| } |
| } |
| } |