File: SolutionGeneration.cs
Web Access
Project: src\src\Workspaces\MSBuild\Test\Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests.csproj (Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.UnitTests.TestFiles;
 
namespace Microsoft.CodeAnalysis.MSBuild.UnitTests;
 
/// <summary>
/// Flexible and extensible API to generate MSBuild projects and solutions without external files or resources.
/// </summary>
public static class SolutionGeneration
{
    public const string NS = "http://schemas.microsoft.com/developer/msbuild/2003";
 
    private const string CSharpProjectTemplate =
@"<?xml version=""1.0"" encoding=""utf-8""?>
<Project ToolsVersion=""12.0"" DefaultTargets=""Build"" xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">
  <Import Project=""$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"" Condition=""Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"" />
  <PropertyGroup>
    <Configuration Condition="" '$(Configuration)' == '' "">Debug</Configuration>
    <Platform Condition="" '$(Platform)' == '' "">AnyCPU</Platform>
  </PropertyGroup>
  <PropertyGroup Condition="" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "">
  </PropertyGroup>
  <PropertyGroup Condition="" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "">
  </PropertyGroup>
  <Import Project=""$(MSBuildToolsPath)\Microsoft.CSharp.targets"" />
</Project>";
 
    private const string SolutionTemplate =
@"
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.30110.0
MinimumVisualStudioVersion = 10.0.40219.1
{0}Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
EndGlobal
";
 
    public const string PublicKey = "00240000048000009400000006020000002400005253413100040000010001003bb5de1b79bee9bf5ba44bdb42974c6f40fdc4b329c8e1b833fa798cf0859529485b2bfc359a08e16f025fe57efd293c4dc3541cb2e0929b1c4a92db87eed7a9454dbd08beb7c7308941384b3bfb088de781b51caef23677f8f6defb671e97e1fc5e0979858e52828c86aca1d4ea1797f1f1254bf64073a28e5be520d5397fb0";
    public const string PublicKeyToken = "39d7e8ec38707fde";
    public static readonly byte[] KeySnk = Resources.Key_snk;
 
    public static IEnumerable<(string fileName, object fileContent)> GetSolutionFiles(params IBuilder[] inputs)
    {
        var list = new List<(string, object)>();
        var projectBuilders = inputs.OfType<ProjectBuilder>();
        var files = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var fileIndex = 1;
        var projectIndex = 1;
 
        // first make sure all projects have names, as a separate loop
        foreach (var project in projectBuilders)
        {
            if (project.Name == null)
            {
                project.Name = "Project" + projectIndex;
                projectIndex++;
            }
        }
 
        foreach (var project in projectBuilders)
        {
            foreach (var document in project.Documents)
            {
                if (document.FilePath == null)
                {
                    document.FilePath = "Document" + fileIndex + (project.Language == LanguageNames.VisualBasic ? ".vb" : ".cs");
                    fileIndex++;
                }
            }
 
            foreach (var projectReference in project.ProjectReferences)
            {
                if (projectReference.Guid == Guid.Empty)
                {
                    var referencedProject = projectBuilders.First(p => p.Name == projectReference.ProjectName);
                    projectReference.Guid = referencedProject.Guid;
                    projectReference.ProjectFileName = referencedProject.Name + referencedProject.Extension;
                }
            }
 
            foreach (var (fileName, fileContent) in project.Files)
            {
                if (files.Add(fileName + fileContent))
                {
                    list.Add((fileName, fileContent));
                }
            }
        }
 
        list.Add(("Solution.sln", GetSolutionContent(projectBuilders)));
 
        return list;
    }
 
    public static IBuilder Project(params IBuilder[] inputs)
    {
        var projectReferences = inputs.OfType<ProjectReferenceBuilder>();
        var documents = inputs.OfType<DocumentBuilder>().ToList();
        var projectName = inputs.OfType<ProjectNameBuilder>().FirstOrDefault();
        var properties = inputs.OfType<PropertyBuilder>().ToList();
        var sign = inputs.OfType<SignBuilder>();
        if (sign != null)
        {
            properties.Add((PropertyBuilder)Property("SignAssembly", "true"));
            properties.Add((PropertyBuilder)Property("AssemblyOriginatorKeyFile", "key.snk"));
            documents.Add((DocumentBuilder)Document(KeySnk, "key.snk", "None"));
        }
 
        return new ProjectBuilder
        {
            Name = projectName?.Name,
            Documents = documents,
            ProjectReferences = projectReferences,
            Properties = properties
        };
    }
 
    public static IBuilder ProjectReference(string projectName)
    {
        return new ProjectReferenceBuilder
        {
            ProjectName = projectName
        };
    }
 
    public static IBuilder ProjectName(string projectName)
    {
        return new ProjectNameBuilder
        {
            Name = projectName
        };
    }
 
    public static IBuilder Property(string propertyName, string propertyValue)
    {
        return new PropertyBuilder
        {
            Name = propertyName,
            Value = propertyValue
        };
    }
 
    public static IBuilder Sign
    {
        get
        {
            return new SignBuilder();
        }
    }
 
    public static IBuilder Document(
        object content = null,
        string filePath = null,
        string itemType = "Compile")
    {
        return new DocumentBuilder
        {
            FilePath = filePath,
            Content = content,
            ItemType = itemType
        };
    }
 
    private static string GetSolutionContent(IEnumerable<ProjectBuilder> projects)
    {
        var sb = new StringBuilder();
        foreach (var project in projects)
        {
            var fileName = project.Name + project.Extension;
            var languageGuid = project.Language == LanguageNames.VisualBasic
                ? "{F184B08F-C81C-45F6-A57F-5ABD9991F28F}"
                : "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}";
            sb.AppendLine(
                string.Format(
                    @"Project(""{0}"") = ""{1}"", ""{2}"", ""{3}""",
                    languageGuid,
                    project.Name,
                    fileName,
                    project.Guid.ToString("B")));
            sb.AppendLine("EndProject");
        }
 
        return string.Format(SolutionTemplate, sb.ToString());
    }
 
    public interface IBuilder
    {
    }
 
    private sealed class ProjectBuilder : IBuilder
    {
        public string Name { get; set; }
        public string Language { get; set; }
        public Guid Guid { get; set; }
        public string OutputType { get; set; }
        public string OutputPath { get; set; }
        public IEnumerable<DocumentBuilder> Documents { get; set; }
        public IEnumerable<ProjectReferenceBuilder> ProjectReferences { get; set; }
        public IEnumerable<PropertyBuilder> Properties { get; set; }
 
        public string Extension
        {
            get
            {
                return Language == LanguageNames.VisualBasic ? ".vbproj" : ".csproj";
            }
        }
 
        public IEnumerable<(string fileName, object fileContent)> Files
        {
            get
            {
                foreach (var document in Documents)
                {
                    yield return (document.FilePath, document.Content);
                }
 
                yield return (Name + Extension, GetProjectContent());
            }
        }
 
        private string GetProjectContent()
        {
            if (Language == LanguageNames.VisualBasic)
            {
                throw new NotImplementedException("Need VB support");
            }
 
            if (Guid == Guid.Empty)
            {
                Guid = Guid.NewGuid();
            }
 
            if (string.IsNullOrEmpty(OutputType))
            {
                OutputType = "Library";
            }
 
            if (string.IsNullOrEmpty(OutputPath))
            {
                OutputPath = ".";
            }
 
            var document = XDocument.Parse(CSharpProjectTemplate);
            var propertyGroup = document.Root.Descendants(XName.Get("PropertyGroup", NS)).First();
            AddXElement(propertyGroup, "ProjectGuid", Guid.ToString("B"));
            AddXElement(propertyGroup, "OutputType", OutputType);
            AddXElement(propertyGroup, "OutputPath", OutputPath);
            AddXElement(propertyGroup, "AssemblyName", Name);
 
            if (Properties != null)
            {
                foreach (var property in Properties)
                {
                    AddXElement(propertyGroup, property.Name, property.Value);
                }
            }
 
            var importTargets = document.Root.Elements().Last();
 
            if (ProjectReferences != null && ProjectReferences.Any())
            {
                AddItemGroup(
                    importTargets,
                    _ => "ProjectReference",
                    ProjectReferences,
                    i => i.ProjectFileName,
                    (projectReference, xmlElement) =>
                    {
                        if (projectReference.Guid != Guid.Empty)
                        {
                            AddXElement(xmlElement, "Project", projectReference.Guid.ToString("B"));
                        }
 
                        AddXElement(xmlElement, "Name", Path.GetFileNameWithoutExtension(projectReference.ProjectName));
                    });
            }
 
            if (Documents != null)
            {
                AddItemGroup(
                    importTargets,
                    i => i.ItemType,
                    Documents,
                    i => i.FilePath);
            }
 
            return document.ToString();
        }
 
        private static void AddItemGroup<T>(
            XElement addBefore,
            Func<T, string> itemTypeSelector,
            IEnumerable<T> items,
            Func<T, string> attributeValueGetter,
            Action<T, XElement> elementModifier = null)
        {
            var itemGroup = CreateXElement("ItemGroup");
            addBefore.AddBeforeSelf(itemGroup);
 
            foreach (var item in items)
            {
                var itemElement = CreateXElement(itemTypeSelector(item));
                itemElement.SetAttributeValue("Include", attributeValueGetter(item));
                elementModifier?.Invoke(item, itemElement);
 
                itemGroup.Add(itemElement);
            }
        }
 
        private static XElement CreateXElement(string name)
        {
            return new XElement(XName.Get(name, NS));
        }
 
        private static void AddXElement(XElement element, string elementName, string elementValue)
        {
            element.Add(new XElement(XName.Get(elementName, NS), elementValue));
        }
    }
 
    private sealed class ProjectReferenceBuilder : IBuilder
    {
        public string ProjectName { get; set; }
        public Guid Guid { get; set; }
        public string ProjectFileName { get; set; }
    }
 
    private sealed class ProjectNameBuilder : IBuilder
    {
        public string Name { get; set; }
    }
 
    private sealed class PropertyBuilder : IBuilder
    {
        public string Name { get; set; }
        public string Value { get; set; }
    }
 
    private sealed class SignBuilder : IBuilder { }
 
    private sealed class DocumentBuilder : IBuilder
    {
        public string FilePath { get; set; }
        public object Content { get; set; }
        public string ItemType { get; set; }
    }
}