--- /dev/null
+/*
+ * 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.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AndroidConstants;
+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.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
+import com.android.sdklib.SdkConstants;
+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.SealedApkException;
+import com.android.sdklib.internal.build.DebugKeyProvider;
+import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
+import com.android.sdklib.internal.build.SignedJarBuilder;
+
+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.IResourceProxy;
+import org.eclipse.core.resources.IResourceProxyVisitor;
+import org.eclipse.core.resources.IWorkspace;
+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.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.BufferedReader;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+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 final IProject mProject;
+ private final AndroidPrintStream mOutStream;
+ private final AndroidPrintStream mErrStream;
+ private final boolean mVerbose;
+ private final boolean mDebugMode;
+
+ /**
+ * 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
+ */
+ public BuildHelper(IProject project, AndroidPrintStream outStream,
+ AndroidPrintStream errStream, boolean debugMode, boolean verbose) {
+ mProject = project;
+ mOutStream = outStream;
+ mErrStream = errStream;
+ mDebugMode = debugMode;
+ mVerbose = verbose;
+ }
+
+ /**
+ * 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 {
+ // need to figure out some path before we can execute aapt;
+
+ // get the resource folder
+ IFolder resFolder = mProject.getFolder(AndroidConstants.WS_RESOURCES);
+
+ // and the assets folder
+ IFolder assetsFolder = mProject.getFolder(AndroidConstants.WS_ASSETS);
+
+ // we need to make sure this one exists.
+ if (assetsFolder.exists() == false) {
+ assetsFolder = null;
+ }
+
+ IPath resLocation = resFolder.getLocation();
+ IPath manifestLocation = manifestFile.getLocation();
+
+ if (resLocation != null && manifestLocation != null) {
+ // list of res folder (main project + maybe libraries)
+ ArrayList<String> osResPaths = new ArrayList<String>();
+ osResPaths.add(resLocation.toOSString()); //main project
+
+ // libraries?
+ if (libProjects != null) {
+ for (IProject lib : libProjects) {
+ IFolder libResFolder = lib.getFolder(SdkConstants.FD_RES);
+ if (libResFolder.exists()) {
+ osResPaths.add(libResFolder.getLocation().toOSString());
+ }
+ }
+ }
+
+ String osManifestPath = manifestLocation.toOSString();
+
+ String osAssetsPath = null;
+ if (assetsFolder != null) {
+ osAssetsPath = assetsFolder.getLocation().toOSString();
+ }
+
+ // build the default resource package
+ executeAapt(osManifestPath, osResPaths, osAssetsPath,
+ outputFolder + File.separator + outputFilename, resFilter,
+ versionCode);
+ }
+ }
+
+ /**
+ * 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 javaProject the java project being compiled
+ * @param libProjects an optional list of library projects (can be null)
+ * @param referencedJavaProjects referenced projects.
+ * @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,
+ final IJavaProject javaProject, List<IProject> libProjects,
+ List<IJavaProject> referencedJavaProjects, 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, javaProject, libProjects,
+ referencedJavaProjects, null /*abiFilter*/,
+ 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 javaProject the java project being compiled
+ * @param libProjects an optional list of library projects (can be null)
+ * @param referencedJavaProjects referenced projects.
+ * @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,
+ final IJavaProject javaProject, List<IProject> libProjects,
+ List<IJavaProject> referencedJavaProjects, String abiFilter, 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);
+
+ // Now we write the standard resources from the project and the referenced projects.
+ writeStandardResources(apkBuilder, javaProject, referencedJavaProjects);
+
+ // Now we write the standard resources from the external jars
+ for (String libraryOsPath : getExternalDependencies(resMarker)) {
+ File libFile = new File(libraryOsPath);
+ if (libFile.isFile()) {
+ JarStatus jarStatus = apkBuilder.addResourcesFromJar(new File(libraryOsPath));
+
+ // 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 = new File(libraryOsPath).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 (libFile.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(libFile);
+ }
+ }
+
+ // 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(), abiFilter);
+ }
+
+ // 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(), abiFilter);
+ }
+ }
+ }
+
+ // seal the APK.
+ apkBuilder.sealApk();
+ } catch (SealedApkException e) {
+ // this won't happen as we control when the apk is sealed.
+ }
+ }
+
+ public String[] getProjectOutputs() throws CoreException {
+ IFolder outputFolder = BaseProjectHelper.getOutputFolder(mProject);
+
+ // get the list of referenced projects output to add
+ List<IProject> javaProjects = ProjectHelper.getReferencedProjects(mProject);
+ List<IJavaProject> referencedJavaProjects = BuildHelper.getJavaProjects(javaProjects);
+
+ // get the project output, and since it's a new list object, just add the outputFolder
+ // of the project directly to it.
+ List<String> projectOutputs = getProjectOutputs(referencedJavaProjects);
+
+ projectOutputs.add(0, outputFolder.getLocation().toOSString());
+
+ return projectOutputs.toArray(new String[projectOutputs.size()]);
+ }
+
+ /**
+ * Returns an array for all the compiled code for the project. This can include the
+ * code compiled by Eclipse for the main project and dependencies (Java only projects), as well
+ * as external jars used by the project or its library.
+ *
+ * This array of paths is compatible with the input for dx and can be passed as is to
+ * {@link #executeDx(IJavaProject, String[], String)}.
+ *
+ * @param resMarker
+ * @return a array (never empty) containing paths to compiled code.
+ * @throws CoreException
+ */
+ public String[] getCompiledCodePaths(boolean includeProjectOutputs, ResourceMarker resMarker)
+ throws CoreException {
+
+ // get the list of libraries to include with the source code
+ String[] libraries = getExternalDependencies(resMarker);
+
+ int startIndex = 0;
+
+ String[] compiledPaths;
+
+ if (includeProjectOutputs) {
+ String[] projectOutputs = getProjectOutputs();
+
+ compiledPaths = new String[libraries.length + projectOutputs.length];
+
+ System.arraycopy(projectOutputs, 0, compiledPaths, 0, projectOutputs.length);
+ startIndex = projectOutputs.length;
+ } else {
+ compiledPaths = new String[libraries.length];
+ }
+
+ System.arraycopy(libraries, 0, compiledPaths, startIndex, libraries.length);
+
+ return compiledPaths;
+ }
+
+ public void runProguard(File proguardConfig, File inputJar, 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());
+
+ command.add("@" + proguardConfig.getAbsolutePath()); //$NON-NLS-1$
+
+ command.add("-injars"); //$NON-NLS-1$
+ StringBuilder sb = new StringBuilder(inputJar.getAbsolutePath());
+ for (String jarFile : jarFiles) {
+ sb.append(File.pathSeparatorChar);
+ sb.append(jarFile);
+ }
+ command.add(sb.toString());
+
+ command.add("-outjars"); //$NON-NLS-1$
+ command.add(obfuscatedJar.getAbsolutePath());
+
+ command.add("-libraryjars"); //$NON-NLS-1$
+ sb = new StringBuilder(target.getPath(IAndroidTarget.ANDROID_JAR));
+ IOptionalLibrary[] libraries = target.getOptionalLibraries();
+ if (libraries != null) {
+ for (IOptionalLibrary lib : libraries) {
+ sb.append(File.pathSeparatorChar);
+ sb.append(lib.getJarPath());
+ }
+ }
+ command.add(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[];
+
+ if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) {
+ // 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.
+
+ commandArray = new String[3];
+
+ // Arg 0 is the proguard.bat path and arg 1 is the user config file
+ 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$
+ } else {
+ // 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().getSdkLocation() + //$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());
+ }
+ }
+
+ // 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);
+ }
+ }
+
+
+ /**
+ * Execute the Dx tool for dalvik code conversion.
+ * @param javaProject The java project
+ * @param inputPath the path to the main input of dex
+ * @param osOutFilePath the path of the dex file to create.
+ *
+ * @throws CoreException
+ * @throws DexException
+ */
+ public void executeDx(IJavaProject javaProject, String[] inputPaths, String osOutFilePath)
+ throws CoreException, DexException {
+
+ // get the dex wrapper
+ Sdk sdk = Sdk.getCurrent();
+ DexWrapper wrapper = sdk.getDexWrapper();
+
+ 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);
+
+ int res = wrapper.run(osOutFilePath,
+ inputPaths,
+ 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);
+ }
+ }
+
+ /**
+ * Executes aapt. If any error happen, files or the project will be marked.
+ * @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.
+ * @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 osManifestPath,
+ List<String> osResPaths, String osAssetsPath, String osOutFilePath,
+ String configFilter, int versionCode) throws AaptExecException, AaptResultException {
+ IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
+
+ // Create the command line.
+ ArrayList<String> commandArray = new ArrayList<String>();
+ commandArray.add(target.getPath(IAndroidTarget.AAPT));
+ commandArray.add("package"); //$NON-NLS-1$
+ commandArray.add("-f");//$NON-NLS-1$
+ if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
+ commandArray.add("-v"); //$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);
+ }
+
+ commandArray.add("-M"); //$NON-NLS-1$
+ commandArray.add(osManifestPath);
+
+ for (String path : osResPaths) {
+ commandArray.add("-S"); //$NON-NLS-1$
+ commandArray.add(path);
+ }
+
+ 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);
+
+ 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());
+ }
+
+ // launch
+ int execError = 1;
+ try {
+ // launch the command line process
+ Process process = Runtime.getRuntime().exec(command);
+
+ // 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 AaptResultException(execError,
+ results.toArray(new String[results.size()]));
+ }
+ } catch (IOException e) {
+ String msg = String.format(Messages.AAPT_Exec_Error, command[0]);
+ throw new AaptExecException(msg, e);
+ } catch (InterruptedException e) {
+ String msg = String.format(Messages.AAPT_Exec_Error, command[0]);
+ throw new AaptExecException(msg, e);
+ }
+ }
+
+ /**
+ * Writes the standard resources of a project and its referenced projects
+ * into a {@link SignedJarBuilder}.
+ * Standard resources are non java/aidl files placed in the java package folders.
+ * @param apkBuilder the {@link ApkBuilder}.
+ * @param javaProject the javaProject object.
+ * @param referencedJavaProjects the java projects that this project references.
+ * @throws ApkCreationException if an error occurred
+ * @throws SealedApkException if the APK is already sealed.
+ * @throws DuplicateFileException if a file conflicts with another already added to the APK
+ * at the same location inside the APK archive.
+ * @throws CoreException
+ */
+ private void writeStandardResources(ApkBuilder apkBuilder, IJavaProject javaProject,
+ List<IJavaProject> referencedJavaProjects)
+ throws DuplicateFileException, ApkCreationException, SealedApkException,
+ CoreException {
+ IWorkspace ws = ResourcesPlugin.getWorkspace();
+ IWorkspaceRoot wsRoot = ws.getRoot();
+
+ // create a list of path already put into the archive, in order to detect conflict
+ ArrayList<String> list = new ArrayList<String>();
+
+ writeStandardProjectResources(apkBuilder, javaProject, wsRoot, list);
+
+ for (IJavaProject referencedJavaProject : referencedJavaProjects) {
+ // only include output from non android referenced project
+ // (This is to handle the case of reference Android projects in the context of
+ // instrumentation projects that need to reference the projects to be tested).
+ if (referencedJavaProject.getProject().hasNature(
+ AndroidConstants.NATURE_DEFAULT) == false) {
+ writeStandardProjectResources(apkBuilder, referencedJavaProject, wsRoot, list);
+ }
+ }
+ }
+
+ /**
+ * Writes the standard resources of a {@link IJavaProject} into a {@link SignedJarBuilder}.
+ * Standard resources are non java/aidl files placed in the java package folders.
+ * @param jarBuilder the {@link ApkBuilder}.
+ * @param javaProject the javaProject object.
+ * @param wsRoot the {@link IWorkspaceRoot}.
+ * @param list a list of files already added to the archive, to detect conflicts.
+ * @throws ApkCreationException if an error occurred
+ * @throws SealedApkException if the APK is already sealed.
+ * @throws DuplicateFileException if a file conflicts with another already added to the APK
+ * at the same location inside the APK archive.
+ * @throws CoreException
+ */
+ private void writeStandardProjectResources(ApkBuilder apkBuilder,
+ IJavaProject javaProject, IWorkspaceRoot wsRoot, ArrayList<String> list)
+ throws DuplicateFileException, ApkCreationException, SealedApkException, CoreException {
+ // get the source pathes
+ List<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject);
+
+ // loop on them and then recursively go through the content looking for matching files.
+ for (IPath sourcePath : sourceFolders) {
+ IResource sourceResource = wsRoot.findMember(sourcePath);
+ if (sourceResource != null && sourceResource.getType() == IResource.FOLDER) {
+ writeFolderResources(apkBuilder, javaProject, (IFolder) sourceResource);
+ }
+ }
+ }
+
+ private void writeFolderResources(ApkBuilder apkBuilder, final IJavaProject javaProject,
+ IFolder root) throws CoreException, ApkCreationException,
+ SealedApkException, DuplicateFileException {
+ final List<IPath> pathsToPackage = new ArrayList<IPath>();
+ root.accept(new IResourceProxyVisitor() {
+ public boolean visit(IResourceProxy proxy) throws CoreException {
+ if (proxy.getType() == IResource.FOLDER) {
+ // If this folder isn't wanted, don't traverse into it.
+ return ApkBuilder.checkFolderForPackaging(proxy.getName());
+ }
+ // If it's not a folder, it must be a file. We won't see any other resource type.
+ if (!ApkBuilder.checkFileForPackaging(proxy.getName())) {
+ return true;
+ }
+ IResource res = proxy.requestResource();
+ if (!javaProject.isOnClasspath(res)) {
+ return true;
+ }
+ // Just record that we need to package this. Packaging here throws
+ // inappropriate checked exceptions.
+ IPath location = res.getLocation();
+ pathsToPackage.add(location);
+ return true;
+ }
+ }, 0);
+ IPath rootLocation = root.getLocation();
+ for (IPath path : pathsToPackage) {
+ IPath archivePath = path.makeRelativeTo(rootLocation);
+ apkBuilder.addFile(path.toFile(), archivePath.toString());
+ }
+ }
+
+ /**
+ * Returns an array of external dependencies used the project. This can be paths to jar files
+ * or to source folders.
+ *
+ * @param resMarker if non null, used to put Resource marker on problem files.
+ * @return an array of OS-specific absolute file paths
+ */
+ private final String[] getExternalDependencies(ResourceMarker resMarker) {
+ // get a java project from it
+ IJavaProject javaProject = JavaCore.create(mProject);
+
+ IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
+
+ ArrayList<String> oslibraryList = new ArrayList<String>();
+ IClasspathEntry[] classpaths = javaProject.readRawClasspath();
+ if (classpaths != null) {
+ for (IClasspathEntry e : classpaths) {
+ if (e.getEntryKind() == IClasspathEntry.CPE_LIBRARY ||
+ e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
+ // if this is a classpath variable reference, we resolve it.
+ if (e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
+ e = JavaCore.getResolvedClasspathEntry(e);
+ }
+
+ // get the IPath
+ IPath path = e.getPath();
+
+ IResource resource = wsRoot.findMember(path);
+ // case of a jar file (which could be relative to the workspace or a full path)
+ if (AndroidConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) {
+ if (resource != null && resource.exists() &&
+ resource.getType() == IResource.FILE) {
+ oslibraryList.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()) {
+ oslibraryList.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) {
+ oslibraryList.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()) {
+ oslibraryList.add(osFullPath);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return oslibraryList.toArray(new String[oslibraryList.size()]);
+ }
+
+ /**
+ * Returns the list of the output folders for the specified {@link IJavaProject} objects, if
+ * they are Android projects.
+ *
+ * @param referencedJavaProjects the java projects.
+ * @return a new list object containing the output folder paths.
+ * @throws CoreException
+ */
+ private List<String> getProjectOutputs(List<IJavaProject> referencedJavaProjects)
+ throws CoreException {
+ ArrayList<String> list = new ArrayList<String>();
+
+ IWorkspace ws = ResourcesPlugin.getWorkspace();
+ IWorkspaceRoot wsRoot = ws.getRoot();
+
+ for (IJavaProject javaProject : referencedJavaProjects) {
+ // only include output from non android referenced project
+ // (This is to handle the case of reference Android projects in the context of
+ // instrumentation projects that need to reference the projects to be tested).
+ if (javaProject.getProject().hasNature(AndroidConstants.NATURE_DEFAULT) == false) {
+ // get the output folder
+ IPath path = null;
+ try {
+ path = javaProject.getOutputLocation();
+ } catch (JavaModelException e) {
+ continue;
+ }
+
+ IResource outputResource = wsRoot.findMember(path);
+ if (outputResource != null && outputResource.getType() == IResource.FOLDER) {
+ String outputOsPath = outputResource.getLocation().toOSString();
+
+ list.add(outputOsPath);
+ }
+ }
+ }
+
+ return list;
+ }
+
+ /**
+ * 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 ouput from
+ * @param results 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> results)
+ throws InterruptedException {
+ // Due to the limited buffer size on windows for the standard io (stderr, stdout), we
+ // *need* to read both stdout and stderr all the time. If we don't and a process output
+ // a large amount, this could deadlock the process.
+
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(process.getErrorStream());
+ BufferedReader errReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = errReader.readLine();
+ if (line != null) {
+ results.add(line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ }.start();
+
+ new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ InputStreamReader is = new InputStreamReader(process.getInputStream());
+ BufferedReader outReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = outReader.readLine();
+ if (line != null) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE,
+ project, line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+
+ }.start();
+
+ // get the return code from the process
+ return process.waitFor();
+ }
+}