blob: 56e0c0938dcb4b14c4b36a736157d5a2622520e1 [file] [log] [blame]
/*
* Copyright (C) 2008 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.project;
import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_SDK;
import com.android.SdkConstants;
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.build.BuildHelper;
import com.android.ide.eclipse.adt.internal.build.DexException;
import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException;
import com.android.ide.eclipse.adt.internal.build.ProguardExecException;
import com.android.ide.eclipse.adt.internal.build.ProguardResultException;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.adt.io.IFileWrapper;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.build.ApkCreationException;
import com.android.sdklib.build.DuplicateFileException;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.xml.AndroidManifest;
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.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Shell;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
/**
* Export helper to export release version of APKs.
*/
public final class ExportHelper {
private static final String HOME_PROPERTY = "user.home"; //$NON-NLS-1$
private static final String HOME_PROPERTY_REF = "${" + HOME_PROPERTY + '}'; //$NON-NLS-1$
private static final String SDK_PROPERTY_REF = "${" + PROPERTY_SDK + '}'; //$NON-NLS-1$
private final static String TEMP_PREFIX = "android_"; //$NON-NLS-1$
/**
* Exports a release version of the application created by the given project.
* @param project the project to export
* @param outputFile the file to write
* @param key the key to used for signing. Can be null.
* @param certificate the certificate used for signing. Can be null.
* @param monitor progress monitor
* @throws CoreException if an error occurs
*/
public static void exportReleaseApk(IProject project, File outputFile, PrivateKey key,
X509Certificate certificate, IProgressMonitor monitor) throws CoreException {
// the export, takes the output of the precompiler & Java builders so it's
// important to call build in case the auto-build option of the workspace is disabled.
// Also enable dependency building to make sure everything is up to date.
// However do not package the APK since we're going to do it manually here, using a
// different output location.
ProjectHelper.compileInReleaseMode(project, monitor);
// if either key or certificate is null, ensure the other is null.
if (key == null) {
certificate = null;
} else if (certificate == null) {
key = null;
}
try {
// check if the manifest declares debuggable as true. While this is a release build,
// debuggable in the manifest will override this and generate a debug build
IResource manifestResource = project.findMember(SdkConstants.FN_ANDROID_MANIFEST_XML);
if (manifestResource.getType() != IResource.FILE) {
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
String.format("%1$s missing.", SdkConstants.FN_ANDROID_MANIFEST_XML)));
}
IFileWrapper manifestFile = new IFileWrapper((IFile) manifestResource);
boolean debugMode = AndroidManifest.getDebuggable(manifestFile);
AndroidPrintStream fakeStream = new AndroidPrintStream(null, null, new OutputStream() {
@Override
public void write(int b) throws IOException {
// do nothing
}
});
ProjectState projectState = Sdk.getProjectState(project);
// get the jumbo mode option
String forceJumboStr = projectState.getProperty(AdtConstants.DEX_OPTIONS_FORCEJUMBO);
Boolean jumbo = Boolean.valueOf(forceJumboStr);
String dexMergerStr = projectState.getProperty(AdtConstants.DEX_OPTIONS_DISABLE_MERGER);
Boolean dexMerger = Boolean.valueOf(dexMergerStr);
BuildToolInfo buildToolInfo = getBuildTools(projectState);
BuildHelper helper = new BuildHelper(
projectState,
buildToolInfo,
fakeStream, fakeStream,
jumbo.booleanValue(),
dexMerger.booleanValue(),
debugMode, false /*verbose*/,
null /*resourceMarker*/);
// get the list of library projects
List<IProject> libProjects = projectState.getFullLibraryProjects();
// Step 1. Package the resources.
// tmp file for the packaged resource file. To not disturb the incremental builders
// output, all intermediary files are created in tmp files.
File resourceFile = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_RES);
resourceFile.deleteOnExit();
// Make sure the PNG crunch cache is up to date
helper.updateCrunchCache();
// get the merged manifest
IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(project);
IFile mergedManifestFile = androidOutputFolder.getFile(
SdkConstants.FN_ANDROID_MANIFEST_XML);
// package the resources.
helper.packageResources(
mergedManifestFile,
libProjects,
null, // res filter
0, // versionCode
resourceFile.getParent(),
resourceFile.getName());
// Step 2. Convert the byte code to Dalvik bytecode
// tmp file for the packaged resource file.
File dexFile = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_DEX);
dexFile.deleteOnExit();
ProjectState state = Sdk.getProjectState(project);
String proguardConfig = state.getProperties().getProperty(
ProjectProperties.PROPERTY_PROGUARD_CONFIG);
boolean runProguard = false;
List<File> proguardConfigFiles = null;
if (proguardConfig != null && proguardConfig.length() > 0) {
// Be tolerant with respect to file and path separators just like
// Ant is. Allow "/" in the property file to mean whatever the file
// separator character is:
if (File.separatorChar != '/' && proguardConfig.indexOf('/') != -1) {
proguardConfig = proguardConfig.replace('/', File.separatorChar);
}
Iterable<String> paths = LintUtils.splitPath(proguardConfig);
for (String path : paths) {
if (path.startsWith(SDK_PROPERTY_REF)) {
path = AdtPrefs.getPrefs().getOsSdkFolder() +
path.substring(SDK_PROPERTY_REF.length());
} else if (path.startsWith(HOME_PROPERTY_REF)) {
path = System.getProperty(HOME_PROPERTY) +
path.substring(HOME_PROPERTY_REF.length());
}
File proguardConfigFile = new File(path);
if (proguardConfigFile.isAbsolute() == false) {
proguardConfigFile = new File(project.getLocation().toFile(), path);
}
if (proguardConfigFile.isFile()) {
if (proguardConfigFiles == null) {
proguardConfigFiles = new ArrayList<File>();
}
proguardConfigFiles.add(proguardConfigFile);
runProguard = true;
} else {
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
"Invalid proguard configuration file path " + proguardConfigFile
+ " does not exist or is not a regular file", null));
}
}
// get the proguard file output by aapt
if (proguardConfigFiles != null) {
IFile proguardFile = androidOutputFolder.getFile(AdtConstants.FN_AAPT_PROGUARD);
proguardConfigFiles.add(proguardFile.getLocation().toFile());
}
}
Collection<String> dxInput;
if (runProguard) {
// get all the compiled code paths. This will contain both project output
// folder and jar files.
Collection<String> paths = helper.getCompiledCodePaths();
// create a jar file containing all the project output (as proguard cannot
// process folders of .class files).
File inputJar = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_JAR);
inputJar.deleteOnExit();
JarOutputStream jos = new JarOutputStream(new FileOutputStream(inputJar));
// a list of the other paths (jar files.)
List<String> jars = new ArrayList<String>();
for (String path : paths) {
File root = new File(path);
if (root.isDirectory()) {
addFileToJar(jos, root, root);
} else if (root.isFile()) {
jars.add(path);
}
}
jos.close();
// destination file for proguard
File obfuscatedJar = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_JAR);
obfuscatedJar.deleteOnExit();
// run proguard
helper.runProguard(proguardConfigFiles, inputJar, jars, obfuscatedJar,
new File(project.getLocation().toFile(), SdkConstants.FD_PROGUARD));
helper.setProguardOutput(obfuscatedJar.getAbsolutePath());
// dx input is proguard's output
dxInput = Collections.singletonList(obfuscatedJar.getAbsolutePath());
} else {
// no proguard, simply get all the compiled code path: project output(s) +
// jar file(s)
dxInput = helper.getCompiledCodePaths();
}
IJavaProject javaProject = JavaCore.create(project);
helper.executeDx(javaProject, dxInput, dexFile.getAbsolutePath());
// Step 3. Final package
helper.finalPackage(
resourceFile.getAbsolutePath(),
dexFile.getAbsolutePath(),
outputFile.getAbsolutePath(),
libProjects,
key,
certificate,
null); //resourceMarker
// success!
} catch (CoreException e) {
throw e;
} catch (ProguardResultException e) {
String msg = String.format("Proguard returned with error code %d. See console",
e.getErrorCode());
AdtPlugin.printErrorToConsole(project, msg);
AdtPlugin.printErrorToConsole(project, (Object[]) e.getOutput());
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
msg, e));
} catch (ProguardExecException e) {
String msg = String.format("Failed to run proguard: %s", e.getMessage());
AdtPlugin.printErrorToConsole(project, msg);
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
msg, e));
} catch (DuplicateFileException e) {
String msg = String.format(
"Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s",
e.getArchivePath(), e.getFile1(), e.getFile2());
AdtPlugin.printErrorToConsole(project, msg);
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
e.getMessage(), e));
} catch (NativeLibInJarException e) {
String msg = e.getMessage();
AdtPlugin.printErrorToConsole(project, msg);
AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo());
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
e.getMessage(), e));
} catch (DexException e) {
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
e.getMessage(), e));
} catch (ApkCreationException e) {
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
e.getMessage(), e));
} catch (Exception e) {
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
"Failed to export application", e));
} finally {
// move back to a debug build.
// By using a normal build, we'll simply rebuild the debug version, and let the
// builder decide whether to build the full package or not.
ProjectHelper.buildWithDeps(project, IncrementalProjectBuilder.FULL_BUILD, monitor);
project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
}
}
public static BuildToolInfo getBuildTools(ProjectState projectState)
throws CoreException {
BuildToolInfo buildToolInfo = projectState.getBuildToolInfo();
if (buildToolInfo == null) {
buildToolInfo = Sdk.getCurrent().getLatestBuildTool();
}
if (buildToolInfo == null) {
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
"No Build Tools installed in the SDK."));
}
return buildToolInfo;
}
/**
* Exports an unsigned release APK after prompting the user for a location.
*
* <strong>Must be called from the UI thread.</strong>
*
* @param project the project to export
*/
public static void exportUnsignedReleaseApk(final IProject project) {
Shell shell = Display.getCurrent().getActiveShell();
// create a default file name for the apk.
String fileName = project.getName() + SdkConstants.DOT_ANDROID_PACKAGE;
// Pop up the file save window to get the file location
FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
fileDialog.setText("Export Project");
fileDialog.setFileName(fileName);
final String saveLocation = fileDialog.open();
if (saveLocation != null) {
new Job("Android Release Export") {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
exportReleaseApk(project,
new File(saveLocation),
null, //key
null, //certificate
monitor);
// this is unsigned export. Let's tell the developers to run zip align
AdtPlugin.displayWarning("Android IDE Plug-in", String.format(
"An unsigned package of the application was saved at\n%1$s\n\n" +
"Before publishing the application you will need to:\n" +
"- Sign the application with your release key,\n" +
"- run zipalign on the signed package. ZipAlign is located in <SDK>/tools/\n\n" +
"Aligning applications allows Android to use application resources\n" +
"more efficiently.", saveLocation));
return Status.OK_STATUS;
} catch (CoreException e) {
AdtPlugin.displayError("Android IDE Plug-in", String.format(
"Error exporting application:\n\n%1$s", e.getMessage()));
return e.getStatus();
}
}
}.schedule();
}
}
/**
* Adds a file to a jar file.
* The <var>rootDirectory</var> dictates the path of the file inside the jar file. It must be
* a parent of <var>file</var>.
* @param jar the jar to add the file to
* @param file the file to add
* @param rootDirectory the rootDirectory.
* @throws IOException
*/
private static void addFileToJar(JarOutputStream jar, File file, File rootDirectory)
throws IOException {
if (file.isDirectory()) {
if (file.getName().equals("META-INF") == false) {
for (File child: file.listFiles()) {
addFileToJar(jar, child, rootDirectory);
}
}
} else if (file.isFile()) {
String rootPath = rootDirectory.getAbsolutePath();
String path = file.getAbsolutePath();
path = path.substring(rootPath.length()).replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
if (path.charAt(0) == '/') {
path = path.substring(1);
}
JarEntry entry = new JarEntry(path);
entry.setTime(file.lastModified());
jar.putNextEntry(entry);
// put the content of the file.
byte[] buffer = new byte[1024];
int count;
BufferedInputStream bis = null;
try {
bis = new BufferedInputStream(new FileInputStream(file));
while ((count = bis.read(buffer)) != -1) {
jar.write(buffer, 0, count);
}
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException ignore) {
}
}
}
jar.closeEntry();
}
}
}