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}