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