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