Merge pull request #631 from kebby/master
[mono.git] / mcs / class / Microsoft.Build.Tasks / Microsoft.Build.Tasks / AssemblyResolver.cs
1 //
2 // AssemblyResolver.cs
3 //
4 // Author:
5 //   Marek Sieradzki (marek.sieradzki@gmail.com)
6 //   Ankit Jain (jankit@novell.com)
7 // 
8 // (C) 2006 Marek Sieradzki
9 // Copyright 2009 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 #if NET_2_0
31
32 using System;
33 using System.Collections.Generic;
34 using System.IO;
35 using System.Linq;
36 using System.Reflection;
37 using System.Security;
38 using Microsoft.Build.Framework;
39 using Microsoft.Build.Utilities;
40 using Mono.PkgConfig;
41
42 namespace Microsoft.Build.Tasks {
43         internal class AssemblyResolver {
44
45                 // name -> (version -> assemblypath)
46                 static Dictionary<string, TargetFrameworkAssemblies> target_framework_cache;
47                 static Dictionary<string, Dictionary<Version, string>> gac;
48                 TaskLoggingHelper log;
49                 List<string> search_log;
50
51                 static LibraryPcFileCache cache;
52
53                 public AssemblyResolver ()
54                 {
55                         if (gac == null) {
56                                 gac = new Dictionary<string, Dictionary<Version, string>> ();
57                                 target_framework_cache = new Dictionary <string, TargetFrameworkAssemblies> ();
58
59                                 GatherGacAssemblies ();
60                         }
61                 }
62
63                 public void ResetSearchLogger ()
64                 {
65                         if (search_log == null)
66                                 search_log = new List<string> ();
67                         else
68                                 search_log.Clear ();
69                 }
70
71                 string GetGacPath ()
72                 {
73                         // NOTE: code from mcs/tools/gacutil/driver.cs
74                         PropertyInfo gac = typeof (System.Environment).GetProperty ("GacPath", BindingFlags.Static | BindingFlags.NonPublic);
75
76                         if (gac == null)
77                                 return null;
78
79                         MethodInfo get_gac = gac.GetGetMethod (true);
80                         return (string) get_gac.Invoke (null, null);
81                 }
82
83                 void GatherGacAssemblies ()
84                 {
85                         string gac_path = GetGacPath ();
86                         if (gac_path == null)
87                                 throw new InvalidOperationException ("XBuild must be run on Mono runtime");
88                         if (!Directory.Exists (gac_path))
89                                 return; // in case mono isn't "installed".
90
91                         Version version;
92                         DirectoryInfo version_info, assembly_info;
93
94                         foreach (string assembly_name in Directory.GetDirectories (gac_path)) {
95                                 assembly_info = new DirectoryInfo (assembly_name);
96                                 foreach (string version_token in Directory.GetDirectories (assembly_name)) {
97                                         foreach (string file in Directory.GetFiles (version_token, "*.dll")) {
98                                                 version_info = new DirectoryInfo (version_token);
99                                                 version = new Version (version_info.Name.Split (
100                                                         new char [] {'_'}, StringSplitOptions.RemoveEmptyEntries) [0]);
101
102                                                 Dictionary<Version, string> assembliesByVersion = new Dictionary <Version, string> ();
103                                                 if (!gac.TryGetValue (assembly_info.Name, out assembliesByVersion)) {
104                                                         assembliesByVersion = new Dictionary <Version, string> ();
105                                                         gac.Add (assembly_info.Name, assembliesByVersion);
106                                                 }
107
108                                                 string found_file;
109                                                 if (assembliesByVersion.TryGetValue (version, out found_file) &&
110                                                         File.GetLastWriteTime (file) <= File.GetLastWriteTime (found_file))
111                                                                 // Duplicate found, take the newer file
112                                                                 continue;
113
114                                                 assembliesByVersion [version] = file;
115                                         }
116                                 }
117                         }
118                 }
119
120                 public ResolvedReference FindInTargetFramework (ITaskItem reference, string framework_dir, bool specific_version)
121                 {
122                         if (!Directory.Exists (framework_dir))
123                                 return null;
124                         
125                         AssemblyName key_aname;
126                         if (!TryGetAssemblyNameFromFullName (reference.ItemSpec, out key_aname))
127                                 return null;
128
129                         TargetFrameworkAssemblies gac_asm;
130                         if (!target_framework_cache.TryGetValue (framework_dir, out gac_asm)) {
131                                 // fill gac_asm
132                                 gac_asm = target_framework_cache [framework_dir] = PopulateTargetFrameworkAssemblies (framework_dir);
133                         }
134
135                         KeyValuePair<AssemblyName, string> pair;
136                         if (gac_asm.NameToAssemblyNameCache.TryGetValue (key_aname.Name, out pair)) {
137                                 if (AssemblyNamesCompatible (key_aname, pair.Key, specific_version)) {
138                                         // gac and tgt frmwk refs are not copied private
139                                         return GetResolvedReference (reference, pair.Value, pair.Key, false,
140                                                         SearchPath.TargetFrameworkDirectory);
141                                 }
142
143                                 LogSearchMessage ("Considered target framework dir {0}, assembly name '{1}' did not " +
144                                                 "match the expected '{2}' (SpecificVersion={3})",
145                                                 framework_dir, pair.Key, key_aname, specific_version);
146                         } else {
147                                 LogSearchMessage ("Considered target framework dir {0}, assembly named '{1}' not found.",
148                                                 framework_dir, key_aname.Name);
149                         }
150                         return null;
151                 }
152
153                 // Look for %(Identity).{dll|exe|..}
154                 // if specific_version==true
155                 //      resolve if assembly names match
156                 // else
157                 //      resolve the valid assembly
158                 public ResolvedReference FindInDirectory (ITaskItem reference, string directory, string [] file_extensions, bool specific_version)
159                 {
160                         string filename = reference.ItemSpec;
161                         int comma_pos = filename.IndexOf (',');
162                         if (comma_pos >= 0)
163                                 filename = filename.Substring (0, comma_pos);
164
165                         // Try as a filename
166                         string path = Path.GetFullPath (Path.Combine (directory, filename));
167                         AssemblyName aname = null;
168                         if (specific_version && !TryGetAssemblyNameFromFullName (reference.ItemSpec, out aname))
169                                 return null;
170
171                         ResolvedReference resolved_ref = ResolveReferenceForPath (path, reference, aname, null, SearchPath.Directory, specific_version);
172                         if (resolved_ref != null)
173                                 return resolved_ref;
174
175                         // try path + Include + {.dll|.exe|..}
176                         foreach (string extn in file_extensions) {
177                                 resolved_ref = ResolveReferenceForPath (path + extn, reference, aname, null, SearchPath.Directory, specific_version);
178                                 if (resolved_ref != null)
179                                         return resolved_ref;
180                         }
181
182                         return null;
183                 }
184
185                 // tries to resolve reference from the given file path, and compares assembly names
186                 // if @specific_version == true, and logs accordingly
187                 ResolvedReference ResolveReferenceForPath (string filename, ITaskItem reference, AssemblyName aname,
188                                         string error_message, SearchPath spath, bool specific_version)
189                 {
190                         AssemblyName found_aname;
191                         if (!TryGetAssemblyNameFromFile (filename, out found_aname)) {
192                                 if (error_message != null)
193                                         log.LogMessage (MessageImportance.Low, error_message);
194                                 return null;
195                         }
196
197                         if (!specific_version || AssemblyNamesCompatible (aname, found_aname, specific_version)) {
198                                 // Check compatibility only if specific_version == true
199                                 return GetResolvedReference (reference, filename, found_aname, true, spath);
200                         } else {
201                                 LogSearchMessage ("Considered '{0}', but assembly name '{1}' did not match the " +
202                                                 "expected '{2}' (SpecificVersion={3})", filename, found_aname, aname, specific_version);
203                                 log.LogMessage (MessageImportance.Low, "Assembly names are not compatible.");
204                         }
205
206                         return null;
207                 }
208
209                 TargetFrameworkAssemblies PopulateTargetFrameworkAssemblies (string directory)
210                 {
211                         TargetFrameworkAssemblies gac_asm = new TargetFrameworkAssemblies (directory);
212                         foreach (string file in Directory.GetFiles (directory, "*.dll")) {
213                                 AssemblyName aname;
214                                 if (TryGetAssemblyNameFromFile (file, out aname))
215                                         gac_asm.NameToAssemblyNameCache [aname.Name] =
216                                                 new KeyValuePair<AssemblyName, string> (aname, file);
217                         }
218
219                         return gac_asm;
220                 }
221
222                 public ResolvedReference ResolveGacReference (ITaskItem reference, bool specific_version)
223                 {
224                         AssemblyName name;
225                         if (!TryGetAssemblyNameFromFullName (reference.ItemSpec, out name))
226                                 return null;
227
228                         if (!gac.ContainsKey (name.Name)) {
229                                 LogSearchMessage ("Considered {0}, but could not find in the GAC.",
230                                                 reference.ItemSpec);
231                                 return null;
232                         }
233
234                         if (name.Version != null) {
235                                 string ret;
236                                 if (gac [name.Name].TryGetValue (name.Version, out ret))
237                                         return GetResolvedReference (reference, ret, name, false, SearchPath.Gac);
238
239                                 // not found
240                                 if (specific_version) {
241                                         LogSearchMessage ("Considered '{0}', but an assembly with the specific version not found.",
242                                                         reference.ItemSpec);
243                                         return null;
244                                 }
245                         }
246
247                         Version [] versions = new Version [gac [name.Name].Keys.Count];
248                         gac [name.Name].Keys.CopyTo (versions, 0);
249                         Array.Sort (versions, (IComparer <Version>) null);
250                         Version highest = versions [versions.Length - 1];
251                         //FIXME: the aname being used here isn't correct, its version should
252                         //       actually match "highest"
253                         return GetResolvedReference (reference, gac [name.Name] [highest], name, false, SearchPath.Gac);
254                 }
255
256                 public ResolvedReference ResolvePkgConfigReference (ITaskItem reference, bool specific_version)
257                 {
258                         PackageAssemblyInfo pkg = null;
259
260                         if (specific_version) {
261                                 pkg = PcCache.GetAssemblyLocation (reference.ItemSpec);
262                         } else {
263                                 // if not specific version, then just match simple name
264                                 string name = reference.ItemSpec;
265                                 if (name.IndexOf (',') > 0)
266                                         name = name.Substring (0, name.IndexOf (','));
267                                 pkg = PcCache.ResolveAssemblyName (name).FirstOrDefault ();
268                         }
269
270                         if (pkg == null) {
271                                 LogSearchMessage ("Considered {0}, but could not find in any pkg-config files.",
272                                                 reference.ItemSpec);
273                                 return null;
274                         }
275
276                         AssemblyName aname;
277                         if (!TryGetAssemblyNameFromFullName (pkg.FullName, out aname))
278                                 return null;
279
280                         ResolvedReference rr = GetResolvedReference (reference, pkg.File, aname,
281                                                 false, SearchPath.PkgConfig);
282                         rr.FoundInSearchPathAsString = String.Format ("{{PkgConfig}} provided by package named {0}",
283                                                         pkg.ParentPackage.Name);
284
285                         return rr;
286                 }
287
288                 // HintPath has a valid assembly
289                 // if specific_version==true
290                 //      resolve if assembly names match
291                 // else
292                 //      resolve the valid assembly
293                 public ResolvedReference ResolveHintPathReference (ITaskItem reference, bool specific_version)
294                 {
295                         string hintpath = reference.GetMetadata ("HintPath");
296                         if (String.IsNullOrEmpty (hintpath)) {
297                                 LogSearchMessage ("HintPath attribute not found");
298                                 return null;
299                         }
300
301                         if (!File.Exists (hintpath)) {
302                                 log.LogMessage (MessageImportance.Low, "HintPath {0} does not exist.", hintpath);
303                                 LogSearchMessage ("Considered {0}, but it does not exist.", hintpath);
304                                 return null;
305                         }
306
307                         AssemblyName aname;
308                         if (!TryGetAssemblyNameFromFullName (reference.ItemSpec, out aname))
309                                 return null;
310
311                         return ResolveReferenceForPath (hintpath, reference, aname,
312                                                 String.Format ("File at HintPath {0}, is either an invalid assembly or the file does not exist.", hintpath),
313                                                 SearchPath.HintPath, specific_version);
314                 }
315
316                 static Dictionary<string, AssemblyName> assemblyNameCache = new Dictionary<string, AssemblyName> ();
317                 public bool TryGetAssemblyNameFromFile (string filename, out AssemblyName aname)
318                 {
319                         filename = Path.GetFullPath (filename);
320                         if (assemblyNameCache.TryGetValue (filename, out aname))
321                             return aname != null;
322
323                         aname = null;
324                         try {
325                                 aname = AssemblyName.GetAssemblyName (filename);
326                         } catch (FileNotFoundException) {
327                                 LogSearchMessage ("Considered '{0}' as a file, but the file does not exist",
328                                                 filename);
329                         } catch (BadImageFormatException) {
330                                 LogSearchMessage ("Considered '{0}' as a file, but it is an invalid assembly",
331                                                 filename);
332                         }
333
334                         assemblyNameCache [filename] = aname;
335                         return aname != null;
336                 }
337
338                 bool TryGetAssemblyNameFromFullName (string full_name, out AssemblyName aname)
339                 {
340                         aname = null;
341                         try {
342                                 aname = new AssemblyName (full_name);
343                         } catch (FileLoadException) {
344                                 LogSearchMessage ("Considered '{0}' as an assembly name, but it is invalid.", full_name);
345                         }
346
347                         return aname != null;
348                 }
349
350                 internal static bool AssemblyNamesCompatible (AssemblyName a, AssemblyName b, bool specificVersion)
351                 {
352                         return AssemblyNamesCompatible (a, b, specificVersion, true);
353                 }
354
355                 // if @specificVersion is true then match full name, else just the simple name
356                 internal static bool AssemblyNamesCompatible (AssemblyName a, AssemblyName b, bool specificVersion,
357                                 bool ignoreCase)
358                 {
359                         if (String.Compare (a.Name, b.Name, ignoreCase) != 0)
360                                 return false;
361
362                         if (!specificVersion)
363                                 // ..and simple names match
364                                 return true;
365
366                         if (a.CultureInfo != null && !a.CultureInfo.Equals (b.CultureInfo))
367                                 return false;
368
369                         if (a.Version != null && a.Version != b.Version)
370                                 return false;
371
372                         byte [] a_bytes = a.GetPublicKeyToken ();
373                         byte [] b_bytes = b.GetPublicKeyToken ();
374
375                         bool a_is_empty = (a_bytes == null || a_bytes.Length == 0);
376                         bool b_is_empty = (b_bytes == null || b_bytes.Length == 0);
377
378                         if (a_is_empty || b_is_empty)
379                                 return true;
380
381                         for (int i = 0; i < a_bytes.Length; i++)
382                                 if (a_bytes [i] != b_bytes [i])
383                                         return false;
384
385                         return true;
386                 }
387
388                 public bool IsStrongNamed (AssemblyName name)
389                 {
390                         return (name.Version != null &&
391                                         name.GetPublicKeyToken () != null &&
392                                         name.GetPublicKeyToken ().Length != 0);
393                 }
394
395                 // FIXME: to get default values of CopyLocal, compare with TargetFrameworkDirectories
396
397                 // If metadata 'Private' is present then use that or use @default_copy_local_value
398                 // as the value for CopyLocal
399                 internal ResolvedReference GetResolvedReference (ITaskItem reference, string filename,
400                                 AssemblyName aname, bool default_copy_local_value, SearchPath search_path)
401                 {
402                         string pvt = reference.GetMetadata ("Private");
403
404                         bool copy_local = default_copy_local_value;
405                         if (!String.IsNullOrEmpty (pvt))
406                                 //FIXME: log a warning for invalid value
407                                 Boolean.TryParse (pvt, out copy_local);
408
409                         ITaskItem new_item = new TaskItem (reference);
410                         new_item.ItemSpec = filename;
411                         return new ResolvedReference (new_item, aname, copy_local, search_path, reference.ItemSpec);
412                 }
413
414                 public void LogSearchMessage (string msg, params object [] args)
415                 {
416                         search_log.Add (String.Format (msg, args));
417                 }
418
419                 public void LogSearchLoggerMessages (MessageImportance importance)
420                 {
421                         foreach (string msg in search_log)
422                                 log.LogMessage (importance, msg);
423                 }
424
425                 public TaskLoggingHelper Log {
426                         set {
427                                 log = value;
428                                 PcFileCacheContext.Log = value;
429                         }
430                 }
431
432                 static LibraryPcFileCache PcCache  {
433                         get {
434                                 if (cache == null) {
435                                         var context = new PcFileCacheContext ();
436                                         cache = new LibraryPcFileCache (context);
437                                         cache.Update ();
438                                 }
439
440                                 return cache;
441                         }
442                 }
443         }
444
445         class TargetFrameworkAssemblies {
446                 public string Path;
447
448                 // assembly (simple) name -> (AssemblyName, file path)
449                 public Dictionary <string, KeyValuePair<AssemblyName, string>> NameToAssemblyNameCache;
450
451                 public TargetFrameworkAssemblies (string path)
452                 {
453                         this.Path = path;
454                         NameToAssemblyNameCache = new Dictionary<string, KeyValuePair<AssemblyName, string>> (
455                                         StringComparer.OrdinalIgnoreCase);
456                 }
457         }
458
459         class PcFileCacheContext : IPcFileCacheContext<LibraryPackageInfo>
460         {
461                 public static TaskLoggingHelper Log;
462
463                 // In the implementation of this method, the host application can extract
464                 // information from the pc file and store it in the PackageInfo object
465                 public void StoreCustomData (PcFile pcfile, LibraryPackageInfo pkg)
466                 {
467                 }
468
469                 // Should return false if the provided package does not have required
470                 // custom data
471                 public bool IsCustomDataComplete (string pcfile, LibraryPackageInfo pkg)
472                 {
473                         return true;
474                 }
475
476                 // Called to report errors
477                 public void ReportError (string message, Exception ex)
478                 {
479                         Log.LogMessage (MessageImportance.Low, "Error loading pkg-config files: {0} : {1}",
480                                         message, ex.ToString ());
481                 }
482         }
483
484         enum SearchPath
485         {
486                 Gac,
487                 TargetFrameworkDirectory,
488                 CandidateAssemblies,
489                 HintPath,
490                 Directory,
491                 RawFileName,
492                 PkgConfig
493         }
494 }
495
496
497
498 #endif