• 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                 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 }