ProjectGeneration.cs 33 KB

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