diff --git a/CHANGELOG.md b/CHANGELOG.md index 3795d56..6ea5459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Code Editor Package for Visual Studio +## [2.0.1] - 2020-03-19 + +When Visual Studio installation is compatible with C# 8.0, setup the language version to not prompt the user with unsupported constructs. (So far Unity only supports C# 7.3). +Use Unity's TypeCache to improve project generation speed. +Properly check for a managed assembly before displaying a warning regarding legacy PDB usage. +Add support for selective project generation (embedded, local, registry, git, builtin, player). + + ## [2.0.0] - 2019-11-06 - Improved Visual Studio and Visual Studio for Mac automatic discovery diff --git a/Editor/FileUtility.cs b/Editor/FileUtility.cs index 9e04ac4..3e98060 100644 --- a/Editor/FileUtility.cs +++ b/Editor/FileUtility.cs @@ -43,6 +43,8 @@ namespace Microsoft.Unity.VisualStudio.Editor if (Path.DirectorySeparatorChar == WinSeparator) path = path.Replace(UnixSeparator, WinSeparator); + if (Path.DirectorySeparatorChar == UnixSeparator) + path = path.Replace(WinSeparator, UnixSeparator); return path.Replace(string.Concat(WinSeparator, WinSeparator), WinSeparator.ToString()); } @@ -57,36 +59,5 @@ namespace Microsoft.Unity.VisualStudio.Editor return string.Equals(Path.GetDirectoryName(fileName), basePath, StringComparison.OrdinalIgnoreCase); } - - public static string FileNameWithoutExtension(string path) - { - if (string.IsNullOrEmpty(path)) - { - return ""; - } - - var indexOfDot = -1; - var indexOfSlash = 0; - for (var i = path.Length - 1; i >= 0; i--) - { - if (indexOfDot == -1 && path[i] == '.') - { - indexOfDot = i; - } - - if (indexOfSlash == 0 && path[i] == '/' || path[i] == '\\') - { - indexOfSlash = i + 1; - break; - } - } - - if (indexOfDot == -1) - { - indexOfDot = path.Length - 1; - } - - return path.Substring(indexOfSlash, indexOfDot - indexOfSlash); - } } } diff --git a/Editor/Image.cs b/Editor/Image.cs new file mode 100644 index 0000000..ca473df --- /dev/null +++ b/Editor/Image.cs @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +using System; +using System.IO; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + public sealed class Image : IDisposable { + + long position; + Stream stream; + + Image (Stream stream) + { + this.stream = stream; + this.position = stream.Position; + this.stream.Position = 0; + } + + bool Advance (int length) + { + if (stream.Position + length >= stream.Length) + return false; + + stream.Seek (length, SeekOrigin.Current); + return true; + } + + bool MoveTo (uint position) + { + if (position >= stream.Length) + return false; + + stream.Position = position; + return true; + } + + void IDisposable.Dispose () + { + stream.Position = position; + } + + ushort ReadUInt16 () + { + return (ushort) (stream.ReadByte () + | (stream.ReadByte () << 8)); + } + + uint ReadUInt32 () + { + return (uint) (stream.ReadByte () + | (stream.ReadByte () << 8) + | (stream.ReadByte () << 16) + | (stream.ReadByte () << 24)); + } + + bool IsManagedAssembly () + { + if (stream.Length < 318) + return false; + if (ReadUInt16 () != 0x5a4d) + return false; + if (!Advance (58)) + return false; + if (!MoveTo (ReadUInt32 ())) + return false; + if (ReadUInt32 () != 0x00004550) + return false; + if (!Advance (20)) + return false; + if (!Advance (ReadUInt16 () == 0x20b ? 222 : 206)) + return false; + + return ReadUInt32 () != 0; + } + + public static bool IsAssembly (string file) + { + if (file == null) + throw new ArgumentNullException ("file"); + + using (var stream = new FileStream (file, FileMode.Open, FileAccess.Read, FileShare.Read)) + return IsAssembly (stream); + } + + public static bool IsAssembly (Stream stream) + { + if (stream == null) + throw new ArgumentNullException (nameof(stream)); + if (!stream.CanRead) + throw new ArgumentException (nameof(stream)); + if (!stream.CanSeek) + throw new ArgumentException (nameof(stream)); + + using (var image = new Image (stream)) + return image.IsManagedAssembly (); + } + } +} diff --git a/Editor/Image.cs.meta b/Editor/Image.cs.meta new file mode 100644 index 0000000..4bcc457 --- /dev/null +++ b/Editor/Image.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8e6c7ea7c059fb547b6723aaf225900b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ProjectGeneration.meta b/Editor/ProjectGeneration.meta new file mode 100644 index 0000000..1618d07 --- /dev/null +++ b/Editor/ProjectGeneration.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8beeeeebc0857854d8b4e2c2895dd7a9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ProjectGeneration/AssemblyNameProvider.cs b/Editor/ProjectGeneration/AssemblyNameProvider.cs new file mode 100644 index 0000000..a663067 --- /dev/null +++ b/Editor/ProjectGeneration/AssemblyNameProvider.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEditor.Compilation; +using UnityEditor.PackageManager; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + public interface IAssemblyNameProvider + { + string[] ProjectSupportedExtensions { get; } + string ProjectGenerationRootNamespace { get; } + ProjectGenerationFlag ProjectGenerationFlag { get; } + + string GetAssemblyNameFromScriptPath(string path); + string GetAssemblyName(string assemblyOutputPath, string assemblyName); + bool IsInternalizedPackagePath(string path); + IEnumerable GetAssemblies(Func shouldFileBePartOfSolution); + IEnumerable GetAllAssetPaths(); + UnityEditor.PackageManager.PackageInfo FindForAssetPath(string assetPath); + ResponseFileData ParseResponseFile(string responseFilePath, string projectDirectory, string[] systemReferenceDirectories); + void ToggleProjectGeneration(ProjectGenerationFlag preference); + } + + public class AssemblyNameProvider : IAssemblyNameProvider + { + ProjectGenerationFlag m_ProjectGenerationFlag = (ProjectGenerationFlag)EditorPrefs.GetInt("unity_project_generation_flag", 0); + + public string[] ProjectSupportedExtensions => EditorSettings.projectGenerationUserExtensions; + + public string ProjectGenerationRootNamespace => EditorSettings.projectGenerationRootNamespace; + + public ProjectGenerationFlag ProjectGenerationFlag + { + get => m_ProjectGenerationFlag; + private set + { + EditorPrefs.SetInt("unity_project_generation_flag", (int)value); + m_ProjectGenerationFlag = value; + } + } + + public string GetAssemblyNameFromScriptPath(string path) + { + return CompilationPipeline.GetAssemblyNameFromScriptPath(path); + } + + public IEnumerable GetAssemblies(Func shouldFileBePartOfSolution) + { + foreach (var assembly in CompilationPipeline.GetAssemblies()) + { + if (assembly.sourceFiles.Any(shouldFileBePartOfSolution)) + { + yield return new Assembly(assembly.name, @"Temp\Bin\Debug\", assembly.sourceFiles, new[] { "DEBUG", "TRACE" }.Concat(assembly.defines).Concat(EditorUserBuildSettings.activeScriptCompilationDefines).ToArray(), assembly.assemblyReferences, assembly.compiledAssemblyReferences, assembly.flags) + { + compilerOptions = + { + ResponseFiles = assembly.compilerOptions.ResponseFiles, + AllowUnsafeCode = assembly.compilerOptions.AllowUnsafeCode, + ApiCompatibilityLevel = assembly.compilerOptions.ApiCompatibilityLevel + } + }; + } + } + + if (ProjectGenerationFlag.HasFlag(ProjectGenerationFlag.PlayerAssemblies)) + { + foreach (var assembly in CompilationPipeline.GetAssemblies(AssembliesType.Player).Where(assembly => assembly.sourceFiles.Any(shouldFileBePartOfSolution))) + { + yield return new Assembly(assembly.name, @"Temp\Bin\Debug\Player\", assembly.sourceFiles, new[] { "DEBUG", "TRACE" }.Concat(assembly.defines).ToArray(), assembly.assemblyReferences, assembly.compiledAssemblyReferences, assembly.flags) + { + compilerOptions = + { + ResponseFiles = assembly.compilerOptions.ResponseFiles, + AllowUnsafeCode = assembly.compilerOptions.AllowUnsafeCode, + ApiCompatibilityLevel = assembly.compilerOptions.ApiCompatibilityLevel + } + }; + } + } + } + + public string GetCompileOutputPath(string assemblyName) + { + if (assemblyName.EndsWith(".Player", StringComparison.Ordinal)) + { + return @"Temp\Bin\Debug\Player\"; + } + else + { + return @"Temp\Bin\Debug\"; + } + } + + public IEnumerable GetAllAssetPaths() + { + return AssetDatabase.GetAllAssetPaths(); + } + + public UnityEditor.PackageManager.PackageInfo FindForAssetPath(string assetPath) + { + return UnityEditor.PackageManager.PackageInfo.FindForAssetPath(assetPath); + } + + public bool IsInternalizedPackagePath(string path) + { + if (string.IsNullOrEmpty(path.Trim())) + { + return false; + } + var packageInfo = FindForAssetPath(path); + if (packageInfo == null) + { + return false; + } + var packageSource = packageInfo.source; + switch (packageSource) + { + case PackageSource.Embedded: + return !ProjectGenerationFlag.HasFlag(ProjectGenerationFlag.Embedded); + case PackageSource.Registry: + return !ProjectGenerationFlag.HasFlag(ProjectGenerationFlag.Registry); + case PackageSource.BuiltIn: + return !ProjectGenerationFlag.HasFlag(ProjectGenerationFlag.BuiltIn); + case PackageSource.Unknown: + return !ProjectGenerationFlag.HasFlag(ProjectGenerationFlag.Unknown); + case PackageSource.Local: + return !ProjectGenerationFlag.HasFlag(ProjectGenerationFlag.Local); + case PackageSource.Git: + return !ProjectGenerationFlag.HasFlag(ProjectGenerationFlag.Git); + case PackageSource.LocalTarball: + return !ProjectGenerationFlag.HasFlag(ProjectGenerationFlag.LocalTarBall); + } + + return false; + } + + public ResponseFileData ParseResponseFile(string responseFilePath, string projectDirectory, string[] systemReferenceDirectories) + { + return CompilationPipeline.ParseResponseFile( + responseFilePath, + projectDirectory, + systemReferenceDirectories + ); + } + + public void ToggleProjectGeneration(ProjectGenerationFlag preference) + { + if (ProjectGenerationFlag.HasFlag(preference)) + { + ProjectGenerationFlag ^= preference; + } + else + { + ProjectGenerationFlag |= preference; + } + } + + public void ResetProjectGenerationFlag() + { + ProjectGenerationFlag = ProjectGenerationFlag.None; + } + + public string GetAssemblyName(string assemblyOutputPath, string assemblyName) + { + return assemblyOutputPath.EndsWith(@"\Player\", StringComparison.Ordinal) ? assemblyName + ".Player" : assemblyName; + } + } +} diff --git a/Editor/ProjectGeneration/AssemblyNameProvider.cs.meta b/Editor/ProjectGeneration/AssemblyNameProvider.cs.meta new file mode 100644 index 0000000..07768d7 --- /dev/null +++ b/Editor/ProjectGeneration/AssemblyNameProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 57537f08f8e923f488e4aadabb831c9b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ProjectGeneration/FileIOProvider.cs b/Editor/ProjectGeneration/FileIOProvider.cs new file mode 100644 index 0000000..c22548c --- /dev/null +++ b/Editor/ProjectGeneration/FileIOProvider.cs @@ -0,0 +1,31 @@ +using System.IO; +using System.Text; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + public interface IFileIO + { + bool Exists(string fileName); + + string ReadAllText(string fileName); + void WriteAllText(string fileName, string content); + } + + class FileIOProvider : IFileIO + { + public bool Exists(string fileName) + { + return File.Exists(fileName); + } + + public string ReadAllText(string fileName) + { + return File.ReadAllText(fileName); + } + + public void WriteAllText(string fileName, string content) + { + File.WriteAllText(fileName, content, Encoding.UTF8); + } + } +} \ No newline at end of file diff --git a/Editor/ProjectGeneration/FileIOProvider.cs.meta b/Editor/ProjectGeneration/FileIOProvider.cs.meta new file mode 100644 index 0000000..bc7a88d --- /dev/null +++ b/Editor/ProjectGeneration/FileIOProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ec80b1fb8938b3b4ab442d10390c5315 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ProjectGeneration/GUIDProvider.cs b/Editor/ProjectGeneration/GUIDProvider.cs new file mode 100644 index 0000000..3b1d16e --- /dev/null +++ b/Editor/ProjectGeneration/GUIDProvider.cs @@ -0,0 +1,21 @@ +namespace Microsoft.Unity.VisualStudio.Editor +{ + public interface IGUIDGenerator + { + string ProjectGuid(string projectName, string assemblyName); + string SolutionGuid(string projectName, ScriptingLanguage scriptingLanguage); + } + + class GUIDProvider : IGUIDGenerator + { + public string ProjectGuid(string projectName, string assemblyName) + { + return SolutionGuidGenerator.GuidForProject(projectName + assemblyName); + } + + public string SolutionGuid(string projectName, ScriptingLanguage scriptingLanguage) + { + return SolutionGuidGenerator.GuidForSolution(projectName, scriptingLanguage); + } + } +} \ No newline at end of file diff --git a/Editor/ProjectGeneration/GUIDProvider.cs.meta b/Editor/ProjectGeneration/GUIDProvider.cs.meta new file mode 100644 index 0000000..c1facea --- /dev/null +++ b/Editor/ProjectGeneration/GUIDProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7652904b1008e324fb7cfb952ea87656 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ProjectGeneration.cs b/Editor/ProjectGeneration/ProjectGeneration.cs similarity index 67% rename from Editor/ProjectGeneration.cs rename to Editor/ProjectGeneration/ProjectGeneration.cs index 5ffc9e0..6fa0782 100644 --- a/Editor/ProjectGeneration.cs +++ b/Editor/ProjectGeneration/ProjectGeneration.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using SR = System.Reflection; using System.Security; using System.Security.Cryptography; using System.Text; @@ -26,50 +27,15 @@ namespace Microsoft.Unity.VisualStudio.Editor CSharp } - public interface IGenerator { + public interface IGenerator + { bool SyncIfNeeded(IEnumerable affectedFiles, IEnumerable reimportedFiles); void Sync(); bool HasSolutionBeenGenerated(); + bool IsSupportedFile(string path); string SolutionFile(); string ProjectDirectory { get; } - void GenerateAll(bool generateAll); - bool IsSupportedFile(string path); - } - - public interface IAssemblyNameProvider - { - string GetAssemblyNameFromScriptPath(string path); - IEnumerable GetAllAssemblies(Func shouldFileBePartOfSolution); - IEnumerable GetAllAssetPaths(); - UnityEditor.PackageManager.PackageInfo FindForAssetPath(string assetPath); - } - - public struct TestSettings { - public bool ShouldSync; - public Dictionary SyncPath; - } - - class AssemblyNameProvider : IAssemblyNameProvider - { - public string GetAssemblyNameFromScriptPath(string path) - { - return CompilationPipeline.GetAssemblyNameFromScriptPath(path); - } - - public IEnumerable GetAllAssemblies(Func shouldFileBePartOfSolution) - { - return CompilationPipeline.GetAssemblies().Where(i => 0 < i.sourceFiles.Length && i.sourceFiles.Any(shouldFileBePartOfSolution)); - } - - public IEnumerable GetAllAssetPaths() - { - return AssetDatabase.GetAllAssetPaths(); - } - - public UnityEditor.PackageManager.PackageInfo FindForAssetPath(string assetPath) - { - return UnityEditor.PackageManager.PackageInfo.FindForAssetPath(assetPath); - } + IAssemblyNameProvider AssemblyNameProvider { get; } } public class ProjectGeneration : IGenerator @@ -99,31 +65,28 @@ namespace Microsoft.Unity.VisualStudio.Editor public string ProjectDirectory { get; } - public TestSettings Settings { get; set; } - readonly string m_ProjectName; readonly IAssemblyNameProvider m_AssemblyNameProvider; - bool m_ShouldGenerateAll; + readonly IFileIO m_FileIOProvider; + readonly IGUIDGenerator m_GUIDGenerator; + VisualStudioInstallation m_CurrentInstallation; + public IAssemblyNameProvider AssemblyNameProvider => m_AssemblyNameProvider; - public ProjectGeneration() : this(Directory.GetParent(Application.dataPath).FullName, new AssemblyNameProvider()) + public ProjectGeneration() : this(Directory.GetParent(Application.dataPath).FullName) { } - public ProjectGeneration(string tempDirectory) : this(tempDirectory, new AssemblyNameProvider()) { + public ProjectGeneration(string tempDirectory) : this(tempDirectory, new AssemblyNameProvider(), new FileIOProvider(), new GUIDProvider()) + { } - public ProjectGeneration(string tempDirectory, IAssemblyNameProvider assemblyNameProvider) { - Settings = new TestSettings { ShouldSync = true }; + public ProjectGeneration(string tempDirectory, IAssemblyNameProvider assemblyNameProvider, IFileIO fileIoProvider, IGUIDGenerator guidGenerator) + { ProjectDirectory = tempDirectory.Replace('\\', '/'); m_ProjectName = Path.GetFileName(ProjectDirectory); m_AssemblyNameProvider = assemblyNameProvider; - - SetupProjectSupportedExtensions(); - } - - public void GenerateAll(bool generateAll) - { - m_ShouldGenerateAll = generateAll; + m_FileIOProvider = fileIoProvider; + m_GUIDGenerator = guidGenerator; } /// @@ -161,10 +124,19 @@ namespace Microsoft.Unity.VisualStudio.Editor return k_ReimportSyncExtensions.Contains(new FileInfo(asset).Extension); } + private void RefreshCurrentInstallation() + { + var editor = CodeEditor.CurrentEditor as VisualStudioEditor; + editor?.TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, out m_CurrentInstallation); + } + public void Sync() { + // We need the exact VS version/capabilities to tweak project generation (analyzers/langversion) + RefreshCurrentInstallation(); + SetupProjectSupportedExtensions(); - bool externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles(); + var externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles(); if (!externalCodeAlreadyGeneratedProjects) { @@ -175,19 +147,19 @@ namespace Microsoft.Unity.VisualStudio.Editor public bool HasSolutionBeenGenerated() { - return File.Exists(SolutionFile()); + return m_FileIOProvider.Exists(SolutionFile()); } void SetupProjectSupportedExtensions() { - m_ProjectSupportedExtensions = EditorSettings.projectGenerationUserExtensions; + m_ProjectSupportedExtensions = m_AssemblyNameProvider.ProjectSupportedExtensions; m_BuiltinSupportedExtensions = EditorSettings.projectGenerationBuiltinExtensions; } bool ShouldFileBePartOfSolution(string file) { // Exclude files coming from packages except if they are internalized. - if (!m_ShouldGenerateAll && IsInternalizedPackagePath(file)) + if (m_AssemblyNameProvider.IsInternalizedPackagePath(file)) { return false; } @@ -227,9 +199,9 @@ namespace Microsoft.Unity.VisualStudio.Editor return false; } - static ScriptingLanguage ScriptingLanguageFor(Assembly island) + static ScriptingLanguage ScriptingLanguageFor(Assembly assembly) { - var files = island.sourceFiles; + var files = assembly.sourceFiles; if (files.Length == 0) return ScriptingLanguage.None; @@ -242,42 +214,22 @@ namespace Microsoft.Unity.VisualStudio.Editor return GetExtensionWithoutDot(path) == "cs" ? ScriptingLanguage.CSharp : ScriptingLanguage.None; } - static List SafeGetTypes(System.Reflection.Assembly a) - { - var ret = new List(); - - try - { - ret = a.GetTypes().ToList(); - } - catch (System.Reflection.ReflectionTypeLoadException rtl) - { - ret = rtl.Types.ToList(); - } - catch (Exception) - { - return new List(); - } - - return ret.Where(r => r != null).ToList(); - } - public void GenerateAndWriteSolutionAndProjects() { - // Only synchronize islands that have associated source files and ones that we actually want in the project. + // Only synchronize assemblies 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. - var assemblies = m_AssemblyNameProvider.GetAllAssemblies(ShouldFileBePartOfSolution); + var assemblies = m_AssemblyNameProvider.GetAssemblies(ShouldFileBePartOfSolution); var allAssetProjectParts = GenerateAllAssetProjectParts(); - var monoIslands = assemblies.ToList(); + var assemblyList = assemblies.ToList(); - SyncSolution(monoIslands); - var allProjectIslands = RelevantIslandsForMode(monoIslands).ToList(); - foreach (Assembly assembly in allProjectIslands) + SyncSolution(assemblyList); + var allProjectAssemblies = RelevantAssembliesForMode(assemblyList).ToList(); + foreach (Assembly assembly in allProjectAssemblies) { var responseFileData = ParseResponseFileData(assembly); - SyncProject(assembly, allAssetProjectParts, responseFileData, allProjectIslands); + SyncProject(assembly, allAssetProjectParts, responseFileData, allProjectAssemblies); } } @@ -285,8 +237,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { var systemReferenceDirectories = CompilationPipeline.GetSystemAssemblyDirectories(assembly.compilerOptions.ApiCompatibilityLevel); - Dictionary responseFilesData = assembly.compilerOptions.ResponseFiles.ToDictionary(x => x, x => CompilationPipeline.ParseResponseFile( - Path.Combine(ProjectDirectory, x), + Dictionary responseFilesData = assembly.compilerOptions.ResponseFiles.ToDictionary(x => x, x => m_AssemblyNameProvider.ParseResponseFile( + x, ProjectDirectory, systemReferenceDirectories )); @@ -313,7 +265,7 @@ namespace Microsoft.Unity.VisualStudio.Editor foreach (string asset in m_AssemblyNameProvider.GetAllAssetPaths()) { // Exclude files coming from packages except if they are internalized. - if (!m_ShouldGenerateAll && IsInternalizedPackagePath(asset)) + if (m_AssemblyNameProvider.IsInternalizedPackagePath(asset)) { continue; } @@ -328,7 +280,7 @@ namespace Microsoft.Unity.VisualStudio.Editor continue; } - assemblyName = FileUtility.FileNameWithoutExtension(assemblyName); + assemblyName = Path.GetFileNameWithoutExtension(assemblyName); if (!stringBuilders.TryGetValue(assemblyName, out var projectBuilder)) { @@ -348,28 +300,13 @@ namespace Microsoft.Unity.VisualStudio.Editor return result; } - bool IsInternalizedPackagePath(string file) - { - if (string.IsNullOrWhiteSpace(file)) - { - return false; - } - - var packageInfo = m_AssemblyNameProvider.FindForAssetPath(file); - if (packageInfo == null) { - return false; - } - var packageSource = packageInfo.source; - return packageSource != PackageSource.Embedded && packageSource != PackageSource.Local; - } - void SyncProject( - Assembly island, + Assembly assembly, Dictionary allAssetsProjectParts, IEnumerable responseFilesData, - List allProjectIslands) + List allProjectAssemblies) { - SyncProjectFileIfNotChanged(ProjectFile(island), ProjectText(island, allAssetsProjectParts, responseFilesData, allProjectIslands)); + SyncProjectFileIfNotChanged(ProjectFile(assembly), ProjectText(assembly, allAssetsProjectParts, responseFilesData, allProjectAssemblies)); } void SyncProjectFileIfNotChanged(string path, string newContents) @@ -389,114 +326,88 @@ namespace Microsoft.Unity.VisualStudio.Editor SyncFileIfNotChanged(path, newContents); } + static IEnumerable GetPostProcessorCallbacks(string name) + { + return TypeCache + .GetTypesDerivedFrom() + .Select(t => t.GetMethod(name, SR.BindingFlags.Public | SR.BindingFlags.NonPublic | SR.BindingFlags.Static)) + .Where(m => m!= null); + } + static void OnGeneratedCSProjectFiles() { - IEnumerable types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => SafeGetTypes(x)) - .Where(x => typeof(AssetPostprocessor).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract); - var args = new object[0]; - foreach (var type in types) + foreach(var method in GetPostProcessorCallbacks(nameof(OnGeneratedCSProjectFiles))) { - var method = type.GetMethod("OnGeneratedCSProjectFiles", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - if (method == null) - { - continue; - } - method.Invoke(null, args); + method.Invoke(null, Array.Empty()); } } static bool OnPreGeneratingCSProjectFiles() { - IEnumerable types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => SafeGetTypes(x)) - .Where(x => typeof(AssetPostprocessor).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract); bool result = false; - foreach (var type in types) + + foreach(var method in GetPostProcessorCallbacks(nameof(OnPreGeneratingCSProjectFiles))) { - 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; + var retValue = method.Invoke(null, Array.Empty()); + if (method.ReturnType == typeof(bool)) + { + result |= (bool)retValue; } } + return result; } + static string InvokeAssetPostProcessorGenerationCallbacks(string name, string path, string content) + { + foreach(var method in GetPostProcessorCallbacks(name)) + { + var args = new [] { path, content }; + var returnValue = method.Invoke(null, args); + if (method.ReturnType == typeof(string)) + { + // We want to chain content update between invocations + content = (string)returnValue; + } + } + + return content; + } + static string OnGeneratedCSProject(string path, string content) { - IEnumerable types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => SafeGetTypes(x)) - .Where(x => typeof(AssetPostprocessor).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract); - 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; + return InvokeAssetPostProcessorGenerationCallbacks(nameof(OnGeneratedCSProject), path, content); } static string OnGeneratedSlnSolution(string path, string content) { - IEnumerable types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => SafeGetTypes(x)) - .Where(x => typeof(AssetPostprocessor).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract); - 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; + return InvokeAssetPostProcessorGenerationCallbacks(nameof(OnGeneratedSlnSolution), path, content); } void SyncFileIfNotChanged(string filename, string newContents) { - if (File.Exists(filename) && - newContents == File.ReadAllText(filename)) + try { - return; + if (m_FileIOProvider.Exists(filename) && newContents == m_FileIOProvider.ReadAllText(filename)) + { + return; + } + } + catch (Exception exception) + { + Debug.LogException(exception); } - if (Settings.ShouldSync) - { - File.WriteAllText(filename, newContents, Encoding.UTF8); - } - else - { - var utf8 = Encoding.UTF8; - byte[] utfBytes = utf8.GetBytes(newContents); - Settings.SyncPath[filename] = utf8.GetString(utfBytes, 0, utfBytes.Length); - } + m_FileIOProvider.WriteAllText(filename, newContents); } string ProjectText(Assembly assembly, Dictionary allAssetsProjectParts, IEnumerable responseFilesData, - List allProjectIslands) + List allProjectAsemblies) { var projectBuilder = new StringBuilder(ProjectHeader(assembly, responseFilesData)); var references = new List(); - var projectReferences = new List(); projectBuilder.Append(@" ").Append(k_WindowsNewline); foreach (string file in assembly.sourceFiles) @@ -517,62 +428,39 @@ namespace Microsoft.Unity.VisualStudio.Editor } projectBuilder.Append(@" ").Append(k_WindowsNewline); - var assemblyName = FileUtility.FileNameWithoutExtension(assembly.outputPath); - projectBuilder.Append(@" ").Append(k_WindowsNewline); + // Append additional non-script files that should be included in project generation. - if (allAssetsProjectParts.TryGetValue(assemblyName, out var additionalAssetsForProject)) + if (allAssetsProjectParts.TryGetValue(assembly.name, out var additionalAssetsForProject)) projectBuilder.Append(additionalAssetsForProject); - var islandRefs = references.Union(assembly.allReferences); - - foreach (string reference in islandRefs) + var responseRefs = responseFilesData.SelectMany(x => x.FullPathReferences.Select(r => r)); + var internalAssemblyReferences = assembly.assemblyReferences + .Where(i => !i.sourceFiles.Any(ShouldFileBePartOfSolution)).Select(i => i.outputPath); + var allReferences = + assembly.compiledAssemblyReferences + .Union(responseRefs) + .Union(references) + .Union(internalAssemblyReferences); + foreach (var reference in allReferences) { - if (reference.EndsWith("/UnityEditor.dll", StringComparison.Ordinal) - || reference.EndsWith("/UnityEngine.dll", StringComparison.Ordinal) - || reference.EndsWith("\\UnityEditor.dll", StringComparison.Ordinal) - || reference.EndsWith("\\UnityEngine.dll", StringComparison.Ordinal)) - continue; - - var match = k_ScriptReferenceExpression.Match(reference); - if (match.Success) - { - // assume csharp language - // Add a reference to a project except if it's a reference to a script assembly - // that we are not generating a project for. This will be the case for assemblies - // coming from .assembly.json files in non-internalized packages. - var dllName = match.Groups["dllname"].Value; - if (allProjectIslands.Any(i => Path.GetFileName(i.outputPath) == dllName)) - { - projectReferences.Add(match); - continue; - } - } - string fullReference = Path.IsPathRooted(reference) ? reference : Path.Combine(ProjectDirectory, reference); - AppendReference(fullReference, projectBuilder); } - var responseRefs = responseFilesData.SelectMany(x => x.FullPathReferences.Select(r => r)); - foreach (var reference in responseRefs) - { - AppendReference(reference, projectBuilder); - } projectBuilder.Append(@" ").Append(k_WindowsNewline); - if (0 < projectReferences.Count) + if (0 < assembly.assemblyReferences.Length) { - projectBuilder.Append(@" ").Append(k_WindowsNewline); - foreach (Match reference in projectReferences) + projectBuilder.Append(" ").Append(k_WindowsNewline); + foreach (Assembly reference in assembly.assemblyReferences.Where(i => i.sourceFiles.Any(ShouldFileBePartOfSolution))) { - var referencedProject = reference.Groups["project"].Value; - - projectBuilder.Append(" ").Append(k_WindowsNewline); - projectBuilder.Append(" {").Append(ProjectGuid(Path.Combine("Temp", reference.Groups["project"].Value + ".dll"))).Append("}").Append(k_WindowsNewline); - projectBuilder.Append(" ").Append(referencedProject).Append("").Append(k_WindowsNewline); + projectBuilder.Append(" ").Append(k_WindowsNewline); + projectBuilder.Append(" {").Append(ProjectGuid(reference)).Append("}").Append(k_WindowsNewline); + projectBuilder.Append(" ").Append(reference.name).Append("").Append(k_WindowsNewline); projectBuilder.Append(" ").Append(k_WindowsNewline); } + projectBuilder.Append(@" ").Append(k_WindowsNewline); } @@ -599,15 +487,15 @@ namespace Microsoft.Unity.VisualStudio.Editor void AppendReference(string fullReference, StringBuilder projectBuilder) { var escapedFullPath = EscapedRelativePathFor(fullReference); - projectBuilder.Append(" ").Append(k_WindowsNewline); + projectBuilder.Append(" ").Append(k_WindowsNewline); projectBuilder.Append(" ").Append(escapedFullPath).Append("").Append(k_WindowsNewline); projectBuilder.Append(" ").Append(k_WindowsNewline); } public string ProjectFile(Assembly assembly) { - return Path.Combine(ProjectDirectory, $"{FileUtility.FileNameWithoutExtension(assembly.outputPath)}.csproj"); - } + return Path.Combine(ProjectDirectory, $"{m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, assembly.name)}.csproj"); + } private static readonly Regex InvalidCharactersRegexPattern = new Regex(@"\?|&|\*|""|<|>|\||#|%|\^|;" + (VisualStudioEditor.IsWindows ? "" : "|:")); public string SolutionFile() @@ -616,7 +504,7 @@ namespace Microsoft.Unity.VisualStudio.Editor } string ProjectHeader( - Assembly island, + Assembly assembly, IEnumerable responseFilesData ) { @@ -625,23 +513,33 @@ namespace Microsoft.Unity.VisualStudio.Editor const string baseDirectory = "."; var targetFrameworkVersion = "v4.7.1"; - var targetLanguageVersion = "latest"; + var targetLanguageVersion = "latest"; // danger: latest is not the same absolute value depending on the VS version. - var projectType = ProjectTypeOf(island.outputPath); + if (m_CurrentInstallation != null && m_CurrentInstallation.SupportsCSharp8) + { + // Current installation is compatible with C# 8. + // But Unity has no support for C# 8 constructs so far, so tell the compiler to accept only C# 7.3 or lower. + targetLanguageVersion = "7.3"; + } + + var projectType = ProjectTypeOf(assembly.name); var arguments = new object[] { - toolsVersion, productVersion, ProjectGuid(island.outputPath), + toolsVersion, + productVersion, + ProjectGuid(assembly), XmlFilename(FileUtility.Normalize(InternalEditorUtility.GetEngineAssemblyPath())), XmlFilename(FileUtility.Normalize(InternalEditorUtility.GetEditorAssemblyPath())), - string.Join(";", new[] { "DEBUG", "TRACE" }.Concat(EditorUserBuildSettings.activeScriptCompilationDefines).Concat(island.defines).Concat(responseFilesData.SelectMany(x => x.Defines)).Distinct().ToArray()), + string.Join(";", assembly.defines.Concat(responseFilesData.SelectMany(x => x.Defines)).Distinct().ToArray()), MSBuildNamespaceUri, - FileUtility.FileNameWithoutExtension(island.outputPath), - EditorSettings.projectGenerationRootNamespace, + assembly.name, + assembly.outputPath, + m_AssemblyNameProvider.ProjectGenerationRootNamespace, targetFrameworkVersion, targetLanguageVersion, baseDirectory, - island.compilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe), + assembly.compilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe), // flavoring projectType + ":" + (int)projectType, EditorUserBuildSettings.activeBuildTarget + ":" + (int)EditorUserBuildSettings.activeBuildTarget, @@ -706,7 +604,7 @@ namespace Microsoft.Unity.VisualStudio.Editor return string.Join("\r\n", @" ", @" ", - @"