2 // SolutionParser.cs: Generates a project file from a solution file.
5 // Jonathan Chambers (joncham@gmail.com)
6 // Ankit Jain <jankit@novell.com>
7 // Lluis Sanchez Gual <lluis@novell.com>
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.
32 using System.Collections.Generic;
35 using System.Text.RegularExpressions;
37 using Microsoft.Build.Evaluation;
38 using Microsoft.Build.Execution;
39 using Microsoft.Build.Construction;
40 using Microsoft.Build.Exceptions;
43 namespace Mono.XBuild.CommandLine {
46 public string FileName;
49 public ProjectInfo (string name, string fileName)
55 public Dictionary<TargetInfo, TargetInfo> TargetMap = new Dictionary<TargetInfo, TargetInfo> ();
56 public Dictionary<Guid, ProjectInfo> Dependencies = new Dictionary<Guid, ProjectInfo> ();
57 public Dictionary<string, ProjectSection> ProjectSections = new Dictionary<string, ProjectSection> ();
58 public List<string> AspNetConfigurations = new List<string> ();
61 class ProjectSection {
63 public Dictionary<string, string> Properties = new Dictionary<string, string> ();
65 public ProjectSection (string name)
72 public string Configuration;
73 public string Platform;
76 public TargetInfo (string configuration, string platform)
77 : this (configuration, platform, false)
81 public TargetInfo (string configuration, string platform, bool build)
83 Configuration = configuration;
90 internal delegate void RaiseWarningHandler (int errorNumber, string message);
92 class SolutionParser {
93 static string[] buildTargets = new string[] { "Build", "Clean", "Rebuild", "Publish" };
95 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}}";
97 static Regex slnVersionRegex = new Regex (@"Microsoft Visual Studio Solution File, Format Version (\d?\d.\d\d)");
98 static Regex projectRegex = new Regex ("Project\\(\"(" + guidExpression + ")\"\\) = \"(.*?)\", \"(.*?)\", \"(" + guidExpression + ")\"(\\s*?)((\\s*?)ProjectSection\\((.*?)\\) = (.*?)EndProjectSection(\\s*?))*(\\s*?)(EndProject)?", RegexOptions.Singleline);
99 static Regex projectDependenciesRegex = new Regex ("ProjectSection\\((.*?)\\) = \\w*(.*?)EndProjectSection", RegexOptions.Singleline);
100 static Regex projectDependencyRegex = new Regex ("\\s*(" + guidExpression + ") = (" + guidExpression + ")");
101 static Regex projectSectionPropertiesRegex = new Regex ("\\s*(?<name>.*) = \"(?<value>.*)\"");
103 static Regex globalRegex = new Regex ("Global(.*)EndGlobal", RegexOptions.Singleline);
104 static Regex globalSectionRegex = new Regex ("GlobalSection\\((.*?)\\) = \\w*(.*?)EndGlobalSection", RegexOptions.Singleline);
106 static Regex solutionConfigurationRegex = new Regex ("\\s*(.*?)\\|(.*?) = (.*?)\\|(.+)");
107 static Regex projectConfigurationActiveCfgRegex = new Regex ("\\s*(" + guidExpression + ")\\.(.+?)\\|(.+?)\\.ActiveCfg = (.+?)\\|(.+)");
108 static Regex projectConfigurationBuildRegex = new Regex ("\\s*(" + guidExpression + ")\\.(.*?)\\|(.*?)\\.Build\\.0 = (.*?)\\|(.+)");
110 static string solutionFolderGuid = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}";
111 static string vcprojGuid = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}";
112 static string websiteProjectGuid = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}";
114 RaiseWarningHandler RaiseWarning;
116 public void ParseSolution (string file, ProjectCollection projects, ProjectRootElement p, RaiseWarningHandler RaiseWarning)
118 this.RaiseWarning = RaiseWarning;
119 EmitBeforeImports (p, file);
121 AddGeneralSettings (file, p);
123 StreamReader reader = new StreamReader (file);
124 string slnVersion = GetSlnFileVersion (reader);
125 if (slnVersion == "12.00")
126 projects.DefaultToolsVersion = "12.0";
127 else if (slnVersion == "11.00")
128 projects.DefaultToolsVersion = "4.0";
129 else if (slnVersion == "10.00")
130 projects.DefaultToolsVersion = "3.5";
132 projects.DefaultToolsVersion = "2.0";
134 string line = reader.ReadToEnd ();
135 line = line.Replace ("\r\n", "\n");
136 string solutionDir = Path.GetDirectoryName (file);
138 List<TargetInfo> solutionTargets = new List<TargetInfo> ();
139 Dictionary<Guid, ProjectInfo> projectInfos = new Dictionary<Guid, ProjectInfo> ();
140 Dictionary<Guid, ProjectInfo> websiteProjectInfos = new Dictionary<Guid, ProjectInfo> ();
141 List<ProjectInfo>[] infosByLevel = null;
142 Dictionary<Guid, ProjectInfo> unsupportedProjectInfos = new Dictionary<Guid, ProjectInfo> ();
144 Match m = projectRegex.Match (line);
146 ProjectInfo projectInfo = new ProjectInfo (m.Groups[2].Value,
147 Path.GetFullPath (Path.Combine (solutionDir,
148 m.Groups [3].Value.Replace ('\\', Path.DirectorySeparatorChar))));
149 if (String.Compare (m.Groups [1].Value, solutionFolderGuid,
150 StringComparison.InvariantCultureIgnoreCase) == 0) {
151 // Ignore solution folders
156 projectInfo.Guid = new Guid (m.Groups [4].Value);
158 if (String.Compare (m.Groups [1].Value, vcprojGuid,
159 StringComparison.InvariantCultureIgnoreCase) == 0) {
161 RaiseWarning (0, string.Format("Ignoring vcproj '{0}'.", projectInfo.Name));
163 unsupportedProjectInfos [projectInfo.Guid] = projectInfo;
168 if (String.Compare (m.Groups [1].Value, websiteProjectGuid,
169 StringComparison.InvariantCultureIgnoreCase) == 0)
170 websiteProjectInfos.Add (new Guid (m.Groups[4].Value), projectInfo);
172 projectInfos.Add (projectInfo.Guid, projectInfo);
174 Match projectSectionMatch = projectDependenciesRegex.Match (m.Groups[6].Value);
175 while (projectSectionMatch.Success) {
176 string section_name = projectSectionMatch.Groups [1].Value;
177 if (String.Compare (section_name, "ProjectDependencies") == 0) {
178 Match projectDependencyMatch = projectDependencyRegex.Match (projectSectionMatch.Value);
179 while (projectDependencyMatch.Success) {
180 // we might not have projectInfo available right now, so
181 // set it to null, and fill it in later
182 projectInfo.Dependencies [new Guid (projectDependencyMatch.Groups[1].Value)] = null;
183 projectDependencyMatch = projectDependencyMatch.NextMatch ();
186 ProjectSection section = new ProjectSection (section_name);
187 Match propertiesMatch = projectSectionPropertiesRegex.Match (
188 projectSectionMatch.Groups [2].Value);
189 while (propertiesMatch.Success) {
190 section.Properties [propertiesMatch.Groups ["name"].Value] =
191 propertiesMatch.Groups ["value"].Value;
193 propertiesMatch = propertiesMatch.NextMatch ();
196 projectInfo.ProjectSections [section_name] = section;
198 projectSectionMatch = projectSectionMatch.NextMatch ();
203 foreach (ProjectInfo projectInfo in projectInfos.Values) {
204 string filename = projectInfo.FileName;
205 string projectDir = Path.GetDirectoryName (filename);
207 if (!File.Exists (filename)) {
208 RaiseWarning (0, String.Format ("Project file {0} referenced in the solution file, " +
209 "not found. Ignoring.", filename));
213 Project currentProject;
215 using (var xmlReader = XmlReader.Create (filename)) {
216 var root = ProjectRootElement.Create (xmlReader, projects);
217 root.FullPath = filename;
218 currentProject = new Project (root, null, null, projects, ProjectLoadSettings.IgnoreMissingImports);
220 } catch (InvalidProjectFileException e) {
221 RaiseWarning (0, e.Message);
225 foreach (var bi in currentProject.GetItems ("ProjectReference")) {
226 ProjectInfo info = null;
227 string projectReferenceGuid = bi.GetMetadataValue ("Project");
228 bool hasGuid = !String.IsNullOrEmpty (projectReferenceGuid);
230 // try to resolve the ProjectReference by GUID
231 // and fallback to project filename
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.EvaluatedInclude));
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.EvaluatedInclude.Replace ('\\', Path.DirectorySeparatorChar)));
249 info = projectInfos.Values.FirstOrDefault (pi => pi.FileName == fullpath);
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.EvaluatedInclude));
257 RaiseWarning (0, String.Format (
258 "{0}: ProjectReference '{1}' not found, neither by guid '{2}' nor by project file name '{3}'.",
259 filename, bi.EvaluatedInclude, projectReferenceGuid.Replace ("{", "").Replace ("}", ""), fullpath));
265 projectInfo.Dependencies [info.Guid] = info;
269 // fill in the project info for deps found in the .sln file
270 foreach (ProjectInfo projectInfo in projectInfos.Values) {
271 List<Guid> missingInfos = new List<Guid> ();
272 foreach (KeyValuePair<Guid, ProjectInfo> dependency in projectInfo.Dependencies) {
273 if (dependency.Value == null)
274 missingInfos.Add (dependency.Key);
277 foreach (Guid guid in missingInfos) {
279 if (projectInfos.TryGetValue (guid, out info))
280 projectInfo.Dependencies [guid] = info;
282 projectInfo.Dependencies.Remove (guid);
286 Match globalMatch = globalRegex.Match (line);
287 Match globalSectionMatch = globalSectionRegex.Match (globalMatch.Groups[1].Value);
288 while (globalSectionMatch.Success) {
289 string sectionType = globalSectionMatch.Groups[1].Value;
290 switch (sectionType) {
291 case "SolutionConfigurationPlatforms":
292 ParseSolutionConfigurationPlatforms (globalSectionMatch.Groups[2].Value, solutionTargets);
294 case "ProjectConfigurationPlatforms":
295 ParseProjectConfigurationPlatforms (globalSectionMatch.Groups[2].Value,
296 projectInfos, websiteProjectInfos);
298 case "SolutionProperties":
299 ParseSolutionProperties (globalSectionMatch.Groups[2].Value);
301 case "NestedProjects":
303 case "MonoDevelopProperties":
306 RaiseWarning (0, string.Format("Don't know how to handle GlobalSection {0}, Ignoring.", sectionType));
309 globalSectionMatch = globalSectionMatch.NextMatch ();
312 int num_levels = AddBuildLevels (p, solutionTargets, projectInfos, ref infosByLevel);
314 AddCurrentSolutionConfigurationContents (p, solutionTargets, projectInfos, websiteProjectInfos);
315 AddProjectReferences (p, projectInfos);
316 AddWebsiteProperties (p, websiteProjectInfos, projectInfos);
317 AddValidateSolutionConfiguration (p);
319 EmitAfterImports (p, file);
321 AddGetFrameworkPathTarget (p);
322 AddWebsiteTargets (p, websiteProjectInfos, projectInfos, infosByLevel, solutionTargets);
323 AddProjectTargets (p, solutionTargets, projectInfos);
324 AddSolutionTargets (p, num_levels, websiteProjectInfos.Values);
327 string GetSlnFileVersion (StreamReader reader)
329 string strInput = null;
332 strInput = reader.ReadLine();
333 if (strInput == null)
336 match = slnVersionRegex.Match(strInput);
337 if (!match.Success) {
338 strInput = reader.ReadLine();
339 if (strInput == null)
341 match = slnVersionRegex.Match (strInput);
345 return match.Groups[1].Value;
350 void EmitBeforeImports (ProjectRootElement p, string file)
353 p.AddImportGroup ().AddImport ("$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\SolutionFile\\ImportBefore\\*").Condition =
354 "'$(ImportByWildcardBeforeSolution)' != 'false' and " +
355 "Exists('$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\SolutionFile\\ImportBefore')";
358 string before_filename = Path.Combine (Path.GetDirectoryName (file), "before." + Path.GetFileName (file) + ".targets");
359 p.AddImportGroup ().AddImport (before_filename).Condition = String.Format ("Exists ('{0}')", before_filename);
362 void EmitAfterImports (ProjectRootElement p, string file)
365 p.AddImportGroup ().AddImport ("$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\SolutionFile\\ImportAfter\\*").Condition =
366 "'$(ImportByWildcardAfterSolution)' != 'false' and " +
367 "Exists('$(MSBuildExtensionsPath)\\$(MSBuildToolsVersion)\\SolutionFile\\ImportAfter')";
370 string after_filename = Path.Combine (Path.GetDirectoryName (file), "after." + Path.GetFileName (file) + ".targets");
371 p.AddImportGroup ().AddImport (after_filename).Condition = String.Format ("Exists ('{0}')", after_filename);
374 void AddGeneralSettings (string solutionFile, ProjectRootElement p)
376 p.DefaultTargets = "Build";
377 p.InitialTargets = "ValidateSolutionConfiguration";
378 //p.AddUsingTask ("CreateTemporaryVCProject", null, "Microsoft.Build.Tasks, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
379 //p.AddUsingTask ("ResolveVCProjectOutput", null, "Microsoft.Build.Tasks, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
381 string solutionFilePath = Path.GetFullPath (solutionFile);
382 var solutionPropertyGroup = p.CreatePropertyGroupElement ();
383 //solutionPropertyGroup.ChildrenReversed = true;
384 solutionPropertyGroup.AddProperty ("SolutionDir", Path.GetDirectoryName (solutionFilePath) + Path.DirectorySeparatorChar);
385 solutionPropertyGroup.AddProperty ("SolutionExt", Path.GetExtension (solutionFile));
386 solutionPropertyGroup.AddProperty ("SolutionFileName", Path.GetFileName (solutionFile));
387 solutionPropertyGroup.AddProperty ("SolutionName", Path.GetFileNameWithoutExtension (solutionFile));
388 solutionPropertyGroup.AddProperty ("SolutionPath", solutionFilePath);
391 void ParseSolutionConfigurationPlatforms (string section, List<TargetInfo> solutionTargets)
393 Match solutionConfigurationPlatform = solutionConfigurationRegex.Match (section);
394 while (solutionConfigurationPlatform.Success) {
395 string solutionConfiguration = solutionConfigurationPlatform.Groups[1].Value;
396 string solutionPlatform = solutionConfigurationPlatform.Groups[2].Value;
397 solutionTargets.Add (new TargetInfo (solutionConfiguration, solutionPlatform));
398 solutionConfigurationPlatform = solutionConfigurationPlatform.NextMatch ();
402 // ignores the website projects, in the websiteProjectInfos
403 void ParseProjectConfigurationPlatforms (string section, Dictionary<Guid, ProjectInfo> projectInfos,
404 Dictionary<Guid, ProjectInfo> websiteProjectInfos)
406 List<Guid> missingGuids = new List<Guid> ();
407 Match projectConfigurationPlatform = projectConfigurationActiveCfgRegex.Match (section);
408 while (projectConfigurationPlatform.Success) {
409 Guid guid = new Guid (projectConfigurationPlatform.Groups[1].Value);
410 ProjectInfo projectInfo;
411 if (!projectInfos.TryGetValue (guid, out projectInfo)) {
412 if (!missingGuids.Contains (guid)) {
413 if (!websiteProjectInfos.ContainsKey (guid))
414 // ignore website projects
415 RaiseWarning (0, string.Format("Failed to find project {0}", guid));
416 missingGuids.Add (guid);
418 projectConfigurationPlatform = projectConfigurationPlatform.NextMatch ();
421 string solConf = projectConfigurationPlatform.Groups[2].Value;
422 string solPlat = projectConfigurationPlatform.Groups[3].Value;
423 string projConf = projectConfigurationPlatform.Groups[4].Value;
424 string projPlat = projectConfigurationPlatform.Groups[5].Value;
425 // hack, what are they doing here?
426 if (projPlat == "Any CPU")
428 projectInfo.TargetMap.Add (new TargetInfo (solConf, solPlat), new TargetInfo (projConf, projPlat));
429 projectConfigurationPlatform = projectConfigurationPlatform.NextMatch ();
431 Match projectConfigurationPlatformBuild = projectConfigurationBuildRegex.Match (section);
432 while (projectConfigurationPlatformBuild.Success) {
433 Guid guid = new Guid (projectConfigurationPlatformBuild.Groups[1].Value);
434 ProjectInfo projectInfo;
435 if (!projectInfos.TryGetValue (guid, out projectInfo)) {
436 if (!missingGuids.Contains (guid)) {
437 RaiseWarning (0, string.Format("Failed to find project {0}", guid));
438 missingGuids.Add (guid);
440 projectConfigurationPlatformBuild = projectConfigurationPlatformBuild.NextMatch ();
443 string solConf = projectConfigurationPlatformBuild.Groups[2].Value;
444 string solPlat = projectConfigurationPlatformBuild.Groups[3].Value;
445 string projConf = projectConfigurationPlatformBuild.Groups[4].Value;
446 string projPlat = projectConfigurationPlatformBuild.Groups[5].Value;
447 // hack, what are they doing here?
448 if (projPlat == "Any CPU")
450 projectInfo.TargetMap[new TargetInfo (solConf, solPlat)] = new TargetInfo (projConf, projPlat, true);
451 projectConfigurationPlatformBuild = projectConfigurationPlatformBuild.NextMatch ();
455 void ParseSolutionProperties (string section)
459 void AddCurrentSolutionConfigurationContents (ProjectRootElement p, List<TargetInfo> solutionTargets,
460 Dictionary<Guid, ProjectInfo> projectInfos,
461 Dictionary<Guid, ProjectInfo> websiteProjectInfos)
463 TargetInfo default_target_info = new TargetInfo ("Debug", "Any CPU");
464 if (solutionTargets.Count > 0) {
466 foreach (TargetInfo tinfo in solutionTargets) {
467 if (String.Compare (tinfo.Platform, "Mixed Platforms") == 0) {
468 default_target_info = tinfo;
475 default_target_info = solutionTargets [0];
478 AddDefaultSolutionConfiguration (p, default_target_info);
480 foreach (TargetInfo solutionTarget in solutionTargets) {
481 var platformPropertyGroup = p.AddPropertyGroup ();
482 platformPropertyGroup.Condition = string.Format (
483 " ('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}') ",
484 solutionTarget.Configuration,
485 solutionTarget.Platform
488 StringBuilder solutionConfigurationContents = new StringBuilder ();
489 solutionConfigurationContents.Append ("<SolutionConfiguration xmlns=\"\">");
490 foreach (KeyValuePair<Guid, ProjectInfo> projectInfo in projectInfos) {
491 AddProjectConfigurationItems (projectInfo.Key, projectInfo.Value, solutionTarget, solutionConfigurationContents);
493 solutionConfigurationContents.Append ("</SolutionConfiguration>");
495 platformPropertyGroup.AddProperty ("CurrentSolutionConfigurationContents",
496 solutionConfigurationContents.ToString ());
500 void AddProjectReferences (ProjectRootElement p, Dictionary<Guid, ProjectInfo> projectInfos)
502 var big = p.AddItemGroup ();
503 foreach (KeyValuePair<Guid, ProjectInfo> pair in projectInfos)
504 big.AddItem ("ProjectReference", pair.Value.FileName);
507 void AddProjectConfigurationItems (Guid guid, ProjectInfo projectInfo, TargetInfo solutionTarget,
508 StringBuilder solutionConfigurationContents)
510 foreach (KeyValuePair<TargetInfo, TargetInfo> targetInfo in projectInfo.TargetMap) {
511 if (solutionTarget.Configuration == targetInfo.Key.Configuration &&
512 solutionTarget.Platform == targetInfo.Key.Platform) {
513 solutionConfigurationContents.AppendFormat (
514 "<ProjectConfiguration Project=\"{0}\">{1}|{2}</ProjectConfiguration>",
515 guid.ToString ("B").ToUpper (), targetInfo.Value.Configuration, targetInfo.Value.Platform);
520 void AddDefaultSolutionConfiguration (ProjectRootElement p, TargetInfo target)
522 var configurationPropertyGroup = p.AddPropertyGroup ();
523 //configurationPropertyGroup.PropertiesReversed = true;
524 configurationPropertyGroup.Condition = " '$(Configuration)' == '' ";
525 configurationPropertyGroup.AddProperty ("Configuration", target.Configuration);
527 var platformPropertyGroup = p.AddPropertyGroup ();
528 //platformPropertyGroup.PropertiesReversed = true;
529 platformPropertyGroup.Condition = " '$(Platform)' == '' ";
530 platformPropertyGroup.AddProperty ("Platform", target.Platform);
532 // emit default for AspNetConfiguration also
533 var aspNetConfigurationPropertyGroup = p.AddPropertyGroup ();
534 //aspNetConfigurationPropertyGroup.PropertiesReversed = true;
535 aspNetConfigurationPropertyGroup.Condition = " ('$(AspNetConfiguration)' == '') ";
536 aspNetConfigurationPropertyGroup.AddProperty ("AspNetConfiguration", "$(Configuration)");
539 void AddWarningForMissingProjectConfiguration (ProjectTargetElement target, string slnConfig, string slnPlatform, string projectName)
541 var task = target.AddTask ("Warning");
542 task.SetParameter ("Text",
543 String.Format ("The project configuration for project '{0}' corresponding " +
544 "to the solution configuration '{1}|{2}' was not found in the solution file.",
545 projectName, slnConfig, slnPlatform));
546 task.Condition = String.Format ("('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}')",
547 slnConfig, slnPlatform);
551 // Website project methods
553 void AddWebsiteProperties (ProjectRootElement p, Dictionary<Guid, ProjectInfo> websiteProjectInfos,
554 Dictionary<Guid, ProjectInfo> projectInfos)
556 var propertyGroupByConfig = new Dictionary<string, ProjectPropertyGroupElement> ();
557 foreach (KeyValuePair<Guid, ProjectInfo> infoPair in websiteProjectInfos) {
558 ProjectInfo info = infoPair.Value;
559 string projectGuid = infoPair.Key.ToString ();
561 ProjectSection section;
562 if (!info.ProjectSections.TryGetValue ("WebsiteProperties", out section)) {
563 RaiseWarning (0, String.Format ("Website project '{0}' does not have the required project section: WebsiteProperties. Ignoring project.", info.Name));
567 //parse project references
568 string [] ref_guids = null;
570 if (section.Properties.TryGetValue ("ProjectReferences", out references)) {
571 ref_guids = references.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries);
572 for (int i = 0; i < ref_guids.Length; i ++) {
574 ref_guids [i] = ref_guids [i].Split ('|') [0];
576 Guid r_guid = new Guid (ref_guids [i]);
577 ProjectInfo ref_info;
578 if (projectInfos.TryGetValue (r_guid, out ref_info))
579 // ignore if not found
580 info.Dependencies [r_guid] = ref_info;
584 foreach (KeyValuePair<string, string> pair in section.Properties) {
585 //looking for -- ConfigName.AspNetCompiler.PropName
586 string [] parts = pair.Key.Split ('.');
587 if (parts.Length != 3 || String.Compare (parts [1], "AspNetCompiler") != 0)
590 string config = parts [0];
591 string propertyName = parts [2];
593 ProjectPropertyGroupElement bpg;
594 if (!propertyGroupByConfig.TryGetValue (config, out bpg)) {
595 bpg = p.AddPropertyGroup ();
596 //bpg.PropertiesReversed = true;
597 bpg.Condition = String.Format (" '$(AspNetConfiguration)' == '{0}' ", config);
598 propertyGroupByConfig [config] = bpg;
601 bpg.AddProperty (String.Format ("Project_{0}_AspNet{1}", projectGuid, propertyName),
604 if (!info.AspNetConfigurations.Contains (config))
605 info.AspNetConfigurations.Add (config);
610 // For WebSite projects
611 // The main "Build" target:
612 // 1. builds all non-website projects
613 // 2. calls target for website project
614 // - gets target path for the referenced projects
615 // - Resolves dependencies, satellites etc for the
616 // referenced project assemblies, and copies them
618 void AddWebsiteTargets (ProjectRootElement p, Dictionary<Guid, ProjectInfo> websiteProjectInfos,
619 Dictionary<Guid, ProjectInfo> projectInfos, List<ProjectInfo>[] infosByLevel,
620 List<TargetInfo> solutionTargets)
622 foreach (ProjectInfo w_info in websiteProjectInfos.Values) {
623 // gets a linear list of dependencies
624 List<ProjectInfo> depInfos = new List<ProjectInfo> ();
625 foreach (List<ProjectInfo> pinfos in infosByLevel) {
626 foreach (ProjectInfo pinfo in pinfos)
627 if (w_info.Dependencies.ContainsKey (pinfo.Guid))
628 depInfos.Add (pinfo);
631 foreach (string buildTarget in new string [] {"Build", "Rebuild"})
632 AddWebsiteTarget (p, w_info, projectInfos, depInfos, solutionTargets, buildTarget);
634 // clean/publish are not supported for website projects
635 foreach (string buildTarget in new string [] {"Clean", "Publish"})
636 AddWebsiteUnsupportedTarget (p, w_info, depInfos, buildTarget);
640 void AddWebsiteTarget (ProjectRootElement p, ProjectInfo webProjectInfo,
641 Dictionary<Guid, ProjectInfo> projectInfos, List<ProjectInfo> depInfos,
642 List<TargetInfo> solutionTargets, string buildTarget)
644 string w_guid = webProjectInfo.Guid.ToString ().ToUpper ();
646 var target = p.AddTarget (GetTargetNameForProject (webProjectInfo.Name, buildTarget));
647 target.Condition = "'$(CurrentSolutionConfigurationContents)' != ''";
648 target.DependsOnTargets = GetWebsiteDependsOnTarget (depInfos, buildTarget);
650 // this item collects all the references
651 string final_ref_item = String.Format ("Project_{0}_References{1}", w_guid,
652 buildTarget != "Build" ? "_" + buildTarget : String.Empty);
654 foreach (TargetInfo targetInfo in solutionTargets) {
656 foreach (ProjectInfo depInfo in depInfos) {
657 TargetInfo projectTargetInfo;
658 if (!depInfo.TargetMap.TryGetValue (targetInfo, out projectTargetInfo))
659 // Ignore, no config, so no target path
662 // GetTargetPath from the referenced project
663 AddWebsiteMSBuildTaskForReference (target, depInfo, projectTargetInfo, targetInfo,
664 final_ref_item, ref_num);
669 // resolve the references
670 AddWebsiteResolveAndCopyReferencesTasks (target, webProjectInfo, final_ref_item, w_guid);
673 // emits the MSBuild task to GetTargetPath for the referenced project
674 void AddWebsiteMSBuildTaskForReference (ProjectTargetElement target, ProjectInfo depInfo, TargetInfo projectTargetInfo,
675 TargetInfo solutionTargetInfo, string final_ref_item, int ref_num)
677 var task = target.AddTask ("MSBuild");
678 task.SetParameter ("Projects", depInfo.FileName);
679 task.SetParameter ("Targets", "GetTargetPath");
681 task.SetParameter ("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));
682 task.Condition = string.Format (" ('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}') ", solutionTargetInfo.Configuration, solutionTargetInfo.Platform);
684 string ref_item = String.Format ("{0}_{1}",
685 final_ref_item, ref_num);
687 task.AddOutputItem ("TargetOutputs", ref_item);
689 task = target.AddTask ("CreateItem");
690 task.SetParameter ("Include", String.Format ("@({0})", ref_item));
691 task.SetParameter ("AdditionalMetadata", String.Format ("Guid={{{0}}}",
692 depInfo.Guid.ToString ().ToUpper ()));
693 task.AddOutputItem ("Include", final_ref_item);
696 void AddWebsiteResolveAndCopyReferencesTasks (ProjectTargetElement target, ProjectInfo webProjectInfo,
697 string final_ref_item, string w_guid)
699 var task = target.AddTask ("ResolveAssemblyReference");
700 task.SetParameter ("Assemblies", String.Format ("@({0}->'%(FullPath)')", final_ref_item));
701 task.SetParameter ("TargetFrameworkDirectories", "$(TargetFrameworkPath)");
702 task.SetParameter ("SearchPaths", "{RawFileName};{TargetFrameworkDirectory};{GAC}");
703 task.SetParameter ("FindDependencies", "true");
704 task.SetParameter ("FindSatellites", "true");
705 task.SetParameter ("FindRelatedFiles", "true");
706 task.Condition = String.Format ("Exists ('%({0}.Identity)')", final_ref_item);
708 string copylocal_item = String.Format ("{0}_CopyLocalFiles", final_ref_item);
709 task.AddOutputItem ("CopyLocalFiles", copylocal_item);
711 // Copy the references
712 task = target.AddTask ("Copy");
713 task.SetParameter ("SourceFiles", String.Format ("@({0})", copylocal_item));
714 task.SetParameter ("DestinationFiles", String.Format (
715 "@({0}->'$(Project_{1}_AspNetPhysicalPath)\\Bin\\%(DestinationSubDirectory)%(Filename)%(Extension)')",
716 copylocal_item, w_guid));
718 // AspNetConfiguration, is config for the website project, useful
719 // for overriding from command line
720 StringBuilder cond = new StringBuilder ();
721 foreach (string config in webProjectInfo.AspNetConfigurations) {
723 cond.Append (" or ");
724 cond.AppendFormat (" ('$(AspNetConfiguration)' == '{0}') ", config);
726 task.Condition = cond.ToString ();
728 task = target.AddTask ("Message");
729 cond = new StringBuilder ();
730 foreach (string config in webProjectInfo.AspNetConfigurations) {
732 cond.Append (" and ");
733 cond.AppendFormat (" ('$(AspNetConfiguration)' != '{0}') ", config);
735 task.Condition = cond.ToString ();
736 task.SetParameter ("Text", "Skipping as the '$(AspNetConfiguration)' configuration is " +
737 "not supported by this website project.");
740 void AddWebsiteUnsupportedTarget (ProjectRootElement p, ProjectInfo webProjectInfo, List<ProjectInfo> depInfos,
743 var target = p.AddTarget (GetTargetNameForProject (webProjectInfo.Name, buildTarget));
744 target.DependsOnTargets = GetWebsiteDependsOnTarget (depInfos, buildTarget);
746 var task = target.AddTask ("Message");
747 task.SetParameter ("Text", String.Format (
748 "Target '{0}' not support for website projects", buildTarget));
751 string GetWebsiteDependsOnTarget (List<ProjectInfo> depInfos, string buildTarget)
753 StringBuilder deps = new StringBuilder ();
754 foreach (ProjectInfo pinfo in depInfos) {
757 deps.Append (GetTargetNameForProject (pinfo.Name, buildTarget));
759 deps.Append (";GetFrameworkPath");
760 return deps.ToString ();
763 void AddGetFrameworkPathTarget (ProjectRootElement p)
765 var t = p.AddTarget ("GetFrameworkPath");
766 var task = t.AddTask ("GetFrameworkPath");
767 task.AddOutputProperty ("Path", "TargetFrameworkPath");
770 void AddValidateSolutionConfiguration (ProjectRootElement p)
772 var t = p.AddTarget ("ValidateSolutionConfiguration");
773 var task = t.AddTask ("Warning");
774 task.SetParameter ("Text", "On windows, an environment variable 'Platform' is set to MCD sometimes, and this overrides the Platform property" +
775 " for Mono msbuild, which could be an invalid Platform for this solution file. And so you are getting the following error." +
776 " You could override it by either setting the environment variable to nothing, as\n" +
778 "Or explicity specify its value on the command line, as\n" +
779 " msbuild Foo.sln /p:Platform=Release");
780 task.Condition = "('$(CurrentSolutionConfigurationContents)' == '') and ('$(SkipInvalidConfigurations)' != 'true')" +
781 " and '$(Platform)' == 'MCD' and '$(OS)' == 'Windows_NT'";
783 task = t.AddTask ("Error");
784 task.SetParameter ("Text", "Invalid solution configuration and platform: \"$(Configuration)|$(Platform)\".");
785 task.Condition = "('$(CurrentSolutionConfigurationContents)' == '') and ('$(SkipInvalidConfigurations)' != 'true')";
786 task = t.AddTask ("Warning");
787 task.SetParameter ("Text", "Invalid solution configuration and platform: \"$(Configuration)|$(Platform)\".");
788 task.Condition = "('$(CurrentSolutionConfigurationContents)' == '') and ('$(SkipInvalidConfigurations)' == 'true')";
789 task = t.AddTask ("Message");
790 task.SetParameter ("Text", "Building solution configuration \"$(Configuration)|$(Platform)\".");
791 task.Condition = "'$(CurrentSolutionConfigurationContents)' != ''";
794 void AddProjectTargets (ProjectRootElement p, List<TargetInfo> solutionTargets, Dictionary<Guid, ProjectInfo> projectInfos)
796 foreach (KeyValuePair<Guid, ProjectInfo> projectInfo in projectInfos) {
797 ProjectInfo project = projectInfo.Value;
798 foreach (string buildTarget in buildTargets) {
799 string target_name = GetTargetNameForProject (project.Name, buildTarget);
800 bool is_build_or_rebuild = buildTarget == "Build" || buildTarget == "Rebuild";
801 var target = p.AddTarget (target_name);
802 target.Condition = "'$(CurrentSolutionConfigurationContents)' != ''";
804 if (is_build_or_rebuild)
805 target.Outputs = "@(CollectedBuildOutput)";
806 if (project.Dependencies.Count > 0)
807 target.DependsOnTargets = String.Join (";",
808 project.Dependencies.Values.Select (
809 di => GetTargetNameForProject (di.Name, buildTarget)).ToArray ());
811 foreach (TargetInfo targetInfo in solutionTargets) {
812 ProjectTaskElement task = null;
813 TargetInfo projectTargetInfo;
814 if (!project.TargetMap.TryGetValue (targetInfo, out projectTargetInfo)) {
815 AddWarningForMissingProjectConfiguration (target, targetInfo.Configuration,
816 targetInfo.Platform, project.Name);
819 if (projectTargetInfo.Build) {
820 task = target.AddTask ("MSBuild");
821 task.SetParameter ("Projects", project.FileName);
822 task.SetParameter ("ToolsVersion", "$(ProjectToolsVersion)");
823 if (is_build_or_rebuild)
824 task.AddOutputItem ("TargetOutputs", "CollectedBuildOutput");
826 if (buildTarget != "Build")
827 task.SetParameter ("Targets", buildTarget);
828 task.SetParameter ("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));
830 task = target.AddTask ("Message");
831 task.SetParameter ("Text", string.Format ("Project \"{0}\" is disabled for solution configuration \"{1}|{2}\".", project.Name, targetInfo.Configuration, targetInfo.Platform));
833 task.Condition = string.Format (" ('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}') ", targetInfo.Configuration, targetInfo.Platform);
840 string GetTargetNameForProject (string projectName, string buildTarget)
843 projectName = projectName.Replace ("\\", "/").Replace (".", "_");
844 string target_name = projectName +
845 (buildTarget == "Build" ? string.Empty : ":" + buildTarget);
847 if (IsBuildTargetName (projectName))
848 target_name = "Solution:" + target_name;
853 bool IsBuildTargetName (string name)
855 foreach (string tgt in buildTargets)
861 // returns number of levels
862 int AddBuildLevels (ProjectRootElement p, List<TargetInfo> solutionTargets, Dictionary<Guid, ProjectInfo> projectInfos,
863 ref List<ProjectInfo>[] infosByLevel)
865 infosByLevel = TopologicalSort<ProjectInfo> (projectInfos.Values);
867 foreach (TargetInfo targetInfo in solutionTargets) {
868 var big = p.AddItemGroup ();
869 big.Condition = String.Format (" ('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}') ",
870 targetInfo.Configuration, targetInfo.Platform);
872 //FIXME: every level has projects that can be built in parallel.
873 // levels are ordered on the basis of the dependency graph
875 for (int i = 0; i < infosByLevel.Length; i ++) {
876 string build_level = String.Format ("BuildLevel{0}", i);
877 string skip_level = String.Format ("SkipLevel{0}", i);
878 string missing_level = String.Format ("MissingConfigLevel{0}", i);
880 foreach (ProjectInfo projectInfo in infosByLevel [i]) {
881 TargetInfo projectTargetInfo;
882 if (!projectInfo.TargetMap.TryGetValue (targetInfo, out projectTargetInfo)) {
883 // missing project config
884 big.AddItem (missing_level, projectInfo.Name);
888 if (projectTargetInfo.Build) {
889 var item = big.AddItem (build_level, projectInfo.FileName);
890 item.AddMetadata ("Configuration", projectTargetInfo.Configuration);
891 item.AddMetadata ("Platform", projectTargetInfo.Platform);
894 big.AddItem (skip_level, projectInfo.Name);
900 return infosByLevel.Length;
903 void AddSolutionTargets (ProjectRootElement p, int num_levels, IEnumerable<ProjectInfo> websiteProjectInfos)
905 foreach (string buildTarget in buildTargets) {
906 var t = p.AddTarget (buildTarget);
907 bool is_build_or_rebuild = buildTarget == "Build" || buildTarget == "Rebuild";
909 t.Condition = "'$(CurrentSolutionConfigurationContents)' != ''";
910 if (is_build_or_rebuild)
911 t.Outputs = "@(CollectedBuildOutput)";
913 ProjectTaskElement task = null;
914 for (int i = 0; i < num_levels; i ++) {
915 string level_str = String.Format ("BuildLevel{0}", i);
916 task = t.AddTask ("MSBuild");
917 task.SetParameter ("Condition", String.Format ("'@({0})' != ''", level_str));
918 task.SetParameter ("Projects", String.Format ("@({0})", level_str));
919 task.SetParameter ("ToolsVersion", "$(ProjectToolsVersion)");
920 task.SetParameter ("Properties",
921 string.Format ("Configuration=%(Configuration); Platform=%(Platform); BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)"));
922 if (buildTarget != "Build")
923 task.SetParameter ("Targets", buildTarget);
924 //FIXME: change this to BuildInParallel=true, when parallel
925 // build support gets added
926 task.SetParameter ("RunEachTargetSeparately", "true");
927 if (is_build_or_rebuild)
928 task.AddOutputItem ("TargetOutputs", "CollectedBuildOutput");
930 level_str = String.Format ("SkipLevel{0}", i);
931 task = t.AddTask ("Message");
932 task.Condition = String.Format ("'@({0})' != ''", level_str);
933 task.SetParameter ("Text",
934 String.Format ("The project '%({0}.Identity)' is disabled for solution " +
935 "configuration '$(Configuration)|$(Platform)'.", level_str));
937 level_str = String.Format ("MissingConfigLevel{0}", i);
938 task = t.AddTask ("Warning");
939 task.Condition = String.Format ("'@({0})' != ''", level_str);
940 task.SetParameter ("Text",
941 String.Format ("The project configuration for project '%({0}.Identity)' " +
942 "corresponding to the solution configuration " +
943 "'$(Configuration)|$(Platform)' was not found.", level_str));
946 // "build" website projects also
947 StringBuilder w_targets = new StringBuilder ();
948 foreach (ProjectInfo info in websiteProjectInfos) {
949 if (w_targets.Length > 0)
950 w_targets.Append (";");
951 w_targets.Append (GetTargetNameForProject (info.Name, buildTarget));
954 task = t.AddTask ("CallTarget");
955 task.SetParameter ("Targets", w_targets.ToString ());
956 task.SetParameter ("RunEachTargetSeparately", "true");
960 // Sorts the ProjectInfo dependency graph, to obtain
961 // a series of build levels with projects. Projects
962 // in each level can be run parallel (no inter-dependency).
963 static List<T>[] TopologicalSort<T> (IEnumerable<T> items) where T: ProjectInfo
966 allItems = items as IList<T>;
967 if (allItems == null)
968 allItems = new List<T> (items);
970 bool[] inserted = new bool[allItems.Count];
971 bool[] triedToInsert = new bool[allItems.Count];
972 int[] levels = new int [allItems.Count];
975 for (int i = 0; i < allItems.Count; ++i) {
976 int d = Insert<T> (i, allItems, levels, inserted, triedToInsert);
981 // Separate out the project infos by build level
982 List<T>[] infosByLevel = new List<T>[maxdepth];
983 for (int i = 0; i < levels.Length; i ++) {
984 int level = levels [i] - 1;
985 if (infosByLevel [level] == null)
986 infosByLevel [level] = new List<T> ();
988 infosByLevel [level].Add (allItems [i]);
994 // returns level# for the project
995 static int Insert<T> (int index, IList<T> allItems, int[] levels, bool[] inserted, bool[] triedToInsert)
998 if (inserted [index])
999 return levels [index];
1001 if (triedToInsert[index])
1002 throw new InvalidOperationException (String.Format (
1003 "Cyclic dependency involving project {0} found in the project dependency graph",
1004 allItems [index].Name));
1006 triedToInsert[index] = true;
1007 ProjectInfo insertItem = allItems[index];
1010 foreach (ProjectInfo dependency in insertItem.Dependencies.Values) {
1011 for (int j = 0; j < allItems.Count; ++j) {
1012 ProjectInfo checkItem = allItems [j];
1013 if (dependency.FileName == checkItem.FileName) {
1014 int d = Insert (j, allItems, levels, inserted, triedToInsert);
1015 maxdepth = d > maxdepth ? d : maxdepth;
1020 levels [index] = maxdepth + 1;
1021 inserted [index] = true;
1023 return levels [index];
1026 public static IEnumerable<string> GetAllProjectFileNames (string solutionFile)
1028 StreamReader reader = new StreamReader (solutionFile);
1029 string line = reader.ReadToEnd ();
1030 line = line.Replace ("\r\n", "\n");
1031 string soln_dir = Path.GetDirectoryName (solutionFile);
1033 Match m = projectRegex.Match (line);
1035 if (String.Compare (m.Groups [1].Value, solutionFolderGuid,
1036 StringComparison.InvariantCultureIgnoreCase) != 0)
1037 yield return Path.Combine (soln_dir, m.Groups [3].Value).Replace ("\\", "/");