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 com.getkeepsafe.dexcount.colors.Color; 019import com.getkeepsafe.dexcount.colors.Styleable; 020import org.gradle.api.GradleException; 021import org.gradle.api.logging.LogLevel; 022 023import java.io.IOException; 024 025/** 026 * An object that can produce formatted output from a {@link PackageTree} instance. 027 */ 028public class CountReporter { 029 /** 030 * The maximum number of method refs and field refs allowed in a single Dex 031 * file. 032 */ 033 private static final int MAX_DEX_REFS = 0xFFFF; // 65535 034 035 private final PackageTree packageTree; 036 private final String variantName; 037 private final Styleable styleable; 038 private final PrintOptions options; 039 private final String inputRepresentation; 040 private final boolean isInstantRun; 041 042 public CountReporter( 043 PackageTree packageTree, 044 String variantName, 045 Styleable styleable, 046 PrintOptions options, 047 String inputRepresentation, 048 boolean isInstantRun) { 049 this.packageTree = packageTree; 050 this.variantName = variantName; 051 this.styleable = styleable; 052 this.options = options; 053 this.inputRepresentation = inputRepresentation; 054 this.isInstantRun = isInstantRun; 055 } 056 057 public void report() throws IOException { 058 try { 059 printPreamble(); 060 printSummary(); 061 printTaskDiagnosticData(); 062 failBuildMaxMethods(); 063 } catch (DexCountException e) { 064 styleable.withStyledOutput(Color.RED, LogLevel.ERROR, out -> { 065 out.println("Error counting dex methods. Please contact the developer at https://github.com/KeepSafe/dexcount-gradle-plugin/issues"); 066 e.printStackTrace(out); 067 }); 068 } 069 } 070 071 private void printPreamble() throws IOException { 072 if (options.getPrintHeader()) { 073 String projectName = getClass().getPackage().getImplementationTitle(); 074 String projectVersion = getClass().getPackage().getImplementationVersion(); 075 076 styleable.withStyledOutput(Color.DEFAULT, out -> { 077 out.println("Dexcount name: " + projectName); 078 out.println("Dexcount version: " + projectVersion); 079 out.println("Dexcount input: " + inputRepresentation); 080 }); 081 } 082 } 083 084 private String percentUsed(int count) { 085 double used = ((double) count / MAX_DEX_REFS) * 100.0; 086 return String.format("%.2f", used); 087 } 088 089 private void printSummary() throws IOException { 090 if (isInstantRun) { 091 styleable.withStyledOutput(Color.RED, out -> { 092 out.println("Warning: Instant Run build detected! Instant Run does not run Proguard; method counts may be inaccurate."); 093 }); 094 } 095 096 Color color = packageTree.getMethodCount() < 50000 ? Color.GREEN : Color.YELLOW; 097 098 styleable.withStyledOutput(color, out -> { 099 String percentMethodsUsed = percentUsed(packageTree.getMethodCount()); 100 String percentFieldsUsed = percentUsed(packageTree.getFieldCount()); 101 String percentClassesUsed = percentUsed(packageTree.getClassCount()); 102 103 int methodsRemaining = Math.max(MAX_DEX_REFS - packageTree.getMethodCount(), 0); 104 int fieldsRemaining = Math.max(MAX_DEX_REFS - packageTree.getFieldCount(), 0); 105 int classesRemaining = Math.max(MAX_DEX_REFS - packageTree.getClassCount(), 0); 106 107 int methodCount, fieldCount, classCount; 108 if (options.isAndroidProject()) { 109 methodCount = packageTree.getMethodCount(); 110 fieldCount = packageTree.getFieldCount(); 111 classCount = packageTree.getClassCount(); 112 } else { 113 methodCount = packageTree.getMethodCountDeclared(); 114 fieldCount = packageTree.getFieldCountDeclared(); 115 classCount = packageTree.getClassCountDeclared(); 116 } 117 118 out.println("Total methods in " + inputRepresentation + ": " + methodCount + " (" + percentMethodsUsed + "% used)"); 119 out.println("Total fields in " + inputRepresentation + ": " + fieldCount + " (" + percentFieldsUsed + "% used)"); 120 out.println("Total classes in " + inputRepresentation + ": " + classCount + " (" + percentClassesUsed + "% used)"); 121 122 if (options.isAndroidProject()) { 123 out.println("Methods remaining in " + inputRepresentation + ": " + methodsRemaining); 124 out.println("Fields remaining in " + inputRepresentation + ": " + fieldsRemaining); 125 out.println("Classes remaining in " + inputRepresentation + ": " + classesRemaining); 126 } 127 }); 128 129 if (options.getTeamCityIntegration() || (options.getTeamCitySlug() != null && options.getTeamCitySlug().length() > 0)) { 130 styleable.withStyledOutput(Color.DEFAULT, out -> { 131 String slug = "Dexcount"; 132 if (options.getTeamCitySlug() != null) { 133 slug += "_" + options.getTeamCitySlug().replace(' ', '_'); 134 } 135 String prefix = slug + "_" + variantName; 136 137 /* 138 * Reports to Team City statistic value 139 * Doc: https://confluence.jetbrains.com/display/TCD9/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-ReportingBuildStatistics 140 */ 141 out.println(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "ClassCount", packageTree.getClassCount())); 142 out.println(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "MethodCount", packageTree.getMethodCount())); 143 out.println(String.format("##teamcity[buildStatisticValue key='%s_%s' value='%d']", prefix, "FieldCount", packageTree.getFieldCount())); 144 }); 145 } 146 } 147 148 private void printTaskDiagnosticData() throws IOException { 149 // Log the entire package list/tree at LogLevel.DEBUG, unless 150 // verbose is enabled (in which case use the default log level). 151 LogLevel level = options.isVerbose() ? null : LogLevel.DEBUG; 152 153 styleable.withStyledOutput(Color.YELLOW, level, out -> { 154 StringBuilder strBuilder = new StringBuilder(); 155 packageTree.print(strBuilder, options.getOutputFormat(), options); 156 157 out.format(strBuilder.toString()); 158 }); 159 } 160 161 private void failBuildMaxMethods() { 162 if (options.getMaxMethodCount() > 0 && packageTree.getMethodCount() > options.getMaxMethodCount()) { 163 String message = String.format("The current APK has %d methods, the current max is: %d.", packageTree.getMethodCount(), options.getMaxMethodCount()); 164 throw new GradleException(message); 165 } 166 } 167}