npy_pkg_config.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import sys
  2. import re
  3. import os
  4. from configparser import RawConfigParser
  5. __all__ = ['FormatError', 'PkgNotFound', 'LibraryInfo', 'VariableSet',
  6. 'read_config', 'parse_flags']
  7. _VAR = re.compile(r'\$\{([a-zA-Z0-9_-]+)\}')
  8. class FormatError(OSError):
  9. """
  10. Exception thrown when there is a problem parsing a configuration file.
  11. """
  12. def __init__(self, msg):
  13. self.msg = msg
  14. def __str__(self):
  15. return self.msg
  16. class PkgNotFound(OSError):
  17. """Exception raised when a package can not be located."""
  18. def __init__(self, msg):
  19. self.msg = msg
  20. def __str__(self):
  21. return self.msg
  22. def parse_flags(line):
  23. """
  24. Parse a line from a config file containing compile flags.
  25. Parameters
  26. ----------
  27. line : str
  28. A single line containing one or more compile flags.
  29. Returns
  30. -------
  31. d : dict
  32. Dictionary of parsed flags, split into relevant categories.
  33. These categories are the keys of `d`:
  34. * 'include_dirs'
  35. * 'library_dirs'
  36. * 'libraries'
  37. * 'macros'
  38. * 'ignored'
  39. """
  40. d = {'include_dirs': [], 'library_dirs': [], 'libraries': [],
  41. 'macros': [], 'ignored': []}
  42. flags = (' ' + line).split(' -')
  43. for flag in flags:
  44. flag = '-' + flag
  45. if len(flag) > 0:
  46. if flag.startswith('-I'):
  47. d['include_dirs'].append(flag[2:].strip())
  48. elif flag.startswith('-L'):
  49. d['library_dirs'].append(flag[2:].strip())
  50. elif flag.startswith('-l'):
  51. d['libraries'].append(flag[2:].strip())
  52. elif flag.startswith('-D'):
  53. d['macros'].append(flag[2:].strip())
  54. else:
  55. d['ignored'].append(flag)
  56. return d
  57. def _escape_backslash(val):
  58. return val.replace('\\', '\\\\')
  59. class LibraryInfo:
  60. """
  61. Object containing build information about a library.
  62. Parameters
  63. ----------
  64. name : str
  65. The library name.
  66. description : str
  67. Description of the library.
  68. version : str
  69. Version string.
  70. sections : dict
  71. The sections of the configuration file for the library. The keys are
  72. the section headers, the values the text under each header.
  73. vars : class instance
  74. A `VariableSet` instance, which contains ``(name, value)`` pairs for
  75. variables defined in the configuration file for the library.
  76. requires : sequence, optional
  77. The required libraries for the library to be installed.
  78. Notes
  79. -----
  80. All input parameters (except "sections" which is a method) are available as
  81. attributes of the same name.
  82. """
  83. def __init__(self, name, description, version, sections, vars, requires=None):
  84. self.name = name
  85. self.description = description
  86. if requires:
  87. self.requires = requires
  88. else:
  89. self.requires = []
  90. self.version = version
  91. self._sections = sections
  92. self.vars = vars
  93. def sections(self):
  94. """
  95. Return the section headers of the config file.
  96. Parameters
  97. ----------
  98. None
  99. Returns
  100. -------
  101. keys : list of str
  102. The list of section headers.
  103. """
  104. return list(self._sections.keys())
  105. def cflags(self, section="default"):
  106. val = self.vars.interpolate(self._sections[section]['cflags'])
  107. return _escape_backslash(val)
  108. def libs(self, section="default"):
  109. val = self.vars.interpolate(self._sections[section]['libs'])
  110. return _escape_backslash(val)
  111. def __str__(self):
  112. m = ['Name: %s' % self.name, 'Description: %s' % self.description]
  113. if self.requires:
  114. m.append('Requires:')
  115. else:
  116. m.append('Requires: %s' % ",".join(self.requires))
  117. m.append('Version: %s' % self.version)
  118. return "\n".join(m)
  119. class VariableSet:
  120. """
  121. Container object for the variables defined in a config file.
  122. `VariableSet` can be used as a plain dictionary, with the variable names
  123. as keys.
  124. Parameters
  125. ----------
  126. d : dict
  127. Dict of items in the "variables" section of the configuration file.
  128. """
  129. def __init__(self, d):
  130. self._raw_data = dict([(k, v) for k, v in d.items()])
  131. self._re = {}
  132. self._re_sub = {}
  133. self._init_parse()
  134. def _init_parse(self):
  135. for k, v in self._raw_data.items():
  136. self._init_parse_var(k, v)
  137. def _init_parse_var(self, name, value):
  138. self._re[name] = re.compile(r'\$\{%s\}' % name)
  139. self._re_sub[name] = value
  140. def interpolate(self, value):
  141. # Brute force: we keep interpolating until there is no '${var}' anymore
  142. # or until interpolated string is equal to input string
  143. def _interpolate(value):
  144. for k in self._re.keys():
  145. value = self._re[k].sub(self._re_sub[k], value)
  146. return value
  147. while _VAR.search(value):
  148. nvalue = _interpolate(value)
  149. if nvalue == value:
  150. break
  151. value = nvalue
  152. return value
  153. def variables(self):
  154. """
  155. Return the list of variable names.
  156. Parameters
  157. ----------
  158. None
  159. Returns
  160. -------
  161. names : list of str
  162. The names of all variables in the `VariableSet` instance.
  163. """
  164. return list(self._raw_data.keys())
  165. # Emulate a dict to set/get variables values
  166. def __getitem__(self, name):
  167. return self._raw_data[name]
  168. def __setitem__(self, name, value):
  169. self._raw_data[name] = value
  170. self._init_parse_var(name, value)
  171. def parse_meta(config):
  172. if not config.has_section('meta'):
  173. raise FormatError("No meta section found !")
  174. d = dict(config.items('meta'))
  175. for k in ['name', 'description', 'version']:
  176. if not k in d:
  177. raise FormatError("Option %s (section [meta]) is mandatory, "
  178. "but not found" % k)
  179. if not 'requires' in d:
  180. d['requires'] = []
  181. return d
  182. def parse_variables(config):
  183. if not config.has_section('variables'):
  184. raise FormatError("No variables section found !")
  185. d = {}
  186. for name, value in config.items("variables"):
  187. d[name] = value
  188. return VariableSet(d)
  189. def parse_sections(config):
  190. return meta_d, r
  191. def pkg_to_filename(pkg_name):
  192. return "%s.ini" % pkg_name
  193. def parse_config(filename, dirs=None):
  194. if dirs:
  195. filenames = [os.path.join(d, filename) for d in dirs]
  196. else:
  197. filenames = [filename]
  198. config = RawConfigParser()
  199. n = config.read(filenames)
  200. if not len(n) >= 1:
  201. raise PkgNotFound("Could not find file(s) %s" % str(filenames))
  202. # Parse meta and variables sections
  203. meta = parse_meta(config)
  204. vars = {}
  205. if config.has_section('variables'):
  206. for name, value in config.items("variables"):
  207. vars[name] = _escape_backslash(value)
  208. # Parse "normal" sections
  209. secs = [s for s in config.sections() if not s in ['meta', 'variables']]
  210. sections = {}
  211. requires = {}
  212. for s in secs:
  213. d = {}
  214. if config.has_option(s, "requires"):
  215. requires[s] = config.get(s, 'requires')
  216. for name, value in config.items(s):
  217. d[name] = value
  218. sections[s] = d
  219. return meta, vars, sections, requires
  220. def _read_config_imp(filenames, dirs=None):
  221. def _read_config(f):
  222. meta, vars, sections, reqs = parse_config(f, dirs)
  223. # recursively add sections and variables of required libraries
  224. for rname, rvalue in reqs.items():
  225. nmeta, nvars, nsections, nreqs = _read_config(pkg_to_filename(rvalue))
  226. # Update var dict for variables not in 'top' config file
  227. for k, v in nvars.items():
  228. if not k in vars:
  229. vars[k] = v
  230. # Update sec dict
  231. for oname, ovalue in nsections[rname].items():
  232. if ovalue:
  233. sections[rname][oname] += ' %s' % ovalue
  234. return meta, vars, sections, reqs
  235. meta, vars, sections, reqs = _read_config(filenames)
  236. # FIXME: document this. If pkgname is defined in the variables section, and
  237. # there is no pkgdir variable defined, pkgdir is automatically defined to
  238. # the path of pkgname. This requires the package to be imported to work
  239. if not 'pkgdir' in vars and "pkgname" in vars:
  240. pkgname = vars["pkgname"]
  241. if not pkgname in sys.modules:
  242. raise ValueError("You should import %s to get information on %s" %
  243. (pkgname, meta["name"]))
  244. mod = sys.modules[pkgname]
  245. vars["pkgdir"] = _escape_backslash(os.path.dirname(mod.__file__))
  246. return LibraryInfo(name=meta["name"], description=meta["description"],
  247. version=meta["version"], sections=sections, vars=VariableSet(vars))
  248. # Trivial cache to cache LibraryInfo instances creation. To be really
  249. # efficient, the cache should be handled in read_config, since a same file can
  250. # be parsed many time outside LibraryInfo creation, but I doubt this will be a
  251. # problem in practice
  252. _CACHE = {}
  253. def read_config(pkgname, dirs=None):
  254. """
  255. Return library info for a package from its configuration file.
  256. Parameters
  257. ----------
  258. pkgname : str
  259. Name of the package (should match the name of the .ini file, without
  260. the extension, e.g. foo for the file foo.ini).
  261. dirs : sequence, optional
  262. If given, should be a sequence of directories - usually including
  263. the NumPy base directory - where to look for npy-pkg-config files.
  264. Returns
  265. -------
  266. pkginfo : class instance
  267. The `LibraryInfo` instance containing the build information.
  268. Raises
  269. ------
  270. PkgNotFound
  271. If the package is not found.
  272. See Also
  273. --------
  274. misc_util.get_info, misc_util.get_pkg_info
  275. Examples
  276. --------
  277. >>> npymath_info = np.distutils.npy_pkg_config.read_config('npymath')
  278. >>> type(npymath_info)
  279. <class 'numpy.distutils.npy_pkg_config.LibraryInfo'>
  280. >>> print(npymath_info)
  281. Name: npymath
  282. Description: Portable, core math library implementing C99 standard
  283. Requires:
  284. Version: 0.1 #random
  285. """
  286. try:
  287. return _CACHE[pkgname]
  288. except KeyError:
  289. v = _read_config_imp(pkg_to_filename(pkgname), dirs)
  290. _CACHE[pkgname] = v
  291. return v
  292. # TODO:
  293. # - implements version comparison (modversion + atleast)
  294. # pkg-config simple emulator - useful for debugging, and maybe later to query
  295. # the system
  296. if __name__ == '__main__':
  297. from optparse import OptionParser
  298. import glob
  299. parser = OptionParser()
  300. parser.add_option("--cflags", dest="cflags", action="store_true",
  301. help="output all preprocessor and compiler flags")
  302. parser.add_option("--libs", dest="libs", action="store_true",
  303. help="output all linker flags")
  304. parser.add_option("--use-section", dest="section",
  305. help="use this section instead of default for options")
  306. parser.add_option("--version", dest="version", action="store_true",
  307. help="output version")
  308. parser.add_option("--atleast-version", dest="min_version",
  309. help="Minimal version")
  310. parser.add_option("--list-all", dest="list_all", action="store_true",
  311. help="Minimal version")
  312. parser.add_option("--define-variable", dest="define_variable",
  313. help="Replace variable with the given value")
  314. (options, args) = parser.parse_args(sys.argv)
  315. if len(args) < 2:
  316. raise ValueError("Expect package name on the command line:")
  317. if options.list_all:
  318. files = glob.glob("*.ini")
  319. for f in files:
  320. info = read_config(f)
  321. print("%s\t%s - %s" % (info.name, info.name, info.description))
  322. pkg_name = args[1]
  323. d = os.environ.get('NPY_PKG_CONFIG_PATH')
  324. if d:
  325. info = read_config(pkg_name, ['numpy/core/lib/npy-pkg-config', '.', d])
  326. else:
  327. info = read_config(pkg_name, ['numpy/core/lib/npy-pkg-config', '.'])
  328. if options.section:
  329. section = options.section
  330. else:
  331. section = "default"
  332. if options.define_variable:
  333. m = re.search(r'([\S]+)=([\S]+)', options.define_variable)
  334. if not m:
  335. raise ValueError("--define-variable option should be of "
  336. "the form --define-variable=foo=bar")
  337. else:
  338. name = m.group(1)
  339. value = m.group(2)
  340. info.vars[name] = value
  341. if options.cflags:
  342. print(info.cflags(section))
  343. if options.libs:
  344. print(info.libs(section))
  345. if options.version:
  346. print(info.version)
  347. if options.min_version:
  348. print(info.version >= options.min_version)