Add unit test for AggregateException.GetBaseException that works on .net but is broke...
[mono.git] / mcs / class / Microsoft.Build.Tasks / Microsoft.Build.Tasks / PcFileCache.cs
1 // 
2 // PcFileCache.cs
3 //  
4 // Author:
5 //       Lluis Sanchez Gual <lluis@novell.com>
6 // 
7 // Copyright (c) 2009 Novell, Inc (http://www.novell.com)
8 // 
9 // Permission is hereby granted, free of charge, to any person obtaining a copy
10 // of this software and associated documentation files (the "Software"), to deal
11 // in the Software without restriction, including without limitation the rights
12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 // copies of the Software, and to permit persons to whom the Software is
14 // furnished to do so, subject to the following conditions:
15 // 
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
18 // 
19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 // THE SOFTWARE.
26
27 using System;
28 using System.Text;
29 using System.Xml;
30 using System.IO;
31 using System.Collections.Generic;
32
33 // IMPORTANT IMPORTANT IMPORTANT IMPORTANT IMPORTANT IMPORTANT
34 // This code is shared with xbuild, which has to build with .NET 2.0,
35 // so no c# 3.0 syntax is allowed here.
36
37 namespace Mono.PkgConfig
38 {
39         internal interface IPcFileCacheContext<TP> where TP:PackageInfo, new()
40         {
41                 // In the implementation of this method, the host application can extract
42                 // information from the pc file and store it in the PackageInfo object
43                 void StoreCustomData (PcFile pcfile, TP pkg);
44                 
45                 // Should return false if the provided package does not have required
46                 // custom data
47                 bool IsCustomDataComplete (string pcfile, TP pkg);
48                 
49                 // Called to report errors
50                 void ReportError (string message, Exception ex);
51         }
52         
53         internal interface IPcFileCacheContext: IPcFileCacheContext<PackageInfo>
54         {
55         }
56         
57         internal abstract class PcFileCache: PcFileCache<PackageInfo>
58         {
59                 public PcFileCache (IPcFileCacheContext ctx): base (ctx)
60                 {
61                 }
62         }
63         
64         internal abstract class PcFileCache<TP> where TP:PackageInfo, new()
65         {
66                 const string CACHE_VERSION = "2";
67                 const string MacOSXExternalPkgConfigDir = "/Library/Frameworks/Mono.framework/External/pkgconfig";
68                 
69                 Dictionary<string, TP> infos = new Dictionary<string, TP> ();
70                 Dictionary<string, List<TP>> filesByFolder = new Dictionary<string, List<TP>> ();
71                 
72                 string cacheFile;
73                 bool hasChanges;
74                 IPcFileCacheContext<TP> ctx;
75                 IEnumerable<string> defaultPaths;
76                 
77                 public PcFileCache (IPcFileCacheContext<TP> ctx)
78                 {
79                         this.ctx = ctx;
80                         try {
81                                 string path = CacheDirectory;
82                                 if (!Directory.Exists (path))
83                                         Directory.CreateDirectory (path);
84                                 cacheFile = Path.Combine (path, "pkgconfig-cache-" + CACHE_VERSION + ".xml");
85                                 
86                                 if (File.Exists (cacheFile))
87                                         Load ();
88                                 
89                         } catch (Exception ex) {
90                                 ctx.ReportError ("pc file cache could not be loaded.", ex);
91                         }
92                 }
93                 
94                 protected abstract string CacheDirectory { get; }
95                 
96                 // Updates the pkg-config index, using the default search directories
97                 public void Update ()
98                 {
99                         Update (GetDefaultPaths ());
100                 }
101
102                 // Updates the pkg-config index, looking for .pc files in the provided directories
103                 public void Update (IEnumerable<string> pkgConfigDirs)
104                 {
105                         foreach (string pcdir in pkgConfigDirs) {
106                                 foreach (string pcfile in Directory.GetFiles (pcdir, "*.pc"))
107                                         GetPackageInfo (pcfile);
108                         }
109                         Save ();
110                 }
111                 
112                 public IEnumerable<TP> GetPackages ()
113                 {
114                         return GetPackages (null);
115                 }
116                 
117                 public IEnumerable<TP> GetPackages (IEnumerable<string> pkgConfigDirs)
118                 {
119                         if (pkgConfigDirs == null)
120                                 pkgConfigDirs = GetDefaultPaths ();
121
122                         foreach (string sp in pkgConfigDirs) {
123                                 List<TP> list;
124                                 if (filesByFolder.TryGetValue (Path.GetFullPath (sp), out list)) {
125                                         foreach (TP p in list)
126                                                 yield return p;
127                                 }
128                         }
129                 }
130                 
131                 public TP GetPackageInfoByName (string name)
132                 {
133                         return GetPackageInfoByName (name, null);
134                 }
135                 
136                 public TP GetPackageInfoByName (string name, IEnumerable<string> pkgConfigDirs)
137                 {
138                         foreach (TP p in GetPackages (pkgConfigDirs))
139                                 if (p.Name == name)
140                                         return p;
141                         return null;
142                 }
143                 
144                 // Returns information about a .pc file
145                 public TP GetPackageInfo (string file)
146                 {
147                         TP info, oldInfo = null;
148                         file = Path.GetFullPath (file);
149                         
150                         DateTime wtime = File.GetLastWriteTime (file);
151                         
152                         lock (infos) {
153                                 if (infos.TryGetValue (file, out info)) {
154                                         if (info.LastWriteTime == wtime)
155                                                 return info;
156                                         oldInfo = info;
157                                 }
158                         }
159
160                         try {
161                                 info = ParsePackageInfo (file);
162                         } catch (Exception ex) {
163                                 ctx.ReportError ("Error while parsing .pc file: " + file, ex);
164                                 info = new TP ();
165                         }
166                         
167                         lock (infos) {
168                                 if (!info.IsValidPackage)
169                                         info = new TP (); // Create a default empty instance
170                                 info.LastWriteTime = wtime;
171                                 Add (file, info, oldInfo);
172                                 hasChanges = true;
173                         }
174                         
175                         return info;
176                 }
177                 
178                 void Add (string file, TP info, TP replacedInfo)
179                 {
180                         infos [file] = info;
181                         string dir = Path.GetFullPath (Path.GetDirectoryName (file));
182                         List<TP> list;
183                         if (!filesByFolder.TryGetValue (dir, out list)) {
184                                 list = new List<TP> ();
185                                 filesByFolder [dir] = list;
186                         }
187                         if (replacedInfo != null) {
188                                 int i = list.IndexOf (replacedInfo);
189                                 if (i != -1) {
190                                         list [i] = info;
191                                         return;
192                                 }
193                         }
194                         list.Add (info);
195                 }
196                 
197                 FileStream OpenFile (FileAccess access)
198                 {
199                         int retries = 6;
200                         FileMode mode = access == FileAccess.Read ? FileMode.Open : FileMode.Create;
201                         Exception lastException = null;
202                         
203                         while (retries > 0) {
204                                 try {
205                                         return new FileStream (cacheFile, mode, access, FileShare.None);
206                                 } catch (Exception ex) {
207                                         // the file may be locked by another app. Wait a bit and try again
208                                         lastException = ex;
209                                         System.Threading.Thread.Sleep (200);
210                                         retries--;
211                                 }
212                         }
213                         ctx.ReportError ("File could not be opened: " + cacheFile, lastException);
214                         return null;
215                 }
216                 
217                 void Load ()
218                 {
219                         // The serializer can't be used because this file is reused in xbuild
220                         using (FileStream fs = OpenFile (FileAccess.Read)) {
221                                 if (fs == null)
222                                         return;
223                                 XmlTextReader xr = new XmlTextReader (fs);
224                                 xr.MoveToContent ();
225                                 xr.ReadStartElement ();
226                                 xr.MoveToContent ();
227                                 
228                                 while (xr.NodeType == XmlNodeType.Element)
229                                         ReadPackage (xr);
230                         }
231                 }
232                 
233                 public void Save ()
234                 {
235                         // The serializer can't be used because this file is reused in xbuild
236                         lock (infos) {
237                                 if (!hasChanges)
238                                         return;
239                                 
240                                 using (FileStream fs = OpenFile (FileAccess.Write)) {
241                                         if (fs == null)
242                                                 return;
243                                         XmlTextWriter tw = new XmlTextWriter (new StreamWriter (fs));
244                                         tw.Formatting = Formatting.Indented;
245                                         
246                                         tw.WriteStartElement ("PcFileCache");
247                                         foreach (KeyValuePair<string,TP> file in infos) {
248                                                 WritePackage (tw, file.Key, file.Value);
249                                         }
250                                         tw.WriteEndElement (); // PcFileCache
251                                         tw.Flush ();
252                                         
253                                         hasChanges = false;
254                                 }
255                         }
256                 }
257                 
258                 void WritePackage (XmlTextWriter tw, string file, TP pinfo)
259                 {
260                         tw.WriteStartElement ("File");
261                         tw.WriteAttributeString ("path", file);
262                         tw.WriteAttributeString ("lastWriteTime", XmlConvert.ToString (pinfo.LastWriteTime, XmlDateTimeSerializationMode.Local));
263                         
264                         if (pinfo.IsValidPackage) {
265                                 if (pinfo.Name != null)
266                                         tw.WriteAttributeString ("name", pinfo.Name);
267                                 if (pinfo.Version != null)
268                                         tw.WriteAttributeString ("version", pinfo.Version);
269                                 if (!string.IsNullOrEmpty (pinfo.Description))
270                                         tw.WriteAttributeString ("description", pinfo.Description);
271                                 if (!string.IsNullOrEmpty (pinfo.Requires))
272                                         tw.WriteAttributeString ("requires", pinfo.Requires);
273                                 if (pinfo.CustomData != null) {
274                                         foreach (KeyValuePair<string,string> cd in pinfo.CustomData)
275                                                 tw.WriteAttributeString (cd.Key, cd.Value);
276                                 }
277                                 WritePackageContent (tw, file, pinfo);
278                         }
279                         tw.WriteEndElement (); // File
280                 }
281                 
282                 protected virtual void WritePackageContent (XmlTextWriter tw, string file, TP pinfo)
283                 {
284                 }
285                 
286                 void ReadPackage (XmlReader tr)
287                 {
288                         TP pinfo = new TP ();
289                         string file = null;
290                         
291                         tr.MoveToFirstAttribute ();
292                         do {
293                                 switch (tr.LocalName) {
294                                         case "path": file = tr.Value; break;
295                                         case "lastWriteTime": pinfo.LastWriteTime = XmlConvert.ToDateTime (tr.Value, XmlDateTimeSerializationMode.Local); break;
296                                         case "name": pinfo.Name = tr.Value; break;
297                                         case "version": pinfo.Version = tr.Value; break;
298                                         case "description": pinfo.Description = tr.Value; break;
299                                         case "requires": pinfo.Requires = tr.Value; break;
300                                         default: pinfo.SetData (tr.LocalName, tr.Value); break;
301                                 }
302                         } while (tr.MoveToNextAttribute ());
303                         
304                         tr.MoveToElement ();
305                         
306                         if (!tr.IsEmptyElement) {
307                                 tr.ReadStartElement ();
308                                 tr.MoveToContent ();
309                                 ReadPackageContent (tr, pinfo);
310                                 tr.MoveToContent ();
311                                 tr.ReadEndElement ();
312                         } else
313                                 tr.Read ();
314                         tr.MoveToContent ();
315                         
316                         if (!pinfo.IsValidPackage || ctx.IsCustomDataComplete (file, pinfo))
317                                 Add (file, pinfo, null);
318                 }
319                 
320                 protected virtual void ReadPackageContent (XmlReader tr, TP pinfo)
321                 {
322                 }
323                 
324                 public object SyncRoot {
325                         get { return infos; }
326                 }
327                 
328                 
329                 TP ParsePackageInfo (string pcfile)
330                 {
331                         PcFile file = new PcFile ();
332                         file.Load (pcfile);
333                         
334                         TP pinfo = new TP ();
335                         pinfo.Name = Path.GetFileNameWithoutExtension (file.FilePath);
336                         
337                         if (!file.HasErrors) {
338                                 pinfo.Version = file.Version;
339                                 pinfo.Description = file.Description;
340                                 pinfo.Requires = file.Requires;
341                                 ParsePackageInfo (file, pinfo);
342                                 if (pinfo.IsValidPackage)
343                                         ctx.StoreCustomData (file, pinfo);
344                         }
345                         return pinfo;
346                 }
347                 
348                 protected virtual void ParsePackageInfo (PcFile file, TP pinfo)
349                 {
350                 }
351                 
352                 IEnumerable<string> GetDefaultPaths ()
353                 {
354                         if (defaultPaths == null) {
355                                 // For mac osx, look in the 'External' dir on macosx,
356                                 // see bug #663180
357                                 string pkgConfigPath = String.Format ("{0}:{1}",
358                                                 Mono.XBuild.Utilities.MSBuildUtils.RunningOnMac ? MacOSXExternalPkgConfigDir : String.Empty,
359                                                 Environment.GetEnvironmentVariable ("PKG_CONFIG_PATH") ?? String.Empty);
360
361                                 string pkgConfigDir = Environment.GetEnvironmentVariable ("PKG_CONFIG_LIBDIR");
362                                 defaultPaths = GetPkgconfigPaths (null, pkgConfigPath, pkgConfigDir);
363                         }
364                         return defaultPaths;
365                 }
366                 
367                 public IEnumerable<string> GetPkgconfigPaths (string prefix, string pkgConfigPath, string pkgConfigLibdir)
368                 {
369                         char[] sep = new char[] { Path.PathSeparator };
370                         
371                         string[] pkgConfigPaths = null;
372                         if (!String.IsNullOrEmpty (pkgConfigPath)) {
373                                 pkgConfigPaths = pkgConfigPath.Split (sep, StringSplitOptions.RemoveEmptyEntries);
374                                 if (pkgConfigPaths.Length == 0)
375                                         pkgConfigPaths = null;
376                         }
377                         
378                         string[] pkgConfigLibdirs = null;
379                         if (!String.IsNullOrEmpty (pkgConfigLibdir)) {
380                                 pkgConfigLibdirs = pkgConfigLibdir.Split (sep, StringSplitOptions.RemoveEmptyEntries);
381                                 if (pkgConfigLibdirs.Length == 0)
382                                         pkgConfigLibdirs = null;
383                         }
384                         
385                         if (prefix == null)
386                                 prefix = PathUp (typeof (int).Assembly.Location, 4);
387                         
388                         IEnumerable<string> paths = GetUnfilteredPkgConfigDirs (pkgConfigPaths, pkgConfigLibdirs, new string [] { prefix });
389                         return NormaliseAndFilterPaths (paths, Environment.CurrentDirectory);
390                 }
391                 
392                 IEnumerable<string> GetUnfilteredPkgConfigDirs (IEnumerable<string> pkgConfigPaths, IEnumerable<string> pkgConfigLibdirs, IEnumerable<string> systemPrefixes)
393                 {
394                         if (pkgConfigPaths != null) {
395                                 foreach (string dir in pkgConfigPaths)
396                                         yield return dir;
397                         }
398                         
399                         if (pkgConfigLibdirs != null) {
400                                 foreach (string dir in pkgConfigLibdirs)
401                                         yield return dir;
402                         } else if (systemPrefixes != null) {
403                                 string[] suffixes = new string [] {
404                                         //FIXME: is this the correct order? share should be before lib but not sure about others.
405                                         Path.Combine ("share", "pkgconfig"),
406                                         Path.Combine ("lib", "pkgconfig"),
407                                         Path.Combine ("lib64", "pkgconfig"),
408                                         Path.Combine ("libdata", "pkgconfig"),
409                                 };
410                                 foreach (string prefix in systemPrefixes)
411                                         foreach (string suffix in suffixes)
412                                                 yield return Path.Combine (prefix, suffix);
413                         }
414                 }
415                 
416                 IEnumerable<string> NormaliseAndFilterPaths (IEnumerable<string> paths, string workingDirectory)
417                 {
418                         Dictionary<string,string> filtered = new Dictionary<string,string> ();
419                         foreach (string p in paths) {
420                                 string path = p;
421                                 if (!Path.IsPathRooted (path))
422                                         path = Path.Combine (workingDirectory, path);
423                                 path = Path.GetFullPath (path);
424                                 if (filtered.ContainsKey (path))
425                                         continue;
426                                 filtered.Add (path,path);
427                                 try {
428                                         if (!Directory.Exists (path))
429                                                 continue;
430                                 } catch (IOException ex) {
431                                         ctx.ReportError ("Error checking for directory '" + path + "'.", ex);
432                                 }
433                                 yield return path;
434                         }
435                 }
436                 
437                 static string PathUp (string path, int up)
438                 {
439                         if (up == 0)
440                                 return path;
441                         for (int i = path.Length -1; i >= 0; i--) {
442                                 if (path[i] == Path.DirectorySeparatorChar) {
443                                         up--;
444                                         if (up == 0)
445                                                 return path.Substring (0, i);
446                                 }
447                         }
448                         return null;
449                 }
450         }
451
452         internal class PcFile
453         {
454                 Dictionary<string,string> variables = new Dictionary<string, string> ();
455                 
456                 string description;
457                 public string Description {
458                         get { return description; }
459                         set { description = value; }
460                 }
461                 
462                 string filePath;
463                 public string FilePath {
464                         get { return filePath; }
465                         set { filePath = value; }
466                 }
467                 
468                 bool hasErrors;
469                 public bool HasErrors {
470                         get { return hasErrors; }
471                         set { hasErrors = value; }
472                 }
473                 
474                 string libs;
475                 public string Libs {
476                         get { return libs; }
477                         set { libs = value; }
478                 }
479                 
480                 string name;
481                 public string Name {
482                         get { return name; }
483                         set { name = value; }
484                 }
485                 
486                 string version;
487                 public string Version {
488                         get { return version; }
489                         set { version = value; }
490                 }
491                 
492                 string requires;
493                 public string Requires {
494                         get { return requires; }
495                         set { requires = value; }
496                 }
497                 
498                 public string GetVariable (string varName)
499                 {
500                         string val;
501                         variables.TryGetValue (varName, out val);
502                         return val;
503                 }
504                 
505                 public void Load (string pcfile)
506                 {
507                         FilePath = pcfile;
508                         variables.Add ("pcfiledir", Path.GetDirectoryName (pcfile));
509                         using (StreamReader reader = new StreamReader (pcfile)) {
510                                 string line;
511                                 while ((line = reader.ReadLine ()) != null) {
512                                         int i = line.IndexOf (':');
513                                         int j = line.IndexOf ('=');
514                                         int k = System.Math.Min (i != -1 ? i : int.MaxValue, j != -1 ? j : int.MaxValue);
515                                         if (k == int.MaxValue)
516                                                 continue;
517                                         string var = line.Substring (0, k).Trim ();
518                                         string value = line.Substring (k + 1).Trim ();
519                                         value = Evaluate (value);
520                                         
521                                         if (k == j) {
522                                                 // Is variable
523                                                 variables [var] = value;
524                                         }
525                                         else {
526                                                 switch (var) {
527                                                         case "Name": Name = value; break;
528                                                         case "Description": Description = value; break;
529                                                         case "Version": Version = value; break;
530                                                         case "Libs": Libs = value; break;
531                                                         case "Requires": Requires = value; break;
532                                                 }
533                                         }
534                                 }
535                         }
536                 }
537                 
538                 string Evaluate (string value)
539                 {
540                         int i = value.IndexOf ("${");
541                         if (i == -1)
542                                 return value;
543
544                         StringBuilder sb = new StringBuilder ();
545                         int last = 0;
546                         while (i != -1 && i < value.Length) {
547                                 sb.Append (value.Substring (last, i - last));
548                                 if (i == 0 || value [i - 1] != '$') {
549                                         // Evaluate if var is not escaped
550                                         i += 2;
551                                         int n = value.IndexOf ('}', i);
552                                         if (n == -1 || n == i) {
553                                                 // Closing bracket not found or empty name
554                                                 HasErrors = true;
555                                                 return value;
556                                         }
557                                         string rname = value.Substring (i, n - i);
558                                         string rval;
559                                         if (variables.TryGetValue (rname, out rval))
560                                                 sb.Append (rval);
561                                         else {
562                                                 HasErrors = true;
563                                                 return value;
564                                         }
565                                         i = n + 1;
566                                         last = i;
567                                 } else
568                                         last = i++;
569                                 
570                                 if (i < value.Length)
571                                         i = value.IndexOf ("${", i);
572                         }
573                         sb.Append (value.Substring (last, value.Length - last));
574                         return sb.ToString ();
575                 }
576         }
577         
578         internal class PackageInfo
579         {
580                 Dictionary<string,string> customData;
581                 DateTime lastWriteTime;
582                 
583                 string name;
584                 public string Name {
585                         get { return name; }
586                         set { name = value; }
587                 }
588                 
589                 string version;
590                 public string Version {
591                         get { return version; }
592                         set { version = value; }
593                 }
594                 
595                 string description;
596                 public string Description {
597                         get { return description; }
598                         set { description = value; }
599                 }
600                 
601                 string requires;
602                 public string Requires {
603                         get { return requires; }
604                         set { requires = value; }
605                 }
606                 
607                 public string GetData (string name)
608                 {
609                         if (customData == null)
610                                 return null;
611                         string res;
612                         customData.TryGetValue (name, out res);
613                         return res;
614                 }
615                 
616                 public void SetData (string name, string value)
617                 {
618                         if (customData == null)
619                                 customData = new Dictionary<string, string> ();
620                         customData [name] = value;
621                 }
622                 
623                 public void RemoveData (string name)
624                 {
625                         if (customData != null)
626                                 customData.Remove (name);
627                 }
628                 
629                 internal Dictionary<string,string> CustomData {
630                         get { return customData; }
631                 }
632                 
633                 internal DateTime LastWriteTime {
634                         get { return lastWriteTime; }
635                         set { lastWriteTime = value; }
636                 }
637                 
638                 internal bool HasCustomData {
639                         get { return customData != null && customData.Count > 0; }
640                 }
641                 
642                 internal protected virtual bool IsValidPackage {
643                         get { return HasCustomData; }
644                 }
645         }
646 }