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