com.android.ide.eclipse.adt.internal.build.BuildHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.android.ide.eclipse.adt.internal.build.BuildHelper.java

Source

/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ide.eclipse.adt.internal.build;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AndroidPrintStream;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
import com.android.sdklib.build.ApkBuilder;
import com.android.sdklib.build.ApkBuilder.JarStatus;
import com.android.sdklib.build.ApkBuilder.SigningInfo;
import com.android.sdklib.build.ApkCreationException;
import com.android.sdklib.build.DuplicateFileException;
import com.android.sdklib.build.RenderScriptProcessor;
import com.android.sdklib.build.SealedApkException;
import com.android.sdklib.internal.build.DebugKeyProvider;
import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
import com.android.utils.GrabProcessOutput;
import com.android.utils.GrabProcessOutput.IProcessOutput;
import com.android.utils.GrabProcessOutput.Wait;
import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jface.preference.IPreferenceStore;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Helper with methods for the last 3 steps of the generation of an APK.
 *
 * {@link #packageResources(IFile, IProject[], String, int, String, String)} packages the
 * application resources using aapt into a zip file that is ready to be integrated into the apk.
 *
 * {@link #executeDx(IJavaProject, String, String, IJavaProject[])} will convert the Java byte
 * code into the Dalvik bytecode.
 *
 * {@link #finalPackage(String, String, String, boolean, IJavaProject, IProject[], IJavaProject[], String, boolean)}
 * will make the apk from all the previous components.
 *
 * This class only executes the 3 above actions. It does not handle the errors, and simply sends
 * them back as custom exceptions.
 *
 * Warnings are handled by the {@link ResourceMarker} interface.
 *
 * Console output (verbose and non verbose) is handled through the {@link AndroidPrintStream} passed
 * to the constructor.
 *
 */
public class BuildHelper {

    private static final String CONSOLE_PREFIX_DX = "Dx"; //$NON-NLS-1$
    private final static String TEMP_PREFIX = "android_"; //$NON-NLS-1$

    private static final String COMMAND_CRUNCH = "crunch"; //$NON-NLS-1$
    private static final String COMMAND_PACKAGE = "package"; //$NON-NLS-1$

    @NonNull
    private final ProjectState mProjectState;
    @NonNull
    private final IProject mProject;
    @NonNull
    private final BuildToolInfo mBuildToolInfo;
    @NonNull
    private final AndroidPrintStream mOutStream;
    @NonNull
    private final AndroidPrintStream mErrStream;
    private final boolean mForceJumbo;
    private final boolean mDisableDexMerger;
    private final boolean mVerbose;
    private final boolean mDebugMode;

    private final Set<String> mCompiledCodePaths = new HashSet<String>();

    public static final boolean BENCHMARK_FLAG = false;
    public static long sStartOverallTime = 0;
    public static long sStartJavaCTime = 0;

    private final static int MILLION = 1000000;
    private String mProguardFile;

    /**
     * An object able to put a marker on a resource.
     */
    public interface ResourceMarker {
        void setWarning(IResource resource, String message);
    }

    /**
     * Creates a new post-compiler helper
     * @param project
     * @param outStream
     * @param errStream
     * @param debugMode whether this is a debug build
     * @param verbose
     * @throws CoreException
     */
    public BuildHelper(@NonNull ProjectState projectState, @NonNull BuildToolInfo buildToolInfo,
            @NonNull AndroidPrintStream outStream, @NonNull AndroidPrintStream errStream, boolean forceJumbo,
            boolean disableDexMerger, boolean debugMode, boolean verbose, ResourceMarker resMarker)
            throws CoreException {
        mProjectState = projectState;
        mProject = projectState.getProject();
        mBuildToolInfo = buildToolInfo;
        mOutStream = outStream;
        mErrStream = errStream;
        mDebugMode = debugMode;
        mVerbose = verbose;
        mForceJumbo = forceJumbo;
        mDisableDexMerger = disableDexMerger;

        gatherPaths(resMarker);
    }

    public void updateCrunchCache() throws AaptExecException, AaptResultException {
        // Benchmarking start
        long startCrunchTime = 0;
        if (BENCHMARK_FLAG) {
            String msg = "BENCHMARK ADT: Starting Initial Packaging (.ap_)"; //$NON-NLS-1$
            AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
            startCrunchTime = System.nanoTime();
        }

        // Get the resources folder to crunch from
        IFolder resFolder = mProject.getFolder(AdtConstants.WS_RESOURCES);
        List<String> resPaths = new ArrayList<String>();
        resPaths.add(resFolder.getLocation().toOSString());

        // Get the output folder where the cache is stored.
        IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(mProject);
        IFolder cacheFolder = binFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_CRUNCHCACHE);
        String cachePath = cacheFolder.getLocation().toOSString();

        /* For crunching, we don't need the osManifestPath, osAssetsPath, or the configFilter
         * parameters for executeAapt
         */
        executeAapt(COMMAND_CRUNCH, "", resPaths, "", cachePath, "", 0);

        // Benchmarking end
        if (BENCHMARK_FLAG) {
            String msg = "BENCHMARK ADT: Ending Initial Package (.ap_). \nTime Elapsed: " //$NON-NLS-1$
                    + ((System.nanoTime() - startCrunchTime) / MILLION) + "ms"; //$NON-NLS-1$
            AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
        }
    }

    /**
     * Packages the resources of the projet into a .ap_ file.
     * @param manifestFile the manifest of the project.
     * @param libProjects the list of library projects that this project depends on.
     * @param resFilter an optional resource filter to be used with the -c option of aapt. If null
     * no filters are used.
     * @param versionCode an optional versionCode to be inserted in the manifest during packaging.
     * If the value is <=0, no values are inserted.
     * @param outputFolder where to write the resource ap_ file.
     * @param outputFilename the name of the resource ap_ file.
     * @throws AaptExecException
     * @throws AaptResultException
     */
    public void packageResources(IFile manifestFile, List<IProject> libProjects, String resFilter, int versionCode,
            String outputFolder, String outputFilename) throws AaptExecException, AaptResultException {

        // Benchmarking start
        long startPackageTime = 0;
        if (BENCHMARK_FLAG) {
            String msg = "BENCHMARK ADT: Starting Initial Packaging (.ap_)"; //$NON-NLS-1$
            AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
            startPackageTime = System.nanoTime();
        }

        // need to figure out some path before we can execute aapt;
        IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(mProject);

        // get the cache folder
        IFolder cacheFolder = binFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_CRUNCHCACHE);

        // get the BC folder
        IFolder bcFolder = binFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_BC);

        // get the resource folder
        IFolder resFolder = mProject.getFolder(AdtConstants.WS_RESOURCES);

        // and the assets folder
        IFolder assetsFolder = mProject.getFolder(AdtConstants.WS_ASSETS);

        // we need to make sure this one exists.
        if (assetsFolder.exists() == false) {
            assetsFolder = null;
        }

        // list of res folder (main project + maybe libraries)
        ArrayList<String> osResPaths = new ArrayList<String>();

        IPath resLocation = resFolder.getLocation();
        IPath manifestLocation = manifestFile.getLocation();

        if (resLocation != null && manifestLocation != null) {

            // png cache folder first.
            addFolderToList(osResPaths, cacheFolder);
            addFolderToList(osResPaths, bcFolder);

            // regular res folder next.
            osResPaths.add(resLocation.toOSString());

            // then libraries
            if (libProjects != null) {
                for (IProject lib : libProjects) {
                    // png cache folder first
                    IFolder libBinFolder = BaseProjectHelper.getAndroidOutputFolder(lib);

                    IFolder libCacheFolder = libBinFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_CRUNCHCACHE);
                    addFolderToList(osResPaths, libCacheFolder);

                    IFolder libBcFolder = libBinFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_BC);
                    addFolderToList(osResPaths, libBcFolder);

                    // regular res folder next.
                    IFolder libResFolder = lib.getFolder(AdtConstants.WS_RESOURCES);
                    addFolderToList(osResPaths, libResFolder);
                }
            }

            String osManifestPath = manifestLocation.toOSString();

            String osAssetsPath = null;
            if (assetsFolder != null) {
                osAssetsPath = assetsFolder.getLocation().toOSString();
            }

            // build the default resource package
            executeAapt(COMMAND_PACKAGE, osManifestPath, osResPaths, osAssetsPath,
                    outputFolder + File.separator + outputFilename, resFilter, versionCode);
        }

        // Benchmarking end
        if (BENCHMARK_FLAG) {
            String msg = "BENCHMARK ADT: Ending Initial Package (.ap_). \nTime Elapsed: " //$NON-NLS-1$
                    + ((System.nanoTime() - startPackageTime) / MILLION) + "ms"; //$NON-NLS-1$
            AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
        }
    }

    /**
     * Adds os path of a folder to a list only if the folder actually exists.
     * @param pathList
     * @param folder
     */
    private void addFolderToList(List<String> pathList, IFolder folder) {
        // use a File instead of the IFolder API to ignore workspace refresh issue.
        File testFile = new File(folder.getLocation().toOSString());
        if (testFile.isDirectory()) {
            pathList.add(testFile.getAbsolutePath());
        }
    }

    /**
     * Makes a final package signed with the debug key.
     *
     * Packages the dex files, the temporary resource file into the final package file.
     *
     * Whether the package is a debug package is controlled with the <var>debugMode</var> parameter
     * in {@link #PostCompilerHelper(IProject, PrintStream, PrintStream, boolean, boolean)}
     *
     * @param intermediateApk The path to the temporary resource file.
     * @param dex The path to the dex file.
     * @param output The path to the final package file to create.
     * @param libProjects an optional list of library projects (can be null)
     * @return true if success, false otherwise.
     * @throws ApkCreationException
     * @throws AndroidLocationException
     * @throws KeytoolException
     * @throws NativeLibInJarException
     * @throws CoreException
     * @throws DuplicateFileException
     */
    public void finalDebugPackage(String intermediateApk, String dex, String output, List<IProject> libProjects,
            ResourceMarker resMarker) throws ApkCreationException, KeytoolException, AndroidLocationException,
            NativeLibInJarException, DuplicateFileException, CoreException {

        AdtPlugin adt = AdtPlugin.getDefault();
        if (adt == null) {
            return;
        }

        // get the debug keystore to use.
        IPreferenceStore store = adt.getPreferenceStore();
        String keystoreOsPath = store.getString(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE);
        if (keystoreOsPath == null || new File(keystoreOsPath).isFile() == false) {
            keystoreOsPath = DebugKeyProvider.getDefaultKeyStoreOsPath();
            AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, Messages.ApkBuilder_Using_Default_Key);
        } else {
            AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject,
                    String.format(Messages.ApkBuilder_Using_s_To_Sign, keystoreOsPath));
        }

        // from the keystore, get the signing info
        SigningInfo info = ApkBuilder.getDebugKey(keystoreOsPath, mVerbose ? mOutStream : null);

        finalPackage(intermediateApk, dex, output, libProjects, info != null ? info.key : null,
                info != null ? info.certificate : null, resMarker);
    }

    /**
     * Makes the final package.
     *
     * Packages the dex files, the temporary resource file into the final package file.
     *
     * Whether the package is a debug package is controlled with the <var>debugMode</var> parameter
     * in {@link #PostCompilerHelper(IProject, PrintStream, PrintStream, boolean, boolean)}
     *
     * @param intermediateApk The path to the temporary resource file.
     * @param dex The path to the dex file.
     * @param output The path to the final package file to create.
     * @param debugSign whether the apk must be signed with the debug key.
     * @param libProjects an optional list of library projects (can be null)
     * @param abiFilter an optional filter. If not null, then only the matching ABI is included in
     * the final archive
     * @return true if success, false otherwise.
     * @throws NativeLibInJarException
     * @throws ApkCreationException
     * @throws CoreException
     * @throws DuplicateFileException
     */
    public void finalPackage(String intermediateApk, String dex, String output, List<IProject> libProjects,
            PrivateKey key, X509Certificate certificate, ResourceMarker resMarker)
            throws NativeLibInJarException, ApkCreationException, DuplicateFileException, CoreException {

        try {
            ApkBuilder apkBuilder = new ApkBuilder(output, intermediateApk, dex, key, certificate,
                    mVerbose ? mOutStream : null);
            apkBuilder.setDebugMode(mDebugMode);

            // either use the full compiled code paths or just the proguard file
            // if present
            Collection<String> pathsCollection = mCompiledCodePaths;
            if (mProguardFile != null) {
                pathsCollection = Collections.singletonList(mProguardFile);
                mProguardFile = null;
            }

            // Now we write the standard resources from all the output paths.
            for (String path : pathsCollection) {
                File file = new File(path);
                if (file.isFile()) {
                    JarStatus jarStatus = apkBuilder.addResourcesFromJar(file);

                    // check if we found native libraries in the external library. This
                    // constitutes an error or warning depending on if they are in lib/
                    if (jarStatus.getNativeLibs().size() > 0) {
                        String libName = file.getName();

                        String msg = String.format(
                                "Native libraries detected in '%1$s'. See console for more information.", libName);

                        ArrayList<String> consoleMsgs = new ArrayList<String>();

                        consoleMsgs.add(String.format(
                                "The library '%1$s' contains native libraries that will not run on the device.",
                                libName));

                        if (jarStatus.hasNativeLibsConflicts()) {
                            consoleMsgs.add(
                                    "Additionally some of those libraries will interfer with the installation of the application because of their location in lib/");
                            consoleMsgs.add("lib/ is reserved for NDK libraries.");
                        }

                        consoleMsgs.add("The following libraries were found:");

                        for (String lib : jarStatus.getNativeLibs()) {
                            consoleMsgs.add(" - " + lib);
                        }

                        String[] consoleStrings = consoleMsgs.toArray(new String[consoleMsgs.size()]);

                        // if there's a conflict or if the prefs force error on any native code in jar
                        // files, throw an exception
                        if (jarStatus.hasNativeLibsConflicts()
                                || AdtPrefs.getPrefs().getBuildForceErrorOnNativeLibInJar()) {
                            throw new NativeLibInJarException(jarStatus, msg, libName, consoleStrings);
                        } else {
                            // otherwise, put a warning, and output to the console also.
                            if (resMarker != null) {
                                resMarker.setWarning(mProject, msg);
                            }

                            for (String string : consoleStrings) {
                                mOutStream.println(string);
                            }
                        }
                    }
                } else if (file.isDirectory()) {
                    // this is technically not a source folder (class folder instead) but since we
                    // only care about Java resources (ie non class/java files) this will do the
                    // same
                    apkBuilder.addSourceFolder(file);
                }
            }

            // now write the native libraries.
            // First look if the lib folder is there.
            IResource libFolder = mProject.findMember(SdkConstants.FD_NATIVE_LIBS);
            if (libFolder != null && libFolder.exists() && libFolder.getType() == IResource.FOLDER) {
                // get a File for the folder.
                apkBuilder.addNativeLibraries(libFolder.getLocation().toFile());
            }

            // next the native libraries for the renderscript support mode.
            if (mProjectState.getRenderScriptSupportMode()) {
                IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(mProject);
                IResource rsLibFolder = androidOutputFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_RS_LIBS);
                File rsLibFolderFile = rsLibFolder.getLocation().toFile();
                if (rsLibFolderFile.isDirectory()) {
                    apkBuilder.addNativeLibraries(rsLibFolderFile);
                }

                File rsLibs = RenderScriptProcessor
                        .getSupportNativeLibFolder(mBuildToolInfo.getLocation().getAbsolutePath());
                if (rsLibs.isDirectory()) {
                    apkBuilder.addNativeLibraries(rsLibs);
                }
            }

            // write the native libraries for the library projects.
            if (libProjects != null) {
                for (IProject lib : libProjects) {
                    libFolder = lib.findMember(SdkConstants.FD_NATIVE_LIBS);
                    if (libFolder != null && libFolder.exists() && libFolder.getType() == IResource.FOLDER) {
                        apkBuilder.addNativeLibraries(libFolder.getLocation().toFile());
                    }
                }
            }

            // seal the APK.
            apkBuilder.sealApk();
        } catch (SealedApkException e) {
            // this won't happen as we control when the apk is sealed.
        }
    }

    public void setProguardOutput(String proguardFile) {
        mProguardFile = proguardFile;
    }

    public Collection<String> getCompiledCodePaths() {
        return mCompiledCodePaths;
    }

    public void runProguard(List<File> proguardConfigs, File inputJar, Collection<String> jarFiles,
            File obfuscatedJar, File logOutput) throws ProguardResultException, ProguardExecException, IOException {
        IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);

        // prepare the command line for proguard
        List<String> command = new ArrayList<String>();
        command.add(AdtPlugin.getOsAbsoluteProguard());

        for (File configFile : proguardConfigs) {
            command.add("-include"); //$NON-NLS-1$
            command.add(quotePath(configFile.getAbsolutePath()));
        }

        command.add("-injars"); //$NON-NLS-1$
        StringBuilder sb = new StringBuilder(quotePath(inputJar.getAbsolutePath()));
        for (String jarFile : jarFiles) {
            sb.append(File.pathSeparatorChar);
            sb.append(quotePath(jarFile));
        }
        command.add(quoteWinArg(sb.toString()));

        command.add("-outjars"); //$NON-NLS-1$
        command.add(quotePath(obfuscatedJar.getAbsolutePath()));

        command.add("-libraryjars"); //$NON-NLS-1$
        sb = new StringBuilder(quotePath(target.getPath(IAndroidTarget.ANDROID_JAR)));
        IOptionalLibrary[] libraries = target.getOptionalLibraries();
        if (libraries != null) {
            for (IOptionalLibrary lib : libraries) {
                sb.append(File.pathSeparatorChar);
                sb.append(quotePath(lib.getJarPath()));
            }
        }
        command.add(quoteWinArg(sb.toString()));

        if (logOutput != null) {
            if (logOutput.isDirectory() == false) {
                logOutput.mkdirs();
            }

            command.add("-dump"); //$NON-NLS-1$
            command.add(new File(logOutput, "dump.txt").getAbsolutePath()); //$NON-NLS-1$

            command.add("-printseeds"); //$NON-NLS-1$
            command.add(new File(logOutput, "seeds.txt").getAbsolutePath()); //$NON-NLS-1$

            command.add("-printusage"); //$NON-NLS-1$
            command.add(new File(logOutput, "usage.txt").getAbsolutePath()); //$NON-NLS-1$

            command.add("-printmapping"); //$NON-NLS-1$
            command.add(new File(logOutput, "mapping.txt").getAbsolutePath()); //$NON-NLS-1$
        }

        String commandArray[] = null;

        if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) {
            commandArray = createWindowsProguardConfig(command);
        }

        if (commandArray == null) {
            // For Mac & Linux, use a regular command string array.
            commandArray = command.toArray(new String[command.size()]);
        }

        // Define PROGUARD_HOME to point to $SDK/tools/proguard if it's not yet defined.
        // The Mac/Linux proguard.sh can infer it correctly but not the proguard.bat one.
        String[] envp = null;
        Map<String, String> envMap = new TreeMap<String, String>(System.getenv());
        if (!envMap.containsKey("PROGUARD_HOME")) { //$NON-NLS-1$
            envMap.put("PROGUARD_HOME", Sdk.getCurrent().getSdkOsLocation() + //$NON-NLS-1$
                    SdkConstants.FD_TOOLS + File.separator + SdkConstants.FD_PROGUARD);
            envp = new String[envMap.size()];
            int i = 0;
            for (Map.Entry<String, String> entry : envMap.entrySet()) {
                envp[i++] = String.format("%1$s=%2$s", //$NON-NLS-1$
                        entry.getKey(), entry.getValue());
            }
        }

        if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
            sb = new StringBuilder();
            for (String c : commandArray) {
                sb.append(c).append(' ');
            }
            AdtPlugin.printToConsole(mProject, sb.toString());
        }

        // launch
        int execError = 1;
        try {
            // launch the command line process
            Process process = Runtime.getRuntime().exec(commandArray, envp);

            // list to store each line of stderr
            ArrayList<String> results = new ArrayList<String>();

            // get the output and return code from the process
            execError = grabProcessOutput(mProject, process, results);

            if (mVerbose) {
                for (String resultString : results) {
                    mOutStream.println(resultString);
                }
            }

            if (execError != 0) {
                throw new ProguardResultException(execError, results.toArray(new String[results.size()]));
            }

        } catch (IOException e) {
            String msg = String.format(Messages.Proguard_Exec_Error, commandArray[0]);
            throw new ProguardExecException(msg, e);
        } catch (InterruptedException e) {
            String msg = String.format(Messages.Proguard_Exec_Error, commandArray[0]);
            throw new ProguardExecException(msg, e);
        }
    }

    /**
     * For tools R8 up to R11, the proguard.bat launcher on Windows only accepts
     * arguments %1..%9. Since we generally have about 15 arguments, we were working
     * around this by generating a temporary config file for proguard and then using
     * that.
     * Starting with tools R12, the proguard.bat launcher has been fixed to take
     * all arguments using %* so we no longer need this hack.
     *
     * @param command
     * @return
     * @throws IOException
     */
    private String[] createWindowsProguardConfig(List<String> command) throws IOException {

        // Arg 0 is the proguard.bat path and arg 1 is the user config file
        String launcher = AdtPlugin.readFile(new File(command.get(0)));
        if (launcher.contains("%*")) { //$NON-NLS-1$
            // This is the launcher from Tools R12. Don't work around it.
            return null;
        }

        // On Windows, proguard.bat can only pass %1...%9 to the java -jar proguard.jar
        // call, but we have at least 15 arguments here so some get dropped silently
        // and quoting is a big issue. So instead we'll work around that by writing
        // all the arguments to a temporary config file.

        String[] commandArray = new String[3];

        commandArray[0] = command.get(0);
        commandArray[1] = command.get(1);

        // Write all the other arguments to a config file
        File argsFile = File.createTempFile(TEMP_PREFIX, ".pro"); //$NON-NLS-1$
        // TODO FIXME this may leave a lot of temp files around on a long session.
        // Should have a better way to clean up e.g. before each build.
        argsFile.deleteOnExit();

        FileWriter fw = new FileWriter(argsFile);

        for (int i = 2; i < command.size(); i++) {
            String s = command.get(i);
            fw.write(s);
            fw.write(s.startsWith("-") ? ' ' : '\n'); //$NON-NLS-1$
        }

        fw.close();

        commandArray[2] = "@" + argsFile.getAbsolutePath(); //$NON-NLS-1$
        return commandArray;
    }

    /**
     * Quotes a single path for proguard to deal with spaces.
     *
     * @param path The path to quote.
     * @return The original path if it doesn't contain a space.
     *   Or the original path surrounded by single quotes if it contains spaces.
     */
    private String quotePath(String path) {
        if (path.indexOf(' ') != -1) {
            path = '\'' + path + '\'';
        }
        return path;
    }

    /**
     * Quotes a compound proguard argument to deal with spaces.
     * <p/>
     * Proguard takes multi-path arguments such as "path1;path2" for some options.
     * When the {@link #quotePath} methods adds quotes for such a path if it contains spaces,
     * the proguard shell wrapper will absorb the quotes, so we need to quote around the
     * quotes.
     *
     * @param path The path to quote.
     * @return The original path if it doesn't contain a single quote.
     *   Or on Windows the original path surrounded by double quotes if it contains a quote.
     */
    private String quoteWinArg(String path) {
        if (path.indexOf('\'') != -1 && SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) {
            path = '"' + path + '"';
        }
        return path;
    }

    /**
     * Execute the Dx tool for dalvik code conversion.
     * @param javaProject The java project
     * @param inputPaths the input paths for DX
     * @param osOutFilePath the path of the dex file to create.
     *
     * @throws CoreException
     * @throws DexException
     */
    public void executeDx(IJavaProject javaProject, Collection<String> inputPaths, String osOutFilePath)
            throws CoreException, DexException {

        // get the dex wrapper
        Sdk sdk = Sdk.getCurrent();
        DexWrapper wrapper = sdk.getDexWrapper(mBuildToolInfo);

        if (wrapper == null) {
            throw new CoreException(
                    new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, Messages.ApkBuilder_UnableBuild_Dex_Not_loaded));
        }

        try {
            // set a temporary prefix on the print streams.
            mOutStream.setPrefix(CONSOLE_PREFIX_DX);
            mErrStream.setPrefix(CONSOLE_PREFIX_DX);

            IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(javaProject.getProject());
            File binFile = binFolder.getLocation().toFile();
            File dexedLibs = new File(binFile, "dexedLibs");
            if (dexedLibs.exists() == false) {
                dexedLibs.mkdir();
            }

            // replace the libs by their dexed versions (dexing them if needed.)
            List<String> finalInputPaths = new ArrayList<String>(inputPaths.size());
            if (mDisableDexMerger || inputPaths.size() == 1) {
                // only one input, no need to put a pre-dexed version, even if this path is
                // just a jar file (case for proguard'ed builds)
                finalInputPaths.addAll(inputPaths);
            } else {

                for (String input : inputPaths) {
                    File inputFile = new File(input);
                    if (inputFile.isDirectory()) {
                        finalInputPaths.add(input);
                    } else if (inputFile.isFile()) {
                        String fileName = getDexFileName(inputFile);

                        File dexedLib = new File(dexedLibs, fileName);
                        String dexedLibPath = dexedLib.getAbsolutePath();

                        if (dexedLib.isFile() == false || dexedLib.lastModified() < inputFile.lastModified()) {

                            if (mVerbose) {
                                mOutStream.println(String.format("Pre-Dexing %1$s -> %2$s", input, fileName));
                            }

                            if (dexedLib.isFile()) {
                                dexedLib.delete();
                            }

                            int res = wrapper.run(dexedLibPath, Collections.singleton(input), mForceJumbo, mVerbose,
                                    mOutStream, mErrStream);

                            if (res != 0) {
                                // output error message and mark the project.
                                String message = String.format(Messages.Dalvik_Error_d, res);
                                throw new DexException(message);
                            }
                        } else {
                            if (mVerbose) {
                                mOutStream.println(String.format("Using Pre-Dexed %1$s <- %2$s", fileName, input));
                            }
                        }

                        finalInputPaths.add(dexedLibPath);
                    }
                }
            }

            if (mVerbose) {
                for (String input : finalInputPaths) {
                    mOutStream.println("Input: " + input);
                }
            }

            int res = wrapper.run(osOutFilePath, finalInputPaths, mForceJumbo, mVerbose, mOutStream, mErrStream);

            mOutStream.setPrefix(null);
            mErrStream.setPrefix(null);

            if (res != 0) {
                // output error message and marker the project.
                String message = String.format(Messages.Dalvik_Error_d, res);
                throw new DexException(message);
            }
        } catch (DexException e) {
            throw e;
        } catch (Throwable t) {
            String message = t.getMessage();
            if (message == null) {
                message = t.getClass().getCanonicalName();
            }
            message = String.format(Messages.Dalvik_Error_s, message);

            throw new DexException(message, t);
        }
    }

    private String getDexFileName(File inputFile) {
        // get the filename
        String name = inputFile.getName();
        // remove the extension
        int pos = name.lastIndexOf('.');
        if (pos != -1) {
            name = name.substring(0, pos);
        }

        // add a hash of the original file path
        HashFunction hashFunction = Hashing.md5();
        HashCode hashCode = hashFunction.hashString(inputFile.getAbsolutePath(), Charsets.UTF_8);

        return name + "-" + hashCode.toString() + ".jar";
    }

    /**
     * Executes aapt. If any error happen, files or the project will be marked.
     * @param command The command for aapt to execute. Currently supported: package and crunch
     * @param osManifestPath The path to the manifest file
     * @param osResPath The path to the res folder
     * @param osAssetsPath The path to the assets folder. This can be null.
     * @param osOutFilePath The path to the temporary resource file to create,
     *   or in the case of crunching the path to the cache to create/update.
     * @param configFilter The configuration filter for the resources to include
     * (used with -c option, for example "port,en,fr" to include portrait, English and French
     * resources.)
     * @param versionCode optional version code to insert in the manifest during packaging. If <=0
     * then no value is inserted
     * @throws AaptExecException
     * @throws AaptResultException
     */
    private void executeAapt(String aaptCommand, String osManifestPath, List<String> osResPaths,
            String osAssetsPath, String osOutFilePath, String configFilter, int versionCode)
            throws AaptExecException, AaptResultException {
        IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);

        String aapt = mBuildToolInfo.getPath(BuildToolInfo.PathId.AAPT);

        // Create the command line.
        ArrayList<String> commandArray = new ArrayList<String>();
        commandArray.add(aapt);
        commandArray.add(aaptCommand);
        if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
            commandArray.add("-v"); //$NON-NLS-1$
        }

        // Common to all commands
        for (String path : osResPaths) {
            commandArray.add("-S"); //$NON-NLS-1$
            commandArray.add(path);
        }

        if (aaptCommand.equals(COMMAND_PACKAGE)) {
            commandArray.add("-f"); //$NON-NLS-1$
            commandArray.add("--no-crunch"); //$NON-NLS-1$

            // if more than one res, this means there's a library (or more) and we need
            // to activate the auto-add-overlay
            if (osResPaths.size() > 1) {
                commandArray.add("--auto-add-overlay"); //$NON-NLS-1$
            }

            if (mDebugMode) {
                commandArray.add("--debug-mode"); //$NON-NLS-1$
            }

            if (versionCode > 0) {
                commandArray.add("--version-code"); //$NON-NLS-1$
                commandArray.add(Integer.toString(versionCode));
            }

            if (configFilter != null) {
                commandArray.add("-c"); //$NON-NLS-1$
                commandArray.add(configFilter);
            }

            // never compress apks.
            commandArray.add("-0");
            commandArray.add("apk");

            commandArray.add("-M"); //$NON-NLS-1$
            commandArray.add(osManifestPath);

            if (osAssetsPath != null) {
                commandArray.add("-A"); //$NON-NLS-1$
                commandArray.add(osAssetsPath);
            }

            commandArray.add("-I"); //$NON-NLS-1$
            commandArray.add(target.getPath(IAndroidTarget.ANDROID_JAR));

            commandArray.add("-F"); //$NON-NLS-1$
            commandArray.add(osOutFilePath);
        } else if (aaptCommand.equals(COMMAND_CRUNCH)) {
            commandArray.add("-C"); //$NON-NLS-1$
            commandArray.add(osOutFilePath);
        }

        String command[] = commandArray.toArray(new String[commandArray.size()]);

        if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
            StringBuilder sb = new StringBuilder();
            for (String c : command) {
                sb.append(c);
                sb.append(' ');
            }
            AdtPlugin.printToConsole(mProject, sb.toString());
        }

        // Benchmarking start
        long startAaptTime = 0;
        if (BENCHMARK_FLAG) {
            String msg = "BENCHMARK ADT: Starting " + aaptCommand //$NON-NLS-1$
                    + " call to Aapt"; //$NON-NLS-1$
            AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
            startAaptTime = System.nanoTime();
        }

        // launch
        try {
            // launch the command line process
            Process process = Runtime.getRuntime().exec(command);

            // list to store each line of stderr
            ArrayList<String> stdErr = new ArrayList<String>();

            // get the output and return code from the process
            int returnCode = grabProcessOutput(mProject, process, stdErr);

            if (mVerbose) {
                for (String stdErrString : stdErr) {
                    mOutStream.println(stdErrString);
                }
            }
            if (returnCode != 0) {
                throw new AaptResultException(returnCode, stdErr.toArray(new String[stdErr.size()]));
            }
        } catch (IOException e) {
            String msg = String.format(Messages.AAPT_Exec_Error_s, command[0]);
            throw new AaptExecException(msg, e);
        } catch (InterruptedException e) {
            String msg = String.format(Messages.AAPT_Exec_Error_s, command[0]);
            throw new AaptExecException(msg, e);
        }

        // Benchmarking end
        if (BENCHMARK_FLAG) {
            String msg = "BENCHMARK ADT: Ending " + aaptCommand //$NON-NLS-1$
                    + " call to Aapt.\nBENCHMARK ADT: Time Elapsed: " //$NON-NLS-1$
                    + ((System.nanoTime() - startAaptTime) / MILLION) + "ms"; //$NON-NLS-1$
            AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
        }
    }

    /**
     * Computes all the project output and dependencies that must go into building the apk.
     *
     * @param resMarker
     * @throws CoreException
     */
    private void gatherPaths(ResourceMarker resMarker) throws CoreException {
        IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();

        // get a java project for the project.
        IJavaProject javaProject = JavaCore.create(mProject);

        // get the output of the main project
        IPath path = javaProject.getOutputLocation();
        IResource outputResource = wsRoot.findMember(path);
        if (outputResource != null && outputResource.getType() == IResource.FOLDER) {
            mCompiledCodePaths.add(outputResource.getLocation().toOSString());
        }

        // we could use IJavaProject.getResolvedClasspath directly, but we actually
        // want to see the containers themselves.
        IClasspathEntry[] classpaths = javaProject.readRawClasspath();
        if (classpaths != null) {
            for (IClasspathEntry e : classpaths) {
                // ignore non exported entries, unless they're in the DEPEDENCIES container,
                // in which case we always want it (there may be some older projects that
                // have it as non exported).
                if (e.isExported() || (e.getEntryKind() == IClasspathEntry.CPE_CONTAINER
                        && e.getPath().toString().equals(AdtConstants.CONTAINER_DEPENDENCIES))) {
                    handleCPE(e, javaProject, wsRoot, resMarker);
                }
            }
        }
    }

    private void handleCPE(IClasspathEntry entry, IJavaProject javaProject, IWorkspaceRoot wsRoot,
            ResourceMarker resMarker) {

        // if this is a classpath variable reference, we resolve it.
        if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
            entry = JavaCore.getResolvedClasspathEntry(entry);
        }

        if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
            IProject refProject = wsRoot.getProject(entry.getPath().lastSegment());
            try {
                // ignore if it's an Android project, or if it's not a Java Project
                if (refProject.hasNature(JavaCore.NATURE_ID)
                        && refProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
                    IJavaProject refJavaProject = JavaCore.create(refProject);

                    // get the output folder
                    IPath path = refJavaProject.getOutputLocation();
                    IResource outputResource = wsRoot.findMember(path);
                    if (outputResource != null && outputResource.getType() == IResource.FOLDER) {
                        mCompiledCodePaths.add(outputResource.getLocation().toOSString());
                    }
                }
            } catch (CoreException exception) {
                // can't query the project nature? ignore
            }

        } else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
            handleClasspathLibrary(entry, wsRoot, resMarker);
        } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
            // get the container
            try {
                IClasspathContainer container = JavaCore.getClasspathContainer(entry.getPath(), javaProject);
                // ignore the system and default_system types as they represent
                // libraries that are part of the runtime.
                if (container != null && container.getKind() == IClasspathContainer.K_APPLICATION) {
                    IClasspathEntry[] entries = container.getClasspathEntries();
                    for (IClasspathEntry cpe : entries) {
                        handleCPE(cpe, javaProject, wsRoot, resMarker);
                    }
                }
            } catch (JavaModelException jme) {
                // can't resolve the container? ignore it.
                AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", entry.getPath());
            }
        }
    }

    private void handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot, ResourceMarker resMarker) {
        // get the IPath
        IPath path = e.getPath();

        IResource resource = wsRoot.findMember(path);

        if (resource != null && resource.getType() == IResource.PROJECT) {
            // if it's a project we should just ignore it because it's going to be added
            // later when we add all the referenced projects.

        } else if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) {
            // case of a jar file (which could be relative to the workspace or a full path)
            if (resource != null && resource.exists() && resource.getType() == IResource.FILE) {
                mCompiledCodePaths.add(resource.getLocation().toOSString());
            } else {
                // if the jar path doesn't match a workspace resource,
                // then we get an OSString and check if this links to a valid file.
                String osFullPath = path.toOSString();

                File f = new File(osFullPath);
                if (f.isFile()) {
                    mCompiledCodePaths.add(osFullPath);
                } else {
                    String message = String.format(Messages.Couldnt_Locate_s_Error, path);
                    // always output to the console
                    mOutStream.println(message);

                    // put a marker
                    if (resMarker != null) {
                        resMarker.setWarning(mProject, message);
                    }
                }
            }
        } else {
            // this can be the case for a class folder.
            if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) {
                mCompiledCodePaths.add(resource.getLocation().toOSString());
            } else {
                // if the path doesn't match a workspace resource,
                // then we get an OSString and check if this links to a valid folder.
                String osFullPath = path.toOSString();

                File f = new File(osFullPath);
                if (f.isDirectory()) {
                    mCompiledCodePaths.add(osFullPath);
                }
            }
        }
    }

    /**
     * Checks a {@link IFile} to make sure it should be packaged as standard resources.
     * @param file the IFile representing the file.
     * @return true if the file should be packaged as standard java resources.
     */
    public static boolean checkFileForPackaging(IFile file) {
        String name = file.getName();

        String ext = file.getFileExtension();
        return ApkBuilder.checkFileForPackaging(name, ext);
    }

    /**
     * Checks whether an {@link IFolder} and its content is valid for packaging into the .apk as
     * standard Java resource.
     * @param folder the {@link IFolder} to check.
     */
    public static boolean checkFolderForPackaging(IFolder folder) {
        String name = folder.getName();
        return ApkBuilder.checkFolderForPackaging(name);
    }

    /**
     * Returns a list of {@link IJavaProject} matching the provided {@link IProject} objects.
     * @param projects the IProject objects.
     * @return a new list object containing the IJavaProject object for the given IProject objects.
     * @throws CoreException
     */
    public static List<IJavaProject> getJavaProjects(List<IProject> projects) throws CoreException {
        ArrayList<IJavaProject> list = new ArrayList<IJavaProject>();

        for (IProject p : projects) {
            if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) {

                list.add(JavaCore.create(p));
            }
        }

        return list;
    }

    /**
     * Get the stderr output of a process and return when the process is done.
     * @param process The process to get the output from
     * @param stderr The array to store the stderr output
     * @return the process return code.
     * @throws InterruptedException
     */
    public final static int grabProcessOutput(final IProject project, final Process process,
            final ArrayList<String> stderr) throws InterruptedException {

        return GrabProcessOutput.grabProcessOutput(process, Wait.WAIT_FOR_READERS, // we really want to make sure we get all the output!
                new IProcessOutput() {

                    @SuppressWarnings("unused")
                    @Override
                    public void out(@Nullable String line) {
                        if (line != null) {
                            // If benchmarking always print the lines that
                            // correspond to benchmarking info returned by ADT
                            if (BENCHMARK_FLAG && line.startsWith("BENCHMARK:")) { //$NON-NLS-1$
                                AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, line);
                            } else {
                                AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, line);
                            }
                        }
                    }

                    @Override
                    public void err(@Nullable String line) {
                        if (line != null) {
                            stderr.add(line);
                            if (BuildVerbosity.VERBOSE == AdtPrefs.getPrefs().getBuildVerbosity()) {
                                AdtPlugin.printErrorToConsole(project, line);
                            }
                        }
                    }
                });
    }
}