1 /*
<lambda>null2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.tools.metalava
18
19 import com.intellij.util.execution.ParametersListUtil
20 import java.io.File
21 import java.io.IOException
22 import java.io.PrintWriter
23 import java.time.LocalDateTime
24 import java.time.format.DateTimeFormatter
25 import java.util.regex.Pattern
26 import kotlin.random.Random
27
28 /**
29 * Proprocess command line arguments.
30 * 1. Prepend/append {@code ENV_VAR_METALAVA_PREPEND_ARGS} and {@code ENV_VAR_METALAVA_PREPEND_ARGS}
31 * 2. Reflect --verbose to {@link options#verbose}.
32 */
33 internal fun preprocessArgv(args: Array<String>): Array<String> {
34 val modifiedArgs =
35 if (args.isEmpty()) {
36 arrayOf("--help")
37 } else if (!isUnderTest()) {
38 val prepend = envVarToArgs(ENV_VAR_METALAVA_PREPEND_ARGS)
39 val append = envVarToArgs(ENV_VAR_METALAVA_APPEND_ARGS)
40 if (prepend.isEmpty() && append.isEmpty()) {
41 args
42 } else {
43 val index = args.indexOf(ARG_GENERATE_DOCUMENTATION)
44 val newArgs =
45 if (index != -1) {
46 args.sliceArray(0 until index) + prepend +
47 args.sliceArray(index until args.size) + append
48 } else {
49 prepend + args + append
50 }
51 newArgs
52 }
53 } else {
54 args
55 }
56
57 // We want to enable verbose log as soon as possible, so we cheat here and try to detect
58 // --verbose and --quiet.
59 // (Note this logic could generate results different from what Options.kt would generate,
60 // for example when "--verbose" is used as a flag value. But that's not a practical problem....)
61 modifiedArgs.forEach { arg ->
62 when (arg) {
63 ARG_QUIET -> {
64 options.quiet = true; options.verbose = false
65 }
66
67 ARG_VERBOSE -> {
68 options.verbose = true; options.quiet = false
69 }
70 }
71 }
72
73 return modifiedArgs
74 }
75
76 /**
77 * Given an environment variable name pointing to a shell argument string,
78 * returns the parsed argument strings (or empty array if not set)
79 */
envVarToArgsnull80 private fun envVarToArgs(varName: String): Array<String> {
81 val value = System.getenv(varName) ?: return emptyArray()
82 return ParametersListUtil.parse(value).toTypedArray()
83 }
84
85 /**
86 * If the {@link ENV_VAR_METALAVA_DUMP_ARGV} environmental variable is set, dump the passed
87 * arguments.
88 *
89 * If the variable is set to"full", also dump the content of the file
90 * specified with "@".
91 *
92 * If the variable is set to "script", it'll generate a "rerun" script instead.
93 */
maybeDumpArgvnull94 internal fun maybeDumpArgv(
95 out: PrintWriter,
96 originalArgs: Array<String>,
97 modifiedArgs: Array<String>
98 ) {
99 val dumpOption = System.getenv(ENV_VAR_METALAVA_DUMP_ARGV)
100 if (dumpOption == null || isUnderTest()) {
101 return
102 }
103
104 // Generate a rerun script, if needed, with the original args.
105 if ("script".equals(dumpOption)) {
106 generateRerunScript(out, originalArgs)
107 }
108
109 val fullDump = "full".equals(dumpOption) // Dump rsp file contents too?
110
111 dumpArgv(out, "Original args", originalArgs, fullDump)
112 dumpArgv(out, "Modified args", modifiedArgs, fullDump)
113 }
114
dumpArgvnull115 private fun dumpArgv(
116 out: PrintWriter,
117 description: String,
118 args: Array<String>,
119 fullDump: Boolean
120 ) {
121 out.println("== $description ==")
122 out.println(" pwd: ${File("").absolutePath}")
123 val sep = Pattern.compile("""\s+""")
124 var i = 0
125 args.forEach { arg ->
126 out.println(" argv[${i++}]: $arg")
127
128 // Optionally dump the content of an "@" file.
129 if (fullDump && arg.startsWith("@")) {
130 val file = arg.substring(1)
131 out.println(" ==FILE CONTENT==")
132 try {
133 File(file).bufferedReader().forEachLine { line ->
134 line.split(sep).forEach { item ->
135 out.println(" | $item")
136 }
137 }
138 } catch (e: IOException) {
139 out.println(" " + e.message)
140 }
141 out.println(" ================")
142 }
143 }
144
145 out.flush()
146 }
147
148 /** Generate a rerun script file name minus the extension. */
createRerunScriptBaseFilenamenull149 private fun createRerunScriptBaseFilename(): String {
150 val timestamp = LocalDateTime.now().format(
151 DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss.SSS"))
152
153 val uniqueInt = Random.nextInt(0, Int.MAX_VALUE)
154 val dir = System.getenv("METALAVA_TEMP") ?: System.getenv("TMP") ?: System.getenv("TEMP") ?: "/tmp"
155 val file = "$PROGRAM_NAME-rerun-${timestamp}_$uniqueInt" // no extension
156
157 return dir + File.separator + file
158 }
159
160 /**
161 * Generate a rerun script, if specified by the command line arguments.
162 */
generateRerunScriptnull163 private fun generateRerunScript(stdout: PrintWriter, args: Array<String>) {
164 val scriptBaseName = createRerunScriptBaseFilename()
165
166 // Java runtime executable.
167 val java = System.getProperty("java.home") + "/bin/java"
168 // Metalava's jar path.
169 val jar = ApiLint::class.java.protectionDomain?.codeSource?.location?.toURI()?.path
170 // JVM options.
171 val jvmOptions = ManagementWrapper.getVmArguments()
172 if (jvmOptions == null || jar == null) {
173 stdout.println("$PROGRAM_NAME unable to get my jar file path.")
174 return
175 }
176
177 val script = File(scriptBaseName + ".sh")
178 var optFileIndex = 0
179 script.printWriter().use { out ->
180 out.println("""
181 |#!/bin/sh
182 |#
183 |# Auto-generated $PROGRAM_NAME rerun script
184 |#
185 |
186 |# Exit on failure
187 |set -e
188 |
189 |cd ${shellEscape(File("").absolutePath)}
190 |
191 |export $ENV_VAR_METALAVA_DUMP_ARGV=1
192 |
193 |# Overwrite JVM options with ${"$"}METALAVA_JVM_OPTS, if available
194 |jvm_opts=(${"$"}METALAVA_JVM_OPTS)
195 |
196 |if [ ${"$"}{#jvm_opts[@]} -eq 0 ] ; then
197 """.trimMargin())
198
199 jvmOptions.forEach {
200 out.println(""" jvm_opts+=(${shellEscape(it)})""")
201 }
202
203 out.println("""
204 |fi
205 |
206 |${"$"}METALAVA_RUN_PREFIX $java "${"$"}{jvm_opts[@]}" \
207 """.trimMargin())
208 out.println(""" -jar $jar \""")
209
210 // Write the actual metalava options
211 args.forEach {
212 if (!it.startsWith("@")) {
213 out.print(if (it.startsWith("-")) " " else " ")
214 out.print(shellEscape(it))
215 out.println(" \\")
216 } else {
217 val optFile = "${scriptBaseName}_arg_${++optFileIndex}.txt"
218 File(it.substring(1)).copyTo(File(optFile), true)
219 out.println(""" @$optFile \""")
220 }
221 }
222 }
223
224 // Show the filename.
225 stdout.println("Generated rerun script: $script")
226 stdout.flush()
227 }
228
229 /** Characters that need escaping in shell scripts. */
<lambda>null230 private val SHELL_UNSAFE_CHARS by lazy { Pattern.compile("""[^\-a-zA-Z0-9_/.,:=@+]""") }
231
232 /** Escape a string as a single word for shell scripts. */
shellEscapenull233 private fun shellEscape(s: String): String {
234 if (!SHELL_UNSAFE_CHARS.matcher(s).find()) {
235 return s
236 }
237 // Just wrap a string in ' ... ', except of it contains a ', it needs to be changed to
238 // '\''.
239 return "'" + s.replace("""'""", """'\''""") + "'"
240 }