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
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, PackageInfo pkg);
41 // Should return false if the provided package does not have required
43 bool IsCustomDataComplete (string pcfile, PackageInfo pkg);
45 // Called to report errors
46 void ReportError (string message, Exception ex);
49 internal class PcFileCache
51 const string CACHE_VERSION = "2";
53 Dictionary<string, PackageInfo> infos = new Dictionary<string, PackageInfo> ();
54 Dictionary<string, PackageAssemblyInfo> assemblyLocations;
57 IPcFileCacheContext ctx;
59 public PcFileCache (IPcFileCacheContext ctx)
63 string path = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
64 path = Path.Combine (path, "xbuild");
65 if (!Directory.Exists (path))
66 Directory.CreateDirectory (path);
67 cacheFile = Path.Combine (path, "pkgconfig-cache-" + CACHE_VERSION + ".xml");
69 if (File.Exists (cacheFile))
72 } catch (Exception ex) {
73 ctx.ReportError ("pc file cache could not be loaded.", ex);
77 // Updates the pkg-config index, using the default search directories
80 string pkgConfigPath = Environment.GetEnvironmentVariable ("PKG_CONFIG_PATH");
81 string pkgConfigDir = Environment.GetEnvironmentVariable ("PKG_CONFIG_LIBDIR");
82 Update (GetPkgconfigPaths (null, pkgConfigPath, pkgConfigDir));
85 // Updates the pkg-config index, looking for .pc files in the provided directories
86 public void Update (IEnumerable<string> pkgConfigDirs)
88 foreach (string pcdir in pkgConfigDirs) {
89 foreach (string pcfile in Directory.GetFiles (pcdir, "*.pc"))
90 GetPackageInfo (pcfile);
95 // Returns the location of an assembly, given the full name
96 public PackageAssemblyInfo GetAssemblyLocation (string fullName)
99 if (assemblyLocations == null) {
100 // Populate on demand
101 assemblyLocations = new Dictionary<string, PackageAssemblyInfo> ();
102 foreach (PackageInfo info in infos.Values) {
103 if (info.IsValidPackage) {
104 foreach (PackageAssemblyInfo asm in info.Assemblies)
105 assemblyLocations [NormalizeAsmName (asm.FullName)] = asm;
110 // This collection is read-only once built, so there is no need for a lock
111 PackageAssemblyInfo pasm;
112 assemblyLocations.TryGetValue (NormalizeAsmName (fullName), out pasm);
116 public IEnumerable<PackageAssemblyInfo> ResolveAssemblyName (string name)
118 foreach (PackageInfo pinfo in infos.Values) {
119 if (pinfo.IsValidPackage) {
120 foreach (PackageAssemblyInfo asm in pinfo.Assemblies) {
121 if (asm.Name == name)
128 // Returns information about a .pc file
129 public PackageInfo GetPackageInfo (string file)
132 file = Path.GetFullPath (file);
134 DateTime wtime = File.GetLastWriteTime (file);
137 if (infos.TryGetValue (file, out info)) {
138 if (info.LastWriteTime == wtime)
144 info = ParsePackageInfo (file);
145 } catch (Exception ex) {
146 ctx.ReportError ("Error while parsing .pc file", ex);
147 info = new PackageInfo ();
151 if (!info.IsValidPackage)
152 info = new PackageInfo (); // Create a default empty instance
153 info.LastWriteTime = wtime;
161 FileStream OpenFile (FileAccess access)
164 FileMode mode = access == FileAccess.Read ? FileMode.Open : FileMode.Create;
165 Exception lastException = null;
167 while (retries > 0) {
169 return new FileStream (cacheFile, mode, access, FileShare.None);
170 } catch (Exception ex) {
171 // the file may be locked by another app. Wait a bit and try again
173 System.Threading.Thread.Sleep (200);
177 ctx.ReportError ("File could not be opened: " + cacheFile, lastException);
183 // The serializer can't be used because this file is reused in xbuild
184 using (FileStream fs = OpenFile (FileAccess.Read)) {
187 XmlTextReader xr = new XmlTextReader (fs);
189 xr.ReadStartElement ();
192 while (xr.NodeType == XmlNodeType.Element)
199 // The serializer can't be used because this file is reused in xbuild
204 using (FileStream fs = OpenFile (FileAccess.Write)) {
207 XmlTextWriter tw = new XmlTextWriter (new StreamWriter (fs));
208 tw.Formatting = Formatting.Indented;
210 tw.WriteStartElement ("PcFileCache");
211 foreach (KeyValuePair<string,PackageInfo> file in infos) {
212 WritePackage (tw, file.Key, file.Value);
214 tw.WriteEndElement (); // PcFileCache
222 void WritePackage (XmlTextWriter tw, string file, PackageInfo pinfo)
224 tw.WriteStartElement ("File");
225 tw.WriteAttributeString ("path", file);
226 tw.WriteAttributeString ("lastWriteTime", XmlConvert.ToString (pinfo.LastWriteTime, XmlDateTimeSerializationMode.Local));
228 if (pinfo.IsValidPackage) {
229 if (pinfo.Name != null)
230 tw.WriteAttributeString ("name", pinfo.Name);
231 if (pinfo.Version != null)
232 tw.WriteAttributeString ("version", pinfo.Version);
233 if (!string.IsNullOrEmpty (pinfo.Description))
234 tw.WriteAttributeString ("description", pinfo.Description);
235 if (!pinfo.IsGacPackage)
236 tw.WriteAttributeString ("gacPackage", "false");
237 if (pinfo.CustomData != null) {
238 foreach (KeyValuePair<string,string> cd in pinfo.CustomData)
239 tw.WriteAttributeString (cd.Key, cd.Value);
241 foreach (PackageAssemblyInfo asm in pinfo.Assemblies) {
242 tw.WriteStartElement ("Assembly");
243 tw.WriteAttributeString ("name", asm.Name);
244 tw.WriteAttributeString ("version", asm.Version);
245 tw.WriteAttributeString ("culture", asm.Culture);
246 tw.WriteAttributeString ("publicKeyToken", asm.PublicKeyToken);
247 tw.WriteAttributeString ("file", asm.File);
248 tw.WriteEndElement (); // Assembly
251 tw.WriteEndElement (); // File
254 void ReadPackage (XmlReader tr)
256 PackageInfo pinfo = new PackageInfo ();
259 tr.MoveToFirstAttribute ();
261 switch (tr.LocalName) {
262 case "path": file = tr.Value; break;
263 case "lastWriteTime": pinfo.LastWriteTime = XmlConvert.ToDateTime (tr.Value, XmlDateTimeSerializationMode.Local); break;
264 case "name": pinfo.Name = tr.Value; break;
265 case "version": pinfo.Version = tr.Value; break;
266 case "description": pinfo.Description = tr.Value; break;
267 case "gacPackage": pinfo.IsGacPackage = tr.Value != "false"; break;
268 default: pinfo.SetData (tr.LocalName, tr.Value); break;
270 } while (tr.MoveToNextAttribute ());
274 if (!tr.IsEmptyElement) {
275 tr.ReadStartElement ();
277 while (tr.NodeType == XmlNodeType.Element) {
278 PackageAssemblyInfo asm = new PackageAssemblyInfo ();
279 asm.Name = tr.GetAttribute ("name");
280 asm.Version = tr.GetAttribute ("version");
281 asm.Culture = tr.GetAttribute ("culture");
282 asm.PublicKeyToken = tr.GetAttribute ("publicKeyToken");
283 asm.File = tr.GetAttribute ("file");
284 if (pinfo.Assemblies == null)
285 pinfo.Assemblies = new List<PackageAssemblyInfo> ();
286 asm.ParentPackage = pinfo;
287 pinfo.Assemblies.Add (asm);
292 tr.ReadEndElement ();
297 if (!pinfo.IsValidPackage || ctx.IsCustomDataComplete (file, pinfo))
298 infos [file] = pinfo;
301 public object SyncRoot {
302 get { return infos; }
306 PackageInfo ParsePackageInfo (string pcfile)
308 PackageInfo pinfo = new PackageInfo ();
309 pinfo.Name = Path.GetFileNameWithoutExtension (pcfile);
310 List<string> fullassemblies = null;
311 bool gacPackageSet = false;
313 PcFile file = new PcFile ();
319 if (file.Libs != null && file.Libs.IndexOf (".dll") != -1) {
320 if (file.Libs.IndexOf ("-lib:") != -1 || file.Libs.IndexOf ("/lib:") != -1) {
321 fullassemblies = GetAssembliesWithLibInfo (file.Libs);
323 fullassemblies = GetAssembliesWithoutLibInfo (file.Libs);
327 string value = file.GetVariable ("Libraries");
328 if (!string.IsNullOrEmpty (value))
329 fullassemblies = GetAssembliesFromLibrariesVar (value);
331 pinfo.Version = file.Version;
332 pinfo.Description = file.Description;
334 value = file.GetVariable ("GacPackage");
336 value = value.ToLower ();
337 pinfo.IsGacPackage = value == "yes" || value == "true";
338 gacPackageSet = true;
341 if (fullassemblies == null)
344 string pcDir = Path.GetDirectoryName (pcfile);
345 string monoPrefix = Path.GetDirectoryName (Path.GetDirectoryName (pcDir));
346 monoPrefix = Path.GetFullPath (monoPrefix + Path.DirectorySeparatorChar + "lib" + Path.DirectorySeparatorChar + "mono" + Path.DirectorySeparatorChar);
348 List<PackageAssemblyInfo> list = new List<PackageAssemblyInfo> ();
349 foreach (string assembly in fullassemblies) {
351 if (Path.IsPathRooted (assembly))
352 asm = Path.GetFullPath (assembly);
354 if (Path.GetDirectoryName (assembly).Length == 0) {
357 asm = Path.GetFullPath (Path.Combine (pcDir, assembly));
360 if (File.Exists (asm)) {
361 PackageAssemblyInfo pi = new PackageAssemblyInfo ();
363 pi.ParentPackage = pinfo;
364 pi.UpdateFromFile (pi.File);
366 if (!gacPackageSet && !asm.StartsWith (monoPrefix) && Path.IsPathRooted (asm)) {
367 // Assembly installed outside $(prefix)/lib/mono. It is most likely not a gac package.
368 gacPackageSet = true;
369 pinfo.IsGacPackage = false;
373 pinfo.Assemblies = list;
374 ctx.StoreCustomData (file, pinfo);
379 private List<string> GetAssembliesWithLibInfo (string line)
381 List<string> references = new List<string> ();
382 List<string> libdirs = new List<string> ();
383 List<string> retval = new List<string> ();
384 foreach (string piece in line.Split (' ')) {
385 if (piece.ToLower ().Trim ().StartsWith ("/r:") || piece.ToLower ().Trim ().StartsWith ("-r:")) {
386 references.Add (piece.Substring (3).Trim ());
387 } else if (piece.ToLower ().Trim ().StartsWith ("/lib:") || piece.ToLower ().Trim ().StartsWith ("-lib:")) {
388 libdirs.Add (piece.Substring (5).Trim ());
392 foreach (string refrnc in references) {
393 foreach (string libdir in libdirs) {
394 if (File.Exists (libdir + Path.DirectorySeparatorChar + refrnc)) {
395 retval.Add (libdir + Path.DirectorySeparatorChar + refrnc);
403 List<string> GetAssembliesFromLibrariesVar (string line)
405 List<string> references = new List<string> ();
406 foreach (string reference in line.Split (' ')) {
407 if (!string.IsNullOrEmpty (reference))
408 references.Add (reference);
413 private List<string> GetAssembliesWithoutLibInfo (string line)
415 List<string> references = new List<string> ();
416 foreach (string reference in line.Split (' ')) {
417 if (reference.ToLower ().Trim ().StartsWith ("/r:") || reference.ToLower ().Trim ().StartsWith ("-r:")) {
418 string final_ref = reference.Substring (3).Trim ();
419 references.Add (final_ref);
425 public IEnumerable<string> GetPkgconfigPaths (string prefix, string pkgConfigPath, string pkgConfigLibdir)
427 char[] sep = new char[] { Path.PathSeparator };
429 string[] pkgConfigPaths = null;
430 if (!String.IsNullOrEmpty (pkgConfigPath)) {
431 pkgConfigPaths = pkgConfigPath.Split (sep, StringSplitOptions.RemoveEmptyEntries);
432 if (pkgConfigPaths.Length == 0)
433 pkgConfigPaths = null;
436 string[] pkgConfigLibdirs = null;
437 if (!String.IsNullOrEmpty (pkgConfigLibdir)) {
438 pkgConfigLibdirs = pkgConfigLibdir.Split (sep, StringSplitOptions.RemoveEmptyEntries);
439 if (pkgConfigLibdirs.Length == 0)
440 pkgConfigLibdirs = null;
444 prefix = PathUp (typeof (int).Assembly.Location, 4);
446 IEnumerable<string> paths = GetUnfilteredPkgConfigDirs (pkgConfigPaths, pkgConfigLibdirs, new string [] { prefix });
447 return NormaliseAndFilterPaths (paths, Environment.CurrentDirectory);
450 IEnumerable<string> GetUnfilteredPkgConfigDirs (IEnumerable<string> pkgConfigPaths, IEnumerable<string> pkgConfigLibdirs, IEnumerable<string> systemPrefixes)
452 if (pkgConfigPaths != null) {
453 foreach (string dir in pkgConfigPaths)
457 if (pkgConfigLibdirs != null) {
458 foreach (string dir in pkgConfigLibdirs)
460 } else if (systemPrefixes != null) {
461 string[] suffixes = new string [] {
462 Path.Combine ("lib", "pkgconfig"),
463 Path.Combine ("lib64", "pkgconfig"),
464 Path.Combine ("libdata", "pkgconfig"),
465 Path.Combine ("share", "pkgconfig"),
467 foreach (string prefix in systemPrefixes)
468 foreach (string suffix in suffixes)
469 yield return Path.Combine (prefix, suffix);
473 IEnumerable<string> NormaliseAndFilterPaths (IEnumerable<string> paths, string workingDirectory)
475 HashSet<string> filtered = new HashSet<string> ();
476 foreach (string p in paths) {
478 if (!Path.IsPathRooted (path))
479 path = Path.Combine (workingDirectory, path);
480 path = Path.GetFullPath (path);
481 if (!filtered.Add (path))
484 if (!Directory.Exists (path))
486 } catch (IOException ex) {
487 ctx.ReportError ("Error checking for directory '" + path + "'.", ex);
493 static string PathUp (string path, int up)
497 for (int i = path.Length -1; i >= 0; i--) {
498 if (path[i] == Path.DirectorySeparatorChar) {
501 return path.Substring (0, i);
507 public static string NormalizeAsmName (string name)
509 int i = name.ToLower ().IndexOf (", publickeytoken=null");
511 name = name.Substring (0, i).Trim ();
512 i = name.ToLower ().IndexOf (", processorarchitecture=");
514 name = name.Substring (0, i).Trim ();
519 internal class PcFile
521 Dictionary<string,string> variables = new Dictionary<string, string> ();
523 public string FilePath { get; set; }
524 public string Name { get; set; }
525 public string Description { get; set; }
526 public string Version { get; set; }
527 public string Libs { get; set; }
528 public bool HasErrors { get; set; }
530 public string GetVariable (string varName)
533 variables.TryGetValue (varName, out val);
537 public void Load (string pcfile)
540 variables.Add ("pcfiledir", Path.GetDirectoryName (pcfile));
541 using (StreamReader reader = new StreamReader (pcfile)) {
543 while ((line = reader.ReadLine ()) != null) {
544 int i = line.IndexOf (':');
545 int j = line.IndexOf ('=');
546 int k = System.Math.Min (i != -1 ? i : int.MaxValue, j != -1 ? j : int.MaxValue);
547 if (k == int.MaxValue)
549 string var = line.Substring (0, k).Trim ();
550 string value = line.Substring (k + 1).Trim ();
551 value = Evaluate (value);
555 variables [var] = value;
559 case "Name": Name = value; break;
560 case "Description": Description = value; break;
561 case "Version": Version = value; break;
562 case "Libs": Libs = value; break;
569 string Evaluate (string value)
571 int i = value.IndexOf ("${");
575 StringBuilder sb = new StringBuilder ();
577 while (i != -1 && i < value.Length) {
578 sb.Append (value.Substring (last, i - last));
579 if (i == 0 || value [i - 1] != '$') {
580 // Evaluate if var is not escaped
582 int n = value.IndexOf ('}', i);
583 if (n == -1 || n == i) {
584 // Closing bracket not found or empty name
588 string rname = value.Substring (i, n - i);
590 if (variables.TryGetValue (rname, out rval))
601 if (i < value.Length - 1)
602 i = value.IndexOf ("${", i);
604 sb.Append (value.Substring (last, value.Length - last));
605 return sb.ToString ();
609 internal class PackageInfo
611 Dictionary<string,string> customData;
613 public PackageInfo ()
618 public string Name { get; set; }
620 public bool IsGacPackage { get; set; }
622 public string Version { get; set; }
624 public string Description { get; set; }
626 internal List<PackageAssemblyInfo> Assemblies { get; set; }
628 public string GetData (string name)
630 if (customData == null)
633 customData.TryGetValue (name, out res);
637 public void SetData (string name, string value)
639 if (customData == null)
640 customData = new Dictionary<string, string> ();
641 customData [name] = value;
644 internal Dictionary<string,string> CustomData {
645 get { return customData; }
648 internal DateTime LastWriteTime { get; set; }
650 internal bool IsValidPackage {
651 get { return Assemblies != null && Assemblies.Count > 0; }
654 internal bool HasCustomData {
655 get { return customData != null && customData.Count > 0; }
659 class PackageAssemblyInfo
661 public string File { get; set; }
665 public string Version;
667 public string Culture;
669 public string PublicKeyToken;
671 public string FullName {
673 string fn = Name + ", Version=" + Version;
674 if (!string.IsNullOrEmpty (Culture))
675 fn += ", Culture=" + Culture;
676 if (!string.IsNullOrEmpty (PublicKeyToken))
677 fn += ", PublicKeyToken=" + PublicKeyToken;
682 public PackageInfo ParentPackage { get; set; }
684 public void UpdateFromFile (string file)
686 Update (System.Reflection.AssemblyName.GetAssemblyName (file));
689 public void Update (System.Reflection.AssemblyName aname)
692 Version = aname.Version.ToString ();
693 if (aname.CultureInfo != null) {
694 if (aname.CultureInfo.LCID == System.Globalization.CultureInfo.InvariantCulture.LCID)
697 Culture = aname.CultureInfo.Name;
699 string fn = aname.ToString ();
700 string key = "publickeytoken=";
701 int i = fn.ToLower().IndexOf (key) + key.Length;
702 int j = fn.IndexOf (',', i);
703 if (j == -1) j = fn.Length;
704 PublicKeyToken = fn.Substring (i, j - i);