Merge branch 'BigIntegerParse'
[mono.git] / mcs / class / Mono.XBuild.Tasks / Mono.XBuild.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         public 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         public interface IPcFileCacheContext: IPcFileCacheContext<PackageInfo>
54         {
55         }
56         
57         public abstract class PcFileCache: PcFileCache<PackageInfo>
58         {
59                 public PcFileCache (IPcFileCacheContext ctx): base (ctx)
60                 {
61                 }
62         }
63         
64         public 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                 
71                 string cacheFile;
72                 bool hasChanges;
73                 IPcFileCacheContext<TP> ctx;
74                 IEnumerable<string> defaultPaths;
75                 
76                 public PcFileCache (IPcFileCacheContext<TP> ctx)
77                 {
78                         this.ctx = ctx;
79                         try {
80                                 string path = CacheDirectory;
81                                 if (!Directory.Exists (path))
82                                         Directory.CreateDirectory (path);
83                                 cacheFile = Path.Combine (path, "pkgconfig-cache-" + CACHE_VERSION + ".xml");
84                                 
85                                 if (File.Exists (cacheFile))
86                                         Load ();
87                                 
88                         } catch (Exception ex) {
89                                 ctx.ReportError ("pc file cache could not be loaded.", ex);
90                         }
91                 }
92                 
93                 protected abstract string CacheDirectory { get; }
94                 
95                 // Updates the pkg-config index, using the default search directories
96                 public void Update ()
97                 {
98                         Update (GetDefaultPaths ());
99                 }
100
101                 // Updates the pkg-config index, looking for .pc files in the provided directories
102                 // Deletes pkg info entries, of which .pc files don't exist, from cache
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
110                         lock (infos) {
111                                 string[] keys = new string [infos.Count];
112                                 infos.Keys.CopyTo (keys, 0);
113                                 foreach (string key in keys) {
114                                         if (!File.Exists (key)) {
115                                                 infos.Remove (key);
116                                                 hasChanges = true;
117                                         }
118                                 }
119                         }
120
121                         Save ();
122                 }
123                 
124                 public IEnumerable<TP> GetPackages ()
125                 {
126                         return GetPackages (null);
127                 }
128                 
129                 public IEnumerable<TP> GetPackages (IEnumerable<string> pkgConfigDirs)
130                 {
131                         if (pkgConfigDirs == null)
132                                 pkgConfigDirs = GetDefaultPaths ();
133                         else
134                                 pkgConfigDirs = NormaliseAndFilterPaths (pkgConfigDirs, Environment.CurrentDirectory);
135
136                         string[] keys = new string [infos.Count];
137                         TP[] vals = new TP [infos.Count];
138                         lock (infos) {
139                                 infos.Keys.CopyTo (keys, 0);
140                                 infos.Values.CopyTo (vals, 0);
141                         }
142
143                         foreach (string sp in pkgConfigDirs) {
144                                 int i = 0;
145                                 foreach (var file in keys) {
146                                         string dirOfFile = Path.GetFullPath (Path.GetDirectoryName (file));
147                                         if (dirOfFile == sp)
148                                                 yield return vals [i];
149                                         i++;
150                                 }
151                         }
152                 }
153                 
154                 public TP GetPackageInfoByName (string name)
155                 {
156                         return GetPackageInfoByName (name, null);
157                 }
158                 
159                 public TP GetPackageInfoByName (string name, IEnumerable<string> pkgConfigDirs)
160                 {
161                         foreach (TP p in GetPackages (pkgConfigDirs))
162                                 if (p.Name == name)
163                                         return p;
164                         return null;
165                 }
166                 
167                 // Returns information about a .pc file
168                 public TP GetPackageInfo (string file)
169                 {
170                         TP info;
171                         file = Path.GetFullPath (file);
172                         
173                         DateTime wtime = File.GetLastWriteTime (file);
174                         
175                         lock (infos) {
176                                 if (infos.TryGetValue (file, out info)) {
177                                         if (info.LastWriteTime == wtime)
178                                                 return info;
179                                 }
180                         }
181
182                         try {
183                                 info = ParsePackageInfo (file);
184                         } catch (Exception ex) {
185                                 ctx.ReportError ("Error while parsing .pc file: " + file, ex);
186                                 info = new TP ();
187                         }
188                         
189                         lock (infos) {
190                                 if (!info.IsValidPackage)
191                                         info = new TP (); // Create a default empty instance
192                                 info.LastWriteTime = wtime;
193                                 infos [file] = info;
194                                 hasChanges = true;
195                         }
196                         
197                         return info;
198                 }
199                 
200                 FileStream OpenFile (FileAccess access)
201                 {
202                         int retries = 6;
203                         FileMode mode = access == FileAccess.Read ? FileMode.Open : FileMode.Create;
204                         Exception lastException = null;
205                         
206                         while (retries > 0) {
207                                 try {
208                                         return new FileStream (cacheFile, mode, access, FileShare.None);
209                                 } catch (Exception ex) {
210                                         // the file may be locked by another app. Wait a bit and try again
211                                         lastException = ex;
212                                         System.Threading.Thread.Sleep (200);
213                                         retries--;
214                                 }
215                         }
216                         ctx.ReportError ("File could not be opened: " + cacheFile, lastException);
217                         return null;
218                 }
219                 
220                 void Load ()
221                 {
222                         // The serializer can't be used because this file is reused in xbuild
223                         using (FileStream fs = OpenFile (FileAccess.Read)) {
224                                 if (fs == null)
225                                         return;
226                                 XmlTextReader xr = new XmlTextReader (fs);
227                                 xr.MoveToContent ();
228                                 xr.ReadStartElement ();
229                                 xr.MoveToContent ();
230                                 
231                                 while (xr.NodeType == XmlNodeType.Element)
232                                         ReadPackage (xr);
233                         }
234                 }
235                 
236                 public void Save ()
237                 {
238                         // The serializer can't be used because this file is reused in xbuild
239                         lock (infos) {
240                                 if (!hasChanges)
241                                         return;
242                                 
243                                 using (FileStream fs = OpenFile (FileAccess.Write)) {
244                                         if (fs == null)
245                                                 return;
246                                         XmlTextWriter tw = new XmlTextWriter (new StreamWriter (fs));
247                                         tw.Formatting = Formatting.Indented;
248                                         
249                                         tw.WriteStartElement ("PcFileCache");
250                                         foreach (KeyValuePair<string,TP> file in infos) {
251                                                 WritePackage (tw, file.Key, file.Value);
252                                         }
253                                         tw.WriteEndElement (); // PcFileCache
254                                         tw.Flush ();
255                                         
256                                         hasChanges = false;
257                                 }
258                         }
259                 }
260                 
261                 void WritePackage (XmlTextWriter tw, string file, TP pinfo)
262                 {
263                         tw.WriteStartElement ("File");
264                         tw.WriteAttributeString ("path", file);
265                         tw.WriteAttributeString ("lastWriteTime", XmlConvert.ToString (pinfo.LastWriteTime, XmlDateTimeSerializationMode.Local));
266                         
267                         if (pinfo.IsValidPackage) {
268                                 if (pinfo.Name != null)
269                                         tw.WriteAttributeString ("name", pinfo.Name);
270                                 if (pinfo.Version != null)
271                                         tw.WriteAttributeString ("version", pinfo.Version);
272                                 if (!string.IsNullOrEmpty (pinfo.Description))
273                                         tw.WriteAttributeString ("description", pinfo.Description);
274                                 if (!string.IsNullOrEmpty (pinfo.Requires))
275                                         tw.WriteAttributeString ("requires", pinfo.Requires);
276                                 if (pinfo.CustomData != null) {
277                                         foreach (KeyValuePair<string,string> cd in pinfo.CustomData)
278                                                 tw.WriteAttributeString (cd.Key, cd.Value);
279                                 }
280                                 WritePackageContent (tw, file, pinfo);
281                         }
282                         tw.WriteEndElement (); // File
283                 }
284                 
285                 protected virtual void WritePackageContent (XmlTextWriter tw, string file, TP pinfo)
286                 {
287                 }
288                 
289                 void ReadPackage (XmlReader tr)
290                 {
291                         TP pinfo = new TP ();
292                         string file = null;
293                         
294                         tr.MoveToFirstAttribute ();
295                         do {
296                                 switch (tr.LocalName) {
297                                         case "path": file = tr.Value; break;
298                                         case "lastWriteTime": pinfo.LastWriteTime = XmlConvert.ToDateTime (tr.Value, XmlDateTimeSerializationMode.Local); break;
299                                         case "name": pinfo.Name = tr.Value; break;
300                                         case "version": pinfo.Version = tr.Value; break;
301                                         case "description": pinfo.Description = tr.Value; break;
302                                         case "requires": pinfo.Requires = tr.Value; break;
303                                         default: pinfo.SetData (tr.LocalName, tr.Value); break;
304                                 }
305                         } while (tr.MoveToNextAttribute ());
306                         
307                         tr.MoveToElement ();
308                         
309                         if (!tr.IsEmptyElement) {
310                                 tr.ReadStartElement ();
311                                 tr.MoveToContent ();
312                                 ReadPackageContent (tr, pinfo);
313                                 tr.MoveToContent ();
314                                 tr.ReadEndElement ();
315                         } else
316                                 tr.Read ();
317                         tr.MoveToContent ();
318                         
319                         if (!pinfo.IsValidPackage || ctx.IsCustomDataComplete (file, pinfo)) {
320                                 lock (infos)
321                                         infos [file] = pinfo;
322                         }
323                 }
324                 
325                 protected virtual void ReadPackageContent (XmlReader tr, TP pinfo)
326                 {
327                 }
328                 
329                 public object SyncRoot {
330                         get { return infos; }
331                 }
332                 
333                 
334                 TP ParsePackageInfo (string pcfile)
335                 {
336                         PcFile file = new PcFile ();
337                         file.Load (pcfile);
338                         
339                         TP pinfo = new TP ();
340                         pinfo.Name = Path.GetFileNameWithoutExtension (file.FilePath);
341                         
342                         if (!file.HasErrors) {
343                                 pinfo.Version = file.Version;
344                                 pinfo.Description = file.Description;
345                                 pinfo.Requires = file.Requires;
346                                 ParsePackageInfo (file, pinfo);
347                                 if (pinfo.IsValidPackage)
348                                         ctx.StoreCustomData (file, pinfo);
349                         }
350                         return pinfo;
351                 }
352                 
353                 protected virtual void ParsePackageInfo (PcFile file, TP pinfo)
354                 {
355                 }
356                 
357                 IEnumerable<string> GetDefaultPaths ()
358                 {
359                         if (defaultPaths == null) {
360                                 // For mac osx, look in the 'External' dir on macosx,
361                                 // see bug #663180
362                                 string pkgConfigPath = String.Format ("{0}:{1}",
363                                                 Mono.XBuild.Utilities.MSBuildUtils.RunningOnMac ? MacOSXExternalPkgConfigDir : String.Empty,
364                                                 Environment.GetEnvironmentVariable ("PKG_CONFIG_PATH") ?? String.Empty);
365
366                                 string pkgConfigDir = Environment.GetEnvironmentVariable ("PKG_CONFIG_LIBDIR");
367                                 defaultPaths = GetPkgconfigPaths (null, pkgConfigPath, pkgConfigDir);
368                         }
369                         return defaultPaths;
370                 }
371                 
372                 public IEnumerable<string> GetPkgconfigPaths (string prefix, string pkgConfigPath, string pkgConfigLibdir)
373                 {
374                         char[] sep = new char[] { Path.PathSeparator };
375                         
376                         string[] pkgConfigPaths = null;
377                         if (!String.IsNullOrEmpty (pkgConfigPath)) {
378                                 pkgConfigPaths = pkgConfigPath.Split (sep, StringSplitOptions.RemoveEmptyEntries);
379                                 if (pkgConfigPaths.Length == 0)
380                                         pkgConfigPaths = null;
381                         }
382                         
383                         string[] pkgConfigLibdirs = null;
384                         if (!String.IsNullOrEmpty (pkgConfigLibdir)) {
385                                 pkgConfigLibdirs = pkgConfigLibdir.Split (sep, StringSplitOptions.RemoveEmptyEntries);
386                                 if (pkgConfigLibdirs.Length == 0)
387                                         pkgConfigLibdirs = null;
388                         }
389                         
390                         if (prefix == null)
391                                 prefix = PathUp (typeof (int).Assembly.Location, 4);
392                         
393                         IEnumerable<string> paths = GetUnfilteredPkgConfigDirs (pkgConfigPaths, pkgConfigLibdirs, new string [] { prefix });
394                         return NormaliseAndFilterPaths (paths, Environment.CurrentDirectory);
395                 }
396                 
397                 IEnumerable<string> GetUnfilteredPkgConfigDirs (IEnumerable<string> pkgConfigPaths, IEnumerable<string> pkgConfigLibdirs, IEnumerable<string> systemPrefixes)
398                 {
399                         if (pkgConfigPaths != null) {
400                                 foreach (string dir in pkgConfigPaths)
401                                         yield return dir;
402                         }
403                         
404                         if (pkgConfigLibdirs != null) {
405                                 foreach (string dir in pkgConfigLibdirs)
406                                         yield return dir;
407                         } else if (systemPrefixes != null) {
408                                 string[] suffixes = new string [] {
409                                         //FIXME: is this the correct order? share should be before lib but not sure about others.
410                                         Path.Combine ("share", "pkgconfig"),
411                                         Path.Combine ("lib", "pkgconfig"),
412                                         Path.Combine ("lib64", "pkgconfig"),
413                                         Path.Combine ("libdata", "pkgconfig"),
414                                 };
415                                 foreach (string prefix in systemPrefixes)
416                                         foreach (string suffix in suffixes)
417                                                 yield return Path.Combine (prefix, suffix);
418                         }
419                 }
420                 
421                 IEnumerable<string> NormaliseAndFilterPaths (IEnumerable<string> paths, string workingDirectory)
422                 {
423                         Dictionary<string,string> filtered = new Dictionary<string,string> ();
424                         foreach (string p in paths) {
425                                 string path = p;
426                                 if (!Path.IsPathRooted (path))
427                                         path = Path.Combine (workingDirectory, path);
428                                 path = Path.GetFullPath (path);
429                                 if (filtered.ContainsKey (path))
430                                         continue;
431                                 filtered.Add (path,path);
432                                 try {
433                                         if (!Directory.Exists (path))
434                                                 continue;
435                                 } catch (IOException ex) {
436                                         ctx.ReportError ("Error checking for directory '" + path + "'.", ex);
437                                 }
438                                 yield return path;
439                         }
440                 }
441                 
442                 static string PathUp (string path, int up)
443                 {
444                         if (up == 0)
445                                 return path;
446                         for (int i = path.Length -1; i >= 0; i--) {
447                                 if (path[i] == Path.DirectorySeparatorChar) {
448                                         up--;
449                                         if (up == 0)
450                                                 return path.Substring (0, i);
451                                 }
452                         }
453                         return null;
454                 }
455         }
456
457         public class PcFile
458         {
459                 Dictionary<string,string> variables = new Dictionary<string, string> ();
460                 
461                 string description;
462                 public string Description {
463                         get { return description; }
464                         set { description = value; }
465                 }
466                 
467                 string filePath;
468                 public string FilePath {
469                         get { return filePath; }
470                         set { filePath = value; }
471                 }
472                 
473                 bool hasErrors;
474                 public bool HasErrors {
475                         get { return hasErrors; }
476                         set { hasErrors = value; }
477                 }
478                 
479                 string libs;
480                 public string Libs {
481                         get { return libs; }
482                         set { libs = value; }
483                 }
484                 
485                 string name;
486                 public string Name {
487                         get { return name; }
488                         set { name = value; }
489                 }
490                 
491                 string version;
492                 public string Version {
493                         get { return version; }
494                         set { version = value; }
495                 }
496                 
497                 string requires;
498                 public string Requires {
499                         get { return requires; }
500                         set { requires = value; }
501                 }
502                 
503                 public string GetVariable (string varName)
504                 {
505                         string val;
506                         variables.TryGetValue (varName, out val);
507                         return val;
508                 }
509                 
510                 public void Load (string pcfile)
511                 {
512                         FilePath = pcfile;
513                         variables.Add ("pcfiledir", Path.GetDirectoryName (pcfile));
514                         using (StreamReader reader = new StreamReader (pcfile)) {
515                                 string line;
516                                 while ((line = reader.ReadLine ()) != null) {
517                                         int i = line.IndexOf (':');
518                                         int j = line.IndexOf ('=');
519                                         int k = System.Math.Min (i != -1 ? i : int.MaxValue, j != -1 ? j : int.MaxValue);
520                                         if (k == int.MaxValue)
521                                                 continue;
522                                         string var = line.Substring (0, k).Trim ();
523                                         string value = line.Substring (k + 1).Trim ();
524                                         value = Evaluate (value);
525                                         
526                                         if (k == j) {
527                                                 // Is variable
528                                                 variables [var] = value;
529                                         }
530                                         else {
531                                                 switch (var) {
532                                                         case "Name": Name = value; break;
533                                                         case "Description": Description = value; break;
534                                                         case "Version": Version = value; break;
535                                                         case "Libs": Libs = value; break;
536                                                         case "Requires": Requires = value; break;
537                                                 }
538                                         }
539                                 }
540                         }
541                 }
542                 
543                 string Evaluate (string value)
544                 {
545                         int i = value.IndexOf ("${");
546                         if (i == -1)
547                                 return value;
548
549                         StringBuilder sb = new StringBuilder ();
550                         int last = 0;
551                         while (i != -1 && i < value.Length) {
552                                 sb.Append (value.Substring (last, i - last));
553                                 if (i == 0 || value [i - 1] != '$') {
554                                         // Evaluate if var is not escaped
555                                         i += 2;
556                                         int n = value.IndexOf ('}', i);
557                                         if (n == -1 || n == i) {
558                                                 // Closing bracket not found or empty name
559                                                 HasErrors = true;
560                                                 return value;
561                                         }
562                                         string rname = value.Substring (i, n - i);
563                                         string rval;
564                                         if (variables.TryGetValue (rname, out rval))
565                                                 sb.Append (rval);
566                                         else {
567                                                 HasErrors = true;
568                                                 return value;
569                                         }
570                                         i = n + 1;
571                                         last = i;
572                                 } else
573                                         last = i++;
574                                 
575                                 if (i < value.Length)
576                                         i = value.IndexOf ("${", i);
577                         }
578                         sb.Append (value.Substring (last, value.Length - last));
579                         return sb.ToString ();
580                 }
581         }
582         
583         public class PackageInfo
584         {
585                 Dictionary<string,string> customData;
586                 DateTime lastWriteTime;
587                 
588                 string name;
589                 public string Name {
590                         get { return name; }
591                         set { name = value; }
592                 }
593                 
594                 string version;
595                 public string Version {
596                         get { return version; }
597                         set { version = value; }
598                 }
599                 
600                 string description;
601                 public string Description {
602                         get { return description; }
603                         set { description = value; }
604                 }
605                 
606                 string requires;
607                 public string Requires {
608                         get { return requires; }
609                         set { requires = value; }
610                 }
611                 
612                 public string GetData (string name)
613                 {
614                         if (customData == null)
615                                 return null;
616                         string res;
617                         customData.TryGetValue (name, out res);
618                         return res;
619                 }
620                 
621                 public void SetData (string name, string value)
622                 {
623                         if (customData == null)
624                                 customData = new Dictionary<string, string> ();
625                         customData [name] = value;
626                 }
627                 
628                 public void RemoveData (string name)
629                 {
630                         if (customData != null)
631                                 customData.Remove (name);
632                 }
633                 
634                 internal Dictionary<string,string> CustomData {
635                         get { return customData; }
636                 }
637                 
638                 internal DateTime LastWriteTime {
639                         get { return lastWriteTime; }
640                         set { lastWriteTime = value; }
641                 }
642                 
643                 internal bool HasCustomData {
644                         get { return customData != null && customData.Count > 0; }
645                 }
646                 
647                 internal protected virtual bool IsValidPackage {
648                         get { return HasCustomData; }
649                 }
650         }
651 }