• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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 vogar.android;
18 
19 import com.google.common.annotations.VisibleForTesting;
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.List;
28 import java.util.jar.JarEntry;
29 import java.util.jar.JarInputStream;
30 import java.util.jar.JarOutputStream;
31 import vogar.Classpath;
32 import vogar.Dexer;
33 import vogar.HostFileCache;
34 import vogar.Language;
35 import vogar.Log;
36 import vogar.Md5Cache;
37 import vogar.ModeId;
38 import vogar.commands.Command;
39 import vogar.commands.Mkdir;
40 import vogar.util.Strings;
41 
42 
43 
44 /**
45  * Android SDK commands such as adb, aapt and dx.
46  */
47 public class AndroidSdk {
48 
49     private static final String D8_COMMAND_NAME = "d8-compat-dx";
50     private static final String DX_COMMAND_NAME = "dx";
51     private static final String ARBITRARY_BUILD_TOOL_NAME = D8_COMMAND_NAME;
52 
53     private final Log log;
54     private final Mkdir mkdir;
55     private final File[] compilationClasspath;
56     private final String androidJarPath;
57     private final String desugarJarPath;
58     private final Md5Cache dexCache;
59     private final Language language;
60 
defaultExpectations()61     public static Collection<File> defaultExpectations() {
62         return Collections.singletonList(new File("libcore/expectations/knownfailures.txt"));
63     }
64 
65     /**
66      * Create an {@link AndroidSdk}.
67      *
68      * <p>Searches the PATH used to run this and scans the file system in order to determine the
69      * compilation class path and android jar path.
70      */
createAndroidSdk( Log log, Mkdir mkdir, ModeId modeId, Language language)71     public static AndroidSdk createAndroidSdk(
72             Log log, Mkdir mkdir, ModeId modeId, Language language) {
73         List<String> path = new Command.Builder(log).args("which", ARBITRARY_BUILD_TOOL_NAME)
74                 .permitNonZeroExitStatus(true)
75                 .execute();
76         if (path.isEmpty()) {
77             throw new RuntimeException(ARBITRARY_BUILD_TOOL_NAME + " not found");
78         }
79         File buildTool = new File(path.get(0)).getAbsoluteFile();
80         String buildToolDirString = getParentFileNOrLast(buildTool, 1).getName();
81 
82         List<String> adbPath = new Command.Builder(log)
83                 .args("which", "adb")
84                 .permitNonZeroExitStatus(true)
85                 .execute();
86 
87         File adb;
88         if (!adbPath.isEmpty()) {
89             adb = new File(adbPath.get(0));
90         } else {
91             adb = null;  // Could not find adb.
92         }
93 
94         /*
95          * Determine if we are running with a provided SDK or in the AOSP source tree.
96          *
97          * Android build tree (target):
98          *  ${ANDROID_BUILD_TOP}/out/host/linux-x86/bin/aapt
99          *  ${ANDROID_BUILD_TOP}/out/host/linux-x86/bin/adb
100          *  ${ANDROID_BUILD_TOP}/out/host/linux-x86/bin/dx
101          *  ${ANDROID_BUILD_TOP}/out/host/linux-x86/bin/desugar.jar
102          *  ${ANDROID_BUILD_TOP}/out/target/common/obj/JAVA_LIBRARIES/core-libart_intermediates
103          *      /classes.jar
104          */
105 
106         File[] compilationClasspath;
107         String androidJarPath;
108         String desugarJarPath = null;
109 
110         // Accept that we are running in an SDK if the user has added the build-tools or
111         // platform-tools to their path.
112         boolean buildToolsPathValid = "build-tools".equals(getParentFileNOrLast(buildTool, 2)
113                 .getName());
114         boolean isAdbPathValid = (adb != null) &&
115                 "platform-tools".equals(getParentFileNOrLast(adb, 1).getName());
116         if (buildToolsPathValid || isAdbPathValid) {
117             File sdkRoot = buildToolsPathValid
118                     ? getParentFileNOrLast(buildTool, 3)  // if build tool path invalid then
119                     : getParentFileNOrLast(adb, 2);  // adb must be valid.
120             File newestPlatform = getNewestPlatform(sdkRoot);
121             log.verbose("Using android platform: " + newestPlatform);
122             compilationClasspath = new File[] { new File(newestPlatform, "android.jar") };
123             androidJarPath = new File(newestPlatform.getAbsolutePath(), "android.jar")
124                     .getAbsolutePath();
125             log.verbose("using android sdk: " + sdkRoot);
126 
127             // There must be a desugar.jar in the build tool directory.
128             desugarJarPath = buildToolDirString + "/desugar.jar";
129             File desugarJarFile = new File(desugarJarPath);
130             if (!desugarJarFile.exists()) {
131                 throw new RuntimeException("Could not find " + desugarJarPath);
132             }
133         } else if ("bin".equals(buildToolDirString)) {
134             log.verbose("Using android source build mode to find dependencies.");
135             String tmpJarPath = "prebuilts/sdk/current/android.jar";
136             String androidBuildTop = System.getenv("ANDROID_BUILD_TOP");
137             if (!com.google.common.base.Strings.isNullOrEmpty(androidBuildTop)) {
138                 tmpJarPath = androidBuildTop + "/prebuilts/sdk/current/android.jar";
139             } else {
140                 log.warn("Assuming current directory is android build tree root.");
141             }
142             androidJarPath = tmpJarPath;
143 
144             String outDir = System.getenv("OUT_DIR");
145             if (Strings.isNullOrEmpty(outDir)) {
146                 if (Strings.isNullOrEmpty(androidBuildTop)) {
147                     outDir = ".";
148                     log.warn("Assuming we are in android build tree root to find libraries.");
149                 } else {
150                     log.verbose("Using ANDROID_BUILD_TOP to find built libraries.");
151                     outDir = androidBuildTop;
152                 }
153                 outDir += "/out/";
154             } else {
155                 log.verbose("Using OUT_DIR environment variable for finding built libs.");
156                 outDir += "/";
157             }
158 
159             String hostOutDir = System.getenv("ANDROID_HOST_OUT");
160             if (!Strings.isNullOrEmpty(hostOutDir)) {
161                 log.verbose("Using ANDROID_HOST_OUT to find host libraries.");
162             } else {
163                 // Handle the case where lunch hasn't been run. Guess the architecture.
164                 log.warn("ANDROID_HOST_OUT not set. Assuming linux-x86");
165                 hostOutDir = outDir + "/host/linux-x86";
166             }
167 
168             String desugarPattern = hostOutDir + "/framework/desugar.jar";
169             File desugarJar = new File(desugarPattern);
170 
171             if (!desugarJar.exists()) {
172                 throw new RuntimeException("Could not find " + desugarPattern);
173             }
174 
175             desugarJarPath = desugarJar.getPath();
176 
177             String pattern = outDir + "target/common/obj/JAVA_LIBRARIES/%s_intermediates/classes";
178             if (modeId.isHost()) {
179                 pattern = outDir + "host/common/obj/JAVA_LIBRARIES/%s_intermediates/classes";
180             }
181             pattern += ".jar";
182 
183             String[] jarNames = modeId.getJarNames();
184             compilationClasspath = new File[jarNames.length];
185             for (int i = 0; i < jarNames.length; i++) {
186                 String jar = jarNames[i];
187                 compilationClasspath[i] = new File(String.format(pattern, jar));
188             }
189         } else {
190             throw new RuntimeException("Couldn't derive Android home from "
191                     + ARBITRARY_BUILD_TOOL_NAME);
192         }
193 
194         return new AndroidSdk(log, mkdir, compilationClasspath, androidJarPath, desugarJarPath,
195                 new HostFileCache(log, mkdir), language);
196     }
197 
198     @VisibleForTesting
AndroidSdk(Log log, Mkdir mkdir, File[] compilationClasspath, String androidJarPath, String desugarJarPath, HostFileCache hostFileCache, Language language)199     AndroidSdk(Log log, Mkdir mkdir, File[] compilationClasspath, String androidJarPath,
200                String desugarJarPath, HostFileCache hostFileCache, Language language) {
201         this.log = log;
202         this.mkdir = mkdir;
203         this.compilationClasspath = compilationClasspath;
204         this.androidJarPath = androidJarPath;
205         this.desugarJarPath = desugarJarPath;
206         this.dexCache = new Md5Cache(log, "dex", hostFileCache);
207         this.language = language;
208     }
209 
210     // Goes up N levels in the filesystem hierarchy. Return the last file that exists if this goes
211     // past /.
getParentFileNOrLast(File f, int n)212     private static File getParentFileNOrLast(File f, int n) {
213         File lastKnownExists = f;
214         for (int i = 0; i < n; i++) {
215             File parentFile = lastKnownExists.getParentFile();
216             if (parentFile == null) {
217                 return lastKnownExists;
218             }
219             lastKnownExists = parentFile;
220         }
221         return lastKnownExists;
222     }
223 
224     /**
225      * Returns the platform directory that has the highest API version. API
226      * platform directories are named like "android-9" or "android-11".
227      */
getNewestPlatform(File sdkRoot)228     private static File getNewestPlatform(File sdkRoot) {
229         File newestPlatform = null;
230         int newestPlatformVersion = 0;
231         File[] platforms = new File(sdkRoot, "platforms").listFiles();
232         if (platforms != null) {
233             for (File platform : platforms) {
234                 try {
235                     int version =
236                             Integer.parseInt(platform.getName().substring("android-".length()));
237                     if (version > newestPlatformVersion) {
238                         newestPlatform = platform;
239                         newestPlatformVersion = version;
240                     }
241                 } catch (NumberFormatException ignore) {
242                     // Ignore non-numeric preview versions like android-Honeycomb
243                 }
244             }
245         }
246         if (newestPlatform == null) {
247             throw new IllegalStateException("Cannot find newest platform in " + sdkRoot);
248         }
249         return newestPlatform;
250     }
251 
defaultSourcePath()252     public static Collection<File> defaultSourcePath() {
253         return filterNonExistentPathsFrom("libcore/support/src/test/java",
254                                           "external/mockwebserver/src/main/java/");
255     }
256 
filterNonExistentPathsFrom(String... paths)257     private static Collection<File> filterNonExistentPathsFrom(String... paths) {
258         ArrayList<File> result = new ArrayList<File>();
259         String buildRoot = System.getenv("ANDROID_BUILD_TOP");
260         for (String path : paths) {
261             File file = new File(buildRoot, path);
262             if (file.exists()) {
263                 result.add(file);
264             }
265         }
266         return result;
267     }
268 
getCompilationClasspath()269     public File[] getCompilationClasspath() {
270         return compilationClasspath;
271     }
272 
273     /**
274      * Converts all the .class files on 'classpath' into a dex file written to 'output'.
275      *
276      * @param multidex could the output be more than 1 dex file?
277      * @param output the File for the classes.dex that will be generated as a result of this call.
278      * @param outputTempDir a temporary directory which can store intermediate files generated.
279      * @param classpath a list of files/directories containing .class files that are
280      *                  merged together and converted into the output (dex) file.
281      * @param dependentCp classes that are referenced in classpath but are not themselves on the
282      *                    classpath must be listed in dependentCp, this is required to be able
283      *                    resolve all class dependencies. The classes in dependentCp are <i>not</i>
284      *                    included in the output dex file.
285      * @param dexer Which dex tool to use
286      */
dex(boolean multidex, File output, File outputTempDir, Classpath classpath, Classpath dependentCp, Dexer dexer)287     public void dex(boolean multidex, File output, File outputTempDir,
288             Classpath classpath, Classpath dependentCp, Dexer dexer) {
289         mkdir.mkdirs(output.getParentFile());
290 
291         String classpathSubKey = dexCache.makeKey(classpath);
292         String cacheKey = null;
293         if (classpathSubKey != null) {
294             String multidexSubKey = "mdex=" + multidex;
295             cacheKey = dexCache.makeKey(classpathSubKey, multidexSubKey);
296             boolean cacheHit = dexCache.getFromCache(output, cacheKey);
297             if (cacheHit) {
298                 log.verbose("dex cache hit for " + classpath);
299                 return;
300             }
301         }
302 
303         // Call desugar first to remove invoke-dynamic LambdaMetaFactory usage,
304         // which ART doesn't support.
305         List<String> desugarOutputFilePaths = desugar(outputTempDir, classpath, dependentCp);
306 
307         /*
308          * We pass --core-library so that we can write tests in the
309          * same package they're testing, even when that's a core
310          * library package. If you're actually just using this tool to
311          * execute arbitrary code, this has the unfortunate
312          * side-effect of preventing "dx" from protecting you from
313          * yourself.
314          *
315          * Memory options pulled from build/core/definitions.mk to
316          * handle large dx input when building dex for APK.
317          */
318 
319         Command.Builder builder = new Command.Builder(log);
320         switch (dexer) {
321             case DX:
322                 builder.args(DX_COMMAND_NAME);
323                 break;
324             case D8:
325                 builder.args(D8_COMMAND_NAME);
326                 break;
327         }
328         builder.args("-JXms16M")
329                 .args("-JXmx1536M")
330                 .args("--min-sdk-version=" + language.getMinApiLevel());
331         if (multidex) {
332             builder.args("--multi-dex");
333         }
334         builder.args("--dex")
335                 .args("--output=" + output)
336                 .args("--core-library")
337                 .args(desugarOutputFilePaths);
338         builder.execute();
339 
340         if (dexer == Dexer.D8 && output.toString().endsWith(".jar")) {
341             try {
342                 fixD8JarOutput(output, desugarOutputFilePaths);
343             } catch (IOException e) {
344                 throw new RuntimeException("Error while fixing d8 output", e);
345             }
346         }
347         dexCache.insert(cacheKey, output);
348     }
349 
350     /**
351      * Produces an output file like dx does. dx generates jar files containing all resources present
352      * in the input files.
353      * d8-compat-dx only produces a jar file containing dex and none of the input resources, and
354      * will produce no file at all if there are no .class files to process.
355      */
fixD8JarOutput(File output, List<String> inputs)356     private static void fixD8JarOutput(File output, List<String> inputs) throws IOException {
357         List<String> filesToMerge = new ArrayList<>(inputs);
358 
359         // JarOutputStream is not keen on appending entries to existing file so we move the output
360         // files if it already exists.
361         File outputCopy = null;
362         if (output.exists()) {
363             outputCopy = new File(output.toString() + ".copy");
364             output.renameTo(outputCopy);
365             filesToMerge.add(outputCopy.toString());
366         }
367 
368         byte[] buffer = new byte[4096];
369         try (JarOutputStream outputJar = new JarOutputStream(new FileOutputStream(output))) {
370             for (String fileToMerge : filesToMerge) {
371                 copyJarContentExcludingClassFiles(buffer, fileToMerge, outputJar);
372             }
373         } finally {
374             if (outputCopy != null) {
375                 outputCopy.delete();
376             }
377         }
378     }
379 
copyJarContentExcludingClassFiles(byte[] buffer, String inputJarName, JarOutputStream outputJar)380     private static void copyJarContentExcludingClassFiles(byte[] buffer, String inputJarName,
381             JarOutputStream outputJar) throws IOException {
382 
383         try (JarInputStream inputJar = new JarInputStream(new FileInputStream(inputJarName))) {
384             for (JarEntry entry = inputJar.getNextJarEntry();
385                     entry != null;
386                     entry = inputJar.getNextJarEntry()) {
387                 if (entry.getName().endsWith(".class")) {
388                     continue;
389                 }
390                 outputJar.putNextEntry(entry);
391                 int length;
392                 while ((length = inputJar.read(buffer)) >= 0) {
393                     if (length > 0) {
394                         outputJar.write(buffer, 0, length);
395                     }
396                 }
397                 outputJar.closeEntry();
398             }
399         }
400     }
401 
402     // Runs desugar on classpath as the input with dependentCp as the classpath_entry.
403     // Returns the generated output list of files.
desugar(File outputTempDir, Classpath classpath, Classpath dependentCp)404     private List<String> desugar(File outputTempDir, Classpath classpath, Classpath dependentCp) {
405         Command.Builder builder = new Command.Builder(log)
406                 .args("java", "-jar", desugarJarPath);
407 
408         // Ensure that libcore is on the bootclasspath for desugar,
409         // otherwise it tries to use the java command's bootclasspath.
410         for (File f : compilationClasspath) {
411             builder.args("--bootclasspath_entry", f.getPath());
412         }
413 
414         // Desugar needs to actively resolve classes that the original inputs
415         // were compiled against. Dx does not; so it doesn't use dependentCp.
416         for (File f : dependentCp.getElements()) {
417             builder.args("--classpath_entry", f.getPath());
418         }
419 
420         builder.args("--core_library")
421                 .args("--min_sdk_version", language.getMinApiLevel());
422 
423         // Build the -i (input) and -o (output) arguments.
424         // Every input from classpath corresponds to a new output temp file into
425         // desugarTempDir.
426         File desugarTempDir;
427         {
428             // Generate a temporary list of files that correspond to the 'classpath';
429             // desugar will then convert the files in 'classpath' into 'desugarClasspath'.
430             if (!outputTempDir.isDirectory()) {
431                 throw new AssertionError(
432                         "outputTempDir must be a directory: " + outputTempDir.getPath());
433             }
434 
435             String desugarTempDirPath = outputTempDir.getPath() + "/desugar";
436             desugarTempDir = new File(desugarTempDirPath);
437             desugarTempDir.mkdirs();
438             if (!desugarTempDir.exists()) {
439                 throw new AssertionError(
440                         "desugarTempDir; failed to create " + desugarTempDirPath);
441             }
442         }
443 
444         // Create unique file names to support non-unique classpath base names.
445         //
446         // For example:
447         //
448         // Classpath("/x/y.jar:/z/y.jar:/a/b.jar") ->
449         // Output Files("${tmp}/0y.jar:${tmp}/1y.jar:${tmp}/2b.jar")
450         int uniqueCounter = 0;
451         List<String> desugarOutputFilePaths = new ArrayList<String>();
452 
453         for (File desugarInput : classpath.getElements()) {
454             String tmpName = uniqueCounter + desugarInput.getName();
455             ++uniqueCounter;
456 
457             String desugarOutputPath = desugarTempDir.getPath() + "/" + tmpName;
458             desugarOutputFilePaths.add(desugarOutputPath);
459 
460             builder.args("-i", desugarInput.getPath())
461                     .args("-o", desugarOutputPath);
462         }
463 
464         builder.execute();
465 
466         return desugarOutputFilePaths;
467     }
468 
packageApk(File apk, File manifest)469     public void packageApk(File apk, File manifest) {
470         new Command(log, "aapt",
471                 "package",
472                 "-F", apk.getPath(),
473                 "-M", manifest.getPath(),
474                 "-I", androidJarPath,
475                 "--version-name", "1.0",
476                 "--version-code", "1").execute();
477     }
478 
addToApk(File apk, File dex)479     public void addToApk(File apk, File dex) {
480         new Command(log, "aapt", "add", "-k", apk.getPath(), dex.getPath()).execute();
481     }
482 
install(File apk)483     public void install(File apk) {
484         new Command(log, "adb", "install", "-r", apk.getPath()).execute();
485     }
486 
uninstall(String packageName)487     public void uninstall(String packageName) {
488         new Command.Builder(log)
489                 .args("adb", "uninstall", packageName)
490                 .permitNonZeroExitStatus(true)
491                 .execute();
492     }
493 }
494