| #!/usr/bin/env python3 |
| |
| # Copyright 2012-2017 The Meson development team |
| |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import os |
| import sys |
| import time |
| import shutil |
| import subprocess |
| import tempfile |
| import platform |
| import argparse |
| from io import StringIO |
| from enum import Enum |
| from glob import glob |
| from pathlib import Path |
| from mesonbuild import compilers |
| from mesonbuild import dependencies |
| from mesonbuild import mesonlib |
| from mesonbuild import mesonmain |
| from mesonbuild import mtest |
| from mesonbuild import mlog |
| from mesonbuild.environment import Environment, detect_ninja |
| from mesonbuild.coredata import backendlist |
| |
| NINJA_1_9_OR_NEWER = False |
| NINJA_CMD = None |
| # If we're on CI, just assume we have ninja in PATH and it's new enough because |
| # we provide that. This avoids having to detect ninja for every subprocess unit |
| # test that we run. |
| if 'CI' in os.environ: |
| NINJA_1_9_OR_NEWER = True |
| NINJA_CMD = 'ninja' |
| else: |
| # Look for 1.9 to see if https://github.com/ninja-build/ninja/issues/1219 |
| # is fixed, else require 1.6 for -w dupbuild=err |
| for v in ('1.9', '1.6'): |
| NINJA_CMD = detect_ninja(v) |
| if NINJA_CMD is not None: |
| if mesonlib.version_compare(v, '>=1.9'): |
| NINJA_1_9_OR_NEWER = True |
| else: |
| mlog.warning('Found ninja <1.9, tests will run slower', once=True) |
| break |
| if NINJA_CMD is None: |
| raise RuntimeError('Could not find Ninja v1.6 or newer') |
| |
| def guess_backend(backend, msbuild_exe: str): |
| # Auto-detect backend if unspecified |
| backend_flags = [] |
| if backend is None: |
| if msbuild_exe is not None and (mesonlib.is_windows() and not _using_intelcl()): |
| backend = 'vs' # Meson will auto-detect VS version to use |
| else: |
| backend = 'ninja' |
| # Set backend arguments for Meson |
| if backend.startswith('vs'): |
| backend_flags = ['--backend=' + backend] |
| backend = Backend.vs |
| elif backend == 'xcode': |
| backend_flags = ['--backend=xcode'] |
| backend = Backend.xcode |
| elif backend == 'ninja': |
| backend_flags = ['--backend=ninja'] |
| backend = Backend.ninja |
| else: |
| raise RuntimeError('Unknown backend: {!r}'.format(backend)) |
| return (backend, backend_flags) |
| |
| |
| def _using_intelcl() -> bool: |
| """ |
| detect if intending to using Intel-Cl compilers (Intel compilers on Windows) |
| Sufficient evidence of intent is that user is working in the Intel compiler |
| shell environment, otherwise this function returns False |
| """ |
| if not mesonlib.is_windows(): |
| return False |
| # handle where user tried to "blank" MKLROOT and left space(s) |
| if not os.environ.get('MKLROOT', '').strip(): |
| return False |
| if (os.environ.get('CC') == 'icl' or |
| os.environ.get('CXX') == 'icl' or |
| os.environ.get('FC') == 'ifort'): |
| return True |
| # Intel-Cl users might not have the CC,CXX,FC envvars set, |
| # but because they're in Intel shell, the exe's below are on PATH |
| if shutil.which('icl') or shutil.which('ifort'): |
| return True |
| mlog.warning('It appears you might be intending to use Intel compiler on Windows ' |
| 'since non-empty environment variable MKLROOT is set to {} ' |
| 'However, Meson cannot find the Intel WIndows compiler executables (icl,ifort).' |
| 'Please try using the Intel shell.'.format(os.environ.get('MKLROOT'))) |
| return False |
| |
| |
| # Fake classes and objects for mocking |
| class FakeBuild: |
| def __init__(self, env): |
| self.environment = env |
| |
| class FakeCompilerOptions: |
| def __init__(self): |
| self.value = [] |
| |
| def get_fake_options(prefix=''): |
| opts = argparse.Namespace() |
| opts.native_file = [] |
| opts.cross_file = None |
| opts.wrap_mode = None |
| opts.prefix = prefix |
| opts.cmd_line_options = {} |
| return opts |
| |
| def get_fake_env(sdir='', bdir=None, prefix='', opts=None): |
| if opts is None: |
| opts = get_fake_options(prefix) |
| env = Environment(sdir, bdir, opts) |
| env.coredata.compiler_options.host['c_args'] = FakeCompilerOptions() |
| env.machines.host.cpu_family = 'x86_64' # Used on macOS inside find_library |
| return env |
| |
| |
| Backend = Enum('Backend', 'ninja vs xcode') |
| |
| if 'MESON_EXE' in os.environ: |
| meson_exe = mesonlib.split_args(os.environ['MESON_EXE']) |
| else: |
| meson_exe = None |
| |
| if mesonlib.is_windows() or mesonlib.is_cygwin(): |
| exe_suffix = '.exe' |
| else: |
| exe_suffix = '' |
| |
| def get_meson_script(): |
| ''' |
| Guess the meson that corresponds to the `mesonbuild` that has been imported |
| so we can run configure and other commands in-process, since mesonmain.run |
| needs to know the meson_command to use. |
| |
| Also used by run_unittests.py to determine what meson to run when not |
| running in-process (which is the default). |
| ''' |
| # Is there a meson.py next to the mesonbuild currently in use? |
| mesonbuild_dir = Path(mesonmain.__file__).resolve().parent.parent |
| meson_script = mesonbuild_dir / 'meson.py' |
| if meson_script.is_file(): |
| return str(meson_script) |
| # Then if mesonbuild is in PYTHONPATH, meson must be in PATH |
| mlog.warning('Could not find meson.py next to the mesonbuild module. ' |
| 'Trying system meson...') |
| meson_cmd = shutil.which('meson') |
| if meson_cmd: |
| return meson_cmd |
| raise RuntimeError('Could not find {!r} or a meson in PATH'.format(meson_script)) |
| |
| def get_backend_args_for_dir(backend, builddir): |
| ''' |
| Visual Studio backend needs to be given the solution to build |
| ''' |
| if backend is Backend.vs: |
| sln_name = glob(os.path.join(builddir, '*.sln'))[0] |
| return [os.path.split(sln_name)[-1]] |
| return [] |
| |
| def find_vcxproj_with_target(builddir, target): |
| import re, fnmatch |
| t, ext = os.path.splitext(target) |
| if ext: |
| p = r'<TargetName>{}</TargetName>\s*<TargetExt>\{}</TargetExt>'.format(t, ext) |
| else: |
| p = r'<TargetName>{}</TargetName>'.format(t) |
| for _, _, files in os.walk(builddir): |
| for f in fnmatch.filter(files, '*.vcxproj'): |
| f = os.path.join(builddir, f) |
| with open(f, 'r', encoding='utf-8') as o: |
| if re.search(p, o.read(), flags=re.MULTILINE): |
| return f |
| raise RuntimeError('No vcxproj matching {!r} in {!r}'.format(p, builddir)) |
| |
| def get_builddir_target_args(backend, builddir, target): |
| dir_args = [] |
| if not target: |
| dir_args = get_backend_args_for_dir(backend, builddir) |
| if target is None: |
| return dir_args |
| if backend is Backend.vs: |
| vcxproj = find_vcxproj_with_target(builddir, target) |
| target_args = [vcxproj] |
| elif backend is Backend.xcode: |
| target_args = ['-target', target] |
| elif backend is Backend.ninja: |
| target_args = [target] |
| else: |
| raise AssertionError('Unknown backend: {!r}'.format(backend)) |
| return target_args + dir_args |
| |
| def get_backend_commands(backend, debug=False): |
| install_cmd = [] |
| uninstall_cmd = [] |
| if backend is Backend.vs: |
| cmd = ['msbuild'] |
| clean_cmd = cmd + ['/target:Clean'] |
| test_cmd = cmd + ['RUN_TESTS.vcxproj'] |
| elif backend is Backend.xcode: |
| cmd = ['xcodebuild'] |
| # In Xcode9 new build system's clean command fails when using a custom build directory. |
| # Maybe use it when CI uses Xcode10 we can remove '-UseNewBuildSystem=FALSE' |
| clean_cmd = cmd + ['-alltargets', 'clean', '-UseNewBuildSystem=FALSE'] |
| test_cmd = cmd + ['-target', 'RUN_TESTS'] |
| elif backend is Backend.ninja: |
| global NINJA_CMD |
| cmd = [NINJA_CMD, '-w', 'dupbuild=err', '-d', 'explain'] |
| if debug: |
| cmd += ['-v'] |
| clean_cmd = cmd + ['clean'] |
| test_cmd = cmd + ['test', 'benchmark'] |
| install_cmd = cmd + ['install'] |
| uninstall_cmd = cmd + ['uninstall'] |
| else: |
| raise AssertionError('Unknown backend: {!r}'.format(backend)) |
| return cmd, clean_cmd, test_cmd, install_cmd, uninstall_cmd |
| |
| def ensure_backend_detects_changes(backend): |
| global NINJA_1_9_OR_NEWER |
| if backend is not Backend.ninja: |
| return |
| need_workaround = False |
| # We're not running on HFS+ which only stores dates in seconds: |
| # https://developer.apple.com/legacy/library/technotes/tn/tn1150.html#HFSPlusDates |
| # XXX: Upgrade Travis image to Apple FS when that becomes available |
| # TODO: Detect HFS+ vs APFS |
| if mesonlib.is_osx(): |
| mlog.warning('Running on HFS+, enabling timestamp resolution workaround', once=True) |
| need_workaround = True |
| # We're using ninja >= 1.9 which has QuLogic's patch for sub-1s resolution |
| # timestamps |
| if not NINJA_1_9_OR_NEWER: |
| mlog.warning('Don\'t have ninja >= 1.9, enabling timestamp resolution workaround', once=True) |
| need_workaround = True |
| # Increase the difference between build.ninja's timestamp and the timestamp |
| # of whatever you changed: https://github.com/ninja-build/ninja/issues/371 |
| if need_workaround: |
| time.sleep(1) |
| |
| def run_mtest_inprocess(commandlist): |
| old_stdout = sys.stdout |
| sys.stdout = mystdout = StringIO() |
| old_stderr = sys.stderr |
| sys.stderr = mystderr = StringIO() |
| try: |
| returncode = mtest.run_with_args(commandlist) |
| finally: |
| sys.stdout = old_stdout |
| sys.stderr = old_stderr |
| return returncode, mystdout.getvalue(), mystderr.getvalue() |
| |
| def clear_meson_configure_class_caches(): |
| compilers.CCompiler.library_dirs_cache = {} |
| compilers.CCompiler.program_dirs_cache = {} |
| compilers.CCompiler.find_library_cache = {} |
| compilers.CCompiler.find_framework_cache = {} |
| dependencies.PkgConfigDependency.pkgbin_cache = {} |
| dependencies.PkgConfigDependency.class_pkgbin = mesonlib.PerMachine(None, None) |
| |
| def run_configure_inprocess(commandlist, env=None): |
| old_stdout = sys.stdout |
| sys.stdout = mystdout = StringIO() |
| old_stderr = sys.stderr |
| sys.stderr = mystderr = StringIO() |
| old_environ = os.environ.copy() |
| if env is not None: |
| os.environ.update(env) |
| try: |
| returncode = mesonmain.run(commandlist, get_meson_script()) |
| finally: |
| sys.stdout = old_stdout |
| sys.stderr = old_stderr |
| clear_meson_configure_class_caches() |
| os.environ.clear() |
| os.environ.update(old_environ) |
| return returncode, mystdout.getvalue(), mystderr.getvalue() |
| |
| def run_configure_external(full_command, env=None): |
| pc, o, e = mesonlib.Popen_safe(full_command, env=env) |
| return pc.returncode, o, e |
| |
| def run_configure(commandlist, env=None): |
| global meson_exe |
| if meson_exe: |
| return run_configure_external(meson_exe + commandlist, env=env) |
| return run_configure_inprocess(commandlist, env=env) |
| |
| def print_system_info(): |
| print(mlog.bold('System information.').get_text(mlog.colorize_console)) |
| print('Architecture:', platform.architecture()) |
| print('Machine:', platform.machine()) |
| print('Platform:', platform.system()) |
| print('Processor:', platform.processor()) |
| print('System:', platform.system()) |
| print('') |
| |
| def main(): |
| print_system_info() |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--cov', action='store_true') |
| parser.add_argument('--backend', default=None, dest='backend', |
| choices=backendlist) |
| parser.add_argument('--cross', default=False, dest='cross', action='store_true') |
| parser.add_argument('--failfast', action='store_true') |
| parser.add_argument('--no-unittests', action='store_true', default=False) |
| (options, _) = parser.parse_known_args() |
| # Enable coverage early... |
| enable_coverage = options.cov |
| if enable_coverage: |
| os.makedirs('.coverage', exist_ok=True) |
| sys.argv.remove('--cov') |
| import coverage |
| coverage.process_startup() |
| returncode = 0 |
| cross = options.cross |
| backend, _ = guess_backend(options.backend, shutil.which('msbuild')) |
| no_unittests = options.no_unittests |
| # Running on a developer machine? Be nice! |
| if not mesonlib.is_windows() and not mesonlib.is_haiku() and 'CI' not in os.environ: |
| os.nice(20) |
| # Appveyor sets the `platform` environment variable which completely messes |
| # up building with the vs2010 and vs2015 backends. |
| # |
| # Specifically, MSBuild reads the `platform` environment variable to set |
| # the configured value for the platform (Win32/x64/arm), which breaks x86 |
| # builds. |
| # |
| # Appveyor setting this also breaks our 'native build arch' detection for |
| # Windows in environment.py:detect_windows_arch() by overwriting the value |
| # of `platform` set by vcvarsall.bat. |
| # |
| # While building for x86, `platform` should be unset. |
| if 'APPVEYOR' in os.environ and os.environ['arch'] == 'x86': |
| os.environ.pop('platform') |
| # Run tests |
| print(mlog.bold('Running unittests.').get_text(mlog.colorize_console)) |
| print(flush=True) |
| # Can't pass arguments to unit tests, so set the backend to use in the environment |
| env = os.environ.copy() |
| env['MESON_UNIT_TEST_BACKEND'] = backend.name |
| with tempfile.TemporaryDirectory() as temp_dir: |
| # Enable coverage on all subsequent processes. |
| if enable_coverage: |
| Path(temp_dir, 'usercustomize.py').open('w').write( |
| 'import coverage\n' |
| 'coverage.process_startup()\n') |
| env['COVERAGE_PROCESS_START'] = '.coveragerc' |
| if 'PYTHONPATH' in env: |
| env['PYTHONPATH'] = os.pathsep.join([temp_dir, env.get('PYTHONPATH')]) |
| else: |
| env['PYTHONPATH'] = temp_dir |
| if not cross: |
| cmd = mesonlib.python_command + ['run_meson_command_tests.py', '-v'] |
| if options.failfast: |
| cmd += ['--failfast'] |
| returncode += subprocess.call(cmd, env=env) |
| if options.failfast and returncode != 0: |
| return returncode |
| if no_unittests: |
| print('Skipping all unit tests.') |
| returncode = 0 |
| else: |
| cmd = mesonlib.python_command + ['run_unittests.py', '-v'] |
| if options.failfast: |
| cmd += ['--failfast'] |
| returncode += subprocess.call(cmd, env=env) |
| if options.failfast and returncode != 0: |
| return returncode |
| cmd = mesonlib.python_command + ['run_project_tests.py'] + sys.argv[1:] |
| returncode += subprocess.call(cmd, env=env) |
| else: |
| cross_test_args = mesonlib.python_command + ['run_cross_test.py'] |
| print(mlog.bold('Running armhf cross tests.').get_text(mlog.colorize_console)) |
| print(flush=True) |
| cmd = cross_test_args + ['cross/ubuntu-armhf.txt'] |
| if options.failfast: |
| cmd += ['--failfast'] |
| returncode += subprocess.call(cmd, env=env) |
| if options.failfast and returncode != 0: |
| return returncode |
| print(mlog.bold('Running mingw-w64 64-bit cross tests.') |
| .get_text(mlog.colorize_console)) |
| print(flush=True) |
| cmd = cross_test_args + ['cross/linux-mingw-w64-64bit.txt'] |
| if options.failfast: |
| cmd += ['--failfast'] |
| returncode += subprocess.call(cmd, env=env) |
| return returncode |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |