• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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