• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
setWarning(IResource resource, String message)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      */
BuildHelper(IProject project, AndroidPrintStream outStream, AndroidPrintStream errStream, boolean debugMode, boolean verbose, ResourceMarker resMarker)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 
updateCrunchCache()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      */
packageResources(IFile manifestFile, List<IProject> libProjects, String resFilter, int versionCode, String outputFolder, String outputFilename)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      */
addFolderToList(List<String> pathList, IFolder folder)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      */
finalDebugPackage(String intermediateApk, String dex, String output, List<IProject> libProjects, ResourceMarker resMarker)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      */
finalPackage(String intermediateApk, String dex, String output, List<IProject> libProjects, PrivateKey key, X509Certificate certificate, ResourceMarker resMarker)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 
setProguardOutput(String proguardFile)453     public void setProguardOutput(String proguardFile) {
454         mProguardFile = proguardFile;
455     }
456 
getCompiledCodePaths()457     public Collection<String> getCompiledCodePaths() {
458         return mCompiledCodePaths;
459     }
460 
runProguard(List<File> proguardConfigs, File inputJar, Collection<String> jarFiles, File obfuscatedJar, File logOutput)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      */
createWindowsProguardConfig(List<String> command)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      */
quotePath(String path)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      */
quoteWinArg(String path)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      */
executeDx(IJavaProject javaProject, Collection<String> inputPaths, String osOutFilePath)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      */
executeAapt(String aaptCommand, String osManifestPath, List<String> osResPaths, String osAssetsPath, String osOutFilePath, String configFilter, int versionCode)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      */
gatherPaths(ResourceMarker resMarker)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 
handleCPE(IClasspathEntry entry, IJavaProject javaProject, IWorkspaceRoot wsRoot, ResourceMarker resMarker)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 
handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot, ResourceMarker resMarker)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      */
checkFileForPackaging(IFile file)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      */
checkFolderForPackaging(IFolder folder)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      */
getJavaProjects(List<IProject> projects)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      */
grabProcessOutput( final IProject project, final Process process, final ArrayList<String> results)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