5 // Lluis Sanchez Gual <lluis@novell.com>
7 // Copyright (c) 2009 Novell, Inc (http://www.novell.com)
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:
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
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
31 using System.Collections.Generic;
33 namespace Mono.PkgConfig
35 internal interface IPcFileCacheContext<TP> where TP:PackageInfo, new()
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);
41 // Should return false if the provided package does not have required
43 bool IsCustomDataComplete (string pcfile, TP pkg);
45 // Called to report errors
46 void ReportError (string message, Exception ex);
49 internal interface IPcFileCacheContext: IPcFileCacheContext<PackageInfo>
53 internal abstract class PcFileCache: PcFileCache<PackageInfo>
55 public PcFileCache (IPcFileCacheContext ctx): base (ctx)
60 internal abstract class PcFileCache<TP> where TP:PackageInfo, new()
62 const string CACHE_VERSION = "2";
64 Dictionary<string, TP> infos = new Dictionary<string, TP> ();
65 Dictionary<string, List<TP>> filesByFolder = new Dictionary<string, List<TP>> ();
69 IPcFileCacheContext<TP> ctx;
70 IEnumerable<string> defaultPaths;
72 public PcFileCache (IPcFileCacheContext<TP> ctx)
76 string path = CacheDirectory;
77 if (!Directory.Exists (path))
78 Directory.CreateDirectory (path);
79 cacheFile = Path.Combine (path, "pkgconfig-cache-" + CACHE_VERSION + ".xml");
81 if (File.Exists (cacheFile))
84 } catch (Exception ex) {
85 ctx.ReportError ("pc file cache could not be loaded.", ex);
89 protected abstract string CacheDirectory { get; }
91 // Updates the pkg-config index, using the default search directories
94 Update (GetDefaultPaths ());
97 // Updates the pkg-config index, looking for .pc files in the provided directories
98 public void Update (IEnumerable<string> pkgConfigDirs)
100 foreach (string pcdir in pkgConfigDirs) {
101 foreach (string pcfile in Directory.GetFiles (pcdir, "*.pc"))
102 GetPackageInfo (pcfile);
107 public IEnumerable<TP> GetPackages ()
109 return GetPackages (null);
112 public IEnumerable<TP> GetPackages (IEnumerable<string> pkgConfigDirs)
114 if (pkgConfigDirs == null)
115 pkgConfigDirs = GetDefaultPaths ();
117 foreach (string sp in pkgConfigDirs) {
119 if (filesByFolder.TryGetValue (Path.GetFullPath (sp), out list)) {
120 foreach (TP p in list)
126 public TP GetPackageInfoByName (string name)
128 return GetPackageInfoByName (name, null);
131 public TP GetPackageInfoByName (string name, IEnumerable<string> pkgConfigDirs)
133 foreach (TP p in GetPackages (pkgConfigDirs))
139 // Returns information about a .pc file
140 public TP GetPackageInfo (string file)
142 TP info, oldInfo = null;
143 file = Path.GetFullPath (file);
145 DateTime wtime = File.GetLastWriteTime (file);
148 if (infos.TryGetValue (file, out info)) {
149 if (info.LastWriteTime == wtime)
156 info = ParsePackageInfo (file);
157 } catch (Exception ex) {
158 ctx.ReportError ("Error while parsing .pc file", ex);
163 if (!info.IsValidPackage)
164 info = new TP (); // Create a default empty instance
165 info.LastWriteTime = wtime;
166 Add (file, info, oldInfo);
173 void Add (string file, TP info, TP replacedInfo)
176 string dir = Path.GetFullPath (Path.GetDirectoryName (file));
178 if (!filesByFolder.TryGetValue (dir, out list)) {
179 list = new List<TP> ();
180 filesByFolder [dir] = list;
182 if (replacedInfo != null) {
183 int i = list.IndexOf (replacedInfo);
192 FileStream OpenFile (FileAccess access)
195 FileMode mode = access == FileAccess.Read ? FileMode.Open : FileMode.Create;
196 Exception lastException = null;
198 while (retries > 0) {
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
204 System.Threading.Thread.Sleep (200);
208 ctx.ReportError ("File could not be opened: " + cacheFile, lastException);
214 // The serializer can't be used because this file is reused in xbuild
215 using (FileStream fs = OpenFile (FileAccess.Read)) {
218 XmlTextReader xr = new XmlTextReader (fs);
220 xr.ReadStartElement ();
223 while (xr.NodeType == XmlNodeType.Element)
230 // The serializer can't be used because this file is reused in xbuild
235 using (FileStream fs = OpenFile (FileAccess.Write)) {
238 XmlTextWriter tw = new XmlTextWriter (new StreamWriter (fs));
239 tw.Formatting = Formatting.Indented;
241 tw.WriteStartElement ("PcFileCache");
242 foreach (KeyValuePair<string,TP> file in infos) {
243 WritePackage (tw, file.Key, file.Value);
245 tw.WriteEndElement (); // PcFileCache
253 void WritePackage (XmlTextWriter tw, string file, TP pinfo)
255 tw.WriteStartElement ("File");
256 tw.WriteAttributeString ("path", file);
257 tw.WriteAttributeString ("lastWriteTime", XmlConvert.ToString (pinfo.LastWriteTime, XmlDateTimeSerializationMode.Local));
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);
270 WritePackageContent (tw, file, pinfo);
272 tw.WriteEndElement (); // File
275 protected virtual void WritePackageContent (XmlTextWriter tw, string file, TP pinfo)
279 void ReadPackage (XmlReader tr)
281 TP pinfo = new TP ();
284 tr.MoveToFirstAttribute ();
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;
294 } while (tr.MoveToNextAttribute ());
298 if (!tr.IsEmptyElement) {
299 tr.ReadStartElement ();
301 ReadPackageContent (tr, pinfo);
303 tr.ReadEndElement ();
308 if (!pinfo.IsValidPackage || ctx.IsCustomDataComplete (file, pinfo))
309 Add (file, pinfo, null);
312 protected virtual void ReadPackageContent (XmlReader tr, TP pinfo)
316 public object SyncRoot {
317 get { return infos; }
321 TP ParsePackageInfo (string pcfile)
323 PcFile file = new PcFile ();
326 TP pinfo = new TP ();
327 pinfo.Name = Path.GetFileNameWithoutExtension (file.FilePath);
329 if (!file.HasErrors) {
330 pinfo.Version = file.Version;
331 pinfo.Description = file.Description;
332 ParsePackageInfo (file, pinfo);
333 ctx.StoreCustomData (file, pinfo);
338 protected virtual void ParsePackageInfo (PcFile file, TP pinfo)
342 IEnumerable<string> GetDefaultPaths ()
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);
352 public IEnumerable<string> GetPkgconfigPaths (string prefix, string pkgConfigPath, string pkgConfigLibdir)
354 char[] sep = new char[] { Path.PathSeparator };
356 string[] pkgConfigPaths = null;
357 if (!String.IsNullOrEmpty (pkgConfigPath)) {
358 pkgConfigPaths = pkgConfigPath.Split (sep, StringSplitOptions.RemoveEmptyEntries);
359 if (pkgConfigPaths.Length == 0)
360 pkgConfigPaths = null;
363 string[] pkgConfigLibdirs = null;
364 if (!String.IsNullOrEmpty (pkgConfigLibdir)) {
365 pkgConfigLibdirs = pkgConfigLibdir.Split (sep, StringSplitOptions.RemoveEmptyEntries);
366 if (pkgConfigLibdirs.Length == 0)
367 pkgConfigLibdirs = null;
371 prefix = PathUp (typeof (int).Assembly.Location, 4);
373 IEnumerable<string> paths = GetUnfilteredPkgConfigDirs (pkgConfigPaths, pkgConfigLibdirs, new string [] { prefix });
374 return NormaliseAndFilterPaths (paths, Environment.CurrentDirectory);
377 IEnumerable<string> GetUnfilteredPkgConfigDirs (IEnumerable<string> pkgConfigPaths, IEnumerable<string> pkgConfigLibdirs, IEnumerable<string> systemPrefixes)
379 if (pkgConfigPaths != null) {
380 foreach (string dir in pkgConfigPaths)
384 if (pkgConfigLibdirs != null) {
385 foreach (string dir in pkgConfigLibdirs)
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"),
394 foreach (string prefix in systemPrefixes)
395 foreach (string suffix in suffixes)
396 yield return Path.Combine (prefix, suffix);
400 IEnumerable<string> NormaliseAndFilterPaths (IEnumerable<string> paths, string workingDirectory)
402 Dictionary<string,string> filtered = new Dictionary<string,string> ();
403 foreach (string p in paths) {
405 if (!Path.IsPathRooted (path))
406 path = Path.Combine (workingDirectory, path);
407 path = Path.GetFullPath (path);
408 if (filtered.ContainsKey (path))
410 filtered.Add (path,path);
412 if (!Directory.Exists (path))
414 } catch (IOException ex) {
415 ctx.ReportError ("Error checking for directory '" + path + "'.", ex);
421 static string PathUp (string path, int up)
425 for (int i = path.Length -1; i >= 0; i--) {
426 if (path[i] == Path.DirectorySeparatorChar) {
429 return path.Substring (0, i);
436 internal class PcFile
438 Dictionary<string,string> variables = new Dictionary<string, string> ();
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; }
447 public string GetVariable (string varName)
450 variables.TryGetValue (varName, out val);
454 public void Load (string pcfile)
457 variables.Add ("pcfiledir", Path.GetDirectoryName (pcfile));
458 using (StreamReader reader = new StreamReader (pcfile)) {
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)
466 string var = line.Substring (0, k).Trim ();
467 string value = line.Substring (k + 1).Trim ();
468 value = Evaluate (value);
472 variables [var] = value;
476 case "Name": Name = value; break;
477 case "Description": Description = value; break;
478 case "Version": Version = value; break;
479 case "Libs": Libs = value; break;
486 string Evaluate (string value)
488 int i = value.IndexOf ("${");
492 StringBuilder sb = new StringBuilder ();
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
499 int n = value.IndexOf ('}', i);
500 if (n == -1 || n == i) {
501 // Closing bracket not found or empty name
505 string rname = value.Substring (i, n - i);
507 if (variables.TryGetValue (rname, out rval))
518 if (i < value.Length - 1)
519 i = value.IndexOf ("${", i);
521 sb.Append (value.Substring (last, value.Length - last));
522 return sb.ToString ();
526 internal class PackageInfo
528 Dictionary<string,string> customData;
530 public string Name { get; set; }
532 public string Version { get; set; }
534 public string Description { get; set; }
536 public string GetData (string name)
538 if (customData == null)
541 customData.TryGetValue (name, out res);
545 public void SetData (string name, string value)
547 if (customData == null)
548 customData = new Dictionary<string, string> ();
549 customData [name] = value;
552 public void RemoveData (string name)
554 if (customData != null)
555 customData.Remove (name);
558 internal Dictionary<string,string> CustomData {
559 get { return customData; }
562 internal DateTime LastWriteTime { get; set; }
564 internal bool HasCustomData {
565 get { return customData != null && customData.Count > 0; }
568 internal protected virtual bool IsValidPackage {
569 get { return HasCustomData; }