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