1 //
2 // Import.cs: Represents a single Import element in an MSBuild project.
3 //
4 // Author:
5 //   Marek Sieradzki (marek.sieradzki@gmail.com)
6 //   Ankit Jain (jankit@novell.com)
7 //
8 // (C) 2006 Marek Sieradzki
9 // Copyright 2011 Novell, Inc (http://www.novell.com)
10 //
11 // Permission is hereby granted, free of charge, to any person obtaining
12 // a copy of this software and associated documentation files (the
13 // "Software"), to deal in the Software without restriction, including
14 // without limitation the rights to use, copy, modify, merge, publish,
15 // distribute, sublicense, and/or sell copies of the Software, and to
16 // permit persons to whom the Software is furnished to do so, subject to
17 // the following conditions:
18 //
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
21 //
22 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 
30 using System;
31 using System.Collections.Generic;
32 using System.IO;
33 using System.Linq;
34 using System.Xml;
35 
36 using Microsoft.Build.Framework;
37 using Microsoft.Build.Utilities;
38 using Mono.XBuild.Utilities;
39 
40 namespace Microsoft.Build.BuildEngine {
41 	public class Import {
42 		XmlElement	importElement;
43 		Project		project;
44 		ImportedProject originalProject;
45 		string		evaluatedProjectPath;
46 
47 		static string DotConfigExtensionsPath = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData),
48 								Path.Combine ("xbuild", "tasks"));
49 		const string MacOSXExternalXBuildDir = "/Library/Frameworks/Mono.framework/External/xbuild";
50 		static string PathSeparatorAsString = Path.PathSeparator.ToString ();
51 
Import(XmlElement importElement, Project project, ImportedProject originalProject)52 		internal Import (XmlElement importElement, Project project, ImportedProject originalProject)
53 			: this (importElement, null, project, originalProject)
54 		{}
55 
56 		// if @alternateProjectPath is available then that it used as the EvaluatedProjectPath!
Import(XmlElement importElement, string alternateProjectPath, Project project, ImportedProject originalProject)57 		internal Import (XmlElement importElement, string alternateProjectPath, Project project, ImportedProject originalProject)
58 		{
59 			if (importElement == null)
60 				throw new ArgumentNullException ("importElement");
61 			if (project == null)
62 				throw new ArgumentNullException ("project");
63 
64 			this.project = project;
65 			this.importElement = importElement;
66 			this.originalProject = originalProject;
67 
68 			if (ProjectPath == String.Empty)
69 				throw new InvalidProjectFileException ("The required attribute \"Project\" is missing from element <Import>.");
70 
71 			if (ConditionParser.ParseAndEvaluate (Condition, project)) {
72 				evaluatedProjectPath = String.IsNullOrEmpty (alternateProjectPath) ? EvaluateProjectPath (ProjectPath) : alternateProjectPath;
73 
74 				evaluatedProjectPath = GetFullPath ();
75 				if (EvaluatedProjectPath == String.Empty)
76 					throw new InvalidProjectFileException ("The required attribute \"Project\" is missing from element <Import>.");
77 			}
78 		}
79 
CheckEvaluatedProjectPathExists()80 		internal bool CheckEvaluatedProjectPathExists ()
81 		{
82 			string path = EvaluatedProjectPath;
83 
84 			if (File.Exists (path))
85 				return true;
86 
87 			if (Path.GetFileName (path) == "Microsoft.CSharp.Targets") {
88 				path = Path.ChangeExtension (path, ".targets");
89 				if (File.Exists (path))
90 					return true;
91 			}
92 
93 			return false;
94 		}
95 
96 		// FIXME: condition
Evaluate(bool ignoreMissingImports)97 		internal void Evaluate (bool ignoreMissingImports)
98 		{
99 			string filename = evaluatedProjectPath;
100 			// NOTE: it's a hack to transform Microsoft.CSharp.Targets to Microsoft.CSharp.targets
101 			if (!File.Exists (filename) && Path.GetFileName (filename) == "Microsoft.CSharp.Targets")
102 				filename = Path.ChangeExtension (filename, ".targets");
103 
104 			if (!File.Exists (filename)) {
105 				if (ignoreMissingImports) {
106 					project.LogWarning (project.FullFileName, "Could not find project file {0}, to import. Ignoring.", filename);
107 					return;
108 				} else {
109 					throw new InvalidProjectFileException (String.Format ("Imported project: \"{0}\" does not exist.", filename));
110 				}
111 			}
112 
113 			ImportedProject importedProject = new ImportedProject ();
114 			importedProject.Load (filename);
115 
116 			project.ProcessElements (importedProject.XmlDocument.DocumentElement, importedProject);
117 		}
118 
EvaluateProjectPath(string file)119 		string EvaluateProjectPath (string file)
120 		{
121 			return Expression.ParseAs<string> (file, ParseOptions.Split, project);
122 		}
123 
GetFullPath()124 		string GetFullPath ()
125 		{
126 			string file = EvaluatedProjectPath;
127 			if (!Path.IsPathRooted (file) && !String.IsNullOrEmpty (ContainedInProjectFileName))
128 				file = Path.Combine (Path.GetDirectoryName (ContainedInProjectFileName), file);
129 
130 			return MSBuildUtils.FromMSBuildPath (file);
131 		}
132 
133 		// For every extension path, in order, finds suitable
134 		// import filename(s) matching the Import, and calls
135 		// @func with them
136 		//
137 		// func: bool func(importPath, from_source_msg)
138 		//
139 		// If for an extension path, atleast one file gets imported,
140 		// then it stops at that.
141 		// So, in case imports like "$(MSBuildExtensionsPath)\foo\*",
142 		// for every extension path, it will try to import the "foo\*",
143 		// and if atleast one file gets successfully imported, then it
144 		// stops at that
ForEachExtensionPathTillFound(XmlElement xmlElement, Project project, ImportedProject importingProject, Func<string, string, bool> func)145 		internal static void ForEachExtensionPathTillFound (XmlElement xmlElement, Project project, ImportedProject importingProject,
146 				Func<string, string, bool> func)
147 		{
148 			string project_attribute = xmlElement.GetAttribute ("Project");
149 			string condition_attribute = xmlElement.GetAttribute ("Condition");
150 
151 			bool has_extn_ref = project_attribute.IndexOf ("$(MSBuildExtensionsPath)") >= 0 ||
152 						project_attribute.IndexOf ("$(MSBuildExtensionsPath32)") >= 0 ||
153 						project_attribute.IndexOf ("$(MSBuildExtensionsPath64)") >= 0;
154 
155 			bool condn_has_extn_ref = condition_attribute.IndexOf ("$(MSBuildExtensionsPath)") >= 0 ||
156 						condition_attribute.IndexOf ("$(MSBuildExtensionsPath32)") >= 0 ||
157 						condition_attribute.IndexOf ("$(MSBuildExtensionsPath64)") >= 0;
158 
159 			// we can skip the following logic in case the condition doesn't reference any extension paths
160 			// and it evaluates to false since nothing would change anyway
161 			if (!condn_has_extn_ref && !ConditionParser.ParseAndEvaluate (condition_attribute, project))
162 				return;
163 
164 			string importingFile = importingProject != null ? importingProject.FullFileName : project.FullFileName;
165 			DirectoryInfo base_dir_info = null;
166 			if (!String.IsNullOrEmpty (importingFile))
167 				base_dir_info = new DirectoryInfo (Path.GetDirectoryName (importingFile));
168 			else
169 				base_dir_info = new DirectoryInfo (Directory.GetCurrentDirectory ());
170 
171 			var importPaths = GetImportPathsFromString (project_attribute, project, base_dir_info);
172 			var extensionPaths = GetExtensionPaths (project);
173 
174 			if (!has_extn_ref) {
175 				foreach (var importPath in importPaths) {
176 					foreach (var extensionPath in extensionPaths) {
177 						has_extn_ref = has_extn_ref || importPath.IndexOf (extensionPath) >= 0;
178 					}
179 				}
180 			}
181 
182 			IEnumerable<string> extn_paths = has_extn_ref ? extensionPaths : new string [] { null };
183 			bool import_needed = false;
184 			var currentLoadSettings = project.ProjectLoadSettings;
185 
186 			try {
187 				foreach (var settings in new ProjectLoadSettings [] { ProjectLoadSettings.None, currentLoadSettings }) {
188 					foreach (string path in extn_paths) {
189 						string extn_msg = null;
190 						if (has_extn_ref) {
191 							project.SetExtensionsPathProperties (path);
192 							extn_msg = "from extension path " + path;
193 						}
194 
195 						// do this after setting new Extension properties, as condition might
196 						// reference it
197 						if (!ConditionParser.ParseAndEvaluate (condition_attribute, project))
198 							continue;
199 
200 						import_needed = true;
201 						project.ProjectLoadSettings = settings;
202 
203 						// We stop if atleast one file got imported.
204 						// Remaining extension paths are *not* tried
205 						bool atleast_one = false;
206 						foreach (string importPath in importPaths) {
207 							try {
208 								if (func (importPath, extn_msg))
209 									atleast_one = true;
210 							} catch (Exception e) {
211 								throw new InvalidProjectFileException (String.Format (
212 											"{0}: Project file could not be imported, it was being imported by " +
213 											"{1}: {2}", importPath, importingFile, e.Message), e);
214 							}
215 						}
216 
217 						if (atleast_one)
218 							return;
219 					}
220 				}
221 			} finally {
222 				project.ProjectLoadSettings = currentLoadSettings;
223 				if (has_extn_ref)
224 					project.SetExtensionsPathProperties (Project.DefaultExtensionsPath);
225 			}
226 
227 			if (import_needed)
228 				throw new InvalidProjectFileException (String.Format ("{0} could not import \"{1}\"", importingFile, project_attribute));
229 		}
230 
231 		// Parses the Project attribute from an Import,
232 		// and returns the import filenames that match.
233 		// This handles wildcards also
GetImportPathsFromString(string import_string, Project project, DirectoryInfo base_dir_info)234 		static IEnumerable<string> GetImportPathsFromString (string import_string, Project project, DirectoryInfo base_dir_info)
235 		{
236 			string parsed_import = Expression.ParseAs<string> (import_string, ParseOptions.AllowItemsNoMetadataAndSplit, project);
237 			if (parsed_import != null)
238 				parsed_import = parsed_import.Trim ();
239 
240 			if (String.IsNullOrEmpty (parsed_import))
241 				throw new InvalidProjectFileException ("The required attribute \"Project\" in Import is empty");
242 
243 			if (DirectoryScanner.HasWildcard (parsed_import)) {
244 				var directoryScanner = new DirectoryScanner () {
245 					Includes = new ITaskItem [] { new TaskItem (parsed_import) },
246 					BaseDirectory = base_dir_info
247 				};
248 				directoryScanner.Scan ();
249 
250 				foreach (ITaskItem matchedItem in directoryScanner.MatchedItems)
251 					yield return matchedItem.ItemSpec;
252 			} else
253 				yield return parsed_import;
254 		}
255 
256 		// Gives a list of extensions paths to try for $(MSBuildExtensionsPath),
257 		// *in-order*
GetExtensionPaths(Project project)258 		static IEnumerable<string> GetExtensionPaths (Project project)
259 		{
260 			// This is a *HACK* to support multiple paths for
261 			// MSBuildExtensionsPath property. Normally it would
262 			// get resolved to a single value, but here we special
263 			// case it and try various paths, see the code below
264 			//
265 			// The property itself will resolve to the default
266 			// location though, so you get that in any other part of the
267 			// project.
268 
269 			string envvar = Environment.GetEnvironmentVariable ("MSBuildExtensionsPath");
270 			envvar = String.Join (PathSeparatorAsString, new string [] {
271 						(envvar ?? String.Empty),
272 						// For mac osx, look in the 'External' dir on macosx,
273 						// see bug #663180
274 						MSBuildUtils.RunningOnMac ? MacOSXExternalXBuildDir : String.Empty,
275 						DotConfigExtensionsPath,
276 						Project.DefaultExtensionsPath});
277 
278 			var pathsTable = new Dictionary<string, string> ();
279 			foreach (string extn_path in envvar.Split (new char [] {Path.PathSeparator}, StringSplitOptions.RemoveEmptyEntries)) {
280 				if (pathsTable.ContainsKey (extn_path))
281 					continue;
282 
283 				if (!Directory.Exists (extn_path)) {
284 					if (extn_path != DotConfigExtensionsPath)
285 						project.ParentEngine.LogMessage (
286 							MessageImportance.Low,
287 							"Extension path '{0}' not found, ignoring.",
288 							extn_path);
289 					continue;
290 				}
291 
292 				pathsTable [extn_path] = extn_path;
293 				yield return extn_path;
294 			}
295 		}
296 
297 		public string Condition {
298 			get {
299 				string s = importElement.GetAttribute ("Condition");
300 				return s == String.Empty ? null : s;
301 			}
302 		}
303 
304 		public string EvaluatedProjectPath {
305 			get { return evaluatedProjectPath; }
306 		}
307 
308 		public bool IsImported {
309 			get { return originalProject != null; }
310 		}
311 
312 		public string ProjectPath {
313 			get { return importElement.GetAttribute ("Project"); }
314 		}
315 
316 		internal string ContainedInProjectFileName {
317 			get { return originalProject != null ? originalProject.FullFileName : project.FullFileName; }
318 		}
319 	}
320 }
321