diff --git a/CHANGELOG.md b/CHANGELOG.md index 673f4e3..bace3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Code Editor Package for Visual Studio +## [2.0.5] - 2020-10-30 + +Integration: + +Disable legacy pdb symbol checking for Unity packages + + ## [2.0.3] - 2020-09-09 Project generation: diff --git a/Editor/AssemblyInfo.cs b/Editor/AssemblyInfo.cs new file mode 100644 index 0000000..88f6069 --- /dev/null +++ b/Editor/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Unity.VisualStudio.EditorTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/Editor/AssemblyInfo.cs.meta b/Editor/AssemblyInfo.cs.meta new file mode 100644 index 0000000..5262a54 --- /dev/null +++ b/Editor/AssemblyInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d791d407901442e49862d3aa783ce8af +timeCreated: 1602756877 \ No newline at end of file diff --git a/Editor/Discovery.cs b/Editor/Discovery.cs index 6ef89b9..893ed3a 100644 --- a/Editor/Discovery.cs +++ b/Editor/Discovery.cs @@ -15,7 +15,10 @@ namespace Microsoft.Unity.VisualStudio.Editor { internal static class Discovery { - public static IEnumerable GetVisualStudioInstallations() + internal const string ManagedWorkload = "Microsoft.VisualStudio.Workload.ManagedGame"; + + + public static IEnumerable GetVisualStudioInstallations() { if (VisualStudioEditor.IsWindows) { @@ -45,7 +48,7 @@ namespace Microsoft.Unity.VisualStudio.Editor return false; } - public static bool TryDiscoverInstallation(string editorPath, out VisualStudioInstallation installation) + public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation) { installation = null; @@ -61,7 +64,7 @@ namespace Microsoft.Unity.VisualStudio.Editor // On Mac we use the .app folder, so we need to access to main assembly if (VisualStudioEditor.IsOSX) fvi = Path.Combine(editorPath, "Contents", "Resources", "lib", "monodevelop", "bin", "VisualStudio.exe"); - + if (!File.Exists(fvi)) return false; @@ -82,7 +85,7 @@ namespace Microsoft.Unity.VisualStudio.Editor } #region VsWhere Json Schema - #pragma warning disable CS0649 +#pragma warning disable CS0649 [Serializable] internal class VsWhereResult { @@ -95,7 +98,7 @@ namespace Microsoft.Unity.VisualStudio.Editor public IEnumerable ToVisualStudioInstallations() { - foreach(var entry in entries) + foreach (var entry in entries) { yield return new VisualStudioInstallation() { @@ -123,7 +126,7 @@ namespace Microsoft.Unity.VisualStudio.Editor public string productDisplayVersion; // non parseable like "16.3.0 Preview 3.0" public string buildVersion; } - #pragma warning restore CS3021 +#pragma warning restore CS3021 #endregion private static IEnumerable QueryVsWhere() diff --git a/Editor/FileUtility.cs b/Editor/FileUtility.cs index 3e98060..3e0817f 100644 --- a/Editor/FileUtility.cs +++ b/Editor/FileUtility.cs @@ -49,6 +49,14 @@ namespace Microsoft.Unity.VisualStudio.Editor return path.Replace(string.Concat(WinSeparator, WinSeparator), WinSeparator.ToString()); } + public static string NormalizeWindowsToUnix(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + + return path.Replace(WinSeparator, UnixSeparator); + } + internal static bool IsFileInProjectDirectory(string fileName) { var basePath = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); diff --git a/Editor/Image.cs b/Editor/Image.cs index ca473df..b2b4fd3 100644 --- a/Editor/Image.cs +++ b/Editor/Image.cs @@ -7,28 +7,29 @@ using System.IO; namespace Microsoft.Unity.VisualStudio.Editor { - public sealed class Image : IDisposable { + public sealed class Image : IDisposable + { long position; Stream stream; - Image (Stream stream) + Image(Stream stream) { this.stream = stream; this.position = stream.Position; this.stream.Position = 0; } - bool Advance (int length) + bool Advance(int length) { if (stream.Position + length >= stream.Length) return false; - stream.Seek (length, SeekOrigin.Current); + stream.Seek(length, SeekOrigin.Current); return true; } - bool MoveTo (uint position) + bool MoveTo(uint position) { if (position >= stream.Length) return false; @@ -37,65 +38,65 @@ namespace Microsoft.Unity.VisualStudio.Editor return true; } - void IDisposable.Dispose () + void IDisposable.Dispose() { stream.Position = position; } - ushort ReadUInt16 () + ushort ReadUInt16() { - return (ushort) (stream.ReadByte () - | (stream.ReadByte () << 8)); + return (ushort)(stream.ReadByte() + | (stream.ReadByte() << 8)); } - uint ReadUInt32 () + uint ReadUInt32() { - return (uint) (stream.ReadByte () - | (stream.ReadByte () << 8) - | (stream.ReadByte () << 16) - | (stream.ReadByte () << 24)); + return (uint)(stream.ReadByte() + | (stream.ReadByte() << 8) + | (stream.ReadByte() << 16) + | (stream.ReadByte() << 24)); } - bool IsManagedAssembly () + bool IsManagedAssembly() { if (stream.Length < 318) return false; - if (ReadUInt16 () != 0x5a4d) + if (ReadUInt16() != 0x5a4d) return false; - if (!Advance (58)) + if (!Advance(58)) return false; - if (!MoveTo (ReadUInt32 ())) + if (!MoveTo(ReadUInt32())) return false; - if (ReadUInt32 () != 0x00004550) + if (ReadUInt32() != 0x00004550) return false; - if (!Advance (20)) + if (!Advance(20)) return false; - if (!Advance (ReadUInt16 () == 0x20b ? 222 : 206)) + if (!Advance(ReadUInt16() == 0x20b ? 222 : 206)) return false; - return ReadUInt32 () != 0; + return ReadUInt32() != 0; } - public static bool IsAssembly (string file) + public static bool IsAssembly(string file) { if (file == null) - throw new ArgumentNullException ("file"); + throw new ArgumentNullException("file"); - using (var stream = new FileStream (file, FileMode.Open, FileAccess.Read, FileShare.Read)) - return IsAssembly (stream); + using (var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + return IsAssembly(stream); } - public static bool IsAssembly (Stream stream) + public static bool IsAssembly(Stream stream) { if (stream == null) - throw new ArgumentNullException (nameof(stream)); + throw new ArgumentNullException(nameof(stream)); if (!stream.CanRead) - throw new ArgumentException (nameof(stream)); + throw new ArgumentException(nameof(stream)); if (!stream.CanSeek) - throw new ArgumentException (nameof(stream)); + throw new ArgumentException(nameof(stream)); - using (var image = new Image (stream)) - return image.IsManagedAssembly (); + using (var image = new Image(stream)) + return image.IsManagedAssembly(); } } } diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents.meta b/Editor/Plugins/AppleEventIntegration.bundle/Contents.meta index 03a3bfb..4ecd726 100644 --- a/Editor/Plugins/AppleEventIntegration.bundle/Contents.meta +++ b/Editor/Plugins/AppleEventIntegration.bundle/Contents.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 058b02c03ea09473aab4dc5fc50af1ef +guid: 543eb5eeeb1d5424ca8876b93fad5326 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents/Info.plist b/Editor/Plugins/AppleEventIntegration.bundle/Contents/Info.plist index 3b52961..9d34eaf 100644 --- a/Editor/Plugins/AppleEventIntegration.bundle/Contents/Info.plist +++ b/Editor/Plugins/AppleEventIntegration.bundle/Contents/Info.plist @@ -3,7 +3,7 @@ BuildMachineOSBuild - 19B88 + 19H2 CFBundleDevelopmentRegion en CFBundleExecutable @@ -14,6 +14,8 @@ 6.0 CFBundleName AppleEventIntegration + CFBundlePackageType + BNDL CFBundleShortVersionString 1.0 CFBundleSupportedPlatforms @@ -25,17 +27,21 @@ DTCompiler com.apple.compilers.llvm.clang.1_0 DTPlatformBuild - 10E125 + 12A7300 + DTPlatformName + macosx DTPlatformVersion - GM + 10.15.6 DTSDKBuild - 18E219 + 19G68 DTSDKName - macosx10.14 + macosx10.15 DTXcode - 1020 + 1201 DTXcodeBuild - 10E125 + 12A7300 + LSMinimumSystemVersion + 10.13 NSHumanReadableCopyright Copyright © 2019 Unity. All rights reserved. diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents/Info.plist.meta b/Editor/Plugins/AppleEventIntegration.bundle/Contents/Info.plist.meta index 1f49dac..2f42690 100644 --- a/Editor/Plugins/AppleEventIntegration.bundle/Contents/Info.plist.meta +++ b/Editor/Plugins/AppleEventIntegration.bundle/Contents/Info.plist.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 680cf1008b4284eddbb82ec4d76644a1 +guid: 29239d79a3471495e9d270601006dad7 DefaultImporter: externalObjects: {} userData: diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS.meta b/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS.meta index 8c69d7f..c99cb41 100644 --- a/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS.meta +++ b/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: ba4355216f6c44abbb17503872c42c97 +guid: e811c7e1c1e9a4b50b237772d317959f folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration b/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration index bf56250..2b35f07 100644 Binary files a/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration and b/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration differ diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration.meta b/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration.meta index 52870b5..ca6a14f 100644 --- a/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration.meta +++ b/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 4e53c7f7b5c7e4a3d890cb596ed56c22 +guid: 9c3599bc139404df2955d3ffd39d60d6 DefaultImporter: externalObjects: {} userData: diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents/_CodeSignature.meta b/Editor/Plugins/AppleEventIntegration.bundle/Contents/_CodeSignature.meta new file mode 100644 index 0000000..677b3cb --- /dev/null +++ b/Editor/Plugins/AppleEventIntegration.bundle/Contents/_CodeSignature.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 11ca2399a9422473eb66bca747f3ad52 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents/_CodeSignature/CodeResources b/Editor/Plugins/AppleEventIntegration.bundle/Contents/_CodeSignature/CodeResources new file mode 100644 index 0000000..d5d0fd7 --- /dev/null +++ b/Editor/Plugins/AppleEventIntegration.bundle/Contents/_CodeSignature/CodeResources @@ -0,0 +1,115 @@ + + + + + files + + files2 + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents/_CodeSignature/CodeResources.meta b/Editor/Plugins/AppleEventIntegration.bundle/Contents/_CodeSignature/CodeResources.meta new file mode 100644 index 0000000..14bac5b --- /dev/null +++ b/Editor/Plugins/AppleEventIntegration.bundle/Contents/_CodeSignature/CodeResources.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3379e8bd711774041a330f218af69b7a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ProjectGeneration/AssemblyNameProvider.cs b/Editor/ProjectGeneration/AssemblyNameProvider.cs index ebe3f6d..90787ad 100644 --- a/Editor/ProjectGeneration/AssemblyNameProvider.cs +++ b/Editor/ProjectGeneration/AssemblyNameProvider.cs @@ -1,3 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Unity Technologies. + * 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.Collections.Generic; using System.Linq; @@ -7,181 +12,175 @@ using UnityEditor.PackageManager; namespace Microsoft.Unity.VisualStudio.Editor { - public interface IAssemblyNameProvider - { - string[] ProjectSupportedExtensions { get; } - string ProjectGenerationRootNamespace { get; } - ProjectGenerationFlag ProjectGenerationFlag { get; } + public interface IAssemblyNameProvider + { + string[] ProjectSupportedExtensions { get; } + string ProjectGenerationRootNamespace { get; } + ProjectGenerationFlag ProjectGenerationFlag { get; } - string GetAssemblyNameFromScriptPath(string path); + 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); - } + 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", - (int)(ProjectGenerationFlag.Local | ProjectGenerationFlag.Embedded)); + public class AssemblyNameProvider : IAssemblyNameProvider + { + ProjectGenerationFlag m_ProjectGenerationFlag = (ProjectGenerationFlag)EditorPrefs.GetInt( + "unity_project_generation_flag", + (int)(ProjectGenerationFlag.Local | ProjectGenerationFlag.Embedded)); - public string[] ProjectSupportedExtensions => EditorSettings.projectGenerationUserExtensions; + public string[] ProjectSupportedExtensions => EditorSettings.projectGenerationUserExtensions; - public string ProjectGenerationRootNamespace => EditorSettings.projectGenerationRootNamespace; + 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 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 string GetAssemblyNameFromScriptPath(string path) + { + return CompilationPipeline.GetAssemblyNameFromScriptPath(path); + } - public IEnumerable GetAssemblies(Func shouldFileBePartOfSolution) - { - foreach (var assembly in CompilationPipeline.GetAssemblies()) - { - if (assembly.sourceFiles.Any(shouldFileBePartOfSolution)) - { - var options = new ScriptCompilerOptions() - { - ResponseFiles = assembly.compilerOptions.ResponseFiles, - AllowUnsafeCode = assembly.compilerOptions.AllowUnsafeCode, - ApiCompatibilityLevel = assembly.compilerOptions.ApiCompatibilityLevel - }; + public IEnumerable GetAssemblies(Func shouldFileBePartOfSolution) + { + foreach (var assembly in CompilationPipeline.GetAssemblies()) + { + if (assembly.sourceFiles.Any(shouldFileBePartOfSolution)) + { + var options = new ScriptCompilerOptions + { + ResponseFiles = assembly.compilerOptions.ResponseFiles, + AllowUnsafeCode = assembly.compilerOptions.AllowUnsafeCode, + ApiCompatibilityLevel = assembly.compilerOptions.ApiCompatibilityLevel + }; - 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, + 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, #if UNITY_2020_2_OR_NEWER options, assembly.rootNamespace); #else - options); + options); #endif - } - } + } + } - if (ProjectGenerationFlag.HasFlag(ProjectGenerationFlag.PlayerAssemblies)) - { - foreach (var assembly in CompilationPipeline.GetAssemblies(AssembliesType.Player).Where(assembly => assembly.sourceFiles.Any(shouldFileBePartOfSolution))) - { - var options = new ScriptCompilerOptions() - { - 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))) + { + var options = new ScriptCompilerOptions + { + ResponseFiles = assembly.compilerOptions.ResponseFiles, + AllowUnsafeCode = assembly.compilerOptions.AllowUnsafeCode, + ApiCompatibilityLevel = assembly.compilerOptions.ApiCompatibilityLevel + }; - 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, + 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, #if UNITY_2020_2_OR_NEWER - options, - assembly.rootNamespace); + options, + assembly.rootNamespace); #else - options); + options); #endif - } - } - } + } + } + } - public string GetCompileOutputPath(string assemblyName) - { - if (assemblyName.EndsWith(".Player", StringComparison.Ordinal)) - { - return @"Temp\Bin\Debug\Player\"; - } - else - { - return @"Temp\Bin\Debug\"; - } - } + public string GetCompileOutputPath(string assemblyName) + { + return assemblyName.EndsWith(".Player", StringComparison.Ordinal) ? @"Temp\Bin\Debug\Player\" : @"Temp\Bin\Debug\"; + } - public IEnumerable GetAllAssetPaths() - { - return AssetDatabase.GetAllAssetPaths(); - } + public IEnumerable GetAllAssetPaths() + { + return AssetDatabase.GetAllAssetPaths(); + } - public UnityEditor.PackageManager.PackageInfo FindForAssetPath(string assetPath) - { - return UnityEditor.PackageManager.PackageInfo.FindForAssetPath(assetPath); - } + 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); - } + 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; - } + return false; + } - public ResponseFileData ParseResponseFile(string responseFilePath, string projectDirectory, string[] systemReferenceDirectories) - { - return CompilationPipeline.ParseResponseFile( - responseFilePath, - projectDirectory, - systemReferenceDirectories - ); - } + 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 ToggleProjectGeneration(ProjectGenerationFlag preference) + { + if (ProjectGenerationFlag.HasFlag(preference)) + { + ProjectGenerationFlag ^= preference; + } + else + { + ProjectGenerationFlag |= preference; + } + } - public void ResetProjectGenerationFlag() - { - ProjectGenerationFlag = ProjectGenerationFlag.None; - } + public void ResetProjectGenerationFlag() + { + ProjectGenerationFlag = ProjectGenerationFlag.None; + } public string GetAssemblyName(string assemblyOutputPath, string assemblyName) { diff --git a/Editor/ProjectGeneration/FileIOProvider.cs b/Editor/ProjectGeneration/FileIOProvider.cs index c22548c..16421b1 100644 --- a/Editor/ProjectGeneration/FileIOProvider.cs +++ b/Editor/ProjectGeneration/FileIOProvider.cs @@ -1,31 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Unity Technologies. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ using System.IO; using System.Text; namespace Microsoft.Unity.VisualStudio.Editor { - public interface IFileIO - { - bool Exists(string fileName); + public interface IFileIO + { + bool Exists(string fileName); - string ReadAllText(string fileName); - void WriteAllText(string fileName, string content); - } + string ReadAllText(string fileName); + void WriteAllText(string fileName, string content); + } - class FileIOProvider : IFileIO - { - public bool Exists(string fileName) - { - return File.Exists(fileName); - } + class FileIOProvider : IFileIO + { + public bool Exists(string fileName) + { + return File.Exists(fileName); + } - public string ReadAllText(string fileName) - { - return File.ReadAllText(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 + public void WriteAllText(string fileName, string content) + { + File.WriteAllText(fileName, content, Encoding.UTF8); + } + } +} diff --git a/Editor/ProjectGeneration/GUIDProvider.cs b/Editor/ProjectGeneration/GUIDProvider.cs index 3b1d16e..829cd4e 100644 --- a/Editor/ProjectGeneration/GUIDProvider.cs +++ b/Editor/ProjectGeneration/GUIDProvider.cs @@ -1,21 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Unity Technologies. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ namespace Microsoft.Unity.VisualStudio.Editor { - public interface IGUIDGenerator - { - string ProjectGuid(string projectName, string assemblyName); - string SolutionGuid(string projectName, ScriptingLanguage scriptingLanguage); - } + 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); - } + 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 + public string SolutionGuid(string projectName, ScriptingLanguage scriptingLanguage) + { + return SolutionGuidGenerator.GuidForSolution(projectName, scriptingLanguage); + } + } +} diff --git a/Editor/ProjectGeneration/ProjectGeneration.cs b/Editor/ProjectGeneration/ProjectGeneration.cs index 2f39944..f3d3a45 100644 --- a/Editor/ProjectGeneration/ProjectGeneration.cs +++ b/Editor/ProjectGeneration/ProjectGeneration.cs @@ -15,922 +15,981 @@ using System.Text.RegularExpressions; using Unity.CodeEditor; using UnityEditor; using UnityEditor.Compilation; -using UnityEditor.PackageManager; using UnityEditorInternal; using UnityEngine; namespace Microsoft.Unity.VisualStudio.Editor { - public enum ScriptingLanguage - { - None, - CSharp - } - - public interface IGenerator - { - bool SyncIfNeeded(IEnumerable affectedFiles, IEnumerable reimportedFiles); - void Sync(); - bool HasSolutionBeenGenerated(); - bool IsSupportedFile(string path); - string SolutionFile(); - string ProjectDirectory { get; } - IAssemblyNameProvider AssemblyNameProvider { get; } - } - - public class ProjectGeneration : IGenerator - { - public static readonly string MSBuildNamespaceUri = "http://schemas.microsoft.com/developer/msbuild/2003"; - - const string k_WindowsNewline = "\r\n"; - - string m_SolutionProjectEntryTemplate = @"Project(""{{{0}}}"") = ""{1}"", ""{2}"", ""{{{3}}}""{4}EndProject"; - - string m_SolutionProjectConfigurationTemplate = string.Join("\r\n", - @" {{{0}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU", - @" {{{0}}}.Debug|Any CPU.Build.0 = Debug|Any CPU", - @" {{{0}}}.Release|Any CPU.ActiveCfg = Release|Any CPU", - @" {{{0}}}.Release|Any CPU.Build.0 = Release|Any CPU").Replace(" ", "\t"); - - static readonly string[] k_ReimportSyncExtensions = { ".dll", ".asmdef" }; - - static readonly Regex k_ScriptReferenceExpression = new Regex( - @"^Library.ScriptAssemblies.(?(?.*)\.dll$)", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - string[] m_ProjectSupportedExtensions = new string[0]; - string[] m_BuiltinSupportedExtensions = new string[0]; - - public string ProjectDirectory { get; } - - readonly string m_ProjectName; - readonly IAssemblyNameProvider m_AssemblyNameProvider; - readonly IFileIO m_FileIOProvider; - readonly IGUIDGenerator m_GUIDGenerator; - VisualStudioInstallation m_CurrentInstallation; - public IAssemblyNameProvider AssemblyNameProvider => m_AssemblyNameProvider; - - public ProjectGeneration() : this(Directory.GetParent(Application.dataPath).FullName) - { - } - - public ProjectGeneration(string tempDirectory) : this(tempDirectory, new AssemblyNameProvider(), new FileIOProvider(), new GUIDProvider()) - { - } - - public ProjectGeneration(string tempDirectory, IAssemblyNameProvider assemblyNameProvider, IFileIO fileIoProvider, IGUIDGenerator guidGenerator) - { - ProjectDirectory = tempDirectory.Replace('\\', '/'); - m_ProjectName = Path.GetFileName(ProjectDirectory); - m_AssemblyNameProvider = assemblyNameProvider; - m_FileIOProvider = fileIoProvider; - m_GUIDGenerator = guidGenerator; - - SetupProjectSupportedExtensions(); - } - - /// - /// Syncs the scripting solution if any affected files are relevant. - /// - /// - /// Whether the solution was synced. - /// - /// - /// A set of files whose status has changed - /// - /// - /// A set of files that got reimported - /// - public bool SyncIfNeeded(IEnumerable affectedFiles, IEnumerable reimportedFiles) - { - SetupProjectSupportedExtensions(); - - // Don't sync if we haven't synced before - if (HasSolutionBeenGenerated() && HasFilesBeenModified(affectedFiles, reimportedFiles)) - { - Sync(); - return true; - } - return false; - } - - bool HasFilesBeenModified(IEnumerable affectedFiles, IEnumerable reimportedFiles) - { - return affectedFiles.Any(ShouldFileBePartOfSolution) || reimportedFiles.Any(ShouldSyncOnReimportedAsset); - } - - static bool ShouldSyncOnReimportedAsset(string asset) - { - 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(); - var externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles(); - - if (!externalCodeAlreadyGeneratedProjects) - { - GenerateAndWriteSolutionAndProjects(); - } - OnGeneratedCSProjectFiles(); - } - - public bool HasSolutionBeenGenerated() - { - return m_FileIOProvider.Exists(SolutionFile()); - } - - void SetupProjectSupportedExtensions() - { - 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_AssemblyNameProvider.IsInternalizedPackagePath(file)) - { - return false; - } - - return IsSupportedFile(file); - } - - static string GetExtensionWithoutDot(string path) - { - // Prevent re-processing and information loss - if (!Path.HasExtension(path)) - return path; - - return Path - .GetExtension(path) - .TrimStart('.') - .ToLower(); - } - - public bool IsSupportedFile(string path) - { - var extension = GetExtensionWithoutDot(path); - - // Dll's are not scripts but still need to be included - if (extension == "dll") - return true; - - if (extension == "asmdef") - return true; - - if (m_BuiltinSupportedExtensions.Contains(extension)) - return true; - - if (m_ProjectSupportedExtensions.Contains(extension)) - return true; - - return false; - } - - static ScriptingLanguage ScriptingLanguageFor(Assembly assembly) - { - var files = assembly.sourceFiles; - - if (files.Length == 0) - return ScriptingLanguage.None; - - return ScriptingLanguageFor(files[0]); - } - - static ScriptingLanguage ScriptingLanguageFor(string path) - { - return GetExtensionWithoutDot(path) == "cs" ? ScriptingLanguage.CSharp : ScriptingLanguage.None; - } - - public void GenerateAndWriteSolutionAndProjects() - { - // 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.GetAssemblies(ShouldFileBePartOfSolution); - - var allAssetProjectParts = GenerateAllAssetProjectParts(); - - var assemblyList = assemblies.ToList(); - - SyncSolution(assemblyList); - var allProjectAssemblies = RelevantAssembliesForMode(assemblyList).ToList(); - foreach (Assembly assembly in allProjectAssemblies) - { - var responseFileData = ParseResponseFileData(assembly); - SyncProject(assembly, allAssetProjectParts, responseFileData, allProjectAssemblies); - } - } - - IEnumerable ParseResponseFileData(Assembly assembly) - { - var systemReferenceDirectories = CompilationPipeline.GetSystemAssemblyDirectories(assembly.compilerOptions.ApiCompatibilityLevel); - - Dictionary responseFilesData = assembly.compilerOptions.ResponseFiles.ToDictionary(x => x, x => m_AssemblyNameProvider.ParseResponseFile( - x, - ProjectDirectory, - systemReferenceDirectories - )); - - Dictionary responseFilesWithErrors = responseFilesData.Where(x => x.Value.Errors.Any()) - .ToDictionary(x => x.Key, x => x.Value); - - if (responseFilesWithErrors.Any()) - { - foreach (var error in responseFilesWithErrors) - foreach (var valueError in error.Value.Errors) - { - Debug.LogError($"{error.Key} Parse Error : {valueError}"); - } - } - - return responseFilesData.Select(x => x.Value); - } - - Dictionary GenerateAllAssetProjectParts() - { - Dictionary stringBuilders = new Dictionary(); - - foreach (string asset in m_AssemblyNameProvider.GetAllAssetPaths()) - { - // Exclude files coming from packages except if they are internalized. - if (m_AssemblyNameProvider.IsInternalizedPackagePath(asset)) - { - continue; - } - - if (IsSupportedFile(asset) && ScriptingLanguage.None == ScriptingLanguageFor(asset)) - { - // Find assembly the asset belongs to by adding script extension and using compilation pipeline. - var assemblyName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset + ".cs"); - - if (string.IsNullOrEmpty(assemblyName)) - { - continue; - } - - assemblyName = Path.GetFileNameWithoutExtension(assemblyName); - - if (!stringBuilders.TryGetValue(assemblyName, out var projectBuilder)) - { - projectBuilder = new StringBuilder(); - stringBuilders[assemblyName] = projectBuilder; - } - - projectBuilder.Append(" ").Append(k_WindowsNewline); - } - } - - var result = new Dictionary(); - - foreach (var entry in stringBuilders) - result[entry.Key] = entry.Value.ToString(); - - return result; - } - - void SyncProject( - Assembly assembly, - Dictionary allAssetsProjectParts, - IEnumerable responseFilesData, - List allProjectAssemblies) - { - SyncProjectFileIfNotChanged(ProjectFile(assembly), ProjectText(assembly, allAssetsProjectParts, responseFilesData, allProjectAssemblies)); - } - - void SyncProjectFileIfNotChanged(string path, string newContents) - { - if (Path.GetExtension(path) == ".csproj") - { - newContents = OnGeneratedCSProject(path, newContents); - } - - SyncFileIfNotChanged(path, newContents); - } - - void SyncSolutionFileIfNotChanged(string path, string newContents) - { - newContents = OnGeneratedSlnSolution(path, newContents); - - 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() - { - foreach(var method in GetPostProcessorCallbacks(nameof(OnGeneratedCSProjectFiles))) - { - method.Invoke(null, Array.Empty()); - } - } - - static bool OnPreGeneratingCSProjectFiles() - { - bool result = false; - - foreach(var method in GetPostProcessorCallbacks(nameof(OnPreGeneratingCSProjectFiles))) - { - 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) - { - return InvokeAssetPostProcessorGenerationCallbacks(nameof(OnGeneratedCSProject), path, content); - } - - static string OnGeneratedSlnSolution(string path, string content) - { - return InvokeAssetPostProcessorGenerationCallbacks(nameof(OnGeneratedSlnSolution), path, content); - } - - void SyncFileIfNotChanged(string filename, string newContents) - { - try - { - if (m_FileIOProvider.Exists(filename) && newContents == m_FileIOProvider.ReadAllText(filename)) - { - return; - } - } - catch (Exception exception) - { - Debug.LogException(exception); - } - - m_FileIOProvider.WriteAllText(filename, newContents); - } - - string ProjectText(Assembly assembly, - Dictionary allAssetsProjectParts, - IEnumerable responseFilesData, - List allProjectAsemblies) - { - var projectBuilder = new StringBuilder(ProjectHeader(assembly, responseFilesData)); - var references = new List(); - - projectBuilder.Append(@" ").Append(k_WindowsNewline); - foreach (string file in assembly.sourceFiles) - { - if (!IsSupportedFile(file)) - continue; - - var extension = Path.GetExtension(file).ToLower(); - var fullFile = EscapedRelativePathFor(file); - if (".dll" != extension) - { - projectBuilder.Append(" ").Append(k_WindowsNewline); - } - else - { - references.Add(fullFile); - } - } - projectBuilder.Append(@" ").Append(k_WindowsNewline); - - projectBuilder.Append(@" ").Append(k_WindowsNewline); - - // Append additional non-script files that should be included in project generation. - if (allAssetsProjectParts.TryGetValue(assembly.name, out var additionalAssetsForProject)) - projectBuilder.Append(additionalAssetsForProject); - - 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) - { - string fullReference = Path.IsPathRooted(reference) ? reference : Path.Combine(ProjectDirectory, reference); - AppendReference(fullReference, projectBuilder); - } - - projectBuilder.Append(@" ").Append(k_WindowsNewline); - - if (0 < assembly.assemblyReferences.Length) - { - projectBuilder.Append(" ").Append(k_WindowsNewline); - foreach (Assembly reference in assembly.assemblyReferences.Where(i => i.sourceFiles.Any(ShouldFileBePartOfSolution))) - { - 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); - } - - projectBuilder.Append(ProjectFooter()); - return projectBuilder.ToString(); - } - - static string XmlFilename(string path) - { - if (string.IsNullOrEmpty(path)) - return path; - - path = path.Replace(@"%", "%25"); - path = path.Replace(@";", "%3b"); - - return XmlEscape(path); - } - - static string XmlEscape(string s) - { - return SecurityElement.Escape(s); - } - - void AppendReference(string fullReference, StringBuilder projectBuilder) - { - var escapedFullPath = EscapedRelativePathFor(fullReference); - projectBuilder.Append(" ").Append(k_WindowsNewline); - projectBuilder.Append(" ").Append(escapedFullPath).Append("").Append(k_WindowsNewline); - projectBuilder.Append(" ").Append(k_WindowsNewline); - } - - public string ProjectFile(Assembly assembly) - { + public enum ScriptingLanguage + { + None, + CSharp + } + + public interface IGenerator + { + bool SyncIfNeeded(IEnumerable affectedFiles, IEnumerable reimportedFiles); + void Sync(); + bool HasSolutionBeenGenerated(); + bool IsSupportedFile(string path); + string SolutionFile(); + string ProjectDirectory { get; } + IAssemblyNameProvider AssemblyNameProvider { get; } + } + + public class ProjectGeneration : IGenerator + { + public static readonly string MSBuildNamespaceUri = "http://schemas.microsoft.com/developer/msbuild/2003"; + public IAssemblyNameProvider AssemblyNameProvider => m_AssemblyNameProvider; + public string ProjectDirectory { get; } + + const string k_WindowsNewline = "\r\n"; + + string m_SolutionProjectEntryTemplate = @"Project(""{{{0}}}"") = ""{1}"", ""{2}"", ""{{{3}}}""{4}EndProject"; + + string m_SolutionProjectConfigurationTemplate = string.Join("\r\n", + @" {{{0}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU", + @" {{{0}}}.Debug|Any CPU.Build.0 = Debug|Any CPU", + @" {{{0}}}.Release|Any CPU.ActiveCfg = Release|Any CPU", + @" {{{0}}}.Release|Any CPU.Build.0 = Release|Any CPU").Replace(" ", "\t"); + + static readonly string[] k_ReimportSyncExtensions = { ".dll", ".asmdef" }; + + static readonly Regex k_ScriptReferenceExpression = new Regex( + @"^Library.ScriptAssemblies.(?(?.*)\.dll$)", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + string[] m_ProjectSupportedExtensions = Array.Empty(); + string[] m_BuiltinSupportedExtensions = Array.Empty(); + + readonly string m_ProjectName; + readonly IAssemblyNameProvider m_AssemblyNameProvider; + readonly IFileIO m_FileIOProvider; + readonly IGUIDGenerator m_GUIDGenerator; + bool m_ShouldGenerateAll; + IVisualStudioInstallation m_CurrentInstallation; + + public ProjectGeneration() : this(Directory.GetParent(Application.dataPath).FullName) + { + } + + public ProjectGeneration(string tempDirectory) : this(tempDirectory, new AssemblyNameProvider(), new FileIOProvider(), new GUIDProvider()) + { + } + + public ProjectGeneration(string tempDirectory, IAssemblyNameProvider assemblyNameProvider, IFileIO fileIoProvider, IGUIDGenerator guidGenerator) + { + ProjectDirectory = FileUtility.NormalizeWindowsToUnix(tempDirectory); + m_ProjectName = Path.GetFileName(ProjectDirectory); + m_AssemblyNameProvider = assemblyNameProvider; + m_FileIOProvider = fileIoProvider; + m_GUIDGenerator = guidGenerator; + + SetupProjectSupportedExtensions(); + } + + /// + /// Syncs the scripting solution if any affected files are relevant. + /// + /// + /// Whether the solution was synced. + /// + /// + /// A set of files whose status has changed + /// + /// + /// A set of files that got reimported + /// + public bool SyncIfNeeded(IEnumerable affectedFiles, IEnumerable reimportedFiles) + { + SetupProjectSupportedExtensions(); + + // See https://devblogs.microsoft.com/setup/configure-visual-studio-across-your-organization-with-vsconfig/ + // We create a .vsconfig file to make sure our ManagedGame workload is installed + CreateVsConfigIfNotFound(); + + // Don't sync if we haven't synced before + if (HasSolutionBeenGenerated() && HasFilesBeenModified(affectedFiles, reimportedFiles)) + { + Sync(); + return true; + } + return false; + } + + private void CreateVsConfigIfNotFound() + { + try + { + var vsConfigFile = VsConfigFile(); + if (m_FileIOProvider.Exists(vsConfigFile)) + return; + + var content = $@"{{ + ""version"": ""1.0"", + ""components"": [ + ""{Discovery.ManagedWorkload}"" + ] +}} +"; + m_FileIOProvider.WriteAllText(vsConfigFile, content); + } + catch (IOException) + { + } + } + + private bool HasFilesBeenModified(IEnumerable affectedFiles, IEnumerable reimportedFiles) + { + return affectedFiles.Any(ShouldFileBePartOfSolution) || reimportedFiles.Any(ShouldSyncOnReimportedAsset); + } + + private static bool ShouldSyncOnReimportedAsset(string asset) + { + 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(); + var externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles(); + + if (!externalCodeAlreadyGeneratedProjects) + { + GenerateAndWriteSolutionAndProjects(); + } + OnGeneratedCSProjectFiles(); + } + + public bool HasSolutionBeenGenerated() + { + return m_FileIOProvider.Exists(SolutionFile()); + } + + private void SetupProjectSupportedExtensions() + { + m_ProjectSupportedExtensions = m_AssemblyNameProvider.ProjectSupportedExtensions; + m_BuiltinSupportedExtensions = EditorSettings.projectGenerationBuiltinExtensions; + } + + private bool ShouldFileBePartOfSolution(string file) + { + // Exclude files coming from packages except if they are internalized. + if (m_AssemblyNameProvider.IsInternalizedPackagePath(file)) + { + return false; + } + + return IsSupportedFile(file); + } + + private static string GetExtensionWithoutDot(string path) + { + // Prevent re-processing and information loss + if (!Path.HasExtension(path)) + return path; + + return Path + .GetExtension(path) + .TrimStart('.') + .ToLower(); + } + + public bool IsSupportedFile(string path) + { + var extension = GetExtensionWithoutDot(path); + + // Dll's are not scripts but still need to be included + if (extension == "dll") + return true; + + if (extension == "asmdef") + return true; + + if (m_BuiltinSupportedExtensions.Contains(extension)) + return true; + + if (m_ProjectSupportedExtensions.Contains(extension)) + return true; + + return false; + } + + private static ScriptingLanguage ScriptingLanguageFor(Assembly assembly) + { + var files = assembly.sourceFiles; + + if (files.Length == 0) + return ScriptingLanguage.None; + + return ScriptingLanguageFor(files[0]); + } + + internal static ScriptingLanguage ScriptingLanguageFor(string path) + { + return GetExtensionWithoutDot(path) == "cs" ? ScriptingLanguage.CSharp : ScriptingLanguage.None; + } + + public void GenerateAndWriteSolutionAndProjects() + { + // 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.GetAssemblies(ShouldFileBePartOfSolution); + + var allAssetProjectParts = GenerateAllAssetProjectParts(); + + var assemblyList = assemblies.ToList(); + + SyncSolution(assemblyList); + + var allProjectAssemblies = RelevantAssembliesForMode(assemblyList).ToList(); + + foreach (Assembly assembly in allProjectAssemblies) + { + SyncProject(assembly, + allAssetProjectParts, + responseFilesData: ParseResponseFileData(assembly), + allProjectAssemblies, +#if UNITY_2020_2_OR_NEWER + assembly.compilerOptions.RoslynAnalyzerDllPaths); +#else + Array.Empty()); +#endif + } + } + + private IEnumerable ParseResponseFileData(Assembly assembly) + { + var systemReferenceDirectories = CompilationPipeline.GetSystemAssemblyDirectories(assembly.compilerOptions.ApiCompatibilityLevel); + + Dictionary responseFilesData = assembly.compilerOptions.ResponseFiles.ToDictionary(x => x, x => m_AssemblyNameProvider.ParseResponseFile( + x, + ProjectDirectory, + systemReferenceDirectories + )); + + Dictionary responseFilesWithErrors = responseFilesData.Where(x => x.Value.Errors.Any()) + .ToDictionary(x => x.Key, x => x.Value); + + if (responseFilesWithErrors.Any()) + { + foreach (var error in responseFilesWithErrors) + foreach (var valueError in error.Value.Errors) + { + Debug.LogError($"{error.Key} Parse Error : {valueError}"); + } + } + + return responseFilesData.Select(x => x.Value); + } + + private Dictionary GenerateAllAssetProjectParts() + { + Dictionary stringBuilders = new Dictionary(); + + foreach (string asset in m_AssemblyNameProvider.GetAllAssetPaths()) + { + // Exclude files coming from packages except if they are internalized. + if (m_AssemblyNameProvider.IsInternalizedPackagePath(asset)) + { + continue; + } + + if (IsSupportedFile(asset) && ScriptingLanguage.None == ScriptingLanguageFor(asset)) + { + // Find assembly the asset belongs to by adding script extension and using compilation pipeline. + var assemblyName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset + ".cs"); + + if (string.IsNullOrEmpty(assemblyName)) + { + continue; + } + + assemblyName = Path.GetFileNameWithoutExtension(assemblyName); + + if (!stringBuilders.TryGetValue(assemblyName, out var projectBuilder)) + { + projectBuilder = new StringBuilder(); + stringBuilders[assemblyName] = projectBuilder; + } + + projectBuilder.Append(" ").Append(k_WindowsNewline); + } + } + + var result = new Dictionary(); + + foreach (var entry in stringBuilders) + result[entry.Key] = entry.Value.ToString(); + + return result; + } + + private void SyncProject( + Assembly assembly, + Dictionary allAssetsProjectParts, + IEnumerable responseFilesData, + List allProjectAssemblies, + string[] roslynAnalyzerDllPaths) + { + SyncProjectFileIfNotChanged( + ProjectFile(assembly), + ProjectText(assembly, allAssetsProjectParts, responseFilesData, allProjectAssemblies, roslynAnalyzerDllPaths)); + } + + private void SyncProjectFileIfNotChanged(string path, string newContents) + { + if (Path.GetExtension(path) == ".csproj") + { + newContents = OnGeneratedCSProject(path, newContents); + } + + SyncFileIfNotChanged(path, newContents); + } + + private void SyncSolutionFileIfNotChanged(string path, string newContents) + { + newContents = OnGeneratedSlnSolution(path, newContents); + + SyncFileIfNotChanged(path, newContents); + } + + private 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() + { + foreach (var method in GetPostProcessorCallbacks(nameof(OnGeneratedCSProjectFiles))) + { + method.Invoke(null, Array.Empty()); + } + } + + private static bool OnPreGeneratingCSProjectFiles() + { + bool result = false; + + foreach (var method in GetPostProcessorCallbacks(nameof(OnPreGeneratingCSProjectFiles))) + { + var retValue = method.Invoke(null, Array.Empty()); + if (method.ReturnType == typeof(bool)) + { + result |= (bool)retValue; + } + } + + return result; + } + + private 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; + } + + private static string OnGeneratedCSProject(string path, string content) + { + return InvokeAssetPostProcessorGenerationCallbacks(nameof(OnGeneratedCSProject), path, content); + } + + private static string OnGeneratedSlnSolution(string path, string content) + { + return InvokeAssetPostProcessorGenerationCallbacks(nameof(OnGeneratedSlnSolution), path, content); + } + + private void SyncFileIfNotChanged(string filename, string newContents) + { + try + { + if (m_FileIOProvider.Exists(filename) && newContents == m_FileIOProvider.ReadAllText(filename)) + { + return; + } + } + catch (Exception exception) + { + Debug.LogException(exception); + } + + m_FileIOProvider.WriteAllText(filename, newContents); + } + + private string ProjectText(Assembly assembly, + Dictionary allAssetsProjectParts, + IEnumerable responseFilesData, + List allProjectAssemblies, + string[] roslynAnalyzerDllPaths) + { + var projectBuilder = new StringBuilder(ProjectHeader(assembly, responseFilesData, roslynAnalyzerDllPaths)); + var references = new List(); + + projectBuilder.Append(@" ").Append(k_WindowsNewline); + foreach (string file in assembly.sourceFiles) + { + if (!IsSupportedFile(file)) + continue; + + var extension = Path.GetExtension(file).ToLower(); + var fullFile = EscapedRelativePathFor(file); + if (".dll" != extension) + { + projectBuilder.Append(" ").Append(k_WindowsNewline); + } + else + { + references.Add(fullFile); + } + } + projectBuilder.Append(@" ").Append(k_WindowsNewline); + + projectBuilder.Append(@" ").Append(k_WindowsNewline); + + // Append additional non-script files that should be included in project generation. + if (allAssetsProjectParts.TryGetValue(assembly.name, out var additionalAssetsForProject)) + projectBuilder.Append(additionalAssetsForProject); + + 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) + { + string fullReference = Path.IsPathRooted(reference) ? reference : Path.Combine(ProjectDirectory, reference); + AppendReference(fullReference, projectBuilder); + } + + projectBuilder.Append(@" ").Append(k_WindowsNewline); + + if (0 < assembly.assemblyReferences.Length) + { + projectBuilder.Append(" ").Append(k_WindowsNewline); + foreach (Assembly reference in assembly.assemblyReferences.Where(i => i.sourceFiles.Any(ShouldFileBePartOfSolution))) + { + 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); + } + + projectBuilder.Append(ProjectFooter()); + return projectBuilder.ToString(); + } + + private static string XmlFilename(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + + path = path.Replace(@"%", "%25"); + path = path.Replace(@";", "%3b"); + + return XmlEscape(path); + } + + private static string XmlEscape(string s) + { + return SecurityElement.Escape(s); + } + + private void AppendReference(string fullReference, StringBuilder projectBuilder) + { + var escapedFullPath = EscapedRelativePathFor(fullReference); + 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, $"{m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, assembly.name)}.csproj"); } - private static readonly Regex InvalidCharactersRegexPattern = new Regex(@"\?|&|\*|""|<|>|\||#|%|\^|;" + (VisualStudioEditor.IsWindows ? "" : "|:")); - public string SolutionFile() - { - return Path.Combine(FileUtility.Normalize(ProjectDirectory), $"{InvalidCharactersRegexPattern.Replace(m_ProjectName,"_")}.sln"); - } + private static readonly Regex InvalidCharactersRegexPattern = new Regex(@"\?|&|\*|""|<|>|\||#|%|\^|;" + (VisualStudioEditor.IsWindows ? "" : "|:")); + public string SolutionFile() + { + return Path.Combine(FileUtility.Normalize(ProjectDirectory), $"{InvalidCharactersRegexPattern.Replace(m_ProjectName, "_")}.sln"); + } - string ProjectHeader( - Assembly assembly, - IEnumerable responseFilesData - ) - { - var toolsVersion = "4.0"; - var productVersion = "10.0.20506"; - const string baseDirectory = "."; + internal string VsConfigFile() + { + return Path.Combine(FileUtility.Normalize(ProjectDirectory), ".vsconfig"); + } - var targetFrameworkVersion = "v4.7.1"; - var targetLanguageVersion = "latest"; // danger: latest is not the same absolute value depending on the VS version. + private string ProjectHeader( + Assembly assembly, + IEnumerable responseFilesData, + string[] roslynAnalyzerDllPaths + ) + { + var toolsVersion = "4.0"; + var productVersion = "10.0.20506"; + const string baseDirectory = "."; - if (m_CurrentInstallation != null && m_CurrentInstallation.SupportsCSharp8) - { - // Current VS installation is compatible with C# 8. + var targetFrameworkVersion = "v4.7.1"; + var targetLanguageVersion = "latest"; // danger: latest is not the same absolute value depending on the VS version. + + if (m_CurrentInstallation != null && m_CurrentInstallation.SupportsCSharp8) + { + // Current VS installation is compatible with C# 8. #if !UNITY_2020_2_OR_NEWER - // Unity 2020.2.0a12 added support for C# 8 - // <=2020.1 has no support for C# 8 constructs, so tell the compiler to accept only C# 7.3 or lower. - targetLanguageVersion = "7.3"; + // Unity 2020.2.0a12 added support for C# 8 + // <=2020.1 has no support for C# 8 constructs, so tell the compiler to accept only C# 7.3 or lower. + targetLanguageVersion = "7.3"; #endif - } + } - var projectType = ProjectTypeOf(assembly.name); + var projectType = ProjectTypeOf(assembly.name); - var arguments = new object[] - { - toolsVersion, - productVersion, - ProjectGuid(assembly), - XmlFilename(FileUtility.Normalize(InternalEditorUtility.GetEngineAssemblyPath())), - XmlFilename(FileUtility.Normalize(InternalEditorUtility.GetEditorAssemblyPath())), - string.Join(";", assembly.defines.Concat(responseFilesData.SelectMany(x => x.Defines)).Distinct().ToArray()), - MSBuildNamespaceUri, - assembly.name, - assembly.outputPath, - GetRootNamespace(assembly), - targetFrameworkVersion, - targetLanguageVersion, - baseDirectory, - assembly.compilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe), + var arguments = new object[] + { + toolsVersion, + productVersion, + ProjectGuid(assembly), + XmlFilename(FileUtility.Normalize(InternalEditorUtility.GetEngineAssemblyPath())), + XmlFilename(FileUtility.Normalize(InternalEditorUtility.GetEditorAssemblyPath())), + string.Join(";", assembly.defines.Concat(responseFilesData.SelectMany(x => x.Defines)).Distinct().ToArray()), + MSBuildNamespaceUri, + assembly.name, + assembly.outputPath, + GetRootNamespace(assembly), + targetFrameworkVersion, + targetLanguageVersion, + baseDirectory, + assembly.compilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe), // flavoring projectType + ":" + (int)projectType, - EditorUserBuildSettings.activeBuildTarget + ":" + (int)EditorUserBuildSettings.activeBuildTarget, - Application.unityVersion, - VisualStudioIntegration.PackageVersion() - }; + EditorUserBuildSettings.activeBuildTarget + ":" + (int)EditorUserBuildSettings.activeBuildTarget, + Application.unityVersion, + VisualStudioIntegration.PackageVersion() + }; - try - { - return string.Format(GetProjectHeaderTemplate(), arguments); - } - catch (Exception) - { - throw new NotSupportedException("Failed creating c# project because the c# project header did not have the correct amount of arguments, which is " + arguments.Length); - } - } + try + { +#if UNITY_2020_2_OR_NEWER + return string.Format(GetProjectHeaderTemplate(roslynAnalyzerDllPaths, assembly.compilerOptions.RoslynAnalyzerRulesetPath), arguments); +#else + return string.Format(GetProjectHeaderTemplate(Array.Empty(), null), arguments); +#endif + } + catch (Exception) + { + throw new NotSupportedException("Failed creating c# project because the c# project header did not have the correct amount of arguments, which is " + arguments.Length); + } + } - private enum ProjectType - { - GamePlugins = 3, - Game = 1, - EditorPlugins = 7, - Editor = 5, - } + private enum ProjectType + { + GamePlugins = 3, + Game = 1, + EditorPlugins = 7, + Editor = 5, + } - private static ProjectType ProjectTypeOf(string fileName) - { - var plugins = fileName.Contains("firstpass"); - var editor = fileName.Contains("Editor"); + private static ProjectType ProjectTypeOf(string fileName) + { + var plugins = fileName.Contains("firstpass"); + var editor = fileName.Contains("Editor"); - if (plugins && editor) - return ProjectType.EditorPlugins; - if (plugins) - return ProjectType.GamePlugins; - if (editor) - return ProjectType.Editor; + if (plugins && editor) + return ProjectType.EditorPlugins; + if (plugins) + return ProjectType.GamePlugins; + if (editor) + return ProjectType.Editor; - return ProjectType.Game; - } + return ProjectType.Game; + } - static string GetSolutionText() - { - return string.Join("\r\n", - @"", - @"Microsoft Visual Studio Solution File, Format Version {0}", - @"# Visual Studio {1}", - @"{2}", - @"Global", - @" GlobalSection(SolutionConfigurationPlatforms) = preSolution", - @" Debug|Any CPU = Debug|Any CPU", - @" Release|Any CPU = Release|Any CPU", - @" EndGlobalSection", - @" GlobalSection(ProjectConfigurationPlatforms) = postSolution", - @"{3}", - @" EndGlobalSection", - @"{4}", - @"EndGlobal", - @"").Replace(" ", "\t"); - } + private static string GetSolutionText() + { + return string.Join("\r\n", + @"", + @"Microsoft Visual Studio Solution File, Format Version {0}", + @"# Visual Studio {1}", + @"{2}", + @"Global", + @" GlobalSection(SolutionConfigurationPlatforms) = preSolution", + @" Debug|Any CPU = Debug|Any CPU", + @" Release|Any CPU = Release|Any CPU", + @" EndGlobalSection", + @" GlobalSection(ProjectConfigurationPlatforms) = postSolution", + @"{3}", + @" EndGlobalSection", + @"{4}", + @"EndGlobal", + @"").Replace(" ", "\t"); + } - static string GetProjectFooterTemplate() - { - return string.Join("\r\n", - @" ", - @" ", - @" ", - @"", - @""); - } + private static string GetProjectFooterTemplate() + { + return string.Join("\r\n", + @" ", + @" ", + @" ", + @"", + @""); + } - string GetProjectHeaderTemplate() - { - var header = new[] - { - @"", - @"", - @" ", - @" {11}", - @" ", - @" ", - @" Debug", - @" AnyCPU", - @" {1}", - @" 2.0", - @" {9}", - @" {{{2}}}", - @" Library", - @" Properties", - @" {7}", - @" {10}", - @" 512", - @" {12}", - @" ", - @" ", - @" true", - @" full", - @" false", - @" {8}", - @" {5}", - @" prompt", - @" 4", - @" 0169", - @" {13}", - @" ", - @" ", - @" pdbonly", - @" true", - @" Temp\bin\Release\", - @" prompt", - @" 4", - @" 0169", - @" {13}", - @" " - }; + private string GetProjectHeaderTemplate(string[] roslynAnalyzerDllPaths, string roslynAnalyzerRulesetPath) + { + var header = new[] + { + @"", + @"", + @" ", + @" {11}", + @" ", + @" ", + @" Debug", + @" AnyCPU", + @" {1}", + @" 2.0", + @" {9}", + @" {{{2}}}", + @" Library", + @" Properties", + @" {7}", + @" {10}", + @" 512", + @" {12}", + @" ", + @" ", + @" true", + @" full", + @" false", + @" {8}", + @" {5}", + @" prompt", + @" 4", + @" 0169", + @" {13}", + @" ", + @" ", + @" pdbonly", + @" true", + @" Temp\bin\Release\", + @" prompt", + @" 4", + @" 0169", + @" {13}", + @" " + }; - var forceExplicitReferences = new[] - { - @" ", - @" true", - @" true", - @" false", - @" false", - @" false", - @" " - }; + var forceExplicitReferences = new[] + { + @" ", + @" true", + @" true", + @" false", + @" false", + @" false", + @" " + }; - var flavoring = new[] - { - @" ", - @" {{E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1}};{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}", - @" Package", - @" {17}", - @" {14}", - @" {15}", - @" {16}", - @" " - }; + var flavoring = new[] + { + @" ", + @" {{E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1}};{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}", + @" Package", + @" {17}", + @" {14}", + @" {15}", + @" {16}", + @" " + }; - var footer = new[] - { - @"" - }; + var footer = new[] + { + @"" + }; - var lines = header - .Concat(forceExplicitReferences) - .Concat(flavoring) - .ToList(); + var lines = header.Concat(forceExplicitReferences).Concat(flavoring).ToList(); - // Only add analyzer block for compatible Visual Studio - if (m_CurrentInstallation != null && m_CurrentInstallation.SupportsAnalyzers) - { - var analyzers = m_CurrentInstallation.GetAnalyzers(); - if (analyzers != null && analyzers.Length > 0) + // Only add analyzer block for compatible Visual Studio + if (m_CurrentInstallation != null && m_CurrentInstallation.SupportsAnalyzers) + { +#if UNITY_2020_2_OR_NEWER + if (roslynAnalyzerRulesetPath != null) { - lines.Add(@" "); - foreach (var analyzer in analyzers) - lines.Add(string.Format(@" ", EscapedRelativePathFor(analyzer))); - lines.Add(@" "); + lines.Add(@" "); + lines.Add($" {roslynAnalyzerRulesetPath}"); + lines.Add(@" "); } - } +#endif - return string.Join("\r\n", lines - .Concat(footer)); - } + string[] analyzers = m_CurrentInstallation.GetAnalyzers(); + string[] allAnalyzers = analyzers != null ? analyzers.Concat(roslynAnalyzerDllPaths).ToArray() : roslynAnalyzerDllPaths; - void SyncSolution(IEnumerable assemblies) - { - if (InvalidCharactersRegexPattern.IsMatch(ProjectDirectory)) - Debug.LogWarning("Project path contains special characters, which can be an issue when opening Visual Studio"); + if (allAnalyzers.Any()) + { + lines.Add(@" "); + foreach (var analyzer in allAnalyzers) + { + lines.Add($@" "); + } + lines.Add(@" "); + } + } - var solutionFile = SolutionFile(); - var previousSolution = m_FileIOProvider.Exists(solutionFile) ? SolutionParser.ParseSolutionFile(solutionFile, m_FileIOProvider) : null; - SyncSolutionFileIfNotChanged(solutionFile, SolutionText(assemblies, previousSolution)); - } + return string.Join("\r\n", lines.Concat(footer)); + } - string SolutionText(IEnumerable assemblies, Solution previousSolution = null) - { - const string fileversion = "12.00"; - const string vsversion = "15"; + private void SyncSolution(IEnumerable assemblies) + { + if (InvalidCharactersRegexPattern.IsMatch(ProjectDirectory)) + Debug.LogWarning("Project path contains special characters, which can be an issue when opening Visual Studio"); - var relevantAssemblies = RelevantAssembliesForMode(assemblies); - var generatedProjects = ToProjectEntries(relevantAssemblies).ToList(); + var solutionFile = SolutionFile(); + var previousSolution = m_FileIOProvider.Exists(solutionFile) ? SolutionParser.ParseSolutionFile(solutionFile, m_FileIOProvider) : null; + SyncSolutionFileIfNotChanged(solutionFile, SolutionText(assemblies, previousSolution)); + } - SolutionProperties[] properties = null; + private string SolutionText(IEnumerable assemblies, Solution previousSolution = null) + { + const string fileversion = "12.00"; + const string vsversion = "15"; - // First, add all projects generated by Unity to the solution - var projects = new List(); - projects.AddRange(generatedProjects); + var relevantAssemblies = RelevantAssembliesForMode(assemblies); + var generatedProjects = ToProjectEntries(relevantAssemblies).ToList(); - if (previousSolution != null) - { - // Add all projects that were previously in the solution and that are not generated by Unity, nor generated in the project root directory - var externalProjects = previousSolution.Projects - .Where(p => p.IsSolutionFolderProjectFactory() || !FileUtility.IsFileInProjectDirectory(p.FileName)) - .Where(p => generatedProjects.All(gp => gp.FileName != p.FileName)); + SolutionProperties[] properties = null; - projects.AddRange(externalProjects); - properties = previousSolution.Properties; - } + // First, add all projects generated by Unity to the solution + var projects = new List(); + projects.AddRange(generatedProjects); - string propertiesText = GetPropertiesText(properties); - string projectEntriesText = GetProjectEntriesText(projects); + if (previousSolution != null) + { + // Add all projects that were previously in the solution and that are not generated by Unity, nor generated in the project root directory + var externalProjects = previousSolution.Projects + .Where(p => p.IsSolutionFolderProjectFactory() || !FileUtility.IsFileInProjectDirectory(p.FileName)) + .Where(p => generatedProjects.All(gp => gp.FileName != p.FileName)); - // do not generate configurations for SolutionFolders - var configurableProjects = projects.Where(p => !p.IsSolutionFolderProjectFactory()); - string projectConfigurationsText = string.Join(k_WindowsNewline, configurableProjects.Select(p => GetProjectActiveConfigurations(p.ProjectGuid)).ToArray()); + projects.AddRange(externalProjects); + properties = previousSolution.Properties; + } - return string.Format(GetSolutionText(), fileversion, vsversion, projectEntriesText, projectConfigurationsText, propertiesText); - } + string propertiesText = GetPropertiesText(properties); + string projectEntriesText = GetProjectEntriesText(projects); - static IEnumerable RelevantAssembliesForMode(IEnumerable assemblies) - { - return assemblies.Where(i => ScriptingLanguage.CSharp == ScriptingLanguageFor(i)); - } + // do not generate configurations for SolutionFolders + var configurableProjects = projects.Where(p => !p.IsSolutionFolderProjectFactory()); + string projectConfigurationsText = string.Join(k_WindowsNewline, configurableProjects.Select(p => GetProjectActiveConfigurations(p.ProjectGuid)).ToArray()); - private string GetPropertiesText(SolutionProperties[] array) - { - if (array == null || array.Length == 0) - { - // HideSolution by default - array = new SolutionProperties[] { - new SolutionProperties() { - Name = "SolutionProperties", - Type = "preSolution", - Entries = new List>() { new KeyValuePair ("HideSolutionNode", "FALSE") } - } - }; - } - var result = new StringBuilder(); + return string.Format(GetSolutionText(), fileversion, vsversion, projectEntriesText, projectConfigurationsText, propertiesText); + } - for (var i = 0; i 0) - result.Append(k_WindowsNewline); + private static IEnumerable RelevantAssembliesForMode(IEnumerable assemblies) + { + return assemblies.Where(i => ScriptingLanguage.CSharp == ScriptingLanguageFor(i)); + } - var properties = array[i]; + private static string GetPropertiesText(SolutionProperties[] array) + { + if (array == null || array.Length == 0) + { + // HideSolution by default + array = new SolutionProperties[] { + new SolutionProperties() { + Name = "SolutionProperties", + Type = "preSolution", + Entries = new List>() { new KeyValuePair ("HideSolutionNode", "FALSE") } + } + }; + } + var result = new StringBuilder(); - result.Append($"\tGlobalSection({properties.Name}) = {properties.Type}"); - result.Append(k_WindowsNewline); + for (var i = 0; i < array.Length; i++) + { + if (i > 0) + result.Append(k_WindowsNewline); - foreach (var entry in properties.Entries) - { - result.Append($"\t\t{entry.Key} = {entry.Value}"); - result.Append(k_WindowsNewline); - } + var properties = array[i]; - result.Append("\tEndGlobalSection"); - } + result.Append($"\tGlobalSection({properties.Name}) = {properties.Type}"); + result.Append(k_WindowsNewline); - return result.ToString(); - } + foreach (var entry in properties.Entries) + { + result.Append($"\t\t{entry.Key} = {entry.Value}"); + result.Append(k_WindowsNewline); + } - /// - /// Get a Project("{guid}") = "MyProject", "MyProject.unityproj", "{projectguid}" - /// entry for each relevant language - /// - string GetProjectEntriesText(IEnumerable entries) - { - var projectEntries = entries.Select(entry => string.Format( - m_SolutionProjectEntryTemplate, - entry.ProjectFactoryGuid, entry.Name, entry.FileName, entry.ProjectGuid, entry.Metadata - )); + result.Append("\tEndGlobalSection"); + } - return string.Join(k_WindowsNewline, projectEntries.ToArray()); - } + return result.ToString(); + } - IEnumerable ToProjectEntries(IEnumerable assemblies) - { - foreach (var assembly in assemblies) - yield return new SolutionProjectEntry() - { - ProjectFactoryGuid = SolutionGuid(assembly), - Name = assembly.name, - FileName = Path.GetFileName(ProjectFile(assembly)), - ProjectGuid = ProjectGuid(assembly), - Metadata = k_WindowsNewline - }; - } + /// + /// Get a Project("{guid}") = "MyProject", "MyProject.unityproj", "{projectguid}" + /// entry for each relevant language + /// + private string GetProjectEntriesText(IEnumerable entries) + { + var projectEntries = entries.Select(entry => string.Format( + m_SolutionProjectEntryTemplate, + entry.ProjectFactoryGuid, entry.Name, entry.FileName, entry.ProjectGuid, entry.Metadata + )); - /// - /// Generate the active configuration string for a given project guid - /// - string GetProjectActiveConfigurations(string projectGuid) - { - return string.Format( - m_SolutionProjectConfigurationTemplate, - projectGuid); - } + return string.Join(k_WindowsNewline, projectEntries.ToArray()); + } - string EscapedRelativePathFor(string file) - { - var projectDir = FileUtility.Normalize(ProjectDirectory); - file = FileUtility.Normalize(file); - var path = SkipPathPrefix(file, projectDir); + private IEnumerable ToProjectEntries(IEnumerable assemblies) + { + foreach (var assembly in assemblies) + yield return new SolutionProjectEntry() + { + ProjectFactoryGuid = SolutionGuid(assembly), + Name = assembly.name, + FileName = Path.GetFileName(ProjectFile(assembly)), + ProjectGuid = ProjectGuid(assembly), + Metadata = k_WindowsNewline + }; + } - var packageInfo = m_AssemblyNameProvider.FindForAssetPath(path.Replace('\\', '/')); - if (packageInfo != null) { - // We have to normalize the path, because the PackageManagerRemapper assumes - // dir seperators will be os specific. - var absolutePath = Path.GetFullPath(FileUtility.Normalize(path)); - path = SkipPathPrefix(absolutePath, projectDir); - } + /// + /// Generate the active configuration string for a given project guid + /// + private string GetProjectActiveConfigurations(string projectGuid) + { + return string.Format( + m_SolutionProjectConfigurationTemplate, + projectGuid); + } - return XmlFilename(path); - } + private string EscapedRelativePathFor(string file) + { + var projectDir = FileUtility.Normalize(ProjectDirectory); + file = FileUtility.Normalize(file); + var path = SkipPathPrefix(file, projectDir); - static string SkipPathPrefix(string path, string prefix) - { - if (path.StartsWith($"{prefix}{Path.DirectorySeparatorChar}") && (path.Length > prefix.Length)) - return path.Substring(prefix.Length + 1); - return path; - } + var packageInfo = m_AssemblyNameProvider.FindForAssetPath(path.Replace('\\', '/')); + if (packageInfo != null) + { + // We have to normalize the path, because the PackageManagerRemapper assumes + // dir seperators will be os specific. + var absolutePath = Path.GetFullPath(FileUtility.Normalize(path)); + path = SkipPathPrefix(absolutePath, projectDir); + } - static string ProjectFooter() - { - return GetProjectFooterTemplate(); - } + return XmlFilename(path); + } - static string GetProjectExtension() - { - return ".csproj"; - } + private static string SkipPathPrefix(string path, string prefix) + { + if (path.StartsWith($"{prefix}{Path.DirectorySeparatorChar}") && (path.Length > prefix.Length)) + return path.Substring(prefix.Length + 1); + return path; + } - string ProjectGuid(Assembly assembly) - { - return m_GUIDGenerator.ProjectGuid( + private static string ProjectFooter() + { + return GetProjectFooterTemplate(); + } + + static string GetProjectExtension() + { + return ".csproj"; + } + + private string ProjectGuid(Assembly assembly) + { + return m_GUIDGenerator.ProjectGuid( m_ProjectName, m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, assembly.name)); } - string SolutionGuid(Assembly assembly) - { - return m_GUIDGenerator.SolutionGuid(m_ProjectName, ScriptingLanguageFor(assembly)); - } + private string SolutionGuid(Assembly assembly) + { + return m_GUIDGenerator.SolutionGuid(m_ProjectName, ScriptingLanguageFor(assembly)); + } - static string GetRootNamespace(Assembly assembly) - { - #if UNITY_2020_2_OR_NEWER + private static string GetRootNamespace(Assembly assembly) + { +#if UNITY_2020_2_OR_NEWER return assembly.rootNamespace; #else - return EditorSettings.projectGenerationRootNamespace; + return EditorSettings.projectGenerationRootNamespace; #endif - } - } + } + } - public static class SolutionGuidGenerator - { - public static string GuidForProject(string projectName) - { - return ComputeGuidHashFor(projectName + "salt"); - } + public static class SolutionGuidGenerator + { + public static string GuidForProject(string projectName) + { + return ComputeGuidHashFor(projectName + "salt"); + } - public static string GuidForSolution(string projectName, ScriptingLanguage language) - { - if (language == ScriptingLanguage.CSharp) - { - // GUID for a C# class library: http://www.codeproject.com/Reference/720512/List-of-Visual-Studio-Project-Type-GUIDs - return "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC"; - } + public static string GuidForSolution(string projectName, ScriptingLanguage language) + { + if (language == ScriptingLanguage.CSharp) + { + // GUID for a C# class library: http://www.codeproject.com/Reference/720512/List-of-Visual-Studio-Project-Type-GUIDs + return "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC"; + } - return ComputeGuidHashFor(projectName); - } + return ComputeGuidHashFor(projectName); + } - static string ComputeGuidHashFor(string input) - { - var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(input)); - return HashAsGuid(HashToString(hash)); - } + private static string ComputeGuidHashFor(string input) + { + var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(input)); + return HashAsGuid(HashToString(hash)); + } - static string HashAsGuid(string hash) - { - var guid = hash.Substring(0, 8) + "-" + hash.Substring(8, 4) + "-" + hash.Substring(12, 4) + "-" + hash.Substring(16, 4) + "-" + hash.Substring(20, 12); - return guid.ToUpper(); - } + private static string HashAsGuid(string hash) + { + var guid = hash.Substring(0, 8) + "-" + hash.Substring(8, 4) + "-" + hash.Substring(12, 4) + "-" + hash.Substring(16, 4) + "-" + hash.Substring(20, 12); + return guid.ToUpper(); + } - static string HashToString(byte[] bs) - { - var sb = new StringBuilder(); - foreach (byte b in bs) - sb.Append(b.ToString("x2")); - return sb.ToString(); - } - } + private static string HashToString(byte[] bs) + { + var sb = new StringBuilder(); + foreach (byte b in bs) + sb.Append(b.ToString("x2")); + return sb.ToString(); + } + } } diff --git a/Editor/ProjectGeneration/ProjectGenerationFlag.cs b/Editor/ProjectGeneration/ProjectGenerationFlag.cs index fd3438c..bbcc369 100644 --- a/Editor/ProjectGeneration/ProjectGenerationFlag.cs +++ b/Editor/ProjectGeneration/ProjectGenerationFlag.cs @@ -1,8 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Unity Technologies. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +using System; namespace Microsoft.Unity.VisualStudio.Editor { diff --git a/Editor/SolutionParser.cs b/Editor/SolutionParser.cs index 81f7ab2..35c9346 100644 --- a/Editor/SolutionParser.cs +++ b/Editor/SolutionParser.cs @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ using System.Collections.Generic; -using System.IO; using System.Text.RegularExpressions; namespace Microsoft.Unity.VisualStudio.Editor diff --git a/Editor/VisualStudioEditor.cs b/Editor/VisualStudioEditor.cs index 0032120..a102468 100644 --- a/Editor/VisualStudioEditor.cs +++ b/Editor/VisualStudioEditor.cs @@ -7,7 +7,6 @@ using System; using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; using UnityEditor; using UnityEngine; using Unity.CodeEditor; @@ -18,14 +17,14 @@ namespace Microsoft.Unity.VisualStudio.Editor [InitializeOnLoad] public class VisualStudioEditor : IExternalCodeEditor { - private static readonly VisualStudioInstallation[] _installations; + private static readonly IVisualStudioInstallation[] _installations; internal static bool IsOSX => Application.platform == RuntimePlatform.OSXEditor; - internal static bool IsWindows => !IsOSX && Path.DirectorySeparatorChar == '\\' && Environment.NewLine == "\r\n"; + internal static bool IsWindows => !IsOSX && Path.DirectorySeparatorChar == FileUtility.WinSeparator && Environment.NewLine == "\r\n"; CodeEditor.Installation[] IExternalCodeEditor.Installations => _installations .Select(i => i.ToCodeEditorInstallation()) - .ToArray(); + .ToArray(); private readonly IGenerator _generator = new ProjectGeneration(); @@ -39,7 +38,7 @@ namespace Microsoft.Unity.VisualStudio.Editor } catch (Exception ex) { - UnityEngine.Debug.Log($"Error detecting Visual Studio installations: {ex}"); + UnityEngine.Debug.LogError($"Error detecting Visual Studio installations: {ex}"); _installations = Array.Empty(); } @@ -64,7 +63,7 @@ namespace Microsoft.Unity.VisualStudio.Editor { } - internal bool TryGetVisualStudioInstallationForPath(string editorPath, out VisualStudioInstallation installation) + internal virtual bool TryGetVisualStudioInstallationForPath(string editorPath, out IVisualStudioInstallation installation) { // lookup for well known installations foreach (var candidate in _installations) @@ -79,7 +78,7 @@ namespace Microsoft.Unity.VisualStudio.Editor return Discovery.TryDiscoverInstallation(editorPath, out installation); } - public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation) + public virtual bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation) { var result = TryGetVisualStudioInstallationForPath(editorPath, out var vsi); installation = vsi == null ? default : vsi.ToCodeEditorInstallation(); @@ -117,14 +116,14 @@ namespace Microsoft.Unity.VisualStudio.Editor } void RegenerateProjectFiles() - { - var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(new GUILayoutOption[] {})); - rect.width = 252; - if (GUI.Button(rect, "Regenerate project files")) - { - _generator.Sync(); - } - } + { + var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(new GUILayoutOption[] { })); + rect.width = 252; + if (GUI.Button(rect, "Regenerate project files")) + { + _generator.Sync(); + } + } void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip) { @@ -143,8 +142,12 @@ namespace Microsoft.Unity.VisualStudio.Editor foreach (var file in importedFiles.Where(a => Path.GetExtension(a) == ".pdb")) { var pdbFile = FileUtility.GetAssetFullPath(file); - var asmFile = Path.ChangeExtension(pdbFile, ".dll"); + // skip Unity packages like com.unity.ext.nunit + if (pdbFile.IndexOf($"{Path.DirectorySeparatorChar}com.unity.", StringComparison.OrdinalIgnoreCase) > 0) + continue; + + var asmFile = Path.ChangeExtension(pdbFile, ".dll"); if (!File.Exists(asmFile) || !Image.IsAssembly(asmFile)) continue; @@ -176,11 +179,31 @@ namespace Microsoft.Unity.VisualStudio.Editor return false; } + private static void CheckCurrentEditorInstallation() + { + var editorPath = CodeEditor.CurrentEditorInstallation; + try + { + if (Discovery.TryDiscoverInstallation(editorPath, out _)) + return; + } + catch (IOException) + { + } + + UnityEngine.Debug.LogWarning($"Visual Studio executable {editorPath} is not found. Please change your settings in Edit > Preferences > External Tools."); + } + public bool OpenProject(string path, int line, int column) { + CheckCurrentEditorInstallation(); + if (!IsSupportedPath(path)) return false; + if (!IsProjectGeneratedFor(path, out var missingFlag)) + UnityEngine.Debug.LogWarning($"You are trying to open {path} outside a generated project. This might cause problems with IntelliSense and debugging. To avoid this, you can change your .csproj preferences in Edit > Preferences > External Tools and enable {GetProjectGenerationFlagDescription(missingFlag)} generation."); + if (IsOSX) return OpenOSXApp(path, line, column); @@ -190,6 +213,67 @@ namespace Microsoft.Unity.VisualStudio.Editor return false; } + private static string GetProjectGenerationFlagDescription(ProjectGenerationFlag flag) + { + switch (flag) + { + case ProjectGenerationFlag.BuiltIn: + return "Built-in packages"; + case ProjectGenerationFlag.Embedded: + return "Embedded packages"; + case ProjectGenerationFlag.Git: + return "Git packages"; + case ProjectGenerationFlag.Local: + return "Local packages"; + case ProjectGenerationFlag.LocalTarBall: + return "Local tarball"; + case ProjectGenerationFlag.PlayerAssemblies: + return "Player projects"; + case ProjectGenerationFlag.Registry: + return "Registry packages"; + case ProjectGenerationFlag.Unknown: + return "Packages from unknown sources"; + case ProjectGenerationFlag.None: + default: + return string.Empty; + } + } + + private bool IsProjectGeneratedFor(string path, out ProjectGenerationFlag missingFlag) + { + missingFlag = ProjectGenerationFlag.None; + + // No need to check when opening the whole solution + if (string.IsNullOrEmpty(path)) + return true; + + // We only want to check for cs scripts + if (ProjectGeneration.ScriptingLanguageFor(path) != ScriptingLanguage.CSharp) + return true; + + // Even on windows, the package manager requires relative path + unix style separators for queries + var basePath = _generator.ProjectDirectory; + var relativePath = FileUtility + .NormalizeWindowsToUnix(path) + .Replace(basePath, string.Empty) + .Trim(FileUtility.UnixSeparator); + + var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(relativePath); + if (packageInfo == null) + return true; + + var source = packageInfo.source; + if (!Enum.TryParse(source.ToString(), out var flag)) + return true; + + if (_generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(flag)) + return true; + + // Return false if we found a source not flagged for generation + missingFlag = flag; + return false; + } + private bool OpenWindowsApp(string path, int line) { var progpath = FileUtility diff --git a/Editor/VisualStudioInstallation.cs b/Editor/VisualStudioInstallation.cs index 9972796..2b1d5f7 100644 --- a/Editor/VisualStudioInstallation.cs +++ b/Editor/VisualStudioInstallation.cs @@ -10,7 +10,16 @@ using IOPath = System.IO.Path; namespace Microsoft.Unity.VisualStudio.Editor { - internal class VisualStudioInstallation + internal interface IVisualStudioInstallation + { + string Path { get; } + bool SupportsAnalyzers { get; } + bool SupportsCSharp8 { get; } + string[] GetAnalyzers(); + CodeEditor.Installation ToCodeEditorInstallation(); + } + + internal class VisualStudioInstallation : IVisualStudioInstallation { public string Name { get; set; } public string Path { get; set; } diff --git a/Editor/VisualStudioIntegration.cs b/Editor/VisualStudioIntegration.cs index 8c1bcc3..a07cdd1 100644 --- a/Editor/VisualStudioIntegration.cs +++ b/Editor/VisualStudioIntegration.cs @@ -33,8 +33,9 @@ namespace Microsoft.Unity.VisualStudio.Editor // - if another application is using this port with exclusive access // - or if the firewall is not properly configured var messagingPort = MessagingPort(); - - try { + + try + { _messager = Messager.BindTo(messagingPort); _messager.ReceiveMessage += ReceiveMessage; } @@ -155,7 +156,7 @@ namespace Microsoft.Unity.VisualStudio.Editor // If the user disabled auto-refresh in Unity, do not try to force refresh the Asset database if (!EditorPrefs.GetBool("kAutoRefresh", true)) return; - + RunOnceOnUpdate(AssetDatabase.Refresh); } diff --git a/package.json b/package.json index e380308..d7fbb87 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,18 @@ "name": "com.unity.ide.visualstudio", "displayName": "Visual Studio Editor", "description": "Code editor integration for supporting Visual Studio as code editor for unity. Adds support for generating csproj files for intellisense purposes, auto discovery of installations, etc.", - "version": "2.0.3", + "version": "2.0.5", "unity": "2020.1", "unityRelease": "0a12", "relatedPackages": { - "com.unity.ide.visualstudio.tests": "2.0.3" + "com.unity.ide.visualstudio.tests": "2.0.5" }, "upmCi": { - "footprint": "a528d76d0398bf543d3597af18e87e2f3a13d40a" + "footprint": "848c02b3f0fe476a599004cd972346a89e39d26f" }, "repository": { "url": "https://github.cds.internal.unity3d.com/unity/com.unity.ide.visualstudio.git", "type": "git", - "revision": "869804bff6e99d0d0e9c1867aebaa070fad875e5" + "revision": "83ca94e82bb6da515dc57e0d860b6b2224f56991" } }