1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048 |
- 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;
- }
- }
- }
|