executor.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. # Copyright 2019 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. # This class contains the basic functionality needed to run any interpreter
  12. # or an interpreter-based tool.
  13. import subprocess as S
  14. from pathlib import Path
  15. from threading import Thread
  16. import typing as T
  17. import re
  18. import os
  19. import shutil
  20. import ctypes
  21. import textwrap
  22. from .. import mlog, mesonlib
  23. from ..mesonlib import PerMachine, Popen_safe, version_compare, MachineChoice
  24. from ..environment import Environment
  25. if T.TYPE_CHECKING:
  26. from ..dependencies.base import ExternalProgram
  27. TYPE_result = T.Tuple[int, T.Optional[str], T.Optional[str]]
  28. class CMakeExecutor:
  29. # The class's copy of the CMake path. Avoids having to search for it
  30. # multiple times in the same Meson invocation.
  31. class_cmakebin = PerMachine(None, None)
  32. class_cmakevers = PerMachine(None, None)
  33. class_cmake_cache = {} # type: T.Dict[T.Any, TYPE_result]
  34. def __init__(self, environment: Environment, version: str, for_machine: MachineChoice, silent: bool = False):
  35. self.min_version = version
  36. self.environment = environment
  37. self.for_machine = for_machine
  38. self.cmakebin, self.cmakevers = self.find_cmake_binary(self.environment, silent=silent)
  39. self.always_capture_stderr = True
  40. self.print_cmout = False
  41. if self.cmakebin is False:
  42. self.cmakebin = None
  43. return
  44. if not version_compare(self.cmakevers, self.min_version):
  45. mlog.warning(
  46. 'The version of CMake', mlog.bold(self.cmakebin.get_path()),
  47. 'is', mlog.bold(self.cmakevers), 'but version', mlog.bold(self.min_version),
  48. 'is required')
  49. self.cmakebin = None
  50. return
  51. def find_cmake_binary(self, environment: Environment, silent: bool = False) -> T.Tuple['ExternalProgram', str]:
  52. from ..dependencies.base import ExternalProgram
  53. # Create an iterator of options
  54. def search():
  55. # Lookup in cross or machine file.
  56. potential_cmakepath = environment.lookup_binary_entry(self.for_machine, 'cmake')
  57. if potential_cmakepath is not None:
  58. mlog.debug('CMake binary for %s specified from cross file, native file, or env var as %s.', self.for_machine, potential_cmakepath)
  59. yield ExternalProgram.from_entry('cmake', potential_cmakepath)
  60. # We never fallback if the user-specified option is no good, so
  61. # stop returning options.
  62. return
  63. mlog.debug('CMake binary missing from cross or native file, or env var undefined.')
  64. # Fallback on hard-coded defaults.
  65. # TODO prefix this for the cross case instead of ignoring thing.
  66. if environment.machines.matches_build_machine(self.for_machine):
  67. for potential_cmakepath in environment.default_cmake:
  68. mlog.debug('Trying a default CMake fallback at', potential_cmakepath)
  69. yield ExternalProgram(potential_cmakepath, silent=True)
  70. # Only search for CMake the first time and store the result in the class
  71. # definition
  72. if CMakeExecutor.class_cmakebin[self.for_machine] is False:
  73. mlog.debug('CMake binary for %s is cached as not found' % self.for_machine)
  74. elif CMakeExecutor.class_cmakebin[self.for_machine] is not None:
  75. mlog.debug('CMake binary for %s is cached.' % self.for_machine)
  76. else:
  77. assert CMakeExecutor.class_cmakebin[self.for_machine] is None
  78. mlog.debug('CMake binary for %s is not cached' % self.for_machine)
  79. for potential_cmakebin in search():
  80. mlog.debug('Trying CMake binary {} for machine {} at {}'
  81. .format(potential_cmakebin.name, self.for_machine, potential_cmakebin.command))
  82. version_if_ok = self.check_cmake(potential_cmakebin)
  83. if not version_if_ok:
  84. continue
  85. if not silent:
  86. mlog.log('Found CMake:', mlog.bold(potential_cmakebin.get_path()),
  87. '(%s)' % version_if_ok)
  88. CMakeExecutor.class_cmakebin[self.for_machine] = potential_cmakebin
  89. CMakeExecutor.class_cmakevers[self.for_machine] = version_if_ok
  90. break
  91. else:
  92. if not silent:
  93. mlog.log('Found CMake:', mlog.red('NO'))
  94. # Set to False instead of None to signify that we've already
  95. # searched for it and not found it
  96. CMakeExecutor.class_cmakebin[self.for_machine] = False
  97. CMakeExecutor.class_cmakevers[self.for_machine] = None
  98. return CMakeExecutor.class_cmakebin[self.for_machine], CMakeExecutor.class_cmakevers[self.for_machine]
  99. def check_cmake(self, cmakebin: 'ExternalProgram') -> T.Optional[str]:
  100. if not cmakebin.found():
  101. mlog.log('Did not find CMake {!r}'.format(cmakebin.name))
  102. return None
  103. try:
  104. p, out = Popen_safe(cmakebin.get_command() + ['--version'])[0:2]
  105. if p.returncode != 0:
  106. mlog.warning('Found CMake {!r} but couldn\'t run it'
  107. ''.format(' '.join(cmakebin.get_command())))
  108. return None
  109. except FileNotFoundError:
  110. mlog.warning('We thought we found CMake {!r} but now it\'s not there. How odd!'
  111. ''.format(' '.join(cmakebin.get_command())))
  112. return None
  113. except PermissionError:
  114. msg = 'Found CMake {!r} but didn\'t have permissions to run it.'.format(' '.join(cmakebin.get_command()))
  115. if not mesonlib.is_windows():
  116. msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.'
  117. mlog.warning(msg)
  118. return None
  119. cmvers = re.sub(r'\s*(cmake|cmake3) version\s*', '', out.split('\n')[0]).strip()
  120. return cmvers
  121. def set_exec_mode(self, print_cmout: T.Optional[bool] = None, always_capture_stderr: T.Optional[bool] = None) -> None:
  122. if print_cmout is not None:
  123. self.print_cmout = print_cmout
  124. if always_capture_stderr is not None:
  125. self.always_capture_stderr = always_capture_stderr
  126. def _cache_key(self, args: T.List[str], build_dir: str, env):
  127. fenv = frozenset(env.items()) if env is not None else None
  128. targs = tuple(args)
  129. return (self.cmakebin, targs, build_dir, fenv)
  130. def _call_cmout_stderr(self, args: T.List[str], build_dir: str, env) -> TYPE_result:
  131. cmd = self.cmakebin.get_command() + args
  132. proc = S.Popen(cmd, stdout=S.PIPE, stderr=S.PIPE, cwd=build_dir, env=env)
  133. # stdout and stderr MUST be read at the same time to avoid pipe
  134. # blocking issues. The easiest way to do this is with a separate
  135. # thread for one of the pipes.
  136. def print_stdout():
  137. while True:
  138. line = proc.stdout.readline()
  139. if not line:
  140. break
  141. mlog.log(line.decode(errors='ignore').strip('\n'))
  142. proc.stdout.close()
  143. t = Thread(target=print_stdout)
  144. t.start()
  145. try:
  146. # Read stderr line by line and log non trace lines
  147. raw_trace = ''
  148. tline_start_reg = re.compile(r'^\s*(.*\.(cmake|txt))\(([0-9]+)\):\s*(\w+)\(.*$')
  149. inside_multiline_trace = False
  150. while True:
  151. line = proc.stderr.readline()
  152. if not line:
  153. break
  154. line = line.decode(errors='ignore')
  155. if tline_start_reg.match(line):
  156. raw_trace += line
  157. inside_multiline_trace = not line.endswith(' )\n')
  158. elif inside_multiline_trace:
  159. raw_trace += line
  160. else:
  161. mlog.warning(line.strip('\n'))
  162. finally:
  163. proc.stderr.close()
  164. t.join()
  165. proc.wait()
  166. return proc.returncode, None, raw_trace
  167. def _call_cmout(self, args: T.List[str], build_dir: str, env) -> TYPE_result:
  168. cmd = self.cmakebin.get_command() + args
  169. proc = S.Popen(cmd, stdout=S.PIPE, stderr=S.STDOUT, cwd=build_dir, env=env)
  170. while True:
  171. line = proc.stdout.readline()
  172. if not line:
  173. break
  174. mlog.log(line.decode(errors='ignore').strip('\n'))
  175. proc.stdout.close()
  176. proc.wait()
  177. return proc.returncode, None, None
  178. def _call_quiet(self, args: T.List[str], build_dir: str, env) -> TYPE_result:
  179. os.makedirs(build_dir, exist_ok=True)
  180. cmd = self.cmakebin.get_command() + args
  181. ret = S.run(cmd, env=env, cwd=build_dir, close_fds=False,
  182. stdout=S.PIPE, stderr=S.PIPE, universal_newlines=False)
  183. rc = ret.returncode
  184. out = ret.stdout.decode(errors='ignore')
  185. err = ret.stderr.decode(errors='ignore')
  186. call = ' '.join(cmd)
  187. mlog.debug("Called `{}` in {} -> {}".format(call, build_dir, rc))
  188. return rc, out, err
  189. def _call_impl(self, args: T.List[str], build_dir: str, env) -> TYPE_result:
  190. if not self.print_cmout:
  191. return self._call_quiet(args, build_dir, env)
  192. else:
  193. if self.always_capture_stderr:
  194. return self._call_cmout_stderr(args, build_dir, env)
  195. else:
  196. return self._call_cmout(args, build_dir, env)
  197. def call(self, args: T.List[str], build_dir: str, env=None, disable_cache: bool = False) -> TYPE_result:
  198. if env is None:
  199. env = os.environ
  200. if disable_cache:
  201. return self._call_impl(args, build_dir, env)
  202. # First check if cached, if not call the real cmake function
  203. cache = CMakeExecutor.class_cmake_cache
  204. key = self._cache_key(args, build_dir, env)
  205. if key not in cache:
  206. cache[key] = self._call_impl(args, build_dir, env)
  207. return cache[key]
  208. def call_with_fake_build(self, args: T.List[str], build_dir: str, env=None) -> TYPE_result:
  209. # First check the cache
  210. cache = CMakeExecutor.class_cmake_cache
  211. key = self._cache_key(args, build_dir, env)
  212. if key in cache:
  213. return cache[key]
  214. os.makedirs(build_dir, exist_ok=True)
  215. # Try to set the correct compiler for C and C++
  216. # This step is required to make try_compile work inside CMake
  217. fallback = os.path.realpath(__file__) # A file used as a fallback wehen everything else fails
  218. compilers = self.environment.coredata.compilers[MachineChoice.BUILD]
  219. def make_abs(exe: str, lang: str) -> str:
  220. if os.path.isabs(exe):
  221. return exe
  222. p = shutil.which(exe)
  223. if p is None:
  224. mlog.debug('Failed to find a {} compiler for CMake. This might cause CMake to fail.'.format(lang))
  225. p = fallback
  226. return p
  227. def choose_compiler(lang: str) -> T.Tuple[str, str]:
  228. exe_list = []
  229. if lang in compilers:
  230. exe_list = compilers[lang].get_exelist()
  231. else:
  232. try:
  233. comp_obj = self.environment.compiler_from_language(lang, MachineChoice.BUILD)
  234. if comp_obj is not None:
  235. exe_list = comp_obj.get_exelist()
  236. except Exception:
  237. pass
  238. if len(exe_list) == 1:
  239. return make_abs(exe_list[0], lang), ''
  240. elif len(exe_list) == 2:
  241. return make_abs(exe_list[1], lang), make_abs(exe_list[0], lang)
  242. else:
  243. mlog.debug('Failed to find a {} compiler for CMake. This might cause CMake to fail.'.format(lang))
  244. return fallback, ''
  245. c_comp, c_launcher = choose_compiler('c')
  246. cxx_comp, cxx_launcher = choose_compiler('cpp')
  247. fortran_comp, fortran_launcher = choose_compiler('fortran')
  248. # on Windows, choose_compiler returns path with \ as separator - replace by / before writing to CMAKE file
  249. c_comp = c_comp.replace('\\', '/')
  250. c_launcher = c_launcher.replace('\\', '/')
  251. cxx_comp = cxx_comp.replace('\\', '/')
  252. cxx_launcher = cxx_launcher.replace('\\', '/')
  253. fortran_comp = fortran_comp.replace('\\', '/')
  254. fortran_launcher = fortran_launcher.replace('\\', '/')
  255. # Reset the CMake cache
  256. (Path(build_dir) / 'CMakeCache.txt').write_text('CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1\n')
  257. # Fake the compiler files
  258. comp_dir = Path(build_dir) / 'CMakeFiles' / self.cmakevers
  259. comp_dir.mkdir(parents=True, exist_ok=True)
  260. c_comp_file = comp_dir / 'CMakeCCompiler.cmake'
  261. cxx_comp_file = comp_dir / 'CMakeCXXCompiler.cmake'
  262. fortran_comp_file = comp_dir / 'CMakeFortranCompiler.cmake'
  263. if c_comp and not c_comp_file.is_file():
  264. c_comp_file.write_text(textwrap.dedent('''\
  265. # Fake CMake file to skip the boring and slow stuff
  266. set(CMAKE_C_COMPILER "{}") # Should be a valid compiler for try_compile, etc.
  267. set(CMAKE_C_COMPILER_LAUNCHER "{}") # The compiler launcher (if presentt)
  268. set(CMAKE_C_COMPILER_ID "GNU") # Pretend we have found GCC
  269. set(CMAKE_COMPILER_IS_GNUCC 1)
  270. set(CMAKE_C_COMPILER_LOADED 1)
  271. set(CMAKE_C_COMPILER_WORKS TRUE)
  272. set(CMAKE_C_ABI_COMPILED TRUE)
  273. set(CMAKE_C_SOURCE_FILE_EXTENSIONS c;m)
  274. set(CMAKE_C_IGNORE_EXTENSIONS h;H;o;O;obj;OBJ;def;DEF;rc;RC)
  275. set(CMAKE_SIZEOF_VOID_P "{}")
  276. '''.format(c_comp, c_launcher, ctypes.sizeof(ctypes.c_voidp))))
  277. if cxx_comp and not cxx_comp_file.is_file():
  278. cxx_comp_file.write_text(textwrap.dedent('''\
  279. # Fake CMake file to skip the boring and slow stuff
  280. set(CMAKE_CXX_COMPILER "{}") # Should be a valid compiler for try_compile, etc.
  281. set(CMAKE_CXX_COMPILER_LAUNCHER "{}") # The compiler launcher (if presentt)
  282. set(CMAKE_CXX_COMPILER_ID "GNU") # Pretend we have found GCC
  283. set(CMAKE_COMPILER_IS_GNUCXX 1)
  284. set(CMAKE_CXX_COMPILER_LOADED 1)
  285. set(CMAKE_CXX_COMPILER_WORKS TRUE)
  286. set(CMAKE_CXX_ABI_COMPILED TRUE)
  287. set(CMAKE_CXX_IGNORE_EXTENSIONS inl;h;hpp;HPP;H;o;O;obj;OBJ;def;DEF;rc;RC)
  288. set(CMAKE_CXX_SOURCE_FILE_EXTENSIONS C;M;c++;cc;cpp;cxx;mm;CPP)
  289. set(CMAKE_SIZEOF_VOID_P "{}")
  290. '''.format(cxx_comp, cxx_launcher, ctypes.sizeof(ctypes.c_voidp))))
  291. if fortran_comp and not fortran_comp_file.is_file():
  292. fortran_comp_file.write_text(textwrap.dedent('''\
  293. # Fake CMake file to skip the boring and slow stuff
  294. set(CMAKE_Fortran_COMPILER "{}") # Should be a valid compiler for try_compile, etc.
  295. set(CMAKE_Fortran_COMPILER_LAUNCHER "{}") # The compiler launcher (if presentt)
  296. set(CMAKE_Fortran_COMPILER_ID "GNU") # Pretend we have found GCC
  297. set(CMAKE_COMPILER_IS_GNUG77 1)
  298. set(CMAKE_Fortran_COMPILER_LOADED 1)
  299. set(CMAKE_Fortran_COMPILER_WORKS TRUE)
  300. set(CMAKE_Fortran_ABI_COMPILED TRUE)
  301. set(CMAKE_Fortran_IGNORE_EXTENSIONS h;H;o;O;obj;OBJ;def;DEF;rc;RC)
  302. set(CMAKE_Fortran_SOURCE_FILE_EXTENSIONS f;F;fpp;FPP;f77;F77;f90;F90;for;For;FOR;f95;F95)
  303. set(CMAKE_SIZEOF_VOID_P "{}")
  304. '''.format(fortran_comp, fortran_launcher, ctypes.sizeof(ctypes.c_voidp))))
  305. return self.call(args, build_dir, env)
  306. def found(self) -> bool:
  307. return self.cmakebin is not None
  308. def version(self) -> str:
  309. return self.cmakevers
  310. def executable_path(self) -> str:
  311. return self.cmakebin.get_path()
  312. def get_command(self) -> T.List[str]:
  313. return self.cmakebin.get_command()
  314. def machine_choice(self) -> MachineChoice:
  315. return self.for_machine