• 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 
32 import vogar.Classpath;
33 import vogar.Dexer;
34 import vogar.HostFileCache;
35 import vogar.Language;
36 import vogar.Log;
37 import vogar.Md5Cache;
38 import vogar.ModeId;
39 import vogar.commands.Command;
40 import vogar.commands.Mkdir;
41 import vogar.util.Strings;
42 
43 
44 
45 /**
46  * Android SDK commands such as adb, aapt and d8.
47  */
48 public class AndroidSdk {
49 
50     private static final String D8_COMMAND_NAME = "d8";
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 Md5Cache dexCache;
58     private final Language language;
59     private final boolean serialDexing;
60     private final boolean verboseDexStats;
61 
defaultExpectations()62     public static Collection<File> defaultExpectations() {
63         return Collections.singletonList(new File("libcore/expectations/knownfailures.txt"));
64     }
65 
66     /**
67      * Create an {@link AndroidSdk}.
68      *
69      * <p>Searches the PATH used to run this and scans the file system in order to determine the
70      * compilation class path and android jar path.
71      */
createAndroidSdk( Log log, Mkdir mkdir, ModeId modeId, Language language, boolean supportBuildFromSource, boolean serialDexing, boolean verboseDexStats)72     public static AndroidSdk createAndroidSdk(
73             Log log, Mkdir mkdir, ModeId modeId, Language language,
74             boolean supportBuildFromSource, boolean serialDexing, boolean verboseDexStats) {
75         List<String> path = new Command.Builder(log).args("which", ARBITRARY_BUILD_TOOL_NAME)
76                 .permitNonZeroExitStatus(true)
77                 .execute();
78         if (path.isEmpty()) {
79             throw new RuntimeException(ARBITRARY_BUILD_TOOL_NAME + " not found");
80         }
81         File buildTool = new File(path.get(0)).getAbsoluteFile();
82         String buildToolDirString = getParentFileNOrLast(buildTool, 1).getName();
83 
84         List<String> adbPath = new Command.Builder(log)
85                 .args("which", "adb")
86                 .permitNonZeroExitStatus(true)
87                 .execute();
88 
89         File adb;
90         if (!adbPath.isEmpty()) {
91             adb = new File(adbPath.get(0));
92         } else {
93             adb = null;  // Could not find adb.
94         }
95 
96         /*
97          * Determine if we are running with a provided SDK or in the AOSP source tree.
98          *
99          * Android build tree (target):
100          *  ${ANDROID_BUILD_TOP}/out/host/linux-x86/bin/aapt
101          *  ${ANDROID_BUILD_TOP}/out/host/linux-x86/bin/adb
102          *  ${ANDROID_BUILD_TOP}/out/host/linux-x86/bin/d8
103          *  ${ANDROID_BUILD_TOP}/out/target/common/obj/JAVA_LIBRARIES/core-libart_intermediates
104          *      /classes.jar
105          */
106 
107         File[] compilationClasspath;
108         String androidJarPath;
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         } else if ("bin".equals(buildToolDirString)) {
127             log.verbose("Using android source build mode to find dependencies.");
128             String tmpJarPath = "prebuilts/sdk/current/public/android.jar";
129             String androidBuildTop = System.getenv("ANDROID_BUILD_TOP");
130             if (!com.google.common.base.Strings.isNullOrEmpty(androidBuildTop)) {
131                 tmpJarPath = androidBuildTop + "/prebuilts/sdk/current/public/android.jar";
132             } else {
133                 log.warn("Assuming current directory is android build tree root.");
134             }
135             androidJarPath = tmpJarPath;
136 
137             String outDir = System.getenv("OUT_DIR");
138             if (Strings.isNullOrEmpty(outDir)) {
139                 if (Strings.isNullOrEmpty(androidBuildTop)) {
140                     outDir = ".";
141                     log.warn("Assuming we are in android build tree root to find libraries.");
142                 } else {
143                     log.verbose("Using ANDROID_BUILD_TOP to find built libraries.");
144                     outDir = androidBuildTop;
145                 }
146                 outDir += "/out/";
147             } else {
148                 log.verbose("Using OUT_DIR environment variable for finding built libs.");
149                 outDir += "/";
150             }
151 
152             String hostOutDir = System.getenv("ANDROID_HOST_OUT");
153             if (!Strings.isNullOrEmpty(hostOutDir)) {
154                 log.verbose("Using ANDROID_HOST_OUT to find host libraries.");
155             } else {
156                 // Handle the case where lunch hasn't been run. Guess the architecture.
157                 log.warn("ANDROID_HOST_OUT not set. Assuming linux-x86");
158                 hostOutDir = outDir + "/host/linux-x86";
159             }
160 
161             if (!supportBuildFromSource) {
162                 compilationClasspath = new File[]{};
163             } else {
164                 String pattern = outDir +
165                         "target/common/obj/JAVA_LIBRARIES/%s_intermediates/classes";
166                 if (modeId.isHost()) {
167                     pattern = outDir + "host/common/obj/JAVA_LIBRARIES/%s_intermediates/classes";
168                 }
169                 pattern += ".jar";
170 
171                 String[] jarNames = modeId.getJarNames();
172                 compilationClasspath = new File[jarNames.length];
173                 List<String> missingJars = new ArrayList<>();
174                 for (int i = 0; i < jarNames.length; i++) {
175                     String jar = jarNames[i];
176                     File file;
177                     if (modeId.isHost()) {
178                         if  ("conscrypt-hostdex".equals(jar)) {
179                             jar = "conscrypt-host-hostdex";
180                         } else if ("core-icu4j-hostdex".equals(jar)) {
181                             jar = "core-icu4j-host-hostdex";
182                         }
183                         file = new File(String.format(pattern, jar));
184                     } else {
185                         file = findApexJar(jar, pattern);
186                         if (file.exists()) {
187                             log.verbose("Using jar " + jar + " from " + file);
188                         } else {
189                             missingJars.add(jar);
190                         }
191                     }
192                     compilationClasspath[i] = file;
193                 }
194                 if (!missingJars.isEmpty()) {
195                     logMissingJars(log, missingJars);
196                     throw new RuntimeException("Unable to locate all jars needed for compilation");
197                 }
198             }
199         } else {
200             throw new RuntimeException("Couldn't derive Android home from "
201                     + ARBITRARY_BUILD_TOOL_NAME);
202         }
203 
204         return new AndroidSdk(log, mkdir, compilationClasspath, androidJarPath,
205                 new HostFileCache(log, mkdir), language, serialDexing, verboseDexStats);
206     }
207 
208     /** Logs jars that couldn't be found ands suggests a command for building them */
logMissingJars(Log log, List<String> missingJars)209     private static void logMissingJars(Log log, List<String> missingJars) {
210         StringBuilder makeCommand = new StringBuilder().append("m ");
211         for (String jarName : missingJars) {
212             String apex = apexForJar(jarName);
213             log.warn("Missing compilation jar " + jarName +
214                     (apex != null ? " from APEX " + apex : ""));
215             makeCommand.append(jarName).append(" ");
216         }
217         log.info("Suggested make command: " + makeCommand);
218     }
219 
220     /** Returns the name of the APEX a particular jar might be located in */
apexForJar(String jar)221     private static String apexForJar(String jar) {
222         if (jar.endsWith(".api.stubs")) {
223             return null;  // API stubs aren't in any APEX.
224         }
225         return "com.android.art.testing";
226     }
227 
228     /**
229      * Depending on the build setup, jars might be located in the intermediates directory
230      * for their APEX or not, so look in both places. Returns the last path searched, so
231      * always non-null but possibly non-existent and so the caller should check.
232      */
findApexJar(String jar, String filePattern)233     private static File findApexJar(String jar, String filePattern) {
234         String apex = apexForJar(jar);
235         if (apex != null) {
236             File file = new File(String.format(filePattern, jar + "." + apex));
237             if (file.exists()) {
238                 return file;
239             }
240         }
241         return new File(String.format(filePattern, jar));
242     }
243 
244     @VisibleForTesting
AndroidSdk(Log log, Mkdir mkdir, File[] compilationClasspath, String androidJarPath, HostFileCache hostFileCache, Language language, boolean serialDexing, boolean verboseDexStats)245     AndroidSdk(Log log, Mkdir mkdir, File[] compilationClasspath, String androidJarPath,
246                HostFileCache hostFileCache, Language language,
247                boolean serialDexing, boolean verboseDexStats) {
248         this.log = log;
249         this.mkdir = mkdir;
250         this.compilationClasspath = compilationClasspath;
251         this.androidJarPath = androidJarPath;
252         this.dexCache = new Md5Cache(log, "dex", hostFileCache);
253         this.language = language;
254         this.serialDexing = serialDexing;
255         this.verboseDexStats = verboseDexStats;
256     }
257 
258     // Goes up N levels in the filesystem hierarchy. Return the last file that exists if this goes
259     // past /.
getParentFileNOrLast(File f, int n)260     private static File getParentFileNOrLast(File f, int n) {
261         File lastKnownExists = f;
262         for (int i = 0; i < n; i++) {
263             File parentFile = lastKnownExists.getParentFile();
264             if (parentFile == null) {
265                 return lastKnownExists;
266             }
267             lastKnownExists = parentFile;
268         }
269         return lastKnownExists;
270     }
271 
272     /**
273      * Returns the platform directory that has the highest API version. API
274      * platform directories are named like "android-9" or "android-11".
275      */
getNewestPlatform(File sdkRoot)276     private static File getNewestPlatform(File sdkRoot) {
277         File newestPlatform = null;
278         int newestPlatformVersion = 0;
279         File[] platforms = new File(sdkRoot, "platforms").listFiles();
280         if (platforms != null) {
281             for (File platform : platforms) {
282                 try {
283                     int version =
284                             Integer.parseInt(platform.getName().substring("android-".length()));
285                     if (version > newestPlatformVersion) {
286                         newestPlatform = platform;
287                         newestPlatformVersion = version;
288                     }
289                 } catch (NumberFormatException ignore) {
290                     // Ignore non-numeric preview versions like android-Honeycomb
291                 }
292             }
293         }
294         if (newestPlatform == null) {
295             throw new IllegalStateException("Cannot find newest platform in " + sdkRoot);
296         }
297         return newestPlatform;
298     }
299 
defaultSourcePath()300     public static Collection<File> defaultSourcePath() {
301         return filterNonExistentPathsFrom("libcore/support/src/test/java",
302                                           "external/mockwebserver/src/main/java/");
303     }
304 
filterNonExistentPathsFrom(String... paths)305     private static Collection<File> filterNonExistentPathsFrom(String... paths) {
306         ArrayList<File> result = new ArrayList<File>();
307         String buildRoot = System.getenv("ANDROID_BUILD_TOP");
308         for (String path : paths) {
309             File file = new File(buildRoot, path);
310             if (file.exists()) {
311                 result.add(file);
312             }
313         }
314         return result;
315     }
316 
getCompilationClasspath()317     public File[] getCompilationClasspath() {
318         return compilationClasspath;
319     }
320 
321     /**
322      * Converts all the .class files on 'classpath' into a dex file written to 'output'.
323      *
324      * @param multidex could the output be more than 1 dex file?
325      * @param output the File for the classes.dex that will be generated as a result of this call.
326      * @param outputTempDir a temporary directory which can store intermediate files generated.
327      * @param classpath a list of files/directories containing .class files that are
328      *                  merged together and converted into the output (dex) file.
329      * @param dependentCp classes that are referenced in classpath but are not themselves on the
330      *                    classpath must be listed in dependentCp, this is required to be able
331      *                    resolve all class dependencies. The classes in dependentCp are <i>not</i>
332      *                    included in the output dex file.
333      * @param dexer Which dex tool to use
334      */
dex(boolean multidex, File output, File outputTempDir, Classpath classpath, Classpath dependentCp, Dexer dexer)335     public void dex(boolean multidex, File output, File outputTempDir,
336             Classpath classpath, Classpath dependentCp, Dexer dexer) {
337         mkdir.mkdirs(output.getParentFile());
338 
339         String classpathSubKey = dexCache.makeKey(classpath);
340         String cacheKey = null;
341         if (classpathSubKey != null) {
342             String multidexSubKey = "mdex=" + multidex;
343             cacheKey = dexCache.makeKey(classpathSubKey, multidexSubKey);
344             boolean cacheHit = dexCache.getFromCache(output, cacheKey);
345             if (cacheHit) {
346                 log.verbose("dex cache hit for " + classpath);
347                 return;
348             }
349         }
350 
351         List<String> filePaths = new ArrayList<String>();
352         for (File file : classpath.getElements()) {
353           filePaths.add(file.getPath());
354         }
355 
356         /*
357          * We pass --core-library so that we can write tests in the
358          * same package they're testing, even when that's a core
359          * library package. If you're actually just using this tool to
360          * execute arbitrary code, this has the unfortunate
361          * side-effect of preventing "d8" from protecting you from
362          * yourself.
363          */
364 
365         Command.Builder builder = new Command.Builder(log);
366         if (verboseDexStats) {
367             builder.args("/usr/bin/time").args("-v");
368         }
369         switch (dexer) {
370             case D8:
371                 List<String> sanitizedOutputFilePaths;
372                 try {
373                     sanitizedOutputFilePaths = removeDexFilesForD8(filePaths, outputTempDir);
374                 } catch (IOException e) {
375                     throw new RuntimeException("Error while removing dex files from archive", e);
376                 }
377                 builder.args(D8_COMMAND_NAME);
378                 builder.args("-JXms16M").args("-JXmx1536M");
379                 builder.args("-JXX:+TieredCompilation").args("-JXX:TieredStopAtLevel=1");
380                 builder.args("-JDcom.android.tools.r8.emitRecordAnnotationsInDex");
381                 builder.args("-JDcom.android.tools.r8.emitPermittedSubclassesAnnotationsInDex");
382                 builder.args("--thread-count").args("1");
383 
384                 // d8 will not allow compiling with a single dex file as the target, but if given
385                 // a directory name will start its output in classes.dex but may overflow into
386                 // multiple dex files. See b/189327238
387                 String outputPath = output.toString();
388                 String dexOverflowPath = null;
389                 if (outputPath.endsWith("/classes.dex")) {
390                     dexOverflowPath = outputPath.replace("classes.dex", "classes2.dex");
391                     outputPath = output.getParentFile().toString();
392                 }
393                 builder
394                     .args("--min-api").args(language.getMinApiLevel())
395                     .args("--output").args(outputPath)
396                     .args(sanitizedOutputFilePaths);
397                 builder.execute();
398                 if (dexOverflowPath != null && new File(dexOverflowPath).exists()) {
399                     // If we were expecting a single dex file and d8 overflows into two
400                     // or more files than fail.
401                     throw new RuntimeException("Dex file overflow " + dexOverflowPath
402                         + ", try --multidex");
403                 }
404                 if (output.toString().endsWith(".jar")) {
405                     try {
406                         fixD8JarOutput(output, filePaths);
407                     } catch (IOException e) {
408                         throw new RuntimeException("Error while fixing d8 output", e);
409                     }
410                 }
411                 break;
412             default:
413                 throw new RuntimeException("Unsupported dexer: " + dexer);
414 
415         }
416 
417         dexCache.insert(cacheKey, output);
418     }
419 
420     /**
421      * Produces an output file like dx does. dx generates jar files containing all resources present
422      * in the input files.
423      * d8 only produces a jar file containing dex and none of the input resources, and
424      * will produce no file at all if there are no .class files to process.
425      */
fixD8JarOutput(File output, List<String> inputs)426     private static void fixD8JarOutput(File output, List<String> inputs) throws IOException {
427         List<String> filesToMerge = new ArrayList<>(inputs);
428 
429         // JarOutputStream is not keen on appending entries to existing file so we move the output
430         // files if it already exists.
431         File outputCopy = null;
432         if (output.exists()) {
433             outputCopy = new File(output.toString() + ".copy");
434             output.renameTo(outputCopy);
435             filesToMerge.add(outputCopy.toString());
436         }
437 
438         byte[] buffer = new byte[4096];
439         try (JarOutputStream outputJar = new JarOutputStream(new FileOutputStream(output))) {
440             for (String fileToMerge : filesToMerge) {
441                 copyJarContentExcludingClassFiles(buffer, fileToMerge, outputJar);
442             }
443         } finally {
444             if (outputCopy != null) {
445                 outputCopy.delete();
446             }
447         }
448     }
449 
450     /**
451      * Generates a file path for a modified d8 input file.
452      * @param inputFile the d8 input file.
453      * @param outputDirectory the directory where the modified file should be written.
454      * @return the destination for the modified d8 input file.
455      */
getModifiedD8Destination(File inputFile, File outputDirectory)456     private static File getModifiedD8Destination(File inputFile, File outputDirectory) {
457         String name = inputFile.getName();
458         int suffixStart = name.lastIndexOf('.');
459         if (suffixStart != -1) {
460             name = name.substring(0, suffixStart);
461         }
462         return new File(outputDirectory, name + "-d8.jar");
463     }
464 
465     /**
466       * Removes DEX files from an archive and preserves the rest.
467       */
removeDexFilesForD8(List<String> fileNames, File tempDir)468     private List<String> removeDexFilesForD8(List<String> fileNames, File tempDir)
469             throws IOException {
470         byte[] buffer = new byte[4096];
471         List<String> processedFiles = new ArrayList<>(fileNames.size());
472         for (String inputFileName : fileNames) {
473             File inputFile = new File(inputFileName);
474             File outputFile = getModifiedD8Destination(inputFile, tempDir);
475             try (JarOutputStream outputJar =
476                     new JarOutputStream(new FileOutputStream(outputFile))) {
477                 copyJarContentExcludingFiles(buffer, inputFileName, outputJar, ".dex");
478             }
479             processedFiles.add(outputFile.toString());
480         }
481         return processedFiles;
482     }
483 
copyJarContentExcludingClassFiles(byte[] buffer, String inputJarName, JarOutputStream outputJar)484     private static void copyJarContentExcludingClassFiles(byte[] buffer, String inputJarName,
485             JarOutputStream outputJar) throws IOException {
486         copyJarContentExcludingFiles(buffer, inputJarName, outputJar, ".class");
487     }
488 
copyJarContentExcludingFiles(byte[] buffer, String inputJarName, JarOutputStream outputJar, String extensionToExclude)489     private static void copyJarContentExcludingFiles(byte[] buffer, String inputJarName,
490             JarOutputStream outputJar, String extensionToExclude) throws IOException {
491 
492         try (JarInputStream inputJar = new JarInputStream(new FileInputStream(inputJarName))) {
493             for (JarEntry entry = inputJar.getNextJarEntry();
494                     entry != null;
495                     entry = inputJar.getNextJarEntry()) {
496                 if (entry.getName().endsWith(extensionToExclude)) {
497                     continue;
498                 }
499 
500                 // Skip directories as they can cause duplicates.
501                 if (entry.isDirectory()) {
502                     continue;
503                 }
504 
505                 outputJar.putNextEntry(entry);
506 
507                 int length;
508                 while ((length = inputJar.read(buffer)) >= 0) {
509                     if (length > 0) {
510                         outputJar.write(buffer, 0, length);
511                     }
512                 }
513                 outputJar.closeEntry();
514             }
515         }
516     }
517 
packageApk(File apk, File manifest)518     public void packageApk(File apk, File manifest) {
519         new Command(log, "aapt",
520                 "package",
521                 "-F", apk.getPath(),
522                 "-M", manifest.getPath(),
523                 "-I", androidJarPath,
524                 "--version-name", "1.0",
525                 "--version-code", "1").execute();
526     }
527 
addToApk(File apk, File dex)528     public void addToApk(File apk, File dex) {
529         new Command(log, "aapt", "add", "-k", apk.getPath(), dex.getPath()).execute();
530     }
531 
install(File apk)532     public void install(File apk) {
533         new Command(log, "adb", "install", "-r", apk.getPath()).execute();
534     }
535 
uninstall(String packageName)536     public void uninstall(String packageName) {
537         new Command.Builder(log)
538                 .args("adb", "uninstall", packageName)
539                 .permitNonZeroExitStatus(true)
540                 .execute();
541     }
542 }
543