diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectContextBuilder.cs b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectContextBuilder.cs index 63551e8..2216860 100644 --- a/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectContextBuilder.cs +++ b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectContextBuilder.cs @@ -9,9 +9,10 @@ using System.Xml; using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; +using Microsoft.Build.Framework; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders.Physical; -using Microsoft.Build.Framework; +using Microsoft.Extensions.ProjectModel.Internal; using NuGet.Frameworks; namespace Microsoft.Extensions.ProjectModel @@ -22,6 +23,7 @@ namespace Microsoft.Extensions.ProjectModel private IFileInfo _fileInfo; private string[] _buildTargets; private Dictionary _globalProperties = new Dictionary(); + private bool _explicitMsBuild; public MsBuildProjectContextBuilder() { @@ -46,7 +48,7 @@ namespace Microsoft.Extensions.ProjectModel return this; } - public MsBuildProjectContextBuilder WithDesignTimeBuild() + public MsBuildProjectContextBuilder AsDesignTimeBuild() { // don't to expensive things WithProperty("DesignTimeBuild", "true"); @@ -55,16 +57,12 @@ namespace Microsoft.Extensions.ProjectModel return this; } - public MsBuildProjectContextBuilder WithMsBuild(MsBuildContext context) + // should be needed in most cases, but can be used to override + public MsBuildProjectContextBuilder UseMsBuild(MsBuildContext context) { - /* - Workaround https://github.com/Microsoft/msbuild/issues/999 - Error: System.TypeInitializationException : The type initializer for 'BuildEnvironmentHelperSingleton' threw an exception. - Could not determine a valid location to MSBuild. Try running this process from the Developer Command Prompt for Visual Studio. - */ - - Environment.SetEnvironmentVariable("MSBUILD_EXE_PATH", context.MsBuildExecutableFullPath); - return WithProperty("MSBuildExtensionsPath", context.ExtensionsPath); + _explicitMsBuild = true; + SetMsBuildContext(context); + return this; } public MsBuildProjectContextBuilder WithProperty(string property, string value) @@ -86,6 +84,13 @@ namespace Microsoft.Extensions.ProjectModel public MsBuildProjectContextBuilder WithProjectFile(IFileInfo fileInfo) { + if (!_explicitMsBuild) + { + var projectDir = Path.GetDirectoryName(fileInfo.PhysicalPath); + var sdk = DotNetCoreSdkResolver.DefaultResolver.ResolveProjectSdk(projectDir); + SetMsBuildContext(MsBuildContext.FromDotNetSdk(sdk)); + } + _fileInfo = fileInfo; return this; } @@ -109,7 +114,6 @@ namespace Microsoft.Extensions.ProjectModel protected virtual void Initialize() { WithBuildTargets(new[] { "ResolveReferences" }); - WithMsBuild(MsBuildContext.FromCurrentDotNetSdk()); WithProperty("_ResolveReferenceDependencies", "true"); } @@ -163,6 +167,18 @@ namespace Microsoft.Extensions.ProjectModel throw new InvalidOperationException(sb.ToString()); } + private void SetMsBuildContext(MsBuildContext context) + { + /* + Workaround https://github.com/Microsoft/msbuild/issues/999 + Error: System.TypeInitializationException : The type initializer for 'BuildEnvironmentHelperSingleton' threw an exception. + Could not determine a valid location to MSBuild. Try running this process from the Developer Command Prompt for Visual Studio. + */ + + Environment.SetEnvironmentVariable("MSBUILD_EXE_PATH", context.MsBuildExecutableFullPath); + WithProperty("MSBuildExtensionsPath", context.ExtensionsPath); + } + private class InMemoryLogger : ILogger { private readonly Stack _onShutdown = new Stack(); diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectFinder.cs b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectFinder.cs new file mode 100644 index 0000000..3b43918 --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectFinder.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +// TODO if this becomes a true API instead of .Sources package, put strings into resource file + +namespace Microsoft.Extensions.ProjectModel +{ + internal class MsBuildProjectFinder + { + private readonly string _directory; + + public MsBuildProjectFinder(string directory) + { + if (string.IsNullOrEmpty(directory)) + { + throw new ArgumentException("Value cannot be null or empty", nameof(directory)); + } + + _directory = directory; + } + + public string FindMsBuildProject(string project = null) + { + var projectPath = project ?? _directory; + + if (!Path.IsPathRooted(projectPath)) + { + projectPath = Path.Combine(_directory, projectPath); + } + + if (Directory.Exists(projectPath)) + { + var projects = FindProjectFiles(projectPath).ToList(); + if (projects.Count > 1) + { + throw MultipleProjectsFound(projectPath); + } + + if (projects.Count == 0) + { + throw NoProjectsFound(projectPath); + } + + return projects[0]; + } + + if (!File.Exists(projectPath)) + { + throw FileDoesNotExist(projectPath); + } + + return projectPath; + } + + protected virtual Exception FileDoesNotExist(string filePath) + => new InvalidOperationException($"No file was found at '{filePath}'."); + + protected virtual Exception MultipleProjectsFound(string directory) + => new InvalidOperationException($"Multiple MSBuild project files found in '{directory}'."); + + protected virtual Exception NoProjectsFound(string directory) + => new InvalidOperationException($"Could not find a MSBuild project file in '{directory}'."); + + protected virtual IEnumerable FindProjectFiles(string directory) + => Directory.EnumerateFileSystemEntries(directory, "*.*proj", SearchOption.TopDirectoryOnly); + } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildProjectContextBuilderTest.cs b/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildProjectContextBuilderTest.cs index 41cfba1..118842e 100644 --- a/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildProjectContextBuilderTest.cs +++ b/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildProjectContextBuilderTest.cs @@ -3,8 +3,11 @@ using System.IO; using System.Linq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.ProjectModel.Tests; using NuGet.Frameworks; using Xunit; +using Xunit.Abstractions; namespace Microsoft.Extensions.ProjectModel { @@ -13,10 +16,12 @@ namespace Microsoft.Extensions.ProjectModel private const string SkipReason = "CI doesn't yet have a new enough version of .NET Core SDK"; private readonly MsBuildFixture _fixture; + private readonly ITestOutputHelper _output; - public MsBuildProjectContextBuilderTest(MsBuildFixture fixture) + public MsBuildProjectContextBuilderTest(MsBuildFixture fixture, ITestOutputHelper output) { _fixture = fixture; + _output = output; } [Fact(Skip = SkipReason)] @@ -24,8 +29,17 @@ namespace Microsoft.Extensions.ProjectModel { using (var fileProvider = new TemporaryFileProvider()) { - // TODO When .NET Core SDK is available, detect and add to this test project - // fileProvider.Add("test.nuget.targets", "Import .NET Core SDK here"); + // TODO remove when SDK becomes available on other feeds + fileProvider.Add("NuGet.config", @" + + + + + + + +"); + fileProvider.Add("test.csproj", @" @@ -34,14 +48,23 @@ namespace Microsoft.Extensions.ProjectModel Microsoft.TestProject TestProject Library - .NETCoreApp - v1.0 + netcoreapp1.0 + bin\$(Configuration) + + + 1.0.0-* + + + 1.0.1 + + + "); @@ -51,22 +74,28 @@ namespace Microsoft.Extensions.ProjectModel var testContext = _fixture.GetMsBuildContext(); + var muxer = Path.Combine(testContext.ExtensionsPath, "../..", "dotnet.exe"); + var result = Command + .Create(muxer, new[] { "restore3", Path.Combine(fileProvider.Root, "test.csproj") }) + .OnErrorLine(l => _output.WriteLine(l)) + .OnOutputLine(l => _output.WriteLine(l)) + .Execute(); + Assert.Equal(0, result.ExitCode); + var expectedCompileItems = new[] { "One.cs", "Two.cs" }.Select(p => Path.Combine(fileProvider.Root, p)).ToArray(); var builder = new MsBuildProjectContextBuilder() - .WithMsBuild(testContext) - .WithDesignTimeBuild() - // In latest version of MSBuild, setting this property causes evaluation errors when SDK is not available - //.WithConfiguration("Debug") + .AsDesignTimeBuild() + .UseMsBuild(testContext) + .WithTargetFramework(FrameworkConstants.CommonFrameworks.NetCoreApp10) + .WithConfiguration("Debug") .WithProjectFile(fileProvider.GetFileInfo("test.csproj")); - // TODO remove ignoreBuildErrors flag - // this always throws because Microsoft.NETCore.SDK is not available. - var context = builder.Build(ignoreBuildErrors: true); + var context = builder.Build(); Assert.False(fileProvider.GetFileInfo("bin").Exists); Assert.False(fileProvider.GetFileInfo("obj").Exists); Assert.Equal(expectedCompileItems, context.CompilationItems.OrderBy(i => i).ToArray()); - Assert.Equal(Path.Combine(fileProvider.Root, "bin", "Debug", "test.dll"), context.AssemblyFullPath); + Assert.Equal(Path.Combine(fileProvider.Root, "bin", "Debug", "netcoreapp1.0", "test.dll"), context.AssemblyFullPath); Assert.True(context.IsClassLibrary); Assert.Equal("TestProject", context.ProjectName); Assert.Equal(FrameworkConstants.CommonFrameworks.NetCoreApp10, context.TargetFramework); diff --git a/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildProjectFinderTest.cs b/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildProjectFinderTest.cs new file mode 100644 index 0000000..3d4155c --- /dev/null +++ b/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildProjectFinderTest.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Xunit; + +namespace Microsoft.Extensions.ProjectModel.MsBuild +{ + public class MsBuildProjectFinderTest + { + [Theory] + [InlineData(".csproj")] + [InlineData(".vbproj")] + [InlineData(".fsproj")] + public void FindsSingleProject(string extension) + { + using (var files = new TemporaryFileProvider()) + { + var filename = "TestProject" + extension; + files.Add(filename, ""); + + var finder = new MsBuildProjectFinder(files.Root); + + Assert.Equal(Path.Combine(files.Root, filename), finder.FindMsBuildProject()); + } + } + + [Fact] + public void ThrowsWhenNoFile() + { + using (var files = new TemporaryFileProvider()) + { + var finder = new MsBuildProjectFinder(files.Root); + + Assert.Throws(() => finder.FindMsBuildProject()); + } + } + + [Fact] + public void ThrowsWhenMultipleFile() + { + using (var files = new TemporaryFileProvider()) + { + files.Add("Test1.csproj", ""); + files.Add("Test2.csproj", ""); + var finder = new MsBuildProjectFinder(files.Root); + + Assert.Throws(() => finder.FindMsBuildProject()); + } + } + + [Fact] + public void ThrowsWhenFileDoesNotExist() + { + using (var files = new TemporaryFileProvider()) + { + var finder = new MsBuildProjectFinder(files.Root); + + Assert.Throws(() => finder.FindMsBuildProject("test.csproj")); + } + } + + [Fact] + public void ThrowsWhenRootDoesNotExist() + { + var files = new TemporaryFileProvider(); + var finder = new MsBuildProjectFinder(files.Root); + files.Dispose(); + Assert.Throws(() => finder.FindMsBuildProject()); + } + } +} diff --git a/test/Microsoft.Extensions.ProjectModel.Tests/project.json b/test/Microsoft.Extensions.ProjectModel.Tests/project.json index 6b0b647..2e1015d 100644 --- a/test/Microsoft.Extensions.ProjectModel.Tests/project.json +++ b/test/Microsoft.Extensions.ProjectModel.Tests/project.json @@ -1,7 +1,10 @@ { "buildOptions": { "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk" + "keyFile": "../../tools/Key.snk", + "compile": { + "include": "../Shared/*.cs" + } }, "dependencies": { "NuGet.Frameworks": "3.5.0-*", @@ -13,7 +16,6 @@ "type": "build", "version": "1.0.0-*" }, - "System.Runtime.InteropServices.RuntimeInformation": "4.0.0", "dotnet-test-xunit": "2.2.0-*", "xunit": "2.2.0-*" }, diff --git a/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildFixture.cs b/test/Shared/MsBuildFixture.cs similarity index 83% rename from test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildFixture.cs rename to test/Shared/MsBuildFixture.cs index 9a7ae4d..b8d1b8c 100644 --- a/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildFixture.cs +++ b/test/Shared/MsBuildFixture.cs @@ -6,12 +6,14 @@ using System.IO; using Microsoft.Extensions.ProjectModel.Internal; using NuGet.Versioning; -namespace Microsoft.Extensions.ProjectModel +namespace Microsoft.Extensions.ProjectModel.Tests { public class MsBuildFixture { - private readonly SemanticVersion _minMsBuildVersion = SemanticVersion.Parse("1.0.0-preview3-00000"); + // TODO remove this when preview3 stabilizies + private readonly SemanticVersion _minMsBuildVersion = SemanticVersion.Parse("1.0.0-preview3-003748"); + // TODO remove when our CI updates to using MSBuild or when Microsoft/msbuild#4213 is resolved internal MsBuildContext GetMsBuildContext() { // for CI