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 39KB

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