/*******************************************************************************
* Copyright (c) 2000, 2010 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Isaac Pacht (isaacp3@gmail.com) - fix for bug 206540
* Anton Leherbauer (Wind River) - [305858] Allow Builder to return null rule
* James Blackburn (Broadcom) - [306822] Provide Context for Builder getRule()
*******************************************************************************/
package org.eclipse.core.internal.events;
import java.util.*;
import org.eclipse.core.internal.dtree.DeltaDataTree;
import org.eclipse.core.internal.resources.*;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.internal.utils.Policy;
import org.eclipse.core.internal.watson.ElementTree;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.*;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.Bundle;
public class BuildManager implements ICoreConstants, IManager, ILifecycleListener {
/**
* Cache used to optimize the common case of an autobuild against a workspace where only a
* single project has changed (and hence only a single delta is interesting).
*/
class DeltaCache {
private Object delta;
private ElementTree newTree;
private ElementTree oldTree;
private IPath projectPath;
public void cache(IPath project, ElementTree anOldTree, ElementTree aNewTree, Object aDelta) {
this.projectPath= project;
this.oldTree= anOldTree;
this.newTree= aNewTree;
this.delta= aDelta;
}
public void flush() {
this.projectPath= null;
this.oldTree= null;
this.newTree= null;
this.delta= null;
}
/**
* Returns the cached resource delta for the given project and trees, or null if there is no
* matching delta in the cache.
*/
public Object getDelta(IPath project, ElementTree anOldTree, ElementTree aNewTree) {
if (delta == null)
return null;
boolean pathsEqual= projectPath == null ? project == null : projectPath.equals(project);
if (pathsEqual && this.oldTree == anOldTree && this.newTree == aNewTree)
return delta;
return null;
}
}
/**
* These builders are added to build tables in place of builders that couldn't be instantiated
*/
class MissingBuilder extends IncrementalProjectBuilder {
private boolean hasBeenBuilt= false;
private String name;
MissingBuilder(String name) {
this.name= name;
}
/**
* Log an exception on the first build, and silently do nothing on subsequent builds.
*/
protected IProject[] build(int kind, Map args, IProgressMonitor monitor) {
if (!hasBeenBuilt && Policy.DEBUG_BUILD_FAILURE) {
hasBeenBuilt= true;
String msg= NLS.bind(Messages.events_skippingBuilder, name, getProject().getName());
Policy.log(IStatus.WARNING, msg, null);
}
return null;
}
}
private static final int TOTAL_BUILD_WORK= Policy.totalWork * 1000;
//the job for performing background autobuild
final AutoBuildJob autoBuildJob;
private boolean building= false;
private final ArrayList builtProjects= new ArrayList();
//the following four fields only apply for the lifetime of a single builder invocation.
protected InternalBuilder currentBuilder;
private DeltaDataTree currentDelta;
private ElementTree currentLastBuiltTree;
private ElementTree currentTree;
/**
* Caches the IResourceDelta for a pair of trees
*/
final private DeltaCache deltaCache= new DeltaCache();
/**
* Caches the DeltaDataTree used to determine if a build is necessary
*/
final private DeltaCache deltaTreeCache= new DeltaCache();
private ILock lock;
//used for the build cycle looping mechanism
private boolean rebuildRequested= false;
private final Bundle systemBundle= Platform.getBundle("org.eclipse.osgi"); //$NON-NLS-1$
//used for debug/trace timing
private long timeStamp= -1;
private Workspace workspace;
public BuildManager(Workspace workspace, ILock workspaceLock) {
this.workspace= workspace;
this.autoBuildJob= new AutoBuildJob(workspace);
this.lock= workspaceLock;
InternalBuilder.buildManager= this;
}
private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map args, MultiStatus status, IProgressMonitor monitor) {
try {
currentBuilder= builder;
//clear any old requests to forget built state
currentBuilder.clearForgetLastBuiltState();
// Figure out want kind of build is needed
boolean clean= trigger == IncrementalProjectBuilder.CLEAN_BUILD;
currentLastBuiltTree= currentBuilder.getLastBuiltTree();
// If no tree is available we have to do a full build
if (!clean && currentLastBuiltTree == null)
trigger= IncrementalProjectBuilder.FULL_BUILD;
//don't build if this builder doesn't respond to the given trigger
if (!builder.getCommand().isBuilding(trigger)) {
if (clean)
currentBuilder.setLastBuiltTree(null);
return;
}
// For incremental builds, grab a pointer to the current state before computing the delta
currentTree= ((trigger == IncrementalProjectBuilder.FULL_BUILD) || clean) ? null : workspace.getElementTree();
int depth= -1;
try {
//short-circuit if none of the projects this builder cares about have changed.
if (!needsBuild(currentBuilder, trigger)) {
//use up the progress allocated for this builder
monitor.beginTask("", 1); //$NON-NLS-1$
monitor.done();
return;
}
String name= currentBuilder.getLabel();
String message;
if (name != null)
message= NLS.bind(Messages.events_invoking_2, name, builder.getProject().getFullPath());
else
message= NLS.bind(Messages.events_invoking_1, builder.getProject().getFullPath());
monitor.subTask(message);
hookStartBuild(builder, trigger);
//release workspace lock while calling builders
depth= getWorkManager().beginUnprotected();
//do the build
SafeRunner.run(getSafeRunnable(trigger, args, status, monitor));
} finally {
if (depth >= 0)
getWorkManager().endUnprotected(depth);
// Be sure to clean up after ourselves.
if (clean || currentBuilder.wasForgetStateRequested()) {
currentBuilder.setLastBuiltTree(null);
} else {
// remember the current state as the last built state.
ElementTree lastTree= workspace.getElementTree();
lastTree.immutable();
currentBuilder.setLastBuiltTree(lastTree);
}
hookEndBuild(builder);
}
} finally {
currentBuilder= null;
currentTree= null;
currentLastBuiltTree= null;
currentDelta= null;
}
}
protected void basicBuild(IProject project, int trigger, ICommand[] commands, MultiStatus status, IProgressMonitor monitor) {
try {
for (int i= 0; i < commands.length; i++) {
checkCanceled(trigger, monitor);
BuildCommand command= (BuildCommand)commands[i];
IProgressMonitor sub= Policy.subMonitorFor(monitor, 1);
IncrementalProjectBuilder builder= getBuilder(project, command, i, status);
if (builder != null)
basicBuild(trigger, builder, command.getArguments(false), status, sub);
}
} catch (CoreException e) {
status.add(e.getStatus());
}
}
/**
* Runs all builders on the given project.
*
* @return A status indicating if the build succeeded or failed
*/
private IStatus basicBuild(IProject project, int trigger, IProgressMonitor monitor) {
if (!canRun(trigger))
return Status.OK_STATUS;
try {
hookStartBuild(trigger);
MultiStatus status= new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, Messages.events_errors, null);
basicBuild(project, trigger, status, monitor);
return status;
} finally {
hookEndBuild(trigger);
}
}
private void basicBuild(final IProject project, final int trigger, final MultiStatus status, final IProgressMonitor monitor) {
try {
final ICommand[] commands;
if (project.isAccessible())
commands= ((Project)project).internalGetDescription().getBuildSpec(false);
else
commands= null;
int work= commands == null ? 0 : commands.length;
monitor.beginTask(NLS.bind(Messages.events_building_1, project.getFullPath()), work);
if (work == 0)
return;
ISafeRunnable code= new ISafeRunnable() {
public void handleException(Throwable e) {
if (e instanceof OperationCanceledException) {
if (Policy.DEBUG_BUILD_INVOKING)
Policy.debug("Build canceled"); //$NON-NLS-1$
throw (OperationCanceledException)e;
}
// don't log the exception....it is already being logged in Workspace#run
// should never get here because the lower-level build code wrappers
// builder exceptions in core exceptions if required.
String errorText= e.getMessage();
if (errorText == null)
errorText= NLS.bind(Messages.events_unknown, e.getClass().getName(), project.getName());
status.add(new Status(IStatus.WARNING, ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, errorText, e));
}
public void run() throws Exception {
basicBuild(project, trigger, commands, status, monitor);
}
};
SafeRunner.run(code);
} finally {
monitor.done();
}
}
/**
* Runs the builder with the given name on the given project.
*
* @return A status indicating if the build succeeded or failed
*/
private IStatus basicBuild(IProject project, int trigger, String builderName, Map args, IProgressMonitor monitor) {
monitor= Policy.monitorFor(monitor);
try {
String message= NLS.bind(Messages.events_building_1, project.getFullPath());
monitor.beginTask(message, 1);
if (!canRun(trigger))
return Status.OK_STATUS;
try {
hookStartBuild(trigger);
MultiStatus status= new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, Messages.events_errors, null);
ICommand command= getCommand(project, builderName, args);
try {
IncrementalProjectBuilder builder= getBuilder(project, command, -1, status);
if (builder != null)
basicBuild(trigger, builder, args, status, Policy.subMonitorFor(monitor, 1));
} catch (CoreException e) {
status.add(e.getStatus());
}
return status;
} finally {
hookEndBuild(trigger);
}
} finally {
monitor.done();
}
}
/**
* Loop the workspace build until no more builders request a rebuild.
*/
private void basicBuildLoop(IProject[] ordered, IProject[] unordered, int trigger, MultiStatus status, IProgressMonitor monitor) {
int projectWork= ordered.length;
if (projectWork > 0)
projectWork= TOTAL_BUILD_WORK / projectWork;
int maxIterations= workspace.getDescription().getMaxBuildIterations();
if (maxIterations <= 0)
maxIterations= 1;
rebuildRequested= true;
for (int iter= 0; rebuildRequested && iter < maxIterations; iter++) {
rebuildRequested= false;
builtProjects.clear();
for (int i= 0; i < ordered.length; i++) {
if (ordered[i].isAccessible()) {
basicBuild(ordered[i], trigger, status, Policy.subMonitorFor(monitor, projectWork));
builtProjects.add(ordered[i]);
}
}
for (int i= 0; i < unordered.length; i++) {
if (unordered[i].isAccessible()) {
basicBuild(unordered[i], trigger, status, Policy.subMonitorFor(monitor, projectWork));
builtProjects.add(unordered[i]);
}
}
//subsequent builds should always be incremental
trigger= IncrementalProjectBuilder.INCREMENTAL_BUILD;
}
}
/**
* Runs all builders on all projects.
*
* @return A status indicating if the build succeeded or failed
*/
public IStatus build(int trigger, IProgressMonitor monitor) {
monitor= Policy.monitorFor(monitor);
try {
monitor.beginTask(Messages.events_building_0, TOTAL_BUILD_WORK);
if (!canRun(trigger))
return Status.OK_STATUS;
try {
hookStartBuild(trigger);
IProject[] ordered= workspace.getBuildOrder();
HashSet leftover= new HashSet(Arrays.asList(workspace.getRoot().getProjects(IContainer.INCLUDE_HIDDEN)));
leftover.removeAll(Arrays.asList(ordered));
IProject[] unordered= (IProject[])leftover.toArray(new IProject[leftover.size()]);
MultiStatus status= new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.BUILD_FAILED, Messages.events_errors, null);
basicBuildLoop(ordered, unordered, trigger, status, monitor);
return status;
} finally {
hookEndBuild(trigger);
}
} finally {
monitor.done();
if (trigger == IncrementalProjectBuilder.INCREMENTAL_BUILD || trigger == IncrementalProjectBuilder.FULL_BUILD)
autoBuildJob.avoidBuild();
}
}
/**
* Runs the builder with the given name on the given project.
*
* @return A status indicating if the build succeeded or failed
*/
public IStatus build(IProject project, int trigger, String builderName, Map args, IProgressMonitor monitor) {
monitor= Policy.monitorFor(monitor);
if (builderName == null)
return basicBuild(project, trigger, monitor);
return basicBuild(project, trigger, builderName, args, monitor);
}
private boolean canRun(int trigger) {
return !building;
}
/**
* Cancel the build if the user has canceled or if an auto-build has been interrupted.
*/
private void checkCanceled(int trigger, IProgressMonitor monitor) {
//if the system is shutting down, don't build
if (systemBundle.getState() == Bundle.STOPPING)
throw new OperationCanceledException();
Policy.checkCanceled(monitor);
//check for auto-cancel only if we are auto-building
if (trigger != IncrementalProjectBuilder.AUTO_BUILD)
return;
//check for request to interrupt the auto-build
if (autoBuildJob.isInterrupted())
throw new OperationCanceledException();
}
/**
* Creates and returns an ArrayList of BuilderPersistentInfo. The list includes entries for all
* builders that are in the builder spec, and that have a last built state, even if they have
* not been instantiated this session.
*/
public ArrayList createBuildersPersistentInfo(IProject project) throws CoreException {
/* get the old builders (those not yet instantiated) */
ArrayList oldInfos= getBuildersPersistentInfo(project);
ICommand[] commands= ((Project)project).internalGetDescription().getBuildSpec(false);
if (commands.length == 0)
return null;
/* build the new list */
ArrayList newInfos= new ArrayList(commands.length);
for (int i= 0; i < commands.length; i++) {
String builderName= commands[i].getBuilderName();
BuilderPersistentInfo info= null;
IncrementalProjectBuilder builder= ((BuildCommand)commands[i]).getBuilder();
if (builder == null) {
// if the builder was not instantiated, use the old info if any.
if (oldInfos != null)
info= getBuilderInfo(oldInfos, builderName, i);
} else if (!(builder instanceof MissingBuilder)) {
ElementTree oldTree= ((InternalBuilder)builder).getLastBuiltTree();
//don't persist build state for builders that have no last built state
if (oldTree != null) {
// if the builder was instantiated, construct a memento with the important info
info= new BuilderPersistentInfo(project.getName(), builderName, i);
info.setLastBuildTree(oldTree);
info.setInterestingProjects(((InternalBuilder)builder).getInterestingProjects());
}
}
if (info != null)
newInfos.add(info);
}
return newInfos;
}
private String debugBuilder() {
return currentBuilder == null ? "<no builder>" : currentBuilder.getClass().getName(); //$NON-NLS-1$
}
private String debugProject() {
if (currentBuilder == null)
return "<no project>"; //$NON-NLS-1$
return currentBuilder.getProject().getFullPath().toString();
}
/**
* Returns a string representation of a build trigger for debugging purposes.
*
* @param trigger The trigger to compute a representation of
* @return A string describing the trigger.
*/
private String debugTrigger(int trigger) {
switch (trigger) {
case IncrementalProjectBuilder.FULL_BUILD:
return "FULL_BUILD"; //$NON-NLS-1$
case IncrementalProjectBuilder.CLEAN_BUILD:
return "CLEAN_BUILD"; //$NON-NLS-1$
case IncrementalProjectBuilder.AUTO_BUILD:
case IncrementalProjectBuilder.INCREMENTAL_BUILD:
default:
return "INCREMENTAL_BUILD"; //$NON-NLS-1$
}
}
/**
* The outermost workspace operation has finished. Do an autobuild if necessary.
*/
public void endTopLevel(boolean needsBuild) {
autoBuildJob.build(needsBuild);
}
/**
* Returns the value of the boolean configuration element attribute with the given name, or
* <code>false</code> if the attribute is missing.
*/
private boolean getBooleanAttribute(IConfigurationElement element, String name) {
String valueString= element.getAttribute(name);
return valueString != null && valueString.equalsIgnoreCase(Boolean.TRUE.toString());
}
/**
* Returns the builder instance corresponding to the given command, or <code>null</code> if the
* builder was not valid.
*
* @param project The project this builder corresponds to
* @param command The build command
* @param buildSpecIndex The index of this builder in the build spec, or -1 if the index is
* unknown
* @param status MultiStatus for collecting errors
*/
private IncrementalProjectBuilder getBuilder(IProject project, ICommand command, int buildSpecIndex, MultiStatus status) throws CoreException {
InternalBuilder result= ((BuildCommand)command).getBuilder();
if (result == null) {
result= initializeBuilder(command.getBuilderName(), project, buildSpecIndex, status);
((BuildCommand)command).setBuilder((IncrementalProjectBuilder)result);
result.setCommand(command);
result.setProject(project);
result.startupOnInitialize();
}
if (!validateNature(result, command.getBuilderName())) {
//skip this builder and null its last built tree because it is invalid
//if the nature gets added or re-enabled a full build will be triggered
result.setLastBuiltTree(null);
return null;
}
return (IncrementalProjectBuilder)result;
}
/**
* Removes the builder persistent info from the map corresponding to the given builder name and
* build spec index, or <code>null</code> if not found
*
* @param buildSpecIndex The index in the build spec, or -1 if unknown
*/
private BuilderPersistentInfo getBuilderInfo(ArrayList infos, String builderName, int buildSpecIndex) {
//try to match on builder index, but if not match is found, use the name alone
//this is because older workspace versions did not store builder infos in build spec order
BuilderPersistentInfo nameMatch= null;
for (Iterator it= infos.iterator(); it.hasNext();) {
BuilderPersistentInfo info= (BuilderPersistentInfo)it.next();
//match on name and build spec index if known
if (info.getBuilderName().equals(builderName)) {
//we have found a match on name alone
if (nameMatch == null)
nameMatch= info;
//see if the index matches
if (buildSpecIndex == -1 || info.getBuildSpecIndex() == -1 || buildSpecIndex == info.getBuildSpecIndex())
return info;
}
}
//no exact index match, so return name match, if any
return nameMatch;
}
/**
* Returns a list of BuilderPersistentInfo. The list includes entries for all builders that are
* in the builder spec, and that have a last built state but have not been instantiated this
* session.
*/
public ArrayList getBuildersPersistentInfo(IProject project) throws CoreException {
return (ArrayList)project.getSessionProperty(K_BUILD_LIST);
}
/**
* Returns a build command for the given builder name and project. First looks for matching
* command in the project's build spec. If none is found, a new command is created and returned.
* This is necessary because IProject.build allows a builder to be executed that is not in the
* build spec.
*/
private ICommand getCommand(IProject project, String builderName, Map args) {
ICommand[] buildSpec= ((Project)project).internalGetDescription().getBuildSpec(false);
for (int i= 0; i < buildSpec.length; i++)
if (buildSpec[i].getBuilderName().equals(builderName))
return buildSpec[i];
//none found, so create a new command
BuildCommand result= new BuildCommand();
result.setBuilderName(builderName);
result.setArguments(args);
return result;
}
IResourceDelta getDelta(IProject project) {
try {
lock.acquire();
if (currentTree == null) {
if (Policy.DEBUG_BUILD_FAILURE)
Policy.debug("Build: no tree for delta " + debugBuilder() + " [" + debugProject() + "]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return null;
}
//check if this builder has indicated it cares about this project
if (!isInterestingProject(project)) {
if (Policy.DEBUG_BUILD_FAILURE)
Policy.debug("Build: project not interesting for this builder " + debugBuilder() + " [" + debugProject() + "] " + project.getFullPath()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return null;
}
//check if this project has changed
if (currentDelta != null && currentDelta.findNodeAt(project.getFullPath()) == null) {
//if the project never existed (not in delta and not in current tree), return null
if (!project.exists())
return null;
//just return an empty delta rooted at this project
return ResourceDeltaFactory.newEmptyDelta(project);
}
//now check against the cache
IResourceDelta result= (IResourceDelta)deltaCache.getDelta(project.getFullPath(), currentLastBuiltTree, currentTree);
if (result != null)
return result;
long startTime= 0L;
if (Policy.DEBUG_BUILD_DELTA) {
startTime= System.currentTimeMillis();
Policy.debug("Computing delta for project: " + project.getName()); //$NON-NLS-1$
}
result= ResourceDeltaFactory.computeDelta(workspace, currentLastBuiltTree, currentTree, project.getFullPath(), -1);
deltaCache.cache(project.getFullPath(), currentLastBuiltTree, currentTree, result);
if (Policy.DEBUG_BUILD_FAILURE && result == null)
Policy.debug("Build: no delta " + debugBuilder() + " [" + debugProject() + "] " + project.getFullPath()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
if (Policy.DEBUG_BUILD_DELTA)
Policy.debug("Finished computing delta, time: " + (System.currentTimeMillis() - startTime) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
return result;
} finally {
lock.release();
}
}
/**
* Returns the safe runnable instance for invoking a builder
*/
private ISafeRunnable getSafeRunnable(final int trigger, final Map args, final MultiStatus status, final IProgressMonitor monitor) {
return new ISafeRunnable() {
public void handleException(Throwable e) {
if (e instanceof OperationCanceledException) {
if (Policy.DEBUG_BUILD_INVOKING)
Policy.debug("Build canceled"); //$NON-NLS-1$
//just discard built state when a builder cancels, to ensure
//that it is called again on the very next build.
currentBuilder.forgetLastBuiltState();
throw (OperationCanceledException)e;
}
//ResourceStats.buildException(e);
// don't log the exception....it is already being logged in SafeRunner#run
//add a generic message to the MultiStatus
String builderName= currentBuilder.getLabel();
if (builderName == null || builderName.length() == 0)
builderName= currentBuilder.getClass().getName();
String pluginId= currentBuilder.getPluginId();
String message= NLS.bind(Messages.events_builderError, builderName, currentBuilder.getProject().getName());
status.add(new Status(IStatus.ERROR, pluginId, IResourceStatus.BUILD_FAILED, message, e));
//add the exception status to the MultiStatus
if (e instanceof CoreException)
status.add(((CoreException)e).getStatus());
}
public void run() throws Exception {
IProject[] prereqs= null;
//invoke the appropriate build method depending on the trigger
if (trigger != IncrementalProjectBuilder.CLEAN_BUILD)
prereqs= currentBuilder.build(trigger, args, monitor);
else
currentBuilder.clean(monitor);
if (prereqs == null)
prereqs= new IProject[0];
currentBuilder.setInterestingProjects((IProject[])prereqs.clone());
}
};
}
/**
* We know the work manager is always available in the middle of a build.
*/
private WorkManager getWorkManager() {
try {
return workspace.getWorkManager();
} catch (CoreException e) {
//cannot happen
}
//avoid compile error
return null;
}
public void handleEvent(LifecycleEvent event) {
IProject project= null;
switch (event.kind) {
case LifecycleEvent.PRE_PROJECT_DELETE:
case LifecycleEvent.PRE_PROJECT_MOVE:
project= (IProject)event.resource;
//make sure the builder persistent info is deleted for the project move case
if (project.isAccessible())
setBuildersPersistentInfo(project, null);
}
}
/**
* Returns true if the given project has been built during this build cycle, and false
* otherwise.
*/
boolean hasBeenBuilt(IProject project) {
return builtProjects.contains(project);
}
/**
* Hook for adding trace options and debug information at the end of a build. This hook is
* called after each builder instance is called.
*/
private void hookEndBuild(IncrementalProjectBuilder builder) {
if (ResourceStats.TRACE_BUILDERS)
ResourceStats.endBuild();
if (!Policy.DEBUG_BUILD_INVOKING || timeStamp == -1)
return; //builder wasn't called or we are not debugging
Policy.debug("Builder finished: " + toString(builder) + " time: " + (System.currentTimeMillis() - timeStamp) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
timeStamp= -1;
}
/**
* Hook for adding trace options and debug information at the end of a build. This hook is
* called at the end of a build cycle invoked by calling a build API method.
*/
private void hookEndBuild(int trigger) {
building= false;
builtProjects.clear();
deltaCache.flush();
deltaTreeCache.flush();
//ensure autobuild runs after a clean
if (trigger == IncrementalProjectBuilder.CLEAN_BUILD)
autoBuildJob.forceBuild();
}
/**
* Hook for adding trace options and debug information at the start of a build. This hook is
* called before each builder instance is called.
*/
private void hookStartBuild(IncrementalProjectBuilder builder, int trigger) {
if (ResourceStats.TRACE_BUILDERS)
ResourceStats.startBuild(builder);
if (Policy.DEBUG_BUILD_INVOKING) {
timeStamp= System.currentTimeMillis();
Policy.debug("Invoking (" + debugTrigger(trigger) + ") on builder: " + toString(builder)); //$NON-NLS-1$ //$NON-NLS-2$
}
}
/**
* Hook for adding trace options and debug information at the start of a build. This hook is
* called when a build API method is called, before any builders start running.
*/
private void hookStartBuild(int trigger) {
building= true;
if (Policy.DEBUG_BUILD_STACK) {
IStatus info= new Status(IStatus.INFO, ResourcesPlugin.PI_RESOURCES, 1, "Starting build: " + debugTrigger(trigger), new RuntimeException().fillInStackTrace()); //$NON-NLS-1$
Policy.log(info);
}
}
/**
* Instantiates the builder with the given name. If the builder, its plugin, or its nature is
* missing, create a placeholder builder to takes its place. This is needed to generate
* appropriate exceptions when somebody tries to invoke the builder, and to prevent trying to
* instantiate it every time a build is run. This method NEVER returns null.
*/
private IncrementalProjectBuilder initializeBuilder(String builderName, IProject project, int buildSpecIndex, MultiStatus status) throws CoreException {
IncrementalProjectBuilder builder= null;
try {
builder= instantiateBuilder(builderName);
} catch (CoreException e) {
status.add(new ResourceStatus(IResourceStatus.BUILD_FAILED, project.getFullPath(), NLS.bind(Messages.events_instantiate_1, builderName), e));
status.add(e.getStatus());
}
if (builder == null) {
//unable to create the builder, so create a placeholder to fill in for it
builder= new MissingBuilder(builderName);
}
// get the map of builders to get the last built tree
ArrayList infos= getBuildersPersistentInfo(project);
if (infos != null) {
BuilderPersistentInfo info= getBuilderInfo(infos, builderName, buildSpecIndex);
if (info != null) {
infos.remove(info);
ElementTree tree= info.getLastBuiltTree();
if (tree != null)
((InternalBuilder)builder).setLastBuiltTree(tree);
((InternalBuilder)builder).setInterestingProjects(info.getInterestingProjects());
}
// delete the build map if it's now empty
if (infos.size() == 0)
setBuildersPersistentInfo(project, null);
}
return builder;
}
/**
* Instantiates and returns the builder with the given name. If the builder, its plugin, or its
* nature is missing, returns null.
*/
private IncrementalProjectBuilder instantiateBuilder(String builderName) throws CoreException {
IExtension extension= Platform.getExtensionRegistry().getExtension(ResourcesPlugin.PI_RESOURCES, ResourcesPlugin.PT_BUILDERS, builderName);
if (extension == null)
return null;
IConfigurationElement[] configs= extension.getConfigurationElements();
if (configs.length == 0)
return null;
String natureId= null;
if (getBooleanAttribute(configs[0], "hasNature")) { //$NON-NLS-1$
//find the nature that owns this builder
String builderId= extension.getUniqueIdentifier();
natureId= workspace.getNatureManager().findNatureForBuilder(builderId);
if (natureId == null)
return null;
}
//The nature exists, or this builder doesn't specify a nature
InternalBuilder builder= (InternalBuilder)configs[0].createExecutableExtension("run"); //$NON-NLS-1$
builder.setPluginId(extension.getContributor().getName());
builder.setLabel(extension.getLabel());
builder.setNatureId(natureId);
builder.setCallOnEmptyDelta(getBooleanAttribute(configs[0], "callOnEmptyDelta")); //$NON-NLS-1$
return (IncrementalProjectBuilder)builder;
}
/**
* Another thread is attempting to modify the workspace. Cancel the autobuild and wait until it
* completes.
*/
public void interrupt() {
autoBuildJob.interrupt();
}
/**
* Returns whether an autobuild is pending (requested but not yet completed).
*/
public boolean isAutobuildBuildPending() {
return autoBuildJob.getState() != Job.NONE;
}
/**
* Returns true if the current builder is interested in changes to the given project, and false
* otherwise.
*/
private boolean isInterestingProject(IProject project) {
if (project.equals(currentBuilder.getProject()))
return true;
IProject[] interestingProjects= currentBuilder.getInterestingProjects();
for (int i= 0; i < interestingProjects.length; i++) {
if (interestingProjects[i].equals(project)) {
return true;
}
}
return false;
}
/**
* Returns true if the given builder needs to be invoked, and false otherwise.
*
* The algorithm is to compute the intersection of the set of projects that have changed since
* the last build, and the set of projects this builder cares about. This is an optimization,
* under the assumption that computing the forward delta once (not the resource delta) is more
* efficient than computing project deltas and invoking builders for projects that haven't
* changed.
*/
private boolean needsBuild(InternalBuilder builder, int trigger) {
//on some triggers we build regardless of the delta
switch (trigger) {
case IncrementalProjectBuilder.CLEAN_BUILD:
return true;
case IncrementalProjectBuilder.FULL_BUILD:
return true;
case IncrementalProjectBuilder.INCREMENTAL_BUILD:
if (currentBuilder.callOnEmptyDelta())
return true;
//fall through and check if there is a delta
}
//compute the delta since the last built state
ElementTree oldTree= builder.getLastBuiltTree();
ElementTree newTree= workspace.getElementTree();
long start= System.currentTimeMillis();
currentDelta= (DeltaDataTree)deltaTreeCache.getDelta(null, oldTree, newTree);
if (currentDelta == null) {
if (Policy.DEBUG_BUILD_NEEDED) {
String message= "Checking if need to build. Starting delta computation between: " + oldTree.toString() + " and " + newTree.toString(); //$NON-NLS-1$ //$NON-NLS-2$
Policy.debug(message);
}
currentDelta= newTree.getDataTree().forwardDeltaWith(oldTree.getDataTree(), ResourceComparator.getBuildComparator());
if (Policy.DEBUG_BUILD_NEEDED)
Policy.debug("End delta computation. (" + (System.currentTimeMillis() - start) + "ms)."); //$NON-NLS-1$ //$NON-NLS-2$
deltaTreeCache.cache(null, oldTree, newTree, currentDelta);
}
//search for the builder's project
if (currentDelta.findNodeAt(builder.getProject().getFullPath()) != null) {
if (Policy.DEBUG_BUILD_NEEDED)
Policy.debug(toString(builder) + " needs building because of changes in: " + builder.getProject().getName()); //$NON-NLS-1$
return true;
}
//search for builder's interesting projects
IProject[] projects= builder.getInterestingProjects();
for (int i= 0; i < projects.length; i++) {
if (currentDelta.findNodeAt(projects[i].getFullPath()) != null) {
if (Policy.DEBUG_BUILD_NEEDED)
Policy.debug(toString(builder) + " needs building because of changes in: " + projects[i].getName()); //$NON-NLS-1$
return true;
}
}
return false;
}
/**
* Removes all builders with the given ID from the build spec. Does nothing if there were no
* such builders in the spec
*/
private void removeBuilders(IProject project, String builderId) throws CoreException {
IProjectDescription desc= project.getDescription();
ICommand[] oldSpec= desc.getBuildSpec();
int oldLength= oldSpec.length;
if (oldLength == 0)
return;
int remaining= 0;
//null out all commands that match the builder to remove
for (int i= 0; i < oldSpec.length; i++) {
if (oldSpec[i].getBuilderName().equals(builderId))
oldSpec[i]= null;
else
remaining++;
}
//check if any were actually removed
if (remaining == oldSpec.length)
return;
ICommand[] newSpec= new ICommand[remaining];
for (int i= 0, newIndex= 0; i < oldLength; i++) {
if (oldSpec[i] != null)
newSpec[newIndex++]= oldSpec[i];
}
desc.setBuildSpec(newSpec);
project.setDescription(desc, IResource.NONE, null);
}
/**
* Hook for builders to request a rebuild.
*/
void requestRebuild() {
rebuildRequested= true;
}
/**
* Sets the builder infos for the given project. The builder infos are an ArrayList of
* BuilderPersistentInfo. The list includes entries for all builders that are in the builder
* spec, and that have a last built state, even if they have not been instantiated this session.
*/
public void setBuildersPersistentInfo(IProject project, ArrayList list) {
try {
project.setSessionProperty(K_BUILD_LIST, list);
} catch (CoreException e) {
//project is missing -- build state will be lost
//can't throw an exception because this happens on startup
Policy.log(new ResourceStatus(IStatus.ERROR, 1, project.getFullPath(), "Project missing in setBuildersPersistentInfo", null)); //$NON-NLS-1$
}
}
public void shutdown(IProgressMonitor monitor) {
autoBuildJob.cancel();
}
public void startup(IProgressMonitor monitor) {
workspace.addLifecycleListener(this);
}
/**
* Returns a string representation of the given builder. For debugging purposes only.
*/
private String toString(InternalBuilder builder) {
String name= builder.getClass().getName();
name= name.substring(name.lastIndexOf('.') + 1);
return name + "(" + builder.getProject().getName() + ")"; //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Returns true if the nature membership rules are satisfied for the given builder extension on
* the given project, and false otherwise. A builder that does not specify that it belongs to a
* nature is always valid. A builder extension that belongs to a nature can be invalid for the
* following reasons:
* <ul>
* <li>The nature that owns the builder does not exist on the given project</li>
* <li>The nature that owns the builder is disabled on the given project</li>
* </ul>
* Furthermore, if the nature that owns the builder does not exist on the project, that builder
* will be removed from the build spec.
*
* Note: This method only validates nature constraints that can vary at runtime. Additional
* checks are done in the instantiateBuilder method for constraints that cannot vary once the
* plugin registry is initialized.
*/
private boolean validateNature(InternalBuilder builder, String builderId) throws CoreException {
String nature= builder.getNatureId();
if (nature == null)
return true;
IProject project= builder.getProject();
if (!project.hasNature(nature)) {
//remove this builder from the build spec
removeBuilders(project, builderId);
return false;
}
return project.isNatureEnabled(nature);
}
/**
* Returns the scheduling rule that is required for building the project.
*/
public ISchedulingRule getRule(IProject project, int trigger, String builderName, Map args) {
MultiStatus status= new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, Messages.events_errors, null);
if (builderName == null) {
final ICommand[] commands;
if (project.isAccessible()) {
Set rules= new HashSet();
commands= ((Project)project).internalGetDescription().getBuildSpec(false);
boolean hasNullBuildRule= false;
for (int i= 0; i < commands.length; i++) {
BuildCommand command= (BuildCommand)commands[i];
try {
IncrementalProjectBuilder builder= getBuilder(project, command, i, status);
if (builder != null) {
ISchedulingRule builderRule= builder.getRule(trigger, args);
if (builderRule != null)
rules.add(builderRule);
else
hasNullBuildRule= true;
}
} catch (CoreException e) {
status.add(e.getStatus());
}
}
if (rules.isEmpty())
return null;
// Bug 306824 - Builders returning a null rule can't work safely if other builders require a non-null rule
// Be pessimistic and fall back to the default build rule (workspace root) in this case.
if (!hasNullBuildRule)
return new MultiRule((ISchedulingRule[])rules.toArray(new ISchedulingRule[rules.size()]));
}
} else {
// Returns the derived resources for the specified builderName
ICommand command= getCommand(project, builderName, args);
try {
IncrementalProjectBuilder builder= getBuilder(project, command, -1, status);
if (builder != null)
return builder.getRule(trigger, args);
} catch (CoreException e) {
status.add(e.getStatus());
}
}
// Log any errors
if (!status.isOK())
Policy.log(status);
return workspace.getRoot();
}
}