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