/* * 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.Sdk; import com.android.prefs.AndroidLocation.AndroidLocationException; 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.SealedApkException; import com.android.sdklib.internal.build.DebugKeyProvider; import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; import com.android.sdklib.util.GrabProcessOutput; import com.android.sdklib.util.GrabProcessOutput.IProcessOutput; import com.android.sdklib.util.GrabProcessOutput.Wait; 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$ private final IProject mProject; private final AndroidPrintStream mOutStream; private final AndroidPrintStream mErrStream; private final boolean mForceJumbo; 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 IProject project, @NonNull AndroidPrintStream outStream, @NonNull AndroidPrintStream errStream, boolean forceJumbo, boolean debugMode, boolean verbose, ResourceMarker resMarker) throws CoreException { mProject = project; mOutStream = outStream; mErrStream = errStream; mDebugMode = debugMode; mVerbose = verbose; mForceJumbo = forceJumbo; 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 cacheFolder = mProject.getFolder(AdtConstants.WS_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; // get the cache folder IFolder cacheFolder = mProject.getFolder(AdtConstants.WS_CRUNCHCACHE); // 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); // regular res folder next. osResPaths.add(resLocation.toOSString()); // then libraries if (libProjects != null) { for (IProject lib : libProjects) { // png cache folder first IFolder libCacheFolder = lib.getFolder(AdtConstants.WS_CRUNCHCACHE); addFolderToList(osResPaths, libCacheFolder); // 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()); } // 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().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()); } } 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(); 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 (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()); 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); @SuppressWarnings("deprecation") String aapt = target.getPath(IAndroidTarget.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); } 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 it's the LIBRARIES 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_LIBRARIES))) { 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); } } } }); } }