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