1 package org.robolectric; 2 3 import java.io.BufferedOutputStream; 4 import java.io.File; 5 import java.io.FileOutputStream; 6 import java.io.IOException; 7 import java.io.InputStream; 8 import java.util.Enumeration; 9 import java.util.Locale; 10 import java.util.Set; 11 import java.util.TreeSet; 12 import java.util.jar.JarEntry; 13 import java.util.jar.JarFile; 14 import java.util.jar.JarOutputStream; 15 import java.util.zip.ZipEntry; 16 import org.robolectric.internal.bytecode.ClassNodeProvider; 17 import org.robolectric.internal.bytecode.InstrumentationConfiguration; 18 import org.robolectric.internal.bytecode.InstrumentationConfiguration.Builder; 19 import org.robolectric.internal.bytecode.OldClassInstrumentor; 20 import org.robolectric.internal.bytecode.ShadowDecorator; 21 import org.robolectric.util.Util; 22 23 /** 24 * Instruments an entire jar. 25 */ 26 public class JarInstrumentor { 27 28 private final InstrumentationConfiguration instrumentationConfiguration; 29 private final ShadowDecorator shadowDecorator; 30 private final OldClassInstrumentor classInstrumentor; 31 JarInstrumentor()32 public JarInstrumentor() { 33 instrumentationConfiguration = createInstrumentationConfiguration(); 34 shadowDecorator = new ShadowDecorator(); 35 classInstrumentor = new OldClassInstrumentor(shadowDecorator); 36 } 37 main(String[] args)38 public static void main(String[] args) throws Exception { 39 new JarInstrumentor().run(args); 40 } 41 run(String[] args)42 private void run(String[] args) throws IOException { 43 if (args.length != 2) { 44 System.err.println("Usage: JarInstrumentor <source jar> <dest jar>"); 45 System.exit(1); 46 } 47 48 instrumentJar(new File(args[0]), new File(args[1])); 49 } 50 instrumentJar(File sourceFile, File destFile)51 private void instrumentJar(File sourceFile, File destFile) throws IOException { 52 long startNs = System.nanoTime(); 53 JarFile jarFile = new JarFile(sourceFile); 54 ClassNodeProvider classNodeProvider = 55 new ClassNodeProvider() { 56 @Override 57 protected byte[] getClassBytes(String className) throws ClassNotFoundException { 58 return JarInstrumentor.getClassBytes(className, jarFile); 59 } 60 }; 61 62 int nonClassCount = 0; 63 int classCount = 0; 64 Set<String> failedClasses = new TreeSet<>(); 65 try (JarOutputStream jarOut = 66 new JarOutputStream( 67 new BufferedOutputStream(new FileOutputStream(destFile), 32 * 1024))) { 68 System.out.println("Instrumenting from " + sourceFile + " to " + destFile); 69 Enumeration<JarEntry> entries = jarFile.entries(); 70 while (entries.hasMoreElements()) { 71 JarEntry jarEntry = entries.nextElement(); 72 73 String name = jarEntry.getName(); 74 if (name.endsWith("/")) { 75 jarOut.putNextEntry(new JarEntry(name)); 76 } else if (name.endsWith(".class")) { 77 String className = name.substring(0, name.length() - ".class".length()).replace('/', '.'); 78 79 boolean classIsRenamed = isClassRenamed(className); 80 if (classIsRenamed) { 81 System.out.println("className = " + className); 82 continue; 83 } 84 85 try { 86 byte[] classBytes = getClassBytes(className, jarFile); 87 byte[] outBytes = 88 classInstrumentor.instrument( 89 classBytes, instrumentationConfiguration, classNodeProvider); 90 jarOut.putNextEntry(new JarEntry(name)); 91 jarOut.write(outBytes); 92 classCount++; 93 } catch (Exception e) { 94 failedClasses.add(className); 95 System.err.print("Failed to instrument " + className + ": "); 96 e.printStackTrace(); 97 } 98 } else { 99 // resources & stuff 100 jarOut.putNextEntry(new JarEntry(name)); 101 Util.copy(jarFile.getInputStream(jarEntry), jarOut); 102 nonClassCount++; 103 } 104 } 105 } 106 long elapsedNs = System.nanoTime() - startNs; 107 System.out.println( 108 String.format( 109 Locale.getDefault(), 110 "Wrote %d classes and %d resources in %1.2f seconds", 111 classCount, 112 nonClassCount, 113 elapsedNs / 1000000000.0)); 114 if (!failedClasses.isEmpty()) { 115 System.out.println("Failed to instrument:"); 116 } 117 for (String failedClass : failedClasses) { 118 System.out.println("- " + failedClass); 119 } 120 } 121 isClassRenamed(String className)122 private boolean isClassRenamed(String className) { 123 String internalName = className.replace('.', '/'); 124 String remappedName = instrumentationConfiguration.mappedTypeName(internalName); 125 return !remappedName.equals(internalName); 126 } 127 getClassBytes(String className, JarFile jarFile)128 private static byte[] getClassBytes(String className, JarFile jarFile) 129 throws ClassNotFoundException { 130 String classFilename = className.replace('.', '/') + ".class"; 131 ZipEntry entry = jarFile.getEntry(classFilename); 132 try { 133 InputStream inputStream; 134 if (entry == null) { 135 inputStream = JarInstrumentor.class.getClassLoader().getResourceAsStream(classFilename); 136 } else { 137 inputStream = jarFile.getInputStream(entry); 138 } 139 if (inputStream == null) { 140 throw new ClassNotFoundException("Couldn't find " + className.replace('/', '.')); 141 } 142 return Util.readBytes(inputStream); 143 } catch (IOException e) { 144 throw new ClassNotFoundException("Couldn't load " + className.replace('/', '.'), e); 145 } 146 } 147 createInstrumentationConfiguration()148 private static InstrumentationConfiguration createInstrumentationConfiguration() { 149 Builder builder = 150 InstrumentationConfiguration.newBuilder() 151 .doNotAcquirePackage("java.") 152 .doNotAcquirePackage("sun.") 153 .doNotAcquirePackage("org.robolectric.annotation.") 154 .doNotAcquirePackage("org.robolectric.internal.") 155 .doNotAcquirePackage("org.robolectric.util.") 156 .doNotAcquirePackage("org.junit."); 157 158 builder 159 .doNotAcquireClass("org.robolectric.TestLifecycle") 160 .doNotAcquireClass("org.robolectric.AndroidManifest") 161 .doNotAcquireClass("org.robolectric.RobolectricTestRunner") 162 .doNotAcquireClass("org.robolectric.RobolectricTestRunner%HelperTestRunner") 163 .doNotAcquireClass("org.robolectric.res.ResourcePath") 164 .doNotAcquireClass("org.robolectric.res.ResourceTable") 165 .doNotAcquireClass("org.robolectric.res.builder.XmlBlock"); 166 167 builder 168 .doNotAcquirePackage("javax.") 169 .doNotAcquirePackage("org.junit") 170 .doNotAcquirePackage("org.hamcrest") 171 .doNotAcquirePackage("org.robolectric.annotation.") 172 .doNotAcquirePackage("org.robolectric.internal.") 173 .doNotAcquirePackage("org.robolectric.manifest.") 174 .doNotAcquirePackage("org.robolectric.res.") 175 .doNotAcquirePackage("org.robolectric.util.") 176 .doNotAcquirePackage("org.robolectric.RobolectricTestRunner$") 177 .doNotAcquirePackage("sun.") 178 .doNotAcquirePackage("com.sun.") 179 .doNotAcquirePackage("org.w3c.") 180 .doNotAcquirePackage("org.xml.") 181 .doNotAcquirePackage("org.specs2") // allows for android projects with mixed scala\java tests to be 182 .doNotAcquirePackage("scala.") // run with Maven Surefire (see the RoboSpecs project on github) 183 .doNotAcquirePackage("kotlin.") 184 // Fix #958: SQLite native library must be loaded once. 185 .doNotAcquirePackage("com.almworks.sqlite4java") 186 .doNotAcquirePackage("org.jacoco."); 187 188 // Instrumenting these classes causes a weird failure. 189 builder.doNotInstrumentClass("android.R") 190 .doNotInstrumentClass("android.R$styleable"); 191 192 builder.addInstrumentedPackage("dalvik.") 193 .addInstrumentedPackage("libcore.") 194 .addInstrumentedPackage("android.") 195 .addInstrumentedPackage("com.android.internal.") 196 .addInstrumentedPackage("org.apache.http.") 197 .addInstrumentedPackage("org.ccil.cowan.tagsoup") 198 .addInstrumentedPackage("org.kxml2."); 199 200 builder.doNotInstrumentPackage("androidx.test"); 201 return builder.build(); 202 } 203 } 204