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