No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProjectGeneration.cs 38KB

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