diff --git a/src/CBT.NuGet.UnitTests/AggregatePackageTests.cs b/src/CBT.NuGet.UnitTests/AggregatePackageTests.cs index 5b800ff..a4e8c27 100644 --- a/src/CBT.NuGet.UnitTests/AggregatePackageTests.cs +++ b/src/CBT.NuGet.UnitTests/AggregatePackageTests.cs @@ -10,24 +10,13 @@ namespace CBT.NuGet.UnitTests { - public class AggregatePackageTests : IDisposable + public class AggregatePackageTests : TestBase { - private string _basePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - public AggregatePackageTests() - { - Directory.CreateDirectory(_basePath); - } - - public void Dispose() - { - Directory.Delete(_basePath, recursive: true); - } - - private void CreateDummyPackage(string _basePath, ICollection filePaths) + private void CreateDummyPackage(string basePath, ICollection filePaths) { foreach (var file in filePaths) { - var fullFilePath = Path.Combine(_basePath, file); + var fullFilePath = Path.Combine(basePath, file); var parent = Path.GetDirectoryName(fullFilePath); if (!Directory.Exists(parent)) { @@ -43,9 +32,9 @@ private void CreateDummyPackage(string _basePath, ICollection filePaths) [Fact] public void ParseAggregatePackageTest() { - string pkg = Path.Combine(_basePath, "pkg"); - string pkg2 = Path.Combine(_basePath, "pkg2"); - string pkg3 = Path.Combine(_basePath, "pkg3"); + string pkg = Path.Combine(TestRootPath, "pkg"); + string pkg2 = Path.Combine(TestRootPath, "pkg2"); + string pkg3 = Path.Combine(TestRootPath, "pkg3"); CreateDummyPackage(pkg, new[] { "fool.txt", "friend\\bat.txt", "cow.txt" }); CreateDummyPackage(pkg2, new[] { "cammel.txt", "sour\\bat.txt", "cow.txt" }); @@ -54,7 +43,7 @@ public void ParseAggregatePackageTest() var aggPkgs = new AggregatePackages(); aggPkgs.BuildEngine = new CBTBuildEngine(); aggPkgs.PackagesToAggregate = $"foo={pkg}|{pkg2}|!{pkg3};foo2= \t {pkg} | {pkg2} | ! {pkg3} \t"; - aggPkgs.AggregateDestRoot = Path.Combine(_basePath, ".agg"); + aggPkgs.AggregateDestRoot = Path.Combine(TestRootPath, ".agg"); var parsedPackagesEnumerator = aggPkgs.ParsePackagesToAggregate().GetEnumerator(); parsedPackagesEnumerator.MoveNext(); @@ -76,9 +65,9 @@ public void ParseAggregatePackageTest() public void CreateAggregatePackageTest() { - string pkg = Path.Combine(_basePath, "pkg"); - string pkg2 = Path.Combine(_basePath, "pkg2"); - string pkg3 = Path.Combine(_basePath, "pkg3"); + string pkg = Path.Combine(TestRootPath, "pkg"); + string pkg2 = Path.Combine(TestRootPath, "pkg2"); + string pkg3 = Path.Combine(TestRootPath, "pkg3"); CreateDummyPackage(pkg, new[] { "fool.txt", "friend\\bat.txt", "cow.txt" }); CreateDummyPackage(pkg2, new[] { "cammel.txt", "sour\\bat.txt", "cow.txt" }); @@ -87,7 +76,7 @@ public void CreateAggregatePackageTest() var aggPkgs = new AggregatePackages(); aggPkgs.BuildEngine = new CBTBuildEngine(); aggPkgs.PackagesToAggregate = $"foo={pkg}|{pkg2}|!{pkg3};foo2={pkg}|!{pkg2}"; - aggPkgs.AggregateDestRoot = Path.Combine(_basePath, ".agg"); + aggPkgs.AggregateDestRoot = Path.Combine(TestRootPath, ".agg"); var parsedPackagesEnumerator = aggPkgs.ParsePackagesToAggregate().GetEnumerator(); parsedPackagesEnumerator.MoveNext(); @@ -127,8 +116,8 @@ public void CreateAggregatePackageTest() [Fact] public void WritePropsAggregatePackageTest() { - string propsFile = Path.Combine(_basePath, "props", "foo.props"); - string propsFileExpect = Path.Combine(_basePath, "props", "fooExpected.props"); + string propsFile = Path.Combine(TestRootPath, "props", "foo.props"); + string propsFileExpect = Path.Combine(TestRootPath, "props", "fooExpected.props"); Dictionary props = new Dictionary(); props.Add("foo", "Myvalue"); props.Add("foo2", "MyValue"); diff --git a/src/CBT.NuGet.UnitTests/CBT.NuGet.UnitTests.csproj b/src/CBT.NuGet.UnitTests/CBT.NuGet.UnitTests.csproj index e1bb95b..573d2d9 100644 --- a/src/CBT.NuGet.UnitTests/CBT.NuGet.UnitTests.csproj +++ b/src/CBT.NuGet.UnitTests/CBT.NuGet.UnitTests.csproj @@ -24,6 +24,8 @@ + + diff --git a/src/CBT.NuGet.UnitTests/MSBuildProjectLoaderTests.cs b/src/CBT.NuGet.UnitTests/MSBuildProjectLoaderTests.cs index 985b5b1..9545310 100644 --- a/src/CBT.NuGet.UnitTests/MSBuildProjectLoaderTests.cs +++ b/src/CBT.NuGet.UnitTests/MSBuildProjectLoaderTests.cs @@ -1,48 +1,61 @@ using CBT.NuGet.Internal; using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; using Microsoft.MSBuildProjectBuilder; using Shouldly; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using Xunit; namespace CBT.NuGet.UnitTests { - public class MSBuildProjectLoaderTests : IDisposable + public class MSBuildProjectLoaderTests : TestBase { private const string MSBuildToolsVersion = "4.0"; - private readonly string _basePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - private readonly string _dirsAPath; - private readonly string _projectAPath; - private readonly string _projectBPath; + private readonly TestBuildEngine _buildEngine = new TestBuildEngine(); + private readonly Lazy _logLazy; public MSBuildProjectLoaderTests() { - _dirsAPath = Path.Combine(_basePath, "dirsA.proj"); - _projectAPath = Path.Combine(_basePath, "ProjectA.proj"); - _projectBPath = Path.Combine(_basePath, "ProjectB.proj"); - - var projB = ProjectBuilder.Create() - .Save(_projectBPath); - - ProjectBuilder - .Create() - .AddItem($"ProjectReference={projB.ProjectRoot.FullPath}") - .Save(_projectAPath); + _logLazy = new Lazy(() => new TaskLoggingHelper(_buildEngine, "TaskName"), isThreadSafe: true); + } - ProjectBuilder - .Create() - .AddProperty("IsTraversal=true") - .AddItem($"ProjectFile={_projectAPath}") - .Save(_dirsAPath); + public TaskLoggingHelper Log => _logLazy.Value; + + [Fact] + public void ArgumentNullException_Log() + { + ArgumentNullException exception = Should.Throw(() => + { + MSBuildProjectLoader unused = new MSBuildProjectLoader(globalProperties: null, toolsVersion: null, log: null); + }); + + exception.ParamName.ShouldBe("log"); } - public void Dispose() + [Fact] + public void BuildFailsIfError() { - Directory.Delete(_basePath, recursive: true); + var dirsProj = ProjectBuilder + .Create() + .AddProperty("IsTraversal=true") + .AddItem("ProjectFile=does not exist") + .Save(GetTempFileName()); + + MSBuildProjectLoader loader = new MSBuildProjectLoader(null, MSBuildToolsVersion, Log); + loader.LoadProjectsAndReferences(new[] {dirsProj.FullPath}); + + Log.HasLoggedErrors.ShouldBe(true); + + _buildEngine.LoggedEvents.Count.ShouldBe(1); + BuildErrorEventArgs errorEvent = _buildEngine.LoggedEvents.FirstOrDefault() as BuildErrorEventArgs; + + errorEvent.ShouldNotBeNull(); + + errorEvent?.Message.ShouldStartWith("The project file could not be loaded. Could not find file "); } [Fact] @@ -54,9 +67,13 @@ public void GlobalPropertiesSetCorrectly() {"Property2", "CEEC5C9FF0F344DAA32A0F545460EB2C"} }; - MSBuildProjectLoader loader = new MSBuildProjectLoader(expectedGlobalProperties, MSBuildToolsVersion, ProjectLoadSettings.Default); + var projectA = ProjectBuilder + .Create() + .Save(GetTempFileName()); - ProjectCollection projectCollection = loader.LoadProjectsAndReferences(new[] {_projectAPath}); + MSBuildProjectLoader loader = new MSBuildProjectLoader(expectedGlobalProperties, MSBuildToolsVersion, Log); + + ProjectCollection projectCollection = loader.LoadProjectsAndReferences(new[] {projectA.FullPath}); projectCollection.GlobalProperties.ShouldBe(expectedGlobalProperties); } @@ -64,21 +81,43 @@ public void GlobalPropertiesSetCorrectly() [Fact] public void ProjectReferencesWork() { - MSBuildProjectLoader loader = new MSBuildProjectLoader(null, MSBuildToolsVersion); + var projectB = ProjectBuilder.Create() + .Save(GetTempFileName()); + + var projectA = ProjectBuilder + .Create() + .AddProjectReference(projectB) + .Save(GetTempFileName()); - ProjectCollection projectCollection = loader.LoadProjectsAndReferences(new[] {_projectAPath}); + MSBuildProjectLoader loader = new MSBuildProjectLoader(null, MSBuildToolsVersion, Log); - projectCollection.LoadedProjects.Select(i => i.FullPath).ShouldBe(new[] {_projectAPath, _projectBPath}); + ProjectCollection projectCollection = loader.LoadProjectsAndReferences(new[] {projectA.FullPath}); + + projectCollection.LoadedProjects.Select(i => i.FullPath).ShouldBe(new[] {projectA.FullPath, projectB.FullPath}); } [Fact] public void TraversalReferencesWork() { - MSBuildProjectLoader loader = new MSBuildProjectLoader(null, MSBuildToolsVersion); + var projectB = ProjectBuilder.Create() + .Save(GetTempFileName()); + + var projectA = ProjectBuilder + .Create() + .AddProjectReference(projectB) + .Save(GetTempFileName()); + + var dirsProj = ProjectBuilder + .Create() + .AddProperty("IsTraversal=true") + .AddItem($"ProjectFile={projectA.FullPath}") + .Save(GetTempFileName()); + + MSBuildProjectLoader loader = new MSBuildProjectLoader(null, MSBuildToolsVersion, Log); - ProjectCollection projectCollection = loader.LoadProjectsAndReferences(new[] {_dirsAPath}); + ProjectCollection projectCollection = loader.LoadProjectsAndReferences(new[] {dirsProj.FullPath}); - projectCollection.LoadedProjects.Select(i => i.FullPath).ShouldBe(new[] {_dirsAPath, _projectAPath, _projectBPath}); + projectCollection.LoadedProjects.Select(i => i.FullPath).ShouldBe(new[] {dirsProj.FullPath, projectA.FullPath, projectB.FullPath}); } } } \ No newline at end of file diff --git a/src/CBT.NuGet.UnitTests/TestBase.cs b/src/CBT.NuGet.UnitTests/TestBase.cs new file mode 100644 index 0000000..6f795f4 --- /dev/null +++ b/src/CBT.NuGet.UnitTests/TestBase.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CBT.NuGet.UnitTests +{ + public abstract class TestBase : IDisposable + { + public string TestRootPath { get; } = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + protected string GetTempFileName() + { + Directory.CreateDirectory(TestRootPath); + + return Path.Combine(TestRootPath, Path.GetRandomFileName()); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (Directory.Exists(TestRootPath)) + { + Directory.Delete(TestRootPath, recursive: true); + } + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/CBT.NuGet.UnitTests/TestBuildEngine.cs b/src/CBT.NuGet.UnitTests/TestBuildEngine.cs new file mode 100644 index 0000000..0d8c9a5 --- /dev/null +++ b/src/CBT.NuGet.UnitTests/TestBuildEngine.cs @@ -0,0 +1,37 @@ +using Microsoft.Build.Framework; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace CBT.NuGet.UnitTests +{ + public class TestBuildEngine : IBuildEngine + { + private readonly ConcurrentBag _events = new ConcurrentBag(); + + public int ColumnNumberOfTaskNode => 0; + + public bool ContinueOnError => false; + + public int LineNumberOfTaskNode => 0; + + public IReadOnlyCollection LoggedEvents => _events.ToList().AsReadOnly(); + + public string ProjectFileOfTaskNode => null; + + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) + { + throw new NotSupportedException(); + } + + public void LogCustomEvent(CustomBuildEventArgs e) => _events.Add(e); + + public void LogErrorEvent(BuildErrorEventArgs e) => _events.Add(e); + + public void LogMessageEvent(BuildMessageEventArgs e) => _events.Add(e); + + public void LogWarningEvent(BuildWarningEventArgs e) => _events.Add(e); + } +} \ No newline at end of file diff --git a/src/CBT.NuGet/Internal/MSBuildProjectLoader.cs b/src/CBT.NuGet/Internal/MSBuildProjectLoader.cs index f3dc3e1..b21f773 100644 --- a/src/CBT.NuGet/Internal/MSBuildProjectLoader.cs +++ b/src/CBT.NuGet/Internal/MSBuildProjectLoader.cs @@ -5,6 +5,8 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Utilities; namespace CBT.NuGet.Internal { @@ -38,17 +40,21 @@ internal sealed class MSBuildProjectLoader /// private readonly string _toolsVersion; + private readonly TaskLoggingHelper _log; + /// /// Initializes a new instance of the MSBuildProjectLoader class. /// /// Specifies the global properties to use when loading projects. /// Specifies the ToolsVersion to use when loading projects. /// Specifies the to use when loading projects. - public MSBuildProjectLoader(IDictionary globalProperties, string toolsVersion, ProjectLoadSettings projectLoadSettings = ProjectLoadSettings.Default) + /// + public MSBuildProjectLoader(IDictionary globalProperties, string toolsVersion, TaskLoggingHelper log, ProjectLoadSettings projectLoadSettings = ProjectLoadSettings.Default) { _globalProperties = globalProperties; _toolsVersion = toolsVersion; _projectLoadSettings = projectLoadSettings; + _log = log ?? throw new ArgumentNullException(nameof(log)); } /// @@ -153,7 +159,17 @@ private bool TryLoadProject(string path, string toolsVersion, ProjectCollection long now = DateTime.Now.Ticks; - project = new Project(path, null, toolsVersion, projectCollection, projectLoadSettings); + try + { + project = new Project(path, null, toolsVersion, projectCollection, projectLoadSettings); + } + catch (Exception e) + { + _log.LogErrorFromException(e); + + return false; + } + if (CollectStats) { diff --git a/src/CBT.NuGet/Tasks/TraversalNuGetRestore.cs b/src/CBT.NuGet/Tasks/TraversalNuGetRestore.cs index bd11449..b14f135 100644 --- a/src/CBT.NuGet/Tasks/TraversalNuGetRestore.cs +++ b/src/CBT.NuGet/Tasks/TraversalNuGetRestore.cs @@ -21,11 +21,16 @@ public sealed class TraversalNuGetRestore : NuGetRestore public override bool Execute() { - MSBuildProjectLoader projectLoader = new MSBuildProjectLoader(GlobalProperties.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries).Where(i => !String.IsNullOrWhiteSpace(i)).Select(i => i.Trim().Split(new[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries)).ToDictionary(i => i.First(), i => i.Last()), MSBuildToolsVersion, ProjectLoadSettings.IgnoreMissingImports); + MSBuildProjectLoader projectLoader = new MSBuildProjectLoader(GlobalProperties.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries).Where(i => !String.IsNullOrWhiteSpace(i)).Select(i => i.Trim().Split(new[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries)).ToDictionary(i => i.First(), i => i.Last()), MSBuildToolsVersion, Log, ProjectLoadSettings.IgnoreMissingImports); Log.LogMessage(MessageImportance.Normal, $"Loading project references for '{Project}'..."); ProjectCollection projectCollection = projectLoader.LoadProjectsAndReferences(new[] { Project }); + if (Log.HasLoggedErrors) + { + return false; + } + Log.LogMessage(MessageImportance.Normal, $"Loaded '{projectCollection.LoadedProjects.Count}' projects"); Log.LogMessage(MessageImportance.Low, "Aggregating packages..."); @@ -62,7 +67,7 @@ public override bool Execute() } } - return ret; + return ret && !Log.HasLoggedErrors; } public bool Execute(string file, string msBuildVersion, string packagesDirectory, bool requireConsent, string solutionDirectory, bool disableParallelProcessing, string[] fallbackSources, bool noCache, string packageSaveMode, string[] sources, string configFile, bool nonInteractive, string verbosity, int timeout, string toolPath, bool enableOptimization, string markerPath, string[] inputs, string msbuildToolsVersion, string project, string globalProperties) diff --git a/src/MSBuildProjectBuilder/MSBuildProjectBuilder.csproj b/src/MSBuildProjectBuilder/MSBuildProjectBuilder.csproj index 98aae98..110e243 100644 --- a/src/MSBuildProjectBuilder/MSBuildProjectBuilder.csproj +++ b/src/MSBuildProjectBuilder/MSBuildProjectBuilder.csproj @@ -7,7 +7,7 @@ {69A17EA2-C5F4-44B4-AE85-95018891CDD5} Library Properties - MSBuildProjectBuilder + Microsoft.MSBuildProjectBuilder MSBuildProjectBuilder v4.5 512 @@ -39,5 +39,8 @@ + + + \ No newline at end of file diff --git a/src/MSBuildProjectBuilder/ProjectBuilder.Build.cs b/src/MSBuildProjectBuilder/ProjectBuilder.Build.cs new file mode 100644 index 0000000..9a19cdc --- /dev/null +++ b/src/MSBuildProjectBuilder/ProjectBuilder.Build.cs @@ -0,0 +1,104 @@ +using Microsoft.Build.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.MSBuildProjectBuilder +{ + public partial class ProjectBuilder + { + /// + /// Stores the logged events from the most recent build. + /// + private TestLogger _buildLog; + + /// + /// Keeps track of whether or not a build is in progress. + /// + private bool _isBuildInProgress; + + /// + /// Gets the result of the last build. If no build was performed, then null is returned. + /// + public bool? LastBuildResult { get; private set; } + + /// + /// Gets the errors logged during the last build. + /// + public IEnumerable LoggedErrors => _buildLog?.Events.Where(i => i is BuildErrorEventArgs).Cast(); + + /// + /// Gets all events logged during the last build. + /// + public IReadOnlyCollection LoggedEvents => _buildLog?.Events; + + /// + /// Gets all messages logged during the last build. + /// + public IEnumerable LoggedMessages => _buildLog?.Events.Where(i => i is BuildMessageEventArgs).Cast(); + + /// + /// Gets the text of all messages logged during the last build. + /// + public IEnumerable LoggedMessageText => LoggedMessages.Select(i => i.Message); + + /// + /// Gets all warnings logged during the last build. + /// + public IEnumerable LoggedWarnings => _buildLog?.Events.Where(i => i is BuildWarningEventArgs).Cast(); + + /// + /// Builds the current project. + /// + /// An optional list of targets to build. + /// The current object. + public ProjectBuilder Build(params string[] targets) + { + if (_isBuildInProgress) + { + throw new NotSupportedException("A build has already been started and only one build can be running at a time"); + } + + _isBuildInProgress = true; + + _buildLog = new TestLogger(); + + LastBuildResult = Project.Build(targets, new[] {_buildLog}); + + _isBuildInProgress = false; + + return this; + } + + /// + /// An implementation of which stores logged events during a build. + /// + private class TestLogger : ILogger + { + // Stores all logged events + private readonly List _events = new List(); + + /// + /// Gets the logged events. + /// + public IReadOnlyCollection Events => _events.AsReadOnly(); + + /// + public string Parameters { get; set; } + + /// + public LoggerVerbosity Verbosity { get; set; } + + /// + public void Initialize(IEventSource eventSource) + { + eventSource.AnyEventRaised += (sender, args) => _events.Add(args); + } + + /// + public void Shutdown() + { + } + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectBuilder/ProjectBuilder.Item.cs b/src/MSBuildProjectBuilder/ProjectBuilder.Item.cs index 16c742d..830b894 100644 --- a/src/MSBuildProjectBuilder/ProjectBuilder.Item.cs +++ b/src/MSBuildProjectBuilder/ProjectBuilder.Item.cs @@ -1,4 +1,5 @@ -using Microsoft.Build.Construction; +using System.IO; +using Microsoft.Build.Construction; using System.Linq; namespace Microsoft.MSBuildProjectBuilder @@ -46,6 +47,17 @@ public ProjectBuilder WithItemMetadata(params ItemMetadata[] metadatas) return this; } + public ProjectBuilder AddProjectReference(params ProjectBuilder[] projects) + { + foreach (ProjectBuilder project in projects) + { + AddItem(new Item("ProjectReference", project.FullPath)) + .WithItemMetadata($"Name={Path.GetFileNameWithoutExtension(project.FullPath)}"); + } + + return this; + } + private void AddMetadataToItem(ProjectItemElement item, ItemMetadata[] metadata) { foreach (var meta in metadata) diff --git a/src/MSBuildProjectBuilder/ProjectBuilder.cs b/src/MSBuildProjectBuilder/ProjectBuilder.cs index 3dcea70..2fbd8cb 100644 --- a/src/MSBuildProjectBuilder/ProjectBuilder.cs +++ b/src/MSBuildProjectBuilder/ProjectBuilder.cs @@ -1,23 +1,20 @@ using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using System; using System.Collections.Generic; namespace Microsoft.MSBuildProjectBuilder { public partial class ProjectBuilder { - public ProjectRootElement ProjectRoot { get; private set; } + private readonly ICollection _lastItemElements = new List(); + private readonly ICollection _lastPropertyElements = new List(); + private readonly Lazy _projectLazy; - private ICollection _lastItemElements = new List(); - - private ICollection _lastPropertyElements = new List(); - - private ProjectItemGroupElement _lastItemGroupElement = null; - - private ProjectPropertyGroupElement _lastPropertyGroupElement = null; - - private ProjectTargetElement _lastTargetElement = null; - - private ProjectElement _lastGroupContainer = null; + private ProjectElement _lastGroupContainer; + private ProjectItemGroupElement _lastItemGroupElement; + private ProjectPropertyGroupElement _lastPropertyGroupElement; + private ProjectTargetElement _lastTargetElement; private ProjectBuilder(string toolsVersion, string defaultTargets, string initialTargets, string label) { @@ -27,8 +24,27 @@ private ProjectBuilder(string toolsVersion, string defaultTargets, string initia ProjectRoot.ToolsVersion = toolsVersion ?? ProjectRoot.ToolsVersion; ProjectRoot.Label = label ?? string.Empty; _lastGroupContainer = ProjectRoot; + + _projectLazy = new Lazy(() => new Project(ProjectRoot), isThreadSafe: true); } - } + /// + /// Gets the full path to the project file. + /// + public string FullPath => ProjectRoot.FullPath; -} + public Project Project + { + get + { + Project project = _projectLazy.Value; + + project.ReevaluateIfNecessary(); + + return project; + } + } + + public ProjectRootElement ProjectRoot { get; } + } +} \ No newline at end of file