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