blob: 78d9d94e4d5d338edc3fcbec11a5b17681880807 [file] [log] [blame]
/*
* 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);
}
}
}
});
}
}