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