update readme (#21797)
[mono-project.git] / mcs / tools / xbuild / SolutionParser.cs
blob44ac70d2f963f328fd8bb2ad840912446e262454
1 //
2 // SolutionParser.cs: Generates a project file from a solution file.
3 //
4 // Author:
5 // Jonathan Chambers (joncham@gmail.com)
6 // Ankit Jain <jankit@novell.com>
7 // Lluis Sanchez Gual <lluis@novell.com>
8 //
9 // (C) 2009 Jonathan Chambers
10 // Copyright 2008, 2009 Novell, Inc (http://www.novell.com)
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 using System;
32 using System.Collections.Generic;
33 using System.Linq;
34 using System.Text;
35 using System.Text.RegularExpressions;
36 using System.IO;
37 using Microsoft.Build.BuildEngine;
39 namespace Mono.XBuild.CommandLine {
40 class ProjectInfo {
41 public string Name;
42 public string FileName;
43 public Guid Guid;
45 public ProjectInfo (string name, string fileName)
47 Name = name;
48 FileName = fileName;
51 public Dictionary<TargetInfo, TargetInfo> TargetMap = new Dictionary<TargetInfo, TargetInfo> ();
52 public Dictionary<Guid, ProjectInfo> Dependencies = new Dictionary<Guid, ProjectInfo> ();
53 public Dictionary<string, ProjectSection> ProjectSections = new Dictionary<string, ProjectSection> ();
54 public List<string> AspNetConfigurations = new List<string> ();
57 class ProjectSection {
58 public string Name;
59 public Dictionary<string, string> Properties = new Dictionary<string, string> ();
61 public ProjectSection (string name)
63 Name = name;
67 struct TargetInfo {
68 public string Configuration;
69 public string Platform;
70 public bool Build;
72 public TargetInfo (string configuration, string platform)
73 : this (configuration, platform, false)
77 public TargetInfo (string configuration, string platform, bool build)
79 Configuration = configuration;
80 Platform = platform;
81 Build = build;
86 internal delegate void RaiseWarningHandler (int errorNumber, string message);
88 class SolutionParser {
89 static string[] buildTargets = new string[] { "Build", "Clean", "Rebuild", "Publish" };
91 static string guidExpression = "{[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}}";
93 static Regex slnVersionRegex = new Regex (@"Microsoft Visual Studio Solution File, Format Version (\d?\d.\d\d)");
94 static Regex projectRegex = new Regex ("Project\\(\"(" + guidExpression + ")\"\\) = \"(.*?)\", \"(.*?)\", \"(" + guidExpression + ")\"(\\s*?)((\\s*?)ProjectSection\\((.*?)\\) = (.*?)EndProjectSection(\\s*?))*(\\s*?)(EndProject)?", RegexOptions.Singleline);
95 static Regex projectDependenciesRegex = new Regex ("ProjectSection\\((.*?)\\) = \\w*(.*?)EndProjectSection", RegexOptions.Singleline);
96 static Regex projectDependencyRegex = new Regex ("\\s*(" + guidExpression + ") = (" + guidExpression + ")");
97 static Regex projectSectionPropertiesRegex = new Regex ("\\s*(?<name>.*) = \"(?<value>.*)\"");
99 static Regex globalRegex = new Regex ("Global(.*)EndGlobal", RegexOptions.Singleline);
100 static Regex globalSectionRegex = new Regex ("GlobalSection\\((.*?)\\) = \\w*(.*?)EndGlobalSection", RegexOptions.Singleline);
102 static Regex solutionConfigurationRegex = new Regex ("\\s*(.*?)\\|(.*?) = (.*?)\\|(.+)");
103 static Regex projectConfigurationActiveCfgRegex = new Regex ("\\s*(" + guidExpression + ")\\.(.+?)\\|(.+?)\\.ActiveCfg = (.+?)\\|(.+)");
104 static Regex projectConfigurationBuildRegex = new Regex ("\\s*(" + guidExpression + ")\\.(.*?)\\|(.*?)\\.Build\\.0 = (.*?)\\|(.+)");
106 static string solutionFolderGuid = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}";
107 static string vcprojGuid = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}";
108 static string websiteProjectGuid = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}";
110 RaiseWarningHandler RaiseWarning;
112 public void ParseSolution (string file, Project p, RaiseWarningHandler RaiseWarning)
114 this.RaiseWarning = RaiseWarning;
115 EmitBeforeImports (p, file);
117 AddGeneralSettings (file, p);
119 StreamReader reader = new StreamReader (file);
121 string slnVersion = GetSlnFileVersion (reader);
123 if (slnVersion == "12.00")
124 #if XBUILD_14
125 p.DefaultToolsVersion = "14.0";
126 #elif XBUILD_12
127 p.DefaultToolsVersion = "12.0";
128 #else
129 p.DefaultToolsVersion = "4.0";
130 #endif
131 else if (slnVersion == "11.00")
132 p.DefaultToolsVersion = "4.0";
133 else if (slnVersion == "10.00")
134 p.DefaultToolsVersion = "3.5";
135 else
136 p.DefaultToolsVersion = "2.0";
138 string line = reader.ReadToEnd ();
139 line = line.Replace ("\r\n", "\n");
140 string solutionDir = Path.GetDirectoryName (file);
142 List<TargetInfo> solutionTargets = new List<TargetInfo> ();
143 Dictionary<Guid, ProjectInfo> projectInfos = new Dictionary<Guid, ProjectInfo> ();
144 Dictionary<Guid, ProjectInfo> websiteProjectInfos = new Dictionary<Guid, ProjectInfo> ();
145 List<ProjectInfo>[] infosByLevel = null;
146 Dictionary<Guid, ProjectInfo> unsupportedProjectInfos = new Dictionary<Guid, ProjectInfo> ();
148 Match m = projectRegex.Match (line);
149 while (m.Success) {
150 ProjectInfo projectInfo = new ProjectInfo (m.Groups[2].Value,
151 Path.GetFullPath (Path.Combine (solutionDir,
152 m.Groups [3].Value.Replace ('\\', Path.DirectorySeparatorChar))));
153 if (String.Compare (m.Groups [1].Value, solutionFolderGuid,
154 StringComparison.InvariantCultureIgnoreCase) == 0) {
155 // Ignore solution folders
156 m = m.NextMatch ();
157 continue;
160 projectInfo.Guid = new Guid (m.Groups [4].Value);
162 if (String.Compare (m.Groups [1].Value, vcprojGuid,
163 StringComparison.InvariantCultureIgnoreCase) == 0) {
164 // Ignore vcproj
165 RaiseWarning (0, string.Format("Ignoring vcproj '{0}'.", projectInfo.Name));
167 unsupportedProjectInfos [projectInfo.Guid] = projectInfo;
168 m = m.NextMatch ();
169 continue;
172 if (String.Compare (m.Groups [1].Value, websiteProjectGuid,
173 StringComparison.InvariantCultureIgnoreCase) == 0)
174 websiteProjectInfos.Add (new Guid (m.Groups[4].Value), projectInfo);
175 else
176 projectInfos.Add (projectInfo.Guid, projectInfo);
178 Match projectSectionMatch = projectDependenciesRegex.Match (m.Groups[6].Value);
179 while (projectSectionMatch.Success) {
180 string section_name = projectSectionMatch.Groups [1].Value;
181 if (String.Compare (section_name, "ProjectDependencies") == 0) {
182 Match projectDependencyMatch = projectDependencyRegex.Match (projectSectionMatch.Value);
183 while (projectDependencyMatch.Success) {
184 // we might not have projectInfo available right now, so
185 // set it to null, and fill it in later
186 projectInfo.Dependencies [new Guid (projectDependencyMatch.Groups[1].Value)] = null;
187 projectDependencyMatch = projectDependencyMatch.NextMatch ();
189 } else {
190 ProjectSection section = new ProjectSection (section_name);
191 Match propertiesMatch = projectSectionPropertiesRegex.Match (
192 projectSectionMatch.Groups [2].Value);
193 while (propertiesMatch.Success) {
194 section.Properties [propertiesMatch.Groups ["name"].Value] =
195 propertiesMatch.Groups ["value"].Value;
197 propertiesMatch = propertiesMatch.NextMatch ();
200 projectInfo.ProjectSections [section_name] = section;
202 projectSectionMatch = projectSectionMatch.NextMatch ();
204 m = m.NextMatch ();
207 foreach (ProjectInfo projectInfo in projectInfos.Values) {
208 string filename = projectInfo.FileName;
209 string projectDir = Path.GetDirectoryName (filename);
211 if (!File.Exists (filename)) {
212 RaiseWarning (0, String.Format ("Project file {0} referenced in the solution file, " +
213 "not found. Ignoring.", filename));
214 continue;
217 Project currentProject = p.ParentEngine.CreateNewProject ();
218 try {
219 currentProject.Load (filename, ProjectLoadSettings.IgnoreMissingImports);
220 } catch (InvalidProjectFileException e) {
221 RaiseWarning (0, e.Message);
222 continue;
225 foreach (BuildItem bi in currentProject.GetEvaluatedItemsByName ("ProjectReference")) {
226 ProjectInfo info = null;
227 string projectReferenceGuid = bi.GetEvaluatedMetadata ("Project");
228 bool hasGuid = !String.IsNullOrEmpty (projectReferenceGuid);
230 // try to resolve the ProjectReference by GUID
231 // and fallback to project filename
233 if (hasGuid) {
234 Guid guid = new Guid (projectReferenceGuid);
235 projectInfos.TryGetValue (guid, out info);
236 if (info == null && unsupportedProjectInfos.TryGetValue (guid, out info)) {
237 RaiseWarning (0, String.Format (
238 "{0}: ProjectReference '{1}' is of an unsupported type. Ignoring.",
239 filename, bi.Include));
240 continue;
244 if (info == null || !hasGuid) {
245 // Project not found by guid or guid not available
246 // Try to find by project file
248 string fullpath = Path.GetFullPath (Path.Combine (projectDir, bi.Include.Replace ('\\', Path.DirectorySeparatorChar)));
249 info = projectInfos.Values.FirstOrDefault (pi => pi.FileName == fullpath);
251 if (info == null) {
252 if (unsupportedProjectInfos.Values.Any (pi => pi.FileName == fullpath))
253 RaiseWarning (0, String.Format (
254 "{0}: ProjectReference '{1}' is of an unsupported type. Ignoring.",
255 filename, bi.Include));
256 else
257 RaiseWarning (0, String.Format (
258 "{0}: ProjectReference '{1}' not found, neither by guid '{2}' nor by project file name '{3}'.",
259 filename, bi.Include, projectReferenceGuid.Replace ("{", "").Replace ("}", ""), fullpath));
264 if (info != null)
265 projectInfo.Dependencies [info.Guid] = info;
268 // unload the project after reading info from it
269 // it'll be reloaded with proper context when building the solution
270 p.ParentEngine.UnloadProject (currentProject);
273 // fill in the project info for deps found in the .sln file
274 foreach (ProjectInfo projectInfo in projectInfos.Values) {
275 List<Guid> missingInfos = new List<Guid> ();
276 foreach (KeyValuePair<Guid, ProjectInfo> dependency in projectInfo.Dependencies) {
277 if (dependency.Value == null)
278 missingInfos.Add (dependency.Key);
281 foreach (Guid guid in missingInfos) {
282 ProjectInfo info;
283 if (projectInfos.TryGetValue (guid, out info))
284 projectInfo.Dependencies [guid] = info;
285 else
286 projectInfo.Dependencies.Remove (guid);
290 Match globalMatch = globalRegex.Match (line);
291 Match globalSectionMatch = globalSectionRegex.Match (globalMatch.Groups[1].Value);
292 while (globalSectionMatch.Success) {
293 string sectionType = globalSectionMatch.Groups[1].Value;
294 switch (sectionType) {
295 case "SolutionConfigurationPlatforms":
296 ParseSolutionConfigurationPlatforms (globalSectionMatch.Groups[2].Value, solutionTargets);
297 break;
298 case "ProjectConfigurationPlatforms":
299 ParseProjectConfigurationPlatforms (globalSectionMatch.Groups[2].Value,
300 projectInfos, websiteProjectInfos);
301 break;
302 case "SolutionProperties":
303 ParseSolutionProperties (globalSectionMatch.Groups[2].Value);
304 break;
305 case "NestedProjects":
306 break;
307 case "MonoDevelopProperties":
308 break;
309 default:
310 RaiseWarning (0, string.Format("Don't know how to handle GlobalSection {0}, Ignoring.", sectionType));
311 break;
313 globalSectionMatch = globalSectionMatch.NextMatch ();
316 int num_levels = AddBuildLevels (p, solutionTargets, projectInfos, ref infosByLevel);
318 AddCurrentSolutionConfigurationContents (p, solutionTargets, projectInfos, websiteProjectInfos);
319 AddProjectReferences (p, projectInfos);
320 AddWebsiteProperties (p, websiteProjectInfos, projectInfos);
321 AddValidateSolutionConfiguration (p);
323 EmitAfterImports (p, file);
325 AddGetFrameworkPathTarget (p);
326 AddWebsiteTargets (p, websiteProjectInfos, projectInfos, infosByLevel, solutionTargets);
327 AddProjectTargets (p, solutionTargets, projectInfos);
328 AddSolutionTargets (p, num_levels, websiteProjectInfos.Values);
331 string GetSlnFileVersion (StreamReader reader)
333 string strInput = null;
334 Match match;
336 strInput = reader.ReadLine();
337 if (strInput == null)
338 return null;
340 match = slnVersionRegex.Match(strInput);
341 if (!match.Success) {
342 strInput = reader.ReadLine();
343 if (strInput == null)
344 return null;
345 match = slnVersionRegex.Match (strInput);
348 if (match.Success)
349 return match.Groups[1].Value;
351 return null;
354 void EmitBeforeImports (Project p, string file)
356 p.AddNewImport ("$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\SolutionFile\\ImportBefore\\*",
357 "'$(ImportByWildcardBeforeSolution)' != 'false' and " +
358 "Exists('$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\SolutionFile\\ImportBefore')");
360 string before_filename = Path.Combine (Path.GetDirectoryName (file), "before." + Path.GetFileName (file) + ".targets");
361 p.AddNewImport (before_filename, String.Format ("Exists ('{0}')", before_filename));
364 void EmitAfterImports (Project p, string file)
366 p.AddNewImport ("$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\SolutionFile\\ImportAfter\\*",
367 "'$(ImportByWildcardAfterSolution)' != 'false' and " +
368 "Exists('$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\SolutionFile\\ImportAfter')");
370 string after_filename = Path.Combine (Path.GetDirectoryName (file), "after." + Path.GetFileName (file) + ".targets");
371 p.AddNewImport (after_filename, String.Format ("Exists ('{0}')", after_filename));
374 void AddGeneralSettings (string solutionFile, Project p)
376 p.DefaultTargets = "Build";
377 p.InitialTargets = "ValidateSolutionConfiguration";
378 p.AddNewUsingTaskFromAssemblyName ("CreateTemporaryVCProject", "Microsoft.Build.Tasks, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
379 p.AddNewUsingTaskFromAssemblyName ("ResolveVCProjectOutput", "Microsoft.Build.Tasks, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
381 string solutionFilePath = Path.GetFullPath (solutionFile);
382 BuildPropertyGroup solutionPropertyGroup = p.AddNewPropertyGroup (true);
383 solutionPropertyGroup.AddNewProperty ("SolutionDir", Path.GetDirectoryName (solutionFilePath) + Path.DirectorySeparatorChar);
384 solutionPropertyGroup.AddNewProperty ("SolutionExt", Path.GetExtension (solutionFile));
385 solutionPropertyGroup.AddNewProperty ("SolutionFileName", Path.GetFileName (solutionFile));
386 solutionPropertyGroup.AddNewProperty ("SolutionName", Path.GetFileNameWithoutExtension (solutionFile));
387 solutionPropertyGroup.AddNewProperty ("SolutionPath", solutionFilePath);
390 void ParseSolutionConfigurationPlatforms (string section, List<TargetInfo> solutionTargets)
392 Match solutionConfigurationPlatform = solutionConfigurationRegex.Match (section);
393 while (solutionConfigurationPlatform.Success) {
394 string solutionConfiguration = solutionConfigurationPlatform.Groups[1].Value;
395 string solutionPlatform = solutionConfigurationPlatform.Groups[2].Value;
396 solutionTargets.Add (new TargetInfo (solutionConfiguration, solutionPlatform));
397 solutionConfigurationPlatform = solutionConfigurationPlatform.NextMatch ();
401 // ignores the website projects, in the websiteProjectInfos
402 void ParseProjectConfigurationPlatforms (string section, Dictionary<Guid, ProjectInfo> projectInfos,
403 Dictionary<Guid, ProjectInfo> websiteProjectInfos)
405 List<Guid> missingGuids = new List<Guid> ();
406 Match projectConfigurationPlatform = projectConfigurationActiveCfgRegex.Match (section);
407 while (projectConfigurationPlatform.Success) {
408 Guid guid = new Guid (projectConfigurationPlatform.Groups[1].Value);
409 ProjectInfo projectInfo;
410 if (!projectInfos.TryGetValue (guid, out projectInfo)) {
411 if (!missingGuids.Contains (guid)) {
412 if (!websiteProjectInfos.ContainsKey (guid))
413 // ignore website projects
414 RaiseWarning (0, string.Format("Failed to find project {0}", guid));
415 missingGuids.Add (guid);
417 projectConfigurationPlatform = projectConfigurationPlatform.NextMatch ();
418 continue;
420 string solConf = projectConfigurationPlatform.Groups[2].Value;
421 string solPlat = projectConfigurationPlatform.Groups[3].Value;
422 string projConf = projectConfigurationPlatform.Groups[4].Value;
423 string projPlat = projectConfigurationPlatform.Groups[5].Value;
424 // hack, what are they doing here?
425 if (projPlat == "Any CPU")
426 projPlat = "AnyCPU";
427 projectInfo.TargetMap.Add (new TargetInfo (solConf, solPlat), new TargetInfo (projConf, projPlat));
428 projectConfigurationPlatform = projectConfigurationPlatform.NextMatch ();
430 Match projectConfigurationPlatformBuild = projectConfigurationBuildRegex.Match (section);
431 while (projectConfigurationPlatformBuild.Success) {
432 Guid guid = new Guid (projectConfigurationPlatformBuild.Groups[1].Value);
433 ProjectInfo projectInfo;
434 if (!projectInfos.TryGetValue (guid, out projectInfo)) {
435 if (!missingGuids.Contains (guid)) {
436 RaiseWarning (0, string.Format("Failed to find project {0}", guid));
437 missingGuids.Add (guid);
439 projectConfigurationPlatformBuild = projectConfigurationPlatformBuild.NextMatch ();
440 continue;
442 string solConf = projectConfigurationPlatformBuild.Groups[2].Value;
443 string solPlat = projectConfigurationPlatformBuild.Groups[3].Value;
444 string projConf = projectConfigurationPlatformBuild.Groups[4].Value;
445 string projPlat = projectConfigurationPlatformBuild.Groups[5].Value;
446 // hack, what are they doing here?
447 if (projPlat == "Any CPU")
448 projPlat = "AnyCPU";
449 projectInfo.TargetMap[new TargetInfo (solConf, solPlat)] = new TargetInfo (projConf, projPlat, true);
450 projectConfigurationPlatformBuild = projectConfigurationPlatformBuild.NextMatch ();
454 void ParseSolutionProperties (string section)
458 void AddCurrentSolutionConfigurationContents (Project p, List<TargetInfo> solutionTargets,
459 Dictionary<Guid, ProjectInfo> projectInfos,
460 Dictionary<Guid, ProjectInfo> websiteProjectInfos)
462 TargetInfo default_target_info = new TargetInfo ("Debug", "Any CPU");
463 if (solutionTargets.Count > 0) {
464 bool found = false;
465 foreach (TargetInfo tinfo in solutionTargets) {
466 if (String.Compare (tinfo.Platform, "Mixed Platforms") == 0) {
467 default_target_info = tinfo;
468 found = true;
469 break;
473 if (!found)
474 default_target_info = solutionTargets [0];
477 AddDefaultSolutionConfiguration (p, default_target_info);
479 foreach (TargetInfo solutionTarget in solutionTargets) {
480 BuildPropertyGroup platformPropertyGroup = p.AddNewPropertyGroup (false);
481 platformPropertyGroup.Condition = string.Format (
482 " ('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}') ",
483 solutionTarget.Configuration,
484 solutionTarget.Platform
487 StringBuilder solutionConfigurationContents = new StringBuilder ();
488 solutionConfigurationContents.Append ("<SolutionConfiguration xmlns=\"\">");
489 foreach (KeyValuePair<Guid, ProjectInfo> projectInfo in projectInfos) {
490 AddProjectConfigurationItems (projectInfo.Key, projectInfo.Value, solutionTarget, solutionConfigurationContents);
492 solutionConfigurationContents.Append ("</SolutionConfiguration>");
494 platformPropertyGroup.AddNewProperty ("CurrentSolutionConfigurationContents",
495 solutionConfigurationContents.ToString ());
499 void AddProjectReferences (Project p, Dictionary<Guid, ProjectInfo> projectInfos)
501 BuildItemGroup big = p.AddNewItemGroup ();
502 foreach (KeyValuePair<Guid, ProjectInfo> pair in projectInfos)
503 big.AddNewItem ("ProjectReference", pair.Value.FileName);
506 void AddProjectConfigurationItems (Guid guid, ProjectInfo projectInfo, TargetInfo solutionTarget,
507 StringBuilder solutionConfigurationContents)
509 foreach (KeyValuePair<TargetInfo, TargetInfo> targetInfo in projectInfo.TargetMap) {
510 if (solutionTarget.Configuration == targetInfo.Key.Configuration &&
511 solutionTarget.Platform == targetInfo.Key.Platform) {
512 solutionConfigurationContents.AppendFormat (
513 "<ProjectConfiguration Project=\"{0}\" AbsolutePath=\"{1}\">{2}|{3}</ProjectConfiguration>",
514 guid.ToString ("B").ToUpper (), projectInfo.FileName, targetInfo.Value.Configuration, targetInfo.Value.Platform);
519 void AddDefaultSolutionConfiguration (Project p, TargetInfo target)
521 BuildPropertyGroup configurationPropertyGroup = p.AddNewPropertyGroup (true);
522 configurationPropertyGroup.Condition = " '$(Configuration)' == '' ";
523 configurationPropertyGroup.AddNewProperty ("Configuration", target.Configuration);
525 BuildPropertyGroup platformPropertyGroup = p.AddNewPropertyGroup (true);
526 platformPropertyGroup.Condition = " '$(Platform)' == '' ";
527 platformPropertyGroup.AddNewProperty ("Platform", target.Platform);
529 // emit default for AspNetConfiguration also
530 BuildPropertyGroup aspNetConfigurationPropertyGroup = p.AddNewPropertyGroup (true);
531 aspNetConfigurationPropertyGroup.Condition = " ('$(AspNetConfiguration)' == '') ";
532 aspNetConfigurationPropertyGroup.AddNewProperty ("AspNetConfiguration", "$(Configuration)");
535 void AddWarningForMissingProjectConfiguration (Target target, string slnConfig, string slnPlatform, string projectName)
537 BuildTask task = target.AddNewTask ("Warning");
538 task.SetParameterValue ("Text",
539 String.Format ("The project configuration for project '{0}' corresponding " +
540 "to the solution configuration '{1}|{2}' was not found in the solution file.",
541 projectName, slnConfig, slnPlatform));
542 task.Condition = String.Format ("('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}')",
543 slnConfig, slnPlatform);
547 // Website project methods
549 void AddWebsiteProperties (Project p, Dictionary<Guid, ProjectInfo> websiteProjectInfos,
550 Dictionary<Guid, ProjectInfo> projectInfos)
552 var propertyGroupByConfig = new Dictionary<string, BuildPropertyGroup> ();
553 foreach (KeyValuePair<Guid, ProjectInfo> infoPair in websiteProjectInfos) {
554 ProjectInfo info = infoPair.Value;
555 string projectGuid = infoPair.Key.ToString ();
557 ProjectSection section;
558 if (!info.ProjectSections.TryGetValue ("WebsiteProperties", out section)) {
559 RaiseWarning (0, String.Format ("Website project '{0}' does not have the required project section: WebsiteProperties. Ignoring project.", info.Name));
560 return;
563 //parse project references
564 string [] ref_guids = null;
565 string references;
566 if (section.Properties.TryGetValue ("ProjectReferences", out references)) {
567 ref_guids = references.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries);
568 for (int i = 0; i < ref_guids.Length; i ++) {
569 // "{guid}|foo.dll"
570 ref_guids [i] = ref_guids [i].Split ('|') [0];
572 Guid r_guid = new Guid (ref_guids [i]);
573 ProjectInfo ref_info;
574 if (projectInfos.TryGetValue (r_guid, out ref_info))
575 // ignore if not found
576 info.Dependencies [r_guid] = ref_info;
580 foreach (KeyValuePair<string, string> pair in section.Properties) {
581 //looking for -- ConfigName.AspNetCompiler.PropName
582 string [] parts = pair.Key.Split ('.');
583 if (parts.Length != 3 || String.Compare (parts [1], "AspNetCompiler") != 0)
584 continue;
586 string config = parts [0];
587 string propertyName = parts [2];
589 BuildPropertyGroup bpg;
590 if (!propertyGroupByConfig.TryGetValue (config, out bpg)) {
591 bpg = p.AddNewPropertyGroup (true);
592 bpg.Condition = String.Format (" '$(AspNetConfiguration)' == '{0}' ", config);
593 propertyGroupByConfig [config] = bpg;
596 bpg.AddNewProperty (String.Format ("Project_{0}_AspNet{1}", projectGuid, propertyName),
597 pair.Value);
599 if (!info.AspNetConfigurations.Contains (config))
600 info.AspNetConfigurations.Add (config);
605 // For WebSite projects
606 // The main "Build" target:
607 // 1. builds all non-website projects
608 // 2. calls target for website project
609 // - gets target path for the referenced projects
610 // - Resolves dependencies, satellites etc for the
611 // referenced project assemblies, and copies them
612 // to bin/ folder
613 void AddWebsiteTargets (Project p, Dictionary<Guid, ProjectInfo> websiteProjectInfos,
614 Dictionary<Guid, ProjectInfo> projectInfos, List<ProjectInfo>[] infosByLevel,
615 List<TargetInfo> solutionTargets)
617 foreach (ProjectInfo w_info in websiteProjectInfos.Values) {
618 // gets a linear list of dependencies
619 List<ProjectInfo> depInfos = new List<ProjectInfo> ();
620 foreach (List<ProjectInfo> pinfos in infosByLevel) {
621 foreach (ProjectInfo pinfo in pinfos)
622 if (w_info.Dependencies.ContainsKey (pinfo.Guid))
623 depInfos.Add (pinfo);
626 foreach (string buildTarget in new string [] {"Build", "Rebuild"})
627 AddWebsiteTarget (p, w_info, projectInfos, depInfos, solutionTargets, buildTarget);
629 // clean/publish are not supported for website projects
630 foreach (string buildTarget in new string [] {"Clean", "Publish"})
631 AddWebsiteUnsupportedTarget (p, w_info, depInfos, buildTarget);
635 void AddWebsiteTarget (Project p, ProjectInfo webProjectInfo,
636 Dictionary<Guid, ProjectInfo> projectInfos, List<ProjectInfo> depInfos,
637 List<TargetInfo> solutionTargets, string buildTarget)
639 string w_guid = webProjectInfo.Guid.ToString ().ToUpper ();
641 Target target = p.Targets.AddNewTarget (GetTargetNameForProject (webProjectInfo.Name, buildTarget));
642 target.Condition = "'$(CurrentSolutionConfigurationContents)' != ''";
643 target.DependsOnTargets = GetWebsiteDependsOnTarget (depInfos, buildTarget);
645 // this item collects all the references
646 string final_ref_item = String.Format ("Project_{0}_References{1}", w_guid,
647 buildTarget != "Build" ? "_" + buildTarget : String.Empty);
649 foreach (TargetInfo targetInfo in solutionTargets) {
650 int ref_num = 0;
651 foreach (ProjectInfo depInfo in depInfos) {
652 TargetInfo projectTargetInfo;
653 if (!depInfo.TargetMap.TryGetValue (targetInfo, out projectTargetInfo))
654 // Ignore, no config, so no target path
655 continue;
657 // GetTargetPath from the referenced project
658 AddWebsiteMSBuildTaskForReference (target, depInfo, projectTargetInfo, targetInfo,
659 final_ref_item, ref_num);
660 ref_num ++;
664 // resolve the references
665 AddWebsiteResolveAndCopyReferencesTasks (target, webProjectInfo, final_ref_item, w_guid);
668 // emits the MSBuild task to GetTargetPath for the referenced project
669 void AddWebsiteMSBuildTaskForReference (Target target, ProjectInfo depInfo, TargetInfo projectTargetInfo,
670 TargetInfo solutionTargetInfo, string final_ref_item, int ref_num)
672 BuildTask task = target.AddNewTask ("MSBuild");
673 task.SetParameterValue ("Projects", depInfo.FileName);
674 task.SetParameterValue ("Targets", "GetTargetPath");
676 task.SetParameterValue ("Properties", string.Format ("Configuration={0}; Platform={1}; BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)", projectTargetInfo.Configuration, projectTargetInfo.Platform));
677 task.Condition = string.Format (" ('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}') ", solutionTargetInfo.Configuration, solutionTargetInfo.Platform);
679 string ref_item = String.Format ("{0}_{1}",
680 final_ref_item, ref_num);
682 task.AddOutputItem ("TargetOutputs", ref_item);
684 task = target.AddNewTask ("CreateItem");
685 task.SetParameterValue ("Include", String.Format ("@({0})", ref_item));
686 task.SetParameterValue ("AdditionalMetadata", String.Format ("Guid={{{0}}}",
687 depInfo.Guid.ToString ().ToUpper ()));
688 task.AddOutputItem ("Include", final_ref_item);
691 void AddWebsiteResolveAndCopyReferencesTasks (Target target, ProjectInfo webProjectInfo,
692 string final_ref_item, string w_guid)
694 BuildTask task = target.AddNewTask ("ResolveAssemblyReference");
695 task.SetParameterValue ("Assemblies", String.Format ("@({0}->'%(FullPath)')", final_ref_item));
696 task.SetParameterValue ("TargetFrameworkDirectories", "$(TargetFrameworkPath)");
697 task.SetParameterValue ("SearchPaths", "{RawFileName};{TargetFrameworkDirectory};{GAC}");
698 task.SetParameterValue ("FindDependencies", "true");
699 task.SetParameterValue ("FindSatellites", "true");
700 task.SetParameterValue ("FindRelatedFiles", "true");
701 task.Condition = String.Format ("Exists ('%({0}.Identity)')", final_ref_item);
703 string copylocal_item = String.Format ("{0}_CopyLocalFiles", final_ref_item);
704 task.AddOutputItem ("CopyLocalFiles", copylocal_item);
706 // Copy the references
707 task = target.AddNewTask ("Copy");
708 task.SetParameterValue ("SourceFiles", String.Format ("@({0})", copylocal_item));
709 task.SetParameterValue ("DestinationFiles", String.Format (
710 "@({0}->'$(Project_{1}_AspNetPhysicalPath)\\Bin\\%(DestinationSubDirectory)%(Filename)%(Extension)')",
711 copylocal_item, w_guid));
713 // AspNetConfiguration, is config for the website project, useful
714 // for overriding from command line
715 StringBuilder cond = new StringBuilder ();
716 foreach (string config in webProjectInfo.AspNetConfigurations) {
717 if (cond.Length > 0)
718 cond.Append (" or ");
719 cond.AppendFormat (" ('$(AspNetConfiguration)' == '{0}') ", config);
721 task.Condition = cond.ToString ();
723 task = target.AddNewTask ("Message");
724 cond = new StringBuilder ();
725 foreach (string config in webProjectInfo.AspNetConfigurations) {
726 if (cond.Length > 0)
727 cond.Append (" and ");
728 cond.AppendFormat (" ('$(AspNetConfiguration)' != '{0}') ", config);
730 task.Condition = cond.ToString ();
731 task.SetParameterValue ("Text", "Skipping as the '$(AspNetConfiguration)' configuration is " +
732 "not supported by this website project.");
735 void AddWebsiteUnsupportedTarget (Project p, ProjectInfo webProjectInfo, List<ProjectInfo> depInfos,
736 string buildTarget)
738 Target target = p.Targets.AddNewTarget (GetTargetNameForProject (webProjectInfo.Name, buildTarget));
739 target.DependsOnTargets = GetWebsiteDependsOnTarget (depInfos, buildTarget);
741 BuildTask task = target.AddNewTask ("Message");
742 task.SetParameterValue ("Text", String.Format (
743 "Target '{0}' not support for website projects", buildTarget));
746 string GetWebsiteDependsOnTarget (List<ProjectInfo> depInfos, string buildTarget)
748 StringBuilder deps = new StringBuilder ();
749 foreach (ProjectInfo pinfo in depInfos) {
750 if (deps.Length > 0)
751 deps.Append (";");
752 deps.Append (GetTargetNameForProject (pinfo.Name, buildTarget));
754 deps.Append (";GetFrameworkPath");
755 return deps.ToString ();
758 void AddGetFrameworkPathTarget (Project p)
760 Target t = p.Targets.AddNewTarget ("GetFrameworkPath");
761 BuildTask task = t.AddNewTask ("GetFrameworkPath");
762 task.AddOutputProperty ("Path", "TargetFrameworkPath");
765 void AddValidateSolutionConfiguration (Project p)
767 Target t = p.Targets.AddNewTarget ("ValidateSolutionConfiguration");
768 BuildTask task = t.AddNewTask ("Warning");
769 task.SetParameterValue ("Text", "On windows, an environment variable 'Platform' is set to MCD sometimes, and this overrides the Platform property" +
770 " for xbuild, which could be an invalid Platform for this solution file. And so you are getting the following error." +
771 " You could override it by either setting the environment variable to nothing, as\n" +
772 " set Platform=\n" +
773 "Or explicity specify its value on the command line, as\n" +
774 " xbuild Foo.sln /p:Platform=Release");
775 task.Condition = "('$(CurrentSolutionConfigurationContents)' == '') and ('$(SkipInvalidConfigurations)' != 'true')" +
776 " and '$(Platform)' == 'MCD' and '$(OS)' == 'Windows_NT'";
778 task = t.AddNewTask ("Error");
779 task.SetParameterValue ("Text", "Invalid solution configuration and platform: \"$(Configuration)|$(Platform)\".");
780 task.Condition = "('$(CurrentSolutionConfigurationContents)' == '') and ('$(SkipInvalidConfigurations)' != 'true')";
781 task = t.AddNewTask ("Warning");
782 task.SetParameterValue ("Text", "Invalid solution configuration and platform: \"$(Configuration)|$(Platform)\".");
783 task.Condition = "('$(CurrentSolutionConfigurationContents)' == '') and ('$(SkipInvalidConfigurations)' == 'true')";
784 task = t.AddNewTask ("Message");
785 task.SetParameterValue ("Text", "Building solution configuration \"$(Configuration)|$(Platform)\".");
786 task.Condition = "'$(CurrentSolutionConfigurationContents)' != ''";
789 void AddProjectTargets (Project p, List<TargetInfo> solutionTargets, Dictionary<Guid, ProjectInfo> projectInfos)
791 foreach (KeyValuePair<Guid, ProjectInfo> projectInfo in projectInfos) {
792 ProjectInfo project = projectInfo.Value;
793 foreach (string buildTarget in buildTargets) {
794 string target_name = GetTargetNameForProject (project.Name, buildTarget);
795 bool is_build_or_rebuild = buildTarget == "Build" || buildTarget == "Rebuild";
796 Target target = p.Targets.AddNewTarget (target_name);
797 target.Condition = "'$(CurrentSolutionConfigurationContents)' != ''";
799 if (is_build_or_rebuild)
800 target.Outputs = "@(CollectedBuildOutput)";
801 if (project.Dependencies.Count > 0)
802 target.DependsOnTargets = String.Join (";",
803 project.Dependencies.Values.Select (
804 di => GetTargetNameForProject (di.Name, buildTarget)).ToArray ());
806 foreach (TargetInfo targetInfo in solutionTargets) {
807 BuildTask task = null;
808 TargetInfo projectTargetInfo;
809 if (!project.TargetMap.TryGetValue (targetInfo, out projectTargetInfo)) {
810 AddWarningForMissingProjectConfiguration (target, targetInfo.Configuration,
811 targetInfo.Platform, project.Name);
812 continue;
814 if (projectTargetInfo.Build) {
815 task = target.AddNewTask ("MSBuild");
816 task.SetParameterValue ("Projects", project.FileName);
817 task.SetParameterValue ("ToolsVersion", "$(ProjectToolsVersion)");
818 if (is_build_or_rebuild)
819 task.AddOutputItem ("TargetOutputs", "CollectedBuildOutput");
821 if (buildTarget != "Build")
822 task.SetParameterValue ("Targets", buildTarget);
823 task.SetParameterValue ("Properties", string.Format ("Configuration={0}; Platform={1}; BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)", projectTargetInfo.Configuration, projectTargetInfo.Platform));
824 } else {
825 task = target.AddNewTask ("Message");
826 task.SetParameterValue ("Text", string.Format ("Project \"{0}\" is disabled for solution configuration \"{1}|{2}\".", project.Name, targetInfo.Configuration, targetInfo.Platform));
828 task.Condition = string.Format (" ('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}') ", targetInfo.Configuration, targetInfo.Platform);
835 string GetTargetNameForProject (string projectName, string buildTarget)
837 //FIXME: hack
838 projectName = projectName.Replace ("\\", "/").Replace (".", "_");
839 string target_name = projectName +
840 (buildTarget == "Build" ? string.Empty : ":" + buildTarget);
842 if (IsBuildTargetName (projectName))
843 target_name = "Solution:" + target_name;
845 return target_name;
848 bool IsBuildTargetName (string name)
850 foreach (string tgt in buildTargets)
851 if (name == tgt)
852 return true;
853 return false;
856 // returns number of levels
857 int AddBuildLevels (Project p, List<TargetInfo> solutionTargets, Dictionary<Guid, ProjectInfo> projectInfos,
858 ref List<ProjectInfo>[] infosByLevel)
860 infosByLevel = TopologicalSort<ProjectInfo> (projectInfos.Values);
862 foreach (TargetInfo targetInfo in solutionTargets) {
863 BuildItemGroup big = p.AddNewItemGroup ();
864 big.Condition = String.Format (" ('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}') ",
865 targetInfo.Configuration, targetInfo.Platform);
867 //FIXME: every level has projects that can be built in parallel.
868 // levels are ordered on the basis of the dependency graph
870 for (int i = 0; i < infosByLevel.Length; i ++) {
871 string build_level = String.Format ("BuildLevel{0}", i);
872 string skip_level = String.Format ("SkipLevel{0}", i);
873 string missing_level = String.Format ("MissingConfigLevel{0}", i);
875 foreach (ProjectInfo projectInfo in infosByLevel [i]) {
876 TargetInfo projectTargetInfo;
877 if (!projectInfo.TargetMap.TryGetValue (targetInfo, out projectTargetInfo)) {
878 // missing project config
879 big.AddNewItem (missing_level, projectInfo.Name);
880 continue;
883 if (projectTargetInfo.Build) {
884 BuildItem item = big.AddNewItem (build_level, projectInfo.FileName);
885 item.SetMetadata ("Configuration", projectTargetInfo.Configuration);
886 item.SetMetadata ("Platform", projectTargetInfo.Platform);
887 } else {
888 // build disabled
889 big.AddNewItem (skip_level, projectInfo.Name);
895 return infosByLevel.Length;
898 void AddSolutionTargets (Project p, int num_levels, IEnumerable<ProjectInfo> websiteProjectInfos)
900 foreach (string buildTarget in buildTargets) {
901 Target t = p.Targets.AddNewTarget (buildTarget);
902 bool is_build_or_rebuild = buildTarget == "Build" || buildTarget == "Rebuild";
904 t.Condition = "'$(CurrentSolutionConfigurationContents)' != ''";
905 if (is_build_or_rebuild)
906 t.Outputs = "@(CollectedBuildOutput)";
908 BuildTask task = null;
909 for (int i = 0; i < num_levels; i ++) {
910 string level_str = String.Format ("BuildLevel{0}", i);
911 task = t.AddNewTask ("MSBuild");
912 task.SetParameterValue ("Condition", String.Format ("'@({0})' != ''", level_str));
913 task.SetParameterValue ("Projects", String.Format ("@({0})", level_str));
914 task.SetParameterValue ("ToolsVersion", "$(ProjectToolsVersion)");
915 task.SetParameterValue ("Properties",
916 string.Format ("Configuration=%(Configuration); Platform=%(Platform); BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)"));
917 if (buildTarget != "Build")
918 task.SetParameterValue ("Targets", buildTarget);
919 //FIXME: change this to BuildInParallel=true, when parallel
920 // build support gets added
921 task.SetParameterValue ("RunEachTargetSeparately", "true");
922 if (is_build_or_rebuild)
923 task.AddOutputItem ("TargetOutputs", "CollectedBuildOutput");
925 level_str = String.Format ("SkipLevel{0}", i);
926 task = t.AddNewTask ("Message");
927 task.Condition = String.Format ("'@({0})' != ''", level_str);
928 task.SetParameterValue ("Text",
929 String.Format ("The project '%({0}.Identity)' is disabled for solution " +
930 "configuration '$(Configuration)|$(Platform)'.", level_str));
932 level_str = String.Format ("MissingConfigLevel{0}", i);
933 task = t.AddNewTask ("Warning");
934 task.Condition = String.Format ("'@({0})' != ''", level_str);
935 task.SetParameterValue ("Text",
936 String.Format ("The project configuration for project '%({0}.Identity)' " +
937 "corresponding to the solution configuration " +
938 "'$(Configuration)|$(Platform)' was not found.", level_str));
941 // "build" website projects also
942 StringBuilder w_targets = new StringBuilder ();
943 foreach (ProjectInfo info in websiteProjectInfos) {
944 if (w_targets.Length > 0)
945 w_targets.Append (";");
946 w_targets.Append (GetTargetNameForProject (info.Name, buildTarget));
949 task = t.AddNewTask ("CallTarget");
950 task.SetParameterValue ("Targets", w_targets.ToString ());
951 task.SetParameterValue ("RunEachTargetSeparately", "true");
955 // Sorts the ProjectInfo dependency graph, to obtain
956 // a series of build levels with projects. Projects
957 // in each level can be run parallel (no inter-dependency).
958 static List<T>[] TopologicalSort<T> (IEnumerable<T> items) where T: ProjectInfo
960 IList<T> allItems;
961 allItems = items as IList<T>;
962 if (allItems == null)
963 allItems = new List<T> (items);
965 bool[] inserted = new bool[allItems.Count];
966 bool[] triedToInsert = new bool[allItems.Count];
967 int[] levels = new int [allItems.Count];
969 int maxdepth = 0;
970 for (int i = 0; i < allItems.Count; ++i) {
971 int d = Insert<T> (i, allItems, levels, inserted, triedToInsert);
972 if (d > maxdepth)
973 maxdepth = d;
976 // Separate out the project infos by build level
977 List<T>[] infosByLevel = new List<T>[maxdepth];
978 for (int i = 0; i < levels.Length; i ++) {
979 int level = levels [i] - 1;
980 if (infosByLevel [level] == null)
981 infosByLevel [level] = new List<T> ();
983 infosByLevel [level].Add (allItems [i]);
986 return infosByLevel;
989 // returns level# for the project
990 static int Insert<T> (int index, IList<T> allItems, int[] levels, bool[] inserted, bool[] triedToInsert)
991 where T: ProjectInfo
993 if (inserted [index])
994 return levels [index];
996 if (triedToInsert[index])
997 throw new InvalidOperationException (String.Format (
998 "Cyclic dependency involving project {0} found in the project dependency graph",
999 allItems [index].Name));
1001 triedToInsert[index] = true;
1002 ProjectInfo insertItem = allItems[index];
1004 int maxdepth = 0;
1005 foreach (ProjectInfo dependency in insertItem.Dependencies.Values) {
1006 for (int j = 0; j < allItems.Count; ++j) {
1007 ProjectInfo checkItem = allItems [j];
1008 if (dependency.FileName == checkItem.FileName) {
1009 int d = Insert (j, allItems, levels, inserted, triedToInsert);
1010 maxdepth = d > maxdepth ? d : maxdepth;
1011 break;
1015 levels [index] = maxdepth + 1;
1016 inserted [index] = true;
1018 return levels [index];
1021 public static IEnumerable<string> GetAllProjectFileNames (string solutionFile)
1023 StreamReader reader = new StreamReader (solutionFile);
1024 string line = reader.ReadToEnd ();
1025 line = line.Replace ("\r\n", "\n");
1026 string soln_dir = Path.GetDirectoryName (solutionFile);
1028 Match m = projectRegex.Match (line);
1029 while (m.Success) {
1030 if (String.Compare (m.Groups [1].Value, solutionFolderGuid,
1031 StringComparison.InvariantCultureIgnoreCase) != 0)
1032 yield return Path.Combine (soln_dir, m.Groups [3].Value).Replace ("\\", "/");
1034 m = m.NextMatch ();