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