1 /* 2 * Copyright (C) 2013 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 17 package com.android.tools.rmtypedefs; 18 19 import com.google.common.collect.Lists; 20 import com.google.common.collect.Sets; 21 import com.google.common.io.Files; 22 23 import org.objectweb.asm.AnnotationVisitor; 24 import org.objectweb.asm.ClassReader; 25 import org.objectweb.asm.ClassVisitor; 26 import org.objectweb.asm.ClassWriter; 27 28 import java.io.File; 29 import java.io.IOException; 30 import java.io.PrintStream; 31 import java.util.ArrayList; 32 import java.util.List; 33 import java.util.Set; 34 35 import static org.objectweb.asm.Opcodes.ASM5; 36 37 /** 38 * Finds and deletes typedef annotation classes (and also warns if their 39 * retention was wrong, such that uses embeds 40 */ 41 public class RmTypeDefs { 42 43 private static final String ANNOTATION = "java/lang/annotation/Annotation"; 44 private static final String STRING_DEF = "android/annotation/StringDef"; 45 private static final String INT_DEF = "android/annotation/IntDef"; 46 private static final String INT_DEF_DESC = "L" + INT_DEF + ";"; 47 private static final String STRING_DEF_DESC = "L" + STRING_DEF + ";"; 48 private static final String RETENTION_DESC = "Ljava/lang/annotation/Retention;"; 49 private static final String RETENTION_POLICY_DESC = "Ljava/lang/annotation/RetentionPolicy;"; 50 private static final String SOURCE_RETENTION_VALUE = "SOURCE"; 51 52 private boolean mQuiet; 53 private boolean mVerbose; 54 private boolean mHaveError; 55 private boolean mDryRun; 56 57 private Set<String> mAnnotationNames = Sets.newHashSet(); 58 private List<File> mAnnotationClassFiles = Lists.newArrayList(); 59 private Set<File> mAnnotationOuterClassFiles = Sets.newHashSet(); 60 main(String[] args)61 public static void main(String[] args) { 62 new RmTypeDefs().run(args); 63 } 64 run(String[] args)65 private void run(String[] args) { 66 if (args.length == 0) { 67 usage(System.err); 68 System.exit(1); 69 } 70 71 List<File> dirs = new ArrayList<File>(); 72 for (String arg : args) { 73 if (arg.equals("--help") || arg.equals("-h")) { 74 usage(System.out); 75 return; 76 } else if (arg.equals("-q") || arg.equals("--quiet") || arg.equals("--silent")) { 77 mQuiet = true; 78 } else if (arg.equals("-v") || arg.equals("--verbose")) { 79 mVerbose = true; 80 } else if (arg.equals("-n") || arg.equals("--dry-run")) { 81 mDryRun = true; 82 } else if (arg.startsWith("-")) { 83 System.err.println("Unknown argument " + arg); 84 usage(System.err); 85 System.exit(1); 86 87 } else { 88 // Other arguments should be file names 89 File file = new File(arg); 90 if (file.exists()) { 91 dirs.add(file); 92 } else { 93 System.err.println(file + " does not exist"); 94 usage(System.err); 95 System.exit(1); 96 } 97 } 98 } 99 100 if (!mQuiet) { 101 System.out.println("Deleting @IntDef and @StringDef annotation class files"); 102 } 103 104 // Record typedef annotation names and files 105 for (File dir : dirs) { 106 checkFile(dir); 107 } 108 109 // Rewrite the .class files for any classes that *contain* typedefs as innerclasses 110 rewriteOuterClasses(); 111 112 // Removes the actual .class files for the typedef annotations 113 deleteAnnotationClasses(); 114 115 System.exit(mHaveError ? -1 : 0); 116 } 117 118 /** 119 * Visits the given directory tree recursively and calls {@link #checkClass(java.io.File)} 120 * for any .class files encountered 121 */ checkFile(File file)122 private void checkFile(File file) { 123 if (file.isDirectory()) { 124 File[] files = file.listFiles(); 125 if (files != null) { 126 for (File f : files) { 127 checkFile(f); 128 } 129 } 130 } else if (file.isFile()) { 131 String path = file.getPath(); 132 if (path.endsWith(".class")) { 133 checkClass(file); 134 } else if (path.endsWith(".jar")) { 135 System.err.println(path + ": Warning: Encountered .jar file; .class files " 136 + "are not scanned and removed inside .jar files"); 137 } 138 } 139 } 140 141 /** 142 * Checks the given .class file to see if it's a typedef annotation, and if so 143 * records that fact by calling {@link #addTypeDef(String, java.io.File)} 144 */ checkClass(File file)145 private void checkClass(File file) { 146 try { 147 byte[] bytes = Files.toByteArray(file); 148 ClassReader classReader = new ClassReader(bytes); 149 classReader.accept(new TypeDefVisitor(file), 0); 150 } catch (IOException e) { 151 System.err.println("Could not read " + file + ": " + e.getLocalizedMessage()); 152 System.exit(1); 153 } 154 } 155 156 /** 157 * Prints usage statement. 158 */ usage(PrintStream out)159 static void usage(PrintStream out) { 160 out.println("Android TypeDef Remover 1.0"); 161 out.println("Copyright (C) 2013 The Android Open Source Project\n"); 162 out.println("Usage: rmtypedefs folder1 [folder2 [folder3...]]\n"); 163 out.println("Options:"); 164 out.println(" -h,--help show this message"); 165 out.println(" -q,--quiet quiet"); 166 out.println(" -v,--verbose verbose"); 167 out.println(" -n,--dry-run dry-run only, leaves files alone"); 168 out.println(" --verify run extra diagnostics to verify file integrity"); 169 } 170 171 /** 172 * Records the given class name (internal name) and class file path as corresponding to a 173 * typedef annotation 174 * */ addTypeDef(String name, File file)175 private void addTypeDef(String name, File file) { 176 mAnnotationClassFiles.add(file); 177 mAnnotationNames.add(name); 178 179 String fileName = file.getName(); 180 int index = fileName.lastIndexOf('$'); 181 if (index != -1) { 182 File parentFile = file.getParentFile(); 183 assert parentFile != null : file; 184 File container = new File(parentFile, fileName.substring(0, index) + ".class"); 185 if (container.exists()) { 186 mAnnotationOuterClassFiles.add(container); 187 } else { 188 System.err.println("Warning: Could not find outer class " + container 189 + " for typedef " + file); 190 mHaveError = true; 191 } 192 } 193 } 194 195 /** 196 * Rewrites the outer classes containing the typedefs such that they no longer refer to 197 * the (now removed) typedef annotation inner classes 198 */ rewriteOuterClasses()199 private void rewriteOuterClasses() { 200 for (File file : mAnnotationOuterClassFiles) { 201 byte[] bytes; 202 try { 203 bytes = Files.toByteArray(file); 204 } catch (IOException e) { 205 System.err.println("Could not read " + file + ": " + e.getLocalizedMessage()); 206 mHaveError = true; 207 continue; 208 } 209 210 ClassWriter classWriter = new ClassWriter(ASM5); 211 ClassVisitor classVisitor = new ClassVisitor(ASM5, classWriter) { 212 @Override 213 public void visitInnerClass(String name, String outerName, String innerName, 214 int access) { 215 if (!mAnnotationNames.contains(name)) { 216 super.visitInnerClass(name, outerName, innerName, access); 217 } 218 } 219 }; 220 ClassReader reader = new ClassReader(bytes); 221 reader.accept(classVisitor, 0); 222 byte[] rewritten = classWriter.toByteArray(); 223 try { 224 Files.write(rewritten, file); 225 } catch (IOException e) { 226 System.err.println("Could not write " + file + ": " + e.getLocalizedMessage()); 227 mHaveError = true; 228 //noinspection UnnecessaryContinue 229 continue; 230 } 231 } 232 } 233 234 /** 235 * Performs the actual deletion (or display, if in dry-run mode) of the typedef annotation 236 * files 237 */ deleteAnnotationClasses()238 private void deleteAnnotationClasses() { 239 for (File mFile : mAnnotationClassFiles) { 240 if (mVerbose) { 241 if (mDryRun) { 242 System.out.println("Would delete " + mFile); 243 } else { 244 System.out.println("Deleting " + mFile); 245 } 246 } 247 if (!mDryRun) { 248 boolean deleted = mFile.delete(); 249 if (!deleted) { 250 System.err.println("Could not delete " + mFile); 251 mHaveError = true; 252 } 253 } 254 } 255 } 256 257 /** 258 * Visitor which visits .class files and checks whether each class is a typedef annotation 259 * (and if so, calls {@link #addTypeDef(String, java.io.File)} 260 */ 261 private class TypeDefVisitor extends ClassVisitor { 262 263 /** Class file name */ 264 private File mFile; 265 266 /** Class name */ 267 private String mName; 268 269 /** Is this class an annotation? */ 270 private boolean mAnnotation; 271 272 /** Is this annotation a typedef? Only applies if {@link #mAnnotation} */ 273 private boolean mTypedef; 274 275 /** Does the annotation have source retention? Only applies if {@link #mAnnotation} */ 276 private boolean mSourceRetention; 277 TypeDefVisitor(File file)278 public TypeDefVisitor(File file) { 279 super(ASM5); 280 mFile = file; 281 } 282 visit( int version, int access, String name, String signature, String superName, String[] interfaces)283 public void visit( 284 int version, 285 int access, 286 String name, 287 String signature, 288 String superName, 289 String[] interfaces) { 290 mName = name; 291 mAnnotation = interfaces != null && interfaces.length >= 1 292 && ANNOTATION.equals(interfaces[0]); 293 294 // Special case: Also delete the actual @IntDef and @StringDef .class files. 295 // These have class file retention 296 mTypedef = name.equals(INT_DEF) || name.equals(STRING_DEF); 297 } 298 visitAnnotation(String desc, boolean visible)299 public AnnotationVisitor visitAnnotation(String desc, boolean visible) { 300 mTypedef = desc.equals(INT_DEF_DESC) || desc.equals(STRING_DEF_DESC); 301 if (desc.equals(RETENTION_DESC)) { 302 return new AnnotationVisitor(ASM5) { 303 public void visitEnum(String name, String desc, String value) { 304 if (desc.equals(RETENTION_POLICY_DESC)) { 305 mSourceRetention = SOURCE_RETENTION_VALUE.equals(value); 306 } 307 } 308 }; 309 } 310 return null; 311 } 312 313 public void visitEnd() { 314 if (mAnnotation && mTypedef) { 315 if (!mSourceRetention && !mName.equals(STRING_DEF) && !mName.equals(INT_DEF)) { 316 System.err.println(mFile + ": Warning: Annotation should be annotated " 317 + "with @Retention(RetentionPolicy.SOURCE)"); 318 mHaveError = true; 319 } 320 321 addTypeDef(mName, mFile); 322 } 323 } 324 } 325 } 326 327