A game about forced loneliness, made by TACStudios
1/*--------------------------------------------------------------------------------------------- 2 * Copyright (c) Unity Technologies. 3 * Copyright (c) Microsoft Corporation. All rights reserved. 4 * Licensed under the MIT License. See License.txt in the project root for license information. 5 *--------------------------------------------------------------------------------------------*/ 6using System; 7using System.Collections.Generic; 8using System.IO; 9using System.Linq; 10using SR = System.Reflection; 11using System.Security; 12using System.Security.Cryptography; 13using System.Text; 14using System.Text.RegularExpressions; 15using Unity.CodeEditor; 16using Unity.Profiling; 17using UnityEditor; 18using UnityEditor.Compilation; 19using UnityEngine; 20 21namespace Microsoft.Unity.VisualStudio.Editor 22{ 23 public enum ScriptingLanguage 24 { 25 None, 26 CSharp 27 } 28 29 public interface IGenerator 30 { 31 bool SyncIfNeeded(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles); 32 void Sync(); 33 bool HasSolutionBeenGenerated(); 34 bool IsSupportedFile(string path); 35 string SolutionFile(); 36 string ProjectDirectory { get; } 37 IAssemblyNameProvider AssemblyNameProvider { get; } 38 } 39 40 public class ProjectGeneration : IGenerator 41 { 42 // do not remove because of the Validation API, used in LegacyStyleProjectGeneration 43 public static readonly string MSBuildNamespaceUri = "http://schemas.microsoft.com/developer/msbuild/2003"; 44 45 public IAssemblyNameProvider AssemblyNameProvider => m_AssemblyNameProvider; 46 public string ProjectDirectory { get; } 47 48 // Use this to have the same newline ending on all platforms for consistency. 49 internal const string k_WindowsNewline = "\r\n"; 50 51 const string m_SolutionProjectEntryTemplate = @"Project(""{{{0}}}"") = ""{1}"", ""{2}"", ""{{{3}}}""{4}EndProject"; 52 53 readonly string m_SolutionProjectConfigurationTemplate = string.Join(k_WindowsNewline, 54 @" {{{0}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU", 55 @" {{{0}}}.Debug|Any CPU.Build.0 = Debug|Any CPU", 56 @" {{{0}}}.Release|Any CPU.ActiveCfg = Release|Any CPU", 57 @" {{{0}}}.Release|Any CPU.Build.0 = Release|Any CPU").Replace(" ", "\t"); 58 59 static readonly string[] k_ReimportSyncExtensions = { ".dll", ".asmdef" }; 60 61 HashSet<string> m_ProjectSupportedExtensions = new HashSet<string>(); 62 HashSet<string> m_BuiltinSupportedExtensions = new HashSet<string>(); 63 64 readonly string m_ProjectName; 65 internal readonly IAssemblyNameProvider m_AssemblyNameProvider; 66 readonly IFileIO m_FileIOProvider; 67 readonly IGUIDGenerator m_GUIDGenerator; 68 bool m_ShouldGenerateAll; 69 IVisualStudioInstallation m_CurrentInstallation; 70 71 public ProjectGeneration() : this(Directory.GetParent(Application.dataPath).FullName) 72 { 73 } 74 75 public ProjectGeneration(string tempDirectory) : this(tempDirectory, new AssemblyNameProvider(), new FileIOProvider(), new GUIDProvider()) 76 { 77 } 78 79 public ProjectGeneration(string tempDirectory, IAssemblyNameProvider assemblyNameProvider, IFileIO fileIoProvider, IGUIDGenerator guidGenerator) 80 { 81 ProjectDirectory = FileUtility.NormalizeWindowsToUnix(tempDirectory); 82 m_ProjectName = Path.GetFileName(ProjectDirectory); 83 m_AssemblyNameProvider = assemblyNameProvider; 84 m_FileIOProvider = fileIoProvider; 85 m_GUIDGenerator = guidGenerator; 86 87 SetupProjectSupportedExtensions(); 88 } 89 90 internal virtual string StyleName => ""; 91 92 /// <summary> 93 /// Syncs the scripting solution if any affected files are relevant. 94 /// </summary> 95 /// <returns> 96 /// Whether the solution was synced. 97 /// </returns> 98 /// <param name='affectedFiles'> 99 /// A set of files whose status has changed 100 /// </param> 101 /// <param name="reimportedFiles"> 102 /// A set of files that got reimported 103 /// </param> 104 public bool SyncIfNeeded(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles) 105 { 106 using (solutionSyncMarker.Auto()) 107 { 108 // We need the exact VS version/capabilities to tweak project generation (analyzers/langversion) 109 RefreshCurrentInstallation(); 110 111 SetupProjectSupportedExtensions(); 112 113 CreateExtraFiles(m_CurrentInstallation); 114 115 // Don't sync if we haven't synced before 116 var affected = affectedFiles as ICollection<string> ?? affectedFiles.ToArray(); 117 var reimported = reimportedFiles as ICollection<string> ?? reimportedFiles.ToArray(); 118 if (!HasFilesBeenModified(affected, reimported)) 119 { 120 return false; 121 } 122 123 var assemblies = m_AssemblyNameProvider.GetAssemblies(ShouldFileBePartOfSolution); 124 var allProjectAssemblies = RelevantAssembliesForMode(assemblies).ToList(); 125 SyncSolution(allProjectAssemblies); 126 127 var allAssetProjectParts = GenerateAllAssetProjectParts(); 128 129 var affectedNames = affected 130 .Select(asset => m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset)) 131 .Where(name => !string.IsNullOrWhiteSpace(name)).Select(name => 132 name.Split(new[] { ".dll" }, StringSplitOptions.RemoveEmptyEntries)[0]); 133 var reimportedNames = reimported 134 .Select(asset => m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset)) 135 .Where(name => !string.IsNullOrWhiteSpace(name)).Select(name => 136 name.Split(new[] { ".dll" }, StringSplitOptions.RemoveEmptyEntries)[0]); 137 var affectedAndReimported = new HashSet<string>(affectedNames.Concat(reimportedNames)); 138 139 foreach (var assembly in allProjectAssemblies) 140 { 141 if (!affectedAndReimported.Contains(assembly.name)) 142 continue; 143 144 SyncProject(assembly, 145 allAssetProjectParts, 146 responseFilesData: ParseResponseFileData(assembly).ToArray()); 147 } 148 149 return true; 150 } 151 } 152 153 private void CreateExtraFiles(IVisualStudioInstallation installation) 154 { 155 installation?.CreateExtraFiles(ProjectDirectory); 156 } 157 158 private bool HasFilesBeenModified(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles) 159 { 160 return affectedFiles.Any(ShouldFileBePartOfSolution) || reimportedFiles.Any(ShouldSyncOnReimportedAsset); 161 } 162 163 private static bool ShouldSyncOnReimportedAsset(string asset) 164 { 165 return k_ReimportSyncExtensions.Contains(new FileInfo(asset).Extension); 166 } 167 168 private void RefreshCurrentInstallation() 169 { 170 var editor = CodeEditor.CurrentEditor as VisualStudioEditor; 171 editor?.TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, lookupDiscoveredInstallations: true, out m_CurrentInstallation); 172 } 173 174 static ProfilerMarker solutionSyncMarker = new ProfilerMarker("SolutionSynchronizerSync"); 175 176 public void Sync() 177 { 178 // We need the exact VS version/capabilities to tweak project generation (analyzers/langversion) 179 RefreshCurrentInstallation(); 180 181 SetupProjectSupportedExtensions(); 182 183 (m_AssemblyNameProvider as AssemblyNameProvider)?.ResetPackageInfoCache(); 184 185 // See https://devblogs.microsoft.com/setup/configure-visual-studio-across-your-organization-with-vsconfig/ 186 // We create a .vsconfig file to make sure our ManagedGame workload is installed 187 CreateExtraFiles(m_CurrentInstallation); 188 189 var externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles(); 190 191 if (!externalCodeAlreadyGeneratedProjects) 192 { 193 GenerateAndWriteSolutionAndProjects(); 194 } 195 196 OnGeneratedCSProjectFiles(); 197 } 198 199 public bool HasSolutionBeenGenerated() 200 { 201 return m_FileIOProvider.Exists(SolutionFile()); 202 } 203 204 private void SetupProjectSupportedExtensions() 205 { 206 m_ProjectSupportedExtensions = new HashSet<string>(m_AssemblyNameProvider.ProjectSupportedExtensions); 207 m_BuiltinSupportedExtensions = new HashSet<string>(EditorSettings.projectGenerationBuiltinExtensions); 208 } 209 210 private bool ShouldFileBePartOfSolution(string file) 211 { 212 // Exclude files coming from packages except if they are internalized. 213 if (m_AssemblyNameProvider.IsInternalizedPackagePath(file)) 214 { 215 return false; 216 } 217 218 return IsSupportedFile(file); 219 } 220 221 private static string GetExtensionWithoutDot(string path) 222 { 223 // Prevent re-processing and information loss 224 if (!Path.HasExtension(path)) 225 return path; 226 227 return Path 228 .GetExtension(path) 229 .TrimStart('.') 230 .ToLower(); 231 } 232 233 public bool IsSupportedFile(string path) 234 { 235 return IsSupportedFile(path, out _); 236 } 237 238 private bool IsSupportedFile(string path, out string extensionWithoutDot) 239 { 240 extensionWithoutDot = GetExtensionWithoutDot(path); 241 242 // Dll's are not scripts but still need to be included 243 if (extensionWithoutDot == "dll") 244 return true; 245 246 if (extensionWithoutDot == "asmdef") 247 return true; 248 249 if (m_BuiltinSupportedExtensions.Contains(extensionWithoutDot)) 250 return true; 251 252 if (m_ProjectSupportedExtensions.Contains(extensionWithoutDot)) 253 return true; 254 255 return false; 256 } 257 258 259 private static ScriptingLanguage ScriptingLanguageFor(Assembly assembly) 260 { 261 var files = assembly.sourceFiles; 262 263 if (files.Length == 0) 264 return ScriptingLanguage.None; 265 266 return ScriptingLanguageForFile(files[0]); 267 } 268 269 internal static ScriptingLanguage ScriptingLanguageForExtension(string extensionWithoutDot) 270 { 271 return extensionWithoutDot == "cs" ? ScriptingLanguage.CSharp : ScriptingLanguage.None; 272 } 273 274 internal static ScriptingLanguage ScriptingLanguageForFile(string path) 275 { 276 return ScriptingLanguageForExtension(GetExtensionWithoutDot(path)); 277 } 278 279 public void GenerateAndWriteSolutionAndProjects() 280 { 281 // Only synchronize assemblies that have associated source files and ones that we actually want in the project. 282 // This also filters out DLLs coming from .asmdef files in packages. 283 var assemblies = m_AssemblyNameProvider.GetAssemblies(ShouldFileBePartOfSolution).ToList(); 284 285 var allAssetProjectParts = GenerateAllAssetProjectParts(); 286 287 SyncSolution(assemblies); 288 289 var allProjectAssemblies = RelevantAssembliesForMode(assemblies); 290 291 foreach (var assembly in allProjectAssemblies) 292 { 293 SyncProject(assembly, 294 allAssetProjectParts, 295 responseFilesData: ParseResponseFileData(assembly).ToArray()); 296 } 297 } 298 299 private IEnumerable<ResponseFileData> ParseResponseFileData(Assembly assembly) 300 { 301 var systemReferenceDirectories = CompilationPipeline.GetSystemAssemblyDirectories(assembly.compilerOptions.ApiCompatibilityLevel); 302 303 Dictionary<string, ResponseFileData> responseFilesData = assembly.compilerOptions.ResponseFiles.ToDictionary(x => x, x => m_AssemblyNameProvider.ParseResponseFile( 304 x, 305 ProjectDirectory, 306 systemReferenceDirectories 307 )); 308 309 Dictionary<string, ResponseFileData> responseFilesWithErrors = responseFilesData.Where(x => x.Value.Errors.Any()) 310 .ToDictionary(x => x.Key, x => x.Value); 311 312 if (responseFilesWithErrors.Any()) 313 { 314 foreach (var error in responseFilesWithErrors) 315 foreach (var valueError in error.Value.Errors) 316 { 317 Debug.LogError($"{error.Key} Parse Error : {valueError}"); 318 } 319 } 320 321 return responseFilesData.Select(x => x.Value); 322 } 323 324 private Dictionary<string, string> GenerateAllAssetProjectParts() 325 { 326 Dictionary<string, StringBuilder> stringBuilders = new Dictionary<string, StringBuilder>(); 327 328 foreach (string asset in m_AssemblyNameProvider.GetAllAssetPaths()) 329 { 330 // Exclude files coming from packages except if they are internalized. 331 if (m_AssemblyNameProvider.IsInternalizedPackagePath(asset)) 332 { 333 continue; 334 } 335 336 if (IsSupportedFile(asset, out var extensionWithoutDot) && ScriptingLanguage.None == ScriptingLanguageForExtension(extensionWithoutDot)) 337 { 338 // Find assembly the asset belongs to by adding script extension and using compilation pipeline. 339 var assemblyName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset); 340 341 if (string.IsNullOrEmpty(assemblyName)) 342 { 343 continue; 344 } 345 346 assemblyName = Path.GetFileNameWithoutExtension(assemblyName); 347 348 if (!stringBuilders.TryGetValue(assemblyName, out var projectBuilder)) 349 { 350 projectBuilder = new StringBuilder(); 351 stringBuilders[assemblyName] = projectBuilder; 352 } 353 354 IncludeAsset(projectBuilder, IncludeAssetTag.None, asset); 355 } 356 } 357 358 var result = new Dictionary<string, string>(); 359 360 foreach (var entry in stringBuilders) 361 result[entry.Key] = entry.Value.ToString(); 362 363 return result; 364 } 365 366 internal enum IncludeAssetTag 367 { 368 Compile, 369 None 370 } 371 372 internal virtual void IncludeAsset(StringBuilder builder, IncludeAssetTag tag, string asset) 373 { 374 var filename = EscapedRelativePathFor(asset, out var packageInfo); 375 376 builder.Append(" <").Append(tag).Append(@" Include=""").Append(filename); 377 if (Path.IsPathRooted(filename) && packageInfo != null) 378 { 379 // We are outside the Unity project and using a package context 380 var linkPath = SkipPathPrefix(asset.NormalizePathSeparators(), packageInfo.assetPath.NormalizePathSeparators()); 381 382 builder.Append(@""">").Append(k_WindowsNewline); 383 builder.Append(" <Link>").Append(linkPath).Append("</Link>").Append(k_WindowsNewline); 384 builder.Append($" </{tag}>").Append(k_WindowsNewline); 385 } 386 else 387 { 388 builder.Append(@""" />").Append(k_WindowsNewline); 389 } 390 } 391 392 private void SyncProject( 393 Assembly assembly, 394 Dictionary<string, string> allAssetsProjectParts, 395 ResponseFileData[] responseFilesData) 396 { 397 SyncProjectFileIfNotChanged( 398 ProjectFile(assembly), 399 ProjectText(assembly, allAssetsProjectParts, responseFilesData)); 400 } 401 402 private void SyncProjectFileIfNotChanged(string path, string newContents) 403 { 404 if (Path.GetExtension(path) == ".csproj") 405 { 406 newContents = OnGeneratedCSProject(path, newContents); 407 } 408 409 SyncFileIfNotChanged(path, newContents); 410 } 411 412 private void SyncSolutionFileIfNotChanged(string path, string newContents) 413 { 414 newContents = OnGeneratedSlnSolution(path, newContents); 415 416 SyncFileIfNotChanged(path, newContents); 417 } 418 419 private static IEnumerable<SR.MethodInfo> GetPostProcessorCallbacks(string name) 420 { 421 return TypeCache 422 .GetTypesDerivedFrom<AssetPostprocessor>() 423 .Where(t => t.Assembly.GetName().Name != KnownAssemblies.Bridge) // never call into the bridge if loaded with the package 424 .Select(t => t.GetMethod(name, SR.BindingFlags.Public | SR.BindingFlags.NonPublic | SR.BindingFlags.Static)) 425 .Where(m => m != null); 426 } 427 428 static void OnGeneratedCSProjectFiles() 429 { 430 foreach (var method in GetPostProcessorCallbacks(nameof(OnGeneratedCSProjectFiles))) 431 { 432 method.Invoke(null, Array.Empty<object>()); 433 } 434 } 435 436 private static bool OnPreGeneratingCSProjectFiles() 437 { 438 bool result = false; 439 440 foreach (var method in GetPostProcessorCallbacks(nameof(OnPreGeneratingCSProjectFiles))) 441 { 442 var retValue = method.Invoke(null, Array.Empty<object>()); 443 if (method.ReturnType == typeof(bool)) 444 { 445 result |= (bool)retValue; 446 } 447 } 448 449 return result; 450 } 451 452 private static string InvokeAssetPostProcessorGenerationCallbacks(string name, string path, string content) 453 { 454 foreach (var method in GetPostProcessorCallbacks(name)) 455 { 456 var args = new[] { path, content }; 457 var returnValue = method.Invoke(null, args); 458 if (method.ReturnType == typeof(string)) 459 { 460 // We want to chain content update between invocations 461 content = (string)returnValue; 462 } 463 } 464 465 return content; 466 } 467 468 private static string OnGeneratedCSProject(string path, string content) 469 { 470 return InvokeAssetPostProcessorGenerationCallbacks(nameof(OnGeneratedCSProject), path, content); 471 } 472 473 private static string OnGeneratedSlnSolution(string path, string content) 474 { 475 return InvokeAssetPostProcessorGenerationCallbacks(nameof(OnGeneratedSlnSolution), path, content); 476 } 477 478 private void SyncFileIfNotChanged(string filename, string newContents) 479 { 480 try 481 { 482 if (m_FileIOProvider.Exists(filename) && newContents == m_FileIOProvider.ReadAllText(filename)) 483 { 484 return; 485 } 486 } 487 catch (Exception exception) 488 { 489 Debug.LogException(exception); 490 } 491 492 m_FileIOProvider.WriteAllText(filename, newContents); 493 } 494 495 private string ProjectText(Assembly assembly, 496 Dictionary<string, string> allAssetsProjectParts, 497 ResponseFileData[] responseFilesData) 498 { 499 ProjectHeader(assembly, responseFilesData, out StringBuilder projectBuilder); 500 501 var references = new List<string>(); 502 503 projectBuilder.Append(@" <ItemGroup>").Append(k_WindowsNewline); 504 foreach (string file in assembly.sourceFiles) 505 { 506 if (!IsSupportedFile(file, out var extensionWithoutDot)) 507 continue; 508 509 if ("dll" != extensionWithoutDot) 510 { 511 IncludeAsset(projectBuilder, IncludeAssetTag.Compile, file); 512 } 513 else 514 { 515 var fullFile = EscapedRelativePathFor(file, out _); 516 references.Add(fullFile); 517 } 518 } 519 projectBuilder.Append(@" </ItemGroup>").Append(k_WindowsNewline); 520 521 // Append additional non-script files that should be included in project generation. 522 if (allAssetsProjectParts.TryGetValue(assembly.name, out var additionalAssetsForProject)) 523 { 524 projectBuilder.Append(@" <ItemGroup>").Append(k_WindowsNewline); 525 526 projectBuilder.Append(additionalAssetsForProject); 527 528 projectBuilder.Append(@" </ItemGroup>").Append(k_WindowsNewline); 529 530 } 531 532 projectBuilder.Append(@" <ItemGroup>").Append(k_WindowsNewline); 533 534 var responseRefs = responseFilesData.SelectMany(x => x.FullPathReferences.Select(r => r)); 535 var internalAssemblyReferences = assembly.assemblyReferences 536 .Where(i => !i.sourceFiles.Any(ShouldFileBePartOfSolution)).Select(i => i.outputPath); 537 var allReferences = 538 assembly.compiledAssemblyReferences 539 .Union(responseRefs) 540 .Union(references) 541 .Union(internalAssemblyReferences); 542 543 foreach (var reference in allReferences) 544 { 545 string fullReference = Path.IsPathRooted(reference) ? reference : Path.Combine(ProjectDirectory, reference); 546 AppendReference(fullReference, projectBuilder); 547 } 548 549 projectBuilder.Append(@" </ItemGroup>").Append(k_WindowsNewline); 550 551 if (0 < assembly.assemblyReferences.Length) 552 { 553 projectBuilder.Append(" <ItemGroup>").Append(k_WindowsNewline); 554 foreach (var reference in assembly.assemblyReferences.Where(i => i.sourceFiles.Any(ShouldFileBePartOfSolution))) 555 { 556 AppendProjectReference(assembly, reference, projectBuilder); 557 } 558 559 projectBuilder.Append(@" </ItemGroup>").Append(k_WindowsNewline); 560 } 561 562 GetProjectFooter(projectBuilder); 563 return projectBuilder.ToString(); 564 } 565 566 private static string XmlFilename(string path) 567 { 568 if (string.IsNullOrEmpty(path)) 569 return path; 570 571 path = path.Replace(@"%", "%25"); 572 path = path.Replace(@";", "%3b"); 573 574 return XmlEscape(path); 575 } 576 577 private static string XmlEscape(string s) 578 { 579 return SecurityElement.Escape(s); 580 } 581 582 internal virtual void AppendProjectReference(Assembly assembly, Assembly reference, StringBuilder projectBuilder) 583 { 584 } 585 586 private void AppendReference(string fullReference, StringBuilder projectBuilder) 587 { 588 var escapedFullPath = EscapedRelativePathFor(fullReference, out _); 589 projectBuilder.Append(@" <Reference Include=""").Append(Path.GetFileNameWithoutExtension(escapedFullPath)).Append(@""">").Append(k_WindowsNewline); 590 projectBuilder.Append(" <HintPath>").Append(escapedFullPath).Append("</HintPath>").Append(k_WindowsNewline); 591 projectBuilder.Append(" <Private>False</Private>").Append(k_WindowsNewline); 592 projectBuilder.Append(" </Reference>").Append(k_WindowsNewline); 593 } 594 595 public string ProjectFile(Assembly assembly) 596 { 597 return Path.Combine(ProjectDirectory, $"{m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, assembly.name)}.csproj"); 598 } 599 600#if UNITY_EDITOR_WIN 601 private static readonly Regex InvalidCharactersRegexPattern = new Regex(@"\?|&|\*|""|<|>|\||#|%|\^|;", RegexOptions.Compiled); 602#else 603 private static readonly Regex InvalidCharactersRegexPattern = new Regex(@"\?|&|\*|""|<|>|\||#|%|\^|;|:", RegexOptions.Compiled); 604#endif 605 606 public string SolutionFile() 607 { 608 return Path.Combine(ProjectDirectory.NormalizePathSeparators(), $"{InvalidCharactersRegexPattern.Replace(m_ProjectName, "_")}.sln"); 609 } 610 611 internal string GetLangVersion(Assembly assembly) 612 { 613 var targetLanguageVersion = "latest"; // danger: latest is not the same absolute value depending on the VS version. 614 if (m_CurrentInstallation != null) 615 { 616 var vsLanguageSupport = m_CurrentInstallation.LatestLanguageVersionSupported; 617 var unityLanguageSupport = UnityInstallation.LatestLanguageVersionSupported(assembly); 618 619 // Use the minimal supported version between VS and Unity, so that compilation will work in both 620 targetLanguageVersion = (vsLanguageSupport <= unityLanguageSupport ? vsLanguageSupport : unityLanguageSupport).ToString(2); // (major, minor) only 621 } 622 623 return targetLanguageVersion; 624 } 625 626 private static IEnumerable<string> GetOtherArguments(ResponseFileData[] responseFilesData, HashSet<string> names) 627 { 628 var lines = responseFilesData 629 .SelectMany(x => x.OtherArguments) 630 .Where(l => !string.IsNullOrEmpty(l)) 631 .Select(l => l.Trim()) 632 .Where(l => l.StartsWith("/") || l.StartsWith("-")); 633 634 foreach (var argument in lines) 635 { 636 var index = argument.IndexOf(":", StringComparison.Ordinal); 637 if (index == -1) 638 continue; 639 640 var key = argument 641 .Substring(1, index - 1) 642 .Trim(); 643 644 if (!names.Contains(key)) 645 continue; 646 647 if (argument.Length <= index) 648 continue; 649 650 yield return argument 651 .Substring(index + 1) 652 .Trim(); 653 } 654 } 655 656 private void SetAnalyzerAndSourceGeneratorProperties(Assembly assembly, ResponseFileData[] responseFilesData, ProjectProperties properties) 657 { 658 if (m_CurrentInstallation == null || !m_CurrentInstallation.SupportsAnalyzers) 659 return; 660 661 // Analyzers provided by VisualStudio 662 var analyzers = new List<string>(m_CurrentInstallation.GetAnalyzers()); 663 var additionalFilePaths = new List<string>(); 664 var rulesetPath = string.Empty; 665 var analyzerConfigPath = string.Empty; 666 var compilerOptions = assembly.compilerOptions; 667 668#if UNITY_2020_2_OR_NEWER 669 // Analyzers + ruleset provided by Unity 670 analyzers.AddRange(compilerOptions.RoslynAnalyzerDllPaths); 671 rulesetPath = compilerOptions.RoslynAnalyzerRulesetPath; 672#endif 673 674 // We have support in 2021.3, 2022.2 but without a backport in 2022.1 675#if UNITY_2021_3 676 // Unfortunately those properties were introduced in a patch release of 2021.3, so not found in 2021.3.2f1 for example 677 var scoType = compilerOptions.GetType(); 678 var afpProperty = scoType.GetProperty("RoslynAdditionalFilePaths"); 679 var acpProperty = scoType.GetProperty("AnalyzerConfigPath"); 680 additionalFilePaths.AddRange(afpProperty?.GetValue(compilerOptions) as string[] ?? Array.Empty<string>()); 681 analyzerConfigPath = acpProperty?.GetValue(compilerOptions) as string ?? analyzerConfigPath; 682#elif UNITY_2022_2_OR_NEWER 683 additionalFilePaths.AddRange(compilerOptions.RoslynAdditionalFilePaths); 684 analyzerConfigPath = compilerOptions.AnalyzerConfigPath; 685#endif 686 687 // Analyzers and additional files provided by csc.rsp 688 analyzers.AddRange(GetOtherArguments(responseFilesData, new HashSet<string>(new[] { "analyzer", "a" }))); 689 additionalFilePaths.AddRange(GetOtherArguments(responseFilesData, new HashSet<string>(new[] { "additionalfile" }))); 690 691 properties.RulesetPath = ToNormalizedPath(rulesetPath); 692 properties.Analyzers = ToNormalizedPaths(analyzers); 693 properties.AnalyzerConfigPath = ToNormalizedPath(analyzerConfigPath); 694 properties.AdditionalFilePaths = ToNormalizedPaths(additionalFilePaths); 695 } 696 697 private string ToNormalizedPath(string path) 698 { 699 return path 700 .MakeAbsolutePath() 701 .NormalizePathSeparators(); 702 } 703 704 private string[] ToNormalizedPaths(IEnumerable<string> values) 705 { 706 return values 707 .Where(a => !string.IsNullOrEmpty(a)) 708 .Select(a => ToNormalizedPath(a)) 709 .Distinct() 710 .ToArray(); 711 } 712 713 private void ProjectHeader( 714 Assembly assembly, 715 ResponseFileData[] responseFilesData, 716 out StringBuilder headerBuilder 717 ) 718 { 719 var projectType = ProjectTypeOf(assembly.name); 720 721 var projectProperties = new ProjectProperties 722 { 723 ProjectGuid = ProjectGuid(assembly), 724 LangVersion = GetLangVersion(assembly), 725 AssemblyName = assembly.name, 726 RootNamespace = GetRootNamespace(assembly), 727 OutputPath = assembly.outputPath, 728 // RSP alterable 729 Defines = assembly.defines.Concat(responseFilesData.SelectMany(x => x.Defines)).Distinct().ToArray(), 730 Unsafe = assembly.compilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe), 731 // VSTU Flavoring 732 FlavoringProjectType = projectType + ":" + (int)projectType, 733 FlavoringBuildTarget = EditorUserBuildSettings.activeBuildTarget + ":" + (int)EditorUserBuildSettings.activeBuildTarget, 734 FlavoringUnityVersion = Application.unityVersion, 735 FlavoringPackageVersion = VisualStudioIntegration.PackageVersion(), 736 }; 737 738 SetAnalyzerAndSourceGeneratorProperties(assembly, responseFilesData, projectProperties); 739 740 GetProjectHeader(projectProperties, out headerBuilder); 741 } 742 743 private enum ProjectType 744 { 745 GamePlugins = 3, 746 Game = 1, 747 EditorPlugins = 7, 748 Editor = 5, 749 } 750 751 private static ProjectType ProjectTypeOf(string fileName) 752 { 753 var plugins = fileName.Contains("firstpass"); 754 var editor = fileName.Contains("Editor"); 755 756 if (plugins && editor) 757 return ProjectType.EditorPlugins; 758 if (plugins) 759 return ProjectType.GamePlugins; 760 if (editor) 761 return ProjectType.Editor; 762 763 return ProjectType.Game; 764 } 765 766 internal virtual void GetProjectHeader(ProjectProperties properties, out StringBuilder headerBuilder) 767 { 768 headerBuilder = default; 769 } 770 771 internal static void GetProjectHeaderConfigurations(ProjectProperties properties, StringBuilder headerBuilder) 772 { 773 const string NoWarn = "0169;USG0001"; 774 775 headerBuilder.Append(@" <PropertyGroup Condition="" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "">").Append(k_WindowsNewline); 776 headerBuilder.Append(@" <DebugSymbols>true</DebugSymbols>").Append(k_WindowsNewline); 777 headerBuilder.Append(@" <DebugType>full</DebugType>").Append(k_WindowsNewline); 778 headerBuilder.Append(@" <Optimize>false</Optimize>").Append(k_WindowsNewline); 779 headerBuilder.Append(@" <OutputPath>").Append(properties.OutputPath).Append(@"</OutputPath>").Append(k_WindowsNewline); 780 headerBuilder.Append(@" <DefineConstants>").Append(string.Join(";", properties.Defines)).Append(@"</DefineConstants>").Append(k_WindowsNewline); 781 headerBuilder.Append(@" <ErrorReport>prompt</ErrorReport>").Append(k_WindowsNewline); 782 headerBuilder.Append(@" <WarningLevel>4</WarningLevel>").Append(k_WindowsNewline); 783 headerBuilder.Append(@" <NoWarn>").Append(NoWarn).Append("</NoWarn>").Append(k_WindowsNewline); 784 headerBuilder.Append(@" <AllowUnsafeBlocks>").Append(properties.Unsafe).Append(@"</AllowUnsafeBlocks>").Append(k_WindowsNewline); 785 headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline); 786 headerBuilder.Append(@" <PropertyGroup Condition="" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "">").Append(k_WindowsNewline); 787 headerBuilder.Append(@" <DebugType>pdbonly</DebugType>").Append(k_WindowsNewline); 788 headerBuilder.Append(@" <Optimize>true</Optimize>").Append(k_WindowsNewline); 789 headerBuilder.Append($" <OutputPath>{@"Temp\bin\Release\".NormalizePathSeparators()}</OutputPath>").Append(k_WindowsNewline); 790 headerBuilder.Append(@" <ErrorReport>prompt</ErrorReport>").Append(k_WindowsNewline); 791 headerBuilder.Append(@" <WarningLevel>4</WarningLevel>").Append(k_WindowsNewline); 792 headerBuilder.Append(@" <NoWarn>").Append(NoWarn).Append("</NoWarn>").Append(k_WindowsNewline); 793 headerBuilder.Append(@" <AllowUnsafeBlocks>").Append(properties.Unsafe).Append(@"</AllowUnsafeBlocks>").Append(k_WindowsNewline); 794 headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline); 795 } 796 797 internal static void GetProjectHeaderAnalyzers(ProjectProperties properties, StringBuilder headerBuilder) 798 { 799 if (!string.IsNullOrEmpty(properties.RulesetPath)) 800 { 801 headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline); 802 headerBuilder.Append(@" <CodeAnalysisRuleSet>").Append(properties.RulesetPath).Append(@"</CodeAnalysisRuleSet>").Append(k_WindowsNewline); 803 headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline); 804 } 805 806 if (properties.Analyzers.Any()) 807 { 808 headerBuilder.Append(@" <ItemGroup>").Append(k_WindowsNewline); 809 foreach (var analyzer in properties.Analyzers) 810 { 811 headerBuilder.Append(@" <Analyzer Include=""").Append(analyzer).Append(@""" />").Append(k_WindowsNewline); 812 } 813 headerBuilder.Append(@" </ItemGroup>").Append(k_WindowsNewline); 814 } 815 816 if (!string.IsNullOrEmpty(properties.AnalyzerConfigPath)) 817 { 818 headerBuilder.Append(@" <ItemGroup>").Append(k_WindowsNewline); 819 headerBuilder.Append(@" <EditorConfigFiles Include=""").Append(properties.AnalyzerConfigPath).Append(@""" />").Append(k_WindowsNewline); 820 headerBuilder.Append(@" </ItemGroup>").Append(k_WindowsNewline); 821 } 822 823 if (properties.AdditionalFilePaths.Any()) 824 { 825 headerBuilder.Append(@" <ItemGroup>").Append(k_WindowsNewline); 826 foreach (var additionalFile in properties.AdditionalFilePaths) 827 { 828 headerBuilder.Append(@" <AdditionalFiles Include=""").Append(additionalFile).Append(@""" />").Append(k_WindowsNewline); 829 } 830 headerBuilder.Append(@" </ItemGroup>").Append(k_WindowsNewline); 831 } 832 } 833 834 internal void GetProjectHeaderVstuFlavoring(ProjectProperties properties, StringBuilder headerBuilder, bool includeProjectTypeGuids = true) 835 { 836 // Flavoring 837 headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline); 838 839 if (includeProjectTypeGuids) 840 { 841 headerBuilder.Append(@" <ProjectTypeGuids>{E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>").Append(k_WindowsNewline); 842 } 843 844 headerBuilder.Append(@" <UnityProjectGenerator>Package</UnityProjectGenerator>").Append(k_WindowsNewline); 845 headerBuilder.Append(@" <UnityProjectGeneratorVersion>").Append(properties.FlavoringPackageVersion).Append(@"</UnityProjectGeneratorVersion>").Append(k_WindowsNewline); 846 headerBuilder.Append(@" <UnityProjectGeneratorStyle>").Append(StyleName).Append("</UnityProjectGeneratorStyle>").Append(k_WindowsNewline); 847 headerBuilder.Append(@" <UnityProjectType>").Append(properties.FlavoringProjectType).Append(@"</UnityProjectType>").Append(k_WindowsNewline); 848 headerBuilder.Append(@" <UnityBuildTarget>").Append(properties.FlavoringBuildTarget).Append(@"</UnityBuildTarget>").Append(k_WindowsNewline); 849 headerBuilder.Append(@" <UnityVersion>").Append(properties.FlavoringUnityVersion).Append(@"</UnityVersion>").Append(k_WindowsNewline); 850 headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline); 851 } 852 853 internal virtual void GetProjectFooter(StringBuilder footerBuilder) 854 { 855 } 856 857 private static string GetSolutionText() 858 { 859 return string.Join(k_WindowsNewline, 860 @"", 861 @"Microsoft Visual Studio Solution File, Format Version {0}", 862 @"# Visual Studio {1}", 863 @"{2}", 864 @"Global", 865 @" GlobalSection(SolutionConfigurationPlatforms) = preSolution", 866 @" Debug|Any CPU = Debug|Any CPU", 867 @" Release|Any CPU = Release|Any CPU", 868 @" EndGlobalSection", 869 @" GlobalSection(ProjectConfigurationPlatforms) = postSolution", 870 @"{3}", 871 @" EndGlobalSection", 872 @"{4}", 873 @"EndGlobal", 874 @"").Replace(" ", "\t"); 875 } 876 877 private void SyncSolution(IEnumerable<Assembly> assemblies) 878 { 879 if (InvalidCharactersRegexPattern.IsMatch(ProjectDirectory)) 880 Debug.LogWarning("Project path contains special characters, which can be an issue when opening Visual Studio"); 881 882 var solutionFile = SolutionFile(); 883 var previousSolution = m_FileIOProvider.Exists(solutionFile) ? SolutionParser.ParseSolutionFile(solutionFile, m_FileIOProvider) : null; 884 SyncSolutionFileIfNotChanged(solutionFile, SolutionText(assemblies, previousSolution)); 885 } 886 887 private string SolutionText(IEnumerable<Assembly> assemblies, Solution previousSolution = null) 888 { 889 const string fileversion = "12.00"; 890 const string vsversion = "15"; 891 892 var relevantAssemblies = RelevantAssembliesForMode(assemblies); 893 var generatedProjects = ToProjectEntries(relevantAssemblies).ToList(); 894 895 SolutionProperties[] properties = null; 896 897 // First, add all projects generated by Unity to the solution 898 var projects = new List<SolutionProjectEntry>(); 899 projects.AddRange(generatedProjects); 900 901 if (previousSolution != null) 902 { 903 // Add all projects that were previously in the solution and that are not generated by Unity, nor generated in the project root directory 904 var externalProjects = previousSolution.Projects 905 .Where(p => p.IsSolutionFolderProjectFactory() || !FileUtility.IsFileInProjectRootDirectory(p.FileName)) 906 .Where(p => generatedProjects.All(gp => gp.FileName != p.FileName)); 907 908 projects.AddRange(externalProjects); 909 properties = previousSolution.Properties; 910 } 911 912 string propertiesText = GetPropertiesText(properties); 913 string projectEntriesText = GetProjectEntriesText(projects); 914 915 // do not generate configurations for SolutionFolders 916 var configurableProjects = projects.Where(p => !p.IsSolutionFolderProjectFactory()); 917 string projectConfigurationsText = string.Join(k_WindowsNewline, configurableProjects.Select(p => GetProjectActiveConfigurations(p.ProjectGuid)).ToArray()); 918 919 return string.Format(GetSolutionText(), fileversion, vsversion, projectEntriesText, projectConfigurationsText, propertiesText); 920 } 921 922 private static IEnumerable<Assembly> RelevantAssembliesForMode(IEnumerable<Assembly> assemblies) 923 { 924 return assemblies.Where(i => ScriptingLanguage.CSharp == ScriptingLanguageFor(i)); 925 } 926 927 private static string GetPropertiesText(SolutionProperties[] array) 928 { 929 if (array == null || array.Length == 0) 930 { 931 // HideSolution by default 932 array = new[] { 933 new SolutionProperties() { 934 Name = "SolutionProperties", 935 Type = "preSolution", 936 Entries = new List<KeyValuePair<string,string>>() { new KeyValuePair<string, string> ("HideSolutionNode", "FALSE") } 937 } 938 }; 939 } 940 var result = new StringBuilder(); 941 942 for (var i = 0; i < array.Length; i++) 943 { 944 if (i > 0) 945 result.Append(k_WindowsNewline); 946 947 var properties = array[i]; 948 949 result.Append($"\tGlobalSection({properties.Name}) = {properties.Type}"); 950 result.Append(k_WindowsNewline); 951 952 foreach (var entry in properties.Entries) 953 { 954 result.Append($"\t\t{entry.Key} = {entry.Value}"); 955 result.Append(k_WindowsNewline); 956 } 957 958 result.Append("\tEndGlobalSection"); 959 } 960 961 return result.ToString(); 962 } 963 964 /// <summary> 965 /// Get a Project("{guid}") = "MyProject", "MyProject.unityproj", "{projectguid}" 966 /// entry for each relevant language 967 /// </summary> 968 private string GetProjectEntriesText(IEnumerable<SolutionProjectEntry> entries) 969 { 970 var projectEntries = entries.Select(entry => string.Format( 971 m_SolutionProjectEntryTemplate, 972 entry.ProjectFactoryGuid, entry.Name, entry.FileName, entry.ProjectGuid, entry.Metadata 973 )); 974 975 return string.Join(k_WindowsNewline, projectEntries.ToArray()); 976 } 977 978 private IEnumerable<SolutionProjectEntry> ToProjectEntries(IEnumerable<Assembly> assemblies) 979 { 980 foreach (var assembly in assemblies) 981 yield return new SolutionProjectEntry() 982 { 983 ProjectFactoryGuid = SolutionGuid(assembly), 984 Name = assembly.name, 985 FileName = Path.GetFileName(ProjectFile(assembly)), 986 ProjectGuid = ProjectGuid(assembly), 987 Metadata = k_WindowsNewline 988 }; 989 } 990 991 /// <summary> 992 /// Generate the active configuration string for a given project guid 993 /// </summary> 994 private string GetProjectActiveConfigurations(string projectGuid) 995 { 996 return string.Format( 997 m_SolutionProjectConfigurationTemplate, 998 projectGuid); 999 } 1000 1001 internal string EscapedRelativePathFor(string file, out UnityEditor.PackageManager.PackageInfo packageInfo) 1002 { 1003 var projectDir = ProjectDirectory.NormalizePathSeparators(); 1004 file = file.NormalizePathSeparators(); 1005 var path = SkipPathPrefix(file, projectDir); 1006 1007 packageInfo = m_AssemblyNameProvider.FindForAssetPath(path.NormalizeWindowsToUnix()); 1008 if (packageInfo != null) 1009 { 1010 // We have to normalize the path, because the PackageManagerRemapper assumes 1011 // dir seperators will be os specific. 1012 var absolutePath = Path.GetFullPath(path.NormalizePathSeparators()); 1013 path = SkipPathPrefix(absolutePath, projectDir); 1014 } 1015 1016 return XmlFilename(path); 1017 } 1018 1019 internal static string SkipPathPrefix(string path, string prefix) 1020 { 1021 if (path.StartsWith($"{prefix}{Path.DirectorySeparatorChar}") && (path.Length > prefix.Length)) 1022 return path.Substring(prefix.Length + 1); 1023 return path; 1024 } 1025 1026 internal static string GetProjectExtension() 1027 { 1028 return ".csproj"; 1029 } 1030 1031 internal string ProjectGuid(string assemblyName) 1032 { 1033 return m_GUIDGenerator.ProjectGuid(m_ProjectName, assemblyName); 1034 } 1035 1036 internal string ProjectGuid(Assembly assembly) 1037 { 1038 return ProjectGuid(m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, assembly.name)); 1039 } 1040 1041 private string SolutionGuid(Assembly assembly) 1042 { 1043 return m_GUIDGenerator.SolutionGuid(m_ProjectName, ScriptingLanguageFor(assembly)); 1044 } 1045 1046 private static string GetRootNamespace(Assembly assembly) 1047 { 1048#if UNITY_2020_2_OR_NEWER 1049 return assembly.rootNamespace; 1050#else 1051 return EditorSettings.projectGenerationRootNamespace; 1052#endif 1053 } 1054 } 1055 1056 public static class SolutionGuidGenerator 1057 { 1058 public static string GuidForProject(string projectName) 1059 { 1060 return ComputeGuidHashFor(projectName + "salt"); 1061 } 1062 1063 public static string GuidForSolution(string projectName, ScriptingLanguage language) 1064 { 1065 if (language == ScriptingLanguage.CSharp) 1066 { 1067 // GUID for a C# class library: http://www.codeproject.com/Reference/720512/List-of-Visual-Studio-Project-Type-GUIDs 1068 return "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC"; 1069 } 1070 1071 return ComputeGuidHashFor(projectName); 1072 } 1073 1074 private static string ComputeGuidHashFor(string input) 1075 { 1076 var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(input)); 1077 return HashAsGuid(HashToString(hash)); 1078 } 1079 1080 private static string HashAsGuid(string hash) 1081 { 1082 var guid = hash.Substring(0, 8) + "-" + hash.Substring(8, 4) + "-" + hash.Substring(12, 4) + "-" + hash.Substring(16, 4) + "-" + hash.Substring(20, 12); 1083 return guid.ToUpper(); 1084 } 1085 1086 private static string HashToString(byte[] bs) 1087 { 1088 var sb = new StringBuilder(); 1089 foreach (byte b in bs) 1090 sb.Append(b.ToString("x2")); 1091 return sb.ToString(); 1092 } 1093 } 1094}