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