• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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