/*
* 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.internal.build.builders;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AndroidConstants;
import com.android.ide.eclipse.adt.AndroidPrintStream;
import com.android.ide.eclipse.adt.internal.build.AaptExecException;
import com.android.ide.eclipse.adt.internal.build.AaptParser;
import com.android.ide.eclipse.adt.internal.build.AaptResultException;
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.Messages;
import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException;
import com.android.ide.eclipse.adt.internal.build.BuildHelper.ResourceMarker;
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.ApkInstallManager;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
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.SdkConstants;
import com.android.sdklib.build.ApkCreationException;
import com.android.sdklib.build.DuplicateFileException;
import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
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.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jdt.core.IJavaModelMarker;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class PostCompilerBuilder extends BaseBuilder {
/** This ID is used in plugin.xml and in each project's .project file.
* It cannot be changed even if the class is renamed/moved */
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$
/**
* 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 AndroidPrintStream mOutStream = null;
private AndroidPrintStream 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 List<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 (BuildHelper.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 BuildHelper.checkFolderForPackaging((IFolder)resource);
}
}
}
}
return true;
}
/**
* Returns if one of the .class file was modified.
*/
boolean needDexConvertion() {
return mConvertToDex;
}
boolean needMakeFinalPackage() {
return mMakeFinalPackage;
}
}
private ResourceMarker mResourceMarker = new ResourceMarker() {
public void setWarning(IResource resource, String message) {
BaseProjectHelper.markResource(resource, AndroidConstants.MARKER_PACKAGING,
message, IMarker.SEVERITY_WARNING);
}
};
public PostCompilerBuilder() {
super();
}
@Override
protected void clean(IProgressMonitor monitor) throws CoreException {
super.clean(monitor);
// Get the project.
IProject project = getProject();
// Clear the project of the generic markers
removeMarkersFromContainer(project, AndroidConstants.MARKER_AAPT_PACKAGE);
removeMarkersFromContainer(project, AndroidConstants.MARKER_PACKAGING);
}
// build() returns a list of project from which this project depends for future compilation.
@SuppressWarnings({"unchecked"})
@Override
protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
throws CoreException {
// get a project object
IProject project = getProject();
// list of referenced projects. This is a mix of java projects and library projects
// and is computed below.
IProject[] allRefProjects = null;
try {
// get the project info
ProjectState projectState = Sdk.getProjectState(project);
if (projectState == null || projectState.isLibrary()) {
// library project do not need to be dexified or packaged.
return null;
}
// get the libraries
List<IProject> libProjects = projectState.getFullLibraryProjects();
IJavaProject javaProject = JavaCore.create(project);
// get the list of referenced projects.
List<IProject> javaProjects = ProjectHelper.getReferencedProjects(project);
List<IJavaProject> referencedJavaProjects = BuildHelper.getJavaProjects(
javaProjects);
// mix the java project and the library projects
final int size = libProjects.size() + javaProjects.size();
ArrayList<IProject> refList = new ArrayList<IProject>(size);
refList.addAll(libProjects);
refList.addAll(javaProjects);
allRefProjects = refList.toArray(new IProject[size]);
// Top level check to make sure the build can move forward.
abortOnBadSetup(javaProject);
// get the output folder, this method returns the path with a trailing
// separator
IFolder outputFolder = BaseProjectHelper.getOutputFolder(project);
// now we need to get the classpath list
List<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.
PostCompilerDeltaVisitor dv = null;
if (kind == FULL_BUILD) {
AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
Messages.Start_Full_Apk_Build);
mPackageResources = true;
mConvertToDex = true;
mBuildFinalPackage = true;
} else {
AdtPlugin.printBuildToConsole(BuildVerbosity.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 PostCompilerDeltaVisitor(this, sourceList, outputFolder);
delta.accept(dv);
// save the state
mPackageResources |= dv.getPackageResources();
mConvertToDex |= dv.getConvertToDex();
mBuildFinalPackage |= dv.getMakeFinalPackage();
}
// if the main resources didn't change, then we check for the library
// ones (will trigger resource repackaging too)
if ((mPackageResources == false || mBuildFinalPackage == false) &&
libProjects.size() > 0) {
for (IProject libProject : libProjects) {
delta = getDelta(libProject);
if (delta != null) {
LibraryDeltaVisitor visitor = new LibraryDeltaVisitor();
delta.accept(visitor);
mPackageResources |= visitor.getResChange();
mBuildFinalPackage |= visitor.getLibChange();
if (mPackageResources && mBuildFinalPackage) {
break;
}
}
}
}
// also go through the delta for all the referenced projects, until we are forced to
// compile anyway
final int referencedCount = referencedJavaProjects.size();
for (int i = 0 ; i < referencedCount &&
(mBuildFinalPackage == false || mConvertToDex == false); i++) {
IJavaProject referencedJavaProject = referencedJavaProjects.get(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(BuildVerbosity.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 allRefProjects;
}
// remove older packaging markers.
removeMarkersFromContainer(javaProject.getProject(), AndroidConstants.MARKER_PACKAGING);
if (outputFolder == null) {
// mark project and exit
markProject(AndroidConstants.MARKER_PACKAGING, Messages.Failed_To_Get_Output,
IMarker.SEVERITY_ERROR);
return allRefProjects;
}
// 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 allRefProjects;
}
// 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;
}
}
// check classes.dex is present. If not we force to recreate it.
if (mConvertToDex == false) {
tmp = outputFolder.findMember(SdkConstants.FN_APK_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(BuildVerbosity.VERBOSE, project, msg);
mBuildFinalPackage = true;
}
}
// 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);
}
// Get the DX output stream. Since the builder is created for the life of the
// project, they can be kept around.
if (mOutStream == null) {
mOutStream = new AndroidPrintStream(project, null /*prefix*/,
AdtPlugin.getOutStream());
mErrStream = new AndroidPrintStream(project, null /*prefix*/,
AdtPlugin.getOutStream());
}
// we need to test all three, as we may need to make the final package
// but not the intermediary ones.
if (mPackageResources || mConvertToDex || mBuildFinalPackage) {
BuildHelper helper = new BuildHelper(project,
mOutStream, mErrStream,
true /*debugMode*/,
AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE);
// resource to the AndroidManifest.xml file
IFile manifestFile = project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML);
if (manifestFile == null || manifestFile.exists() == false) {
// mark project and exit
String msg = String.format(Messages.s_File_Missing,
SdkConstants.FN_ANDROID_MANIFEST_XML);
markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR);
return allRefProjects;
}
IPath binLocation = outputFolder.getLocation();
if (binLocation == null) {
markProject(AndroidConstants.MARKER_PACKAGING, Messages.Output_Missing,
IMarker.SEVERITY_ERROR);
return allRefProjects;
}
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();
// first we check if we need to package the resources.
if (mPackageResources) {
// remove some aapt_package only markers.
removeMarkersFromContainer(project, AndroidConstants.MARKER_AAPT_PACKAGE);
try {
helper.packageResources(manifestFile, libProjects, null /*resfilter*/,
0 /*versionCode */, osBinPath,
AndroidConstants.FN_RESOURCES_AP_);
} catch (AaptExecException e) {
BaseProjectHelper.markResource(project, AndroidConstants.MARKER_PACKAGING,
e.getMessage(), IMarker.SEVERITY_ERROR);
return allRefProjects;
} catch (AaptResultException e) {
// attempt to parse the error output
String[] aaptOutput = e.getOutput();
boolean parsingError = AaptParser.parseOutput(aaptOutput, project);
// if we couldn't parse the output we display it in the console.
if (parsingError) {
AdtPlugin.printErrorToConsole(project, (Object[]) aaptOutput);
// 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.
BaseProjectHelper.markResource(project,
AndroidConstants.MARKER_PACKAGING,
Messages.Unparsed_AAPT_Errors,
IMarker.SEVERITY_ERROR);
}
}
// 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) {
try {
String[] dxInputPaths = helper.getCompiledCodePaths(
true /*includeProjectOutputs*/, mResourceMarker);
helper.executeDx(javaProject, dxInputPaths, osBinPath + File.separator +
SdkConstants.FN_APK_CLASSES_DEX);
} catch (DexException e) {
String message = e.getMessage();
AdtPlugin.printErrorToConsole(project, message);
BaseProjectHelper.markResource(project, AndroidConstants.MARKER_PACKAGING,
message, IMarker.SEVERITY_ERROR);
Throwable cause = e.getCause();
if (cause instanceof NoClassDefFoundError
|| cause instanceof NoSuchMethodError) {
AdtPlugin.printErrorToConsole(project, Messages.Incompatible_VM_Warning,
Messages.Requires_1_5_Error);
}
// dx failed, we return
return allRefProjects;
}
// 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 +
SdkConstants.FN_APK_CLASSES_DEX;
try {
helper.finalDebugPackage(
osBinPath + File.separator + AndroidConstants.FN_RESOURCES_AP_,
classesDexPath, osFinalPackagePath,
javaProject, libProjects, referencedJavaProjects, mResourceMarker);
} catch (KeytoolException e) {
String eMessage = e.getMessage();
// mark the project with the standard message
String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
BaseProjectHelper.markResource(project, AndroidConstants.MARKER_PACKAGING, msg,
IMarker.SEVERITY_ERROR);
// output more info in the console
AdtPlugin.printErrorToConsole(project,
msg,
String.format(Messages.ApkBuilder_JAVA_HOME_is_s, e.getJavaHome()),
Messages.ApkBuilder_Update_or_Execute_manually_s,
e.getCommandLine());
return allRefProjects;
} catch (ApkCreationException e) {
String eMessage = e.getMessage();
// mark the project with the standard message
String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
BaseProjectHelper.markResource(project, AndroidConstants.MARKER_PACKAGING, msg,
IMarker.SEVERITY_ERROR);
} catch (AndroidLocationException e) {
String eMessage = e.getMessage();
// mark the project with the standard message
String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
BaseProjectHelper.markResource(project, AndroidConstants.MARKER_PACKAGING, msg,
IMarker.SEVERITY_ERROR);
} catch (NativeLibInJarException e) {
String msg = e.getMessage();
BaseProjectHelper.markResource(project, AndroidConstants.MARKER_PACKAGING,
msg, IMarker.SEVERITY_ERROR);
AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo());
} catch (CoreException e) {
// mark project and return
String msg = String.format(Messages.Final_Archive_Error_s, e.getMessage());
AdtPlugin.printErrorToConsole(project, msg);
BaseProjectHelper.markResource(project, AndroidConstants.MARKER_PACKAGING, msg,
IMarker.SEVERITY_ERROR);
} catch (DuplicateFileException e) {
String msg1 = String.format(
"Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s",
e.getArchivePath(), e.getFile1(), e.getFile2());
String msg2 = String.format(Messages.Final_Archive_Error_s, msg1);
AdtPlugin.printErrorToConsole(project, msg2);
BaseProjectHelper.markResource(project, AndroidConstants.MARKER_PACKAGING, msg2,
IMarker.SEVERITY_ERROR);
}
// 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);
// reset the installation manager to force new installs of this project
ApkInstallManager.getInstance().resetInstallationFor(project);
AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(),
"Build Success!");
}
} catch (AbortBuildException e) {
return allRefProjects;
} catch (Exception exception) {
// try to catch other exception to actually display an error. This will be useful
// if we get an NPE or something so that we can at least notify the user that something
// went wrong.
// first check if this is a CoreException we threw to cancel the build.
if (exception instanceof CoreException) {
if (((CoreException)exception).getStatus().getSeverity() == IStatus.CANCEL) {
// Project is already marked with an error. Nothing to do
return allRefProjects;
}
}
String msg = exception.getMessage();
if (msg == null) {
msg = exception.getClass().getCanonicalName();
}
msg = String.format("Unknown error: %1$s", msg);
AdtPlugin.logAndPrintError(exception, project.getName(), msg);
markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR);
}
return allRefProjects;
}
@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);
}
@Override
protected void abortOnBadSetup(IJavaProject javaProject) throws AbortBuildException {
super.abortOnBadSetup(javaProject);
IProject iProject = getProject();
// do a (hopefully quick) search for Precompiler type markers. Those are always only
// errors.
stopOnMarker(iProject, AndroidConstants.MARKER_AAPT_COMPILE, IResource.DEPTH_INFINITE,
false /*checkSeverity*/);
stopOnMarker(iProject, AndroidConstants.MARKER_AIDL, IResource.DEPTH_INFINITE,
false /*checkSeverity*/);
stopOnMarker(iProject, AndroidConstants.MARKER_RENDERSCRIPT, IResource.DEPTH_INFINITE,
false /*checkSeverity*/);
stopOnMarker(iProject, AndroidConstants.MARKER_ANDROID, IResource.DEPTH_ZERO,
false /*checkSeverity*/);
// do a search for JDT markers. Those can be errors or warnings
stopOnMarker(iProject, IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER,
IResource.DEPTH_INFINITE, true /*checkSeverity*/);
stopOnMarker(iProject, IJavaModelMarker.BUILDPATH_PROBLEM_MARKER,
IResource.DEPTH_INFINITE, true /*checkSeverity*/);
}
}