| /******************************************************************************* |
| * Copyright (c) 2000, 2021 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * 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() |
| * Broadcom Corporation - ongoing development |
| * Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427 |
| * Torbjörn Svensson (STMicroelectronics) - bug #552606 |
| *******************************************************************************/ |
| package org.eclipse.core.internal.events; |
| |
| import java.util.*; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.function.Supplier; |
| import java.util.stream.Collectors; |
| import org.eclipse.core.internal.dtree.DeltaDataTree; |
| import org.eclipse.core.internal.resources.*; |
| import org.eclipse.core.internal.resources.ComputeProjectOrder.Digraph; |
| 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 { |
| |
| private static final String BUILDER_INIT = "BuilderInitInfo"; //$NON-NLS-1$ |
| |
| /** |
| * 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). |
| */ |
| static class DeltaCache<E> { |
| private final Map<IPath, E> deltas = new HashMap<>(); |
| private ElementTree newTree; |
| private ElementTree oldTree; |
| |
| public void flush() { |
| deltas.clear(); |
| this.oldTree = null; |
| this.newTree = null; |
| } |
| |
| /** |
| * Returns the cached resource delta for the given project and trees, or |
| * calls calculator to compute a new delta if there is no matching one in the cache. |
| */ |
| public E computeIfAbsent(IPath project, ElementTree anOldTree, ElementTree aNewTree, Supplier<E> calculator) { |
| if (!(areEqual(this.oldTree, anOldTree) && areEqual(this.newTree, aNewTree))) { |
| this.oldTree = anOldTree; |
| this.newTree = aNewTree; |
| deltas.clear(); |
| } |
| return deltas.computeIfAbsent(project, p -> calculator.get()); |
| } |
| |
| private static boolean areEqual(ElementTree cached, ElementTree requested) { |
| return !ElementTree.hasChanges(requested, cached, ResourceComparator.getBuildComparator(), true); |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| @Override |
| protected IProject[] build(int kind, Map<String, String> 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; |
| } |
| |
| String getName() { |
| return name; |
| } |
| |
| @Override |
| public ISchedulingRule getRule(int kind, Map<String, String> args) { |
| return null; |
| } |
| |
| } |
| |
| private static final int TOTAL_BUILD_WORK = Policy.totalWork * 1000; |
| |
| //the job for performing background autobuild |
| final AutoBuildJob autoBuildJob; |
| private final Set<IProject> builtProjects = Collections.synchronizedSet(new HashSet<>()); |
| |
| //the following four fields only apply for the lifetime of a single builder invocation. |
| protected final Set<InternalBuilder> currentBuilders; |
| private DeltaDataTree currentDelta; |
| private ElementTree currentLastBuiltTree; |
| private ElementTree currentTree; |
| |
| /** |
| * Caches the IResourceDelta for a pair of trees |
| */ |
| final private DeltaCache<IResourceDelta> deltaCache = new DeltaCache<>(); |
| /** |
| * Caches the DeltaDataTree used to determine if a build is necessary |
| */ |
| final private DeltaCache<DeltaDataTree> deltaTreeCache = new DeltaCache<>(); |
| |
| private ILock lock; |
| |
| /** |
| * {@code true} if we can exit inner build loop cycle early after |
| * {@link #requestRebuild()} is set by one build config and before following |
| * build configs are executed. Default is {@code false}. |
| */ |
| private boolean earlyExitFromBuildLoopAllowed; |
| |
| /** |
| * Used for the build cycle looping mechanism. If true, build loop over multiple |
| * projects will be restarted again for all projects in the loop |
| */ |
| private boolean rebuildRequested; |
| |
| /** |
| * Set of projects for which builders requested rebuild. Has no effect if any |
| * builder requested rebuild of everything via {@link #rebuildRequested} |
| */ |
| private final Set<IProject> projectsToRebuild; |
| |
| /** |
| * Map of projects for which builders requested rebuild for the current build |
| * cycle. If the value is "true" - stop building project with other builders |
| * immediately, "false" to continue build and start project build again after |
| * all builders were done. If no value is set, no rebuild is requested. |
| */ |
| private final Map<IProject, Boolean> restartBuildImmediately; |
| |
| // Shows if we are in the parallel build loop or not |
| boolean parallelBuild; |
| |
| private final Bundle systemBundle = Platform.getBundle("org.eclipse.osgi"); //$NON-NLS-1$ |
| |
| // protects against concurrent access of session stored builders during builder initialization |
| private Object builderInitializationLock = new Object(); |
| |
| //used for debug/trace timing |
| private long timeStamp = -1; |
| private long overallTimeStamp = -1; |
| private Workspace workspace; |
| |
| public BuildManager(Workspace workspace, ILock workspaceLock) { |
| this.workspace = workspace; |
| this.currentBuilders = Collections.synchronizedSet(new HashSet<>()); |
| this.autoBuildJob = new AutoBuildJob(workspace); |
| projectsToRebuild = ConcurrentHashMap.newKeySet(); |
| restartBuildImmediately = new ConcurrentHashMap<>(); |
| this.lock = workspaceLock; |
| InternalBuilder.buildManager = this; |
| setEarlyExitFromBuildLoopAllowed( |
| Boolean.getBoolean("org.eclipse.core.resources.allowEarlyBuildLoopExit")); //$NON-NLS-1$ ); |
| } |
| |
| private void basicBuild(int trigger, IncrementalProjectBuilder builder, Map<String, String> args, MultiStatus status, IProgressMonitor monitor) { |
| InternalBuilder currentBuilder = builder; // downcast to make package methods visible |
| try { |
| currentBuilders.add(currentBuilder); |
| //clear any old requests to forget built state |
| currentBuilder.clearLastBuiltStateRequests(); |
| // Figure out want kind of build is needed |
| boolean clean = trigger == IncrementalProjectBuilder.CLEAN_BUILD; |
| currentLastBuiltTree = currentBuilder.getLastBuiltTree(); |
| |
| // Does the build command respond to this trigger? |
| boolean isBuilding = builder.getCommand().isBuilding(trigger); |
| |
| // If no tree is available we have to do a full build |
| if (!clean && currentLastBuiltTree == null) { |
| // Bug 306746 - Don't promote build to FULL_BUILD if builder doesn't AUTO_BUILD |
| if (trigger == IncrementalProjectBuilder.AUTO_BUILD && !isBuilding) |
| return; |
| // Without a build tree the build is promoted to FULL_BUILD |
| trigger = IncrementalProjectBuilder.FULL_BUILD; |
| isBuilding = isBuilding || builder.getCommand().isBuilding(trigger); |
| } |
| |
| //don't build if this builder doesn't respond to the trigger |
| if (!isBuilding) { |
| 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; |
| ISchedulingRule rule = null; |
| 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; |
| } |
| rule = builder.getRule(trigger, args); |
| 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); |
| // Make the current tree immutable before releasing the WS lock |
| if (rule != null && currentTree != null) { |
| workspace.newWorkingTree(); |
| } |
| //release workspace lock while calling builders |
| depth = getWorkManager().beginUnprotected(); |
| // Acquire the rule required for running this builder |
| if (rule != null) { |
| Job.getJobManager().beginRule(rule, monitor); |
| // Now that we've acquired the rule, changes may have been made concurrently, ensure we're pointing at the |
| // correct currentTree so delta contains concurrent changes made in areas guarded by the scheduling rule |
| if (currentTree != null) |
| currentTree = workspace.getElementTree(); |
| } |
| //do the build |
| SafeRunner.run(getSafeRunnable(currentBuilder, trigger, args, status, monitor)); |
| } finally { |
| // Re-acquire the WS lock, then release the scheduling rule |
| if (depth >= 0) { |
| getWorkManager().endUnprotected(depth); |
| } |
| if (rule != null) { |
| Job.getJobManager().endRule(rule); |
| } |
| // Be sure to clean up after ourselves. |
| if (clean || currentBuilder.wasForgetStateRequested()) { |
| currentBuilder.setLastBuiltTree(null); |
| } else if (currentBuilder.wasRememberStateRequested()) { |
| // If remember last build state, and FULL_BUILD |
| // last tree must be set to => null for next build |
| if (trigger == IncrementalProjectBuilder.FULL_BUILD) { |
| currentBuilder.setLastBuiltTree(null); |
| } |
| // else don't modify the last built tree |
| } else { |
| // remember the current state as the last built state. |
| ElementTree lastTree = workspace.getElementTree(); |
| lastTree.immutable(); |
| currentBuilder.setLastBuiltTree(lastTree); |
| } |
| hookEndBuild(builder); |
| } |
| } finally { |
| currentBuilders.remove(currentBuilder); |
| currentTree = null; |
| currentLastBuiltTree = null; |
| currentDelta = null; |
| } |
| } |
| |
| protected void basicBuild(IBuildConfiguration buildConfiguration, int trigger, IBuildContext context, ICommand[] commands, MultiStatus status, IProgressMonitor monitor) { |
| int remainingIterations = Math.max(1, workspace.getDescription().getMaxBuildIterations()); |
| |
| try { |
| boolean shouldRebuild = true; |
| while (shouldRebuild) { |
| shouldRebuild = false; |
| 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(buildConfiguration, command, i, status, context); |
| if (builder != null) { |
| basicBuild(trigger, builder, command.getArguments(false), status, sub); |
| |
| // Check if the builder requested rebuild |
| IProject project = builder.getProject(); |
| Boolean restartImmediately = restartBuildImmediately.remove(project); |
| if (restartImmediately != null) { |
| remainingIterations--; |
| if (remainingIterations > 0) { |
| if (!restartImmediately) { |
| // process building all builders and restart after that |
| shouldRebuild = true; |
| } else { |
| // First builder doesn't need to restart anything |
| if (i > 0) { |
| // Start for loop again, input can be important for all builders before |
| shouldRebuild = true; |
| break; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } catch (CoreException e) { |
| status.add(e.getStatus()); |
| } |
| } |
| |
| /** |
| * Runs all builders on the given project config. |
| * @return A status indicating if the build succeeded or failed |
| */ |
| private IStatus basicBuild(IBuildConfiguration buildConfiguration, int trigger, IBuildContext context, IProgressMonitor monitor) { |
| try { |
| hookStartBuild(new IBuildConfiguration[] {buildConfiguration}, trigger); |
| MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, Messages.events_errors, null); |
| basicBuild(buildConfiguration, trigger, context, status, monitor); |
| return status; |
| } finally { |
| hookEndBuild(trigger); |
| } |
| } |
| |
| private void basicBuild(final IBuildConfiguration buildConfiguration, final int trigger, final IBuildContext context, final MultiStatus status, final IProgressMonitor monitor) { |
| try { |
| final IProject project = buildConfiguration.getProject(); |
| 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() { |
| @Override |
| 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)); |
| } |
| |
| @Override |
| public void run() throws Exception { |
| basicBuild(buildConfiguration, trigger, context, commands, status, monitor); |
| } |
| }; |
| SafeRunner.run(code); |
| } finally { |
| monitor.done(); |
| } |
| } |
| |
| /** |
| * Runs the builder with the given name on the given project config. |
| * @return A status indicating if the build succeeded or failed |
| */ |
| private IStatus basicBuild(IBuildConfiguration buildConfiguration, int trigger, String builderName, Map<String, String> args, IProgressMonitor monitor) { |
| final IProject project = buildConfiguration.getProject(); |
| monitor = Policy.monitorFor(monitor); |
| try { |
| String message = NLS.bind(Messages.events_building_1, project.getFullPath()); |
| monitor.beginTask(message, 1); |
| try { |
| hookStartBuild(new IBuildConfiguration[] {buildConfiguration}, trigger); |
| MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, Messages.events_errors, null); |
| ICommand command = getCommand(project, builderName, args); |
| try { |
| IBuildContext context = new BuildContext(buildConfiguration); |
| IncrementalProjectBuilder builder = getBuilder(buildConfiguration, command, -1, status, context); |
| 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(IBuildConfiguration[] configs, IBuildConfiguration[] requestedConfigs, int trigger, MultiStatus status, IProgressMonitor monitor) { |
| int projectWork = configs.length > 0 ? TOTAL_BUILD_WORK / configs.length : 0; |
| int maxIterations = workspace.getDescription().getMaxBuildIterations(); |
| // Scale allowed iterations count depending on affected projects - |
| // allow at least two build cycles per project |
| maxIterations = Math.max(configs.length * 2, maxIterations); |
| if (maxIterations <= 0) { |
| maxIterations = 1; |
| } |
| |
| rebuildRequested = true; |
| boolean rebuildSomething = true; |
| for (int iter = 0; rebuildSomething && iter < maxIterations; iter++) { |
| // Used for compatibility reason with requestRebuild() |
| boolean rebuildAll = rebuildRequested; |
| final boolean lastIteration = iter == maxIterations - 1; |
| |
| if (rebuildAll) { |
| // default build loop |
| basicBuildLoop(configs, requestedConfigs, trigger, status, monitor, projectWork, lastIteration); |
| } else { |
| // rebuild only projects requested by builders during previous build cycle |
| List<IBuildConfiguration> allConfigs = Arrays.asList(workspace.getBuildOrder()); |
| IBuildConfiguration[] configurations = allConfigs.stream() |
| .filter(c -> projectsToRebuild.contains(c.getProject())).toArray(IBuildConfiguration[]::new); |
| basicBuildLoop(configurations, requestedConfigs, trigger, status, monitor, projectWork, lastIteration); |
| } |
| if (rebuildRequested) { |
| rebuildSomething = true; |
| projectsToRebuild.clear(); |
| restartBuildImmediately.clear(); |
| } else if (!projectsToRebuild.isEmpty()) { |
| rebuildSomething = true; |
| } else { |
| rebuildSomething = false; |
| } |
| |
| // subsequent builds should always be incremental |
| // i.e. autobuild if not requested by user |
| // INCREMENTAL_BUILD would not be auto interrupted by user actions |
| if (trigger != IncrementalProjectBuilder.AUTO_BUILD) { |
| trigger = IncrementalProjectBuilder.INCREMENTAL_BUILD; |
| } |
| } |
| } |
| |
| private void basicBuildLoop(IBuildConfiguration[] configs, IBuildConfiguration[] requestedConfigs, int trigger, |
| MultiStatus status, IProgressMonitor monitor, int projectWork, final boolean lastIteration) { |
| |
| // If we are rebuilding anything, we can clear already build projects. |
| // If we build only few dedicated projects, all others, already built |
| // projects should be added again to the "built" list, otherwise |
| // hasBeenBuilt() will return "false" for them and they would not considered |
| // for a rebuild if requested by one of the projects to be re-built now. |
| if (rebuildRequested) { |
| builtProjects.clear(); |
| } else { |
| builtProjects.removeAll(projectsToRebuild); |
| } |
| |
| // Clear all the rebuild related flags before entering new build cycle |
| rebuildRequested = false; |
| projectsToRebuild.clear(); |
| restartBuildImmediately.clear(); |
| |
| // Basic loop over projects |
| for (IBuildConfiguration config : configs) { |
| if (config.getProject().isAccessible()) { |
| IBuildContext context = new BuildContext(config, requestedConfigs, configs); |
| |
| // Inner loop over builders in one project |
| basicBuild(config, trigger, context, status, Policy.subMonitorFor(monitor, projectWork)); |
| builtProjects.add(config.getProject()); |
| |
| // Check if we should continue with other projects |
| if ((rebuildRequested || !projectsToRebuild.isEmpty()) |
| && isEarlyExitFromBuildLoopAllowed()) { |
| if (lastIteration) { |
| // run build for all projects at least once |
| continue; |
| } |
| // Don't build following projects if one of the predecessors |
| // requested rebuild anyway, just start main loop from scratch |
| break; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Runs all builders on all the given project configs, in the order that |
| * they are given. |
| * @return A status indicating if the build succeeded or failed |
| */ |
| public IStatus build(IBuildConfiguration[] configs, IBuildConfiguration[] requestedConfigs, int trigger, IProgressMonitor monitor) { |
| monitor = Policy.monitorFor(monitor); |
| try { |
| monitor.beginTask(Messages.events_building_0, TOTAL_BUILD_WORK); |
| try { |
| hookStartBuild(configs, trigger); |
| MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.BUILD_FAILED, Messages.events_errors, null); |
| basicBuildLoop(configs, requestedConfigs, trigger, status, monitor); |
| return status; |
| } finally { |
| hookEndBuild(trigger); |
| } |
| } finally { |
| endBuild(trigger, monitor); |
| } |
| } |
| |
| /** |
| * Runs all builders on all the given project configs, in the order that |
| * they are given. |
| * @return A status indicating if the build succeeded or failed |
| */ |
| public IStatus buildParallel(Digraph<IBuildConfiguration> configs, IBuildConfiguration[] requestedConfigs, int trigger, JobGroup buildJobGroup, IProgressMonitor monitor) { |
| parallelBuild = true; |
| monitor = Policy.monitorFor(monitor); |
| try { |
| monitor.beginTask(Messages.events_building_0, TOTAL_BUILD_WORK); |
| try { |
| builtProjects.clear(); |
| hookStartBuild(configs.vertexList.stream().map(vertex -> vertex.id).toArray(IBuildConfiguration[]::new), trigger); |
| MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.BUILD_FAILED, Messages.events_errors, null); |
| parallelBuildLoop(configs, requestedConfigs, trigger, buildJobGroup, status, monitor); |
| return status; |
| } finally { |
| hookEndBuild(trigger); |
| } |
| } finally { |
| endBuild(trigger, monitor); |
| parallelBuild = false; |
| } |
| } |
| |
| private void endBuild(int trigger, IProgressMonitor monitor) { |
| boolean cancelledBuild = monitor.isCanceled(); |
| monitor.done(); |
| if (trigger == IncrementalProjectBuilder.INCREMENTAL_BUILD || trigger == IncrementalProjectBuilder.FULL_BUILD) { |
| autoBuildJob.avoidBuild(); |
| } else if (cancelledBuild) { |
| // Bug 538789: if a build was explicitly cancelled, don't trigger auto-build jobs until a build is requested |
| autoBuildJob.avoidBuildIfNotInterrupted(); |
| } |
| } |
| |
| private void parallelBuildLoop(final Digraph<IBuildConfiguration> configs, IBuildConfiguration[] requestedConfigs, int trigger, JobGroup buildJobGroup, MultiStatus status, IProgressMonitor monitor) { |
| final int projectWork = configs.vertexList.size() > 0 ? TOTAL_BUILD_WORK / configs.vertexList.size() : 0; |
| builtProjects.clear(); |
| final GraphProcessor<IBuildConfiguration> graphProcessor = new GraphProcessor<>(configs, IBuildConfiguration.class, (config, graphCrawler) -> { |
| IBuildContext context = new BuildContext(config, requestedConfigs, graphCrawler.getSequentialOrder()); // TODO consider passing Digraph to BuildConfig? |
| try { |
| workspace.prepareOperation(null, monitor); |
| workspace.beginOperation(false); |
| basicBuild(config, trigger, context, status, Policy.subMonitorFor(monitor, projectWork)); |
| workspace.endOperation(null, false); |
| builtProjects.add(config.getProject()); |
| } catch (CoreException ex) { |
| status.add(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, ex.getMessage(), ex)); |
| } |
| }, config -> getRule(config, trigger, null, Collections.emptyMap()), buildJobGroup); |
| graphProcessor.processGraphWithParallelJobs(); |
| try { |
| Job.getJobManager().join(graphProcessor, monitor); |
| } catch (OperationCanceledException | InterruptedException e) { |
| status.add(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, e.getMessage(), e)); |
| } |
| } |
| |
| /** |
| * Runs the builder with the given name on the given project config. |
| * @return A status indicating if the build succeeded or failed |
| */ |
| public IStatus build(IBuildConfiguration buildConfiguration, int trigger, String builderName, Map<String, String> args, IProgressMonitor monitor) { |
| monitor = Policy.monitorFor(monitor); |
| rebuildRequested = false; |
| projectsToRebuild.clear(); |
| restartBuildImmediately.clear(); |
| if (builderName == null) { |
| IBuildContext context = new BuildContext(buildConfiguration); |
| return basicBuild(buildConfiguration, trigger, context, monitor); |
| } |
| return basicBuild(buildConfiguration, trigger, builderName, args, monitor); |
| } |
| |
| /** |
| * 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 for all configs that are |
| * in the builder spec, and that have a last built state, even if they |
| * have not been instantiated this session. |
| * |
| * e.g. |
| * For a project with 3 builders, 2 build configurations and the second |
| * builder doesn't support configurations. |
| * The returned List of BuilderInfos is ordered: |
| * builder_id, config_name,builder_index |
| * builder_1, config_1, 1 |
| * builder_1, config_2, 1 |
| * builder_2, null, 2 |
| * builder_3, config_1, 3 |
| * builder_3, config_1, 3 |
| * |
| */ |
| public ArrayList<BuilderPersistentInfo> createBuildersPersistentInfo(IProject project) throws CoreException { |
| /* get the old builders (those not yet instantiated) */ |
| ArrayList<BuilderPersistentInfo> oldInfos = getBuildersPersistentInfo(project); |
| |
| ProjectDescription desc = ((Project) project).internalGetDescription(); |
| ICommand[] commands = desc.getBuildSpec(false); |
| if (commands.length == 0) |
| return null; |
| IBuildConfiguration[] configs = project.getBuildConfigs(); |
| |
| /* build the new list */ |
| ArrayList<BuilderPersistentInfo> newInfos = new ArrayList<>(commands.length * configs.length); |
| for (int i = 0; i < commands.length; i++) { |
| BuildCommand command = (BuildCommand) commands[i]; |
| String builderName = command.getBuilderName(); |
| |
| // If the builder doesn't support configurations, only 1 delta tree to persist |
| boolean supportsConfigs = command.supportsConfigs(); |
| int numberConfigs = supportsConfigs ? configs.length : 1; |
| |
| for (int j = 0; j < numberConfigs; j++) { |
| IBuildConfiguration config = configs[j]; |
| BuilderPersistentInfo info = null; |
| IncrementalProjectBuilder builder = ((BuildCommand) commands[i]).getBuilder(config); |
| if (builder == null) { |
| // if the builder was not instantiated, use the old info if any. |
| if (oldInfos != null) |
| info = getBuilderInfo(oldInfos, builderName, supportsConfigs ? config.getName() : null, 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(), supportsConfigs ? config.getName() : null, builderName, i); |
| info.setLastBuildTree(oldTree); |
| info.setInterestingProjects(((InternalBuilder) builder).getInterestingProjects()); |
| } |
| } |
| if (info != null) |
| newInfos.add(info); |
| } |
| } |
| return newInfos; |
| } |
| |
| private String debugBuilder() { |
| return currentBuilders == null ? "<no builder>" : currentBuilders.getClass().getName(); //$NON-NLS-1$ |
| } |
| |
| private String debugProject() { |
| if (currentBuilders == null) |
| return "<no project>"; //$NON-NLS-1$ |
| return "[" + currentBuilders.stream().map(builder -> builder.getProject().getFullPath().toString()).collect(Collectors.joining(",")) + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| } |
| |
| /** |
| * 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 buildConfiguration The project config 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(IBuildConfiguration buildConfiguration, ICommand command, int buildSpecIndex, MultiStatus status) throws CoreException { |
| BuildCommand buildCommand = (BuildCommand) command; |
| InternalBuilder result = buildCommand.getBuilder(buildConfiguration); |
| String builderName = command.getBuilderName(); |
| IProject project = buildConfiguration.getProject(); |
| |
| if (result == null) { |
| // Synchronized builderInitializationLock blocks below are used to avoid |
| // locking during initializeBuilder() call and to make sure two threads |
| // trying to init the same builder in parallel will get properly |
| // initialized builder without deadlocks or ConcurrentModificationException |
| // See bug 538102 and bug 517411. |
| BuilderPersistentInfo info; |
| synchronized (builderInitializationLock) { |
| // get the map of builders to get the last built tree |
| BuilderPersistentInfo builderInitInProgress = getBuilderInitInfo(project, builderName); |
| if (builderInitInProgress != null) { |
| info = builderInitInProgress; |
| } else { |
| info = removePersistentBuilderInfo(builderName, buildConfiguration, buildSpecIndex); |
| setBuilderInitInfo(project, builderName, info); |
| } |
| } |
| |
| result = buildCommand.getBuilder(buildConfiguration); |
| if (result == null) { |
| // Not synchronized on builderInitializationLock to avoid deadlocks if the builder init code |
| // requests a lock held by another thread which may be waiting on builderInitializationLock |
| result = initializeBuilder(command, builderName, buildConfiguration, info, status); |
| } |
| |
| synchronized (builderInitializationLock) { |
| // the build command holds only one builder per configuration |
| // so query the builder for the configuration once more, |
| // in case another builder was added since we last checked |
| InternalBuilder other = buildCommand.getBuilder(buildConfiguration); |
| if (other == null) { |
| buildCommand.addBuilder(buildConfiguration, (IncrementalProjectBuilder) result); |
| } else { |
| result = other; |
| } |
| setBuilderInitInfo(project, builderName, null); |
| } |
| } |
| |
| // Ensure the build configuration stays fresh for non-config aware builders |
| result.setBuildConfig(buildConfiguration); |
| if (!validateNature(result, 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 |
| result.setLastBuiltTree(null); |
| return null; |
| } |
| return (IncrementalProjectBuilder) result; |
| } |
| |
| /** |
| * Returns the builder instance corresponding to the given command, or |
| * <code>null</code> if the builder was not valid, and sets its context |
| * to the one supplied. |
| * |
| * @param buildConfiguration The project config 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(IBuildConfiguration buildConfiguration, ICommand command, int buildSpecIndex, MultiStatus status, IBuildContext context) throws CoreException { |
| InternalBuilder builder = getBuilder(buildConfiguration, command, buildSpecIndex, status); |
| if (builder != null) |
| builder.setContext(context); |
| return (IncrementalProjectBuilder) builder; |
| } |
| |
| /** |
| * Removes the builder persistent info from the map corresponding to the |
| * given builder name, configuration name and build spec index, or <code>null</code> if not found |
| * |
| * @param configName or null if the builder doesn't support configurations |
| * @param buildSpecIndex The index in the build spec, or -1 if unknown |
| */ |
| private BuilderPersistentInfo getBuilderInfo(ArrayList<BuilderPersistentInfo> infos, String builderName, String configName, int buildSpecIndex) { |
| //try to match on builder index, but if not match is found, use the builder name and config name |
| //this is because older workspace versions did not store builder infos in build spec order |
| BuilderPersistentInfo nameMatch = null; |
| for (BuilderPersistentInfo info : infos) { |
| // match on name, config name and build spec index if known |
| // Note: the config name may be null for builders that don't support configurations, or old workspaces |
| if (info.getBuilderName().equals(builderName) && (info.getConfigName() == null || info.getConfigName().equals(configName))) { |
| //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. |
| */ |
| @SuppressWarnings({"unchecked"}) |
| public ArrayList<BuilderPersistentInfo> getBuildersPersistentInfo(IProject project) throws CoreException { |
| return (ArrayList<BuilderPersistentInfo>) 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<String, String> args) { |
| ICommand[] buildSpec = ((Project) project).internalGetDescription().getBuildSpec(false); |
| for (ICommand element : buildSpec) |
| if (element.getBuilderName().equals(builderName)) |
| return element; |
| //none found, so create a new command |
| BuildCommand result = new BuildCommand(); |
| result.setBuilderName(builderName); |
| result.setArguments(args); |
| return result; |
| } |
| |
| /** |
| * Gets a workspace delta for a given project, based on the state of the workspace |
| * tree the last time the current builder was run. |
| * <p> |
| * Returns null if: |
| * <ul> |
| * <li> The state of the workspace is unknown. </li> |
| * <li> The current builder has not indicated that it is interested in deltas |
| * for the given project. </li> |
| * <li> If the project does not exist. </li> |
| * </ul> |
| * <p> |
| * Deltas are computed once and cached for efficiency. |
| * |
| * @param project the project to get a delta for |
| */ |
| 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; |
| } |
| Set<InternalBuilder> interestedBuilders = getInterestedBuilders(project); |
| //check if this builder has indicated it cares about this project |
| if (interestedBuilders.isEmpty()) { |
| if (Policy.DEBUG_BUILD_FAILURE) |
| Policy.debug("Build: project not interesting for current builders " + 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 resultDelta = deltaCache.computeIfAbsent(project.getFullPath(), currentLastBuiltTree, currentTree, () -> { |
| long startTime = 0L; |
| if (Policy.DEBUG_BUILD_DELTA) { |
| startTime = System.currentTimeMillis(); |
| Policy.debug("Computing delta for project: " + project.getName()); //$NON-NLS-1$ |
| } |
| IResourceDelta result = ResourceDeltaFactory.computeDelta(workspace, currentLastBuiltTree, currentTree, project.getFullPath(), -1); |
| 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" + ((ResourceDelta) result).toDeepDebugString()); //$NON-NLS-1$ //$NON-NLS-2$ |
| |
| return result; |
| }); |
| return resultDelta; |
| } finally { |
| lock.release(); |
| } |
| } |
| |
| /** |
| * Returns the safe runnable instance for invoking a builder |
| * @param currentBuilder |
| */ |
| private ISafeRunnable getSafeRunnable(final InternalBuilder currentBuilder, final int trigger, final Map<String, String> args, final MultiStatus status, final IProgressMonitor monitor) { |
| return new ISafeRunnable() { |
| @Override |
| 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()); |
| } |
| |
| @Override |
| 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 { |
| if (currentBuilder instanceof IIncrementalProjectBuilder2) { |
| ((IIncrementalProjectBuilder2) currentBuilder).clean(args, monitor); |
| } else { |
| currentBuilder.clean(monitor); |
| } |
| } |
| if (prereqs == null) |
| prereqs = new IProject[0]; |
| currentBuilder.setInterestingProjects(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; |
| } |
| |
| @Override |
| 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 at least one of the given project's configs have 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) { |
| builtProjects.clear(); |
| deltaCache.flush(); |
| deltaTreeCache.flush(); |
| //ensure autobuild runs after a clean |
| if (trigger == IncrementalProjectBuilder.CLEAN_BUILD) |
| autoBuildJob.forceBuild(); |
| if (Policy.DEBUG_BUILD_INVOKING) { |
| Policy.debug("Top-level build-end time: " + (System.currentTimeMillis() - overallTimeStamp)); //$NON-NLS-1$ |
| overallTimeStamp = -1; |
| } |
| } |
| |
| /** |
| * 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(IBuildConfiguration[] configs, int trigger) { |
| if (Policy.DEBUG_BUILD_STACK) |
| Policy.debug(new RuntimeException("Starting build: " + debugTrigger(trigger))); //$NON-NLS-1$ |
| if (Policy.DEBUG_BUILD_INVOKING) { |
| overallTimeStamp = System.currentTimeMillis(); |
| StringBuilder sb = new StringBuilder("Top-level build-start of: "); //$NON-NLS-1$ |
| for (IBuildConfiguration config : configs) |
| sb.append(config).append(", "); //$NON-NLS-1$ |
| sb.append(debugTrigger(trigger)); |
| Policy.debug(sb.toString()); |
| } |
| } |
| |
| /** |
| * 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 InternalBuilder initializeBuilder(ICommand command, String builderName, IBuildConfiguration buildConfiguration, BuilderPersistentInfo info, MultiStatus status) { |
| IProject project = buildConfiguration.getProject(); |
| InternalBuilder 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); |
| } |
| |
| if (info != null) { |
| ElementTree tree = info.getLastBuiltTree(); |
| if (tree != null) { |
| builder.setLastBuiltTree(tree); |
| } |
| builder.setInterestingProjects(info.getInterestingProjects()); |
| } |
| builder.setCommand(command); |
| builder.setBuildConfig(buildConfiguration); |
| builder.startupOnInitialize(); |
| return builder; |
| } |
| |
| private BuilderPersistentInfo removePersistentBuilderInfo(String builderName, IBuildConfiguration buildConfiguration, int buildSpecIndex) throws CoreException { |
| IProject project = buildConfiguration.getProject(); |
| ArrayList<BuilderPersistentInfo> infos = getBuildersPersistentInfo(project); |
| if (infos != null) { |
| BuilderPersistentInfo info = getBuilderInfo(infos, builderName, buildConfiguration.getName(), buildSpecIndex); |
| if (info != null) { |
| infos.remove(info); |
| // delete the build map if it's now empty |
| if (infos.isEmpty()) { |
| setBuildersPersistentInfo(project, null); |
| } |
| return info; |
| } |
| |
| } |
| return null; |
| } |
| |
| /** |
| * 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(InternalBuilder currentBuilder, IProject project) { |
| if (project.equals(currentBuilder.getProject())) |
| return true; |
| IProject[] interestingProjects = currentBuilder.getInterestingProjects(); |
| for (IProject interestingProject : interestingProjects) { |
| if (interestingProject.equals(project)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private Set<InternalBuilder> getInterestedBuilders(final IProject project) { |
| final Set<InternalBuilder> res = new HashSet<>(); |
| for (final InternalBuilder builder : this.currentBuilders) { |
| if (isInterestingProject(builder, project)) { |
| res.add(builder); |
| } |
| } |
| return res; |
| } |
| |
| /** |
| * Returns true if the given builder needs to be invoked, and false |
| * otherwise. |
| * |
| * The algorithm is to compute the intersection of the set of build configs that |
| * have changed since the last build, and the set of build configs 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 : |
| for (InternalBuilder currentBuilder : this.currentBuilders) { |
| 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 = deltaTreeCache.computeIfAbsent(null, oldTree, newTree, () -> { |
| if (Policy.DEBUG_BUILD_NEEDED) { |
| String message = "Checking if need to build. Starting delta computation between: " + oldTree + " and " //$NON-NLS-1$ //$NON-NLS-2$ |
| + newTree; |
| Policy.debug(message); |
| } |
| DeltaDataTree computed = 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$ |
| |
| return computed; |
| }); |
| |
| //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 (IProject project : projects) { |
| if (currentDelta.findNodeAt(project.getFullPath()) != null) { |
| if (Policy.DEBUG_BUILD_NEEDED) |
| Policy.debug(toString(builder) + " needs building because of changes in: " + project.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; |
| } |
| |
| /** |
| * Hook for builders to request a rebuild for given project during the current |
| * build call. The builders configured to run after the current one will be |
| * still processed. To force an immediate rebuild of a project that wasn't fully |
| * built yet, {@code processOtherBuilders} argument should be set to |
| * {@code false}. |
| * <p> |
| * <b>Note</b> if {@code processOtherBuilders} is set to {@code false}, the |
| * project that is built with current builder will be only rebuilt again, if |
| * this builder is not the first one configured to run. |
| * |
| * @param processOtherBuilders to continue building project with other builders |
| * and not start from scratch immediately |
| */ |
| void requestRebuild(IProject project, boolean processOtherBuilders) { |
| if (project == null) { |
| return; |
| } |
| restartBuildImmediately.put(project, !processOtherBuilders); |
| } |
| |
| /** |
| * Hook for builders to request a rebuild for given projects. This request will |
| * cause the main build loop to cycle once again <b>at least</b> for given |
| * projects but the build loop also may run over all projects in build cycle if |
| * the {@link #requestRebuild()} flag was set. |
| * <p> |
| * <b>Note</b> the current project (that is currently built with current |
| * builder) will be not rebuilt in the current builld cycle, but scheduled for |
| * rebuild on next round. To perform immediate rebuild of the current project, |
| * use {@link #requestRebuild(IProject, boolean)}. |
| * |
| * @param toBeRebuilt to be rebuilt on next build round |
| * @param current project currently built with current builder |
| */ |
| void requestRebuild(Collection<IProject> toBeRebuilt, IProject current) { |
| for (IProject project : toBeRebuilt) { |
| if (project != null && hasBeenBuilt(project) || project.equals(current)) { |
| requestRebuildOnNextRound(project); |
| } |
| } |
| } |
| |
| /** |
| * Hook for builders to request an <b>unconditional<b> rebuild for given |
| * project, in the next build round, independently if the project was already |
| * built or not. |
| */ |
| void requestRebuildOnNextRound(IProject project) { |
| projectsToRebuild.add(project); |
| } |
| |
| /** |
| * Sets the builder infos for the given build config. 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, List<BuilderPersistentInfo> 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 |
| logProjectAccessError(project, e, "Project missing in setBuildersPersistentInfo"); //$NON-NLS-1$ |
| } |
| } |
| |
| private void setBuilderInitInfo(IProject project, String builderName, BuilderPersistentInfo info) { |
| try { |
| project.setSessionProperty(keyForBuilderInfo(builderName), info); |
| } catch (CoreException e) { |
| //project is missing -- build state will be lost |
| //can't throw an exception because this happens on startup |
| logProjectAccessError(project, e, "Project missing in setBuilderInitInfo"); //$NON-NLS-1$ |
| } |
| } |
| |
| private BuilderPersistentInfo getBuilderInitInfo(IProject project, String builderName) { |
| try { |
| return (BuilderPersistentInfo) project.getSessionProperty(keyForBuilderInfo(builderName)); |
| } catch (CoreException e) { |
| //project is missing -- build state will be lost |
| //can't throw an exception because this happens on startup |
| logProjectAccessError(project, e, "Project missing in getBuilderInitInfo"); //$NON-NLS-1$ |
| } |
| return null; |
| } |
| |
| private void logProjectAccessError(IProject project, CoreException e, String message) { |
| Policy.log(new ResourceStatus(IStatus.ERROR, 1, project.getFullPath(), message, e)); |
| } |
| |
| private static QualifiedName keyForBuilderInfo(String builderName) { |
| return new QualifiedName(ResourcesPlugin.PI_RESOURCES, BUILDER_INIT + builderName); |
| } |
| |
| @Override |
| public void shutdown(IProgressMonitor monitor) { |
| autoBuildJob.cancel(); |
| } |
| |
| @Override |
| 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); |
| if (builder instanceof MissingBuilder) |
| name = name + ": '" + ((MissingBuilder) builder).getName() + "'"; //$NON-NLS-1$ //$NON-NLS-2$ |
| return name + "(" + builder.getBuildConfig() + ")"; //$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(IBuildConfiguration buildConfiguration, int trigger, String builderName, Map<String, String> buildArgs) { |
| IProject project = buildConfiguration.getProject(); |
| MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, Messages.events_errors, null); |
| if (builderName == null) { |
| final ICommand[] commands; |
| if (project.isAccessible()) { |
| Set<ISchedulingRule> rules = new HashSet<>(); |
| commands = ((Project) project).internalGetDescription().getBuildSpec(false); |
| boolean hasNullBuildRule = false; |
| BuildContext context = new BuildContext(buildConfiguration); |
| for (int i = 0; i < commands.length; i++) { |
| BuildCommand command = (BuildCommand) commands[i]; |
| Map<String, String> allArgs = command.getArguments(true); |
| if (allArgs == null) { |
| allArgs = buildArgs; |
| } else if (buildArgs != null) { |
| allArgs.putAll(buildArgs); |
| } |
| try { |
| IncrementalProjectBuilder builder = getBuilder(buildConfiguration, command, i, status, context); |
| if (builder != null) { |
| ISchedulingRule builderRule = builder.getRule(trigger, allArgs); |
| 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(rules.toArray(new ISchedulingRule[rules.size()])); |
| } |
| } else { |
| // Returns the derived resources for the specified builderName |
| ICommand command = getCommand(project, builderName, buildArgs); |
| Map<String, String> allArgs = new HashMap<>(); |
| if (command.getArguments() != null) { |
| allArgs.putAll(command.getArguments()); |
| } |
| if (buildArgs != null) { |
| allArgs.putAll(buildArgs); |
| } |
| try { |
| IncrementalProjectBuilder builder = getBuilder(buildConfiguration, command, -1, status); |
| if (builder != null) |
| return builder.getRule(trigger, allArgs); |
| |
| } catch (CoreException e) { |
| status.add(e.getStatus()); |
| } |
| } |
| // Log any errors |
| if (!status.isOK()) |
| Policy.log(status); |
| return workspace.getRoot(); |
| } |
| |
| /** |
| * @return {@code true} if the projects build loop can restart immediately after |
| * rebuild request, {@code false} if the loop will continue building all |
| * not yet built projects |
| */ |
| public boolean isEarlyExitFromBuildLoopAllowed() { |
| return earlyExitFromBuildLoopAllowed; |
| } |
| |
| /** |
| * @param earlyExitFromBuildLoopAllowed {@code true} if the projects build loop |
| * should restart immediately after rebuild |
| * request, {@code false} if the loop |
| * should continue building all not yet |
| * built projects |
| */ |
| public void setEarlyExitFromBuildLoopAllowed(boolean earlyExitFromBuildLoopAllowed) { |
| this.earlyExitFromBuildLoopAllowed = earlyExitFromBuildLoopAllowed; |
| } |
| |
| } |