ProjectGeneration.cs 35 KB

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