diff --git a/src/main/kotlin/com/getkeepsafe/dexcount/SourceFile.kt b/src/main/kotlin/com/getkeepsafe/dexcount/SourceFile.kt index 7fc2e1a6..f208ac01 100644 --- a/src/main/kotlin/com/getkeepsafe/dexcount/SourceFile.kt +++ b/src/main/kotlin/com/getkeepsafe/dexcount/SourceFile.kt @@ -111,11 +111,27 @@ internal class DexFile( @JvmStatic private fun extractDexFromAar(file: File, dxTimeoutSecs: Int): List { // unzip classes.jar from the AAR + var minSdk = 13 // the default minSdkVersion of dx val tempClasses = file.unzip { entries -> - val jarFile = entries.find { it.name.matches(Regex("classes\\.jar")) } - makeTempFile("classes.jar").also { - jarFile!!.writeTo(it) + var tempFile: File? = null + + for (entry in entries) { + if (entry.name == "AndroidManifest.xml") { + val manifestText = entry.inputStream().bufferedReader().use { it.readText() } + val match = Regex("""android:minSdkVersion="(\d+)"""").find(manifestText) + if (match != null) { + minSdk = match.groupValues[1].toInt() + } + } + + if (entry.name.matches(Regex("classes\\.jar"))) { + tempFile = makeTempFile("classes.jar").also { + entry.writeTo(it) + } + } } + + checkNotNull(tempFile) { "No classes.jar file found in ${file.canonicalPath}" } } // convert it to DEX format by using the Android dx tool @@ -137,36 +153,34 @@ internal class DexFile( throw Exception ("dx tool not found at " + dxExe.absolutePath) } - // ~/android-sdk/build-tools/23.0.3/dx --dex --output=temp.dex classes.jar - val tempDex = makeTempFile("classes.dex") + // Figure out if this version of dx has the --min-sdk-version flag + val dexHelp = runProcess(dxExe, listOf("--help"), timeoutMillis = 5000L) + if (dexHelp.timedOut || dexHelp.exitCode != 1) { // dx --help seems to always exit with code 1 + throw DexCountException("Unable to run dx --help (file: $dxExe)") + } - val sout = StringBuilder() - val serr = StringBuilder() - val proc = ProcessBuilder().command( - dxExe.absolutePath, - "--dex", - "--output=${tempDex.absolutePath}", - tempClasses.absolutePath) - .start() + val hasMinSdkFlag = dexHelp.stdout.contains("--min-sdk-version") || dexHelp.stderr.contains("--min-sdk-version") - val didFinish = proc.waitForProcessOutput( - stdout = sout, - stderr = serr, - timeoutMillis = TimeUnit.SECONDS.toMillis(dxTimeoutSecs.toLong())) + // ~/android-sdk/build-tools/23.0.3/dx --dex --output=temp.dex classes.jar + val tempDex = makeTempFile("classes.dex") - val exitCode = if (didFinish) proc.exitValue() else -1 - proc.dispose() + val dexArgs = mutableListOf("--dex", "--output=${tempDex.absolutePath}") + if (hasMinSdkFlag) { + dexArgs += "--min-sdk-version=$minSdk" + } + dexArgs += tempClasses.absolutePath - if (!didFinish) { + val dexRun = runProcess(dxExe, dexArgs, TimeUnit.SECONDS.toMillis(dxTimeoutSecs.toLong())) + if (dexRun.timedOut) { throw DexCountException("dx timed out after $dxTimeoutSecs seconds") } - if (exitCode != 0) { - throw DexCountException("dx exited with exit code $exitCode\nstderr=$serr") + if (dexRun.exitCode != 0) { + throw DexCountException("dx exited with exit code ${dexRun.exitCode}\nstderr=${dexRun.stderr}") } if (!tempDex.exists()) { - throw DexCountException("Error converting classes.jar into classes.dex: $serr") + throw DexCountException("Error converting classes.jar into classes.dex: ${dexRun.stderr}") } return listOf(DexFile(tempDex, true)) @@ -344,6 +358,30 @@ private class StreamableZipEntry( } } +private data class Execution( + val timedOut: Boolean, + val exitCode: Int, + val stdout: StringBuilder, + val stderr: StringBuilder +) + +private fun runProcess(exe: File, args: List, timeoutMillis: Long): Execution { + val stdout = StringBuilder() + val stderr = StringBuilder() + + val proc = ProcessBuilder( + exe.absolutePath, + *args.toTypedArray() + ).start() + + val didFinish = proc.waitForProcessOutput(stdout, stderr, timeoutMillis) + val exitCode = if (didFinish) proc.exitValue() else -1 + + proc.dispose() + + return Execution(!didFinish, exitCode, stdout, stderr) +} + private fun Process.waitForProcessOutput(stdout: Appendable, stderr: Appendable, timeoutMillis: Long): Boolean { val (o, e) = consumeStdout(stdout) to consumeStderr(stderr)