ProjectGeneration.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Security;
  6. using System.Security.Cryptography;
  7. using System.Text;
  8. using UnityEditor;
  9. using UnityEditor.Compilation;
  10. using UnityEngine;
  11. using UnityEngine.Profiling;
  12. namespace VSCodeEditor
  13. {
  14. public interface IGenerator
  15. {
  16. bool SyncIfNeeded(List<string> affectedFiles, string[] reimportedFiles);
  17. void Sync();
  18. string SolutionFile();
  19. string ProjectDirectory { get; }
  20. IAssemblyNameProvider AssemblyNameProvider { get; }
  21. void GenerateAll(bool generateAll);
  22. bool SolutionExists();
  23. }
  24. public class ProjectGeneration : IGenerator
  25. {
  26. enum ScriptingLanguage
  27. {
  28. None,
  29. CSharp
  30. }
  31. public static readonly string MSBuildNamespaceUri = "http://schemas.microsoft.com/developer/msbuild/2003";
  32. const string k_WindowsNewline = "\r\n";
  33. const string k_SettingsJson = @"{
  34. ""files.exclude"":
  35. {
  36. ""**/.DS_Store"":true,
  37. ""**/.git"":true,
  38. ""**/.gitignore"":true,
  39. ""**/.gitmodules"":true,
  40. ""**/*.booproj"":true,
  41. ""**/*.pidb"":true,
  42. ""**/*.suo"":true,
  43. ""**/*.user"":true,
  44. ""**/*.userprefs"":true,
  45. ""**/*.unityproj"":true,
  46. ""**/*.dll"":true,
  47. ""**/*.exe"":true,
  48. ""**/*.pdf"":true,
  49. ""**/*.mid"":true,
  50. ""**/*.midi"":true,
  51. ""**/*.wav"":true,
  52. ""**/*.gif"":true,
  53. ""**/*.ico"":true,
  54. ""**/*.jpg"":true,
  55. ""**/*.jpeg"":true,
  56. ""**/*.png"":true,
  57. ""**/*.psd"":true,
  58. ""**/*.tga"":true,
  59. ""**/*.tif"":true,
  60. ""**/*.tiff"":true,
  61. ""**/*.3ds"":true,
  62. ""**/*.3DS"":true,
  63. ""**/*.fbx"":true,
  64. ""**/*.FBX"":true,
  65. ""**/*.lxo"":true,
  66. ""**/*.LXO"":true,
  67. ""**/*.ma"":true,
  68. ""**/*.MA"":true,
  69. ""**/*.obj"":true,
  70. ""**/*.OBJ"":true,
  71. ""**/*.asset"":true,
  72. ""**/*.cubemap"":true,
  73. ""**/*.flare"":true,
  74. ""**/*.mat"":true,
  75. ""**/*.meta"":true,
  76. ""**/*.prefab"":true,
  77. ""**/*.unity"":true,
  78. ""build/"":true,
  79. ""Build/"":true,
  80. ""Library/"":true,
  81. ""library/"":true,
  82. ""obj/"":true,
  83. ""Obj/"":true,
  84. ""ProjectSettings/"":true,
  85. ""temp/"":true,
  86. ""Temp/"":true
  87. }
  88. }";
  89. /// <summary>
  90. /// Map source extensions to ScriptingLanguages
  91. /// </summary>
  92. static readonly Dictionary<string, ScriptingLanguage> k_BuiltinSupportedExtensions = new Dictionary<string, ScriptingLanguage>
  93. {
  94. { "cs", ScriptingLanguage.CSharp },
  95. { "uxml", ScriptingLanguage.None },
  96. { "uss", ScriptingLanguage.None },
  97. { "shader", ScriptingLanguage.None },
  98. { "compute", ScriptingLanguage.None },
  99. { "cginc", ScriptingLanguage.None },
  100. { "hlsl", ScriptingLanguage.None },
  101. { "glslinc", ScriptingLanguage.None },
  102. { "template", ScriptingLanguage.None },
  103. { "raytrace", ScriptingLanguage.None }
  104. };
  105. string m_SolutionProjectEntryTemplate = string.Join("\r\n", @"Project(""{{{0}}}"") = ""{1}"", ""{2}"", ""{{{3}}}""", @"EndProject").Replace(" ", "\t");
  106. string m_SolutionProjectConfigurationTemplate = string.Join("\r\n", @" {{{0}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU", @" {{{0}}}.Debug|Any CPU.Build.0 = Debug|Any CPU").Replace(" ", "\t");
  107. static readonly string[] k_ReimportSyncExtensions = { ".dll", ".asmdef" };
  108. string[] m_ProjectSupportedExtensions = new string[0];
  109. public string ProjectDirectory { get; }
  110. IAssemblyNameProvider IGenerator.AssemblyNameProvider => m_AssemblyNameProvider;
  111. public void GenerateAll(bool generateAll)
  112. {
  113. m_AssemblyNameProvider.ToggleProjectGeneration(
  114. ProjectGenerationFlag.BuiltIn
  115. | ProjectGenerationFlag.Embedded
  116. | ProjectGenerationFlag.Git
  117. | ProjectGenerationFlag.Local
  118. #if UNITY_2019_3_OR_NEWER
  119. | ProjectGenerationFlag.LocalTarBall
  120. #endif
  121. | ProjectGenerationFlag.PlayerAssemblies
  122. | ProjectGenerationFlag.Registry
  123. | ProjectGenerationFlag.Unknown);
  124. }
  125. readonly string m_ProjectName;
  126. readonly IAssemblyNameProvider m_AssemblyNameProvider;
  127. readonly IFileIO m_FileIOProvider;
  128. readonly IGUIDGenerator m_GUIDProvider;
  129. const string k_ToolsVersion = "4.0";
  130. const string k_ProductVersion = "10.0.20506";
  131. const string k_BaseDirectory = ".";
  132. const string k_TargetFrameworkVersion = "v4.7.1";
  133. const string k_TargetLanguageVersion = "latest";
  134. public ProjectGeneration(string tempDirectory)
  135. : this(tempDirectory, new AssemblyNameProvider(), new FileIOProvider(), new GUIDProvider()) { }
  136. public ProjectGeneration(string tempDirectory, IAssemblyNameProvider assemblyNameProvider, IFileIO fileIO, IGUIDGenerator guidGenerator)
  137. {
  138. ProjectDirectory = tempDirectory.Replace('\\', '/');
  139. m_ProjectName = Path.GetFileName(ProjectDirectory);
  140. m_AssemblyNameProvider = assemblyNameProvider;
  141. m_FileIOProvider = fileIO;
  142. m_GUIDProvider = guidGenerator;
  143. }
  144. /// <summary>
  145. /// Syncs the scripting solution if any affected files are relevant.
  146. /// </summary>
  147. /// <returns>
  148. /// Whether the solution was synced.
  149. /// </returns>
  150. /// <param name='affectedFiles'>
  151. /// A set of files whose status has changed
  152. /// </param>
  153. /// <param name="reimportedFiles">
  154. /// A set of files that got reimported
  155. /// </param>
  156. public bool SyncIfNeeded(List<string> affectedFiles, string[] reimportedFiles)
  157. {
  158. Profiler.BeginSample("SolutionSynchronizerSync");
  159. SetupProjectSupportedExtensions();
  160. // Don't sync if we haven't synced before
  161. if (SolutionExists() && HasFilesBeenModified(affectedFiles, reimportedFiles))
  162. {
  163. var assemblies = m_AssemblyNameProvider.GetAssemblies(ShouldFileBePartOfSolution);
  164. var allProjectAssemblies = RelevantAssembliesForMode(assemblies).ToList();
  165. var allAssetProjectParts = GenerateAllAssetProjectParts();
  166. var affectedNames = affectedFiles.Select(asset => m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset)).Where(name => !string.IsNullOrWhiteSpace(name)).Select(name => name.Split(new [] {".dll"}, StringSplitOptions.RemoveEmptyEntries)[0]);
  167. var reimportedNames = reimportedFiles.Select(asset => m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset)).Where(name => !string.IsNullOrWhiteSpace(name)).Select(name => name.Split(new [] {".dll"}, StringSplitOptions.RemoveEmptyEntries)[0]);
  168. var affectedAndReimported = new HashSet<string>(affectedNames.Concat(reimportedNames));
  169. var assemblyNames = new HashSet<string>(allProjectAssemblies.Select(assembly => Path.GetFileName(assembly.outputPath)));
  170. foreach (var assembly in allProjectAssemblies)
  171. {
  172. if (!affectedAndReimported.Contains(assembly.name))
  173. continue;
  174. SyncProject(assembly, allAssetProjectParts, ParseResponseFileData(assembly), assemblyNames);
  175. }
  176. Profiler.EndSample();
  177. return true;
  178. }
  179. Profiler.EndSample();
  180. return false;
  181. }
  182. bool HasFilesBeenModified(List<string> affectedFiles, string[] reimportedFiles)
  183. {
  184. return affectedFiles.Any(ShouldFileBePartOfSolution) || reimportedFiles.Any(ShouldSyncOnReimportedAsset);
  185. }
  186. static bool ShouldSyncOnReimportedAsset(string asset)
  187. {
  188. return k_ReimportSyncExtensions.Contains(new FileInfo(asset).Extension);
  189. }
  190. public void Sync()
  191. {
  192. SetupProjectSupportedExtensions();
  193. GenerateAndWriteSolutionAndProjects();
  194. }
  195. public bool SolutionExists()
  196. {
  197. return m_FileIOProvider.Exists(SolutionFile());
  198. }
  199. void SetupProjectSupportedExtensions()
  200. {
  201. m_ProjectSupportedExtensions = m_AssemblyNameProvider.ProjectSupportedExtensions;
  202. }
  203. bool ShouldFileBePartOfSolution(string file)
  204. {
  205. // Exclude files coming from packages except if they are internalized.
  206. if (m_AssemblyNameProvider.IsInternalizedPackagePath(file))
  207. {
  208. return false;
  209. }
  210. return HasValidExtension(file);
  211. }
  212. bool HasValidExtension(string file)
  213. {
  214. string extension = Path.GetExtension(file);
  215. // Dll's are not scripts but still need to be included..
  216. if (extension == ".dll")
  217. return true;
  218. if (file.ToLower().EndsWith(".asmdef"))
  219. return true;
  220. return IsSupportedExtension(extension);
  221. }
  222. bool IsSupportedExtension(string extension)
  223. {
  224. extension = extension.TrimStart('.');
  225. if (k_BuiltinSupportedExtensions.ContainsKey(extension))
  226. return true;
  227. if (m_ProjectSupportedExtensions.Contains(extension))
  228. return true;
  229. return false;
  230. }
  231. static ScriptingLanguage ScriptingLanguageFor(Assembly assembly)
  232. {
  233. return ScriptingLanguageFor(GetExtensionOfSourceFiles(assembly.sourceFiles));
  234. }
  235. static string GetExtensionOfSourceFiles(string[] files)
  236. {
  237. return files.Length > 0 ? GetExtensionOfSourceFile(files[0]) : "NA";
  238. }
  239. static string GetExtensionOfSourceFile(string file)
  240. {
  241. var ext = Path.GetExtension(file).ToLower();
  242. ext = ext.Substring(1); //strip dot
  243. return ext;
  244. }
  245. static ScriptingLanguage ScriptingLanguageFor(string extension)
  246. {
  247. return k_BuiltinSupportedExtensions.TryGetValue(extension.TrimStart('.'), out var result)
  248. ? result
  249. : ScriptingLanguage.None;
  250. }
  251. public void GenerateAndWriteSolutionAndProjects()
  252. {
  253. // Only synchronize assemblies that have associated source files and ones that we actually want in the project.
  254. // This also filters out DLLs coming from .asmdef files in packages.
  255. var assemblies = m_AssemblyNameProvider.GetAssemblies(ShouldFileBePartOfSolution);
  256. var allAssetProjectParts = GenerateAllAssetProjectParts();
  257. SyncSolution(assemblies);
  258. var allProjectAssemblies = RelevantAssembliesForMode(assemblies).ToList();
  259. var assemblyNames = new HashSet<string>(allProjectAssemblies.Select(assembly => Path.GetFileName(assembly.outputPath)));
  260. foreach (Assembly assembly in allProjectAssemblies)
  261. {
  262. var responseFileData = ParseResponseFileData(assembly);
  263. SyncProject(assembly, allAssetProjectParts, responseFileData, assemblyNames);
  264. }
  265. WriteVSCodeSettingsFiles();
  266. }
  267. List<ResponseFileData> ParseResponseFileData(Assembly assembly)
  268. {
  269. var systemReferenceDirectories = CompilationPipeline.GetSystemAssemblyDirectories(assembly.compilerOptions.ApiCompatibilityLevel);
  270. Dictionary<string, ResponseFileData> responseFilesData = assembly.compilerOptions.ResponseFiles.ToDictionary(x => x, x => m_AssemblyNameProvider.ParseResponseFile(
  271. x,
  272. ProjectDirectory,
  273. systemReferenceDirectories
  274. ));
  275. Dictionary<string, ResponseFileData> responseFilesWithErrors = responseFilesData.Where(x => x.Value.Errors.Any())
  276. .ToDictionary(x => x.Key, x => x.Value);
  277. if (responseFilesWithErrors.Any())
  278. {
  279. foreach (var error in responseFilesWithErrors)
  280. foreach (var valueError in error.Value.Errors)
  281. {
  282. Debug.LogError($"{error.Key} Parse Error : {valueError}");
  283. }
  284. }
  285. return responseFilesData.Select(x => x.Value).ToList();
  286. }
  287. Dictionary<string, string> GenerateAllAssetProjectParts()
  288. {
  289. Dictionary<string, StringBuilder> stringBuilders = new Dictionary<string, StringBuilder>();
  290. foreach (string asset in m_AssemblyNameProvider.GetAllAssetPaths())
  291. {
  292. // Exclude files coming from packages except if they are internalized.
  293. // TODO: We need assets from the assembly API
  294. if (m_AssemblyNameProvider.IsInternalizedPackagePath(asset))
  295. {
  296. continue;
  297. }
  298. string extension = Path.GetExtension(asset);
  299. if (IsSupportedExtension(extension) && ScriptingLanguage.None == ScriptingLanguageFor(extension))
  300. {
  301. // Find assembly the asset belongs to by adding script extension and using compilation pipeline.
  302. var assemblyName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset);
  303. if (string.IsNullOrEmpty(assemblyName))
  304. {
  305. continue;
  306. }
  307. assemblyName = Path.GetFileNameWithoutExtension(assemblyName);
  308. if (!stringBuilders.TryGetValue(assemblyName, out var projectBuilder))
  309. {
  310. projectBuilder = new StringBuilder();
  311. stringBuilders[assemblyName] = projectBuilder;
  312. }
  313. projectBuilder.Append(" <None Include=\"").Append(EscapedRelativePathFor(asset)).Append("\" />").Append(k_WindowsNewline);
  314. }
  315. }
  316. var result = new Dictionary<string, string>();
  317. foreach (var entry in stringBuilders)
  318. result[entry.Key] = entry.Value.ToString();
  319. return result;
  320. }
  321. void SyncProject(
  322. Assembly assembly,
  323. Dictionary<string, string> allAssetsProjectParts,
  324. List<ResponseFileData> responseFilesData,
  325. HashSet<string> assemblyNames)
  326. {
  327. SyncProjectFileIfNotChanged(ProjectFile(assembly), ProjectText(assembly, allAssetsProjectParts, responseFilesData, assemblyNames, GetAllRoslynAnalyzerPaths().ToArray()));
  328. }
  329. private IEnumerable<string> GetAllRoslynAnalyzerPaths()
  330. {
  331. return m_AssemblyNameProvider.GetRoslynAnalyzerPaths();
  332. }
  333. void SyncProjectFileIfNotChanged(string path, string newContents)
  334. {
  335. SyncFileIfNotChanged(path, newContents);
  336. }
  337. void SyncSolutionFileIfNotChanged(string path, string newContents)
  338. {
  339. SyncFileIfNotChanged(path, newContents);
  340. }
  341. void SyncFileIfNotChanged(string filename, string newContents)
  342. {
  343. if (m_FileIOProvider.Exists(filename))
  344. {
  345. var currentContents = m_FileIOProvider.ReadAllText(filename);
  346. if (currentContents == newContents)
  347. {
  348. return;
  349. }
  350. }
  351. m_FileIOProvider.WriteAllText(filename, newContents);
  352. }
  353. string ProjectText(
  354. Assembly assembly,
  355. Dictionary<string, string> allAssetsProjectParts,
  356. List<ResponseFileData> responseFilesData,
  357. HashSet<string> assemblyNames,
  358. string[] roslynAnalyzerDllPaths)
  359. {
  360. var projectBuilder = new StringBuilder();
  361. ProjectHeader(assembly, responseFilesData, roslynAnalyzerDllPaths, projectBuilder);
  362. var references = new List<string>();
  363. foreach (string file in assembly.sourceFiles)
  364. {
  365. if (!HasValidExtension(file))
  366. continue;
  367. var extension = Path.GetExtension(file).ToLower();
  368. var fullFile = EscapedRelativePathFor(file);
  369. if (".dll" != extension)
  370. {
  371. projectBuilder.Append(" <Compile Include=\"").Append(fullFile).Append("\" />").Append(k_WindowsNewline);
  372. }
  373. else
  374. {
  375. references.Add(fullFile);
  376. }
  377. }
  378. // Append additional non-script files that should be included in project generation.
  379. if (allAssetsProjectParts.TryGetValue(assembly.name, out var additionalAssetsForProject))
  380. projectBuilder.Append(additionalAssetsForProject);
  381. var responseRefs = responseFilesData.SelectMany(x => x.FullPathReferences.Select(r => r));
  382. var internalAssemblyReferences = assembly.assemblyReferences
  383. .Where(i => !i.sourceFiles.Any(ShouldFileBePartOfSolution)).Select(i => i.outputPath);
  384. var allReferences =
  385. assembly.compiledAssemblyReferences
  386. .Union(responseRefs)
  387. .Union(references)
  388. .Union(internalAssemblyReferences)
  389. .Except(roslynAnalyzerDllPaths);
  390. foreach (var reference in allReferences)
  391. {
  392. string fullReference = Path.IsPathRooted(reference) ? reference : Path.Combine(ProjectDirectory, reference);
  393. AppendReference(fullReference, projectBuilder);
  394. }
  395. if (0 < assembly.assemblyReferences.Length)
  396. {
  397. projectBuilder.Append(" </ItemGroup>").Append(k_WindowsNewline);
  398. projectBuilder.Append(" <ItemGroup>").Append(k_WindowsNewline);
  399. foreach (Assembly reference in assembly.assemblyReferences.Where(i => i.sourceFiles.Any(ShouldFileBePartOfSolution)))
  400. {
  401. var referencedProject = reference.outputPath;
  402. projectBuilder.Append(" <ProjectReference Include=\"").Append(reference.name).Append(GetProjectExtension()).Append("\">").Append(k_WindowsNewline);
  403. projectBuilder.Append(" <Project>{").Append(ProjectGuid(reference.name)).Append("}</Project>").Append(k_WindowsNewline);
  404. projectBuilder.Append(" <Name>").Append(reference.name).Append("</Name>").Append(k_WindowsNewline);
  405. projectBuilder.Append(" <ReferenceOutputAssembly>false</ReferenceOutputAssembly>").Append(k_WindowsNewline);
  406. projectBuilder.Append(" </ProjectReference>").Append(k_WindowsNewline);
  407. }
  408. }
  409. projectBuilder.Append(ProjectFooter());
  410. return projectBuilder.ToString();
  411. }
  412. static void AppendReference(string fullReference, StringBuilder projectBuilder)
  413. {
  414. //replace \ with / and \\ with /
  415. var escapedFullPath = SecurityElement.Escape(fullReference);
  416. escapedFullPath = escapedFullPath.Replace("\\\\", "/");
  417. escapedFullPath = escapedFullPath.Replace("\\", "/");
  418. projectBuilder.Append(" <Reference Include=\"").Append(Path.GetFileNameWithoutExtension(escapedFullPath)).Append("\">").Append(k_WindowsNewline);
  419. projectBuilder.Append(" <HintPath>").Append(escapedFullPath).Append("</HintPath>").Append(k_WindowsNewline);
  420. projectBuilder.Append(" </Reference>").Append(k_WindowsNewline);
  421. }
  422. public string ProjectFile(Assembly assembly)
  423. {
  424. var fileBuilder = new StringBuilder(assembly.name);
  425. fileBuilder.Append(".csproj");
  426. return Path.Combine(ProjectDirectory, fileBuilder.ToString());
  427. }
  428. public string SolutionFile()
  429. {
  430. return Path.Combine(ProjectDirectory, $"{m_ProjectName}.sln");
  431. }
  432. void ProjectHeader(
  433. Assembly assembly,
  434. List<ResponseFileData> responseFilesData,
  435. string[] roslynAnalyzerDllPaths,
  436. StringBuilder builder
  437. )
  438. {
  439. var otherArguments = GetOtherArgumentsFromResponseFilesData(responseFilesData);
  440. GetProjectHeaderTemplate(
  441. builder,
  442. ProjectGuid(assembly.name),
  443. assembly.name,
  444. string.Join(";", new[] { "DEBUG", "TRACE" }.Concat(assembly.defines).Concat(responseFilesData.SelectMany(x => x.Defines)).Concat(EditorUserBuildSettings.activeScriptCompilationDefines).Distinct().ToArray()),
  445. assembly.compilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe),
  446. GenerateAnalyserItemGroup(otherArguments["analyzer"].Concat(otherArguments["a"])
  447. .SelectMany(x => x.Split(';'))
  448. .Concat(roslynAnalyzerDllPaths)
  449. .Distinct()
  450. .ToArray()));
  451. }
  452. private static ILookup<string, string> GetOtherArgumentsFromResponseFilesData(List<ResponseFileData> responseFilesData)
  453. {
  454. var paths = responseFilesData.SelectMany(x =>
  455. {
  456. return x.OtherArguments.Where(a => a.StartsWith("/") || a.StartsWith("-"))
  457. .Select(b =>
  458. {
  459. var index = b.IndexOf(":", StringComparison.Ordinal);
  460. if (index > 0 && b.Length > index)
  461. {
  462. var key = b.Substring(1, index - 1);
  463. return new KeyValuePair<string, string>(key, b.Substring(index + 1));
  464. }
  465. const string warnaserror = "warnaserror";
  466. if (b.Substring(1).StartsWith(warnaserror))
  467. {
  468. return new KeyValuePair<string, string>(warnaserror, b.Substring(warnaserror.Length + 1));
  469. }
  470. return default;
  471. });
  472. })
  473. .Distinct()
  474. .ToLookup(o => o.Key, pair => pair.Value);
  475. return paths;
  476. }
  477. private static string GenerateAnalyserItemGroup(string[] paths)
  478. {
  479. // <ItemGroup>
  480. // <Analyzer Include="..\packages\Comments_analyser.1.0.6626.21356\analyzers\dotnet\cs\Comments_analyser.dll" />
  481. // <Analyzer Include="..\packages\UnityEngineAnalyzer.1.0.0.0\analyzers\dotnet\cs\UnityEngineAnalyzer.dll" />
  482. // </ItemGroup>
  483. if (!paths.Any())
  484. return string.Empty;
  485. var analyserBuilder = new StringBuilder();
  486. analyserBuilder.Append(" <ItemGroup>").Append(k_WindowsNewline);
  487. foreach (var path in paths)
  488. {
  489. analyserBuilder.Append($" <Analyzer Include=\"{path}\" />").Append(k_WindowsNewline);
  490. }
  491. analyserBuilder.Append(" </ItemGroup>").Append(k_WindowsNewline);
  492. return analyserBuilder.ToString();
  493. }
  494. static string GetSolutionText()
  495. {
  496. return string.Join("\r\n", @"", @"Microsoft Visual Studio Solution File, Format Version {0}", @"# Visual Studio {1}", @"{2}", @"Global", @" GlobalSection(SolutionConfigurationPlatforms) = preSolution", @" Debug|Any CPU = Debug|Any CPU", @" EndGlobalSection", @" GlobalSection(ProjectConfigurationPlatforms) = postSolution", @"{3}", @" EndGlobalSection", @" GlobalSection(SolutionProperties) = preSolution", @" HideSolutionNode = FALSE", @" EndGlobalSection", @"EndGlobal", @"").Replace(" ", "\t");
  497. }
  498. static string GetProjectFooterTemplate()
  499. {
  500. return string.Join("\r\n", @" </ItemGroup>", @" <Import Project=""$(MSBuildToolsPath)\Microsoft.CSharp.targets"" />", @" <!-- To modify your build process, add your task inside one of the targets below and uncomment it.", @" Other similar extension points exist, see Microsoft.Common.targets.", @" <Target Name=""BeforeBuild"">", @" </Target>", @" <Target Name=""AfterBuild"">", @" </Target>", @" -->", @"</Project>", @"");
  501. }
  502. static void GetProjectHeaderTemplate(
  503. StringBuilder builder,
  504. string assemblyGUID,
  505. string assemblyName,
  506. string defines,
  507. bool allowUnsafe,
  508. string analyzerBlock
  509. )
  510. {
  511. builder.Append(@"<?xml version=""1.0"" encoding=""utf-8""?>").Append(k_WindowsNewline);
  512. builder.Append(@"<Project ToolsVersion=""").Append(k_ToolsVersion).Append(@""" DefaultTargets=""Build"" xmlns=""").Append(MSBuildNamespaceUri).Append(@""">").Append(k_WindowsNewline);
  513. builder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
  514. builder.Append(@" <LangVersion>").Append(k_TargetLanguageVersion).Append("</LangVersion>").Append(k_WindowsNewline);
  515. builder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
  516. builder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
  517. builder.Append(@" <Configuration Condition="" '$(Configuration)' == '' "">Debug</Configuration>").Append(k_WindowsNewline);
  518. builder.Append(@" <Platform Condition="" '$(Platform)' == '' "">AnyCPU</Platform>").Append(k_WindowsNewline);
  519. builder.Append(@" <ProductVersion>").Append(k_ProductVersion).Append("</ProductVersion>").Append(k_WindowsNewline);
  520. builder.Append(@" <SchemaVersion>2.0</SchemaVersion>").Append(k_WindowsNewline);
  521. builder.Append(@" <RootNamespace>").Append(EditorSettings.projectGenerationRootNamespace).Append("</RootNamespace>").Append(k_WindowsNewline);
  522. builder.Append(@" <ProjectGuid>{").Append(assemblyGUID).Append("}</ProjectGuid>").Append(k_WindowsNewline);
  523. builder.Append(@" <OutputType>Library</OutputType>").Append(k_WindowsNewline);
  524. builder.Append(@" <AppDesignerFolder>Properties</AppDesignerFolder>").Append(k_WindowsNewline);
  525. builder.Append(@" <AssemblyName>").Append(assemblyName).Append("</AssemblyName>").Append(k_WindowsNewline);
  526. builder.Append(@" <TargetFrameworkVersion>").Append(k_TargetFrameworkVersion).Append("</TargetFrameworkVersion>").Append(k_WindowsNewline);
  527. builder.Append(@" <FileAlignment>512</FileAlignment>").Append(k_WindowsNewline);
  528. builder.Append(@" <BaseDirectory>").Append(k_BaseDirectory).Append("</BaseDirectory>").Append(k_WindowsNewline);
  529. builder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
  530. builder.Append(@" <PropertyGroup Condition="" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "">").Append(k_WindowsNewline);
  531. builder.Append(@" <DebugSymbols>true</DebugSymbols>").Append(k_WindowsNewline);
  532. builder.Append(@" <DebugType>full</DebugType>").Append(k_WindowsNewline);
  533. builder.Append(@" <Optimize>false</Optimize>").Append(k_WindowsNewline);
  534. builder.Append(@" <OutputPath>Temp\bin\Debug\</OutputPath>").Append(k_WindowsNewline);
  535. builder.Append(@" <DefineConstants>").Append(defines).Append("</DefineConstants>").Append(k_WindowsNewline);
  536. builder.Append(@" <ErrorReport>prompt</ErrorReport>").Append(k_WindowsNewline);
  537. builder.Append(@" <WarningLevel>4</WarningLevel>").Append(k_WindowsNewline);
  538. builder.Append(@" <NoWarn>0169</NoWarn>").Append(k_WindowsNewline);
  539. builder.Append(@" <AllowUnsafeBlocks>").Append(allowUnsafe).Append("</AllowUnsafeBlocks>").Append(k_WindowsNewline);
  540. builder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
  541. builder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
  542. builder.Append(@" <NoConfig>true</NoConfig>").Append(k_WindowsNewline);
  543. builder.Append(@" <NoStdLib>true</NoStdLib>").Append(k_WindowsNewline);
  544. builder.Append(@" <AddAdditionalExplicitAssemblyReferences>false</AddAdditionalExplicitAssemblyReferences>").Append(k_WindowsNewline);
  545. builder.Append(@" <ImplicitlyExpandNETStandardFacades>false</ImplicitlyExpandNETStandardFacades>").Append(k_WindowsNewline);
  546. builder.Append(@" <ImplicitlyExpandDesignTimeFacades>false</ImplicitlyExpandDesignTimeFacades>").Append(k_WindowsNewline);
  547. builder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
  548. builder.Append(analyzerBlock);
  549. builder.Append(@" <ItemGroup>").Append(k_WindowsNewline);
  550. }
  551. void SyncSolution(IEnumerable<Assembly> assemblies)
  552. {
  553. SyncSolutionFileIfNotChanged(SolutionFile(), SolutionText(assemblies));
  554. }
  555. string SolutionText(IEnumerable<Assembly> assemblies)
  556. {
  557. var fileversion = "11.00";
  558. var vsversion = "2010";
  559. var relevantAssemblies = RelevantAssembliesForMode(assemblies);
  560. string projectEntries = GetProjectEntries(relevantAssemblies);
  561. string projectConfigurations = string.Join(k_WindowsNewline, relevantAssemblies.Select(i => GetProjectActiveConfigurations(ProjectGuid(i.name))).ToArray());
  562. return string.Format(GetSolutionText(), fileversion, vsversion, projectEntries, projectConfigurations);
  563. }
  564. static IEnumerable<Assembly> RelevantAssembliesForMode(IEnumerable<Assembly> assemblies)
  565. {
  566. return assemblies.Where(i => ScriptingLanguage.CSharp == ScriptingLanguageFor(i));
  567. }
  568. /// <summary>
  569. /// Get a Project("{guid}") = "MyProject", "MyProject.csproj", "{projectguid}"
  570. /// entry for each relevant language
  571. /// </summary>
  572. string GetProjectEntries(IEnumerable<Assembly> assemblies)
  573. {
  574. var projectEntries = assemblies.Select(i => string.Format(
  575. m_SolutionProjectEntryTemplate,
  576. SolutionGuid(i),
  577. i.name,
  578. Path.GetFileName(ProjectFile(i)),
  579. ProjectGuid(i.name)
  580. ));
  581. return string.Join(k_WindowsNewline, projectEntries.ToArray());
  582. }
  583. /// <summary>
  584. /// Generate the active configuration string for a given project guid
  585. /// </summary>
  586. string GetProjectActiveConfigurations(string projectGuid)
  587. {
  588. return string.Format(
  589. m_SolutionProjectConfigurationTemplate,
  590. projectGuid);
  591. }
  592. string EscapedRelativePathFor(string file)
  593. {
  594. var projectDir = ProjectDirectory.Replace('/', '\\');
  595. file = file.Replace('/', '\\');
  596. var path = SkipPathPrefix(file, projectDir);
  597. var packageInfo = m_AssemblyNameProvider.FindForAssetPath(path.Replace('\\', '/'));
  598. if (packageInfo != null)
  599. {
  600. // We have to normalize the path, because the PackageManagerRemapper assumes
  601. // dir seperators will be os specific.
  602. var absolutePath = Path.GetFullPath(NormalizePath(path)).Replace('/', '\\');
  603. path = SkipPathPrefix(absolutePath, projectDir);
  604. }
  605. return SecurityElement.Escape(path);
  606. }
  607. static string SkipPathPrefix(string path, string prefix)
  608. {
  609. if (path.StartsWith($@"{prefix}\"))
  610. return path.Substring(prefix.Length + 1);
  611. return path;
  612. }
  613. static string NormalizePath(string path)
  614. {
  615. if (Path.DirectorySeparatorChar == '\\')
  616. return path.Replace('/', Path.DirectorySeparatorChar);
  617. return path.Replace('\\', Path.DirectorySeparatorChar);
  618. }
  619. string ProjectGuid(string assembly)
  620. {
  621. return m_GUIDProvider.ProjectGuid(m_ProjectName, assembly);
  622. }
  623. string SolutionGuid(Assembly assembly)
  624. {
  625. return m_GUIDProvider.SolutionGuid(m_ProjectName, GetExtensionOfSourceFiles(assembly.sourceFiles));
  626. }
  627. static string ProjectFooter()
  628. {
  629. return GetProjectFooterTemplate();
  630. }
  631. static string GetProjectExtension()
  632. {
  633. return ".csproj";
  634. }
  635. void WriteVSCodeSettingsFiles()
  636. {
  637. var vsCodeDirectory = Path.Combine(ProjectDirectory, ".vscode");
  638. if (!m_FileIOProvider.Exists(vsCodeDirectory))
  639. m_FileIOProvider.CreateDirectory(vsCodeDirectory);
  640. var vsCodeSettingsJson = Path.Combine(vsCodeDirectory, "settings.json");
  641. if (!m_FileIOProvider.Exists(vsCodeSettingsJson))
  642. m_FileIOProvider.WriteAllText(vsCodeSettingsJson, k_SettingsJson);
  643. }
  644. }
  645. public static class SolutionGuidGenerator
  646. {
  647. static MD5 mD5 = MD5CryptoServiceProvider.Create();
  648. public static string GuidForProject(string projectName)
  649. {
  650. return ComputeGuidHashFor(projectName + "salt");
  651. }
  652. public static string GuidForSolution(string projectName, string sourceFileExtension)
  653. {
  654. if (sourceFileExtension.ToLower() == "cs")
  655. // GUID for a C# class library: http://www.codeproject.com/Reference/720512/List-of-Visual-Studio-Project-Type-GUIDs
  656. return "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC";
  657. return ComputeGuidHashFor(projectName);
  658. }
  659. static string ComputeGuidHashFor(string input)
  660. {
  661. var hash = mD5.ComputeHash(Encoding.Default.GetBytes(input));
  662. return new Guid(hash).ToString();
  663. }
  664. }
  665. }