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