blob: 421d377fddf5d49f4dcc661756b0b40a5ab43df6 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2002 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v0.5
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v05.html
*
* Contributors:
* IBM - Initial API and implementation
******************************************************************************/
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.*;
import org.eclipse.core.internal.watson.ElementTree;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
public class BuildManager implements ICoreConstants, IManager, ILifecycleListener {
protected Workspace workspace;
protected boolean building = false;
//used for debug/trace timing
private long timeStamp = -1;
//the following four fields only apply for the lifetime of
//a single builder invocation.
protected ElementTree currentTree;
protected ElementTree lastBuiltTree;
protected InternalBuilder currentBuilder;
protected DeltaDataTree currentDelta;
/**
* 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 ElementTree oldTree;
private ElementTree newTree;
private IPath projectPath;
private IResourceDelta delta;
/**
* Returns the cached resource delta for the given project and trees, or
* null if there is no matching delta in the cache.
*/
public IResourceDelta getDelta(IPath project, ElementTree oldTree, ElementTree newTree) {
if (delta == null)
return null;
if (projectPath.equals(project) && this.oldTree == oldTree && this.newTree == newTree)
return delta;
return null;
}
public void cache(IPath project, ElementTree oldTree, ElementTree newTree, IResourceDelta delta) {
this.projectPath = project;
this.oldTree = oldTree;
this.newTree = newTree;
this.delta = delta;
}
public void flush() {
this.projectPath = null;
this.oldTree = null;
this.newTree = null;
this.delta = null;
}
}
final protected DeltaCache deltaCache = new DeltaCache();
/**
* These builders are added to build tables in place of builders that couldn't be instantiated
*/
class MissingBuilder extends IncrementalProjectBuilder {
private String name;
private boolean hasBeenBuilt = false;
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) throws CoreException {
if (!hasBeenBuilt) {
hasBeenBuilt = true;
String msg = Policy.bind("events.skippingBuilder", new String[] {name, getProject().getName()}); //$NON-NLS-1$
IStatus status = new Status(IStatus.WARNING, ResourcesPlugin.PI_RESOURCES, 1, msg, null);
ResourcesPlugin.getPlugin().getLog().log(status);
}
return null;
}
}
public BuildManager(Workspace workspace) {
this.workspace = workspace;
}
protected void basicBuild(int trigger, IncrementalProjectBuilder builder, Map args, MultiStatus status, IProgressMonitor monitor) {
try {
currentBuilder = (InternalBuilder) builder;
//clear any old requests to forget built state
currentBuilder.clearForgetLastBuiltState();
// Figure out which trees are involved based on the trigger and tree availabilty.
lastBuiltTree = currentBuilder.getLastBuiltTree();
boolean fullBuild = (trigger == IncrementalProjectBuilder.FULL_BUILD) || (lastBuiltTree == null);
// Grab a pointer to the current state before computing the delta
currentTree = fullBuild ? null : workspace.getElementTree();
try {
//short-circuit if none of the projects this builder cares about have changed.
if (!fullBuild && !needsBuild(currentBuilder))
return;
String name = currentBuilder.getLabel();
String message;
if (name != null)
message = Policy.bind("events.invoking.2", name, builder.getProject().getFullPath().toString()); //$NON-NLS-1$
else
message = Policy.bind("events.invoking.1", builder.getProject().getFullPath().toString()); //$NON-NLS-1$
monitor.subTask(message);
hookStartBuild(builder);
//do the build
Platform.run(getSafeRunnable(trigger, args, status, monitor));
} finally {
hookEndBuild(builder);
// Be sure to clean up after ourselves.
ElementTree lastTree = workspace.getElementTree();
lastTree.immutable();
if (!currentBuilder.wasForgetStateRequested()) {
// remember the current state as the last built state.
currentBuilder.setLastBuiltTree(lastTree);
}
}
} finally {
currentBuilder = null;
currentTree = null;
lastBuiltTree = null;
currentDelta = null;
}
}
protected void basicBuild(final IProject project, final int trigger, final MultiStatus status, final IProgressMonitor monitor) {
if (!project.isAccessible())
return;
final ICommand[] commands = ((Project) project).internalGetDescription().getBuildSpec(false);
if (commands.length == 0)
return;
ISafeRunnable code = new ISafeRunnable() {
public void run() throws Exception {
basicBuild(project, trigger, commands, status, monitor);
}
public void handleException(Throwable e) {
if (e instanceof OperationCanceledException)
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 message = e.getMessage();
if (message == null)
message = Policy.bind("events.unknown", e.getClass().getName(), currentBuilder.getClass().getName()); //$NON-NLS-1$
status.add(new Status(Status.WARNING, ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, message, e));
}
};
Platform.run(code);
}
protected void basicBuild(IProject project, int trigger, String builderName, Map args, MultiStatus status, IProgressMonitor monitor) {
IncrementalProjectBuilder builder = null;
try {
builder = getBuilder(builderName, project, status);
if (!validateNature(builder, builderName)) {
//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
((InternalBuilder)builder).setLastBuiltTree(null);
return;
}
} catch (CoreException e) {
status.add(e.getStatus());
return;
}
basicBuild(trigger, builder, args, status, monitor);
}
protected void basicBuild(IProject project, int trigger, ICommand[] commands, MultiStatus status, IProgressMonitor monitor) {
monitor = Policy.monitorFor(monitor);
try {
String message = Policy.bind("events.building.1", project.getFullPath().toString()); //$NON-NLS-1$
monitor.beginTask(message, Math.max(1, commands.length));
for (int i = 0; i < commands.length; i++) {
IProgressMonitor sub = Policy.subMonitorFor(monitor, 1);
BuildCommand command = (BuildCommand) commands[i];
basicBuild(project, trigger, command.getBuilderName(), command.getArguments(false), status, sub);
Policy.checkCanceled(monitor);
}
} finally {
monitor.done();
}
}
public void build(int trigger, IProgressMonitor monitor) throws CoreException {
monitor = Policy.monitorFor(monitor);
try {
monitor.beginTask(Policy.bind("events.building.0"), Policy.totalWork); //$NON-NLS-1$
if (!canRun(trigger))
return;
try {
building = true;
IProject[] ordered = workspace.getBuildOrder();
IProject[] unordered = null;
HashSet leftover = new HashSet(5);
leftover.addAll(Arrays.asList(workspace.getRoot().getProjects()));
leftover.removeAll(Arrays.asList(ordered));
unordered = (IProject[]) leftover.toArray(new IProject[leftover.size()]);
int num = ordered.length + unordered.length;
MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.BUILD_FAILED, Policy.bind("events.errors"), null); //$NON-NLS-1$
for (int i = 0; i < ordered.length; i++)
if (ordered[i].isAccessible())
basicBuild(ordered[i], trigger, status, Policy.subMonitorFor(monitor, Policy.totalWork / num));
for (int i = 0; i < unordered.length; i++)
if (unordered[i].isAccessible())
basicBuild(unordered[i], trigger, status, Policy.subMonitorFor(monitor, Policy.totalWork / num));
// if the status is not ok, throw an exception
if (!status.isOK())
throw new ResourceException(status);
} finally {
building = false;
deltaCache.flush();
}
} finally {
monitor.done();
}
}
public void build(IProject project, int trigger, IProgressMonitor monitor) throws CoreException {
if (!canRun(trigger))
return;
try {
building = true;
MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, Policy.bind("events.errors"), null); //$NON-NLS-1$
basicBuild(project, trigger, status, monitor);
if (!status.isOK())
throw new ResourceException(status);
} finally {
building = false;
deltaCache.flush();
}
}
public void build(IProject project, int kind, String builderName, Map args, IProgressMonitor monitor) throws CoreException {
monitor = Policy.monitorFor(monitor);
try {
String message = Policy.bind("events.building.1", project.getFullPath().toString()); //$NON-NLS-1$
monitor.beginTask(message, 1);
if (!canRun(kind))
return;
try {
building = true;
MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, Policy.bind("events.errors"), null); //$NON-NLS-1$
basicBuild(project, kind, builderName, args, status, Policy.subMonitorFor(monitor, 1));
if (!status.isOK())
throw new ResourceException(status);
} finally {
building = false;
}
} finally {
monitor.done();
deltaCache.flush();
}
}
protected boolean canRun(int trigger) {
return !building;
}
/**
* Creates and returns a Map mapping String(builder name) -> BuilderPersistentInfo.
* The table 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 Map createBuildersPersistentInfo(IProject project) throws CoreException {
/* get the old builder map */
Map oldInfos = getBuildersPersistentInfo(project);
ICommand[] buildCommands = ((Project) project).internalGetDescription().getBuildSpec(false);
if (buildCommands.length == 0)
return null;
/* build the new map */
Map newInfos = new HashMap(buildCommands.length * 2);
Hashtable instantiatedBuilders = getBuilders(project);
for (int i = 0; i < buildCommands.length; i++) {
String builderName = buildCommands[i].getBuilderName();
BuilderPersistentInfo info = null;
IncrementalProjectBuilder builder = (IncrementalProjectBuilder) instantiatedBuilders.get(builderName);
if (builder == null) {
// if the builder was not instantiated, use the old info if any.
if (oldInfos != null)
info = (BuilderPersistentInfo) oldInfos.get(builderName);
} 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();
info.setProjectName(project.getName());
info.setBuilderName(builderName);
info.setLastBuildTree(oldTree);
info.setInterestingProjects(((InternalBuilder)builder).getInterestingProjects());
}
}
if (info != null)
newInfos.put(builderName, info);
}
return newInfos;
}
protected String debugBuilder() {
return currentBuilder == null ? "<no builder>" : currentBuilder.getClass().getName(); //$NON-NLS-1$
}
protected String debugProject() {
if (currentBuilder== null)
return "<no project>"; //$NON-NLS-1$
return currentBuilder.getProject().getFullPath().toString();
}
protected IncrementalProjectBuilder getBuilder(String builderName, IProject project, MultiStatus status) throws CoreException {
Hashtable builders = getBuilders(project);
IncrementalProjectBuilder result = (IncrementalProjectBuilder) builders.get(builderName);
if (result != null)
return result;
result = initializeBuilder(builderName, project, status);
builders.put(builderName, result);
((InternalBuilder) result).setProject(project);
((InternalBuilder) result).startupOnInitialize();
return result;
}
/**
* Returns a hashtable of all instantiated builders for the given project.
* This hashtable maps String(builder name) -> Builder.
*/
protected Hashtable getBuilders(IProject project) {
ProjectInfo info = (ProjectInfo) workspace.getResourceInfo(project.getFullPath(), false, false);
Assert.isNotNull(info, Policy.bind("events.noProject", project.getName())); //$NON-NLS-1$
return info.getBuilders();
}
/**
* Returns a Map mapping String(builder name) -> BuilderPersistentInfo.
* The map 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 Map getBuildersPersistentInfo(IProject project) throws CoreException {
return (Map) project.getSessionProperty(K_BUILD_MAP);
}
protected IResourceDelta getDelta(IProject project) {
if (currentTree == null) {
if (Policy.DEBUG_BUILD_FAILURE)
System.out.println("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)
System.out.println("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) {
//just return an empty delta rooted at this project
return ResourceDeltaFactory.newEmptyDelta(project);
}
//now check against the cache
IResourceDelta result = deltaCache.getDelta(project.getFullPath(), lastBuiltTree, currentTree);
if (result != null)
return result;
long startTime = 0L;
if (Policy.DEBUG_BUILD_DELTA) {
startTime = System.currentTimeMillis();
System.out.println("Computing delta for project: " + project.getName()); //$NON-NLS-1$
}
result = ResourceDeltaFactory.computeDelta(workspace, lastBuiltTree, currentTree, project.getFullPath(), false);
deltaCache.cache(project.getFullPath(), lastBuiltTree, currentTree, result);
if (Policy.DEBUG_BUILD_FAILURE && result == null)
System.out.println("Build: no delta " + debugBuilder() + " [" + debugProject() + "] " + project.getFullPath()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
if (Policy.DEBUG_BUILD_DELTA)
System.out.println("Finished computing delta, time: " + (System.currentTimeMillis()-startTime) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
return result;
}
/**
* Returns the safe runnable instance for invoking a builder
*/
protected ISafeRunnable getSafeRunnable(final int trigger, final Map args, final MultiStatus status, final IProgressMonitor monitor) {
return new ISafeRunnable() {
public void run() throws Exception {
IProject[] builders = currentBuilder.build(trigger, args, monitor);
if (builders == null)
builders = new IProject[0];
currentBuilder.setInterestingProjects((IProject[]) builders.clone());
}
public void handleException(Throwable e) {
if (e instanceof OperationCanceledException)
throw (OperationCanceledException) e;
//ResourceStats.buildException(e);
// don't log the exception....it is already being logged in Platform#run
if (e instanceof CoreException)
status.add(((CoreException) e).getStatus());
else {
String pluginId = currentBuilder.getPluginDescriptor().getUniqueIdentifier();
String message = e.getMessage();
if (message == null)
message = Policy.bind("events.unknown", e.getClass().getName(), currentBuilder.getClass().getName()); //$NON-NLS-1$
status.add(new Status(IStatus.WARNING, pluginId, IResourceStatus.BUILD_FAILED, message, e));
}
}
};
}
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);
}
}
/**
* Hook for adding trace options and debug information at the start of a build.
*/
private void hookStartBuild(IncrementalProjectBuilder builder) {
if (Policy.MONITOR_BUILDERS)
EventStats.startBuild(builder);
if (Policy.DEBUG_BUILD_INVOKING) {
timeStamp = System.currentTimeMillis();
System.out.println("Invoking builder: " + toString(builder)); //$NON-NLS-1$
}
}
/**
* Hook for adding trace options and debug information at the end of a build.
*/
private void hookEndBuild(IncrementalProjectBuilder builder) {
if (Policy.MONITOR_BUILDERS)
EventStats.endBuild();
if (!Policy.DEBUG_BUILD_INVOKING || timeStamp == -1)
return; //builder wasn't called or we are not debugging
System.out.println("Builder finished: " + toString(builder) + " time: " + (System.currentTimeMillis() - timeStamp) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
timeStamp = -1;
}
/**
* 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.
*/
protected IncrementalProjectBuilder initializeBuilder(String builderName, IProject project, MultiStatus status) throws CoreException {
IncrementalProjectBuilder builder = null;
try {
builder = instantiateBuilder(builderName);
} catch (CoreException e) {
status.add(new ResourceStatus(IResourceStatus.BUILD_FAILED, project.getFullPath(), Policy.bind("events.instantiate.1", builderName), e)); //$NON-NLS-1$
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
Map infos = getBuildersPersistentInfo(project);
if (infos != null) {
BuilderPersistentInfo info = (BuilderPersistentInfo) infos.remove(builderName);
if (info != null) {
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.
*/
protected IncrementalProjectBuilder instantiateBuilder(String builderName) throws CoreException {
IExtension extension = Platform.getPluginRegistry().getExtension(ResourcesPlugin.PI_RESOURCES, ResourcesPlugin.PT_BUILDERS, builderName);
if (extension == null)
return null;
IConfigurationElement[] configs = extension.getConfigurationElements();
if (configs.length == 0)
return null;
String hasNature = configs[0].getAttribute("hasNature"); //$NON-NLS-1$ FIXME: should this be in a field somewhere?
String natureId = null;
if (hasNature != null && hasNature.equalsIgnoreCase(Boolean.TRUE.toString())) {
//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.setPluginDescriptor(extension.getDeclaringPluginDescriptor());
builder.setLabel(extension.getLabel());
builder.setNatureId(natureId);
return (IncrementalProjectBuilder)builder;
}
/**
* Returns true if the current builder is interested in changes
* to the given project, and false otherwise.
*/
protected 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.
*/
protected boolean needsBuild(InternalBuilder builder) {
//compute the delta since the last built state
ElementTree oldTree = builder.getLastBuiltTree();
ElementTree newTree = workspace.getElementTree();
currentDelta = newTree.getDataTree().forwardDeltaWith(oldTree.getDataTree(), ResourceComparator.getComparator(false));
//search for the builder's project
if (currentDelta.findNodeAt(builder.getProject().getFullPath()) != null) {
if (Policy.DEBUG_NEEDS_BUILD)
System.out.println(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_NEEDS_BUILD)
System.out.println(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
*/
protected 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);
}
/**
* Sets the builder map for the given project. The builder map is
* a Map mapping String(builder name) -> BuilderPersistentInfo.
* The map 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, Map map) {
try {
project.setSessionProperty(K_BUILD_MAP, map);
} catch (CoreException e) {
//project is missing -- build state will be lost
//can't throw an exception because this happens on startup
IStatus error = new ResourceStatus(IStatus.ERROR, 1, project.getFullPath(), "Project missing in setBuildersPersistentInfo", null); //$NON-NLS-1$
ResourcesPlugin.getPlugin().getLog().log(error);
}
}
public void shutdown(IProgressMonitor monitor) {
}
public void startup(IProgressMonitor monitor) {
workspace.addLifecycleListener(this);
}
/**
* Returns a string representation of the given builder.
* For debugging purposes only.
*/
protected 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 satisifed 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.
*/
protected 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);
}
}