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 // 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.
37 namespace Mono.PkgConfig
39 public interface IPcFileCacheContext<TP> where TP:PackageInfo, new()
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);
45 // Should return false if the provided package does not have required
47 bool IsCustomDataComplete (string pcfile, TP pkg);
49 // Called to report errors
50 void ReportError (string message, Exception ex);
53 public interface IPcFileCacheContext: IPcFileCacheContext<PackageInfo>
57 public abstract class PcFileCache: PcFileCache<PackageInfo>
59 public PcFileCache (IPcFileCacheContext ctx): base (ctx)
64 public abstract class PcFileCache<TP> where TP:PackageInfo, new()
66 const string CACHE_VERSION = "2";
67 const string MacOSXExternalPkgConfigDir = "/Library/Frameworks/Mono.framework/External/pkgconfig";
69 Dictionary<string, TP> infos = new Dictionary<string, TP> ();
73 IPcFileCacheContext<TP> ctx;
74 IEnumerable<string> defaultPaths;
76 public PcFileCache (IPcFileCacheContext<TP> ctx)
80 string path = CacheDirectory;
81 if (!Directory.Exists (path))
82 Directory.CreateDirectory (path);
83 cacheFile = Path.Combine (path, "pkgconfig-cache-" + CACHE_VERSION + ".xml");
85 if (File.Exists (cacheFile))
88 } catch (Exception ex) {
89 ctx.ReportError ("pc file cache could not be loaded.", ex);
93 protected abstract string CacheDirectory { get; }
95 // Updates the pkg-config index, using the default search directories
98 Update (GetDefaultPaths ());
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)
105 foreach (string pcdir in pkgConfigDirs) {
106 foreach (string pcfile in Directory.GetFiles (pcdir, "*.pc"))
107 GetPackageInfo (pcfile);
111 string[] keys = new string [infos.Count];
112 infos.Keys.CopyTo (keys, 0);
113 foreach (string key in keys) {
114 if (!File.Exists (key)) {
124 public IEnumerable<TP> GetPackages ()
126 return GetPackages (null);
129 public IEnumerable<TP> GetPackages (IEnumerable<string> pkgConfigDirs)
131 if (pkgConfigDirs == null)
132 pkgConfigDirs = GetDefaultPaths ();
134 pkgConfigDirs = NormaliseAndFilterPaths (pkgConfigDirs, Environment.CurrentDirectory);
136 string[] keys = new string [infos.Count];
137 TP[] vals = new TP [infos.Count];
139 infos.Keys.CopyTo (keys, 0);
140 infos.Values.CopyTo (vals, 0);
143 foreach (string sp in pkgConfigDirs) {
145 foreach (var file in keys) {
146 string dirOfFile = Path.GetFullPath (Path.GetDirectoryName (file));
148 yield return vals [i];
154 public TP GetPackageInfoByName (string name)
156 return GetPackageInfoByName (name, null);
159 public TP GetPackageInfoByName (string name, IEnumerable<string> pkgConfigDirs)
161 foreach (TP p in GetPackages (pkgConfigDirs))
167 // Returns information about a .pc file
168 public TP GetPackageInfo (string file)
171 file = Path.GetFullPath (file);
173 DateTime wtime = File.GetLastWriteTime (file);
176 if (infos.TryGetValue (file, out info)) {
177 if (info.LastWriteTime == wtime)
183 info = ParsePackageInfo (file);
184 } catch (Exception ex) {
185 ctx.ReportError ("Error while parsing .pc file: " + file, ex);
190 if (!info.IsValidPackage)
191 info = new TP (); // Create a default empty instance
192 info.LastWriteTime = wtime;
200 FileStream OpenFile (FileAccess access)
203 FileMode mode = access == FileAccess.Read ? FileMode.Open : FileMode.Create;
204 Exception lastException = null;
206 while (retries > 0) {
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
212 System.Threading.Thread.Sleep (200);
216 ctx.ReportError ("File could not be opened: " + cacheFile, lastException);
222 // The serializer can't be used because this file is reused in xbuild
223 using (FileStream fs = OpenFile (FileAccess.Read)) {
226 XmlTextReader xr = new XmlTextReader (fs);
228 xr.ReadStartElement ();
231 while (xr.NodeType == XmlNodeType.Element)
238 // The serializer can't be used because this file is reused in xbuild
243 using (FileStream fs = OpenFile (FileAccess.Write)) {
246 XmlTextWriter tw = new XmlTextWriter (new StreamWriter (fs));
247 tw.Formatting = Formatting.Indented;
249 tw.WriteStartElement ("PcFileCache");
250 foreach (KeyValuePair<string,TP> file in infos) {
251 WritePackage (tw, file.Key, file.Value);
253 tw.WriteEndElement (); // PcFileCache
261 void WritePackage (XmlTextWriter tw, string file, TP pinfo)
263 tw.WriteStartElement ("File");
264 tw.WriteAttributeString ("path", file);
265 tw.WriteAttributeString ("lastWriteTime", XmlConvert.ToString (pinfo.LastWriteTime, XmlDateTimeSerializationMode.Local));
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);
280 WritePackageContent (tw, file, pinfo);
282 tw.WriteEndElement (); // File
285 protected virtual void WritePackageContent (XmlTextWriter tw, string file, TP pinfo)
289 void ReadPackage (XmlReader tr)
291 TP pinfo = new TP ();
294 tr.MoveToFirstAttribute ();
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;
305 } while (tr.MoveToNextAttribute ());
309 if (!tr.IsEmptyElement) {
310 tr.ReadStartElement ();
312 ReadPackageContent (tr, pinfo);
314 tr.ReadEndElement ();
319 if (!pinfo.IsValidPackage || ctx.IsCustomDataComplete (file, pinfo)) {
321 infos [file] = pinfo;
325 protected virtual void ReadPackageContent (XmlReader tr, TP pinfo)
329 public object SyncRoot {
330 get { return infos; }
334 TP ParsePackageInfo (string pcfile)
336 PcFile file = new PcFile ();
339 TP pinfo = new TP ();
340 pinfo.Name = Path.GetFileNameWithoutExtension (file.FilePath);
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);
353 protected virtual void ParsePackageInfo (PcFile file, TP pinfo)
357 IEnumerable<string> GetDefaultPaths ()
359 if (defaultPaths == null) {
360 // For mac osx, look in the 'External' dir on macosx,
362 string pkgConfigPath = String.Format ("{0}:{1}",
363 Mono.XBuild.Utilities.MSBuildUtils.RunningOnMac ? MacOSXExternalPkgConfigDir : String.Empty,
364 Environment.GetEnvironmentVariable ("PKG_CONFIG_PATH") ?? String.Empty);
366 string pkgConfigDir = Environment.GetEnvironmentVariable ("PKG_CONFIG_LIBDIR");
367 defaultPaths = GetPkgconfigPaths (null, pkgConfigPath, pkgConfigDir);
372 public IEnumerable<string> GetPkgconfigPaths (string prefix, string pkgConfigPath, string pkgConfigLibdir)
374 char[] sep = new char[] { Path.PathSeparator };
376 string[] pkgConfigPaths = null;
377 if (!String.IsNullOrEmpty (pkgConfigPath)) {
378 pkgConfigPaths = pkgConfigPath.Split (sep, StringSplitOptions.RemoveEmptyEntries);
379 if (pkgConfigPaths.Length == 0)
380 pkgConfigPaths = null;
383 string[] pkgConfigLibdirs = null;
384 if (!String.IsNullOrEmpty (pkgConfigLibdir)) {
385 pkgConfigLibdirs = pkgConfigLibdir.Split (sep, StringSplitOptions.RemoveEmptyEntries);
386 if (pkgConfigLibdirs.Length == 0)
387 pkgConfigLibdirs = null;
391 prefix = PathUp (typeof (int).Assembly.Location, 4);
393 IEnumerable<string> paths = GetUnfilteredPkgConfigDirs (pkgConfigPaths, pkgConfigLibdirs, new string [] { prefix });
394 return NormaliseAndFilterPaths (paths, Environment.CurrentDirectory);
397 IEnumerable<string> GetUnfilteredPkgConfigDirs (IEnumerable<string> pkgConfigPaths, IEnumerable<string> pkgConfigLibdirs, IEnumerable<string> systemPrefixes)
399 if (pkgConfigPaths != null) {
400 foreach (string dir in pkgConfigPaths)
404 if (pkgConfigLibdirs != null) {
405 foreach (string dir in pkgConfigLibdirs)
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"),
415 foreach (string prefix in systemPrefixes)
416 foreach (string suffix in suffixes)
417 yield return Path.Combine (prefix, suffix);
421 IEnumerable<string> NormaliseAndFilterPaths (IEnumerable<string> paths, string workingDirectory)
423 Dictionary<string,string> filtered = new Dictionary<string,string> ();
424 foreach (string p in paths) {
426 if (!Path.IsPathRooted (path))
427 path = Path.Combine (workingDirectory, path);
428 path = Path.GetFullPath (path);
429 if (filtered.ContainsKey (path))
431 filtered.Add (path,path);
433 if (!Directory.Exists (path))
435 } catch (IOException ex) {
436 ctx.ReportError ("Error checking for directory '" + path + "'.", ex);
442 static string PathUp (string path, int up)
446 for (int i = path.Length -1; i >= 0; i--) {
447 if (path[i] == Path.DirectorySeparatorChar) {
450 return path.Substring (0, i);
459 Dictionary<string,string> variables = new Dictionary<string, string> ();
462 public string Description {
463 get { return description; }
464 set { description = value; }
468 public string FilePath {
469 get { return filePath; }
470 set { filePath = value; }
474 public bool HasErrors {
475 get { return hasErrors; }
476 set { hasErrors = value; }
482 set { libs = value; }
488 set { name = value; }
492 public string Version {
493 get { return version; }
494 set { version = value; }
498 public string Requires {
499 get { return requires; }
500 set { requires = value; }
503 public string GetVariable (string varName)
506 variables.TryGetValue (varName, out val);
510 public void Load (string pcfile)
513 variables.Add ("pcfiledir", Path.GetDirectoryName (pcfile));
514 using (StreamReader reader = new StreamReader (pcfile)) {
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)
522 string var = line.Substring (0, k).Trim ();
523 string value = line.Substring (k + 1).Trim ();
524 value = Evaluate (value);
528 variables [var] = value;
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;
543 string Evaluate (string value)
545 int i = value.IndexOf ("${");
549 StringBuilder sb = new StringBuilder ();
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
556 int n = value.IndexOf ('}', i);
557 if (n == -1 || n == i) {
558 // Closing bracket not found or empty name
562 string rname = value.Substring (i, n - i);
564 if (variables.TryGetValue (rname, out rval))
575 if (i < value.Length)
576 i = value.IndexOf ("${", i);
578 sb.Append (value.Substring (last, value.Length - last));
579 return sb.ToString ();
583 public class PackageInfo
585 Dictionary<string,string> customData;
586 DateTime lastWriteTime;
591 set { name = value; }
595 public string Version {
596 get { return version; }
597 set { version = value; }
601 public string Description {
602 get { return description; }
603 set { description = value; }
607 public string Requires {
608 get { return requires; }
609 set { requires = value; }
612 public string GetData (string name)
614 if (customData == null)
617 customData.TryGetValue (name, out res);
621 public void SetData (string name, string value)
623 if (customData == null)
624 customData = new Dictionary<string, string> ();
625 customData [name] = value;
628 public void RemoveData (string name)
630 if (customData != null)
631 customData.Remove (name);
634 internal Dictionary<string,string> CustomData {
635 get { return customData; }
638 internal DateTime LastWriteTime {
639 get { return lastWriteTime; }
640 set { lastWriteTime = value; }
643 internal bool HasCustomData {
644 get { return customData != null && customData.Count > 0; }
647 internal protected virtual bool IsValidPackage {
648 get { return HasCustomData; }