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