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