// // // // // $Revision$ // using System; using System.Collections.Generic; using System.Linq; using ICSharpCode.Core; using System.Diagnostics; using System.ComponentModel; namespace ICSharpCode.SharpDevelop.Project { public enum BuildOnExecuteSetting { [Description("${res:Dialog.Options.IDEOptions.ProjectAndSolutionOptions.WhenRunning.DoNotBuild}")] DoNotBuild, [Description("${res:Dialog.Options.IDEOptions.ProjectAndSolutionOptions.WhenRunning.BuildOnlyModified}")] BuildOnlyModified, [Description("${res:Dialog.Options.IDEOptions.ProjectAndSolutionOptions.WhenRunning.BuildModifiedAndDependent}")] BuildModifiedAndDependent, [Description("${res:Dialog.Options.IDEOptions.ProjectAndSolutionOptions.WhenRunning.RegularBuild}")] RegularBuild } /// /// Tracks changes to projects and causes only modified projects /// to be recompiled. /// static class BuildModifiedProjectsOnlyService { public static BuildOnExecuteSetting Setting { get { return PropertyService.Get("BuildOnExecute", BuildOnExecuteSetting.RegularBuild); } set { PropertyService.Set("BuildOnExecute", value); } } static readonly Dictionary unmodifiedProjects = new Dictionary(); static BuildModifiedProjectsOnlyService() { // these actions cause a full recompilation: ProjectService.SolutionClosed += MarkAllForRecompilation; ProjectService.SolutionConfigurationChanged += MarkAllForRecompilation; ProjectService.SolutionSaved += MarkAllForRecompilation; ProjectService.BuildFinished += ProjectService_BuildFinished; FileUtility.FileSaved += OnFileSaved; } public static void Initialize() { // first call to init causes static ctor calls } static void ProjectService_BuildFinished(object sender, BuildEventArgs e) { // at the end of an successful build, mark all built projects as unmodified if (e.Results.Result == BuildResultCode.Success) { lock (unmodifiedProjects) { CompilationPass pass = new CompilationPass(); foreach (IBuildable b in e.Results.BuiltProjects) { IProject p = GetProjectFromBuildable(b); if (p != null) { unmodifiedProjects[p] = pass; } } } } // at the end of a cleaning build, mark all projects as requiring a rebuild if (e.Options.ProjectTarget == BuildTarget.Clean || e.Options.TargetForDependencies == BuildTarget.Clean) { lock (unmodifiedProjects) { unmodifiedProjects.Clear(); } } } static IProject GetProjectFromBuildable(IBuildable b) { while (b is Wrapper) b = ((Wrapper)b).wrapped; return b as IProject; } static void MarkAllForRecompilation(object sender, EventArgs e) { lock (unmodifiedProjects) { unmodifiedProjects.Clear(); } } static void OnFileSaved(object sender, FileNameEventArgs e) { if (ProjectService.OpenSolution != null) { foreach (IProject p in ProjectService.OpenSolution.Projects) { if (p.FindFile(e.FileName) != null || FileUtility.IsEqualFileName(p.FileName, e.FileName)) { lock (unmodifiedProjects) { unmodifiedProjects.Remove(p); } } } } } public static IBuildable WrapBuildable(IBuildable buildable) { switch (Setting) { case BuildOnExecuteSetting.DoNotBuild: return new DummyBuildable(buildable); case BuildOnExecuteSetting.BuildModifiedAndDependent: case BuildOnExecuteSetting.BuildOnlyModified: lock (unmodifiedProjects) { foreach (var pair in unmodifiedProjects) { LoggingService.Debug(pair.Key.Name + ": " + pair.Value); } } return new WrapperFactory().GetWrapper(buildable); case BuildOnExecuteSetting.RegularBuild: return buildable; default: throw new NotSupportedException(); } } sealed class DummyBuildable : IBuildable { IBuildable wrappedBuildable; public DummyBuildable(IBuildable wrappedBuildable) { this.wrappedBuildable = wrappedBuildable; } public string Name { get { return wrappedBuildable.Name; } } public Solution ParentSolution { get { return wrappedBuildable.ParentSolution; } } public ProjectBuildOptions CreateProjectBuildOptions(BuildOptions options, bool isRootBuildable) { return null; } public ICollection GetBuildDependencies(ProjectBuildOptions buildOptions) { return new IBuildable[0]; } public void StartBuild(ThreadSafeServiceContainer buildServices, ProjectBuildOptions buildOptions, IBuildFeedbackSink feedbackSink) { } } sealed class CompilationPass { public readonly int Index; static int nextIndex; public CompilationPass() { Index = System.Threading.Interlocked.Increment(ref nextIndex); } public override string ToString() { return "[CompilationPass " + Index + "]"; } } sealed class WrapperFactory { public readonly CompilationPass CurrentPass = new CompilationPass(); readonly Dictionary dict = new Dictionary(); public IBuildable GetWrapper(IBuildable wrapped) { IBuildable b; lock (dict) { if (!dict.TryGetValue(wrapped, out b)) b = dict[wrapped] = new Wrapper(wrapped, this); } return b; } } sealed class Wrapper : IBuildable { internal readonly IBuildable wrapped; internal readonly WrapperFactory factory; public Wrapper(IBuildable wrapped, WrapperFactory factory) { this.wrapped = wrapped; this.factory = factory; } public string Name { get { return wrapped.Name; } } public Solution ParentSolution { get { return wrapped.ParentSolution; } } public ProjectBuildOptions CreateProjectBuildOptions(BuildOptions options, bool isRootBuildable) { return wrapped.CreateProjectBuildOptions(options, isRootBuildable); } Dictionary> cachedBuildDependencies = new Dictionary>(); ICollection cachedBuildDependenciesForNullOptions; public ICollection GetBuildDependencies(ProjectBuildOptions buildOptions) { List result = new List(); foreach (IBuildable b in wrapped.GetBuildDependencies(buildOptions)) { result.Add(factory.GetWrapper(b)); } lock (cachedBuildDependencies) { if (buildOptions != null) cachedBuildDependencies[buildOptions] = result; else cachedBuildDependenciesForNullOptions = result; } return result; } CompilationPass lastCompilationPass; /// /// Returns true if "this" was recompiled after "comparisonPass". /// internal bool WasRecompiledAfter(CompilationPass comparisonPass) { Debug.Assert(comparisonPass != null); if (lastCompilationPass == null) return true; return lastCompilationPass.Index > comparisonPass.Index; } public void StartBuild(ThreadSafeServiceContainer buildServices, ProjectBuildOptions buildOptions, IBuildFeedbackSink feedbackSink) { IProject p = wrapped as IProject; if (p == null) { wrapped.StartBuild(buildServices, buildOptions, feedbackSink); } else { lock (unmodifiedProjects) { if (!unmodifiedProjects.TryGetValue(p, out lastCompilationPass)) { lastCompilationPass = null; } } if (lastCompilationPass != null && Setting == BuildOnExecuteSetting.BuildModifiedAndDependent) { lock (cachedBuildDependencies) { var dependencies = buildOptions != null ? cachedBuildDependencies[buildOptions] : cachedBuildDependenciesForNullOptions; if (dependencies.OfType().Any(w=>w.WasRecompiledAfter(lastCompilationPass))) { lastCompilationPass = null; } } } if (lastCompilationPass != null) { feedbackSink.ReportMessage( StringParser.Parse("${res:MainWindow.CompilerMessages.SkipProjectNoChanges}", new string[,] {{ "Name", p.Name }}) ); feedbackSink.Done(true); } else { lastCompilationPass = factory.CurrentPass; wrapped.StartBuild(buildServices, buildOptions, new BuildFeedbackSink(p, feedbackSink, factory.CurrentPass)); } } } /// /// Wraps a build feedback sink and marks a project as requiring recompilation when /// compilation was not successful. /// sealed class BuildFeedbackSink : IBuildFeedbackSink { IProject project; IBuildFeedbackSink sink; CompilationPass currentPass; public BuildFeedbackSink(IProject p, IBuildFeedbackSink sink, CompilationPass currentPass) { Debug.Assert(p != null); Debug.Assert(sink != null); Debug.Assert(currentPass != null); this.project = p; this.sink = sink; this.currentPass = currentPass; } public Gui.IProgressMonitor ProgressMonitor { get { return sink.ProgressMonitor; } } public void ReportError(BuildError error) { sink.ReportError(error); } public void ReportMessage(string message) { sink.ReportMessage(message); } public void Done(bool success) { if (success) { lock (unmodifiedProjects) { unmodifiedProjects[project] = currentPass; } } sink.Done(success); } } } } }