8c21891659c4da7501ca4bccfd032dcd282257e3
[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                         if (!Directory.Exists (framework_dir))
122                                 return null;
123                         
124                         AssemblyName key_aname = new AssemblyName (reference.ItemSpec);
125                         TargetFrameworkAssemblies gac_asm;
126                         if (!target_framework_cache.TryGetValue (framework_dir, out gac_asm)) {
127                                 // fill gac_asm
128                                 gac_asm = target_framework_cache [framework_dir] = PopulateTargetFrameworkAssemblies (framework_dir);
129                         }
130
131                         KeyValuePair<AssemblyName, string> pair;
132                         if (gac_asm.NameToAssemblyNameCache.TryGetValue (key_aname.Name, out pair)) {
133                                 if (AssemblyNamesCompatible (key_aname, pair.Key, specific_version)) {
134                                         // gac and tgt frmwk refs are not copied private
135                                         return GetResolvedReference (reference, pair.Value, pair.Key, false,
136                                                         SearchPath.TargetFrameworkDirectory);
137                                 }
138
139                                 LogSearchMessage ("Considered target framework dir {0}, assembly name '{1}' did not " +
140                                                 "match the expected '{2}' (SpecificVersion={3})",
141                                                 framework_dir, pair.Key, key_aname, specific_version);
142                         } else {
143                                 LogSearchMessage ("Considered target framework dir {0}, assembly named '{1}' not found.",
144                                                 framework_dir, key_aname.Name);
145                         }
146                         return null;
147                 }
148
149                 // Look for %(Identity).{dll|exe|..}
150                 // if specific_version==true
151                 //      resolve if assembly names match
152                 // else
153                 //      resolve the valid assembly
154                 public ResolvedReference FindInDirectory (ITaskItem reference, string directory, string [] file_extensions, bool specific_version)
155                 {
156                         string filename = reference.ItemSpec;
157                         int comma_pos = filename.IndexOf (',');
158                         if (comma_pos >= 0)
159                                 filename = filename.Substring (0, comma_pos);
160
161                         // Try as a filename
162                         string path = Path.GetFullPath (Path.Combine (directory, filename));
163                         AssemblyName aname = specific_version ? new AssemblyName (reference.ItemSpec) : null;
164
165                         ResolvedReference resolved_ref = ResolveReferenceForPath (path, reference, aname, null, SearchPath.Directory, specific_version);
166                         if (resolved_ref != null)
167                                 return resolved_ref;
168
169                         // try path + Include + {.dll|.exe|..}
170                         foreach (string extn in file_extensions) {
171                                 resolved_ref = ResolveReferenceForPath (path + extn, reference, aname, null, SearchPath.Directory, specific_version);
172                                 if (resolved_ref != null)
173                                         return resolved_ref;
174                         }
175
176                         return null;
177                 }
178
179                 // tries to resolve reference from the given file path, and compares assembly names
180                 // if @specific_version == true, and logs accordingly
181                 ResolvedReference ResolveReferenceForPath (string filename, ITaskItem reference, AssemblyName aname,
182                                         string error_message, SearchPath spath, bool specific_version)
183                 {
184                         AssemblyName found_aname = GetAssemblyNameFromFile (filename);
185                         if (found_aname == null) {
186                                 if (error_message != null)
187                                         log.LogMessage (MessageImportance.Low, error_message);
188                                 return null;
189                         }
190
191                         if (!specific_version || AssemblyNamesCompatible (aname, found_aname, specific_version)) {
192                                 // Check compatibility only if specific_version == true
193                                 return GetResolvedReference (reference, filename, found_aname, true, spath);
194                         } else {
195                                 LogSearchMessage ("Considered '{0}', but assembly name '{1}' did not match the " +
196                                                 "expected '{2}' (SpecificVersion={3})", filename, found_aname, aname, specific_version);
197                                 log.LogMessage (MessageImportance.Low, "Assembly names are not compatible.");
198                         }
199
200                         return null;
201                 }
202
203                 TargetFrameworkAssemblies PopulateTargetFrameworkAssemblies (string directory)
204                 {
205                         TargetFrameworkAssemblies gac_asm = new TargetFrameworkAssemblies (directory);
206                         foreach (string file in Directory.GetFiles (directory, "*.dll")) {
207                                 AssemblyName aname = GetAssemblyNameFromFile (file);
208                                 if (aname != null)
209                                         gac_asm.NameToAssemblyNameCache [aname.Name] =
210                                                 new KeyValuePair<AssemblyName, string> (aname, file);
211                         }
212
213                         return gac_asm;
214                 }
215
216                 public ResolvedReference ResolveGacReference (ITaskItem reference, bool specific_version)
217                 {
218                         AssemblyName name = new AssemblyName (reference.ItemSpec);
219                         if (!gac.ContainsKey (name.Name)) {
220                                 LogSearchMessage ("Considered {0}, but could not find in the GAC.",
221                                                 reference.ItemSpec);
222                                 return null;
223                         }
224
225                         if (name.Version != null) {
226                                 string ret;
227                                 if (gac [name.Name].TryGetValue (name.Version, out ret))
228                                         return GetResolvedReference (reference, ret, name, false, SearchPath.Gac);
229
230                                 // not found
231                                 if (specific_version) {
232                                         LogSearchMessage ("Considered '{0}', but an assembly with the specific version not found.",
233                                                         reference.ItemSpec);
234                                         return null;
235                                 }
236                         }
237
238                         Version [] versions = new Version [gac [name.Name].Keys.Count];
239                         gac [name.Name].Keys.CopyTo (versions, 0);
240                         Array.Sort (versions, (IComparer <Version>) null);
241                         Version highest = versions [versions.Length - 1];
242                         //FIXME: the aname being used here isn't correct, its version should
243                         //       actually match "highest"
244                         return GetResolvedReference (reference, gac [name.Name] [highest], name, false, SearchPath.Gac);
245                 }
246
247                 public ResolvedReference ResolvePkgConfigReference (ITaskItem reference, bool specific_version)
248                 {
249                         PackageAssemblyInfo pkg = null;
250
251                         if (specific_version) {
252                                 pkg = PcCache.GetAssemblyLocation (reference.ItemSpec);
253                         } else {
254                                 // if not specific version, then just match simple name
255                                 string name = reference.ItemSpec;
256                                 if (name.IndexOf (',') > 0)
257                                         name = name.Substring (0, name.IndexOf (','));
258                                 pkg = PcCache.ResolveAssemblyName (name).FirstOrDefault ();
259                         }
260
261                         if (pkg == null) {
262                                 LogSearchMessage ("Considered {0}, but could not find in any pkg-config files.",
263                                                 reference.ItemSpec);
264                                 return null;
265                         }
266
267                         ResolvedReference rr = GetResolvedReference (reference, pkg.File, new AssemblyName (pkg.FullName),
268                                                 false, SearchPath.PkgConfig);
269                         rr.FoundInSearchPathAsString = String.Format ("{{PkgConfig}} provided by package named {0}",
270                                                         pkg.ParentPackage.Name);
271
272                         return rr;
273                 }
274
275                 // HintPath has a valid assembly
276                 // if specific_version==true
277                 //      resolve if assembly names match
278                 // else
279                 //      resolve the valid assembly
280                 public ResolvedReference ResolveHintPathReference (ITaskItem reference, bool specific_version)
281                 {
282                         string hintpath = reference.GetMetadata ("HintPath");
283                         if (String.IsNullOrEmpty (hintpath)) {
284                                 LogSearchMessage ("HintPath attribute not found");
285                                 return null;
286                         }
287
288                         if (!File.Exists (hintpath)) {
289                                 log.LogMessage (MessageImportance.Low, "HintPath {0} does not exist.", hintpath);
290                                 LogSearchMessage ("Considered {0}, but it does not exist.", hintpath);
291                                 return null;
292                         }
293
294                         return ResolveReferenceForPath (hintpath, reference, new AssemblyName (reference.ItemSpec),
295                                                 String.Format ("File at HintPath {0}, is either an invalid assembly or the file does not exist.", hintpath),
296                                                 SearchPath.HintPath, specific_version);
297                 }
298
299                 public AssemblyName GetAssemblyNameFromFile (string filename)
300                 {
301                         AssemblyName aname = null;
302                         filename = Path.GetFullPath (filename);
303                         try {
304                                 aname = AssemblyName.GetAssemblyName (filename);
305                         } catch (FileNotFoundException) {
306                                 LogSearchMessage ("Considered '{0}' as a file, but the file does not exist",
307                                                 filename);
308                         } catch (BadImageFormatException) {
309                                 LogSearchMessage ("Considered '{0}' as a file, but it is an invalid assembly",
310                                                 filename);
311                         }
312
313                         return aname;
314                 }
315
316                 internal static bool AssemblyNamesCompatible (AssemblyName a, AssemblyName b, bool specificVersion)
317                 {
318                         return AssemblyNamesCompatible (a, b, specificVersion, true);
319                 }
320
321                 // if @specificVersion is true then match full name, else just the simple name
322                 internal static bool AssemblyNamesCompatible (AssemblyName a, AssemblyName b, bool specificVersion,
323                                 bool ignoreCase)
324                 {
325                         if (String.Compare (a.Name, b.Name, ignoreCase) != 0)
326                                 return false;
327
328                         if (!specificVersion)
329                                 // ..and simple names match
330                                 return true;
331
332                         if (a.CultureInfo != null && !a.CultureInfo.Equals (b.CultureInfo))
333                                 return false;
334
335                         if (a.Version != null && a.Version != b.Version)
336                                 return false;
337
338                         byte [] a_bytes = a.GetPublicKeyToken ();
339                         byte [] b_bytes = b.GetPublicKeyToken ();
340
341                         bool a_is_empty = (a_bytes == null || a_bytes.Length == 0);
342                         bool b_is_empty = (b_bytes == null || b_bytes.Length == 0);
343
344                         if (a_is_empty && b_is_empty)
345                                 return true;
346
347                         if (a_is_empty || b_is_empty)
348                                 return false;
349
350                         for (int i = 0; i < a_bytes.Length; i++)
351                                 if (a_bytes [i] != b_bytes [i])
352                                         return false;
353
354                         return true;
355                 }
356
357                 public bool IsStrongNamed (AssemblyName name)
358                 {
359                         return (name.Version != null &&
360                                         name.GetPublicKeyToken () != null &&
361                                         name.GetPublicKeyToken ().Length != 0);
362                 }
363
364                 // FIXME: to get default values of CopyLocal, compare with TargetFrameworkDirectories
365
366                 // If metadata 'Private' is present then use that or use @default_copy_local_value
367                 // as the value for CopyLocal
368                 internal ResolvedReference GetResolvedReference (ITaskItem reference, string filename,
369                                 AssemblyName aname, bool default_copy_local_value, SearchPath search_path)
370                 {
371                         string pvt = reference.GetMetadata ("Private");
372
373                         bool copy_local = default_copy_local_value;
374                         if (!String.IsNullOrEmpty (pvt))
375                                 //FIXME: log a warning for invalid value
376                                 Boolean.TryParse (pvt, out copy_local);
377
378                         ITaskItem new_item = new TaskItem (reference);
379                         new_item.ItemSpec = filename;
380                         return new ResolvedReference (new_item, aname, copy_local, search_path, reference.ItemSpec);
381                 }
382
383                 public void LogSearchMessage (string msg, params object [] args)
384                 {
385                         search_log.Add (String.Format (msg, args));
386                 }
387
388                 public void LogSearchLoggerMessages (MessageImportance importance)
389                 {
390                         foreach (string msg in search_log)
391                                 log.LogMessage (importance, msg);
392                 }
393
394                 public TaskLoggingHelper Log {
395                         set {
396                                 log = value;
397                                 PcFileCacheContext.Log = value;
398                         }
399                 }
400
401                 static LibraryPcFileCache PcCache  {
402                         get {
403                                 if (cache == null) {
404                                         var context = new PcFileCacheContext ();
405                                         cache = new LibraryPcFileCache (context);
406                                         cache.Update ();
407                                 }
408
409                                 return cache;
410                         }
411                 }
412         }
413
414         class TargetFrameworkAssemblies {
415                 public string Path;
416
417                 // assembly (simple) name -> (AssemblyName, file path)
418                 public Dictionary <string, KeyValuePair<AssemblyName, string>> NameToAssemblyNameCache;
419
420                 public TargetFrameworkAssemblies (string path)
421                 {
422                         this.Path = path;
423                         NameToAssemblyNameCache = new Dictionary<string, KeyValuePair<AssemblyName, string>> (
424                                         StringComparer.InvariantCultureIgnoreCase);
425                 }
426         }
427
428         class PcFileCacheContext : IPcFileCacheContext<LibraryPackageInfo>
429         {
430                 public static TaskLoggingHelper Log;
431
432                 // In the implementation of this method, the host application can extract
433                 // information from the pc file and store it in the PackageInfo object
434                 public void StoreCustomData (PcFile pcfile, LibraryPackageInfo pkg)
435                 {
436                 }
437
438                 // Should return false if the provided package does not have required
439                 // custom data
440                 public bool IsCustomDataComplete (string pcfile, LibraryPackageInfo pkg)
441                 {
442                         return true;
443                 }
444
445                 // Called to report errors
446                 public void ReportError (string message, Exception ex)
447                 {
448                         Log.LogMessage (MessageImportance.Low, "Error loading pkg-config files: {0} : {1}",
449                                         message, ex.ToString ());
450                 }
451         }
452
453         enum SearchPath
454         {
455                 Gac,
456                 TargetFrameworkDirectory,
457                 CandidateAssemblies,
458                 HintPath,
459                 Directory,
460                 RawFileName,
461                 PkgConfig
462         }
463 }
464
465
466
467 #endif