// Copyright (c) Microsoft. 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.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; #if !MSBUILD12 using Microsoft.Build.Construction; #endif using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; using System.Runtime.InteropServices; namespace Microsoft.CodeAnalysis.MSBuild { /// /// A workspace that can be populated by opening MSBuild solution and project files. /// public sealed class MSBuildWorkspace : Workspace { // used to serialize access to public methods private readonly NonReentrantLock _serializationLock = new NonReentrantLock(); private MSBuildProjectLoader _loader; private MSBuildWorkspace( HostServices hostServices, ImmutableDictionary properties) : base(hostServices, "MSBuildWorkspace") { _loader = new MSBuildProjectLoader(this, properties); } /// /// Create a new instance of a workspace that can be populated by opening solution and project files. /// public static MSBuildWorkspace Create() { return Create(ImmutableDictionary.Empty); } /// /// Create a new instance of a workspace that can be populated by opening solution and project files. /// /// An optional set of MSBuild properties used when interpreting project files. /// These are the same properties that are passed to msbuild via the /property:<n>=<v> command line argument. public static MSBuildWorkspace Create(IDictionary properties) { return Create(properties, DesktopMefHostServices.DefaultServices); } /// /// Create a new instance of a workspace that can be populated by opening solution and project files. /// /// The used to configure this workspace. public static MSBuildWorkspace Create(HostServices hostServices) { return Create(ImmutableDictionary.Empty, hostServices); } /// /// Create a new instance of a workspace that can be populated by opening solution and project files. /// /// The MSBuild properties used when interpreting project files. /// These are the same properties that are passed to msbuild via the /property:<n>=<v> command line argument. /// The used to configure this workspace. public static MSBuildWorkspace Create(IDictionary properties, HostServices hostServices) { if (properties == null) { throw new ArgumentNullException(nameof(properties)); } if (hostServices == null) { throw new ArgumentNullException(nameof(hostServices)); } return new MSBuildWorkspace(hostServices, properties.ToImmutableDictionary()); } /// /// The MSBuild properties used when interpreting project files. /// These are the same properties that are passed to msbuild via the /property:<n>=<v> command line argument. /// public ImmutableDictionary Properties { get { return _loader.Properties; } } /// /// Determines if metadata from existing output assemblies is loaded instead of opening referenced projects. /// If the referenced project is already opened, the metadata will not be loaded. /// If the metadata assembly cannot be found the referenced project will be opened instead. /// public bool LoadMetadataForReferencedProjects { get { return _loader.LoadMetadataForReferencedProjects; } set { _loader.LoadMetadataForReferencedProjects = value; } } /// /// Determines if unrecognized projects are skipped when solutions or projects are opened. /// /// An project is unrecognized if it either has /// a) an invalid file path, /// b) a non-existent project file, /// c) has an unrecognized file extension or /// d) a file extension associated with an unsupported language. /// /// If unrecognized projects cannot be skipped a corresponding exception is thrown. /// public bool SkipUnrecognizedProjects { get { return _loader.SkipUnrecognizedProjects; } set { _loader.SkipUnrecognizedProjects = value; } } /// /// Associates a project file extension with a language name. /// public void AssociateFileExtensionWithLanguage(string projectFileExtension, string language) { _loader.AssociateFileExtensionWithLanguage(projectFileExtension, language); } /// /// Close the open solution, and reset the workspace to a new empty solution. /// public void CloseSolution() { using (_serializationLock.DisposableWait()) { this.ClearSolution(); } } private string GetAbsolutePath(string path, string baseDirectoryPath) { return Path.GetFullPath(FileUtilities.ResolveRelativePath(path, baseDirectoryPath) ?? path); } #region Open Solution & Project /// /// Open a solution file and all referenced projects. /// public async Task OpenSolutionAsync(string solutionFilePath, CancellationToken cancellationToken = default(CancellationToken)) { if (solutionFilePath == null) { throw new ArgumentNullException(nameof(solutionFilePath)); } this.ClearSolution(); var solutionInfo = await _loader.LoadSolutionInfoAsync(solutionFilePath, cancellationToken: cancellationToken).ConfigureAwait(false); // construct workspace from loaded project infos this.OnSolutionAdded(solutionInfo); this.UpdateReferencesAfterAdd(); return this.CurrentSolution; } /// /// Open a project file and all referenced projects. /// public async Task OpenProjectAsync(string projectFilePath, CancellationToken cancellationToken = default(CancellationToken)) { if (projectFilePath == null) { throw new ArgumentNullException(nameof(projectFilePath)); } var projects = await _loader.LoadProjectInfoAsync(projectFilePath, GetCurrentProjectMap(), cancellationToken).ConfigureAwait(false); // add projects to solution foreach (var project in projects) { this.OnProjectAdded(project); } this.UpdateReferencesAfterAdd(); return this.CurrentSolution.GetProject(projects[0].Id); } private Dictionary GetCurrentProjectMap() { return this.CurrentSolution.Projects .Where(p => !string.IsNullOrEmpty(p.FilePath)) .ToDictionary(p => p.FilePath, p => p.Id); } #endregion #region Apply Changes public override bool CanApplyChange(ApplyChangesKind feature) { switch (feature) { case ApplyChangesKind.ChangeDocument: case ApplyChangesKind.AddDocument: case ApplyChangesKind.RemoveDocument: case ApplyChangesKind.AddMetadataReference: case ApplyChangesKind.RemoveMetadataReference: case ApplyChangesKind.AddProjectReference: case ApplyChangesKind.RemoveProjectReference: case ApplyChangesKind.AddAnalyzerReference: case ApplyChangesKind.RemoveAnalyzerReference: return true; default: return false; } } private bool HasProjectFileChanges(ProjectChanges changes) { return changes.GetAddedDocuments().Any() || changes.GetRemovedDocuments().Any() || changes.GetAddedMetadataReferences().Any() || changes.GetRemovedMetadataReferences().Any() || changes.GetAddedProjectReferences().Any() || changes.GetRemovedProjectReferences().Any() || changes.GetAddedAnalyzerReferences().Any() || changes.GetRemovedAnalyzerReferences().Any(); } private IProjectFile _applyChangesProjectFile; public override bool TryApplyChanges(Solution newSolution) { using (_serializationLock.DisposableWait()) { return base.TryApplyChanges(newSolution); } } protected override void ApplyProjectChanges(ProjectChanges projectChanges) { System.Diagnostics.Debug.Assert(_applyChangesProjectFile == null); var project = projectChanges.OldProject ?? projectChanges.NewProject; try { // if we need to modify the project file, load it first. if (this.HasProjectFileChanges(projectChanges)) { var projectPath = project.FilePath; IProjectFileLoader loader; if (_loader.TryGetLoaderFromProjectPath(projectPath, out loader)) { try { _applyChangesProjectFile = loader.LoadProjectFileAsync(projectPath, _loader.Properties, CancellationToken.None).Result; } catch (System.IO.IOException exception) { this.OnWorkspaceFailed(new ProjectDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, projectChanges.ProjectId)); } } } // do normal apply operations base.ApplyProjectChanges(projectChanges); // save project file if (_applyChangesProjectFile != null) { try { _applyChangesProjectFile.Save(); } catch (System.IO.IOException exception) { this.OnWorkspaceFailed(new ProjectDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, projectChanges.ProjectId)); } } } finally { _applyChangesProjectFile = null; } } protected override void ApplyDocumentTextChanged(DocumentId documentId, SourceText text) { var document = this.CurrentSolution.GetDocument(documentId); if (document != null) { Encoding encoding = DetermineEncoding(text, document); this.SaveDocumentText(documentId, document.FilePath, text, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); this.OnDocumentTextChanged(documentId, text, PreservationMode.PreserveValue); } } private static Encoding DetermineEncoding(SourceText text, Document document) { if (text.Encoding != null) { return text.Encoding; } try { using (ExceptionHelpers.SuppressFailFast()) { using (var stream = new FileStream(document.FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { var onDiskText = EncodedStringText.Create(stream); return onDiskText.Encoding; } } } catch (IOException) { } catch (InvalidDataException) { } return null; } protected override void ApplyDocumentAdded(DocumentInfo info, SourceText text) { System.Diagnostics.Debug.Assert(_applyChangesProjectFile != null); var project = this.CurrentSolution.GetProject(info.Id.ProjectId); IProjectFileLoader loader; if (_loader.TryGetLoaderFromProjectPath(project.FilePath, out loader)) { var extension = _applyChangesProjectFile.GetDocumentExtension(info.SourceCodeKind); var fileName = Path.ChangeExtension(info.Name, extension); var relativePath = (info.Folders != null && info.Folders.Count > 0) ? Path.Combine(Path.Combine(info.Folders.ToArray()), fileName) : fileName; var fullPath = GetAbsolutePath(relativePath, Path.GetDirectoryName(project.FilePath)); var newDocumentInfo = info.WithName(fileName) .WithFilePath(fullPath) .WithTextLoader(new FileTextLoader(fullPath, text.Encoding)); // add document to project file _applyChangesProjectFile.AddDocument(relativePath); // add to solution this.OnDocumentAdded(newDocumentInfo); // save text to disk if (text != null) { this.SaveDocumentText(info.Id, fullPath, text, text.Encoding ?? Encoding.UTF8); } } } private void SaveDocumentText(DocumentId id, string fullPath, SourceText newText, Encoding encoding) { try { using (ExceptionHelpers.SuppressFailFast()) { var dir = Path.GetDirectoryName(fullPath); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } Debug.Assert(encoding != null); using (var writer = new StreamWriter(fullPath, append: false, encoding: encoding)) { newText.Write(writer); } } } catch (IOException exception) { this.OnWorkspaceFailed(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, id)); } } protected override void ApplyDocumentRemoved(DocumentId documentId) { Debug.Assert(_applyChangesProjectFile != null); var document = this.CurrentSolution.GetDocument(documentId); if (document != null) { _applyChangesProjectFile.RemoveDocument(document.FilePath); this.DeleteDocumentFile(document.Id, document.FilePath); this.OnDocumentRemoved(documentId); } } private void DeleteDocumentFile(DocumentId documentId, string fullPath) { try { if (File.Exists(fullPath)) { File.Delete(fullPath); } } catch (IOException exception) { this.OnWorkspaceFailed(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, documentId)); } catch (NotSupportedException exception) { this.OnWorkspaceFailed(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, documentId)); } catch (UnauthorizedAccessException exception) { this.OnWorkspaceFailed(new DocumentDiagnostic(WorkspaceDiagnosticKind.Failure, exception.Message, documentId)); } } protected override void ApplyMetadataReferenceAdded(ProjectId projectId, MetadataReference metadataReference) { Debug.Assert(_applyChangesProjectFile != null); var identity = GetAssemblyIdentity(projectId, metadataReference); _applyChangesProjectFile.AddMetadataReference(metadataReference, identity); this.OnMetadataReferenceAdded(projectId, metadataReference); } protected override void ApplyMetadataReferenceRemoved(ProjectId projectId, MetadataReference metadataReference) { Debug.Assert(_applyChangesProjectFile != null); var identity = GetAssemblyIdentity(projectId, metadataReference); _applyChangesProjectFile.RemoveMetadataReference(metadataReference, identity); this.OnMetadataReferenceRemoved(projectId, metadataReference); } private AssemblyIdentity GetAssemblyIdentity(ProjectId projectId, MetadataReference metadataReference) { var project = this.CurrentSolution.GetProject(projectId); if (!project.MetadataReferences.Contains(metadataReference)) { project = project.AddMetadataReference(metadataReference); } var compilation = project.GetCompilationAsync(CancellationToken.None).WaitAndGetResult(CancellationToken.None); var symbol = compilation.GetAssemblyOrModuleSymbol(metadataReference) as IAssemblySymbol; return symbol != null ? symbol.Identity : null; } protected override void ApplyProjectReferenceAdded(ProjectId projectId, ProjectReference projectReference) { Debug.Assert(_applyChangesProjectFile != null); var project = this.CurrentSolution.GetProject(projectReference.ProjectId); if (project != null) { _applyChangesProjectFile.AddProjectReference(project.Name, new ProjectFileReference(project.FilePath, projectReference.Aliases)); } this.OnProjectReferenceAdded(projectId, projectReference); } protected override void ApplyProjectReferenceRemoved(ProjectId projectId, ProjectReference projectReference) { Debug.Assert(_applyChangesProjectFile != null); var project = this.CurrentSolution.GetProject(projectReference.ProjectId); if (project != null) { _applyChangesProjectFile.RemoveProjectReference(project.Name, project.FilePath); } this.OnProjectReferenceRemoved(projectId, projectReference); } protected override void ApplyAnalyzerReferenceAdded(ProjectId projectId, AnalyzerReference analyzerReference) { Debug.Assert(_applyChangesProjectFile != null); _applyChangesProjectFile.AddAnalyzerReference(analyzerReference); this.OnAnalyzerReferenceAdded(projectId, analyzerReference); } protected override void ApplyAnalyzerReferenceRemoved(ProjectId projectId, AnalyzerReference analyzerReference) { Debug.Assert(_applyChangesProjectFile != null); _applyChangesProjectFile.RemoveAnalyzerReference(analyzerReference); this.OnAnalyzerReferenceRemoved(projectId, analyzerReference); } } #endregion }