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