diff --git a/DotNetTools.sln b/DotNetTools.sln index c68103961f..44d6a3dac3 100644 --- a/DotNetTools.sln +++ b/DotNetTools.sln @@ -38,6 +38,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{59E02BDF EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "NuGetPackager", "tools\NuGetPackager\NuGetPackager.xproj", "{8B781D87-1FC3-4A34-9089-2BDF6B562B85}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.ProjectModel.Sources", "src\Microsoft.Extensions.ProjectModel.Sources\Microsoft.Extensions.ProjectModel.Sources.xproj", "{99D6CE89-7302-4C3A-83EB-D534C24644D2}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.ProjectModel.Tests", "test\Microsoft.Extensions.ProjectModel.Tests\Microsoft.Extensions.ProjectModel.Tests.xproj", "{1A66A831-4F06-46D9-B483-70A4E75A2F7F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -88,6 +92,14 @@ Global {8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Release|Any CPU.Build.0 = Release|Any CPU + {99D6CE89-7302-4C3A-83EB-D534C24644D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99D6CE89-7302-4C3A-83EB-D534C24644D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99D6CE89-7302-4C3A-83EB-D534C24644D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99D6CE89-7302-4C3A-83EB-D534C24644D2}.Release|Any CPU.Build.0 = Release|Any CPU + {1A66A831-4F06-46D9-B483-70A4E75A2F7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A66A831-4F06-46D9-B483-70A4E75A2F7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A66A831-4F06-46D9-B483-70A4E75A2F7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A66A831-4F06-46D9-B483-70A4E75A2F7F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -105,5 +117,7 @@ Global {8A2E6961-6B12-4A8E-8215-3E7301D52EAC} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} {53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4} {8B781D87-1FC3-4A34-9089-2BDF6B562B85} = {59E02BDF-98DE-4D64-B576-2D0299D5E052} + {99D6CE89-7302-4C3A-83EB-D534C24644D2} = {66517987-2A5A-4330-B130-207039378FD4} + {1A66A831-4F06-46D9-B483-70A4E75A2F7F} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} EndGlobalSection EndGlobal diff --git a/NuGet.config b/NuGet.config index 0fd623ffdd..18b373abe1 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,6 +1,7 @@  + diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index aa4710b372..d0e3b370e2 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -30,6 +30,12 @@ } } }, + "adx-nonshipping": { + "rules": [], + "packages": { + "Microsoft.Extensions.ProjectModel.Sources": {} + } + }, "Default": { "rules": [ "DefaultCompositeRule" diff --git a/global.json b/global.json index 553b2244a3..1920b5a762 100644 --- a/global.json +++ b/global.json @@ -1,3 +1,3 @@ { - "projects": [ "src"] + "projects": [ "src", "test"] } diff --git a/makefile.shade b/makefile.shade index 83ca855489..f957f23173 100644 --- a/makefile.shade +++ b/makefile.shade @@ -21,5 +21,11 @@ k-standard-goals "-n src/Microsoft.Extensions.SecretManager.Tools/Microsoft.Extensions.SecretManager.Tools.nuspec " + "-n src/Microsoft.Extensions.Caching.SqlConfig.Tools/Microsoft.Extensions.Caching.SqlConfig.Tools.nuspec " + "-n src/Microsoft.DotNet.Watcher.Tools/Microsoft.DotNet.Watcher.Tools.nuspec "); + + DotnetPack("src/Microsoft.Extensions.ProjectModel.Sources/project.json", BUILD_DIR_LOCAL, E("Configuration"), E("KOREBUILD_DOTNET_PACK_OPTIONS") + " --no-build"); + foreach (var nupkg in Files.Include(Path.Combine(BUILD_DIR_LOCAL, "*/" + E("Configuration") + "/*.nupkg"))) + { + File.Copy(nupkg, Path.Combine(BUILD_DIR_LOCAL, Path.GetFileName(nupkg)), true); + } } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/DotNet/DotNetProjectContext.cs b/src/Microsoft.Extensions.ProjectModel.Sources/DotNet/DotNetProjectContext.cs new file mode 100644 index 0000000000..53d8b3939f --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/DotNet/DotNetProjectContext.cs @@ -0,0 +1,102 @@ +// 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 Microsoft.DotNet.ProjectModel; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NuGet.Frameworks; + +namespace Microsoft.Extensions.ProjectModel +{ + internal class DotNetProjectContext : IProjectContext + { + private readonly ProjectContext _projectContext; + private readonly OutputPaths _paths; + private readonly Lazy _rawProject; + private readonly CommonCompilerOptions _compilerOptions; + + public DotNetProjectContext(ProjectContext projectContext, string configuration, string outputPath) + { + if (projectContext == null) + { + throw new ArgumentNullException(nameof(projectContext)); + } + + if (string.IsNullOrEmpty(configuration)) + { + throw new ArgumentNullException(nameof(configuration)); + } + + _rawProject = new Lazy(() => + { + using (var stream = new FileStream(projectContext.ProjectFile.ProjectFilePath, FileMode.Open, FileAccess.Read)) + using (var streamReader = new StreamReader(stream)) + using (var jsonReader = new JsonTextReader(streamReader)) + { + return JObject.Load(jsonReader); + } + }); + + Configuration = configuration; + _projectContext = projectContext; + + _paths = projectContext.GetOutputPaths(configuration, buidBasePath: null, outputPath: outputPath); + _compilerOptions = _projectContext.ProjectFile.GetCompilerOptions(TargetFramework, Configuration); + + // Workaround https://github.com/dotnet/cli/issues/3164 + IsClassLibrary = !(_compilerOptions.EmitEntryPoint + ?? projectContext.ProjectFile.GetCompilerOptions(null, configuration).EmitEntryPoint.GetValueOrDefault()); + } + + public bool IsClassLibrary { get; } + + public NuGetFramework TargetFramework => _projectContext.TargetFramework; + public string Config => _paths.RuntimeFiles.Config; + public string DepsJson => _paths.RuntimeFiles.DepsJson; + public string RuntimeConfigJson => _paths.RuntimeFiles.RuntimeConfigJson; + public string PackagesDirectory => _projectContext.PackagesDirectory; + + public string AssemblyFullPath => + !IsClassLibrary && (_projectContext.IsPortable || TargetFramework.IsDesktop()) + ? _paths.RuntimeFiles.Executable + : _paths.RuntimeFiles.Assembly; + + public string Configuration { get; } + public string ProjectFullPath => _projectContext.ProjectFile.ProjectFilePath; + public string ProjectName => _projectContext.ProjectFile.Name; + // TODO read from xproj if available + public string RootNamespace => _projectContext.ProjectFile.Name; + public string TargetDirectory => _paths.RuntimeOutputPath; + public string Platform => _compilerOptions.Platform; + + public IEnumerable CompilationItems + => _compilerOptions.CompileInclude.ResolveFiles(); + + public IEnumerable EmbededItems + => _compilerOptions.EmbedInclude.ResolveFiles(); + + /// + /// Returns string values of top-level keys in the project.json file + /// + /// + /// + /// + public string FindProperty(string propertyName) => FindProperty(propertyName); + + public TProperty FindProperty(string propertyName) + { + foreach (var item in _rawProject.Value) + { + if (item.Key.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) + { + return item.Value.Value(); + } + } + + return default(TProperty); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/DotNet/IncludeContextExtensions.cs b/src/Microsoft.Extensions.ProjectModel.Sources/DotNet/IncludeContextExtensions.cs new file mode 100644 index 0000000000..938d84ed5c --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/DotNet/IncludeContextExtensions.cs @@ -0,0 +1,25 @@ +// 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.Linq; +using Microsoft.DotNet.ProjectModel.Files; + +namespace Microsoft.Extensions.ProjectModel +{ + internal static class IncludeContextExtensions + { + public static IEnumerable ResolveFiles(this IncludeContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return IncludeFilesResolver + .GetIncludeFiles(context, "/", diagnostics: null) + .Select(f => f.SourcePath); + } + } +} diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/IProjectContext.cs b/src/Microsoft.Extensions.ProjectModel.Sources/IProjectContext.cs new file mode 100644 index 0000000000..64af21c0db --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/IProjectContext.cs @@ -0,0 +1,28 @@ +// 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.Collections.Generic; +using NuGet.Frameworks; + +namespace Microsoft.Extensions.ProjectModel +{ + public interface IProjectContext + { + string ProjectName { get; } + string Configuration { get; } + string Platform { get; } + string ProjectFullPath { get; } + string RootNamespace { get; } + bool IsClassLibrary { get; } + NuGetFramework TargetFramework { get; } + string Config { get; } + string DepsJson { get; } + string RuntimeConfigJson { get; } + string PackagesDirectory { get; } + string TargetDirectory { get; } + string AssemblyFullPath { get; } + IEnumerable CompilationItems { get; } + IEnumerable EmbededItems { get; } + string FindProperty(string propertyName); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/Internal/DotNetCoreSdk.cs b/src/Microsoft.Extensions.ProjectModel.Sources/Internal/DotNetCoreSdk.cs new file mode 100644 index 0000000000..40dd59d6d9 --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/Internal/DotNetCoreSdk.cs @@ -0,0 +1,11 @@ +// 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. + +namespace Microsoft.Extensions.ProjectModel.Internal +{ + internal class DotNetCoreSdk + { + public string BasePath { get; set; } + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/Internal/DotNetCoreSdkResolver.cs b/src/Microsoft.Extensions.ProjectModel.Sources/Internal/DotNetCoreSdkResolver.cs new file mode 100644 index 0000000000..684654674f --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/Internal/DotNetCoreSdkResolver.cs @@ -0,0 +1,129 @@ +// 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; +using Microsoft.DotNet.Cli.Utils; +using Newtonsoft.Json; +using NuGet.Versioning; + +namespace Microsoft.Extensions.ProjectModel.Internal +{ + internal class DotNetCoreSdkResolver + { + private readonly string _installationDir; + + /// + /// Represents a resolver that uses the currently executing to find the .NET Core SDK installation + /// + public static readonly DotNetCoreSdkResolver DefaultResolver = new DotNetCoreSdkResolver(Path.GetDirectoryName(new Muxer().MuxerPath)); + + /// + /// Instantiates a resolver that locates the SDK + /// + /// The directory containing dotnet muxer, aka DOTNET_HOME + public DotNetCoreSdkResolver(string installationDir) + { + _installationDir = installationDir; + } + + /// + /// Find the latest SDK installation (uses SemVer 1.0 to determine what is "latest") + /// + /// Path to SDK root directory + public DotNetCoreSdk ResolveLatest() + { + var latest = FindInstalled() + .Select(d => new { path = d, version = SemanticVersion.Parse(Path.GetFileName(d)) }) + .OrderByDescending(sdk => sdk.version) + .FirstOrDefault(); + + if (latest == null) + { + throw CreateSdkNotInstalledException(); + } + + return new DotNetCoreSdk + { + BasePath = latest.path, + Version = latest.version.ToFullString() + }; + } + + public DotNetCoreSdk ResolveProjectSdk(string projectDir) + { + var sdkVersion = ResolveGlobalJsonSdkVersion(projectDir); + if (string.IsNullOrEmpty(sdkVersion)) + { + return ResolveLatest(); + } + + var sdk = FindInstalled() + .Where(p => Path.GetFileName(p).Equals(sdkVersion, StringComparison.OrdinalIgnoreCase)) + .Select(d => new { path = d, version = SemanticVersion.Parse(Path.GetFileName(d)) }) + .FirstOrDefault(); + + if (sdk == null) + { + throw CreateSdkNotInstalledException(); + } + + return new DotNetCoreSdk + { + BasePath = sdk.path, + Version = sdk.version.ToFullString() + }; + } + + private Exception CreateSdkNotInstalledException() + { + return new DirectoryNotFoundException($"Could not find an installation of the .NET Core SDK in '{_installationDir}'"); + } + + private IEnumerable FindInstalled() + => Directory.EnumerateDirectories(Path.Combine(_installationDir, "sdk")); + + private string ResolveGlobalJsonSdkVersion(string start) + { + var dir = new DirectoryInfo(start); + FileInfo fileInfo = null; + while (dir != null) + { + var candidate = Path.Combine(dir.FullName, "global.json"); + if (File.Exists(candidate)) + { + fileInfo = new FileInfo(candidate); + break; + } + dir = dir.Parent; + } + if (fileInfo == null) + { + return null; + } + try + { + var contents = File.ReadAllText(fileInfo.FullName); + var globalJson = JsonConvert.DeserializeObject(contents); + return globalJson?.sdk?.version; + } + catch (JsonException) + { + // TODO log + return null; + } + } + + private class GlobalJsonStub + { + public GlobalJsonSdkStub sdk { get; set; } + + public class GlobalJsonSdkStub + { + public string version { get; set; } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/Microsoft.Extensions.ProjectModel.Sources.xproj b/src/Microsoft.Extensions.ProjectModel.Sources/Microsoft.Extensions.ProjectModel.Sources.xproj new file mode 100644 index 0000000000..772d9c1c91 --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/Microsoft.Extensions.ProjectModel.Sources.xproj @@ -0,0 +1,18 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 99d6ce89-7302-4c3a-83eb-d534c24644d2 + Microsoft.Extensions.ProjectModel + .\obj + .\bin\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildContext.cs b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildContext.cs new file mode 100644 index 0000000000..4b50ca089e --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildContext.cs @@ -0,0 +1,39 @@ +// 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 Microsoft.Extensions.ProjectModel.Internal; + +namespace Microsoft.Extensions.ProjectModel +{ + /// + /// Represents the msbuild context used to parse a project model + /// + internal class MsBuildContext + { + public string MsBuildExecutableFullPath { get; private set; } + public string ExtensionsPath { get; private set; } + + public static MsBuildContext FromCurrentDotNetSdk() + { + var sdk = DotNetCoreSdkResolver.DefaultResolver.ResolveLatest(); + return FromDotNetSdk(sdk); + } + + public static MsBuildContext FromDotNetSdk(DotNetCoreSdk sdk) + { + if (sdk == null) + { + throw new ArgumentNullException(nameof(sdk)); + } + + return new MsBuildContext + { + // might change... See https://github.com/Microsoft/msbuild/issues/1136 + MsBuildExecutableFullPath = Path.Combine(sdk.BasePath, "MSBuild.exe"), + ExtensionsPath = sdk.BasePath + }; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildExtensions.cs b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildExtensions.cs new file mode 100644 index 0000000000..eef3bef01c --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildExtensions.cs @@ -0,0 +1,14 @@ +// 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.Linq; + +namespace Microsoft.Build.Execution +{ + internal static class MsBuildExtensions + { + public static string FindProperty(this ProjectInstance projectInstance, string propertyName, StringComparison comparer) + => projectInstance.Properties.FirstOrDefault(p => p.Name.Equals(propertyName, comparer))?.EvaluatedValue; + } +} diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectContext.cs b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectContext.cs new file mode 100644 index 0000000000..d8ca3020bb --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectContext.cs @@ -0,0 +1,59 @@ +// 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 Microsoft.Build.Execution; +using NuGet.Frameworks; +using System.Linq; + +namespace Microsoft.Extensions.ProjectModel +{ + internal class MsBuildProjectContext : IProjectContext + { + private const string CompileItemName = "Compile"; + private const string EmbedItemName = "EmbeddedResource"; + private const string FullPathMetadataName = "FullPath"; + + private readonly ProjectInstance _project; + private readonly string _name; + + public MsBuildProjectContext(string name, string configuration, ProjectInstance project) + { + _project = project; + + Configuration = configuration; + _name = name; + } + + public string FindProperty(string propertyName) + { + return _project.GetProperty(propertyName)?.EvaluatedValue; + } + + public string ProjectName => FindProperty("ProjectName") ?? _name; + public string Configuration { get; } + + public NuGetFramework TargetFramework => NuGetFramework.Parse(FindProperty("NuGetTargetMoniker")); + public bool IsClassLibrary => FindProperty("OutputType").Equals("Library", StringComparison.OrdinalIgnoreCase); + + // TODO get from actual properties according to TFM + public string Config => AssemblyFullPath + ".config"; + public string DepsJson => Path.Combine(TargetDirectory, Path.GetFileNameWithoutExtension(AssemblyFullPath) + ".deps.json"); + public string RuntimeConfigJson => Path.Combine(TargetDirectory, Path.GetFileNameWithoutExtension(AssemblyFullPath) + ".runtimeconfig.json"); + + public string PackagesDirectory => FindProperty("NuGetPackageRoot"); + public string AssemblyFullPath => FindProperty("TargetPath"); + public string Platform => FindProperty("Platform"); + public string ProjectFullPath => _project.FullPath; + public string RootNamespace => FindProperty("RootNamespace") ?? ProjectName; + public string TargetDirectory => FindProperty("TargetDir"); + + public IEnumerable CompilationItems + => _project.GetItems(CompileItemName).Select(i => i.GetMetadataValue(FullPathMetadataName)); + + public IEnumerable EmbededItems + => _project.GetItems(EmbedItemName).Select(i => i.GetMetadataValue(FullPathMetadataName)); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectContextBuilder.cs b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectContextBuilder.cs new file mode 100644 index 0000000000..63551e8aa6 --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/MsBuild/MsBuildProjectContextBuilder.cs @@ -0,0 +1,198 @@ +// 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.Text; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; +using Microsoft.Build.Framework; +using NuGet.Frameworks; + +namespace Microsoft.Extensions.ProjectModel +{ + internal class MsBuildProjectContextBuilder + { + private string _configuration; + private IFileInfo _fileInfo; + private string[] _buildTargets; + private Dictionary _globalProperties = new Dictionary(); + + public MsBuildProjectContextBuilder() + { + Initialize(); + } + + public MsBuildProjectContextBuilder WithBuildTargets(string[] targets) + { + if (targets == null) + { + throw new ArgumentNullException(nameof(targets)); + } + + _buildTargets = targets; + return this; + } + + public MsBuildProjectContextBuilder WithConfiguration(string configuration) + { + _configuration = configuration; + WithProperty("Configuration", configuration); + return this; + } + + public MsBuildProjectContextBuilder WithDesignTimeBuild() + { + // don't to expensive things + WithProperty("DesignTimeBuild", "true"); + WithProperty("_ResolveReferenceDependencies", "true"); + WithProperty("BuildProjectReferences", "false"); + return this; + } + + public MsBuildProjectContextBuilder WithMsBuild(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); + } + + public MsBuildProjectContextBuilder WithProperty(string property, string value) + { + _globalProperties[property] = value; + return this; + } + + public MsBuildProjectContextBuilder WithProjectFile(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentNullException(nameof(filePath)); + } + + var fileInfo = new PhysicalFileInfo(new FileInfo(filePath)); + return WithProjectFile(fileInfo); + } + + public MsBuildProjectContextBuilder WithProjectFile(IFileInfo fileInfo) + { + _fileInfo = fileInfo; + return this; + } + + public MsBuildProjectContextBuilder WithTargetFramework(NuGetFramework framework) + => WithTargetFramework(framework.GetShortFolderName()); + + public MsBuildProjectContextBuilder WithTargetFramework(string framework) + => WithProperty("TargetFramework", framework); + + public virtual MsBuildProjectContext Build(bool ignoreBuildErrors = false) + { + var projectCollection = CreateProjectCollection(); + var project = CreateProject(_fileInfo, _configuration, _globalProperties, projectCollection); + var projectInstance = CreateProjectInstance(project, _buildTargets, ignoreBuildErrors); + + var name = Path.GetFileNameWithoutExtension(_fileInfo.Name); + return new MsBuildProjectContext(name, _configuration, projectInstance); + } + + protected virtual void Initialize() + { + WithBuildTargets(new[] { "ResolveReferences" }); + WithMsBuild(MsBuildContext.FromCurrentDotNetSdk()); + WithProperty("_ResolveReferenceDependencies", "true"); + } + + protected virtual ProjectCollection CreateProjectCollection() => new ProjectCollection(); + + protected virtual Project CreateProject(IFileInfo fileInfo, + string configuration, + IDictionary globalProps, + ProjectCollection projectCollection) + { + using (var stream = fileInfo.CreateReadStream()) + { + var xmlReader = XmlReader.Create(stream); + + var xml = ProjectRootElement.Create(xmlReader, projectCollection, preserveFormatting: true); + xml.FullPath = fileInfo.PhysicalPath; + + return new Project(xml, globalProps, toolsVersion: null, projectCollection: projectCollection); + } + } + + protected virtual ProjectInstance CreateProjectInstance(Project project, string[] targets, bool ignoreErrors) + { + var projectInstance = project.CreateProjectInstance(); + if (targets.Length == 0) + { + return projectInstance; + } + + var logger = new InMemoryLogger(); + projectInstance.Build(targets, new[] { logger }); + + if (!ignoreErrors && logger.Errors.Count > 0) + { + throw CreateBuildFailedException(project.FullPath, logger.Errors); + } + + return projectInstance; + } + + private Exception CreateBuildFailedException(string filePath, IList errors) + { + var sb = new StringBuilder(); + sb.AppendLine($"Building '{filePath}' failed."); + + for (var i = 0; i < errors.Count; i++) + { + sb.Append(i).Append(" :").AppendLine(errors[i].Message); + } + + throw new InvalidOperationException(sb.ToString()); + } + + private class InMemoryLogger : ILogger + { + private readonly Stack _onShutdown = new Stack(); + + internal IList Errors = new List(); + + public string Parameters { get; set; } + public LoggerVerbosity Verbosity { get; set; } + + public void Initialize(IEventSource eventSource) + { + eventSource.ErrorRaised += OnError; + _onShutdown.Push(() => + { + eventSource.ErrorRaised -= OnError; + }); + } + + private void OnError(object sender, BuildErrorEventArgs e) + { + Errors.Add(e); + } + + public void Shutdown() + { + while (_onShutdown.Count > 0) + { + _onShutdown.Pop()?.Invoke(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.ProjectModel.Sources/project.json b/src/Microsoft.Extensions.ProjectModel.Sources/project.json new file mode 100644 index 0000000000..23d8c31849 --- /dev/null +++ b/src/Microsoft.Extensions.ProjectModel.Sources/project.json @@ -0,0 +1,7 @@ +{ + "version": "1.0.0-*", + "shared": "**/*.cs", + "frameworks": { + "netstandard1.0": {} + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj b/test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj index 3a1a6e45b7..58c2a08157 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj @@ -11,9 +11,11 @@ .\obj .\bin\ - 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.Extensions.ProjectModel.Tests/Microsoft.Extensions.ProjectModel.Tests.xproj b/test/Microsoft.Extensions.ProjectModel.Tests/Microsoft.Extensions.ProjectModel.Tests.xproj new file mode 100644 index 0000000000..a056486759 --- /dev/null +++ b/test/Microsoft.Extensions.ProjectModel.Tests/Microsoft.Extensions.ProjectModel.Tests.xproj @@ -0,0 +1,21 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 1a66a831-4f06-46d9-b483-70a4e75a2f7f + Microsoft.Extensions.ProjectModel + .\obj + .\bin\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/DotNetCoreSdkResolverTest.cs b/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/DotNetCoreSdkResolverTest.cs new file mode 100644 index 0000000000..344cb20777 --- /dev/null +++ b/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/DotNetCoreSdkResolverTest.cs @@ -0,0 +1,57 @@ +// 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 Microsoft.Extensions.ProjectModel.Internal; +using Xunit; + +namespace Microsoft.Extensions.ProjectModel +{ + public class DotNetCoreSdkResolverTest : IDisposable + { + private readonly string _fakeInstallDir; + public DotNetCoreSdkResolverTest() + { + _fakeInstallDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_fakeInstallDir); + Directory.CreateDirectory(Path.Combine(_fakeInstallDir, "sdk")); + } + + [Fact] + public void ResolveLatest() + { + var project = Path.Combine(_fakeInstallDir, "project"); + Directory.CreateDirectory(project); + Directory.CreateDirectory(Path.Combine(_fakeInstallDir, "sdk", "1.0.1")); + Directory.CreateDirectory(Path.Combine(_fakeInstallDir, "sdk", "1.0.0")); + Directory.CreateDirectory(Path.Combine(_fakeInstallDir, "sdk", "1.0.0-beta1")); + var sdk = new DotNetCoreSdkResolver(_fakeInstallDir).ResolveLatest(); + Assert.Equal("1.0.1", sdk.Version); + Assert.Equal(Path.Combine(_fakeInstallDir, "sdk", "1.0.1"), sdk.BasePath); + } + + [Fact] + public void ResolveProjectSdk() + { + var project = Path.Combine(_fakeInstallDir, "project"); + Directory.CreateDirectory(project); + Directory.CreateDirectory(Path.Combine(_fakeInstallDir, "sdk", "1.0.0")); + Directory.CreateDirectory(Path.Combine(_fakeInstallDir, "sdk", "1.0.0-abc-123")); + Directory.CreateDirectory(Path.Combine(_fakeInstallDir, "sdk", "1.0.0-xyz-123")); + File.WriteAllText(Path.Combine(_fakeInstallDir, "global.json"), @"{ + ""sdk"": { + ""version"": ""1.0.0-abc-123"" + } + }"); + var sdk = new DotNetCoreSdkResolver(_fakeInstallDir).ResolveProjectSdk(project); + Assert.Equal("1.0.0-abc-123", sdk.Version); + Assert.Equal(Path.Combine(_fakeInstallDir, "sdk", "1.0.0-abc-123"), sdk.BasePath); + } + + public void Dispose() + { + Directory.Delete(_fakeInstallDir, recursive: true); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildFixture.cs b/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildFixture.cs new file mode 100644 index 0000000000..9a7ae4d6ee --- /dev/null +++ b/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildFixture.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.Extensions.ProjectModel.Internal; +using NuGet.Versioning; + +namespace Microsoft.Extensions.ProjectModel +{ + public class MsBuildFixture + { + private readonly SemanticVersion _minMsBuildVersion = SemanticVersion.Parse("1.0.0-preview3-00000"); + + internal MsBuildContext GetMsBuildContext() + { + // for CI + var sdk = DotNetCoreSdkResolver.DefaultResolver.ResolveLatest(); + + // for dev work in VS + if (SemanticVersion.Parse(sdk.Version) < _minMsBuildVersion) + { + var home = Environment.GetEnvironmentVariable("USERPROFILE") + ?? Environment.GetEnvironmentVariable("HOME"); + var dotnetHome = Path.Combine(home, ".dotnet"); + var resovler = new DotNetCoreSdkResolver(dotnetHome); + sdk = resovler.ResolveLatest(); + } + + if (SemanticVersion.Parse(sdk.Version) < _minMsBuildVersion) + { + throw new InvalidOperationException($"Version of .NET Core SDK found in '{sdk.BasePath}' is not new enough for these tests."); + } + + return MsBuildContext.FromDotNetSdk(sdk); + } + } +} \ 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 new file mode 100644 index 0000000000..41cfba10c7 --- /dev/null +++ b/test/Microsoft.Extensions.ProjectModel.Tests/MsBuild/MsBuildProjectContextBuilderTest.cs @@ -0,0 +1,77 @@ +// 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.IO; +using System.Linq; +using NuGet.Frameworks; +using Xunit; + +namespace Microsoft.Extensions.ProjectModel +{ + public class MsBuildProjectContextBuilderTest : IClassFixture + { + private const string SkipReason = "CI doesn't yet have a new enough version of .NET Core SDK"; + + private readonly MsBuildFixture _fixture; + + public MsBuildProjectContextBuilderTest(MsBuildFixture fixture) + { + _fixture = fixture; + } + + [Fact(Skip = SkipReason)] + public void ExecutesDesignTimeBuild() + { + 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"); + fileProvider.Add("test.csproj", @" + + + + + Microsoft.TestProject + TestProject + Library + .NETCoreApp + v1.0 + + + + + + + + +"); + fileProvider.Add("One.cs", "public class Abc {}"); + fileProvider.Add("Two.cs", "public class Abc2 {}"); + fileProvider.Add("Excluded.cs", "public class Abc {}"); + + var testContext = _fixture.GetMsBuildContext(); + + 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") + .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); + + 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.True(context.IsClassLibrary); + Assert.Equal("TestProject", context.ProjectName); + Assert.Equal(FrameworkConstants.CommonFrameworks.NetCoreApp10, context.TargetFramework); + Assert.Equal("Microsoft.TestProject", context.RootNamespace); + } + } + } +} diff --git a/test/Microsoft.Extensions.ProjectModel.Tests/Utilities/TemporaryFileProvider.cs b/test/Microsoft.Extensions.ProjectModel.Tests/Utilities/TemporaryFileProvider.cs new file mode 100644 index 0000000000..9e21d6a371 --- /dev/null +++ b/test/Microsoft.Extensions.ProjectModel.Tests/Utilities/TemporaryFileProvider.cs @@ -0,0 +1,29 @@ +// 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 System.Text; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.Extensions.ProjectModel +{ + internal class TemporaryFileProvider : PhysicalFileProvider + { + public TemporaryFileProvider() + :base(Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "tmpfiles", Guid.NewGuid().ToString())).FullName) + { + } + + public void Add(string filename, string contents) + { + File.WriteAllText(Path.Combine(this.Root, filename), contents, Encoding.UTF8); + } + + public new void Dispose() + { + base.Dispose(); + Directory.Delete(Root, recursive: true); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.ProjectModel.Tests/project.json b/test/Microsoft.Extensions.ProjectModel.Tests/project.json new file mode 100644 index 0000000000..6b0b647c3e --- /dev/null +++ b/test/Microsoft.Extensions.ProjectModel.Tests/project.json @@ -0,0 +1,31 @@ +{ + "buildOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "dependencies": { + "NuGet.Frameworks": "3.5.0-*", + "Microsoft.DotNet.Cli.Utils": "1.0.0-*", + "Microsoft.DotNet.ProjectModel": "1.0.0-*", + "Microsoft.Build.Runtime": "15.1.298-preview5", + "Microsoft.Extensions.FileProviders.Physical": "1.1.0-*", + "Microsoft.Extensions.ProjectModel.Sources": { + "type": "build", + "version": "1.0.0-*" + }, + "System.Runtime.InteropServices.RuntimeInformation": "4.0.0", + "dotnet-test-xunit": "2.2.0-*", + "xunit": "2.2.0-*" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0", + "type": "platform" + } + } + } + }, + "testRunner": "xunit" +}