1#!/usr/bin/env python3 2 3# Copyright 2012-2021 The Meson development team 4 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8 9# http://www.apache.org/licenses/LICENSE-2.0 10 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17# Work around some pathlib bugs... 18from mesonbuild import _pathlib 19import sys 20sys.modules['pathlib'] = _pathlib 21 22import collections 23import os 24import time 25import shutil 26import subprocess 27import platform 28import argparse 29import traceback 30from io import StringIO 31from enum import Enum 32from glob import glob 33from pathlib import Path 34from unittest import mock 35import typing as T 36 37from mesonbuild import compilers 38from mesonbuild import dependencies 39from mesonbuild import mesonlib 40from mesonbuild import mesonmain 41from mesonbuild import mtest 42from mesonbuild import mlog 43from mesonbuild.environment import Environment, detect_ninja 44from mesonbuild.coredata import backendlist, version as meson_version 45from mesonbuild.mesonlib import OptionKey 46 47NINJA_1_9_OR_NEWER = False 48NINJA_CMD = None 49# If we're on CI, just assume we have ninja in PATH and it's new enough because 50# we provide that. This avoids having to detect ninja for every subprocess unit 51# test that we run. 52if 'CI' in os.environ: 53 NINJA_1_9_OR_NEWER = True 54 NINJA_CMD = ['ninja'] 55else: 56 # Look for 1.9 to see if https://github.com/ninja-build/ninja/issues/1219 57 # is fixed 58 NINJA_CMD = detect_ninja('1.9') 59 if NINJA_CMD is not None: 60 NINJA_1_9_OR_NEWER = True 61 else: 62 mlog.warning('Found ninja <1.9, tests will run slower', once=True) 63 NINJA_CMD = detect_ninja() 64if NINJA_CMD is None: 65 raise RuntimeError('Could not find Ninja v1.7 or newer') 66 67def guess_backend(backend_str: str, msbuild_exe: str) -> T.Tuple['Backend', T.List[str]]: 68 # Auto-detect backend if unspecified 69 backend_flags = [] 70 if backend_str is None: 71 if msbuild_exe is not None and (mesonlib.is_windows() and not _using_intelcl()): 72 backend_str = 'vs' # Meson will auto-detect VS version to use 73 else: 74 backend_str = 'ninja' 75 76 # Set backend arguments for Meson 77 if backend_str.startswith('vs'): 78 backend_flags = ['--backend=' + backend_str] 79 backend = Backend.vs 80 elif backend_str == 'xcode': 81 backend_flags = ['--backend=xcode'] 82 backend = Backend.xcode 83 elif backend_str == 'ninja': 84 backend_flags = ['--backend=ninja'] 85 backend = Backend.ninja 86 else: 87 raise RuntimeError(f'Unknown backend: {backend_str!r}') 88 return (backend, backend_flags) 89 90 91def _using_intelcl() -> bool: 92 """ 93 detect if intending to using Intel-Cl compilers (Intel compilers on Windows) 94 Sufficient evidence of intent is that user is working in the Intel compiler 95 shell environment, otherwise this function returns False 96 """ 97 if not mesonlib.is_windows(): 98 return False 99 # handle where user tried to "blank" MKLROOT and left space(s) 100 if not os.environ.get('MKLROOT', '').strip(): 101 return False 102 if (os.environ.get('CC') == 'icl' or 103 os.environ.get('CXX') == 'icl' or 104 os.environ.get('FC') == 'ifort'): 105 return True 106 # Intel-Cl users might not have the CC,CXX,FC envvars set, 107 # but because they're in Intel shell, the exe's below are on PATH 108 if shutil.which('icl') or shutil.which('ifort'): 109 return True 110 mlog.warning('It appears you might be intending to use Intel compiler on Windows ' 111 'since non-empty environment variable MKLROOT is set to {} ' 112 'However, Meson cannot find the Intel WIndows compiler executables (icl,ifort).' 113 'Please try using the Intel shell.'.format(os.environ.get('MKLROOT'))) 114 return False 115 116 117# Fake classes and objects for mocking 118class FakeBuild: 119 def __init__(self, env): 120 self.environment = env 121 122class FakeCompilerOptions: 123 def __init__(self): 124 self.value = [] 125 126# TODO: use a typing.Protocol here 127def get_fake_options(prefix: str = '') -> argparse.Namespace: 128 opts = argparse.Namespace() 129 opts.native_file = [] 130 opts.cross_file = None 131 opts.wrap_mode = None 132 opts.prefix = prefix 133 opts.cmd_line_options = {} 134 return opts 135 136def get_fake_env(sdir='', bdir=None, prefix='', opts=None): 137 if opts is None: 138 opts = get_fake_options(prefix) 139 env = Environment(sdir, bdir, opts) 140 env.coredata.options[OptionKey('args', lang='c')] = FakeCompilerOptions() 141 env.machines.host.cpu_family = 'x86_64' # Used on macOS inside find_library 142 return env 143 144 145Backend = Enum('Backend', 'ninja vs xcode') 146 147if 'MESON_EXE' in os.environ: 148 meson_exe = mesonlib.split_args(os.environ['MESON_EXE']) 149else: 150 meson_exe = None 151 152if mesonlib.is_windows() or mesonlib.is_cygwin(): 153 exe_suffix = '.exe' 154else: 155 exe_suffix = '' 156 157def get_meson_script() -> str: 158 ''' 159 Guess the meson that corresponds to the `mesonbuild` that has been imported 160 so we can run configure and other commands in-process, since mesonmain.run 161 needs to know the meson_command to use. 162 163 Also used by run_unittests.py to determine what meson to run when not 164 running in-process (which is the default). 165 ''' 166 # Is there a meson.py next to the mesonbuild currently in use? 167 mesonbuild_dir = Path(mesonmain.__file__).resolve().parent.parent 168 meson_script = mesonbuild_dir / 'meson.py' 169 if meson_script.is_file(): 170 return str(meson_script) 171 # Then if mesonbuild is in PYTHONPATH, meson must be in PATH 172 mlog.warning('Could not find meson.py next to the mesonbuild module. ' 173 'Trying system meson...') 174 meson_cmd = shutil.which('meson') 175 if meson_cmd: 176 return meson_cmd 177 raise RuntimeError(f'Could not find {meson_script!r} or a meson in PATH') 178 179def get_backend_args_for_dir(backend: Backend, builddir: str) -> T.List[str]: 180 ''' 181 Visual Studio backend needs to be given the solution to build 182 ''' 183 if backend is Backend.vs: 184 sln_name = glob(os.path.join(builddir, '*.sln'))[0] 185 return [os.path.split(sln_name)[-1]] 186 return [] 187 188def find_vcxproj_with_target(builddir, target): 189 import re, fnmatch 190 t, ext = os.path.splitext(target) 191 if ext: 192 p = fr'<TargetName>{t}</TargetName>\s*<TargetExt>\{ext}</TargetExt>' 193 else: 194 p = fr'<TargetName>{t}</TargetName>' 195 for _, _, files in os.walk(builddir): 196 for f in fnmatch.filter(files, '*.vcxproj'): 197 f = os.path.join(builddir, f) 198 with open(f, encoding='utf-8') as o: 199 if re.search(p, o.read(), flags=re.MULTILINE): 200 return f 201 raise RuntimeError(f'No vcxproj matching {p!r} in {builddir!r}') 202 203def get_builddir_target_args(backend: Backend, builddir, target): 204 dir_args = [] 205 if not target: 206 dir_args = get_backend_args_for_dir(backend, builddir) 207 if target is None: 208 return dir_args 209 if backend is Backend.vs: 210 vcxproj = find_vcxproj_with_target(builddir, target) 211 target_args = [vcxproj] 212 elif backend is Backend.xcode: 213 target_args = ['-target', target] 214 elif backend is Backend.ninja: 215 target_args = [target] 216 else: 217 raise AssertionError(f'Unknown backend: {backend!r}') 218 return target_args + dir_args 219 220def get_backend_commands(backend: Backend, debug: bool = False) -> \ 221 T.Tuple[T.List[str], T.List[str], T.List[str], T.List[str], T.List[str]]: 222 install_cmd: T.List[str] = [] 223 uninstall_cmd: T.List[str] = [] 224 clean_cmd: T.List[str] 225 cmd: T.List[str] 226 test_cmd: T.List[str] 227 if backend is Backend.vs: 228 cmd = ['msbuild'] 229 clean_cmd = cmd + ['/target:Clean'] 230 test_cmd = cmd + ['RUN_TESTS.vcxproj'] 231 elif backend is Backend.xcode: 232 cmd = ['xcodebuild'] 233 clean_cmd = cmd + ['-alltargets', 'clean'] 234 test_cmd = cmd + ['-target', 'RUN_TESTS'] 235 elif backend is Backend.ninja: 236 global NINJA_CMD 237 cmd = NINJA_CMD + ['-w', 'dupbuild=err', '-d', 'explain'] 238 if debug: 239 cmd += ['-v'] 240 clean_cmd = cmd + ['clean'] 241 test_cmd = cmd + ['test', 'benchmark'] 242 install_cmd = cmd + ['install'] 243 uninstall_cmd = cmd + ['uninstall'] 244 else: 245 raise AssertionError(f'Unknown backend: {backend!r}') 246 return cmd, clean_cmd, test_cmd, install_cmd, uninstall_cmd 247 248def ensure_backend_detects_changes(backend: Backend) -> None: 249 global NINJA_1_9_OR_NEWER 250 if backend is not Backend.ninja: 251 return 252 need_workaround = False 253 # We're using ninja >= 1.9 which has QuLogic's patch for sub-1s resolution 254 # timestamps 255 if not NINJA_1_9_OR_NEWER: 256 mlog.warning('Don\'t have ninja >= 1.9, enabling timestamp resolution workaround', once=True) 257 need_workaround = True 258 # Increase the difference between build.ninja's timestamp and the timestamp 259 # of whatever you changed: https://github.com/ninja-build/ninja/issues/371 260 if need_workaround: 261 time.sleep(1) 262 263def run_mtest_inprocess(commandlist: T.List[str]) -> T.Tuple[int, str, str]: 264 stderr = StringIO() 265 stdout = StringIO() 266 with mock.patch.object(sys, 'stdout', stdout), mock.patch.object(sys, 'stderr', stderr): 267 returncode = mtest.run_with_args(commandlist) 268 return returncode, stdout.getvalue(), stderr.getvalue() 269 270def clear_meson_configure_class_caches() -> None: 271 compilers.CCompiler.find_library_cache = {} 272 compilers.CCompiler.find_framework_cache = {} 273 dependencies.PkgConfigDependency.pkgbin_cache = {} 274 dependencies.PkgConfigDependency.class_pkgbin = mesonlib.PerMachine(None, None) 275 mesonlib.project_meson_versions = collections.defaultdict(str) 276 277def run_configure_inprocess(commandlist: T.List[str], env: T.Optional[T.Dict[str, str]] = None, catch_exception: bool = False) -> T.Tuple[int, str, str]: 278 stderr = StringIO() 279 stdout = StringIO() 280 returncode = 0 281 with mock.patch.dict(os.environ, env or {}), mock.patch.object(sys, 'stdout', stdout), mock.patch.object(sys, 'stderr', stderr): 282 try: 283 returncode = mesonmain.run(commandlist, get_meson_script()) 284 except Exception: 285 if catch_exception: 286 returncode = 1 287 traceback.print_exc() 288 else: 289 raise 290 finally: 291 clear_meson_configure_class_caches() 292 return returncode, stdout.getvalue(), stderr.getvalue() 293 294def run_configure_external(full_command: T.List[str], env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, str, str]: 295 pc, o, e = mesonlib.Popen_safe(full_command, env=env) 296 return pc.returncode, o, e 297 298def run_configure(commandlist: T.List[str], env: T.Optional[T.Dict[str, str]] = None, catch_exception: bool = False) -> T.Tuple[int, str, str]: 299 global meson_exe 300 if meson_exe: 301 return run_configure_external(meson_exe + commandlist, env=env) 302 return run_configure_inprocess(commandlist, env=env, catch_exception=catch_exception) 303 304def print_system_info(): 305 print(mlog.bold('System information.')) 306 print('Architecture:', platform.architecture()) 307 print('Machine:', platform.machine()) 308 print('Platform:', platform.system()) 309 print('Processor:', platform.processor()) 310 print('System:', platform.system()) 311 print('') 312 print(flush=True) 313 314def main(): 315 print_system_info() 316 parser = argparse.ArgumentParser() 317 parser.add_argument('--backend', default=None, dest='backend', 318 choices=backendlist) 319 parser.add_argument('--cross', default=[], dest='cross', action='append') 320 parser.add_argument('--cross-only', action='store_true') 321 parser.add_argument('--failfast', action='store_true') 322 parser.add_argument('--no-unittests', action='store_true', default=False) 323 (options, _) = parser.parse_known_args() 324 returncode = 0 325 backend, _ = guess_backend(options.backend, shutil.which('msbuild')) 326 no_unittests = options.no_unittests 327 # Running on a developer machine? Be nice! 328 if not mesonlib.is_windows() and not mesonlib.is_haiku() and 'CI' not in os.environ: 329 os.nice(20) 330 # Appveyor sets the `platform` environment variable which completely messes 331 # up building with the vs2010 and vs2015 backends. 332 # 333 # Specifically, MSBuild reads the `platform` environment variable to set 334 # the configured value for the platform (Win32/x64/arm), which breaks x86 335 # builds. 336 # 337 # Appveyor setting this also breaks our 'native build arch' detection for 338 # Windows in environment.py:detect_windows_arch() by overwriting the value 339 # of `platform` set by vcvarsall.bat. 340 # 341 # While building for x86, `platform` should be unset. 342 if 'APPVEYOR' in os.environ and os.environ['arch'] == 'x86': 343 os.environ.pop('platform') 344 # Run tests 345 # Can't pass arguments to unit tests, so set the backend to use in the environment 346 env = os.environ.copy() 347 if not options.cross: 348 cmd = mesonlib.python_command + ['run_meson_command_tests.py', '-v'] 349 if options.failfast: 350 cmd += ['--failfast'] 351 returncode += subprocess.call(cmd, env=env) 352 if options.failfast and returncode != 0: 353 return returncode 354 if no_unittests: 355 print('Skipping all unit tests.') 356 print(flush=True) 357 returncode = 0 358 else: 359 print(mlog.bold('Running unittests.')) 360 print(flush=True) 361 cmd = mesonlib.python_command + ['run_unittests.py', '--backend=' + backend.name, '-v'] 362 if options.failfast: 363 cmd += ['--failfast'] 364 returncode += subprocess.call(cmd, env=env) 365 if options.failfast and returncode != 0: 366 return returncode 367 cmd = mesonlib.python_command + ['run_project_tests.py'] + sys.argv[1:] 368 returncode += subprocess.call(cmd, env=env) 369 else: 370 cross_test_args = mesonlib.python_command + ['run_cross_test.py'] 371 for cf in options.cross: 372 print(mlog.bold(f'Running {cf} cross tests.')) 373 print(flush=True) 374 cmd = cross_test_args + ['cross/' + cf] 375 if options.failfast: 376 cmd += ['--failfast'] 377 if options.cross_only: 378 cmd += ['--cross-only'] 379 returncode += subprocess.call(cmd, env=env) 380 if options.failfast and returncode != 0: 381 return returncode 382 return returncode 383 384if __name__ == '__main__': 385 mesonmain.setup_vsenv() 386 print('Meson build system', meson_version, 'Project and Unit Tests') 387 raise SystemExit(main()) 388