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