diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java | 1225 |
1 files changed, 1225 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java new file mode 100644 index 000000000..78d9d94e4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java @@ -0,0 +1,1225 @@ +/* + * 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); + } + } + } + }); + } +} |