406 lines
15 KiB
Python
Executable File
406 lines
15 KiB
Python
Executable File
#!/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, version as meson_version
|
|
|
|
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
|
|
NINJA_CMD = detect_ninja('1.9')
|
|
if NINJA_CMD is not None:
|
|
NINJA_1_9_OR_NEWER = True
|
|
else:
|
|
mlog.warning('Found ninja <1.9, tests will run slower', once=True)
|
|
NINJA_CMD = detect_ninja()
|
|
if NINJA_CMD is None:
|
|
raise RuntimeError('Could not find Ninja v1.7 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('')
|
|
print(flush=True)
|
|
|
|
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=[], dest='cross', action='append')
|
|
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
|
|
# 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.')
|
|
print(flush=True)
|
|
returncode = 0
|
|
else:
|
|
print(mlog.bold('Running unittests.').get_text(mlog.colorize_console()))
|
|
print(flush=True)
|
|
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']
|
|
for cf in options.cross:
|
|
print(mlog.bold('Running {} cross tests.'.format(cf)).get_text(mlog.colorize_console()))
|
|
print(flush=True)
|
|
cmd = cross_test_args + ['cross/' + cf]
|
|
if options.failfast:
|
|
cmd += ['--failfast']
|
|
returncode += subprocess.call(cmd, env=env)
|
|
if options.failfast and returncode != 0:
|
|
return returncode
|
|
return returncode
|
|
|
|
if __name__ == '__main__':
|
|
print('Meson build system', meson_version, 'Project and Unit Tests')
|
|
raise SystemExit(main())
|