pkgconfig.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. # Copyright 2015 The Meson development team
  2. # Licensed under the Apache License, Version 2.0 (the "License");
  3. # you may not use this file except in compliance with the License.
  4. # You may obtain a copy of the License at
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. # Unless required by applicable law or agreed to in writing, software
  7. # distributed under the License is distributed on an "AS IS" BASIS,
  8. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. # See the License for the specific language governing permissions and
  10. # limitations under the License.
  11. import os
  12. from pathlib import PurePath
  13. from .. import build
  14. from .. import dependencies
  15. from .. import mesonlib
  16. from .. import mlog
  17. from . import ModuleReturnValue
  18. from . import ExtensionModule
  19. from ..interpreterbase import permittedKwargs, FeatureNew, FeatureNewKwargs
  20. class DependenciesHelper:
  21. def __init__(self, name):
  22. self.name = name
  23. self.pub_libs = []
  24. self.pub_reqs = []
  25. self.priv_libs = []
  26. self.priv_reqs = []
  27. self.cflags = []
  28. self.version_reqs = {}
  29. def add_pub_libs(self, libs):
  30. libs, reqs, cflags = self._process_libs(libs, True)
  31. self.pub_libs += libs
  32. self.pub_reqs += reqs
  33. self.cflags += cflags
  34. def add_priv_libs(self, libs):
  35. libs, reqs, _ = self._process_libs(libs, False)
  36. self.priv_libs += libs
  37. self.priv_reqs += reqs
  38. def add_pub_reqs(self, reqs):
  39. self.pub_reqs += self._process_reqs(reqs)
  40. def add_priv_reqs(self, reqs):
  41. self.priv_reqs += self._process_reqs(reqs)
  42. def _process_reqs(self, reqs):
  43. '''Returns string names of requirements'''
  44. processed_reqs = []
  45. for obj in mesonlib.listify(reqs, unholder=True):
  46. if hasattr(obj, 'generated_pc'):
  47. processed_reqs.append(obj.generated_pc)
  48. elif hasattr(obj, 'pcdep'):
  49. pcdeps = mesonlib.listify(obj.pcdep)
  50. for d in pcdeps:
  51. processed_reqs.append(d.name)
  52. self.add_version_reqs(d.name, obj.version_reqs)
  53. elif isinstance(obj, dependencies.PkgConfigDependency):
  54. if obj.found():
  55. processed_reqs.append(obj.name)
  56. self.add_version_reqs(obj.name, obj.version_reqs)
  57. elif isinstance(obj, str):
  58. name, version_req = self.split_version_req(obj)
  59. processed_reqs.append(name)
  60. self.add_version_reqs(name, version_req)
  61. elif isinstance(obj, dependencies.Dependency) and not obj.found():
  62. pass
  63. else:
  64. raise mesonlib.MesonException('requires argument not a string, '
  65. 'library with pkgconfig-generated file '
  66. 'or pkgconfig-dependency object, '
  67. 'got {!r}'.format(obj))
  68. return processed_reqs
  69. def add_cflags(self, cflags):
  70. self.cflags += mesonlib.stringlistify(cflags)
  71. def _process_libs(self, libs, public):
  72. libs = mesonlib.listify(libs, unholder=True)
  73. processed_libs = []
  74. processed_reqs = []
  75. processed_cflags = []
  76. for obj in libs:
  77. shared_library_only = getattr(obj, 'shared_library_only', False)
  78. if hasattr(obj, 'pcdep'):
  79. pcdeps = mesonlib.listify(obj.pcdep)
  80. for d in pcdeps:
  81. processed_reqs.append(d.name)
  82. self.add_version_reqs(d.name, obj.version_reqs)
  83. elif hasattr(obj, 'generated_pc'):
  84. processed_reqs.append(obj.generated_pc)
  85. elif isinstance(obj, dependencies.PkgConfigDependency):
  86. if obj.found():
  87. processed_reqs.append(obj.name)
  88. self.add_version_reqs(obj.name, obj.version_reqs)
  89. elif isinstance(obj, dependencies.ThreadDependency):
  90. processed_libs += obj.get_compiler().thread_link_flags(obj.env)
  91. processed_cflags += obj.get_compiler().thread_flags(obj.env)
  92. elif isinstance(obj, dependencies.Dependency):
  93. if obj.found():
  94. processed_libs += obj.get_link_args()
  95. processed_cflags += obj.get_compile_args()
  96. elif isinstance(obj, build.SharedLibrary) and shared_library_only:
  97. # Do not pull dependencies for shared libraries because they are
  98. # only required for static linking. Adding private requires has
  99. # the side effect of exposing their cflags, which is the
  100. # intended behaviour of pkg-config but force Debian to add more
  101. # than needed build deps.
  102. # See https://bugs.freedesktop.org/show_bug.cgi?id=105572
  103. processed_libs.append(obj)
  104. if public:
  105. if not hasattr(obj, 'generated_pc'):
  106. obj.generated_pc = self.name
  107. elif isinstance(obj, (build.SharedLibrary, build.StaticLibrary)):
  108. processed_libs.append(obj)
  109. if public:
  110. if not hasattr(obj, 'generated_pc'):
  111. obj.generated_pc = self.name
  112. if isinstance(obj, build.StaticLibrary) and public:
  113. self.add_pub_libs(obj.get_dependencies(internal=False))
  114. self.add_pub_libs(obj.get_external_deps())
  115. else:
  116. self.add_priv_libs(obj.get_dependencies(internal=False))
  117. self.add_priv_libs(obj.get_external_deps())
  118. elif isinstance(obj, str):
  119. processed_libs.append(obj)
  120. else:
  121. raise mesonlib.MesonException('library argument not a string, library or dependency object.')
  122. return processed_libs, processed_reqs, processed_cflags
  123. def add_version_reqs(self, name, version_reqs):
  124. if version_reqs:
  125. if name not in self.version_reqs:
  126. self.version_reqs[name] = set()
  127. # Note that pkg-config is picky about whitespace.
  128. # 'foo > 1.2' is ok but 'foo>1.2' is not.
  129. # foo, bar' is ok, but 'foo,bar' is not.
  130. new_vreqs = [s for s in mesonlib.stringlistify(version_reqs)]
  131. self.version_reqs[name].update(new_vreqs)
  132. def split_version_req(self, s):
  133. for op in ['>=', '<=', '!=', '==', '=', '>', '<']:
  134. pos = s.find(op)
  135. if pos > 0:
  136. return s[0:pos].strip(), s[pos:].strip()
  137. return s, None
  138. def format_vreq(self, vreq):
  139. # vreq are '>=1.0' and pkgconfig wants '>= 1.0'
  140. for op in ['>=', '<=', '!=', '==', '=', '>', '<']:
  141. if vreq.startswith(op):
  142. return op + ' ' + vreq[len(op):]
  143. return vreq
  144. def format_reqs(self, reqs):
  145. result = []
  146. for name in reqs:
  147. vreqs = self.version_reqs.get(name, None)
  148. if vreqs:
  149. result += [name + ' ' + self.format_vreq(vreq) for vreq in vreqs]
  150. else:
  151. result += [name]
  152. return ', '.join(result)
  153. def remove_dups(self):
  154. def _fn(xs, libs=False):
  155. # Remove duplicates whilst preserving original order
  156. result = []
  157. for x in xs:
  158. # Don't de-dup unknown strings to avoid messing up arguments like:
  159. # ['-framework', 'CoreAudio', '-framework', 'CoreMedia']
  160. if x not in result or (libs and (isinstance(x, str) and not x.endswith(('-l', '-L')))):
  161. result.append(x)
  162. return result
  163. self.pub_libs = _fn(self.pub_libs, True)
  164. self.pub_reqs = _fn(self.pub_reqs)
  165. self.priv_libs = _fn(self.priv_libs, True)
  166. self.priv_reqs = _fn(self.priv_reqs)
  167. self.cflags = _fn(self.cflags)
  168. # Remove from private libs/reqs if they are in public already
  169. self.priv_libs = [i for i in self.priv_libs if i not in self.pub_libs]
  170. self.priv_reqs = [i for i in self.priv_reqs if i not in self.pub_reqs]
  171. class PkgConfigModule(ExtensionModule):
  172. def _get_lname(self, l, msg, pcfile):
  173. # Nothing special
  174. if not l.name_prefix_set:
  175. return l.name
  176. # Sometimes people want the library to start with 'lib' everywhere,
  177. # which is achieved by setting name_prefix to '' and the target name to
  178. # 'libfoo'. In that case, try to get the pkg-config '-lfoo' arg correct.
  179. if l.prefix == '' and l.name.startswith('lib'):
  180. return l.name[3:]
  181. # If the library is imported via an import library which is always
  182. # named after the target name, '-lfoo' is correct.
  183. if l.import_filename:
  184. return l.name
  185. # In other cases, we can't guarantee that the compiler will be able to
  186. # find the library via '-lfoo', so tell the user that.
  187. mlog.warning(msg.format(l.name, 'name_prefix', l.name, pcfile))
  188. return l.name
  189. def _escape(self, value):
  190. '''
  191. We cannot use shlex.quote because it quotes with ' and " which does not
  192. work with pkg-config and pkgconf at all.
  193. '''
  194. # We should always write out paths with / because pkg-config requires
  195. # spaces to be quoted with \ and that messes up on Windows:
  196. # https://bugs.freedesktop.org/show_bug.cgi?id=103203
  197. if isinstance(value, PurePath):
  198. value = value.as_posix()
  199. return value.replace(' ', '\ ')
  200. def _make_relative(self, prefix, subdir):
  201. if isinstance(prefix, PurePath):
  202. prefix = prefix.as_posix()
  203. if isinstance(subdir, PurePath):
  204. subdir = subdir.as_posix()
  205. if subdir.startswith(prefix):
  206. subdir = subdir.replace(prefix, '')
  207. return subdir
  208. def generate_pkgconfig_file(self, state, deps, subdirs, name, description,
  209. url, version, pcfile, conflicts, variables):
  210. deps.remove_dups()
  211. coredata = state.environment.get_coredata()
  212. outdir = state.environment.scratch_dir
  213. fname = os.path.join(outdir, pcfile)
  214. prefix = PurePath(coredata.get_builtin_option('prefix'))
  215. # These always return paths relative to prefix
  216. libdir = PurePath(coredata.get_builtin_option('libdir'))
  217. incdir = PurePath(coredata.get_builtin_option('includedir'))
  218. with open(fname, 'w') as ofile:
  219. ofile.write('prefix={}\n'.format(self._escape(prefix)))
  220. ofile.write('libdir={}\n'.format(self._escape('${prefix}' / libdir)))
  221. ofile.write('includedir={}\n'.format(self._escape('${prefix}' / incdir)))
  222. if variables:
  223. ofile.write('\n')
  224. for k, v in variables:
  225. ofile.write('{}={}\n'.format(k, self._escape(v)))
  226. ofile.write('\n')
  227. ofile.write('Name: %s\n' % name)
  228. if len(description) > 0:
  229. ofile.write('Description: %s\n' % description)
  230. if len(url) > 0:
  231. ofile.write('URL: %s\n' % url)
  232. ofile.write('Version: %s\n' % version)
  233. reqs_str = deps.format_reqs(deps.pub_reqs)
  234. if len(reqs_str) > 0:
  235. ofile.write('Requires: {}\n'.format(reqs_str))
  236. reqs_str = deps.format_reqs(deps.priv_reqs)
  237. if len(reqs_str) > 0:
  238. ofile.write('Requires.private: {}\n'.format(reqs_str))
  239. if len(conflicts) > 0:
  240. ofile.write('Conflicts: {}\n'.format(' '.join(conflicts)))
  241. def generate_libs_flags(libs):
  242. msg = 'Library target {0!r} has {1!r} set. Compilers ' \
  243. 'may not find it from its \'-l{2}\' linker flag in the ' \
  244. '{3!r} pkg-config file.'
  245. Lflags = []
  246. for l in libs:
  247. if isinstance(l, str):
  248. yield l
  249. else:
  250. install_dir = l.get_custom_install_dir()[0]
  251. if install_dir is False:
  252. continue
  253. if isinstance(install_dir, str):
  254. Lflag = '-L${prefix}/%s ' % self._escape(self._make_relative(prefix, install_dir))
  255. else: # install_dir is True
  256. Lflag = '-L${libdir}'
  257. if Lflag not in Lflags:
  258. Lflags.append(Lflag)
  259. yield Lflag
  260. lname = self._get_lname(l, msg, pcfile)
  261. # If using a custom suffix, the compiler may not be able to
  262. # find the library
  263. if l.name_suffix_set:
  264. mlog.warning(msg.format(l.name, 'name_suffix', lname, pcfile))
  265. yield '-l%s' % lname
  266. if len(deps.pub_libs) > 0:
  267. ofile.write('Libs: {}\n'.format(' '.join(generate_libs_flags(deps.pub_libs))))
  268. if len(deps.priv_libs) > 0:
  269. ofile.write('Libs.private: {}\n'.format(' '.join(generate_libs_flags(deps.priv_libs))))
  270. ofile.write('Cflags:')
  271. for h in subdirs:
  272. ofile.write(' ')
  273. if h == '.':
  274. ofile.write('-I${includedir}')
  275. else:
  276. ofile.write(self._escape(PurePath('-I${includedir}') / h))
  277. for f in deps.cflags:
  278. ofile.write(' ')
  279. ofile.write(self._escape(f))
  280. ofile.write('\n')
  281. @FeatureNewKwargs('pkgconfig.generate', '0.42.0', ['extra_cflags'])
  282. @FeatureNewKwargs('pkgconfig.generate', '0.41.0', ['variables'])
  283. @permittedKwargs({'libraries', 'version', 'name', 'description', 'filebase',
  284. 'subdirs', 'requires', 'requires_private', 'libraries_private',
  285. 'install_dir', 'extra_cflags', 'variables', 'url', 'd_module_versions'})
  286. def generate(self, state, args, kwargs):
  287. if 'variables' in kwargs:
  288. FeatureNew('custom pkgconfig variables', '0.41.0').use()
  289. default_version = state.project_version['version']
  290. default_install_dir = None
  291. default_description = None
  292. default_name = None
  293. mainlib = None
  294. if len(args) == 1:
  295. FeatureNew('pkgconfig.generate optional positional argument', '0.46.0').use()
  296. mainlib = getattr(args[0], 'held_object', args[0])
  297. if not isinstance(mainlib, (build.StaticLibrary, build.SharedLibrary)):
  298. raise mesonlib.MesonException('Pkgconfig_gen first positional argument must be a library object')
  299. default_name = mainlib.name
  300. default_description = state.project_name + ': ' + mainlib.name
  301. install_dir = mainlib.get_custom_install_dir()[0]
  302. if isinstance(install_dir, str):
  303. default_install_dir = os.path.join(install_dir, 'pkgconfig')
  304. elif len(args) > 1:
  305. raise mesonlib.MesonException('Too many positional arguments passed to Pkgconfig_gen.')
  306. subdirs = mesonlib.stringlistify(kwargs.get('subdirs', ['.']))
  307. version = kwargs.get('version', default_version)
  308. if not isinstance(version, str):
  309. raise mesonlib.MesonException('Version must be specified.')
  310. name = kwargs.get('name', default_name)
  311. if not isinstance(name, str):
  312. raise mesonlib.MesonException('Name not specified.')
  313. filebase = kwargs.get('filebase', name)
  314. if not isinstance(filebase, str):
  315. raise mesonlib.MesonException('Filebase must be a string.')
  316. description = kwargs.get('description', default_description)
  317. if not isinstance(description, str):
  318. raise mesonlib.MesonException('Description is not a string.')
  319. url = kwargs.get('url', '')
  320. if not isinstance(url, str):
  321. raise mesonlib.MesonException('URL is not a string.')
  322. conflicts = mesonlib.stringlistify(kwargs.get('conflicts', []))
  323. deps = DependenciesHelper(filebase)
  324. if mainlib:
  325. deps.add_pub_libs(mainlib)
  326. deps.add_pub_libs(kwargs.get('libraries', []))
  327. deps.add_priv_libs(kwargs.get('libraries_private', []))
  328. deps.add_pub_reqs(kwargs.get('requires', []))
  329. deps.add_priv_reqs(kwargs.get('requires_private', []))
  330. deps.add_cflags(kwargs.get('extra_cflags', []))
  331. dversions = kwargs.get('d_module_versions', None)
  332. if dversions:
  333. compiler = state.environment.coredata.compilers.get('d')
  334. if compiler:
  335. deps.add_cflags(compiler.get_feature_args({'versions': dversions}, None))
  336. def parse_variable_list(stringlist):
  337. reserved = ['prefix', 'libdir', 'includedir']
  338. variables = []
  339. for var in stringlist:
  340. # foo=bar=baz is ('foo', 'bar=baz')
  341. l = var.split('=', 1)
  342. if len(l) < 2:
  343. raise mesonlib.MesonException('Invalid variable "{}". Variables must be in \'name=value\' format'.format(var))
  344. name, value = l[0].strip(), l[1].strip()
  345. if not name or not value:
  346. raise mesonlib.MesonException('Invalid variable "{}". Variables must be in \'name=value\' format'.format(var))
  347. # Variable names must not contain whitespaces
  348. if any(c.isspace() for c in name):
  349. raise mesonlib.MesonException('Invalid whitespace in assignment "{}"'.format(var))
  350. if name in reserved:
  351. raise mesonlib.MesonException('Variable "{}" is reserved'.format(name))
  352. variables.append((name, value))
  353. return variables
  354. variables = parse_variable_list(mesonlib.stringlistify(kwargs.get('variables', [])))
  355. pcfile = filebase + '.pc'
  356. pkgroot = kwargs.get('install_dir', default_install_dir)
  357. if pkgroot is None:
  358. pkgroot = os.path.join(state.environment.coredata.get_builtin_option('libdir'), 'pkgconfig')
  359. if not isinstance(pkgroot, str):
  360. raise mesonlib.MesonException('Install_dir must be a string.')
  361. self.generate_pkgconfig_file(state, deps, subdirs, name, description, url,
  362. version, pcfile, conflicts, variables)
  363. res = build.Data(mesonlib.File(True, state.environment.get_scratch_dir(), pcfile), pkgroot)
  364. return ModuleReturnValue(res, [res])
  365. def initialize(*args, **kwargs):
  366. return PkgConfigModule(*args, **kwargs)