1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php 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 com.android.ide.eclipse.adt.internal.build; 18 19 import com.android.SdkConstants; 20 import com.android.annotations.NonNull; 21 import com.android.annotations.Nullable; 22 import com.android.ide.eclipse.adt.AdtConstants; 23 import com.android.ide.eclipse.adt.AdtPlugin; 24 import com.android.ide.eclipse.adt.AndroidPrintStream; 25 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 26 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; 27 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 28 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 29 import com.android.prefs.AndroidLocation.AndroidLocationException; 30 import com.android.sdklib.BuildToolInfo; 31 import com.android.sdklib.IAndroidTarget; 32 import com.android.sdklib.IAndroidTarget.IOptionalLibrary; 33 import com.android.sdklib.build.ApkBuilder; 34 import com.android.sdklib.build.ApkBuilder.JarStatus; 35 import com.android.sdklib.build.ApkBuilder.SigningInfo; 36 import com.android.sdklib.build.ApkCreationException; 37 import com.android.sdklib.build.DuplicateFileException; 38 import com.android.sdklib.build.SealedApkException; 39 import com.android.sdklib.internal.build.DebugKeyProvider; 40 import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; 41 import com.android.sdklib.util.GrabProcessOutput; 42 import com.android.sdklib.util.GrabProcessOutput.IProcessOutput; 43 import com.android.sdklib.util.GrabProcessOutput.Wait; 44 import com.google.common.hash.HashCode; 45 import com.google.common.hash.HashFunction; 46 import com.google.common.hash.Hashing; 47 48 import org.eclipse.core.resources.IFile; 49 import org.eclipse.core.resources.IFolder; 50 import org.eclipse.core.resources.IProject; 51 import org.eclipse.core.resources.IResource; 52 import org.eclipse.core.resources.IWorkspaceRoot; 53 import org.eclipse.core.resources.ResourcesPlugin; 54 import org.eclipse.core.runtime.CoreException; 55 import org.eclipse.core.runtime.IPath; 56 import org.eclipse.core.runtime.IStatus; 57 import org.eclipse.core.runtime.Status; 58 import org.eclipse.jdt.core.IClasspathContainer; 59 import org.eclipse.jdt.core.IClasspathEntry; 60 import org.eclipse.jdt.core.IJavaProject; 61 import org.eclipse.jdt.core.JavaCore; 62 import org.eclipse.jdt.core.JavaModelException; 63 import org.eclipse.jface.preference.IPreferenceStore; 64 65 import java.io.File; 66 import java.io.FileWriter; 67 import java.io.IOException; 68 import java.io.PrintStream; 69 import java.security.PrivateKey; 70 import java.security.cert.X509Certificate; 71 import java.util.ArrayList; 72 import java.util.Collection; 73 import java.util.Collections; 74 import java.util.HashSet; 75 import java.util.List; 76 import java.util.Map; 77 import java.util.Set; 78 import java.util.TreeMap; 79 80 /** 81 * Helper with methods for the last 3 steps of the generation of an APK. 82 * 83 * {@link #packageResources(IFile, IProject[], String, int, String, String)} packages the 84 * application resources using aapt into a zip file that is ready to be integrated into the apk. 85 * 86 * {@link #executeDx(IJavaProject, String, String, IJavaProject[])} will convert the Java byte 87 * code into the Dalvik bytecode. 88 * 89 * {@link #finalPackage(String, String, String, boolean, IJavaProject, IProject[], IJavaProject[], String, boolean)} 90 * will make the apk from all the previous components. 91 * 92 * This class only executes the 3 above actions. It does not handle the errors, and simply sends 93 * them back as custom exceptions. 94 * 95 * Warnings are handled by the {@link ResourceMarker} interface. 96 * 97 * Console output (verbose and non verbose) is handled through the {@link AndroidPrintStream} passed 98 * to the constructor. 99 * 100 */ 101 public class BuildHelper { 102 103 private static final String CONSOLE_PREFIX_DX = "Dx"; //$NON-NLS-1$ 104 private final static String TEMP_PREFIX = "android_"; //$NON-NLS-1$ 105 106 private static final String COMMAND_CRUNCH = "crunch"; //$NON-NLS-1$ 107 private static final String COMMAND_PACKAGE = "package"; //$NON-NLS-1$ 108 109 @NonNull 110 private final IProject mProject; 111 @NonNull 112 private final BuildToolInfo mBuildToolInfo; 113 @NonNull 114 private final AndroidPrintStream mOutStream; 115 @NonNull 116 private final AndroidPrintStream mErrStream; 117 private final boolean mForceJumbo; 118 private final boolean mDisableDexMerger; 119 private final boolean mVerbose; 120 private final boolean mDebugMode; 121 122 private final Set<String> mCompiledCodePaths = new HashSet<String>(); 123 124 public static final boolean BENCHMARK_FLAG = false; 125 public static long sStartOverallTime = 0; 126 public static long sStartJavaCTime = 0; 127 128 private final static int MILLION = 1000000; 129 private String mProguardFile; 130 131 /** 132 * An object able to put a marker on a resource. 133 */ 134 public interface ResourceMarker { 135 void setWarning(IResource resource, String message); 136 } 137 138 /** 139 * Creates a new post-compiler helper 140 * @param project 141 * @param outStream 142 * @param errStream 143 * @param debugMode whether this is a debug build 144 * @param verbose 145 * @throws CoreException 146 */ 147 public BuildHelper(@NonNull IProject project, 148 @NonNull BuildToolInfo buildToolInfo, 149 @NonNull AndroidPrintStream outStream, 150 @NonNull AndroidPrintStream errStream, 151 boolean forceJumbo, boolean disableDexMerger, boolean debugMode, 152 boolean verbose, ResourceMarker resMarker) throws CoreException { 153 mProject = project; 154 mBuildToolInfo = buildToolInfo; 155 mOutStream = outStream; 156 mErrStream = errStream; 157 mDebugMode = debugMode; 158 mVerbose = verbose; 159 mForceJumbo = forceJumbo; 160 mDisableDexMerger = disableDexMerger; 161 162 gatherPaths(resMarker); 163 } 164 165 public void updateCrunchCache() throws AaptExecException, AaptResultException { 166 // Benchmarking start 167 long startCrunchTime = 0; 168 if (BENCHMARK_FLAG) { 169 String msg = "BENCHMARK ADT: Starting Initial Packaging (.ap_)"; //$NON-NLS-1$ 170 AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); 171 startCrunchTime = System.nanoTime(); 172 } 173 174 // Get the resources folder to crunch from 175 IFolder resFolder = mProject.getFolder(AdtConstants.WS_RESOURCES); 176 List<String> resPaths = new ArrayList<String>(); 177 resPaths.add(resFolder.getLocation().toOSString()); 178 179 // Get the output folder where the cache is stored. 180 IFolder cacheFolder = mProject.getFolder(AdtConstants.WS_CRUNCHCACHE); 181 String cachePath = cacheFolder.getLocation().toOSString(); 182 183 /* For crunching, we don't need the osManifestPath, osAssetsPath, or the configFilter 184 * parameters for executeAapt 185 */ 186 executeAapt(COMMAND_CRUNCH, "", resPaths, "", cachePath, "", 0); 187 188 // Benchmarking end 189 if (BENCHMARK_FLAG) { 190 String msg = "BENCHMARK ADT: Ending Initial Package (.ap_). \nTime Elapsed: " //$NON-NLS-1$ 191 + ((System.nanoTime() - startCrunchTime)/MILLION) + "ms"; //$NON-NLS-1$ 192 AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); 193 } 194 } 195 196 /** 197 * Packages the resources of the projet into a .ap_ file. 198 * @param manifestFile the manifest of the project. 199 * @param libProjects the list of library projects that this project depends on. 200 * @param resFilter an optional resource filter to be used with the -c option of aapt. If null 201 * no filters are used. 202 * @param versionCode an optional versionCode to be inserted in the manifest during packaging. 203 * If the value is <=0, no values are inserted. 204 * @param outputFolder where to write the resource ap_ file. 205 * @param outputFilename the name of the resource ap_ file. 206 * @throws AaptExecException 207 * @throws AaptResultException 208 */ 209 public void packageResources(IFile manifestFile, List<IProject> libProjects, String resFilter, 210 int versionCode, String outputFolder, String outputFilename) 211 throws AaptExecException, AaptResultException { 212 213 // Benchmarking start 214 long startPackageTime = 0; 215 if (BENCHMARK_FLAG) { 216 String msg = "BENCHMARK ADT: Starting Initial Packaging (.ap_)"; //$NON-NLS-1$ 217 AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); 218 startPackageTime = System.nanoTime(); 219 } 220 221 // need to figure out some path before we can execute aapt; 222 223 // get the cache folder 224 IFolder cacheFolder = mProject.getFolder(AdtConstants.WS_CRUNCHCACHE); 225 226 // get the resource folder 227 IFolder resFolder = mProject.getFolder(AdtConstants.WS_RESOURCES); 228 229 // and the assets folder 230 IFolder assetsFolder = mProject.getFolder(AdtConstants.WS_ASSETS); 231 232 // we need to make sure this one exists. 233 if (assetsFolder.exists() == false) { 234 assetsFolder = null; 235 } 236 237 // list of res folder (main project + maybe libraries) 238 ArrayList<String> osResPaths = new ArrayList<String>(); 239 240 IPath resLocation = resFolder.getLocation(); 241 IPath manifestLocation = manifestFile.getLocation(); 242 243 if (resLocation != null && manifestLocation != null) { 244 245 // png cache folder first. 246 addFolderToList(osResPaths, cacheFolder); 247 248 // regular res folder next. 249 osResPaths.add(resLocation.toOSString()); 250 251 // then libraries 252 if (libProjects != null) { 253 for (IProject lib : libProjects) { 254 // png cache folder first 255 IFolder libCacheFolder = lib.getFolder(AdtConstants.WS_CRUNCHCACHE); 256 addFolderToList(osResPaths, libCacheFolder); 257 258 // regular res folder next. 259 IFolder libResFolder = lib.getFolder(AdtConstants.WS_RESOURCES); 260 addFolderToList(osResPaths, libResFolder); 261 } 262 } 263 264 String osManifestPath = manifestLocation.toOSString(); 265 266 String osAssetsPath = null; 267 if (assetsFolder != null) { 268 osAssetsPath = assetsFolder.getLocation().toOSString(); 269 } 270 271 // build the default resource package 272 executeAapt(COMMAND_PACKAGE, osManifestPath, osResPaths, osAssetsPath, 273 outputFolder + File.separator + outputFilename, resFilter, 274 versionCode); 275 } 276 277 // Benchmarking end 278 if (BENCHMARK_FLAG) { 279 String msg = "BENCHMARK ADT: Ending Initial Package (.ap_). \nTime Elapsed: " //$NON-NLS-1$ 280 + ((System.nanoTime() - startPackageTime)/MILLION) + "ms"; //$NON-NLS-1$ 281 AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); 282 } 283 } 284 285 /** 286 * Adds os path of a folder to a list only if the folder actually exists. 287 * @param pathList 288 * @param folder 289 */ 290 private void addFolderToList(List<String> pathList, IFolder folder) { 291 // use a File instead of the IFolder API to ignore workspace refresh issue. 292 File testFile = new File(folder.getLocation().toOSString()); 293 if (testFile.isDirectory()) { 294 pathList.add(testFile.getAbsolutePath()); 295 } 296 } 297 298 /** 299 * Makes a final package signed with the debug key. 300 * 301 * Packages the dex files, the temporary resource file into the final package file. 302 * 303 * Whether the package is a debug package is controlled with the <var>debugMode</var> parameter 304 * in {@link #PostCompilerHelper(IProject, PrintStream, PrintStream, boolean, boolean)} 305 * 306 * @param intermediateApk The path to the temporary resource file. 307 * @param dex The path to the dex file. 308 * @param output The path to the final package file to create. 309 * @param libProjects an optional list of library projects (can be null) 310 * @return true if success, false otherwise. 311 * @throws ApkCreationException 312 * @throws AndroidLocationException 313 * @throws KeytoolException 314 * @throws NativeLibInJarException 315 * @throws CoreException 316 * @throws DuplicateFileException 317 */ 318 public void finalDebugPackage(String intermediateApk, String dex, String output, 319 List<IProject> libProjects, ResourceMarker resMarker) 320 throws ApkCreationException, KeytoolException, AndroidLocationException, 321 NativeLibInJarException, DuplicateFileException, CoreException { 322 323 AdtPlugin adt = AdtPlugin.getDefault(); 324 if (adt == null) { 325 return; 326 } 327 328 // get the debug keystore to use. 329 IPreferenceStore store = adt.getPreferenceStore(); 330 String keystoreOsPath = store.getString(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE); 331 if (keystoreOsPath == null || new File(keystoreOsPath).isFile() == false) { 332 keystoreOsPath = DebugKeyProvider.getDefaultKeyStoreOsPath(); 333 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, 334 Messages.ApkBuilder_Using_Default_Key); 335 } else { 336 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, 337 String.format(Messages.ApkBuilder_Using_s_To_Sign, keystoreOsPath)); 338 } 339 340 // from the keystore, get the signing info 341 SigningInfo info = ApkBuilder.getDebugKey(keystoreOsPath, mVerbose ? mOutStream : null); 342 343 finalPackage(intermediateApk, dex, output, libProjects, 344 info != null ? info.key : null, info != null ? info.certificate : null, resMarker); 345 } 346 347 /** 348 * Makes the final package. 349 * 350 * Packages the dex files, the temporary resource file into the final package file. 351 * 352 * Whether the package is a debug package is controlled with the <var>debugMode</var> parameter 353 * in {@link #PostCompilerHelper(IProject, PrintStream, PrintStream, boolean, boolean)} 354 * 355 * @param intermediateApk The path to the temporary resource file. 356 * @param dex The path to the dex file. 357 * @param output The path to the final package file to create. 358 * @param debugSign whether the apk must be signed with the debug key. 359 * @param libProjects an optional list of library projects (can be null) 360 * @param abiFilter an optional filter. If not null, then only the matching ABI is included in 361 * the final archive 362 * @return true if success, false otherwise. 363 * @throws NativeLibInJarException 364 * @throws ApkCreationException 365 * @throws CoreException 366 * @throws DuplicateFileException 367 */ 368 public void finalPackage(String intermediateApk, String dex, String output, 369 List<IProject> libProjects, 370 PrivateKey key, X509Certificate certificate, ResourceMarker resMarker) 371 throws NativeLibInJarException, ApkCreationException, DuplicateFileException, 372 CoreException { 373 374 try { 375 ApkBuilder apkBuilder = new ApkBuilder(output, intermediateApk, dex, 376 key, certificate, 377 mVerbose ? mOutStream: null); 378 apkBuilder.setDebugMode(mDebugMode); 379 380 // either use the full compiled code paths or just the proguard file 381 // if present 382 Collection<String> pathsCollection = mCompiledCodePaths; 383 if (mProguardFile != null) { 384 pathsCollection = Collections.singletonList(mProguardFile); 385 mProguardFile = null; 386 } 387 388 // Now we write the standard resources from all the output paths. 389 for (String path : pathsCollection) { 390 File file = new File(path); 391 if (file.isFile()) { 392 JarStatus jarStatus = apkBuilder.addResourcesFromJar(file); 393 394 // check if we found native libraries in the external library. This 395 // constitutes an error or warning depending on if they are in lib/ 396 if (jarStatus.getNativeLibs().size() > 0) { 397 String libName = file.getName(); 398 399 String msg = String.format( 400 "Native libraries detected in '%1$s'. See console for more information.", 401 libName); 402 403 ArrayList<String> consoleMsgs = new ArrayList<String>(); 404 405 consoleMsgs.add(String.format( 406 "The library '%1$s' contains native libraries that will not run on the device.", 407 libName)); 408 409 if (jarStatus.hasNativeLibsConflicts()) { 410 consoleMsgs.add("Additionally some of those libraries will interfer with the installation of the application because of their location in lib/"); 411 consoleMsgs.add("lib/ is reserved for NDK libraries."); 412 } 413 414 consoleMsgs.add("The following libraries were found:"); 415 416 for (String lib : jarStatus.getNativeLibs()) { 417 consoleMsgs.add(" - " + lib); 418 } 419 420 String[] consoleStrings = consoleMsgs.toArray(new String[consoleMsgs.size()]); 421 422 // if there's a conflict or if the prefs force error on any native code in jar 423 // files, throw an exception 424 if (jarStatus.hasNativeLibsConflicts() || 425 AdtPrefs.getPrefs().getBuildForceErrorOnNativeLibInJar()) { 426 throw new NativeLibInJarException(jarStatus, msg, libName, consoleStrings); 427 } else { 428 // otherwise, put a warning, and output to the console also. 429 if (resMarker != null) { 430 resMarker.setWarning(mProject, msg); 431 } 432 433 for (String string : consoleStrings) { 434 mOutStream.println(string); 435 } 436 } 437 } 438 } else if (file.isDirectory()) { 439 // this is technically not a source folder (class folder instead) but since we 440 // only care about Java resources (ie non class/java files) this will do the 441 // same 442 apkBuilder.addSourceFolder(file); 443 } 444 } 445 446 // now write the native libraries. 447 // First look if the lib folder is there. 448 IResource libFolder = mProject.findMember(SdkConstants.FD_NATIVE_LIBS); 449 if (libFolder != null && libFolder.exists() && 450 libFolder.getType() == IResource.FOLDER) { 451 // get a File for the folder. 452 apkBuilder.addNativeLibraries(libFolder.getLocation().toFile()); 453 } 454 455 // write the native libraries for the library projects. 456 if (libProjects != null) { 457 for (IProject lib : libProjects) { 458 libFolder = lib.findMember(SdkConstants.FD_NATIVE_LIBS); 459 if (libFolder != null && libFolder.exists() && 460 libFolder.getType() == IResource.FOLDER) { 461 apkBuilder.addNativeLibraries(libFolder.getLocation().toFile()); 462 } 463 } 464 } 465 466 // seal the APK. 467 apkBuilder.sealApk(); 468 } catch (SealedApkException e) { 469 // this won't happen as we control when the apk is sealed. 470 } 471 } 472 473 public void setProguardOutput(String proguardFile) { 474 mProguardFile = proguardFile; 475 } 476 477 public Collection<String> getCompiledCodePaths() { 478 return mCompiledCodePaths; 479 } 480 481 public void runProguard(List<File> proguardConfigs, File inputJar, Collection<String> jarFiles, 482 File obfuscatedJar, File logOutput) 483 throws ProguardResultException, ProguardExecException, IOException { 484 IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); 485 486 // prepare the command line for proguard 487 List<String> command = new ArrayList<String>(); 488 command.add(AdtPlugin.getOsAbsoluteProguard()); 489 490 for (File configFile : proguardConfigs) { 491 command.add("-include"); //$NON-NLS-1$ 492 command.add(quotePath(configFile.getAbsolutePath())); 493 } 494 495 command.add("-injars"); //$NON-NLS-1$ 496 StringBuilder sb = new StringBuilder(quotePath(inputJar.getAbsolutePath())); 497 for (String jarFile : jarFiles) { 498 sb.append(File.pathSeparatorChar); 499 sb.append(quotePath(jarFile)); 500 } 501 command.add(quoteWinArg(sb.toString())); 502 503 command.add("-outjars"); //$NON-NLS-1$ 504 command.add(quotePath(obfuscatedJar.getAbsolutePath())); 505 506 command.add("-libraryjars"); //$NON-NLS-1$ 507 sb = new StringBuilder(quotePath(target.getPath(IAndroidTarget.ANDROID_JAR))); 508 IOptionalLibrary[] libraries = target.getOptionalLibraries(); 509 if (libraries != null) { 510 for (IOptionalLibrary lib : libraries) { 511 sb.append(File.pathSeparatorChar); 512 sb.append(quotePath(lib.getJarPath())); 513 } 514 } 515 command.add(quoteWinArg(sb.toString())); 516 517 if (logOutput != null) { 518 if (logOutput.isDirectory() == false) { 519 logOutput.mkdirs(); 520 } 521 522 command.add("-dump"); //$NON-NLS-1$ 523 command.add(new File(logOutput, "dump.txt").getAbsolutePath()); //$NON-NLS-1$ 524 525 command.add("-printseeds"); //$NON-NLS-1$ 526 command.add(new File(logOutput, "seeds.txt").getAbsolutePath()); //$NON-NLS-1$ 527 528 command.add("-printusage"); //$NON-NLS-1$ 529 command.add(new File(logOutput, "usage.txt").getAbsolutePath()); //$NON-NLS-1$ 530 531 command.add("-printmapping"); //$NON-NLS-1$ 532 command.add(new File(logOutput, "mapping.txt").getAbsolutePath()); //$NON-NLS-1$ 533 } 534 535 String commandArray[] = null; 536 537 if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { 538 commandArray = createWindowsProguardConfig(command); 539 } 540 541 if (commandArray == null) { 542 // For Mac & Linux, use a regular command string array. 543 commandArray = command.toArray(new String[command.size()]); 544 } 545 546 // Define PROGUARD_HOME to point to $SDK/tools/proguard if it's not yet defined. 547 // The Mac/Linux proguard.sh can infer it correctly but not the proguard.bat one. 548 String[] envp = null; 549 Map<String, String> envMap = new TreeMap<String, String>(System.getenv()); 550 if (!envMap.containsKey("PROGUARD_HOME")) { //$NON-NLS-1$ 551 envMap.put("PROGUARD_HOME", Sdk.getCurrent().getSdkLocation() + //$NON-NLS-1$ 552 SdkConstants.FD_TOOLS + File.separator + 553 SdkConstants.FD_PROGUARD); 554 envp = new String[envMap.size()]; 555 int i = 0; 556 for (Map.Entry<String, String> entry : envMap.entrySet()) { 557 envp[i++] = String.format("%1$s=%2$s", //$NON-NLS-1$ 558 entry.getKey(), 559 entry.getValue()); 560 } 561 } 562 563 if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { 564 sb = new StringBuilder(); 565 for (String c : commandArray) { 566 sb.append(c).append(' '); 567 } 568 AdtPlugin.printToConsole(mProject, sb.toString()); 569 } 570 571 // launch 572 int execError = 1; 573 try { 574 // launch the command line process 575 Process process = Runtime.getRuntime().exec(commandArray, envp); 576 577 // list to store each line of stderr 578 ArrayList<String> results = new ArrayList<String>(); 579 580 // get the output and return code from the process 581 execError = grabProcessOutput(mProject, process, results); 582 583 if (mVerbose) { 584 for (String resultString : results) { 585 mOutStream.println(resultString); 586 } 587 } 588 589 if (execError != 0) { 590 throw new ProguardResultException(execError, 591 results.toArray(new String[results.size()])); 592 } 593 594 } catch (IOException e) { 595 String msg = String.format(Messages.Proguard_Exec_Error, commandArray[0]); 596 throw new ProguardExecException(msg, e); 597 } catch (InterruptedException e) { 598 String msg = String.format(Messages.Proguard_Exec_Error, commandArray[0]); 599 throw new ProguardExecException(msg, e); 600 } 601 } 602 603 /** 604 * For tools R8 up to R11, the proguard.bat launcher on Windows only accepts 605 * arguments %1..%9. Since we generally have about 15 arguments, we were working 606 * around this by generating a temporary config file for proguard and then using 607 * that. 608 * Starting with tools R12, the proguard.bat launcher has been fixed to take 609 * all arguments using %* so we no longer need this hack. 610 * 611 * @param command 612 * @return 613 * @throws IOException 614 */ 615 private String[] createWindowsProguardConfig(List<String> command) throws IOException { 616 617 // Arg 0 is the proguard.bat path and arg 1 is the user config file 618 String launcher = AdtPlugin.readFile(new File(command.get(0))); 619 if (launcher.contains("%*")) { //$NON-NLS-1$ 620 // This is the launcher from Tools R12. Don't work around it. 621 return null; 622 } 623 624 // On Windows, proguard.bat can only pass %1...%9 to the java -jar proguard.jar 625 // call, but we have at least 15 arguments here so some get dropped silently 626 // and quoting is a big issue. So instead we'll work around that by writing 627 // all the arguments to a temporary config file. 628 629 String[] commandArray = new String[3]; 630 631 commandArray[0] = command.get(0); 632 commandArray[1] = command.get(1); 633 634 // Write all the other arguments to a config file 635 File argsFile = File.createTempFile(TEMP_PREFIX, ".pro"); //$NON-NLS-1$ 636 // TODO FIXME this may leave a lot of temp files around on a long session. 637 // Should have a better way to clean up e.g. before each build. 638 argsFile.deleteOnExit(); 639 640 FileWriter fw = new FileWriter(argsFile); 641 642 for (int i = 2; i < command.size(); i++) { 643 String s = command.get(i); 644 fw.write(s); 645 fw.write(s.startsWith("-") ? ' ' : '\n'); //$NON-NLS-1$ 646 } 647 648 fw.close(); 649 650 commandArray[2] = "@" + argsFile.getAbsolutePath(); //$NON-NLS-1$ 651 return commandArray; 652 } 653 654 /** 655 * Quotes a single path for proguard to deal with spaces. 656 * 657 * @param path The path to quote. 658 * @return The original path if it doesn't contain a space. 659 * Or the original path surrounded by single quotes if it contains spaces. 660 */ 661 private String quotePath(String path) { 662 if (path.indexOf(' ') != -1) { 663 path = '\'' + path + '\''; 664 } 665 return path; 666 } 667 668 /** 669 * Quotes a compound proguard argument to deal with spaces. 670 * <p/> 671 * Proguard takes multi-path arguments such as "path1;path2" for some options. 672 * When the {@link #quotePath} methods adds quotes for such a path if it contains spaces, 673 * the proguard shell wrapper will absorb the quotes, so we need to quote around the 674 * quotes. 675 * 676 * @param path The path to quote. 677 * @return The original path if it doesn't contain a single quote. 678 * Or on Windows the original path surrounded by double quotes if it contains a quote. 679 */ 680 private String quoteWinArg(String path) { 681 if (path.indexOf('\'') != -1 && 682 SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { 683 path = '"' + path + '"'; 684 } 685 return path; 686 } 687 688 689 /** 690 * Execute the Dx tool for dalvik code conversion. 691 * @param javaProject The java project 692 * @param inputPaths the input paths for DX 693 * @param osOutFilePath the path of the dex file to create. 694 * 695 * @throws CoreException 696 * @throws DexException 697 */ 698 public void executeDx(IJavaProject javaProject, Collection<String> inputPaths, 699 String osOutFilePath) 700 throws CoreException, DexException { 701 702 // get the dex wrapper 703 Sdk sdk = Sdk.getCurrent(); 704 DexWrapper wrapper = sdk.getDexWrapper(mBuildToolInfo); 705 706 if (wrapper == null) { 707 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 708 Messages.ApkBuilder_UnableBuild_Dex_Not_loaded)); 709 } 710 711 try { 712 // set a temporary prefix on the print streams. 713 mOutStream.setPrefix(CONSOLE_PREFIX_DX); 714 mErrStream.setPrefix(CONSOLE_PREFIX_DX); 715 716 IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(javaProject.getProject()); 717 File binFile = binFolder.getLocation().toFile(); 718 File dexedLibs = new File(binFile, "dexedLibs"); 719 if (dexedLibs.exists() == false) { 720 dexedLibs.mkdir(); 721 } 722 723 // replace the libs by their dexed versions (dexing them if needed.) 724 List<String> finalInputPaths = new ArrayList<String>(inputPaths.size()); 725 if (mDisableDexMerger || inputPaths.size() == 1) { 726 // only one input, no need to put a pre-dexed version, even if this path is 727 // just a jar file (case for proguard'ed builds) 728 finalInputPaths.addAll(inputPaths); 729 } else { 730 731 for (String input : inputPaths) { 732 File inputFile = new File(input); 733 if (inputFile.isDirectory()) { 734 finalInputPaths.add(input); 735 } else if (inputFile.isFile()) { 736 String fileName = getDexFileName(inputFile); 737 738 File dexedLib = new File(dexedLibs, fileName); 739 String dexedLibPath = dexedLib.getAbsolutePath(); 740 741 if (dexedLib.isFile() == false || 742 dexedLib.lastModified() < inputFile.lastModified()) { 743 744 if (mVerbose) { 745 mOutStream.println( 746 String.format("Pre-Dexing %1$s -> %2$s", input, fileName)); 747 } 748 749 if (dexedLib.isFile()) { 750 dexedLib.delete(); 751 } 752 753 int res = wrapper.run(dexedLibPath, Collections.singleton(input), 754 mForceJumbo, mVerbose, mOutStream, mErrStream); 755 756 if (res != 0) { 757 // output error message and mark the project. 758 String message = String.format(Messages.Dalvik_Error_d, res); 759 throw new DexException(message); 760 } 761 } else { 762 if (mVerbose) { 763 mOutStream.println( 764 String.format("Using Pre-Dexed %1$s <- %2$s", 765 fileName, input)); 766 } 767 } 768 769 finalInputPaths.add(dexedLibPath); 770 } 771 } 772 } 773 774 if (mVerbose) { 775 for (String input : finalInputPaths) { 776 mOutStream.println("Input: " + input); 777 } 778 } 779 780 int res = wrapper.run(osOutFilePath, 781 finalInputPaths, 782 mForceJumbo, 783 mVerbose, 784 mOutStream, mErrStream); 785 786 mOutStream.setPrefix(null); 787 mErrStream.setPrefix(null); 788 789 if (res != 0) { 790 // output error message and marker the project. 791 String message = String.format(Messages.Dalvik_Error_d, res); 792 throw new DexException(message); 793 } 794 } catch (DexException e) { 795 throw e; 796 } catch (Throwable t) { 797 String message = t.getMessage(); 798 if (message == null) { 799 message = t.getClass().getCanonicalName(); 800 } 801 message = String.format(Messages.Dalvik_Error_s, message); 802 803 throw new DexException(message, t); 804 } 805 } 806 807 private String getDexFileName(File inputFile) { 808 // get the filename 809 String name = inputFile.getName(); 810 // remove the extension 811 int pos = name.lastIndexOf('.'); 812 if (pos != -1) { 813 name = name.substring(0, pos); 814 } 815 816 // add a hash of the original file path 817 HashFunction hashFunction = Hashing.md5(); 818 HashCode hashCode = hashFunction.hashString(inputFile.getAbsolutePath()); 819 820 return name + "-" + hashCode.toString() + ".jar"; 821 } 822 823 /** 824 * Executes aapt. If any error happen, files or the project will be marked. 825 * @param command The command for aapt to execute. Currently supported: package and crunch 826 * @param osManifestPath The path to the manifest file 827 * @param osResPath The path to the res folder 828 * @param osAssetsPath The path to the assets folder. This can be null. 829 * @param osOutFilePath The path to the temporary resource file to create, 830 * or in the case of crunching the path to the cache to create/update. 831 * @param configFilter The configuration filter for the resources to include 832 * (used with -c option, for example "port,en,fr" to include portrait, English and French 833 * resources.) 834 * @param versionCode optional version code to insert in the manifest during packaging. If <=0 835 * then no value is inserted 836 * @throws AaptExecException 837 * @throws AaptResultException 838 */ 839 private void executeAapt(String aaptCommand, String osManifestPath, 840 List<String> osResPaths, String osAssetsPath, String osOutFilePath, 841 String configFilter, int versionCode) throws AaptExecException, AaptResultException { 842 IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); 843 844 String aapt = mBuildToolInfo.getPath(BuildToolInfo.PathId.AAPT); 845 846 // Create the command line. 847 ArrayList<String> commandArray = new ArrayList<String>(); 848 commandArray.add(aapt); 849 commandArray.add(aaptCommand); 850 if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { 851 commandArray.add("-v"); //$NON-NLS-1$ 852 } 853 854 // Common to all commands 855 for (String path : osResPaths) { 856 commandArray.add("-S"); //$NON-NLS-1$ 857 commandArray.add(path); 858 } 859 860 if (aaptCommand.equals(COMMAND_PACKAGE)) { 861 commandArray.add("-f"); //$NON-NLS-1$ 862 commandArray.add("--no-crunch"); //$NON-NLS-1$ 863 864 // if more than one res, this means there's a library (or more) and we need 865 // to activate the auto-add-overlay 866 if (osResPaths.size() > 1) { 867 commandArray.add("--auto-add-overlay"); //$NON-NLS-1$ 868 } 869 870 if (mDebugMode) { 871 commandArray.add("--debug-mode"); //$NON-NLS-1$ 872 } 873 874 if (versionCode > 0) { 875 commandArray.add("--version-code"); //$NON-NLS-1$ 876 commandArray.add(Integer.toString(versionCode)); 877 } 878 879 if (configFilter != null) { 880 commandArray.add("-c"); //$NON-NLS-1$ 881 commandArray.add(configFilter); 882 } 883 884 commandArray.add("-M"); //$NON-NLS-1$ 885 commandArray.add(osManifestPath); 886 887 if (osAssetsPath != null) { 888 commandArray.add("-A"); //$NON-NLS-1$ 889 commandArray.add(osAssetsPath); 890 } 891 892 commandArray.add("-I"); //$NON-NLS-1$ 893 commandArray.add(target.getPath(IAndroidTarget.ANDROID_JAR)); 894 895 commandArray.add("-F"); //$NON-NLS-1$ 896 commandArray.add(osOutFilePath); 897 } else if (aaptCommand.equals(COMMAND_CRUNCH)) { 898 commandArray.add("-C"); //$NON-NLS-1$ 899 commandArray.add(osOutFilePath); 900 } 901 902 String command[] = commandArray.toArray( 903 new String[commandArray.size()]); 904 905 if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { 906 StringBuilder sb = new StringBuilder(); 907 for (String c : command) { 908 sb.append(c); 909 sb.append(' '); 910 } 911 AdtPlugin.printToConsole(mProject, sb.toString()); 912 } 913 914 // Benchmarking start 915 long startAaptTime = 0; 916 if (BENCHMARK_FLAG) { 917 String msg = "BENCHMARK ADT: Starting " + aaptCommand //$NON-NLS-1$ 918 + " call to Aapt"; //$NON-NLS-1$ 919 AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); 920 startAaptTime = System.nanoTime(); 921 } 922 923 // launch 924 try { 925 // launch the command line process 926 Process process = Runtime.getRuntime().exec(command); 927 928 // list to store each line of stderr 929 ArrayList<String> stdErr = new ArrayList<String>(); 930 931 // get the output and return code from the process 932 int returnCode = grabProcessOutput(mProject, process, stdErr); 933 934 if (mVerbose) { 935 for (String stdErrString : stdErr) { 936 mOutStream.println(stdErrString); 937 } 938 } 939 if (returnCode != 0) { 940 throw new AaptResultException(returnCode, 941 stdErr.toArray(new String[stdErr.size()])); 942 } 943 } catch (IOException e) { 944 String msg = String.format(Messages.AAPT_Exec_Error_s, command[0]); 945 throw new AaptExecException(msg, e); 946 } catch (InterruptedException e) { 947 String msg = String.format(Messages.AAPT_Exec_Error_s, command[0]); 948 throw new AaptExecException(msg, e); 949 } 950 951 // Benchmarking end 952 if (BENCHMARK_FLAG) { 953 String msg = "BENCHMARK ADT: Ending " + aaptCommand //$NON-NLS-1$ 954 + " call to Aapt.\nBENCHMARK ADT: Time Elapsed: " //$NON-NLS-1$ 955 + ((System.nanoTime() - startAaptTime)/MILLION) + "ms"; //$NON-NLS-1$ 956 AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); 957 } 958 } 959 960 /** 961 * Computes all the project output and dependencies that must go into building the apk. 962 * 963 * @param resMarker 964 * @throws CoreException 965 */ 966 private void gatherPaths(ResourceMarker resMarker) 967 throws CoreException { 968 IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); 969 970 // get a java project for the project. 971 IJavaProject javaProject = JavaCore.create(mProject); 972 973 974 // get the output of the main project 975 IPath path = javaProject.getOutputLocation(); 976 IResource outputResource = wsRoot.findMember(path); 977 if (outputResource != null && outputResource.getType() == IResource.FOLDER) { 978 mCompiledCodePaths.add(outputResource.getLocation().toOSString()); 979 } 980 981 // we could use IJavaProject.getResolvedClasspath directly, but we actually 982 // want to see the containers themselves. 983 IClasspathEntry[] classpaths = javaProject.readRawClasspath(); 984 if (classpaths != null) { 985 for (IClasspathEntry e : classpaths) { 986 // ignore non exported entries, unless they're in the DEPEDENCIES container, 987 // in which case we always want it (there may be some older projects that 988 // have it as non exported). 989 if (e.isExported() || 990 (e.getEntryKind() == IClasspathEntry.CPE_CONTAINER && 991 e.getPath().toString().equals(AdtConstants.CONTAINER_DEPENDENCIES))) { 992 handleCPE(e, javaProject, wsRoot, resMarker); 993 } 994 } 995 } 996 } 997 998 private void handleCPE(IClasspathEntry entry, IJavaProject javaProject, 999 IWorkspaceRoot wsRoot, ResourceMarker resMarker) { 1000 1001 // if this is a classpath variable reference, we resolve it. 1002 if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { 1003 entry = JavaCore.getResolvedClasspathEntry(entry); 1004 } 1005 1006 if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) { 1007 IProject refProject = wsRoot.getProject(entry.getPath().lastSegment()); 1008 try { 1009 // ignore if it's an Android project, or if it's not a Java Project 1010 if (refProject.hasNature(JavaCore.NATURE_ID) && 1011 refProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 1012 IJavaProject refJavaProject = JavaCore.create(refProject); 1013 1014 // get the output folder 1015 IPath path = refJavaProject.getOutputLocation(); 1016 IResource outputResource = wsRoot.findMember(path); 1017 if (outputResource != null && outputResource.getType() == IResource.FOLDER) { 1018 mCompiledCodePaths.add(outputResource.getLocation().toOSString()); 1019 } 1020 } 1021 } catch (CoreException exception) { 1022 // can't query the project nature? ignore 1023 } 1024 1025 } else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { 1026 handleClasspathLibrary(entry, wsRoot, resMarker); 1027 } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { 1028 // get the container 1029 try { 1030 IClasspathContainer container = JavaCore.getClasspathContainer( 1031 entry.getPath(), javaProject); 1032 // ignore the system and default_system types as they represent 1033 // libraries that are part of the runtime. 1034 if (container != null && container.getKind() == IClasspathContainer.K_APPLICATION) { 1035 IClasspathEntry[] entries = container.getClasspathEntries(); 1036 for (IClasspathEntry cpe : entries) { 1037 handleCPE(cpe, javaProject, wsRoot, resMarker); 1038 } 1039 } 1040 } catch (JavaModelException jme) { 1041 // can't resolve the container? ignore it. 1042 AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", entry.getPath()); 1043 } 1044 } 1045 } 1046 1047 private void handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot, 1048 ResourceMarker resMarker) { 1049 // get the IPath 1050 IPath path = e.getPath(); 1051 1052 IResource resource = wsRoot.findMember(path); 1053 1054 if (resource != null && resource.getType() == IResource.PROJECT) { 1055 // if it's a project we should just ignore it because it's going to be added 1056 // later when we add all the referenced projects. 1057 1058 } else if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { 1059 // case of a jar file (which could be relative to the workspace or a full path) 1060 if (resource != null && resource.exists() && 1061 resource.getType() == IResource.FILE) { 1062 mCompiledCodePaths.add(resource.getLocation().toOSString()); 1063 } else { 1064 // if the jar path doesn't match a workspace resource, 1065 // then we get an OSString and check if this links to a valid file. 1066 String osFullPath = path.toOSString(); 1067 1068 File f = new File(osFullPath); 1069 if (f.isFile()) { 1070 mCompiledCodePaths.add(osFullPath); 1071 } else { 1072 String message = String.format( Messages.Couldnt_Locate_s_Error, 1073 path); 1074 // always output to the console 1075 mOutStream.println(message); 1076 1077 // put a marker 1078 if (resMarker != null) { 1079 resMarker.setWarning(mProject, message); 1080 } 1081 } 1082 } 1083 } else { 1084 // this can be the case for a class folder. 1085 if (resource != null && resource.exists() && 1086 resource.getType() == IResource.FOLDER) { 1087 mCompiledCodePaths.add(resource.getLocation().toOSString()); 1088 } else { 1089 // if the path doesn't match a workspace resource, 1090 // then we get an OSString and check if this links to a valid folder. 1091 String osFullPath = path.toOSString(); 1092 1093 File f = new File(osFullPath); 1094 if (f.isDirectory()) { 1095 mCompiledCodePaths.add(osFullPath); 1096 } 1097 } 1098 } 1099 } 1100 1101 /** 1102 * Checks a {@link IFile} to make sure it should be packaged as standard resources. 1103 * @param file the IFile representing the file. 1104 * @return true if the file should be packaged as standard java resources. 1105 */ 1106 public static boolean checkFileForPackaging(IFile file) { 1107 String name = file.getName(); 1108 1109 String ext = file.getFileExtension(); 1110 return ApkBuilder.checkFileForPackaging(name, ext); 1111 } 1112 1113 /** 1114 * Checks whether an {@link IFolder} and its content is valid for packaging into the .apk as 1115 * standard Java resource. 1116 * @param folder the {@link IFolder} to check. 1117 */ 1118 public static boolean checkFolderForPackaging(IFolder folder) { 1119 String name = folder.getName(); 1120 return ApkBuilder.checkFolderForPackaging(name); 1121 } 1122 1123 /** 1124 * Returns a list of {@link IJavaProject} matching the provided {@link IProject} objects. 1125 * @param projects the IProject objects. 1126 * @return a new list object containing the IJavaProject object for the given IProject objects. 1127 * @throws CoreException 1128 */ 1129 public static List<IJavaProject> getJavaProjects(List<IProject> projects) throws CoreException { 1130 ArrayList<IJavaProject> list = new ArrayList<IJavaProject>(); 1131 1132 for (IProject p : projects) { 1133 if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) { 1134 1135 list.add(JavaCore.create(p)); 1136 } 1137 } 1138 1139 return list; 1140 } 1141 1142 /** 1143 * Get the stderr output of a process and return when the process is done. 1144 * @param process The process to get the output from 1145 * @param stderr The array to store the stderr output 1146 * @return the process return code. 1147 * @throws InterruptedException 1148 */ 1149 public final static int grabProcessOutput( 1150 final IProject project, 1151 final Process process, 1152 final ArrayList<String> stderr) 1153 throws InterruptedException { 1154 1155 return GrabProcessOutput.grabProcessOutput( 1156 process, 1157 Wait.WAIT_FOR_READERS, // we really want to make sure we get all the output! 1158 new IProcessOutput() { 1159 1160 @SuppressWarnings("unused") 1161 @Override 1162 public void out(@Nullable String line) { 1163 if (line != null) { 1164 // If benchmarking always print the lines that 1165 // correspond to benchmarking info returned by ADT 1166 if (BENCHMARK_FLAG && line.startsWith("BENCHMARK:")) { //$NON-NLS-1$ 1167 AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, 1168 project, line); 1169 } else { 1170 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, 1171 project, line); 1172 } 1173 } 1174 } 1175 1176 @Override 1177 public void err(@Nullable String line) { 1178 if (line != null) { 1179 stderr.add(line); 1180 if (BuildVerbosity.VERBOSE == AdtPrefs.getPrefs().getBuildVerbosity()) { 1181 AdtPlugin.printErrorToConsole(project, line); 1182 } 1183 } 1184 } 1185 }); 1186 } 1187 } 1188