com.getkeepsafe.dexcount.DexMethodCountTask.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dexcount-gradle-plugin Show documentation
Show all versions of dexcount-gradle-plugin Show documentation
A Gradle plugin for counting methods in an .apk
/*
* Copyright (C) 2015-2016 KeepSafe Software
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getkeepsafe.dexcount
import com.android.annotations.Nullable
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.logging.Logger
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction
class DexMethodCountTask extends DefaultTask {
/**
* The maximum number of method refs and field refs allowed in a single Dex
* file.
*/
private static final int MAX_DEX_REFS = 0xFFFF // 65535
PackageTree tree
String variantOutputName
/**
* An APK, AAR, or .dex file. Will be null for Android projects
* using build-tools version 3.0 and above.
*/
@InputFile
@Optional
File inputFile
/**
* The output directory of the 'package' task; will contain an
* APK or an AAR. Will be null for Android projects using
* build-tools versions before 3.0.
*/
@InputDirectory
@Optional
File inputDirectory
@Nullable
File mappingFile
File outputFile
File summaryFile
File chartDir
DexMethodCountExtension config
long startTime
long ioTime
long treegenTime
long outputTime
boolean isInstantRun
@TaskAction countMethods() {
try {
if (!checkIfApkExists()) {
return
}
printPreamble()
generatePackageTree()
printSummary()
printFullTree()
printChart()
printTaskDiagnosticData()
failBuildMaxMethods()
} catch (DexCountException e) {
withStyledOutput() { out ->
out.error("Error counting dex methods. Please contact the developer at https://github.com/KeepSafe/dexcount-gradle-plugin/issues", e)
}
}
}
static percentUsed(int count) {
def used = ((double) count / MAX_DEX_REFS) * 100.0
return sprintf("%.2f", used)
}
def printPreamble() {
if (config.printVersion) {
def projectName = getClass().package.implementationTitle
def projectVersion = getClass().package.implementationVersion
withStyledOutput() { out ->
out.lifecycle("Dexcount name: $projectName")
out.lifecycle("Dexcount version: $projectVersion")
out.debug( "inputFile: $inputFile")
out.debug( "inputDirectory: $inputDirectory")
}
}
}
/**
* Prints a summary of method and field counts
* @return
*/
def printSummary() {
def filename = fileToCount().name
if (isInstantRun) {
withStyledOutput() { out ->
out.warn("Warning: Instant Run build detected! Instant Run does not run Proguard; method counts may be inaccurate.")
}
}
withStyledOutput() { out ->
def percentMethodsUsed = percentUsed(tree.methodCount)
def percentFieldsUsed = percentUsed(tree.fieldCount)
def methodsRemaining = Math.max(MAX_DEX_REFS - tree.methodCount, 0)
def fieldsRemaining = Math.max(MAX_DEX_REFS - tree.fieldCount, 0)
out.warn("Total methods in ${filename}: ${tree.methodCount} ($percentMethodsUsed% used)")
out.warn("Total fields in ${filename}: ${tree.fieldCount} ($percentFieldsUsed% used)")
out.warn("Methods remaining in ${filename}: $methodsRemaining")
out.warn("Fields remaining in ${filename}: $fieldsRemaining")
}
if (summaryFile != null) {
summaryFile.parentFile.mkdirs()
summaryFile.createNewFile()
final String headers = "methods,fields"
final String counts = "${tree.methodCount},${tree.fieldCount}"
summaryFile.withOutputStream { stream ->
def appendableStream = new PrintStream(stream)
appendableStream.println(headers)
appendableStream.println(counts)
}
}
if (getPrintOptions().teamCityIntegration || config.teamCitySlug != null) {
withStyledOutput() { out ->
def prefix = "Dexcount"
if (config.teamCitySlug != null) {
def slug = config.teamCitySlug.replace(' ', '_') // Not sure how TeamCity would handle spaces?
prefix = "${prefix}_${slug}"
}
printTeamCityStatisticValue(out, "${prefix}_${variantOutputName}_ClassCount", tree.classCount)
printTeamCityStatisticValue(out, "${prefix}_${variantOutputName}_MethodCount", tree.methodCount)
printTeamCityStatisticValue(out, "${prefix}_${variantOutputName}_FieldCount", tree.fieldCount)
}
}
}
/**
* Reports to Team City statistic value
* Doc: https://confluence.jetbrains.com/display/TCD9/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-ReportingBuildStatistics
*/
static printTeamCityStatisticValue(Logger out, String key, int value) {
out.lifecycle("##teamcity[buildStatisticValue key='${key}' value='${value}']")
}
/**
* Prints the package tree to the usual outputs/dexcount/variant.txt file.
*/
def printFullTree() {
printToFile(outputFile) { PrintStream out ->
print(tree, out)
}
outputTime = System.currentTimeMillis()
}
/**
* Prints the package tree as chart to the outputs/dexcount/${variant}Chart directory.
*/
def printChart() {
def printOptions = getPrintOptions()
printOptions.includeClasses = true
printToFile(new File(chartDir, "data.js")) { PrintStream out ->
out.print("var data = ")
tree.printJson(out, printOptions)
}
["chart-builder.js", "d3.v3.min.js", "index.html", "styles.css"].each { String resourceName ->
def resource = getClass().getResourceAsStream("/com/getkeepsafe/dexcount/" + resourceName)
def targetFile = new File(chartDir, resourceName)
targetFile.write resource.text
}
}
/**
* Logs the package tree to stdout at {@code LogLevel.DEBUG}, or at the
* default level if verbose-mode is configured.
*/
def printTaskDiagnosticData() {
// Log the entire package list/tree at LogLevel.DEBUG, unless
// verbose is enabled (in which case use the default log level).
def level = config.verbose ? LogLevel.LIFECYCLE : LogLevel.DEBUG
withStyledOutput() { out ->
def strBuilder = new StringBuilder()
print(tree, strBuilder)
out.log(level, strBuilder.toString())
out.log(level, "\n\nTask runtimes:")
out.log(level, "--------------")
out.log(level, "parsing: ${ioTime - startTime} ms")
out.log(level, "counting: ${treegenTime - ioTime} ms")
out.log(level, "printing: ${outputTime - treegenTime} ms")
out.log(level, "total: ${outputTime - startTime} ms")
out.log(level, "")
out.log(level, "Task inputs:")
out.log(level, "inputDir: $inputDirectory")
out.log(level, "inputFile: $inputFile")
}
}
def print(PackageTree tree, Appendable out) {
tree.print(out, config.format, getPrintOptions())
}
def withStyledOutput(@ClosureParams(value = SimpleType, options = ['org.gradle.api.logging.Logger']) Closure closure) {
// TODO: Actually make this stylized when we have our own solution: https://github.com/KeepSafe/dexcount-gradle-plugin/issues/124
closure(getLogger())
}
def printToFile(
File file,
@ClosureParams(value = SimpleType, options = ['java.io.PrintStream']) Closure closure) {
if (outputFile != null) {
file.parentFile.mkdirs()
file.createNewFile()
file.withOutputStream { stream ->
def out = new PrintStream(stream)
closure(out)
out.flush()
out.close()
}
}
}
/**
* Creates a new PackageTree and populates it with the method and field
* counts of the current dex/apk file.
*/
def generatePackageTree() {
def file = fileToCount()
if (file == null) {
throw new AssertionError("file is null: inputDirectory=$inputDirectory inputFile=$inputFile")
}
startTime = System.currentTimeMillis()
// Create a de-obfuscator based on the current Proguard mapping file.
// If none is given, we'll get a default mapping.
def deobs = getDeobfuscator()
def dataList = DexFile.extractDexData(file, config.dxTimeoutSec)
ioTime = System.currentTimeMillis()
try {
tree = new PackageTree(deobs)
dataList*.getMethodRefs().flatten().each {
tree.addMethodRef(it)
}
dataList*.getFieldRefs().flatten().each {
tree.addFieldRef(it)
}
} finally {
dataList*.dispose()
}
treegenTime = System.currentTimeMillis()
isInstantRun = dataList.any { it.isInstantRun }
}
def getPrintOptions() {
return new PrintOptions(
includeClassCount: config.includeClassCount,
includeMethodCount: true,
includeFieldCount: config.includeFieldCount,
includeTotalMethodCount: config.includeTotalMethodCount,
teamCityIntegration: config.teamCityIntegration,
orderByMethodCount: config.orderByMethodCount,
includeClasses: config.includeClasses,
printHeader: true,
maxTreeDepth: config.maxTreeDepth)
}
def getDeobfuscator() {
if (mappingFile != null && !mappingFile.exists()) {
withStyledOutput() {
it.debug("Mapping file specified at ${mappingFile.absolutePath} does not exist, assuming output is not obfuscated.")
}
mappingFile = null
}
return Deobfuscator.create(mappingFile)
}
def checkIfApkExists() {
def file = fileToCount()
return file != null && file.exists()
}
def fileToCount() {
if (inputDirectory != null) {
def fileList = inputDirectory.listFiles(new ApkFilenameFilter())
return fileList.length > 0 ? fileList[0] : null
} else {
return inputFile
}
}
/**
* Fails the build when a user specifies a "max method count" for their current build.
*/
def failBuildMaxMethods() {
if (config.maxMethodCount > 0 && tree.methodCount > config.maxMethodCount) {
throw new GradleException(String.format("The current APK has %d methods, the current max is: %d.", tree.methodCount, config.maxMethodCount))
}
}
// Tried to use a closure for this, but Groovy cannot decide between java.io.FilenameFilter
// and java.io.FileFilter. If we have to make it ugly, might as well make it efficient.
static class ApkFilenameFilter implements FilenameFilter {
@Override
boolean accept(File dir, String name) {
return name != null && name.endsWith(".apk")
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy