001/* 002 * Copyright (C) 2015-2019 KeepSafe Software 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.getkeepsafe.dexcount; 017 018import org.gradle.api.GradleException; 019import org.slf4j.Logger; 020 021import java.io.IOException; 022 023/** 024 * An object that can produce formatted output from a {@link PackageTree} instance. 025 */ 026public class CountReporter { 027 /** 028 * The maximum number of method refs and field refs allowed in a single Dex 029 * file. 030 */ 031 private static final int MAX_DEX_REFS = 0xFFFF; // 65535 032 033 private final PackageTree packageTree; 034 private final String variantName; 035 private final Logger logger; 036 private final PrintOptions options; 037 private final String inputRepresentation; 038 private final boolean isInstantRun; 039 040 public CountReporter( 041 PackageTree packageTree, 042 String variantName, 043 Logger logger, 044 PrintOptions options, 045 String inputRepresentation, 046 boolean isInstantRun) { 047 this.packageTree = packageTree; 048 this.variantName = variantName; 049 this.logger = logger; 050 this.options = options; 051 this.inputRepresentation = inputRepresentation; 052 this.isInstantRun = isInstantRun; 053 } 054 055 public void report() throws IOException { 056 try { 057 printPreamble(); 058 printSummary(); 059 printTaskDiagnosticData(); 060 failBuildMaxMethods(); 061 } catch (DexCountException e) { 062 logger.error("Error counting dex methods. Please contact the developer at https://github.com/KeepSafe/dexcount-gradle-plugin/issues", e); 063 } 064 } 065 066 private void printPreamble() { 067 if (options.getPrintHeader()) { 068 String projectName = getClass().getPackage().getImplementationTitle(); 069 String projectVersion = getClass().getPackage().getImplementationVersion(); 070 071 logger.warn("Dexcount name: {}", projectName); 072 logger.warn("Dexcount version: {}", projectVersion); 073 logger.warn("Dexcount input: {}", inputRepresentation); 074 } 075 } 076 077 private String percentUsed(int count) { 078 double used = ((double) count / MAX_DEX_REFS) * 100.0; 079 return String.format("%.2f", used); 080 } 081 082 private void printSummary() { 083 if (isInstantRun) { 084 logger.warn("Warning: Instant Run build detected! Instant Run does not run Proguard; method counts may be inaccurate."); 085 } 086 087 String percentMethodsUsed = percentUsed(packageTree.getMethodCount()); 088 String percentFieldsUsed = percentUsed(packageTree.getFieldCount()); 089 String percentClassesUsed = percentUsed(packageTree.getClassCount()); 090 091 int methodsRemaining = Math.max(MAX_DEX_REFS - packageTree.getMethodCount(), 0); 092 int fieldsRemaining = Math.max(MAX_DEX_REFS - packageTree.getFieldCount(), 0); 093 int classesRemaining = Math.max(MAX_DEX_REFS - packageTree.getClassCount(), 0); 094 095 int methodCount, fieldCount, classCount; 096 if (options.isAndroidProject()) { 097 methodCount = packageTree.getMethodCount(); 098 fieldCount = packageTree.getFieldCount(); 099 classCount = packageTree.getClassCount(); 100 } else { 101 methodCount = packageTree.getMethodCountDeclared(); 102 fieldCount = packageTree.getFieldCountDeclared(); 103 classCount = packageTree.getClassCountDeclared(); 104 } 105 106 logger.warn("Total methods in " + inputRepresentation + ": " + methodCount + " (" + percentMethodsUsed + "% used)"); 107 logger.warn("Total fields in " + inputRepresentation + ": " + fieldCount + " (" + percentFieldsUsed + "% used)"); 108 logger.warn("Total classes in " + inputRepresentation + ": " + classCount + " (" + percentClassesUsed + "% used)"); 109 110 if (options.isAndroidProject()) { 111 logger.warn("Methods remaining in " + inputRepresentation + ": " + methodsRemaining); 112 logger.warn("Fields remaining in " + inputRepresentation + ": " + fieldsRemaining); 113 logger.warn("Classes remaining in " + inputRepresentation + ": " + classesRemaining); 114 } 115 116 if (options.getTeamCityIntegration() || (options.getTeamCitySlug() != null && options.getTeamCitySlug().length() > 0)) { 117 String slug = "Dexcount"; 118 if (options.getTeamCitySlug() != null) { 119 slug += "_" + options.getTeamCitySlug().replace(' ', '_'); 120 } 121 String prefix = slug + "_" + variantName; 122 123 /* 124 * Reports to Team City statistic value 125 * Doc: https://confluence.jetbrains.com/display/TCD9/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-ReportingBuildStatistics 126 */ 127 logger.warn(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "ClassCount", packageTree.getClassCount())); 128 logger.warn(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "MethodCount", packageTree.getMethodCount())); 129 logger.warn(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "FieldCount", packageTree.getFieldCount())); 130 } 131 } 132 133 private void printTaskDiagnosticData() throws IOException { 134 StringBuilder strBuilder = new StringBuilder(); 135 packageTree.print(strBuilder, options.getOutputFormat(), options); 136 137 if (options.isVerbose()) { 138 logger.warn(strBuilder.toString()); 139 } else { 140 logger.info(strBuilder.toString()); 141 } 142 } 143 144 private void failBuildMaxMethods() { 145 if (options.getMaxMethodCount() > 0 && packageTree.getMethodCount() > options.getMaxMethodCount()) { 146 String message = String.format("The current APK has %d methods, the current max is: %d.", packageTree.getMethodCount(), options.getMaxMethodCount()); 147 throw new GradleException(message); 148 } 149 } 150}