From d1e4dd05ad818112d067dd465b5b3c387498fdc7 Mon Sep 17 00:00:00 2001 From: Unity Technologies <@unity> Date: Tue, 27 Jun 2023 00:00:00 +0000 Subject: [PATCH] com.unity.ide.visualstudio@2.0.20 ## [2.0.20] - 2023-06-27 Integration: - Internal API refactoring. ## [2.0.19] - 2023-06-14 Integration: - Add support for Visual Studio Code. Project generation: - Add support for Sdk Style poject generation. - Fix an issue related to missing properties with 2021.3. --- CHANGELOG.md | 22 +- .../COMIntegration/Release/COMIntegration.exe | Bin 294400 -> 294400 bytes Editor/Cli.cs | 2 +- Editor/Discovery.cs | 158 ++------ .../Contents/MacOS/AppleEventIntegration | Bin 153664 -> 153664 bytes Editor/ProcessRunner.cs | 24 +- .../ProjectGeneration/AssemblyNameProvider.cs | 6 + .../LegacyStyleProjectGeneration.cs | 98 +++++ .../LegacyStyleProjectGeneration.cs.meta | 11 + Editor/ProjectGeneration/ProjectGeneration.cs | 176 ++++----- .../SdkStyleProjectGeneration.cs | 82 ++++ .../SdkStyleProjectGeneration.cs.meta | 11 + Editor/VisualStudioCodeInstallation.cs | 345 +++++++++++++++++ Editor/VisualStudioCodeInstallation.cs.meta | 11 + Editor/VisualStudioEditor.cs | 291 +++++--------- Editor/VisualStudioForMacInstallation.cs | 180 +++++++++ Editor/VisualStudioForMacInstallation.cs.meta | 11 + Editor/VisualStudioForWindowsInstallation.cs | 363 ++++++++++++++++++ ...VisualStudioForWindowsInstallation.cs.meta | 11 + Editor/VisualStudioInstallation.cs | 190 +-------- ValidationConfig.json | 2 +- ValidationExceptions.json | 9 +- ValidationExceptions.json.meta | 2 +- package.json | 10 +- 24 files changed, 1390 insertions(+), 625 deletions(-) create mode 100644 Editor/ProjectGeneration/LegacyStyleProjectGeneration.cs create mode 100644 Editor/ProjectGeneration/LegacyStyleProjectGeneration.cs.meta create mode 100644 Editor/ProjectGeneration/SdkStyleProjectGeneration.cs create mode 100644 Editor/ProjectGeneration/SdkStyleProjectGeneration.cs.meta create mode 100644 Editor/VisualStudioCodeInstallation.cs create mode 100644 Editor/VisualStudioCodeInstallation.cs.meta create mode 100644 Editor/VisualStudioForMacInstallation.cs create mode 100644 Editor/VisualStudioForMacInstallation.cs.meta create mode 100644 Editor/VisualStudioForWindowsInstallation.cs create mode 100644 Editor/VisualStudioForWindowsInstallation.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bcf186..0a694c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Code Editor Package for Visual Studio +## [2.0.20] - 2023-06-27 + +Integration: + +- Internal API refactoring. + + + +## [2.0.19] - 2023-06-14 + +Integration: + +- Add support for Visual Studio Code. + +Project generation: + +- Add support for Sdk Style poject generation. +- Fix an issue related to missing properties with 2021.3. + + ## [2.0.18] - 2023-03-17 Integration: @@ -10,7 +30,6 @@ Project generation: - Add extra compiler options for analyzers and source generators. - ## [2.0.17] - 2022-12-06 Integration: @@ -24,7 +43,6 @@ Project generation: - Update supported C# versions. - Performance improvements. - ## [2.0.16] - 2022-06-08 Integration: diff --git a/Editor/COMIntegration/Release/COMIntegration.exe b/Editor/COMIntegration/Release/COMIntegration.exe index d67f42c7ae6ea75e1595d62fef625618b41b62cb..4622082f65ba68b348530d845daa0b8609c11c6e 100644 GIT binary patch delta 40 scmZqpBiQgqaDxCN^NuC6nuQtLg&9GZX}d5Zv*jg_;CBDZ%o|q$048h>t^fc4 delta 40 rcmZqpBiQgqaDxCN^S%g?W?{y5VMY*U+Ahq GetVisualStudioInstallations() { - if (VisualStudioEditor.IsWindows) - { - foreach (var installation in QueryVsWhere()) - yield return installation; - } + foreach (var installation in VisualStudioForWindowsInstallation.GetVisualStudioInstallations()) + yield return installation; - if (VisualStudioEditor.IsOSX) - { - var candidates = Directory.EnumerateDirectories("/Applications", "*.app"); - foreach (var candidate in candidates) - { - if (TryDiscoverInstallation(candidate, out var installation)) - yield return installation; - } - } - } + foreach (var installation in VisualStudioForMacInstallation.GetVisualStudioInstallations()) + yield return installation; - private static bool IsCandidateForDiscovery(string path) - { - if (File.Exists(path) && VisualStudioEditor.IsWindows && Regex.IsMatch(path, "devenv.exe$", RegexOptions.IgnoreCase)) - return true; - - if (Directory.Exists(path) && VisualStudioEditor.IsOSX && Regex.IsMatch(path, "Visual\\s?Studio(?!.*Code.*).*.app$", RegexOptions.IgnoreCase)) - return true; - - return false; + foreach (var installation in VisualStudioCodeInstallation.GetVisualStudioInstallations()) + yield return installation; } public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation) { - installation = null; - - if (string.IsNullOrEmpty(editorPath)) - return false; - - if (!IsCandidateForDiscovery(editorPath)) - return false; - - // On windows we use the executable directly, so we can query extra information - var fvi = editorPath; - - // On Mac we use the .app folder, so we need to access to main assembly - if (VisualStudioEditor.IsOSX) + try { - fvi = Path.Combine(editorPath, "Contents/Resources/lib/monodevelop/bin/VisualStudio.exe"); + if (VisualStudioForWindowsInstallation.TryDiscoverInstallation(editorPath, out installation)) + return true; - if (!File.Exists(fvi)) - fvi = Path.Combine(editorPath, "Contents/MonoBundle/VisualStudio.exe"); + if (VisualStudioForMacInstallation.TryDiscoverInstallation(editorPath, out installation)) + return true; - if (!File.Exists(fvi)) - fvi = Path.Combine(editorPath, "Contents/MonoBundle/VisualStudio.dll"); + if (VisualStudioCodeInstallation.TryDiscoverInstallation(editorPath, out installation)) + return true; + } + catch (IOException) + { + installation = null; } - if (!File.Exists(fvi)) - return false; - - // VS preview are not using the isPrerelease flag so far - // On Windows FileDescription contains "Preview", but not on Mac - var vi = FileVersionInfo.GetVersionInfo(fvi); - var version = new Version(vi.ProductVersion); - var isPrerelease = vi.IsPreRelease || string.Concat(editorPath, "/" + vi.FileDescription).ToLower().Contains("preview"); - - installation = new VisualStudioInstallation() - { - IsPrerelease = isPrerelease, - Name = $"{vi.FileDescription}{(isPrerelease && VisualStudioEditor.IsOSX ? " Preview" : string.Empty)} [{version.ToString(3)}]", - Path = editorPath, - Version = version - }; - return true; + return false; } - #region VsWhere Json Schema -#pragma warning disable CS0649 - [Serializable] - internal class VsWhereResult + public static void Initialize() { - public VsWhereEntry[] entries; - - public static VsWhereResult FromJson(string json) - { - return JsonUtility.FromJson("{ \"" + nameof(VsWhereResult.entries) + "\": " + json + " }"); - } - - public IEnumerable ToVisualStudioInstallations() - { - foreach (var entry in entries) - { - yield return new VisualStudioInstallation() - { - Name = $"{entry.displayName} [{entry.catalog.productDisplayVersion}]", - Path = entry.productPath, - IsPrerelease = entry.isPrerelease, - Version = Version.Parse(entry.catalog.buildVersion) - }; - } - } - } - - [Serializable] - internal class VsWhereEntry - { - public string displayName; - public bool isPrerelease; - public string productPath; - public VsWhereCatalog catalog; - } - - [Serializable] - internal class VsWhereCatalog - { - public string productDisplayVersion; // non parseable like "16.3.0 Preview 3.0" - public string buildVersion; - } -#pragma warning restore CS3021 - #endregion - - private static IEnumerable QueryVsWhere() - { - var progpath = _vsWherePath; - - if (string.IsNullOrWhiteSpace(progpath)) - return Enumerable.Empty(); - - var result = ProcessRunner.StartAndWaitForExit(progpath, "-prerelease -format json -utf8"); - - if (!result.Success) - throw new Exception($"Failure while running vswhere: {result.Error}"); - - // Do not catch any JsonException here, this will be handled by the caller - return VsWhereResult - .FromJson(result.Output) - .ToVisualStudioInstallations(); + VisualStudioForWindowsInstallation.Initialize(); + VisualStudioForMacInstallation.Initialize(); + VisualStudioCodeInstallation.Initialize(); } } } diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration b/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration index 59720340e9c1621608d50165939d383911d2adaf..038318eb1b9c419690cb8f3c03f0ca6b642312fd 100644 GIT binary patch delta 108 zcmV-y0F(c~unEAh39yjF1D2$kvy;Sb3?NdY^Rif`9op_P^gh)w(|xm=>yyV9E3>9> zl=kUsI@N@(;|v*NV&+@F0^2g*B1*429r*0xL(T_%WYAfmh diff --git a/Editor/ProcessRunner.cs b/Editor/ProcessRunner.cs index ebb8ecb..72b18aa 100644 --- a/Editor/ProcessRunner.cs +++ b/Editor/ProcessRunner.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Unity.VisualStudio.Editor @@ -21,19 +22,34 @@ namespace Microsoft.Unity.VisualStudio.Editor { public const int DefaultTimeoutInMilliseconds = 300000; - public static ProcessStartInfo ProcessStartInfoFor(string filename, string arguments) + public static ProcessStartInfo ProcessStartInfoFor(string filename, string arguments, bool redirect = true, bool shell = false) { return new ProcessStartInfo { - UseShellExecute = false, + UseShellExecute = shell, CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, + RedirectStandardOutput = redirect, + RedirectStandardError = redirect, FileName = filename, Arguments = arguments }; } + public static void Start(string filename, string arguments) + { + Start(ProcessStartInfoFor(filename, arguments, false)); + } + + public static void Start(ProcessStartInfo processStartInfo) + { + var process = new Process { StartInfo = processStartInfo }; + + using (process) + { + process.Start(); + } + } + public static ProcessRunnerResult StartAndWaitForExit(string filename, string arguments, int timeoutms = DefaultTimeoutInMilliseconds, Action onOutputReceived = null) { return StartAndWaitForExit(ProcessStartInfoFor(filename, arguments), timeoutms, onOutputReceived); diff --git a/Editor/ProjectGeneration/AssemblyNameProvider.cs b/Editor/ProjectGeneration/AssemblyNameProvider.cs index 7dba7c3..6902843 100644 --- a/Editor/ProjectGeneration/AssemblyNameProvider.cs +++ b/Editor/ProjectGeneration/AssemblyNameProvider.cs @@ -41,6 +41,12 @@ namespace Microsoft.Unity.VisualStudio.Editor public string ProjectGenerationRootNamespace => EditorSettings.projectGenerationRootNamespace; public ProjectGenerationFlag ProjectGenerationFlag + { + get { return ProjectGenerationFlagImpl; } + private set { ProjectGenerationFlagImpl = value;} + } + + internal virtual ProjectGenerationFlag ProjectGenerationFlagImpl { get => m_ProjectGenerationFlag; private set diff --git a/Editor/ProjectGeneration/LegacyStyleProjectGeneration.cs b/Editor/ProjectGeneration/LegacyStyleProjectGeneration.cs new file mode 100644 index 0000000..47cfdc1 --- /dev/null +++ b/Editor/ProjectGeneration/LegacyStyleProjectGeneration.cs @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * 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.Text; +using UnityEditor.Compilation; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + + internal class LegacyStyleProjectGeneration : ProjectGeneration + { + public LegacyStyleProjectGeneration(string tempDirectory, IAssemblyNameProvider assemblyNameProvider, IFileIO fileIoProvider, IGUIDGenerator guidGenerator) : base(tempDirectory, assemblyNameProvider, fileIoProvider, guidGenerator) + { + } + + public LegacyStyleProjectGeneration(string tempDirectory) : base(tempDirectory) + { + } + + public LegacyStyleProjectGeneration() + { + } + + internal override void GetProjectHeader(ProjectProperties properties, out StringBuilder headerBuilder) + { + headerBuilder = new StringBuilder(); + + //Header + headerBuilder.Append(@"").Append(k_WindowsNewline); + headerBuilder.Append($@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.LangVersion).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" Debug").Append(k_WindowsNewline); + headerBuilder.Append(@" AnyCPU").Append(k_WindowsNewline); + headerBuilder.Append(@" 10.0.20506").Append(k_WindowsNewline); + headerBuilder.Append(@" 2.0").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.RootNamespace).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" {").Append(properties.ProjectGuid).Append(@"}").Append(k_WindowsNewline); + headerBuilder.Append(@" Library").Append(k_WindowsNewline); + headerBuilder.Append(@" Properties").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.AssemblyName).Append(@"").Append(k_WindowsNewline); + // In the end, given we use NoConfig/NoStdLib (see below), hardcoding the target framework version with the legacy format will have no impact, even when targeting netstandard/net48 from Unity. + // And VSTU/Unity Game workload has a dependency towards net471 reference assemblies, so IDE will not complain that this specific SDK is not available. + // Unity already selected proper API surface through referenced DLLs for us. + headerBuilder.Append(@" v4.7.1").Append(k_WindowsNewline); + headerBuilder.Append(@" 512").Append(k_WindowsNewline); + headerBuilder.Append(@" .").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + + GetProjectHeaderConfigurations(properties, headerBuilder); + + // Explicit references + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" true").Append(k_WindowsNewline); + headerBuilder.Append(@" true").Append(k_WindowsNewline); + headerBuilder.Append(@" false").Append(k_WindowsNewline); + headerBuilder.Append(@" false").Append(k_WindowsNewline); + headerBuilder.Append(@" false").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + + GetProjectHeaderVstuFlavoring(properties, headerBuilder); + GetProjectHeaderAnalyzers(properties, headerBuilder); + } + + internal override void AppendProjectReference(Assembly assembly, Assembly reference, StringBuilder projectBuilder) + { + // If the current assembly is a Player project, we want to project-reference the corresponding Player project + var referenceName = m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, reference.name); + + projectBuilder.Append(@" ").Append(k_WindowsNewline); + projectBuilder.Append(" {").Append(ProjectGuid(referenceName)).Append("}").Append(k_WindowsNewline); + projectBuilder.Append(" ").Append(referenceName).Append("").Append(k_WindowsNewline); + projectBuilder.Append(" ").Append(k_WindowsNewline); + } + + internal override void GetProjectFooter(StringBuilder footerBuilder) + { + footerBuilder.Append(string.Join(k_WindowsNewline, + @" ", + @" ", + @" ", + @"", + @"")); + } + } +} diff --git a/Editor/ProjectGeneration/LegacyStyleProjectGeneration.cs.meta b/Editor/ProjectGeneration/LegacyStyleProjectGeneration.cs.meta new file mode 100644 index 0000000..4681c14 --- /dev/null +++ b/Editor/ProjectGeneration/LegacyStyleProjectGeneration.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3070b6395b0f5f04faaab13164c6256d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ProjectGeneration/ProjectGeneration.cs b/Editor/ProjectGeneration/ProjectGeneration.cs index 35d77b0..e8e0dad 100644 --- a/Editor/ProjectGeneration/ProjectGeneration.cs +++ b/Editor/ProjectGeneration/ProjectGeneration.cs @@ -39,12 +39,14 @@ namespace Microsoft.Unity.VisualStudio.Editor public class ProjectGeneration : IGenerator { + // do not remove because of the Validation API, used in LegacyStyleProjectGeneration public static readonly string MSBuildNamespaceUri = "http://schemas.microsoft.com/developer/msbuild/2003"; + public IAssemblyNameProvider AssemblyNameProvider => m_AssemblyNameProvider; public string ProjectDirectory { get; } // Use this to have the same newline ending on all platforms for consistency. - const string k_WindowsNewline = "\r\n"; + internal const string k_WindowsNewline = "\r\n"; const string m_SolutionProjectEntryTemplate = @"Project(""{{{0}}}"") = ""{1}"", ""{2}"", ""{{{3}}}""{4}EndProject"; @@ -60,7 +62,7 @@ namespace Microsoft.Unity.VisualStudio.Editor HashSet m_BuiltinSupportedExtensions = new HashSet(); readonly string m_ProjectName; - readonly IAssemblyNameProvider m_AssemblyNameProvider; + internal readonly IAssemblyNameProvider m_AssemblyNameProvider; readonly IFileIO m_FileIOProvider; readonly IGUIDGenerator m_GUIDGenerator; bool m_ShouldGenerateAll; @@ -106,9 +108,7 @@ namespace Microsoft.Unity.VisualStudio.Editor 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(); + CreateExtraFiles(m_CurrentInstallation); // Don't sync if we haven't synced before var affected = affectedFiles as ICollection ?? affectedFiles.ToArray(); @@ -148,26 +148,9 @@ namespace Microsoft.Unity.VisualStudio.Editor } } - private void CreateVsConfigIfNotFound() + private void CreateExtraFiles(IVisualStudioInstallation installation) { - 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) - { - } + installation?.CreateExtraFiles(ProjectDirectory); } private bool HasFilesBeenModified(IEnumerable affectedFiles, IEnumerable reimportedFiles) @@ -183,7 +166,7 @@ namespace Microsoft.Unity.VisualStudio.Editor private void RefreshCurrentInstallation() { var editor = CodeEditor.CurrentEditor as VisualStudioEditor; - editor?.TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, searchInstallations: true, out m_CurrentInstallation); + editor?.TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, lookupDiscoveredInstallations: true, out m_CurrentInstallation); } static ProfilerMarker solutionSyncMarker = new ProfilerMarker("SolutionSynchronizerSync"); @@ -199,7 +182,7 @@ namespace Microsoft.Unity.VisualStudio.Editor // 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(); + CreateExtraFiles(m_CurrentInstallation); var externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles(); @@ -366,7 +349,7 @@ namespace Microsoft.Unity.VisualStudio.Editor stringBuilders[assemblyName] = projectBuilder; } - IncludeAsset(projectBuilder, "None", asset); + IncludeAsset(projectBuilder, IncludeAssetTag.None, asset); } } @@ -378,7 +361,13 @@ namespace Microsoft.Unity.VisualStudio.Editor return result; } - private void IncludeAsset(StringBuilder builder, string tag, string asset) + internal enum IncludeAssetTag + { + Compile, + None + } + + internal virtual void IncludeAsset(StringBuilder builder, IncludeAssetTag tag, string asset) { var filename = EscapedRelativePathFor(asset, out var packageInfo); @@ -517,7 +506,7 @@ namespace Microsoft.Unity.VisualStudio.Editor if ("dll" != extensionWithoutDot) { - IncludeAsset(projectBuilder, "Compile", file); + IncludeAsset(projectBuilder, IncludeAssetTag.Compile, file); } else { @@ -562,19 +551,13 @@ namespace Microsoft.Unity.VisualStudio.Editor projectBuilder.Append(" ").Append(k_WindowsNewline); foreach (var reference in assembly.assemblyReferences.Where(i => i.sourceFiles.Any(ShouldFileBePartOfSolution))) { - // If the current assembly is a Player project, we want to project-reference the corresponding Player project - var referenceName = m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, reference.name); - - projectBuilder.Append(@" ").Append(k_WindowsNewline); - projectBuilder.Append(" {").Append(ProjectGuid(referenceName)).Append("}").Append(k_WindowsNewline); - projectBuilder.Append(" ").Append(referenceName).Append("").Append(k_WindowsNewline); - projectBuilder.Append(" ").Append(k_WindowsNewline); + AppendProjectReference(assembly, reference, projectBuilder); } projectBuilder.Append(@" ").Append(k_WindowsNewline); } - projectBuilder.Append(GetProjectFooter()); + GetProjectFooter(projectBuilder); return projectBuilder.ToString(); } @@ -594,6 +577,10 @@ namespace Microsoft.Unity.VisualStudio.Editor return SecurityElement.Escape(s); } + internal virtual void AppendProjectReference(Assembly assembly, Assembly reference, StringBuilder projectBuilder) + { + } + private void AppendReference(string fullReference, StringBuilder projectBuilder) { var escapedFullPath = EscapedRelativePathFor(fullReference, out _); @@ -614,11 +601,6 @@ namespace Microsoft.Unity.VisualStudio.Editor return Path.Combine(ProjectDirectory.NormalizePathSeparators(), $"{InvalidCharactersRegexPattern.Replace(m_ProjectName, "_")}.sln"); } - internal string VsConfigFile() - { - return Path.Combine(ProjectDirectory.NormalizePathSeparators(), ".vsconfig"); - } - internal string GetLangVersion(Assembly assembly) { var targetLanguageVersion = "latest"; // danger: latest is not the same absolute value depending on the VS version. @@ -674,16 +656,25 @@ namespace Microsoft.Unity.VisualStudio.Editor var additionalFilePaths = new List(); var rulesetPath = string.Empty; var analyzerConfigPath = string.Empty; + var compilerOptions = assembly.compilerOptions; #if UNITY_2020_2_OR_NEWER // Analyzers + ruleset provided by Unity - analyzers.AddRange(assembly.compilerOptions.RoslynAnalyzerDllPaths); - rulesetPath = assembly.compilerOptions.RoslynAnalyzerRulesetPath; + analyzers.AddRange(compilerOptions.RoslynAnalyzerDllPaths); + rulesetPath = compilerOptions.RoslynAnalyzerRulesetPath; #endif -#if UNITY_2021_3_OR_NEWER && !UNITY_2022_1 // we have support in 2021.3, 2022.2 but without a backport in 2022.1 - additionalFilePaths.AddRange(assembly.compilerOptions.RoslynAdditionalFilePaths); - analyzerConfigPath = assembly.compilerOptions.AnalyzerConfigPath; + // We have support in 2021.3, 2022.2 but without a backport in 2022.1 +#if UNITY_2021_3 + // Unfortunately those properties were introduced in a patch release of 2021.3, so not found in 2021.3.2f1 for example + var scoType = compilerOptions.GetType(); + var afpProperty = scoType.GetProperty("RoslynAdditionalFilePaths"); + var acpProperty = scoType.GetProperty("AnalyzerConfigPath"); + additionalFilePaths.AddRange(afpProperty?.GetValue(compilerOptions) as string[] ?? Array.Empty()); + analyzerConfigPath = acpProperty?.GetValue(compilerOptions) as string ?? analyzerConfigPath; +#elif UNITY_2022_2_OR_NEWER + additionalFilePaths.AddRange(compilerOptions.RoslynAdditionalFilePaths); + analyzerConfigPath = compilerOptions.AnalyzerConfigPath; #endif // Analyzers and additional files provided by csc.rsp @@ -765,30 +756,13 @@ namespace Microsoft.Unity.VisualStudio.Editor return ProjectType.Game; } - private void GetProjectHeader(ProjectProperties properties, out StringBuilder headerBuilder) + internal virtual void GetProjectHeader(ProjectProperties properties, out StringBuilder headerBuilder) { - headerBuilder = new StringBuilder(); + headerBuilder = default; + } - //Header - headerBuilder.Append(@"").Append(k_WindowsNewline); - headerBuilder.Append(@"").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(properties.LangVersion).Append(@"").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(k_WindowsNewline); - headerBuilder.Append(@" Debug").Append(k_WindowsNewline); - headerBuilder.Append(@" AnyCPU").Append(k_WindowsNewline); - headerBuilder.Append(@" 10.0.20506").Append(k_WindowsNewline); - headerBuilder.Append(@" 2.0").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(properties.RootNamespace).Append(@"").Append(k_WindowsNewline); - headerBuilder.Append(@" {").Append(properties.ProjectGuid).Append(@"}").Append(k_WindowsNewline); - headerBuilder.Append(@" Library").Append(k_WindowsNewline); - headerBuilder.Append(@" Properties").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(properties.AssemblyName).Append(@"").Append(k_WindowsNewline); - headerBuilder.Append(@" v4.7.1").Append(k_WindowsNewline); - headerBuilder.Append(@" 512").Append(k_WindowsNewline); - headerBuilder.Append(@" .").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(k_WindowsNewline); + internal static void GetProjectHeaderConfigurations(ProjectProperties properties, StringBuilder headerBuilder) + { headerBuilder.Append(@" ").Append(k_WindowsNewline); headerBuilder.Append(@" true").Append(k_WindowsNewline); headerBuilder.Append(@" full").Append(k_WindowsNewline); @@ -809,26 +783,10 @@ namespace Microsoft.Unity.VisualStudio.Editor headerBuilder.Append(@" 0169").Append(k_WindowsNewline); headerBuilder.Append(@" ").Append(properties.Unsafe).Append(@"").Append(k_WindowsNewline); headerBuilder.Append(@" ").Append(k_WindowsNewline); + } - // Explicit references - headerBuilder.Append(@" ").Append(k_WindowsNewline); - headerBuilder.Append(@" true").Append(k_WindowsNewline); - headerBuilder.Append(@" true").Append(k_WindowsNewline); - headerBuilder.Append(@" false").Append(k_WindowsNewline); - headerBuilder.Append(@" false").Append(k_WindowsNewline); - headerBuilder.Append(@" false").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(k_WindowsNewline); - - // Flavoring - headerBuilder.Append(@" ").Append(k_WindowsNewline); - headerBuilder.Append(@" {E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}").Append(k_WindowsNewline); - headerBuilder.Append(@" Package").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(properties.FlavoringPackageVersion).Append(@"").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(properties.FlavoringProjectType).Append(@"").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(properties.FlavoringBuildTarget).Append(@"").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(properties.FlavoringUnityVersion).Append(@"").Append(k_WindowsNewline); - headerBuilder.Append(@" ").Append(k_WindowsNewline); - + internal static void GetProjectHeaderAnalyzers(ProjectProperties properties, StringBuilder headerBuilder) + { if (!string.IsNullOrEmpty(properties.RulesetPath)) { headerBuilder.Append(@" ").Append(k_WindowsNewline); @@ -864,20 +822,26 @@ namespace Microsoft.Unity.VisualStudio.Editor } } - private static string GetProjectFooter() + internal static void GetProjectHeaderVstuFlavoring(ProjectProperties properties, StringBuilder headerBuilder, bool includeProjectTypeGuids = true) + { + // Flavoring + headerBuilder.Append(@" ").Append(k_WindowsNewline); + + if (includeProjectTypeGuids) + { + headerBuilder.Append(@" {E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}").Append(k_WindowsNewline); + } + + headerBuilder.Append(@" Package").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.FlavoringPackageVersion).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.FlavoringProjectType).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.FlavoringBuildTarget).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.FlavoringUnityVersion).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + } + + internal virtual void GetProjectFooter(StringBuilder footerBuilder) { - return string.Join(k_WindowsNewline, - @" ", - @" ", - @" ", - @"", - @""); } private static string GetSolutionText() @@ -1024,7 +988,7 @@ namespace Microsoft.Unity.VisualStudio.Editor projectGuid); } - private string EscapedRelativePathFor(string file, out UnityEditor.PackageManager.PackageInfo packageInfo) + internal string EscapedRelativePathFor(string file, out UnityEditor.PackageManager.PackageInfo packageInfo) { var projectDir = ProjectDirectory.NormalizePathSeparators(); file = file.NormalizePathSeparators(); @@ -1042,24 +1006,24 @@ namespace Microsoft.Unity.VisualStudio.Editor return XmlFilename(path); } - private static string SkipPathPrefix(string path, string prefix) + internal 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; } - static string GetProjectExtension() + internal static string GetProjectExtension() { return ".csproj"; } - private string ProjectGuid(string assemblyName) + internal string ProjectGuid(string assemblyName) { return m_GUIDGenerator.ProjectGuid(m_ProjectName, assemblyName); } - private string ProjectGuid(Assembly assembly) + internal string ProjectGuid(Assembly assembly) { return ProjectGuid(m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, assembly.name)); } diff --git a/Editor/ProjectGeneration/SdkStyleProjectGeneration.cs b/Editor/ProjectGeneration/SdkStyleProjectGeneration.cs new file mode 100644 index 0000000..f458414 --- /dev/null +++ b/Editor/ProjectGeneration/SdkStyleProjectGeneration.cs @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * 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; +using UnityEditor.Compilation; +using UnityEngine; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + internal class SdkStyleProjectGeneration : ProjectGeneration + { + internal class SdkStyleAssemblyNameProvider : AssemblyNameProvider + { + // disable PlayerGeneration with SdkStyle projects + internal override ProjectGenerationFlag ProjectGenerationFlagImpl => base.ProjectGenerationFlagImpl & ~ProjectGenerationFlag.PlayerAssemblies; + } + + public SdkStyleProjectGeneration() : base( + Directory.GetParent(Application.dataPath)?.FullName, + new SdkStyleAssemblyNameProvider(), + new FileIOProvider(), + new GUIDProvider()) + { + } + + internal override void GetProjectHeader(ProjectProperties properties, out StringBuilder headerBuilder) + { + headerBuilder = new StringBuilder(); + + headerBuilder.Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" false").Append(k_WindowsNewline); + headerBuilder.Append(@" false").Append(k_WindowsNewline); + headerBuilder.Append(@" false").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.LangVersion).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" Debug;Release").Append(k_WindowsNewline); + headerBuilder.Append(@" Debug").Append(k_WindowsNewline); + headerBuilder.Append(@" AnyCPU").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.RootNamespace).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" Library").Append(k_WindowsNewline); + headerBuilder.Append(@" Properties").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.AssemblyName).Append(@"").Append(k_WindowsNewline); + // In the end, given we use NoConfig/NoStdLib (see below), hardcoding the target framework version will have no impact, even when targeting netstandard/net48 from Unity. + // But with SDK style we use netstandard2.0 (net471 for legacy), so 3rd party tools will not fail to work when .NETFW reference assemblies are not installed. + // Unity already selected proper API surface through referenced DLLs for us. + headerBuilder.Append(@" netstandard2.0").Append(k_WindowsNewline); + headerBuilder.Append(@" .").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + + GetProjectHeaderConfigurations(properties, headerBuilder); + + // Explicit references + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" true").Append(k_WindowsNewline); + headerBuilder.Append(@" true").Append(k_WindowsNewline); + headerBuilder.Append(@" true").Append(k_WindowsNewline); + headerBuilder.Append(@" true").Append(k_WindowsNewline); + headerBuilder.Append(@" MSB3277").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + + GetProjectHeaderVstuFlavoring(properties, headerBuilder, false); + GetProjectHeaderAnalyzers(properties, headerBuilder); + } + + internal override void AppendProjectReference(Assembly assembly, Assembly reference, StringBuilder projectBuilder) + { + // If the current assembly is a Player project, we want to project-reference the corresponding Player project + var referenceName = m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, reference.name); + projectBuilder.Append(@" ").Append(k_WindowsNewline); + } + + internal override void GetProjectFooter(StringBuilder footerBuilder) + { + footerBuilder.Append("").Append(k_WindowsNewline); + } + } +} diff --git a/Editor/ProjectGeneration/SdkStyleProjectGeneration.cs.meta b/Editor/ProjectGeneration/SdkStyleProjectGeneration.cs.meta new file mode 100644 index 0000000..79137b4 --- /dev/null +++ b/Editor/ProjectGeneration/SdkStyleProjectGeneration.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e8aed47cdce10fd4cae32fefa2c34f8f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/VisualStudioCodeInstallation.cs b/Editor/VisualStudioCodeInstallation.cs new file mode 100644 index 0000000..55c6102 --- /dev/null +++ b/Editor/VisualStudioCodeInstallation.cs @@ -0,0 +1,345 @@ +/*--------------------------------------------------------------------------------------------- + * 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using UnityEngine; +using IOPath = System.IO.Path; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + internal class VisualStudioCodeInstallation : VisualStudioInstallation + { + private static readonly IGenerator _generator = new SdkStyleProjectGeneration(); + + public override bool SupportsAnalyzers + { + get + { + return true; + } + } + + public override Version LatestLanguageVersionSupported + { + get + { + return new Version(11, 0); + } + } + + private string GetExtensionPath() + { + var vscode = IsPrerelease ? ".vscode-insiders" : ".vscode"; + var extensionsPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), vscode, "extensions"); + if (!Directory.Exists(extensionsPath)) + return null; + + return Directory + .EnumerateDirectories(extensionsPath, "visualstudiotoolsforunity.vstuc*") // publisherid.extensionid + .OrderByDescending(n => n) + .FirstOrDefault(); + } + + public override string[] GetAnalyzers() + { + var vstuPath = GetExtensionPath(); + if (string.IsNullOrEmpty(vstuPath)) + return Array.Empty(); + + return GetAnalyzers(vstuPath); } + + public override IGenerator ProjectGenerator + { + get + { + return _generator; + } + } + + private static bool IsCandidateForDiscovery(string path) + { + if (VisualStudioEditor.IsOSX) + return Directory.Exists(path) && Regex.IsMatch(path, ".*Code.*.app$", RegexOptions.IgnoreCase); + + if (VisualStudioEditor.IsWindows) + return File.Exists(path) && Regex.IsMatch(path, ".*Code.*.exe$", RegexOptions.IgnoreCase); + + return File.Exists(path) && path.EndsWith("code", StringComparison.OrdinalIgnoreCase); + } + + [Serializable] + internal class VisualStudioCodeManifest + { + public string name; + public string version; + } + + public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation) + { + installation = null; + + if (string.IsNullOrEmpty(editorPath)) + return false; + + if (!IsCandidateForDiscovery(editorPath)) + return false; + + Version version = null; + var isPrerelease = false; + + try + { + var manifestBase = GetRealPath(editorPath); + + if (VisualStudioEditor.IsWindows) // on Windows, editorPath is a file, resources as subdirectory + manifestBase = IOPath.GetDirectoryName(manifestBase); + else if (VisualStudioEditor.IsOSX) // on Mac, editorPath is a directory + manifestBase = IOPath.Combine(manifestBase, "Contents"); + else // on Linux, editorPath is a file, in a bin sub-directory + manifestBase = Directory.GetParent(manifestBase)?.Parent?.FullName; + + if (manifestBase == null) + return false; + + var manifestFullPath = IOPath.Combine(manifestBase, @"resources", "app", "package.json"); + if (File.Exists(manifestFullPath)) + { + var manifest = JsonUtility.FromJson(File.ReadAllText(manifestFullPath)); + Version.TryParse(manifest.version.Split('-').First(), out version); + isPrerelease = manifest.version.ToLower().Contains("insider"); + } + } + catch (Exception) + { + // do not fail if we are not able to retrieve the exact version number + } + + isPrerelease = isPrerelease || editorPath.ToLower().Contains("insider"); + installation = new VisualStudioCodeInstallation() + { + IsPrerelease = isPrerelease, + Name = "Visual Studio Code" + (isPrerelease ? " - Insider" : string.Empty) + (version != null ? $" [{version.ToString(3)}]" : string.Empty), + Path = editorPath, + Version = version ?? new Version() + }; + + return true; + } + + public static IEnumerable GetVisualStudioInstallations() + { + var candidates = new List(); + + if (VisualStudioEditor.IsWindows) + { + var localAppPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs"); + var programFiles = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)); + + foreach (var basePath in new[] {localAppPath, programFiles}) + { + candidates.Add(IOPath.Combine(basePath, "Microsoft VS Code", "Code.exe")); + candidates.Add(IOPath.Combine(basePath, "Microsoft VS Code Insiders", "Code - Insiders.exe")); + } + } + else if (VisualStudioEditor.IsOSX) + { + var appPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)); + candidates.AddRange(Directory.EnumerateDirectories(appPath, "Visual Studio Code*.app")); + } + else + { + candidates.Add("/usr/bin/code"); + candidates.Add("/bin/code"); + candidates.Add("/usr/local/bin/code"); + } + + foreach (var candidate in candidates) + { + if (TryDiscoverInstallation(candidate, out var installation)) + yield return installation; + } + } + +#if UNITY_EDITOR_LINUX + [DllImport ("libc")] + private static extern int readlink(string path, byte[] buffer, int buflen); + + internal static string GetRealPath(string path) + { + byte[] buf = new byte[512]; + int ret = readlink(path, buf, buf.Length); + if (ret == -1) return path; + char[] cbuf = new char[512]; + int chars = System.Text.Encoding.Default.GetChars(buf, 0, ret, cbuf, 0); + return new String(cbuf, 0, chars); + } +#else + internal static string GetRealPath(string path) + { + return path; + } +#endif + + public override void CreateExtraFiles(string projectDirectory) + { + try + { + // see https://tattoocoder.com/recommending-vscode-extensions-within-your-open-source-projects/ + var vscodeDirectory = IOPath.Combine(projectDirectory.NormalizePathSeparators(), ".vscode"); + Directory.CreateDirectory(vscodeDirectory); + + CreateRecommendedExtensionsFile(vscodeDirectory); + CreateSettingsFile(vscodeDirectory); + CreateLaunchFile(vscodeDirectory); + } + catch (IOException) + { + } + } + + private static void CreateLaunchFile(string vscodeDirectory) + { + var launchFile = IOPath.Combine(vscodeDirectory, "launch.json"); + if (File.Exists(launchFile)) + return; + + const string content = @"{ + ""version"": ""0.2.0"", + ""configurations"": [ + { + ""name"": ""Attach to Unity"", + ""type"": ""vstuc"", + ""request"": ""attach"", + } + ] +}"; + + File.WriteAllText(launchFile, content); + } + + private static void CreateSettingsFile(string vscodeDirectory) + { + var settingsFile = IOPath.Combine(vscodeDirectory, "settings.json"); + if (File.Exists(settingsFile)) + return; + + const string content = @"{ + ""files.exclude"": + { + ""**/.DS_Store"":true, + ""**/.git"":true, + ""**/.vs"":true, + ""**/.gitmodules"":true, + ""**/.vsconfig"":true, + ""**/*.booproj"":true, + ""**/*.pidb"":true, + ""**/*.suo"":true, + ""**/*.user"":true, + ""**/*.userprefs"":true, + ""**/*.unityproj"":true, + ""**/*.dll"":true, + ""**/*.exe"":true, + ""**/*.pdf"":true, + ""**/*.mid"":true, + ""**/*.midi"":true, + ""**/*.wav"":true, + ""**/*.gif"":true, + ""**/*.ico"":true, + ""**/*.jpg"":true, + ""**/*.jpeg"":true, + ""**/*.png"":true, + ""**/*.psd"":true, + ""**/*.tga"":true, + ""**/*.tif"":true, + ""**/*.tiff"":true, + ""**/*.3ds"":true, + ""**/*.3DS"":true, + ""**/*.fbx"":true, + ""**/*.FBX"":true, + ""**/*.lxo"":true, + ""**/*.LXO"":true, + ""**/*.ma"":true, + ""**/*.MA"":true, + ""**/*.obj"":true, + ""**/*.OBJ"":true, + ""**/*.asset"":true, + ""**/*.cubemap"":true, + ""**/*.flare"":true, + ""**/*.mat"":true, + ""**/*.meta"":true, + ""**/*.prefab"":true, + ""**/*.unity"":true, + ""build/"":true, + ""Build/"":true, + ""Library/"":true, + ""library/"":true, + ""obj/"":true, + ""Obj/"":true, + ""Logs/"":true, + ""logs/"":true, + ""ProjectSettings/"":true, + ""UserSettings/"":true, + ""temp/"":true, + ""Temp/"":true + }, + ""omnisharp.enableRoslynAnalyzers"": true +}"; + + File.WriteAllText(settingsFile, content); + } + + private static void CreateRecommendedExtensionsFile(string vscodeDirectory) + { + var extensionFile = IOPath.Combine(vscodeDirectory, "extensions.json"); + if (File.Exists(extensionFile)) + return; + + const string content = @"{ + ""recommendations"": [ + ""visualstudiotoolsforunity.vstuc"" + ] +} +"; + File.WriteAllText(extensionFile, content); + } + + public override bool Open(string path, int line, int column, string solution) + { + line = Math.Max(1, line); + column = Math.Max(0, column); + + var directory = IOPath.GetDirectoryName(solution); + var application = Path; + + ProcessRunner.Start(string.IsNullOrEmpty(path) ? + ProcessStartInfoFor(application, $"\"{directory}\"") : + ProcessStartInfoFor(application, $"\"{directory}\" -g \"{path}\":{line}:{column}")); + + return true; + } + + private static ProcessStartInfo ProcessStartInfoFor(string application, string arguments) + { + if (!VisualStudioEditor.IsOSX) + return ProcessRunner.ProcessStartInfoFor(application, arguments, redirect: false); + + // wrap with built-in OSX open feature + arguments = $"-n \"{application}\" --args {arguments}"; + application = "open"; + return ProcessRunner.ProcessStartInfoFor(application, arguments, redirect:false, shell: true); + } + + public static void Initialize() + { + } + } +} diff --git a/Editor/VisualStudioCodeInstallation.cs.meta b/Editor/VisualStudioCodeInstallation.cs.meta new file mode 100644 index 0000000..699535a --- /dev/null +++ b/Editor/VisualStudioCodeInstallation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1be5c96ff30e6ec40876f28fd9ab7e24 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/VisualStudioEditor.cs b/Editor/VisualStudioEditor.cs index 7367d9e..a956699 100644 --- a/Editor/VisualStudioEditor.cs +++ b/Editor/VisualStudioEditor.cs @@ -4,15 +4,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ using System; +using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Runtime.CompilerServices; using UnityEditor; using UnityEngine; using Unity.CodeEditor; -using System.Threading; -using System.Collections.Concurrent; [assembly: InternalsVisibleTo("Unity.VisualStudio.EditorTests")] [assembly: InternalsVisibleTo("Unity.VisualStudio.Standalone.EditorTests")] @@ -26,39 +24,53 @@ namespace Microsoft.Unity.VisualStudio.Editor internal static bool IsOSX => Application.platform == RuntimePlatform.OSXEditor; internal static bool IsWindows => !IsOSX && Path.DirectorySeparatorChar == FileUtility.WinSeparator && Environment.NewLine == "\r\n"; - CodeEditor.Installation[] IExternalCodeEditor.Installations => _discoverInstallations.Result - .Select(i => i.ToCodeEditorInstallation()) + CodeEditor.Installation[] IExternalCodeEditor.Installations => _discoverInstallations + .Result + .Values + .Select(v => v.ToCodeEditorInstallation()) .ToArray(); - private static readonly AsyncOperation _discoverInstallations; - - private readonly IGenerator _generator = new ProjectGeneration(); + private static readonly AsyncOperation> _discoverInstallations; static VisualStudioEditor() { if (!UnityInstallation.IsMainUnityEditorProcess) return; - if (IsWindows) - Discovery.FindVSWhere(); - + Discovery.Initialize(); CodeEditor.Register(new VisualStudioEditor()); - _discoverInstallations = AsyncOperation.Run(DiscoverInstallations); + _discoverInstallations = AsyncOperation>.Run(DiscoverInstallations); } - private static IVisualStudioInstallation[] DiscoverInstallations() +#if UNITY_2019_4_OR_NEWER && !UNITY_2020 + [InitializeOnLoadMethod] + static void LegacyVisualStudioCodePackageDisabler() + { + // disable legacy Visual Studio Code packages + var editor = CodeEditor.Editor.GetCodeEditorForPath("code.cmd"); + if (editor == null) + return; + + if (editor is VisualStudioEditor) + return; + + CodeEditor.Unregister(editor); + } +#endif + + private static Dictionary DiscoverInstallations() { try { return Discovery .GetVisualStudioInstallations() - .ToArray(); + .ToDictionary(i => Path.GetFullPath(i.Path), i => i); } catch (Exception ex) { - UnityEngine.Debug.LogError($"Error detecting Visual Studio installations: {ex}"); - return Array.Empty(); + Debug.LogError($"Error detecting Visual Studio installations: {ex}"); + return new Dictionary(); } } @@ -68,36 +80,33 @@ namespace Microsoft.Unity.VisualStudio.Editor // keeping it for now given it is public, so we need a major bump to remove it public void CreateIfDoesntExist() { - if (!_generator.HasSolutionBeenGenerated()) - _generator.Sync(); + if (!TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation)) + return; + + var generator = installation.ProjectGenerator; + if (!generator.HasSolutionBeenGenerated()) + generator.Sync(); } public void Initialize(string editorInstallationPath) { } - internal virtual bool TryGetVisualStudioInstallationForPath(string editorPath, bool searchInstallations, out IVisualStudioInstallation installation) + internal virtual bool TryGetVisualStudioInstallationForPath(string editorPath, bool lookupDiscoveredInstallations, out IVisualStudioInstallation installation) { - if (searchInstallations) - { - // lookup for well known installations - foreach (var candidate in _discoverInstallations.Result) - { - if (!string.Equals(Path.GetFullPath(editorPath), Path.GetFullPath(candidate.Path), StringComparison.OrdinalIgnoreCase)) - continue; + editorPath = Path.GetFullPath(editorPath); - installation = candidate; - return true; - } - } + // lookup for well known installations + if (lookupDiscoveredInstallations && _discoverInstallations.Result.TryGetValue(editorPath, out installation)) + return true; return Discovery.TryDiscoverInstallation(editorPath, out installation); } public virtual bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation) { - var result = TryGetVisualStudioInstallationForPath(editorPath, searchInstallations: false, out var vsi); - installation = vsi == null ? default : vsi.ToCodeEditorInstallation(); + var result = TryGetVisualStudioInstallationForPath(editorPath, lookupDiscoveredInstallations: false, out var vsi); + installation = vsi?.ToCodeEditorInstallation() ?? default; return result; } @@ -106,6 +115,9 @@ namespace Microsoft.Unity.VisualStudio.Editor GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); + if (!TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation)) + return; + var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(GetType().Assembly); var style = new GUIStyle @@ -119,41 +131,44 @@ namespace Microsoft.Unity.VisualStudio.Editor EditorGUILayout.LabelField("Generate .csproj files for:"); EditorGUI.indentLevel++; - SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", ""); - SettingsButton(ProjectGenerationFlag.Local, "Local packages", ""); - SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", ""); - SettingsButton(ProjectGenerationFlag.Git, "Git packages", ""); - SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", ""); - SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", ""); - SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", ""); - SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'"); - RegenerateProjectFiles(); + SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "", installation); + SettingsButton(ProjectGenerationFlag.Local, "Local packages", "", installation); + SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", "", installation); + SettingsButton(ProjectGenerationFlag.Git, "Git packages", "", installation); + SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", "", installation); + SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", "", installation); + SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", "", installation); + SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'", installation); + RegenerateProjectFiles(installation); EditorGUI.indentLevel--; } - void RegenerateProjectFiles() + private static void RegenerateProjectFiles(IVisualStudioInstallation installation) { - var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(new GUILayoutOption[] { })); + var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect()); rect.width = 252; if (GUI.Button(rect, "Regenerate project files")) { - _generator.Sync(); + installation.ProjectGenerator.Sync(); } } - void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip) + private static void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip, IVisualStudioInstallation installation) { - var prevValue = _generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference); + var generator = installation.ProjectGenerator; + var prevValue = generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference); + var newValue = EditorGUILayout.Toggle(new GUIContent(guiMessage, toolTip), prevValue); if (newValue != prevValue) - { - _generator.AssemblyNameProvider.ToggleProjectGeneration(preference); - } + generator.AssemblyNameProvider.ToggleProjectGeneration(preference); } public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles) { - _generator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles); + if (TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation)) + { + installation.ProjectGenerator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles); + } foreach (var file in importedFiles.Where(a => Path.GetExtension(a) == ".pdb")) { @@ -170,16 +185,19 @@ namespace Microsoft.Unity.VisualStudio.Editor if (Symbols.IsPortableSymbolFile(pdbFile)) continue; - UnityEngine.Debug.LogWarning($"Unity is only able to load mdb or portable-pdb symbols. {file} is using a legacy pdb format."); + Debug.LogWarning($"Unity is only able to load mdb or portable-pdb symbols. {file} is using a legacy pdb format."); } } public void SyncAll() { - _generator.Sync(); + if (TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation)) + { + installation.ProjectGenerator.Sync(); + } } - bool IsSupportedPath(string path) + private static bool IsSupportedPath(string path, IGenerator generator) { // Path is empty with "Open C# Project", as we only want to open the solution without specific files if (string.IsNullOrEmpty(path)) @@ -188,44 +206,27 @@ namespace Microsoft.Unity.VisualStudio.Editor // cs, uxml, uss, shader, compute, cginc, hlsl, glslinc, template are part of Unity builtin extensions // txt, xml, fnt, cd are -often- par of Unity user extensions // asdmdef is mandatory included - if (_generator.IsSupportedFile(path)) - return true; - - 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."); + return generator.IsSupportedFile(path); } public bool OpenProject(string path, int line, int column) { - CheckCurrentEditorInstallation(); + var editorPath = CodeEditor.CurrentEditorInstallation; - if (!IsSupportedPath(path)) + if (!Discovery.TryDiscoverInstallation(editorPath, out var installation)) { + Debug.LogWarning($"Visual Studio executable {editorPath} is not found. Please change your settings in Edit > Preferences > External Tools."); + return false; + } + + var generator = installation.ProjectGenerator; + if (!IsSupportedPath(path, generator)) 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 (!IsProjectGeneratedFor(path, generator, out var missingFlag)) + 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); - - if (IsWindows) - return OpenWindowsApp(path, line); - - return false; + var solution = GetOrGenerateSolutionFile(generator); + return installation.Open(path, line, column, solution); } private static string GetProjectGenerationFlagDescription(ProjectGenerationFlag flag) @@ -253,7 +254,7 @@ namespace Microsoft.Unity.VisualStudio.Editor } } - private bool IsProjectGeneratedFor(string path, out ProjectGenerationFlag missingFlag) + private static bool IsProjectGeneratedFor(string path, IGenerator generator, out ProjectGenerationFlag missingFlag) { missingFlag = ProjectGenerationFlag.None; @@ -266,9 +267,9 @@ namespace Microsoft.Unity.VisualStudio.Editor 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) + var basePath = generator.ProjectDirectory; + var relativePath = path + .NormalizeWindowsToUnix() .Replace(basePath, string.Empty) .Trim(FileUtility.UnixSeparator); @@ -280,7 +281,7 @@ namespace Microsoft.Unity.VisualStudio.Editor if (!Enum.TryParse(source.ToString(), out var flag)) return true; - if (_generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(flag)) + if (generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(flag)) return true; // Return false if we found a source not flagged for generation @@ -288,118 +289,10 @@ namespace Microsoft.Unity.VisualStudio.Editor return false; } - private enum COMIntegrationState + private static string GetOrGenerateSolutionFile(IGenerator generator) { - Running, - DisplayProgressBar, - ClearProgressBar, - Exited - } - - private bool OpenWindowsApp(string path, int line) - { - var progpath = FileUtility.GetPackageAssetFullPath("Editor", "COMIntegration", "Release", "COMIntegration.exe"); - - if (string.IsNullOrWhiteSpace(progpath)) - return false; - - string absolutePath = ""; - if (!string.IsNullOrWhiteSpace(path)) - { - absolutePath = Path.GetFullPath(path); - } - - // We remove all invalid chars from the solution filename, but we cannot prevent the user from using a specific path for the Unity project - // So process the fullpath to make it compatible with VS - var solution = GetOrGenerateSolutionFile(path); - if (!string.IsNullOrWhiteSpace(solution)) - { - solution = $"\"{solution}\""; - solution = solution.Replace("^", "^^"); - } - - - var psi = ProcessRunner.ProcessStartInfoFor(progpath, $"\"{CodeEditor.CurrentEditorInstallation}\" {solution} \"{absolutePath}\" {line}"); - psi.StandardOutputEncoding = System.Text.Encoding.Unicode; - psi.StandardErrorEncoding = System.Text.Encoding.Unicode; - - // inter thread communication - var messages = new BlockingCollection(); - - var asyncStart = AsyncOperation.Run( - () => ProcessRunner.StartAndWaitForExit(psi, onOutputReceived: data => OnOutputReceived(data, messages)), - e => new ProcessRunnerResult {Success = false, Error = e.Message, Output = string.Empty}, - () => messages.Add(COMIntegrationState.Exited) - ); - - MonitorCOMIntegration(messages); - - var result = asyncStart.Result; - - if (!result.Success && !string.IsNullOrWhiteSpace(result.Error)) - Debug.LogError($"Error while starting Visual Studio: {result.Error}"); - - return result.Success; - } - - private static void MonitorCOMIntegration(BlockingCollection messages) - { - var displayingProgress = false; - COMIntegrationState state; - - do - { - state = messages.Take(); - switch (state) - { - case COMIntegrationState.ClearProgressBar: - EditorUtility.ClearProgressBar(); - displayingProgress = false; - break; - case COMIntegrationState.DisplayProgressBar: - EditorUtility.DisplayProgressBar("Opening Visual Studio", "Starting up Visual Studio, this might take some time.", .5f); - displayingProgress = true; - break; - } - } while (state != COMIntegrationState.Exited); - - // Make sure the progress bar is properly cleared in case of COMIntegration failure - if (displayingProgress) - EditorUtility.ClearProgressBar(); - } - - private static readonly COMIntegrationState[] ProgressBarCommands = {COMIntegrationState.DisplayProgressBar, COMIntegrationState.ClearProgressBar}; - private static void OnOutputReceived(string data, BlockingCollection messages) - { - if (data == null) - return; - - foreach (var cmd in ProgressBarCommands) - { - if (data.IndexOf(cmd.ToString(), StringComparison.OrdinalIgnoreCase) >= 0) - messages.Add(cmd); - } - } - - [DllImport("AppleEventIntegration")] - static extern bool OpenVisualStudio(string appPath, string solutionPath, string filePath, int line); - - bool OpenOSXApp(string path, int line, int column) - { - string absolutePath = ""; - if (!string.IsNullOrWhiteSpace(path)) - { - absolutePath = Path.GetFullPath(path); - } - - var solution = GetOrGenerateSolutionFile(path); - return OpenVisualStudio(CodeEditor.CurrentEditorInstallation, solution, absolutePath, line); - } - - private string GetOrGenerateSolutionFile(string path) - { - _generator.Sync(); - return _generator.SolutionFile(); + generator.Sync(); + return generator.SolutionFile(); } } } diff --git a/Editor/VisualStudioForMacInstallation.cs b/Editor/VisualStudioForMacInstallation.cs new file mode 100644 index 0000000..5d9fb10 --- /dev/null +++ b/Editor/VisualStudioForMacInstallation.cs @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * 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.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Unity.CodeEditor; +using IOPath = System.IO.Path; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + internal class VisualStudioForMacInstallation : VisualStudioInstallation + { + // C# language version support for Visual Studio for Mac + private static readonly VersionPair[] OSXVersionTable = + { + // VisualStudio for Mac 2022 + new VersionPair(17,4, /* => */ 11,0), + new VersionPair(17,0, /* => */ 10,0), + + // VisualStudio for Mac 8.x + new VersionPair(8,8, /* => */ 9,0), + new VersionPair(8,3, /* => */ 8,0), + new VersionPair(8,0, /* => */ 7,3), + }; + + private static readonly IGenerator _generator = new LegacyStyleProjectGeneration(); + + public override bool SupportsAnalyzers + { + get + { + return Version >= new Version(8, 3); + } + } + + public override Version LatestLanguageVersionSupported + { + get + { + return GetLatestLanguageVersionSupported(OSXVersionTable); + } + } + + private string GetExtensionPath() + { + const string addinName = "MonoDevelop.Unity"; + const string addinAssembly = addinName + ".dll"; + + // user addins repository + var localAddins = IOPath.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Personal), + $"Library/Application Support/VisualStudio/${Version.Major}.0" + "/LocalInstall/Addins"); + + // In the user addins repository, the addins are suffixed by their versions, like `MonoDevelop.Unity.1.0` + // When installing another local user addin, MD will remove files inside the folder + // So we browse all VSTUM addins, and return the one with an addin assembly + if (Directory.Exists(localAddins)) + { + foreach (var folder in Directory.GetDirectories(localAddins, addinName + "*", SearchOption.TopDirectoryOnly)) + { + if (File.Exists(IOPath.Combine(folder, addinAssembly))) + return folder; + } + } + + // Check in Visual Studio.app/ + // In that case the name of the addin is used + var addinPath = IOPath.Combine(Path, $"Contents/Resources/lib/monodevelop/AddIns/{addinName}"); + if (File.Exists(IOPath.Combine(addinPath, addinAssembly))) + return addinPath; + + addinPath = IOPath.Combine(Path, $"Contents/MonoBundle/Addins/{addinName}"); + if (File.Exists(IOPath.Combine(addinPath, addinAssembly))) + return addinPath; + + return null; + } + + public override string[] GetAnalyzers() + { + var vstuPath = GetExtensionPath(); + if (string.IsNullOrEmpty(vstuPath)) + return Array.Empty(); + + return GetAnalyzers(vstuPath); + } + + public override IGenerator ProjectGenerator + { + get + { + return _generator; + } + } + + private static bool IsCandidateForDiscovery(string path) + { + return Directory.Exists(path) && VisualStudioEditor.IsOSX && Regex.IsMatch(path, "Visual\\s?Studio(?!.*Code.*).*.app$", RegexOptions.IgnoreCase); + } + + public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation) + { + installation = null; + + if (string.IsNullOrEmpty(editorPath)) + return false; + + if (!IsCandidateForDiscovery(editorPath)) + return false; + + // On Mac we use the .app folder, so we need to access to main assembly + var fvi = IOPath.Combine(editorPath, "Contents/Resources/lib/monodevelop/bin/VisualStudio.exe"); + + if (!File.Exists(fvi)) + fvi = IOPath.Combine(editorPath, "Contents/MonoBundle/VisualStudio.exe"); + + if (!File.Exists(fvi)) + fvi = IOPath.Combine(editorPath, "Contents/MonoBundle/VisualStudio.dll"); + + if (!File.Exists(fvi)) + return false; + + // VS preview are not using the isPrerelease flag so far + // On Windows FileDescription contains "Preview", but not on Mac + var vi = FileVersionInfo.GetVersionInfo(fvi); + var version = new Version(vi.ProductVersion); + var isPrerelease = vi.IsPreRelease || string.Concat(editorPath, "/" + vi.FileDescription).ToLower().Contains("preview"); + + installation = new VisualStudioForMacInstallation() + { + IsPrerelease = isPrerelease, + Name = $"{vi.FileDescription}{(isPrerelease ? " Preview" : string.Empty)} [{version.ToString(3)}]", + Path = editorPath, + Version = version + }; + return true; + } + + public static IEnumerable GetVisualStudioInstallations() + { + if (!VisualStudioEditor.IsOSX) + yield break; + + var candidates = Directory.EnumerateDirectories("/Applications", "*.app"); + foreach (var candidate in candidates) + { + if (TryDiscoverInstallation(candidate, out var installation)) + yield return installation; + } + } + + [DllImport("AppleEventIntegration")] + private static extern bool OpenVisualStudio(string appPath, string solutionPath, string filePath, int line); + + public override void CreateExtraFiles(string projectDirectory) + { + } + + public override bool Open(string path, int line, int column, string solution) + { + string absolutePath = ""; + if (!string.IsNullOrWhiteSpace(path)) + { + absolutePath = IOPath.GetFullPath(path); + } + + return OpenVisualStudio(CodeEditor.CurrentEditorInstallation, solution, absolutePath, line); + } + + public static void Initialize() + { + } + } +} diff --git a/Editor/VisualStudioForMacInstallation.cs.meta b/Editor/VisualStudioForMacInstallation.cs.meta new file mode 100644 index 0000000..dc0c105 --- /dev/null +++ b/Editor/VisualStudioForMacInstallation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c64241ee5e302b478d7f2522bbaa4e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/VisualStudioForWindowsInstallation.cs b/Editor/VisualStudioForWindowsInstallation.cs new file mode 100644 index 0000000..c193c17 --- /dev/null +++ b/Editor/VisualStudioForWindowsInstallation.cs @@ -0,0 +1,363 @@ +/*--------------------------------------------------------------------------------------------- + * 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Win32; +using Unity.CodeEditor; +using UnityEditor; +using UnityEngine; +using Debug = UnityEngine.Debug; +using IOPath = System.IO.Path; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + internal class VisualStudioForWindowsInstallation : VisualStudioInstallation + { + // C# language version support for Visual Studio + private static readonly VersionPair[] WindowsVersionTable = + { + // VisualStudio 2022 + new VersionPair(17,4, /* => */ 11,0), + new VersionPair(17,0, /* => */ 10,0), + + // VisualStudio 2019 + new VersionPair(16,8, /* => */ 9,0), + new VersionPair(16,0, /* => */ 8,0), + + // VisualStudio 2017 + new VersionPair(15,7, /* => */ 7,3), + new VersionPair(15,5, /* => */ 7,2), + new VersionPair(15,3, /* => */ 7,1), + new VersionPair(15,0, /* => */ 7,0), + }; + + private static string _vsWherePath = null; + private static readonly IGenerator _generator = new LegacyStyleProjectGeneration(); + + public override bool SupportsAnalyzers + { + get + { + return Version >= new Version(16, 3); + } + } + + public override Version LatestLanguageVersionSupported + { + get + { + return GetLatestLanguageVersionSupported(WindowsVersionTable); + } + } + + private static string ReadRegistry(RegistryKey hive, string keyName, string valueName) + { + try + { + var unitykey = hive.OpenSubKey(keyName); + + var result = (string)unitykey?.GetValue(valueName); + return result; + } + catch (Exception) + { + return null; + } + } + + private string GetWindowsBridgeFromRegistry() + { + var keyName = $"Software\\Microsoft\\Microsoft Visual Studio {Version.Major}.0 Tools for Unity"; + const string valueName = "UnityExtensionPath"; + + var bridge = ReadRegistry(Registry.CurrentUser, keyName, valueName); + if (string.IsNullOrEmpty(bridge)) + bridge = ReadRegistry(Registry.LocalMachine, keyName, valueName); + + return bridge; + } + + private string GetExtensionPath() + { + const string extensionName = "Visual Studio Tools for Unity"; + const string extensionAssembly = "SyntaxTree.VisualStudio.Unity.dll"; + + var vsDirectory = IOPath.GetDirectoryName(Path); + var vstuDirectory = IOPath.Combine(vsDirectory, "Extensions", "Microsoft", extensionName); + + if (File.Exists(IOPath.Combine(vstuDirectory, extensionAssembly))) + return vstuDirectory; + + return null; + } + + public override string[] GetAnalyzers() + { + var vstuPath = GetExtensionPath(); + if (string.IsNullOrEmpty(vstuPath)) + return Array.Empty(); + + var analyzers = GetAnalyzers(vstuPath); + if (analyzers?.Length > 0) + return analyzers; + + var bridge = GetWindowsBridgeFromRegistry(); + if (File.Exists(bridge)) + return GetAnalyzers(IOPath.Combine(IOPath.GetDirectoryName(bridge), "..")); + + return Array.Empty(); + } + + public override IGenerator ProjectGenerator + { + get + { + return _generator; + } + } + + private static bool IsCandidateForDiscovery(string path) + { + return File.Exists(path) && VisualStudioEditor.IsWindows && Regex.IsMatch(path, "devenv.exe$", RegexOptions.IgnoreCase); + } + + public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation) + { + installation = null; + + if (string.IsNullOrEmpty(editorPath)) + return false; + + if (!IsCandidateForDiscovery(editorPath)) + return false; + + // On windows we use the executable directly, so we can query extra information + if (!File.Exists(editorPath)) + return false; + + // VS preview are not using the isPrerelease flag so far + // On Windows FileDescription contains "Preview", but not on Mac + var vi = FileVersionInfo.GetVersionInfo(editorPath); + var version = new Version(vi.ProductVersion); + var isPrerelease = vi.IsPreRelease || string.Concat(editorPath, "/" + vi.FileDescription).ToLower().Contains("preview"); + + installation = new VisualStudioForWindowsInstallation() + { + IsPrerelease = isPrerelease, + Name = $"{FormatProductName(vi.FileDescription)} [{version.ToString(3)}]", + Path = editorPath, + Version = version + }; + return true; + } + + public static string FormatProductName(string productName) + { + if (string.IsNullOrEmpty(productName)) + return string.Empty; + + return productName.Replace("Microsoft ", string.Empty); + } + + public static IEnumerable GetVisualStudioInstallations() + { + if (!VisualStudioEditor.IsWindows) + yield break; + + foreach (var installation in QueryVsWhere()) + yield return installation; + } + + #region VsWhere Json Schema +#pragma warning disable CS0649 + [Serializable] + internal class VsWhereResult + { + public VsWhereEntry[] entries; + + public static VsWhereResult FromJson(string json) + { + return JsonUtility.FromJson("{ \"" + nameof(VsWhereResult.entries) + "\": " + json + " }"); + } + + public IEnumerable ToVisualStudioInstallations() + { + foreach (var entry in entries) + { + yield return new VisualStudioForWindowsInstallation + { + Name = $"{FormatProductName(entry.displayName)} [{entry.catalog.productDisplayVersion}]", + Path = entry.productPath, + IsPrerelease = entry.isPrerelease, + Version = Version.Parse(entry.catalog.buildVersion) + }; + } + } + } + + [Serializable] + internal class VsWhereEntry + { + public string displayName; + public bool isPrerelease; + public string productPath; + public VsWhereCatalog catalog; + } + + [Serializable] + internal class VsWhereCatalog + { + public string productDisplayVersion; // non parseable like "16.3.0 Preview 3.0" + public string buildVersion; + } +#pragma warning restore CS3021 + #endregion + + private static IEnumerable QueryVsWhere() + { + var progpath = _vsWherePath; + + if (string.IsNullOrWhiteSpace(progpath)) + return Enumerable.Empty(); + + var result = ProcessRunner.StartAndWaitForExit(progpath, "-prerelease -format json -utf8"); + + if (!result.Success) + throw new Exception($"Failure while running vswhere: {result.Error}"); + + // Do not catch any JsonException here, this will be handled by the caller + return VsWhereResult + .FromJson(result.Output) + .ToVisualStudioInstallations(); + } + + private enum COMIntegrationState + { + Running, + DisplayProgressBar, + ClearProgressBar, + Exited + } + + public override void CreateExtraFiles(string projectDirectory) + { + // 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 + try + { + var vsConfigFile = IOPath.Combine(projectDirectory.NormalizePathSeparators(), ".vsconfig"); + if (File.Exists(vsConfigFile)) + return; + + const string content = @"{ + ""version"": ""1.0"", + ""components"": [ + ""Microsoft.VisualStudio.Workload.ManagedGame"" + ] +} +"; + File.WriteAllText(vsConfigFile, content); + } + catch (IOException) + { + } + } + + public override bool Open(string path, int line, int column, string solution) + { + var progpath = FileUtility.GetPackageAssetFullPath("Editor", "COMIntegration", "Release", "COMIntegration.exe"); + + if (string.IsNullOrWhiteSpace(progpath)) + return false; + + string absolutePath = ""; + if (!string.IsNullOrWhiteSpace(path)) + { + absolutePath = IOPath.GetFullPath(path); + } + + // We remove all invalid chars from the solution filename, but we cannot prevent the user from using a specific path for the Unity project + // So process the fullpath to make it compatible with VS + if (!string.IsNullOrWhiteSpace(solution)) + { + solution = $"\"{solution}\""; + solution = solution.Replace("^", "^^"); + } + + var psi = ProcessRunner.ProcessStartInfoFor(progpath, $"\"{CodeEditor.CurrentEditorInstallation}\" {solution} \"{absolutePath}\" {line}"); + psi.StandardOutputEncoding = System.Text.Encoding.Unicode; + psi.StandardErrorEncoding = System.Text.Encoding.Unicode; + + // inter thread communication + var messages = new BlockingCollection(); + + var asyncStart = AsyncOperation.Run( + () => ProcessRunner.StartAndWaitForExit(psi, onOutputReceived: data => OnOutputReceived(data, messages)), + e => new ProcessRunnerResult {Success = false, Error = e.Message, Output = string.Empty}, + () => messages.Add(COMIntegrationState.Exited) + ); + + MonitorCOMIntegration(messages); + + var result = asyncStart.Result; + + if (!result.Success && !string.IsNullOrWhiteSpace(result.Error)) + Debug.LogError($"Error while starting Visual Studio: {result.Error}"); + + return result.Success; + } + + private static void MonitorCOMIntegration(BlockingCollection messages) + { + var displayingProgress = false; + COMIntegrationState state; + + do + { + state = messages.Take(); + switch (state) + { + case COMIntegrationState.ClearProgressBar: + EditorUtility.ClearProgressBar(); + displayingProgress = false; + break; + case COMIntegrationState.DisplayProgressBar: + EditorUtility.DisplayProgressBar("Opening Visual Studio", "Starting up Visual Studio, this might take some time.", .5f); + displayingProgress = true; + break; + } + } while (state != COMIntegrationState.Exited); + + // Make sure the progress bar is properly cleared in case of COMIntegration failure + if (displayingProgress) + EditorUtility.ClearProgressBar(); + } + + private static readonly COMIntegrationState[] ProgressBarCommands = {COMIntegrationState.DisplayProgressBar, COMIntegrationState.ClearProgressBar}; + private static void OnOutputReceived(string data, BlockingCollection messages) + { + if (data == null) + return; + + foreach (var cmd in ProgressBarCommands) + { + if (data.IndexOf(cmd.ToString(), StringComparison.OrdinalIgnoreCase) >= 0) + messages.Add(cmd); + } + } + + public static void Initialize() + { + if (VisualStudioEditor.IsWindows) + _vsWherePath = FileUtility.GetPackageAssetFullPath("Editor", "VSWhere", "vswhere.exe"); + } + } +} diff --git a/Editor/VisualStudioForWindowsInstallation.cs.meta b/Editor/VisualStudioForWindowsInstallation.cs.meta new file mode 100644 index 0000000..ca91ae2 --- /dev/null +++ b/Editor/VisualStudioForWindowsInstallation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: be7ef402265a7a549b2e43c11d1a22c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/VisualStudioInstallation.cs b/Editor/VisualStudioInstallation.cs index a927dd1..fa14e9b 100644 --- a/Editor/VisualStudioInstallation.cs +++ b/Editor/VisualStudioInstallation.cs @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ using System; using System.IO; -using Microsoft.Win32; using Unity.CodeEditor; using IOPath = System.IO.Path; @@ -17,165 +16,41 @@ namespace Microsoft.Unity.VisualStudio.Editor Version LatestLanguageVersionSupported { get; } string[] GetAnalyzers(); CodeEditor.Installation ToCodeEditorInstallation(); + bool Open(string path, int line, int column, string solutionPath); + IGenerator ProjectGenerator { get; } + void CreateExtraFiles(string projectDirectory); } - internal class VisualStudioInstallation : IVisualStudioInstallation + internal abstract class VisualStudioInstallation : IVisualStudioInstallation { public string Name { get; set; } public string Path { get; set; } public Version Version { get; set; } public bool IsPrerelease { get; set; } - public bool SupportsAnalyzers + public abstract bool SupportsAnalyzers { get; } + public abstract Version LatestLanguageVersionSupported { get; } + public abstract string[] GetAnalyzers(); + public abstract IGenerator ProjectGenerator { get; } + public abstract void CreateExtraFiles(string projectDirectory); + public abstract bool Open(string path, int line, int column, string solutionPath); + + protected Version GetLatestLanguageVersionSupported(VersionPair[] versions) { - get + if (versions != null) { - if (VisualStudioEditor.IsWindows) - return Version >= new Version(16, 3); - - if (VisualStudioEditor.IsOSX) - return Version >= new Version(8, 3); - - return false; - } - } - - // C# language version support for Visual Studio - private static VersionPair[] WindowsVersionTable = - { - // VisualStudio 2022 - new VersionPair(17,4, /* => */ 11,0), - new VersionPair(17,0, /* => */ 10,0), - - // VisualStudio 2019 - new VersionPair(16,8, /* => */ 9,0), - new VersionPair(16,0, /* => */ 8,0), - - // VisualStudio 2017 - new VersionPair(15,7, /* => */ 7,3), - new VersionPair(15,5, /* => */ 7,2), - new VersionPair(15,3, /* => */ 7,1), - new VersionPair(15,0, /* => */ 7,0), - }; - - // C# language version support for Visual Studio for Mac - private static VersionPair[] OSXVersionTable = - { - // VisualStudio for Mac 2022 - new VersionPair(17,4, /* => */ 11,0), - new VersionPair(17,0, /* => */ 10,0), - - // VisualStudio for Mac 8.x - new VersionPair(8,8, /* => */ 9,0), - new VersionPair(8,3, /* => */ 8,0), - new VersionPair(8,0, /* => */ 7,3), - }; - - public Version LatestLanguageVersionSupported - { - get - { - VersionPair[] versions = null; - - if (VisualStudioEditor.IsWindows) - versions = WindowsVersionTable; - - if (VisualStudioEditor.IsOSX) - versions = OSXVersionTable; - - if (versions != null) + foreach (var entry in versions) { - foreach (var entry in versions) - { - if (Version >= entry.IdeVersion) - return entry.LanguageVersion; - } + if (Version >= entry.IdeVersion) + return entry.LanguageVersion; } - - // default to 7.0 given we support at least VS 2017 - return new Version(7, 0); } + + // default to 7.0 + return new Version(7, 0); } - private static string ReadRegistry(RegistryKey hive, string keyName, string valueName) - { - try - { - var unitykey = hive.OpenSubKey(keyName); - - var result = (string)unitykey?.GetValue(valueName); - return result; - } - catch (Exception) - { - return null; - } - } - - private string GetWindowsBridgeFromRegistry() - { - var keyName = $"Software\\Microsoft\\Microsoft Visual Studio {Version.Major}.0 Tools for Unity"; - const string valueName = "UnityExtensionPath"; - - var bridge = ReadRegistry(Registry.CurrentUser, keyName, valueName); - if (string.IsNullOrEmpty(bridge)) - bridge = ReadRegistry(Registry.LocalMachine, keyName, valueName); - - return bridge; - } - - // We only use this to find analyzers, we do not need to load this assembly anymore - private string GetExtensionPath() - { - if (VisualStudioEditor.IsWindows) - { - const string extensionName = "Visual Studio Tools for Unity"; - const string extensionAssembly = "SyntaxTree.VisualStudio.Unity.dll"; - - var vsDirectory = IOPath.GetDirectoryName(Path); - var vstuDirectory = IOPath.Combine(vsDirectory, "Extensions", "Microsoft", extensionName); - - if (File.Exists(IOPath.Combine(vstuDirectory, extensionAssembly))) - return vstuDirectory; - } - - if (VisualStudioEditor.IsOSX) - { - const string addinName = "MonoDevelop.Unity"; - const string addinAssembly = addinName + ".dll"; - - // user addins repository - var localAddins = IOPath.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Personal), - $"Library/Application Support/VisualStudio/${Version.Major}.0" + "/LocalInstall/Addins"); - - // In the user addins repository, the addins are suffixed by their versions, like `MonoDevelop.Unity.1.0` - // When installing another local user addin, MD will remove files inside the folder - // So we browse all VSTUM addins, and return the one with an addin assembly - if (Directory.Exists(localAddins)) - { - foreach (var folder in Directory.GetDirectories(localAddins, addinName + "*", SearchOption.TopDirectoryOnly)) - { - if (File.Exists(IOPath.Combine(folder, addinAssembly))) - return folder; - } - } - - // Check in Visual Studio.app/ - // In that case the name of the addin is used - var addinPath = IOPath.Combine(Path, $"Contents/Resources/lib/monodevelop/AddIns/{addinName}"); - if (File.Exists(IOPath.Combine(addinPath, addinAssembly))) - return addinPath; - - addinPath = IOPath.Combine(Path, $"Contents/MonoBundle/Addins/{addinName}"); - if (File.Exists(IOPath.Combine(addinPath, addinAssembly))) - return addinPath; - } - - return null; - } - - private static string[] GetAnalyzers(string path) + protected static string[] GetAnalyzers(string path) { var analyzersDirectory = IOPath.GetFullPath(IOPath.Combine(path, "Analyzers")); @@ -185,31 +60,6 @@ namespace Microsoft.Unity.VisualStudio.Editor return Array.Empty(); } - public string[] GetAnalyzers() - { - var vstuPath = GetExtensionPath(); - if (string.IsNullOrEmpty(vstuPath)) - return Array.Empty(); - - if (VisualStudioEditor.IsOSX) - return GetAnalyzers(vstuPath); - - if (VisualStudioEditor.IsWindows) - { - var analyzers = GetAnalyzers(vstuPath); - if (analyzers?.Length > 0) - return analyzers; - - var bridge = GetWindowsBridgeFromRegistry(); - if (File.Exists(bridge)) - return GetAnalyzers(IOPath.Combine(IOPath.GetDirectoryName(bridge), "..")); - } - - // Local assets - // return FileUtility.FindPackageAssetFullPath("Analyzers a:packages", ".Analyzers.dll"); - return Array.Empty(); - } - public CodeEditor.Installation ToCodeEditorInstallation() { return new CodeEditor.Installation() { Name = Name, Path = Path }; diff --git a/ValidationConfig.json b/ValidationConfig.json index 6a638b8..caf9c3e 100644 --- a/ValidationConfig.json +++ b/ValidationConfig.json @@ -7,7 +7,7 @@ "Targets": "+", "Files": [ - "Editor/Discovery.cs" + "Editor/VisualStudioForWindowsInstallation.cs" ], "Patterns": [ diff --git a/ValidationExceptions.json b/ValidationExceptions.json index 4db0381..69e4b4b 100644 --- a/ValidationExceptions.json +++ b/ValidationExceptions.json @@ -2,8 +2,13 @@ "ErrorExceptions": [ { "ValidationTest": "API Validation", - "ExceptionMessage": "Failed comparing against assemblies of previously promoted version of package. \nThis is most likely because the assemblies that were compared against were built with a different version of Unity. \nIf you are certain that there are no API changes warranting bumping the package version then you can add an exception for this error:\nRead more about this error and potential solutions at https://docs.unity3d.com/Packages/com.unity.package-validation-suite@latest/index.html?preview=1&subfolder=/manual/validation_exceptions.html#", - "PackageVersion": "2.0.11" + "ExceptionMessage": "Breaking changes require a new major version.", + "PackageVersion": "2.0.18" + }, + { + "ValidationTest": "API Validation", + "ExceptionMessage": "Additions require a new minor or major version.", + "PackageVersion": "2.0.18" } ], "WarningExceptions": [] diff --git a/ValidationExceptions.json.meta b/ValidationExceptions.json.meta index b0b2427..ae19e91 100644 --- a/ValidationExceptions.json.meta +++ b/ValidationExceptions.json.meta @@ -4,4 +4,4 @@ TextScriptImporter: externalObjects: {} userData: assetBundleName: - assetBundleVariant: + assetBundleVariant: \ No newline at end of file diff --git a/package.json b/package.json index d460d81..b6a3971 100644 --- a/package.json +++ b/package.json @@ -2,25 +2,25 @@ "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.18", + "version": "2.0.20", "unity": "2019.4", "unityRelease": "25f1", "dependencies": { "com.unity.test-framework": "1.1.9" }, "relatedPackages": { - "com.unity.ide.visualstudio.tests": "2.0.18" + "com.unity.ide.visualstudio.tests": "2.0.20" }, "_upm": { - "changelog": "Integration:\n\n- Performance improvements with `EditorApplication.update` callbacks.\n \nProject generation:\n\n- Add extra compiler options for analyzers and source generators." + "changelog": "Integration:\n\n- Internal API refactoring." }, "upmCi": { - "footprint": "1d7ac8985c088423201e27b93ccdc6292ff941c9" + "footprint": "7d769a8558c7768417b16fc2ac8477cf69234049" }, "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.ide.visualstudio@2.0/manual/index.html", "repository": { "url": "https://github.cds.internal.unity3d.com/unity/com.unity.ide.visualstudio.git", "type": "git", - "revision": "9d3c07127cbe1916b8abbfd18f71fb8d9df8008c" + "revision": "b7bf23d23806ac75645bfa12acadcfc11468a383" } }