blob: f8a969e94ffd7841ccdd2736931c4ae87a08d550 [file] [log] [blame]
/*
* Copyright (C) 2007 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.build;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.project.ProjectHelper;
import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
import com.android.ide.eclipse.adt.sdk.Sdk;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import com.android.jarutils.DebugKeyProvider;
import com.android.jarutils.JavaResourceFilter;
import com.android.jarutils.SignedJarBuilder;
import com.android.jarutils.DebugKeyProvider.IKeyGenOutput;
import com.android.jarutils.DebugKeyProvider.KeytoolException;
import com.android.jarutils.SignedJarBuilder.IZipEntryFilter;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
public class ApkBuilder extends BaseBuilder {
public static final String ID = "com.android.ide.eclipse.adt.ApkBuilder"; //$NON-NLS-1$
private static final String PROPERTY_CONVERT_TO_DEX = "convertToDex"; //$NON-NLS-1$
private static final String PROPERTY_PACKAGE_RESOURCES = "packageResources"; //$NON-NLS-1$
private static final String PROPERTY_BUILD_APK = "buildApk"; //$NON-NLS-1$
private static final String DX_PREFIX = "Dx"; //$NON-NLS-1$
/**
* Dex conversion flag. This is set to true if one of the changed/added/removed
* file is a .class file. Upon visiting all the delta resource, if this
* flag is true, then we know we'll have to make the "classes.dex" file.
*/
private boolean mConvertToDex = false;
/**
* Package resources flag. This is set to true if one of the changed/added/removed
* file is a resource file. Upon visiting all the delta resource, if
* this flag is true, then we know we'll have to repackage the resources.
*/
private boolean mPackageResources = false;
/**
* Final package build flag.
*/
private boolean mBuildFinalPackage = false;
private PrintStream mOutStream = null;
private PrintStream mErrStream = null;
/**
* Basic Resource Delta Visitor class to check if a referenced project had a change in its
* compiled java files.
*/
private static class ReferencedProjectDeltaVisitor implements IResourceDeltaVisitor {
private boolean mConvertToDex = false;
private boolean mMakeFinalPackage;
private IPath mOutputFolder;
private ArrayList<IPath> mSourceFolders;
private ReferencedProjectDeltaVisitor(IJavaProject javaProject) {
try {
mOutputFolder = javaProject.getOutputLocation();
mSourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject);
} catch (JavaModelException e) {
} finally {
}
}
/**
* {@inheritDoc}
* @throws CoreException
*/
public boolean visit(IResourceDelta delta) throws CoreException {
// no need to keep looking if we already know we need to convert
// to dex and make the final package.
if (mConvertToDex && mMakeFinalPackage) {
return false;
}
// get the resource and the path segments.
IResource resource = delta.getResource();
IPath resourceFullPath = resource.getFullPath();
if (mOutputFolder.isPrefixOf(resourceFullPath)) {
int type = resource.getType();
if (type == IResource.FILE) {
String ext = resource.getFileExtension();
if (AndroidConstants.EXT_CLASS.equals(ext)) {
mConvertToDex = true;
}
}
return true;
} else {
for (IPath sourceFullPath : mSourceFolders) {
if (sourceFullPath.isPrefixOf(resourceFullPath)) {
int type = resource.getType();
if (type == IResource.FILE) {
// check if the file is a valid file that would be
// included during the final packaging.
if (checkFileForPackaging((IFile)resource)) {
mMakeFinalPackage = true;
}
return false;
} else if (type == IResource.FOLDER) {
// if this is a folder, we check if this is a valid folder as well.
// If this is a folder that needs to be ignored, we must return false,
// so that we ignore its content.
return checkFolderForPackaging((IFolder)resource);
}
}
}
}
return true;
}
/**
* Returns if one of the .class file was modified.
*/
boolean needDexConvertion() {
return mConvertToDex;
}
boolean needMakeFinalPackage() {
return mMakeFinalPackage;
}
}
/**
* {@link IZipEntryFilter} to filter out everything that is not a standard java resources.
* <p/>Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when
* we only want the java resources from external jars.
*/
private final IZipEntryFilter mJavaResourcesFilter = new JavaResourceFilter();
public ApkBuilder() {
super();
}
// build() returns a list of project from which this project depends for future compilation.
@SuppressWarnings("unchecked") //$NON-NLS-1$
@Override
protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
throws CoreException {
// get a project object
IProject project = getProject();
// Top level check to make sure the build can move forward.
abortOnBadSetup(project);
// get the list of referenced projects.
IProject[] referencedProjects = ProjectHelper.getReferencedProjects(project);
IJavaProject[] referencedJavaProjects = getJavaProjects(referencedProjects);
// get the output folder, this method returns the path with a trailing
// separator
IJavaProject javaProject = JavaCore.create(project);
IFolder outputFolder = BaseProjectHelper.getOutputFolder(project);
// now we need to get the classpath list
ArrayList<IPath> sourceList = BaseProjectHelper.getSourceClasspaths(javaProject);
// First thing we do is go through the resource delta to not
// lose it if we have to abort the build for any reason.
ApkDeltaVisitor dv = null;
if (kind == FULL_BUILD) {
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
Messages.Start_Full_Apk_Build);
mPackageResources = true;
mConvertToDex = true;
mBuildFinalPackage = true;
} else {
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
Messages.Start_Inc_Apk_Build);
// go through the resources and see if something changed.
IResourceDelta delta = getDelta(project);
if (delta == null) {
mPackageResources = true;
mConvertToDex = true;
mBuildFinalPackage = true;
} else {
dv = new ApkDeltaVisitor(this, sourceList, outputFolder);
delta.accept(dv);
// save the state
mPackageResources |= dv.getPackageResources();
mConvertToDex |= dv.getConvertToDex();
mBuildFinalPackage |= dv.getMakeFinalPackage();
}
// also go through the delta for all the referenced projects, until we are forced to
// compile anyway
for (int i = 0 ; i < referencedJavaProjects.length &&
(mBuildFinalPackage == false || mConvertToDex == false); i++) {
IJavaProject referencedJavaProject = referencedJavaProjects[i];
delta = getDelta(referencedJavaProject.getProject());
if (delta != null) {
ReferencedProjectDeltaVisitor refProjectDv = new ReferencedProjectDeltaVisitor(
referencedJavaProject);
delta.accept(refProjectDv);
// save the state
mConvertToDex |= refProjectDv.needDexConvertion();
mBuildFinalPackage |= refProjectDv.needMakeFinalPackage();
}
}
}
// store the build status in the persistent storage
saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , mConvertToDex);
saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
if (dv != null && dv.mXmlError) {
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
Messages.Xml_Error);
// if there was some XML errors, we just return w/o doing
// anything since we've put some markers in the files anyway
return referencedProjects;
}
if (outputFolder == null) {
// mark project and exit
markProject(AdtConstants.MARKER_ADT, Messages.Failed_To_Get_Output,
IMarker.SEVERITY_ERROR);
return referencedProjects;
}
// first thing we do is check that the SDK directory has been setup.
String osSdkFolder = AdtPlugin.getOsSdkFolder();
if (osSdkFolder.length() == 0) {
// this has already been checked in the precompiler. Therefore,
// while we do have to cancel the build, we don't have to return
// any error or throw anything.
return referencedProjects;
}
// get the extra configs for the project.
// The map contains (name, filter) where 'name' is a name to be used in the apk filename,
// and filter is the resource filter to be used in the aapt -c parameters to restrict
// which resource configurations to package in the apk.
Map<String, String> configs = Sdk.getCurrent().getProjectApkConfigs(project);
// do some extra check, in case the output files are not present. This
// will force to recreate them.
IResource tmp = null;
if (mPackageResources == false) {
// check the full resource package
tmp = outputFolder.findMember(AndroidConstants.FN_RESOURCES_AP_);
if (tmp == null || tmp.exists() == false) {
mPackageResources = true;
mBuildFinalPackage = true;
} else {
// if the full package is present, we check the filtered resource packages as well
if (configs != null) {
Set<Entry<String, String>> entrySet = configs.entrySet();
for (Entry<String, String> entry : entrySet) {
String filename = String.format(AndroidConstants.FN_RESOURCES_S_AP_,
entry.getKey());
tmp = outputFolder.findMember(filename);
if (tmp == null || (tmp instanceof IFile &&
tmp.exists() == false)) {
String msg = String.format(Messages.s_Missing_Repackaging, filename);
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
mPackageResources = true;
mBuildFinalPackage = true;
break;
}
}
}
}
}
// check classes.dex is present. If not we force to recreate it.
if (mConvertToDex == false) {
tmp = outputFolder.findMember(AndroidConstants.FN_CLASSES_DEX);
if (tmp == null || tmp.exists() == false) {
mConvertToDex = true;
mBuildFinalPackage = true;
}
}
// also check the final file(s)!
String finalPackageName = ProjectHelper.getApkFilename(project, null /*config*/);
if (mBuildFinalPackage == false) {
tmp = outputFolder.findMember(finalPackageName);
if (tmp == null || (tmp instanceof IFile &&
tmp.exists() == false)) {
String msg = String.format(Messages.s_Missing_Repackaging, finalPackageName);
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
mBuildFinalPackage = true;
} else if (configs != null) {
// if the full apk is present, we check the filtered apk as well
Set<Entry<String, String>> entrySet = configs.entrySet();
for (Entry<String, String> entry : entrySet) {
String filename = ProjectHelper.getApkFilename(project, entry.getKey());
tmp = outputFolder.findMember(filename);
if (tmp == null || (tmp instanceof IFile &&
tmp.exists() == false)) {
String msg = String.format(Messages.s_Missing_Repackaging, filename);
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
mBuildFinalPackage = true;
break;
}
}
}
}
// at this point we know if we need to recreate the temporary apk
// or the dex file, but we don't know if we simply need to recreate them
// because they are missing
// refresh the output directory first
IContainer ic = outputFolder.getParent();
if (ic != null) {
ic.refreshLocal(IResource.DEPTH_ONE, monitor);
}
// we need to test all three, as we may need to make the final package
// but not the intermediary ones.
if (mPackageResources || mConvertToDex || mBuildFinalPackage) {
IPath binLocation = outputFolder.getLocation();
if (binLocation == null) {
markProject(AdtConstants.MARKER_ADT, Messages.Output_Missing,
IMarker.SEVERITY_ERROR);
return referencedProjects;
}
String osBinPath = binLocation.toOSString();
// Remove the old .apk.
// This make sure that if the apk is corrupted, then dx (which would attempt
// to open it), will not fail.
String osFinalPackagePath = osBinPath + File.separator + finalPackageName;
File finalPackage = new File(osFinalPackagePath);
// if delete failed, this is not really a problem, as the final package generation
// handle already present .apk, and if that one failed as well, the user will be
// notified.
finalPackage.delete();
if (configs != null) {
Set<Entry<String, String>> entrySet = configs.entrySet();
for (Entry<String, String> entry : entrySet) {
String packageFilepath = osBinPath + File.separator +
ProjectHelper.getApkFilename(project, entry.getKey());
finalPackage = new File(packageFilepath);
finalPackage.delete();
}
}
// first we check if we need to package the resources.
if (mPackageResources) {
// remove some aapt_package only markers.
removeMarkersFromContainer(project, AndroidConstants.MARKER_AAPT_PACKAGE);
// need to figure out some path before we can execute aapt;
// resource to the AndroidManifest.xml file
IResource manifestResource = project .findMember(
AndroidConstants.WS_SEP + AndroidConstants.FN_ANDROID_MANIFEST);
if (manifestResource == null
|| manifestResource.exists() == false) {
// mark project and exit
String msg = String.format(Messages.s_File_Missing,
AndroidConstants.FN_ANDROID_MANIFEST);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
return referencedProjects;
}
// get the resource folder
IFolder resFolder = project.getFolder(
AndroidConstants.WS_RESOURCES);
// and the assets folder
IFolder assetsFolder = project.getFolder(
AndroidConstants.WS_ASSETS);
// we need to make sure this one exists.
if (assetsFolder.exists() == false) {
assetsFolder = null;
}
IPath resLocation = resFolder.getLocation();
IPath manifestLocation = manifestResource.getLocation();
if (resLocation != null && manifestLocation != null) {
String osResPath = resLocation.toOSString();
String osManifestPath = manifestLocation.toOSString();
String osAssetsPath = null;
if (assetsFolder != null) {
osAssetsPath = assetsFolder.getLocation().toOSString();
}
// build the default resource package
if (executeAapt(project, osManifestPath, osResPath,
osAssetsPath, osBinPath + File.separator +
AndroidConstants.FN_RESOURCES_AP_, null /*configFilter*/) == false) {
// aapt failed. Whatever files that needed to be marked
// have already been marked. We just return.
return referencedProjects;
}
// now do the same thing for all the configured resource packages.
if (configs != null) {
Set<Entry<String, String>> entrySet = configs.entrySet();
for (Entry<String, String> entry : entrySet) {
String outPathFormat = osBinPath + File.separator +
AndroidConstants.FN_RESOURCES_S_AP_;
String outPath = String.format(outPathFormat, entry.getKey());
if (executeAapt(project, osManifestPath, osResPath,
osAssetsPath, outPath, entry.getValue()) == false) {
// aapt failed. Whatever files that needed to be marked
// have already been marked. We just return.
return referencedProjects;
}
}
}
// build has been done. reset the state of the builder
mPackageResources = false;
// and store it
saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
}
}
// then we check if we need to package the .class into classes.dex
if (mConvertToDex) {
if (executeDx(javaProject, osBinPath, osBinPath + File.separator +
AndroidConstants.FN_CLASSES_DEX, referencedJavaProjects) == false) {
// dx failed, we return
return referencedProjects;
}
// build has been done. reset the state of the builder
mConvertToDex = false;
// and store it
saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
}
// now we need to make the final package from the intermediary apk
// and classes.dex.
// This is the default package with all the resources.
String classesDexPath = osBinPath + File.separator + AndroidConstants.FN_CLASSES_DEX;
if (finalPackage(osBinPath + File.separator + AndroidConstants.FN_RESOURCES_AP_,
classesDexPath,osFinalPackagePath, javaProject,
referencedJavaProjects) == false) {
return referencedProjects;
}
// now do the same thing for all the configured resource packages.
if (configs != null) {
String resPathFormat = osBinPath + File.separator +
AndroidConstants.FN_RESOURCES_S_AP_;
Set<Entry<String, String>> entrySet = configs.entrySet();
for (Entry<String, String> entry : entrySet) {
// make the filename for the resource package.
String resPath = String.format(resPathFormat, entry.getKey());
// make the filename for the apk to generate
String apkOsFilePath = osBinPath + File.separator +
ProjectHelper.getApkFilename(project, entry.getKey());
if (finalPackage(resPath, classesDexPath, apkOsFilePath, javaProject,
referencedJavaProjects) == false) {
return referencedProjects;
}
}
}
// we are done.
// get the resource to bin
outputFolder.refreshLocal(IResource.DEPTH_ONE, monitor);
// build has been done. reset the state of the builder
mBuildFinalPackage = false;
// and store it
saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
"Build Success!");
}
return referencedProjects;
}
@Override
protected void startupOnInitialize() {
super.startupOnInitialize();
// load the build status. We pass true as the default value to
// force a recompile in case the property was not found
mConvertToDex = loadProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , true);
mPackageResources = loadProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, true);
mBuildFinalPackage = loadProjectBooleanProperty(PROPERTY_BUILD_APK, true);
}
/**
* Executes aapt. If any error happen, files or the project will be marked.
* @param project The Project
* @param osManifestPath The path to the manifest file
* @param osResPath The path to the res folder
* @param osAssetsPath The path to the assets folder. This can be null.
* @param osOutFilePath The path to the temporary resource file to create.
* @param configFilter The configuration filter for the resources to include
* (used with -c option, for example "port,en,fr" to include portrait, English and French
* resources.)
* @return true if success, false otherwise.
*/
private boolean executeAapt(IProject project, String osManifestPath,
String osResPath, String osAssetsPath, String osOutFilePath, String configFilter) {
IAndroidTarget target = Sdk.getCurrent().getTarget(project);
// Create the command line.
ArrayList<String> commandArray = new ArrayList<String>();
commandArray.add(target.getPath(IAndroidTarget.AAPT));
commandArray.add("package"); //$NON-NLS-1$
commandArray.add("-f");//$NON-NLS-1$
if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) {
commandArray.add("-v"); //$NON-NLS-1$
}
if (configFilter != null) {
commandArray.add("-c"); //$NON-NLS-1$
commandArray.add(configFilter);
}
commandArray.add("-M"); //$NON-NLS-1$
commandArray.add(osManifestPath);
commandArray.add("-S"); //$NON-NLS-1$
commandArray.add(osResPath);
if (osAssetsPath != null) {
commandArray.add("-A"); //$NON-NLS-1$
commandArray.add(osAssetsPath);
}
commandArray.add("-I"); //$NON-NLS-1$
commandArray.add(target.getPath(IAndroidTarget.ANDROID_JAR));
commandArray.add("-F"); //$NON-NLS-1$
commandArray.add(osOutFilePath);
String command[] = commandArray.toArray(
new String[commandArray.size()]);
if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) {
StringBuilder sb = new StringBuilder();
for (String c : command) {
sb.append(c);
sb.append(' ');
}
AdtPlugin.printToConsole(project, sb.toString());
}
// launch
int execError = 1;
try {
// launch the command line process
Process process = Runtime.getRuntime().exec(command);
// list to store each line of stderr
ArrayList<String> results = new ArrayList<String>();
// get the output and return code from the process
execError = grabProcessOutput(process, results);
// attempt to parse the error output
boolean parsingError = parseAaptOutput(results, project);
// if we couldn't parse the output we display it in the console.
if (parsingError) {
if (execError != 0) {
AdtPlugin.printErrorToConsole(project, results.toArray());
} else {
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_ALWAYS, project,
results.toArray());
}
}
// We need to abort if the exec failed.
if (execError != 0) {
// if the exec failed, and we couldn't parse the error output (and therefore
// not all files that should have been marked, were marked), we put a generic
// marker on the project and abort.
if (parsingError) {
markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AAPT_Errors,
IMarker.SEVERITY_ERROR);
}
// abort if exec failed.
return false;
}
} catch (IOException e1) {
String msg = String.format(Messages.AAPT_Exec_Error, command[0]);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
return false;
} catch (InterruptedException e) {
String msg = String.format(Messages.AAPT_Exec_Error, command[0]);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
return false;
}
return true;
}
/**
* Execute the Dx tool for dalvik code conversion.
* @param javaProject The java project
* @param osBinPath the path to the output folder of the project
* @param osOutFilePath the path of the dex file to create.
* @param referencedJavaProjects the list of referenced projects for this project.
*
* @throws CoreException
*/
private boolean executeDx(IJavaProject javaProject, String osBinPath, String osOutFilePath,
IJavaProject[] referencedJavaProjects) throws CoreException {
IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject());
AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target);
if (targetData == null) {
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
Messages.ApkBuilder_UnableBuild_Dex_Not_loaded));
}
// get the dex wrapper
DexWrapper wrapper = targetData.getDexWrapper();
if (wrapper == null) {
throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
Messages.ApkBuilder_UnableBuild_Dex_Not_loaded));
}
// make sure dx use the proper output streams.
// first make sure we actually have the streams available.
if (mOutStream == null) {
IProject project = getProject();
mOutStream = AdtPlugin.getOutPrintStream(project, DX_PREFIX);
mErrStream = AdtPlugin.getErrPrintStream(project, DX_PREFIX);
}
try {
// get the list of libraries to include with the source code
String[] libraries = getExternalJars();
// get the list of referenced projects output to add
String[] projectOutputs = getProjectOutputs(referencedJavaProjects);
String[] fileNames = new String[1 + projectOutputs.length + libraries.length];
// first this project output
fileNames[0] = osBinPath;
// then other project output
System.arraycopy(projectOutputs, 0, fileNames, 1, projectOutputs.length);
// then external jars.
System.arraycopy(libraries, 0, fileNames, 1 + projectOutputs.length, libraries.length);
int res = wrapper.run(osOutFilePath, fileNames,
AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE,
mOutStream, mErrStream);
if (res != 0) {
// output error message and marker the project.
String message = String.format(Messages.Dalvik_Error_d,
res);
AdtPlugin.printErrorToConsole(getProject(), message);
markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR);
return false;
}
} catch (Throwable ex) {
String message = ex.getMessage();
if (message == null) {
message = ex.getClass().getCanonicalName();
}
message = String.format(Messages.Dalvik_Error_s, message);
AdtPlugin.printErrorToConsole(getProject(), message);
markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR);
if ((ex instanceof NoClassDefFoundError)
|| (ex instanceof NoSuchMethodError)) {
AdtPlugin.printErrorToConsole(getProject(), Messages.Incompatible_VM_Warning,
Messages.Requires_1_5_Error);
}
return false;
}
return true;
}
/**
* Makes the final package. Package the dex files, the temporary resource file into the final
* package file.
* @param intermediateApk The path to the temporary resource file.
* @param dex The path to the dex file.
* @param output The path to the final package file to create.
* @param javaProject
* @param referencedJavaProjects
* @return true if success, false otherwise.
*/
private boolean finalPackage(String intermediateApk, String dex, String output,
final IJavaProject javaProject, IJavaProject[] referencedJavaProjects) {
FileOutputStream fos = null;
try {
IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
String osKeyPath = store.getString(AdtPlugin.PREFS_CUSTOM_DEBUG_KEYSTORE);
if (osKeyPath == null || new File(osKeyPath).exists() == false) {
osKeyPath = DebugKeyProvider.getDefaultKeyStoreOsPath();
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
Messages.ApkBuilder_Using_Default_Key);
} else {
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
String.format(Messages.ApkBuilder_Using_s_To_Sign, osKeyPath));
}
// TODO: get the store type from somewhere else.
DebugKeyProvider provider = new DebugKeyProvider(osKeyPath, null /* storeType */,
new IKeyGenOutput() {
public void err(String message) {
AdtPlugin.printErrorToConsole(javaProject.getProject(),
Messages.ApkBuilder_Signing_Key_Creation_s + message);
}
public void out(String message) {
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE,
javaProject.getProject(),
Messages.ApkBuilder_Signing_Key_Creation_s + message);
}
});
PrivateKey key = provider.getDebugKey();
X509Certificate certificate = (X509Certificate)provider.getCertificate();
if (key == null) {
String msg = String.format(Messages.Final_Archive_Error_s,
Messages.ApkBuilder_Unable_To_Gey_Key);
AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
return false;
}
// compare the certificate expiration date
if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
// TODO, regenerate a new one.
String msg = String.format(Messages.Final_Archive_Error_s,
String.format(Messages.ApkBuilder_Certificate_Expired_on_s,
DateFormat.getInstance().format(certificate.getNotAfter())));
AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
return false;
}
// create the jar builder.
fos = new FileOutputStream(output);
SignedJarBuilder builder = new SignedJarBuilder(fos, key, certificate);
// add the intermediate file containing the compiled resources.
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
String.format(Messages.ApkBuilder_Packaging_s, intermediateApk));
FileInputStream fis = new FileInputStream(intermediateApk);
try {
builder.writeZip(fis, null /* filter */);
} finally {
fis.close();
}
// Now we add the new file to the zip archive for the classes.dex file.
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
String.format(Messages.ApkBuilder_Packaging_s, AndroidConstants.FN_CLASSES_DEX));
File entryFile = new File(dex);
builder.writeFile(entryFile, AndroidConstants.FN_CLASSES_DEX);
// Now we write the standard resources from the project and the referenced projects.
writeStandardResources(builder, javaProject, referencedJavaProjects);
// Now we write the standard resources from the external libraries
for (String libraryOsPath : getExternalJars()) {
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
String.format(Messages.ApkBuilder_Packaging_s, libraryOsPath));
try {
fis = new FileInputStream(libraryOsPath);
builder.writeZip(fis, mJavaResourcesFilter);
} finally {
fis.close();
}
}
// now write the native libraries.
// First look if the lib folder is there.
IResource libFolder = javaProject.getProject().findMember(SdkConstants.FD_NATIVE_LIBS);
if (libFolder != null && libFolder.exists() &&
libFolder.getType() == IResource.FOLDER) {
// look inside and put .so in lib/* by keeping the relative folder path.
writeNativeLibraries(libFolder.getFullPath().segmentCount(), builder, libFolder);
}
// close the jar file and write the manifest and sign it.
builder.close();
} catch (GeneralSecurityException e1) {
// mark project and return
String msg = String.format(Messages.Final_Archive_Error_s, e1.getMessage());
AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
return false;
} catch (IOException e1) {
// mark project and return
String msg = String.format(Messages.Final_Archive_Error_s, e1.getMessage());
AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
return false;
} catch (KeytoolException e) {
String eMessage = e.getMessage();
// mark the project with the standard message
String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
// output more info in the console
AdtPlugin.printErrorToConsole(javaProject.getProject(),
msg,
String.format(Messages.ApkBuilder_JAVA_HOME_is_s, e.getJavaHome()),
Messages.ApkBuilder_Update_or_Execute_manually_s,
e.getCommandLine());
} catch (AndroidLocationException e) {
String eMessage = e.getMessage();
// mark the project with the standard message
String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
// and also output it in the console
AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
} catch (CoreException e) {
// mark project and return
String msg = String.format(Messages.Final_Archive_Error_s, e.getMessage());
AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
return false;
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
// pass.
}
}
}
return true;
}
/**
* Writes native libraries into a {@link SignedJarBuilder}.
* <p/>This recursively go through folder and writes .so files.
* The path in the archive is based on the root folder containing the libraries in the project.
* Its segment count is passed to the method to compute the resources path relative to the root
* folder.
* Native libraries in the archive must be in a "lib" folder. Everything in the project native
* lib folder directly goes in this "lib" folder in the archive.
*
*
* @param rootSegmentCount The number of segment of the path of the folder containing the
* libraries. This is used to compute the path in the archive.
* @param jarBuilder the {@link SignedJarBuilder} used to create the archive.
* @param resource the IResource to write.
* @throws CoreException
* @throws IOException
*/
private void writeNativeLibraries(int rootSegmentCount, SignedJarBuilder jarBuilder,
IResource resource) throws CoreException, IOException {
if (resource.getType() == IResource.FILE) {
IPath path = resource.getFullPath();
// check the extension.
if (path.getFileExtension().equalsIgnoreCase(AndroidConstants.EXT_NATIVE_LIB)) {
// remove the first segment to build the path inside the archive.
path = path.removeFirstSegments(rootSegmentCount);
// add it to the archive.
IPath apkPath = new Path(SdkConstants.FD_APK_NATIVE_LIBS);
apkPath = apkPath.append(path);
// writes the file in the apk.
jarBuilder.writeFile(resource.getLocation().toFile(), apkPath.toString());
}
} else if (resource.getType() == IResource.FOLDER) {
IResource[] members = ((IFolder)resource).members();
for (IResource member : members) {
writeNativeLibraries(rootSegmentCount, jarBuilder, member);
}
}
}
/**
* Writes the standard resources of a project and its referenced projects
* into a {@link SignedJarBuilder}.
* Standard resources are non java/aidl files placed in the java package folders.
* @param jarBuilder the {@link SignedJarBuilder}.
* @param javaProject the javaProject object.
* @param referencedJavaProjects the java projects that this project references.
* @throws IOException
* @throws CoreException
*/
private void writeStandardResources(SignedJarBuilder jarBuilder, IJavaProject javaProject,
IJavaProject[] referencedJavaProjects) throws IOException, CoreException {
IWorkspace ws = ResourcesPlugin.getWorkspace();
IWorkspaceRoot wsRoot = ws.getRoot();
// create a list of path already put into the archive, in order to detect conflict
ArrayList<String> list = new ArrayList<String>();
writeStandardProjectResources(jarBuilder, javaProject, wsRoot, list);
for (IJavaProject referencedJavaProject : referencedJavaProjects) {
if (referencedJavaProject.getProject().hasNature(AndroidConstants.NATURE)) {
writeStandardProjectResources(jarBuilder, referencedJavaProject, wsRoot, list);
}
}
}
/**
* Writes the standard resources of a {@link IJavaProject} into a {@link SignedJarBuilder}.
* Standard resources are non java/aidl files placed in the java package folders.
* @param jarBuilder the {@link SignedJarBuilder}.
* @param javaProject the javaProject object.
* @param wsRoot the {@link IWorkspaceRoot}.
* @param list a list of files already added to the archive, to detect conflicts.
* @throws IOException
*/
private void writeStandardProjectResources(SignedJarBuilder jarBuilder,
IJavaProject javaProject, IWorkspaceRoot wsRoot, ArrayList<String> list)
throws IOException {
// get the source pathes
ArrayList<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject);
// loop on them and then recursively go through the content looking for matching files.
for (IPath sourcePath : sourceFolders) {
IResource sourceResource = wsRoot.findMember(sourcePath);
if (sourceResource != null && sourceResource.getType() == IResource.FOLDER) {
writeStandardSourceFolderResources(jarBuilder, sourcePath, (IFolder)sourceResource,
list);
}
}
}
/**
* Recursively writes the standard resources of a source folder into a {@link SignedJarBuilder}.
* Standard resources are non java/aidl files placed in the java package folders.
* @param jarBuilder the {@link SignedJarBuilder}.
* @param sourceFolder the {@link IPath} of the source folder.
* @param currentFolder The current folder we're recursively processing.
* @param list a list of files already added to the archive, to detect conflicts.
* @throws IOException
*/
private void writeStandardSourceFolderResources(SignedJarBuilder jarBuilder, IPath sourceFolder,
IFolder currentFolder, ArrayList<String> list) throws IOException {
try {
IResource[] members = currentFolder.members();
for (IResource member : members) {
int type = member.getType();
if (type == IResource.FILE && member.exists()) {
if (checkFileForPackaging((IFile)member)) {
// this files must be added to the archive.
IPath fullPath = member.getFullPath();
// We need to create its path inside the archive.
// This path is relative to the source folder.
IPath relativePath = fullPath.removeFirstSegments(
sourceFolder.segmentCount());
String zipPath = relativePath.toString();
// lets check it's not already in the list of path added to the archive
if (list.indexOf(zipPath) != -1) {
AdtPlugin.printErrorToConsole(getProject(),
String.format(
Messages.ApkBuilder_s_Conflict_with_file_s,
fullPath, zipPath));
} else {
// get the File object
File entryFile = member.getLocation().toFile();
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
String.format(Messages.ApkBuilder_Packaging_s_into_s, fullPath, zipPath));
// write it in the zip archive
jarBuilder.writeFile(entryFile, zipPath);
// and add it to the list of entries
list.add(zipPath);
}
}
} else if (type == IResource.FOLDER) {
if (checkFolderForPackaging((IFolder)member)) {
writeStandardSourceFolderResources(jarBuilder, sourceFolder,
(IFolder)member, list);
}
}
}
} catch (CoreException e) {
// if we can't get the members of the folder, we just don't do anything.
}
}
/**
* Returns the list of the output folders for the specified {@link IJavaProject} objects, if
* they are Android projects.
*
* @param referencedJavaProjects the java projects.
* @return an array, always. Can be empty.
* @throws CoreException
*/
private String[] getProjectOutputs(IJavaProject[] referencedJavaProjects) throws CoreException {
ArrayList<String> list = new ArrayList<String>();
IWorkspace ws = ResourcesPlugin.getWorkspace();
IWorkspaceRoot wsRoot = ws.getRoot();
for (IJavaProject javaProject : referencedJavaProjects) {
if (javaProject.getProject().hasNature(AndroidConstants.NATURE)) {
// get the output folder
IPath path = null;
try {
path = javaProject.getOutputLocation();
} catch (JavaModelException e) {
continue;
}
IResource outputResource = wsRoot.findMember(path);
if (outputResource != null && outputResource.getType() == IResource.FOLDER) {
String outputOsPath = outputResource.getLocation().toOSString();
list.add(outputOsPath);
}
}
}
return list.toArray(new String[list.size()]);
}
/**
* Returns an array of {@link IJavaProject} matching the provided {@link IProject} objects.
* @param projects the IProject objects.
* @return an array, always. Can be empty.
* @throws CoreException
*/
private IJavaProject[] getJavaProjects(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.toArray(new IJavaProject[list.size()]);
}
/**
* 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.
*/
static boolean checkFileForPackaging(IFile file) {
String name = file.getName();
String ext = file.getFileExtension();
return JavaResourceFilter.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.
*/
static boolean checkFolderForPackaging(IFolder folder) {
String name = folder.getName();
return JavaResourceFilter.checkFolderForPackaging(name);
}
}