Rasagar/Library/PackageCache/com.unity.ide.rider/Rider/Editor/ProjectGeneration/ProjectGeneration.cs

1049 lines
42 KiB
C#
Raw Normal View History

2024-08-26 13:07:20 -07:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Text;
using Packages.Rider.Editor.Util;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
namespace Packages.Rider.Editor.ProjectGeneration
{
internal class ProjectGeneration : IGenerator
{
private enum ScriptingLanguage
{
None,
CSharp
}
/// <summary>
/// Map source extensions to ScriptingLanguages
/// </summary>
private static readonly Dictionary<string, ScriptingLanguage> k_BuiltinSupportedExtensions =
new Dictionary<string, ScriptingLanguage>
{
{ ".cs", ScriptingLanguage.CSharp },
{ ".uxml", ScriptingLanguage.None },
{ ".uss", ScriptingLanguage.None },
{ ".shader", ScriptingLanguage.None },
{ ".compute", ScriptingLanguage.None },
{ ".cginc", ScriptingLanguage.None },
{ ".hlsl", ScriptingLanguage.None },
{ ".glslinc", ScriptingLanguage.None },
{ ".template", ScriptingLanguage.None },
{ ".raytrace", ScriptingLanguage.None },
{ ".json", ScriptingLanguage.None},
{ ".rsp", ScriptingLanguage.None},
{ ".asmdef", ScriptingLanguage.None},
{ ".asmref", ScriptingLanguage.None},
{ ".xaml", ScriptingLanguage.None},
{ ".tt", ScriptingLanguage.None},
{ ".t4", ScriptingLanguage.None},
{ ".ttinclude", ScriptingLanguage.None}
};
private string[] m_ProjectSupportedExtensions = Array.Empty<string>();
// Note that ProjectDirectory can be assumed to be the result of Path.GetFullPath
public string ProjectDirectory { get; }
public string ProjectDirectoryWithSlash { get; }
private readonly string m_ProjectName;
private readonly IAssemblyNameProvider m_AssemblyNameProvider;
private readonly IFileIO m_FileIOProvider;
private readonly IGUIDGenerator m_GUIDGenerator;
private readonly Dictionary<string, string> m_ProjectGuids = new Dictionary<string, string>();
// If we have multiple projects, the same assembly references are reused for each. Caching the normalised paths and
// names is actually cheaper than recalculating each time, in terms of both time and memory allocations
private readonly Dictionary<string, string> m_NormalisedPaths = new Dictionary<string, string>();
private readonly Dictionary<string, string> m_AssemblyNames = new Dictionary<string, string>();
internal static bool isRiderProjectGeneration; // workaround to https://github.cds.internal.unity3d.com/unity/com.unity.ide.rider/issues/28
IAssemblyNameProvider IGenerator.AssemblyNameProvider => m_AssemblyNameProvider;
public ProjectGeneration()
: this(Directory.GetParent(Application.dataPath).FullName) { }
public ProjectGeneration(string projectDirectory)
: this(projectDirectory, new AssemblyNameProvider(), new FileIOProvider(), new GUIDProvider()) { }
public ProjectGeneration(string projectDirectory, IAssemblyNameProvider assemblyNameProvider, IFileIO fileIoProvider, IGUIDGenerator guidGenerator)
{
ProjectDirectory = Path.GetFullPath(projectDirectory.NormalizePath());
ProjectDirectoryWithSlash = ProjectDirectory + Path.DirectorySeparatorChar;
m_ProjectName = Path.GetFileName(ProjectDirectory);
m_AssemblyNameProvider = assemblyNameProvider;
m_FileIOProvider = fileIoProvider;
m_GUIDGenerator = guidGenerator;
}
/// <summary>
/// Syncs the scripting solution if any affected files are relevant.
/// </summary>
/// <returns>
/// Whether the solution was synced.
/// </returns>
/// <param name='affectedFiles'>
/// A set of files whose status has changed
/// </param>
/// <param name="reimportedFiles">
/// A set of files that got reimported
/// </param>
/// <param name="checkProjectFiles">
/// Check if project files were changed externally
/// </param>
public bool SyncIfNeeded(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles, bool checkProjectFiles = false)
{
SetupSupportedExtensions();
PackageManagerTracker.SyncIfNeeded(checkProjectFiles);
if (HasFilesBeenModified(affectedFiles, reimportedFiles) || RiderScriptEditorData.instance.hasChanges
|| RiderScriptEditorData.instance.HasChangesInCompilationDefines()
|| (checkProjectFiles && LastWriteTracker.HasLastWriteTimeChanged()))
{
Sync();
return true;
}
return false;
}
private bool HasFilesBeenModified(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles)
{
return affectedFiles.Any(ShouldFileBePartOfSolution) || reimportedFiles.Any(ShouldSyncOnReimportedAsset);
}
private static bool ShouldSyncOnReimportedAsset(string asset)
{
var extension = Path.GetExtension(asset);
return extension == ".asmdef" || extension == ".asmref" || Path.GetFileName(asset) == "csc.rsp";
}
public void Sync()
{
SetupSupportedExtensions();
var types = GetAssetPostprocessorTypes();
isRiderProjectGeneration = true;
var externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles(types);
isRiderProjectGeneration = false;
if (!externalCodeAlreadyGeneratedProjects)
{
GenerateAndWriteSolutionAndProjects(types);
}
OnGeneratedCSProjectFiles(types);
m_AssemblyNameProvider.ResetCaches();
m_AssemblyNames.Clear();
m_NormalisedPaths.Clear();
m_ProjectGuids.Clear();
_buffer = null;
RiderScriptEditorData.instance.hasChanges = false;
RiderScriptEditorData.instance.InvalidateSavedCompilationDefines();
}
public bool HasSolutionBeenGenerated()
{
return m_FileIOProvider.Exists(SolutionFile());
}
private void SetupSupportedExtensions()
{
var extensions = m_AssemblyNameProvider.ProjectSupportedExtensions;
m_ProjectSupportedExtensions = new string[extensions.Length];
for (var i = 0; i < extensions.Length; i++)
{
m_ProjectSupportedExtensions[i] = "." + extensions[i];
}
}
private bool ShouldFileBePartOfSolution(string file)
{
// Exclude files coming from packages except if they are internalized.
if (m_AssemblyNameProvider.IsInternalizedPackagePath(file))
{
return false;
}
return HasValidExtension(file);
}
public bool HasValidExtension(string file)
{
// Dll's are not scripts but still need to be included..
if (file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
return true;
var extension = Path.GetExtension(file);
return IsSupportedExtension(extension);
}
private bool IsSupportedExtension(string extension)
{
return k_BuiltinSupportedExtensions.ContainsKey(extension) || m_ProjectSupportedExtensions.Contains(extension);
}
private class AssemblyUsage
{
private readonly HashSet<string> m_ProjectAssemblies = new HashSet<string>();
private readonly HashSet<string> m_PrecompiledAssemblies = new HashSet<string>();
public void AddProjectAssembly(Assembly assembly)
{
m_ProjectAssemblies.Add(assembly.name);
}
public void AddPrecompiledAssembly(Assembly assembly)
{
m_PrecompiledAssemblies.Add(assembly.name);
}
public bool IsProjectAssembly(Assembly assembly) => m_ProjectAssemblies.Contains(assembly.name);
public bool IsPrecompiledAssembly(Assembly assembly) => m_PrecompiledAssemblies.Contains(assembly.name);
}
private void GenerateAndWriteSolutionAndProjects(Type[] types)
{
// Only synchronize islands that have associated source files and ones that we actually want in the project.
// This also filters out DLLs coming from .asmdef files in packages.
// Get all of the assemblies that Unity will compile from source. This includes Assembly-CSharp, all user assembly
// definitions, and all packages. Not all of the returned assemblies will require project files - by default,
// registry, git and local tarball packages are pre-compiled by Unity and will not require a project. This can be
// changed by the user in the External Tools settings page.
// Each assembly instance contains source files, output path, defines, compiler options and references. There
// will be `compiledAssemblyReferences`, which are DLLs, such as UnityEngine.dll, and assembly references, which
// are references to other assemblies that Unity will compile from source. Again, these assemblies might be
// projects, or pre-compiled by Unity, depending on the options selected by the user.
var allAssemblies = m_AssemblyNameProvider.GetAllAssemblies();
var assemblyUsage = new AssemblyUsage();
foreach (var assembly in allAssemblies)
{
if (assembly.sourceFiles.Any(ShouldFileBePartOfSolution))
assemblyUsage.AddProjectAssembly(assembly);
else
assemblyUsage.AddPrecompiledAssembly(assembly);
}
// Get additional assets (other than source files) that we want to add to the projects, e.g. shaders, asmdef, etc.
var additionalAssetsByAssembly = GetAdditionalAssets();
var projectParts = new List<ProjectPart>();
var assemblyNamesWithSource = new HashSet<string>();
foreach (var assembly in allAssemblies)
{
if (!assemblyUsage.IsProjectAssembly(assembly))
continue;
// TODO: Will this check ever be true? Player assemblies don't have the same name as editor assemblies, right?
if (assemblyNamesWithSource.Contains(assembly.name))
projectParts.Add(new ProjectPart(assembly.name, assembly, new List<string>())); // do not add asset project parts to both editor and player projects
else
{
additionalAssetsByAssembly.TryGetValue(assembly.name, out var additionalAssetsForProject);
projectParts.Add(new ProjectPart(assembly.name, assembly, additionalAssetsForProject));
assemblyNamesWithSource.Add(assembly.name);
}
}
// If there are any assets that should be in a separate assembly, but that assembly folder doesn't contain any
// source files, we'll have orphaned assets. Create a project for these assemblies, with references based on the
// Rider package assembly
// TODO: Would this produce the same results if we removed the check for ShouldFileBePartOfSolution above?
// I suspect the only difference would be output path and references, and potentially simplify things
var executingAssemblyName = typeof(ProjectGeneration).Assembly.GetName().Name;
var riderAssembly = m_AssemblyNameProvider.GetNamedAssembly(executingAssemblyName);
string[] coreReferences = null;
foreach (var pair in additionalAssetsByAssembly)
{
var assembly = pair.Key;
var additionalAssets = pair.Value;
if (!assemblyNamesWithSource.Contains(assembly))
{
if (coreReferences == null)
{
coreReferences = riderAssembly?.compiledAssemblyReferences.Where(a =>
a.EndsWith("UnityEditor.dll", StringComparison.Ordinal) ||
a.EndsWith("UnityEngine.dll", StringComparison.Ordinal) ||
a.EndsWith("UnityEngine.CoreModule.dll", StringComparison.Ordinal)).ToArray();
}
projectParts.Add(AddProjectPart(assembly, riderAssembly, coreReferences, additionalAssets));
}
}
var stringBuilder = new StringBuilder();
SyncSolution(stringBuilder, projectParts, types);
stringBuilder.Clear();
foreach (var projectPart in projectParts)
{
SyncProject(stringBuilder, projectPart, assemblyUsage, types);
stringBuilder.Clear();
}
}
private static ProjectPart AddProjectPart(string assemblyName, Assembly riderAssembly, string[] coreReferences,
List<string> additionalAssets)
{
Assembly assembly = null;
if (riderAssembly != null)
{
// We want to add those references, so that Rider would detect Unity path and version and provide rich features for shader files
// Note that output path will be Library/ScriptAssemblies
assembly = new Assembly(assemblyName, riderAssembly.outputPath, Array.Empty<string>(),
new []{"UNITY_EDITOR"},
Array.Empty<Assembly>(),
coreReferences,
riderAssembly.flags);
}
return new ProjectPart(assemblyName, assembly, additionalAssets);
}
private Dictionary<string, List<string>> GetAdditionalAssets()
{
var assemblyDllNames = new FilePathTrie<string>();
var interestingAssets = new List<string>();
foreach (var assetPath in m_AssemblyNameProvider.GetAllAssetPaths())
{
if (m_AssemblyNameProvider.IsInternalizedPackagePath(assetPath))
continue;
// Find all the .asmdef and .asmref files. Then get the assembly for a file in the same folder. Anything in that
// folder or below will be in the same assembly (unless there's another nested .asmdef, obvs)
if (assetPath.EndsWith(".asmdef", StringComparison.OrdinalIgnoreCase)
|| assetPath.EndsWith(".asmref", StringComparison.OrdinalIgnoreCase))
{
// This call is very expensive when working with a very large project (e.g. called for 50,000+ assets), hence
// the approach of working with assembly definition root folders. We don't need a real script file to get the
// assembly DLL name
var assemblyDllName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(assetPath + ".cs");
assemblyDllNames.Insert(Path.GetDirectoryName(assetPath), assemblyDllName);
}
interestingAssets.Add(assetPath);
}
const string fallbackAssemblyDllName = "Assembly-CSharp.dll";
var assetsByAssemblyDll = new Dictionary<string, List<string>>();
foreach (var asset in interestingAssets)
{
// TODO: Can we remove folders from generated projects?
// Why do we add them? We get an asset for every folder, including intermediate folders. We add folders that
// contain assets that we don't add to project files, so they appear empty. Adding them to the project file does
// not give us anything special - they appear as a folder in the Solution Explorer, so we can right click and
// add a file, but we could also "Show All Files" and do the same. Equally, Rider defaults to the Unity Explorer
// view, which shows all files and folders by default.
// We gain nothing by adding folders, and for very large projects, it can be very expensive to discover what
// project they should be added to, since most paths will be _above_ asmdef files, or inside Assets (which
// requires the full expensive check due to Editor, Resources, etc.)
// (E.g. an example large project with 45,600 assets, 5,000 are folders and only 2,500 are useful assets)
if (AssetDatabase.IsValidFolder(asset))
{
// var assemblyDllName = assemblyDllNames.FindClosestMatch(asset);
// if (string.IsNullOrEmpty(assemblyDllName))
// {
// // Can't find it in trie (Assembly-CSharp and related projects don't have .asmdef files)
// assemblyDllName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath($"{asset}/asset.cs");
// }
// if (string.IsNullOrEmpty(assemblyDllName))
// assemblyDllName = fallbackAssemblyDllName;
//
// if (!stringBuilders.TryGetValue(assemblyDllName, out var projectBuilder))
// {
// projectBuilder = new StringBuilder();
// stringBuilders[assemblyDllName] = projectBuilder;
//}
//
// projectBuilder.Append(" <Folder Include=\"")
// .Append(m_FileIOProvider.EscapedRelativePathFor(asset, ProjectDirectoryWithSlash))
// .Append("\" />")
// .AppendLine();
}
else
{
if (!asset.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) && IsSupportedExtension(Path.GetExtension(asset)))
{
var assemblyDllName = assemblyDllNames.FindClosestMatch(asset);
if (string.IsNullOrEmpty(assemblyDllName))
{
// Can't find it in trie (Assembly-CSharp and related projects don't have .asmdef files)
assemblyDllName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath($"{asset}.cs");
}
if (string.IsNullOrEmpty(assemblyDllName))
assemblyDllName = fallbackAssemblyDllName;
if (!assetsByAssemblyDll.TryGetValue(assemblyDllName, out var assets))
{
assets = new List<string>();
assetsByAssemblyDll[assemblyDllName] = assets;
}
assets.Add(m_FileIOProvider.EscapedRelativePathFor(asset, ProjectDirectoryWithSlash));
}
}
}
var assetsByAssemblyName = new Dictionary<string, List<string>>(assetsByAssemblyDll.Count);
foreach (var entry in assetsByAssemblyDll)
{
var assemblyName = FileSystemUtil.FileNameWithoutExtension(entry.Key);
assetsByAssemblyName[assemblyName] = entry.Value;
}
return assetsByAssemblyName;
}
private void SyncProject(StringBuilder stringBuilder, ProjectPart island, AssemblyUsage assemblyUsage, Type[] types)
{
SyncProjectFileIfNotChanged(
ProjectFile(island),
ProjectText(stringBuilder, island, assemblyUsage),
types);
}
private void SyncProjectFileIfNotChanged(string path, string newContents, Type[] types)
{
if (Path.GetExtension(path) == ".csproj")
{
newContents = OnGeneratedCSProject(path, newContents, types);
}
SyncFileIfNotChanged(path, newContents);
}
private void SyncSolutionFileIfNotChanged(string path, string newContents, Type[] types)
{
newContents = OnGeneratedSlnSolution(path, newContents, types);
SyncFileIfNotChanged(path, newContents);
}
private static void OnGeneratedCSProjectFiles(Type[] types)
{
foreach (var type in types)
{
var method = type.GetMethod("OnGeneratedCSProjectFiles",
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Static);
if (method == null)
{
continue;
}
Debug.LogWarning("OnGeneratedCSProjectFiles is not supported.");
// RIDER-51958
//method.Invoke(null, args);
}
}
public static Type[] GetAssetPostprocessorTypes()
{
return TypeCache.GetTypesDerivedFrom<AssetPostprocessor>().ToArray(); // doesn't find types from EditorPlugin, which is fine
}
private static bool OnPreGeneratingCSProjectFiles(Type[] types)
{
var result = false;
foreach (var type in types)
{
var args = new object[0];
var method = type.GetMethod("OnPreGeneratingCSProjectFiles",
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Static);
if (method == null)
{
continue;
}
var returnValue = method.Invoke(null, args);
if (method.ReturnType == typeof(bool))
{
result |= (bool)returnValue;
}
}
return result;
}
private static string OnGeneratedCSProject(string path, string content, Type[] types)
{
foreach (var type in types)
{
var args = new[] { path, content };
var method = type.GetMethod("OnGeneratedCSProject",
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Static);
if (method == null)
{
continue;
}
var returnValue = method.Invoke(null, args);
if (method.ReturnType == typeof(string))
{
content = (string)returnValue;
}
}
return content;
}
private static string OnGeneratedSlnSolution(string path, string content, Type[] types)
{
foreach (var type in types)
{
var args = new[] { path, content };
var method = type.GetMethod("OnGeneratedSlnSolution",
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Static);
if (method == null)
{
continue;
}
var returnValue = method.Invoke(null, args);
if (method.ReturnType == typeof(string))
{
content = (string)returnValue;
}
}
return content;
}
private void SyncFileIfNotChanged(string path, string newContents)
{
if (HasChanged(path, newContents))
m_FileIOProvider.WriteAllText(path, newContents);
}
private static char[] _buffer = null;
private bool HasChanged(string path, string newContents)
{
try
{
if (!m_FileIOProvider.Exists(path))
return true;
const int bufferSize = 100 * 1024; // 100kb - big enough to read most project files in a single read
if (_buffer == null)
_buffer = new char[bufferSize];
using (var reader = m_FileIOProvider.GetReader(path))
{
int read, offset = 0;
do
{
read = reader.ReadBlock(_buffer, 0, _buffer.Length);
for (var i = 0; i < read; i++)
{
if (_buffer[i] != newContents[offset + i])
return true;
}
offset += read;
} while (read > 0);
var isSame = offset == newContents.Length;
return !isSame;
}
}
catch (Exception exception)
{
Debug.LogException(exception);
return true;
}
}
private string ProjectText(StringBuilder projectBuilder, ProjectPart assembly, AssemblyUsage assemblyUsage)
{
var responseFilesData = assembly.GetResponseFileData(m_AssemblyNameProvider, ProjectDirectory);
ProjectHeader(projectBuilder, assembly, responseFilesData);
projectBuilder.AppendLine(" <ItemGroup>");
foreach (var file in assembly.SourceFiles)
{
var fullFile = m_FileIOProvider.EscapedRelativePathFor(file, ProjectDirectory);
projectBuilder.Append(" <Compile Include=\"").Append(fullFile).AppendLine("\" />");
}
foreach (var additionalAsset in (IEnumerable<string>)assembly.AdditionalAssets ?? Array.Empty<string>())
projectBuilder.Append(" <None Include=\"").Append(additionalAsset).AppendLine("\" />");
var binaryReferences = new HashSet<string>(assembly.CompiledAssemblyReferences);
foreach (var responseFileData in responseFilesData)
binaryReferences.UnionWith(responseFileData.FullPathReferences);
foreach (var assemblyReference in assembly.AssemblyReferences)
{
if (assemblyUsage.IsPrecompiledAssembly(assemblyReference))
binaryReferences.Add(assemblyReference.outputPath);
}
foreach (var reference in binaryReferences)
{
var escapedFullPath = GetNormalisedAssemblyPath(reference);
var assemblyName = GetAssemblyNameFromPath(reference);
projectBuilder
.Append(" <Reference Include=\"").Append(assemblyName).AppendLine("\">")
.Append(" <HintPath>").Append(escapedFullPath).AppendLine("</HintPath>")
.AppendLine(" </Reference>");
}
if (0 < assembly.AssemblyReferences.Length)
{
projectBuilder
.AppendLine(" </ItemGroup>")
.AppendLine(" <ItemGroup>");
foreach (var reference in assembly.AssemblyReferences)
{
if (assemblyUsage.IsProjectAssembly(reference))
{
var name = m_AssemblyNameProvider.GetProjectName(reference.name, reference.defines);
projectBuilder
.Append(" <ProjectReference Include=\"").Append(name).AppendLine(".csproj\">")
.Append(" <Project>{").Append(ProjectGuid(name)).AppendLine("}</Project>")
.Append(" <Name>").Append(name).AppendLine("</Name>")
.AppendLine(" </ProjectReference>");
}
}
}
projectBuilder
.AppendLine(" </ItemGroup>")
.AppendLine(" <Import Project=\"$(MSBuildToolsPath)\\Microsoft.CSharp.targets\" />")
.AppendLine(
" <!-- To modify your build process, add your task inside one of the targets below and uncomment it.")
.AppendLine(" Other similar extension points exist, see Microsoft.Common.targets.")
.AppendLine(" <Target Name=\"BeforeBuild\">")
.AppendLine(" </Target>")
.AppendLine(" <Target Name=\"AfterBuild\">")
.AppendLine(" </Target>")
.AppendLine(" -->")
.AppendLine("</Project>");
return projectBuilder.ToString();
}
private string ProjectFile(ProjectPart projectPart)
{
return Path.Combine(ProjectDirectory, $"{m_AssemblyNameProvider.GetProjectName(projectPart.Name, projectPart.Defines)}.csproj");
}
public string SolutionFile()
{
return Path.Combine(ProjectDirectory, $"{m_ProjectName}.sln");
}
private void ProjectHeader(StringBuilder stringBuilder, ProjectPart assembly, List<ResponseFileData> responseFilesData)
{
var responseFilesDataArgs = GetOtherArgumentsFromResponseFilesData(responseFilesData);
stringBuilder
.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
.AppendLine(
"<Project ToolsVersion=\"4.0\" DefaultTargets=\"Build\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">")
.AppendLine(" <PropertyGroup>")
.Append(" <LangVersion>").Append(GetLangVersion(responseFilesDataArgs["langversion"], assembly)).AppendLine("</LangVersion>")
.AppendLine(
" <_TargetFrameworkDirectories>non_empty_path_generated_by_unity.rider.package</_TargetFrameworkDirectories>")
.AppendLine(
" <_FullFrameworkReferenceAssemblyPaths>non_empty_path_generated_by_unity.rider.package</_FullFrameworkReferenceAssemblyPaths>")
.AppendLine(" <DisableHandlePackageFileConflicts>true</DisableHandlePackageFileConflicts>");
var rulesetPaths = GetRoslynAnalyzerRulesetPaths(assembly, responseFilesDataArgs);
foreach (var path in rulesetPaths)
stringBuilder.Append(" <CodeAnalysisRuleSet>").Append(path).AppendLine("</CodeAnalysisRuleSet>");
stringBuilder
.AppendLine(" </PropertyGroup>")
.AppendLine(" <PropertyGroup>")
.AppendLine(" <Configuration Condition=\" '$(Configuration)' == '' \">Debug</Configuration>")
.AppendLine(" <Platform Condition=\" '$(Platform)' == '' \">AnyCPU</Platform>")
.AppendLine(" <ProductVersion>10.0.20506</ProductVersion>")
.AppendLine(" <SchemaVersion>2.0</SchemaVersion>")
.Append(" <RootNamespace>").Append(assembly.RootNamespace).AppendLine("</RootNamespace>")
.Append(" <ProjectGuid>{").Append(ProjectGuid(m_AssemblyNameProvider.GetProjectName(assembly.Name, assembly.Defines))).AppendLine("}</ProjectGuid>")
.AppendLine(
" <ProjectTypeGuids>{E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>")
.AppendLine(" <OutputType>Library</OutputType>")
.AppendLine(" <AppDesignerFolder>Properties</AppDesignerFolder>")
.Append(" <AssemblyName>").Append(assembly.Name).AppendLine("</AssemblyName>")
.AppendLine(" <TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>")
.AppendLine(" <FileAlignment>512</FileAlignment>")
.AppendLine(" <BaseDirectory>.</BaseDirectory>")
.AppendLine(" </PropertyGroup>")
.AppendLine(" <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' \">")
.AppendLine(" <DebugSymbols>true</DebugSymbols>")
.AppendLine(" <DebugType>full</DebugType>")
.AppendLine(" <Optimize>false</Optimize>")
.Append(" <OutputPath>").Append(assembly.OutputPath).AppendLine("</OutputPath>");
var defines = new HashSet<string>(assembly.Defines);
foreach (var responseFileData in responseFilesData)
defines.UnionWith(responseFileData.Defines);
stringBuilder
.Append(" <DefineConstants>").CompatibleAppendJoin(';', defines).AppendLine("</DefineConstants>")
.AppendLine(" <ErrorReport>prompt</ErrorReport>");
var warningLevel = responseFilesDataArgs["warn"].Concat(responseFilesDataArgs["w"]).Distinct().FirstOrDefault();
stringBuilder
.Append(" <WarningLevel>").Append(!string.IsNullOrWhiteSpace(warningLevel) ? warningLevel : "4").AppendLine("</WarningLevel>")
.Append(" <NoWarn>").CompatibleAppendJoin(',', GetNoWarn(responseFilesDataArgs["nowarn"].ToList())).AppendLine("</NoWarn>")
.Append(" <AllowUnsafeBlocks>").Append(assembly.CompilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe)).AppendLine("</AllowUnsafeBlocks>");
AppendWarningAsError(stringBuilder, responseFilesDataArgs["warnaserror"],
responseFilesDataArgs["warnaserror-"], responseFilesDataArgs["warnaserror+"]);
// TODO: Can we have multiple documentation files in a project file?
foreach (var docFile in responseFilesDataArgs["doc"])
stringBuilder.Append(" <DocumentationFile>").Append(docFile).AppendLine("</DocumentationFile>");
var nullable = responseFilesDataArgs["nullable"].FirstOrDefault();
if (!string.IsNullOrEmpty(nullable))
stringBuilder.Append(" <Nullable>").Append(nullable).AppendLine("</Nullable>");
stringBuilder
.AppendLine(" </PropertyGroup>")
.AppendLine(" <PropertyGroup>")
.AppendLine(" <NoConfig>true</NoConfig>")
.AppendLine(" <NoStdLib>true</NoStdLib>")
.AppendLine(" <AddAdditionalExplicitAssemblyReferences>false</AddAdditionalExplicitAssemblyReferences>")
.AppendLine(" <ImplicitlyExpandNETStandardFacades>false</ImplicitlyExpandNETStandardFacades>")
.AppendLine(" <ImplicitlyExpandDesignTimeFacades>false</ImplicitlyExpandDesignTimeFacades>")
.AppendLine(" </PropertyGroup>");
var analyzers = GetRoslynAnalyzers(assembly, responseFilesDataArgs);
if (analyzers.Length > 0)
{
stringBuilder.AppendLine(" <ItemGroup>");
foreach (var analyzer in analyzers)
stringBuilder.AppendLine($" <Analyzer Include=\"{analyzer.NormalizePath()}\" />");
stringBuilder.AppendLine(" </ItemGroup>");
}
var additionalFiles = GetRoslynAdditionalFiles(assembly, responseFilesDataArgs);
if (additionalFiles.Length > 0)
{
stringBuilder.AppendLine(" <ItemGroup>");
foreach (var additionalFile in additionalFiles)
stringBuilder.AppendLine($" <AdditionalFiles Include=\"{additionalFile}\" />");
stringBuilder.AppendLine(" </ItemGroup>");
}
var configFile = GetGlobalAnalyzerConfigFile(assembly);
if (!string.IsNullOrEmpty(configFile))
{
stringBuilder
.AppendLine(" <ItemGroup>")
.Append(" <GlobalAnalyzerConfigFiles Include=\"").Append(configFile).AppendLine("\" />")
.AppendLine(" </ItemGroup>");
}
}
private static string GetGlobalAnalyzerConfigFile(ProjectPart assembly)
{
var configFile = string.Empty;
#if UNITY_2021_3 // https://github.com/JetBrains/resharper-unity/issues/2401
var type = assembly.CompilerOptions.GetType();
var propertyInfo = type.GetProperty("AnalyzerConfigPath");
if (propertyInfo != null && propertyInfo.GetValue(assembly.CompilerOptions) is string value)
{
configFile = value;
}
#elif UNITY_2022_2_OR_NEWER
configFile = assembly.CompilerOptions.AnalyzerConfigPath; // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Compilation.ScriptCompilerOptions.AnalyzerConfigPath.html
#endif
return configFile;
}
private static string[] GetRoslynAdditionalFiles(ProjectPart assembly, ILookup<string, string> otherResponseFilesData)
{
var additionalFilePathsFromCompilationPipeline = Array.Empty<string>();
#if UNITY_2021_3 // https://github.com/JetBrains/resharper-unity/issues/2401
var type = assembly.CompilerOptions.GetType();
var propertyInfo = type.GetProperty("RoslynAdditionalFilePaths");
if (propertyInfo != null && propertyInfo.GetValue(assembly.CompilerOptions) is string[] value)
{
additionalFilePathsFromCompilationPipeline = value;
}
#elif UNITY_2022_2_OR_NEWER // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Compilation.ScriptCompilerOptions.RoslynAdditionalFilePaths.html
additionalFilePathsFromCompilationPipeline = assembly.CompilerOptions.RoslynAdditionalFilePaths;
#endif
return otherResponseFilesData["additionalfile"]
.SelectMany(x=>x.Split(';'))
.Concat(additionalFilePathsFromCompilationPipeline)
.Distinct().ToArray();
}
string[] GetRoslynAnalyzers(ProjectPart assembly, ILookup<string, string> otherResponseFilesData)
{
#if UNITY_2020_2_OR_NEWER
return otherResponseFilesData["analyzer"].Concat(otherResponseFilesData["a"])
.SelectMany(x=>x.Split(';'))
#if !ROSLYN_ANALYZER_FIX
.Concat(m_AssemblyNameProvider.GetRoslynAnalyzerPaths())
#else
.Concat(assembly.CompilerOptions.RoslynAnalyzerDllPaths)
#endif
.Select(GetNormalisedAssemblyPath)
.Distinct()
.ToArray();
#else
return otherResponseFilesData["analyzer"].Concat(otherResponseFilesData["a"])
.SelectMany(x=>x.Split(';'))
.Distinct()
.Select(GetNormalisedAssemblyPath)
.ToArray();
#endif
}
private IEnumerable<string> GetRoslynAnalyzerRulesetPaths(ProjectPart assembly, ILookup<string, string> otherResponseFilesData)
{
var paths = new HashSet<string>(otherResponseFilesData["ruleset"]);
#if UNITY_2020_2_OR_NEWER
if (!string.IsNullOrEmpty(assembly.CompilerOptions.RoslynAnalyzerRulesetPath))
paths.Add(assembly.CompilerOptions.RoslynAnalyzerRulesetPath);
#endif
return paths.Select(GetNormalisedAssemblyPath);
}
private static void AppendWarningAsError(StringBuilder stringBuilder,
IEnumerable<string> args, IEnumerable<string> argsMinus, IEnumerable<string> argsPlus)
{
var treatWarningsAsErrors = false;
var warningIds = new List<string>();
var notWarningIds = new List<string>(argsMinus);
foreach (var s in args)
{
if (s == "+" || s == "") treatWarningsAsErrors = true;
else if (s == "-") treatWarningsAsErrors = false;
else warningIds.Add(s);
}
warningIds.AddRange(argsPlus);
stringBuilder.Append(" <TreatWarningsAsErrors>").Append(treatWarningsAsErrors) .AppendLine("</TreatWarningsAsErrors>");
if (warningIds.Count > 0)
stringBuilder.Append(" <WarningsAsErrors>").CompatibleAppendJoin(';', warningIds).AppendLine("</WarningsAsErrors>");
if (notWarningIds.Count > 0)
stringBuilder.Append(" <WarningsNotAsErrors>").CompatibleAppendJoin(';', notWarningIds) .AppendLine("</WarningsNotAsErrors>");
}
private void SyncSolution(StringBuilder stringBuilder, List<ProjectPart> islands, Type[] types)
{
SyncSolutionFileIfNotChanged(SolutionFile(), SolutionText(stringBuilder, islands), types);
}
private string SolutionText(StringBuilder stringBuilder, List<ProjectPart> islands)
{
stringBuilder
.AppendLine()
.AppendLine("Microsoft Visual Studio Solution File, Format Version 11.00")
.AppendLine("# Visual Studio 2010");
foreach (var island in islands)
{
var projectName = m_AssemblyNameProvider.GetProjectName(island.Name, island.Defines);
// GUID is for C# class libraries
stringBuilder
.Append("Project(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"")
.Append(island.Name)
.Append("\", \"")
.Append(projectName)
.Append(".csproj\", \"{")
.Append(ProjectGuid(projectName))
.AppendLine("}\"")
.AppendLine("EndProject");
}
stringBuilder.AppendLine("Global")
.AppendLine("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution")
.AppendLine("\t\tDebug|Any CPU = Debug|Any CPU")
.AppendLine("\tEndGlobalSection")
.AppendLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution");
foreach (var island in islands)
{
var projectGuid = ProjectGuid(m_AssemblyNameProvider.GetProjectName(island.Name, island.Defines));
stringBuilder
.Append("\t\t{").Append(projectGuid).AppendLine("}.Debug|Any CPU.ActiveCfg = Debug|Any CPU")
.Append("\t\t{").Append(projectGuid).AppendLine("}.Debug|Any CPU.Build.0 = Debug|Any CPU");
}
stringBuilder.AppendLine("\tEndGlobalSection")
.AppendLine("\tGlobalSection(SolutionProperties) = preSolution")
.AppendLine("\t\tHideSolutionNode = FALSE")
.AppendLine("\tEndGlobalSection")
.AppendLine("EndGlobal");
return stringBuilder.ToString();
}
private static ILookup<string, string> GetOtherArgumentsFromResponseFilesData(List<ResponseFileData> responseFilesData)
{
var paths = responseFilesData.SelectMany(x =>
{
return x.OtherArguments
.Where(a => a.StartsWith("/", StringComparison.Ordinal) || a.StartsWith("-", StringComparison.Ordinal))
.Select(b =>
{
var index = b.IndexOf(":", StringComparison.Ordinal);
if (index > 0 && b.Length > index)
{
var key = b.Substring(1, index - 1);
return new KeyValuePair<string, string>(key, b.Substring(index + 1));
}
const string warnaserror = "warnaserror";
if (b.Substring(1).StartsWith(warnaserror, StringComparison.Ordinal))
{
return new KeyValuePair<string, string>(warnaserror, b.Substring(warnaserror.Length + 1));
}
const string nullable = "nullable";
if (b.Substring(1).StartsWith(nullable, StringComparison.Ordinal))
{
var res = b.Substring(nullable.Length + 1);
if (string.IsNullOrWhiteSpace(res) || res.Equals("+"))
res = "enable";
else if (res.Equals("-"))
res = "disable";
return new KeyValuePair<string, string>(nullable, res);
}
return default;
});
})
.Distinct()
.ToLookup(o => o.Key, pair => pair.Value);
return paths;
}
private string GetLangVersion(IEnumerable<string> langVersionList, ProjectPart assembly)
{
var langVersion = langVersionList.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(langVersion))
return langVersion;
#if UNITY_2020_2_OR_NEWER
return assembly.CompilerOptions.LanguageVersion;
#else
return "latest";
#endif
}
public static IEnumerable<string> GetNoWarn(List<string> codes)
{
#if UNITY_2019_4 || UNITY_2020_1 // RIDER-77206 Unity 2020.1.3 'PlayerSettings' does not contain a definition for 'suppressCommonWarnings'
var type = typeof(PlayerSettings);
var propertyInfo = type.GetProperty("suppressCommonWarnings");
if (propertyInfo != null && propertyInfo.GetValue(null) is bool && (bool)propertyInfo.GetValue(null))
{
codes.AddRange(new[] {"0169", "0649"});
}
#elif UNITY_2020_2_OR_NEWER
if (PlayerSettings.suppressCommonWarnings)
codes.AddRange(new[] {"0169", "0649"});
#endif
return codes.Distinct();
}
private string ProjectGuid(string name)
{
if (!m_ProjectGuids.TryGetValue(name, out var guid))
{
guid = m_GUIDGenerator.ProjectGuid(m_ProjectName + name);
m_ProjectGuids.Add(name, guid);
}
return guid;
}
private string GetNormalisedAssemblyPath(string path)
{
if (!m_NormalisedPaths.TryGetValue(path, out var normalisedPath))
{
normalisedPath = Path.IsPathRooted(path) ? path : Path.GetFullPath(path);
normalisedPath = SecurityElement.Escape(normalisedPath).NormalizePath();
m_NormalisedPaths.Add(path, normalisedPath);
}
return normalisedPath;
}
private string GetAssemblyNameFromPath(string path)
{
if (!m_AssemblyNames.TryGetValue(path, out var name))
{
name = FileSystemUtil.FileNameWithoutExtension(path);
m_AssemblyNames.Add(path, name);
}
return name;
}
}
internal class FilePathTrie<TData>
{
private static readonly char[] Separators = { '\\', '/' };
private readonly TrieNode m_Root = new TrieNode();
private class TrieNode
{
public Dictionary<string, TrieNode> Children;
public TData Data;
}
public void Insert(string filePath, TData data)
{
var parts = filePath.Split(Separators);
var node = m_Root;
foreach (var part in parts)
{
if (node.Children == null)
node.Children = new Dictionary<string, TrieNode>(StringComparer.OrdinalIgnoreCase);
// ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd
if (!node.Children.ContainsKey(part))
node.Children[part] = new TrieNode();
node = node.Children[part];
}
node.Data = data;
}
public TData FindClosestMatch(string filePath)
{
var parts = filePath.Split(Separators);
var node = m_Root;
foreach (var part in parts)
{
if (node.Children != null && node.Children.TryGetValue(part, out var next))
node = next;
else
break;
}
return node.Data;
}
}
}