mesonlib.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. # Copyright 2012-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. """A library of random helper functionality."""
  12. import platform, subprocess, operator, os, shutil, re, sys
  13. from glob import glob
  14. class MesonException(Exception):
  15. def __init__(self, *args, **kwargs):
  16. Exception.__init__(self, *args, **kwargs)
  17. class File:
  18. def __init__(self, is_built, subdir, fname):
  19. self.is_built = is_built
  20. self.subdir = subdir
  21. self.fname = fname
  22. def __str__(self):
  23. return os.path.join(self.subdir, self.fname)
  24. def __repr__(self):
  25. ret = '<File: {0}'
  26. if not self.is_built:
  27. ret += ' (not built)'
  28. ret += '>'
  29. return ret.format(os.path.join(self.subdir, self.fname))
  30. @staticmethod
  31. def from_source_file(source_root, subdir, fname):
  32. if not os.path.isfile(os.path.join(source_root, subdir, fname)):
  33. raise MesonException('File %s does not exist.' % fname)
  34. return File(False, subdir, fname)
  35. @staticmethod
  36. def from_built_file(subdir, fname):
  37. return File(True, subdir, fname)
  38. @staticmethod
  39. def from_absolute_file(fname):
  40. return File(False, '', fname)
  41. def rel_to_builddir(self, build_to_src):
  42. if self.is_built:
  43. return os.path.join(self.subdir, self.fname)
  44. else:
  45. return os.path.join(build_to_src, self.subdir, self.fname)
  46. def endswith(self, ending):
  47. return self.fname.endswith(ending)
  48. def split(self, s):
  49. return self.fname.split(s)
  50. def __eq__(self, other):
  51. return (self.fname, self.subdir, self.is_built) == (other.fname, other.subdir, other.is_built)
  52. def __hash__(self):
  53. return hash((self.fname, self.subdir, self.is_built))
  54. def flatten(item):
  55. if not isinstance(item, list):
  56. return item
  57. result = []
  58. for i in item:
  59. if isinstance(i, list):
  60. result += flatten(i)
  61. else:
  62. result.append(i)
  63. return result
  64. def is_osx():
  65. return platform.system().lower() == 'darwin'
  66. def is_linux():
  67. return platform.system().lower() == 'linux'
  68. def is_windows():
  69. platname = platform.system().lower()
  70. return platname == 'windows' or 'mingw' in platname
  71. def is_debianlike():
  72. return os.path.isfile('/etc/debian_version')
  73. def exe_exists(arglist):
  74. try:
  75. p = subprocess.Popen(arglist, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  76. p.communicate()
  77. if p.returncode == 0:
  78. return True
  79. except FileNotFoundError:
  80. pass
  81. return False
  82. def detect_vcs(source_dir):
  83. vcs_systems = [
  84. dict(name = 'git', cmd = 'git', repo_dir = '.git', get_rev = 'git describe --dirty=+', rev_regex = '(.*)', dep = '.git/logs/HEAD'),
  85. dict(name = 'mercurial', cmd = 'hg', repo_dir = '.hg', get_rev = 'hg id -i', rev_regex = '(.*)', dep = '.hg/dirstate'),
  86. dict(name = 'subversion', cmd = 'svn', repo_dir = '.svn', get_rev = 'svn info', rev_regex = 'Revision: (.*)', dep = '.svn/wc.db'),
  87. dict(name = 'bazaar', cmd = 'bzr', repo_dir = '.bzr', get_rev = 'bzr revno', rev_regex = '(.*)', dep = '.bzr'),
  88. ]
  89. segs = source_dir.replace('\\', '/').split('/')
  90. for i in range(len(segs), -1, -1):
  91. curdir = '/'.join(segs[:i])
  92. for vcs in vcs_systems:
  93. if os.path.isdir(os.path.join(curdir, vcs['repo_dir'])) and shutil.which(vcs['cmd']):
  94. vcs['wc_dir'] = curdir
  95. return vcs
  96. return None
  97. def grab_leading_numbers(vstr):
  98. result = []
  99. for x in vstr.split('.'):
  100. try:
  101. result.append(int(x))
  102. except ValueError:
  103. break
  104. return result
  105. numpart = re.compile('[0-9.]+')
  106. def version_compare(vstr1, vstr2):
  107. match = numpart.match(vstr1.strip())
  108. if match is None:
  109. raise MesonException('Uncomparable version string %s.' % vstr1)
  110. vstr1 = match.group(0)
  111. if vstr2.startswith('>='):
  112. cmpop = operator.ge
  113. vstr2 = vstr2[2:]
  114. elif vstr2.startswith('<='):
  115. cmpop = operator.le
  116. vstr2 = vstr2[2:]
  117. elif vstr2.startswith('!='):
  118. cmpop = operator.ne
  119. vstr2 = vstr2[2:]
  120. elif vstr2.startswith('=='):
  121. cmpop = operator.eq
  122. vstr2 = vstr2[2:]
  123. elif vstr2.startswith('='):
  124. cmpop = operator.eq
  125. vstr2 = vstr2[1:]
  126. elif vstr2.startswith('>'):
  127. cmpop = operator.gt
  128. vstr2 = vstr2[1:]
  129. elif vstr2.startswith('<'):
  130. cmpop = operator.lt
  131. vstr2 = vstr2[1:]
  132. else:
  133. cmpop = operator.eq
  134. varr1 = grab_leading_numbers(vstr1)
  135. varr2 = grab_leading_numbers(vstr2)
  136. return cmpop(varr1, varr2)
  137. def default_libdir():
  138. try:
  139. pc = subprocess.Popen(['dpkg-architecture', '-qDEB_HOST_MULTIARCH'],
  140. stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
  141. (stdo, _) = pc.communicate()
  142. if pc.returncode == 0:
  143. archpath = stdo.decode().strip()
  144. return 'lib/' + archpath
  145. except Exception:
  146. pass
  147. if os.path.isdir('/usr/lib64'):
  148. return 'lib64'
  149. return 'lib'
  150. def default_libexecdir():
  151. # There is no way to auto-detect this, so it must be set at build time
  152. return 'libexec'
  153. def default_prefix():
  154. return 'c:/' if is_windows() else '/usr/local'
  155. def get_library_dirs():
  156. if is_windows():
  157. return ['C:/mingw/lib'] # Fixme
  158. if is_osx():
  159. return ['/usr/lib'] # Fix me as well.
  160. # The following is probably Debian/Ubuntu specific.
  161. # /usr/local/lib is first because it contains stuff
  162. # installed by the sysadmin and is probably more up-to-date
  163. # than /usr/lib. If you feel that this search order is
  164. # problematic, please raise the issue on the mailing list.
  165. unixdirs = ['/usr/local/lib', '/usr/lib', '/lib']
  166. plat = subprocess.check_output(['uname', '-m']).decode().strip()
  167. # This is a terrible hack. I admit it and I'm really sorry.
  168. # I just don't know what the correct solution is.
  169. if plat == 'i686':
  170. plat = 'i386'
  171. if plat.startswith('arm'):
  172. plat = 'arm'
  173. unixdirs += glob('/usr/lib/' + plat + '*')
  174. if os.path.exists('/usr/lib64'):
  175. unixdirs.append('/usr/lib64')
  176. unixdirs += glob('/lib/' + plat + '*')
  177. if os.path.exists('/lib64'):
  178. unixdirs.append('/lib64')
  179. unixdirs += glob('/lib/' + plat + '*')
  180. return unixdirs
  181. def do_replacement(regex, line, confdata):
  182. match = re.search(regex, line)
  183. while match:
  184. varname = match.group(1)
  185. if varname in confdata.keys():
  186. (var, desc) = confdata.get(varname)
  187. if isinstance(var, str):
  188. pass
  189. elif isinstance(var, int):
  190. var = str(var)
  191. else:
  192. raise RuntimeError('Tried to replace a variable with something other than a string or int.')
  193. else:
  194. var = ''
  195. line = line.replace('@' + varname + '@', var)
  196. match = re.search(regex, line)
  197. return line
  198. def do_mesondefine(line, confdata):
  199. arr = line.split()
  200. if len(arr) != 2:
  201. raise MesonException('#mesondefine does not contain exactly two tokens: %s', line.strip())
  202. varname = arr[1]
  203. try:
  204. (v, desc) = confdata.get(varname)
  205. except KeyError:
  206. return '/* #undef %s */\n' % varname
  207. if isinstance(v, bool):
  208. if v:
  209. return '#define %s\n' % varname
  210. else:
  211. return '#undef %s\n' % varname
  212. elif isinstance(v, int):
  213. return '#define %s %d\n' % (varname, v)
  214. elif isinstance(v, str):
  215. return '#define %s %s\n' % (varname, v)
  216. else:
  217. raise MesonException('#mesondefine argument "%s" is of unknown type.' % varname)
  218. def do_conf_file(src, dst, confdata):
  219. try:
  220. with open(src) as f:
  221. data = f.readlines()
  222. except Exception:
  223. raise MesonException('Could not read input file %s.' % src)
  224. # Only allow (a-z, A-Z, 0-9, _, -) as valid characters for a define
  225. # Also allow escaping '@' with '\@'
  226. regex = re.compile(r'[^\\]?@([-a-zA-Z0-9_]+)@')
  227. result = []
  228. for line in data:
  229. if line.startswith('#mesondefine'):
  230. line = do_mesondefine(line, confdata)
  231. else:
  232. line = do_replacement(regex, line, confdata)
  233. result.append(line)
  234. dst_tmp = dst + '~'
  235. with open(dst_tmp, 'w') as f:
  236. f.writelines(result)
  237. shutil.copymode(src, dst_tmp)
  238. replace_if_different(dst, dst_tmp)
  239. def dump_conf_header(ofilename, cdata):
  240. with open(ofilename, 'w') as ofile:
  241. ofile.write('''/*
  242. * Autogenerated by the Meson build system.
  243. * Do not edit, your changes will be lost.
  244. */
  245. #pragma once
  246. ''')
  247. for k in sorted(cdata.keys()):
  248. (v, desc) = cdata.get(k)
  249. if desc:
  250. ofile.write('/* %s */\n' % desc)
  251. if isinstance(v, bool):
  252. if v:
  253. ofile.write('#define %s\n\n' % k)
  254. else:
  255. ofile.write('#undef %s\n\n' % k)
  256. elif isinstance(v, (int, str)):
  257. ofile.write('#define %s %s\n\n' % (k, v))
  258. else:
  259. raise MesonException('Unknown data type in configuration file entry: ' + k)
  260. def replace_if_different(dst, dst_tmp):
  261. # If contents are identical, don't touch the file to prevent
  262. # unnecessary rebuilds.
  263. try:
  264. with open(dst, 'r') as f1, open(dst_tmp, 'r') as f2:
  265. if f1.read() == f2.read():
  266. os.unlink(dst_tmp)
  267. return
  268. except FileNotFoundError:
  269. pass
  270. os.replace(dst_tmp, dst)
  271. def stringlistify(item):
  272. if isinstance(item, str):
  273. item = [item]
  274. if not isinstance(item, list):
  275. raise MesonException('Item is not an array')
  276. for i in item:
  277. if not isinstance(i, str):
  278. raise MesonException('List item not a string.')
  279. return item
  280. def expand_arguments(args):
  281. expended_args = []
  282. for arg in args:
  283. if not arg.startswith('@'):
  284. expended_args.append(arg)
  285. continue
  286. args_file = arg[1:]
  287. try:
  288. with open(args_file) as f:
  289. extended_args = f.read().split()
  290. expended_args += extended_args
  291. except Exception as e:
  292. print('Error expanding command line arguments, %s not found' % args_file)
  293. print(e)
  294. return None
  295. return expended_args