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