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