• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2017 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 @file:JvmName("Driver")
17 
18 package com.android.tools.metalava
19 
20 import com.android.SdkConstants.DOT_JAR
21 import com.android.SdkConstants.DOT_JAVA
22 import com.android.SdkConstants.DOT_KT
23 import com.android.SdkConstants.DOT_TXT
24 import com.android.ide.common.process.CachedProcessOutputHandler
25 import com.android.ide.common.process.DefaultProcessExecutor
26 import com.android.ide.common.process.ProcessInfoBuilder
27 import com.android.ide.common.process.ProcessOutput
28 import com.android.ide.common.process.ProcessOutputHandler
29 import com.android.tools.lint.UastEnvironment
30 import com.android.tools.lint.annotations.Extractor
31 import com.android.tools.lint.checks.infrastructure.ClassName
32 import com.android.tools.lint.detector.api.assertionsEnabled
33 import com.android.tools.metalava.CompatibilityCheck.CheckRequest
34 import com.android.tools.metalava.apilevels.ApiGenerator
35 import com.android.tools.metalava.model.ClassItem
36 import com.android.tools.metalava.model.Codebase
37 import com.android.tools.metalava.model.Item
38 import com.android.tools.metalava.model.PackageDocs
39 import com.android.tools.metalava.model.psi.PsiBasedCodebase
40 import com.android.tools.metalava.model.psi.packageHtmlToJavadoc
41 import com.android.tools.metalava.model.text.TextCodebase
42 import com.android.tools.metalava.model.visitors.ApiVisitor
43 import com.android.tools.metalava.stub.StubWriter
44 import com.android.utils.StdLogger
45 import com.android.utils.StdLogger.Level.ERROR
46 import com.google.common.base.Stopwatch
47 import com.google.common.collect.Lists
48 import com.google.common.io.Files
49 import com.intellij.core.CoreApplicationEnvironment
50 import com.intellij.openapi.diagnostic.DefaultLogger
51 import com.intellij.openapi.util.Disposer
52 import com.intellij.pom.java.LanguageLevel
53 import com.intellij.psi.javadoc.CustomJavadocTagProvider
54 import com.intellij.psi.javadoc.JavadocTagInfo
55 import org.jetbrains.kotlin.config.CommonConfigurationKeys.MODULE_NAME
56 import org.jetbrains.kotlin.config.LanguageVersionSettings
57 import java.io.File
58 import java.io.IOException
59 import java.io.OutputStream
60 import java.io.OutputStreamWriter
61 import java.io.PrintWriter
62 import java.util.concurrent.TimeUnit.SECONDS
63 import java.util.function.Predicate
64 import kotlin.system.exitProcess
65 import kotlin.text.Charsets.UTF_8
66 
67 const val PROGRAM_NAME = "metalava"
68 const val HELP_PROLOGUE = "$PROGRAM_NAME extracts metadata from source code to generate artifacts such as the " +
69     "signature files, the SDK stub files, external annotations etc."
70 const val PACKAGE_HTML = "package.html"
71 const val OVERVIEW_HTML = "overview.html"
72 
73 @Suppress("PropertyName") // Can't mark const because trimIndent() :-(
74 val BANNER: String = """
75                 _        _
76  _ __ ___   ___| |_ __ _| | __ ___   ____ _
77 | '_ ` _ \ / _ \ __/ _` | |/ _` \ \ / / _` |
78 | | | | | |  __/ || (_| | | (_| |\ V / (_| |
79 |_| |_| |_|\___|\__\__,_|_|\__,_| \_/ \__,_|
80 """.trimIndent()
81 
82 fun main(args: Array<String>) {
83     run(args, setExitCode = true)
84 }
85 
86 internal var hasFileReadViolations = false
87 
88 /**
89  * The metadata driver is a command line interface to extracting various metadata
90  * from a source tree (or existing signature files etc). Run with --help to see
91  * more details.
92  */
runnull93 fun run(
94     originalArgs: Array<String>,
95     stdout: PrintWriter = PrintWriter(OutputStreamWriter(System.out)),
96     stderr: PrintWriter = PrintWriter(OutputStreamWriter(System.err)),
97     setExitCode: Boolean = false
98 ): Boolean {
99     var exitCode = 0
100 
101     try {
102         val modifiedArgs = preprocessArgv(originalArgs)
103 
104         progress("$PROGRAM_NAME started\n")
105 
106         // Dump the arguments, and maybe generate a rerun-script.
107         maybeDumpArgv(stdout, originalArgs, modifiedArgs)
108 
109         // Actual work begins here.
110         compatibility = Compatibility(compat = Options.useCompatMode(modifiedArgs))
111         options = Options(modifiedArgs, stdout, stderr)
112 
113         maybeActivateSandbox()
114 
115         processFlags()
116 
117         if (options.allReporters.any { it.hasErrors() } && !options.passBaselineUpdates) {
118             // Repeat the errors at the end to make it easy to find the actual problems.
119             if (options.repeatErrorsMax > 0) {
120                 repeatErrors(stderr, options.allReporters, options.repeatErrorsMax)
121             }
122             exitCode = -1
123         }
124         if (hasFileReadViolations) {
125             if (options.strictInputFiles.shouldFail) {
126                 stderr.print("Error: ")
127                 exitCode = -1
128             } else {
129                 stderr.print("Warning: ")
130             }
131             stderr.println("$PROGRAM_NAME detected access to files that are not explicitly specified. See ${options.strictInputViolationsFile} for details.")
132         }
133     } catch (e: DriverException) {
134         stdout.flush()
135         stderr.flush()
136 
137         val prefix = if (e.exitCode != 0) { "Aborting: " } else { "" }
138 
139         if (e.stderr.isNotBlank()) {
140             stderr.println("\n${prefix}${e.stderr}")
141         }
142         if (e.stdout.isNotBlank()) {
143             stdout.println("\n${prefix}${e.stdout}")
144         }
145         exitCode = e.exitCode
146     } finally {
147         disposeUastEnvironment()
148     }
149 
150     // Update and close all baseline files.
151     options.allBaselines.forEach { baseline ->
152         if (options.verbose) {
153             baseline.dumpStats(options.stdout)
154         }
155         if (baseline.close()) {
156             if (!options.quiet) {
157                 stdout.println("$PROGRAM_NAME wrote updated baseline to ${baseline.updateFile}")
158             }
159         }
160     }
161 
162     options.reportEvenIfSuppressedWriter?.close()
163     options.strictInputViolationsPrintWriter?.close()
164 
165     // Show failure messages, if any.
166     options.allReporters.forEach {
167         it.writeErrorMessage(stderr)
168     }
169 
170     stdout.flush()
171     stderr.flush()
172 
173     if (setExitCode) {
174         exit(exitCode)
175     }
176 
177     return exitCode == 0
178 }
179 
exitnull180 private fun exit(exitCode: Int = 0) {
181     if (options.verbose) {
182         progress("$PROGRAM_NAME exiting with exit code $exitCode\n")
183     }
184     options.stdout.flush()
185     options.stderr.flush()
186     exitProcess(exitCode)
187 }
188 
maybeActivateSandboxnull189 private fun maybeActivateSandbox() {
190     // Set up a sandbox to detect access to files that are not explicitly specified.
191     if (options.strictInputFiles == Options.StrictInputFileMode.PERMISSIVE) {
192         return
193     }
194 
195     val writer = options.strictInputViolationsPrintWriter!!
196 
197     // Writes all violations to [Options.strictInputFiles].
198     // If Options.StrictInputFile.Mode is STRICT, then all violations on reads are logged, and the
199     // tool exits with a negative error code if there are any file read violations. Directory read
200     // violations are logged, but are considered to be a "warning" and doesn't affect the exit code.
201     // If STRICT_WARN, all violations on reads are logged similar to STRICT, but the exit code is
202     // unaffected.
203     // If STRICT_WITH_STACK, similar to STRICT, but also logs the stack trace to
204     // Options.strictInputFiles.
205     // See [FileReadSandbox] for the details.
206     FileReadSandbox.activate(object : FileReadSandbox.Listener {
207         var seen = mutableSetOf<String>()
208         override fun onViolation(absolutePath: String, isDirectory: Boolean) {
209             if (!seen.contains(absolutePath)) {
210                 val suffix = if (isDirectory) "/" else ""
211                 writer.println("$absolutePath$suffix")
212                 if (options.strictInputFiles == Options.StrictInputFileMode.STRICT_WITH_STACK) {
213                     Throwable().printStackTrace(writer)
214                 }
215                 seen.add(absolutePath)
216                 if (!isDirectory) {
217                     hasFileReadViolations = true
218                 }
219             }
220         }
221     })
222 }
223 
repeatErrorsnull224 private fun repeatErrors(writer: PrintWriter, reporters: List<Reporter>, max: Int) {
225     writer.println("Error: $PROGRAM_NAME detected the following problems:")
226     val totalErrors = reporters.sumBy { it.errorCount }
227     var remainingCap = max
228     var totalShown = 0
229     reporters.forEach {
230         var numShown = it.printErrors(writer, remainingCap)
231         remainingCap -= numShown
232         totalShown += numShown
233     }
234     if (totalShown < totalErrors) {
235         writer.println("${totalErrors - totalShown} more error(s) omitted. Search the log for 'error:' to find all of them.")
236     }
237 }
238 
processFlagsnull239 private fun processFlags() {
240     val stopwatch = Stopwatch.createStarted()
241 
242     processNonCodebaseFlags()
243 
244     val sources = options.sources
245     val codebase =
246         if (sources.isNotEmpty() && sources[0].path.endsWith(DOT_TXT)) {
247             // Make sure all the source files have .txt extensions.
248             sources.firstOrNull { !it.path.endsWith(DOT_TXT) }?. let {
249                 throw DriverException("Inconsistent input file types: The first file is of $DOT_TXT, but detected different extension in ${it.path}")
250             }
251             SignatureFileLoader.loadFiles(sources, options.inputKotlinStyleNulls)
252         } else if (options.apiJar != null) {
253             loadFromJarFile(options.apiJar!!)
254         } else if (sources.size == 1 && sources[0].path.endsWith(DOT_JAR)) {
255             loadFromJarFile(sources[0])
256         } else if (sources.isNotEmpty() || options.sourcePath.isNotEmpty()) {
257             loadFromSources()
258         } else {
259             return
260         }
261     options.manifest?.let { codebase.manifest = it }
262 
263     if (options.verbose) {
264         progress("$PROGRAM_NAME analyzed API in ${stopwatch.elapsed(SECONDS)} seconds\n")
265     }
266 
267     options.subtractApi?.let {
268         progress("Subtracting API: ")
269         subtractApi(codebase, it)
270     }
271 
272     val androidApiLevelXml = options.generateApiLevelXml
273     val apiLevelJars = options.apiLevelJars
274     if (androidApiLevelXml != null && apiLevelJars != null) {
275         progress("Generating API levels XML descriptor file, ${androidApiLevelXml.name}: ")
276         ApiGenerator.generate(apiLevelJars, androidApiLevelXml, codebase)
277     }
278 
279     if (options.docStubsDir != null && codebase.supportsDocumentation()) {
280         progress("Enhancing docs: ")
281         val docAnalyzer = DocAnalyzer(codebase)
282         docAnalyzer.enhance()
283 
284         val applyApiLevelsXml = options.applyApiLevelsXml
285         if (applyApiLevelsXml != null) {
286             progress("Applying API levels")
287             docAnalyzer.applyApiLevels(applyApiLevelsXml)
288         }
289     }
290 
291     // Generate the documentation stubs *before* we migrate nullness information.
292     options.docStubsDir?.let {
293         createStubFiles(
294             it, codebase, docStubs = true,
295             writeStubList = options.docStubsSourceList != null
296         )
297     }
298 
299     // Based on the input flags, generates various output files such
300     // as signature files and/or stubs files
301     options.apiFile?.let { apiFile ->
302         val apiType = ApiType.PUBLIC_API
303         val apiEmit = apiType.getEmitFilter()
304         val apiReference = apiType.getReferenceFilter()
305 
306         createReportFile(codebase, apiFile, "API") { printWriter ->
307             SignatureWriter(printWriter, apiEmit, apiReference, codebase.preFiltered)
308         }
309     }
310 
311     options.apiXmlFile?.let { apiFile ->
312         val apiType = ApiType.PUBLIC_API
313         val apiEmit = apiType.getEmitFilter()
314         val apiReference = apiType.getReferenceFilter()
315 
316         createReportFile(codebase, apiFile, "XML API") { printWriter ->
317             JDiffXmlWriter(printWriter, apiEmit, apiReference, codebase.preFiltered)
318         }
319     }
320 
321     options.removedApiFile?.let { apiFile ->
322         val unfiltered = codebase.original ?: codebase
323 
324         val apiType = ApiType.REMOVED
325         val removedEmit = apiType.getEmitFilter()
326         val removedReference = apiType.getReferenceFilter()
327 
328         createReportFile(unfiltered, apiFile, "removed API") { printWriter ->
329             SignatureWriter(printWriter, removedEmit, removedReference, codebase.original != null)
330         }
331     }
332 
333     options.dexApiFile?.let { apiFile ->
334         val apiFilter = FilterPredicate(ApiPredicate())
335         val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
336         val apiReference = ApiPredicate(ignoreShown = true)
337         val dexApiEmit = memberIsNotCloned.and(apiFilter)
338 
339         createReportFile(
340             codebase, apiFile, "DEX API"
341         ) { printWriter -> DexApiWriter(printWriter, dexApiEmit, apiReference) }
342     }
343 
344     options.removedDexApiFile?.let { apiFile ->
345         val unfiltered = codebase.original ?: codebase
346 
347         val removedFilter = FilterPredicate(ApiPredicate(matchRemoved = true))
348         val removedReference = ApiPredicate(ignoreShown = true, ignoreRemoved = true)
349         val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
350         val removedDexEmit = memberIsNotCloned.and(removedFilter)
351 
352         createReportFile(
353             unfiltered, apiFile, "removed DEX API"
354         ) { printWriter -> DexApiWriter(printWriter, removedDexEmit, removedReference) }
355     }
356 
357     options.proguard?.let { proguard ->
358         val apiEmit = FilterPredicate(ApiPredicate())
359         val apiReference = ApiPredicate(ignoreShown = true)
360         createReportFile(
361             codebase, proguard, "Proguard file"
362         ) { printWriter -> ProguardWriter(printWriter, apiEmit, apiReference) }
363     }
364 
365     options.sdkValueDir?.let { dir ->
366         dir.mkdirs()
367         SdkFileWriter(codebase, dir).generate()
368     }
369 
370     for (check in options.compatibilityChecks) {
371         checkCompatibility(codebase, check)
372     }
373 
374     val previousApiFile = options.migrateNullsFrom
375     if (previousApiFile != null) {
376         val previous =
377             if (previousApiFile.path.endsWith(DOT_JAR)) {
378                 loadFromJarFile(previousApiFile)
379             } else {
380                 SignatureFileLoader.load(
381                     file = previousApiFile,
382                     kotlinStyleNulls = options.inputKotlinStyleNulls
383                 )
384             }
385 
386         // If configured, checks for newly added nullness information compared
387         // to the previous stable API and marks the newly annotated elements
388         // as migrated (which will cause the Kotlin compiler to treat problems
389         // as warnings instead of errors
390 
391         migrateNulls(codebase, previous)
392 
393         previous.dispose()
394     }
395 
396     convertToWarningNullabilityAnnotations(codebase, options.forceConvertToWarningNullabilityAnnotations)
397 
398     // Now that we've migrated nullness information we can proceed to write non-doc stubs, if any.
399 
400     options.stubsDir?.let {
401         createStubFiles(
402             it, codebase, docStubs = false,
403             writeStubList = options.stubsSourceList != null
404         )
405 
406         val stubAnnotations = options.copyStubAnnotationsFrom
407         if (stubAnnotations != null) {
408             // Support pointing to both stub-annotations and stub-annotations/src/main/java
409             val src = File(stubAnnotations, "src${File.separator}main${File.separator}java")
410             val source = if (src.isDirectory) src else stubAnnotations
411             source.listFiles()?.forEach { file ->
412                 RewriteAnnotations().copyAnnotations(codebase, file, File(it, file.name))
413             }
414         }
415     }
416 
417     if (options.docStubsDir == null && options.stubsDir == null) {
418         val writeStubsFile: (File) -> Unit = { file ->
419             val root = File("").absoluteFile
420             val rootPath = root.path
421             val contents = sources.joinToString(" ") {
422                 val path = it.path
423                 if (path.startsWith(rootPath)) {
424                     path.substring(rootPath.length)
425                 } else {
426                     path
427                 }
428             }
429             file.writeText(contents)
430         }
431         options.stubsSourceList?.let(writeStubsFile)
432         options.docStubsSourceList?.let(writeStubsFile)
433     }
434     options.externalAnnotations?.let { extractAnnotations(codebase, it) }
435 
436     // Coverage stats?
437     if (options.dumpAnnotationStatistics) {
438         progress("Measuring annotation statistics: ")
439         AnnotationStatistics(codebase).count()
440     }
441     if (options.annotationCoverageOf.isNotEmpty()) {
442         progress("Measuring annotation coverage: ")
443         AnnotationStatistics(codebase).measureCoverageOf(options.annotationCoverageOf)
444     }
445 
446     if (options.verbose) {
447         val packageCount = codebase.size()
448         progress("$PROGRAM_NAME finished handling $packageCount packages in ${stopwatch.elapsed(SECONDS)} seconds\n")
449     }
450 
451     invokeDocumentationTool()
452 }
453 
subtractApinull454 fun subtractApi(codebase: Codebase, subtractApiFile: File) {
455     val path = subtractApiFile.path
456     val oldCodebase =
457         when {
458             path.endsWith(DOT_TXT) -> SignatureFileLoader.load(subtractApiFile)
459             path.endsWith(DOT_JAR) -> loadFromJarFile(subtractApiFile)
460             else -> throw DriverException("Unsupported $ARG_SUBTRACT_API format, expected .txt or .jar: ${subtractApiFile.name}")
461         }
462 
463     CodebaseComparator().compare(object : ComparisonVisitor() {
464         override fun compare(old: ClassItem, new: ClassItem) {
465             new.emit = false
466         }
467     }, oldCodebase, codebase, ApiType.ALL.getReferenceFilter())
468 }
469 
processNonCodebaseFlagsnull470 fun processNonCodebaseFlags() {
471     // --copy-annotations?
472     val privateAnnotationsSource = options.privateAnnotationsSource
473     val privateAnnotationsTarget = options.privateAnnotationsTarget
474     if (privateAnnotationsSource != null && privateAnnotationsTarget != null) {
475         val rewrite = RewriteAnnotations()
476         // Support pointing to both stub-annotations and stub-annotations/src/main/java
477         val src = File(privateAnnotationsSource, "src${File.separator}main${File.separator}java")
478         val source = if (src.isDirectory) src else privateAnnotationsSource
479         source.listFiles()?.forEach { file ->
480             rewrite.modifyAnnotationSources(null, file, File(privateAnnotationsTarget, file.name))
481         }
482     }
483 
484     // --rewrite-annotations?
485     options.rewriteAnnotations?.let { RewriteAnnotations().rewriteAnnotations(it) }
486 
487     // Convert android.jar files?
488     options.androidJarSignatureFiles?.let { root ->
489         // Generate API signature files for all the historical JAR files
490         ConvertJarsToSignatureFiles().convertJars(root)
491     }
492 
493     for (convert in options.convertToXmlFiles) {
494         val signatureApi = SignatureFileLoader.load(
495             file = convert.fromApiFile,
496             kotlinStyleNulls = options.inputKotlinStyleNulls
497         )
498 
499         val apiType = ApiType.ALL
500         val apiEmit = apiType.getEmitFilter()
501         val strip = convert.strip
502         val apiReference = if (strip) apiType.getEmitFilter() else apiType.getReferenceFilter()
503         val baseFile = convert.baseApiFile
504 
505         val outputApi =
506             if (baseFile != null) {
507                 // Convert base on a diff
508                 val baseApi = SignatureFileLoader.load(
509                     file = baseFile,
510                     kotlinStyleNulls = options.inputKotlinStyleNulls
511                 )
512 
513                 val includeFields =
514                     if (convert.outputFormat == FileFormat.V2) true else compatibility.includeFieldsInApiDiff
515                 TextCodebase.computeDelta(baseFile, baseApi, signatureApi, includeFields)
516             } else {
517                 signatureApi
518             }
519 
520         if (outputApi.isEmpty() && baseFile != null && compatibility.compat) {
521             // doclava compatibility: emits error warning instead of emitting empty <api/> element
522             options.stdout.println("No API change detected, not generating diff")
523         } else {
524             val output = convert.outputFile
525             if (convert.outputFormat == FileFormat.JDIFF) {
526                 // See JDiff's XMLToAPI#nameAPI
527                 val apiName = convert.outputFile.nameWithoutExtension.replace(' ', '_')
528                 createReportFile(outputApi, output, "JDiff File") { printWriter ->
529                     JDiffXmlWriter(printWriter, apiEmit, apiReference, signatureApi.preFiltered && !strip, apiName)
530                 }
531             } else {
532                 val prevOptions = options
533                 val prevCompatibility = compatibility
534                 try {
535                     when (convert.outputFormat) {
536                         FileFormat.V1 -> {
537                             compatibility = Compatibility(true)
538                             options = Options(emptyArray(), options.stdout, options.stderr)
539                             FileFormat.V1.configureOptions(options, compatibility)
540                         }
541                         FileFormat.V2 -> {
542                             compatibility = Compatibility(false)
543                             options = Options(emptyArray(), options.stdout, options.stderr)
544                             FileFormat.V2.configureOptions(options, compatibility)
545                         }
546                         else -> error("Unsupported format ${convert.outputFormat}")
547                     }
548 
549                     createReportFile(outputApi, output, "Diff API File") { printWriter ->
550                         SignatureWriter(
551                             printWriter, apiEmit, apiReference, signatureApi.preFiltered && !strip
552                         )
553                     }
554                 } finally {
555                     options = prevOptions
556                     compatibility = prevCompatibility
557                 }
558             }
559         }
560     }
561 }
562 
563 /**
564  * Checks compatibility of the given codebase with the codebase described in the
565  * signature file.
566  */
checkCompatibilitynull567 fun checkCompatibility(
568     codebase: Codebase,
569     check: CheckRequest
570 ) {
571     progress("Checking API compatibility ($check): ")
572     val signatureFile = check.file
573 
574     val current =
575         if (signatureFile.path.endsWith(DOT_JAR)) {
576             loadFromJarFile(signatureFile)
577         } else {
578             SignatureFileLoader.load(
579                 file = signatureFile,
580                 kotlinStyleNulls = options.inputKotlinStyleNulls
581             )
582         }
583 
584     if (current is TextCodebase && current.format > FileFormat.V1 && options.outputFormat == FileFormat.V1) {
585         throw DriverException("Cannot perform compatibility check of signature file $signatureFile in format ${current.format} without analyzing current codebase with $ARG_FORMAT=${current.format}")
586     }
587 
588     var newBase: Codebase? = null
589     var oldBase: Codebase? = null
590     val releaseType = check.releaseType
591     val apiType = check.apiType
592 
593     // If diffing with a system-api or test-api (or other signature-based codebase
594     // generated from --show-annotations), the API is partial: it's only listing
595     // the API that is *different* from the base API. This really confuses the
596     // codebase comparison when diffing with a complete codebase, since it looks like
597     // many classes and members have been added and removed. Therefore, the comparison
598     // is simpler if we just make the comparison with the same generated signature
599     // file. If we've only emitted one for the new API, use it directly, if not, generate
600     // it first
601     val new =
602         if (check.codebase != null) {
603             SignatureFileLoader.load(
604                 file = check.codebase,
605                 kotlinStyleNulls = options.inputKotlinStyleNulls
606             )
607         } else if (!options.showUnannotated || apiType != ApiType.PUBLIC_API) {
608             if (options.baseApiForCompatCheck != null) {
609                 // This option does not make sense with showAnnotation, as the "base" in that case
610                 // is the non-annotated APIs.
611                 throw DriverException(ARG_CHECK_COMPATIBILITY_BASE_API +
612                     " is not compatible with --showAnnotation.")
613             }
614 
615             newBase = codebase
616             oldBase = newBase
617 
618             codebase
619         } else {
620             // Fast path: if we've already generated a signature file and it's identical, we're good!
621             val apiFile = options.apiFile
622             if (apiFile != null && apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
623                 return
624             }
625 
626             val baseApiFile = options.baseApiForCompatCheck
627             if (baseApiFile != null) {
628                 oldBase = SignatureFileLoader.load(
629                     file = baseApiFile,
630                     kotlinStyleNulls = options.inputKotlinStyleNulls
631                 )
632                 newBase = oldBase
633             }
634 
635             codebase
636         }
637 
638     // If configured, compares the new API with the previous API and reports
639     // any incompatibilities.
640     CompatibilityCheck.checkCompatibility(new, current, releaseType, apiType, oldBase, newBase)
641 
642     // Make sure the text files are identical too? (only applies for *current.txt;
643     // last-released is expected to differ)
644     if (releaseType == ReleaseType.DEV && !options.allowCompatibleDifferences) {
645         val apiFile = if (new.location.isFile)
646             new.location
647         else
648             apiType.getSignatureFile(codebase, "compat-diff-signatures-$apiType")
649 
650         fun getCanonicalSignatures(file: File): String {
651             // Get rid of trailing newlines and Windows line endings
652             val text = file.readText(UTF_8)
653             return text.replace("\r\n", "\n").trim()
654         }
655         val currentTxt = getCanonicalSignatures(signatureFile)
656         val newTxt = getCanonicalSignatures(apiFile)
657         if (newTxt != currentTxt) {
658             val diff = getNativeDiff(signatureFile, apiFile) ?: getDiff(currentTxt, newTxt, 1)
659             val updateApi = if (isBuildingAndroid())
660                 "Run make update-api to update.\n"
661             else
662                 ""
663             val message =
664                 """
665                     Your changes have resulted in differences in the signature file
666                     for the ${apiType.displayName} API.
667 
668                     The changes may be compatible, but the signature file needs to be updated.
669                     $updateApi
670                     Diffs:
671                 """.trimIndent() + "\n" + diff
672 
673             throw DriverException(exitCode = -1, stderr = message)
674         }
675     }
676 }
677 
createTempFilenull678 fun createTempFile(namePrefix: String, nameSuffix: String): File {
679     val tempFolder = options.tempFolder
680     return if (tempFolder != null) {
681         val preferred = File(tempFolder, namePrefix + nameSuffix)
682         if (!preferred.exists()) {
683             return preferred
684         }
685         File.createTempFile(namePrefix, nameSuffix, tempFolder)
686     } else {
687         File.createTempFile(namePrefix, nameSuffix)
688     }
689 }
690 
invokeDocumentationToolnull691 fun invokeDocumentationTool() {
692     if (options.noDocs) {
693         return
694     }
695 
696     val args = options.invokeDocumentationToolArguments
697     if (args.isNotEmpty()) {
698         if (!options.quiet) {
699             options.stdout.println(
700                 "Invoking external documentation tool ${args[0]} with arguments\n\"${
701                 args.slice(1 until args.size).joinToString(separator = "\",\n\"") { it }}\""
702             )
703             options.stdout.flush()
704         }
705 
706         val builder = ProcessInfoBuilder()
707 
708         builder.setExecutable(File(args[0]))
709         builder.addArgs(args.slice(1 until args.size))
710 
711         val processOutputHandler =
712             if (options.quiet) {
713                 CachedProcessOutputHandler()
714             } else {
715                 object : ProcessOutputHandler {
716                     override fun handleOutput(processOutput: ProcessOutput?) {
717                     }
718 
719                     override fun createOutput(): ProcessOutput {
720                         val out = PrintWriterOutputStream(options.stdout)
721                         val err = PrintWriterOutputStream(options.stderr)
722                         return object : ProcessOutput {
723                             override fun getStandardOutput(): OutputStream {
724                                 return out
725                             }
726 
727                             override fun getErrorOutput(): OutputStream {
728                                 return err
729                             }
730 
731                             override fun close() {
732                                 out.flush()
733                                 err.flush()
734                             }
735                         }
736                     }
737                 }
738             }
739 
740         val result = DefaultProcessExecutor(StdLogger(ERROR))
741             .execute(builder.createProcess(), processOutputHandler)
742 
743         val exitCode = result.exitValue
744         if (!options.quiet) {
745             options.stdout.println("${args[0]} finished with exitCode $exitCode")
746             options.stdout.flush()
747         }
748         if (exitCode != 0) {
749             val stdout = if (processOutputHandler is CachedProcessOutputHandler)
750                 processOutputHandler.processOutput.standardOutputAsString
751             else ""
752             val stderr = if (processOutputHandler is CachedProcessOutputHandler)
753                 processOutputHandler.processOutput.errorOutputAsString
754             else ""
755             throw DriverException(
756                 stdout = "Invoking documentation tool ${args[0]} failed with exit code $exitCode\n$stdout",
757                 stderr = stderr,
758                 exitCode = exitCode
759             )
760         }
761     }
762 }
763 
764 class PrintWriterOutputStream(private val writer: PrintWriter) : OutputStream() {
765 
writenull766     override fun write(b: ByteArray) {
767         writer.write(String(b, UTF_8))
768     }
769 
writenull770     override fun write(b: Int) {
771         write(byteArrayOf(b.toByte()), 0, 1)
772     }
773 
writenull774     override fun write(b: ByteArray, off: Int, len: Int) {
775         writer.write(String(b, off, len, UTF_8))
776     }
777 
flushnull778     override fun flush() {
779         writer.flush()
780     }
781 
closenull782     override fun close() {
783         writer.close()
784     }
785 }
786 
migrateNullsnull787 private fun migrateNulls(codebase: Codebase, previous: Codebase) {
788     previous.compareWith(NullnessMigration(), codebase)
789 }
790 
convertToWarningNullabilityAnnotationsnull791 private fun convertToWarningNullabilityAnnotations(codebase: Codebase, filter: PackageFilter?) {
792     if (filter != null) {
793         // Our caller has asked for these APIs to not trigger nullness errors (only warnings) if
794         // their callers make incorrect nullness assumptions (for example, calling a function on a
795         // reference of nullable type). The way to communicate this to kotlinc is to mark these
796         // APIs as RecentlyNullable/RecentlyNonNull
797         codebase.accept(MarkPackagesAsRecent(filter))
798     }
799 }
800 
loadFromSourcesnull801 private fun loadFromSources(): Codebase {
802     progress("Processing sources: ")
803 
804     val sources = if (options.sources.isEmpty()) {
805         if (options.verbose) {
806             options.stdout.println("No source files specified: recursively including all sources found in the source path (${options.sourcePath.joinToString()}})")
807         }
808         gatherSources(options.sourcePath)
809     } else {
810         options.sources
811     }
812 
813     progress("Reading Codebase: ")
814     val codebase = parseSources(sources, "Codebase loaded from source folders")
815 
816     progress("Analyzing API: ")
817 
818     val analyzer = ApiAnalyzer(codebase)
819     analyzer.mergeExternalInclusionAnnotations()
820     analyzer.computeApi()
821 
822     val filterEmit = ApiPredicate(ignoreShown = true, ignoreRemoved = false)
823     val apiEmit = ApiPredicate(ignoreShown = true)
824     val apiReference = ApiPredicate(ignoreShown = true)
825 
826     // Copy methods from soon-to-be-hidden parents into descendant classes, when necessary. Do
827     // this before merging annotations or performing checks on the API to ensure that these methods
828     // can have annotations added and are checked properly.
829     progress("Insert missing stubs methods: ")
830     analyzer.generateInheritedStubs(apiEmit, apiReference)
831 
832     analyzer.mergeExternalQualifierAnnotations()
833     options.nullabilityAnnotationsValidator?.validateAllFrom(codebase, options.validateNullabilityFromList)
834     options.nullabilityAnnotationsValidator?.report()
835     analyzer.handleStripping()
836 
837     // General API checks for Android APIs
838     AndroidApiChecks().check(codebase)
839 
840     if (options.checkApi) {
841         progress("API Lint: ")
842         val localTimer = Stopwatch.createStarted()
843         // See if we should provide a previous codebase to provide a delta from?
844         val previousApiFile = options.checkApiBaselineApiFile
845         val previous =
846             when {
847                 previousApiFile == null -> null
848                 previousApiFile.path.endsWith(DOT_JAR) -> loadFromJarFile(previousApiFile)
849                 else -> SignatureFileLoader.load(
850                     file = previousApiFile,
851                     kotlinStyleNulls = options.inputKotlinStyleNulls
852                 )
853             }
854         val apiLintReporter = options.reporterApiLint
855         ApiLint.check(codebase, previous, apiLintReporter)
856         progress("$PROGRAM_NAME ran api-lint in ${localTimer.elapsed(SECONDS)} seconds with ${apiLintReporter.getBaselineDescription()}")
857     }
858 
859     // Compute default constructors (and add missing package private constructors
860     // to make stubs compilable if necessary). Do this after all the checks as
861     // these are not part of the API.
862     if (options.stubsDir != null || options.docStubsDir != null) {
863         progress("Insert missing constructors: ")
864         analyzer.addConstructors(filterEmit)
865     }
866 
867     progress("Performing misc API checks: ")
868     analyzer.performChecks()
869 
870     return codebase
871 }
872 
873 /**
874  * Returns a codebase initialized from the given Java or Kotlin source files, with the given
875  * description. The codebase will use a project environment initialized according to the current
876  * [options].
877  */
parseSourcesnull878 internal fun parseSources(
879     sources: List<File>,
880     description: String,
881     sourcePath: List<File> = options.sourcePath,
882     classpath: List<File> = options.classpath,
883     javaLanguageLevel: LanguageLevel = options.javaLanguageLevel,
884     kotlinLanguageLevel: LanguageVersionSettings = options.kotlinLanguageLevel,
885     manifest: File? = options.manifest,
886     currentApiLevel: Int = options.currentApiLevel + if (options.currentCodeName != null) 1 else 0
887 ): PsiBasedCodebase {
888     val sourceRoots = mutableListOf<File>()
889     sourcePath.filterTo(sourceRoots) { it.path.isNotBlank() }
890     // Add in source roots implied by the source files
891     if (options.allowImplicitRoot) {
892         extractRoots(sources, sourceRoots)
893     }
894 
895     val config = UastEnvironment.Configuration.create()
896     config.javaLanguageLevel = javaLanguageLevel
897     config.kotlinLanguageLevel = kotlinLanguageLevel
898     config.addSourceRoots(sourceRoots.map { it.absoluteFile })
899     config.addClasspathRoots(classpath.map { it.absoluteFile })
900 
901     val environment = createProjectEnvironment(config)
902 
903     val kotlinFiles = sources.filter { it.path.endsWith(DOT_KT) }
904     environment.analyzeFiles(kotlinFiles)
905 
906     val rootDir = sourceRoots.firstOrNull() ?: sourcePath.firstOrNull() ?: File("").canonicalFile
907 
908     val units = Extractor.createUnitsForFiles(environment.ideaProject, sources)
909     val packageDocs = gatherPackageJavadoc(sources, sourceRoots)
910 
911     val codebase = PsiBasedCodebase(rootDir, description)
912     codebase.initialize(environment, units, packageDocs)
913     codebase.manifest = manifest
914     codebase.apiLevel = currentApiLevel
915     return codebase
916 }
917 
loadFromJarFilenull918 fun loadFromJarFile(apiJar: File, manifest: File? = null, preFiltered: Boolean = false): Codebase {
919     progress("Processing jar file: ")
920 
921     val config = UastEnvironment.Configuration.create()
922     config.addClasspathRoots(listOf(apiJar))
923 
924     val environment = createProjectEnvironment(config)
925     environment.analyzeFiles(emptyList()) // Initializes PSI machinery.
926 
927     val codebase = PsiBasedCodebase(apiJar, "Codebase loaded from $apiJar")
928     codebase.initialize(environment, apiJar, preFiltered)
929     if (manifest != null) {
930         codebase.manifest = options.manifest
931     }
932     val apiEmit = ApiPredicate(ignoreShown = true)
933     val apiReference = ApiPredicate(ignoreShown = true)
934     val analyzer = ApiAnalyzer(codebase)
935     analyzer.mergeExternalInclusionAnnotations()
936     analyzer.computeApi()
937     analyzer.mergeExternalQualifierAnnotations()
938     options.nullabilityAnnotationsValidator?.validateAllFrom(codebase, options.validateNullabilityFromList)
939     options.nullabilityAnnotationsValidator?.report()
940     analyzer.generateInheritedStubs(apiEmit, apiReference)
941     return codebase
942 }
943 
944 internal const val METALAVA_SYNTHETIC_SUFFIX = "metalava_module"
945 
createProjectEnvironmentnull946 private fun createProjectEnvironment(config: UastEnvironment.Configuration): UastEnvironment {
947     ensurePsiFileCapacity()
948 
949     // Note: the Kotlin module name affects the naming of certain synthetic methods.
950     config.kotlinCompilerConfig.put(MODULE_NAME, METALAVA_SYNTHETIC_SUFFIX)
951 
952     val environment = UastEnvironment.create(config)
953     uastEnvironments.add(environment)
954 
955     if (!assertionsEnabled() &&
956         System.getenv(ENV_VAR_METALAVA_DUMP_ARGV) == null &&
957         !isUnderTest()
958     ) {
959         DefaultLogger.disableStderrDumping(environment.ideaProject)
960     }
961 
962     // Missing service needed in metalava but not in lint: javadoc handling
963     environment.ideaProject.registerService(
964         com.intellij.psi.javadoc.JavadocManager::class.java,
965         com.intellij.psi.impl.source.javadoc.JavadocManagerImpl::class.java
966     )
967     CoreApplicationEnvironment.registerExtensionPoint(
968         environment.ideaProject.extensionArea, JavadocTagInfo.EP_NAME, JavadocTagInfo::class.java
969     )
970     CoreApplicationEnvironment.registerApplicationExtensionPoint(
971         CustomJavadocTagProvider.EP_NAME, CustomJavadocTagProvider::class.java
972     )
973 
974     return environment
975 }
976 
977 private val uastEnvironments = mutableListOf<UastEnvironment>()
978 
disposeUastEnvironmentnull979 private fun disposeUastEnvironment() {
980     // Codebase.dispose() is not consistently called, so we dispose the environments here too.
981     for (env in uastEnvironments) {
982         if (!Disposer.isDisposed(env.ideaProject)) {
983             env.dispose()
984         }
985     }
986     uastEnvironments.clear()
987     UastEnvironment.disposeApplicationEnvironment()
988 }
989 
ensurePsiFileCapacitynull990 private fun ensurePsiFileCapacity() {
991     val fileSize = System.getProperty("idea.max.intellisense.filesize")
992     if (fileSize == null) {
993         // Ensure we can handle large compilation units like android.R
994         System.setProperty("idea.max.intellisense.filesize", "100000")
995     }
996 }
997 
extractAnnotationsnull998 private fun extractAnnotations(codebase: Codebase, file: File) {
999     val localTimer = Stopwatch.createStarted()
1000 
1001     options.externalAnnotations?.let { outputFile ->
1002         @Suppress("UNCHECKED_CAST")
1003         ExtractAnnotations(
1004             codebase,
1005             outputFile
1006         ).extractAnnotations()
1007         if (options.verbose) {
1008             progress("$PROGRAM_NAME extracted annotations into $file in ${localTimer.elapsed(SECONDS)} seconds\n")
1009         }
1010     }
1011 }
1012 
createStubFilesnull1013 private fun createStubFiles(stubDir: File, codebase: Codebase, docStubs: Boolean, writeStubList: Boolean) {
1014     // Generating stubs from a sig-file-based codebase is problematic
1015     assert(codebase.supportsDocumentation())
1016 
1017     // Temporary bug workaround for org.chromium.arc
1018     if (options.sourcePath.firstOrNull()?.path?.endsWith("org.chromium.arc") == true) {
1019         codebase.findClass("org.chromium.mojo.bindings.Callbacks")?.hidden = true
1020     }
1021 
1022     if (docStubs) {
1023         progress("Generating documentation stub files: ")
1024     } else {
1025         progress("Generating stub files: ")
1026     }
1027 
1028     val localTimer = Stopwatch.createStarted()
1029     val prevCompatibility = compatibility
1030     if (compatibility.compat) {
1031         compatibility = Compatibility(false)
1032         // But preserve the setting for whether we want to erase throws signatures (to ensure the API
1033         // stays compatible)
1034         compatibility.useErasureInThrows = prevCompatibility.useErasureInThrows
1035     }
1036 
1037     val stubWriter =
1038         StubWriter(
1039             codebase = codebase,
1040             stubsDir = stubDir,
1041             generateAnnotations = options.generateAnnotations,
1042             preFiltered = codebase.preFiltered,
1043             docStubs = docStubs
1044         )
1045     codebase.accept(stubWriter)
1046 
1047     if (docStubs) {
1048         // Overview docs? These are generally in the empty package.
1049         codebase.findPackage("")?.let { empty ->
1050             val overview = codebase.getPackageDocs()?.getOverviewDocumentation(empty)
1051             if (overview != null && overview.isNotBlank()) {
1052                 stubWriter.writeDocOverview(empty, overview)
1053             }
1054         }
1055     }
1056 
1057     if (writeStubList) {
1058         // Optionally also write out a list of source files that were generated; used
1059         // for example to point javadoc to the stubs output to generate documentation
1060         val file = if (docStubs) {
1061             options.docStubsSourceList ?: options.stubsSourceList
1062         } else {
1063             options.stubsSourceList
1064         }
1065         file?.let {
1066             val root = File("").absoluteFile
1067             stubWriter.writeSourceList(it, root)
1068         }
1069     }
1070 
1071     compatibility = prevCompatibility
1072 
1073     progress(
1074         "$PROGRAM_NAME wrote ${if (docStubs) "documentation" else ""} stubs directory $stubDir in ${
1075         localTimer.elapsed(SECONDS)} seconds\n"
1076     )
1077 }
1078 
createReportFilenull1079 fun createReportFile(
1080     codebase: Codebase,
1081     apiFile: File,
1082     description: String?,
1083     createVisitor: (PrintWriter) -> ApiVisitor
1084 ) {
1085     if (description != null) {
1086         progress("Writing $description file: ")
1087     }
1088     val localTimer = Stopwatch.createStarted()
1089     try {
1090         val writer = PrintWriter(Files.asCharSink(apiFile, UTF_8).openBufferedStream())
1091         writer.use { printWriter ->
1092             val apiWriter = createVisitor(printWriter)
1093             codebase.accept(apiWriter)
1094         }
1095     } catch (e: IOException) {
1096         reporter.report(Issues.IO_ERROR, apiFile, "Cannot open file for write.")
1097     }
1098     if (description != null && options.verbose) {
1099         progress("$PROGRAM_NAME wrote $description file $apiFile in ${localTimer.elapsed(SECONDS)} seconds\n")
1100     }
1101 }
1102 
skippableDirectorynull1103 private fun skippableDirectory(file: File): Boolean = file.path.endsWith(".git") && file.name == ".git"
1104 
1105 private fun addSourceFiles(list: MutableList<File>, file: File) {
1106     if (file.isDirectory) {
1107         if (skippableDirectory(file)) {
1108             return
1109         }
1110         if (java.nio.file.Files.isSymbolicLink(file.toPath())) {
1111             reporter.report(
1112                 Issues.IGNORING_SYMLINK, file,
1113                 "Ignoring symlink during source file discovery directory traversal"
1114             )
1115             return
1116         }
1117         val files = file.listFiles()
1118         if (files != null) {
1119             for (child in files) {
1120                 addSourceFiles(list, child)
1121             }
1122         }
1123     } else if (file.isFile) {
1124         when {
1125             file.name.endsWith(DOT_JAVA) ||
1126             file.name.endsWith(DOT_KT) ||
1127             file.name.equals(PACKAGE_HTML) ||
1128             file.name.equals(OVERVIEW_HTML) -> list.add(file)
1129         }
1130     }
1131 }
1132 
gatherSourcesnull1133 fun gatherSources(sourcePath: List<File>): List<File> {
1134     val sources = Lists.newArrayList<File>()
1135     for (file in sourcePath) {
1136         if (file.path.isBlank()) {
1137             // --source-path "" means don't search source path; use "." for pwd
1138             continue
1139         }
1140         addSourceFiles(sources, file.absoluteFile)
1141     }
1142     return sources.sortedWith(compareBy { it.name })
1143 }
1144 
gatherPackageJavadocnull1145 private fun gatherPackageJavadoc(sources: List<File>, sourceRoots: List<File>): PackageDocs {
1146     val packageComments = HashMap<String, String>(100)
1147     val overviewHtml = HashMap<String, String>(10)
1148     val hiddenPackages = HashSet<String>(100)
1149     val sortedSourceRoots = sourceRoots.sortedBy { -it.name.length }
1150     for (file in sources) {
1151         var javadoc = false
1152         val map = when (file.name) {
1153             PACKAGE_HTML -> {
1154                 javadoc = true; packageComments
1155             }
1156             OVERVIEW_HTML -> {
1157                 overviewHtml
1158             }
1159             else -> continue
1160         }
1161         var contents = Files.asCharSource(file, UTF_8).read()
1162         if (javadoc) {
1163             contents = packageHtmlToJavadoc(contents)
1164         }
1165 
1166         // Figure out the package: if there is a java file in the same directory, get the package
1167         // name from the java file. Otherwise, guess from the directory path + source roots.
1168         // NOTE: This causes metalava to read files other than the ones explicitly passed to it.
1169         var pkg = file.parentFile?.listFiles()
1170             ?.filter { it.name.endsWith(DOT_JAVA) }
1171             ?.asSequence()?.mapNotNull { findPackage(it) }
1172             ?.firstOrNull()
1173         if (pkg == null) {
1174             // Strip the longest prefix source root.
1175             val prefix = sortedSourceRoots.firstOrNull { file.startsWith(it) }?.path ?: ""
1176             pkg = file.parentFile.path.substring(prefix.length).trim('/').replace("/", ".")
1177         }
1178         map[pkg] = contents
1179         if (contents.contains("@hide")) {
1180             hiddenPackages.add(pkg)
1181         }
1182     }
1183 
1184     return PackageDocs(packageComments, overviewHtml, hiddenPackages)
1185 }
1186 
extractRootsnull1187 fun extractRoots(sources: List<File>, sourceRoots: MutableList<File> = mutableListOf()): List<File> {
1188     // Cache for each directory since computing root for a source file is
1189     // expensive
1190     val dirToRootCache = mutableMapOf<String, File>()
1191     for (file in sources) {
1192         val parent = file.parentFile ?: continue
1193         val found = dirToRootCache[parent.path]
1194         if (found != null) {
1195             continue
1196         }
1197 
1198         val root = findRoot(file) ?: continue
1199         dirToRootCache[parent.path] = root
1200 
1201         if (!sourceRoots.contains(root)) {
1202             sourceRoots.add(root)
1203         }
1204     }
1205 
1206     return sourceRoots
1207 }
1208 
1209 /**
1210  * If given a full path to a Java or Kotlin source file, produces the path to
1211  * the source root if possible.
1212  */
findRootnull1213 private fun findRoot(file: File): File? {
1214     val path = file.path
1215     if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_KT)) {
1216         val pkg = findPackage(file) ?: return null
1217         val parent = file.parentFile ?: return null
1218         val endIndex = parent.path.length - pkg.length
1219         val before = path[endIndex - 1]
1220         if (before == '/' || before == '\\') {
1221             return File(path.substring(0, endIndex))
1222         } else {
1223             reporter.report(
1224                 Issues.IO_ERROR, file, "$PROGRAM_NAME was unable to determine the package name. " +
1225                     "This usually means that a source file was where the directory does not seem to match the package " +
1226                     "declaration; we expected the path $path to end with /${pkg.replace('.', '/') + '/' + file.name}"
1227             )
1228         }
1229     }
1230 
1231     return null
1232 }
1233 
1234 /** Finds the package of the given Java/Kotlin source file, if possible */
findPackagenull1235 fun findPackage(file: File): String? {
1236     val source = Files.asCharSource(file, UTF_8).read()
1237     return findPackage(source)
1238 }
1239 
1240 /** Finds the package of the given Java/Kotlin source code, if possible */
findPackagenull1241 fun findPackage(source: String): String? {
1242     return ClassName(source).packageName
1243 }
1244 
1245 /** Whether metalava is running unit tests */
isUnderTestnull1246 fun isUnderTest() = java.lang.Boolean.getBoolean(ENV_VAR_METALAVA_TESTS_RUNNING)
1247 
1248 /** Whether metalava is being invoked as part of an Android platform build */
1249 fun isBuildingAndroid() = System.getenv("ANDROID_BUILD_TOP") != null && !isUnderTest()
1250