summaryrefslogtreecommitdiffstats
path: root/mesonbuild
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-29 04:41:38 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-29 04:41:38 +0000
commit7b6e527f440cd7e6f8be2b07cee320ee6ca18786 (patch)
tree4a2738d69fa2814659fdadddf5826282e73d81f4 /mesonbuild
parentInitial commit. (diff)
downloadmeson-upstream.tar.xz
meson-upstream.zip
Adding upstream version 1.0.1.upstream/1.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--mesonbuild/__init__.py0
-rw-r--r--mesonbuild/_pathlib.py73
-rw-r--r--mesonbuild/_typing.py81
-rw-r--r--mesonbuild/arglist.py331
-rw-r--r--mesonbuild/ast/__init__.py34
-rw-r--r--mesonbuild/ast/interpreter.py447
-rw-r--r--mesonbuild/ast/introspection.py362
-rw-r--r--mesonbuild/ast/postprocess.py120
-rw-r--r--mesonbuild/ast/printer.py399
-rw-r--r--mesonbuild/ast/visitor.py146
-rw-r--r--mesonbuild/backend/__init__.py0
-rw-r--r--mesonbuild/backend/backends.py1914
-rw-r--r--mesonbuild/backend/ninjabackend.py3623
-rw-r--r--mesonbuild/backend/vs2010backend.py1575
-rw-r--r--mesonbuild/backend/vs2012backend.py43
-rw-r--r--mesonbuild/backend/vs2013backend.py42
-rw-r--r--mesonbuild/backend/vs2015backend.py43
-rw-r--r--mesonbuild/backend/vs2017backend.py67
-rw-r--r--mesonbuild/backend/vs2019backend.py62
-rw-r--r--mesonbuild/backend/vs2022backend.py62
-rw-r--r--mesonbuild/backend/xcodebackend.py1719
-rw-r--r--mesonbuild/build.py2905
-rw-r--r--mesonbuild/cmake/__init__.py48
-rw-r--r--mesonbuild/cmake/common.py348
-rw-r--r--mesonbuild/cmake/data/__init__.py0
-rw-r--r--mesonbuild/cmake/data/preload.cmake82
-rw-r--r--mesonbuild/cmake/executor.py254
-rw-r--r--mesonbuild/cmake/fileapi.py321
-rw-r--r--mesonbuild/cmake/generator.py196
-rw-r--r--mesonbuild/cmake/interpreter.py1266
-rw-r--r--mesonbuild/cmake/toolchain.py258
-rw-r--r--mesonbuild/cmake/traceparser.py825
-rw-r--r--mesonbuild/cmake/tracetargets.py119
-rw-r--r--mesonbuild/compilers/__init__.py97
-rw-r--r--mesonbuild/compilers/asm.py231
-rw-r--r--mesonbuild/compilers/c.py740
-rw-r--r--mesonbuild/compilers/c_function_attributes.py141
-rw-r--r--mesonbuild/compilers/compilers.py1342
-rw-r--r--mesonbuild/compilers/cpp.py890
-rw-r--r--mesonbuild/compilers/cs.py154
-rw-r--r--mesonbuild/compilers/cuda.py791
-rw-r--r--mesonbuild/compilers/cython.py96
-rw-r--r--mesonbuild/compilers/d.py1022
-rw-r--r--mesonbuild/compilers/detect.py1317
-rw-r--r--mesonbuild/compilers/fortran.py546
-rw-r--r--mesonbuild/compilers/java.py125
-rw-r--r--mesonbuild/compilers/mixins/__init__.py0
-rw-r--r--mesonbuild/compilers/mixins/arm.py199
-rw-r--r--mesonbuild/compilers/mixins/ccrx.py134
-rw-r--r--mesonbuild/compilers/mixins/clang.py180
-rw-r--r--mesonbuild/compilers/mixins/clike.py1349
-rw-r--r--mesonbuild/compilers/mixins/compcert.py137
-rw-r--r--mesonbuild/compilers/mixins/elbrus.py101
-rw-r--r--mesonbuild/compilers/mixins/emscripten.py101
-rw-r--r--mesonbuild/compilers/mixins/gnu.py649
-rw-r--r--mesonbuild/compilers/mixins/intel.py185
-rw-r--r--mesonbuild/compilers/mixins/islinker.py130
-rw-r--r--mesonbuild/compilers/mixins/pgi.py113
-rw-r--r--mesonbuild/compilers/mixins/ti.py151
-rw-r--r--mesonbuild/compilers/mixins/visualstudio.py500
-rw-r--r--mesonbuild/compilers/mixins/xc16.py132
-rw-r--r--mesonbuild/compilers/objc.py114
-rw-r--r--mesonbuild/compilers/objcpp.py115
-rw-r--r--mesonbuild/compilers/rust.py208
-rw-r--r--mesonbuild/compilers/swift.py130
-rw-r--r--mesonbuild/compilers/vala.py140
-rw-r--r--mesonbuild/coredata.py1284
-rw-r--r--mesonbuild/dependencies/__init__.py286
-rw-r--r--mesonbuild/dependencies/base.py635
-rw-r--r--mesonbuild/dependencies/boost.py1090
-rw-r--r--mesonbuild/dependencies/cmake.py653
-rw-r--r--mesonbuild/dependencies/coarrays.py87
-rw-r--r--mesonbuild/dependencies/configtool.py186
-rw-r--r--mesonbuild/dependencies/cuda.py292
-rw-r--r--mesonbuild/dependencies/data/CMakeLists.txt98
-rw-r--r--mesonbuild/dependencies/data/CMakeListsLLVM.txt94
-rw-r--r--mesonbuild/dependencies/data/CMakePathInfo.txt31
-rw-r--r--mesonbuild/dependencies/data/__init__.py0
-rw-r--r--mesonbuild/dependencies/detect.py227
-rw-r--r--mesonbuild/dependencies/dev.py679
-rw-r--r--mesonbuild/dependencies/dub.py404
-rw-r--r--mesonbuild/dependencies/factory.py156
-rw-r--r--mesonbuild/dependencies/framework.py121
-rw-r--r--mesonbuild/dependencies/hdf5.py180
-rw-r--r--mesonbuild/dependencies/misc.py724
-rw-r--r--mesonbuild/dependencies/mpi.py237
-rw-r--r--mesonbuild/dependencies/pkgconfig.py505
-rw-r--r--mesonbuild/dependencies/platform.py60
-rw-r--r--mesonbuild/dependencies/qt.py486
-rw-r--r--mesonbuild/dependencies/scalapack.py156
-rw-r--r--mesonbuild/dependencies/ui.py255
-rw-r--r--mesonbuild/depfile.py91
-rw-r--r--mesonbuild/envconfig.py455
-rw-r--r--mesonbuild/environment.py858
-rw-r--r--mesonbuild/interpreter/__init__.py59
-rw-r--r--mesonbuild/interpreter/compiler.py781
-rw-r--r--mesonbuild/interpreter/dependencyfallbacks.py373
-rw-r--r--mesonbuild/interpreter/interpreter.py3275
-rw-r--r--mesonbuild/interpreter/interpreterobjects.py987
-rw-r--r--mesonbuild/interpreter/kwargs.py310
-rw-r--r--mesonbuild/interpreter/mesonmain.py456
-rw-r--r--mesonbuild/interpreter/primitives/__init__.py29
-rw-r--r--mesonbuild/interpreter/primitives/array.py108
-rw-r--r--mesonbuild/interpreter/primitives/boolean.py52
-rw-r--r--mesonbuild/interpreter/primitives/dict.py88
-rw-r--r--mesonbuild/interpreter/primitives/integer.py81
-rw-r--r--mesonbuild/interpreter/primitives/range.py38
-rw-r--r--mesonbuild/interpreter/primitives/string.py233
-rw-r--r--mesonbuild/interpreter/type_checking.py479
-rw-r--r--mesonbuild/interpreterbase/__init__.py135
-rw-r--r--mesonbuild/interpreterbase/_unholder.py35
-rw-r--r--mesonbuild/interpreterbase/baseobjects.py182
-rw-r--r--mesonbuild/interpreterbase/decorators.py791
-rw-r--r--mesonbuild/interpreterbase/disabler.py45
-rw-r--r--mesonbuild/interpreterbase/exceptions.py33
-rw-r--r--mesonbuild/interpreterbase/helpers.py56
-rw-r--r--mesonbuild/interpreterbase/interpreterbase.py604
-rw-r--r--mesonbuild/interpreterbase/operator.py32
-rw-r--r--mesonbuild/linkers/__init__.py136
-rw-r--r--mesonbuild/linkers/detect.py258
-rw-r--r--mesonbuild/linkers/linkers.py1556
-rw-r--r--mesonbuild/mcompile.py361
-rw-r--r--mesonbuild/mconf.py331
-rw-r--r--mesonbuild/mdevenv.py215
-rw-r--r--mesonbuild/mdist.py347
-rw-r--r--mesonbuild/mesondata.py48
-rw-r--r--mesonbuild/mesonlib.py35
-rw-r--r--mesonbuild/mesonmain.py290
-rw-r--r--mesonbuild/minit.py192
-rw-r--r--mesonbuild/minstall.py774
-rw-r--r--mesonbuild/mintro.py583
-rw-r--r--mesonbuild/mlog.py465
-rw-r--r--mesonbuild/modules/__init__.py262
-rw-r--r--mesonbuild/modules/cmake.py453
-rw-r--r--mesonbuild/modules/cuda.py383
-rw-r--r--mesonbuild/modules/dlang.py136
-rw-r--r--mesonbuild/modules/external_project.py307
-rw-r--r--mesonbuild/modules/fs.py315
-rw-r--r--mesonbuild/modules/gnome.py2165
-rw-r--r--mesonbuild/modules/hotdoc.py457
-rw-r--r--mesonbuild/modules/i18n.py390
-rw-r--r--mesonbuild/modules/icestorm.py131
-rw-r--r--mesonbuild/modules/java.py117
-rw-r--r--mesonbuild/modules/keyval.py75
-rw-r--r--mesonbuild/modules/modtest.py44
-rw-r--r--mesonbuild/modules/pkgconfig.py742
-rw-r--r--mesonbuild/modules/python.py835
-rw-r--r--mesonbuild/modules/python3.py85
-rw-r--r--mesonbuild/modules/qt.py608
-rw-r--r--mesonbuild/modules/qt4.py28
-rw-r--r--mesonbuild/modules/qt5.py28
-rw-r--r--mesonbuild/modules/qt6.py28
-rw-r--r--mesonbuild/modules/rust.py250
-rw-r--r--mesonbuild/modules/simd.py88
-rw-r--r--mesonbuild/modules/sourceset.py307
-rw-r--r--mesonbuild/modules/wayland.py160
-rw-r--r--mesonbuild/modules/windows.py212
-rw-r--r--mesonbuild/mparser.py839
-rw-r--r--mesonbuild/msetup.py311
-rwxr-xr-xmesonbuild/msubprojects.py726
-rw-r--r--mesonbuild/mtest.py2120
-rw-r--r--mesonbuild/munstable_coredata.py115
-rw-r--r--mesonbuild/optinterpreter.py225
-rw-r--r--mesonbuild/programs.py375
-rw-r--r--mesonbuild/rewriter.py1067
-rw-r--r--mesonbuild/scripts/__init__.py21
-rw-r--r--mesonbuild/scripts/clangformat.py61
-rw-r--r--mesonbuild/scripts/clangtidy.py35
-rw-r--r--mesonbuild/scripts/cleantrees.py45
-rwxr-xr-xmesonbuild/scripts/cmake_run_ctgt.py103
-rw-r--r--mesonbuild/scripts/cmd_or_ps.ps117
-rw-r--r--mesonbuild/scripts/copy.py19
-rw-r--r--mesonbuild/scripts/coverage.py202
-rw-r--r--mesonbuild/scripts/delwithsuffix.py37
-rw-r--r--mesonbuild/scripts/depfixer.py505
-rw-r--r--mesonbuild/scripts/depscan.py208
-rw-r--r--mesonbuild/scripts/dirchanger.py30
-rwxr-xr-xmesonbuild/scripts/env2mfile.py368
-rw-r--r--mesonbuild/scripts/externalproject.py116
-rw-r--r--mesonbuild/scripts/gettext.py96
-rw-r--r--mesonbuild/scripts/gtkdochelper.py296
-rw-r--r--mesonbuild/scripts/hotdochelper.py40
-rw-r--r--mesonbuild/scripts/itstool.py86
-rw-r--r--mesonbuild/scripts/meson_exe.py124
-rw-r--r--mesonbuild/scripts/msgfmthelper.py39
-rw-r--r--mesonbuild/scripts/regen_checker.py65
-rw-r--r--mesonbuild/scripts/run_tool.py68
-rw-r--r--mesonbuild/scripts/scanbuild.py66
-rw-r--r--mesonbuild/scripts/symbolextractor.py333
-rw-r--r--mesonbuild/scripts/tags.py54
-rw-r--r--mesonbuild/scripts/test_loaded_modules.py11
-rw-r--r--mesonbuild/scripts/uninstall.py51
-rw-r--r--mesonbuild/scripts/vcstagger.py45
-rw-r--r--mesonbuild/scripts/yasm.py22
-rw-r--r--mesonbuild/templates/__init__.py0
-rw-r--r--mesonbuild/templates/cpptemplates.py187
-rw-r--r--mesonbuild/templates/cstemplates.py136
-rw-r--r--mesonbuild/templates/ctemplates.py168
-rw-r--r--mesonbuild/templates/cudatemplates.py187
-rw-r--r--mesonbuild/templates/dlangtemplates.py145
-rw-r--r--mesonbuild/templates/fortrantemplates.py142
-rw-r--r--mesonbuild/templates/javatemplates.py138
-rw-r--r--mesonbuild/templates/mesontemplates.py77
-rw-r--r--mesonbuild/templates/objcpptemplates.py168
-rw-r--r--mesonbuild/templates/objctemplates.py168
-rw-r--r--mesonbuild/templates/rusttemplates.py115
-rw-r--r--mesonbuild/templates/samplefactory.py44
-rw-r--r--mesonbuild/templates/sampleimpl.py22
-rw-r--r--mesonbuild/templates/valatemplates.py125
-rw-r--r--mesonbuild/utils/__init__.py0
-rw-r--r--mesonbuild/utils/core.py151
-rw-r--r--mesonbuild/utils/platform.py38
-rw-r--r--mesonbuild/utils/posix.py43
-rw-r--r--mesonbuild/utils/universal.py2365
-rw-r--r--mesonbuild/utils/vsenv.py123
-rw-r--r--mesonbuild/utils/win32.py40
-rw-r--r--mesonbuild/wrap/__init__.py59
-rw-r--r--mesonbuild/wrap/wrap.py833
-rw-r--r--mesonbuild/wrap/wraptool.py231
219 files changed, 78460 insertions, 0 deletions
diff --git a/mesonbuild/__init__.py b/mesonbuild/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/mesonbuild/__init__.py
diff --git a/mesonbuild/_pathlib.py b/mesonbuild/_pathlib.py
new file mode 100644
index 0000000..640b5ed
--- /dev/null
+++ b/mesonbuild/_pathlib.py
@@ -0,0 +1,73 @@
+# Copyright 2021 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.
+
+'''
+ This module soly exists to work around a pathlib.resolve bug on
+ certain Windows systems:
+
+ https://github.com/mesonbuild/meson/issues/7295
+ https://bugs.python.org/issue31842
+
+ It should **never** be used directly. Instead, it is automatically
+ used when `import pathlib` is used. This is achieved by messing with
+ `sys.modules['pathlib']` in mesonmain.
+
+ Additionally, the sole purpose of this module is to work around a
+ python bug. This only bugfixes to pathlib functions and classes are
+ allowed here. Finally, this file should be removed once all upstream
+ python bugs are fixed and it is OK to tell our users to "just upgrade
+ python".
+'''
+
+import pathlib
+import os
+import platform
+
+__all__ = [
+ 'PurePath',
+ 'PurePosixPath',
+ 'PureWindowsPath',
+ 'Path',
+]
+
+PurePath = pathlib.PurePath
+PurePosixPath = pathlib.PurePosixPath
+PureWindowsPath = pathlib.PureWindowsPath
+
+# Only patch on platforms where the bug occurs
+if platform.system().lower() in {'windows'}:
+ # Can not directly inherit from pathlib.Path because the __new__
+ # operator of pathlib.Path() returns a {Posix,Windows}Path object.
+ class Path(type(pathlib.Path())):
+ def resolve(self, strict: bool = False) -> 'Path':
+ '''
+ Work around a resolve bug on certain Windows systems:
+
+ https://github.com/mesonbuild/meson/issues/7295
+ https://bugs.python.org/issue31842
+ '''
+
+ try:
+ return super().resolve(strict=strict)
+ except OSError:
+ return Path(os.path.normpath(self))
+else:
+ Path = pathlib.Path
+ PosixPath = pathlib.PosixPath
+ WindowsPath = pathlib.WindowsPath
+
+ __all__ += [
+ 'PosixPath',
+ 'WindowsPath',
+ ]
diff --git a/mesonbuild/_typing.py b/mesonbuild/_typing.py
new file mode 100644
index 0000000..d3cfa39
--- /dev/null
+++ b/mesonbuild/_typing.py
@@ -0,0 +1,81 @@
+# SPDX-License-Identifer: Apache-2.0
+# Copyright 2020 The Meson development team
+# Copyright © 2020-2021 Intel Corporation
+
+# 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.
+
+"""Meson specific typing helpers.
+
+Holds typing helper classes, such as the ImmutableProtocol classes
+"""
+
+__all__ = [
+ 'Protocol',
+ 'ImmutableListProtocol'
+]
+
+import typing
+
+# We can change this to typing when we require python 3.8
+from typing_extensions import Protocol
+
+
+T = typing.TypeVar('T')
+
+
+class StringProtocol(Protocol):
+ def __str__(self) -> str: ...
+
+class SizedStringProtocol(Protocol, StringProtocol, typing.Sized):
+ pass
+
+class ImmutableListProtocol(Protocol[T]):
+
+ """A protocol used in cases where a list is returned, but should not be
+ mutated.
+
+ This provides all of the methods of a Sequence, as well as copy(). copy()
+ returns a list, which allows mutation as it's a copy and that's (hopefully)
+ safe.
+
+ One particular case this is important is for cached values, since python is
+ a pass-by-reference language.
+ """
+
+ def __iter__(self) -> typing.Iterator[T]: ...
+
+ @typing.overload
+ def __getitem__(self, index: int) -> T: ...
+ @typing.overload
+ def __getitem__(self, index: slice) -> typing.List[T]: ...
+
+ def __contains__(self, item: T) -> bool: ...
+
+ def __reversed__(self) -> typing.Iterator[T]: ...
+
+ def __len__(self) -> int: ...
+
+ def __add__(self, other: typing.List[T]) -> typing.List[T]: ...
+
+ def __eq__(self, other: typing.Any) -> bool: ...
+ def __ne__(self, other: typing.Any) -> bool: ...
+ def __le__(self, other: typing.Any) -> bool: ...
+ def __lt__(self, other: typing.Any) -> bool: ...
+ def __gt__(self, other: typing.Any) -> bool: ...
+ def __ge__(self, other: typing.Any) -> bool: ...
+
+ def count(self, item: T) -> int: ...
+
+ def index(self, item: T) -> int: ...
+
+ def copy(self) -> typing.List[T]: ...
diff --git a/mesonbuild/arglist.py b/mesonbuild/arglist.py
new file mode 100644
index 0000000..475d954
--- /dev/null
+++ b/mesonbuild/arglist.py
@@ -0,0 +1,331 @@
+# Copyright 2012-2020 The Meson development team
+# Copyright © 2020 Intel Corporation
+
+# 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.
+from __future__ import annotations
+
+from functools import lru_cache
+import collections
+import enum
+import os
+import re
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .linkers import StaticLinker
+ from .compilers import Compiler
+
+# execinfo is a compiler lib on BSD
+UNIXY_COMPILER_INTERNAL_LIBS = ['m', 'c', 'pthread', 'dl', 'rt', 'execinfo'] # type: T.List[str]
+
+
+class Dedup(enum.Enum):
+
+ """What kind of deduplication can be done to compiler args.
+
+ OVERRIDDEN - Whether an argument can be 'overridden' by a later argument.
+ For example, -DFOO defines FOO and -UFOO undefines FOO. In this case,
+ we can safely remove the previous occurrence and add a new one. The
+ same is true for include paths and library paths with -I and -L.
+ UNIQUE - Arguments that once specified cannot be undone, such as `-c` or
+ `-pipe`. New instances of these can be completely skipped.
+ NO_DEDUP - Whether it matters where or how many times on the command-line
+ a particular argument is present. This can matter for symbol
+ resolution in static or shared libraries, so we cannot de-dup or
+ reorder them.
+ """
+
+ NO_DEDUP = 0
+ UNIQUE = 1
+ OVERRIDDEN = 2
+
+
+class CompilerArgs(T.MutableSequence[str]):
+ '''
+ List-like class that manages a list of compiler arguments. Should be used
+ while constructing compiler arguments from various sources. Can be
+ operated with ordinary lists, so this does not need to be used
+ everywhere.
+
+ All arguments must be inserted and stored in GCC-style (-lfoo, -Idir, etc)
+ and can converted to the native type of each compiler by using the
+ .to_native() method to which you must pass an instance of the compiler or
+ the compiler class.
+
+ New arguments added to this class (either with .append(), .extend(), or +=)
+ are added in a way that ensures that they override previous arguments.
+ For example:
+
+ >>> a = ['-Lfoo', '-lbar']
+ >>> a += ['-Lpho', '-lbaz']
+ >>> print(a)
+ ['-Lpho', '-Lfoo', '-lbar', '-lbaz']
+
+ Arguments will also be de-duped if they can be de-duped safely.
+
+ Note that because of all this, this class is not commutative and does not
+ preserve the order of arguments if it is safe to not. For example:
+ >>> ['-Ifoo', '-Ibar'] + ['-Ifez', '-Ibaz', '-Werror']
+ ['-Ifez', '-Ibaz', '-Ifoo', '-Ibar', '-Werror']
+ >>> ['-Ifez', '-Ibaz', '-Werror'] + ['-Ifoo', '-Ibar']
+ ['-Ifoo', '-Ibar', '-Ifez', '-Ibaz', '-Werror']
+
+ '''
+ # Arg prefixes that override by prepending instead of appending
+ prepend_prefixes = () # type: T.Tuple[str, ...]
+
+ # Arg prefixes and args that must be de-duped by returning 2
+ dedup2_prefixes = () # type: T.Tuple[str, ...]
+ dedup2_suffixes = () # type: T.Tuple[str, ...]
+ dedup2_args = () # type: T.Tuple[str, ...]
+
+ # Arg prefixes and args that must be de-duped by returning 1
+ #
+ # NOTE: not thorough. A list of potential corner cases can be found in
+ # https://github.com/mesonbuild/meson/pull/4593#pullrequestreview-182016038
+ dedup1_prefixes = () # type: T.Tuple[str, ...]
+ dedup1_suffixes = ('.lib', '.dll', '.so', '.dylib', '.a') # type: T.Tuple[str, ...]
+ # Match a .so of the form path/to/libfoo.so.0.1.0
+ # Only UNIX shared libraries require this. Others have a fixed extension.
+ dedup1_regex = re.compile(r'([\/\\]|\A)lib.*\.so(\.[0-9]+)?(\.[0-9]+)?(\.[0-9]+)?$')
+ dedup1_args = () # type: T.Tuple[str, ...]
+ # In generate_link() we add external libs without de-dup, but we must
+ # *always* de-dup these because they're special arguments to the linker
+ # TODO: these should probably move too
+ always_dedup_args = tuple('-l' + lib for lib in UNIXY_COMPILER_INTERNAL_LIBS) # type : T.Tuple[str, ...]
+
+ def __init__(self, compiler: T.Union['Compiler', 'StaticLinker'],
+ iterable: T.Optional[T.Iterable[str]] = None):
+ self.compiler = compiler
+ self._container = list(iterable) if iterable is not None else [] # type: T.List[str]
+ self.pre = collections.deque() # type: T.Deque[str]
+ self.post = collections.deque() # type: T.Deque[str]
+
+ # Flush the saved pre and post list into the _container list
+ #
+ # This correctly deduplicates the entries after _can_dedup definition
+ # Note: This function is designed to work without delete operations, as deletions are worsening the performance a lot.
+ def flush_pre_post(self) -> None:
+ new = [] # type: T.List[str]
+ pre_flush_set = set() # type: T.Set[str]
+ post_flush = collections.deque() # type: T.Deque[str]
+ post_flush_set = set() # type: T.Set[str]
+
+ #The two lists are here walked from the front to the back, in order to not need removals for deduplication
+ for a in self.pre:
+ dedup = self._can_dedup(a)
+ if a not in pre_flush_set:
+ new.append(a)
+ if dedup is Dedup.OVERRIDDEN:
+ pre_flush_set.add(a)
+ for a in reversed(self.post):
+ dedup = self._can_dedup(a)
+ if a not in post_flush_set:
+ post_flush.appendleft(a)
+ if dedup is Dedup.OVERRIDDEN:
+ post_flush_set.add(a)
+
+ #pre and post will overwrite every element that is in the container
+ #only copy over args that are in _container but not in the post flush or pre flush set
+ if pre_flush_set or post_flush_set:
+ for a in self._container:
+ if a not in post_flush_set and a not in pre_flush_set:
+ new.append(a)
+ else:
+ new.extend(self._container)
+ new.extend(post_flush)
+
+ self._container = new
+ self.pre.clear()
+ self.post.clear()
+
+ def __iter__(self) -> T.Iterator[str]:
+ self.flush_pre_post()
+ return iter(self._container)
+
+ @T.overload # noqa: F811
+ def __getitem__(self, index: int) -> str: # noqa: F811
+ pass
+
+ @T.overload # noqa: F811
+ def __getitem__(self, index: slice) -> T.MutableSequence[str]: # noqa: F811
+ pass
+
+ def __getitem__(self, index: T.Union[int, slice]) -> T.Union[str, T.MutableSequence[str]]: # noqa: F811
+ self.flush_pre_post()
+ return self._container[index]
+
+ @T.overload # noqa: F811
+ def __setitem__(self, index: int, value: str) -> None: # noqa: F811
+ pass
+
+ @T.overload # noqa: F811
+ def __setitem__(self, index: slice, value: T.Iterable[str]) -> None: # noqa: F811
+ pass
+
+ def __setitem__(self, index: T.Union[int, slice], value: T.Union[str, T.Iterable[str]]) -> None: # noqa: F811
+ self.flush_pre_post()
+ self._container[index] = value # type: ignore # TODO: fix 'Invalid index type' and 'Incompatible types in assignment' errors
+
+ def __delitem__(self, index: T.Union[int, slice]) -> None:
+ self.flush_pre_post()
+ del self._container[index]
+
+ def __len__(self) -> int:
+ return len(self._container) + len(self.pre) + len(self.post)
+
+ def insert(self, index: int, value: str) -> None:
+ self.flush_pre_post()
+ self._container.insert(index, value)
+
+ def copy(self) -> 'CompilerArgs':
+ self.flush_pre_post()
+ return type(self)(self.compiler, self._container.copy())
+
+ @classmethod
+ @lru_cache(maxsize=None)
+ def _can_dedup(cls, arg: str) -> Dedup:
+ """Returns whether the argument can be safely de-duped.
+
+ In addition to these, we handle library arguments specially.
+ With GNU ld, we surround library arguments with -Wl,--start/end-gr -> Dedupoup
+ to recursively search for symbols in the libraries. This is not needed
+ with other linkers.
+ """
+
+ # A standalone argument must never be deduplicated because it is
+ # defined by what comes _after_ it. Thus dedupping this:
+ # -D FOO -D BAR
+ # would yield either
+ # -D FOO BAR
+ # or
+ # FOO -D BAR
+ # both of which are invalid.
+ if arg in cls.dedup2_prefixes:
+ return Dedup.NO_DEDUP
+ if arg in cls.dedup2_args or \
+ arg.startswith(cls.dedup2_prefixes) or \
+ arg.endswith(cls.dedup2_suffixes):
+ return Dedup.OVERRIDDEN
+ if arg in cls.dedup1_args or \
+ arg.startswith(cls.dedup1_prefixes) or \
+ arg.endswith(cls.dedup1_suffixes) or \
+ re.search(cls.dedup1_regex, arg):
+ return Dedup.UNIQUE
+ return Dedup.NO_DEDUP
+
+ @classmethod
+ @lru_cache(maxsize=None)
+ def _should_prepend(cls, arg: str) -> bool:
+ return arg.startswith(cls.prepend_prefixes)
+
+ def to_native(self, copy: bool = False) -> T.List[str]:
+ # Check if we need to add --start/end-group for circular dependencies
+ # between static libraries, and for recursively searching for symbols
+ # needed by static libraries that are provided by object files or
+ # shared libraries.
+ self.flush_pre_post()
+ if copy:
+ new = self.copy()
+ else:
+ new = self
+ return self.compiler.unix_args_to_native(new._container)
+
+ def append_direct(self, arg: str) -> None:
+ '''
+ Append the specified argument without any reordering or de-dup except
+ for absolute paths to libraries, etc, which can always be de-duped
+ safely.
+ '''
+ self.flush_pre_post()
+ if os.path.isabs(arg):
+ self.append(arg)
+ else:
+ self._container.append(arg)
+
+ def extend_direct(self, iterable: T.Iterable[str]) -> None:
+ '''
+ Extend using the elements in the specified iterable without any
+ reordering or de-dup except for absolute paths where the order of
+ include search directories is not relevant
+ '''
+ self.flush_pre_post()
+ for elem in iterable:
+ self.append_direct(elem)
+
+ def extend_preserving_lflags(self, iterable: T.Iterable[str]) -> None:
+ normal_flags = []
+ lflags = []
+ for i in iterable:
+ if i not in self.always_dedup_args and (i.startswith('-l') or i.startswith('-L')):
+ lflags.append(i)
+ else:
+ normal_flags.append(i)
+ self.extend(normal_flags)
+ self.extend_direct(lflags)
+
+ def __add__(self, args: T.Iterable[str]) -> 'CompilerArgs':
+ self.flush_pre_post()
+ new = self.copy()
+ new += args
+ return new
+
+ def __iadd__(self, args: T.Iterable[str]) -> 'CompilerArgs':
+ '''
+ Add two CompilerArgs while taking into account overriding of arguments
+ and while preserving the order of arguments as much as possible
+ '''
+ tmp_pre = collections.deque() # type: T.Deque[str]
+ if not isinstance(args, collections.abc.Iterable):
+ raise TypeError(f'can only concatenate Iterable[str] (not "{args}") to CompilerArgs')
+ for arg in args:
+ # If the argument can be de-duped, do it either by removing the
+ # previous occurrence of it and adding a new one, or not adding the
+ # new occurrence.
+ dedup = self._can_dedup(arg)
+ if dedup is Dedup.UNIQUE:
+ # Argument already exists and adding a new instance is useless
+ if arg in self._container or arg in self.pre or arg in self.post:
+ continue
+ if self._should_prepend(arg):
+ tmp_pre.appendleft(arg)
+ else:
+ self.post.append(arg)
+ self.pre.extendleft(tmp_pre)
+ #pre and post is going to be merged later before a iter call
+ return self
+
+ def __radd__(self, args: T.Iterable[str]) -> 'CompilerArgs':
+ self.flush_pre_post()
+ new = type(self)(self.compiler, args)
+ new += self
+ return new
+
+ def __eq__(self, other: object) -> T.Union[bool]:
+ self.flush_pre_post()
+ # Only allow equality checks against other CompilerArgs and lists instances
+ if isinstance(other, CompilerArgs):
+ return self.compiler == other.compiler and self._container == other._container
+ elif isinstance(other, list):
+ return self._container == other
+ return NotImplemented
+
+ def append(self, arg: str) -> None:
+ self += [arg]
+
+ def extend(self, args: T.Iterable[str]) -> None:
+ self += args
+
+ def __repr__(self) -> str:
+ self.flush_pre_post()
+ return f'CompilerArgs({self.compiler!r}, {self._container!r})'
diff --git a/mesonbuild/ast/__init__.py b/mesonbuild/ast/__init__.py
new file mode 100644
index 0000000..d14620f
--- /dev/null
+++ b/mesonbuild/ast/__init__.py
@@ -0,0 +1,34 @@
+# Copyright 2019 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool.
+
+__all__ = [
+ 'AstConditionLevel',
+ 'AstInterpreter',
+ 'AstIDGenerator',
+ 'AstIndentationGenerator',
+ 'AstJSONPrinter',
+ 'AstVisitor',
+ 'AstPrinter',
+ 'IntrospectionInterpreter',
+ 'BUILD_TARGET_FUNCTIONS',
+]
+
+from .interpreter import AstInterpreter
+from .introspection import IntrospectionInterpreter, BUILD_TARGET_FUNCTIONS
+from .visitor import AstVisitor
+from .postprocess import AstConditionLevel, AstIDGenerator, AstIndentationGenerator
+from .printer import AstPrinter, AstJSONPrinter
diff --git a/mesonbuild/ast/interpreter.py b/mesonbuild/ast/interpreter.py
new file mode 100644
index 0000000..7484e04
--- /dev/null
+++ b/mesonbuild/ast/interpreter.py
@@ -0,0 +1,447 @@
+# Copyright 2016 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool.
+from __future__ import annotations
+
+import os
+import sys
+import typing as T
+
+from .. import mparser, mesonlib
+from .. import environment
+
+from ..interpreterbase import (
+ MesonInterpreterObject,
+ InterpreterBase,
+ InvalidArguments,
+ BreakRequest,
+ ContinueRequest,
+ default_resolve_key,
+)
+
+from ..interpreter import (
+ StringHolder,
+ BooleanHolder,
+ IntegerHolder,
+ ArrayHolder,
+ DictHolder,
+)
+
+from ..mparser import (
+ ArgumentNode,
+ ArithmeticNode,
+ ArrayNode,
+ AssignmentNode,
+ BaseNode,
+ ElementaryNode,
+ EmptyNode,
+ IdNode,
+ MethodNode,
+ NotNode,
+ PlusAssignmentNode,
+ TernaryNode,
+)
+
+if T.TYPE_CHECKING:
+ from .visitor import AstVisitor
+ from ..interpreter import Interpreter
+ from ..interpreterbase import TYPE_nkwargs, TYPE_nvar
+ from ..mparser import (
+ AndNode,
+ ComparisonNode,
+ ForeachClauseNode,
+ IfClauseNode,
+ IndexNode,
+ OrNode,
+ UMinusNode,
+ )
+
+class DontCareObject(MesonInterpreterObject):
+ pass
+
+class MockExecutable(MesonInterpreterObject):
+ pass
+
+class MockStaticLibrary(MesonInterpreterObject):
+ pass
+
+class MockSharedLibrary(MesonInterpreterObject):
+ pass
+
+class MockCustomTarget(MesonInterpreterObject):
+ pass
+
+class MockRunTarget(MesonInterpreterObject):
+ pass
+
+ADD_SOURCE = 0
+REMOVE_SOURCE = 1
+
+_T = T.TypeVar('_T')
+_V = T.TypeVar('_V')
+
+class AstInterpreter(InterpreterBase):
+ def __init__(self, source_root: str, subdir: str, subproject: str, visitors: T.Optional[T.List[AstVisitor]] = None):
+ super().__init__(source_root, subdir, subproject)
+ self.visitors = visitors if visitors is not None else []
+ self.processed_buildfiles = set() # type: T.Set[str]
+ self.assignments = {} # type: T.Dict[str, BaseNode]
+ self.assign_vals = {} # type: T.Dict[str, T.Any]
+ self.reverse_assignment = {} # type: T.Dict[str, BaseNode]
+ self.funcs.update({'project': self.func_do_nothing,
+ 'test': self.func_do_nothing,
+ 'benchmark': self.func_do_nothing,
+ 'install_headers': self.func_do_nothing,
+ 'install_man': self.func_do_nothing,
+ 'install_data': self.func_do_nothing,
+ 'install_subdir': self.func_do_nothing,
+ 'install_symlink': self.func_do_nothing,
+ 'install_emptydir': self.func_do_nothing,
+ 'configuration_data': self.func_do_nothing,
+ 'configure_file': self.func_do_nothing,
+ 'find_program': self.func_do_nothing,
+ 'include_directories': self.func_do_nothing,
+ 'add_global_arguments': self.func_do_nothing,
+ 'add_global_link_arguments': self.func_do_nothing,
+ 'add_project_arguments': self.func_do_nothing,
+ 'add_project_dependencies': self.func_do_nothing,
+ 'add_project_link_arguments': self.func_do_nothing,
+ 'message': self.func_do_nothing,
+ 'generator': self.func_do_nothing,
+ 'error': self.func_do_nothing,
+ 'run_command': self.func_do_nothing,
+ 'assert': self.func_do_nothing,
+ 'subproject': self.func_do_nothing,
+ 'dependency': self.func_do_nothing,
+ 'get_option': self.func_do_nothing,
+ 'join_paths': self.func_do_nothing,
+ 'environment': self.func_do_nothing,
+ 'import': self.func_do_nothing,
+ 'vcs_tag': self.func_do_nothing,
+ 'add_languages': self.func_do_nothing,
+ 'declare_dependency': self.func_do_nothing,
+ 'files': self.func_do_nothing,
+ 'executable': self.func_do_nothing,
+ 'static_library': self.func_do_nothing,
+ 'shared_library': self.func_do_nothing,
+ 'library': self.func_do_nothing,
+ 'build_target': self.func_do_nothing,
+ 'custom_target': self.func_do_nothing,
+ 'run_target': self.func_do_nothing,
+ 'subdir': self.func_subdir,
+ 'set_variable': self.func_do_nothing,
+ 'get_variable': self.func_do_nothing,
+ 'unset_variable': self.func_do_nothing,
+ 'is_disabler': self.func_do_nothing,
+ 'is_variable': self.func_do_nothing,
+ 'disabler': self.func_do_nothing,
+ 'gettext': self.func_do_nothing,
+ 'jar': self.func_do_nothing,
+ 'warning': self.func_do_nothing,
+ 'shared_module': self.func_do_nothing,
+ 'option': self.func_do_nothing,
+ 'both_libraries': self.func_do_nothing,
+ 'add_test_setup': self.func_do_nothing,
+ 'find_library': self.func_do_nothing,
+ 'subdir_done': self.func_do_nothing,
+ 'alias_target': self.func_do_nothing,
+ 'summary': self.func_do_nothing,
+ 'range': self.func_do_nothing,
+ 'structured_sources': self.func_do_nothing,
+ 'debug': self.func_do_nothing,
+ })
+
+ def _unholder_args(self, args: _T, kwargs: _V) -> T.Tuple[_T, _V]:
+ return args, kwargs
+
+ def _holderify(self, res: _T) -> _T:
+ return res
+
+ def func_do_nothing(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> bool:
+ return True
+
+ def load_root_meson_file(self) -> None:
+ super().load_root_meson_file()
+ for i in self.visitors:
+ self.ast.accept(i)
+
+ def func_subdir(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None:
+ args = self.flatten_args(args)
+ if len(args) != 1 or not isinstance(args[0], str):
+ sys.stderr.write(f'Unable to evaluate subdir({args}) in AstInterpreter --> Skipping\n')
+ return
+
+ prev_subdir = self.subdir
+ subdir = os.path.join(prev_subdir, args[0])
+ absdir = os.path.join(self.source_root, subdir)
+ buildfilename = os.path.join(subdir, environment.build_filename)
+ absname = os.path.join(self.source_root, buildfilename)
+ symlinkless_dir = os.path.realpath(absdir)
+ build_file = os.path.join(symlinkless_dir, 'meson.build')
+ if build_file in self.processed_buildfiles:
+ sys.stderr.write('Trying to enter {} which has already been visited --> Skipping\n'.format(args[0]))
+ return
+ self.processed_buildfiles.add(build_file)
+
+ if not os.path.isfile(absname):
+ sys.stderr.write(f'Unable to find build file {buildfilename} --> Skipping\n')
+ return
+ with open(absname, encoding='utf-8') as f:
+ code = f.read()
+ assert isinstance(code, str)
+ try:
+ codeblock = mparser.Parser(code, absname).parse()
+ except mesonlib.MesonException as me:
+ me.file = absname
+ raise me
+
+ self.subdir = subdir
+ for i in self.visitors:
+ codeblock.accept(i)
+ self.evaluate_codeblock(codeblock)
+ self.subdir = prev_subdir
+
+ def method_call(self, node: BaseNode) -> bool:
+ return True
+
+ def evaluate_fstring(self, node: mparser.FormatStringNode) -> str:
+ assert isinstance(node, mparser.FormatStringNode)
+ return node.value
+
+ def evaluate_arraystatement(self, cur: mparser.ArrayNode) -> TYPE_nvar:
+ return self.reduce_arguments(cur.args)[0]
+
+ def evaluate_arithmeticstatement(self, cur: ArithmeticNode) -> int:
+ self.evaluate_statement(cur.left)
+ self.evaluate_statement(cur.right)
+ return 0
+
+ def evaluate_uminusstatement(self, cur: UMinusNode) -> int:
+ self.evaluate_statement(cur.value)
+ return 0
+
+ def evaluate_ternary(self, node: TernaryNode) -> None:
+ assert isinstance(node, TernaryNode)
+ self.evaluate_statement(node.condition)
+ self.evaluate_statement(node.trueblock)
+ self.evaluate_statement(node.falseblock)
+
+ def evaluate_dictstatement(self, node: mparser.DictNode) -> TYPE_nkwargs:
+ def resolve_key(node: mparser.BaseNode) -> str:
+ if isinstance(node, mparser.StringNode):
+ return node.value
+ return '__AST_UNKNOWN__'
+ arguments, kwargs = self.reduce_arguments(node.args, key_resolver=resolve_key)
+ assert not arguments
+ self.argument_depth += 1
+ for key, value in kwargs.items():
+ if isinstance(key, BaseNode):
+ self.evaluate_statement(key)
+ self.argument_depth -= 1
+ return {}
+
+ def evaluate_plusassign(self, node: PlusAssignmentNode) -> None:
+ assert isinstance(node, PlusAssignmentNode)
+ # Cheat by doing a reassignment
+ self.assignments[node.var_name] = node.value # Save a reference to the value node
+ if node.value.ast_id:
+ self.reverse_assignment[node.value.ast_id] = node
+ self.assign_vals[node.var_name] = self.evaluate_statement(node.value)
+
+ def evaluate_indexing(self, node: IndexNode) -> int:
+ return 0
+
+ def unknown_function_called(self, func_name: str) -> None:
+ pass
+
+ def reduce_arguments(
+ self,
+ args: mparser.ArgumentNode,
+ key_resolver: T.Callable[[mparser.BaseNode], str] = default_resolve_key,
+ duplicate_key_error: T.Optional[str] = None,
+ ) -> T.Tuple[T.List[TYPE_nvar], TYPE_nkwargs]:
+ if isinstance(args, ArgumentNode):
+ kwargs = {} # type: T.Dict[str, TYPE_nvar]
+ for key, val in args.kwargs.items():
+ kwargs[key_resolver(key)] = val
+ if args.incorrect_order():
+ raise InvalidArguments('All keyword arguments must be after positional arguments.')
+ return self.flatten_args(args.arguments), kwargs
+ else:
+ return self.flatten_args(args), {}
+
+ def evaluate_comparison(self, node: ComparisonNode) -> bool:
+ self.evaluate_statement(node.left)
+ self.evaluate_statement(node.right)
+ return False
+
+ def evaluate_andstatement(self, cur: AndNode) -> bool:
+ self.evaluate_statement(cur.left)
+ self.evaluate_statement(cur.right)
+ return False
+
+ def evaluate_orstatement(self, cur: OrNode) -> bool:
+ self.evaluate_statement(cur.left)
+ self.evaluate_statement(cur.right)
+ return False
+
+ def evaluate_notstatement(self, cur: NotNode) -> bool:
+ self.evaluate_statement(cur.value)
+ return False
+
+ def evaluate_foreach(self, node: ForeachClauseNode) -> None:
+ try:
+ self.evaluate_codeblock(node.block)
+ except ContinueRequest:
+ pass
+ except BreakRequest:
+ pass
+
+ def evaluate_if(self, node: IfClauseNode) -> None:
+ for i in node.ifs:
+ self.evaluate_codeblock(i.block)
+ if not isinstance(node.elseblock, EmptyNode):
+ self.evaluate_codeblock(node.elseblock)
+
+ def get_variable(self, varname: str) -> int:
+ return 0
+
+ def assignment(self, node: AssignmentNode) -> None:
+ assert isinstance(node, AssignmentNode)
+ self.assignments[node.var_name] = node.value # Save a reference to the value node
+ if node.value.ast_id:
+ self.reverse_assignment[node.value.ast_id] = node
+ self.assign_vals[node.var_name] = self.evaluate_statement(node.value) # Evaluate the value just in case
+
+ def resolve_node(self, node: BaseNode, include_unknown_args: bool = False, id_loop_detect: T.Optional[T.List[str]] = None) -> T.Optional[T.Any]:
+ def quick_resolve(n: BaseNode, loop_detect: T.Optional[T.List[str]] = None) -> T.Any:
+ if loop_detect is None:
+ loop_detect = []
+ if isinstance(n, IdNode):
+ assert isinstance(n.value, str)
+ if n.value in loop_detect or n.value not in self.assignments:
+ return []
+ return quick_resolve(self.assignments[n.value], loop_detect = loop_detect + [n.value])
+ elif isinstance(n, ElementaryNode):
+ return n.value
+ else:
+ return n
+
+ if id_loop_detect is None:
+ id_loop_detect = []
+ result = None
+
+ if not isinstance(node, BaseNode):
+ return None
+
+ assert node.ast_id
+ if node.ast_id in id_loop_detect:
+ return None # Loop detected
+ id_loop_detect += [node.ast_id]
+
+ # Try to evealuate the value of the node
+ if isinstance(node, IdNode):
+ result = quick_resolve(node)
+
+ elif isinstance(node, ElementaryNode):
+ result = node.value
+
+ elif isinstance(node, NotNode):
+ result = self.resolve_node(node.value, include_unknown_args, id_loop_detect)
+ if isinstance(result, bool):
+ result = not result
+
+ elif isinstance(node, ArrayNode):
+ result = node.args.arguments.copy()
+
+ elif isinstance(node, ArgumentNode):
+ result = node.arguments.copy()
+
+ elif isinstance(node, ArithmeticNode):
+ if node.operation != 'add':
+ return None # Only handle string and array concats
+ l = quick_resolve(node.left)
+ r = quick_resolve(node.right)
+ if isinstance(l, str) and isinstance(r, str):
+ result = l + r # String concatenation detected
+ else:
+ result = self.flatten_args(l, include_unknown_args, id_loop_detect) + self.flatten_args(r, include_unknown_args, id_loop_detect)
+
+ elif isinstance(node, MethodNode):
+ src = quick_resolve(node.source_object)
+ margs = self.flatten_args(node.args.arguments, include_unknown_args, id_loop_detect)
+ mkwargs = {} # type: T.Dict[str, TYPE_nvar]
+ try:
+ if isinstance(src, str):
+ result = StringHolder(src, T.cast('Interpreter', self)).method_call(node.name, margs, mkwargs)
+ elif isinstance(src, bool):
+ result = BooleanHolder(src, T.cast('Interpreter', self)).method_call(node.name, margs, mkwargs)
+ elif isinstance(src, int):
+ result = IntegerHolder(src, T.cast('Interpreter', self)).method_call(node.name, margs, mkwargs)
+ elif isinstance(src, list):
+ result = ArrayHolder(src, T.cast('Interpreter', self)).method_call(node.name, margs, mkwargs)
+ elif isinstance(src, dict):
+ result = DictHolder(src, T.cast('Interpreter', self)).method_call(node.name, margs, mkwargs)
+ except mesonlib.MesonException:
+ return None
+
+ # Ensure that the result is fully resolved (no more nodes)
+ if isinstance(result, BaseNode):
+ result = self.resolve_node(result, include_unknown_args, id_loop_detect)
+ elif isinstance(result, list):
+ new_res = [] # type: T.List[TYPE_nvar]
+ for i in result:
+ if isinstance(i, BaseNode):
+ resolved = self.resolve_node(i, include_unknown_args, id_loop_detect)
+ if resolved is not None:
+ new_res += self.flatten_args(resolved, include_unknown_args, id_loop_detect)
+ else:
+ new_res += [i]
+ result = new_res
+
+ return result
+
+ def flatten_args(self, args_raw: T.Union[TYPE_nvar, T.Sequence[TYPE_nvar]], include_unknown_args: bool = False, id_loop_detect: T.Optional[T.List[str]] = None) -> T.List[TYPE_nvar]:
+ # Make sure we are always dealing with lists
+ if isinstance(args_raw, list):
+ args = args_raw
+ else:
+ args = [args_raw]
+
+ flattend_args = [] # type: T.List[TYPE_nvar]
+
+ # Resolve the contents of args
+ for i in args:
+ if isinstance(i, BaseNode):
+ resolved = self.resolve_node(i, include_unknown_args, id_loop_detect)
+ if resolved is not None:
+ if not isinstance(resolved, list):
+ resolved = [resolved]
+ flattend_args += resolved
+ elif isinstance(i, (str, bool, int, float)) or include_unknown_args:
+ flattend_args += [i]
+ return flattend_args
+
+ def flatten_kwargs(self, kwargs: T.Dict[str, TYPE_nvar], include_unknown_args: bool = False) -> T.Dict[str, TYPE_nvar]:
+ flattend_kwargs = {}
+ for key, val in kwargs.items():
+ if isinstance(val, BaseNode):
+ resolved = self.resolve_node(val, include_unknown_args)
+ if resolved is not None:
+ flattend_kwargs[key] = resolved
+ elif isinstance(val, (str, bool, int, float)) or include_unknown_args:
+ flattend_kwargs[key] = val
+ return flattend_kwargs
diff --git a/mesonbuild/ast/introspection.py b/mesonbuild/ast/introspection.py
new file mode 100644
index 0000000..194c15b
--- /dev/null
+++ b/mesonbuild/ast/introspection.py
@@ -0,0 +1,362 @@
+# Copyright 2018 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool
+
+from __future__ import annotations
+import argparse
+import copy
+import os
+import typing as T
+
+from .. import compilers, environment, mesonlib, optinterpreter
+from .. import coredata as cdata
+from ..build import Executable, Jar, SharedLibrary, SharedModule, StaticLibrary
+from ..compilers import detect_compiler_for
+from ..interpreterbase import InvalidArguments
+from ..mesonlib import MachineChoice, OptionKey
+from ..mparser import BaseNode, ArithmeticNode, ArrayNode, ElementaryNode, IdNode, FunctionNode, StringNode
+from .interpreter import AstInterpreter
+
+if T.TYPE_CHECKING:
+ from ..build import BuildTarget
+ from ..interpreterbase import TYPE_nvar
+ from .visitor import AstVisitor
+
+
+# TODO: it would be nice to not have to duplicate this
+BUILD_TARGET_FUNCTIONS = [
+ 'executable', 'jar', 'library', 'shared_library', 'shared_module',
+ 'static_library', 'both_libraries'
+]
+
+class IntrospectionHelper(argparse.Namespace):
+ # mimic an argparse namespace
+ def __init__(self, cross_file: str):
+ super().__init__()
+ self.cross_file = cross_file # type: str
+ self.native_file = None # type: str
+ self.cmd_line_options = {} # type: T.Dict[str, str]
+
+ def __eq__(self, other: object) -> bool:
+ return NotImplemented
+
+class IntrospectionInterpreter(AstInterpreter):
+ # Interpreter to detect the options without a build directory
+ # Most of the code is stolen from interpreter.Interpreter
+ def __init__(self,
+ source_root: str,
+ subdir: str,
+ backend: str,
+ visitors: T.Optional[T.List[AstVisitor]] = None,
+ cross_file: T.Optional[str] = None,
+ subproject: str = '',
+ subproject_dir: str = 'subprojects',
+ env: T.Optional[environment.Environment] = None):
+ visitors = visitors if visitors is not None else []
+ super().__init__(source_root, subdir, subproject, visitors=visitors)
+
+ options = IntrospectionHelper(cross_file)
+ self.cross_file = cross_file
+ if env is None:
+ self.environment = environment.Environment(source_root, None, options)
+ else:
+ self.environment = env
+ self.subproject_dir = subproject_dir
+ self.coredata = self.environment.get_coredata()
+ self.option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt')
+ self.backend = backend
+ self.default_options = {OptionKey('backend'): self.backend}
+ self.project_data = {} # type: T.Dict[str, T.Any]
+ self.targets = [] # type: T.List[T.Dict[str, T.Any]]
+ self.dependencies = [] # type: T.List[T.Dict[str, T.Any]]
+ self.project_node = None # type: BaseNode
+
+ self.funcs.update({
+ 'add_languages': self.func_add_languages,
+ 'dependency': self.func_dependency,
+ 'executable': self.func_executable,
+ 'jar': self.func_jar,
+ 'library': self.func_library,
+ 'project': self.func_project,
+ 'shared_library': self.func_shared_lib,
+ 'shared_module': self.func_shared_module,
+ 'static_library': self.func_static_lib,
+ 'both_libraries': self.func_both_lib,
+ })
+
+ def func_project(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None:
+ if self.project_node:
+ raise InvalidArguments('Second call to project()')
+ self.project_node = node
+ if len(args) < 1:
+ raise InvalidArguments('Not enough arguments to project(). Needs at least the project name.')
+
+ proj_name = args[0]
+ proj_vers = kwargs.get('version', 'undefined')
+ proj_langs = self.flatten_args(args[1:])
+ if isinstance(proj_vers, ElementaryNode):
+ proj_vers = proj_vers.value
+ if not isinstance(proj_vers, str):
+ proj_vers = 'undefined'
+ self.project_data = {'descriptive_name': proj_name, 'version': proj_vers}
+
+ if os.path.exists(self.option_file):
+ oi = optinterpreter.OptionInterpreter(self.subproject)
+ oi.process(self.option_file)
+ self.coredata.update_project_options(oi.options)
+
+ def_opts = self.flatten_args(kwargs.get('default_options', []))
+ _project_default_options = mesonlib.stringlistify(def_opts)
+ self.project_default_options = cdata.create_options_dict(_project_default_options, self.subproject)
+ self.default_options.update(self.project_default_options)
+ self.coredata.set_default_options(self.default_options, self.subproject, self.environment)
+
+ if not self.is_subproject() and 'subproject_dir' in kwargs:
+ spdirname = kwargs['subproject_dir']
+ if isinstance(spdirname, StringNode):
+ assert isinstance(spdirname.value, str)
+ self.subproject_dir = spdirname.value
+ if not self.is_subproject():
+ self.project_data['subprojects'] = []
+ subprojects_dir = os.path.join(self.source_root, self.subproject_dir)
+ if os.path.isdir(subprojects_dir):
+ for i in os.listdir(subprojects_dir):
+ if os.path.isdir(os.path.join(subprojects_dir, i)):
+ self.do_subproject(i)
+
+ self.coredata.init_backend_options(self.backend)
+ options = {k: v for k, v in self.environment.options.items() if k.is_backend()}
+
+ self.coredata.set_options(options)
+ self._add_languages(proj_langs, True, MachineChoice.HOST)
+ self._add_languages(proj_langs, True, MachineChoice.BUILD)
+
+ def do_subproject(self, dirname: str) -> None:
+ subproject_dir_abs = os.path.join(self.environment.get_source_dir(), self.subproject_dir)
+ subpr = os.path.join(subproject_dir_abs, dirname)
+ try:
+ subi = IntrospectionInterpreter(subpr, '', self.backend, cross_file=self.cross_file, subproject=dirname, subproject_dir=self.subproject_dir, env=self.environment, visitors=self.visitors)
+ subi.analyze()
+ subi.project_data['name'] = dirname
+ self.project_data['subprojects'] += [subi.project_data]
+ except (mesonlib.MesonException, RuntimeError):
+ return
+
+ def func_add_languages(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None:
+ kwargs = self.flatten_kwargs(kwargs)
+ required = kwargs.get('required', True)
+ if isinstance(required, cdata.UserFeatureOption):
+ required = required.is_enabled()
+ if 'native' in kwargs:
+ native = kwargs.get('native', False)
+ self._add_languages(args, required, MachineChoice.BUILD if native else MachineChoice.HOST)
+ else:
+ for for_machine in [MachineChoice.BUILD, MachineChoice.HOST]:
+ self._add_languages(args, required, for_machine)
+
+ def _add_languages(self, raw_langs: T.List[TYPE_nvar], required: bool, for_machine: MachineChoice) -> None:
+ langs = [] # type: T.List[str]
+ for l in self.flatten_args(raw_langs):
+ if isinstance(l, str):
+ langs.append(l)
+ elif isinstance(l, StringNode):
+ langs.append(l.value)
+
+ for lang in sorted(langs, key=compilers.sort_clink):
+ lang = lang.lower()
+ if lang not in self.coredata.compilers[for_machine]:
+ try:
+ comp = detect_compiler_for(self.environment, lang, for_machine)
+ except mesonlib.MesonException:
+ # do we even care about introspecting this language?
+ if required:
+ raise
+ else:
+ continue
+ if self.subproject:
+ options = {}
+ for k in comp.get_options():
+ v = copy.copy(self.coredata.options[k])
+ k = k.evolve(subproject=self.subproject)
+ options[k] = v
+ self.coredata.add_compiler_options(options, lang, for_machine, self.environment)
+
+ def func_dependency(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None:
+ args = self.flatten_args(args)
+ kwargs = self.flatten_kwargs(kwargs)
+ if not args:
+ return
+ name = args[0]
+ has_fallback = 'fallback' in kwargs
+ required = kwargs.get('required', True)
+ version = kwargs.get('version', [])
+ if not isinstance(version, list):
+ version = [version]
+ if isinstance(required, ElementaryNode):
+ required = required.value
+ if not isinstance(required, bool):
+ required = False
+ self.dependencies += [{
+ 'name': name,
+ 'required': required,
+ 'version': version,
+ 'has_fallback': has_fallback,
+ 'conditional': node.condition_level > 0,
+ 'node': node
+ }]
+
+ def build_target(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs_raw: T.Dict[str, TYPE_nvar], targetclass: T.Type[BuildTarget]) -> T.Optional[T.Dict[str, T.Any]]:
+ args = self.flatten_args(args)
+ if not args or not isinstance(args[0], str):
+ return None
+ name = args[0]
+ srcqueue = [node]
+ extra_queue = []
+
+ # Process the sources BEFORE flattening the kwargs, to preserve the original nodes
+ if 'sources' in kwargs_raw:
+ srcqueue += mesonlib.listify(kwargs_raw['sources'])
+
+ if 'extra_files' in kwargs_raw:
+ extra_queue += mesonlib.listify(kwargs_raw['extra_files'])
+
+ kwargs = self.flatten_kwargs(kwargs_raw, True)
+
+ def traverse_nodes(inqueue: T.List[BaseNode]) -> T.List[BaseNode]:
+ res = [] # type: T.List[BaseNode]
+ while inqueue:
+ curr = inqueue.pop(0)
+ arg_node = None
+ assert isinstance(curr, BaseNode)
+ if isinstance(curr, FunctionNode):
+ arg_node = curr.args
+ elif isinstance(curr, ArrayNode):
+ arg_node = curr.args
+ elif isinstance(curr, IdNode):
+ # Try to resolve the ID and append the node to the queue
+ assert isinstance(curr.value, str)
+ var_name = curr.value
+ if var_name in self.assignments:
+ tmp_node = self.assignments[var_name]
+ if isinstance(tmp_node, (ArrayNode, IdNode, FunctionNode)):
+ inqueue += [tmp_node]
+ elif isinstance(curr, ArithmeticNode):
+ inqueue += [curr.left, curr.right]
+ if arg_node is None:
+ continue
+ arg_nodes = arg_node.arguments.copy()
+ # Pop the first element if the function is a build target function
+ if isinstance(curr, FunctionNode) and curr.func_name in BUILD_TARGET_FUNCTIONS:
+ arg_nodes.pop(0)
+ elemetary_nodes = [x for x in arg_nodes if isinstance(x, (str, StringNode))]
+ inqueue += [x for x in arg_nodes if isinstance(x, (FunctionNode, ArrayNode, IdNode, ArithmeticNode))]
+ if elemetary_nodes:
+ res += [curr]
+ return res
+
+ source_nodes = traverse_nodes(srcqueue)
+ extraf_nodes = traverse_nodes(extra_queue)
+
+ # Make sure nothing can crash when creating the build class
+ kwargs_reduced = {k: v for k, v in kwargs.items() if k in targetclass.known_kwargs and k in {'install', 'build_by_default', 'build_always'}}
+ kwargs_reduced = {k: v.value if isinstance(v, ElementaryNode) else v for k, v in kwargs_reduced.items()}
+ kwargs_reduced = {k: v for k, v in kwargs_reduced.items() if not isinstance(v, BaseNode)}
+ for_machine = MachineChoice.HOST
+ objects = [] # type: T.List[T.Any]
+ empty_sources = [] # type: T.List[T.Any]
+ # Passing the unresolved sources list causes errors
+ target = targetclass(name, self.subdir, self.subproject, for_machine, empty_sources, [], objects,
+ self.environment, self.coredata.compilers[for_machine], kwargs_reduced)
+ target.process_compilers()
+ target.process_compilers_late([])
+
+ new_target = {
+ 'name': target.get_basename(),
+ 'id': target.get_id(),
+ 'type': target.get_typename(),
+ 'defined_in': os.path.normpath(os.path.join(self.source_root, self.subdir, environment.build_filename)),
+ 'subdir': self.subdir,
+ 'build_by_default': target.build_by_default,
+ 'installed': target.should_install(),
+ 'outputs': target.get_outputs(),
+ 'sources': source_nodes,
+ 'extra_files': extraf_nodes,
+ 'kwargs': kwargs,
+ 'node': node,
+ }
+
+ self.targets += [new_target]
+ return new_target
+
+ def build_library(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
+ default_library = self.coredata.get_option(OptionKey('default_library'))
+ if default_library == 'shared':
+ return self.build_target(node, args, kwargs, SharedLibrary)
+ elif default_library == 'static':
+ return self.build_target(node, args, kwargs, StaticLibrary)
+ elif default_library == 'both':
+ return self.build_target(node, args, kwargs, SharedLibrary)
+ return None
+
+ def func_executable(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
+ return self.build_target(node, args, kwargs, Executable)
+
+ def func_static_lib(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
+ return self.build_target(node, args, kwargs, StaticLibrary)
+
+ def func_shared_lib(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
+ return self.build_target(node, args, kwargs, SharedLibrary)
+
+ def func_both_lib(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
+ return self.build_target(node, args, kwargs, SharedLibrary)
+
+ def func_shared_module(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
+ return self.build_target(node, args, kwargs, SharedModule)
+
+ def func_library(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
+ return self.build_library(node, args, kwargs)
+
+ def func_jar(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
+ return self.build_target(node, args, kwargs, Jar)
+
+ def func_build_target(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
+ if 'target_type' not in kwargs:
+ return None
+ target_type = kwargs.pop('target_type')
+ if isinstance(target_type, ElementaryNode):
+ target_type = target_type.value
+ if target_type == 'executable':
+ return self.build_target(node, args, kwargs, Executable)
+ elif target_type == 'shared_library':
+ return self.build_target(node, args, kwargs, SharedLibrary)
+ elif target_type == 'static_library':
+ return self.build_target(node, args, kwargs, StaticLibrary)
+ elif target_type == 'both_libraries':
+ return self.build_target(node, args, kwargs, SharedLibrary)
+ elif target_type == 'library':
+ return self.build_library(node, args, kwargs)
+ elif target_type == 'jar':
+ return self.build_target(node, args, kwargs, Jar)
+ return None
+
+ def is_subproject(self) -> bool:
+ return self.subproject != ''
+
+ def analyze(self) -> None:
+ self.load_root_meson_file()
+ self.sanity_check_ast()
+ self.parse_project()
+ self.run()
diff --git a/mesonbuild/ast/postprocess.py b/mesonbuild/ast/postprocess.py
new file mode 100644
index 0000000..0c28af0
--- /dev/null
+++ b/mesonbuild/ast/postprocess.py
@@ -0,0 +1,120 @@
+# Copyright 2019 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool
+from __future__ import annotations
+
+from . import AstVisitor
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .. import mparser
+
+class AstIndentationGenerator(AstVisitor):
+ def __init__(self) -> None:
+ self.level = 0
+
+ def visit_default_func(self, node: mparser.BaseNode) -> None:
+ # Store the current level in the node
+ node.level = self.level
+
+ def visit_ArrayNode(self, node: mparser.ArrayNode) -> None:
+ self.visit_default_func(node)
+ self.level += 1
+ node.args.accept(self)
+ self.level -= 1
+
+ def visit_DictNode(self, node: mparser.DictNode) -> None:
+ self.visit_default_func(node)
+ self.level += 1
+ node.args.accept(self)
+ self.level -= 1
+
+ def visit_MethodNode(self, node: mparser.MethodNode) -> None:
+ self.visit_default_func(node)
+ node.source_object.accept(self)
+ self.level += 1
+ node.args.accept(self)
+ self.level -= 1
+
+ def visit_FunctionNode(self, node: mparser.FunctionNode) -> None:
+ self.visit_default_func(node)
+ self.level += 1
+ node.args.accept(self)
+ self.level -= 1
+
+ def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None:
+ self.visit_default_func(node)
+ self.level += 1
+ node.items.accept(self)
+ node.block.accept(self)
+ self.level -= 1
+
+ def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None:
+ self.visit_default_func(node)
+ for i in node.ifs:
+ i.accept(self)
+ if node.elseblock:
+ self.level += 1
+ node.elseblock.accept(self)
+ self.level -= 1
+
+ def visit_IfNode(self, node: mparser.IfNode) -> None:
+ self.visit_default_func(node)
+ self.level += 1
+ node.condition.accept(self)
+ node.block.accept(self)
+ self.level -= 1
+
+class AstIDGenerator(AstVisitor):
+ def __init__(self) -> None:
+ self.counter = {} # type: T.Dict[str, int]
+
+ def visit_default_func(self, node: mparser.BaseNode) -> None:
+ name = type(node).__name__
+ if name not in self.counter:
+ self.counter[name] = 0
+ node.ast_id = name + '#' + str(self.counter[name])
+ self.counter[name] += 1
+
+class AstConditionLevel(AstVisitor):
+ def __init__(self) -> None:
+ self.condition_level = 0
+
+ def visit_default_func(self, node: mparser.BaseNode) -> None:
+ node.condition_level = self.condition_level
+
+ def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None:
+ self.visit_default_func(node)
+ self.condition_level += 1
+ node.items.accept(self)
+ node.block.accept(self)
+ self.condition_level -= 1
+
+ def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None:
+ self.visit_default_func(node)
+ for i in node.ifs:
+ i.accept(self)
+ if node.elseblock:
+ self.condition_level += 1
+ node.elseblock.accept(self)
+ self.condition_level -= 1
+
+ def visit_IfNode(self, node: mparser.IfNode) -> None:
+ self.visit_default_func(node)
+ self.condition_level += 1
+ node.condition.accept(self)
+ node.block.accept(self)
+ self.condition_level -= 1
diff --git a/mesonbuild/ast/printer.py b/mesonbuild/ast/printer.py
new file mode 100644
index 0000000..3c53910
--- /dev/null
+++ b/mesonbuild/ast/printer.py
@@ -0,0 +1,399 @@
+# Copyright 2019 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool
+from __future__ import annotations
+
+from .. import mparser
+from . import AstVisitor
+import re
+import typing as T
+
+arithmic_map = {
+ 'add': '+',
+ 'sub': '-',
+ 'mod': '%',
+ 'mul': '*',
+ 'div': '/'
+}
+
+class AstPrinter(AstVisitor):
+ def __init__(self, indent: int = 2, arg_newline_cutoff: int = 5, update_ast_line_nos: bool = False):
+ self.result = ''
+ self.indent = indent
+ self.arg_newline_cutoff = arg_newline_cutoff
+ self.ci = ''
+ self.is_newline = True
+ self.last_level = 0
+ self.curr_line = 1 if update_ast_line_nos else None
+
+ def post_process(self) -> None:
+ self.result = re.sub(r'\s+\n', '\n', self.result)
+
+ def append(self, data: str, node: mparser.BaseNode) -> None:
+ self.last_level = node.level
+ if self.is_newline:
+ self.result += ' ' * (node.level * self.indent)
+ self.result += data
+ self.is_newline = False
+
+ def append_padded(self, data: str, node: mparser.BaseNode) -> None:
+ if self.result and self.result[-1] not in [' ', '\n']:
+ data = ' ' + data
+ self.append(data + ' ', node)
+
+ def newline(self) -> None:
+ self.result += '\n'
+ self.is_newline = True
+ if self.curr_line is not None:
+ self.curr_line += 1
+
+ def visit_BooleanNode(self, node: mparser.BooleanNode) -> None:
+ self.append('true' if node.value else 'false', node)
+ node.lineno = self.curr_line or node.lineno
+
+ def visit_IdNode(self, node: mparser.IdNode) -> None:
+ assert isinstance(node.value, str)
+ self.append(node.value, node)
+ node.lineno = self.curr_line or node.lineno
+
+ def visit_NumberNode(self, node: mparser.NumberNode) -> None:
+ self.append(str(node.value), node)
+ node.lineno = self.curr_line or node.lineno
+
+ def escape(self, val: str) -> str:
+ return val.translate(str.maketrans({'\'': '\\\'',
+ '\\': '\\\\'}))
+
+ def visit_StringNode(self, node: mparser.StringNode) -> None:
+ assert isinstance(node.value, str)
+ self.append("'" + self.escape(node.value) + "'", node)
+ node.lineno = self.curr_line or node.lineno
+
+ def visit_FormatStringNode(self, node: mparser.FormatStringNode) -> None:
+ assert isinstance(node.value, str)
+ self.append("f'" + node.value + "'", node)
+ node.lineno = self.curr_line or node.lineno
+
+ def visit_ContinueNode(self, node: mparser.ContinueNode) -> None:
+ self.append('continue', node)
+ node.lineno = self.curr_line or node.lineno
+
+ def visit_BreakNode(self, node: mparser.BreakNode) -> None:
+ self.append('break', node)
+ node.lineno = self.curr_line or node.lineno
+
+ def visit_ArrayNode(self, node: mparser.ArrayNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ self.append('[', node)
+ node.args.accept(self)
+ self.append(']', node)
+
+ def visit_DictNode(self, node: mparser.DictNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ self.append('{', node)
+ node.args.accept(self)
+ self.append('}', node)
+
+ def visit_OrNode(self, node: mparser.OrNode) -> None:
+ node.left.accept(self)
+ self.append_padded('or', node)
+ node.lineno = self.curr_line or node.lineno
+ node.right.accept(self)
+
+ def visit_AndNode(self, node: mparser.AndNode) -> None:
+ node.left.accept(self)
+ self.append_padded('and', node)
+ node.lineno = self.curr_line or node.lineno
+ node.right.accept(self)
+
+ def visit_ComparisonNode(self, node: mparser.ComparisonNode) -> None:
+ node.left.accept(self)
+ self.append_padded(node.ctype, node)
+ node.lineno = self.curr_line or node.lineno
+ node.right.accept(self)
+
+ def visit_ArithmeticNode(self, node: mparser.ArithmeticNode) -> None:
+ node.left.accept(self)
+ self.append_padded(arithmic_map[node.operation], node)
+ node.lineno = self.curr_line or node.lineno
+ node.right.accept(self)
+
+ def visit_NotNode(self, node: mparser.NotNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ self.append_padded('not', node)
+ node.value.accept(self)
+
+ def visit_CodeBlockNode(self, node: mparser.CodeBlockNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ for i in node.lines:
+ i.accept(self)
+ self.newline()
+
+ def visit_IndexNode(self, node: mparser.IndexNode) -> None:
+ node.iobject.accept(self)
+ node.lineno = self.curr_line or node.lineno
+ self.append('[', node)
+ node.index.accept(self)
+ self.append(']', node)
+
+ def visit_MethodNode(self, node: mparser.MethodNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ node.source_object.accept(self)
+ self.append('.' + node.name + '(', node)
+ node.args.accept(self)
+ self.append(')', node)
+
+ def visit_FunctionNode(self, node: mparser.FunctionNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ self.append(node.func_name + '(', node)
+ node.args.accept(self)
+ self.append(')', node)
+
+ def visit_AssignmentNode(self, node: mparser.AssignmentNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ self.append(node.var_name + ' = ', node)
+ node.value.accept(self)
+
+ def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ self.append(node.var_name + ' += ', node)
+ node.value.accept(self)
+
+ def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ self.append_padded('foreach', node)
+ self.append_padded(', '.join(node.varnames), node)
+ self.append_padded(':', node)
+ node.items.accept(self)
+ self.newline()
+ node.block.accept(self)
+ self.append('endforeach', node)
+
+ def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ prefix = ''
+ for i in node.ifs:
+ self.append_padded(prefix + 'if', node)
+ prefix = 'el'
+ i.accept(self)
+ if not isinstance(node.elseblock, mparser.EmptyNode):
+ self.append('else', node)
+ node.elseblock.accept(self)
+ self.append('endif', node)
+
+ def visit_UMinusNode(self, node: mparser.UMinusNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ self.append_padded('-', node)
+ node.value.accept(self)
+
+ def visit_IfNode(self, node: mparser.IfNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ node.condition.accept(self)
+ self.newline()
+ node.block.accept(self)
+
+ def visit_TernaryNode(self, node: mparser.TernaryNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ node.condition.accept(self)
+ self.append_padded('?', node)
+ node.trueblock.accept(self)
+ self.append_padded(':', node)
+ node.falseblock.accept(self)
+
+ def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None:
+ node.lineno = self.curr_line or node.lineno
+ break_args = (len(node.arguments) + len(node.kwargs)) > self.arg_newline_cutoff
+ for i in node.arguments + list(node.kwargs.values()):
+ if not isinstance(i, (mparser.ElementaryNode, mparser.IndexNode)):
+ break_args = True
+ if break_args:
+ self.newline()
+ for i in node.arguments:
+ i.accept(self)
+ self.append(', ', node)
+ if break_args:
+ self.newline()
+ for key, val in node.kwargs.items():
+ key.accept(self)
+ self.append_padded(':', node)
+ val.accept(self)
+ self.append(', ', node)
+ if break_args:
+ self.newline()
+ if break_args:
+ self.result = re.sub(r', \n$', '\n', self.result)
+ else:
+ self.result = re.sub(r', $', '', self.result)
+
+class AstJSONPrinter(AstVisitor):
+ def __init__(self) -> None:
+ self.result = {} # type: T.Dict[str, T.Any]
+ self.current = self.result
+
+ def _accept(self, key: str, node: mparser.BaseNode) -> None:
+ old = self.current
+ data = {} # type: T.Dict[str, T.Any]
+ self.current = data
+ node.accept(self)
+ self.current = old
+ self.current[key] = data
+
+ def _accept_list(self, key: str, nodes: T.Sequence[mparser.BaseNode]) -> None:
+ old = self.current
+ datalist = [] # type: T.List[T.Dict[str, T.Any]]
+ for i in nodes:
+ self.current = {}
+ i.accept(self)
+ datalist += [self.current]
+ self.current = old
+ self.current[key] = datalist
+
+ def _raw_accept(self, node: mparser.BaseNode, data: T.Dict[str, T.Any]) -> None:
+ old = self.current
+ self.current = data
+ node.accept(self)
+ self.current = old
+
+ def setbase(self, node: mparser.BaseNode) -> None:
+ self.current['node'] = type(node).__name__
+ self.current['lineno'] = node.lineno
+ self.current['colno'] = node.colno
+ self.current['end_lineno'] = node.end_lineno
+ self.current['end_colno'] = node.end_colno
+
+ def visit_default_func(self, node: mparser.BaseNode) -> None:
+ self.setbase(node)
+
+ def gen_ElementaryNode(self, node: mparser.ElementaryNode) -> None:
+ self.current['value'] = node.value
+ self.setbase(node)
+
+ def visit_BooleanNode(self, node: mparser.BooleanNode) -> None:
+ self.gen_ElementaryNode(node)
+
+ def visit_IdNode(self, node: mparser.IdNode) -> None:
+ self.gen_ElementaryNode(node)
+
+ def visit_NumberNode(self, node: mparser.NumberNode) -> None:
+ self.gen_ElementaryNode(node)
+
+ def visit_StringNode(self, node: mparser.StringNode) -> None:
+ self.gen_ElementaryNode(node)
+
+ def visit_FormatStringNode(self, node: mparser.FormatStringNode) -> None:
+ self.gen_ElementaryNode(node)
+
+ def visit_ArrayNode(self, node: mparser.ArrayNode) -> None:
+ self._accept('args', node.args)
+ self.setbase(node)
+
+ def visit_DictNode(self, node: mparser.DictNode) -> None:
+ self._accept('args', node.args)
+ self.setbase(node)
+
+ def visit_OrNode(self, node: mparser.OrNode) -> None:
+ self._accept('left', node.left)
+ self._accept('right', node.right)
+ self.setbase(node)
+
+ def visit_AndNode(self, node: mparser.AndNode) -> None:
+ self._accept('left', node.left)
+ self._accept('right', node.right)
+ self.setbase(node)
+
+ def visit_ComparisonNode(self, node: mparser.ComparisonNode) -> None:
+ self._accept('left', node.left)
+ self._accept('right', node.right)
+ self.current['ctype'] = node.ctype
+ self.setbase(node)
+
+ def visit_ArithmeticNode(self, node: mparser.ArithmeticNode) -> None:
+ self._accept('left', node.left)
+ self._accept('right', node.right)
+ self.current['op'] = arithmic_map[node.operation]
+ self.setbase(node)
+
+ def visit_NotNode(self, node: mparser.NotNode) -> None:
+ self._accept('right', node.value)
+ self.setbase(node)
+
+ def visit_CodeBlockNode(self, node: mparser.CodeBlockNode) -> None:
+ self._accept_list('lines', node.lines)
+ self.setbase(node)
+
+ def visit_IndexNode(self, node: mparser.IndexNode) -> None:
+ self._accept('object', node.iobject)
+ self._accept('index', node.index)
+ self.setbase(node)
+
+ def visit_MethodNode(self, node: mparser.MethodNode) -> None:
+ self._accept('object', node.source_object)
+ self._accept('args', node.args)
+ self.current['name'] = node.name
+ self.setbase(node)
+
+ def visit_FunctionNode(self, node: mparser.FunctionNode) -> None:
+ self._accept('args', node.args)
+ self.current['name'] = node.func_name
+ self.setbase(node)
+
+ def visit_AssignmentNode(self, node: mparser.AssignmentNode) -> None:
+ self._accept('value', node.value)
+ self.current['var_name'] = node.var_name
+ self.setbase(node)
+
+ def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode) -> None:
+ self._accept('value', node.value)
+ self.current['var_name'] = node.var_name
+ self.setbase(node)
+
+ def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None:
+ self._accept('items', node.items)
+ self._accept('block', node.block)
+ self.current['varnames'] = node.varnames
+ self.setbase(node)
+
+ def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None:
+ self._accept_list('ifs', node.ifs)
+ self._accept('else', node.elseblock)
+ self.setbase(node)
+
+ def visit_UMinusNode(self, node: mparser.UMinusNode) -> None:
+ self._accept('right', node.value)
+ self.setbase(node)
+
+ def visit_IfNode(self, node: mparser.IfNode) -> None:
+ self._accept('condition', node.condition)
+ self._accept('block', node.block)
+ self.setbase(node)
+
+ def visit_TernaryNode(self, node: mparser.TernaryNode) -> None:
+ self._accept('condition', node.condition)
+ self._accept('true', node.trueblock)
+ self._accept('false', node.falseblock)
+ self.setbase(node)
+
+ def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None:
+ self._accept_list('positional', node.arguments)
+ kwargs_list = [] # type: T.List[T.Dict[str, T.Dict[str, T.Any]]]
+ for key, val in node.kwargs.items():
+ key_res = {} # type: T.Dict[str, T.Any]
+ val_res = {} # type: T.Dict[str, T.Any]
+ self._raw_accept(key, key_res)
+ self._raw_accept(val, val_res)
+ kwargs_list += [{'key': key_res, 'val': val_res}]
+ self.current['kwargs'] = kwargs_list
+ self.setbase(node)
diff --git a/mesonbuild/ast/visitor.py b/mesonbuild/ast/visitor.py
new file mode 100644
index 0000000..8a0e77b
--- /dev/null
+++ b/mesonbuild/ast/visitor.py
@@ -0,0 +1,146 @@
+# Copyright 2019 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool
+from __future__ import annotations
+
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .. import mparser
+
+class AstVisitor:
+ def __init__(self) -> None:
+ pass
+
+ def visit_default_func(self, node: mparser.BaseNode) -> None:
+ pass
+
+ def visit_BooleanNode(self, node: mparser.BooleanNode) -> None:
+ self.visit_default_func(node)
+
+ def visit_IdNode(self, node: mparser.IdNode) -> None:
+ self.visit_default_func(node)
+
+ def visit_NumberNode(self, node: mparser.NumberNode) -> None:
+ self.visit_default_func(node)
+
+ def visit_StringNode(self, node: mparser.StringNode) -> None:
+ self.visit_default_func(node)
+
+ def visit_FormatStringNode(self, node: mparser.FormatStringNode) -> None:
+ self.visit_default_func(node)
+
+ def visit_ContinueNode(self, node: mparser.ContinueNode) -> None:
+ self.visit_default_func(node)
+
+ def visit_BreakNode(self, node: mparser.BreakNode) -> None:
+ self.visit_default_func(node)
+
+ def visit_ArrayNode(self, node: mparser.ArrayNode) -> None:
+ self.visit_default_func(node)
+ node.args.accept(self)
+
+ def visit_DictNode(self, node: mparser.DictNode) -> None:
+ self.visit_default_func(node)
+ node.args.accept(self)
+
+ def visit_EmptyNode(self, node: mparser.EmptyNode) -> None:
+ self.visit_default_func(node)
+
+ def visit_OrNode(self, node: mparser.OrNode) -> None:
+ self.visit_default_func(node)
+ node.left.accept(self)
+ node.right.accept(self)
+
+ def visit_AndNode(self, node: mparser.AndNode) -> None:
+ self.visit_default_func(node)
+ node.left.accept(self)
+ node.right.accept(self)
+
+ def visit_ComparisonNode(self, node: mparser.ComparisonNode) -> None:
+ self.visit_default_func(node)
+ node.left.accept(self)
+ node.right.accept(self)
+
+ def visit_ArithmeticNode(self, node: mparser.ArithmeticNode) -> None:
+ self.visit_default_func(node)
+ node.left.accept(self)
+ node.right.accept(self)
+
+ def visit_NotNode(self, node: mparser.NotNode) -> None:
+ self.visit_default_func(node)
+ node.value.accept(self)
+
+ def visit_CodeBlockNode(self, node: mparser.CodeBlockNode) -> None:
+ self.visit_default_func(node)
+ for i in node.lines:
+ i.accept(self)
+
+ def visit_IndexNode(self, node: mparser.IndexNode) -> None:
+ self.visit_default_func(node)
+ node.iobject.accept(self)
+ node.index.accept(self)
+
+ def visit_MethodNode(self, node: mparser.MethodNode) -> None:
+ self.visit_default_func(node)
+ node.source_object.accept(self)
+ node.args.accept(self)
+
+ def visit_FunctionNode(self, node: mparser.FunctionNode) -> None:
+ self.visit_default_func(node)
+ node.args.accept(self)
+
+ def visit_AssignmentNode(self, node: mparser.AssignmentNode) -> None:
+ self.visit_default_func(node)
+ node.value.accept(self)
+
+ def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode) -> None:
+ self.visit_default_func(node)
+ node.value.accept(self)
+
+ def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None:
+ self.visit_default_func(node)
+ node.items.accept(self)
+ node.block.accept(self)
+
+ def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None:
+ self.visit_default_func(node)
+ for i in node.ifs:
+ i.accept(self)
+ node.elseblock.accept(self)
+
+ def visit_UMinusNode(self, node: mparser.UMinusNode) -> None:
+ self.visit_default_func(node)
+ node.value.accept(self)
+
+ def visit_IfNode(self, node: mparser.IfNode) -> None:
+ self.visit_default_func(node)
+ node.condition.accept(self)
+ node.block.accept(self)
+
+ def visit_TernaryNode(self, node: mparser.TernaryNode) -> None:
+ self.visit_default_func(node)
+ node.condition.accept(self)
+ node.trueblock.accept(self)
+ node.falseblock.accept(self)
+
+ def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None:
+ self.visit_default_func(node)
+ for i in node.arguments:
+ i.accept(self)
+ for key, val in node.kwargs.items():
+ key.accept(self)
+ val.accept(self)
diff --git a/mesonbuild/backend/__init__.py b/mesonbuild/backend/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/mesonbuild/backend/__init__.py
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py
new file mode 100644
index 0000000..27004f8
--- /dev/null
+++ b/mesonbuild/backend/backends.py
@@ -0,0 +1,1914 @@
+# Copyright 2012-2016 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.
+from __future__ import annotations
+
+from collections import OrderedDict
+from dataclasses import dataclass, InitVar
+from functools import lru_cache
+from itertools import chain
+from pathlib import Path
+import copy
+import enum
+import json
+import os
+import pickle
+import re
+import shutil
+import typing as T
+import hashlib
+
+from .. import build
+from .. import dependencies
+from .. import programs
+from .. import mesonlib
+from .. import mlog
+from ..compilers import LANGUAGES_USING_LDFLAGS, detect
+from ..mesonlib import (
+ File, MachineChoice, MesonException, OrderedSet,
+ classify_unity_sources, OptionKey, join_args,
+ ExecutableSerialisation
+)
+
+if T.TYPE_CHECKING:
+ from .._typing import ImmutableListProtocol
+ from ..arglist import CompilerArgs
+ from ..compilers import Compiler
+ from ..environment import Environment
+ from ..interpreter import Interpreter, Test
+ from ..linkers import StaticLinker
+ from ..mesonlib import FileMode, FileOrString
+
+ from typing_extensions import TypedDict
+
+ _ALL_SOURCES_TYPE = T.List[T.Union[File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]]
+
+ class TargetIntrospectionData(TypedDict):
+
+ language: str
+ compiler: T.List[str]
+ parameters: T.List[str]
+ sources: T.List[str]
+ generated_sources: T.List[str]
+
+
+# Languages that can mix with C or C++ but don't support unity builds yet
+# because the syntax we use for unity builds is specific to C/++/ObjC/++.
+# Assembly files cannot be unitified and neither can LLVM IR files
+LANGS_CANT_UNITY = ('d', 'fortran', 'vala')
+
+@dataclass(eq=False)
+class RegenInfo:
+ source_dir: str
+ build_dir: str
+ depfiles: T.List[str]
+
+class TestProtocol(enum.Enum):
+
+ EXITCODE = 0
+ TAP = 1
+ GTEST = 2
+ RUST = 3
+
+ @classmethod
+ def from_str(cls, string: str) -> 'TestProtocol':
+ if string == 'exitcode':
+ return cls.EXITCODE
+ elif string == 'tap':
+ return cls.TAP
+ elif string == 'gtest':
+ return cls.GTEST
+ elif string == 'rust':
+ return cls.RUST
+ raise MesonException(f'unknown test format {string}')
+
+ def __str__(self) -> str:
+ cls = type(self)
+ if self is cls.EXITCODE:
+ return 'exitcode'
+ elif self is cls.GTEST:
+ return 'gtest'
+ elif self is cls.RUST:
+ return 'rust'
+ return 'tap'
+
+
+@dataclass(eq=False)
+class CleanTrees:
+ '''
+ Directories outputted by custom targets that have to be manually cleaned
+ because on Linux `ninja clean` only deletes empty directories.
+ '''
+ build_dir: str
+ trees: T.List[str]
+
+@dataclass(eq=False)
+class InstallData:
+ source_dir: str
+ build_dir: str
+ prefix: str
+ libdir: str
+ strip_bin: T.List[str]
+ # TODO: in python 3.8 or with typing_Extensions this could be:
+ # `T.Union[T.Literal['preserve'], int]`, which would be more accurate.
+ install_umask: T.Union[str, int]
+ mesonintrospect: T.List[str]
+ version: str
+
+ def __post_init__(self) -> None:
+ self.targets: T.List[TargetInstallData] = []
+ self.headers: T.List[InstallDataBase] = []
+ self.man: T.List[InstallDataBase] = []
+ self.emptydir: T.List[InstallEmptyDir] = []
+ self.data: T.List[InstallDataBase] = []
+ self.symlinks: T.List[InstallSymlinkData] = []
+ self.install_scripts: T.List[ExecutableSerialisation] = []
+ self.install_subdirs: T.List[SubdirInstallData] = []
+
+@dataclass(eq=False)
+class TargetInstallData:
+ fname: str
+ outdir: str
+ outdir_name: InitVar[T.Optional[str]]
+ strip: bool
+ install_name_mappings: T.Mapping[str, str]
+ rpath_dirs_to_remove: T.Set[bytes]
+ install_rpath: str
+ # TODO: install_mode should just always be a FileMode object
+ install_mode: T.Optional['FileMode']
+ subproject: str
+ optional: bool = False
+ tag: T.Optional[str] = None
+ can_strip: bool = False
+
+ def __post_init__(self, outdir_name: T.Optional[str]) -> None:
+ if outdir_name is None:
+ outdir_name = os.path.join('{prefix}', self.outdir)
+ self.out_name = os.path.join(outdir_name, os.path.basename(self.fname))
+
+@dataclass(eq=False)
+class InstallEmptyDir:
+ path: str
+ install_mode: 'FileMode'
+ subproject: str
+ tag: T.Optional[str] = None
+
+@dataclass(eq=False)
+class InstallDataBase:
+ path: str
+ install_path: str
+ install_path_name: str
+ install_mode: 'FileMode'
+ subproject: str
+ tag: T.Optional[str] = None
+ data_type: T.Optional[str] = None
+
+@dataclass(eq=False)
+class InstallSymlinkData:
+ target: str
+ name: str
+ install_path: str
+ subproject: str
+ tag: T.Optional[str] = None
+ allow_missing: bool = False
+
+# cannot use dataclass here because "exclude" is out of order
+class SubdirInstallData(InstallDataBase):
+ def __init__(self, path: str, install_path: str, install_path_name: str,
+ install_mode: 'FileMode', exclude: T.Tuple[T.Set[str], T.Set[str]],
+ subproject: str, tag: T.Optional[str] = None, data_type: T.Optional[str] = None):
+ super().__init__(path, install_path, install_path_name, install_mode, subproject, tag, data_type)
+ self.exclude = exclude
+
+
+@dataclass(eq=False)
+class TestSerialisation:
+ name: str
+ project_name: str
+ suite: T.List[str]
+ fname: T.List[str]
+ is_cross_built: bool
+ exe_wrapper: T.Optional[programs.ExternalProgram]
+ needs_exe_wrapper: bool
+ is_parallel: bool
+ cmd_args: T.List[str]
+ env: build.EnvironmentVariables
+ should_fail: bool
+ timeout: T.Optional[int]
+ workdir: T.Optional[str]
+ extra_paths: T.List[str]
+ protocol: TestProtocol
+ priority: int
+ cmd_is_built: bool
+ cmd_is_exe: bool
+ depends: T.List[str]
+ version: str
+ verbose: bool
+
+ def __post_init__(self) -> None:
+ if self.exe_wrapper is not None:
+ assert isinstance(self.exe_wrapper, programs.ExternalProgram)
+
+
+def get_backend_from_name(backend: str, build: T.Optional[build.Build] = None, interpreter: T.Optional['Interpreter'] = None) -> T.Optional['Backend']:
+ if backend == 'ninja':
+ from . import ninjabackend
+ return ninjabackend.NinjaBackend(build, interpreter)
+ elif backend == 'vs':
+ from . import vs2010backend
+ return vs2010backend.autodetect_vs_version(build, interpreter)
+ elif backend == 'vs2010':
+ from . import vs2010backend
+ return vs2010backend.Vs2010Backend(build, interpreter)
+ elif backend == 'vs2012':
+ from . import vs2012backend
+ return vs2012backend.Vs2012Backend(build, interpreter)
+ elif backend == 'vs2013':
+ from . import vs2013backend
+ return vs2013backend.Vs2013Backend(build, interpreter)
+ elif backend == 'vs2015':
+ from . import vs2015backend
+ return vs2015backend.Vs2015Backend(build, interpreter)
+ elif backend == 'vs2017':
+ from . import vs2017backend
+ return vs2017backend.Vs2017Backend(build, interpreter)
+ elif backend == 'vs2019':
+ from . import vs2019backend
+ return vs2019backend.Vs2019Backend(build, interpreter)
+ elif backend == 'vs2022':
+ from . import vs2022backend
+ return vs2022backend.Vs2022Backend(build, interpreter)
+ elif backend == 'xcode':
+ from . import xcodebackend
+ return xcodebackend.XCodeBackend(build, interpreter)
+ return None
+
+# This class contains the basic functionality that is needed by all backends.
+# Feel free to move stuff in and out of it as you see fit.
+class Backend:
+
+ environment: T.Optional['Environment']
+
+ def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional['Interpreter']):
+ # Make it possible to construct a dummy backend
+ # This is used for introspection without a build directory
+ if build is None:
+ self.environment = None
+ return
+ self.build = build
+ self.interpreter = interpreter
+ self.environment = build.environment
+ self.processed_targets: T.Set[str] = set()
+ self.name = '<UNKNOWN>'
+ self.build_dir = self.environment.get_build_dir()
+ self.source_dir = self.environment.get_source_dir()
+ self.build_to_src = mesonlib.relpath(self.environment.get_source_dir(),
+ self.environment.get_build_dir())
+ self.src_to_build = mesonlib.relpath(self.environment.get_build_dir(),
+ self.environment.get_source_dir())
+
+ def generate(self) -> None:
+ raise RuntimeError(f'generate is not implemented in {type(self).__name__}')
+
+ def get_target_filename(self, t: T.Union[build.Target, build.CustomTargetIndex], *, warn_multi_output: bool = True) -> str:
+ if isinstance(t, build.CustomTarget):
+ if warn_multi_output and len(t.get_outputs()) != 1:
+ mlog.warning(f'custom_target {t.name!r} has more than one output! '
+ 'Using the first one.')
+ filename = t.get_outputs()[0]
+ elif isinstance(t, build.CustomTargetIndex):
+ filename = t.get_outputs()[0]
+ else:
+ assert isinstance(t, build.BuildTarget), t
+ filename = t.get_filename()
+ return os.path.join(self.get_target_dir(t), filename)
+
+ def get_target_filename_abs(self, target: T.Union[build.Target, build.CustomTargetIndex]) -> str:
+ return os.path.join(self.environment.get_build_dir(), self.get_target_filename(target))
+
+ def get_source_dir_include_args(self, target: build.BuildTarget, compiler: 'Compiler', *, absolute_path: bool = False) -> T.List[str]:
+ curdir = target.get_subdir()
+ if absolute_path:
+ lead = self.source_dir
+ else:
+ lead = self.build_to_src
+ tmppath = os.path.normpath(os.path.join(lead, curdir))
+ return compiler.get_include_args(tmppath, False)
+
+ def get_build_dir_include_args(self, target: build.BuildTarget, compiler: 'Compiler', *, absolute_path: bool = False) -> T.List[str]:
+ if absolute_path:
+ curdir = os.path.join(self.build_dir, target.get_subdir())
+ else:
+ curdir = target.get_subdir()
+ if curdir == '':
+ curdir = '.'
+ return compiler.get_include_args(curdir, False)
+
+ def get_target_filename_for_linking(self, target: T.Union[build.Target, build.CustomTargetIndex]) -> T.Optional[str]:
+ # On some platforms (msvc for instance), the file that is used for
+ # dynamic linking is not the same as the dynamic library itself. This
+ # file is called an import library, and we want to link against that.
+ # On all other platforms, we link to the library directly.
+ if isinstance(target, build.SharedLibrary):
+ link_lib = target.get_import_filename() or target.get_filename()
+ return os.path.join(self.get_target_dir(target), link_lib)
+ elif isinstance(target, build.StaticLibrary):
+ return os.path.join(self.get_target_dir(target), target.get_filename())
+ elif isinstance(target, (build.CustomTarget, build.CustomTargetIndex)):
+ if not target.is_linkable_target():
+ raise MesonException(f'Tried to link against custom target "{target.name}", which is not linkable.')
+ return os.path.join(self.get_target_dir(target), target.get_filename())
+ elif isinstance(target, build.Executable):
+ if target.import_filename:
+ return os.path.join(self.get_target_dir(target), target.get_import_filename())
+ else:
+ return None
+ raise AssertionError(f'BUG: Tried to link to {target!r} which is not linkable')
+
+ @lru_cache(maxsize=None)
+ def get_target_dir(self, target: T.Union[build.Target, build.CustomTargetIndex]) -> str:
+ if isinstance(target, build.RunTarget):
+ # this produces no output, only a dummy top-level name
+ dirname = ''
+ elif self.environment.coredata.get_option(OptionKey('layout')) == 'mirror':
+ dirname = target.get_subdir()
+ else:
+ dirname = 'meson-out'
+ return dirname
+
+ def get_target_dir_relative_to(self, t: build.Target, o: build.Target) -> str:
+ '''Get a target dir relative to another target's directory'''
+ target_dir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(t))
+ othert_dir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(o))
+ return os.path.relpath(target_dir, othert_dir)
+
+ def get_target_source_dir(self, target: build.Target) -> str:
+ # if target dir is empty, avoid extraneous trailing / from os.path.join()
+ target_dir = self.get_target_dir(target)
+ if target_dir:
+ return os.path.join(self.build_to_src, target_dir)
+ return self.build_to_src
+
+ def get_target_private_dir(self, target: T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]) -> str:
+ return os.path.join(self.get_target_filename(target, warn_multi_output=False) + '.p')
+
+ def get_target_private_dir_abs(self, target: T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]) -> str:
+ return os.path.join(self.environment.get_build_dir(), self.get_target_private_dir(target))
+
+ @lru_cache(maxsize=None)
+ def get_target_generated_dir(
+ self, target: T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex],
+ gensrc: T.Union[build.CustomTarget, build.CustomTargetIndex, build.GeneratedList],
+ src: str) -> str:
+ """
+ Takes a BuildTarget, a generator source (CustomTarget or GeneratedList),
+ and a generated source filename.
+ Returns the full path of the generated source relative to the build root
+ """
+ # CustomTarget generators output to the build dir of the CustomTarget
+ if isinstance(gensrc, (build.CustomTarget, build.CustomTargetIndex)):
+ return os.path.join(self.get_target_dir(gensrc), src)
+ # GeneratedList generators output to the private build directory of the
+ # target that the GeneratedList is used in
+ return os.path.join(self.get_target_private_dir(target), src)
+
+ def get_unity_source_file(self, target: T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex],
+ suffix: str, number: int) -> mesonlib.File:
+ # There is a potential conflict here, but it is unlikely that
+ # anyone both enables unity builds and has a file called foo-unity.cpp.
+ osrc = f'{target.name}-unity{number}.{suffix}'
+ return mesonlib.File.from_built_file(self.get_target_private_dir(target), osrc)
+
+ def generate_unity_files(self, target: build.BuildTarget, unity_src: str) -> T.List[mesonlib.File]:
+ abs_files: T.List[str] = []
+ result: T.List[mesonlib.File] = []
+ compsrcs = classify_unity_sources(target.compilers.values(), unity_src)
+ unity_size = target.get_option(OptionKey('unity_size'))
+ assert isinstance(unity_size, int), 'for mypy'
+
+ def init_language_file(suffix: str, unity_file_number: int) -> T.TextIO:
+ unity_src = self.get_unity_source_file(target, suffix, unity_file_number)
+ outfileabs = unity_src.absolute_path(self.environment.get_source_dir(),
+ self.environment.get_build_dir())
+ outfileabs_tmp = outfileabs + '.tmp'
+ abs_files.append(outfileabs)
+ outfileabs_tmp_dir = os.path.dirname(outfileabs_tmp)
+ if not os.path.exists(outfileabs_tmp_dir):
+ os.makedirs(outfileabs_tmp_dir)
+ result.append(unity_src)
+ return open(outfileabs_tmp, 'w', encoding='utf-8')
+
+ # For each language, generate unity source files and return the list
+ for comp, srcs in compsrcs.items():
+ files_in_current = unity_size + 1
+ unity_file_number = 0
+ # TODO: this could be simplified with an algorithm that pre-sorts
+ # the sources into the size of chunks we want
+ ofile = None
+ for src in srcs:
+ if files_in_current >= unity_size:
+ if ofile:
+ ofile.close()
+ ofile = init_language_file(comp.get_default_suffix(), unity_file_number)
+ unity_file_number += 1
+ files_in_current = 0
+ ofile.write(f'#include<{src}>\n')
+ files_in_current += 1
+ if ofile:
+ ofile.close()
+
+ for x in abs_files:
+ mesonlib.replace_if_different(x, x + '.tmp')
+ return result
+
+ @staticmethod
+ def relpath(todir: str, fromdir: str) -> str:
+ return os.path.relpath(os.path.join('dummyprefixdir', todir),
+ os.path.join('dummyprefixdir', fromdir))
+
+ def flatten_object_list(self, target: build.BuildTarget, proj_dir_to_build_root: str = ''
+ ) -> T.Tuple[T.List[str], T.List[build.BuildTargetTypes]]:
+ obj_list, deps = self._flatten_object_list(target, target.get_objects(), proj_dir_to_build_root)
+ return list(dict.fromkeys(obj_list)), deps
+
+ def determine_ext_objs(self, objects: build.ExtractedObjects, proj_dir_to_build_root: str = '') -> T.List[str]:
+ obj_list, _ = self._flatten_object_list(objects.target, [objects], proj_dir_to_build_root)
+ return list(dict.fromkeys(obj_list))
+
+ def _flatten_object_list(self, target: build.BuildTarget,
+ objects: T.Sequence[T.Union[str, 'File', build.ExtractedObjects]],
+ proj_dir_to_build_root: str) -> T.Tuple[T.List[str], T.List[build.BuildTargetTypes]]:
+ obj_list: T.List[str] = []
+ deps: T.List[build.BuildTargetTypes] = []
+ for obj in objects:
+ if isinstance(obj, str):
+ o = os.path.join(proj_dir_to_build_root,
+ self.build_to_src, target.get_subdir(), obj)
+ obj_list.append(o)
+ elif isinstance(obj, mesonlib.File):
+ if obj.is_built:
+ o = os.path.join(proj_dir_to_build_root,
+ obj.rel_to_builddir(self.build_to_src))
+ obj_list.append(o)
+ else:
+ o = os.path.join(proj_dir_to_build_root,
+ self.build_to_src)
+ obj_list.append(obj.rel_to_builddir(o))
+ elif isinstance(obj, build.ExtractedObjects):
+ if obj.recursive:
+ objs, d = self._flatten_object_list(obj.target, obj.objlist, proj_dir_to_build_root)
+ obj_list.extend(objs)
+ deps.extend(d)
+ obj_list.extend(self._determine_ext_objs(obj, proj_dir_to_build_root))
+ deps.append(obj.target)
+ else:
+ raise MesonException('Unknown data type in object list.')
+ return obj_list, deps
+
+ @staticmethod
+ def is_swift_target(target: build.BuildTarget) -> bool:
+ for s in target.sources:
+ if s.endswith('swift'):
+ return True
+ return False
+
+ def determine_swift_dep_dirs(self, target: build.BuildTarget) -> T.List[str]:
+ result: T.List[str] = []
+ for l in target.link_targets:
+ result.append(self.get_target_private_dir_abs(l))
+ return result
+
+ def get_executable_serialisation(
+ self, cmd: T.Sequence[T.Union[programs.ExternalProgram, build.BuildTarget, build.CustomTarget, File, str]],
+ workdir: T.Optional[str] = None,
+ extra_bdeps: T.Optional[T.List[build.BuildTarget]] = None,
+ capture: T.Optional[bool] = None,
+ feed: T.Optional[bool] = None,
+ env: T.Optional[build.EnvironmentVariables] = None,
+ tag: T.Optional[str] = None,
+ verbose: bool = False) -> 'ExecutableSerialisation':
+
+ # XXX: cmd_args either need to be lowered to strings, or need to be checked for non-string arguments, right?
+ exe, *raw_cmd_args = cmd
+ if isinstance(exe, programs.ExternalProgram):
+ exe_cmd = exe.get_command()
+ exe_for_machine = exe.for_machine
+ elif isinstance(exe, build.BuildTarget):
+ exe_cmd = [self.get_target_filename_abs(exe)]
+ exe_for_machine = exe.for_machine
+ elif isinstance(exe, build.CustomTarget):
+ # The output of a custom target can either be directly runnable
+ # or not, that is, a script, a native binary or a cross compiled
+ # binary when exe wrapper is available and when it is not.
+ # This implementation is not exhaustive but it works in the
+ # common cases.
+ exe_cmd = [self.get_target_filename_abs(exe)]
+ exe_for_machine = MachineChoice.BUILD
+ elif isinstance(exe, mesonlib.File):
+ exe_cmd = [exe.rel_to_builddir(self.environment.source_dir)]
+ exe_for_machine = MachineChoice.BUILD
+ else:
+ exe_cmd = [exe]
+ exe_for_machine = MachineChoice.BUILD
+
+ cmd_args: T.List[str] = []
+ for c in raw_cmd_args:
+ if isinstance(c, programs.ExternalProgram):
+ p = c.get_path()
+ assert isinstance(p, str)
+ cmd_args.append(p)
+ elif isinstance(c, (build.BuildTarget, build.CustomTarget)):
+ cmd_args.append(self.get_target_filename_abs(c))
+ elif isinstance(c, mesonlib.File):
+ cmd_args.append(c.rel_to_builddir(self.environment.source_dir))
+ else:
+ cmd_args.append(c)
+
+ machine = self.environment.machines[exe_for_machine]
+ if machine.is_windows() or machine.is_cygwin():
+ extra_paths = self.determine_windows_extra_paths(exe, extra_bdeps or [])
+ else:
+ extra_paths = []
+
+ is_cross_built = not self.environment.machines.matches_build_machine(exe_for_machine)
+ if is_cross_built and self.environment.need_exe_wrapper():
+ exe_wrapper = self.environment.get_exe_wrapper()
+ if not exe_wrapper or not exe_wrapper.found():
+ msg = 'An exe_wrapper is needed but was not found. Please define one ' \
+ 'in cross file and check the command and/or add it to PATH.'
+ raise MesonException(msg)
+ else:
+ if exe_cmd[0].endswith('.jar'):
+ exe_cmd = ['java', '-jar'] + exe_cmd
+ elif exe_cmd[0].endswith('.exe') and not (mesonlib.is_windows() or mesonlib.is_cygwin() or mesonlib.is_wsl()):
+ exe_cmd = ['mono'] + exe_cmd
+ exe_wrapper = None
+
+ workdir = workdir or self.environment.get_build_dir()
+ return ExecutableSerialisation(exe_cmd + cmd_args, env,
+ exe_wrapper, workdir,
+ extra_paths, capture, feed, tag, verbose)
+
+ def as_meson_exe_cmdline(self, exe: T.Union[str, mesonlib.File, build.BuildTarget, build.CustomTarget, programs.ExternalProgram],
+ cmd_args: T.Sequence[T.Union[str, mesonlib.File, build.BuildTarget, build.CustomTarget, programs.ExternalProgram]],
+ workdir: T.Optional[str] = None,
+ extra_bdeps: T.Optional[T.List[build.BuildTarget]] = None,
+ capture: T.Optional[bool] = None,
+ feed: T.Optional[bool] = None,
+ force_serialize: bool = False,
+ env: T.Optional[build.EnvironmentVariables] = None,
+ verbose: bool = False) -> T.Tuple[T.Sequence[T.Union[str, File, build.Target, programs.ExternalProgram]], str]:
+ '''
+ Serialize an executable for running with a generator or a custom target
+ '''
+ cmd: T.List[T.Union[str, mesonlib.File, build.BuildTarget, build.CustomTarget, programs.ExternalProgram]] = []
+ cmd.append(exe)
+ cmd.extend(cmd_args)
+ es = self.get_executable_serialisation(cmd, workdir, extra_bdeps, capture, feed, env, verbose=verbose)
+ reasons: T.List[str] = []
+ if es.extra_paths:
+ reasons.append('to set PATH')
+
+ if es.exe_wrapper:
+ reasons.append('to use exe_wrapper')
+
+ if workdir:
+ reasons.append('to set workdir')
+
+ if any('\n' in c for c in es.cmd_args):
+ reasons.append('because command contains newlines')
+
+ if env and env.varnames:
+ reasons.append('to set env')
+
+ # force_serialize passed to this function means that the VS backend has
+ # decided it absolutely cannot use real commands. This is "always",
+ # because it's not clear what will work (other than compilers) and so
+ # we don't bother to handle a variety of common cases that probably do
+ # work.
+ #
+ # It's also overridden for a few conditions that can't be handled
+ # inside a command line
+
+ can_use_env = not force_serialize
+ force_serialize = force_serialize or bool(reasons)
+
+ if capture:
+ reasons.append('to capture output')
+ if feed:
+ reasons.append('to feed input')
+
+ if can_use_env and reasons == ['to set env'] and shutil.which('env'):
+ envlist = []
+ for k, v in env.get_env({}).items():
+ envlist.append(f'{k}={v}')
+ return ['env'] + envlist + es.cmd_args, ', '.join(reasons)
+
+ if not force_serialize:
+ if not capture and not feed:
+ return es.cmd_args, ''
+ args: T.List[str] = []
+ if capture:
+ args += ['--capture', str(capture)]
+ if feed:
+ args += ['--feed', str(feed)]
+
+ return (
+ self.environment.get_build_command() + ['--internal', 'exe'] + args + ['--'] + es.cmd_args,
+ ', '.join(reasons)
+ )
+
+ if isinstance(exe, (programs.ExternalProgram,
+ build.BuildTarget, build.CustomTarget)):
+ basename = exe.name
+ elif isinstance(exe, mesonlib.File):
+ basename = os.path.basename(exe.fname)
+ else:
+ basename = os.path.basename(exe)
+
+ # Can't just use exe.name here; it will likely be run more than once
+ # Take a digest of the cmd args, env, workdir, capture, and feed. This
+ # avoids collisions and also makes the name deterministic over
+ # regenerations which avoids a rebuild by Ninja because the cmdline
+ # stays the same.
+ hasher = hashlib.sha1()
+ if es.env:
+ es.env.hash(hasher)
+ hasher.update(bytes(str(es.cmd_args), encoding='utf-8'))
+ hasher.update(bytes(str(es.workdir), encoding='utf-8'))
+ hasher.update(bytes(str(capture), encoding='utf-8'))
+ hasher.update(bytes(str(feed), encoding='utf-8'))
+ digest = hasher.hexdigest()
+ scratch_file = f'meson_exe_{basename}_{digest}.dat'
+ exe_data = os.path.join(self.environment.get_scratch_dir(), scratch_file)
+ with open(exe_data, 'wb') as f:
+ pickle.dump(es, f)
+ return (self.environment.get_build_command() + ['--internal', 'exe', '--unpickle', exe_data],
+ ', '.join(reasons))
+
+ def serialize_tests(self) -> T.Tuple[str, str]:
+ test_data = os.path.join(self.environment.get_scratch_dir(), 'meson_test_setup.dat')
+ with open(test_data, 'wb') as datafile:
+ self.write_test_file(datafile)
+ benchmark_data = os.path.join(self.environment.get_scratch_dir(), 'meson_benchmark_setup.dat')
+ with open(benchmark_data, 'wb') as datafile:
+ self.write_benchmark_file(datafile)
+ return test_data, benchmark_data
+
+ def determine_linker_and_stdlib_args(self, target: build.BuildTarget) -> T.Tuple[T.Union['Compiler', 'StaticLinker'], T.List[str]]:
+ '''
+ If we're building a static library, there is only one static linker.
+ Otherwise, we query the target for the dynamic linker.
+ '''
+ if isinstance(target, build.StaticLibrary):
+ return self.build.static_linker[target.for_machine], []
+ l, stdlib_args = target.get_clink_dynamic_linker_and_stdlibs()
+ return l, stdlib_args
+
+ @staticmethod
+ def _libdir_is_system(libdir: str, compilers: T.Mapping[str, 'Compiler'], env: 'Environment') -> bool:
+ libdir = os.path.normpath(libdir)
+ for cc in compilers.values():
+ if libdir in cc.get_library_dirs(env):
+ return True
+ return False
+
+ def get_external_rpath_dirs(self, target: build.BuildTarget) -> T.Set[str]:
+ args: T.List[str] = []
+ for lang in LANGUAGES_USING_LDFLAGS:
+ try:
+ e = self.environment.coredata.get_external_link_args(target.for_machine, lang)
+ if isinstance(e, str):
+ args.append(e)
+ else:
+ args.extend(e)
+ except Exception:
+ pass
+ return self.get_rpath_dirs_from_link_args(args)
+
+ @staticmethod
+ def get_rpath_dirs_from_link_args(args: T.List[str]) -> T.Set[str]:
+ dirs: T.Set[str] = set()
+ # Match rpath formats:
+ # -Wl,-rpath=
+ # -Wl,-rpath,
+ rpath_regex = re.compile(r'-Wl,-rpath[=,]([^,]+)')
+ # Match solaris style compat runpath formats:
+ # -Wl,-R
+ # -Wl,-R,
+ runpath_regex = re.compile(r'-Wl,-R[,]?([^,]+)')
+ # Match symbols formats:
+ # -Wl,--just-symbols=
+ # -Wl,--just-symbols,
+ symbols_regex = re.compile(r'-Wl,--just-symbols[=,]([^,]+)')
+ for arg in args:
+ rpath_match = rpath_regex.match(arg)
+ if rpath_match:
+ for dir in rpath_match.group(1).split(':'):
+ dirs.add(dir)
+ runpath_match = runpath_regex.match(arg)
+ if runpath_match:
+ for dir in runpath_match.group(1).split(':'):
+ # The symbols arg is an rpath if the path is a directory
+ if Path(dir).is_dir():
+ dirs.add(dir)
+ symbols_match = symbols_regex.match(arg)
+ if symbols_match:
+ for dir in symbols_match.group(1).split(':'):
+ # Prevent usage of --just-symbols to specify rpath
+ if Path(dir).is_dir():
+ raise MesonException(f'Invalid arg for --just-symbols, {dir} is a directory.')
+ return dirs
+
+ @lru_cache(maxsize=None)
+ def rpaths_for_non_system_absolute_shared_libraries(self, target: build.BuildTarget, exclude_system: bool = True) -> 'ImmutableListProtocol[str]':
+ paths: OrderedSet[str] = OrderedSet()
+ for dep in target.external_deps:
+ if not isinstance(dep, (dependencies.ExternalLibrary, dependencies.PkgConfigDependency)):
+ continue
+ for libpath in dep.link_args:
+ # For all link args that are absolute paths to a library file, add RPATH args
+ if not os.path.isabs(libpath):
+ continue
+ libdir = os.path.dirname(libpath)
+ if exclude_system and self._libdir_is_system(libdir, target.compilers, self.environment):
+ # No point in adding system paths.
+ continue
+ # Don't remove rpaths specified in LDFLAGS.
+ if libdir in self.get_external_rpath_dirs(target):
+ continue
+ # Windows doesn't support rpaths, but we use this function to
+ # emulate rpaths by setting PATH, so also accept DLLs here
+ if os.path.splitext(libpath)[1] not in ['.dll', '.lib', '.so', '.dylib']:
+ continue
+ if libdir.startswith(self.environment.get_source_dir()):
+ rel_to_src = libdir[len(self.environment.get_source_dir()) + 1:]
+ assert not os.path.isabs(rel_to_src), f'rel_to_src: {rel_to_src} is absolute'
+ paths.add(os.path.join(self.build_to_src, rel_to_src))
+ else:
+ paths.add(libdir)
+ # Don't remove rpaths specified by the dependency
+ paths.difference_update(self.get_rpath_dirs_from_link_args(dep.link_args))
+ for i in chain(target.link_targets, target.link_whole_targets):
+ if isinstance(i, build.BuildTarget):
+ paths.update(self.rpaths_for_non_system_absolute_shared_libraries(i, exclude_system))
+ return list(paths)
+
+ # This may take other types
+ def determine_rpath_dirs(self, target: T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]
+ ) -> T.Tuple[str, ...]:
+ result: OrderedSet[str]
+ if self.environment.coredata.get_option(OptionKey('layout')) == 'mirror':
+ # Need a copy here
+ result = OrderedSet(target.get_link_dep_subdirs())
+ else:
+ result = OrderedSet()
+ result.add('meson-out')
+ if isinstance(target, build.BuildTarget):
+ result.update(self.rpaths_for_non_system_absolute_shared_libraries(target))
+ target.rpath_dirs_to_remove.update([d.encode('utf-8') for d in result])
+ return tuple(result)
+
+ @staticmethod
+ def canonicalize_filename(fname: str) -> str:
+ parts = Path(fname).parts
+ hashed = ''
+ if len(parts) > 5:
+ temp = '/'.join(parts[-5:])
+ # is it shorter to hash the beginning of the path?
+ if len(fname) > len(temp) + 41:
+ hashed = hashlib.sha1(fname.encode('utf-8')).hexdigest() + '_'
+ fname = temp
+ for ch in ('/', '\\', ':'):
+ fname = fname.replace(ch, '_')
+ return hashed + fname
+
+ def object_filename_from_source(self, target: build.BuildTarget, source: 'FileOrString') -> str:
+ assert isinstance(source, mesonlib.File)
+ if isinstance(target, build.CompileTarget):
+ return target.sources_map[source]
+ build_dir = self.environment.get_build_dir()
+ rel_src = source.rel_to_builddir(self.build_to_src)
+
+ # foo.vala files compile down to foo.c and then foo.c.o, not foo.vala.o
+ if rel_src.endswith(('.vala', '.gs')):
+ # See description in generate_vala_compile for this logic.
+ if source.is_built:
+ if os.path.isabs(rel_src):
+ rel_src = rel_src[len(build_dir) + 1:]
+ rel_src = os.path.relpath(rel_src, self.get_target_private_dir(target))
+ else:
+ rel_src = os.path.basename(rel_src)
+ # A meson- prefixed directory is reserved; hopefully no-one creates a file name with such a weird prefix.
+ gen_source = 'meson-generated_' + rel_src[:-5] + '.c'
+ elif source.is_built:
+ if os.path.isabs(rel_src):
+ rel_src = rel_src[len(build_dir) + 1:]
+ targetdir = self.get_target_private_dir(target)
+ # A meson- prefixed directory is reserved; hopefully no-one creates a file name with such a weird prefix.
+ gen_source = 'meson-generated_' + os.path.relpath(rel_src, targetdir)
+ else:
+ if os.path.isabs(rel_src):
+ # Use the absolute path directly to avoid file name conflicts
+ gen_source = rel_src
+ else:
+ gen_source = os.path.relpath(os.path.join(build_dir, rel_src),
+ os.path.join(self.environment.get_source_dir(), target.get_subdir()))
+ machine = self.environment.machines[target.for_machine]
+ return self.canonicalize_filename(gen_source) + '.' + machine.get_object_suffix()
+
+ def _determine_ext_objs(self, extobj: 'build.ExtractedObjects', proj_dir_to_build_root: str) -> T.List[str]:
+ result: T.List[str] = []
+
+ # Merge sources and generated sources
+ raw_sources = list(extobj.srclist)
+ for gensrc in extobj.genlist:
+ for r in gensrc.get_outputs():
+ path = self.get_target_generated_dir(extobj.target, gensrc, r)
+ dirpart, fnamepart = os.path.split(path)
+ raw_sources.append(File(True, dirpart, fnamepart))
+
+ # Filter out headers and all non-source files
+ sources: T.List['FileOrString'] = []
+ for s in raw_sources:
+ if self.environment.is_source(s):
+ sources.append(s)
+ elif self.environment.is_object(s):
+ result.append(s.relative_name())
+
+ # extobj could contain only objects and no sources
+ if not sources:
+ return result
+
+ targetdir = self.get_target_private_dir(extobj.target)
+
+ # With unity builds, sources don't map directly to objects,
+ # we only support extracting all the objects in this mode,
+ # so just return all object files.
+ if extobj.target.is_unity:
+ compsrcs = classify_unity_sources(extobj.target.compilers.values(), sources)
+ sources = []
+ unity_size = extobj.target.get_option(OptionKey('unity_size'))
+ assert isinstance(unity_size, int), 'for mypy'
+
+ for comp, srcs in compsrcs.items():
+ if comp.language in LANGS_CANT_UNITY:
+ sources += srcs
+ continue
+ for i in range(len(srcs) // unity_size + 1):
+ _src = self.get_unity_source_file(extobj.target,
+ comp.get_default_suffix(), i)
+ sources.append(_src)
+
+ for osrc in sources:
+ objname = self.object_filename_from_source(extobj.target, osrc)
+ objpath = os.path.join(proj_dir_to_build_root, targetdir, objname)
+ result.append(objpath)
+
+ return result
+
+ def get_pch_include_args(self, compiler: 'Compiler', target: build.BuildTarget) -> T.List[str]:
+ args: T.List[str] = []
+ pchpath = self.get_target_private_dir(target)
+ includeargs = compiler.get_include_args(pchpath, False)
+ p = target.get_pch(compiler.get_language())
+ if p:
+ args += compiler.get_pch_use_args(pchpath, p[0])
+ return includeargs + args
+
+ def create_msvc_pch_implementation(self, target: build.BuildTarget, lang: str, pch_header: str) -> str:
+ # We have to include the language in the file name, otherwise
+ # pch.c and pch.cpp will both end up as pch.obj in VS backends.
+ impl_name = f'meson_pch-{lang}.{lang}'
+ pch_rel_to_build = os.path.join(self.get_target_private_dir(target), impl_name)
+ # Make sure to prepend the build dir, since the working directory is
+ # not defined. Otherwise, we might create the file in the wrong path.
+ pch_file = os.path.join(self.build_dir, pch_rel_to_build)
+ os.makedirs(os.path.dirname(pch_file), exist_ok=True)
+
+ content = f'#include "{os.path.basename(pch_header)}"'
+ pch_file_tmp = pch_file + '.tmp'
+ with open(pch_file_tmp, 'w', encoding='utf-8') as f:
+ f.write(content)
+ mesonlib.replace_if_different(pch_file, pch_file_tmp)
+ return pch_rel_to_build
+
+ @staticmethod
+ def escape_extra_args(args: T.List[str]) -> T.List[str]:
+ # all backslashes in defines are doubly-escaped
+ extra_args: T.List[str] = []
+ for arg in args:
+ if arg.startswith(('-D', '/D')):
+ arg = arg.replace('\\', '\\\\')
+ extra_args.append(arg)
+
+ return extra_args
+
+ def get_no_stdlib_args(self, target: 'build.BuildTarget', compiler: 'Compiler') -> T.List[str]:
+ if compiler.language in self.build.stdlibs[target.for_machine]:
+ return compiler.get_no_stdinc_args()
+ return []
+
+ def generate_basic_compiler_args(self, target: build.BuildTarget, compiler: 'Compiler', no_warn_args: bool = False) -> 'CompilerArgs':
+ # Create an empty commands list, and start adding arguments from
+ # various sources in the order in which they must override each other
+ # starting from hard-coded defaults followed by build options and so on.
+ commands = compiler.compiler_args()
+
+ copt_proxy = target.get_options()
+ # First, the trivial ones that are impossible to override.
+ #
+ # Add -nostdinc/-nostdinc++ if needed; can't be overridden
+ commands += self.get_no_stdlib_args(target, compiler)
+ # Add things like /NOLOGO or -pipe; usually can't be overridden
+ commands += compiler.get_always_args()
+ # Only add warning-flags by default if the buildtype enables it, and if
+ # we weren't explicitly asked to not emit warnings (for Vala, f.ex)
+ if no_warn_args:
+ commands += compiler.get_no_warn_args()
+ else:
+ # warning_level is a string, but mypy can't determine that
+ commands += compiler.get_warn_args(T.cast('str', target.get_option(OptionKey('warning_level'))))
+ # Add -Werror if werror=true is set in the build options set on the
+ # command-line or default_options inside project(). This only sets the
+ # action to be done for warnings if/when they are emitted, so it's ok
+ # to set it after get_no_warn_args() or get_warn_args().
+ if target.get_option(OptionKey('werror')):
+ commands += compiler.get_werror_args()
+ # Add compile args for c_* or cpp_* build options set on the
+ # command-line or default_options inside project().
+ commands += compiler.get_option_compile_args(copt_proxy)
+
+ # Add buildtype args: optimization level, debugging, etc.
+ buildtype = target.get_option(OptionKey('buildtype'))
+ assert isinstance(buildtype, str), 'for mypy'
+ commands += compiler.get_buildtype_args(buildtype)
+
+ optimization = target.get_option(OptionKey('optimization'))
+ assert isinstance(optimization, str), 'for mypy'
+ commands += compiler.get_optimization_args(optimization)
+
+ debug = target.get_option(OptionKey('debug'))
+ assert isinstance(debug, bool), 'for mypy'
+ commands += compiler.get_debug_args(debug)
+
+ # Add compile args added using add_project_arguments()
+ commands += self.build.get_project_args(compiler, target.subproject, target.for_machine)
+ # Add compile args added using add_global_arguments()
+ # These override per-project arguments
+ commands += self.build.get_global_args(compiler, target.for_machine)
+ # Compile args added from the env: CFLAGS/CXXFLAGS, etc, or the cross
+ # file. We want these to override all the defaults, but not the
+ # per-target compile args.
+ commands += self.environment.coredata.get_external_args(target.for_machine, compiler.get_language())
+ # Using both /Z7 or /ZI and /Zi at the same times produces a compiler warning.
+ # We do not add /Z7 or /ZI by default. If it is being used it is because the user has explicitly enabled it.
+ # /Zi needs to be removed in that case to avoid cl's warning to that effect (D9025 : overriding '/Zi' with '/ZI')
+ if ('/Zi' in commands) and (('/ZI' in commands) or ('/Z7' in commands)):
+ commands.remove('/Zi')
+ # Always set -fPIC for shared libraries
+ if isinstance(target, build.SharedLibrary):
+ commands += compiler.get_pic_args()
+ # Set -fPIC for static libraries by default unless explicitly disabled
+ if isinstance(target, build.StaticLibrary) and target.pic:
+ commands += compiler.get_pic_args()
+ elif isinstance(target, (build.StaticLibrary, build.Executable)) and target.pie:
+ commands += compiler.get_pie_args()
+ # Add compile args needed to find external dependencies. Link args are
+ # added while generating the link command.
+ # NOTE: We must preserve the order in which external deps are
+ # specified, so we reverse the list before iterating over it.
+ for dep in reversed(target.get_external_deps()):
+ if not dep.found():
+ continue
+
+ if compiler.language == 'vala':
+ if isinstance(dep, dependencies.PkgConfigDependency):
+ if dep.name == 'glib-2.0' and dep.version_reqs is not None:
+ for req in dep.version_reqs:
+ if req.startswith(('>=', '==')):
+ commands += ['--target-glib', req[2:]]
+ break
+ commands += ['--pkg', dep.name]
+ elif isinstance(dep, dependencies.ExternalLibrary):
+ commands += dep.get_link_args('vala')
+ else:
+ commands += compiler.get_dependency_compile_args(dep)
+ # Qt needs -fPIC for executables
+ # XXX: We should move to -fPIC for all executables
+ if isinstance(target, build.Executable):
+ commands += dep.get_exe_args(compiler)
+ # For 'automagic' deps: Boost and GTest. Also dependency('threads').
+ # pkg-config puts the thread flags itself via `Cflags:`
+ # Fortran requires extra include directives.
+ if compiler.language == 'fortran':
+ for lt in chain(target.link_targets, target.link_whole_targets):
+ priv_dir = self.get_target_private_dir(lt)
+ commands += compiler.get_include_args(priv_dir, False)
+ return commands
+
+ def build_target_link_arguments(self, compiler: 'Compiler', deps: T.List[build.Target]) -> T.List[str]:
+ args: T.List[str] = []
+ for d in deps:
+ if not d.is_linkable_target():
+ raise RuntimeError(f'Tried to link with a non-library target "{d.get_basename()}".')
+ arg = self.get_target_filename_for_linking(d)
+ if not arg:
+ continue
+ if compiler.get_language() == 'd':
+ arg = '-Wl,' + arg
+ else:
+ arg = compiler.get_linker_lib_prefix() + arg
+ args.append(arg)
+ return args
+
+ def get_mingw_extra_paths(self, target: build.BuildTarget) -> T.List[str]:
+ paths: OrderedSet[str] = OrderedSet()
+ # The cross bindir
+ root = self.environment.properties[target.for_machine].get_root()
+ if root:
+ paths.add(os.path.join(root, 'bin'))
+ # The toolchain bindir
+ sys_root = self.environment.properties[target.for_machine].get_sys_root()
+ if sys_root:
+ paths.add(os.path.join(sys_root, 'bin'))
+ # Get program and library dirs from all target compilers
+ if isinstance(target, build.BuildTarget):
+ for cc in target.compilers.values():
+ paths.update(cc.get_program_dirs(self.environment))
+ paths.update(cc.get_library_dirs(self.environment))
+ return list(paths)
+
+ def determine_windows_extra_paths(
+ self, target: T.Union[build.BuildTarget, build.CustomTarget, programs.ExternalProgram, mesonlib.File, str],
+ extra_bdeps: T.Sequence[T.Union[build.BuildTarget, build.CustomTarget]]) -> T.List[str]:
+ """On Windows there is no such thing as an rpath.
+
+ We must determine all locations of DLLs that this exe
+ links to and return them so they can be used in unit
+ tests.
+ """
+ result: T.Set[str] = set()
+ prospectives: T.Set[build.BuildTargetTypes] = set()
+ if isinstance(target, build.BuildTarget):
+ prospectives.update(target.get_transitive_link_deps())
+ # External deps
+ for deppath in self.rpaths_for_non_system_absolute_shared_libraries(target, exclude_system=False):
+ result.add(os.path.normpath(os.path.join(self.environment.get_build_dir(), deppath)))
+ for bdep in extra_bdeps:
+ prospectives.add(bdep)
+ if isinstance(bdep, build.BuildTarget):
+ prospectives.update(bdep.get_transitive_link_deps())
+ # Internal deps
+ for ld in prospectives:
+ dirseg = os.path.join(self.environment.get_build_dir(), self.get_target_dir(ld))
+ result.add(dirseg)
+ if (isinstance(target, build.BuildTarget) and
+ not self.environment.machines.matches_build_machine(target.for_machine)):
+ result.update(self.get_mingw_extra_paths(target))
+ return list(result)
+
+ def write_benchmark_file(self, datafile: T.BinaryIO) -> None:
+ self.write_test_serialisation(self.build.get_benchmarks(), datafile)
+
+ def write_test_file(self, datafile: T.BinaryIO) -> None:
+ self.write_test_serialisation(self.build.get_tests(), datafile)
+
+ def create_test_serialisation(self, tests: T.List['Test']) -> T.List[TestSerialisation]:
+ arr: T.List[TestSerialisation] = []
+ for t in sorted(tests, key=lambda tst: -1 * tst.priority):
+ exe = t.get_exe()
+ if isinstance(exe, programs.ExternalProgram):
+ cmd = exe.get_command()
+ else:
+ cmd = [os.path.join(self.environment.get_build_dir(), self.get_target_filename(exe))]
+ if isinstance(exe, (build.BuildTarget, programs.ExternalProgram)):
+ test_for_machine = exe.for_machine
+ else:
+ # E.g. an external verifier or simulator program run on a generated executable.
+ # Can always be run without a wrapper.
+ test_for_machine = MachineChoice.BUILD
+
+ # we allow passing compiled executables to tests, which may be cross built.
+ # We need to consider these as well when considering whether the target is cross or not.
+ for a in t.cmd_args:
+ if isinstance(a, build.BuildTarget):
+ if a.for_machine is MachineChoice.HOST:
+ test_for_machine = MachineChoice.HOST
+ break
+
+ is_cross = self.environment.is_cross_build(test_for_machine)
+ exe_wrapper = self.environment.get_exe_wrapper()
+ machine = self.environment.machines[exe.for_machine]
+ if machine.is_windows() or machine.is_cygwin():
+ extra_bdeps: T.List[T.Union[build.BuildTarget, build.CustomTarget]] = []
+ if isinstance(exe, build.CustomTarget):
+ extra_bdeps = list(exe.get_transitive_build_target_deps())
+ extra_paths = self.determine_windows_extra_paths(exe, extra_bdeps)
+ else:
+ extra_paths = []
+
+ cmd_args: T.List[str] = []
+ depends: T.Set[build.Target] = set(t.depends)
+ if isinstance(exe, build.Target):
+ depends.add(exe)
+ for a in t.cmd_args:
+ if isinstance(a, build.Target):
+ depends.add(a)
+ elif isinstance(a, build.CustomTargetIndex):
+ depends.add(a.target)
+ if isinstance(a, build.BuildTarget):
+ extra_paths += self.determine_windows_extra_paths(a, [])
+
+ if isinstance(a, mesonlib.File):
+ a = os.path.join(self.environment.get_build_dir(), a.rel_to_builddir(self.build_to_src))
+ cmd_args.append(a)
+ elif isinstance(a, str):
+ cmd_args.append(a)
+ elif isinstance(a, (build.Target, build.CustomTargetIndex)):
+ cmd_args.extend(self.construct_target_rel_paths(a, t.workdir))
+ else:
+ raise MesonException('Bad object in test command.')
+
+ t_env = copy.deepcopy(t.env)
+ if not machine.is_windows() and not machine.is_cygwin() and not machine.is_darwin():
+ ld_lib_path: T.Set[str] = set()
+ for d in depends:
+ if isinstance(d, build.BuildTarget):
+ for l in d.get_all_link_deps():
+ if isinstance(l, build.SharedLibrary):
+ ld_lib_path.add(os.path.join(self.environment.get_build_dir(), l.get_subdir()))
+ if ld_lib_path:
+ t_env.prepend('LD_LIBRARY_PATH', list(ld_lib_path), ':')
+
+ ts = TestSerialisation(t.get_name(), t.project_name, t.suite, cmd, is_cross,
+ exe_wrapper, self.environment.need_exe_wrapper(),
+ t.is_parallel, cmd_args, t_env,
+ t.should_fail, t.timeout, t.workdir,
+ extra_paths, t.protocol, t.priority,
+ isinstance(exe, build.Target),
+ isinstance(exe, build.Executable),
+ [x.get_id() for x in depends],
+ self.environment.coredata.version,
+ t.verbose)
+ arr.append(ts)
+ return arr
+
+ def write_test_serialisation(self, tests: T.List['Test'], datafile: T.BinaryIO) -> None:
+ pickle.dump(self.create_test_serialisation(tests), datafile)
+
+ def construct_target_rel_paths(self, t: T.Union[build.Target, build.CustomTargetIndex], workdir: T.Optional[str]) -> T.List[str]:
+ target_dir = self.get_target_dir(t)
+ # ensure that test executables can be run when passed as arguments
+ if isinstance(t, build.Executable) and workdir is None:
+ target_dir = target_dir or '.'
+
+ if isinstance(t, build.BuildTarget):
+ outputs = [t.get_filename()]
+ else:
+ assert isinstance(t, (build.CustomTarget, build.CustomTargetIndex))
+ outputs = t.get_outputs()
+
+ outputs = [os.path.join(target_dir, x) for x in outputs]
+ if workdir is not None:
+ assert os.path.isabs(workdir)
+ outputs = [os.path.join(self.environment.get_build_dir(), x) for x in outputs]
+ outputs = [os.path.relpath(x, workdir) for x in outputs]
+ return outputs
+
+ def generate_depmf_install(self, d: InstallData) -> None:
+ if self.build.dep_manifest_name is None:
+ return
+ ifilename = os.path.join(self.environment.get_build_dir(), 'depmf.json')
+ ofilename = os.path.join(self.environment.get_prefix(), self.build.dep_manifest_name)
+ out_name = os.path.join('{prefix}', self.build.dep_manifest_name)
+ mfobj = {'type': 'dependency manifest', 'version': '1.0',
+ 'projects': {k: v.to_json() for k, v in self.build.dep_manifest.items()}}
+ with open(ifilename, 'w', encoding='utf-8') as f:
+ f.write(json.dumps(mfobj))
+ # Copy file from, to, and with mode unchanged
+ d.data.append(InstallDataBase(ifilename, ofilename, out_name, None, '',
+ tag='devel', data_type='depmf'))
+
+ def get_regen_filelist(self) -> T.List[str]:
+ '''List of all files whose alteration means that the build
+ definition needs to be regenerated.'''
+ deps = OrderedSet([str(Path(self.build_to_src) / df)
+ for df in self.interpreter.get_build_def_files()])
+ if self.environment.is_cross_build():
+ deps.update(self.environment.coredata.cross_files)
+ deps.update(self.environment.coredata.config_files)
+ deps.add('meson-private/coredata.dat')
+ self.check_clock_skew(deps)
+ return list(deps)
+
+ def generate_regen_info(self) -> None:
+ deps = self.get_regen_filelist()
+ regeninfo = RegenInfo(self.environment.get_source_dir(),
+ self.environment.get_build_dir(),
+ deps)
+ filename = os.path.join(self.environment.get_scratch_dir(),
+ 'regeninfo.dump')
+ with open(filename, 'wb') as f:
+ pickle.dump(regeninfo, f)
+
+ def check_clock_skew(self, file_list: T.Iterable[str]) -> None:
+ # If a file that leads to reconfiguration has a time
+ # stamp in the future, it will trigger an eternal reconfigure
+ # loop.
+ import time
+ now = time.time()
+ for f in file_list:
+ absf = os.path.join(self.environment.get_build_dir(), f)
+ ftime = os.path.getmtime(absf)
+ delta = ftime - now
+ # On Windows disk time stamps sometimes point
+ # to the future by a minuscule amount, less than
+ # 0.001 seconds. I don't know why.
+ if delta > 0.001:
+ raise MesonException(f'Clock skew detected. File {absf} has a time stamp {delta:.4f}s in the future.')
+
+ def build_target_to_cmd_array(self, bt: T.Union[build.BuildTarget, programs.ExternalProgram]) -> T.List[str]:
+ if isinstance(bt, build.BuildTarget):
+ arr = [os.path.join(self.environment.get_build_dir(), self.get_target_filename(bt))]
+ else:
+ arr = bt.get_command()
+ return arr
+
+ def replace_extra_args(self, args: T.List[str], genlist: 'build.GeneratedList') -> T.List[str]:
+ final_args: T.List[str] = []
+ for a in args:
+ if a == '@EXTRA_ARGS@':
+ final_args += genlist.get_extra_args()
+ else:
+ final_args.append(a)
+ return final_args
+
+ def replace_outputs(self, args: T.List[str], private_dir: str, output_list: T.List[str]) -> T.List[str]:
+ newargs: T.List[str] = []
+ regex = re.compile(r'@OUTPUT(\d+)@')
+ for arg in args:
+ m = regex.search(arg)
+ while m is not None:
+ index = int(m.group(1))
+ src = f'@OUTPUT{index}@'
+ arg = arg.replace(src, os.path.join(private_dir, output_list[index]))
+ m = regex.search(arg)
+ newargs.append(arg)
+ return newargs
+
+ def get_build_by_default_targets(self) -> 'T.OrderedDict[str, T.Union[build.BuildTarget, build.CustomTarget]]':
+ result: 'T.OrderedDict[str, T.Union[build.BuildTarget, build.CustomTarget]]' = OrderedDict()
+ # Get all build and custom targets that must be built by default
+ for name, b in self.build.get_targets().items():
+ if b.build_by_default:
+ result[name] = b
+ return result
+
+ def get_testlike_targets(self, benchmark: bool = False) -> T.OrderedDict[str, T.Union[build.BuildTarget, build.CustomTarget]]:
+ result: T.OrderedDict[str, T.Union[build.BuildTarget, build.CustomTarget]] = OrderedDict()
+ targets = self.build.get_benchmarks() if benchmark else self.build.get_tests()
+ for t in targets:
+ exe = t.exe
+ if isinstance(exe, (build.CustomTarget, build.BuildTarget)):
+ result[exe.get_id()] = exe
+ for arg in t.cmd_args:
+ if not isinstance(arg, (build.CustomTarget, build.BuildTarget)):
+ continue
+ result[arg.get_id()] = arg
+ for dep in t.depends:
+ assert isinstance(dep, (build.CustomTarget, build.BuildTarget))
+ result[dep.get_id()] = dep
+ return result
+
+ @lru_cache(maxsize=None)
+ def get_custom_target_provided_by_generated_source(self, generated_source: build.CustomTarget) -> 'ImmutableListProtocol[str]':
+ libs: T.List[str] = []
+ for f in generated_source.get_outputs():
+ if self.environment.is_library(f):
+ libs.append(os.path.join(self.get_target_dir(generated_source), f))
+ return libs
+
+ @lru_cache(maxsize=None)
+ def get_custom_target_provided_libraries(self, target: T.Union[build.BuildTarget, build.CustomTarget]) -> 'ImmutableListProtocol[str]':
+ libs: T.List[str] = []
+ for t in target.get_generated_sources():
+ if not isinstance(t, build.CustomTarget):
+ continue
+ libs.extend(self.get_custom_target_provided_by_generated_source(t))
+ return libs
+
+ def get_custom_target_sources(self, target: build.CustomTarget) -> T.List[str]:
+ '''
+ Custom target sources can be of various object types; strings, File,
+ BuildTarget, even other CustomTargets.
+ Returns the path to them relative to the build root directory.
+ '''
+ srcs: T.List[str] = []
+ for i in target.get_sources():
+ if isinstance(i, str):
+ fname = [os.path.join(self.build_to_src, target.subdir, i)]
+ elif isinstance(i, build.BuildTarget):
+ fname = [self.get_target_filename(i)]
+ elif isinstance(i, (build.CustomTarget, build.CustomTargetIndex)):
+ fname = [os.path.join(self.get_custom_target_output_dir(i), p) for p in i.get_outputs()]
+ elif isinstance(i, build.GeneratedList):
+ fname = [os.path.join(self.get_target_private_dir(target), p) for p in i.get_outputs()]
+ elif isinstance(i, build.ExtractedObjects):
+ fname = self.determine_ext_objs(i)
+ elif isinstance(i, programs.ExternalProgram):
+ assert i.found(), "This shouldn't be possible"
+ assert i.path is not None, 'for mypy'
+ fname = [i.path]
+ else:
+ fname = [i.rel_to_builddir(self.build_to_src)]
+ if target.absolute_paths:
+ fname = [os.path.join(self.environment.get_build_dir(), f) for f in fname]
+ srcs += fname
+ return srcs
+
+ def get_custom_target_depend_files(self, target: build.CustomTarget, absolute_paths: bool = False) -> T.List[str]:
+ deps: T.List[str] = []
+ for i in target.depend_files:
+ if isinstance(i, mesonlib.File):
+ if absolute_paths:
+ deps.append(i.absolute_path(self.environment.get_source_dir(),
+ self.environment.get_build_dir()))
+ else:
+ deps.append(i.rel_to_builddir(self.build_to_src))
+ else:
+ if absolute_paths:
+ deps.append(os.path.join(self.environment.get_source_dir(), target.subdir, i))
+ else:
+ deps.append(os.path.join(self.build_to_src, target.subdir, i))
+ return deps
+
+ def get_custom_target_output_dir(self, target: T.Union[build.Target, build.CustomTargetIndex]) -> str:
+ # The XCode backend is special. A target foo/bar does
+ # not go to ${BUILDDIR}/foo/bar but instead to
+ # ${BUILDDIR}/${BUILDTYPE}/foo/bar.
+ # Currently we set the include dir to be the former,
+ # and not the latter. Thus we need this extra customisation
+ # point. If in the future we make include dirs et al match
+ # ${BUILDDIR}/${BUILDTYPE} instead, this becomes unnecessary.
+ return self.get_target_dir(target)
+
+ @lru_cache(maxsize=None)
+ def get_normpath_target(self, source: str) -> str:
+ return os.path.normpath(source)
+
+ def get_custom_target_dirs(self, target: build.CustomTarget, compiler: 'Compiler', *,
+ absolute_path: bool = False) -> T.List[str]:
+ custom_target_include_dirs: T.List[str] = []
+ for i in target.get_generated_sources():
+ # Generator output goes into the target private dir which is
+ # already in the include paths list. Only custom targets have their
+ # own target build dir.
+ if not isinstance(i, (build.CustomTarget, build.CustomTargetIndex)):
+ continue
+ idir = self.get_normpath_target(self.get_custom_target_output_dir(i))
+ if not idir:
+ idir = '.'
+ if absolute_path:
+ idir = os.path.join(self.environment.get_build_dir(), idir)
+ if idir not in custom_target_include_dirs:
+ custom_target_include_dirs.append(idir)
+ return custom_target_include_dirs
+
+ def get_custom_target_dir_include_args(
+ self, target: build.CustomTarget, compiler: 'Compiler', *,
+ absolute_path: bool = False) -> T.List[str]:
+ incs: T.List[str] = []
+ for i in self.get_custom_target_dirs(target, compiler, absolute_path=absolute_path):
+ incs += compiler.get_include_args(i, False)
+ return incs
+
+ def eval_custom_target_command(
+ self, target: build.CustomTarget, absolute_outputs: bool = False) -> \
+ T.Tuple[T.List[str], T.List[str], T.List[str]]:
+ # We want the outputs to be absolute only when using the VS backend
+ # XXX: Maybe allow the vs backend to use relative paths too?
+ source_root = self.build_to_src
+ build_root = '.'
+ outdir = self.get_custom_target_output_dir(target)
+ if absolute_outputs:
+ source_root = self.environment.get_source_dir()
+ build_root = self.environment.get_build_dir()
+ outdir = os.path.join(self.environment.get_build_dir(), outdir)
+ outputs = [os.path.join(outdir, i) for i in target.get_outputs()]
+ inputs = self.get_custom_target_sources(target)
+ # Evaluate the command list
+ cmd: T.List[str] = []
+ for i in target.command:
+ if isinstance(i, build.BuildTarget):
+ cmd += self.build_target_to_cmd_array(i)
+ continue
+ elif isinstance(i, build.CustomTarget):
+ # GIR scanner will attempt to execute this binary but
+ # it assumes that it is in path, so always give it a full path.
+ tmp = i.get_outputs()[0]
+ i = os.path.join(self.get_custom_target_output_dir(i), tmp)
+ elif isinstance(i, mesonlib.File):
+ i = i.rel_to_builddir(self.build_to_src)
+ if target.absolute_paths or absolute_outputs:
+ i = os.path.join(self.environment.get_build_dir(), i)
+ # FIXME: str types are blindly added ignoring 'target.absolute_paths'
+ # because we can't know if they refer to a file or just a string
+ elif isinstance(i, str):
+ if '@SOURCE_ROOT@' in i:
+ i = i.replace('@SOURCE_ROOT@', source_root)
+ if '@BUILD_ROOT@' in i:
+ i = i.replace('@BUILD_ROOT@', build_root)
+ if '@CURRENT_SOURCE_DIR@' in i:
+ i = i.replace('@CURRENT_SOURCE_DIR@', os.path.join(source_root, target.subdir))
+ if '@DEPFILE@' in i:
+ if target.depfile is None:
+ msg = f'Custom target {target.name!r} has @DEPFILE@ but no depfile ' \
+ 'keyword argument.'
+ raise MesonException(msg)
+ dfilename = os.path.join(outdir, target.depfile)
+ i = i.replace('@DEPFILE@', dfilename)
+ if '@PRIVATE_DIR@' in i:
+ if target.absolute_paths:
+ pdir = self.get_target_private_dir_abs(target)
+ else:
+ pdir = self.get_target_private_dir(target)
+ i = i.replace('@PRIVATE_DIR@', pdir)
+ else:
+ raise RuntimeError(f'Argument {i} is of unknown type {type(i)}')
+ cmd.append(i)
+ # Substitute the rest of the template strings
+ values = mesonlib.get_filenames_templates_dict(inputs, outputs)
+ cmd = mesonlib.substitute_values(cmd, values)
+ # This should not be necessary but removing it breaks
+ # building GStreamer on Windows. The underlying issue
+ # is problems with quoting backslashes on Windows
+ # which is the seventh circle of hell. The downside is
+ # that this breaks custom targets whose command lines
+ # have backslashes. If you try to fix this be sure to
+ # check that it does not break GST.
+ #
+ # The bug causes file paths such as c:\foo to get escaped
+ # into c:\\foo.
+ #
+ # Unfortunately we have not been able to come up with an
+ # isolated test case for this so unless you manage to come up
+ # with one, the only way is to test the building with Gst's
+ # setup. Note this in your MR or ping us and we will get it
+ # fixed.
+ #
+ # https://github.com/mesonbuild/meson/pull/737
+ cmd = [i.replace('\\', '/') for i in cmd]
+ return inputs, outputs, cmd
+
+ def get_run_target_env(self, target: build.RunTarget) -> build.EnvironmentVariables:
+ env = target.env if target.env else build.EnvironmentVariables()
+ if target.default_env:
+ introspect_cmd = join_args(self.environment.get_build_command() + ['introspect'])
+ env.set('MESON_SOURCE_ROOT', [self.environment.get_source_dir()])
+ env.set('MESON_BUILD_ROOT', [self.environment.get_build_dir()])
+ env.set('MESON_SUBDIR', [target.subdir])
+ env.set('MESONINTROSPECT', [introspect_cmd])
+ return env
+
+ def run_postconf_scripts(self) -> None:
+ from ..scripts.meson_exe import run_exe
+ introspect_cmd = join_args(self.environment.get_build_command() + ['introspect'])
+ env = {'MESON_SOURCE_ROOT': self.environment.get_source_dir(),
+ 'MESON_BUILD_ROOT': self.environment.get_build_dir(),
+ 'MESONINTROSPECT': introspect_cmd,
+ }
+
+ for s in self.build.postconf_scripts:
+ name = ' '.join(s.cmd_args)
+ mlog.log(f'Running postconf script {name!r}')
+ run_exe(s, env)
+
+ @lru_cache(maxsize=1)
+ def create_install_data(self) -> InstallData:
+ strip_bin = self.environment.lookup_binary_entry(MachineChoice.HOST, 'strip')
+ if strip_bin is None:
+ if self.environment.is_cross_build():
+ mlog.warning('Cross file does not specify strip binary, result will not be stripped.')
+ else:
+ # TODO go through all candidates, like others
+ strip_bin = [detect.defaults['strip'][0]]
+
+ umask = self.environment.coredata.get_option(OptionKey('install_umask'))
+ assert isinstance(umask, (str, int)), 'for mypy'
+
+ d = InstallData(self.environment.get_source_dir(),
+ self.environment.get_build_dir(),
+ self.environment.get_prefix(),
+ self.environment.get_libdir(),
+ strip_bin,
+ umask,
+ self.environment.get_build_command() + ['introspect'],
+ self.environment.coredata.version)
+ self.generate_depmf_install(d)
+ self.generate_target_install(d)
+ self.generate_header_install(d)
+ self.generate_man_install(d)
+ self.generate_emptydir_install(d)
+ self.generate_data_install(d)
+ self.generate_symlink_install(d)
+ self.generate_custom_install_script(d)
+ self.generate_subdir_install(d)
+ return d
+
+ def create_install_data_files(self) -> None:
+ install_data_file = os.path.join(self.environment.get_scratch_dir(), 'install.dat')
+ with open(install_data_file, 'wb') as ofile:
+ pickle.dump(self.create_install_data(), ofile)
+
+ def guess_install_tag(self, fname: str, outdir: T.Optional[str] = None) -> T.Optional[str]:
+ prefix = self.environment.get_prefix()
+ bindir = Path(prefix, self.environment.get_bindir())
+ libdir = Path(prefix, self.environment.get_libdir())
+ incdir = Path(prefix, self.environment.get_includedir())
+ _ldir = self.environment.coredata.get_option(mesonlib.OptionKey('localedir'))
+ assert isinstance(_ldir, str), 'for mypy'
+ localedir = Path(prefix, _ldir)
+ dest_path = Path(prefix, outdir, Path(fname).name) if outdir else Path(prefix, fname)
+ if bindir in dest_path.parents:
+ return 'runtime'
+ elif libdir in dest_path.parents:
+ if dest_path.suffix in {'.a', '.pc'}:
+ return 'devel'
+ elif dest_path.suffix in {'.so', '.dll'}:
+ return 'runtime'
+ elif incdir in dest_path.parents:
+ return 'devel'
+ elif localedir in dest_path.parents:
+ return 'i18n'
+ elif 'installed-tests' in dest_path.parts:
+ return 'tests'
+ elif 'systemtap' in dest_path.parts:
+ return 'systemtap'
+ mlog.debug('Failed to guess install tag for', dest_path)
+ return None
+
+ def generate_target_install(self, d: InstallData) -> None:
+ for t in self.build.get_targets().values():
+ if not t.should_install():
+ continue
+ outdirs, install_dir_names, custom_install_dir = t.get_install_dir()
+ # Sanity-check the outputs and install_dirs
+ num_outdirs, num_out = len(outdirs), len(t.get_outputs())
+ if num_outdirs not in {1, num_out}:
+ m = 'Target {!r} has {} outputs: {!r}, but only {} "install_dir"s were found.\n' \
+ "Pass 'false' for outputs that should not be installed and 'true' for\n" \
+ 'using the default installation directory for an output.'
+ raise MesonException(m.format(t.name, num_out, t.get_outputs(), num_outdirs))
+ assert len(t.install_tag) == num_out
+ install_mode = t.get_custom_install_mode()
+ # because mypy gets confused type narrowing in lists
+ first_outdir = outdirs[0]
+ first_outdir_name = install_dir_names[0]
+
+ # Install the target output(s)
+ if isinstance(t, build.BuildTarget):
+ # In general, stripping static archives is tricky and full of pitfalls.
+ # Wholesale stripping of static archives with a command such as
+ #
+ # strip libfoo.a
+ #
+ # is broken, as GNU's strip will remove *every* symbol in a static
+ # archive. One solution to this nonintuitive behaviour would be
+ # to only strip local/debug symbols. Unfortunately, strip arguments
+ # are not specified by POSIX and therefore not portable. GNU's `-g`
+ # option (i.e. remove debug symbols) is equivalent to Apple's `-S`.
+ #
+ # TODO: Create GNUStrip/AppleStrip/etc. hierarchy for more
+ # fine-grained stripping of static archives.
+ can_strip = not isinstance(t, build.StaticLibrary)
+ should_strip = can_strip and t.get_option(OptionKey('strip'))
+ assert isinstance(should_strip, bool), 'for mypy'
+ # Install primary build output (library/executable/jar, etc)
+ # Done separately because of strip/aliases/rpath
+ if first_outdir is not False:
+ tag = t.install_tag[0] or ('devel' if isinstance(t, build.StaticLibrary) else 'runtime')
+ mappings = t.get_link_deps_mapping(d.prefix)
+ i = TargetInstallData(self.get_target_filename(t), first_outdir,
+ first_outdir_name,
+ should_strip, mappings, t.rpath_dirs_to_remove,
+ t.install_rpath, install_mode, t.subproject,
+ tag=tag, can_strip=can_strip)
+ d.targets.append(i)
+
+ for alias, to, tag in t.get_aliases():
+ alias = os.path.join(first_outdir, alias)
+ s = InstallSymlinkData(to, alias, first_outdir, t.subproject, tag, allow_missing=True)
+ d.symlinks.append(s)
+
+ if isinstance(t, (build.SharedLibrary, build.SharedModule, build.Executable)):
+ # On toolchains/platforms that use an import library for
+ # linking (separate from the shared library with all the
+ # code), we need to install that too (dll.a/.lib).
+ if t.get_import_filename():
+ if custom_install_dir:
+ # If the DLL is installed into a custom directory,
+ # install the import library into the same place so
+ # it doesn't go into a surprising place
+ implib_install_dir = first_outdir
+ else:
+ implib_install_dir = self.environment.get_import_lib_dir()
+ # Install the import library; may not exist for shared modules
+ i = TargetInstallData(self.get_target_filename_for_linking(t),
+ implib_install_dir, first_outdir_name,
+ False, {}, set(), '', install_mode,
+ t.subproject, optional=isinstance(t, build.SharedModule),
+ tag='devel')
+ d.targets.append(i)
+
+ if not should_strip and t.get_debug_filename():
+ debug_file = os.path.join(self.get_target_dir(t), t.get_debug_filename())
+ i = TargetInstallData(debug_file, first_outdir,
+ first_outdir_name,
+ False, {}, set(), '',
+ install_mode, t.subproject,
+ optional=True, tag='devel')
+ d.targets.append(i)
+ # Install secondary outputs. Only used for Vala right now.
+ if num_outdirs > 1:
+ for output, outdir, outdir_name, tag in zip(t.get_outputs()[1:], outdirs[1:], install_dir_names[1:], t.install_tag[1:]):
+ # User requested that we not install this output
+ if outdir is False:
+ continue
+ f = os.path.join(self.get_target_dir(t), output)
+ i = TargetInstallData(f, outdir, outdir_name, False, {}, set(), None,
+ install_mode, t.subproject,
+ tag=tag)
+ d.targets.append(i)
+ elif isinstance(t, build.CustomTarget):
+ # If only one install_dir is specified, assume that all
+ # outputs will be installed into it. This is for
+ # backwards-compatibility and because it makes sense to
+ # avoid repetition since this is a common use-case.
+ #
+ # To selectively install only some outputs, pass `false` as
+ # the install_dir for the corresponding output by index
+ #
+ # XXX: this wouldn't be needed if we just always matches outdirs
+ # to the length of outputs…
+ if num_outdirs == 1 and num_out > 1:
+ if first_outdir is not False:
+ for output, tag in zip(t.get_outputs(), t.install_tag):
+ tag = tag or self.guess_install_tag(output, first_outdir)
+ f = os.path.join(self.get_target_dir(t), output)
+ i = TargetInstallData(f, first_outdir, first_outdir_name,
+ False, {}, set(), None, install_mode,
+ t.subproject, optional=not t.build_by_default,
+ tag=tag)
+ d.targets.append(i)
+ else:
+ for output, outdir, outdir_name, tag in zip(t.get_outputs(), outdirs, install_dir_names, t.install_tag):
+ # User requested that we not install this output
+ if outdir is False:
+ continue
+ tag = tag or self.guess_install_tag(output, outdir)
+ f = os.path.join(self.get_target_dir(t), output)
+ i = TargetInstallData(f, outdir, outdir_name,
+ False, {}, set(), None, install_mode,
+ t.subproject, optional=not t.build_by_default,
+ tag=tag)
+ d.targets.append(i)
+
+ def generate_custom_install_script(self, d: InstallData) -> None:
+ d.install_scripts = self.build.install_scripts
+ for i in d.install_scripts:
+ if not i.tag:
+ mlog.debug('Failed to guess install tag for install script:', ' '.join(i.cmd_args))
+
+ def generate_header_install(self, d: InstallData) -> None:
+ incroot = self.environment.get_includedir()
+ headers = self.build.get_headers()
+
+ srcdir = self.environment.get_source_dir()
+ builddir = self.environment.get_build_dir()
+ for h in headers:
+ outdir = outdir_name = h.get_custom_install_dir()
+ if outdir is None:
+ subdir = h.get_install_subdir()
+ if subdir is None:
+ outdir = incroot
+ outdir_name = '{includedir}'
+ else:
+ outdir = os.path.join(incroot, subdir)
+ outdir_name = os.path.join('{includedir}', subdir)
+
+ for f in h.get_sources():
+ if not isinstance(f, File):
+ raise MesonException(f'Invalid header type {f!r} can\'t be installed')
+ abspath = f.absolute_path(srcdir, builddir)
+ i = InstallDataBase(abspath, outdir, outdir_name, h.get_custom_install_mode(), h.subproject, tag='devel')
+ d.headers.append(i)
+
+ def generate_man_install(self, d: InstallData) -> None:
+ manroot = self.environment.get_mandir()
+ man = self.build.get_man()
+ for m in man:
+ for f in m.get_sources():
+ num = f.split('.')[-1]
+ subdir = m.get_custom_install_dir()
+ if subdir is None:
+ if m.locale:
+ subdir = os.path.join('{mandir}', m.locale, 'man' + num)
+ else:
+ subdir = os.path.join('{mandir}', 'man' + num)
+ fname = f.fname
+ if m.locale: # strip locale from file name
+ fname = fname.replace(f'.{m.locale}', '')
+ srcabs = f.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir())
+ dstname = os.path.join(subdir, os.path.basename(fname))
+ dstabs = dstname.replace('{mandir}', manroot)
+ i = InstallDataBase(srcabs, dstabs, dstname, m.get_custom_install_mode(), m.subproject, tag='man')
+ d.man.append(i)
+
+ def generate_emptydir_install(self, d: InstallData) -> None:
+ emptydir: T.List[build.EmptyDir] = self.build.get_emptydir()
+ for e in emptydir:
+ tag = e.install_tag or self.guess_install_tag(e.path)
+ i = InstallEmptyDir(e.path, e.install_mode, e.subproject, tag)
+ d.emptydir.append(i)
+
+ def generate_data_install(self, d: InstallData) -> None:
+ data = self.build.get_data()
+ srcdir = self.environment.get_source_dir()
+ builddir = self.environment.get_build_dir()
+ for de in data:
+ assert isinstance(de, build.Data)
+ subdir = de.install_dir
+ subdir_name = de.install_dir_name
+ if not subdir:
+ subdir = os.path.join(self.environment.get_datadir(), self.interpreter.build.project_name)
+ subdir_name = os.path.join('{datadir}', self.interpreter.build.project_name)
+ for src_file, dst_name in zip(de.sources, de.rename):
+ assert isinstance(src_file, mesonlib.File)
+ dst_abs = os.path.join(subdir, dst_name)
+ dstdir_name = os.path.join(subdir_name, dst_name)
+ tag = de.install_tag or self.guess_install_tag(dst_abs)
+ i = InstallDataBase(src_file.absolute_path(srcdir, builddir), dst_abs, dstdir_name,
+ de.install_mode, de.subproject, tag=tag, data_type=de.data_type)
+ d.data.append(i)
+
+ def generate_symlink_install(self, d: InstallData) -> None:
+ links: T.List[build.SymlinkData] = self.build.get_symlinks()
+ for l in links:
+ assert isinstance(l, build.SymlinkData)
+ install_dir = l.install_dir
+ name_abs = os.path.join(install_dir, l.name)
+ tag = l.install_tag or self.guess_install_tag(name_abs)
+ s = InstallSymlinkData(l.target, name_abs, install_dir, l.subproject, tag)
+ d.symlinks.append(s)
+
+ def generate_subdir_install(self, d: InstallData) -> None:
+ for sd in self.build.get_install_subdirs():
+ if sd.from_source_dir:
+ from_dir = self.environment.get_source_dir()
+ else:
+ from_dir = self.environment.get_build_dir()
+ src_dir = os.path.join(from_dir,
+ sd.source_subdir,
+ sd.installable_subdir).rstrip('/')
+ dst_dir = os.path.join(self.environment.get_prefix(),
+ sd.install_dir)
+ dst_name = os.path.join('{prefix}', sd.install_dir)
+ if sd.install_dir != sd.install_dir_name:
+ dst_name = sd.install_dir_name
+ if not sd.strip_directory:
+ dst_dir = os.path.join(dst_dir, os.path.basename(src_dir))
+ dst_name = os.path.join(dst_name, os.path.basename(src_dir))
+ tag = sd.install_tag or self.guess_install_tag(os.path.join(sd.install_dir, 'dummy'))
+ i = SubdirInstallData(src_dir, dst_dir, dst_name, sd.install_mode, sd.exclude, sd.subproject, tag)
+ d.install_subdirs.append(i)
+
+ def get_introspection_data(self, target_id: str, target: build.Target) -> T.List['TargetIntrospectionData']:
+ '''
+ Returns a list of source dicts with the following format for a given target:
+ [
+ {
+ "language": "<LANG>",
+ "compiler": ["result", "of", "comp.get_exelist()"],
+ "parameters": ["list", "of", "compiler", "parameters],
+ "sources": ["list", "of", "all", "<LANG>", "source", "files"],
+ "generated_sources": ["list", "of", "generated", "source", "files"]
+ }
+ ]
+
+ This is a limited fallback / reference implementation. The backend should override this method.
+ '''
+ if isinstance(target, (build.CustomTarget, build.BuildTarget)):
+ source_list_raw = target.sources
+ source_list = []
+ for j in source_list_raw:
+ if isinstance(j, mesonlib.File):
+ source_list += [j.absolute_path(self.source_dir, self.build_dir)]
+ elif isinstance(j, str):
+ source_list += [os.path.join(self.source_dir, j)]
+ elif isinstance(j, (build.CustomTarget, build.BuildTarget)):
+ source_list += [os.path.join(self.build_dir, j.get_subdir(), o) for o in j.get_outputs()]
+ source_list = [os.path.normpath(s) for s in source_list]
+
+ compiler: T.List[str] = []
+ if isinstance(target, build.CustomTarget):
+ tmp_compiler = target.command
+ for j in tmp_compiler:
+ if isinstance(j, mesonlib.File):
+ compiler += [j.absolute_path(self.source_dir, self.build_dir)]
+ elif isinstance(j, str):
+ compiler += [j]
+ elif isinstance(j, (build.BuildTarget, build.CustomTarget)):
+ compiler += j.get_outputs()
+ else:
+ raise RuntimeError(f'Type "{type(j).__name__}" is not supported in get_introspection_data. This is a bug')
+
+ return [{
+ 'language': 'unknown',
+ 'compiler': compiler,
+ 'parameters': [],
+ 'sources': source_list,
+ 'generated_sources': []
+ }]
+
+ return []
+
+ def get_devenv(self) -> build.EnvironmentVariables:
+ env = build.EnvironmentVariables()
+ extra_paths = set()
+ library_paths = set()
+ build_machine = self.environment.machines[MachineChoice.BUILD]
+ host_machine = self.environment.machines[MachineChoice.HOST]
+ need_wine = not build_machine.is_windows() and host_machine.is_windows()
+ for t in self.build.get_targets().values():
+ in_default_dir = t.should_install() and not t.get_install_dir()[2]
+ if t.for_machine != MachineChoice.HOST or not in_default_dir:
+ continue
+ tdir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(t))
+ if isinstance(t, build.Executable):
+ # Add binaries that are going to be installed in bindir into PATH
+ # so they get used by default instead of searching on system when
+ # in developer environment.
+ extra_paths.add(tdir)
+ if host_machine.is_windows() or host_machine.is_cygwin():
+ # On windows we cannot rely on rpath to run executables from build
+ # directory. We have to add in PATH the location of every DLL needed.
+ library_paths.update(self.determine_windows_extra_paths(t, []))
+ elif isinstance(t, build.SharedLibrary):
+ # Add libraries that are going to be installed in libdir into
+ # LD_LIBRARY_PATH. This allows running system applications using
+ # that library.
+ library_paths.add(tdir)
+ if need_wine:
+ # Executable paths should be in both PATH and WINEPATH.
+ # - Having them in PATH makes bash completion find it,
+ # and make running "foo.exe" find it when wine-binfmt is installed.
+ # - Having them in WINEPATH makes "wine foo.exe" find it.
+ library_paths.update(extra_paths)
+ if library_paths:
+ if need_wine:
+ env.prepend('WINEPATH', list(library_paths), separator=';')
+ elif host_machine.is_windows() or host_machine.is_cygwin():
+ extra_paths.update(library_paths)
+ elif host_machine.is_darwin():
+ env.prepend('DYLD_LIBRARY_PATH', list(library_paths))
+ else:
+ env.prepend('LD_LIBRARY_PATH', list(library_paths))
+ if extra_paths:
+ env.prepend('PATH', list(extra_paths))
+ return env
+
+ def compiler_to_generator(self, target: build.BuildTarget,
+ compiler: 'Compiler',
+ sources: _ALL_SOURCES_TYPE,
+ output_templ: str) -> build.GeneratedList:
+ '''
+ Some backends don't support custom compilers. This is a convenience
+ method to convert a Compiler to a Generator.
+ '''
+ exelist = compiler.get_exelist()
+ exe = programs.ExternalProgram(exelist[0])
+ args = exelist[1:]
+ # FIXME: There are many other args missing
+ commands = self.generate_basic_compiler_args(target, compiler)
+ commands += compiler.get_dependency_gen_args('@OUTPUT@', '@DEPFILE@')
+ commands += compiler.get_output_args('@OUTPUT@')
+ commands += compiler.get_compile_only_args() + ['@INPUT@']
+ commands += self.get_source_dir_include_args(target, compiler)
+ commands += self.get_build_dir_include_args(target, compiler)
+ generator = build.Generator(exe, args + commands.to_native(), [output_templ], depfile='@PLAINNAME@.d')
+ return generator.process_files(sources, self.interpreter)
+
+ def compile_target_to_generator(self, target: build.CompileTarget) -> build.GeneratedList:
+ all_sources = T.cast('_ALL_SOURCES_TYPE', target.sources) + T.cast('_ALL_SOURCES_TYPE', target.generated)
+ return self.compiler_to_generator(target, target.compiler, all_sources, target.output_templ)
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
new file mode 100644
index 0000000..3d6bfb9
--- /dev/null
+++ b/mesonbuild/backend/ninjabackend.py
@@ -0,0 +1,3623 @@
+# 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.
+from __future__ import annotations
+
+from collections import OrderedDict
+from dataclasses import dataclass
+from enum import Enum, unique
+from functools import lru_cache
+from pathlib import PurePath, Path
+from textwrap import dedent
+import itertools
+import json
+import os
+import pickle
+import re
+import shlex
+import subprocess
+import typing as T
+
+from . import backends
+from .. import modules
+from ..modules import gnome
+from .. import environment, mesonlib
+from .. import build
+from .. import mlog
+from .. import compilers
+from ..arglist import CompilerArgs
+from ..compilers import Compiler
+from ..linkers import ArLinker, RSPFileSyntax
+from ..mesonlib import (
+ File, LibType, MachineChoice, MesonException, OrderedSet, PerMachine,
+ ProgressBar, quote_arg
+)
+from ..mesonlib import get_compiler_for_source, has_path_sep, OptionKey
+from .backends import CleanTrees
+from ..build import GeneratedList, InvalidArguments
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from .._typing import ImmutableListProtocol
+ from ..build import ExtractedObjects
+ from ..interpreter import Interpreter
+ from ..linkers import DynamicLinker, StaticLinker
+ from ..compilers.cs import CsCompiler
+ from ..compilers.fortran import FortranCompiler
+
+ RUST_EDITIONS = Literal['2015', '2018', '2021']
+
+
+FORTRAN_INCLUDE_PAT = r"^\s*#?include\s*['\"](\w+\.\w+)['\"]"
+FORTRAN_MODULE_PAT = r"^\s*\bmodule\b\s+(\w+)\s*(?:!+.*)*$"
+FORTRAN_SUBMOD_PAT = r"^\s*\bsubmodule\b\s*\((\w+:?\w+)\)\s*(\w+)"
+FORTRAN_USE_PAT = r"^\s*use,?\s*(?:non_intrinsic)?\s*(?:::)?\s*(\w+)"
+
+def cmd_quote(s):
+ # see: https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
+
+ # backslash escape any existing double quotes
+ # any existing backslashes preceding a quote are doubled
+ s = re.sub(r'(\\*)"', lambda m: '\\' * (len(m.group(1)) * 2 + 1) + '"', s)
+ # any terminal backslashes likewise need doubling
+ s = re.sub(r'(\\*)$', lambda m: '\\' * (len(m.group(1)) * 2), s)
+ # and double quote
+ s = f'"{s}"'
+
+ return s
+
+def gcc_rsp_quote(s):
+ # see: the function buildargv() in libiberty
+ #
+ # this differs from sh-quoting in that a backslash *always* escapes the
+ # following character, even inside single quotes.
+
+ s = s.replace('\\', '\\\\')
+
+ return shlex.quote(s)
+
+# How ninja executes command lines differs between Unix and Windows
+# (see https://ninja-build.org/manual.html#ref_rule_command)
+if mesonlib.is_windows():
+ quote_func = cmd_quote
+ execute_wrapper = ['cmd', '/c'] # unused
+ rmfile_prefix = ['del', '/f', '/s', '/q', '{}', '&&']
+else:
+ quote_func = quote_arg
+ execute_wrapper = []
+ rmfile_prefix = ['rm', '-f', '{}', '&&']
+
+
+def get_rsp_threshold():
+ '''Return a conservative estimate of the commandline size in bytes
+ above which a response file should be used. May be overridden for
+ debugging by setting environment variable MESON_RSP_THRESHOLD.'''
+
+ if mesonlib.is_windows():
+ # Usually 32k, but some projects might use cmd.exe,
+ # and that has a limit of 8k.
+ limit = 8192
+ else:
+ # On Linux, ninja always passes the commandline as a single
+ # big string to /bin/sh, and the kernel limits the size of a
+ # single argument; see MAX_ARG_STRLEN
+ limit = 131072
+ # Be conservative
+ limit = limit / 2
+ return int(os.environ.get('MESON_RSP_THRESHOLD', limit))
+
+# a conservative estimate of the command-line length limit
+rsp_threshold = get_rsp_threshold()
+
+# ninja variables whose value should remain unquoted. The value of these ninja
+# variables (or variables we use them in) is interpreted directly by ninja
+# (e.g. the value of the depfile variable is a pathname that ninja will read
+# from, etc.), so it must not be shell quoted.
+raw_names = {'DEPFILE_UNQUOTED', 'DESC', 'pool', 'description', 'targetdep', 'dyndep'}
+
+NINJA_QUOTE_BUILD_PAT = re.compile(r"[$ :\n]")
+NINJA_QUOTE_VAR_PAT = re.compile(r"[$ \n]")
+
+def ninja_quote(text: str, is_build_line=False) -> str:
+ if is_build_line:
+ quote_re = NINJA_QUOTE_BUILD_PAT
+ else:
+ quote_re = NINJA_QUOTE_VAR_PAT
+ # Fast path for when no quoting is necessary
+ if not quote_re.search(text):
+ return text
+ if '\n' in text:
+ errmsg = f'''Ninja does not support newlines in rules. The content was:
+
+{text}
+
+Please report this error with a test case to the Meson bug tracker.'''
+ raise MesonException(errmsg)
+ return quote_re.sub(r'$\g<0>', text)
+
+class TargetDependencyScannerInfo:
+ def __init__(self, private_dir: str, source2object: T.Dict[str, str]):
+ self.private_dir = private_dir
+ self.source2object = source2object
+
+@unique
+class Quoting(Enum):
+ both = 0
+ notShell = 1
+ notNinja = 2
+ none = 3
+
+class NinjaCommandArg:
+ def __init__(self, s, quoting = Quoting.both):
+ self.s = s
+ self.quoting = quoting
+
+ def __str__(self):
+ return self.s
+
+ @staticmethod
+ def list(l, q):
+ return [NinjaCommandArg(i, q) for i in l]
+
+class NinjaComment:
+ def __init__(self, comment):
+ self.comment = comment
+
+ def write(self, outfile):
+ for l in self.comment.split('\n'):
+ outfile.write('# ')
+ outfile.write(l)
+ outfile.write('\n')
+ outfile.write('\n')
+
+class NinjaRule:
+ def __init__(self, rule, command, args, description,
+ rspable = False, deps = None, depfile = None, extra = None,
+ rspfile_quote_style: RSPFileSyntax = RSPFileSyntax.GCC):
+
+ def strToCommandArg(c):
+ if isinstance(c, NinjaCommandArg):
+ return c
+
+ # deal with common cases here, so we don't have to explicitly
+ # annotate the required quoting everywhere
+ if c == '&&':
+ # shell constructs shouldn't be shell quoted
+ return NinjaCommandArg(c, Quoting.notShell)
+ if c.startswith('$'):
+ var = re.search(r'\$\{?(\w*)\}?', c).group(1)
+ if var not in raw_names:
+ # ninja variables shouldn't be ninja quoted, and their value
+ # is already shell quoted
+ return NinjaCommandArg(c, Quoting.none)
+ else:
+ # shell quote the use of ninja variables whose value must
+ # not be shell quoted (as it also used by ninja)
+ return NinjaCommandArg(c, Quoting.notNinja)
+
+ return NinjaCommandArg(c)
+
+ self.name = rule
+ self.command = [strToCommandArg(c) for c in command] # includes args which never go into a rspfile
+ self.args = [strToCommandArg(a) for a in args] # args which will go into a rspfile, if used
+ self.description = description
+ self.deps = deps # depstyle 'gcc' or 'msvc'
+ self.depfile = depfile
+ self.extra = extra
+ self.rspable = rspable # if a rspfile can be used
+ self.refcount = 0
+ self.rsprefcount = 0
+ self.rspfile_quote_style = rspfile_quote_style
+
+ if self.depfile == '$DEPFILE':
+ self.depfile += '_UNQUOTED'
+
+ @staticmethod
+ def _quoter(x, qf = quote_func):
+ if isinstance(x, NinjaCommandArg):
+ if x.quoting == Quoting.none:
+ return x.s
+ elif x.quoting == Quoting.notNinja:
+ return qf(x.s)
+ elif x.quoting == Quoting.notShell:
+ return ninja_quote(x.s)
+ # fallthrough
+ return ninja_quote(qf(str(x)))
+
+ def write(self, outfile):
+ if self.rspfile_quote_style is RSPFileSyntax.MSVC:
+ rspfile_quote_func = cmd_quote
+ else:
+ rspfile_quote_func = gcc_rsp_quote
+
+ def rule_iter():
+ if self.refcount:
+ yield ''
+ if self.rsprefcount:
+ yield '_RSP'
+
+ for rsp in rule_iter():
+ outfile.write(f'rule {self.name}{rsp}\n')
+ if rsp == '_RSP':
+ outfile.write(' command = {} @$out.rsp\n'.format(' '.join([self._quoter(x) for x in self.command])))
+ outfile.write(' rspfile = $out.rsp\n')
+ outfile.write(' rspfile_content = {}\n'.format(' '.join([self._quoter(x, rspfile_quote_func) for x in self.args])))
+ else:
+ outfile.write(' command = {}\n'.format(' '.join([self._quoter(x) for x in self.command + self.args])))
+ if self.deps:
+ outfile.write(f' deps = {self.deps}\n')
+ if self.depfile:
+ outfile.write(f' depfile = {self.depfile}\n')
+ outfile.write(f' description = {self.description}\n')
+ if self.extra:
+ for l in self.extra.split('\n'):
+ outfile.write(' ')
+ outfile.write(l)
+ outfile.write('\n')
+ outfile.write('\n')
+
+ def length_estimate(self, infiles, outfiles, elems):
+ # determine variables
+ # this order of actions only approximates ninja's scoping rules, as
+ # documented at: https://ninja-build.org/manual.html#ref_scope
+ ninja_vars = {}
+ for e in elems:
+ (name, value) = e
+ ninja_vars[name] = value
+ ninja_vars['deps'] = self.deps
+ ninja_vars['depfile'] = self.depfile
+ ninja_vars['in'] = infiles
+ ninja_vars['out'] = outfiles
+
+ # expand variables in command
+ command = ' '.join([self._quoter(x) for x in self.command + self.args])
+ estimate = len(command)
+ for m in re.finditer(r'(\${\w+}|\$\w+)?[^$]*', command):
+ if m.start(1) != -1:
+ estimate -= m.end(1) - m.start(1) + 1
+ chunk = m.group(1)
+ if chunk[1] == '{':
+ chunk = chunk[2:-1]
+ else:
+ chunk = chunk[1:]
+ chunk = ninja_vars.get(chunk, []) # undefined ninja variables are empty
+ estimate += len(' '.join(chunk))
+
+ # determine command length
+ return estimate
+
+class NinjaBuildElement:
+ def __init__(self, all_outputs, outfilenames, rulename, infilenames, implicit_outs=None):
+ self.implicit_outfilenames = implicit_outs or []
+ if isinstance(outfilenames, str):
+ self.outfilenames = [outfilenames]
+ else:
+ self.outfilenames = outfilenames
+ assert isinstance(rulename, str)
+ self.rulename = rulename
+ if isinstance(infilenames, str):
+ self.infilenames = [infilenames]
+ else:
+ self.infilenames = infilenames
+ self.deps = OrderedSet()
+ self.orderdeps = OrderedSet()
+ self.elems = []
+ self.all_outputs = all_outputs
+ self.output_errors = ''
+
+ def add_dep(self, dep):
+ if isinstance(dep, list):
+ self.deps.update(dep)
+ else:
+ self.deps.add(dep)
+
+ def add_orderdep(self, dep):
+ if isinstance(dep, list):
+ self.orderdeps.update(dep)
+ else:
+ self.orderdeps.add(dep)
+
+ def add_item(self, name, elems):
+ # Always convert from GCC-style argument naming to the naming used by the
+ # current compiler. Also filter system include paths, deduplicate, etc.
+ if isinstance(elems, CompilerArgs):
+ elems = elems.to_native()
+ if isinstance(elems, str):
+ elems = [elems]
+ self.elems.append((name, elems))
+
+ if name == 'DEPFILE':
+ self.elems.append((name + '_UNQUOTED', elems))
+
+ def _should_use_rspfile(self):
+ # 'phony' is a rule built-in to ninja
+ if self.rulename == 'phony':
+ return False
+
+ if not self.rule.rspable:
+ return False
+
+ infilenames = ' '.join([ninja_quote(i, True) for i in self.infilenames])
+ outfilenames = ' '.join([ninja_quote(i, True) for i in self.outfilenames])
+
+ return self.rule.length_estimate(infilenames,
+ outfilenames,
+ self.elems) >= rsp_threshold
+
+ def count_rule_references(self):
+ if self.rulename != 'phony':
+ if self._should_use_rspfile():
+ self.rule.rsprefcount += 1
+ else:
+ self.rule.refcount += 1
+
+ def write(self, outfile):
+ if self.output_errors:
+ raise MesonException(self.output_errors)
+ ins = ' '.join([ninja_quote(i, True) for i in self.infilenames])
+ outs = ' '.join([ninja_quote(i, True) for i in self.outfilenames])
+ implicit_outs = ' '.join([ninja_quote(i, True) for i in self.implicit_outfilenames])
+ if implicit_outs:
+ implicit_outs = ' | ' + implicit_outs
+ use_rspfile = self._should_use_rspfile()
+ if use_rspfile:
+ rulename = self.rulename + '_RSP'
+ mlog.debug(f'Command line for building {self.outfilenames} is long, using a response file')
+ else:
+ rulename = self.rulename
+ line = f'build {outs}{implicit_outs}: {rulename} {ins}'
+ if len(self.deps) > 0:
+ line += ' | ' + ' '.join([ninja_quote(x, True) for x in sorted(self.deps)])
+ if len(self.orderdeps) > 0:
+ line += ' || ' + ' '.join([ninja_quote(x, True) for x in sorted(self.orderdeps)])
+ line += '\n'
+ # This is the only way I could find to make this work on all
+ # platforms including Windows command shell. Slash is a dir separator
+ # on Windows, too, so all characters are unambiguous and, more importantly,
+ # do not require quoting, unless explicitly specified, which is necessary for
+ # the csc compiler.
+ line = line.replace('\\', '/')
+ if mesonlib.is_windows():
+ # Support network paths as backslash, otherwise they are interpreted as
+ # arguments for compile/link commands when using MSVC
+ line = ' '.join(
+ (l.replace('//', '\\\\', 1) if l.startswith('//') else l)
+ for l in line.split(' ')
+ )
+ outfile.write(line)
+
+ if use_rspfile:
+ if self.rule.rspfile_quote_style is RSPFileSyntax.MSVC:
+ qf = cmd_quote
+ else:
+ qf = gcc_rsp_quote
+ else:
+ qf = quote_func
+
+ for e in self.elems:
+ (name, elems) = e
+ should_quote = name not in raw_names
+ line = f' {name} = '
+ newelems = []
+ for i in elems:
+ if not should_quote or i == '&&': # Hackety hack hack
+ newelems.append(ninja_quote(i))
+ else:
+ newelems.append(ninja_quote(qf(i)))
+ line += ' '.join(newelems)
+ line += '\n'
+ outfile.write(line)
+ outfile.write('\n')
+
+ def check_outputs(self):
+ for n in self.outfilenames:
+ if n in self.all_outputs:
+ self.output_errors = f'Multiple producers for Ninja target "{n}". Please rename your targets.'
+ self.all_outputs[n] = True
+
+@dataclass
+class RustDep:
+
+ name: str
+
+ # equal to the order value of the `RustCrate`
+ crate: int
+
+ def to_json(self) -> T.Dict[str, object]:
+ return {
+ "crate": self.crate,
+ "name": self.name,
+ }
+
+@dataclass
+class RustCrate:
+
+ # When the json file is written, the list of Crates will be sorted by this
+ # value
+ order: int
+
+ display_name: str
+ root_module: str
+ edition: RUST_EDITIONS
+ deps: T.List[RustDep]
+ cfg: T.List[str]
+ is_proc_macro: bool
+
+ # This is set to True for members of this project, and False for all
+ # subprojects
+ is_workspace_member: bool
+ proc_macro_dylib_path: T.Optional[str] = None
+
+ def to_json(self) -> T.Dict[str, object]:
+ ret: T.Dict[str, object] = {
+ "display_name": self.display_name,
+ "root_module": self.root_module,
+ "edition": self.edition,
+ "cfg": self.cfg,
+ "is_proc_macro": self.is_proc_macro,
+ "deps": [d.to_json() for d in self.deps],
+ }
+
+ if self.is_proc_macro:
+ assert self.proc_macro_dylib_path is not None, "This shouldn't happen"
+ ret["proc_macro_dylib_path"] = self.proc_macro_dylib_path
+
+ return ret
+
+
+class NinjaBackend(backends.Backend):
+
+ def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]):
+ super().__init__(build, interpreter)
+ self.name = 'ninja'
+ self.ninja_filename = 'build.ninja'
+ self.fortran_deps = {}
+ self.all_outputs = {}
+ self.introspection_data = {}
+ self.created_llvm_ir_rule = PerMachine(False, False)
+ self.rust_crates: T.Dict[str, RustCrate] = {}
+
+ def create_phony_target(self, all_outputs, dummy_outfile, rulename, phony_infilename, implicit_outs=None):
+ '''
+ We need to use aliases for targets that might be used as directory
+ names to workaround a Ninja bug that breaks `ninja -t clean`.
+ This is used for 'reserved' targets such as 'test', 'install',
+ 'benchmark', etc, and also for RunTargets.
+ https://github.com/mesonbuild/meson/issues/1644
+ '''
+ if dummy_outfile.startswith('meson-internal__'):
+ raise AssertionError(f'Invalid usage of create_phony_target with {dummy_outfile!r}')
+
+ to_name = f'meson-internal__{dummy_outfile}'
+ elem = NinjaBuildElement(all_outputs, dummy_outfile, 'phony', to_name)
+ self.add_build(elem)
+
+ return NinjaBuildElement(all_outputs, to_name, rulename, phony_infilename, implicit_outs)
+
+ def detect_vs_dep_prefix(self, tempfilename):
+ '''VS writes its dependency in a locale dependent format.
+ Detect the search prefix to use.'''
+ # TODO don't hard-code host
+ for compiler in self.environment.coredata.compilers.host.values():
+ # Have to detect the dependency format
+
+ # IFort / masm on windows is MSVC like, but doesn't have /showincludes
+ if compiler.language in {'fortran', 'masm'}:
+ continue
+ if compiler.id == 'pgi' and mesonlib.is_windows():
+ # for the purpose of this function, PGI doesn't act enough like MSVC
+ return open(tempfilename, 'a', encoding='utf-8')
+ if compiler.get_argument_syntax() == 'msvc':
+ break
+ else:
+ # None of our compilers are MSVC, we're done.
+ return open(tempfilename, 'a', encoding='utf-8')
+ filebase = 'incdetect.' + compilers.lang_suffixes[compiler.language][0]
+ filename = os.path.join(self.environment.get_scratch_dir(),
+ filebase)
+ with open(filename, 'w', encoding='utf-8') as f:
+ f.write(dedent('''\
+ #include<stdio.h>
+ int dummy;
+ '''))
+
+ # The output of cl dependency information is language
+ # and locale dependent. Any attempt at converting it to
+ # Python strings leads to failure. We _must_ do this detection
+ # in raw byte mode and write the result in raw bytes.
+ pc = subprocess.Popen(compiler.get_exelist() +
+ ['/showIncludes', '/c', filebase],
+ cwd=self.environment.get_scratch_dir(),
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ (stdout, stderr) = pc.communicate()
+
+ # We want to match 'Note: including file: ' in the line
+ # 'Note: including file: d:\MyDir\include\stdio.h', however
+ # different locales have different messages with a different
+ # number of colons. Match up to the the drive name 'd:\'.
+ # When used in cross compilation, the path separator is a
+ # forward slash rather than a backslash so handle both; i.e.
+ # the path is /MyDir/include/stdio.h.
+ # With certain cross compilation wrappings of MSVC, the paths
+ # use backslashes, but without the leading drive name, so
+ # allow the path to start with any path separator, i.e.
+ # \MyDir\include\stdio.h.
+ matchre = re.compile(rb"^(.*\s)([a-zA-Z]:[\\/]|[\\\/]).*stdio.h$")
+
+ def detect_prefix(out):
+ for line in re.split(rb'\r?\n', out):
+ match = matchre.match(line)
+ if match:
+ with open(tempfilename, 'ab') as binfile:
+ binfile.write(b'msvc_deps_prefix = ' + match.group(1) + b'\n')
+ return open(tempfilename, 'a', encoding='utf-8')
+ return None
+
+ # Some cl wrappers (e.g. Squish Coco) output dependency info
+ # to stderr rather than stdout
+ result = detect_prefix(stdout) or detect_prefix(stderr)
+ if result:
+ return result
+
+ raise MesonException(f'Could not determine vs dep dependency prefix string. output: {stderr} {stdout}')
+
+ def generate(self):
+ ninja = environment.detect_ninja_command_and_version(log=True)
+ if self.build.need_vsenv:
+ builddir = Path(self.environment.get_build_dir())
+ try:
+ # For prettier printing, reduce to a relative path. If
+ # impossible (e.g., because builddir and cwd are on
+ # different Windows drives), skip and use the full path.
+ builddir = builddir.relative_to(Path.cwd())
+ except ValueError:
+ pass
+ meson_command = mesonlib.join_args(mesonlib.get_meson_command())
+ mlog.log()
+ mlog.log('Visual Studio environment is needed to run Ninja. It is recommended to use Meson wrapper:')
+ mlog.log(f'{meson_command} compile -C {builddir}')
+ if ninja is None:
+ raise MesonException('Could not detect Ninja v1.8.2 or newer')
+ (self.ninja_command, self.ninja_version) = ninja
+ outfilename = os.path.join(self.environment.get_build_dir(), self.ninja_filename)
+ tempfilename = outfilename + '~'
+ with open(tempfilename, 'w', encoding='utf-8') as outfile:
+ outfile.write(f'# This is the build file for project "{self.build.get_project()}"\n')
+ outfile.write('# It is autogenerated by the Meson build system.\n')
+ outfile.write('# Do not edit by hand.\n\n')
+ outfile.write('ninja_required_version = 1.8.2\n\n')
+
+ num_pools = self.environment.coredata.options[OptionKey('backend_max_links')].value
+ if num_pools > 0:
+ outfile.write(f'''pool link_pool
+ depth = {num_pools}
+
+''')
+
+ with self.detect_vs_dep_prefix(tempfilename) as outfile:
+ self.generate_rules()
+
+ self.build_elements = []
+ self.generate_phony()
+ self.add_build_comment(NinjaComment('Build rules for targets'))
+ for t in ProgressBar(self.build.get_targets().values(), desc='Generating targets'):
+ self.generate_target(t)
+ self.add_build_comment(NinjaComment('Test rules'))
+ self.generate_tests()
+ self.add_build_comment(NinjaComment('Install rules'))
+ self.generate_install()
+ self.generate_dist()
+ key = OptionKey('b_coverage')
+ if (key in self.environment.coredata.options and
+ self.environment.coredata.options[key].value):
+ gcovr_exe, gcovr_version, lcov_exe, genhtml_exe, _ = environment.find_coverage_tools()
+ if gcovr_exe or (lcov_exe and genhtml_exe):
+ self.add_build_comment(NinjaComment('Coverage rules'))
+ self.generate_coverage_rules(gcovr_exe, gcovr_version)
+ else:
+ # FIXME: since we explicitly opted in, should this be an error?
+ # The docs just say these targets will be created "if possible".
+ mlog.warning('Need gcovr or lcov/genhtml to generate any coverage reports')
+ self.add_build_comment(NinjaComment('Suffix'))
+ self.generate_utils()
+ self.generate_ending()
+
+ self.write_rules(outfile)
+ self.write_builds(outfile)
+
+ default = 'default all\n\n'
+ outfile.write(default)
+ # Only overwrite the old build file after the new one has been
+ # fully created.
+ os.replace(tempfilename, outfilename)
+ mlog.cmd_ci_include(outfilename) # For CI debugging
+ # Refresh Ninja's caches. https://github.com/ninja-build/ninja/pull/1685
+ if mesonlib.version_compare(self.ninja_version, '>=1.10.0') and os.path.exists('.ninja_deps'):
+ subprocess.call(self.ninja_command + ['-t', 'restat'])
+ subprocess.call(self.ninja_command + ['-t', 'cleandead'])
+ self.generate_compdb()
+ self.generate_rust_project_json()
+
+ def generate_rust_project_json(self) -> None:
+ """Generate a rust-analyzer compatible rust-project.json file."""
+ if not self.rust_crates:
+ return
+ with open(os.path.join(self.environment.get_build_dir(), 'rust-project.json'),
+ 'w', encoding='utf-8') as f:
+ json.dump(
+ {
+ "sysroot_src": os.path.join(self.environment.coredata.compilers.host['rust'].get_sysroot(),
+ 'lib/rustlib/src/rust/library/'),
+ "crates": [c.to_json() for c in self.rust_crates.values()],
+ },
+ f, indent=4)
+
+ # http://clang.llvm.org/docs/JSONCompilationDatabase.html
+ def generate_compdb(self):
+ rules = []
+ # TODO: Rather than an explicit list here, rules could be marked in the
+ # rule store as being wanted in compdb
+ for for_machine in MachineChoice:
+ for compiler in self.environment.coredata.compilers[for_machine].values():
+ rules += [f"{rule}{ext}" for rule in [self.compiler_to_rule_name(compiler)]
+ for ext in ['', '_RSP']]
+ rules += [f"{rule}{ext}" for rule in [self.compiler_to_pch_rule_name(compiler)]
+ for ext in ['', '_RSP']]
+ compdb_options = ['-x'] if mesonlib.version_compare(self.ninja_version, '>=1.9') else []
+ ninja_compdb = self.ninja_command + ['-t', 'compdb'] + compdb_options + rules
+ builddir = self.environment.get_build_dir()
+ try:
+ jsondb = subprocess.check_output(ninja_compdb, cwd=builddir)
+ with open(os.path.join(builddir, 'compile_commands.json'), 'wb') as f:
+ f.write(jsondb)
+ except Exception:
+ mlog.warning('Could not create compilation database.', fatal=False)
+
+ # Get all generated headers. Any source file might need them so
+ # we need to add an order dependency to them.
+ def get_generated_headers(self, target):
+ if hasattr(target, 'cached_generated_headers'):
+ return target.cached_generated_headers
+ header_deps = []
+ # XXX: Why don't we add deps to CustomTarget headers here?
+ for genlist in target.get_generated_sources():
+ if isinstance(genlist, (build.CustomTarget, build.CustomTargetIndex)):
+ continue
+ for src in genlist.get_outputs():
+ if self.environment.is_header(src):
+ header_deps.append(self.get_target_generated_dir(target, genlist, src))
+ if 'vala' in target.compilers and not isinstance(target, build.Executable):
+ vala_header = File.from_built_file(self.get_target_dir(target), target.vala_header)
+ header_deps.append(vala_header)
+ # Recurse and find generated headers
+ for dep in itertools.chain(target.link_targets, target.link_whole_targets):
+ if isinstance(dep, (build.StaticLibrary, build.SharedLibrary)):
+ header_deps += self.get_generated_headers(dep)
+ target.cached_generated_headers = header_deps
+ return header_deps
+
+ def get_target_generated_sources(self, target: build.BuildTarget) -> T.MutableMapping[str, File]:
+ """
+ Returns a dictionary with the keys being the path to the file
+ (relative to the build directory) and the value being the File object
+ representing the same path.
+ """
+ srcs: T.MutableMapping[str, File] = OrderedDict()
+ for gensrc in target.get_generated_sources():
+ for s in gensrc.get_outputs():
+ rel_src = self.get_target_generated_dir(target, gensrc, s)
+ srcs[rel_src] = File.from_built_relative(rel_src)
+ return srcs
+
+ def get_target_sources(self, target: build.BuildTarget) -> T.MutableMapping[str, File]:
+ srcs: T.MutableMapping[str, File] = OrderedDict()
+ for s in target.get_sources():
+ # BuildTarget sources are always mesonlib.File files which are
+ # either in the source root, or generated with configure_file and
+ # in the build root
+ if not isinstance(s, File):
+ raise InvalidArguments(f'All sources in target {s!r} must be of type mesonlib.File')
+ f = s.rel_to_builddir(self.build_to_src)
+ srcs[f] = s
+ return srcs
+
+ def get_target_source_can_unity(self, target, source):
+ if isinstance(source, File):
+ source = source.fname
+ if self.environment.is_llvm_ir(source) or \
+ self.environment.is_assembly(source):
+ return False
+ suffix = os.path.splitext(source)[1][1:].lower()
+ for lang in backends.LANGS_CANT_UNITY:
+ if lang not in target.compilers:
+ continue
+ if suffix in target.compilers[lang].file_suffixes:
+ return False
+ return True
+
+ def create_target_source_introspection(self, target: build.Target, comp: compilers.Compiler, parameters, sources, generated_sources):
+ '''
+ Adds the source file introspection information for a language of a target
+
+ Internal introspection storage formart:
+ self.introspection_data = {
+ '<target ID>': {
+ <id tuple>: {
+ 'language: 'lang',
+ 'compiler': ['comp', 'exe', 'list'],
+ 'parameters': ['UNIQUE', 'parameter', 'list'],
+ 'sources': [],
+ 'generated_sources': [],
+ }
+ }
+ }
+ '''
+ tid = target.get_id()
+ lang = comp.get_language()
+ tgt = self.introspection_data[tid]
+ # Find an existing entry or create a new one
+ id_hash = (lang, tuple(parameters))
+ src_block = tgt.get(id_hash, None)
+ if src_block is None:
+ # Convert parameters
+ if isinstance(parameters, CompilerArgs):
+ parameters = parameters.to_native(copy=True)
+ parameters = comp.compute_parameters_with_absolute_paths(parameters, self.build_dir)
+ # The new entry
+ src_block = {
+ 'language': lang,
+ 'compiler': comp.get_exelist(),
+ 'parameters': parameters,
+ 'sources': [],
+ 'generated_sources': [],
+ }
+ tgt[id_hash] = src_block
+ # Make source files absolute
+ sources = [x.absolute_path(self.source_dir, self.build_dir) if isinstance(x, File) else os.path.normpath(os.path.join(self.build_dir, x))
+ for x in sources]
+ generated_sources = [x.absolute_path(self.source_dir, self.build_dir) if isinstance(x, File) else os.path.normpath(os.path.join(self.build_dir, x))
+ for x in generated_sources]
+ # Add the source files
+ src_block['sources'] += sources
+ src_block['generated_sources'] += generated_sources
+
+ def generate_target(self, target):
+ try:
+ if isinstance(target, build.BuildTarget):
+ os.makedirs(self.get_target_private_dir_abs(target))
+ except FileExistsError:
+ pass
+ if isinstance(target, build.CustomTarget):
+ self.generate_custom_target(target)
+ if isinstance(target, build.RunTarget):
+ self.generate_run_target(target)
+ compiled_sources = []
+ source2object = {}
+ name = target.get_id()
+ if name in self.processed_targets:
+ return
+ self.processed_targets.add(name)
+ # Initialize an empty introspection source list
+ self.introspection_data[name] = {}
+ # Generate rules for all dependency targets
+ self.process_target_dependencies(target)
+
+ self.generate_shlib_aliases(target, self.get_target_dir(target))
+
+ # If target uses a language that cannot link to C objects,
+ # just generate for that language and return.
+ if isinstance(target, build.Jar):
+ self.generate_jar_target(target)
+ return
+ if target.uses_rust():
+ self.generate_rust_target(target)
+ return
+ if 'cs' in target.compilers:
+ self.generate_cs_target(target)
+ return
+ if 'swift' in target.compilers:
+ self.generate_swift_target(target)
+ return
+
+ # Pre-existing target C/C++ sources to be built; dict of full path to
+ # source relative to build root and the original File object.
+ target_sources: T.MutableMapping[str, File]
+
+ # GeneratedList and CustomTarget sources to be built; dict of the full
+ # path to source relative to build root and the generating target/list
+ generated_sources: T.MutableMapping[str, File]
+
+ # List of sources that have been transpiled from a DSL (like Vala) into
+ # a language that is haneled below, such as C or C++
+ transpiled_sources: T.List[str]
+
+ if 'vala' in target.compilers:
+ # Sources consumed by valac are filtered out. These only contain
+ # C/C++ sources, objects, generated libs, and unknown sources now.
+ target_sources, generated_sources, \
+ transpiled_sources = self.generate_vala_compile(target)
+ elif 'cython' in target.compilers:
+ target_sources, generated_sources, \
+ transpiled_sources = self.generate_cython_transpile(target)
+ else:
+ target_sources = self.get_target_sources(target)
+ generated_sources = self.get_target_generated_sources(target)
+ transpiled_sources = []
+ self.scan_fortran_module_outputs(target)
+ # Generate rules for GeneratedLists
+ self.generate_generator_list_rules(target)
+
+ # Generate rules for building the remaining source files in this target
+ outname = self.get_target_filename(target)
+ obj_list = []
+ is_unity = target.is_unity
+ header_deps = []
+ unity_src = []
+ unity_deps = [] # Generated sources that must be built before compiling a Unity target.
+ header_deps += self.get_generated_headers(target)
+
+ if is_unity:
+ # Warn about incompatible sources if a unity build is enabled
+ langs = set(target.compilers.keys())
+ langs_cant = langs.intersection(backends.LANGS_CANT_UNITY)
+ if langs_cant:
+ langs_are = langs = ', '.join(langs_cant).upper()
+ langs_are += ' are' if len(langs_cant) > 1 else ' is'
+ msg = f'{langs_are} not supported in Unity builds yet, so {langs} ' \
+ f'sources in the {target.name!r} target will be compiled normally'
+ mlog.log(mlog.red('FIXME'), msg)
+
+ # Get a list of all generated headers that will be needed while building
+ # this target's sources (generated sources and pre-existing sources).
+ # This will be set as dependencies of all the target's sources. At the
+ # same time, also deal with generated sources that need to be compiled.
+ generated_source_files = []
+ for rel_src in generated_sources.keys():
+ raw_src = File.from_built_relative(rel_src)
+ if self.environment.is_source(rel_src):
+ if is_unity and self.get_target_source_can_unity(target, rel_src):
+ unity_deps.append(raw_src)
+ abs_src = os.path.join(self.environment.get_build_dir(), rel_src)
+ unity_src.append(abs_src)
+ else:
+ generated_source_files.append(raw_src)
+ elif self.environment.is_object(rel_src):
+ obj_list.append(rel_src)
+ elif self.environment.is_library(rel_src) or modules.is_module_library(rel_src):
+ pass
+ else:
+ # Assume anything not specifically a source file is a header. This is because
+ # people generate files with weird suffixes (.inc, .fh) that they then include
+ # in their source files.
+ header_deps.append(raw_src)
+
+ # For D language, the object of generated source files are added
+ # as order only deps because other files may depend on them
+ d_generated_deps = []
+
+ # These are the generated source files that need to be built for use by
+ # this target. We create the Ninja build file elements for this here
+ # because we need `header_deps` to be fully generated in the above loop.
+ for src in generated_source_files:
+ if self.environment.is_llvm_ir(src):
+ o, s = self.generate_llvm_ir_compile(target, src)
+ else:
+ o, s = self.generate_single_compile(target, src, True,
+ order_deps=header_deps)
+ compiled_sources.append(s)
+ source2object[s] = o
+ obj_list.append(o)
+ if s.split('.')[-1] in compilers.lang_suffixes['d']:
+ d_generated_deps.append(o)
+
+ use_pch = self.environment.coredata.options.get(OptionKey('b_pch'))
+ if use_pch and target.has_pch():
+ pch_objects = self.generate_pch(target, header_deps=header_deps)
+ else:
+ pch_objects = []
+
+ o, od = self.flatten_object_list(target)
+ obj_targets = [t for t in od if t.uses_fortran()]
+ obj_list.extend(o)
+
+ fortran_order_deps = [self.get_target_filename(t) for t in obj_targets]
+ fortran_inc_args: T.List[str] = []
+ if target.uses_fortran():
+ fortran_inc_args = mesonlib.listify([target.compilers['fortran'].get_include_args(
+ self.get_target_private_dir(t), is_system=False) for t in obj_targets])
+
+ # Generate compilation targets for C sources generated from Vala
+ # sources. This can be extended to other $LANG->C compilers later if
+ # necessary. This needs to be separate for at least Vala
+ #
+ # Do not try to unity-build the generated c files from vala, as these
+ # often contain duplicate symbols and will fail to compile properly
+ vala_generated_source_files = []
+ for src in transpiled_sources:
+ raw_src = File.from_built_relative(src)
+ # Generated targets are ordered deps because the must exist
+ # before the sources compiling them are used. After the first
+ # compile we get precise dependency info from dep files.
+ # This should work in all cases. If it does not, then just
+ # move them from orderdeps to proper deps.
+ if self.environment.is_header(src):
+ header_deps.append(raw_src)
+ else:
+ # We gather all these and generate compile rules below
+ # after `header_deps` (above) is fully generated
+ vala_generated_source_files.append(raw_src)
+ for src in vala_generated_source_files:
+ # Passing 'vala' here signifies that we want the compile
+ # arguments to be specialized for C code generated by
+ # valac. For instance, no warnings should be emitted.
+ o, s = self.generate_single_compile(target, src, 'vala', [], header_deps)
+ obj_list.append(o)
+
+ # Generate compile targets for all the pre-existing sources for this target
+ for src in target_sources.values():
+ if not self.environment.is_header(src):
+ if self.environment.is_llvm_ir(src):
+ o, s = self.generate_llvm_ir_compile(target, src)
+ obj_list.append(o)
+ elif is_unity and self.get_target_source_can_unity(target, src):
+ abs_src = os.path.join(self.environment.get_build_dir(),
+ src.rel_to_builddir(self.build_to_src))
+ unity_src.append(abs_src)
+ else:
+ o, s = self.generate_single_compile(target, src, False, [],
+ header_deps + d_generated_deps + fortran_order_deps,
+ fortran_inc_args)
+ obj_list.append(o)
+ compiled_sources.append(s)
+ source2object[s] = o
+
+ if is_unity:
+ for src in self.generate_unity_files(target, unity_src):
+ o, s = self.generate_single_compile(target, src, True, unity_deps + header_deps + d_generated_deps,
+ fortran_order_deps, fortran_inc_args)
+ obj_list.append(o)
+ compiled_sources.append(s)
+ source2object[s] = o
+ if isinstance(target, build.CompileTarget):
+ # Skip the link stage for this special type of target
+ return
+ linker, stdlib_args = self.determine_linker_and_stdlib_args(target)
+ if isinstance(target, build.StaticLibrary) and target.prelink:
+ final_obj_list = self.generate_prelink(target, obj_list)
+ else:
+ final_obj_list = obj_list
+ elem = self.generate_link(target, outname, final_obj_list, linker, pch_objects, stdlib_args=stdlib_args)
+ self.generate_dependency_scan_target(target, compiled_sources, source2object, generated_source_files, fortran_order_deps)
+ self.add_build(elem)
+
+ def should_use_dyndeps_for_target(self, target: 'build.BuildTarget') -> bool:
+ if mesonlib.version_compare(self.ninja_version, '<1.10.0'):
+ return False
+ if 'fortran' in target.compilers:
+ return True
+ if 'cpp' not in target.compilers:
+ return False
+ if '-fmodules-ts' in target.extra_args.get('cpp', []):
+ return True
+ # Currently only the preview version of Visual Studio is supported.
+ cpp = target.compilers['cpp']
+ if cpp.get_id() != 'msvc':
+ return False
+ cppversion = self.environment.coredata.options[OptionKey('std', machine=target.for_machine, lang='cpp')].value
+ if cppversion not in ('latest', 'c++latest', 'vc++latest'):
+ return False
+ if not mesonlib.current_vs_supports_modules():
+ return False
+ if mesonlib.version_compare(cpp.version, '<19.28.28617'):
+ return False
+ return True
+
+ def generate_dependency_scan_target(self, target, compiled_sources, source2object, generated_source_files: T.List[mesonlib.File],
+ object_deps: T.List[str]) -> None:
+ if not self.should_use_dyndeps_for_target(target):
+ return
+ depscan_file = self.get_dep_scan_file_for(target)
+ pickle_base = target.name + '.dat'
+ pickle_file = os.path.join(self.get_target_private_dir(target), pickle_base).replace('\\', '/')
+ pickle_abs = os.path.join(self.get_target_private_dir_abs(target), pickle_base).replace('\\', '/')
+ json_abs = os.path.join(self.get_target_private_dir_abs(target), f'{target.name}-deps.json').replace('\\', '/')
+ rule_name = 'depscan'
+ scan_sources = self.select_sources_to_scan(compiled_sources)
+
+ # Dump the sources as a json list. This avoids potential probllems where
+ # the number of sources passed to depscan exceedes the limit imposed by
+ # the OS.
+ with open(json_abs, 'w', encoding='utf-8') as f:
+ json.dump(scan_sources, f)
+ elem = NinjaBuildElement(self.all_outputs, depscan_file, rule_name, json_abs)
+ elem.add_item('picklefile', pickle_file)
+ # Add any generated outputs to the order deps of the scan target, so
+ # that those sources are present
+ for g in generated_source_files:
+ elem.orderdeps.add(g.relative_name())
+ elem.orderdeps.update(object_deps)
+ scaninfo = TargetDependencyScannerInfo(self.get_target_private_dir(target), source2object)
+ with open(pickle_abs, 'wb') as p:
+ pickle.dump(scaninfo, p)
+ self.add_build(elem)
+
+ def select_sources_to_scan(self, compiled_sources):
+ # in practice pick up C++ and Fortran files. If some other language
+ # requires scanning (possibly Java to deal with inner class files)
+ # then add them here.
+ all_suffixes = set(compilers.lang_suffixes['cpp']) | set(compilers.lang_suffixes['fortran'])
+ selected_sources = []
+ for source in compiled_sources:
+ ext = os.path.splitext(source)[1][1:]
+ if ext != 'C':
+ ext = ext.lower()
+ if ext in all_suffixes:
+ selected_sources.append(source)
+ return selected_sources
+
+ def process_target_dependencies(self, target):
+ for t in target.get_dependencies():
+ if t.get_id() not in self.processed_targets:
+ self.generate_target(t)
+
+ def custom_target_generator_inputs(self, target):
+ for s in target.sources:
+ if isinstance(s, build.GeneratedList):
+ self.generate_genlist_for_target(s, target)
+
+ def unwrap_dep_list(self, target):
+ deps = []
+ for i in target.get_dependencies():
+ # FIXME, should not grab element at zero but rather expand all.
+ if isinstance(i, list):
+ i = i[0]
+ # Add a dependency on all the outputs of this target
+ for output in i.get_outputs():
+ deps.append(os.path.join(self.get_target_dir(i), output))
+ return deps
+
+ def generate_custom_target(self, target):
+ self.custom_target_generator_inputs(target)
+ (srcs, ofilenames, cmd) = self.eval_custom_target_command(target)
+ deps = self.unwrap_dep_list(target)
+ deps += self.get_custom_target_depend_files(target)
+ if target.build_always_stale:
+ deps.append('PHONY')
+ if target.depfile is None:
+ rulename = 'CUSTOM_COMMAND'
+ else:
+ rulename = 'CUSTOM_COMMAND_DEP'
+ elem = NinjaBuildElement(self.all_outputs, ofilenames, rulename, srcs)
+ elem.add_dep(deps)
+ for d in target.extra_depends:
+ # Add a dependency on all the outputs of this target
+ for output in d.get_outputs():
+ elem.add_dep(os.path.join(self.get_target_dir(d), output))
+
+ cmd, reason = self.as_meson_exe_cmdline(target.command[0], cmd[1:],
+ extra_bdeps=target.get_transitive_build_target_deps(),
+ capture=ofilenames[0] if target.capture else None,
+ feed=srcs[0] if target.feed else None,
+ env=target.env,
+ verbose=target.console)
+ if reason:
+ cmd_type = f' (wrapped by meson {reason})'
+ else:
+ cmd_type = ''
+ if target.depfile is not None:
+ depfile = target.get_dep_outname(elem.infilenames)
+ rel_dfile = os.path.join(self.get_target_dir(target), depfile)
+ abs_pdir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target))
+ os.makedirs(abs_pdir, exist_ok=True)
+ elem.add_item('DEPFILE', rel_dfile)
+ if target.console:
+ elem.add_item('pool', 'console')
+ full_name = Path(target.subdir, target.name).as_posix()
+ elem.add_item('COMMAND', cmd)
+ elem.add_item('description', f'Generating {full_name} with a custom command{cmd_type}')
+ self.add_build(elem)
+ self.processed_targets.add(target.get_id())
+
+ def build_run_target_name(self, target):
+ if target.subproject != '':
+ subproject_prefix = f'{target.subproject}@@'
+ else:
+ subproject_prefix = ''
+ return f'{subproject_prefix}{target.name}'
+
+ def generate_run_target(self, target: build.RunTarget):
+ target_name = self.build_run_target_name(target)
+ if not target.command:
+ # This is an alias target, it has no command, it just depends on
+ # other targets.
+ elem = NinjaBuildElement(self.all_outputs, target_name, 'phony', [])
+ else:
+ target_env = self.get_run_target_env(target)
+ _, _, cmd = self.eval_custom_target_command(target)
+ meson_exe_cmd, reason = self.as_meson_exe_cmdline(target.command[0], cmd[1:],
+ env=target_env,
+ verbose=True)
+ cmd_type = f' (wrapped by meson {reason})' if reason else ''
+ elem = self.create_phony_target(self.all_outputs, target_name, 'CUSTOM_COMMAND', [])
+ elem.add_item('COMMAND', meson_exe_cmd)
+ elem.add_item('description', f'Running external command {target.name}{cmd_type}')
+ elem.add_item('pool', 'console')
+ deps = self.unwrap_dep_list(target)
+ deps += self.get_custom_target_depend_files(target)
+ elem.add_dep(deps)
+ self.add_build(elem)
+ self.processed_targets.add(target.get_id())
+
+ def generate_coverage_command(self, elem, outputs):
+ targets = self.build.get_targets().values()
+ use_llvm_cov = False
+ for target in targets:
+ if not hasattr(target, 'compilers'):
+ continue
+ for compiler in target.compilers.values():
+ if compiler.get_id() == 'clang' and not compiler.info.is_darwin():
+ use_llvm_cov = True
+ break
+ elem.add_item('COMMAND', self.environment.get_build_command() +
+ ['--internal', 'coverage'] +
+ outputs +
+ [self.environment.get_source_dir(),
+ os.path.join(self.environment.get_source_dir(),
+ self.build.get_subproject_dir()),
+ self.environment.get_build_dir(),
+ self.environment.get_log_dir()] +
+ (['--use_llvm_cov'] if use_llvm_cov else []))
+
+ def generate_coverage_rules(self, gcovr_exe: T.Optional[str], gcovr_version: T.Optional[str]):
+ e = self.create_phony_target(self.all_outputs, 'coverage', 'CUSTOM_COMMAND', 'PHONY')
+ self.generate_coverage_command(e, [])
+ e.add_item('description', 'Generates coverage reports')
+ self.add_build(e)
+ self.generate_coverage_legacy_rules(gcovr_exe, gcovr_version)
+
+ def generate_coverage_legacy_rules(self, gcovr_exe: T.Optional[str], gcovr_version: T.Optional[str]):
+ e = self.create_phony_target(self.all_outputs, 'coverage-html', 'CUSTOM_COMMAND', 'PHONY')
+ self.generate_coverage_command(e, ['--html'])
+ e.add_item('description', 'Generates HTML coverage report')
+ self.add_build(e)
+
+ if gcovr_exe:
+ e = self.create_phony_target(self.all_outputs, 'coverage-xml', 'CUSTOM_COMMAND', 'PHONY')
+ self.generate_coverage_command(e, ['--xml'])
+ e.add_item('description', 'Generates XML coverage report')
+ self.add_build(e)
+
+ e = self.create_phony_target(self.all_outputs, 'coverage-text', 'CUSTOM_COMMAND', 'PHONY')
+ self.generate_coverage_command(e, ['--text'])
+ e.add_item('description', 'Generates text coverage report')
+ self.add_build(e)
+
+ if mesonlib.version_compare(gcovr_version, '>=4.2'):
+ e = self.create_phony_target(self.all_outputs, 'coverage-sonarqube', 'CUSTOM_COMMAND', 'PHONY')
+ self.generate_coverage_command(e, ['--sonarqube'])
+ e.add_item('description', 'Generates Sonarqube XML coverage report')
+ self.add_build(e)
+
+ def generate_install(self):
+ self.create_install_data_files()
+ elem = self.create_phony_target(self.all_outputs, 'install', 'CUSTOM_COMMAND', 'PHONY')
+ elem.add_dep('all')
+ elem.add_item('DESC', 'Installing files.')
+ elem.add_item('COMMAND', self.environment.get_build_command() + ['install', '--no-rebuild'])
+ elem.add_item('pool', 'console')
+ self.add_build(elem)
+
+ def generate_tests(self):
+ self.serialize_tests()
+ cmd = self.environment.get_build_command(True) + ['test', '--no-rebuild']
+ if not self.environment.coredata.get_option(OptionKey('stdsplit')):
+ cmd += ['--no-stdsplit']
+ if self.environment.coredata.get_option(OptionKey('errorlogs')):
+ cmd += ['--print-errorlogs']
+ elem = self.create_phony_target(self.all_outputs, 'test', 'CUSTOM_COMMAND', ['all', 'PHONY'])
+ elem.add_item('COMMAND', cmd)
+ elem.add_item('DESC', 'Running all tests.')
+ elem.add_item('pool', 'console')
+ self.add_build(elem)
+
+ # And then benchmarks.
+ cmd = self.environment.get_build_command(True) + [
+ 'test', '--benchmark', '--logbase',
+ 'benchmarklog', '--num-processes=1', '--no-rebuild']
+ elem = self.create_phony_target(self.all_outputs, 'benchmark', 'CUSTOM_COMMAND', ['all', 'PHONY'])
+ elem.add_item('COMMAND', cmd)
+ elem.add_item('DESC', 'Running benchmark suite.')
+ elem.add_item('pool', 'console')
+ self.add_build(elem)
+
+ def generate_rules(self):
+ self.rules = []
+ self.ruledict = {}
+
+ self.add_rule_comment(NinjaComment('Rules for module scanning.'))
+ self.generate_scanner_rules()
+ self.add_rule_comment(NinjaComment('Rules for compiling.'))
+ self.generate_compile_rules()
+ self.add_rule_comment(NinjaComment('Rules for linking.'))
+ self.generate_static_link_rules()
+ self.generate_dynamic_link_rules()
+ self.add_rule_comment(NinjaComment('Other rules'))
+ # Ninja errors out if you have deps = gcc but no depfile, so we must
+ # have two rules for custom commands.
+ self.add_rule(NinjaRule('CUSTOM_COMMAND', ['$COMMAND'], [], '$DESC',
+ extra='restat = 1'))
+ self.add_rule(NinjaRule('CUSTOM_COMMAND_DEP', ['$COMMAND'], [], '$DESC',
+ deps='gcc', depfile='$DEPFILE',
+ extra='restat = 1'))
+ self.add_rule(NinjaRule('COPY_FILE', self.environment.get_build_command() + ['--internal', 'copy'],
+ ['$in', '$out'], 'Copying $in to $out'))
+
+ c = self.environment.get_build_command() + \
+ ['--internal',
+ 'regenerate',
+ self.environment.get_source_dir(),
+ self.environment.get_build_dir()]
+ self.add_rule(NinjaRule('REGENERATE_BUILD',
+ c, [],
+ 'Regenerating build files.',
+ extra='generator = 1'))
+
+ def add_rule_comment(self, comment):
+ self.rules.append(comment)
+
+ def add_build_comment(self, comment):
+ self.build_elements.append(comment)
+
+ def add_rule(self, rule):
+ if rule.name in self.ruledict:
+ raise MesonException(f'Tried to add rule {rule.name} twice.')
+ self.rules.append(rule)
+ self.ruledict[rule.name] = rule
+
+ def add_build(self, build):
+ build.check_outputs()
+ self.build_elements.append(build)
+
+ if build.rulename != 'phony':
+ # reference rule
+ if build.rulename in self.ruledict:
+ build.rule = self.ruledict[build.rulename]
+ else:
+ mlog.warning(f"build statement for {build.outfilenames} references non-existent rule {build.rulename}")
+
+ def write_rules(self, outfile):
+ for b in self.build_elements:
+ if isinstance(b, NinjaBuildElement):
+ b.count_rule_references()
+
+ for r in self.rules:
+ r.write(outfile)
+
+ def write_builds(self, outfile):
+ for b in ProgressBar(self.build_elements, desc='Writing build.ninja'):
+ b.write(outfile)
+
+ def generate_phony(self):
+ self.add_build_comment(NinjaComment('Phony build target, always out of date'))
+ elem = NinjaBuildElement(self.all_outputs, 'PHONY', 'phony', '')
+ self.add_build(elem)
+
+ def generate_jar_target(self, target: build.Jar):
+ fname = target.get_filename()
+ outname_rel = os.path.join(self.get_target_dir(target), fname)
+ src_list = target.get_sources()
+ resources = target.get_java_resources()
+ class_list = []
+ compiler = target.compilers['java']
+ c = 'c'
+ m = 'm'
+ e = ''
+ f = 'f'
+ main_class = target.get_main_class()
+ if main_class != '':
+ e = 'e'
+
+ # Add possible java generated files to src list
+ generated_sources = self.get_target_generated_sources(target)
+ gen_src_list = []
+ for rel_src in generated_sources.keys():
+ raw_src = File.from_built_relative(rel_src)
+ if rel_src.endswith('.java'):
+ gen_src_list.append(raw_src)
+
+ compile_args = self.determine_single_java_compile_args(target, compiler)
+ for src in src_list + gen_src_list:
+ plain_class_path = self.generate_single_java_compile(src, target, compiler, compile_args)
+ class_list.append(plain_class_path)
+ class_dep_list = [os.path.join(self.get_target_private_dir(target), i) for i in class_list]
+ manifest_path = os.path.join(self.get_target_private_dir(target), 'META-INF', 'MANIFEST.MF')
+ manifest_fullpath = os.path.join(self.environment.get_build_dir(), manifest_path)
+ os.makedirs(os.path.dirname(manifest_fullpath), exist_ok=True)
+ with open(manifest_fullpath, 'w', encoding='utf-8') as manifest:
+ if any(target.link_targets):
+ manifest.write('Class-Path: ')
+ cp_paths = [os.path.join(self.get_target_dir(l), l.get_filename()) for l in target.link_targets]
+ manifest.write(' '.join(cp_paths))
+ manifest.write('\n')
+ jar_rule = 'java_LINKER'
+ commands = [c + m + e + f]
+ commands.append(manifest_path)
+ if e != '':
+ commands.append(main_class)
+ commands.append(self.get_target_filename(target))
+ # Java compilation can produce an arbitrary number of output
+ # class files for a single source file. Thus tell jar to just
+ # grab everything in the final package.
+ commands += ['-C', self.get_target_private_dir(target), '.']
+ elem = NinjaBuildElement(self.all_outputs, outname_rel, jar_rule, [])
+ elem.add_dep(class_dep_list)
+ if resources:
+ # Copy all resources into the root of the jar.
+ elem.add_orderdep(self.__generate_sources_structure(Path(self.get_target_private_dir(target)), resources)[0])
+ elem.add_item('ARGS', commands)
+ self.add_build(elem)
+ # Create introspection information
+ self.create_target_source_introspection(target, compiler, compile_args, src_list, gen_src_list)
+
+ def generate_cs_resource_tasks(self, target):
+ args = []
+ deps = []
+ for r in target.resources:
+ rel_sourcefile = os.path.join(self.build_to_src, target.subdir, r)
+ if r.endswith('.resources'):
+ a = '-resource:' + rel_sourcefile
+ elif r.endswith('.txt') or r.endswith('.resx'):
+ ofilebase = os.path.splitext(os.path.basename(r))[0] + '.resources'
+ ofilename = os.path.join(self.get_target_private_dir(target), ofilebase)
+ elem = NinjaBuildElement(self.all_outputs, ofilename, "CUSTOM_COMMAND", rel_sourcefile)
+ elem.add_item('COMMAND', ['resgen', rel_sourcefile, ofilename])
+ elem.add_item('DESC', f'Compiling resource {rel_sourcefile}')
+ self.add_build(elem)
+ deps.append(ofilename)
+ a = '-resource:' + ofilename
+ else:
+ raise InvalidArguments(f'Unknown resource file {r}.')
+ args.append(a)
+ return args, deps
+
+ def generate_cs_target(self, target: build.BuildTarget):
+ buildtype = target.get_option(OptionKey('buildtype'))
+ fname = target.get_filename()
+ outname_rel = os.path.join(self.get_target_dir(target), fname)
+ src_list = target.get_sources()
+ compiler = target.compilers['cs']
+ rel_srcs = [os.path.normpath(s.rel_to_builddir(self.build_to_src)) for s in src_list]
+ deps = []
+ commands = compiler.compiler_args(target.extra_args.get('cs', []))
+ commands += compiler.get_buildtype_args(buildtype)
+ commands += compiler.get_optimization_args(target.get_option(OptionKey('optimization')))
+ commands += compiler.get_debug_args(target.get_option(OptionKey('debug')))
+ if isinstance(target, build.Executable):
+ commands.append('-target:exe')
+ elif isinstance(target, build.SharedLibrary):
+ commands.append('-target:library')
+ else:
+ raise MesonException('Unknown C# target type.')
+ (resource_args, resource_deps) = self.generate_cs_resource_tasks(target)
+ commands += resource_args
+ deps += resource_deps
+ commands += compiler.get_output_args(outname_rel)
+ for l in target.link_targets:
+ lname = os.path.join(self.get_target_dir(l), l.get_filename())
+ commands += compiler.get_link_args(lname)
+ deps.append(lname)
+ if '-g' in commands:
+ outputs = [outname_rel, outname_rel + '.mdb']
+ else:
+ outputs = [outname_rel]
+ generated_sources = self.get_target_generated_sources(target)
+ generated_rel_srcs = []
+ for rel_src in generated_sources.keys():
+ if rel_src.lower().endswith('.cs'):
+ generated_rel_srcs.append(os.path.normpath(rel_src))
+ deps.append(os.path.normpath(rel_src))
+
+ for dep in target.get_external_deps():
+ commands.extend_direct(dep.get_link_args())
+ commands += self.build.get_project_args(compiler, target.subproject, target.for_machine)
+ commands += self.build.get_global_args(compiler, target.for_machine)
+
+ elem = NinjaBuildElement(self.all_outputs, outputs, self.compiler_to_rule_name(compiler), rel_srcs + generated_rel_srcs)
+ elem.add_dep(deps)
+ elem.add_item('ARGS', commands)
+ self.add_build(elem)
+
+ self.generate_generator_list_rules(target)
+ self.create_target_source_introspection(target, compiler, commands, rel_srcs, generated_rel_srcs)
+
+ def determine_single_java_compile_args(self, target, compiler):
+ args = []
+ args += compiler.get_buildtype_args(target.get_option(OptionKey('buildtype')))
+ args += self.build.get_global_args(compiler, target.for_machine)
+ args += self.build.get_project_args(compiler, target.subproject, target.for_machine)
+ args += target.get_java_args()
+ args += compiler.get_output_args(self.get_target_private_dir(target))
+ args += target.get_classpath_args()
+ curdir = target.get_subdir()
+ sourcepath = os.path.join(self.build_to_src, curdir) + os.pathsep
+ sourcepath += os.path.normpath(curdir) + os.pathsep
+ for i in target.include_dirs:
+ for idir in i.get_incdirs():
+ sourcepath += os.path.join(self.build_to_src, i.curdir, idir) + os.pathsep
+ args += ['-sourcepath', sourcepath]
+ return args
+
+ def generate_single_java_compile(self, src, target, compiler, args):
+ deps = [os.path.join(self.get_target_dir(l), l.get_filename()) for l in target.link_targets]
+ generated_sources = self.get_target_generated_sources(target)
+ for rel_src in generated_sources.keys():
+ if rel_src.endswith('.java'):
+ deps.append(rel_src)
+ rel_src = src.rel_to_builddir(self.build_to_src)
+ plain_class_path = src.fname[:-4] + 'class'
+ rel_obj = os.path.join(self.get_target_private_dir(target), plain_class_path)
+ element = NinjaBuildElement(self.all_outputs, rel_obj, self.compiler_to_rule_name(compiler), rel_src)
+ element.add_dep(deps)
+ element.add_item('ARGS', args)
+ self.add_build(element)
+ return plain_class_path
+
+ def generate_java_link(self):
+ rule = 'java_LINKER'
+ command = ['jar', '$ARGS']
+ description = 'Creating JAR $out'
+ self.add_rule(NinjaRule(rule, command, [], description))
+
+ def determine_dep_vapis(self, target):
+ """
+ Peek into the sources of BuildTargets we're linking with, and if any of
+ them was built with Vala, assume that it also generated a .vapi file of
+ the same name as the BuildTarget and return the path to it relative to
+ the build directory.
+ """
+ result = OrderedSet()
+ for dep in itertools.chain(target.link_targets, target.link_whole_targets):
+ if not dep.is_linkable_target():
+ continue
+ for i in dep.sources:
+ if hasattr(i, 'fname'):
+ i = i.fname
+ if i.split('.')[-1] in compilers.lang_suffixes['vala']:
+ vapiname = dep.vala_vapi
+ fullname = os.path.join(self.get_target_dir(dep), vapiname)
+ result.add(fullname)
+ break
+ return list(result)
+
+ def split_vala_sources(self, t: build.BuildTarget) -> \
+ T.Tuple[T.MutableMapping[str, File], T.MutableMapping[str, File],
+ T.Tuple[T.MutableMapping[str, File], T.MutableMapping]]:
+ """
+ Splits the target's sources into .vala, .gs, .vapi, and other sources.
+ Handles both pre-existing and generated sources.
+
+ Returns a tuple (vala, vapi, others) each of which is a dictionary with
+ the keys being the path to the file (relative to the build directory)
+ and the value being the object that generated or represents the file.
+ """
+ vala: T.MutableMapping[str, File] = OrderedDict()
+ vapi: T.MutableMapping[str, File] = OrderedDict()
+ others: T.MutableMapping[str, File] = OrderedDict()
+ othersgen: T.MutableMapping[str, File] = OrderedDict()
+ # Split pre-existing sources
+ for s in t.get_sources():
+ # BuildTarget sources are always mesonlib.File files which are
+ # either in the source root, or generated with configure_file and
+ # in the build root
+ if not isinstance(s, File):
+ raise InvalidArguments(f'All sources in target {t!r} must be of type mesonlib.File, not {s!r}')
+ f = s.rel_to_builddir(self.build_to_src)
+ if s.endswith(('.vala', '.gs')):
+ srctype = vala
+ elif s.endswith('.vapi'):
+ srctype = vapi
+ else:
+ srctype = others
+ srctype[f] = s
+ # Split generated sources
+ for gensrc in t.get_generated_sources():
+ for s in gensrc.get_outputs():
+ f = self.get_target_generated_dir(t, gensrc, s)
+ if s.endswith(('.vala', '.gs')):
+ srctype = vala
+ elif s.endswith('.vapi'):
+ srctype = vapi
+ # Generated non-Vala (C/C++) sources. Won't be used for
+ # generating the Vala compile rule below.
+ else:
+ srctype = othersgen
+ # Duplicate outputs are disastrous
+ if f in srctype and srctype[f] is not gensrc:
+ msg = 'Duplicate output {0!r} from {1!r} {2!r}; ' \
+ 'conflicts with {0!r} from {4!r} {3!r}' \
+ ''.format(f, type(gensrc).__name__, gensrc.name,
+ srctype[f].name, type(srctype[f]).__name__)
+ raise InvalidArguments(msg)
+ # Store 'somefile.vala': GeneratedList (or CustomTarget)
+ srctype[f] = gensrc
+ return vala, vapi, (others, othersgen)
+
+ def generate_vala_compile(self, target: build.BuildTarget) -> \
+ T.Tuple[T.MutableMapping[str, File], T.MutableMapping[str, File], T.List[str]]:
+ """Vala is compiled into C. Set up all necessary build steps here."""
+ (vala_src, vapi_src, other_src) = self.split_vala_sources(target)
+ extra_dep_files = []
+ if not vala_src:
+ raise InvalidArguments(f'Vala library {target.name!r} has no Vala or Genie source files.')
+
+ valac = target.compilers['vala']
+ c_out_dir = self.get_target_private_dir(target)
+ # C files generated by valac
+ vala_c_src: T.List[str] = []
+ # Files generated by valac
+ valac_outputs: T.List = []
+ # All sources that are passed to valac on the commandline
+ all_files = list(vapi_src)
+ # Passed as --basedir
+ srcbasedir = os.path.join(self.build_to_src, target.get_subdir())
+ for (vala_file, gensrc) in vala_src.items():
+ all_files.append(vala_file)
+ # Figure out where the Vala compiler will write the compiled C file
+ #
+ # If the Vala file is in a subdir of the build dir (in our case
+ # because it was generated/built by something else), and is also
+ # a subdir of --basedir (because the builddir is in the source
+ # tree, and the target subdir is the source root), the subdir
+ # components from the source root till the private builddir will be
+ # duplicated inside the private builddir. Otherwise, just the
+ # basename will be used.
+ #
+ # If the Vala file is outside the build directory, the paths from
+ # the --basedir till the subdir will be duplicated inside the
+ # private builddir.
+ if isinstance(gensrc, (build.CustomTarget, build.GeneratedList)) or gensrc.is_built:
+ vala_c_file = os.path.splitext(os.path.basename(vala_file))[0] + '.c'
+ # Check if the vala file is in a subdir of --basedir
+ abs_srcbasedir = os.path.join(self.environment.get_source_dir(), target.get_subdir())
+ abs_vala_file = os.path.join(self.environment.get_build_dir(), vala_file)
+ if PurePath(os.path.commonpath((abs_srcbasedir, abs_vala_file))) == PurePath(abs_srcbasedir):
+ vala_c_subdir = PurePath(abs_vala_file).parent.relative_to(abs_srcbasedir)
+ vala_c_file = os.path.join(str(vala_c_subdir), vala_c_file)
+ else:
+ path_to_target = os.path.join(self.build_to_src, target.get_subdir())
+ if vala_file.startswith(path_to_target):
+ vala_c_file = os.path.splitext(os.path.relpath(vala_file, path_to_target))[0] + '.c'
+ else:
+ vala_c_file = os.path.splitext(os.path.basename(vala_file))[0] + '.c'
+ # All this will be placed inside the c_out_dir
+ vala_c_file = os.path.join(c_out_dir, vala_c_file)
+ vala_c_src.append(vala_c_file)
+ valac_outputs.append(vala_c_file)
+
+ args = self.generate_basic_compiler_args(target, valac)
+ args += valac.get_colorout_args(self.environment.coredata.options.get(OptionKey('b_colorout')).value)
+ # Tell Valac to output everything in our private directory. Sadly this
+ # means it will also preserve the directory components of Vala sources
+ # found inside the build tree (generated sources).
+ args += ['--directory', c_out_dir]
+ args += ['--basedir', srcbasedir]
+ if target.is_linkable_target():
+ # Library name
+ args += ['--library', target.name]
+ # Outputted header
+ hname = os.path.join(self.get_target_dir(target), target.vala_header)
+ args += ['--header', hname]
+ if target.is_unity:
+ # Without this the declarations will get duplicated in the .c
+ # files and cause a build failure when all of them are
+ # #include-d in one .c file.
+ # https://github.com/mesonbuild/meson/issues/1969
+ args += ['--use-header']
+ valac_outputs.append(hname)
+ # Outputted vapi file
+ vapiname = os.path.join(self.get_target_dir(target), target.vala_vapi)
+ # Force valac to write the vapi and gir files in the target build dir.
+ # Without this, it will write it inside c_out_dir
+ args += ['--vapi', os.path.join('..', target.vala_vapi)]
+ valac_outputs.append(vapiname)
+ target.outputs += [target.vala_header, target.vala_vapi]
+ target.install_tag += ['devel', 'devel']
+ # Install header and vapi to default locations if user requests this
+ if len(target.install_dir) > 1 and target.install_dir[1] is True:
+ target.install_dir[1] = self.environment.get_includedir()
+ if len(target.install_dir) > 2 and target.install_dir[2] is True:
+ target.install_dir[2] = os.path.join(self.environment.get_datadir(), 'vala', 'vapi')
+ # Generate GIR if requested
+ if isinstance(target.vala_gir, str):
+ girname = os.path.join(self.get_target_dir(target), target.vala_gir)
+ args += ['--gir', os.path.join('..', target.vala_gir)]
+ valac_outputs.append(girname)
+ target.outputs.append(target.vala_gir)
+ target.install_tag.append('devel')
+ # Install GIR to default location if requested by user
+ if len(target.install_dir) > 3 and target.install_dir[3] is True:
+ target.install_dir[3] = os.path.join(self.environment.get_datadir(), 'gir-1.0')
+ # Detect gresources and add --gresources arguments for each
+ for gensrc in other_src[1].values():
+ if isinstance(gensrc, gnome.GResourceTarget):
+ gres_xml, = self.get_custom_target_sources(gensrc)
+ args += ['--gresources=' + gres_xml]
+ extra_args = []
+
+ for a in target.extra_args.get('vala', []):
+ if isinstance(a, File):
+ relname = a.rel_to_builddir(self.build_to_src)
+ extra_dep_files.append(relname)
+ extra_args.append(relname)
+ else:
+ extra_args.append(a)
+ dependency_vapis = self.determine_dep_vapis(target)
+ extra_dep_files += dependency_vapis
+ args += extra_args
+ element = NinjaBuildElement(self.all_outputs, valac_outputs,
+ self.compiler_to_rule_name(valac),
+ all_files + dependency_vapis)
+ element.add_item('ARGS', args)
+ element.add_dep(extra_dep_files)
+ self.add_build(element)
+ self.create_target_source_introspection(target, valac, args, all_files, [])
+ return other_src[0], other_src[1], vala_c_src
+
+ def generate_cython_transpile(self, target: build.BuildTarget) -> \
+ T.Tuple[T.MutableMapping[str, File], T.MutableMapping[str, File], T.List[str]]:
+ """Generate rules for transpiling Cython files to C or C++
+
+ XXX: Currently only C is handled.
+ """
+ static_sources: T.MutableMapping[str, File] = OrderedDict()
+ generated_sources: T.MutableMapping[str, File] = OrderedDict()
+ cython_sources: T.List[str] = []
+
+ cython = target.compilers['cython']
+
+ args: T.List[str] = []
+ args += cython.get_always_args()
+ args += cython.get_buildtype_args(target.get_option(OptionKey('buildtype')))
+ args += cython.get_debug_args(target.get_option(OptionKey('debug')))
+ args += cython.get_optimization_args(target.get_option(OptionKey('optimization')))
+ args += cython.get_option_compile_args(target.get_options())
+ args += self.build.get_global_args(cython, target.for_machine)
+ args += self.build.get_project_args(cython, target.subproject, target.for_machine)
+ args += target.get_extra_args('cython')
+
+ ext = target.get_option(OptionKey('language', machine=target.for_machine, lang='cython'))
+
+ pyx_sources = [] # Keep track of sources we're adding to build
+
+ for src in target.get_sources():
+ if src.endswith('.pyx'):
+ output = os.path.join(self.get_target_private_dir(target), f'{src}.{ext}')
+ element = NinjaBuildElement(
+ self.all_outputs, [output],
+ self.compiler_to_rule_name(cython),
+ [src.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir())])
+ element.add_item('ARGS', args)
+ self.add_build(element)
+ # TODO: introspection?
+ cython_sources.append(output)
+ pyx_sources.append(element)
+ else:
+ static_sources[src.rel_to_builddir(self.build_to_src)] = src
+
+ header_deps = [] # Keep track of generated headers for those sources
+ for gen in target.get_generated_sources():
+ for ssrc in gen.get_outputs():
+ if isinstance(gen, GeneratedList):
+ ssrc = os.path.join(self.get_target_private_dir(target), ssrc)
+ else:
+ ssrc = os.path.join(gen.get_subdir(), ssrc)
+ if ssrc.endswith('.pyx'):
+ output = os.path.join(self.get_target_private_dir(target), f'{ssrc}.{ext}')
+ element = NinjaBuildElement(
+ self.all_outputs, [output],
+ self.compiler_to_rule_name(cython),
+ [ssrc])
+ element.add_item('ARGS', args)
+ self.add_build(element)
+ pyx_sources.append(element)
+ # TODO: introspection?
+ cython_sources.append(output)
+ else:
+ generated_sources[ssrc] = mesonlib.File.from_built_file(gen.get_subdir(), ssrc)
+ # Following logic in L883-900 where we determine whether to add generated source
+ # as a header(order-only) dep to the .so compilation rule
+ if not self.environment.is_source(ssrc) and \
+ not self.environment.is_object(ssrc) and \
+ not self.environment.is_library(ssrc) and \
+ not modules.is_module_library(ssrc):
+ header_deps.append(ssrc)
+ for source in pyx_sources:
+ source.add_orderdep(header_deps)
+
+ return static_sources, generated_sources, cython_sources
+
+ def _generate_copy_target(self, src: 'mesonlib.FileOrString', output: Path) -> None:
+ """Create a target to copy a source file from one location to another."""
+ if isinstance(src, File):
+ instr = src.absolute_path(self.environment.source_dir, self.environment.build_dir)
+ else:
+ instr = src
+ elem = NinjaBuildElement(self.all_outputs, [str(output)], 'COPY_FILE', [instr])
+ elem.add_orderdep(instr)
+ self.add_build(elem)
+
+ def __generate_sources_structure(self, root: Path, structured_sources: build.StructuredSources) -> T.Tuple[T.List[str], T.Optional[str]]:
+ first_file: T.Optional[str] = None
+ orderdeps: T.List[str] = []
+ for path, files in structured_sources.sources.items():
+ for file in files:
+ if isinstance(file, File):
+ out = root / path / Path(file.fname).name
+ orderdeps.append(str(out))
+ self._generate_copy_target(file, out)
+ if first_file is None:
+ first_file = str(out)
+ else:
+ for f in file.get_outputs():
+ out = root / path / f
+ orderdeps.append(str(out))
+ self._generate_copy_target(str(Path(file.subdir) / f), out)
+ if first_file is None:
+ first_file = str(out)
+ return orderdeps, first_file
+
+ def _add_rust_project_entry(self, name: str, main_rust_file: str, args: CompilerArgs,
+ from_subproject: bool, is_proc_macro: bool,
+ output: str, deps: T.List[RustDep]) -> None:
+ raw_edition: T.Optional[str] = mesonlib.first(reversed(args), lambda x: x.startswith('--edition'))
+ edition: RUST_EDITIONS = '2015' if not raw_edition else raw_edition.split('=')[-1]
+
+ cfg: T.List[str] = []
+ arg_itr: T.Iterator[str] = iter(args)
+ for arg in arg_itr:
+ if arg == '--cfg':
+ cfg.append(next(arg_itr))
+ elif arg.startswith('--cfg'):
+ cfg.append(arg[len('--cfg'):])
+
+ crate = RustCrate(
+ len(self.rust_crates),
+ name,
+ main_rust_file,
+ edition,
+ deps,
+ cfg,
+ is_workspace_member=not from_subproject,
+ is_proc_macro=is_proc_macro,
+ proc_macro_dylib_path=output if is_proc_macro else None,
+ )
+
+ self.rust_crates[name] = crate
+
+ def generate_rust_target(self, target: build.BuildTarget) -> None:
+ rustc = target.compilers['rust']
+ # Rust compiler takes only the main file as input and
+ # figures out what other files are needed via import
+ # statements and magic.
+ base_proxy = target.get_options()
+ args = rustc.compiler_args()
+ # Compiler args for compiling this target
+ args += compilers.get_base_compile_args(base_proxy, rustc)
+ self.generate_generator_list_rules(target)
+
+ # dependencies need to cause a relink, they're not just for ordering
+ deps = [
+ os.path.join(t.subdir, t.get_filename())
+ for t in itertools.chain(target.link_targets, target.link_whole_targets)
+ ]
+
+ # Dependencies for rust-project.json
+ project_deps: T.List[RustDep] = []
+
+ orderdeps: T.List[str] = []
+
+ main_rust_file = None
+ if target.structured_sources:
+ if target.structured_sources.needs_copy():
+ _ods, main_rust_file = self.__generate_sources_structure(Path(
+ self.get_target_private_dir(target)) / 'structured', target.structured_sources)
+ orderdeps.extend(_ods)
+ else:
+ # The only way to get here is to have only files in the "root"
+ # positional argument, which are all generated into the same
+ # directory
+ g = target.structured_sources.first_file()
+
+ if isinstance(g, File):
+ main_rust_file = g.rel_to_builddir(self.build_to_src)
+ elif isinstance(g, GeneratedList):
+ main_rust_file = os.path.join(self.get_target_private_dir(target), g.get_outputs()[0])
+ else:
+ main_rust_file = os.path.join(g.get_subdir(), g.get_outputs()[0])
+
+ for f in target.structured_sources.as_list():
+ if isinstance(f, File):
+ orderdeps.append(f.rel_to_builddir(self.build_to_src))
+ else:
+ orderdeps.extend([os.path.join(self.build_to_src, f.subdir, s)
+ for s in f.get_outputs()])
+
+ for i in target.get_sources():
+ if not rustc.can_compile(i):
+ raise InvalidArguments(f'Rust target {target.get_basename()} contains a non-rust source file.')
+ if main_rust_file is None:
+ main_rust_file = i.rel_to_builddir(self.build_to_src)
+ for g in target.get_generated_sources():
+ for i in g.get_outputs():
+ if not rustc.can_compile(i):
+ raise InvalidArguments(f'Rust target {target.get_basename()} contains a non-rust source file.')
+ if isinstance(g, GeneratedList):
+ fname = os.path.join(self.get_target_private_dir(target), i)
+ else:
+ fname = os.path.join(g.get_subdir(), i)
+ if main_rust_file is None:
+ main_rust_file = fname
+ orderdeps.append(fname)
+ if main_rust_file is None:
+ raise RuntimeError('A Rust target has no Rust sources. This is weird. Also a bug. Please report')
+ target_name = os.path.join(target.subdir, target.get_filename())
+ if isinstance(target, build.Executable):
+ cratetype = 'bin'
+ elif hasattr(target, 'rust_crate_type'):
+ cratetype = target.rust_crate_type
+ elif isinstance(target, build.SharedLibrary):
+ cratetype = 'dylib'
+ elif isinstance(target, build.StaticLibrary):
+ cratetype = 'rlib'
+ else:
+ raise InvalidArguments('Unknown target type for rustc.')
+ args.extend(['--crate-type', cratetype])
+
+ # If we're dynamically linking, add those arguments
+ #
+ # Rust is super annoying, calling -C link-arg foo does not work, it has
+ # to be -C link-arg=foo
+ if cratetype in {'bin', 'dylib'}:
+ args.extend(rustc.get_linker_always_args())
+
+ args += self.generate_basic_compiler_args(target, rustc, False)
+ # Rustc replaces - with _. spaces are not allowed, so we replace them with underscores
+ args += ['--crate-name', target.name.replace('-', '_').replace(' ', '_')]
+ depfile = os.path.join(target.subdir, target.name + '.d')
+ args += ['--emit', f'dep-info={depfile}', '--emit', 'link']
+ args += target.get_extra_args('rust')
+ output = rustc.get_output_args(os.path.join(target.subdir, target.get_filename()))
+ args += output
+ linkdirs = mesonlib.OrderedSet()
+ external_deps = target.external_deps.copy()
+ # TODO: we likely need to use verbatim to handle name_prefix and name_suffix
+ for d in target.link_targets:
+ linkdirs.add(d.subdir)
+ if d.uses_rust():
+ # specify `extern CRATE_NAME=OUTPUT_FILE` for each Rust
+ # dependency, so that collisions with libraries in rustc's
+ # sysroot don't cause ambiguity
+ args += ['--extern', '{}={}'.format(d.name, os.path.join(d.subdir, d.filename))]
+ project_deps.append(RustDep(d.name, self.rust_crates[d.name].order))
+ elif d.typename == 'static library':
+ # Rustc doesn't follow Meson's convention that static libraries
+ # are called .a, and import libraries are .lib, so we have to
+ # manually handle that.
+ if rustc.linker.id in {'link', 'lld-link'}:
+ args += ['-C', f'link-arg={self.get_target_filename_for_linking(d)}']
+ else:
+ args += ['-l', f'static={d.name}']
+ external_deps.extend(d.external_deps)
+ else:
+ # Rust uses -l for non rust dependencies, but we still need to
+ # add dylib=foo
+ args += ['-l', f'dylib={d.name}']
+
+ # Since 1.61.0 Rust has a special modifier for whole-archive linking,
+ # before that it would treat linking two static libraries as
+ # whole-archive linking. However, to make this work we have to disable
+ # bundling, which can't be done until 1.63.0… So for 1.61–1.62 we just
+ # have to hope that the default cases of +whole-archive are sufficent.
+ # See: https://github.com/rust-lang/rust/issues/99429
+ if mesonlib.version_compare(rustc.version, '>= 1.63.0'):
+ whole_archive = ':+whole-archive,-bundle'
+ else:
+ whole_archive = ''
+
+ if mesonlib.version_compare(rustc.version, '>= 1.67.0'):
+ verbatim = ',+verbatim'
+ else:
+ verbatim = ''
+
+ for d in target.link_whole_targets:
+ linkdirs.add(d.subdir)
+ if d.uses_rust():
+ # specify `extern CRATE_NAME=OUTPUT_FILE` for each Rust
+ # dependency, so that collisions with libraries in rustc's
+ # sysroot don't cause ambiguity
+ args += ['--extern', '{}={}'.format(d.name, os.path.join(d.subdir, d.filename))]
+ project_deps.append(RustDep(d.name, self.rust_crates[d.name].order))
+ else:
+ if rustc.linker.id in {'link', 'lld-link'}:
+ if verbatim:
+ # If we can use the verbatim modifier, then everything is great
+ args += ['-l', f'static{whole_archive}{verbatim}={d.get_outputs()[0]}']
+ elif isinstance(target, build.StaticLibrary):
+ # If we don't, for static libraries the only option is
+ # to make a copy, since we can't pass objects in, or
+ # directly affect the archiver. but we're not going to
+ # do that given how quickly rustc versions go out of
+ # support unless there's a compelling reason to do so.
+ # This only affects 1.61–1.66
+ mlog.warning('Due to limitations in Rustc versions 1.61–1.66 and meson library naming',
+ 'whole-archive linking with MSVC may or may not work. Upgrade rustc to',
+ '>= 1.67. A best effort is being made, but likely won\'t work')
+ args += ['-l', f'static={d.name}']
+ else:
+ # When doing dynamic linking (binaries and [c]dylibs),
+ # we can instead just proxy the correct arguments to the linker
+ for link_whole_arg in rustc.linker.get_link_whole_for([self.get_target_filename_for_linking(d)]):
+ args += ['-C', f'link-arg={link_whole_arg}']
+ else:
+ args += ['-l', f'static{whole_archive}={d.name}']
+ external_deps.extend(d.external_deps)
+ for e in external_deps:
+ for a in e.get_link_args():
+ if a.endswith(('.dll', '.so', '.dylib')):
+ dir_, lib = os.path.split(a)
+ linkdirs.add(dir_)
+ lib, ext = os.path.splitext(lib)
+ if lib.startswith('lib'):
+ lib = lib[3:]
+ args.extend(['-l', f'dylib={lib}'])
+ elif a.startswith('-L'):
+ args.append(a)
+ elif a.startswith('-l'):
+ _type = 'static' if e.static else 'dylib'
+ args.extend(['-l', f'{_type}={a[2:]}'])
+ for d in linkdirs:
+ if d == '':
+ d = '.'
+ args += ['-L', d]
+ has_shared_deps = any(isinstance(dep, build.SharedLibrary) for dep in target.get_dependencies())
+ if isinstance(target, build.SharedLibrary) or has_shared_deps:
+ # add prefer-dynamic if any of the Rust libraries we link
+ # against are dynamic, otherwise we'll end up with
+ # multiple implementations of crates
+ args += ['-C', 'prefer-dynamic']
+
+ # build the usual rpath arguments as well...
+
+ # Set runtime-paths so we can run executables without needing to set
+ # LD_LIBRARY_PATH, etc in the environment. Doesn't work on Windows.
+ if has_path_sep(target.name):
+ # Target names really should not have slashes in them, but
+ # unfortunately we did not check for that and some downstream projects
+ # now have them. Once slashes are forbidden, remove this bit.
+ target_slashname_workaround_dir = os.path.join(os.path.dirname(target.name),
+ self.get_target_dir(target))
+ else:
+ target_slashname_workaround_dir = self.get_target_dir(target)
+ rpath_args, target.rpath_dirs_to_remove = (
+ rustc.build_rpath_args(self.environment,
+ self.environment.get_build_dir(),
+ target_slashname_workaround_dir,
+ self.determine_rpath_dirs(target),
+ target.build_rpath,
+ target.install_rpath))
+ # ... but then add rustc's sysroot to account for rustup
+ # installations
+ for rpath_arg in rpath_args:
+ args += ['-C', 'link-arg=' + rpath_arg + ':' + os.path.join(rustc.get_sysroot(), 'lib')]
+
+ self._add_rust_project_entry(target.name, main_rust_file, args, bool(target.subproject),
+ #XXX: There is a fix for this pending
+ getattr(target, 'rust_crate_type', '') == 'procmacro',
+ output, project_deps)
+
+ compiler_name = self.compiler_to_rule_name(rustc)
+ element = NinjaBuildElement(self.all_outputs, target_name, compiler_name, main_rust_file)
+ if orderdeps:
+ element.add_orderdep(orderdeps)
+ if deps:
+ element.add_dep(deps)
+ element.add_item('ARGS', args)
+ element.add_item('targetdep', depfile)
+ element.add_item('cratetype', cratetype)
+ self.add_build(element)
+ if isinstance(target, build.SharedLibrary):
+ self.generate_shsym(target)
+ self.create_target_source_introspection(target, rustc, args, [main_rust_file], [])
+
+ @staticmethod
+ def get_rule_suffix(for_machine: MachineChoice) -> str:
+ return PerMachine('_FOR_BUILD', '')[for_machine]
+
+ @classmethod
+ def get_compiler_rule_name(cls, lang: str, for_machine: MachineChoice, mode: str = 'COMPILER') -> str:
+ return f'{lang}_{mode}{cls.get_rule_suffix(for_machine)}'
+
+ @classmethod
+ def compiler_to_rule_name(cls, compiler: Compiler) -> str:
+ return cls.get_compiler_rule_name(compiler.get_language(), compiler.for_machine, compiler.mode)
+
+ @classmethod
+ def compiler_to_pch_rule_name(cls, compiler: Compiler) -> str:
+ return cls.get_compiler_rule_name(compiler.get_language(), compiler.for_machine, 'PCH')
+
+ def swift_module_file_name(self, target):
+ return os.path.join(self.get_target_private_dir(target),
+ self.target_swift_modulename(target) + '.swiftmodule')
+
+ def target_swift_modulename(self, target):
+ return target.name
+
+ def determine_swift_dep_modules(self, target):
+ result = []
+ for l in target.link_targets:
+ if self.is_swift_target(l):
+ result.append(self.swift_module_file_name(l))
+ return result
+
+ def get_swift_link_deps(self, target):
+ result = []
+ for l in target.link_targets:
+ result.append(self.get_target_filename(l))
+ return result
+
+ def split_swift_generated_sources(self, target):
+ all_srcs = self.get_target_generated_sources(target)
+ srcs = []
+ others = []
+ for i in all_srcs:
+ if i.endswith('.swift'):
+ srcs.append(i)
+ else:
+ others.append(i)
+ return srcs, others
+
+ def generate_swift_target(self, target):
+ module_name = self.target_swift_modulename(target)
+ swiftc = target.compilers['swift']
+ abssrc = []
+ relsrc = []
+ abs_headers = []
+ header_imports = []
+ for i in target.get_sources():
+ if swiftc.can_compile(i):
+ rels = i.rel_to_builddir(self.build_to_src)
+ abss = os.path.normpath(os.path.join(self.environment.get_build_dir(), rels))
+ relsrc.append(rels)
+ abssrc.append(abss)
+ elif self.environment.is_header(i):
+ relh = i.rel_to_builddir(self.build_to_src)
+ absh = os.path.normpath(os.path.join(self.environment.get_build_dir(), relh))
+ abs_headers.append(absh)
+ header_imports += swiftc.get_header_import_args(absh)
+ else:
+ raise InvalidArguments(f'Swift target {target.get_basename()} contains a non-swift source file.')
+ os.makedirs(self.get_target_private_dir_abs(target), exist_ok=True)
+ compile_args = swiftc.get_compile_only_args()
+ compile_args += swiftc.get_optimization_args(target.get_option(OptionKey('optimization')))
+ compile_args += swiftc.get_debug_args(target.get_option(OptionKey('debug')))
+ compile_args += swiftc.get_module_args(module_name)
+ compile_args += self.build.get_project_args(swiftc, target.subproject, target.for_machine)
+ compile_args += self.build.get_global_args(swiftc, target.for_machine)
+ for i in reversed(target.get_include_dirs()):
+ basedir = i.get_curdir()
+ for d in i.get_incdirs():
+ if d not in ('', '.'):
+ expdir = os.path.join(basedir, d)
+ else:
+ expdir = basedir
+ srctreedir = os.path.normpath(os.path.join(self.environment.get_build_dir(), self.build_to_src, expdir))
+ sargs = swiftc.get_include_args(srctreedir, False)
+ compile_args += sargs
+ link_args = swiftc.get_output_args(os.path.join(self.environment.get_build_dir(), self.get_target_filename(target)))
+ link_args += self.build.get_project_link_args(swiftc, target.subproject, target.for_machine)
+ link_args += self.build.get_global_link_args(swiftc, target.for_machine)
+ rundir = self.get_target_private_dir(target)
+ out_module_name = self.swift_module_file_name(target)
+ in_module_files = self.determine_swift_dep_modules(target)
+ abs_module_dirs = self.determine_swift_dep_dirs(target)
+ module_includes = []
+ for x in abs_module_dirs:
+ module_includes += swiftc.get_include_args(x, False)
+ link_deps = self.get_swift_link_deps(target)
+ abs_link_deps = [os.path.join(self.environment.get_build_dir(), x) for x in link_deps]
+ for d in target.link_targets:
+ reldir = self.get_target_dir(d)
+ if reldir == '':
+ reldir = '.'
+ link_args += ['-L', os.path.normpath(os.path.join(self.environment.get_build_dir(), reldir))]
+ (rel_generated, _) = self.split_swift_generated_sources(target)
+ abs_generated = [os.path.join(self.environment.get_build_dir(), x) for x in rel_generated]
+ # We need absolute paths because swiftc needs to be invoked in a subdir
+ # and this is the easiest way about it.
+ objects = [] # Relative to swift invocation dir
+ rel_objects = [] # Relative to build.ninja
+ for i in abssrc + abs_generated:
+ base = os.path.basename(i)
+ oname = os.path.splitext(base)[0] + '.o'
+ objects.append(oname)
+ rel_objects.append(os.path.join(self.get_target_private_dir(target), oname))
+
+ rulename = self.compiler_to_rule_name(swiftc)
+
+ # Swiftc does not seem to be able to emit objects and module files in one go.
+ elem = NinjaBuildElement(self.all_outputs, rel_objects, rulename, abssrc)
+ elem.add_dep(in_module_files + rel_generated)
+ elem.add_dep(abs_headers)
+ elem.add_item('ARGS', compile_args + header_imports + abs_generated + module_includes)
+ elem.add_item('RUNDIR', rundir)
+ self.add_build(elem)
+ elem = NinjaBuildElement(self.all_outputs, out_module_name, rulename, abssrc)
+ elem.add_dep(in_module_files + rel_generated)
+ elem.add_item('ARGS', compile_args + abs_generated + module_includes + swiftc.get_mod_gen_args())
+ elem.add_item('RUNDIR', rundir)
+ self.add_build(elem)
+ if isinstance(target, build.StaticLibrary):
+ elem = self.generate_link(target, self.get_target_filename(target),
+ rel_objects, self.build.static_linker[target.for_machine])
+ self.add_build(elem)
+ elif isinstance(target, build.Executable):
+ elem = NinjaBuildElement(self.all_outputs, self.get_target_filename(target), rulename, [])
+ elem.add_dep(rel_objects)
+ elem.add_dep(link_deps)
+ elem.add_item('ARGS', link_args + swiftc.get_std_exe_link_args() + objects + abs_link_deps)
+ elem.add_item('RUNDIR', rundir)
+ self.add_build(elem)
+ else:
+ raise MesonException('Swift supports only executable and static library targets.')
+ # Introspection information
+ self.create_target_source_introspection(target, swiftc, compile_args + header_imports + module_includes, relsrc, rel_generated)
+
+ def _rsp_options(self, tool: T.Union['Compiler', 'StaticLinker', 'DynamicLinker']) -> T.Dict[str, T.Union[bool, RSPFileSyntax]]:
+ """Helper method to get rsp options.
+
+ rsp_file_syntax() is only guaranteed to be implemented if
+ can_linker_accept_rsp() returns True.
+ """
+ options = {'rspable': tool.can_linker_accept_rsp()}
+ if options['rspable']:
+ options['rspfile_quote_style'] = tool.rsp_file_syntax()
+ return options
+
+ def generate_static_link_rules(self):
+ num_pools = self.environment.coredata.options[OptionKey('backend_max_links')].value
+ if 'java' in self.environment.coredata.compilers.host:
+ self.generate_java_link()
+ for for_machine in MachineChoice:
+ static_linker = self.build.static_linker[for_machine]
+ if static_linker is None:
+ continue
+ rule = 'STATIC_LINKER{}'.format(self.get_rule_suffix(for_machine))
+ cmdlist = []
+ args = ['$in']
+ # FIXME: Must normalize file names with pathlib.Path before writing
+ # them out to fix this properly on Windows. See:
+ # https://github.com/mesonbuild/meson/issues/1517
+ # https://github.com/mesonbuild/meson/issues/1526
+ if isinstance(static_linker, ArLinker) and not mesonlib.is_windows():
+ # `ar` has no options to overwrite archives. It always appends,
+ # which is never what we want. Delete an existing library first if
+ # it exists. https://github.com/mesonbuild/meson/issues/1355
+ cmdlist = execute_wrapper + [c.format('$out') for c in rmfile_prefix]
+ cmdlist += static_linker.get_exelist()
+ cmdlist += ['$LINK_ARGS']
+ cmdlist += NinjaCommandArg.list(static_linker.get_output_args('$out'), Quoting.none)
+ description = 'Linking static target $out'
+ if num_pools > 0:
+ pool = 'pool = link_pool'
+ else:
+ pool = None
+
+ options = self._rsp_options(static_linker)
+ self.add_rule(NinjaRule(rule, cmdlist, args, description, **options, extra=pool))
+
+ def generate_dynamic_link_rules(self):
+ num_pools = self.environment.coredata.options[OptionKey('backend_max_links')].value
+ for for_machine in MachineChoice:
+ complist = self.environment.coredata.compilers[for_machine]
+ for langname, compiler in complist.items():
+ if langname in {'java', 'vala', 'rust', 'cs', 'cython'}:
+ continue
+ rule = '{}_LINKER{}'.format(langname, self.get_rule_suffix(for_machine))
+ command = compiler.get_linker_exelist()
+ args = ['$ARGS'] + NinjaCommandArg.list(compiler.get_linker_output_args('$out'), Quoting.none) + ['$in', '$LINK_ARGS']
+ description = 'Linking target $out'
+ if num_pools > 0:
+ pool = 'pool = link_pool'
+ else:
+ pool = None
+
+ options = self._rsp_options(compiler)
+ self.add_rule(NinjaRule(rule, command, args, description, **options, extra=pool))
+
+ args = self.environment.get_build_command() + \
+ ['--internal',
+ 'symbolextractor',
+ self.environment.get_build_dir(),
+ '$in',
+ '$IMPLIB',
+ '$out']
+ symrule = 'SHSYM'
+ symcmd = args + ['$CROSS']
+ syndesc = 'Generating symbol file $out'
+ synstat = 'restat = 1'
+ self.add_rule(NinjaRule(symrule, symcmd, [], syndesc, extra=synstat))
+
+ def generate_java_compile_rule(self, compiler):
+ rule = self.compiler_to_rule_name(compiler)
+ command = compiler.get_exelist() + ['$ARGS', '$in']
+ description = 'Compiling Java object $in'
+ self.add_rule(NinjaRule(rule, command, [], description))
+
+ def generate_cs_compile_rule(self, compiler: 'CsCompiler') -> None:
+ rule = self.compiler_to_rule_name(compiler)
+ command = compiler.get_exelist()
+ args = ['$ARGS', '$in']
+ description = 'Compiling C Sharp target $out'
+ self.add_rule(NinjaRule(rule, command, args, description,
+ rspable=mesonlib.is_windows(),
+ rspfile_quote_style=compiler.rsp_file_syntax()))
+
+ def generate_vala_compile_rules(self, compiler):
+ rule = self.compiler_to_rule_name(compiler)
+ command = compiler.get_exelist() + ['$ARGS', '$in']
+ description = 'Compiling Vala source $in'
+ self.add_rule(NinjaRule(rule, command, [], description, extra='restat = 1'))
+
+ def generate_cython_compile_rules(self, compiler: 'Compiler') -> None:
+ rule = self.compiler_to_rule_name(compiler)
+ description = 'Compiling Cython source $in'
+ command = compiler.get_exelist()
+
+ depargs = compiler.get_dependency_gen_args('$out', '$DEPFILE')
+ depfile = '$out.dep' if depargs else None
+
+ args = depargs + ['$ARGS', '$in']
+ args += NinjaCommandArg.list(compiler.get_output_args('$out'), Quoting.none)
+ self.add_rule(NinjaRule(rule, command + args, [],
+ description,
+ depfile=depfile,
+ extra='restat = 1'))
+
+ def generate_rust_compile_rules(self, compiler):
+ rule = self.compiler_to_rule_name(compiler)
+ command = compiler.get_exelist() + ['$ARGS', '$in']
+ description = 'Compiling Rust source $in'
+ depfile = '$targetdep'
+ depstyle = 'gcc'
+ self.add_rule(NinjaRule(rule, command, [], description, deps=depstyle,
+ depfile=depfile))
+
+ def generate_swift_compile_rules(self, compiler):
+ rule = self.compiler_to_rule_name(compiler)
+ full_exe = self.environment.get_build_command() + [
+ '--internal',
+ 'dirchanger',
+ '$RUNDIR',
+ ]
+ invoc = full_exe + compiler.get_exelist()
+ command = invoc + ['$ARGS', '$in']
+ description = 'Compiling Swift source $in'
+ self.add_rule(NinjaRule(rule, command, [], description))
+
+ def use_dyndeps_for_fortran(self) -> bool:
+ '''Use the new Ninja feature for scanning dependencies during build,
+ rather than up front. Remove this and all old scanning code once Ninja
+ minimum version is bumped to 1.10.'''
+ return mesonlib.version_compare(self.ninja_version, '>=1.10.0')
+
+ def generate_fortran_dep_hack(self, crstr: str) -> None:
+ if self.use_dyndeps_for_fortran():
+ return
+ rule = f'FORTRAN_DEP_HACK{crstr}'
+ if mesonlib.is_windows():
+ cmd = ['cmd', '/C']
+ else:
+ cmd = ['true']
+ self.add_rule_comment(NinjaComment('''Workaround for these issues:
+https://groups.google.com/forum/#!topic/ninja-build/j-2RfBIOd_8
+https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485'''))
+ self.add_rule(NinjaRule(rule, cmd, [], 'Dep hack', extra='restat = 1'))
+
+ def generate_llvm_ir_compile_rule(self, compiler):
+ if self.created_llvm_ir_rule[compiler.for_machine]:
+ return
+ rule = self.get_compiler_rule_name('llvm_ir', compiler.for_machine)
+ command = compiler.get_exelist()
+ args = ['$ARGS'] + NinjaCommandArg.list(compiler.get_output_args('$out'), Quoting.none) + compiler.get_compile_only_args() + ['$in']
+ description = 'Compiling LLVM IR object $in'
+
+ options = self._rsp_options(compiler)
+
+ self.add_rule(NinjaRule(rule, command, args, description, **options))
+ self.created_llvm_ir_rule[compiler.for_machine] = True
+
+ def generate_compile_rule_for(self, langname, compiler):
+ if langname == 'java':
+ self.generate_java_compile_rule(compiler)
+ return
+ if langname == 'cs':
+ if self.environment.machines.matches_build_machine(compiler.for_machine):
+ self.generate_cs_compile_rule(compiler)
+ return
+ if langname == 'vala':
+ self.generate_vala_compile_rules(compiler)
+ return
+ if langname == 'rust':
+ self.generate_rust_compile_rules(compiler)
+ return
+ if langname == 'swift':
+ if self.environment.machines.matches_build_machine(compiler.for_machine):
+ self.generate_swift_compile_rules(compiler)
+ return
+ if langname == 'cython':
+ self.generate_cython_compile_rules(compiler)
+ return
+ crstr = self.get_rule_suffix(compiler.for_machine)
+ if langname == 'fortran':
+ self.generate_fortran_dep_hack(crstr)
+ rule = self.compiler_to_rule_name(compiler)
+ depargs = NinjaCommandArg.list(compiler.get_dependency_gen_args('$out', '$DEPFILE'), Quoting.none)
+ command = compiler.get_exelist()
+ args = ['$ARGS'] + depargs + NinjaCommandArg.list(compiler.get_output_args('$out'), Quoting.none) + compiler.get_compile_only_args() + ['$in']
+ description = f'Compiling {compiler.get_display_language()} object $out'
+ if compiler.get_argument_syntax() == 'msvc':
+ deps = 'msvc'
+ depfile = None
+ else:
+ deps = 'gcc'
+ depfile = '$DEPFILE'
+ options = self._rsp_options(compiler)
+ self.add_rule(NinjaRule(rule, command, args, description, **options,
+ deps=deps, depfile=depfile))
+
+ def generate_pch_rule_for(self, langname, compiler):
+ if langname not in {'c', 'cpp'}:
+ return
+ rule = self.compiler_to_pch_rule_name(compiler)
+ depargs = compiler.get_dependency_gen_args('$out', '$DEPFILE')
+
+ if compiler.get_argument_syntax() == 'msvc':
+ output = []
+ else:
+ output = NinjaCommandArg.list(compiler.get_output_args('$out'), Quoting.none)
+ command = compiler.get_exelist() + ['$ARGS'] + depargs + output + compiler.get_compile_only_args() + ['$in']
+ description = 'Precompiling header $in'
+ if compiler.get_argument_syntax() == 'msvc':
+ deps = 'msvc'
+ depfile = None
+ else:
+ deps = 'gcc'
+ depfile = '$DEPFILE'
+ self.add_rule(NinjaRule(rule, command, [], description, deps=deps,
+ depfile=depfile))
+
+ def generate_scanner_rules(self):
+ rulename = 'depscan'
+ if rulename in self.ruledict:
+ # Scanning command is the same for native and cross compilation.
+ return
+ command = self.environment.get_build_command() + \
+ ['--internal', 'depscan']
+ args = ['$picklefile', '$out', '$in']
+ description = 'Module scanner.'
+ rule = NinjaRule(rulename, command, args, description)
+ self.add_rule(rule)
+
+ def generate_compile_rules(self):
+ for for_machine in MachineChoice:
+ clist = self.environment.coredata.compilers[for_machine]
+ for langname, compiler in clist.items():
+ if compiler.get_id() == 'clang':
+ self.generate_llvm_ir_compile_rule(compiler)
+ self.generate_compile_rule_for(langname, compiler)
+ self.generate_pch_rule_for(langname, compiler)
+ for mode in compiler.get_modes():
+ self.generate_compile_rule_for(langname, mode)
+
+ def generate_generator_list_rules(self, target):
+ # CustomTargets have already written their rules and
+ # CustomTargetIndexes don't actually get generated, so write rules for
+ # GeneratedLists here
+ for genlist in target.get_generated_sources():
+ if isinstance(genlist, (build.CustomTarget, build.CustomTargetIndex)):
+ continue
+ self.generate_genlist_for_target(genlist, target)
+
+ def replace_paths(self, target, args, override_subdir=None):
+ if override_subdir:
+ source_target_dir = os.path.join(self.build_to_src, override_subdir)
+ else:
+ source_target_dir = self.get_target_source_dir(target)
+ relout = self.get_target_private_dir(target)
+ args = [x.replace("@SOURCE_DIR@", self.build_to_src).replace("@BUILD_DIR@", relout)
+ for x in args]
+ args = [x.replace("@CURRENT_SOURCE_DIR@", source_target_dir) for x in args]
+ args = [x.replace("@SOURCE_ROOT@", self.build_to_src).replace("@BUILD_ROOT@", '.')
+ for x in args]
+ args = [x.replace('\\', '/') for x in args]
+ return args
+
+ def generate_genlist_for_target(self, genlist: build.GeneratedList, target: build.BuildTarget) -> None:
+ generator = genlist.get_generator()
+ subdir = genlist.subdir
+ exe = generator.get_exe()
+ infilelist = genlist.get_inputs()
+ outfilelist = genlist.get_outputs()
+ extra_dependencies = self.get_custom_target_depend_files(genlist)
+ for i, curfile in enumerate(infilelist):
+ if len(generator.outputs) == 1:
+ sole_output = os.path.join(self.get_target_private_dir(target), outfilelist[i])
+ else:
+ sole_output = f'{curfile}'
+ infilename = curfile.rel_to_builddir(self.build_to_src)
+ base_args = generator.get_arglist(infilename)
+ outfiles = genlist.get_outputs_for(curfile)
+ outfiles = [os.path.join(self.get_target_private_dir(target), of) for of in outfiles]
+ if generator.depfile is None:
+ rulename = 'CUSTOM_COMMAND'
+ args = base_args
+ else:
+ rulename = 'CUSTOM_COMMAND_DEP'
+ depfilename = generator.get_dep_outname(infilename)
+ depfile = os.path.join(self.get_target_private_dir(target), depfilename)
+ args = [x.replace('@DEPFILE@', depfile) for x in base_args]
+ args = [x.replace("@INPUT@", infilename).replace('@OUTPUT@', sole_output)
+ for x in args]
+ args = self.replace_outputs(args, self.get_target_private_dir(target), outfilelist)
+ # We have consumed output files, so drop them from the list of remaining outputs.
+ if len(generator.outputs) > 1:
+ outfilelist = outfilelist[len(generator.outputs):]
+ args = self.replace_paths(target, args, override_subdir=subdir)
+ cmdlist, reason = self.as_meson_exe_cmdline(exe,
+ self.replace_extra_args(args, genlist),
+ capture=outfiles[0] if generator.capture else None)
+ abs_pdir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target))
+ os.makedirs(abs_pdir, exist_ok=True)
+
+ elem = NinjaBuildElement(self.all_outputs, outfiles, rulename, infilename)
+ elem.add_dep([self.get_target_filename(x) for x in generator.depends])
+ if generator.depfile is not None:
+ elem.add_item('DEPFILE', depfile)
+ if len(extra_dependencies) > 0:
+ elem.add_dep(extra_dependencies)
+
+ if len(generator.outputs) == 1:
+ what = f'{sole_output!r}'
+ else:
+ # since there are multiple outputs, we log the source that caused the rebuild
+ what = f'from {sole_output!r}.'
+ if reason:
+ reason = f' (wrapped by meson {reason})'
+ elem.add_item('DESC', f'Generating {what}{reason}.')
+
+ if isinstance(exe, build.BuildTarget):
+ elem.add_dep(self.get_target_filename(exe))
+ elem.add_item('COMMAND', cmdlist)
+ self.add_build(elem)
+
+ def scan_fortran_module_outputs(self, target):
+ """
+ Find all module and submodule made available in a Fortran code file.
+ """
+ if self.use_dyndeps_for_fortran():
+ return
+ compiler = None
+ # TODO other compilers
+ for lang, c in self.environment.coredata.compilers.host.items():
+ if lang == 'fortran':
+ compiler = c
+ break
+ if compiler is None:
+ self.fortran_deps[target.get_basename()] = {}
+ return
+
+ modre = re.compile(FORTRAN_MODULE_PAT, re.IGNORECASE)
+ submodre = re.compile(FORTRAN_SUBMOD_PAT, re.IGNORECASE)
+ module_files = {}
+ submodule_files = {}
+ for s in target.get_sources():
+ # FIXME, does not work for Fortran sources generated by
+ # custom_target() and generator() as those are run after
+ # the configuration (configure_file() is OK)
+ if not compiler.can_compile(s):
+ continue
+ filename = s.absolute_path(self.environment.get_source_dir(),
+ self.environment.get_build_dir())
+ # Fortran keywords must be ASCII.
+ with open(filename, encoding='ascii', errors='ignore') as f:
+ for line in f:
+ modmatch = modre.match(line)
+ if modmatch is not None:
+ modname = modmatch.group(1).lower()
+ if modname in module_files:
+ raise InvalidArguments(
+ f'Namespace collision: module {modname} defined in '
+ f'two files {module_files[modname]} and {s}.')
+ module_files[modname] = s
+ else:
+ submodmatch = submodre.match(line)
+ if submodmatch is not None:
+ # '_' is arbitrarily used to distinguish submod from mod.
+ parents = submodmatch.group(1).lower().split(':')
+ submodname = parents[0] + '_' + submodmatch.group(2).lower()
+
+ if submodname in submodule_files:
+ raise InvalidArguments(
+ f'Namespace collision: submodule {submodname} defined in '
+ f'two files {submodule_files[submodname]} and {s}.')
+ submodule_files[submodname] = s
+
+ self.fortran_deps[target.get_basename()] = {**module_files, **submodule_files}
+
+ def get_fortran_deps(self, compiler: FortranCompiler, src: Path, target) -> T.List[str]:
+ """
+ Find all module and submodule needed by a Fortran target
+ """
+ if self.use_dyndeps_for_fortran():
+ return []
+
+ dirname = Path(self.get_target_private_dir(target))
+ tdeps = self.fortran_deps[target.get_basename()]
+ srcdir = Path(self.source_dir)
+
+ mod_files = _scan_fortran_file_deps(src, srcdir, dirname, tdeps, compiler)
+ return mod_files
+
+ def get_no_stdlib_link_args(self, target, linker):
+ if hasattr(linker, 'language') and linker.language in self.build.stdlibs[target.for_machine]:
+ return linker.get_no_stdlib_link_args()
+ return []
+
+ def get_compile_debugfile_args(self, compiler, target, objfile):
+ # The way MSVC uses PDB files is documented exactly nowhere so
+ # the following is what we have been able to decipher via
+ # reverse engineering.
+ #
+ # Each object file gets the path of its PDB file written
+ # inside it. This can be either the final PDB (for, say,
+ # foo.exe) or an object pdb (for foo.obj). If the former, then
+ # each compilation step locks the pdb file for writing, which
+ # is a bottleneck and object files from one target can not be
+ # used in a different target. The latter seems to be the
+ # sensible one (and what Unix does) but there is a catch. If
+ # you try to use precompiled headers MSVC will error out
+ # because both source and pch pdbs go in the same file and
+ # they must be the same.
+ #
+ # This means:
+ #
+ # - pch files must be compiled anew for every object file (negating
+ # the entire point of having them in the first place)
+ # - when using pch, output must go to the target pdb
+ #
+ # Since both of these are broken in some way, use the one that
+ # works for each target. This unfortunately means that you
+ # can't combine pch and object extraction in a single target.
+ #
+ # PDB files also lead to filename collisions. A target foo.exe
+ # has a corresponding foo.pdb. A shared library foo.dll _also_
+ # has pdb file called foo.pdb. So will a static library
+ # foo.lib, which clobbers both foo.pdb _and_ the dll file's
+ # export library called foo.lib (by default, currently we name
+ # them libfoo.a to avoidt this issue). You can give the files
+ # unique names such as foo_exe.pdb but VC also generates a
+ # bunch of other files which take their names from the target
+ # basename (i.e. "foo") and stomp on each other.
+ #
+ # CMake solves this problem by doing two things. First of all
+ # static libraries do not generate pdb files at
+ # all. Presumably you don't need them and VC is smart enough
+ # to look up the original data when linking (speculation, not
+ # tested). The second solution is that you can only have
+ # target named "foo" as an exe, shared lib _or_ static
+ # lib. This makes filename collisions not happen. The downside
+ # is that you can't have an executable foo that uses a shared
+ # library libfoo.so, which is a common idiom on Unix.
+ #
+ # If you feel that the above is completely wrong and all of
+ # this is actually doable, please send patches.
+
+ if target.has_pch():
+ tfilename = self.get_target_filename_abs(target)
+ return compiler.get_compile_debugfile_args(tfilename, pch=True)
+ else:
+ return compiler.get_compile_debugfile_args(objfile, pch=False)
+
+ def get_link_debugfile_name(self, linker, target, outname):
+ return linker.get_link_debugfile_name(outname)
+
+ def get_link_debugfile_args(self, linker, target, outname):
+ return linker.get_link_debugfile_args(outname)
+
+ def generate_llvm_ir_compile(self, target, src):
+ base_proxy = target.get_options()
+ compiler = get_compiler_for_source(target.compilers.values(), src)
+ commands = compiler.compiler_args()
+ # Compiler args for compiling this target
+ commands += compilers.get_base_compile_args(base_proxy, compiler)
+ if isinstance(src, File):
+ if src.is_built:
+ src_filename = os.path.join(src.subdir, src.fname)
+ else:
+ src_filename = src.fname
+ elif os.path.isabs(src):
+ src_filename = os.path.basename(src)
+ else:
+ src_filename = src
+ obj_basename = self.canonicalize_filename(src_filename)
+ rel_obj = os.path.join(self.get_target_private_dir(target), obj_basename)
+ rel_obj += '.' + self.environment.machines[target.for_machine].get_object_suffix()
+ commands += self.get_compile_debugfile_args(compiler, target, rel_obj)
+ if isinstance(src, File) and src.is_built:
+ rel_src = src.fname
+ elif isinstance(src, File):
+ rel_src = src.rel_to_builddir(self.build_to_src)
+ else:
+ raise InvalidArguments(f'Invalid source type: {src!r}')
+ # Write the Ninja build command
+ compiler_name = self.get_compiler_rule_name('llvm_ir', compiler.for_machine)
+ element = NinjaBuildElement(self.all_outputs, rel_obj, compiler_name, rel_src)
+ element.add_item('ARGS', commands)
+ self.add_build(element)
+ return (rel_obj, rel_src)
+
+ @lru_cache(maxsize=None)
+ def generate_inc_dir(self, compiler: 'Compiler', d: str, basedir: str, is_system: bool) -> \
+ T.Tuple['ImmutableListProtocol[str]', 'ImmutableListProtocol[str]']:
+ # Avoid superfluous '/.' at the end of paths when d is '.'
+ if d not in ('', '.'):
+ expdir = os.path.normpath(os.path.join(basedir, d))
+ else:
+ expdir = basedir
+ srctreedir = os.path.normpath(os.path.join(self.build_to_src, expdir))
+ sargs = compiler.get_include_args(srctreedir, is_system)
+ # There may be include dirs where a build directory has not been
+ # created for some source dir. For example if someone does this:
+ #
+ # inc = include_directories('foo/bar/baz')
+ #
+ # But never subdir()s into the actual dir.
+ if os.path.isdir(os.path.join(self.environment.get_build_dir(), expdir)):
+ bargs = compiler.get_include_args(expdir, is_system)
+ else:
+ bargs = []
+ return (sargs, bargs)
+
+ def _generate_single_compile(self, target: build.BuildTarget, compiler: 'Compiler',
+ is_generated: bool = False) -> 'CompilerArgs':
+ commands = self._generate_single_compile_base_args(target, compiler)
+ commands += self._generate_single_compile_target_args(target, compiler, is_generated)
+ return commands
+
+ def _generate_single_compile_base_args(self, target: build.BuildTarget, compiler: 'Compiler') -> 'CompilerArgs':
+ base_proxy = target.get_options()
+ # Create an empty commands list, and start adding arguments from
+ # various sources in the order in which they must override each other
+ commands = compiler.compiler_args()
+ # Start with symbol visibility.
+ commands += compiler.gnu_symbol_visibility_args(target.gnu_symbol_visibility)
+ # Add compiler args for compiling this target derived from 'base' build
+ # options passed on the command-line, in default_options, etc.
+ # These have the lowest priority.
+ commands += compilers.get_base_compile_args(base_proxy,
+ compiler)
+ return commands
+
+ @lru_cache(maxsize=None)
+ def _generate_single_compile_target_args(self, target: build.BuildTarget, compiler: 'Compiler',
+ is_generated: bool = False) -> 'ImmutableListProtocol[str]':
+ # The code generated by valac is usually crap and has tons of unused
+ # variables and such, so disable warnings for Vala C sources.
+ no_warn_args = is_generated == 'vala'
+ # Add compiler args and include paths from several sources; defaults,
+ # build options, external dependencies, etc.
+ commands = self.generate_basic_compiler_args(target, compiler, no_warn_args)
+ # Add custom target dirs as includes automatically, but before
+ # target-specific include directories.
+ if target.implicit_include_directories:
+ commands += self.get_custom_target_dir_include_args(target, compiler)
+ # Add include dirs from the `include_directories:` kwarg on the target
+ # and from `include_directories:` of internal deps of the target.
+ #
+ # Target include dirs should override internal deps include dirs.
+ # This is handled in BuildTarget.process_kwargs()
+ #
+ # Include dirs from internal deps should override include dirs from
+ # external deps and must maintain the order in which they are specified.
+ # Hence, we must reverse the list so that the order is preserved.
+ for i in reversed(target.get_include_dirs()):
+ basedir = i.get_curdir()
+ # We should iterate include dirs in reversed orders because
+ # -Ipath will add to begin of array. And without reverse
+ # flags will be added in reversed order.
+ for d in reversed(i.get_incdirs()):
+ # Add source subdir first so that the build subdir overrides it
+ (compile_obj, includeargs) = self.generate_inc_dir(compiler, d, basedir, i.is_system)
+ commands += compile_obj
+ commands += includeargs
+ for d in i.get_extra_build_dirs():
+ commands += compiler.get_include_args(d, i.is_system)
+ # Add per-target compile args, f.ex, `c_args : ['-DFOO']`. We set these
+ # near the end since these are supposed to override everything else.
+ commands += self.escape_extra_args(target.get_extra_args(compiler.get_language()))
+
+ # D specific additional flags
+ if compiler.language == 'd':
+ commands += compiler.get_feature_args(target.d_features, self.build_to_src)
+
+ # Add source dir and build dir. Project-specific and target-specific
+ # include paths must override per-target compile args, include paths
+ # from external dependencies, internal dependencies, and from
+ # per-target `include_directories:`
+ #
+ # We prefer headers in the build dir over the source dir since, for
+ # instance, the user might have an srcdir == builddir Autotools build
+ # in their source tree. Many projects that are moving to Meson have
+ # both Meson and Autotools in parallel as part of the transition.
+ if target.implicit_include_directories:
+ commands += self.get_source_dir_include_args(target, compiler)
+ if target.implicit_include_directories:
+ commands += self.get_build_dir_include_args(target, compiler)
+ # Finally add the private dir for the target to the include path. This
+ # must override everything else and must be the final path added.
+ commands += compiler.get_include_args(self.get_target_private_dir(target), False)
+ return commands
+
+ def generate_single_compile(self, target: build.BuildTarget, src,
+ is_generated=False, header_deps=None,
+ order_deps: T.Optional[T.List[str]] = None,
+ extra_args: T.Optional[T.List[str]] = None) -> None:
+ """
+ Compiles C/C++, ObjC/ObjC++, Fortran, and D sources
+ """
+ header_deps = header_deps if header_deps is not None else []
+ order_deps = order_deps if order_deps is not None else []
+
+ if isinstance(src, str) and src.endswith('.h'):
+ raise AssertionError(f'BUG: sources should not contain headers {src!r}')
+
+ compiler = get_compiler_for_source(target.compilers.values(), src)
+ commands = self._generate_single_compile_base_args(target, compiler)
+
+ # Include PCH header as first thing as it must be the first one or it will be
+ # ignored by gcc https://gcc.gnu.org/bugzilla/show_bug.cgi?id=100462
+ if self.environment.coredata.options.get(OptionKey('b_pch')) and is_generated != 'pch':
+ commands += self.get_pch_include_args(compiler, target)
+
+ commands += self._generate_single_compile_target_args(target, compiler, is_generated)
+ commands = commands.compiler.compiler_args(commands)
+
+ # Create introspection information
+ if is_generated is False:
+ self.create_target_source_introspection(target, compiler, commands, [src], [])
+ else:
+ self.create_target_source_introspection(target, compiler, commands, [], [src])
+
+ build_dir = self.environment.get_build_dir()
+ if isinstance(src, File):
+ rel_src = src.rel_to_builddir(self.build_to_src)
+ if os.path.isabs(rel_src):
+ # Source files may not be from the source directory if they originate in source-only libraries,
+ # so we can't assert that the absolute path is anywhere in particular.
+ if src.is_built:
+ assert rel_src.startswith(build_dir)
+ rel_src = rel_src[len(build_dir) + 1:]
+ elif is_generated:
+ raise AssertionError(f'BUG: broken generated source file handling for {src!r}')
+ else:
+ raise InvalidArguments(f'Invalid source type: {src!r}')
+ obj_basename = self.object_filename_from_source(target, src)
+ rel_obj = os.path.join(self.get_target_private_dir(target), obj_basename)
+ dep_file = compiler.depfile_for_object(rel_obj)
+
+ # Add MSVC debug file generation compile flags: /Fd /FS
+ commands += self.get_compile_debugfile_args(compiler, target, rel_obj)
+
+ # PCH handling
+ if self.environment.coredata.options.get(OptionKey('b_pch')):
+ pchlist = target.get_pch(compiler.language)
+ else:
+ pchlist = []
+ if not pchlist:
+ pch_dep = []
+ elif compiler.id == 'intel':
+ pch_dep = []
+ else:
+ arr = []
+ i = os.path.join(self.get_target_private_dir(target), compiler.get_pch_name(pchlist[0]))
+ arr.append(i)
+ pch_dep = arr
+
+ compiler_name = self.compiler_to_rule_name(compiler)
+ extra_deps = []
+ if compiler.get_language() == 'fortran':
+ # Can't read source file to scan for deps if it's generated later
+ # at build-time. Skip scanning for deps, and just set the module
+ # outdir argument instead.
+ # https://github.com/mesonbuild/meson/issues/1348
+ if not is_generated:
+ abs_src = Path(build_dir) / rel_src
+ extra_deps += self.get_fortran_deps(compiler, abs_src, target)
+ if not self.use_dyndeps_for_fortran():
+ # Dependency hack. Remove once multiple outputs in Ninja is fixed:
+ # https://groups.google.com/forum/#!topic/ninja-build/j-2RfBIOd_8
+ for modname, srcfile in self.fortran_deps[target.get_basename()].items():
+ modfile = os.path.join(self.get_target_private_dir(target),
+ compiler.module_name_to_filename(modname))
+
+ if srcfile == src:
+ crstr = self.get_rule_suffix(target.for_machine)
+ depelem = NinjaBuildElement(self.all_outputs,
+ modfile,
+ 'FORTRAN_DEP_HACK' + crstr,
+ rel_obj)
+ self.add_build(depelem)
+ commands += compiler.get_module_outdir_args(self.get_target_private_dir(target))
+ if extra_args is not None:
+ commands.extend(extra_args)
+
+ element = NinjaBuildElement(self.all_outputs, rel_obj, compiler_name, rel_src)
+ self.add_header_deps(target, element, header_deps)
+ for d in extra_deps:
+ element.add_dep(d)
+ for d in order_deps:
+ if isinstance(d, File):
+ d = d.rel_to_builddir(self.build_to_src)
+ elif not self.has_dir_part(d):
+ d = os.path.join(self.get_target_private_dir(target), d)
+ element.add_orderdep(d)
+ element.add_dep(pch_dep)
+ for i in self.get_fortran_orderdeps(target, compiler):
+ element.add_orderdep(i)
+ if dep_file:
+ element.add_item('DEPFILE', dep_file)
+ element.add_item('ARGS', commands)
+
+ self.add_dependency_scanner_entries_to_element(target, compiler, element, src)
+ self.add_build(element)
+ assert isinstance(rel_obj, str)
+ assert isinstance(rel_src, str)
+ return (rel_obj, rel_src.replace('\\', '/'))
+
+ def add_dependency_scanner_entries_to_element(self, target, compiler, element, src):
+ if not self.should_use_dyndeps_for_target(target):
+ return
+ extension = os.path.splitext(src.fname)[1][1:]
+ if extension != 'C':
+ extension = extension.lower()
+ if not (extension in compilers.lang_suffixes['fortran'] or extension in compilers.lang_suffixes['cpp']):
+ return
+ dep_scan_file = self.get_dep_scan_file_for(target)
+ element.add_item('dyndep', dep_scan_file)
+ element.add_orderdep(dep_scan_file)
+
+ def get_dep_scan_file_for(self, target):
+ return os.path.join(self.get_target_private_dir(target), 'depscan.dd')
+
+ def add_header_deps(self, target, ninja_element, header_deps):
+ for d in header_deps:
+ if isinstance(d, File):
+ d = d.rel_to_builddir(self.build_to_src)
+ elif not self.has_dir_part(d):
+ d = os.path.join(self.get_target_private_dir(target), d)
+ ninja_element.add_dep(d)
+
+ def has_dir_part(self, fname):
+ # FIXME FIXME: The usage of this is a terrible and unreliable hack
+ if isinstance(fname, File):
+ return fname.subdir != ''
+ return has_path_sep(fname)
+
+ # Fortran is a bit weird (again). When you link against a library, just compiling a source file
+ # requires the mod files that are output when single files are built. To do this right we would need to
+ # scan all inputs and write out explicit deps for each file. That is stoo slow and too much effort so
+ # instead just have an ordered dependency on the library. This ensures all required mod files are created.
+ # The real deps are then detected via dep file generation from the compiler. This breaks on compilers that
+ # produce incorrect dep files but such is life.
+ def get_fortran_orderdeps(self, target, compiler):
+ if compiler.language != 'fortran':
+ return []
+ return [
+ os.path.join(self.get_target_dir(lt), lt.get_filename())
+ for lt in itertools.chain(target.link_targets, target.link_whole_targets)
+ ]
+
+ def generate_msvc_pch_command(self, target, compiler, pch):
+ header = pch[0]
+ pchname = compiler.get_pch_name(header)
+ dst = os.path.join(self.get_target_private_dir(target), pchname)
+
+ commands = []
+ commands += self.generate_basic_compiler_args(target, compiler)
+
+ if len(pch) == 1:
+ # Auto generate PCH.
+ source = self.create_msvc_pch_implementation(target, compiler.get_language(), pch[0])
+ pch_header_dir = os.path.dirname(os.path.join(self.build_to_src, target.get_source_subdir(), header))
+ commands += compiler.get_include_args(pch_header_dir, False)
+ else:
+ source = os.path.join(self.build_to_src, target.get_source_subdir(), pch[1])
+
+ just_name = os.path.basename(header)
+ (objname, pch_args) = compiler.gen_pch_args(just_name, source, dst)
+ commands += pch_args
+ commands += self._generate_single_compile(target, compiler)
+ commands += self.get_compile_debugfile_args(compiler, target, objname)
+ dep = dst + '.' + compiler.get_depfile_suffix()
+ return commands, dep, dst, [objname], source
+
+ def generate_gcc_pch_command(self, target, compiler, pch):
+ commands = self._generate_single_compile(target, compiler)
+ if pch.split('.')[-1] == 'h' and compiler.language == 'cpp':
+ # Explicitly compile pch headers as C++. If Clang is invoked in C++ mode, it actually warns if
+ # this option is not set, and for gcc it also makes sense to use it.
+ commands += ['-x', 'c++-header']
+ dst = os.path.join(self.get_target_private_dir(target),
+ os.path.basename(pch) + '.' + compiler.get_pch_suffix())
+ dep = dst + '.' + compiler.get_depfile_suffix()
+ return commands, dep, dst, [] # Gcc does not create an object file during pch generation.
+
+ def generate_pch(self, target, header_deps=None):
+ header_deps = header_deps if header_deps is not None else []
+ pch_objects = []
+ for lang in ['c', 'cpp']:
+ pch = target.get_pch(lang)
+ if not pch:
+ continue
+ if not has_path_sep(pch[0]) or not has_path_sep(pch[-1]):
+ msg = f'Precompiled header of {target.get_basename()!r} must not be in the same ' \
+ 'directory as source, please put it in a subdirectory.'
+ raise InvalidArguments(msg)
+ compiler: Compiler = target.compilers[lang]
+ if compiler.get_argument_syntax() == 'msvc':
+ (commands, dep, dst, objs, src) = self.generate_msvc_pch_command(target, compiler, pch)
+ extradep = os.path.join(self.build_to_src, target.get_source_subdir(), pch[0])
+ elif compiler.id == 'intel':
+ # Intel generates on target generation
+ continue
+ else:
+ src = os.path.join(self.build_to_src, target.get_source_subdir(), pch[0])
+ (commands, dep, dst, objs) = self.generate_gcc_pch_command(target, compiler, pch[0])
+ extradep = None
+ pch_objects += objs
+ rulename = self.compiler_to_pch_rule_name(compiler)
+ elem = NinjaBuildElement(self.all_outputs, dst, rulename, src)
+ if extradep is not None:
+ elem.add_dep(extradep)
+ self.add_header_deps(target, elem, header_deps)
+ elem.add_item('ARGS', commands)
+ elem.add_item('DEPFILE', dep)
+ self.add_build(elem)
+ return pch_objects
+
+ def get_target_shsym_filename(self, target):
+ # Always name the .symbols file after the primary build output because it always exists
+ targetdir = self.get_target_private_dir(target)
+ return os.path.join(targetdir, target.get_filename() + '.symbols')
+
+ def generate_shsym(self, target):
+ target_file = self.get_target_filename(target)
+ symname = self.get_target_shsym_filename(target)
+ elem = NinjaBuildElement(self.all_outputs, symname, 'SHSYM', target_file)
+ # The library we will actually link to, which is an import library on Windows (not the DLL)
+ elem.add_item('IMPLIB', self.get_target_filename_for_linking(target))
+ if self.environment.is_cross_build():
+ elem.add_item('CROSS', '--cross-host=' + self.environment.machines[target.for_machine].system)
+ self.add_build(elem)
+
+ def get_import_filename(self, target):
+ return os.path.join(self.get_target_dir(target), target.import_filename)
+
+ def get_target_type_link_args(self, target, linker):
+ commands = []
+ if isinstance(target, build.Executable):
+ # Currently only used with the Swift compiler to add '-emit-executable'
+ commands += linker.get_std_exe_link_args()
+ # If export_dynamic, add the appropriate linker arguments
+ if target.export_dynamic:
+ commands += linker.gen_export_dynamic_link_args(self.environment)
+ # If implib, and that's significant on this platform (i.e. Windows using either GCC or Visual Studio)
+ if target.import_filename:
+ commands += linker.gen_import_library_args(self.get_import_filename(target))
+ if target.pie:
+ commands += linker.get_pie_link_args()
+ elif isinstance(target, build.SharedLibrary):
+ if isinstance(target, build.SharedModule):
+ options = self.environment.coredata.options
+ commands += linker.get_std_shared_module_link_args(options)
+ else:
+ commands += linker.get_std_shared_lib_link_args()
+ # All shared libraries are PIC
+ commands += linker.get_pic_args()
+ if not isinstance(target, build.SharedModule) or target.force_soname:
+ # Add -Wl,-soname arguments on Linux, -install_name on OS X
+ commands += linker.get_soname_args(
+ self.environment, target.prefix, target.name, target.suffix,
+ target.soversion, target.darwin_versions)
+ # This is only visited when building for Windows using either GCC or Visual Studio
+ if target.vs_module_defs and hasattr(linker, 'gen_vs_module_defs_args'):
+ commands += linker.gen_vs_module_defs_args(target.vs_module_defs.rel_to_builddir(self.build_to_src))
+ # This is only visited when building for Windows using either GCC or Visual Studio
+ if target.import_filename:
+ commands += linker.gen_import_library_args(self.get_import_filename(target))
+ elif isinstance(target, build.StaticLibrary):
+ commands += linker.get_std_link_args(self.environment, not target.should_install())
+ else:
+ raise RuntimeError('Unknown build target type.')
+ return commands
+
+ def get_target_type_link_args_post_dependencies(self, target, linker):
+ commands = []
+ if isinstance(target, build.Executable):
+ # If gui_app is significant on this platform, add the appropriate linker arguments.
+ # Unfortunately this can't be done in get_target_type_link_args, because some misguided
+ # libraries (such as SDL2) add -mwindows to their link flags.
+ m = self.environment.machines[target.for_machine]
+
+ if m.is_windows() or m.is_cygwin():
+ if target.gui_app is not None:
+ commands += linker.get_gui_app_args(target.gui_app)
+ else:
+ commands += linker.get_win_subsystem_args(target.win_subsystem)
+ return commands
+
+ def get_link_whole_args(self, linker, target):
+ use_custom = False
+ if linker.id == 'msvc':
+ # Expand our object lists manually if we are on pre-Visual Studio 2015 Update 2
+ # (incidentally, the "linker" here actually refers to cl.exe)
+ if mesonlib.version_compare(linker.version, '<19.00.23918'):
+ use_custom = True
+
+ if use_custom:
+ objects_from_static_libs: T.List[ExtractedObjects] = []
+ for dep in target.link_whole_targets:
+ l = dep.extract_all_objects(False)
+ objects_from_static_libs += self.determine_ext_objs(l, '')
+ objects_from_static_libs.extend(self.flatten_object_list(dep)[0])
+
+ return objects_from_static_libs
+ else:
+ target_args = self.build_target_link_arguments(linker, target.link_whole_targets)
+ return linker.get_link_whole_for(target_args) if target_args else []
+
+ @lru_cache(maxsize=None)
+ def guess_library_absolute_path(self, linker, libname, search_dirs, patterns) -> Path:
+ from ..compilers.c import CCompiler
+ for d in search_dirs:
+ for p in patterns:
+ trial = CCompiler._get_trials_from_pattern(p, d, libname)
+ if not trial:
+ continue
+ trial = CCompiler._get_file_from_list(self.environment, trial)
+ if not trial:
+ continue
+ # Return the first result
+ return trial
+
+ def guess_external_link_dependencies(self, linker, target, commands, internal):
+ # Ideally the linker would generate dependency information that could be used.
+ # But that has 2 problems:
+ # * currently ld can not create dependency information in a way that ninja can use:
+ # https://sourceware.org/bugzilla/show_bug.cgi?id=22843
+ # * Meson optimizes libraries from the same build using the symbol extractor.
+ # Just letting ninja use ld generated dependencies would undo this optimization.
+ search_dirs = OrderedSet()
+ libs = OrderedSet()
+ absolute_libs = []
+
+ build_dir = self.environment.get_build_dir()
+ # the following loop sometimes consumes two items from command in one pass
+ it = iter(linker.native_args_to_unix(commands))
+ for item in it:
+ if item in internal and not item.startswith('-'):
+ continue
+
+ if item.startswith('-L'):
+ if len(item) > 2:
+ path = item[2:]
+ else:
+ try:
+ path = next(it)
+ except StopIteration:
+ mlog.warning("Generated linker command has -L argument without following path")
+ break
+ if not os.path.isabs(path):
+ path = os.path.join(build_dir, path)
+ search_dirs.add(path)
+ elif item.startswith('-l'):
+ if len(item) > 2:
+ lib = item[2:]
+ else:
+ try:
+ lib = next(it)
+ except StopIteration:
+ mlog.warning("Generated linker command has '-l' argument without following library name")
+ break
+ libs.add(lib)
+ elif os.path.isabs(item) and self.environment.is_library(item) and os.path.isfile(item):
+ absolute_libs.append(item)
+
+ guessed_dependencies = []
+ # TODO The get_library_naming requirement currently excludes link targets that use d or fortran as their main linker
+ try:
+ static_patterns = linker.get_library_naming(self.environment, LibType.STATIC, strict=True)
+ shared_patterns = linker.get_library_naming(self.environment, LibType.SHARED, strict=True)
+ search_dirs = tuple(search_dirs) + tuple(linker.get_library_dirs(self.environment))
+ for libname in libs:
+ # be conservative and record most likely shared and static resolution, because we don't know exactly
+ # which one the linker will prefer
+ staticlibs = self.guess_library_absolute_path(linker, libname,
+ search_dirs, static_patterns)
+ sharedlibs = self.guess_library_absolute_path(linker, libname,
+ search_dirs, shared_patterns)
+ if staticlibs:
+ guessed_dependencies.append(staticlibs.resolve().as_posix())
+ if sharedlibs:
+ guessed_dependencies.append(sharedlibs.resolve().as_posix())
+ except (mesonlib.MesonException, AttributeError) as e:
+ if 'get_library_naming' not in str(e):
+ raise
+
+ return guessed_dependencies + absolute_libs
+
+ def generate_prelink(self, target, obj_list):
+ assert isinstance(target, build.StaticLibrary)
+ prelink_name = os.path.join(self.get_target_private_dir(target), target.name + '-prelink.o')
+ elem = NinjaBuildElement(self.all_outputs, [prelink_name], 'CUSTOM_COMMAND', obj_list)
+
+ prelinker = target.get_prelinker()
+ cmd = prelinker.exelist[:]
+ cmd += prelinker.get_prelink_args(prelink_name, obj_list)
+
+ cmd = self.replace_paths(target, cmd)
+ elem.add_item('COMMAND', cmd)
+ elem.add_item('description', f'Prelinking {prelink_name}.')
+ self.add_build(elem)
+ return [prelink_name]
+
+ def generate_link(self, target: build.BuildTarget, outname, obj_list, linker: T.Union['Compiler', 'StaticLinker'], extra_args=None, stdlib_args=None):
+ extra_args = extra_args if extra_args is not None else []
+ stdlib_args = stdlib_args if stdlib_args is not None else []
+ implicit_outs = []
+ if isinstance(target, build.StaticLibrary):
+ linker_base = 'STATIC'
+ else:
+ linker_base = linker.get_language() # Fixme.
+ if isinstance(target, build.SharedLibrary):
+ self.generate_shsym(target)
+ crstr = self.get_rule_suffix(target.for_machine)
+ linker_rule = linker_base + '_LINKER' + crstr
+ # Create an empty commands list, and start adding link arguments from
+ # various sources in the order in which they must override each other
+ # starting from hard-coded defaults followed by build options and so on.
+ #
+ # Once all the linker options have been passed, we will start passing
+ # libraries and library paths from internal and external sources.
+ commands = linker.compiler_args()
+ # First, the trivial ones that are impossible to override.
+ #
+ # Add linker args for linking this target derived from 'base' build
+ # options passed on the command-line, in default_options, etc.
+ # These have the lowest priority.
+ if isinstance(target, build.StaticLibrary):
+ commands += linker.get_base_link_args(target.get_options())
+ else:
+ commands += compilers.get_base_link_args(target.get_options(),
+ linker,
+ isinstance(target, build.SharedModule),
+ self.environment.get_build_dir())
+ # Add -nostdlib if needed; can't be overridden
+ commands += self.get_no_stdlib_link_args(target, linker)
+ # Add things like /NOLOGO; usually can't be overridden
+ commands += linker.get_linker_always_args()
+ # Add buildtype linker args: optimization level, etc.
+ commands += linker.get_buildtype_linker_args(target.get_option(OptionKey('buildtype')))
+ # Add /DEBUG and the pdb filename when using MSVC
+ if target.get_option(OptionKey('debug')):
+ commands += self.get_link_debugfile_args(linker, target, outname)
+ debugfile = self.get_link_debugfile_name(linker, target, outname)
+ if debugfile is not None:
+ implicit_outs += [debugfile]
+ # Add link args specific to this BuildTarget type, such as soname args,
+ # PIC, import library generation, etc.
+ commands += self.get_target_type_link_args(target, linker)
+ # Archives that are copied wholesale in the result. Must be before any
+ # other link targets so missing symbols from whole archives are found in those.
+ if not isinstance(target, build.StaticLibrary):
+ commands += self.get_link_whole_args(linker, target)
+
+ if not isinstance(target, build.StaticLibrary):
+ # Add link args added using add_project_link_arguments()
+ commands += self.build.get_project_link_args(linker, target.subproject, target.for_machine)
+ # Add link args added using add_global_link_arguments()
+ # These override per-project link arguments
+ commands += self.build.get_global_link_args(linker, target.for_machine)
+ # Link args added from the env: LDFLAGS. We want these to override
+ # all the defaults but not the per-target link args.
+ commands += self.environment.coredata.get_external_link_args(target.for_machine, linker.get_language())
+
+ # Now we will add libraries and library paths from various sources
+
+ # Set runtime-paths so we can run executables without needing to set
+ # LD_LIBRARY_PATH, etc in the environment. Doesn't work on Windows.
+ if has_path_sep(target.name):
+ # Target names really should not have slashes in them, but
+ # unfortunately we did not check for that and some downstream projects
+ # now have them. Once slashes are forbidden, remove this bit.
+ target_slashname_workaround_dir = os.path.join(
+ os.path.dirname(target.name),
+ self.get_target_dir(target))
+ else:
+ target_slashname_workaround_dir = self.get_target_dir(target)
+ (rpath_args, target.rpath_dirs_to_remove) = (
+ linker.build_rpath_args(self.environment,
+ self.environment.get_build_dir(),
+ target_slashname_workaround_dir,
+ self.determine_rpath_dirs(target),
+ target.build_rpath,
+ target.install_rpath))
+ commands += rpath_args
+
+ # Add link args to link to all internal libraries (link_with:) and
+ # internal dependencies needed by this target.
+ if linker_base == 'STATIC':
+ # Link arguments of static libraries are not put in the command
+ # line of the library. They are instead appended to the command
+ # line where the static library is used.
+ dependencies = []
+ else:
+ dependencies = target.get_dependencies()
+ internal = self.build_target_link_arguments(linker, dependencies)
+ commands += internal
+ # Only non-static built targets need link args and link dependencies
+ if not isinstance(target, build.StaticLibrary):
+ # For 'automagic' deps: Boost and GTest. Also dependency('threads').
+ # pkg-config puts the thread flags itself via `Cflags:`
+
+ commands += linker.get_target_link_args(target)
+ # External deps must be last because target link libraries may depend on them.
+ for dep in target.get_external_deps():
+ # Extend without reordering or de-dup to preserve `-L -l` sets
+ # https://github.com/mesonbuild/meson/issues/1718
+ commands.extend_preserving_lflags(linker.get_dependency_link_args(dep))
+ for d in target.get_dependencies():
+ if isinstance(d, build.StaticLibrary):
+ for dep in d.get_external_deps():
+ commands.extend_preserving_lflags(linker.get_dependency_link_args(dep))
+
+ # Add link args specific to this BuildTarget type that must not be overridden by dependencies
+ commands += self.get_target_type_link_args_post_dependencies(target, linker)
+
+ # Add link args for c_* or cpp_* build options. Currently this only
+ # adds c_winlibs and cpp_winlibs when building for Windows. This needs
+ # to be after all internal and external libraries so that unresolved
+ # symbols from those can be found here. This is needed when the
+ # *_winlibs that we want to link to are static mingw64 libraries.
+ if isinstance(linker, Compiler):
+ # The static linker doesn't know what language it is building, so we
+ # don't know what option. Fortunately, it doesn't care to see the
+ # language-specific options either.
+ #
+ # We shouldn't check whether we are making a static library, because
+ # in the LTO case we do use a real compiler here.
+ commands += linker.get_option_link_args(self.environment.coredata.options)
+
+ dep_targets = []
+ dep_targets.extend(self.guess_external_link_dependencies(linker, target, commands, internal))
+
+ # Add libraries generated by custom targets
+ custom_target_libraries = self.get_custom_target_provided_libraries(target)
+ commands += extra_args
+ commands += custom_target_libraries
+ commands += stdlib_args # Standard library arguments go last, because they never depend on anything.
+ dep_targets.extend([self.get_dependency_filename(t) for t in dependencies])
+ dep_targets.extend([self.get_dependency_filename(t)
+ for t in target.link_depends])
+ elem = NinjaBuildElement(self.all_outputs, outname, linker_rule, obj_list, implicit_outs=implicit_outs)
+ elem.add_dep(dep_targets + custom_target_libraries)
+ elem.add_item('LINK_ARGS', commands)
+ return elem
+
+ def get_dependency_filename(self, t):
+ if isinstance(t, build.SharedLibrary):
+ return self.get_target_shsym_filename(t)
+ elif isinstance(t, mesonlib.File):
+ if t.is_built:
+ return t.relative_name()
+ else:
+ return t.absolute_path(self.environment.get_source_dir(),
+ self.environment.get_build_dir())
+ return self.get_target_filename(t)
+
+ def generate_shlib_aliases(self, target, outdir):
+ for alias, to, tag in target.get_aliases():
+ aliasfile = os.path.join(self.environment.get_build_dir(), outdir, alias)
+ try:
+ os.remove(aliasfile)
+ except Exception:
+ pass
+ try:
+ os.symlink(to, aliasfile)
+ except NotImplementedError:
+ mlog.debug("Library versioning disabled because symlinks are not supported.")
+ except OSError:
+ mlog.debug("Library versioning disabled because we do not have symlink creation privileges.")
+
+ def generate_custom_target_clean(self, trees: T.List[str]) -> str:
+ e = self.create_phony_target(self.all_outputs, 'clean-ctlist', 'CUSTOM_COMMAND', 'PHONY')
+ d = CleanTrees(self.environment.get_build_dir(), trees)
+ d_file = os.path.join(self.environment.get_scratch_dir(), 'cleantrees.dat')
+ e.add_item('COMMAND', self.environment.get_build_command() + ['--internal', 'cleantrees', d_file])
+ e.add_item('description', 'Cleaning custom target directories')
+ self.add_build(e)
+ # Write out the data file passed to the script
+ with open(d_file, 'wb') as ofile:
+ pickle.dump(d, ofile)
+ return 'clean-ctlist'
+
+ def generate_gcov_clean(self):
+ gcno_elem = self.create_phony_target(self.all_outputs, 'clean-gcno', 'CUSTOM_COMMAND', 'PHONY')
+ gcno_elem.add_item('COMMAND', mesonlib.get_meson_command() + ['--internal', 'delwithsuffix', '.', 'gcno'])
+ gcno_elem.add_item('description', 'Deleting gcno files')
+ self.add_build(gcno_elem)
+
+ gcda_elem = self.create_phony_target(self.all_outputs, 'clean-gcda', 'CUSTOM_COMMAND', 'PHONY')
+ gcda_elem.add_item('COMMAND', mesonlib.get_meson_command() + ['--internal', 'delwithsuffix', '.', 'gcda'])
+ gcda_elem.add_item('description', 'Deleting gcda files')
+ self.add_build(gcda_elem)
+
+ def get_user_option_args(self):
+ cmds = []
+ for (k, v) in self.environment.coredata.options.items():
+ if k.is_project():
+ cmds.append('-D' + str(k) + '=' + (v.value if isinstance(v.value, str) else str(v.value).lower()))
+ # The order of these arguments must be the same between runs of Meson
+ # to ensure reproducible output. The order we pass them shouldn't
+ # affect behavior in any other way.
+ return sorted(cmds)
+
+ def generate_dist(self):
+ elem = self.create_phony_target(self.all_outputs, 'dist', 'CUSTOM_COMMAND', 'PHONY')
+ elem.add_item('DESC', 'Creating source packages')
+ elem.add_item('COMMAND', self.environment.get_build_command() + ['dist'])
+ elem.add_item('pool', 'console')
+ self.add_build(elem)
+
+ def generate_scanbuild(self):
+ if not environment.detect_scanbuild():
+ return
+ if 'scan-build' in self.all_outputs:
+ return
+ cmd = self.environment.get_build_command() + \
+ ['--internal', 'scanbuild', self.environment.source_dir, self.environment.build_dir] + \
+ self.environment.get_build_command() + self.get_user_option_args()
+ elem = self.create_phony_target(self.all_outputs, 'scan-build', 'CUSTOM_COMMAND', 'PHONY')
+ elem.add_item('COMMAND', cmd)
+ elem.add_item('pool', 'console')
+ self.add_build(elem)
+
+ def generate_clangtool(self, name, extra_arg=None):
+ target_name = 'clang-' + name
+ extra_args = []
+ if extra_arg:
+ target_name += f'-{extra_arg}'
+ extra_args.append(f'--{extra_arg}')
+ if not os.path.exists(os.path.join(self.environment.source_dir, '.clang-' + name)) and \
+ not os.path.exists(os.path.join(self.environment.source_dir, '_clang-' + name)):
+ return
+ if target_name in self.all_outputs:
+ return
+ cmd = self.environment.get_build_command() + \
+ ['--internal', 'clang' + name, self.environment.source_dir, self.environment.build_dir] + \
+ extra_args
+ elem = self.create_phony_target(self.all_outputs, target_name, 'CUSTOM_COMMAND', 'PHONY')
+ elem.add_item('COMMAND', cmd)
+ elem.add_item('pool', 'console')
+ self.add_build(elem)
+
+ def generate_clangformat(self):
+ if not environment.detect_clangformat():
+ return
+ self.generate_clangtool('format')
+ self.generate_clangtool('format', 'check')
+
+ def generate_clangtidy(self):
+ import shutil
+ if not shutil.which('clang-tidy'):
+ return
+ self.generate_clangtool('tidy')
+
+ def generate_tags(self, tool, target_name):
+ import shutil
+ if not shutil.which(tool):
+ return
+ if target_name in self.all_outputs:
+ return
+ cmd = self.environment.get_build_command() + \
+ ['--internal', 'tags', tool, self.environment.source_dir]
+ elem = self.create_phony_target(self.all_outputs, target_name, 'CUSTOM_COMMAND', 'PHONY')
+ elem.add_item('COMMAND', cmd)
+ elem.add_item('pool', 'console')
+ self.add_build(elem)
+
+ # For things like scan-build and other helper tools we might have.
+ def generate_utils(self):
+ self.generate_scanbuild()
+ self.generate_clangformat()
+ self.generate_clangtidy()
+ self.generate_tags('etags', 'TAGS')
+ self.generate_tags('ctags', 'ctags')
+ self.generate_tags('cscope', 'cscope')
+ cmd = self.environment.get_build_command() + ['--internal', 'uninstall']
+ elem = self.create_phony_target(self.all_outputs, 'uninstall', 'CUSTOM_COMMAND', 'PHONY')
+ elem.add_item('COMMAND', cmd)
+ elem.add_item('pool', 'console')
+ self.add_build(elem)
+
+ def generate_ending(self):
+ for targ, deps in [
+ ('all', self.get_build_by_default_targets()),
+ ('meson-test-prereq', self.get_testlike_targets()),
+ ('meson-benchmark-prereq', self.get_testlike_targets(True))]:
+ targetlist = []
+ # These must also be built by default.
+ # XXX: Sometime in the future these should be built only before running tests.
+ if targ == 'all':
+ targetlist.extend(['meson-test-prereq', 'meson-benchmark-prereq'])
+ for t in deps.values():
+ # Add the first output of each target to the 'all' target so that
+ # they are all built
+ targetlist.append(os.path.join(self.get_target_dir(t), t.get_outputs()[0]))
+
+ elem = NinjaBuildElement(self.all_outputs, targ, 'phony', targetlist)
+ self.add_build(elem)
+
+ elem = self.create_phony_target(self.all_outputs, 'clean', 'CUSTOM_COMMAND', 'PHONY')
+ elem.add_item('COMMAND', self.ninja_command + ['-t', 'clean'])
+ elem.add_item('description', 'Cleaning')
+
+ # If we have custom targets in this project, add all their outputs to
+ # the list that is passed to the `cleantrees.py` script. The script
+ # will manually delete all custom_target outputs that are directories
+ # instead of files. This is needed because on platforms other than
+ # Windows, Ninja only deletes directories while cleaning if they are
+ # empty. https://github.com/mesonbuild/meson/issues/1220
+ ctlist = []
+ for t in self.build.get_targets().values():
+ if isinstance(t, build.CustomTarget):
+ # Create a list of all custom target outputs
+ for o in t.get_outputs():
+ ctlist.append(os.path.join(self.get_target_dir(t), o))
+ if ctlist:
+ elem.add_dep(self.generate_custom_target_clean(ctlist))
+
+ if OptionKey('b_coverage') in self.environment.coredata.options and \
+ self.environment.coredata.options[OptionKey('b_coverage')].value:
+ self.generate_gcov_clean()
+ elem.add_dep('clean-gcda')
+ elem.add_dep('clean-gcno')
+ self.add_build(elem)
+
+ deps = self.get_regen_filelist()
+ elem = NinjaBuildElement(self.all_outputs, 'build.ninja', 'REGENERATE_BUILD', deps)
+ elem.add_item('pool', 'console')
+ self.add_build(elem)
+
+ elem = NinjaBuildElement(self.all_outputs, 'reconfigure', 'REGENERATE_BUILD', 'PHONY')
+ elem.add_item('pool', 'console')
+ self.add_build(elem)
+
+ elem = NinjaBuildElement(self.all_outputs, deps, 'phony', '')
+ self.add_build(elem)
+
+ def get_introspection_data(self, target_id: str, target: build.Target) -> T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]]:
+ if target_id not in self.introspection_data or len(self.introspection_data[target_id]) == 0:
+ return super().get_introspection_data(target_id, target)
+
+ result = []
+ for i in self.introspection_data[target_id].values():
+ result += [i]
+ return result
+
+
+def _scan_fortran_file_deps(src: Path, srcdir: Path, dirname: Path, tdeps, compiler) -> T.List[str]:
+ """
+ scan a Fortran file for dependencies. Needs to be distinct from target
+ to allow for recursion induced by `include` statements.er
+
+ It makes a number of assumptions, including
+
+ * `use`, `module`, `submodule` name is not on a continuation line
+
+ Regex
+ -----
+
+ * `incre` works for `#include "foo.f90"` and `include "foo.f90"`
+ * `usere` works for legacy and Fortran 2003 `use` statements
+ * `submodre` is for Fortran >= 2008 `submodule`
+ """
+
+ incre = re.compile(FORTRAN_INCLUDE_PAT, re.IGNORECASE)
+ usere = re.compile(FORTRAN_USE_PAT, re.IGNORECASE)
+ submodre = re.compile(FORTRAN_SUBMOD_PAT, re.IGNORECASE)
+
+ mod_files = []
+ src = Path(src)
+ with src.open(encoding='ascii', errors='ignore') as f:
+ for line in f:
+ # included files
+ incmatch = incre.match(line)
+ if incmatch is not None:
+ incfile = src.parent / incmatch.group(1)
+ # NOTE: src.parent is most general, in particular for CMake subproject with Fortran file
+ # having an `include 'foo.f'` statement.
+ if incfile.suffix.lower()[1:] in compiler.file_suffixes:
+ mod_files.extend(_scan_fortran_file_deps(incfile, srcdir, dirname, tdeps, compiler))
+ # modules
+ usematch = usere.match(line)
+ if usematch is not None:
+ usename = usematch.group(1).lower()
+ if usename == 'intrinsic': # this keeps the regex simpler
+ continue
+ if usename not in tdeps:
+ # The module is not provided by any source file. This
+ # is due to:
+ # a) missing file/typo/etc
+ # b) using a module provided by the compiler, such as
+ # OpenMP
+ # There's no easy way to tell which is which (that I
+ # know of) so just ignore this and go on. Ideally we
+ # would print a warning message to the user but this is
+ # a common occurrence, which would lead to lots of
+ # distracting noise.
+ continue
+ srcfile = srcdir / tdeps[usename].fname # type: Path
+ if not srcfile.is_file():
+ if srcfile.name != src.name: # generated source file
+ pass
+ else: # subproject
+ continue
+ elif srcfile.samefile(src): # self-reference
+ continue
+
+ mod_name = compiler.module_name_to_filename(usename)
+ mod_files.append(str(dirname / mod_name))
+ else: # submodules
+ submodmatch = submodre.match(line)
+ if submodmatch is not None:
+ parents = submodmatch.group(1).lower().split(':')
+ assert len(parents) in {1, 2}, (
+ 'submodule ancestry must be specified as'
+ f' ancestor:parent but Meson found {parents}')
+
+ ancestor_child = '_'.join(parents)
+ if ancestor_child not in tdeps:
+ raise MesonException("submodule {} relies on ancestor module {} that was not found.".format(submodmatch.group(2).lower(), ancestor_child.split('_', maxsplit=1)[0]))
+ submodsrcfile = srcdir / tdeps[ancestor_child].fname # type: Path
+ if not submodsrcfile.is_file():
+ if submodsrcfile.name != src.name: # generated source file
+ pass
+ else: # subproject
+ continue
+ elif submodsrcfile.samefile(src): # self-reference
+ continue
+ mod_name = compiler.module_name_to_filename(ancestor_child)
+ mod_files.append(str(dirname / mod_name))
+ return mod_files
diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py
new file mode 100644
index 0000000..cf15175
--- /dev/null
+++ b/mesonbuild/backend/vs2010backend.py
@@ -0,0 +1,1575 @@
+# Copyright 2014-2016 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.
+
+from __future__ import annotations
+import copy
+import itertools
+import os
+import xml.dom.minidom
+import xml.etree.ElementTree as ET
+import uuid
+import typing as T
+from pathlib import Path, PurePath
+import re
+
+from . import backends
+from .. import build
+from .. import dependencies
+from .. import mlog
+from .. import compilers
+from ..mesonlib import (
+ File, MesonException, replace_if_different, OptionKey, version_compare, MachineChoice
+)
+from ..environment import Environment, build_filename
+
+if T.TYPE_CHECKING:
+ from ..interpreter import Interpreter
+
+
+def autodetect_vs_version(build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]) -> backends.Backend:
+ vs_version = os.getenv('VisualStudioVersion', None)
+ vs_install_dir = os.getenv('VSINSTALLDIR', None)
+ if not vs_install_dir:
+ raise MesonException('Could not detect Visual Studio: Environment variable VSINSTALLDIR is not set!\n'
+ 'Are you running meson from the Visual Studio Developer Command Prompt?')
+ # VisualStudioVersion is set since Visual Studio 11.0, but sometimes
+ # vcvarsall.bat doesn't set it, so also use VSINSTALLDIR
+ if vs_version == '11.0' or 'Visual Studio 11' in vs_install_dir:
+ from mesonbuild.backend.vs2012backend import Vs2012Backend
+ return Vs2012Backend(build, interpreter)
+ if vs_version == '12.0' or 'Visual Studio 12' in vs_install_dir:
+ from mesonbuild.backend.vs2013backend import Vs2013Backend
+ return Vs2013Backend(build, interpreter)
+ if vs_version == '14.0' or 'Visual Studio 14' in vs_install_dir:
+ from mesonbuild.backend.vs2015backend import Vs2015Backend
+ return Vs2015Backend(build, interpreter)
+ if vs_version == '15.0' or 'Visual Studio 17' in vs_install_dir or \
+ 'Visual Studio\\2017' in vs_install_dir:
+ from mesonbuild.backend.vs2017backend import Vs2017Backend
+ return Vs2017Backend(build, interpreter)
+ if vs_version == '16.0' or 'Visual Studio 19' in vs_install_dir or \
+ 'Visual Studio\\2019' in vs_install_dir:
+ from mesonbuild.backend.vs2019backend import Vs2019Backend
+ return Vs2019Backend(build, interpreter)
+ if vs_version == '17.0' or 'Visual Studio 22' in vs_install_dir or \
+ 'Visual Studio\\2022' in vs_install_dir:
+ from mesonbuild.backend.vs2022backend import Vs2022Backend
+ return Vs2022Backend(build, interpreter)
+ if 'Visual Studio 10.0' in vs_install_dir:
+ return Vs2010Backend(build, interpreter)
+ raise MesonException('Could not detect Visual Studio using VisualStudioVersion: {!r} or VSINSTALLDIR: {!r}!\n'
+ 'Please specify the exact backend to use.'.format(vs_version, vs_install_dir))
+
+
+def split_o_flags_args(args):
+ """
+ Splits any /O args and returns them. Does not take care of flags overriding
+ previous ones. Skips non-O flag arguments.
+
+ ['/Ox', '/Ob1'] returns ['/Ox', '/Ob1']
+ ['/Oxj', '/MP'] returns ['/Ox', '/Oj']
+ """
+ o_flags = []
+ for arg in args:
+ if not arg.startswith('/O'):
+ continue
+ flags = list(arg[2:])
+ # Assume that this one can't be clumped with the others since it takes
+ # an argument itself
+ if 'b' in flags:
+ o_flags.append(arg)
+ else:
+ o_flags += ['/O' + f for f in flags]
+ return o_flags
+
+
+def generate_guid_from_path(path, path_type):
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, 'meson-vs-' + path_type + ':' + str(path))).upper()
+
+def detect_microsoft_gdk(platform: str) -> bool:
+ return re.match(r'Gaming\.(Desktop|Xbox.XboxOne|Xbox.Scarlett)\.x64', platform, re.IGNORECASE)
+
+class Vs2010Backend(backends.Backend):
+ def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]):
+ super().__init__(build, interpreter)
+ self.name = 'vs2010'
+ self.project_file_version = '10.0.30319.1'
+ self.sln_file_version = '11.00'
+ self.sln_version_comment = '2010'
+ self.platform_toolset = None
+ self.vs_version = '2010'
+ self.windows_target_platform_version = None
+ self.subdirs = {}
+ self.handled_target_deps = {}
+
+ def get_target_private_dir(self, target):
+ return os.path.join(self.get_target_dir(target), target.get_id())
+
+ def generate_custom_generator_commands(self, target, parent_node):
+ generator_output_files = []
+ custom_target_include_dirs = []
+ custom_target_output_files = []
+ target_private_dir = self.relpath(self.get_target_private_dir(target), self.get_target_dir(target))
+ down = self.target_to_build_root(target)
+ for genlist in target.get_generated_sources():
+ if isinstance(genlist, (build.CustomTarget, build.CustomTargetIndex)):
+ for i in genlist.get_outputs():
+ # Path to the generated source from the current vcxproj dir via the build root
+ ipath = os.path.join(down, self.get_target_dir(genlist), i)
+ custom_target_output_files.append(ipath)
+ idir = self.relpath(self.get_target_dir(genlist), self.get_target_dir(target))
+ if idir not in custom_target_include_dirs:
+ custom_target_include_dirs.append(idir)
+ else:
+ generator = genlist.get_generator()
+ exe = generator.get_exe()
+ infilelist = genlist.get_inputs()
+ outfilelist = genlist.get_outputs()
+ source_dir = os.path.join(down, self.build_to_src, genlist.subdir)
+ idgroup = ET.SubElement(parent_node, 'ItemGroup')
+ for i, curfile in enumerate(infilelist):
+ if len(infilelist) == len(outfilelist):
+ sole_output = os.path.join(target_private_dir, outfilelist[i])
+ else:
+ sole_output = ''
+ infilename = os.path.join(down, curfile.rel_to_builddir(self.build_to_src))
+ deps = self.get_custom_target_depend_files(genlist, True)
+ base_args = generator.get_arglist(infilename)
+ outfiles_rel = genlist.get_outputs_for(curfile)
+ outfiles = [os.path.join(target_private_dir, of) for of in outfiles_rel]
+ generator_output_files += outfiles
+ args = [x.replace("@INPUT@", infilename).replace('@OUTPUT@', sole_output)
+ for x in base_args]
+ args = self.replace_outputs(args, target_private_dir, outfiles_rel)
+ args = [x.replace("@SOURCE_DIR@", self.environment.get_source_dir())
+ .replace("@BUILD_DIR@", target_private_dir)
+ for x in args]
+ args = [x.replace("@CURRENT_SOURCE_DIR@", source_dir) for x in args]
+ args = [x.replace("@SOURCE_ROOT@", self.environment.get_source_dir())
+ .replace("@BUILD_ROOT@", self.environment.get_build_dir())
+ for x in args]
+ args = [x.replace('\\', '/') for x in args]
+ # Always use a wrapper because MSBuild eats random characters when
+ # there are many arguments.
+ tdir_abs = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target))
+ cmd, _ = self.as_meson_exe_cmdline(
+ exe,
+ self.replace_extra_args(args, genlist),
+ workdir=tdir_abs,
+ capture=outfiles[0] if generator.capture else None,
+ force_serialize=True
+ )
+ deps = cmd[-1:] + deps
+ abs_pdir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target))
+ os.makedirs(abs_pdir, exist_ok=True)
+ cbs = ET.SubElement(idgroup, 'CustomBuild', Include=infilename)
+ ET.SubElement(cbs, 'Command').text = ' '.join(self.quote_arguments(cmd))
+ ET.SubElement(cbs, 'Outputs').text = ';'.join(outfiles)
+ ET.SubElement(cbs, 'AdditionalInputs').text = ';'.join(deps)
+ return generator_output_files, custom_target_output_files, custom_target_include_dirs
+
+ def generate(self):
+ target_machine = self.interpreter.builtin['target_machine'].cpu_family_method(None, None)
+ if target_machine in {'64', 'x86_64'}:
+ # amd64 or x86_64
+ target_system = self.interpreter.builtin['target_machine'].system_method(None, None)
+ if detect_microsoft_gdk(target_system):
+ self.platform = target_system
+ else:
+ self.platform = 'x64'
+ elif target_machine == 'x86':
+ # x86
+ self.platform = 'Win32'
+ elif target_machine in {'aarch64', 'arm64'}:
+ target_cpu = self.interpreter.builtin['target_machine'].cpu_method(None, None)
+ if target_cpu == 'arm64ec':
+ self.platform = 'arm64ec'
+ else:
+ self.platform = 'arm64'
+ elif 'arm' in target_machine.lower():
+ self.platform = 'ARM'
+ else:
+ raise MesonException('Unsupported Visual Studio platform: ' + target_machine)
+
+ build_machine = self.interpreter.builtin['build_machine'].cpu_family_method(None, None)
+ if build_machine in {'64', 'x86_64'}:
+ # amd64 or x86_64
+ self.build_platform = 'x64'
+ elif build_machine == 'x86':
+ # x86
+ self.build_platform = 'Win32'
+ elif build_machine in {'aarch64', 'arm64'}:
+ target_cpu = self.interpreter.builtin['build_machine'].cpu_method(None, None)
+ if target_cpu == 'arm64ec':
+ self.build_platform = 'arm64ec'
+ else:
+ self.build_platform = 'arm64'
+ elif 'arm' in build_machine.lower():
+ self.build_platform = 'ARM'
+ else:
+ raise MesonException('Unsupported Visual Studio platform: ' + build_machine)
+
+ self.buildtype = self.environment.coredata.get_option(OptionKey('buildtype'))
+ self.optimization = self.environment.coredata.get_option(OptionKey('optimization'))
+ self.debug = self.environment.coredata.get_option(OptionKey('debug'))
+ try:
+ self.sanitize = self.environment.coredata.get_option(OptionKey('b_sanitize'))
+ except MesonException:
+ self.sanitize = 'none'
+ sln_filename = os.path.join(self.environment.get_build_dir(), self.build.project_name + '.sln')
+ projlist = self.generate_projects()
+ self.gen_testproj('RUN_TESTS', os.path.join(self.environment.get_build_dir(), 'RUN_TESTS.vcxproj'))
+ self.gen_installproj('RUN_INSTALL', os.path.join(self.environment.get_build_dir(), 'RUN_INSTALL.vcxproj'))
+ self.gen_regenproj('REGEN', os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj'))
+ self.generate_solution(sln_filename, projlist)
+ self.generate_regen_info()
+ Vs2010Backend.touch_regen_timestamp(self.environment.get_build_dir())
+
+ @staticmethod
+ def get_regen_stampfile(build_dir: str) -> None:
+ return os.path.join(os.path.join(build_dir, Environment.private_dir), 'regen.stamp')
+
+ @staticmethod
+ def touch_regen_timestamp(build_dir: str) -> None:
+ with open(Vs2010Backend.get_regen_stampfile(build_dir), 'w', encoding='utf-8'):
+ pass
+
+ def get_vcvars_command(self):
+ has_arch_values = 'VSCMD_ARG_TGT_ARCH' in os.environ and 'VSCMD_ARG_HOST_ARCH' in os.environ
+
+ # Use vcvarsall.bat if we found it.
+ if 'VCINSTALLDIR' in os.environ:
+ vs_version = os.environ['VisualStudioVersion'] \
+ if 'VisualStudioVersion' in os.environ else None
+ relative_path = 'Auxiliary\\Build\\' if vs_version is not None and vs_version >= '15.0' else ''
+ script_path = os.environ['VCINSTALLDIR'] + relative_path + 'vcvarsall.bat'
+ if os.path.exists(script_path):
+ if has_arch_values:
+ target_arch = os.environ['VSCMD_ARG_TGT_ARCH']
+ host_arch = os.environ['VSCMD_ARG_HOST_ARCH']
+ else:
+ target_arch = os.environ.get('Platform', 'x86')
+ host_arch = target_arch
+ arch = host_arch + '_' + target_arch if host_arch != target_arch else target_arch
+ return f'"{script_path}" {arch}'
+
+ # Otherwise try the VS2017 Developer Command Prompt.
+ if 'VS150COMNTOOLS' in os.environ and has_arch_values:
+ script_path = os.environ['VS150COMNTOOLS'] + 'VsDevCmd.bat'
+ if os.path.exists(script_path):
+ return '"%s" -arch=%s -host_arch=%s' % \
+ (script_path, os.environ['VSCMD_ARG_TGT_ARCH'], os.environ['VSCMD_ARG_HOST_ARCH'])
+ return ''
+
+ def get_obj_target_deps(self, obj_list):
+ result = {}
+ for o in obj_list:
+ if isinstance(o, build.ExtractedObjects):
+ result[o.target.get_id()] = o.target
+ return result.items()
+
+ def get_target_deps(self, t: T.Dict[T.Any, build.Target], recursive=False):
+ all_deps: T.Dict[str, build.Target] = {}
+ for target in t.values():
+ if isinstance(target, build.CustomTarget):
+ for d in target.get_target_dependencies():
+ # FIXME: this isn't strictly correct, as the target doesn't
+ # Get dependencies on non-targets, such as Files
+ if isinstance(d, build.Target):
+ all_deps[d.get_id()] = d
+ elif isinstance(target, build.RunTarget):
+ for d in target.get_dependencies():
+ all_deps[d.get_id()] = d
+ elif isinstance(target, build.BuildTarget):
+ for ldep in target.link_targets:
+ if isinstance(ldep, build.CustomTargetIndex):
+ all_deps[ldep.get_id()] = ldep.target
+ else:
+ all_deps[ldep.get_id()] = ldep
+ for ldep in target.link_whole_targets:
+ if isinstance(ldep, build.CustomTargetIndex):
+ all_deps[ldep.get_id()] = ldep.target
+ else:
+ all_deps[ldep.get_id()] = ldep
+
+ for ldep in target.link_depends:
+ if isinstance(ldep, build.CustomTargetIndex):
+ all_deps[ldep.get_id()] = ldep.target
+ elif isinstance(ldep, File):
+ # Already built, no target references needed
+ pass
+ else:
+ all_deps[ldep.get_id()] = ldep
+
+ for obj_id, objdep in self.get_obj_target_deps(target.objects):
+ all_deps[obj_id] = objdep
+ else:
+ raise MesonException(f'Unknown target type for target {target}')
+
+ for gendep in target.get_generated_sources():
+ if isinstance(gendep, build.CustomTarget):
+ all_deps[gendep.get_id()] = gendep
+ elif isinstance(gendep, build.CustomTargetIndex):
+ all_deps[gendep.target.get_id()] = gendep.target
+ else:
+ generator = gendep.get_generator()
+ gen_exe = generator.get_exe()
+ if isinstance(gen_exe, build.Executable):
+ all_deps[gen_exe.get_id()] = gen_exe
+ for d in itertools.chain(generator.depends, gendep.depends):
+ if isinstance(d, build.CustomTargetIndex):
+ all_deps[d.get_id()] = d.target
+ elif isinstance(d, build.Target):
+ all_deps[d.get_id()] = d
+ # FIXME: we don't handle other kinds of deps correctly here, such
+ # as GeneratedLists, StructuredSources, and generated File.
+
+ if not t or not recursive:
+ return all_deps
+ ret = self.get_target_deps(all_deps, recursive)
+ ret.update(all_deps)
+ return ret
+
+ def generate_solution_dirs(self, ofile, parents):
+ prj_templ = 'Project("{%s}") = "%s", "%s", "{%s}"\n'
+ iterpaths = reversed(parents)
+ # Skip first path
+ next(iterpaths)
+ for path in iterpaths:
+ if path not in self.subdirs:
+ basename = path.name
+ identifier = generate_guid_from_path(path, 'subdir')
+ # top-level directories have None as their parent_dir
+ parent_dir = path.parent
+ parent_identifier = self.subdirs[parent_dir][0] \
+ if parent_dir != PurePath('.') else None
+ self.subdirs[path] = (identifier, parent_identifier)
+ prj_line = prj_templ % (
+ self.environment.coredata.lang_guids['directory'],
+ basename, basename, self.subdirs[path][0])
+ ofile.write(prj_line)
+ ofile.write('EndProject\n')
+
+ def generate_solution(self, sln_filename, projlist):
+ default_projlist = self.get_build_by_default_targets()
+ default_projlist.update(self.get_testlike_targets())
+ sln_filename_tmp = sln_filename + '~'
+ # Note using the utf-8 BOM requires the blank line, otherwise Visual Studio Version Selector fails.
+ # Without the BOM, VSVS fails if there is a blank line.
+ with open(sln_filename_tmp, 'w', encoding='utf-8-sig') as ofile:
+ ofile.write('\nMicrosoft Visual Studio Solution File, Format Version %s\n' % self.sln_file_version)
+ ofile.write('# Visual Studio %s\n' % self.sln_version_comment)
+ prj_templ = 'Project("{%s}") = "%s", "%s", "{%s}"\n'
+ for prj in projlist:
+ coredata = self.environment.coredata
+ if coredata.get_option(OptionKey('layout')) == 'mirror':
+ self.generate_solution_dirs(ofile, prj[1].parents)
+ target = self.build.targets[prj[0]]
+ lang = 'default'
+ if hasattr(target, 'compilers') and target.compilers:
+ for lang_out in target.compilers.keys():
+ lang = lang_out
+ break
+ prj_line = prj_templ % (
+ self.environment.coredata.lang_guids[lang],
+ prj[0], prj[1], prj[2])
+ ofile.write(prj_line)
+ target_dict = {target.get_id(): target}
+ # Get recursive deps
+ recursive_deps = self.get_target_deps(
+ target_dict, recursive=True)
+ ofile.write('EndProject\n')
+ for dep, target in recursive_deps.items():
+ if prj[0] in default_projlist:
+ default_projlist[dep] = target
+
+ test_line = prj_templ % (self.environment.coredata.lang_guids['default'],
+ 'RUN_TESTS', 'RUN_TESTS.vcxproj',
+ self.environment.coredata.test_guid)
+ ofile.write(test_line)
+ ofile.write('EndProject\n')
+ regen_line = prj_templ % (self.environment.coredata.lang_guids['default'],
+ 'REGEN', 'REGEN.vcxproj',
+ self.environment.coredata.regen_guid)
+ ofile.write(regen_line)
+ ofile.write('EndProject\n')
+ install_line = prj_templ % (self.environment.coredata.lang_guids['default'],
+ 'RUN_INSTALL', 'RUN_INSTALL.vcxproj',
+ self.environment.coredata.install_guid)
+ ofile.write(install_line)
+ ofile.write('EndProject\n')
+ ofile.write('Global\n')
+ ofile.write('\tGlobalSection(SolutionConfigurationPlatforms) = '
+ 'preSolution\n')
+ ofile.write('\t\t%s|%s = %s|%s\n' %
+ (self.buildtype, self.platform, self.buildtype,
+ self.platform))
+ ofile.write('\tEndGlobalSection\n')
+ ofile.write('\tGlobalSection(ProjectConfigurationPlatforms) = '
+ 'postSolution\n')
+ ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' %
+ (self.environment.coredata.regen_guid, self.buildtype,
+ self.platform, self.buildtype, self.platform))
+ ofile.write('\t\t{%s}.%s|%s.Build.0 = %s|%s\n' %
+ (self.environment.coredata.regen_guid, self.buildtype,
+ self.platform, self.buildtype, self.platform))
+ # Create the solution configuration
+ for p in projlist:
+ if p[3] is MachineChoice.BUILD:
+ config_platform = self.build_platform
+ else:
+ config_platform = self.platform
+ # Add to the list of projects in this solution
+ ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' %
+ (p[2], self.buildtype, self.platform,
+ self.buildtype, config_platform))
+ if p[0] in default_projlist and \
+ not isinstance(self.build.targets[p[0]], build.RunTarget):
+ # Add to the list of projects to be built
+ ofile.write('\t\t{%s}.%s|%s.Build.0 = %s|%s\n' %
+ (p[2], self.buildtype, self.platform,
+ self.buildtype, config_platform))
+ ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' %
+ (self.environment.coredata.test_guid, self.buildtype,
+ self.platform, self.buildtype, self.platform))
+ ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' %
+ (self.environment.coredata.install_guid, self.buildtype,
+ self.platform, self.buildtype, self.platform))
+ ofile.write('\tEndGlobalSection\n')
+ ofile.write('\tGlobalSection(SolutionProperties) = preSolution\n')
+ ofile.write('\t\tHideSolutionNode = FALSE\n')
+ ofile.write('\tEndGlobalSection\n')
+ if self.subdirs:
+ ofile.write('\tGlobalSection(NestedProjects) = '
+ 'preSolution\n')
+ for p in projlist:
+ if p[1].parent != PurePath('.'):
+ ofile.write("\t\t{{{}}} = {{{}}}\n".format(p[2], self.subdirs[p[1].parent][0]))
+ for subdir in self.subdirs.values():
+ if subdir[1]:
+ ofile.write("\t\t{{{}}} = {{{}}}\n".format(subdir[0], subdir[1]))
+ ofile.write('\tEndGlobalSection\n')
+ ofile.write('EndGlobal\n')
+ replace_if_different(sln_filename, sln_filename_tmp)
+
+ def generate_projects(self):
+ startup_project = self.environment.coredata.options[OptionKey('backend_startup_project')].value
+ projlist = []
+ startup_idx = 0
+ for (i, (name, target)) in enumerate(self.build.targets.items()):
+ if startup_project and startup_project == target.get_basename():
+ startup_idx = i
+ outdir = Path(
+ self.environment.get_build_dir(),
+ self.get_target_dir(target)
+ )
+ outdir.mkdir(exist_ok=True, parents=True)
+ fname = name + '.vcxproj'
+ target_dir = PurePath(self.get_target_dir(target))
+ relname = target_dir / fname
+ projfile_path = outdir / fname
+ proj_uuid = self.environment.coredata.target_guids[name]
+ self.gen_vcxproj(target, str(projfile_path), proj_uuid)
+ projlist.append((name, relname, proj_uuid, target.for_machine))
+
+ # Put the startup project first in the project list
+ if startup_idx:
+ projlist.insert(0, projlist.pop(startup_idx))
+
+ return projlist
+
+ def split_sources(self, srclist):
+ sources = []
+ headers = []
+ objects = []
+ languages = []
+ for i in srclist:
+ if self.environment.is_header(i):
+ headers.append(i)
+ elif self.environment.is_object(i):
+ objects.append(i)
+ elif self.environment.is_source(i):
+ sources.append(i)
+ lang = self.lang_from_source_file(i)
+ if lang not in languages:
+ languages.append(lang)
+ elif self.environment.is_library(i):
+ pass
+ else:
+ # Everything that is not an object or source file is considered a header.
+ headers.append(i)
+ return sources, headers, objects, languages
+
+ def target_to_build_root(self, target):
+ if self.get_target_dir(target) == '':
+ return ''
+
+ directories = os.path.normpath(self.get_target_dir(target)).split(os.sep)
+ return os.sep.join(['..'] * len(directories))
+
+ def quote_arguments(self, arr):
+ return ['"%s"' % i for i in arr]
+
+ def add_project_reference(self, root, include, projid, link_outputs=False):
+ ig = ET.SubElement(root, 'ItemGroup')
+ pref = ET.SubElement(ig, 'ProjectReference', Include=include)
+ ET.SubElement(pref, 'Project').text = '{%s}' % projid
+ if not link_outputs:
+ # Do not link in generated .lib files from dependencies automatically.
+ # We only use the dependencies for ordering and link in the generated
+ # objects and .lib files manually.
+ ET.SubElement(pref, 'LinkLibraryDependencies').text = 'false'
+
+ def add_target_deps(self, root, target):
+ target_dict = {target.get_id(): target}
+ for dep in self.get_target_deps(target_dict).values():
+ if dep.get_id() in self.handled_target_deps[target.get_id()]:
+ # This dependency was already handled manually.
+ continue
+ relpath = self.get_target_dir_relative_to(dep, target)
+ vcxproj = os.path.join(relpath, dep.get_id() + '.vcxproj')
+ tid = self.environment.coredata.target_guids[dep.get_id()]
+ self.add_project_reference(root, vcxproj, tid)
+
+ def create_basic_project(self, target_name, *,
+ temp_dir,
+ guid,
+ conftype='Utility',
+ target_ext=None,
+ target_platform=None):
+ root = ET.Element('Project', {'DefaultTargets': "Build",
+ 'ToolsVersion': '4.0',
+ 'xmlns': 'http://schemas.microsoft.com/developer/msbuild/2003'})
+
+ confitems = ET.SubElement(root, 'ItemGroup', {'Label': 'ProjectConfigurations'})
+ if not target_platform:
+ target_platform = self.platform
+ prjconf = ET.SubElement(confitems, 'ProjectConfiguration',
+ {'Include': self.buildtype + '|' + target_platform})
+ p = ET.SubElement(prjconf, 'Configuration')
+ p.text = self.buildtype
+ pl = ET.SubElement(prjconf, 'Platform')
+ pl.text = target_platform
+
+ # Globals
+ globalgroup = ET.SubElement(root, 'PropertyGroup', Label='Globals')
+ guidelem = ET.SubElement(globalgroup, 'ProjectGuid')
+ guidelem.text = '{%s}' % guid
+ kw = ET.SubElement(globalgroup, 'Keyword')
+ kw.text = self.platform + 'Proj'
+ # XXX Wasn't here before for anything but gen_vcxproj , but seems fine?
+ ns = ET.SubElement(globalgroup, 'RootNamespace')
+ ns.text = target_name
+
+ p = ET.SubElement(globalgroup, 'Platform')
+ p.text = target_platform
+ pname = ET.SubElement(globalgroup, 'ProjectName')
+ pname.text = target_name
+ if self.windows_target_platform_version:
+ ET.SubElement(globalgroup, 'WindowsTargetPlatformVersion').text = self.windows_target_platform_version
+ ET.SubElement(globalgroup, 'UseMultiToolTask').text = 'true'
+
+ ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.Default.props')
+
+ # Start configuration
+ type_config = ET.SubElement(root, 'PropertyGroup', Label='Configuration')
+ ET.SubElement(type_config, 'ConfigurationType').text = conftype
+ ET.SubElement(type_config, 'CharacterSet').text = 'MultiByte'
+ # Fixme: wasn't here before for gen_vcxproj()
+ ET.SubElement(type_config, 'UseOfMfc').text = 'false'
+ if self.platform_toolset:
+ ET.SubElement(type_config, 'PlatformToolset').text = self.platform_toolset
+
+ # End configuration section (but it can be added to further via type_config)
+ ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.props')
+
+ # Project information
+ direlem = ET.SubElement(root, 'PropertyGroup')
+ fver = ET.SubElement(direlem, '_ProjectFileVersion')
+ fver.text = self.project_file_version
+ outdir = ET.SubElement(direlem, 'OutDir')
+ outdir.text = '.\\'
+ intdir = ET.SubElement(direlem, 'IntDir')
+ intdir.text = temp_dir + '\\'
+
+ tname = ET.SubElement(direlem, 'TargetName')
+ tname.text = target_name
+
+ if target_ext:
+ ET.SubElement(direlem, 'TargetExt').text = target_ext
+
+ return (root, type_config)
+
+ def gen_run_target_vcxproj(self, target, ofname, guid):
+ (root, type_config) = self.create_basic_project(target.name,
+ temp_dir=target.get_id(),
+ guid=guid)
+ depend_files = self.get_custom_target_depend_files(target)
+
+ if not target.command:
+ # This is an alias target and thus doesn't run any command. It's
+ # enough to emit the references to the other projects for them to
+ # be built/run/..., if necessary.
+ assert isinstance(target, build.AliasTarget)
+ assert len(depend_files) == 0
+ else:
+ assert not isinstance(target, build.AliasTarget)
+
+ target_env = self.get_run_target_env(target)
+ _, _, cmd_raw = self.eval_custom_target_command(target)
+ wrapper_cmd, _ = self.as_meson_exe_cmdline(target.command[0], cmd_raw[1:],
+ force_serialize=True, env=target_env,
+ verbose=True)
+ self.add_custom_build(root, 'run_target', ' '.join(self.quote_arguments(wrapper_cmd)),
+ deps=depend_files)
+
+ # The import is needed even for alias targets, otherwise the build
+ # target isn't defined
+ ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets')
+ self.add_regen_dependency(root)
+ self.add_target_deps(root, target)
+ self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname)
+
+ def gen_custom_target_vcxproj(self, target, ofname, guid):
+ if target.for_machine is MachineChoice.BUILD:
+ platform = self.build_platform
+ else:
+ platform = self.platform
+ (root, type_config) = self.create_basic_project(target.name,
+ temp_dir=target.get_id(),
+ guid=guid,
+ target_platform=platform)
+ # We need to always use absolute paths because our invocation is always
+ # from the target dir, not the build root.
+ target.absolute_paths = True
+ (srcs, ofilenames, cmd) = self.eval_custom_target_command(target, True)
+ depend_files = self.get_custom_target_depend_files(target, True)
+ # Always use a wrapper because MSBuild eats random characters when
+ # there are many arguments.
+ tdir_abs = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target))
+ extra_bdeps = target.get_transitive_build_target_deps()
+ wrapper_cmd, _ = self.as_meson_exe_cmdline(target.command[0], cmd[1:],
+ # All targets run from the target dir
+ workdir=tdir_abs,
+ extra_bdeps=extra_bdeps,
+ capture=ofilenames[0] if target.capture else None,
+ feed=srcs[0] if target.feed else None,
+ force_serialize=True,
+ env=target.env,
+ verbose=target.console)
+ if target.build_always_stale:
+ # Use a nonexistent file to always consider the target out-of-date.
+ ofilenames += [self.nonexistent_file(os.path.join(self.environment.get_scratch_dir(),
+ 'outofdate.file'))]
+ self.add_custom_build(root, 'custom_target', ' '.join(self.quote_arguments(wrapper_cmd)),
+ deps=wrapper_cmd[-1:] + srcs + depend_files, outputs=ofilenames,
+ verify_files=not target.build_always_stale)
+ ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets')
+ self.generate_custom_generator_commands(target, root)
+ self.add_regen_dependency(root)
+ self.add_target_deps(root, target)
+ self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname)
+
+ def gen_compile_target_vcxproj(self, target, ofname, guid):
+ if target.for_machine is MachineChoice.BUILD:
+ platform = self.build_platform
+ else:
+ platform = self.platform
+ (root, type_config) = self.create_basic_project(target.name,
+ temp_dir=target.get_id(),
+ guid=guid,
+ target_platform=platform)
+ ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets')
+ target.generated = [self.compile_target_to_generator(target)]
+ target.sources = []
+ self.generate_custom_generator_commands(target, root)
+ self.add_regen_dependency(root)
+ self.add_target_deps(root, target)
+ self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname)
+
+ @classmethod
+ def lang_from_source_file(cls, src):
+ ext = src.split('.')[-1]
+ if ext in compilers.c_suffixes:
+ return 'c'
+ if ext in compilers.cpp_suffixes:
+ return 'cpp'
+ raise MesonException(f'Could not guess language from source file {src}.')
+
+ def add_pch(self, pch_sources, lang, inc_cl):
+ if lang in pch_sources:
+ self.use_pch(pch_sources, lang, inc_cl)
+
+ def create_pch(self, pch_sources, lang, inc_cl):
+ pch = ET.SubElement(inc_cl, 'PrecompiledHeader')
+ pch.text = 'Create'
+ self.add_pch_files(pch_sources, lang, inc_cl)
+
+ def use_pch(self, pch_sources, lang, inc_cl):
+ pch = ET.SubElement(inc_cl, 'PrecompiledHeader')
+ pch.text = 'Use'
+ header = self.add_pch_files(pch_sources, lang, inc_cl)
+ pch_include = ET.SubElement(inc_cl, 'ForcedIncludeFiles')
+ pch_include.text = header + ';%(ForcedIncludeFiles)'
+
+ def add_pch_files(self, pch_sources, lang, inc_cl):
+ header = os.path.basename(pch_sources[lang][0])
+ pch_file = ET.SubElement(inc_cl, 'PrecompiledHeaderFile')
+ # When USING PCHs, MSVC will not do the regular include
+ # directory lookup, but simply use a string match to find the
+ # PCH to use. That means the #include directive must match the
+ # pch_file.text used during PCH CREATION verbatim.
+ # When CREATING a PCH, MSVC will do the include directory
+ # lookup to find the actual PCH header to use. Thus, the PCH
+ # header must either be in the include_directories of the target
+ # or be in the same directory as the PCH implementation.
+ pch_file.text = header
+ pch_out = ET.SubElement(inc_cl, 'PrecompiledHeaderOutputFile')
+ pch_out.text = f'$(IntDir)$(TargetName)-{lang}.pch'
+
+ # Need to set the name for the pdb, as cl otherwise gives it a static
+ # name. Which leads to problems when there is more than one pch
+ # (e.g. for different languages).
+ pch_pdb = ET.SubElement(inc_cl, 'ProgramDataBaseFileName')
+ pch_pdb.text = f'$(IntDir)$(TargetName)-{lang}.pdb'
+
+ return header
+
+ def is_argument_with_msbuild_xml_entry(self, entry):
+ # Remove arguments that have a top level XML entry so
+ # they are not used twice.
+ # FIXME add args as needed.
+ if entry[1:].startswith('fsanitize'):
+ return True
+ return entry[1:].startswith('M')
+
+ def add_additional_options(self, lang, parent_node, file_args):
+ args = []
+ for arg in file_args[lang].to_native():
+ if self.is_argument_with_msbuild_xml_entry(arg):
+ continue
+ if arg == '%(AdditionalOptions)':
+ args.append(arg)
+ else:
+ args.append(self.escape_additional_option(arg))
+ ET.SubElement(parent_node, "AdditionalOptions").text = ' '.join(args)
+
+ def add_preprocessor_defines(self, lang, parent_node, file_defines):
+ defines = []
+ for define in file_defines[lang]:
+ if define == '%(PreprocessorDefinitions)':
+ defines.append(define)
+ else:
+ defines.append(self.escape_preprocessor_define(define))
+ ET.SubElement(parent_node, "PreprocessorDefinitions").text = ';'.join(defines)
+
+ def add_include_dirs(self, lang, parent_node, file_inc_dirs):
+ dirs = file_inc_dirs[lang]
+ ET.SubElement(parent_node, "AdditionalIncludeDirectories").text = ';'.join(dirs)
+
+ @staticmethod
+ def has_objects(objects, additional_objects, generated_objects):
+ # Ignore generated objects, those are automatically used by MSBuild because they are part of
+ # the CustomBuild Outputs.
+ return len(objects) + len(additional_objects) > 0
+
+ @staticmethod
+ def add_generated_objects(node, generated_objects):
+ # Do not add generated objects to project file. Those are automatically used by MSBuild, because
+ # they are part of the CustomBuild Outputs.
+ return
+
+ @staticmethod
+ def escape_preprocessor_define(define):
+ # See: https://msdn.microsoft.com/en-us/library/bb383819.aspx
+ table = str.maketrans({'%': '%25', '$': '%24', '@': '%40',
+ "'": '%27', ';': '%3B', '?': '%3F', '*': '%2A',
+ # We need to escape backslash because it'll be un-escaped by
+ # Windows during process creation when it parses the arguments
+ # Basically, this converts `\` to `\\`.
+ '\\': '\\\\'})
+ return define.translate(table)
+
+ @staticmethod
+ def escape_additional_option(option):
+ # See: https://msdn.microsoft.com/en-us/library/bb383819.aspx
+ table = str.maketrans({'%': '%25', '$': '%24', '@': '%40',
+ "'": '%27', ';': '%3B', '?': '%3F', '*': '%2A', ' ': '%20'})
+ option = option.translate(table)
+ # Since we're surrounding the option with ", if it ends in \ that will
+ # escape the " when the process arguments are parsed and the starting
+ # " will not terminate. So we escape it if that's the case. I'm not
+ # kidding, this is how escaping works for process args on Windows.
+ if option.endswith('\\'):
+ option += '\\'
+ return f'"{option}"'
+
+ @staticmethod
+ def split_link_args(args):
+ """
+ Split a list of link arguments into three lists:
+ * library search paths
+ * library filenames (or paths)
+ * other link arguments
+ """
+ lpaths = []
+ libs = []
+ other = []
+ for arg in args:
+ if arg.startswith('/LIBPATH:'):
+ lpath = arg[9:]
+ # De-dup library search paths by removing older entries when
+ # a new one is found. This is necessary because unlike other
+ # search paths such as the include path, the library is
+ # searched for in the newest (right-most) search path first.
+ if lpath in lpaths:
+ lpaths.remove(lpath)
+ lpaths.append(lpath)
+ elif arg.startswith(('/', '-')):
+ other.append(arg)
+ # It's ok if we miss libraries with non-standard extensions here.
+ # They will go into the general link arguments.
+ elif arg.endswith('.lib') or arg.endswith('.a'):
+ # De-dup
+ if arg not in libs:
+ libs.append(arg)
+ else:
+ other.append(arg)
+ return lpaths, libs, other
+
+ def _get_cl_compiler(self, target):
+ for lang, c in target.compilers.items():
+ if lang in {'c', 'cpp'}:
+ return c
+ # No source files, only objects, but we still need a compiler, so
+ # return a found compiler
+ if len(target.objects) > 0:
+ for lang, c in self.environment.coredata.compilers[target.for_machine].items():
+ if lang in {'c', 'cpp'}:
+ return c
+ raise MesonException('Could not find a C or C++ compiler. MSVC can only build C/C++ projects.')
+
+ def _prettyprint_vcxproj_xml(self, tree, ofname):
+ ofname_tmp = ofname + '~'
+ tree.write(ofname_tmp, encoding='utf-8', xml_declaration=True)
+
+ # ElementTree can not do prettyprinting so do it manually
+ doc = xml.dom.minidom.parse(ofname_tmp)
+ with open(ofname_tmp, 'w', encoding='utf-8') as of:
+ of.write(doc.toprettyxml())
+ replace_if_different(ofname, ofname_tmp)
+
+ def gen_vcxproj(self, target, ofname, guid):
+ mlog.debug(f'Generating vcxproj {target.name}.')
+ subsystem = 'Windows'
+ self.handled_target_deps[target.get_id()] = []
+ if isinstance(target, build.Executable):
+ conftype = 'Application'
+ if target.gui_app is not None:
+ if not target.gui_app:
+ subsystem = 'Console'
+ else:
+ # If someone knows how to set the version properly,
+ # please send a patch.
+ subsystem = target.win_subsystem.split(',')[0]
+ elif isinstance(target, build.StaticLibrary):
+ conftype = 'StaticLibrary'
+ elif isinstance(target, build.SharedLibrary):
+ conftype = 'DynamicLibrary'
+ elif isinstance(target, build.CustomTarget):
+ return self.gen_custom_target_vcxproj(target, ofname, guid)
+ elif isinstance(target, build.RunTarget):
+ return self.gen_run_target_vcxproj(target, ofname, guid)
+ elif isinstance(target, build.CompileTarget):
+ return self.gen_compile_target_vcxproj(target, ofname, guid)
+ else:
+ raise MesonException(f'Unknown target type for {target.get_basename()}')
+ # Prefix to use to access the build root from the vcxproj dir
+ down = self.target_to_build_root(target)
+ # Prefix to use to access the source tree's root from the vcxproj dir
+ proj_to_src_root = os.path.join(down, self.build_to_src)
+ # Prefix to use to access the source tree's subdir from the vcxproj dir
+ proj_to_src_dir = os.path.join(proj_to_src_root, self.get_target_dir(target))
+ (sources, headers, objects, languages) = self.split_sources(target.sources)
+ if target.is_unity:
+ sources = self.generate_unity_files(target, sources)
+ compiler = self._get_cl_compiler(target)
+ build_args = compiler.get_buildtype_args(self.buildtype)
+ build_args += compiler.get_optimization_args(self.optimization)
+ build_args += compiler.get_debug_args(self.debug)
+ build_args += compiler.sanitizer_compile_args(self.sanitize)
+ buildtype_link_args = compiler.get_buildtype_linker_args(self.buildtype)
+ vscrt_type = self.environment.coredata.options[OptionKey('b_vscrt')]
+ target_name = target.name
+ if target.for_machine is MachineChoice.BUILD:
+ platform = self.build_platform
+ else:
+ platform = self.platform
+
+ tfilename = os.path.splitext(target.get_filename())
+
+ (root, type_config) = self.create_basic_project(tfilename[0],
+ temp_dir=target.get_id(),
+ guid=guid,
+ conftype=conftype,
+ target_ext=tfilename[1],
+ target_platform=platform)
+
+ # FIXME: Should these just be set in create_basic_project(), even if
+ # irrelevant for current target?
+
+ # FIXME: Meson's LTO support needs to be integrated here
+ ET.SubElement(type_config, 'WholeProgramOptimization').text = 'false'
+ # Let VS auto-set the RTC level
+ ET.SubElement(type_config, 'BasicRuntimeChecks').text = 'Default'
+ # Incremental linking increases code size
+ if '/INCREMENTAL:NO' in buildtype_link_args:
+ ET.SubElement(type_config, 'LinkIncremental').text = 'false'
+
+ # Build information
+ compiles = ET.SubElement(root, 'ItemDefinitionGroup')
+ clconf = ET.SubElement(compiles, 'ClCompile')
+ # CRT type; debug or release
+ if vscrt_type.value == 'from_buildtype':
+ if self.buildtype == 'debug':
+ ET.SubElement(type_config, 'UseDebugLibraries').text = 'true'
+ ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebugDLL'
+ else:
+ ET.SubElement(type_config, 'UseDebugLibraries').text = 'false'
+ ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDLL'
+ elif vscrt_type.value == 'static_from_buildtype':
+ if self.buildtype == 'debug':
+ ET.SubElement(type_config, 'UseDebugLibraries').text = 'true'
+ ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebug'
+ else:
+ ET.SubElement(type_config, 'UseDebugLibraries').text = 'false'
+ ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreaded'
+ elif vscrt_type.value == 'mdd':
+ ET.SubElement(type_config, 'UseDebugLibraries').text = 'true'
+ ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebugDLL'
+ elif vscrt_type.value == 'mt':
+ # FIXME, wrong
+ ET.SubElement(type_config, 'UseDebugLibraries').text = 'false'
+ ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreaded'
+ elif vscrt_type.value == 'mtd':
+ # FIXME, wrong
+ ET.SubElement(type_config, 'UseDebugLibraries').text = 'true'
+ ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebug'
+ else:
+ ET.SubElement(type_config, 'UseDebugLibraries').text = 'false'
+ ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDLL'
+ # Sanitizers
+ if '/fsanitize=address' in build_args:
+ ET.SubElement(type_config, 'EnableASAN').text = 'true'
+ # Debug format
+ if '/ZI' in build_args:
+ ET.SubElement(clconf, 'DebugInformationFormat').text = 'EditAndContinue'
+ elif '/Zi' in build_args:
+ ET.SubElement(clconf, 'DebugInformationFormat').text = 'ProgramDatabase'
+ elif '/Z7' in build_args:
+ ET.SubElement(clconf, 'DebugInformationFormat').text = 'OldStyle'
+ else:
+ ET.SubElement(clconf, 'DebugInformationFormat').text = 'None'
+ # Runtime checks
+ if '/RTC1' in build_args:
+ ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'EnableFastChecks'
+ elif '/RTCu' in build_args:
+ ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'UninitializedLocalUsageCheck'
+ elif '/RTCs' in build_args:
+ ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'StackFrameRuntimeCheck'
+ # Exception handling has to be set in the xml in addition to the "AdditionalOptions" because otherwise
+ # cl will give warning D9025: overriding '/Ehs' with cpp_eh value
+ if 'cpp' in target.compilers:
+ eh = self.environment.coredata.options[OptionKey('eh', machine=target.for_machine, lang='cpp')]
+ if eh.value == 'a':
+ ET.SubElement(clconf, 'ExceptionHandling').text = 'Async'
+ elif eh.value == 's':
+ ET.SubElement(clconf, 'ExceptionHandling').text = 'SyncCThrow'
+ elif eh.value == 'none':
+ ET.SubElement(clconf, 'ExceptionHandling').text = 'false'
+ else: # 'sc' or 'default'
+ ET.SubElement(clconf, 'ExceptionHandling').text = 'Sync'
+ generated_files, custom_target_output_files, generated_files_include_dirs = self.generate_custom_generator_commands(
+ target, root)
+ (gen_src, gen_hdrs, gen_objs, gen_langs) = self.split_sources(generated_files)
+ (custom_src, custom_hdrs, custom_objs, custom_langs) = self.split_sources(custom_target_output_files)
+ gen_src += custom_src
+ gen_hdrs += custom_hdrs
+ gen_langs += custom_langs
+
+ # Arguments, include dirs, defines for all files in the current target
+ target_args = []
+ target_defines = []
+ target_inc_dirs = []
+ # Arguments, include dirs, defines passed to individual files in
+ # a target; perhaps because the args are language-specific
+ #
+ # file_args is also later split out into defines and include_dirs in
+ # case someone passed those in there
+ file_args = {l: c.compiler_args() for l, c in target.compilers.items()}
+ file_defines = {l: [] for l in target.compilers}
+ file_inc_dirs = {l: [] for l in target.compilers}
+ # The order in which these compile args are added must match
+ # generate_single_compile() and generate_basic_compiler_args()
+ for l, comp in target.compilers.items():
+ if l in file_args:
+ file_args[l] += compilers.get_base_compile_args(
+ target.get_options(), comp)
+ file_args[l] += comp.get_option_compile_args(
+ target.get_options())
+
+ # Add compile args added using add_project_arguments()
+ for l, args in self.build.projects_args[target.for_machine].get(target.subproject, {}).items():
+ if l in file_args:
+ file_args[l] += args
+ # Add compile args added using add_global_arguments()
+ # These override per-project arguments
+ for l, args in self.build.global_args[target.for_machine].items():
+ if l in file_args:
+ file_args[l] += args
+ # Compile args added from the env or cross file: CFLAGS/CXXFLAGS, etc. We want these
+ # to override all the defaults, but not the per-target compile args.
+ for l in file_args.keys():
+ opts = self.environment.coredata.options[OptionKey('args', machine=target.for_machine, lang=l)]
+ file_args[l] += opts.value
+ for args in file_args.values():
+ # This is where Visual Studio will insert target_args, target_defines,
+ # etc, which are added later from external deps (see below).
+ args += ['%(AdditionalOptions)', '%(PreprocessorDefinitions)', '%(AdditionalIncludeDirectories)']
+ # Add custom target dirs as includes automatically, but before
+ # target-specific include dirs. See _generate_single_compile() in
+ # the ninja backend for caveats.
+ args += ['-I' + arg for arg in generated_files_include_dirs]
+ # Add include dirs from the `include_directories:` kwarg on the target
+ # and from `include_directories:` of internal deps of the target.
+ #
+ # Target include dirs should override internal deps include dirs.
+ # This is handled in BuildTarget.process_kwargs()
+ #
+ # Include dirs from internal deps should override include dirs from
+ # external deps and must maintain the order in which they are
+ # specified. Hence, we must reverse so that the order is preserved.
+ #
+ # These are per-target, but we still add them as per-file because we
+ # need them to be looked in first.
+ for d in reversed(target.get_include_dirs()):
+ # reversed is used to keep order of includes
+ for i in reversed(d.get_incdirs()):
+ curdir = os.path.join(d.get_curdir(), i)
+ try:
+ args.append('-I' + self.relpath(curdir, target.subdir)) # build dir
+ args.append('-I' + os.path.join(proj_to_src_root, curdir)) # src dir
+ except ValueError:
+ # Include is on different drive
+ args.append('-I' + os.path.normpath(curdir))
+ for i in d.get_extra_build_dirs():
+ curdir = os.path.join(d.get_curdir(), i)
+ args.append('-I' + self.relpath(curdir, target.subdir)) # build dir
+ # Add per-target compile args, f.ex, `c_args : ['/DFOO']`. We set these
+ # near the end since these are supposed to override everything else.
+ for l, args in target.extra_args.items():
+ if l in file_args:
+ file_args[l] += args
+ # The highest priority includes. In order of directory search:
+ # target private dir, target build dir, target source dir
+ for args in file_args.values():
+ t_inc_dirs = [self.relpath(self.get_target_private_dir(target),
+ self.get_target_dir(target))]
+ if target.implicit_include_directories:
+ t_inc_dirs += ['.', proj_to_src_dir]
+ args += ['-I' + arg for arg in t_inc_dirs]
+
+ # Split preprocessor defines and include directories out of the list of
+ # all extra arguments. The rest go into %(AdditionalOptions).
+ for l, args in file_args.items():
+ for arg in args[:]:
+ if arg.startswith(('-D', '/D')) or arg == '%(PreprocessorDefinitions)':
+ file_args[l].remove(arg)
+ # Don't escape the marker
+ if arg == '%(PreprocessorDefinitions)':
+ define = arg
+ else:
+ define = arg[2:]
+ # De-dup
+ if define not in file_defines[l]:
+ file_defines[l].append(define)
+ elif arg.startswith(('-I', '/I')) or arg == '%(AdditionalIncludeDirectories)':
+ file_args[l].remove(arg)
+ # Don't escape the marker
+ if arg == '%(AdditionalIncludeDirectories)':
+ inc_dir = arg
+ else:
+ inc_dir = arg[2:]
+ # De-dup
+ if inc_dir not in file_inc_dirs[l]:
+ file_inc_dirs[l].append(inc_dir)
+ # Add include dirs to target as well so that "Go to Document" works in headers
+ if inc_dir not in target_inc_dirs:
+ target_inc_dirs.append(inc_dir)
+
+ # Split compile args needed to find external dependencies
+ # Link args are added while generating the link command
+ for d in reversed(target.get_external_deps()):
+ # Cflags required by external deps might have UNIX-specific flags,
+ # so filter them out if needed
+ if isinstance(d, dependencies.OpenMPDependency):
+ ET.SubElement(clconf, 'OpenMPSupport').text = 'true'
+ else:
+ d_compile_args = compiler.unix_args_to_native(d.get_compile_args())
+ for arg in d_compile_args:
+ if arg.startswith(('-D', '/D')):
+ define = arg[2:]
+ # De-dup
+ if define in target_defines:
+ target_defines.remove(define)
+ target_defines.append(define)
+ elif arg.startswith(('-I', '/I')):
+ inc_dir = arg[2:]
+ # De-dup
+ if inc_dir not in target_inc_dirs:
+ target_inc_dirs.append(inc_dir)
+ else:
+ target_args.append(arg)
+
+ languages += gen_langs
+ if '/Gw' in build_args:
+ target_args.append('/Gw')
+ if len(target_args) > 0:
+ target_args.append('%(AdditionalOptions)')
+ ET.SubElement(clconf, "AdditionalOptions").text = ' '.join(target_args)
+ ET.SubElement(clconf, 'AdditionalIncludeDirectories').text = ';'.join(target_inc_dirs)
+ target_defines.append('%(PreprocessorDefinitions)')
+ ET.SubElement(clconf, 'PreprocessorDefinitions').text = ';'.join(target_defines)
+ ET.SubElement(clconf, 'FunctionLevelLinking').text = 'true'
+ # Warning level
+ warning_level = target.get_option(OptionKey('warning_level'))
+ ET.SubElement(clconf, 'WarningLevel').text = 'Level' + str(1 + int(warning_level))
+ if target.get_option(OptionKey('werror')):
+ ET.SubElement(clconf, 'TreatWarningAsError').text = 'true'
+ # Optimization flags
+ o_flags = split_o_flags_args(build_args)
+ if '/Ox' in o_flags:
+ ET.SubElement(clconf, 'Optimization').text = 'Full'
+ elif '/O2' in o_flags:
+ ET.SubElement(clconf, 'Optimization').text = 'MaxSpeed'
+ elif '/O1' in o_flags:
+ ET.SubElement(clconf, 'Optimization').text = 'MinSpace'
+ elif '/Od' in o_flags:
+ ET.SubElement(clconf, 'Optimization').text = 'Disabled'
+ if '/Oi' in o_flags:
+ ET.SubElement(clconf, 'IntrinsicFunctions').text = 'true'
+ if '/Ob1' in o_flags:
+ ET.SubElement(clconf, 'InlineFunctionExpansion').text = 'OnlyExplicitInline'
+ elif '/Ob2' in o_flags:
+ ET.SubElement(clconf, 'InlineFunctionExpansion').text = 'AnySuitable'
+ # Size-preserving flags
+ if '/Os' in o_flags:
+ ET.SubElement(clconf, 'FavorSizeOrSpeed').text = 'Size'
+ # Note: setting FavorSizeOrSpeed with clang-cl conflicts with /Od and can make debugging difficult, so don't.
+ elif '/Od' not in o_flags:
+ ET.SubElement(clconf, 'FavorSizeOrSpeed').text = 'Speed'
+ # Note: SuppressStartupBanner is /NOLOGO and is 'true' by default
+ self.generate_lang_standard_info(file_args, clconf)
+ pch_sources = {}
+ if self.environment.coredata.options.get(OptionKey('b_pch')):
+ for lang in ['c', 'cpp']:
+ pch = target.get_pch(lang)
+ if not pch:
+ continue
+ if compiler.id == 'msvc':
+ if len(pch) == 1:
+ # Auto generate PCH.
+ src = os.path.join(down, self.create_msvc_pch_implementation(target, lang, pch[0]))
+ pch_header_dir = os.path.dirname(os.path.join(proj_to_src_dir, pch[0]))
+ else:
+ src = os.path.join(proj_to_src_dir, pch[1])
+ pch_header_dir = None
+ pch_sources[lang] = [pch[0], src, lang, pch_header_dir]
+ else:
+ # I don't know whether its relevant but let's handle other compilers
+ # used with a vs backend
+ pch_sources[lang] = [pch[0], None, lang, None]
+
+ resourcecompile = ET.SubElement(compiles, 'ResourceCompile')
+ ET.SubElement(resourcecompile, 'PreprocessorDefinitions')
+
+ # Linker options
+ link = ET.SubElement(compiles, 'Link')
+ extra_link_args = compiler.compiler_args()
+ # FIXME: Can these buildtype linker args be added as tags in the
+ # vcxproj file (similar to buildtype compiler args) instead of in
+ # AdditionalOptions?
+ extra_link_args += compiler.get_buildtype_linker_args(self.buildtype)
+ # Generate Debug info
+ if self.debug:
+ self.generate_debug_information(link)
+ else:
+ ET.SubElement(link, 'GenerateDebugInformation').text = 'false'
+ if not isinstance(target, build.StaticLibrary):
+ if isinstance(target, build.SharedModule):
+ options = self.environment.coredata.options
+ extra_link_args += compiler.get_std_shared_module_link_args(options)
+ # Add link args added using add_project_link_arguments()
+ extra_link_args += self.build.get_project_link_args(compiler, target.subproject, target.for_machine)
+ # Add link args added using add_global_link_arguments()
+ # These override per-project link arguments
+ extra_link_args += self.build.get_global_link_args(compiler, target.for_machine)
+ # Link args added from the env: LDFLAGS, or the cross file. We want
+ # these to override all the defaults but not the per-target link
+ # args.
+ extra_link_args += self.environment.coredata.get_external_link_args(
+ target.for_machine, compiler.get_language())
+ # Only non-static built targets need link args and link dependencies
+ extra_link_args += target.link_args
+ # External deps must be last because target link libraries may depend on them.
+ for dep in target.get_external_deps():
+ # Extend without reordering or de-dup to preserve `-L -l` sets
+ # https://github.com/mesonbuild/meson/issues/1718
+ if isinstance(dep, dependencies.OpenMPDependency):
+ ET.SubElement(clconf, 'OpenMPSuppport').text = 'true'
+ else:
+ extra_link_args.extend_direct(dep.get_link_args())
+ for d in target.get_dependencies():
+ if isinstance(d, build.StaticLibrary):
+ for dep in d.get_external_deps():
+ if isinstance(dep, dependencies.OpenMPDependency):
+ ET.SubElement(clconf, 'OpenMPSuppport').text = 'true'
+ else:
+ extra_link_args.extend_direct(dep.get_link_args())
+ # Add link args for c_* or cpp_* build options. Currently this only
+ # adds c_winlibs and cpp_winlibs when building for Windows. This needs
+ # to be after all internal and external libraries so that unresolved
+ # symbols from those can be found here. This is needed when the
+ # *_winlibs that we want to link to are static mingw64 libraries.
+ extra_link_args += compiler.get_option_link_args(self.environment.coredata.options)
+ (additional_libpaths, additional_links, extra_link_args) = self.split_link_args(extra_link_args.to_native())
+
+ # Add more libraries to be linked if needed
+ for t in target.get_dependencies():
+ if isinstance(t, build.CustomTargetIndex):
+ # We don't need the actual project here, just the library name
+ lobj = t
+ else:
+ lobj = self.build.targets[t.get_id()]
+ linkname = os.path.join(down, self.get_target_filename_for_linking(lobj))
+ if t in target.link_whole_targets:
+ if compiler.id == 'msvc' and version_compare(compiler.version, '<19.00.23918'):
+ # Expand our object lists manually if we are on pre-Visual Studio 2015 Update 2
+ l = t.extract_all_objects(False)
+
+ # Unfortunately, we can't use self.object_filename_from_source()
+ for gen in l.genlist:
+ for src in gen.get_outputs():
+ if self.environment.is_source(src):
+ path = self.get_target_generated_dir(t, gen, src)
+ gen_src_ext = '.' + os.path.splitext(path)[1][1:]
+ extra_link_args.append(path[:-len(gen_src_ext)] + '.obj')
+
+ for src in l.srclist:
+ obj_basename = None
+ if self.environment.is_source(src):
+ obj_basename = self.object_filename_from_source(t, src)
+ target_private_dir = self.relpath(self.get_target_private_dir(t),
+ self.get_target_dir(t))
+ rel_obj = os.path.join(target_private_dir, obj_basename)
+ extra_link_args.append(rel_obj)
+
+ extra_link_args.extend(self.flatten_object_list(t))
+ else:
+ # /WHOLEARCHIVE:foo must go into AdditionalOptions
+ extra_link_args += compiler.get_link_whole_for(linkname)
+ # To force Visual Studio to build this project even though it
+ # has no sources, we include a reference to the vcxproj file
+ # that builds this target. Technically we should add this only
+ # if the current target has no sources, but it doesn't hurt to
+ # have 'extra' references.
+ trelpath = self.get_target_dir_relative_to(t, target)
+ tvcxproj = os.path.join(trelpath, t.get_id() + '.vcxproj')
+ tid = self.environment.coredata.target_guids[t.get_id()]
+ self.add_project_reference(root, tvcxproj, tid, link_outputs=True)
+ # Mark the dependency as already handled to not have
+ # multiple references to the same target.
+ self.handled_target_deps[target.get_id()].append(t.get_id())
+ else:
+ # Other libraries go into AdditionalDependencies
+ if linkname not in additional_links:
+ additional_links.append(linkname)
+ for lib in self.get_custom_target_provided_libraries(target):
+ additional_links.append(self.relpath(lib, self.get_target_dir(target)))
+ additional_objects = []
+ for o in self.flatten_object_list(target, down)[0]:
+ assert isinstance(o, str)
+ additional_objects.append(o)
+ for o in custom_objs:
+ additional_objects.append(o)
+
+ if len(extra_link_args) > 0:
+ extra_link_args.append('%(AdditionalOptions)')
+ ET.SubElement(link, "AdditionalOptions").text = ' '.join(extra_link_args)
+ if len(additional_libpaths) > 0:
+ additional_libpaths.insert(0, '%(AdditionalLibraryDirectories)')
+ ET.SubElement(link, 'AdditionalLibraryDirectories').text = ';'.join(additional_libpaths)
+ if len(additional_links) > 0:
+ additional_links.append('%(AdditionalDependencies)')
+ ET.SubElement(link, 'AdditionalDependencies').text = ';'.join(additional_links)
+ ofile = ET.SubElement(link, 'OutputFile')
+ ofile.text = f'$(OutDir){target.get_filename()}'
+ subsys = ET.SubElement(link, 'SubSystem')
+ subsys.text = subsystem
+ if isinstance(target, (build.SharedLibrary, build.Executable)) and target.get_import_filename():
+ # DLLs built with MSVC always have an import library except when
+ # they're data-only DLLs, but we don't support those yet.
+ ET.SubElement(link, 'ImportLibrary').text = target.get_import_filename()
+ if isinstance(target, build.SharedLibrary):
+ # Add module definitions file, if provided
+ if target.vs_module_defs:
+ relpath = os.path.join(down, target.vs_module_defs.rel_to_builddir(self.build_to_src))
+ ET.SubElement(link, 'ModuleDefinitionFile').text = relpath
+ if self.debug:
+ pdb = ET.SubElement(link, 'ProgramDataBaseFileName')
+ pdb.text = f'$(OutDir){target_name}.pdb'
+ targetmachine = ET.SubElement(link, 'TargetMachine')
+ if target.for_machine is MachineChoice.BUILD:
+ targetplatform = platform.lower()
+ else:
+ targetplatform = self.platform.lower()
+ if targetplatform == 'win32':
+ targetmachine.text = 'MachineX86'
+ elif targetplatform == 'x64' or detect_microsoft_gdk(targetplatform):
+ targetmachine.text = 'MachineX64'
+ elif targetplatform == 'arm':
+ targetmachine.text = 'MachineARM'
+ elif targetplatform == 'arm64':
+ targetmachine.text = 'MachineARM64'
+ elif targetplatform == 'arm64ec':
+ targetmachine.text = 'MachineARM64EC'
+ else:
+ raise MesonException('Unsupported Visual Studio target machine: ' + targetplatform)
+ # /nologo
+ ET.SubElement(link, 'SuppressStartupBanner').text = 'true'
+ # /release
+ if not self.environment.coredata.get_option(OptionKey('debug')):
+ ET.SubElement(link, 'SetChecksum').text = 'true'
+
+ meson_file_group = ET.SubElement(root, 'ItemGroup')
+ ET.SubElement(meson_file_group, 'None', Include=os.path.join(proj_to_src_dir, build_filename))
+
+ # Visual Studio can't load projects that present duplicated items. Filter them out
+ # by keeping track of already added paths.
+ def path_normalize_add(path, lis):
+ normalized = os.path.normcase(os.path.normpath(path))
+ if normalized not in lis:
+ lis.append(normalized)
+ return True
+ else:
+ return False
+
+ previous_includes = []
+ if len(headers) + len(gen_hdrs) + len(target.extra_files) + len(pch_sources) > 0:
+ inc_hdrs = ET.SubElement(root, 'ItemGroup')
+ for h in headers:
+ relpath = os.path.join(down, h.rel_to_builddir(self.build_to_src))
+ if path_normalize_add(relpath, previous_includes):
+ ET.SubElement(inc_hdrs, 'CLInclude', Include=relpath)
+ for h in gen_hdrs:
+ if path_normalize_add(h, previous_includes):
+ ET.SubElement(inc_hdrs, 'CLInclude', Include=h)
+ for h in target.extra_files:
+ relpath = os.path.join(down, h.rel_to_builddir(self.build_to_src))
+ if path_normalize_add(relpath, previous_includes):
+ ET.SubElement(inc_hdrs, 'CLInclude', Include=relpath)
+ for headers in pch_sources.values():
+ path = os.path.join(proj_to_src_dir, headers[0])
+ if path_normalize_add(path, previous_includes):
+ ET.SubElement(inc_hdrs, 'CLInclude', Include=path)
+
+ previous_sources = []
+ if len(sources) + len(gen_src) + len(pch_sources) > 0:
+ inc_src = ET.SubElement(root, 'ItemGroup')
+ for s in sources:
+ relpath = os.path.join(down, s.rel_to_builddir(self.build_to_src))
+ if path_normalize_add(relpath, previous_sources):
+ inc_cl = ET.SubElement(inc_src, 'CLCompile', Include=relpath)
+ lang = Vs2010Backend.lang_from_source_file(s)
+ self.add_pch(pch_sources, lang, inc_cl)
+ self.add_additional_options(lang, inc_cl, file_args)
+ self.add_preprocessor_defines(lang, inc_cl, file_defines)
+ self.add_include_dirs(lang, inc_cl, file_inc_dirs)
+ ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + \
+ self.object_filename_from_source(target, s)
+ for s in gen_src:
+ if path_normalize_add(s, previous_sources):
+ inc_cl = ET.SubElement(inc_src, 'CLCompile', Include=s)
+ lang = Vs2010Backend.lang_from_source_file(s)
+ self.add_pch(pch_sources, lang, inc_cl)
+ self.add_additional_options(lang, inc_cl, file_args)
+ self.add_preprocessor_defines(lang, inc_cl, file_defines)
+ self.add_include_dirs(lang, inc_cl, file_inc_dirs)
+ s = File.from_built_file(target.get_subdir(), s)
+ ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + \
+ self.object_filename_from_source(target, s)
+ for lang, headers in pch_sources.items():
+ impl = headers[1]
+ if impl and path_normalize_add(impl, previous_sources):
+ inc_cl = ET.SubElement(inc_src, 'CLCompile', Include=impl)
+ self.create_pch(pch_sources, lang, inc_cl)
+ self.add_additional_options(lang, inc_cl, file_args)
+ self.add_preprocessor_defines(lang, inc_cl, file_defines)
+ pch_header_dir = pch_sources[lang][3]
+ if pch_header_dir:
+ inc_dirs = copy.deepcopy(file_inc_dirs)
+ inc_dirs[lang] = [pch_header_dir] + inc_dirs[lang]
+ else:
+ inc_dirs = file_inc_dirs
+ self.add_include_dirs(lang, inc_cl, inc_dirs)
+ # XXX: Do we need to set the object file name name here too?
+
+ previous_objects = []
+ if self.has_objects(objects, additional_objects, gen_objs):
+ inc_objs = ET.SubElement(root, 'ItemGroup')
+ for s in objects:
+ relpath = os.path.join(down, s.rel_to_builddir(self.build_to_src))
+ if path_normalize_add(relpath, previous_objects):
+ ET.SubElement(inc_objs, 'Object', Include=relpath)
+ for s in additional_objects:
+ if path_normalize_add(s, previous_objects):
+ ET.SubElement(inc_objs, 'Object', Include=s)
+ self.add_generated_objects(inc_objs, gen_objs)
+
+ ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets')
+ self.add_regen_dependency(root)
+ self.add_target_deps(root, target)
+ self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname)
+
+ def gen_regenproj(self, project_name, ofname):
+ guid = self.environment.coredata.regen_guid
+ (root, type_config) = self.create_basic_project(project_name,
+ temp_dir='regen-temp',
+ guid=guid)
+
+ action = ET.SubElement(root, 'ItemDefinitionGroup')
+ midl = ET.SubElement(action, 'Midl')
+ ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)'
+ ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)'
+ ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h'
+ ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb'
+ ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c'
+ ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c'
+ regen_command = self.environment.get_build_command() + ['--internal', 'regencheck']
+ cmd_templ = '''call %s > NUL
+"%s" "%s"'''
+ regen_command = cmd_templ % \
+ (self.get_vcvars_command(), '" "'.join(regen_command), self.environment.get_scratch_dir())
+ self.add_custom_build(root, 'regen', regen_command, deps=self.get_regen_filelist(),
+ outputs=[Vs2010Backend.get_regen_stampfile(self.environment.get_build_dir())],
+ msg='Checking whether solution needs to be regenerated.')
+ ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets')
+ ET.SubElement(root, 'ImportGroup', Label='ExtensionTargets')
+ self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname)
+
+ def gen_testproj(self, target_name, ofname):
+ guid = self.environment.coredata.test_guid
+ (root, type_config) = self.create_basic_project(target_name,
+ temp_dir='test-temp',
+ guid=guid)
+
+ action = ET.SubElement(root, 'ItemDefinitionGroup')
+ midl = ET.SubElement(action, 'Midl')
+ ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)'
+ ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)'
+ ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h'
+ ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb'
+ ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c'
+ ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c'
+ # FIXME: No benchmarks?
+ test_command = self.environment.get_build_command() + ['test', '--no-rebuild']
+ if not self.environment.coredata.get_option(OptionKey('stdsplit')):
+ test_command += ['--no-stdsplit']
+ if self.environment.coredata.get_option(OptionKey('errorlogs')):
+ test_command += ['--print-errorlogs']
+ self.serialize_tests()
+ self.add_custom_build(root, 'run_tests', '"%s"' % ('" "'.join(test_command)))
+ ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets')
+ self.add_regen_dependency(root)
+ self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname)
+
+ def gen_installproj(self, target_name, ofname):
+ self.create_install_data_files()
+
+ guid = self.environment.coredata.install_guid
+ (root, type_config) = self.create_basic_project(target_name,
+ temp_dir='install-temp',
+ guid=guid)
+
+ action = ET.SubElement(root, 'ItemDefinitionGroup')
+ midl = ET.SubElement(action, 'Midl')
+ ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)'
+ ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)'
+ ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h'
+ ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb'
+ ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c'
+ ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c'
+ install_command = self.environment.get_build_command() + ['install', '--no-rebuild']
+ self.add_custom_build(root, 'run_install', '"%s"' % ('" "'.join(install_command)))
+ ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets')
+ self.add_regen_dependency(root)
+ self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname)
+
+ def add_custom_build(self, node, rulename, command, deps=None, outputs=None, msg=None, verify_files=True):
+ igroup = ET.SubElement(node, 'ItemGroup')
+ rulefile = os.path.join(self.environment.get_scratch_dir(), rulename + '.rule')
+ if not os.path.exists(rulefile):
+ with open(rulefile, 'w', encoding='utf-8') as f:
+ f.write("# Meson regen file.")
+ custombuild = ET.SubElement(igroup, 'CustomBuild', Include=rulefile)
+ if msg:
+ message = ET.SubElement(custombuild, 'Message')
+ message.text = msg
+ if not verify_files:
+ ET.SubElement(custombuild, 'VerifyInputsAndOutputsExist').text = 'false'
+
+ # If a command ever were to change the current directory or set local
+ # variables this would need to be more complicated, as msbuild by
+ # default executes all CustomBuilds in a project using the same
+ # shell. Right now such tasks are all done inside the meson_exe
+ # wrapper. The trailing newline appears to be necessary to allow
+ # parallel custom builds to work.
+ ET.SubElement(custombuild, 'Command').text = f"{command}\n"
+
+ if not outputs:
+ # Use a nonexistent file to always consider the target out-of-date.
+ outputs = [self.nonexistent_file(os.path.join(self.environment.get_scratch_dir(),
+ 'outofdate.file'))]
+ ET.SubElement(custombuild, 'Outputs').text = ';'.join(outputs)
+ if deps:
+ ET.SubElement(custombuild, 'AdditionalInputs').text = ';'.join(deps)
+
+ @staticmethod
+ def nonexistent_file(prefix):
+ i = 0
+ file = prefix
+ while os.path.exists(file):
+ file = '%s%d' % (prefix, i)
+ return file
+
+ def generate_debug_information(self, link):
+ # valid values for vs2015 is 'false', 'true', 'DebugFastLink'
+ ET.SubElement(link, 'GenerateDebugInformation').text = 'true'
+
+ def add_regen_dependency(self, root):
+ regen_vcxproj = os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj')
+ self.add_project_reference(root, regen_vcxproj, self.environment.coredata.regen_guid)
+
+ def generate_lang_standard_info(self, file_args, clconf):
+ pass
diff --git a/mesonbuild/backend/vs2012backend.py b/mesonbuild/backend/vs2012backend.py
new file mode 100644
index 0000000..af8d5df
--- /dev/null
+++ b/mesonbuild/backend/vs2012backend.py
@@ -0,0 +1,43 @@
+# Copyright 2014-2016 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.
+from __future__ import annotations
+
+import typing as T
+
+from .vs2010backend import Vs2010Backend
+from ..mesonlib import MesonException
+
+if T.TYPE_CHECKING:
+ from ..build import Build
+ from ..interpreter import Interpreter
+
+class Vs2012Backend(Vs2010Backend):
+ def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]):
+ super().__init__(build, interpreter)
+ self.name = 'vs2012'
+ self.vs_version = '2012'
+ self.sln_file_version = '12.00'
+ self.sln_version_comment = '2012'
+ if self.environment is not None:
+ # TODO: we assume host == build
+ comps = self.environment.coredata.compilers.host
+ if comps and all(c.id == 'intel-cl' for c in comps.values()):
+ c = list(comps.values())[0]
+ if c.version.startswith('19'):
+ self.platform_toolset = 'Intel C++ Compiler 19.0'
+ else:
+ # We don't have support for versions older than 2019 right now.
+ raise MesonException('There is currently no support for ICL before 19, patches welcome.')
+ if self.platform_toolset is None:
+ self.platform_toolset = 'v110'
diff --git a/mesonbuild/backend/vs2013backend.py b/mesonbuild/backend/vs2013backend.py
new file mode 100644
index 0000000..44d45d6
--- /dev/null
+++ b/mesonbuild/backend/vs2013backend.py
@@ -0,0 +1,42 @@
+# Copyright 2014-2016 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.
+from __future__ import annotations
+
+from .vs2010backend import Vs2010Backend
+from ..mesonlib import MesonException
+import typing as T
+
+if T.TYPE_CHECKING:
+ from ..build import Build
+ from ..interpreter import Interpreter
+
+class Vs2013Backend(Vs2010Backend):
+ def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]):
+ super().__init__(build, interpreter)
+ self.name = 'vs2013'
+ self.vs_version = '2013'
+ self.sln_file_version = '12.00'
+ self.sln_version_comment = '2013'
+ if self.environment is not None:
+ # TODO: we assume host == build
+ comps = self.environment.coredata.compilers.host
+ if comps and all(c.id == 'intel-cl' for c in comps.values()):
+ c = list(comps.values())[0]
+ if c.version.startswith('19'):
+ self.platform_toolset = 'Intel C++ Compiler 19.0'
+ else:
+ # We don't have support for versions older than 2019 right now.
+ raise MesonException('There is currently no support for ICL before 19, patches welcome.')
+ if self.platform_toolset is None:
+ self.platform_toolset = 'v120'
diff --git a/mesonbuild/backend/vs2015backend.py b/mesonbuild/backend/vs2015backend.py
new file mode 100644
index 0000000..25e0a5e
--- /dev/null
+++ b/mesonbuild/backend/vs2015backend.py
@@ -0,0 +1,43 @@
+# Copyright 2014-2016 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.
+from __future__ import annotations
+
+import typing as T
+
+from .vs2010backend import Vs2010Backend
+from ..mesonlib import MesonException
+
+if T.TYPE_CHECKING:
+ from ..build import Build
+ from ..interpreter import Interpreter
+
+class Vs2015Backend(Vs2010Backend):
+ def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]):
+ super().__init__(build, interpreter)
+ self.name = 'vs2015'
+ self.vs_version = '2015'
+ self.sln_file_version = '12.00'
+ self.sln_version_comment = '14'
+ if self.environment is not None:
+ # TODO: we assume host == build
+ comps = self.environment.coredata.compilers.host
+ if comps and all(c.id == 'intel-cl' for c in comps.values()):
+ c = list(comps.values())[0]
+ if c.version.startswith('19'):
+ self.platform_toolset = 'Intel C++ Compiler 19.0'
+ else:
+ # We don't have support for versions older than 2019 right now.
+ raise MesonException('There is currently no support for ICL before 19, patches welcome.')
+ if self.platform_toolset is None:
+ self.platform_toolset = 'v140'
diff --git a/mesonbuild/backend/vs2017backend.py b/mesonbuild/backend/vs2017backend.py
new file mode 100644
index 0000000..4ed5e48
--- /dev/null
+++ b/mesonbuild/backend/vs2017backend.py
@@ -0,0 +1,67 @@
+# Copyright 2014-2016 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.
+from __future__ import annotations
+
+import os
+import typing as T
+import xml.etree.ElementTree as ET
+
+from .vs2010backend import Vs2010Backend
+from ..mesonlib import MesonException
+
+if T.TYPE_CHECKING:
+ from ..build import Build
+ from ..interpreter import Interpreter
+
+
+class Vs2017Backend(Vs2010Backend):
+ def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]):
+ super().__init__(build, interpreter)
+ self.name = 'vs2017'
+ self.vs_version = '2017'
+ self.sln_file_version = '12.00'
+ self.sln_version_comment = '15'
+ # We assume that host == build
+ if self.environment is not None:
+ comps = self.environment.coredata.compilers.host
+ if comps:
+ if comps and all(c.id == 'clang-cl' for c in comps.values()):
+ self.platform_toolset = 'llvm'
+ elif comps and all(c.id == 'intel-cl' for c in comps.values()):
+ c = list(comps.values())[0]
+ if c.version.startswith('19'):
+ self.platform_toolset = 'Intel C++ Compiler 19.0'
+ else:
+ # We don't have support for versions older than 2019 right now.
+ raise MesonException('There is currently no support for ICL before 19, patches welcome.')
+ if self.platform_toolset is None:
+ self.platform_toolset = 'v141'
+ # WindowsSDKVersion should be set by command prompt.
+ sdk_version = os.environ.get('WindowsSDKVersion', None)
+ if sdk_version:
+ self.windows_target_platform_version = sdk_version.rstrip('\\')
+
+ def generate_debug_information(self, link):
+ # valid values for vs2017 is 'false', 'true', 'DebugFastLink', 'DebugFull'
+ ET.SubElement(link, 'GenerateDebugInformation').text = 'DebugFull'
+
+ def generate_lang_standard_info(self, file_args, clconf):
+ if 'cpp' in file_args:
+ optargs = [x for x in file_args['cpp'] if x.startswith('/std:c++')]
+ if optargs:
+ ET.SubElement(clconf, 'LanguageStandard').text = optargs[0].replace("/std:c++", "stdcpp")
+ if 'c' in file_args:
+ optargs = [x for x in file_args['c'] if x.startswith('/std:c')]
+ if optargs:
+ ET.SubElement(clconf, 'LanguageStandard_C').text = optargs[0].replace("/std:c", "stdc")
diff --git a/mesonbuild/backend/vs2019backend.py b/mesonbuild/backend/vs2019backend.py
new file mode 100644
index 0000000..0734336
--- /dev/null
+++ b/mesonbuild/backend/vs2019backend.py
@@ -0,0 +1,62 @@
+# Copyright 2014-2019 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.
+from __future__ import annotations
+
+import os
+import typing as T
+import xml.etree.ElementTree as ET
+
+from .vs2010backend import Vs2010Backend
+
+if T.TYPE_CHECKING:
+ from ..build import Build
+ from ..interpreter import Interpreter
+
+
+class Vs2019Backend(Vs2010Backend):
+ def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]):
+ super().__init__(build, interpreter)
+ self.name = 'vs2019'
+ self.sln_file_version = '12.00'
+ self.sln_version_comment = 'Version 16'
+ if self.environment is not None:
+ comps = self.environment.coredata.compilers.host
+ if comps and all(c.id == 'clang-cl' for c in comps.values()):
+ self.platform_toolset = 'ClangCL'
+ elif comps and all(c.id == 'intel-cl' for c in comps.values()):
+ c = list(comps.values())[0]
+ if c.version.startswith('19'):
+ self.platform_toolset = 'Intel C++ Compiler 19.0'
+ # We don't have support for versions older than 2019 right now.
+ if not self.platform_toolset:
+ self.platform_toolset = 'v142'
+ self.vs_version = '2019'
+ # WindowsSDKVersion should be set by command prompt.
+ sdk_version = os.environ.get('WindowsSDKVersion', None)
+ if sdk_version:
+ self.windows_target_platform_version = sdk_version.rstrip('\\')
+
+ def generate_debug_information(self, link):
+ # valid values for vs2019 is 'false', 'true', 'DebugFastLink', 'DebugFull'
+ ET.SubElement(link, 'GenerateDebugInformation').text = 'DebugFull'
+
+ def generate_lang_standard_info(self, file_args, clconf):
+ if 'cpp' in file_args:
+ optargs = [x for x in file_args['cpp'] if x.startswith('/std:c++')]
+ if optargs:
+ ET.SubElement(clconf, 'LanguageStandard').text = optargs[0].replace("/std:c++", "stdcpp")
+ if 'c' in file_args:
+ optargs = [x for x in file_args['c'] if x.startswith('/std:c')]
+ if optargs:
+ ET.SubElement(clconf, 'LanguageStandard_C').text = optargs[0].replace("/std:c", "stdc")
diff --git a/mesonbuild/backend/vs2022backend.py b/mesonbuild/backend/vs2022backend.py
new file mode 100644
index 0000000..b1f93c3
--- /dev/null
+++ b/mesonbuild/backend/vs2022backend.py
@@ -0,0 +1,62 @@
+# Copyright 2014-2021 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.
+from __future__ import annotations
+
+import os
+import typing as T
+import xml.etree.ElementTree as ET
+
+from .vs2010backend import Vs2010Backend
+
+if T.TYPE_CHECKING:
+ from ..build import Build
+ from ..interpreter import Interpreter
+
+
+class Vs2022Backend(Vs2010Backend):
+ def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]):
+ super().__init__(build, interpreter)
+ self.name = 'vs2022'
+ self.sln_file_version = '12.00'
+ self.sln_version_comment = 'Version 17'
+ if self.environment is not None:
+ comps = self.environment.coredata.compilers.host
+ if comps and all(c.id == 'clang-cl' for c in comps.values()):
+ self.platform_toolset = 'ClangCL'
+ elif comps and all(c.id == 'intel-cl' for c in comps.values()):
+ c = list(comps.values())[0]
+ if c.version.startswith('19'):
+ self.platform_toolset = 'Intel C++ Compiler 19.0'
+ # We don't have support for versions older than 2022 right now.
+ if not self.platform_toolset:
+ self.platform_toolset = 'v143'
+ self.vs_version = '2022'
+ # WindowsSDKVersion should be set by command prompt.
+ sdk_version = os.environ.get('WindowsSDKVersion', None)
+ if sdk_version:
+ self.windows_target_platform_version = sdk_version.rstrip('\\')
+
+ def generate_debug_information(self, link):
+ # valid values for vs2022 is 'false', 'true', 'DebugFastLink', 'DebugFull'
+ ET.SubElement(link, 'GenerateDebugInformation').text = 'DebugFull'
+
+ def generate_lang_standard_info(self, file_args, clconf):
+ if 'cpp' in file_args:
+ optargs = [x for x in file_args['cpp'] if x.startswith('/std:c++')]
+ if optargs:
+ ET.SubElement(clconf, 'LanguageStandard').text = optargs[0].replace("/std:c++", "stdcpp")
+ if 'c' in file_args:
+ optargs = [x for x in file_args['c'] if x.startswith('/std:c')]
+ if optargs:
+ ET.SubElement(clconf, 'LanguageStandard_C').text = optargs[0].replace("/std:c", "stdc")
diff --git a/mesonbuild/backend/xcodebackend.py b/mesonbuild/backend/xcodebackend.py
new file mode 100644
index 0000000..605ee22
--- /dev/null
+++ b/mesonbuild/backend/xcodebackend.py
@@ -0,0 +1,1719 @@
+# Copyright 2014-2021 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.
+from __future__ import annotations
+
+import uuid, os, operator
+import typing as T
+
+from . import backends
+from .. import build
+from .. import dependencies
+from .. import mesonlib
+from .. import mlog
+from ..mesonlib import MesonException, OptionKey
+
+if T.TYPE_CHECKING:
+ from ..interpreter import Interpreter
+
+INDENT = '\t'
+XCODETYPEMAP = {'c': 'sourcecode.c.c',
+ 'a': 'archive.ar',
+ 'cc': 'sourcecode.cpp.cpp',
+ 'cxx': 'sourcecode.cpp.cpp',
+ 'cpp': 'sourcecode.cpp.cpp',
+ 'c++': 'sourcecode.cpp.cpp',
+ 'm': 'sourcecode.c.objc',
+ 'mm': 'sourcecode.cpp.objcpp',
+ 'h': 'sourcecode.c.h',
+ 'hpp': 'sourcecode.cpp.h',
+ 'hxx': 'sourcecode.cpp.h',
+ 'hh': 'sourcecode.cpp.hh',
+ 'inc': 'sourcecode.c.h',
+ 'swift': 'sourcecode.swift',
+ 'dylib': 'compiled.mach-o.dylib',
+ 'o': 'compiled.mach-o.objfile',
+ 's': 'sourcecode.asm',
+ 'asm': 'sourcecode.asm',
+ }
+LANGNAMEMAP = {'c': 'C',
+ 'cpp': 'CPLUSPLUS',
+ 'objc': 'OBJC',
+ 'objcpp': 'OBJCPLUSPLUS',
+ 'swift': 'SWIFT_'
+ }
+OPT2XCODEOPT = {'plain': None,
+ '0': '0',
+ 'g': '0',
+ '1': '1',
+ '2': '2',
+ '3': '3',
+ 's': 's',
+ }
+BOOL2XCODEBOOL = {True: 'YES', False: 'NO'}
+LINKABLE_EXTENSIONS = {'.o', '.a', '.obj', '.so', '.dylib'}
+
+class FileTreeEntry:
+
+ def __init__(self):
+ self.subdirs = {}
+ self.targets = []
+
+class PbxItem:
+ def __init__(self, value, comment = ''):
+ self.value = value
+ self.comment = comment
+
+class PbxArray:
+ def __init__(self):
+ self.items = []
+
+ def add_item(self, item, comment=''):
+ if isinstance(item, PbxArrayItem):
+ self.items.append(item)
+ else:
+ self.items.append(PbxArrayItem(item, comment))
+
+ def write(self, ofile, indent_level):
+ ofile.write('(\n')
+ indent_level += 1
+ for i in self.items:
+ if i.comment:
+ ofile.write(indent_level*INDENT + f'{i.value} {i.comment},\n')
+ else:
+ ofile.write(indent_level*INDENT + f'{i.value},\n')
+ indent_level -= 1
+ ofile.write(indent_level*INDENT + ');\n')
+
+class PbxArrayItem:
+ def __init__(self, value, comment = ''):
+ self.value = value
+ if comment:
+ if '/*' in comment:
+ self.comment = comment
+ else:
+ self.comment = f'/* {comment} */'
+ else:
+ self.comment = comment
+
+class PbxComment:
+ def __init__(self, text):
+ assert isinstance(text, str)
+ assert '/*' not in text
+ self.text = f'/* {text} */'
+
+ def write(self, ofile, indent_level):
+ ofile.write(f'\n{self.text}\n')
+
+class PbxDictItem:
+ def __init__(self, key, value, comment = ''):
+ self.key = key
+ self.value = value
+ if comment:
+ if '/*' in comment:
+ self.comment = comment
+ else:
+ self.comment = f'/* {comment} */'
+ else:
+ self.comment = comment
+
+class PbxDict:
+ def __init__(self):
+ # This class is a bit weird, because we want to write PBX dicts in
+ # defined order _and_ we want to write intermediate comments also in order.
+ self.keys = set()
+ self.items = []
+
+ def add_item(self, key, value, comment=''):
+ assert key not in self.keys
+ item = PbxDictItem(key, value, comment)
+ self.keys.add(key)
+ self.items.append(item)
+
+ def has_item(self, key):
+ return key in self.keys
+
+ def add_comment(self, comment):
+ if isinstance(comment, str):
+ self.items.append(PbxComment(str))
+ else:
+ assert isinstance(comment, PbxComment)
+ self.items.append(comment)
+
+ def write(self, ofile, indent_level):
+ ofile.write('{\n')
+ indent_level += 1
+ for i in self.items:
+ if isinstance(i, PbxComment):
+ i.write(ofile, indent_level)
+ elif isinstance(i, PbxDictItem):
+ if isinstance(i.value, (str, int)):
+ if i.comment:
+ ofile.write(indent_level*INDENT + f'{i.key} = {i.value} {i.comment};\n')
+ else:
+ ofile.write(indent_level*INDENT + f'{i.key} = {i.value};\n')
+ elif isinstance(i.value, PbxDict):
+ if i.comment:
+ ofile.write(indent_level*INDENT + f'{i.key} {i.comment} = ')
+ else:
+ ofile.write(indent_level*INDENT + f'{i.key} = ')
+ i.value.write(ofile, indent_level)
+ elif isinstance(i.value, PbxArray):
+ if i.comment:
+ ofile.write(indent_level*INDENT + f'{i.key} {i.comment} = ')
+ else:
+ ofile.write(indent_level*INDENT + f'{i.key} = ')
+ i.value.write(ofile, indent_level)
+ else:
+ print(i)
+ print(i.key)
+ print(i.value)
+ raise RuntimeError('missing code')
+ else:
+ print(i)
+ raise RuntimeError('missing code2')
+
+ indent_level -= 1
+ ofile.write(indent_level*INDENT + '}')
+ if indent_level == 0:
+ ofile.write('\n')
+ else:
+ ofile.write(';\n')
+
+class XCodeBackend(backends.Backend):
+ def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]):
+ super().__init__(build, interpreter)
+ self.name = 'xcode'
+ self.project_uid = self.environment.coredata.lang_guids['default'].replace('-', '')[:24]
+ self.buildtype = self.environment.coredata.get_option(OptionKey('buildtype'))
+ self.project_conflist = self.gen_id()
+ self.maingroup_id = self.gen_id()
+ self.all_id = self.gen_id()
+ self.all_buildconf_id = self.gen_id()
+ self.buildtypes = [self.buildtype]
+ self.test_id = self.gen_id()
+ self.test_command_id = self.gen_id()
+ self.test_buildconf_id = self.gen_id()
+ self.regen_id = self.gen_id()
+ self.regen_command_id = self.gen_id()
+ self.regen_buildconf_id = self.gen_id()
+ self.regen_dependency_id = self.gen_id()
+ self.top_level_dict = PbxDict()
+ self.generator_outputs = {}
+ # In Xcode files are not accessed via their file names, but rather every one of them
+ # gets an unique id. More precisely they get one unique id per target they are used
+ # in. If you generate only one id per file and use them, compilation will work but the
+ # UI will only show the file in one target but not the others. Thus they key is
+ # a tuple containing the target and filename.
+ self.buildfile_ids = {}
+ # That is not enough, though. Each target/file combination also gets a unique id
+ # in the file reference section. Because why not. This means that a source file
+ # that is used in two targets gets a total of four unique ID numbers.
+ self.fileref_ids = {}
+
+ def write_pbxfile(self, top_level_dict, ofilename):
+ tmpname = ofilename + '.tmp'
+ with open(tmpname, 'w', encoding='utf-8') as ofile:
+ ofile.write('// !$*UTF8*$!\n')
+ top_level_dict.write(ofile, 0)
+ os.replace(tmpname, ofilename)
+
+ def gen_id(self):
+ return str(uuid.uuid4()).upper().replace('-', '')[:24]
+
+ def get_target_dir(self, target):
+ dirname = os.path.join(target.get_subdir(), self.environment.coredata.get_option(OptionKey('buildtype')))
+ #os.makedirs(os.path.join(self.environment.get_build_dir(), dirname), exist_ok=True)
+ return dirname
+
+ def get_custom_target_output_dir(self, target):
+ dirname = target.get_subdir()
+ os.makedirs(os.path.join(self.environment.get_build_dir(), dirname), exist_ok=True)
+ return dirname
+
+ def target_to_build_root(self, target):
+ if self.get_target_dir(target) == '':
+ return ''
+ directories = os.path.normpath(self.get_target_dir(target)).split(os.sep)
+ return os.sep.join(['..'] * len(directories))
+
+ def object_filename_from_source(self, target, source):
+ # Xcode has the following naming scheme:
+ # projectname.build/debug/prog@exe.build/Objects-normal/x86_64/func.o
+ project = self.build.project_name
+ buildtype = self.buildtype
+ tname = target.get_id()
+ arch = 'x86_64'
+ if isinstance(source, mesonlib.File):
+ source = source.fname
+ stem = os.path.splitext(os.path.basename(source))[0]
+ obj_path = f'{project}.build/{buildtype}/{tname}.build/Objects-normal/{arch}/{stem}.o'
+ return obj_path
+
+ def generate(self):
+ self.serialize_tests()
+ # Cache the result as the method rebuilds the array every time it is called.
+ self.build_targets = self.build.get_build_targets()
+ self.custom_targets = self.build.get_custom_targets()
+ self.generate_filemap()
+ self.generate_buildstylemap()
+ self.generate_build_phase_map()
+ self.generate_build_configuration_map()
+ self.generate_build_configurationlist_map()
+ self.generate_project_configurations_map()
+ self.generate_buildall_configurations_map()
+ self.generate_test_configurations_map()
+ self.generate_native_target_map()
+ self.generate_native_frameworks_map()
+ self.generate_custom_target_map()
+ self.generate_generator_target_map()
+ self.generate_source_phase_map()
+ self.generate_target_dependency_map()
+ self.generate_pbxdep_map()
+ self.generate_containerproxy_map()
+ self.generate_target_file_maps()
+ self.generate_build_file_maps()
+ self.proj_dir = os.path.join(self.environment.get_build_dir(), self.build.project_name + '.xcodeproj')
+ os.makedirs(self.proj_dir, exist_ok=True)
+ self.proj_file = os.path.join(self.proj_dir, 'project.pbxproj')
+ objects_dict = self.generate_prefix(self.top_level_dict)
+ objects_dict.add_comment(PbxComment('Begin PBXAggregateTarget section'))
+ self.generate_pbx_aggregate_target(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXAggregateTarget section'))
+ objects_dict.add_comment(PbxComment('Begin PBXBuildFile section'))
+ self.generate_pbx_build_file(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXBuildFile section'))
+ objects_dict.add_comment(PbxComment('Begin PBXBuildStyle section'))
+ self.generate_pbx_build_style(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXBuildStyle section'))
+ objects_dict.add_comment(PbxComment('Begin PBXContainerItemProxy section'))
+ self.generate_pbx_container_item_proxy(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXContainerItemProxy section'))
+ objects_dict.add_comment(PbxComment('Begin PBXFileReference section'))
+ self.generate_pbx_file_reference(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXFileReference section'))
+ objects_dict.add_comment(PbxComment('Begin PBXFrameworksBuildPhase section'))
+ self.generate_pbx_frameworks_buildphase(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXFrameworksBuildPhase section'))
+ objects_dict.add_comment(PbxComment('Begin PBXGroup section'))
+ self.generate_pbx_group(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXGroup section'))
+ objects_dict.add_comment(PbxComment('Begin PBXNativeTarget section'))
+ self.generate_pbx_native_target(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXNativeTarget section'))
+ objects_dict.add_comment(PbxComment('Begin PBXProject section'))
+ self.generate_pbx_project(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXProject section'))
+ objects_dict.add_comment(PbxComment('Begin PBXShellScriptBuildPhase section'))
+ self.generate_pbx_shell_build_phase(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXShellScriptBuildPhase section'))
+ objects_dict.add_comment(PbxComment('Begin PBXSourcesBuildPhase section'))
+ self.generate_pbx_sources_build_phase(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXSourcesBuildPhase section'))
+ objects_dict.add_comment(PbxComment('Begin PBXTargetDependency section'))
+ self.generate_pbx_target_dependency(objects_dict)
+ objects_dict.add_comment(PbxComment('End PBXTargetDependency section'))
+ objects_dict.add_comment(PbxComment('Begin XCBuildConfiguration section'))
+ self.generate_xc_build_configuration(objects_dict)
+ objects_dict.add_comment(PbxComment('End XCBuildConfiguration section'))
+ objects_dict.add_comment(PbxComment('Begin XCConfigurationList section'))
+ self.generate_xc_configurationList(objects_dict)
+ objects_dict.add_comment(PbxComment('End XCConfigurationList section'))
+ self.generate_suffix(self.top_level_dict)
+ self.write_pbxfile(self.top_level_dict, self.proj_file)
+ self.generate_regen_info()
+
+ def get_xcodetype(self, fname):
+ extension = fname.split('.')[-1]
+ if extension == 'C':
+ extension = 'cpp'
+ xcodetype = XCODETYPEMAP.get(extension.lower())
+ if not xcodetype:
+ xcodetype = 'sourcecode.unknown'
+ return xcodetype
+
+ def generate_filemap(self):
+ self.filemap = {} # Key is source file relative to src root.
+ self.target_filemap = {}
+ for name, t in self.build_targets.items():
+ for s in t.sources:
+ if isinstance(s, mesonlib.File):
+ s = os.path.join(s.subdir, s.fname)
+ self.filemap[s] = self.gen_id()
+ for o in t.objects:
+ if isinstance(o, str):
+ o = os.path.join(t.subdir, o)
+ self.filemap[o] = self.gen_id()
+ self.target_filemap[name] = self.gen_id()
+
+ def generate_buildstylemap(self):
+ self.buildstylemap = {self.buildtype: self.gen_id()}
+
+ def generate_build_phase_map(self):
+ for tname, t in self.build_targets.items():
+ # generate id for our own target-name
+ t.buildphasemap = {}
+ t.buildphasemap[tname] = self.gen_id()
+ # each target can have it's own Frameworks/Sources/..., generate id's for those
+ t.buildphasemap['Frameworks'] = self.gen_id()
+ t.buildphasemap['Resources'] = self.gen_id()
+ t.buildphasemap['Sources'] = self.gen_id()
+
+ def generate_build_configuration_map(self):
+ self.buildconfmap = {}
+ for t in self.build_targets:
+ bconfs = {self.buildtype: self.gen_id()}
+ self.buildconfmap[t] = bconfs
+ for t in self.custom_targets:
+ bconfs = {self.buildtype: self.gen_id()}
+ self.buildconfmap[t] = bconfs
+
+ def generate_project_configurations_map(self):
+ self.project_configurations = {self.buildtype: self.gen_id()}
+
+ def generate_buildall_configurations_map(self):
+ self.buildall_configurations = {self.buildtype: self.gen_id()}
+
+ def generate_test_configurations_map(self):
+ self.test_configurations = {self.buildtype: self.gen_id()}
+
+ def generate_build_configurationlist_map(self):
+ self.buildconflistmap = {}
+ for t in self.build_targets:
+ self.buildconflistmap[t] = self.gen_id()
+ for t in self.custom_targets:
+ self.buildconflistmap[t] = self.gen_id()
+
+ def generate_native_target_map(self):
+ self.native_targets = {}
+ for t in self.build_targets:
+ self.native_targets[t] = self.gen_id()
+
+ def generate_custom_target_map(self):
+ self.shell_targets = {}
+ self.custom_target_output_buildfile = {}
+ self.custom_target_output_fileref = {}
+ for tname, t in self.custom_targets.items():
+ self.shell_targets[tname] = self.gen_id()
+ if not isinstance(t, build.CustomTarget):
+ continue
+ (srcs, ofilenames, cmd) = self.eval_custom_target_command(t)
+ for o in ofilenames:
+ self.custom_target_output_buildfile[o] = self.gen_id()
+ self.custom_target_output_fileref[o] = self.gen_id()
+
+ def generate_generator_target_map(self):
+ # Generator objects do not have natural unique ids
+ # so use a counter.
+ self.generator_fileref_ids = {}
+ self.generator_buildfile_ids = {}
+ for tname, t in self.build_targets.items():
+ generator_id = 0
+ for genlist in t.generated:
+ if not isinstance(genlist, build.GeneratedList):
+ continue
+ self.gen_single_target_map(genlist, tname, t, generator_id)
+ generator_id += 1
+ # FIXME add outputs.
+ for tname, t in self.custom_targets.items():
+ generator_id = 0
+ for genlist in t.sources:
+ if not isinstance(genlist, build.GeneratedList):
+ continue
+ self.gen_single_target_map(genlist, tname, t, generator_id)
+ generator_id += 1
+
+ def gen_single_target_map(self, genlist, tname, t, generator_id):
+ k = (tname, generator_id)
+ assert k not in self.shell_targets
+ self.shell_targets[k] = self.gen_id()
+ ofile_abs = []
+ for i in genlist.get_inputs():
+ for o_base in genlist.get_outputs_for(i):
+ o = os.path.join(self.get_target_private_dir(t), o_base)
+ ofile_abs.append(os.path.join(self.environment.get_build_dir(), o))
+ assert k not in self.generator_outputs
+ self.generator_outputs[k] = ofile_abs
+ buildfile_ids = []
+ fileref_ids = []
+ for i in range(len(ofile_abs)):
+ buildfile_ids.append(self.gen_id())
+ fileref_ids.append(self.gen_id())
+ self.generator_buildfile_ids[k] = buildfile_ids
+ self.generator_fileref_ids[k] = fileref_ids
+
+ def generate_native_frameworks_map(self):
+ self.native_frameworks = {}
+ self.native_frameworks_fileref = {}
+ for t in self.build_targets.values():
+ for dep in t.get_external_deps():
+ if isinstance(dep, dependencies.AppleFrameworks):
+ for f in dep.frameworks:
+ self.native_frameworks[f] = self.gen_id()
+ self.native_frameworks_fileref[f] = self.gen_id()
+
+ def generate_target_dependency_map(self):
+ self.target_dependency_map = {}
+ for tname, t in self.build_targets.items():
+ for target in t.link_targets:
+ if isinstance(target, build.CustomTargetIndex):
+ k = (tname, target.target.get_basename())
+ if k in self.target_dependency_map:
+ continue
+ else:
+ k = (tname, target.get_basename())
+ assert k not in self.target_dependency_map
+ self.target_dependency_map[k] = self.gen_id()
+ for tname, t in self.custom_targets.items():
+ k = tname
+ assert k not in self.target_dependency_map
+ self.target_dependency_map[k] = self.gen_id()
+
+ def generate_pbxdep_map(self):
+ self.pbx_dep_map = {}
+ self.pbx_custom_dep_map = {}
+ for t in self.build_targets:
+ self.pbx_dep_map[t] = self.gen_id()
+ for t in self.custom_targets:
+ self.pbx_custom_dep_map[t] = self.gen_id()
+
+ def generate_containerproxy_map(self):
+ self.containerproxy_map = {}
+ for t in self.build_targets:
+ self.containerproxy_map[t] = self.gen_id()
+
+ def generate_target_file_maps(self):
+ self.generate_target_file_maps_impl(self.build_targets)
+ self.generate_target_file_maps_impl(self.custom_targets)
+
+ def generate_target_file_maps_impl(self, targets):
+ for tname, t in targets.items():
+ for s in t.sources:
+ if isinstance(s, mesonlib.File):
+ s = os.path.join(s.subdir, s.fname)
+ if not isinstance(s, str):
+ continue
+ k = (tname, s)
+ assert k not in self.buildfile_ids
+ self.buildfile_ids[k] = self.gen_id()
+ assert k not in self.fileref_ids
+ self.fileref_ids[k] = self.gen_id()
+ if not hasattr(t, 'objects'):
+ continue
+ for o in t.objects:
+ if isinstance(o, build.ExtractedObjects):
+ # Extracted objects do not live in "the Xcode world".
+ continue
+ if isinstance(o, mesonlib.File):
+ o = os.path.join(o.subdir, o.fname)
+ if isinstance(o, str):
+ o = os.path.join(t.subdir, o)
+ k = (tname, o)
+ assert k not in self.buildfile_ids
+ self.buildfile_ids[k] = self.gen_id()
+ assert k not in self.fileref_ids
+ self.fileref_ids[k] = self.gen_id()
+ else:
+ raise RuntimeError('Unknown input type ' + str(o))
+
+ def generate_build_file_maps(self):
+ for buildfile in self.interpreter.get_build_def_files():
+ assert isinstance(buildfile, str)
+ self.buildfile_ids[buildfile] = self.gen_id()
+ self.fileref_ids[buildfile] = self.gen_id()
+
+ def generate_source_phase_map(self):
+ self.source_phase = {}
+ for t in self.build_targets:
+ self.source_phase[t] = self.gen_id()
+
+ def generate_pbx_aggregate_target(self, objects_dict):
+ self.custom_aggregate_targets = {}
+ self.build_all_tdep_id = self.gen_id()
+ # FIXME: filter out targets that are not built by default.
+ target_dependencies = [self.pbx_dep_map[t] for t in self.build_targets]
+ custom_target_dependencies = [self.pbx_custom_dep_map[t] for t in self.custom_targets]
+ aggregated_targets = []
+ aggregated_targets.append((self.all_id, 'ALL_BUILD',
+ self.all_buildconf_id,
+ [],
+ [self.regen_dependency_id] + target_dependencies + custom_target_dependencies))
+ aggregated_targets.append((self.test_id,
+ 'RUN_TESTS',
+ self.test_buildconf_id,
+ [self.test_command_id],
+ [self.regen_dependency_id, self.build_all_tdep_id]))
+ aggregated_targets.append((self.regen_id,
+ 'REGENERATE',
+ self.regen_buildconf_id,
+ [self.regen_command_id],
+ []))
+ for tname, t in self.build.get_custom_targets().items():
+ ct_id = self.gen_id()
+ self.custom_aggregate_targets[tname] = ct_id
+ build_phases = []
+ dependencies = [self.regen_dependency_id]
+ generator_id = 0
+ for s in t.sources:
+ if not isinstance(s, build.GeneratedList):
+ continue
+ build_phases.append(self.shell_targets[(tname, generator_id)])
+ for d in s.depends:
+ dependencies.append(self.pbx_custom_dep_map[d.get_id()])
+ generator_id += 1
+ build_phases.append(self.shell_targets[tname])
+ aggregated_targets.append((ct_id, tname, self.buildconflistmap[tname], build_phases, dependencies))
+
+ # Sort objects by ID before writing
+ sorted_aggregated_targets = sorted(aggregated_targets, key=operator.itemgetter(0))
+ for t in sorted_aggregated_targets:
+ agt_dict = PbxDict()
+ name = t[1]
+ buildconf_id = t[2]
+ build_phases = t[3]
+ dependencies = t[4]
+ agt_dict.add_item('isa', 'PBXAggregateTarget')
+ agt_dict.add_item('buildConfigurationList', buildconf_id, f'Build configuration list for PBXAggregateTarget "{name}"')
+ bp_arr = PbxArray()
+ agt_dict.add_item('buildPhases', bp_arr)
+ for bp in build_phases:
+ bp_arr.add_item(bp, 'ShellScript')
+ dep_arr = PbxArray()
+ agt_dict.add_item('dependencies', dep_arr)
+ for td in dependencies:
+ dep_arr.add_item(td, 'PBXTargetDependency')
+ agt_dict.add_item('name', f'"{name}"')
+ agt_dict.add_item('productName', f'"{name}"')
+ objects_dict.add_item(t[0], agt_dict, name)
+
+ def generate_pbx_build_file(self, objects_dict):
+ for tname, t in self.build_targets.items():
+ for dep in t.get_external_deps():
+ if isinstance(dep, dependencies.AppleFrameworks):
+ for f in dep.frameworks:
+ fw_dict = PbxDict()
+ fwkey = self.native_frameworks[f]
+ if fwkey not in objects_dict.keys:
+ objects_dict.add_item(fwkey, fw_dict, f'{f}.framework in Frameworks')
+ fw_dict.add_item('isa', 'PBXBuildFile')
+ fw_dict.add_item('fileRef', self.native_frameworks_fileref[f], f)
+
+ for s in t.sources:
+ in_build_dir = False
+ if isinstance(s, mesonlib.File):
+ if s.is_built:
+ in_build_dir = True
+ s = os.path.join(s.subdir, s.fname)
+
+ if not isinstance(s, str):
+ continue
+ sdict = PbxDict()
+ k = (tname, s)
+ idval = self.buildfile_ids[k]
+ fileref = self.fileref_ids[k]
+ if in_build_dir:
+ fullpath = os.path.join(self.environment.get_build_dir(), s)
+ else:
+ fullpath = os.path.join(self.environment.get_source_dir(), s)
+ sdict.add_item('isa', 'PBXBuildFile')
+ sdict.add_item('fileRef', fileref, fullpath)
+ objects_dict.add_item(idval, sdict)
+
+ for o in t.objects:
+ if isinstance(o, build.ExtractedObjects):
+ # Object files are not source files as such. We add them
+ # by hand in linker flags. It is also not particularly
+ # clear how to define build files in Xcode's file format.
+ continue
+ if isinstance(o, mesonlib.File):
+ o = os.path.join(o.subdir, o.fname)
+ elif isinstance(o, str):
+ o = os.path.join(t.subdir, o)
+ idval = self.buildfile_ids[(tname, o)]
+ k = (tname, o)
+ fileref = self.fileref_ids[k]
+ assert o not in self.filemap
+ self.filemap[o] = idval
+ fullpath = os.path.join(self.environment.get_source_dir(), o)
+ fullpath2 = fullpath
+ o_dict = PbxDict()
+ objects_dict.add_item(idval, o_dict, fullpath)
+ o_dict.add_item('isa', 'PBXBuildFile')
+ o_dict.add_item('fileRef', fileref, fullpath2)
+
+ generator_id = 0
+ for g in t.generated:
+ if not isinstance(g, build.GeneratedList):
+ continue
+ self.create_generator_shellphase(objects_dict, tname, generator_id)
+ generator_id += 1
+
+ # Custom targets are shell build phases in Xcode terminology.
+ for tname, t in self.custom_targets.items():
+ if not isinstance(t, build.CustomTarget):
+ continue
+ (srcs, ofilenames, cmd) = self.eval_custom_target_command(t)
+ for o in ofilenames:
+ custom_dict = PbxDict()
+ objects_dict.add_item(self.custom_target_output_buildfile[o], custom_dict, f'/* {o} */')
+ custom_dict.add_item('isa', 'PBXBuildFile')
+ custom_dict.add_item('fileRef', self.custom_target_output_fileref[o])
+ generator_id = 0
+ for g in t.sources:
+ if not isinstance(g, build.GeneratedList):
+ continue
+ self.create_generator_shellphase(objects_dict, tname, generator_id)
+ generator_id += 1
+
+ def create_generator_shellphase(self, objects_dict, tname, generator_id):
+ file_ids = self.generator_buildfile_ids[(tname, generator_id)]
+ ref_ids = self.generator_fileref_ids[(tname, generator_id)]
+ assert len(ref_ids) == len(file_ids)
+ for file_o, ref_id in zip(file_ids, ref_ids):
+ odict = PbxDict()
+ objects_dict.add_item(file_o, odict)
+ odict.add_item('isa', 'PBXBuildFile')
+ odict.add_item('fileRef', ref_id)
+
+ def generate_pbx_build_style(self, objects_dict):
+ # FIXME: Xcode 9 and later does not uses PBXBuildStyle and it gets removed. Maybe we can remove this part.
+ for name, idval in self.buildstylemap.items():
+ styledict = PbxDict()
+ objects_dict.add_item(idval, styledict, name)
+ styledict.add_item('isa', 'PBXBuildStyle')
+ settings_dict = PbxDict()
+ styledict.add_item('buildSettings', settings_dict)
+ settings_dict.add_item('COPY_PHASE_STRIP', 'NO')
+ styledict.add_item('name', f'"{name}"')
+
+ def generate_pbx_container_item_proxy(self, objects_dict):
+ for t in self.build_targets:
+ proxy_dict = PbxDict()
+ objects_dict.add_item(self.containerproxy_map[t], proxy_dict, 'PBXContainerItemProxy')
+ proxy_dict.add_item('isa', 'PBXContainerItemProxy')
+ proxy_dict.add_item('containerPortal', self.project_uid, 'Project object')
+ proxy_dict.add_item('proxyType', '1')
+ proxy_dict.add_item('remoteGlobalIDString', self.native_targets[t])
+ proxy_dict.add_item('remoteInfo', '"' + t + '"')
+
+ def generate_pbx_file_reference(self, objects_dict):
+ for tname, t in self.build_targets.items():
+ for dep in t.get_external_deps():
+ if isinstance(dep, dependencies.AppleFrameworks):
+ for f in dep.frameworks:
+ fw_dict = PbxDict()
+ framework_fileref = self.native_frameworks_fileref[f]
+ if objects_dict.has_item(framework_fileref):
+ continue
+ objects_dict.add_item(framework_fileref, fw_dict, f)
+ fw_dict.add_item('isa', 'PBXFileReference')
+ fw_dict.add_item('lastKnownFileType', 'wrapper.framework')
+ fw_dict.add_item('name', f'{f}.framework')
+ fw_dict.add_item('path', f'System/Library/Frameworks/{f}.framework')
+ fw_dict.add_item('sourceTree', 'SDKROOT')
+ for s in t.sources:
+ in_build_dir = False
+ if isinstance(s, mesonlib.File):
+ if s.is_built:
+ in_build_dir = True
+ s = os.path.join(s.subdir, s.fname)
+ if not isinstance(s, str):
+ continue
+ idval = self.fileref_ids[(tname, s)]
+ fullpath = os.path.join(self.environment.get_source_dir(), s)
+ src_dict = PbxDict()
+ xcodetype = self.get_xcodetype(s)
+ name = os.path.basename(s)
+ path = s
+ objects_dict.add_item(idval, src_dict, fullpath)
+ src_dict.add_item('isa', 'PBXFileReference')
+ src_dict.add_item('explicitFileType', '"' + xcodetype + '"')
+ src_dict.add_item('fileEncoding', '4')
+ if in_build_dir:
+ src_dict.add_item('name', '"' + name + '"')
+ # This makes no sense. This should say path instead of name
+ # but then the path gets added twice.
+ src_dict.add_item('path', '"' + name + '"')
+ src_dict.add_item('sourceTree', 'BUILD_ROOT')
+ else:
+ src_dict.add_item('name', '"' + name + '"')
+ src_dict.add_item('path', '"' + path + '"')
+ src_dict.add_item('sourceTree', 'SOURCE_ROOT')
+
+ generator_id = 0
+ for g in t.generated:
+ if not isinstance(g, build.GeneratedList):
+ continue
+ outputs = self.generator_outputs[(tname, generator_id)]
+ ref_ids = self.generator_fileref_ids[tname, generator_id]
+ assert len(ref_ids) == len(outputs)
+ for o, ref_id in zip(outputs, ref_ids):
+ odict = PbxDict()
+ name = os.path.basename(o)
+ objects_dict.add_item(ref_id, odict, o)
+ xcodetype = self.get_xcodetype(o)
+ rel_name = mesonlib.relpath(o, self.environment.get_source_dir())
+ odict.add_item('isa', 'PBXFileReference')
+ odict.add_item('explicitFileType', '"' + xcodetype + '"')
+ odict.add_item('fileEncoding', '4')
+ odict.add_item('name', f'"{name}"')
+ odict.add_item('path', f'"{rel_name}"')
+ odict.add_item('sourceTree', 'SOURCE_ROOT')
+
+ generator_id += 1
+
+ for o in t.objects:
+ if isinstance(o, build.ExtractedObjects):
+ # Same as with pbxbuildfile.
+ continue
+ if isinstance(o, mesonlib.File):
+ fullpath = o.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir())
+ o = os.path.join(o.subdir, o.fname)
+ else:
+ o = os.path.join(t.subdir, o)
+ fullpath = os.path.join(self.environment.get_source_dir(), o)
+ idval = self.fileref_ids[(tname, o)]
+ rel_name = mesonlib.relpath(fullpath, self.environment.get_source_dir())
+ o_dict = PbxDict()
+ name = os.path.basename(o)
+ objects_dict.add_item(idval, o_dict, fullpath)
+ o_dict.add_item('isa', 'PBXFileReference')
+ o_dict.add_item('explicitFileType', '"' + self.get_xcodetype(o) + '"')
+ o_dict.add_item('fileEncoding', '4')
+ o_dict.add_item('name', f'"{name}"')
+ o_dict.add_item('path', f'"{rel_name}"')
+ o_dict.add_item('sourceTree', 'SOURCE_ROOT')
+ for tname, idval in self.target_filemap.items():
+ target_dict = PbxDict()
+ objects_dict.add_item(idval, target_dict, tname)
+ t = self.build_targets[tname]
+ fname = t.get_filename()
+ reftype = 0
+ if isinstance(t, build.Executable):
+ typestr = 'compiled.mach-o.executable'
+ path = fname
+ elif isinstance(t, build.SharedLibrary):
+ typestr = self.get_xcodetype('dummy.dylib')
+ path = fname
+ else:
+ typestr = self.get_xcodetype(fname)
+ path = '"%s"' % t.get_filename()
+ target_dict.add_item('isa', 'PBXFileReference')
+ target_dict.add_item('explicitFileType', '"' + typestr + '"')
+ if ' ' in path and path[0] != '"':
+ target_dict.add_item('path', f'"{path}"')
+ else:
+ target_dict.add_item('path', path)
+ target_dict.add_item('refType', reftype)
+ target_dict.add_item('sourceTree', 'BUILT_PRODUCTS_DIR')
+
+ for tname, t in self.custom_targets.items():
+ if not isinstance(t, build.CustomTarget):
+ continue
+ (srcs, ofilenames, cmd) = self.eval_custom_target_command(t)
+ for s in t.sources:
+ if isinstance(s, mesonlib.File):
+ s = os.path.join(s.subdir, s.fname)
+ elif isinstance(s, str):
+ s = os.path.join(t.subdir, s)
+ else:
+ continue
+ custom_dict = PbxDict()
+ typestr = self.get_xcodetype(s)
+ custom_dict.add_item('isa', 'PBXFileReference')
+ custom_dict.add_item('explicitFileType', '"' + typestr + '"')
+ custom_dict.add_item('name', f'"{s}"')
+ custom_dict.add_item('path', f'"{s}"')
+ custom_dict.add_item('refType', 0)
+ custom_dict.add_item('sourceTree', 'SOURCE_ROOT')
+ objects_dict.add_item(self.fileref_ids[(tname, s)], custom_dict)
+ for o in ofilenames:
+ custom_dict = PbxDict()
+ typestr = self.get_xcodetype(o)
+ custom_dict.add_item('isa', 'PBXFileReference')
+ custom_dict.add_item('explicitFileType', '"' + typestr + '"')
+ custom_dict.add_item('name', o)
+ custom_dict.add_item('path', os.path.join(self.src_to_build, o))
+ custom_dict.add_item('refType', 0)
+ custom_dict.add_item('sourceTree', 'SOURCE_ROOT')
+ objects_dict.add_item(self.custom_target_output_fileref[o], custom_dict)
+
+ for buildfile in self.interpreter.get_build_def_files():
+ basename = os.path.split(buildfile)[1]
+ buildfile_dict = PbxDict()
+ typestr = self.get_xcodetype(buildfile)
+ buildfile_dict.add_item('isa', 'PBXFileReference')
+ buildfile_dict.add_item('explicitFileType', '"' + typestr + '"')
+ buildfile_dict.add_item('name', f'"{basename}"')
+ buildfile_dict.add_item('path', f'"{buildfile}"')
+ buildfile_dict.add_item('refType', 0)
+ buildfile_dict.add_item('sourceTree', 'SOURCE_ROOT')
+ objects_dict.add_item(self.fileref_ids[buildfile], buildfile_dict)
+
+ def generate_pbx_frameworks_buildphase(self, objects_dict):
+ for t in self.build_targets.values():
+ bt_dict = PbxDict()
+ objects_dict.add_item(t.buildphasemap['Frameworks'], bt_dict, 'Frameworks')
+ bt_dict.add_item('isa', 'PBXFrameworksBuildPhase')
+ bt_dict.add_item('buildActionMask', 2147483647)
+ file_list = PbxArray()
+ bt_dict.add_item('files', file_list)
+ for dep in t.get_external_deps():
+ if isinstance(dep, dependencies.AppleFrameworks):
+ for f in dep.frameworks:
+ file_list.add_item(self.native_frameworks[f], f'{f}.framework in Frameworks')
+ bt_dict.add_item('runOnlyForDeploymentPostprocessing', 0)
+
+ def generate_pbx_group(self, objects_dict):
+ groupmap = {}
+ target_src_map = {}
+ for t in self.build_targets:
+ groupmap[t] = self.gen_id()
+ target_src_map[t] = self.gen_id()
+ for t in self.custom_targets:
+ groupmap[t] = self.gen_id()
+ target_src_map[t] = self.gen_id()
+ projecttree_id = self.gen_id()
+ resources_id = self.gen_id()
+ products_id = self.gen_id()
+ frameworks_id = self.gen_id()
+ main_dict = PbxDict()
+ objects_dict.add_item(self.maingroup_id, main_dict)
+ main_dict.add_item('isa', 'PBXGroup')
+ main_children = PbxArray()
+ main_dict.add_item('children', main_children)
+ main_children.add_item(projecttree_id, 'Project tree')
+ main_children.add_item(resources_id, 'Resources')
+ main_children.add_item(products_id, 'Products')
+ main_children.add_item(frameworks_id, 'Frameworks')
+ main_dict.add_item('sourceTree', '"<group>"')
+
+ self.add_projecttree(objects_dict, projecttree_id)
+
+ resource_dict = PbxDict()
+ objects_dict.add_item(resources_id, resource_dict, 'Resources')
+ resource_dict.add_item('isa', 'PBXGroup')
+ resource_children = PbxArray()
+ resource_dict.add_item('children', resource_children)
+ resource_dict.add_item('name', 'Resources')
+ resource_dict.add_item('sourceTree', '"<group>"')
+
+ frameworks_dict = PbxDict()
+ objects_dict.add_item(frameworks_id, frameworks_dict, 'Frameworks')
+ frameworks_dict.add_item('isa', 'PBXGroup')
+ frameworks_children = PbxArray()
+ frameworks_dict.add_item('children', frameworks_children)
+ # write frameworks
+
+ for t in self.build_targets.values():
+ for dep in t.get_external_deps():
+ if isinstance(dep, dependencies.AppleFrameworks):
+ for f in dep.frameworks:
+ frameworks_children.add_item(self.native_frameworks_fileref[f], f)
+
+ frameworks_dict.add_item('name', 'Frameworks')
+ frameworks_dict.add_item('sourceTree', '"<group>"')
+
+ for tname, t in self.custom_targets.items():
+ target_dict = PbxDict()
+ objects_dict.add_item(groupmap[tname], target_dict, tname)
+ target_dict.add_item('isa', 'PBXGroup')
+ target_children = PbxArray()
+ target_dict.add_item('children', target_children)
+ target_children.add_item(target_src_map[tname], 'Source files')
+ if t.subproject:
+ target_dict.add_item('name', f'"{t.subproject} • {t.name}"')
+ else:
+ target_dict.add_item('name', f'"{t.name}"')
+ target_dict.add_item('sourceTree', '"<group>"')
+ source_files_dict = PbxDict()
+ objects_dict.add_item(target_src_map[tname], source_files_dict, 'Source files')
+ source_files_dict.add_item('isa', 'PBXGroup')
+ source_file_children = PbxArray()
+ source_files_dict.add_item('children', source_file_children)
+ for s in t.sources:
+ if isinstance(s, mesonlib.File):
+ s = os.path.join(s.subdir, s.fname)
+ elif isinstance(s, str):
+ s = os.path.join(t.subdir, s)
+ else:
+ continue
+ source_file_children.add_item(self.fileref_ids[(tname, s)], s)
+ source_files_dict.add_item('name', '"Source files"')
+ source_files_dict.add_item('sourceTree', '"<group>"')
+
+ # And finally products
+ product_dict = PbxDict()
+ objects_dict.add_item(products_id, product_dict, 'Products')
+ product_dict.add_item('isa', 'PBXGroup')
+ product_children = PbxArray()
+ product_dict.add_item('children', product_children)
+ for t in self.build_targets:
+ product_children.add_item(self.target_filemap[t], t)
+ product_dict.add_item('name', 'Products')
+ product_dict.add_item('sourceTree', '"<group>"')
+
+ def write_group_target_entry(self, objects_dict, t):
+ tid = t.get_id()
+ group_id = self.gen_id()
+ target_dict = PbxDict()
+ objects_dict.add_item(group_id, target_dict, tid)
+ target_dict.add_item('isa', 'PBXGroup')
+ target_children = PbxArray()
+ target_dict.add_item('children', target_children)
+ target_dict.add_item('name', f'"{t} · target"')
+ target_dict.add_item('sourceTree', '"<group>"')
+ source_files_dict = PbxDict()
+ for s in t.sources:
+ if isinstance(s, mesonlib.File):
+ s = os.path.join(s.subdir, s.fname)
+ elif isinstance(s, str):
+ s = os.path.join(t.subdir, s)
+ else:
+ continue
+ target_children.add_item(self.fileref_ids[(tid, s)], s)
+ for o in t.objects:
+ if isinstance(o, build.ExtractedObjects):
+ # Do not show built object files in the project tree.
+ continue
+ if isinstance(o, mesonlib.File):
+ o = os.path.join(o.subdir, o.fname)
+ else:
+ o = os.path.join(t.subdir, o)
+ target_children.add_item(self.fileref_ids[(tid, o)], o)
+ source_files_dict.add_item('name', '"Source files"')
+ source_files_dict.add_item('sourceTree', '"<group>"')
+ return group_id
+
+ def add_projecttree(self, objects_dict, projecttree_id):
+ root_dict = PbxDict()
+ objects_dict.add_item(projecttree_id, root_dict, "Root of project tree")
+ root_dict.add_item('isa', 'PBXGroup')
+ target_children = PbxArray()
+ root_dict.add_item('children', target_children)
+ root_dict.add_item('name', '"Project root"')
+ root_dict.add_item('sourceTree', '"<group>"')
+
+ project_tree = self.generate_project_tree()
+ self.write_tree(objects_dict, project_tree, target_children, '')
+
+ def write_tree(self, objects_dict, tree_node, children_array, current_subdir):
+ for subdir_name, subdir_node in tree_node.subdirs.items():
+ subdir_dict = PbxDict()
+ subdir_children = PbxArray()
+ subdir_id = self.gen_id()
+ objects_dict.add_item(subdir_id, subdir_dict)
+ children_array.add_item(subdir_id)
+ subdir_dict.add_item('isa', 'PBXGroup')
+ subdir_dict.add_item('children', subdir_children)
+ subdir_dict.add_item('name', f'"{subdir_name}"')
+ subdir_dict.add_item('sourceTree', '"<group>"')
+ self.write_tree(objects_dict, subdir_node, subdir_children, os.path.join(current_subdir, subdir_name))
+ for target in tree_node.targets:
+ group_id = self.write_group_target_entry(objects_dict, target)
+ children_array.add_item(group_id)
+ potentials = [os.path.join(current_subdir, 'meson.build'),
+ os.path.join(current_subdir, 'meson_options.txt')]
+ for bf in potentials:
+ i = self.fileref_ids.get(bf, None)
+ if i:
+ children_array.add_item(i)
+
+ def generate_project_tree(self):
+ tree_info = FileTreeEntry()
+ for tname, t in self.build_targets.items():
+ self.add_target_to_tree(tree_info, t)
+ return tree_info
+
+ def add_target_to_tree(self, tree_root, t):
+ current_node = tree_root
+ path_segments = t.subdir.split('/')
+ for s in path_segments:
+ if not s:
+ continue
+ if s not in current_node.subdirs:
+ current_node.subdirs[s] = FileTreeEntry()
+ current_node = current_node.subdirs[s]
+ current_node.targets.append(t)
+
+ def generate_pbx_native_target(self, objects_dict):
+ for tname, idval in self.native_targets.items():
+ ntarget_dict = PbxDict()
+ t = self.build_targets[tname]
+ objects_dict.add_item(idval, ntarget_dict, tname)
+ ntarget_dict.add_item('isa', 'PBXNativeTarget')
+ ntarget_dict.add_item('buildConfigurationList', self.buildconflistmap[tname], f'Build configuration list for PBXNativeTarget "{tname}"')
+ buildphases_array = PbxArray()
+ ntarget_dict.add_item('buildPhases', buildphases_array)
+ generator_id = 0
+ for g in t.generated:
+ # Custom target are handled via inter-target dependencies.
+ # Generators are built as a shellscriptbuildphase.
+ if isinstance(g, build.GeneratedList):
+ buildphases_array.add_item(self.shell_targets[(tname, generator_id)], f'Generator {generator_id}/{tname}')
+ generator_id += 1
+ for bpname, bpval in t.buildphasemap.items():
+ buildphases_array.add_item(bpval, f'{bpname} yyy')
+ ntarget_dict.add_item('buildRules', PbxArray())
+ dep_array = PbxArray()
+ ntarget_dict.add_item('dependencies', dep_array)
+ dep_array.add_item(self.regen_dependency_id)
+ # These dependencies only tell Xcode that the deps must be built
+ # before this one. They don't set up linkage or anything
+ # like that. Those are set up in the XCBuildConfiguration.
+ for lt in self.build_targets[tname].link_targets:
+ # NOT DOCUMENTED, may need to make different links
+ # to same target have different targetdependency item.
+ if isinstance(lt, build.CustomTarget):
+ dep_array.add_item(self.pbx_custom_dep_map[lt.get_id()], lt.name)
+ elif isinstance(lt, build.CustomTargetIndex):
+ dep_array.add_item(self.pbx_custom_dep_map[lt.target.get_id()], lt.target.name)
+ else:
+ idval = self.pbx_dep_map[lt.get_id()]
+ dep_array.add_item(idval, 'PBXTargetDependency')
+ for o in t.objects:
+ if isinstance(o, build.ExtractedObjects):
+ source_target_id = o.target.get_id()
+ idval = self.pbx_dep_map[source_target_id]
+ dep_array.add_item(idval, 'PBXTargetDependency')
+ generator_id = 0
+ for o in t.generated:
+ if isinstance(o, build.CustomTarget):
+ dep_array.add_item(self.pbx_custom_dep_map[o.get_id()], o.name)
+ elif isinstance(o, build.CustomTargetIndex):
+ dep_array.add_item(self.pbx_custom_dep_map[o.target.get_id()], o.target.name)
+
+ generator_id += 1
+
+ ntarget_dict.add_item('name', f'"{tname}"')
+ ntarget_dict.add_item('productName', f'"{tname}"')
+ ntarget_dict.add_item('productReference', self.target_filemap[tname], tname)
+ if isinstance(t, build.Executable):
+ typestr = 'com.apple.product-type.tool'
+ elif isinstance(t, build.StaticLibrary):
+ typestr = 'com.apple.product-type.library.static'
+ elif isinstance(t, build.SharedLibrary):
+ typestr = 'com.apple.product-type.library.dynamic'
+ else:
+ raise MesonException('Unknown target type for %s' % tname)
+ ntarget_dict.add_item('productType', f'"{typestr}"')
+
+ def generate_pbx_project(self, objects_dict):
+ project_dict = PbxDict()
+ objects_dict.add_item(self.project_uid, project_dict, 'Project object')
+ project_dict.add_item('isa', 'PBXProject')
+ attr_dict = PbxDict()
+ project_dict.add_item('attributes', attr_dict)
+ attr_dict.add_item('BuildIndependentTargetsInParallel', 'YES')
+ project_dict.add_item('buildConfigurationList', self.project_conflist, f'Build configuration list for PBXProject "{self.build.project_name}"')
+ project_dict.add_item('buildSettings', PbxDict())
+ style_arr = PbxArray()
+ project_dict.add_item('buildStyles', style_arr)
+ for name, idval in self.buildstylemap.items():
+ style_arr.add_item(idval, name)
+ project_dict.add_item('compatibilityVersion', '"Xcode 3.2"')
+ project_dict.add_item('hasScannedForEncodings', 0)
+ project_dict.add_item('mainGroup', self.maingroup_id)
+ project_dict.add_item('projectDirPath', '"' + self.environment.get_source_dir() + '"')
+ project_dict.add_item('projectRoot', '""')
+ targets_arr = PbxArray()
+ project_dict.add_item('targets', targets_arr)
+ targets_arr.add_item(self.all_id, 'ALL_BUILD')
+ targets_arr.add_item(self.test_id, 'RUN_TESTS')
+ targets_arr.add_item(self.regen_id, 'REGENERATE')
+ for t in self.build_targets:
+ targets_arr.add_item(self.native_targets[t], t)
+ for t in self.custom_targets:
+ targets_arr.add_item(self.custom_aggregate_targets[t], t)
+
+ def generate_pbx_shell_build_phase(self, objects_dict):
+ self.generate_test_shell_build_phase(objects_dict)
+ self.generate_regen_shell_build_phase(objects_dict)
+ self.generate_custom_target_shell_build_phases(objects_dict)
+ self.generate_generator_target_shell_build_phases(objects_dict)
+
+ def generate_test_shell_build_phase(self, objects_dict):
+ shell_dict = PbxDict()
+ objects_dict.add_item(self.test_command_id, shell_dict, 'ShellScript')
+ shell_dict.add_item('isa', 'PBXShellScriptBuildPhase')
+ shell_dict.add_item('buildActionMask', 2147483647)
+ shell_dict.add_item('files', PbxArray())
+ shell_dict.add_item('inputPaths', PbxArray())
+ shell_dict.add_item('outputPaths', PbxArray())
+ shell_dict.add_item('runOnlyForDeploymentPostprocessing', 0)
+ shell_dict.add_item('shellPath', '/bin/sh')
+ cmd = mesonlib.get_meson_command() + ['test', '--no-rebuild', '-C', self.environment.get_build_dir()]
+ cmdstr = ' '.join(["'%s'" % i for i in cmd])
+ shell_dict.add_item('shellScript', f'"{cmdstr}"')
+ shell_dict.add_item('showEnvVarsInLog', 0)
+
+ def generate_regen_shell_build_phase(self, objects_dict):
+ shell_dict = PbxDict()
+ objects_dict.add_item(self.regen_command_id, shell_dict, 'ShellScript')
+ shell_dict.add_item('isa', 'PBXShellScriptBuildPhase')
+ shell_dict.add_item('buildActionMask', 2147483647)
+ shell_dict.add_item('files', PbxArray())
+ shell_dict.add_item('inputPaths', PbxArray())
+ shell_dict.add_item('outputPaths', PbxArray())
+ shell_dict.add_item('runOnlyForDeploymentPostprocessing', 0)
+ shell_dict.add_item('shellPath', '/bin/sh')
+ cmd = mesonlib.get_meson_command() + ['--internal', 'regencheck', os.path.join(self.environment.get_build_dir(), 'meson-private')]
+ cmdstr = ' '.join(["'%s'" % i for i in cmd])
+ shell_dict.add_item('shellScript', f'"{cmdstr}"')
+ shell_dict.add_item('showEnvVarsInLog', 0)
+
+ def generate_custom_target_shell_build_phases(self, objects_dict):
+ # Custom targets are shell build phases in Xcode terminology.
+ for tname, t in self.custom_targets.items():
+ if not isinstance(t, build.CustomTarget):
+ continue
+ (srcs, ofilenames, cmd) = self.eval_custom_target_command(t, absolute_outputs=True)
+ fixed_cmd, _ = self.as_meson_exe_cmdline(cmd[0],
+ cmd[1:],
+ capture=ofilenames[0] if t.capture else None,
+ feed=srcs[0] if t.feed else None,
+ env=t.env)
+ custom_dict = PbxDict()
+ objects_dict.add_item(self.shell_targets[tname], custom_dict, f'/* Custom target {tname} */')
+ custom_dict.add_item('isa', 'PBXShellScriptBuildPhase')
+ custom_dict.add_item('buildActionMask', 2147483647)
+ custom_dict.add_item('files', PbxArray())
+ custom_dict.add_item('inputPaths', PbxArray())
+ outarray = PbxArray()
+ custom_dict.add_item('name', '"Generate {}."'.format(ofilenames[0]))
+ custom_dict.add_item('outputPaths', outarray)
+ for o in ofilenames:
+ outarray.add_item(os.path.join(self.environment.get_build_dir(), o))
+ custom_dict.add_item('runOnlyForDeploymentPostprocessing', 0)
+ custom_dict.add_item('shellPath', '/bin/sh')
+ workdir = self.environment.get_build_dir()
+ quoted_cmd = []
+ for c in fixed_cmd:
+ quoted_cmd.append(c.replace('"', chr(92) + '"'))
+ cmdstr = ' '.join([f"\\'{x}\\'" for x in quoted_cmd])
+ custom_dict.add_item('shellScript', f'"cd {workdir}; {cmdstr}"')
+ custom_dict.add_item('showEnvVarsInLog', 0)
+
+ def generate_generator_target_shell_build_phases(self, objects_dict):
+ for tname, t in self.build_targets.items():
+ generator_id = 0
+ for genlist in t.generated:
+ if isinstance(genlist, build.GeneratedList):
+ self.generate_single_generator_phase(tname, t, genlist, generator_id, objects_dict)
+ generator_id += 1
+ for tname, t in self.custom_targets.items():
+ generator_id = 0
+ for genlist in t.sources:
+ if isinstance(genlist, build.GeneratedList):
+ self.generate_single_generator_phase(tname, t, genlist, generator_id, objects_dict)
+ generator_id += 1
+
+ def generate_single_generator_phase(self, tname, t, genlist, generator_id, objects_dict):
+ # TODO: this should be rewritten to use the meson wrapper, like the other generators do
+ # Currently it doesn't handle a host binary that requires an exe wrapper correctly.
+ generator = genlist.get_generator()
+ exe = generator.get_exe()
+ exe_arr = self.build_target_to_cmd_array(exe)
+ workdir = self.environment.get_build_dir()
+ gen_dict = PbxDict()
+ objects_dict.add_item(self.shell_targets[(tname, generator_id)], gen_dict, f'"Generator {generator_id}/{tname}"')
+ infilelist = genlist.get_inputs()
+ outfilelist = genlist.get_outputs()
+ gen_dict.add_item('isa', 'PBXShellScriptBuildPhase')
+ gen_dict.add_item('buildActionMask', 2147483647)
+ gen_dict.add_item('files', PbxArray())
+ gen_dict.add_item('inputPaths', PbxArray())
+ gen_dict.add_item('name', f'"Generator {generator_id}/{tname}"')
+ commands = [["cd", workdir]] # Array of arrays, each one a single command, will get concatenated below.
+ k = (tname, generator_id)
+ ofile_abs = self.generator_outputs[k]
+ outarray = PbxArray()
+ gen_dict.add_item('outputPaths', outarray)
+ for of in ofile_abs:
+ outarray.add_item(of)
+ for i in infilelist:
+ # This might be needed to be added to inputPaths. It's not done yet as it is
+ # unclear whether it is necessary, what actually happens when it is defined
+ # and currently the build works without it.
+ #infile_abs = i.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir())
+ infilename = i.rel_to_builddir(self.build_to_src)
+ base_args = generator.get_arglist(infilename)
+ for o_base in genlist.get_outputs_for(i):
+ o = os.path.join(self.get_target_private_dir(t), o_base)
+ args = []
+ for arg in base_args:
+ arg = arg.replace("@INPUT@", infilename)
+ arg = arg.replace('@OUTPUT@', o).replace('@BUILD_DIR@', self.get_target_private_dir(t))
+ arg = arg.replace("@CURRENT_SOURCE_DIR@", os.path.join(self.build_to_src, t.subdir))
+ args.append(arg)
+ args = self.replace_outputs(args, self.get_target_private_dir(t), outfilelist)
+ args = self.replace_extra_args(args, genlist)
+ if generator.capture:
+ # When capturing, stdout is the output. Forward it with the shell.
+ full_command = ['('] + exe_arr + args + ['>', o, ')']
+ else:
+ full_command = exe_arr + args
+ commands.append(full_command)
+ gen_dict.add_item('runOnlyForDeploymentPostprocessing', 0)
+ gen_dict.add_item('shellPath', '/bin/sh')
+ quoted_cmds = []
+ for cmnd in commands:
+ q = []
+ for c in cmnd:
+ if ' ' in c:
+ q.append(f'\\"{c}\\"')
+ else:
+ q.append(c)
+ quoted_cmds.append(' '.join(q))
+ cmdstr = '"' + ' && '.join(quoted_cmds) + '"'
+ gen_dict.add_item('shellScript', cmdstr)
+ gen_dict.add_item('showEnvVarsInLog', 0)
+
+ def generate_pbx_sources_build_phase(self, objects_dict):
+ for name in self.source_phase:
+ phase_dict = PbxDict()
+ t = self.build_targets[name]
+ objects_dict.add_item(t.buildphasemap[name], phase_dict, 'Sources')
+ phase_dict.add_item('isa', 'PBXSourcesBuildPhase')
+ phase_dict.add_item('buildActionMask', 2147483647)
+ file_arr = PbxArray()
+ phase_dict.add_item('files', file_arr)
+ for s in self.build_targets[name].sources:
+ s = os.path.join(s.subdir, s.fname)
+ if not self.environment.is_header(s):
+ file_arr.add_item(self.buildfile_ids[(name, s)], os.path.join(self.environment.get_source_dir(), s))
+ generator_id = 0
+ for gt in t.generated:
+ if isinstance(gt, build.CustomTarget):
+ (srcs, ofilenames, cmd) = self.eval_custom_target_command(gt)
+ for o in ofilenames:
+ file_arr.add_item(self.custom_target_output_buildfile[o],
+ os.path.join(self.environment.get_build_dir(), o))
+ elif isinstance(gt, build.CustomTargetIndex):
+ for o in gt.get_outputs():
+ file_arr.add_item(self.custom_target_output_buildfile[o],
+ os.path.join(self.environment.get_build_dir(), o))
+ elif isinstance(gt, build.GeneratedList):
+ genfiles = self.generator_buildfile_ids[(name, generator_id)]
+ generator_id += 1
+ for o in genfiles:
+ file_arr.add_item(o)
+ else:
+ raise RuntimeError('Unknown input type: ' + str(gt))
+ phase_dict.add_item('runOnlyForDeploymentPostprocessing', 0)
+
+ def generate_pbx_target_dependency(self, objects_dict):
+ all_dict = PbxDict()
+ objects_dict.add_item(self.build_all_tdep_id, all_dict, 'ALL_BUILD')
+ all_dict.add_item('isa', 'PBXTargetDependency')
+ all_dict.add_item('target', self.all_id)
+ targets = []
+ targets.append((self.regen_dependency_id, self.regen_id, 'REGEN', None))
+ for t in self.build_targets:
+ idval = self.pbx_dep_map[t] # VERIFY: is this correct?
+ targets.append((idval, self.native_targets[t], t, self.containerproxy_map[t]))
+
+ for t in self.custom_targets:
+ idval = self.pbx_custom_dep_map[t]
+ targets.append((idval, self.custom_aggregate_targets[t], t, None)) # self.containerproxy_map[t]))
+
+ # Sort object by ID
+ sorted_targets = sorted(targets, key=operator.itemgetter(0))
+ for t in sorted_targets:
+ t_dict = PbxDict()
+ objects_dict.add_item(t[0], t_dict, 'PBXTargetDependency')
+ t_dict.add_item('isa', 'PBXTargetDependency')
+ t_dict.add_item('target', t[1], t[2])
+ if t[3] is not None:
+ t_dict.add_item('targetProxy', t[3], 'PBXContainerItemProxy')
+
+ def generate_xc_build_configuration(self, objects_dict):
+ # First the setup for the toplevel project.
+ for buildtype in self.buildtypes:
+ bt_dict = PbxDict()
+ objects_dict.add_item(self.project_configurations[buildtype], bt_dict, buildtype)
+ bt_dict.add_item('isa', 'XCBuildConfiguration')
+ settings_dict = PbxDict()
+ bt_dict.add_item('buildSettings', settings_dict)
+ settings_dict.add_item('ARCHS', '"$(NATIVE_ARCH_ACTUAL)"')
+ settings_dict.add_item('ONLY_ACTIVE_ARCH', 'YES')
+ settings_dict.add_item('SWIFT_VERSION', '5.0')
+ settings_dict.add_item('SDKROOT', '"macosx"')
+ settings_dict.add_item('SYMROOT', '"%s/build"' % self.environment.get_build_dir())
+ bt_dict.add_item('name', f'"{buildtype}"')
+
+ # Then the all target.
+ for buildtype in self.buildtypes:
+ bt_dict = PbxDict()
+ objects_dict.add_item(self.buildall_configurations[buildtype], bt_dict, buildtype)
+ bt_dict.add_item('isa', 'XCBuildConfiguration')
+ settings_dict = PbxDict()
+ bt_dict.add_item('buildSettings', settings_dict)
+ settings_dict.add_item('SYMROOT', '"%s"' % self.environment.get_build_dir())
+ warn_array = PbxArray()
+ warn_array.add_item('"$(inherited)"')
+ settings_dict.add_item('WARNING_CFLAGS', warn_array)
+
+ bt_dict.add_item('name', f'"{buildtype}"')
+
+ # Then the test target.
+ for buildtype in self.buildtypes:
+ bt_dict = PbxDict()
+ objects_dict.add_item(self.test_configurations[buildtype], bt_dict, buildtype)
+ bt_dict.add_item('isa', 'XCBuildConfiguration')
+ settings_dict = PbxDict()
+ bt_dict.add_item('buildSettings', settings_dict)
+ settings_dict.add_item('SYMROOT', '"%s"' % self.environment.get_build_dir())
+ warn_array = PbxArray()
+ settings_dict.add_item('WARNING_CFLAGS', warn_array)
+ warn_array.add_item('"$(inherited)"')
+ bt_dict.add_item('name', f'"{buildtype}"')
+
+ # Now finally targets.
+ for target_name, target in self.build_targets.items():
+ self.generate_single_build_target(objects_dict, target_name, target)
+
+ for target_name, target in self.custom_targets.items():
+ bt_dict = PbxDict()
+ objects_dict.add_item(self.buildconfmap[target_name][buildtype], bt_dict, buildtype)
+ bt_dict.add_item('isa', 'XCBuildConfiguration')
+ settings_dict = PbxDict()
+ bt_dict.add_item('buildSettings', settings_dict)
+ settings_dict.add_item('ARCHS', '"$(NATIVE_ARCH_ACTUAL)"')
+ settings_dict.add_item('ONLY_ACTIVE_ARCH', 'YES')
+ settings_dict.add_item('SDKROOT', '"macosx"')
+ settings_dict.add_item('SYMROOT', '"%s/build"' % self.environment.get_build_dir())
+ bt_dict.add_item('name', f'"{buildtype}"')
+
+ def determine_internal_dep_link_args(self, target, buildtype):
+ links_dylib = False
+ dep_libs = []
+ for l in target.link_targets:
+ if isinstance(target, build.SharedModule) and isinstance(l, build.Executable):
+ continue
+ if isinstance(l, build.CustomTargetIndex):
+ rel_dir = self.get_custom_target_output_dir(l.target)
+ libname = l.get_filename()
+ elif isinstance(l, build.CustomTarget):
+ rel_dir = self.get_custom_target_output_dir(l)
+ libname = l.get_filename()
+ else:
+ rel_dir = self.get_target_dir(l)
+ libname = l.get_filename()
+ abs_path = os.path.join(self.environment.get_build_dir(), rel_dir, libname)
+ dep_libs.append("'%s'" % abs_path)
+ if isinstance(l, build.SharedLibrary):
+ links_dylib = True
+ if isinstance(l, build.StaticLibrary):
+ (sub_libs, sub_links_dylib) = self.determine_internal_dep_link_args(l, buildtype)
+ dep_libs += sub_libs
+ links_dylib = links_dylib or sub_links_dylib
+ return (dep_libs, links_dylib)
+
+ def generate_single_build_target(self, objects_dict, target_name, target):
+ for buildtype in self.buildtypes:
+ dep_libs = []
+ links_dylib = False
+ headerdirs = []
+ for d in target.include_dirs:
+ for sd in d.incdirs:
+ cd = os.path.join(d.curdir, sd)
+ headerdirs.append(os.path.join(self.environment.get_source_dir(), cd))
+ headerdirs.append(os.path.join(self.environment.get_build_dir(), cd))
+ for extra in d.extra_build_dirs:
+ headerdirs.append(os.path.join(self.environment.get_build_dir(), extra))
+ (dep_libs, links_dylib) = self.determine_internal_dep_link_args(target, buildtype)
+ if links_dylib:
+ dep_libs = ['-Wl,-search_paths_first', '-Wl,-headerpad_max_install_names'] + dep_libs
+ dylib_version = None
+ if isinstance(target, build.SharedLibrary):
+ if isinstance(target, build.SharedModule):
+ ldargs = []
+ else:
+ ldargs = ['-dynamiclib']
+ ldargs += ['-Wl,-headerpad_max_install_names'] + dep_libs
+ install_path = os.path.join(self.environment.get_build_dir(), target.subdir, buildtype)
+ dylib_version = target.soversion
+ else:
+ ldargs = dep_libs
+ install_path = ''
+ if dylib_version is not None:
+ product_name = target.get_basename() + '.' + dylib_version
+ else:
+ product_name = target.get_basename()
+ ldargs += target.link_args
+ # Swift is special. Again. You can't mix Swift with other languages
+ # in the same target. Thus for Swift we only use
+ if self.is_swift_target(target):
+ linker, stdlib_args = target.compilers['swift'], []
+ else:
+ linker, stdlib_args = self.determine_linker_and_stdlib_args(target)
+ if not isinstance(target, build.StaticLibrary):
+ ldargs += self.build.get_project_link_args(linker, target.subproject, target.for_machine)
+ ldargs += self.build.get_global_link_args(linker, target.for_machine)
+ cargs = []
+ for dep in target.get_external_deps():
+ cargs += dep.get_compile_args()
+ ldargs += dep.get_link_args()
+ for o in target.objects:
+ # Add extracted objects to the link line by hand.
+ if isinstance(o, build.ExtractedObjects):
+ added_objs = set()
+ for objname_rel in self.determine_ext_objs(o):
+ objname_abs = os.path.join(self.environment.get_build_dir(), o.target.subdir, objname_rel)
+ if objname_abs not in added_objs:
+ added_objs.add(objname_abs)
+ ldargs += [r'\"' + objname_abs + r'\"']
+ generator_id = 0
+ for o in target.generated:
+ if isinstance(o, build.GeneratedList):
+ outputs = self.generator_outputs[target_name, generator_id]
+ generator_id += 1
+ for o_abs in outputs:
+ if o_abs.endswith('.o') or o_abs.endswith('.obj'):
+ ldargs += [r'\"' + o_abs + r'\"']
+ else:
+ if isinstance(o, build.CustomTarget):
+ (srcs, ofilenames, cmd) = self.eval_custom_target_command(o)
+ for ofname in ofilenames:
+ if os.path.splitext(ofname)[-1] in LINKABLE_EXTENSIONS:
+ ldargs += [r'\"' + os.path.join(self.environment.get_build_dir(), ofname) + r'\"']
+ elif isinstance(o, build.CustomTargetIndex):
+ for ofname in o.get_outputs():
+ if os.path.splitext(ofname)[-1] in LINKABLE_EXTENSIONS:
+ ldargs += [r'\"' + os.path.join(self.environment.get_build_dir(), ofname) + r'\"']
+ else:
+ raise RuntimeError(o)
+ if isinstance(target, build.SharedModule):
+ options = self.environment.coredata.options
+ ldargs += linker.get_std_shared_module_link_args(options)
+ elif isinstance(target, build.SharedLibrary):
+ ldargs += linker.get_std_shared_lib_link_args()
+ ldstr = ' '.join(ldargs)
+ valid = self.buildconfmap[target_name][buildtype]
+ langargs = {}
+ for lang in self.environment.coredata.compilers[target.for_machine]:
+ if lang not in LANGNAMEMAP:
+ continue
+ compiler = target.compilers.get(lang)
+ if compiler is None:
+ continue
+ # Start with warning args
+ warn_args = compiler.get_warn_args(target.get_option(OptionKey('warning_level')))
+ copt_proxy = target.get_options()
+ std_args = compiler.get_option_compile_args(copt_proxy)
+ # Add compile args added using add_project_arguments()
+ pargs = self.build.projects_args[target.for_machine].get(target.subproject, {}).get(lang, [])
+ # Add compile args added using add_global_arguments()
+ # These override per-project arguments
+ gargs = self.build.global_args[target.for_machine].get(lang, [])
+ targs = target.get_extra_args(lang)
+ args = warn_args + std_args + pargs + gargs + targs
+ if lang == 'swift':
+ # For some reason putting Swift module dirs in HEADER_SEARCH_PATHS does not work,
+ # but adding -I/path to manual args does work.
+ swift_dep_dirs = self.determine_swift_dep_dirs(target)
+ for d in swift_dep_dirs:
+ args += compiler.get_include_args(d, False)
+ if args:
+ lang_cargs = cargs
+ if compiler and target.implicit_include_directories:
+ # It is unclear what is the cwd when xcode runs. -I. does not seem to
+ # add the root build dir to the search path. So add an absolute path instead.
+ # This may break reproducible builds, in which case patches are welcome.
+ lang_cargs += self.get_custom_target_dir_include_args(target, compiler, absolute_path=True)
+ # Xcode can not handle separate compilation flags for C and ObjectiveC. They are both
+ # put in OTHER_CFLAGS. Same with C++ and ObjectiveC++.
+ if lang == 'objc':
+ lang = 'c'
+ elif lang == 'objcpp':
+ lang = 'cpp'
+ langname = LANGNAMEMAP[lang]
+ if langname in langargs:
+ langargs[langname] += args
+ else:
+ langargs[langname] = args
+ langargs[langname] += lang_cargs
+ symroot = os.path.join(self.environment.get_build_dir(), target.subdir)
+ bt_dict = PbxDict()
+ objects_dict.add_item(valid, bt_dict, buildtype)
+ bt_dict.add_item('isa', 'XCBuildConfiguration')
+ settings_dict = PbxDict()
+ bt_dict.add_item('buildSettings', settings_dict)
+ settings_dict.add_item('COMBINE_HIDPI_IMAGES', 'YES')
+ if isinstance(target, build.SharedModule):
+ settings_dict.add_item('DYLIB_CURRENT_VERSION', '""')
+ settings_dict.add_item('DYLIB_COMPATIBILITY_VERSION', '""')
+ else:
+ if dylib_version is not None:
+ settings_dict.add_item('DYLIB_CURRENT_VERSION', f'"{dylib_version}"')
+ if target.prefix:
+ settings_dict.add_item('EXECUTABLE_PREFIX', target.prefix)
+ if target.suffix:
+ suffix = '.' + target.suffix
+ settings_dict.add_item('EXECUTABLE_SUFFIX', suffix)
+ settings_dict.add_item('GCC_GENERATE_DEBUGGING_SYMBOLS', BOOL2XCODEBOOL[target.get_option(OptionKey('debug'))])
+ settings_dict.add_item('GCC_INLINES_ARE_PRIVATE_EXTERN', 'NO')
+ opt_flag = OPT2XCODEOPT[target.get_option(OptionKey('optimization'))]
+ if opt_flag is not None:
+ settings_dict.add_item('GCC_OPTIMIZATION_LEVEL', opt_flag)
+ if target.has_pch:
+ # Xcode uses GCC_PREFIX_HEADER which only allows one file per target/executable. Precompiling various header files and
+ # applying a particular pch to each source file will require custom scripts (as a build phase) and build flags per each
+ # file. Since Xcode itself already discourages precompiled headers in favor of modules we don't try much harder here.
+ pchs = target.get_pch('c') + target.get_pch('cpp') + target.get_pch('objc') + target.get_pch('objcpp')
+ # Make sure to use headers (other backends require implementation files like *.c *.cpp, etc; these should not be used here)
+ pchs = [pch for pch in pchs if pch.endswith('.h') or pch.endswith('.hh') or pch.endswith('hpp')]
+ if pchs:
+ if len(pchs) > 1:
+ mlog.warning(f'Unsupported Xcode configuration: More than 1 precompiled header found "{pchs!s}". Target "{target.name}" might not compile correctly.')
+ relative_pch_path = os.path.join(target.get_subdir(), pchs[0]) # Path relative to target so it can be used with "$(PROJECT_DIR)"
+ settings_dict.add_item('GCC_PRECOMPILE_PREFIX_HEADER', 'YES')
+ settings_dict.add_item('GCC_PREFIX_HEADER', f'"$(PROJECT_DIR)/{relative_pch_path}"')
+ settings_dict.add_item('GCC_PREPROCESSOR_DEFINITIONS', '""')
+ settings_dict.add_item('GCC_SYMBOLS_PRIVATE_EXTERN', 'NO')
+ header_arr = PbxArray()
+ unquoted_headers = []
+ unquoted_headers.append(self.get_target_private_dir_abs(target))
+ if target.implicit_include_directories:
+ unquoted_headers.append(os.path.join(self.environment.get_build_dir(), target.get_subdir()))
+ unquoted_headers.append(os.path.join(self.environment.get_source_dir(), target.get_subdir()))
+ if headerdirs:
+ for i in headerdirs:
+ i = os.path.normpath(i)
+ unquoted_headers.append(i)
+ for i in unquoted_headers:
+ header_arr.add_item(f'"\\"{i}\\""')
+ settings_dict.add_item('HEADER_SEARCH_PATHS', header_arr)
+ settings_dict.add_item('INSTALL_PATH', f'"{install_path}"')
+ settings_dict.add_item('LIBRARY_SEARCH_PATHS', '""')
+ if isinstance(target, build.SharedModule):
+ settings_dict.add_item('LIBRARY_STYLE', 'BUNDLE')
+ settings_dict.add_item('MACH_O_TYPE', 'mh_bundle')
+ elif isinstance(target, build.SharedLibrary):
+ settings_dict.add_item('LIBRARY_STYLE', 'DYNAMIC')
+ self.add_otherargs(settings_dict, langargs)
+ settings_dict.add_item('OTHER_LDFLAGS', f'"{ldstr}"')
+ settings_dict.add_item('OTHER_REZFLAGS', '""')
+ if ' ' in product_name:
+ settings_dict.add_item('PRODUCT_NAME', f'"{product_name}"')
+ else:
+ settings_dict.add_item('PRODUCT_NAME', product_name)
+ settings_dict.add_item('SECTORDER_FLAGS', '""')
+ settings_dict.add_item('SYMROOT', f'"{symroot}"')
+ sysheader_arr = PbxArray()
+ # XCode will change every -I flag that points inside these directories
+ # to an -isystem. Thus set nothing in it since we control our own
+ # include flags.
+ settings_dict.add_item('SYSTEM_HEADER_SEARCH_PATHS', sysheader_arr)
+ settings_dict.add_item('USE_HEADERMAP', 'NO')
+ warn_array = PbxArray()
+ settings_dict.add_item('WARNING_CFLAGS', warn_array)
+ warn_array.add_item('"$(inherited)"')
+ bt_dict.add_item('name', buildtype)
+
+ def add_otherargs(self, settings_dict, langargs):
+ for langname, args in langargs.items():
+ if args:
+ quoted_args = []
+ for a in args:
+ # This works but
+ # a) it's ugly as sin
+ # b) I don't know why it works or why every backslash must be escaped into eight backslashes
+ a = a.replace(chr(92), 8*chr(92)) # chr(92) is backslash, this how we smuggle it in without Python's quoting grabbing it.
+ a = a.replace(r'"', r'\\\"')
+ if ' ' in a or "'" in a:
+ a = r'\"' + a + r'\"'
+ quoted_args.append(a)
+ settings_dict.add_item(f'OTHER_{langname}FLAGS', '"' + ' '.join(quoted_args) + '"')
+
+ def generate_xc_configurationList(self, objects_dict):
+ # FIXME: sort items
+ conf_dict = PbxDict()
+ objects_dict.add_item(self.project_conflist, conf_dict, f'Build configuration list for PBXProject "{self.build.project_name}"')
+ conf_dict.add_item('isa', 'XCConfigurationList')
+ confs_arr = PbxArray()
+ conf_dict.add_item('buildConfigurations', confs_arr)
+ for buildtype in self.buildtypes:
+ confs_arr.add_item(self.project_configurations[buildtype], buildtype)
+ conf_dict.add_item('defaultConfigurationIsVisible', 0)
+ conf_dict.add_item('defaultConfigurationName', self.buildtype)
+
+ # Now the all target
+ all_dict = PbxDict()
+ objects_dict.add_item(self.all_buildconf_id, all_dict, 'Build configuration list for PBXAggregateTarget "ALL_BUILD"')
+ all_dict.add_item('isa', 'XCConfigurationList')
+ conf_arr = PbxArray()
+ all_dict.add_item('buildConfigurations', conf_arr)
+ for buildtype in self.buildtypes:
+ conf_arr.add_item(self.buildall_configurations[buildtype], buildtype)
+ all_dict.add_item('defaultConfigurationIsVisible', 0)
+ all_dict.add_item('defaultConfigurationName', self.buildtype)
+
+ # Test target
+ test_dict = PbxDict()
+ objects_dict.add_item(self.test_buildconf_id, test_dict, 'Build configuration list for PBXAggregateTarget "RUN_TEST"')
+ test_dict.add_item('isa', 'XCConfigurationList')
+ conf_arr = PbxArray()
+ test_dict.add_item('buildConfigurations', conf_arr)
+ for buildtype in self.buildtypes:
+ conf_arr.add_item(self.test_configurations[buildtype], buildtype)
+ test_dict.add_item('defaultConfigurationIsVisible', 0)
+ test_dict.add_item('defaultConfigurationName', self.buildtype)
+
+ # Regen target
+ regen_dict = PbxDict()
+ objects_dict.add_item(self.regen_buildconf_id, test_dict, 'Build configuration list for PBXAggregateTarget "REGENERATE"')
+ regen_dict.add_item('isa', 'XCConfigurationList')
+ conf_arr = PbxArray()
+ regen_dict.add_item('buildConfigurations', conf_arr)
+ for buildtype in self.buildtypes:
+ conf_arr.add_item(self.test_configurations[buildtype], buildtype)
+ regen_dict.add_item('defaultConfigurationIsVisible', 0)
+ regen_dict.add_item('defaultConfigurationName', self.buildtype)
+
+ for target_name in self.build_targets:
+ t_dict = PbxDict()
+ listid = self.buildconflistmap[target_name]
+ objects_dict.add_item(listid, t_dict, f'Build configuration list for PBXNativeTarget "{target_name}"')
+ t_dict.add_item('isa', 'XCConfigurationList')
+ conf_arr = PbxArray()
+ t_dict.add_item('buildConfigurations', conf_arr)
+ idval = self.buildconfmap[target_name][self.buildtype]
+ conf_arr.add_item(idval, self.buildtype)
+ t_dict.add_item('defaultConfigurationIsVisible', 0)
+ t_dict.add_item('defaultConfigurationName', self.buildtype)
+
+ for target_name in self.custom_targets:
+ t_dict = PbxDict()
+ listid = self.buildconflistmap[target_name]
+ objects_dict.add_item(listid, t_dict, f'Build configuration list for PBXAggregateTarget "{target_name}"')
+ t_dict.add_item('isa', 'XCConfigurationList')
+ conf_arr = PbxArray()
+ t_dict.add_item('buildConfigurations', conf_arr)
+ idval = self.buildconfmap[target_name][self.buildtype]
+ conf_arr.add_item(idval, self.buildtype)
+ t_dict.add_item('defaultConfigurationIsVisible', 0)
+ t_dict.add_item('defaultConfigurationName', self.buildtype)
+
+ def generate_prefix(self, pbxdict):
+ pbxdict.add_item('archiveVersion', '1')
+ pbxdict.add_item('classes', PbxDict())
+ pbxdict.add_item('objectVersion', '46')
+ objects_dict = PbxDict()
+ pbxdict.add_item('objects', objects_dict)
+
+ return objects_dict
+
+ def generate_suffix(self, pbxdict):
+ pbxdict.add_item('rootObject', self.project_uid, 'Project object')
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
new file mode 100644
index 0000000..a37ee92
--- /dev/null
+++ b/mesonbuild/build.py
@@ -0,0 +1,2905 @@
+# 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.
+
+from __future__ import annotations
+from collections import defaultdict, OrderedDict
+from dataclasses import dataclass, field
+from functools import lru_cache
+import copy
+import hashlib
+import itertools, pathlib
+import os
+import pickle
+import re
+import textwrap
+import typing as T
+
+
+from . import environment
+from . import dependencies
+from . import mlog
+from . import programs
+from .mesonlib import (
+ HoldableObject, SecondLevelHolder,
+ File, MesonException, MachineChoice, PerMachine, OrderedSet, listify,
+ extract_as_list, typeslistify, stringlistify, classify_unity_sources,
+ get_filenames_templates_dict, substitute_values, has_path_sep,
+ OptionKey, PerMachineDefaultable, OptionOverrideProxy,
+ MesonBugException, EnvironmentVariables, pickle_load,
+)
+from .compilers import (
+ is_object, clink_langs, sort_clink, all_languages,
+ is_known_suffix, detect_static_linker
+)
+from .interpreterbase import FeatureNew, FeatureDeprecated
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Literal
+ from ._typing import ImmutableListProtocol
+ from .backend.backends import Backend, ExecutableSerialisation
+ from .compilers import Compiler
+ from .interpreter.interpreter import Test, SourceOutputs, Interpreter
+ from .interpreterbase import SubProject
+ from .linkers import StaticLinker
+ from .mesonlib import FileMode, FileOrString
+ from .modules import ModuleState
+ from .mparser import BaseNode
+ from .wrap import WrapMode
+
+ GeneratedTypes = T.Union['CustomTarget', 'CustomTargetIndex', 'GeneratedList']
+ LibTypes = T.Union['SharedLibrary', 'StaticLibrary', 'CustomTarget', 'CustomTargetIndex']
+ BuildTargetTypes = T.Union['BuildTarget', 'CustomTarget', 'CustomTargetIndex']
+
+pch_kwargs = {'c_pch', 'cpp_pch'}
+
+lang_arg_kwargs = {f'{lang}_args' for lang in all_languages}
+lang_arg_kwargs |= {
+ 'd_import_dirs',
+ 'd_unittest',
+ 'd_module_versions',
+ 'd_debug',
+}
+
+vala_kwargs = {'vala_header', 'vala_gir', 'vala_vapi'}
+rust_kwargs = {'rust_crate_type'}
+cs_kwargs = {'resources', 'cs_args'}
+
+buildtarget_kwargs = {
+ 'build_by_default',
+ 'build_rpath',
+ 'dependencies',
+ 'extra_files',
+ 'gui_app',
+ 'link_with',
+ 'link_whole',
+ 'link_args',
+ 'link_depends',
+ 'implicit_include_directories',
+ 'include_directories',
+ 'install',
+ 'install_rpath',
+ 'install_dir',
+ 'install_mode',
+ 'install_tag',
+ 'name_prefix',
+ 'name_suffix',
+ 'native',
+ 'objects',
+ 'override_options',
+ 'sources',
+ 'gnu_symbol_visibility',
+ 'link_language',
+ 'win_subsystem',
+}
+
+known_build_target_kwargs = (
+ buildtarget_kwargs |
+ lang_arg_kwargs |
+ pch_kwargs |
+ vala_kwargs |
+ rust_kwargs |
+ cs_kwargs)
+
+known_exe_kwargs = known_build_target_kwargs | {'implib', 'export_dynamic', 'pie'}
+known_shlib_kwargs = known_build_target_kwargs | {'version', 'soversion', 'vs_module_defs', 'darwin_versions'}
+known_shmod_kwargs = known_build_target_kwargs | {'vs_module_defs'}
+known_stlib_kwargs = known_build_target_kwargs | {'pic', 'prelink'}
+known_jar_kwargs = known_exe_kwargs | {'main_class', 'java_resources'}
+
+def _process_install_tag(install_tag: T.Optional[T.List[T.Optional[str]]],
+ num_outputs: int) -> T.List[T.Optional[str]]:
+ _install_tag: T.List[T.Optional[str]]
+ if not install_tag:
+ _install_tag = [None] * num_outputs
+ elif len(install_tag) == 1:
+ _install_tag = install_tag * num_outputs
+ else:
+ _install_tag = install_tag
+ return _install_tag
+
+
+@lru_cache(maxsize=None)
+def get_target_macos_dylib_install_name(ld) -> str:
+ name = ['@rpath/', ld.prefix, ld.name]
+ if ld.soversion is not None:
+ name.append('.' + ld.soversion)
+ name.append('.dylib')
+ return ''.join(name)
+
+class InvalidArguments(MesonException):
+ pass
+
+@dataclass(eq=False)
+class DependencyOverride(HoldableObject):
+ dep: dependencies.Dependency
+ node: 'BaseNode'
+ explicit: bool = True
+
+@dataclass(eq=False)
+class Headers(HoldableObject):
+ sources: T.List[File]
+ install_subdir: T.Optional[str]
+ custom_install_dir: T.Optional[str]
+ custom_install_mode: 'FileMode'
+ subproject: str
+
+ # TODO: we really don't need any of these methods, but they're preserved to
+ # keep APIs relying on them working.
+
+ def set_install_subdir(self, subdir: str) -> None:
+ self.install_subdir = subdir
+
+ def get_install_subdir(self) -> T.Optional[str]:
+ return self.install_subdir
+
+ def get_sources(self) -> T.List[File]:
+ return self.sources
+
+ def get_custom_install_dir(self) -> T.Optional[str]:
+ return self.custom_install_dir
+
+ def get_custom_install_mode(self) -> 'FileMode':
+ return self.custom_install_mode
+
+
+@dataclass(eq=False)
+class Man(HoldableObject):
+ sources: T.List[File]
+ custom_install_dir: T.Optional[str]
+ custom_install_mode: 'FileMode'
+ subproject: str
+ locale: T.Optional[str]
+
+ def get_custom_install_dir(self) -> T.Optional[str]:
+ return self.custom_install_dir
+
+ def get_custom_install_mode(self) -> 'FileMode':
+ return self.custom_install_mode
+
+ def get_sources(self) -> T.List['File']:
+ return self.sources
+
+
+@dataclass(eq=False)
+class EmptyDir(HoldableObject):
+ path: str
+ install_mode: 'FileMode'
+ subproject: str
+ install_tag: T.Optional[str] = None
+
+
+@dataclass(eq=False)
+class InstallDir(HoldableObject):
+ source_subdir: str
+ installable_subdir: str
+ install_dir: str
+ install_dir_name: str
+ install_mode: 'FileMode'
+ exclude: T.Tuple[T.Set[str], T.Set[str]]
+ strip_directory: bool
+ subproject: str
+ from_source_dir: bool = True
+ install_tag: T.Optional[str] = None
+
+@dataclass(eq=False)
+class DepManifest:
+ version: str
+ license: T.List[str]
+
+ def to_json(self) -> T.Dict[str, T.Union[str, T.List[str]]]:
+ return {
+ 'version': self.version,
+ 'license': self.license,
+ }
+
+
+# literally everything isn't dataclass stuff
+class Build:
+ """A class that holds the status of one build including
+ all dependencies and so on.
+ """
+
+ def __init__(self, environment: environment.Environment):
+ self.project_name = 'name of master project'
+ self.project_version = None
+ self.environment = environment
+ self.projects = {}
+ self.targets: 'T.OrderedDict[str, T.Union[CustomTarget, BuildTarget]]' = OrderedDict()
+ self.global_args: PerMachine[T.Dict[str, T.List[str]]] = PerMachine({}, {})
+ self.global_link_args: PerMachine[T.Dict[str, T.List[str]]] = PerMachine({}, {})
+ self.projects_args: PerMachine[T.Dict[str, T.Dict[str, T.List[str]]]] = PerMachine({}, {})
+ self.projects_link_args: PerMachine[T.Dict[str, T.Dict[str, T.List[str]]]] = PerMachine({}, {})
+ self.tests: T.List['Test'] = []
+ self.benchmarks: T.List['Test'] = []
+ self.headers: T.List[Headers] = []
+ self.man: T.List[Man] = []
+ self.emptydir: T.List[EmptyDir] = []
+ self.data: T.List[Data] = []
+ self.symlinks: T.List[SymlinkData] = []
+ self.static_linker: PerMachine[StaticLinker] = PerMachine(None, None)
+ self.subprojects = {}
+ self.subproject_dir = ''
+ self.install_scripts: T.List['ExecutableSerialisation'] = []
+ self.postconf_scripts: T.List['ExecutableSerialisation'] = []
+ self.dist_scripts: T.List['ExecutableSerialisation'] = []
+ self.install_dirs: T.List[InstallDir] = []
+ self.dep_manifest_name: T.Optional[str] = None
+ self.dep_manifest: T.Dict[str, DepManifest] = {}
+ self.stdlibs = PerMachine({}, {})
+ self.test_setups: T.Dict[str, TestSetup] = {}
+ self.test_setup_default_name = None
+ self.find_overrides: T.Dict[str, T.Union['Executable', programs.ExternalProgram, programs.OverrideProgram]] = {}
+ self.searched_programs: T.Set[str] = set() # The list of all programs that have been searched for.
+
+ # If we are doing a cross build we need two caches, if we're doing a
+ # build == host compilation the both caches should point to the same place.
+ self.dependency_overrides: PerMachine[T.Dict[T.Tuple, DependencyOverride]] = PerMachineDefaultable.default(
+ environment.is_cross_build(), {}, {})
+ self.devenv: T.List[EnvironmentVariables] = []
+ self.modules: T.List[str] = []
+ self.need_vsenv = False
+
+ def get_build_targets(self):
+ build_targets = OrderedDict()
+ for name, t in self.targets.items():
+ if isinstance(t, BuildTarget):
+ build_targets[name] = t
+ return build_targets
+
+ def get_custom_targets(self):
+ custom_targets = OrderedDict()
+ for name, t in self.targets.items():
+ if isinstance(t, CustomTarget):
+ custom_targets[name] = t
+ return custom_targets
+
+ def copy(self) -> Build:
+ other = Build(self.environment)
+ for k, v in self.__dict__.items():
+ if isinstance(v, (list, dict, set, OrderedDict)):
+ other.__dict__[k] = v.copy()
+ else:
+ other.__dict__[k] = v
+ return other
+
+ def merge(self, other: Build) -> None:
+ for k, v in other.__dict__.items():
+ self.__dict__[k] = v
+
+ def ensure_static_linker(self, compiler: Compiler) -> None:
+ if self.static_linker[compiler.for_machine] is None and compiler.needs_static_linker():
+ self.static_linker[compiler.for_machine] = detect_static_linker(self.environment, compiler)
+
+ def get_project(self):
+ return self.projects['']
+
+ def get_subproject_dir(self):
+ return self.subproject_dir
+
+ def get_targets(self) -> 'T.OrderedDict[str, T.Union[CustomTarget, BuildTarget]]':
+ return self.targets
+
+ def get_tests(self) -> T.List['Test']:
+ return self.tests
+
+ def get_benchmarks(self) -> T.List['Test']:
+ return self.benchmarks
+
+ def get_headers(self) -> T.List['Headers']:
+ return self.headers
+
+ def get_man(self) -> T.List['Man']:
+ return self.man
+
+ def get_data(self) -> T.List['Data']:
+ return self.data
+
+ def get_symlinks(self) -> T.List['SymlinkData']:
+ return self.symlinks
+
+ def get_emptydir(self) -> T.List['EmptyDir']:
+ return self.emptydir
+
+ def get_install_subdirs(self) -> T.List['InstallDir']:
+ return self.install_dirs
+
+ def get_global_args(self, compiler: 'Compiler', for_machine: 'MachineChoice') -> T.List[str]:
+ d = self.global_args[for_machine]
+ return d.get(compiler.get_language(), [])
+
+ def get_project_args(self, compiler: 'Compiler', project: str, for_machine: 'MachineChoice') -> T.List[str]:
+ d = self.projects_args[for_machine]
+ args = d.get(project)
+ if not args:
+ return []
+ return args.get(compiler.get_language(), [])
+
+ def get_global_link_args(self, compiler: 'Compiler', for_machine: 'MachineChoice') -> T.List[str]:
+ d = self.global_link_args[for_machine]
+ return d.get(compiler.get_language(), [])
+
+ def get_project_link_args(self, compiler: 'Compiler', project: str, for_machine: 'MachineChoice') -> T.List[str]:
+ d = self.projects_link_args[for_machine]
+
+ link_args = d.get(project)
+ if not link_args:
+ return []
+
+ return link_args.get(compiler.get_language(), [])
+
+@dataclass(eq=False)
+class IncludeDirs(HoldableObject):
+
+ """Internal representation of an include_directories call."""
+
+ curdir: str
+ incdirs: T.List[str]
+ is_system: bool
+ # Interpreter has validated that all given directories
+ # actually exist.
+ extra_build_dirs: T.List[str] = field(default_factory=list)
+
+ def __repr__(self) -> str:
+ r = '<{} {}/{}>'
+ return r.format(self.__class__.__name__, self.curdir, self.incdirs)
+
+ def get_curdir(self) -> str:
+ return self.curdir
+
+ def get_incdirs(self) -> T.List[str]:
+ return self.incdirs
+
+ def get_extra_build_dirs(self) -> T.List[str]:
+ return self.extra_build_dirs
+
+ def to_string_list(self, sourcedir: str, builddir: T.Optional[str] = None) -> T.List[str]:
+ """Convert IncludeDirs object to a list of strings.
+
+ :param sourcedir: The absolute source directory
+ :param builddir: The absolute build directory, option, build dir will not
+ be added if this is unset
+ :returns: A list of strings (without compiler argument)
+ """
+ strlist: T.List[str] = []
+ for idir in self.incdirs:
+ strlist.append(os.path.join(sourcedir, self.curdir, idir))
+ if builddir:
+ strlist.append(os.path.join(builddir, self.curdir, idir))
+ return strlist
+
+@dataclass(eq=False)
+class ExtractedObjects(HoldableObject):
+ '''
+ Holds a list of sources for which the objects must be extracted
+ '''
+ target: 'BuildTarget'
+ srclist: T.List[File] = field(default_factory=list)
+ genlist: T.List['GeneratedTypes'] = field(default_factory=list)
+ objlist: T.List[T.Union[str, 'File', 'ExtractedObjects']] = field(default_factory=list)
+ recursive: bool = True
+
+ def __post_init__(self) -> None:
+ if self.target.is_unity:
+ self.check_unity_compatible()
+
+ def __repr__(self) -> str:
+ r = '<{0} {1!r}: {2}>'
+ return r.format(self.__class__.__name__, self.target.name, self.srclist)
+
+ @staticmethod
+ def get_sources(sources: T.Sequence['FileOrString'], generated_sources: T.Sequence['GeneratedTypes']) -> T.List['FileOrString']:
+ # Merge sources and generated sources
+ sources = list(sources)
+ for gensrc in generated_sources:
+ for s in gensrc.get_outputs():
+ # We cannot know the path where this source will be generated,
+ # but all we need here is the file extension to determine the
+ # compiler.
+ sources.append(s)
+
+ # Filter out headers and all non-source files
+ return [s for s in sources if environment.is_source(s)]
+
+ def classify_all_sources(self, sources: T.List[FileOrString], generated_sources: T.Sequence['GeneratedTypes']) -> T.Dict['Compiler', T.List['FileOrString']]:
+ sources_ = self.get_sources(sources, generated_sources)
+ return classify_unity_sources(self.target.compilers.values(), sources_)
+
+ def check_unity_compatible(self) -> None:
+ # Figure out if the extracted object list is compatible with a Unity
+ # build. When we're doing a Unified build, we go through the sources,
+ # and create a single source file from each subset of the sources that
+ # can be compiled with a specific compiler. Then we create one object
+ # from each unified source file. So for each compiler we can either
+ # extra all its sources or none.
+ cmpsrcs = self.classify_all_sources(self.target.sources, self.target.generated)
+ extracted_cmpsrcs = self.classify_all_sources(self.srclist, self.genlist)
+
+ for comp, srcs in extracted_cmpsrcs.items():
+ if set(srcs) != set(cmpsrcs[comp]):
+ raise MesonException('Single object files can not be extracted '
+ 'in Unity builds. You can only extract all '
+ 'the object files for each compiler at once.')
+
+
+@dataclass(eq=False, order=False)
+class StructuredSources(HoldableObject):
+
+ """A container for sources in languages that use filesystem hierarchy.
+
+ Languages like Rust and Cython rely on the layout of files in the filesystem
+ as part of the compiler implementation. This structure allows us to
+ represent the required filesystem layout.
+ """
+
+ sources: T.DefaultDict[str, T.List[T.Union[File, CustomTarget, CustomTargetIndex, GeneratedList]]] = field(
+ default_factory=lambda: defaultdict(list))
+
+ def __add__(self, other: StructuredSources) -> StructuredSources:
+ sources = self.sources.copy()
+ for k, v in other.sources.items():
+ sources[k].extend(v)
+ return StructuredSources(sources)
+
+ def __bool__(self) -> bool:
+ return bool(self.sources)
+
+ def first_file(self) -> T.Union[File, CustomTarget, CustomTargetIndex, GeneratedList]:
+ """Get the first source in the root
+
+ :return: The first source in the root
+ """
+ return self.sources[''][0]
+
+ def as_list(self) -> T.List[T.Union[File, CustomTarget, CustomTargetIndex, GeneratedList]]:
+ return list(itertools.chain.from_iterable(self.sources.values()))
+
+ def needs_copy(self) -> bool:
+ """Do we need to create a structure in the build directory.
+
+ This allows us to avoid making copies if the structures exists in the
+ source dir. Which could happen in situations where a generated source
+ only exists in some configurations
+ """
+ for files in self.sources.values():
+ for f in files:
+ if isinstance(f, File):
+ if f.is_built:
+ return True
+ else:
+ return True
+ return False
+
+
+@dataclass(eq=False)
+class Target(HoldableObject):
+
+ # TODO: should Target be an abc.ABCMeta?
+
+ name: str
+ subdir: str
+ subproject: 'SubProject'
+ build_by_default: bool
+ for_machine: MachineChoice
+ environment: environment.Environment
+
+ def __post_init__(self) -> None:
+ if has_path_sep(self.name):
+ # Fix failing test 53 when this becomes an error.
+ mlog.warning(textwrap.dedent(f'''\
+ Target "{self.name}" has a path separator in its name.
+ This is not supported, it can cause unexpected failures and will become
+ a hard error in the future.
+ '''))
+ self.install = False
+ self.build_always_stale = False
+ self.options = OptionOverrideProxy({}, self.environment.coredata.options, self.subproject)
+ self.extra_files = [] # type: T.List[File]
+ if not hasattr(self, 'typename'):
+ raise RuntimeError(f'Target type is not set for target class "{type(self).__name__}". This is a bug')
+
+ # dataclass comparators?
+ def __lt__(self, other: object) -> bool:
+ if not isinstance(other, Target):
+ return NotImplemented
+ return self.get_id() < other.get_id()
+
+ def __le__(self, other: object) -> bool:
+ if not isinstance(other, Target):
+ return NotImplemented
+ return self.get_id() <= other.get_id()
+
+ def __gt__(self, other: object) -> bool:
+ if not isinstance(other, Target):
+ return NotImplemented
+ return self.get_id() > other.get_id()
+
+ def __ge__(self, other: object) -> bool:
+ if not isinstance(other, Target):
+ return NotImplemented
+ return self.get_id() >= other.get_id()
+
+ def get_default_install_dir(self) -> T.Tuple[str, str]:
+ raise NotImplementedError
+
+ def get_custom_install_dir(self) -> T.List[T.Union[str, Literal[False]]]:
+ raise NotImplementedError
+
+ def get_install_dir(self) -> T.Tuple[T.List[T.Union[str, Literal[False]]], str, Literal[False]]:
+ # Find the installation directory.
+ default_install_dir, default_install_dir_name = self.get_default_install_dir()
+ outdirs = self.get_custom_install_dir()
+ if outdirs and outdirs[0] != default_install_dir and outdirs[0] is not True:
+ # Either the value is set to a non-default value, or is set to
+ # False (which means we want this specific output out of many
+ # outputs to not be installed).
+ custom_install_dir = True
+ install_dir_names = [getattr(i, 'optname', None) for i in outdirs]
+ else:
+ custom_install_dir = False
+ # if outdirs is empty we need to set to something, otherwise we set
+ # only the first value to the default.
+ if outdirs:
+ outdirs[0] = default_install_dir
+ else:
+ outdirs = [default_install_dir]
+ install_dir_names = [default_install_dir_name] * len(outdirs)
+
+ return outdirs, install_dir_names, custom_install_dir
+
+ def get_basename(self) -> str:
+ return self.name
+
+ def get_subdir(self) -> str:
+ return self.subdir
+
+ def get_typename(self) -> str:
+ return self.typename
+
+ @staticmethod
+ def _get_id_hash(target_id):
+ # We don't really need cryptographic security here.
+ # Small-digest hash function with unlikely collision is good enough.
+ h = hashlib.sha256()
+ h.update(target_id.encode(encoding='utf-8', errors='replace'))
+ # This ID should be case-insensitive and should work in Visual Studio,
+ # e.g. it should not start with leading '-'.
+ return h.hexdigest()[:7]
+
+ @staticmethod
+ def construct_id_from_path(subdir: str, name: str, type_suffix: str) -> str:
+ """Construct target ID from subdir, name and type suffix.
+
+ This helper function is made public mostly for tests."""
+ # This ID must also be a valid file name on all OSs.
+ # It should also avoid shell metacharacters for obvious
+ # reasons. '@' is not used as often as '_' in source code names.
+ # In case of collisions consider using checksums.
+ # FIXME replace with assert when slash in names is prohibited
+ name_part = name.replace('/', '@').replace('\\', '@')
+ assert not has_path_sep(type_suffix)
+ my_id = name_part + type_suffix
+ if subdir:
+ subdir_part = Target._get_id_hash(subdir)
+ # preserve myid for better debuggability
+ return subdir_part + '@@' + my_id
+ return my_id
+
+ def get_id(self) -> str:
+ return self.construct_id_from_path(
+ self.subdir, self.name, self.type_suffix())
+
+ def process_kwargs_base(self, kwargs: T.Dict[str, T.Any]) -> None:
+ if 'build_by_default' in kwargs:
+ self.build_by_default = kwargs['build_by_default']
+ if not isinstance(self.build_by_default, bool):
+ raise InvalidArguments('build_by_default must be a boolean value.')
+ elif kwargs.get('install', False):
+ # For backward compatibility, if build_by_default is not explicitly
+ # set, use the value of 'install' if it's enabled.
+ self.build_by_default = True
+
+ self.set_option_overrides(self.parse_overrides(kwargs))
+
+ def set_option_overrides(self, option_overrides: T.Dict[OptionKey, str]) -> None:
+ self.options.overrides = {}
+ for k, v in option_overrides.items():
+ if k.lang:
+ self.options.overrides[k.evolve(machine=self.for_machine)] = v
+ else:
+ self.options.overrides[k] = v
+
+ def get_options(self) -> OptionOverrideProxy:
+ return self.options
+
+ def get_option(self, key: 'OptionKey') -> T.Union[str, int, bool, 'WrapMode']:
+ # We don't actually have wrapmode here to do an assert, so just do a
+ # cast, we know what's in coredata anyway.
+ # TODO: if it's possible to annotate get_option or validate_option_value
+ # in the future we might be able to remove the cast here
+ return T.cast('T.Union[str, int, bool, WrapMode]', self.options[key].value)
+
+ @staticmethod
+ def parse_overrides(kwargs: T.Dict[str, T.Any]) -> T.Dict[OptionKey, str]:
+ opts = kwargs.get('override_options', [])
+
+ # In this case we have an already parsed and ready to go dictionary
+ # provided by typed_kwargs
+ if isinstance(opts, dict):
+ return T.cast('T.Dict[OptionKey, str]', opts)
+
+ result: T.Dict[OptionKey, str] = {}
+ overrides = stringlistify(opts)
+ for o in overrides:
+ if '=' not in o:
+ raise InvalidArguments('Overrides must be of form "key=value"')
+ k, v = o.split('=', 1)
+ key = OptionKey.from_string(k.strip())
+ v = v.strip()
+ result[key] = v
+ return result
+
+ def is_linkable_target(self) -> bool:
+ return False
+
+ def get_outputs(self) -> T.List[str]:
+ return []
+
+ def should_install(self) -> bool:
+ return False
+
+class BuildTarget(Target):
+ known_kwargs = known_build_target_kwargs
+
+ install_dir: T.List[T.Union[str, Literal[False]]]
+
+ def __init__(self, name: str, subdir: str, subproject: SubProject, for_machine: MachineChoice,
+ sources: T.List['SourceOutputs'], structured_sources: T.Optional[StructuredSources],
+ objects, environment: environment.Environment, compilers: T.Dict[str, 'Compiler'], kwargs):
+ super().__init__(name, subdir, subproject, True, for_machine, environment)
+ self.all_compilers = compilers
+ self.compilers = OrderedDict() # type: OrderedDict[str, Compiler]
+ self.objects: T.List[T.Union[str, 'File', 'ExtractedObjects']] = []
+ self.structured_sources = structured_sources
+ self.external_deps: T.List[dependencies.Dependency] = []
+ self.include_dirs: T.List['IncludeDirs'] = []
+ self.link_language = kwargs.get('link_language')
+ self.link_targets: T.List[LibTypes] = []
+ self.link_whole_targets: T.List[T.Union[StaticLibrary, CustomTarget, CustomTargetIndex]] = []
+ self.link_depends = []
+ self.added_deps = set()
+ self.name_prefix_set = False
+ self.name_suffix_set = False
+ self.filename = 'no_name'
+ # The list of all files outputted by this target. Useful in cases such
+ # as Vala which generates .vapi and .h besides the compiled output.
+ self.outputs = [self.filename]
+ self.need_install = False
+ self.pch: T.Dict[str, T.List[str]] = {}
+ self.extra_args: T.Dict[str, T.List['FileOrString']] = {}
+ self.sources: T.List[File] = []
+ self.generated: T.List['GeneratedTypes'] = []
+ self.d_features = defaultdict(list)
+ self.pic = False
+ self.pie = False
+ # Track build_rpath entries so we can remove them at install time
+ self.rpath_dirs_to_remove: T.Set[bytes] = set()
+ self.process_sourcelist(sources)
+ # Objects can be:
+ # 1. Pre-existing objects provided by the user with the `objects:` kwarg
+ # 2. Compiled objects created by and extracted from another target
+ self.process_objectlist(objects)
+ self.process_kwargs(kwargs)
+ self.check_unknown_kwargs(kwargs)
+ if not any([self.sources, self.generated, self.objects, self.link_whole_targets, self.structured_sources]):
+ mlog.warning(f'Build target {name} has no sources. '
+ 'This was never supposed to be allowed but did because of a bug, '
+ 'support will be removed in a future release of Meson')
+ self.validate_install()
+ self.check_module_linking()
+
+ def post_init(self) -> None:
+ ''' Initialisations and checks requiring the final list of compilers to be known
+ '''
+ self.validate_sources()
+ if self.structured_sources and any([self.sources, self.generated]):
+ raise MesonException('cannot mix structured sources and unstructured sources')
+ if self.structured_sources and 'rust' not in self.compilers:
+ raise MesonException('structured sources are only supported in Rust targets')
+
+ def __repr__(self):
+ repr_str = "<{0} {1}: {2}>"
+ return repr_str.format(self.__class__.__name__, self.get_id(), self.filename)
+
+ def __str__(self):
+ return f"{self.name}"
+
+ @property
+ def is_unity(self) -> bool:
+ unity_opt = self.get_option(OptionKey('unity'))
+ return unity_opt == 'on' or (unity_opt == 'subprojects' and self.subproject != '')
+
+ def validate_install(self):
+ if self.for_machine is MachineChoice.BUILD and self.need_install:
+ if self.environment.is_cross_build():
+ raise InvalidArguments('Tried to install a target for the build machine in a cross build.')
+ else:
+ mlog.warning('Installing target build for the build machine. This will fail in a cross build.')
+
+ def check_unknown_kwargs(self, kwargs):
+ # Override this method in derived classes that have more
+ # keywords.
+ self.check_unknown_kwargs_int(kwargs, self.known_kwargs)
+
+ def check_unknown_kwargs_int(self, kwargs, known_kwargs):
+ unknowns = []
+ for k in kwargs:
+ if k not in known_kwargs:
+ unknowns.append(k)
+ if len(unknowns) > 0:
+ mlog.warning('Unknown keyword argument(s) in target {}: {}.'.format(self.name, ', '.join(unknowns)))
+
+ def process_objectlist(self, objects):
+ assert isinstance(objects, list)
+ for s in objects:
+ if isinstance(s, (str, File, ExtractedObjects)):
+ self.objects.append(s)
+ elif isinstance(s, (GeneratedList, CustomTarget)):
+ msg = 'Generated files are not allowed in the \'objects\' kwarg ' + \
+ f'for target {self.name!r}.\nIt is meant only for ' + \
+ 'pre-built object files that are shipped with the\nsource ' + \
+ 'tree. Try adding it in the list of sources.'
+ raise InvalidArguments(msg)
+ else:
+ raise InvalidArguments(f'Bad object of type {type(s).__name__!r} in target {self.name!r}.')
+
+ def process_sourcelist(self, sources: T.List['SourceOutputs']) -> None:
+ """Split sources into generated and static sources.
+
+ Sources can be:
+ 1. Pre-existing source files in the source tree (static)
+ 2. Pre-existing sources generated by configure_file in the build tree.
+ (static as they are only regenerated if meson itself is regenerated)
+ 3. Sources files generated by another target or a Generator (generated)
+ """
+ added_sources: T.Set[File] = set() # If the same source is defined multiple times, use it only once.
+ for s in sources:
+ if isinstance(s, File):
+ if s not in added_sources:
+ self.sources.append(s)
+ added_sources.add(s)
+ elif isinstance(s, (CustomTarget, CustomTargetIndex, GeneratedList)):
+ self.generated.append(s)
+
+ @staticmethod
+ def can_compile_remove_sources(compiler: 'Compiler', sources: T.List['FileOrString']) -> bool:
+ removed = False
+ for s in sources[:]:
+ if compiler.can_compile(s):
+ sources.remove(s)
+ removed = True
+ return removed
+
+ def process_compilers_late(self, extra_languages: T.List[str]):
+ """Processes additional compilers after kwargs have been evaluated.
+
+ This can add extra compilers that might be required by keyword
+ arguments, such as link_with or dependencies. It will also try to guess
+ which compiler to use if one hasn't been selected already.
+ """
+ for lang in extra_languages:
+ self.compilers[lang] = self.all_compilers[lang]
+
+ # did user override clink_langs for this target?
+ link_langs = [self.link_language] if self.link_language else clink_langs
+
+ # If this library is linked against another library we need to consider
+ # the languages of those libraries as well.
+ if self.link_targets or self.link_whole_targets:
+ for t in itertools.chain(self.link_targets, self.link_whole_targets):
+ if isinstance(t, (CustomTarget, CustomTargetIndex)):
+ continue # We can't know anything about these.
+ for name, compiler in t.compilers.items():
+ if name in link_langs and name not in self.compilers:
+ self.compilers[name] = compiler
+
+ if not self.compilers:
+ # No source files or parent targets, target consists of only object
+ # files of unknown origin. Just add the first clink compiler
+ # that we have and hope that it can link these objects
+ for lang in link_langs:
+ if lang in self.all_compilers:
+ self.compilers[lang] = self.all_compilers[lang]
+ break
+
+ # Now that we have the final list of compilers we can sort it according
+ # to clink_langs and do sanity checks.
+ self.compilers = OrderedDict(sorted(self.compilers.items(),
+ key=lambda t: sort_clink(t[0])))
+ self.post_init()
+
+ def process_compilers(self) -> T.List[str]:
+ '''
+ Populate self.compilers, which is the list of compilers that this
+ target will use for compiling all its sources.
+ We also add compilers that were used by extracted objects to simplify
+ dynamic linker determination.
+ Returns a list of missing languages that we can add implicitly, such as
+ C/C++ compiler for cython.
+ '''
+ missing_languages: T.List[str] = []
+ if not any([self.sources, self.generated, self.objects, self.structured_sources]):
+ return missing_languages
+ # Pre-existing sources
+ sources: T.List['FileOrString'] = list(self.sources)
+ generated = self.generated.copy()
+
+ if self.structured_sources:
+ for v in self.structured_sources.sources.values():
+ for src in v:
+ if isinstance(src, (str, File)):
+ sources.append(src)
+ else:
+ generated.append(src)
+
+ # All generated sources
+ for gensrc in generated:
+ for s in gensrc.get_outputs():
+ # Generated objects can't be compiled, so don't use them for
+ # compiler detection. If our target only has generated objects,
+ # we will fall back to using the first c-like compiler we find,
+ # which is what we need.
+ if not is_object(s):
+ sources.append(s)
+ for d in self.external_deps:
+ for s in d.sources:
+ if isinstance(s, (str, File)):
+ sources.append(s)
+
+ # Sources that were used to create our extracted objects
+ for o in self.objects:
+ if not isinstance(o, ExtractedObjects):
+ continue
+ compsrcs = o.classify_all_sources(o.srclist, [])
+ for comp in compsrcs:
+ # Don't add Vala sources since that will pull in the Vala
+ # compiler even though we will never use it since we are
+ # dealing with compiled C code.
+ if comp.language == 'vala':
+ continue
+ if comp.language not in self.compilers:
+ self.compilers[comp.language] = comp
+ if sources:
+ # For each source, try to add one compiler that can compile it.
+ #
+ # If it has a suffix that belongs to a known language, we must have
+ # a compiler for that language.
+ #
+ # Otherwise, it's ok if no compilers can compile it, because users
+ # are expected to be able to add arbitrary non-source files to the
+ # sources list
+ for s in sources:
+ for lang, compiler in self.all_compilers.items():
+ if compiler.can_compile(s):
+ if lang not in self.compilers:
+ self.compilers[lang] = compiler
+ break
+ else:
+ if is_known_suffix(s):
+ path = pathlib.Path(str(s)).as_posix()
+ m = f'No {self.for_machine.get_lower_case_name()} machine compiler for {path!r}'
+ raise MesonException(m)
+
+ # If all our sources are Vala, our target also needs the C compiler but
+ # it won't get added above.
+ if 'vala' in self.compilers and 'c' not in self.compilers:
+ self.compilers['c'] = self.all_compilers['c']
+ if 'cython' in self.compilers:
+ key = OptionKey('language', machine=self.for_machine, lang='cython')
+ value = self.get_option(key)
+
+ try:
+ self.compilers[value] = self.all_compilers[value]
+ except KeyError:
+ missing_languages.append(value)
+
+ return missing_languages
+
+ def validate_sources(self):
+ if len(self.compilers) > 1 and any(lang in self.compilers for lang in ['cs', 'java']):
+ langs = ', '.join(self.compilers.keys())
+ raise InvalidArguments(f'Cannot mix those languages into a target: {langs}')
+
+ def process_link_depends(self, sources):
+ """Process the link_depends keyword argument.
+
+ This is designed to handle strings, Files, and the output of Custom
+ Targets. Notably it doesn't handle generator() returned objects, since
+ adding them as a link depends would inherently cause them to be
+ generated twice, since the output needs to be passed to the ld_args and
+ link_depends.
+ """
+ sources = listify(sources)
+ for s in sources:
+ if isinstance(s, File):
+ self.link_depends.append(s)
+ elif isinstance(s, str):
+ self.link_depends.append(
+ File.from_source_file(self.environment.source_dir, self.subdir, s))
+ elif hasattr(s, 'get_outputs'):
+ self.link_depends.append(s)
+ else:
+ raise InvalidArguments(
+ 'Link_depends arguments must be strings, Files, '
+ 'or a Custom Target, or lists thereof.')
+
+ def get_original_kwargs(self):
+ return self.kwargs
+
+ def copy_kwargs(self, kwargs):
+ self.kwargs = copy.copy(kwargs)
+ for k, v in self.kwargs.items():
+ if isinstance(v, list):
+ self.kwargs[k] = listify(v, flatten=True)
+ for t in ['dependencies', 'link_with', 'include_directories', 'sources']:
+ if t in self.kwargs:
+ self.kwargs[t] = listify(self.kwargs[t], flatten=True)
+
+ def extract_objects(self, srclist: T.List[T.Union['FileOrString', 'GeneratedTypes']]) -> ExtractedObjects:
+ sources_set = set(self.sources)
+ generated_set = set(self.generated)
+
+ obj_src: T.List['File'] = []
+ obj_gen: T.List['GeneratedTypes'] = []
+ for src in srclist:
+ if isinstance(src, (str, File)):
+ if isinstance(src, str):
+ src = File(False, self.subdir, src)
+ else:
+ FeatureNew.single_use('File argument for extract_objects', '0.50.0', self.subproject)
+ if src not in sources_set:
+ raise MesonException(f'Tried to extract unknown source {src}.')
+ obj_src.append(src)
+ elif isinstance(src, (CustomTarget, CustomTargetIndex, GeneratedList)):
+ FeatureNew.single_use('Generated sources for extract_objects', '0.61.0', self.subproject)
+ target = src.target if isinstance(src, CustomTargetIndex) else src
+ if src not in generated_set and target not in generated_set:
+ raise MesonException(f'Tried to extract unknown source {target.get_basename()}.')
+ obj_gen.append(src)
+ else:
+ raise MesonException(f'Object extraction arguments must be strings, Files or targets (got {type(src).__name__}).')
+ return ExtractedObjects(self, obj_src, obj_gen)
+
+ def extract_all_objects(self, recursive: bool = True) -> ExtractedObjects:
+ return ExtractedObjects(self, self.sources, self.generated, self.objects,
+ recursive)
+
+ def get_all_link_deps(self) -> ImmutableListProtocol[BuildTargetTypes]:
+ return self.get_transitive_link_deps()
+
+ @lru_cache(maxsize=None)
+ def get_transitive_link_deps(self) -> ImmutableListProtocol[BuildTargetTypes]:
+ result: T.List[Target] = []
+ for i in self.link_targets:
+ result += i.get_all_link_deps()
+ return result
+
+ def get_link_deps_mapping(self, prefix: str) -> T.Mapping[str, str]:
+ return self.get_transitive_link_deps_mapping(prefix)
+
+ @lru_cache(maxsize=None)
+ def get_transitive_link_deps_mapping(self, prefix: str) -> T.Mapping[str, str]:
+ result: T.Dict[str, str] = {}
+ for i in self.link_targets:
+ mapping = i.get_link_deps_mapping(prefix)
+ #we are merging two dictionaries, while keeping the earlier one dominant
+ result_tmp = mapping.copy()
+ result_tmp.update(result)
+ result = result_tmp
+ return result
+
+ @lru_cache(maxsize=None)
+ def get_link_dep_subdirs(self) -> T.AbstractSet[str]:
+ result: OrderedSet[str] = OrderedSet()
+ for i in self.link_targets:
+ if not isinstance(i, StaticLibrary):
+ result.add(i.get_subdir())
+ result.update(i.get_link_dep_subdirs())
+ return result
+
+ def get_default_install_dir(self) -> T.Tuple[str, str]:
+ return self.environment.get_libdir(), '{libdir}'
+
+ def get_custom_install_dir(self) -> T.List[T.Union[str, Literal[False]]]:
+ return self.install_dir
+
+ def get_custom_install_mode(self) -> T.Optional['FileMode']:
+ return self.install_mode
+
+ def process_kwargs(self, kwargs):
+ self.process_kwargs_base(kwargs)
+ self.copy_kwargs(kwargs)
+ kwargs.get('modules', [])
+ self.need_install = kwargs.get('install', self.need_install)
+ llist = extract_as_list(kwargs, 'link_with')
+ for linktarget in llist:
+ if isinstance(linktarget, dependencies.ExternalLibrary):
+ raise MesonException(textwrap.dedent('''\
+ An external library was used in link_with keyword argument, which
+ is reserved for libraries built as part of this project. External
+ libraries must be passed using the dependencies keyword argument
+ instead, because they are conceptually "external dependencies",
+ just like those detected with the dependency() function.
+ '''))
+ self.link(linktarget)
+ lwhole = extract_as_list(kwargs, 'link_whole')
+ for linktarget in lwhole:
+ self.link_whole(linktarget)
+
+ for lang in all_languages:
+ lang_args = extract_as_list(kwargs, f'{lang}_args')
+ self.add_compiler_args(lang, lang_args)
+
+ self.add_pch('c', extract_as_list(kwargs, 'c_pch'))
+ self.add_pch('cpp', extract_as_list(kwargs, 'cpp_pch'))
+
+ if not isinstance(self, Executable) or kwargs.get('export_dynamic', False):
+ self.vala_header = kwargs.get('vala_header', self.name + '.h')
+ self.vala_vapi = kwargs.get('vala_vapi', self.name + '.vapi')
+ self.vala_gir = kwargs.get('vala_gir', None)
+
+ dfeatures = defaultdict(list)
+ dfeature_unittest = kwargs.get('d_unittest', False)
+ if dfeature_unittest:
+ dfeatures['unittest'] = dfeature_unittest
+ dfeature_versions = kwargs.get('d_module_versions', [])
+ if dfeature_versions:
+ dfeatures['versions'] = dfeature_versions
+ dfeature_debug = kwargs.get('d_debug', [])
+ if dfeature_debug:
+ dfeatures['debug'] = dfeature_debug
+ if 'd_import_dirs' in kwargs:
+ dfeature_import_dirs = extract_as_list(kwargs, 'd_import_dirs')
+ for d in dfeature_import_dirs:
+ if not isinstance(d, IncludeDirs):
+ raise InvalidArguments('Arguments to d_import_dirs must be include_directories.')
+ dfeatures['import_dirs'] = dfeature_import_dirs
+ if dfeatures:
+ self.d_features = dfeatures
+
+ self.link_args = extract_as_list(kwargs, 'link_args')
+ for i in self.link_args:
+ if not isinstance(i, str):
+ raise InvalidArguments('Link_args arguments must be strings.')
+ for l in self.link_args:
+ if '-Wl,-rpath' in l or l.startswith('-rpath'):
+ mlog.warning(textwrap.dedent('''\
+ Please do not define rpath with a linker argument, use install_rpath
+ or build_rpath properties instead.
+ This will become a hard error in a future Meson release.
+ '''))
+ self.process_link_depends(kwargs.get('link_depends', []))
+ # Target-specific include dirs must be added BEFORE include dirs from
+ # internal deps (added inside self.add_deps()) to override them.
+ inclist = extract_as_list(kwargs, 'include_directories')
+ self.add_include_dirs(inclist)
+ # Add dependencies (which also have include_directories)
+ deplist = extract_as_list(kwargs, 'dependencies')
+ self.add_deps(deplist)
+ # If an item in this list is False, the output corresponding to
+ # the list index of that item will not be installed
+ self.install_dir = typeslistify(kwargs.get('install_dir', []),
+ (str, bool))
+ self.install_mode = kwargs.get('install_mode', None)
+ self.install_tag = stringlistify(kwargs.get('install_tag', [None]))
+ main_class = kwargs.get('main_class', '')
+ if not isinstance(main_class, str):
+ raise InvalidArguments('Main class must be a string')
+ self.main_class = main_class
+ if isinstance(self, Executable):
+ # This kwarg is deprecated. The value of "none" means that the kwarg
+ # was not specified and win_subsystem should be used instead.
+ self.gui_app = None
+ if 'gui_app' in kwargs:
+ if 'win_subsystem' in kwargs:
+ raise InvalidArguments('Can specify only gui_app or win_subsystem for a target, not both.')
+ self.gui_app = kwargs['gui_app']
+ if not isinstance(self.gui_app, bool):
+ raise InvalidArguments('Argument gui_app must be boolean.')
+ self.win_subsystem = self.validate_win_subsystem(kwargs.get('win_subsystem', 'console'))
+ elif 'gui_app' in kwargs:
+ raise InvalidArguments('Argument gui_app can only be used on executables.')
+ elif 'win_subsystem' in kwargs:
+ raise InvalidArguments('Argument win_subsystem can only be used on executables.')
+ extra_files = extract_as_list(kwargs, 'extra_files')
+ for i in extra_files:
+ assert isinstance(i, File)
+ trial = os.path.join(self.environment.get_source_dir(), i.subdir, i.fname)
+ if not os.path.isfile(trial):
+ raise InvalidArguments(f'Tried to add non-existing extra file {i}.')
+ self.extra_files = extra_files
+ self.install_rpath: str = kwargs.get('install_rpath', '')
+ if not isinstance(self.install_rpath, str):
+ raise InvalidArguments('Install_rpath is not a string.')
+ self.build_rpath = kwargs.get('build_rpath', '')
+ if not isinstance(self.build_rpath, str):
+ raise InvalidArguments('Build_rpath is not a string.')
+ resources = extract_as_list(kwargs, 'resources')
+ for r in resources:
+ if not isinstance(r, str):
+ raise InvalidArguments('Resource argument is not a string.')
+ trial = os.path.join(self.environment.get_source_dir(), self.subdir, r)
+ if not os.path.isfile(trial):
+ raise InvalidArguments(f'Tried to add non-existing resource {r}.')
+ self.resources = resources
+ if 'name_prefix' in kwargs:
+ name_prefix = kwargs['name_prefix']
+ if isinstance(name_prefix, list):
+ if name_prefix:
+ raise InvalidArguments('name_prefix array must be empty to signify default.')
+ else:
+ if not isinstance(name_prefix, str):
+ raise InvalidArguments('name_prefix must be a string.')
+ self.prefix = name_prefix
+ self.name_prefix_set = True
+ if 'name_suffix' in kwargs:
+ name_suffix = kwargs['name_suffix']
+ if isinstance(name_suffix, list):
+ if name_suffix:
+ raise InvalidArguments('name_suffix array must be empty to signify default.')
+ else:
+ if not isinstance(name_suffix, str):
+ raise InvalidArguments('name_suffix must be a string.')
+ if name_suffix == '':
+ raise InvalidArguments('name_suffix should not be an empty string. '
+ 'If you want meson to use the default behaviour '
+ 'for each platform pass `[]` (empty array)')
+ self.suffix = name_suffix
+ self.name_suffix_set = True
+ if isinstance(self, StaticLibrary):
+ # You can't disable PIC on OS X. The compiler ignores -fno-PIC.
+ # PIC is always on for Windows (all code is position-independent
+ # since library loading is done differently)
+ m = self.environment.machines[self.for_machine]
+ if m.is_darwin() or m.is_windows():
+ self.pic = True
+ else:
+ self.pic = self._extract_pic_pie(kwargs, 'pic', 'b_staticpic')
+ if isinstance(self, Executable) or (isinstance(self, StaticLibrary) and not self.pic):
+ # Executables must be PIE on Android
+ if self.environment.machines[self.for_machine].is_android():
+ self.pie = True
+ else:
+ self.pie = self._extract_pic_pie(kwargs, 'pie', 'b_pie')
+ self.implicit_include_directories = kwargs.get('implicit_include_directories', True)
+ if not isinstance(self.implicit_include_directories, bool):
+ raise InvalidArguments('Implicit_include_directories must be a boolean.')
+ self.gnu_symbol_visibility = kwargs.get('gnu_symbol_visibility', '')
+ if not isinstance(self.gnu_symbol_visibility, str):
+ raise InvalidArguments('GNU symbol visibility must be a string.')
+ if self.gnu_symbol_visibility != '':
+ permitted = ['default', 'internal', 'hidden', 'protected', 'inlineshidden']
+ if self.gnu_symbol_visibility not in permitted:
+ raise InvalidArguments('GNU symbol visibility arg {} not one of: {}'.format(self.gnu_symbol_visibility, ', '.join(permitted)))
+
+ def validate_win_subsystem(self, value: str) -> str:
+ value = value.lower()
+ if re.fullmatch(r'(boot_application|console|efi_application|efi_boot_service_driver|efi_rom|efi_runtime_driver|native|posix|windows)(,\d+(\.\d+)?)?', value) is None:
+ raise InvalidArguments(f'Invalid value for win_subsystem: {value}.')
+ return value
+
+ def _extract_pic_pie(self, kwargs, arg: str, option: str):
+ # Check if we have -fPIC, -fpic, -fPIE, or -fpie in cflags
+ all_flags = self.extra_args['c'] + self.extra_args['cpp']
+ if '-f' + arg.lower() in all_flags or '-f' + arg.upper() in all_flags:
+ mlog.warning(f"Use the '{arg}' kwarg instead of passing '-f{arg}' manually to {self.name!r}")
+ return True
+
+ k = OptionKey(option)
+ if arg in kwargs:
+ val = kwargs[arg]
+ elif k in self.environment.coredata.options:
+ val = self.environment.coredata.options[k].value
+ else:
+ val = False
+
+ if not isinstance(val, bool):
+ raise InvalidArguments(f'Argument {arg} to {self.name!r} must be boolean')
+ return val
+
+ def get_filename(self) -> str:
+ return self.filename
+
+ def get_outputs(self) -> T.List[str]:
+ return self.outputs
+
+ def get_extra_args(self, language):
+ return self.extra_args.get(language, [])
+
+ def get_dependencies(self, exclude=None):
+ transitive_deps = []
+ if exclude is None:
+ exclude = []
+ for t in itertools.chain(self.link_targets, self.link_whole_targets):
+ if t in transitive_deps or t in exclude:
+ continue
+ transitive_deps.append(t)
+ if isinstance(t, StaticLibrary):
+ transitive_deps += t.get_dependencies(transitive_deps + exclude)
+ return transitive_deps
+
+ def get_source_subdir(self):
+ return self.subdir
+
+ def get_sources(self):
+ return self.sources
+
+ def get_objects(self) -> T.List[T.Union[str, 'File', 'ExtractedObjects']]:
+ return self.objects
+
+ def get_generated_sources(self) -> T.List['GeneratedTypes']:
+ return self.generated
+
+ def should_install(self) -> bool:
+ return self.need_install
+
+ def has_pch(self) -> bool:
+ return bool(self.pch)
+
+ def get_pch(self, language: str) -> T.List[str]:
+ return self.pch.get(language, [])
+
+ def get_include_dirs(self) -> T.List['IncludeDirs']:
+ return self.include_dirs
+
+ def add_deps(self, deps):
+ deps = listify(deps)
+ for dep in deps:
+ if dep in self.added_deps:
+ continue
+
+ if isinstance(dep, dependencies.InternalDependency):
+ # Those parts that are internal.
+ self.process_sourcelist(dep.sources)
+ self.add_include_dirs(dep.include_directories, dep.get_include_type())
+ for l in dep.libraries:
+ self.link(l)
+ for l in dep.whole_libraries:
+ self.link_whole(l)
+ if dep.get_compile_args() or dep.get_link_args():
+ # Those parts that are external.
+ extpart = dependencies.InternalDependency('undefined',
+ [],
+ dep.get_compile_args(),
+ dep.get_link_args(),
+ [], [], [], [], {}, [], [])
+ self.external_deps.append(extpart)
+ # Deps of deps.
+ self.add_deps(dep.ext_deps)
+ elif isinstance(dep, dependencies.Dependency):
+ if dep not in self.external_deps:
+ self.external_deps.append(dep)
+ self.process_sourcelist(dep.get_sources())
+ self.add_deps(dep.ext_deps)
+ elif isinstance(dep, BuildTarget):
+ raise InvalidArguments('''Tried to use a build target as a dependency.
+You probably should put it in link_with instead.''')
+ else:
+ # This is a bit of a hack. We do not want Build to know anything
+ # about the interpreter so we can't import it and use isinstance.
+ # This should be reliable enough.
+ if hasattr(dep, 'held_object'):
+ # FIXME: subproject is not a real ObjectHolder so we have to do this by hand
+ dep = dep.held_object
+ if hasattr(dep, 'project_args_frozen') or hasattr(dep, 'global_args_frozen'):
+ raise InvalidArguments('Tried to use subproject object as a dependency.\n'
+ 'You probably wanted to use a dependency declared in it instead.\n'
+ 'Access it by calling get_variable() on the subproject object.')
+ raise InvalidArguments(f'Argument is of an unacceptable type {type(dep).__name__!r}.\nMust be '
+ 'either an external dependency (returned by find_library() or '
+ 'dependency()) or an internal dependency (returned by '
+ 'declare_dependency()).')
+
+ dep_d_features = dep.d_features
+
+ for feature in ('versions', 'import_dirs'):
+ if feature in dep_d_features:
+ self.d_features[feature].extend(dep_d_features[feature])
+
+ self.added_deps.add(dep)
+
+ def get_external_deps(self) -> T.List[dependencies.Dependency]:
+ return self.external_deps
+
+ def is_internal(self) -> bool:
+ return False
+
+ def link(self, target):
+ for t in listify(target):
+ if isinstance(self, StaticLibrary) and self.need_install:
+ if isinstance(t, (CustomTarget, CustomTargetIndex)):
+ if not t.should_install():
+ mlog.warning(f'Try to link an installed static library target {self.name} with a'
+ 'custom target that is not installed, this might cause problems'
+ 'when you try to use this static library')
+ elif t.is_internal() and not t.uses_rust():
+ # When we're a static library and we link_with to an
+ # internal/convenience library, promote to link_whole.
+ #
+ # There are cases we cannot do this, however. In Rust, for
+ # example, this can't be done with Rust ABI libraries, though
+ # it could be done with C ABI libraries, though there are
+ # several meson issues that need to be fixed:
+ # https://github.com/mesonbuild/meson/issues/10722
+ # https://github.com/mesonbuild/meson/issues/10723
+ # https://github.com/mesonbuild/meson/issues/10724
+ return self.link_whole(t)
+ if not isinstance(t, (Target, CustomTargetIndex)):
+ raise InvalidArguments(f'{t!r} is not a target.')
+ if not t.is_linkable_target():
+ raise InvalidArguments(f"Link target '{t!s}' is not linkable.")
+ if isinstance(self, SharedLibrary) and isinstance(t, StaticLibrary) and not t.pic:
+ msg = f"Can't link non-PIC static library {t.name!r} into shared library {self.name!r}. "
+ msg += "Use the 'pic' option to static_library to build with PIC."
+ raise InvalidArguments(msg)
+ if self.for_machine is not t.for_machine:
+ msg = f'Tried to mix libraries for machines {self.for_machine} and {t.for_machine} in target {self.name!r}'
+ if self.environment.is_cross_build():
+ raise InvalidArguments(msg + ' This is not possible in a cross build.')
+ else:
+ mlog.warning(msg + ' This will fail in cross build.')
+ self.link_targets.append(t)
+
+ def link_whole(self, target):
+ for t in listify(target):
+ if isinstance(t, (CustomTarget, CustomTargetIndex)):
+ if not t.is_linkable_target():
+ raise InvalidArguments(f'Custom target {t!r} is not linkable.')
+ if t.links_dynamically():
+ raise InvalidArguments('Can only link_whole custom targets that are static archives.')
+ if isinstance(self, StaticLibrary):
+ # FIXME: We could extract the .a archive to get object files
+ raise InvalidArguments('Cannot link_whole a custom target into a static library')
+ elif not isinstance(t, StaticLibrary):
+ raise InvalidArguments(f'{t!r} is not a static library.')
+ elif isinstance(self, SharedLibrary) and not t.pic:
+ msg = f"Can't link non-PIC static library {t.name!r} into shared library {self.name!r}. "
+ msg += "Use the 'pic' option to static_library to build with PIC."
+ raise InvalidArguments(msg)
+ if self.for_machine is not t.for_machine:
+ msg = f'Tried to mix libraries for machines {self.for_machine} and {t.for_machine} in target {self.name!r}'
+ if self.environment.is_cross_build():
+ raise InvalidArguments(msg + ' This is not possible in a cross build.')
+ else:
+ mlog.warning(msg + ' This will fail in cross build.')
+ if isinstance(self, StaticLibrary):
+ # When we're a static library and we link_whole: to another static
+ # library, we need to add that target's objects to ourselves.
+ self.objects += t.extract_all_objects_recurse()
+ self.link_whole_targets.append(t)
+
+ def extract_all_objects_recurse(self) -> T.List[T.Union[str, 'ExtractedObjects']]:
+ objs = [self.extract_all_objects()]
+ for t in self.link_targets:
+ if t.is_internal():
+ objs += t.extract_all_objects_recurse()
+ return objs
+
+ def add_pch(self, language: str, pchlist: T.List[str]) -> None:
+ if not pchlist:
+ return
+ elif len(pchlist) == 1:
+ if not environment.is_header(pchlist[0]):
+ raise InvalidArguments(f'PCH argument {pchlist[0]} is not a header.')
+ elif len(pchlist) == 2:
+ if environment.is_header(pchlist[0]):
+ if not environment.is_source(pchlist[1]):
+ raise InvalidArguments('PCH definition must contain one header and at most one source.')
+ elif environment.is_source(pchlist[0]):
+ if not environment.is_header(pchlist[1]):
+ raise InvalidArguments('PCH definition must contain one header and at most one source.')
+ pchlist = [pchlist[1], pchlist[0]]
+ else:
+ raise InvalidArguments(f'PCH argument {pchlist[0]} is of unknown type.')
+
+ if os.path.dirname(pchlist[0]) != os.path.dirname(pchlist[1]):
+ raise InvalidArguments('PCH files must be stored in the same folder.')
+
+ FeatureDeprecated.single_use('PCH source files', '0.50.0', self.subproject,
+ 'Only a single header file should be used.')
+ elif len(pchlist) > 2:
+ raise InvalidArguments('PCH definition may have a maximum of 2 files.')
+ for f in pchlist:
+ if not isinstance(f, str):
+ raise MesonException('PCH arguments must be strings.')
+ if not os.path.isfile(os.path.join(self.environment.source_dir, self.subdir, f)):
+ raise MesonException(f'File {f} does not exist.')
+ self.pch[language] = pchlist
+
+ def add_include_dirs(self, args: T.Sequence['IncludeDirs'], set_is_system: T.Optional[str] = None) -> None:
+ ids: T.List['IncludeDirs'] = []
+ for a in args:
+ if not isinstance(a, IncludeDirs):
+ raise InvalidArguments('Include directory to be added is not an include directory object.')
+ ids.append(a)
+ if set_is_system is None:
+ set_is_system = 'preserve'
+ if set_is_system != 'preserve':
+ is_system = set_is_system == 'system'
+ ids = [IncludeDirs(x.get_curdir(), x.get_incdirs(), is_system, x.get_extra_build_dirs()) for x in ids]
+ self.include_dirs += ids
+
+ def add_compiler_args(self, language: str, args: T.List['FileOrString']) -> None:
+ args = listify(args)
+ for a in args:
+ if not isinstance(a, (str, File)):
+ raise InvalidArguments('A non-string passed to compiler args.')
+ if language in self.extra_args:
+ self.extra_args[language] += args
+ else:
+ self.extra_args[language] = args
+
+ def get_aliases(self) -> T.List[T.Tuple[str, str, str]]:
+ return []
+
+ def get_langs_used_by_deps(self) -> T.List[str]:
+ '''
+ Sometimes you want to link to a C++ library that exports C API, which
+ means the linker must link in the C++ stdlib, and we must use a C++
+ compiler for linking. The same is also applicable for objc/objc++, etc,
+ so we can keep using clink_langs for the priority order.
+
+ See: https://github.com/mesonbuild/meson/issues/1653
+ '''
+ langs = [] # type: T.List[str]
+
+ # Check if any of the external libraries were written in this language
+ for dep in self.external_deps:
+ if dep.language is None:
+ continue
+ if dep.language not in langs:
+ langs.append(dep.language)
+ # Check if any of the internal libraries this target links to were
+ # written in this language
+ for link_target in itertools.chain(self.link_targets, self.link_whole_targets):
+ if isinstance(link_target, (CustomTarget, CustomTargetIndex)):
+ continue
+ for language in link_target.compilers:
+ if language not in langs:
+ langs.append(language)
+
+ return langs
+
+ def get_prelinker(self):
+ if self.link_language:
+ comp = self.all_compilers[self.link_language]
+ return comp
+ for l in clink_langs:
+ if l in self.compilers:
+ try:
+ prelinker = self.all_compilers[l]
+ except KeyError:
+ raise MesonException(
+ f'Could not get a prelinker linker for build target {self.name!r}. '
+ f'Requires a compiler for language "{l}", but that is not '
+ 'a project language.')
+ return prelinker
+ raise MesonException(f'Could not determine prelinker for {self.name!r}.')
+
+ def get_clink_dynamic_linker_and_stdlibs(self) -> T.Tuple['Compiler', T.List[str]]:
+ '''
+ We use the order of languages in `clink_langs` to determine which
+ linker to use in case the target has sources compiled with multiple
+ compilers. All languages other than those in this list have their own
+ linker.
+ Note that Vala outputs C code, so Vala sources can use any linker
+ that can link compiled C. We don't actually need to add an exception
+ for Vala here because of that.
+ '''
+ # If the user set the link_language, just return that.
+ if self.link_language:
+ comp = self.all_compilers[self.link_language]
+ return comp, comp.language_stdlib_only_link_flags(self.environment)
+
+ # Since dependencies could come from subprojects, they could have
+ # languages we don't have in self.all_compilers. Use the global list of
+ # all compilers here.
+ all_compilers = self.environment.coredata.compilers[self.for_machine]
+
+ # Languages used by dependencies
+ dep_langs = self.get_langs_used_by_deps()
+
+ # This set contains all the languages a linker can link natively
+ # without extra flags. For instance, nvcc (cuda) can link C++
+ # without injecting -lc++/-lstdc++, see
+ # https://github.com/mesonbuild/meson/issues/10570
+ MASK_LANGS = frozenset([
+ # (language, linker)
+ ('cpp', 'cuda'),
+ ])
+ # Pick a compiler based on the language priority-order
+ for l in clink_langs:
+ if l in self.compilers or l in dep_langs:
+ try:
+ linker = all_compilers[l]
+ except KeyError:
+ raise MesonException(
+ f'Could not get a dynamic linker for build target {self.name!r}. '
+ f'Requires a linker for language "{l}", but that is not '
+ 'a project language.')
+ stdlib_args: T.List[str] = []
+ for dl in itertools.chain(self.compilers, dep_langs):
+ if dl != linker.language and (dl, linker.language) not in MASK_LANGS:
+ stdlib_args += all_compilers[dl].language_stdlib_only_link_flags(self.environment)
+ # Type of var 'linker' is Compiler.
+ # Pretty hard to fix because the return value is passed everywhere
+ return linker, stdlib_args
+
+ # None of our compilers can do clink, this happens for example if the
+ # target only has ASM sources. Pick the first capable compiler.
+ for l in clink_langs:
+ try:
+ comp = self.all_compilers[l]
+ return comp, comp.language_stdlib_only_link_flags(self.environment)
+ except KeyError:
+ pass
+
+ raise AssertionError(f'Could not get a dynamic linker for build target {self.name!r}')
+
+ def uses_rust(self) -> bool:
+ return 'rust' in self.compilers
+
+ def uses_fortran(self) -> bool:
+ return 'fortran' in self.compilers
+
+ def get_using_msvc(self) -> bool:
+ '''
+ Check if the dynamic linker is MSVC. Used by Executable, StaticLibrary,
+ and SharedLibrary for deciding when to use MSVC-specific file naming
+ and debug filenames.
+
+ If at least some code is built with MSVC and the final library is
+ linked with MSVC, we can be sure that some debug info will be
+ generated. We only check the dynamic linker here because the static
+ linker is guaranteed to be of the same type.
+
+ Interesting cases:
+ 1. The Vala compiler outputs C code to be compiled by whatever
+ C compiler we're using, so all objects will still be created by the
+ MSVC compiler.
+ 2. If the target contains only objects, process_compilers guesses and
+ picks the first compiler that smells right.
+ '''
+ # Rustc can use msvc style linkers
+ if self.uses_rust():
+ compiler = self.all_compilers['rust']
+ else:
+ compiler, _ = self.get_clink_dynamic_linker_and_stdlibs()
+ # Mixing many languages with MSVC is not supported yet so ignore stdlibs.
+ return compiler and compiler.get_linker_id() in {'link', 'lld-link', 'xilink', 'optlink'}
+
+ def check_module_linking(self):
+ '''
+ Warn if shared modules are linked with target: (link_with) #2865
+ '''
+ for link_target in self.link_targets:
+ if isinstance(link_target, SharedModule) and not link_target.force_soname:
+ if self.environment.machines[self.for_machine].is_darwin():
+ raise MesonException(
+ f'target {self.name} links against shared module {link_target.name}. This is not permitted on OSX')
+ elif self.environment.machines[self.for_machine].is_android() and isinstance(self, SharedModule):
+ # Android requires shared modules that use symbols from other shared modules to
+ # be linked before they can be dlopen()ed in the correct order. Not doing so
+ # leads to a missing symbol error: https://github.com/android/ndk/issues/201
+ link_target.force_soname = True
+ else:
+ mlog.deprecation(f'target {self.name} links against shared module {link_target.name}, which is incorrect.'
+ '\n '
+ f'This will be an error in the future, so please use shared_library() for {link_target.name} instead.'
+ '\n '
+ f'If shared_module() was used for {link_target.name} because it has references to undefined symbols,'
+ '\n '
+ 'use shared_libary() with `override_options: [\'b_lundef=false\']` instead.')
+ link_target.force_soname = True
+
+class Generator(HoldableObject):
+ def __init__(self, exe: T.Union['Executable', programs.ExternalProgram],
+ arguments: T.List[str],
+ output: T.List[str],
+ # how2dataclass
+ *,
+ depfile: T.Optional[str] = None,
+ capture: bool = False,
+ depends: T.Optional[T.List[T.Union[BuildTarget, 'CustomTarget']]] = None,
+ name: str = 'Generator'):
+ self.exe = exe
+ self.depfile = depfile
+ self.capture = capture
+ self.depends: T.List[T.Union[BuildTarget, 'CustomTarget']] = depends or []
+ self.arglist = arguments
+ self.outputs = output
+ self.name = name
+
+ def __repr__(self) -> str:
+ repr_str = "<{0}: {1}>"
+ return repr_str.format(self.__class__.__name__, self.exe)
+
+ def get_exe(self) -> T.Union['Executable', programs.ExternalProgram]:
+ return self.exe
+
+ def get_base_outnames(self, inname: str) -> T.List[str]:
+ plainname = os.path.basename(inname)
+ basename = os.path.splitext(plainname)[0]
+ bases = [x.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname) for x in self.outputs]
+ return bases
+
+ def get_dep_outname(self, inname: str) -> T.List[str]:
+ if self.depfile is None:
+ raise InvalidArguments('Tried to get dep name for rule that does not have dependency file defined.')
+ plainname = os.path.basename(inname)
+ basename = os.path.splitext(plainname)[0]
+ return self.depfile.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname)
+
+ def get_arglist(self, inname: str) -> T.List[str]:
+ plainname = os.path.basename(inname)
+ basename = os.path.splitext(plainname)[0]
+ return [x.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname) for x in self.arglist]
+
+ @staticmethod
+ def is_parent_path(parent: str, trial: str) -> bool:
+ relpath = pathlib.PurePath(trial).relative_to(parent)
+ return relpath.parts[0] != '..' # For subdirs we can only go "down".
+
+ def process_files(self, files: T.Iterable[T.Union[str, File, 'CustomTarget', 'CustomTargetIndex', 'GeneratedList']],
+ state: T.Union['Interpreter', 'ModuleState'],
+ preserve_path_from: T.Optional[str] = None,
+ extra_args: T.Optional[T.List[str]] = None) -> 'GeneratedList':
+ output = GeneratedList(self, state.subdir, preserve_path_from, extra_args=extra_args if extra_args is not None else [])
+
+ for e in files:
+ if isinstance(e, CustomTarget):
+ output.depends.add(e)
+ if isinstance(e, CustomTargetIndex):
+ output.depends.add(e.target)
+
+ if isinstance(e, (CustomTarget, CustomTargetIndex, GeneratedList)):
+ output.depends.add(e)
+ fs = [File.from_built_file(state.subdir, f) for f in e.get_outputs()]
+ elif isinstance(e, str):
+ fs = [File.from_source_file(state.environment.source_dir, state.subdir, e)]
+ else:
+ fs = [e]
+
+ for f in fs:
+ if preserve_path_from:
+ abs_f = f.absolute_path(state.environment.source_dir, state.environment.build_dir)
+ if not self.is_parent_path(preserve_path_from, abs_f):
+ raise InvalidArguments('generator.process: When using preserve_path_from, all input files must be in a subdirectory of the given dir.')
+ output.add_file(f, state)
+ return output
+
+
+@dataclass(eq=False)
+class GeneratedList(HoldableObject):
+
+ """The output of generator.process."""
+
+ generator: Generator
+ subdir: str
+ preserve_path_from: T.Optional[str]
+ extra_args: T.List[str]
+
+ def __post_init__(self) -> None:
+ self.name = self.generator.exe
+ self.depends: T.Set[GeneratedTypes] = set()
+ self.infilelist: T.List['File'] = []
+ self.outfilelist: T.List[str] = []
+ self.outmap: T.Dict[File, T.List[str]] = {}
+ self.extra_depends = [] # XXX: Doesn't seem to be used?
+ self.depend_files: T.List[File] = []
+
+ if self.extra_args is None:
+ self.extra_args: T.List[str] = []
+
+ if isinstance(self.generator.exe, programs.ExternalProgram):
+ if not self.generator.exe.found():
+ raise InvalidArguments('Tried to use not-found external program as generator')
+ path = self.generator.exe.get_path()
+ if os.path.isabs(path):
+ # Can only add a dependency on an external program which we
+ # know the absolute path of
+ self.depend_files.append(File.from_absolute_file(path))
+
+ def add_preserved_path_segment(self, infile: File, outfiles: T.List[str], state: T.Union['Interpreter', 'ModuleState']) -> T.List[str]:
+ result: T.List[str] = []
+ in_abs = infile.absolute_path(state.environment.source_dir, state.environment.build_dir)
+ assert os.path.isabs(self.preserve_path_from)
+ rel = os.path.relpath(in_abs, self.preserve_path_from)
+ path_segment = os.path.dirname(rel)
+ for of in outfiles:
+ result.append(os.path.join(path_segment, of))
+ return result
+
+ def add_file(self, newfile: File, state: T.Union['Interpreter', 'ModuleState']) -> None:
+ self.infilelist.append(newfile)
+ outfiles = self.generator.get_base_outnames(newfile.fname)
+ if self.preserve_path_from:
+ outfiles = self.add_preserved_path_segment(newfile, outfiles, state)
+ self.outfilelist += outfiles
+ self.outmap[newfile] = outfiles
+
+ def get_inputs(self) -> T.List['File']:
+ return self.infilelist
+
+ def get_outputs(self) -> T.List[str]:
+ return self.outfilelist
+
+ def get_outputs_for(self, filename: 'File') -> T.List[str]:
+ return self.outmap[filename]
+
+ def get_generator(self) -> 'Generator':
+ return self.generator
+
+ def get_extra_args(self) -> T.List[str]:
+ return self.extra_args
+
+ def get_subdir(self) -> str:
+ return self.subdir
+
+
+class Executable(BuildTarget):
+ known_kwargs = known_exe_kwargs
+
+ typename = 'executable'
+
+ def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice,
+ sources: T.List[SourceOutputs], structured_sources: T.Optional['StructuredSources'],
+ objects, environment: environment.Environment, compilers: T.Dict[str, 'Compiler'],
+ kwargs):
+ key = OptionKey('b_pie')
+ if 'pie' not in kwargs and key in environment.coredata.options:
+ kwargs['pie'] = environment.coredata.options[key].value
+ super().__init__(name, subdir, subproject, for_machine, sources, structured_sources, objects,
+ environment, compilers, kwargs)
+ # Check for export_dynamic
+ self.export_dynamic = kwargs.get('export_dynamic', False)
+ if not isinstance(self.export_dynamic, bool):
+ raise InvalidArguments('"export_dynamic" keyword argument must be a boolean')
+ self.implib = kwargs.get('implib')
+ if not isinstance(self.implib, (bool, str, type(None))):
+ raise InvalidArguments('"export_dynamic" keyword argument must be a boolean or string')
+ if self.implib:
+ self.export_dynamic = True
+ if self.export_dynamic and self.implib is False:
+ raise InvalidArguments('"implib" keyword argument must not be false for if "export_dynamic" is true')
+ # Only linkwithable if using export_dynamic
+ self.is_linkwithable = self.export_dynamic
+ # Remember that this exe was returned by `find_program()` through an override
+ self.was_returned_by_find_program = False
+
+ def post_init(self) -> None:
+ super().post_init()
+ machine = self.environment.machines[self.for_machine]
+ # Unless overridden, executables have no suffix or prefix. Except on
+ # Windows and with C#/Mono executables where the suffix is 'exe'
+ if not hasattr(self, 'prefix'):
+ self.prefix = ''
+ if not hasattr(self, 'suffix'):
+ # Executable for Windows or C#/Mono
+ if machine.is_windows() or machine.is_cygwin() or 'cs' in self.compilers:
+ self.suffix = 'exe'
+ elif machine.system.startswith('wasm') or machine.system == 'emscripten':
+ self.suffix = 'js'
+ elif ('c' in self.compilers and self.compilers['c'].get_id().startswith('armclang') or
+ 'cpp' in self.compilers and self.compilers['cpp'].get_id().startswith('armclang')):
+ self.suffix = 'axf'
+ elif ('c' in self.compilers and self.compilers['c'].get_id().startswith('ccrx') or
+ 'cpp' in self.compilers and self.compilers['cpp'].get_id().startswith('ccrx')):
+ self.suffix = 'abs'
+ elif ('c' in self.compilers and self.compilers['c'].get_id().startswith('xc16')):
+ self.suffix = 'elf'
+ elif ('c' in self.compilers and self.compilers['c'].get_id() in {'ti', 'c2000'} or
+ 'cpp' in self.compilers and self.compilers['cpp'].get_id() in {'ti', 'c2000'}):
+ self.suffix = 'out'
+ else:
+ self.suffix = machine.get_exe_suffix()
+ self.filename = self.name
+ if self.suffix:
+ self.filename += '.' + self.suffix
+ self.outputs = [self.filename]
+
+ # The import library this target will generate
+ self.import_filename = None
+ # The import library that Visual Studio would generate (and accept)
+ self.vs_import_filename = None
+ # The import library that GCC would generate (and prefer)
+ self.gcc_import_filename = None
+ # The debugging information file this target will generate
+ self.debug_filename = None
+
+ # If using export_dynamic, set the import library name
+ if self.export_dynamic:
+ implib_basename = self.name + '.exe'
+ if isinstance(self.implib, str):
+ implib_basename = self.implib
+ if machine.is_windows() or machine.is_cygwin():
+ self.vs_import_filename = f'{implib_basename}.lib'
+ self.gcc_import_filename = f'lib{implib_basename}.a'
+ if self.get_using_msvc():
+ self.import_filename = self.vs_import_filename
+ else:
+ self.import_filename = self.gcc_import_filename
+
+ if machine.is_windows() and ('cs' in self.compilers or
+ self.uses_rust() or
+ self.get_using_msvc()):
+ self.debug_filename = self.name + '.pdb'
+
+ def get_default_install_dir(self) -> T.Tuple[str, str]:
+ return self.environment.get_bindir(), '{bindir}'
+
+ def description(self):
+ '''Human friendly description of the executable'''
+ return self.name
+
+ def type_suffix(self):
+ return "@exe"
+
+ def get_import_filename(self) -> T.Optional[str]:
+ """
+ The name of the import library that will be outputted by the compiler
+
+ Returns None if there is no import library required for this platform
+ """
+ return self.import_filename
+
+ def get_import_filenameslist(self):
+ if self.import_filename:
+ return [self.vs_import_filename, self.gcc_import_filename]
+ return []
+
+ def get_debug_filename(self) -> T.Optional[str]:
+ """
+ The name of debuginfo file that will be created by the compiler
+
+ Returns None if the build won't create any debuginfo file
+ """
+ return self.debug_filename
+
+ def is_linkable_target(self):
+ return self.is_linkwithable
+
+ def get_command(self) -> 'ImmutableListProtocol[str]':
+ """Provides compatibility with ExternalProgram.
+
+ Since you can override ExternalProgram instances with Executables.
+ """
+ return self.outputs
+
+class StaticLibrary(BuildTarget):
+ known_kwargs = known_stlib_kwargs
+
+ typename = 'static library'
+
+ def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice,
+ sources: T.List[SourceOutputs], structured_sources: T.Optional['StructuredSources'],
+ objects, environment: environment.Environment, compilers: T.Dict[str, 'Compiler'],
+ kwargs):
+ self.prelink = kwargs.get('prelink', False)
+ if not isinstance(self.prelink, bool):
+ raise InvalidArguments('Prelink keyword argument must be a boolean.')
+ super().__init__(name, subdir, subproject, for_machine, sources, structured_sources, objects,
+ environment, compilers, kwargs)
+
+ def post_init(self) -> None:
+ super().post_init()
+ if 'cs' in self.compilers:
+ raise InvalidArguments('Static libraries not supported for C#.')
+ if 'rust' in self.compilers:
+ # If no crate type is specified, or it's the generic lib type, use rlib
+ if not hasattr(self, 'rust_crate_type') or self.rust_crate_type == 'lib':
+ mlog.debug('Defaulting Rust static library target crate type to rlib')
+ self.rust_crate_type = 'rlib'
+ # Don't let configuration proceed with a non-static crate type
+ elif self.rust_crate_type not in ['rlib', 'staticlib']:
+ raise InvalidArguments(f'Crate type "{self.rust_crate_type}" invalid for static libraries; must be "rlib" or "staticlib"')
+ # By default a static library is named libfoo.a even on Windows because
+ # MSVC does not have a consistent convention for what static libraries
+ # are called. The MSVC CRT uses libfoo.lib syntax but nothing else uses
+ # it and GCC only looks for static libraries called foo.lib and
+ # libfoo.a. However, we cannot use foo.lib because that's the same as
+ # the import library. Using libfoo.a is ok because people using MSVC
+ # always pass the library filename while linking anyway.
+ if not hasattr(self, 'prefix'):
+ self.prefix = 'lib'
+ if not hasattr(self, 'suffix'):
+ if 'rust' in self.compilers:
+ if not hasattr(self, 'rust_crate_type') or self.rust_crate_type == 'rlib':
+ # default Rust static library suffix
+ self.suffix = 'rlib'
+ elif self.rust_crate_type == 'staticlib':
+ self.suffix = 'a'
+ else:
+ self.suffix = 'a'
+ self.filename = self.prefix + self.name + '.' + self.suffix
+ self.outputs = [self.filename]
+
+ def get_link_deps_mapping(self, prefix: str) -> T.Mapping[str, str]:
+ return {}
+
+ def get_default_install_dir(self) -> T.Tuple[str, str]:
+ return self.environment.get_static_lib_dir(), '{libdir_static}'
+
+ def type_suffix(self):
+ return "@sta"
+
+ def process_kwargs(self, kwargs):
+ super().process_kwargs(kwargs)
+ if 'rust_crate_type' in kwargs:
+ rust_crate_type = kwargs['rust_crate_type']
+ if isinstance(rust_crate_type, str):
+ self.rust_crate_type = rust_crate_type
+ else:
+ raise InvalidArguments(f'Invalid rust_crate_type "{rust_crate_type}": must be a string.')
+
+ def is_linkable_target(self):
+ return True
+
+ def is_internal(self) -> bool:
+ return not self.need_install
+
+class SharedLibrary(BuildTarget):
+ known_kwargs = known_shlib_kwargs
+
+ typename = 'shared library'
+
+ def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice,
+ sources: T.List[SourceOutputs], structured_sources: T.Optional['StructuredSources'],
+ objects, environment: environment.Environment, compilers: T.Dict[str, 'Compiler'],
+ kwargs):
+ self.soversion = None
+ self.ltversion = None
+ # Max length 2, first element is compatibility_version, second is current_version
+ self.darwin_versions = []
+ self.vs_module_defs = None
+ # The import library this target will generate
+ self.import_filename = None
+ # The import library that Visual Studio would generate (and accept)
+ self.vs_import_filename = None
+ # The import library that GCC would generate (and prefer)
+ self.gcc_import_filename = None
+ # The debugging information file this target will generate
+ self.debug_filename = None
+ # Use by the pkgconfig module
+ self.shared_library_only = False
+ super().__init__(name, subdir, subproject, for_machine, sources, structured_sources, objects,
+ environment, compilers, kwargs)
+
+ def post_init(self) -> None:
+ super().post_init()
+ if 'rust' in self.compilers:
+ # If no crate type is specified, or it's the generic lib type, use dylib
+ if not hasattr(self, 'rust_crate_type') or self.rust_crate_type == 'lib':
+ mlog.debug('Defaulting Rust dynamic library target crate type to "dylib"')
+ self.rust_crate_type = 'dylib'
+ # Don't let configuration proceed with a non-dynamic crate type
+ elif self.rust_crate_type not in ['dylib', 'cdylib', 'proc-macro']:
+ raise InvalidArguments(f'Crate type "{self.rust_crate_type}" invalid for dynamic libraries; must be "dylib", "cdylib", or "proc-macro"')
+ if not hasattr(self, 'prefix'):
+ self.prefix = None
+ if not hasattr(self, 'suffix'):
+ self.suffix = None
+ self.basic_filename_tpl = '{0.prefix}{0.name}.{0.suffix}'
+ self.determine_filenames()
+
+ def get_link_deps_mapping(self, prefix: str) -> T.Mapping[str, str]:
+ result: T.Dict[str, str] = {}
+ mappings = self.get_transitive_link_deps_mapping(prefix)
+ old = get_target_macos_dylib_install_name(self)
+ if old not in mappings:
+ fname = self.get_filename()
+ outdirs, _, _ = self.get_install_dir()
+ new = os.path.join(prefix, outdirs[0], fname)
+ result.update({old: new})
+ mappings.update(result)
+ return mappings
+
+ def get_default_install_dir(self) -> T.Tuple[str, str]:
+ return self.environment.get_shared_lib_dir(), '{libdir_shared}'
+
+ def determine_filenames(self):
+ """
+ See https://github.com/mesonbuild/meson/pull/417 for details.
+
+ First we determine the filename template (self.filename_tpl), then we
+ set the output filename (self.filename).
+
+ The template is needed while creating aliases (self.get_aliases),
+ which are needed while generating .so shared libraries for Linux.
+
+ Besides this, there's also the import library name, which is only used
+ on Windows since on that platform the linker uses a separate library
+ called the "import library" during linking instead of the shared
+ library (DLL). The toolchain will output an import library in one of
+ two formats: GCC or Visual Studio.
+
+ When we're building with Visual Studio, the import library that will be
+ generated by the toolchain is self.vs_import_filename, and with
+ MinGW/GCC, it's self.gcc_import_filename. self.import_filename will
+ always contain the import library name this target will generate.
+ """
+ prefix = ''
+ suffix = ''
+ create_debug_file = False
+ self.filename_tpl = self.basic_filename_tpl
+ # NOTE: manual prefix/suffix override is currently only tested for C/C++
+ # C# and Mono
+ if 'cs' in self.compilers:
+ prefix = ''
+ suffix = 'dll'
+ self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}'
+ create_debug_file = True
+ # C, C++, Swift, Vala
+ # Only Windows uses a separate import library for linking
+ # For all other targets/platforms import_filename stays None
+ elif self.environment.machines[self.for_machine].is_windows():
+ suffix = 'dll'
+ self.vs_import_filename = '{}{}.lib'.format(self.prefix if self.prefix is not None else '', self.name)
+ self.gcc_import_filename = '{}{}.dll.a'.format(self.prefix if self.prefix is not None else 'lib', self.name)
+ if self.uses_rust():
+ # Shared library is of the form foo.dll
+ prefix = ''
+ # Import library is called foo.dll.lib
+ self.import_filename = f'{self.name}.dll.lib'
+ # Debug files(.pdb) is only created with debug buildtype
+ create_debug_file = self.environment.coredata.get_option(OptionKey("debug"))
+ elif self.get_using_msvc():
+ # Shared library is of the form foo.dll
+ prefix = ''
+ # Import library is called foo.lib
+ self.import_filename = self.vs_import_filename
+ # Debug files(.pdb) is only created with debug buildtype
+ create_debug_file = self.environment.coredata.get_option(OptionKey("debug"))
+ # Assume GCC-compatible naming
+ else:
+ # Shared library is of the form libfoo.dll
+ prefix = 'lib'
+ # Import library is called libfoo.dll.a
+ self.import_filename = self.gcc_import_filename
+ # Shared library has the soversion if it is defined
+ if self.soversion:
+ self.filename_tpl = '{0.prefix}{0.name}-{0.soversion}.{0.suffix}'
+ else:
+ self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}'
+ elif self.environment.machines[self.for_machine].is_cygwin():
+ suffix = 'dll'
+ self.gcc_import_filename = '{}{}.dll.a'.format(self.prefix if self.prefix is not None else 'lib', self.name)
+ # Shared library is of the form cygfoo.dll
+ # (ld --dll-search-prefix=cyg is the default)
+ prefix = 'cyg'
+ # Import library is called libfoo.dll.a
+ self.import_filename = self.gcc_import_filename
+ if self.soversion:
+ self.filename_tpl = '{0.prefix}{0.name}-{0.soversion}.{0.suffix}'
+ else:
+ self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}'
+ elif self.environment.machines[self.for_machine].is_darwin():
+ prefix = 'lib'
+ suffix = 'dylib'
+ # On macOS, the filename can only contain the major version
+ if self.soversion:
+ # libfoo.X.dylib
+ self.filename_tpl = '{0.prefix}{0.name}.{0.soversion}.{0.suffix}'
+ else:
+ # libfoo.dylib
+ self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}'
+ elif self.environment.machines[self.for_machine].is_android():
+ prefix = 'lib'
+ suffix = 'so'
+ # Android doesn't support shared_library versioning
+ self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}'
+ else:
+ prefix = 'lib'
+ suffix = 'so'
+ if self.ltversion:
+ # libfoo.so.X[.Y[.Z]] (.Y and .Z are optional)
+ self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}.{0.ltversion}'
+ elif self.soversion:
+ # libfoo.so.X
+ self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}.{0.soversion}'
+ else:
+ # No versioning, libfoo.so
+ self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}'
+ if self.prefix is None:
+ self.prefix = prefix
+ if self.suffix is None:
+ self.suffix = suffix
+ self.filename = self.filename_tpl.format(self)
+ # There may have been more outputs added by the time we get here, so
+ # only replace the first entry
+ self.outputs[0] = self.filename
+ if create_debug_file:
+ self.debug_filename = os.path.splitext(self.filename)[0] + '.pdb'
+
+ @staticmethod
+ def _validate_darwin_versions(darwin_versions):
+ try:
+ if isinstance(darwin_versions, int):
+ darwin_versions = str(darwin_versions)
+ if isinstance(darwin_versions, str):
+ darwin_versions = 2 * [darwin_versions]
+ if not isinstance(darwin_versions, list):
+ raise InvalidArguments('Shared library darwin_versions: must be a string, integer,'
+ f'or a list, not {darwin_versions!r}')
+ if len(darwin_versions) > 2:
+ raise InvalidArguments('Shared library darwin_versions: list must contain 2 or fewer elements')
+ if len(darwin_versions) == 1:
+ darwin_versions = 2 * darwin_versions
+ for i, v in enumerate(darwin_versions[:]):
+ if isinstance(v, int):
+ v = str(v)
+ if not isinstance(v, str):
+ raise InvalidArguments('Shared library darwin_versions: list elements '
+ f'must be strings or integers, not {v!r}')
+ if not re.fullmatch(r'[0-9]+(\.[0-9]+){0,2}', v):
+ raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z where '
+ 'X, Y, Z are numbers, and Y and Z are optional')
+ parts = v.split('.')
+ if len(parts) in {1, 2, 3} and int(parts[0]) > 65535:
+ raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z '
+ 'where X is [0, 65535] and Y, Z are optional')
+ if len(parts) in {2, 3} and int(parts[1]) > 255:
+ raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z '
+ 'where Y is [0, 255] and Y, Z are optional')
+ if len(parts) == 3 and int(parts[2]) > 255:
+ raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z '
+ 'where Z is [0, 255] and Y, Z are optional')
+ darwin_versions[i] = v
+ except ValueError:
+ raise InvalidArguments('Shared library darwin_versions: value is invalid')
+ return darwin_versions
+
+ def process_kwargs(self, kwargs):
+ super().process_kwargs(kwargs)
+
+ if not self.environment.machines[self.for_machine].is_android():
+ # Shared library version
+ if 'version' in kwargs:
+ self.ltversion = kwargs['version']
+ if not isinstance(self.ltversion, str):
+ raise InvalidArguments('Shared library version needs to be a string, not ' + type(self.ltversion).__name__)
+ if not re.fullmatch(r'[0-9]+(\.[0-9]+){0,2}', self.ltversion):
+ raise InvalidArguments(f'Invalid Shared library version "{self.ltversion}". Must be of the form X.Y.Z where all three are numbers. Y and Z are optional.')
+ # Try to extract/deduce the soversion
+ if 'soversion' in kwargs:
+ self.soversion = kwargs['soversion']
+ if isinstance(self.soversion, int):
+ self.soversion = str(self.soversion)
+ if not isinstance(self.soversion, str):
+ raise InvalidArguments('Shared library soversion is not a string or integer.')
+ elif self.ltversion:
+ # library version is defined, get the soversion from that
+ # We replicate what Autotools does here and take the first
+ # number of the version by default.
+ self.soversion = self.ltversion.split('.')[0]
+ # macOS, iOS and tvOS dylib compatibility_version and current_version
+ if 'darwin_versions' in kwargs:
+ self.darwin_versions = self._validate_darwin_versions(kwargs['darwin_versions'])
+ elif self.soversion:
+ # If unspecified, pick the soversion
+ self.darwin_versions = 2 * [self.soversion]
+
+ # Visual Studio module-definitions file
+ if 'vs_module_defs' in kwargs:
+ path = kwargs['vs_module_defs']
+ if isinstance(path, str):
+ if os.path.isabs(path):
+ self.vs_module_defs = File.from_absolute_file(path)
+ else:
+ self.vs_module_defs = File.from_source_file(self.environment.source_dir, self.subdir, path)
+ elif isinstance(path, File):
+ # When passing a generated file.
+ self.vs_module_defs = path
+ elif hasattr(path, 'get_filename'):
+ # When passing output of a Custom Target
+ self.vs_module_defs = File.from_built_file(path.subdir, path.get_filename())
+ else:
+ raise InvalidArguments(
+ 'Shared library vs_module_defs must be either a string, '
+ 'a file object or a Custom Target')
+ self.process_link_depends(path)
+
+ if 'rust_crate_type' in kwargs:
+ rust_crate_type = kwargs['rust_crate_type']
+ if isinstance(rust_crate_type, str):
+ self.rust_crate_type = rust_crate_type
+ else:
+ raise InvalidArguments(f'Invalid rust_crate_type "{rust_crate_type}": must be a string.')
+ if rust_crate_type == 'proc-macro':
+ FeatureNew.single_use('Rust crate type "proc-macro"', '0.62.0', self.subproject)
+
+ def get_import_filename(self) -> T.Optional[str]:
+ """
+ The name of the import library that will be outputted by the compiler
+
+ Returns None if there is no import library required for this platform
+ """
+ return self.import_filename
+
+ def get_debug_filename(self) -> T.Optional[str]:
+ """
+ The name of debuginfo file that will be created by the compiler
+
+ Returns None if the build won't create any debuginfo file
+ """
+ return self.debug_filename
+
+ def get_import_filenameslist(self):
+ if self.import_filename:
+ return [self.vs_import_filename, self.gcc_import_filename]
+ return []
+
+ def get_all_link_deps(self):
+ return [self] + self.get_transitive_link_deps()
+
+ def get_aliases(self) -> T.List[T.Tuple[str, str, str]]:
+ """
+ If the versioned library name is libfoo.so.0.100.0, aliases are:
+ * libfoo.so.0 (soversion) -> libfoo.so.0.100.0
+ * libfoo.so (unversioned; for linking) -> libfoo.so.0
+ Same for dylib:
+ * libfoo.dylib (unversioned; for linking) -> libfoo.0.dylib
+ """
+ aliases: T.List[T.Tuple[str, str, str]] = []
+ # Aliases are only useful with .so and .dylib libraries. Also if
+ # there's no self.soversion (no versioning), we don't need aliases.
+ if self.suffix not in ('so', 'dylib') or not self.soversion:
+ return aliases
+ # With .so libraries, the minor and micro versions are also in the
+ # filename. If ltversion != soversion we create an soversion alias:
+ # libfoo.so.0 -> libfoo.so.0.100.0
+ # Where libfoo.so.0.100.0 is the actual library
+ if self.suffix == 'so' and self.ltversion and self.ltversion != self.soversion:
+ alias_tpl = self.filename_tpl.replace('ltversion', 'soversion')
+ ltversion_filename = alias_tpl.format(self)
+ tag = self.install_tag[0] or 'runtime'
+ aliases.append((ltversion_filename, self.filename, tag))
+ # libfoo.so.0/libfoo.0.dylib is the actual library
+ else:
+ ltversion_filename = self.filename
+ # Unversioned alias:
+ # libfoo.so -> libfoo.so.0
+ # libfoo.dylib -> libfoo.0.dylib
+ tag = self.install_tag[0] or 'devel'
+ aliases.append((self.basic_filename_tpl.format(self), ltversion_filename, tag))
+ return aliases
+
+ def type_suffix(self):
+ return "@sha"
+
+ def is_linkable_target(self):
+ return True
+
+# A shared library that is meant to be used with dlopen rather than linking
+# into something else.
+class SharedModule(SharedLibrary):
+ known_kwargs = known_shmod_kwargs
+
+ typename = 'shared module'
+
+ def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice,
+ sources: T.List[SourceOutputs], structured_sources: T.Optional['StructuredSources'],
+ objects, environment: environment.Environment,
+ compilers: T.Dict[str, 'Compiler'], kwargs):
+ if 'version' in kwargs:
+ raise MesonException('Shared modules must not specify the version kwarg.')
+ if 'soversion' in kwargs:
+ raise MesonException('Shared modules must not specify the soversion kwarg.')
+ super().__init__(name, subdir, subproject, for_machine, sources,
+ structured_sources, objects, environment, compilers, kwargs)
+ # We need to set the soname in cases where build files link the module
+ # to build targets, see: https://github.com/mesonbuild/meson/issues/9492
+ self.force_soname = False
+
+ def get_default_install_dir(self) -> T.Tuple[str, str]:
+ return self.environment.get_shared_module_dir(), '{moduledir_shared}'
+
+class BothLibraries(SecondLevelHolder):
+ def __init__(self, shared: SharedLibrary, static: StaticLibrary) -> None:
+ self._preferred_library = 'shared'
+ self.shared = shared
+ self.static = static
+ self.subproject = self.shared.subproject
+
+ def __repr__(self) -> str:
+ return f'<BothLibraries: static={repr(self.static)}; shared={repr(self.shared)}>'
+
+ def get_default_object(self) -> BuildTarget:
+ if self._preferred_library == 'shared':
+ return self.shared
+ elif self._preferred_library == 'static':
+ return self.static
+ raise MesonBugException(f'self._preferred_library == "{self._preferred_library}" is neither "shared" nor "static".')
+
+class CommandBase:
+
+ depend_files: T.List[File]
+ dependencies: T.List[T.Union[BuildTarget, 'CustomTarget']]
+ subproject: str
+
+ def flatten_command(self, cmd: T.Sequence[T.Union[str, File, programs.ExternalProgram, BuildTargetTypes]]) -> \
+ T.List[T.Union[str, File, BuildTarget, 'CustomTarget']]:
+ cmd = listify(cmd)
+ final_cmd: T.List[T.Union[str, File, BuildTarget, 'CustomTarget']] = []
+ for c in cmd:
+ if isinstance(c, str):
+ final_cmd.append(c)
+ elif isinstance(c, File):
+ self.depend_files.append(c)
+ final_cmd.append(c)
+ elif isinstance(c, programs.ExternalProgram):
+ if not c.found():
+ raise InvalidArguments('Tried to use not-found external program in "command"')
+ path = c.get_path()
+ if os.path.isabs(path):
+ # Can only add a dependency on an external program which we
+ # know the absolute path of
+ self.depend_files.append(File.from_absolute_file(path))
+ final_cmd += c.get_command()
+ elif isinstance(c, (BuildTarget, CustomTarget)):
+ self.dependencies.append(c)
+ final_cmd.append(c)
+ elif isinstance(c, CustomTargetIndex):
+ FeatureNew.single_use('CustomTargetIndex for command argument', '0.60', self.subproject)
+ self.dependencies.append(c.target)
+ final_cmd += self.flatten_command(File.from_built_file(c.get_subdir(), c.get_filename()))
+ elif isinstance(c, list):
+ final_cmd += self.flatten_command(c)
+ else:
+ raise InvalidArguments(f'Argument {c!r} in "command" is invalid')
+ return final_cmd
+
+class CustomTarget(Target, CommandBase):
+
+ typename = 'custom'
+
+ def __init__(self,
+ name: T.Optional[str],
+ subdir: str,
+ subproject: str,
+ environment: environment.Environment,
+ command: T.Sequence[T.Union[
+ str, BuildTargetTypes, GeneratedList,
+ programs.ExternalProgram, File]],
+ sources: T.Sequence[T.Union[
+ str, File, BuildTargetTypes, ExtractedObjects,
+ GeneratedList, programs.ExternalProgram]],
+ outputs: T.List[str],
+ *,
+ build_always_stale: bool = False,
+ build_by_default: T.Optional[bool] = None,
+ capture: bool = False,
+ console: bool = False,
+ depend_files: T.Optional[T.Sequence[FileOrString]] = None,
+ extra_depends: T.Optional[T.Sequence[T.Union[str, SourceOutputs]]] = None,
+ depfile: T.Optional[str] = None,
+ env: T.Optional[EnvironmentVariables] = None,
+ feed: bool = False,
+ install: bool = False,
+ install_dir: T.Optional[T.List[T.Union[str, Literal[False]]]] = None,
+ install_mode: T.Optional[FileMode] = None,
+ install_tag: T.Optional[T.List[T.Optional[str]]] = None,
+ absolute_paths: bool = False,
+ backend: T.Optional['Backend'] = None,
+ ):
+ # TODO expose keyword arg to make MachineChoice.HOST configurable
+ super().__init__(name, subdir, subproject, False, MachineChoice.HOST, environment)
+ self.sources = list(sources)
+ self.outputs = substitute_values(
+ outputs, get_filenames_templates_dict(
+ get_sources_string_names(sources, backend),
+ []))
+ self.build_by_default = build_by_default if build_by_default is not None else install
+ self.build_always_stale = build_always_stale
+ self.capture = capture
+ self.console = console
+ self.depend_files = list(depend_files or [])
+ self.dependencies: T.List[T.Union[CustomTarget, BuildTarget]] = []
+ # must be after depend_files and dependencies
+ self.command = self.flatten_command(command)
+ self.depfile = depfile
+ self.env = env or EnvironmentVariables()
+ self.extra_depends = list(extra_depends or [])
+ self.feed = feed
+ self.install = install
+ self.install_dir = list(install_dir or [])
+ self.install_mode = install_mode
+ self.install_tag = _process_install_tag(install_tag, len(self.outputs))
+ self.name = name if name else self.outputs[0]
+
+ # Whether to use absolute paths for all files on the commandline
+ self.absolute_paths = absolute_paths
+
+ def get_default_install_dir(self) -> T.Tuple[str, str]:
+ return None, None
+
+ def __repr__(self):
+ repr_str = "<{0} {1}: {2}>"
+ return repr_str.format(self.__class__.__name__, self.get_id(), self.command)
+
+ def get_target_dependencies(self) -> T.List[T.Union[SourceOutputs, str]]:
+ deps: T.List[T.Union[SourceOutputs, str]] = []
+ deps.extend(self.dependencies)
+ deps.extend(self.extra_depends)
+ for c in self.sources:
+ if isinstance(c, CustomTargetIndex):
+ deps.append(c.target)
+ elif not isinstance(c, programs.ExternalProgram):
+ deps.append(c)
+ return deps
+
+ def get_transitive_build_target_deps(self) -> T.Set[T.Union[BuildTarget, 'CustomTarget']]:
+ '''
+ Recursively fetch the build targets that this custom target depends on,
+ whether through `command:`, `depends:`, or `sources:` The recursion is
+ only performed on custom targets.
+ This is useful for setting PATH on Windows for finding required DLLs.
+ F.ex, if you have a python script that loads a C module that links to
+ other DLLs in your project.
+ '''
+ bdeps: T.Set[T.Union[BuildTarget, 'CustomTarget']] = set()
+ deps = self.get_target_dependencies()
+ for d in deps:
+ if isinstance(d, BuildTarget):
+ bdeps.add(d)
+ elif isinstance(d, CustomTarget):
+ bdeps.update(d.get_transitive_build_target_deps())
+ return bdeps
+
+ def get_dependencies(self):
+ return self.dependencies
+
+ def should_install(self) -> bool:
+ return self.install
+
+ def get_custom_install_dir(self) -> T.List[T.Union[str, Literal[False]]]:
+ return self.install_dir
+
+ def get_custom_install_mode(self) -> T.Optional['FileMode']:
+ return self.install_mode
+
+ def get_outputs(self) -> T.List[str]:
+ return self.outputs
+
+ def get_filename(self) -> str:
+ return self.outputs[0]
+
+ def get_sources(self) -> T.List[T.Union[str, File, BuildTarget, GeneratedTypes, ExtractedObjects, programs.ExternalProgram]]:
+ return self.sources
+
+ def get_generated_lists(self) -> T.List[GeneratedList]:
+ genlists: T.List[GeneratedList] = []
+ for c in self.sources:
+ if isinstance(c, GeneratedList):
+ genlists.append(c)
+ return genlists
+
+ def get_generated_sources(self) -> T.List[GeneratedList]:
+ return self.get_generated_lists()
+
+ def get_dep_outname(self, infilenames):
+ if self.depfile is None:
+ raise InvalidArguments('Tried to get depfile name for custom_target that does not have depfile defined.')
+ if infilenames:
+ plainname = os.path.basename(infilenames[0])
+ basename = os.path.splitext(plainname)[0]
+ return self.depfile.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname)
+ else:
+ if '@BASENAME@' in self.depfile or '@PLAINNAME@' in self.depfile:
+ raise InvalidArguments('Substitution in depfile for custom_target that does not have an input file.')
+ return self.depfile
+
+ def is_linkable_target(self) -> bool:
+ if len(self.outputs) != 1:
+ return False
+ suf = os.path.splitext(self.outputs[0])[-1]
+ return suf in {'.a', '.dll', '.lib', '.so', '.dylib'}
+
+ def links_dynamically(self) -> bool:
+ """Whether this target links dynamically or statically
+
+ Does not assert the target is linkable, just that it is not shared
+
+ :return: True if is dynamically linked, otherwise False
+ """
+ suf = os.path.splitext(self.outputs[0])[-1]
+ return suf not in {'.a', '.lib'}
+
+ def get_link_deps_mapping(self, prefix: str) -> T.Mapping[str, str]:
+ return {}
+
+ def get_link_dep_subdirs(self) -> T.AbstractSet[str]:
+ return OrderedSet()
+
+ def get_all_link_deps(self):
+ return []
+
+ def is_internal(self) -> bool:
+ '''
+ Returns True if this is a not installed static library.
+ '''
+ if len(self.outputs) != 1:
+ return False
+ return CustomTargetIndex(self, self.outputs[0]).is_internal()
+
+ def extract_all_objects_recurse(self) -> T.List[T.Union[str, 'ExtractedObjects']]:
+ return self.get_outputs()
+
+ def type_suffix(self):
+ return "@cus"
+
+ def __getitem__(self, index: int) -> 'CustomTargetIndex':
+ return CustomTargetIndex(self, self.outputs[index])
+
+ def __setitem__(self, index, value):
+ raise NotImplementedError
+
+ def __delitem__(self, index):
+ raise NotImplementedError
+
+ def __iter__(self):
+ for i in self.outputs:
+ yield CustomTargetIndex(self, i)
+
+ def __len__(self) -> int:
+ return len(self.outputs)
+
+class CompileTarget(BuildTarget):
+ '''
+ Target that only compile sources without linking them together.
+ It can be used as preprocessor, or transpiler.
+ '''
+
+ typename = 'compile'
+
+ def __init__(self,
+ name: str,
+ subdir: str,
+ subproject: str,
+ environment: environment.Environment,
+ sources: T.List[File],
+ output_templ: str,
+ compiler: Compiler,
+ kwargs):
+ compilers = {compiler.get_language(): compiler}
+ super().__init__(name, subdir, subproject, compiler.for_machine,
+ sources, None, [], environment, compilers, kwargs)
+ self.filename = name
+ self.compiler = compiler
+ self.output_templ = output_templ
+ self.outputs = []
+ for f in sources:
+ plainname = os.path.basename(f.fname)
+ basename = os.path.splitext(plainname)[0]
+ self.outputs.append(output_templ.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname))
+ self.sources_map = dict(zip(sources, self.outputs))
+
+ def type_suffix(self) -> str:
+ return "@compile"
+
+ @property
+ def is_unity(self) -> bool:
+ return False
+
+
+class RunTarget(Target, CommandBase):
+
+ typename = 'run'
+
+ def __init__(self, name: str,
+ command: T.Sequence[T.Union[str, File, BuildTargetTypes, programs.ExternalProgram]],
+ dependencies: T.Sequence[Target],
+ subdir: str,
+ subproject: str,
+ environment: environment.Environment,
+ env: T.Optional['EnvironmentVariables'] = None,
+ default_env: bool = True):
+ # These don't produce output artifacts
+ super().__init__(name, subdir, subproject, False, MachineChoice.BUILD, environment)
+ self.dependencies = dependencies
+ self.depend_files = []
+ self.command = self.flatten_command(command)
+ self.absolute_paths = False
+ self.env = env
+ self.default_env = default_env
+
+ def __repr__(self) -> str:
+ repr_str = "<{0} {1}: {2}>"
+ return repr_str.format(self.__class__.__name__, self.get_id(), self.command[0])
+
+ def get_dependencies(self) -> T.List[T.Union[BuildTarget, 'CustomTarget']]:
+ return self.dependencies
+
+ def get_generated_sources(self) -> T.List['GeneratedTypes']:
+ return []
+
+ def get_sources(self) -> T.List[File]:
+ return []
+
+ def should_install(self) -> bool:
+ return False
+
+ def get_filename(self) -> str:
+ return self.name
+
+ def get_outputs(self) -> T.List[str]:
+ if isinstance(self.name, str):
+ return [self.name]
+ elif isinstance(self.name, list):
+ return self.name
+ else:
+ raise RuntimeError('RunTarget: self.name is neither a list nor a string. This is a bug')
+
+ def type_suffix(self) -> str:
+ return "@run"
+
+class AliasTarget(RunTarget):
+ def __init__(self, name: str, dependencies: T.Sequence['Target'],
+ subdir: str, subproject: str, environment: environment.Environment):
+ super().__init__(name, [], dependencies, subdir, subproject, environment)
+
+ def __repr__(self):
+ repr_str = "<{0} {1}>"
+ return repr_str.format(self.__class__.__name__, self.get_id())
+
+class Jar(BuildTarget):
+ known_kwargs = known_jar_kwargs
+
+ typename = 'jar'
+
+ def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice,
+ sources: T.List[SourceOutputs], structured_sources: T.Optional['StructuredSources'],
+ objects, environment: environment.Environment, compilers: T.Dict[str, 'Compiler'],
+ kwargs):
+ super().__init__(name, subdir, subproject, for_machine, sources, structured_sources, objects,
+ environment, compilers, kwargs)
+ for s in self.sources:
+ if not s.endswith('.java'):
+ raise InvalidArguments(f'Jar source {s} is not a java file.')
+ for t in self.link_targets:
+ if not isinstance(t, Jar):
+ raise InvalidArguments(f'Link target {t} is not a jar target.')
+ if self.structured_sources:
+ raise InvalidArguments('structured sources are not supported in Java targets.')
+ self.filename = self.name + '.jar'
+ self.outputs = [self.filename]
+ self.java_args = kwargs.get('java_args', [])
+ self.java_resources: T.Optional[StructuredSources] = kwargs.get('java_resources', None)
+
+ def get_main_class(self):
+ return self.main_class
+
+ def type_suffix(self):
+ return "@jar"
+
+ def get_java_args(self):
+ return self.java_args
+
+ def get_java_resources(self) -> T.Optional[StructuredSources]:
+ return self.java_resources
+
+ def validate_install(self):
+ # All jar targets are installable.
+ pass
+
+ def is_linkable_target(self):
+ return True
+
+ def get_classpath_args(self):
+ cp_paths = [os.path.join(l.get_subdir(), l.get_filename()) for l in self.link_targets]
+ cp_string = os.pathsep.join(cp_paths)
+ if cp_string:
+ return ['-cp', os.pathsep.join(cp_paths)]
+ return []
+
+ def get_default_install_dir(self) -> T.Tuple[str, str]:
+ return self.environment.get_jar_dir(), '{jardir}'
+
+@dataclass(eq=False)
+class CustomTargetIndex(HoldableObject):
+
+ """A special opaque object returned by indexing a CustomTarget. This object
+ exists in Meson, but acts as a proxy in the backends, making targets depend
+ on the CustomTarget it's derived from, but only adding one source file to
+ the sources.
+ """
+
+ typename: T.ClassVar[str] = 'custom'
+
+ target: T.Union[CustomTarget, CompileTarget]
+ output: str
+
+ def __post_init__(self) -> None:
+ self.for_machine = self.target.for_machine
+
+ @property
+ def name(self) -> str:
+ return f'{self.target.name}[{self.output}]'
+
+ def __repr__(self):
+ return '<CustomTargetIndex: {!r}[{}]>'.format(self.target, self.output)
+
+ def get_outputs(self) -> T.List[str]:
+ return [self.output]
+
+ def get_subdir(self) -> str:
+ return self.target.get_subdir()
+
+ def get_filename(self) -> str:
+ return self.output
+
+ def get_id(self) -> str:
+ return self.target.get_id()
+
+ def get_all_link_deps(self):
+ return self.target.get_all_link_deps()
+
+ def get_link_deps_mapping(self, prefix: str) -> T.Mapping[str, str]:
+ return self.target.get_link_deps_mapping(prefix)
+
+ def get_link_dep_subdirs(self) -> T.AbstractSet[str]:
+ return self.target.get_link_dep_subdirs()
+
+ def is_linkable_target(self) -> bool:
+ suf = os.path.splitext(self.output)[-1]
+ return suf in {'.a', '.dll', '.lib', '.so', '.dylib'}
+
+ def links_dynamically(self) -> bool:
+ """Whether this target links dynamically or statically
+
+ Does not assert the target is linkable, just that it is not shared
+
+ :return: True if is dynamically linked, otherwise False
+ """
+ suf = os.path.splitext(self.output)[-1]
+ return suf not in {'.a', '.lib'}
+
+ def should_install(self) -> bool:
+ return self.target.should_install()
+
+ def is_internal(self) -> bool:
+ '''
+ Returns True if this is a not installed static library
+ '''
+ suf = os.path.splitext(self.output)[-1]
+ return suf in {'.a', '.lib'} and not self.should_install()
+
+ def extract_all_objects_recurse(self) -> T.List[T.Union[str, 'ExtractedObjects']]:
+ return self.target.extract_all_objects_recurse()
+
+ def get_custom_install_dir(self) -> T.List[T.Union[str, Literal[False]]]:
+ return self.target.get_custom_install_dir()
+
+class ConfigurationData(HoldableObject):
+ def __init__(self, initial_values: T.Optional[T.Union[
+ T.Dict[str, T.Tuple[T.Union[str, int, bool], T.Optional[str]]],
+ T.Dict[str, T.Union[str, int, bool]]]
+ ] = None):
+ super().__init__()
+ self.values: T.Dict[str, T.Tuple[T.Union[str, int, bool], T.Optional[str]]] = \
+ {k: v if isinstance(v, tuple) else (v, None) for k, v in initial_values.items()} if initial_values else {}
+ self.used: bool = False
+
+ def __repr__(self) -> str:
+ return repr(self.values)
+
+ def __contains__(self, value: str) -> bool:
+ return value in self.values
+
+ def __bool__(self) -> bool:
+ return bool(self.values)
+
+ def get(self, name: str) -> T.Tuple[T.Union[str, int, bool], T.Optional[str]]:
+ return self.values[name] # (val, desc)
+
+ def keys(self) -> T.Iterator[str]:
+ return self.values.keys()
+
+# A bit poorly named, but this represents plain data files to copy
+# during install.
+@dataclass(eq=False)
+class Data(HoldableObject):
+ sources: T.List[File]
+ install_dir: str
+ install_dir_name: str
+ install_mode: 'FileMode'
+ subproject: str
+ rename: T.List[str] = None
+ install_tag: T.Optional[str] = None
+ data_type: str = None
+
+ def __post_init__(self) -> None:
+ if self.rename is None:
+ self.rename = [os.path.basename(f.fname) for f in self.sources]
+
+@dataclass(eq=False)
+class SymlinkData(HoldableObject):
+ target: str
+ name: str
+ install_dir: str
+ subproject: str
+ install_tag: T.Optional[str] = None
+
+ def __post_init__(self) -> None:
+ if self.name != os.path.basename(self.name):
+ raise InvalidArguments(f'Link name is "{self.name}", but link names cannot contain path separators. '
+ 'The dir part should be in install_dir.')
+
+@dataclass(eq=False)
+class TestSetup:
+ exe_wrapper: T.List[str]
+ gdb: bool
+ timeout_multiplier: int
+ env: EnvironmentVariables
+ exclude_suites: T.List[str]
+
+def get_sources_string_names(sources, backend):
+ '''
+ For the specified list of @sources which can be strings, Files, or targets,
+ get all the output basenames.
+ '''
+ names = []
+ for s in sources:
+ if isinstance(s, str):
+ names.append(s)
+ elif isinstance(s, (BuildTarget, CustomTarget, CustomTargetIndex, GeneratedList)):
+ names += s.get_outputs()
+ elif isinstance(s, ExtractedObjects):
+ names += backend.determine_ext_objs(s)
+ elif isinstance(s, File):
+ names.append(s.fname)
+ else:
+ raise AssertionError(f'Unknown source type: {s!r}')
+ return names
+
+def load(build_dir: str) -> Build:
+ filename = os.path.join(build_dir, 'meson-private', 'build.dat')
+ try:
+ return pickle_load(filename, 'Build data', Build)
+ except FileNotFoundError:
+ raise MesonException(f'No such build data file as {filename!r}.')
+
+
+def save(obj: Build, filename: str) -> None:
+ with open(filename, 'wb') as f:
+ pickle.dump(obj, f)
diff --git a/mesonbuild/cmake/__init__.py b/mesonbuild/cmake/__init__.py
new file mode 100644
index 0000000..16c1322
--- /dev/null
+++ b/mesonbuild/cmake/__init__.py
@@ -0,0 +1,48 @@
+# Copyright 2019 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool.
+
+__all__ = [
+ 'CMakeExecutor',
+ 'CMakeExecScope',
+ 'CMakeException',
+ 'CMakeFileAPI',
+ 'CMakeInterpreter',
+ 'CMakeTarget',
+ 'CMakeToolchain',
+ 'CMakeTraceLine',
+ 'CMakeTraceParser',
+ 'SingleTargetOptions',
+ 'TargetOptions',
+ 'parse_generator_expressions',
+ 'language_map',
+ 'backend_generator_map',
+ 'cmake_get_generator_args',
+ 'cmake_defines_to_args',
+ 'check_cmake_args',
+ 'cmake_is_debug',
+ 'resolve_cmake_trace_targets',
+ 'ResolvedTarget',
+]
+
+from .common import CMakeException, SingleTargetOptions, TargetOptions, cmake_defines_to_args, language_map, backend_generator_map, cmake_get_generator_args, check_cmake_args, cmake_is_debug
+from .executor import CMakeExecutor
+from .fileapi import CMakeFileAPI
+from .generator import parse_generator_expressions
+from .interpreter import CMakeInterpreter
+from .toolchain import CMakeToolchain, CMakeExecScope
+from .traceparser import CMakeTarget, CMakeTraceLine, CMakeTraceParser
+from .tracetargets import resolve_cmake_trace_targets, ResolvedTarget
diff --git a/mesonbuild/cmake/common.py b/mesonbuild/cmake/common.py
new file mode 100644
index 0000000..accb7c9
--- /dev/null
+++ b/mesonbuild/cmake/common.py
@@ -0,0 +1,348 @@
+# Copyright 2019 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool.
+from __future__ import annotations
+
+from ..mesonlib import MesonException, OptionKey
+from .. import mlog
+from pathlib import Path
+import typing as T
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+
+language_map = {
+ 'c': 'C',
+ 'cpp': 'CXX',
+ 'cuda': 'CUDA',
+ 'objc': 'OBJC',
+ 'objcpp': 'OBJCXX',
+ 'cs': 'CSharp',
+ 'java': 'Java',
+ 'fortran': 'Fortran',
+ 'swift': 'Swift',
+}
+
+backend_generator_map = {
+ 'ninja': 'Ninja',
+ 'xcode': 'Xcode',
+ 'vs2010': 'Visual Studio 10 2010',
+ 'vs2012': 'Visual Studio 11 2012',
+ 'vs2013': 'Visual Studio 12 2013',
+ 'vs2015': 'Visual Studio 14 2015',
+ 'vs2017': 'Visual Studio 15 2017',
+ 'vs2019': 'Visual Studio 16 2019',
+ 'vs2022': 'Visual Studio 17 2022',
+}
+
+blacklist_cmake_defs = [
+ 'CMAKE_TOOLCHAIN_FILE',
+ 'CMAKE_PROJECT_INCLUDE',
+ 'MESON_PRELOAD_FILE',
+ 'MESON_PS_CMAKE_CURRENT_BINARY_DIR',
+ 'MESON_PS_CMAKE_CURRENT_SOURCE_DIR',
+ 'MESON_PS_DELAYED_CALLS',
+ 'MESON_PS_LOADED',
+ 'MESON_FIND_ROOT_PATH',
+ 'MESON_CMAKE_SYSROOT',
+ 'MESON_PATHS_LIST',
+ 'MESON_CMAKE_ROOT',
+]
+
+def cmake_is_debug(env: 'Environment') -> bool:
+ if OptionKey('b_vscrt') in env.coredata.options:
+ is_debug = env.coredata.get_option(OptionKey('buildtype')) == 'debug'
+ if env.coredata.options[OptionKey('b_vscrt')].value in {'mdd', 'mtd'}:
+ is_debug = True
+ return is_debug
+ else:
+ # Don't directly assign to is_debug to make mypy happy
+ debug_opt = env.coredata.get_option(OptionKey('debug'))
+ assert isinstance(debug_opt, bool)
+ return debug_opt
+
+class CMakeException(MesonException):
+ pass
+
+class CMakeBuildFile:
+ def __init__(self, file: Path, is_cmake: bool, is_temp: bool) -> None:
+ self.file = file
+ self.is_cmake = is_cmake
+ self.is_temp = is_temp
+
+ def __repr__(self) -> str:
+ return f'<{self.__class__.__name__}: {self.file}; cmake={self.is_cmake}; temp={self.is_temp}>'
+
+def _flags_to_list(raw: str) -> T.List[str]:
+ # Convert a raw commandline string into a list of strings
+ res = []
+ curr = ''
+ escape = False
+ in_string = False
+ for i in raw:
+ if escape:
+ # If the current char is not a quote, the '\' is probably important
+ if i not in ['"', "'"]:
+ curr += '\\'
+ curr += i
+ escape = False
+ elif i == '\\':
+ escape = True
+ elif i in {'"', "'"}:
+ in_string = not in_string
+ elif i in {' ', '\n'}:
+ if in_string:
+ curr += i
+ else:
+ res += [curr]
+ curr = ''
+ else:
+ curr += i
+ res += [curr]
+ res = [r for r in res if len(r) > 0]
+ return res
+
+def cmake_get_generator_args(env: 'Environment') -> T.List[str]:
+ backend_name = env.coredata.get_option(OptionKey('backend'))
+ assert isinstance(backend_name, str)
+ assert backend_name in backend_generator_map
+ return ['-G', backend_generator_map[backend_name]]
+
+def cmake_defines_to_args(raw: T.Any, permissive: bool = False) -> T.List[str]:
+ res = [] # type: T.List[str]
+ if not isinstance(raw, list):
+ raw = [raw]
+
+ for i in raw:
+ if not isinstance(i, dict):
+ raise MesonException('Invalid CMake defines. Expected a dict, but got a {}'.format(type(i).__name__))
+ for key, val in i.items():
+ assert isinstance(key, str)
+ if key in blacklist_cmake_defs:
+ mlog.warning('Setting', mlog.bold(key), 'is not supported. See the meson docs for cross compilation support:')
+ mlog.warning(' - URL: https://mesonbuild.com/CMake-module.html#cross-compilation')
+ mlog.warning(' --> Ignoring this option')
+ continue
+ if isinstance(val, (str, int, float)):
+ res += [f'-D{key}={val}']
+ elif isinstance(val, bool):
+ val_str = 'ON' if val else 'OFF'
+ res += [f'-D{key}={val_str}']
+ else:
+ raise MesonException('Type "{}" of "{}" is not supported as for a CMake define value'.format(type(val).__name__, key))
+
+ return res
+
+# TODO: this functuin will become obsolete once the `cmake_args` kwarg is dropped
+def check_cmake_args(args: T.List[str]) -> T.List[str]:
+ res = [] # type: T.List[str]
+ dis = ['-D' + x for x in blacklist_cmake_defs]
+ assert dis # Ensure that dis is not empty.
+ for i in args:
+ if any(i.startswith(x) for x in dis):
+ mlog.warning('Setting', mlog.bold(i), 'is not supported. See the meson docs for cross compilation support:')
+ mlog.warning(' - URL: https://mesonbuild.com/CMake-module.html#cross-compilation')
+ mlog.warning(' --> Ignoring this option')
+ continue
+ res += [i]
+ return res
+
+class CMakeInclude:
+ def __init__(self, path: Path, isSystem: bool = False):
+ self.path = path
+ self.isSystem = isSystem
+
+ def __repr__(self) -> str:
+ return f'<CMakeInclude: {self.path} -- isSystem = {self.isSystem}>'
+
+class CMakeFileGroup:
+ def __init__(self, data: T.Dict[str, T.Any]) -> None:
+ self.defines = data.get('defines', '') # type: str
+ self.flags = _flags_to_list(data.get('compileFlags', '')) # type: T.List[str]
+ self.is_generated = data.get('isGenerated', False) # type: bool
+ self.language = data.get('language', 'C') # type: str
+ self.sources = [Path(x) for x in data.get('sources', [])] # type: T.List[Path]
+
+ # Fix the include directories
+ self.includes = [] # type: T.List[CMakeInclude]
+ for i in data.get('includePath', []):
+ if isinstance(i, dict) and 'path' in i:
+ isSystem = i.get('isSystem', False)
+ assert isinstance(isSystem, bool)
+ assert isinstance(i['path'], str)
+ self.includes += [CMakeInclude(Path(i['path']), isSystem)]
+ elif isinstance(i, str):
+ self.includes += [CMakeInclude(Path(i))]
+
+ def log(self) -> None:
+ mlog.log('flags =', mlog.bold(', '.join(self.flags)))
+ mlog.log('defines =', mlog.bold(', '.join(self.defines)))
+ mlog.log('includes =', mlog.bold(', '.join([str(x) for x in self.includes])))
+ mlog.log('is_generated =', mlog.bold('true' if self.is_generated else 'false'))
+ mlog.log('language =', mlog.bold(self.language))
+ mlog.log('sources:')
+ for i in self.sources:
+ with mlog.nested():
+ mlog.log(i.as_posix())
+
+class CMakeTarget:
+ def __init__(self, data: T.Dict[str, T.Any]) -> None:
+ self.artifacts = [Path(x) for x in data.get('artifacts', [])] # type: T.List[Path]
+ self.src_dir = Path(data.get('sourceDirectory', '')) # type: Path
+ self.build_dir = Path(data.get('buildDirectory', '')) # type: Path
+ self.name = data.get('name', '') # type: str
+ self.full_name = data.get('fullName', '') # type: str
+ self.install = data.get('hasInstallRule', False) # type: bool
+ self.install_paths = [Path(x) for x in set(data.get('installPaths', []))] # type: T.List[Path]
+ self.link_lang = data.get('linkerLanguage', '') # type: str
+ self.link_libraries = _flags_to_list(data.get('linkLibraries', '')) # type: T.List[str]
+ self.link_flags = _flags_to_list(data.get('linkFlags', '')) # type: T.List[str]
+ self.link_lang_flags = _flags_to_list(data.get('linkLanguageFlags', '')) # type: T.List[str]
+ # self.link_path = Path(data.get('linkPath', '')) # type: Path
+ self.type = data.get('type', 'EXECUTABLE') # type: str
+ # self.is_generator_provided = data.get('isGeneratorProvided', False) # type: bool
+ self.files = [] # type: T.List[CMakeFileGroup]
+
+ for i in data.get('fileGroups', []):
+ self.files += [CMakeFileGroup(i)]
+
+ def log(self) -> None:
+ mlog.log('artifacts =', mlog.bold(', '.join([x.as_posix() for x in self.artifacts])))
+ mlog.log('src_dir =', mlog.bold(self.src_dir.as_posix()))
+ mlog.log('build_dir =', mlog.bold(self.build_dir.as_posix()))
+ mlog.log('name =', mlog.bold(self.name))
+ mlog.log('full_name =', mlog.bold(self.full_name))
+ mlog.log('install =', mlog.bold('true' if self.install else 'false'))
+ mlog.log('install_paths =', mlog.bold(', '.join([x.as_posix() for x in self.install_paths])))
+ mlog.log('link_lang =', mlog.bold(self.link_lang))
+ mlog.log('link_libraries =', mlog.bold(', '.join(self.link_libraries)))
+ mlog.log('link_flags =', mlog.bold(', '.join(self.link_flags)))
+ mlog.log('link_lang_flags =', mlog.bold(', '.join(self.link_lang_flags)))
+ # mlog.log('link_path =', mlog.bold(self.link_path))
+ mlog.log('type =', mlog.bold(self.type))
+ # mlog.log('is_generator_provided =', mlog.bold('true' if self.is_generator_provided else 'false'))
+ for idx, i in enumerate(self.files):
+ mlog.log(f'Files {idx}:')
+ with mlog.nested():
+ i.log()
+
+class CMakeProject:
+ def __init__(self, data: T.Dict[str, T.Any]) -> None:
+ self.src_dir = Path(data.get('sourceDirectory', '')) # type: Path
+ self.build_dir = Path(data.get('buildDirectory', '')) # type: Path
+ self.name = data.get('name', '') # type: str
+ self.targets = [] # type: T.List[CMakeTarget]
+
+ for i in data.get('targets', []):
+ self.targets += [CMakeTarget(i)]
+
+ def log(self) -> None:
+ mlog.log('src_dir =', mlog.bold(self.src_dir.as_posix()))
+ mlog.log('build_dir =', mlog.bold(self.build_dir.as_posix()))
+ mlog.log('name =', mlog.bold(self.name))
+ for idx, i in enumerate(self.targets):
+ mlog.log(f'Target {idx}:')
+ with mlog.nested():
+ i.log()
+
+class CMakeConfiguration:
+ def __init__(self, data: T.Dict[str, T.Any]) -> None:
+ self.name = data.get('name', '') # type: str
+ self.projects = [] # type: T.List[CMakeProject]
+ for i in data.get('projects', []):
+ self.projects += [CMakeProject(i)]
+
+ def log(self) -> None:
+ mlog.log('name =', mlog.bold(self.name))
+ for idx, i in enumerate(self.projects):
+ mlog.log(f'Project {idx}:')
+ with mlog.nested():
+ i.log()
+
+class SingleTargetOptions:
+ def __init__(self) -> None:
+ self.opts = {} # type: T.Dict[str, str]
+ self.lang_args = {} # type: T.Dict[str, T.List[str]]
+ self.link_args = [] # type: T.List[str]
+ self.install = 'preserve'
+
+ def set_opt(self, opt: str, val: str) -> None:
+ self.opts[opt] = val
+
+ def append_args(self, lang: str, args: T.List[str]) -> None:
+ if lang not in self.lang_args:
+ self.lang_args[lang] = []
+ self.lang_args[lang] += args
+
+ def append_link_args(self, args: T.List[str]) -> None:
+ self.link_args += args
+
+ def set_install(self, install: bool) -> None:
+ self.install = 'true' if install else 'false'
+
+ def get_override_options(self, initial: T.List[str]) -> T.List[str]:
+ res = [] # type: T.List[str]
+ for i in initial:
+ opt = i[:i.find('=')]
+ if opt not in self.opts:
+ res += [i]
+ res += [f'{k}={v}' for k, v in self.opts.items()]
+ return res
+
+ def get_compile_args(self, lang: str, initial: T.List[str]) -> T.List[str]:
+ if lang in self.lang_args:
+ return initial + self.lang_args[lang]
+ return initial
+
+ def get_link_args(self, initial: T.List[str]) -> T.List[str]:
+ return initial + self.link_args
+
+ def get_install(self, initial: bool) -> bool:
+ return {'preserve': initial, 'true': True, 'false': False}[self.install]
+
+class TargetOptions:
+ def __init__(self) -> None:
+ self.global_options = SingleTargetOptions()
+ self.target_options = {} # type: T.Dict[str, SingleTargetOptions]
+
+ def __getitem__(self, tgt: str) -> SingleTargetOptions:
+ if tgt not in self.target_options:
+ self.target_options[tgt] = SingleTargetOptions()
+ return self.target_options[tgt]
+
+ def get_override_options(self, tgt: str, initial: T.List[str]) -> T.List[str]:
+ initial = self.global_options.get_override_options(initial)
+ if tgt in self.target_options:
+ initial = self.target_options[tgt].get_override_options(initial)
+ return initial
+
+ def get_compile_args(self, tgt: str, lang: str, initial: T.List[str]) -> T.List[str]:
+ initial = self.global_options.get_compile_args(lang, initial)
+ if tgt in self.target_options:
+ initial = self.target_options[tgt].get_compile_args(lang, initial)
+ return initial
+
+ def get_link_args(self, tgt: str, initial: T.List[str]) -> T.List[str]:
+ initial = self.global_options.get_link_args(initial)
+ if tgt in self.target_options:
+ initial = self.target_options[tgt].get_link_args(initial)
+ return initial
+
+ def get_install(self, tgt: str, initial: bool) -> bool:
+ initial = self.global_options.get_install(initial)
+ if tgt in self.target_options:
+ initial = self.target_options[tgt].get_install(initial)
+ return initial
diff --git a/mesonbuild/cmake/data/__init__.py b/mesonbuild/cmake/data/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/mesonbuild/cmake/data/__init__.py
diff --git a/mesonbuild/cmake/data/preload.cmake b/mesonbuild/cmake/data/preload.cmake
new file mode 100644
index 0000000..234860b
--- /dev/null
+++ b/mesonbuild/cmake/data/preload.cmake
@@ -0,0 +1,82 @@
+if(MESON_PS_LOADED)
+ return()
+endif()
+
+set(MESON_PS_LOADED ON)
+
+cmake_policy(PUSH)
+cmake_policy(SET CMP0054 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0054.html
+
+# Dummy macros that have a special meaning in the meson code
+macro(meson_ps_execute_delayed_calls)
+endmacro()
+
+macro(meson_ps_reload_vars)
+endmacro()
+
+macro(meson_ps_disabled_function)
+ message(WARNING "The function '${ARGV0}' is disabled in the context of CMake subprojects.\n"
+ "This should not be an issue but may lead to compilation errors.")
+endmacro()
+
+# Helper macro to inspect the current CMake state
+macro(meson_ps_inspect_vars)
+ set(MESON_PS_CMAKE_CURRENT_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}")
+ set(MESON_PS_CMAKE_CURRENT_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+ meson_ps_execute_delayed_calls()
+endmacro()
+
+
+# Override some system functions with custom code and forward the args
+# to the original function
+macro(add_custom_command)
+ meson_ps_inspect_vars()
+ _add_custom_command(${ARGV})
+endmacro()
+
+macro(add_custom_target)
+ meson_ps_inspect_vars()
+ _add_custom_target(${ARGV})
+endmacro()
+
+macro(set_property)
+ meson_ps_inspect_vars()
+ _set_property(${ARGV})
+endmacro()
+
+function(set_source_files_properties)
+ set(FILES)
+ set(I 0)
+ set(PROPERTIES OFF)
+
+ while(I LESS ARGC)
+ if(NOT PROPERTIES)
+ if("${ARGV${I}}" STREQUAL "PROPERTIES")
+ set(PROPERTIES ON)
+ else()
+ list(APPEND FILES "${ARGV${I}}")
+ endif()
+
+ math(EXPR I "${I} + 1")
+ else()
+ set(ID_IDX ${I})
+ math(EXPR PROP_IDX "${ID_IDX} + 1")
+
+ set(ID "${ARGV${ID_IDX}}")
+ set(PROP "${ARGV${PROP_IDX}}")
+
+ set_property(SOURCE ${FILES} PROPERTY "${ID}" "${PROP}")
+ math(EXPR I "${I} + 2")
+ endif()
+ endwhile()
+endfunction()
+
+# Disable some functions that would mess up the CMake meson integration
+macro(target_precompile_headers)
+ meson_ps_disabled_function(target_precompile_headers)
+endmacro()
+
+set(MESON_PS_DELAYED_CALLS add_custom_command;add_custom_target;set_property)
+meson_ps_reload_vars()
+
+cmake_policy(POP)
diff --git a/mesonbuild/cmake/executor.py b/mesonbuild/cmake/executor.py
new file mode 100644
index 0000000..c22c0ca
--- /dev/null
+++ b/mesonbuild/cmake/executor.py
@@ -0,0 +1,254 @@
+# Copyright 2019 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool.
+from __future__ import annotations
+
+import subprocess as S
+from threading import Thread
+import typing as T
+import re
+import os
+
+from .. import mlog
+from ..mesonlib import PerMachine, Popen_safe, version_compare, is_windows, OptionKey
+from ..programs import find_external_program, NonExistingExternalProgram
+
+if T.TYPE_CHECKING:
+ from pathlib import Path
+
+ from ..environment import Environment
+ from ..mesonlib import MachineChoice
+ from ..programs import ExternalProgram
+
+ TYPE_result = T.Tuple[int, T.Optional[str], T.Optional[str]]
+ TYPE_cache_key = T.Tuple[str, T.Tuple[str, ...], str, T.FrozenSet[T.Tuple[str, str]]]
+
+class CMakeExecutor:
+ # The class's copy of the CMake path. Avoids having to search for it
+ # multiple times in the same Meson invocation.
+ class_cmakebin = PerMachine(None, None) # type: PerMachine[T.Optional[ExternalProgram]]
+ class_cmakevers = PerMachine(None, None) # type: PerMachine[T.Optional[str]]
+ class_cmake_cache = {} # type: T.Dict[T.Any, TYPE_result]
+
+ def __init__(self, environment: 'Environment', version: str, for_machine: MachineChoice, silent: bool = False):
+ self.min_version = version
+ self.environment = environment
+ self.for_machine = for_machine
+ self.cmakebin, self.cmakevers = self.find_cmake_binary(self.environment, silent=silent)
+ self.always_capture_stderr = True
+ self.print_cmout = False
+ self.prefix_paths = [] # type: T.List[str]
+ self.extra_cmake_args = [] # type: T.List[str]
+
+ if self.cmakebin is None:
+ return
+
+ if not version_compare(self.cmakevers, self.min_version):
+ mlog.warning(
+ 'The version of CMake', mlog.bold(self.cmakebin.get_path()),
+ 'is', mlog.bold(self.cmakevers), 'but version', mlog.bold(self.min_version),
+ 'is required')
+ self.cmakebin = None
+ return
+
+ self.prefix_paths = self.environment.coredata.options[OptionKey('cmake_prefix_path', machine=self.for_machine)].value
+ if self.prefix_paths:
+ self.extra_cmake_args += ['-DCMAKE_PREFIX_PATH={}'.format(';'.join(self.prefix_paths))]
+
+ def find_cmake_binary(self, environment: 'Environment', silent: bool = False) -> T.Tuple[T.Optional['ExternalProgram'], T.Optional[str]]:
+ # Only search for CMake the first time and store the result in the class
+ # definition
+ if isinstance(CMakeExecutor.class_cmakebin[self.for_machine], NonExistingExternalProgram):
+ mlog.debug(f'CMake binary for {self.for_machine} is cached as not found')
+ return None, None
+ elif CMakeExecutor.class_cmakebin[self.for_machine] is not None:
+ mlog.debug(f'CMake binary for {self.for_machine} is cached.')
+ else:
+ assert CMakeExecutor.class_cmakebin[self.for_machine] is None
+
+ mlog.debug(f'CMake binary for {self.for_machine} is not cached')
+ for potential_cmakebin in find_external_program(
+ environment, self.for_machine, 'cmake', 'CMake',
+ environment.default_cmake, allow_default_for_cross=False):
+ version_if_ok = self.check_cmake(potential_cmakebin)
+ if not version_if_ok:
+ continue
+ if not silent:
+ mlog.log('Found CMake:', mlog.bold(potential_cmakebin.get_path()),
+ f'({version_if_ok})')
+ CMakeExecutor.class_cmakebin[self.for_machine] = potential_cmakebin
+ CMakeExecutor.class_cmakevers[self.for_machine] = version_if_ok
+ break
+ else:
+ if not silent:
+ mlog.log('Found CMake:', mlog.red('NO'))
+ # Set to False instead of None to signify that we've already
+ # searched for it and not found it
+ CMakeExecutor.class_cmakebin[self.for_machine] = NonExistingExternalProgram()
+ CMakeExecutor.class_cmakevers[self.for_machine] = None
+ return None, None
+
+ return CMakeExecutor.class_cmakebin[self.for_machine], CMakeExecutor.class_cmakevers[self.for_machine]
+
+ def check_cmake(self, cmakebin: 'ExternalProgram') -> T.Optional[str]:
+ if not cmakebin.found():
+ mlog.log(f'Did not find CMake {cmakebin.name!r}')
+ return None
+ try:
+ cmd = cmakebin.get_command()
+ p, out = Popen_safe(cmd + ['--version'])[0:2]
+ if p.returncode != 0:
+ mlog.warning('Found CMake {!r} but couldn\'t run it'
+ ''.format(' '.join(cmd)))
+ return None
+ except FileNotFoundError:
+ mlog.warning('We thought we found CMake {!r} but now it\'s not there. How odd!'
+ ''.format(' '.join(cmd)))
+ return None
+ except PermissionError:
+ msg = 'Found CMake {!r} but didn\'t have permissions to run it.'.format(' '.join(cmd))
+ if not is_windows():
+ msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.'
+ mlog.warning(msg)
+ return None
+
+ cmvers = re.search(r'(cmake|cmake3)\s*version\s*([\d.]+)', out)
+ if cmvers is not None:
+ return cmvers.group(2)
+ mlog.warning(f'We thought we found CMake {cmd!r}, but it was missing the expected '
+ 'version string in its output.')
+ return None
+
+ def set_exec_mode(self, print_cmout: T.Optional[bool] = None, always_capture_stderr: T.Optional[bool] = None) -> None:
+ if print_cmout is not None:
+ self.print_cmout = print_cmout
+ if always_capture_stderr is not None:
+ self.always_capture_stderr = always_capture_stderr
+
+ def _cache_key(self, args: T.List[str], build_dir: Path, env: T.Optional[T.Dict[str, str]]) -> TYPE_cache_key:
+ fenv = frozenset(env.items()) if env is not None else frozenset()
+ targs = tuple(args)
+ return (self.cmakebin.get_path(), targs, build_dir.as_posix(), fenv)
+
+ def _call_cmout_stderr(self, args: T.List[str], build_dir: Path, env: T.Optional[T.Dict[str, str]]) -> TYPE_result:
+ cmd = self.cmakebin.get_command() + args
+ proc = S.Popen(cmd, stdout=S.PIPE, stderr=S.PIPE, cwd=str(build_dir), env=env) # TODO [PYTHON_37]: drop Path conversion
+
+ # stdout and stderr MUST be read at the same time to avoid pipe
+ # blocking issues. The easiest way to do this is with a separate
+ # thread for one of the pipes.
+ def print_stdout() -> None:
+ while True:
+ line = proc.stdout.readline()
+ if not line:
+ break
+ mlog.log(line.decode(errors='ignore').strip('\n'))
+ proc.stdout.close()
+
+ t = Thread(target=print_stdout)
+ t.start()
+
+ try:
+ # Read stderr line by line and log non trace lines
+ raw_trace = ''
+ tline_start_reg = re.compile(r'^\s*(.*\.(cmake|txt))\(([0-9]+)\):\s*(\w+)\(.*$')
+ inside_multiline_trace = False
+ while True:
+ line_raw = proc.stderr.readline()
+ if not line_raw:
+ break
+ line = line_raw.decode(errors='ignore')
+ if tline_start_reg.match(line):
+ raw_trace += line
+ inside_multiline_trace = not line.endswith(' )\n')
+ elif inside_multiline_trace:
+ raw_trace += line
+ else:
+ mlog.warning(line.strip('\n'))
+
+ finally:
+ proc.stderr.close()
+ t.join()
+ proc.wait()
+
+ return proc.returncode, None, raw_trace
+
+ def _call_cmout(self, args: T.List[str], build_dir: Path, env: T.Optional[T.Dict[str, str]]) -> TYPE_result:
+ cmd = self.cmakebin.get_command() + args
+ proc = S.Popen(cmd, stdout=S.PIPE, stderr=S.STDOUT, cwd=str(build_dir), env=env) # TODO [PYTHON_37]: drop Path conversion
+ while True:
+ line = proc.stdout.readline()
+ if not line:
+ break
+ mlog.log(line.decode(errors='ignore').strip('\n'))
+ proc.stdout.close()
+ proc.wait()
+ return proc.returncode, None, None
+
+ def _call_quiet(self, args: T.List[str], build_dir: Path, env: T.Optional[T.Dict[str, str]]) -> TYPE_result:
+ build_dir.mkdir(parents=True, exist_ok=True)
+ cmd = self.cmakebin.get_command() + args
+ ret = S.run(cmd, env=env, cwd=str(build_dir), close_fds=False,
+ stdout=S.PIPE, stderr=S.PIPE, universal_newlines=False) # TODO [PYTHON_37]: drop Path conversion
+ rc = ret.returncode
+ out = ret.stdout.decode(errors='ignore')
+ err = ret.stderr.decode(errors='ignore')
+ return rc, out, err
+
+ def _call_impl(self, args: T.List[str], build_dir: Path, env: T.Optional[T.Dict[str, str]]) -> TYPE_result:
+ mlog.debug(f'Calling CMake ({self.cmakebin.get_command()}) in {build_dir} with:')
+ for i in args:
+ mlog.debug(f' - "{i}"')
+ if not self.print_cmout:
+ return self._call_quiet(args, build_dir, env)
+ else:
+ if self.always_capture_stderr:
+ return self._call_cmout_stderr(args, build_dir, env)
+ else:
+ return self._call_cmout(args, build_dir, env)
+
+ def call(self, args: T.List[str], build_dir: Path, env: T.Optional[T.Dict[str, str]] = None, disable_cache: bool = False) -> TYPE_result:
+ if env is None:
+ env = os.environ.copy()
+
+ args = args + self.extra_cmake_args
+ if disable_cache:
+ return self._call_impl(args, build_dir, env)
+
+ # First check if cached, if not call the real cmake function
+ cache = CMakeExecutor.class_cmake_cache
+ key = self._cache_key(args, build_dir, env)
+ if key not in cache:
+ cache[key] = self._call_impl(args, build_dir, env)
+ return cache[key]
+
+ def found(self) -> bool:
+ return self.cmakebin is not None
+
+ def version(self) -> str:
+ return self.cmakevers
+
+ def executable_path(self) -> str:
+ return self.cmakebin.get_path()
+
+ def get_command(self) -> T.List[str]:
+ return self.cmakebin.get_command()
+
+ def get_cmake_prefix_paths(self) -> T.List[str]:
+ return self.prefix_paths
+
+ def machine_choice(self) -> MachineChoice:
+ return self.for_machine
diff --git a/mesonbuild/cmake/fileapi.py b/mesonbuild/cmake/fileapi.py
new file mode 100644
index 0000000..9605f92
--- /dev/null
+++ b/mesonbuild/cmake/fileapi.py
@@ -0,0 +1,321 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from .common import CMakeException, CMakeBuildFile, CMakeConfiguration
+import typing as T
+from .. import mlog
+from pathlib import Path
+import json
+import re
+
+STRIP_KEYS = ['cmake', 'reply', 'backtrace', 'backtraceGraph', 'version']
+
+class CMakeFileAPI:
+ def __init__(self, build_dir: Path):
+ self.build_dir = build_dir
+ self.api_base_dir = self.build_dir / '.cmake' / 'api' / 'v1'
+ self.request_dir = self.api_base_dir / 'query' / 'client-meson'
+ self.reply_dir = self.api_base_dir / 'reply'
+ self.cmake_sources = [] # type: T.List[CMakeBuildFile]
+ self.cmake_configurations = [] # type: T.List[CMakeConfiguration]
+ self.kind_resolver_map = {
+ 'codemodel': self._parse_codemodel,
+ 'cmakeFiles': self._parse_cmakeFiles,
+ }
+
+ def get_cmake_sources(self) -> T.List[CMakeBuildFile]:
+ return self.cmake_sources
+
+ def get_cmake_configurations(self) -> T.List[CMakeConfiguration]:
+ return self.cmake_configurations
+
+ def setup_request(self) -> None:
+ self.request_dir.mkdir(parents=True, exist_ok=True)
+
+ query = {
+ 'requests': [
+ {'kind': 'codemodel', 'version': {'major': 2, 'minor': 0}},
+ {'kind': 'cmakeFiles', 'version': {'major': 1, 'minor': 0}},
+ ]
+ }
+
+ query_file = self.request_dir / 'query.json'
+ query_file.write_text(json.dumps(query, indent=2), encoding='utf-8')
+
+ def load_reply(self) -> None:
+ if not self.reply_dir.is_dir():
+ raise CMakeException('No response from the CMake file API')
+
+ root = None
+ reg_index = re.compile(r'^index-.*\.json$')
+ for i in self.reply_dir.iterdir():
+ if reg_index.match(i.name):
+ root = i
+ break
+
+ if not root:
+ raise CMakeException('Failed to find the CMake file API index')
+
+ index = self._reply_file_content(root) # Load the root index
+ index = self._strip_data(index) # Avoid loading duplicate files
+ index = self._resolve_references(index) # Load everything
+ index = self._strip_data(index) # Strip unused data (again for loaded files)
+
+ # Debug output
+ debug_json = self.build_dir / '..' / 'fileAPI.json'
+ debug_json = debug_json.resolve()
+ debug_json.write_text(json.dumps(index, indent=2), encoding='utf-8')
+ mlog.cmd_ci_include(debug_json.as_posix())
+
+ # parse the JSON
+ for i in index['objects']:
+ assert isinstance(i, dict)
+ assert 'kind' in i
+ assert i['kind'] in self.kind_resolver_map
+
+ self.kind_resolver_map[i['kind']](i)
+
+ def _parse_codemodel(self, data: T.Dict[str, T.Any]) -> None:
+ assert 'configurations' in data
+ assert 'paths' in data
+
+ source_dir = data['paths']['source']
+ build_dir = data['paths']['build']
+
+ # The file API output differs quite a bit from the server
+ # output. It is more flat than the server output and makes
+ # heavy use of references. Here these references are
+ # resolved and the resulting data structure is identical
+ # to the CMake serve output.
+
+ def helper_parse_dir(dir_entry: T.Dict[str, T.Any]) -> T.Tuple[Path, Path]:
+ src_dir = Path(dir_entry.get('source', '.'))
+ bld_dir = Path(dir_entry.get('build', '.'))
+ src_dir = src_dir if src_dir.is_absolute() else source_dir / src_dir
+ bld_dir = bld_dir if bld_dir.is_absolute() else build_dir / bld_dir
+ src_dir = src_dir.resolve()
+ bld_dir = bld_dir.resolve()
+
+ return src_dir, bld_dir
+
+ def parse_sources(comp_group: T.Dict[str, T.Any], tgt: T.Dict[str, T.Any]) -> T.Tuple[T.List[Path], T.List[Path], T.List[int]]:
+ gen = []
+ src = []
+ idx = []
+
+ src_list_raw = tgt.get('sources', [])
+ for i in comp_group.get('sourceIndexes', []):
+ if i >= len(src_list_raw) or 'path' not in src_list_raw[i]:
+ continue
+ if src_list_raw[i].get('isGenerated', False):
+ gen += [Path(src_list_raw[i]['path'])]
+ else:
+ src += [Path(src_list_raw[i]['path'])]
+ idx += [i]
+
+ return src, gen, idx
+
+ def parse_target(tgt: T.Dict[str, T.Any]) -> T.Dict[str, T.Any]:
+ src_dir, bld_dir = helper_parse_dir(cnf.get('paths', {}))
+
+ # Parse install paths (if present)
+ install_paths = []
+ if 'install' in tgt:
+ prefix = Path(tgt['install']['prefix']['path'])
+ install_paths = [prefix / x['path'] for x in tgt['install']['destinations']]
+ install_paths = list(set(install_paths))
+
+ # On the first look, it looks really nice that the CMake devs have
+ # decided to use arrays for the linker flags. However, this feeling
+ # soon turns into despair when you realize that there only one entry
+ # per type in most cases, and we still have to do manual string splitting.
+ link_flags = []
+ link_libs = []
+ for i in tgt.get('link', {}).get('commandFragments', []):
+ if i['role'] == 'flags':
+ link_flags += [i['fragment']]
+ elif i['role'] == 'libraries':
+ link_libs += [i['fragment']]
+ elif i['role'] == 'libraryPath':
+ link_flags += ['-L{}'.format(i['fragment'])]
+ elif i['role'] == 'frameworkPath':
+ link_flags += ['-F{}'.format(i['fragment'])]
+ for i in tgt.get('archive', {}).get('commandFragments', []):
+ if i['role'] == 'flags':
+ link_flags += [i['fragment']]
+
+ # TODO The `dependencies` entry is new in the file API.
+ # maybe we can make use of that in addition to the
+ # implicit dependency detection
+ tgt_data = {
+ 'artifacts': [Path(x.get('path', '')) for x in tgt.get('artifacts', [])],
+ 'sourceDirectory': src_dir,
+ 'buildDirectory': bld_dir,
+ 'name': tgt.get('name', ''),
+ 'fullName': tgt.get('nameOnDisk', ''),
+ 'hasInstallRule': 'install' in tgt,
+ 'installPaths': install_paths,
+ 'linkerLanguage': tgt.get('link', {}).get('language', 'CXX'),
+ 'linkLibraries': ' '.join(link_libs), # See previous comment block why we join the array
+ 'linkFlags': ' '.join(link_flags), # See previous comment block why we join the array
+ 'type': tgt.get('type', 'EXECUTABLE'),
+ 'fileGroups': [],
+ }
+
+ processed_src_idx = []
+ for cg in tgt.get('compileGroups', []):
+ # Again, why an array, when there is usually only one element
+ # and arguments are separated with spaces...
+ flags = []
+ for i in cg.get('compileCommandFragments', []):
+ flags += [i['fragment']]
+
+ cg_data = {
+ 'defines': [x.get('define', '') for x in cg.get('defines', [])],
+ 'compileFlags': ' '.join(flags),
+ 'language': cg.get('language', 'C'),
+ 'isGenerated': None, # Set later, flag is stored per source file
+ 'sources': [],
+ 'includePath': cg.get('includes', []),
+ }
+
+ normal_src, generated_src, src_idx = parse_sources(cg, tgt)
+ if normal_src:
+ cg_data = dict(cg_data)
+ cg_data['isGenerated'] = False
+ cg_data['sources'] = normal_src
+ tgt_data['fileGroups'] += [cg_data]
+ if generated_src:
+ cg_data = dict(cg_data)
+ cg_data['isGenerated'] = True
+ cg_data['sources'] = generated_src
+ tgt_data['fileGroups'] += [cg_data]
+ processed_src_idx += src_idx
+
+ # Object libraries have no compile groups, only source groups.
+ # So we add all the source files to a dummy source group that were
+ # not found in the previous loop
+ normal_src = []
+ generated_src = []
+ for idx, src in enumerate(tgt.get('sources', [])):
+ if idx in processed_src_idx:
+ continue
+
+ if src.get('isGenerated', False):
+ generated_src += [src['path']]
+ else:
+ normal_src += [src['path']]
+
+ if normal_src:
+ tgt_data['fileGroups'] += [{
+ 'isGenerated': False,
+ 'sources': normal_src,
+ }]
+ if generated_src:
+ tgt_data['fileGroups'] += [{
+ 'isGenerated': True,
+ 'sources': generated_src,
+ }]
+ return tgt_data
+
+ def parse_project(pro: T.Dict[str, T.Any]) -> T.Dict[str, T.Any]:
+ # Only look at the first directory specified in directoryIndexes
+ # TODO Figure out what the other indexes are there for
+ p_src_dir = source_dir
+ p_bld_dir = build_dir
+ try:
+ p_src_dir, p_bld_dir = helper_parse_dir(cnf['directories'][pro['directoryIndexes'][0]])
+ except (IndexError, KeyError):
+ pass
+
+ pro_data = {
+ 'name': pro.get('name', ''),
+ 'sourceDirectory': p_src_dir,
+ 'buildDirectory': p_bld_dir,
+ 'targets': [],
+ }
+
+ for ref in pro.get('targetIndexes', []):
+ tgt = {}
+ try:
+ tgt = cnf['targets'][ref]
+ except (IndexError, KeyError):
+ pass
+ pro_data['targets'] += [parse_target(tgt)]
+
+ return pro_data
+
+ for cnf in data.get('configurations', []):
+ cnf_data = {
+ 'name': cnf.get('name', ''),
+ 'projects': [],
+ }
+
+ for pro in cnf.get('projects', []):
+ cnf_data['projects'] += [parse_project(pro)]
+
+ self.cmake_configurations += [CMakeConfiguration(cnf_data)]
+
+ def _parse_cmakeFiles(self, data: T.Dict[str, T.Any]) -> None:
+ assert 'inputs' in data
+ assert 'paths' in data
+
+ src_dir = Path(data['paths']['source'])
+
+ for i in data['inputs']:
+ path = Path(i['path'])
+ path = path if path.is_absolute() else src_dir / path
+ self.cmake_sources += [CMakeBuildFile(path, i.get('isCMake', False), i.get('isGenerated', False))]
+
+ def _strip_data(self, data: T.Any) -> T.Any:
+ if isinstance(data, list):
+ for idx, i in enumerate(data):
+ data[idx] = self._strip_data(i)
+
+ elif isinstance(data, dict):
+ new = {}
+ for key, val in data.items():
+ if key not in STRIP_KEYS:
+ new[key] = self._strip_data(val)
+ data = new
+
+ return data
+
+ def _resolve_references(self, data: T.Any) -> T.Any:
+ if isinstance(data, list):
+ for idx, i in enumerate(data):
+ data[idx] = self._resolve_references(i)
+
+ elif isinstance(data, dict):
+ # Check for the "magic" reference entry and insert
+ # it into the root data dict
+ if 'jsonFile' in data:
+ data.update(self._reply_file_content(data['jsonFile']))
+
+ for key, val in data.items():
+ data[key] = self._resolve_references(val)
+
+ return data
+
+ def _reply_file_content(self, filename: Path) -> T.Dict[str, T.Any]:
+ real_path = self.reply_dir / filename
+ if not real_path.exists():
+ raise CMakeException(f'File "{real_path}" does not exist')
+
+ data = json.loads(real_path.read_text(encoding='utf-8'))
+ assert isinstance(data, dict)
+ for i in data.keys():
+ assert isinstance(i, str)
+ return data
diff --git a/mesonbuild/cmake/generator.py b/mesonbuild/cmake/generator.py
new file mode 100644
index 0000000..7903dd4
--- /dev/null
+++ b/mesonbuild/cmake/generator.py
@@ -0,0 +1,196 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from .. import mesonlib
+from .. import mlog
+from .common import cmake_is_debug
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .traceparser import CMakeTraceParser, CMakeTarget
+
+def parse_generator_expressions(
+ raw: str,
+ trace: 'CMakeTraceParser',
+ *,
+ context_tgt: T.Optional['CMakeTarget'] = None,
+ ) -> str:
+ '''Parse CMake generator expressions
+
+ Most generator expressions are simply ignored for
+ simplicety, however some are required for some common
+ use cases.
+ '''
+
+ # Early abort if no generator expression present
+ if '$<' not in raw:
+ return raw
+
+ out = '' # type: str
+ i = 0 # type: int
+
+ def equal(arg: str) -> str:
+ col_pos = arg.find(',')
+ if col_pos < 0:
+ return '0'
+ else:
+ return '1' if arg[:col_pos] == arg[col_pos + 1:] else '0'
+
+ def vers_comp(op: str, arg: str) -> str:
+ col_pos = arg.find(',')
+ if col_pos < 0:
+ return '0'
+ else:
+ return '1' if mesonlib.version_compare(arg[:col_pos], '{}{}'.format(op, arg[col_pos + 1:])) else '0'
+
+ def target_property(arg: str) -> str:
+ # We can't really support this since we don't have any context
+ if ',' not in arg:
+ if context_tgt is None:
+ return ''
+ return ';'.join(context_tgt.properties.get(arg, []))
+
+ args = arg.split(',')
+ props = trace.targets[args[0]].properties.get(args[1], []) if args[0] in trace.targets else []
+ return ';'.join(props)
+
+ def target_file(arg: str) -> str:
+ if arg not in trace.targets:
+ mlog.warning(f"Unable to evaluate the cmake variable '$<TARGET_FILE:{arg}>'.")
+ return ''
+ tgt = trace.targets[arg]
+
+ cfgs = []
+ cfg = ''
+
+ if 'IMPORTED_CONFIGURATIONS' in tgt.properties:
+ cfgs = [x for x in tgt.properties['IMPORTED_CONFIGURATIONS'] if x]
+ cfg = cfgs[0]
+
+ if cmake_is_debug(trace.env):
+ if 'DEBUG' in cfgs:
+ cfg = 'DEBUG'
+ elif 'RELEASE' in cfgs:
+ cfg = 'RELEASE'
+ else:
+ if 'RELEASE' in cfgs:
+ cfg = 'RELEASE'
+
+ if f'IMPORTED_IMPLIB_{cfg}' in tgt.properties:
+ return ';'.join([x for x in tgt.properties[f'IMPORTED_IMPLIB_{cfg}'] if x])
+ elif 'IMPORTED_IMPLIB' in tgt.properties:
+ return ';'.join([x for x in tgt.properties['IMPORTED_IMPLIB'] if x])
+ elif f'IMPORTED_LOCATION_{cfg}' in tgt.properties:
+ return ';'.join([x for x in tgt.properties[f'IMPORTED_LOCATION_{cfg}'] if x])
+ elif 'IMPORTED_LOCATION' in tgt.properties:
+ return ';'.join([x for x in tgt.properties['IMPORTED_LOCATION'] if x])
+ return ''
+
+ supported = {
+ # Boolean functions
+ 'BOOL': lambda x: '0' if x.upper() in {'0', 'FALSE', 'OFF', 'N', 'NO', 'IGNORE', 'NOTFOUND'} or x.endswith('-NOTFOUND') else '1',
+ 'AND': lambda x: '1' if all(y == '1' for y in x.split(',')) else '0',
+ 'OR': lambda x: '1' if any(y == '1' for y in x.split(',')) else '0',
+ 'NOT': lambda x: '0' if x == '1' else '1',
+
+ 'IF': lambda x: x.split(',')[1] if x.split(',')[0] == '1' else x.split(',')[2],
+
+ '0': lambda x: '',
+ '1': lambda x: x,
+
+ # String operations
+ 'STREQUAL': equal,
+ 'EQUAL': equal,
+ 'VERSION_LESS': lambda x: vers_comp('<', x),
+ 'VERSION_GREATER': lambda x: vers_comp('>', x),
+ 'VERSION_EQUAL': lambda x: vers_comp('=', x),
+ 'VERSION_LESS_EQUAL': lambda x: vers_comp('<=', x),
+ 'VERSION_GREATER_EQUAL': lambda x: vers_comp('>=', x),
+
+ # String modification
+ 'LOWER_CASE': lambda x: x.lower(),
+ 'UPPER_CASE': lambda x: x.upper(),
+
+ # Always assume the BUILD_INTERFACE is valid.
+ # INSTALL_INTERFACE is always invalid for subprojects and
+ # it should also never appear in CMake config files, used
+ # for dependencies
+ 'INSTALL_INTERFACE': lambda x: '',
+ 'BUILD_INTERFACE': lambda x: x,
+
+ # Constants
+ 'ANGLE-R': lambda x: '>',
+ 'COMMA': lambda x: ',',
+ 'SEMICOLON': lambda x: ';',
+
+ # Target related expressions
+ 'TARGET_EXISTS': lambda x: '1' if x in trace.targets else '0',
+ 'TARGET_NAME_IF_EXISTS': lambda x: x if x in trace.targets else '',
+ 'TARGET_PROPERTY': target_property,
+ 'TARGET_FILE': target_file,
+ } # type: T.Dict[str, T.Callable[[str], str]]
+
+ # Recursively evaluate generator expressions
+ def eval_generator_expressions() -> str:
+ nonlocal i
+ i += 2
+
+ func = '' # type: str
+ args = '' # type: str
+ res = '' # type: str
+ exp = '' # type: str
+
+ # Determine the body of the expression
+ while i < len(raw):
+ if raw[i] == '>':
+ # End of the generator expression
+ break
+ elif i < len(raw) - 1 and raw[i] == '$' and raw[i + 1] == '<':
+ # Nested generator expression
+ exp += eval_generator_expressions()
+ else:
+ # Generator expression body
+ exp += raw[i]
+
+ i += 1
+
+ # Split the expression into a function and arguments part
+ col_pos = exp.find(':')
+ if col_pos < 0:
+ func = exp
+ else:
+ func = exp[:col_pos]
+ args = exp[col_pos + 1:]
+
+ func = func.strip()
+ args = args.strip()
+
+ # Evaluate the function
+ if func in supported:
+ res = supported[func](args)
+
+ return res
+
+ while i < len(raw):
+ if i < len(raw) - 1 and raw[i] == '$' and raw[i + 1] == '<':
+ # Generator expression detected --> try resolving it
+ out += eval_generator_expressions()
+ else:
+ # Normal string, leave unchanged
+ out += raw[i]
+
+ i += 1
+
+ return out
diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py
new file mode 100644
index 0000000..f88d091
--- /dev/null
+++ b/mesonbuild/cmake/interpreter.py
@@ -0,0 +1,1266 @@
+# Copyright 2019 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool.
+from __future__ import annotations
+
+from functools import lru_cache
+from os import environ
+from pathlib import Path
+import re
+import typing as T
+
+from .common import CMakeException, CMakeTarget, language_map, cmake_get_generator_args, check_cmake_args
+from .fileapi import CMakeFileAPI
+from .executor import CMakeExecutor
+from .toolchain import CMakeToolchain, CMakeExecScope
+from .traceparser import CMakeTraceParser
+from .tracetargets import resolve_cmake_trace_targets
+from .. import mlog, mesonlib
+from ..mesonlib import MachineChoice, OrderedSet, path_is_in_root, relative_to_if_possible, OptionKey
+from ..mesondata import DataFile
+from ..compilers.compilers import assembler_suffixes, lang_suffixes, header_suffixes, obj_suffixes, lib_suffixes, is_header
+from ..programs import ExternalProgram
+from ..coredata import FORBIDDEN_TARGET_NAMES
+from ..mparser import (
+ Token,
+ BaseNode,
+ CodeBlockNode,
+ FunctionNode,
+ ArrayNode,
+ ArgumentNode,
+ AssignmentNode,
+ BooleanNode,
+ StringNode,
+ IdNode,
+ IndexNode,
+ MethodNode,
+ NumberNode,
+)
+
+
+if T.TYPE_CHECKING:
+ from .common import CMakeConfiguration, TargetOptions
+ from .traceparser import CMakeGeneratorTarget
+ from .._typing import ImmutableListProtocol
+ from ..build import Build
+ from ..backend.backends import Backend
+ from ..environment import Environment
+
+ TYPE_mixed = T.Union[str, int, bool, Path, BaseNode]
+ TYPE_mixed_list = T.Union[TYPE_mixed, T.Sequence[TYPE_mixed]]
+ TYPE_mixed_kwargs = T.Dict[str, TYPE_mixed_list]
+
+# Disable all warnings automatically enabled with --trace and friends
+# See https://cmake.org/cmake/help/latest/variable/CMAKE_POLICY_WARNING_CMPNNNN.html
+disable_policy_warnings = [
+ 'CMP0025',
+ 'CMP0047',
+ 'CMP0056',
+ 'CMP0060',
+ 'CMP0065',
+ 'CMP0066',
+ 'CMP0067',
+ 'CMP0082',
+ 'CMP0089',
+ 'CMP0102',
+]
+
+target_type_map = {
+ 'STATIC_LIBRARY': 'static_library',
+ 'MODULE_LIBRARY': 'shared_module',
+ 'SHARED_LIBRARY': 'shared_library',
+ 'EXECUTABLE': 'executable',
+ 'OBJECT_LIBRARY': 'static_library',
+ 'INTERFACE_LIBRARY': 'header_only'
+}
+
+skip_targets = ['UTILITY']
+
+blacklist_compiler_flags = [
+ '-Wall', '-Wextra', '-Weverything', '-Werror', '-Wpedantic', '-pedantic', '-w',
+ '/W1', '/W2', '/W3', '/W4', '/Wall', '/WX', '/w',
+ '/O1', '/O2', '/Ob', '/Od', '/Og', '/Oi', '/Os', '/Ot', '/Ox', '/Oy', '/Ob0',
+ '/RTC1', '/RTCc', '/RTCs', '/RTCu',
+ '/Z7', '/Zi', '/ZI',
+]
+
+blacklist_link_flags = [
+ '/machine:x64', '/machine:x86', '/machine:arm', '/machine:ebc',
+ '/debug', '/debug:fastlink', '/debug:full', '/debug:none',
+ '/incremental',
+]
+
+blacklist_clang_cl_link_flags = ['/GR', '/EHsc', '/MDd', '/Zi', '/RTC1']
+
+blacklist_link_libs = [
+ 'kernel32.lib',
+ 'user32.lib',
+ 'gdi32.lib',
+ 'winspool.lib',
+ 'shell32.lib',
+ 'ole32.lib',
+ 'oleaut32.lib',
+ 'uuid.lib',
+ 'comdlg32.lib',
+ 'advapi32.lib'
+]
+
+transfer_dependencies_from = ['header_only']
+
+_cmake_name_regex = re.compile(r'[^_a-zA-Z0-9]')
+def _sanitize_cmake_name(name: str) -> str:
+ name = _cmake_name_regex.sub('_', name)
+ if name in FORBIDDEN_TARGET_NAMES or name.startswith('meson'):
+ name = 'cm_' + name
+ return name
+
+class OutputTargetMap:
+ rm_so_version = re.compile(r'(\.[0-9]+)+$')
+
+ def __init__(self, build_dir: Path):
+ self.tgt_map: T.Dict[str, T.Union['ConverterTarget', 'ConverterCustomTarget']] = {}
+ self.build_dir = build_dir
+
+ def add(self, tgt: T.Union['ConverterTarget', 'ConverterCustomTarget']) -> None:
+ def assign_keys(keys: T.List[str]) -> None:
+ for i in [x for x in keys if x]:
+ self.tgt_map[i] = tgt
+ keys = [self._target_key(tgt.cmake_name)]
+ if isinstance(tgt, ConverterTarget):
+ keys += [tgt.full_name]
+ keys += [self._rel_artifact_key(x) for x in tgt.artifacts]
+ keys += [self._base_artifact_key(x) for x in tgt.artifacts]
+ if isinstance(tgt, ConverterCustomTarget):
+ keys += [self._rel_generated_file_key(x) for x in tgt.original_outputs]
+ keys += [self._base_generated_file_key(x) for x in tgt.original_outputs]
+ assign_keys(keys)
+
+ def _return_first_valid_key(self, keys: T.List[str]) -> T.Optional[T.Union['ConverterTarget', 'ConverterCustomTarget']]:
+ for i in keys:
+ if i and i in self.tgt_map:
+ return self.tgt_map[i]
+ return None
+
+ def target(self, name: str) -> T.Optional[T.Union['ConverterTarget', 'ConverterCustomTarget']]:
+ return self._return_first_valid_key([self._target_key(name)])
+
+ def executable(self, name: str) -> T.Optional['ConverterTarget']:
+ tgt = self.target(name)
+ if tgt is None or not isinstance(tgt, ConverterTarget):
+ return None
+ if tgt.meson_func() != 'executable':
+ return None
+ return tgt
+
+ def artifact(self, name: str) -> T.Optional[T.Union['ConverterTarget', 'ConverterCustomTarget']]:
+ keys = []
+ candidates = [name, OutputTargetMap.rm_so_version.sub('', name)]
+ for i in lib_suffixes:
+ if not name.endswith('.' + i):
+ continue
+ new_name = name[:-len(i) - 1]
+ new_name = OutputTargetMap.rm_so_version.sub('', new_name)
+ candidates += [f'{new_name}.{i}']
+ for i in candidates:
+ keys += [self._rel_artifact_key(Path(i)), Path(i).name, self._base_artifact_key(Path(i))]
+ return self._return_first_valid_key(keys)
+
+ def generated(self, name: Path) -> T.Optional['ConverterCustomTarget']:
+ res = self._return_first_valid_key([self._rel_generated_file_key(name), self._base_generated_file_key(name)])
+ assert res is None or isinstance(res, ConverterCustomTarget)
+ return res
+
+ # Utility functions to generate local keys
+ def _rel_path(self, fname: Path) -> T.Optional[Path]:
+ try:
+ return fname.resolve().relative_to(self.build_dir)
+ except ValueError:
+ pass
+ return None
+
+ def _target_key(self, tgt_name: str) -> str:
+ return f'__tgt_{tgt_name}__'
+
+ def _rel_generated_file_key(self, fname: Path) -> T.Optional[str]:
+ path = self._rel_path(fname)
+ return f'__relgen_{path.as_posix()}__' if path else None
+
+ def _base_generated_file_key(self, fname: Path) -> str:
+ return f'__gen_{fname.name}__'
+
+ def _rel_artifact_key(self, fname: Path) -> T.Optional[str]:
+ path = self._rel_path(fname)
+ return f'__relart_{path.as_posix()}__' if path else None
+
+ def _base_artifact_key(self, fname: Path) -> str:
+ return f'__art_{fname.name}__'
+
+class ConverterTarget:
+ def __init__(self, target: CMakeTarget, env: 'Environment', for_machine: MachineChoice) -> None:
+ self.env = env
+ self.for_machine = for_machine
+ self.artifacts = target.artifacts
+ self.src_dir = target.src_dir
+ self.build_dir = target.build_dir
+ self.name = target.name
+ self.cmake_name = target.name
+ self.full_name = target.full_name
+ self.type = target.type
+ self.install = target.install
+ self.install_dir: T.Optional[Path] = None
+ self.link_libraries = target.link_libraries
+ self.link_flags = target.link_flags + target.link_lang_flags
+ self.depends_raw: T.List[str] = []
+ self.depends: T.List[T.Union[ConverterTarget, ConverterCustomTarget]] = []
+
+ if target.install_paths:
+ self.install_dir = target.install_paths[0]
+
+ self.languages: T.Set[str] = set()
+ self.sources: T.List[Path] = []
+ self.generated: T.List[Path] = []
+ self.generated_ctgt: T.List[CustomTargetReference] = []
+ self.includes: T.List[Path] = []
+ self.sys_includes: T.List[Path] = []
+ self.link_with: T.List[T.Union[ConverterTarget, ConverterCustomTarget]] = []
+ self.object_libs: T.List[ConverterTarget] = []
+ self.compile_opts: T.Dict[str, T.List[str]] = {}
+ self.public_compile_opts: T.List[str] = []
+ self.pie = False
+
+ # Project default override options (c_std, cpp_std, etc.)
+ self.override_options: T.List[str] = []
+
+ # Convert the target name to a valid meson target name
+ self.name = _sanitize_cmake_name(self.name)
+
+ self.generated_raw: T.List[Path] = []
+
+ for i in target.files:
+ languages: T.Set[str] = set()
+ src_suffixes: T.Set[str] = set()
+
+ # Insert suffixes
+ for j in i.sources:
+ if not j.suffix:
+ continue
+ src_suffixes.add(j.suffix[1:])
+
+ # Determine the meson language(s)
+ # Extract the default language from the explicit CMake field
+ lang_cmake_to_meson = {val.lower(): key for key, val in language_map.items()}
+ languages.add(lang_cmake_to_meson.get(i.language.lower(), 'c'))
+
+ # Determine missing languages from the source suffixes
+ for sfx in src_suffixes:
+ for key, val in lang_suffixes.items():
+ if sfx in val:
+ languages.add(key)
+ break
+
+ # Register the new languages and initialize the compile opts array
+ for lang in languages:
+ self.languages.add(lang)
+ if lang not in self.compile_opts:
+ self.compile_opts[lang] = []
+
+ # Add arguments, but avoid duplicates
+ args = i.flags
+ args += [f'-D{x}' for x in i.defines]
+ for lang in languages:
+ self.compile_opts[lang] += [x for x in args if x not in self.compile_opts[lang]]
+
+ # Handle include directories
+ self.includes += [x.path for x in i.includes if x.path not in self.includes and not x.isSystem]
+ self.sys_includes += [x.path for x in i.includes if x.path not in self.sys_includes and x.isSystem]
+
+ # Add sources to the right array
+ if i.is_generated:
+ self.generated_raw += i.sources
+ else:
+ self.sources += i.sources
+
+ def __repr__(self) -> str:
+ return f'<{self.__class__.__name__}: {self.name}>'
+
+ std_regex = re.compile(r'([-]{1,2}std=|/std:v?|[-]{1,2}std:)(.*)')
+
+ def postprocess(self, output_target_map: OutputTargetMap, root_src_dir: Path, subdir: Path, install_prefix: Path, trace: CMakeTraceParser) -> None:
+ # Detect setting the C and C++ standard and do additional compiler args manipulation
+ for i in ['c', 'cpp']:
+ if i not in self.compile_opts:
+ continue
+
+ temp = []
+ for j in self.compile_opts[i]:
+ m = ConverterTarget.std_regex.match(j)
+ ctgt = output_target_map.generated(Path(j))
+ if m:
+ std = m.group(2)
+ supported = self._all_lang_stds(i)
+ if std not in supported:
+ mlog.warning(
+ 'Unknown {0}_std "{1}" -> Ignoring. Try setting the project-'
+ 'level {0}_std if build errors occur. Known '
+ '{0}_stds are: {2}'.format(i, std, ' '.join(supported)),
+ once=True
+ )
+ continue
+ self.override_options += [f'{i}_std={std}']
+ elif j in {'-fPIC', '-fpic', '-fPIE', '-fpie'}:
+ self.pie = True
+ elif isinstance(ctgt, ConverterCustomTarget):
+ # Sometimes projects pass generated source files as compiler
+ # flags. Add these as generated sources to ensure that the
+ # corresponding custom target is run.2
+ self.generated_raw += [Path(j)]
+ temp += [j]
+ elif j in blacklist_compiler_flags:
+ pass
+ else:
+ temp += [j]
+
+ self.compile_opts[i] = temp
+
+ # Make sure to force enable -fPIC for OBJECT libraries
+ if self.type.upper() == 'OBJECT_LIBRARY':
+ self.pie = True
+
+ # Use the CMake trace, if required
+ tgt = trace.targets.get(self.cmake_name)
+ if tgt:
+ self.depends_raw = trace.targets[self.cmake_name].depends
+
+ rtgt = resolve_cmake_trace_targets(self.cmake_name, trace, self.env)
+ self.includes += [Path(x) for x in rtgt.include_directories]
+ self.link_flags += rtgt.link_flags
+ self.public_compile_opts += rtgt.public_compile_opts
+ self.link_libraries += rtgt.libraries
+
+ elif self.type.upper() not in ['EXECUTABLE', 'OBJECT_LIBRARY']:
+ mlog.warning('CMake: Target', mlog.bold(self.cmake_name), 'not found in CMake trace. This can lead to build errors')
+
+ temp = []
+ for i in self.link_libraries:
+ # Let meson handle this arcane magic
+ if ',-rpath,' in i:
+ continue
+ if not Path(i).is_absolute():
+ link_with = output_target_map.artifact(i)
+ if link_with:
+ self.link_with += [link_with]
+ continue
+
+ temp += [i]
+ self.link_libraries = temp
+
+ # Filter out files that are not supported by the language
+ supported = list(assembler_suffixes) + list(header_suffixes) + list(obj_suffixes)
+ for i in self.languages:
+ supported += list(lang_suffixes[i])
+ supported = [f'.{x}' for x in supported]
+ self.sources = [x for x in self.sources if any(x.name.endswith(y) for y in supported)]
+ self.generated_raw = [x for x in self.generated_raw if any(x.name.endswith(y) for y in supported)]
+
+ # Make paths relative
+ def rel_path(x: Path, is_header: bool, is_generated: bool) -> T.Optional[Path]:
+ if not x.is_absolute():
+ x = self.src_dir / x
+ x = x.resolve()
+ assert x.is_absolute()
+ if not x.exists() and not any(x.name.endswith(y) for y in obj_suffixes) and not is_generated:
+ if path_is_in_root(x, Path(self.env.get_build_dir()), resolve=True):
+ x.mkdir(parents=True, exist_ok=True)
+ return x.relative_to(Path(self.env.get_build_dir()) / subdir)
+ else:
+ mlog.warning('CMake: path', mlog.bold(x.as_posix()), 'does not exist.')
+ mlog.warning(' --> Ignoring. This can lead to build errors.')
+ return None
+ if x in trace.explicit_headers:
+ return None
+ if (
+ path_is_in_root(x, Path(self.env.get_source_dir()))
+ and not (
+ path_is_in_root(x, root_src_dir) or
+ path_is_in_root(x, Path(self.env.get_build_dir()))
+ )
+ ):
+ mlog.warning('CMake: path', mlog.bold(x.as_posix()), 'is inside the root project but', mlog.bold('not'), 'inside the subproject.')
+ mlog.warning(' --> Ignoring. This can lead to build errors.')
+ return None
+ if path_is_in_root(x, Path(self.env.get_build_dir())) and is_header:
+ return x.relative_to(Path(self.env.get_build_dir()) / subdir)
+ if path_is_in_root(x, root_src_dir):
+ return x.relative_to(root_src_dir)
+ return x
+
+ build_dir_rel = self.build_dir.relative_to(Path(self.env.get_build_dir()) / subdir)
+ self.generated_raw = [rel_path(x, False, True) for x in self.generated_raw]
+ self.includes = list(OrderedSet([rel_path(x, True, False) for x in OrderedSet(self.includes)] + [build_dir_rel]))
+ self.sys_includes = list(OrderedSet([rel_path(x, True, False) for x in OrderedSet(self.sys_includes)]))
+ self.sources = [rel_path(x, False, False) for x in self.sources]
+
+ # Resolve custom targets
+ for gen_file in self.generated_raw:
+ ctgt = output_target_map.generated(gen_file)
+ if ctgt:
+ assert isinstance(ctgt, ConverterCustomTarget)
+ ref = ctgt.get_ref(gen_file)
+ assert isinstance(ref, CustomTargetReference) and ref.valid()
+ self.generated_ctgt += [ref]
+ elif gen_file is not None:
+ self.generated += [gen_file]
+
+ # Remove delete entries
+ self.includes = [x for x in self.includes if x is not None]
+ self.sys_includes = [x for x in self.sys_includes if x is not None]
+ self.sources = [x for x in self.sources if x is not None]
+
+ # Make sure '.' is always in the include directories
+ if Path('.') not in self.includes:
+ self.includes += [Path('.')]
+
+ # make install dir relative to the install prefix
+ if self.install_dir and self.install_dir.is_absolute():
+ if path_is_in_root(self.install_dir, install_prefix):
+ self.install_dir = self.install_dir.relative_to(install_prefix)
+
+ # Remove blacklisted options and libs
+ def check_flag(flag: str) -> bool:
+ if flag.lower() in blacklist_link_flags or flag in blacklist_compiler_flags + blacklist_clang_cl_link_flags:
+ return False
+ if flag.startswith('/D'):
+ return False
+ return True
+
+ self.link_libraries = [x for x in self.link_libraries if x.lower() not in blacklist_link_libs]
+ self.link_flags = [x for x in self.link_flags if check_flag(x)]
+
+ # Handle OSX frameworks
+ def handle_frameworks(flags: T.List[str]) -> T.List[str]:
+ res: T.List[str] = []
+ for i in flags:
+ p = Path(i)
+ if not p.exists() or not p.name.endswith('.framework'):
+ res += [i]
+ continue
+ res += ['-framework', p.stem]
+ return res
+
+ self.link_libraries = handle_frameworks(self.link_libraries)
+ self.link_flags = handle_frameworks(self.link_flags)
+
+ # Handle explicit CMake add_dependency() calls
+ for i in self.depends_raw:
+ dep_tgt = output_target_map.target(i)
+ if dep_tgt:
+ self.depends.append(dep_tgt)
+
+ def process_object_libs(self, obj_target_list: T.List['ConverterTarget'], linker_workaround: bool) -> None:
+ # Try to detect the object library(s) from the generated input sources
+ temp = [x for x in self.generated if any(x.name.endswith('.' + y) for y in obj_suffixes)]
+ stem = [x.stem for x in temp]
+ exts = self._all_source_suffixes()
+ # Temp now stores the source filenames of the object files
+ for i in obj_target_list:
+ source_files = [x.name for x in i.sources + i.generated]
+ for j in stem:
+ # On some platforms (specifically looking at you Windows with vs20xy backend) CMake does
+ # not produce object files with the format `foo.cpp.obj`, instead it skipps the language
+ # suffix and just produces object files like `foo.obj`. Thus we have to do our best to
+ # undo this step and guess the correct language suffix of the object file. This is done
+ # by trying all language suffixes meson knows and checking if one of them fits.
+ candidates = [j]
+ if not any(j.endswith('.' + x) for x in exts):
+ mlog.warning('Object files do not contain source file extensions, thus falling back to guessing them.', once=True)
+ candidates += [f'{j}.{x}' for x in exts]
+ if any(x in source_files for x in candidates):
+ if linker_workaround:
+ self._append_objlib_sources(i)
+ else:
+ self.includes += i.includes
+ self.includes = list(OrderedSet(self.includes))
+ self.object_libs += [i]
+ break
+
+ # Filter out object files from the sources
+ self.generated = [x for x in self.generated if not any(x.name.endswith('.' + y) for y in obj_suffixes)]
+
+ def _append_objlib_sources(self, tgt: 'ConverterTarget') -> None:
+ self.includes += tgt.includes
+ self.sources += tgt.sources
+ self.generated += tgt.generated
+ self.generated_ctgt += tgt.generated_ctgt
+ self.includes = list(OrderedSet(self.includes))
+ self.sources = list(OrderedSet(self.sources))
+ self.generated = list(OrderedSet(self.generated))
+ self.generated_ctgt = list(OrderedSet(self.generated_ctgt))
+
+ # Inherit compiler arguments since they may be required for building
+ for lang, opts in tgt.compile_opts.items():
+ if lang not in self.compile_opts:
+ self.compile_opts[lang] = []
+ self.compile_opts[lang] += [x for x in opts if x not in self.compile_opts[lang]]
+
+ @lru_cache(maxsize=None)
+ def _all_source_suffixes(self) -> 'ImmutableListProtocol[str]':
+ suffixes: T.List[str] = []
+ for exts in lang_suffixes.values():
+ suffixes.extend(exts)
+ return suffixes
+
+ @lru_cache(maxsize=None)
+ def _all_lang_stds(self, lang: str) -> 'ImmutableListProtocol[str]':
+ try:
+ res = self.env.coredata.options[OptionKey('std', machine=MachineChoice.BUILD, lang=lang)].choices
+ except KeyError:
+ return []
+
+ # TODO: Get rid of this once we have proper typing for options
+ assert isinstance(res, list)
+ for i in res:
+ assert isinstance(i, str)
+
+ return res
+
+ def process_inter_target_dependencies(self) -> None:
+ # Move the dependencies from all transfer_dependencies_from to the target
+ to_process = list(self.depends)
+ processed = []
+ new_deps = []
+ for i in to_process:
+ processed += [i]
+ if isinstance(i, ConverterTarget) and i.meson_func() in transfer_dependencies_from:
+ to_process += [x for x in i.depends if x not in processed]
+ else:
+ new_deps += [i]
+ self.depends = list(OrderedSet(new_deps))
+
+ def cleanup_dependencies(self) -> None:
+ # Clear the dependencies from targets that where moved from
+ if self.meson_func() in transfer_dependencies_from:
+ self.depends = []
+
+ def meson_func(self) -> str:
+ return target_type_map.get(self.type.upper())
+
+ def log(self) -> None:
+ mlog.log('Target', mlog.bold(self.name), f'({self.cmake_name})')
+ mlog.log(' -- artifacts: ', mlog.bold(str(self.artifacts)))
+ mlog.log(' -- full_name: ', mlog.bold(self.full_name))
+ mlog.log(' -- type: ', mlog.bold(self.type))
+ mlog.log(' -- install: ', mlog.bold('true' if self.install else 'false'))
+ mlog.log(' -- install_dir: ', mlog.bold(self.install_dir.as_posix() if self.install_dir else ''))
+ mlog.log(' -- link_libraries: ', mlog.bold(str(self.link_libraries)))
+ mlog.log(' -- link_with: ', mlog.bold(str(self.link_with)))
+ mlog.log(' -- object_libs: ', mlog.bold(str(self.object_libs)))
+ mlog.log(' -- link_flags: ', mlog.bold(str(self.link_flags)))
+ mlog.log(' -- languages: ', mlog.bold(str(self.languages)))
+ mlog.log(' -- includes: ', mlog.bold(str(self.includes)))
+ mlog.log(' -- sys_includes: ', mlog.bold(str(self.sys_includes)))
+ mlog.log(' -- sources: ', mlog.bold(str(self.sources)))
+ mlog.log(' -- generated: ', mlog.bold(str(self.generated)))
+ mlog.log(' -- generated_ctgt: ', mlog.bold(str(self.generated_ctgt)))
+ mlog.log(' -- pie: ', mlog.bold('true' if self.pie else 'false'))
+ mlog.log(' -- override_opts: ', mlog.bold(str(self.override_options)))
+ mlog.log(' -- depends: ', mlog.bold(str(self.depends)))
+ mlog.log(' -- options:')
+ for key, val in self.compile_opts.items():
+ mlog.log(' -', key, '=', mlog.bold(str(val)))
+
+class CustomTargetReference:
+ def __init__(self, ctgt: 'ConverterCustomTarget', index: int) -> None:
+ self.ctgt = ctgt
+ self.index = index
+
+ def __repr__(self) -> str:
+ if self.valid():
+ return '<{}: {} [{}]>'.format(self.__class__.__name__, self.ctgt.name, self.ctgt.outputs[self.index])
+ else:
+ return f'<{self.__class__.__name__}: INVALID REFERENCE>'
+
+ def valid(self) -> bool:
+ return self.ctgt is not None and self.index >= 0
+
+ def filename(self) -> str:
+ return self.ctgt.outputs[self.index]
+
+class ConverterCustomTarget:
+ tgt_counter = 0
+ out_counter = 0
+
+ def __init__(self, target: CMakeGeneratorTarget, env: 'Environment', for_machine: MachineChoice) -> None:
+ assert target.current_bin_dir is not None
+ assert target.current_src_dir is not None
+ self.name = target.name
+ if not self.name:
+ self.name = f'custom_tgt_{ConverterCustomTarget.tgt_counter}'
+ ConverterCustomTarget.tgt_counter += 1
+ self.cmake_name = str(self.name)
+ self.original_outputs = list(target.outputs)
+ self.outputs = [x.name for x in self.original_outputs]
+ self.conflict_map: T.Dict[str, str] = {}
+ self.command: T.List[T.List[T.Union[str, ConverterTarget]]] = []
+ self.working_dir = target.working_dir
+ self.depends_raw = target.depends
+ self.inputs: T.List[T.Union[str, CustomTargetReference]] = []
+ self.depends: T.List[T.Union[ConverterTarget, ConverterCustomTarget]] = []
+ self.current_bin_dir = target.current_bin_dir
+ self.current_src_dir = target.current_src_dir
+ self.env = env
+ self.for_machine = for_machine
+ self._raw_target = target
+
+ # Convert the target name to a valid meson target name
+ self.name = _sanitize_cmake_name(self.name)
+
+ def __repr__(self) -> str:
+ return f'<{self.__class__.__name__}: {self.name} {self.outputs}>'
+
+ def postprocess(self, output_target_map: OutputTargetMap, root_src_dir: Path, all_outputs: T.List[str], trace: CMakeTraceParser) -> None:
+ # Default the working directory to ${CMAKE_CURRENT_BINARY_DIR}
+ if self.working_dir is None:
+ self.working_dir = self.current_bin_dir
+
+ # relative paths in the working directory are always relative
+ # to ${CMAKE_CURRENT_BINARY_DIR}
+ if not self.working_dir.is_absolute():
+ self.working_dir = self.current_bin_dir / self.working_dir
+
+ # Modify the original outputs if they are relative. Again,
+ # relative paths are relative to ${CMAKE_CURRENT_BINARY_DIR}
+ def ensure_absolute(x: Path) -> Path:
+ if x.is_absolute():
+ return x
+ else:
+ return self.current_bin_dir / x
+ self.original_outputs = [ensure_absolute(x) for x in self.original_outputs]
+
+ # Ensure that there is no duplicate output in the project so
+ # that meson can handle cases where the same filename is
+ # generated in multiple directories
+ temp_outputs: T.List[str] = []
+ for i in self.outputs:
+ if i in all_outputs:
+ old = str(i)
+ i = f'c{ConverterCustomTarget.out_counter}_{i}'
+ ConverterCustomTarget.out_counter += 1
+ self.conflict_map[old] = i
+ all_outputs += [i]
+ temp_outputs += [i]
+ self.outputs = temp_outputs
+
+ # Check if the command is a build target
+ commands: T.List[T.List[T.Union[str, ConverterTarget]]] = []
+ for curr_cmd in self._raw_target.command:
+ assert isinstance(curr_cmd, list)
+ assert curr_cmd[0] != '', "An empty string is not a valid executable"
+ cmd: T.List[T.Union[str, ConverterTarget]] = []
+
+ for j in curr_cmd:
+ if not j:
+ continue
+ target = output_target_map.executable(j)
+ if target:
+ # When cross compiling, binaries have to be executed with an exe_wrapper (for instance wine for mingw-w64)
+ if self.env.exe_wrapper is not None and self.env.properties[self.for_machine].get_cmake_use_exe_wrapper():
+ assert isinstance(self.env.exe_wrapper, ExternalProgram)
+ cmd += self.env.exe_wrapper.get_command()
+ cmd += [target]
+ continue
+ elif j in trace.targets:
+ trace_tgt = trace.targets[j]
+ if trace_tgt.type == 'EXECUTABLE' and 'IMPORTED_LOCATION' in trace_tgt.properties:
+ cmd += trace_tgt.properties['IMPORTED_LOCATION']
+ continue
+ mlog.debug(f'CMake: Found invalid CMake target "{j}" --> ignoring \n{trace_tgt}')
+
+ # Fallthrough on error
+ cmd += [j]
+
+ commands += [cmd]
+ self.command = commands
+
+ # If the custom target does not declare any output, create a dummy
+ # one that can be used as dependency.
+ if not self.outputs:
+ self.outputs = [self.name + '.h']
+
+ # Check dependencies and input files
+ for i in self.depends_raw:
+ if not i:
+ continue
+ raw = Path(i)
+ art = output_target_map.artifact(i)
+ tgt = output_target_map.target(i)
+ gen = output_target_map.generated(raw)
+
+ rel_to_root = None
+ try:
+ rel_to_root = raw.relative_to(root_src_dir)
+ except ValueError:
+ rel_to_root = None
+
+ # First check for existing files. Only then check for existing
+ # targets, etc. This reduces the chance of misdetecting input files
+ # as outputs from other targets.
+ # See https://github.com/mesonbuild/meson/issues/6632
+ if not raw.is_absolute() and (self.current_src_dir / raw).is_file():
+ self.inputs += [(self.current_src_dir / raw).relative_to(root_src_dir).as_posix()]
+ elif raw.is_absolute() and raw.exists() and rel_to_root is not None:
+ self.inputs += [rel_to_root.as_posix()]
+ elif art:
+ self.depends += [art]
+ elif tgt:
+ self.depends += [tgt]
+ elif gen:
+ ctgt_ref = gen.get_ref(raw)
+ assert ctgt_ref is not None
+ self.inputs += [ctgt_ref]
+
+ def process_inter_target_dependencies(self) -> None:
+ # Move the dependencies from all transfer_dependencies_from to the target
+ to_process = list(self.depends)
+ processed = []
+ new_deps = []
+ for i in to_process:
+ processed += [i]
+ if isinstance(i, ConverterTarget) and i.meson_func() in transfer_dependencies_from:
+ to_process += [x for x in i.depends if x not in processed]
+ else:
+ new_deps += [i]
+ self.depends = list(OrderedSet(new_deps))
+
+ def get_ref(self, fname: Path) -> T.Optional[CustomTargetReference]:
+ name = fname.name
+ try:
+ if name in self.conflict_map:
+ name = self.conflict_map[name]
+ idx = self.outputs.index(name)
+ return CustomTargetReference(self, idx)
+ except ValueError:
+ return None
+
+ def log(self) -> None:
+ mlog.log('Custom Target', mlog.bold(self.name), f'({self.cmake_name})')
+ mlog.log(' -- command: ', mlog.bold(str(self.command)))
+ mlog.log(' -- outputs: ', mlog.bold(str(self.outputs)))
+ mlog.log(' -- conflict_map: ', mlog.bold(str(self.conflict_map)))
+ mlog.log(' -- working_dir: ', mlog.bold(str(self.working_dir)))
+ mlog.log(' -- depends_raw: ', mlog.bold(str(self.depends_raw)))
+ mlog.log(' -- inputs: ', mlog.bold(str(self.inputs)))
+ mlog.log(' -- depends: ', mlog.bold(str(self.depends)))
+
+class CMakeInterpreter:
+ def __init__(self, build: 'Build', subdir: Path, src_dir: Path, install_prefix: Path, env: 'Environment', backend: 'Backend'):
+ self.build = build
+ self.subdir = subdir
+ self.src_dir = src_dir
+ self.build_dir_rel = subdir / '__CMake_build'
+ self.build_dir = Path(env.get_build_dir()) / self.build_dir_rel
+ self.install_prefix = install_prefix
+ self.env = env
+ self.for_machine = MachineChoice.HOST # TODO make parameter
+ self.backend_name = backend.name
+ self.linkers: T.Set[str] = set()
+ self.fileapi = CMakeFileAPI(self.build_dir)
+
+ # Raw CMake results
+ self.bs_files: T.List[Path] = []
+ self.codemodel_configs: T.Optional[T.List[CMakeConfiguration]] = None
+ self.cmake_stderr: T.Optional[str] = None
+
+ # Analysed data
+ self.project_name = ''
+ self.languages: T.List[str] = []
+ self.targets: T.List[ConverterTarget] = []
+ self.custom_targets: T.List[ConverterCustomTarget] = []
+ self.trace: CMakeTraceParser
+ self.output_target_map = OutputTargetMap(self.build_dir)
+
+ # Generated meson data
+ self.generated_targets: T.Dict[str, T.Dict[str, T.Optional[str]]] = {}
+ self.internal_name_map: T.Dict[str, str] = {}
+
+ # Do some special handling for object libraries for certain configurations
+ self._object_lib_workaround = False
+ if self.backend_name.startswith('vs'):
+ for comp in self.env.coredata.compilers[self.for_machine].values():
+ if comp.get_linker_id() == 'link':
+ self._object_lib_workaround = True
+ break
+
+ def configure(self, extra_cmake_options: T.List[str]) -> CMakeExecutor:
+ # Find CMake
+ # TODO: Using MachineChoice.BUILD should always be correct here, but also evaluate the use of self.for_machine
+ cmake_exe = CMakeExecutor(self.env, '>=3.14', MachineChoice.BUILD)
+ if not cmake_exe.found():
+ raise CMakeException('Unable to find CMake')
+ self.trace = CMakeTraceParser(cmake_exe.version(), self.build_dir, self.env, permissive=True)
+
+ preload_file = DataFile('cmake/data/preload.cmake').write_to_private(self.env)
+ toolchain = CMakeToolchain(cmake_exe, self.env, self.for_machine, CMakeExecScope.SUBPROJECT, self.build_dir, preload_file)
+ toolchain_file = toolchain.write()
+
+ # TODO: drop this check once the deprecated `cmake_args` kwarg is removed
+ extra_cmake_options = check_cmake_args(extra_cmake_options)
+
+ cmake_args = []
+ cmake_args += cmake_get_generator_args(self.env)
+ cmake_args += [f'-DCMAKE_INSTALL_PREFIX={self.install_prefix}']
+ cmake_args += extra_cmake_options
+ trace_args = self.trace.trace_args()
+ cmcmp_args = [f'-DCMAKE_POLICY_WARNING_{x}=OFF' for x in disable_policy_warnings]
+
+ self.fileapi.setup_request()
+
+ # Run CMake
+ mlog.log()
+ with mlog.nested():
+ mlog.log('Configuring the build directory with', mlog.bold('CMake'), 'version', mlog.cyan(cmake_exe.version()))
+ mlog.log(mlog.bold('Running CMake with:'), ' '.join(cmake_args))
+ mlog.log(mlog.bold(' - build directory: '), self.build_dir.as_posix())
+ mlog.log(mlog.bold(' - source directory: '), self.src_dir.as_posix())
+ mlog.log(mlog.bold(' - toolchain file: '), toolchain_file.as_posix())
+ mlog.log(mlog.bold(' - preload file: '), preload_file.as_posix())
+ mlog.log(mlog.bold(' - trace args: '), ' '.join(trace_args))
+ mlog.log(mlog.bold(' - disabled policy warnings:'), '[{}]'.format(', '.join(disable_policy_warnings)))
+ mlog.log()
+ self.build_dir.mkdir(parents=True, exist_ok=True)
+ os_env = environ.copy()
+ os_env['LC_ALL'] = 'C'
+ final_args = cmake_args + trace_args + cmcmp_args + toolchain.get_cmake_args() + [self.src_dir.as_posix()]
+
+ cmake_exe.set_exec_mode(print_cmout=True, always_capture_stderr=self.trace.requires_stderr())
+ rc, _, self.cmake_stderr = cmake_exe.call(final_args, self.build_dir, env=os_env, disable_cache=True)
+
+ mlog.log()
+ h = mlog.green('SUCCEEDED') if rc == 0 else mlog.red('FAILED')
+ mlog.log('CMake configuration:', h)
+ if rc != 0:
+ # get the last CMake error - We only need the message function for this:
+ self.trace.functions = {'message': self.trace.functions['message']}
+ self.trace.parse(self.cmake_stderr)
+ error = f': {self.trace.errors[-1]}' if self.trace.errors else ''
+ raise CMakeException(f'Failed to configure the CMake subproject{error}')
+
+ return cmake_exe
+
+ def initialise(self, extra_cmake_options: T.List[str]) -> None:
+ # Configure the CMake project to generate the file API data
+ self.configure(extra_cmake_options)
+
+ # Parse the result
+ self.fileapi.load_reply()
+
+ # Load the buildsystem file list
+ cmake_files = self.fileapi.get_cmake_sources()
+ self.bs_files = [x.file for x in cmake_files if not x.is_cmake and not x.is_temp]
+ self.bs_files = [relative_to_if_possible(x, Path(self.env.get_source_dir())) for x in self.bs_files]
+ self.bs_files = [x for x in self.bs_files if not path_is_in_root(x, Path(self.env.get_build_dir()), resolve=True)]
+ self.bs_files = list(OrderedSet(self.bs_files))
+
+ # Load the codemodel configurations
+ self.codemodel_configs = self.fileapi.get_cmake_configurations()
+
+ def analyse(self) -> None:
+ if self.codemodel_configs is None:
+ raise CMakeException('CMakeInterpreter was not initialized')
+
+ # Clear analyser data
+ self.project_name = ''
+ self.languages = []
+ self.targets = []
+ self.custom_targets = []
+
+ # Parse the trace
+ self.trace.parse(self.cmake_stderr)
+
+ # Find all targets
+ added_target_names: T.List[str] = []
+ for i_0 in self.codemodel_configs:
+ for j_0 in i_0.projects:
+ if not self.project_name:
+ self.project_name = j_0.name
+ for k_0 in j_0.targets:
+ # Avoid duplicate targets from different configurations and known
+ # dummy CMake internal target types
+ if k_0.type not in skip_targets and k_0.name not in added_target_names:
+ added_target_names += [k_0.name]
+ self.targets += [ConverterTarget(k_0, self.env, self.for_machine)]
+
+ # Add interface targets from trace, if not already present.
+ # This step is required because interface targets were removed from
+ # the CMake file API output.
+ api_target_name_list = [x.name for x in self.targets]
+ for i_1 in self.trace.targets.values():
+ if i_1.type != 'INTERFACE' or i_1.name in api_target_name_list or i_1.imported:
+ continue
+ dummy = CMakeTarget({
+ 'name': i_1.name,
+ 'type': 'INTERFACE_LIBRARY',
+ 'sourceDirectory': self.src_dir,
+ 'buildDirectory': self.build_dir,
+ })
+ self.targets += [ConverterTarget(dummy, self.env, self.for_machine)]
+
+ for i_2 in self.trace.custom_targets:
+ self.custom_targets += [ConverterCustomTarget(i_2, self.env, self.for_machine)]
+
+ # generate the output_target_map
+ for i_3 in [*self.targets, *self.custom_targets]:
+ assert isinstance(i_3, (ConverterTarget, ConverterCustomTarget))
+ self.output_target_map.add(i_3)
+
+ # First pass: Basic target cleanup
+ object_libs = []
+ custom_target_outputs: T.List[str] = []
+ for ctgt in self.custom_targets:
+ ctgt.postprocess(self.output_target_map, self.src_dir, custom_target_outputs, self.trace)
+ for tgt in self.targets:
+ tgt.postprocess(self.output_target_map, self.src_dir, self.subdir, self.install_prefix, self.trace)
+ if tgt.type == 'OBJECT_LIBRARY':
+ object_libs += [tgt]
+ self.languages += [x for x in tgt.languages if x not in self.languages]
+
+ # Second pass: Detect object library dependencies
+ for tgt in self.targets:
+ tgt.process_object_libs(object_libs, self._object_lib_workaround)
+
+ # Third pass: Reassign dependencies to avoid some loops
+ for tgt in self.targets:
+ tgt.process_inter_target_dependencies()
+ for ctgt in self.custom_targets:
+ ctgt.process_inter_target_dependencies()
+
+ # Fourth pass: Remove rassigned dependencies
+ for tgt in self.targets:
+ tgt.cleanup_dependencies()
+
+ mlog.log('CMake project', mlog.bold(self.project_name), 'has', mlog.bold(str(len(self.targets) + len(self.custom_targets))), 'build targets.')
+
+ def pretend_to_be_meson(self, options: TargetOptions) -> CodeBlockNode:
+ if not self.project_name:
+ raise CMakeException('CMakeInterpreter was not analysed')
+
+ def token(tid: str = 'string', val: TYPE_mixed = '') -> Token:
+ return Token(tid, self.subdir.as_posix(), 0, 0, 0, None, val)
+
+ def string(value: str) -> StringNode:
+ return StringNode(token(val=value))
+
+ def id_node(value: str) -> IdNode:
+ return IdNode(token(val=value))
+
+ def number(value: int) -> NumberNode:
+ return NumberNode(token(val=value))
+
+ def nodeify(value: TYPE_mixed_list) -> BaseNode:
+ if isinstance(value, str):
+ return string(value)
+ if isinstance(value, Path):
+ return string(value.as_posix())
+ elif isinstance(value, bool):
+ return BooleanNode(token(val=value))
+ elif isinstance(value, int):
+ return number(value)
+ elif isinstance(value, list):
+ return array(value)
+ elif isinstance(value, BaseNode):
+ return value
+ raise RuntimeError('invalid type of value: {} ({})'.format(type(value).__name__, str(value)))
+
+ def indexed(node: BaseNode, index: int) -> IndexNode:
+ return IndexNode(node, nodeify(index))
+
+ def array(elements: TYPE_mixed_list) -> ArrayNode:
+ args = ArgumentNode(token())
+ if not isinstance(elements, list):
+ elements = [args]
+ args.arguments += [nodeify(x) for x in elements if x is not None]
+ return ArrayNode(args, 0, 0, 0, 0)
+
+ def function(name: str, args: T.Optional[TYPE_mixed_list] = None, kwargs: T.Optional[TYPE_mixed_kwargs] = None) -> FunctionNode:
+ args = [] if args is None else args
+ kwargs = {} if kwargs is None else kwargs
+ args_n = ArgumentNode(token())
+ if not isinstance(args, list):
+ assert isinstance(args, (str, int, bool, Path, BaseNode))
+ args = [args]
+ args_n.arguments = [nodeify(x) for x in args if x is not None]
+ args_n.kwargs = {id_node(k): nodeify(v) for k, v in kwargs.items() if v is not None}
+ func_n = FunctionNode(self.subdir.as_posix(), 0, 0, 0, 0, name, args_n)
+ return func_n
+
+ def method(obj: BaseNode, name: str, args: T.Optional[TYPE_mixed_list] = None, kwargs: T.Optional[TYPE_mixed_kwargs] = None) -> MethodNode:
+ args = [] if args is None else args
+ kwargs = {} if kwargs is None else kwargs
+ args_n = ArgumentNode(token())
+ if not isinstance(args, list):
+ assert isinstance(args, (str, int, bool, Path, BaseNode))
+ args = [args]
+ args_n.arguments = [nodeify(x) for x in args if x is not None]
+ args_n.kwargs = {id_node(k): nodeify(v) for k, v in kwargs.items() if v is not None}
+ return MethodNode(self.subdir.as_posix(), 0, 0, obj, name, args_n)
+
+ def assign(var_name: str, value: BaseNode) -> AssignmentNode:
+ return AssignmentNode(self.subdir.as_posix(), 0, 0, var_name, value)
+
+ # Generate the root code block and the project function call
+ root_cb = CodeBlockNode(token())
+ root_cb.lines += [function('project', [self.project_name] + self.languages)]
+
+ # Add the run script for custom commands
+
+ # Add the targets
+ processing: T.List[str] = []
+ processed: T.Dict[str, T.Dict[str, T.Optional[str]]] = {}
+ name_map: T.Dict[str, str] = {}
+
+ def extract_tgt(tgt: T.Union[ConverterTarget, ConverterCustomTarget, CustomTargetReference]) -> IdNode:
+ tgt_name = None
+ if isinstance(tgt, (ConverterTarget, ConverterCustomTarget)):
+ tgt_name = tgt.name
+ elif isinstance(tgt, CustomTargetReference):
+ tgt_name = tgt.ctgt.name
+ assert tgt_name is not None and tgt_name in processed
+ res_var = processed[tgt_name]['tgt']
+ return id_node(res_var) if res_var else None
+
+ def detect_cycle(tgt: T.Union[ConverterTarget, ConverterCustomTarget]) -> None:
+ if tgt.name in processing:
+ raise CMakeException('Cycle in CMake inputs/dependencies detected')
+ processing.append(tgt.name)
+
+ def resolve_ctgt_ref(ref: CustomTargetReference) -> T.Union[IdNode, IndexNode]:
+ tgt_var = extract_tgt(ref)
+ if len(ref.ctgt.outputs) == 1:
+ return tgt_var
+ else:
+ return indexed(tgt_var, ref.index)
+
+ def process_target(tgt: ConverterTarget) -> None:
+ detect_cycle(tgt)
+
+ # First handle inter target dependencies
+ link_with: T.List[IdNode] = []
+ objec_libs: T.List[IdNode] = []
+ sources: T.List[Path] = []
+ generated: T.List[T.Union[IdNode, IndexNode]] = []
+ generated_filenames: T.List[str] = []
+ custom_targets: T.List[ConverterCustomTarget] = []
+ dependencies: T.List[IdNode] = []
+ for i in tgt.link_with:
+ assert isinstance(i, ConverterTarget)
+ if i.name not in processed:
+ process_target(i)
+ link_with += [extract_tgt(i)]
+ for i in tgt.object_libs:
+ assert isinstance(i, ConverterTarget)
+ if i.name not in processed:
+ process_target(i)
+ objec_libs += [extract_tgt(i)]
+ for i in tgt.depends:
+ if not isinstance(i, ConverterCustomTarget):
+ continue
+ if i.name not in processed:
+ process_custom_target(i)
+ dependencies += [extract_tgt(i)]
+
+ # Generate the source list and handle generated sources
+ sources += tgt.sources
+ sources += tgt.generated
+
+ for ctgt_ref in tgt.generated_ctgt:
+ ctgt = ctgt_ref.ctgt
+ if ctgt.name not in processed:
+ process_custom_target(ctgt)
+ generated += [resolve_ctgt_ref(ctgt_ref)]
+ generated_filenames += [ctgt_ref.filename()]
+ if ctgt not in custom_targets:
+ custom_targets += [ctgt]
+
+ # Add all header files from all used custom targets. This
+ # ensures that all custom targets are built before any
+ # sources of the current target are compiled and thus all
+ # header files are present. This step is necessary because
+ # CMake always ensures that a custom target is executed
+ # before another target if at least one output is used.
+ for ctgt in custom_targets:
+ for j in ctgt.outputs:
+ if not is_header(j) or j in generated_filenames:
+ continue
+
+ generated += [resolve_ctgt_ref(ctgt.get_ref(Path(j)))]
+ generated_filenames += [j]
+
+ # Determine the meson function to use for the build target
+ tgt_func = tgt.meson_func()
+ if not tgt_func:
+ raise CMakeException(f'Unknown target type "{tgt.type}"')
+
+ # Determine the variable names
+ inc_var = f'{tgt.name}_inc'
+ dir_var = f'{tgt.name}_dir'
+ sys_var = f'{tgt.name}_sys'
+ src_var = f'{tgt.name}_src'
+ dep_var = f'{tgt.name}_dep'
+ tgt_var = tgt.name
+
+ install_tgt = options.get_install(tgt.cmake_name, tgt.install)
+
+ # Generate target kwargs
+ tgt_kwargs: TYPE_mixed_kwargs = {
+ 'build_by_default': install_tgt,
+ 'link_args': options.get_link_args(tgt.cmake_name, tgt.link_flags + tgt.link_libraries),
+ 'link_with': link_with,
+ 'include_directories': id_node(inc_var),
+ 'install': install_tgt,
+ 'override_options': options.get_override_options(tgt.cmake_name, tgt.override_options),
+ 'objects': [method(x, 'extract_all_objects') for x in objec_libs],
+ }
+
+ # Only set if installed and only override if it is set
+ if install_tgt and tgt.install_dir:
+ tgt_kwargs['install_dir'] = tgt.install_dir
+
+ # Handle compiler args
+ for key, val in tgt.compile_opts.items():
+ tgt_kwargs[f'{key}_args'] = options.get_compile_args(tgt.cmake_name, key, val)
+
+ # Handle -fPCI, etc
+ if tgt_func == 'executable':
+ tgt_kwargs['pie'] = tgt.pie
+ elif tgt_func == 'static_library':
+ tgt_kwargs['pic'] = tgt.pie
+
+ # declare_dependency kwargs
+ dep_kwargs: TYPE_mixed_kwargs = {
+ 'link_args': tgt.link_flags + tgt.link_libraries,
+ 'link_with': id_node(tgt_var),
+ 'compile_args': tgt.public_compile_opts,
+ 'include_directories': id_node(inc_var),
+ }
+
+ if dependencies:
+ generated += dependencies
+
+ # Generate the function nodes
+ dir_node = assign(dir_var, function('include_directories', tgt.includes))
+ sys_node = assign(sys_var, function('include_directories', tgt.sys_includes, {'is_system': True}))
+ inc_node = assign(inc_var, array([id_node(dir_var), id_node(sys_var)]))
+ node_list = [dir_node, sys_node, inc_node]
+ if tgt_func == 'header_only':
+ del dep_kwargs['link_with']
+ dep_node = assign(dep_var, function('declare_dependency', kwargs=dep_kwargs))
+ node_list += [dep_node]
+ src_var = None
+ tgt_var = None
+ else:
+ src_node = assign(src_var, function('files', sources))
+ tgt_node = assign(tgt_var, function(tgt_func, [tgt_var, id_node(src_var), *generated], tgt_kwargs))
+ node_list += [src_node, tgt_node]
+ if tgt_func in {'static_library', 'shared_library'}:
+ dep_node = assign(dep_var, function('declare_dependency', kwargs=dep_kwargs))
+ node_list += [dep_node]
+ elif tgt_func == 'shared_module':
+ del dep_kwargs['link_with']
+ dep_node = assign(dep_var, function('declare_dependency', kwargs=dep_kwargs))
+ node_list += [dep_node]
+ else:
+ dep_var = None
+
+ # Add the nodes to the ast
+ root_cb.lines += node_list
+ processed[tgt.name] = {'inc': inc_var, 'src': src_var, 'dep': dep_var, 'tgt': tgt_var, 'func': tgt_func}
+ name_map[tgt.cmake_name] = tgt.name
+
+ def process_custom_target(tgt: ConverterCustomTarget) -> None:
+ # CMake allows to specify multiple commands in a custom target.
+ # To map this to meson, a helper script is used to execute all
+ # commands in order. This additionally allows setting the working
+ # directory.
+
+ detect_cycle(tgt)
+ tgt_var = tgt.name
+
+ def resolve_source(x: T.Union[str, ConverterTarget, ConverterCustomTarget, CustomTargetReference]) -> T.Union[str, IdNode, IndexNode]:
+ if isinstance(x, ConverterTarget):
+ if x.name not in processed:
+ process_target(x)
+ return extract_tgt(x)
+ if isinstance(x, ConverterCustomTarget):
+ if x.name not in processed:
+ process_custom_target(x)
+ return extract_tgt(x)
+ elif isinstance(x, CustomTargetReference):
+ if x.ctgt.name not in processed:
+ process_custom_target(x.ctgt)
+ return resolve_ctgt_ref(x)
+ else:
+ return x
+
+ # Generate the command list
+ command: T.List[T.Union[str, IdNode, IndexNode]] = []
+ command += mesonlib.get_meson_command()
+ command += ['--internal', 'cmake_run_ctgt']
+ command += ['-o', '@OUTPUT@']
+ if tgt.original_outputs:
+ command += ['-O'] + [x.as_posix() for x in tgt.original_outputs]
+ command += ['-d', tgt.working_dir.as_posix()]
+
+ # Generate the commands. Subcommands are separated by ';;;'
+ for cmd in tgt.command:
+ command += [resolve_source(x) for x in cmd] + [';;;']
+
+ tgt_kwargs: TYPE_mixed_kwargs = {
+ 'input': [resolve_source(x) for x in tgt.inputs],
+ 'output': tgt.outputs,
+ 'command': command,
+ 'depends': [resolve_source(x) for x in tgt.depends],
+ }
+
+ root_cb.lines += [assign(tgt_var, function('custom_target', [tgt.name], tgt_kwargs))]
+ processed[tgt.name] = {'inc': None, 'src': None, 'dep': None, 'tgt': tgt_var, 'func': 'custom_target'}
+ name_map[tgt.cmake_name] = tgt.name
+
+ # Now generate the target function calls
+ for ctgt in self.custom_targets:
+ if ctgt.name not in processed:
+ process_custom_target(ctgt)
+ for tgt in self.targets:
+ if tgt.name not in processed:
+ process_target(tgt)
+
+ self.generated_targets = processed
+ self.internal_name_map = name_map
+ return root_cb
+
+ def target_info(self, target: str) -> T.Optional[T.Dict[str, str]]:
+ # Try resolving the target name
+ # start by checking if there is a 100% match (excluding the name prefix)
+ prx_tgt = _sanitize_cmake_name(target)
+ if prx_tgt in self.generated_targets:
+ return self.generated_targets[prx_tgt]
+ # check if there exists a name mapping
+ if target in self.internal_name_map:
+ target = self.internal_name_map[target]
+ assert target in self.generated_targets
+ return self.generated_targets[target]
+ return None
+
+ def target_list(self) -> T.List[str]:
+ return list(self.internal_name_map.keys())
diff --git a/mesonbuild/cmake/toolchain.py b/mesonbuild/cmake/toolchain.py
new file mode 100644
index 0000000..477629e
--- /dev/null
+++ b/mesonbuild/cmake/toolchain.py
@@ -0,0 +1,258 @@
+# Copyright 2020 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.
+from __future__ import annotations
+
+from pathlib import Path
+from .traceparser import CMakeTraceParser
+from ..envconfig import CMakeSkipCompilerTest
+from .common import language_map, cmake_get_generator_args
+from .. import mlog
+
+import shutil
+import typing as T
+from enum import Enum
+from textwrap import dedent
+
+if T.TYPE_CHECKING:
+ from .executor import CMakeExecutor
+ from ..environment import Environment
+ from ..compilers import Compiler
+ from ..mesonlib import MachineChoice
+
+class CMakeExecScope(Enum):
+ SUBPROJECT = 'subproject'
+ DEPENDENCY = 'dependency'
+
+class CMakeToolchain:
+ def __init__(self, cmakebin: 'CMakeExecutor', env: 'Environment', for_machine: MachineChoice, exec_scope: CMakeExecScope, build_dir: Path, preload_file: T.Optional[Path] = None) -> None:
+ self.env = env
+ self.cmakebin = cmakebin
+ self.for_machine = for_machine
+ self.exec_scope = exec_scope
+ self.preload_file = preload_file
+ self.build_dir = build_dir
+ self.build_dir = self.build_dir.resolve()
+ self.toolchain_file = build_dir / 'CMakeMesonToolchainFile.cmake'
+ self.cmcache_file = build_dir / 'CMakeCache.txt'
+ self.minfo = self.env.machines[self.for_machine]
+ self.properties = self.env.properties[self.for_machine]
+ self.compilers = self.env.coredata.compilers[self.for_machine]
+ self.cmakevars = self.env.cmakevars[self.for_machine]
+ self.cmakestate = self.env.coredata.cmake_cache[self.for_machine]
+
+ self.variables = self.get_defaults()
+ self.variables.update(self.cmakevars.get_variables())
+
+ # Determine whether CMake the compiler test should be skipped
+ skip_status = self.properties.get_cmake_skip_compiler_test()
+ self.skip_check = skip_status == CMakeSkipCompilerTest.ALWAYS
+ if skip_status == CMakeSkipCompilerTest.DEP_ONLY and self.exec_scope == CMakeExecScope.DEPENDENCY:
+ self.skip_check = True
+ if not self.properties.get_cmake_defaults():
+ self.skip_check = False
+
+ assert self.toolchain_file.is_absolute()
+
+ def write(self) -> Path:
+ if not self.toolchain_file.parent.exists():
+ self.toolchain_file.parent.mkdir(parents=True)
+ self.toolchain_file.write_text(self.generate(), encoding='utf-8')
+ self.cmcache_file.write_text(self.generate_cache(), encoding='utf-8')
+ mlog.cmd_ci_include(self.toolchain_file.as_posix())
+ return self.toolchain_file
+
+ def get_cmake_args(self) -> T.List[str]:
+ args = ['-DCMAKE_TOOLCHAIN_FILE=' + self.toolchain_file.as_posix()]
+ if self.preload_file is not None:
+ args += ['-DMESON_PRELOAD_FILE=' + self.preload_file.as_posix()]
+ return args
+
+ @staticmethod
+ def _print_vars(vars: T.Dict[str, T.List[str]]) -> str:
+ res = ''
+ for key, value in vars.items():
+ res += 'set(' + key
+ for i in value:
+ res += f' "{i}"'
+ res += ')\n'
+ return res
+
+ def generate(self) -> str:
+ res = dedent('''\
+ ######################################
+ ### AUTOMATICALLY GENERATED FILE ###
+ ######################################
+
+ # This file was generated from the configuration in the
+ # relevant meson machine file. See the meson documentation
+ # https://mesonbuild.com/Machine-files.html for more information
+
+ if(DEFINED MESON_PRELOAD_FILE)
+ include("${MESON_PRELOAD_FILE}")
+ endif()
+
+ ''')
+
+ # Escape all \ in the values
+ for key, value in self.variables.items():
+ self.variables[key] = [x.replace('\\', '/') for x in value]
+
+ # Set compiler
+ if self.skip_check:
+ self.update_cmake_compiler_state()
+ res += '# CMake compiler state variables\n'
+ for lang, vars in self.cmakestate:
+ res += f'# -- Variables for language {lang}\n'
+ res += self._print_vars(vars)
+ res += '\n'
+ res += '\n'
+
+ # Set variables from the current machine config
+ res += '# Variables from meson\n'
+ res += self._print_vars(self.variables)
+ res += '\n'
+
+ # Add the user provided toolchain file
+ user_file = self.properties.get_cmake_toolchain_file()
+ if user_file is not None:
+ res += dedent('''
+ # Load the CMake toolchain file specified by the user
+ include("{}")
+
+ '''.format(user_file.as_posix()))
+
+ return res
+
+ def generate_cache(self) -> str:
+ if not self.skip_check:
+ return ''
+
+ res = ''
+ for name, v in self.cmakestate.cmake_cache.items():
+ res += f'{name}:{v.type}={";".join(v.value)}\n'
+ return res
+
+ def get_defaults(self) -> T.Dict[str, T.List[str]]:
+ defaults = {} # type: T.Dict[str, T.List[str]]
+
+ # Do nothing if the user does not want automatic defaults
+ if not self.properties.get_cmake_defaults():
+ return defaults
+
+ # Best effort to map the meson system name to CMAKE_SYSTEM_NAME, which
+ # is not trivial since CMake lacks a list of all supported
+ # CMAKE_SYSTEM_NAME values.
+ SYSTEM_MAP = {
+ 'android': 'Android',
+ 'linux': 'Linux',
+ 'windows': 'Windows',
+ 'freebsd': 'FreeBSD',
+ 'darwin': 'Darwin',
+ } # type: T.Dict[str, str]
+
+ # Only set these in a cross build. Otherwise CMake will trip up in native
+ # builds and thing they are cross (which causes TRY_RUN() to break)
+ if self.env.is_cross_build(when_building_for=self.for_machine):
+ defaults['CMAKE_SYSTEM_NAME'] = [SYSTEM_MAP.get(self.minfo.system, self.minfo.system)]
+ defaults['CMAKE_SYSTEM_PROCESSOR'] = [self.minfo.cpu_family]
+
+ defaults['CMAKE_SIZEOF_VOID_P'] = ['8' if self.minfo.is_64_bit else '4']
+
+ sys_root = self.properties.get_sys_root()
+ if sys_root:
+ defaults['CMAKE_SYSROOT'] = [sys_root]
+
+ def make_abs(exe: str) -> str:
+ if Path(exe).is_absolute():
+ return exe
+
+ p = shutil.which(exe)
+ if p is None:
+ return exe
+ return p
+
+ # Set the compiler variables
+ for lang, comp_obj in self.compilers.items():
+ prefix = 'CMAKE_{}_'.format(language_map.get(lang, lang.upper()))
+
+ exe_list = comp_obj.get_exelist()
+ if not exe_list:
+ continue
+
+ if len(exe_list) >= 2 and not self.is_cmdline_option(comp_obj, exe_list[1]):
+ defaults[prefix + 'COMPILER_LAUNCHER'] = [make_abs(exe_list[0])]
+ exe_list = exe_list[1:]
+
+ exe_list[0] = make_abs(exe_list[0])
+ defaults[prefix + 'COMPILER'] = exe_list
+ if comp_obj.get_id() == 'clang-cl':
+ defaults['CMAKE_LINKER'] = comp_obj.get_linker_exelist()
+
+ return defaults
+
+ @staticmethod
+ def is_cmdline_option(compiler: 'Compiler', arg: str) -> bool:
+ if compiler.get_argument_syntax() == 'msvc':
+ return arg.startswith('/')
+ else:
+ return arg.startswith('-')
+
+ def update_cmake_compiler_state(self) -> None:
+ # Check if all variables are already cached
+ if self.cmakestate.languages.issuperset(self.compilers.keys()):
+ return
+
+ # Generate the CMakeLists.txt
+ mlog.debug('CMake Toolchain: Calling CMake once to generate the compiler state')
+ languages = list(self.compilers.keys())
+ lang_ids = [language_map.get(x, x.upper()) for x in languages]
+ cmake_content = dedent(f'''
+ cmake_minimum_required(VERSION 3.7)
+ project(CompInfo {' '.join(lang_ids)})
+ ''')
+
+ build_dir = Path(self.env.scratch_dir) / '__CMake_compiler_info__'
+ build_dir.mkdir(parents=True, exist_ok=True)
+ cmake_file = build_dir / 'CMakeLists.txt'
+ cmake_file.write_text(cmake_content, encoding='utf-8')
+
+ # Generate the temporary toolchain file
+ temp_toolchain_file = build_dir / 'CMakeMesonTempToolchainFile.cmake'
+ temp_toolchain_file.write_text(CMakeToolchain._print_vars(self.variables), encoding='utf-8')
+
+ # Configure
+ trace = CMakeTraceParser(self.cmakebin.version(), build_dir, self.env)
+ self.cmakebin.set_exec_mode(print_cmout=False, always_capture_stderr=trace.requires_stderr())
+ cmake_args = []
+ cmake_args += trace.trace_args()
+ cmake_args += cmake_get_generator_args(self.env)
+ cmake_args += [f'-DCMAKE_TOOLCHAIN_FILE={temp_toolchain_file.as_posix()}', '.']
+ rc, _, raw_trace = self.cmakebin.call(cmake_args, build_dir=build_dir, disable_cache=True)
+
+ if rc != 0:
+ mlog.warning('CMake Toolchain: Failed to determine CMake compilers state')
+ return
+
+ # Parse output
+ trace.parse(raw_trace)
+ self.cmakestate.cmake_cache = {**trace.cache}
+
+ vars_by_file = {k.name: v for (k, v) in trace.vars_by_file.items()}
+
+ for lang in languages:
+ lang_cmake = language_map.get(lang, lang.upper())
+ file_name = f'CMake{lang_cmake}Compiler.cmake'
+ vars = vars_by_file.setdefault(file_name, {})
+ vars[f'CMAKE_{lang_cmake}_COMPILER_FORCED'] = ['1']
+ self.cmakestate.update(lang, vars)
diff --git a/mesonbuild/cmake/traceparser.py b/mesonbuild/cmake/traceparser.py
new file mode 100644
index 0000000..5fcba80
--- /dev/null
+++ b/mesonbuild/cmake/traceparser.py
@@ -0,0 +1,825 @@
+# Copyright 2019 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool.
+from __future__ import annotations
+
+from .common import CMakeException
+from .generator import parse_generator_expressions
+from .. import mlog
+from ..mesonlib import version_compare
+
+import typing as T
+from pathlib import Path
+from functools import lru_cache
+import re
+import json
+import textwrap
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+
+class CMakeTraceLine:
+ def __init__(self, file_str: str, line: int, func: str, args: T.List[str]) -> None:
+ self.file = CMakeTraceLine._to_path(file_str)
+ self.line = line
+ self.func = func.lower()
+ self.args = args
+
+ @staticmethod
+ @lru_cache(maxsize=None)
+ def _to_path(file_str: str) -> Path:
+ return Path(file_str)
+
+ def __repr__(self) -> str:
+ s = 'CMake TRACE: {0}:{1} {2}({3})'
+ return s.format(self.file, self.line, self.func, self.args)
+
+class CMakeCacheEntry(T.NamedTuple):
+ value: T.List[str]
+ type: str
+
+class CMakeTarget:
+ def __init__(
+ self,
+ name: str,
+ target_type: str,
+ properties: T.Optional[T.Dict[str, T.List[str]]] = None,
+ imported: bool = False,
+ tline: T.Optional[CMakeTraceLine] = None
+ ):
+ if properties is None:
+ properties = {}
+ self.name = name
+ self.type = target_type
+ self.properties = properties
+ self.imported = imported
+ self.tline = tline
+ self.depends = [] # type: T.List[str]
+ self.current_bin_dir = None # type: T.Optional[Path]
+ self.current_src_dir = None # type: T.Optional[Path]
+
+ def __repr__(self) -> str:
+ s = 'CMake TARGET:\n -- name: {}\n -- type: {}\n -- imported: {}\n -- properties: {{\n{} }}\n -- tline: {}'
+ propSTR = ''
+ for i in self.properties:
+ propSTR += " '{}': {}\n".format(i, self.properties[i])
+ return s.format(self.name, self.type, self.imported, propSTR, self.tline)
+
+ def strip_properties(self) -> None:
+ # Strip the strings in the properties
+ if not self.properties:
+ return
+ for key, val in self.properties.items():
+ self.properties[key] = [x.strip() for x in val]
+ assert all(';' not in x for x in self.properties[key])
+
+class CMakeGeneratorTarget(CMakeTarget):
+ def __init__(self, name: str) -> None:
+ super().__init__(name, 'CUSTOM', {})
+ self.outputs = [] # type: T.List[Path]
+ self._outputs_str = [] # type: T.List[str]
+ self.command = [] # type: T.List[T.List[str]]
+ self.working_dir = None # type: T.Optional[Path]
+
+class CMakeTraceParser:
+ def __init__(self, cmake_version: str, build_dir: Path, env: 'Environment', permissive: bool = True) -> None:
+ self.vars: T.Dict[str, T.List[str]] = {}
+ self.vars_by_file: T.Dict[Path, T.Dict[str, T.List[str]]] = {}
+ self.targets: T.Dict[str, CMakeTarget] = {}
+ self.cache: T.Dict[str, CMakeCacheEntry] = {}
+
+ self.explicit_headers = set() # type: T.Set[Path]
+
+ # T.List of targes that were added with add_custom_command to generate files
+ self.custom_targets = [] # type: T.List[CMakeGeneratorTarget]
+
+ self.env = env
+ self.permissive = permissive # type: bool
+ self.cmake_version = cmake_version # type: str
+ self.trace_file = 'cmake_trace.txt'
+ self.trace_file_path = build_dir / self.trace_file
+ self.trace_format = 'json-v1' if version_compare(cmake_version, '>=3.17') else 'human'
+
+ self.errors: T.List[str] = []
+
+ # State for delayed command execution. Delayed command execution is realised
+ # with a custom CMake file that overrides some functions and adds some
+ # introspection information to the trace.
+ self.delayed_commands = [] # type: T.List[str]
+ self.stored_commands = [] # type: T.List[CMakeTraceLine]
+
+ # All supported functions
+ self.functions = {
+ 'set': self._cmake_set,
+ 'unset': self._cmake_unset,
+ 'add_executable': self._cmake_add_executable,
+ 'add_library': self._cmake_add_library,
+ 'add_custom_command': self._cmake_add_custom_command,
+ 'add_custom_target': self._cmake_add_custom_target,
+ 'set_property': self._cmake_set_property,
+ 'set_target_properties': self._cmake_set_target_properties,
+ 'target_compile_definitions': self._cmake_target_compile_definitions,
+ 'target_compile_options': self._cmake_target_compile_options,
+ 'target_include_directories': self._cmake_target_include_directories,
+ 'target_link_libraries': self._cmake_target_link_libraries,
+ 'target_link_options': self._cmake_target_link_options,
+ 'add_dependencies': self._cmake_add_dependencies,
+ 'message': self._cmake_message,
+
+ # Special functions defined in the preload script.
+ # These functions do nothing in the CMake code, but have special
+ # meaning here in the trace parser.
+ 'meson_ps_execute_delayed_calls': self._meson_ps_execute_delayed_calls,
+ 'meson_ps_reload_vars': self._meson_ps_reload_vars,
+ 'meson_ps_disabled_function': self._meson_ps_disabled_function,
+ } # type: T.Dict[str, T.Callable[[CMakeTraceLine], None]]
+
+ if version_compare(self.cmake_version, '<3.17.0'):
+ mlog.deprecation(textwrap.dedent(f'''\
+ CMake support for versions <3.17 is deprecated since Meson 0.62.0.
+ |
+ | However, Meson was only able to find CMake {self.cmake_version}.
+ |
+ | Support for all CMake versions below 3.17.0 will be removed once
+ | newer CMake versions are more widely adopted. If you encounter
+ | any errors please try upgrading CMake to a newer version first.
+ '''), once=True)
+
+ def trace_args(self) -> T.List[str]:
+ arg_map = {
+ 'human': ['--trace', '--trace-expand'],
+ 'json-v1': ['--trace-expand', '--trace-format=json-v1'],
+ }
+
+ base_args = ['--no-warn-unused-cli']
+ if not self.requires_stderr():
+ base_args += [f'--trace-redirect={self.trace_file}']
+
+ return arg_map[self.trace_format] + base_args
+
+ def requires_stderr(self) -> bool:
+ return version_compare(self.cmake_version, '<3.16')
+
+ def parse(self, trace: T.Optional[str] = None) -> None:
+ # First load the trace (if required)
+ if not self.requires_stderr():
+ if not self.trace_file_path.exists and not self.trace_file_path.is_file():
+ raise CMakeException(f'CMake: Trace file "{self.trace_file_path!s}" not found')
+ trace = self.trace_file_path.read_text(errors='ignore', encoding='utf-8')
+ if not trace:
+ raise CMakeException('CMake: The CMake trace was not provided or is empty')
+
+ # Second parse the trace
+ lexer1 = None
+ if self.trace_format == 'human':
+ lexer1 = self._lex_trace_human(trace)
+ elif self.trace_format == 'json-v1':
+ lexer1 = self._lex_trace_json(trace)
+ else:
+ raise CMakeException(f'CMake: Internal error: Invalid trace format {self.trace_format}. Expected [human, json-v1]')
+
+ # Primary pass -- parse everything
+ for l in lexer1:
+ # store the function if its execution should be delayed
+ if l.func in self.delayed_commands:
+ self.stored_commands += [l]
+ continue
+
+ # "Execute" the CMake function if supported
+ fn = self.functions.get(l.func, None)
+ if fn:
+ fn(l)
+
+ # Evaluate generator expressions
+ strlist_gen: T.Callable[[T.List[str]], T.List[str]] = lambda strlist: parse_generator_expressions(';'.join(strlist), self).split(';') if strlist else []
+ pathlist_gen: T.Callable[[T.List[str]], T.List[Path]] = lambda strlist: [Path(x) for x in parse_generator_expressions(';'.join(strlist), self).split(';')] if strlist else []
+
+ self.vars = {k: strlist_gen(v) for k, v in self.vars.items()}
+ self.vars_by_file = {
+ p: {k: strlist_gen(v) for k, v in d.items()}
+ for p, d in self.vars_by_file.items()
+ }
+ self.explicit_headers = {Path(parse_generator_expressions(str(x), self)) for x in self.explicit_headers}
+ self.cache = {
+ k: CMakeCacheEntry(
+ strlist_gen(v.value),
+ v.type
+ )
+ for k, v in self.cache.items()
+ }
+
+ for tgt in self.targets.values():
+ tgtlist_gen: T.Callable[[T.List[str], CMakeTarget], T.List[str]] = lambda strlist, t: parse_generator_expressions(';'.join(strlist), self, context_tgt=t).split(';') if strlist else []
+ tgt.name = parse_generator_expressions(tgt.name, self, context_tgt=tgt)
+ tgt.type = parse_generator_expressions(tgt.type, self, context_tgt=tgt)
+ tgt.properties = {
+ k: tgtlist_gen(v, tgt) for k, v in tgt.properties.items()
+ } if tgt.properties is not None else None
+ tgt.depends = tgtlist_gen(tgt.depends, tgt)
+
+ for ctgt in self.custom_targets:
+ ctgt.outputs = pathlist_gen(ctgt._outputs_str)
+ temp = ctgt.command
+ ctgt.command = [strlist_gen(x) for x in ctgt.command]
+ for command, src in zip(ctgt.command, temp):
+ if command[0] == "":
+ raise CMakeException(
+ "We evaluated the cmake variable '{}' to an empty string, which is not a valid path to an executable.".format(src[0])
+ )
+ ctgt.working_dir = Path(parse_generator_expressions(str(ctgt.working_dir), self)) if ctgt.working_dir is not None else None
+
+ # Postprocess
+ for tgt in self.targets.values():
+ tgt.strip_properties()
+
+ def get_first_cmake_var_of(self, var_list: T.List[str]) -> T.List[str]:
+ # Return the first found CMake variable in list var_list
+ for i in var_list:
+ if i in self.vars:
+ return self.vars[i]
+
+ return []
+
+ def get_cmake_var(self, var: str) -> T.List[str]:
+ # Return the value of the CMake variable var or an empty list if var does not exist
+ if var in self.vars:
+ return self.vars[var]
+
+ return []
+
+ def var_to_str(self, var: str) -> T.Optional[str]:
+ if var in self.vars and self.vars[var]:
+ return self.vars[var][0]
+
+ return None
+
+ def _str_to_bool(self, expr: T.Union[str, T.List[str]]) -> bool:
+ if not expr:
+ return False
+ if isinstance(expr, list):
+ expr_str = expr[0]
+ else:
+ expr_str = expr
+ expr_str = expr_str.upper()
+ return expr_str not in ['0', 'OFF', 'NO', 'FALSE', 'N', 'IGNORE'] and not expr_str.endswith('NOTFOUND')
+
+ def var_to_bool(self, var: str) -> bool:
+ return self._str_to_bool(self.vars.get(var, []))
+
+ def _gen_exception(self, function: str, error: str, tline: CMakeTraceLine) -> None:
+ # Generate an exception if the parser is not in permissive mode
+
+ if self.permissive:
+ mlog.debug(f'CMake trace warning: {function}() {error}\n{tline}')
+ return None
+ raise CMakeException(f'CMake: {function}() {error}\n{tline}')
+
+ def _cmake_set(self, tline: CMakeTraceLine) -> None:
+ """Handler for the CMake set() function in all variaties.
+
+ comes in three flavors:
+ set(<var> <value> [PARENT_SCOPE])
+ set(<var> <value> CACHE <type> <docstring> [FORCE])
+ set(ENV{<var>} <value>)
+
+ We don't support the ENV variant, and any uses of it will be ignored
+ silently. the other two variates are supported, with some caveats:
+ - we don't properly handle scoping, so calls to set() inside a
+ function without PARENT_SCOPE set could incorrectly shadow the
+ outer scope.
+ - We don't honor the type of CACHE arguments
+ """
+ # DOC: https://cmake.org/cmake/help/latest/command/set.html
+
+ cache_type = None
+ cache_force = 'FORCE' in tline.args
+ try:
+ cache_idx = tline.args.index('CACHE')
+ cache_type = tline.args[cache_idx + 1]
+ except (ValueError, IndexError):
+ pass
+
+ # 1st remove PARENT_SCOPE and CACHE from args
+ args = []
+ for i in tline.args:
+ if not i or i == 'PARENT_SCOPE':
+ continue
+
+ # Discard everything after the CACHE keyword
+ if i == 'CACHE':
+ break
+
+ args.append(i)
+
+ if len(args) < 1:
+ return self._gen_exception('set', 'requires at least one argument', tline)
+
+ # Now that we've removed extra arguments all that should be left is the
+ # variable identifier and the value, join the value back together to
+ # ensure spaces in the value are correctly handled. This assumes that
+ # variable names don't have spaces. Please don't do that...
+ identifier = args.pop(0)
+ value = ' '.join(args)
+
+ # Write to the CMake cache instead
+ if cache_type:
+ # Honor how the CMake FORCE parameter works
+ if identifier not in self.cache or cache_force:
+ self.cache[identifier] = CMakeCacheEntry(value.split(';'), cache_type)
+
+ if not value:
+ # Same as unset
+ if identifier in self.vars:
+ del self.vars[identifier]
+ else:
+ self.vars[identifier] = value.split(';')
+ self.vars_by_file.setdefault(tline.file, {})[identifier] = value.split(';')
+
+ def _cmake_unset(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/unset.html
+ if len(tline.args) < 1:
+ return self._gen_exception('unset', 'requires at least one argument', tline)
+
+ if tline.args[0] in self.vars:
+ del self.vars[tline.args[0]]
+
+ def _cmake_add_executable(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/add_executable.html
+ args = list(tline.args) # Make a working copy
+
+ # Make sure the exe is imported
+ is_imported = True
+ if 'IMPORTED' not in args:
+ return self._gen_exception('add_executable', 'non imported executables are not supported', tline)
+
+ args.remove('IMPORTED')
+
+ if len(args) < 1:
+ return self._gen_exception('add_executable', 'requires at least 1 argument', tline)
+
+ self.targets[args[0]] = CMakeTarget(args[0], 'EXECUTABLE', {}, tline=tline, imported=is_imported)
+
+ def _cmake_add_library(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/add_library.html
+ args = list(tline.args) # Make a working copy
+
+ # Make sure the lib is imported
+ if 'INTERFACE' in args:
+ args.remove('INTERFACE')
+
+ if len(args) < 1:
+ return self._gen_exception('add_library', 'interface library name not specified', tline)
+
+ self.targets[args[0]] = CMakeTarget(args[0], 'INTERFACE', {}, tline=tline, imported='IMPORTED' in args)
+ elif 'IMPORTED' in args:
+ args.remove('IMPORTED')
+
+ # Now, only look at the first two arguments (target_name and target_type) and ignore the rest
+ if len(args) < 2:
+ return self._gen_exception('add_library', 'requires at least 2 arguments', tline)
+
+ self.targets[args[0]] = CMakeTarget(args[0], args[1], {}, tline=tline, imported=True)
+ elif 'ALIAS' in args:
+ args.remove('ALIAS')
+
+ # Now, only look at the first two arguments (target_name and target_ref) and ignore the rest
+ if len(args) < 2:
+ return self._gen_exception('add_library', 'requires at least 2 arguments', tline)
+
+ # Simulate the ALIAS with INTERFACE_LINK_LIBRARIES
+ self.targets[args[0]] = CMakeTarget(args[0], 'ALIAS', {'INTERFACE_LINK_LIBRARIES': [args[1]]}, tline=tline)
+ elif 'OBJECT' in args:
+ return self._gen_exception('add_library', 'OBJECT libraries are not supported', tline)
+ else:
+ self.targets[args[0]] = CMakeTarget(args[0], 'NORMAL', {}, tline=tline)
+
+ def _cmake_add_custom_command(self, tline: CMakeTraceLine, name: T.Optional[str] = None) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/add_custom_command.html
+ args = self._flatten_args(list(tline.args)) # Commands can be passed as ';' separated lists
+
+ if not args:
+ return self._gen_exception('add_custom_command', 'requires at least 1 argument', tline)
+
+ # Skip the second function signature
+ if args[0] == 'TARGET':
+ return self._gen_exception('add_custom_command', 'TARGET syntax is currently not supported', tline)
+
+ magic_keys = ['OUTPUT', 'COMMAND', 'MAIN_DEPENDENCY', 'DEPENDS', 'BYPRODUCTS',
+ 'IMPLICIT_DEPENDS', 'WORKING_DIRECTORY', 'COMMENT', 'DEPFILE',
+ 'JOB_POOL', 'VERBATIM', 'APPEND', 'USES_TERMINAL', 'COMMAND_EXPAND_LISTS']
+
+ target = CMakeGeneratorTarget(name)
+
+ def handle_output(key: str, target: CMakeGeneratorTarget) -> None:
+ target._outputs_str += [key]
+
+ def handle_command(key: str, target: CMakeGeneratorTarget) -> None:
+ if key == 'ARGS':
+ return
+ target.command[-1] += [key]
+
+ def handle_depends(key: str, target: CMakeGeneratorTarget) -> None:
+ target.depends += [key]
+
+ working_dir = None
+
+ def handle_working_dir(key: str, target: CMakeGeneratorTarget) -> None:
+ nonlocal working_dir
+ if working_dir is None:
+ working_dir = key
+ else:
+ working_dir += ' '
+ working_dir += key
+
+ fn = None
+
+ for i in args:
+ if i in magic_keys:
+ if i == 'OUTPUT':
+ fn = handle_output
+ elif i == 'DEPENDS':
+ fn = handle_depends
+ elif i == 'WORKING_DIRECTORY':
+ fn = handle_working_dir
+ elif i == 'COMMAND':
+ fn = handle_command
+ target.command += [[]]
+ else:
+ fn = None
+ continue
+
+ if fn is not None:
+ fn(i, target)
+
+ cbinary_dir = self.var_to_str('MESON_PS_CMAKE_CURRENT_BINARY_DIR')
+ csource_dir = self.var_to_str('MESON_PS_CMAKE_CURRENT_SOURCE_DIR')
+
+ target.working_dir = Path(working_dir) if working_dir else None
+ target.current_bin_dir = Path(cbinary_dir) if cbinary_dir else None
+ target.current_src_dir = Path(csource_dir) if csource_dir else None
+ target._outputs_str = self._guess_files(target._outputs_str)
+ target.depends = self._guess_files(target.depends)
+ target.command = [self._guess_files(x) for x in target.command]
+
+ self.custom_targets += [target]
+ if name:
+ self.targets[name] = target
+
+ def _cmake_add_custom_target(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/add_custom_target.html
+ # We only the first parameter (the target name) is interesting
+ if len(tline.args) < 1:
+ return self._gen_exception('add_custom_target', 'requires at least one argument', tline)
+
+ # It's pretty much the same as a custom command
+ self._cmake_add_custom_command(tline, tline.args[0])
+
+ def _cmake_set_property(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/set_property.html
+ args = list(tline.args)
+
+ scope = args.pop(0)
+
+ append = False
+ targets = []
+ while args:
+ curr = args.pop(0)
+ # XXX: APPEND_STRING is specifically *not* supposed to create a
+ # list, is treating them as aliases really okay?
+ if curr in {'APPEND', 'APPEND_STRING'}:
+ append = True
+ continue
+
+ if curr == 'PROPERTY':
+ break
+
+ targets += curr.split(';')
+
+ if not args:
+ return self._gen_exception('set_property', 'faild to parse argument list', tline)
+
+ if len(args) == 1:
+ # Tries to set property to nothing so nothing has to be done
+ return
+
+ identifier = args.pop(0)
+ if self.trace_format == 'human':
+ value = ' '.join(args).split(';')
+ else:
+ value = [y for x in args for y in x.split(';')]
+ if not value:
+ return
+
+ def do_target(t: str) -> None:
+ if t not in self.targets:
+ return self._gen_exception('set_property', f'TARGET {t} not found', tline)
+
+ tgt = self.targets[t]
+ if identifier not in tgt.properties:
+ tgt.properties[identifier] = []
+
+ if append:
+ tgt.properties[identifier] += value
+ else:
+ tgt.properties[identifier] = value
+
+ def do_source(src: str) -> None:
+ if identifier != 'HEADER_FILE_ONLY' or not self._str_to_bool(value):
+ return
+
+ current_src_dir = self.var_to_str('MESON_PS_CMAKE_CURRENT_SOURCE_DIR')
+ if not current_src_dir:
+ mlog.warning(textwrap.dedent('''\
+ CMake trace: set_property(SOURCE) called before the preload script was loaded.
+ Unable to determine CMAKE_CURRENT_SOURCE_DIR. This can lead to build errors.
+ '''))
+ current_src_dir = '.'
+
+ cur_p = Path(current_src_dir)
+ src_p = Path(src)
+
+ if not src_p.is_absolute():
+ src_p = cur_p / src_p
+ self.explicit_headers.add(src_p)
+
+ if scope == 'TARGET':
+ for i in targets:
+ do_target(i)
+ elif scope == 'SOURCE':
+ files = self._guess_files(targets)
+ for i in files:
+ do_source(i)
+
+ def _cmake_set_target_properties(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/set_target_properties.html
+ args = list(tline.args)
+
+ targets = []
+ while args:
+ curr = args.pop(0)
+ if curr == 'PROPERTIES':
+ break
+
+ targets.append(curr)
+
+ # Now we need to try to reconsitute the original quoted format of the
+ # arguments, as a property value could have spaces in it. Unlike
+ # set_property() this is not context free. There are two approaches I
+ # can think of, both have drawbacks:
+ #
+ # 1. Assume that the property will be capitalized ([A-Z_]), this is
+ # convention but cmake doesn't require it.
+ # 2. Maintain a copy of the list here: https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html#target-properties
+ #
+ # Neither of these is awesome for obvious reasons. I'm going to try
+ # option 1 first and fall back to 2, as 1 requires less code and less
+ # synchroniztion for cmake changes.
+ #
+ # With the JSON output format, introduced in CMake 3.17, spaces are
+ # handled properly and we don't have to do either options
+
+ arglist = [] # type: T.List[T.Tuple[str, T.List[str]]]
+ if self.trace_format == 'human':
+ name = args.pop(0)
+ values = [] # type: T.List[str]
+ prop_regex = re.compile(r'^[A-Z_]+$')
+ for a in args:
+ if prop_regex.match(a):
+ if values:
+ arglist.append((name, ' '.join(values).split(';')))
+ name = a
+ values = []
+ else:
+ values.append(a)
+ if values:
+ arglist.append((name, ' '.join(values).split(';')))
+ else:
+ arglist = [(x[0], x[1].split(';')) for x in zip(args[::2], args[1::2])]
+
+ for name, value in arglist:
+ for i in targets:
+ if i not in self.targets:
+ return self._gen_exception('set_target_properties', f'TARGET {i} not found', tline)
+
+ self.targets[i].properties[name] = value
+
+ def _cmake_add_dependencies(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/add_dependencies.html
+ args = list(tline.args)
+
+ if len(args) < 2:
+ return self._gen_exception('add_dependencies', 'takes at least 2 arguments', tline)
+
+ target = self.targets.get(args[0])
+ if not target:
+ return self._gen_exception('add_dependencies', 'target not found', tline)
+
+ for i in args[1:]:
+ target.depends += i.split(';')
+
+ def _cmake_target_compile_definitions(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/target_compile_definitions.html
+ self._parse_common_target_options('target_compile_definitions', 'COMPILE_DEFINITIONS', 'INTERFACE_COMPILE_DEFINITIONS', tline)
+
+ def _cmake_target_compile_options(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/target_compile_options.html
+ self._parse_common_target_options('target_compile_options', 'COMPILE_OPTIONS', 'INTERFACE_COMPILE_OPTIONS', tline)
+
+ def _cmake_target_include_directories(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/target_include_directories.html
+ self._parse_common_target_options('target_include_directories', 'INCLUDE_DIRECTORIES', 'INTERFACE_INCLUDE_DIRECTORIES', tline, ignore=['SYSTEM', 'BEFORE'], paths=True)
+
+ def _cmake_target_link_options(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/target_link_options.html
+ self._parse_common_target_options('target_link_options', 'LINK_OPTIONS', 'INTERFACE_LINK_OPTIONS', tline)
+
+ def _cmake_target_link_libraries(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/target_link_libraries.html
+ self._parse_common_target_options('target_link_options', 'LINK_LIBRARIES', 'INTERFACE_LINK_LIBRARIES', tline)
+
+ def _cmake_message(self, tline: CMakeTraceLine) -> None:
+ # DOC: https://cmake.org/cmake/help/latest/command/message.html
+ args = list(tline.args)
+
+ if len(args) < 1:
+ return self._gen_exception('message', 'takes at least 1 argument', tline)
+
+ if args[0].upper().strip() not in ['FATAL_ERROR', 'SEND_ERROR']:
+ return
+
+ self.errors += [' '.join(args[1:])]
+
+ def _parse_common_target_options(self, func: str, private_prop: str, interface_prop: str, tline: CMakeTraceLine, ignore: T.Optional[T.List[str]] = None, paths: bool = False) -> None:
+ if ignore is None:
+ ignore = ['BEFORE']
+
+ args = list(tline.args)
+
+ if len(args) < 1:
+ return self._gen_exception(func, 'requires at least one argument', tline)
+
+ target = args[0]
+ if target not in self.targets:
+ return self._gen_exception(func, f'TARGET {target} not found', tline)
+
+ interface = []
+ private = []
+
+ mode = 'PUBLIC'
+ for i in args[1:]:
+ if i in ignore:
+ continue
+
+ if i in {'INTERFACE', 'LINK_INTERFACE_LIBRARIES', 'PUBLIC', 'PRIVATE', 'LINK_PUBLIC', 'LINK_PRIVATE'}:
+ mode = i
+ continue
+
+ if mode in {'INTERFACE', 'LINK_INTERFACE_LIBRARIES', 'PUBLIC', 'LINK_PUBLIC'}:
+ interface += i.split(';')
+
+ if mode in {'PUBLIC', 'PRIVATE', 'LINK_PRIVATE'}:
+ private += i.split(';')
+
+ if paths:
+ interface = self._guess_files(interface)
+ private = self._guess_files(private)
+
+ interface = [x for x in interface if x]
+ private = [x for x in private if x]
+
+ for j in [(private_prop, private), (interface_prop, interface)]:
+ if not j[0] in self.targets[target].properties:
+ self.targets[target].properties[j[0]] = []
+
+ self.targets[target].properties[j[0]] += j[1]
+
+ def _meson_ps_execute_delayed_calls(self, tline: CMakeTraceLine) -> None:
+ for l in self.stored_commands:
+ fn = self.functions.get(l.func, None)
+ if fn:
+ fn(l)
+
+ # clear the stored commands
+ self.stored_commands = []
+
+ def _meson_ps_reload_vars(self, tline: CMakeTraceLine) -> None:
+ self.delayed_commands = self.get_cmake_var('MESON_PS_DELAYED_CALLS')
+
+ def _meson_ps_disabled_function(self, tline: CMakeTraceLine) -> None:
+ args = list(tline.args)
+ if not args:
+ mlog.error('Invalid preload.cmake script! At least one argument to `meson_ps_disabled_function` is expected')
+ return
+ mlog.warning(f'The CMake function "{args[0]}" was disabled to avoid compatibility issues with Meson.')
+
+ def _lex_trace_human(self, trace: str) -> T.Generator[CMakeTraceLine, None, None]:
+ # The trace format is: '<file>(<line>): <func>(<args -- can contain \n> )\n'
+ reg_tline = re.compile(r'\s*(.*\.(cmake|txt))\(([0-9]+)\):\s*(\w+)\(([\s\S]*?) ?\)\s*\n', re.MULTILINE)
+ reg_other = re.compile(r'[^\n]*\n')
+ loc = 0
+ while loc < len(trace):
+ mo_file_line = reg_tline.match(trace, loc)
+ if not mo_file_line:
+ skip_match = reg_other.match(trace, loc)
+ if not skip_match:
+ print(trace[loc:])
+ raise CMakeException('Failed to parse CMake trace')
+
+ loc = skip_match.end()
+ continue
+
+ loc = mo_file_line.end()
+
+ file = mo_file_line.group(1)
+ line = mo_file_line.group(3)
+ func = mo_file_line.group(4)
+ args = mo_file_line.group(5)
+ argl = args.split(' ')
+ argl = [a.strip() for a in argl]
+
+ yield CMakeTraceLine(file, int(line), func, argl)
+
+ def _lex_trace_json(self, trace: str) -> T.Generator[CMakeTraceLine, None, None]:
+ lines = trace.splitlines(keepends=False)
+ lines.pop(0) # The first line is the version
+ for i in lines:
+ data = json.loads(i)
+ assert isinstance(data['file'], str)
+ assert isinstance(data['line'], int)
+ assert isinstance(data['cmd'], str)
+ assert isinstance(data['args'], list)
+ args = data['args']
+ for j in args:
+ assert isinstance(j, str)
+ yield CMakeTraceLine(data['file'], data['line'], data['cmd'], args)
+
+ def _flatten_args(self, args: T.List[str]) -> T.List[str]:
+ # Split lists in arguments
+ res = [] # type: T.List[str]
+ for i in args:
+ res += i.split(';')
+ return res
+
+ def _guess_files(self, broken_list: T.List[str]) -> T.List[str]:
+ # Nothing has to be done for newer formats
+ if self.trace_format != 'human':
+ return broken_list
+
+ # Try joining file paths that contain spaces
+
+ reg_start = re.compile(r'^([A-Za-z]:)?/(.*/)*[^./]+$')
+ reg_end = re.compile(r'^.*\.[a-zA-Z]+$')
+
+ fixed_list = [] # type: T.List[str]
+ curr_str = None # type: T.Optional[str]
+ path_found = False # type: bool
+
+ for i in broken_list:
+ if curr_str is None:
+ curr_str = i
+ path_found = False
+ elif Path(curr_str).is_file():
+ # Abort concatenation if curr_str is an existing file
+ fixed_list += [curr_str]
+ curr_str = i
+ path_found = False
+ elif not reg_start.match(curr_str):
+ # Abort concatenation if curr_str no longer matches the regex
+ fixed_list += [curr_str]
+ curr_str = i
+ path_found = False
+ elif reg_end.match(i):
+ # File detected
+ curr_str = f'{curr_str} {i}'
+ fixed_list += [curr_str]
+ curr_str = None
+ path_found = False
+ elif Path(f'{curr_str} {i}').exists():
+ # Path detected
+ curr_str = f'{curr_str} {i}'
+ path_found = True
+ elif path_found:
+ # Add path to fixed_list after ensuring the whole path is in curr_str
+ fixed_list += [curr_str]
+ curr_str = i
+ path_found = False
+ else:
+ curr_str = f'{curr_str} {i}'
+ path_found = False
+
+ if curr_str:
+ fixed_list += [curr_str]
+ return fixed_list
diff --git a/mesonbuild/cmake/tracetargets.py b/mesonbuild/cmake/tracetargets.py
new file mode 100644
index 0000000..338364d
--- /dev/null
+++ b/mesonbuild/cmake/tracetargets.py
@@ -0,0 +1,119 @@
+# SPDX-License-Identifer: Apache-2.0
+# Copyright 2021 The Meson development team
+from __future__ import annotations
+
+from .common import cmake_is_debug
+from .. import mlog
+
+from pathlib import Path
+import re
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .traceparser import CMakeTraceParser
+ from ..environment import Environment
+ from ..compilers import Compiler
+ from ..dependencies import MissingCompiler
+
+class ResolvedTarget:
+ def __init__(self) -> None:
+ self.include_directories: T.List[str] = []
+ self.link_flags: T.List[str] = []
+ self.public_compile_opts: T.List[str] = []
+ self.libraries: T.List[str] = []
+
+def resolve_cmake_trace_targets(target_name: str,
+ trace: 'CMakeTraceParser',
+ env: 'Environment',
+ *,
+ clib_compiler: T.Union['MissingCompiler', 'Compiler'] = None,
+ not_found_warning: T.Callable[[str], None] = lambda x: None) -> ResolvedTarget:
+ res = ResolvedTarget()
+ targets = [target_name]
+
+ # recognise arguments we should pass directly to the linker
+ reg_is_lib = re.compile(r'^(-l[a-zA-Z0-9_]+|-l?pthread)$')
+ reg_is_maybe_bare_lib = re.compile(r'^[a-zA-Z0-9_]+$')
+
+ is_debug = cmake_is_debug(env)
+
+ processed_targets: T.List[str] = []
+ while len(targets) > 0:
+ curr = targets.pop(0)
+
+ # Skip already processed targets
+ if curr in processed_targets:
+ continue
+
+ if curr not in trace.targets:
+ if reg_is_lib.match(curr):
+ res.libraries += [curr]
+ elif Path(curr).is_absolute() and Path(curr).exists():
+ res.libraries += [curr]
+ elif env.machines.build.is_windows() and reg_is_maybe_bare_lib.match(curr) and clib_compiler:
+ # On Windows, CMake library dependencies can be passed as bare library names,
+ # CMake brute-forces a combination of prefix/suffix combinations to find the
+ # right library. Assume any bare argument passed which is not also a CMake
+ # target must be a system library we should try to link against.
+ res.libraries += clib_compiler.find_library(curr, env, [])
+ else:
+ not_found_warning(curr)
+ continue
+
+ tgt = trace.targets[curr]
+ cfgs = []
+ cfg = ''
+ mlog.debug(tgt)
+
+ if 'INTERFACE_INCLUDE_DIRECTORIES' in tgt.properties:
+ res.include_directories += [x for x in tgt.properties['INTERFACE_INCLUDE_DIRECTORIES'] if x]
+
+ if 'INTERFACE_LINK_OPTIONS' in tgt.properties:
+ res.link_flags += [x for x in tgt.properties['INTERFACE_LINK_OPTIONS'] if x]
+
+ if 'INTERFACE_COMPILE_DEFINITIONS' in tgt.properties:
+ res.public_compile_opts += ['-D' + re.sub('^-D', '', x) for x in tgt.properties['INTERFACE_COMPILE_DEFINITIONS'] if x]
+
+ if 'INTERFACE_COMPILE_OPTIONS' in tgt.properties:
+ res.public_compile_opts += [x for x in tgt.properties['INTERFACE_COMPILE_OPTIONS'] if x]
+
+ if 'IMPORTED_CONFIGURATIONS' in tgt.properties:
+ cfgs = [x for x in tgt.properties['IMPORTED_CONFIGURATIONS'] if x]
+ cfg = cfgs[0]
+
+ if is_debug:
+ if 'DEBUG' in cfgs:
+ cfg = 'DEBUG'
+ elif 'RELEASE' in cfgs:
+ cfg = 'RELEASE'
+ else:
+ if 'RELEASE' in cfgs:
+ cfg = 'RELEASE'
+
+ if f'IMPORTED_IMPLIB_{cfg}' in tgt.properties:
+ res.libraries += [x for x in tgt.properties[f'IMPORTED_IMPLIB_{cfg}'] if x]
+ elif 'IMPORTED_IMPLIB' in tgt.properties:
+ res.libraries += [x for x in tgt.properties['IMPORTED_IMPLIB'] if x]
+ elif f'IMPORTED_LOCATION_{cfg}' in tgt.properties:
+ res.libraries += [x for x in tgt.properties[f'IMPORTED_LOCATION_{cfg}'] if x]
+ elif 'IMPORTED_LOCATION' in tgt.properties:
+ res.libraries += [x for x in tgt.properties['IMPORTED_LOCATION'] if x]
+
+ if 'LINK_LIBRARIES' in tgt.properties:
+ targets += [x for x in tgt.properties['LINK_LIBRARIES'] if x]
+ if 'INTERFACE_LINK_LIBRARIES' in tgt.properties:
+ targets += [x for x in tgt.properties['INTERFACE_LINK_LIBRARIES'] if x]
+
+ if f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}' in tgt.properties:
+ targets += [x for x in tgt.properties[f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}'] if x]
+ elif 'IMPORTED_LINK_DEPENDENT_LIBRARIES' in tgt.properties:
+ targets += [x for x in tgt.properties['IMPORTED_LINK_DEPENDENT_LIBRARIES'] if x]
+
+ processed_targets += [curr]
+
+ res.include_directories = sorted(set(res.include_directories))
+ res.link_flags = sorted(set(res.link_flags))
+ res.public_compile_opts = sorted(set(res.public_compile_opts))
+ res.libraries = sorted(set(res.libraries))
+
+ return res
diff --git a/mesonbuild/compilers/__init__.py b/mesonbuild/compilers/__init__.py
new file mode 100644
index 0000000..c516aab
--- /dev/null
+++ b/mesonbuild/compilers/__init__.py
@@ -0,0 +1,97 @@
+# Copyright 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.
+
+# Public symbols for compilers sub-package when using 'from . import compilers'
+__all__ = [
+ 'Compiler',
+ 'RunResult',
+
+ 'all_languages',
+ 'base_options',
+ 'clib_langs',
+ 'clink_langs',
+ 'c_suffixes',
+ 'cpp_suffixes',
+ 'get_base_compile_args',
+ 'get_base_link_args',
+ 'is_assembly',
+ 'is_header',
+ 'is_library',
+ 'is_llvm_ir',
+ 'is_object',
+ 'is_source',
+ 'is_known_suffix',
+ 'lang_suffixes',
+ 'LANGUAGES_USING_LDFLAGS',
+ 'sort_clink',
+ 'SUFFIX_TO_LANG',
+
+ 'compiler_from_language',
+ 'detect_compiler_for',
+ 'detect_static_linker',
+ 'detect_c_compiler',
+ 'detect_cpp_compiler',
+ 'detect_cuda_compiler',
+ 'detect_fortran_compiler',
+ 'detect_objc_compiler',
+ 'detect_objcpp_compiler',
+ 'detect_java_compiler',
+ 'detect_cs_compiler',
+ 'detect_vala_compiler',
+ 'detect_rust_compiler',
+ 'detect_d_compiler',
+ 'detect_swift_compiler',
+]
+
+# Bring symbols from each module into compilers sub-package namespace
+from .compilers import (
+ Compiler,
+ RunResult,
+ all_languages,
+ base_options,
+ clib_langs,
+ clink_langs,
+ c_suffixes,
+ cpp_suffixes,
+ get_base_compile_args,
+ get_base_link_args,
+ is_header,
+ is_source,
+ is_assembly,
+ is_llvm_ir,
+ is_object,
+ is_library,
+ is_known_suffix,
+ lang_suffixes,
+ LANGUAGES_USING_LDFLAGS,
+ sort_clink,
+ SUFFIX_TO_LANG,
+)
+from .detect import (
+ compiler_from_language,
+ detect_compiler_for,
+ detect_static_linker,
+ detect_c_compiler,
+ detect_cpp_compiler,
+ detect_cuda_compiler,
+ detect_objc_compiler,
+ detect_objcpp_compiler,
+ detect_fortran_compiler,
+ detect_java_compiler,
+ detect_cs_compiler,
+ detect_vala_compiler,
+ detect_rust_compiler,
+ detect_d_compiler,
+ detect_swift_compiler,
+)
diff --git a/mesonbuild/compilers/asm.py b/mesonbuild/compilers/asm.py
new file mode 100644
index 0000000..9a95599
--- /dev/null
+++ b/mesonbuild/compilers/asm.py
@@ -0,0 +1,231 @@
+import os
+import typing as T
+
+from ..mesonlib import EnvironmentException, get_meson_command
+from .compilers import Compiler
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+
+nasm_optimization_args = {
+ 'plain': [],
+ '0': ['-O0'],
+ 'g': ['-O0'],
+ '1': ['-O1'],
+ '2': ['-Ox'],
+ '3': ['-Ox'],
+ 's': ['-Ox'],
+} # type: T.Dict[str, T.List[str]]
+
+
+class NasmCompiler(Compiler):
+ language = 'nasm'
+ id = 'nasm'
+
+ def needs_static_linker(self) -> bool:
+ return True
+
+ def get_always_args(self) -> T.List[str]:
+ cpu = '64' if self.info.is_64_bit else '32'
+ if self.info.is_windows() or self.info.is_cygwin():
+ plat = 'win'
+ define = f'WIN{cpu}'
+ elif self.info.is_darwin():
+ plat = 'macho'
+ define = 'MACHO'
+ else:
+ plat = 'elf'
+ define = 'ELF'
+ args = ['-f', f'{plat}{cpu}', f'-D{define}']
+ if self.info.is_64_bit:
+ args.append('-D__x86_64__')
+ return args
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['-Werror']
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return ['-o', outputname]
+
+ def unix_args_to_native(self, args: T.List[str]) -> T.List[str]:
+ outargs = []
+ for arg in args:
+ if arg == '-pthread':
+ continue
+ outargs.append(arg)
+ return outargs
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return nasm_optimization_args[optimization_level]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ if is_debug:
+ if self.info.is_windows():
+ return []
+ return ['-g', '-F', 'dwarf']
+ return []
+
+ def get_depfile_suffix(self) -> str:
+ return 'd'
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return ['-MD', outfile, '-MQ', outtarget]
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ if self.info.cpu_family not in {'x86', 'x86_64'}:
+ raise EnvironmentException(f'ASM compiler {self.id!r} does not support {self.info.cpu_family} CPU family')
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ # FIXME: Not implemented
+ return []
+
+ def get_pic_args(self) -> T.List[str]:
+ return []
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if not path:
+ path = '.'
+ return ['-I' + path]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+ return parameter_list
+
+ def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ return []
+
+class YasmCompiler(NasmCompiler):
+ id = 'yasm'
+
+ def get_exelist(self, ccache: bool = True) -> T.List[str]:
+ # Wrap yasm executable with an internal script that will write depfile.
+ exelist = super().get_exelist(ccache)
+ return get_meson_command() + ['--internal', 'yasm'] + exelist
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ if is_debug:
+ if self.info.is_windows():
+ return ['-g', 'null']
+ return ['-g', 'dwarf2']
+ return []
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return ['--depfile', outfile]
+
+# https://learn.microsoft.com/en-us/cpp/assembler/masm/ml-and-ml64-command-line-reference
+class MasmCompiler(Compiler):
+ language = 'masm'
+ id = 'ml'
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return ['/c']
+
+ def get_argument_syntax(self) -> str:
+ return 'msvc'
+
+ def needs_static_linker(self) -> bool:
+ return True
+
+ def get_always_args(self) -> T.List[str]:
+ return ['/nologo']
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['/WX']
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return ['/Fo', outputname]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return []
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ if is_debug:
+ return ['/Zi']
+ return []
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ if self.info.cpu_family not in {'x86', 'x86_64'}:
+ raise EnvironmentException(f'ASM compiler {self.id!r} does not support {self.info.cpu_family} CPU family')
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ # FIXME: Not implemented
+ return []
+
+ def get_pic_args(self) -> T.List[str]:
+ return []
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if not path:
+ path = '.'
+ return ['-I' + path]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I' or i[:2] == '/I':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+ return parameter_list
+
+ def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ return []
+
+ def depfile_for_object(self, objfile: str) -> T.Optional[str]:
+ return None
+
+
+# https://learn.microsoft.com/en-us/cpp/assembler/arm/arm-assembler-command-line-reference
+class MasmARMCompiler(Compiler):
+ language = 'masm'
+ id = 'armasm'
+
+ def needs_static_linker(self) -> bool:
+ return True
+
+ def get_always_args(self) -> T.List[str]:
+ return ['-nologo']
+
+ def get_werror_args(self) -> T.List[str]:
+ return []
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return ['-o', outputname]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return []
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ if is_debug:
+ return ['-g']
+ return []
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ if self.info.cpu_family not in {'arm', 'aarch64'}:
+ raise EnvironmentException(f'ASM compiler {self.id!r} does not support {self.info.cpu_family} CPU family')
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ # FIXME: Not implemented
+ return []
+
+ def get_pic_args(self) -> T.List[str]:
+ return []
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if not path:
+ path = '.'
+ return ['-i' + path]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+ return parameter_list
+
+ def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ return []
+
+ def depfile_for_object(self, objfile: str) -> T.Optional[str]:
+ return None
diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py
new file mode 100644
index 0000000..4d9283a
--- /dev/null
+++ b/mesonbuild/compilers/c.py
@@ -0,0 +1,740 @@
+# Copyright 2012-2020 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.
+from __future__ import annotations
+
+import os.path
+import typing as T
+
+from .. import coredata
+from .. import mlog
+from ..mesonlib import MesonException, version_compare, OptionKey
+from .c_function_attributes import C_FUNC_ATTRIBUTES
+from .mixins.clike import CLikeCompiler
+from .mixins.ccrx import CcrxCompiler
+from .mixins.xc16 import Xc16Compiler
+from .mixins.compcert import CompCertCompiler
+from .mixins.ti import TICompiler
+from .mixins.arm import ArmCompiler, ArmclangCompiler
+from .mixins.visualstudio import MSVCCompiler, ClangClCompiler
+from .mixins.gnu import GnuCompiler
+from .mixins.gnu import gnu_common_warning_args, gnu_c_warning_args
+from .mixins.intel import IntelGnuLikeCompiler, IntelVisualStudioLikeCompiler
+from .mixins.clang import ClangCompiler
+from .mixins.elbrus import ElbrusCompiler
+from .mixins.pgi import PGICompiler
+from .mixins.emscripten import EmscriptenMixin
+from .compilers import (
+ gnu_winlibs,
+ msvc_winlibs,
+ Compiler,
+)
+
+if T.TYPE_CHECKING:
+ from ..coredata import MutableKeyedOptionDictType, KeyedOptionDictType
+ from ..dependencies import Dependency
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..linkers import DynamicLinker
+ from ..mesonlib import MachineChoice
+ from ..programs import ExternalProgram
+ from .compilers import CompileCheckMode
+
+ CompilerMixinBase = Compiler
+else:
+ CompilerMixinBase = object
+
+
+class CCompiler(CLikeCompiler, Compiler):
+ def attribute_check_func(self, name: str) -> str:
+ try:
+ return C_FUNC_ATTRIBUTES[name]
+ except KeyError:
+ raise MesonException(f'Unknown function attribute "{name}"')
+
+ language = 'c'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ # If a child ObjC or CPP class has already set it, don't set it ourselves
+ Compiler.__init__(self, ccache, exelist, version, for_machine, info,
+ is_cross=is_cross, full_version=full_version, linker=linker)
+ CLikeCompiler.__init__(self, exe_wrapper)
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ return ['-nostdinc']
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ code = 'int main(void) { int class=0; return class; }\n'
+ return self._sanity_check_impl(work_dir, environment, 'sanitycheckc.c', code)
+
+ def has_header_symbol(self, hname: str, symbol: str, prefix: str,
+ env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[['CompileCheckMode'], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ fargs = {'prefix': prefix, 'header': hname, 'symbol': symbol}
+ t = '''{prefix}
+ #include <{header}>
+ int main(void) {{
+ /* If it's not defined as a macro, try to use as a symbol */
+ #ifndef {symbol}
+ {symbol};
+ #endif
+ return 0;
+ }}'''
+ return self.compiles(t.format(**fargs), env, extra_args=extra_args,
+ dependencies=dependencies)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = super().get_options()
+ opts.update({
+ OptionKey('std', machine=self.for_machine, lang=self.language): coredata.UserComboOption(
+ 'C language standard to use',
+ ['none'],
+ 'none',
+ )
+ })
+ return opts
+
+
+class _ClangCStds(CompilerMixinBase):
+
+ """Mixin class for clang based compilers for setting C standards.
+
+ This is used by both ClangCCompiler and ClangClCompiler, as they share
+ the same versions
+ """
+
+ _C17_VERSION = '>=6.0.0'
+ _C18_VERSION = '>=8.0.0'
+ _C2X_VERSION = '>=9.0.0'
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = super().get_options()
+ c_stds = ['c89', 'c99', 'c11']
+ g_stds = ['gnu89', 'gnu99', 'gnu11']
+ # https://releases.llvm.org/6.0.0/tools/clang/docs/ReleaseNotes.html
+ # https://en.wikipedia.org/wiki/Xcode#Latest_versions
+ if version_compare(self.version, self._C17_VERSION):
+ c_stds += ['c17']
+ g_stds += ['gnu17']
+ if version_compare(self.version, self._C18_VERSION):
+ c_stds += ['c18']
+ g_stds += ['gnu18']
+ if version_compare(self.version, self._C2X_VERSION):
+ c_stds += ['c2x']
+ g_stds += ['gnu2x']
+ opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + c_stds + g_stds
+ return opts
+
+
+class ClangCCompiler(_ClangCStds, ClangCompiler, CCompiler):
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross, info, exe_wrapper, linker=linker, full_version=full_version)
+ ClangCompiler.__init__(self, defines)
+ default_warn_args = ['-Wall', '-Winvalid-pch']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': ['-Weverything']}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = super().get_options()
+ if self.info.is_windows() or self.info.is_cygwin():
+ opts.update({
+ OptionKey('winlibs', machine=self.for_machine, lang=self.language): coredata.UserArrayOption(
+ 'Standard Win libraries to link against',
+ gnu_winlibs,
+ ),
+ })
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ std = options[OptionKey('std', machine=self.for_machine, lang=self.language)]
+ if std.value != 'none':
+ args.append('-std=' + std.value)
+ return args
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ if self.info.is_windows() or self.info.is_cygwin():
+ # without a typedict mypy can't understand this.
+ libs = options[OptionKey('winlibs', machine=self.for_machine, lang=self.language)].value.copy()
+ assert isinstance(libs, list)
+ for l in libs:
+ assert isinstance(l, str)
+ return libs
+ return []
+
+
+class ArmLtdClangCCompiler(ClangCCompiler):
+
+ id = 'armltdclang'
+
+
+class AppleClangCCompiler(ClangCCompiler):
+
+ """Handle the differences between Apple Clang and Vanilla Clang.
+
+ Right now this just handles the differences between the versions that new
+ C standards were added.
+ """
+
+ _C17_VERSION = '>=10.0.0'
+ _C18_VERSION = '>=11.0.0'
+ _C2X_VERSION = '>=11.0.0'
+
+
+class EmscriptenCCompiler(EmscriptenMixin, ClangCCompiler):
+
+ id = 'emscripten'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ full_version: T.Optional[str] = None):
+ if not is_cross:
+ raise MesonException('Emscripten compiler can only be used for cross compilation.')
+ if not version_compare(version, '>=1.39.19'):
+ raise MesonException('Meson requires Emscripten >= 1.39.19')
+ ClangCCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper=exe_wrapper, linker=linker,
+ defines=defines, full_version=full_version)
+
+
+class ArmclangCCompiler(ArmclangCompiler, CCompiler):
+ '''
+ Keil armclang
+ '''
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ ArmclangCompiler.__init__(self)
+ default_warn_args = ['-Wall', '-Winvalid-pch']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': ['-Weverything']}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'c90', 'c99', 'c11', 'gnu90', 'gnu99', 'gnu11']
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ std = options[OptionKey('std', machine=self.for_machine, lang=self.language)]
+ if std.value != 'none':
+ args.append('-std=' + std.value)
+ return args
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+
+class GnuCCompiler(GnuCompiler, CCompiler):
+
+ _C18_VERSION = '>=8.0.0'
+ _C2X_VERSION = '>=9.0.0'
+ _INVALID_PCH_VERSION = ">=3.4.0"
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross, info, exe_wrapper, linker=linker, full_version=full_version)
+ GnuCompiler.__init__(self, defines)
+ default_warn_args = ['-Wall']
+ if version_compare(self.version, self._INVALID_PCH_VERSION):
+ default_warn_args += ['-Winvalid-pch']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': (default_warn_args + ['-Wextra', '-Wpedantic'] +
+ self.supported_warn_args(gnu_common_warning_args) +
+ self.supported_warn_args(gnu_c_warning_args))}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CCompiler.get_options(self)
+ c_stds = ['c89', 'c99', 'c11']
+ g_stds = ['gnu89', 'gnu99', 'gnu11']
+ if version_compare(self.version, self._C18_VERSION):
+ c_stds += ['c17', 'c18']
+ g_stds += ['gnu17', 'gnu18']
+ if version_compare(self.version, self._C2X_VERSION):
+ c_stds += ['c2x']
+ g_stds += ['gnu2x']
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none'] + c_stds + g_stds
+ if self.info.is_windows() or self.info.is_cygwin():
+ opts.update({
+ key.evolve('winlibs'): coredata.UserArrayOption(
+ 'Standard Win libraries to link against',
+ gnu_winlibs,
+ ),
+ })
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ std = options[OptionKey('std', lang=self.language, machine=self.for_machine)]
+ if std.value != 'none':
+ args.append('-std=' + std.value)
+ return args
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ if self.info.is_windows() or self.info.is_cygwin():
+ # without a typeddict mypy can't figure this out
+ libs: T.List[str] = options[OptionKey('winlibs', lang=self.language, machine=self.for_machine)].value.copy()
+ assert isinstance(libs, list)
+ for l in libs:
+ assert isinstance(l, str)
+ return libs
+ return []
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ return ['-fpch-preprocess', '-include', os.path.basename(header)]
+
+
+class PGICCompiler(PGICompiler, CCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ PGICompiler.__init__(self)
+
+
+class NvidiaHPC_CCompiler(PGICompiler, CCompiler):
+
+ id = 'nvidia_hpc'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ PGICompiler.__init__(self)
+
+
+class ElbrusCCompiler(ElbrusCompiler, CCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ ElbrusCompiler.__init__(self)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CCompiler.get_options(self)
+ stds = ['c89', 'c9x', 'c99', 'gnu89', 'gnu9x', 'gnu99']
+ stds += ['iso9899:1990', 'iso9899:199409', 'iso9899:1999']
+ if version_compare(self.version, '>=1.20.00'):
+ stds += ['c11', 'gnu11']
+ if version_compare(self.version, '>=1.21.00') and version_compare(self.version, '<1.22.00'):
+ stds += ['c90', 'c1x', 'gnu90', 'gnu1x', 'iso9899:2011']
+ if version_compare(self.version, '>=1.23.00'):
+ stds += ['c90', 'c1x', 'gnu90', 'gnu1x', 'iso9899:2011']
+ if version_compare(self.version, '>=1.26.00'):
+ stds += ['c17', 'c18', 'iso9899:2017', 'iso9899:2018', 'gnu17', 'gnu18']
+ opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + stds
+ return opts
+
+ # Elbrus C compiler does not have lchmod, but there is only linker warning, not compiler error.
+ # So we should explicitly fail at this case.
+ def has_function(self, funcname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Optional[T.List[str]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ if funcname == 'lchmod':
+ return False, False
+ else:
+ return super().has_function(funcname, prefix, env,
+ extra_args=extra_args,
+ dependencies=dependencies)
+
+
+class IntelCCompiler(IntelGnuLikeCompiler, CCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ IntelGnuLikeCompiler.__init__(self)
+ self.lang_header = 'c-header'
+ default_warn_args = ['-Wall', '-w3']
+ self.warn_args = {'0': [],
+ '1': default_warn_args + ['-diag-disable:remark'],
+ '2': default_warn_args + ['-Wextra', '-diag-disable:remark'],
+ '3': default_warn_args + ['-Wextra', '-diag-disable:remark'],
+ 'everything': default_warn_args + ['-Wextra']}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CCompiler.get_options(self)
+ c_stds = ['c89', 'c99']
+ g_stds = ['gnu89', 'gnu99']
+ if version_compare(self.version, '>=16.0.0'):
+ c_stds += ['c11']
+ opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + c_stds + g_stds
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ std = options[OptionKey('std', machine=self.for_machine, lang=self.language)]
+ if std.value != 'none':
+ args.append('-std=' + std.value)
+ return args
+
+
+class IntelLLVMCCompiler(ClangCCompiler):
+
+ id = 'intel-llvm'
+
+
+class VisualStudioLikeCCompilerMixin(CompilerMixinBase):
+
+ """Shared methods that apply to MSVC-like C compilers."""
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = super().get_options()
+ opts.update({
+ OptionKey('winlibs', machine=self.for_machine, lang=self.language): coredata.UserArrayOption(
+ 'Windows libs to link against.',
+ msvc_winlibs,
+ ),
+ })
+ return opts
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ # need a TypeDict to make this work
+ key = OptionKey('winlibs', machine=self.for_machine, lang=self.language)
+ libs = options[key].value.copy()
+ assert isinstance(libs, list)
+ for l in libs:
+ assert isinstance(l, str)
+ return libs
+
+
+class VisualStudioCCompiler(MSVCCompiler, VisualStudioLikeCCompilerMixin, CCompiler):
+
+ _C11_VERSION = '>=19.28'
+ _C17_VERSION = '>=19.28'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo', target: str,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ MSVCCompiler.__init__(self, target)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = super().get_options()
+ c_stds = ['c89', 'c99']
+ # Need to have these to be compatible with projects
+ # that set c_std to e.g. gnu99.
+ # https://github.com/mesonbuild/meson/issues/7611
+ g_stds = ['gnu89', 'gnu90', 'gnu9x', 'gnu99']
+ if version_compare(self.version, self._C11_VERSION):
+ c_stds += ['c11']
+ g_stds += ['gnu1x', 'gnu11']
+ if version_compare(self.version, self._C17_VERSION):
+ c_stds += ['c17', 'c18']
+ g_stds += ['gnu17', 'gnu18']
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none'] + c_stds + g_stds
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ std = options[OptionKey('std', machine=self.for_machine, lang=self.language)]
+ if std.value.startswith('gnu'):
+ mlog.log_once(
+ 'cl.exe does not actually support gnu standards, and meson '
+ 'will instead demote to the nearest ISO C standard. This '
+ 'may cause compilation to fail.')
+ # As of MVSC 16.8, /std:c11 and /std:c17 are the only valid C standard options.
+ if std.value in {'c11', 'gnu1x', 'gnu11'}:
+ args.append('/std:c11')
+ elif std.value in {'c17', 'c18', 'gnu17', 'gnu18'}:
+ args.append('/std:c17')
+ return args
+
+
+class ClangClCCompiler(_ClangCStds, ClangClCompiler, VisualStudioLikeCCompilerMixin, CCompiler):
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo', target: str,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, [], exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ ClangClCompiler.__init__(self, target)
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key].value
+ if std != "none":
+ return [f'/clang:-std={std}']
+ return []
+
+
+class IntelClCCompiler(IntelVisualStudioLikeCompiler, VisualStudioLikeCCompilerMixin, CCompiler):
+
+ """Intel "ICL" compiler abstraction."""
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo', target: str,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, [], exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ IntelVisualStudioLikeCompiler.__init__(self, target)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = super().get_options()
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'c89', 'c99', 'c11']
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value == 'c89':
+ mlog.log_once("ICL doesn't explicitly implement c89, setting the standard to 'none', which is close.")
+ elif std.value != 'none':
+ args.append('/Qstd:' + std.value)
+ return args
+
+
+class IntelLLVMClCCompiler(IntelClCCompiler):
+
+ id = 'intel-llvm-cl'
+
+
+class ArmCCompiler(ArmCompiler, CCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ ArmCompiler.__init__(self)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'c89', 'c99', 'c11']
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append('--' + std.value)
+ return args
+
+
+class CcrxCCompiler(CcrxCompiler, CCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ CcrxCompiler.__init__(self)
+
+ # Override CCompiler.get_always_args
+ def get_always_args(self) -> T.List[str]:
+ return ['-nologo']
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'c89', 'c99']
+ return opts
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ return []
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value == 'c89':
+ args.append('-lang=c')
+ elif std.value == 'c99':
+ args.append('-lang=c99')
+ return args
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return []
+
+ def get_no_optimization_args(self) -> T.List[str]:
+ return ['-optimize=0']
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [f'-output=obj={target}']
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['-change_message=error']
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if path == '':
+ path = '.'
+ return ['-include=' + path]
+
+
+class Xc16CCompiler(Xc16Compiler, CCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ Xc16Compiler.__init__(self)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'c89', 'c99', 'gnu89', 'gnu99']
+ return opts
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ return []
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append('-ansi')
+ args.append('-std=' + std.value)
+ return args
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return []
+
+ def get_no_optimization_args(self) -> T.List[str]:
+ return ['-O0']
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [f'-o{target}']
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['-change_message=error']
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if path == '':
+ path = '.'
+ return ['-I' + path]
+
+class CompCertCCompiler(CompCertCompiler, CCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ CompCertCompiler.__init__(self)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'c89', 'c99']
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+ def get_no_optimization_args(self) -> T.List[str]:
+ return ['-O0']
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [f'-o{target}']
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['-Werror']
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if path == '':
+ path = '.'
+ return ['-I' + path]
+
+class TICCompiler(TICompiler, CCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ TICompiler.__init__(self)
+
+ # Override CCompiler.get_always_args
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'c89', 'c99', 'c11']
+ return opts
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ return []
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append('--' + std.value)
+ return args
+
+class C2000CCompiler(TICCompiler):
+ # Required for backwards compat with projects created before ti-cgt support existed
+ id = 'c2000'
diff --git a/mesonbuild/compilers/c_function_attributes.py b/mesonbuild/compilers/c_function_attributes.py
new file mode 100644
index 0000000..f663bfc
--- /dev/null
+++ b/mesonbuild/compilers/c_function_attributes.py
@@ -0,0 +1,141 @@
+# These functions are based on the following code:
+# https://git.savannah.gnu.org/gitweb/?p=autoconf-archive.git;a=blob_plain;f=m4/ax_gcc_func_attribute.m4,
+# which is licensed under the following terms:
+#
+# Copyright (c) 2013 Gabriele Svelto <gabriele.svelto@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+#
+
+C_FUNC_ATTRIBUTES = {
+ 'alias': '''
+ int foo(void) { return 0; }
+ int bar(void) __attribute__((alias("foo")));''',
+ 'aligned':
+ 'int foo(void) __attribute__((aligned(32)));',
+ 'alloc_size':
+ 'void *foo(int a) __attribute__((alloc_size(1)));',
+ 'always_inline':
+ 'inline __attribute__((always_inline)) int foo(void) { return 0; }',
+ 'artificial':
+ 'inline __attribute__((artificial)) int foo(void) { return 0; }',
+ 'cold':
+ 'int foo(void) __attribute__((cold));',
+ 'const':
+ 'int foo(void) __attribute__((const));',
+ 'constructor':
+ 'int foo(void) __attribute__((constructor));',
+ 'constructor_priority':
+ 'int foo( void ) __attribute__((__constructor__(65535/2)));',
+ 'deprecated':
+ 'int foo(void) __attribute__((deprecated("")));',
+ 'destructor':
+ 'int foo(void) __attribute__((destructor));',
+ 'dllexport':
+ '__declspec(dllexport) int foo(void) { return 0; }',
+ 'dllimport':
+ '__declspec(dllimport) int foo(void);',
+ 'error':
+ 'int foo(void) __attribute__((error("")));',
+ 'externally_visible':
+ 'int foo(void) __attribute__((externally_visible));',
+ 'fallthrough': '''
+ int foo( void ) {
+ switch (0) {
+ case 1: __attribute__((fallthrough));
+ case 2: break;
+ }
+ return 0;
+ };''',
+ 'flatten':
+ 'int foo(void) __attribute__((flatten));',
+ 'format':
+ 'int foo(const char * p, ...) __attribute__((format(printf, 1, 2)));',
+ 'format_arg':
+ 'char * foo(const char * p) __attribute__((format_arg(1)));',
+ 'force_align_arg_pointer':
+ '__attribute__((force_align_arg_pointer)) int foo(void) { return 0; }',
+ 'gnu_inline':
+ 'inline __attribute__((gnu_inline)) int foo(void) { return 0; }',
+ 'hot':
+ 'int foo(void) __attribute__((hot));',
+ 'ifunc':
+ ('int my_foo(void) { return 0; }'
+ 'static int (*resolve_foo(void))(void) { return my_foo; }'
+ 'int foo(void) __attribute__((ifunc("resolve_foo")));'),
+ 'leaf':
+ '__attribute__((leaf)) int foo(void) { return 0; }',
+ 'malloc':
+ 'int *foo(void) __attribute__((malloc));',
+ 'noclone':
+ 'int foo(void) __attribute__((noclone));',
+ 'noinline':
+ '__attribute__((noinline)) int foo(void) { return 0; }',
+ 'nonnull':
+ 'int foo(char * p) __attribute__((nonnull(1)));',
+ 'noreturn':
+ 'int foo(void) __attribute__((noreturn));',
+ 'nothrow':
+ 'int foo(void) __attribute__((nothrow));',
+ 'optimize':
+ '__attribute__((optimize(3))) int foo(void) { return 0; }',
+ 'packed':
+ 'struct __attribute__((packed)) foo { int bar; };',
+ 'pure':
+ 'int foo(void) __attribute__((pure));',
+ 'returns_nonnull':
+ 'int *foo(void) __attribute__((returns_nonnull));',
+ 'section': '''
+ #if defined(__APPLE__) && defined(__MACH__)
+ extern int foo __attribute__((section("__BAR,__bar")));
+ #else
+ extern int foo __attribute__((section(".bar")));
+ #endif''',
+ 'sentinel':
+ 'int foo(const char *bar, ...) __attribute__((sentinel));',
+ 'unused':
+ 'int foo(void) __attribute__((unused));',
+ 'used':
+ 'int foo(void) __attribute__((used));',
+ 'visibility': '''
+ int foo_def(void) __attribute__((visibility("default")));
+ int foo_hid(void) __attribute__((visibility("hidden")));
+ int foo_int(void) __attribute__((visibility("internal")));''',
+ 'visibility:default':
+ 'int foo(void) __attribute__((visibility("default")));',
+ 'visibility:hidden':
+ 'int foo(void) __attribute__((visibility("hidden")));',
+ 'visibility:internal':
+ 'int foo(void) __attribute__((visibility("internal")));',
+ 'visibility:protected':
+ 'int foo(void) __attribute__((visibility("protected")));',
+ 'warning':
+ 'int foo(void) __attribute__((warning("")));',
+ 'warn_unused_result':
+ 'int foo(void) __attribute__((warn_unused_result));',
+ 'weak':
+ 'int foo(void) __attribute__((weak));',
+ 'weakref': '''
+ static int foo(void) { return 0; }
+ static int var(void) __attribute__((weakref("foo")));''',
+ 'retain': '__attribute__((retain)) int x;',
+}
+
+CXX_FUNC_ATTRIBUTES = {
+ # Alias must be applied to the mangled name in C++
+ 'alias':
+ ('extern "C" {'
+ 'int foo(void) { return 0; }'
+ '}'
+ 'int bar(void) __attribute__((alias("foo")));'
+ ),
+ 'ifunc':
+ ('extern "C" {'
+ 'int my_foo(void) { return 0; }'
+ 'static int (*resolve_foo(void))(void) { return my_foo; }'
+ '}'
+ 'int foo(void) __attribute__((ifunc("resolve_foo")));'),
+}
diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py
new file mode 100644
index 0000000..c5a51cb
--- /dev/null
+++ b/mesonbuild/compilers/compilers.py
@@ -0,0 +1,1342 @@
+# Copyright 2012-2022 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.
+from __future__ import annotations
+
+import abc
+import contextlib, os.path, re
+import enum
+import itertools
+import typing as T
+from functools import lru_cache
+
+from .. import coredata
+from .. import mlog
+from .. import mesonlib
+from ..mesonlib import (
+ HoldableObject,
+ EnvironmentException, MesonException,
+ Popen_safe, LibType, TemporaryDirectoryWinProof, OptionKey,
+)
+
+from ..arglist import CompilerArgs
+
+if T.TYPE_CHECKING:
+ from ..build import BuildTarget
+ from ..coredata import MutableKeyedOptionDictType, KeyedOptionDictType
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..linkers import DynamicLinker, RSPFileSyntax
+ from ..mesonlib import MachineChoice
+ from ..dependencies import Dependency
+
+ CompilerType = T.TypeVar('CompilerType', bound='Compiler')
+ _T = T.TypeVar('_T')
+
+"""This file contains the data files of all compilers Meson knows
+about. To support a new compiler, add its information below.
+Also add corresponding autodetection code in environment.py."""
+
+header_suffixes = {'h', 'hh', 'hpp', 'hxx', 'H', 'ipp', 'moc', 'vapi', 'di'}
+obj_suffixes = {'o', 'obj', 'res'}
+# To the emscripten compiler, .js files are libraries
+lib_suffixes = {'a', 'lib', 'dll', 'dll.a', 'dylib', 'so', 'js'}
+# Mapping of language to suffixes of files that should always be in that language
+# This means we can't include .h headers here since they could be C, C++, ObjC, etc.
+# First suffix is the language's default.
+lang_suffixes = {
+ 'c': ('c',),
+ 'cpp': ('cpp', 'cc', 'cxx', 'c++', 'hh', 'hpp', 'ipp', 'hxx', 'ino', 'ixx', 'C'),
+ 'cuda': ('cu',),
+ # f90, f95, f03, f08 are for free-form fortran ('f90' recommended)
+ # f, for, ftn, fpp are for fixed-form fortran ('f' or 'for' recommended)
+ 'fortran': ('f90', 'f95', 'f03', 'f08', 'f', 'for', 'ftn', 'fpp'),
+ 'd': ('d', 'di'),
+ 'objc': ('m',),
+ 'objcpp': ('mm',),
+ 'rust': ('rs',),
+ 'vala': ('vala', 'vapi', 'gs'),
+ 'cs': ('cs',),
+ 'swift': ('swift',),
+ 'java': ('java',),
+ 'cython': ('pyx', ),
+ 'nasm': ('asm',),
+ 'masm': ('masm',),
+}
+all_languages = lang_suffixes.keys()
+c_cpp_suffixes = {'h'}
+cpp_suffixes = set(lang_suffixes['cpp']) | c_cpp_suffixes
+c_suffixes = set(lang_suffixes['c']) | c_cpp_suffixes
+assembler_suffixes = {'s', 'S', 'asm', 'masm'}
+llvm_ir_suffixes = {'ll'}
+all_suffixes = set(itertools.chain(*lang_suffixes.values(), assembler_suffixes, llvm_ir_suffixes, c_cpp_suffixes))
+source_suffixes = all_suffixes - header_suffixes
+# List of languages that by default consume and output libraries following the
+# C ABI; these can generally be used interchangeably
+# This must be sorted, see sort_clink().
+clib_langs = ('objcpp', 'cpp', 'objc', 'c', 'fortran')
+# List of languages that can be linked with C code directly by the linker
+# used in build.py:process_compilers() and build.py:get_dynamic_linker()
+# This must be sorted, see sort_clink().
+clink_langs = ('d', 'cuda') + clib_langs
+
+SUFFIX_TO_LANG = dict(itertools.chain(*(
+ [(suffix, lang) for suffix in v] for lang, v in lang_suffixes.items()))) # type: T.Dict[str, str]
+
+# Languages that should use LDFLAGS arguments when linking.
+LANGUAGES_USING_LDFLAGS = {'objcpp', 'cpp', 'objc', 'c', 'fortran', 'd', 'cuda'} # type: T.Set[str]
+# Languages that should use CPPFLAGS arguments when linking.
+LANGUAGES_USING_CPPFLAGS = {'c', 'cpp', 'objc', 'objcpp'} # type: T.Set[str]
+soregex = re.compile(r'.*\.so(\.[0-9]+)?(\.[0-9]+)?(\.[0-9]+)?$')
+
+# Environment variables that each lang uses.
+CFLAGS_MAPPING: T.Mapping[str, str] = {
+ 'c': 'CFLAGS',
+ 'cpp': 'CXXFLAGS',
+ 'cuda': 'CUFLAGS',
+ 'objc': 'OBJCFLAGS',
+ 'objcpp': 'OBJCXXFLAGS',
+ 'fortran': 'FFLAGS',
+ 'd': 'DFLAGS',
+ 'vala': 'VALAFLAGS',
+ 'rust': 'RUSTFLAGS',
+ 'cython': 'CYTHONFLAGS',
+ 'cs': 'CSFLAGS', # This one might not be standard.
+}
+
+# All these are only for C-linkable languages; see `clink_langs` above.
+
+def sort_clink(lang: str) -> int:
+ '''
+ Sorting function to sort the list of languages according to
+ reversed(compilers.clink_langs) and append the unknown langs in the end.
+ The purpose is to prefer C over C++ for files that can be compiled by
+ both such as assembly, C, etc. Also applies to ObjC, ObjC++, etc.
+ '''
+ if lang not in clink_langs:
+ return 1
+ return -clink_langs.index(lang)
+
+def is_header(fname: 'mesonlib.FileOrString') -> bool:
+ if isinstance(fname, mesonlib.File):
+ fname = fname.fname
+ suffix = fname.split('.')[-1]
+ return suffix in header_suffixes
+
+def is_source(fname: 'mesonlib.FileOrString') -> bool:
+ if isinstance(fname, mesonlib.File):
+ fname = fname.fname
+ suffix = fname.split('.')[-1].lower()
+ return suffix in source_suffixes
+
+def is_assembly(fname: 'mesonlib.FileOrString') -> bool:
+ if isinstance(fname, mesonlib.File):
+ fname = fname.fname
+ suffix = fname.split('.')[-1]
+ return suffix in assembler_suffixes
+
+def is_llvm_ir(fname: 'mesonlib.FileOrString') -> bool:
+ if isinstance(fname, mesonlib.File):
+ fname = fname.fname
+ suffix = fname.split('.')[-1]
+ return suffix in llvm_ir_suffixes
+
+@lru_cache(maxsize=None)
+def cached_by_name(fname: 'mesonlib.FileOrString') -> bool:
+ suffix = fname.split('.')[-1]
+ return suffix in obj_suffixes
+
+def is_object(fname: 'mesonlib.FileOrString') -> bool:
+ if isinstance(fname, mesonlib.File):
+ fname = fname.fname
+ return cached_by_name(fname)
+
+def is_library(fname: 'mesonlib.FileOrString') -> bool:
+ if isinstance(fname, mesonlib.File):
+ fname = fname.fname
+
+ if soregex.match(fname):
+ return True
+
+ suffix = fname.split('.')[-1]
+ return suffix in lib_suffixes
+
+def is_known_suffix(fname: 'mesonlib.FileOrString') -> bool:
+ if isinstance(fname, mesonlib.File):
+ fname = fname.fname
+ suffix = fname.split('.')[-1]
+
+ return suffix in all_suffixes
+
+
+class CompileCheckMode(enum.Enum):
+
+ PREPROCESS = 'preprocess'
+ COMPILE = 'compile'
+ LINK = 'link'
+
+
+cuda_buildtype_args = {'plain': [],
+ 'debug': ['-g', '-G'],
+ 'debugoptimized': ['-g', '-lineinfo'],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+java_buildtype_args = {'plain': [],
+ 'debug': ['-g'],
+ 'debugoptimized': ['-g'],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+rust_buildtype_args = {'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+d_gdc_buildtype_args = {'plain': [],
+ 'debug': [],
+ 'debugoptimized': ['-finline-functions'],
+ 'release': ['-finline-functions'],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+d_ldc_buildtype_args = {'plain': [],
+ 'debug': [],
+ 'debugoptimized': ['-enable-inlining', '-Hkeep-all-bodies'],
+ 'release': ['-enable-inlining', '-Hkeep-all-bodies'],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+d_dmd_buildtype_args = {'plain': [],
+ 'debug': [],
+ 'debugoptimized': ['-inline'],
+ 'release': ['-inline'],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+mono_buildtype_args = {'plain': [],
+ 'debug': [],
+ 'debugoptimized': ['-optimize+'],
+ 'release': ['-optimize+'],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+swift_buildtype_args = {'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+gnu_winlibs = ['-lkernel32', '-luser32', '-lgdi32', '-lwinspool', '-lshell32',
+ '-lole32', '-loleaut32', '-luuid', '-lcomdlg32', '-ladvapi32'] # type: T.List[str]
+
+msvc_winlibs = ['kernel32.lib', 'user32.lib', 'gdi32.lib',
+ 'winspool.lib', 'shell32.lib', 'ole32.lib', 'oleaut32.lib',
+ 'uuid.lib', 'comdlg32.lib', 'advapi32.lib'] # type: T.List[str]
+
+clike_optimization_args = {'plain': [],
+ '0': [],
+ 'g': [],
+ '1': ['-O1'],
+ '2': ['-O2'],
+ '3': ['-O3'],
+ 's': ['-Os'],
+ } # type: T.Dict[str, T.List[str]]
+
+cuda_optimization_args = {'plain': [],
+ '0': [],
+ 'g': ['-O0'],
+ '1': ['-O1'],
+ '2': ['-O2'],
+ '3': ['-O3'],
+ 's': ['-O3']
+ } # type: T.Dict[str, T.List[str]]
+
+cuda_debug_args = {False: [],
+ True: ['-g']} # type: T.Dict[bool, T.List[str]]
+
+clike_debug_args = {False: [],
+ True: ['-g']} # type: T.Dict[bool, T.List[str]]
+
+base_options: 'KeyedOptionDictType' = {
+ OptionKey('b_pch'): coredata.UserBooleanOption('Use precompiled headers', True),
+ OptionKey('b_lto'): coredata.UserBooleanOption('Use link time optimization', False),
+ OptionKey('b_lto_threads'): coredata.UserIntegerOption('Use multiple threads for Link Time Optimization', (None, None, 0)),
+ OptionKey('b_lto_mode'): coredata.UserComboOption('Select between different LTO modes.',
+ ['default', 'thin'],
+ 'default'),
+ OptionKey('b_thinlto_cache'): coredata.UserBooleanOption('Use LLVM ThinLTO caching for faster incremental builds', False),
+ OptionKey('b_thinlto_cache_dir'): coredata.UserStringOption('Directory to store ThinLTO cache objects', ''),
+ OptionKey('b_sanitize'): coredata.UserComboOption('Code sanitizer to use',
+ ['none', 'address', 'thread', 'undefined', 'memory', 'leak', 'address,undefined'],
+ 'none'),
+ OptionKey('b_lundef'): coredata.UserBooleanOption('Use -Wl,--no-undefined when linking', True),
+ OptionKey('b_asneeded'): coredata.UserBooleanOption('Use -Wl,--as-needed when linking', True),
+ OptionKey('b_pgo'): coredata.UserComboOption('Use profile guided optimization',
+ ['off', 'generate', 'use'],
+ 'off'),
+ OptionKey('b_coverage'): coredata.UserBooleanOption('Enable coverage tracking.', False),
+ OptionKey('b_colorout'): coredata.UserComboOption('Use colored output',
+ ['auto', 'always', 'never'],
+ 'always'),
+ OptionKey('b_ndebug'): coredata.UserComboOption('Disable asserts', ['true', 'false', 'if-release'], 'false'),
+ OptionKey('b_staticpic'): coredata.UserBooleanOption('Build static libraries as position independent', True),
+ OptionKey('b_pie'): coredata.UserBooleanOption('Build executables as position independent', False),
+ OptionKey('b_bitcode'): coredata.UserBooleanOption('Generate and embed bitcode (only macOS/iOS/tvOS)', False),
+ OptionKey('b_vscrt'): coredata.UserComboOption('VS run-time library type to use.',
+ ['none', 'md', 'mdd', 'mt', 'mtd', 'from_buildtype', 'static_from_buildtype'],
+ 'from_buildtype'),
+}
+
+def option_enabled(boptions: T.Set[OptionKey], options: 'KeyedOptionDictType',
+ option: OptionKey) -> bool:
+ try:
+ if option not in boptions:
+ return False
+ ret = options[option].value
+ assert isinstance(ret, bool), 'must return bool' # could also be str
+ return ret
+ except KeyError:
+ return False
+
+
+def get_option_value(options: 'KeyedOptionDictType', opt: OptionKey, fallback: '_T') -> '_T':
+ """Get the value of an option, or the fallback value."""
+ try:
+ v: '_T' = options[opt].value
+ except KeyError:
+ return fallback
+
+ assert isinstance(v, type(fallback)), f'Should have {type(fallback)!r} but was {type(v)!r}'
+ # Mypy doesn't understand that the above assert ensures that v is type _T
+ return v
+
+
+def get_base_compile_args(options: 'KeyedOptionDictType', compiler: 'Compiler') -> T.List[str]:
+ args = [] # type T.List[str]
+ try:
+ if options[OptionKey('b_lto')].value:
+ args.extend(compiler.get_lto_compile_args(
+ threads=get_option_value(options, OptionKey('b_lto_threads'), 0),
+ mode=get_option_value(options, OptionKey('b_lto_mode'), 'default')))
+ except KeyError:
+ pass
+ try:
+ args += compiler.get_colorout_args(options[OptionKey('b_colorout')].value)
+ except KeyError:
+ pass
+ try:
+ args += compiler.sanitizer_compile_args(options[OptionKey('b_sanitize')].value)
+ except KeyError:
+ pass
+ try:
+ pgo_val = options[OptionKey('b_pgo')].value
+ if pgo_val == 'generate':
+ args.extend(compiler.get_profile_generate_args())
+ elif pgo_val == 'use':
+ args.extend(compiler.get_profile_use_args())
+ except KeyError:
+ pass
+ try:
+ if options[OptionKey('b_coverage')].value:
+ args += compiler.get_coverage_args()
+ except KeyError:
+ pass
+ try:
+ if (options[OptionKey('b_ndebug')].value == 'true' or
+ (options[OptionKey('b_ndebug')].value == 'if-release' and
+ options[OptionKey('buildtype')].value in {'release', 'plain'})):
+ args += compiler.get_disable_assert_args()
+ except KeyError:
+ pass
+ # This does not need a try...except
+ if option_enabled(compiler.base_options, options, OptionKey('b_bitcode')):
+ args.append('-fembed-bitcode')
+ try:
+ crt_val = options[OptionKey('b_vscrt')].value
+ buildtype = options[OptionKey('buildtype')].value
+ try:
+ args += compiler.get_crt_compile_args(crt_val, buildtype)
+ except AttributeError:
+ pass
+ except KeyError:
+ pass
+ return args
+
+def get_base_link_args(options: 'KeyedOptionDictType', linker: 'Compiler',
+ is_shared_module: bool, build_dir: str) -> T.List[str]:
+ args = [] # type: T.List[str]
+ try:
+ if options[OptionKey('b_lto')].value:
+ thinlto_cache_dir = None
+ if get_option_value(options, OptionKey('b_thinlto_cache'), False):
+ thinlto_cache_dir = get_option_value(options, OptionKey('b_thinlto_cache_dir'), '')
+ if thinlto_cache_dir == '':
+ thinlto_cache_dir = os.path.join(build_dir, 'meson-private', 'thinlto-cache')
+ args.extend(linker.get_lto_link_args(
+ threads=get_option_value(options, OptionKey('b_lto_threads'), 0),
+ mode=get_option_value(options, OptionKey('b_lto_mode'), 'default'),
+ thinlto_cache_dir=thinlto_cache_dir))
+ except KeyError:
+ pass
+ try:
+ args += linker.sanitizer_link_args(options[OptionKey('b_sanitize')].value)
+ except KeyError:
+ pass
+ try:
+ pgo_val = options[OptionKey('b_pgo')].value
+ if pgo_val == 'generate':
+ args.extend(linker.get_profile_generate_args())
+ elif pgo_val == 'use':
+ args.extend(linker.get_profile_use_args())
+ except KeyError:
+ pass
+ try:
+ if options[OptionKey('b_coverage')].value:
+ args += linker.get_coverage_link_args()
+ except KeyError:
+ pass
+
+ as_needed = option_enabled(linker.base_options, options, OptionKey('b_asneeded'))
+ bitcode = option_enabled(linker.base_options, options, OptionKey('b_bitcode'))
+ # Shared modules cannot be built with bitcode_bundle because
+ # -bitcode_bundle is incompatible with -undefined and -bundle
+ if bitcode and not is_shared_module:
+ args.extend(linker.bitcode_args())
+ elif as_needed:
+ # -Wl,-dead_strip_dylibs is incompatible with bitcode
+ args.extend(linker.get_asneeded_args())
+
+ # Apple's ld (the only one that supports bitcode) does not like -undefined
+ # arguments or -headerpad_max_install_names when bitcode is enabled
+ if not bitcode:
+ args.extend(linker.headerpad_args())
+ if (not is_shared_module and
+ option_enabled(linker.base_options, options, OptionKey('b_lundef'))):
+ args.extend(linker.no_undefined_link_args())
+ else:
+ args.extend(linker.get_allow_undefined_link_args())
+
+ try:
+ crt_val = options[OptionKey('b_vscrt')].value
+ buildtype = options[OptionKey('buildtype')].value
+ try:
+ args += linker.get_crt_link_args(crt_val, buildtype)
+ except AttributeError:
+ pass
+ except KeyError:
+ pass
+ return args
+
+
+class CrossNoRunException(MesonException):
+ pass
+
+class RunResult(HoldableObject):
+ def __init__(self, compiled: bool, returncode: int = 999,
+ stdout: str = 'UNDEFINED', stderr: str = 'UNDEFINED'):
+ self.compiled = compiled
+ self.returncode = returncode
+ self.stdout = stdout
+ self.stderr = stderr
+
+
+class CompileResult(HoldableObject):
+
+ """The result of Compiler.compiles (and friends)."""
+
+ def __init__(self, stdo: T.Optional[str] = None, stde: T.Optional[str] = None,
+ command: T.Optional[T.List[str]] = None,
+ returncode: int = 999,
+ input_name: T.Optional[str] = None,
+ output_name: T.Optional[str] = None,
+ cached: bool = False):
+ self.stdout = stdo
+ self.stderr = stde
+ self.input_name = input_name
+ self.output_name = output_name
+ self.command = command or []
+ self.cached = cached
+ self.returncode = returncode
+
+
+class Compiler(HoldableObject, metaclass=abc.ABCMeta):
+ # Libraries to ignore in find_library() since they are provided by the
+ # compiler or the C library. Currently only used for MSVC.
+ ignore_libs = [] # type: T.List[str]
+ # Libraries that are internal compiler implementations, and must not be
+ # manually searched.
+ internal_libs = [] # type: T.List[str]
+
+ LINKER_PREFIX = None # type: T.Union[None, str, T.List[str]]
+ INVOKES_LINKER = True
+
+ language: str
+ id: str
+ warn_args: T.Dict[str, T.List[str]]
+ mode: str = 'COMPILER'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str,
+ for_machine: MachineChoice, info: 'MachineInfo',
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None, is_cross: bool = False):
+ self.exelist = ccache + exelist
+ self.exelist_no_ccache = exelist
+ # In case it's been overridden by a child class already
+ if not hasattr(self, 'file_suffixes'):
+ self.file_suffixes = lang_suffixes[self.language]
+ if not hasattr(self, 'can_compile_suffixes'):
+ self.can_compile_suffixes = set(self.file_suffixes)
+ self.default_suffix = self.file_suffixes[0]
+ self.version = version
+ self.full_version = full_version
+ self.for_machine = for_machine
+ self.base_options: T.Set[OptionKey] = set()
+ self.linker = linker
+ self.info = info
+ self.is_cross = is_cross
+ self.modes: T.List[Compiler] = []
+
+ def __repr__(self) -> str:
+ repr_str = "<{0}: v{1} `{2}`>"
+ return repr_str.format(self.__class__.__name__, self.version,
+ ' '.join(self.exelist))
+
+ @lru_cache(maxsize=None)
+ def can_compile(self, src: 'mesonlib.FileOrString') -> bool:
+ if isinstance(src, mesonlib.File):
+ src = src.fname
+ suffix = os.path.splitext(src)[1]
+ if suffix != '.C':
+ suffix = suffix.lower()
+ return bool(suffix) and suffix[1:] in self.can_compile_suffixes
+
+ def get_id(self) -> str:
+ return self.id
+
+ def get_modes(self) -> T.List[Compiler]:
+ return self.modes
+
+ def get_linker_id(self) -> str:
+ # There is not guarantee that we have a dynamic linker instance, as
+ # some languages don't have separate linkers and compilers. In those
+ # cases return the compiler id
+ try:
+ return self.linker.id
+ except AttributeError:
+ return self.id
+
+ def get_version_string(self) -> str:
+ details = [self.id, self.version]
+ if self.full_version:
+ details += ['"%s"' % (self.full_version)]
+ return '(%s)' % (' '.join(details))
+
+ def get_language(self) -> str:
+ return self.language
+
+ @classmethod
+ def get_display_language(cls) -> str:
+ return cls.language.capitalize()
+
+ def get_default_suffix(self) -> str:
+ return self.default_suffix
+
+ def get_define(self, dname: str, prefix: str, env: 'Environment',
+ extra_args: T.Union[T.List[str], T.Callable[[CompileCheckMode], T.List[str]]],
+ dependencies: T.List['Dependency'],
+ disable_cache: bool = False) -> T.Tuple[str, bool]:
+ raise EnvironmentException('%s does not support get_define ' % self.get_id())
+
+ def compute_int(self, expression: str, low: T.Optional[int], high: T.Optional[int],
+ guess: T.Optional[int], prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]],
+ dependencies: T.Optional[T.List['Dependency']]) -> int:
+ raise EnvironmentException('%s does not support compute_int ' % self.get_id())
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ raise EnvironmentException('%s does not support compute_parameters_with_absolute_paths ' % self.get_id())
+
+ def has_members(self, typename: str, membernames: T.List[str],
+ prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ raise EnvironmentException('%s does not support has_member(s) ' % self.get_id())
+
+ def has_type(self, typename: str, prefix: str, env: 'Environment',
+ extra_args: T.Union[T.List[str], T.Callable[[CompileCheckMode], T.List[str]]], *,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ raise EnvironmentException('%s does not support has_type ' % self.get_id())
+
+ def symbols_have_underscore_prefix(self, env: 'Environment') -> bool:
+ raise EnvironmentException('%s does not support symbols_have_underscore_prefix ' % self.get_id())
+
+ def get_exelist(self, ccache: bool = True) -> T.List[str]:
+ return self.exelist.copy() if ccache else self.exelist_no_ccache.copy()
+
+ def get_linker_exelist(self) -> T.List[str]:
+ return self.linker.get_exelist()
+
+ @abc.abstractmethod
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ pass
+
+ def get_linker_output_args(self, outputname: str) -> T.List[str]:
+ return self.linker.get_output_args(outputname)
+
+ def get_linker_search_args(self, dirname: str) -> T.List[str]:
+ return self.linker.get_search_args(dirname)
+
+ def get_builtin_define(self, define: str) -> T.Optional[str]:
+ raise EnvironmentException('%s does not support get_builtin_define.' % self.id)
+
+ def has_builtin_define(self, define: str) -> bool:
+ raise EnvironmentException('%s does not support has_builtin_define.' % self.id)
+
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+ def can_linker_accept_rsp(self) -> bool:
+ """
+ Determines whether the linker can accept arguments using the @rsp syntax.
+ """
+ return self.linker.get_accepts_rsp()
+
+ def get_linker_always_args(self) -> T.List[str]:
+ return self.linker.get_always_args()
+
+ def get_linker_lib_prefix(self) -> str:
+ return self.linker.get_lib_prefix()
+
+ def gen_import_library_args(self, implibname: str) -> T.List[str]:
+ """
+ Used only on Windows for libraries that need an import library.
+ This currently means C, C++, Fortran.
+ """
+ return []
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ return {}
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return self.linker.get_option_args(options)
+
+ def check_header(self, hname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ """Check that header is usable.
+
+ Returns a two item tuple of bools. The first bool is whether the
+ check succeeded, the second is whether the result was cached (True)
+ or run fresh (False).
+ """
+ raise EnvironmentException('Language %s does not support header checks.' % self.get_display_language())
+
+ def has_header(self, hname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None,
+ disable_cache: bool = False) -> T.Tuple[bool, bool]:
+ """Check that header is exists.
+
+ This check will return true if the file exists, even if it contains:
+
+ ```c
+ # error "You thought you could use this, LOLZ!"
+ ```
+
+ Use check_header if your header only works in some cases.
+
+ Returns a two item tuple of bools. The first bool is whether the
+ check succeeded, the second is whether the result was cached (True)
+ or run fresh (False).
+ """
+ raise EnvironmentException('Language %s does not support header checks.' % self.get_display_language())
+
+ def has_header_symbol(self, hname: str, symbol: str, prefix: str,
+ env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ raise EnvironmentException('Language %s does not support header symbol checks.' % self.get_display_language())
+
+ def run(self, code: 'mesonlib.FileOrString', env: 'Environment', *,
+ extra_args: T.Union[T.List[str], T.Callable[[CompileCheckMode], T.List[str]], None] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> RunResult:
+ raise EnvironmentException('Language %s does not support run checks.' % self.get_display_language())
+
+ def sizeof(self, typename: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> int:
+ raise EnvironmentException('Language %s does not support sizeof checks.' % self.get_display_language())
+
+ def alignment(self, typename: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Optional[T.List[str]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> int:
+ raise EnvironmentException('Language %s does not support alignment checks.' % self.get_display_language())
+
+ def has_function(self, funcname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Optional[T.List[str]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ """See if a function exists.
+
+ Returns a two item tuple of bools. The first bool is whether the
+ check succeeded, the second is whether the result was cached (True)
+ or run fresh (False).
+ """
+ raise EnvironmentException('Language %s does not support function checks.' % self.get_display_language())
+
+ @classmethod
+ def _unix_args_to_native(cls, args: T.List[str], info: MachineInfo) -> T.List[str]:
+ "Always returns a copy that can be independently mutated"
+ return args.copy()
+
+ def unix_args_to_native(self, args: T.List[str]) -> T.List[str]:
+ return self._unix_args_to_native(args, self.info)
+
+ @classmethod
+ def native_args_to_unix(cls, args: T.List[str]) -> T.List[str]:
+ "Always returns a copy that can be independently mutated"
+ return args.copy()
+
+ def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str],
+ libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]:
+ raise EnvironmentException(f'Language {self.get_display_language()} does not support library finding.')
+
+ def get_library_naming(self, env: 'Environment', libtype: LibType,
+ strict: bool = False) -> T.Optional[T.Tuple[str, ...]]:
+ raise EnvironmentException(
+ 'Language {} does not support get_library_naming.'.format(
+ self.get_display_language()))
+
+ def get_program_dirs(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def has_multi_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]:
+ raise EnvironmentException(
+ 'Language {} does not support has_multi_arguments.'.format(
+ self.get_display_language()))
+
+ def has_multi_link_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]:
+ return self.linker.has_multi_arguments(args, env)
+
+ def _get_compile_output(self, dirname: str, mode: str) -> str:
+ # TODO: mode should really be an enum
+ # In pre-processor mode, the output is sent to stdout and discarded
+ if mode == 'preprocess':
+ return None
+ # Extension only matters if running results; '.exe' is
+ # guaranteed to be executable on every platform.
+ if mode == 'link':
+ suffix = 'exe'
+ else:
+ suffix = 'obj'
+ return os.path.join(dirname, 'output.' + suffix)
+
+ def get_compiler_args_for_mode(self, mode: CompileCheckMode) -> T.List[str]:
+ # TODO: mode should really be an enum
+ args = [] # type: T.List[str]
+ args += self.get_always_args()
+ if mode is CompileCheckMode.COMPILE:
+ args += self.get_compile_only_args()
+ elif mode is CompileCheckMode.PREPROCESS:
+ args += self.get_preprocess_only_args()
+ else:
+ assert mode is CompileCheckMode.LINK
+ return args
+
+ def compiler_args(self, args: T.Optional[T.Iterable[str]] = None) -> CompilerArgs:
+ """Return an appropriate CompilerArgs instance for this class."""
+ return CompilerArgs(self, args)
+
+ @contextlib.contextmanager
+ def compile(self, code: 'mesonlib.FileOrString',
+ extra_args: T.Union[None, CompilerArgs, T.List[str]] = None,
+ *, mode: str = 'link', want_output: bool = False,
+ temp_dir: T.Optional[str] = None) -> T.Iterator[T.Optional[CompileResult]]:
+ # TODO: there isn't really any reason for this to be a contextmanager
+ if extra_args is None:
+ extra_args = []
+
+ with TemporaryDirectoryWinProof(dir=temp_dir) as tmpdirname:
+ no_ccache = False
+ if isinstance(code, str):
+ srcname = os.path.join(tmpdirname,
+ 'testfile.' + self.default_suffix)
+ with open(srcname, 'w', encoding='utf-8') as ofile:
+ ofile.write(code)
+ # ccache would result in a cache miss
+ no_ccache = True
+ contents = code
+ else:
+ srcname = code.fname
+ if not is_object(code.fname):
+ with open(code.fname, encoding='utf-8') as f:
+ contents = f.read()
+ else:
+ contents = '<binary>'
+
+ # Construct the compiler command-line
+ commands = self.compiler_args()
+ commands.append(srcname)
+
+ # Preprocess mode outputs to stdout, so no output args
+ output = self._get_compile_output(tmpdirname, mode)
+ if mode != 'preprocess':
+ commands += self.get_output_args(output)
+ commands.extend(self.get_compiler_args_for_mode(CompileCheckMode(mode)))
+
+ # extra_args must be last because it could contain '/link' to
+ # pass args to VisualStudio's linker. In that case everything
+ # in the command line after '/link' is given to the linker.
+ if extra_args:
+ commands += extra_args
+ # Generate full command-line with the exelist
+ command_list = self.get_exelist(ccache=not no_ccache) + commands.to_native()
+ mlog.debug('Running compile:')
+ mlog.debug('Working directory: ', tmpdirname)
+ mlog.debug('Command line: ', ' '.join(command_list), '\n')
+ mlog.debug('Code:\n', contents)
+ os_env = os.environ.copy()
+ os_env['LC_ALL'] = 'C'
+ if no_ccache:
+ os_env['CCACHE_DISABLE'] = '1'
+ p, stdo, stde = Popen_safe(command_list, cwd=tmpdirname, env=os_env)
+ mlog.debug('Compiler stdout:\n', stdo)
+ mlog.debug('Compiler stderr:\n', stde)
+
+ result = CompileResult(stdo, stde, command_list, p.returncode, input_name=srcname)
+ if want_output:
+ result.output_name = output
+ yield result
+
+ @contextlib.contextmanager
+ def cached_compile(self, code: 'mesonlib.FileOrString', cdata: coredata.CoreData, *,
+ extra_args: T.Union[None, T.List[str], CompilerArgs] = None,
+ mode: str = 'link',
+ temp_dir: T.Optional[str] = None) -> T.Iterator[T.Optional[CompileResult]]:
+ # TODO: There's isn't really any reason for this to be a context manager
+
+ # Calculate the key
+ textra_args = tuple(extra_args) if extra_args is not None else tuple() # type: T.Tuple[str, ...]
+ key = (tuple(self.exelist), self.version, code, textra_args, mode) # type: coredata.CompilerCheckCacheKey
+
+ # Check if not cached, and generate, otherwise get from the cache
+ if key in cdata.compiler_check_cache:
+ p = cdata.compiler_check_cache[key]
+ p.cached = True
+ mlog.debug('Using cached compile:')
+ mlog.debug('Cached command line: ', ' '.join(p.command), '\n')
+ mlog.debug('Code:\n', code)
+ mlog.debug('Cached compiler stdout:\n', p.stdout)
+ mlog.debug('Cached compiler stderr:\n', p.stderr)
+ yield p
+ else:
+ with self.compile(code, extra_args=extra_args, mode=mode, want_output=False, temp_dir=temp_dir) as p:
+ cdata.compiler_check_cache[key] = p
+ yield p
+
+ def get_colorout_args(self, colortype: str) -> T.List[str]:
+ # TODO: colortype can probably be an emum
+ return []
+
+ # Some compilers (msvc) write debug info to a separate file.
+ # These args specify where it should be written.
+ def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]:
+ return []
+
+ def get_link_debugfile_name(self, targetfile: str) -> str:
+ return self.linker.get_debugfile_name(targetfile)
+
+ def get_link_debugfile_args(self, targetfile: str) -> T.List[str]:
+ return self.linker.get_debugfile_args(targetfile)
+
+ def get_std_shared_lib_link_args(self) -> T.List[str]:
+ return self.linker.get_std_shared_lib_args()
+
+ def get_std_shared_module_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return self.linker.get_std_shared_module_args(options)
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ return self.linker.get_link_whole_for(args)
+
+ def get_allow_undefined_link_args(self) -> T.List[str]:
+ return self.linker.get_allow_undefined_args()
+
+ def no_undefined_link_args(self) -> T.List[str]:
+ return self.linker.no_undefined_args()
+
+ def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
+ """Compiler arguments needed to enable the given instruction set.
+
+ Return type ay be an empty list meaning nothing needed or None
+ meaning the given set is not supported.
+ """
+ return None
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ return self.linker.build_rpath_args(
+ env, build_dir, from_dir, rpath_paths, build_rpath, install_rpath)
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def thread_link_flags(self, env: 'Environment') -> T.List[str]:
+ return self.linker.thread_flags(env)
+
+ def openmp_flags(self) -> T.List[str]:
+ raise EnvironmentException('Language %s does not support OpenMP flags.' % self.get_display_language())
+
+ def openmp_link_flags(self) -> T.List[str]:
+ return self.openmp_flags()
+
+ def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def gnu_symbol_visibility_args(self, vistype: str) -> T.List[str]:
+ return []
+
+ def get_gui_app_args(self, value: bool) -> T.List[str]:
+ # Only used on Windows
+ return self.linker.get_gui_app_args(value)
+
+ def get_win_subsystem_args(self, value: str) -> T.List[str]:
+ # By default the dynamic linker is going to return an empty
+ # array in case it either doesn't support Windows subsystems
+ # or does not target Windows
+ return self.linker.get_win_subsystem_args(value)
+
+ def has_func_attribute(self, name: str, env: 'Environment') -> T.Tuple[bool, bool]:
+ raise EnvironmentException(
+ f'Language {self.get_display_language()} does not support function attributes.')
+
+ def get_pic_args(self) -> T.List[str]:
+ m = 'Language {} does not support position-independent code'
+ raise EnvironmentException(m.format(self.get_display_language()))
+
+ def get_pie_args(self) -> T.List[str]:
+ m = 'Language {} does not support position-independent executable'
+ raise EnvironmentException(m.format(self.get_display_language()))
+
+ def get_pie_link_args(self) -> T.List[str]:
+ return self.linker.get_pie_args()
+
+ def get_argument_syntax(self) -> str:
+ """Returns the argument family type.
+
+ Compilers fall into families if they try to emulate the command line
+ interface of another compiler. For example, clang is in the GCC family
+ since it accepts most of the same arguments as GCC. ICL (ICC on
+ windows) is in the MSVC family since it accepts most of the same
+ arguments as MSVC.
+ """
+ return 'other'
+
+ def get_profile_generate_args(self) -> T.List[str]:
+ raise EnvironmentException(
+ '%s does not support get_profile_generate_args ' % self.get_id())
+
+ def get_profile_use_args(self) -> T.List[str]:
+ raise EnvironmentException(
+ '%s does not support get_profile_use_args ' % self.get_id())
+
+ def remove_linkerlike_args(self, args: T.List[str]) -> T.List[str]:
+ rm_exact = ('-headerpad_max_install_names',)
+ rm_prefixes = ('-Wl,', '-L',)
+ rm_next = ('-L', '-framework',)
+ ret = [] # T.List[str]
+ iargs = iter(args)
+ for arg in iargs:
+ # Remove this argument
+ if arg in rm_exact:
+ continue
+ # If the argument starts with this, but is not *exactly* this
+ # f.ex., '-L' should match ['-Lfoo'] but not ['-L', 'foo']
+ if arg.startswith(rm_prefixes) and arg not in rm_prefixes:
+ continue
+ # Ignore this argument and the one after it
+ if arg in rm_next:
+ next(iargs)
+ continue
+ ret.append(arg)
+ return ret
+
+ def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]:
+ return []
+
+ def get_lto_link_args(self, *, threads: int = 0, mode: str = 'default',
+ thinlto_cache_dir: T.Optional[str] = None) -> T.List[str]:
+ return self.linker.get_lto_args()
+
+ def sanitizer_compile_args(self, value: str) -> T.List[str]:
+ return []
+
+ def sanitizer_link_args(self, value: str) -> T.List[str]:
+ return self.linker.sanitizer_args(value)
+
+ def get_asneeded_args(self) -> T.List[str]:
+ return self.linker.get_asneeded_args()
+
+ def headerpad_args(self) -> T.List[str]:
+ return self.linker.headerpad_args()
+
+ def bitcode_args(self) -> T.List[str]:
+ return self.linker.bitcode_args()
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ raise EnvironmentException(f'{self.id} does not implement get_buildtype_args')
+
+ def get_buildtype_linker_args(self, buildtype: str) -> T.List[str]:
+ return self.linker.get_buildtype_args(buildtype)
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str,
+ darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ return self.linker.get_soname_args(
+ env, prefix, shlib_name, suffix, soversion,
+ darwin_versions)
+
+ def get_target_link_args(self, target: 'BuildTarget') -> T.List[str]:
+ return target.link_args
+
+ def get_dependency_compile_args(self, dep: 'Dependency') -> T.List[str]:
+ return dep.get_compile_args()
+
+ def get_dependency_link_args(self, dep: 'Dependency') -> T.List[str]:
+ return dep.get_link_args()
+
+ @classmethod
+ def use_linker_args(cls, linker: str, version: str) -> T.List[str]:
+ """Get a list of arguments to pass to the compiler to set the linker.
+ """
+ return []
+
+ def get_coverage_args(self) -> T.List[str]:
+ return []
+
+ def get_coverage_link_args(self) -> T.List[str]:
+ return self.linker.get_coverage_args()
+
+ def get_disable_assert_args(self) -> T.List[str]:
+ return []
+
+ def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ raise EnvironmentException('This compiler does not support Windows CRT selection')
+
+ def get_crt_link_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ raise EnvironmentException('This compiler does not support Windows CRT selection')
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return []
+
+ def get_preprocess_only_args(self) -> T.List[str]:
+ raise EnvironmentException('This compiler does not have a preprocessor')
+
+ def get_preprocess_to_file_args(self) -> T.List[str]:
+ return self.get_preprocess_only_args()
+
+ def get_default_include_dirs(self) -> T.List[str]:
+ # TODO: This is a candidate for returning an immutable list
+ return []
+
+ def get_largefile_args(self) -> T.List[str]:
+ '''Enable transparent large-file-support for 32-bit UNIX systems'''
+ if not (self.get_argument_syntax() == 'msvc' or self.info.is_darwin()):
+ # Enable large-file support unconditionally on all platforms other
+ # than macOS and MSVC. macOS is now 64-bit-only so it doesn't
+ # need anything special, and MSVC doesn't have automatic LFS.
+ # You must use the 64-bit counterparts explicitly.
+ # glibc, musl, and uclibc, and all BSD libcs support this. On Android,
+ # support for transparent LFS is available depending on the version of
+ # Bionic: https://github.com/android/platform_bionic#32-bit-abi-bugs
+ # https://code.google.com/p/android/issues/detail?id=64613
+ #
+ # If this breaks your code, fix it! It's been 20+ years!
+ return ['-D_FILE_OFFSET_BITS=64']
+ # We don't enable -D_LARGEFILE64_SOURCE since that enables
+ # transitionary features and must be enabled by programs that use
+ # those features explicitly.
+ return []
+
+ def get_library_dirs(self, env: 'Environment',
+ elf_class: T.Optional[int] = None) -> T.List[str]:
+ return []
+
+ def get_return_value(self,
+ fname: str,
+ rtype: str,
+ prefix: str,
+ env: 'Environment',
+ extra_args: T.Optional[T.List[str]],
+ dependencies: T.Optional[T.List['Dependency']]) -> T.Union[str, int]:
+ raise EnvironmentException(f'{self.id} does not support get_return_value')
+
+ def find_framework(self,
+ name: str,
+ env: 'Environment',
+ extra_dirs: T.List[str],
+ allow_system: bool = True) -> T.Optional[T.List[str]]:
+ raise EnvironmentException(f'{self.id} does not support find_framework')
+
+ def find_framework_paths(self, env: 'Environment') -> T.List[str]:
+ raise EnvironmentException(f'{self.id} does not support find_framework_paths')
+
+ def attribute_check_func(self, name: str) -> str:
+ raise EnvironmentException(f'{self.id} does not support attribute checks')
+
+ def get_pch_suffix(self) -> str:
+ raise EnvironmentException(f'{self.id} does not support pre compiled headers')
+
+ def get_pch_name(self, name: str) -> str:
+ raise EnvironmentException(f'{self.id} does not support pre compiled headers')
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ raise EnvironmentException(f'{self.id} does not support pre compiled headers')
+
+ def get_has_func_attribute_extra_args(self, name: str) -> T.List[str]:
+ raise EnvironmentException(f'{self.id} does not support function attributes')
+
+ def name_string(self) -> str:
+ return ' '.join(self.exelist)
+
+ @abc.abstractmethod
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ """Check that this compiler actually works.
+
+ This should provide a simple compile/link test. Something as simple as:
+ ```python
+ main(): return 0
+ ```
+ is good enough here.
+ """
+
+ def split_shlib_to_parts(self, fname: str) -> T.Tuple[T.Optional[str], str]:
+ return None, fname
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return []
+
+ def get_std_exe_link_args(self) -> T.List[str]:
+ # TODO: is this a linker property?
+ return []
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ return []
+
+ def depfile_for_object(self, objfile: str) -> T.Optional[str]:
+ return objfile + '.' + self.get_depfile_suffix()
+
+ def get_depfile_suffix(self) -> str:
+ raise EnvironmentException(f'{self.id} does not implement get_depfile_suffix')
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ """Arguments to turn off default inclusion of standard libraries."""
+ return []
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ return []
+
+ def get_werror_args(self) -> T.List[str]:
+ return []
+
+ @abc.abstractmethod
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ pass
+
+ def get_module_incdir_args(self) -> T.Tuple[str, ...]:
+ raise EnvironmentException(f'{self.id} does not implement get_module_incdir_args')
+
+ def get_module_outdir_args(self, path: str) -> T.List[str]:
+ raise EnvironmentException(f'{self.id} does not implement get_module_outdir_args')
+
+ def module_name_to_filename(self, module_name: str) -> str:
+ raise EnvironmentException(f'{self.id} does not implement module_name_to_filename')
+
+ def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
+ """Arguments to pass the compiler and/or linker for checks.
+
+ The default implementation turns off optimizations.
+
+ Examples of things that go here:
+ - extra arguments for error checking
+ - Arguments required to make the compiler exit with a non-zero status
+ when something is wrong.
+ """
+ return self.get_no_optimization_args()
+
+ def get_no_optimization_args(self) -> T.List[str]:
+ """Arguments to the compiler to turn off all optimizations."""
+ return []
+
+ def build_wrapper_args(self, env: 'Environment',
+ extra_args: T.Union[None, CompilerArgs, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]],
+ dependencies: T.Optional[T.List['Dependency']],
+ mode: CompileCheckMode = CompileCheckMode.COMPILE) -> CompilerArgs:
+ """Arguments to pass the build_wrapper helper.
+
+ This generally needs to be set on a per-language baises. It provides
+ a hook for languages to handle dependencies and extra args. The base
+ implementation handles the most common cases, namely adding the
+ check_arguments, unwrapping dependencies, and appending extra args.
+ """
+ if callable(extra_args):
+ extra_args = extra_args(mode)
+ if extra_args is None:
+ extra_args = []
+ if dependencies is None:
+ dependencies = []
+
+ # Collect compiler arguments
+ args = self.compiler_args(self.get_compiler_check_args(mode))
+ for d in dependencies:
+ # Add compile flags needed by dependencies
+ args += d.get_compile_args()
+ if mode is CompileCheckMode.LINK:
+ # Add link flags needed to find dependencies
+ args += d.get_link_args()
+
+ if mode is CompileCheckMode.COMPILE:
+ # Add DFLAGS from the env
+ args += env.coredata.get_external_args(self.for_machine, self.language)
+ elif mode is CompileCheckMode.LINK:
+ # Add LDFLAGS from the env
+ args += env.coredata.get_external_link_args(self.for_machine, self.language)
+ # extra_args must override all other arguments, so we add them last
+ args += extra_args
+ return args
+
+ @contextlib.contextmanager
+ def _build_wrapper(self, code: 'mesonlib.FileOrString', env: 'Environment',
+ extra_args: T.Union[None, CompilerArgs, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None,
+ mode: str = 'compile', want_output: bool = False,
+ disable_cache: bool = False,
+ temp_dir: str = None) -> T.Iterator[T.Optional[CompileResult]]:
+ """Helper for getting a cacched value when possible.
+
+ This method isn't meant to be called externally, it's mean to be
+ wrapped by other methods like compiles() and links().
+ """
+ args = self.build_wrapper_args(env, extra_args, dependencies, CompileCheckMode(mode))
+ if disable_cache or want_output:
+ with self.compile(code, extra_args=args, mode=mode, want_output=want_output, temp_dir=env.scratch_dir) as r:
+ yield r
+ else:
+ with self.cached_compile(code, env.coredata, extra_args=args, mode=mode, temp_dir=env.scratch_dir) as r:
+ yield r
+
+ def compiles(self, code: 'mesonlib.FileOrString', env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], CompilerArgs, T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None,
+ mode: str = 'compile',
+ disable_cache: bool = False) -> T.Tuple[bool, bool]:
+ with self._build_wrapper(code, env, extra_args, dependencies, mode, disable_cache=disable_cache) as p:
+ return p.returncode == 0, p.cached
+
+ def links(self, code: 'mesonlib.FileOrString', env: 'Environment', *,
+ compiler: T.Optional['Compiler'] = None,
+ extra_args: T.Union[None, T.List[str], CompilerArgs, T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None,
+ mode: str = 'compile',
+ disable_cache: bool = False) -> T.Tuple[bool, bool]:
+ if compiler:
+ with compiler._build_wrapper(code, env, dependencies=dependencies, want_output=True) as r:
+ objfile = mesonlib.File.from_absolute_file(r.output_name)
+ return self.compiles(objfile, env, extra_args=extra_args,
+ dependencies=dependencies, mode='link', disable_cache=True)
+
+ return self.compiles(code, env, extra_args=extra_args,
+ dependencies=dependencies, mode='link', disable_cache=disable_cache)
+
+ def get_feature_args(self, kwargs: T.Dict[str, T.Any], build_to_src: str) -> T.List[str]:
+ """Used by D for extra language features."""
+ # TODO: using a TypeDict here would improve this
+ raise EnvironmentException(f'{self.id} does not implement get_feature_args')
+
+ def get_prelink_args(self, prelink_name: str, obj_list: T.List[str]) -> T.List[str]:
+ raise EnvironmentException(f'{self.id} does not know how to do prelinking.')
+
+ def rsp_file_syntax(self) -> 'RSPFileSyntax':
+ """The format of the RSP file that this compiler supports.
+
+ If `self.can_linker_accept_rsp()` returns True, then this needs to
+ be implemented
+ """
+ return self.linker.rsp_file_syntax()
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ """Arguments required for a debug build."""
+ return []
+
+ def get_no_warn_args(self) -> T.List[str]:
+ """Arguments to completely disable warnings."""
+ return []
+
+ def needs_static_linker(self) -> bool:
+ raise NotImplementedError(f'There is no static linker for {self.language}')
+
+ def get_preprocessor(self) -> Compiler:
+ """Get compiler's preprocessor.
+ """
+ raise EnvironmentException(f'{self.get_id()} does not support preprocessor')
+
+def get_global_options(lang: str,
+ comp: T.Type[Compiler],
+ for_machine: MachineChoice,
+ env: 'Environment') -> 'KeyedOptionDictType':
+ """Retrieve options that apply to all compilers for a given language."""
+ description = f'Extra arguments passed to the {lang}'
+ argkey = OptionKey('args', lang=lang, machine=for_machine)
+ largkey = argkey.evolve('link_args')
+ envkey = argkey.evolve('env_args')
+
+ comp_key = argkey if argkey in env.options else envkey
+
+ comp_options = env.options.get(comp_key, [])
+ link_options = env.options.get(largkey, [])
+
+ cargs = coredata.UserArrayOption(
+ description + ' compiler',
+ comp_options, split_args=True, user_input=True, allow_dups=True)
+
+ largs = coredata.UserArrayOption(
+ description + ' linker',
+ link_options, split_args=True, user_input=True, allow_dups=True)
+
+ if comp.INVOKES_LINKER and comp_key == envkey:
+ # If the compiler acts as a linker driver, and we're using the
+ # environment variable flags for both the compiler and linker
+ # arguments, then put the compiler flags in the linker flags as well.
+ # This is how autotools works, and the env vars freature is for
+ # autotools compatibility.
+ largs.extend_value(comp_options)
+
+ opts: 'KeyedOptionDictType' = {argkey: cargs, largkey: largs}
+
+ return opts
diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py
new file mode 100644
index 0000000..4466a1b
--- /dev/null
+++ b/mesonbuild/compilers/cpp.py
@@ -0,0 +1,890 @@
+# 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.
+from __future__ import annotations
+
+import copy
+import functools
+import os.path
+import typing as T
+
+from .. import coredata
+from .. import mlog
+from ..mesonlib import MesonException, version_compare, OptionKey
+
+from .compilers import (
+ gnu_winlibs,
+ msvc_winlibs,
+ Compiler,
+)
+from .c_function_attributes import CXX_FUNC_ATTRIBUTES, C_FUNC_ATTRIBUTES
+from .mixins.clike import CLikeCompiler
+from .mixins.ccrx import CcrxCompiler
+from .mixins.ti import TICompiler
+from .mixins.arm import ArmCompiler, ArmclangCompiler
+from .mixins.visualstudio import MSVCCompiler, ClangClCompiler
+from .mixins.gnu import GnuCompiler, gnu_common_warning_args, gnu_cpp_warning_args
+from .mixins.intel import IntelGnuLikeCompiler, IntelVisualStudioLikeCompiler
+from .mixins.clang import ClangCompiler
+from .mixins.elbrus import ElbrusCompiler
+from .mixins.pgi import PGICompiler
+from .mixins.emscripten import EmscriptenMixin
+
+if T.TYPE_CHECKING:
+ from .compilers import CompileCheckMode
+ from ..coredata import MutableKeyedOptionDictType, KeyedOptionDictType
+ from ..dependencies import Dependency
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..linkers import DynamicLinker
+ from ..mesonlib import MachineChoice
+ from ..programs import ExternalProgram
+ CompilerMixinBase = CLikeCompiler
+else:
+ CompilerMixinBase = object
+
+
+def non_msvc_eh_options(eh: str, args: T.List[str]) -> None:
+ if eh == 'none':
+ args.append('-fno-exceptions')
+ elif eh in {'s', 'c'}:
+ mlog.warning('non-MSVC compilers do not support ' + eh + ' exception handling.' +
+ 'You may want to set eh to \'default\'.')
+
+class CPPCompiler(CLikeCompiler, Compiler):
+ def attribute_check_func(self, name: str) -> str:
+ try:
+ return CXX_FUNC_ATTRIBUTES.get(name, C_FUNC_ATTRIBUTES[name])
+ except KeyError:
+ raise MesonException(f'Unknown function attribute "{name}"')
+
+ language = 'cpp'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ # If a child ObjCPP class has already set it, don't set it ourselves
+ Compiler.__init__(self, ccache, exelist, version, for_machine, info,
+ is_cross=is_cross, linker=linker,
+ full_version=full_version)
+ CLikeCompiler.__init__(self, exe_wrapper)
+
+ @staticmethod
+ def get_display_language() -> str:
+ return 'C++'
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ return ['-nostdinc++']
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ code = 'class breakCCompiler;int main(void) { return 0; }\n'
+ return self._sanity_check_impl(work_dir, environment, 'sanitycheckcpp.cc', code)
+
+ def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
+ # -fpermissive allows non-conforming code to compile which is necessary
+ # for many C++ checks. Particularly, the has_header_symbol check is
+ # too strict without this and always fails.
+ return super().get_compiler_check_args(mode) + ['-fpermissive']
+
+ def has_header_symbol(self, hname: str, symbol: str, prefix: str,
+ env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ # Check if it's a C-like symbol
+ found, cached = super().has_header_symbol(hname, symbol, prefix, env,
+ extra_args=extra_args,
+ dependencies=dependencies)
+ if found:
+ return True, cached
+ # Check if it's a class or a template
+ if extra_args is None:
+ extra_args = []
+ t = f'''{prefix}
+ #include <{hname}>
+ using {symbol};
+ int main(void) {{ return 0; }}'''
+ return self.compiles(t, env, extra_args=extra_args,
+ dependencies=dependencies)
+
+ def _test_cpp_std_arg(self, cpp_std_value: str) -> bool:
+ # Test whether the compiler understands a -std=XY argument
+ assert cpp_std_value.startswith('-std=')
+
+ # This test does not use has_multi_arguments() for two reasons:
+ # 1. has_multi_arguments() requires an env argument, which the compiler
+ # object does not have at this point.
+ # 2. even if it did have an env object, that might contain another more
+ # recent -std= argument, which might lead to a cascaded failure.
+ CPP_TEST = 'int i = static_cast<int>(0);'
+ with self.compile(CPP_TEST, extra_args=[cpp_std_value], mode='compile') as p:
+ if p.returncode == 0:
+ mlog.debug(f'Compiler accepts {cpp_std_value}:', 'YES')
+ return True
+ else:
+ mlog.debug(f'Compiler accepts {cpp_std_value}:', 'NO')
+ return False
+
+ @functools.lru_cache()
+ def _find_best_cpp_std(self, cpp_std: str) -> str:
+ # The initial version mapping approach to make falling back
+ # from '-std=c++14' to '-std=c++1y' was too brittle. For instance,
+ # Apple's Clang uses a different versioning scheme to upstream LLVM,
+ # making the whole detection logic awfully brittle. Instead, let's
+ # just see if feeding GCC or Clang our '-std=' setting works, and
+ # if not, try the fallback argument.
+ CPP_FALLBACKS = {
+ 'c++11': 'c++0x',
+ 'gnu++11': 'gnu++0x',
+ 'c++14': 'c++1y',
+ 'gnu++14': 'gnu++1y',
+ 'c++17': 'c++1z',
+ 'gnu++17': 'gnu++1z',
+ 'c++20': 'c++2a',
+ 'gnu++20': 'gnu++2a',
+ }
+
+ # Currently, remapping is only supported for Clang, Elbrus and GCC
+ assert self.id in frozenset(['clang', 'lcc', 'gcc', 'emscripten', 'armltdclang', 'intel-llvm'])
+
+ if cpp_std not in CPP_FALLBACKS:
+ # 'c++03' and 'c++98' don't have fallback types
+ return '-std=' + cpp_std
+
+ for i in (cpp_std, CPP_FALLBACKS[cpp_std]):
+ cpp_std_value = '-std=' + i
+ if self._test_cpp_std_arg(cpp_std_value):
+ return cpp_std_value
+
+ raise MesonException(f'C++ Compiler does not support -std={cpp_std}')
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = super().get_options()
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts.update({
+ key: coredata.UserComboOption(
+ 'C++ language standard to use',
+ ['none'],
+ 'none',
+ ),
+ })
+ return opts
+
+
+class ClangCPPCompiler(ClangCompiler, CPPCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ ClangCompiler.__init__(self, defines)
+ default_warn_args = ['-Wall', '-Winvalid-pch']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': ['-Weverything']}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CPPCompiler.get_options(self)
+ key = OptionKey('key', machine=self.for_machine, lang=self.language)
+ opts.update({
+ key.evolve('eh'): coredata.UserComboOption(
+ 'C++ exception handling type.',
+ ['none', 'default', 'a', 's', 'sc'],
+ 'default',
+ ),
+ key.evolve('rtti'): coredata.UserBooleanOption('Enable RTTI', True),
+ })
+ opts[key.evolve('std')].choices = [
+ 'none', 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z',
+ 'c++2a', 'c++20', 'gnu++11', 'gnu++14', 'gnu++17', 'gnu++1z',
+ 'gnu++2a', 'gnu++20',
+ ]
+ if self.info.is_windows() or self.info.is_cygwin():
+ opts.update({
+ key.evolve('winlibs'): coredata.UserArrayOption(
+ 'Standard Win libraries to link against',
+ gnu_winlibs,
+ ),
+ })
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append(self._find_best_cpp_std(std.value))
+
+ non_msvc_eh_options(options[key.evolve('eh')].value, args)
+
+ if not options[key.evolve('rtti')].value:
+ args.append('-fno-rtti')
+
+ return args
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ if self.info.is_windows() or self.info.is_cygwin():
+ # without a typedict mypy can't understand this.
+ key = OptionKey('winlibs', machine=self.for_machine, lang=self.language)
+ libs = options[key].value.copy()
+ assert isinstance(libs, list)
+ for l in libs:
+ assert isinstance(l, str)
+ return libs
+ return []
+
+ def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]:
+ # We need to apply the search prefix here, as these link arguments may
+ # be passed to a different compiler with a different set of default
+ # search paths, such as when using Clang for C/C++ and gfortran for
+ # fortran,
+ search_dirs: T.List[str] = []
+ for d in self.get_compiler_dirs(env, 'libraries'):
+ search_dirs.append(f'-L{d}')
+ return search_dirs + ['-lstdc++']
+
+
+class ArmLtdClangCPPCompiler(ClangCPPCompiler):
+
+ id = 'armltdclang'
+
+
+class AppleClangCPPCompiler(ClangCPPCompiler):
+ def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]:
+ # We need to apply the search prefix here, as these link arguments may
+ # be passed to a different compiler with a different set of default
+ # search paths, such as when using Clang for C/C++ and gfortran for
+ # fortran,
+ search_dirs: T.List[str] = []
+ for d in self.get_compiler_dirs(env, 'libraries'):
+ search_dirs.append(f'-L{d}')
+ return search_dirs + ['-lc++']
+
+
+class EmscriptenCPPCompiler(EmscriptenMixin, ClangCPPCompiler):
+
+ id = 'emscripten'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ full_version: T.Optional[str] = None):
+ if not is_cross:
+ raise MesonException('Emscripten compiler can only be used for cross compilation.')
+ if not version_compare(version, '>=1.39.19'):
+ raise MesonException('Meson requires Emscripten >= 1.39.19')
+ ClangCPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper=exe_wrapper, linker=linker,
+ defines=defines, full_version=full_version)
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append(self._find_best_cpp_std(std.value))
+ return args
+
+
+class ArmclangCPPCompiler(ArmclangCompiler, CPPCompiler):
+ '''
+ Keil armclang
+ '''
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ ArmclangCompiler.__init__(self)
+ default_warn_args = ['-Wall', '-Winvalid-pch']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': ['-Weverything']}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CPPCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts.update({
+ key.evolve('eh'): coredata.UserComboOption(
+ 'C++ exception handling type.',
+ ['none', 'default', 'a', 's', 'sc'],
+ 'default',
+ ),
+ })
+ opts[key].choices = [
+ 'none', 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'gnu++98',
+ 'gnu++03', 'gnu++11', 'gnu++14', 'gnu++17',
+ ]
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append('-std=' + std.value)
+
+ non_msvc_eh_options(options[key.evolve('eh')].value, args)
+
+ return args
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+
+class GnuCPPCompiler(GnuCompiler, CPPCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ GnuCompiler.__init__(self, defines)
+ default_warn_args = ['-Wall', '-Winvalid-pch']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': (default_warn_args + ['-Wextra', '-Wpedantic'] +
+ self.supported_warn_args(gnu_common_warning_args) +
+ self.supported_warn_args(gnu_cpp_warning_args))}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts = CPPCompiler.get_options(self)
+ opts.update({
+ key.evolve('eh'): coredata.UserComboOption(
+ 'C++ exception handling type.',
+ ['none', 'default', 'a', 's', 'sc'],
+ 'default',
+ ),
+ key.evolve('rtti'): coredata.UserBooleanOption('Enable RTTI', True),
+ key.evolve('debugstl'): coredata.UserBooleanOption(
+ 'STL debug mode',
+ False,
+ )
+ })
+ opts[key].choices = [
+ 'none', 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z',
+ 'c++2a', 'c++20', 'gnu++03', 'gnu++11', 'gnu++14', 'gnu++17',
+ 'gnu++1z', 'gnu++2a', 'gnu++20',
+ ]
+ if self.info.is_windows() or self.info.is_cygwin():
+ opts.update({
+ key.evolve('winlibs'): coredata.UserArrayOption(
+ 'Standard Win libraries to link against',
+ gnu_winlibs,
+ ),
+ })
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append(self._find_best_cpp_std(std.value))
+
+ non_msvc_eh_options(options[key.evolve('eh')].value, args)
+
+ if not options[key.evolve('rtti')].value:
+ args.append('-fno-rtti')
+
+ if options[key.evolve('debugstl')].value:
+ args.append('-D_GLIBCXX_DEBUG=1')
+ return args
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ if self.info.is_windows() or self.info.is_cygwin():
+ # without a typedict mypy can't understand this.
+ key = OptionKey('winlibs', machine=self.for_machine, lang=self.language)
+ libs = options[key].value.copy()
+ assert isinstance(libs, list)
+ for l in libs:
+ assert isinstance(l, str)
+ return libs
+ return []
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ return ['-fpch-preprocess', '-include', os.path.basename(header)]
+
+ def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]:
+ # We need to apply the search prefix here, as these link arguments may
+ # be passed to a different compiler with a different set of default
+ # search paths, such as when using Clang for C/C++ and gfortran for
+ # fortran,
+ search_dirs: T.List[str] = []
+ for d in self.get_compiler_dirs(env, 'libraries'):
+ search_dirs.append(f'-L{d}')
+ return ['-lstdc++']
+
+
+class PGICPPCompiler(PGICompiler, CPPCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ PGICompiler.__init__(self)
+
+
+class NvidiaHPC_CPPCompiler(PGICompiler, CPPCompiler):
+
+ id = 'nvidia_hpc'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ PGICompiler.__init__(self)
+
+
+class ElbrusCPPCompiler(ElbrusCompiler, CPPCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ ElbrusCompiler.__init__(self)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CPPCompiler.get_options(self)
+
+ cpp_stds = ['none', 'c++98', 'gnu++98']
+ if version_compare(self.version, '>=1.20.00'):
+ cpp_stds += ['c++03', 'c++0x', 'c++11', 'gnu++03', 'gnu++0x', 'gnu++11']
+ if version_compare(self.version, '>=1.21.00') and version_compare(self.version, '<1.22.00'):
+ cpp_stds += ['c++14', 'gnu++14', 'c++1y', 'gnu++1y']
+ if version_compare(self.version, '>=1.22.00'):
+ cpp_stds += ['c++14', 'gnu++14']
+ if version_compare(self.version, '>=1.23.00'):
+ cpp_stds += ['c++1y', 'gnu++1y']
+ if version_compare(self.version, '>=1.24.00'):
+ cpp_stds += ['c++1z', 'c++17', 'gnu++1z', 'gnu++17']
+ if version_compare(self.version, '>=1.25.00'):
+ cpp_stds += ['c++2a', 'gnu++2a']
+ if version_compare(self.version, '>=1.26.00'):
+ cpp_stds += ['c++20', 'gnu++20']
+
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts.update({
+ key.evolve('eh'): coredata.UserComboOption(
+ 'C++ exception handling type.',
+ ['none', 'default', 'a', 's', 'sc'],
+ 'default',
+ ),
+ key.evolve('debugstl'): coredata.UserBooleanOption(
+ 'STL debug mode',
+ False,
+ ),
+ })
+ opts[key].choices = cpp_stds
+ return opts
+
+ # Elbrus C++ compiler does not have lchmod, but there is only linker warning, not compiler error.
+ # So we should explicitly fail at this case.
+ def has_function(self, funcname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Optional[T.List[str]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ if funcname == 'lchmod':
+ return False, False
+ else:
+ return super().has_function(funcname, prefix, env,
+ extra_args=extra_args,
+ dependencies=dependencies)
+
+ # Elbrus C++ compiler does not support RTTI, so don't check for it.
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append(self._find_best_cpp_std(std.value))
+
+ non_msvc_eh_options(options[key.evolve('eh')].value, args)
+
+ if options[key.evolve('debugstl')].value:
+ args.append('-D_GLIBCXX_DEBUG=1')
+ return args
+
+
+class IntelCPPCompiler(IntelGnuLikeCompiler, CPPCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ IntelGnuLikeCompiler.__init__(self)
+ self.lang_header = 'c++-header'
+ default_warn_args = ['-Wall', '-w3', '-Wpch-messages']
+ self.warn_args = {'0': [],
+ '1': default_warn_args + ['-diag-disable:remark'],
+ '2': default_warn_args + ['-Wextra', '-diag-disable:remark'],
+ '3': default_warn_args + ['-Wextra', '-diag-disable:remark'],
+ 'everything': default_warn_args + ['-Wextra']}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CPPCompiler.get_options(self)
+ # Every Unix compiler under the sun seems to accept -std=c++03,
+ # with the exception of ICC. Instead of preventing the user from
+ # globally requesting C++03, we transparently remap it to C++98
+ c_stds = ['c++98', 'c++03']
+ g_stds = ['gnu++98', 'gnu++03']
+ if version_compare(self.version, '>=15.0.0'):
+ c_stds += ['c++11', 'c++14']
+ g_stds += ['gnu++11']
+ if version_compare(self.version, '>=16.0.0'):
+ c_stds += ['c++17']
+ if version_compare(self.version, '>=17.0.0'):
+ g_stds += ['gnu++14']
+ if version_compare(self.version, '>=19.1.0'):
+ c_stds += ['c++2a']
+ g_stds += ['gnu++2a']
+
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts.update({
+ key.evolve('eh'): coredata.UserComboOption(
+ 'C++ exception handling type.',
+ ['none', 'default', 'a', 's', 'sc'],
+ 'default',
+ ),
+ key.evolve('rtti'): coredata.UserBooleanOption('Enable RTTI', True),
+ key.evolve('debugstl'): coredata.UserBooleanOption('STL debug mode', False),
+ })
+ opts[key].choices = ['none'] + c_stds + g_stds
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ remap_cpp03 = {
+ 'c++03': 'c++98',
+ 'gnu++03': 'gnu++98'
+ }
+ args.append('-std=' + remap_cpp03.get(std.value, std.value))
+ if options[key.evolve('eh')].value == 'none':
+ args.append('-fno-exceptions')
+ if not options[key.evolve('rtti')].value:
+ args.append('-fno-rtti')
+ if options[key.evolve('debugstl')].value:
+ args.append('-D_GLIBCXX_DEBUG=1')
+ return args
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+
+class IntelLLVMCPPCompiler(ClangCPPCompiler):
+
+ id = 'intel-llvm'
+
+
+class VisualStudioLikeCPPCompilerMixin(CompilerMixinBase):
+
+ """Mixin for C++ specific method overrides in MSVC-like compilers."""
+
+ VC_VERSION_MAP = {
+ 'none': (True, None),
+ 'vc++11': (True, 11),
+ 'vc++14': (True, 14),
+ 'vc++17': (True, 17),
+ 'vc++20': (True, 20),
+ 'vc++latest': (True, "latest"),
+ 'c++11': (False, 11),
+ 'c++14': (False, 14),
+ 'c++17': (False, 17),
+ 'c++20': (False, 20),
+ 'c++latest': (False, "latest"),
+ }
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ # need a typeddict for this
+ key = OptionKey('winlibs', machine=self.for_machine, lang=self.language)
+ return T.cast('T.List[str]', options[key].value[:])
+
+ def _get_options_impl(self, opts: 'MutableKeyedOptionDictType', cpp_stds: T.List[str]) -> 'MutableKeyedOptionDictType':
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts.update({
+ key.evolve('eh'): coredata.UserComboOption(
+ 'C++ exception handling type.',
+ ['none', 'default', 'a', 's', 'sc'],
+ 'default',
+ ),
+ key.evolve('rtti'): coredata.UserBooleanOption('Enable RTTI', True),
+ key.evolve('winlibs'): coredata.UserArrayOption(
+ 'Windows libs to link against.',
+ msvc_winlibs,
+ ),
+ })
+ opts[key.evolve('std')].choices = cpp_stds
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+
+ eh = options[key.evolve('eh')]
+ if eh.value == 'default':
+ args.append('/EHsc')
+ elif eh.value == 'none':
+ args.append('/EHs-c-')
+ else:
+ args.append('/EH' + eh.value)
+
+ if not options[key.evolve('rtti')].value:
+ args.append('/GR-')
+
+ permissive, ver = self.VC_VERSION_MAP[options[key].value]
+
+ if ver is not None:
+ args.append(f'/std:c++{ver}')
+
+ if not permissive:
+ args.append('/permissive-')
+
+ return args
+
+ def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
+ # XXX: this is a hack because so much GnuLike stuff is in the base CPPCompiler class.
+ return Compiler.get_compiler_check_args(self, mode)
+
+
+class CPP11AsCPP14Mixin(CompilerMixinBase):
+
+ """Mixin class for VisualStudio and ClangCl to replace C++11 std with C++14.
+
+ This is a limitation of Clang and MSVC that ICL doesn't share.
+ """
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ # Note: there is no explicit flag for supporting C++11; we attempt to do the best we can
+ # which means setting the C++ standard version to C++14, in compilers that support it
+ # (i.e., after VS2015U3)
+ # if one is using anything before that point, one cannot set the standard.
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ if options[key].value in {'vc++11', 'c++11'}:
+ mlog.warning(self.id, 'does not support C++11;',
+ 'attempting best effort; setting the standard to C++14', once=True)
+ # Don't mutate anything we're going to change, we need to use
+ # deepcopy since we're messing with members, and we can't simply
+ # copy the members because the option proxy doesn't support it.
+ options = copy.deepcopy(options)
+ if options[key].value == 'vc++11':
+ options[key].value = 'vc++14'
+ else:
+ options[key].value = 'c++14'
+ return super().get_option_compile_args(options)
+
+
+class VisualStudioCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixin, MSVCCompiler, CPPCompiler):
+
+ id = 'msvc'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo', target: str,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ MSVCCompiler.__init__(self, target)
+
+ # By default, MSVC has a broken __cplusplus define that pretends to be c++98:
+ # https://docs.microsoft.com/en-us/cpp/build/reference/zc-cplusplus?view=msvc-160
+ # Pass the flag to enable a truthful define, if possible.
+ if version_compare(self.version, '>= 19.14.26428'):
+ self.always_args = self.always_args + ['/Zc:__cplusplus']
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ cpp_stds = ['none', 'c++11', 'vc++11']
+ # Visual Studio 2015 and later
+ if version_compare(self.version, '>=19'):
+ cpp_stds.extend(['c++14', 'c++latest', 'vc++latest'])
+ # Visual Studio 2017 and later
+ if version_compare(self.version, '>=19.11'):
+ cpp_stds.extend(['vc++14', 'c++17', 'vc++17'])
+ if version_compare(self.version, '>=19.29'):
+ cpp_stds.extend(['c++20', 'vc++20'])
+ return self._get_options_impl(super().get_options(), cpp_stds)
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ if options[key].value != 'none' and version_compare(self.version, '<19.00.24210'):
+ mlog.warning('This version of MSVC does not support cpp_std arguments')
+ options = copy.copy(options)
+ options[key].value = 'none'
+
+ args = super().get_option_compile_args(options)
+
+ if version_compare(self.version, '<19.11'):
+ try:
+ i = args.index('/permissive-')
+ except ValueError:
+ return args
+ del args[i]
+ return args
+
+class ClangClCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixin, ClangClCompiler, CPPCompiler):
+
+ id = 'clang-cl'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo', target: str,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, [], exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ ClangClCompiler.__init__(self, target)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ cpp_stds = ['none', 'c++11', 'vc++11', 'c++14', 'vc++14', 'c++17', 'vc++17', 'c++20', 'vc++20', 'c++latest']
+ return self._get_options_impl(super().get_options(), cpp_stds)
+
+
+class IntelClCPPCompiler(VisualStudioLikeCPPCompilerMixin, IntelVisualStudioLikeCompiler, CPPCompiler):
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo', target: str,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, [], exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ IntelVisualStudioLikeCompiler.__init__(self, target)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ # This has only been tested with version 19.0,
+ cpp_stds = ['none', 'c++11', 'vc++11', 'c++14', 'vc++14', 'c++17', 'vc++17', 'c++latest']
+ return self._get_options_impl(super().get_options(), cpp_stds)
+
+ def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
+ # XXX: this is a hack because so much GnuLike stuff is in the base CPPCompiler class.
+ return IntelVisualStudioLikeCompiler.get_compiler_check_args(self, mode)
+
+
+class IntelLLVMClCPPCompiler(IntelClCPPCompiler):
+
+ id = 'intel-llvm-cl'
+
+
+class ArmCPPCompiler(ArmCompiler, CPPCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ ArmCompiler.__init__(self)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CPPCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'c++03', 'c++11']
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value == 'c++11':
+ args.append('--cpp11')
+ elif std.value == 'c++03':
+ args.append('--cpp')
+ return args
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+ def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
+ return []
+
+
+class CcrxCPPCompiler(CcrxCompiler, CPPCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ CcrxCompiler.__init__(self)
+
+ # Override CCompiler.get_always_args
+ def get_always_args(self) -> T.List[str]:
+ return ['-nologo', '-lang=cpp']
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return []
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [f'-output=obj={target}']
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+ def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
+ return []
+
+class TICPPCompiler(TICompiler, CPPCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ TICompiler.__init__(self)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = CPPCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'c++03']
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append('--' + std.value)
+ return args
+
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+class C2000CPPCompiler(TICPPCompiler):
+ # Required for backwards compat with projects created before ti-cgt support existed
+ id = 'c2000'
diff --git a/mesonbuild/compilers/cs.py b/mesonbuild/compilers/cs.py
new file mode 100644
index 0000000..14fcfd7
--- /dev/null
+++ b/mesonbuild/compilers/cs.py
@@ -0,0 +1,154 @@
+# 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.
+from __future__ import annotations
+
+import os.path, subprocess
+import textwrap
+import typing as T
+
+from ..mesonlib import EnvironmentException
+from ..linkers import RSPFileSyntax
+
+from .compilers import Compiler, mono_buildtype_args
+from .mixins.islinker import BasicLinkerIsCompilerMixin
+
+if T.TYPE_CHECKING:
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..mesonlib import MachineChoice
+
+cs_optimization_args = {
+ 'plain': [],
+ '0': [],
+ 'g': [],
+ '1': ['-optimize+'],
+ '2': ['-optimize+'],
+ '3': ['-optimize+'],
+ 's': ['-optimize+'],
+ } # type: T.Dict[str, T.List[str]]
+
+
+class CsCompiler(BasicLinkerIsCompilerMixin, Compiler):
+
+ language = 'cs'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ info: 'MachineInfo', runner: T.Optional[str] = None):
+ super().__init__([], exelist, version, for_machine, info)
+ self.runner = runner
+
+ @classmethod
+ def get_display_language(cls) -> str:
+ return 'C sharp'
+
+ def get_always_args(self) -> T.List[str]:
+ return ['/nologo']
+
+ def get_linker_always_args(self) -> T.List[str]:
+ return ['/nologo']
+
+ def get_output_args(self, fname: str) -> T.List[str]:
+ return ['-out:' + fname]
+
+ def get_link_args(self, fname: str) -> T.List[str]:
+ return ['-r:' + fname]
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['-warnaserror']
+
+ def get_pic_args(self) -> T.List[str]:
+ return []
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-L':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+ if i[:5] == '-lib:':
+ parameter_list[idx] = i[:5] + os.path.normpath(os.path.join(build_dir, i[5:]))
+
+ return parameter_list
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ return []
+
+ def get_pch_name(self, header_name: str) -> str:
+ return ''
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ src = 'sanity.cs'
+ obj = 'sanity.exe'
+ source_name = os.path.join(work_dir, src)
+ with open(source_name, 'w', encoding='utf-8') as ofile:
+ ofile.write(textwrap.dedent('''
+ public class Sanity {
+ static public void Main () {
+ }
+ }
+ '''))
+ pc = subprocess.Popen(self.exelist + self.get_always_args() + [src], cwd=work_dir)
+ pc.wait()
+ if pc.returncode != 0:
+ raise EnvironmentException('C# compiler %s can not compile programs.' % self.name_string())
+ if self.runner:
+ cmdlist = [self.runner, obj]
+ else:
+ cmdlist = [os.path.join(work_dir, obj)]
+ pe = subprocess.Popen(cmdlist, cwd=work_dir)
+ pe.wait()
+ if pe.returncode != 0:
+ raise EnvironmentException('Executables created by Mono compiler %s are not runnable.' % self.name_string())
+
+ def needs_static_linker(self) -> bool:
+ return False
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return mono_buildtype_args[buildtype]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return ['-debug'] if is_debug else []
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return cs_optimization_args[optimization_level]
+
+
+class MonoCompiler(CsCompiler):
+
+ id = 'mono'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ info: 'MachineInfo'):
+ super().__init__(exelist, version, for_machine, info, runner='mono')
+
+ def rsp_file_syntax(self) -> 'RSPFileSyntax':
+ return RSPFileSyntax.GCC
+
+
+class VisualStudioCsCompiler(CsCompiler):
+
+ id = 'csc'
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ res = mono_buildtype_args[buildtype]
+ if not self.info.is_windows():
+ tmp = []
+ for flag in res:
+ if flag == '-debug':
+ flag = '-debug:portable'
+ tmp.append(flag)
+ res = tmp
+ return res
+
+ def rsp_file_syntax(self) -> 'RSPFileSyntax':
+ return RSPFileSyntax.MSVC
diff --git a/mesonbuild/compilers/cuda.py b/mesonbuild/compilers/cuda.py
new file mode 100644
index 0000000..d008d0c
--- /dev/null
+++ b/mesonbuild/compilers/cuda.py
@@ -0,0 +1,791 @@
+# 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.
+from __future__ import annotations
+
+import enum
+import os.path
+import string
+import typing as T
+
+from .. import coredata
+from .. import mlog
+from ..mesonlib import (
+ EnvironmentException, Popen_safe, OptionOverrideProxy,
+ is_windows, LibType, OptionKey, version_compare,
+)
+from .compilers import (Compiler, cuda_buildtype_args, cuda_optimization_args,
+ cuda_debug_args)
+
+if T.TYPE_CHECKING:
+ from .compilers import CompileCheckMode
+ from ..build import BuildTarget
+ from ..coredata import MutableKeyedOptionDictType, KeyedOptionDictType
+ from ..dependencies import Dependency
+ from ..environment import Environment # noqa: F401
+ from ..envconfig import MachineInfo
+ from ..linkers import DynamicLinker
+ from ..mesonlib import MachineChoice
+ from ..programs import ExternalProgram
+
+
+class _Phase(enum.Enum):
+
+ COMPILER = 'compiler'
+ LINKER = 'linker'
+
+
+class CudaCompiler(Compiler):
+
+ LINKER_PREFIX = '-Xlinker='
+ language = 'cuda'
+
+ # NVCC flags taking no arguments.
+ _FLAG_PASSTHRU_NOARGS = {
+ # NVCC --long-option, NVCC -short-option CUDA Toolkit 11.2.1 Reference
+ '--objdir-as-tempdir', '-objtemp', # 4.2.1.2
+ '--generate-dependency-targets', '-MP', # 4.2.1.12
+ '--allow-unsupported-compiler', '-allow-unsupported-compiler', # 4.2.1.14
+ '--link', # 4.2.2.1
+ '--lib', '-lib', # 4.2.2.2
+ '--device-link', '-dlink', # 4.2.2.3
+ '--device-c', '-dc', # 4.2.2.4
+ '--device-w', '-dw', # 4.2.2.5
+ '--cuda', '-cuda', # 4.2.2.6
+ '--compile', '-c', # 4.2.2.7
+ '--fatbin', '-fatbin', # 4.2.2.8
+ '--cubin', '-cubin', # 4.2.2.9
+ '--ptx', '-ptx', # 4.2.2.10
+ '--preprocess', '-E', # 4.2.2.11
+ '--generate-dependencies', '-M', # 4.2.2.12
+ '--generate-nonsystem-dependencies', '-MM', # 4.2.2.13
+ '--generate-dependencies-with-compile', '-MD', # 4.2.2.14
+ '--generate-nonsystem-dependencies-with-compile', '-MMD', # 4.2.2.15
+ '--run', # 4.2.2.16
+ '--profile', '-pg', # 4.2.3.1
+ '--debug', '-g', # 4.2.3.2
+ '--device-debug', '-G', # 4.2.3.3
+ '--extensible-whole-program', '-ewp', # 4.2.3.4
+ '--generate-line-info', '-lineinfo', # 4.2.3.5
+ '--dlink-time-opt', '-dlto', # 4.2.3.8
+ '--no-exceptions', '-noeh', # 4.2.3.11
+ '--shared', '-shared', # 4.2.3.12
+ '--no-host-device-initializer-list', '-nohdinitlist', # 4.2.3.15
+ '--expt-relaxed-constexpr', '-expt-relaxed-constexpr', # 4.2.3.16
+ '--extended-lambda', '-extended-lambda', # 4.2.3.17
+ '--expt-extended-lambda', '-expt-extended-lambda', # 4.2.3.18
+ '--m32', '-m32', # 4.2.3.20
+ '--m64', '-m64', # 4.2.3.21
+ '--forward-unknown-to-host-compiler', '-forward-unknown-to-host-compiler', # 4.2.5.1
+ '--forward-unknown-to-host-linker', '-forward-unknown-to-host-linker', # 4.2.5.2
+ '--dont-use-profile', '-noprof', # 4.2.5.3
+ '--dryrun', '-dryrun', # 4.2.5.5
+ '--verbose', '-v', # 4.2.5.6
+ '--keep', '-keep', # 4.2.5.7
+ '--save-temps', '-save-temps', # 4.2.5.9
+ '--clean-targets', '-clean', # 4.2.5.10
+ '--no-align-double', # 4.2.5.16
+ '--no-device-link', '-nodlink', # 4.2.5.17
+ '--allow-unsupported-compiler', '-allow-unsupported-compiler', # 4.2.5.18
+ '--use_fast_math', '-use_fast_math', # 4.2.7.7
+ '--extra-device-vectorization', '-extra-device-vectorization', # 4.2.7.12
+ '--compile-as-tools-patch', '-astoolspatch', # 4.2.7.13
+ '--keep-device-functions', '-keep-device-functions', # 4.2.7.14
+ '--disable-warnings', '-w', # 4.2.8.1
+ '--source-in-ptx', '-src-in-ptx', # 4.2.8.2
+ '--restrict', '-restrict', # 4.2.8.3
+ '--Wno-deprecated-gpu-targets', '-Wno-deprecated-gpu-targets', # 4.2.8.4
+ '--Wno-deprecated-declarations', '-Wno-deprecated-declarations', # 4.2.8.5
+ '--Wreorder', '-Wreorder', # 4.2.8.6
+ '--Wdefault-stream-launch', '-Wdefault-stream-launch', # 4.2.8.7
+ '--Wext-lambda-captures-this', '-Wext-lambda-captures-this', # 4.2.8.8
+ '--display-error-number', '-err-no', # 4.2.8.10
+ '--resource-usage', '-res-usage', # 4.2.8.14
+ '--help', '-h', # 4.2.8.15
+ '--version', '-V', # 4.2.8.16
+ '--list-gpu-code', '-code-ls', # 4.2.8.20
+ '--list-gpu-arch', '-arch-ls', # 4.2.8.21
+ }
+ # Dictionary of NVCC flags taking either one argument or a comma-separated list.
+ # Maps --long to -short options, because the short options are more GCC-like.
+ _FLAG_LONG2SHORT_WITHARGS = {
+ '--output-file': '-o', # 4.2.1.1
+ '--pre-include': '-include', # 4.2.1.3
+ '--library': '-l', # 4.2.1.4
+ '--define-macro': '-D', # 4.2.1.5
+ '--undefine-macro': '-U', # 4.2.1.6
+ '--include-path': '-I', # 4.2.1.7
+ '--system-include': '-isystem', # 4.2.1.8
+ '--library-path': '-L', # 4.2.1.9
+ '--output-directory': '-odir', # 4.2.1.10
+ '--dependency-output': '-MF', # 4.2.1.11
+ '--compiler-bindir': '-ccbin', # 4.2.1.13
+ '--archiver-binary': '-arbin', # 4.2.1.15
+ '--cudart': '-cudart', # 4.2.1.16
+ '--cudadevrt': '-cudadevrt', # 4.2.1.17
+ '--libdevice-directory': '-ldir', # 4.2.1.18
+ '--target-directory': '-target-dir', # 4.2.1.19
+ '--optimization-info': '-opt-info', # 4.2.3.6
+ '--optimize': '-O', # 4.2.3.7
+ '--ftemplate-backtrace-limit': '-ftemplate-backtrace-limit', # 4.2.3.9
+ '--ftemplate-depth': '-ftemplate-depth', # 4.2.3.10
+ '--x': '-x', # 4.2.3.13
+ '--std': '-std', # 4.2.3.14
+ '--machine': '-m', # 4.2.3.19
+ '--compiler-options': '-Xcompiler', # 4.2.4.1
+ '--linker-options': '-Xlinker', # 4.2.4.2
+ '--archive-options': '-Xarchive', # 4.2.4.3
+ '--ptxas-options': '-Xptxas', # 4.2.4.4
+ '--nvlink-options': '-Xnvlink', # 4.2.4.5
+ '--threads': '-t', # 4.2.5.4
+ '--keep-dir': '-keep-dir', # 4.2.5.8
+ '--run-args': '-run-args', # 4.2.5.11
+ '--input-drive-prefix': '-idp', # 4.2.5.12
+ '--dependency-drive-prefix': '-ddp', # 4.2.5.13
+ '--drive-prefix': '-dp', # 4.2.5.14
+ '--dependency-target-name': '-MT', # 4.2.5.15
+ '--default-stream': '-default-stream', # 4.2.6.1
+ '--gpu-architecture': '-arch', # 4.2.7.1
+ '--gpu-code': '-code', # 4.2.7.2
+ '--generate-code': '-gencode', # 4.2.7.3
+ '--relocatable-device-code': '-rdc', # 4.2.7.4
+ '--entries': '-e', # 4.2.7.5
+ '--maxrregcount': '-maxrregcount', # 4.2.7.6
+ '--ftz': '-ftz', # 4.2.7.8
+ '--prec-div': '-prec-div', # 4.2.7.9
+ '--prec-sqrt': '-prec-sqrt', # 4.2.7.10
+ '--fmad': '-fmad', # 4.2.7.11
+ '--Werror': '-Werror', # 4.2.8.9
+ '--diag-error': '-diag-error', # 4.2.8.11
+ '--diag-suppress': '-diag-suppress', # 4.2.8.12
+ '--diag-warn': '-diag-warn', # 4.2.8.13
+ '--options-file': '-optf', # 4.2.8.17
+ '--time': '-time', # 4.2.8.18
+ '--qpp-config': '-qpp-config', # 4.2.8.19
+ }
+ # Reverse map -short to --long options.
+ _FLAG_SHORT2LONG_WITHARGS = {v: k for k, v in _FLAG_LONG2SHORT_WITHARGS.items()}
+
+ id = 'nvcc'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, exe_wrapper: T.Optional['ExternalProgram'],
+ host_compiler: Compiler, info: 'MachineInfo',
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ super().__init__(ccache, exelist, version, for_machine, info, linker=linker, full_version=full_version, is_cross=is_cross)
+ self.exe_wrapper = exe_wrapper
+ self.host_compiler = host_compiler
+ self.base_options = host_compiler.base_options
+ self.warn_args = {level: self._to_host_flags(flags) for level, flags in host_compiler.warn_args.items()}
+
+ @classmethod
+ def _shield_nvcc_list_arg(cls, arg: str, listmode: bool = True) -> str:
+ r"""
+ Shield an argument against both splitting by NVCC's list-argument
+ parse logic, and interpretation by any shell.
+
+ NVCC seems to consider every comma , that is neither escaped by \ nor inside
+ a double-quoted string a split-point. Single-quotes do not provide protection
+ against splitting; In fact, after splitting they are \-escaped. Unfortunately,
+ double-quotes don't protect against shell expansion. What follows is a
+ complex dance to accommodate everybody.
+ """
+
+ SQ = "'"
+ DQ = '"'
+ CM = ","
+ BS = "\\"
+ DQSQ = DQ+SQ+DQ
+ quotable = set(string.whitespace+'"$`\\')
+
+ if CM not in arg or not listmode:
+ if SQ not in arg:
+ # If any of the special characters "$`\ or whitespace are present, single-quote.
+ # Otherwise return bare.
+ if set(arg).intersection(quotable):
+ return SQ+arg+SQ
+ else:
+ return arg # Easy case: no splits, no quoting.
+ else:
+ # There are single quotes. Double-quote them, and single-quote the
+ # strings between them.
+ l = [cls._shield_nvcc_list_arg(s) for s in arg.split(SQ)]
+ l = sum([[s, DQSQ] for s in l][:-1], []) # Interleave l with DQSQs
+ return ''.join(l)
+ else:
+ # A comma is present, and list mode was active.
+ # We apply (what we guess is) the (primitive) NVCC splitting rule:
+ l = ['']
+ instring = False
+ argit = iter(arg)
+ for c in argit:
+ if c == CM and not instring:
+ l.append('')
+ elif c == DQ:
+ l[-1] += c
+ instring = not instring
+ elif c == BS:
+ try:
+ l[-1] += next(argit)
+ except StopIteration:
+ break
+ else:
+ l[-1] += c
+
+ # Shield individual strings, without listmode, then return them with
+ # escaped commas between them.
+ l = [cls._shield_nvcc_list_arg(s, listmode=False) for s in l]
+ return r'\,'.join(l)
+
+ @classmethod
+ def _merge_flags(cls, flags: T.List[str]) -> T.List[str]:
+ r"""
+ The flags to NVCC gets exceedingly verbose and unreadable when too many of them
+ are shielded with -Xcompiler. Merge consecutive -Xcompiler-wrapped arguments
+ into one.
+ """
+ if len(flags) <= 1:
+ return flags
+ flagit = iter(flags)
+ xflags = []
+
+ def is_xcompiler_flag_isolated(flag: str) -> bool:
+ return flag == '-Xcompiler'
+
+ def is_xcompiler_flag_glued(flag: str) -> bool:
+ return flag.startswith('-Xcompiler=')
+
+ def is_xcompiler_flag(flag: str) -> bool:
+ return is_xcompiler_flag_isolated(flag) or is_xcompiler_flag_glued(flag)
+
+ def get_xcompiler_val(flag: str, flagit: T.Iterator[str]) -> str:
+ if is_xcompiler_flag_glued(flag):
+ return flag[len('-Xcompiler='):]
+ else:
+ try:
+ return next(flagit)
+ except StopIteration:
+ return ""
+
+ ingroup = False
+ for flag in flagit:
+ if not is_xcompiler_flag(flag):
+ ingroup = False
+ xflags.append(flag)
+ elif ingroup:
+ xflags[-1] += ','
+ xflags[-1] += get_xcompiler_val(flag, flagit)
+ elif is_xcompiler_flag_isolated(flag):
+ ingroup = True
+ xflags.append(flag)
+ xflags.append(get_xcompiler_val(flag, flagit))
+ elif is_xcompiler_flag_glued(flag):
+ ingroup = True
+ xflags.append(flag)
+ else:
+ raise ValueError("-Xcompiler flag merging failed, unknown argument form!")
+ return xflags
+
+ def _to_host_flags(self, flags: T.List[str], phase: _Phase = _Phase.COMPILER) -> T.List[str]:
+ """
+ Translate generic "GCC-speak" plus particular "NVCC-speak" flags to NVCC flags.
+
+ NVCC's "short" flags have broad similarities to the GCC standard, but have
+ gratuitous, irritating differences.
+ """
+
+ xflags = []
+ flagit = iter(flags)
+
+ for flag in flagit:
+ # The CUDA Toolkit Documentation, in 4.1. Command Option Types and Notation,
+ # specifies that NVCC does not parse the standard flags as GCC does. It has
+ # its own strategy, to wit:
+ #
+ # nvcc recognizes three types of command options: boolean options, single
+ # value options, and list options.
+ #
+ # Boolean options do not have an argument; they are either specified on a
+ # command line or not. Single value options must be specified at most once,
+ # and list options may be repeated. Examples of each of these option types
+ # are, respectively: --verbose (switch to verbose mode), --output-file
+ # (specify output file), and --include-path (specify include path).
+ #
+ # Single value options and list options must have arguments, which must
+ # follow the name of the option itself by either one of more spaces or an
+ # equals character. When a one-character short name such as -I, -l, and -L
+ # is used, the value of the option may also immediately follow the option
+ # itself without being separated by spaces or an equal character. The
+ # individual values of list options may be separated by commas in a single
+ # instance of the option, or the option may be repeated, or any
+ # combination of these two cases.
+ #
+ # One strange consequence of this choice is that directory and filenames that
+ # contain commas (',') cannot be passed to NVCC (at least, not as easily as
+ # in GCC). Another strange consequence is that it is legal to supply flags
+ # such as
+ #
+ # -lpthread,rt,dl,util
+ # -l pthread,rt,dl,util
+ # -l=pthread,rt,dl,util
+ #
+ # and each of the above alternatives is equivalent to GCC-speak
+ #
+ # -lpthread -lrt -ldl -lutil
+ # -l pthread -l rt -l dl -l util
+ # -l=pthread -l=rt -l=dl -l=util
+ #
+ # *With the exception of commas in the name*, GCC-speak for these list flags
+ # is a strict subset of NVCC-speak, so we passthrough those flags.
+ #
+ # The -D macro-define flag is documented as somehow shielding commas from
+ # splitting a definition. Balanced parentheses, braces and single-quotes
+ # around the comma are not sufficient, but balanced double-quotes are. The
+ # shielding appears to work with -l, -I, -L flags as well, for instance.
+ #
+ # Since our goal is to replicate GCC-speak as much as possible, we check for
+ # commas in all list-arguments and shield them with double-quotes. We make
+ # an exception for -D (where this would be value-changing) and -U (because
+ # it isn't possible to define a macro with a comma in the name).
+
+ if flag in self._FLAG_PASSTHRU_NOARGS:
+ xflags.append(flag)
+ continue
+
+ # Handle breakup of flag-values into a flag-part and value-part.
+ if flag[:1] not in '-/':
+ # This is not a flag. It's probably a file input. Pass it through.
+ xflags.append(flag)
+ continue
+ elif flag[:1] == '/':
+ # This is ambiguously either an MVSC-style /switch or an absolute path
+ # to a file. For some magical reason the following works acceptably in
+ # both cases.
+ wrap = '"' if ',' in flag else ''
+ xflags.append(f'-X{phase.value}={wrap}{flag}{wrap}')
+ continue
+ elif len(flag) >= 2 and flag[0] == '-' and flag[1] in 'IDULlmOxmte':
+ # This is a single-letter short option. These options (with the
+ # exception of -o) are allowed to receive their argument with neither
+ # space nor = sign before them. Detect and separate them in that event.
+ if flag[2:3] == '': # -I something
+ try:
+ val = next(flagit)
+ except StopIteration:
+ pass
+ elif flag[2:3] == '=': # -I=something
+ val = flag[3:]
+ else: # -Isomething
+ val = flag[2:]
+ flag = flag[:2] # -I
+ elif flag in self._FLAG_LONG2SHORT_WITHARGS or \
+ flag in self._FLAG_SHORT2LONG_WITHARGS:
+ # This is either -o or a multi-letter flag, and it is receiving its
+ # value isolated.
+ try:
+ val = next(flagit) # -o something
+ except StopIteration:
+ pass
+ elif flag.split('=', 1)[0] in self._FLAG_LONG2SHORT_WITHARGS or \
+ flag.split('=', 1)[0] in self._FLAG_SHORT2LONG_WITHARGS:
+ # This is either -o or a multi-letter flag, and it is receiving its
+ # value after an = sign.
+ flag, val = flag.split('=', 1) # -o=something
+ # Some dependencies (e.g., BoostDependency) add unspaced "-isystem/usr/include" arguments
+ elif flag.startswith('-isystem'):
+ val = flag[8:].strip()
+ flag = flag[:8]
+ else:
+ # This is a flag, and it's foreign to NVCC.
+ #
+ # We do not know whether this GCC-speak flag takes an isolated
+ # argument. Assuming it does not (the vast majority indeed don't),
+ # wrap this argument in an -Xcompiler flag and send it down to NVCC.
+ if flag == '-ffast-math':
+ xflags.append('-use_fast_math')
+ xflags.append('-Xcompiler='+flag)
+ elif flag == '-fno-fast-math':
+ xflags.append('-ftz=false')
+ xflags.append('-prec-div=true')
+ xflags.append('-prec-sqrt=true')
+ xflags.append('-Xcompiler='+flag)
+ elif flag == '-freciprocal-math':
+ xflags.append('-prec-div=false')
+ xflags.append('-Xcompiler='+flag)
+ elif flag == '-fno-reciprocal-math':
+ xflags.append('-prec-div=true')
+ xflags.append('-Xcompiler='+flag)
+ else:
+ xflags.append('-Xcompiler='+self._shield_nvcc_list_arg(flag))
+ # The above should securely handle GCC's -Wl, -Wa, -Wp, arguments.
+ continue
+
+ assert val is not None # Should only trip if there is a missing argument.
+
+ # Take care of the various NVCC-supported flags that need special handling.
+ flag = self._FLAG_LONG2SHORT_WITHARGS.get(flag, flag)
+
+ if flag in {'-include', '-isystem', '-I', '-L', '-l'}:
+ # These flags are known to GCC, but list-valued in NVCC. They potentially
+ # require double-quoting to prevent NVCC interpreting the flags as lists
+ # when GCC would not have done so.
+ #
+ # We avoid doing this quoting for -D to avoid redefining macros and for
+ # -U because it isn't possible to define a macro with a comma in the name.
+ # -U with comma arguments is impossible in GCC-speak (and thus unambiguous
+ #in NVCC-speak, albeit unportable).
+ if len(flag) == 2:
+ xflags.append(flag+self._shield_nvcc_list_arg(val))
+ elif flag == '-isystem' and val in self.host_compiler.get_default_include_dirs():
+ # like GnuLikeCompiler, we have to filter out include directories specified
+ # with -isystem that overlap with the host compiler's search path
+ pass
+ else:
+ xflags.append(flag)
+ xflags.append(self._shield_nvcc_list_arg(val))
+ elif flag == '-O':
+ # Handle optimization levels GCC knows about that NVCC does not.
+ if val == 'fast':
+ xflags.append('-O3')
+ xflags.append('-use_fast_math')
+ xflags.append('-Xcompiler')
+ xflags.append(flag+val)
+ elif val in {'s', 'g', 'z'}:
+ xflags.append('-Xcompiler')
+ xflags.append(flag+val)
+ else:
+ xflags.append(flag+val)
+ elif flag in {'-D', '-U', '-m', '-t'}:
+ xflags.append(flag+val) # For style, keep glued.
+ elif flag in {'-std'}:
+ xflags.append(flag+'='+val) # For style, keep glued.
+ else:
+ xflags.append(flag)
+ xflags.append(val)
+
+ return self._merge_flags(xflags)
+
+ def needs_static_linker(self) -> bool:
+ return False
+
+ def thread_link_flags(self, environment: 'Environment') -> T.List[str]:
+ return self._to_host_flags(self.host_compiler.thread_link_flags(environment), _Phase.LINKER)
+
+ def sanity_check(self, work_dir: str, env: 'Environment') -> None:
+ mlog.debug('Sanity testing ' + self.get_display_language() + ' compiler:', ' '.join(self.exelist))
+ mlog.debug('Is cross compiler: %s.' % str(self.is_cross))
+
+ sname = 'sanitycheckcuda.cu'
+ code = r'''
+ #include <cuda_runtime.h>
+ #include <stdio.h>
+
+ __global__ void kernel (void) {}
+
+ int main(void){
+ struct cudaDeviceProp prop;
+ int count, i;
+ cudaError_t ret = cudaGetDeviceCount(&count);
+ if(ret != cudaSuccess){
+ fprintf(stderr, "%d\n", (int)ret);
+ }else{
+ for(i=0;i<count;i++){
+ if(cudaGetDeviceProperties(&prop, i) == cudaSuccess){
+ fprintf(stdout, "%d.%d\n", prop.major, prop.minor);
+ }
+ }
+ }
+ fflush(stderr);
+ fflush(stdout);
+ return 0;
+ }
+ '''
+ binname = sname.rsplit('.', 1)[0]
+ binname += '_cross' if self.is_cross else ''
+ source_name = os.path.join(work_dir, sname)
+ binary_name = os.path.join(work_dir, binname + '.exe')
+ with open(source_name, 'w', encoding='utf-8') as ofile:
+ ofile.write(code)
+
+ # The Sanity Test for CUDA language will serve as both a sanity test
+ # and a native-build GPU architecture detection test, useful later.
+ #
+ # For this second purpose, NVCC has very handy flags, --run and
+ # --run-args, that allow one to run an application with the
+ # environment set up properly. Of course, this only works for native
+ # builds; For cross builds we must still use the exe_wrapper (if any).
+ self.detected_cc = ''
+ flags = []
+
+ # Disable warnings, compile with statically-linked runtime for minimum
+ # reliance on the system.
+ flags += ['-w', '-cudart', 'static', source_name]
+
+ # Use the -ccbin option, if available, even during sanity checking.
+ # Otherwise, on systems where CUDA does not support the default compiler,
+ # NVCC becomes unusable.
+ flags += self.get_ccbin_args(env.coredata.options)
+
+ # If cross-compiling, we can't run the sanity check, only compile it.
+ if self.is_cross and self.exe_wrapper is None:
+ # Linking cross built apps is painful. You can't really
+ # tell if you should use -nostdlib or not and for example
+ # on OSX the compiler binary is the same but you need
+ # a ton of compiler flags to differentiate between
+ # arm and x86_64. So just compile.
+ flags += self.get_compile_only_args()
+ flags += self.get_output_args(binary_name)
+
+ # Compile sanity check
+ cmdlist = self.exelist + flags
+ mlog.debug('Sanity check compiler command line: ', ' '.join(cmdlist))
+ pc, stdo, stde = Popen_safe(cmdlist, cwd=work_dir)
+ mlog.debug('Sanity check compile stdout: ')
+ mlog.debug(stdo)
+ mlog.debug('-----\nSanity check compile stderr:')
+ mlog.debug(stde)
+ mlog.debug('-----')
+ if pc.returncode != 0:
+ raise EnvironmentException(f'Compiler {self.name_string()} can not compile programs.')
+
+ # Run sanity check (if possible)
+ if self.is_cross:
+ if self.exe_wrapper is None:
+ return
+ else:
+ cmdlist = self.exe_wrapper.get_command() + [binary_name]
+ else:
+ cmdlist = self.exelist + ['--run', '"' + binary_name + '"']
+ mlog.debug('Sanity check run command line: ', ' '.join(cmdlist))
+ pe, stdo, stde = Popen_safe(cmdlist, cwd=work_dir)
+ mlog.debug('Sanity check run stdout: ')
+ mlog.debug(stdo)
+ mlog.debug('-----\nSanity check run stderr:')
+ mlog.debug(stde)
+ mlog.debug('-----')
+ pe.wait()
+ if pe.returncode != 0:
+ raise EnvironmentException(f'Executables created by {self.language} compiler {self.name_string()} are not runnable.')
+
+ # Interpret the result of the sanity test.
+ # As mentioned above, it is not only a sanity test but also a GPU
+ # architecture detection test.
+ if stde == '':
+ self.detected_cc = stdo
+ else:
+ mlog.debug('cudaGetDeviceCount() returned ' + stde)
+
+ def has_header_symbol(self, hname: str, symbol: str, prefix: str,
+ env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ if extra_args is None:
+ extra_args = []
+ fargs = {'prefix': prefix, 'header': hname, 'symbol': symbol}
+ # Check if it's a C-like symbol
+ t = '''{prefix}
+ #include <{header}>
+ int main(void) {{
+ /* If it's not defined as a macro, try to use as a symbol */
+ #ifndef {symbol}
+ {symbol};
+ #endif
+ return 0;
+ }}'''
+ found, cached = self.compiles(t.format_map(fargs), env, extra_args=extra_args, dependencies=dependencies)
+ if found:
+ return True, cached
+ # Check if it's a class or a template
+ t = '''{prefix}
+ #include <{header}>
+ using {symbol};
+ int main(void) {{
+ return 0;
+ }}'''
+ return self.compiles(t.format_map(fargs), env, extra_args=extra_args, dependencies=dependencies)
+
+ _CPP14_VERSION = '>=9.0'
+ _CPP17_VERSION = '>=11.0'
+ _CPP20_VERSION = '>=12.0'
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = super().get_options()
+ std_key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ ccbindir_key = OptionKey('ccbindir', machine=self.for_machine, lang=self.language)
+
+ cpp_stds = ['none', 'c++03', 'c++11']
+ if version_compare(self.version, self._CPP14_VERSION):
+ cpp_stds += ['c++14']
+ if version_compare(self.version, self._CPP17_VERSION):
+ cpp_stds += ['c++17']
+ if version_compare(self.version, self._CPP20_VERSION):
+ cpp_stds += ['c++20']
+
+ opts.update({
+ std_key: coredata.UserComboOption('C++ language standard to use with CUDA',
+ cpp_stds, 'none'),
+ ccbindir_key: coredata.UserStringOption('CUDA non-default toolchain directory to use (-ccbin)',
+ ''),
+ })
+ return opts
+
+ def _to_host_compiler_options(self, options: 'KeyedOptionDictType') -> 'KeyedOptionDictType':
+ """
+ Convert an NVCC Option set to a host compiler's option set.
+ """
+
+ # We must strip the -std option from the host compiler option set, as NVCC has
+ # its own -std flag that may not agree with the host compiler's.
+ host_options = {key: options.get(key, opt) for key, opt in self.host_compiler.get_options().items()}
+ std_key = OptionKey('std', machine=self.for_machine, lang=self.host_compiler.language)
+ overrides = {std_key: 'none'}
+ return OptionOverrideProxy(overrides, host_options)
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = self.get_ccbin_args(options)
+ # On Windows, the version of the C++ standard used by nvcc is dictated by
+ # the combination of CUDA version and MSVC version; the --std= is thus ignored
+ # and attempting to use it will result in a warning: https://stackoverflow.com/a/51272091/741027
+ if not is_windows():
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append('--std=' + std.value)
+
+ return args + self._to_host_flags(self.host_compiler.get_option_compile_args(self._to_host_compiler_options(options)))
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = self.get_ccbin_args(options)
+ return args + self._to_host_flags(self.host_compiler.get_option_link_args(self._to_host_compiler_options(options)), _Phase.LINKER)
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str,
+ darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ return self._to_host_flags(self.host_compiler.get_soname_args(
+ env, prefix, shlib_name, suffix, soversion, darwin_versions), _Phase.LINKER)
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return ['-c']
+
+ def get_no_optimization_args(self) -> T.List[str]:
+ return ['-O0']
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ # alternatively, consider simply redirecting this to the host compiler, which would
+ # give us more control over options like "optimize for space" (which nvcc doesn't support):
+ # return self._to_host_flags(self.host_compiler.get_optimization_args(optimization_level))
+ return cuda_optimization_args[optimization_level]
+
+ def sanitizer_compile_args(self, value: str) -> T.List[str]:
+ return self._to_host_flags(self.host_compiler.sanitizer_compile_args(value))
+
+ def sanitizer_link_args(self, value: str) -> T.List[str]:
+ return self._to_host_flags(self.host_compiler.sanitizer_link_args(value))
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return cuda_debug_args[is_debug]
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['-Werror=cross-execution-space-call,deprecated-declarations,reorder']
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ return self.warn_args[level]
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ # nvcc doesn't support msvc's "Edit and Continue" PDB format; "downgrade" to
+ # a regular PDB to avoid cl's warning to that effect (D9025 : overriding '/ZI' with '/Zi')
+ host_args = ['/Zi' if arg == '/ZI' else arg for arg in self.host_compiler.get_buildtype_args(buildtype)]
+ return cuda_buildtype_args[buildtype] + self._to_host_flags(host_args)
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if path == '':
+ path = '.'
+ return ['-isystem=' + path] if is_system else ['-I' + path]
+
+ def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]:
+ return self._to_host_flags(self.host_compiler.get_compile_debugfile_args(rel_obj, pch))
+
+ def get_link_debugfile_args(self, targetfile: str) -> T.List[str]:
+ return self._to_host_flags(self.host_compiler.get_link_debugfile_args(targetfile), _Phase.LINKER)
+
+ def get_depfile_suffix(self) -> str:
+ return 'd'
+
+ def get_buildtype_linker_args(self, buildtype: str) -> T.List[str]:
+ return self._to_host_flags(self.host_compiler.get_buildtype_linker_args(buildtype), _Phase.LINKER)
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ (rpath_args, rpath_dirs_to_remove) = self.host_compiler.build_rpath_args(
+ env, build_dir, from_dir, rpath_paths, build_rpath, install_rpath)
+ return (self._to_host_flags(rpath_args, _Phase.LINKER), rpath_dirs_to_remove)
+
+ def linker_to_compiler_args(self, args: T.List[str]) -> T.List[str]:
+ return args
+
+ def get_pic_args(self) -> T.List[str]:
+ return self._to_host_flags(self.host_compiler.get_pic_args())
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ return []
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return ['-o', target]
+
+ def get_std_exe_link_args(self) -> T.List[str]:
+ return self._to_host_flags(self.host_compiler.get_std_exe_link_args(), _Phase.LINKER)
+
+ def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str],
+ libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]:
+ return ['-l' + libname] # FIXME
+
+ def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ return self._to_host_flags(self.host_compiler.get_crt_compile_args(crt_val, buildtype))
+
+ def get_crt_link_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ # nvcc defaults to static, release version of msvc runtime and provides no
+ # native option to override it; override it with /NODEFAULTLIB
+ host_link_arg_overrides = []
+ host_crt_compile_args = self.host_compiler.get_crt_compile_args(crt_val, buildtype)
+ if any(arg in {'/MDd', '/MD', '/MTd'} for arg in host_crt_compile_args):
+ host_link_arg_overrides += ['/NODEFAULTLIB:LIBCMT.lib']
+ return self._to_host_flags(host_link_arg_overrides + self.host_compiler.get_crt_link_args(crt_val, buildtype), _Phase.LINKER)
+
+ def get_target_link_args(self, target: 'BuildTarget') -> T.List[str]:
+ return self._to_host_flags(super().get_target_link_args(target), _Phase.LINKER)
+
+ def get_dependency_compile_args(self, dep: 'Dependency') -> T.List[str]:
+ return self._to_host_flags(super().get_dependency_compile_args(dep))
+
+ def get_dependency_link_args(self, dep: 'Dependency') -> T.List[str]:
+ return self._to_host_flags(super().get_dependency_link_args(dep), _Phase.LINKER)
+
+ def get_ccbin_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ key = OptionKey('ccbindir', machine=self.for_machine, lang=self.language)
+ ccbindir = options[key].value
+ if isinstance(ccbindir, str) and ccbindir != '':
+ return [self._shield_nvcc_list_arg('-ccbin='+ccbindir, False)]
+ else:
+ return []
+
+ def get_profile_generate_args(self) -> T.List[str]:
+ return ['-Xcompiler=' + x for x in self.host_compiler.get_profile_generate_args()]
+
+ def get_profile_use_args(self) -> T.List[str]:
+ return ['-Xcompiler=' + x for x in self.host_compiler.get_profile_use_args()]
+
+ def get_disable_assert_args(self) -> T.List[str]:
+ return self.host_compiler.get_disable_assert_args()
diff --git a/mesonbuild/compilers/cython.py b/mesonbuild/compilers/cython.py
new file mode 100644
index 0000000..9bbfebe
--- /dev/null
+++ b/mesonbuild/compilers/cython.py
@@ -0,0 +1,96 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright © 2021 Intel Corporation
+from __future__ import annotations
+
+"""Abstraction for Cython language compilers."""
+
+import typing as T
+
+from .. import coredata
+from ..mesonlib import EnvironmentException, OptionKey, version_compare
+from .compilers import Compiler
+
+if T.TYPE_CHECKING:
+ from ..coredata import MutableKeyedOptionDictType, KeyedOptionDictType
+ from ..environment import Environment
+
+
+class CythonCompiler(Compiler):
+
+ """Cython Compiler."""
+
+ language = 'cython'
+ id = 'cython'
+
+ def needs_static_linker(self) -> bool:
+ # We transpile into C, so we don't need any linker
+ return False
+
+ def get_always_args(self) -> T.List[str]:
+ return ['--fast-fail']
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['-Werror']
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return ['-o', outputname]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ # Cython doesn't have optimization levels itself, the underlying
+ # compiler might though
+ return []
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ if version_compare(self.version, '>=0.29.33'):
+ return ['-M']
+ return []
+
+ def get_depfile_suffix(self) -> str:
+ return 'dep'
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ code = 'print("hello world")'
+ with self.cached_compile(code, environment.coredata) as p:
+ if p.returncode != 0:
+ raise EnvironmentException(f'Cython compiler {self.id!r} cannot compile programs')
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ # Cython doesn't implement this, but Meson requires an implementation
+ return []
+
+ def get_pic_args(self) -> T.List[str]:
+ # We can lie here, it's fine
+ return []
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ new: T.List[str] = []
+ for i in parameter_list:
+ new.append(i)
+
+ return new
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = super().get_options()
+ opts.update({
+ OptionKey('version', machine=self.for_machine, lang=self.language): coredata.UserComboOption(
+ 'Python version to target',
+ ['2', '3'],
+ '3',
+ ),
+ OptionKey('language', machine=self.for_machine, lang=self.language): coredata.UserComboOption(
+ 'Output C or C++ files',
+ ['c', 'cpp'],
+ 'c',
+ )
+ })
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args: T.List[str] = []
+ key = options[OptionKey('version', machine=self.for_machine, lang=self.language)]
+ args.append(f'-{key.value}')
+ lang = options[OptionKey('language', machine=self.for_machine, lang=self.language)]
+ if lang.value == 'cpp':
+ args.append('--cplus')
+ return args
diff --git a/mesonbuild/compilers/d.py b/mesonbuild/compilers/d.py
new file mode 100644
index 0000000..90c0498
--- /dev/null
+++ b/mesonbuild/compilers/d.py
@@ -0,0 +1,1022 @@
+# Copyright 2012-2022 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.
+from __future__ import annotations
+
+import os.path
+import re
+import subprocess
+import typing as T
+
+from .. import mesonlib
+from .. import mlog
+from ..arglist import CompilerArgs
+from ..linkers import RSPFileSyntax
+from ..mesonlib import (
+ EnvironmentException, version_compare, OptionKey, is_windows
+)
+
+from . import compilers
+from .compilers import (
+ d_dmd_buildtype_args,
+ d_gdc_buildtype_args,
+ d_ldc_buildtype_args,
+ clike_debug_args,
+ Compiler,
+ CompileCheckMode,
+)
+from .mixins.gnu import GnuCompiler
+from .mixins.gnu import gnu_common_warning_args
+
+if T.TYPE_CHECKING:
+ from ..dependencies import Dependency
+ from ..programs import ExternalProgram
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..linkers import DynamicLinker
+ from ..mesonlib import MachineChoice
+
+ CompilerMixinBase = Compiler
+else:
+ CompilerMixinBase = object
+
+d_feature_args = {'gcc': {'unittest': '-funittest',
+ 'debug': '-fdebug',
+ 'version': '-fversion',
+ 'import_dir': '-J'
+ },
+ 'llvm': {'unittest': '-unittest',
+ 'debug': '-d-debug',
+ 'version': '-d-version',
+ 'import_dir': '-J'
+ },
+ 'dmd': {'unittest': '-unittest',
+ 'debug': '-debug',
+ 'version': '-version',
+ 'import_dir': '-J'
+ }
+ } # type: T.Dict[str, T.Dict[str, str]]
+
+ldc_optimization_args = {'plain': [],
+ '0': [],
+ 'g': [],
+ '1': ['-O1'],
+ '2': ['-O2'],
+ '3': ['-O3'],
+ 's': ['-Oz'],
+ } # type: T.Dict[str, T.List[str]]
+
+dmd_optimization_args = {'plain': [],
+ '0': [],
+ 'g': [],
+ '1': ['-O'],
+ '2': ['-O'],
+ '3': ['-O'],
+ 's': ['-O'],
+ } # type: T.Dict[str, T.List[str]]
+
+
+class DmdLikeCompilerMixin(CompilerMixinBase):
+
+ """Mixin class for DMD and LDC.
+
+ LDC has a number of DMD like arguments, and this class allows for code
+ sharing between them as makes sense.
+ """
+
+ def __init__(self, dmd_frontend_version: T.Optional[str]):
+ if dmd_frontend_version is None:
+ self._dmd_has_depfile = False
+ else:
+ # -makedeps switch introduced in 2.095 frontend
+ self._dmd_has_depfile = version_compare(dmd_frontend_version, ">=2.095.0")
+
+ if T.TYPE_CHECKING:
+ mscrt_args = {} # type: T.Dict[str, T.List[str]]
+
+ def _get_target_arch_args(self) -> T.List[str]: ...
+
+ LINKER_PREFIX = '-L='
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return ['-of=' + outputname]
+
+ def get_linker_output_args(self, outputname: str) -> T.List[str]:
+ return ['-of=' + outputname]
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if path == "":
+ path = "."
+ return ['-I=' + path]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:3] == '-I=':
+ parameter_list[idx] = i[:3] + os.path.normpath(os.path.join(build_dir, i[3:]))
+ if i[:4] == '-L-L':
+ parameter_list[idx] = i[:4] + os.path.normpath(os.path.join(build_dir, i[4:]))
+ if i[:5] == '-L=-L':
+ parameter_list[idx] = i[:5] + os.path.normpath(os.path.join(build_dir, i[5:]))
+ if i[:6] == '-Wl,-L':
+ parameter_list[idx] = i[:6] + os.path.normpath(os.path.join(build_dir, i[6:]))
+
+ return parameter_list
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ return ['-wi']
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['-w']
+
+ def get_coverage_args(self) -> T.List[str]:
+ return ['-cov']
+
+ def get_coverage_link_args(self) -> T.List[str]:
+ return []
+
+ def get_preprocess_only_args(self) -> T.List[str]:
+ return ['-E']
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return ['-c']
+
+ def get_depfile_suffix(self) -> str:
+ return 'deps'
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ if self._dmd_has_depfile:
+ return [f'-makedeps={outfile}']
+ return []
+
+ def get_pic_args(self) -> T.List[str]:
+ if self.info.is_windows():
+ return []
+ return ['-fPIC']
+
+ def get_feature_args(self, kwargs: T.Dict[str, T.Any], build_to_src: str) -> T.List[str]:
+ # TODO: using a TypeDict here would improve this
+ res = []
+ # get_feature_args can be called multiple times for the same target when there is generated source
+ # so we have to copy the kwargs (target.d_features) dict before popping from it
+ kwargs = kwargs.copy()
+ if 'unittest' in kwargs:
+ unittest = kwargs.pop('unittest')
+ unittest_arg = d_feature_args[self.id]['unittest']
+ if not unittest_arg:
+ raise EnvironmentException('D compiler %s does not support the "unittest" feature.' % self.name_string())
+ if unittest:
+ res.append(unittest_arg)
+
+ if 'debug' in kwargs:
+ debug_level = -1
+ debugs = kwargs.pop('debug')
+ if not isinstance(debugs, list):
+ debugs = [debugs]
+
+ debug_arg = d_feature_args[self.id]['debug']
+ if not debug_arg:
+ raise EnvironmentException('D compiler %s does not support conditional debug identifiers.' % self.name_string())
+
+ # Parse all debug identifiers and the largest debug level identifier
+ for d in debugs:
+ if isinstance(d, int):
+ if d > debug_level:
+ debug_level = d
+ elif isinstance(d, str) and d.isdigit():
+ if int(d) > debug_level:
+ debug_level = int(d)
+ else:
+ res.append(f'{debug_arg}={d}')
+
+ if debug_level >= 0:
+ res.append(f'{debug_arg}={debug_level}')
+
+ if 'versions' in kwargs:
+ version_level = -1
+ versions = kwargs.pop('versions')
+ if not isinstance(versions, list):
+ versions = [versions]
+
+ version_arg = d_feature_args[self.id]['version']
+ if not version_arg:
+ raise EnvironmentException('D compiler %s does not support conditional version identifiers.' % self.name_string())
+
+ # Parse all version identifiers and the largest version level identifier
+ for v in versions:
+ if isinstance(v, int):
+ if v > version_level:
+ version_level = v
+ elif isinstance(v, str) and v.isdigit():
+ if int(v) > version_level:
+ version_level = int(v)
+ else:
+ res.append(f'{version_arg}={v}')
+
+ if version_level >= 0:
+ res.append(f'{version_arg}={version_level}')
+
+ if 'import_dirs' in kwargs:
+ import_dirs = kwargs.pop('import_dirs')
+ if not isinstance(import_dirs, list):
+ import_dirs = [import_dirs]
+
+ import_dir_arg = d_feature_args[self.id]['import_dir']
+ if not import_dir_arg:
+ raise EnvironmentException('D compiler %s does not support the "string import directories" feature.' % self.name_string())
+ for idir_obj in import_dirs:
+ basedir = idir_obj.get_curdir()
+ for idir in idir_obj.get_incdirs():
+ bldtreedir = os.path.join(basedir, idir)
+ # Avoid superfluous '/.' at the end of paths when d is '.'
+ if idir not in ('', '.'):
+ expdir = bldtreedir
+ else:
+ expdir = basedir
+ srctreedir = os.path.join(build_to_src, expdir)
+ res.append(f'{import_dir_arg}{srctreedir}')
+ res.append(f'{import_dir_arg}{bldtreedir}')
+
+ if kwargs:
+ raise EnvironmentException('Unknown D compiler feature(s) selected: %s' % ', '.join(kwargs.keys()))
+
+ return res
+
+ def get_buildtype_linker_args(self, buildtype: str) -> T.List[str]:
+ if buildtype != 'plain':
+ return self._get_target_arch_args()
+ return []
+
+ def gen_import_library_args(self, implibname: str) -> T.List[str]:
+ return self.linker.import_library_args(implibname)
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ if self.info.is_windows():
+ return ([], set())
+
+ # GNU ld, solaris ld, and lld acting like GNU ld
+ if self.linker.id.startswith('ld'):
+ # The way that dmd and ldc pass rpath to gcc is different than we would
+ # do directly, each argument -rpath and the value to rpath, need to be
+ # split into two separate arguments both prefaced with the -L=.
+ args = []
+ (rpath_args, rpath_dirs_to_remove) = super().build_rpath_args(
+ env, build_dir, from_dir, rpath_paths, build_rpath, install_rpath)
+ for r in rpath_args:
+ if ',' in r:
+ a, b = r.split(',', maxsplit=1)
+ args.append(a)
+ args.append(self.LINKER_PREFIX + b)
+ else:
+ args.append(r)
+ return (args, rpath_dirs_to_remove)
+
+ return super().build_rpath_args(
+ env, build_dir, from_dir, rpath_paths, build_rpath, install_rpath)
+
+ @classmethod
+ def _translate_args_to_nongnu(cls, args: T.List[str], info: MachineInfo, link_id: str) -> T.List[str]:
+ # Translate common arguments to flags the LDC/DMD compilers
+ # can understand.
+ # The flags might have been added by pkg-config files,
+ # and are therefore out of the user's control.
+ dcargs = []
+ # whether we hit a linker argument that expect another arg
+ # see the comment in the "-L" section
+ link_expect_arg = False
+ link_flags_with_arg = [
+ '-rpath', '-rpath-link', '-soname', '-compatibility_version', '-current_version',
+ ]
+ for arg in args:
+ # Translate OS specific arguments first.
+ osargs = [] # type: T.List[str]
+ if info.is_windows():
+ osargs = cls.translate_arg_to_windows(arg)
+ elif info.is_darwin():
+ osargs = cls._translate_arg_to_osx(arg)
+ if osargs:
+ dcargs.extend(osargs)
+ continue
+
+ # Translate common D arguments here.
+ if arg == '-pthread':
+ continue
+ if arg.startswith('-fstack-protector'):
+ continue
+ if arg.startswith('-D') and not (arg == '-D' or arg.startswith(('-Dd', '-Df'))):
+ # ignore all '-D*' flags (like '-D_THREAD_SAFE')
+ # unless they are related to documentation
+ continue
+ if arg.startswith('-Wl,'):
+ # Translate linker arguments here.
+ linkargs = arg[arg.index(',') + 1:].split(',')
+ for la in linkargs:
+ dcargs.append('-L=' + la.strip())
+ continue
+ elif arg.startswith(('-link-defaultlib', '-linker', '-link-internally', '-linkonce-templates', '-lib')):
+ # these are special arguments to the LDC linker call,
+ # arguments like "-link-defaultlib-shared" do *not*
+ # denote a library to be linked, but change the default
+ # Phobos/DRuntime linking behavior, while "-linker" sets the
+ # default linker.
+ dcargs.append(arg)
+ continue
+ elif arg.startswith('-l'):
+ # translate library link flag
+ dcargs.append('-L=' + arg)
+ continue
+ elif arg.startswith('-isystem'):
+ # translate -isystem system include path
+ # this flag might sometimes be added by C library Cflags via
+ # pkg-config.
+ # NOTE: -isystem and -I are not 100% equivalent, so this is just
+ # a workaround for the most common cases.
+ if arg.startswith('-isystem='):
+ dcargs.append('-I=' + arg[9:])
+ else:
+ dcargs.append('-I' + arg[8:])
+ continue
+ elif arg.startswith('-idirafter'):
+ # same as -isystem, but appends the path instead
+ if arg.startswith('-idirafter='):
+ dcargs.append('-I=' + arg[11:])
+ else:
+ dcargs.append('-I' + arg[10:])
+ continue
+ elif arg.startswith('-L'):
+ # The D linker expect library search paths in the form of -L=-L/path (the '=' is optional).
+ #
+ # This function receives a mix of arguments already prepended
+ # with -L for the D linker driver and other linker arguments.
+ # The arguments starting with -L can be:
+ # - library search path (with or without a second -L)
+ # - it can come from pkg-config (a single -L)
+ # - or from the user passing linker flags (-L-L would be expected)
+ # - arguments like "-L=-rpath" that expect a second argument (also prepended with -L)
+ # - arguments like "-L=@rpath/xxx" without a second argument (on Apple platform)
+ # - arguments like "-L=/SUBSYSTEM:CONSOLE (for Windows linker)
+ #
+ # The logic that follows tries to detect all these cases (some may be missing)
+ # in order to prepend a -L only for the library search paths with a single -L
+
+ if arg.startswith('-L='):
+ suffix = arg[3:]
+ else:
+ suffix = arg[2:]
+
+ if link_expect_arg:
+ # flags like rpath and soname expect a path or filename respectively,
+ # we must not alter it (i.e. prefixing with -L for a lib search path)
+ dcargs.append(arg)
+ link_expect_arg = False
+ continue
+
+ if suffix in link_flags_with_arg:
+ link_expect_arg = True
+
+ if suffix.startswith('-') or suffix.startswith('@'):
+ # this is not search path
+ dcargs.append(arg)
+ continue
+
+ # linker flag such as -L=/DEBUG must pass through
+ if info.is_windows() and link_id == 'link' and suffix.startswith('/'):
+ dcargs.append(arg)
+ continue
+
+ # Make sure static library files are passed properly to the linker.
+ if arg.endswith('.a') or arg.endswith('.lib'):
+ if len(suffix) > 0 and not suffix.startswith('-'):
+ dcargs.append('-L=' + suffix)
+ continue
+
+ dcargs.append('-L=' + arg)
+ continue
+ elif not arg.startswith('-') and arg.endswith(('.a', '.lib')):
+ # ensure static libraries are passed through to the linker
+ dcargs.append('-L=' + arg)
+ continue
+ else:
+ dcargs.append(arg)
+
+ return dcargs
+
+ @classmethod
+ def translate_arg_to_windows(cls, arg: str) -> T.List[str]:
+ args = []
+ if arg.startswith('-Wl,'):
+ # Translate linker arguments here.
+ linkargs = arg[arg.index(',') + 1:].split(',')
+ for la in linkargs:
+ if la.startswith('--out-implib='):
+ # Import library name
+ args.append('-L=/IMPLIB:' + la[13:].strip())
+ elif arg.startswith('-mscrtlib='):
+ args.append(arg)
+ mscrtlib = arg[10:].lower()
+ if cls is LLVMDCompiler:
+ # Default crt libraries for LDC2 must be excluded for other
+ # selected crt options.
+ if mscrtlib != 'libcmt':
+ args.append('-L=/NODEFAULTLIB:libcmt')
+ args.append('-L=/NODEFAULTLIB:libvcruntime')
+
+ # Fixes missing definitions for printf-functions in VS2017
+ if mscrtlib.startswith('msvcrt'):
+ args.append('-L=/DEFAULTLIB:legacy_stdio_definitions.lib')
+
+ return args
+
+ @classmethod
+ def _translate_arg_to_osx(cls, arg: str) -> T.List[str]:
+ args = []
+ if arg.startswith('-install_name'):
+ args.append('-L=' + arg)
+ return args
+
+ @classmethod
+ def _unix_args_to_native(cls, args: T.List[str], info: MachineInfo, link_id: str = '') -> T.List[str]:
+ return cls._translate_args_to_nongnu(args, info, link_id)
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ ddebug_args = []
+ if is_debug:
+ ddebug_args = [d_feature_args[self.id]['debug']]
+
+ return clike_debug_args[is_debug] + ddebug_args
+
+ def _get_crt_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ if not self.info.is_windows():
+ return []
+
+ if crt_val in self.mscrt_args:
+ return self.mscrt_args[crt_val]
+ assert crt_val in {'from_buildtype', 'static_from_buildtype'}
+
+ dbg = 'mdd'
+ rel = 'md'
+ if crt_val == 'static_from_buildtype':
+ dbg = 'mtd'
+ rel = 'mt'
+
+ # Match what build type flags used to do.
+ if buildtype == 'plain':
+ return []
+ elif buildtype == 'debug':
+ return self.mscrt_args[dbg]
+ elif buildtype == 'debugoptimized':
+ return self.mscrt_args[rel]
+ elif buildtype == 'release':
+ return self.mscrt_args[rel]
+ elif buildtype == 'minsize':
+ return self.mscrt_args[rel]
+ else:
+ assert buildtype == 'custom'
+ raise EnvironmentException('Requested C runtime based on buildtype, but buildtype is "custom".')
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str,
+ darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ sargs = super().get_soname_args(env, prefix, shlib_name, suffix,
+ soversion, darwin_versions)
+
+ # LDC and DMD actually do use a linker, but they proxy all of that with
+ # their own arguments
+ if self.linker.id.startswith('ld.'):
+ soargs = []
+ for arg in sargs:
+ a, b = arg.split(',', maxsplit=1)
+ soargs.append(a)
+ soargs.append(self.LINKER_PREFIX + b)
+ return soargs
+ elif self.linker.id.startswith('ld64'):
+ soargs = []
+ for arg in sargs:
+ if not arg.startswith(self.LINKER_PREFIX):
+ soargs.append(self.LINKER_PREFIX + arg)
+ else:
+ soargs.append(arg)
+ return soargs
+ else:
+ return sargs
+
+ def get_allow_undefined_link_args(self) -> T.List[str]:
+ args = self.linker.get_allow_undefined_args()
+ if self.info.is_darwin():
+ # On macOS we're passing these options to the C compiler, but
+ # they're linker options and need -Wl, so clang/gcc knows what to
+ # do with them. I'm assuming, but don't know for certain, that
+ # ldc/dmd do some kind of mapping internally for arguments they
+ # understand, but pass arguments they don't understand directly.
+ args = [a.replace('-L=', '-Xcc=-Wl,') for a in args]
+ return args
+
+
+class DCompilerArgs(CompilerArgs):
+ prepend_prefixes = ('-I', '-L')
+ dedup2_prefixes = ('-I', )
+
+
+class DCompiler(Compiler):
+ mscrt_args = {
+ 'none': ['-mscrtlib='],
+ 'md': ['-mscrtlib=msvcrt'],
+ 'mdd': ['-mscrtlib=msvcrtd'],
+ 'mt': ['-mscrtlib=libcmt'],
+ 'mtd': ['-mscrtlib=libcmtd'],
+ }
+
+ language = 'd'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ info: 'MachineInfo', arch: str, *,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None,
+ is_cross: bool = False):
+ super().__init__([], exelist, version, for_machine, info, linker=linker,
+ full_version=full_version, is_cross=is_cross)
+ self.arch = arch
+ self.exe_wrapper = exe_wrapper
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ source_name = os.path.join(work_dir, 'sanity.d')
+ output_name = os.path.join(work_dir, 'dtest')
+ with open(source_name, 'w', encoding='utf-8') as ofile:
+ ofile.write('''void main() { }''')
+ pc = subprocess.Popen(self.exelist + self.get_output_args(output_name) + self._get_target_arch_args() + [source_name], cwd=work_dir)
+ pc.wait()
+ if pc.returncode != 0:
+ raise EnvironmentException('D compiler %s can not compile programs.' % self.name_string())
+ if self.is_cross:
+ if self.exe_wrapper is None:
+ # Can't check if the binaries run so we have to assume they do
+ return
+ cmdlist = self.exe_wrapper.get_command() + [output_name]
+ else:
+ cmdlist = [output_name]
+ if subprocess.call(cmdlist) != 0:
+ raise EnvironmentException('Executables created by D compiler %s are not runnable.' % self.name_string())
+
+ def needs_static_linker(self) -> bool:
+ return True
+
+ def get_depfile_suffix(self) -> str:
+ return 'deps'
+
+ def get_pic_args(self) -> T.List[str]:
+ if self.info.is_windows():
+ return []
+ return ['-fPIC']
+
+ def get_feature_args(self, kwargs: T.Dict[str, T.Any], build_to_src: str) -> T.List[str]:
+ # TODO: using a TypeDict here would improve this
+ res = []
+ # get_feature_args can be called multiple times for the same target when there is generated source
+ # so we have to copy the kwargs (target.d_features) dict before popping from it
+ kwargs = kwargs.copy()
+ if 'unittest' in kwargs:
+ unittest = kwargs.pop('unittest')
+ unittest_arg = d_feature_args[self.id]['unittest']
+ if not unittest_arg:
+ raise EnvironmentException('D compiler %s does not support the "unittest" feature.' % self.name_string())
+ if unittest:
+ res.append(unittest_arg)
+
+ if 'debug' in kwargs:
+ debug_level = -1
+ debugs = kwargs.pop('debug')
+ if not isinstance(debugs, list):
+ debugs = [debugs]
+
+ debug_arg = d_feature_args[self.id]['debug']
+ if not debug_arg:
+ raise EnvironmentException('D compiler %s does not support conditional debug identifiers.' % self.name_string())
+
+ # Parse all debug identifiers and the largest debug level identifier
+ for d in debugs:
+ if isinstance(d, int):
+ if d > debug_level:
+ debug_level = d
+ elif isinstance(d, str) and d.isdigit():
+ if int(d) > debug_level:
+ debug_level = int(d)
+ else:
+ res.append(f'{debug_arg}={d}')
+
+ if debug_level >= 0:
+ res.append(f'{debug_arg}={debug_level}')
+
+ if 'versions' in kwargs:
+ version_level = -1
+ versions = kwargs.pop('versions')
+ if not isinstance(versions, list):
+ versions = [versions]
+
+ version_arg = d_feature_args[self.id]['version']
+ if not version_arg:
+ raise EnvironmentException('D compiler %s does not support conditional version identifiers.' % self.name_string())
+
+ # Parse all version identifiers and the largest version level identifier
+ for v in versions:
+ if isinstance(v, int):
+ if v > version_level:
+ version_level = v
+ elif isinstance(v, str) and v.isdigit():
+ if int(v) > version_level:
+ version_level = int(v)
+ else:
+ res.append(f'{version_arg}={v}')
+
+ if version_level >= 0:
+ res.append(f'{version_arg}={version_level}')
+
+ if 'import_dirs' in kwargs:
+ import_dirs = kwargs.pop('import_dirs')
+ if not isinstance(import_dirs, list):
+ import_dirs = [import_dirs]
+
+ import_dir_arg = d_feature_args[self.id]['import_dir']
+ if not import_dir_arg:
+ raise EnvironmentException('D compiler %s does not support the "string import directories" feature.' % self.name_string())
+ for idir_obj in import_dirs:
+ basedir = idir_obj.get_curdir()
+ for idir in idir_obj.get_incdirs():
+ bldtreedir = os.path.join(basedir, idir)
+ # Avoid superfluous '/.' at the end of paths when d is '.'
+ if idir not in ('', '.'):
+ expdir = bldtreedir
+ else:
+ expdir = basedir
+ srctreedir = os.path.join(build_to_src, expdir)
+ res.append(f'{import_dir_arg}{srctreedir}')
+ res.append(f'{import_dir_arg}{bldtreedir}')
+
+ if kwargs:
+ raise EnvironmentException('Unknown D compiler feature(s) selected: %s' % ', '.join(kwargs.keys()))
+
+ return res
+
+ def get_buildtype_linker_args(self, buildtype: str) -> T.List[str]:
+ if buildtype != 'plain':
+ return self._get_target_arch_args()
+ return []
+
+ def compiler_args(self, args: T.Optional[T.Iterable[str]] = None) -> DCompilerArgs:
+ return DCompilerArgs(self, args)
+
+ def has_multi_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]:
+ return self.compiles('int i;\n', env, extra_args=args)
+
+ def _get_target_arch_args(self) -> T.List[str]:
+ # LDC2 on Windows targets to current OS architecture, but
+ # it should follow the target specified by the MSVC toolchain.
+ if self.info.is_windows():
+ if self.arch == 'x86_64':
+ return ['-m64']
+ return ['-m32']
+ return []
+
+ def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ return []
+
+ def get_crt_link_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ return []
+
+ def _get_compile_extra_args(self, extra_args: T.Union[T.List[str], T.Callable[[CompileCheckMode], T.List[str]], None] = None) -> T.List[str]:
+ args = self._get_target_arch_args()
+ if extra_args:
+ if callable(extra_args):
+ extra_args = extra_args(CompileCheckMode.COMPILE)
+ if isinstance(extra_args, list):
+ args.extend(extra_args)
+ elif isinstance(extra_args, str):
+ args.append(extra_args)
+ return args
+
+ def run(self, code: 'mesonlib.FileOrString', env: 'Environment', *,
+ extra_args: T.Union[T.List[str], T.Callable[[CompileCheckMode], T.List[str]], None] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> compilers.RunResult:
+ need_exe_wrapper = env.need_exe_wrapper(self.for_machine)
+ if need_exe_wrapper and self.exe_wrapper is None:
+ raise compilers.CrossNoRunException('Can not run test applications in this cross environment.')
+ extra_args = self._get_compile_extra_args(extra_args)
+ with self._build_wrapper(code, env, extra_args, dependencies, mode='link', want_output=True) as p:
+ if p.returncode != 0:
+ mlog.debug(f'Could not compile test file {p.input_name}: {p.returncode}\n')
+ return compilers.RunResult(False)
+ if need_exe_wrapper:
+ cmdlist = self.exe_wrapper.get_command() + [p.output_name]
+ else:
+ cmdlist = [p.output_name]
+ try:
+ pe, so, se = mesonlib.Popen_safe(cmdlist)
+ except Exception as e:
+ mlog.debug(f'Could not run: {cmdlist} (error: {e})\n')
+ return compilers.RunResult(False)
+
+ mlog.debug('Program stdout:\n')
+ mlog.debug(so)
+ mlog.debug('Program stderr:\n')
+ mlog.debug(se)
+ return compilers.RunResult(True, pe.returncode, so, se)
+
+ def sizeof(self, typename: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> int:
+ if extra_args is None:
+ extra_args = []
+ t = f'''
+ import std.stdio : writeln;
+ {prefix}
+ void main() {{
+ writeln(({typename}).sizeof);
+ }}
+ '''
+ res = self.run(t, env, extra_args=extra_args,
+ dependencies=dependencies)
+ if not res.compiled:
+ return -1
+ if res.returncode != 0:
+ raise mesonlib.EnvironmentException('Could not run sizeof test binary.')
+ return int(res.stdout)
+
+ def alignment(self, typename: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Optional[T.List[str]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> int:
+ if extra_args is None:
+ extra_args = []
+ t = f'''
+ import std.stdio : writeln;
+ {prefix}
+ void main() {{
+ writeln(({typename}).alignof);
+ }}
+ '''
+ res = self.run(t, env, extra_args=extra_args,
+ dependencies=dependencies)
+ if not res.compiled:
+ raise mesonlib.EnvironmentException('Could not compile alignment test.')
+ if res.returncode != 0:
+ raise mesonlib.EnvironmentException('Could not run alignment test binary.')
+ align = int(res.stdout)
+ if align == 0:
+ raise mesonlib.EnvironmentException(f'Could not determine alignment of {typename}. Sorry. You might want to file a bug.')
+ return align
+
+ def has_header(self, hname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[['CompileCheckMode'], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None,
+ disable_cache: bool = False) -> T.Tuple[bool, bool]:
+
+ extra_args = self._get_compile_extra_args(extra_args)
+ code = f'''{prefix}
+ import {hname};
+ '''
+ return self.compiles(code, env, extra_args=extra_args,
+ dependencies=dependencies, mode='compile', disable_cache=disable_cache)
+
+class GnuDCompiler(GnuCompiler, DCompiler):
+
+ # we mostly want DCompiler, but that gives us the Compiler.LINKER_PREFIX instead
+ LINKER_PREFIX = GnuCompiler.LINKER_PREFIX
+ id = 'gcc'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ info: 'MachineInfo', arch: str, *,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None,
+ is_cross: bool = False):
+ DCompiler.__init__(self, exelist, version, for_machine, info, arch,
+ exe_wrapper=exe_wrapper, linker=linker,
+ full_version=full_version, is_cross=is_cross)
+ GnuCompiler.__init__(self, {})
+ default_warn_args = ['-Wall', '-Wdeprecated']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': (default_warn_args + ['-Wextra', '-Wpedantic'] +
+ self.supported_warn_args(gnu_common_warning_args))}
+
+ self.base_options = {
+ OptionKey(o) for o in [
+ 'b_colorout', 'b_sanitize', 'b_staticpic', 'b_vscrt',
+ 'b_coverage', 'b_pgo', 'b_ndebug']}
+
+ self._has_color_support = version_compare(self.version, '>=4.9')
+ # dependencies were implemented before, but broken - support was fixed in GCC 7.1+
+ # (and some backported versions)
+ self._has_deps_support = version_compare(self.version, '>=7.1')
+
+ def get_colorout_args(self, colortype: str) -> T.List[str]:
+ if self._has_color_support:
+ super().get_colorout_args(colortype)
+ return []
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ if self._has_deps_support:
+ return super().get_dependency_gen_args(outtarget, outfile)
+ return []
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ return self.warn_args[level]
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return d_gdc_buildtype_args[buildtype]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I' or i[:2] == '-L':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+
+ return parameter_list
+
+ def get_allow_undefined_link_args(self) -> T.List[str]:
+ return self.linker.get_allow_undefined_args()
+
+ def get_linker_always_args(self) -> T.List[str]:
+ args = super().get_linker_always_args()
+ if self.info.is_windows():
+ return args
+ return args + ['-shared-libphobos']
+
+ def get_disable_assert_args(self) -> T.List[str]:
+ return ['-frelease']
+
+# LDC uses the DMD frontend code to parse and analyse the code.
+# It then uses LLVM for the binary code generation and optimizations.
+# This function retrieves the dmd frontend version, which determines
+# the common features between LDC and DMD.
+# We need the complete version text because the match is not on first line
+# of version_output
+def find_ldc_dmd_frontend_version(version_output: T.Optional[str]) -> T.Optional[str]:
+ if version_output is None:
+ return None
+ version_regex = re.search(r'DMD v(\d+\.\d+\.\d+)', version_output)
+ if version_regex:
+ return version_regex.group(1)
+ return None
+
+class LLVMDCompiler(DmdLikeCompilerMixin, DCompiler):
+
+ id = 'llvm'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ info: 'MachineInfo', arch: str, *,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None,
+ is_cross: bool = False, version_output: T.Optional[str] = None):
+ DCompiler.__init__(self, exelist, version, for_machine, info, arch,
+ exe_wrapper=exe_wrapper, linker=linker,
+ full_version=full_version, is_cross=is_cross)
+ DmdLikeCompilerMixin.__init__(self, dmd_frontend_version=find_ldc_dmd_frontend_version(version_output))
+ self.base_options = {OptionKey(o) for o in ['b_coverage', 'b_colorout', 'b_vscrt', 'b_ndebug']}
+
+ def get_colorout_args(self, colortype: str) -> T.List[str]:
+ if colortype == 'always':
+ return ['-enable-color']
+ return []
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ if level in {'2', '3'}:
+ return ['-wi', '-dw']
+ elif level == '1':
+ return ['-wi']
+ return []
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ if buildtype != 'plain':
+ return self._get_target_arch_args() + d_ldc_buildtype_args[buildtype]
+ return d_ldc_buildtype_args[buildtype]
+
+ def get_pic_args(self) -> T.List[str]:
+ return ['-relocation-model=pic']
+
+ def get_crt_link_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ return self._get_crt_args(crt_val, buildtype)
+
+ def unix_args_to_native(self, args: T.List[str]) -> T.List[str]:
+ return self._unix_args_to_native(args, self.info, self.linker.id)
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return ldc_optimization_args[optimization_level]
+
+ @classmethod
+ def use_linker_args(cls, linker: str, version: str) -> T.List[str]:
+ return [f'-linker={linker}']
+
+ def get_linker_always_args(self) -> T.List[str]:
+ args = super().get_linker_always_args()
+ if self.info.is_windows():
+ return args
+ return args + ['-link-defaultlib-shared']
+
+ def get_disable_assert_args(self) -> T.List[str]:
+ return ['--release']
+
+ def rsp_file_syntax(self) -> RSPFileSyntax:
+ # We use `mesonlib.is_windows` here because we want to know what the
+ # build machine is, not the host machine. This really means we would
+ # have the Environment not the MachineInfo in the compiler.
+ return RSPFileSyntax.MSVC if is_windows() else RSPFileSyntax.GCC
+
+
+class DmdDCompiler(DmdLikeCompilerMixin, DCompiler):
+
+ id = 'dmd'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ info: 'MachineInfo', arch: str, *,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None,
+ is_cross: bool = False):
+ DCompiler.__init__(self, exelist, version, for_machine, info, arch,
+ exe_wrapper=exe_wrapper, linker=linker,
+ full_version=full_version, is_cross=is_cross)
+ DmdLikeCompilerMixin.__init__(self, version)
+ self.base_options = {OptionKey(o) for o in ['b_coverage', 'b_colorout', 'b_vscrt', 'b_ndebug']}
+
+ def get_colorout_args(self, colortype: str) -> T.List[str]:
+ if colortype == 'always':
+ return ['-color=on']
+ return []
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ if buildtype != 'plain':
+ return self._get_target_arch_args() + d_dmd_buildtype_args[buildtype]
+ return d_dmd_buildtype_args[buildtype]
+
+ def get_std_exe_link_args(self) -> T.List[str]:
+ if self.info.is_windows():
+ # DMD links against D runtime only when main symbol is found,
+ # so these needs to be inserted when linking static D libraries.
+ if self.arch == 'x86_64':
+ return ['phobos64.lib']
+ elif self.arch == 'x86_mscoff':
+ return ['phobos32mscoff.lib']
+ return ['phobos.lib']
+ return []
+
+ def get_std_shared_lib_link_args(self) -> T.List[str]:
+ libname = 'libphobos2.so'
+ if self.info.is_windows():
+ if self.arch == 'x86_64':
+ libname = 'phobos64.lib'
+ elif self.arch == 'x86_mscoff':
+ libname = 'phobos32mscoff.lib'
+ else:
+ libname = 'phobos.lib'
+ return ['-shared', '-defaultlib=' + libname]
+
+ def _get_target_arch_args(self) -> T.List[str]:
+ # DMD32 and DMD64 on 64-bit Windows defaults to 32-bit (OMF).
+ # Force the target to 64-bit in order to stay consistent
+ # across the different platforms.
+ if self.info.is_windows():
+ if self.arch == 'x86_64':
+ return ['-m64']
+ elif self.arch == 'x86_mscoff':
+ return ['-m32mscoff']
+ return ['-m32']
+ return []
+
+ def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ return self._get_crt_args(crt_val, buildtype)
+
+ def unix_args_to_native(self, args: T.List[str]) -> T.List[str]:
+ return self._unix_args_to_native(args, self.info, self.linker.id)
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return dmd_optimization_args[optimization_level]
+
+ def can_linker_accept_rsp(self) -> bool:
+ return False
+
+ def get_linker_always_args(self) -> T.List[str]:
+ args = super().get_linker_always_args()
+ if self.info.is_windows():
+ return args
+ return args + ['-defaultlib=phobos2', '-debuglib=phobos2']
+
+ def get_disable_assert_args(self) -> T.List[str]:
+ return ['-release']
+
+ def rsp_file_syntax(self) -> RSPFileSyntax:
+ return RSPFileSyntax.MSVC
diff --git a/mesonbuild/compilers/detect.py b/mesonbuild/compilers/detect.py
new file mode 100644
index 0000000..1f37833
--- /dev/null
+++ b/mesonbuild/compilers/detect.py
@@ -0,0 +1,1317 @@
+# Copyright 2012-2022 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.
+from __future__ import annotations
+
+from ..mesonlib import (
+ MesonException, EnvironmentException, MachineChoice, join_args,
+ search_version, is_windows, Popen_safe, windows_proof_rm,
+)
+from ..envconfig import BinaryTable
+from .. import mlog
+
+from ..linkers import guess_win_linker, guess_nix_linker
+
+import subprocess
+import platform
+import re
+import shutil
+import tempfile
+import os
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .compilers import Compiler
+ from .c import CCompiler
+ from .cpp import CPPCompiler
+ from .fortran import FortranCompiler
+ from .rust import RustCompiler
+ from ..linkers import StaticLinker
+ from ..environment import Environment
+ from ..programs import ExternalProgram
+
+
+# Default compilers and linkers
+# =============================
+
+defaults: T.Dict[str, T.List[str]] = {}
+
+# List of potential compilers.
+if is_windows():
+ # Intel C and C++ compiler is icl on Windows, but icc and icpc elsewhere.
+ # Search for icl before cl, since Intel "helpfully" provides a
+ # cl.exe that returns *exactly the same thing* that microsofts
+ # cl.exe does, and if icl is present, it's almost certainly what
+ # you want.
+ defaults['c'] = ['icl', 'cl', 'cc', 'gcc', 'clang', 'clang-cl', 'pgcc']
+ # There is currently no pgc++ for Windows, only for Mac and Linux.
+ defaults['cpp'] = ['icl', 'cl', 'c++', 'g++', 'clang++', 'clang-cl']
+ defaults['fortran'] = ['ifort', 'gfortran', 'flang', 'pgfortran', 'g95']
+ # Clang and clang++ are valid, but currently unsupported.
+ defaults['objc'] = ['cc', 'gcc']
+ defaults['objcpp'] = ['c++', 'g++']
+ defaults['cs'] = ['csc', 'mcs']
+else:
+ if platform.machine().lower() == 'e2k':
+ defaults['c'] = ['cc', 'gcc', 'lcc', 'clang']
+ defaults['cpp'] = ['c++', 'g++', 'l++', 'clang++']
+ defaults['objc'] = ['clang']
+ defaults['objcpp'] = ['clang++']
+ else:
+ defaults['c'] = ['cc', 'gcc', 'clang', 'nvc', 'pgcc', 'icc', 'icx']
+ defaults['cpp'] = ['c++', 'g++', 'clang++', 'nvc++', 'pgc++', 'icpc', 'icpx']
+ defaults['objc'] = ['cc', 'gcc', 'clang']
+ defaults['objcpp'] = ['c++', 'g++', 'clang++']
+ defaults['fortran'] = ['gfortran', 'flang', 'nvfortran', 'pgfortran', 'ifort', 'ifx', 'g95']
+ defaults['cs'] = ['mcs', 'csc']
+defaults['d'] = ['ldc2', 'ldc', 'gdc', 'dmd']
+defaults['java'] = ['javac']
+defaults['cuda'] = ['nvcc']
+defaults['rust'] = ['rustc']
+defaults['swift'] = ['swiftc']
+defaults['vala'] = ['valac']
+defaults['cython'] = ['cython', 'cython3'] # Official name is cython, but Debian renamed it to cython3.
+defaults['static_linker'] = ['ar', 'gar']
+defaults['strip'] = ['strip']
+defaults['vs_static_linker'] = ['lib']
+defaults['clang_cl_static_linker'] = ['llvm-lib']
+defaults['cuda_static_linker'] = ['nvlink']
+defaults['gcc_static_linker'] = ['gcc-ar']
+defaults['clang_static_linker'] = ['llvm-ar']
+defaults['nasm'] = ['nasm', 'yasm']
+
+
+def compiler_from_language(env: 'Environment', lang: str, for_machine: MachineChoice) -> T.Optional[Compiler]:
+ lang_map: T.Dict[str, T.Callable[['Environment', MachineChoice], Compiler]] = {
+ 'c': detect_c_compiler,
+ 'cpp': detect_cpp_compiler,
+ 'objc': detect_objc_compiler,
+ 'cuda': detect_cuda_compiler,
+ 'objcpp': detect_objcpp_compiler,
+ 'java': detect_java_compiler,
+ 'cs': detect_cs_compiler,
+ 'vala': detect_vala_compiler,
+ 'd': detect_d_compiler,
+ 'rust': detect_rust_compiler,
+ 'fortran': detect_fortran_compiler,
+ 'swift': detect_swift_compiler,
+ 'cython': detect_cython_compiler,
+ 'nasm': detect_nasm_compiler,
+ 'masm': detect_masm_compiler,
+ }
+ return lang_map[lang](env, for_machine) if lang in lang_map else None
+
+def detect_compiler_for(env: 'Environment', lang: str, for_machine: MachineChoice) -> T.Optional[Compiler]:
+ comp = compiler_from_language(env, lang, for_machine)
+ if comp is not None:
+ assert comp.for_machine == for_machine
+ env.coredata.process_new_compiler(lang, comp, env)
+ return comp
+
+
+# Helpers
+# =======
+
+def _get_compilers(env: 'Environment', lang: str, for_machine: MachineChoice) -> T.Tuple[T.List[T.List[str]], T.List[str], T.Optional['ExternalProgram']]:
+ '''
+ The list of compilers is detected in the exact same way for
+ C, C++, ObjC, ObjC++, Fortran, CS so consolidate it here.
+ '''
+ value = env.lookup_binary_entry(for_machine, lang)
+ if value is not None:
+ comp, ccache = BinaryTable.parse_entry(value)
+ # Return value has to be a list of compiler 'choices'
+ compilers = [comp]
+ else:
+ if not env.machines.matches_build_machine(for_machine):
+ raise EnvironmentException(f'{lang!r} compiler binary not defined in cross or native file')
+ compilers = [[x] for x in defaults[lang]]
+ ccache = BinaryTable.detect_compiler_cache()
+
+ if env.machines.matches_build_machine(for_machine):
+ exe_wrap: T.Optional[ExternalProgram] = None
+ else:
+ exe_wrap = env.get_exe_wrapper()
+
+ return compilers, ccache, exe_wrap
+
+def _handle_exceptions(
+ exceptions: T.Mapping[str, T.Union[Exception, str]],
+ binaries: T.List[T.List[str]],
+ bintype: str = 'compiler') -> T.NoReturn:
+ errmsg = f'Unknown {bintype}(s): {binaries}'
+ if exceptions:
+ errmsg += '\nThe following exception(s) were encountered:'
+ for c, e in exceptions.items():
+ errmsg += f'\nRunning `{c}` gave "{e}"'
+ raise EnvironmentException(errmsg)
+
+
+# Linker specific
+# ===============
+
+def detect_static_linker(env: 'Environment', compiler: Compiler) -> StaticLinker:
+ from . import d
+ from ..linkers import linkers
+ linker = env.lookup_binary_entry(compiler.for_machine, 'ar')
+ if linker is not None:
+ trials = [linker]
+ else:
+ default_linkers = [[l] for l in defaults['static_linker']]
+ if compiler.language == 'cuda':
+ trials = [defaults['cuda_static_linker']] + default_linkers
+ elif compiler.get_argument_syntax() == 'msvc':
+ trials = [defaults['vs_static_linker'], defaults['clang_cl_static_linker']]
+ elif compiler.id == 'gcc':
+ # Use gcc-ar if available; needed for LTO
+ trials = [defaults['gcc_static_linker']] + default_linkers
+ elif compiler.id == 'clang':
+ # Use llvm-ar if available; needed for LTO
+ trials = [defaults['clang_static_linker']] + default_linkers
+ elif compiler.language == 'd':
+ # Prefer static linkers over linkers used by D compilers
+ if is_windows():
+ trials = [defaults['vs_static_linker'], defaults['clang_cl_static_linker'], compiler.get_linker_exelist()]
+ else:
+ trials = default_linkers
+ elif compiler.id == 'intel-cl' and compiler.language == 'c': # why not cpp? Is this a bug?
+ # Intel has it's own linker that acts like microsoft's lib
+ trials = [['xilib']]
+ elif is_windows() and compiler.id == 'pgi': # this handles cpp / nvidia HPC, in addition to just c/fortran
+ trials = [['ar']] # For PGI on Windows, "ar" is just a wrapper calling link/lib.
+ else:
+ trials = default_linkers
+ popen_exceptions = {}
+ for linker in trials:
+ linker_name = os.path.basename(linker[0])
+
+ if any(os.path.basename(x) in {'lib', 'lib.exe', 'llvm-lib', 'llvm-lib.exe', 'xilib', 'xilib.exe'} for x in linker):
+ arg = '/?'
+ elif linker_name in {'ar2000', 'ar2000.exe', 'ar430', 'ar430.exe', 'armar', 'armar.exe'}:
+ arg = '?'
+ else:
+ arg = '--version'
+ try:
+ p, out, err = Popen_safe(linker + [arg])
+ except OSError as e:
+ popen_exceptions[join_args(linker + [arg])] = e
+ continue
+ if "xilib: executing 'lib'" in err:
+ return linkers.IntelVisualStudioLinker(linker, getattr(compiler, 'machine', None))
+ if '/OUT:' in out.upper() or '/OUT:' in err.upper():
+ return linkers.VisualStudioLinker(linker, getattr(compiler, 'machine', None))
+ if 'ar-Error-Unknown switch: --version' in err:
+ return linkers.PGIStaticLinker(linker)
+ if p.returncode == 0 and 'armar' in linker_name:
+ return linkers.ArmarLinker(linker)
+ if 'DMD32 D Compiler' in out or 'DMD64 D Compiler' in out:
+ assert isinstance(compiler, d.DCompiler)
+ return linkers.DLinker(linker, compiler.arch)
+ if 'LDC - the LLVM D compiler' in out:
+ assert isinstance(compiler, d.DCompiler)
+ return linkers.DLinker(linker, compiler.arch, rsp_syntax=compiler.rsp_file_syntax())
+ if 'GDC' in out and ' based on D ' in out:
+ assert isinstance(compiler, d.DCompiler)
+ return linkers.DLinker(linker, compiler.arch)
+ if err.startswith('Renesas') and 'rlink' in linker_name:
+ return linkers.CcrxLinker(linker)
+ if out.startswith('GNU ar') and 'xc16-ar' in linker_name:
+ return linkers.Xc16Linker(linker)
+ if 'Texas Instruments Incorporated' in out:
+ if 'ar2000' in linker_name:
+ return linkers.C2000Linker(linker)
+ else:
+ return linkers.TILinker(linker)
+ if out.startswith('The CompCert'):
+ return linkers.CompCertLinker(linker)
+ if p.returncode == 0:
+ return linkers.ArLinker(compiler.for_machine, linker)
+ if p.returncode == 1 and err.startswith('usage'): # OSX
+ return linkers.AppleArLinker(compiler.for_machine, linker)
+ if p.returncode == 1 and err.startswith('Usage'): # AIX
+ return linkers.AIXArLinker(linker)
+ if p.returncode == 1 and err.startswith('ar: bad option: --'): # Solaris
+ return linkers.ArLinker(compiler.for_machine, linker)
+ _handle_exceptions(popen_exceptions, trials, 'linker')
+ raise EnvironmentException('Unreachable code (exception to make mypy happy)')
+
+
+# Compilers
+# =========
+
+
+def _detect_c_or_cpp_compiler(env: 'Environment', lang: str, for_machine: MachineChoice, *, override_compiler: T.Optional[T.List[str]] = None) -> Compiler:
+ """Shared implementation for finding the C or C++ compiler to use.
+
+ the override_compiler option is provided to allow compilers which use
+ the compiler (GCC or Clang usually) as their shared linker, to find
+ the linker they need.
+ """
+ from . import c, cpp
+ from ..linkers import linkers
+ popen_exceptions: T.Dict[str, T.Union[Exception, str]] = {}
+ compilers, ccache, exe_wrap = _get_compilers(env, lang, for_machine)
+ if override_compiler is not None:
+ compilers = [override_compiler]
+ is_cross = env.is_cross_build(for_machine)
+ info = env.machines[for_machine]
+ cls: T.Union[T.Type[CCompiler], T.Type[CPPCompiler]]
+
+ for compiler in compilers:
+ if isinstance(compiler, str):
+ compiler = [compiler]
+ compiler_name = os.path.basename(compiler[0])
+
+ if any(os.path.basename(x) in {'cl', 'cl.exe', 'clang-cl', 'clang-cl.exe'} for x in compiler):
+ # Watcom C provides it's own cl.exe clone that mimics an older
+ # version of Microsoft's compiler. Since Watcom's cl.exe is
+ # just a wrapper, we skip using it if we detect its presence
+ # so as not to confuse Meson when configuring for MSVC.
+ #
+ # Additionally the help text of Watcom's cl.exe is paged, and
+ # the binary will not exit without human intervention. In
+ # practice, Meson will block waiting for Watcom's cl.exe to
+ # exit, which requires user input and thus will never exit.
+ if 'WATCOM' in os.environ:
+ def sanitize(p: str) -> str:
+ return os.path.normcase(os.path.abspath(p))
+
+ watcom_cls = [sanitize(os.path.join(os.environ['WATCOM'], 'BINNT', 'cl')),
+ sanitize(os.path.join(os.environ['WATCOM'], 'BINNT', 'cl.exe')),
+ sanitize(os.path.join(os.environ['WATCOM'], 'BINNT64', 'cl')),
+ sanitize(os.path.join(os.environ['WATCOM'], 'BINNT64', 'cl.exe'))]
+ found_cl = sanitize(shutil.which('cl'))
+ if found_cl in watcom_cls:
+ mlog.debug('Skipping unsupported cl.exe clone at:', found_cl)
+ continue
+ arg = '/?'
+ elif 'armcc' in compiler_name:
+ arg = '--vsn'
+ elif 'ccrx' in compiler_name:
+ arg = '-v'
+ elif 'xc16' in compiler_name:
+ arg = '--version'
+ elif 'ccomp' in compiler_name:
+ arg = '-version'
+ elif compiler_name in {'cl2000', 'cl2000.exe', 'cl430', 'cl430.exe', 'armcl', 'armcl.exe'}:
+ # TI compiler
+ arg = '-version'
+ elif compiler_name in {'icl', 'icl.exe'}:
+ # if you pass anything to icl you get stuck in a pager
+ arg = ''
+ else:
+ arg = '--version'
+
+ cmd = compiler + [arg]
+ try:
+ mlog.debug('-----')
+ mlog.debug(f'Detecting compiler via: {join_args(cmd)}')
+ p, out, err = Popen_safe(cmd)
+ mlog.debug(f'compiler returned {p}')
+ mlog.debug(f'compiler stdout:\n{out}')
+ mlog.debug(f'compiler stderr:\n{err}')
+ except OSError as e:
+ popen_exceptions[join_args(cmd)] = e
+ continue
+
+ if 'ccrx' in compiler_name:
+ out = err
+
+ full_version = out.split('\n', 1)[0]
+ version = search_version(out)
+
+ guess_gcc_or_lcc: T.Optional[str] = None
+ if 'Free Software Foundation' in out or 'xt-' in out:
+ guess_gcc_or_lcc = 'gcc'
+ if 'e2k' in out and 'lcc' in out:
+ guess_gcc_or_lcc = 'lcc'
+ if 'Microchip Technology' in out:
+ # this output has "Free Software Foundation" in its version
+ guess_gcc_or_lcc = None
+
+ if guess_gcc_or_lcc:
+ defines = _get_gnu_compiler_defines(compiler)
+ if not defines:
+ popen_exceptions[join_args(compiler)] = 'no pre-processor defines'
+ continue
+
+ if guess_gcc_or_lcc == 'lcc':
+ version = _get_lcc_version_from_defines(defines)
+ cls = c.ElbrusCCompiler if lang == 'c' else cpp.ElbrusCPPCompiler
+ else:
+ version = _get_gnu_version_from_defines(defines)
+ cls = c.GnuCCompiler if lang == 'c' else cpp.GnuCPPCompiler
+
+ linker = guess_nix_linker(env, compiler, cls, version, for_machine)
+
+ return cls(
+ ccache, compiler, version, for_machine, is_cross,
+ info, exe_wrap, defines=defines, full_version=full_version,
+ linker=linker)
+
+ if 'Emscripten' in out:
+ cls = c.EmscriptenCCompiler if lang == 'c' else cpp.EmscriptenCPPCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+
+ # emcc requires a file input in order to pass arguments to the
+ # linker. It'll exit with an error code, but still print the
+ # linker version.
+ with tempfile.NamedTemporaryFile(suffix='.c') as f:
+ cmd = compiler + [cls.LINKER_PREFIX + "--version", f.name]
+ _, o, _ = Popen_safe(cmd)
+
+ linker = linkers.WASMDynamicLinker(
+ compiler, for_machine, cls.LINKER_PREFIX,
+ [], version=search_version(o))
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, linker=linker, full_version=full_version)
+
+ if 'Arm C/C++/Fortran Compiler' in out:
+ arm_ver_match = re.search(r'version (\d+)\.(\d+)\.?(\d+)? \(build number (\d+)\)', out)
+ assert arm_ver_match is not None, 'for mypy' # because mypy *should* be complaning that this could be None
+ version = '.'.join([x for x in arm_ver_match.groups() if x is not None])
+ if lang == 'c':
+ cls = c.ArmLtdClangCCompiler
+ elif lang == 'cpp':
+ cls = cpp.ArmLtdClangCPPCompiler
+ linker = guess_nix_linker(env, compiler, cls, version, for_machine)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, linker=linker)
+ if 'armclang' in out:
+ # The compiler version is not present in the first line of output,
+ # instead it is present in second line, startswith 'Component:'.
+ # So, searching for the 'Component' in out although we know it is
+ # present in second line, as we are not sure about the
+ # output format in future versions
+ arm_ver_match = re.search('.*Component.*', out)
+ if arm_ver_match is None:
+ popen_exceptions[join_args(compiler)] = 'version string not found'
+ continue
+ arm_ver_str = arm_ver_match.group(0)
+ # Override previous values
+ version = search_version(arm_ver_str)
+ full_version = arm_ver_str
+ cls = c.ArmclangCCompiler if lang == 'c' else cpp.ArmclangCPPCompiler
+ linker = linkers.ArmClangDynamicLinker(for_machine, version=version)
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+ if 'CL.EXE COMPATIBILITY' in out:
+ # if this is clang-cl masquerading as cl, detect it as cl, not
+ # clang
+ arg = '--version'
+ try:
+ p, out, err = Popen_safe(compiler + [arg])
+ except OSError as e:
+ popen_exceptions[join_args(compiler + [arg])] = e
+ version = search_version(out)
+ match = re.search('^Target: (.*?)-', out, re.MULTILINE)
+ if match:
+ target = match.group(1)
+ else:
+ target = 'unknown target'
+ cls = c.ClangClCCompiler if lang == 'c' else cpp.ClangClCPPCompiler
+ linker = guess_win_linker(env, ['lld-link'], cls, version, for_machine)
+ return cls(
+ compiler, version, for_machine, is_cross, info, target,
+ exe_wrap, linker=linker)
+ if 'clang' in out or 'Clang' in out:
+ linker = None
+
+ defines = _get_clang_compiler_defines(compiler)
+
+ # Even if the for_machine is darwin, we could be using vanilla
+ # clang.
+ if 'Apple' in out:
+ cls = c.AppleClangCCompiler if lang == 'c' else cpp.AppleClangCPPCompiler
+ else:
+ cls = c.ClangCCompiler if lang == 'c' else cpp.ClangCPPCompiler
+
+ if 'windows' in out or env.machines[for_machine].is_windows():
+ # If we're in a MINGW context this actually will use a gnu
+ # style ld, but for clang on "real" windows we'll use
+ # either link.exe or lld-link.exe
+ try:
+ linker = guess_win_linker(env, compiler, cls, version, for_machine, invoked_directly=False)
+ except MesonException:
+ pass
+ if linker is None:
+ linker = guess_nix_linker(env, compiler, cls, version, for_machine)
+
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, defines=defines, full_version=full_version, linker=linker)
+
+ if 'Intel(R) C++ Intel(R)' in err:
+ version = search_version(err)
+ target = 'x86' if 'IA-32' in err else 'x86_64'
+ cls = c.IntelClCCompiler if lang == 'c' else cpp.IntelClCPPCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.XilinkDynamicLinker(for_machine, [], version=version)
+ return cls(
+ compiler, version, for_machine, is_cross, info, target,
+ exe_wrap, linker=linker)
+ if 'Intel(R) oneAPI DPC++/C++ Compiler for applications' in err:
+ version = search_version(err)
+ target = 'x86' if 'IA-32' in err else 'x86_64'
+ cls = c.IntelLLVMClCCompiler if lang == 'c' else cpp.IntelLLVMClCPPCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.XilinkDynamicLinker(for_machine, [], version=version)
+ return cls(
+ compiler, version, for_machine, is_cross, info, target,
+ exe_wrap, linker=linker)
+ if 'Microsoft' in out or 'Microsoft' in err:
+ # Latest versions of Visual Studio print version
+ # number to stderr but earlier ones print version
+ # on stdout. Why? Lord only knows.
+ # Check both outputs to figure out version.
+ for lookat in [err, out]:
+ version = search_version(lookat)
+ if version != 'unknown version':
+ break
+ else:
+ raise EnvironmentException(f'Failed to detect MSVC compiler version: stderr was\n{err!r}')
+ cl_signature = lookat.split('\n', maxsplit=1)[0]
+ match = re.search(r'.*(x86|x64|ARM|ARM64)([^_A-Za-z0-9]|$)', cl_signature)
+ if match:
+ target = match.group(1)
+ else:
+ m = f'Failed to detect MSVC compiler target architecture: \'cl /?\' output is\n{cl_signature}'
+ raise EnvironmentException(m)
+ cls = c.VisualStudioCCompiler if lang == 'c' else cpp.VisualStudioCPPCompiler
+ linker = guess_win_linker(env, ['link'], cls, version, for_machine)
+ # As of this writing, CCache does not support MSVC but sccache does.
+ if 'sccache' not in ccache:
+ ccache = []
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info, target,
+ exe_wrap, full_version=cl_signature, linker=linker)
+ if 'PGI Compilers' in out:
+ cls = c.PGICCompiler if lang == 'c' else cpp.PGICPPCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.PGIDynamicLinker(compiler, for_machine, cls.LINKER_PREFIX, [], version=version)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross,
+ info, exe_wrap, linker=linker)
+ if 'NVIDIA Compilers and Tools' in out:
+ cls = c.NvidiaHPC_CCompiler if lang == 'c' else cpp.NvidiaHPC_CPPCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.NvidiaHPC_DynamicLinker(compiler, for_machine, cls.LINKER_PREFIX, [], version=version)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross,
+ info, exe_wrap, linker=linker)
+ if '(ICC)' in out:
+ cls = c.IntelCCompiler if lang == 'c' else cpp.IntelCPPCompiler
+ l = guess_nix_linker(env, compiler, cls, version, for_machine)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=l)
+ if 'Intel(R) oneAPI' in out:
+ cls = c.IntelLLVMCCompiler if lang == 'c' else cpp.IntelLLVMCPPCompiler
+ l = guess_nix_linker(env, compiler, cls, version, for_machine)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=l)
+ if 'TMS320C2000 C/C++' in out or 'MSP430 C/C++' in out or 'TI ARM C/C++ Compiler' in out:
+ lnk: T.Union[T.Type[linkers.C2000DynamicLinker], T.Type[linkers.TIDynamicLinker]]
+ if 'TMS320C2000 C/C++' in out:
+ cls = c.C2000CCompiler if lang == 'c' else cpp.C2000CPPCompiler
+ lnk = linkers.C2000DynamicLinker
+ else:
+ cls = c.TICCompiler if lang == 'c' else cpp.TICPPCompiler
+ lnk = linkers.TIDynamicLinker
+
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = lnk(compiler, for_machine, version=version)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+ if 'ARM' in out:
+ cls = c.ArmCCompiler if lang == 'c' else cpp.ArmCPPCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.ArmDynamicLinker(for_machine, version=version)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross,
+ info, exe_wrap, full_version=full_version, linker=linker)
+ if 'RX Family' in out:
+ cls = c.CcrxCCompiler if lang == 'c' else cpp.CcrxCPPCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.CcrxDynamicLinker(for_machine, version=version)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+
+ if 'Microchip Technology' in out:
+ cls = c.Xc16CCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.Xc16DynamicLinker(for_machine, version=version)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+
+ if 'CompCert' in out:
+ cls = c.CompCertCCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.CompCertDynamicLinker(for_machine, version=version)
+ return cls(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+
+ _handle_exceptions(popen_exceptions, compilers)
+ raise EnvironmentException(f'Unknown compiler {compilers}')
+
+def detect_c_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ return _detect_c_or_cpp_compiler(env, 'c', for_machine)
+
+def detect_cpp_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ return _detect_c_or_cpp_compiler(env, 'cpp', for_machine)
+
+def detect_cuda_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ from .cuda import CudaCompiler
+ from ..linkers.linkers import CudaLinker
+ popen_exceptions = {}
+ is_cross = env.is_cross_build(for_machine)
+ compilers, ccache, exe_wrap = _get_compilers(env, 'cuda', for_machine)
+ info = env.machines[for_machine]
+ for compiler in compilers:
+ arg = '--version'
+ try:
+ p, out, err = Popen_safe(compiler + [arg])
+ except OSError as e:
+ popen_exceptions[join_args(compiler + [arg])] = e
+ continue
+ # Example nvcc printout:
+ #
+ # nvcc: NVIDIA (R) Cuda compiler driver
+ # Copyright (c) 2005-2018 NVIDIA Corporation
+ # Built on Sat_Aug_25_21:08:01_CDT_2018
+ # Cuda compilation tools, release 10.0, V10.0.130
+ #
+ # search_version() first finds the "10.0" after "release",
+ # rather than the more precise "10.0.130" after "V".
+ # The patch version number is occasionally important; For
+ # instance, on Linux,
+ # - CUDA Toolkit 8.0.44 requires NVIDIA Driver 367.48
+ # - CUDA Toolkit 8.0.61 requires NVIDIA Driver 375.26
+ # Luckily, the "V" also makes it very simple to extract
+ # the full version:
+ version = out.strip().rsplit('V', maxsplit=1)[-1]
+ cpp_compiler = detect_cpp_compiler(env, for_machine)
+ cls = CudaCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = CudaLinker(compiler, for_machine, CudaCompiler.LINKER_PREFIX, [], version=CudaLinker.parse_version())
+ return cls(ccache, compiler, version, for_machine, is_cross, exe_wrap, host_compiler=cpp_compiler, info=info, linker=linker)
+ raise EnvironmentException(f'Could not find suitable CUDA compiler: "{"; ".join([" ".join(c) for c in compilers])}"')
+
+def detect_fortran_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ from . import fortran
+ from ..linkers import linkers
+ popen_exceptions: T.Dict[str, T.Union[Exception, str]] = {}
+ compilers, ccache, exe_wrap = _get_compilers(env, 'fortran', for_machine)
+ is_cross = env.is_cross_build(for_machine)
+ info = env.machines[for_machine]
+ cls: T.Type[FortranCompiler]
+ for compiler in compilers:
+ for arg in ['--version', '-V']:
+ try:
+ p, out, err = Popen_safe(compiler + [arg])
+ except OSError as e:
+ popen_exceptions[join_args(compiler + [arg])] = e
+ continue
+
+ version = search_version(out)
+ full_version = out.split('\n', 1)[0]
+
+ guess_gcc_or_lcc: T.Optional[str] = None
+ if 'GNU Fortran' in out:
+ guess_gcc_or_lcc = 'gcc'
+ if 'e2k' in out and 'lcc' in out:
+ guess_gcc_or_lcc = 'lcc'
+
+ if guess_gcc_or_lcc:
+ defines = _get_gnu_compiler_defines(compiler)
+ if not defines:
+ popen_exceptions[join_args(compiler)] = 'no pre-processor defines'
+ continue
+ if guess_gcc_or_lcc == 'lcc':
+ version = _get_lcc_version_from_defines(defines)
+ cls = fortran.ElbrusFortranCompiler
+ linker = guess_nix_linker(env, compiler, cls, version, for_machine)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, defines, full_version=full_version, linker=linker)
+ else:
+ version = _get_gnu_version_from_defines(defines)
+ cls = fortran.GnuFortranCompiler
+ linker = guess_nix_linker(env, compiler, cls, version, for_machine)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, defines, full_version=full_version, linker=linker)
+
+ if 'Arm C/C++/Fortran Compiler' in out:
+ cls = fortran.ArmLtdFlangFortranCompiler
+ arm_ver_match = re.search(r'version (\d+)\.(\d+)\.?(\d+)? \(build number (\d+)\)', out)
+ assert arm_ver_match is not None, 'for mypy' # because mypy *should* be complaning that this could be None
+ version = '.'.join([x for x in arm_ver_match.groups() if x is not None])
+ linker = guess_nix_linker(env, compiler, cls, version, for_machine)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, linker=linker)
+ if 'G95' in out:
+ cls = fortran.G95FortranCompiler
+ linker = guess_nix_linker(env, compiler, cls, version, for_machine)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+
+ if 'Sun Fortran' in err:
+ version = search_version(err)
+ cls = fortran.SunFortranCompiler
+ linker = guess_nix_linker(env, compiler, cls, version, for_machine)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+
+ if 'Intel(R) Fortran Compiler for applications' in err:
+ version = search_version(err)
+ target = 'x86' if 'IA-32' in err else 'x86_64'
+ cls = fortran.IntelLLVMClFortranCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.XilinkDynamicLinker(for_machine, [], version=version)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ target, exe_wrap, linker=linker)
+
+ if 'Intel(R) Visual Fortran' in err or 'Intel(R) Fortran' in err:
+ version = search_version(err)
+ target = 'x86' if 'IA-32' in err else 'x86_64'
+ cls = fortran.IntelClFortranCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.XilinkDynamicLinker(for_machine, [], version=version)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ target, exe_wrap, linker=linker)
+
+ if 'ifort (IFORT)' in out:
+ cls = fortran.IntelFortranCompiler
+ linker = guess_nix_linker(env, compiler, cls, version, for_machine)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+
+ if 'ifx (IFORT)' in out:
+ cls = fortran.IntelLLVMFortranCompiler
+ linker = guess_nix_linker(env, compiler, cls, version, for_machine)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+
+ if 'PathScale EKOPath(tm)' in err:
+ return fortran.PathScaleFortranCompiler(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version)
+
+ if 'PGI Compilers' in out:
+ cls = fortran.PGIFortranCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.PGIDynamicLinker(compiler, for_machine,
+ cls.LINKER_PREFIX, [], version=version)
+ return cls(
+ compiler, version, for_machine, is_cross, info, exe_wrap,
+ full_version=full_version, linker=linker)
+
+ if 'NVIDIA Compilers and Tools' in out:
+ cls = fortran.NvidiaHPC_FortranCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.PGIDynamicLinker(compiler, for_machine,
+ cls.LINKER_PREFIX, [], version=version)
+ return cls(
+ compiler, version, for_machine, is_cross, info, exe_wrap,
+ full_version=full_version, linker=linker)
+
+ if 'flang' in out or 'clang' in out:
+ cls = fortran.FlangFortranCompiler
+ linker = guess_nix_linker(env,
+ compiler, cls, version, for_machine)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+
+ if 'Open64 Compiler Suite' in err:
+ cls = fortran.Open64FortranCompiler
+ linker = guess_nix_linker(env,
+ compiler, cls, version, for_machine)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+
+ if 'NAG Fortran' in err:
+ full_version = err.split('\n', 1)[0]
+ version = full_version.split()[-1]
+ cls = fortran.NAGFortranCompiler
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ linker = linkers.NAGDynamicLinker(
+ compiler, for_machine, cls.LINKER_PREFIX, [],
+ version=version)
+ return cls(
+ compiler, version, for_machine, is_cross, info,
+ exe_wrap, full_version=full_version, linker=linker)
+
+ _handle_exceptions(popen_exceptions, compilers)
+ raise EnvironmentException('Unreachable code (exception to make mypy happy)')
+
+def detect_objc_compiler(env: 'Environment', for_machine: MachineChoice) -> 'Compiler':
+ return _detect_objc_or_objcpp_compiler(env, 'objc', for_machine)
+
+def detect_objcpp_compiler(env: 'Environment', for_machine: MachineChoice) -> 'Compiler':
+ return _detect_objc_or_objcpp_compiler(env, 'objcpp', for_machine)
+
+def _detect_objc_or_objcpp_compiler(env: 'Environment', lang: str, for_machine: MachineChoice) -> 'Compiler':
+ from . import objc, objcpp
+ popen_exceptions: T.Dict[str, T.Union[Exception, str]] = {}
+ compilers, ccache, exe_wrap = _get_compilers(env, lang, for_machine)
+ is_cross = env.is_cross_build(for_machine)
+ info = env.machines[for_machine]
+ comp: T.Union[T.Type[objc.ObjCCompiler], T.Type[objcpp.ObjCPPCompiler]]
+
+ for compiler in compilers:
+ arg = ['--version']
+ try:
+ p, out, err = Popen_safe(compiler + arg)
+ except OSError as e:
+ popen_exceptions[join_args(compiler + arg)] = e
+ continue
+ version = search_version(out)
+ if 'Free Software Foundation' in out:
+ defines = _get_gnu_compiler_defines(compiler)
+ if not defines:
+ popen_exceptions[join_args(compiler)] = 'no pre-processor defines'
+ continue
+ version = _get_gnu_version_from_defines(defines)
+ comp = objc.GnuObjCCompiler if lang == 'objc' else objcpp.GnuObjCPPCompiler
+ linker = guess_nix_linker(env, compiler, comp, version, for_machine)
+ return comp(
+ ccache, compiler, version, for_machine, is_cross, info,
+ exe_wrap, defines, linker=linker)
+ if 'clang' in out:
+ linker = None
+ defines = _get_clang_compiler_defines(compiler)
+ if not defines:
+ popen_exceptions[join_args(compiler)] = 'no pre-processor defines'
+ continue
+ if 'Apple' in out:
+ comp = objc.AppleClangObjCCompiler if lang == 'objc' else objcpp.AppleClangObjCPPCompiler
+ else:
+ comp = objc.ClangObjCCompiler if lang == 'objc' else objcpp.ClangObjCPPCompiler
+ if 'windows' in out or env.machines[for_machine].is_windows():
+ # If we're in a MINGW context this actually will use a gnu style ld
+ try:
+ linker = guess_win_linker(env, compiler, comp, version, for_machine)
+ except MesonException:
+ pass
+
+ if not linker:
+ linker = guess_nix_linker(env, compiler, comp, version, for_machine)
+ return comp(
+ ccache, compiler, version, for_machine,
+ is_cross, info, exe_wrap, linker=linker, defines=defines)
+ _handle_exceptions(popen_exceptions, compilers)
+ raise EnvironmentException('Unreachable code (exception to make mypy happy)')
+
+def detect_java_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ from .java import JavaCompiler
+ exelist = env.lookup_binary_entry(for_machine, 'java')
+ info = env.machines[for_machine]
+ if exelist is None:
+ # TODO support fallback
+ exelist = [defaults['java'][0]]
+
+ try:
+ p, out, err = Popen_safe(exelist + ['-version'])
+ except OSError:
+ raise EnvironmentException('Could not execute Java compiler: {}'.format(join_args(exelist)))
+ if 'javac' in out or 'javac' in err:
+ version = search_version(err if 'javac' in err else out)
+ if not version or version == 'unknown version':
+ parts = (err if 'javac' in err else out).split()
+ if len(parts) > 1:
+ version = parts[1]
+ comp_class = JavaCompiler
+ env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env)
+ return comp_class(exelist, version, for_machine, info)
+ raise EnvironmentException('Unknown compiler: ' + join_args(exelist))
+
+def detect_cs_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ from . import cs
+ compilers, ccache, exe_wrap = _get_compilers(env, 'cs', for_machine)
+ popen_exceptions = {}
+ info = env.machines[for_machine]
+ for comp in compilers:
+ try:
+ p, out, err = Popen_safe(comp + ['--version'])
+ except OSError as e:
+ popen_exceptions[join_args(comp + ['--version'])] = e
+ continue
+
+ version = search_version(out)
+ cls: T.Type[cs.CsCompiler]
+ if 'Mono' in out:
+ cls = cs.MonoCompiler
+ elif "Visual C#" in out:
+ cls = cs.VisualStudioCsCompiler
+ else:
+ continue
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ return cls(comp, version, for_machine, info)
+
+ _handle_exceptions(popen_exceptions, compilers)
+ raise EnvironmentException('Unreachable code (exception to make mypy happy)')
+
+def detect_cython_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ """Search for a cython compiler."""
+ from .cython import CythonCompiler
+ compilers, _, _ = _get_compilers(env, 'cython', MachineChoice.BUILD)
+ is_cross = env.is_cross_build(for_machine)
+ info = env.machines[for_machine]
+
+ popen_exceptions: T.Dict[str, Exception] = {}
+ for comp in compilers:
+ try:
+ err = Popen_safe(comp + ['-V'])[2]
+ except OSError as e:
+ popen_exceptions[join_args(comp + ['-V'])] = e
+ continue
+
+ version = search_version(err)
+ if 'Cython' in err:
+ comp_class = CythonCompiler
+ env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env)
+ return comp_class([], comp, version, for_machine, info, is_cross=is_cross)
+ _handle_exceptions(popen_exceptions, compilers)
+ raise EnvironmentException('Unreachable code (exception to make mypy happy)')
+
+def detect_vala_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ from .vala import ValaCompiler
+ exelist = env.lookup_binary_entry(MachineChoice.BUILD, 'vala')
+ is_cross = env.is_cross_build(for_machine)
+ info = env.machines[for_machine]
+ if exelist is None:
+ # TODO support fallback
+ exelist = [defaults['vala'][0]]
+
+ try:
+ p, out = Popen_safe(exelist + ['--version'])[0:2]
+ except OSError:
+ raise EnvironmentException('Could not execute Vala compiler: {}'.format(join_args(exelist)))
+ version = search_version(out)
+ if 'Vala' in out:
+ comp_class = ValaCompiler
+ env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env)
+ return comp_class(exelist, version, for_machine, is_cross, info)
+ raise EnvironmentException('Unknown compiler: ' + join_args(exelist))
+
+def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> RustCompiler:
+ from . import rust
+ from ..linkers import linkers
+ popen_exceptions = {} # type: T.Dict[str, Exception]
+ compilers, _, exe_wrap = _get_compilers(env, 'rust', for_machine)
+ is_cross = env.is_cross_build(for_machine)
+ info = env.machines[for_machine]
+
+ cc = detect_c_compiler(env, for_machine)
+ is_link_exe = isinstance(cc.linker, linkers.VisualStudioLikeLinkerMixin)
+ override = env.lookup_binary_entry(for_machine, 'rust_ld')
+
+ for compiler in compilers:
+ arg = ['--version']
+ try:
+ out = Popen_safe(compiler + arg)[1]
+ except OSError as e:
+ popen_exceptions[join_args(compiler + arg)] = e
+ continue
+
+ version = search_version(out)
+ cls: T.Type[RustCompiler] = rust.RustCompiler
+
+ # Clippy is a wrapper around rustc, but it doesn't have rustc in it's
+ # output. We can otherwise treat it as rustc.
+ if 'clippy' in out:
+ out = 'rustc'
+ cls = rust.ClippyRustCompiler
+
+ if 'rustc' in out:
+ # On Linux and mac rustc will invoke gcc (clang for mac
+ # presumably) and it can do this windows, for dynamic linking.
+ # this means the easiest way to C compiler for dynamic linking.
+ # figure out what linker to use is to just get the value of the
+ # C compiler and use that as the basis of the rust linker.
+ # However, there are two things we need to change, if CC is not
+ # the default use that, and second add the necessary arguments
+ # to rust to use -fuse-ld
+
+ if any(a.startswith('linker=') for a in compiler):
+ mlog.warning(
+ 'Please do not put -C linker= in your compiler '
+ 'command, set rust_ld=command in your cross file '
+ 'or use the RUST_LD environment variable, otherwise meson '
+ 'will override your selection.')
+
+ compiler = compiler.copy() # avoid mutating the original list
+
+ if override is None:
+ extra_args: T.Dict[str, T.Union[str, bool]] = {}
+ always_args: T.List[str] = []
+ if is_link_exe:
+ compiler.extend(cls.use_linker_args(cc.linker.exelist[0], ''))
+ extra_args['direct'] = True
+ extra_args['machine'] = cc.linker.machine
+ else:
+ exelist = cc.linker.exelist + cc.linker.get_always_args()
+ if 'ccache' in exelist[0]:
+ del exelist[0]
+ c = exelist.pop(0)
+ compiler.extend(cls.use_linker_args(c, ''))
+
+ # Also ensure that we pass any extra arguments to the linker
+ for l in exelist:
+ compiler.extend(['-C', f'link-arg={l}'])
+
+ # This trickery with type() gets us the class of the linker
+ # so we can initialize a new copy for the Rust Compiler
+ # TODO rewrite this without type: ignore
+ assert cc.linker is not None, 'for mypy'
+ if is_link_exe:
+ linker = type(cc.linker)(for_machine, always_args, exelist=cc.linker.exelist, # type: ignore
+ version=cc.linker.version, **extra_args) # type: ignore
+ else:
+ linker = type(cc.linker)(compiler, for_machine, cc.LINKER_PREFIX,
+ always_args=always_args, version=cc.linker.version,
+ **extra_args)
+ elif 'link' in override[0]:
+ linker = guess_win_linker(env,
+ override, cls, version, for_machine, use_linker_prefix=False)
+ # rustc takes linker arguments without a prefix, and
+ # inserts the correct prefix itself.
+ assert isinstance(linker, linkers.VisualStudioLikeLinkerMixin)
+ linker.direct = True
+ compiler.extend(cls.use_linker_args(linker.exelist[0], ''))
+ else:
+ # On linux and macos rust will invoke the c compiler for
+ # linking, on windows it will use lld-link or link.exe.
+ # we will simply ask for the C compiler that corresponds to
+ # it, and use that.
+ cc = _detect_c_or_cpp_compiler(env, 'c', for_machine, override_compiler=override)
+ linker = cc.linker
+
+ # Of course, we're not going to use any of that, we just
+ # need it to get the proper arguments to pass to rustc
+ c = linker.exelist[1] if linker.exelist[0].endswith('ccache') else linker.exelist[0]
+ compiler.extend(cls.use_linker_args(c, ''))
+
+ env.coredata.add_lang_args(cls.language, cls, for_machine, env)
+ return cls(
+ compiler, version, for_machine, is_cross, info, exe_wrap,
+ linker=linker)
+
+ _handle_exceptions(popen_exceptions, compilers)
+ raise EnvironmentException('Unreachable code (exception to make mypy happy)')
+
+def detect_d_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ from . import c, d
+ info = env.machines[for_machine]
+
+ # Detect the target architecture, required for proper architecture handling on Windows.
+ # MSVC compiler is required for correct platform detection.
+ c_compiler = {'c': detect_c_compiler(env, for_machine)}
+ is_msvc = isinstance(c_compiler['c'], c.VisualStudioCCompiler)
+ if not is_msvc:
+ c_compiler = {}
+
+ # Import here to avoid circular imports
+ from ..environment import detect_cpu_family
+ arch = detect_cpu_family(c_compiler)
+ if is_msvc and arch == 'x86':
+ arch = 'x86_mscoff'
+
+ popen_exceptions = {}
+ is_cross = env.is_cross_build(for_machine)
+ compilers, ccache, exe_wrap = _get_compilers(env, 'd', for_machine)
+ cls: T.Type[d.DCompiler]
+ for exelist in compilers:
+ # Search for a D compiler.
+ # We prefer LDC over GDC unless overridden with the DC
+ # environment variable because LDC has a much more
+ # up to date language version at time (2016).
+ if os.path.basename(exelist[-1]).startswith(('ldmd', 'gdmd')):
+ raise EnvironmentException(
+ f'Meson does not support {exelist[-1]} as it is only a DMD frontend for another compiler.'
+ 'Please provide a valid value for DC or unset it so that Meson can resolve the compiler by itself.')
+ try:
+ p, out = Popen_safe(exelist + ['--version'])[0:2]
+ except OSError as e:
+ popen_exceptions[join_args(exelist + ['--version'])] = e
+ continue
+ version = search_version(out)
+ full_version = out.split('\n', 1)[0]
+
+ if 'LLVM D compiler' in out:
+ cls = d.LLVMDCompiler
+ # LDC seems to require a file
+ # We cannot use NamedTemproraryFile on windows, its documented
+ # to not work for our uses. So, just use mkstemp and only have
+ # one path for simplicity.
+ o, f = tempfile.mkstemp('.d')
+ os.close(o)
+
+ try:
+ if info.is_windows() or info.is_cygwin():
+ objfile = os.path.basename(f)[:-1] + 'obj'
+ linker = guess_win_linker(env,
+ exelist,
+ cls, full_version, for_machine,
+ use_linker_prefix=True, invoked_directly=False,
+ extra_args=[f])
+ else:
+ # LDC writes an object file to the current working directory.
+ # Clean it up.
+ objfile = os.path.basename(f)[:-1] + 'o'
+ linker = guess_nix_linker(env,
+ exelist, cls, full_version, for_machine,
+ extra_args=[f])
+ finally:
+ windows_proof_rm(f)
+ windows_proof_rm(objfile)
+
+ return cls(
+ exelist, version, for_machine, info, arch,
+ full_version=full_version, linker=linker, version_output=out)
+ elif 'gdc' in out:
+ cls = d.GnuDCompiler
+ linker = guess_nix_linker(env, exelist, cls, version, for_machine)
+ return cls(
+ exelist, version, for_machine, info, arch,
+ exe_wrapper=exe_wrap, is_cross=is_cross,
+ full_version=full_version, linker=linker)
+ elif 'The D Language Foundation' in out or 'Digital Mars' in out:
+ cls = d.DmdDCompiler
+ # DMD seems to require a file
+ # We cannot use NamedTemproraryFile on windows, its documented
+ # to not work for our uses. So, just use mkstemp and only have
+ # one path for simplicity.
+ o, f = tempfile.mkstemp('.d')
+ os.close(o)
+
+ # DMD as different detection logic for x86 and x86_64
+ arch_arg = '-m64' if arch == 'x86_64' else '-m32'
+
+ try:
+ if info.is_windows() or info.is_cygwin():
+ objfile = os.path.basename(f)[:-1] + 'obj'
+ linker = guess_win_linker(env,
+ exelist, cls, full_version, for_machine,
+ invoked_directly=False, extra_args=[f, arch_arg])
+ else:
+ objfile = os.path.basename(f)[:-1] + 'o'
+ linker = guess_nix_linker(env,
+ exelist, cls, full_version, for_machine,
+ extra_args=[f, arch_arg])
+ finally:
+ windows_proof_rm(f)
+ windows_proof_rm(objfile)
+
+ return cls(
+ exelist, version, for_machine, info, arch,
+ full_version=full_version, linker=linker)
+ raise EnvironmentException('Unknown compiler: ' + join_args(exelist))
+
+ _handle_exceptions(popen_exceptions, compilers)
+ raise EnvironmentException('Unreachable code (exception to make mypy happy)')
+
+def detect_swift_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ from .swift import SwiftCompiler
+ exelist = env.lookup_binary_entry(for_machine, 'swift')
+ is_cross = env.is_cross_build(for_machine)
+ info = env.machines[for_machine]
+ if exelist is None:
+ # TODO support fallback
+ exelist = [defaults['swift'][0]]
+
+ try:
+ p, _, err = Popen_safe(exelist + ['-v'])
+ except OSError:
+ raise EnvironmentException('Could not execute Swift compiler: {}'.format(join_args(exelist)))
+ version = search_version(err)
+ if 'Swift' in err:
+ # As for 5.0.1 swiftc *requires* a file to check the linker:
+ with tempfile.NamedTemporaryFile(suffix='.swift') as f:
+ cls = SwiftCompiler
+ linker = guess_nix_linker(env,
+ exelist, cls, version, for_machine,
+ extra_args=[f.name])
+ return cls(
+ exelist, version, for_machine, is_cross, info, linker=linker)
+
+ raise EnvironmentException('Unknown compiler: ' + join_args(exelist))
+
+def detect_nasm_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ from .asm import NasmCompiler, YasmCompiler
+ compilers, _, _ = _get_compilers(env, 'nasm', for_machine)
+ is_cross = env.is_cross_build(for_machine)
+
+ # We need a C compiler to properly detect the machine info and linker
+ cc = detect_c_compiler(env, for_machine)
+ if not is_cross:
+ from ..environment import detect_machine_info
+ info = detect_machine_info({'c': cc})
+ else:
+ info = env.machines[for_machine]
+
+ popen_exceptions: T.Dict[str, Exception] = {}
+ for comp in compilers:
+ if comp == ['nasm'] and is_windows() and not shutil.which(comp[0]):
+ # nasm is not in PATH on Windows by default
+ default_path = os.path.join(os.environ['ProgramFiles'], 'NASM')
+ comp[0] = shutil.which(comp[0], path=default_path) or comp[0]
+ try:
+ output = Popen_safe(comp + ['--version'])[1]
+ except OSError as e:
+ popen_exceptions[' '.join(comp + ['--version'])] = e
+ continue
+
+ version = search_version(output)
+ if 'NASM' in output:
+ comp_class = NasmCompiler
+ env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env)
+ return comp_class([], comp, version, for_machine, info, cc.linker, is_cross=is_cross)
+ elif 'yasm' in output:
+ comp_class = YasmCompiler
+ env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env)
+ return comp_class([], comp, version, for_machine, info, cc.linker, is_cross=is_cross)
+ _handle_exceptions(popen_exceptions, compilers)
+ raise EnvironmentException('Unreachable code (exception to make mypy happy)')
+
+def detect_masm_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler:
+ # We need a C compiler to properly detect the machine info and linker
+ is_cross = env.is_cross_build(for_machine)
+ cc = detect_c_compiler(env, for_machine)
+ if not is_cross:
+ from ..environment import detect_machine_info
+ info = detect_machine_info({'c': cc})
+ else:
+ info = env.machines[for_machine]
+
+ from .asm import MasmCompiler, MasmARMCompiler
+ comp_class: T.Type[Compiler]
+ if info.cpu_family == 'x86':
+ comp = ['ml']
+ comp_class = MasmCompiler
+ arg = '/?'
+ elif info.cpu_family == 'x86_64':
+ comp = ['ml64']
+ comp_class = MasmCompiler
+ arg = '/?'
+ elif info.cpu_family == 'arm':
+ comp = ['armasm']
+ comp_class = MasmARMCompiler
+ arg = '-h'
+ elif info.cpu_family == 'aarch64':
+ comp = ['armasm64']
+ comp_class = MasmARMCompiler
+ arg = '-h'
+ else:
+ raise EnvironmentException(f'Platform {info.cpu_family} not supported by MASM')
+
+ popen_exceptions: T.Dict[str, Exception] = {}
+ try:
+ output = Popen_safe(comp + [arg])[2]
+ version = search_version(output)
+ env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env)
+ return comp_class([], comp, version, for_machine, info, cc.linker, is_cross=is_cross)
+ except OSError as e:
+ popen_exceptions[' '.join(comp + [arg])] = e
+ _handle_exceptions(popen_exceptions, [comp])
+ raise EnvironmentException('Unreachable code (exception to make mypy happy)')
+
+# GNU/Clang defines and version
+# =============================
+
+def _get_gnu_compiler_defines(compiler: T.List[str]) -> T.Dict[str, str]:
+ """
+ Detect GNU compiler platform type (Apple, MinGW, Unix)
+ """
+ # Arguments to output compiler pre-processor defines to stdout
+ # gcc, g++, and gfortran all support these arguments
+ args = compiler + ['-E', '-dM', '-']
+ mlog.debug(f'Running command: {join_args(args)}')
+ p, output, error = Popen_safe(args, write='', stdin=subprocess.PIPE)
+ if p.returncode != 0:
+ raise EnvironmentException('Unable to detect GNU compiler type:\n'
+ f'Compiler stdout:\n{output}\n-----\n'
+ f'Compiler stderr:\n{error}\n-----\n')
+ # Parse several lines of the type:
+ # `#define ___SOME_DEF some_value`
+ # and extract `___SOME_DEF`
+ defines: T.Dict[str, str] = {}
+ for line in output.split('\n'):
+ if not line:
+ continue
+ d, *rest = line.split(' ', 2)
+ if d != '#define':
+ continue
+ if len(rest) == 1:
+ defines[rest[0]] = ''
+ if len(rest) == 2:
+ defines[rest[0]] = rest[1]
+ return defines
+
+def _get_clang_compiler_defines(compiler: T.List[str]) -> T.Dict[str, str]:
+ """
+ Get the list of Clang pre-processor defines
+ """
+ args = compiler + ['-E', '-dM', '-']
+ mlog.debug(f'Running command: {join_args(args)}')
+ p, output, error = Popen_safe(args, write='', stdin=subprocess.PIPE)
+ if p.returncode != 0:
+ raise EnvironmentException('Unable to get clang pre-processor defines:\n'
+ f'Compiler stdout:\n{output}\n-----\n'
+ f'Compiler stderr:\n{error}\n-----\n')
+ defines: T.Dict[str, str] = {}
+ for line in output.split('\n'):
+ if not line:
+ continue
+ d, *rest = line.split(' ', 2)
+ if d != '#define':
+ continue
+ if len(rest) == 1:
+ defines[rest[0]] = ''
+ if len(rest) == 2:
+ defines[rest[0]] = rest[1]
+ return defines
+
+def _get_gnu_version_from_defines(defines: T.Dict[str, str]) -> str:
+ dot = '.'
+ major = defines.get('__GNUC__', '0')
+ minor = defines.get('__GNUC_MINOR__', '0')
+ patch = defines.get('__GNUC_PATCHLEVEL__', '0')
+ return dot.join((major, minor, patch))
+
+def _get_lcc_version_from_defines(defines: T.Dict[str, str]) -> str:
+ dot = '.'
+ generation_and_major = defines.get('__LCC__', '100')
+ generation = generation_and_major[:1]
+ major = generation_and_major[1:]
+ minor = defines.get('__LCC_MINOR__', '0')
+ return dot.join((generation, major, minor))
diff --git a/mesonbuild/compilers/fortran.py b/mesonbuild/compilers/fortran.py
new file mode 100644
index 0000000..90ca010
--- /dev/null
+++ b/mesonbuild/compilers/fortran.py
@@ -0,0 +1,546 @@
+# 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.
+from __future__ import annotations
+
+import typing as T
+import os
+
+from .. import coredata
+from .compilers import (
+ clike_debug_args,
+ Compiler,
+)
+from .mixins.clike import CLikeCompiler
+from .mixins.gnu import (
+ GnuCompiler, gnulike_buildtype_args, gnu_optimization_args
+)
+from .mixins.intel import IntelGnuLikeCompiler, IntelVisualStudioLikeCompiler
+from .mixins.clang import ClangCompiler
+from .mixins.elbrus import ElbrusCompiler
+from .mixins.pgi import PGICompiler
+
+from mesonbuild.mesonlib import (
+ version_compare, MesonException,
+ LibType, OptionKey,
+)
+
+if T.TYPE_CHECKING:
+ from ..coredata import MutableKeyedOptionDictType, KeyedOptionDictType
+ from ..dependencies import Dependency
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..linkers import DynamicLinker
+ from ..mesonlib import MachineChoice
+ from ..programs import ExternalProgram
+ from .compilers import CompileCheckMode
+
+
+class FortranCompiler(CLikeCompiler, Compiler):
+
+ language = 'fortran'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ Compiler.__init__(self, [], exelist, version, for_machine, info,
+ is_cross=is_cross, full_version=full_version, linker=linker)
+ CLikeCompiler.__init__(self, exe_wrapper)
+
+ def has_function(self, funcname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Optional[T.List[str]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ raise MesonException('Fortran does not have "has_function" capability.\n'
+ 'It is better to test if a Fortran capability is working like:\n\n'
+ "meson.get_compiler('fortran').links('block; end block; end program')\n\n"
+ 'that example is to see if the compiler has Fortran 2008 Block element.')
+
+ def _get_basic_compiler_args(self, env: 'Environment', mode: CompileCheckMode) -> T.Tuple[T.List[str], T.List[str]]:
+ cargs = env.coredata.get_external_args(self.for_machine, self.language)
+ largs = env.coredata.get_external_link_args(self.for_machine, self.language)
+ return cargs, largs
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ source_name = 'sanitycheckf.f90'
+ code = 'program main; print *, "Fortran compilation is working."; end program\n'
+ return self._sanity_check_impl(work_dir, environment, source_name, code)
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return gnulike_buildtype_args[buildtype]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return gnu_optimization_args[optimization_level]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return clike_debug_args[is_debug]
+
+ def get_preprocess_only_args(self) -> T.List[str]:
+ return ['-cpp'] + super().get_preprocess_only_args()
+
+ def get_module_incdir_args(self) -> T.Tuple[str, ...]:
+ return ('-I', )
+
+ def get_module_outdir_args(self, path: str) -> T.List[str]:
+ return ['-module', path]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I' or i[:2] == '-L':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+
+ return parameter_list
+
+ def module_name_to_filename(self, module_name: str) -> str:
+ if '_' in module_name: # submodule
+ s = module_name.lower()
+ if self.id in {'gcc', 'intel', 'intel-cl'}:
+ filename = s.replace('_', '@') + '.smod'
+ elif self.id in {'pgi', 'flang'}:
+ filename = s.replace('_', '-') + '.mod'
+ else:
+ filename = s + '.mod'
+ else: # module
+ filename = module_name.lower() + '.mod'
+
+ return filename
+
+ def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str],
+ libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]:
+ code = 'stop; end program'
+ return self._find_library_impl(libname, env, extra_dirs, code, libtype)
+
+ def has_multi_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]:
+ return self._has_multi_arguments(args, env, 'stop; end program')
+
+ def has_multi_link_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]:
+ return self._has_multi_link_arguments(args, env, 'stop; end program')
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = super().get_options()
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts.update({
+ key: coredata.UserComboOption(
+ 'Fortran language standard to use',
+ ['none'],
+ 'none',
+ ),
+ })
+ return opts
+
+
+class GnuFortranCompiler(GnuCompiler, FortranCompiler):
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine,
+ is_cross, info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ GnuCompiler.__init__(self, defines)
+ default_warn_args = ['-Wall']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic', '-fimplicit-none'],
+ 'everything': default_warn_args + ['-Wextra', '-Wpedantic', '-fimplicit-none']}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = FortranCompiler.get_options(self)
+ fortran_stds = ['legacy', 'f95', 'f2003']
+ if version_compare(self.version, '>=4.4.0'):
+ fortran_stds += ['f2008']
+ if version_compare(self.version, '>=8.0.0'):
+ fortran_stds += ['f2018']
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none'] + fortran_stds
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append('-std=' + std.value)
+ return args
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ # Disabled until this is fixed:
+ # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=62162
+ # return ['-cpp', '-MD', '-MQ', outtarget]
+ return []
+
+ def get_module_outdir_args(self, path: str) -> T.List[str]:
+ return ['-J' + path]
+
+ def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]:
+ # We need to apply the search prefix here, as these link arguments may
+ # be passed to a different compiler with a different set of default
+ # search paths, such as when using Clang for C/C++ and gfortran for
+ # fortran,
+ search_dirs: T.List[str] = []
+ for d in self.get_compiler_dirs(env, 'libraries'):
+ search_dirs.append(f'-L{d}')
+ return search_dirs + ['-lgfortran', '-lm']
+
+ def has_header(self, hname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[['CompileCheckMode'], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None,
+ disable_cache: bool = False) -> T.Tuple[bool, bool]:
+ '''
+ Derived from mixins/clike.py:has_header, but without C-style usage of
+ __has_include which breaks with GCC-Fortran 10:
+ https://github.com/mesonbuild/meson/issues/7017
+ '''
+ code = f'{prefix}\n#include <{hname}>'
+ return self.compiles(code, env, extra_args=extra_args,
+ dependencies=dependencies, mode='preprocess', disable_cache=disable_cache)
+
+
+class ElbrusFortranCompiler(ElbrusCompiler, FortranCompiler):
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ ElbrusCompiler.__init__(self)
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = FortranCompiler.get_options(self)
+ fortran_stds = ['f95', 'f2003', 'f2008', 'gnu', 'legacy', 'f2008ts']
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none'] + fortran_stds
+ return opts
+
+ def get_module_outdir_args(self, path: str) -> T.List[str]:
+ return ['-J' + path]
+
+
+class G95FortranCompiler(FortranCompiler):
+
+ LINKER_PREFIX = '-Wl,'
+ id = 'g95'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine,
+ is_cross, info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ default_warn_args = ['-Wall']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-pedantic'],
+ 'everything': default_warn_args + ['-Wextra', '-pedantic']}
+
+ def get_module_outdir_args(self, path: str) -> T.List[str]:
+ return ['-fmod=' + path]
+
+ def get_no_warn_args(self) -> T.List[str]:
+ # FIXME: Confirm that there's no compiler option to disable all warnings
+ return []
+
+
+class SunFortranCompiler(FortranCompiler):
+
+ LINKER_PREFIX = '-Wl,'
+ id = 'sun'
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return ['-fpp']
+
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ return []
+
+ def get_module_incdir_args(self) -> T.Tuple[str, ...]:
+ return ('-M', )
+
+ def get_module_outdir_args(self, path: str) -> T.List[str]:
+ return ['-moddir=' + path]
+
+ def openmp_flags(self) -> T.List[str]:
+ return ['-xopenmp']
+
+
+class IntelFortranCompiler(IntelGnuLikeCompiler, FortranCompiler):
+
+ file_suffixes = ('f90', 'f', 'for', 'ftn', 'fpp', )
+ id = 'intel'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine,
+ is_cross, info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ # FIXME: Add support for OS X and Windows in detect_fortran_compiler so
+ # we are sent the type of compiler
+ IntelGnuLikeCompiler.__init__(self)
+ default_warn_args = ['-warn', 'general', '-warn', 'truncated_source']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-warn', 'unused'],
+ '3': ['-warn', 'all'],
+ 'everything': ['-warn', 'all']}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = FortranCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'legacy', 'f95', 'f2003', 'f2008', 'f2018']
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ stds = {'legacy': 'none', 'f95': 'f95', 'f2003': 'f03', 'f2008': 'f08', 'f2018': 'f18'}
+ if std.value != 'none':
+ args.append('-stand=' + stds[std.value])
+ return args
+
+ def get_preprocess_only_args(self) -> T.List[str]:
+ return ['-cpp', '-EP']
+
+ def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]:
+ # TODO: needs default search path added
+ return ['-lifcore', '-limf']
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return ['-gen-dep=' + outtarget, '-gen-depformat=make']
+
+
+class IntelLLVMFortranCompiler(IntelFortranCompiler):
+
+ id = 'intel-llvm'
+
+
+class IntelClFortranCompiler(IntelVisualStudioLikeCompiler, FortranCompiler):
+
+ file_suffixes = ('f90', 'f', 'for', 'ftn', 'fpp', )
+ always_args = ['/nologo']
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo', target: str,
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine,
+ is_cross, info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ IntelVisualStudioLikeCompiler.__init__(self, target)
+
+ default_warn_args = ['/warn:general', '/warn:truncated_source']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['/warn:unused'],
+ '3': ['/warn:all'],
+ 'everything': ['/warn:all']}
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ opts = FortranCompiler.get_options(self)
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ opts[key].choices = ['none', 'legacy', 'f95', 'f2003', 'f2008', 'f2018']
+ return opts
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ stds = {'legacy': 'none', 'f95': 'f95', 'f2003': 'f03', 'f2008': 'f08', 'f2018': 'f18'}
+ if std.value != 'none':
+ args.append('/stand:' + stds[std.value])
+ return args
+
+ def get_module_outdir_args(self, path: str) -> T.List[str]:
+ return ['/module:' + path]
+
+
+class IntelLLVMClFortranCompiler(IntelClFortranCompiler):
+
+ id = 'intel-llvm-cl'
+
+class PathScaleFortranCompiler(FortranCompiler):
+
+ id = 'pathscale'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine,
+ is_cross, info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ default_warn_args = ['-fullwarn']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args,
+ '3': default_warn_args,
+ 'everything': default_warn_args}
+
+ def openmp_flags(self) -> T.List[str]:
+ return ['-mp']
+
+
+class PGIFortranCompiler(PGICompiler, FortranCompiler):
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine,
+ is_cross, info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ PGICompiler.__init__(self)
+
+ default_warn_args = ['-Minform=inform']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args,
+ '3': default_warn_args + ['-Mdclchk'],
+ 'everything': default_warn_args + ['-Mdclchk']}
+
+ def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]:
+ # TODO: needs default search path added
+ return ['-lpgf90rtl', '-lpgf90', '-lpgf90_rpm1', '-lpgf902',
+ '-lpgf90rtl', '-lpgftnrtl', '-lrt']
+
+
+class NvidiaHPC_FortranCompiler(PGICompiler, FortranCompiler):
+
+ id = 'nvidia_hpc'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine,
+ is_cross, info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ PGICompiler.__init__(self)
+
+ default_warn_args = ['-Minform=inform']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args,
+ '3': default_warn_args + ['-Mdclchk'],
+ 'everything': default_warn_args + ['-Mdclchk']}
+
+
+class FlangFortranCompiler(ClangCompiler, FortranCompiler):
+
+ id = 'flang'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine,
+ is_cross, info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ ClangCompiler.__init__(self, {})
+ default_warn_args = ['-Minform=inform']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args,
+ '3': default_warn_args,
+ 'everything': default_warn_args}
+
+ def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]:
+ # We need to apply the search prefix here, as these link arguments may
+ # be passed to a different compiler with a different set of default
+ # search paths, such as when using Clang for C/C++ and gfortran for
+ # fortran,
+ # XXX: Untested....
+ search_dirs: T.List[str] = []
+ for d in self.get_compiler_dirs(env, 'libraries'):
+ search_dirs.append(f'-L{d}')
+ return search_dirs + ['-lflang', '-lpgmath']
+
+class ArmLtdFlangFortranCompiler(FlangFortranCompiler):
+
+ id = 'armltdflang'
+
+class Open64FortranCompiler(FortranCompiler):
+
+ id = 'open64'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine,
+ is_cross, info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ default_warn_args = ['-fullwarn']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args,
+ '3': default_warn_args,
+ 'everything': default_warn_args}
+
+ def openmp_flags(self) -> T.List[str]:
+ return ['-mp']
+
+
+class NAGFortranCompiler(FortranCompiler):
+
+ id = 'nagfor'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool,
+ info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ FortranCompiler.__init__(self, exelist, version, for_machine,
+ is_cross, info, exe_wrapper, linker=linker,
+ full_version=full_version)
+ # Warnings are on by default; -w disables (by category):
+ self.warn_args = {
+ '0': ['-w=all'],
+ '1': [],
+ '2': [],
+ '3': [],
+ 'everything': [],
+ }
+
+ def get_always_args(self) -> T.List[str]:
+ return self.get_nagfor_quiet(self.version)
+
+ def get_module_outdir_args(self, path: str) -> T.List[str]:
+ return ['-mdir', path]
+
+ @staticmethod
+ def get_nagfor_quiet(version: str) -> T.List[str]:
+ return ['-quiet'] if version_compare(version, '>=7100') else []
+
+ def get_pic_args(self) -> T.List[str]:
+ return ['-PIC']
+
+ def get_preprocess_only_args(self) -> T.List[str]:
+ return ['-fpp']
+
+ def get_std_exe_link_args(self) -> T.List[str]:
+ return self.get_always_args()
+
+ def openmp_flags(self) -> T.List[str]:
+ return ['-openmp']
diff --git a/mesonbuild/compilers/java.py b/mesonbuild/compilers/java.py
new file mode 100644
index 0000000..ebae509
--- /dev/null
+++ b/mesonbuild/compilers/java.py
@@ -0,0 +1,125 @@
+# 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.
+from __future__ import annotations
+
+import os
+import os.path
+import shutil
+import subprocess
+import textwrap
+import typing as T
+
+from ..mesonlib import EnvironmentException
+from .compilers import Compiler, java_buildtype_args
+from .mixins.islinker import BasicLinkerIsCompilerMixin
+
+if T.TYPE_CHECKING:
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..mesonlib import MachineChoice
+
+class JavaCompiler(BasicLinkerIsCompilerMixin, Compiler):
+
+ language = 'java'
+ id = 'unknown'
+
+ _WARNING_LEVELS: T.Dict[str, T.List[str]] = {
+ '0': ['-nowarn'],
+ '1': ['-Xlint:all'],
+ '2': ['-Xlint:all', '-Xdoclint:all'],
+ '3': ['-Xlint:all', '-Xdoclint:all'],
+ }
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ info: 'MachineInfo', full_version: T.Optional[str] = None):
+ super().__init__([], exelist, version, for_machine, info, full_version=full_version)
+ self.javarunner = 'java'
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ return self._WARNING_LEVELS[level]
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['-Werror']
+
+ def get_no_warn_args(self) -> T.List[str]:
+ return ['-nowarn']
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ if outputname == '':
+ outputname = './'
+ return ['-d', outputname, '-s', outputname]
+
+ def get_pic_args(self) -> T.List[str]:
+ return []
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ return []
+
+ def get_pch_name(self, name: str) -> str:
+ return ''
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return java_buildtype_args[buildtype]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i in {'-cp', '-classpath', '-sourcepath'} and idx + 1 < len(parameter_list):
+ path_list = parameter_list[idx + 1].split(os.pathsep)
+ path_list = [os.path.normpath(os.path.join(build_dir, x)) for x in path_list]
+ parameter_list[idx + 1] = os.pathsep.join(path_list)
+
+ return parameter_list
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ src = 'SanityCheck.java'
+ obj = 'SanityCheck'
+ source_name = os.path.join(work_dir, src)
+ with open(source_name, 'w', encoding='utf-8') as ofile:
+ ofile.write(textwrap.dedent(
+ '''class SanityCheck {
+ public static void main(String[] args) {
+ int i;
+ }
+ }
+ '''))
+ pc = subprocess.Popen(self.exelist + [src], cwd=work_dir)
+ pc.wait()
+ if pc.returncode != 0:
+ raise EnvironmentException(f'Java compiler {self.name_string()} can not compile programs.')
+ runner = shutil.which(self.javarunner)
+ if runner:
+ cmdlist = [runner, obj]
+ pe = subprocess.Popen(cmdlist, cwd=work_dir)
+ pe.wait()
+ if pe.returncode != 0:
+ raise EnvironmentException(f'Executables created by Java compiler {self.name_string()} are not runnable.')
+ else:
+ m = "Java Virtual Machine wasn't found, but it's needed by Meson. " \
+ "Please install a JRE.\nIf you have specific needs where this " \
+ "requirement doesn't make sense, please open a bug at " \
+ "https://github.com/mesonbuild/meson/issues/new and tell us " \
+ "all about it."
+ raise EnvironmentException(m)
+
+ def needs_static_linker(self) -> bool:
+ return False
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return []
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ if is_debug:
+ return ['-g']
+ return ['-g:none']
diff --git a/mesonbuild/compilers/mixins/__init__.py b/mesonbuild/compilers/mixins/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/mesonbuild/compilers/mixins/__init__.py
diff --git a/mesonbuild/compilers/mixins/arm.py b/mesonbuild/compilers/mixins/arm.py
new file mode 100644
index 0000000..5d0fb87
--- /dev/null
+++ b/mesonbuild/compilers/mixins/arm.py
@@ -0,0 +1,199 @@
+# Copyright 2012-2020 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.
+from __future__ import annotations
+
+"""Representations specific to the arm family of compilers."""
+
+import os
+import typing as T
+
+from ... import mesonlib
+from ...linkers import ArmClangDynamicLinker
+from ...mesonlib import OptionKey
+from ..compilers import clike_debug_args
+from .clang import clang_color_args
+
+if T.TYPE_CHECKING:
+ from ...environment import Environment
+ from ...compilers.compilers import Compiler
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+arm_buildtype_args = {
+ 'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+} # type: T.Dict[str, T.List[str]]
+
+arm_optimization_args = {
+ 'plain': [],
+ '0': ['-O0'],
+ 'g': ['-g'],
+ '1': ['-O1'],
+ '2': [], # Compiler defaults to -O2
+ '3': ['-O3', '-Otime'],
+ 's': ['-O3'], # Compiler defaults to -Ospace
+} # type: T.Dict[str, T.List[str]]
+
+armclang_buildtype_args = {
+ 'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+} # type: T.Dict[str, T.List[str]]
+
+armclang_optimization_args = {
+ 'plain': [],
+ '0': [], # Compiler defaults to -O0
+ 'g': ['-g'],
+ '1': ['-O1'],
+ '2': ['-O2'],
+ '3': ['-O3'],
+ 's': ['-Oz']
+} # type: T.Dict[str, T.List[str]]
+
+
+class ArmCompiler(Compiler):
+
+ """Functionality that is common to all ARM family compilers."""
+
+ id = 'arm'
+
+ def __init__(self) -> None:
+ if not self.is_cross:
+ raise mesonlib.EnvironmentException('armcc supports only cross-compilation.')
+ default_warn_args = [] # type: T.List[str]
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + [],
+ '3': default_warn_args + [],
+ 'everything': default_warn_args + []} # type: T.Dict[str, T.List[str]]
+ # Assembly
+ self.can_compile_suffixes.add('s')
+
+ def get_pic_args(self) -> T.List[str]:
+ # FIXME: Add /ropi, /rwpi, /fpic etc. qualifiers to --apcs
+ return []
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return arm_buildtype_args[buildtype]
+
+ # Override CCompiler.get_always_args
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return ['--depend_target', outtarget, '--depend', outfile, '--depend_single_line']
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ # FIXME: Add required arguments
+ # NOTE from armcc user guide:
+ # "Support for Precompiled Header (PCH) files is deprecated from ARM Compiler 5.05
+ # onwards on all platforms. Note that ARM Compiler on Windows 8 never supported
+ # PCH files."
+ return []
+
+ def get_pch_suffix(self) -> str:
+ # NOTE from armcc user guide:
+ # "Support for Precompiled Header (PCH) files is deprecated from ARM Compiler 5.05
+ # onwards on all platforms. Note that ARM Compiler on Windows 8 never supported
+ # PCH files."
+ return 'pch'
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def get_coverage_args(self) -> T.List[str]:
+ return []
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return arm_optimization_args[optimization_level]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return clike_debug_args[is_debug]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I' or i[:2] == '-L':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+
+ return parameter_list
+
+
+class ArmclangCompiler(Compiler):
+ '''
+ This is the Keil armclang.
+ '''
+
+ id = 'armclang'
+
+ def __init__(self) -> None:
+ if not self.is_cross:
+ raise mesonlib.EnvironmentException('armclang supports only cross-compilation.')
+ # Check whether 'armlink' is available in path
+ if not isinstance(self.linker, ArmClangDynamicLinker):
+ raise mesonlib.EnvironmentException(f'Unsupported Linker {self.linker.exelist}, must be armlink')
+ if not mesonlib.version_compare(self.version, '==' + self.linker.version):
+ raise mesonlib.EnvironmentException('armlink version does not match with compiler version')
+ self.base_options = {
+ OptionKey(o) for o in
+ ['b_pch', 'b_lto', 'b_pgo', 'b_sanitize', 'b_coverage',
+ 'b_ndebug', 'b_staticpic', 'b_colorout']}
+ # Assembly
+ self.can_compile_suffixes.add('s')
+
+ def get_pic_args(self) -> T.List[str]:
+ # PIC support is not enabled by default for ARM,
+ # if users want to use it, they need to add the required arguments explicitly
+ return []
+
+ def get_colorout_args(self, colortype: str) -> T.List[str]:
+ return clang_color_args[colortype][:]
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return armclang_buildtype_args[buildtype]
+
+ def get_pch_suffix(self) -> str:
+ return 'gch'
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ # Workaround for Clang bug http://llvm.org/bugs/show_bug.cgi?id=15136
+ # This flag is internal to Clang (or at least not documented on the man page)
+ # so it might change semantics at any time.
+ return ['-include-pch', os.path.join(pch_dir, self.get_pch_name(header))]
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return ['-MD', '-MT', outtarget, '-MF', outfile]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return armclang_optimization_args[optimization_level]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return clike_debug_args[is_debug]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I' or i[:2] == '-L':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+
+ return parameter_list
diff --git a/mesonbuild/compilers/mixins/ccrx.py b/mesonbuild/compilers/mixins/ccrx.py
new file mode 100644
index 0000000..1c22214
--- /dev/null
+++ b/mesonbuild/compilers/mixins/ccrx.py
@@ -0,0 +1,134 @@
+# Copyright 2012-2019 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.
+from __future__ import annotations
+
+"""Representations specific to the Renesas CC-RX compiler family."""
+
+import os
+import typing as T
+
+from ...mesonlib import EnvironmentException
+
+if T.TYPE_CHECKING:
+ from ...envconfig import MachineInfo
+ from ...environment import Environment
+ from ...compilers.compilers import Compiler
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+ccrx_buildtype_args = {
+ 'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+} # type: T.Dict[str, T.List[str]]
+
+ccrx_optimization_args = {
+ '0': ['-optimize=0'],
+ 'g': ['-optimize=0'],
+ '1': ['-optimize=1'],
+ '2': ['-optimize=2'],
+ '3': ['-optimize=max'],
+ 's': ['-optimize=2', '-size']
+} # type: T.Dict[str, T.List[str]]
+
+ccrx_debug_args = {
+ False: [],
+ True: ['-debug']
+} # type: T.Dict[bool, T.List[str]]
+
+
+class CcrxCompiler(Compiler):
+
+ if T.TYPE_CHECKING:
+ is_cross = True
+ can_compile_suffixes = set() # type: T.Set[str]
+
+ id = 'ccrx'
+
+ def __init__(self) -> None:
+ if not self.is_cross:
+ raise EnvironmentException('ccrx supports only cross-compilation.')
+ # Assembly
+ self.can_compile_suffixes.add('src')
+ default_warn_args = [] # type: T.List[str]
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + [],
+ '3': default_warn_args + [],
+ 'everything': default_warn_args + []} # type: T.Dict[str, T.List[str]]
+
+ def get_pic_args(self) -> T.List[str]:
+ # PIC support is not enabled by default for CCRX,
+ # if users want to use it, they need to add the required arguments explicitly
+ return []
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return ccrx_buildtype_args[buildtype]
+
+ def get_pch_suffix(self) -> str:
+ return 'pch'
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ return []
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def get_coverage_args(self) -> T.List[str]:
+ return []
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ return []
+
+ def get_no_stdlib_link_args(self) -> T.List[str]:
+ return []
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return ccrx_optimization_args[optimization_level]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return ccrx_debug_args[is_debug]
+
+ @classmethod
+ def _unix_args_to_native(cls, args: T.List[str], info: MachineInfo) -> T.List[str]:
+ result = []
+ for i in args:
+ if i.startswith('-D'):
+ i = '-define=' + i[2:]
+ if i.startswith('-I'):
+ i = '-include=' + i[2:]
+ if i.startswith('-Wl,-rpath='):
+ continue
+ elif i == '--print-search-dirs':
+ continue
+ elif i.startswith('-L'):
+ continue
+ elif not i.startswith('-lib=') and i.endswith(('.a', '.lib')):
+ i = '-lib=' + i
+ result.append(i)
+ return result
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:9] == '-include=':
+ parameter_list[idx] = i[:9] + os.path.normpath(os.path.join(build_dir, i[9:]))
+
+ return parameter_list
diff --git a/mesonbuild/compilers/mixins/clang.py b/mesonbuild/compilers/mixins/clang.py
new file mode 100644
index 0000000..cdb4c23
--- /dev/null
+++ b/mesonbuild/compilers/mixins/clang.py
@@ -0,0 +1,180 @@
+# Copyright 2019-2022 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.
+from __future__ import annotations
+
+"""Abstractions for the LLVM/Clang compiler family."""
+
+import os
+import shutil
+import typing as T
+
+from ... import mesonlib
+from ...linkers import AppleDynamicLinker, ClangClDynamicLinker, LLVMDynamicLinker, GnuGoldDynamicLinker, \
+ MoldDynamicLinker
+from ...mesonlib import OptionKey
+from ..compilers import CompileCheckMode
+from .gnu import GnuLikeCompiler
+
+if T.TYPE_CHECKING:
+ from ...environment import Environment
+ from ...dependencies import Dependency # noqa: F401
+
+clang_color_args = {
+ 'auto': ['-fcolor-diagnostics'],
+ 'always': ['-fcolor-diagnostics'],
+ 'never': ['-fno-color-diagnostics'],
+} # type: T.Dict[str, T.List[str]]
+
+clang_optimization_args = {
+ 'plain': [],
+ '0': ['-O0'],
+ 'g': ['-Og'],
+ '1': ['-O1'],
+ '2': ['-O2'],
+ '3': ['-O3'],
+ 's': ['-Oz'],
+} # type: T.Dict[str, T.List[str]]
+
+class ClangCompiler(GnuLikeCompiler):
+
+ id = 'clang'
+
+ def __init__(self, defines: T.Optional[T.Dict[str, str]]):
+ super().__init__()
+ self.defines = defines or {}
+ self.base_options.update(
+ {OptionKey('b_colorout'), OptionKey('b_lto_threads'), OptionKey('b_lto_mode'), OptionKey('b_thinlto_cache'),
+ OptionKey('b_thinlto_cache_dir')})
+
+ # TODO: this really should be part of the linker base_options, but
+ # linkers don't have base_options.
+ if isinstance(self.linker, AppleDynamicLinker):
+ self.base_options.add(OptionKey('b_bitcode'))
+ # All Clang backends can also do LLVM IR
+ self.can_compile_suffixes.add('ll')
+
+ def get_colorout_args(self, colortype: str) -> T.List[str]:
+ return clang_color_args[colortype][:]
+
+ def has_builtin_define(self, define: str) -> bool:
+ return define in self.defines
+
+ def get_builtin_define(self, define: str) -> T.Optional[str]:
+ return self.defines.get(define)
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return clang_optimization_args[optimization_level]
+
+ def get_pch_suffix(self) -> str:
+ return 'pch'
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ # Workaround for Clang bug http://llvm.org/bugs/show_bug.cgi?id=15136
+ # This flag is internal to Clang (or at least not documented on the man page)
+ # so it might change semantics at any time.
+ return ['-include-pch', os.path.join(pch_dir, self.get_pch_name(header))]
+
+ def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
+ # Clang is different than GCC, it will return True when a symbol isn't
+ # defined in a header. Specifically this seems to have something to do
+ # with functions that may be in a header on some systems, but not all of
+ # them. `strlcat` specifically with can trigger this.
+ myargs: T.List[str] = ['-Werror=implicit-function-declaration']
+ if mode is CompileCheckMode.COMPILE:
+ myargs.extend(['-Werror=unknown-warning-option', '-Werror=unused-command-line-argument'])
+ if mesonlib.version_compare(self.version, '>=3.6.0'):
+ myargs.append('-Werror=ignored-optimization-argument')
+ return super().get_compiler_check_args(mode) + myargs
+
+ def has_function(self, funcname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Optional[T.List[str]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ if extra_args is None:
+ extra_args = []
+ # Starting with XCode 8, we need to pass this to force linker
+ # visibility to obey OS X/iOS/tvOS minimum version targets with
+ # -mmacosx-version-min, -miphoneos-version-min, -mtvos-version-min etc.
+ # https://github.com/Homebrew/homebrew-core/issues/3727
+ # TODO: this really should be communicated by the linker
+ if isinstance(self.linker, AppleDynamicLinker) and mesonlib.version_compare(self.version, '>=8.0'):
+ extra_args.append('-Wl,-no_weak_imports')
+ return super().has_function(funcname, prefix, env, extra_args=extra_args,
+ dependencies=dependencies)
+
+ def openmp_flags(self) -> T.List[str]:
+ if mesonlib.version_compare(self.version, '>=3.8.0'):
+ return ['-fopenmp']
+ elif mesonlib.version_compare(self.version, '>=3.7.0'):
+ return ['-fopenmp=libomp']
+ else:
+ # Shouldn't work, but it'll be checked explicitly in the OpenMP dependency.
+ return []
+
+ @classmethod
+ def use_linker_args(cls, linker: str, version: str) -> T.List[str]:
+ # Clang additionally can use a linker specified as a path, which GCC
+ # (and other gcc-like compilers) cannot. This is because clang (being
+ # llvm based) is retargetable, while GCC is not.
+ #
+
+ # qcld: Qualcomm Snapdragon linker, based on LLVM
+ if linker == 'qcld':
+ return ['-fuse-ld=qcld']
+ if linker == 'mold':
+ return ['-fuse-ld=mold']
+
+ if shutil.which(linker):
+ if not shutil.which(linker):
+ raise mesonlib.MesonException(
+ f'Cannot find linker {linker}.')
+ return [f'-fuse-ld={linker}']
+ return super().use_linker_args(linker, version)
+
+ def get_has_func_attribute_extra_args(self, name: str) -> T.List[str]:
+ # Clang only warns about unknown or ignored attributes, so force an
+ # error.
+ return ['-Werror=attributes']
+
+ def get_coverage_link_args(self) -> T.List[str]:
+ return ['--coverage']
+
+ def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]:
+ args: T.List[str] = []
+ if mode == 'thin':
+ # ThinLTO requires the use of gold, lld, ld64, lld-link or mold 1.1+
+ if isinstance(self.linker, (MoldDynamicLinker)):
+ # https://github.com/rui314/mold/commit/46995bcfc3e3113133620bf16445c5f13cd76a18
+ if not mesonlib.version_compare(self.linker.version, '>=1.1'):
+ raise mesonlib.MesonException("LLVM's ThinLTO requires mold 1.1+")
+ elif not isinstance(self.linker, (AppleDynamicLinker, ClangClDynamicLinker, LLVMDynamicLinker, GnuGoldDynamicLinker)):
+ raise mesonlib.MesonException(f"LLVM's ThinLTO only works with gold, lld, lld-link, ld64 or mold, not {self.linker.id}")
+ args.append(f'-flto={mode}')
+ else:
+ assert mode == 'default', 'someone forgot to wire something up'
+ args.extend(super().get_lto_compile_args(threads=threads))
+ return args
+
+ def get_lto_link_args(self, *, threads: int = 0, mode: str = 'default',
+ thinlto_cache_dir: T.Optional[str] = None) -> T.List[str]:
+ args = self.get_lto_compile_args(threads=threads, mode=mode)
+ if mode == 'thin' and thinlto_cache_dir is not None:
+ # We check for ThinLTO linker support above in get_lto_compile_args, and all of them support
+ # get_thinlto_cache_args as well
+ args.extend(self.linker.get_thinlto_cache_args(thinlto_cache_dir))
+ # In clang -flto-jobs=0 means auto, and is the default if unspecified, just like in meson
+ if threads > 0:
+ if not mesonlib.version_compare(self.version, '>=4.0.0'):
+ raise mesonlib.MesonException('clang support for LTO threads requires clang >=4.0')
+ args.append(f'-flto-jobs={threads}')
+ return args
diff --git a/mesonbuild/compilers/mixins/clike.py b/mesonbuild/compilers/mixins/clike.py
new file mode 100644
index 0000000..d44dd4d
--- /dev/null
+++ b/mesonbuild/compilers/mixins/clike.py
@@ -0,0 +1,1349 @@
+# Copyright 2012-2022 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.
+from __future__ import annotations
+
+
+"""Mixin classes to be shared between C and C++ compilers.
+
+Without this we'll end up with awful diamond inherintance problems. The goal
+of this is to have mixin's, which are classes that are designed *not* to be
+standalone, they only work through inheritance.
+"""
+
+import collections
+import functools
+import glob
+import itertools
+import os
+import re
+import subprocess
+import copy
+import typing as T
+from pathlib import Path
+
+from ... import arglist
+from ... import mesonlib
+from ... import mlog
+from ...linkers import GnuLikeDynamicLinkerMixin, SolarisDynamicLinker, CompCertDynamicLinker
+from ...mesonlib import LibType
+from ...coredata import OptionKey
+from .. import compilers
+from ..compilers import CompileCheckMode
+from .visualstudio import VisualStudioLikeCompiler
+
+if T.TYPE_CHECKING:
+ from ...dependencies import Dependency
+ from ..._typing import ImmutableListProtocol
+ from ...environment import Environment
+ from ...compilers.compilers import Compiler
+ from ...programs import ExternalProgram
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+GROUP_FLAGS = re.compile(r'''\.so (?:\.[0-9]+)? (?:\.[0-9]+)? (?:\.[0-9]+)?$ |
+ ^(?:-Wl,)?-l |
+ \.a$''', re.X)
+
+class CLikeCompilerArgs(arglist.CompilerArgs):
+ prepend_prefixes = ('-I', '-L')
+ dedup2_prefixes = ('-I', '-isystem', '-L', '-D', '-U')
+
+ # NOTE: not thorough. A list of potential corner cases can be found in
+ # https://github.com/mesonbuild/meson/pull/4593#pullrequestreview-182016038
+ dedup1_prefixes = ('-l', '-Wl,-l', '-Wl,--export-dynamic')
+ dedup1_suffixes = ('.lib', '.dll', '.so', '.dylib', '.a')
+ dedup1_args = ('-c', '-S', '-E', '-pipe', '-pthread')
+
+ def to_native(self, copy: bool = False) -> T.List[str]:
+ # This seems to be allowed, but could never work?
+ assert isinstance(self.compiler, compilers.Compiler), 'How did you get here'
+
+ # Check if we need to add --start/end-group for circular dependencies
+ # between static libraries, and for recursively searching for symbols
+ # needed by static libraries that are provided by object files or
+ # shared libraries.
+ self.flush_pre_post()
+ if copy:
+ new = self.copy()
+ else:
+ new = self
+ # This covers all ld.bfd, ld.gold, ld.gold, and xild on Linux, which
+ # all act like (or are) gnu ld
+ # TODO: this could probably be added to the DynamicLinker instead
+ if isinstance(self.compiler.linker, (GnuLikeDynamicLinkerMixin, SolarisDynamicLinker, CompCertDynamicLinker)):
+ group_start = -1
+ group_end = -1
+ for i, each in enumerate(new):
+ if not GROUP_FLAGS.search(each):
+ continue
+ group_end = i
+ if group_start < 0:
+ # First occurrence of a library
+ group_start = i
+ if group_start >= 0:
+ # Last occurrence of a library
+ new.insert(group_end + 1, '-Wl,--end-group')
+ new.insert(group_start, '-Wl,--start-group')
+ # Remove system/default include paths added with -isystem
+ default_dirs = self.compiler.get_default_include_dirs()
+ if default_dirs:
+ real_default_dirs = [self._cached_realpath(i) for i in default_dirs]
+ bad_idx_list = [] # type: T.List[int]
+ for i, each in enumerate(new):
+ if not each.startswith('-isystem'):
+ continue
+
+ # Remove the -isystem and the path if the path is a default path
+ if (each == '-isystem' and
+ i < (len(new) - 1) and
+ self._cached_realpath(new[i + 1]) in real_default_dirs):
+ bad_idx_list += [i, i + 1]
+ elif each.startswith('-isystem=') and self._cached_realpath(each[9:]) in real_default_dirs:
+ bad_idx_list += [i]
+ elif self._cached_realpath(each[8:]) in real_default_dirs:
+ bad_idx_list += [i]
+ for i in reversed(bad_idx_list):
+ new.pop(i)
+ return self.compiler.unix_args_to_native(new._container)
+
+ @staticmethod
+ @functools.lru_cache(maxsize=None)
+ def _cached_realpath(arg: str) -> str:
+ return os.path.realpath(arg)
+
+ def __repr__(self) -> str:
+ self.flush_pre_post()
+ return f'CLikeCompilerArgs({self.compiler!r}, {self._container!r})'
+
+
+class CLikeCompiler(Compiler):
+
+ """Shared bits for the C and CPP Compilers."""
+
+ if T.TYPE_CHECKING:
+ warn_args = {} # type: T.Dict[str, T.List[str]]
+
+ # TODO: Replace this manual cache with functools.lru_cache
+ find_library_cache = {} # type: T.Dict[T.Tuple[T.Tuple[str, ...], str, T.Tuple[str, ...], str, LibType], T.Optional[T.List[str]]]
+ find_framework_cache = {} # type: T.Dict[T.Tuple[T.Tuple[str, ...], str, T.Tuple[str, ...], bool], T.Optional[T.List[str]]]
+ internal_libs = arglist.UNIXY_COMPILER_INTERNAL_LIBS
+
+ def __init__(self, exe_wrapper: T.Optional['ExternalProgram'] = None):
+ # If a child ObjC or CPP class has already set it, don't set it ourselves
+ self.can_compile_suffixes.add('h')
+ # If the exe wrapper was not found, pretend it wasn't set so that the
+ # sanity check is skipped and compiler checks use fallbacks.
+ if not exe_wrapper or not exe_wrapper.found() or not exe_wrapper.get_command():
+ self.exe_wrapper = None
+ else:
+ self.exe_wrapper = exe_wrapper
+ # Lazy initialized in get_preprocessor()
+ self.preprocessor: T.Optional[Compiler] = None
+
+ def compiler_args(self, args: T.Optional[T.Iterable[str]] = None) -> CLikeCompilerArgs:
+ # This is correct, mypy just doesn't understand co-operative inheritance
+ return CLikeCompilerArgs(self, args)
+
+ def needs_static_linker(self) -> bool:
+ return True # When compiling static libraries, so yes.
+
+ def get_always_args(self) -> T.List[str]:
+ '''
+ Args that are always-on for all C compilers other than MSVC
+ '''
+ return self.get_largefile_args()
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ return ['-nostdinc']
+
+ def get_no_stdlib_link_args(self) -> T.List[str]:
+ return ['-nostdlib']
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ # TODO: this should be an enum
+ return self.warn_args[level]
+
+ def get_no_warn_args(self) -> T.List[str]:
+ # Almost every compiler uses this for disabling warnings
+ return ['-w']
+
+ def get_depfile_suffix(self) -> str:
+ return 'd'
+
+ def get_preprocess_only_args(self) -> T.List[str]:
+ return ['-E', '-P']
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return ['-c']
+
+ def get_no_optimization_args(self) -> T.List[str]:
+ return ['-O0']
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return ['-o', outputname]
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['-Werror']
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if path == '':
+ path = '.'
+ if is_system:
+ return ['-isystem', path]
+ return ['-I' + path]
+
+ def get_compiler_dirs(self, env: 'Environment', name: str) -> T.List[str]:
+ '''
+ Get dirs from the compiler, either `libraries:` or `programs:`
+ '''
+ return []
+
+ @functools.lru_cache()
+ def _get_library_dirs(self, env: 'Environment',
+ elf_class: T.Optional[int] = None) -> 'ImmutableListProtocol[str]':
+ # TODO: replace elf_class with enum
+ dirs = self.get_compiler_dirs(env, 'libraries')
+ if elf_class is None or elf_class == 0:
+ return dirs
+
+ # if we do have an elf class for 32-bit or 64-bit, we want to check that
+ # the directory in question contains libraries of the appropriate class. Since
+ # system directories aren't mixed, we only need to check one file for each
+ # directory and go by that. If we can't check the file for some reason, assume
+ # the compiler knows what it's doing, and accept the directory anyway.
+ retval = []
+ for d in dirs:
+ files = [f for f in os.listdir(d) if f.endswith('.so') and os.path.isfile(os.path.join(d, f))]
+ # if no files, accept directory and move on
+ if not files:
+ retval.append(d)
+ continue
+
+ for f in files:
+ file_to_check = os.path.join(d, f)
+ try:
+ with open(file_to_check, 'rb') as fd:
+ header = fd.read(5)
+ # if file is not an ELF file, it's weird, but accept dir
+ # if it is elf, and the class matches, accept dir
+ if header[1:4] != b'ELF' or int(header[4]) == elf_class:
+ retval.append(d)
+ # at this point, it's an ELF file which doesn't match the
+ # appropriate elf_class, so skip this one
+ # stop scanning after the first successful read
+ break
+ except OSError:
+ # Skip the file if we can't read it
+ pass
+
+ return retval
+
+ def get_library_dirs(self, env: 'Environment',
+ elf_class: T.Optional[int] = None) -> T.List[str]:
+ """Wrap the lru_cache so that we return a new copy and don't allow
+ mutation of the cached value.
+ """
+ return self._get_library_dirs(env, elf_class).copy()
+
+ @functools.lru_cache()
+ def _get_program_dirs(self, env: 'Environment') -> 'ImmutableListProtocol[str]':
+ '''
+ Programs used by the compiler. Also where toolchain DLLs such as
+ libstdc++-6.dll are found with MinGW.
+ '''
+ return self.get_compiler_dirs(env, 'programs')
+
+ def get_program_dirs(self, env: 'Environment') -> T.List[str]:
+ return self._get_program_dirs(env).copy()
+
+ def get_pic_args(self) -> T.List[str]:
+ return ['-fPIC']
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ return ['-include', os.path.basename(header)]
+
+ def get_pch_name(self, name: str) -> str:
+ return os.path.basename(name) + '.' + self.get_pch_suffix()
+
+ def get_default_include_dirs(self) -> T.List[str]:
+ return []
+
+ def gen_export_dynamic_link_args(self, env: 'Environment') -> T.List[str]:
+ return self.linker.export_dynamic_args(env)
+
+ def gen_import_library_args(self, implibname: str) -> T.List[str]:
+ return self.linker.import_library_args(implibname)
+
+ def _sanity_check_impl(self, work_dir: str, environment: 'Environment',
+ sname: str, code: str) -> None:
+ mlog.debug('Sanity testing ' + self.get_display_language() + ' compiler:', mesonlib.join_args(self.exelist))
+ mlog.debug(f'Is cross compiler: {self.is_cross!s}.')
+
+ source_name = os.path.join(work_dir, sname)
+ binname = sname.rsplit('.', 1)[0]
+ mode = CompileCheckMode.LINK
+ if self.is_cross:
+ binname += '_cross'
+ if self.exe_wrapper is None:
+ # Linking cross built C/C++ apps is painful. You can't really
+ # tell if you should use -nostdlib or not and for example
+ # on OSX the compiler binary is the same but you need
+ # a ton of compiler flags to differentiate between
+ # arm and x86_64. So just compile.
+ mode = CompileCheckMode.COMPILE
+ cargs, largs = self._get_basic_compiler_args(environment, mode)
+ extra_flags = cargs + self.linker_to_compiler_args(largs)
+
+ # Is a valid executable output for all toolchains and platforms
+ binname += '.exe'
+ # Write binary check source
+ binary_name = os.path.join(work_dir, binname)
+ with open(source_name, 'w', encoding='utf-8') as ofile:
+ ofile.write(code)
+ # Compile sanity check
+ # NOTE: extra_flags must be added at the end. On MSVC, it might contain a '/link' argument
+ # after which all further arguments will be passed directly to the linker
+ cmdlist = self.exelist + [sname] + self.get_output_args(binname) + extra_flags
+ pc, stdo, stde = mesonlib.Popen_safe(cmdlist, cwd=work_dir)
+ mlog.debug('Sanity check compiler command line:', mesonlib.join_args(cmdlist))
+ mlog.debug('Sanity check compile stdout:')
+ mlog.debug(stdo)
+ mlog.debug('-----\nSanity check compile stderr:')
+ mlog.debug(stde)
+ mlog.debug('-----')
+ if pc.returncode != 0:
+ raise mesonlib.EnvironmentException(f'Compiler {self.name_string()} can not compile programs.')
+ # Run sanity check
+ if self.is_cross:
+ if self.exe_wrapper is None:
+ # Can't check if the binaries run so we have to assume they do
+ return
+ cmdlist = self.exe_wrapper.get_command() + [binary_name]
+ else:
+ cmdlist = [binary_name]
+ mlog.debug('Running test binary command: ', mesonlib.join_args(cmdlist))
+ try:
+ # fortran code writes to stdout
+ pe = subprocess.run(cmdlist, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ except Exception as e:
+ raise mesonlib.EnvironmentException(f'Could not invoke sanity test executable: {e!s}.')
+ if pe.returncode != 0:
+ raise mesonlib.EnvironmentException(f'Executables created by {self.language} compiler {self.name_string()} are not runnable.')
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ code = 'int main(void) { int class=0; return class; }\n'
+ return self._sanity_check_impl(work_dir, environment, 'sanitycheckc.c', code)
+
+ def check_header(self, hname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[['CompileCheckMode'], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ code = f'''{prefix}
+ #include <{hname}>'''
+ return self.compiles(code, env, extra_args=extra_args,
+ dependencies=dependencies)
+
+ def has_header(self, hname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[['CompileCheckMode'], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None,
+ disable_cache: bool = False) -> T.Tuple[bool, bool]:
+ code = f'''{prefix}
+ #ifdef __has_include
+ #if !__has_include("{hname}")
+ #error "Header '{hname}' could not be found"
+ #endif
+ #else
+ #include <{hname}>
+ #endif'''
+ return self.compiles(code, env, extra_args=extra_args,
+ dependencies=dependencies, mode='preprocess', disable_cache=disable_cache)
+
+ def has_header_symbol(self, hname: str, symbol: str, prefix: str,
+ env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ t = f'''{prefix}
+ #include <{hname}>
+ int main(void) {{
+ /* If it's not defined as a macro, try to use as a symbol */
+ #ifndef {symbol}
+ {symbol};
+ #endif
+ return 0;
+ }}'''
+ return self.compiles(t, env, extra_args=extra_args,
+ dependencies=dependencies)
+
+ def _get_basic_compiler_args(self, env: 'Environment', mode: CompileCheckMode) -> T.Tuple[T.List[str], T.List[str]]:
+ cargs = [] # type: T.List[str]
+ largs = [] # type: T.List[str]
+ if mode is CompileCheckMode.LINK:
+ # Sometimes we need to manually select the CRT to use with MSVC.
+ # One example is when trying to do a compiler check that involves
+ # linking with static libraries since MSVC won't select a CRT for
+ # us in that case and will error out asking us to pick one.
+ try:
+ crt_val = env.coredata.options[OptionKey('b_vscrt')].value
+ buildtype = env.coredata.options[OptionKey('buildtype')].value
+ cargs += self.get_crt_compile_args(crt_val, buildtype)
+ except (KeyError, AttributeError):
+ pass
+
+ # Add CFLAGS/CXXFLAGS/OBJCFLAGS/OBJCXXFLAGS and CPPFLAGS from the env
+ sys_args = env.coredata.get_external_args(self.for_machine, self.language)
+ if isinstance(sys_args, str):
+ sys_args = [sys_args]
+ # Apparently it is a thing to inject linker flags both
+ # via CFLAGS _and_ LDFLAGS, even though the former are
+ # also used during linking. These flags can break
+ # argument checks. Thanks, Autotools.
+ cleaned_sys_args = self.remove_linkerlike_args(sys_args)
+ cargs += cleaned_sys_args
+
+ if mode is CompileCheckMode.LINK:
+ ld_value = env.lookup_binary_entry(self.for_machine, self.language + '_ld')
+ if ld_value is not None:
+ largs += self.use_linker_args(ld_value[0], self.version)
+
+ # Add LDFLAGS from the env
+ sys_ld_args = env.coredata.get_external_link_args(self.for_machine, self.language)
+ # CFLAGS and CXXFLAGS go to both linking and compiling, but we want them
+ # to only appear on the command line once. Remove dupes.
+ largs += [x for x in sys_ld_args if x not in sys_args]
+
+ cargs += self.get_compiler_args_for_mode(mode)
+ return cargs, largs
+
+ def build_wrapper_args(self, env: 'Environment',
+ extra_args: T.Union[None, arglist.CompilerArgs, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]],
+ dependencies: T.Optional[T.List['Dependency']],
+ mode: CompileCheckMode = CompileCheckMode.COMPILE) -> arglist.CompilerArgs:
+ # TODO: the caller should handle the listfing of these arguments
+ if extra_args is None:
+ extra_args = []
+ else:
+ # TODO: we want to do this in the caller
+ extra_args = mesonlib.listify(extra_args)
+ extra_args = mesonlib.listify([e(mode.value) if callable(e) else e for e in extra_args])
+
+ if dependencies is None:
+ dependencies = []
+ elif not isinstance(dependencies, collections.abc.Iterable):
+ # TODO: we want to ensure the front end does the listifing here
+ dependencies = [dependencies]
+ # Collect compiler arguments
+ cargs = self.compiler_args() # type: arglist.CompilerArgs
+ largs = [] # type: T.List[str]
+ for d in dependencies:
+ # Add compile flags needed by dependencies
+ cargs += d.get_compile_args()
+ if mode is CompileCheckMode.LINK:
+ # Add link flags needed to find dependencies
+ largs += d.get_link_args()
+
+ ca, la = self._get_basic_compiler_args(env, mode)
+ cargs += ca
+ largs += la
+
+ cargs += self.get_compiler_check_args(mode)
+
+ # on MSVC compiler and linker flags must be separated by the "/link" argument
+ # at this point, the '/link' argument may already be part of extra_args, otherwise, it is added here
+ if self.linker_to_compiler_args([]) == ['/link'] and largs != [] and '/link' not in extra_args:
+ extra_args += ['/link']
+
+ args = cargs + extra_args + largs
+ return args
+
+ def run(self, code: 'mesonlib.FileOrString', env: 'Environment', *,
+ extra_args: T.Union[T.List[str], T.Callable[[CompileCheckMode], T.List[str]], None] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> compilers.RunResult:
+ need_exe_wrapper = env.need_exe_wrapper(self.for_machine)
+ if need_exe_wrapper and self.exe_wrapper is None:
+ raise compilers.CrossNoRunException('Can not run test applications in this cross environment.')
+ with self._build_wrapper(code, env, extra_args, dependencies, mode='link', want_output=True) as p:
+ if p.returncode != 0:
+ mlog.debug(f'Could not compile test file {p.input_name}: {p.returncode}\n')
+ return compilers.RunResult(False)
+ if need_exe_wrapper:
+ cmdlist = self.exe_wrapper.get_command() + [p.output_name]
+ else:
+ cmdlist = [p.output_name]
+ try:
+ pe, so, se = mesonlib.Popen_safe(cmdlist)
+ except Exception as e:
+ mlog.debug(f'Could not run: {cmdlist} (error: {e})\n')
+ return compilers.RunResult(False)
+
+ mlog.debug('Program stdout:\n')
+ mlog.debug(so)
+ mlog.debug('Program stderr:\n')
+ mlog.debug(se)
+ return compilers.RunResult(True, pe.returncode, so, se)
+
+ def _compile_int(self, expression: str, prefix: str, env: 'Environment',
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]],
+ dependencies: T.Optional[T.List['Dependency']]) -> bool:
+ t = f'''#include <stdio.h>
+ {prefix}
+ int main(void) {{ static int a[1-2*!({expression})]; a[0]=0; return 0; }}'''
+ return self.compiles(t, env, extra_args=extra_args,
+ dependencies=dependencies)[0]
+
+ def cross_compute_int(self, expression: str, low: T.Optional[int], high: T.Optional[int],
+ guess: T.Optional[int], prefix: str, env: 'Environment',
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> int:
+ # Try user's guess first
+ if isinstance(guess, int):
+ if self._compile_int(f'{expression} == {guess}', prefix, env, extra_args, dependencies):
+ return guess
+
+ # If no bounds are given, compute them in the limit of int32
+ maxint = 0x7fffffff
+ minint = -0x80000000
+ if not isinstance(low, int) or not isinstance(high, int):
+ if self._compile_int(f'{expression} >= 0', prefix, env, extra_args, dependencies):
+ low = cur = 0
+ while self._compile_int(f'{expression} > {cur}', prefix, env, extra_args, dependencies):
+ low = cur + 1
+ if low > maxint:
+ raise mesonlib.EnvironmentException('Cross-compile check overflowed')
+ cur = min(cur * 2 + 1, maxint)
+ high = cur
+ else:
+ high = cur = -1
+ while self._compile_int(f'{expression} < {cur}', prefix, env, extra_args, dependencies):
+ high = cur - 1
+ if high < minint:
+ raise mesonlib.EnvironmentException('Cross-compile check overflowed')
+ cur = max(cur * 2, minint)
+ low = cur
+ else:
+ # Sanity check limits given by user
+ if high < low:
+ raise mesonlib.EnvironmentException('high limit smaller than low limit')
+ condition = f'{expression} <= {high} && {expression} >= {low}'
+ if not self._compile_int(condition, prefix, env, extra_args, dependencies):
+ raise mesonlib.EnvironmentException('Value out of given range')
+
+ # Binary search
+ while low != high:
+ cur = low + int((high - low) / 2)
+ if self._compile_int(f'{expression} <= {cur}', prefix, env, extra_args, dependencies):
+ high = cur
+ else:
+ low = cur + 1
+
+ return low
+
+ def compute_int(self, expression: str, low: T.Optional[int], high: T.Optional[int],
+ guess: T.Optional[int], prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]],
+ dependencies: T.Optional[T.List['Dependency']] = None) -> int:
+ if extra_args is None:
+ extra_args = []
+ if self.is_cross:
+ return self.cross_compute_int(expression, low, high, guess, prefix, env, extra_args, dependencies)
+ t = f'''#include<stdio.h>
+ {prefix}
+ int main(void) {{
+ printf("%ld\\n", (long)({expression}));
+ return 0;
+ }}'''
+ res = self.run(t, env, extra_args=extra_args,
+ dependencies=dependencies)
+ if not res.compiled:
+ return -1
+ if res.returncode != 0:
+ raise mesonlib.EnvironmentException('Could not run compute_int test binary.')
+ return int(res.stdout)
+
+ def cross_sizeof(self, typename: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> int:
+ if extra_args is None:
+ extra_args = []
+ t = f'''#include <stdio.h>
+ {prefix}
+ int main(void) {{
+ {typename} something;
+ return 0;
+ }}'''
+ if not self.compiles(t, env, extra_args=extra_args,
+ dependencies=dependencies)[0]:
+ return -1
+ return self.cross_compute_int(f'sizeof({typename})', None, None, None, prefix, env, extra_args, dependencies)
+
+ def sizeof(self, typename: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> int:
+ if extra_args is None:
+ extra_args = []
+ if self.is_cross:
+ return self.cross_sizeof(typename, prefix, env, extra_args=extra_args,
+ dependencies=dependencies)
+ t = f'''#include<stdio.h>
+ {prefix}
+ int main(void) {{
+ printf("%ld\\n", (long)(sizeof({typename})));
+ return 0;
+ }}'''
+ res = self.run(t, env, extra_args=extra_args,
+ dependencies=dependencies)
+ if not res.compiled:
+ return -1
+ if res.returncode != 0:
+ raise mesonlib.EnvironmentException('Could not run sizeof test binary.')
+ return int(res.stdout)
+
+ def cross_alignment(self, typename: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Optional[T.List[str]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> int:
+ if extra_args is None:
+ extra_args = []
+ t = f'''#include <stdio.h>
+ {prefix}
+ int main(void) {{
+ {typename} something;
+ return 0;
+ }}'''
+ if not self.compiles(t, env, extra_args=extra_args,
+ dependencies=dependencies)[0]:
+ return -1
+ t = f'''#include <stddef.h>
+ {prefix}
+ struct tmp {{
+ char c;
+ {typename} target;
+ }};'''
+ return self.cross_compute_int('offsetof(struct tmp, target)', None, None, None, t, env, extra_args, dependencies)
+
+ def alignment(self, typename: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Optional[T.List[str]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> int:
+ if extra_args is None:
+ extra_args = []
+ if self.is_cross:
+ return self.cross_alignment(typename, prefix, env, extra_args=extra_args,
+ dependencies=dependencies)
+ t = f'''#include <stdio.h>
+ #include <stddef.h>
+ {prefix}
+ struct tmp {{
+ char c;
+ {typename} target;
+ }};
+ int main(void) {{
+ printf("%d", (int)offsetof(struct tmp, target));
+ return 0;
+ }}'''
+ res = self.run(t, env, extra_args=extra_args,
+ dependencies=dependencies)
+ if not res.compiled:
+ raise mesonlib.EnvironmentException('Could not compile alignment test.')
+ if res.returncode != 0:
+ raise mesonlib.EnvironmentException('Could not run alignment test binary.')
+ align = int(res.stdout)
+ if align == 0:
+ raise mesonlib.EnvironmentException(f'Could not determine alignment of {typename}. Sorry. You might want to file a bug.')
+ return align
+
+ def get_define(self, dname: str, prefix: str, env: 'Environment',
+ extra_args: T.Union[T.List[str], T.Callable[[CompileCheckMode], T.List[str]]],
+ dependencies: T.Optional[T.List['Dependency']],
+ disable_cache: bool = False) -> T.Tuple[str, bool]:
+ delim = '"MESON_GET_DEFINE_DELIMITER"'
+ code = f'''
+ {prefix}
+ #ifndef {dname}
+ # define {dname}
+ #endif
+ {delim}\n{dname}'''
+ args = self.build_wrapper_args(env, extra_args, dependencies,
+ mode=CompileCheckMode.PREPROCESS).to_native()
+ func = functools.partial(self.cached_compile, code, env.coredata, extra_args=args, mode='preprocess')
+ if disable_cache:
+ func = functools.partial(self.compile, code, extra_args=args, mode='preprocess', temp_dir=env.scratch_dir)
+ with func() as p:
+ cached = p.cached
+ if p.returncode != 0:
+ raise mesonlib.EnvironmentException(f'Could not get define {dname!r}')
+ # Get the preprocessed value after the delimiter,
+ # minus the extra newline at the end and
+ # merge string literals.
+ return self._concatenate_string_literals(p.stdout.split(delim + '\n')[-1][:-1]), cached
+
+ def get_return_value(self, fname: str, rtype: str, prefix: str,
+ env: 'Environment', extra_args: T.Optional[T.List[str]],
+ dependencies: T.Optional[T.List['Dependency']]) -> T.Union[str, int]:
+ # TODO: rtype should be an enum.
+ # TODO: maybe we can use overload to tell mypy when this will return int vs str?
+ if rtype == 'string':
+ fmt = '%s'
+ cast = '(char*)'
+ elif rtype == 'int':
+ fmt = '%lli'
+ cast = '(long long int)'
+ else:
+ raise AssertionError(f'BUG: Unknown return type {rtype!r}')
+ code = f'''{prefix}
+ #include <stdio.h>
+ int main(void) {{
+ printf ("{fmt}", {cast} {fname}());
+ return 0;
+ }}'''
+ res = self.run(code, env, extra_args=extra_args, dependencies=dependencies)
+ if not res.compiled:
+ raise mesonlib.EnvironmentException(f'Could not get return value of {fname}()')
+ if rtype == 'string':
+ return res.stdout
+ elif rtype == 'int':
+ try:
+ return int(res.stdout.strip())
+ except ValueError:
+ raise mesonlib.EnvironmentException(f'Return value of {fname}() is not an int')
+ assert False, 'Unreachable'
+
+ @staticmethod
+ def _no_prototype_templ() -> T.Tuple[str, str]:
+ """
+ Try to find the function without a prototype from a header by defining
+ our own dummy prototype and trying to link with the C library (and
+ whatever else the compiler links in by default). This is very similar
+ to the check performed by Autoconf for AC_CHECK_FUNCS.
+ """
+ # Define the symbol to something else since it is defined by the
+ # includes or defines listed by the user or by the compiler. This may
+ # include, for instance _GNU_SOURCE which must be defined before
+ # limits.h, which includes features.h
+ # Then, undef the symbol to get rid of it completely.
+ head = '''
+ #define {func} meson_disable_define_of_{func}
+ {prefix}
+ #include <limits.h>
+ #undef {func}
+ '''
+ # Override any GCC internal prototype and declare our own definition for
+ # the symbol. Use char because that's unlikely to be an actual return
+ # value for a function which ensures that we override the definition.
+ head += '''
+ #ifdef __cplusplus
+ extern "C"
+ #endif
+ char {func} (void);
+ '''
+ # The actual function call
+ main = '''
+ int main(void) {{
+ return {func} ();
+ }}'''
+ return head, main
+
+ @staticmethod
+ def _have_prototype_templ() -> T.Tuple[str, str]:
+ """
+ Returns a head-er and main() call that uses the headers listed by the
+ user for the function prototype while checking if a function exists.
+ """
+ # Add the 'prefix', aka defines, includes, etc that the user provides
+ # This may include, for instance _GNU_SOURCE which must be defined
+ # before limits.h, which includes features.h
+ head = '{prefix}\n#include <limits.h>\n'
+ # We don't know what the function takes or returns, so return it as an int.
+ # Just taking the address or comparing it to void is not enough because
+ # compilers are smart enough to optimize it away. The resulting binary
+ # is not run so we don't care what the return value is.
+ main = '''\nint main(void) {{
+ void *a = (void*) &{func};
+ long long b = (long long) a;
+ return (int) b;
+ }}'''
+ return head, main
+
+ def has_function(self, funcname: str, prefix: str, env: 'Environment', *,
+ extra_args: T.Optional[T.List[str]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ """Determine if a function exists.
+
+ First, this function looks for the symbol in the default libraries
+ provided by the compiler (stdlib + a few others usually). If that
+ fails, it checks if any of the headers specified in the prefix provide
+ an implementation of the function, and if that fails, it checks if it's
+ implemented as a compiler-builtin.
+ """
+ if extra_args is None:
+ extra_args = []
+
+ # Short-circuit if the check is already provided by the cross-info file
+ varname = 'has function ' + funcname
+ varname = varname.replace(' ', '_')
+ if self.is_cross:
+ val = env.properties.host.get(varname, None)
+ if val is not None:
+ if isinstance(val, bool):
+ return val, False
+ raise mesonlib.EnvironmentException(f'Cross variable {varname} is not a boolean.')
+
+ # TODO: we really need a protocol for this,
+ #
+ # class StrProto(typing.Protocol):
+ # def __str__(self) -> str: ...
+ fargs = {'prefix': prefix, 'func': funcname} # type: T.Dict[str, T.Union[str, bool, int]]
+
+ # glibc defines functions that are not available on Linux as stubs that
+ # fail with ENOSYS (such as e.g. lchmod). In this case we want to fail
+ # instead of detecting the stub as a valid symbol.
+ # We already included limits.h earlier to ensure that these are defined
+ # for stub functions.
+ stubs_fail = '''
+ #if defined __stub_{func} || defined __stub___{func}
+ fail fail fail this function is not going to work
+ #endif
+ '''
+
+ # If we have any includes in the prefix supplied by the user, assume
+ # that the user wants us to use the symbol prototype defined in those
+ # includes. If not, then try to do the Autoconf-style check with
+ # a dummy prototype definition of our own.
+ # This is needed when the linker determines symbol availability from an
+ # SDK based on the prototype in the header provided by the SDK.
+ # Ignoring this prototype would result in the symbol always being
+ # marked as available.
+ if '#include' in prefix:
+ head, main = self._have_prototype_templ()
+ else:
+ head, main = self._no_prototype_templ()
+ templ = head + stubs_fail + main
+
+ res, cached = self.links(templ.format(**fargs), env, extra_args=extra_args,
+ dependencies=dependencies)
+ if res:
+ return True, cached
+
+ # MSVC does not have compiler __builtin_-s.
+ if self.get_id() in {'msvc', 'intel-cl'}:
+ return False, False
+
+ # Detect function as a built-in
+ #
+ # Some functions like alloca() are defined as compiler built-ins which
+ # are inlined by the compiler and you can't take their address, so we
+ # need to look for them differently. On nice compilers like clang, we
+ # can just directly use the __has_builtin() macro.
+ fargs['no_includes'] = '#include' not in prefix
+ is_builtin = funcname.startswith('__builtin_')
+ fargs['is_builtin'] = is_builtin
+ fargs['__builtin_'] = '' if is_builtin else '__builtin_'
+ t = '''{prefix}
+ int main(void) {{
+
+ /* With some toolchains (MSYS2/mingw for example) the compiler
+ * provides various builtins which are not really implemented and
+ * fall back to the stdlib where they aren't provided and fail at
+ * build/link time. In case the user provides a header, including
+ * the header didn't lead to the function being defined, and the
+ * function we are checking isn't a builtin itself we assume the
+ * builtin is not functional and we just error out. */
+ #if !{no_includes:d} && !defined({func}) && !{is_builtin:d}
+ #error "No definition for {__builtin_}{func} found in the prefix"
+ #endif
+
+ #ifdef __has_builtin
+ #if !__has_builtin({__builtin_}{func})
+ #error "{__builtin_}{func} not found"
+ #endif
+ #elif ! defined({func})
+ {__builtin_}{func};
+ #endif
+ return 0;
+ }}'''
+ return self.links(t.format(**fargs), env, extra_args=extra_args,
+ dependencies=dependencies)
+
+ def has_members(self, typename: str, membernames: T.List[str],
+ prefix: str, env: 'Environment', *,
+ extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ if extra_args is None:
+ extra_args = []
+ # Create code that accesses all members
+ members = ''.join(f'foo.{member};\n' for member in membernames)
+ t = f'''{prefix}
+ void bar(void) {{
+ {typename} foo;
+ {members}
+ }}'''
+ return self.compiles(t, env, extra_args=extra_args,
+ dependencies=dependencies)
+
+ def has_type(self, typename: str, prefix: str, env: 'Environment',
+ extra_args: T.Union[T.List[str], T.Callable[[CompileCheckMode], T.List[str]]], *,
+ dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
+ t = f'''{prefix}
+ void bar(void) {{
+ sizeof({typename});
+ }}'''
+ return self.compiles(t, env, extra_args=extra_args,
+ dependencies=dependencies)
+
+ def _symbols_have_underscore_prefix_searchbin(self, env: 'Environment') -> bool:
+ '''
+ Check if symbols have underscore prefix by compiling a small test binary
+ and then searching the binary for the string,
+ '''
+ symbol_name = b'meson_uscore_prefix'
+ code = '''#ifdef __cplusplus
+ extern "C" {
+ #endif
+ void ''' + symbol_name.decode() + ''' (void) {}
+ #ifdef __cplusplus
+ }
+ #endif
+ '''
+ args = self.get_compiler_check_args(CompileCheckMode.COMPILE)
+ n = '_symbols_have_underscore_prefix_searchbin'
+ with self._build_wrapper(code, env, extra_args=args, mode='compile', want_output=True, temp_dir=env.scratch_dir) as p:
+ if p.returncode != 0:
+ raise RuntimeError(f'BUG: Unable to compile {n!r} check: {p.stderr}')
+ if not os.path.isfile(p.output_name):
+ raise RuntimeError(f'BUG: Can\'t find compiled test code for {n!r} check')
+ with open(p.output_name, 'rb') as o:
+ for line in o:
+ # Check if the underscore form of the symbol is somewhere
+ # in the output file.
+ if b'_' + symbol_name in line:
+ mlog.debug("Underscore prefix check found prefixed function in binary")
+ return True
+ # Else, check if the non-underscored form is present
+ elif symbol_name in line:
+ mlog.debug("Underscore prefix check found non-prefixed function in binary")
+ return False
+ raise RuntimeError(f'BUG: {n!r} check did not find symbol string in binary')
+
+ def _symbols_have_underscore_prefix_define(self, env: 'Environment') -> T.Optional[bool]:
+ '''
+ Check if symbols have underscore prefix by querying the
+ __USER_LABEL_PREFIX__ define that most compilers provide
+ for this. Return if functions have underscore prefix or None
+ if it was not possible to determine, like when the compiler
+ does not set the define or the define has an unexpected value.
+ '''
+ delim = '"MESON_HAVE_UNDERSCORE_DELIMITER" '
+ code = f'''
+ #ifndef __USER_LABEL_PREFIX__
+ #define MESON_UNDERSCORE_PREFIX unsupported
+ #else
+ #define MESON_UNDERSCORE_PREFIX __USER_LABEL_PREFIX__
+ #endif
+ {delim}MESON_UNDERSCORE_PREFIX
+ '''
+ with self._build_wrapper(code, env, mode='preprocess', want_output=False, temp_dir=env.scratch_dir) as p:
+ if p.returncode != 0:
+ raise RuntimeError(f'BUG: Unable to preprocess _symbols_have_underscore_prefix_define check: {p.stdout}')
+ symbol_prefix = p.stdout.partition(delim)[-1].rstrip()
+
+ mlog.debug(f'Queried compiler for function prefix: __USER_LABEL_PREFIX__ is "{symbol_prefix!s}"')
+ if symbol_prefix == '_':
+ return True
+ elif symbol_prefix == '':
+ return False
+ else:
+ return None
+
+ def _symbols_have_underscore_prefix_list(self, env: 'Environment') -> T.Optional[bool]:
+ '''
+ Check if symbols have underscore prefix by consulting a hardcoded
+ list of cases where we know the results.
+ Return if functions have underscore prefix or None if unknown.
+ '''
+ m = env.machines[self.for_machine]
+ # Darwin always uses the underscore prefix, not matter what
+ if m.is_darwin():
+ return True
+ # Windows uses the underscore prefix on x86 (32bit) only
+ if m.is_windows() or m.is_cygwin():
+ return m.cpu_family == 'x86'
+ return None
+
+ def symbols_have_underscore_prefix(self, env: 'Environment') -> bool:
+ '''
+ Check if the compiler prefixes an underscore to global C symbols
+ '''
+ # First, try to query the compiler directly
+ result = self._symbols_have_underscore_prefix_define(env)
+ if result is not None:
+ return result
+
+ # Else, try to consult a hardcoded list of cases we know
+ # absolutely have an underscore prefix
+ result = self._symbols_have_underscore_prefix_list(env)
+ if result is not None:
+ return result
+
+ # As a last resort, try search in a compiled binary, which is the
+ # most unreliable way of checking this, see #5482
+ return self._symbols_have_underscore_prefix_searchbin(env)
+
+ def _get_patterns(self, env: 'Environment', prefixes: T.List[str], suffixes: T.List[str], shared: bool = False) -> T.List[str]:
+ patterns = [] # type: T.List[str]
+ for p in prefixes:
+ for s in suffixes:
+ patterns.append(p + '{}.' + s)
+ if shared and env.machines[self.for_machine].is_openbsd():
+ # Shared libraries on OpenBSD can be named libfoo.so.X.Y:
+ # https://www.openbsd.org/faq/ports/specialtopics.html#SharedLibs
+ #
+ # This globbing is probably the best matching we can do since regex
+ # is expensive. It's wrong in many edge cases, but it will match
+ # correctly-named libraries and hopefully no one on OpenBSD names
+ # their files libfoo.so.9a.7b.1.0
+ for p in prefixes:
+ patterns.append(p + '{}.so.[0-9]*.[0-9]*')
+ return patterns
+
+ def get_library_naming(self, env: 'Environment', libtype: LibType, strict: bool = False) -> T.Tuple[str, ...]:
+ '''
+ Get library prefixes and suffixes for the target platform ordered by
+ priority
+ '''
+ stlibext = ['a']
+ # We've always allowed libname to be both `foo` and `libfoo`, and now
+ # people depend on it. Also, some people use prebuilt `foo.so` instead
+ # of `libfoo.so` for unknown reasons, and may also want to create
+ # `foo.so` by setting name_prefix to ''
+ if strict and not isinstance(self, VisualStudioLikeCompiler): # lib prefix is not usually used with msvc
+ prefixes = ['lib']
+ else:
+ prefixes = ['lib', '']
+ # Library suffixes and prefixes
+ if env.machines[self.for_machine].is_darwin():
+ shlibext = ['dylib', 'so']
+ elif env.machines[self.for_machine].is_windows():
+ # FIXME: .lib files can be import or static so we should read the
+ # file, figure out which one it is, and reject the wrong kind.
+ if isinstance(self, VisualStudioLikeCompiler):
+ shlibext = ['lib']
+ else:
+ shlibext = ['dll.a', 'lib', 'dll']
+ # Yep, static libraries can also be foo.lib
+ stlibext += ['lib']
+ elif env.machines[self.for_machine].is_cygwin():
+ shlibext = ['dll', 'dll.a']
+ prefixes = ['cyg'] + prefixes
+ else:
+ # Linux/BSDs
+ shlibext = ['so']
+ # Search priority
+ if libtype is LibType.PREFER_SHARED:
+ patterns = self._get_patterns(env, prefixes, shlibext, True)
+ patterns.extend([x for x in self._get_patterns(env, prefixes, stlibext, False) if x not in patterns])
+ elif libtype is LibType.PREFER_STATIC:
+ patterns = self._get_patterns(env, prefixes, stlibext, False)
+ patterns.extend([x for x in self._get_patterns(env, prefixes, shlibext, True) if x not in patterns])
+ elif libtype is LibType.SHARED:
+ patterns = self._get_patterns(env, prefixes, shlibext, True)
+ else:
+ assert libtype is LibType.STATIC
+ patterns = self._get_patterns(env, prefixes, stlibext, False)
+ return tuple(patterns)
+
+ @staticmethod
+ def _sort_shlibs_openbsd(libs: T.List[str]) -> T.List[str]:
+ filtered = [] # type: T.List[str]
+ for lib in libs:
+ # Validate file as a shared library of type libfoo.so.X.Y
+ ret = lib.rsplit('.so.', maxsplit=1)
+ if len(ret) != 2:
+ continue
+ try:
+ float(ret[1])
+ except ValueError:
+ continue
+ filtered.append(lib)
+ float_cmp = lambda x: float(x.rsplit('.so.', maxsplit=1)[1])
+ return sorted(filtered, key=float_cmp, reverse=True)
+
+ @classmethod
+ def _get_trials_from_pattern(cls, pattern: str, directory: str, libname: str) -> T.List[Path]:
+ f = Path(directory) / pattern.format(libname)
+ # Globbing for OpenBSD
+ if '*' in pattern:
+ # NOTE: globbing matches directories and broken symlinks
+ # so we have to do an isfile test on it later
+ return [Path(x) for x in cls._sort_shlibs_openbsd(glob.glob(str(f)))]
+ return [f]
+
+ @staticmethod
+ def _get_file_from_list(env: 'Environment', paths: T.List[Path]) -> Path:
+ '''
+ We just check whether the library exists. We can't do a link check
+ because the library might have unresolved symbols that require other
+ libraries. On macOS we check if the library matches our target
+ architecture.
+ '''
+ # If not building on macOS for Darwin, do a simple file check
+ if not env.machines.host.is_darwin() or not env.machines.build.is_darwin():
+ for p in paths:
+ if p.is_file():
+ return p
+ # Run `lipo` and check if the library supports the arch we want
+ for p in paths:
+ if not p.is_file():
+ continue
+ archs = mesonlib.darwin_get_object_archs(str(p))
+ if archs and env.machines.host.cpu_family in archs:
+ return p
+ else:
+ mlog.debug(f'Rejected {p}, supports {archs} but need {env.machines.host.cpu_family}')
+ return None
+
+ @functools.lru_cache()
+ def output_is_64bit(self, env: 'Environment') -> bool:
+ '''
+ returns true if the output produced is 64-bit, false if 32-bit
+ '''
+ return self.sizeof('void *', '', env) == 8
+
+ def _find_library_real(self, libname: str, env: 'Environment', extra_dirs: T.List[str], code: str, libtype: LibType) -> T.Optional[T.List[str]]:
+ # First try if we can just add the library as -l.
+ # Gcc + co seem to prefer builtin lib dirs to -L dirs.
+ # Only try to find std libs if no extra dirs specified.
+ # The built-in search procedure will always favour .so and then always
+ # search for .a. This is only allowed if libtype is LibType.PREFER_SHARED
+ if ((not extra_dirs and libtype is LibType.PREFER_SHARED) or
+ libname in self.internal_libs):
+ cargs = ['-l' + libname]
+ largs = self.get_linker_always_args() + self.get_allow_undefined_link_args()
+ extra_args = cargs + self.linker_to_compiler_args(largs)
+
+ if self.links(code, env, extra_args=extra_args, disable_cache=True)[0]:
+ return cargs
+ # Don't do a manual search for internal libs
+ if libname in self.internal_libs:
+ return None
+ # Not found or we want to use a specific libtype? Try to find the
+ # library file itself.
+ patterns = self.get_library_naming(env, libtype)
+ # try to detect if we are 64-bit or 32-bit. If we can't
+ # detect, we will just skip path validity checks done in
+ # get_library_dirs() call
+ try:
+ if self.output_is_64bit(env):
+ elf_class = 2
+ else:
+ elf_class = 1
+ except (mesonlib.MesonException, KeyError): # TODO evaluate if catching KeyError is wanted here
+ elf_class = 0
+ # Search in the specified dirs, and then in the system libraries
+ for d in itertools.chain(extra_dirs, self.get_library_dirs(env, elf_class)):
+ for p in patterns:
+ trials = self._get_trials_from_pattern(p, d, libname)
+ if not trials:
+ continue
+ trial = self._get_file_from_list(env, trials)
+ if not trial:
+ continue
+ if libname.startswith('lib') and trial.name.startswith(libname):
+ mlog.warning(f'find_library({libname!r}) starting in "lib" only works by accident and is not portable')
+ return [trial.as_posix()]
+ return None
+
+ def _find_library_impl(self, libname: str, env: 'Environment', extra_dirs: T.List[str],
+ code: str, libtype: LibType) -> T.Optional[T.List[str]]:
+ # These libraries are either built-in or invalid
+ if libname in self.ignore_libs:
+ return []
+ if isinstance(extra_dirs, str):
+ extra_dirs = [extra_dirs]
+ key = (tuple(self.exelist), libname, tuple(extra_dirs), code, libtype)
+ if key not in self.find_library_cache:
+ value = self._find_library_real(libname, env, extra_dirs, code, libtype)
+ self.find_library_cache[key] = value
+ else:
+ value = self.find_library_cache[key]
+ if value is None:
+ return None
+ return value.copy()
+
+ def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str],
+ libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]:
+ code = 'int main(void) { return 0; }\n'
+ return self._find_library_impl(libname, env, extra_dirs, code, libtype)
+
+ def find_framework_paths(self, env: 'Environment') -> T.List[str]:
+ '''
+ These are usually /Library/Frameworks and /System/Library/Frameworks,
+ unless you select a particular macOS SDK with the -isysroot flag.
+ You can also add to this by setting -F in CFLAGS.
+ '''
+ # TODO: this really needs to be *AppleClang*, not just any clang.
+ if self.id != 'clang':
+ raise mesonlib.MesonException('Cannot find framework path with non-clang compiler')
+ # Construct the compiler command-line
+ commands = self.get_exelist(ccache=False) + ['-v', '-E', '-']
+ commands += self.get_always_args()
+ # Add CFLAGS/CXXFLAGS/OBJCFLAGS/OBJCXXFLAGS from the env
+ commands += env.coredata.get_external_args(self.for_machine, self.language)
+ mlog.debug('Finding framework path by running: ', ' '.join(commands), '\n')
+ os_env = os.environ.copy()
+ os_env['LC_ALL'] = 'C'
+ _, _, stde = mesonlib.Popen_safe(commands, env=os_env, stdin=subprocess.PIPE)
+ paths = [] # T.List[str]
+ for line in stde.split('\n'):
+ if '(framework directory)' not in line:
+ continue
+ # line is of the form:
+ # ` /path/to/framework (framework directory)`
+ paths.append(line[:-21].strip())
+ return paths
+
+ def _find_framework_real(self, name: str, env: 'Environment', extra_dirs: T.List[str], allow_system: bool) -> T.Optional[T.List[str]]:
+ code = 'int main(void) { return 0; }'
+ link_args = []
+ for d in extra_dirs:
+ link_args += ['-F' + d]
+ # We can pass -Z to disable searching in the system frameworks, but
+ # then we must also pass -L/usr/lib to pick up libSystem.dylib
+ extra_args = [] if allow_system else ['-Z', '-L/usr/lib']
+ link_args += ['-framework', name]
+ if self.links(code, env, extra_args=(extra_args + link_args), disable_cache=True)[0]:
+ return link_args
+ return None
+
+ def _find_framework_impl(self, name: str, env: 'Environment', extra_dirs: T.List[str],
+ allow_system: bool) -> T.Optional[T.List[str]]:
+ if isinstance(extra_dirs, str):
+ extra_dirs = [extra_dirs]
+ key = (tuple(self.exelist), name, tuple(extra_dirs), allow_system)
+ if key in self.find_framework_cache:
+ value = self.find_framework_cache[key]
+ else:
+ value = self._find_framework_real(name, env, extra_dirs, allow_system)
+ self.find_framework_cache[key] = value
+ if value is None:
+ return None
+ return value.copy()
+
+ def find_framework(self, name: str, env: 'Environment', extra_dirs: T.List[str],
+ allow_system: bool = True) -> T.Optional[T.List[str]]:
+ '''
+ Finds the framework with the specified name, and returns link args for
+ the same or returns None when the framework is not found.
+ '''
+ # TODO: should probably check for macOS?
+ return self._find_framework_impl(name, env, extra_dirs, allow_system)
+
+ def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ # TODO: does this belong here or in GnuLike or maybe PosixLike?
+ return []
+
+ def get_crt_link_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ # TODO: does this belong here or in GnuLike or maybe PosixLike?
+ return []
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ # TODO: does this belong here or in GnuLike or maybe PosixLike?
+ host_m = env.machines[self.for_machine]
+ if host_m.is_haiku() or host_m.is_darwin():
+ return []
+ return ['-pthread']
+
+ def linker_to_compiler_args(self, args: T.List[str]) -> T.List[str]:
+ return args.copy()
+
+ def has_arguments(self, args: T.List[str], env: 'Environment', code: str,
+ mode: str) -> T.Tuple[bool, bool]:
+ return self.compiles(code, env, extra_args=args, mode=mode)
+
+ def _has_multi_arguments(self, args: T.List[str], env: 'Environment', code: str) -> T.Tuple[bool, bool]:
+ new_args = [] # type: T.List[str]
+ for arg in args:
+ # some compilers, e.g. GCC, don't warn for unsupported warning-disable
+ # flags, so when we are testing a flag like "-Wno-forgotten-towel", also
+ # check the equivalent enable flag too "-Wforgotten-towel"
+ if arg.startswith('-Wno-'):
+ new_args.append('-W' + arg[5:])
+ if arg.startswith('-Wl,'):
+ mlog.warning(f'{arg} looks like a linker argument, '
+ 'but has_argument and other similar methods only '
+ 'support checking compiler arguments. Using them '
+ 'to check linker arguments are never supported, '
+ 'and results are likely to be wrong regardless of '
+ 'the compiler you are using. has_link_argument or '
+ 'other similar method can be used instead.')
+ new_args.append(arg)
+ return self.has_arguments(new_args, env, code, mode='compile')
+
+ def has_multi_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]:
+ return self._has_multi_arguments(args, env, 'extern int i;\nint i;\n')
+
+ def _has_multi_link_arguments(self, args: T.List[str], env: 'Environment', code: str) -> T.Tuple[bool, bool]:
+ # First time we check for link flags we need to first check if we have
+ # --fatal-warnings, otherwise some linker checks could give some
+ # false positive.
+ args = self.linker.fatal_warnings() + args
+ args = self.linker_to_compiler_args(args)
+ return self.has_arguments(args, env, code, mode='link')
+
+ def has_multi_link_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]:
+ return self._has_multi_link_arguments(args, env, 'int main(void) { return 0; }\n')
+
+ @staticmethod
+ def _concatenate_string_literals(s: str) -> str:
+ pattern = re.compile(r'(?P<pre>.*([^\\]")|^")(?P<str1>([^\\"]|\\.)*)"\s+"(?P<str2>([^\\"]|\\.)*)(?P<post>".*)')
+ ret = s
+ m = pattern.match(ret)
+ while m:
+ ret = ''.join(m.group('pre', 'str1', 'str2', 'post'))
+ m = pattern.match(ret)
+ return ret
+
+ def get_has_func_attribute_extra_args(self, name: str) -> T.List[str]:
+ # Most compilers (such as GCC and Clang) only warn about unknown or
+ # ignored attributes, so force an error. Overridden in GCC and Clang
+ # mixins.
+ return ['-Werror']
+
+ def has_func_attribute(self, name: str, env: 'Environment') -> T.Tuple[bool, bool]:
+ # Just assume that if we're not on windows that dllimport and dllexport
+ # don't work
+ m = env.machines[self.for_machine]
+ if not (m.is_windows() or m.is_cygwin()):
+ if name in {'dllimport', 'dllexport'}:
+ return False, False
+
+ return self.compiles(self.attribute_check_func(name), env,
+ extra_args=self.get_has_func_attribute_extra_args(name))
+
+ def get_disable_assert_args(self) -> T.List[str]:
+ return ['-DNDEBUG']
+
+ @functools.lru_cache(maxsize=None)
+ def can_compile(self, src: 'mesonlib.FileOrString') -> bool:
+ # Files we preprocess can be anything, e.g. .in
+ if self.mode == 'PREPROCESSOR':
+ return True
+ return super().can_compile(src)
+
+ def get_preprocessor(self) -> Compiler:
+ if not self.preprocessor:
+ self.preprocessor = copy.copy(self)
+ self.preprocessor.exelist = self.exelist + self.get_preprocess_to_file_args()
+ self.preprocessor.mode = 'PREPROCESSOR'
+ self.modes.append(self.preprocessor)
+ return self.preprocessor
diff --git a/mesonbuild/compilers/mixins/compcert.py b/mesonbuild/compilers/mixins/compcert.py
new file mode 100644
index 0000000..a0394a9
--- /dev/null
+++ b/mesonbuild/compilers/mixins/compcert.py
@@ -0,0 +1,137 @@
+# Copyright 2012-2019 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.
+from __future__ import annotations
+
+"""Representations specific to the CompCert C compiler family."""
+
+import os
+import re
+import typing as T
+
+if T.TYPE_CHECKING:
+ from envconfig import MachineInfo
+ from ...environment import Environment
+ from ...compilers.compilers import Compiler
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+ccomp_buildtype_args = {
+ 'plain': [''],
+ 'debug': ['-O0', '-g'],
+ 'debugoptimized': ['-O0', '-g'],
+ 'release': ['-O3'],
+ 'minsize': ['-Os'],
+ 'custom': ['-Obranchless'],
+} # type: T.Dict[str, T.List[str]]
+
+ccomp_optimization_args = {
+ 'plain': [],
+ '0': ['-O0'],
+ 'g': ['-O0'],
+ '1': ['-O1'],
+ '2': ['-O2'],
+ '3': ['-O3'],
+ 's': ['-Os']
+} # type: T.Dict[str, T.List[str]]
+
+ccomp_debug_args = {
+ False: [],
+ True: ['-g']
+} # type: T.Dict[bool, T.List[str]]
+
+# As of CompCert 20.04, these arguments should be passed to the underlying gcc linker (via -WUl,<arg>)
+# There are probably (many) more, but these are those used by picolibc
+ccomp_args_to_wul = [
+ r"^-ffreestanding$",
+ r"^-r$"
+] # type: T.List[str]
+
+class CompCertCompiler(Compiler):
+
+ id = 'ccomp'
+
+ def __init__(self) -> None:
+ # Assembly
+ self.can_compile_suffixes.add('s')
+ default_warn_args = [] # type: T.List[str]
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + [],
+ '3': default_warn_args + [],
+ 'everything': default_warn_args + []} # type: T.Dict[str, T.List[str]]
+
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+ def get_pic_args(self) -> T.List[str]:
+ # As of now, CompCert does not support PIC
+ return []
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return ccomp_buildtype_args[buildtype]
+
+ def get_pch_suffix(self) -> str:
+ return 'pch'
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ return []
+
+ @classmethod
+ def _unix_args_to_native(cls, args: T.List[str], info: MachineInfo) -> T.List[str]:
+ "Always returns a copy that can be independently mutated"
+ patched_args = [] # type: T.List[str]
+ for arg in args:
+ added = 0
+ for ptrn in ccomp_args_to_wul:
+ if re.match(ptrn, arg):
+ patched_args.append('-WUl,' + arg)
+ added = 1
+ if not added:
+ patched_args.append(arg)
+ return patched_args
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def get_preprocess_only_args(self) -> T.List[str]:
+ return ['-E']
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return ['-c']
+
+ def get_coverage_args(self) -> T.List[str]:
+ return []
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ return ['-nostdinc']
+
+ def get_no_stdlib_link_args(self) -> T.List[str]:
+ return ['-nostdlib']
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return ccomp_optimization_args[optimization_level]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return ccomp_debug_args[is_debug]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:9] == '-I':
+ parameter_list[idx] = i[:9] + os.path.normpath(os.path.join(build_dir, i[9:]))
+
+ return parameter_list
diff --git a/mesonbuild/compilers/mixins/elbrus.py b/mesonbuild/compilers/mixins/elbrus.py
new file mode 100644
index 0000000..872649b
--- /dev/null
+++ b/mesonbuild/compilers/mixins/elbrus.py
@@ -0,0 +1,101 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+"""Abstractions for the Elbrus family of compilers."""
+
+import os
+import typing as T
+import subprocess
+import re
+
+from .gnu import GnuLikeCompiler
+from .gnu import gnu_optimization_args
+from ...mesonlib import Popen_safe, OptionKey
+
+if T.TYPE_CHECKING:
+ from ...environment import Environment
+ from ...coredata import KeyedOptionDictType
+
+
+class ElbrusCompiler(GnuLikeCompiler):
+ # Elbrus compiler is nearly like GCC, but does not support
+ # PCH, LTO, sanitizers and color output as of version 1.21.x.
+
+ id = 'lcc'
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.base_options = {OptionKey(o) for o in ['b_pgo', 'b_coverage', 'b_ndebug', 'b_staticpic', 'b_lundef', 'b_asneeded']}
+ default_warn_args = ['-Wall']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': default_warn_args + ['-Wextra', '-Wpedantic']}
+
+ # FIXME: use _build_wrapper to call this so that linker flags from the env
+ # get applied
+ def get_library_dirs(self, env: 'Environment', elf_class: T.Optional[int] = None) -> T.List[str]:
+ os_env = os.environ.copy()
+ os_env['LC_ALL'] = 'C'
+ stdo = Popen_safe(self.get_exelist(ccache=False) + ['--print-search-dirs'], env=os_env)[1]
+ for line in stdo.split('\n'):
+ if line.startswith('libraries:'):
+ # lcc does not include '=' in --print-search-dirs output. Also it could show nonexistent dirs.
+ libstr = line.split(' ', 1)[1]
+ return [os.path.realpath(p) for p in libstr.split(':') if os.path.exists(p)]
+ return []
+
+ def get_program_dirs(self, env: 'Environment') -> T.List[str]:
+ os_env = os.environ.copy()
+ os_env['LC_ALL'] = 'C'
+ stdo = Popen_safe(self.get_exelist(ccache=False) + ['--print-search-dirs'], env=os_env)[1]
+ for line in stdo.split('\n'):
+ if line.startswith('programs:'):
+ # lcc does not include '=' in --print-search-dirs output.
+ libstr = line.split(' ', 1)[1]
+ return [os.path.realpath(p) for p in libstr.split(':')]
+ return []
+
+ def get_default_include_dirs(self) -> T.List[str]:
+ os_env = os.environ.copy()
+ os_env['LC_ALL'] = 'C'
+ p = subprocess.Popen(self.get_exelist(ccache=False) + ['-xc', '-E', '-v', '-'], env=os_env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stderr = p.stderr.read().decode('utf-8', errors='replace')
+ includes = []
+ for line in stderr.split('\n'):
+ if line.lstrip().startswith('--sys_include'):
+ includes.append(re.sub(r'\s*\\$', '', re.sub(r'^\s*--sys_include\s*', '', line)))
+ return includes
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return gnu_optimization_args[optimization_level]
+
+ def get_prelink_args(self, prelink_name: str, obj_list: T.List[str]) -> T.List[str]:
+ return ['-r', '-nodefaultlibs', '-nostartfiles', '-o', prelink_name] + obj_list
+
+ def get_pch_suffix(self) -> str:
+ # Actually it's not supported for now, but probably will be supported in future
+ return 'pch'
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ std = options[OptionKey('std', lang=self.language, machine=self.for_machine)]
+ if std.value != 'none':
+ args.append('-std=' + std.value)
+ return args
+
+ def openmp_flags(self) -> T.List[str]:
+ return ['-fopenmp']
diff --git a/mesonbuild/compilers/mixins/emscripten.py b/mesonbuild/compilers/mixins/emscripten.py
new file mode 100644
index 0000000..77b1c33
--- /dev/null
+++ b/mesonbuild/compilers/mixins/emscripten.py
@@ -0,0 +1,101 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+"""Provides a mixin for shared code between C and C++ Emscripten compilers."""
+
+import os.path
+import typing as T
+
+from ... import coredata
+from ... import mesonlib
+from ...mesonlib import OptionKey
+from ...mesonlib import LibType
+
+if T.TYPE_CHECKING:
+ from ...environment import Environment
+ from ...compilers.compilers import Compiler
+ from ...dependencies import Dependency
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+
+def wrap_js_includes(args: T.List[str]) -> T.List[str]:
+ final_args = []
+ for i in args:
+ if i.endswith('.js') and not i.startswith('-'):
+ final_args += ['--js-library', i]
+ else:
+ final_args += [i]
+ return final_args
+
+class EmscriptenMixin(Compiler):
+
+ def _get_compile_output(self, dirname: str, mode: str) -> str:
+ # In pre-processor mode, the output is sent to stdout and discarded
+ if mode == 'preprocess':
+ return None
+ # Unlike sane toolchains, emcc infers the kind of output from its name.
+ # This is the only reason why this method is overridden; compiler tests
+ # do not work well with the default exe/obj suffices.
+ if mode == 'link':
+ suffix = 'js'
+ else:
+ suffix = 'o'
+ return os.path.join(dirname, 'output.' + suffix)
+
+ def thread_link_flags(self, env: 'Environment') -> T.List[str]:
+ args = ['-pthread']
+ count: int = env.coredata.options[OptionKey('thread_count', lang=self.language, machine=self.for_machine)].value
+ if count:
+ args.append(f'-sPTHREAD_POOL_SIZE={count}')
+ return args
+
+ def get_options(self) -> 'coredata.MutableKeyedOptionDictType':
+ opts = super().get_options()
+ key = OptionKey('thread_count', machine=self.for_machine, lang=self.language)
+ opts.update({
+ key: coredata.UserIntegerOption(
+ 'Number of threads to use in web assembly, set to 0 to disable',
+ (0, None, 4), # Default was picked at random
+ ),
+ })
+
+ return opts
+
+ @classmethod
+ def native_args_to_unix(cls, args: T.List[str]) -> T.List[str]:
+ return wrap_js_includes(super().native_args_to_unix(args))
+
+ def get_dependency_link_args(self, dep: 'Dependency') -> T.List[str]:
+ return wrap_js_includes(super().get_dependency_link_args(dep))
+
+ def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str],
+ libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]:
+ if not libname.endswith('.js'):
+ return super().find_library(libname, env, extra_dirs, libtype)
+ if os.path.isabs(libname):
+ if os.path.exists(libname):
+ return [libname]
+ if len(extra_dirs) == 0:
+ raise mesonlib.EnvironmentException('Looking up Emscripten JS libraries requires either an absolute path or specifying extra_dirs.')
+ for d in extra_dirs:
+ abs_path = os.path.join(d, libname)
+ if os.path.exists(abs_path):
+ return [abs_path]
+ return None
diff --git a/mesonbuild/compilers/mixins/gnu.py b/mesonbuild/compilers/mixins/gnu.py
new file mode 100644
index 0000000..022e7fd
--- /dev/null
+++ b/mesonbuild/compilers/mixins/gnu.py
@@ -0,0 +1,649 @@
+# Copyright 2019-2022 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.
+from __future__ import annotations
+
+"""Provides mixins for GNU compilers and GNU-like compilers."""
+
+import abc
+import functools
+import os
+import multiprocessing
+import pathlib
+import re
+import subprocess
+import typing as T
+
+from ... import mesonlib
+from ... import mlog
+from ...mesonlib import OptionKey
+
+if T.TYPE_CHECKING:
+ from ..._typing import ImmutableListProtocol
+ from ...environment import Environment
+ from ..compilers import Compiler
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+# XXX: prevent circular references.
+# FIXME: this really is a posix interface not a c-like interface
+clike_debug_args = {
+ False: [],
+ True: ['-g'],
+} # type: T.Dict[bool, T.List[str]]
+
+gnulike_buildtype_args = {
+ 'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+} # type: T.Dict[str, T.List[str]]
+
+gnu_optimization_args = {
+ 'plain': [],
+ '0': ['-O0'],
+ 'g': ['-Og'],
+ '1': ['-O1'],
+ '2': ['-O2'],
+ '3': ['-O3'],
+ 's': ['-Os'],
+} # type: T.Dict[str, T.List[str]]
+
+gnulike_instruction_set_args = {
+ 'mmx': ['-mmmx'],
+ 'sse': ['-msse'],
+ 'sse2': ['-msse2'],
+ 'sse3': ['-msse3'],
+ 'ssse3': ['-mssse3'],
+ 'sse41': ['-msse4.1'],
+ 'sse42': ['-msse4.2'],
+ 'avx': ['-mavx'],
+ 'avx2': ['-mavx2'],
+ 'neon': ['-mfpu=neon'],
+} # type: T.Dict[str, T.List[str]]
+
+gnu_symbol_visibility_args = {
+ '': [],
+ 'default': ['-fvisibility=default'],
+ 'internal': ['-fvisibility=internal'],
+ 'hidden': ['-fvisibility=hidden'],
+ 'protected': ['-fvisibility=protected'],
+ 'inlineshidden': ['-fvisibility=hidden', '-fvisibility-inlines-hidden'],
+} # type: T.Dict[str, T.List[str]]
+
+gnu_color_args = {
+ 'auto': ['-fdiagnostics-color=auto'],
+ 'always': ['-fdiagnostics-color=always'],
+ 'never': ['-fdiagnostics-color=never'],
+} # type: T.Dict[str, T.List[str]]
+
+# Warnings collected from the GCC source and documentation. This is an
+# objective set of all the warnings flags that apply to general projects: the
+# only ones omitted are those that require a project-specific value, or are
+# related to non-standard or legacy language support. This behaves roughly
+# like -Weverything in clang. Warnings implied by -Wall, -Wextra, or
+# higher-level warnings already enabled here are not included in these lists to
+# keep them as short as possible. History goes back to GCC 3.0.0, everything
+# earlier is considered historical and listed under version 0.0.0.
+
+# GCC warnings for all C-family languages
+# Omitted non-general warnings:
+# -Wabi=
+# -Waggregate-return
+# -Walloc-size-larger-than=BYTES
+# -Walloca-larger-than=BYTES
+# -Wframe-larger-than=BYTES
+# -Wlarger-than=BYTES
+# -Wstack-usage=BYTES
+# -Wsystem-headers
+# -Wtrampolines
+# -Wvla-larger-than=BYTES
+#
+# Omitted warnings enabled elsewhere in meson:
+# -Winvalid-pch (GCC 3.4.0)
+gnu_common_warning_args = {
+ "0.0.0": [
+ "-Wcast-qual",
+ "-Wconversion",
+ "-Wfloat-equal",
+ "-Wformat=2",
+ "-Winline",
+ "-Wmissing-declarations",
+ "-Wredundant-decls",
+ "-Wshadow",
+ "-Wundef",
+ "-Wuninitialized",
+ "-Wwrite-strings",
+ ],
+ "3.0.0": [
+ "-Wdisabled-optimization",
+ "-Wpacked",
+ "-Wpadded",
+ ],
+ "3.3.0": [
+ "-Wmultichar",
+ "-Wswitch-default",
+ "-Wswitch-enum",
+ "-Wunused-macros",
+ ],
+ "4.0.0": [
+ "-Wmissing-include-dirs",
+ ],
+ "4.1.0": [
+ "-Wunsafe-loop-optimizations",
+ "-Wstack-protector",
+ ],
+ "4.2.0": [
+ "-Wstrict-overflow=5",
+ ],
+ "4.3.0": [
+ "-Warray-bounds=2",
+ "-Wlogical-op",
+ "-Wstrict-aliasing=3",
+ "-Wvla",
+ ],
+ "4.6.0": [
+ "-Wdouble-promotion",
+ "-Wsuggest-attribute=const",
+ "-Wsuggest-attribute=noreturn",
+ "-Wsuggest-attribute=pure",
+ "-Wtrampolines",
+ ],
+ "4.7.0": [
+ "-Wvector-operation-performance",
+ ],
+ "4.8.0": [
+ "-Wsuggest-attribute=format",
+ ],
+ "4.9.0": [
+ "-Wdate-time",
+ ],
+ "5.1.0": [
+ "-Wformat-signedness",
+ "-Wnormalized=nfc",
+ ],
+ "6.1.0": [
+ "-Wduplicated-cond",
+ "-Wnull-dereference",
+ "-Wshift-negative-value",
+ "-Wshift-overflow=2",
+ "-Wunused-const-variable=2",
+ ],
+ "7.1.0": [
+ "-Walloca",
+ "-Walloc-zero",
+ "-Wformat-overflow=2",
+ "-Wformat-truncation=2",
+ "-Wstringop-overflow=3",
+ ],
+ "7.2.0": [
+ "-Wduplicated-branches",
+ ],
+ "8.1.0": [
+ "-Wattribute-alias=2",
+ "-Wcast-align=strict",
+ "-Wsuggest-attribute=cold",
+ "-Wsuggest-attribute=malloc",
+ ],
+ "10.1.0": [
+ "-Wanalyzer-too-complex",
+ "-Warith-conversion",
+ ],
+ "12.1.0": [
+ "-Wbidi-chars=ucn",
+ "-Wopenacc-parallelism",
+ "-Wtrivial-auto-var-init",
+ ],
+} # type: T.Dict[str, T.List[str]]
+
+# GCC warnings for C
+# Omitted non-general or legacy warnings:
+# -Wc11-c2x-compat
+# -Wc90-c99-compat
+# -Wc99-c11-compat
+# -Wdeclaration-after-statement
+# -Wtraditional
+# -Wtraditional-conversion
+gnu_c_warning_args = {
+ "0.0.0": [
+ "-Wbad-function-cast",
+ "-Wmissing-prototypes",
+ "-Wnested-externs",
+ "-Wstrict-prototypes",
+ ],
+ "3.4.0": [
+ "-Wold-style-definition",
+ "-Winit-self",
+ ],
+ "4.1.0": [
+ "-Wc++-compat",
+ ],
+ "4.5.0": [
+ "-Wunsuffixed-float-constants",
+ ],
+} # type: T.Dict[str, T.List[str]]
+
+# GCC warnings for C++
+# Omitted non-general or legacy warnings:
+# -Wc++0x-compat
+# -Wc++1z-compat
+# -Wc++2a-compat
+# -Wctad-maybe-unsupported
+# -Wnamespaces
+# -Wtemplates
+gnu_cpp_warning_args = {
+ "0.0.0": [
+ "-Wctor-dtor-privacy",
+ "-Weffc++",
+ "-Wnon-virtual-dtor",
+ "-Wold-style-cast",
+ "-Woverloaded-virtual",
+ "-Wsign-promo",
+ ],
+ "4.0.1": [
+ "-Wstrict-null-sentinel",
+ ],
+ "4.6.0": [
+ "-Wnoexcept",
+ ],
+ "4.7.0": [
+ "-Wzero-as-null-pointer-constant",
+ ],
+ "4.8.0": [
+ "-Wabi-tag",
+ "-Wuseless-cast",
+ ],
+ "4.9.0": [
+ "-Wconditionally-supported",
+ ],
+ "5.1.0": [
+ "-Wsuggest-final-methods",
+ "-Wsuggest-final-types",
+ "-Wsuggest-override",
+ ],
+ "6.1.0": [
+ "-Wmultiple-inheritance",
+ "-Wplacement-new=2",
+ "-Wvirtual-inheritance",
+ ],
+ "7.1.0": [
+ "-Waligned-new=all",
+ "-Wnoexcept-type",
+ "-Wregister",
+ ],
+ "8.1.0": [
+ "-Wcatch-value=3",
+ "-Wextra-semi",
+ ],
+ "9.1.0": [
+ "-Wdeprecated-copy-dtor",
+ "-Wredundant-move",
+ ],
+ "10.1.0": [
+ "-Wcomma-subscript",
+ "-Wmismatched-tags",
+ "-Wredundant-tags",
+ "-Wvolatile",
+ ],
+ "11.1.0": [
+ "-Wdeprecated-enum-enum-conversion",
+ "-Wdeprecated-enum-float-conversion",
+ "-Winvalid-imported-macros",
+ ],
+} # type: T.Dict[str, T.List[str]]
+
+# GCC warnings for Objective C and Objective C++
+# Omitted non-general or legacy warnings:
+# -Wtraditional
+# -Wtraditional-conversion
+gnu_objc_warning_args = {
+ "0.0.0": [
+ "-Wselector",
+ ],
+ "3.3": [
+ "-Wundeclared-selector",
+ ],
+ "4.1.0": [
+ "-Wassign-intercept",
+ "-Wstrict-selector-match",
+ ],
+} # type: T.Dict[str, T.List[str]]
+
+
+@functools.lru_cache(maxsize=None)
+def gnulike_default_include_dirs(compiler: T.Tuple[str, ...], lang: str) -> 'ImmutableListProtocol[str]':
+ lang_map = {
+ 'c': 'c',
+ 'cpp': 'c++',
+ 'objc': 'objective-c',
+ 'objcpp': 'objective-c++'
+ }
+ if lang not in lang_map:
+ return []
+ lang = lang_map[lang]
+ env = os.environ.copy()
+ env["LC_ALL"] = 'C'
+ cmd = list(compiler) + [f'-x{lang}', '-E', '-v', '-']
+ _, stdout, _ = mesonlib.Popen_safe(cmd, stderr=subprocess.STDOUT, env=env)
+ parse_state = 0
+ paths = [] # type: T.List[str]
+ for line in stdout.split('\n'):
+ line = line.strip(' \n\r\t')
+ if parse_state == 0:
+ if line == '#include "..." search starts here:':
+ parse_state = 1
+ elif parse_state == 1:
+ if line == '#include <...> search starts here:':
+ parse_state = 2
+ else:
+ paths.append(line)
+ elif parse_state == 2:
+ if line == 'End of search list.':
+ break
+ else:
+ paths.append(line)
+ if not paths:
+ mlog.warning('No include directory found parsing "{cmd}" output'.format(cmd=" ".join(cmd)))
+ # Append a normalized copy of paths to make path lookup easier
+ paths += [os.path.normpath(x) for x in paths]
+ return paths
+
+
+class GnuLikeCompiler(Compiler, metaclass=abc.ABCMeta):
+ """
+ GnuLikeCompiler is a common interface to all compilers implementing
+ the GNU-style commandline interface. This includes GCC, Clang
+ and ICC. Certain functionality between them is different and requires
+ that the actual concrete subclass define their own implementation.
+ """
+
+ LINKER_PREFIX = '-Wl,'
+
+ def __init__(self) -> None:
+ self.base_options = {
+ OptionKey(o) for o in ['b_pch', 'b_lto', 'b_pgo', 'b_coverage',
+ 'b_ndebug', 'b_staticpic', 'b_pie']}
+ if not (self.info.is_windows() or self.info.is_cygwin() or self.info.is_openbsd()):
+ self.base_options.add(OptionKey('b_lundef'))
+ if not self.info.is_windows() or self.info.is_cygwin():
+ self.base_options.add(OptionKey('b_asneeded'))
+ if not self.info.is_hurd():
+ self.base_options.add(OptionKey('b_sanitize'))
+ # All GCC-like backends can do assembly
+ self.can_compile_suffixes.add('s')
+
+ def get_pic_args(self) -> T.List[str]:
+ if self.info.is_windows() or self.info.is_cygwin() or self.info.is_darwin():
+ return [] # On Window and OS X, pic is always on.
+ return ['-fPIC']
+
+ def get_pie_args(self) -> T.List[str]:
+ return ['-fPIE']
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return gnulike_buildtype_args[buildtype]
+
+ @abc.abstractmethod
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ pass
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return clike_debug_args[is_debug]
+
+ @abc.abstractmethod
+ def get_pch_suffix(self) -> str:
+ pass
+
+ def split_shlib_to_parts(self, fname: str) -> T.Tuple[str, str]:
+ return os.path.dirname(fname), fname
+
+ def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
+ return gnulike_instruction_set_args.get(instruction_set, None)
+
+ def get_default_include_dirs(self) -> T.List[str]:
+ return gnulike_default_include_dirs(tuple(self.get_exelist(ccache=False)), self.language).copy()
+
+ @abc.abstractmethod
+ def openmp_flags(self) -> T.List[str]:
+ pass
+
+ def gnu_symbol_visibility_args(self, vistype: str) -> T.List[str]:
+ if vistype == 'inlineshidden' and self.language not in {'cpp', 'objcpp'}:
+ vistype = 'hidden'
+ return gnu_symbol_visibility_args[vistype]
+
+ def gen_vs_module_defs_args(self, defsfile: str) -> T.List[str]:
+ if not isinstance(defsfile, str):
+ raise RuntimeError('Module definitions file should be str')
+ # On Windows targets, .def files may be specified on the linker command
+ # line like an object file.
+ if self.info.is_windows() or self.info.is_cygwin():
+ return [defsfile]
+ # For other targets, discard the .def file.
+ return []
+
+ def get_argument_syntax(self) -> str:
+ return 'gcc'
+
+ def get_profile_generate_args(self) -> T.List[str]:
+ return ['-fprofile-generate']
+
+ def get_profile_use_args(self) -> T.List[str]:
+ return ['-fprofile-use']
+
+ def get_gui_app_args(self, value: bool) -> T.List[str]:
+ return ['-mwindows' if value else '-mconsole']
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I' or i[:2] == '-L':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+
+ return parameter_list
+
+ @functools.lru_cache()
+ def _get_search_dirs(self, env: 'Environment') -> str:
+ extra_args = ['--print-search-dirs']
+ with self._build_wrapper('', env, extra_args=extra_args,
+ dependencies=None, mode='compile',
+ want_output=True) as p:
+ return p.stdout
+
+ def _split_fetch_real_dirs(self, pathstr: str) -> T.List[str]:
+ # We need to use the path separator used by the compiler for printing
+ # lists of paths ("gcc --print-search-dirs"). By default
+ # we assume it uses the platform native separator.
+ pathsep = os.pathsep
+
+ # clang uses ':' instead of ';' on Windows https://reviews.llvm.org/D61121
+ # so we need to repair things like 'C:\foo:C:\bar'
+ if pathsep == ';':
+ pathstr = re.sub(r':([^/\\])', r';\1', pathstr)
+
+ # pathlib treats empty paths as '.', so filter those out
+ paths = [p for p in pathstr.split(pathsep) if p]
+
+ result = []
+ for p in paths:
+ # GCC returns paths like this:
+ # /usr/lib/gcc/x86_64-linux-gnu/8/../../../../x86_64-linux-gnu/lib
+ # It would make sense to normalize them to get rid of the .. parts
+ # Sadly when you are on a merged /usr fs it also kills these:
+ # /lib/x86_64-linux-gnu
+ # since /lib is a symlink to /usr/lib. This would mean
+ # paths under /lib would be considered not a "system path",
+ # which is wrong and breaks things. Store everything, just to be sure.
+ pobj = pathlib.Path(p)
+ unresolved = pobj.as_posix()
+ if pobj.exists():
+ if unresolved not in result:
+ result.append(unresolved)
+ try:
+ resolved = pathlib.Path(p).resolve().as_posix()
+ if resolved not in result:
+ result.append(resolved)
+ except FileNotFoundError:
+ pass
+ return result
+
+ def get_compiler_dirs(self, env: 'Environment', name: str) -> T.List[str]:
+ '''
+ Get dirs from the compiler, either `libraries:` or `programs:`
+ '''
+ stdo = self._get_search_dirs(env)
+ for line in stdo.split('\n'):
+ if line.startswith(name + ':'):
+ return self._split_fetch_real_dirs(line.split('=', 1)[1])
+ return []
+
+ def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]:
+ # This provides a base for many compilers, GCC and Clang override this
+ # for their specific arguments
+ return ['-flto']
+
+ def sanitizer_compile_args(self, value: str) -> T.List[str]:
+ if value == 'none':
+ return []
+ args = ['-fsanitize=' + value]
+ if 'address' in value: # for -fsanitize=address,undefined
+ args.append('-fno-omit-frame-pointer')
+ return args
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return ['-o', target]
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return ['-MD', '-MQ', outtarget, '-MF', outfile]
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return ['-c']
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if not path:
+ path = '.'
+ if is_system:
+ return ['-isystem' + path]
+ return ['-I' + path]
+
+ @classmethod
+ def use_linker_args(cls, linker: str, version: str) -> T.List[str]:
+ if linker not in {'gold', 'bfd', 'lld'}:
+ raise mesonlib.MesonException(
+ f'Unsupported linker, only bfd, gold, and lld are supported, not {linker}.')
+ return [f'-fuse-ld={linker}']
+
+ def get_coverage_args(self) -> T.List[str]:
+ return ['--coverage']
+
+ def get_preprocess_to_file_args(self) -> T.List[str]:
+ # We want to allow preprocessing files with any extension, such as
+ # foo.c.in. In that case we need to tell GCC/CLANG to treat them as
+ # assembly file.
+ return self.get_preprocess_only_args() + ['-x', 'assembler-with-cpp']
+
+
+class GnuCompiler(GnuLikeCompiler):
+ """
+ GnuCompiler represents an actual GCC in its many incarnations.
+ Compilers imitating GCC (Clang/Intel) should use the GnuLikeCompiler ABC.
+ """
+ id = 'gcc'
+
+ def __init__(self, defines: T.Optional[T.Dict[str, str]]):
+ super().__init__()
+ self.defines = defines or {}
+ self.base_options.update({OptionKey('b_colorout'), OptionKey('b_lto_threads')})
+
+ def get_colorout_args(self, colortype: str) -> T.List[str]:
+ if mesonlib.version_compare(self.version, '>=4.9.0'):
+ return gnu_color_args[colortype][:]
+ return []
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ # Mypy doesn't understand cooperative inheritance
+ args = super().get_warn_args(level)
+ if mesonlib.version_compare(self.version, '<4.8.0') and '-Wpedantic' in args:
+ # -Wpedantic was added in 4.8.0
+ # https://gcc.gnu.org/gcc-4.8/changes.html
+ args[args.index('-Wpedantic')] = '-pedantic'
+ return args
+
+ def supported_warn_args(self, warn_args_by_version: T.Dict[str, T.List[str]]) -> T.List[str]:
+ result = []
+ for version, warn_args in warn_args_by_version.items():
+ if mesonlib.version_compare(self.version, '>=' + version):
+ result += warn_args
+ return result
+
+ def has_builtin_define(self, define: str) -> bool:
+ return define in self.defines
+
+ def get_builtin_define(self, define: str) -> T.Optional[str]:
+ if define in self.defines:
+ return self.defines[define]
+ return None
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return gnu_optimization_args[optimization_level]
+
+ def get_pch_suffix(self) -> str:
+ return 'gch'
+
+ def openmp_flags(self) -> T.List[str]:
+ return ['-fopenmp']
+
+ def has_arguments(self, args: T.List[str], env: 'Environment', code: str,
+ mode: str) -> T.Tuple[bool, bool]:
+ # For some compiler command line arguments, the GNU compilers will
+ # emit a warning on stderr indicating that an option is valid for a
+ # another language, but still complete with exit_success
+ with self._build_wrapper(code, env, args, None, mode) as p:
+ result = p.returncode == 0
+ if self.language in {'cpp', 'objcpp'} and 'is valid for C/ObjC' in p.stderr:
+ result = False
+ if self.language in {'c', 'objc'} and 'is valid for C++/ObjC++' in p.stderr:
+ result = False
+ return result, p.cached
+
+ def get_has_func_attribute_extra_args(self, name: str) -> T.List[str]:
+ # GCC only warns about unknown or ignored attributes, so force an
+ # error.
+ return ['-Werror=attributes']
+
+ def get_prelink_args(self, prelink_name: str, obj_list: T.List[str]) -> T.List[str]:
+ return ['-r', '-o', prelink_name] + obj_list
+
+ def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]:
+ if threads == 0:
+ if mesonlib.version_compare(self.version, '>= 10.0'):
+ return ['-flto=auto']
+ # This matches clang's behavior of using the number of cpus
+ return [f'-flto={multiprocessing.cpu_count()}']
+ elif threads > 0:
+ return [f'-flto={threads}']
+ return super().get_lto_compile_args(threads=threads)
+
+ @classmethod
+ def use_linker_args(cls, linker: str, version: str) -> T.List[str]:
+ if linker == 'mold' and mesonlib.version_compare(version, '>=12.0.1'):
+ return ['-fuse-ld=mold']
+ return super().use_linker_args(linker, version)
+
+ def get_profile_use_args(self) -> T.List[str]:
+ return super().get_profile_use_args() + ['-fprofile-correction']
diff --git a/mesonbuild/compilers/mixins/intel.py b/mesonbuild/compilers/mixins/intel.py
new file mode 100644
index 0000000..b793fa8
--- /dev/null
+++ b/mesonbuild/compilers/mixins/intel.py
@@ -0,0 +1,185 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+"""Abstractions for the Intel Compiler families.
+
+Intel provides both a posix/gcc-like compiler (ICC) for MacOS and Linux,
+with Meson mixin IntelGnuLikeCompiler.
+For Windows, the Intel msvc-like compiler (ICL) Meson mixin
+is IntelVisualStudioLikeCompiler.
+"""
+
+import os
+import typing as T
+
+from ... import mesonlib
+from ..compilers import CompileCheckMode
+from .gnu import GnuLikeCompiler
+from .visualstudio import VisualStudioLikeCompiler
+
+# XXX: avoid circular dependencies
+# TODO: this belongs in a posix compiler class
+# NOTE: the default Intel optimization is -O2, unlike GNU which defaults to -O0.
+# this can be surprising, particularly for debug builds, so we specify the
+# default as -O0.
+# https://software.intel.com/en-us/cpp-compiler-developer-guide-and-reference-o
+# https://software.intel.com/en-us/cpp-compiler-developer-guide-and-reference-g
+# https://software.intel.com/en-us/fortran-compiler-developer-guide-and-reference-o
+# https://software.intel.com/en-us/fortran-compiler-developer-guide-and-reference-g
+# https://software.intel.com/en-us/fortran-compiler-developer-guide-and-reference-traceback
+# https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
+
+
+class IntelGnuLikeCompiler(GnuLikeCompiler):
+ """
+ Tested on linux for ICC 14.0.3, 15.0.6, 16.0.4, 17.0.1, 19.0
+ debugoptimized: -g -O2
+ release: -O3
+ minsize: -O2
+ """
+
+ BUILD_ARGS = {
+ 'plain': [],
+ 'debug': ["-g", "-traceback"],
+ 'debugoptimized': ["-g", "-traceback"],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+ OPTIM_ARGS: T.Dict[str, T.List[str]] = {
+ 'plain': [],
+ '0': ['-O0'],
+ 'g': ['-O0'],
+ '1': ['-O1'],
+ '2': ['-O2'],
+ '3': ['-O3'],
+ 's': ['-Os'],
+ }
+ id = 'intel'
+
+ def __init__(self) -> None:
+ super().__init__()
+ # As of 19.0.0 ICC doesn't have sanitizer, color, or lto support.
+ #
+ # It does have IPO, which serves much the same purpose as LOT, but
+ # there is an unfortunate rule for using IPO (you can't control the
+ # name of the output file) which break assumptions meson makes
+ self.base_options = {mesonlib.OptionKey(o) for o in [
+ 'b_pch', 'b_lundef', 'b_asneeded', 'b_pgo', 'b_coverage',
+ 'b_ndebug', 'b_staticpic', 'b_pie']}
+ self.lang_header = 'none'
+
+ def get_pch_suffix(self) -> str:
+ return 'pchi'
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ return ['-pch', '-pch_dir', os.path.join(pch_dir), '-x',
+ self.lang_header, '-include', header, '-x', 'none']
+
+ def get_pch_name(self, header_name: str) -> str:
+ return os.path.basename(header_name) + '.' + self.get_pch_suffix()
+
+ def openmp_flags(self) -> T.List[str]:
+ if mesonlib.version_compare(self.version, '>=15.0.0'):
+ return ['-qopenmp']
+ else:
+ return ['-openmp']
+
+ def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
+ extra_args = [
+ '-diag-error', '10006', # ignoring unknown option
+ '-diag-error', '10148', # Option not supported
+ '-diag-error', '10155', # ignoring argument required
+ '-diag-error', '10156', # ignoring not argument allowed
+ '-diag-error', '10157', # Ignoring argument of the wrong type
+ '-diag-error', '10158', # Argument must be separate. Can be hit by trying an option like -foo-bar=foo when -foo=bar is a valid option but -foo-bar isn't
+ ]
+ return super().get_compiler_check_args(mode) + extra_args
+
+ def get_profile_generate_args(self) -> T.List[str]:
+ return ['-prof-gen=threadsafe']
+
+ def get_profile_use_args(self) -> T.List[str]:
+ return ['-prof-use']
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return self.BUILD_ARGS[buildtype]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return self.OPTIM_ARGS[optimization_level]
+
+ def get_has_func_attribute_extra_args(self, name: str) -> T.List[str]:
+ return ['-diag-error', '1292']
+
+
+class IntelVisualStudioLikeCompiler(VisualStudioLikeCompiler):
+
+ """Abstractions for ICL, the Intel compiler on Windows."""
+
+ BUILD_ARGS = {
+ 'plain': [],
+ 'debug': ["/Zi", "/traceback"],
+ 'debugoptimized': ["/Zi", "/traceback"],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+ OPTIM_ARGS: T.Dict[str, T.List[str]] = {
+ 'plain': [],
+ '0': ['/Od'],
+ 'g': ['/Od'],
+ '1': ['/O1'],
+ '2': ['/O2'],
+ '3': ['/O3'],
+ 's': ['/Os'],
+ }
+
+ id = 'intel-cl'
+
+ def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
+ args = super().get_compiler_check_args(mode)
+ if mode is not CompileCheckMode.LINK:
+ args.extend([
+ '/Qdiag-error:10006', # ignoring unknown option
+ '/Qdiag-error:10148', # Option not supported
+ '/Qdiag-error:10155', # ignoring argument required
+ '/Qdiag-error:10156', # ignoring not argument allowed
+ '/Qdiag-error:10157', # Ignoring argument of the wrong type
+ '/Qdiag-error:10158', # Argument must be separate. Can be hit by trying an option like -foo-bar=foo when -foo=bar is a valid option but -foo-bar isn't
+ ])
+ return args
+
+ def get_toolset_version(self) -> T.Optional[str]:
+ # ICL provides a cl.exe that returns the version of MSVC it tries to
+ # emulate, so we'll get the version from that and pass it to the same
+ # function the real MSVC uses to calculate the toolset version.
+ _, _, err = mesonlib.Popen_safe(['cl.exe'])
+ v1, v2, *_ = mesonlib.search_version(err).split('.')
+ version = int(v1 + v2)
+ return self._calculate_toolset_version(version)
+
+ def openmp_flags(self) -> T.List[str]:
+ return ['/Qopenmp']
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return self.BUILD_ARGS[buildtype]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return self.OPTIM_ARGS[optimization_level]
+
+ def get_pch_base_name(self, header: str) -> str:
+ return os.path.basename(header)
diff --git a/mesonbuild/compilers/mixins/islinker.py b/mesonbuild/compilers/mixins/islinker.py
new file mode 100644
index 0000000..711e3e3
--- /dev/null
+++ b/mesonbuild/compilers/mixins/islinker.py
@@ -0,0 +1,130 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+"""Mixins for compilers that *are* linkers.
+
+While many compilers (such as gcc and clang) are used by meson to dispatch
+linker commands and other (like MSVC) are not, a few (such as DMD) actually
+are both the linker and compiler in one binary. This module provides mixin
+classes for those cases.
+"""
+
+import typing as T
+
+from ...mesonlib import EnvironmentException, MesonException, is_windows
+
+if T.TYPE_CHECKING:
+ from ...coredata import KeyedOptionDictType
+ from ...environment import Environment
+ from ...compilers.compilers import Compiler
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+
+class BasicLinkerIsCompilerMixin(Compiler):
+
+ """Provides a baseline of methods that a linker would implement.
+
+ In every case this provides a "no" or "empty" answer. If a compiler
+ implements any of these it needs a different mixin or to override that
+ functionality itself.
+ """
+
+ def sanitizer_link_args(self, value: str) -> T.List[str]:
+ return []
+
+ def get_lto_link_args(self, *, threads: int = 0, mode: str = 'default',
+ thinlto_cache_dir: T.Optional[str] = None) -> T.List[str]:
+ return []
+
+ def can_linker_accept_rsp(self) -> bool:
+ return is_windows()
+
+ def get_linker_exelist(self) -> T.List[str]:
+ return self.exelist.copy()
+
+ def get_linker_output_args(self, outputname: str) -> T.List[str]:
+ return []
+
+ def get_linker_always_args(self) -> T.List[str]:
+ return []
+
+ def get_linker_lib_prefix(self) -> str:
+ return ''
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+ def has_multi_link_args(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]:
+ return False, False
+
+ def get_link_debugfile_args(self, targetfile: str) -> T.List[str]:
+ return []
+
+ def get_std_shared_lib_link_args(self) -> T.List[str]:
+ return []
+
+ def get_std_shared_module_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return self.get_std_shared_lib_link_args()
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ raise EnvironmentException(f'Linker {self.id} does not support link_whole')
+
+ def get_allow_undefined_link_args(self) -> T.List[str]:
+ raise EnvironmentException(f'Linker {self.id} does not support allow undefined')
+
+ def get_pie_link_args(self) -> T.List[str]:
+ raise EnvironmentException(f'Linker {self.id} does not support position-independent executable')
+
+ def get_undefined_link_args(self) -> T.List[str]:
+ return []
+
+ def get_coverage_link_args(self) -> T.List[str]:
+ return []
+
+ def no_undefined_link_args(self) -> T.List[str]:
+ return []
+
+ def bitcode_args(self) -> T.List[str]:
+ raise MesonException("This linker doesn't support bitcode bundles")
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str,
+ darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ raise MesonException("This linker doesn't support soname args")
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ return ([], set())
+
+ def get_asneeded_args(self) -> T.List[str]:
+ return []
+
+ def get_buildtype_linker_args(self, buildtype: str) -> T.List[str]:
+ return []
+
+ def get_link_debugfile_name(self, targetfile: str) -> str:
+ return ''
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def thread_link_flags(self, env: 'Environment') -> T.List[str]:
+ return []
diff --git a/mesonbuild/compilers/mixins/pgi.py b/mesonbuild/compilers/mixins/pgi.py
new file mode 100644
index 0000000..2fa736c
--- /dev/null
+++ b/mesonbuild/compilers/mixins/pgi.py
@@ -0,0 +1,113 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+"""Abstractions for the PGI family of compilers."""
+
+import typing as T
+import os
+from pathlib import Path
+
+from ..compilers import clike_debug_args, clike_optimization_args
+from ...mesonlib import OptionKey
+
+if T.TYPE_CHECKING:
+ from ...environment import Environment
+ from ...compilers.compilers import Compiler
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+pgi_buildtype_args = {
+ 'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+} # type: T.Dict[str, T.List[str]]
+
+
+class PGICompiler(Compiler):
+
+ id = 'pgi'
+
+ def __init__(self) -> None:
+ self.base_options = {OptionKey('b_pch')}
+
+ default_warn_args = ['-Minform=inform']
+ self.warn_args: T.Dict[str, T.List[str]] = {
+ '0': [],
+ '1': default_warn_args,
+ '2': default_warn_args,
+ '3': default_warn_args,
+ 'everything': default_warn_args
+ }
+
+ def get_module_incdir_args(self) -> T.Tuple[str]:
+ return ('-module', )
+
+ def get_no_warn_args(self) -> T.List[str]:
+ return ['-silent']
+
+ def gen_import_library_args(self, implibname: str) -> T.List[str]:
+ return []
+
+ def get_pic_args(self) -> T.List[str]:
+ # PGI -fPIC is Linux only.
+ if self.info.is_linux():
+ return ['-fPIC']
+ return []
+
+ def openmp_flags(self) -> T.List[str]:
+ return ['-mp']
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return pgi_buildtype_args[buildtype]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return clike_optimization_args[optimization_level]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return clike_debug_args[is_debug]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I' or i[:2] == '-L':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+ return parameter_list
+
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+ def get_pch_suffix(self) -> str:
+ # PGI defaults to .pch suffix for PCH on Linux and Windows with --pch option
+ return 'pch'
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ # PGI supports PCH for C++ only.
+ hdr = Path(pch_dir).resolve().parent / header
+ if self.language == 'cpp':
+ return ['--pch',
+ '--pch_dir', str(hdr.parent),
+ f'-I{hdr.parent}']
+ else:
+ return []
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ # PGI cannot accept -pthread, it's already threaded
+ return []
diff --git a/mesonbuild/compilers/mixins/ti.py b/mesonbuild/compilers/mixins/ti.py
new file mode 100644
index 0000000..950c97f
--- /dev/null
+++ b/mesonbuild/compilers/mixins/ti.py
@@ -0,0 +1,151 @@
+# Copyright 2012-2019 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.
+from __future__ import annotations
+
+"""Representations specific to the Texas Instruments compiler family."""
+
+import os
+import typing as T
+
+from ...mesonlib import EnvironmentException
+
+if T.TYPE_CHECKING:
+ from ...envconfig import MachineInfo
+ from ...environment import Environment
+ from ...compilers.compilers import Compiler
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+ti_buildtype_args = {
+ 'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+} # type: T.Dict[str, T.List[str]]
+
+ti_optimization_args = {
+ 'plain': [],
+ '0': ['-O0'],
+ 'g': ['-Ooff'],
+ '1': ['-O1'],
+ '2': ['-O2'],
+ '3': ['-O3'],
+ 's': ['-O4']
+} # type: T.Dict[str, T.List[str]]
+
+ti_debug_args = {
+ False: [],
+ True: ['-g']
+} # type: T.Dict[bool, T.List[str]]
+
+
+class TICompiler(Compiler):
+
+ id = 'ti'
+
+ def __init__(self) -> None:
+ if not self.is_cross:
+ raise EnvironmentException('TI compilers only support cross-compilation.')
+
+ self.can_compile_suffixes.add('asm') # Assembly
+ self.can_compile_suffixes.add('cla') # Control Law Accelerator (CLA) used in C2000
+
+ default_warn_args = [] # type: T.List[str]
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + [],
+ '3': default_warn_args + [],
+ 'everything': default_warn_args + []} # type: T.Dict[str, T.List[str]]
+
+ def get_pic_args(self) -> T.List[str]:
+ # PIC support is not enabled by default for TI compilers,
+ # if users want to use it, they need to add the required arguments explicitly
+ return []
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return ti_buildtype_args[buildtype]
+
+ def get_pch_suffix(self) -> str:
+ return 'pch'
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ return []
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def get_coverage_args(self) -> T.List[str]:
+ return []
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ return []
+
+ def get_no_stdlib_link_args(self) -> T.List[str]:
+ return []
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return ti_optimization_args[optimization_level]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return ti_debug_args[is_debug]
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return []
+
+ def get_no_optimization_args(self) -> T.List[str]:
+ return ['-Ooff']
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [f'--output_file={target}']
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['--emit_warnings_as_errors']
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if path == '':
+ path = '.'
+ return ['-I=' + path]
+
+ @classmethod
+ def _unix_args_to_native(cls, args: T.List[str], info: MachineInfo) -> T.List[str]:
+ result = []
+ for i in args:
+ if i.startswith('-D'):
+ i = '--define=' + i[2:]
+ if i.startswith('-Wl,-rpath='):
+ continue
+ elif i == '--print-search-dirs':
+ continue
+ elif i.startswith('-L'):
+ continue
+ result.append(i)
+ return result
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:15] == '--include_path=':
+ parameter_list[idx] = i[:15] + os.path.normpath(os.path.join(build_dir, i[15:]))
+ if i[:2] == '-I':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+
+ return parameter_list
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return ['--preproc_with_compile', f'--preproc_dependency={outfile}']
diff --git a/mesonbuild/compilers/mixins/visualstudio.py b/mesonbuild/compilers/mixins/visualstudio.py
new file mode 100644
index 0000000..864d3b4
--- /dev/null
+++ b/mesonbuild/compilers/mixins/visualstudio.py
@@ -0,0 +1,500 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+"""Abstractions to simplify compilers that implement an MSVC compatible
+interface.
+"""
+
+import abc
+import os
+import typing as T
+
+from ... import arglist
+from ... import mesonlib
+from ... import mlog
+
+if T.TYPE_CHECKING:
+ from ...environment import Environment
+ from ...dependencies import Dependency
+ from .clike import CLikeCompiler as Compiler
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+vs32_instruction_set_args = {
+ 'mmx': ['/arch:SSE'], # There does not seem to be a flag just for MMX
+ 'sse': ['/arch:SSE'],
+ 'sse2': ['/arch:SSE2'],
+ 'sse3': ['/arch:AVX'], # VS leaped from SSE2 directly to AVX.
+ 'sse41': ['/arch:AVX'],
+ 'sse42': ['/arch:AVX'],
+ 'avx': ['/arch:AVX'],
+ 'avx2': ['/arch:AVX2'],
+ 'neon': None,
+} # T.Dicst[str, T.Optional[T.List[str]]]
+
+# The 64 bit compiler defaults to /arch:avx.
+vs64_instruction_set_args = {
+ 'mmx': ['/arch:AVX'],
+ 'sse': ['/arch:AVX'],
+ 'sse2': ['/arch:AVX'],
+ 'sse3': ['/arch:AVX'],
+ 'ssse3': ['/arch:AVX'],
+ 'sse41': ['/arch:AVX'],
+ 'sse42': ['/arch:AVX'],
+ 'avx': ['/arch:AVX'],
+ 'avx2': ['/arch:AVX2'],
+ 'neon': None,
+} # T.Dicst[str, T.Optional[T.List[str]]]
+
+msvc_optimization_args = {
+ 'plain': [],
+ '0': ['/Od'],
+ 'g': [], # No specific flag to optimize debugging, /Zi or /ZI will create debug information
+ '1': ['/O1'],
+ '2': ['/O2'],
+ '3': ['/O2', '/Gw'],
+ 's': ['/O1', '/Gw'],
+} # type: T.Dict[str, T.List[str]]
+
+msvc_debug_args = {
+ False: [],
+ True: ['/Zi']
+} # type: T.Dict[bool, T.List[str]]
+
+
+class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta):
+
+ """A common interface for all compilers implementing an MSVC-style
+ interface.
+
+ A number of compilers attempt to mimic MSVC, with varying levels of
+ success, such as Clang-CL and ICL (the Intel C/C++ Compiler for Windows).
+ This class implements as much common logic as possible.
+ """
+
+ std_warn_args = ['/W3']
+ std_opt_args = ['/O2']
+ ignore_libs = arglist.UNIXY_COMPILER_INTERNAL_LIBS + ['execinfo']
+ internal_libs = [] # type: T.List[str]
+
+ crt_args = {
+ 'none': [],
+ 'md': ['/MD'],
+ 'mdd': ['/MDd'],
+ 'mt': ['/MT'],
+ 'mtd': ['/MTd'],
+ } # type: T.Dict[str, T.List[str]]
+
+ # /showIncludes is needed for build dependency tracking in Ninja
+ # See: https://ninja-build.org/manual.html#_deps
+ # Assume UTF-8 sources by default, but self.unix_args_to_native() removes it
+ # if `/source-charset` is set too.
+ # It is also dropped if Visual Studio 2013 or earlier is used, since it would
+ # not be supported in that case.
+ always_args = ['/nologo', '/showIncludes', '/utf-8']
+ warn_args = {
+ '0': [],
+ '1': ['/W2'],
+ '2': ['/W3'],
+ '3': ['/W4'],
+ 'everything': ['/Wall'],
+ } # type: T.Dict[str, T.List[str]]
+
+ INVOKES_LINKER = False
+
+ def __init__(self, target: str):
+ self.base_options = {mesonlib.OptionKey(o) for o in ['b_pch', 'b_ndebug', 'b_vscrt']} # FIXME add lto, pgo and the like
+ self.target = target
+ self.is_64 = ('x64' in target) or ('x86_64' in target)
+ # do some canonicalization of target machine
+ if 'x86_64' in target:
+ self.machine = 'x64'
+ elif '86' in target:
+ self.machine = 'x86'
+ elif 'aarch64' in target:
+ self.machine = 'arm64'
+ elif 'arm' in target:
+ self.machine = 'arm'
+ else:
+ self.machine = target
+ if mesonlib.version_compare(self.version, '>=19.28.29910'): # VS 16.9.0 includes cl 19.28.29910
+ self.base_options.add(mesonlib.OptionKey('b_sanitize'))
+ assert self.linker is not None
+ self.linker.machine = self.machine
+
+ # Override CCompiler.get_always_args
+ def get_always_args(self) -> T.List[str]:
+ # TODO: use ImmutableListProtocol[str] here instead
+ return self.always_args.copy()
+
+ def get_pch_suffix(self) -> str:
+ return 'pch'
+
+ def get_pch_name(self, header: str) -> str:
+ chopped = os.path.basename(header).split('.')[:-1]
+ chopped.append(self.get_pch_suffix())
+ pchname = '.'.join(chopped)
+ return pchname
+
+ def get_pch_base_name(self, header: str) -> str:
+ # This needs to be implemented by inheriting classes
+ raise NotImplementedError
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ base = self.get_pch_base_name(header)
+ pchname = self.get_pch_name(header)
+ return ['/FI' + base, '/Yu' + base, '/Fp' + os.path.join(pch_dir, pchname)]
+
+ def get_preprocess_only_args(self) -> T.List[str]:
+ return ['/EP']
+
+ def get_preprocess_to_file_args(self) -> T.List[str]:
+ return ['/EP', '/P']
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return ['/c']
+
+ def get_no_optimization_args(self) -> T.List[str]:
+ return ['/Od', '/Oi-']
+
+ def sanitizer_compile_args(self, value: str) -> T.List[str]:
+ if value == 'none':
+ return []
+ if value != 'address':
+ raise mesonlib.MesonException('VS only supports address sanitizer at the moment.')
+ return ['/fsanitize=address']
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ if self.mode == 'PREPROCESSOR':
+ return ['/Fi' + target]
+ if target.endswith('.exe'):
+ return ['/Fe' + target]
+ return ['/Fo' + target]
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return []
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return msvc_debug_args[is_debug]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ args = msvc_optimization_args[optimization_level]
+ if mesonlib.version_compare(self.version, '<18.0'):
+ args = [arg for arg in args if arg != '/Gw']
+ return args
+
+ def linker_to_compiler_args(self, args: T.List[str]) -> T.List[str]:
+ return ['/link'] + args
+
+ def get_pic_args(self) -> T.List[str]:
+ return [] # PIC is handled by the loader on Windows
+
+ def gen_vs_module_defs_args(self, defsfile: str) -> T.List[str]:
+ if not isinstance(defsfile, str):
+ raise RuntimeError('Module definitions file should be str')
+ # With MSVC, DLLs only export symbols that are explicitly exported,
+ # so if a module defs file is specified, we use that to export symbols
+ return ['/DEF:' + defsfile]
+
+ def gen_pch_args(self, header: str, source: str, pchname: str) -> T.Tuple[str, T.List[str]]:
+ objname = os.path.splitext(pchname)[0] + '.obj'
+ return objname, ['/Yc' + header, '/Fp' + pchname, '/Fo' + objname]
+
+ def openmp_flags(self) -> T.List[str]:
+ return ['/openmp']
+
+ def openmp_link_flags(self) -> T.List[str]:
+ return []
+
+ # FIXME, no idea what these should be.
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ @classmethod
+ def unix_args_to_native(cls, args: T.List[str]) -> T.List[str]:
+ result: T.List[str] = []
+ for i in args:
+ # -mms-bitfields is specific to MinGW-GCC
+ # -pthread is only valid for GCC
+ if i in {'-mms-bitfields', '-pthread'}:
+ continue
+ if i.startswith('-LIBPATH:'):
+ i = '/LIBPATH:' + i[9:]
+ elif i.startswith('-L'):
+ i = '/LIBPATH:' + i[2:]
+ # Translate GNU-style -lfoo library name to the import library
+ elif i.startswith('-l'):
+ name = i[2:]
+ if name in cls.ignore_libs:
+ # With MSVC, these are provided by the C runtime which is
+ # linked in by default
+ continue
+ else:
+ i = name + '.lib'
+ elif i.startswith('-isystem'):
+ # just use /I for -isystem system include path s
+ if i.startswith('-isystem='):
+ i = '/I' + i[9:]
+ else:
+ i = '/I' + i[8:]
+ elif i.startswith('-idirafter'):
+ # same as -isystem, but appends the path instead
+ if i.startswith('-idirafter='):
+ i = '/I' + i[11:]
+ else:
+ i = '/I' + i[10:]
+ # -pthread in link flags is only used on Linux
+ elif i == '-pthread':
+ continue
+ # cl.exe does not allow specifying both, so remove /utf-8 that we
+ # added automatically in the case the user overrides it manually.
+ elif (i.startswith('/source-charset:')
+ or i.startswith('/execution-charset:')
+ or i == '/validate-charset-'):
+ try:
+ result.remove('/utf-8')
+ except ValueError:
+ pass
+ result.append(i)
+ return result
+
+ @classmethod
+ def native_args_to_unix(cls, args: T.List[str]) -> T.List[str]:
+ result = []
+ for arg in args:
+ if arg.startswith(('/LIBPATH:', '-LIBPATH:')):
+ result.append('-L' + arg[9:])
+ elif arg.endswith(('.a', '.lib')) and not os.path.isabs(arg):
+ result.append('-l' + arg)
+ else:
+ result.append(arg)
+ return result
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['/WX']
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if path == '':
+ path = '.'
+ # msvc does not have a concept of system header dirs.
+ return ['-I' + path]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I' or i[:2] == '/I':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+ elif i[:9] == '/LIBPATH:':
+ parameter_list[idx] = i[:9] + os.path.normpath(os.path.join(build_dir, i[9:]))
+
+ return parameter_list
+
+ # Visual Studio is special. It ignores some arguments it does not
+ # understand and you can't tell it to error out on those.
+ # http://stackoverflow.com/questions/15259720/how-can-i-make-the-microsoft-c-compiler-treat-unknown-flags-as-errors-rather-t
+ def has_arguments(self, args: T.List[str], env: 'Environment', code: str, mode: str) -> T.Tuple[bool, bool]:
+ warning_text = '4044' if mode == 'link' else '9002'
+ with self._build_wrapper(code, env, extra_args=args, mode=mode) as p:
+ if p.returncode != 0:
+ return False, p.cached
+ return not (warning_text in p.stderr or warning_text in p.stdout), p.cached
+
+ def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]:
+ pdbarr = rel_obj.split('.')[:-1]
+ pdbarr += ['pdb']
+ args = ['/Fd' + '.'.join(pdbarr)]
+ return args
+
+ def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
+ if self.is_64:
+ return vs64_instruction_set_args.get(instruction_set, None)
+ return vs32_instruction_set_args.get(instruction_set, None)
+
+ def _calculate_toolset_version(self, version: int) -> T.Optional[str]:
+ if version < 1310:
+ return '7.0'
+ elif version < 1400:
+ return '7.1' # (Visual Studio 2003)
+ elif version < 1500:
+ return '8.0' # (Visual Studio 2005)
+ elif version < 1600:
+ return '9.0' # (Visual Studio 2008)
+ elif version < 1700:
+ return '10.0' # (Visual Studio 2010)
+ elif version < 1800:
+ return '11.0' # (Visual Studio 2012)
+ elif version < 1900:
+ return '12.0' # (Visual Studio 2013)
+ elif version < 1910:
+ return '14.0' # (Visual Studio 2015)
+ elif version < 1920:
+ return '14.1' # (Visual Studio 2017)
+ elif version < 1930:
+ return '14.2' # (Visual Studio 2019)
+ elif version < 1940:
+ return '14.3' # (Visual Studio 2022)
+ mlog.warning(f'Could not find toolset for version {self.version!r}')
+ return None
+
+ def get_toolset_version(self) -> T.Optional[str]:
+ # See boost/config/compiler/visualc.cpp for up to date mapping
+ try:
+ version = int(''.join(self.version.split('.')[0:2]))
+ except ValueError:
+ return None
+ return self._calculate_toolset_version(version)
+
+ def get_default_include_dirs(self) -> T.List[str]:
+ if 'INCLUDE' not in os.environ:
+ return []
+ return os.environ['INCLUDE'].split(os.pathsep)
+
+ def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ if crt_val in self.crt_args:
+ return self.crt_args[crt_val]
+ assert crt_val in {'from_buildtype', 'static_from_buildtype'}
+ dbg = 'mdd'
+ rel = 'md'
+ if crt_val == 'static_from_buildtype':
+ dbg = 'mtd'
+ rel = 'mt'
+ # Match what build type flags used to do.
+ if buildtype == 'plain':
+ return []
+ elif buildtype == 'debug':
+ return self.crt_args[dbg]
+ elif buildtype == 'debugoptimized':
+ return self.crt_args[rel]
+ elif buildtype == 'release':
+ return self.crt_args[rel]
+ elif buildtype == 'minsize':
+ return self.crt_args[rel]
+ else:
+ assert buildtype == 'custom'
+ raise mesonlib.EnvironmentException('Requested C runtime based on buildtype, but buildtype is "custom".')
+
+ def has_func_attribute(self, name: str, env: 'Environment') -> T.Tuple[bool, bool]:
+ # MSVC doesn't have __attribute__ like Clang and GCC do, so just return
+ # false without compiling anything
+ return name in {'dllimport', 'dllexport'}, False
+
+ def get_argument_syntax(self) -> str:
+ return 'msvc'
+
+ def symbols_have_underscore_prefix(self, env: 'Environment') -> bool:
+ '''
+ Check if the compiler prefixes an underscore to global C symbols.
+
+ This overrides the Clike method, as for MSVC checking the
+ underscore prefix based on the compiler define never works,
+ so do not even try.
+ '''
+ # Try to consult a hardcoded list of cases we know
+ # absolutely have an underscore prefix
+ result = self._symbols_have_underscore_prefix_list(env)
+ if result is not None:
+ return result
+
+ # As a last resort, try search in a compiled binary
+ return self._symbols_have_underscore_prefix_searchbin(env)
+
+
+class MSVCCompiler(VisualStudioLikeCompiler):
+
+ """Specific to the Microsoft Compilers."""
+
+ id = 'msvc'
+
+ def __init__(self, target: str):
+ super().__init__(target)
+
+ # Visual Studio 2013 and erlier don't support the /utf-8 argument.
+ # We want to remove it. We also want to make an explicit copy so we
+ # don't mutate class constant state
+ if mesonlib.version_compare(self.version, '<19.00') and '/utf-8' in self.always_args:
+ self.always_args = [r for r in self.always_args if r != '/utf-8']
+
+ def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]:
+ args = super().get_compile_debugfile_args(rel_obj, pch)
+ # When generating a PDB file with PCH, all compile commands write
+ # to the same PDB file. Hence, we need to serialize the PDB
+ # writes using /FS since we do parallel builds. This slows down the
+ # build obviously, which is why we only do this when PCH is on.
+ # This was added in Visual Studio 2013 (MSVC 18.0). Before that it was
+ # always on: https://msdn.microsoft.com/en-us/library/dn502518.aspx
+ if pch and mesonlib.version_compare(self.version, '>=18.0'):
+ args = ['/FS'] + args
+ return args
+
+ # Override CCompiler.get_always_args
+ # We want to drop '/utf-8' for Visual Studio 2013 and earlier
+ def get_always_args(self) -> T.List[str]:
+ return self.always_args
+
+ def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
+ if self.version.split('.')[0] == '16' and instruction_set == 'avx':
+ # VS documentation says that this exists and should work, but
+ # it does not. The headers do not contain AVX intrinsics
+ # and they can not be called.
+ return None
+ return super().get_instruction_set_args(instruction_set)
+
+ def get_pch_base_name(self, header: str) -> str:
+ return os.path.basename(header)
+
+
+class ClangClCompiler(VisualStudioLikeCompiler):
+
+ """Specific to Clang-CL."""
+
+ id = 'clang-cl'
+
+ def __init__(self, target: str):
+ super().__init__(target)
+
+ # Assembly
+ self.can_compile_suffixes.add('s')
+
+ def has_arguments(self, args: T.List[str], env: 'Environment', code: str, mode: str) -> T.Tuple[bool, bool]:
+ if mode != 'link':
+ args = args + ['-Werror=unknown-argument', '-Werror=unknown-warning-option']
+ return super().has_arguments(args, env, code, mode)
+
+ def get_toolset_version(self) -> T.Optional[str]:
+ # XXX: what is the right thing to do here?
+ return '14.1'
+
+ def get_pch_base_name(self, header: str) -> str:
+ return header
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ if path == '':
+ path = '.'
+ return ['/clang:-isystem' + path] if is_system else ['-I' + path]
+
+ def get_dependency_compile_args(self, dep: 'Dependency') -> T.List[str]:
+ if dep.get_include_type() == 'system':
+ converted = []
+ for i in dep.get_compile_args():
+ if i.startswith('-isystem'):
+ converted += ['/clang:' + i]
+ else:
+ converted += [i]
+ return converted
+ else:
+ return dep.get_compile_args()
diff --git a/mesonbuild/compilers/mixins/xc16.py b/mesonbuild/compilers/mixins/xc16.py
new file mode 100644
index 0000000..09949a2
--- /dev/null
+++ b/mesonbuild/compilers/mixins/xc16.py
@@ -0,0 +1,132 @@
+# Copyright 2012-2019 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.
+from __future__ import annotations
+
+"""Representations specific to the Microchip XC16 C compiler family."""
+
+import os
+import typing as T
+
+from ...mesonlib import EnvironmentException
+
+if T.TYPE_CHECKING:
+ from ...envconfig import MachineInfo
+ from ...environment import Environment
+ from ...compilers.compilers import Compiler
+else:
+ # This is a bit clever, for mypy we pretend that these mixins descend from
+ # Compiler, so we get all of the methods and attributes defined for us, but
+ # for runtime we make them descend from object (which all classes normally
+ # do). This gives up DRYer type checking, with no runtime impact
+ Compiler = object
+
+xc16_buildtype_args = {
+ 'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+} # type: T.Dict[str, T.List[str]]
+
+xc16_optimization_args = {
+ 'plain': [],
+ '0': ['-O0'],
+ 'g': ['-O0'],
+ '1': ['-O1'],
+ '2': ['-O2'],
+ '3': ['-O3'],
+ 's': ['-Os']
+} # type: T.Dict[str, T.List[str]]
+
+xc16_debug_args = {
+ False: [],
+ True: []
+} # type: T.Dict[bool, T.List[str]]
+
+
+class Xc16Compiler(Compiler):
+
+ id = 'xc16'
+
+ def __init__(self) -> None:
+ if not self.is_cross:
+ raise EnvironmentException('xc16 supports only cross-compilation.')
+ # Assembly
+ self.can_compile_suffixes.add('s')
+ default_warn_args = [] # type: T.List[str]
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + [],
+ '3': default_warn_args + [],
+ 'everything': default_warn_args + []} # type: T.Dict[str, T.List[str]]
+
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+ def get_pic_args(self) -> T.List[str]:
+ # PIC support is not enabled by default for xc16,
+ # if users want to use it, they need to add the required arguments explicitly
+ return []
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return xc16_buildtype_args[buildtype]
+
+ def get_pch_suffix(self) -> str:
+ return 'pch'
+
+ def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
+ return []
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def get_coverage_args(self) -> T.List[str]:
+ return []
+
+ def get_no_stdinc_args(self) -> T.List[str]:
+ return ['-nostdinc']
+
+ def get_no_stdlib_link_args(self) -> T.List[str]:
+ return ['--nostdlib']
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return xc16_optimization_args[optimization_level]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return xc16_debug_args[is_debug]
+
+ @classmethod
+ def _unix_args_to_native(cls, args: T.List[str], info: MachineInfo) -> T.List[str]:
+ result = []
+ for i in args:
+ if i.startswith('-D'):
+ i = '-D' + i[2:]
+ if i.startswith('-I'):
+ i = '-I' + i[2:]
+ if i.startswith('-Wl,-rpath='):
+ continue
+ elif i == '--print-search-dirs':
+ continue
+ elif i.startswith('-L'):
+ continue
+ result.append(i)
+ return result
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:9] == '-I':
+ parameter_list[idx] = i[:9] + os.path.normpath(os.path.join(build_dir, i[9:]))
+
+ return parameter_list
diff --git a/mesonbuild/compilers/objc.py b/mesonbuild/compilers/objc.py
new file mode 100644
index 0000000..83dcaad
--- /dev/null
+++ b/mesonbuild/compilers/objc.py
@@ -0,0 +1,114 @@
+# 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.
+from __future__ import annotations
+
+import typing as T
+
+from .. import coredata
+from ..mesonlib import OptionKey
+
+from .compilers import Compiler
+from .mixins.clike import CLikeCompiler
+from .mixins.gnu import GnuCompiler, gnu_common_warning_args, gnu_objc_warning_args
+from .mixins.clang import ClangCompiler
+
+if T.TYPE_CHECKING:
+ from ..programs import ExternalProgram
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..linkers import DynamicLinker
+ from ..mesonlib import MachineChoice
+
+
+class ObjCCompiler(CLikeCompiler, Compiler):
+
+ language = 'objc'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrap: T.Optional['ExternalProgram'],
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ Compiler.__init__(self, ccache, exelist, version, for_machine, info,
+ is_cross=is_cross, full_version=full_version,
+ linker=linker)
+ CLikeCompiler.__init__(self, exe_wrap)
+
+ @staticmethod
+ def get_display_language() -> str:
+ return 'Objective-C'
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ code = '#import<stddef.h>\nint main(void) { return 0; }\n'
+ return self._sanity_check_impl(work_dir, environment, 'sanitycheckobjc.m', code)
+
+
+class GnuObjCCompiler(GnuCompiler, ObjCCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ ObjCCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ GnuCompiler.__init__(self, defines)
+ default_warn_args = ['-Wall', '-Winvalid-pch']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': (default_warn_args + ['-Wextra', '-Wpedantic'] +
+ self.supported_warn_args(gnu_common_warning_args) +
+ self.supported_warn_args(gnu_objc_warning_args))}
+
+
+class ClangObjCCompiler(ClangCompiler, ObjCCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ ObjCCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ ClangCompiler.__init__(self, defines)
+ default_warn_args = ['-Wall', '-Winvalid-pch']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': ['-Weverything']}
+
+ def get_options(self) -> 'coredata.MutableKeyedOptionDictType':
+ opts = super().get_options()
+ opts.update({
+ OptionKey('std', machine=self.for_machine, lang='c'): coredata.UserComboOption(
+ 'C language standard to use',
+ ['none', 'c89', 'c99', 'c11', 'c17', 'gnu89', 'gnu99', 'gnu11', 'gnu17'],
+ 'none',
+ )
+ })
+ return opts
+
+ def get_option_compile_args(self, options: 'coredata.KeyedOptionDictType') -> T.List[str]:
+ args = []
+ std = options[OptionKey('std', machine=self.for_machine, lang='c')]
+ if std.value != 'none':
+ args.append('-std=' + std.value)
+ return args
+
+class AppleClangObjCCompiler(ClangObjCCompiler):
+
+ """Handle the differences between Apple's clang and vanilla clang."""
diff --git a/mesonbuild/compilers/objcpp.py b/mesonbuild/compilers/objcpp.py
new file mode 100644
index 0000000..1f9f756
--- /dev/null
+++ b/mesonbuild/compilers/objcpp.py
@@ -0,0 +1,115 @@
+# 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.
+from __future__ import annotations
+
+import typing as T
+
+from .. import coredata
+from ..mesonlib import OptionKey
+
+from .mixins.clike import CLikeCompiler
+from .compilers import Compiler
+from .mixins.gnu import GnuCompiler, gnu_common_warning_args, gnu_objc_warning_args
+from .mixins.clang import ClangCompiler
+
+if T.TYPE_CHECKING:
+ from ..programs import ExternalProgram
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..linkers import DynamicLinker
+ from ..mesonlib import MachineChoice
+
+class ObjCPPCompiler(CLikeCompiler, Compiler):
+
+ language = 'objcpp'
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrap: T.Optional['ExternalProgram'],
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ Compiler.__init__(self, ccache, exelist, version, for_machine, info,
+ is_cross=is_cross, full_version=full_version,
+ linker=linker)
+ CLikeCompiler.__init__(self, exe_wrap)
+
+ @staticmethod
+ def get_display_language() -> str:
+ return 'Objective-C++'
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ code = '#import<stdio.h>\nclass MyClass;int main(void) { return 0; }\n'
+ return self._sanity_check_impl(work_dir, environment, 'sanitycheckobjcpp.mm', code)
+
+
+class GnuObjCPPCompiler(GnuCompiler, ObjCPPCompiler):
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ ObjCPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ GnuCompiler.__init__(self, defines)
+ default_warn_args = ['-Wall', '-Winvalid-pch']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': (default_warn_args + ['-Wextra', '-Wpedantic'] +
+ self.supported_warn_args(gnu_common_warning_args) +
+ self.supported_warn_args(gnu_objc_warning_args))}
+
+
+class ClangObjCPPCompiler(ClangCompiler, ObjCPPCompiler):
+
+ def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ defines: T.Optional[T.Dict[str, str]] = None,
+ linker: T.Optional['DynamicLinker'] = None,
+ full_version: T.Optional[str] = None):
+ ObjCPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross,
+ info, exe_wrapper, linker=linker, full_version=full_version)
+ ClangCompiler.__init__(self, defines)
+ default_warn_args = ['-Wall', '-Winvalid-pch']
+ self.warn_args = {'0': [],
+ '1': default_warn_args,
+ '2': default_warn_args + ['-Wextra'],
+ '3': default_warn_args + ['-Wextra', '-Wpedantic'],
+ 'everything': ['-Weverything']}
+
+ def get_options(self) -> 'coredata.MutableKeyedOptionDictType':
+ opts = super().get_options()
+ opts.update({
+ OptionKey('std', machine=self.for_machine, lang='cpp'): coredata.UserComboOption(
+ 'C++ language standard to use',
+ ['none', 'c++98', 'c++11', 'c++14', 'c++17', 'gnu++98', 'gnu++11', 'gnu++14', 'gnu++17'],
+ 'none',
+ )
+ })
+ return opts
+
+ def get_option_compile_args(self, options: 'coredata.KeyedOptionDictType') -> T.List[str]:
+ args = []
+ std = options[OptionKey('std', machine=self.for_machine, lang='cpp')]
+ if std.value != 'none':
+ args.append('-std=' + std.value)
+ return args
+
+
+class AppleClangObjCPPCompiler(ClangObjCPPCompiler):
+
+ """Handle the differences between Apple's clang and vanilla clang."""
diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py
new file mode 100644
index 0000000..9e5ebc8
--- /dev/null
+++ b/mesonbuild/compilers/rust.py
@@ -0,0 +1,208 @@
+# Copyright 2012-2022 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.
+from __future__ import annotations
+
+import subprocess, os.path
+import textwrap
+import typing as T
+
+from .. import coredata
+from ..mesonlib import EnvironmentException, MesonException, Popen_safe, OptionKey
+from .compilers import Compiler, rust_buildtype_args, clike_debug_args
+
+if T.TYPE_CHECKING:
+ from ..coredata import MutableKeyedOptionDictType, KeyedOptionDictType
+ from ..envconfig import MachineInfo
+ from ..environment import Environment # noqa: F401
+ from ..linkers import DynamicLinker
+ from ..mesonlib import MachineChoice
+ from ..programs import ExternalProgram
+
+
+rust_optimization_args = {
+ 'plain': [],
+ '0': [],
+ 'g': ['-C', 'opt-level=0'],
+ '1': ['-C', 'opt-level=1'],
+ '2': ['-C', 'opt-level=2'],
+ '3': ['-C', 'opt-level=3'],
+ 's': ['-C', 'opt-level=s'],
+} # type: T.Dict[str, T.List[str]]
+
+class RustCompiler(Compiler):
+
+ # rustc doesn't invoke the compiler itself, it doesn't need a LINKER_PREFIX
+ language = 'rust'
+ id = 'rustc'
+
+ _WARNING_LEVELS: T.Dict[str, T.List[str]] = {
+ '0': ['-A', 'warnings'],
+ '1': [],
+ '2': [],
+ '3': ['-W', 'warnings'],
+ }
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo',
+ exe_wrapper: T.Optional['ExternalProgram'] = None,
+ full_version: T.Optional[str] = None,
+ linker: T.Optional['DynamicLinker'] = None):
+ super().__init__([], exelist, version, for_machine, info,
+ is_cross=is_cross, full_version=full_version,
+ linker=linker)
+ self.exe_wrapper = exe_wrapper
+ self.base_options.add(OptionKey('b_colorout'))
+ if 'link' in self.linker.id:
+ self.base_options.add(OptionKey('b_vscrt'))
+
+ def needs_static_linker(self) -> bool:
+ return False
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ source_name = os.path.join(work_dir, 'sanity.rs')
+ output_name = os.path.join(work_dir, 'rusttest')
+ with open(source_name, 'w', encoding='utf-8') as ofile:
+ ofile.write(textwrap.dedent(
+ '''fn main() {
+ }
+ '''))
+ pc = subprocess.Popen(self.exelist + ['-o', output_name, source_name],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ cwd=work_dir)
+ _stdo, _stde = pc.communicate()
+ assert isinstance(_stdo, bytes)
+ assert isinstance(_stde, bytes)
+ stdo = _stdo.decode('utf-8', errors='replace')
+ stde = _stde.decode('utf-8', errors='replace')
+ if pc.returncode != 0:
+ raise EnvironmentException('Rust compiler {} can not compile programs.\n{}\n{}'.format(
+ self.name_string(),
+ stdo,
+ stde))
+ if self.is_cross:
+ if self.exe_wrapper is None:
+ # Can't check if the binaries run so we have to assume they do
+ return
+ cmdlist = self.exe_wrapper.get_command() + [output_name]
+ else:
+ cmdlist = [output_name]
+ pe = subprocess.Popen(cmdlist, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ pe.wait()
+ if pe.returncode != 0:
+ raise EnvironmentException('Executables created by Rust compiler %s are not runnable.' % self.name_string())
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return ['--dep-info', outfile]
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return rust_buildtype_args[buildtype]
+
+ def get_sysroot(self) -> str:
+ cmd = self.get_exelist(ccache=False) + ['--print', 'sysroot']
+ p, stdo, stde = Popen_safe(cmd)
+ return stdo.split('\n', maxsplit=1)[0]
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return clike_debug_args[is_debug]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return rust_optimization_args[optimization_level]
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-L':
+ for j in ['dependency', 'crate', 'native', 'framework', 'all']:
+ combined_len = len(j) + 3
+ if i[:combined_len] == f'-L{j}=':
+ parameter_list[idx] = i[:combined_len] + os.path.normpath(os.path.join(build_dir, i[combined_len:]))
+ break
+
+ return parameter_list
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return ['-o', outputname]
+
+ @classmethod
+ def use_linker_args(cls, linker: str, version: str) -> T.List[str]:
+ return ['-C', f'linker={linker}']
+
+ # Rust does not have a use_linker_args because it dispatches to a gcc-like
+ # C compiler for dynamic linking, as such we invoke the C compiler's
+ # use_linker_args method instead.
+
+ def get_options(self) -> 'MutableKeyedOptionDictType':
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ return {
+ key: coredata.UserComboOption(
+ 'Rust edition to use',
+ ['none', '2015', '2018', '2021'],
+ 'none',
+ ),
+ }
+
+ def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ args = []
+ key = OptionKey('std', machine=self.for_machine, lang=self.language)
+ std = options[key]
+ if std.value != 'none':
+ args.append('--edition=' + std.value)
+ return args
+
+ def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
+ # Rust handles this for us, we don't need to do anything
+ return []
+
+ def get_colorout_args(self, colortype: str) -> T.List[str]:
+ if colortype in {'always', 'never', 'auto'}:
+ return [f'--color={colortype}']
+ raise MesonException(f'Invalid color type for rust {colortype}')
+
+ def get_linker_always_args(self) -> T.List[str]:
+ args: T.List[str] = []
+ for a in super().get_linker_always_args():
+ args.extend(['-C', f'link-arg={a}'])
+ return args
+
+ def get_werror_args(self) -> T.List[str]:
+ # Use -D warnings, which makes every warning not explicitly allowed an
+ # error
+ return ['-D', 'warnings']
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ # TODO: I'm not really sure what to put here, Rustc doesn't have warning
+ return self._WARNING_LEVELS[level]
+
+ def get_no_warn_args(self) -> T.List[str]:
+ return self._WARNING_LEVELS["0"]
+
+ def get_pic_args(self) -> T.List[str]:
+ # This defaults to
+ return ['-C', 'relocation-model=pic']
+
+ def get_pie_args(self) -> T.List[str]:
+ # Rustc currently has no way to toggle this, it's controlled by whether
+ # pic is on by rustc
+ return []
+
+
+class ClippyRustCompiler(RustCompiler):
+
+ """Clippy is a linter that wraps Rustc.
+
+ This just provides us a different id
+ """
+
+ id = 'clippy-driver rustc'
diff --git a/mesonbuild/compilers/swift.py b/mesonbuild/compilers/swift.py
new file mode 100644
index 0000000..ec4c7a3
--- /dev/null
+++ b/mesonbuild/compilers/swift.py
@@ -0,0 +1,130 @@
+# 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.
+from __future__ import annotations
+
+import subprocess, os.path
+import typing as T
+
+from ..mesonlib import EnvironmentException
+
+from .compilers import Compiler, swift_buildtype_args, clike_debug_args
+
+if T.TYPE_CHECKING:
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..linkers import DynamicLinker
+ from ..mesonlib import MachineChoice
+
+swift_optimization_args = {
+ 'plain': [],
+ '0': [],
+ 'g': [],
+ '1': ['-O'],
+ '2': ['-O'],
+ '3': ['-O'],
+ 's': ['-O'],
+} # type: T.Dict[str, T.List[str]]
+
+class SwiftCompiler(Compiler):
+
+ LINKER_PREFIX = ['-Xlinker']
+ language = 'swift'
+ id = 'llvm'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo', full_version: T.Optional[str] = None,
+ linker: T.Optional['DynamicLinker'] = None):
+ super().__init__([], exelist, version, for_machine, info,
+ is_cross=is_cross, full_version=full_version,
+ linker=linker)
+ self.version = version
+
+ def needs_static_linker(self) -> bool:
+ return True
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['--fatal-warnings']
+
+ def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+ return ['-emit-dependencies']
+
+ def depfile_for_object(self, objfile: str) -> T.Optional[str]:
+ return os.path.splitext(objfile)[0] + '.' + self.get_depfile_suffix()
+
+ def get_depfile_suffix(self) -> str:
+ return 'd'
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return ['-o', target]
+
+ def get_header_import_args(self, headername: str) -> T.List[str]:
+ return ['-import-objc-header', headername]
+
+ def get_warn_args(self, level: str) -> T.List[str]:
+ return []
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return swift_buildtype_args[buildtype]
+
+ def get_std_exe_link_args(self) -> T.List[str]:
+ return ['-emit-executable']
+
+ def get_module_args(self, modname: str) -> T.List[str]:
+ return ['-module-name', modname]
+
+ def get_mod_gen_args(self) -> T.List[str]:
+ return ['-emit-module']
+
+ def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
+ return ['-I' + path]
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return ['-c']
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:2] == '-I' or i[:2] == '-L':
+ parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
+
+ return parameter_list
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ src = 'swifttest.swift'
+ source_name = os.path.join(work_dir, src)
+ output_name = os.path.join(work_dir, 'swifttest')
+ extra_flags: T.List[str] = []
+ extra_flags += environment.coredata.get_external_args(self.for_machine, self.language)
+ if self.is_cross:
+ extra_flags += self.get_compile_only_args()
+ else:
+ extra_flags += environment.coredata.get_external_link_args(self.for_machine, self.language)
+ with open(source_name, 'w', encoding='utf-8') as ofile:
+ ofile.write('''print("Swift compilation is working.")
+''')
+ pc = subprocess.Popen(self.exelist + extra_flags + ['-emit-executable', '-o', output_name, src], cwd=work_dir)
+ pc.wait()
+ if pc.returncode != 0:
+ raise EnvironmentException('Swift compiler %s can not compile programs.' % self.name_string())
+ if self.is_cross:
+ # Can't check if the binaries run so we have to assume they do
+ return
+ if subprocess.call(output_name) != 0:
+ raise EnvironmentException('Executables created by Swift compiler %s are not runnable.' % self.name_string())
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return clike_debug_args[is_debug]
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return swift_optimization_args[optimization_level]
diff --git a/mesonbuild/compilers/vala.py b/mesonbuild/compilers/vala.py
new file mode 100644
index 0000000..e56a9fc
--- /dev/null
+++ b/mesonbuild/compilers/vala.py
@@ -0,0 +1,140 @@
+# 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.
+from __future__ import annotations
+
+import os.path
+import typing as T
+
+from .. import mlog
+from ..mesonlib import EnvironmentException, version_compare, OptionKey
+
+from .compilers import Compiler, LibType
+
+if T.TYPE_CHECKING:
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..mesonlib import MachineChoice
+
+class ValaCompiler(Compiler):
+
+ language = 'vala'
+ id = 'valac'
+
+ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice,
+ is_cross: bool, info: 'MachineInfo'):
+ super().__init__([], exelist, version, for_machine, info, is_cross=is_cross)
+ self.version = version
+ self.base_options = {OptionKey('b_colorout')}
+
+ def needs_static_linker(self) -> bool:
+ return False # Because compiles into C.
+
+ def get_optimization_args(self, optimization_level: str) -> T.List[str]:
+ return []
+
+ def get_debug_args(self, is_debug: bool) -> T.List[str]:
+ return ['--debug'] if is_debug else []
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [] # Because compiles into C.
+
+ def get_compile_only_args(self) -> T.List[str]:
+ return [] # Because compiles into C.
+
+ def get_pic_args(self) -> T.List[str]:
+ return []
+
+ def get_pie_args(self) -> T.List[str]:
+ return []
+
+ def get_pie_link_args(self) -> T.List[str]:
+ return []
+
+ def get_always_args(self) -> T.List[str]:
+ return ['-C']
+
+ def get_warn_args(self, warning_level: str) -> T.List[str]:
+ return []
+
+ def get_no_warn_args(self) -> T.List[str]:
+ return ['--disable-warnings']
+
+ def get_werror_args(self) -> T.List[str]:
+ return ['--fatal-warnings']
+
+ def get_colorout_args(self, colortype: str) -> T.List[str]:
+ if version_compare(self.version, '>=0.37.1'):
+ return ['--color=' + colortype]
+ return []
+
+ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
+ build_dir: str) -> T.List[str]:
+ for idx, i in enumerate(parameter_list):
+ if i[:9] == '--girdir=':
+ parameter_list[idx] = i[:9] + os.path.normpath(os.path.join(build_dir, i[9:]))
+ if i[:10] == '--vapidir=':
+ parameter_list[idx] = i[:10] + os.path.normpath(os.path.join(build_dir, i[10:]))
+ if i[:13] == '--includedir=':
+ parameter_list[idx] = i[:13] + os.path.normpath(os.path.join(build_dir, i[13:]))
+ if i[:14] == '--metadatadir=':
+ parameter_list[idx] = i[:14] + os.path.normpath(os.path.join(build_dir, i[14:]))
+
+ return parameter_list
+
+ def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
+ code = 'class MesonSanityCheck : Object { }'
+ extra_flags: T.List[str] = []
+ extra_flags += environment.coredata.get_external_args(self.for_machine, self.language)
+ if self.is_cross:
+ extra_flags += self.get_compile_only_args()
+ else:
+ extra_flags += environment.coredata.get_external_link_args(self.for_machine, self.language)
+ with self.cached_compile(code, environment.coredata, extra_args=extra_flags, mode='compile') as p:
+ if p.returncode != 0:
+ msg = f'Vala compiler {self.name_string()!r} can not compile programs'
+ raise EnvironmentException(msg)
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ if buildtype in {'debug', 'debugoptimized', 'minsize'}:
+ return ['--debug']
+ return []
+
+ def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str],
+ libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]:
+ if extra_dirs and isinstance(extra_dirs, str):
+ extra_dirs = [extra_dirs]
+ # Valac always looks in the default vapi dir, so only search there if
+ # no extra dirs are specified.
+ if not extra_dirs:
+ code = 'class MesonFindLibrary : Object { }'
+ args: T.List[str] = []
+ args += env.coredata.get_external_args(self.for_machine, self.language)
+ vapi_args = ['--pkg', libname]
+ args += vapi_args
+ with self.cached_compile(code, env.coredata, extra_args=args, mode='compile') as p:
+ if p.returncode == 0:
+ return vapi_args
+ # Not found? Try to find the vapi file itself.
+ for d in extra_dirs:
+ vapi = os.path.join(d, libname + '.vapi')
+ if os.path.isfile(vapi):
+ return [vapi]
+ mlog.debug(f'Searched {extra_dirs!r} and {libname!r} wasn\'t found')
+ return None
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def thread_link_flags(self, env: 'Environment') -> T.List[str]:
+ return []
diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py
new file mode 100644
index 0000000..9b01dc8
--- /dev/null
+++ b/mesonbuild/coredata.py
@@ -0,0 +1,1284 @@
+# Copyright 2012-2022 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.
+from __future__ import annotations
+
+from . import mlog, mparser
+import pickle, os, uuid
+import sys
+from itertools import chain
+from pathlib import PurePath
+from collections import OrderedDict
+from .mesonlib import (
+ HoldableObject,
+ MesonException, EnvironmentException, MachineChoice, PerMachine,
+ PerMachineDefaultable, default_libdir, default_libexecdir,
+ default_prefix, default_datadir, default_includedir, default_infodir,
+ default_localedir, default_mandir, default_sbindir, default_sysconfdir,
+ split_args, OptionKey, OptionType, stringlistify,
+ pickle_load, replace_if_different
+)
+from .wrap import WrapMode
+import ast
+import argparse
+import configparser
+import enum
+import shlex
+import typing as T
+
+if T.TYPE_CHECKING:
+ from . import dependencies
+ from .compilers.compilers import Compiler, CompileResult
+ from .dependencies.detect import TV_DepID
+ from .environment import Environment
+ from .mesonlib import OptionOverrideProxy, FileOrString
+ from .cmake.traceparser import CMakeCacheEntry
+
+ OptionDictType = T.Union[T.Dict[str, 'UserOption[T.Any]'], OptionOverrideProxy]
+ MutableKeyedOptionDictType = T.Dict['OptionKey', 'UserOption[T.Any]']
+ KeyedOptionDictType = T.Union[MutableKeyedOptionDictType, OptionOverrideProxy]
+ CompilerCheckCacheKey = T.Tuple[T.Tuple[str, ...], str, FileOrString, T.Tuple[str, ...], str]
+
+ # typeshed
+ StrOrBytesPath = T.Union[str, bytes, os.PathLike[str], os.PathLike[bytes]]
+
+# Check major_versions_differ() if changing versioning scheme.
+#
+# Pip requires that RCs are named like this: '0.1.0.rc1'
+# But the corresponding Git tag needs to be '0.1.0rc1'
+version = '1.0.1'
+
+backendlist = ['ninja', 'vs', 'vs2010', 'vs2012', 'vs2013', 'vs2015', 'vs2017', 'vs2019', 'vs2022', 'xcode']
+
+default_yielding = False
+
+# Can't bind this near the class method it seems, sadly.
+_T = T.TypeVar('_T')
+
+
+class MesonVersionMismatchException(MesonException):
+ '''Build directory generated with Meson version is incompatible with current version'''
+ def __init__(self, old_version: str, current_version: str) -> None:
+ super().__init__(f'Build directory has been generated with Meson version {old_version}, '
+ f'which is incompatible with the current version {current_version}.')
+ self.old_version = old_version
+ self.current_version = current_version
+
+
+class UserOption(T.Generic[_T], HoldableObject):
+ def __init__(self, description: str, choices: T.Optional[T.Union[str, T.List[_T]]], yielding: T.Optional[bool]):
+ super().__init__()
+ self.choices = choices
+ self.description = description
+ if yielding is None:
+ yielding = default_yielding
+ if not isinstance(yielding, bool):
+ raise MesonException('Value of "yielding" must be a boolean.')
+ self.yielding = yielding
+ self.deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False
+
+ def listify(self, value: T.Any) -> T.List[T.Any]:
+ return [value]
+
+ def printable_value(self) -> T.Union[str, int, bool, T.List[T.Union[str, int, bool]]]:
+ assert isinstance(self.value, (str, int, bool, list))
+ return self.value
+
+ # Check that the input is a valid value and return the
+ # "cleaned" or "native" version. For example the Boolean
+ # option could take the string "true" and return True.
+ def validate_value(self, value: T.Any) -> _T:
+ raise RuntimeError('Derived option class did not override validate_value.')
+
+ def set_value(self, newvalue: T.Any) -> None:
+ self.value = self.validate_value(newvalue)
+
+class UserStringOption(UserOption[str]):
+ def __init__(self, description: str, value: T.Any, yielding: T.Optional[bool] = None):
+ super().__init__(description, None, yielding)
+ self.set_value(value)
+
+ def validate_value(self, value: T.Any) -> str:
+ if not isinstance(value, str):
+ raise MesonException('Value "%s" for string option is not a string.' % str(value))
+ return value
+
+class UserBooleanOption(UserOption[bool]):
+ def __init__(self, description: str, value, yielding: T.Optional[bool] = None) -> None:
+ super().__init__(description, [True, False], yielding)
+ self.set_value(value)
+
+ def __bool__(self) -> bool:
+ return self.value
+
+ def validate_value(self, value: T.Any) -> bool:
+ if isinstance(value, bool):
+ return value
+ if not isinstance(value, str):
+ raise MesonException(f'Value {value} cannot be converted to a boolean')
+ if value.lower() == 'true':
+ return True
+ if value.lower() == 'false':
+ return False
+ raise MesonException('Value %s is not boolean (true or false).' % value)
+
+class UserIntegerOption(UserOption[int]):
+ def __init__(self, description: str, value: T.Any, yielding: T.Optional[bool] = None):
+ min_value, max_value, default_value = value
+ self.min_value = min_value
+ self.max_value = max_value
+ c = []
+ if min_value is not None:
+ c.append('>=' + str(min_value))
+ if max_value is not None:
+ c.append('<=' + str(max_value))
+ choices = ', '.join(c)
+ super().__init__(description, choices, yielding)
+ self.set_value(default_value)
+
+ def validate_value(self, value: T.Any) -> int:
+ if isinstance(value, str):
+ value = self.toint(value)
+ if not isinstance(value, int):
+ raise MesonException('New value for integer option is not an integer.')
+ if self.min_value is not None and value < self.min_value:
+ raise MesonException('New value %d is less than minimum value %d.' % (value, self.min_value))
+ if self.max_value is not None and value > self.max_value:
+ raise MesonException('New value %d is more than maximum value %d.' % (value, self.max_value))
+ return value
+
+ def toint(self, valuestring: str) -> int:
+ try:
+ return int(valuestring)
+ except ValueError:
+ raise MesonException('Value string "%s" is not convertible to an integer.' % valuestring)
+
+class OctalInt(int):
+ # NinjaBackend.get_user_option_args uses str() to converts it to a command line option
+ # UserUmaskOption.toint() uses int(str, 8) to convert it to an integer
+ # So we need to use oct instead of dec here if we do not want values to be misinterpreted.
+ def __str__(self):
+ return oct(int(self))
+
+class UserUmaskOption(UserIntegerOption, UserOption[T.Union[str, OctalInt]]):
+ def __init__(self, description: str, value: T.Any, yielding: T.Optional[bool] = None):
+ super().__init__(description, (0, 0o777, value), yielding)
+ self.choices = ['preserve', '0000-0777']
+
+ def printable_value(self) -> str:
+ if self.value == 'preserve':
+ return self.value
+ return format(self.value, '04o')
+
+ def validate_value(self, value: T.Any) -> T.Union[str, OctalInt]:
+ if value is None or value == 'preserve':
+ return 'preserve'
+ return OctalInt(super().validate_value(value))
+
+ def toint(self, valuestring: T.Union[str, OctalInt]) -> int:
+ try:
+ return int(valuestring, 8)
+ except ValueError as e:
+ raise MesonException(f'Invalid mode: {e}')
+
+class UserComboOption(UserOption[str]):
+ def __init__(self, description: str, choices: T.List[str], value: T.Any, yielding: T.Optional[bool] = None):
+ super().__init__(description, choices, yielding)
+ if not isinstance(self.choices, list):
+ raise MesonException('Combo choices must be an array.')
+ for i in self.choices:
+ if not isinstance(i, str):
+ raise MesonException('Combo choice elements must be strings.')
+ self.set_value(value)
+
+ def validate_value(self, value: T.Any) -> str:
+ if value not in self.choices:
+ if isinstance(value, bool):
+ _type = 'boolean'
+ elif isinstance(value, (int, float)):
+ _type = 'number'
+ else:
+ _type = 'string'
+ optionsstring = ', '.join([f'"{item}"' for item in self.choices])
+ raise MesonException('Value "{}" (of type "{}") for combo option "{}" is not one of the choices.'
+ ' Possible choices are (as string): {}.'.format(
+ value, _type, self.description, optionsstring))
+ return value
+
+class UserArrayOption(UserOption[T.List[str]]):
+ def __init__(self, description: str, value: T.Union[str, T.List[str]], split_args: bool = False, user_input: bool = False, allow_dups: bool = False, **kwargs: T.Any) -> None:
+ super().__init__(description, kwargs.get('choices', []), yielding=kwargs.get('yielding', None))
+ self.split_args = split_args
+ self.allow_dups = allow_dups
+ self.value = self.validate_value(value, user_input=user_input)
+
+ def listify(self, value: T.Union[str, T.List[str]], user_input: bool = True) -> T.List[str]:
+ # User input is for options defined on the command line (via -D
+ # options). Users can put their input in as a comma separated
+ # string, but for defining options in meson_options.txt the format
+ # should match that of a combo
+ if not user_input and isinstance(value, str) and not value.startswith('['):
+ raise MesonException('Value does not define an array: ' + value)
+
+ if isinstance(value, str):
+ if value.startswith('['):
+ try:
+ newvalue = ast.literal_eval(value)
+ except ValueError:
+ raise MesonException(f'malformed option {value}')
+ elif value == '':
+ newvalue = []
+ else:
+ if self.split_args:
+ newvalue = split_args(value)
+ else:
+ newvalue = [v.strip() for v in value.split(',')]
+ elif isinstance(value, list):
+ newvalue = value
+ else:
+ raise MesonException(f'"{value}" should be a string array, but it is not')
+ return newvalue
+
+ def validate_value(self, value: T.Union[str, T.List[str]], user_input: bool = True) -> T.List[str]:
+ newvalue = self.listify(value, user_input)
+
+ if not self.allow_dups and len(set(newvalue)) != len(newvalue):
+ msg = 'Duplicated values in array option is deprecated. ' \
+ 'This will become a hard error in the future.'
+ mlog.deprecation(msg)
+ for i in newvalue:
+ if not isinstance(i, str):
+ raise MesonException(f'String array element "{newvalue!s}" is not a string.')
+ if self.choices:
+ bad = [x for x in newvalue if x not in self.choices]
+ if bad:
+ raise MesonException('Options "{}" are not in allowed choices: "{}"'.format(
+ ', '.join(bad), ', '.join(self.choices)))
+ return newvalue
+
+ def extend_value(self, value: T.Union[str, T.List[str]]) -> None:
+ """Extend the value with an additional value."""
+ new = self.validate_value(value)
+ self.set_value(self.value + new)
+
+
+class UserFeatureOption(UserComboOption):
+ static_choices = ['enabled', 'disabled', 'auto']
+
+ def __init__(self, description: str, value: T.Any, yielding: T.Optional[bool] = None):
+ super().__init__(description, self.static_choices, value, yielding)
+ self.name: T.Optional[str] = None # TODO: Refactor options to all store their name
+
+ def is_enabled(self) -> bool:
+ return self.value == 'enabled'
+
+ def is_disabled(self) -> bool:
+ return self.value == 'disabled'
+
+ def is_auto(self) -> bool:
+ return self.value == 'auto'
+
+
+class DependencyCacheType(enum.Enum):
+
+ OTHER = 0
+ PKG_CONFIG = 1
+ CMAKE = 2
+
+ @classmethod
+ def from_type(cls, dep: 'dependencies.Dependency') -> 'DependencyCacheType':
+ from . import dependencies
+ # As more types gain search overrides they'll need to be added here
+ if isinstance(dep, dependencies.PkgConfigDependency):
+ return cls.PKG_CONFIG
+ if isinstance(dep, dependencies.CMakeDependency):
+ return cls.CMAKE
+ return cls.OTHER
+
+
+class DependencySubCache:
+
+ def __init__(self, type_: DependencyCacheType):
+ self.types = [type_]
+ self.__cache: T.Dict[T.Tuple[str, ...], 'dependencies.Dependency'] = {}
+
+ def __getitem__(self, key: T.Tuple[str, ...]) -> 'dependencies.Dependency':
+ return self.__cache[key]
+
+ def __setitem__(self, key: T.Tuple[str, ...], value: 'dependencies.Dependency') -> None:
+ self.__cache[key] = value
+
+ def __contains__(self, key: T.Tuple[str, ...]) -> bool:
+ return key in self.__cache
+
+ def values(self) -> T.Iterable['dependencies.Dependency']:
+ return self.__cache.values()
+
+
+class DependencyCache:
+
+ """Class that stores a cache of dependencies.
+
+ This class is meant to encapsulate the fact that we need multiple keys to
+ successfully lookup by providing a simple get/put interface.
+ """
+
+ def __init__(self, builtins: 'KeyedOptionDictType', for_machine: MachineChoice):
+ self.__cache = OrderedDict() # type: T.MutableMapping[TV_DepID, DependencySubCache]
+ self.__builtins = builtins
+ self.__pkg_conf_key = OptionKey('pkg_config_path', machine=for_machine)
+ self.__cmake_key = OptionKey('cmake_prefix_path', machine=for_machine)
+
+ def __calculate_subkey(self, type_: DependencyCacheType) -> T.Tuple[str, ...]:
+ data: T.Dict[str, T.List[str]] = {
+ DependencyCacheType.PKG_CONFIG: stringlistify(self.__builtins[self.__pkg_conf_key].value),
+ DependencyCacheType.CMAKE: stringlistify(self.__builtins[self.__cmake_key].value),
+ DependencyCacheType.OTHER: [],
+ }
+ assert type_ in data, 'Someone forgot to update subkey calculations for a new type'
+ return tuple(data[type_])
+
+ def __iter__(self) -> T.Iterator['TV_DepID']:
+ return self.keys()
+
+ def put(self, key: 'TV_DepID', dep: 'dependencies.Dependency') -> None:
+ t = DependencyCacheType.from_type(dep)
+ if key not in self.__cache:
+ self.__cache[key] = DependencySubCache(t)
+ subkey = self.__calculate_subkey(t)
+ self.__cache[key][subkey] = dep
+
+ def get(self, key: 'TV_DepID') -> T.Optional['dependencies.Dependency']:
+ """Get a value from the cache.
+
+ If there is no cache entry then None will be returned.
+ """
+ try:
+ val = self.__cache[key]
+ except KeyError:
+ return None
+
+ for t in val.types:
+ subkey = self.__calculate_subkey(t)
+ try:
+ return val[subkey]
+ except KeyError:
+ pass
+ return None
+
+ def values(self) -> T.Iterator['dependencies.Dependency']:
+ for c in self.__cache.values():
+ yield from c.values()
+
+ def keys(self) -> T.Iterator['TV_DepID']:
+ return iter(self.__cache.keys())
+
+ def items(self) -> T.Iterator[T.Tuple['TV_DepID', T.List['dependencies.Dependency']]]:
+ for k, v in self.__cache.items():
+ vs = []
+ for t in v.types:
+ subkey = self.__calculate_subkey(t)
+ if subkey in v:
+ vs.append(v[subkey])
+ yield k, vs
+
+ def clear(self) -> None:
+ self.__cache.clear()
+
+
+class CMakeStateCache:
+ """Class that stores internal CMake compiler states.
+
+ This cache is used to reduce the startup overhead of CMake by caching
+ all internal CMake compiler variables.
+ """
+
+ def __init__(self) -> None:
+ self.__cache: T.Dict[str, T.Dict[str, T.List[str]]] = {}
+ self.cmake_cache: T.Dict[str, 'CMakeCacheEntry'] = {}
+
+ def __iter__(self) -> T.Iterator[T.Tuple[str, T.Dict[str, T.List[str]]]]:
+ return iter(self.__cache.items())
+
+ def items(self) -> T.Iterator[T.Tuple[str, T.Dict[str, T.List[str]]]]:
+ return iter(self.__cache.items())
+
+ def update(self, language: str, variables: T.Dict[str, T.List[str]]):
+ if language not in self.__cache:
+ self.__cache[language] = {}
+ self.__cache[language].update(variables)
+
+ @property
+ def languages(self) -> T.Set[str]:
+ return set(self.__cache.keys())
+
+
+# Can't bind this near the class method it seems, sadly.
+_V = T.TypeVar('_V')
+
+# This class contains all data that must persist over multiple
+# invocations of Meson. It is roughly the same thing as
+# cmakecache.
+
+class CoreData:
+
+ def __init__(self, options: argparse.Namespace, scratch_dir: str, meson_command: T.List[str]):
+ self.lang_guids = {
+ 'default': '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942',
+ 'c': '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942',
+ 'cpp': '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942',
+ 'test': '3AC096D0-A1C2-E12C-1390-A8335801FDAB',
+ 'directory': '2150E333-8FDC-42A3-9474-1A3956D46DE8',
+ }
+ self.test_guid = str(uuid.uuid4()).upper()
+ self.regen_guid = str(uuid.uuid4()).upper()
+ self.install_guid = str(uuid.uuid4()).upper()
+ self.meson_command = meson_command
+ self.target_guids = {}
+ self.version = version
+ self.options: 'MutableKeyedOptionDictType' = {}
+ self.cross_files = self.__load_config_files(options, scratch_dir, 'cross')
+ self.compilers = PerMachine(OrderedDict(), OrderedDict()) # type: PerMachine[T.Dict[str, Compiler]]
+
+ # Set of subprojects that have already been initialized once, this is
+ # required to be stored and reloaded with the coredata, as we don't
+ # want to overwrite options for such subprojects.
+ self.initialized_subprojects: T.Set[str] = set()
+
+ # For host == build configuraitons these caches should be the same.
+ self.deps: PerMachine[DependencyCache] = PerMachineDefaultable.default(
+ self.is_cross_build(),
+ DependencyCache(self.options, MachineChoice.BUILD),
+ DependencyCache(self.options, MachineChoice.HOST))
+
+ self.compiler_check_cache: T.Dict['CompilerCheckCacheKey', 'CompileResult'] = OrderedDict()
+
+ # CMake cache
+ self.cmake_cache: PerMachine[CMakeStateCache] = PerMachine(CMakeStateCache(), CMakeStateCache())
+
+ # Only to print a warning if it changes between Meson invocations.
+ self.config_files = self.__load_config_files(options, scratch_dir, 'native')
+ self.builtin_options_libdir_cross_fixup()
+ self.init_builtins('')
+
+ @staticmethod
+ def __load_config_files(options: argparse.Namespace, scratch_dir: str, ftype: str) -> T.List[str]:
+ # Need to try and make the passed filenames absolute because when the
+ # files are parsed later we'll have chdir()d.
+ if ftype == 'cross':
+ filenames = options.cross_file
+ else:
+ filenames = options.native_file
+
+ if not filenames:
+ return []
+
+ found_invalid = [] # type: T.List[str]
+ missing = [] # type: T.List[str]
+ real = [] # type: T.List[str]
+ for i, f in enumerate(filenames):
+ f = os.path.expanduser(os.path.expandvars(f))
+ if os.path.exists(f):
+ if os.path.isfile(f):
+ real.append(os.path.abspath(f))
+ continue
+ elif os.path.isdir(f):
+ found_invalid.append(os.path.abspath(f))
+ else:
+ # in this case we've been passed some kind of pipe, copy
+ # the contents of that file into the meson private (scratch)
+ # directory so that it can be re-read when wiping/reconfiguring
+ copy = os.path.join(scratch_dir, f'{uuid.uuid4()}.{ftype}.ini')
+ with open(f, encoding='utf-8') as rf:
+ with open(copy, 'w', encoding='utf-8') as wf:
+ wf.write(rf.read())
+ real.append(copy)
+
+ # Also replace the command line argument, as the pipe
+ # probably won't exist on reconfigure
+ filenames[i] = copy
+ continue
+ if sys.platform != 'win32':
+ paths = [
+ os.environ.get('XDG_DATA_HOME', os.path.expanduser('~/.local/share')),
+ ] + os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':')
+ for path in paths:
+ path_to_try = os.path.join(path, 'meson', ftype, f)
+ if os.path.isfile(path_to_try):
+ real.append(path_to_try)
+ break
+ else:
+ missing.append(f)
+ else:
+ missing.append(f)
+
+ if missing:
+ if found_invalid:
+ mlog.log('Found invalid candidates for', ftype, 'file:', *found_invalid)
+ mlog.log('Could not find any valid candidate for', ftype, 'files:', *missing)
+ raise MesonException(f'Cannot find specified {ftype} file: {f}')
+ return real
+
+ def builtin_options_libdir_cross_fixup(self):
+ # By default set libdir to "lib" when cross compiling since
+ # getting the "system default" is always wrong on multiarch
+ # platforms as it gets a value like lib/x86_64-linux-gnu.
+ if self.cross_files:
+ BUILTIN_OPTIONS[OptionKey('libdir')].default = 'lib'
+
+ def sanitize_prefix(self, prefix):
+ prefix = os.path.expanduser(prefix)
+ if not os.path.isabs(prefix):
+ raise MesonException(f'prefix value {prefix!r} must be an absolute path')
+ if prefix.endswith('/') or prefix.endswith('\\'):
+ # On Windows we need to preserve the trailing slash if the
+ # string is of type 'C:\' because 'C:' is not an absolute path.
+ if len(prefix) == 3 and prefix[1] == ':':
+ pass
+ # If prefix is a single character, preserve it since it is
+ # the root directory.
+ elif len(prefix) == 1:
+ pass
+ else:
+ prefix = prefix[:-1]
+ return prefix
+
+ def sanitize_dir_option_value(self, prefix: str, option: OptionKey, value: T.Any) -> T.Any:
+ '''
+ If the option is an installation directory option, the value is an
+ absolute path and resides within prefix, return the value
+ as a path relative to the prefix. Otherwise, return it as is.
+
+ This way everyone can do f.ex, get_option('libdir') and usually get
+ the library directory relative to prefix, even though it really
+ should not be relied upon.
+ '''
+ try:
+ value = PurePath(value)
+ except TypeError:
+ return value
+ if option.name.endswith('dir') and value.is_absolute() and \
+ option not in BULITIN_DIR_NOPREFIX_OPTIONS:
+ try:
+ # Try to relativize the path.
+ value = value.relative_to(prefix)
+ except ValueError:
+ # Path is not relative, let’s keep it as is.
+ pass
+ if '..' in value.parts:
+ raise MesonException(
+ f'The value of the \'{option}\' option is \'{value}\' but '
+ 'directory options are not allowed to contain \'..\'.\n'
+ f'If you need a path outside of the {prefix!r} prefix, '
+ 'please use an absolute path.'
+ )
+ # .as_posix() keeps the posix-like file separators Meson uses.
+ return value.as_posix()
+
+ def init_builtins(self, subproject: str) -> None:
+ # Create builtin options with default values
+ for key, opt in BUILTIN_OPTIONS.items():
+ self.add_builtin_option(self.options, key.evolve(subproject=subproject), opt)
+ for for_machine in iter(MachineChoice):
+ for key, opt in BUILTIN_OPTIONS_PER_MACHINE.items():
+ self.add_builtin_option(self.options, key.evolve(subproject=subproject, machine=for_machine), opt)
+
+ @staticmethod
+ def add_builtin_option(opts_map: 'MutableKeyedOptionDictType', key: OptionKey,
+ opt: 'BuiltinOption') -> None:
+ if key.subproject:
+ if opt.yielding:
+ # This option is global and not per-subproject
+ return
+ value = opts_map[key.as_root()].value
+ else:
+ value = None
+ opts_map[key] = opt.init_option(key, value, default_prefix())
+
+ def init_backend_options(self, backend_name: str) -> None:
+ if backend_name == 'ninja':
+ self.options[OptionKey('backend_max_links')] = UserIntegerOption(
+ 'Maximum number of linker processes to run or 0 for no '
+ 'limit',
+ (0, None, 0))
+ elif backend_name.startswith('vs'):
+ self.options[OptionKey('backend_startup_project')] = UserStringOption(
+ 'Default project to execute in Visual Studio',
+ '')
+
+ def get_option(self, key: OptionKey) -> T.Union[T.List[str], str, int, bool, WrapMode]:
+ try:
+ v = self.options[key].value
+ if key.name == 'wrap_mode':
+ return WrapMode[v]
+ return v
+ except KeyError:
+ pass
+
+ try:
+ v = self.options[key.as_root()]
+ if v.yielding:
+ if key.name == 'wrap_mode':
+ return WrapMode[v.value]
+ return v.value
+ except KeyError:
+ pass
+
+ raise MesonException(f'Tried to get unknown builtin option {str(key)}')
+
+ def set_option(self, key: OptionKey, value) -> None:
+ if key.is_builtin():
+ if key.name == 'prefix':
+ value = self.sanitize_prefix(value)
+ else:
+ prefix = self.options[OptionKey('prefix')].value
+ value = self.sanitize_dir_option_value(prefix, key, value)
+
+ try:
+ opt = self.options[key]
+ except KeyError:
+ raise MesonException(f'Tried to set unknown builtin option {str(key)}')
+
+ if opt.deprecated is True:
+ mlog.deprecation(f'Option {key.name!r} is deprecated')
+ elif isinstance(opt.deprecated, list):
+ for v in opt.listify(value):
+ if v in opt.deprecated:
+ mlog.deprecation(f'Option {key.name!r} value {v!r} is deprecated')
+ elif isinstance(opt.deprecated, dict):
+ def replace(v):
+ newvalue = opt.deprecated.get(v)
+ if newvalue is not None:
+ mlog.deprecation(f'Option {key.name!r} value {v!r} is replaced by {newvalue!r}')
+ return newvalue
+ return v
+ newvalue = [replace(v) for v in opt.listify(value)]
+ value = ','.join(newvalue)
+ elif isinstance(opt.deprecated, str):
+ # Option is deprecated and replaced by another. Note that a project
+ # option could be replaced by a built-in or module option, which is
+ # why we use OptionKey.from_string(newname) instead of
+ # key.evolve(newname). We set the value on both the old and new names,
+ # assuming they accept the same value. That could for example be
+ # achieved by adding the values from old option as deprecated on the
+ # new option, for example in the case of boolean option is replaced
+ # by a feature option with a different name.
+ newname = opt.deprecated
+ newkey = OptionKey.from_string(newname).evolve(subproject=key.subproject)
+ mlog.deprecation(f'Option {key.name!r} is replaced by {newname!r}')
+ self.set_option(newkey, value)
+
+ opt.set_value(value)
+
+ if key.name == 'buildtype':
+ self._set_others_from_buildtype(value)
+ elif key.name in {'wrap_mode', 'force_fallback_for'}:
+ # We could have the system dependency cached for a dependency that
+ # is now forced to use subproject fallback. We probably could have
+ # more fine grained cache invalidation, but better be safe.
+ self.clear_deps_cache()
+
+ def clear_deps_cache(self):
+ self.deps.host.clear()
+ self.deps.build.clear()
+
+ def get_nondefault_buildtype_args(self):
+ result = []
+ value = self.options[OptionKey('buildtype')].value
+ if value == 'plain':
+ opt = 'plain'
+ debug = False
+ elif value == 'debug':
+ opt = '0'
+ debug = True
+ elif value == 'debugoptimized':
+ opt = '2'
+ debug = True
+ elif value == 'release':
+ opt = '3'
+ debug = False
+ elif value == 'minsize':
+ opt = 's'
+ debug = True
+ else:
+ assert value == 'custom'
+ return []
+ actual_opt = self.options[OptionKey('optimization')].value
+ actual_debug = self.options[OptionKey('debug')].value
+ if actual_opt != opt:
+ result.append(('optimization', actual_opt, opt))
+ if actual_debug != debug:
+ result.append(('debug', actual_debug, debug))
+ return result
+
+ def _set_others_from_buildtype(self, value: str) -> None:
+ if value == 'plain':
+ opt = 'plain'
+ debug = False
+ elif value == 'debug':
+ opt = '0'
+ debug = True
+ elif value == 'debugoptimized':
+ opt = '2'
+ debug = True
+ elif value == 'release':
+ opt = '3'
+ debug = False
+ elif value == 'minsize':
+ opt = 's'
+ debug = True
+ else:
+ assert value == 'custom'
+ return
+ self.options[OptionKey('optimization')].set_value(opt)
+ self.options[OptionKey('debug')].set_value(debug)
+
+ @staticmethod
+ def is_per_machine_option(optname: OptionKey) -> bool:
+ if optname.name in BUILTIN_OPTIONS_PER_MACHINE:
+ return True
+ return optname.lang is not None
+
+ def validate_option_value(self, option_name: OptionKey, override_value):
+ try:
+ opt = self.options[option_name]
+ except KeyError:
+ raise MesonException(f'Tried to validate unknown option {str(option_name)}')
+ try:
+ return opt.validate_value(override_value)
+ except MesonException as e:
+ raise type(e)(('Validation failed for option %s: ' % option_name) + str(e)) \
+ .with_traceback(sys.exc_info()[2])
+
+ def get_external_args(self, for_machine: MachineChoice, lang: str) -> T.List[str]:
+ return self.options[OptionKey('args', machine=for_machine, lang=lang)].value
+
+ def get_external_link_args(self, for_machine: MachineChoice, lang: str) -> T.List[str]:
+ return self.options[OptionKey('link_args', machine=for_machine, lang=lang)].value
+
+ def update_project_options(self, options: 'MutableKeyedOptionDictType') -> None:
+ for key, value in options.items():
+ if not key.is_project():
+ continue
+ if key not in self.options:
+ self.options[key] = value
+ continue
+
+ oldval = self.options[key]
+ if type(oldval) != type(value):
+ self.options[key] = value
+ elif oldval.choices != value.choices:
+ # If the choices have changed, use the new value, but attempt
+ # to keep the old options. If they are not valid keep the new
+ # defaults but warn.
+ self.options[key] = value
+ try:
+ value.set_value(oldval.value)
+ except MesonException:
+ mlog.warning(f'Old value(s) of {key} are no longer valid, resetting to default ({value.value}).')
+
+ def is_cross_build(self, when_building_for: MachineChoice = MachineChoice.HOST) -> bool:
+ if when_building_for == MachineChoice.BUILD:
+ return False
+ return len(self.cross_files) > 0
+
+ def copy_build_options_from_regular_ones(self) -> None:
+ assert not self.is_cross_build()
+ for k in BUILTIN_OPTIONS_PER_MACHINE:
+ o = self.options[k]
+ self.options[k.as_build()].set_value(o.value)
+ for bk, bv in self.options.items():
+ if bk.machine is MachineChoice.BUILD:
+ hk = bk.as_host()
+ try:
+ hv = self.options[hk]
+ bv.set_value(hv.value)
+ except KeyError:
+ continue
+
+ def set_options(self, options: T.Dict[OptionKey, T.Any], subproject: str = '') -> None:
+ if not self.is_cross_build():
+ options = {k: v for k, v in options.items() if k.machine is not MachineChoice.BUILD}
+ # Set prefix first because it's needed to sanitize other options
+ pfk = OptionKey('prefix')
+ if pfk in options:
+ prefix = self.sanitize_prefix(options[pfk])
+ self.options[OptionKey('prefix')].set_value(prefix)
+ for key in BULITIN_DIR_NOPREFIX_OPTIONS:
+ if key not in options:
+ self.options[key].set_value(BUILTIN_OPTIONS[key].prefixed_default(key, prefix))
+
+ unknown_options: T.List[OptionKey] = []
+ for k, v in options.items():
+ if k == pfk:
+ continue
+ elif k in self.options:
+ self.set_option(k, v)
+ elif k.machine != MachineChoice.BUILD and k.type != OptionType.COMPILER:
+ unknown_options.append(k)
+ if unknown_options:
+ unknown_options_str = ', '.join(sorted(str(s) for s in unknown_options))
+ sub = f'In subproject {subproject}: ' if subproject else ''
+ raise MesonException(f'{sub}Unknown options: "{unknown_options_str}"')
+
+ if not self.is_cross_build():
+ self.copy_build_options_from_regular_ones()
+
+ def set_default_options(self, default_options: T.MutableMapping[OptionKey, str], subproject: str, env: 'Environment') -> None:
+ # Main project can set default options on subprojects, but subprojects
+ # can only set default options on themself.
+ # Preserve order: if env.options has 'buildtype' it must come after
+ # 'optimization' if it is in default_options.
+ options: T.MutableMapping[OptionKey, T.Any] = OrderedDict()
+ for k, v in default_options.items():
+ if not subproject or k.subproject == subproject:
+ options[k] = v
+ options.update(env.options)
+ env.options = options
+
+ # Create a subset of options, keeping only project and builtin
+ # options for this subproject.
+ # Language and backend specific options will be set later when adding
+ # languages and setting the backend (builtin options must be set first
+ # to know which backend we'll use).
+ options = OrderedDict()
+
+ for k, v in env.options.items():
+ # If this is a subproject, don't use other subproject options
+ if k.subproject and k.subproject != subproject:
+ continue
+ # If the option is a builtin and is yielding then it's not allowed per subproject.
+ #
+ # Always test this using the HOST machine, as many builtin options
+ # are not valid for the BUILD machine, but the yielding value does
+ # not differ between them even when they are valid for both.
+ if subproject and k.is_builtin() and self.options[k.evolve(subproject='', machine=MachineChoice.HOST)].yielding:
+ continue
+ # Skip base, compiler, and backend options, they are handled when
+ # adding languages and setting backend.
+ if k.type in {OptionType.COMPILER, OptionType.BACKEND, OptionType.BASE}:
+ continue
+ options[k] = v
+
+ self.set_options(options, subproject=subproject)
+
+ def add_compiler_options(self, options: 'MutableKeyedOptionDictType', lang: str, for_machine: MachineChoice,
+ env: 'Environment') -> None:
+ for k, o in options.items():
+ value = env.options.get(k)
+ if value is not None:
+ o.set_value(value)
+ self.options.setdefault(k, o)
+
+ def add_lang_args(self, lang: str, comp: T.Type['Compiler'],
+ for_machine: MachineChoice, env: 'Environment') -> None:
+ """Add global language arguments that are needed before compiler/linker detection."""
+ from .compilers import compilers
+ # These options are all new at this point, because the compiler is
+ # responsible for adding its own options, thus calling
+ # `self.options.update()`` is perfectly safe.
+ self.options.update(compilers.get_global_options(lang, comp, for_machine, env))
+
+ def process_new_compiler(self, lang: str, comp: 'Compiler', env: 'Environment') -> None:
+ from . import compilers
+
+ self.compilers[comp.for_machine][lang] = comp
+ self.add_compiler_options(comp.get_options(), lang, comp.for_machine, env)
+
+ enabled_opts: T.List[OptionKey] = []
+ for key in comp.base_options:
+ if key in self.options:
+ continue
+ oobj = compilers.base_options[key]
+ if key in env.options:
+ oobj.set_value(env.options[key])
+ enabled_opts.append(key)
+ self.options[key] = oobj
+ self.emit_base_options_warnings(enabled_opts)
+
+ def emit_base_options_warnings(self, enabled_opts: T.List[OptionKey]) -> None:
+ if OptionKey('b_bitcode') in enabled_opts:
+ mlog.warning('Base option \'b_bitcode\' is enabled, which is incompatible with many linker options. Incompatible options such as \'b_asneeded\' have been disabled.', fatal=False)
+ mlog.warning('Please see https://mesonbuild.com/Builtin-options.html#Notes_about_Apple_Bitcode_support for more details.', fatal=False)
+
+class CmdLineFileParser(configparser.ConfigParser):
+ def __init__(self) -> None:
+ # We don't want ':' as key delimiter, otherwise it would break when
+ # storing subproject options like "subproject:option=value"
+ super().__init__(delimiters=['='], interpolation=None)
+
+ def read(self, filenames: T.Union['StrOrBytesPath', T.Iterable['StrOrBytesPath']], encoding: str = 'utf-8') -> T.List[str]:
+ return super().read(filenames, encoding)
+
+ def optionxform(self, option: str) -> str:
+ # Don't call str.lower() on keys
+ return option
+
+class MachineFileParser():
+ def __init__(self, filenames: T.List[str]) -> None:
+ self.parser = CmdLineFileParser()
+ self.constants = {'True': True, 'False': False}
+ self.sections = {}
+
+ self.parser.read(filenames)
+
+ # Parse [constants] first so they can be used in other sections
+ if self.parser.has_section('constants'):
+ self.constants.update(self._parse_section('constants'))
+
+ for s in self.parser.sections():
+ if s == 'constants':
+ continue
+ self.sections[s] = self._parse_section(s)
+
+ def _parse_section(self, s):
+ self.scope = self.constants.copy()
+ section = {}
+ for entry, value in self.parser.items(s):
+ if ' ' in entry or '\t' in entry or "'" in entry or '"' in entry:
+ raise EnvironmentException(f'Malformed variable name {entry!r} in machine file.')
+ # Windows paths...
+ value = value.replace('\\', '\\\\')
+ try:
+ ast = mparser.Parser(value, 'machinefile').parse()
+ res = self._evaluate_statement(ast.lines[0])
+ except MesonException:
+ raise EnvironmentException(f'Malformed value in machine file variable {entry!r}.')
+ except KeyError as e:
+ raise EnvironmentException(f'Undefined constant {e.args[0]!r} in machine file variable {entry!r}.')
+ section[entry] = res
+ self.scope[entry] = res
+ return section
+
+ def _evaluate_statement(self, node):
+ if isinstance(node, (mparser.StringNode)):
+ return node.value
+ elif isinstance(node, mparser.BooleanNode):
+ return node.value
+ elif isinstance(node, mparser.NumberNode):
+ return node.value
+ elif isinstance(node, mparser.ArrayNode):
+ return [self._evaluate_statement(arg) for arg in node.args.arguments]
+ elif isinstance(node, mparser.IdNode):
+ return self.scope[node.value]
+ elif isinstance(node, mparser.ArithmeticNode):
+ l = self._evaluate_statement(node.left)
+ r = self._evaluate_statement(node.right)
+ if node.operation == 'add':
+ if (isinstance(l, str) and isinstance(r, str)) or \
+ (isinstance(l, list) and isinstance(r, list)):
+ return l + r
+ elif node.operation == 'div':
+ if isinstance(l, str) and isinstance(r, str):
+ return os.path.join(l, r)
+ raise EnvironmentException('Unsupported node type')
+
+def parse_machine_files(filenames):
+ parser = MachineFileParser(filenames)
+ return parser.sections
+
+def get_cmd_line_file(build_dir: str) -> str:
+ return os.path.join(build_dir, 'meson-private', 'cmd_line.txt')
+
+def read_cmd_line_file(build_dir: str, options: argparse.Namespace) -> None:
+ filename = get_cmd_line_file(build_dir)
+ if not os.path.isfile(filename):
+ return
+
+ config = CmdLineFileParser()
+ config.read(filename)
+
+ # Do a copy because config is not really a dict. options.cmd_line_options
+ # overrides values from the file.
+ d = {OptionKey.from_string(k): v for k, v in config['options'].items()}
+ d.update(options.cmd_line_options)
+ options.cmd_line_options = d
+
+ properties = config['properties']
+ if not options.cross_file:
+ options.cross_file = ast.literal_eval(properties.get('cross_file', '[]'))
+ if not options.native_file:
+ # This will be a string in the form: "['first', 'second', ...]", use
+ # literal_eval to get it into the list of strings.
+ options.native_file = ast.literal_eval(properties.get('native_file', '[]'))
+
+def write_cmd_line_file(build_dir: str, options: argparse.Namespace) -> None:
+ filename = get_cmd_line_file(build_dir)
+ config = CmdLineFileParser()
+
+ properties = OrderedDict()
+ if options.cross_file:
+ properties['cross_file'] = options.cross_file
+ if options.native_file:
+ properties['native_file'] = options.native_file
+
+ config['options'] = {str(k): str(v) for k, v in options.cmd_line_options.items()}
+ config['properties'] = properties
+ with open(filename, 'w', encoding='utf-8') as f:
+ config.write(f)
+
+def update_cmd_line_file(build_dir: str, options: argparse.Namespace):
+ filename = get_cmd_line_file(build_dir)
+ config = CmdLineFileParser()
+ config.read(filename)
+ config['options'].update({str(k): str(v) for k, v in options.cmd_line_options.items()})
+ with open(filename, 'w', encoding='utf-8') as f:
+ config.write(f)
+
+def format_cmd_line_options(options: argparse.Namespace) -> str:
+ cmdline = ['-D{}={}'.format(str(k), v) for k, v in options.cmd_line_options.items()]
+ if options.cross_file:
+ cmdline += [f'--cross-file={f}' for f in options.cross_file]
+ if options.native_file:
+ cmdline += [f'--native-file={f}' for f in options.native_file]
+ return ' '.join([shlex.quote(x) for x in cmdline])
+
+def major_versions_differ(v1: str, v2: str) -> bool:
+ v1_major, v1_minor = v1.rsplit('.', 1)
+ v2_major, v2_minor = v2.rsplit('.', 1)
+ # Major version differ, or one is development version but not the other.
+ return v1_major != v2_major or ('99' in {v1_minor, v2_minor} and v1_minor != v2_minor)
+
+def load(build_dir: str) -> CoreData:
+ filename = os.path.join(build_dir, 'meson-private', 'coredata.dat')
+ obj = pickle_load(filename, 'Coredata', CoreData)
+ assert isinstance(obj, CoreData), 'for mypy'
+ return obj
+
+
+def save(obj: CoreData, build_dir: str) -> str:
+ filename = os.path.join(build_dir, 'meson-private', 'coredata.dat')
+ prev_filename = filename + '.prev'
+ tempfilename = filename + '~'
+ if major_versions_differ(obj.version, version):
+ raise MesonException('Fatal version mismatch corruption.')
+ if os.path.exists(filename):
+ import shutil
+ shutil.copyfile(filename, prev_filename)
+ with open(tempfilename, 'wb') as f:
+ pickle.dump(obj, f)
+ f.flush()
+ os.fsync(f.fileno())
+ replace_if_different(filename, tempfilename)
+ return filename
+
+
+def register_builtin_arguments(parser: argparse.ArgumentParser) -> None:
+ for n, b in BUILTIN_OPTIONS.items():
+ b.add_to_argparse(str(n), parser, '')
+ for n, b in BUILTIN_OPTIONS_PER_MACHINE.items():
+ b.add_to_argparse(str(n), parser, ' (just for host machine)')
+ b.add_to_argparse(str(n.as_build()), parser, ' (just for build machine)')
+ parser.add_argument('-D', action='append', dest='projectoptions', default=[], metavar="option",
+ help='Set the value of an option, can be used several times to set multiple options.')
+
+def create_options_dict(options: T.List[str], subproject: str = '') -> T.Dict[OptionKey, str]:
+ result: T.OrderedDict[OptionKey, str] = OrderedDict()
+ for o in options:
+ try:
+ (key, value) = o.split('=', 1)
+ except ValueError:
+ raise MesonException(f'Option {o!r} must have a value separated by equals sign.')
+ k = OptionKey.from_string(key)
+ if subproject:
+ k = k.evolve(subproject=subproject)
+ result[k] = value
+ return result
+
+def parse_cmd_line_options(args: argparse.Namespace) -> None:
+ args.cmd_line_options = create_options_dict(args.projectoptions)
+
+ # Merge builtin options set with --option into the dict.
+ for key in chain(
+ BUILTIN_OPTIONS.keys(),
+ (k.as_build() for k in BUILTIN_OPTIONS_PER_MACHINE.keys()),
+ BUILTIN_OPTIONS_PER_MACHINE.keys(),
+ ):
+ name = str(key)
+ value = getattr(args, name, None)
+ if value is not None:
+ if key in args.cmd_line_options:
+ cmdline_name = BuiltinOption.argparse_name_to_arg(name)
+ raise MesonException(
+ f'Got argument {name} as both -D{name} and {cmdline_name}. Pick one.')
+ args.cmd_line_options[key] = value
+ delattr(args, name)
+
+
+_U = T.TypeVar('_U', bound=UserOption[_T])
+
+class BuiltinOption(T.Generic[_T, _U]):
+
+ """Class for a builtin option type.
+
+ There are some cases that are not fully supported yet.
+ """
+
+ def __init__(self, opt_type: T.Type[_U], description: str, default: T.Any, yielding: bool = True, *,
+ choices: T.Any = None):
+ self.opt_type = opt_type
+ self.description = description
+ self.default = default
+ self.choices = choices
+ self.yielding = yielding
+
+ def init_option(self, name: 'OptionKey', value: T.Optional[T.Any], prefix: str) -> _U:
+ """Create an instance of opt_type and return it."""
+ if value is None:
+ value = self.prefixed_default(name, prefix)
+ keywords = {'yielding': self.yielding, 'value': value}
+ if self.choices:
+ keywords['choices'] = self.choices
+ return self.opt_type(self.description, **keywords)
+
+ def _argparse_action(self) -> T.Optional[str]:
+ # If the type is a boolean, the presence of the argument in --foo form
+ # is to enable it. Disabling happens by using -Dfoo=false, which is
+ # parsed under `args.projectoptions` and does not hit this codepath.
+ if isinstance(self.default, bool):
+ return 'store_true'
+ return None
+
+ def _argparse_choices(self) -> T.Any:
+ if self.opt_type is UserBooleanOption:
+ return [True, False]
+ elif self.opt_type is UserFeatureOption:
+ return UserFeatureOption.static_choices
+ return self.choices
+
+ @staticmethod
+ def argparse_name_to_arg(name: str) -> str:
+ if name == 'warning_level':
+ return '--warnlevel'
+ else:
+ return '--' + name.replace('_', '-')
+
+ def prefixed_default(self, name: 'OptionKey', prefix: str = '') -> T.Any:
+ if self.opt_type in [UserComboOption, UserIntegerOption]:
+ return self.default
+ try:
+ return BULITIN_DIR_NOPREFIX_OPTIONS[name][prefix]
+ except KeyError:
+ pass
+ return self.default
+
+ def add_to_argparse(self, name: str, parser: argparse.ArgumentParser, help_suffix: str) -> None:
+ kwargs = OrderedDict()
+
+ c = self._argparse_choices()
+ b = self._argparse_action()
+ h = self.description
+ if not b:
+ h = '{} (default: {}).'.format(h.rstrip('.'), self.prefixed_default(name))
+ else:
+ kwargs['action'] = b
+ if c and not b:
+ kwargs['choices'] = c
+ kwargs['default'] = argparse.SUPPRESS
+ kwargs['dest'] = name
+
+ cmdline_name = self.argparse_name_to_arg(name)
+ parser.add_argument(cmdline_name, help=h + help_suffix, **kwargs)
+
+
+# Update `docs/markdown/Builtin-options.md` after changing the options below
+# Also update mesonlib._BUILTIN_NAMES. See the comment there for why this is required.
+BUILTIN_DIR_OPTIONS: 'MutableKeyedOptionDictType' = OrderedDict([
+ (OptionKey('prefix'), BuiltinOption(UserStringOption, 'Installation prefix', default_prefix())),
+ (OptionKey('bindir'), BuiltinOption(UserStringOption, 'Executable directory', 'bin')),
+ (OptionKey('datadir'), BuiltinOption(UserStringOption, 'Data file directory', default_datadir())),
+ (OptionKey('includedir'), BuiltinOption(UserStringOption, 'Header file directory', default_includedir())),
+ (OptionKey('infodir'), BuiltinOption(UserStringOption, 'Info page directory', default_infodir())),
+ (OptionKey('libdir'), BuiltinOption(UserStringOption, 'Library directory', default_libdir())),
+ (OptionKey('libexecdir'), BuiltinOption(UserStringOption, 'Library executable directory', default_libexecdir())),
+ (OptionKey('localedir'), BuiltinOption(UserStringOption, 'Locale data directory', default_localedir())),
+ (OptionKey('localstatedir'), BuiltinOption(UserStringOption, 'Localstate data directory', 'var')),
+ (OptionKey('mandir'), BuiltinOption(UserStringOption, 'Manual page directory', default_mandir())),
+ (OptionKey('sbindir'), BuiltinOption(UserStringOption, 'System executable directory', default_sbindir())),
+ (OptionKey('sharedstatedir'), BuiltinOption(UserStringOption, 'Architecture-independent data directory', 'com')),
+ (OptionKey('sysconfdir'), BuiltinOption(UserStringOption, 'Sysconf data directory', default_sysconfdir())),
+])
+
+BUILTIN_CORE_OPTIONS: 'MutableKeyedOptionDictType' = OrderedDict([
+ (OptionKey('auto_features'), BuiltinOption(UserFeatureOption, "Override value of all 'auto' features", 'auto')),
+ (OptionKey('backend'), BuiltinOption(UserComboOption, 'Backend to use', 'ninja', choices=backendlist)),
+ (OptionKey('buildtype'), BuiltinOption(UserComboOption, 'Build type to use', 'debug',
+ choices=['plain', 'debug', 'debugoptimized', 'release', 'minsize', 'custom'])),
+ (OptionKey('debug'), BuiltinOption(UserBooleanOption, 'Enable debug symbols and other information', True)),
+ (OptionKey('default_library'), BuiltinOption(UserComboOption, 'Default library type', 'shared', choices=['shared', 'static', 'both'],
+ yielding=False)),
+ (OptionKey('errorlogs'), BuiltinOption(UserBooleanOption, "Whether to print the logs from failing tests", True)),
+ (OptionKey('install_umask'), BuiltinOption(UserUmaskOption, 'Default umask to apply on permissions of installed files', '022')),
+ (OptionKey('layout'), BuiltinOption(UserComboOption, 'Build directory layout', 'mirror', choices=['mirror', 'flat'])),
+ (OptionKey('optimization'), BuiltinOption(UserComboOption, 'Optimization level', '0', choices=['plain', '0', 'g', '1', '2', '3', 's'])),
+ (OptionKey('prefer_static'), BuiltinOption(UserBooleanOption, 'Whether to try static linking before shared linking', False)),
+ (OptionKey('stdsplit'), BuiltinOption(UserBooleanOption, 'Split stdout and stderr in test logs', True)),
+ (OptionKey('strip'), BuiltinOption(UserBooleanOption, 'Strip targets on install', False)),
+ (OptionKey('unity'), BuiltinOption(UserComboOption, 'Unity build', 'off', choices=['on', 'off', 'subprojects'])),
+ (OptionKey('unity_size'), BuiltinOption(UserIntegerOption, 'Unity block size', (2, None, 4))),
+ (OptionKey('warning_level'), BuiltinOption(UserComboOption, 'Compiler warning level to use', '1', choices=['0', '1', '2', '3', 'everything'], yielding=False)),
+ (OptionKey('werror'), BuiltinOption(UserBooleanOption, 'Treat warnings as errors', False, yielding=False)),
+ (OptionKey('wrap_mode'), BuiltinOption(UserComboOption, 'Wrap mode', 'default', choices=['default', 'nofallback', 'nodownload', 'forcefallback', 'nopromote'])),
+ (OptionKey('force_fallback_for'), BuiltinOption(UserArrayOption, 'Force fallback for those subprojects', [])),
+
+ # Pkgconfig module
+ (OptionKey('relocatable', module='pkgconfig'),
+ BuiltinOption(UserBooleanOption, 'Generate pkgconfig files as relocatable', False)),
+
+ # Python module
+ (OptionKey('install_env', module='python'),
+ BuiltinOption(UserComboOption, 'Which python environment to install to', 'prefix', choices=['auto', 'prefix', 'system', 'venv'])),
+ (OptionKey('platlibdir', module='python'),
+ BuiltinOption(UserStringOption, 'Directory for site-specific, platform-specific files.', '')),
+ (OptionKey('purelibdir', module='python'),
+ BuiltinOption(UserStringOption, 'Directory for site-specific, non-platform-specific files.', '')),
+])
+
+BUILTIN_OPTIONS = OrderedDict(chain(BUILTIN_DIR_OPTIONS.items(), BUILTIN_CORE_OPTIONS.items()))
+
+BUILTIN_OPTIONS_PER_MACHINE: 'MutableKeyedOptionDictType' = OrderedDict([
+ (OptionKey('pkg_config_path'), BuiltinOption(UserArrayOption, 'List of additional paths for pkg-config to search', [])),
+ (OptionKey('cmake_prefix_path'), BuiltinOption(UserArrayOption, 'List of additional prefixes for cmake to search', [])),
+])
+
+# Special prefix-dependent defaults for installation directories that reside in
+# a path outside of the prefix in FHS and common usage.
+BULITIN_DIR_NOPREFIX_OPTIONS: T.Dict[OptionKey, T.Dict[str, str]] = {
+ OptionKey('sysconfdir'): {'/usr': '/etc'},
+ OptionKey('localstatedir'): {'/usr': '/var', '/usr/local': '/var/local'},
+ OptionKey('sharedstatedir'): {'/usr': '/var/lib', '/usr/local': '/var/local/lib'},
+ OptionKey('platlibdir', module='python'): {},
+ OptionKey('purelibdir', module='python'): {},
+}
+
+FORBIDDEN_TARGET_NAMES = {'clean': None,
+ 'clean-ctlist': None,
+ 'clean-gcno': None,
+ 'clean-gcda': None,
+ 'coverage': None,
+ 'coverage-text': None,
+ 'coverage-xml': None,
+ 'coverage-html': None,
+ 'phony': None,
+ 'PHONY': None,
+ 'all': None,
+ 'test': None,
+ 'benchmark': None,
+ 'install': None,
+ 'uninstall': None,
+ 'build.ninja': None,
+ 'scan-build': None,
+ 'reconfigure': None,
+ 'dist': None,
+ 'distcheck': None,
+ }
diff --git a/mesonbuild/dependencies/__init__.py b/mesonbuild/dependencies/__init__.py
new file mode 100644
index 0000000..b6fdb18
--- /dev/null
+++ b/mesonbuild/dependencies/__init__.py
@@ -0,0 +1,286 @@
+# Copyright 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.
+
+from .boost import BoostDependency
+from .cuda import CudaDependency
+from .hdf5 import hdf5_factory
+from .base import Dependency, InternalDependency, ExternalDependency, NotFoundDependency, MissingCompiler
+from .base import (
+ ExternalLibrary, DependencyException, DependencyMethods,
+ BuiltinDependency, SystemDependency, get_leaf_external_dependencies)
+from .cmake import CMakeDependency
+from .configtool import ConfigToolDependency
+from .dub import DubDependency
+from .framework import ExtraFrameworkDependency
+from .pkgconfig import PkgConfigDependency
+from .factory import DependencyFactory
+from .detect import find_external_dependency, get_dep_identifier, packages, _packages_accept_language
+from .dev import (
+ ValgrindDependency, JNISystemDependency, JDKSystemDependency, gmock_factory, gtest_factory,
+ llvm_factory, zlib_factory)
+from .coarrays import coarray_factory
+from .mpi import mpi_factory
+from .scalapack import scalapack_factory
+from .misc import (
+ BlocksDependency, OpenMPDependency, cups_factory, curses_factory, gpgme_factory,
+ libgcrypt_factory, libwmf_factory, netcdf_factory, pcap_factory, python3_factory,
+ shaderc_factory, threads_factory, ThreadDependency, iconv_factory, intl_factory,
+ dl_factory, openssl_factory, libcrypto_factory, libssl_factory,
+)
+from .platform import AppleFrameworks
+from .qt import qt4_factory, qt5_factory, qt6_factory
+from .ui import GnuStepDependency, WxDependency, gl_factory, sdl2_factory, vulkan_factory
+
+__all__ = [
+ 'Dependency',
+ 'InternalDependency',
+ 'ExternalDependency',
+ 'SystemDependency',
+ 'BuiltinDependency',
+ 'NotFoundDependency',
+ 'ExternalLibrary',
+ 'DependencyException',
+ 'DependencyMethods',
+ 'MissingCompiler',
+
+ 'CMakeDependency',
+ 'ConfigToolDependency',
+ 'DubDependency',
+ 'ExtraFrameworkDependency',
+ 'PkgConfigDependency',
+
+ 'DependencyFactory',
+
+ 'ThreadDependency',
+
+ 'find_external_dependency',
+ 'get_dep_identifier',
+ 'get_leaf_external_dependencies',
+]
+
+"""Dependency representations and discovery logic.
+
+Meson attempts to largely abstract away dependency discovery information, and
+to encapsulate that logic itself so that the DSL doesn't have too much direct
+information. There are some cases where this is impossible/undesirable, such
+as the `get_variable()` method.
+
+Meson has four primary dependency types:
+ 1. pkg-config
+ 2. apple frameworks
+ 3. CMake
+ 4. system
+
+Plus a few more niche ones.
+
+When a user calls `dependency('foo')` Meson creates a list of candidates, and
+tries those candidates in order to find one that matches the criteria
+provided by the user (such as version requirements, or optional components
+that are required.)
+
+Except to work around bugs or handle odd corner cases, pkg-config and CMake
+generally just workâ„¢, though there are exceptions. Most of this package is
+concerned with dependencies that don't (always) provide CMake and/or
+pkg-config files.
+
+For these cases one needs to write a `system` dependency. These dependencies
+descend directly from `ExternalDependency`, in their constructor they
+manually set up the necessary link and compile args (and additional
+dependencies as necessary).
+
+For example, imagine a dependency called Foo, it uses an environment variable
+called `$FOO_ROOT` to point to its install root, which looks like this:
+```txt
+$FOOROOT
+→ include/
+→ lib/
+```
+To use Foo, you need its include directory, and you need to link to
+`lib/libfoo.ext`.
+
+You could write code that looks like:
+
+```python
+class FooSystemDependency(ExternalDependency):
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, environment, kwargs)
+ root = os.environ.get('FOO_ROOT')
+ if root is None:
+ mlog.debug('$FOO_ROOT is unset.')
+ self.is_found = False
+ return
+
+ lib = self.clib_compiler.find_library('foo', environment, [os.path.join(root, 'lib')])
+ if lib is None:
+ mlog.debug('Could not find lib.')
+ self.is_found = False
+ return
+
+ self.compile_args.append(f'-I{os.path.join(root, "include")}')
+ self.link_args.append(lib)
+ self.is_found = True
+```
+
+This code will look for `FOO_ROOT` in the environment, handle `FOO_ROOT` being
+undefined gracefully, then set its `compile_args` and `link_args` gracefully.
+It will also gracefully handle not finding the required lib (hopefully that
+doesn't happen, but it could if, for example, the lib is only static and
+shared linking is requested).
+
+There are a couple of things about this that still aren't ideal. For one, we
+don't want to be reading random environment variables at this point. Those
+should actually be added to `envconfig.Properties` and read in
+`environment.Environment._set_default_properties_from_env` (see how
+`BOOST_ROOT` is handled). We can also handle the `static` keyword and the
+`prefer_static` built-in option. So now that becomes:
+
+```python
+class FooSystemDependency(ExternalDependency):
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, environment, kwargs)
+ root = environment.properties[self.for_machine].foo_root
+ if root is None:
+ mlog.debug('foo_root is unset.')
+ self.is_found = False
+ return
+
+ get_option = environment.coredata.get_option
+ static_opt = kwargs.get('static', get_option(Mesonlib.OptionKey('prefer_static'))
+ static = Mesonlib.LibType.STATIC if static_opt else Mesonlib.LibType.SHARED
+ lib = self.clib_compiler.find_library(
+ 'foo', environment, [os.path.join(root, 'lib')], libtype=static)
+ if lib is None:
+ mlog.debug('Could not find lib.')
+ self.is_found = False
+ return
+
+ self.compile_args.append(f'-I{os.path.join(root, "include")}')
+ self.link_args.append(lib)
+ self.is_found = True
+```
+
+This is nicer in a couple of ways. First we can properly cross compile as we
+are allowed to set `FOO_ROOT` for both the build and host machines, it also
+means that users can override this in their machine files, and if that
+environment variables changes during a Meson reconfigure Meson won't re-read
+it, this is important for reproducibility. Finally, Meson will figure out
+whether it should be finding `libfoo.so` or `libfoo.a` (or the platform
+specific names). Things are looking pretty good now, so it can be added to
+the `packages` dict below:
+
+```python
+packages.update({
+ 'foo': FooSystemDependency,
+})
+```
+
+Now, what if foo also provides pkg-config, but it's only shipped on Unices,
+or only included in very recent versions of the dependency? We can use the
+`DependencyFactory` class:
+
+```python
+foo_factory = DependencyFactory(
+ 'foo',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM],
+ system_class=FooSystemDependency,
+)
+```
+
+This is a helper function that will generate a default pkg-config based
+dependency, and use the `FooSystemDependency` as well. It can also handle
+custom finders for pkg-config and cmake based dependencies that need some
+extra help. You would then add the `foo_factory` to packages instead of
+`FooSystemDependency`:
+
+```python
+packages.update({
+ 'foo': foo_factory,
+})
+```
+
+If you have a dependency that is very complicated, (such as having multiple
+implementations) you may need to write your own factory function. There are a
+number of examples in this package.
+
+_Note_ before we moved to factory functions it was common to use an
+`ExternalDependency` class that would instantiate different types of
+dependencies and hold the one it found. There are a number of drawbacks to
+this approach, and no new dependencies should do this.
+"""
+
+# This is a dict where the keys should be strings, and the values must be one
+# of:
+# - An ExternalDependency subclass
+# - A DependencyFactory object
+# - A callable with a signature of (Environment, MachineChoice, Dict[str, Any]) -> List[Callable[[], ExternalDependency]]
+packages.update({
+ # From dev:
+ 'gtest': gtest_factory,
+ 'gmock': gmock_factory,
+ 'llvm': llvm_factory,
+ 'valgrind': ValgrindDependency,
+ 'zlib': zlib_factory,
+ 'jni': JNISystemDependency,
+ 'jdk': JDKSystemDependency,
+
+ 'boost': BoostDependency,
+ 'cuda': CudaDependency,
+
+ # per-file
+ 'coarray': coarray_factory,
+ 'hdf5': hdf5_factory,
+ 'mpi': mpi_factory,
+ 'scalapack': scalapack_factory,
+
+ # From misc:
+ 'blocks': BlocksDependency,
+ 'curses': curses_factory,
+ 'netcdf': netcdf_factory,
+ 'openmp': OpenMPDependency,
+ 'python3': python3_factory,
+ 'threads': threads_factory,
+ 'pcap': pcap_factory,
+ 'cups': cups_factory,
+ 'libwmf': libwmf_factory,
+ 'libgcrypt': libgcrypt_factory,
+ 'gpgme': gpgme_factory,
+ 'shaderc': shaderc_factory,
+ 'iconv': iconv_factory,
+ 'intl': intl_factory,
+ 'dl': dl_factory,
+ 'openssl': openssl_factory,
+ 'libcrypto': libcrypto_factory,
+ 'libssl': libssl_factory,
+
+ # From platform:
+ 'appleframeworks': AppleFrameworks,
+
+ # From ui:
+ 'gl': gl_factory,
+ 'gnustep': GnuStepDependency,
+ 'qt4': qt4_factory,
+ 'qt5': qt5_factory,
+ 'qt6': qt6_factory,
+ 'sdl2': sdl2_factory,
+ 'wxwidgets': WxDependency,
+ 'vulkan': vulkan_factory,
+})
+_packages_accept_language.update({
+ 'hdf5',
+ 'mpi',
+ 'netcdf',
+ 'openmp',
+})
diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py
new file mode 100644
index 0000000..d826026
--- /dev/null
+++ b/mesonbuild/dependencies/base.py
@@ -0,0 +1,635 @@
+# Copyright 2013-2018 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.
+
+# This file contains the detection logic for external dependencies.
+# Custom logic for several other packages are in separate files.
+
+from __future__ import annotations
+import copy
+import os
+import collections
+import itertools
+import typing as T
+from enum import Enum
+
+from .. import mlog, mesonlib
+from ..compilers import clib_langs
+from ..mesonlib import LibType, MachineChoice, MesonException, HoldableObject, OptionKey
+from ..mesonlib import version_compare_many
+#from ..interpreterbase import FeatureDeprecated, FeatureNew
+
+if T.TYPE_CHECKING:
+ from .._typing import ImmutableListProtocol
+ from ..build import StructuredSources
+ from ..compilers.compilers import Compiler
+ from ..environment import Environment
+ from ..interpreterbase import FeatureCheckBase
+ from ..build import (
+ CustomTarget, IncludeDirs, CustomTargetIndex, LibTypes,
+ StaticLibrary
+ )
+ from ..mesonlib import FileOrString
+
+
+class DependencyException(MesonException):
+ '''Exceptions raised while trying to find dependencies'''
+
+
+class MissingCompiler:
+ """Represent a None Compiler - when no tool chain is found.
+ replacing AttributeError with DependencyException"""
+
+ def __getattr__(self, item: str) -> T.Any:
+ if item.startswith('__'):
+ raise AttributeError()
+ raise DependencyException('no toolchain found')
+
+ def __bool__(self) -> bool:
+ return False
+
+
+class DependencyMethods(Enum):
+ # Auto means to use whatever dependency checking mechanisms in whatever order meson thinks is best.
+ AUTO = 'auto'
+ PKGCONFIG = 'pkg-config'
+ CMAKE = 'cmake'
+ # The dependency is provided by the standard library and does not need to be linked
+ BUILTIN = 'builtin'
+ # Just specify the standard link arguments, assuming the operating system provides the library.
+ SYSTEM = 'system'
+ # This is only supported on OSX - search the frameworks directory by name.
+ EXTRAFRAMEWORK = 'extraframework'
+ # Detect using the sysconfig module.
+ SYSCONFIG = 'sysconfig'
+ # Specify using a "program"-config style tool
+ CONFIG_TOOL = 'config-tool'
+ # For backwards compatibility
+ SDLCONFIG = 'sdlconfig'
+ CUPSCONFIG = 'cups-config'
+ PCAPCONFIG = 'pcap-config'
+ LIBWMFCONFIG = 'libwmf-config'
+ QMAKE = 'qmake'
+ # Misc
+ DUB = 'dub'
+
+
+DependencyTypeName = T.NewType('DependencyTypeName', str)
+
+
+class Dependency(HoldableObject):
+
+ @classmethod
+ def _process_include_type_kw(cls, kwargs: T.Dict[str, T.Any]) -> str:
+ if 'include_type' not in kwargs:
+ return 'preserve'
+ if not isinstance(kwargs['include_type'], str):
+ raise DependencyException('The include_type kwarg must be a string type')
+ if kwargs['include_type'] not in ['preserve', 'system', 'non-system']:
+ raise DependencyException("include_type may only be one of ['preserve', 'system', 'non-system']")
+ return kwargs['include_type']
+
+ def __init__(self, type_name: DependencyTypeName, kwargs: T.Dict[str, T.Any]) -> None:
+ self.name = "null"
+ self.version: T.Optional[str] = None
+ self.language: T.Optional[str] = None # None means C-like
+ self.is_found = False
+ self.type_name = type_name
+ self.compile_args: T.List[str] = []
+ self.link_args: T.List[str] = []
+ # Raw -L and -l arguments without manual library searching
+ # If None, self.link_args will be used
+ self.raw_link_args: T.Optional[T.List[str]] = None
+ self.sources: T.List[T.Union['FileOrString', 'CustomTarget', 'StructuredSources']] = []
+ self.include_type = self._process_include_type_kw(kwargs)
+ self.ext_deps: T.List[Dependency] = []
+ self.d_features: T.DefaultDict[str, T.List[T.Any]] = collections.defaultdict(list)
+ self.featurechecks: T.List['FeatureCheckBase'] = []
+ self.feature_since: T.Optional[T.Tuple[str, str]] = None
+
+ def __repr__(self) -> str:
+ return f'<{self.__class__.__name__} {self.name}: {self.is_found}>'
+
+ def is_built(self) -> bool:
+ return False
+
+ def summary_value(self) -> T.Union[str, mlog.AnsiDecorator, mlog.AnsiText]:
+ if not self.found():
+ return mlog.red('NO')
+ if not self.version:
+ return mlog.green('YES')
+ return mlog.AnsiText(mlog.green('YES'), ' ', mlog.cyan(self.version))
+
+ def get_compile_args(self) -> T.List[str]:
+ if self.include_type == 'system':
+ converted = []
+ for i in self.compile_args:
+ if i.startswith('-I') or i.startswith('/I'):
+ converted += ['-isystem' + i[2:]]
+ else:
+ converted += [i]
+ return converted
+ if self.include_type == 'non-system':
+ converted = []
+ for i in self.compile_args:
+ if i.startswith('-isystem'):
+ converted += ['-I' + i[8:]]
+ else:
+ converted += [i]
+ return converted
+ return self.compile_args
+
+ def get_all_compile_args(self) -> T.List[str]:
+ """Get the compile arguments from this dependency and it's sub dependencies."""
+ return list(itertools.chain(self.get_compile_args(),
+ *(d.get_all_compile_args() for d in self.ext_deps)))
+
+ def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]:
+ if raw and self.raw_link_args is not None:
+ return self.raw_link_args
+ return self.link_args
+
+ def get_all_link_args(self) -> T.List[str]:
+ """Get the link arguments from this dependency and it's sub dependencies."""
+ return list(itertools.chain(self.get_link_args(),
+ *(d.get_all_link_args() for d in self.ext_deps)))
+
+ def found(self) -> bool:
+ return self.is_found
+
+ def get_sources(self) -> T.List[T.Union['FileOrString', 'CustomTarget', 'StructuredSources']]:
+ """Source files that need to be added to the target.
+ As an example, gtest-all.cc when using GTest."""
+ return self.sources
+
+ def get_name(self) -> str:
+ return self.name
+
+ def get_version(self) -> str:
+ if self.version:
+ return self.version
+ else:
+ return 'unknown'
+
+ def get_include_dirs(self) -> T.List['IncludeDirs']:
+ return []
+
+ def get_include_type(self) -> str:
+ return self.include_type
+
+ def get_exe_args(self, compiler: 'Compiler') -> T.List[str]:
+ return []
+
+ def get_pkgconfig_variable(self, variable_name: str,
+ define_variable: 'ImmutableListProtocol[str]',
+ default: T.Optional[str]) -> str:
+ raise DependencyException(f'{self.name!r} is not a pkgconfig dependency')
+
+ def get_configtool_variable(self, variable_name: str) -> str:
+ raise DependencyException(f'{self.name!r} is not a config-tool dependency')
+
+ def get_partial_dependency(self, *, compile_args: bool = False,
+ link_args: bool = False, links: bool = False,
+ includes: bool = False, sources: bool = False) -> 'Dependency':
+ """Create a new dependency that contains part of the parent dependency.
+
+ The following options can be inherited:
+ links -- all link_with arguments
+ includes -- all include_directory and -I/-isystem calls
+ sources -- any source, header, or generated sources
+ compile_args -- any compile args
+ link_args -- any link args
+
+ Additionally the new dependency will have the version parameter of it's
+ parent (if any) and the requested values of any dependencies will be
+ added as well.
+ """
+ raise RuntimeError('Unreachable code in partial_dependency called')
+
+ def _add_sub_dependency(self, deplist: T.Iterable[T.Callable[[], 'Dependency']]) -> bool:
+ """Add an internal dependency from a list of possible dependencies.
+
+ This method is intended to make it easier to add additional
+ dependencies to another dependency internally.
+
+ Returns true if the dependency was successfully added, false
+ otherwise.
+ """
+ for d in deplist:
+ dep = d()
+ if dep.is_found:
+ self.ext_deps.append(dep)
+ return True
+ return False
+
+ def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
+ configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
+ default_value: T.Optional[str] = None,
+ pkgconfig_define: T.Optional[T.List[str]] = None) -> str:
+ if default_value is not None:
+ return default_value
+ raise DependencyException(f'No default provided for dependency {self!r}, which is not pkg-config, cmake, or config-tool based.')
+
+ def generate_system_dependency(self, include_type: str) -> 'Dependency':
+ new_dep = copy.deepcopy(self)
+ new_dep.include_type = self._process_include_type_kw({'include_type': include_type})
+ return new_dep
+
+class InternalDependency(Dependency):
+ def __init__(self, version: str, incdirs: T.List['IncludeDirs'], compile_args: T.List[str],
+ link_args: T.List[str],
+ libraries: T.List[LibTypes],
+ whole_libraries: T.List[T.Union[StaticLibrary, CustomTarget, CustomTargetIndex]],
+ sources: T.Sequence[T.Union[FileOrString, CustomTarget, StructuredSources]],
+ ext_deps: T.List[Dependency], variables: T.Dict[str, str],
+ d_module_versions: T.List[T.Union[str, int]], d_import_dirs: T.List['IncludeDirs']):
+ super().__init__(DependencyTypeName('internal'), {})
+ self.version = version
+ self.is_found = True
+ self.include_directories = incdirs
+ self.compile_args = compile_args
+ self.link_args = link_args
+ self.libraries = libraries
+ self.whole_libraries = whole_libraries
+ self.sources = list(sources)
+ self.ext_deps = ext_deps
+ self.variables = variables
+ if d_module_versions:
+ self.d_features['versions'] = d_module_versions
+ if d_import_dirs:
+ self.d_features['import_dirs'] = d_import_dirs
+
+ def __deepcopy__(self, memo: T.Dict[int, 'InternalDependency']) -> 'InternalDependency':
+ result = self.__class__.__new__(self.__class__)
+ assert isinstance(result, InternalDependency)
+ memo[id(self)] = result
+ for k, v in self.__dict__.items():
+ if k in {'libraries', 'whole_libraries'}:
+ setattr(result, k, copy.copy(v))
+ else:
+ setattr(result, k, copy.deepcopy(v, memo))
+ return result
+
+ def summary_value(self) -> mlog.AnsiDecorator:
+ # Omit the version. Most of the time it will be just the project
+ # version, which is uninteresting in the summary.
+ return mlog.green('YES')
+
+ def is_built(self) -> bool:
+ if self.sources or self.libraries or self.whole_libraries:
+ return True
+ return any(d.is_built() for d in self.ext_deps)
+
+ def get_pkgconfig_variable(self, variable_name: str,
+ define_variable: 'ImmutableListProtocol[str]',
+ default: T.Optional[str]) -> str:
+ raise DependencyException('Method "get_pkgconfig_variable()" is '
+ 'invalid for an internal dependency')
+
+ def get_configtool_variable(self, variable_name: str) -> str:
+ raise DependencyException('Method "get_configtool_variable()" is '
+ 'invalid for an internal dependency')
+
+ def get_partial_dependency(self, *, compile_args: bool = False,
+ link_args: bool = False, links: bool = False,
+ includes: bool = False, sources: bool = False) -> 'InternalDependency':
+ final_compile_args = self.compile_args.copy() if compile_args else []
+ final_link_args = self.link_args.copy() if link_args else []
+ final_libraries = self.libraries.copy() if links else []
+ final_whole_libraries = self.whole_libraries.copy() if links else []
+ final_sources = self.sources.copy() if sources else []
+ final_includes = self.include_directories.copy() if includes else []
+ final_deps = [d.get_partial_dependency(
+ compile_args=compile_args, link_args=link_args, links=links,
+ includes=includes, sources=sources) for d in self.ext_deps]
+ return InternalDependency(
+ self.version, final_includes, final_compile_args,
+ final_link_args, final_libraries, final_whole_libraries,
+ final_sources, final_deps, self.variables, [], [])
+
+ def get_include_dirs(self) -> T.List['IncludeDirs']:
+ return self.include_directories
+
+ def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
+ configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
+ default_value: T.Optional[str] = None,
+ pkgconfig_define: T.Optional[T.List[str]] = None) -> str:
+ val = self.variables.get(internal, default_value)
+ if val is not None:
+ return val
+ raise DependencyException(f'Could not get an internal variable and no default provided for {self!r}')
+
+ def generate_link_whole_dependency(self) -> Dependency:
+ from ..build import SharedLibrary, CustomTarget, CustomTargetIndex
+ new_dep = copy.deepcopy(self)
+ for x in new_dep.libraries:
+ if isinstance(x, SharedLibrary):
+ raise MesonException('Cannot convert a dependency to link_whole when it contains a '
+ 'SharedLibrary')
+ elif isinstance(x, (CustomTarget, CustomTargetIndex)) and x.links_dynamically():
+ raise MesonException('Cannot convert a dependency to link_whole when it contains a '
+ 'CustomTarget or CustomTargetIndex which is a shared library')
+
+ # Mypy doesn't understand that the above is a TypeGuard
+ new_dep.whole_libraries += T.cast('T.List[T.Union[StaticLibrary, CustomTarget, CustomTargetIndex]]',
+ new_dep.libraries)
+ new_dep.libraries = []
+ return new_dep
+
+class HasNativeKwarg:
+ def __init__(self, kwargs: T.Dict[str, T.Any]):
+ self.for_machine = self.get_for_machine_from_kwargs(kwargs)
+
+ def get_for_machine_from_kwargs(self, kwargs: T.Dict[str, T.Any]) -> MachineChoice:
+ return MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST
+
+class ExternalDependency(Dependency, HasNativeKwarg):
+ def __init__(self, type_name: DependencyTypeName, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None):
+ Dependency.__init__(self, type_name, kwargs)
+ self.env = environment
+ self.name = type_name # default
+ self.is_found = False
+ self.language = language
+ version_reqs = kwargs.get('version', None)
+ if isinstance(version_reqs, str):
+ version_reqs = [version_reqs]
+ self.version_reqs: T.Optional[T.List[str]] = version_reqs
+ self.required = kwargs.get('required', True)
+ self.silent = kwargs.get('silent', False)
+ self.static = kwargs.get('static', self.env.coredata.get_option(OptionKey('prefer_static')))
+ self.libtype = LibType.STATIC if self.static else LibType.PREFER_SHARED
+ if not isinstance(self.static, bool):
+ raise DependencyException('Static keyword must be boolean')
+ # Is this dependency to be run on the build platform?
+ HasNativeKwarg.__init__(self, kwargs)
+ self.clib_compiler = detect_compiler(self.name, environment, self.for_machine, self.language)
+
+ def get_compiler(self) -> T.Union['MissingCompiler', 'Compiler']:
+ return self.clib_compiler
+
+ def get_partial_dependency(self, *, compile_args: bool = False,
+ link_args: bool = False, links: bool = False,
+ includes: bool = False, sources: bool = False) -> Dependency:
+ new = copy.copy(self)
+ if not compile_args:
+ new.compile_args = []
+ if not link_args:
+ new.link_args = []
+ if not sources:
+ new.sources = []
+ if not includes:
+ pass # TODO maybe filter compile_args?
+ if not sources:
+ new.sources = []
+
+ return new
+
+ def log_details(self) -> str:
+ return ''
+
+ def log_info(self) -> str:
+ return ''
+
+ @staticmethod
+ def log_tried() -> str:
+ return ''
+
+ # Check if dependency version meets the requirements
+ def _check_version(self) -> None:
+ if not self.is_found:
+ return
+
+ if self.version_reqs:
+ # an unknown version can never satisfy any requirement
+ if not self.version:
+ self.is_found = False
+ found_msg: mlog.TV_LoggableList = []
+ found_msg += ['Dependency', mlog.bold(self.name), 'found:']
+ found_msg += [mlog.red('NO'), 'unknown version, but need:', self.version_reqs]
+ mlog.log(*found_msg)
+
+ if self.required:
+ m = f'Unknown version, but need {self.version_reqs!r}.'
+ raise DependencyException(m)
+
+ else:
+ (self.is_found, not_found, found) = \
+ version_compare_many(self.version, self.version_reqs)
+ if not self.is_found:
+ found_msg = ['Dependency', mlog.bold(self.name), 'found:']
+ found_msg += [mlog.red('NO'),
+ 'found', mlog.normal_cyan(self.version), 'but need:',
+ mlog.bold(', '.join([f"'{e}'" for e in not_found]))]
+ if found:
+ found_msg += ['; matched:',
+ ', '.join([f"'{e}'" for e in found])]
+ mlog.log(*found_msg)
+
+ if self.required:
+ m = 'Invalid version, need {!r} {!r} found {!r}.'
+ raise DependencyException(m.format(self.name, not_found, self.version))
+ return
+
+
+class NotFoundDependency(Dependency):
+ def __init__(self, name: str, environment: 'Environment') -> None:
+ super().__init__(DependencyTypeName('not-found'), {})
+ self.env = environment
+ self.name = name
+ self.is_found = False
+
+ def get_partial_dependency(self, *, compile_args: bool = False,
+ link_args: bool = False, links: bool = False,
+ includes: bool = False, sources: bool = False) -> 'NotFoundDependency':
+ return copy.copy(self)
+
+
+class ExternalLibrary(ExternalDependency):
+ def __init__(self, name: str, link_args: T.List[str], environment: 'Environment',
+ language: str, silent: bool = False) -> None:
+ super().__init__(DependencyTypeName('library'), environment, {}, language=language)
+ self.name = name
+ self.language = language
+ self.is_found = False
+ if link_args:
+ self.is_found = True
+ self.link_args = link_args
+ if not silent:
+ if self.is_found:
+ mlog.log('Library', mlog.bold(name), 'found:', mlog.green('YES'))
+ else:
+ mlog.log('Library', mlog.bold(name), 'found:', mlog.red('NO'))
+
+ def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]:
+ '''
+ External libraries detected using a compiler must only be used with
+ compatible code. For instance, Vala libraries (.vapi files) cannot be
+ used with C code, and not all Rust library types can be linked with
+ C-like code. Note that C++ libraries *can* be linked with C code with
+ a C++ linker (and vice-versa).
+ '''
+ # Using a vala library in a non-vala target, or a non-vala library in a vala target
+ # XXX: This should be extended to other non-C linkers such as Rust
+ if (self.language == 'vala' and language != 'vala') or \
+ (language == 'vala' and self.language != 'vala'):
+ return []
+ return super().get_link_args(language=language, raw=raw)
+
+ def get_partial_dependency(self, *, compile_args: bool = False,
+ link_args: bool = False, links: bool = False,
+ includes: bool = False, sources: bool = False) -> 'ExternalLibrary':
+ # External library only has link_args, so ignore the rest of the
+ # interface.
+ new = copy.copy(self)
+ if not link_args:
+ new.link_args = []
+ return new
+
+
+def get_leaf_external_dependencies(deps: T.List[Dependency]) -> T.List[Dependency]:
+ if not deps:
+ # Ensure that we always return a new instance
+ return deps.copy()
+ final_deps = []
+ while deps:
+ next_deps = []
+ for d in mesonlib.listify(deps):
+ if not isinstance(d, Dependency) or d.is_built():
+ raise DependencyException('Dependencies must be external dependencies')
+ final_deps.append(d)
+ next_deps.extend(d.ext_deps)
+ deps = next_deps
+ return final_deps
+
+
+def sort_libpaths(libpaths: T.List[str], refpaths: T.List[str]) -> T.List[str]:
+ """Sort <libpaths> according to <refpaths>
+
+ It is intended to be used to sort -L flags returned by pkg-config.
+ Pkg-config returns flags in random order which cannot be relied on.
+ """
+ if len(refpaths) == 0:
+ return list(libpaths)
+
+ def key_func(libpath: str) -> T.Tuple[int, int]:
+ common_lengths: T.List[int] = []
+ for refpath in refpaths:
+ try:
+ common_path: str = os.path.commonpath([libpath, refpath])
+ except ValueError:
+ common_path = ''
+ common_lengths.append(len(common_path))
+ max_length = max(common_lengths)
+ max_index = common_lengths.index(max_length)
+ reversed_max_length = len(refpaths[max_index]) - max_length
+ return (max_index, reversed_max_length)
+ return sorted(libpaths, key=key_func)
+
+def strip_system_libdirs(environment: 'Environment', for_machine: MachineChoice, link_args: T.List[str]) -> T.List[str]:
+ """Remove -L<system path> arguments.
+
+ leaving these in will break builds where a user has a version of a library
+ in the system path, and a different version not in the system path if they
+ want to link against the non-system path version.
+ """
+ exclude = {f'-L{p}' for p in environment.get_compiler_system_dirs(for_machine)}
+ return [l for l in link_args if l not in exclude]
+
+def process_method_kw(possible: T.Iterable[DependencyMethods], kwargs: T.Dict[str, T.Any]) -> T.List[DependencyMethods]:
+ method = kwargs.get('method', 'auto') # type: T.Union[DependencyMethods, str]
+ if isinstance(method, DependencyMethods):
+ return [method]
+ # TODO: try/except?
+ if method not in [e.value for e in DependencyMethods]:
+ raise DependencyException(f'method {method!r} is invalid')
+ method = DependencyMethods(method)
+
+ # Raise FeatureNew where appropriate
+ if method is DependencyMethods.CONFIG_TOOL:
+ # FIXME: needs to get a handle on the subproject
+ # FeatureNew.single_use('Configuration method "config-tool"', '0.44.0')
+ pass
+ # This sets per-tool config methods which are deprecated to to the new
+ # generic CONFIG_TOOL value.
+ if method in [DependencyMethods.SDLCONFIG, DependencyMethods.CUPSCONFIG,
+ DependencyMethods.PCAPCONFIG, DependencyMethods.LIBWMFCONFIG]:
+ # FIXME: needs to get a handle on the subproject
+ #FeatureDeprecated.single_use(f'Configuration method {method.value}', '0.44', 'Use "config-tool" instead.')
+ method = DependencyMethods.CONFIG_TOOL
+ if method is DependencyMethods.QMAKE:
+ # FIXME: needs to get a handle on the subproject
+ # FeatureDeprecated.single_use('Configuration method "qmake"', '0.58', 'Use "config-tool" instead.')
+ method = DependencyMethods.CONFIG_TOOL
+
+ # Set the detection method. If the method is set to auto, use any available method.
+ # If method is set to a specific string, allow only that detection method.
+ if method == DependencyMethods.AUTO:
+ methods = list(possible)
+ elif method in possible:
+ methods = [method]
+ else:
+ raise DependencyException(
+ 'Unsupported detection method: {}, allowed methods are {}'.format(
+ method.value,
+ mlog.format_list([x.value for x in [DependencyMethods.AUTO] + list(possible)])))
+
+ return methods
+
+def detect_compiler(name: str, env: 'Environment', for_machine: MachineChoice,
+ language: T.Optional[str]) -> T.Union['MissingCompiler', 'Compiler']:
+ """Given a language and environment find the compiler used."""
+ compilers = env.coredata.compilers[for_machine]
+
+ # Set the compiler for this dependency if a language is specified,
+ # else try to pick something that looks usable.
+ if language:
+ if language not in compilers:
+ m = name.capitalize() + ' requires a {0} compiler, but ' \
+ '{0} is not in the list of project languages'
+ raise DependencyException(m.format(language.capitalize()))
+ return compilers[language]
+ else:
+ for lang in clib_langs:
+ try:
+ return compilers[lang]
+ except KeyError:
+ continue
+ return MissingCompiler()
+
+
+class SystemDependency(ExternalDependency):
+
+ """Dependency base for System type dependencies."""
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
+ language: T.Optional[str] = None) -> None:
+ super().__init__(DependencyTypeName('system'), env, kwargs, language=language)
+ self.name = name
+
+ @staticmethod
+ def log_tried() -> str:
+ return 'system'
+
+
+class BuiltinDependency(ExternalDependency):
+
+ """Dependency base for Builtin type dependencies."""
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
+ language: T.Optional[str] = None) -> None:
+ super().__init__(DependencyTypeName('builtin'), env, kwargs, language=language)
+ self.name = name
+
+ @staticmethod
+ def log_tried() -> str:
+ return 'builtin'
diff --git a/mesonbuild/dependencies/boost.py b/mesonbuild/dependencies/boost.py
new file mode 100644
index 0000000..4ebd88d
--- /dev/null
+++ b/mesonbuild/dependencies/boost.py
@@ -0,0 +1,1090 @@
+# Copyright 2013-2020 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.
+from __future__ import annotations
+
+import re
+import dataclasses
+import functools
+import typing as T
+from pathlib import Path
+
+from .. import mlog
+from .. import mesonlib
+
+from .base import DependencyException, SystemDependency
+from .pkgconfig import PkgConfigDependency
+from .misc import threads_factory
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment, Properties
+
+# On windows 3 directory layouts are supported:
+# * The default layout (versioned) installed:
+# - $BOOST_ROOT/include/boost-x_x/boost/*.hpp
+# - $BOOST_ROOT/lib/*.lib
+# * The non-default layout (system) installed:
+# - $BOOST_ROOT/include/boost/*.hpp
+# - $BOOST_ROOT/lib/*.lib
+# * The pre-built binaries from sf.net:
+# - $BOOST_ROOT/boost/*.hpp
+# - $BOOST_ROOT/lib<arch>-<compiler>/*.lib where arch=32/64 and compiler=msvc-14.1
+#
+# Note that we should also try to support:
+# mingw-w64 / Windows : libboost_<module>-mt.a (location = <prefix>/mingw64/lib/)
+# libboost_<module>-mt.dll.a
+#
+# The `modules` argument accept library names. This is because every module that
+# has libraries to link against also has multiple options regarding how to
+# link. See for example:
+# * http://www.boost.org/doc/libs/1_65_1/libs/test/doc/html/boost_test/usage_variants.html
+# * http://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace/configuration_and_build.html
+# * http://www.boost.org/doc/libs/1_65_1/libs/math/doc/html/math_toolkit/main_tr1.html
+
+# **On Unix**, official packaged versions of boost libraries follow the following schemes:
+#
+# Linux / Debian: libboost_<module>.so -> libboost_<module>.so.1.66.0
+# Linux / Red Hat: libboost_<module>.so -> libboost_<module>.so.1.66.0
+# Linux / OpenSuse: libboost_<module>.so -> libboost_<module>.so.1.66.0
+# Win / Cygwin: libboost_<module>.dll.a (location = /usr/lib)
+# libboost_<module>.a
+# cygboost_<module>_1_64.dll (location = /usr/bin)
+# Win / VS: boost_<module>-vc<ver>-mt[-gd]-<arch>-1_67.dll (location = C:/local/boost_1_67_0)
+# Mac / homebrew: libboost_<module>.dylib + libboost_<module>-mt.dylib (location = /usr/local/lib)
+# Mac / macports: libboost_<module>.dylib + libboost_<module>-mt.dylib (location = /opt/local/lib)
+#
+# Its not clear that any other abi tags (e.g. -gd) are used in official packages.
+#
+# On Linux systems, boost libs have multithreading support enabled, but without the -mt tag.
+#
+# Boost documentation recommends using complex abi tags like "-lboost_regex-gcc34-mt-d-1_36".
+# (See http://www.boost.org/doc/libs/1_66_0/more/getting_started/unix-variants.html#library-naming)
+# However, its not clear that any Unix distribution follows this scheme.
+# Furthermore, the boost documentation for unix above uses examples from windows like
+# "libboost_regex-vc71-mt-d-x86-1_34.lib", so apparently the abi tags may be more aimed at windows.
+#
+# We follow the following strategy for finding modules:
+# A) Detect potential boost root directories (uses also BOOST_ROOT env var)
+# B) Foreach candidate
+# 1. Look for the boost headers (boost/version.pp)
+# 2. Find all boost libraries
+# 2.1 Add all libraries in lib*
+# 2.2 Filter out non boost libraries
+# 2.3 Filter the renaining libraries based on the meson requirements (static/shared, etc.)
+# 2.4 Ensure that all libraries have the same boost tag (and are thus compatible)
+# 3. Select the libraries matching the requested modules
+
+@dataclasses.dataclass(eq=False, order=False)
+class UnknownFileException(Exception):
+ path: Path
+
+@functools.total_ordering
+class BoostIncludeDir():
+ def __init__(self, path: Path, version_int: int):
+ self.path = path
+ self.version_int = version_int
+ major = int(self.version_int / 100000)
+ minor = int((self.version_int / 100) % 1000)
+ patch = int(self.version_int % 100)
+ self.version = f'{major}.{minor}.{patch}'
+ self.version_lib = f'{major}_{minor}'
+
+ def __repr__(self) -> str:
+ return f'<BoostIncludeDir: {self.version} -- {self.path}>'
+
+ def __lt__(self, other: object) -> bool:
+ if isinstance(other, BoostIncludeDir):
+ return (self.version_int, self.path) < (other.version_int, other.path)
+ return NotImplemented
+
+@functools.total_ordering
+class BoostLibraryFile():
+ # Python libraries are special because of the included
+ # minor version in the module name.
+ boost_python_libs = ['boost_python', 'boost_numpy']
+ reg_python_mod_split = re.compile(r'(boost_[a-zA-Z]+)([0-9]*)')
+
+ reg_abi_tag = re.compile(r'^s?g?y?d?p?n?$')
+ reg_ver_tag = re.compile(r'^[0-9_]+$')
+
+ def __init__(self, path: Path):
+ self.path = path
+ self.name = self.path.name
+
+ # Initialize default properties
+ self.static = False
+ self.toolset = ''
+ self.arch = ''
+ self.version_lib = ''
+ self.mt = True
+
+ self.runtime_static = False
+ self.runtime_debug = False
+ self.python_debug = False
+ self.debug = False
+ self.stlport = False
+ self.deprecated_iostreams = False
+
+ # Post process the library name
+ name_parts = self.name.split('.')
+ self.basename = name_parts[0]
+ self.suffixes = name_parts[1:]
+ self.vers_raw = [x for x in self.suffixes if x.isdigit()]
+ self.suffixes = [x for x in self.suffixes if not x.isdigit()]
+ self.nvsuffix = '.'.join(self.suffixes) # Used for detecting the library type
+ self.nametags = self.basename.split('-')
+ self.mod_name = self.nametags[0]
+ if self.mod_name.startswith('lib'):
+ self.mod_name = self.mod_name[3:]
+
+ # Set library version if possible
+ if len(self.vers_raw) >= 2:
+ self.version_lib = '{}_{}'.format(self.vers_raw[0], self.vers_raw[1])
+
+ # Detecting library type
+ if self.nvsuffix in {'so', 'dll', 'dll.a', 'dll.lib', 'dylib'}:
+ self.static = False
+ elif self.nvsuffix in {'a', 'lib'}:
+ self.static = True
+ else:
+ raise UnknownFileException(self.path)
+
+ # boost_.lib is the dll import library
+ if self.basename.startswith('boost_') and self.nvsuffix == 'lib':
+ self.static = False
+
+ # Process tags
+ tags = self.nametags[1:]
+ # Filter out the python version tag and fix modname
+ if self.is_python_lib():
+ tags = self.fix_python_name(tags)
+ if not tags:
+ return
+
+ # Without any tags mt is assumed, however, an absence of mt in the name
+ # with tags present indicates that the lib was built without mt support
+ self.mt = False
+ for i in tags:
+ if i == 'mt':
+ self.mt = True
+ elif len(i) == 3 and i[1:] in {'32', '64'}:
+ self.arch = i
+ elif BoostLibraryFile.reg_abi_tag.match(i):
+ self.runtime_static = 's' in i
+ self.runtime_debug = 'g' in i
+ self.python_debug = 'y' in i
+ self.debug = 'd' in i
+ self.stlport = 'p' in i
+ self.deprecated_iostreams = 'n' in i
+ elif BoostLibraryFile.reg_ver_tag.match(i):
+ self.version_lib = i
+ else:
+ self.toolset = i
+
+ def __repr__(self) -> str:
+ return f'<LIB: {self.abitag} {self.mod_name:<32} {self.path}>'
+
+ def __lt__(self, other: object) -> bool:
+ if isinstance(other, BoostLibraryFile):
+ return (
+ self.mod_name, self.static, self.version_lib, self.arch,
+ not self.mt, not self.runtime_static,
+ not self.debug, self.runtime_debug, self.python_debug,
+ self.stlport, self.deprecated_iostreams,
+ self.name,
+ ) < (
+ other.mod_name, other.static, other.version_lib, other.arch,
+ not other.mt, not other.runtime_static,
+ not other.debug, other.runtime_debug, other.python_debug,
+ other.stlport, other.deprecated_iostreams,
+ other.name,
+ )
+ return NotImplemented
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, BoostLibraryFile):
+ return self.name == other.name
+ return NotImplemented
+
+ def __hash__(self) -> int:
+ return hash(self.name)
+
+ @property
+ def abitag(self) -> str:
+ abitag = ''
+ abitag += 'S' if self.static else '-'
+ abitag += 'M' if self.mt else '-'
+ abitag += ' '
+ abitag += 's' if self.runtime_static else '-'
+ abitag += 'g' if self.runtime_debug else '-'
+ abitag += 'y' if self.python_debug else '-'
+ abitag += 'd' if self.debug else '-'
+ abitag += 'p' if self.stlport else '-'
+ abitag += 'n' if self.deprecated_iostreams else '-'
+ abitag += ' ' + (self.arch or '???')
+ abitag += ' ' + (self.toolset or '?')
+ abitag += ' ' + (self.version_lib or 'x_xx')
+ return abitag
+
+ def is_boost(self) -> bool:
+ return any(self.name.startswith(x) for x in ['libboost_', 'boost_'])
+
+ def is_python_lib(self) -> bool:
+ return any(self.mod_name.startswith(x) for x in BoostLibraryFile.boost_python_libs)
+
+ def fix_python_name(self, tags: T.List[str]) -> T.List[str]:
+ # Handle the boost_python naming madeness.
+ # See https://github.com/mesonbuild/meson/issues/4788 for some distro
+ # specific naming variations.
+ other_tags = [] # type: T.List[str]
+
+ # Split the current modname into the base name and the version
+ m_cur = BoostLibraryFile.reg_python_mod_split.match(self.mod_name)
+ cur_name = m_cur.group(1)
+ cur_vers = m_cur.group(2)
+
+ # Update the current version string if the new version string is longer
+ def update_vers(new_vers: str) -> None:
+ nonlocal cur_vers
+ new_vers = new_vers.replace('_', '')
+ new_vers = new_vers.replace('.', '')
+ if not new_vers.isdigit():
+ return
+ if len(new_vers) > len(cur_vers):
+ cur_vers = new_vers
+
+ for i in tags:
+ if i.startswith('py'):
+ update_vers(i[2:])
+ elif i.isdigit():
+ update_vers(i)
+ elif len(i) >= 3 and i[0].isdigit and i[2].isdigit() and i[1] == '.':
+ update_vers(i)
+ else:
+ other_tags += [i]
+
+ self.mod_name = cur_name + cur_vers
+ return other_tags
+
+ def mod_name_matches(self, mod_name: str) -> bool:
+ if self.mod_name == mod_name:
+ return True
+ if not self.is_python_lib():
+ return False
+
+ m_cur = BoostLibraryFile.reg_python_mod_split.match(self.mod_name)
+ m_arg = BoostLibraryFile.reg_python_mod_split.match(mod_name)
+
+ if not m_cur or not m_arg:
+ return False
+
+ if m_cur.group(1) != m_arg.group(1):
+ return False
+
+ cur_vers = m_cur.group(2)
+ arg_vers = m_arg.group(2)
+
+ # Always assume python 2 if nothing is specified
+ if not arg_vers:
+ arg_vers = '2'
+
+ return cur_vers.startswith(arg_vers)
+
+ def version_matches(self, version_lib: str) -> bool:
+ # If no version tag is present, assume that it fits
+ if not self.version_lib or not version_lib:
+ return True
+ return self.version_lib == version_lib
+
+ def arch_matches(self, arch: str) -> bool:
+ # If no version tag is present, assume that it fits
+ if not self.arch or not arch:
+ return True
+ return self.arch == arch
+
+ def vscrt_matches(self, vscrt: str) -> bool:
+ # If no vscrt tag present, assume that it fits ['/MD', '/MDd', '/MT', '/MTd']
+ if not vscrt:
+ return True
+ if vscrt in {'/MD', '-MD'}:
+ return not self.runtime_static and not self.runtime_debug
+ elif vscrt in {'/MDd', '-MDd'}:
+ return not self.runtime_static and self.runtime_debug
+ elif vscrt in {'/MT', '-MT'}:
+ return (self.runtime_static or not self.static) and not self.runtime_debug
+ elif vscrt in {'/MTd', '-MTd'}:
+ return (self.runtime_static or not self.static) and self.runtime_debug
+
+ mlog.warning(f'Boost: unknown vscrt tag {vscrt}. This may cause the compilation to fail. Please consider reporting this as a bug.', once=True)
+ return True
+
+ def get_compiler_args(self) -> T.List[str]:
+ args = [] # type: T.List[str]
+ if self.mod_name in boost_libraries:
+ libdef = boost_libraries[self.mod_name] # type: BoostLibrary
+ if self.static:
+ args += libdef.static
+ else:
+ args += libdef.shared
+ if self.mt:
+ args += libdef.multi
+ else:
+ args += libdef.single
+ return args
+
+ def get_link_args(self) -> T.List[str]:
+ return [self.path.as_posix()]
+
+class BoostDependency(SystemDependency):
+ def __init__(self, environment: Environment, kwargs: T.Dict[str, T.Any]) -> None:
+ super().__init__('boost', environment, kwargs, language='cpp')
+ buildtype = environment.coredata.get_option(mesonlib.OptionKey('buildtype'))
+ assert isinstance(buildtype, str)
+ self.debug = buildtype.startswith('debug')
+ self.multithreading = kwargs.get('threading', 'multi') == 'multi'
+
+ self.boost_root = None # type: T.Optional[Path]
+ self.explicit_static = 'static' in kwargs
+
+ # Extract and validate modules
+ self.modules = mesonlib.extract_as_list(kwargs, 'modules') # type: T.List[str]
+ for i in self.modules:
+ if not isinstance(i, str):
+ raise DependencyException('Boost module argument is not a string.')
+ if i.startswith('boost_'):
+ raise DependencyException('Boost modules must be passed without the boost_ prefix')
+
+ self.modules_found = [] # type: T.List[str]
+ self.modules_missing = [] # type: T.List[str]
+
+ # Do we need threads?
+ if 'thread' in self.modules:
+ if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})):
+ self.is_found = False
+ return
+
+ # Try figuring out the architecture tag
+ self.arch = environment.machines[self.for_machine].cpu_family
+ self.arch = boost_arch_map.get(self.arch, None)
+
+ # First, look for paths specified in a machine file
+ props = self.env.properties[self.for_machine]
+ if any(x in self.env.properties[self.for_machine] for x in
+ ['boost_includedir', 'boost_librarydir', 'boost_root']):
+ self.detect_boost_machine_file(props)
+ return
+
+ # Finally, look for paths from .pc files and from searching the filesystem
+ self.detect_roots()
+
+ def check_and_set_roots(self, roots: T.List[Path], use_system: bool) -> None:
+ roots = list(mesonlib.OrderedSet(roots))
+ for j in roots:
+ # 1. Look for the boost headers (boost/version.hpp)
+ mlog.debug(f'Checking potential boost root {j.as_posix()}')
+ inc_dirs = self.detect_inc_dirs(j)
+ inc_dirs = sorted(inc_dirs, reverse=True) # Prefer the newer versions
+
+ # Early abort when boost is not found
+ if not inc_dirs:
+ continue
+
+ lib_dirs = self.detect_lib_dirs(j, use_system)
+ self.is_found = self.run_check(inc_dirs, lib_dirs)
+ if self.is_found:
+ self.boost_root = j
+ break
+
+ def detect_boost_machine_file(self, props: 'Properties') -> None:
+ """Detect boost with values in the machine file or environment.
+
+ The machine file values are defaulted to the environment values.
+ """
+ # XXX: if we had a TypedDict we wouldn't need this
+ incdir = props.get('boost_includedir')
+ assert incdir is None or isinstance(incdir, str)
+ libdir = props.get('boost_librarydir')
+ assert libdir is None or isinstance(libdir, str)
+
+ if incdir and libdir:
+ inc_dir = Path(incdir)
+ lib_dir = Path(libdir)
+
+ if not inc_dir.is_absolute() or not lib_dir.is_absolute():
+ raise DependencyException('Paths given for boost_includedir and boost_librarydir in machine file must be absolute')
+
+ mlog.debug('Trying to find boost with:')
+ mlog.debug(f' - boost_includedir = {inc_dir}')
+ mlog.debug(f' - boost_librarydir = {lib_dir}')
+
+ return self.detect_split_root(inc_dir, lib_dir)
+
+ elif incdir or libdir:
+ raise DependencyException('Both boost_includedir *and* boost_librarydir have to be set in your machine file (one is not enough)')
+
+ rootdir = props.get('boost_root')
+ # It shouldn't be possible to get here without something in boost_root
+ assert rootdir
+
+ raw_paths = mesonlib.stringlistify(rootdir)
+ paths = [Path(x) for x in raw_paths]
+ if paths and any(not x.is_absolute() for x in paths):
+ raise DependencyException('boost_root path given in machine file must be absolute')
+
+ self.check_and_set_roots(paths, use_system=False)
+
+ def run_check(self, inc_dirs: T.List[BoostIncludeDir], lib_dirs: T.List[Path]) -> bool:
+ mlog.debug(' - potential library dirs: {}'.format([x.as_posix() for x in lib_dirs]))
+ mlog.debug(' - potential include dirs: {}'.format([x.path.as_posix() for x in inc_dirs]))
+
+ # 2. Find all boost libraries
+ libs = [] # type: T.List[BoostLibraryFile]
+ for i in lib_dirs:
+ libs = self.detect_libraries(i)
+ if libs:
+ mlog.debug(f' - found boost library dir: {i}')
+ # mlog.debug(' - raw library list:')
+ # for j in libs:
+ # mlog.debug(' - {}'.format(j))
+ break
+ libs = sorted(set(libs))
+
+ modules = ['boost_' + x for x in self.modules]
+ for inc in inc_dirs:
+ mlog.debug(f' - found boost {inc.version} include dir: {inc.path}')
+ f_libs = self.filter_libraries(libs, inc.version_lib)
+
+ mlog.debug(' - filtered library list:')
+ for j in f_libs:
+ mlog.debug(f' - {j}')
+
+ # 3. Select the libraries matching the requested modules
+ not_found = [] # type: T.List[str]
+ selected_modules = [] # type: T.List[BoostLibraryFile]
+ for mod in modules:
+ found = False
+ for l in f_libs:
+ if l.mod_name_matches(mod):
+ selected_modules += [l]
+ found = True
+ break
+ if not found:
+ not_found += [mod]
+
+ # log the result
+ mlog.debug(' - found:')
+ comp_args = [] # type: T.List[str]
+ link_args = [] # type: T.List[str]
+ for j in selected_modules:
+ c_args = j.get_compiler_args()
+ l_args = j.get_link_args()
+ mlog.debug(' - {:<24} link={} comp={}'.format(j.mod_name, str(l_args), str(c_args)))
+ comp_args += c_args
+ link_args += l_args
+
+ comp_args = list(mesonlib.OrderedSet(comp_args))
+ link_args = list(mesonlib.OrderedSet(link_args))
+
+ self.modules_found = [x.mod_name for x in selected_modules]
+ self.modules_found = [x[6:] for x in self.modules_found]
+ self.modules_found = sorted(set(self.modules_found))
+ self.modules_missing = not_found
+ self.modules_missing = [x[6:] for x in self.modules_missing]
+ self.modules_missing = sorted(set(self.modules_missing))
+
+ # if we found all modules we are done
+ if not not_found:
+ self.version = inc.version
+ self.compile_args = ['-I' + inc.path.as_posix()]
+ self.compile_args += comp_args
+ self.compile_args += self._extra_compile_args()
+ self.compile_args = list(mesonlib.OrderedSet(self.compile_args))
+ self.link_args = link_args
+ mlog.debug(f' - final compile args: {self.compile_args}')
+ mlog.debug(f' - final link args: {self.link_args}')
+ return True
+
+ # in case we missed something log it and try again
+ mlog.debug(' - NOT found:')
+ for mod in not_found:
+ mlog.debug(f' - {mod}')
+
+ return False
+
+ def detect_inc_dirs(self, root: Path) -> T.List[BoostIncludeDir]:
+ candidates = [] # type: T.List[Path]
+ inc_root = root / 'include'
+
+ candidates += [root / 'boost']
+ candidates += [inc_root / 'boost']
+ if inc_root.is_dir():
+ for i in inc_root.iterdir():
+ if not i.is_dir() or not i.name.startswith('boost-'):
+ continue
+ candidates += [i / 'boost']
+ candidates = [x for x in candidates if x.is_dir()]
+ candidates = [x / 'version.hpp' for x in candidates]
+ candidates = [x for x in candidates if x.exists()]
+ return [self._include_dir_from_version_header(x) for x in candidates]
+
+ def detect_lib_dirs(self, root: Path, use_system: bool) -> T.List[Path]:
+ # First check the system include paths. Only consider those within the
+ # given root path
+
+ if use_system:
+ system_dirs_t = self.clib_compiler.get_library_dirs(self.env)
+ system_dirs = [Path(x) for x in system_dirs_t]
+ system_dirs = [x.resolve() for x in system_dirs if x.exists()]
+ system_dirs = [x for x in system_dirs if mesonlib.path_is_in_root(x, root)]
+ system_dirs = list(mesonlib.OrderedSet(system_dirs))
+
+ if system_dirs:
+ return system_dirs
+
+ # No system include paths were found --> fall back to manually looking
+ # for library dirs in root
+ dirs = [] # type: T.List[Path]
+ subdirs = [] # type: T.List[Path]
+ for i in root.iterdir():
+ if i.is_dir() and i.name.startswith('lib'):
+ dirs += [i]
+
+ # Some distros put libraries not directly inside /usr/lib but in /usr/lib/x86_64-linux-gnu
+ for i in dirs:
+ for j in i.iterdir():
+ if j.is_dir() and j.name.endswith('-linux-gnu'):
+ subdirs += [j]
+
+ # Filter out paths that don't match the target arch to avoid finding
+ # the wrong libraries. See https://github.com/mesonbuild/meson/issues/7110
+ if not self.arch:
+ return dirs + subdirs
+
+ arch_list_32 = ['32', 'i386']
+ arch_list_64 = ['64']
+
+ raw_list = dirs + subdirs
+ no_arch = [x for x in raw_list if not any(y in x.name for y in arch_list_32 + arch_list_64)]
+
+ matching_arch = [] # type: T.List[Path]
+ if '32' in self.arch:
+ matching_arch = [x for x in raw_list if any(y in x.name for y in arch_list_32)]
+ elif '64' in self.arch:
+ matching_arch = [x for x in raw_list if any(y in x.name for y in arch_list_64)]
+
+ return sorted(matching_arch) + sorted(no_arch)
+
+ def filter_libraries(self, libs: T.List[BoostLibraryFile], lib_vers: str) -> T.List[BoostLibraryFile]:
+ # MSVC is very picky with the library tags
+ vscrt = ''
+ try:
+ crt_val = self.env.coredata.options[mesonlib.OptionKey('b_vscrt')].value
+ buildtype = self.env.coredata.options[mesonlib.OptionKey('buildtype')].value
+ vscrt = self.clib_compiler.get_crt_compile_args(crt_val, buildtype)[0]
+ except (KeyError, IndexError, AttributeError):
+ pass
+
+ # mlog.debug(' - static: {}'.format(self.static))
+ # mlog.debug(' - not explicit static: {}'.format(not self.explicit_static))
+ # mlog.debug(' - mt: {}'.format(self.multithreading))
+ # mlog.debug(' - version: {}'.format(lib_vers))
+ # mlog.debug(' - arch: {}'.format(self.arch))
+ # mlog.debug(' - vscrt: {}'.format(vscrt))
+ libs = [x for x in libs if x.static == self.static or not self.explicit_static]
+ libs = [x for x in libs if x.mt == self.multithreading]
+ libs = [x for x in libs if x.version_matches(lib_vers)]
+ libs = [x for x in libs if x.arch_matches(self.arch)]
+ libs = [x for x in libs if x.vscrt_matches(vscrt)]
+ libs = [x for x in libs if x.nvsuffix != 'dll'] # Only link to import libraries
+
+ # Only filter by debug when we are building in release mode. Debug
+ # libraries are automatically preferred through sorting otherwise.
+ if not self.debug:
+ libs = [x for x in libs if not x.debug]
+
+ # Take the abitag from the first library and filter by it. This
+ # ensures that we have a set of libraries that are always compatible.
+ if not libs:
+ return []
+ abitag = libs[0].abitag
+ libs = [x for x in libs if x.abitag == abitag]
+
+ return libs
+
+ def detect_libraries(self, libdir: Path) -> T.List[BoostLibraryFile]:
+ libs = set() # type: T.Set[BoostLibraryFile]
+ for i in libdir.iterdir():
+ if not i.is_file():
+ continue
+ if not any(i.name.startswith(x) for x in ['libboost_', 'boost_']):
+ continue
+ # Windows binaries from SourceForge ship with PDB files alongside
+ # DLLs (#8325). Ignore them.
+ if i.name.endswith('.pdb'):
+ continue
+
+ try:
+ libs.add(BoostLibraryFile(i.resolve()))
+ except UnknownFileException as e:
+ mlog.warning('Boost: ignoring unknown file {} under lib directory'.format(e.path.name))
+
+ return [x for x in libs if x.is_boost()] # Filter out no boost libraries
+
+ def detect_split_root(self, inc_dir: Path, lib_dir: Path) -> None:
+ boost_inc_dir = None
+ for j in [inc_dir / 'version.hpp', inc_dir / 'boost' / 'version.hpp']:
+ if j.is_file():
+ boost_inc_dir = self._include_dir_from_version_header(j)
+ break
+ if not boost_inc_dir:
+ self.is_found = False
+ return
+
+ self.is_found = self.run_check([boost_inc_dir], [lib_dir])
+
+ def detect_roots(self) -> None:
+ roots = [] # type: T.List[Path]
+
+ # Try getting the BOOST_ROOT from a boost.pc if it exists. This primarily
+ # allows BoostDependency to find boost from Conan. See #5438
+ try:
+ boost_pc = PkgConfigDependency('boost', self.env, {'required': False})
+ if boost_pc.found():
+ boost_root = boost_pc.get_pkgconfig_variable('prefix', [], None)
+ if boost_root:
+ roots += [Path(boost_root)]
+ except DependencyException:
+ pass
+
+ # Add roots from system paths
+ inc_paths = [Path(x) for x in self.clib_compiler.get_default_include_dirs()]
+ inc_paths = [x.parent for x in inc_paths if x.exists()]
+ inc_paths = [x.resolve() for x in inc_paths]
+ roots += inc_paths
+
+ # Add system paths
+ if self.env.machines[self.for_machine].is_windows():
+ # Where boost built from source actually installs it
+ c_root = Path('C:/Boost')
+ if c_root.is_dir():
+ roots += [c_root]
+
+ # Where boost documentation says it should be
+ prog_files = Path('C:/Program Files/boost')
+ # Where boost prebuilt binaries are
+ local_boost = Path('C:/local')
+
+ candidates = [] # type: T.List[Path]
+ if prog_files.is_dir():
+ candidates += [*prog_files.iterdir()]
+ if local_boost.is_dir():
+ candidates += [*local_boost.iterdir()]
+
+ roots += [x for x in candidates if x.name.lower().startswith('boost') and x.is_dir()]
+ else:
+ tmp = [] # type: T.List[Path]
+
+ # Add some default system paths
+ tmp += [Path('/opt/local')]
+ tmp += [Path('/usr/local/opt/boost')]
+ tmp += [Path('/usr/local')]
+ tmp += [Path('/usr')]
+
+ # Cleanup paths
+ tmp = [x for x in tmp if x.is_dir()]
+ tmp = [x.resolve() for x in tmp]
+ roots += tmp
+
+ self.check_and_set_roots(roots, use_system=True)
+
+ def log_details(self) -> str:
+ res = ''
+ if self.modules_found:
+ res += 'found: ' + ', '.join(self.modules_found)
+ if self.modules_missing:
+ if res:
+ res += ' | '
+ res += 'missing: ' + ', '.join(self.modules_missing)
+ return res
+
+ def log_info(self) -> str:
+ if self.boost_root:
+ return self.boost_root.as_posix()
+ return ''
+
+ def _include_dir_from_version_header(self, hfile: Path) -> BoostIncludeDir:
+ # Extract the version with a regex. Using clib_compiler.get_define would
+ # also work, however, this is slower (since it the compiler has to be
+ # invoked) and overkill since the layout of the header is always the same.
+ assert hfile.exists()
+ raw = hfile.read_text(encoding='utf-8')
+ m = re.search(r'#define\s+BOOST_VERSION\s+([0-9]+)', raw)
+ if not m:
+ mlog.debug(f'Failed to extract version information from {hfile}')
+ return BoostIncludeDir(hfile.parents[1], 0)
+ return BoostIncludeDir(hfile.parents[1], int(m.group(1)))
+
+ def _extra_compile_args(self) -> T.List[str]:
+ # BOOST_ALL_DYN_LINK should not be required with the known defines below
+ return ['-DBOOST_ALL_NO_LIB'] # Disable automatic linking
+
+
+# See https://www.boost.org/doc/libs/1_72_0/more/getting_started/unix-variants.html#library-naming
+# See https://mesonbuild.com/Reference-tables.html#cpu-families
+boost_arch_map = {
+ 'aarch64': 'a64',
+ 'arc': 'a32',
+ 'arm': 'a32',
+ 'ia64': 'i64',
+ 'mips': 'm32',
+ 'mips64': 'm64',
+ 'ppc': 'p32',
+ 'ppc64': 'p64',
+ 'sparc': 's32',
+ 'sparc64': 's64',
+ 'x86': 'x32',
+ 'x86_64': 'x64',
+}
+
+
+#### ---- BEGIN GENERATED ---- ####
+# #
+# Generated with tools/boost_names.py:
+# - boost version: 1.73.0
+# - modules found: 159
+# - libraries found: 43
+#
+
+class BoostLibrary():
+ def __init__(self, name: str, shared: T.List[str], static: T.List[str], single: T.List[str], multi: T.List[str]):
+ self.name = name
+ self.shared = shared
+ self.static = static
+ self.single = single
+ self.multi = multi
+
+class BoostModule():
+ def __init__(self, name: str, key: str, desc: str, libs: T.List[str]):
+ self.name = name
+ self.key = key
+ self.desc = desc
+ self.libs = libs
+
+
+# dict of all know libraries with additional compile options
+boost_libraries = {
+ 'boost_atomic': BoostLibrary(
+ name='boost_atomic',
+ shared=['-DBOOST_ATOMIC_DYN_LINK=1'],
+ static=['-DBOOST_ATOMIC_STATIC_LINK=1'],
+ single=[],
+ multi=[],
+ ),
+ 'boost_chrono': BoostLibrary(
+ name='boost_chrono',
+ shared=['-DBOOST_CHRONO_DYN_LINK=1'],
+ static=['-DBOOST_CHRONO_STATIC_LINK=1'],
+ single=['-DBOOST_CHRONO_THREAD_DISABLED'],
+ multi=[],
+ ),
+ 'boost_container': BoostLibrary(
+ name='boost_container',
+ shared=['-DBOOST_CONTAINER_DYN_LINK=1'],
+ static=['-DBOOST_CONTAINER_STATIC_LINK=1'],
+ single=[],
+ multi=[],
+ ),
+ 'boost_context': BoostLibrary(
+ name='boost_context',
+ shared=['-DBOOST_CONTEXT_DYN_LINK=1'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_contract': BoostLibrary(
+ name='boost_contract',
+ shared=['-DBOOST_CONTRACT_DYN_LINK'],
+ static=['-DBOOST_CONTRACT_STATIC_LINK'],
+ single=['-DBOOST_CONTRACT_DISABLE_THREADS'],
+ multi=[],
+ ),
+ 'boost_coroutine': BoostLibrary(
+ name='boost_coroutine',
+ shared=['-DBOOST_COROUTINES_DYN_LINK=1'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_date_time': BoostLibrary(
+ name='boost_date_time',
+ shared=['-DBOOST_DATE_TIME_DYN_LINK=1'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_exception': BoostLibrary(
+ name='boost_exception',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_fiber': BoostLibrary(
+ name='boost_fiber',
+ shared=['-DBOOST_FIBERS_DYN_LINK=1'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_fiber_numa': BoostLibrary(
+ name='boost_fiber_numa',
+ shared=['-DBOOST_FIBERS_DYN_LINK=1'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_filesystem': BoostLibrary(
+ name='boost_filesystem',
+ shared=['-DBOOST_FILESYSTEM_DYN_LINK=1'],
+ static=['-DBOOST_FILESYSTEM_STATIC_LINK=1'],
+ single=[],
+ multi=[],
+ ),
+ 'boost_graph': BoostLibrary(
+ name='boost_graph',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_iostreams': BoostLibrary(
+ name='boost_iostreams',
+ shared=['-DBOOST_IOSTREAMS_DYN_LINK=1'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_locale': BoostLibrary(
+ name='boost_locale',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_log': BoostLibrary(
+ name='boost_log',
+ shared=['-DBOOST_LOG_DYN_LINK=1'],
+ static=[],
+ single=['-DBOOST_LOG_NO_THREADS'],
+ multi=[],
+ ),
+ 'boost_log_setup': BoostLibrary(
+ name='boost_log_setup',
+ shared=['-DBOOST_LOG_SETUP_DYN_LINK=1'],
+ static=[],
+ single=['-DBOOST_LOG_NO_THREADS'],
+ multi=[],
+ ),
+ 'boost_math_c99': BoostLibrary(
+ name='boost_math_c99',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_math_c99f': BoostLibrary(
+ name='boost_math_c99f',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_math_c99l': BoostLibrary(
+ name='boost_math_c99l',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_math_tr1': BoostLibrary(
+ name='boost_math_tr1',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_math_tr1f': BoostLibrary(
+ name='boost_math_tr1f',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_math_tr1l': BoostLibrary(
+ name='boost_math_tr1l',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_mpi': BoostLibrary(
+ name='boost_mpi',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_nowide': BoostLibrary(
+ name='boost_nowide',
+ shared=['-DBOOST_NOWIDE_DYN_LINK=1'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_prg_exec_monitor': BoostLibrary(
+ name='boost_prg_exec_monitor',
+ shared=['-DBOOST_TEST_DYN_LINK=1'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_program_options': BoostLibrary(
+ name='boost_program_options',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_random': BoostLibrary(
+ name='boost_random',
+ shared=['-DBOOST_RANDOM_DYN_LINK'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_regex': BoostLibrary(
+ name='boost_regex',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_serialization': BoostLibrary(
+ name='boost_serialization',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_stacktrace_addr2line': BoostLibrary(
+ name='boost_stacktrace_addr2line',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_stacktrace_backtrace': BoostLibrary(
+ name='boost_stacktrace_backtrace',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_stacktrace_basic': BoostLibrary(
+ name='boost_stacktrace_basic',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_stacktrace_noop': BoostLibrary(
+ name='boost_stacktrace_noop',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_stacktrace_windbg': BoostLibrary(
+ name='boost_stacktrace_windbg',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_stacktrace_windbg_cached': BoostLibrary(
+ name='boost_stacktrace_windbg_cached',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_system': BoostLibrary(
+ name='boost_system',
+ shared=['-DBOOST_SYSTEM_DYN_LINK=1'],
+ static=['-DBOOST_SYSTEM_STATIC_LINK=1'],
+ single=[],
+ multi=[],
+ ),
+ 'boost_test_exec_monitor': BoostLibrary(
+ name='boost_test_exec_monitor',
+ shared=['-DBOOST_TEST_DYN_LINK=1'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_thread': BoostLibrary(
+ name='boost_thread',
+ shared=['-DBOOST_THREAD_BUILD_DLL=1', '-DBOOST_THREAD_USE_DLL=1'],
+ static=['-DBOOST_THREAD_BUILD_LIB=1', '-DBOOST_THREAD_USE_LIB=1'],
+ single=[],
+ multi=[],
+ ),
+ 'boost_timer': BoostLibrary(
+ name='boost_timer',
+ shared=['-DBOOST_TIMER_DYN_LINK=1'],
+ static=['-DBOOST_TIMER_STATIC_LINK=1'],
+ single=[],
+ multi=[],
+ ),
+ 'boost_type_erasure': BoostLibrary(
+ name='boost_type_erasure',
+ shared=['-DBOOST_TYPE_ERASURE_DYN_LINK'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_unit_test_framework': BoostLibrary(
+ name='boost_unit_test_framework',
+ shared=['-DBOOST_TEST_DYN_LINK=1'],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_wave': BoostLibrary(
+ name='boost_wave',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+ 'boost_wserialization': BoostLibrary(
+ name='boost_wserialization',
+ shared=[],
+ static=[],
+ single=[],
+ multi=[],
+ ),
+}
+
+# #
+#### ---- END GENERATED ---- ####
diff --git a/mesonbuild/dependencies/cmake.py b/mesonbuild/dependencies/cmake.py
new file mode 100644
index 0000000..abd31a1
--- /dev/null
+++ b/mesonbuild/dependencies/cmake.py
@@ -0,0 +1,653 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+from .base import ExternalDependency, DependencyException, DependencyTypeName
+from ..mesonlib import is_windows, MesonException, PerMachine, stringlistify, extract_as_list
+from ..cmake import CMakeExecutor, CMakeTraceParser, CMakeException, CMakeToolchain, CMakeExecScope, check_cmake_args, resolve_cmake_trace_targets, cmake_is_debug
+from .. import mlog
+import importlib.resources
+from pathlib import Path
+import functools
+import re
+import os
+import shutil
+import textwrap
+import typing as T
+
+if T.TYPE_CHECKING:
+ from ..cmake import CMakeTarget
+ from ..environment import Environment
+ from ..envconfig import MachineInfo
+
+class CMakeInfo(T.NamedTuple):
+ module_paths: T.List[str]
+ cmake_root: str
+ archs: T.List[str]
+ common_paths: T.List[str]
+
+class CMakeDependency(ExternalDependency):
+ # The class's copy of the CMake path. Avoids having to search for it
+ # multiple times in the same Meson invocation.
+ class_cmakeinfo: PerMachine[T.Optional[CMakeInfo]] = PerMachine(None, None)
+ # Version string for the minimum CMake version
+ class_cmake_version = '>=3.4'
+ # CMake generators to try (empty for no generator)
+ class_cmake_generators = ['', 'Ninja', 'Unix Makefiles', 'Visual Studio 10 2010']
+ class_working_generator: T.Optional[str] = None
+
+ def _gen_exception(self, msg: str) -> DependencyException:
+ return DependencyException(f'Dependency {self.name} not found: {msg}')
+
+ def _main_cmake_file(self) -> str:
+ return 'CMakeLists.txt'
+
+ def _extra_cmake_opts(self) -> T.List[str]:
+ return []
+
+ def _map_module_list(self, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]]) -> T.List[T.Tuple[str, bool]]:
+ # Map the input module list to something else
+ # This function will only be executed AFTER the initial CMake
+ # interpreter pass has completed. Thus variables defined in the
+ # CMakeLists.txt can be accessed here.
+ #
+ # Both the modules and components inputs contain the original lists.
+ return modules
+
+ def _map_component_list(self, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]]) -> T.List[T.Tuple[str, bool]]:
+ # Map the input components list to something else. This
+ # function will be executed BEFORE the initial CMake interpreter
+ # pass. Thus variables from the CMakeLists.txt can NOT be accessed.
+ #
+ # Both the modules and components inputs contain the original lists.
+ return components
+
+ def _original_module_name(self, module: str) -> str:
+ # Reverse the module mapping done by _map_module_list for
+ # one module
+ return module
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None, force_use_global_compilers: bool = False) -> None:
+ # Gather a list of all languages to support
+ self.language_list = [] # type: T.List[str]
+ if language is None or force_use_global_compilers:
+ compilers = None
+ if kwargs.get('native', False):
+ compilers = environment.coredata.compilers.build
+ else:
+ compilers = environment.coredata.compilers.host
+
+ candidates = ['c', 'cpp', 'fortran', 'objc', 'objcxx']
+ self.language_list += [x for x in candidates if x in compilers]
+ else:
+ self.language_list += [language]
+
+ # Add additional languages if required
+ if 'fortran' in self.language_list:
+ self.language_list += ['c']
+
+ # Ensure that the list is unique
+ self.language_list = list(set(self.language_list))
+
+ super().__init__(DependencyTypeName('cmake'), environment, kwargs, language=language)
+ self.name = name
+ self.is_libtool = False
+ # Store a copy of the CMake path on the object itself so it is
+ # stored in the pickled coredata and recovered.
+ self.cmakebin: T.Optional[CMakeExecutor] = None
+ self.cmakeinfo: T.Optional[CMakeInfo] = None
+
+ # Where all CMake "build dirs" are located
+ self.cmake_root_dir = environment.scratch_dir
+
+ # T.List of successfully found modules
+ self.found_modules: T.List[str] = []
+
+ # Initialize with None before the first return to avoid
+ # AttributeError exceptions in derived classes
+ self.traceparser: T.Optional[CMakeTraceParser] = None
+
+ # TODO further evaluate always using MachineChoice.BUILD
+ self.cmakebin = CMakeExecutor(environment, CMakeDependency.class_cmake_version, self.for_machine, silent=self.silent)
+ if not self.cmakebin.found():
+ self.cmakebin = None
+ msg = f'CMake binary for machine {self.for_machine} not found. Giving up.'
+ if self.required:
+ raise DependencyException(msg)
+ mlog.debug(msg)
+ return
+
+ # Setup the trace parser
+ self.traceparser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir(), self.env)
+
+ cm_args = stringlistify(extract_as_list(kwargs, 'cmake_args'))
+ cm_args = check_cmake_args(cm_args)
+ if CMakeDependency.class_cmakeinfo[self.for_machine] is None:
+ CMakeDependency.class_cmakeinfo[self.for_machine] = self._get_cmake_info(cm_args)
+ self.cmakeinfo = CMakeDependency.class_cmakeinfo[self.for_machine]
+ if self.cmakeinfo is None:
+ raise self._gen_exception('Unable to obtain CMake system information')
+
+ package_version = kwargs.get('cmake_package_version', '')
+ if not isinstance(package_version, str):
+ raise DependencyException('Keyword "cmake_package_version" must be a string.')
+ components = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'components'))]
+ modules = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'modules'))]
+ modules += [(x, False) for x in stringlistify(extract_as_list(kwargs, 'optional_modules'))]
+ cm_path = stringlistify(extract_as_list(kwargs, 'cmake_module_path'))
+ cm_path = [x if os.path.isabs(x) else os.path.join(environment.get_source_dir(), x) for x in cm_path]
+ if cm_path:
+ cm_args.append('-DCMAKE_MODULE_PATH=' + ';'.join(cm_path))
+ if not self._preliminary_find_check(name, cm_path, self.cmakebin.get_cmake_prefix_paths(), environment.machines[self.for_machine]):
+ mlog.debug('Preliminary CMake check failed. Aborting.')
+ return
+ self._detect_dep(name, package_version, modules, components, cm_args)
+
+ def __repr__(self) -> str:
+ return f'<{self.__class__.__name__} {self.name}: {self.is_found} {self.version_reqs}>'
+
+ def _get_cmake_info(self, cm_args: T.List[str]) -> T.Optional[CMakeInfo]:
+ mlog.debug("Extracting basic cmake information")
+
+ # Try different CMake generators since specifying no generator may fail
+ # in cygwin for some reason
+ gen_list = []
+ # First try the last working generator
+ if CMakeDependency.class_working_generator is not None:
+ gen_list += [CMakeDependency.class_working_generator]
+ gen_list += CMakeDependency.class_cmake_generators
+
+ temp_parser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir(), self.env)
+ toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir())
+ toolchain.write()
+
+ for i in gen_list:
+ mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto'))
+
+ # Prepare options
+ cmake_opts = temp_parser.trace_args() + toolchain.get_cmake_args() + ['.']
+ cmake_opts += cm_args
+ if len(i) > 0:
+ cmake_opts = ['-G', i] + cmake_opts
+
+ # Run CMake
+ ret1, out1, err1 = self._call_cmake(cmake_opts, 'CMakePathInfo.txt')
+
+ # Current generator was successful
+ if ret1 == 0:
+ CMakeDependency.class_working_generator = i
+ break
+
+ mlog.debug(f'CMake failed to gather system information for generator {i} with error code {ret1}')
+ mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n')
+
+ # Check if any generator succeeded
+ if ret1 != 0:
+ return None
+
+ try:
+ temp_parser.parse(err1)
+ except MesonException:
+ return None
+
+ def process_paths(l: T.List[str]) -> T.Set[str]:
+ if is_windows():
+ # Cannot split on ':' on Windows because its in the drive letter
+ tmp = [x.split(os.pathsep) for x in l]
+ else:
+ # https://github.com/mesonbuild/meson/issues/7294
+ tmp = [re.split(r':|;', x) for x in l]
+ flattened = [x for sublist in tmp for x in sublist]
+ return set(flattened)
+
+ # Extract the variables and sanity check them
+ root_paths_set = process_paths(temp_parser.get_cmake_var('MESON_FIND_ROOT_PATH'))
+ root_paths_set.update(process_paths(temp_parser.get_cmake_var('MESON_CMAKE_SYSROOT')))
+ root_paths = sorted(root_paths_set)
+ root_paths = [x for x in root_paths if os.path.isdir(x)]
+ module_paths_set = process_paths(temp_parser.get_cmake_var('MESON_PATHS_LIST'))
+ rooted_paths: T.List[str] = []
+ for j in [Path(x) for x in root_paths]:
+ for p in [Path(x) for x in module_paths_set]:
+ rooted_paths.append(str(j / p.relative_to(p.anchor)))
+ module_paths = sorted(module_paths_set.union(rooted_paths))
+ module_paths = [x for x in module_paths if os.path.isdir(x)]
+ archs = temp_parser.get_cmake_var('MESON_ARCH_LIST')
+
+ common_paths = ['lib', 'lib32', 'lib64', 'libx32', 'share']
+ for i in archs:
+ common_paths += [os.path.join('lib', i)]
+
+ res = CMakeInfo(
+ module_paths=module_paths,
+ cmake_root=temp_parser.get_cmake_var('MESON_CMAKE_ROOT')[0],
+ archs=archs,
+ common_paths=common_paths,
+ )
+
+ mlog.debug(f' -- Module search paths: {res.module_paths}')
+ mlog.debug(f' -- CMake root: {res.cmake_root}')
+ mlog.debug(f' -- CMake architectures: {res.archs}')
+ mlog.debug(f' -- CMake lib search paths: {res.common_paths}')
+
+ return res
+
+ @staticmethod
+ @functools.lru_cache(maxsize=None)
+ def _cached_listdir(path: str) -> T.Tuple[T.Tuple[str, str], ...]:
+ try:
+ return tuple((x, str(x).lower()) for x in os.listdir(path))
+ except OSError:
+ return tuple()
+
+ @staticmethod
+ @functools.lru_cache(maxsize=None)
+ def _cached_isdir(path: str) -> bool:
+ try:
+ return os.path.isdir(path)
+ except OSError:
+ return False
+
+ def _preliminary_find_check(self, name: str, module_path: T.List[str], prefix_path: T.List[str], machine: 'MachineInfo') -> bool:
+ lname = str(name).lower()
+
+ # Checks <path>, <path>/cmake, <path>/CMake
+ def find_module(path: str) -> bool:
+ for i in [path, os.path.join(path, 'cmake'), os.path.join(path, 'CMake')]:
+ if not self._cached_isdir(i):
+ continue
+
+ # Check the directory case insensitive
+ content = self._cached_listdir(i)
+ candidates = ['Find{}.cmake', '{}Config.cmake', '{}-config.cmake']
+ candidates = [x.format(name).lower() for x in candidates]
+ if any(x[1] in candidates for x in content):
+ return True
+ return False
+
+ # Search in <path>/(lib/<arch>|lib*|share) for cmake files
+ def search_lib_dirs(path: str) -> bool:
+ for i in [os.path.join(path, x) for x in self.cmakeinfo.common_paths]:
+ if not self._cached_isdir(i):
+ continue
+
+ # Check <path>/(lib/<arch>|lib*|share)/cmake/<name>*/
+ cm_dir = os.path.join(i, 'cmake')
+ if self._cached_isdir(cm_dir):
+ content = self._cached_listdir(cm_dir)
+ content = tuple(x for x in content if x[1].startswith(lname))
+ for k in content:
+ if find_module(os.path.join(cm_dir, k[0])):
+ return True
+
+ # <path>/(lib/<arch>|lib*|share)/<name>*/
+ # <path>/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/
+ content = self._cached_listdir(i)
+ content = tuple(x for x in content if x[1].startswith(lname))
+ for k in content:
+ if find_module(os.path.join(i, k[0])):
+ return True
+
+ return False
+
+ # Check the user provided and system module paths
+ for i in module_path + [os.path.join(self.cmakeinfo.cmake_root, 'Modules')]:
+ if find_module(i):
+ return True
+
+ # Check the user provided prefix paths
+ for i in prefix_path:
+ if search_lib_dirs(i):
+ return True
+
+ # Check PATH
+ system_env = [] # type: T.List[str]
+ for i in os.environ.get('PATH', '').split(os.pathsep):
+ if i.endswith('/bin') or i.endswith('\\bin'):
+ i = i[:-4]
+ if i.endswith('/sbin') or i.endswith('\\sbin'):
+ i = i[:-5]
+ system_env += [i]
+
+ # Check the system paths
+ for i in self.cmakeinfo.module_paths + system_env:
+ if find_module(i):
+ return True
+
+ if search_lib_dirs(i):
+ return True
+
+ content = self._cached_listdir(i)
+ content = tuple(x for x in content if x[1].startswith(lname))
+ for k in content:
+ if search_lib_dirs(os.path.join(i, k[0])):
+ return True
+
+ # Mac framework support
+ if machine.is_darwin():
+ for j in [f'{lname}.framework', f'{lname}.app']:
+ for k in content:
+ if k[1] != j:
+ continue
+ if find_module(os.path.join(i, k[0], 'Resources')) or find_module(os.path.join(i, k[0], 'Version')):
+ return True
+
+ # Check the environment path
+ env_path = os.environ.get(f'{name}_DIR')
+ if env_path and find_module(env_path):
+ return True
+
+ # Check the Linux CMake registry
+ linux_reg = Path.home() / '.cmake' / 'packages'
+ for p in [linux_reg / name, linux_reg / lname]:
+ if p.exists():
+ return True
+
+ return False
+
+ def _detect_dep(self, name: str, package_version: str, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]], args: T.List[str]) -> None:
+ # Detect a dependency with CMake using the '--find-package' mode
+ # and the trace output (stderr)
+ #
+ # When the trace output is enabled CMake prints all functions with
+ # parameters to stderr as they are executed. Since CMake 3.4.0
+ # variables ("${VAR}") are also replaced in the trace output.
+ mlog.debug('\nDetermining dependency {!r} with CMake executable '
+ '{!r}'.format(name, self.cmakebin.executable_path()))
+
+ # Try different CMake generators since specifying no generator may fail
+ # in cygwin for some reason
+ gen_list = []
+ # First try the last working generator
+ if CMakeDependency.class_working_generator is not None:
+ gen_list += [CMakeDependency.class_working_generator]
+ gen_list += CMakeDependency.class_cmake_generators
+
+ # Map the components
+ comp_mapped = self._map_component_list(modules, components)
+ toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir())
+ toolchain.write()
+
+ for i in gen_list:
+ mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto'))
+
+ # Prepare options
+ cmake_opts = []
+ cmake_opts += [f'-DNAME={name}']
+ cmake_opts += ['-DARCHS={}'.format(';'.join(self.cmakeinfo.archs))]
+ cmake_opts += [f'-DVERSION={package_version}']
+ cmake_opts += ['-DCOMPS={}'.format(';'.join([x[0] for x in comp_mapped]))]
+ cmake_opts += args
+ cmake_opts += self.traceparser.trace_args()
+ cmake_opts += toolchain.get_cmake_args()
+ cmake_opts += self._extra_cmake_opts()
+ cmake_opts += ['.']
+ if len(i) > 0:
+ cmake_opts = ['-G', i] + cmake_opts
+
+ # Run CMake
+ ret1, out1, err1 = self._call_cmake(cmake_opts, self._main_cmake_file())
+
+ # Current generator was successful
+ if ret1 == 0:
+ CMakeDependency.class_working_generator = i
+ break
+
+ mlog.debug(f'CMake failed for generator {i} and package {name} with error code {ret1}')
+ mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n')
+
+ # Check if any generator succeeded
+ if ret1 != 0:
+ return
+
+ try:
+ self.traceparser.parse(err1)
+ except CMakeException as e:
+ e2 = self._gen_exception(str(e))
+ if self.required:
+ raise
+ else:
+ self.compile_args = []
+ self.link_args = []
+ self.is_found = False
+ self.reason = e2
+ return
+
+ # Whether the package is found or not is always stored in PACKAGE_FOUND
+ self.is_found = self.traceparser.var_to_bool('PACKAGE_FOUND')
+ if not self.is_found:
+ return
+
+ # Try to detect the version
+ vers_raw = self.traceparser.get_cmake_var('PACKAGE_VERSION')
+
+ if len(vers_raw) > 0:
+ self.version = vers_raw[0]
+ self.version.strip('"\' ')
+
+ # Post-process module list. Used in derived classes to modify the
+ # module list (append prepend a string, etc.).
+ modules = self._map_module_list(modules, components)
+ autodetected_module_list = False
+
+ # Try guessing a CMake target if none is provided
+ if len(modules) == 0:
+ for i in self.traceparser.targets:
+ tg = i.lower()
+ lname = name.lower()
+ if f'{lname}::{lname}' == tg or lname == tg.replace('::', ''):
+ mlog.debug(f'Guessed CMake target \'{i}\'')
+ modules = [(i, True)]
+ autodetected_module_list = True
+ break
+
+ # Failed to guess a target --> try the old-style method
+ if len(modules) == 0:
+ # Warn when there might be matching imported targets but no automatic match was used
+ partial_modules: T.List[CMakeTarget] = []
+ for k, v in self.traceparser.targets.items():
+ tg = k.lower()
+ lname = name.lower()
+ if tg.startswith(f'{lname}::'):
+ partial_modules += [v]
+ if partial_modules:
+ mlog.warning(textwrap.dedent(f'''\
+ Could not find and exact match for the CMake dependency {name}.
+
+ However, Meson found the following partial matches:
+
+ {[x.name for x in partial_modules]}
+
+ Using imported is recommended, since this approach is less error prone
+ and better supported by Meson. Consider explicitly specifying one of
+ these in the dependency call with:
+
+ dependency('{name}', modules: ['{name}::<name>', ...])
+
+ Meson will now continue to use the old-style {name}_LIBRARIES CMake
+ variables to extract the dependency information since no explicit
+ target is currently specified.
+
+ '''))
+ mlog.debug('More info for the partial match targets:')
+ for tgt in partial_modules:
+ mlog.debug(tgt)
+
+ incDirs = [x for x in self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS') if x]
+ defs = [x for x in self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS') if x]
+ libs_raw = [x for x in self.traceparser.get_cmake_var('PACKAGE_LIBRARIES') if x]
+
+ # CMake has a "fun" API, where certain keywords describing
+ # configurations can be in the *_LIBRARIES vraiables. See:
+ # - https://github.com/mesonbuild/meson/issues/9197
+ # - https://gitlab.freedesktop.org/libnice/libnice/-/issues/140
+ # - https://cmake.org/cmake/help/latest/command/target_link_libraries.html#overview (the last point in the section)
+ libs: T.List[str] = []
+ cfg_matches = True
+ is_debug = cmake_is_debug(self.env)
+ cm_tag_map = {'debug': is_debug, 'optimized': not is_debug, 'general': True}
+ for i in libs_raw:
+ if i.lower() in cm_tag_map:
+ cfg_matches = cm_tag_map[i.lower()]
+ continue
+ if cfg_matches:
+ libs += [i]
+ # According to the CMake docs, a keyword only works for the
+ # directly the following item and all items without a keyword
+ # are implizitly `general`
+ cfg_matches = True
+
+ # Try to use old style variables if no module is specified
+ if len(libs) > 0:
+ self.compile_args = [f'-I{x}' for x in incDirs] + defs
+ self.link_args = []
+ for j in libs:
+ rtgt = resolve_cmake_trace_targets(j, self.traceparser, self.env, clib_compiler=self.clib_compiler)
+ self.link_args += rtgt.libraries
+ self.compile_args += [f'-I{x}' for x in rtgt.include_directories]
+ self.compile_args += rtgt.public_compile_opts
+ mlog.debug(f'using old-style CMake variables for dependency {name}')
+ mlog.debug(f'Include Dirs: {incDirs}')
+ mlog.debug(f'Compiler Definitions: {defs}')
+ mlog.debug(f'Libraries: {libs}')
+ return
+
+ # Even the old-style approach failed. Nothing else we can do here
+ self.is_found = False
+ raise self._gen_exception('CMake: failed to guess a CMake target for {}.\n'
+ 'Try to explicitly specify one or more targets with the "modules" property.\n'
+ 'Valid targets are:\n{}'.format(name, list(self.traceparser.targets.keys())))
+
+ # Set dependencies with CMake targets
+ # recognise arguments we should pass directly to the linker
+ incDirs = []
+ compileOptions = []
+ libraries = []
+
+ for i, required in modules:
+ if i not in self.traceparser.targets:
+ if not required:
+ mlog.warning('CMake: T.Optional module', mlog.bold(self._original_module_name(i)), 'for', mlog.bold(name), 'was not found')
+ continue
+ raise self._gen_exception('CMake: invalid module {} for {}.\n'
+ 'Try to explicitly specify one or more targets with the "modules" property.\n'
+ 'Valid targets are:\n{}'.format(self._original_module_name(i), name, list(self.traceparser.targets.keys())))
+
+ if not autodetected_module_list:
+ self.found_modules += [i]
+
+ rtgt = resolve_cmake_trace_targets(i, self.traceparser, self.env,
+ clib_compiler=self.clib_compiler,
+ not_found_warning=lambda x:
+ mlog.warning('CMake: Dependency', mlog.bold(x), 'for', mlog.bold(name), 'was not found')
+ )
+ incDirs += rtgt.include_directories
+ compileOptions += rtgt.public_compile_opts
+ libraries += rtgt.libraries + rtgt.link_flags
+
+ # Make sure all elements in the lists are unique and sorted
+ incDirs = sorted(set(incDirs))
+ compileOptions = sorted(set(compileOptions))
+ libraries = sorted(set(libraries))
+
+ mlog.debug(f'Include Dirs: {incDirs}')
+ mlog.debug(f'Compiler Options: {compileOptions}')
+ mlog.debug(f'Libraries: {libraries}')
+
+ self.compile_args = compileOptions + [f'-I{x}' for x in incDirs]
+ self.link_args = libraries
+
+ def _get_build_dir(self) -> Path:
+ build_dir = Path(self.cmake_root_dir) / f'cmake_{self.name}'
+ build_dir.mkdir(parents=True, exist_ok=True)
+ return build_dir
+
+ def _setup_cmake_dir(self, cmake_file: str) -> Path:
+ # Setup the CMake build environment and return the "build" directory
+ build_dir = self._get_build_dir()
+
+ # Remove old CMake cache so we can try out multiple generators
+ cmake_cache = build_dir / 'CMakeCache.txt'
+ cmake_files = build_dir / 'CMakeFiles'
+ if cmake_cache.exists():
+ cmake_cache.unlink()
+ shutil.rmtree(cmake_files.as_posix(), ignore_errors=True)
+
+ # Insert language parameters into the CMakeLists.txt and write new CMakeLists.txt
+ cmake_txt = importlib.resources.read_text('mesonbuild.dependencies.data', cmake_file, encoding = 'utf-8')
+
+ # In general, some Fortran CMake find_package() also require C language enabled,
+ # even if nothing from C is directly used. An easy Fortran example that fails
+ # without C language is
+ # find_package(Threads)
+ # To make this general to
+ # any other language that might need this, we use a list for all
+ # languages and expand in the cmake Project(... LANGUAGES ...) statement.
+ from ..cmake import language_map
+ cmake_language = [language_map[x] for x in self.language_list if x in language_map]
+ if not cmake_language:
+ cmake_language += ['NONE']
+
+ cmake_txt = textwrap.dedent("""
+ cmake_minimum_required(VERSION ${{CMAKE_VERSION}})
+ project(MesonTemp LANGUAGES {})
+ """).format(' '.join(cmake_language)) + cmake_txt
+
+ cm_file = build_dir / 'CMakeLists.txt'
+ cm_file.write_text(cmake_txt, encoding='utf-8')
+ mlog.cmd_ci_include(cm_file.absolute().as_posix())
+
+ return build_dir
+
+ def _call_cmake(self,
+ args: T.List[str],
+ cmake_file: str,
+ env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, T.Optional[str], T.Optional[str]]:
+ build_dir = self._setup_cmake_dir(cmake_file)
+ return self.cmakebin.call(args, build_dir, env=env)
+
+ @staticmethod
+ def log_tried() -> str:
+ return 'cmake'
+
+ def log_details(self) -> str:
+ modules = [self._original_module_name(x) for x in self.found_modules]
+ modules = sorted(set(modules))
+ if modules:
+ return 'modules: ' + ', '.join(modules)
+ return ''
+
+ def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
+ configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
+ default_value: T.Optional[str] = None,
+ pkgconfig_define: T.Optional[T.List[str]] = None) -> str:
+ if cmake and self.traceparser is not None:
+ try:
+ v = self.traceparser.vars[cmake]
+ except KeyError:
+ pass
+ else:
+ # CMake does NOT have a list datatype. We have no idea whether
+ # anything is a string or a string-separated-by-; Internally,
+ # we treat them as the latter and represent everything as a
+ # list, because it is convenient when we are mostly handling
+ # imported targets, which have various properties that are
+ # actually lists.
+ #
+ # As a result we need to convert them back to strings when grabbing
+ # raw variables the user requested.
+ return ';'.join(v)
+ if default_value is not None:
+ return default_value
+ raise DependencyException(f'Could not get cmake variable and no default provided for {self!r}')
diff --git a/mesonbuild/dependencies/coarrays.py b/mesonbuild/dependencies/coarrays.py
new file mode 100644
index 0000000..70cf4f8
--- /dev/null
+++ b/mesonbuild/dependencies/coarrays.py
@@ -0,0 +1,87 @@
+# Copyright 2013-2019 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.
+from __future__ import annotations
+
+import functools
+import typing as T
+
+from .base import DependencyMethods, detect_compiler, SystemDependency
+from .cmake import CMakeDependency
+from .pkgconfig import PkgConfigDependency
+from .factory import factory_methods
+
+if T.TYPE_CHECKING:
+ from . factory import DependencyGenerator
+ from ..environment import Environment, MachineChoice
+
+
+@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE, DependencyMethods.SYSTEM})
+def coarray_factory(env: 'Environment',
+ for_machine: 'MachineChoice',
+ kwargs: T.Dict[str, T.Any],
+ methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
+ fcid = detect_compiler('coarray', env, for_machine, 'fortran').get_id()
+ candidates: T.List['DependencyGenerator'] = []
+
+ if fcid == 'gcc':
+ # OpenCoarrays is the most commonly used method for Fortran Coarray with GCC
+ if DependencyMethods.PKGCONFIG in methods:
+ for pkg in ['caf-openmpi', 'caf']:
+ candidates.append(functools.partial(
+ PkgConfigDependency, pkg, env, kwargs, language='fortran'))
+
+ if DependencyMethods.CMAKE in methods:
+ if 'modules' not in kwargs:
+ kwargs['modules'] = 'OpenCoarrays::caf_mpi'
+ candidates.append(functools.partial(
+ CMakeDependency, 'OpenCoarrays', env, kwargs, language='fortran'))
+
+ if DependencyMethods.SYSTEM in methods:
+ candidates.append(functools.partial(CoarrayDependency, env, kwargs))
+
+ return candidates
+
+
+class CoarrayDependency(SystemDependency):
+ """
+ Coarrays are a Fortran 2008 feature.
+
+ Coarrays are sometimes implemented via external library (GCC+OpenCoarrays),
+ while other compilers just build in support (Cray, IBM, Intel, NAG).
+ Coarrays may be thought of as a high-level language abstraction of
+ low-level MPI calls.
+ """
+ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ super().__init__('coarray', environment, kwargs, language='fortran')
+ kwargs['required'] = False
+ kwargs['silent'] = True
+
+ cid = self.get_compiler().get_id()
+ if cid == 'gcc':
+ # Fallback to single image
+ self.compile_args = ['-fcoarray=single']
+ self.version = 'single image (fallback)'
+ self.is_found = True
+ elif cid == 'intel':
+ # Coarrays are built into Intel compilers, no external library needed
+ self.is_found = True
+ self.link_args = ['-coarray=shared']
+ self.compile_args = self.link_args
+ elif cid == 'intel-cl':
+ # Coarrays are built into Intel compilers, no external library needed
+ self.is_found = True
+ self.compile_args = ['/Qcoarray:shared']
+ elif cid == 'nagfor':
+ # NAG doesn't require any special arguments for Coarray
+ self.is_found = True
diff --git a/mesonbuild/dependencies/configtool.py b/mesonbuild/dependencies/configtool.py
new file mode 100644
index 0000000..1f16a43
--- /dev/null
+++ b/mesonbuild/dependencies/configtool.py
@@ -0,0 +1,186 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+from .base import ExternalDependency, DependencyException, DependencyTypeName
+from ..mesonlib import listify, Popen_safe, split_args, version_compare, version_compare_many
+from ..programs import find_external_program
+from .. import mlog
+import re
+import typing as T
+
+from mesonbuild import mesonlib
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+
+class ConfigToolDependency(ExternalDependency):
+
+ """Class representing dependencies found using a config tool.
+
+ Takes the following extra keys in kwargs that it uses internally:
+ :tools List[str]: A list of tool names to use
+ :version_arg str: The argument to pass to the tool to get it's version
+ :skip_version str: The argument to pass to the tool to ignore its version
+ (if ``version_arg`` fails, but it may start accepting it in the future)
+ Because some tools are stupid and don't accept --version
+ :returncode_value int: The value of the correct returncode
+ Because some tools are stupid and don't return 0
+ """
+
+ tools: T.Optional[T.List[str]] = None
+ tool_name: T.Optional[str] = None
+ version_arg = '--version'
+ skip_version: T.Optional[str] = None
+ __strip_version = re.compile(r'^[0-9][0-9.]+')
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None):
+ super().__init__(DependencyTypeName('config-tool'), environment, kwargs, language=language)
+ self.name = name
+ # You may want to overwrite the class version in some cases
+ self.tools = listify(kwargs.get('tools', self.tools))
+ if not self.tool_name:
+ self.tool_name = self.tools[0]
+ if 'version_arg' in kwargs:
+ self.version_arg = kwargs['version_arg']
+
+ req_version_raw = kwargs.get('version', None)
+ if req_version_raw is not None:
+ req_version = mesonlib.stringlistify(req_version_raw)
+ else:
+ req_version = []
+ tool, version = self.find_config(req_version, kwargs.get('returncode_value', 0))
+ self.config = tool
+ self.is_found = self.report_config(version, req_version)
+ if not self.is_found:
+ self.config = None
+ return
+ self.version = version
+
+ def _sanitize_version(self, version: str) -> str:
+ """Remove any non-numeric, non-point version suffixes."""
+ m = self.__strip_version.match(version)
+ if m:
+ # Ensure that there isn't a trailing '.', such as an input like
+ # `1.2.3.git-1234`
+ return m.group(0).rstrip('.')
+ return version
+
+ def find_config(self, versions: T.List[str], returncode: int = 0) \
+ -> T.Tuple[T.Optional[T.List[str]], T.Optional[str]]:
+ """Helper method that searches for config tool binaries in PATH and
+ returns the one that best matches the given version requirements.
+ """
+ best_match: T.Tuple[T.Optional[T.List[str]], T.Optional[str]] = (None, None)
+ for potential_bin in find_external_program(
+ self.env, self.for_machine, self.tool_name,
+ self.tool_name, self.tools, allow_default_for_cross=False):
+ if not potential_bin.found():
+ continue
+ tool = potential_bin.get_command()
+ try:
+ p, out = Popen_safe(tool + [self.version_arg])[:2]
+ except (FileNotFoundError, PermissionError):
+ continue
+ if p.returncode != returncode:
+ if self.skip_version:
+ # maybe the executable is valid even if it doesn't support --version
+ p = Popen_safe(tool + [self.skip_version])[0]
+ if p.returncode != returncode:
+ continue
+ else:
+ continue
+
+ out = self._sanitize_version(out.strip())
+ # Some tools, like pcap-config don't supply a version, but also
+ # don't fail with --version, in that case just assume that there is
+ # only one version and return it.
+ if not out:
+ return (tool, None)
+ if versions:
+ is_found = version_compare_many(out, versions)[0]
+ # This allows returning a found version without a config tool,
+ # which is useful to inform the user that you found version x,
+ # but y was required.
+ if not is_found:
+ tool = None
+ if best_match[1]:
+ if version_compare(out, '> {}'.format(best_match[1])):
+ best_match = (tool, out)
+ else:
+ best_match = (tool, out)
+
+ return best_match
+
+ def report_config(self, version: T.Optional[str], req_version: T.List[str]) -> bool:
+ """Helper method to print messages about the tool."""
+
+ found_msg: T.List[T.Union[str, mlog.AnsiDecorator]] = [mlog.bold(self.tool_name), 'found:']
+
+ if self.config is None:
+ found_msg.append(mlog.red('NO'))
+ if version is not None and req_version:
+ found_msg.append(f'found {version!r} but need {req_version!r}')
+ elif req_version:
+ found_msg.append(f'need {req_version!r}')
+ else:
+ found_msg += [mlog.green('YES'), '({})'.format(' '.join(self.config)), version]
+
+ mlog.log(*found_msg)
+
+ return self.config is not None
+
+ def get_config_value(self, args: T.List[str], stage: str) -> T.List[str]:
+ p, out, err = Popen_safe(self.config + args)
+ if p.returncode != 0:
+ if self.required:
+ raise DependencyException(f'Could not generate {stage} for {self.name}.\n{err}')
+ return []
+ return split_args(out)
+
+ def get_configtool_variable(self, variable_name: str) -> str:
+ p, out, _ = Popen_safe(self.config + [f'--{variable_name}'])
+ if p.returncode != 0:
+ if self.required:
+ raise DependencyException(
+ 'Could not get variable "{}" for dependency {}'.format(
+ variable_name, self.name))
+ variable = out.strip()
+ mlog.debug(f'Got config-tool variable {variable_name} : {variable}')
+ return variable
+
+ @staticmethod
+ def log_tried() -> str:
+ return 'config-tool'
+
+ def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
+ configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
+ default_value: T.Optional[str] = None,
+ pkgconfig_define: T.Optional[T.List[str]] = None) -> str:
+ if configtool:
+ # In the not required case '' (empty string) will be returned if the
+ # variable is not found. Since '' is a valid value to return we
+ # set required to True here to force and error, and use the
+ # finally clause to ensure it's restored.
+ restore = self.required
+ self.required = True
+ try:
+ return self.get_configtool_variable(configtool)
+ except DependencyException:
+ pass
+ finally:
+ self.required = restore
+ if default_value is not None:
+ return default_value
+ raise DependencyException(f'Could not get config-tool variable and no default provided for {self!r}')
diff --git a/mesonbuild/dependencies/cuda.py b/mesonbuild/dependencies/cuda.py
new file mode 100644
index 0000000..89e562f
--- /dev/null
+++ b/mesonbuild/dependencies/cuda.py
@@ -0,0 +1,292 @@
+# Copyright 2013-2019 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.
+from __future__ import annotations
+
+import glob
+import re
+import os
+import typing as T
+from pathlib import Path
+
+from .. import mesonlib
+from .. import mlog
+from ..environment import detect_cpu_family
+from .base import DependencyException, SystemDependency
+
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+ from ..compilers import Compiler
+
+ TV_ResultTuple = T.Tuple[T.Optional[str], T.Optional[str], bool]
+
+class CudaDependency(SystemDependency):
+
+ supported_languages = ['cuda', 'cpp', 'c'] # see also _default_language
+
+ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ compilers = environment.coredata.compilers[self.get_for_machine_from_kwargs(kwargs)]
+ language = self._detect_language(compilers)
+ if language not in self.supported_languages:
+ raise DependencyException(f'Language \'{language}\' is not supported by the CUDA Toolkit. Supported languages are {self.supported_languages}.')
+
+ super().__init__('cuda', environment, kwargs, language=language)
+ self.lib_modules: T.Dict[str, T.List[str]] = {}
+ self.requested_modules = self.get_requested(kwargs)
+ if 'cudart' not in self.requested_modules:
+ self.requested_modules = ['cudart'] + self.requested_modules
+
+ (self.cuda_path, self.version, self.is_found) = self._detect_cuda_path_and_version()
+ if not self.is_found:
+ return
+
+ if not os.path.isabs(self.cuda_path):
+ raise DependencyException(f'CUDA Toolkit path must be absolute, got \'{self.cuda_path}\'.')
+
+ # nvcc already knows where to find the CUDA Toolkit, but if we're compiling
+ # a mixed C/C++/CUDA project, we still need to make the include dir searchable
+ if self.language != 'cuda' or len(compilers) > 1:
+ self.incdir = os.path.join(self.cuda_path, 'include')
+ self.compile_args += [f'-I{self.incdir}']
+
+ if self.language != 'cuda':
+ arch_libdir = self._detect_arch_libdir()
+ self.libdir = os.path.join(self.cuda_path, arch_libdir)
+ mlog.debug('CUDA library directory is', mlog.bold(self.libdir))
+ else:
+ self.libdir = None
+
+ self.is_found = self._find_requested_libraries()
+
+ @classmethod
+ def _detect_language(cls, compilers: T.Dict[str, 'Compiler']) -> str:
+ for lang in cls.supported_languages:
+ if lang in compilers:
+ return lang
+ return list(compilers.keys())[0]
+
+ def _detect_cuda_path_and_version(self) -> TV_ResultTuple:
+ self.env_var = self._default_path_env_var()
+ mlog.debug('Default path env var:', mlog.bold(self.env_var))
+
+ version_reqs = self.version_reqs
+ if self.language == 'cuda':
+ nvcc_version = self._strip_patch_version(self.get_compiler().version)
+ mlog.debug('nvcc version:', mlog.bold(nvcc_version))
+ if version_reqs:
+ # make sure nvcc version satisfies specified version requirements
+ (found_some, not_found, found) = mesonlib.version_compare_many(nvcc_version, version_reqs)
+ if not_found:
+ msg = f'The current nvcc version {nvcc_version} does not satisfy the specified CUDA Toolkit version requirements {version_reqs}.'
+ return self._report_dependency_error(msg, (None, None, False))
+
+ # use nvcc version to find a matching CUDA Toolkit
+ version_reqs = [f'={nvcc_version}']
+ else:
+ nvcc_version = None
+
+ paths = [(path, self._cuda_toolkit_version(path), default) for (path, default) in self._cuda_paths()]
+ if version_reqs:
+ return self._find_matching_toolkit(paths, version_reqs, nvcc_version)
+
+ defaults = [(path, version) for (path, version, default) in paths if default]
+ if defaults:
+ return (defaults[0][0], defaults[0][1], True)
+
+ platform_msg = 'set the CUDA_PATH environment variable' if self._is_windows() \
+ else 'set the CUDA_PATH environment variable/create the \'/usr/local/cuda\' symbolic link'
+ msg = f'Please specify the desired CUDA Toolkit version (e.g. dependency(\'cuda\', version : \'>=10.1\')) or {platform_msg} to point to the location of your desired version.'
+ return self._report_dependency_error(msg, (None, None, False))
+
+ def _find_matching_toolkit(self, paths: T.List[TV_ResultTuple], version_reqs: T.List[str], nvcc_version: T.Optional[str]) -> TV_ResultTuple:
+ # keep the default paths order intact, sort the rest in the descending order
+ # according to the toolkit version
+ part_func: T.Callable[[TV_ResultTuple], bool] = lambda t: not t[2]
+ defaults_it, rest_it = mesonlib.partition(part_func, paths)
+ defaults = list(defaults_it)
+ paths = defaults + sorted(rest_it, key=lambda t: mesonlib.Version(t[1]), reverse=True)
+ mlog.debug(f'Search paths: {paths}')
+
+ if nvcc_version and defaults:
+ default_src = f"the {self.env_var} environment variable" if self.env_var else "the \'/usr/local/cuda\' symbolic link"
+ nvcc_warning = 'The default CUDA Toolkit as designated by {} ({}) doesn\'t match the current nvcc version {} and will be ignored.'.format(default_src, os.path.realpath(defaults[0][0]), nvcc_version)
+ else:
+ nvcc_warning = None
+
+ for (path, version, default) in paths:
+ (found_some, not_found, found) = mesonlib.version_compare_many(version, version_reqs)
+ if not not_found:
+ if not default and nvcc_warning:
+ mlog.warning(nvcc_warning)
+ return (path, version, True)
+
+ if nvcc_warning:
+ mlog.warning(nvcc_warning)
+ return (None, None, False)
+
+ def _default_path_env_var(self) -> T.Optional[str]:
+ env_vars = ['CUDA_PATH'] if self._is_windows() else ['CUDA_PATH', 'CUDA_HOME', 'CUDA_ROOT']
+ env_vars = [var for var in env_vars if var in os.environ]
+ user_defaults = {os.environ[var] for var in env_vars}
+ if len(user_defaults) > 1:
+ mlog.warning('Environment variables {} point to conflicting toolkit locations ({}). Toolkit selection might produce unexpected results.'.format(', '.join(env_vars), ', '.join(user_defaults)))
+ return env_vars[0] if env_vars else None
+
+ def _cuda_paths(self) -> T.List[T.Tuple[str, bool]]:
+ return ([(os.environ[self.env_var], True)] if self.env_var else []) \
+ + (self._cuda_paths_win() if self._is_windows() else self._cuda_paths_nix())
+
+ def _cuda_paths_win(self) -> T.List[T.Tuple[str, bool]]:
+ env_vars = os.environ.keys()
+ return [(os.environ[var], False) for var in env_vars if var.startswith('CUDA_PATH_')]
+
+ def _cuda_paths_nix(self) -> T.List[T.Tuple[str, bool]]:
+ # include /usr/local/cuda default only if no env_var was found
+ pattern = '/usr/local/cuda-*' if self.env_var else '/usr/local/cuda*'
+ return [(path, os.path.basename(path) == 'cuda') for path in glob.iglob(pattern)]
+
+ toolkit_version_regex = re.compile(r'^CUDA Version\s+(.*)$')
+ path_version_win_regex = re.compile(r'^v(.*)$')
+ path_version_nix_regex = re.compile(r'^cuda-(.*)$')
+ cudart_version_regex = re.compile(r'#define\s+CUDART_VERSION\s+([0-9]+)')
+
+ def _cuda_toolkit_version(self, path: str) -> str:
+ version = self._read_toolkit_version_txt(path)
+ if version:
+ return version
+ version = self._read_cuda_runtime_api_version(path)
+ if version:
+ return version
+
+ mlog.debug('Falling back to extracting version from path')
+ path_version_regex = self.path_version_win_regex if self._is_windows() else self.path_version_nix_regex
+ try:
+ m = path_version_regex.match(os.path.basename(path))
+ if m:
+ return m.group(1)
+ else:
+ mlog.warning(f'Could not detect CUDA Toolkit version for {path}')
+ except Exception as e:
+ mlog.warning(f'Could not detect CUDA Toolkit version for {path}: {e!s}')
+
+ return '0.0'
+
+ def _read_cuda_runtime_api_version(self, path_str: str) -> T.Optional[str]:
+ path = Path(path_str)
+ for i in path.rglob('cuda_runtime_api.h'):
+ raw = i.read_text(encoding='utf-8')
+ m = self.cudart_version_regex.search(raw)
+ if not m:
+ continue
+ try:
+ vers_int = int(m.group(1))
+ except ValueError:
+ continue
+ # use // for floor instead of / which produces a float
+ major = vers_int // 1000 # type: int
+ minor = (vers_int - major * 1000) // 10 # type: int
+ return f'{major}.{minor}'
+ return None
+
+ def _read_toolkit_version_txt(self, path: str) -> T.Optional[str]:
+ # Read 'version.txt' at the root of the CUDA Toolkit directory to determine the toolkit version
+ version_file_path = os.path.join(path, 'version.txt')
+ try:
+ with open(version_file_path, encoding='utf-8') as version_file:
+ version_str = version_file.readline() # e.g. 'CUDA Version 10.1.168'
+ m = self.toolkit_version_regex.match(version_str)
+ if m:
+ return self._strip_patch_version(m.group(1))
+ except Exception as e:
+ mlog.debug(f'Could not read CUDA Toolkit\'s version file {version_file_path}: {e!s}')
+
+ return None
+
+ @classmethod
+ def _strip_patch_version(cls, version: str) -> str:
+ return '.'.join(version.split('.')[:2])
+
+ def _detect_arch_libdir(self) -> str:
+ arch = detect_cpu_family(self.env.coredata.compilers.host)
+ machine = self.env.machines[self.for_machine]
+ msg = '{} architecture is not supported in {} version of the CUDA Toolkit.'
+ if machine.is_windows():
+ libdirs = {'x86': 'Win32', 'x86_64': 'x64'}
+ if arch not in libdirs:
+ raise DependencyException(msg.format(arch, 'Windows'))
+ return os.path.join('lib', libdirs[arch])
+ elif machine.is_linux():
+ libdirs = {'x86_64': 'lib64', 'ppc64': 'lib', 'aarch64': 'lib64', 'loongarch64': 'lib64'}
+ if arch not in libdirs:
+ raise DependencyException(msg.format(arch, 'Linux'))
+ return libdirs[arch]
+ elif machine.is_darwin():
+ libdirs = {'x86_64': 'lib64'}
+ if arch not in libdirs:
+ raise DependencyException(msg.format(arch, 'macOS'))
+ return libdirs[arch]
+ else:
+ raise DependencyException('CUDA Toolkit: unsupported platform.')
+
+ def _find_requested_libraries(self) -> bool:
+ all_found = True
+
+ for module in self.requested_modules:
+ args = self.clib_compiler.find_library(module, self.env, [self.libdir] if self.libdir else [])
+ if args is None:
+ self._report_dependency_error(f'Couldn\'t find requested CUDA module \'{module}\'')
+ all_found = False
+ else:
+ mlog.debug(f'Link args for CUDA module \'{module}\' are {args}')
+ self.lib_modules[module] = args
+
+ return all_found
+
+ def _is_windows(self) -> bool:
+ return self.env.machines[self.for_machine].is_windows()
+
+ @T.overload
+ def _report_dependency_error(self, msg: str) -> None: ...
+
+ @T.overload
+ def _report_dependency_error(self, msg: str, ret_val: TV_ResultTuple) -> TV_ResultTuple: ... # noqa: F811
+
+ def _report_dependency_error(self, msg: str, ret_val: T.Optional[TV_ResultTuple] = None) -> T.Optional[TV_ResultTuple]: # noqa: F811
+ if self.required:
+ raise DependencyException(msg)
+
+ mlog.debug(msg)
+ return ret_val
+
+ def log_details(self) -> str:
+ module_str = ', '.join(self.requested_modules)
+ return 'modules: ' + module_str
+
+ def log_info(self) -> str:
+ return self.cuda_path if self.cuda_path else ''
+
+ def get_requested(self, kwargs: T.Dict[str, T.Any]) -> T.List[str]:
+ candidates = mesonlib.extract_as_list(kwargs, 'modules')
+ for c in candidates:
+ if not isinstance(c, str):
+ raise DependencyException('CUDA module argument is not a string.')
+ return candidates
+
+ def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]:
+ args: T.List[str] = []
+ if self.libdir:
+ args += self.clib_compiler.get_linker_search_args(self.libdir)
+ for lib in self.requested_modules:
+ args += self.lib_modules[lib]
+ return args
diff --git a/mesonbuild/dependencies/data/CMakeLists.txt b/mesonbuild/dependencies/data/CMakeLists.txt
new file mode 100644
index 0000000..acbf648
--- /dev/null
+++ b/mesonbuild/dependencies/data/CMakeLists.txt
@@ -0,0 +1,98 @@
+# fail noisily if attempt to use this file without setting:
+# cmake_minimum_required(VERSION ${CMAKE_VERSION})
+# project(... LANGUAGES ...)
+
+cmake_policy(SET CMP0000 NEW)
+
+set(PACKAGE_FOUND FALSE)
+set(_packageName "${NAME}")
+string(TOUPPER "${_packageName}" PACKAGE_NAME)
+
+while(TRUE)
+ if ("${VERSION}" STREQUAL "")
+ find_package("${NAME}" QUIET COMPONENTS ${COMPS})
+ else()
+ find_package("${NAME}" "${VERSION}" QUIET COMPONENTS ${COMPS})
+ endif()
+
+ # ARCHS has to be set via the CMD interface
+ if(${_packageName}_FOUND OR ${PACKAGE_NAME}_FOUND OR "${ARCHS}" STREQUAL "")
+ break()
+ endif()
+
+ list(GET ARCHS 0 CMAKE_LIBRARY_ARCHITECTURE)
+ list(REMOVE_AT ARCHS 0)
+endwhile()
+
+if(${_packageName}_FOUND OR ${PACKAGE_NAME}_FOUND)
+ set(PACKAGE_FOUND TRUE)
+
+ # Check the following variables:
+ # FOO_VERSION
+ # Foo_VERSION
+ # FOO_VERSION_STRING
+ # Foo_VERSION_STRING
+ if(NOT DEFINED PACKAGE_VERSION)
+ if(DEFINED ${_packageName}_VERSION)
+ set(PACKAGE_VERSION "${${_packageName}_VERSION}")
+ elseif(DEFINED ${PACKAGE_NAME}_VERSION)
+ set(PACKAGE_VERSION "${${PACKAGE_NAME}_VERSION}")
+ elseif(DEFINED ${_packageName}_VERSION_STRING)
+ set(PACKAGE_VERSION "${${_packageName}_VERSION_STRING}")
+ elseif(DEFINED ${PACKAGE_NAME}_VERSION_STRING)
+ set(PACKAGE_VERSION "${${PACKAGE_NAME}_VERSION_STRING}")
+ endif()
+ endif()
+
+ # Check the following variables:
+ # FOO_LIBRARIES
+ # Foo_LIBRARIES
+ # FOO_LIBS
+ # Foo_LIBS
+ set(libs)
+ if(DEFINED ${_packageName}_LIBRARIES)
+ set(libs ${_packageName}_LIBRARIES)
+ elseif(DEFINED ${PACKAGE_NAME}_LIBRARIES)
+ set(libs ${PACKAGE_NAME}_LIBRARIES)
+ elseif(DEFINED ${_packageName}_LIBS)
+ set(libs ${_packageName}_LIBS)
+ elseif(DEFINED ${PACKAGE_NAME}_LIBS)
+ set(libs ${PACKAGE_NAME}_LIBS)
+ endif()
+
+ # Check the following variables:
+ # FOO_INCLUDE_DIRS
+ # Foo_INCLUDE_DIRS
+ # FOO_INCLUDES
+ # Foo_INCLUDES
+ # FOO_INCLUDE_DIR
+ # Foo_INCLUDE_DIR
+ set(includes)
+ if(DEFINED ${_packageName}_INCLUDE_DIRS)
+ set(includes ${_packageName}_INCLUDE_DIRS)
+ elseif(DEFINED ${PACKAGE_NAME}_INCLUDE_DIRS)
+ set(includes ${PACKAGE_NAME}_INCLUDE_DIRS)
+ elseif(DEFINED ${_packageName}_INCLUDES)
+ set(includes ${_packageName}_INCLUDES)
+ elseif(DEFINED ${PACKAGE_NAME}_INCLUDES)
+ set(includes ${PACKAGE_NAME}_INCLUDES)
+ elseif(DEFINED ${_packageName}_INCLUDE_DIR)
+ set(includes ${_packageName}_INCLUDE_DIR)
+ elseif(DEFINED ${PACKAGE_NAME}_INCLUDE_DIR)
+ set(includes ${PACKAGE_NAME}_INCLUDE_DIR)
+ endif()
+
+ # Check the following variables:
+ # FOO_DEFINITIONS
+ # Foo_DEFINITIONS
+ set(definitions)
+ if(DEFINED ${_packageName}_DEFINITIONS)
+ set(definitions ${_packageName}_DEFINITIONS)
+ elseif(DEFINED ${PACKAGE_NAME}_DEFINITIONS)
+ set(definitions ${PACKAGE_NAME}_DEFINITIONS)
+ endif()
+
+ set(PACKAGE_INCLUDE_DIRS "${${includes}}")
+ set(PACKAGE_DEFINITIONS "${${definitions}}")
+ set(PACKAGE_LIBRARIES "${${libs}}")
+endif()
diff --git a/mesonbuild/dependencies/data/CMakeListsLLVM.txt b/mesonbuild/dependencies/data/CMakeListsLLVM.txt
new file mode 100644
index 0000000..da23189
--- /dev/null
+++ b/mesonbuild/dependencies/data/CMakeListsLLVM.txt
@@ -0,0 +1,94 @@
+
+set(PACKAGE_FOUND FALSE)
+
+while(TRUE)
+ find_package(LLVM REQUIRED CONFIG QUIET)
+
+ # ARCHS has to be set via the CMD interface
+ if(LLVM_FOUND OR "${ARCHS}" STREQUAL "")
+ break()
+ endif()
+
+ list(GET ARCHS 0 CMAKE_LIBRARY_ARCHITECTURE)
+ list(REMOVE_AT ARCHS 0)
+endwhile()
+
+if(LLVM_FOUND)
+ set(PACKAGE_FOUND TRUE)
+
+ foreach(mod IN LISTS LLVM_MESON_MODULES)
+ # Reset variables
+ set(out_mods)
+ set(real_mods)
+
+ # Generate a lower and upper case version
+ string(TOLOWER "${mod}" mod_L)
+ string(TOUPPER "${mod}" mod_U)
+
+ # Get the mapped components
+ llvm_map_components_to_libnames(out_mods ${mod} ${mod_L} ${mod_U})
+ list(SORT out_mods)
+ list(REMOVE_DUPLICATES out_mods)
+
+ # Make sure that the modules exist
+ foreach(i IN LISTS out_mods)
+ if(TARGET ${i})
+ list(APPEND real_mods ${i})
+ endif()
+ endforeach()
+
+ # Set the output variables
+ set(MESON_LLVM_TARGETS_${mod} ${real_mods})
+ foreach(i IN LISTS real_mods)
+ set(MESON_TARGET_TO_LLVM_${i} ${mod})
+ endforeach()
+ endforeach()
+
+ # Check the following variables:
+ # LLVM_PACKAGE_VERSION
+ # LLVM_VERSION
+ # LLVM_VERSION_STRING
+ if(NOT DEFINED PACKAGE_VERSION)
+ if(DEFINED LLVM_PACKAGE_VERSION)
+ set(PACKAGE_VERSION "${LLVM_PACKAGE_VERSION}")
+ elseif(DEFINED LLVM_VERSION)
+ set(PACKAGE_VERSION "${LLVM_VERSION}")
+ elseif(DEFINED LLVM_VERSION_STRING)
+ set(PACKAGE_VERSION "${LLVM_VERSION_STRING}")
+ endif()
+ endif()
+
+ # Check the following variables:
+ # LLVM_LIBRARIES
+ # LLVM_LIBS
+ set(libs)
+ if(DEFINED LLVM_LIBRARIES)
+ set(libs LLVM_LIBRARIES)
+ elseif(DEFINED LLVM_LIBS)
+ set(libs LLVM_LIBS)
+ endif()
+
+ # Check the following variables:
+ # LLVM_INCLUDE_DIRS
+ # LLVM_INCLUDES
+ # LLVM_INCLUDE_DIR
+ set(includes)
+ if(DEFINED LLVM_INCLUDE_DIRS)
+ set(includes LLVM_INCLUDE_DIRS)
+ elseif(DEFINED LLVM_INCLUDES)
+ set(includes LLVM_INCLUDES)
+ elseif(DEFINED LLVM_INCLUDE_DIR)
+ set(includes LLVM_INCLUDE_DIR)
+ endif()
+
+ # Check the following variables:
+ # LLVM_DEFINITIONS
+ set(definitions)
+ if(DEFINED LLVM_DEFINITIONS)
+ set(definitions LLVM_DEFINITIONS)
+ endif()
+
+ set(PACKAGE_INCLUDE_DIRS "${${includes}}")
+ set(PACKAGE_DEFINITIONS "${${definitions}}")
+ set(PACKAGE_LIBRARIES "${${libs}}")
+endif()
diff --git a/mesonbuild/dependencies/data/CMakePathInfo.txt b/mesonbuild/dependencies/data/CMakePathInfo.txt
new file mode 100644
index 0000000..662ec58
--- /dev/null
+++ b/mesonbuild/dependencies/data/CMakePathInfo.txt
@@ -0,0 +1,31 @@
+cmake_minimum_required(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}.${CMAKE_PATCH_VERSION})
+
+set(TMP_PATHS_LIST)
+list(APPEND TMP_PATHS_LIST ${CMAKE_PREFIX_PATH})
+list(APPEND TMP_PATHS_LIST ${CMAKE_FRAMEWORK_PATH})
+list(APPEND TMP_PATHS_LIST ${CMAKE_APPBUNDLE_PATH})
+list(APPEND TMP_PATHS_LIST $ENV{CMAKE_PREFIX_PATH})
+list(APPEND TMP_PATHS_LIST $ENV{CMAKE_FRAMEWORK_PATH})
+list(APPEND TMP_PATHS_LIST $ENV{CMAKE_APPBUNDLE_PATH})
+list(APPEND TMP_PATHS_LIST ${CMAKE_SYSTEM_PREFIX_PATH})
+list(APPEND TMP_PATHS_LIST ${CMAKE_SYSTEM_FRAMEWORK_PATH})
+list(APPEND TMP_PATHS_LIST ${CMAKE_SYSTEM_APPBUNDLE_PATH})
+
+set(LIB_ARCH_LIST)
+if(CMAKE_LIBRARY_ARCHITECTURE_REGEX)
+ file(GLOB implicit_dirs RELATIVE /lib /lib/*-linux-gnu* )
+ foreach(dir ${implicit_dirs})
+ if("${dir}" MATCHES "${CMAKE_LIBRARY_ARCHITECTURE_REGEX}")
+ list(APPEND LIB_ARCH_LIST "${dir}")
+ endif()
+ endforeach()
+endif()
+
+# "Export" these variables:
+set(MESON_ARCH_LIST ${LIB_ARCH_LIST})
+set(MESON_PATHS_LIST ${TMP_PATHS_LIST})
+set(MESON_CMAKE_ROOT ${CMAKE_ROOT})
+set(MESON_CMAKE_SYSROOT ${CMAKE_SYSROOT})
+set(MESON_FIND_ROOT_PATH ${CMAKE_FIND_ROOT_PATH})
+
+message(STATUS ${TMP_PATHS_LIST})
diff --git a/mesonbuild/dependencies/data/__init__.py b/mesonbuild/dependencies/data/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/mesonbuild/dependencies/data/__init__.py
diff --git a/mesonbuild/dependencies/detect.py b/mesonbuild/dependencies/detect.py
new file mode 100644
index 0000000..4c7a477
--- /dev/null
+++ b/mesonbuild/dependencies/detect.py
@@ -0,0 +1,227 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+from .base import ExternalDependency, DependencyException, DependencyMethods, NotFoundDependency
+from .cmake import CMakeDependency
+from .dub import DubDependency
+from .framework import ExtraFrameworkDependency
+from .pkgconfig import PkgConfigDependency
+
+from ..mesonlib import listify, MachineChoice, PerMachine
+from .. import mlog
+import functools
+import typing as T
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+ from .factory import DependencyFactory, WrappedFactoryFunc, DependencyGenerator
+
+ TV_DepIDEntry = T.Union[str, bool, int, T.Tuple[str, ...]]
+ TV_DepID = T.Tuple[T.Tuple[str, TV_DepIDEntry], ...]
+
+# These must be defined in this file to avoid cyclical references.
+packages: T.Dict[
+ str,
+ T.Union[T.Type[ExternalDependency], 'DependencyFactory', 'WrappedFactoryFunc']
+] = {}
+_packages_accept_language: T.Set[str] = set()
+
+def get_dep_identifier(name: str, kwargs: T.Dict[str, T.Any]) -> 'TV_DepID':
+ identifier: 'TV_DepID' = (('name', name), )
+ from ..interpreter import permitted_dependency_kwargs
+ assert len(permitted_dependency_kwargs) == 19, \
+ 'Extra kwargs have been added to dependency(), please review if it makes sense to handle it here'
+ for key, value in kwargs.items():
+ # 'version' is irrelevant for caching; the caller must check version matches
+ # 'native' is handled above with `for_machine`
+ # 'required' is irrelevant for caching; the caller handles it separately
+ # 'fallback' and 'allow_fallback' is not part of the cache because,
+ # once a dependency has been found through a fallback, it should
+ # be used for the rest of the Meson run.
+ # 'default_options' is only used in fallback case
+ # 'not_found_message' has no impact on the dependency lookup
+ # 'include_type' is handled after the dependency lookup
+ if key in {'version', 'native', 'required', 'fallback', 'allow_fallback', 'default_options',
+ 'not_found_message', 'include_type'}:
+ continue
+ # All keyword arguments are strings, ints, or lists (or lists of lists)
+ if isinstance(value, list):
+ for i in value:
+ assert isinstance(i, str)
+ value = tuple(frozenset(listify(value)))
+ else:
+ assert isinstance(value, (str, bool, int))
+ identifier = (*identifier, (key, value),)
+ return identifier
+
+display_name_map = {
+ 'boost': 'Boost',
+ 'cuda': 'CUDA',
+ 'dub': 'DUB',
+ 'gmock': 'GMock',
+ 'gtest': 'GTest',
+ 'hdf5': 'HDF5',
+ 'llvm': 'LLVM',
+ 'mpi': 'MPI',
+ 'netcdf': 'NetCDF',
+ 'openmp': 'OpenMP',
+ 'wxwidgets': 'WxWidgets',
+}
+
+def find_external_dependency(name: str, env: 'Environment', kwargs: T.Dict[str, object]) -> T.Union['ExternalDependency', NotFoundDependency]:
+ assert name
+ required = kwargs.get('required', True)
+ if not isinstance(required, bool):
+ raise DependencyException('Keyword "required" must be a boolean.')
+ if not isinstance(kwargs.get('method', ''), str):
+ raise DependencyException('Keyword "method" must be a string.')
+ lname = name.lower()
+ if lname not in _packages_accept_language and 'language' in kwargs:
+ raise DependencyException(f'{name} dependency does not accept "language" keyword argument')
+ if not isinstance(kwargs.get('version', ''), (str, list)):
+ raise DependencyException('Keyword "Version" must be string or list.')
+
+ # display the dependency name with correct casing
+ display_name = display_name_map.get(lname, lname)
+
+ for_machine = MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST
+
+ type_text = PerMachine('Build-time', 'Run-time')[for_machine] + ' dependency'
+
+ # build a list of dependency methods to try
+ candidates = _build_external_dependency_list(name, env, for_machine, kwargs)
+
+ pkg_exc: T.List[DependencyException] = []
+ pkgdep: T.List[ExternalDependency] = []
+ details = ''
+
+ for c in candidates:
+ # try this dependency method
+ try:
+ d = c()
+ d._check_version()
+ pkgdep.append(d)
+ except DependencyException as e:
+ assert isinstance(c, functools.partial), 'for mypy'
+ bettermsg = f'Dependency lookup for {name} with method {c.func.log_tried()!r} failed: {e}'
+ mlog.debug(bettermsg)
+ e.args = (bettermsg,)
+ pkg_exc.append(e)
+ else:
+ pkg_exc.append(None)
+ details = d.log_details()
+ if details:
+ details = '(' + details + ') '
+ if 'language' in kwargs:
+ details += 'for ' + d.language + ' '
+
+ # if the dependency was found
+ if d.found():
+
+ info: mlog.TV_LoggableList = []
+ if d.version:
+ info.append(mlog.normal_cyan(d.version))
+
+ log_info = d.log_info()
+ if log_info:
+ info.append('(' + log_info + ')')
+
+ mlog.log(type_text, mlog.bold(display_name), details + 'found:', mlog.green('YES'), *info)
+
+ return d
+
+ # otherwise, the dependency could not be found
+ tried_methods = [d.log_tried() for d in pkgdep if d.log_tried()]
+ if tried_methods:
+ tried = mlog.format_list(tried_methods)
+ else:
+ tried = ''
+
+ mlog.log(type_text, mlog.bold(display_name), details + 'found:', mlog.red('NO'),
+ f'(tried {tried})' if tried else '')
+
+ if required:
+ # if an exception occurred with the first detection method, re-raise it
+ # (on the grounds that it came from the preferred dependency detection
+ # method)
+ if pkg_exc and pkg_exc[0]:
+ raise pkg_exc[0]
+
+ # we have a list of failed ExternalDependency objects, so we can report
+ # the methods we tried to find the dependency
+ raise DependencyException(f'Dependency "{name}" not found' +
+ (f', tried {tried}' if tried else ''))
+
+ return NotFoundDependency(name, env)
+
+
+def _build_external_dependency_list(name: str, env: 'Environment', for_machine: MachineChoice,
+ kwargs: T.Dict[str, T.Any]) -> T.List['DependencyGenerator']:
+ # First check if the method is valid
+ if 'method' in kwargs and kwargs['method'] not in [e.value for e in DependencyMethods]:
+ raise DependencyException('method {!r} is invalid'.format(kwargs['method']))
+
+ # Is there a specific dependency detector for this dependency?
+ lname = name.lower()
+ if lname in packages:
+ # Create the list of dependency object constructors using a factory
+ # class method, if one exists, otherwise the list just consists of the
+ # constructor
+ if isinstance(packages[lname], type):
+ entry1 = T.cast('T.Type[ExternalDependency]', packages[lname]) # mypy doesn't understand isinstance(..., type)
+ if issubclass(entry1, ExternalDependency):
+ func: T.Callable[[], 'ExternalDependency'] = functools.partial(entry1, env, kwargs)
+ dep = [func]
+ else:
+ entry2 = T.cast('T.Union[DependencyFactory, WrappedFactoryFunc]', packages[lname])
+ dep = entry2(env, for_machine, kwargs)
+ return dep
+
+ candidates: T.List['DependencyGenerator'] = []
+
+ # If it's explicitly requested, use the dub detection method (only)
+ if 'dub' == kwargs.get('method', ''):
+ candidates.append(functools.partial(DubDependency, name, env, kwargs))
+ return candidates
+
+ # If it's explicitly requested, use the pkgconfig detection method (only)
+ if 'pkg-config' == kwargs.get('method', ''):
+ candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs))
+ return candidates
+
+ # If it's explicitly requested, use the CMake detection method (only)
+ if 'cmake' == kwargs.get('method', ''):
+ candidates.append(functools.partial(CMakeDependency, name, env, kwargs))
+ return candidates
+
+ # If it's explicitly requested, use the Extraframework detection method (only)
+ if 'extraframework' == kwargs.get('method', ''):
+ # On OSX, also try framework dependency detector
+ if env.machines[for_machine].is_darwin():
+ candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs))
+ return candidates
+
+ # Otherwise, just use the pkgconfig and cmake dependency detector
+ if 'auto' == kwargs.get('method', 'auto'):
+ candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs))
+
+ # On OSX, also try framework dependency detector
+ if env.machines[for_machine].is_darwin():
+ candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs))
+
+ # Only use CMake as a last resort, since it might not work 100% (see #6113)
+ candidates.append(functools.partial(CMakeDependency, name, env, kwargs))
+
+ return candidates
diff --git a/mesonbuild/dependencies/dev.py b/mesonbuild/dependencies/dev.py
new file mode 100644
index 0000000..cc02842
--- /dev/null
+++ b/mesonbuild/dependencies/dev.py
@@ -0,0 +1,679 @@
+# Copyright 2013-2019 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.
+
+# This file contains the detection logic for external dependencies useful for
+# development purposes, such as testing, debugging, etc..
+
+from __future__ import annotations
+
+import glob
+import os
+import re
+import pathlib
+import shutil
+import subprocess
+import typing as T
+
+from mesonbuild.interpreterbase.decorators import FeatureDeprecated
+
+from .. import mesonlib, mlog
+from ..environment import get_llvm_tool_names
+from ..mesonlib import version_compare, stringlistify, extract_as_list
+from .base import DependencyException, DependencyMethods, detect_compiler, strip_system_libdirs, SystemDependency, ExternalDependency, DependencyTypeName
+from .cmake import CMakeDependency
+from .configtool import ConfigToolDependency
+from .factory import DependencyFactory
+from .misc import threads_factory
+from .pkgconfig import PkgConfigDependency
+
+if T.TYPE_CHECKING:
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..mesonlib import MachineChoice
+ from typing_extensions import TypedDict
+
+ class JNISystemDependencyKW(TypedDict):
+ modules: T.List[str]
+ # FIXME: When dependency() moves to typed Kwargs, this should inherit
+ # from its TypedDict type.
+ version: T.Optional[str]
+
+
+def get_shared_library_suffix(environment: 'Environment', for_machine: MachineChoice) -> str:
+ """This is only guaranteed to work for languages that compile to machine
+ code, not for languages like C# that use a bytecode and always end in .dll
+ """
+ m = environment.machines[for_machine]
+ if m.is_windows():
+ return '.dll'
+ elif m.is_darwin():
+ return '.dylib'
+ return '.so'
+
+
+class GTestDependencySystem(SystemDependency):
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ super().__init__(name, environment, kwargs, language='cpp')
+ self.main = kwargs.get('main', False)
+ self.src_dirs = ['/usr/src/gtest/src', '/usr/src/googletest/googletest/src']
+ if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})):
+ self.is_found = False
+ return
+ self.detect()
+
+ def detect(self) -> None:
+ gtest_detect = self.clib_compiler.find_library("gtest", self.env, [])
+ gtest_main_detect = self.clib_compiler.find_library("gtest_main", self.env, [])
+ if gtest_detect and (not self.main or gtest_main_detect):
+ self.is_found = True
+ self.compile_args = []
+ self.link_args = gtest_detect
+ if self.main:
+ self.link_args += gtest_main_detect
+ self.sources = []
+ self.prebuilt = True
+ elif self.detect_srcdir():
+ self.is_found = True
+ self.compile_args = ['-I' + d for d in self.src_include_dirs]
+ self.link_args = []
+ if self.main:
+ self.sources = [self.all_src, self.main_src]
+ else:
+ self.sources = [self.all_src]
+ self.prebuilt = False
+ else:
+ self.is_found = False
+
+ def detect_srcdir(self) -> bool:
+ for s in self.src_dirs:
+ if os.path.exists(s):
+ self.src_dir = s
+ self.all_src = mesonlib.File.from_absolute_file(
+ os.path.join(self.src_dir, 'gtest-all.cc'))
+ self.main_src = mesonlib.File.from_absolute_file(
+ os.path.join(self.src_dir, 'gtest_main.cc'))
+ self.src_include_dirs = [os.path.normpath(os.path.join(self.src_dir, '..')),
+ os.path.normpath(os.path.join(self.src_dir, '../include')),
+ ]
+ return True
+ return False
+
+ def log_info(self) -> str:
+ if self.prebuilt:
+ return 'prebuilt'
+ else:
+ return 'building self'
+
+
+class GTestDependencyPC(PkgConfigDependency):
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ assert name == 'gtest'
+ if kwargs.get('main'):
+ name = 'gtest_main'
+ super().__init__(name, environment, kwargs)
+
+
+class GMockDependencySystem(SystemDependency):
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ super().__init__(name, environment, kwargs, language='cpp')
+ self.main = kwargs.get('main', False)
+ if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})):
+ self.is_found = False
+ return
+
+ # If we are getting main() from GMock, we definitely
+ # want to avoid linking in main() from GTest
+ gtest_kwargs = kwargs.copy()
+ if self.main:
+ gtest_kwargs['main'] = False
+
+ # GMock without GTest is pretty much useless
+ # this also mimics the structure given in WrapDB,
+ # where GMock always pulls in GTest
+ found = self._add_sub_dependency(gtest_factory(environment, self.for_machine, gtest_kwargs))
+ if not found:
+ self.is_found = False
+ return
+
+ # GMock may be a library or just source.
+ # Work with both.
+ gmock_detect = self.clib_compiler.find_library("gmock", self.env, [])
+ gmock_main_detect = self.clib_compiler.find_library("gmock_main", self.env, [])
+ if gmock_detect and (not self.main or gmock_main_detect):
+ self.is_found = True
+ self.link_args += gmock_detect
+ if self.main:
+ self.link_args += gmock_main_detect
+ self.prebuilt = True
+ return
+
+ for d in ['/usr/src/googletest/googlemock/src', '/usr/src/gmock/src', '/usr/src/gmock']:
+ if os.path.exists(d):
+ self.is_found = True
+ # Yes, we need both because there are multiple
+ # versions of gmock that do different things.
+ d2 = os.path.normpath(os.path.join(d, '..'))
+ self.compile_args += ['-I' + d, '-I' + d2, '-I' + os.path.join(d2, 'include')]
+ all_src = mesonlib.File.from_absolute_file(os.path.join(d, 'gmock-all.cc'))
+ main_src = mesonlib.File.from_absolute_file(os.path.join(d, 'gmock_main.cc'))
+ if self.main:
+ self.sources += [all_src, main_src]
+ else:
+ self.sources += [all_src]
+ self.prebuilt = False
+ return
+
+ self.is_found = False
+
+ def log_info(self) -> str:
+ if self.prebuilt:
+ return 'prebuilt'
+ else:
+ return 'building self'
+
+
+class GMockDependencyPC(PkgConfigDependency):
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ assert name == 'gmock'
+ if kwargs.get('main'):
+ name = 'gmock_main'
+ super().__init__(name, environment, kwargs)
+
+
+class LLVMDependencyConfigTool(ConfigToolDependency):
+ """
+ LLVM uses a special tool, llvm-config, which has arguments for getting
+ c args, cxx args, and ldargs as well as version.
+ """
+ tool_name = 'llvm-config'
+ __cpp_blacklist = {'-DNDEBUG'}
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ self.tools = get_llvm_tool_names('llvm-config')
+
+ # Fedora starting with Fedora 30 adds a suffix of the number
+ # of bits in the isa that llvm targets, for example, on x86_64
+ # and aarch64 the name will be llvm-config-64, on x86 and arm
+ # it will be llvm-config-32.
+ if environment.machines[self.get_for_machine_from_kwargs(kwargs)].is_64_bit:
+ self.tools.append('llvm-config-64')
+ else:
+ self.tools.append('llvm-config-32')
+
+ # It's necessary for LLVM <= 3.8 to use the C++ linker. For 3.9 and 4.0
+ # the C linker works fine if only using the C API.
+ super().__init__(name, environment, kwargs, language='cpp')
+ self.provided_modules: T.List[str] = []
+ self.required_modules: mesonlib.OrderedSet[str] = mesonlib.OrderedSet()
+ self.module_details: T.List[str] = []
+ if not self.is_found:
+ return
+
+ self.provided_modules = self.get_config_value(['--components'], 'modules')
+ modules = stringlistify(extract_as_list(kwargs, 'modules'))
+ self.check_components(modules)
+ opt_modules = stringlistify(extract_as_list(kwargs, 'optional_modules'))
+ self.check_components(opt_modules, required=False)
+
+ cargs = mesonlib.OrderedSet(self.get_config_value(['--cppflags'], 'compile_args'))
+ self.compile_args = list(cargs.difference(self.__cpp_blacklist))
+
+ if version_compare(self.version, '>= 3.9'):
+ self._set_new_link_args(environment)
+ else:
+ self._set_old_link_args()
+ self.link_args = strip_system_libdirs(environment, self.for_machine, self.link_args)
+ self.link_args = self.__fix_bogus_link_args(self.link_args)
+ if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})):
+ self.is_found = False
+ return
+
+ def __fix_bogus_link_args(self, args: T.List[str]) -> T.List[str]:
+ """This function attempts to fix bogus link arguments that llvm-config
+ generates.
+
+ Currently it works around the following:
+ - FreeBSD: when statically linking -l/usr/lib/libexecinfo.so will
+ be generated, strip the -l in cases like this.
+ - Windows: We may get -LIBPATH:... which is later interpreted as
+ "-L IBPATH:...", if we're using an msvc like compilers convert
+ that to "/LIBPATH", otherwise to "-L ..."
+ """
+
+ new_args = []
+ for arg in args:
+ if arg.startswith('-l') and arg.endswith('.so'):
+ new_args.append(arg.lstrip('-l'))
+ elif arg.startswith('-LIBPATH:'):
+ cpp = self.env.coredata.compilers[self.for_machine]['cpp']
+ new_args.extend(cpp.get_linker_search_args(arg.lstrip('-LIBPATH:')))
+ else:
+ new_args.append(arg)
+ return new_args
+
+ def __check_libfiles(self, shared: bool) -> None:
+ """Use llvm-config's --libfiles to check if libraries exist."""
+ mode = '--link-shared' if shared else '--link-static'
+
+ # Set self.required to true to force an exception in get_config_value
+ # if the returncode != 0
+ restore = self.required
+ self.required = True
+
+ try:
+ # It doesn't matter what the stage is, the caller needs to catch
+ # the exception anyway.
+ self.link_args = self.get_config_value(['--libfiles', mode], '')
+ finally:
+ self.required = restore
+
+ def _set_new_link_args(self, environment: 'Environment') -> None:
+ """How to set linker args for LLVM versions >= 3.9"""
+ try:
+ mode = self.get_config_value(['--shared-mode'], 'link_args')[0]
+ except IndexError:
+ mlog.debug('llvm-config --shared-mode returned an error')
+ self.is_found = False
+ return
+
+ if not self.static and mode == 'static':
+ # If llvm is configured with LLVM_BUILD_LLVM_DYLIB but not with
+ # LLVM_LINK_LLVM_DYLIB and not LLVM_BUILD_SHARED_LIBS (which
+ # upstream doesn't recommend using), then llvm-config will lie to
+ # you about how to do shared-linking. It wants to link to a a bunch
+ # of individual shared libs (which don't exist because llvm wasn't
+ # built with LLVM_BUILD_SHARED_LIBS.
+ #
+ # Therefore, we'll try to get the libfiles, if the return code is 0
+ # or we get an empty list, then we'll try to build a working
+ # configuration by hand.
+ try:
+ self.__check_libfiles(True)
+ except DependencyException:
+ lib_ext = get_shared_library_suffix(environment, self.for_machine)
+ libdir = self.get_config_value(['--libdir'], 'link_args')[0]
+ # Sort for reproducibility
+ matches = sorted(glob.iglob(os.path.join(libdir, f'libLLVM*{lib_ext}')))
+ if not matches:
+ if self.required:
+ raise
+ self.is_found = False
+ return
+
+ self.link_args = self.get_config_value(['--ldflags'], 'link_args')
+ libname = os.path.basename(matches[0]).rstrip(lib_ext).lstrip('lib')
+ self.link_args.append(f'-l{libname}')
+ return
+ elif self.static and mode == 'shared':
+ # If, however LLVM_BUILD_SHARED_LIBS is true # (*cough* gentoo *cough*)
+ # then this is correct. Building with LLVM_BUILD_SHARED_LIBS has a side
+ # effect, it stops the generation of static archives. Therefore we need
+ # to check for that and error out on static if this is the case
+ try:
+ self.__check_libfiles(False)
+ except DependencyException:
+ if self.required:
+ raise
+ self.is_found = False
+ return
+
+ link_args = ['--link-static', '--system-libs'] if self.static else ['--link-shared']
+ self.link_args = self.get_config_value(
+ ['--libs', '--ldflags'] + link_args + list(self.required_modules),
+ 'link_args')
+
+ def _set_old_link_args(self) -> None:
+ """Setting linker args for older versions of llvm.
+
+ Old versions of LLVM bring an extra level of insanity with them.
+ llvm-config will provide the correct arguments for static linking, but
+ not for shared-linnking, we have to figure those out ourselves, because
+ of course we do.
+ """
+ if self.static:
+ self.link_args = self.get_config_value(
+ ['--libs', '--ldflags', '--system-libs'] + list(self.required_modules),
+ 'link_args')
+ else:
+ # llvm-config will provide arguments for static linking, so we get
+ # to figure out for ourselves what to link with. We'll do that by
+ # checking in the directory provided by --libdir for a library
+ # called libLLVM-<ver>.(so|dylib|dll)
+ libdir = self.get_config_value(['--libdir'], 'link_args')[0]
+
+ expected_name = f'libLLVM-{self.version}'
+ re_name = re.compile(fr'{expected_name}.(so|dll|dylib)$')
+
+ for file_ in os.listdir(libdir):
+ if re_name.match(file_):
+ self.link_args = [f'-L{libdir}',
+ '-l{}'.format(os.path.splitext(file_.lstrip('lib'))[0])]
+ break
+ else:
+ raise DependencyException(
+ 'Could not find a dynamically linkable library for LLVM.')
+
+ def check_components(self, modules: T.List[str], required: bool = True) -> None:
+ """Check for llvm components (modules in meson terms).
+
+ The required option is whether the module is required, not whether LLVM
+ is required.
+ """
+ for mod in sorted(set(modules)):
+ status = ''
+
+ if mod not in self.provided_modules:
+ if required:
+ self.is_found = False
+ if self.required:
+ raise DependencyException(
+ f'Could not find required LLVM Component: {mod}')
+ status = '(missing)'
+ else:
+ status = '(missing but optional)'
+ else:
+ self.required_modules.add(mod)
+
+ self.module_details.append(mod + status)
+
+ def log_details(self) -> str:
+ if self.module_details:
+ return 'modules: ' + ', '.join(self.module_details)
+ return ''
+
+class LLVMDependencyCMake(CMakeDependency):
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ self.llvm_modules = stringlistify(extract_as_list(kwargs, 'modules'))
+ self.llvm_opt_modules = stringlistify(extract_as_list(kwargs, 'optional_modules'))
+
+ compilers = None
+ if kwargs.get('native', False):
+ compilers = env.coredata.compilers.build
+ else:
+ compilers = env.coredata.compilers.host
+ if not compilers or not all(x in compilers for x in ('c', 'cpp')):
+ # Initialize basic variables
+ ExternalDependency.__init__(self, DependencyTypeName('cmake'), env, kwargs)
+
+ # Initialize CMake specific variables
+ self.found_modules: T.List[str] = []
+ self.name = name
+
+ # Warn and return
+ mlog.warning('The LLVM dependency was not found via CMake since both a C and C++ compiler are required.')
+ return
+
+ super().__init__(name, env, kwargs, language='cpp', force_use_global_compilers=True)
+
+ # Cmake will always create a statically linked binary, so don't use
+ # cmake if dynamic is required
+ if not self.static:
+ self.is_found = False
+ mlog.warning('Ignoring LLVM CMake dependency because dynamic was requested')
+ return
+
+ if self.traceparser is None:
+ return
+
+ # Extract extra include directories and definitions
+ inc_dirs = self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS')
+ defs = self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS')
+ # LLVM explicitly uses space-separated variables rather than semicolon lists
+ if len(defs) == 1:
+ defs = defs[0].split(' ')
+ temp = ['-I' + x for x in inc_dirs] + defs
+ self.compile_args += [x for x in temp if x not in self.compile_args]
+ if not self._add_sub_dependency(threads_factory(env, self.for_machine, {})):
+ self.is_found = False
+ return
+
+ def _main_cmake_file(self) -> str:
+ # Use a custom CMakeLists.txt for LLVM
+ return 'CMakeListsLLVM.txt'
+
+ def _extra_cmake_opts(self) -> T.List[str]:
+ return ['-DLLVM_MESON_MODULES={}'.format(';'.join(self.llvm_modules + self.llvm_opt_modules))]
+
+ def _map_module_list(self, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]]) -> T.List[T.Tuple[str, bool]]:
+ res = []
+ for mod, required in modules:
+ cm_targets = self.traceparser.get_cmake_var(f'MESON_LLVM_TARGETS_{mod}')
+ if not cm_targets:
+ if required:
+ raise self._gen_exception(f'LLVM module {mod} was not found')
+ else:
+ mlog.warning('Optional LLVM module', mlog.bold(mod), 'was not found')
+ continue
+ for i in cm_targets:
+ res += [(i, required)]
+ return res
+
+ def _original_module_name(self, module: str) -> str:
+ orig_name = self.traceparser.get_cmake_var(f'MESON_TARGET_TO_LLVM_{module}')
+ if orig_name:
+ return orig_name[0]
+ return module
+
+
+class ValgrindDependency(PkgConfigDependency):
+ '''
+ Consumers of Valgrind usually only need the compile args and do not want to
+ link to its (static) libraries.
+ '''
+ def __init__(self, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__('valgrind', env, kwargs)
+
+ def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]:
+ return []
+
+
+class ZlibSystemDependency(SystemDependency):
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, environment, kwargs)
+ from ..compilers.c import AppleClangCCompiler
+ from ..compilers.cpp import AppleClangCPPCompiler
+
+ m = self.env.machines[self.for_machine]
+
+ # I'm not sure this is entirely correct. What if we're cross compiling
+ # from something to macOS?
+ if ((m.is_darwin() and isinstance(self.clib_compiler, (AppleClangCCompiler, AppleClangCPPCompiler))) or
+ m.is_freebsd() or m.is_dragonflybsd() or m.is_android()):
+ # No need to set includes,
+ # on macos xcode/clang will do that for us.
+ # on freebsd zlib.h is in /usr/include
+
+ self.is_found = True
+ self.link_args = ['-lz']
+ else:
+ if self.clib_compiler.get_argument_syntax() == 'msvc':
+ libs = ['zlib1', 'zlib']
+ else:
+ libs = ['z']
+ for lib in libs:
+ l = self.clib_compiler.find_library(lib, environment, [])
+ h = self.clib_compiler.has_header('zlib.h', '', environment, dependencies=[self])
+ if l and h[0]:
+ self.is_found = True
+ self.link_args = l
+ break
+ else:
+ return
+
+ v, _ = self.clib_compiler.get_define('ZLIB_VERSION', '#include <zlib.h>', self.env, [], [self])
+ self.version = v.strip('"')
+
+
+class JNISystemDependency(SystemDependency):
+ def __init__(self, environment: 'Environment', kwargs: JNISystemDependencyKW):
+ super().__init__('jni', environment, T.cast('T.Dict[str, T.Any]', kwargs))
+
+ self.feature_since = ('0.62.0', '')
+
+ m = self.env.machines[self.for_machine]
+
+ if 'java' not in environment.coredata.compilers[self.for_machine]:
+ detect_compiler(self.name, environment, self.for_machine, 'java')
+ self.javac = environment.coredata.compilers[self.for_machine]['java']
+ self.version = self.javac.version
+
+ modules: T.List[str] = mesonlib.listify(kwargs.get('modules', []))
+ for module in modules:
+ if module not in {'jvm', 'awt'}:
+ log = mlog.error if self.required else mlog.debug
+ log(f'Unknown JNI module ({module})')
+ self.is_found = False
+ return
+
+ if 'version' in kwargs and not version_compare(self.version, kwargs['version']):
+ mlog.error(f'Incorrect JDK version found ({self.version}), wanted {kwargs["version"]}')
+ self.is_found = False
+ return
+
+ self.java_home = environment.properties[self.for_machine].get_java_home()
+ if not self.java_home:
+ self.java_home = pathlib.Path(shutil.which(self.javac.exelist[0])).resolve().parents[1]
+ if m.is_darwin():
+ problem_java_prefix = pathlib.Path('/System/Library/Frameworks/JavaVM.framework/Versions')
+ if problem_java_prefix in self.java_home.parents:
+ res = subprocess.run(['/usr/libexec/java_home', '--failfast', '--arch', m.cpu_family],
+ stdout=subprocess.PIPE)
+ if res.returncode != 0:
+ log = mlog.error if self.required else mlog.debug
+ log('JAVA_HOME could not be discovered on the system. Please set it explicitly.')
+ self.is_found = False
+ return
+ self.java_home = pathlib.Path(res.stdout.decode().strip())
+
+ platform_include_dir = self.__machine_info_to_platform_include_dir(m)
+ if platform_include_dir is None:
+ mlog.error("Could not find a JDK platform include directory for your OS, please open an issue or provide a pull request.")
+ self.is_found = False
+ return
+
+ java_home_include = self.java_home / 'include'
+ self.compile_args.append(f'-I{java_home_include}')
+ self.compile_args.append(f'-I{java_home_include / platform_include_dir}')
+
+ if modules:
+ if m.is_windows():
+ java_home_lib = self.java_home / 'lib'
+ java_home_lib_server = java_home_lib
+ else:
+ if version_compare(self.version, '<= 1.8.0'):
+ java_home_lib = self.java_home / 'jre' / 'lib' / self.__cpu_translate(m.cpu_family)
+ else:
+ java_home_lib = self.java_home / 'lib'
+
+ java_home_lib_server = java_home_lib / 'server'
+
+ if 'jvm' in modules:
+ jvm = self.clib_compiler.find_library('jvm', environment, extra_dirs=[str(java_home_lib_server)])
+ if jvm is None:
+ mlog.debug('jvm library not found.')
+ self.is_found = False
+ else:
+ self.link_args.extend(jvm)
+ if 'awt' in modules:
+ jawt = self.clib_compiler.find_library('jawt', environment, extra_dirs=[str(java_home_lib)])
+ if jawt is None:
+ mlog.debug('jawt library not found.')
+ self.is_found = False
+ else:
+ self.link_args.extend(jawt)
+
+ self.is_found = True
+
+ @staticmethod
+ def __cpu_translate(cpu: str) -> str:
+ '''
+ The JDK and Meson have a disagreement here, so translate it over. In the event more
+ translation needs to be done, add to following dict.
+ '''
+ java_cpus = {
+ 'x86_64': 'amd64',
+ }
+
+ return java_cpus.get(cpu, cpu)
+
+ @staticmethod
+ def __machine_info_to_platform_include_dir(m: 'MachineInfo') -> T.Optional[str]:
+ '''Translates the machine information to the platform-dependent include directory
+
+ When inspecting a JDK release tarball or $JAVA_HOME, inside the `include/` directory is a
+ platform-dependent directory that must be on the target's include path in addition to the
+ parent `include/` directory.
+ '''
+ if m.is_linux():
+ return 'linux'
+ elif m.is_windows():
+ return 'win32'
+ elif m.is_darwin():
+ return 'darwin'
+ elif m.is_sunos():
+ return 'solaris'
+ elif m.is_freebsd():
+ return 'freebsd'
+ elif m.is_netbsd():
+ return 'netbsd'
+ elif m.is_openbsd():
+ return 'openbsd'
+ elif m.is_dragonflybsd():
+ return 'dragonfly'
+
+ return None
+
+
+class JDKSystemDependency(JNISystemDependency):
+ def __init__(self, environment: 'Environment', kwargs: JNISystemDependencyKW):
+ super().__init__(environment, kwargs)
+
+ self.feature_since = ('0.59.0', '')
+ self.featurechecks.append(FeatureDeprecated(
+ 'jdk system dependency',
+ '0.62.0',
+ 'Use the jni system dependency instead'
+ ))
+
+
+llvm_factory = DependencyFactory(
+ 'LLVM',
+ [DependencyMethods.CMAKE, DependencyMethods.CONFIG_TOOL],
+ cmake_class=LLVMDependencyCMake,
+ configtool_class=LLVMDependencyConfigTool,
+)
+
+gtest_factory = DependencyFactory(
+ 'gtest',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM],
+ pkgconfig_class=GTestDependencyPC,
+ system_class=GTestDependencySystem,
+)
+
+gmock_factory = DependencyFactory(
+ 'gmock',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM],
+ pkgconfig_class=GMockDependencyPC,
+ system_class=GMockDependencySystem,
+)
+
+zlib_factory = DependencyFactory(
+ 'zlib',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE, DependencyMethods.SYSTEM],
+ cmake_name='ZLIB',
+ system_class=ZlibSystemDependency,
+)
diff --git a/mesonbuild/dependencies/dub.py b/mesonbuild/dependencies/dub.py
new file mode 100644
index 0000000..ac2b667
--- /dev/null
+++ b/mesonbuild/dependencies/dub.py
@@ -0,0 +1,404 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+from .base import ExternalDependency, DependencyException, DependencyTypeName
+from .pkgconfig import PkgConfigDependency
+from ..mesonlib import (Popen_safe, OptionKey, join_args)
+from ..programs import ExternalProgram
+from .. import mlog
+import re
+import os
+import json
+import typing as T
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+
+class DubDependency(ExternalDependency):
+ class_dubbin = None
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(DependencyTypeName('dub'), environment, kwargs, language='d')
+ self.name = name
+ from ..compilers.d import DCompiler, d_feature_args
+
+ _temp_comp = super().get_compiler()
+ assert isinstance(_temp_comp, DCompiler)
+ self.compiler = _temp_comp
+
+ if 'required' in kwargs:
+ self.required = kwargs.get('required')
+
+ if DubDependency.class_dubbin is None:
+ self.dubbin = self._check_dub()
+ DubDependency.class_dubbin = self.dubbin
+ else:
+ self.dubbin = DubDependency.class_dubbin
+
+ if not self.dubbin:
+ if self.required:
+ raise DependencyException('DUB not found.')
+ self.is_found = False
+ return
+
+ assert isinstance(self.dubbin, ExternalProgram)
+ mlog.debug('Determining dependency {!r} with DUB executable '
+ '{!r}'.format(name, self.dubbin.get_path()))
+
+ # if an explicit version spec was stated, use this when querying Dub
+ main_pack_spec = name
+ if 'version' in kwargs:
+ version_spec = kwargs['version']
+ if isinstance(version_spec, list):
+ version_spec = " ".join(version_spec)
+ main_pack_spec = f'{name}@{version_spec}'
+
+ # we need to know the target architecture
+ dub_arch = self.compiler.arch
+
+ # we need to know the build type as well
+ dub_buildtype = str(environment.coredata.get_option(OptionKey('buildtype')))
+ # MESON types: choices=['plain', 'debug', 'debugoptimized', 'release', 'minsize', 'custom'])),
+ # DUB types: debug (default), plain, release, release-debug, release-nobounds, unittest, profile, profile-gc,
+ # docs, ddox, cov, unittest-cov, syntax and custom
+ if dub_buildtype == 'debugoptimized':
+ dub_buildtype = 'release-debug'
+ elif dub_buildtype == 'minsize':
+ dub_buildtype = 'release'
+
+ # Ask dub for the package
+ describe_cmd = [
+ 'describe', main_pack_spec, '--arch=' + dub_arch,
+ '--build=' + dub_buildtype, '--compiler=' + self.compiler.get_exelist()[-1]
+ ]
+ ret, res, err = self._call_dubbin(describe_cmd)
+
+ if ret != 0:
+ mlog.debug('DUB describe failed: ' + err)
+ if 'locally' in err:
+ fetch_cmd = ['dub', 'fetch', main_pack_spec]
+ mlog.error(mlog.bold(main_pack_spec), 'is not present locally. You may try the following command:')
+ mlog.log(mlog.bold(join_args(fetch_cmd)))
+ self.is_found = False
+ return
+
+ # A command that might be useful in case of missing DUB package
+ def dub_build_deep_command() -> str:
+ cmd = [
+ 'dub', 'run', 'dub-build-deep', '--yes', '--', main_pack_spec,
+ '--arch=' + dub_arch, '--compiler=' + self.compiler.get_exelist()[-1],
+ '--build=' + dub_buildtype
+ ]
+ return join_args(cmd)
+
+ dub_comp_id = self.compiler.get_id().replace('llvm', 'ldc').replace('gcc', 'gdc')
+ description = json.loads(res)
+
+ self.compile_args = []
+ self.link_args = self.raw_link_args = []
+
+ show_buildtype_warning = False
+
+ def find_package_target(pkg: T.Dict[str, str]) -> bool:
+ nonlocal show_buildtype_warning
+ # try to find a static library in a DUB folder corresponding to
+ # version, configuration, compiler, arch and build-type
+ # if can find, add to link_args.
+ # link_args order is meaningful, so this function MUST be called in the right order
+ pack_id = f'{pkg["name"]}@{pkg["version"]}'
+ (tgt_file, compatibilities) = self._find_compatible_package_target(description, pkg, dub_comp_id)
+ if tgt_file is None:
+ if not compatibilities:
+ mlog.error(mlog.bold(pack_id), 'not found')
+ elif 'compiler' not in compatibilities:
+ mlog.error(mlog.bold(pack_id), 'found but not compiled with ', mlog.bold(dub_comp_id))
+ elif dub_comp_id != 'gdc' and 'compiler_version' not in compatibilities:
+ mlog.error(mlog.bold(pack_id), 'found but not compiled with', mlog.bold(f'{dub_comp_id}-{self.compiler.version}'))
+ elif 'arch' not in compatibilities:
+ mlog.error(mlog.bold(pack_id), 'found but not compiled for', mlog.bold(dub_arch))
+ elif 'platform' not in compatibilities:
+ mlog.error(mlog.bold(pack_id), 'found but not compiled for', mlog.bold(description['platform'].join('.')))
+ elif 'configuration' not in compatibilities:
+ mlog.error(mlog.bold(pack_id), 'found but not compiled for the', mlog.bold(pkg['configuration']), 'configuration')
+ else:
+ mlog.error(mlog.bold(pack_id), 'not found')
+
+ mlog.log('You may try the following command to install the necessary DUB libraries:')
+ mlog.log(mlog.bold(dub_build_deep_command()))
+
+ return False
+
+ if 'build_type' not in compatibilities:
+ mlog.warning(mlog.bold(pack_id), 'found but not compiled as', mlog.bold(dub_buildtype))
+ show_buildtype_warning = True
+
+ self.link_args.append(tgt_file)
+ return True
+
+ # Main algorithm:
+ # 1. Ensure that the target is a compatible library type (not dynamic)
+ # 2. Find a compatible built library for the main dependency
+ # 3. Do the same for each sub-dependency.
+ # link_args MUST be in the same order than the "linkDependencies" of the main target
+ # 4. Add other build settings (imports, versions etc.)
+
+ # 1
+ self.is_found = False
+ packages = {}
+ for pkg in description['packages']:
+ packages[pkg['name']] = pkg
+
+ if not pkg['active']:
+ continue
+
+ if pkg['targetType'] == 'dynamicLibrary':
+ mlog.error('DUB dynamic library dependencies are not supported.')
+ self.is_found = False
+ return
+
+ ## check that the main dependency is indeed a library
+ if pkg['name'] == name:
+ self.is_found = True
+
+ if pkg['targetType'] not in ['library', 'sourceLibrary', 'staticLibrary']:
+ mlog.error(mlog.bold(name), "found but it isn't a library")
+ self.is_found = False
+ return
+
+ self.version = pkg['version']
+ self.pkg = pkg
+
+ # collect all targets
+ targets = {}
+ for tgt in description['targets']:
+ targets[tgt['rootPackage']] = tgt
+
+ if name not in targets:
+ self.is_found = False
+ if self.pkg['targetType'] == 'sourceLibrary':
+ # source libraries have no associated targets,
+ # but some build settings like import folders must be found from the package object.
+ # Current algo only get these from "buildSettings" in the target object.
+ # Let's save this for a future PR.
+ # (See openssl DUB package for example of sourceLibrary)
+ mlog.error('DUB targets of type', mlog.bold('sourceLibrary'), 'are not supported.')
+ else:
+ mlog.error('Could not find target description for', mlog.bold(main_pack_spec))
+
+ if not self.is_found:
+ mlog.error(f'Could not find {name} in DUB description')
+ return
+
+ # Current impl only supports static libraries
+ self.static = True
+
+ # 2
+ if not find_package_target(self.pkg):
+ self.is_found = False
+ return
+
+ # 3
+ for link_dep in targets[name]['linkDependencies']:
+ pkg = packages[link_dep]
+ if not find_package_target(pkg):
+ self.is_found = False
+ return
+
+ if show_buildtype_warning:
+ mlog.log('If it is not suitable, try the following command and reconfigure Meson with', mlog.bold('--clearcache'))
+ mlog.log(mlog.bold(dub_build_deep_command()))
+
+ # 4
+ bs = targets[name]['buildSettings']
+
+ for flag in bs['dflags']:
+ self.compile_args.append(flag)
+
+ for path in bs['importPaths']:
+ self.compile_args.append('-I' + path)
+
+ for path in bs['stringImportPaths']:
+ if 'import_dir' not in d_feature_args[self.compiler.id]:
+ break
+ flag = d_feature_args[self.compiler.id]['import_dir']
+ self.compile_args.append(f'{flag}={path}')
+
+ for ver in bs['versions']:
+ if 'version' not in d_feature_args[self.compiler.id]:
+ break
+ flag = d_feature_args[self.compiler.id]['version']
+ self.compile_args.append(f'{flag}={ver}')
+
+ if bs['mainSourceFile']:
+ self.compile_args.append(bs['mainSourceFile'])
+
+ # pass static libraries
+ # linkerFiles are added during step 3
+ # for file in bs['linkerFiles']:
+ # self.link_args.append(file)
+
+ for file in bs['sourceFiles']:
+ # sourceFiles may contain static libraries
+ if file.endswith('.lib') or file.endswith('.a'):
+ self.link_args.append(file)
+
+ for flag in bs['lflags']:
+ self.link_args.append(flag)
+
+ is_windows = self.env.machines.host.is_windows()
+ if is_windows:
+ winlibs = ['kernel32', 'user32', 'gdi32', 'winspool', 'shell32', 'ole32',
+ 'oleaut32', 'uuid', 'comdlg32', 'advapi32', 'ws2_32']
+
+ for lib in bs['libs']:
+ if os.name != 'nt':
+ # trying to add system libraries by pkg-config
+ pkgdep = PkgConfigDependency(lib, environment, {'required': 'true', 'silent': 'true'})
+ if pkgdep.is_found:
+ for arg in pkgdep.get_compile_args():
+ self.compile_args.append(arg)
+ for arg in pkgdep.get_link_args():
+ self.link_args.append(arg)
+ for arg in pkgdep.get_link_args(raw=True):
+ self.raw_link_args.append(arg)
+ continue
+
+ if is_windows and lib in winlibs:
+ self.link_args.append(lib + '.lib')
+ continue
+
+ # fallback
+ self.link_args.append('-l'+lib)
+
+ # This function finds the target of the provided JSON package, built for the right
+ # compiler, architecture, configuration...
+ # It returns (target|None, {compatibilities})
+ # If None is returned for target, compatibilities will list what other targets were found without full compatibility
+ def _find_compatible_package_target(self, jdesc: T.Dict[str, str], jpack: T.Dict[str, str], dub_comp_id: str) -> T.Tuple[str, T.Set[str]]:
+ dub_build_path = os.path.join(jpack['path'], '.dub', 'build')
+
+ if not os.path.exists(dub_build_path):
+ return (None, None)
+
+ # try to find a dir like library-debug-linux.posix-x86_64-ldc_2081-EF934983A3319F8F8FF2F0E107A363BA
+
+ # fields are:
+ # - configuration
+ # - build type
+ # - platform
+ # - architecture
+ # - compiler id (dmd, ldc, gdc)
+ # - compiler version or frontend id or frontend version?
+
+ conf = jpack['configuration']
+ build_type = jdesc['buildType']
+ platforms = jdesc['platform']
+ archs = jdesc['architecture']
+
+ # Get D frontend version implemented in the compiler, or the compiler version itself
+ # gdc doesn't support this
+ comp_versions = []
+
+ if dub_comp_id != 'gdc':
+ comp_versions.append(self.compiler.version)
+
+ ret, res = self._call_compbin(['--version'])[0:2]
+ if ret != 0:
+ mlog.error('Failed to run {!r}', mlog.bold(dub_comp_id))
+ return (None, None)
+ d_ver_reg = re.search('v[0-9].[0-9][0-9][0-9].[0-9]', res) # Ex.: v2.081.2
+
+ if d_ver_reg is not None:
+ frontend_version = d_ver_reg.group()
+ frontend_id = frontend_version.rsplit('.', 1)[0].replace('v', '').replace('.', '') # Fix structure. Ex.: 2081
+ comp_versions.extend([frontend_version, frontend_id])
+
+ compatibilities: T.Set[str] = set()
+
+ # build_type is not in check_list because different build types might be compatible.
+ # We do show a WARNING that the build type is not the same.
+ # It might be critical in release builds, and acceptable otherwise
+ check_list = ('configuration', 'platform', 'arch', 'compiler', 'compiler_version')
+
+ for entry in os.listdir(dub_build_path):
+
+ target = os.path.join(dub_build_path, entry, jpack['targetFileName'])
+ if not os.path.exists(target):
+ # unless Dub and Meson are racing, the target file should be present
+ # when the directory is present
+ mlog.debug("WARNING: Could not find a Dub target: " + target)
+ continue
+
+ # we build a new set for each entry, because if this target is returned
+ # we want to return only the compatibilities associated to this target
+ # otherwise we could miss the WARNING about build_type
+ comps = set()
+
+ if conf in entry:
+ comps.add('configuration')
+
+ if build_type in entry:
+ comps.add('build_type')
+
+ if all(platform in entry for platform in platforms):
+ comps.add('platform')
+
+ if all(arch in entry for arch in archs):
+ comps.add('arch')
+
+ if dub_comp_id in entry:
+ comps.add('compiler')
+
+ if dub_comp_id == 'gdc' or any(cv in entry for cv in comp_versions):
+ comps.add('compiler_version')
+
+ if all(key in comps for key in check_list):
+ return (target, comps)
+ else:
+ compatibilities = set.union(compatibilities, comps)
+
+ return (None, compatibilities)
+
+ def _call_dubbin(self, args: T.List[str], env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, str, str]:
+ assert isinstance(self.dubbin, ExternalProgram)
+ p, out, err = Popen_safe(self.dubbin.get_command() + args, env=env)
+ return p.returncode, out.strip(), err.strip()
+
+ def _call_compbin(self, args: T.List[str], env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, str, str]:
+ p, out, err = Popen_safe(self.compiler.get_exelist() + args, env=env)
+ return p.returncode, out.strip(), err.strip()
+
+ def _check_dub(self) -> T.Union[bool, ExternalProgram]:
+ dubbin: T.Union[bool, ExternalProgram] = ExternalProgram('dub', silent=True)
+ assert isinstance(dubbin, ExternalProgram)
+ if dubbin.found():
+ try:
+ p, out = Popen_safe(dubbin.get_command() + ['--version'])[0:2]
+ if p.returncode != 0:
+ mlog.warning('Found dub {!r} but couldn\'t run it'
+ ''.format(' '.join(dubbin.get_command())))
+ # Set to False instead of None to signify that we've already
+ # searched for it and not found it
+ dubbin = False
+ except (FileNotFoundError, PermissionError):
+ dubbin = False
+ else:
+ dubbin = False
+ if isinstance(dubbin, ExternalProgram):
+ mlog.log('Found DUB:', mlog.bold(dubbin.get_path()),
+ '(%s)' % out.strip())
+ else:
+ mlog.log('Found DUB:', mlog.red('NO'))
+ return dubbin
diff --git a/mesonbuild/dependencies/factory.py b/mesonbuild/dependencies/factory.py
new file mode 100644
index 0000000..d50ce0f
--- /dev/null
+++ b/mesonbuild/dependencies/factory.py
@@ -0,0 +1,156 @@
+# Copyright 2013-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# 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.
+from __future__ import annotations
+
+import functools
+import typing as T
+
+from .base import DependencyException, DependencyMethods
+from .base import process_method_kw
+from .base import BuiltinDependency, SystemDependency
+from .cmake import CMakeDependency
+from .framework import ExtraFrameworkDependency
+from .pkgconfig import PkgConfigDependency
+
+if T.TYPE_CHECKING:
+ from .base import ExternalDependency
+ from .configtool import ConfigToolDependency
+ from ..environment import Environment
+ from ..mesonlib import MachineChoice
+
+ DependencyGenerator = T.Callable[[], ExternalDependency]
+ FactoryFunc = T.Callable[
+ [
+ 'Environment',
+ MachineChoice,
+ T.Dict[str, T.Any],
+ T.List[DependencyMethods]
+ ],
+ T.List[DependencyGenerator]
+ ]
+
+ WrappedFactoryFunc = T.Callable[
+ [
+ 'Environment',
+ MachineChoice,
+ T.Dict[str, T.Any]
+ ],
+ T.List[DependencyGenerator]
+ ]
+
+ # This should be str, Environment, T.Dict[str, T.Any], T.Optional[str]
+ # But if you try that, you get error: Cannot infer type of lambda
+ CmakeDependencyFunc = T.Callable[..., CMakeDependency]
+
+class DependencyFactory:
+
+ """Factory to get dependencies from multiple sources.
+
+ This class provides an initializer that takes a set of names and classes
+ for various kinds of dependencies. When the initialized object is called
+ it returns a list of callables return Dependency objects to try in order.
+
+ :name: The name of the dependency. This will be passed as the name
+ parameter of the each dependency unless it is overridden on a per
+ type basis.
+ :methods: An ordered list of DependencyMethods. This is the order
+ dependencies will be returned in unless they are removed by the
+ _process_method function
+ :*_name: This will overwrite the name passed to the corresponding class.
+ For example, if the name is 'zlib', but cmake calls the dependency
+ 'Z', then using `cmake_name='Z'` will pass the name as 'Z' to cmake.
+ :*_class: A *type* or callable that creates a class, and has the
+ signature of an ExternalDependency
+ :system_class: If you pass DependencyMethods.SYSTEM in methods, you must
+ set this argument.
+ """
+
+ def __init__(self, name: str, methods: T.List[DependencyMethods], *,
+ extra_kwargs: T.Optional[T.Dict[str, T.Any]] = None,
+ pkgconfig_name: T.Optional[str] = None,
+ pkgconfig_class: 'T.Type[PkgConfigDependency]' = PkgConfigDependency,
+ cmake_name: T.Optional[str] = None,
+ cmake_class: 'T.Union[T.Type[CMakeDependency], CmakeDependencyFunc]' = CMakeDependency,
+ configtool_class: 'T.Optional[T.Type[ConfigToolDependency]]' = None,
+ framework_name: T.Optional[str] = None,
+ framework_class: 'T.Type[ExtraFrameworkDependency]' = ExtraFrameworkDependency,
+ builtin_class: 'T.Type[BuiltinDependency]' = BuiltinDependency,
+ system_class: 'T.Type[SystemDependency]' = SystemDependency):
+
+ if DependencyMethods.CONFIG_TOOL in methods and not configtool_class:
+ raise DependencyException('A configtool must have a custom class')
+
+ self.extra_kwargs = extra_kwargs or {}
+ self.methods = methods
+ self.classes: T.Dict[
+ DependencyMethods,
+ T.Callable[['Environment', T.Dict[str, T.Any]], ExternalDependency]
+ ] = {
+ # Just attach the correct name right now, either the generic name
+ # or the method specific name.
+ DependencyMethods.EXTRAFRAMEWORK: functools.partial(framework_class, framework_name or name),
+ DependencyMethods.PKGCONFIG: functools.partial(pkgconfig_class, pkgconfig_name or name),
+ DependencyMethods.CMAKE: functools.partial(cmake_class, cmake_name or name),
+ DependencyMethods.SYSTEM: functools.partial(system_class, name),
+ DependencyMethods.BUILTIN: functools.partial(builtin_class, name),
+ DependencyMethods.CONFIG_TOOL: None,
+ }
+ if configtool_class is not None:
+ self.classes[DependencyMethods.CONFIG_TOOL] = functools.partial(configtool_class, name)
+
+ @staticmethod
+ def _process_method(method: DependencyMethods, env: 'Environment', for_machine: MachineChoice) -> bool:
+ """Report whether a method is valid or not.
+
+ If the method is valid, return true, otherwise return false. This is
+ used in a list comprehension to filter methods that are not possible.
+
+ By default this only remove EXTRAFRAMEWORK dependencies for non-mac platforms.
+ """
+ # Extra frameworks are only valid for macOS and other apple products
+ if (method is DependencyMethods.EXTRAFRAMEWORK and
+ not env.machines[for_machine].is_darwin()):
+ return False
+ return True
+
+ def __call__(self, env: 'Environment', for_machine: MachineChoice,
+ kwargs: T.Dict[str, T.Any]) -> T.List['DependencyGenerator']:
+ """Return a list of Dependencies with the arguments already attached."""
+ methods = process_method_kw(self.methods, kwargs)
+ nwargs = self.extra_kwargs.copy()
+ nwargs.update(kwargs)
+
+ return [functools.partial(self.classes[m], env, nwargs) for m in methods
+ if self._process_method(m, env, for_machine)]
+
+
+def factory_methods(methods: T.Set[DependencyMethods]) -> T.Callable[['FactoryFunc'], 'WrappedFactoryFunc']:
+ """Decorator for handling methods for dependency factory functions.
+
+ This helps to make factory functions self documenting
+ >>> @factory_methods([DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE])
+ >>> def factory(env: Environment, for_machine: MachineChoice, kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
+ >>> pass
+ """
+
+ def inner(func: 'FactoryFunc') -> 'WrappedFactoryFunc':
+
+ @functools.wraps(func)
+ def wrapped(env: 'Environment', for_machine: MachineChoice, kwargs: T.Dict[str, T.Any]) -> T.List['DependencyGenerator']:
+ return func(env, for_machine, kwargs, process_method_kw(methods, kwargs))
+
+ return wrapped
+
+ return inner
diff --git a/mesonbuild/dependencies/framework.py b/mesonbuild/dependencies/framework.py
new file mode 100644
index 0000000..b02b3ce
--- /dev/null
+++ b/mesonbuild/dependencies/framework.py
@@ -0,0 +1,121 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+from .base import DependencyTypeName, ExternalDependency, DependencyException
+from ..mesonlib import MesonException, Version, stringlistify
+from .. import mlog
+from pathlib import Path
+import typing as T
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+
+class ExtraFrameworkDependency(ExternalDependency):
+ system_framework_paths: T.Optional[T.List[str]] = None
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
+ paths = stringlistify(kwargs.get('paths', []))
+ super().__init__(DependencyTypeName('extraframeworks'), env, kwargs, language=language)
+ self.name = name
+ # Full path to framework directory
+ self.framework_path: T.Optional[str] = None
+ if not self.clib_compiler:
+ raise DependencyException('No C-like compilers are available')
+ if self.system_framework_paths is None:
+ try:
+ self.system_framework_paths = self.clib_compiler.find_framework_paths(self.env)
+ except MesonException as e:
+ if 'non-clang' in str(e):
+ # Apple frameworks can only be found (and used) with the
+ # system compiler. It is not available so bail immediately.
+ self.is_found = False
+ return
+ raise
+ self.detect(name, paths)
+
+ def detect(self, name: str, paths: T.List[str]) -> None:
+ if not paths:
+ paths = self.system_framework_paths
+ for p in paths:
+ mlog.debug(f'Looking for framework {name} in {p}')
+ # We need to know the exact framework path because it's used by the
+ # Qt5 dependency class, and for setting the include path. We also
+ # want to avoid searching in an invalid framework path which wastes
+ # time and can cause a false positive.
+ framework_path = self._get_framework_path(p, name)
+ if framework_path is None:
+ continue
+ # We want to prefer the specified paths (in order) over the system
+ # paths since these are "extra" frameworks.
+ # For example, Python2's framework is in /System/Library/Frameworks and
+ # Python3's framework is in /Library/Frameworks, but both are called
+ # Python.framework. We need to know for sure that the framework was
+ # found in the path we expect.
+ allow_system = p in self.system_framework_paths
+ args = self.clib_compiler.find_framework(name, self.env, [p], allow_system)
+ if args is None:
+ continue
+ self.link_args = args
+ self.framework_path = framework_path.as_posix()
+ self.compile_args = ['-F' + self.framework_path]
+ # We need to also add -I includes to the framework because all
+ # cross-platform projects such as OpenGL, Python, Qt, GStreamer,
+ # etc do not use "framework includes":
+ # https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Tasks/IncludingFrameworks.html
+ incdir = self._get_framework_include_path(framework_path)
+ if incdir:
+ self.compile_args += ['-I' + incdir]
+ self.is_found = True
+ return
+
+ def _get_framework_path(self, path: str, name: str) -> T.Optional[Path]:
+ p = Path(path)
+ lname = name.lower()
+ for d in p.glob('*.framework/'):
+ if lname == d.name.rsplit('.', 1)[0].lower():
+ return d
+ return None
+
+ def _get_framework_latest_version(self, path: Path) -> str:
+ versions = []
+ for each in path.glob('Versions/*'):
+ # macOS filesystems are usually case-insensitive
+ if each.name.lower() == 'current':
+ continue
+ versions.append(Version(each.name))
+ if len(versions) == 0:
+ # most system frameworks do not have a 'Versions' directory
+ return 'Headers'
+ return 'Versions/{}/Headers'.format(sorted(versions)[-1]._s)
+
+ def _get_framework_include_path(self, path: Path) -> T.Optional[str]:
+ # According to the spec, 'Headers' must always be a symlink to the
+ # Headers directory inside the currently-selected version of the
+ # framework, but sometimes frameworks are broken. Look in 'Versions'
+ # for the currently-selected version or pick the latest one.
+ trials = ('Headers', 'Versions/Current/Headers',
+ self._get_framework_latest_version(path))
+ for each in trials:
+ trial = path / each
+ if trial.is_dir():
+ return trial.as_posix()
+ return None
+
+ def log_info(self) -> str:
+ return self.framework_path or ''
+
+ @staticmethod
+ def log_tried() -> str:
+ return 'framework'
diff --git a/mesonbuild/dependencies/hdf5.py b/mesonbuild/dependencies/hdf5.py
new file mode 100644
index 0000000..4e5820a
--- /dev/null
+++ b/mesonbuild/dependencies/hdf5.py
@@ -0,0 +1,180 @@
+# Copyright 2013-2019 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.
+
+# This file contains the detection logic for miscellaneous external dependencies.
+from __future__ import annotations
+
+import functools
+import os
+import re
+import subprocess
+from pathlib import Path
+
+from ..mesonlib import Popen_safe, OrderedSet, join_args
+from ..programs import ExternalProgram
+from .base import DependencyException, DependencyMethods
+from .configtool import ConfigToolDependency
+from .pkgconfig import PkgConfigDependency
+from .factory import factory_methods
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .factory import DependencyGenerator
+ from ..environment import Environment
+ from ..mesonlib import MachineChoice
+
+
+class HDF5PkgConfigDependency(PkgConfigDependency):
+
+ """Handle brokenness in the HDF5 pkg-config files."""
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
+ language = language or 'c'
+ if language not in {'c', 'cpp', 'fortran'}:
+ raise DependencyException(f'Language {language} is not supported with HDF5.')
+
+ super().__init__(name, environment, kwargs, language)
+ if not self.is_found:
+ return
+
+ # some broken pkgconfig don't actually list the full path to the needed includes
+ newinc = [] # type: T.List[str]
+ for arg in self.compile_args:
+ if arg.startswith('-I'):
+ stem = 'static' if self.static else 'shared'
+ if (Path(arg[2:]) / stem).is_dir():
+ newinc.append('-I' + str(Path(arg[2:]) / stem))
+ self.compile_args += newinc
+
+ link_args = [] # type: T.List[str]
+ for larg in self.get_link_args():
+ lpath = Path(larg)
+ # some pkg-config hdf5.pc (e.g. Ubuntu) don't include the commonly-used HL HDF5 libraries,
+ # so let's add them if they exist
+ # additionally, some pkgconfig HDF5 HL files are malformed so let's be sure to find HL anyway
+ if lpath.is_file():
+ hl = []
+ if language == 'cpp':
+ hl += ['_hl_cpp', '_cpp']
+ elif language == 'fortran':
+ hl += ['_hl_fortran', 'hl_fortran', '_fortran']
+ hl += ['_hl'] # C HL library, always needed
+
+ suffix = '.' + lpath.name.split('.', 1)[1] # in case of .dll.a
+ for h in hl:
+ hlfn = lpath.parent / (lpath.name.split('.', 1)[0] + h + suffix)
+ if hlfn.is_file():
+ link_args.append(str(hlfn))
+ # HDF5 C libs are required by other HDF5 languages
+ link_args.append(larg)
+ else:
+ link_args.append(larg)
+
+ self.link_args = link_args
+
+
+class HDF5ConfigToolDependency(ConfigToolDependency):
+
+ """Wrapper around hdf5 binary config tools."""
+
+ version_arg = '-showconfig'
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
+ language = language or 'c'
+ if language not in {'c', 'cpp', 'fortran'}:
+ raise DependencyException(f'Language {language} is not supported with HDF5.')
+
+ if language == 'c':
+ cenv = 'CC'
+ tools = ['h5cc', 'h5pcc']
+ elif language == 'cpp':
+ cenv = 'CXX'
+ tools = ['h5c++', 'h5pc++']
+ elif language == 'fortran':
+ cenv = 'FC'
+ tools = ['h5fc', 'h5pfc']
+ else:
+ raise DependencyException('How did you get here?')
+
+ # We need this before we call super()
+ for_machine = self.get_for_machine_from_kwargs(kwargs)
+
+ nkwargs = kwargs.copy()
+ nkwargs['tools'] = tools
+
+ # Override the compiler that the config tools are going to use by
+ # setting the environment variables that they use for the compiler and
+ # linkers.
+ compiler = environment.coredata.compilers[for_machine][language]
+ try:
+ os.environ[f'HDF5_{cenv}'] = join_args(compiler.get_exelist())
+ os.environ[f'HDF5_{cenv}LINKER'] = join_args(compiler.get_linker_exelist())
+ super().__init__(name, environment, nkwargs, language)
+ finally:
+ del os.environ[f'HDF5_{cenv}']
+ del os.environ[f'HDF5_{cenv}LINKER']
+ if not self.is_found:
+ return
+
+ # We first need to call the tool with -c to get the compile arguments
+ # and then without -c to get the link arguments.
+ args = self.get_config_value(['-show', '-c'], 'args')[1:]
+ args += self.get_config_value(['-show', '-noshlib' if self.static else '-shlib'], 'args')[1:]
+ for arg in args:
+ if arg.startswith(('-I', '-f', '-D')) or arg == '-pthread':
+ self.compile_args.append(arg)
+ elif arg.startswith(('-L', '-l', '-Wl')):
+ self.link_args.append(arg)
+ elif Path(arg).is_file():
+ self.link_args.append(arg)
+
+ # If the language is not C we need to add C as a subdependency
+ if language != 'c':
+ nkwargs = kwargs.copy()
+ nkwargs['language'] = 'c'
+ # I'm being too clever for mypy and pylint
+ self.is_found = self._add_sub_dependency(hdf5_factory(environment, for_machine, nkwargs)) # pylint: disable=no-value-for-parameter
+
+ def _sanitize_version(self, ver: str) -> str:
+ v = re.search(r'\s*HDF5 Version: (\d+\.\d+\.\d+)', ver)
+ return v.group(1)
+
+
+@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL})
+def hdf5_factory(env: 'Environment', for_machine: 'MachineChoice',
+ kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
+ language = kwargs.get('language')
+ candidates: T.List['DependencyGenerator'] = []
+
+ if DependencyMethods.PKGCONFIG in methods:
+ # Use an ordered set so that these remain the first tried pkg-config files
+ pkgconfig_files = OrderedSet(['hdf5', 'hdf5-serial'])
+ PCEXE = PkgConfigDependency._detect_pkgbin(False, env, for_machine)
+ pcenv = PkgConfigDependency.setup_env(os.environ, env, for_machine)
+ if PCEXE:
+ assert isinstance(PCEXE, ExternalProgram)
+ # some distros put hdf5-1.2.3.pc with version number in .pc filename.
+ ret, stdout, _ = Popen_safe(PCEXE.get_command() + ['--list-all'], stderr=subprocess.DEVNULL, env=pcenv)
+ if ret.returncode == 0:
+ for pkg in stdout.split('\n'):
+ if pkg.startswith('hdf5'):
+ pkgconfig_files.add(pkg.split(' ', 1)[0])
+
+ for pkg in pkgconfig_files:
+ candidates.append(functools.partial(HDF5PkgConfigDependency, pkg, env, kwargs, language))
+
+ if DependencyMethods.CONFIG_TOOL in methods:
+ candidates.append(functools.partial(HDF5ConfigToolDependency, 'hdf5', env, kwargs, language))
+
+ return candidates
diff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py
new file mode 100644
index 0000000..2913d84
--- /dev/null
+++ b/mesonbuild/dependencies/misc.py
@@ -0,0 +1,724 @@
+# Copyright 2013-2019 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.
+
+# This file contains the detection logic for miscellaneous external dependencies.
+from __future__ import annotations
+
+from pathlib import Path
+import functools
+import re
+import sysconfig
+import typing as T
+
+from .. import mesonlib
+from .. import mlog
+from ..environment import detect_cpu_family
+from .base import DependencyException, DependencyMethods
+from .base import BuiltinDependency, SystemDependency
+from .cmake import CMakeDependency
+from .configtool import ConfigToolDependency
+from .factory import DependencyFactory, factory_methods
+from .pkgconfig import PkgConfigDependency
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment, MachineChoice
+ from .factory import DependencyGenerator
+
+
+@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE})
+def netcdf_factory(env: 'Environment',
+ for_machine: 'MachineChoice',
+ kwargs: T.Dict[str, T.Any],
+ methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
+ language = kwargs.get('language', 'c')
+ if language not in ('c', 'cpp', 'fortran'):
+ raise DependencyException(f'Language {language} is not supported with NetCDF.')
+
+ candidates: T.List['DependencyGenerator'] = []
+
+ if DependencyMethods.PKGCONFIG in methods:
+ if language == 'fortran':
+ pkg = 'netcdf-fortran'
+ else:
+ pkg = 'netcdf'
+
+ candidates.append(functools.partial(PkgConfigDependency, pkg, env, kwargs, language=language))
+
+ if DependencyMethods.CMAKE in methods:
+ candidates.append(functools.partial(CMakeDependency, 'NetCDF', env, kwargs, language=language))
+
+ return candidates
+
+
+class DlBuiltinDependency(BuiltinDependency):
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, env, kwargs)
+ self.feature_since = ('0.62.0', "consider checking for `dlopen` with and without `find_library('dl')`")
+
+ if self.clib_compiler.has_function('dlopen', '#include <dlfcn.h>', env)[0]:
+ self.is_found = True
+
+
+class DlSystemDependency(SystemDependency):
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, env, kwargs)
+ self.feature_since = ('0.62.0', "consider checking for `dlopen` with and without `find_library('dl')`")
+
+ h = self.clib_compiler.has_header('dlfcn.h', '', env)
+ self.link_args = self.clib_compiler.find_library('dl', env, [], self.libtype)
+
+ if h[0] and self.link_args:
+ self.is_found = True
+
+
+class OpenMPDependency(SystemDependency):
+ # Map date of specification release (which is the macro value) to a version.
+ VERSIONS = {
+ '201811': '5.0',
+ '201611': '5.0-revision1', # This is supported by ICC 19.x
+ '201511': '4.5',
+ '201307': '4.0',
+ '201107': '3.1',
+ '200805': '3.0',
+ '200505': '2.5',
+ '200203': '2.0',
+ '199810': '1.0',
+ }
+
+ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ language = kwargs.get('language')
+ super().__init__('openmp', environment, kwargs, language=language)
+ self.is_found = False
+ if self.clib_compiler.get_id() == 'nagfor':
+ # No macro defined for OpenMP, but OpenMP 3.1 is supported.
+ self.version = '3.1'
+ self.is_found = True
+ self.compile_args = self.link_args = self.clib_compiler.openmp_flags()
+ return
+ if self.clib_compiler.get_id() == 'pgi':
+ # through at least PGI 19.4, there is no macro defined for OpenMP, but OpenMP 3.1 is supported.
+ self.version = '3.1'
+ self.is_found = True
+ self.compile_args = self.link_args = self.clib_compiler.openmp_flags()
+ return
+ try:
+ openmp_date = self.clib_compiler.get_define(
+ '_OPENMP', '', self.env, self.clib_compiler.openmp_flags(), [self], disable_cache=True)[0]
+ except mesonlib.EnvironmentException as e:
+ mlog.debug('OpenMP support not available in the compiler')
+ mlog.debug(e)
+ openmp_date = None
+
+ if openmp_date:
+ try:
+ self.version = self.VERSIONS[openmp_date]
+ except KeyError:
+ mlog.debug(f'Could not find an OpenMP version matching {openmp_date}')
+ if openmp_date == '_OPENMP':
+ mlog.debug('This can be caused by flags such as gcc\'s `-fdirectives-only`, which affect preprocessor behavior.')
+ return
+ # Flang has omp_lib.h
+ header_names = ('omp.h', 'omp_lib.h')
+ for name in header_names:
+ if self.clib_compiler.has_header(name, '', self.env, dependencies=[self], disable_cache=True)[0]:
+ self.is_found = True
+ self.compile_args = self.clib_compiler.openmp_flags()
+ self.link_args = self.clib_compiler.openmp_link_flags()
+ break
+ if not self.is_found:
+ mlog.log(mlog.yellow('WARNING:'), 'OpenMP found but omp.h missing.')
+
+
+class ThreadDependency(SystemDependency):
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ super().__init__(name, environment, kwargs)
+ self.is_found = True
+ # Happens if you are using a language with threads
+ # concept without C, such as plain Cuda.
+ if not self.clib_compiler:
+ self.compile_args = []
+ self.link_args = []
+ else:
+ self.compile_args = self.clib_compiler.thread_flags(environment)
+ self.link_args = self.clib_compiler.thread_link_flags(environment)
+
+
+class BlocksDependency(SystemDependency):
+ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ super().__init__('blocks', environment, kwargs)
+ self.name = 'blocks'
+ self.is_found = False
+
+ if self.env.machines[self.for_machine].is_darwin():
+ self.compile_args = []
+ self.link_args = []
+ else:
+ self.compile_args = ['-fblocks']
+ self.link_args = ['-lBlocksRuntime']
+
+ if not self.clib_compiler.has_header('Block.h', '', environment, disable_cache=True) or \
+ not self.clib_compiler.find_library('BlocksRuntime', environment, []):
+ mlog.log(mlog.red('ERROR:'), 'BlocksRuntime not found.')
+ return
+
+ source = '''
+ int main(int argc, char **argv)
+ {
+ int (^callback)(void) = ^ int (void) { return 0; };
+ return callback();
+ }'''
+
+ with self.clib_compiler.compile(source, extra_args=self.compile_args + self.link_args) as p:
+ if p.returncode != 0:
+ mlog.log(mlog.red('ERROR:'), 'Compiler does not support blocks extension.')
+ return
+
+ self.is_found = True
+
+
+class Python3DependencySystem(SystemDependency):
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ super().__init__(name, environment, kwargs)
+
+ if not environment.machines.matches_build_machine(self.for_machine):
+ return
+ if not environment.machines[self.for_machine].is_windows():
+ return
+
+ self.name = 'python3'
+ # We can only be sure that it is Python 3 at this point
+ self.version = '3'
+ self._find_libpy3_windows(environment)
+
+ @staticmethod
+ def get_windows_python_arch() -> T.Optional[str]:
+ pyplat = sysconfig.get_platform()
+ if pyplat == 'mingw':
+ pycc = sysconfig.get_config_var('CC')
+ if pycc.startswith('x86_64'):
+ return '64'
+ elif pycc.startswith(('i686', 'i386')):
+ return '32'
+ else:
+ mlog.log(f'MinGW Python built with unknown CC {pycc!r}, please file a bug')
+ return None
+ elif pyplat == 'win32':
+ return '32'
+ elif pyplat in {'win64', 'win-amd64'}:
+ return '64'
+ mlog.log(f'Unknown Windows Python platform {pyplat!r}')
+ return None
+
+ def get_windows_link_args(self) -> T.Optional[T.List[str]]:
+ pyplat = sysconfig.get_platform()
+ if pyplat.startswith('win'):
+ vernum = sysconfig.get_config_var('py_version_nodot')
+ if self.static:
+ libpath = Path('libs') / f'libpython{vernum}.a'
+ else:
+ comp = self.get_compiler()
+ if comp.id == "gcc":
+ libpath = Path(f'python{vernum}.dll')
+ else:
+ libpath = Path('libs') / f'python{vernum}.lib'
+ lib = Path(sysconfig.get_config_var('base')) / libpath
+ elif pyplat == 'mingw':
+ if self.static:
+ libname = sysconfig.get_config_var('LIBRARY')
+ else:
+ libname = sysconfig.get_config_var('LDLIBRARY')
+ lib = Path(sysconfig.get_config_var('LIBDIR')) / libname
+ if not lib.exists():
+ mlog.log('Could not find Python3 library {!r}'.format(str(lib)))
+ return None
+ return [str(lib)]
+
+ def _find_libpy3_windows(self, env: 'Environment') -> None:
+ '''
+ Find python3 libraries on Windows and also verify that the arch matches
+ what we are building for.
+ '''
+ pyarch = self.get_windows_python_arch()
+ if pyarch is None:
+ self.is_found = False
+ return
+ arch = detect_cpu_family(env.coredata.compilers.host)
+ if arch == 'x86':
+ arch = '32'
+ elif arch == 'x86_64':
+ arch = '64'
+ else:
+ # We can't cross-compile Python 3 dependencies on Windows yet
+ mlog.log(f'Unknown architecture {arch!r} for',
+ mlog.bold(self.name))
+ self.is_found = False
+ return
+ # Pyarch ends in '32' or '64'
+ if arch != pyarch:
+ mlog.log('Need', mlog.bold(self.name), 'for {}-bit, but '
+ 'found {}-bit'.format(arch, pyarch))
+ self.is_found = False
+ return
+ # This can fail if the library is not found
+ largs = self.get_windows_link_args()
+ if largs is None:
+ self.is_found = False
+ return
+ self.link_args = largs
+ # Compile args
+ inc = sysconfig.get_path('include')
+ platinc = sysconfig.get_path('platinclude')
+ self.compile_args = ['-I' + inc]
+ if inc != platinc:
+ self.compile_args.append('-I' + platinc)
+ self.version = sysconfig.get_config_var('py_version')
+ self.is_found = True
+
+ @staticmethod
+ def log_tried() -> str:
+ return 'sysconfig'
+
+class PcapDependencyConfigTool(ConfigToolDependency):
+
+ tools = ['pcap-config']
+ tool_name = 'pcap-config'
+
+ # version 1.10.2 added error checking for invalid arguments
+ # version 1.10.3 will hopefully add actual support for --version
+ skip_version = '--help'
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, environment, kwargs)
+ if not self.is_found:
+ return
+ self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
+ self.link_args = self.get_config_value(['--libs'], 'link_args')
+ if self.version is None:
+ # older pcap-config versions don't support this
+ self.version = self.get_pcap_lib_version()
+
+ def get_pcap_lib_version(self) -> T.Optional[str]:
+ # Since we seem to need to run a program to discover the pcap version,
+ # we can't do that when cross-compiling
+ # FIXME: this should be handled if we have an exe_wrapper
+ if not self.env.machines.matches_build_machine(self.for_machine):
+ return None
+
+ v = self.clib_compiler.get_return_value('pcap_lib_version', 'string',
+ '#include <pcap.h>', self.env, [], [self])
+ v = re.sub(r'libpcap version ', '', str(v))
+ v = re.sub(r' -- Apple version.*$', '', v)
+ return v
+
+
+class CupsDependencyConfigTool(ConfigToolDependency):
+
+ tools = ['cups-config']
+ tool_name = 'cups-config'
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, environment, kwargs)
+ if not self.is_found:
+ return
+ self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
+ self.link_args = self.get_config_value(['--ldflags', '--libs'], 'link_args')
+
+
+class LibWmfDependencyConfigTool(ConfigToolDependency):
+
+ tools = ['libwmf-config']
+ tool_name = 'libwmf-config'
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, environment, kwargs)
+ if not self.is_found:
+ return
+ self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
+ self.link_args = self.get_config_value(['--libs'], 'link_args')
+
+
+class LibGCryptDependencyConfigTool(ConfigToolDependency):
+
+ tools = ['libgcrypt-config']
+ tool_name = 'libgcrypt-config'
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, environment, kwargs)
+ if not self.is_found:
+ return
+ self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
+ self.link_args = self.get_config_value(['--libs'], 'link_args')
+ self.version = self.get_config_value(['--version'], 'version')[0]
+
+
+class GpgmeDependencyConfigTool(ConfigToolDependency):
+
+ tools = ['gpgme-config']
+ tool_name = 'gpg-config'
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, environment, kwargs)
+ if not self.is_found:
+ return
+ self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
+ self.link_args = self.get_config_value(['--libs'], 'link_args')
+ self.version = self.get_config_value(['--version'], 'version')[0]
+
+
+class ShadercDependency(SystemDependency):
+
+ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__('shaderc', environment, kwargs)
+
+ static_lib = 'shaderc_combined'
+ shared_lib = 'shaderc_shared'
+
+ libs = [shared_lib, static_lib]
+ if self.static:
+ libs.reverse()
+
+ cc = self.get_compiler()
+
+ for lib in libs:
+ self.link_args = cc.find_library(lib, environment, [])
+ if self.link_args is not None:
+ self.is_found = True
+
+ if self.static and lib != static_lib:
+ mlog.warning(f'Static library {static_lib!r} not found for dependency '
+ f'{self.name!r}, may not be statically linked')
+
+ break
+
+
+class CursesConfigToolDependency(ConfigToolDependency):
+
+ """Use the curses config tools."""
+
+ tool = 'curses-config'
+ # ncurses5.4-config is for macOS Catalina
+ tools = ['ncursesw6-config', 'ncursesw5-config', 'ncurses6-config', 'ncurses5-config', 'ncurses5.4-config']
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None):
+ super().__init__(name, env, kwargs, language)
+ if not self.is_found:
+ return
+ self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
+ self.link_args = self.get_config_value(['--libs'], 'link_args')
+
+
+class CursesSystemDependency(SystemDependency):
+
+ """Curses dependency the hard way.
+
+ This replaces hand rolled find_library() and has_header() calls. We
+ provide this for portability reasons, there are a large number of curses
+ implementations, and the differences between them can be very annoying.
+ """
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, env, kwargs)
+
+ candidates = [
+ ('pdcurses', ['pdcurses/curses.h']),
+ ('ncursesw', ['ncursesw/ncurses.h', 'ncurses.h']),
+ ('ncurses', ['ncurses/ncurses.h', 'ncurses/curses.h', 'ncurses.h']),
+ ('curses', ['curses.h']),
+ ]
+
+ # Not sure how else to elegently break out of both loops
+ for lib, headers in candidates:
+ l = self.clib_compiler.find_library(lib, env, [])
+ if l:
+ for header in headers:
+ h = self.clib_compiler.has_header(header, '', env)
+ if h[0]:
+ self.is_found = True
+ self.link_args = l
+ # Not sure how to find version for non-ncurses curses
+ # implementations. The one in illumos/OpenIndiana
+ # doesn't seem to have a version defined in the header.
+ if lib.startswith('ncurses'):
+ v, _ = self.clib_compiler.get_define('NCURSES_VERSION', f'#include <{header}>', env, [], [self])
+ self.version = v.strip('"')
+ if lib.startswith('pdcurses'):
+ v_major, _ = self.clib_compiler.get_define('PDC_VER_MAJOR', f'#include <{header}>', env, [], [self])
+ v_minor, _ = self.clib_compiler.get_define('PDC_VER_MINOR', f'#include <{header}>', env, [], [self])
+ self.version = f'{v_major}.{v_minor}'
+
+ # Check the version if possible, emit a warning if we can't
+ req = kwargs.get('version')
+ if req:
+ if self.version:
+ self.is_found = mesonlib.version_compare(self.version, req)
+ else:
+ mlog.warning('Cannot determine version of curses to compare against.')
+
+ if self.is_found:
+ mlog.debug('Curses library:', l)
+ mlog.debug('Curses header:', header)
+ break
+ if self.is_found:
+ break
+
+
+class IconvBuiltinDependency(BuiltinDependency):
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, env, kwargs)
+ self.feature_since = ('0.60.0', "consider checking for `iconv_open` with and without `find_library('iconv')`")
+ code = '''#include <iconv.h>\n\nint main() {\n iconv_open("","");\n}''' # [ignore encoding] this is C, not python, Mr. Lint
+
+ if self.clib_compiler.links(code, env)[0]:
+ self.is_found = True
+
+
+class IconvSystemDependency(SystemDependency):
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, env, kwargs)
+ self.feature_since = ('0.60.0', "consider checking for `iconv_open` with and without find_library('iconv')")
+
+ h = self.clib_compiler.has_header('iconv.h', '', env)
+ self.link_args = self.clib_compiler.find_library('iconv', env, [], self.libtype)
+
+ if h[0] and self.link_args:
+ self.is_found = True
+
+
+class IntlBuiltinDependency(BuiltinDependency):
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, env, kwargs)
+ self.feature_since = ('0.59.0', "consider checking for `ngettext` with and without `find_library('intl')`")
+ code = '''#include <libintl.h>\n\nint main() {\n gettext("Hello world");\n}'''
+
+ if self.clib_compiler.links(code, env)[0]:
+ self.is_found = True
+
+
+class IntlSystemDependency(SystemDependency):
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, env, kwargs)
+ self.feature_since = ('0.59.0', "consider checking for `ngettext` with and without `find_library('intl')`")
+
+ h = self.clib_compiler.has_header('libintl.h', '', env)
+ self.link_args = self.clib_compiler.find_library('intl', env, [], self.libtype)
+
+ if h[0] and self.link_args:
+ self.is_found = True
+
+ if self.static:
+ if not self._add_sub_dependency(iconv_factory(env, self.for_machine, {'static': True})):
+ self.is_found = False
+ return
+
+
+class OpensslSystemDependency(SystemDependency):
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, env, kwargs)
+
+ dependency_kwargs = {
+ 'method': 'system',
+ 'static': self.static,
+ }
+ if not self.clib_compiler.has_header('openssl/ssl.h', '', env)[0]:
+ return
+
+ # openssl >= 3 only
+ self.version = self.clib_compiler.get_define('OPENSSL_VERSION_STR', '#include <openssl/opensslv.h>', env, [], [self])[0]
+ # openssl < 3 only
+ if not self.version:
+ version_hex = self.clib_compiler.get_define('OPENSSL_VERSION_NUMBER', '#include <openssl/opensslv.h>', env, [], [self])[0]
+ if not version_hex:
+ return
+ version_hex = version_hex.rstrip('L')
+ version_ints = [((int(version_hex.rstrip('L'), 16) >> 4 + i) & 0xFF) for i in (24, 16, 8, 0)]
+ # since this is openssl, the format is 1.2.3a in four parts
+ self.version = '.'.join(str(i) for i in version_ints[:3]) + chr(ord('a') + version_ints[3] - 1)
+
+ if name == 'openssl':
+ if self._add_sub_dependency(libssl_factory(env, self.for_machine, dependency_kwargs)) and \
+ self._add_sub_dependency(libcrypto_factory(env, self.for_machine, dependency_kwargs)):
+ self.is_found = True
+ return
+ else:
+ self.link_args = self.clib_compiler.find_library(name.lstrip('lib'), env, [], self.libtype)
+ if not self.link_args:
+ return
+
+ if not self.static:
+ self.is_found = True
+ else:
+ if name == 'libssl':
+ if self._add_sub_dependency(libcrypto_factory(env, self.for_machine, dependency_kwargs)):
+ self.is_found = True
+ elif name == 'libcrypto':
+ use_threads = self.clib_compiler.has_header_symbol('openssl/opensslconf.h', 'OPENSSL_THREADS', '', env, dependencies=[self])[0]
+ if not use_threads or self._add_sub_dependency(threads_factory(env, self.for_machine, {})):
+ self.is_found = True
+ # only relevant on platforms where it is distributed with the libc, in which case it always succeeds
+ sublib = self.clib_compiler.find_library('dl', env, [], self.libtype)
+ if sublib:
+ self.link_args.extend(sublib)
+
+
+@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.SYSTEM})
+def curses_factory(env: 'Environment',
+ for_machine: 'MachineChoice',
+ kwargs: T.Dict[str, T.Any],
+ methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
+ candidates: T.List['DependencyGenerator'] = []
+
+ if DependencyMethods.PKGCONFIG in methods:
+ pkgconfig_files = ['pdcurses', 'ncursesw', 'ncurses', 'curses']
+ for pkg in pkgconfig_files:
+ candidates.append(functools.partial(PkgConfigDependency, pkg, env, kwargs))
+
+ # There are path handling problems with these methods on msys, and they
+ # don't apply to windows otherwise (cygwin is handled separately from
+ # windows)
+ if not env.machines[for_machine].is_windows():
+ if DependencyMethods.CONFIG_TOOL in methods:
+ candidates.append(functools.partial(CursesConfigToolDependency, 'curses', env, kwargs))
+
+ if DependencyMethods.SYSTEM in methods:
+ candidates.append(functools.partial(CursesSystemDependency, 'curses', env, kwargs))
+
+ return candidates
+
+
+@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM})
+def shaderc_factory(env: 'Environment',
+ for_machine: 'MachineChoice',
+ kwargs: T.Dict[str, T.Any],
+ methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
+ """Custom DependencyFactory for ShaderC.
+
+ ShaderC's odd you get three different libraries from the same build
+ thing are just easier to represent as a separate function than
+ twisting DependencyFactory even more.
+ """
+ candidates: T.List['DependencyGenerator'] = []
+
+ if DependencyMethods.PKGCONFIG in methods:
+ # ShaderC packages their shared and static libs together
+ # and provides different pkg-config files for each one. We
+ # smooth over this difference by handling the static
+ # keyword before handing off to the pkg-config handler.
+ shared_libs = ['shaderc']
+ static_libs = ['shaderc_combined', 'shaderc_static']
+
+ if kwargs.get('static', env.coredata.get_option(mesonlib.OptionKey('prefer_static'))):
+ c = [functools.partial(PkgConfigDependency, name, env, kwargs)
+ for name in static_libs + shared_libs]
+ else:
+ c = [functools.partial(PkgConfigDependency, name, env, kwargs)
+ for name in shared_libs + static_libs]
+ candidates.extend(c)
+
+ if DependencyMethods.SYSTEM in methods:
+ candidates.append(functools.partial(ShadercDependency, env, kwargs))
+
+ return candidates
+
+
+cups_factory = DependencyFactory(
+ 'cups',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK, DependencyMethods.CMAKE],
+ configtool_class=CupsDependencyConfigTool,
+ cmake_name='Cups',
+)
+
+dl_factory = DependencyFactory(
+ 'dl',
+ [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM],
+ builtin_class=DlBuiltinDependency,
+ system_class=DlSystemDependency,
+)
+
+gpgme_factory = DependencyFactory(
+ 'gpgme',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
+ configtool_class=GpgmeDependencyConfigTool,
+)
+
+libgcrypt_factory = DependencyFactory(
+ 'libgcrypt',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
+ configtool_class=LibGCryptDependencyConfigTool,
+)
+
+libwmf_factory = DependencyFactory(
+ 'libwmf',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
+ configtool_class=LibWmfDependencyConfigTool,
+)
+
+pcap_factory = DependencyFactory(
+ 'pcap',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
+ configtool_class=PcapDependencyConfigTool,
+ pkgconfig_name='libpcap',
+)
+
+python3_factory = DependencyFactory(
+ 'python3',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.EXTRAFRAMEWORK],
+ system_class=Python3DependencySystem,
+ # There is no version number in the macOS version number
+ framework_name='Python',
+ # There is a python in /System/Library/Frameworks, but that's python 2.x,
+ # Python 3 will always be in /Library
+ extra_kwargs={'paths': ['/Library/Frameworks']},
+)
+
+threads_factory = DependencyFactory(
+ 'threads',
+ [DependencyMethods.SYSTEM, DependencyMethods.CMAKE],
+ cmake_name='Threads',
+ system_class=ThreadDependency,
+)
+
+iconv_factory = DependencyFactory(
+ 'iconv',
+ [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM],
+ builtin_class=IconvBuiltinDependency,
+ system_class=IconvSystemDependency,
+)
+
+intl_factory = DependencyFactory(
+ 'intl',
+ [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM],
+ builtin_class=IntlBuiltinDependency,
+ system_class=IntlSystemDependency,
+)
+
+openssl_factory = DependencyFactory(
+ 'openssl',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.CMAKE],
+ system_class=OpensslSystemDependency,
+ cmake_class=lambda name, env, kwargs: CMakeDependency('OpenSSL', env, dict(kwargs, modules=['OpenSSL::Crypto', 'OpenSSL::SSL'])),
+)
+
+libcrypto_factory = DependencyFactory(
+ 'libcrypto',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.CMAKE],
+ system_class=OpensslSystemDependency,
+ cmake_class=lambda name, env, kwargs: CMakeDependency('OpenSSL', env, dict(kwargs, modules=['OpenSSL::Crypto'])),
+)
+
+libssl_factory = DependencyFactory(
+ 'libssl',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.CMAKE],
+ system_class=OpensslSystemDependency,
+ cmake_class=lambda name, env, kwargs: CMakeDependency('OpenSSL', env, dict(kwargs, modules=['OpenSSL::SSL'])),
+)
diff --git a/mesonbuild/dependencies/mpi.py b/mesonbuild/dependencies/mpi.py
new file mode 100644
index 0000000..8f83ce4
--- /dev/null
+++ b/mesonbuild/dependencies/mpi.py
@@ -0,0 +1,237 @@
+# Copyright 2013-2019 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.
+from __future__ import annotations
+
+import functools
+import typing as T
+import os
+import re
+
+from ..environment import detect_cpu_family
+from .base import DependencyMethods, detect_compiler, SystemDependency
+from .configtool import ConfigToolDependency
+from .factory import factory_methods
+from .pkgconfig import PkgConfigDependency
+
+if T.TYPE_CHECKING:
+ from .factory import DependencyGenerator
+ from ..environment import Environment, MachineChoice
+
+
+@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.SYSTEM})
+def mpi_factory(env: 'Environment',
+ for_machine: 'MachineChoice',
+ kwargs: T.Dict[str, T.Any],
+ methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
+ language = kwargs.get('language', 'c')
+ if language not in {'c', 'cpp', 'fortran'}:
+ # OpenMPI doesn't work without any other languages
+ return []
+
+ candidates: T.List['DependencyGenerator'] = []
+ compiler = detect_compiler('mpi', env, for_machine, language)
+ if not compiler:
+ return []
+ compiler_is_intel = compiler.get_id() in {'intel', 'intel-cl'}
+
+ # Only OpenMPI has pkg-config, and it doesn't work with the intel compilers
+ if DependencyMethods.PKGCONFIG in methods and not compiler_is_intel:
+ pkg_name = None
+ if language == 'c':
+ pkg_name = 'ompi-c'
+ elif language == 'cpp':
+ pkg_name = 'ompi-cxx'
+ elif language == 'fortran':
+ pkg_name = 'ompi-fort'
+ candidates.append(functools.partial(
+ PkgConfigDependency, pkg_name, env, kwargs, language=language))
+
+ if DependencyMethods.CONFIG_TOOL in methods:
+ nwargs = kwargs.copy()
+
+ if compiler_is_intel:
+ if env.machines[for_machine].is_windows():
+ nwargs['version_arg'] = '-v'
+ nwargs['returncode_value'] = 3
+
+ if language == 'c':
+ tool_names = [os.environ.get('I_MPI_CC'), 'mpiicc']
+ elif language == 'cpp':
+ tool_names = [os.environ.get('I_MPI_CXX'), 'mpiicpc']
+ elif language == 'fortran':
+ tool_names = [os.environ.get('I_MPI_F90'), 'mpiifort']
+
+ cls = IntelMPIConfigToolDependency # type: T.Type[ConfigToolDependency]
+ else: # OpenMPI, which doesn't work with intel
+ #
+ # We try the environment variables for the tools first, but then
+ # fall back to the hardcoded names
+ if language == 'c':
+ tool_names = [os.environ.get('MPICC'), 'mpicc']
+ elif language == 'cpp':
+ tool_names = [os.environ.get('MPICXX'), 'mpic++', 'mpicxx', 'mpiCC']
+ elif language == 'fortran':
+ tool_names = [os.environ.get(e) for e in ['MPIFC', 'MPIF90', 'MPIF77']]
+ tool_names.extend(['mpifort', 'mpif90', 'mpif77'])
+
+ cls = OpenMPIConfigToolDependency
+
+ tool_names = [t for t in tool_names if t] # remove empty environment variables
+ assert tool_names
+
+ nwargs['tools'] = tool_names
+ candidates.append(functools.partial(
+ cls, tool_names[0], env, nwargs, language=language))
+
+ if DependencyMethods.SYSTEM in methods:
+ candidates.append(functools.partial(
+ MSMPIDependency, 'msmpi', env, kwargs, language=language))
+
+ return candidates
+
+
+class _MPIConfigToolDependency(ConfigToolDependency):
+
+ def _filter_compile_args(self, args: T.List[str]) -> T.List[str]:
+ """
+ MPI wrappers return a bunch of garbage args.
+ Drop -O2 and everything that is not needed.
+ """
+ result = []
+ multi_args: T.Tuple[str, ...] = ('-I', )
+ if self.language == 'fortran':
+ fc = self.env.coredata.compilers[self.for_machine]['fortran']
+ multi_args += fc.get_module_incdir_args()
+
+ include_next = False
+ for f in args:
+ if f.startswith(('-D', '-f') + multi_args) or f == '-pthread' \
+ or (f.startswith('-W') and f != '-Wall' and not f.startswith('-Werror')):
+ result.append(f)
+ if f in multi_args:
+ # Path is a separate argument.
+ include_next = True
+ elif include_next:
+ include_next = False
+ result.append(f)
+ return result
+
+ def _filter_link_args(self, args: T.List[str]) -> T.List[str]:
+ """
+ MPI wrappers return a bunch of garbage args.
+ Drop -O2 and everything that is not needed.
+ """
+ result = []
+ include_next = False
+ for f in args:
+ if self._is_link_arg(f):
+ result.append(f)
+ if f in {'-L', '-Xlinker'}:
+ include_next = True
+ elif include_next:
+ include_next = False
+ result.append(f)
+ return result
+
+ def _is_link_arg(self, f: str) -> bool:
+ if self.clib_compiler.id == 'intel-cl':
+ return f == '/link' or f.startswith('/LIBPATH') or f.endswith('.lib') # always .lib whether static or dynamic
+ else:
+ return (f.startswith(('-L', '-l', '-Xlinker')) or
+ f == '-pthread' or
+ (f.startswith('-W') and f != '-Wall' and not f.startswith('-Werror')))
+
+
+class IntelMPIConfigToolDependency(_MPIConfigToolDependency):
+
+ """Wrapper around Intel's mpiicc and friends."""
+
+ version_arg = '-v' # --version is not the same as -v
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
+ language: T.Optional[str] = None):
+ super().__init__(name, env, kwargs, language=language)
+ if not self.is_found:
+ return
+
+ args = self.get_config_value(['-show'], 'link and compile args')
+ self.compile_args = self._filter_compile_args(args)
+ self.link_args = self._filter_link_args(args)
+
+ def _sanitize_version(self, out: str) -> str:
+ v = re.search(r'(\d{4}) Update (\d)', out)
+ if v:
+ return '{}.{}'.format(v.group(1), v.group(2))
+ return out
+
+
+class OpenMPIConfigToolDependency(_MPIConfigToolDependency):
+
+ """Wrapper around OpenMPI mpicc and friends."""
+
+ version_arg = '--showme:version'
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
+ language: T.Optional[str] = None):
+ super().__init__(name, env, kwargs, language=language)
+ if not self.is_found:
+ return
+
+ c_args = self.get_config_value(['--showme:compile'], 'compile_args')
+ self.compile_args = self._filter_compile_args(c_args)
+
+ l_args = self.get_config_value(['--showme:link'], 'link_args')
+ self.link_args = self._filter_link_args(l_args)
+
+ def _sanitize_version(self, out: str) -> str:
+ v = re.search(r'\d+.\d+.\d+', out)
+ if v:
+ return v.group(0)
+ return out
+
+
+class MSMPIDependency(SystemDependency):
+
+ """The Microsoft MPI."""
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
+ language: T.Optional[str] = None):
+ super().__init__(name, env, kwargs, language=language)
+ # MSMPI only supports the C API
+ if language not in {'c', 'fortran', None}:
+ self.is_found = False
+ return
+ # MSMPI is only for windows, obviously
+ if not self.env.machines[self.for_machine].is_windows():
+ return
+
+ incdir = os.environ.get('MSMPI_INC')
+ arch = detect_cpu_family(self.env.coredata.compilers.host)
+ libdir = None
+ if arch == 'x86':
+ libdir = os.environ.get('MSMPI_LIB32')
+ post = 'x86'
+ elif arch == 'x86_64':
+ libdir = os.environ.get('MSMPI_LIB64')
+ post = 'x64'
+
+ if libdir is None or incdir is None:
+ self.is_found = False
+ return
+
+ self.is_found = True
+ self.link_args = ['-l' + os.path.join(libdir, 'msmpi')]
+ self.compile_args = ['-I' + incdir, '-I' + os.path.join(incdir, post)]
+ if self.language == 'fortran':
+ self.link_args.append('-l' + os.path.join(libdir, 'msmpifec'))
diff --git a/mesonbuild/dependencies/pkgconfig.py b/mesonbuild/dependencies/pkgconfig.py
new file mode 100644
index 0000000..76dc3ef
--- /dev/null
+++ b/mesonbuild/dependencies/pkgconfig.py
@@ -0,0 +1,505 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+from pathlib import Path
+
+from .base import ExternalDependency, DependencyException, sort_libpaths, DependencyTypeName
+from ..mesonlib import OptionKey, OrderedSet, PerMachine, Popen_safe
+from ..programs import find_external_program, ExternalProgram
+from .. import mlog
+from pathlib import PurePath
+import re
+import os
+import shlex
+import typing as T
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+ from ..mesonlib import MachineChoice
+ from .._typing import ImmutableListProtocol
+ from ..build import EnvironmentVariables
+
+class PkgConfigDependency(ExternalDependency):
+ # The class's copy of the pkg-config path. Avoids having to search for it
+ # multiple times in the same Meson invocation.
+ class_pkgbin: PerMachine[T.Union[None, bool, ExternalProgram]] = PerMachine(None, None)
+ # We cache all pkg-config subprocess invocations to avoid redundant calls
+ pkgbin_cache: T.Dict[
+ T.Tuple[ExternalProgram, T.Tuple[str, ...], T.FrozenSet[T.Tuple[str, str]]],
+ T.Tuple[int, str, str]
+ ] = {}
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
+ super().__init__(DependencyTypeName('pkgconfig'), environment, kwargs, language=language)
+ self.name = name
+ self.is_libtool = False
+ # Store a copy of the pkg-config path on the object itself so it is
+ # stored in the pickled coredata and recovered.
+ self.pkgbin = self._detect_pkgbin(self.silent, self.env, self.for_machine)
+ if self.pkgbin is False:
+ self.pkgbin = None
+ msg = f'Pkg-config binary for machine {self.for_machine} not found. Giving up.'
+ if self.required:
+ raise DependencyException(msg)
+ else:
+ mlog.debug(msg)
+ return
+
+ assert isinstance(self.pkgbin, ExternalProgram)
+ mlog.debug('Determining dependency {!r} with pkg-config executable '
+ '{!r}'.format(name, self.pkgbin.get_path()))
+ ret, self.version, _ = self._call_pkgbin(['--modversion', name])
+ if ret != 0:
+ return
+
+ self.is_found = True
+
+ try:
+ # Fetch cargs to be used while using this dependency
+ self._set_cargs()
+ # Fetch the libraries and library paths needed for using this
+ self._set_libs()
+ except DependencyException as e:
+ mlog.debug(f"pkg-config error with '{name}': {e}")
+ if self.required:
+ raise
+ else:
+ self.compile_args = []
+ self.link_args = []
+ self.is_found = False
+ self.reason = e
+
+ def __repr__(self) -> str:
+ s = '<{0} {1}: {2} {3}>'
+ return s.format(self.__class__.__name__, self.name, self.is_found,
+ self.version_reqs)
+
+ @classmethod
+ def _detect_pkgbin(cls, silent: bool, env: Environment,
+ for_machine: MachineChoice) -> T.Union[None, bool, ExternalProgram]:
+ # Only search for pkg-config for each machine the first time and store
+ # the result in the class definition
+ if cls.class_pkgbin[for_machine] is False:
+ mlog.debug(f'Pkg-config binary for {for_machine} is cached as not found.')
+ elif cls.class_pkgbin[for_machine] is not None:
+ mlog.debug(f'Pkg-config binary for {for_machine} is cached.')
+ else:
+ assert cls.class_pkgbin[for_machine] is None, 'for mypy'
+ mlog.debug(f'Pkg-config binary for {for_machine} is not cached.')
+ for potential_pkgbin in find_external_program(
+ env, for_machine, 'pkgconfig', 'Pkg-config',
+ env.default_pkgconfig, allow_default_for_cross=False):
+ version_if_ok = cls.check_pkgconfig(env, potential_pkgbin)
+ if not version_if_ok:
+ continue
+ if not silent:
+ mlog.log('Found pkg-config:', mlog.bold(potential_pkgbin.get_path()),
+ f'({version_if_ok})')
+ cls.class_pkgbin[for_machine] = potential_pkgbin
+ break
+ else:
+ if not silent:
+ mlog.log('Found Pkg-config:', mlog.red('NO'))
+ # Set to False instead of None to signify that we've already
+ # searched for it and not found it
+ cls.class_pkgbin[for_machine] = False
+
+ return cls.class_pkgbin[for_machine]
+
+ def _call_pkgbin_real(self, args: T.List[str], env: T.Dict[str, str]) -> T.Tuple[int, str, str]:
+ assert isinstance(self.pkgbin, ExternalProgram)
+ cmd = self.pkgbin.get_command() + args
+ p, out, err = Popen_safe(cmd, env=env)
+ rc, out, err = p.returncode, out.strip(), err.strip()
+ call = ' '.join(cmd)
+ mlog.debug(f"Called `{call}` -> {rc}")
+ if out:
+ mlog.debug(f'stdout:\n{out}\n-----------')
+ if err:
+ mlog.debug(f'stderr:\n{err}\n-----------')
+ return rc, out, err
+
+ @staticmethod
+ def get_env(environment: 'Environment', for_machine: MachineChoice,
+ uninstalled: bool = False) -> 'EnvironmentVariables':
+ from ..build import EnvironmentVariables
+ env = EnvironmentVariables()
+ key = OptionKey('pkg_config_path', machine=for_machine)
+ extra_paths: T.List[str] = environment.coredata.options[key].value[:]
+ if uninstalled:
+ uninstalled_path = Path(environment.get_build_dir(), 'meson-uninstalled').as_posix()
+ if uninstalled_path not in extra_paths:
+ extra_paths.append(uninstalled_path)
+ env.set('PKG_CONFIG_PATH', extra_paths)
+ sysroot = environment.properties[for_machine].get_sys_root()
+ if sysroot:
+ env.set('PKG_CONFIG_SYSROOT_DIR', [sysroot])
+ pkg_config_libdir_prop = environment.properties[for_machine].get_pkg_config_libdir()
+ if pkg_config_libdir_prop:
+ env.set('PKG_CONFIG_LIBDIR', pkg_config_libdir_prop)
+ return env
+
+ @staticmethod
+ def setup_env(env: T.MutableMapping[str, str], environment: 'Environment', for_machine: MachineChoice,
+ uninstalled: bool = False) -> T.Dict[str, str]:
+ envvars = PkgConfigDependency.get_env(environment, for_machine, uninstalled)
+ env = envvars.get_env(env)
+ # Dump all PKG_CONFIG environment variables
+ for key, value in env.items():
+ if key.startswith('PKG_'):
+ mlog.debug(f'env[{key}]: {value}')
+ return env
+
+ def _call_pkgbin(self, args: T.List[str], env: T.Optional[T.MutableMapping[str, str]] = None) -> T.Tuple[int, str, str]:
+ assert isinstance(self.pkgbin, ExternalProgram)
+ env = env or os.environ
+ env = PkgConfigDependency.setup_env(env, self.env, self.for_machine)
+
+ fenv = frozenset(env.items())
+ targs = tuple(args)
+ cache = PkgConfigDependency.pkgbin_cache
+ if (self.pkgbin, targs, fenv) not in cache:
+ cache[(self.pkgbin, targs, fenv)] = self._call_pkgbin_real(args, env)
+ return cache[(self.pkgbin, targs, fenv)]
+
+ def _convert_mingw_paths(self, args: T.List[str]) -> T.List[str]:
+ '''
+ Both MSVC and native Python on Windows cannot handle MinGW-esque /c/foo
+ paths so convert them to C:/foo. We cannot resolve other paths starting
+ with / like /home/foo so leave them as-is so that the user gets an
+ error/warning from the compiler/linker.
+ '''
+ if not self.env.machines.build.is_windows():
+ return args
+ converted = []
+ for arg in args:
+ pargs: T.Tuple[str, ...] = tuple()
+ # Library search path
+ if arg.startswith('-L/'):
+ pargs = PurePath(arg[2:]).parts
+ tmpl = '-L{}:/{}'
+ elif arg.startswith('-I/'):
+ pargs = PurePath(arg[2:]).parts
+ tmpl = '-I{}:/{}'
+ # Full path to library or .la file
+ elif arg.startswith('/'):
+ pargs = PurePath(arg).parts
+ tmpl = '{}:/{}'
+ elif arg.startswith(('-L', '-I')) or (len(arg) > 2 and arg[1] == ':'):
+ # clean out improper '\\ ' as comes from some Windows pkg-config files
+ arg = arg.replace('\\ ', ' ')
+ if len(pargs) > 1 and len(pargs[1]) == 1:
+ arg = tmpl.format(pargs[1], '/'.join(pargs[2:]))
+ converted.append(arg)
+ return converted
+
+ def _split_args(self, cmd: str) -> T.List[str]:
+ # pkg-config paths follow Unix conventions, even on Windows; split the
+ # output using shlex.split rather than mesonlib.split_args
+ return shlex.split(cmd)
+
+ def _set_cargs(self) -> None:
+ env = None
+ if self.language == 'fortran':
+ # gfortran doesn't appear to look in system paths for INCLUDE files,
+ # so don't allow pkg-config to suppress -I flags for system paths
+ env = os.environ.copy()
+ env['PKG_CONFIG_ALLOW_SYSTEM_CFLAGS'] = '1'
+ ret, out, err = self._call_pkgbin(['--cflags', self.name], env=env)
+ if ret != 0:
+ raise DependencyException(f'Could not generate cargs for {self.name}:\n{err}\n')
+ self.compile_args = self._convert_mingw_paths(self._split_args(out))
+
+ def _search_libs(self, out: str, out_raw: str) -> T.Tuple[T.List[str], T.List[str]]:
+ '''
+ @out: PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs
+ @out_raw: pkg-config --libs
+
+ We always look for the file ourselves instead of depending on the
+ compiler to find it with -lfoo or foo.lib (if possible) because:
+ 1. We want to be able to select static or shared
+ 2. We need the full path of the library to calculate RPATH values
+ 3. De-dup of libraries is easier when we have absolute paths
+
+ Libraries that are provided by the toolchain or are not found by
+ find_library() will be added with -L -l pairs.
+ '''
+ # Library paths should be safe to de-dup
+ #
+ # First, figure out what library paths to use. Originally, we were
+ # doing this as part of the loop, but due to differences in the order
+ # of -L values between pkg-config and pkgconf, we need to do that as
+ # a separate step. See:
+ # https://github.com/mesonbuild/meson/issues/3951
+ # https://github.com/mesonbuild/meson/issues/4023
+ #
+ # Separate system and prefix paths, and ensure that prefix paths are
+ # always searched first.
+ prefix_libpaths: OrderedSet[str] = OrderedSet()
+ # We also store this raw_link_args on the object later
+ raw_link_args = self._convert_mingw_paths(self._split_args(out_raw))
+ for arg in raw_link_args:
+ if arg.startswith('-L') and not arg.startswith(('-L-l', '-L-L')):
+ path = arg[2:]
+ if not os.path.isabs(path):
+ # Resolve the path as a compiler in the build directory would
+ path = os.path.join(self.env.get_build_dir(), path)
+ prefix_libpaths.add(path)
+ # Library paths are not always ordered in a meaningful way
+ #
+ # Instead of relying on pkg-config or pkgconf to provide -L flags in a
+ # specific order, we reorder library paths ourselves, according to th
+ # order specified in PKG_CONFIG_PATH. See:
+ # https://github.com/mesonbuild/meson/issues/4271
+ #
+ # Only prefix_libpaths are reordered here because there should not be
+ # too many system_libpaths to cause library version issues.
+ pkg_config_path: T.List[str] = self.env.coredata.options[OptionKey('pkg_config_path', machine=self.for_machine)].value
+ pkg_config_path = self._convert_mingw_paths(pkg_config_path)
+ prefix_libpaths = OrderedSet(sort_libpaths(list(prefix_libpaths), pkg_config_path))
+ system_libpaths: OrderedSet[str] = OrderedSet()
+ full_args = self._convert_mingw_paths(self._split_args(out))
+ for arg in full_args:
+ if arg.startswith(('-L-l', '-L-L')):
+ # These are D language arguments, not library paths
+ continue
+ if arg.startswith('-L') and arg[2:] not in prefix_libpaths:
+ system_libpaths.add(arg[2:])
+ # Use this re-ordered path list for library resolution
+ libpaths = list(prefix_libpaths) + list(system_libpaths)
+ # Track -lfoo libraries to avoid duplicate work
+ libs_found: OrderedSet[str] = OrderedSet()
+ # Track not-found libraries to know whether to add library paths
+ libs_notfound = []
+ # Generate link arguments for this library
+ link_args = []
+ for lib in full_args:
+ if lib.startswith(('-L-l', '-L-L')):
+ # These are D language arguments, add them as-is
+ pass
+ elif lib.startswith('-L'):
+ # We already handled library paths above
+ continue
+ elif lib.startswith('-l:'):
+ # see: https://stackoverflow.com/questions/48532868/gcc-library-option-with-a-colon-llibevent-a
+ # also : See the documentation of -lnamespec | --library=namespec in the linker manual
+ # https://sourceware.org/binutils/docs-2.18/ld/Options.html
+
+ # Don't resolve the same -l:libfoo.a argument again
+ if lib in libs_found:
+ continue
+ libfilename = lib[3:]
+ foundname = None
+ for libdir in libpaths:
+ target = os.path.join(libdir, libfilename)
+ if os.path.exists(target):
+ foundname = target
+ break
+ if foundname is None:
+ if lib in libs_notfound:
+ continue
+ else:
+ mlog.warning('Library {!r} not found for dependency {!r}, may '
+ 'not be successfully linked'.format(libfilename, self.name))
+ libs_notfound.append(lib)
+ else:
+ lib = foundname
+ elif lib.startswith('-l'):
+ # Don't resolve the same -lfoo argument again
+ if lib in libs_found:
+ continue
+ if self.clib_compiler:
+ args = self.clib_compiler.find_library(lib[2:], self.env,
+ libpaths, self.libtype)
+ # If the project only uses a non-clib language such as D, Rust,
+ # C#, Python, etc, all we can do is limp along by adding the
+ # arguments as-is and then adding the libpaths at the end.
+ else:
+ args = None
+ if args is not None:
+ libs_found.add(lib)
+ # Replace -l arg with full path to library if available
+ # else, library is either to be ignored, or is provided by
+ # the compiler, can't be resolved, and should be used as-is
+ if args:
+ if not args[0].startswith('-l'):
+ lib = args[0]
+ else:
+ continue
+ else:
+ # Library wasn't found, maybe we're looking in the wrong
+ # places or the library will be provided with LDFLAGS or
+ # LIBRARY_PATH from the environment (on macOS), and many
+ # other edge cases that we can't account for.
+ #
+ # Add all -L paths and use it as -lfoo
+ if lib in libs_notfound:
+ continue
+ if self.static:
+ mlog.warning('Static library {!r} not found for dependency {!r}, may '
+ 'not be statically linked'.format(lib[2:], self.name))
+ libs_notfound.append(lib)
+ elif lib.endswith(".la"):
+ shared_libname = self.extract_libtool_shlib(lib)
+ shared_lib = os.path.join(os.path.dirname(lib), shared_libname)
+ if not os.path.exists(shared_lib):
+ shared_lib = os.path.join(os.path.dirname(lib), ".libs", shared_libname)
+
+ if not os.path.exists(shared_lib):
+ raise DependencyException(f'Got a libtools specific "{lib}" dependencies'
+ 'but we could not compute the actual shared'
+ 'library path')
+ self.is_libtool = True
+ lib = shared_lib
+ if lib in link_args:
+ continue
+ link_args.append(lib)
+ # Add all -Lbar args if we have -lfoo args in link_args
+ if libs_notfound:
+ # Order of -L flags doesn't matter with ld, but it might with other
+ # linkers such as MSVC, so prepend them.
+ link_args = ['-L' + lp for lp in prefix_libpaths] + link_args
+ return link_args, raw_link_args
+
+ def _set_libs(self) -> None:
+ env = None
+ libcmd = ['--libs']
+
+ if self.static:
+ libcmd.append('--static')
+
+ libcmd.append(self.name)
+
+ # Force pkg-config to output -L fields even if they are system
+ # paths so we can do manual searching with cc.find_library() later.
+ env = os.environ.copy()
+ env['PKG_CONFIG_ALLOW_SYSTEM_LIBS'] = '1'
+ ret, out, err = self._call_pkgbin(libcmd, env=env)
+ if ret != 0:
+ raise DependencyException(f'Could not generate libs for {self.name}:\n{err}\n')
+ # Also get the 'raw' output without -Lfoo system paths for adding -L
+ # args with -lfoo when a library can't be found, and also in
+ # gnome.generate_gir + gnome.gtkdoc which need -L -l arguments.
+ ret, out_raw, err_raw = self._call_pkgbin(libcmd)
+ if ret != 0:
+ raise DependencyException(f'Could not generate libs for {self.name}:\n\n{out_raw}')
+ self.link_args, self.raw_link_args = self._search_libs(out, out_raw)
+
+ def get_pkgconfig_variable(self, variable_name: str,
+ define_variable: 'ImmutableListProtocol[str]',
+ default: T.Optional[str]) -> str:
+ options = ['--variable=' + variable_name, self.name]
+
+ if define_variable:
+ options = ['--define-variable=' + '='.join(define_variable)] + options
+
+ ret, out, err = self._call_pkgbin(options)
+ variable = ''
+ if ret != 0:
+ if self.required:
+ raise DependencyException(f'dependency {self.name} not found:\n{err}\n')
+ else:
+ variable = out.strip()
+
+ # pkg-config doesn't distinguish between empty and non-existent variables
+ # use the variable list to check for variable existence
+ if not variable:
+ ret, out, _ = self._call_pkgbin(['--print-variables', self.name])
+ if not re.search(r'^' + variable_name + r'$', out, re.MULTILINE):
+ if default is not None:
+ variable = default
+ else:
+ mlog.warning(f"pkgconfig variable '{variable_name}' not defined for dependency {self.name}.")
+
+ mlog.debug(f'Got pkgconfig variable {variable_name} : {variable}')
+ return variable
+
+ @staticmethod
+ def check_pkgconfig(env: Environment, pkgbin: ExternalProgram) -> T.Optional[str]:
+ if not pkgbin.found():
+ mlog.log(f'Did not find pkg-config by name {pkgbin.name!r}')
+ return None
+ command_as_string = ' '.join(pkgbin.get_command())
+ try:
+ helptext = Popen_safe(pkgbin.get_command() + ['--help'])[1]
+ if 'Pure-Perl' in helptext:
+ mlog.log(f'found pkg-config {command_as_string!r} but it is Strawberry Perl and thus broken. Ignoring...')
+ return None
+ p, out = Popen_safe(pkgbin.get_command() + ['--version'])[0:2]
+ if p.returncode != 0:
+ mlog.warning(f'Found pkg-config {command_as_string!r} but it failed when run')
+ return None
+ except FileNotFoundError:
+ mlog.warning(f'We thought we found pkg-config {command_as_string!r} but now it\'s not there. How odd!')
+ return None
+ except PermissionError:
+ msg = f'Found pkg-config {command_as_string!r} but didn\'t have permissions to run it.'
+ if not env.machines.build.is_windows():
+ msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.'
+ mlog.warning(msg)
+ return None
+ return out.strip()
+
+ def extract_field(self, la_file: str, fieldname: str) -> T.Optional[str]:
+ with open(la_file, encoding='utf-8') as f:
+ for line in f:
+ arr = line.strip().split('=')
+ if arr[0] == fieldname:
+ return arr[1][1:-1]
+ return None
+
+ def extract_dlname_field(self, la_file: str) -> T.Optional[str]:
+ return self.extract_field(la_file, 'dlname')
+
+ def extract_libdir_field(self, la_file: str) -> T.Optional[str]:
+ return self.extract_field(la_file, 'libdir')
+
+ def extract_libtool_shlib(self, la_file: str) -> T.Optional[str]:
+ '''
+ Returns the path to the shared library
+ corresponding to this .la file
+ '''
+ dlname = self.extract_dlname_field(la_file)
+ if dlname is None:
+ return None
+
+ # Darwin uses absolute paths where possible; since the libtool files never
+ # contain absolute paths, use the libdir field
+ if self.env.machines[self.for_machine].is_darwin():
+ dlbasename = os.path.basename(dlname)
+ libdir = self.extract_libdir_field(la_file)
+ if libdir is None:
+ return dlbasename
+ return os.path.join(libdir, dlbasename)
+ # From the comments in extract_libtool(), older libtools had
+ # a path rather than the raw dlname
+ return os.path.basename(dlname)
+
+ @staticmethod
+ def log_tried() -> str:
+ return 'pkgconfig'
+
+ def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
+ configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
+ default_value: T.Optional[str] = None,
+ pkgconfig_define: T.Optional[T.List[str]] = None) -> str:
+ if pkgconfig:
+ try:
+ return self.get_pkgconfig_variable(pkgconfig, pkgconfig_define or [], default_value)
+ except DependencyException:
+ pass
+ if default_value is not None:
+ return default_value
+ raise DependencyException(f'Could not get pkg-config variable and no default provided for {self!r}')
diff --git a/mesonbuild/dependencies/platform.py b/mesonbuild/dependencies/platform.py
new file mode 100644
index 0000000..6d32555
--- /dev/null
+++ b/mesonbuild/dependencies/platform.py
@@ -0,0 +1,60 @@
+# Copyright 2013-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.
+
+# This file contains the detection logic for external dependencies that are
+# platform-specific (generally speaking).
+from __future__ import annotations
+
+from .base import DependencyTypeName, ExternalDependency, DependencyException
+from ..mesonlib import MesonException
+import typing as T
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+
+class AppleFrameworks(ExternalDependency):
+ def __init__(self, env: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ super().__init__(DependencyTypeName('appleframeworks'), env, kwargs)
+ modules = kwargs.get('modules', [])
+ if isinstance(modules, str):
+ modules = [modules]
+ if not modules:
+ raise DependencyException("AppleFrameworks dependency requires at least one module.")
+ self.frameworks = modules
+ if not self.clib_compiler:
+ raise DependencyException('No C-like compilers are available, cannot find the framework')
+ self.is_found = True
+ for f in self.frameworks:
+ try:
+ args = self.clib_compiler.find_framework(f, env, [])
+ except MesonException as e:
+ if 'non-clang' in str(e):
+ self.is_found = False
+ self.link_args = []
+ self.compile_args = []
+ return
+ raise
+
+ if args is not None:
+ # No compile args are needed for system frameworks
+ self.link_args += args
+ else:
+ self.is_found = False
+
+ def log_info(self) -> str:
+ return ', '.join(self.frameworks)
+
+ @staticmethod
+ def log_tried() -> str:
+ return 'framework'
diff --git a/mesonbuild/dependencies/qt.py b/mesonbuild/dependencies/qt.py
new file mode 100644
index 0000000..6dd712d
--- /dev/null
+++ b/mesonbuild/dependencies/qt.py
@@ -0,0 +1,486 @@
+# Copyright 2013-2017 The Meson development team
+# Copyright © 2021 Intel Corporation
+# SPDX-license-identifier: Apache-2.0
+
+# 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.
+from __future__ import annotations
+
+"""Dependency finders for the Qt framework."""
+
+import abc
+import re
+import os
+import typing as T
+
+from .base import DependencyException, DependencyMethods
+from .configtool import ConfigToolDependency
+from .framework import ExtraFrameworkDependency
+from .pkgconfig import PkgConfigDependency
+from .factory import DependencyFactory
+from .. import mlog
+from .. import mesonlib
+
+if T.TYPE_CHECKING:
+ from ..compilers import Compiler
+ from ..envconfig import MachineInfo
+ from ..environment import Environment
+ from ..dependencies import MissingCompiler
+
+
+def _qt_get_private_includes(mod_inc_dir: str, module: str, mod_version: str) -> T.List[str]:
+ # usually Qt5 puts private headers in /QT_INSTALL_HEADERS/module/VERSION/module/private
+ # except for at least QtWebkit and Enginio where the module version doesn't match Qt version
+ # as an example with Qt 5.10.1 on linux you would get:
+ # /usr/include/qt5/QtCore/5.10.1/QtCore/private/
+ # /usr/include/qt5/QtWidgets/5.10.1/QtWidgets/private/
+ # /usr/include/qt5/QtWebKit/5.212.0/QtWebKit/private/
+
+ # on Qt4 when available private folder is directly in module folder
+ # like /usr/include/QtCore/private/
+ if int(mod_version.split('.')[0]) < 5:
+ return []
+
+ private_dir = os.path.join(mod_inc_dir, mod_version)
+ # fallback, let's try to find a directory with the latest version
+ if not os.path.exists(private_dir):
+ dirs = [filename for filename in os.listdir(mod_inc_dir)
+ if os.path.isdir(os.path.join(mod_inc_dir, filename))]
+
+ for dirname in sorted(dirs, reverse=True):
+ if len(dirname.split('.')) == 3:
+ private_dir = dirname
+ break
+ return [private_dir, os.path.join(private_dir, 'Qt' + module)]
+
+
+def get_qmake_host_bins(qvars: T.Dict[str, str]) -> str:
+ # Prefer QT_HOST_BINS (qt5, correct for cross and native compiling)
+ # but fall back to QT_INSTALL_BINS (qt4)
+ if 'QT_HOST_BINS' in qvars:
+ return qvars['QT_HOST_BINS']
+ return qvars['QT_INSTALL_BINS']
+
+
+def get_qmake_host_libexecs(qvars: T.Dict[str, str]) -> T.Optional[str]:
+ if 'QT_HOST_LIBEXECS' in qvars:
+ return qvars['QT_HOST_LIBEXECS']
+ return qvars.get('QT_INSTALL_LIBEXECS')
+
+
+def _get_modules_lib_suffix(version: str, info: 'MachineInfo', is_debug: bool) -> str:
+ """Get the module suffix based on platform and debug type."""
+ suffix = ''
+ if info.is_windows():
+ if is_debug:
+ suffix += 'd'
+ if version.startswith('4'):
+ suffix += '4'
+ if info.is_darwin():
+ if is_debug:
+ suffix += '_debug'
+ if mesonlib.version_compare(version, '>= 5.14.0'):
+ if info.is_android():
+ if info.cpu_family == 'x86':
+ suffix += '_x86'
+ elif info.cpu_family == 'x86_64':
+ suffix += '_x86_64'
+ elif info.cpu_family == 'arm':
+ suffix += '_armeabi-v7a'
+ elif info.cpu_family == 'aarch64':
+ suffix += '_arm64-v8a'
+ else:
+ mlog.warning(f'Android target arch "{info.cpu_family}"" for Qt5 is unknown, '
+ 'module detection may not work')
+ return suffix
+
+
+class QtExtraFrameworkDependency(ExtraFrameworkDependency):
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None):
+ super().__init__(name, env, kwargs, language=language)
+ self.mod_name = name[2:]
+
+ def get_compile_args(self, with_private_headers: bool = False, qt_version: str = "0") -> T.List[str]:
+ if self.found():
+ mod_inc_dir = os.path.join(self.framework_path, 'Headers')
+ args = ['-I' + mod_inc_dir]
+ if with_private_headers:
+ args += ['-I' + dirname for dirname in _qt_get_private_includes(mod_inc_dir, self.mod_name, qt_version)]
+ return args
+ return []
+
+
+class _QtBase:
+
+ """Mixin class for shared components between PkgConfig and Qmake."""
+
+ link_args: T.List[str]
+ clib_compiler: T.Union['MissingCompiler', 'Compiler']
+ env: 'Environment'
+ libexecdir: T.Optional[str] = None
+
+ def __init__(self, name: str, kwargs: T.Dict[str, T.Any]):
+ self.name = name
+ self.qtname = name.capitalize()
+ self.qtver = name[-1]
+ if self.qtver == "4":
+ self.qtpkgname = 'Qt'
+ else:
+ self.qtpkgname = self.qtname
+
+ self.private_headers = T.cast('bool', kwargs.get('private_headers', False))
+
+ self.requested_modules = mesonlib.stringlistify(mesonlib.extract_as_list(kwargs, 'modules'))
+ if not self.requested_modules:
+ raise DependencyException('No ' + self.qtname + ' modules specified.')
+
+ self.qtmain = T.cast('bool', kwargs.get('main', False))
+ if not isinstance(self.qtmain, bool):
+ raise DependencyException('"main" argument must be a boolean')
+
+ def _link_with_qt_winmain(self, is_debug: bool, libdir: T.Union[str, T.List[str]]) -> bool:
+ libdir = mesonlib.listify(libdir) # TODO: shouldn't be necessary
+ base_name = self.get_qt_winmain_base_name(is_debug)
+ qt_winmain = self.clib_compiler.find_library(base_name, self.env, libdir)
+ if qt_winmain:
+ self.link_args.append(qt_winmain[0])
+ return True
+ return False
+
+ def get_qt_winmain_base_name(self, is_debug: bool) -> str:
+ return 'qtmaind' if is_debug else 'qtmain'
+
+ def get_exe_args(self, compiler: 'Compiler') -> T.List[str]:
+ # Originally this was -fPIE but nowadays the default
+ # for upstream and distros seems to be -reduce-relocations
+ # which requires -fPIC. This may cause a performance
+ # penalty when using self-built Qt or on platforms
+ # where -fPIC is not required. If this is an issue
+ # for you, patches are welcome.
+ return compiler.get_pic_args()
+
+ def log_details(self) -> str:
+ return f'modules: {", ".join(sorted(self.requested_modules))}'
+
+
+class QtPkgConfigDependency(_QtBase, PkgConfigDependency, metaclass=abc.ABCMeta):
+
+ """Specialization of the PkgConfigDependency for Qt."""
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ _QtBase.__init__(self, name, kwargs)
+
+ # Always use QtCore as the "main" dependency, since it has the extra
+ # pkg-config variables that a user would expect to get. If "Core" is
+ # not a requested module, delete the compile and link arguments to
+ # avoid linking with something they didn't ask for
+ PkgConfigDependency.__init__(self, self.qtpkgname + 'Core', env, kwargs)
+ if 'Core' not in self.requested_modules:
+ self.compile_args = []
+ self.link_args = []
+
+ for m in self.requested_modules:
+ mod = PkgConfigDependency(self.qtpkgname + m, self.env, kwargs, language=self.language)
+ if not mod.found():
+ self.is_found = False
+ return
+ if self.private_headers:
+ qt_inc_dir = mod.get_pkgconfig_variable('includedir', [], None)
+ mod_private_dir = os.path.join(qt_inc_dir, 'Qt' + m)
+ if not os.path.isdir(mod_private_dir):
+ # At least some versions of homebrew don't seem to set this
+ # up correctly. /usr/local/opt/qt/include/Qt + m_name is a
+ # symlink to /usr/local/opt/qt/include, but the pkg-config
+ # file points to /usr/local/Cellar/qt/x.y.z/Headers/, and
+ # the Qt + m_name there is not a symlink, it's a file
+ mod_private_dir = qt_inc_dir
+ mod_private_inc = _qt_get_private_includes(mod_private_dir, m, mod.version)
+ for directory in mod_private_inc:
+ mod.compile_args.append('-I' + directory)
+ self._add_sub_dependency([lambda: mod])
+
+ if self.env.machines[self.for_machine].is_windows() and self.qtmain:
+ # Check if we link with debug binaries
+ debug_lib_name = self.qtpkgname + 'Core' + _get_modules_lib_suffix(self.version, self.env.machines[self.for_machine], True)
+ is_debug = False
+ for arg in self.get_link_args():
+ if arg == f'-l{debug_lib_name}' or arg.endswith(f'{debug_lib_name}.lib') or arg.endswith(f'{debug_lib_name}.a'):
+ is_debug = True
+ break
+ libdir = self.get_pkgconfig_variable('libdir', [], None)
+ if not self._link_with_qt_winmain(is_debug, libdir):
+ self.is_found = False
+ return
+
+ self.bindir = self.get_pkgconfig_host_bins(self)
+ if not self.bindir:
+ # If exec_prefix is not defined, the pkg-config file is broken
+ prefix = self.get_pkgconfig_variable('exec_prefix', [], None)
+ if prefix:
+ self.bindir = os.path.join(prefix, 'bin')
+
+ self.libexecdir = self.get_pkgconfig_host_libexecs(self)
+
+ @staticmethod
+ @abc.abstractmethod
+ def get_pkgconfig_host_bins(core: PkgConfigDependency) -> T.Optional[str]:
+ pass
+
+ @staticmethod
+ @abc.abstractmethod
+ def get_pkgconfig_host_libexecs(core: PkgConfigDependency) -> T.Optional[str]:
+ pass
+
+ @abc.abstractmethod
+ def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
+ pass
+
+ def log_info(self) -> str:
+ return 'pkg-config'
+
+
+class QmakeQtDependency(_QtBase, ConfigToolDependency, metaclass=abc.ABCMeta):
+
+ """Find Qt using Qmake as a config-tool."""
+
+ tool_name = 'qmake'
+ version_arg = '-v'
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ _QtBase.__init__(self, name, kwargs)
+ self.tools = [f'qmake{self.qtver}', f'qmake-{self.name}', 'qmake']
+
+ # Add additional constraints that the Qt version is met, but preserve
+ # any version requrements the user has set as well. For example, if Qt5
+ # is requested, add "">= 5, < 6", but if the user has ">= 5.6", don't
+ # lose that.
+ kwargs = kwargs.copy()
+ _vers = mesonlib.listify(kwargs.get('version', []))
+ _vers.extend([f'>= {self.qtver}', f'< {int(self.qtver) + 1}'])
+ kwargs['version'] = _vers
+
+ ConfigToolDependency.__init__(self, name, env, kwargs)
+ if not self.found():
+ return
+
+ # Query library path, header path, and binary path
+ stdo = self.get_config_value(['-query'], 'args')
+ qvars: T.Dict[str, str] = {}
+ for line in stdo:
+ line = line.strip()
+ if line == '':
+ continue
+ k, v = line.split(':', 1)
+ qvars[k] = v
+ # Qt on macOS uses a framework, but Qt for iOS/tvOS does not
+ xspec = qvars.get('QMAKE_XSPEC', '')
+ if self.env.machines.host.is_darwin() and not any(s in xspec for s in ['ios', 'tvos']):
+ mlog.debug("Building for macOS, looking for framework")
+ self._framework_detect(qvars, self.requested_modules, kwargs)
+ # Sometimes Qt is built not as a framework (for instance, when using conan pkg manager)
+ # skip and fall back to normal procedure then
+ if self.is_found:
+ return
+ else:
+ mlog.debug("Building for macOS, couldn't find framework, falling back to library search")
+ incdir = qvars['QT_INSTALL_HEADERS']
+ self.compile_args.append('-I' + incdir)
+ libdir = qvars['QT_INSTALL_LIBS']
+ # Used by qt.compilers_detect()
+ self.bindir = get_qmake_host_bins(qvars)
+ self.libexecdir = get_qmake_host_libexecs(qvars)
+
+ # Use the buildtype by default, but look at the b_vscrt option if the
+ # compiler supports it.
+ is_debug = self.env.coredata.get_option(mesonlib.OptionKey('buildtype')) == 'debug'
+ if mesonlib.OptionKey('b_vscrt') in self.env.coredata.options:
+ if self.env.coredata.options[mesonlib.OptionKey('b_vscrt')].value in {'mdd', 'mtd'}:
+ is_debug = True
+ modules_lib_suffix = _get_modules_lib_suffix(self.version, self.env.machines[self.for_machine], is_debug)
+
+ for module in self.requested_modules:
+ mincdir = os.path.join(incdir, 'Qt' + module)
+ self.compile_args.append('-I' + mincdir)
+
+ if module == 'QuickTest':
+ define_base = 'QMLTEST'
+ elif module == 'Test':
+ define_base = 'TESTLIB'
+ else:
+ define_base = module.upper()
+ self.compile_args.append(f'-DQT_{define_base}_LIB')
+
+ if self.private_headers:
+ priv_inc = self.get_private_includes(mincdir, module)
+ for directory in priv_inc:
+ self.compile_args.append('-I' + directory)
+ libfiles = self.clib_compiler.find_library(
+ self.qtpkgname + module + modules_lib_suffix, self.env,
+ mesonlib.listify(libdir)) # TODO: shouldn't be necissary
+ if libfiles:
+ libfile = libfiles[0]
+ else:
+ mlog.log("Could not find:", module,
+ self.qtpkgname + module + modules_lib_suffix,
+ 'in', libdir)
+ self.is_found = False
+ break
+ self.link_args.append(libfile)
+
+ if self.env.machines[self.for_machine].is_windows() and self.qtmain:
+ if not self._link_with_qt_winmain(is_debug, libdir):
+ self.is_found = False
+
+ def _sanitize_version(self, version: str) -> str:
+ m = re.search(rf'({self.qtver}(\.\d+)+)', version)
+ if m:
+ return m.group(0).rstrip('.')
+ return version
+
+ @abc.abstractmethod
+ def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
+ pass
+
+ def _framework_detect(self, qvars: T.Dict[str, str], modules: T.List[str], kwargs: T.Dict[str, T.Any]) -> None:
+ libdir = qvars['QT_INSTALL_LIBS']
+
+ # ExtraFrameworkDependency doesn't support any methods
+ fw_kwargs = kwargs.copy()
+ fw_kwargs.pop('method', None)
+ fw_kwargs['paths'] = [libdir]
+
+ for m in modules:
+ fname = 'Qt' + m
+ mlog.debug('Looking for qt framework ' + fname)
+ fwdep = QtExtraFrameworkDependency(fname, self.env, fw_kwargs, language=self.language)
+ if fwdep.found():
+ self.compile_args.append('-F' + libdir)
+ self.compile_args += fwdep.get_compile_args(with_private_headers=self.private_headers,
+ qt_version=self.version)
+ self.link_args += fwdep.get_link_args()
+ else:
+ self.is_found = False
+ break
+ else:
+ self.is_found = True
+ # Used by self.compilers_detect()
+ self.bindir = get_qmake_host_bins(qvars)
+ self.libexecdir = get_qmake_host_libexecs(qvars)
+
+ def log_info(self) -> str:
+ return 'qmake'
+
+
+class Qt6WinMainMixin:
+
+ def get_qt_winmain_base_name(self, is_debug: bool) -> str:
+ return 'Qt6EntryPointd' if is_debug else 'Qt6EntryPoint'
+
+
+class Qt4ConfigToolDependency(QmakeQtDependency):
+
+ def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
+ return []
+
+
+class Qt5ConfigToolDependency(QmakeQtDependency):
+
+ def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
+ return _qt_get_private_includes(mod_inc_dir, module, self.version)
+
+
+class Qt6ConfigToolDependency(Qt6WinMainMixin, QmakeQtDependency):
+
+ def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
+ return _qt_get_private_includes(mod_inc_dir, module, self.version)
+
+
+class Qt4PkgConfigDependency(QtPkgConfigDependency):
+
+ @staticmethod
+ def get_pkgconfig_host_bins(core: PkgConfigDependency) -> T.Optional[str]:
+ # Only return one bins dir, because the tools are generally all in one
+ # directory for Qt4, in Qt5, they must all be in one directory. Return
+ # the first one found among the bin variables, in case one tool is not
+ # configured to be built.
+ applications = ['moc', 'uic', 'rcc', 'lupdate', 'lrelease']
+ for application in applications:
+ try:
+ return os.path.dirname(core.get_pkgconfig_variable(f'{application}_location', [], None))
+ except mesonlib.MesonException:
+ pass
+ return None
+
+ def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
+ return []
+
+ @staticmethod
+ def get_pkgconfig_host_libexecs(core: PkgConfigDependency) -> str:
+ return None
+
+
+class Qt5PkgConfigDependency(QtPkgConfigDependency):
+
+ @staticmethod
+ def get_pkgconfig_host_bins(core: PkgConfigDependency) -> str:
+ return core.get_pkgconfig_variable('host_bins', [], None)
+
+ @staticmethod
+ def get_pkgconfig_host_libexecs(core: PkgConfigDependency) -> str:
+ return None
+
+ def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
+ return _qt_get_private_includes(mod_inc_dir, module, self.version)
+
+
+class Qt6PkgConfigDependency(Qt6WinMainMixin, QtPkgConfigDependency):
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, env, kwargs)
+ if not self.libexecdir:
+ mlog.debug(f'detected Qt6 {self.version} pkg-config dependency does not '
+ 'have proper tools support, ignoring')
+ self.is_found = False
+
+ @staticmethod
+ def get_pkgconfig_host_bins(core: PkgConfigDependency) -> str:
+ return core.get_pkgconfig_variable('bindir', [], None)
+
+ @staticmethod
+ def get_pkgconfig_host_libexecs(core: PkgConfigDependency) -> str:
+ # Qt6 pkg-config for Qt defines libexecdir from 6.3+
+ return core.get_pkgconfig_variable('libexecdir', [], None)
+
+ def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
+ return _qt_get_private_includes(mod_inc_dir, module, self.version)
+
+
+qt4_factory = DependencyFactory(
+ 'qt4',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
+ pkgconfig_class=Qt4PkgConfigDependency,
+ configtool_class=Qt4ConfigToolDependency,
+)
+
+qt5_factory = DependencyFactory(
+ 'qt5',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
+ pkgconfig_class=Qt5PkgConfigDependency,
+ configtool_class=Qt5ConfigToolDependency,
+)
+
+qt6_factory = DependencyFactory(
+ 'qt6',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
+ pkgconfig_class=Qt6PkgConfigDependency,
+ configtool_class=Qt6ConfigToolDependency,
+)
diff --git a/mesonbuild/dependencies/scalapack.py b/mesonbuild/dependencies/scalapack.py
new file mode 100644
index 0000000..be8ee70
--- /dev/null
+++ b/mesonbuild/dependencies/scalapack.py
@@ -0,0 +1,156 @@
+# Copyright 2013-2020 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.
+from __future__ import annotations
+
+from pathlib import Path
+import functools
+import os
+import typing as T
+
+from ..mesonlib import OptionKey
+from .base import DependencyMethods
+from .base import DependencyException
+from .cmake import CMakeDependency
+from .pkgconfig import PkgConfigDependency
+from .factory import factory_methods
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment, MachineChoice
+ from .factory import DependencyGenerator
+
+
+@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE})
+def scalapack_factory(env: 'Environment', for_machine: 'MachineChoice',
+ kwargs: T.Dict[str, T.Any],
+ methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
+ candidates: T.List['DependencyGenerator'] = []
+
+ if DependencyMethods.PKGCONFIG in methods:
+ static_opt = kwargs.get('static', env.coredata.get_option(OptionKey('prefer_static')))
+ mkl = 'mkl-static-lp64-iomp' if static_opt else 'mkl-dynamic-lp64-iomp'
+ candidates.append(functools.partial(
+ MKLPkgConfigDependency, mkl, env, kwargs))
+
+ for pkg in ['scalapack-openmpi', 'scalapack']:
+ candidates.append(functools.partial(
+ PkgConfigDependency, pkg, env, kwargs))
+
+ if DependencyMethods.CMAKE in methods:
+ candidates.append(functools.partial(
+ CMakeDependency, 'Scalapack', env, kwargs))
+
+ return candidates
+
+
+class MKLPkgConfigDependency(PkgConfigDependency):
+
+ """PkgConfigDependency for Intel MKL.
+
+ MKL's pkg-config is pretty much borked in every way. We need to apply a
+ bunch of fixups to make it work correctly.
+ """
+
+ def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
+ language: T.Optional[str] = None):
+ _m = os.environ.get('MKLROOT')
+ self.__mklroot = Path(_m).resolve() if _m else None
+
+ # We need to call down into the normal super() method even if we don't
+ # find mklroot, otherwise we won't have all of the instance variables
+ # initialized that meson expects.
+ super().__init__(name, env, kwargs, language=language)
+
+ # Doesn't work with gcc on windows, but does on Linux
+ if (not self.__mklroot or (env.machines[self.for_machine].is_windows()
+ and self.clib_compiler.id == 'gcc')):
+ self.is_found = False
+
+ # This can happen either because we're using GCC, we couldn't find the
+ # mklroot, or the pkg-config couldn't find it.
+ if not self.is_found:
+ return
+
+ assert self.version != '', 'This should not happen if we didn\'t return above'
+
+ if self.version == 'unknown':
+ # At least by 2020 the version is in the pkg-config, just not with
+ # the correct name
+ v = self.get_variable(pkgconfig='Version', default_value='')
+
+ if not v and self.__mklroot:
+ try:
+ v = (
+ self.__mklroot.as_posix()
+ .split('compilers_and_libraries_')[1]
+ .split('/', 1)[0]
+ )
+ except IndexError:
+ pass
+
+ if v:
+ assert isinstance(v, str)
+ self.version = v
+
+ def _set_libs(self) -> None:
+ super()._set_libs()
+
+ if self.env.machines[self.for_machine].is_windows():
+ suffix = '.lib'
+ elif self.static:
+ suffix = '.a'
+ else:
+ suffix = ''
+ libdir = self.__mklroot / 'lib/intel64'
+
+ if self.clib_compiler.id == 'gcc':
+ for i, a in enumerate(self.link_args):
+ # only replace in filename, not in directory names
+ dirname, basename = os.path.split(a)
+ if 'mkl_intel_lp64' in basename:
+ basename = basename.replace('intel', 'gf')
+ self.link_args[i] = '/' + os.path.join(dirname, basename)
+ # MKL pkg-config omits scalapack
+ # be sure "-L" and "-Wl" are first if present
+ i = 0
+ for j, a in enumerate(self.link_args):
+ if a.startswith(('-L', '-Wl')):
+ i = j + 1
+ elif j > 3:
+ break
+ if self.env.machines[self.for_machine].is_windows() or self.static:
+ self.link_args.insert(
+ i, str(libdir / ('mkl_scalapack_lp64' + suffix))
+ )
+ self.link_args.insert(
+ i + 1, str(libdir / ('mkl_blacs_intelmpi_lp64' + suffix))
+ )
+ else:
+ self.link_args.insert(i, '-lmkl_scalapack_lp64')
+ self.link_args.insert(i + 1, '-lmkl_blacs_intelmpi_lp64')
+
+ def _set_cargs(self) -> None:
+ env = None
+ if self.language == 'fortran':
+ # gfortran doesn't appear to look in system paths for INCLUDE files,
+ # so don't allow pkg-config to suppress -I flags for system paths
+ env = os.environ.copy()
+ env['PKG_CONFIG_ALLOW_SYSTEM_CFLAGS'] = '1'
+ ret, out, err = self._call_pkgbin([
+ '--cflags', self.name,
+ '--define-variable=prefix=' + self.__mklroot.as_posix()],
+ env=env)
+ if ret != 0:
+ raise DependencyException('Could not generate cargs for %s:\n%s\n' %
+ (self.name, err))
+ self.compile_args = self._convert_mingw_paths(self._split_args(out))
diff --git a/mesonbuild/dependencies/ui.py b/mesonbuild/dependencies/ui.py
new file mode 100644
index 0000000..2c341af
--- /dev/null
+++ b/mesonbuild/dependencies/ui.py
@@ -0,0 +1,255 @@
+# Copyright 2013-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.
+
+# This file contains the detection logic for external dependencies that
+# are UI-related.
+from __future__ import annotations
+
+import os
+import subprocess
+import typing as T
+
+from .. import mlog
+from .. import mesonlib
+from ..mesonlib import (
+ Popen_safe, extract_as_list, version_compare_many
+)
+from ..environment import detect_cpu_family
+
+from .base import DependencyException, DependencyMethods, DependencyTypeName, SystemDependency
+from .configtool import ConfigToolDependency
+from .factory import DependencyFactory
+
+if T.TYPE_CHECKING:
+ from ..environment import Environment
+
+
+class GLDependencySystem(SystemDependency):
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ super().__init__(name, environment, kwargs)
+
+ if self.env.machines[self.for_machine].is_darwin():
+ self.is_found = True
+ # FIXME: Use AppleFrameworks dependency
+ self.link_args = ['-framework', 'OpenGL']
+ # FIXME: Detect version using self.clib_compiler
+ return
+ if self.env.machines[self.for_machine].is_windows():
+ self.is_found = True
+ # FIXME: Use self.clib_compiler.find_library()
+ self.link_args = ['-lopengl32']
+ # FIXME: Detect version using self.clib_compiler
+ return
+
+class GnuStepDependency(ConfigToolDependency):
+
+ tools = ['gnustep-config']
+ tool_name = 'gnustep-config'
+
+ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
+ super().__init__('gnustep', environment, kwargs, language='objc')
+ if not self.is_found:
+ return
+ self.modules = kwargs.get('modules', [])
+ self.compile_args = self.filter_args(
+ self.get_config_value(['--objc-flags'], 'compile_args'))
+ self.link_args = self.weird_filter(self.get_config_value(
+ ['--gui-libs' if 'gui' in self.modules else '--base-libs'],
+ 'link_args'))
+
+ def find_config(self, versions: T.Optional[T.List[str]] = None, returncode: int = 0) -> T.Tuple[T.Optional[T.List[str]], T.Optional[str]]:
+ tool = [self.tools[0]]
+ try:
+ p, out = Popen_safe(tool + ['--help'])[:2]
+ except (FileNotFoundError, PermissionError):
+ return (None, None)
+ if p.returncode != returncode:
+ return (None, None)
+ self.config = tool
+ found_version = self.detect_version()
+ if versions and not version_compare_many(found_version, versions)[0]:
+ return (None, found_version)
+
+ return (tool, found_version)
+
+ @staticmethod
+ def weird_filter(elems: T.List[str]) -> T.List[str]:
+ """When building packages, the output of the enclosing Make is
+ sometimes mixed among the subprocess output. I have no idea why. As a
+ hack filter out everything that is not a flag.
+ """
+ return [e for e in elems if e.startswith('-')]
+
+ @staticmethod
+ def filter_args(args: T.List[str]) -> T.List[str]:
+ """gnustep-config returns a bunch of garbage args such as -O2 and so
+ on. Drop everything that is not needed.
+ """
+ result = []
+ for f in args:
+ if f.startswith('-D') \
+ or f.startswith('-f') \
+ or f.startswith('-I') \
+ or f == '-pthread' \
+ or (f.startswith('-W') and not f == '-Wall'):
+ result.append(f)
+ return result
+
+ def detect_version(self) -> str:
+ gmake = self.get_config_value(['--variable=GNUMAKE'], 'variable')[0]
+ makefile_dir = self.get_config_value(['--variable=GNUSTEP_MAKEFILES'], 'variable')[0]
+ # This Makefile has the GNUStep version set
+ base_make = os.path.join(makefile_dir, 'Additional', 'base.make')
+ # Print the Makefile variable passed as the argument. For instance, if
+ # you run the make target `print-SOME_VARIABLE`, this will print the
+ # value of the variable `SOME_VARIABLE`.
+ printver = "print-%:\n\t@echo '$($*)'"
+ env = os.environ.copy()
+ # See base.make to understand why this is set
+ env['FOUNDATION_LIB'] = 'gnu'
+ p, o, e = Popen_safe([gmake, '-f', '-', '-f', base_make,
+ 'print-GNUSTEP_BASE_VERSION'],
+ env=env, write=printver, stdin=subprocess.PIPE)
+ version = o.strip()
+ if not version:
+ mlog.debug("Couldn't detect GNUStep version, falling back to '1'")
+ # Fallback to setting some 1.x version
+ version = '1'
+ return version
+
+
+class SDL2DependencyConfigTool(ConfigToolDependency):
+
+ tools = ['sdl2-config']
+ tool_name = 'sdl2-config'
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__(name, environment, kwargs)
+ if not self.is_found:
+ return
+ self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
+ self.link_args = self.get_config_value(['--libs'], 'link_args')
+
+
+class WxDependency(ConfigToolDependency):
+
+ tools = ['wx-config-3.0', 'wx-config-3.1', 'wx-config', 'wx-config-gtk3']
+ tool_name = 'wx-config'
+
+ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+ super().__init__('WxWidgets', environment, kwargs, language='cpp')
+ if not self.is_found:
+ return
+ self.requested_modules = self.get_requested(kwargs)
+
+ extra_args = []
+ if self.static:
+ extra_args.append('--static=yes')
+
+ # Check to make sure static is going to work
+ err = Popen_safe(self.config + extra_args)[2]
+ if 'No config found to match' in err:
+ mlog.debug('WxWidgets is missing static libraries.')
+ self.is_found = False
+ return
+
+ # wx-config seems to have a cflags as well but since it requires C++,
+ # this should be good, at least for now.
+ self.compile_args = self.get_config_value(['--cxxflags'] + extra_args + self.requested_modules, 'compile_args')
+ self.link_args = self.get_config_value(['--libs'] + extra_args + self.requested_modules, 'link_args')
+
+ @staticmethod
+ def get_requested(kwargs: T.Dict[str, T.Any]) -> T.List[str]:
+ if 'modules' not in kwargs:
+ return []
+ candidates = extract_as_list(kwargs, 'modules')
+ for c in candidates:
+ if not isinstance(c, str):
+ raise DependencyException('wxwidgets module argument is not a string')
+ return candidates
+
+
+class VulkanDependencySystem(SystemDependency):
+
+ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
+ super().__init__(name, environment, kwargs, language=language)
+
+ try:
+ self.vulkan_sdk = os.environ['VULKAN_SDK']
+ if not os.path.isabs(self.vulkan_sdk):
+ raise DependencyException('VULKAN_SDK must be an absolute path.')
+ except KeyError:
+ self.vulkan_sdk = None
+
+ if self.vulkan_sdk:
+ # TODO: this config might not work on some platforms, fix bugs as reported
+ # we should at least detect other 64-bit platforms (e.g. armv8)
+ lib_name = 'vulkan'
+ lib_dir = 'lib'
+ inc_dir = 'include'
+ if mesonlib.is_windows():
+ lib_name = 'vulkan-1'
+ lib_dir = 'Lib32'
+ inc_dir = 'Include'
+ if detect_cpu_family(self.env.coredata.compilers.host) == 'x86_64':
+ lib_dir = 'Lib'
+
+ # make sure header and lib are valid
+ inc_path = os.path.join(self.vulkan_sdk, inc_dir)
+ header = os.path.join(inc_path, 'vulkan', 'vulkan.h')
+ lib_path = os.path.join(self.vulkan_sdk, lib_dir)
+ find_lib = self.clib_compiler.find_library(lib_name, environment, [lib_path])
+
+ if not find_lib:
+ raise DependencyException('VULKAN_SDK point to invalid directory (no lib)')
+
+ if not os.path.isfile(header):
+ raise DependencyException('VULKAN_SDK point to invalid directory (no include)')
+
+ # XXX: this is very odd, and may deserve being removed
+ self.type_name = DependencyTypeName('vulkan_sdk')
+ self.is_found = True
+ self.compile_args.append('-I' + inc_path)
+ self.link_args.append('-L' + lib_path)
+ self.link_args.append('-l' + lib_name)
+
+ # TODO: find a way to retrieve the version from the sdk?
+ # Usually it is a part of the path to it (but does not have to be)
+ return
+ else:
+ # simply try to guess it, usually works on linux
+ libs = self.clib_compiler.find_library('vulkan', environment, [])
+ if libs is not None and self.clib_compiler.has_header('vulkan/vulkan.h', '', environment, disable_cache=True)[0]:
+ self.is_found = True
+ for lib in libs:
+ self.link_args.append(lib)
+ return
+
+gl_factory = DependencyFactory(
+ 'gl',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM],
+ system_class=GLDependencySystem,
+)
+
+sdl2_factory = DependencyFactory(
+ 'sdl2',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK],
+ configtool_class=SDL2DependencyConfigTool,
+)
+
+vulkan_factory = DependencyFactory(
+ 'vulkan',
+ [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM],
+ system_class=VulkanDependencySystem,
+)
diff --git a/mesonbuild/depfile.py b/mesonbuild/depfile.py
new file mode 100644
index 0000000..d346136
--- /dev/null
+++ b/mesonbuild/depfile.py
@@ -0,0 +1,91 @@
+# Copyright 2019 Red Hat, Inc.
+# 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.
+from __future__ import annotations
+
+import typing as T
+
+
+def parse(lines: T.Iterable[str]) -> T.List[T.Tuple[T.List[str], T.List[str]]]:
+ rules: T.List[T.Tuple[T.List[str], T.List[str]]] = []
+ targets: T.List[str] = []
+ deps: T.List[str] = []
+ in_deps = False
+ out = ''
+ for line in lines:
+ if not line.endswith('\n'):
+ line += '\n'
+ escape = None
+ for c in line:
+ if escape:
+ if escape == '$' and c != '$':
+ out += '$'
+ if escape == '\\' and c == '\n':
+ continue
+ out += c
+ escape = None
+ continue
+ if c in {'\\', '$'}:
+ escape = c
+ continue
+ elif c in {' ', '\n'}:
+ if out != '':
+ if in_deps:
+ deps.append(out)
+ else:
+ targets.append(out)
+ out = ''
+ if c == '\n':
+ rules.append((targets, deps))
+ targets = []
+ deps = []
+ in_deps = False
+ continue
+ elif c == ':':
+ targets.append(out)
+ out = ''
+ in_deps = True
+ continue
+ out += c
+ return rules
+
+class Target(T.NamedTuple):
+
+ deps: T.Set[str]
+
+
+class DepFile:
+ def __init__(self, lines: T.Iterable[str]):
+ rules = parse(lines)
+ depfile: T.Dict[str, Target] = {}
+ for (targets, deps) in rules:
+ for target in targets:
+ t = depfile.setdefault(target, Target(deps=set()))
+ for dep in deps:
+ t.deps.add(dep)
+ self.depfile = depfile
+
+ def get_all_dependencies(self, name: str, visited: T.Optional[T.Set[str]] = None) -> T.List[str]:
+ deps: T.Set[str] = set()
+ if not visited:
+ visited = set()
+ if name in visited:
+ return []
+ visited.add(name)
+
+ target = self.depfile.get(name)
+ if not target:
+ return []
+ deps.update(target.deps)
+ for dep in target.deps:
+ deps.update(self.get_all_dependencies(dep, visited))
+ return sorted(deps)
diff --git a/mesonbuild/envconfig.py b/mesonbuild/envconfig.py
new file mode 100644
index 0000000..90117c1
--- /dev/null
+++ b/mesonbuild/envconfig.py
@@ -0,0 +1,455 @@
+# Copyright 2012-2016 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.
+from __future__ import annotations
+
+from dataclasses import dataclass
+import subprocess
+import typing as T
+from enum import Enum
+
+from . import mesonlib
+from .mesonlib import EnvironmentException, HoldableObject
+from . import mlog
+from pathlib import Path
+
+
+# These classes contains all the data pulled from configuration files (native
+# and cross file currently), and also assists with the reading environment
+# variables.
+#
+# At this time there isn't an ironclad difference between this an other sources
+# of state like `coredata`. But one rough guide is much what is in `coredata` is
+# the *output* of the configuration process: the final decisions after tests.
+# This, on the other hand has *inputs*. The config files are parsed, but
+# otherwise minimally transformed. When more complex fallbacks (environment
+# detection) exist, they are defined elsewhere as functions that construct
+# instances of these classes.
+
+
+known_cpu_families = (
+ 'aarch64',
+ 'alpha',
+ 'arc',
+ 'arm',
+ 'avr',
+ 'c2000',
+ 'csky',
+ 'dspic',
+ 'e2k',
+ 'ft32',
+ 'ia64',
+ 'loongarch64',
+ 'm68k',
+ 'microblaze',
+ 'mips',
+ 'mips64',
+ 'msp430',
+ 'parisc',
+ 'pic24',
+ 'ppc',
+ 'ppc64',
+ 'riscv32',
+ 'riscv64',
+ 'rl78',
+ 'rx',
+ 's390',
+ 's390x',
+ 'sh4',
+ 'sparc',
+ 'sparc64',
+ 'wasm32',
+ 'wasm64',
+ 'x86',
+ 'x86_64',
+)
+
+# It would feel more natural to call this "64_BIT_CPU_FAMILIES", but
+# python identifiers cannot start with numbers
+CPU_FAMILIES_64_BIT = [
+ 'aarch64',
+ 'alpha',
+ 'ia64',
+ 'loongarch64',
+ 'mips64',
+ 'ppc64',
+ 'riscv64',
+ 's390x',
+ 'sparc64',
+ 'wasm64',
+ 'x86_64',
+]
+
+# Map from language identifiers to environment variables.
+ENV_VAR_COMPILER_MAP: T.Mapping[str, str] = {
+ # Compilers
+ 'c': 'CC',
+ 'cpp': 'CXX',
+ 'cs': 'CSC',
+ 'd': 'DC',
+ 'fortran': 'FC',
+ 'objc': 'OBJC',
+ 'objcpp': 'OBJCXX',
+ 'rust': 'RUSTC',
+ 'vala': 'VALAC',
+ 'nasm': 'NASM',
+
+ # Linkers
+ 'c_ld': 'CC_LD',
+ 'cpp_ld': 'CXX_LD',
+ 'd_ld': 'DC_LD',
+ 'fortran_ld': 'FC_LD',
+ 'objc_ld': 'OBJC_LD',
+ 'objcpp_ld': 'OBJCXX_LD',
+ 'rust_ld': 'RUSTC_LD',
+}
+
+# Map from utility names to environment variables.
+ENV_VAR_TOOL_MAP: T.Mapping[str, str] = {
+ # Binutils
+ 'ar': 'AR',
+ 'as': 'AS',
+ 'ld': 'LD',
+ 'nm': 'NM',
+ 'objcopy': 'OBJCOPY',
+ 'objdump': 'OBJDUMP',
+ 'ranlib': 'RANLIB',
+ 'readelf': 'READELF',
+ 'size': 'SIZE',
+ 'strings': 'STRINGS',
+ 'strip': 'STRIP',
+ 'windres': 'WINDRES',
+
+ # Other tools
+ 'cmake': 'CMAKE',
+ 'qmake': 'QMAKE',
+ 'pkgconfig': 'PKG_CONFIG',
+ 'pkg-config': 'PKG_CONFIG',
+ 'make': 'MAKE',
+ 'vapigen': 'VAPIGEN',
+ 'llvm-config': 'LLVM_CONFIG',
+}
+
+ENV_VAR_PROG_MAP = {**ENV_VAR_COMPILER_MAP, **ENV_VAR_TOOL_MAP}
+
+# Deprecated environment variables mapped from the new variable to the old one
+# Deprecated in 0.54.0
+DEPRECATED_ENV_PROG_MAP: T.Mapping[str, str] = {
+ 'd_ld': 'D_LD',
+ 'fortran_ld': 'F_LD',
+ 'rust_ld': 'RUST_LD',
+ 'objcpp_ld': 'OBJCPP_LD',
+}
+
+class CMakeSkipCompilerTest(Enum):
+ ALWAYS = 'always'
+ NEVER = 'never'
+ DEP_ONLY = 'dep_only'
+
+class Properties:
+ def __init__(
+ self,
+ properties: T.Optional[T.Dict[str, T.Optional[T.Union[str, bool, int, T.List[str]]]]] = None,
+ ):
+ self.properties = properties or {} # type: T.Dict[str, T.Optional[T.Union[str, bool, int, T.List[str]]]]
+
+ def has_stdlib(self, language: str) -> bool:
+ return language + '_stdlib' in self.properties
+
+ # Some of get_stdlib, get_root, get_sys_root are wider than is actually
+ # true, but without heterogenious dict annotations it's not practical to
+ # narrow them
+ def get_stdlib(self, language: str) -> T.Union[str, T.List[str]]:
+ stdlib = self.properties[language + '_stdlib']
+ if isinstance(stdlib, str):
+ return stdlib
+ assert isinstance(stdlib, list)
+ for i in stdlib:
+ assert isinstance(i, str)
+ return stdlib
+
+ def get_root(self) -> T.Optional[str]:
+ root = self.properties.get('root', None)
+ assert root is None or isinstance(root, str)
+ return root
+
+ def get_sys_root(self) -> T.Optional[str]:
+ sys_root = self.properties.get('sys_root', None)
+ assert sys_root is None or isinstance(sys_root, str)
+ return sys_root
+
+ def get_pkg_config_libdir(self) -> T.Optional[T.List[str]]:
+ p = self.properties.get('pkg_config_libdir', None)
+ if p is None:
+ return p
+ res = mesonlib.listify(p)
+ for i in res:
+ assert isinstance(i, str)
+ return res
+
+ def get_cmake_defaults(self) -> bool:
+ if 'cmake_defaults' not in self.properties:
+ return True
+ res = self.properties['cmake_defaults']
+ assert isinstance(res, bool)
+ return res
+
+ def get_cmake_toolchain_file(self) -> T.Optional[Path]:
+ if 'cmake_toolchain_file' not in self.properties:
+ return None
+ raw = self.properties['cmake_toolchain_file']
+ assert isinstance(raw, str)
+ cmake_toolchain_file = Path(raw)
+ if not cmake_toolchain_file.is_absolute():
+ raise EnvironmentException(f'cmake_toolchain_file ({raw}) is not absolute')
+ return cmake_toolchain_file
+
+ def get_cmake_skip_compiler_test(self) -> CMakeSkipCompilerTest:
+ if 'cmake_skip_compiler_test' not in self.properties:
+ return CMakeSkipCompilerTest.DEP_ONLY
+ raw = self.properties['cmake_skip_compiler_test']
+ assert isinstance(raw, str)
+ try:
+ return CMakeSkipCompilerTest(raw)
+ except ValueError:
+ raise EnvironmentException(
+ '"{}" is not a valid value for cmake_skip_compiler_test. Supported values are {}'
+ .format(raw, [e.value for e in CMakeSkipCompilerTest]))
+
+ def get_cmake_use_exe_wrapper(self) -> bool:
+ if 'cmake_use_exe_wrapper' not in self.properties:
+ return True
+ res = self.properties['cmake_use_exe_wrapper']
+ assert isinstance(res, bool)
+ return res
+
+ def get_java_home(self) -> T.Optional[Path]:
+ value = T.cast('T.Optional[str]', self.properties.get('java_home'))
+ return Path(value) if value else None
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, type(self)):
+ return self.properties == other.properties
+ return NotImplemented
+
+ # TODO consider removing so Properties is less freeform
+ def __getitem__(self, key: str) -> T.Optional[T.Union[str, bool, int, T.List[str]]]:
+ return self.properties[key]
+
+ # TODO consider removing so Properties is less freeform
+ def __contains__(self, item: T.Union[str, bool, int, T.List[str]]) -> bool:
+ return item in self.properties
+
+ # TODO consider removing, for same reasons as above
+ def get(self, key: str, default: T.Optional[T.Union[str, bool, int, T.List[str]]] = None) -> T.Optional[T.Union[str, bool, int, T.List[str]]]:
+ return self.properties.get(key, default)
+
+@dataclass(unsafe_hash=True)
+class MachineInfo(HoldableObject):
+ system: str
+ cpu_family: str
+ cpu: str
+ endian: str
+
+ def __post_init__(self) -> None:
+ self.is_64_bit: bool = self.cpu_family in CPU_FAMILIES_64_BIT
+
+ def __repr__(self) -> str:
+ return f'<MachineInfo: {self.system} {self.cpu_family} ({self.cpu})>'
+
+ @classmethod
+ def from_literal(cls, literal: T.Dict[str, str]) -> 'MachineInfo':
+ minimum_literal = {'cpu', 'cpu_family', 'endian', 'system'}
+ if set(literal) < minimum_literal:
+ raise EnvironmentException(
+ f'Machine info is currently {literal}\n' +
+ 'but is missing {}.'.format(minimum_literal - set(literal)))
+
+ cpu_family = literal['cpu_family']
+ if cpu_family not in known_cpu_families:
+ mlog.warning(f'Unknown CPU family {cpu_family}, please report this at https://github.com/mesonbuild/meson/issues/new')
+
+ endian = literal['endian']
+ if endian not in ('little', 'big'):
+ mlog.warning(f'Unknown endian {endian}')
+
+ return cls(literal['system'], cpu_family, literal['cpu'], endian)
+
+ def is_windows(self) -> bool:
+ """
+ Machine is windows?
+ """
+ return self.system == 'windows'
+
+ def is_cygwin(self) -> bool:
+ """
+ Machine is cygwin?
+ """
+ return self.system == 'cygwin'
+
+ def is_linux(self) -> bool:
+ """
+ Machine is linux?
+ """
+ return self.system == 'linux'
+
+ def is_darwin(self) -> bool:
+ """
+ Machine is Darwin (iOS/tvOS/OS X)?
+ """
+ return self.system in {'darwin', 'ios', 'tvos'}
+
+ def is_android(self) -> bool:
+ """
+ Machine is Android?
+ """
+ return self.system == 'android'
+
+ def is_haiku(self) -> bool:
+ """
+ Machine is Haiku?
+ """
+ return self.system == 'haiku'
+
+ def is_netbsd(self) -> bool:
+ """
+ Machine is NetBSD?
+ """
+ return self.system == 'netbsd'
+
+ def is_openbsd(self) -> bool:
+ """
+ Machine is OpenBSD?
+ """
+ return self.system == 'openbsd'
+
+ def is_dragonflybsd(self) -> bool:
+ """Machine is DragonflyBSD?"""
+ return self.system == 'dragonfly'
+
+ def is_freebsd(self) -> bool:
+ """Machine is FreeBSD?"""
+ return self.system == 'freebsd'
+
+ def is_sunos(self) -> bool:
+ """Machine is illumos or Solaris?"""
+ return self.system == 'sunos'
+
+ def is_hurd(self) -> bool:
+ """
+ Machine is GNU/Hurd?
+ """
+ return self.system == 'gnu'
+
+ def is_irix(self) -> bool:
+ """Machine is IRIX?"""
+ return self.system.startswith('irix')
+
+ # Various prefixes and suffixes for import libraries, shared libraries,
+ # static libraries, and executables.
+ # Versioning is added to these names in the backends as-needed.
+ def get_exe_suffix(self) -> str:
+ if self.is_windows() or self.is_cygwin():
+ return 'exe'
+ else:
+ return ''
+
+ def get_object_suffix(self) -> str:
+ if self.is_windows():
+ return 'obj'
+ else:
+ return 'o'
+
+ def libdir_layout_is_win(self) -> bool:
+ return self.is_windows() or self.is_cygwin()
+
+class BinaryTable:
+
+ def __init__(
+ self,
+ binaries: T.Optional[T.Dict[str, T.Union[str, T.List[str]]]] = None,
+ ):
+ self.binaries: T.Dict[str, T.List[str]] = {}
+ if binaries:
+ for name, command in binaries.items():
+ if not isinstance(command, (list, str)):
+ raise mesonlib.MesonException(
+ f'Invalid type {command!r} for entry {name!r} in cross file')
+ self.binaries[name] = mesonlib.listify(command)
+
+ @staticmethod
+ def detect_ccache() -> T.List[str]:
+ try:
+ subprocess.check_call(['ccache', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ except (OSError, subprocess.CalledProcessError):
+ return []
+ return ['ccache']
+
+ @staticmethod
+ def detect_sccache() -> T.List[str]:
+ try:
+ subprocess.check_call(['sccache', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ except (OSError, subprocess.CalledProcessError):
+ return []
+ return ['sccache']
+
+ @staticmethod
+ def detect_compiler_cache() -> T.List[str]:
+ # Sccache is "newer" so it is assumed that people would prefer it by default.
+ cache = BinaryTable.detect_sccache()
+ if cache:
+ return cache
+ return BinaryTable.detect_ccache()
+
+ @classmethod
+ def parse_entry(cls, entry: T.Union[str, T.List[str]]) -> T.Tuple[T.List[str], T.List[str]]:
+ compiler = mesonlib.stringlistify(entry)
+ # Ensure ccache exists and remove it if it doesn't
+ if compiler[0] == 'ccache':
+ compiler = compiler[1:]
+ ccache = cls.detect_ccache()
+ elif compiler[0] == 'sccache':
+ compiler = compiler[1:]
+ ccache = cls.detect_sccache()
+ else:
+ ccache = []
+ # Return value has to be a list of compiler 'choices'
+ return compiler, ccache
+
+ def lookup_entry(self, name: str) -> T.Optional[T.List[str]]:
+ """Lookup binary in cross/native file and fallback to environment.
+
+ Returns command with args as list if found, Returns `None` if nothing is
+ found.
+ """
+ command = self.binaries.get(name)
+ if not command:
+ return None
+ elif not command[0].strip():
+ return None
+ return command
+
+class CMakeVariables:
+ def __init__(self, variables: T.Optional[T.Dict[str, T.Any]] = None) -> None:
+ variables = variables or {}
+ self.variables = {} # type: T.Dict[str, T.List[str]]
+
+ for key, value in variables.items():
+ value = mesonlib.listify(value)
+ for i in value:
+ if not isinstance(i, str):
+ raise EnvironmentException(f"Value '{i}' of CMake variable '{key}' defined in a machine file is a {type(i).__name__} and not a str")
+ self.variables[key] = value
+
+ def get_variables(self) -> T.Dict[str, T.List[str]]:
+ return self.variables
diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py
new file mode 100644
index 0000000..a06d35e
--- /dev/null
+++ b/mesonbuild/environment.py
@@ -0,0 +1,858 @@
+# Copyright 2012-2020 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.
+from __future__ import annotations
+
+import itertools
+import os, platform, re, sys, shutil
+import typing as T
+import collections
+
+from . import coredata
+from . import mesonlib
+from .mesonlib import (
+ MesonException, MachineChoice, Popen_safe, PerMachine,
+ PerMachineDefaultable, PerThreeMachineDefaultable, split_args, quote_arg, OptionKey,
+ search_version, MesonBugException
+)
+from . import mlog
+from .programs import ExternalProgram
+
+from .envconfig import (
+ BinaryTable, MachineInfo, Properties, known_cpu_families, CMakeVariables,
+)
+from . import compilers
+from .compilers import (
+ Compiler,
+ is_assembly,
+ is_header,
+ is_library,
+ is_llvm_ir,
+ is_object,
+ is_source,
+)
+
+from functools import lru_cache
+from mesonbuild import envconfig
+
+if T.TYPE_CHECKING:
+ import argparse
+ from configparser import ConfigParser
+
+ from .wrap.wrap import Resolver
+
+ CompilersDict = T.Dict[str, Compiler]
+
+
+build_filename = 'meson.build'
+
+
+def _get_env_var(for_machine: MachineChoice, is_cross: bool, var_name: str) -> T.Optional[str]:
+ """
+ Returns the exact env var and the value.
+ """
+ candidates = PerMachine(
+ # The prefixed build version takes priority, but if we are native
+ # compiling we fall back on the unprefixed host version. This
+ # allows native builds to never need to worry about the 'BUILD_*'
+ # ones.
+ ([var_name + '_FOR_BUILD'] if is_cross else [var_name]),
+ # Always just the unprefixed host versions
+ [var_name]
+ )[for_machine]
+ for var in candidates:
+ value = os.environ.get(var)
+ if value is not None:
+ break
+ else:
+ formatted = ', '.join([f'{var!r}' for var in candidates])
+ mlog.debug(f'None of {formatted} are defined in the environment, not changing global flags.')
+ return None
+ mlog.debug(f'Using {var!r} from environment with value: {value!r}')
+ return value
+
+
+def detect_gcovr(min_version='3.3', log=False):
+ gcovr_exe = 'gcovr'
+ try:
+ p, found = Popen_safe([gcovr_exe, '--version'])[0:2]
+ except (FileNotFoundError, PermissionError):
+ # Doesn't exist in PATH or isn't executable
+ return None, None
+ found = search_version(found)
+ if p.returncode == 0 and mesonlib.version_compare(found, '>=' + min_version):
+ if log:
+ mlog.log('Found gcovr-{} at {}'.format(found, quote_arg(shutil.which(gcovr_exe))))
+ return gcovr_exe, found
+ return None, None
+
+def detect_llvm_cov():
+ tools = get_llvm_tool_names('llvm-cov')
+ for tool in tools:
+ if mesonlib.exe_exists([tool, '--version']):
+ return tool
+ return None
+
+def find_coverage_tools() -> T.Tuple[T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str]]:
+ gcovr_exe, gcovr_version = detect_gcovr()
+
+ llvm_cov_exe = detect_llvm_cov()
+
+ lcov_exe = 'lcov'
+ genhtml_exe = 'genhtml'
+
+ if not mesonlib.exe_exists([lcov_exe, '--version']):
+ lcov_exe = None
+ if not mesonlib.exe_exists([genhtml_exe, '--version']):
+ genhtml_exe = None
+
+ return gcovr_exe, gcovr_version, lcov_exe, genhtml_exe, llvm_cov_exe
+
+def detect_ninja(version: str = '1.8.2', log: bool = False) -> T.List[str]:
+ r = detect_ninja_command_and_version(version, log)
+ return r[0] if r else None
+
+def detect_ninja_command_and_version(version: str = '1.8.2', log: bool = False) -> T.Tuple[T.List[str], str]:
+ env_ninja = os.environ.get('NINJA', None)
+ for n in [env_ninja] if env_ninja else ['ninja', 'ninja-build', 'samu']:
+ prog = ExternalProgram(n, silent=True)
+ if not prog.found():
+ continue
+ try:
+ p, found = Popen_safe(prog.command + ['--version'])[0:2]
+ except (FileNotFoundError, PermissionError):
+ # Doesn't exist in PATH or isn't executable
+ continue
+ found = found.strip()
+ # Perhaps we should add a way for the caller to know the failure mode
+ # (not found or too old)
+ if p.returncode == 0 and mesonlib.version_compare(found, '>=' + version):
+ if log:
+ name = os.path.basename(n)
+ if name.endswith('-' + found):
+ name = name[0:-1 - len(found)]
+ if name == 'ninja-build':
+ name = 'ninja'
+ if name == 'samu':
+ name = 'samurai'
+ mlog.log('Found {}-{} at {}'.format(name, found,
+ ' '.join([quote_arg(x) for x in prog.command])))
+ return (prog.command, found)
+
+def get_llvm_tool_names(tool: str) -> T.List[str]:
+ # Ordered list of possible suffixes of LLVM executables to try. Start with
+ # base, then try newest back to oldest (3.5 is arbitrary), and finally the
+ # devel version. Please note that the development snapshot in Debian does
+ # not have a distinct name. Do not move it to the beginning of the list
+ # unless it becomes a stable release.
+ suffixes = [
+ '', # base (no suffix)
+ '-14', '14',
+ '-13', '13',
+ '-12', '12',
+ '-11', '11',
+ '-10', '10',
+ '-9', '90',
+ '-8', '80',
+ '-7', '70',
+ '-6.0', '60',
+ '-5.0', '50',
+ '-4.0', '40',
+ '-3.9', '39',
+ '-3.8', '38',
+ '-3.7', '37',
+ '-3.6', '36',
+ '-3.5', '35',
+ '-15', # Debian development snapshot
+ '-devel', # FreeBSD development snapshot
+ ]
+ names = []
+ for suffix in suffixes:
+ names.append(tool + suffix)
+ return names
+
+def detect_scanbuild() -> T.List[str]:
+ """ Look for scan-build binary on build platform
+
+ First, if a SCANBUILD env variable has been provided, give it precedence
+ on all platforms.
+
+ For most platforms, scan-build is found is the PATH contains a binary
+ named "scan-build". However, some distribution's package manager (FreeBSD)
+ don't. For those, loop through a list of candidates to see if one is
+ available.
+
+ Return: a single-element list of the found scan-build binary ready to be
+ passed to Popen()
+ """
+ exelist = []
+ if 'SCANBUILD' in os.environ:
+ exelist = split_args(os.environ['SCANBUILD'])
+
+ else:
+ tools = get_llvm_tool_names('scan-build')
+ for tool in tools:
+ if shutil.which(tool) is not None:
+ exelist = [shutil.which(tool)]
+ break
+
+ if exelist:
+ tool = exelist[0]
+ if os.path.isfile(tool) and os.access(tool, os.X_OK):
+ return [tool]
+ return []
+
+def detect_clangformat() -> T.List[str]:
+ """ Look for clang-format binary on build platform
+
+ Do the same thing as detect_scanbuild to find clang-format except it
+ currently does not check the environment variable.
+
+ Return: a single-element list of the found clang-format binary ready to be
+ passed to Popen()
+ """
+ tools = get_llvm_tool_names('clang-format')
+ for tool in tools:
+ path = shutil.which(tool)
+ if path is not None:
+ return [path]
+ return []
+
+def detect_windows_arch(compilers: CompilersDict) -> str:
+ """
+ Detecting the 'native' architecture of Windows is not a trivial task. We
+ cannot trust that the architecture that Python is built for is the 'native'
+ one because you can run 32-bit apps on 64-bit Windows using WOW64 and
+ people sometimes install 32-bit Python on 64-bit Windows.
+
+ We also can't rely on the architecture of the OS itself, since it's
+ perfectly normal to compile and run 32-bit applications on Windows as if
+ they were native applications. It's a terrible experience to require the
+ user to supply a cross-info file to compile 32-bit applications on 64-bit
+ Windows. Thankfully, the only way to compile things with Visual Studio on
+ Windows is by entering the 'msvc toolchain' environment, which can be
+ easily detected.
+
+ In the end, the sanest method is as follows:
+ 1. Check environment variables that are set by Windows and WOW64 to find out
+ if this is x86 (possibly in WOW64), if so use that as our 'native'
+ architecture.
+ 2. If the compiler toolchain target architecture is x86, use that as our
+ 'native' architecture.
+ 3. Otherwise, use the actual Windows architecture
+
+ """
+ os_arch = mesonlib.windows_detect_native_arch()
+ if os_arch == 'x86':
+ return os_arch
+ # If we're on 64-bit Windows, 32-bit apps can be compiled without
+ # cross-compilation. So if we're doing that, just set the native arch as
+ # 32-bit and pretend like we're running under WOW64. Else, return the
+ # actual Windows architecture that we deduced above.
+ for compiler in compilers.values():
+ if compiler.id == 'msvc' and (compiler.target in {'x86', '80x86'}):
+ return 'x86'
+ if compiler.id == 'clang-cl' and compiler.target == 'x86':
+ return 'x86'
+ if compiler.id == 'gcc' and compiler.has_builtin_define('__i386__'):
+ return 'x86'
+ return os_arch
+
+def any_compiler_has_define(compilers: CompilersDict, define):
+ for c in compilers.values():
+ try:
+ if c.has_builtin_define(define):
+ return True
+ except mesonlib.MesonException:
+ # Ignore compilers that do not support has_builtin_define.
+ pass
+ return False
+
+def detect_cpu_family(compilers: CompilersDict) -> str:
+ """
+ Python is inconsistent in its platform module.
+ It returns different values for the same cpu.
+ For x86 it might return 'x86', 'i686' or somesuch.
+ Do some canonicalization.
+ """
+ if mesonlib.is_windows():
+ trial = detect_windows_arch(compilers)
+ elif mesonlib.is_freebsd() or mesonlib.is_netbsd() or mesonlib.is_openbsd() or mesonlib.is_qnx() or mesonlib.is_aix():
+ trial = platform.processor().lower()
+ else:
+ trial = platform.machine().lower()
+ if trial.startswith('i') and trial.endswith('86'):
+ trial = 'x86'
+ elif trial == 'bepc':
+ trial = 'x86'
+ elif trial == 'arm64':
+ trial = 'aarch64'
+ elif trial.startswith('aarch64'):
+ # This can be `aarch64_be`
+ trial = 'aarch64'
+ elif trial.startswith('arm') or trial.startswith('earm'):
+ trial = 'arm'
+ elif trial.startswith(('powerpc64', 'ppc64')):
+ trial = 'ppc64'
+ elif trial.startswith(('powerpc', 'ppc')) or trial in {'macppc', 'power macintosh'}:
+ trial = 'ppc'
+ elif trial in {'amd64', 'x64', 'i86pc'}:
+ trial = 'x86_64'
+ elif trial in {'sun4u', 'sun4v'}:
+ trial = 'sparc64'
+ elif trial.startswith('mips'):
+ if '64' not in trial:
+ trial = 'mips'
+ else:
+ trial = 'mips64'
+ elif trial in {'ip30', 'ip35'}:
+ trial = 'mips64'
+
+ # On Linux (and maybe others) there can be any mixture of 32/64 bit code in
+ # the kernel, Python, system, 32-bit chroot on 64-bit host, etc. The only
+ # reliable way to know is to check the compiler defines.
+ if trial == 'x86_64':
+ if any_compiler_has_define(compilers, '__i386__'):
+ trial = 'x86'
+ elif trial == 'aarch64':
+ if any_compiler_has_define(compilers, '__arm__'):
+ trial = 'arm'
+ # Add more quirks here as bugs are reported. Keep in sync with detect_cpu()
+ # below.
+ elif trial == 'parisc64':
+ # ATM there is no 64 bit userland for PA-RISC. Thus always
+ # report it as 32 bit for simplicity.
+ trial = 'parisc'
+ elif trial == 'ppc':
+ # AIX always returns powerpc, check here for 64-bit
+ if any_compiler_has_define(compilers, '__64BIT__'):
+ trial = 'ppc64'
+
+ if trial not in known_cpu_families:
+ mlog.warning(f'Unknown CPU family {trial!r}, please report this at '
+ 'https://github.com/mesonbuild/meson/issues/new with the '
+ 'output of `uname -a` and `cat /proc/cpuinfo`')
+
+ return trial
+
+def detect_cpu(compilers: CompilersDict) -> str:
+ if mesonlib.is_windows():
+ trial = detect_windows_arch(compilers)
+ elif mesonlib.is_freebsd() or mesonlib.is_netbsd() or mesonlib.is_openbsd() or mesonlib.is_aix():
+ trial = platform.processor().lower()
+ else:
+ trial = platform.machine().lower()
+
+ if trial in {'amd64', 'x64', 'i86pc'}:
+ trial = 'x86_64'
+ if trial == 'x86_64':
+ # Same check as above for cpu_family
+ if any_compiler_has_define(compilers, '__i386__'):
+ trial = 'i686' # All 64 bit cpus have at least this level of x86 support.
+ elif trial.startswith('aarch64') or trial.startswith('arm64'):
+ # Same check as above for cpu_family
+ if any_compiler_has_define(compilers, '__arm__'):
+ trial = 'arm'
+ else:
+ # for aarch64_be
+ trial = 'aarch64'
+ elif trial.startswith('earm'):
+ trial = 'arm'
+ elif trial == 'e2k':
+ # Make more precise CPU detection for Elbrus platform.
+ trial = platform.processor().lower()
+ elif trial.startswith('mips'):
+ if '64' not in trial:
+ trial = 'mips'
+ else:
+ trial = 'mips64'
+ elif trial == 'ppc':
+ # AIX always returns powerpc, check here for 64-bit
+ if any_compiler_has_define(compilers, '__64BIT__'):
+ trial = 'ppc64'
+
+ # Add more quirks here as bugs are reported. Keep in sync with
+ # detect_cpu_family() above.
+ return trial
+
+def detect_system() -> str:
+ if sys.platform == 'cygwin':
+ return 'cygwin'
+ return platform.system().lower()
+
+def detect_msys2_arch() -> T.Optional[str]:
+ return os.environ.get('MSYSTEM_CARCH', None)
+
+def detect_machine_info(compilers: T.Optional[CompilersDict] = None) -> MachineInfo:
+ """Detect the machine we're running on
+
+ If compilers are not provided, we cannot know as much. None out those
+ fields to avoid accidentally depending on partial knowledge. The
+ underlying ''detect_*'' method can be called to explicitly use the
+ partial information.
+ """
+ return MachineInfo(
+ detect_system(),
+ detect_cpu_family(compilers) if compilers is not None else None,
+ detect_cpu(compilers) if compilers is not None else None,
+ sys.byteorder)
+
+# TODO make this compare two `MachineInfo`s purely. How important is the
+# `detect_cpu_family({})` distinction? It is the one impediment to that.
+def machine_info_can_run(machine_info: MachineInfo):
+ """Whether we can run binaries for this machine on the current machine.
+
+ Can almost always run 32-bit binaries on 64-bit natively if the host
+ and build systems are the same. We don't pass any compilers to
+ detect_cpu_family() here because we always want to know the OS
+ architecture, not what the compiler environment tells us.
+ """
+ if machine_info.system != detect_system():
+ return False
+ true_build_cpu_family = detect_cpu_family({})
+ return \
+ (machine_info.cpu_family == true_build_cpu_family) or \
+ ((true_build_cpu_family == 'x86_64') and (machine_info.cpu_family == 'x86')) or \
+ ((true_build_cpu_family == 'aarch64') and (machine_info.cpu_family == 'arm'))
+
+class Environment:
+ private_dir = 'meson-private'
+ log_dir = 'meson-logs'
+ info_dir = 'meson-info'
+
+ def __init__(self, source_dir: T.Optional[str], build_dir: T.Optional[str], options: 'argparse.Namespace') -> None:
+ self.source_dir = source_dir
+ self.build_dir = build_dir
+ # Do not try to create build directories when build_dir is none.
+ # This reduced mode is used by the --buildoptions introspector
+ if build_dir is not None:
+ self.scratch_dir = os.path.join(build_dir, Environment.private_dir)
+ self.log_dir = os.path.join(build_dir, Environment.log_dir)
+ self.info_dir = os.path.join(build_dir, Environment.info_dir)
+ os.makedirs(self.scratch_dir, exist_ok=True)
+ os.makedirs(self.log_dir, exist_ok=True)
+ os.makedirs(self.info_dir, exist_ok=True)
+ try:
+ self.coredata = coredata.load(self.get_build_dir()) # type: coredata.CoreData
+ self.first_invocation = False
+ except FileNotFoundError:
+ self.create_new_coredata(options)
+ except coredata.MesonVersionMismatchException as e:
+ # This is routine, but tell the user the update happened
+ mlog.log('Regenerating configuration from scratch:', str(e))
+ coredata.read_cmd_line_file(self.build_dir, options)
+ self.create_new_coredata(options)
+ except MesonException as e:
+ # If we stored previous command line options, we can recover from
+ # a broken/outdated coredata.
+ if os.path.isfile(coredata.get_cmd_line_file(self.build_dir)):
+ mlog.warning('Regenerating configuration from scratch.')
+ mlog.log('Reason:', mlog.red(str(e)))
+ coredata.read_cmd_line_file(self.build_dir, options)
+ self.create_new_coredata(options)
+ else:
+ raise e
+ else:
+ # Just create a fresh coredata in this case
+ self.scratch_dir = ''
+ self.create_new_coredata(options)
+
+ ## locally bind some unfrozen configuration
+
+ # Stores machine infos, the only *three* machine one because we have a
+ # target machine info on for the user (Meson never cares about the
+ # target machine.)
+ machines: PerThreeMachineDefaultable[MachineInfo] = PerThreeMachineDefaultable()
+
+ # Similar to coredata.compilers, but lower level in that there is no
+ # meta data, only names/paths.
+ binaries = PerMachineDefaultable() # type: PerMachineDefaultable[BinaryTable]
+
+ # Misc other properties about each machine.
+ properties = PerMachineDefaultable() # type: PerMachineDefaultable[Properties]
+
+ # CMake toolchain variables
+ cmakevars = PerMachineDefaultable() # type: PerMachineDefaultable[CMakeVariables]
+
+ ## Setup build machine defaults
+
+ # Will be fully initialized later using compilers later.
+ machines.build = detect_machine_info()
+
+ # Just uses hard-coded defaults and environment variables. Might be
+ # overwritten by a native file.
+ binaries.build = BinaryTable()
+ properties.build = Properties()
+
+ # Options with the key parsed into an OptionKey type.
+ #
+ # Note that order matters because of 'buildtype', if it is after
+ # 'optimization' and 'debug' keys, it override them.
+ self.options: T.MutableMapping[OptionKey, T.Union[str, T.List[str]]] = collections.OrderedDict()
+
+ ## Read in native file(s) to override build machine configuration
+
+ if self.coredata.config_files is not None:
+ config = coredata.parse_machine_files(self.coredata.config_files)
+ binaries.build = BinaryTable(config.get('binaries', {}))
+ properties.build = Properties(config.get('properties', {}))
+ cmakevars.build = CMakeVariables(config.get('cmake', {}))
+ self._load_machine_file_options(
+ config, properties.build,
+ MachineChoice.BUILD if self.coredata.cross_files else MachineChoice.HOST)
+
+ ## Read in cross file(s) to override host machine configuration
+
+ if self.coredata.cross_files:
+ config = coredata.parse_machine_files(self.coredata.cross_files)
+ properties.host = Properties(config.get('properties', {}))
+ binaries.host = BinaryTable(config.get('binaries', {}))
+ cmakevars.host = CMakeVariables(config.get('cmake', {}))
+ if 'host_machine' in config:
+ machines.host = MachineInfo.from_literal(config['host_machine'])
+ if 'target_machine' in config:
+ machines.target = MachineInfo.from_literal(config['target_machine'])
+ # Keep only per machine options from the native file. The cross
+ # file takes precedence over all other options.
+ for key, value in list(self.options.items()):
+ if self.coredata.is_per_machine_option(key):
+ self.options[key.as_build()] = value
+ self._load_machine_file_options(config, properties.host, MachineChoice.HOST)
+
+ ## "freeze" now initialized configuration, and "save" to the class.
+
+ self.machines = machines.default_missing()
+ self.binaries = binaries.default_missing()
+ self.properties = properties.default_missing()
+ self.cmakevars = cmakevars.default_missing()
+
+ # Command line options override those from cross/native files
+ self.options.update(options.cmd_line_options)
+
+ # Take default value from env if not set in cross/native files or command line.
+ self._set_default_options_from_env()
+ self._set_default_binaries_from_env()
+ self._set_default_properties_from_env()
+
+ # Warn if the user is using two different ways of setting build-type
+ # options that override each other
+ bt = OptionKey('buildtype')
+ db = OptionKey('debug')
+ op = OptionKey('optimization')
+ if bt in self.options and (db in self.options or op in self.options):
+ mlog.warning('Recommend using either -Dbuildtype or -Doptimization + -Ddebug. '
+ 'Using both is redundant since they override each other. '
+ 'See: https://mesonbuild.com/Builtin-options.html#build-type-options')
+
+ exe_wrapper = self.lookup_binary_entry(MachineChoice.HOST, 'exe_wrapper')
+ if exe_wrapper is not None:
+ self.exe_wrapper = ExternalProgram.from_bin_list(self, MachineChoice.HOST, 'exe_wrapper')
+ else:
+ self.exe_wrapper = None
+
+ self.default_cmake = ['cmake']
+ self.default_pkgconfig = ['pkg-config']
+ self.wrap_resolver: T.Optional['Resolver'] = None
+
+ def _load_machine_file_options(self, config: 'ConfigParser', properties: Properties, machine: MachineChoice) -> None:
+ """Read the contents of a Machine file and put it in the options store."""
+
+ # Look for any options in the deprecated paths section, warn about
+ # those, then assign them. They will be overwritten by the ones in the
+ # "built-in options" section if they're in both sections.
+ paths = config.get('paths')
+ if paths:
+ mlog.deprecation('The [paths] section is deprecated, use the [built-in options] section instead.')
+ for k, v in paths.items():
+ self.options[OptionKey.from_string(k).evolve(machine=machine)] = v
+
+ # Next look for compiler options in the "properties" section, this is
+ # also deprecated, and these will also be overwritten by the "built-in
+ # options" section. We need to remove these from this section, as well.
+ deprecated_properties: T.Set[str] = set()
+ for lang in compilers.all_languages:
+ deprecated_properties.add(lang + '_args')
+ deprecated_properties.add(lang + '_link_args')
+ for k, v in properties.properties.copy().items():
+ if k in deprecated_properties:
+ mlog.deprecation(f'{k} in the [properties] section of the machine file is deprecated, use the [built-in options] section.')
+ self.options[OptionKey.from_string(k).evolve(machine=machine)] = v
+ del properties.properties[k]
+
+ for section, values in config.items():
+ if ':' in section:
+ subproject, section = section.split(':')
+ else:
+ subproject = ''
+ if section == 'built-in options':
+ for k, v in values.items():
+ key = OptionKey.from_string(k)
+ # If we're in the cross file, and there is a `build.foo` warn about that. Later we'll remove it.
+ if machine is MachineChoice.HOST and key.machine is not machine:
+ mlog.deprecation('Setting build machine options in cross files, please use a native file instead, this will be removed in meson 0.60', once=True)
+ if key.subproject:
+ raise MesonException('Do not set subproject options in [built-in options] section, use [subproject:built-in options] instead.')
+ self.options[key.evolve(subproject=subproject, machine=machine)] = v
+ elif section == 'project options' and machine is MachineChoice.HOST:
+ # Project options are only for the host machine, we don't want
+ # to read these from the native file
+ for k, v in values.items():
+ # Project options are always for the host machine
+ key = OptionKey.from_string(k)
+ if key.subproject:
+ raise MesonException('Do not set subproject options in [built-in options] section, use [subproject:built-in options] instead.')
+ self.options[key.evolve(subproject=subproject)] = v
+
+ def _set_default_options_from_env(self) -> None:
+ opts: T.List[T.Tuple[str, str]] = (
+ [(v, f'{k}_args') for k, v in compilers.compilers.CFLAGS_MAPPING.items()] +
+ [
+ ('PKG_CONFIG_PATH', 'pkg_config_path'),
+ ('CMAKE_PREFIX_PATH', 'cmake_prefix_path'),
+ ('LDFLAGS', 'ldflags'),
+ ('CPPFLAGS', 'cppflags'),
+ ]
+ )
+
+ env_opts: T.DefaultDict[OptionKey, T.List[str]] = collections.defaultdict(list)
+
+ for (evar, keyname), for_machine in itertools.product(opts, MachineChoice):
+ p_env = _get_env_var(for_machine, self.is_cross_build(), evar)
+ if p_env is not None:
+ # these may contain duplicates, which must be removed, else
+ # a duplicates-in-array-option warning arises.
+ if keyname == 'cmake_prefix_path':
+ if self.machines[for_machine].is_windows():
+ # Cannot split on ':' on Windows because its in the drive letter
+ _p_env = p_env.split(os.pathsep)
+ else:
+ # https://github.com/mesonbuild/meson/issues/7294
+ _p_env = re.split(r':|;', p_env)
+ p_list = list(mesonlib.OrderedSet(_p_env))
+ elif keyname == 'pkg_config_path':
+ p_list = list(mesonlib.OrderedSet(p_env.split(os.pathsep)))
+ else:
+ p_list = split_args(p_env)
+ p_list = [e for e in p_list if e] # filter out any empty elements
+
+ # Take env vars only on first invocation, if the env changes when
+ # reconfiguring it gets ignored.
+ # FIXME: We should remember if we took the value from env to warn
+ # if it changes on future invocations.
+ if self.first_invocation:
+ if keyname == 'ldflags':
+ key = OptionKey('link_args', machine=for_machine, lang='c') # needs a language to initialize properly
+ for lang in compilers.compilers.LANGUAGES_USING_LDFLAGS:
+ key = key.evolve(lang=lang)
+ env_opts[key].extend(p_list)
+ elif keyname == 'cppflags':
+ key = OptionKey('env_args', machine=for_machine, lang='c')
+ for lang in compilers.compilers.LANGUAGES_USING_CPPFLAGS:
+ key = key.evolve(lang=lang)
+ env_opts[key].extend(p_list)
+ else:
+ key = OptionKey.from_string(keyname).evolve(machine=for_machine)
+ if evar in compilers.compilers.CFLAGS_MAPPING.values():
+ # If this is an environment variable, we have to
+ # store it separately until the compiler is
+ # instantiated, as we don't know whether the
+ # compiler will want to use these arguments at link
+ # time and compile time (instead of just at compile
+ # time) until we're instantiating that `Compiler`
+ # object. This is required so that passing
+ # `-Dc_args=` on the command line and `$CFLAGS`
+ # have subtely different behavior. `$CFLAGS` will be
+ # added to the linker command line if the compiler
+ # acts as a linker driver, `-Dc_args` will not.
+ #
+ # We still use the original key as the base here, as
+ # we want to inherit the machine and the compiler
+ # language
+ key = key.evolve('env_args')
+ env_opts[key].extend(p_list)
+
+ # Only store options that are not already in self.options,
+ # otherwise we'd override the machine files
+ for k, v in env_opts.items():
+ if k not in self.options:
+ self.options[k] = v
+
+ def _set_default_binaries_from_env(self) -> None:
+ """Set default binaries from the environment.
+
+ For example, pkg-config can be set via PKG_CONFIG, or in the machine
+ file. We want to set the default to the env variable.
+ """
+ opts = itertools.chain(envconfig.DEPRECATED_ENV_PROG_MAP.items(),
+ envconfig.ENV_VAR_PROG_MAP.items())
+
+ for (name, evar), for_machine in itertools.product(opts, MachineChoice):
+ p_env = _get_env_var(for_machine, self.is_cross_build(), evar)
+ if p_env is not None:
+ self.binaries[for_machine].binaries.setdefault(name, mesonlib.split_args(p_env))
+
+ def _set_default_properties_from_env(self) -> None:
+ """Properties which can also be set from the environment."""
+ # name, evar, split
+ opts: T.List[T.Tuple[str, T.List[str], bool]] = [
+ ('boost_includedir', ['BOOST_INCLUDEDIR'], False),
+ ('boost_librarydir', ['BOOST_LIBRARYDIR'], False),
+ ('boost_root', ['BOOST_ROOT', 'BOOSTROOT'], True),
+ ('java_home', ['JAVA_HOME'], False),
+ ]
+
+ for (name, evars, split), for_machine in itertools.product(opts, MachineChoice):
+ for evar in evars:
+ p_env = _get_env_var(for_machine, self.is_cross_build(), evar)
+ if p_env is not None:
+ if split:
+ self.properties[for_machine].properties.setdefault(name, p_env.split(os.pathsep))
+ else:
+ self.properties[for_machine].properties.setdefault(name, p_env)
+ break
+
+ def create_new_coredata(self, options: 'argparse.Namespace') -> None:
+ # WARNING: Don't use any values from coredata in __init__. It gets
+ # re-initialized with project options by the interpreter during
+ # build file parsing.
+ # meson_command is used by the regenchecker script, which runs meson
+ self.coredata = coredata.CoreData(options, self.scratch_dir, mesonlib.get_meson_command())
+ self.first_invocation = True
+
+ def is_cross_build(self, when_building_for: MachineChoice = MachineChoice.HOST) -> bool:
+ return self.coredata.is_cross_build(when_building_for)
+
+ def dump_coredata(self) -> str:
+ return coredata.save(self.coredata, self.get_build_dir())
+
+ def get_log_dir(self) -> str:
+ return self.log_dir
+
+ def get_coredata(self) -> coredata.CoreData:
+ return self.coredata
+
+ @staticmethod
+ def get_build_command(unbuffered: bool = False) -> T.List[str]:
+ cmd = mesonlib.get_meson_command()
+ if cmd is None:
+ raise MesonBugException('No command?')
+ cmd = cmd.copy()
+ if unbuffered and 'python' in os.path.basename(cmd[0]):
+ cmd.insert(1, '-u')
+ return cmd
+
+ def is_header(self, fname: 'mesonlib.FileOrString') -> bool:
+ return is_header(fname)
+
+ def is_source(self, fname: 'mesonlib.FileOrString') -> bool:
+ return is_source(fname)
+
+ def is_assembly(self, fname: 'mesonlib.FileOrString') -> bool:
+ return is_assembly(fname)
+
+ def is_llvm_ir(self, fname: 'mesonlib.FileOrString') -> bool:
+ return is_llvm_ir(fname)
+
+ def is_object(self, fname: 'mesonlib.FileOrString') -> bool:
+ return is_object(fname)
+
+ @lru_cache(maxsize=None)
+ def is_library(self, fname):
+ return is_library(fname)
+
+ def lookup_binary_entry(self, for_machine: MachineChoice, name: str) -> T.Optional[T.List[str]]:
+ return self.binaries[for_machine].lookup_entry(name)
+
+ def get_scratch_dir(self) -> str:
+ return self.scratch_dir
+
+ def get_source_dir(self) -> str:
+ return self.source_dir
+
+ def get_build_dir(self) -> str:
+ return self.build_dir
+
+ def get_import_lib_dir(self) -> str:
+ "Install dir for the import library (library used for linking)"
+ return self.get_libdir()
+
+ def get_shared_module_dir(self) -> str:
+ "Install dir for shared modules that are loaded at runtime"
+ return self.get_libdir()
+
+ def get_shared_lib_dir(self) -> str:
+ "Install dir for the shared library"
+ m = self.machines.host
+ # Windows has no RPATH or similar, so DLLs must be next to EXEs.
+ if m.is_windows() or m.is_cygwin():
+ return self.get_bindir()
+ return self.get_libdir()
+
+ def get_jar_dir(self) -> str:
+ """Install dir for JAR files"""
+ return f"{self.get_datadir()}/java"
+
+ def get_static_lib_dir(self) -> str:
+ "Install dir for the static library"
+ return self.get_libdir()
+
+ def get_prefix(self) -> str:
+ return self.coredata.get_option(OptionKey('prefix'))
+
+ def get_libdir(self) -> str:
+ return self.coredata.get_option(OptionKey('libdir'))
+
+ def get_libexecdir(self) -> str:
+ return self.coredata.get_option(OptionKey('libexecdir'))
+
+ def get_bindir(self) -> str:
+ return self.coredata.get_option(OptionKey('bindir'))
+
+ def get_includedir(self) -> str:
+ return self.coredata.get_option(OptionKey('includedir'))
+
+ def get_mandir(self) -> str:
+ return self.coredata.get_option(OptionKey('mandir'))
+
+ def get_datadir(self) -> str:
+ return self.coredata.get_option(OptionKey('datadir'))
+
+ def get_compiler_system_dirs(self, for_machine: MachineChoice):
+ for comp in self.coredata.compilers[for_machine].values():
+ if comp.id == 'clang':
+ index = 1
+ break
+ elif comp.id == 'gcc':
+ index = 2
+ break
+ else:
+ # This option is only supported by gcc and clang. If we don't get a
+ # GCC or Clang compiler return and empty list.
+ return []
+
+ p, out, _ = Popen_safe(comp.get_exelist() + ['-print-search-dirs'])
+ if p.returncode != 0:
+ raise mesonlib.MesonException('Could not calculate system search dirs')
+ out = out.split('\n')[index].lstrip('libraries: =').split(':')
+ return [os.path.normpath(p) for p in out]
+
+ def need_exe_wrapper(self, for_machine: MachineChoice = MachineChoice.HOST):
+ value = self.properties[for_machine].get('needs_exe_wrapper', None)
+ if value is not None:
+ return value
+ return not machine_info_can_run(self.machines[for_machine])
+
+ def get_exe_wrapper(self) -> T.Optional[ExternalProgram]:
+ if not self.need_exe_wrapper():
+ return None
+ return self.exe_wrapper
diff --git a/mesonbuild/interpreter/__init__.py b/mesonbuild/interpreter/__init__.py
new file mode 100644
index 0000000..016e4dc
--- /dev/null
+++ b/mesonbuild/interpreter/__init__.py
@@ -0,0 +1,59 @@
+# SPDX-license-identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# 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.
+
+"""Meson interpreter."""
+
+__all__ = [
+ 'Interpreter',
+ 'permitted_dependency_kwargs',
+
+ 'CompilerHolder',
+
+ 'ExecutableHolder',
+ 'BuildTargetHolder',
+ 'CustomTargetHolder',
+ 'CustomTargetIndexHolder',
+ 'MachineHolder',
+ 'Test',
+ 'ConfigurationDataHolder',
+ 'SubprojectHolder',
+ 'DependencyHolder',
+ 'GeneratedListHolder',
+ 'ExternalProgramHolder',
+ 'extract_required_kwarg',
+
+ 'ArrayHolder',
+ 'BooleanHolder',
+ 'DictHolder',
+ 'IntegerHolder',
+ 'StringHolder',
+]
+
+from .interpreter import Interpreter, permitted_dependency_kwargs
+from .compiler import CompilerHolder
+from .interpreterobjects import (ExecutableHolder, BuildTargetHolder, CustomTargetHolder,
+ CustomTargetIndexHolder, MachineHolder, Test,
+ ConfigurationDataHolder, SubprojectHolder, DependencyHolder,
+ GeneratedListHolder, ExternalProgramHolder,
+ extract_required_kwarg)
+
+from .primitives import (
+ ArrayHolder,
+ BooleanHolder,
+ DictHolder,
+ IntegerHolder,
+ StringHolder,
+)
diff --git a/mesonbuild/interpreter/compiler.py b/mesonbuild/interpreter/compiler.py
new file mode 100644
index 0000000..95126cf
--- /dev/null
+++ b/mesonbuild/interpreter/compiler.py
@@ -0,0 +1,781 @@
+# SPDX-Licnese-Identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+from __future__ import annotations
+
+import enum
+import functools
+import os
+import typing as T
+
+from .. import build
+from .. import coredata
+from .. import dependencies
+from .. import mesonlib
+from .. import mlog
+from ..compilers import SUFFIX_TO_LANG
+from ..compilers.compilers import CompileCheckMode
+from ..interpreterbase import (ObjectHolder, noPosargs, noKwargs,
+ FeatureNew, disablerIfNotFound,
+ InterpreterException)
+from ..interpreterbase.decorators import ContainerTypeInfo, typed_kwargs, KwargInfo, typed_pos_args
+from ..mesonlib import OptionKey
+from .interpreterobjects import (extract_required_kwarg, extract_search_dirs)
+from .type_checking import REQUIRED_KW, in_set_validator, NoneType
+
+if T.TYPE_CHECKING:
+ from ..interpreter import Interpreter
+ from ..compilers import Compiler, RunResult
+ from ..interpreterbase import TYPE_var, TYPE_kwargs
+ from .kwargs import ExtractRequired, ExtractSearchDirs
+
+ from typing_extensions import TypedDict, Literal
+
+ class GetSupportedArgumentKw(TypedDict):
+
+ checked: Literal['warn', 'require', 'off']
+
+ class AlignmentKw(TypedDict):
+
+ prefix: str
+ args: T.List[str]
+ dependencies: T.List[dependencies.Dependency]
+
+ class CompileKW(TypedDict):
+
+ name: str
+ no_builtin_args: bool
+ include_directories: T.List[build.IncludeDirs]
+ args: T.List[str]
+ dependencies: T.List[dependencies.Dependency]
+
+ class CommonKW(TypedDict):
+
+ prefix: str
+ no_builtin_args: bool
+ include_directories: T.List[build.IncludeDirs]
+ args: T.List[str]
+ dependencies: T.List[dependencies.Dependency]
+
+ class CompupteIntKW(CommonKW):
+
+ guess: T.Optional[int]
+ high: T.Optional[int]
+ low: T.Optional[int]
+
+ class HeaderKW(CommonKW, ExtractRequired):
+ pass
+
+ class FindLibraryKW(ExtractRequired, ExtractSearchDirs):
+
+ disabler: bool
+ has_headers: T.List[str]
+ static: bool
+
+ # This list must be all of the `HeaderKW` values with `header_`
+ # prepended to the key
+ header_args: T.List[str]
+ header_dependencies: T.List[dependencies.Dependency]
+ header_include_directories: T.List[build.IncludeDirs]
+ header_no_builtin_args: bool
+ header_prefix: str
+ header_required: T.Union[bool, coredata.UserFeatureOption]
+
+ class PreprocessKW(TypedDict):
+ output: str
+ compile_args: T.List[str]
+ include_directories: T.List[build.IncludeDirs]
+
+
+class _TestMode(enum.Enum):
+
+ """Whether we're doing a compiler or linker check."""
+
+ COMPILER = 0
+ LINKER = 1
+
+
+class TryRunResultHolder(ObjectHolder['RunResult']):
+ def __init__(self, res: 'RunResult', interpreter: 'Interpreter'):
+ super().__init__(res, interpreter)
+ self.methods.update({'returncode': self.returncode_method,
+ 'compiled': self.compiled_method,
+ 'stdout': self.stdout_method,
+ 'stderr': self.stderr_method,
+ })
+
+ @noPosargs
+ @noKwargs
+ def returncode_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> int:
+ return self.held_object.returncode
+
+ @noPosargs
+ @noKwargs
+ def compiled_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
+ return self.held_object.compiled
+
+ @noPosargs
+ @noKwargs
+ def stdout_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.held_object.stdout
+
+ @noPosargs
+ @noKwargs
+ def stderr_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.held_object.stderr
+
+
+_ARGS_KW: KwargInfo[T.List[str]] = KwargInfo(
+ 'args',
+ ContainerTypeInfo(list, str),
+ listify=True,
+ default=[],
+)
+_DEPENDENCIES_KW: KwargInfo[T.List['dependencies.Dependency']] = KwargInfo(
+ 'dependencies',
+ ContainerTypeInfo(list, dependencies.Dependency),
+ listify=True,
+ default=[],
+)
+_INCLUDE_DIRS_KW: KwargInfo[T.List[build.IncludeDirs]] = KwargInfo(
+ 'include_directories',
+ ContainerTypeInfo(list, build.IncludeDirs),
+ default=[],
+ listify=True,
+)
+_PREFIX_KW: KwargInfo[str] = KwargInfo(
+ 'prefix',
+ (str, ContainerTypeInfo(list, str)),
+ default='',
+ since_values={list: '1.0.0'},
+ convertor=lambda x: '\n'.join(x) if isinstance(x, list) else x)
+
+_NO_BUILTIN_ARGS_KW = KwargInfo('no_builtin_args', bool, default=False)
+_NAME_KW = KwargInfo('name', str, default='')
+
+# Many of the compiler methods take this kwarg signature exactly, this allows
+# simplifying the `typed_kwargs` calls
+_COMMON_KWS: T.List[KwargInfo] = [_ARGS_KW, _DEPENDENCIES_KW, _INCLUDE_DIRS_KW, _PREFIX_KW, _NO_BUILTIN_ARGS_KW]
+
+# Common methods of compiles, links, runs, and similar
+_COMPILES_KWS: T.List[KwargInfo] = [_NAME_KW, _ARGS_KW, _DEPENDENCIES_KW, _INCLUDE_DIRS_KW, _NO_BUILTIN_ARGS_KW]
+
+_HEADER_KWS: T.List[KwargInfo] = [REQUIRED_KW.evolve(since='0.50.0', default=False), *_COMMON_KWS]
+
+class CompilerHolder(ObjectHolder['Compiler']):
+ def __init__(self, compiler: 'Compiler', interpreter: 'Interpreter'):
+ super().__init__(compiler, interpreter)
+ self.environment = self.env
+ self.methods.update({'compiles': self.compiles_method,
+ 'links': self.links_method,
+ 'get_id': self.get_id_method,
+ 'get_linker_id': self.get_linker_id_method,
+ 'compute_int': self.compute_int_method,
+ 'sizeof': self.sizeof_method,
+ 'get_define': self.get_define_method,
+ 'check_header': self.check_header_method,
+ 'has_header': self.has_header_method,
+ 'has_header_symbol': self.has_header_symbol_method,
+ 'run': self.run_method,
+ 'has_function': self.has_function_method,
+ 'has_member': self.has_member_method,
+ 'has_members': self.has_members_method,
+ 'has_type': self.has_type_method,
+ 'alignment': self.alignment_method,
+ 'version': self.version_method,
+ 'cmd_array': self.cmd_array_method,
+ 'find_library': self.find_library_method,
+ 'has_argument': self.has_argument_method,
+ 'has_function_attribute': self.has_func_attribute_method,
+ 'get_supported_function_attributes': self.get_supported_function_attributes_method,
+ 'has_multi_arguments': self.has_multi_arguments_method,
+ 'get_supported_arguments': self.get_supported_arguments_method,
+ 'first_supported_argument': self.first_supported_argument_method,
+ 'has_link_argument': self.has_link_argument_method,
+ 'has_multi_link_arguments': self.has_multi_link_arguments_method,
+ 'get_supported_link_arguments': self.get_supported_link_arguments_method,
+ 'first_supported_link_argument': self.first_supported_link_argument_method,
+ 'symbols_have_underscore_prefix': self.symbols_have_underscore_prefix_method,
+ 'get_argument_syntax': self.get_argument_syntax_method,
+ 'preprocess': self.preprocess_method,
+ })
+
+ @property
+ def compiler(self) -> 'Compiler':
+ return self.held_object
+
+ def _dep_msg(self, deps: T.List['dependencies.Dependency'], compile_only: bool, endl: str) -> str:
+ msg_single = 'with dependency {}'
+ msg_many = 'with dependencies {}'
+ names = []
+ for d in deps:
+ if isinstance(d, dependencies.InternalDependency):
+ FeatureNew.single_use('compiler method "dependencies" kwarg with internal dep', '0.57.0', self.subproject,
+ location=self.current_node)
+ continue
+ if isinstance(d, dependencies.ExternalLibrary):
+ if compile_only:
+ continue
+ name = '-l' + d.name
+ else:
+ name = d.name
+ names.append(name)
+ if not names:
+ return endl
+ tpl = msg_many if len(names) > 1 else msg_single
+ if endl is None:
+ endl = ''
+ return tpl.format(', '.join(names)) + endl
+
+ @noPosargs
+ @noKwargs
+ def version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.compiler.version
+
+ @noPosargs
+ @noKwargs
+ def cmd_array_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> T.List[str]:
+ return self.compiler.exelist
+
+ def _determine_args(self, nobuiltins: bool,
+ incdirs: T.List[build.IncludeDirs],
+ extra_args: T.List[str],
+ mode: CompileCheckMode = CompileCheckMode.LINK) -> T.List[str]:
+ args: T.List[str] = []
+ for i in incdirs:
+ for idir in i.to_string_list(self.environment.get_source_dir()):
+ args.extend(self.compiler.get_include_args(idir, False))
+ if not nobuiltins:
+ opts = self.environment.coredata.options
+ args += self.compiler.get_option_compile_args(opts)
+ if mode is CompileCheckMode.LINK:
+ args.extend(self.compiler.get_option_link_args(opts))
+ args.extend(extra_args)
+ return args
+
+ def _determine_dependencies(self, deps: T.List['dependencies.Dependency'], compile_only: bool = False, endl: str = ':') -> T.Tuple[T.List['dependencies.Dependency'], str]:
+ deps = dependencies.get_leaf_external_dependencies(deps)
+ return deps, self._dep_msg(deps, compile_only, endl)
+
+ @typed_pos_args('compiler.alignment', str)
+ @typed_kwargs(
+ 'compiler.alignment',
+ _PREFIX_KW,
+ _ARGS_KW,
+ _DEPENDENCIES_KW,
+ )
+ def alignment_method(self, args: T.Tuple[str], kwargs: 'AlignmentKw') -> int:
+ typename = args[0]
+ deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=self.compiler.is_cross)
+ result = self.compiler.alignment(typename, kwargs['prefix'], self.environment,
+ extra_args=kwargs['args'],
+ dependencies=deps)
+ mlog.log('Checking for alignment of', mlog.bold(typename, True), msg, result)
+ return result
+
+ @typed_pos_args('compiler.run', (str, mesonlib.File))
+ @typed_kwargs('compiler.run', *_COMPILES_KWS)
+ def run_method(self, args: T.Tuple['mesonlib.FileOrString'], kwargs: 'CompileKW') -> 'RunResult':
+ code = args[0]
+ if isinstance(code, mesonlib.File):
+ self.interpreter.add_build_def_file(code)
+ code = mesonlib.File.from_absolute_file(
+ code.rel_to_builddir(self.environment.source_dir))
+ testname = kwargs['name']
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=False, endl=None)
+ result = self.compiler.run(code, self.environment, extra_args=extra_args,
+ dependencies=deps)
+ if testname:
+ if not result.compiled:
+ h = mlog.red('DID NOT COMPILE')
+ elif result.returncode == 0:
+ h = mlog.green('YES')
+ else:
+ h = mlog.red(f'NO ({result.returncode})')
+ mlog.log('Checking if', mlog.bold(testname, True), msg, 'runs:', h)
+ return result
+
+ @noPosargs
+ @noKwargs
+ def get_id_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.compiler.get_id()
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('compiler.get_linker_id', '0.53.0')
+ def get_linker_id_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.compiler.get_linker_id()
+
+ @noPosargs
+ @noKwargs
+ def symbols_have_underscore_prefix_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
+ '''
+ Check if the compiler prefixes _ (underscore) to global C symbols
+ See: https://en.wikipedia.org/wiki/Name_mangling#C
+ '''
+ return self.compiler.symbols_have_underscore_prefix(self.environment)
+
+ @typed_pos_args('compiler.has_member', str, str)
+ @typed_kwargs('compiler.has_member', *_COMMON_KWS)
+ def has_member_method(self, args: T.Tuple[str, str], kwargs: 'CommonKW') -> bool:
+ typename, membername = args
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'])
+ had, cached = self.compiler.has_members(typename, [membername], kwargs['prefix'],
+ self.environment,
+ extra_args=extra_args,
+ dependencies=deps)
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ if had:
+ hadtxt = mlog.green('YES')
+ else:
+ hadtxt = mlog.red('NO')
+ mlog.log('Checking whether type', mlog.bold(typename, True),
+ 'has member', mlog.bold(membername, True), msg, hadtxt, cached_msg)
+ return had
+
+ @typed_pos_args('compiler.has_members', str, varargs=str, min_varargs=1)
+ @typed_kwargs('compiler.has_members', *_COMMON_KWS)
+ def has_members_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'CommonKW') -> bool:
+ typename, membernames = args
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'])
+ had, cached = self.compiler.has_members(typename, membernames, kwargs['prefix'],
+ self.environment,
+ extra_args=extra_args,
+ dependencies=deps)
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ if had:
+ hadtxt = mlog.green('YES')
+ else:
+ hadtxt = mlog.red('NO')
+ members = mlog.bold(', '.join([f'"{m}"' for m in membernames]))
+ mlog.log('Checking whether type', mlog.bold(typename, True),
+ 'has members', members, msg, hadtxt, cached_msg)
+ return had
+
+ @typed_pos_args('compiler.has_function', str)
+ @typed_kwargs('compiler.has_function', *_COMMON_KWS)
+ def has_function_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> bool:
+ funcname = args[0]
+ extra_args = self._determine_args(kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=False)
+ had, cached = self.compiler.has_function(funcname, kwargs['prefix'], self.environment,
+ extra_args=extra_args,
+ dependencies=deps)
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ if had:
+ hadtxt = mlog.green('YES')
+ else:
+ hadtxt = mlog.red('NO')
+ mlog.log('Checking for function', mlog.bold(funcname, True), msg, hadtxt, cached_msg)
+ return had
+
+ @typed_pos_args('compiler.has_type', str)
+ @typed_kwargs('compiler.has_type', *_COMMON_KWS)
+ def has_type_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> bool:
+ typename = args[0]
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'])
+ had, cached = self.compiler.has_type(typename, kwargs['prefix'], self.environment,
+ extra_args=extra_args, dependencies=deps)
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ if had:
+ hadtxt = mlog.green('YES')
+ else:
+ hadtxt = mlog.red('NO')
+ mlog.log('Checking for type', mlog.bold(typename, True), msg, hadtxt, cached_msg)
+ return had
+
+ @FeatureNew('compiler.compute_int', '0.40.0')
+ @typed_pos_args('compiler.compute_int', str)
+ @typed_kwargs(
+ 'compiler.compute_int',
+ KwargInfo('low', (int, NoneType)),
+ KwargInfo('high', (int, NoneType)),
+ KwargInfo('guess', (int, NoneType)),
+ *_COMMON_KWS,
+ )
+ def compute_int_method(self, args: T.Tuple[str], kwargs: 'CompupteIntKW') -> int:
+ expression = args[0]
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=self.compiler.is_cross)
+ res = self.compiler.compute_int(expression, kwargs['low'], kwargs['high'],
+ kwargs['guess'], kwargs['prefix'],
+ self.environment, extra_args=extra_args,
+ dependencies=deps)
+ mlog.log('Computing int of', mlog.bold(expression, True), msg, res)
+ return res
+
+ @typed_pos_args('compiler.sizeof', str)
+ @typed_kwargs('compiler.sizeof', *_COMMON_KWS)
+ def sizeof_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> int:
+ element = args[0]
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=self.compiler.is_cross)
+ esize = self.compiler.sizeof(element, kwargs['prefix'], self.environment,
+ extra_args=extra_args, dependencies=deps)
+ mlog.log('Checking for size of', mlog.bold(element, True), msg, esize)
+ return esize
+
+ @FeatureNew('compiler.get_define', '0.40.0')
+ @typed_pos_args('compiler.get_define', str)
+ @typed_kwargs('compiler.get_define', *_COMMON_KWS)
+ def get_define_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> str:
+ element = args[0]
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'])
+ value, cached = self.compiler.get_define(element, kwargs['prefix'], self.environment,
+ extra_args=extra_args,
+ dependencies=deps)
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ mlog.log('Fetching value of define', mlog.bold(element, True), msg, value, cached_msg)
+ return value
+
+ @typed_pos_args('compiler.compiles', (str, mesonlib.File))
+ @typed_kwargs('compiler.compiles', *_COMPILES_KWS)
+ def compiles_method(self, args: T.Tuple['mesonlib.FileOrString'], kwargs: 'CompileKW') -> bool:
+ code = args[0]
+ if isinstance(code, mesonlib.File):
+ self.interpreter.add_build_def_file(code)
+ code = mesonlib.File.from_absolute_file(
+ code.rel_to_builddir(self.environment.source_dir))
+ testname = kwargs['name']
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'], endl=None)
+ result, cached = self.compiler.compiles(code, self.environment,
+ extra_args=extra_args,
+ dependencies=deps)
+ if testname:
+ if result:
+ h = mlog.green('YES')
+ else:
+ h = mlog.red('NO')
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ mlog.log('Checking if', mlog.bold(testname, True), msg, 'compiles:', h, cached_msg)
+ return result
+
+ @typed_pos_args('compiler.links', (str, mesonlib.File))
+ @typed_kwargs('compiler.links', *_COMPILES_KWS)
+ def links_method(self, args: T.Tuple['mesonlib.FileOrString'], kwargs: 'CompileKW') -> bool:
+ code = args[0]
+ compiler = None
+ if isinstance(code, mesonlib.File):
+ self.interpreter.add_build_def_file(code)
+ code = mesonlib.File.from_absolute_file(
+ code.rel_to_builddir(self.environment.source_dir))
+ suffix = code.suffix
+ if suffix not in self.compiler.file_suffixes:
+ for_machine = self.compiler.for_machine
+ clist = self.interpreter.coredata.compilers[for_machine]
+ if suffix not in SUFFIX_TO_LANG:
+ # just pass it to the compiler driver
+ mlog.warning(f'Unknown suffix for test file {code}')
+ elif SUFFIX_TO_LANG[suffix] not in clist:
+ mlog.warning(f'Passed {SUFFIX_TO_LANG[suffix]} source to links method, not specified for {for_machine.get_lower_case_name()} machine.')
+ else:
+ compiler = clist[SUFFIX_TO_LANG[suffix]]
+
+ testname = kwargs['name']
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=False)
+ result, cached = self.compiler.links(code, self.environment,
+ compiler=compiler,
+ extra_args=extra_args,
+ dependencies=deps)
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ if testname:
+ if result:
+ h = mlog.green('YES')
+ else:
+ h = mlog.red('NO')
+ mlog.log('Checking if', mlog.bold(testname, True), msg, 'links:', h, cached_msg)
+ return result
+
+ @FeatureNew('compiler.check_header', '0.47.0')
+ @typed_pos_args('compiler.check_header', str)
+ @typed_kwargs('compiler.check_header', *_HEADER_KWS)
+ def check_header_method(self, args: T.Tuple[str], kwargs: 'HeaderKW') -> bool:
+ hname = args[0]
+ disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False)
+ if disabled:
+ mlog.log('Check usable header', mlog.bold(hname, True), 'skipped: feature', mlog.bold(feature), 'disabled')
+ return False
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'])
+ haz, cached = self.compiler.check_header(hname, kwargs['prefix'], self.environment,
+ extra_args=extra_args,
+ dependencies=deps)
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ if required and not haz:
+ raise InterpreterException(f'{self.compiler.get_display_language()} header {hname!r} not usable')
+ elif haz:
+ h = mlog.green('YES')
+ else:
+ h = mlog.red('NO')
+ mlog.log('Check usable header', mlog.bold(hname, True), msg, h, cached_msg)
+ return haz
+
+ def _has_header_impl(self, hname: str, kwargs: 'HeaderKW') -> bool:
+ disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False)
+ if disabled:
+ mlog.log('Has header', mlog.bold(hname, True), 'skipped: feature', mlog.bold(feature), 'disabled')
+ return False
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'])
+ haz, cached = self.compiler.has_header(hname, kwargs['prefix'], self.environment,
+ extra_args=extra_args, dependencies=deps)
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ if required and not haz:
+ raise InterpreterException(f'{self.compiler.get_display_language()} header {hname!r} not found')
+ elif haz:
+ h = mlog.green('YES')
+ else:
+ h = mlog.red('NO')
+ mlog.log('Has header', mlog.bold(hname, True), msg, h, cached_msg)
+ return haz
+
+ @typed_pos_args('compiler.has_header', str)
+ @typed_kwargs('compiler.has_header', *_HEADER_KWS)
+ def has_header_method(self, args: T.Tuple[str], kwargs: 'HeaderKW') -> bool:
+ return self._has_header_impl(args[0], kwargs)
+
+ @typed_pos_args('compiler.has_header_symbol', str, str)
+ @typed_kwargs('compiler.has_header_symbol', *_HEADER_KWS)
+ def has_header_symbol_method(self, args: T.Tuple[str, str], kwargs: 'HeaderKW') -> bool:
+ hname, symbol = args
+ disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False)
+ if disabled:
+ mlog.log('Header', mlog.bold(hname, True), 'has symbol', mlog.bold(symbol, True), 'skipped: feature', mlog.bold(feature), 'disabled')
+ return False
+ extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
+ deps, msg = self._determine_dependencies(kwargs['dependencies'])
+ haz, cached = self.compiler.has_header_symbol(hname, symbol, kwargs['prefix'], self.environment,
+ extra_args=extra_args,
+ dependencies=deps)
+ if required and not haz:
+ raise InterpreterException(f'{self.compiler.get_display_language()} symbol {symbol} not found in header {hname}')
+ elif haz:
+ h = mlog.green('YES')
+ else:
+ h = mlog.red('NO')
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ mlog.log('Header', mlog.bold(hname, True), 'has symbol', mlog.bold(symbol, True), msg, h, cached_msg)
+ return haz
+
+ def notfound_library(self, libname: str) -> 'dependencies.ExternalLibrary':
+ lib = dependencies.ExternalLibrary(libname, None,
+ self.environment,
+ self.compiler.language,
+ silent=True)
+ return lib
+
+ @disablerIfNotFound
+ @typed_pos_args('compiler.find_library', str)
+ @typed_kwargs(
+ 'compiler.find_library',
+ KwargInfo('required', (bool, coredata.UserFeatureOption), default=True),
+ KwargInfo('has_headers', ContainerTypeInfo(list, str), listify=True, default=[], since='0.50.0'),
+ KwargInfo('static', (bool, NoneType), since='0.51.0'),
+ KwargInfo('disabler', bool, default=False, since='0.49.0'),
+ KwargInfo('dirs', ContainerTypeInfo(list, str), listify=True, default=[]),
+ *(k.evolve(name=f'header_{k.name}') for k in _HEADER_KWS)
+ )
+ def find_library_method(self, args: T.Tuple[str], kwargs: 'FindLibraryKW') -> 'dependencies.ExternalLibrary':
+ # TODO add dependencies support?
+ libname = args[0]
+
+ disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
+ if disabled:
+ mlog.log('Library', mlog.bold(libname), 'skipped: feature', mlog.bold(feature), 'disabled')
+ return self.notfound_library(libname)
+
+ # This could be done with a comprehension, but that confuses the type
+ # checker, and having it check this seems valuable
+ has_header_kwargs: 'HeaderKW' = {
+ 'required': required,
+ 'args': kwargs['header_args'],
+ 'dependencies': kwargs['header_dependencies'],
+ 'include_directories': kwargs['header_include_directories'],
+ 'prefix': kwargs['header_prefix'],
+ 'no_builtin_args': kwargs['header_no_builtin_args'],
+ }
+ for h in kwargs['has_headers']:
+ if not self._has_header_impl(h, has_header_kwargs):
+ return self.notfound_library(libname)
+
+ search_dirs = extract_search_dirs(kwargs)
+
+ prefer_static = self.environment.coredata.get_option(OptionKey('prefer_static'))
+ if kwargs['static'] is True:
+ libtype = mesonlib.LibType.STATIC
+ elif kwargs['static'] is False:
+ libtype = mesonlib.LibType.SHARED
+ elif prefer_static:
+ libtype = mesonlib.LibType.PREFER_STATIC
+ else:
+ libtype = mesonlib.LibType.PREFER_SHARED
+ linkargs = self.compiler.find_library(libname, self.environment, search_dirs, libtype)
+ if required and not linkargs:
+ if libtype == mesonlib.LibType.PREFER_SHARED:
+ libtype_s = 'shared or static'
+ else:
+ libtype_s = libtype.name.lower()
+ raise InterpreterException('{} {} library {!r} not found'
+ .format(self.compiler.get_display_language(),
+ libtype_s, libname))
+ lib = dependencies.ExternalLibrary(libname, linkargs, self.environment,
+ self.compiler.language)
+ return lib
+
+ def _has_argument_impl(self, arguments: T.Union[str, T.List[str]],
+ mode: _TestMode = _TestMode.COMPILER) -> bool:
+ """Shared implementation for methods checking compiler and linker arguments."""
+ # This simplifies the callers
+ if isinstance(arguments, str):
+ arguments = [arguments]
+ test = self.compiler.has_multi_link_arguments if mode is _TestMode.LINKER else self.compiler.has_multi_arguments
+ result, cached = test(arguments, self.environment)
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ mlog.log(
+ 'Compiler for',
+ self.compiler.get_display_language(),
+ 'supports{}'.format(' link' if mode is _TestMode.LINKER else ''),
+ 'arguments {}:'.format(' '.join(arguments)),
+ mlog.green('YES') if result else mlog.red('NO'),
+ cached_msg)
+ return result
+
+ @noKwargs
+ @typed_pos_args('compiler.has_argument', str)
+ def has_argument_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool:
+ return self._has_argument_impl([args[0]])
+
+ @noKwargs
+ @typed_pos_args('compiler.has_multi_arguments', varargs=str)
+ @FeatureNew('compiler.has_multi_arguments', '0.37.0')
+ def has_multi_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> bool:
+ return self._has_argument_impl(args[0])
+
+ @FeatureNew('compiler.get_supported_arguments', '0.43.0')
+ @typed_pos_args('compiler.get_supported_arguments', varargs=str)
+ @typed_kwargs(
+ 'compiler.get_supported_arguments',
+ KwargInfo('checked', str, default='off', since='0.59.0',
+ validator=in_set_validator({'warn', 'require', 'off'})),
+ )
+ def get_supported_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'GetSupportedArgumentKw') -> T.List[str]:
+ supported_args: T.List[str] = []
+ checked = kwargs['checked']
+
+ for arg in args[0]:
+ if not self._has_argument_impl([arg]):
+ msg = f'Compiler for {self.compiler.get_display_language()} does not support "{arg}"'
+ if checked == 'warn':
+ mlog.warning(msg)
+ elif checked == 'require':
+ raise mesonlib.MesonException(msg)
+ else:
+ supported_args.append(arg)
+ return supported_args
+
+ @noKwargs
+ @typed_pos_args('compiler.first_supported_argument', varargs=str)
+ def first_supported_argument_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> T.List[str]:
+ for arg in args[0]:
+ if self._has_argument_impl([arg]):
+ mlog.log('First supported argument:', mlog.bold(arg))
+ return [arg]
+ mlog.log('First supported argument:', mlog.red('None'))
+ return []
+
+ @FeatureNew('compiler.has_link_argument', '0.46.0')
+ @noKwargs
+ @typed_pos_args('compiler.has_link_argument', str)
+ def has_link_argument_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool:
+ return self._has_argument_impl([args[0]], mode=_TestMode.LINKER)
+
+ @FeatureNew('compiler.has_multi_link_argument', '0.46.0')
+ @noKwargs
+ @typed_pos_args('compiler.has_multi_link_argument', varargs=str)
+ def has_multi_link_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> bool:
+ return self._has_argument_impl(args[0], mode=_TestMode.LINKER)
+
+ @FeatureNew('compiler.get_supported_link_arguments', '0.46.0')
+ @noKwargs
+ @typed_pos_args('compiler.get_supported_link_arguments', varargs=str)
+ def get_supported_link_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> T.List[str]:
+ supported_args: T.List[str] = []
+ for arg in args[0]:
+ if self._has_argument_impl([arg], mode=_TestMode.LINKER):
+ supported_args.append(arg)
+ return supported_args
+
+ @FeatureNew('compiler.first_supported_link_argument_method', '0.46.0')
+ @noKwargs
+ @typed_pos_args('compiler.first_supported_link_argument', varargs=str)
+ def first_supported_link_argument_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> T.List[str]:
+ for arg in args[0]:
+ if self._has_argument_impl([arg], mode=_TestMode.LINKER):
+ mlog.log('First supported link argument:', mlog.bold(arg))
+ return [arg]
+ mlog.log('First supported link argument:', mlog.red('None'))
+ return []
+
+ def _has_function_attribute_impl(self, attr: str) -> bool:
+ """Common helper for function attribute testing."""
+ result, cached = self.compiler.has_func_attribute(attr, self.environment)
+ cached_msg = mlog.blue('(cached)') if cached else ''
+ h = mlog.green('YES') if result else mlog.red('NO')
+ mlog.log(f'Compiler for {self.compiler.get_display_language()} supports function attribute {attr}:', h, cached_msg)
+ return result
+
+ @FeatureNew('compiler.has_function_attribute', '0.48.0')
+ @noKwargs
+ @typed_pos_args('compiler.has_function_attribute', str)
+ def has_func_attribute_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool:
+ return self._has_function_attribute_impl(args[0])
+
+ @FeatureNew('compiler.get_supported_function_attributes', '0.48.0')
+ @noKwargs
+ @typed_pos_args('compiler.get_supported_function_attributes', varargs=str)
+ def get_supported_function_attributes_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> T.List[str]:
+ return [a for a in args[0] if self._has_function_attribute_impl(a)]
+
+ @FeatureNew('compiler.get_argument_syntax_method', '0.49.0')
+ @noPosargs
+ @noKwargs
+ def get_argument_syntax_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.compiler.get_argument_syntax()
+
+ @FeatureNew('compiler.preprocess', '0.64.0')
+ @typed_pos_args('compiler.preprocess', varargs=(mesonlib.File, str), min_varargs=1)
+ @typed_kwargs(
+ 'compiler.preprocess',
+ KwargInfo('output', str, default='@PLAINNAME@.i'),
+ KwargInfo('compile_args', ContainerTypeInfo(list, str), listify=True, default=[]),
+ _INCLUDE_DIRS_KW,
+ )
+ def preprocess_method(self, args: T.Tuple[T.List['mesonlib.FileOrString']], kwargs: 'PreprocessKW') -> T.List[build.CustomTargetIndex]:
+ compiler = self.compiler.get_preprocessor()
+ sources = self.interpreter.source_strings_to_files(args[0])
+ tg_kwargs = {
+ f'{self.compiler.language}_args': kwargs['compile_args'],
+ 'build_by_default': False,
+ 'include_directories': kwargs['include_directories'],
+ }
+ tg = build.CompileTarget(
+ 'preprocessor',
+ self.interpreter.subdir,
+ self.subproject,
+ self.environment,
+ sources,
+ kwargs['output'],
+ compiler,
+ tg_kwargs)
+ self.interpreter.add_target(tg.name, tg)
+ # Expose this target as list of its outputs, so user can pass them to
+ # other targets, list outputs, etc.
+ private_dir = os.path.relpath(self.interpreter.backend.get_target_private_dir(tg), self.interpreter.subdir)
+ return [build.CustomTargetIndex(tg, os.path.join(private_dir, o)) for o in tg.outputs]
diff --git a/mesonbuild/interpreter/dependencyfallbacks.py b/mesonbuild/interpreter/dependencyfallbacks.py
new file mode 100644
index 0000000..54be990
--- /dev/null
+++ b/mesonbuild/interpreter/dependencyfallbacks.py
@@ -0,0 +1,373 @@
+from __future__ import annotations
+
+from .interpreterobjects import extract_required_kwarg
+from .. import mlog
+from .. import dependencies
+from .. import build
+from ..wrap import WrapMode
+from ..mesonlib import OptionKey, extract_as_list, stringlistify, version_compare_many, listify
+from ..dependencies import Dependency, DependencyException, NotFoundDependency
+from ..interpreterbase import (MesonInterpreterObject, FeatureNew,
+ InterpreterException, InvalidArguments)
+
+import typing as T
+if T.TYPE_CHECKING:
+ from .interpreter import Interpreter
+ from ..interpreterbase import TYPE_nkwargs, TYPE_nvar
+ from .interpreterobjects import SubprojectHolder
+
+
+class DependencyFallbacksHolder(MesonInterpreterObject):
+ def __init__(self, interpreter: 'Interpreter', names: T.List[str], allow_fallback: T.Optional[bool] = None,
+ default_options: T.Optional[T.List[str]] = None) -> None:
+ super().__init__(subproject=interpreter.subproject)
+ self.interpreter = interpreter
+ self.subproject = interpreter.subproject
+ self.coredata = interpreter.coredata
+ self.build = interpreter.build
+ self.environment = interpreter.environment
+ self.wrap_resolver = interpreter.environment.wrap_resolver
+ self.allow_fallback = allow_fallback
+ self.subproject_name: T.Optional[str] = None
+ self.subproject_varname: T.Optional[str] = None
+ self.subproject_kwargs = {'default_options': default_options or []}
+ self.names: T.List[str] = []
+ self.forcefallback: bool = False
+ self.nofallback: bool = False
+ for name in names:
+ if not name:
+ raise InterpreterException('dependency_fallbacks empty name \'\' is not allowed')
+ if '<' in name or '>' in name or '=' in name:
+ raise InvalidArguments('Characters <, > and = are forbidden in dependency names. To specify'
+ 'version\n requirements use the \'version\' keyword argument instead.')
+ if name in self.names:
+ raise InterpreterException(f'dependency_fallbacks name {name!r} is duplicated')
+ self.names.append(name)
+ self._display_name = self.names[0] if self.names else '(anonymous)'
+
+ def set_fallback(self, fbinfo: T.Optional[T.Union[T.List[str], str]]) -> None:
+ # Legacy: This converts dependency()'s fallback kwargs.
+ if fbinfo is None:
+ return
+ if self.allow_fallback is not None:
+ raise InvalidArguments('"fallback" and "allow_fallback" arguments are mutually exclusive')
+ fbinfo = stringlistify(fbinfo)
+ if len(fbinfo) == 0:
+ # dependency('foo', fallback: []) is the same as dependency('foo', allow_fallback: false)
+ self.allow_fallback = False
+ return
+ if len(fbinfo) == 1:
+ FeatureNew.single_use('Fallback without variable name', '0.53.0', self.subproject)
+ subp_name, varname = fbinfo[0], None
+ elif len(fbinfo) == 2:
+ subp_name, varname = fbinfo
+ else:
+ raise InterpreterException('Fallback info must have one or two items.')
+ self._subproject_impl(subp_name, varname)
+
+ def _subproject_impl(self, subp_name: str, varname: str) -> None:
+ assert self.subproject_name is None
+ self.subproject_name = subp_name
+ self.subproject_varname = varname
+
+ def _do_dependency_cache(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
+ name = func_args[0]
+ cached_dep = self._get_cached_dep(name, kwargs)
+ if cached_dep:
+ self._verify_fallback_consistency(cached_dep)
+ return cached_dep
+
+ def _do_dependency(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
+ # Note that there is no df.dependency() method, this is called for names
+ # given as positional arguments to dependency_fallbacks(name1, ...).
+ # We use kwargs from the dependency() function, for things like version,
+ # module, etc.
+ name = func_args[0]
+ self._handle_featurenew_dependencies(name)
+ dep = dependencies.find_external_dependency(name, self.environment, kwargs)
+ if dep.found():
+ for_machine = self.interpreter.machine_from_native_kwarg(kwargs)
+ identifier = dependencies.get_dep_identifier(name, kwargs)
+ self.coredata.deps[for_machine].put(identifier, dep)
+ return dep
+ return None
+
+ def _do_existing_subproject(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
+ subp_name = func_args[0]
+ varname = self.subproject_varname
+ if subp_name and self._get_subproject(subp_name):
+ return self._get_subproject_dep(subp_name, varname, kwargs)
+ return None
+
+ def _do_subproject(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
+ if self.forcefallback:
+ mlog.log('Looking for a fallback subproject for the dependency',
+ mlog.bold(self._display_name), 'because:\nUse of fallback dependencies is forced.')
+ elif self.nofallback:
+ mlog.log('Not looking for a fallback subproject for the dependency',
+ mlog.bold(self._display_name), 'because:\nUse of fallback dependencies is disabled.')
+ return None
+ else:
+ mlog.log('Looking for a fallback subproject for the dependency',
+ mlog.bold(self._display_name))
+
+ # dependency('foo', static: true) should implicitly add
+ # default_options: ['default_library=static']
+ static = kwargs.get('static')
+ default_options = stringlistify(func_kwargs.get('default_options', []))
+ if static is not None and not any('default_library' in i for i in default_options):
+ default_library = 'static' if static else 'shared'
+ opt = f'default_library={default_library}'
+ mlog.log(f'Building fallback subproject with {opt}')
+ default_options.append(opt)
+ func_kwargs['default_options'] = default_options
+
+ # Configure the subproject
+ subp_name = self.subproject_name
+ varname = self.subproject_varname
+ func_kwargs.setdefault('version', [])
+ if 'default_options' in kwargs and isinstance(kwargs['default_options'], str):
+ func_kwargs['default_options'] = listify(kwargs['default_options'])
+ self.interpreter.do_subproject(subp_name, 'meson', func_kwargs)
+ return self._get_subproject_dep(subp_name, varname, kwargs)
+
+ def _get_subproject(self, subp_name: str) -> T.Optional[SubprojectHolder]:
+ sub = self.interpreter.subprojects.get(subp_name)
+ if sub and sub.found():
+ return sub
+ return None
+
+ def _get_subproject_dep(self, subp_name: str, varname: str, kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
+ # Verify the subproject is found
+ subproject = self._get_subproject(subp_name)
+ if not subproject:
+ mlog.log('Dependency', mlog.bold(self._display_name), 'from subproject',
+ mlog.bold(subp_name), 'found:', mlog.red('NO'),
+ mlog.blue('(subproject failed to configure)'))
+ return None
+
+ # The subproject has been configured. If for any reason the dependency
+ # cannot be found in this subproject we have to return not-found object
+ # instead of None, because we don't want to continue the lookup on the
+ # system.
+
+ # Check if the subproject overridden at least one of the names we got.
+ cached_dep = None
+ for name in self.names:
+ cached_dep = self._get_cached_dep(name, kwargs)
+ if cached_dep:
+ break
+
+ # If we have cached_dep we did all the checks and logging already in
+ # self._get_cached_dep().
+ if cached_dep:
+ self._verify_fallback_consistency(cached_dep)
+ return cached_dep
+
+ # Legacy: Use the variable name if provided instead of relying on the
+ # subproject to override one of our dependency names
+ if not varname:
+ # If no variable name is specified, check if the wrap file has one.
+ # If the wrap file has a variable name, better use it because the
+ # subproject most probably is not using meson.override_dependency().
+ for name in self.names:
+ varname = self.wrap_resolver.get_varname(subp_name, name)
+ if varname:
+ break
+ if not varname:
+ mlog.warning(f'Subproject {subp_name!r} did not override {self._display_name!r} dependency and no variable name specified')
+ mlog.log('Dependency', mlog.bold(self._display_name), 'from subproject',
+ mlog.bold(subproject.subdir), 'found:', mlog.red('NO'))
+ return self._notfound_dependency()
+
+ var_dep = self._get_subproject_variable(subproject, varname) or self._notfound_dependency()
+ if not var_dep.found():
+ mlog.log('Dependency', mlog.bold(self._display_name), 'from subproject',
+ mlog.bold(subproject.subdir), 'found:', mlog.red('NO'))
+ return var_dep
+
+ wanted = stringlistify(kwargs.get('version', []))
+ found = var_dep.get_version()
+ if not self._check_version(wanted, found):
+ mlog.log('Dependency', mlog.bold(self._display_name), 'from subproject',
+ mlog.bold(subproject.subdir), 'found:', mlog.red('NO'),
+ 'found', mlog.normal_cyan(found), 'but need:',
+ mlog.bold(', '.join([f"'{e}'" for e in wanted])))
+ return self._notfound_dependency()
+
+ mlog.log('Dependency', mlog.bold(self._display_name), 'from subproject',
+ mlog.bold(subproject.subdir), 'found:', mlog.green('YES'),
+ mlog.normal_cyan(found) if found else None)
+ return var_dep
+
+ def _get_cached_dep(self, name: str, kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
+ # Unlike other methods, this one returns not-found dependency instead
+ # of None in the case the dependency is cached as not-found, or if cached
+ # version does not match. In that case we don't want to continue with
+ # other candidates.
+ for_machine = self.interpreter.machine_from_native_kwarg(kwargs)
+ identifier = dependencies.get_dep_identifier(name, kwargs)
+ wanted_vers = stringlistify(kwargs.get('version', []))
+
+ override = self.build.dependency_overrides[for_machine].get(identifier)
+ if override:
+ info = [mlog.blue('(overridden)' if override.explicit else '(cached)')]
+ cached_dep = override.dep
+ # We don't implicitly override not-found dependencies, but user could
+ # have explicitly called meson.override_dependency() with a not-found
+ # dep.
+ if not cached_dep.found():
+ mlog.log('Dependency', mlog.bold(self._display_name),
+ 'found:', mlog.red('NO'), *info)
+ return cached_dep
+ else:
+ info = [mlog.blue('(cached)')]
+ cached_dep = self.coredata.deps[for_machine].get(identifier)
+
+ if cached_dep:
+ found_vers = cached_dep.get_version()
+ if not self._check_version(wanted_vers, found_vers):
+ if not override:
+ # We cached this dependency on disk from a previous run,
+ # but it could got updated on the system in the meantime.
+ return None
+ mlog.log('Dependency', mlog.bold(name),
+ 'found:', mlog.red('NO'),
+ 'found', mlog.normal_cyan(found_vers), 'but need:',
+ mlog.bold(', '.join([f"'{e}'" for e in wanted_vers])),
+ *info)
+ return self._notfound_dependency()
+ if found_vers:
+ info = [mlog.normal_cyan(found_vers), *info]
+ mlog.log('Dependency', mlog.bold(self._display_name),
+ 'found:', mlog.green('YES'), *info)
+ return cached_dep
+ return None
+
+ def _get_subproject_variable(self, subproject: SubprojectHolder, varname: str) -> T.Optional[Dependency]:
+ try:
+ var_dep = subproject.get_variable_method([varname], {})
+ except InvalidArguments:
+ var_dep = None
+ if not isinstance(var_dep, Dependency):
+ mlog.warning(f'Variable {varname!r} in the subproject {subproject.subdir!r} is',
+ 'not found' if var_dep is None else 'not a dependency object')
+ return None
+ return var_dep
+
+ def _verify_fallback_consistency(self, cached_dep: Dependency) -> None:
+ subp_name = self.subproject_name
+ varname = self.subproject_varname
+ subproject = self._get_subproject(subp_name)
+ if subproject and varname:
+ var_dep = self._get_subproject_variable(subproject, varname)
+ if var_dep and cached_dep.found() and var_dep != cached_dep:
+ mlog.warning(f'Inconsistency: Subproject has overridden the dependency with another variable than {varname!r}')
+
+ def _handle_featurenew_dependencies(self, name: str) -> None:
+ 'Do a feature check on dependencies used by this subproject'
+ if name == 'mpi':
+ FeatureNew.single_use('MPI Dependency', '0.42.0', self.subproject)
+ elif name == 'pcap':
+ FeatureNew.single_use('Pcap Dependency', '0.42.0', self.subproject)
+ elif name == 'vulkan':
+ FeatureNew.single_use('Vulkan Dependency', '0.42.0', self.subproject)
+ elif name == 'libwmf':
+ FeatureNew.single_use('LibWMF Dependency', '0.44.0', self.subproject)
+ elif name == 'openmp':
+ FeatureNew.single_use('OpenMP Dependency', '0.46.0', self.subproject)
+
+ def _notfound_dependency(self) -> NotFoundDependency:
+ return NotFoundDependency(self.names[0] if self.names else '', self.environment)
+
+ @staticmethod
+ def _check_version(wanted: T.List[str], found: str) -> bool:
+ if not wanted:
+ return True
+ return not (found == 'undefined' or not version_compare_many(found, wanted)[0])
+
+ def _get_candidates(self) -> T.List[T.Tuple[T.Callable[[TYPE_nkwargs, TYPE_nvar, TYPE_nkwargs], T.Optional[Dependency]], TYPE_nvar, TYPE_nkwargs]]:
+ candidates = []
+ # 1. check if any of the names is cached already.
+ for name in self.names:
+ candidates.append((self._do_dependency_cache, [name], {}))
+ # 2. check if the subproject fallback has already been configured.
+ if self.subproject_name:
+ candidates.append((self._do_existing_subproject, [self.subproject_name], self.subproject_kwargs))
+ # 3. check external dependency if we are not forced to use subproject
+ if not self.forcefallback or not self.subproject_name:
+ for name in self.names:
+ candidates.append((self._do_dependency, [name], {}))
+ # 4. configure the subproject
+ if self.subproject_name:
+ candidates.append((self._do_subproject, [self.subproject_name], self.subproject_kwargs))
+ return candidates
+
+ def lookup(self, kwargs: TYPE_nkwargs, force_fallback: bool = False) -> Dependency:
+ mods = extract_as_list(kwargs, 'modules')
+ if mods:
+ self._display_name += ' (modules: {})'.format(', '.join(str(i) for i in mods))
+
+ disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
+ if disabled:
+ mlog.log('Dependency', mlog.bold(self._display_name), 'skipped: feature', mlog.bold(feature), 'disabled')
+ return self._notfound_dependency()
+
+ # Check if usage of the subproject fallback is forced
+ wrap_mode = self.coredata.get_option(OptionKey('wrap_mode'))
+ assert isinstance(wrap_mode, WrapMode), 'for mypy'
+ force_fallback_for = self.coredata.get_option(OptionKey('force_fallback_for'))
+ assert isinstance(force_fallback_for, list), 'for mypy'
+ self.nofallback = wrap_mode == WrapMode.nofallback
+ self.forcefallback = (force_fallback or
+ wrap_mode == WrapMode.forcefallback or
+ any(name in force_fallback_for for name in self.names) or
+ self.subproject_name in force_fallback_for)
+
+ # Add an implicit subproject fallback if none has been set explicitly,
+ # unless implicit fallback is not allowed.
+ # Legacy: self.allow_fallback can be None when that kwarg is not defined
+ # in dependency('name'). In that case we don't want to use implicit
+ # fallback when required is false because user will typically fallback
+ # manually using cc.find_library() for example.
+ if not self.subproject_name and self.allow_fallback is not False:
+ for name in self.names:
+ subp_name, varname = self.wrap_resolver.find_dep_provider(name)
+ if subp_name:
+ self.forcefallback |= subp_name in force_fallback_for
+ if self.forcefallback or self.allow_fallback is True or required or self._get_subproject(subp_name):
+ self._subproject_impl(subp_name, varname)
+ break
+
+ candidates = self._get_candidates()
+
+ # writing just "dependency('')" is an error, because it can only fail
+ if not candidates and required:
+ raise InvalidArguments('Dependency is required but has no candidates.')
+
+ # Try all candidates, only the last one is really required.
+ last = len(candidates) - 1
+ for i, item in enumerate(candidates):
+ func, func_args, func_kwargs = item
+ func_kwargs['required'] = required and (i == last)
+ kwargs['required'] = required and (i == last)
+ dep = func(kwargs, func_args, func_kwargs)
+ if dep and dep.found():
+ # Override this dependency to have consistent results in subsequent
+ # dependency lookups.
+ for name in self.names:
+ for_machine = self.interpreter.machine_from_native_kwarg(kwargs)
+ identifier = dependencies.get_dep_identifier(name, kwargs)
+ if identifier not in self.build.dependency_overrides[for_machine]:
+ self.build.dependency_overrides[for_machine][identifier] = \
+ build.DependencyOverride(dep, self.interpreter.current_node, explicit=False)
+ return dep
+ elif required and (dep or i == last):
+ # This was the last candidate or the dependency has been cached
+ # as not-found, or cached dependency version does not match,
+ # otherwise func() would have returned None instead.
+ raise DependencyException(f'Dependency {self._display_name!r} is required but not found.')
+ elif dep:
+ # Same as above, but the dependency is not required.
+ return dep
+ return self._notfound_dependency()
diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py
new file mode 100644
index 0000000..a21c809
--- /dev/null
+++ b/mesonbuild/interpreter/interpreter.py
@@ -0,0 +1,3275 @@
+# Copyright 2012-2021 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.
+from __future__ import annotations
+
+from .. import mparser
+from .. import environment
+from .. import coredata
+from .. import dependencies
+from .. import mlog
+from .. import build
+from .. import optinterpreter
+from .. import compilers
+from .. import envconfig
+from ..wrap import wrap, WrapMode
+from .. import mesonlib
+from ..mesonlib import (MesonBugException, HoldableObject, FileMode, MachineChoice, OptionKey,
+ listify, extract_as_list, has_path_sep, PerMachine)
+from ..programs import ExternalProgram, NonExistingExternalProgram
+from ..dependencies import Dependency
+from ..depfile import DepFile
+from ..interpreterbase import ContainerTypeInfo, InterpreterBase, KwargInfo, typed_kwargs, typed_pos_args
+from ..interpreterbase import noPosargs, noKwargs, permittedKwargs, noArgsFlattening, noSecondLevelHolderResolving, unholder_return
+from ..interpreterbase import InterpreterException, InvalidArguments, InvalidCode, SubdirDoneRequest
+from ..interpreterbase import Disabler, disablerIfNotFound
+from ..interpreterbase import FeatureNew, FeatureDeprecated, FeatureNewKwargs, FeatureDeprecatedKwargs
+from ..interpreterbase import ObjectHolder
+from ..modules import ExtensionModule, ModuleObject, MutableModuleObject, NewExtensionModule, NotFoundExtensionModule
+from ..cmake import CMakeInterpreter
+from ..backend.backends import ExecutableSerialisation
+
+from . import interpreterobjects as OBJ
+from . import compiler as compilerOBJ
+from .mesonmain import MesonMain
+from .dependencyfallbacks import DependencyFallbacksHolder
+from .interpreterobjects import (
+ SubprojectHolder,
+ Test,
+ RunProcess,
+ extract_required_kwarg,
+ extract_search_dirs,
+ NullSubprojectInterpreter,
+)
+from .type_checking import (
+ COMMAND_KW,
+ CT_BUILD_ALWAYS,
+ CT_BUILD_ALWAYS_STALE,
+ CT_BUILD_BY_DEFAULT,
+ CT_INPUT_KW,
+ CT_INSTALL_DIR_KW,
+ MULTI_OUTPUT_KW,
+ OUTPUT_KW,
+ DEFAULT_OPTIONS,
+ DEPENDENCIES_KW,
+ DEPENDS_KW,
+ DEPEND_FILES_KW,
+ DEPFILE_KW,
+ DISABLER_KW,
+ D_MODULE_VERSIONS_KW,
+ ENV_KW,
+ ENV_METHOD_KW,
+ ENV_SEPARATOR_KW,
+ INCLUDE_DIRECTORIES,
+ INSTALL_KW,
+ INSTALL_DIR_KW,
+ INSTALL_MODE_KW,
+ LINK_WITH_KW,
+ LINK_WHOLE_KW,
+ CT_INSTALL_TAG_KW,
+ INSTALL_TAG_KW,
+ LANGUAGE_KW,
+ NATIVE_KW,
+ PRESERVE_PATH_KW,
+ REQUIRED_KW,
+ SOURCES_KW,
+ VARIABLES_KW,
+ TEST_KWS,
+ NoneType,
+ in_set_validator,
+ env_convertor_with_method
+)
+from . import primitives as P_OBJ
+
+from pathlib import Path
+from enum import Enum
+import os
+import shutil
+import uuid
+import re
+import stat
+import collections
+import typing as T
+import textwrap
+import importlib
+import copy
+
+if T.TYPE_CHECKING:
+ import argparse
+
+ from typing_extensions import Literal
+
+ from . import kwargs as kwtypes
+ from ..backend.backends import Backend
+ from ..interpreterbase.baseobjects import InterpreterObject, TYPE_var, TYPE_kwargs
+ from ..programs import OverrideProgram
+
+ # Input source types passed to Targets
+ SourceInputs = T.Union[mesonlib.File, build.GeneratedList, build.BuildTarget, build.BothLibraries,
+ build.CustomTargetIndex, build.CustomTarget, build.GeneratedList,
+ build.ExtractedObjects, str]
+ # Input source types passed to the build.Target classes
+ SourceOutputs = T.Union[mesonlib.File, build.GeneratedList,
+ build.BuildTarget, build.CustomTargetIndex, build.CustomTarget,
+ build.ExtractedObjects, build.GeneratedList, build.StructuredSources]
+
+
+def _project_version_validator(value: T.Union[T.List, str, mesonlib.File, None]) -> T.Optional[str]:
+ if isinstance(value, list):
+ if len(value) != 1:
+ return 'when passed as array must have a length of 1'
+ elif not isinstance(value[0], mesonlib.File):
+ return 'when passed as array must contain a File'
+ return None
+
+
+def stringifyUserArguments(args: T.List[T.Any], quote: bool = False) -> str:
+ if isinstance(args, list):
+ return '[%s]' % ', '.join([stringifyUserArguments(x, True) for x in args])
+ elif isinstance(args, dict):
+ return '{%s}' % ', '.join(['{} : {}'.format(stringifyUserArguments(k, True), stringifyUserArguments(v, True)) for k, v in args.items()])
+ elif isinstance(args, bool):
+ return 'true' if args else 'false'
+ elif isinstance(args, int):
+ return str(args)
+ elif isinstance(args, str):
+ return f"'{args}'" if quote else args
+ raise InvalidArguments('Function accepts only strings, integers, bools, lists, dictionaries and lists thereof.')
+
+class Summary:
+ def __init__(self, project_name: str, project_version: str):
+ self.project_name = project_name
+ self.project_version = project_version
+ self.sections = collections.defaultdict(dict)
+ self.max_key_len = 0
+
+ def add_section(self, section: str, values: T.Dict[str, T.Any], bool_yn: bool,
+ list_sep: T.Optional[str], subproject: str) -> None:
+ for k, v in values.items():
+ if k in self.sections[section]:
+ raise InterpreterException(f'Summary section {section!r} already have key {k!r}')
+ formatted_values = []
+ for i in listify(v):
+ if isinstance(i, bool) and bool_yn:
+ formatted_values.append(mlog.green('YES') if i else mlog.red('NO'))
+ elif isinstance(i, (str, int, bool)):
+ formatted_values.append(str(i))
+ elif isinstance(i, (ExternalProgram, Dependency)):
+ FeatureNew.single_use('dependency or external program in summary', '0.57.0', subproject)
+ formatted_values.append(i.summary_value())
+ elif isinstance(i, Disabler):
+ FeatureNew.single_use('disabler in summary', '0.64.0', subproject)
+ formatted_values.append(mlog.red('NO'))
+ elif isinstance(i, coredata.UserOption):
+ FeatureNew.single_use('feature option in summary', '0.58.0', subproject)
+ formatted_values.append(i.printable_value())
+ else:
+ m = 'Summary value in section {!r}, key {!r}, must be string, integer, boolean, dependency, disabler, or external program'
+ raise InterpreterException(m.format(section, k))
+ self.sections[section][k] = (formatted_values, list_sep)
+ self.max_key_len = max(self.max_key_len, len(k))
+
+ def dump(self):
+ mlog.log(self.project_name, mlog.normal_cyan(self.project_version))
+ for section, values in self.sections.items():
+ mlog.log('') # newline
+ if section:
+ mlog.log(' ', mlog.bold(section))
+ for k, v in values.items():
+ v, list_sep = v
+ padding = self.max_key_len - len(k)
+ end = ' ' if v else ''
+ mlog.log(' ' * 3, k + ' ' * padding + ':', end=end)
+ indent = self.max_key_len + 6
+ self.dump_value(v, list_sep, indent)
+ mlog.log('') # newline
+
+ def dump_value(self, arr, list_sep, indent):
+ lines_sep = '\n' + ' ' * indent
+ if list_sep is None:
+ mlog.log(*arr, sep=lines_sep)
+ return
+ max_len = shutil.get_terminal_size().columns
+ line = []
+ line_len = indent
+ lines_sep = list_sep.rstrip() + lines_sep
+ for v in arr:
+ v_len = len(v) + len(list_sep)
+ if line and line_len + v_len > max_len:
+ mlog.log(*line, sep=list_sep, end=lines_sep)
+ line_len = indent
+ line = []
+ line.append(v)
+ line_len += v_len
+ mlog.log(*line, sep=list_sep)
+
+known_library_kwargs = (
+ build.known_shlib_kwargs |
+ build.known_stlib_kwargs
+)
+
+known_build_target_kwargs = (
+ known_library_kwargs |
+ build.known_exe_kwargs |
+ build.known_jar_kwargs |
+ {'target_type'}
+)
+
+class InterpreterRuleRelaxation(Enum):
+ ''' Defines specific relaxations of the Meson rules.
+
+ This is intended to be used for automatically converted
+ projects (CMake subprojects, build system mixing) that
+ generate a Meson AST via introspection, etc.
+ '''
+
+ ALLOW_BUILD_DIR_FILE_REFFERENCES = 1
+
+permitted_dependency_kwargs = {
+ 'allow_fallback',
+ 'cmake_args',
+ 'cmake_module_path',
+ 'cmake_package_version',
+ 'components',
+ 'default_options',
+ 'fallback',
+ 'include_type',
+ 'language',
+ 'main',
+ 'method',
+ 'modules',
+ 'native',
+ 'not_found_message',
+ 'optional_modules',
+ 'private_headers',
+ 'required',
+ 'static',
+ 'version',
+}
+
+implicit_check_false_warning = """You should add the boolean check kwarg to the run_command call.
+ It currently defaults to false,
+ but it will default to true in future releases of meson.
+ See also: https://github.com/mesonbuild/meson/issues/9300"""
+class Interpreter(InterpreterBase, HoldableObject):
+
+ def __init__(
+ self,
+ _build: build.Build,
+ backend: T.Optional[Backend] = None,
+ subproject: str = '',
+ subdir: str = '',
+ subproject_dir: str = 'subprojects',
+ default_project_options: T.Optional[T.Dict[OptionKey, str]] = None,
+ mock: bool = False,
+ ast: T.Optional[mparser.CodeBlockNode] = None,
+ is_translated: bool = False,
+ relaxations: T.Optional[T.Set[InterpreterRuleRelaxation]] = None,
+ user_defined_options: T.Optional['argparse.Namespace'] = None,
+ ) -> None:
+ super().__init__(_build.environment.get_source_dir(), subdir, subproject)
+ self.active_projectname = ''
+ self.build = _build
+ self.environment = self.build.environment
+ self.coredata = self.environment.get_coredata()
+ self.backend = backend
+ self.summary: T.Dict[str, 'Summary'] = {}
+ self.modules: T.Dict[str, NewExtensionModule] = {}
+ # Subproject directory is usually the name of the subproject, but can
+ # be different for dependencies provided by wrap files.
+ self.subproject_directory_name = subdir.split(os.path.sep)[-1]
+ self.subproject_dir = subproject_dir
+ self.option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt')
+ self.relaxations = relaxations or set()
+ if not mock and ast is None:
+ self.load_root_meson_file()
+ self.sanity_check_ast()
+ elif ast is not None:
+ self.ast = ast
+ self.sanity_check_ast()
+ self.builtin.update({'meson': MesonMain(self.build, self)})
+ self.generators: T.List[build.Generator] = []
+ self.processed_buildfiles = set() # type: T.Set[str]
+ self.project_args_frozen = False
+ self.global_args_frozen = False # implies self.project_args_frozen
+ self.subprojects: T.Dict[str, SubprojectHolder] = {}
+ self.subproject_stack: T.List[str] = []
+ self.configure_file_outputs: T.Dict[str, int] = {}
+ # Passed from the outside, only used in subprojects.
+ if default_project_options:
+ self.default_project_options = default_project_options.copy()
+ else:
+ self.default_project_options = {}
+ self.project_default_options: T.Dict[OptionKey, str] = {}
+ self.build_func_dict()
+ self.build_holder_map()
+ self.user_defined_options = user_defined_options
+ self.compilers: PerMachine[T.Dict[str, 'compilers.Compiler']] = PerMachine({}, {})
+
+ # build_def_files needs to be defined before parse_project is called
+ #
+ # For non-meson subprojects, we'll be using the ast. Even if it does
+ # exist we don't want to add a dependency on it, it's autogenerated
+ # from the actual build files, and is just for reference.
+ self.build_def_files: mesonlib.OrderedSet[str] = mesonlib.OrderedSet()
+ build_filename = os.path.join(self.subdir, environment.build_filename)
+ if not is_translated:
+ self.build_def_files.add(build_filename)
+ if not mock:
+ self.parse_project()
+ self._redetect_machines()
+
+ def __getnewargs_ex__(self) -> T.Tuple[T.Tuple[object], T.Dict[str, object]]:
+ raise MesonBugException('This class is unpicklable')
+
+ def _redetect_machines(self) -> None:
+ # Re-initialize machine descriptions. We can do a better job now because we
+ # have the compilers needed to gain more knowledge, so wipe out old
+ # inference and start over.
+ machines = self.build.environment.machines.miss_defaulting()
+ machines.build = environment.detect_machine_info(self.coredata.compilers.build)
+ self.build.environment.machines = machines.default_missing()
+ assert self.build.environment.machines.build.cpu is not None
+ assert self.build.environment.machines.host.cpu is not None
+ assert self.build.environment.machines.target.cpu is not None
+
+ self.builtin['build_machine'] = \
+ OBJ.MachineHolder(self.build.environment.machines.build, self)
+ self.builtin['host_machine'] = \
+ OBJ.MachineHolder(self.build.environment.machines.host, self)
+ self.builtin['target_machine'] = \
+ OBJ.MachineHolder(self.build.environment.machines.target, self)
+
+ def build_func_dict(self) -> None:
+ self.funcs.update({'add_global_arguments': self.func_add_global_arguments,
+ 'add_global_link_arguments': self.func_add_global_link_arguments,
+ 'add_languages': self.func_add_languages,
+ 'add_project_arguments': self.func_add_project_arguments,
+ 'add_project_dependencies': self.func_add_project_dependencies,
+ 'add_project_link_arguments': self.func_add_project_link_arguments,
+ 'add_test_setup': self.func_add_test_setup,
+ 'alias_target': self.func_alias_target,
+ 'assert': self.func_assert,
+ 'benchmark': self.func_benchmark,
+ 'both_libraries': self.func_both_lib,
+ 'build_target': self.func_build_target,
+ 'configuration_data': self.func_configuration_data,
+ 'configure_file': self.func_configure_file,
+ 'custom_target': self.func_custom_target,
+ 'debug': self.func_debug,
+ 'declare_dependency': self.func_declare_dependency,
+ 'dependency': self.func_dependency,
+ 'disabler': self.func_disabler,
+ 'environment': self.func_environment,
+ 'error': self.func_error,
+ 'executable': self.func_executable,
+ 'files': self.func_files,
+ 'find_library': self.func_find_library,
+ 'find_program': self.func_find_program,
+ 'generator': self.func_generator,
+ 'get_option': self.func_get_option,
+ 'get_variable': self.func_get_variable,
+ 'gettext': self.func_gettext,
+ 'import': self.func_import,
+ 'include_directories': self.func_include_directories,
+ 'install_data': self.func_install_data,
+ 'install_emptydir': self.func_install_emptydir,
+ 'install_headers': self.func_install_headers,
+ 'install_man': self.func_install_man,
+ 'install_subdir': self.func_install_subdir,
+ 'install_symlink': self.func_install_symlink,
+ 'is_disabler': self.func_is_disabler,
+ 'is_variable': self.func_is_variable,
+ 'jar': self.func_jar,
+ 'join_paths': self.func_join_paths,
+ 'library': self.func_library,
+ 'message': self.func_message,
+ 'option': self.func_option,
+ 'project': self.func_project,
+ 'range': self.func_range,
+ 'run_command': self.func_run_command,
+ 'run_target': self.func_run_target,
+ 'set_variable': self.func_set_variable,
+ 'structured_sources': self.func_structured_sources,
+ 'subdir': self.func_subdir,
+ 'shared_library': self.func_shared_lib,
+ 'shared_module': self.func_shared_module,
+ 'static_library': self.func_static_lib,
+ 'subdir_done': self.func_subdir_done,
+ 'subproject': self.func_subproject,
+ 'summary': self.func_summary,
+ 'test': self.func_test,
+ 'unset_variable': self.func_unset_variable,
+ 'vcs_tag': self.func_vcs_tag,
+ 'warning': self.func_warning,
+ })
+ if 'MESON_UNIT_TEST' in os.environ:
+ self.funcs.update({'exception': self.func_exception})
+
+ def build_holder_map(self) -> None:
+ '''
+ Build a mapping of `HoldableObject` types to their corresponding
+ `ObjectHolder`s. This mapping is used in `InterpreterBase` to automatically
+ holderify all returned values from methods and functions.
+ '''
+ self.holder_map.update({
+ # Primitives
+ list: P_OBJ.ArrayHolder,
+ dict: P_OBJ.DictHolder,
+ int: P_OBJ.IntegerHolder,
+ bool: P_OBJ.BooleanHolder,
+ str: P_OBJ.StringHolder,
+ P_OBJ.MesonVersionString: P_OBJ.MesonVersionStringHolder,
+ P_OBJ.DependencyVariableString: P_OBJ.DependencyVariableStringHolder,
+ P_OBJ.OptionString: P_OBJ.OptionStringHolder,
+
+ # Meson types
+ mesonlib.File: OBJ.FileHolder,
+ build.SharedLibrary: OBJ.SharedLibraryHolder,
+ build.StaticLibrary: OBJ.StaticLibraryHolder,
+ build.BothLibraries: OBJ.BothLibrariesHolder,
+ build.SharedModule: OBJ.SharedModuleHolder,
+ build.Executable: OBJ.ExecutableHolder,
+ build.Jar: OBJ.JarHolder,
+ build.CustomTarget: OBJ.CustomTargetHolder,
+ build.CustomTargetIndex: OBJ.CustomTargetIndexHolder,
+ build.Generator: OBJ.GeneratorHolder,
+ build.GeneratedList: OBJ.GeneratedListHolder,
+ build.ExtractedObjects: OBJ.GeneratedObjectsHolder,
+ build.RunTarget: OBJ.RunTargetHolder,
+ build.AliasTarget: OBJ.AliasTargetHolder,
+ build.Headers: OBJ.HeadersHolder,
+ build.Man: OBJ.ManHolder,
+ build.EmptyDir: OBJ.EmptyDirHolder,
+ build.Data: OBJ.DataHolder,
+ build.SymlinkData: OBJ.SymlinkDataHolder,
+ build.InstallDir: OBJ.InstallDirHolder,
+ build.IncludeDirs: OBJ.IncludeDirsHolder,
+ build.EnvironmentVariables: OBJ.EnvironmentVariablesHolder,
+ build.StructuredSources: OBJ.StructuredSourcesHolder,
+ compilers.RunResult: compilerOBJ.TryRunResultHolder,
+ dependencies.ExternalLibrary: OBJ.ExternalLibraryHolder,
+ coredata.UserFeatureOption: OBJ.FeatureOptionHolder,
+ envconfig.MachineInfo: OBJ.MachineHolder,
+ build.ConfigurationData: OBJ.ConfigurationDataHolder,
+ })
+
+ '''
+ Build a mapping of `HoldableObject` base classes to their
+ corresponding `ObjectHolder`s. The difference to `self.holder_map`
+ is that the keys here define an upper bound instead of requiring an
+ exact match.
+
+ The mappings defined here are only used when there was no direct hit
+ found in `self.holder_map`.
+ '''
+ self.bound_holder_map.update({
+ dependencies.Dependency: OBJ.DependencyHolder,
+ ExternalProgram: OBJ.ExternalProgramHolder,
+ compilers.Compiler: compilerOBJ.CompilerHolder,
+ ModuleObject: OBJ.ModuleObjectHolder,
+ MutableModuleObject: OBJ.MutableModuleObjectHolder,
+ })
+
+ def append_holder_map(self, held_type: T.Type[mesonlib.HoldableObject], holder_type: T.Type[ObjectHolder]) -> None:
+ '''
+ Adds one additional mapping to the `holder_map`.
+
+ The intended use for this function is in the `initialize` method of
+ modules to register custom object holders.
+ '''
+ self.holder_map.update({
+ held_type: holder_type
+ })
+
+ def process_new_values(self, invalues: T.List[T.Union[TYPE_var, ExecutableSerialisation]]) -> None:
+ invalues = listify(invalues)
+ for v in invalues:
+ if isinstance(v, ObjectHolder):
+ raise InterpreterException('Modules must not return ObjectHolders')
+ if isinstance(v, (build.BuildTarget, build.CustomTarget, build.RunTarget)):
+ self.add_target(v.name, v)
+ elif isinstance(v, list):
+ self.process_new_values(v)
+ elif isinstance(v, ExecutableSerialisation):
+ v.subproject = self.subproject
+ self.build.install_scripts.append(v)
+ elif isinstance(v, build.Data):
+ self.build.data.append(v)
+ elif isinstance(v, build.SymlinkData):
+ self.build.symlinks.append(v)
+ elif isinstance(v, dependencies.InternalDependency):
+ # FIXME: This is special cased and not ideal:
+ # The first source is our new VapiTarget, the rest are deps
+ self.process_new_values(v.sources[0])
+ elif isinstance(v, build.InstallDir):
+ self.build.install_dirs.append(v)
+ elif isinstance(v, Test):
+ self.build.tests.append(v)
+ elif isinstance(v, (int, str, bool, Disabler, ObjectHolder, build.GeneratedList,
+ ExternalProgram, build.ConfigurationData)):
+ pass
+ else:
+ raise InterpreterException(f'Module returned a value of unknown type {v!r}.')
+
+ def get_build_def_files(self) -> mesonlib.OrderedSet[str]:
+ return self.build_def_files
+
+ def add_build_def_file(self, f: mesonlib.FileOrString) -> None:
+ # Use relative path for files within source directory, and absolute path
+ # for system files. Skip files within build directory. Also skip not regular
+ # files (e.g. /dev/stdout) Normalize the path to avoid duplicates, this
+ # is especially important to convert '/' to '\' on Windows.
+ if isinstance(f, mesonlib.File):
+ if f.is_built:
+ return
+ f = os.path.normpath(f.relative_name())
+ elif os.path.isfile(f) and not f.startswith('/dev'):
+ srcdir = Path(self.environment.get_source_dir())
+ builddir = Path(self.environment.get_build_dir())
+ try:
+ f_ = Path(f).resolve()
+ except OSError:
+ f_ = Path(f)
+ s = f_.stat()
+ if (hasattr(s, 'st_file_attributes') and
+ s.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT != 0 and
+ s.st_reparse_tag == stat.IO_REPARSE_TAG_APPEXECLINK):
+ # This is a Windows Store link which we can't
+ # resolve, so just do our best otherwise.
+ f_ = f_.parent.resolve() / f_.name
+ else:
+ raise
+ if builddir in f_.parents:
+ return
+ if srcdir in f_.parents:
+ f_ = f_.relative_to(srcdir)
+ f = str(f_)
+ else:
+ return
+ if f not in self.build_def_files:
+ self.build_def_files.add(f)
+
+ def get_variables(self) -> T.Dict[str, InterpreterObject]:
+ return self.variables
+
+ def check_stdlibs(self) -> None:
+ machine_choices = [MachineChoice.HOST]
+ if self.coredata.is_cross_build():
+ machine_choices.append(MachineChoice.BUILD)
+ for for_machine in machine_choices:
+ props = self.build.environment.properties[for_machine]
+ for l in self.coredata.compilers[for_machine].keys():
+ try:
+ di = mesonlib.stringlistify(props.get_stdlib(l))
+ except KeyError:
+ continue
+ if len(di) == 1:
+ FeatureNew.single_use('stdlib without variable name', '0.56.0', self.subproject, location=self.current_node)
+ kwargs = {'native': for_machine is MachineChoice.BUILD,
+ }
+ name = l + '_stdlib'
+ df = DependencyFallbacksHolder(self, [name])
+ df.set_fallback(di)
+ dep = df.lookup(kwargs, force_fallback=True)
+ self.build.stdlibs[for_machine][l] = dep
+
+ @typed_pos_args('import', str)
+ @typed_kwargs(
+ 'import',
+ REQUIRED_KW.evolve(since='0.59.0'),
+ DISABLER_KW.evolve(since='0.59.0'),
+ )
+ @disablerIfNotFound
+ def func_import(self, node: mparser.BaseNode, args: T.Tuple[str],
+ kwargs: 'kwtypes.FuncImportModule') -> T.Union[ExtensionModule, NewExtensionModule, NotFoundExtensionModule]:
+ modname = args[0]
+ disabled, required, _ = extract_required_kwarg(kwargs, self.subproject)
+ if disabled:
+ return NotFoundExtensionModule(modname)
+
+ expect_unstable = False
+ # Some tests use "unstable_" instead of "unstable-", and that happens to work because
+ # of implementation details
+ if modname.startswith(('unstable-', 'unstable_')):
+ if modname.startswith('unstable_'):
+ mlog.deprecation(f'Importing unstable modules as "{modname}" instead of "{modname.replace("_", "-", 1)}"',
+ location=node)
+ real_modname = modname[len('unstable') + 1:] # + 1 to handle the - or _
+ expect_unstable = True
+ else:
+ real_modname = modname
+
+ if real_modname in self.modules:
+ return self.modules[real_modname]
+ try:
+ module = importlib.import_module(f'mesonbuild.modules.{real_modname}')
+ except ImportError:
+ if required:
+ raise InvalidArguments(f'Module "{modname}" does not exist')
+ ext_module = NotFoundExtensionModule(real_modname)
+ else:
+ ext_module = module.initialize(self)
+ assert isinstance(ext_module, (ExtensionModule, NewExtensionModule))
+ self.build.modules.append(real_modname)
+ if ext_module.INFO.added:
+ FeatureNew.single_use(f'module {ext_module.INFO.name}', ext_module.INFO.added, self.subproject, location=node)
+ if ext_module.INFO.deprecated:
+ FeatureDeprecated.single_use(f'module {ext_module.INFO.name}', ext_module.INFO.deprecated, self.subproject, location=node)
+ if expect_unstable and not ext_module.INFO.unstable and ext_module.INFO.stabilized is None:
+ raise InvalidArguments(f'Module {ext_module.INFO.name} has never been unstable, remove "unstable-" prefix.')
+ if ext_module.INFO.stabilized is not None:
+ if expect_unstable:
+ FeatureDeprecated.single_use(
+ f'module {ext_module.INFO.name} has been stabilized',
+ ext_module.INFO.stabilized, self.subproject,
+ 'drop "unstable-" prefix from the module name',
+ location=node)
+ else:
+ FeatureNew.single_use(
+ f'module {ext_module.INFO.name} as stable module',
+ ext_module.INFO.stabilized, self.subproject,
+ f'Consider either adding "unstable-" to the module name, or updating the meson required version to ">= {ext_module.INFO.stabilized}"',
+ location=node)
+ elif ext_module.INFO.unstable:
+ if not expect_unstable:
+ if required:
+ raise InvalidArguments(f'Module "{ext_module.INFO.name}" has not been stabilized, and must be imported as unstable-{ext_module.INFO.name}')
+ ext_module = NotFoundExtensionModule(real_modname)
+ else:
+ mlog.warning(f'Module {ext_module.INFO.name} has no backwards or forwards compatibility and might not exist in future releases.', location=node, fatal=False)
+
+ self.modules[real_modname] = ext_module
+ return ext_module
+
+ @typed_pos_args('files', varargs=str)
+ @noKwargs
+ def func_files(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> T.List[mesonlib.File]:
+ return self.source_strings_to_files(args[0])
+
+ @noPosargs
+ @typed_kwargs(
+ 'declare_dependency',
+ KwargInfo('compile_args', ContainerTypeInfo(list, str), listify=True, default=[]),
+ INCLUDE_DIRECTORIES.evolve(name='d_import_dirs', since='0.62.0'),
+ D_MODULE_VERSIONS_KW.evolve(since='0.62.0'),
+ KwargInfo('link_args', ContainerTypeInfo(list, str), listify=True, default=[]),
+ DEPENDENCIES_KW,
+ INCLUDE_DIRECTORIES,
+ LINK_WITH_KW,
+ LINK_WHOLE_KW.evolve(since='0.46.0'),
+ SOURCES_KW,
+ VARIABLES_KW.evolve(since='0.54.0', since_values={list: '0.56.0'}),
+ KwargInfo('version', (str, NoneType)),
+ )
+ def func_declare_dependency(self, node, args, kwargs):
+ deps = kwargs['dependencies']
+ incs = self.extract_incdirs(kwargs)
+ libs = kwargs['link_with']
+ libs_whole = kwargs['link_whole']
+ sources = self.source_strings_to_files(kwargs['sources'])
+ compile_args = kwargs['compile_args']
+ link_args = kwargs['link_args']
+ variables = kwargs['variables']
+ version = kwargs['version']
+ if version is None:
+ version = self.project_version
+ d_module_versions = kwargs['d_module_versions']
+ d_import_dirs = self.extract_incdirs(kwargs, 'd_import_dirs')
+ srcdir = Path(self.environment.source_dir)
+ # convert variables which refer to an -uninstalled.pc style datadir
+ for k, v in variables.items():
+ try:
+ p = Path(v)
+ except ValueError:
+ continue
+ else:
+ if not self.is_subproject() and srcdir / self.subproject_dir in p.parents:
+ continue
+ if p.is_absolute() and p.is_dir() and srcdir / self.root_subdir in [p] + list(Path(os.path.abspath(p)).parents):
+ variables[k] = P_OBJ.DependencyVariableString(v)
+ for d in deps:
+ if not isinstance(d, dependencies.Dependency):
+ raise InterpreterException('Invalid dependency')
+
+ dep = dependencies.InternalDependency(version, incs, compile_args,
+ link_args, libs, libs_whole, sources, deps,
+ variables, d_module_versions, d_import_dirs)
+ return dep
+
+ @typed_pos_args('assert', bool, optargs=[str])
+ @noKwargs
+ def func_assert(self, node: mparser.FunctionNode, args: T.Tuple[bool, T.Optional[str]],
+ kwargs: 'TYPE_kwargs') -> None:
+ value, message = args
+ if message is None:
+ FeatureNew.single_use('assert function without message argument', '0.53.0', self.subproject, location=node)
+
+ if not value:
+ if message is None:
+ from ..ast import AstPrinter
+ printer = AstPrinter()
+ node.args.arguments[0].accept(printer)
+ message = printer.result
+ raise InterpreterException('Assert failed: ' + message)
+
+ def validate_arguments(self, args, argcount, arg_types):
+ if argcount is not None:
+ if argcount != len(args):
+ raise InvalidArguments(f'Expected {argcount} arguments, got {len(args)}.')
+ for actual, wanted in zip(args, arg_types):
+ if wanted is not None:
+ if not isinstance(actual, wanted):
+ raise InvalidArguments('Incorrect argument type.')
+
+ # Executables aren't actually accepted, but we allow them here to allow for
+ # better error messages when overridden
+ @typed_pos_args(
+ 'run_command',
+ (build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str),
+ varargs=(build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str))
+ @typed_kwargs(
+ 'run_command',
+ KwargInfo('check', (bool, NoneType), since='0.47.0'),
+ KwargInfo('capture', bool, default=True, since='0.47.0'),
+ ENV_KW.evolve(since='0.50.0'),
+ )
+ def func_run_command(self, node: mparser.BaseNode,
+ args: T.Tuple[T.Union[build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str],
+ T.List[T.Union[build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str]]],
+ kwargs: 'kwtypes.RunCommand') -> RunProcess:
+ return self.run_command_impl(node, args, kwargs)
+
+ def run_command_impl(self,
+ node: mparser.BaseNode,
+ args: T.Tuple[T.Union[build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str],
+ T.List[T.Union[build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str]]],
+ kwargs: 'kwtypes.RunCommand',
+ in_builddir: bool = False) -> RunProcess:
+ cmd, cargs = args
+ capture = kwargs['capture']
+ env = kwargs['env']
+ srcdir = self.environment.get_source_dir()
+ builddir = self.environment.get_build_dir()
+
+ check = kwargs['check']
+ if check is None:
+ mlog.warning(implicit_check_false_warning, once=True)
+ check = False
+
+ overridden_msg = ('Program {!r} was overridden with the compiled '
+ 'executable {!r} and therefore cannot be used during '
+ 'configuration')
+ expanded_args: T.List[str] = []
+ if isinstance(cmd, build.Executable):
+ for name, exe in self.build.find_overrides.items():
+ if cmd == exe:
+ progname = name
+ break
+ else:
+ raise MesonBugException('cmd was a built executable but not found in overrides table')
+ raise InterpreterException(overridden_msg.format(progname, cmd.description()))
+ if isinstance(cmd, ExternalProgram):
+ if not cmd.found():
+ raise InterpreterException(f'command {cmd.get_name()!r} not found or not executable')
+ elif isinstance(cmd, compilers.Compiler):
+ exelist = cmd.get_exelist()
+ cmd = exelist[0]
+ prog = ExternalProgram(cmd, silent=True)
+ if not prog.found():
+ raise InterpreterException(f'Program {cmd!r} not found or not executable')
+ cmd = prog
+ expanded_args = exelist[1:]
+ else:
+ if isinstance(cmd, mesonlib.File):
+ cmd = cmd.absolute_path(srcdir, builddir)
+ # Prefer scripts in the current source directory
+ search_dir = os.path.join(srcdir, self.subdir)
+ prog = ExternalProgram(cmd, silent=True, search_dir=search_dir)
+ if not prog.found():
+ raise InterpreterException(f'Program or command {cmd!r} not found or not executable')
+ cmd = prog
+ for a in cargs:
+ if isinstance(a, str):
+ expanded_args.append(a)
+ elif isinstance(a, mesonlib.File):
+ expanded_args.append(a.absolute_path(srcdir, builddir))
+ elif isinstance(a, ExternalProgram):
+ expanded_args.append(a.get_path())
+ elif isinstance(a, compilers.Compiler):
+ FeatureNew.single_use('Compiler object as a variadic argument to `run_command`', '0.61.0', self.subproject, location=node)
+ prog = ExternalProgram(a.exelist[0], silent=True)
+ if not prog.found():
+ raise InterpreterException(f'Program {cmd!r} not found or not executable')
+ expanded_args.append(prog.get_path())
+ else:
+ raise InterpreterException(overridden_msg.format(a.name, cmd.description()))
+
+ # If any file that was used as an argument to the command
+ # changes, we must re-run the configuration step.
+ self.add_build_def_file(cmd.get_path())
+ for a in expanded_args:
+ if not os.path.isabs(a):
+ a = os.path.join(builddir if in_builddir else srcdir, self.subdir, a)
+ self.add_build_def_file(a)
+
+ return RunProcess(cmd, expanded_args, env, srcdir, builddir, self.subdir,
+ self.environment.get_build_command() + ['introspect'],
+ in_builddir=in_builddir, check=check, capture=capture)
+
+ def func_gettext(self, nodes, args, kwargs):
+ raise InterpreterException('Gettext() function has been moved to module i18n. Import it and use i18n.gettext() instead')
+
+ def func_option(self, nodes, args, kwargs):
+ raise InterpreterException('Tried to call option() in build description file. All options must be in the option file.')
+
+ @typed_pos_args('subproject', str)
+ @typed_kwargs(
+ 'subproject',
+ REQUIRED_KW,
+ DEFAULT_OPTIONS.evolve(since='0.38.0'),
+ KwargInfo('version', ContainerTypeInfo(list, str), default=[], listify=True),
+ )
+ def func_subproject(self, nodes: mparser.BaseNode, args: T.Tuple[str], kwargs: kwtypes.Subproject) -> SubprojectHolder:
+ kw: kwtypes.DoSubproject = {
+ 'required': kwargs['required'],
+ 'default_options': kwargs['default_options'],
+ 'version': kwargs['version'],
+ 'options': None,
+ 'cmake_options': [],
+ }
+ return self.do_subproject(args[0], 'meson', kw)
+
+ def disabled_subproject(self, subp_name: str, disabled_feature: T.Optional[str] = None,
+ exception: T.Optional[Exception] = None) -> SubprojectHolder:
+ sub = SubprojectHolder(NullSubprojectInterpreter(), os.path.join(self.subproject_dir, subp_name),
+ disabled_feature=disabled_feature, exception=exception)
+ self.subprojects[subp_name] = sub
+ return sub
+
+ def do_subproject(self, subp_name: str, method: Literal['meson', 'cmake'], kwargs: kwtypes.DoSubproject) -> SubprojectHolder:
+ disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
+ if disabled:
+ mlog.log('Subproject', mlog.bold(subp_name), ':', 'skipped: feature', mlog.bold(feature), 'disabled')
+ return self.disabled_subproject(subp_name, disabled_feature=feature)
+
+ default_options = coredata.create_options_dict(kwargs['default_options'], subp_name)
+
+ if subp_name == '':
+ raise InterpreterException('Subproject name must not be empty.')
+ if subp_name[0] == '.':
+ raise InterpreterException('Subproject name must not start with a period.')
+ if '..' in subp_name:
+ raise InterpreterException('Subproject name must not contain a ".." path segment.')
+ if os.path.isabs(subp_name):
+ raise InterpreterException('Subproject name must not be an absolute path.')
+ if has_path_sep(subp_name):
+ mlog.warning('Subproject name has a path separator. This may cause unexpected behaviour.',
+ location=self.current_node)
+ if subp_name in self.subproject_stack:
+ fullstack = self.subproject_stack + [subp_name]
+ incpath = ' => '.join(fullstack)
+ raise InvalidCode(f'Recursive include of subprojects: {incpath}.')
+ if subp_name in self.subprojects:
+ subproject = self.subprojects[subp_name]
+ if required and not subproject.found():
+ raise InterpreterException(f'Subproject "{subproject.subdir}" required but not found.')
+ if kwargs['version']:
+ pv = self.build.subprojects[subp_name]
+ wanted = kwargs['version']
+ if pv == 'undefined' or not mesonlib.version_compare_many(pv, wanted)[0]:
+ raise InterpreterException(f'Subproject {subp_name} version is {pv} but {wanted} required.')
+ return subproject
+
+ r = self.environment.wrap_resolver
+ try:
+ subdir = r.resolve(subp_name, method)
+ except wrap.WrapException as e:
+ if not required:
+ mlog.log(e)
+ mlog.log('Subproject ', mlog.bold(subp_name), 'is buildable:', mlog.red('NO'), '(disabling)')
+ return self.disabled_subproject(subp_name, exception=e)
+ raise e
+
+ subdir_abs = os.path.join(self.environment.get_source_dir(), subdir)
+ os.makedirs(os.path.join(self.build.environment.get_build_dir(), subdir), exist_ok=True)
+ self.global_args_frozen = True
+
+ stack = ':'.join(self.subproject_stack + [subp_name])
+ m = ['\nExecuting subproject', mlog.bold(stack)]
+ if method != 'meson':
+ m += ['method', mlog.bold(method)]
+ mlog.log(*m, '\n', nested=False)
+
+ try:
+ if method == 'meson':
+ return self._do_subproject_meson(subp_name, subdir, default_options, kwargs)
+ elif method == 'cmake':
+ return self._do_subproject_cmake(subp_name, subdir, subdir_abs, default_options, kwargs)
+ else:
+ raise mesonlib.MesonBugException(f'The method {method} is invalid for the subproject {subp_name}')
+ # Invalid code is always an error
+ except InvalidCode:
+ raise
+ except Exception as e:
+ if not required:
+ with mlog.nested(subp_name):
+ # Suppress the 'ERROR:' prefix because this exception is not
+ # fatal and VS CI treat any logs with "ERROR:" as fatal.
+ mlog.exception(e, prefix=mlog.yellow('Exception:'))
+ mlog.log('\nSubproject', mlog.bold(subdir), 'is buildable:', mlog.red('NO'), '(disabling)')
+ return self.disabled_subproject(subp_name, exception=e)
+ raise e
+
+ def _do_subproject_meson(self, subp_name: str, subdir: str,
+ default_options: T.Dict[OptionKey, str],
+ kwargs: kwtypes.DoSubproject,
+ ast: T.Optional[mparser.CodeBlockNode] = None,
+ build_def_files: T.Optional[T.List[str]] = None,
+ is_translated: bool = False,
+ relaxations: T.Optional[T.Set[InterpreterRuleRelaxation]] = None) -> SubprojectHolder:
+ with mlog.nested(subp_name):
+ new_build = self.build.copy()
+ subi = Interpreter(new_build, self.backend, subp_name, subdir, self.subproject_dir,
+ default_options, ast=ast, is_translated=is_translated,
+ relaxations=relaxations,
+ user_defined_options=self.user_defined_options)
+ # Those lists are shared by all interpreters. That means that
+ # even if the subproject fails, any modification that the subproject
+ # made to those lists will affect the parent project.
+ subi.subprojects = self.subprojects
+ subi.modules = self.modules
+ subi.holder_map = self.holder_map
+ subi.bound_holder_map = self.bound_holder_map
+ subi.summary = self.summary
+
+ subi.subproject_stack = self.subproject_stack + [subp_name]
+ current_active = self.active_projectname
+ current_warnings_counter = mlog.log_warnings_counter
+ mlog.log_warnings_counter = 0
+ subi.run()
+ subi_warnings = mlog.log_warnings_counter
+ mlog.log_warnings_counter = current_warnings_counter
+
+ mlog.log('Subproject', mlog.bold(subp_name), 'finished.')
+
+ mlog.log()
+
+ if kwargs['version']:
+ pv = subi.project_version
+ wanted = kwargs['version']
+ if pv == 'undefined' or not mesonlib.version_compare_many(pv, wanted)[0]:
+ raise InterpreterException(f'Subproject {subp_name} version is {pv} but {wanted} required.')
+ self.active_projectname = current_active
+ self.subprojects.update(subi.subprojects)
+ self.subprojects[subp_name] = SubprojectHolder(subi, subdir, warnings=subi_warnings)
+ # Duplicates are possible when subproject uses files from project root
+ if build_def_files:
+ self.build_def_files.update(build_def_files)
+ # We always need the subi.build_def_files, to propgate sub-sub-projects
+ self.build_def_files.update(subi.build_def_files)
+ self.build.merge(subi.build)
+ self.build.subprojects[subp_name] = subi.project_version
+ return self.subprojects[subp_name]
+
+ def _do_subproject_cmake(self, subp_name: str, subdir: str, subdir_abs: str,
+ default_options: T.Dict[OptionKey, str],
+ kwargs: kwtypes.DoSubproject) -> SubprojectHolder:
+ with mlog.nested(subp_name):
+ new_build = self.build.copy()
+ prefix = self.coredata.options[OptionKey('prefix')].value
+
+ from ..modules.cmake import CMakeSubprojectOptions
+ options = kwargs['options'] or CMakeSubprojectOptions()
+ cmake_options = kwargs['cmake_options'] + options.cmake_options
+ cm_int = CMakeInterpreter(new_build, Path(subdir), Path(subdir_abs), Path(prefix), new_build.environment, self.backend)
+ cm_int.initialise(cmake_options)
+ cm_int.analyse()
+
+ # Generate a meson ast and execute it with the normal do_subproject_meson
+ ast = cm_int.pretend_to_be_meson(options.target_options)
+
+ mlog.log()
+ with mlog.nested('cmake-ast'):
+ mlog.log('Processing generated meson AST')
+
+ # Debug print the generated meson file
+ from ..ast import AstIndentationGenerator, AstPrinter
+ printer = AstPrinter(update_ast_line_nos=True)
+ ast.accept(AstIndentationGenerator())
+ ast.accept(printer)
+ printer.post_process()
+ meson_filename = os.path.join(self.build.environment.get_build_dir(), subdir, 'meson.build')
+ with open(meson_filename, "w", encoding='utf-8') as f:
+ f.write(printer.result)
+
+ mlog.log('Build file:', meson_filename)
+ mlog.cmd_ci_include(meson_filename)
+ mlog.log()
+
+ result = self._do_subproject_meson(
+ subp_name, subdir, default_options,
+ kwargs, ast,
+ [str(f) for f in cm_int.bs_files],
+ is_translated=True,
+ relaxations={
+ InterpreterRuleRelaxation.ALLOW_BUILD_DIR_FILE_REFFERENCES,
+ }
+ )
+ result.cm_interpreter = cm_int
+
+ mlog.log()
+ return result
+
+ def get_option_internal(self, optname: str) -> coredata.UserOption:
+ key = OptionKey.from_string(optname).evolve(subproject=self.subproject)
+
+ if not key.is_project():
+ for opts in [self.coredata.options, compilers.base_options]:
+ v = opts.get(key)
+ if v is None or v.yielding:
+ v = opts.get(key.as_root())
+ if v is not None:
+ assert isinstance(v, coredata.UserOption), 'for mypy'
+ return v
+
+ try:
+ opt = self.coredata.options[key]
+ if opt.yielding and key.subproject and key.as_root() in self.coredata.options:
+ popt = self.coredata.options[key.as_root()]
+ if type(opt) is type(popt):
+ opt = popt
+ else:
+ # Get class name, then option type as a string
+ opt_type = opt.__class__.__name__[4:][:-6].lower()
+ popt_type = popt.__class__.__name__[4:][:-6].lower()
+ # This is not a hard error to avoid dependency hell, the workaround
+ # when this happens is to simply set the subproject's option directly.
+ mlog.warning('Option {0!r} of type {1!r} in subproject {2!r} cannot yield '
+ 'to parent option of type {3!r}, ignoring parent value. '
+ 'Use -D{2}:{0}=value to set the value for this option manually'
+ '.'.format(optname, opt_type, self.subproject, popt_type),
+ location=self.current_node)
+ return opt
+ except KeyError:
+ pass
+
+ raise InterpreterException(f'Tried to access unknown option {optname!r}.')
+
+ @typed_pos_args('get_option', str)
+ @noKwargs
+ def func_get_option(self, nodes: mparser.BaseNode, args: T.Tuple[str],
+ kwargs: 'TYPE_kwargs') -> T.Union[coredata.UserOption, 'TYPE_var']:
+ optname = args[0]
+ if ':' in optname:
+ raise InterpreterException('Having a colon in option name is forbidden, '
+ 'projects are not allowed to directly access '
+ 'options of other subprojects.')
+ opt = self.get_option_internal(optname)
+ if isinstance(opt, coredata.UserFeatureOption):
+ opt.name = optname
+ return opt
+ elif isinstance(opt, coredata.UserOption):
+ if isinstance(opt.value, str):
+ return P_OBJ.OptionString(opt.value, f'{{{optname}}}')
+ return opt.value
+ return opt
+
+ @typed_pos_args('configuration_data', optargs=[dict])
+ @noKwargs
+ def func_configuration_data(self, node: mparser.BaseNode, args: T.Tuple[T.Optional[T.Dict[str, T.Any]]],
+ kwargs: 'TYPE_kwargs') -> build.ConfigurationData:
+ initial_values = args[0]
+ if initial_values is not None:
+ FeatureNew.single_use('configuration_data dictionary', '0.49.0', self.subproject, location=node)
+ for k, v in initial_values.items():
+ if not isinstance(v, (str, int, bool)):
+ raise InvalidArguments(
+ f'"configuration_data": initial value dictionary key "{k!r}"" must be "str | int | bool", not "{v!r}"')
+ return build.ConfigurationData(initial_values)
+
+ def set_backend(self) -> None:
+ # The backend is already set when parsing subprojects
+ if self.backend is not None:
+ return
+ backend = self.coredata.get_option(OptionKey('backend'))
+ from ..backend import backends
+ self.backend = backends.get_backend_from_name(backend, self.build, self)
+
+ if self.backend is None:
+ raise InterpreterException(f'Unknown backend "{backend}".')
+ if backend != self.backend.name:
+ if self.backend.name.startswith('vs'):
+ mlog.log('Auto detected Visual Studio backend:', mlog.bold(self.backend.name))
+ self.coredata.set_option(OptionKey('backend'), self.backend.name)
+
+ # Only init backend options on first invocation otherwise it would
+ # override values previously set from command line.
+ if self.environment.first_invocation:
+ self.coredata.init_backend_options(backend)
+
+ options = {k: v for k, v in self.environment.options.items() if k.is_backend()}
+ self.coredata.set_options(options)
+
+ @typed_pos_args('project', str, varargs=str)
+ @typed_kwargs(
+ 'project',
+ DEFAULT_OPTIONS,
+ KwargInfo('meson_version', (str, NoneType)),
+ KwargInfo(
+ 'version',
+ (str, mesonlib.File, NoneType, list),
+ default='undefined',
+ validator=_project_version_validator,
+ convertor=lambda x: x[0] if isinstance(x, list) else x,
+ ),
+ KwargInfo('license', ContainerTypeInfo(list, str), default=['unknown'], listify=True),
+ KwargInfo('subproject_dir', str, default='subprojects'),
+ )
+ def func_project(self, node: mparser.FunctionNode, args: T.Tuple[str, T.List[str]], kwargs: 'kwtypes.Project') -> None:
+ proj_name, proj_langs = args
+ if ':' in proj_name:
+ raise InvalidArguments(f"Project name {proj_name!r} must not contain ':'")
+
+ # This needs to be evaluated as early as possible, as meson uses this
+ # for things like deprecation testing.
+ if kwargs['meson_version']:
+ cv = coredata.version
+ pv = kwargs['meson_version']
+ if not mesonlib.version_compare(cv, pv):
+ raise InterpreterException(f'Meson version is {cv} but project requires {pv}')
+ mesonlib.project_meson_versions[self.subproject] = kwargs['meson_version']
+
+ if os.path.exists(self.option_file):
+ oi = optinterpreter.OptionInterpreter(self.subproject)
+ oi.process(self.option_file)
+ self.coredata.update_project_options(oi.options)
+ self.add_build_def_file(self.option_file)
+
+ # Do not set default_options on reconfigure otherwise it would override
+ # values previously set from command line. That means that changing
+ # default_options in a project will trigger a reconfigure but won't
+ # have any effect.
+ self.project_default_options = coredata.create_options_dict(
+ kwargs['default_options'], self.subproject)
+
+ # If this is the first invocation we always need to initialize
+ # builtins, if this is a subproject that is new in a re-invocation we
+ # need to initialize builtins for that
+ if self.environment.first_invocation or (self.subproject != '' and self.subproject not in self.coredata.initialized_subprojects):
+ default_options = self.project_default_options.copy()
+ default_options.update(self.default_project_options)
+ self.coredata.init_builtins(self.subproject)
+ self.coredata.initialized_subprojects.add(self.subproject)
+ else:
+ default_options = {}
+ self.coredata.set_default_options(default_options, self.subproject, self.environment)
+
+ if not self.is_subproject():
+ self.build.project_name = proj_name
+ self.active_projectname = proj_name
+
+ version = kwargs['version']
+ if isinstance(version, mesonlib.File):
+ FeatureNew.single_use('version from file', '0.57.0', self.subproject, location=node)
+ self.add_build_def_file(version)
+ ifname = version.absolute_path(self.environment.source_dir,
+ self.environment.build_dir)
+ try:
+ ver_data = Path(ifname).read_text(encoding='utf-8').split('\n')
+ except FileNotFoundError:
+ raise InterpreterException('Version file not found.')
+ if len(ver_data) == 2 and ver_data[1] == '':
+ ver_data = ver_data[0:1]
+ if len(ver_data) != 1:
+ raise InterpreterException('Version file must contain exactly one line of text.')
+ self.project_version = ver_data[0]
+ else:
+ self.project_version = version
+
+ if self.build.project_version is None:
+ self.build.project_version = self.project_version
+ proj_license = kwargs['license']
+ self.build.dep_manifest[proj_name] = build.DepManifest(self.project_version, proj_license)
+ if self.subproject in self.build.projects:
+ raise InvalidCode('Second call to project().')
+
+ # spdirname is the subproject_dir for this project, relative to self.subdir.
+ # self.subproject_dir is the subproject_dir for the main project, relative to top source dir.
+ spdirname = kwargs['subproject_dir']
+ if not isinstance(spdirname, str):
+ raise InterpreterException('Subproject_dir must be a string')
+ if os.path.isabs(spdirname):
+ raise InterpreterException('Subproject_dir must not be an absolute path.')
+ if spdirname.startswith('.'):
+ raise InterpreterException('Subproject_dir must not begin with a period.')
+ if '..' in spdirname:
+ raise InterpreterException('Subproject_dir must not contain a ".." segment.')
+ if not self.is_subproject():
+ self.subproject_dir = spdirname
+ self.build.subproject_dir = self.subproject_dir
+
+ # Load wrap files from this (sub)project.
+ wrap_mode = self.coredata.get_option(OptionKey('wrap_mode'))
+ if not self.is_subproject() or wrap_mode != WrapMode.nopromote:
+ subdir = os.path.join(self.subdir, spdirname)
+ r = wrap.Resolver(self.environment.get_source_dir(), subdir, self.subproject, wrap_mode)
+ if self.is_subproject():
+ self.environment.wrap_resolver.merge_wraps(r)
+ else:
+ self.environment.wrap_resolver = r
+
+ self.build.projects[self.subproject] = proj_name
+ mlog.log('Project name:', mlog.bold(proj_name))
+ mlog.log('Project version:', mlog.bold(self.project_version))
+
+ if not self.is_subproject():
+ # We have to activate VS before adding languages and before calling
+ # self.set_backend() otherwise it wouldn't be able to detect which
+ # vs backend version we need. But after setting default_options in case
+ # the project sets vs backend by default.
+ backend = self.coredata.get_option(OptionKey('backend'))
+ force_vsenv = self.user_defined_options.vsenv or backend.startswith('vs')
+ if mesonlib.setup_vsenv(force_vsenv):
+ self.build.need_vsenv = True
+
+ self.add_languages(proj_langs, True, MachineChoice.HOST)
+ self.add_languages(proj_langs, False, MachineChoice.BUILD)
+
+ self.set_backend()
+ if not self.is_subproject():
+ self.check_stdlibs()
+
+ @typed_kwargs('add_languages', KwargInfo('native', (bool, NoneType), since='0.54.0'), REQUIRED_KW)
+ @typed_pos_args('add_languages', varargs=str)
+ def func_add_languages(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'kwtypes.FuncAddLanguages') -> bool:
+ langs = args[0]
+ disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
+ native = kwargs['native']
+
+ if disabled:
+ for lang in sorted(langs, key=compilers.sort_clink):
+ mlog.log('Compiler for language', mlog.bold(lang), 'skipped: feature', mlog.bold(feature), 'disabled')
+ return False
+ if native is not None:
+ return self.add_languages(langs, required, self.machine_from_native_kwarg(kwargs))
+ else:
+ # absent 'native' means 'both' for backwards compatibility
+ tv = FeatureNew.get_target_version(self.subproject)
+ if FeatureNew.check_version(tv, '0.54.0'):
+ mlog.warning('add_languages is missing native:, assuming languages are wanted for both host and build.',
+ location=node)
+
+ success = self.add_languages(langs, False, MachineChoice.BUILD)
+ success &= self.add_languages(langs, required, MachineChoice.HOST)
+ return success
+
+ @noArgsFlattening
+ @noKwargs
+ def func_message(self, node: mparser.BaseNode, args, kwargs):
+ if len(args) > 1:
+ FeatureNew.single_use('message with more than one argument', '0.54.0', self.subproject, location=node)
+ args_str = [stringifyUserArguments(i) for i in args]
+ self.message_impl(args_str)
+
+ def message_impl(self, args):
+ mlog.log(mlog.bold('Message:'), *args)
+
+ @noArgsFlattening
+ @FeatureNew('summary', '0.53.0')
+ @typed_pos_args('summary', (str, dict), optargs=[object])
+ @typed_kwargs(
+ 'summary',
+ KwargInfo('section', str, default=''),
+ KwargInfo('bool_yn', bool, default=False),
+ KwargInfo('list_sep', (str, NoneType), since='0.54.0')
+ )
+ def func_summary(self, node: mparser.BaseNode, args: T.Tuple[T.Union[str, T.Dict[str, T.Any]], T.Optional[T.Any]],
+ kwargs: 'kwtypes.Summary') -> None:
+ if args[1] is None:
+ if not isinstance(args[0], dict):
+ raise InterpreterException('Summary first argument must be dictionary.')
+ values = args[0]
+ else:
+ if not isinstance(args[0], str):
+ raise InterpreterException('Summary first argument must be string.')
+ values = {args[0]: args[1]}
+ self.summary_impl(kwargs['section'], values, kwargs)
+
+ def summary_impl(self, section: str, values, kwargs: 'kwtypes.Summary') -> None:
+ if self.subproject not in self.summary:
+ self.summary[self.subproject] = Summary(self.active_projectname, self.project_version)
+ self.summary[self.subproject].add_section(
+ section, values, kwargs['bool_yn'], kwargs['list_sep'], self.subproject)
+
+ def _print_summary(self) -> None:
+ # Add automatic 'Supbrojects' section in main project.
+ all_subprojects = collections.OrderedDict()
+ for name, subp in sorted(self.subprojects.items()):
+ value = subp.found()
+ if subp.disabled_feature:
+ value = [value, f'Feature {subp.disabled_feature!r} disabled']
+ elif subp.exception:
+ value = [value, str(subp.exception)]
+ elif subp.warnings > 0:
+ value = [value, f'{subp.warnings} warnings']
+ all_subprojects[name] = value
+ if all_subprojects:
+ self.summary_impl('Subprojects', all_subprojects,
+ {'bool_yn': True,
+ 'list_sep': ' ',
+ })
+ # Add automatic section with all user defined options
+ if self.user_defined_options:
+ values = collections.OrderedDict()
+ if self.user_defined_options.cross_file:
+ values['Cross files'] = self.user_defined_options.cross_file
+ if self.user_defined_options.native_file:
+ values['Native files'] = self.user_defined_options.native_file
+ sorted_options = sorted(self.user_defined_options.cmd_line_options.items())
+ values.update({str(k): v for k, v in sorted_options})
+ if values:
+ self.summary_impl('User defined options', values, {'bool_yn': False, 'list_sep': None})
+ # Print all summaries, main project last.
+ mlog.log('') # newline
+ main_summary = self.summary.pop('', None)
+ for subp_name, summary in sorted(self.summary.items()):
+ if self.subprojects[subp_name].found():
+ summary.dump()
+ if main_summary:
+ main_summary.dump()
+
+ @noArgsFlattening
+ @FeatureNew('warning', '0.44.0')
+ @noKwargs
+ def func_warning(self, node, args, kwargs):
+ if len(args) > 1:
+ FeatureNew.single_use('warning with more than one argument', '0.54.0', self.subproject, location=node)
+ args_str = [stringifyUserArguments(i) for i in args]
+ mlog.warning(*args_str, location=node)
+
+ @noArgsFlattening
+ @noKwargs
+ def func_error(self, node, args, kwargs):
+ if len(args) > 1:
+ FeatureNew.single_use('error with more than one argument', '0.58.0', self.subproject, location=node)
+ args_str = [stringifyUserArguments(i) for i in args]
+ raise InterpreterException('Problem encountered: ' + ' '.join(args_str))
+
+ @noArgsFlattening
+ @FeatureNew('debug', '0.63.0')
+ @noKwargs
+ def func_debug(self, node, args, kwargs):
+ args_str = [stringifyUserArguments(i) for i in args]
+ mlog.debug('Debug:', *args_str)
+
+ @noKwargs
+ @noPosargs
+ def func_exception(self, node, args, kwargs):
+ raise RuntimeError('unit test traceback :)')
+
+ def add_languages(self, args: T.List[str], required: bool, for_machine: MachineChoice) -> bool:
+ success = self.add_languages_for(args, required, for_machine)
+ if not self.coredata.is_cross_build():
+ self.coredata.copy_build_options_from_regular_ones()
+ self._redetect_machines()
+ return success
+
+ def should_skip_sanity_check(self, for_machine: MachineChoice) -> bool:
+ should = self.environment.properties.host.get('skip_sanity_check', False)
+ if not isinstance(should, bool):
+ raise InterpreterException('Option skip_sanity_check must be a boolean.')
+ if for_machine != MachineChoice.HOST and not should:
+ return False
+ if not self.environment.is_cross_build() and not should:
+ return False
+ return should
+
+ def add_languages_for(self, args: T.List[str], required: bool, for_machine: MachineChoice) -> bool:
+ args = [a.lower() for a in args]
+ langs = set(self.compilers[for_machine].keys())
+ langs.update(args)
+ # We'd really like to add cython's default language here, but it can't
+ # actually be done because the cython compiler hasn't been initialized,
+ # so we can't actually get the option yet. Because we can't know what
+ # compiler to add by default, and we don't want to add unnecessary
+ # compilers we don't add anything for cython here, and instead do it
+ # When the first cython target using a particular language is used.
+ if 'vala' in langs and 'c' not in langs:
+ FeatureNew.single_use('Adding Vala language without C', '0.59.0', self.subproject, location=self.current_node)
+ args.append('c')
+ if 'nasm' in langs:
+ FeatureNew.single_use('Adding NASM language', '0.64.0', self.subproject, location=self.current_node)
+
+ success = True
+ for lang in sorted(args, key=compilers.sort_clink):
+ if lang in self.compilers[for_machine]:
+ continue
+ machine_name = for_machine.get_lower_case_name()
+ comp = self.coredata.compilers[for_machine].get(lang)
+ if not comp:
+ try:
+ comp = compilers.detect_compiler_for(self.environment, lang, for_machine)
+ if comp is None:
+ raise InvalidArguments(f'Tried to use unknown language "{lang}".')
+ if self.should_skip_sanity_check(for_machine):
+ mlog.log_once('Cross compiler sanity tests disabled via the cross file.')
+ else:
+ comp.sanity_check(self.environment.get_scratch_dir(), self.environment)
+ except Exception:
+ if not required:
+ mlog.log('Compiler for language',
+ mlog.bold(lang), 'for the', machine_name,
+ 'machine not found.')
+ success = False
+ continue
+ else:
+ raise
+
+ # Add per-subproject compiler options. They inherit value from main project.
+ if self.subproject:
+ options = {}
+ for k in comp.get_options():
+ v = copy.copy(self.coredata.options[k])
+ k = k.evolve(subproject=self.subproject)
+ options[k] = v
+ self.coredata.add_compiler_options(options, lang, for_machine, self.environment)
+
+ if for_machine == MachineChoice.HOST or self.environment.is_cross_build():
+ logger_fun = mlog.log
+ else:
+ logger_fun = mlog.debug
+ logger_fun(comp.get_display_language(), 'compiler for the', machine_name, 'machine:',
+ mlog.bold(' '.join(comp.get_exelist())), comp.get_version_string())
+ if comp.linker is not None:
+ logger_fun(comp.get_display_language(), 'linker for the', machine_name, 'machine:',
+ mlog.bold(' '.join(comp.linker.get_exelist())), comp.linker.id, comp.linker.version)
+ self.build.ensure_static_linker(comp)
+ self.compilers[for_machine][lang] = comp
+
+ return success
+
+ def program_from_file_for(self, for_machine: MachineChoice, prognames: T.List[mesonlib.FileOrString]
+ ) -> T.Optional[ExternalProgram]:
+ for p in prognames:
+ if isinstance(p, mesonlib.File):
+ continue # Always points to a local (i.e. self generated) file.
+ if not isinstance(p, str):
+ raise InterpreterException('Executable name must be a string')
+ prog = ExternalProgram.from_bin_list(self.environment, for_machine, p)
+ # if the machine file specified something, it may be a regular
+ # not-found program but we still want to return that
+ if not isinstance(prog, NonExistingExternalProgram):
+ return prog
+ return None
+
+ def program_from_system(self, args: T.List[mesonlib.FileOrString], search_dirs: T.List[str],
+ extra_info: T.List[mlog.TV_Loggable]) -> T.Optional[ExternalProgram]:
+ # Search for scripts relative to current subdir.
+ # Do not cache found programs because find_program('foobar')
+ # might give different results when run from different source dirs.
+ source_dir = os.path.join(self.environment.get_source_dir(), self.subdir)
+ for exename in args:
+ if isinstance(exename, mesonlib.File):
+ if exename.is_built:
+ search_dir = os.path.join(self.environment.get_build_dir(),
+ exename.subdir)
+ else:
+ search_dir = os.path.join(self.environment.get_source_dir(),
+ exename.subdir)
+ exename = exename.fname
+ extra_search_dirs = []
+ elif isinstance(exename, str):
+ search_dir = source_dir
+ extra_search_dirs = search_dirs
+ else:
+ raise InvalidArguments(f'find_program only accepts strings and files, not {exename!r}')
+ extprog = ExternalProgram(exename, search_dir=search_dir,
+ extra_search_dirs=extra_search_dirs,
+ silent=True)
+ if extprog.found():
+ extra_info.append(f"({' '.join(extprog.get_command())})")
+ return extprog
+ return None
+
+ def program_from_overrides(self, command_names: T.List[mesonlib.FileOrString],
+ extra_info: T.List['mlog.TV_Loggable']
+ ) -> T.Optional[T.Union[ExternalProgram, OverrideProgram, build.Executable]]:
+ for name in command_names:
+ if not isinstance(name, str):
+ continue
+ if name in self.build.find_overrides:
+ exe = self.build.find_overrides[name]
+ extra_info.append(mlog.blue('(overridden)'))
+ return exe
+ return None
+
+ def store_name_lookups(self, command_names: T.List[mesonlib.FileOrString]) -> None:
+ for name in command_names:
+ if isinstance(name, str):
+ self.build.searched_programs.add(name)
+
+ def add_find_program_override(self, name: str, exe: T.Union[build.Executable, ExternalProgram, 'OverrideProgram']) -> None:
+ if name in self.build.searched_programs:
+ raise InterpreterException(f'Tried to override finding of executable "{name}" which has already been found.')
+ if name in self.build.find_overrides:
+ raise InterpreterException(f'Tried to override executable "{name}" which has already been overridden.')
+ self.build.find_overrides[name] = exe
+
+ def notfound_program(self, args: T.List[mesonlib.FileOrString]) -> ExternalProgram:
+ return NonExistingExternalProgram(' '.join(
+ [a if isinstance(a, str) else a.absolute_path(self.environment.source_dir, self.environment.build_dir)
+ for a in args]))
+
+ # TODO update modules to always pass `for_machine`. It is bad-form to assume
+ # the host machine.
+ def find_program_impl(self, args: T.List[mesonlib.FileOrString],
+ for_machine: MachineChoice = MachineChoice.HOST,
+ required: bool = True, silent: bool = True,
+ wanted: T.Union[str, T.List[str]] = '',
+ search_dirs: T.Optional[T.List[str]] = None,
+ version_func: T.Optional[T.Callable[[T.Union['ExternalProgram', 'build.Executable', 'OverrideProgram']], str]] = None
+ ) -> T.Union['ExternalProgram', 'build.Executable', 'OverrideProgram']:
+ args = mesonlib.listify(args)
+
+ extra_info: T.List[mlog.TV_Loggable] = []
+ progobj = self.program_lookup(args, for_machine, required, search_dirs, extra_info)
+ if progobj is None:
+ progobj = self.notfound_program(args)
+
+ if isinstance(progobj, ExternalProgram) and not progobj.found():
+ if not silent:
+ mlog.log('Program', mlog.bold(progobj.get_name()), 'found:', mlog.red('NO'))
+ if required:
+ m = 'Program {!r} not found or not executable'
+ raise InterpreterException(m.format(progobj.get_name()))
+ return progobj
+
+ if wanted:
+ if version_func:
+ version = version_func(progobj)
+ elif isinstance(progobj, build.Executable):
+ if progobj.subproject:
+ interp = self.subprojects[progobj.subproject].held_object
+ else:
+ interp = self
+ assert isinstance(interp, Interpreter)
+ version = interp.project_version
+ else:
+ version = progobj.get_version(self)
+ is_found, not_found, _ = mesonlib.version_compare_many(version, wanted)
+ if not is_found:
+ mlog.log('Program', mlog.bold(progobj.name), 'found:', mlog.red('NO'),
+ 'found', mlog.normal_cyan(version), 'but need:',
+ mlog.bold(', '.join([f"'{e}'" for e in not_found])), *extra_info)
+ if required:
+ m = 'Invalid version of program, need {!r} {!r} found {!r}.'
+ raise InterpreterException(m.format(progobj.name, not_found, version))
+ return self.notfound_program(args)
+ extra_info.insert(0, mlog.normal_cyan(version))
+
+ # Only store successful lookups
+ self.store_name_lookups(args)
+ if not silent:
+ mlog.log('Program', mlog.bold(progobj.name), 'found:', mlog.green('YES'), *extra_info)
+ if isinstance(progobj, build.Executable):
+ progobj.was_returned_by_find_program = True
+ return progobj
+
+ def program_lookup(self, args: T.List[mesonlib.FileOrString], for_machine: MachineChoice,
+ required: bool, search_dirs: T.List[str], extra_info: T.List[mlog.TV_Loggable]
+ ) -> T.Optional[T.Union[ExternalProgram, build.Executable, OverrideProgram]]:
+ progobj = self.program_from_overrides(args, extra_info)
+ if progobj:
+ return progobj
+
+ fallback = None
+ wrap_mode = self.coredata.get_option(OptionKey('wrap_mode'))
+ if wrap_mode != WrapMode.nofallback and self.environment.wrap_resolver:
+ fallback = self.environment.wrap_resolver.find_program_provider(args)
+ if fallback and wrap_mode == WrapMode.forcefallback:
+ return self.find_program_fallback(fallback, args, required, extra_info)
+
+ progobj = self.program_from_file_for(for_machine, args)
+ if progobj is None:
+ progobj = self.program_from_system(args, search_dirs, extra_info)
+ if progobj is None and args[0].endswith('python3'):
+ prog = ExternalProgram('python3', mesonlib.python_command, silent=True)
+ progobj = prog if prog.found() else None
+ if progobj is None and fallback and required:
+ progobj = self.find_program_fallback(fallback, args, required, extra_info)
+
+ return progobj
+
+ def find_program_fallback(self, fallback: str, args: T.List[mesonlib.FileOrString],
+ required: bool, extra_info: T.List[mlog.TV_Loggable]
+ ) -> T.Optional[T.Union[ExternalProgram, build.Executable, OverrideProgram]]:
+ mlog.log('Fallback to subproject', mlog.bold(fallback), 'which provides program',
+ mlog.bold(' '.join(args)))
+ sp_kwargs: kwtypes.DoSubproject = {
+ 'required': required,
+ 'default_options': [],
+ 'version': [],
+ 'cmake_options': [],
+ 'options': None,
+ }
+ self.do_subproject(fallback, 'meson', sp_kwargs)
+ return self.program_from_overrides(args, extra_info)
+
+ @typed_pos_args('find_program', varargs=(str, mesonlib.File), min_varargs=1)
+ @typed_kwargs(
+ 'find_program',
+ DISABLER_KW.evolve(since='0.49.0'),
+ NATIVE_KW,
+ REQUIRED_KW,
+ KwargInfo('dirs', ContainerTypeInfo(list, str), default=[], listify=True, since='0.53.0'),
+ KwargInfo('version', ContainerTypeInfo(list, str), default=[], listify=True, since='0.52.0'),
+ )
+ @disablerIfNotFound
+ def func_find_program(self, node: mparser.BaseNode, args: T.Tuple[T.List[mesonlib.FileOrString]],
+ kwargs: 'kwtypes.FindProgram',
+ ) -> T.Union['build.Executable', ExternalProgram, 'OverrideProgram']:
+ disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
+ if disabled:
+ mlog.log('Program', mlog.bold(' '.join(args[0])), 'skipped: feature', mlog.bold(feature), 'disabled')
+ return self.notfound_program(args[0])
+
+ search_dirs = extract_search_dirs(kwargs)
+ return self.find_program_impl(args[0], kwargs['native'], required=required,
+ silent=False, wanted=kwargs['version'],
+ search_dirs=search_dirs)
+
+ def func_find_library(self, node, args, kwargs):
+ raise InvalidCode('find_library() is removed, use meson.get_compiler(\'name\').find_library() instead.\n'
+ 'Look here for documentation: http://mesonbuild.com/Reference-manual.html#compiler-object\n'
+ 'Look here for example: http://mesonbuild.com/howtox.html#add-math-library-lm-portably\n'
+ )
+
+ # When adding kwargs, please check if they make sense in dependencies.get_dep_identifier()
+ @FeatureNewKwargs('dependency', '0.57.0', ['cmake_package_version'])
+ @FeatureNewKwargs('dependency', '0.56.0', ['allow_fallback'])
+ @FeatureNewKwargs('dependency', '0.54.0', ['components'])
+ @FeatureNewKwargs('dependency', '0.52.0', ['include_type'])
+ @FeatureNewKwargs('dependency', '0.50.0', ['not_found_message', 'cmake_module_path', 'cmake_args'])
+ @FeatureNewKwargs('dependency', '0.49.0', ['disabler'])
+ @FeatureNewKwargs('dependency', '0.40.0', ['method'])
+ @FeatureNewKwargs('dependency', '0.38.0', ['default_options'])
+ @disablerIfNotFound
+ @permittedKwargs(permitted_dependency_kwargs)
+ @typed_pos_args('dependency', varargs=str, min_varargs=1)
+ def func_dependency(self, node: mparser.BaseNode, args: T.Tuple[T.List[str]], kwargs) -> Dependency:
+ # Replace '' by empty list of names
+ names = [n for n in args[0] if n]
+ if len(names) > 1:
+ FeatureNew('dependency with more than one name', '0.60.0').use(self.subproject)
+ allow_fallback = kwargs.get('allow_fallback')
+ if allow_fallback is not None and not isinstance(allow_fallback, bool):
+ raise InvalidArguments('"allow_fallback" argument must be boolean')
+ fallback = kwargs.get('fallback')
+ default_options = kwargs.get('default_options')
+ df = DependencyFallbacksHolder(self, names, allow_fallback, default_options)
+ df.set_fallback(fallback)
+ not_found_message = kwargs.get('not_found_message', '')
+ if not isinstance(not_found_message, str):
+ raise InvalidArguments('The not_found_message must be a string.')
+ try:
+ d = df.lookup(kwargs)
+ except Exception:
+ if not_found_message:
+ self.message_impl([not_found_message])
+ raise
+ assert isinstance(d, Dependency)
+ if not d.found() and not_found_message:
+ self.message_impl([not_found_message])
+ # Ensure the correct include type
+ if 'include_type' in kwargs:
+ wanted = kwargs['include_type']
+ if not isinstance(wanted, str):
+ raise InvalidArguments('The `include_type` kwarg must be a string')
+ actual = d.get_include_type()
+ if wanted != actual:
+ mlog.debug(f'Current include type of {args[0]} is {actual}. Converting to requested {wanted}')
+ d = d.generate_system_dependency(wanted)
+ if d.feature_since is not None:
+ version, extra_msg = d.feature_since
+ FeatureNew.single_use(f'dep {d.name!r} custom lookup', version, self.subproject, extra_msg, node)
+ for f in d.featurechecks:
+ f.use(self.subproject, node)
+ return d
+
+ @FeatureNew('disabler', '0.44.0')
+ @noKwargs
+ @noPosargs
+ def func_disabler(self, node, args, kwargs):
+ return Disabler()
+
+ @FeatureNewKwargs('executable', '0.42.0', ['implib'])
+ @FeatureNewKwargs('executable', '0.56.0', ['win_subsystem'])
+ @FeatureDeprecatedKwargs('executable', '0.56.0', ['gui_app'], extra_message="Use 'win_subsystem' instead.")
+ @permittedKwargs(build.known_exe_kwargs)
+ def func_executable(self, node, args, kwargs):
+ return self.build_target(node, args, kwargs, build.Executable)
+
+ @permittedKwargs(build.known_stlib_kwargs)
+ def func_static_lib(self, node, args, kwargs):
+ return self.build_target(node, args, kwargs, build.StaticLibrary)
+
+ @permittedKwargs(build.known_shlib_kwargs)
+ def func_shared_lib(self, node, args, kwargs):
+ holder = self.build_target(node, args, kwargs, build.SharedLibrary)
+ holder.shared_library_only = True
+ return holder
+
+ @permittedKwargs(known_library_kwargs)
+ def func_both_lib(self, node, args, kwargs):
+ return self.build_both_libraries(node, args, kwargs)
+
+ @FeatureNew('shared_module', '0.37.0')
+ @permittedKwargs(build.known_shmod_kwargs)
+ def func_shared_module(self, node, args, kwargs):
+ return self.build_target(node, args, kwargs, build.SharedModule)
+
+ @permittedKwargs(known_library_kwargs)
+ def func_library(self, node, args, kwargs):
+ return self.build_library(node, args, kwargs)
+
+ @permittedKwargs(build.known_jar_kwargs)
+ def func_jar(self, node, args, kwargs):
+ return self.build_target(node, args, kwargs, build.Jar)
+
+ @FeatureNewKwargs('build_target', '0.40.0', ['link_whole', 'override_options'])
+ @permittedKwargs(known_build_target_kwargs)
+ def func_build_target(self, node, args, kwargs):
+ if 'target_type' not in kwargs:
+ raise InterpreterException('Missing target_type keyword argument')
+ target_type = kwargs.pop('target_type')
+ if target_type == 'executable':
+ return self.build_target(node, args, kwargs, build.Executable)
+ elif target_type == 'shared_library':
+ return self.build_target(node, args, kwargs, build.SharedLibrary)
+ elif target_type == 'shared_module':
+ FeatureNew.single_use(
+ 'build_target(target_type: \'shared_module\')',
+ '0.51.0', self.subproject, location=node)
+ return self.build_target(node, args, kwargs, build.SharedModule)
+ elif target_type == 'static_library':
+ return self.build_target(node, args, kwargs, build.StaticLibrary)
+ elif target_type == 'both_libraries':
+ return self.build_both_libraries(node, args, kwargs)
+ elif target_type == 'library':
+ return self.build_library(node, args, kwargs)
+ elif target_type == 'jar':
+ return self.build_target(node, args, kwargs, build.Jar)
+ else:
+ raise InterpreterException('Unknown target_type.')
+
+ @noPosargs
+ @typed_kwargs(
+ 'vcs_tag',
+ CT_INPUT_KW.evolve(required=True),
+ MULTI_OUTPUT_KW,
+ # Cannot use the COMMAND_KW because command is allowed to be empty
+ KwargInfo(
+ 'command',
+ ContainerTypeInfo(list, (str, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, ExternalProgram, mesonlib.File)),
+ listify=True,
+ default=[],
+ ),
+ KwargInfo('fallback', (str, NoneType)),
+ KwargInfo('replace_string', str, default='@VCS_TAG@'),
+ )
+ def func_vcs_tag(self, node: mparser.BaseNode, args: T.List['TYPE_var'], kwargs: 'kwtypes.VcsTag') -> build.CustomTarget:
+ if kwargs['fallback'] is None:
+ FeatureNew.single_use('Optional fallback in vcs_tag', '0.41.0', self.subproject, location=node)
+ fallback = kwargs['fallback'] or self.project_version
+ replace_string = kwargs['replace_string']
+ regex_selector = '(.*)' # default regex selector for custom command: use complete output
+ vcs_cmd = kwargs['command']
+ source_dir = os.path.normpath(os.path.join(self.environment.get_source_dir(), self.subdir))
+ if vcs_cmd:
+ if isinstance(vcs_cmd[0], (str, mesonlib.File)):
+ if isinstance(vcs_cmd[0], mesonlib.File):
+ FeatureNew.single_use('vcs_tag with file as the first argument', '0.62.0', self.subproject, location=node)
+ maincmd = self.find_program_impl(vcs_cmd[0], required=False)
+ if maincmd.found():
+ vcs_cmd[0] = maincmd
+ else:
+ FeatureNew.single_use('vcs_tag with custom_tgt, external_program, or exe as the first argument', '0.63.0', self.subproject, location=node)
+ else:
+ vcs = mesonlib.detect_vcs(source_dir)
+ if vcs:
+ mlog.log('Found {} repository at {}'.format(vcs['name'], vcs['wc_dir']))
+ vcs_cmd = vcs['get_rev'].split()
+ regex_selector = vcs['rev_regex']
+ else:
+ vcs_cmd = [' '] # executing this cmd will fail in vcstagger.py and force to use the fallback string
+ # vcstagger.py parameters: infile, outfile, fallback, source_dir, replace_string, regex_selector, command...
+
+ self._validate_custom_target_outputs(len(kwargs['input']) > 1, kwargs['output'], "vcs_tag")
+
+ cmd = self.environment.get_build_command() + \
+ ['--internal',
+ 'vcstagger',
+ '@INPUT0@',
+ '@OUTPUT0@',
+ fallback,
+ source_dir,
+ replace_string,
+ regex_selector] + vcs_cmd
+
+ tg = build.CustomTarget(
+ kwargs['output'][0],
+ self.subdir,
+ self.subproject,
+ self.environment,
+ cmd,
+ self.source_strings_to_files(kwargs['input']),
+ kwargs['output'],
+ build_by_default=True,
+ build_always_stale=True,
+ )
+ self.add_target(tg.name, tg)
+ return tg
+
+ @FeatureNew('subdir_done', '0.46.0')
+ @noPosargs
+ @noKwargs
+ def func_subdir_done(self, node: mparser.BaseNode, args: TYPE_var, kwargs: TYPE_kwargs) -> T.NoReturn:
+ raise SubdirDoneRequest()
+
+ @staticmethod
+ def _validate_custom_target_outputs(has_multi_in: bool, outputs: T.Iterable[str], name: str) -> None:
+ """Checks for additional invalid values in a custom_target output.
+
+ This cannot be done with typed_kwargs because it requires the number of
+ inputs.
+ """
+ for out in outputs:
+ if has_multi_in and ('@PLAINNAME@' in out or '@BASENAME@' in out):
+ raise InvalidArguments(f'{name}: output cannot contain "@PLAINNAME@" or "@BASENAME@" '
+ 'when there is more than one input (we can\'t know which to use)')
+
+ @typed_pos_args('custom_target', optargs=[str])
+ @typed_kwargs(
+ 'custom_target',
+ COMMAND_KW,
+ CT_BUILD_ALWAYS,
+ CT_BUILD_ALWAYS_STALE,
+ CT_BUILD_BY_DEFAULT,
+ CT_INPUT_KW,
+ CT_INSTALL_DIR_KW,
+ CT_INSTALL_TAG_KW,
+ MULTI_OUTPUT_KW,
+ DEPENDS_KW,
+ DEPEND_FILES_KW,
+ DEPFILE_KW,
+ ENV_KW.evolve(since='0.57.0'),
+ INSTALL_KW,
+ INSTALL_MODE_KW.evolve(since='0.47.0'),
+ KwargInfo('feed', bool, default=False, since='0.59.0'),
+ KwargInfo('capture', bool, default=False),
+ KwargInfo('console', bool, default=False, since='0.48.0'),
+ )
+ def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str],
+ kwargs: 'kwtypes.CustomTarget') -> build.CustomTarget:
+ if kwargs['depfile'] and ('@BASENAME@' in kwargs['depfile'] or '@PLAINNAME@' in kwargs['depfile']):
+ FeatureNew.single_use('substitutions in custom_target depfile', '0.47.0', self.subproject, location=node)
+ install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
+
+ # Don't mutate the kwargs
+
+ build_by_default = kwargs['build_by_default']
+ build_always_stale = kwargs['build_always_stale']
+ # Remap build_always to build_by_default and build_always_stale
+ if kwargs['build_always'] is not None and kwargs['build_always_stale'] is not None:
+ raise InterpreterException('CustomTarget: "build_always" and "build_always_stale" are mutually exclusive')
+
+ if build_by_default is None and kwargs['install']:
+ build_by_default = True
+
+ elif kwargs['build_always'] is not None:
+ if build_by_default is None:
+ build_by_default = kwargs['build_always']
+ build_always_stale = kwargs['build_by_default']
+
+ # These are are nullaable so that we can know whether they're explicitly
+ # set or not. If they haven't been overwritten, set them to their true
+ # default
+ if build_by_default is None:
+ build_by_default = False
+ if build_always_stale is None:
+ build_always_stale = False
+
+ name = args[0]
+ if name is None:
+ # name will default to first output, but we cannot do that yet because
+ # they could need substitutions (e.g. @BASENAME@) first. CustomTarget()
+ # will take care of setting a proper default but name must be an empty
+ # string in the meantime.
+ FeatureNew.single_use('custom_target() with no name argument', '0.60.0', self.subproject, location=node)
+ name = ''
+ inputs = self.source_strings_to_files(kwargs['input'], strict=False)
+ command = kwargs['command']
+ if command and isinstance(command[0], str):
+ command[0] = self.find_program_impl([command[0]])
+
+ if len(inputs) > 1 and kwargs['feed']:
+ raise InvalidArguments('custom_target: "feed" keyword argument can only be used used with a single input')
+ if len(kwargs['output']) > 1 and kwargs['capture']:
+ raise InvalidArguments('custom_target: "capture" keyword argument can only be used used with a single output')
+ if kwargs['capture'] and kwargs['console']:
+ raise InvalidArguments('custom_target: "capture" and "console" keyword arguments are mutually exclusive')
+ for c in command:
+ if kwargs['capture'] and isinstance(c, str) and '@OUTPUT@' in c:
+ raise InvalidArguments('custom_target: "capture" keyword argument cannot be used with "@OUTPUT@"')
+ if kwargs['feed'] and isinstance(c, str) and '@INPUT@' in c:
+ raise InvalidArguments('custom_target: "feed" keyword argument cannot be used with "@INPUT@"')
+ if kwargs['install'] and not kwargs['install_dir']:
+ raise InvalidArguments('custom_target: "install_dir" keyword argument must be set when "install" is true.')
+ if len(kwargs['install_dir']) > 1:
+ FeatureNew.single_use('multiple install_dir for custom_target', '0.40.0', self.subproject, location=node)
+ if len(kwargs['install_tag']) not in {0, 1, len(kwargs['output'])}:
+ raise InvalidArguments('custom_target: install_tag argument must have 0 or 1 outputs, '
+ 'or the same number of elements as the output keyword argument. '
+ f'(there are {len(kwargs["install_tag"])} install_tags, '
+ f'and {len(kwargs["output"])} outputs)')
+
+ for t in kwargs['output']:
+ self.validate_forbidden_targets(t)
+ self._validate_custom_target_outputs(len(inputs) > 1, kwargs['output'], "custom_target")
+
+ tg = build.CustomTarget(
+ name,
+ self.subdir,
+ self.subproject,
+ self.environment,
+ command,
+ inputs,
+ kwargs['output'],
+ build_always_stale=build_always_stale,
+ build_by_default=build_by_default,
+ capture=kwargs['capture'],
+ console=kwargs['console'],
+ depend_files=kwargs['depend_files'],
+ depfile=kwargs['depfile'],
+ extra_depends=kwargs['depends'],
+ env=kwargs['env'],
+ feed=kwargs['feed'],
+ install=kwargs['install'],
+ install_dir=kwargs['install_dir'],
+ install_mode=install_mode,
+ install_tag=kwargs['install_tag'],
+ backend=self.backend)
+ self.add_target(tg.name, tg)
+ return tg
+
+ @typed_pos_args('run_target', str)
+ @typed_kwargs(
+ 'run_target',
+ COMMAND_KW,
+ DEPENDS_KW,
+ ENV_KW.evolve(since='0.57.0'),
+ )
+ def func_run_target(self, node: mparser.FunctionNode, args: T.Tuple[str],
+ kwargs: 'kwtypes.RunTarget') -> build.RunTarget:
+ all_args = kwargs['command'].copy()
+
+ for i in listify(all_args):
+ if isinstance(i, ExternalProgram) and not i.found():
+ raise InterpreterException(f'Tried to use non-existing executable {i.name!r}')
+ if isinstance(all_args[0], str):
+ all_args[0] = self.find_program_impl([all_args[0]])
+ name = args[0]
+ tg = build.RunTarget(name, all_args, kwargs['depends'], self.subdir, self.subproject, self.environment,
+ kwargs['env'])
+ self.add_target(name, tg)
+ return tg
+
+ @FeatureNew('alias_target', '0.52.0')
+ @typed_pos_args('alias_target', str, varargs=build.Target, min_varargs=1)
+ @noKwargs
+ def func_alias_target(self, node: mparser.BaseNode, args: T.Tuple[str, T.List[build.Target]],
+ kwargs: 'TYPE_kwargs') -> build.AliasTarget:
+ name, deps = args
+ tg = build.AliasTarget(name, deps, self.subdir, self.subproject, self.environment)
+ self.add_target(name, tg)
+ return tg
+
+ @typed_pos_args('generator', (build.Executable, ExternalProgram))
+ @typed_kwargs(
+ 'generator',
+ KwargInfo('arguments', ContainerTypeInfo(list, str, allow_empty=False), required=True, listify=True),
+ KwargInfo('output', ContainerTypeInfo(list, str, allow_empty=False), required=True, listify=True),
+ DEPFILE_KW,
+ DEPENDS_KW,
+ KwargInfo('capture', bool, default=False, since='0.43.0'),
+ )
+ def func_generator(self, node: mparser.FunctionNode,
+ args: T.Tuple[T.Union[build.Executable, ExternalProgram]],
+ kwargs: 'kwtypes.FuncGenerator') -> build.Generator:
+ for rule in kwargs['output']:
+ if '@BASENAME@' not in rule and '@PLAINNAME@' not in rule:
+ raise InvalidArguments('Every element of "output" must contain @BASENAME@ or @PLAINNAME@.')
+ if has_path_sep(rule):
+ raise InvalidArguments('"output" must not contain a directory separator.')
+ if len(kwargs['output']) > 1:
+ for o in kwargs['output']:
+ if '@OUTPUT@' in o:
+ raise InvalidArguments('Tried to use @OUTPUT@ in a rule with more than one output.')
+
+ gen = build.Generator(args[0], **kwargs)
+ self.generators.append(gen)
+ return gen
+
+ @typed_pos_args('benchmark', str, (build.Executable, build.Jar, ExternalProgram, mesonlib.File))
+ @typed_kwargs('benchmark', *TEST_KWS)
+ def func_benchmark(self, node: mparser.BaseNode,
+ args: T.Tuple[str, T.Union[build.Executable, build.Jar, ExternalProgram, mesonlib.File]],
+ kwargs: 'kwtypes.FuncBenchmark') -> None:
+ self.add_test(node, args, kwargs, False)
+
+ @typed_pos_args('test', str, (build.Executable, build.Jar, ExternalProgram, mesonlib.File))
+ @typed_kwargs('test', *TEST_KWS, KwargInfo('is_parallel', bool, default=True))
+ def func_test(self, node: mparser.BaseNode,
+ args: T.Tuple[str, T.Union[build.Executable, build.Jar, ExternalProgram, mesonlib.File]],
+ kwargs: 'kwtypes.FuncTest') -> None:
+ self.add_test(node, args, kwargs, True)
+
+ def unpack_env_kwarg(self, kwargs: T.Union[build.EnvironmentVariables, T.Dict[str, 'TYPE_var'], T.List['TYPE_var'], str]) -> build.EnvironmentVariables:
+ envlist = kwargs.get('env')
+ if envlist is None:
+ return build.EnvironmentVariables()
+ msg = ENV_KW.validator(envlist)
+ if msg:
+ raise InvalidArguments(f'"env": {msg}')
+ return ENV_KW.convertor(envlist)
+
+ def make_test(self, node: mparser.BaseNode,
+ args: T.Tuple[str, T.Union[build.Executable, build.Jar, ExternalProgram, mesonlib.File]],
+ kwargs: 'kwtypes.BaseTest') -> Test:
+ name = args[0]
+ if ':' in name:
+ mlog.deprecation(f'":" is not allowed in test name "{name}", it has been replaced with "_"',
+ location=node)
+ name = name.replace(':', '_')
+ exe = args[1]
+ if isinstance(exe, ExternalProgram):
+ if not exe.found():
+ raise InvalidArguments('Tried to use not-found external program as test exe')
+ elif isinstance(exe, mesonlib.File):
+ exe = self.find_program_impl([exe])
+
+ env = self.unpack_env_kwarg(kwargs)
+
+ if kwargs['timeout'] <= 0:
+ FeatureNew.single_use('test() timeout <= 0', '0.57.0', self.subproject, location=node)
+
+ prj = self.subproject if self.is_subproject() else self.build.project_name
+
+ suite: T.List[str] = []
+ for s in kwargs['suite']:
+ if s:
+ s = ':' + s
+ suite.append(prj.replace(' ', '_').replace(':', '_') + s)
+
+ return Test(name,
+ prj,
+ suite,
+ exe,
+ kwargs['depends'],
+ kwargs.get('is_parallel', False),
+ kwargs['args'],
+ env,
+ kwargs['should_fail'],
+ kwargs['timeout'],
+ kwargs['workdir'],
+ kwargs['protocol'],
+ kwargs['priority'],
+ kwargs['verbose'])
+
+ def add_test(self, node: mparser.BaseNode, args: T.List, kwargs: T.Dict[str, T.Any], is_base_test: bool):
+ t = self.make_test(node, args, kwargs)
+ if is_base_test:
+ self.build.tests.append(t)
+ mlog.debug('Adding test', mlog.bold(t.name, True))
+ else:
+ self.build.benchmarks.append(t)
+ mlog.debug('Adding benchmark', mlog.bold(t.name, True))
+
+ @typed_pos_args('install_headers', varargs=(str, mesonlib.File))
+ @typed_kwargs(
+ 'install_headers',
+ PRESERVE_PATH_KW,
+ KwargInfo('subdir', (str, NoneType)),
+ INSTALL_MODE_KW.evolve(since='0.47.0'),
+ INSTALL_DIR_KW,
+ )
+ def func_install_headers(self, node: mparser.BaseNode,
+ args: T.Tuple[T.List['mesonlib.FileOrString']],
+ kwargs: 'kwtypes.FuncInstallHeaders') -> build.Headers:
+ install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
+ source_files = self.source_strings_to_files(args[0])
+ install_subdir = kwargs['subdir']
+ if install_subdir is not None:
+ if kwargs['install_dir'] is not None:
+ raise InterpreterException('install_headers: cannot specify both "install_dir" and "subdir". Use only "install_dir".')
+ if os.path.isabs(install_subdir):
+ mlog.deprecation('Subdir keyword must not be an absolute path. This will be a hard error in the next release.')
+ else:
+ install_subdir = ''
+
+ dirs = collections.defaultdict(list)
+ ret_headers = []
+ if kwargs['preserve_path']:
+ for file in source_files:
+ dirname = os.path.dirname(file.fname)
+ dirs[dirname].append(file)
+ else:
+ dirs[''].extend(source_files)
+
+ for childdir in dirs:
+ h = build.Headers(dirs[childdir], os.path.join(install_subdir, childdir), kwargs['install_dir'],
+ install_mode, self.subproject)
+ ret_headers.append(h)
+ self.build.headers.append(h)
+
+ return ret_headers
+
+ @typed_pos_args('install_man', varargs=(str, mesonlib.File))
+ @typed_kwargs(
+ 'install_man',
+ KwargInfo('locale', (str, NoneType), since='0.58.0'),
+ INSTALL_MODE_KW.evolve(since='0.47.0'),
+ INSTALL_DIR_KW,
+ )
+ def func_install_man(self, node: mparser.BaseNode,
+ args: T.Tuple[T.List['mesonlib.FileOrString']],
+ kwargs: 'kwtypes.FuncInstallMan') -> build.Man:
+ install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
+ # We just need to narrow this, because the input is limited to files and
+ # Strings as inputs, so only Files will be returned
+ sources = self.source_strings_to_files(args[0])
+ for s in sources:
+ try:
+ num = int(s.rsplit('.', 1)[-1])
+ except (IndexError, ValueError):
+ num = 0
+ if not 1 <= num <= 9:
+ raise InvalidArguments('Man file must have a file extension of a number between 1 and 9')
+
+ m = build.Man(sources, kwargs['install_dir'], install_mode,
+ self.subproject, kwargs['locale'])
+ self.build.man.append(m)
+
+ return m
+
+ @FeatureNew('install_emptydir', '0.60.0')
+ @typed_kwargs(
+ 'install_emptydir',
+ INSTALL_MODE_KW,
+ KwargInfo('install_tag', (str, NoneType), since='0.62.0')
+ )
+ def func_install_emptydir(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs) -> None:
+ d = build.EmptyDir(args[0], kwargs['install_mode'], self.subproject, kwargs['install_tag'])
+ self.build.emptydir.append(d)
+
+ return d
+
+ @FeatureNew('install_symlink', '0.61.0')
+ @typed_pos_args('symlink_name', str)
+ @typed_kwargs(
+ 'install_symlink',
+ KwargInfo('pointing_to', str, required=True),
+ KwargInfo('install_dir', str, required=True),
+ INSTALL_TAG_KW,
+ )
+ def func_install_symlink(self, node: mparser.BaseNode,
+ args: T.Tuple[T.List[str]],
+ kwargs) -> build.SymlinkData:
+ name = args[0] # Validation while creating the SymlinkData object
+ target = kwargs['pointing_to']
+ l = build.SymlinkData(target, name, kwargs['install_dir'],
+ self.subproject, kwargs['install_tag'])
+ self.build.symlinks.append(l)
+ return l
+
+ @FeatureNew('structured_sources', '0.62.0')
+ @typed_pos_args('structured_sources', object, optargs=[dict])
+ @noKwargs
+ @noArgsFlattening
+ def func_structured_sources(
+ self, node: mparser.BaseNode,
+ args: T.Tuple[object, T.Optional[T.Dict[str, object]]],
+ kwargs: 'TYPE_kwargs') -> build.StructuredSources:
+ valid_types = (str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)
+ sources: T.Dict[str, T.List[T.Union[mesonlib.File, 'build.GeneratedTypes']]] = collections.defaultdict(list)
+
+ for arg in mesonlib.listify(args[0]):
+ if not isinstance(arg, valid_types):
+ raise InvalidArguments(f'structured_sources: type "{type(arg)}" is not valid')
+ if isinstance(arg, str):
+ arg = mesonlib.File.from_source_file(self.environment.source_dir, self.subdir, arg)
+ sources[''].append(arg)
+ if args[1]:
+ if '' in args[1]:
+ raise InvalidArguments('structured_sources: keys to dictionary argument may not be an empty string.')
+ for k, v in args[1].items():
+ for arg in mesonlib.listify(v):
+ if not isinstance(arg, valid_types):
+ raise InvalidArguments(f'structured_sources: type "{type(arg)}" is not valid')
+ if isinstance(arg, str):
+ arg = mesonlib.File.from_source_file(self.environment.source_dir, self.subdir, arg)
+ sources[k].append(arg)
+ return build.StructuredSources(sources)
+
+ @typed_pos_args('subdir', str)
+ @typed_kwargs(
+ 'subdir',
+ KwargInfo(
+ 'if_found',
+ ContainerTypeInfo(list, object),
+ validator=lambda a: 'Objects must have a found() method' if not all(hasattr(x, 'found') for x in a) else None,
+ since='0.44.0',
+ default=[],
+ listify=True,
+ ),
+ )
+ def func_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'kwtypes.Subdir') -> None:
+ mesonlib.check_direntry_issues(args)
+ if '..' in args[0]:
+ raise InvalidArguments('Subdir contains ..')
+ if self.subdir == '' and args[0] == self.subproject_dir:
+ raise InvalidArguments('Must not go into subprojects dir with subdir(), use subproject() instead.')
+ if self.subdir == '' and args[0].startswith('meson-'):
+ raise InvalidArguments('The "meson-" prefix is reserved and cannot be used for top-level subdir().')
+ if args[0] == '':
+ raise InvalidArguments("The argument given to subdir() is the empty string ''. This is prohibited.")
+ for i in kwargs['if_found']:
+ if not i.found():
+ return
+
+ prev_subdir = self.subdir
+ subdir = os.path.join(prev_subdir, args[0])
+ if os.path.isabs(subdir):
+ raise InvalidArguments('Subdir argument must be a relative path.')
+ absdir = os.path.join(self.environment.get_source_dir(), subdir)
+ symlinkless_dir = os.path.realpath(absdir)
+ build_file = os.path.join(symlinkless_dir, 'meson.build')
+ if build_file in self.processed_buildfiles:
+ raise InvalidArguments(f'Tried to enter directory "{subdir}", which has already been visited.')
+ self.processed_buildfiles.add(build_file)
+ self.subdir = subdir
+ os.makedirs(os.path.join(self.environment.build_dir, subdir), exist_ok=True)
+ buildfilename = os.path.join(self.subdir, environment.build_filename)
+ self.build_def_files.add(buildfilename)
+ absname = os.path.join(self.environment.get_source_dir(), buildfilename)
+ if not os.path.isfile(absname):
+ self.subdir = prev_subdir
+ raise InterpreterException(f"Non-existent build file '{buildfilename!s}'")
+ with open(absname, encoding='utf-8') as f:
+ code = f.read()
+ assert isinstance(code, str)
+ try:
+ codeblock = mparser.Parser(code, absname).parse()
+ except mesonlib.MesonException as me:
+ me.file = absname
+ raise me
+ try:
+ self.evaluate_codeblock(codeblock)
+ except SubdirDoneRequest:
+ pass
+ self.subdir = prev_subdir
+
+ def _get_kwarg_install_mode(self, kwargs: T.Dict[str, T.Any]) -> T.Optional[FileMode]:
+ if kwargs.get('install_mode', None) is None:
+ return None
+ if isinstance(kwargs['install_mode'], FileMode):
+ return kwargs['install_mode']
+ install_mode: T.List[str] = []
+ mode = mesonlib.typeslistify(kwargs.get('install_mode', []), (str, int))
+ for m in mode:
+ # We skip any arguments that are set to `false`
+ if m is False:
+ m = None
+ install_mode.append(m)
+ if len(install_mode) > 3:
+ raise InvalidArguments('Keyword argument install_mode takes at '
+ 'most 3 arguments.')
+ if len(install_mode) > 0 and install_mode[0] is not None and \
+ not isinstance(install_mode[0], str):
+ raise InvalidArguments('Keyword argument install_mode requires the '
+ 'permissions arg to be a string or false')
+ return FileMode(*install_mode)
+
+ # This is either ignored on basically any OS nowadays, or silently gets
+ # ignored (Solaris) or triggers an "illegal operation" error (FreeBSD).
+ # It was likely added "because it exists", but should never be used. In
+ # theory it is useful for directories, but we never apply modes to
+ # directories other than in install_emptydir.
+ def _warn_kwarg_install_mode_sticky(self, mode: FileMode) -> None:
+ if mode.perms > 0 and mode.perms & stat.S_ISVTX:
+ mlog.deprecation('install_mode with the sticky bit on a file does not do anything and will '
+ 'be ignored since Meson 0.64.0', location=self.current_node)
+ perms = stat.filemode(mode.perms - stat.S_ISVTX)[1:]
+ return FileMode(perms, mode.owner, mode.group)
+ else:
+ return mode
+
+ @typed_pos_args('install_data', varargs=(str, mesonlib.File))
+ @typed_kwargs(
+ 'install_data',
+ KwargInfo('sources', ContainerTypeInfo(list, (str, mesonlib.File)), listify=True, default=[]),
+ KwargInfo('rename', ContainerTypeInfo(list, str), default=[], listify=True, since='0.46.0'),
+ INSTALL_MODE_KW.evolve(since='0.38.0'),
+ INSTALL_TAG_KW.evolve(since='0.60.0'),
+ INSTALL_DIR_KW,
+ PRESERVE_PATH_KW.evolve(since='0.64.0'),
+ )
+ def func_install_data(self, node: mparser.BaseNode,
+ args: T.Tuple[T.List['mesonlib.FileOrString']],
+ kwargs: 'kwtypes.FuncInstallData') -> build.Data:
+ sources = self.source_strings_to_files(args[0] + kwargs['sources'])
+ rename = kwargs['rename'] or None
+ if rename:
+ if len(rename) != len(sources):
+ raise InvalidArguments(
+ '"rename" and "sources" argument lists must be the same length if "rename" is given. '
+ f'Rename has {len(rename)} elements and sources has {len(sources)}.')
+
+ install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
+ return self.install_data_impl(sources, kwargs['install_dir'], install_mode,
+ rename, kwargs['install_tag'],
+ preserve_path=kwargs['preserve_path'])
+
+ def install_data_impl(self, sources: T.List[mesonlib.File], install_dir: T.Optional[str],
+ install_mode: FileMode, rename: T.Optional[str],
+ tag: T.Optional[str],
+ install_dir_name: T.Optional[str] = None,
+ install_data_type: T.Optional[str] = None,
+ preserve_path: bool = False) -> build.Data:
+
+ """Just the implementation with no validation."""
+ idir = install_dir or ''
+ idir_name = install_dir_name or idir or '{datadir}'
+ if isinstance(idir_name, P_OBJ.OptionString):
+ idir_name = idir_name.optname
+ dirs = collections.defaultdict(list)
+ ret_data = []
+ if preserve_path:
+ for file in sources:
+ dirname = os.path.dirname(file.fname)
+ dirs[dirname].append(file)
+ else:
+ dirs[''].extend(sources)
+
+ for childdir, files in dirs.items():
+ d = build.Data(files, os.path.join(idir, childdir), os.path.join(idir_name, childdir),
+ install_mode, self.subproject, rename, tag, install_data_type)
+ ret_data.append(d)
+
+ self.build.data.extend(ret_data)
+ return ret_data
+
+ @typed_pos_args('install_subdir', str)
+ @typed_kwargs(
+ 'install_subdir',
+ KwargInfo('install_dir', str, required=True),
+ KwargInfo('strip_directory', bool, default=False),
+ KwargInfo('exclude_files', ContainerTypeInfo(list, str),
+ default=[], listify=True, since='0.42.0',
+ validator=lambda x: 'cannot be absolute' if any(os.path.isabs(d) for d in x) else None),
+ KwargInfo('exclude_directories', ContainerTypeInfo(list, str),
+ default=[], listify=True, since='0.42.0',
+ validator=lambda x: 'cannot be absolute' if any(os.path.isabs(d) for d in x) else None),
+ INSTALL_MODE_KW.evolve(since='0.38.0'),
+ INSTALL_TAG_KW.evolve(since='0.60.0'),
+ )
+ def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str],
+ kwargs: 'kwtypes.FuncInstallSubdir') -> build.InstallDir:
+ exclude = (set(kwargs['exclude_files']), set(kwargs['exclude_directories']))
+
+ srcdir = os.path.join(self.environment.source_dir, self.subdir, args[0])
+ if not os.path.isdir(srcdir) or not any(os.scandir(srcdir)):
+ FeatureNew.single_use('install_subdir with empty directory', '0.47.0', self.subproject, location=node)
+ FeatureDeprecated.single_use('install_subdir with empty directory', '0.60.0', self.subproject,
+ 'It worked by accident and is buggy. Use install_emptydir instead.', node)
+ install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
+
+ idir_name = kwargs['install_dir']
+ if isinstance(idir_name, P_OBJ.OptionString):
+ idir_name = idir_name.optname
+
+ idir = build.InstallDir(
+ self.subdir,
+ args[0],
+ kwargs['install_dir'],
+ idir_name,
+ install_mode,
+ exclude,
+ kwargs['strip_directory'],
+ self.subproject,
+ install_tag=kwargs['install_tag'])
+ self.build.install_dirs.append(idir)
+ return idir
+
+ @noPosargs
+ @typed_kwargs(
+ 'configure_file',
+ DEPFILE_KW.evolve(since='0.52.0'),
+ INSTALL_MODE_KW.evolve(since='0.47.0,'),
+ INSTALL_TAG_KW.evolve(since='0.60.0'),
+ KwargInfo('capture', bool, default=False, since='0.41.0'),
+ KwargInfo(
+ 'command',
+ (ContainerTypeInfo(list, (build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str), allow_empty=False), NoneType),
+ listify=True,
+ ),
+ KwargInfo(
+ 'configuration',
+ (ContainerTypeInfo(dict, (str, int, bool)), build.ConfigurationData, NoneType),
+ ),
+ KwargInfo(
+ 'copy', bool, default=False, since='0.47.0',
+ deprecated='0.64.0', deprecated_message='Use fs.copyfile instead',
+ ),
+ KwargInfo('encoding', str, default='utf-8', since='0.47.0'),
+ KwargInfo('format', str, default='meson', since='0.46.0',
+ validator=in_set_validator({'meson', 'cmake', 'cmake@'})),
+ KwargInfo(
+ 'input',
+ ContainerTypeInfo(list, (mesonlib.File, str)),
+ listify=True,
+ default=[],
+ ),
+ # Cannot use shared implementation until None backwards compat is dropped
+ KwargInfo('install', (bool, NoneType), since='0.50.0'),
+ KwargInfo('install_dir', (str, bool), default='',
+ validator=lambda x: 'must be `false` if boolean' if x is True else None),
+ OUTPUT_KW,
+ KwargInfo('output_format', str, default='c', since='0.47.0',
+ validator=in_set_validator({'c', 'nasm'})),
+ )
+ def func_configure_file(self, node: mparser.BaseNode, args: T.List[TYPE_var],
+ kwargs: kwtypes.ConfigureFile):
+ actions = sorted(x for x in ['configuration', 'command', 'copy']
+ if kwargs[x] not in [None, False])
+ num_actions = len(actions)
+ if num_actions == 0:
+ raise InterpreterException('Must specify an action with one of these '
+ 'keyword arguments: \'configuration\', '
+ '\'command\', or \'copy\'.')
+ elif num_actions == 2:
+ raise InterpreterException('Must not specify both {!r} and {!r} '
+ 'keyword arguments since they are '
+ 'mutually exclusive.'.format(*actions))
+ elif num_actions == 3:
+ raise InterpreterException('Must specify one of {!r}, {!r}, and '
+ '{!r} keyword arguments since they are '
+ 'mutually exclusive.'.format(*actions))
+
+ if kwargs['capture'] and not kwargs['command']:
+ raise InvalidArguments('configure_file: "capture" keyword requires "command" keyword.')
+
+ install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
+
+ fmt = kwargs['format']
+ output_format = kwargs['output_format']
+ depfile = kwargs['depfile']
+
+ # Validate input
+ inputs = self.source_strings_to_files(kwargs['input'])
+ inputs_abs = []
+ for f in inputs:
+ if isinstance(f, mesonlib.File):
+ inputs_abs.append(f.absolute_path(self.environment.source_dir,
+ self.environment.build_dir))
+ self.add_build_def_file(f)
+ else:
+ raise InterpreterException('Inputs can only be strings or file objects')
+
+ # Validate output
+ output = kwargs['output']
+ if inputs_abs:
+ values = mesonlib.get_filenames_templates_dict(inputs_abs, None)
+ outputs = mesonlib.substitute_values([output], values)
+ output = outputs[0]
+ if depfile:
+ depfile = mesonlib.substitute_values([depfile], values)[0]
+ ofile_rpath = os.path.join(self.subdir, output)
+ if ofile_rpath in self.configure_file_outputs:
+ mesonbuildfile = os.path.join(self.subdir, 'meson.build')
+ current_call = f"{mesonbuildfile}:{self.current_lineno}"
+ first_call = "{}:{}".format(mesonbuildfile, self.configure_file_outputs[ofile_rpath])
+ mlog.warning('Output file', mlog.bold(ofile_rpath, True), 'for configure_file() at', current_call, 'overwrites configure_file() output at', first_call)
+ else:
+ self.configure_file_outputs[ofile_rpath] = self.current_lineno
+ (ofile_path, ofile_fname) = os.path.split(os.path.join(self.subdir, output))
+ ofile_abs = os.path.join(self.environment.build_dir, ofile_path, ofile_fname)
+
+ # Perform the appropriate action
+ if kwargs['configuration'] is not None:
+ conf = kwargs['configuration']
+ if isinstance(conf, dict):
+ FeatureNew.single_use('configure_file.configuration dictionary', '0.49.0', self.subproject, location=node)
+ for k, v in conf.items():
+ if not isinstance(v, (str, int, bool)):
+ raise InvalidArguments(
+ f'"configuration_data": initial value dictionary key "{k!r}"" must be "str | int | bool", not "{v!r}"')
+ conf = build.ConfigurationData(conf)
+ mlog.log('Configuring', mlog.bold(output), 'using configuration')
+ if len(inputs) > 1:
+ raise InterpreterException('At most one input file can given in configuration mode')
+ if inputs:
+ os.makedirs(os.path.join(self.environment.build_dir, self.subdir), exist_ok=True)
+ file_encoding = kwargs['encoding']
+ missing_variables, confdata_useless = \
+ mesonlib.do_conf_file(inputs_abs[0], ofile_abs, conf,
+ fmt, file_encoding)
+ if missing_variables:
+ var_list = ", ".join(repr(m) for m in sorted(missing_variables))
+ mlog.warning(
+ f"The variable(s) {var_list} in the input file '{inputs[0]}' are not "
+ "present in the given configuration data.", location=node)
+ if confdata_useless:
+ ifbase = os.path.basename(inputs_abs[0])
+ tv = FeatureNew.get_target_version(self.subproject)
+ if FeatureNew.check_version(tv, '0.47.0'):
+ mlog.warning('Got an empty configuration_data() object and found no '
+ f'substitutions in the input file {ifbase!r}. If you want to '
+ 'copy a file to the build dir, use the \'copy:\' keyword '
+ 'argument added in 0.47.0', location=node)
+ else:
+ mesonlib.dump_conf_header(ofile_abs, conf, output_format)
+ conf.used = True
+ elif kwargs['command'] is not None:
+ if len(inputs) > 1:
+ FeatureNew.single_use('multiple inputs in configure_file()', '0.52.0', self.subproject, location=node)
+ # We use absolute paths for input and output here because the cwd
+ # that the command is run from is 'unspecified', so it could change.
+ # Currently it's builddir/subdir for in_builddir else srcdir/subdir.
+ values = mesonlib.get_filenames_templates_dict(inputs_abs, [ofile_abs])
+ if depfile:
+ depfile = os.path.join(self.environment.get_scratch_dir(), depfile)
+ values['@DEPFILE@'] = depfile
+ # Substitute @INPUT@, @OUTPUT@, etc here.
+ _cmd = mesonlib.substitute_values(kwargs['command'], values)
+ mlog.log('Configuring', mlog.bold(output), 'with command')
+ cmd, *args = _cmd
+ res = self.run_command_impl(node, (cmd, args),
+ {'capture': True, 'check': True, 'env': build.EnvironmentVariables()},
+ True)
+ if kwargs['capture']:
+ dst_tmp = ofile_abs + '~'
+ file_encoding = kwargs['encoding']
+ with open(dst_tmp, 'w', encoding=file_encoding) as f:
+ f.writelines(res.stdout)
+ if inputs_abs:
+ shutil.copymode(inputs_abs[0], dst_tmp)
+ mesonlib.replace_if_different(ofile_abs, dst_tmp)
+ if depfile:
+ mlog.log('Reading depfile:', mlog.bold(depfile))
+ with open(depfile, encoding='utf-8') as f:
+ df = DepFile(f.readlines())
+ deps = df.get_all_dependencies(ofile_fname)
+ for dep in deps:
+ self.add_build_def_file(dep)
+
+ elif kwargs['copy']:
+ if len(inputs_abs) != 1:
+ raise InterpreterException('Exactly one input file must be given in copy mode')
+ os.makedirs(os.path.join(self.environment.build_dir, self.subdir), exist_ok=True)
+ shutil.copy2(inputs_abs[0], ofile_abs)
+
+ # Install file if requested, we check for the empty string
+ # for backwards compatibility. That was the behaviour before
+ # 0.45.0 so preserve it.
+ idir = kwargs['install_dir']
+ if idir is False:
+ idir = ''
+ FeatureDeprecated.single_use('configure_file install_dir: false', '0.50.0',
+ self.subproject, 'Use the `install:` kwarg instead', location=node)
+ install = kwargs['install'] if kwargs['install'] is not None else idir != ''
+ if install:
+ if not idir:
+ raise InterpreterException(
+ '"install_dir" must be specified when "install" in a configure_file is true')
+ idir_name = idir
+ if isinstance(idir_name, P_OBJ.OptionString):
+ idir_name = idir_name.optname
+ cfile = mesonlib.File.from_built_file(ofile_path, ofile_fname)
+ install_tag = kwargs['install_tag']
+ self.build.data.append(build.Data([cfile], idir, idir_name, install_mode, self.subproject,
+ install_tag=install_tag, data_type='configure'))
+ return mesonlib.File.from_built_file(self.subdir, output)
+
+ def extract_incdirs(self, kwargs, key: str = 'include_directories'):
+ prospectives = extract_as_list(kwargs, key)
+ if key == 'include_directories':
+ for i in prospectives:
+ if isinstance(i, str):
+ FeatureNew.single_use('include_directories kwarg of type string', '0.50.0', self.subproject,
+ f'Use include_directories({i!r}) instead', location=self.current_node)
+ break
+
+ result = []
+ for p in prospectives:
+ if isinstance(p, build.IncludeDirs):
+ result.append(p)
+ elif isinstance(p, str):
+ result.append(self.build_incdir_object([p]))
+ else:
+ raise InterpreterException('Include directory objects can only be created from strings or include directories.')
+ return result
+
+ @typed_pos_args('include_directories', varargs=str)
+ @typed_kwargs('include_directories', KwargInfo('is_system', bool, default=False))
+ def func_include_directories(self, node: mparser.BaseNode, args: T.Tuple[T.List[str]],
+ kwargs: 'kwtypes.FuncIncludeDirectories') -> build.IncludeDirs:
+ return self.build_incdir_object(args[0], kwargs['is_system'])
+
+ def build_incdir_object(self, incdir_strings: T.List[str], is_system: bool = False) -> build.IncludeDirs:
+ if not isinstance(is_system, bool):
+ raise InvalidArguments('Is_system must be boolean.')
+ src_root = self.environment.get_source_dir()
+ build_root = self.environment.get_build_dir()
+ absbase_src = os.path.join(src_root, self.subdir)
+ absbase_build = os.path.join(build_root, self.subdir)
+
+ for a in incdir_strings:
+ if a.startswith(src_root):
+ raise InvalidArguments(textwrap.dedent('''\
+ Tried to form an absolute path to a dir in the source tree.
+ You should not do that but use relative paths instead, for
+ directories that are part of your project.
+
+ To get include path to any directory relative to the current dir do
+
+ incdir = include_directories(dirname)
+
+ After this incdir will contain both the current source dir as well as the
+ corresponding build dir. It can then be used in any subdirectory and
+ Meson will take care of all the busywork to make paths work.
+
+ Dirname can even be '.' to mark the current directory. Though you should
+ remember that the current source and build directories are always
+ put in the include directories by default so you only need to do
+ include_directories('.') if you intend to use the result in a
+ different subdirectory.
+
+ Note that this error message can also be triggered by
+ external dependencies being installed within your source
+ tree - it's not recommended to do this.
+ '''))
+ else:
+ try:
+ self.validate_within_subproject(self.subdir, a)
+ except InterpreterException:
+ mlog.warning('include_directories sandbox violation!', location=self.current_node)
+ print(textwrap.dedent(f'''\
+ The project is trying to access the directory {a!r} which belongs to a different
+ subproject. This is a problem as it hardcodes the relative paths of these two projects.
+ This makes it impossible to compile the project in any other directory layout and also
+ prevents the subproject from changing its own directory layout.
+
+ Instead of poking directly at the internals the subproject should be executed and
+ it should set a variable that the caller can then use. Something like:
+
+ # In subproject
+ some_dep = declare_dependency(include_directories: include_directories('include'))
+
+ # In subproject wrap file
+ [provide]
+ some = some_dep
+
+ # In parent project
+ some_dep = dependency('some')
+ executable(..., dependencies: [some_dep])
+
+ This warning will become a hard error in a future Meson release.
+ '''))
+ absdir_src = os.path.join(absbase_src, a)
+ absdir_build = os.path.join(absbase_build, a)
+ if not os.path.isdir(absdir_src) and not os.path.isdir(absdir_build):
+ raise InvalidArguments(f'Include dir {a} does not exist.')
+ i = build.IncludeDirs(self.subdir, incdir_strings, is_system)
+ return i
+
+ @typed_pos_args('add_test_setup', str)
+ @typed_kwargs(
+ 'add_test_setup',
+ KwargInfo('exe_wrapper', ContainerTypeInfo(list, (str, ExternalProgram)), listify=True, default=[]),
+ KwargInfo('gdb', bool, default=False),
+ KwargInfo('timeout_multiplier', int, default=1),
+ KwargInfo('exclude_suites', ContainerTypeInfo(list, str), listify=True, default=[], since='0.57.0'),
+ KwargInfo('is_default', bool, default=False, since='0.49.0'),
+ ENV_KW,
+ )
+ def func_add_test_setup(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'kwtypes.AddTestSetup') -> None:
+ setup_name = args[0]
+ if re.fullmatch('([_a-zA-Z][_0-9a-zA-Z]*:)?[_a-zA-Z][_0-9a-zA-Z]*', setup_name) is None:
+ raise InterpreterException('Setup name may only contain alphanumeric characters.')
+ if ":" not in setup_name:
+ setup_name = f'{(self.subproject if self.subproject else self.build.project_name)}:{setup_name}'
+
+ exe_wrapper: T.List[str] = []
+ for i in kwargs['exe_wrapper']:
+ if isinstance(i, str):
+ exe_wrapper.append(i)
+ else:
+ if not i.found():
+ raise InterpreterException('Tried to use non-found executable.')
+ exe_wrapper += i.get_command()
+
+ timeout_multiplier = kwargs['timeout_multiplier']
+ if timeout_multiplier <= 0:
+ FeatureNew('add_test_setup() timeout_multiplier <= 0', '0.57.0').use(self.subproject)
+
+ if kwargs['is_default']:
+ if self.build.test_setup_default_name is not None:
+ raise InterpreterException(f'{self.build.test_setup_default_name!r} is already set as default. '
+ 'is_default can be set to true only once')
+ self.build.test_setup_default_name = setup_name
+ self.build.test_setups[setup_name] = build.TestSetup(exe_wrapper, kwargs['gdb'], timeout_multiplier, kwargs['env'],
+ kwargs['exclude_suites'])
+
+ @typed_pos_args('add_global_arguments', varargs=str)
+ @typed_kwargs('add_global_arguments', NATIVE_KW, LANGUAGE_KW)
+ def func_add_global_arguments(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
+ self._add_global_arguments(node, self.build.global_args[kwargs['native']], args[0], kwargs)
+
+ @typed_pos_args('add_global_link_arguments', varargs=str)
+ @typed_kwargs('add_global_arguments', NATIVE_KW, LANGUAGE_KW)
+ def func_add_global_link_arguments(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
+ self._add_global_arguments(node, self.build.global_link_args[kwargs['native']], args[0], kwargs)
+
+ @typed_pos_args('add_project_arguments', varargs=str)
+ @typed_kwargs('add_project_arguments', NATIVE_KW, LANGUAGE_KW)
+ def func_add_project_arguments(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
+ self._add_project_arguments(node, self.build.projects_args[kwargs['native']], args[0], kwargs)
+
+ @typed_pos_args('add_project_link_arguments', varargs=str)
+ @typed_kwargs('add_global_arguments', NATIVE_KW, LANGUAGE_KW)
+ def func_add_project_link_arguments(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
+ self._add_project_arguments(node, self.build.projects_link_args[kwargs['native']], args[0], kwargs)
+
+ @FeatureNew('add_project_dependencies', '0.63.0')
+ @typed_pos_args('add_project_dependencies', varargs=dependencies.Dependency)
+ @typed_kwargs('add_project_dependencies', NATIVE_KW, LANGUAGE_KW)
+ def func_add_project_dependencies(self, node: mparser.FunctionNode, args: T.Tuple[T.List[dependencies.Dependency]], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
+ for_machine = kwargs['native']
+ for lang in kwargs['language']:
+ if lang not in self.compilers[for_machine]:
+ raise InvalidCode(f'add_project_dependencies() called before add_language() for language "{lang}"')
+
+ for d in dependencies.get_leaf_external_dependencies(args[0]):
+ compile_args = list(d.get_compile_args())
+ system_incdir = d.get_include_type() == 'system'
+ for i in d.get_include_dirs():
+ for lang in kwargs['language']:
+ comp = self.coredata.compilers[for_machine][lang]
+ for idir in i.to_string_list(self.environment.get_source_dir(), self.environment.get_build_dir()):
+ compile_args.extend(comp.get_include_args(idir, system_incdir))
+
+ self._add_project_arguments(node, self.build.projects_args[for_machine], compile_args, kwargs)
+ self._add_project_arguments(node, self.build.projects_link_args[for_machine], d.get_link_args(), kwargs)
+
+ def _warn_about_builtin_args(self, args: T.List[str]) -> None:
+ # -Wpedantic is deliberately not included, since some people want to use it but not use -Wextra
+ # see e.g.
+ # https://github.com/mesonbuild/meson/issues/3275#issuecomment-641354956
+ # https://github.com/mesonbuild/meson/issues/3742
+ warnargs = ('/W1', '/W2', '/W3', '/W4', '/Wall', '-Wall', '-Wextra')
+ optargs = ('-O0', '-O2', '-O3', '-Os', '-Oz', '/O1', '/O2', '/Os')
+ for arg in args:
+ if arg in warnargs:
+ mlog.warning(f'Consider using the built-in warning_level option instead of using "{arg}".',
+ location=self.current_node)
+ elif arg in optargs:
+ mlog.warning(f'Consider using the built-in optimization level instead of using "{arg}".',
+ location=self.current_node)
+ elif arg == '-Werror':
+ mlog.warning(f'Consider using the built-in werror option instead of using "{arg}".',
+ location=self.current_node)
+ elif arg == '-g':
+ mlog.warning(f'Consider using the built-in debug option instead of using "{arg}".',
+ location=self.current_node)
+ elif arg.startswith('-fsanitize'):
+ mlog.warning(f'Consider using the built-in option for sanitizers instead of using "{arg}".',
+ location=self.current_node)
+ elif arg.startswith('-std=') or arg.startswith('/std:'):
+ mlog.warning(f'Consider using the built-in option for language standard version instead of using "{arg}".',
+ location=self.current_node)
+
+ def _add_global_arguments(self, node: mparser.FunctionNode, argsdict: T.Dict[str, T.List[str]],
+ args: T.List[str], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
+ if self.is_subproject():
+ msg = f'Function \'{node.func_name}\' cannot be used in subprojects because ' \
+ 'there is no way to make that reliable.\nPlease only call ' \
+ 'this if is_subproject() returns false. Alternatively, ' \
+ 'define a variable that\ncontains your language-specific ' \
+ 'arguments and add it to the appropriate *_args kwarg ' \
+ 'in each target.'
+ raise InvalidCode(msg)
+ frozen = self.project_args_frozen or self.global_args_frozen
+ self._add_arguments(node, argsdict, frozen, args, kwargs)
+
+ def _add_project_arguments(self, node: mparser.FunctionNode, argsdict: T.Dict[str, T.Dict[str, T.List[str]]],
+ args: T.List[str], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
+ if self.subproject not in argsdict:
+ argsdict[self.subproject] = {}
+ self._add_arguments(node, argsdict[self.subproject],
+ self.project_args_frozen, args, kwargs)
+
+ def _add_arguments(self, node: mparser.FunctionNode, argsdict: T.Dict[str, T.List[str]],
+ args_frozen: bool, args: T.List[str], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
+ if args_frozen:
+ msg = f'Tried to use \'{node.func_name}\' after a build target has been declared.\n' \
+ 'This is not permitted. Please declare all arguments before your targets.'
+ raise InvalidCode(msg)
+
+ self._warn_about_builtin_args(args)
+
+ for lang in kwargs['language']:
+ argsdict[lang] = argsdict.get(lang, []) + args
+
+ @noArgsFlattening
+ @typed_pos_args('environment', optargs=[(str, list, dict)])
+ @typed_kwargs('environment', ENV_METHOD_KW, ENV_SEPARATOR_KW.evolve(since='0.62.0'))
+ def func_environment(self, node: mparser.FunctionNode, args: T.Tuple[T.Union[None, str, T.List['TYPE_var'], T.Dict[str, 'TYPE_var']]],
+ kwargs: 'TYPE_kwargs') -> build.EnvironmentVariables:
+ init = args[0]
+ if init is not None:
+ FeatureNew.single_use('environment positional arguments', '0.52.0', self.subproject, location=node)
+ msg = ENV_KW.validator(init)
+ if msg:
+ raise InvalidArguments(f'"environment": {msg}')
+ if isinstance(init, dict) and any(i for i in init.values() if isinstance(i, list)):
+ FeatureNew.single_use('List of string in dictionary value', '0.62.0', self.subproject, location=node)
+ return env_convertor_with_method(init, kwargs['method'], kwargs['separator'])
+ return build.EnvironmentVariables()
+
+ @typed_pos_args('join_paths', varargs=str, min_varargs=1)
+ @noKwargs
+ def func_join_paths(self, node: mparser.BaseNode, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> str:
+ parts = args[0]
+ other = os.path.join('', *parts[1:]).replace('\\', '/')
+ ret = os.path.join(*parts).replace('\\', '/')
+ if isinstance(parts[0], P_OBJ.DependencyVariableString) and '..' not in other:
+ return P_OBJ.DependencyVariableString(ret)
+ elif isinstance(parts[0], P_OBJ.OptionString):
+ name = os.path.join(parts[0].optname, other)
+ return P_OBJ.OptionString(ret, name)
+ else:
+ return ret
+
+ def run(self) -> None:
+ super().run()
+ mlog.log('Build targets in project:', mlog.bold(str(len(self.build.targets))))
+ FeatureNew.report(self.subproject)
+ FeatureDeprecated.report(self.subproject)
+ if not self.is_subproject():
+ self.print_extra_warnings()
+ self._print_summary()
+
+ def print_extra_warnings(self) -> None:
+ # TODO cross compilation
+ for c in self.coredata.compilers.host.values():
+ if c.get_id() == 'clang':
+ self.check_clang_asan_lundef()
+ break
+
+ def check_clang_asan_lundef(self) -> None:
+ if OptionKey('b_lundef') not in self.coredata.options:
+ return
+ if OptionKey('b_sanitize') not in self.coredata.options:
+ return
+ if (self.coredata.options[OptionKey('b_lundef')].value and
+ self.coredata.options[OptionKey('b_sanitize')].value != 'none'):
+ mlog.warning('''Trying to use {} sanitizer on Clang with b_lundef.
+This will probably not work.
+Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey('b_sanitize')].value),
+ location=self.current_node)
+
+ # Check that the indicated file is within the same subproject
+ # as we currently are. This is to stop people doing
+ # nasty things like:
+ #
+ # f = files('../../master_src/file.c')
+ #
+ # Note that this is validated only when the file
+ # object is generated. The result can be used in a different
+ # subproject than it is defined in (due to e.g. a
+ # declare_dependency).
+ def validate_within_subproject(self, subdir, fname):
+ srcdir = Path(self.environment.source_dir)
+ builddir = Path(self.environment.build_dir)
+ if isinstance(fname, P_OBJ.DependencyVariableString):
+ def validate_installable_file(fpath: Path) -> bool:
+ installablefiles: T.Set[Path] = set()
+ for d in self.build.data:
+ for s in d.sources:
+ installablefiles.add(Path(s.absolute_path(srcdir, builddir)))
+ installabledirs = [str(Path(srcdir, s.source_subdir)) for s in self.build.install_dirs]
+ if fpath in installablefiles:
+ return True
+ for d in installabledirs:
+ if str(fpath).startswith(d):
+ return True
+ return False
+
+ norm = Path(fname)
+ # variables built from a dep.get_variable are allowed to refer to
+ # subproject files, as long as they are scheduled to be installed.
+ if validate_installable_file(norm):
+ return
+ norm = Path(os.path.abspath(Path(srcdir, subdir, fname)))
+ if os.path.isdir(norm):
+ inputtype = 'directory'
+ else:
+ inputtype = 'file'
+ if InterpreterRuleRelaxation.ALLOW_BUILD_DIR_FILE_REFFERENCES in self.relaxations and builddir in norm.parents:
+ return
+ if srcdir not in norm.parents:
+ # Grabbing files outside the source tree is ok.
+ # This is for vendor stuff like:
+ #
+ # /opt/vendorsdk/src/file_with_license_restrictions.c
+ return
+ project_root = Path(srcdir, self.root_subdir)
+ subproject_dir = project_root / self.subproject_dir
+ if norm == project_root:
+ return
+ if project_root not in norm.parents:
+ raise InterpreterException(f'Sandbox violation: Tried to grab {inputtype} {norm.name} outside current (sub)project.')
+ if subproject_dir == norm or subproject_dir in norm.parents:
+ raise InterpreterException(f'Sandbox violation: Tried to grab {inputtype} {norm.name} from a nested subproject.')
+
+ @T.overload
+ def source_strings_to_files(self, sources: T.List['mesonlib.FileOrString'], strict: bool = True) -> T.List['mesonlib.File']: ...
+
+ @T.overload
+ def source_strings_to_files(self, sources: T.List['mesonlib.FileOrString'], strict: bool = False) -> T.List['mesonlib.FileOrString']: ... # noqa: F811
+
+ @T.overload
+ def source_strings_to_files(self, sources: T.List[mesonlib.FileOrString, build.GeneratedTypes]) -> T.List[T.Union[mesonlib.File, build.GeneratedTypes]]: ... # noqa: F811
+
+ @T.overload
+ def source_strings_to_files(self, sources: T.List['SourceInputs'], strict: bool = True) -> T.List['SourceOutputs']: ... # noqa: F811
+
+ def source_strings_to_files(self, sources: T.List['SourceInputs'], strict: bool = True) -> T.List['SourceOutputs']: # noqa: F811
+ """Lower inputs to a list of Targets and Files, replacing any strings.
+
+ :param sources: A raw (Meson DSL) list of inputs (targets, files, and
+ strings)
+ :raises InterpreterException: if any of the inputs are of an invalid type
+ :return: A list of Targets and Files
+ """
+ mesonlib.check_direntry_issues(sources)
+ if not isinstance(sources, list):
+ sources = [sources]
+ results: T.List['SourceOutputs'] = []
+ for s in sources:
+ if isinstance(s, str):
+ if not strict and s.startswith(self.environment.get_build_dir()):
+ results.append(s)
+ mlog.warning(f'Source item {s!r} cannot be converted to File object, because it is a generated file. '
+ 'This will become a hard error in the future.', location=self.current_node)
+ else:
+ self.validate_within_subproject(self.subdir, s)
+ results.append(mesonlib.File.from_source_file(self.environment.source_dir, self.subdir, s))
+ elif isinstance(s, mesonlib.File):
+ results.append(s)
+ elif isinstance(s, (build.GeneratedList, build.BuildTarget,
+ build.CustomTargetIndex, build.CustomTarget,
+ build.ExtractedObjects, build.StructuredSources)):
+ results.append(s)
+ else:
+ raise InterpreterException(f'Source item is {s!r} instead of '
+ 'string or File-type object')
+ return results
+
+ @staticmethod
+ def validate_forbidden_targets(name: str) -> None:
+ if name.startswith('meson-internal__'):
+ raise InvalidArguments("Target names starting with 'meson-internal__' are reserved "
+ "for Meson's internal use. Please rename.")
+ if name.startswith('meson-') and '.' not in name:
+ raise InvalidArguments("Target names starting with 'meson-' and without a file extension "
+ "are reserved for Meson's internal use. Please rename.")
+ if name in coredata.FORBIDDEN_TARGET_NAMES:
+ raise InvalidArguments(f"Target name '{name}' is reserved for Meson's "
+ "internal use. Please rename.")
+
+ def add_target(self, name: str, tobj: build.Target) -> None:
+ if name == '':
+ raise InterpreterException('Target name must not be empty.')
+ if name.strip() == '':
+ raise InterpreterException('Target name must not consist only of whitespace.')
+ if has_path_sep(name):
+ pathseg = os.path.join(self.subdir, os.path.split(name)[0])
+ if os.path.exists(os.path.join(self.source_root, pathseg)):
+ raise InvalidArguments(textwrap.dedent(f'''\
+ Target "{name}" has a path segment pointing to directory "{pathseg}". This is an error.
+ To define a target that builds in that directory you must define it
+ in the meson.build file in that directory.
+ '''))
+ self.validate_forbidden_targets(name)
+ # To permit an executable and a shared library to have the
+ # same name, such as "foo.exe" and "libfoo.a".
+ idname = tobj.get_id()
+ if idname in self.build.targets:
+ raise InvalidCode(f'Tried to create target "{name}", but a target of that name already exists.')
+
+ if isinstance(tobj, build.BuildTarget):
+ missing_languages = tobj.process_compilers()
+ self.add_languages(missing_languages, True, tobj.for_machine)
+ tobj.process_compilers_late(missing_languages)
+ self.add_stdlib_info(tobj)
+
+ self.build.targets[idname] = tobj
+ if idname not in self.coredata.target_guids:
+ self.coredata.target_guids[idname] = str(uuid.uuid4()).upper()
+
+ @FeatureNew('both_libraries', '0.46.0')
+ def build_both_libraries(self, node, args, kwargs):
+ shared_lib = self.build_target(node, args, kwargs, build.SharedLibrary)
+ static_lib = self.build_target(node, args, kwargs, build.StaticLibrary)
+
+ # Check if user forces non-PIC static library.
+ pic = True
+ key = OptionKey('b_staticpic')
+ if 'pic' in kwargs:
+ pic = kwargs['pic']
+ elif key in self.environment.coredata.options:
+ pic = self.environment.coredata.options[key].value
+
+ if self.backend.name == 'xcode':
+ # Xcode is a bit special in that you can't (at least for the moment)
+ # form a library only from object file inputs. The simple but inefficient
+ # solution is to use the sources directly. This will lead to them being
+ # built twice. This is unfortunate and slow, but at least it works.
+ # Feel free to submit patches to get this fixed if it is an
+ # issue for you.
+ reuse_object_files = False
+ else:
+ reuse_object_files = pic
+
+ if reuse_object_files:
+ # Replace sources with objects from the shared library to avoid
+ # building them twice. We post-process the static library instead of
+ # removing sources from args because sources could also come from
+ # any InternalDependency, see BuildTarget.add_deps().
+ static_lib.objects.append(build.ExtractedObjects(shared_lib, shared_lib.sources, shared_lib.generated, []))
+ static_lib.sources = []
+ static_lib.generated = []
+ # Compilers with no corresponding sources confuses the backend.
+ # Keep only compilers used for linking
+ static_lib.compilers = {k: v for k, v in static_lib.compilers.items() if k in compilers.clink_langs}
+
+ return build.BothLibraries(shared_lib, static_lib)
+
+ def build_library(self, node, args, kwargs):
+ default_library = self.coredata.get_option(OptionKey('default_library', subproject=self.subproject))
+ if default_library == 'shared':
+ return self.build_target(node, args, kwargs, build.SharedLibrary)
+ elif default_library == 'static':
+ return self.build_target(node, args, kwargs, build.StaticLibrary)
+ elif default_library == 'both':
+ return self.build_both_libraries(node, args, kwargs)
+ else:
+ raise InterpreterException(f'Unknown default_library value: {default_library}.')
+
+ def build_target(self, node: mparser.BaseNode, args, kwargs, targetclass):
+ @FeatureNewKwargs('build target', '0.42.0', ['rust_crate_type', 'build_rpath', 'implicit_include_directories'])
+ @FeatureNewKwargs('build target', '0.41.0', ['rust_args'])
+ @FeatureNewKwargs('build target', '0.38.0', ['build_by_default'])
+ @FeatureNewKwargs('build target', '0.48.0', ['gnu_symbol_visibility'])
+ def build_target_decorator_caller(self, node, args, kwargs):
+ return True
+
+ build_target_decorator_caller(self, node, args, kwargs)
+
+ if not args:
+ raise InterpreterException('Target does not have a name.')
+ name, *sources = args
+ for_machine = self.machine_from_native_kwarg(kwargs)
+ if 'sources' in kwargs:
+ sources += listify(kwargs['sources'])
+ sources = self.source_strings_to_files(sources)
+ objs = extract_as_list(kwargs, 'objects')
+ kwargs['dependencies'] = extract_as_list(kwargs, 'dependencies')
+ kwargs['install_mode'] = self._get_kwarg_install_mode(kwargs)
+ if 'extra_files' in kwargs:
+ ef = extract_as_list(kwargs, 'extra_files')
+ kwargs['extra_files'] = self.source_strings_to_files(ef)
+ self.check_sources_exist(os.path.join(self.source_root, self.subdir), sources)
+ if targetclass not in {build.Executable, build.SharedLibrary, build.SharedModule, build.StaticLibrary, build.Jar}:
+ mlog.debug('Unknown target type:', str(targetclass))
+ raise RuntimeError('Unreachable code')
+ self.kwarg_strings_to_includedirs(kwargs)
+
+ # Filter out kwargs from other target types. For example 'soversion'
+ # passed to library() when default_library == 'static'.
+ kwargs = {k: v for k, v in kwargs.items() if k in targetclass.known_kwargs}
+
+ srcs: T.List['SourceInputs'] = []
+ struct: T.Optional[build.StructuredSources] = build.StructuredSources()
+ for s in sources:
+ if isinstance(s, build.StructuredSources):
+ struct = struct + s
+ else:
+ srcs.append(s)
+
+ if not struct:
+ struct = None
+ else:
+ # Validate that we won't end up with two outputs with the same name.
+ # i.e, don't allow:
+ # [structured_sources('foo/bar.rs'), structured_sources('bar/bar.rs')]
+ for v in struct.sources.values():
+ outputs: T.Set[str] = set()
+ for f in v:
+ o: T.List[str]
+ if isinstance(f, str):
+ o = [os.path.basename(f)]
+ elif isinstance(f, mesonlib.File):
+ o = [f.fname]
+ else:
+ o = f.get_outputs()
+ conflicts = outputs.intersection(o)
+ if conflicts:
+ raise InvalidArguments.from_node(
+ f"Conflicting sources in structured sources: {', '.join(sorted(conflicts))}",
+ node=node)
+ outputs.update(o)
+
+ kwargs['include_directories'] = self.extract_incdirs(kwargs)
+ target = targetclass(name, self.subdir, self.subproject, for_machine, srcs, struct, objs,
+ self.environment, self.compilers[for_machine], kwargs)
+ target.project_version = self.project_version
+
+ self.add_target(name, target)
+ self.project_args_frozen = True
+ return target
+
+ def kwarg_strings_to_includedirs(self, kwargs):
+ if 'd_import_dirs' in kwargs:
+ items = mesonlib.extract_as_list(kwargs, 'd_import_dirs')
+ cleaned_items = []
+ for i in items:
+ if isinstance(i, str):
+ # BW compatibility. This was permitted so we must support it
+ # for a few releases so people can transition to "correct"
+ # path declarations.
+ if os.path.normpath(i).startswith(self.environment.get_source_dir()):
+ mlog.warning('''Building a path to the source dir is not supported. Use a relative path instead.
+This will become a hard error in the future.''', location=self.current_node)
+ i = os.path.relpath(i, os.path.join(self.environment.get_source_dir(), self.subdir))
+ i = self.build_incdir_object([i])
+ cleaned_items.append(i)
+ kwargs['d_import_dirs'] = cleaned_items
+
+ def add_stdlib_info(self, target):
+ for l in target.compilers.keys():
+ dep = self.build.stdlibs[target.for_machine].get(l, None)
+ if dep:
+ target.add_deps(dep)
+
+ def check_sources_exist(self, subdir, sources):
+ for s in sources:
+ if not isinstance(s, str):
+ continue # This means a generated source and they always exist.
+ fname = os.path.join(subdir, s)
+ if not os.path.isfile(fname):
+ raise InterpreterException(f'Tried to add non-existing source file {s}.')
+
+ # Only permit object extraction from the same subproject
+ def validate_extraction(self, buildtarget: mesonlib.HoldableObject) -> None:
+ if self.subproject != buildtarget.subproject:
+ raise InterpreterException('Tried to extract objects from a different subproject.')
+
+ def is_subproject(self) -> bool:
+ return self.subproject != ''
+
+ @typed_pos_args('set_variable', str, object)
+ @noKwargs
+ @noArgsFlattening
+ @noSecondLevelHolderResolving
+ def func_set_variable(self, node: mparser.BaseNode, args: T.Tuple[str, object], kwargs: 'TYPE_kwargs') -> None:
+ varname, value = args
+ self.set_variable(varname, value, holderify=True)
+
+ @typed_pos_args('get_variable', (str, Disabler), optargs=[object])
+ @noKwargs
+ @noArgsFlattening
+ @unholder_return
+ def func_get_variable(self, node: mparser.BaseNode, args: T.Tuple[T.Union[str, Disabler], T.Optional[object]],
+ kwargs: 'TYPE_kwargs') -> 'TYPE_var':
+ varname, fallback = args
+ if isinstance(varname, Disabler):
+ return varname
+
+ try:
+ return self.variables[varname]
+ except KeyError:
+ if fallback is not None:
+ return self._holderify(fallback)
+ raise InterpreterException(f'Tried to get unknown variable "{varname}".')
+
+ @typed_pos_args('is_variable', str)
+ @noKwargs
+ def func_is_variable(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool:
+ return args[0] in self.variables
+
+ @FeatureNew('unset_variable', '0.60.0')
+ @typed_pos_args('unset_variable', str)
+ @noKwargs
+ def func_unset_variable(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> None:
+ varname = args[0]
+ try:
+ del self.variables[varname]
+ except KeyError:
+ raise InterpreterException(f'Tried to unset unknown variable "{varname}".')
+
+ @staticmethod
+ def machine_from_native_kwarg(kwargs: T.Dict[str, T.Any]) -> MachineChoice:
+ native = kwargs.get('native', False)
+ if not isinstance(native, bool):
+ raise InvalidArguments('Argument to "native" must be a boolean.')
+ return MachineChoice.BUILD if native else MachineChoice.HOST
+
+ @FeatureNew('is_disabler', '0.52.0')
+ @typed_pos_args('is_disabler', object)
+ @noKwargs
+ def func_is_disabler(self, node: mparser.BaseNode, args: T.Tuple[object], kwargs: 'TYPE_kwargs') -> bool:
+ return isinstance(args[0], Disabler)
+
+ @noKwargs
+ @FeatureNew('range', '0.58.0')
+ @typed_pos_args('range', int, optargs=[int, int])
+ def func_range(self, node, args: T.Tuple[int, T.Optional[int], T.Optional[int]], kwargs: T.Dict[str, T.Any]) -> P_OBJ.RangeHolder:
+ start, stop, step = args
+ # Just like Python's range, we allow range(stop), range(start, stop), or
+ # range(start, stop, step)
+ if stop is None:
+ stop = start
+ start = 0
+ if step is None:
+ step = 1
+ # This is more strict than Python's range()
+ if start < 0:
+ raise InterpreterException('start cannot be negative')
+ if stop < start:
+ raise InterpreterException('stop cannot be less than start')
+ if step < 1:
+ raise InterpreterException('step must be >=1')
+ return P_OBJ.RangeHolder(start, stop, step, subproject=self.subproject)
diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py
new file mode 100644
index 0000000..538d134
--- /dev/null
+++ b/mesonbuild/interpreter/interpreterobjects.py
@@ -0,0 +1,987 @@
+from __future__ import annotations
+import os
+import shlex
+import subprocess
+import copy
+import textwrap
+
+from pathlib import Path, PurePath
+
+from .. import mesonlib
+from .. import coredata
+from .. import build
+from .. import mlog
+
+from ..modules import ModuleReturnValue, ModuleObject, ModuleState, ExtensionModule
+from ..backend.backends import TestProtocol
+from ..interpreterbase import (
+ ContainerTypeInfo, KwargInfo, MesonOperator,
+ MesonInterpreterObject, ObjectHolder, MutableInterpreterObject,
+ FeatureNew, FeatureDeprecated,
+ typed_pos_args, typed_kwargs, typed_operator,
+ noArgsFlattening, noPosargs, noKwargs, unholder_return,
+ flatten, resolve_second_level_holders, InterpreterException, InvalidArguments, InvalidCode)
+from ..interpreter.type_checking import NoneType, ENV_SEPARATOR_KW
+from ..dependencies import Dependency, ExternalLibrary, InternalDependency
+from ..programs import ExternalProgram
+from ..mesonlib import HoldableObject, OptionKey, listify, Popen_safe
+
+import typing as T
+
+if T.TYPE_CHECKING:
+ from . import kwargs
+ from ..cmake.interpreter import CMakeInterpreter
+ from ..envconfig import MachineInfo
+ from ..interpreterbase import FeatureCheckBase, InterpreterObject, SubProject, TYPE_var, TYPE_kwargs, TYPE_nvar, TYPE_nkwargs
+ from .interpreter import Interpreter
+
+ from typing_extensions import TypedDict
+
+ class EnvironmentSeparatorKW(TypedDict):
+
+ separator: str
+
+
+def extract_required_kwarg(kwargs: 'kwargs.ExtractRequired',
+ subproject: 'SubProject',
+ feature_check: T.Optional[FeatureCheckBase] = None,
+ default: bool = True) -> T.Tuple[bool, bool, T.Optional[str]]:
+ val = kwargs.get('required', default)
+ disabled = False
+ required = False
+ feature: T.Optional[str] = None
+ if isinstance(val, coredata.UserFeatureOption):
+ if not feature_check:
+ feature_check = FeatureNew('User option "feature"', '0.47.0')
+ feature_check.use(subproject)
+ feature = val.name
+ if val.is_disabled():
+ disabled = True
+ elif val.is_enabled():
+ required = True
+ elif isinstance(val, bool):
+ required = val
+ else:
+ raise InterpreterException('required keyword argument must be boolean or a feature option')
+
+ # Keep boolean value in kwargs to simplify other places where this kwarg is
+ # checked.
+ # TODO: this should be removed, and those callers should learn about FeatureOptions
+ kwargs['required'] = required
+
+ return disabled, required, feature
+
+def extract_search_dirs(kwargs: 'kwargs.ExtractSearchDirs') -> T.List[str]:
+ search_dirs_str = mesonlib.stringlistify(kwargs.get('dirs', []))
+ search_dirs = [Path(d).expanduser() for d in search_dirs_str]
+ for d in search_dirs:
+ if mesonlib.is_windows() and d.root.startswith('\\'):
+ # a Unix-path starting with `/` that is not absolute on Windows.
+ # discard without failing for end-user ease of cross-platform directory arrays
+ continue
+ if not d.is_absolute():
+ raise InvalidCode(f'Search directory {d} is not an absolute path.')
+ return [str(s) for s in search_dirs]
+
+class FeatureOptionHolder(ObjectHolder[coredata.UserFeatureOption]):
+ def __init__(self, option: coredata.UserFeatureOption, interpreter: 'Interpreter'):
+ super().__init__(option, interpreter)
+ if option and option.is_auto():
+ # TODO: we need to cast here because options is not a TypedDict
+ auto = T.cast('coredata.UserFeatureOption', self.env.coredata.options[OptionKey('auto_features')])
+ self.held_object = copy.copy(auto)
+ self.held_object.name = option.name
+ self.methods.update({'enabled': self.enabled_method,
+ 'disabled': self.disabled_method,
+ 'allowed': self.allowed_method,
+ 'auto': self.auto_method,
+ 'require': self.require_method,
+ 'disable_auto_if': self.disable_auto_if_method,
+ })
+
+ @property
+ def value(self) -> str:
+ return 'disabled' if not self.held_object else self.held_object.value
+
+ def as_disabled(self) -> coredata.UserFeatureOption:
+ disabled = copy.deepcopy(self.held_object)
+ disabled.value = 'disabled'
+ return disabled
+
+ @noPosargs
+ @noKwargs
+ def enabled_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ return self.value == 'enabled'
+
+ @noPosargs
+ @noKwargs
+ def disabled_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ return self.value == 'disabled'
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('feature_option.allowed()', '0.59.0')
+ def allowed_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ return self.value != 'disabled'
+
+ @noPosargs
+ @noKwargs
+ def auto_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ return self.value == 'auto'
+
+ @FeatureNew('feature_option.require()', '0.59.0')
+ @typed_pos_args('feature_option.require', bool)
+ @typed_kwargs(
+ 'feature_option.require',
+ KwargInfo('error_message', (str, NoneType))
+ )
+ def require_method(self, args: T.Tuple[bool], kwargs: 'kwargs.FeatureOptionRequire') -> coredata.UserFeatureOption:
+ if args[0]:
+ return copy.deepcopy(self.held_object)
+
+ if self.value == 'enabled':
+ err_msg = f'Feature {self.held_object.name} cannot be enabled'
+ if kwargs['error_message']:
+ err_msg += f': {kwargs["error_message"]}'
+ raise InterpreterException(err_msg)
+ return self.as_disabled()
+
+ @FeatureNew('feature_option.disable_auto_if()', '0.59.0')
+ @noKwargs
+ @typed_pos_args('feature_option.disable_auto_if', bool)
+ def disable_auto_if_method(self, args: T.Tuple[bool], kwargs: TYPE_kwargs) -> coredata.UserFeatureOption:
+ return copy.deepcopy(self.held_object) if self.value != 'auto' or not args[0] else self.as_disabled()
+
+
+class RunProcess(MesonInterpreterObject):
+
+ def __init__(self,
+ cmd: ExternalProgram,
+ args: T.List[str],
+ env: build.EnvironmentVariables,
+ source_dir: str,
+ build_dir: str,
+ subdir: str,
+ mesonintrospect: T.List[str],
+ in_builddir: bool = False,
+ check: bool = False,
+ capture: bool = True) -> None:
+ super().__init__()
+ if not isinstance(cmd, ExternalProgram):
+ raise AssertionError('BUG: RunProcess must be passed an ExternalProgram')
+ self.capture = capture
+ self.returncode, self.stdout, self.stderr = self.run_command(cmd, args, env, source_dir, build_dir, subdir, mesonintrospect, in_builddir, check)
+ self.methods.update({'returncode': self.returncode_method,
+ 'stdout': self.stdout_method,
+ 'stderr': self.stderr_method,
+ })
+
+ def run_command(self,
+ cmd: ExternalProgram,
+ args: T.List[str],
+ env: build.EnvironmentVariables,
+ source_dir: str,
+ build_dir: str,
+ subdir: str,
+ mesonintrospect: T.List[str],
+ in_builddir: bool,
+ check: bool = False) -> T.Tuple[int, str, str]:
+ command_array = cmd.get_command() + args
+ menv = {'MESON_SOURCE_ROOT': source_dir,
+ 'MESON_BUILD_ROOT': build_dir,
+ 'MESON_SUBDIR': subdir,
+ 'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in mesonintrospect]),
+ }
+ if in_builddir:
+ cwd = os.path.join(build_dir, subdir)
+ else:
+ cwd = os.path.join(source_dir, subdir)
+ child_env = os.environ.copy()
+ child_env.update(menv)
+ child_env = env.get_env(child_env)
+ stdout = subprocess.PIPE if self.capture else subprocess.DEVNULL
+ mlog.debug('Running command:', mesonlib.join_args(command_array))
+ try:
+ p, o, e = Popen_safe(command_array, stdout=stdout, env=child_env, cwd=cwd)
+ if self.capture:
+ mlog.debug('--- stdout ---')
+ mlog.debug(o)
+ else:
+ o = ''
+ mlog.debug('--- stdout disabled ---')
+ mlog.debug('--- stderr ---')
+ mlog.debug(e)
+ mlog.debug('')
+
+ if check and p.returncode != 0:
+ raise InterpreterException('Command `{}` failed with status {}.'.format(mesonlib.join_args(command_array), p.returncode))
+
+ return p.returncode, o, e
+ except FileNotFoundError:
+ raise InterpreterException('Could not execute command `%s`.' % mesonlib.join_args(command_array))
+
+ @noPosargs
+ @noKwargs
+ def returncode_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> int:
+ return self.returncode
+
+ @noPosargs
+ @noKwargs
+ def stdout_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.stdout
+
+ @noPosargs
+ @noKwargs
+ def stderr_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.stderr
+
+class EnvironmentVariablesHolder(ObjectHolder[build.EnvironmentVariables], MutableInterpreterObject):
+
+ def __init__(self, obj: build.EnvironmentVariables, interpreter: 'Interpreter'):
+ super().__init__(obj, interpreter)
+ self.methods.update({'set': self.set_method,
+ 'append': self.append_method,
+ 'prepend': self.prepend_method,
+ })
+
+ def __repr__(self) -> str:
+ repr_str = "<{0}: {1}>"
+ return repr_str.format(self.__class__.__name__, self.held_object.envvars)
+
+ def __deepcopy__(self, memo: T.Dict[str, object]) -> 'EnvironmentVariablesHolder':
+ # Avoid trying to copy the interpreter
+ return EnvironmentVariablesHolder(copy.deepcopy(self.held_object), self.interpreter)
+
+ def warn_if_has_name(self, name: str) -> None:
+ # Multiple append/prepend operations was not supported until 0.58.0.
+ if self.held_object.has_name(name):
+ m = f'Overriding previous value of environment variable {name!r} with a new one'
+ FeatureNew(m, '0.58.0').use(self.subproject, self.current_node)
+
+ @typed_pos_args('environment.set', str, varargs=str, min_varargs=1)
+ @typed_kwargs('environment.set', ENV_SEPARATOR_KW)
+ def set_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None:
+ name, values = args
+ self.held_object.set(name, values, kwargs['separator'])
+
+ @typed_pos_args('environment.append', str, varargs=str, min_varargs=1)
+ @typed_kwargs('environment.append', ENV_SEPARATOR_KW)
+ def append_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None:
+ name, values = args
+ self.warn_if_has_name(name)
+ self.held_object.append(name, values, kwargs['separator'])
+
+ @typed_pos_args('environment.prepend', str, varargs=str, min_varargs=1)
+ @typed_kwargs('environment.prepend', ENV_SEPARATOR_KW)
+ def prepend_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None:
+ name, values = args
+ self.warn_if_has_name(name)
+ self.held_object.prepend(name, values, kwargs['separator'])
+
+
+_CONF_DATA_SET_KWS: KwargInfo[T.Optional[str]] = KwargInfo('description', (str, NoneType))
+
+
+class ConfigurationDataHolder(ObjectHolder[build.ConfigurationData], MutableInterpreterObject):
+
+ def __init__(self, obj: build.ConfigurationData, interpreter: 'Interpreter'):
+ super().__init__(obj, interpreter)
+ self.methods.update({'set': self.set_method,
+ 'set10': self.set10_method,
+ 'set_quoted': self.set_quoted_method,
+ 'has': self.has_method,
+ 'get': self.get_method,
+ 'keys': self.keys_method,
+ 'get_unquoted': self.get_unquoted_method,
+ 'merge_from': self.merge_from_method,
+ })
+
+ def __deepcopy__(self, memo: T.Dict) -> 'ConfigurationDataHolder':
+ return ConfigurationDataHolder(copy.deepcopy(self.held_object), self.interpreter)
+
+ def is_used(self) -> bool:
+ return self.held_object.used
+
+ def __check_used(self) -> None:
+ if self.is_used():
+ raise InterpreterException("Can not set values on configuration object that has been used.")
+
+ @typed_pos_args('configuration_data.set', str, (str, int, bool))
+ @typed_kwargs('configuration_data.set', _CONF_DATA_SET_KWS)
+ def set_method(self, args: T.Tuple[str, T.Union[str, int, bool]], kwargs: 'kwargs.ConfigurationDataSet') -> None:
+ self.__check_used()
+ self.held_object.values[args[0]] = (args[1], kwargs['description'])
+
+ @typed_pos_args('configuration_data.set_quoted', str, str)
+ @typed_kwargs('configuration_data.set_quoted', _CONF_DATA_SET_KWS)
+ def set_quoted_method(self, args: T.Tuple[str, str], kwargs: 'kwargs.ConfigurationDataSet') -> None:
+ self.__check_used()
+ escaped_val = '\\"'.join(args[1].split('"'))
+ self.held_object.values[args[0]] = (f'"{escaped_val}"', kwargs['description'])
+
+ @typed_pos_args('configuration_data.set10', str, (int, bool))
+ @typed_kwargs('configuration_data.set10', _CONF_DATA_SET_KWS)
+ def set10_method(self, args: T.Tuple[str, T.Union[int, bool]], kwargs: 'kwargs.ConfigurationDataSet') -> None:
+ self.__check_used()
+ # bool is a subclass of int, so we need to check for bool explicitly.
+ # We already have typed_pos_args checking that this is either a bool or
+ # an int.
+ if not isinstance(args[1], bool):
+ mlog.deprecation('configuration_data.set10 with number. the `set10` '
+ 'method should only be used with booleans',
+ location=self.interpreter.current_node)
+ if args[1] < 0:
+ mlog.warning('Passing a number that is less than 0 may not have the intended result, '
+ 'as meson will treat all non-zero values as true.',
+ location=self.interpreter.current_node)
+ self.held_object.values[args[0]] = (int(args[1]), kwargs['description'])
+
+ @typed_pos_args('configuration_data.has', (str, int, bool))
+ @noKwargs
+ def has_method(self, args: T.Tuple[T.Union[str, int, bool]], kwargs: TYPE_kwargs) -> bool:
+ return args[0] in self.held_object.values
+
+ @FeatureNew('configuration_data.get()', '0.38.0')
+ @typed_pos_args('configuration_data.get', str, optargs=[(str, int, bool)])
+ @noKwargs
+ def get_method(self, args: T.Tuple[str, T.Optional[T.Union[str, int, bool]]],
+ kwargs: TYPE_kwargs) -> T.Union[str, int, bool]:
+ name = args[0]
+ if name in self.held_object:
+ return self.held_object.get(name)[0]
+ elif args[1] is not None:
+ return args[1]
+ raise InterpreterException(f'Entry {name} not in configuration data.')
+
+ @FeatureNew('configuration_data.get_unquoted()', '0.44.0')
+ @typed_pos_args('configuration_data.get_unquoted', str, optargs=[(str, int, bool)])
+ @noKwargs
+ def get_unquoted_method(self, args: T.Tuple[str, T.Optional[T.Union[str, int, bool]]],
+ kwargs: TYPE_kwargs) -> T.Union[str, int, bool]:
+ name = args[0]
+ if name in self.held_object:
+ val = self.held_object.get(name)[0]
+ elif args[1] is not None:
+ val = args[1]
+ else:
+ raise InterpreterException(f'Entry {name} not in configuration data.')
+ if isinstance(val, str) and val[0] == '"' and val[-1] == '"':
+ return val[1:-1]
+ return val
+
+ def get(self, name: str) -> T.Tuple[T.Union[str, int, bool], T.Optional[str]]:
+ return self.held_object.values[name]
+
+ @FeatureNew('configuration_data.keys()', '0.57.0')
+ @noPosargs
+ @noKwargs
+ def keys_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> T.List[str]:
+ return sorted(self.keys())
+
+ def keys(self) -> T.List[str]:
+ return list(self.held_object.values.keys())
+
+ @typed_pos_args('configuration_data.merge_from', build.ConfigurationData)
+ @noKwargs
+ def merge_from_method(self, args: T.Tuple[build.ConfigurationData], kwargs: TYPE_kwargs) -> None:
+ from_object = args[0]
+ self.held_object.values.update(from_object.values)
+
+
+_PARTIAL_DEP_KWARGS = [
+ KwargInfo('compile_args', bool, default=False),
+ KwargInfo('link_args', bool, default=False),
+ KwargInfo('links', bool, default=False),
+ KwargInfo('includes', bool, default=False),
+ KwargInfo('sources', bool, default=False),
+]
+
+class DependencyHolder(ObjectHolder[Dependency]):
+ def __init__(self, dep: Dependency, interpreter: 'Interpreter'):
+ super().__init__(dep, interpreter)
+ self.methods.update({'found': self.found_method,
+ 'type_name': self.type_name_method,
+ 'version': self.version_method,
+ 'name': self.name_method,
+ 'get_pkgconfig_variable': self.pkgconfig_method,
+ 'get_configtool_variable': self.configtool_method,
+ 'get_variable': self.variable_method,
+ 'partial_dependency': self.partial_dependency_method,
+ 'include_type': self.include_type_method,
+ 'as_system': self.as_system_method,
+ 'as_link_whole': self.as_link_whole_method,
+ })
+
+ def found(self) -> bool:
+ return self.found_method([], {})
+
+ @noPosargs
+ @noKwargs
+ def type_name_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.type_name
+
+ @noPosargs
+ @noKwargs
+ def found_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ if self.held_object.type_name == 'internal':
+ return True
+ return self.held_object.found()
+
+ @noPosargs
+ @noKwargs
+ def version_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.get_version()
+
+ @noPosargs
+ @noKwargs
+ def name_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.get_name()
+
+ @FeatureDeprecated('dependency.get_pkgconfig_variable', '0.56.0',
+ 'use dependency.get_variable(pkgconfig : ...) instead')
+ @typed_pos_args('dependency.get_pkgconfig_variable', str)
+ @typed_kwargs(
+ 'dependency.get_pkgconfig_variable',
+ KwargInfo('default', (str, NoneType)),
+ KwargInfo(
+ 'define_variable',
+ ContainerTypeInfo(list, str, pairs=True),
+ default=[],
+ listify=True,
+ validator=lambda x: 'must be of length 2 or empty' if len(x) not in {0, 2} else None,
+ ),
+ )
+ def pkgconfig_method(self, args: T.Tuple[str], kwargs: 'kwargs.DependencyPkgConfigVar') -> str:
+ return self.held_object.get_pkgconfig_variable(args[0], **kwargs)
+
+ @FeatureNew('dependency.get_configtool_variable', '0.44.0')
+ @FeatureDeprecated('dependency.get_configtool_variable', '0.56.0',
+ 'use dependency.get_variable(configtool : ...) instead')
+ @noKwargs
+ @typed_pos_args('dependency.get_config_tool_variable', str)
+ def configtool_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.get_configtool_variable(args[0])
+
+ @FeatureNew('dependency.partial_dependency', '0.46.0')
+ @noPosargs
+ @typed_kwargs('dependency.partial_dependency', *_PARTIAL_DEP_KWARGS)
+ def partial_dependency_method(self, args: T.List[TYPE_nvar], kwargs: 'kwargs.DependencyMethodPartialDependency') -> Dependency:
+ pdep = self.held_object.get_partial_dependency(**kwargs)
+ return pdep
+
+ @FeatureNew('dependency.get_variable', '0.51.0')
+ @typed_pos_args('dependency.get_variable', optargs=[str])
+ @typed_kwargs(
+ 'dependency.get_variable',
+ KwargInfo('cmake', (str, NoneType)),
+ KwargInfo('pkgconfig', (str, NoneType)),
+ KwargInfo('configtool', (str, NoneType)),
+ KwargInfo('internal', (str, NoneType), since='0.54.0'),
+ KwargInfo('default_value', (str, NoneType)),
+ KwargInfo('pkgconfig_define', ContainerTypeInfo(list, str, pairs=True), default=[], listify=True),
+ )
+ def variable_method(self, args: T.Tuple[T.Optional[str]], kwargs: 'kwargs.DependencyGetVariable') -> str:
+ default_varname = args[0]
+ if default_varname is not None:
+ FeatureNew('Positional argument to dependency.get_variable()', '0.58.0').use(self.subproject, self.current_node)
+ return self.held_object.get_variable(
+ cmake=kwargs['cmake'] or default_varname,
+ pkgconfig=kwargs['pkgconfig'] or default_varname,
+ configtool=kwargs['configtool'] or default_varname,
+ internal=kwargs['internal'] or default_varname,
+ default_value=kwargs['default_value'],
+ pkgconfig_define=kwargs['pkgconfig_define'],
+ )
+
+ @FeatureNew('dependency.include_type', '0.52.0')
+ @noPosargs
+ @noKwargs
+ def include_type_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.get_include_type()
+
+ @FeatureNew('dependency.as_system', '0.52.0')
+ @noKwargs
+ @typed_pos_args('dependency.as_system', optargs=[str])
+ def as_system_method(self, args: T.Tuple[T.Optional[str]], kwargs: TYPE_kwargs) -> Dependency:
+ return self.held_object.generate_system_dependency(args[0] or 'system')
+
+ @FeatureNew('dependency.as_link_whole', '0.56.0')
+ @noKwargs
+ @noPosargs
+ def as_link_whole_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> Dependency:
+ if not isinstance(self.held_object, InternalDependency):
+ raise InterpreterException('as_link_whole method is only supported on declare_dependency() objects')
+ new_dep = self.held_object.generate_link_whole_dependency()
+ return new_dep
+
+class ExternalProgramHolder(ObjectHolder[ExternalProgram]):
+ def __init__(self, ep: ExternalProgram, interpreter: 'Interpreter') -> None:
+ super().__init__(ep, interpreter)
+ self.methods.update({'found': self.found_method,
+ 'path': self.path_method,
+ 'version': self.version_method,
+ 'full_path': self.full_path_method})
+
+ @noPosargs
+ @noKwargs
+ def found_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ return self.found()
+
+ @noPosargs
+ @noKwargs
+ @FeatureDeprecated('ExternalProgram.path', '0.55.0',
+ 'use ExternalProgram.full_path() instead')
+ def path_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self._full_path()
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('ExternalProgram.full_path', '0.55.0')
+ def full_path_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self._full_path()
+
+ def _full_path(self) -> str:
+ if not self.found():
+ raise InterpreterException('Unable to get the path of a not-found external program')
+ path = self.held_object.get_path()
+ assert path is not None
+ return path
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('ExternalProgram.version', '0.62.0')
+ def version_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ if not self.found():
+ raise InterpreterException('Unable to get the version of a not-found external program')
+ try:
+ return self.held_object.get_version(self.interpreter)
+ except mesonlib.MesonException:
+ return 'unknown'
+
+ def found(self) -> bool:
+ return self.held_object.found()
+
+class ExternalLibraryHolder(ObjectHolder[ExternalLibrary]):
+ def __init__(self, el: ExternalLibrary, interpreter: 'Interpreter'):
+ super().__init__(el, interpreter)
+ self.methods.update({'found': self.found_method,
+ 'type_name': self.type_name_method,
+ 'partial_dependency': self.partial_dependency_method,
+ })
+
+ @noPosargs
+ @noKwargs
+ def type_name_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.type_name
+
+ @noPosargs
+ @noKwargs
+ def found_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ return self.held_object.found()
+
+ @FeatureNew('dependency.partial_dependency', '0.46.0')
+ @noPosargs
+ @typed_kwargs('dependency.partial_dependency', *_PARTIAL_DEP_KWARGS)
+ def partial_dependency_method(self, args: T.List[TYPE_nvar], kwargs: 'kwargs.DependencyMethodPartialDependency') -> Dependency:
+ pdep = self.held_object.get_partial_dependency(**kwargs)
+ return pdep
+
+# A machine that's statically known from the cross file
+class MachineHolder(ObjectHolder['MachineInfo']):
+ def __init__(self, machine_info: 'MachineInfo', interpreter: 'Interpreter'):
+ super().__init__(machine_info, interpreter)
+ self.methods.update({'system': self.system_method,
+ 'cpu': self.cpu_method,
+ 'cpu_family': self.cpu_family_method,
+ 'endian': self.endian_method,
+ })
+
+ @noPosargs
+ @noKwargs
+ def cpu_family_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.cpu_family
+
+ @noPosargs
+ @noKwargs
+ def cpu_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.cpu
+
+ @noPosargs
+ @noKwargs
+ def system_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.system
+
+ @noPosargs
+ @noKwargs
+ def endian_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.endian
+
+class IncludeDirsHolder(ObjectHolder[build.IncludeDirs]):
+ pass
+
+class FileHolder(ObjectHolder[mesonlib.File]):
+ pass
+
+class HeadersHolder(ObjectHolder[build.Headers]):
+ pass
+
+class DataHolder(ObjectHolder[build.Data]):
+ pass
+
+class SymlinkDataHolder(ObjectHolder[build.SymlinkData]):
+ pass
+
+class InstallDirHolder(ObjectHolder[build.InstallDir]):
+ pass
+
+class ManHolder(ObjectHolder[build.Man]):
+ pass
+
+class EmptyDirHolder(ObjectHolder[build.EmptyDir]):
+ pass
+
+class GeneratedObjectsHolder(ObjectHolder[build.ExtractedObjects]):
+ pass
+
+class Test(MesonInterpreterObject):
+ def __init__(self, name: str, project: str, suite: T.List[str],
+ exe: T.Union[ExternalProgram, build.Executable, build.CustomTarget],
+ depends: T.List[T.Union[build.CustomTarget, build.BuildTarget]],
+ is_parallel: bool,
+ cmd_args: T.List[T.Union[str, mesonlib.File, build.Target]],
+ env: build.EnvironmentVariables,
+ should_fail: bool, timeout: int, workdir: T.Optional[str], protocol: str,
+ priority: int, verbose: bool):
+ super().__init__()
+ self.name = name
+ self.suite = listify(suite)
+ self.project_name = project
+ self.exe = exe
+ self.depends = depends
+ self.is_parallel = is_parallel
+ self.cmd_args = cmd_args
+ self.env = env
+ self.should_fail = should_fail
+ self.timeout = timeout
+ self.workdir = workdir
+ self.protocol = TestProtocol.from_str(protocol)
+ self.priority = priority
+ self.verbose = verbose
+
+ def get_exe(self) -> T.Union[ExternalProgram, build.Executable, build.CustomTarget]:
+ return self.exe
+
+ def get_name(self) -> str:
+ return self.name
+
+class NullSubprojectInterpreter(HoldableObject):
+ pass
+
+# TODO: This should really be an `ObjectHolder`, but the additional stuff in this
+# class prevents this. Thus, this class should be split into a pure
+# `ObjectHolder` and a class specifically for storing in `Interpreter`.
+class SubprojectHolder(MesonInterpreterObject):
+
+ def __init__(self, subinterpreter: T.Union['Interpreter', NullSubprojectInterpreter],
+ subdir: str,
+ warnings: int = 0,
+ disabled_feature: T.Optional[str] = None,
+ exception: T.Optional[Exception] = None) -> None:
+ super().__init__()
+ self.held_object = subinterpreter
+ self.warnings = warnings
+ self.disabled_feature = disabled_feature
+ self.exception = exception
+ self.subdir = PurePath(subdir).as_posix()
+ self.cm_interpreter: T.Optional[CMakeInterpreter] = None
+ self.methods.update({'get_variable': self.get_variable_method,
+ 'found': self.found_method,
+ })
+
+ @noPosargs
+ @noKwargs
+ def found_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ return self.found()
+
+ def found(self) -> bool:
+ return not isinstance(self.held_object, NullSubprojectInterpreter)
+
+ @noKwargs
+ @noArgsFlattening
+ @unholder_return
+ def get_variable_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> T.Union[TYPE_var, InterpreterObject]:
+ if len(args) < 1 or len(args) > 2:
+ raise InterpreterException('Get_variable takes one or two arguments.')
+ if isinstance(self.held_object, NullSubprojectInterpreter): # == not self.found()
+ raise InterpreterException(f'Subproject "{self.subdir}" disabled can\'t get_variable on it.')
+ varname = args[0]
+ if not isinstance(varname, str):
+ raise InterpreterException('Get_variable first argument must be a string.')
+ try:
+ return self.held_object.variables[varname]
+ except KeyError:
+ pass
+
+ if len(args) == 2:
+ return self.held_object._holderify(args[1])
+
+ raise InvalidArguments(f'Requested variable "{varname}" not found.')
+
+class ModuleObjectHolder(ObjectHolder[ModuleObject]):
+ def method_call(self, method_name: str, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> TYPE_var:
+ modobj = self.held_object
+ method = modobj.methods.get(method_name)
+ if not method:
+ raise InvalidCode(f'Unknown method {method_name!r} in object.')
+ if not getattr(method, 'no-args-flattening', False):
+ args = flatten(args)
+ if not getattr(method, 'no-second-level-holder-flattening', False):
+ args, kwargs = resolve_second_level_holders(args, kwargs)
+ state = ModuleState(self.interpreter)
+ # Many modules do for example self.interpreter.find_program_impl(),
+ # so we have to ensure they use the current interpreter and not the one
+ # that first imported that module, otherwise it will use outdated
+ # overrides.
+ if isinstance(modobj, ExtensionModule):
+ modobj.interpreter = self.interpreter
+ ret = method(state, args, kwargs)
+ if isinstance(ret, ModuleReturnValue):
+ self.interpreter.process_new_values(ret.new_objects)
+ ret = ret.return_value
+ return ret
+
+class MutableModuleObjectHolder(ModuleObjectHolder, MutableInterpreterObject):
+ def __deepcopy__(self, memo: T.Dict[int, T.Any]) -> 'MutableModuleObjectHolder':
+ # Deepcopy only held object, not interpreter
+ modobj = copy.deepcopy(self.held_object, memo)
+ return MutableModuleObjectHolder(modobj, self.interpreter)
+
+
+_BuildTarget = T.TypeVar('_BuildTarget', bound=T.Union[build.BuildTarget, build.BothLibraries])
+
+class BuildTargetHolder(ObjectHolder[_BuildTarget]):
+ def __init__(self, target: _BuildTarget, interp: 'Interpreter'):
+ super().__init__(target, interp)
+ self.methods.update({'extract_objects': self.extract_objects_method,
+ 'extract_all_objects': self.extract_all_objects_method,
+ 'name': self.name_method,
+ 'get_id': self.get_id_method,
+ 'outdir': self.outdir_method,
+ 'full_path': self.full_path_method,
+ 'path': self.path_method,
+ 'found': self.found_method,
+ 'private_dir_include': self.private_dir_include_method,
+ })
+
+ def __repr__(self) -> str:
+ r = '<{} {}: {}>'
+ h = self.held_object
+ assert isinstance(h, build.BuildTarget)
+ return r.format(self.__class__.__name__, h.get_id(), h.filename)
+
+ @property
+ def _target_object(self) -> build.BuildTarget:
+ if isinstance(self.held_object, build.BothLibraries):
+ return self.held_object.get_default_object()
+ assert isinstance(self.held_object, build.BuildTarget)
+ return self.held_object
+
+ def is_cross(self) -> bool:
+ return not self._target_object.environment.machines.matches_build_machine(self._target_object.for_machine)
+
+ @noPosargs
+ @noKwargs
+ def found_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ if not (isinstance(self.held_object, build.Executable) and self.held_object.was_returned_by_find_program):
+ FeatureNew.single_use('BuildTarget.found', '0.59.0', subproject=self.held_object.subproject)
+ return True
+
+ @noPosargs
+ @noKwargs
+ def private_dir_include_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> build.IncludeDirs:
+ return build.IncludeDirs('', [], False, [self.interpreter.backend.get_target_private_dir(self._target_object)])
+
+ @noPosargs
+ @noKwargs
+ def full_path_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.interpreter.backend.get_target_filename_abs(self._target_object)
+
+ @noPosargs
+ @noKwargs
+ @FeatureDeprecated('BuildTarget.path', '0.55.0', 'Use BuildTarget.full_path instead')
+ def path_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.interpreter.backend.get_target_filename_abs(self._target_object)
+
+ @noPosargs
+ @noKwargs
+ def outdir_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.interpreter.backend.get_target_dir(self._target_object)
+
+ @noKwargs
+ @typed_pos_args('extract_objects', varargs=(mesonlib.File, str, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList))
+ def extract_objects_method(self, args: T.Tuple[T.List[T.Union[mesonlib.FileOrString, 'build.GeneratedTypes']]], kwargs: TYPE_nkwargs) -> build.ExtractedObjects:
+ return self._target_object.extract_objects(args[0])
+
+ @noPosargs
+ @typed_kwargs(
+ 'extract_all_objects',
+ KwargInfo(
+ 'recursive', bool, default=False, since='0.46.0',
+ not_set_warning=textwrap.dedent('''\
+ extract_all_objects called without setting recursive
+ keyword argument. Meson currently defaults to
+ non-recursive to maintain backward compatibility but
+ the default will be changed in the future.
+ ''')
+ )
+ )
+ def extract_all_objects_method(self, args: T.List[TYPE_nvar], kwargs: 'kwargs.BuildTargeMethodExtractAllObjects') -> build.ExtractedObjects:
+ return self._target_object.extract_all_objects(kwargs['recursive'])
+
+ @noPosargs
+ @noKwargs
+ def get_id_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self._target_object.get_id()
+
+ @FeatureNew('name', '0.54.0')
+ @noPosargs
+ @noKwargs
+ def name_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self._target_object.name
+
+class ExecutableHolder(BuildTargetHolder[build.Executable]):
+ pass
+
+class StaticLibraryHolder(BuildTargetHolder[build.StaticLibrary]):
+ pass
+
+class SharedLibraryHolder(BuildTargetHolder[build.SharedLibrary]):
+ pass
+
+class BothLibrariesHolder(BuildTargetHolder[build.BothLibraries]):
+ def __init__(self, libs: build.BothLibraries, interp: 'Interpreter'):
+ # FIXME: This build target always represents the shared library, but
+ # that should be configurable.
+ super().__init__(libs, interp)
+ self.methods.update({'get_shared_lib': self.get_shared_lib_method,
+ 'get_static_lib': self.get_static_lib_method,
+ })
+
+ def __repr__(self) -> str:
+ r = '<{} {}: {}, {}: {}>'
+ h1 = self.held_object.shared
+ h2 = self.held_object.static
+ return r.format(self.__class__.__name__, h1.get_id(), h1.filename, h2.get_id(), h2.filename)
+
+ @noPosargs
+ @noKwargs
+ def get_shared_lib_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> build.SharedLibrary:
+ return self.held_object.shared
+
+ @noPosargs
+ @noKwargs
+ def get_static_lib_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> build.StaticLibrary:
+ return self.held_object.static
+
+class SharedModuleHolder(BuildTargetHolder[build.SharedModule]):
+ pass
+
+class JarHolder(BuildTargetHolder[build.Jar]):
+ pass
+
+class CustomTargetIndexHolder(ObjectHolder[build.CustomTargetIndex]):
+ def __init__(self, target: build.CustomTargetIndex, interp: 'Interpreter'):
+ super().__init__(target, interp)
+ self.methods.update({'full_path': self.full_path_method,
+ })
+
+ @FeatureNew('custom_target[i].full_path', '0.54.0')
+ @noPosargs
+ @noKwargs
+ def full_path_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ assert self.interpreter.backend is not None
+ return self.interpreter.backend.get_target_filename_abs(self.held_object)
+
+class CustomTargetHolder(ObjectHolder[build.CustomTarget]):
+ def __init__(self, target: 'build.CustomTarget', interp: 'Interpreter'):
+ super().__init__(target, interp)
+ self.methods.update({'full_path': self.full_path_method,
+ 'to_list': self.to_list_method,
+ })
+
+ self.operators.update({
+ MesonOperator.INDEX: self.op_index,
+ })
+
+ def __repr__(self) -> str:
+ r = '<{} {}: {}>'
+ h = self.held_object
+ return r.format(self.__class__.__name__, h.get_id(), h.command)
+
+ @noPosargs
+ @noKwargs
+ def full_path_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.interpreter.backend.get_target_filename_abs(self.held_object)
+
+ @FeatureNew('custom_target.to_list', '0.54.0')
+ @noPosargs
+ @noKwargs
+ def to_list_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> T.List[build.CustomTargetIndex]:
+ result = []
+ for i in self.held_object:
+ result.append(i)
+ return result
+
+ @noKwargs
+ @typed_operator(MesonOperator.INDEX, int)
+ def op_index(self, other: int) -> build.CustomTargetIndex:
+ try:
+ return self.held_object[other]
+ except IndexError:
+ raise InvalidArguments(f'Index {other} out of bounds of custom target {self.held_object.name} output of size {len(self.held_object)}.')
+
+class RunTargetHolder(ObjectHolder[build.RunTarget]):
+ pass
+
+class AliasTargetHolder(ObjectHolder[build.AliasTarget]):
+ pass
+
+class GeneratedListHolder(ObjectHolder[build.GeneratedList]):
+ pass
+
+class GeneratorHolder(ObjectHolder[build.Generator]):
+ def __init__(self, gen: build.Generator, interpreter: 'Interpreter'):
+ super().__init__(gen, interpreter)
+ self.methods.update({'process': self.process_method})
+
+ @typed_pos_args('generator.process', min_varargs=1, varargs=(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList))
+ @typed_kwargs(
+ 'generator.process',
+ KwargInfo('preserve_path_from', (str, NoneType), since='0.45.0'),
+ KwargInfo('extra_args', ContainerTypeInfo(list, str), listify=True, default=[]),
+ )
+ def process_method(self,
+ args: T.Tuple[T.List[T.Union[str, mesonlib.File, 'build.GeneratedTypes']]],
+ kwargs: 'kwargs.GeneratorProcess') -> build.GeneratedList:
+ preserve_path_from = kwargs['preserve_path_from']
+ if preserve_path_from is not None:
+ preserve_path_from = os.path.normpath(preserve_path_from)
+ if not os.path.isabs(preserve_path_from):
+ # This is a bit of a hack. Fix properly before merging.
+ raise InvalidArguments('Preserve_path_from must be an absolute path for now. Sorry.')
+
+ if any(isinstance(a, (build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)) for a in args[0]):
+ FeatureNew.single_use(
+ 'Calling generator.process with CustomTarget or Index of CustomTarget.',
+ '0.57.0', self.interpreter.subproject)
+
+ gl = self.held_object.process_files(args[0], self.interpreter,
+ preserve_path_from, extra_args=kwargs['extra_args'])
+
+ return gl
+
+
+class StructuredSourcesHolder(ObjectHolder[build.StructuredSources]):
+
+ def __init__(self, sources: build.StructuredSources, interp: 'Interpreter'):
+ super().__init__(sources, interp)
diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py
new file mode 100644
index 0000000..fb02374
--- /dev/null
+++ b/mesonbuild/interpreter/kwargs.py
@@ -0,0 +1,310 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright © 2021 The Meson Developers
+# Copyright © 2021 Intel Corporation
+from __future__ import annotations
+
+"""Keyword Argument type annotations."""
+
+import typing as T
+
+from typing_extensions import TypedDict, Literal, Protocol
+
+from .. import build
+from .. import coredata
+from ..compilers import Compiler
+from ..mesonlib import MachineChoice, File, FileMode, FileOrString
+from ..modules.cmake import CMakeSubprojectOptions
+from ..programs import ExternalProgram
+
+
+class FuncAddProjectArgs(TypedDict):
+
+ """Keyword Arguments for the add_*_arguments family of arguments.
+
+ including `add_global_arguments`, `add_project_arguments`, and their
+ link variants
+
+ Because of the use of a convertor function, we get the native keyword as
+ a MachineChoice instance already.
+ """
+
+ native: MachineChoice
+ language: T.List[str]
+
+
+class BaseTest(TypedDict):
+
+ """Shared base for the Rust module."""
+
+ args: T.List[T.Union[str, File, build.Target]]
+ should_fail: bool
+ timeout: int
+ workdir: T.Optional[str]
+ depends: T.List[T.Union[build.CustomTarget, build.BuildTarget]]
+ priority: int
+ env: build.EnvironmentVariables
+ suite: T.List[str]
+
+
+class FuncBenchmark(BaseTest):
+
+ """Keyword Arguments shared between `test` and `benchmark`."""
+
+ protocol: Literal['exitcode', 'tap', 'gtest', 'rust']
+
+
+class FuncTest(FuncBenchmark):
+
+ """Keyword Arguments for `test`
+
+ `test` only adds the `is_prallel` argument over benchmark, so inherintance
+ is helpful here.
+ """
+
+ is_parallel: bool
+
+
+class ExtractRequired(TypedDict):
+
+ """Keyword Arguments consumed by the `extract_required_kwargs` function.
+
+ Any function that uses the `required` keyword argument which accepts either
+ a boolean or a feature option should inherit it's arguments from this class.
+ """
+
+ required: T.Union[bool, coredata.UserFeatureOption]
+
+
+class ExtractSearchDirs(TypedDict):
+
+ """Keyword arguments consumed by the `extract_search_dirs` function.
+
+ See the not in `ExtractRequired`
+ """
+
+ dirs: T.List[str]
+
+
+class FuncGenerator(TypedDict):
+
+ """Keyword rguments for the generator function."""
+
+ arguments: T.List[str]
+ output: T.List[str]
+ depfile: T.Optional[str]
+ capture: bool
+ depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]]
+
+
+class GeneratorProcess(TypedDict):
+
+ """Keyword Arguments for generator.process."""
+
+ preserve_path_from: T.Optional[str]
+ extra_args: T.List[str]
+
+class DependencyMethodPartialDependency(TypedDict):
+
+ """ Keyword Arguments for the dep.partial_dependency methods """
+
+ compile_args: bool
+ link_args: bool
+ links: bool
+ includes: bool
+ sources: bool
+
+class BuildTargeMethodExtractAllObjects(TypedDict):
+ recursive: bool
+
+class FuncInstallSubdir(TypedDict):
+
+ install_dir: str
+ strip_directory: bool
+ exclude_files: T.List[str]
+ exclude_directories: T.List[str]
+ install_mode: FileMode
+
+
+class FuncInstallData(TypedDict):
+
+ install_dir: str
+ sources: T.List[FileOrString]
+ rename: T.List[str]
+ install_mode: FileMode
+
+
+class FuncInstallHeaders(TypedDict):
+
+ install_dir: T.Optional[str]
+ install_mode: FileMode
+ subdir: T.Optional[str]
+
+
+class FuncInstallMan(TypedDict):
+
+ install_dir: T.Optional[str]
+ install_mode: FileMode
+ locale: T.Optional[str]
+
+
+class FuncImportModule(ExtractRequired):
+
+ disabler: bool
+
+
+class FuncIncludeDirectories(TypedDict):
+
+ is_system: bool
+
+class FuncAddLanguages(ExtractRequired):
+
+ native: T.Optional[bool]
+
+class RunTarget(TypedDict):
+
+ command: T.List[T.Union[str, build.BuildTarget, build.CustomTarget, ExternalProgram, File]]
+ depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]]
+ env: build.EnvironmentVariables
+
+
+class CustomTarget(TypedDict):
+
+ build_always: bool
+ build_always_stale: T.Optional[bool]
+ build_by_default: T.Optional[bool]
+ capture: bool
+ command: T.List[T.Union[str, build.BuildTarget, build.CustomTarget,
+ build.CustomTargetIndex, ExternalProgram, File]]
+ console: bool
+ depend_files: T.List[FileOrString]
+ depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]]
+ depfile: T.Optional[str]
+ env: build.EnvironmentVariables
+ feed: bool
+ input: T.List[T.Union[str, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex,
+ build.ExtractedObjects, build.GeneratedList, ExternalProgram, File]]
+ install: bool
+ install_dir: T.List[T.Union[str, T.Literal[False]]]
+ install_mode: FileMode
+ install_tag: T.List[T.Optional[str]]
+ output: T.List[str]
+
+class AddTestSetup(TypedDict):
+
+ exe_wrapper: T.List[T.Union[str, ExternalProgram]]
+ gdb: bool
+ timeout_multiplier: int
+ is_default: bool
+ exclude_suites: T.List[str]
+ env: build.EnvironmentVariables
+
+
+class Project(TypedDict):
+
+ version: T.Optional[FileOrString]
+ meson_version: T.Optional[str]
+ default_options: T.List[str]
+ license: T.List[str]
+ subproject_dir: str
+
+
+class _FoundProto(Protocol):
+
+ """Protocol for subdir arguments.
+
+ This allows us to define any object that has a found(self) -> bool method
+ """
+
+ def found(self) -> bool: ...
+
+
+class Subdir(TypedDict):
+
+ if_found: T.List[_FoundProto]
+
+
+class Summary(TypedDict):
+
+ section: str
+ bool_yn: bool
+ list_sep: T.Optional[str]
+
+
+class FindProgram(ExtractRequired, ExtractSearchDirs):
+
+ native: MachineChoice
+ version: T.List[str]
+
+
+class RunCommand(TypedDict):
+
+ check: bool
+ capture: T.Optional[bool]
+ env: build.EnvironmentVariables
+
+
+class FeatureOptionRequire(TypedDict):
+
+ error_message: T.Optional[str]
+
+
+class DependencyPkgConfigVar(TypedDict):
+
+ default: T.Optional[str]
+ define_variable: T.List[str]
+
+
+class DependencyGetVariable(TypedDict):
+
+ cmake: T.Optional[str]
+ pkgconfig: T.Optional[str]
+ configtool: T.Optional[str]
+ internal: T.Optional[str]
+ default_value: T.Optional[str]
+ pkgconfig_define: T.List[str]
+
+
+class ConfigurationDataSet(TypedDict):
+
+ description: T.Optional[str]
+
+class VcsTag(TypedDict):
+
+ command: T.List[T.Union[str, build.BuildTarget, build.CustomTarget,
+ build.CustomTargetIndex, ExternalProgram, File]]
+ fallback: T.Optional[str]
+ input: T.List[T.Union[str, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex,
+ build.ExtractedObjects, build.GeneratedList, ExternalProgram, File]]
+ output: T.List[str]
+ replace_string: str
+
+
+class ConfigureFile(TypedDict):
+
+ output: str
+ capture: bool
+ format: T.Literal['meson', 'cmake', 'cmake@']
+ output_format: T.Literal['c', 'nasm']
+ depfile: T.Optional[str]
+ install: T.Optional[bool]
+ install_dir: T.Union[str, T.Literal[False]]
+ install_mode: FileMode
+ install_tag: T.Optional[str]
+ encoding: str
+ command: T.Optional[T.List[T.Union[build.Executable, ExternalProgram, Compiler, File, str]]]
+ input: T.List[FileOrString]
+ configuration: T.Optional[T.Union[T.Dict[str, T.Union[str, int, bool]], build.ConfigurationData]]
+
+
+class Subproject(ExtractRequired):
+
+ default_options: T.List[str]
+ version: T.List[str]
+
+
+class DoSubproject(ExtractRequired):
+
+ default_options: T.List[str]
+ version: T.List[str]
+ cmake_options: T.List[str]
+ options: T.Optional[CMakeSubprojectOptions]
diff --git a/mesonbuild/interpreter/mesonmain.py b/mesonbuild/interpreter/mesonmain.py
new file mode 100644
index 0000000..01d0029
--- /dev/null
+++ b/mesonbuild/interpreter/mesonmain.py
@@ -0,0 +1,456 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+import os
+import typing as T
+
+from .. import mesonlib
+from .. import dependencies
+from .. import build
+from .. import mlog
+
+from ..mesonlib import MachineChoice, OptionKey
+from ..programs import OverrideProgram, ExternalProgram
+from ..interpreter.type_checking import ENV_KW, ENV_METHOD_KW, ENV_SEPARATOR_KW, env_convertor_with_method
+from ..interpreterbase import (MesonInterpreterObject, FeatureNew, FeatureDeprecated,
+ typed_pos_args, noArgsFlattening, noPosargs, noKwargs,
+ typed_kwargs, KwargInfo, InterpreterException)
+from .primitives import MesonVersionString
+from .type_checking import NATIVE_KW, NoneType
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Literal
+ from ..backend.backends import ExecutableSerialisation
+ from ..compilers import Compiler
+ from ..interpreterbase import TYPE_kwargs, TYPE_var
+ from .interpreter import Interpreter
+
+ from typing_extensions import TypedDict
+
+ class FuncOverrideDependency(TypedDict):
+
+ native: mesonlib.MachineChoice
+ static: T.Optional[bool]
+
+ class AddInstallScriptKW(TypedDict):
+
+ skip_if_destdir: bool
+ install_tag: str
+
+ class NativeKW(TypedDict):
+
+ native: mesonlib.MachineChoice
+
+ class AddDevenvKW(TypedDict):
+ method: Literal['set', 'prepend', 'append']
+ separator: str
+
+
+class MesonMain(MesonInterpreterObject):
+ def __init__(self, build: 'build.Build', interpreter: 'Interpreter'):
+ super().__init__(subproject=interpreter.subproject)
+ self.build = build
+ self.interpreter = interpreter
+ self.methods.update({'get_compiler': self.get_compiler_method,
+ 'is_cross_build': self.is_cross_build_method,
+ 'has_exe_wrapper': self.has_exe_wrapper_method,
+ 'can_run_host_binaries': self.can_run_host_binaries_method,
+ 'is_unity': self.is_unity_method,
+ 'is_subproject': self.is_subproject_method,
+ 'current_source_dir': self.current_source_dir_method,
+ 'current_build_dir': self.current_build_dir_method,
+ 'source_root': self.source_root_method,
+ 'build_root': self.build_root_method,
+ 'project_source_root': self.project_source_root_method,
+ 'project_build_root': self.project_build_root_method,
+ 'global_source_root': self.global_source_root_method,
+ 'global_build_root': self.global_build_root_method,
+ 'add_install_script': self.add_install_script_method,
+ 'add_postconf_script': self.add_postconf_script_method,
+ 'add_dist_script': self.add_dist_script_method,
+ 'install_dependency_manifest': self.install_dependency_manifest_method,
+ 'override_dependency': self.override_dependency_method,
+ 'override_find_program': self.override_find_program_method,
+ 'project_version': self.project_version_method,
+ 'project_license': self.project_license_method,
+ 'version': self.version_method,
+ 'project_name': self.project_name_method,
+ 'get_cross_property': self.get_cross_property_method,
+ 'get_external_property': self.get_external_property_method,
+ 'has_external_property': self.has_external_property_method,
+ 'backend': self.backend_method,
+ 'add_devenv': self.add_devenv_method,
+ })
+
+ def _find_source_script(
+ self, name: str, prog: T.Union[str, mesonlib.File, build.Executable, ExternalProgram],
+ args: T.List[str]) -> 'ExecutableSerialisation':
+ largs: T.List[T.Union[str, build.Executable, ExternalProgram]] = []
+
+ if isinstance(prog, (build.Executable, ExternalProgram)):
+ FeatureNew.single_use(f'Passing executable/found program object to script parameter of {name}',
+ '0.55.0', self.subproject, location=self.current_node)
+ largs.append(prog)
+ else:
+ if isinstance(prog, mesonlib.File):
+ FeatureNew.single_use(f'Passing file object to script parameter of {name}',
+ '0.57.0', self.subproject, location=self.current_node)
+ found = self.interpreter.find_program_impl([prog])
+ largs.append(found)
+
+ largs.extend(args)
+ es = self.interpreter.backend.get_executable_serialisation(largs)
+ es.subproject = self.interpreter.subproject
+ return es
+
+ def _process_script_args(
+ self, name: str, args: T.Sequence[T.Union[
+ str, mesonlib.File, build.BuildTarget, build.CustomTarget,
+ build.CustomTargetIndex,
+ ExternalProgram,
+ ]]) -> T.List[str]:
+ script_args = [] # T.List[str]
+ new = False
+ for a in args:
+ if isinstance(a, str):
+ script_args.append(a)
+ elif isinstance(a, mesonlib.File):
+ new = True
+ script_args.append(a.rel_to_builddir(self.interpreter.environment.source_dir))
+ elif isinstance(a, (build.BuildTarget, build.CustomTarget, build.CustomTargetIndex)):
+ new = True
+ script_args.extend([os.path.join(a.get_subdir(), o) for o in a.get_outputs()])
+
+ # This feels really hacky, but I'm not sure how else to fix
+ # this without completely rewriting install script handling.
+ # This is complicated by the fact that the install target
+ # depends on all.
+ if isinstance(a, build.CustomTargetIndex):
+ a.target.build_by_default = True
+ else:
+ a.build_by_default = True
+ else:
+ script_args.extend(a.command)
+ new = True
+
+ if new:
+ FeatureNew.single_use(
+ f'Calling "{name}" with File, CustomTarget, Index of CustomTarget, '
+ 'Executable, or ExternalProgram',
+ '0.55.0', self.interpreter.subproject, location=self.current_node)
+ return script_args
+
+ @typed_pos_args(
+ 'meson.add_install_script',
+ (str, mesonlib.File, build.Executable, ExternalProgram),
+ varargs=(str, mesonlib.File, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, ExternalProgram)
+ )
+ @typed_kwargs(
+ 'meson.add_install_script',
+ KwargInfo('skip_if_destdir', bool, default=False, since='0.57.0'),
+ KwargInfo('install_tag', (str, NoneType), since='0.60.0'),
+ )
+ def add_install_script_method(
+ self,
+ args: T.Tuple[T.Union[str, mesonlib.File, build.Executable, ExternalProgram],
+ T.List[T.Union[str, mesonlib.File, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, ExternalProgram]]],
+ kwargs: 'AddInstallScriptKW') -> None:
+ script_args = self._process_script_args('add_install_script', args[1])
+ script = self._find_source_script('add_install_script', args[0], script_args)
+ script.skip_if_destdir = kwargs['skip_if_destdir']
+ script.tag = kwargs['install_tag']
+ self.build.install_scripts.append(script)
+
+ @typed_pos_args(
+ 'meson.add_postconf_script',
+ (str, mesonlib.File, ExternalProgram),
+ varargs=(str, mesonlib.File, ExternalProgram)
+ )
+ @noKwargs
+ def add_postconf_script_method(
+ self,
+ args: T.Tuple[T.Union[str, mesonlib.File, ExternalProgram],
+ T.List[T.Union[str, mesonlib.File, ExternalProgram]]],
+ kwargs: 'TYPE_kwargs') -> None:
+ script_args = self._process_script_args('add_postconf_script', args[1])
+ script = self._find_source_script('add_postconf_script', args[0], script_args)
+ self.build.postconf_scripts.append(script)
+
+ @typed_pos_args(
+ 'meson.add_dist_script',
+ (str, mesonlib.File, ExternalProgram),
+ varargs=(str, mesonlib.File, ExternalProgram)
+ )
+ @noKwargs
+ def add_dist_script_method(
+ self,
+ args: T.Tuple[T.Union[str, mesonlib.File, ExternalProgram],
+ T.List[T.Union[str, mesonlib.File, ExternalProgram]]],
+ kwargs: 'TYPE_kwargs') -> None:
+ if args[1]:
+ FeatureNew.single_use('Calling "add_dist_script" with multiple arguments',
+ '0.49.0', self.interpreter.subproject, location=self.current_node)
+ if self.interpreter.subproject != '':
+ FeatureNew.single_use('Calling "add_dist_script" in a subproject',
+ '0.58.0', self.interpreter.subproject, location=self.current_node)
+ script_args = self._process_script_args('add_dist_script', args[1])
+ script = self._find_source_script('add_dist_script', args[0], script_args)
+ self.build.dist_scripts.append(script)
+
+ @noPosargs
+ @noKwargs
+ def current_source_dir_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ src = self.interpreter.environment.source_dir
+ sub = self.interpreter.subdir
+ if sub == '':
+ return src
+ return os.path.join(src, sub)
+
+ @noPosargs
+ @noKwargs
+ def current_build_dir_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ src = self.interpreter.environment.build_dir
+ sub = self.interpreter.subdir
+ if sub == '':
+ return src
+ return os.path.join(src, sub)
+
+ @noPosargs
+ @noKwargs
+ def backend_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.interpreter.backend.name
+
+ @noPosargs
+ @noKwargs
+ @FeatureDeprecated('meson.source_root', '0.56.0', 'use meson.project_source_root() or meson.global_source_root() instead.')
+ def source_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.interpreter.environment.source_dir
+
+ @noPosargs
+ @noKwargs
+ @FeatureDeprecated('meson.build_root', '0.56.0', 'use meson.project_build_root() or meson.global_build_root() instead.')
+ def build_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.interpreter.environment.build_dir
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('meson.project_source_root', '0.56.0')
+ def project_source_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ src = self.interpreter.environment.source_dir
+ sub = self.interpreter.root_subdir
+ if sub == '':
+ return src
+ return os.path.join(src, sub)
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('meson.project_build_root', '0.56.0')
+ def project_build_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ src = self.interpreter.environment.build_dir
+ sub = self.interpreter.root_subdir
+ if sub == '':
+ return src
+ return os.path.join(src, sub)
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('meson.global_source_root', '0.58.0')
+ def global_source_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.interpreter.environment.source_dir
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('meson.global_build_root', '0.58.0')
+ def global_build_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.interpreter.environment.build_dir
+
+ @noPosargs
+ @noKwargs
+ @FeatureDeprecated('meson.has_exe_wrapper', '0.55.0', 'use meson.can_run_host_binaries instead.')
+ def has_exe_wrapper_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
+ return self._can_run_host_binaries_impl()
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('meson.can_run_host_binaries', '0.55.0')
+ def can_run_host_binaries_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
+ return self._can_run_host_binaries_impl()
+
+ def _can_run_host_binaries_impl(self) -> bool:
+ return not (
+ self.build.environment.is_cross_build() and
+ self.build.environment.need_exe_wrapper() and
+ self.build.environment.exe_wrapper is None
+ )
+
+ @noPosargs
+ @noKwargs
+ def is_cross_build_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
+ return self.build.environment.is_cross_build()
+
+ @typed_pos_args('meson.get_compiler', str)
+ @typed_kwargs('meson.get_compiler', NATIVE_KW)
+ def get_compiler_method(self, args: T.Tuple[str], kwargs: 'NativeKW') -> 'Compiler':
+ cname = args[0]
+ for_machine = kwargs['native']
+ clist = self.interpreter.coredata.compilers[for_machine]
+ try:
+ return clist[cname]
+ except KeyError:
+ raise InterpreterException(f'Tried to access compiler for language "{cname}", not specified for {for_machine.get_lower_case_name()} machine.')
+
+ @noPosargs
+ @noKwargs
+ def is_unity_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
+ optval = self.interpreter.environment.coredata.get_option(OptionKey('unity'))
+ return optval == 'on' or (optval == 'subprojects' and self.interpreter.is_subproject())
+
+ @noPosargs
+ @noKwargs
+ def is_subproject_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
+ return self.interpreter.is_subproject()
+
+ @typed_pos_args('meson.install_dependency_manifest', str)
+ @noKwargs
+ def install_dependency_manifest_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> None:
+ self.build.dep_manifest_name = args[0]
+
+ @FeatureNew('meson.override_find_program', '0.46.0')
+ @typed_pos_args('meson.override_find_program', str, (mesonlib.File, ExternalProgram, build.Executable))
+ @noKwargs
+ def override_find_program_method(self, args: T.Tuple[str, T.Union[mesonlib.File, ExternalProgram, build.Executable]], kwargs: 'TYPE_kwargs') -> None:
+ name, exe = args
+ if isinstance(exe, mesonlib.File):
+ abspath = exe.absolute_path(self.interpreter.environment.source_dir,
+ self.interpreter.environment.build_dir)
+ if not os.path.exists(abspath):
+ raise InterpreterException(f'Tried to override {name} with a file that does not exist.')
+ exe = OverrideProgram(name, [abspath])
+ self.interpreter.add_find_program_override(name, exe)
+
+ @typed_kwargs(
+ 'meson.override_dependency',
+ NATIVE_KW,
+ KwargInfo('static', (bool, NoneType), since='0.60.0'),
+ )
+ @typed_pos_args('meson.override_dependency', str, dependencies.Dependency)
+ @FeatureNew('meson.override_dependency', '0.54.0')
+ def override_dependency_method(self, args: T.Tuple[str, dependencies.Dependency], kwargs: 'FuncOverrideDependency') -> None:
+ name, dep = args
+ if not name:
+ raise InterpreterException('First argument must be a string and cannot be empty')
+
+ optkey = OptionKey('default_library', subproject=self.interpreter.subproject)
+ default_library = self.interpreter.coredata.get_option(optkey)
+ assert isinstance(default_library, str), 'for mypy'
+ static = kwargs['static']
+ if static is None:
+ # We don't know if dep represents a static or shared library, could
+ # be a mix of both. We assume it is following default_library
+ # value.
+ self._override_dependency_impl(name, dep, kwargs, static=None)
+ if default_library == 'static':
+ self._override_dependency_impl(name, dep, kwargs, static=True)
+ elif default_library == 'shared':
+ self._override_dependency_impl(name, dep, kwargs, static=False)
+ else:
+ self._override_dependency_impl(name, dep, kwargs, static=True)
+ self._override_dependency_impl(name, dep, kwargs, static=False)
+ else:
+ # dependency('foo') without specifying static kwarg should find this
+ # override regardless of the static value here. But do not raise error
+ # if it has already been overridden, which would happen when overriding
+ # static and shared separately:
+ # meson.override_dependency('foo', shared_dep, static: false)
+ # meson.override_dependency('foo', static_dep, static: true)
+ # In that case dependency('foo') would return the first override.
+ self._override_dependency_impl(name, dep, kwargs, static=None, permissive=True)
+ self._override_dependency_impl(name, dep, kwargs, static=static)
+
+ def _override_dependency_impl(self, name: str, dep: dependencies.Dependency, kwargs: 'FuncOverrideDependency',
+ static: T.Optional[bool], permissive: bool = False) -> None:
+ # We need the cast here as get_dep_identifier works on such a dict,
+ # which FuncOverrideDependency is, but mypy can't fgure that out
+ nkwargs = T.cast('T.Dict[str, T.Any]', kwargs.copy())
+ if static is None:
+ del nkwargs['static']
+ else:
+ nkwargs['static'] = static
+ identifier = dependencies.get_dep_identifier(name, nkwargs)
+ for_machine = kwargs['native']
+ override = self.build.dependency_overrides[for_machine].get(identifier)
+ if override:
+ if permissive:
+ return
+ m = 'Tried to override dependency {!r} which has already been resolved or overridden at {}'
+ location = mlog.get_error_location_string(override.node.filename, override.node.lineno)
+ raise InterpreterException(m.format(name, location))
+ self.build.dependency_overrides[for_machine][identifier] = \
+ build.DependencyOverride(dep, self.interpreter.current_node)
+
+ @noPosargs
+ @noKwargs
+ def project_version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.build.dep_manifest[self.interpreter.active_projectname].version
+
+ @FeatureNew('meson.project_license()', '0.45.0')
+ @noPosargs
+ @noKwargs
+ def project_license_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> T.List[str]:
+ return self.build.dep_manifest[self.interpreter.active_projectname].license
+
+ @noPosargs
+ @noKwargs
+ def version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> MesonVersionString:
+ return MesonVersionString(self.interpreter.coredata.version)
+
+ @noPosargs
+ @noKwargs
+ def project_name_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.interpreter.active_projectname
+
+ def __get_external_property_impl(self, propname: str, fallback: T.Optional[object], machine: MachineChoice) -> object:
+ """Shared implementation for get_cross_property and get_external_property."""
+ try:
+ return self.interpreter.environment.properties[machine][propname]
+ except KeyError:
+ if fallback is not None:
+ return fallback
+ raise InterpreterException(f'Unknown property for {machine.get_lower_case_name()} machine: {propname}')
+
+ @noArgsFlattening
+ @FeatureDeprecated('meson.get_cross_property', '0.58.0', 'Use meson.get_external_property() instead')
+ @typed_pos_args('meson.get_cross_property', str, optargs=[object])
+ @noKwargs
+ def get_cross_property_method(self, args: T.Tuple[str, T.Optional[object]], kwargs: 'TYPE_kwargs') -> object:
+ propname, fallback = args
+ return self.__get_external_property_impl(propname, fallback, MachineChoice.HOST)
+
+ @noArgsFlattening
+ @FeatureNew('meson.get_external_property', '0.54.0')
+ @typed_pos_args('meson.get_external_property', str, optargs=[object])
+ @typed_kwargs('meson.get_external_property', NATIVE_KW)
+ def get_external_property_method(self, args: T.Tuple[str, T.Optional[object]], kwargs: 'NativeKW') -> object:
+ propname, fallback = args
+ return self.__get_external_property_impl(propname, fallback, kwargs['native'])
+
+ @FeatureNew('meson.has_external_property', '0.58.0')
+ @typed_pos_args('meson.has_external_property', str)
+ @typed_kwargs('meson.has_external_property', NATIVE_KW)
+ def has_external_property_method(self, args: T.Tuple[str], kwargs: 'NativeKW') -> bool:
+ prop_name = args[0]
+ return prop_name in self.interpreter.environment.properties[kwargs['native']]
+
+ @FeatureNew('add_devenv', '0.58.0')
+ @typed_kwargs('environment', ENV_METHOD_KW, ENV_SEPARATOR_KW.evolve(since='0.62.0'))
+ @typed_pos_args('add_devenv', (str, list, dict, build.EnvironmentVariables))
+ def add_devenv_method(self, args: T.Tuple[T.Union[str, list, dict, build.EnvironmentVariables]],
+ kwargs: 'AddDevenvKW') -> None:
+ env = args[0]
+ msg = ENV_KW.validator(env)
+ if msg:
+ raise build.InvalidArguments(f'"add_devenv": {msg}')
+ converted = env_convertor_with_method(env, kwargs['method'], kwargs['separator'])
+ assert isinstance(converted, build.EnvironmentVariables)
+ self.build.devenv.append(converted)
diff --git a/mesonbuild/interpreter/primitives/__init__.py b/mesonbuild/interpreter/primitives/__init__.py
new file mode 100644
index 0000000..aebef41
--- /dev/null
+++ b/mesonbuild/interpreter/primitives/__init__.py
@@ -0,0 +1,29 @@
+# Copyright 2021 The Meson development team
+# SPDX-license-identifier: Apache-2.0
+
+__all__ = [
+ 'ArrayHolder',
+ 'BooleanHolder',
+ 'DictHolder',
+ 'IntegerHolder',
+ 'RangeHolder',
+ 'StringHolder',
+ 'MesonVersionString',
+ 'MesonVersionStringHolder',
+ 'DependencyVariableString',
+ 'DependencyVariableStringHolder',
+ 'OptionString',
+ 'OptionStringHolder',
+]
+
+from .array import ArrayHolder
+from .boolean import BooleanHolder
+from .dict import DictHolder
+from .integer import IntegerHolder
+from .range import RangeHolder
+from .string import (
+ StringHolder,
+ MesonVersionString, MesonVersionStringHolder,
+ DependencyVariableString, DependencyVariableStringHolder,
+ OptionString, OptionStringHolder,
+)
diff --git a/mesonbuild/interpreter/primitives/array.py b/mesonbuild/interpreter/primitives/array.py
new file mode 100644
index 0000000..eeea112
--- /dev/null
+++ b/mesonbuild/interpreter/primitives/array.py
@@ -0,0 +1,108 @@
+# Copyright 2021 The Meson development team
+# SPDX-license-identifier: Apache-2.0
+from __future__ import annotations
+
+import typing as T
+
+from ...interpreterbase import (
+ ObjectHolder,
+ IterableObject,
+ MesonOperator,
+ typed_operator,
+ noKwargs,
+ noPosargs,
+ noArgsFlattening,
+ typed_pos_args,
+ FeatureNew,
+
+ TYPE_var,
+
+ InvalidArguments,
+)
+from ...mparser import PlusAssignmentNode
+
+if T.TYPE_CHECKING:
+ # Object holders need the actual interpreter
+ from ...interpreter import Interpreter
+ from ...interpreterbase import TYPE_kwargs
+
+class ArrayHolder(ObjectHolder[T.List[TYPE_var]], IterableObject):
+ def __init__(self, obj: T.List[TYPE_var], interpreter: 'Interpreter') -> None:
+ super().__init__(obj, interpreter)
+ self.methods.update({
+ 'contains': self.contains_method,
+ 'length': self.length_method,
+ 'get': self.get_method,
+ })
+
+ self.trivial_operators.update({
+ MesonOperator.EQUALS: (list, lambda x: self.held_object == x),
+ MesonOperator.NOT_EQUALS: (list, lambda x: self.held_object != x),
+ MesonOperator.IN: (object, lambda x: x in self.held_object),
+ MesonOperator.NOT_IN: (object, lambda x: x not in self.held_object),
+ })
+
+ # Use actual methods for functions that require additional checks
+ self.operators.update({
+ MesonOperator.PLUS: self.op_plus,
+ MesonOperator.INDEX: self.op_index,
+ })
+
+ def display_name(self) -> str:
+ return 'array'
+
+ def iter_tuple_size(self) -> None:
+ return None
+
+ def iter_self(self) -> T.Iterator[TYPE_var]:
+ return iter(self.held_object)
+
+ def size(self) -> int:
+ return len(self.held_object)
+
+ @noArgsFlattening
+ @noKwargs
+ @typed_pos_args('array.contains', object)
+ def contains_method(self, args: T.Tuple[object], kwargs: TYPE_kwargs) -> bool:
+ def check_contains(el: T.List[TYPE_var]) -> bool:
+ for element in el:
+ if isinstance(element, list):
+ found = check_contains(element)
+ if found:
+ return True
+ if element == args[0]:
+ return True
+ return False
+ return check_contains(self.held_object)
+
+ @noKwargs
+ @noPosargs
+ def length_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> int:
+ return len(self.held_object)
+
+ @noArgsFlattening
+ @noKwargs
+ @typed_pos_args('array.get', int, optargs=[object])
+ def get_method(self, args: T.Tuple[int, T.Optional[TYPE_var]], kwargs: TYPE_kwargs) -> TYPE_var:
+ index = args[0]
+ if index < -len(self.held_object) or index >= len(self.held_object):
+ if args[1] is None:
+ raise InvalidArguments(f'Array index {index} is out of bounds for array of size {len(self.held_object)}.')
+ return args[1]
+ return self.held_object[index]
+
+ @typed_operator(MesonOperator.PLUS, object)
+ def op_plus(self, other: TYPE_var) -> T.List[TYPE_var]:
+ if not isinstance(other, list):
+ if not isinstance(self.current_node, PlusAssignmentNode):
+ FeatureNew.single_use('list.<plus>', '0.60.0', self.subproject, 'The right hand operand was not a list.',
+ location=self.current_node)
+ other = [other]
+ return self.held_object + other
+
+ @typed_operator(MesonOperator.INDEX, int)
+ def op_index(self, other: int) -> TYPE_var:
+ try:
+ return self.held_object[other]
+ except IndexError:
+ raise InvalidArguments(f'Index {other} out of bounds of array of size {len(self.held_object)}.')
diff --git a/mesonbuild/interpreter/primitives/boolean.py b/mesonbuild/interpreter/primitives/boolean.py
new file mode 100644
index 0000000..4b49caf
--- /dev/null
+++ b/mesonbuild/interpreter/primitives/boolean.py
@@ -0,0 +1,52 @@
+# Copyright 2021 The Meson development team
+# SPDX-license-identifier: Apache-2.0
+from __future__ import annotations
+
+from ...interpreterbase import (
+ ObjectHolder,
+ MesonOperator,
+ typed_pos_args,
+ noKwargs,
+ noPosargs,
+
+ InvalidArguments
+)
+
+import typing as T
+
+if T.TYPE_CHECKING:
+ # Object holders need the actual interpreter
+ from ...interpreter import Interpreter
+ from ...interpreterbase import TYPE_var, TYPE_kwargs
+
+class BooleanHolder(ObjectHolder[bool]):
+ def __init__(self, obj: bool, interpreter: 'Interpreter') -> None:
+ super().__init__(obj, interpreter)
+ self.methods.update({
+ 'to_int': self.to_int_method,
+ 'to_string': self.to_string_method,
+ })
+
+ self.trivial_operators.update({
+ MesonOperator.BOOL: (None, lambda x: self.held_object),
+ MesonOperator.NOT: (None, lambda x: not self.held_object),
+ MesonOperator.EQUALS: (bool, lambda x: self.held_object == x),
+ MesonOperator.NOT_EQUALS: (bool, lambda x: self.held_object != x),
+ })
+
+ def display_name(self) -> str:
+ return 'bool'
+
+ @noKwargs
+ @noPosargs
+ def to_int_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> int:
+ return 1 if self.held_object else 0
+
+ @noKwargs
+ @typed_pos_args('bool.to_string', optargs=[str, str])
+ def to_string_method(self, args: T.Tuple[T.Optional[str], T.Optional[str]], kwargs: TYPE_kwargs) -> str:
+ true_str = args[0] or 'true'
+ false_str = args[1] or 'false'
+ if any(x is not None for x in args) and not all(x is not None for x in args):
+ raise InvalidArguments('bool.to_string() must have either no arguments or exactly two string arguments that signify what values to return for true and false.')
+ return true_str if self.held_object else false_str
diff --git a/mesonbuild/interpreter/primitives/dict.py b/mesonbuild/interpreter/primitives/dict.py
new file mode 100644
index 0000000..ac7c99b
--- /dev/null
+++ b/mesonbuild/interpreter/primitives/dict.py
@@ -0,0 +1,88 @@
+# Copyright 2021 The Meson development team
+# SPDX-license-identifier: Apache-2.0
+from __future__ import annotations
+
+import typing as T
+
+from ...interpreterbase import (
+ ObjectHolder,
+ IterableObject,
+ MesonOperator,
+ typed_operator,
+ noKwargs,
+ noPosargs,
+ noArgsFlattening,
+ typed_pos_args,
+
+ TYPE_var,
+
+ InvalidArguments,
+)
+
+if T.TYPE_CHECKING:
+ # Object holders need the actual interpreter
+ from ...interpreter import Interpreter
+ from ...interpreterbase import TYPE_kwargs
+
+class DictHolder(ObjectHolder[T.Dict[str, TYPE_var]], IterableObject):
+ def __init__(self, obj: T.Dict[str, TYPE_var], interpreter: 'Interpreter') -> None:
+ super().__init__(obj, interpreter)
+ self.methods.update({
+ 'has_key': self.has_key_method,
+ 'keys': self.keys_method,
+ 'get': self.get_method,
+ })
+
+ self.trivial_operators.update({
+ # Arithmetic
+ MesonOperator.PLUS: (dict, lambda x: {**self.held_object, **x}),
+
+ # Comparison
+ MesonOperator.EQUALS: (dict, lambda x: self.held_object == x),
+ MesonOperator.NOT_EQUALS: (dict, lambda x: self.held_object != x),
+ MesonOperator.IN: (str, lambda x: x in self.held_object),
+ MesonOperator.NOT_IN: (str, lambda x: x not in self.held_object),
+ })
+
+ # Use actual methods for functions that require additional checks
+ self.operators.update({
+ MesonOperator.INDEX: self.op_index,
+ })
+
+ def display_name(self) -> str:
+ return 'dict'
+
+ def iter_tuple_size(self) -> int:
+ return 2
+
+ def iter_self(self) -> T.Iterator[T.Tuple[str, TYPE_var]]:
+ return iter(self.held_object.items())
+
+ def size(self) -> int:
+ return len(self.held_object)
+
+ @noKwargs
+ @typed_pos_args('dict.has_key', str)
+ def has_key_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> bool:
+ return args[0] in self.held_object
+
+ @noKwargs
+ @noPosargs
+ def keys_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> T.List[str]:
+ return sorted(self.held_object)
+
+ @noArgsFlattening
+ @noKwargs
+ @typed_pos_args('dict.get', str, optargs=[object])
+ def get_method(self, args: T.Tuple[str, T.Optional[TYPE_var]], kwargs: TYPE_kwargs) -> TYPE_var:
+ if args[0] in self.held_object:
+ return self.held_object[args[0]]
+ if args[1] is not None:
+ return args[1]
+ raise InvalidArguments(f'Key {args[0]!r} is not in the dictionary.')
+
+ @typed_operator(MesonOperator.INDEX, str)
+ def op_index(self, other: str) -> TYPE_var:
+ if other not in self.held_object:
+ raise InvalidArguments(f'Key {other} is not in the dictionary.')
+ return self.held_object[other]
diff --git a/mesonbuild/interpreter/primitives/integer.py b/mesonbuild/interpreter/primitives/integer.py
new file mode 100644
index 0000000..f433f57
--- /dev/null
+++ b/mesonbuild/interpreter/primitives/integer.py
@@ -0,0 +1,81 @@
+# Copyright 2021 The Meson development team
+# SPDX-license-identifier: Apache-2.0
+from __future__ import annotations
+
+from ...interpreterbase import (
+ ObjectHolder,
+ MesonOperator,
+ typed_operator,
+ noKwargs,
+ noPosargs,
+
+ InvalidArguments
+)
+
+import typing as T
+
+if T.TYPE_CHECKING:
+ # Object holders need the actual interpreter
+ from ...interpreter import Interpreter
+ from ...interpreterbase import TYPE_var, TYPE_kwargs
+
+class IntegerHolder(ObjectHolder[int]):
+ def __init__(self, obj: int, interpreter: 'Interpreter') -> None:
+ super().__init__(obj, interpreter)
+ self.methods.update({
+ 'is_even': self.is_even_method,
+ 'is_odd': self.is_odd_method,
+ 'to_string': self.to_string_method,
+ })
+
+ self.trivial_operators.update({
+ # Arithmetic
+ MesonOperator.UMINUS: (None, lambda x: -self.held_object),
+ MesonOperator.PLUS: (int, lambda x: self.held_object + x),
+ MesonOperator.MINUS: (int, lambda x: self.held_object - x),
+ MesonOperator.TIMES: (int, lambda x: self.held_object * x),
+
+ # Comparison
+ MesonOperator.EQUALS: (int, lambda x: self.held_object == x),
+ MesonOperator.NOT_EQUALS: (int, lambda x: self.held_object != x),
+ MesonOperator.GREATER: (int, lambda x: self.held_object > x),
+ MesonOperator.LESS: (int, lambda x: self.held_object < x),
+ MesonOperator.GREATER_EQUALS: (int, lambda x: self.held_object >= x),
+ MesonOperator.LESS_EQUALS: (int, lambda x: self.held_object <= x),
+ })
+
+ # Use actual methods for functions that require additional checks
+ self.operators.update({
+ MesonOperator.DIV: self.op_div,
+ MesonOperator.MOD: self.op_mod,
+ })
+
+ def display_name(self) -> str:
+ return 'int'
+
+ @noKwargs
+ @noPosargs
+ def is_even_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ return self.held_object % 2 == 0
+
+ @noKwargs
+ @noPosargs
+ def is_odd_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
+ return self.held_object % 2 != 0
+
+ @noKwargs
+ @noPosargs
+ def to_string_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return str(self.held_object)
+
+ @typed_operator(MesonOperator.DIV, int)
+ def op_div(self, other: int) -> int:
+ if other == 0:
+ raise InvalidArguments('Tried to divide by 0')
+ return self.held_object // other
+
+ @typed_operator(MesonOperator.MOD, int)
+ def op_mod(self, other: int) -> int:
+ if other == 0:
+ raise InvalidArguments('Tried to divide by 0')
+ return self.held_object % other
diff --git a/mesonbuild/interpreter/primitives/range.py b/mesonbuild/interpreter/primitives/range.py
new file mode 100644
index 0000000..5eb5e03
--- /dev/null
+++ b/mesonbuild/interpreter/primitives/range.py
@@ -0,0 +1,38 @@
+# Copyright 2021 The Meson development team
+# SPDX-license-identifier: Apache-2.0
+from __future__ import annotations
+
+import typing as T
+
+from ...interpreterbase import (
+ MesonInterpreterObject,
+ IterableObject,
+ MesonOperator,
+ InvalidArguments,
+)
+
+if T.TYPE_CHECKING:
+ from ...interpreterbase import SubProject
+
+class RangeHolder(MesonInterpreterObject, IterableObject):
+ def __init__(self, start: int, stop: int, step: int, *, subproject: 'SubProject') -> None:
+ super().__init__(subproject=subproject)
+ self.range = range(start, stop, step)
+ self.operators.update({
+ MesonOperator.INDEX: self.op_index,
+ })
+
+ def op_index(self, other: int) -> int:
+ try:
+ return self.range[other]
+ except IndexError:
+ raise InvalidArguments(f'Index {other} out of bounds of range.')
+
+ def iter_tuple_size(self) -> None:
+ return None
+
+ def iter_self(self) -> T.Iterator[int]:
+ return iter(self.range)
+
+ def size(self) -> int:
+ return len(self.range)
diff --git a/mesonbuild/interpreter/primitives/string.py b/mesonbuild/interpreter/primitives/string.py
new file mode 100644
index 0000000..d9f6a06
--- /dev/null
+++ b/mesonbuild/interpreter/primitives/string.py
@@ -0,0 +1,233 @@
+# Copyright 2021 The Meson development team
+# SPDX-license-identifier: Apache-2.0
+from __future__ import annotations
+
+import re
+import os
+
+import typing as T
+
+from ...mesonlib import version_compare
+from ...interpreterbase import (
+ ObjectHolder,
+ MesonOperator,
+ FeatureNew,
+ typed_operator,
+ noArgsFlattening,
+ noKwargs,
+ noPosargs,
+ typed_pos_args,
+
+ InvalidArguments,
+)
+
+
+if T.TYPE_CHECKING:
+ # Object holders need the actual interpreter
+ from ...interpreter import Interpreter
+ from ...interpreterbase import TYPE_var, TYPE_kwargs
+
+class StringHolder(ObjectHolder[str]):
+ def __init__(self, obj: str, interpreter: 'Interpreter') -> None:
+ super().__init__(obj, interpreter)
+ self.methods.update({
+ 'contains': self.contains_method,
+ 'startswith': self.startswith_method,
+ 'endswith': self.endswith_method,
+ 'format': self.format_method,
+ 'join': self.join_method,
+ 'replace': self.replace_method,
+ 'split': self.split_method,
+ 'strip': self.strip_method,
+ 'substring': self.substring_method,
+ 'to_int': self.to_int_method,
+ 'to_lower': self.to_lower_method,
+ 'to_upper': self.to_upper_method,
+ 'underscorify': self.underscorify_method,
+ 'version_compare': self.version_compare_method,
+ })
+
+ self.trivial_operators.update({
+ # Arithmetic
+ MesonOperator.PLUS: (str, lambda x: self.held_object + x),
+
+ # Comparison
+ MesonOperator.EQUALS: (str, lambda x: self.held_object == x),
+ MesonOperator.NOT_EQUALS: (str, lambda x: self.held_object != x),
+ MesonOperator.GREATER: (str, lambda x: self.held_object > x),
+ MesonOperator.LESS: (str, lambda x: self.held_object < x),
+ MesonOperator.GREATER_EQUALS: (str, lambda x: self.held_object >= x),
+ MesonOperator.LESS_EQUALS: (str, lambda x: self.held_object <= x),
+ })
+
+ # Use actual methods for functions that require additional checks
+ self.operators.update({
+ MesonOperator.DIV: self.op_div,
+ MesonOperator.INDEX: self.op_index,
+ MesonOperator.IN: self.op_in,
+ MesonOperator.NOT_IN: self.op_notin,
+ })
+
+ def display_name(self) -> str:
+ return 'str'
+
+ @noKwargs
+ @typed_pos_args('str.contains', str)
+ def contains_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> bool:
+ return self.held_object.find(args[0]) >= 0
+
+ @noKwargs
+ @typed_pos_args('str.startswith', str)
+ def startswith_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> bool:
+ return self.held_object.startswith(args[0])
+
+ @noKwargs
+ @typed_pos_args('str.endswith', str)
+ def endswith_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> bool:
+ return self.held_object.endswith(args[0])
+
+ @noArgsFlattening
+ @noKwargs
+ @typed_pos_args('str.format', varargs=object)
+ def format_method(self, args: T.Tuple[T.List[object]], kwargs: TYPE_kwargs) -> str:
+ arg_strings: T.List[str] = []
+ for arg in args[0]:
+ if isinstance(arg, bool): # Python boolean is upper case.
+ arg = str(arg).lower()
+ arg_strings.append(str(arg))
+
+ def arg_replace(match: T.Match[str]) -> str:
+ idx = int(match.group(1))
+ if idx >= len(arg_strings):
+ raise InvalidArguments(f'Format placeholder @{idx}@ out of range.')
+ return arg_strings[idx]
+
+ return re.sub(r'@(\d+)@', arg_replace, self.held_object)
+
+ @noKwargs
+ @typed_pos_args('str.join', varargs=str)
+ def join_method(self, args: T.Tuple[T.List[str]], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.join(args[0])
+
+ @noKwargs
+ @typed_pos_args('str.replace', str, str)
+ def replace_method(self, args: T.Tuple[str, str], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.replace(args[0], args[1])
+
+ @noKwargs
+ @typed_pos_args('str.split', optargs=[str])
+ def split_method(self, args: T.Tuple[T.Optional[str]], kwargs: TYPE_kwargs) -> T.List[str]:
+ return self.held_object.split(args[0])
+
+ @noKwargs
+ @typed_pos_args('str.strip', optargs=[str])
+ def strip_method(self, args: T.Tuple[T.Optional[str]], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.strip(args[0])
+
+ @noKwargs
+ @typed_pos_args('str.substring', optargs=[int, int])
+ def substring_method(self, args: T.Tuple[T.Optional[int], T.Optional[int]], kwargs: TYPE_kwargs) -> str:
+ start = args[0] if args[0] is not None else 0
+ end = args[1] if args[1] is not None else len(self.held_object)
+ return self.held_object[start:end]
+
+ @noKwargs
+ @noPosargs
+ def to_int_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> int:
+ try:
+ return int(self.held_object)
+ except ValueError:
+ raise InvalidArguments(f'String {self.held_object!r} cannot be converted to int')
+
+ @noKwargs
+ @noPosargs
+ def to_lower_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.lower()
+
+ @noKwargs
+ @noPosargs
+ def to_upper_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return self.held_object.upper()
+
+ @noKwargs
+ @noPosargs
+ def underscorify_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
+ return re.sub(r'[^a-zA-Z0-9]', '_', self.held_object)
+
+ @noKwargs
+ @typed_pos_args('str.version_compare', str)
+ def version_compare_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> bool:
+ return version_compare(self.held_object, args[0])
+
+ @staticmethod
+ def _op_div(this: str, other: str) -> str:
+ return os.path.join(this, other).replace('\\', '/')
+
+ @FeatureNew('/ with string arguments', '0.49.0')
+ @typed_operator(MesonOperator.DIV, str)
+ def op_div(self, other: str) -> str:
+ return self._op_div(self.held_object, other)
+
+ @typed_operator(MesonOperator.INDEX, int)
+ def op_index(self, other: int) -> str:
+ try:
+ return self.held_object[other]
+ except IndexError:
+ raise InvalidArguments(f'Index {other} out of bounds of string of size {len(self.held_object)}.')
+
+ @FeatureNew('"in" string operator', '1.0.0')
+ @typed_operator(MesonOperator.IN, str)
+ def op_in(self, other: str) -> bool:
+ return other in self.held_object
+
+ @FeatureNew('"not in" string operator', '1.0.0')
+ @typed_operator(MesonOperator.NOT_IN, str)
+ def op_notin(self, other: str) -> bool:
+ return other not in self.held_object
+
+
+class MesonVersionString(str):
+ pass
+
+class MesonVersionStringHolder(StringHolder):
+ @noKwargs
+ @typed_pos_args('str.version_compare', str)
+ def version_compare_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> bool:
+ self.interpreter.tmp_meson_version = args[0]
+ return version_compare(self.held_object, args[0])
+
+# These special subclasses of string exist to cover the case where a dependency
+# exports a string variable interchangeable with a system dependency. This
+# matters because a dependency can only have string-type get_variable() return
+# values. If at any time dependencies start supporting additional variable
+# types, this class could be deprecated.
+class DependencyVariableString(str):
+ pass
+
+class DependencyVariableStringHolder(StringHolder):
+ def op_div(self, other: str) -> T.Union[str, DependencyVariableString]:
+ ret = super().op_div(other)
+ if '..' in other:
+ return ret
+ return DependencyVariableString(ret)
+
+
+class OptionString(str):
+ optname: str
+
+ def __new__(cls, value: str, name: str) -> 'OptionString':
+ obj = str.__new__(cls, value)
+ obj.optname = name
+ return obj
+
+ def __getnewargs__(self) -> T.Tuple[str, str]: # type: ignore # because the entire point of this is to diverge
+ return (str(self), self.optname)
+
+
+class OptionStringHolder(StringHolder):
+ held_object: OptionString
+
+ def op_div(self, other: str) -> T.Union[str, OptionString]:
+ ret = super().op_div(other)
+ name = self._op_div(self.held_object.optname, other)
+ return OptionString(ret, name)
diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py
new file mode 100644
index 0000000..7e1bd80
--- /dev/null
+++ b/mesonbuild/interpreter/type_checking.py
@@ -0,0 +1,479 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright © 2021 Intel Corporation
+
+"""Helpers for strict type checking."""
+
+from __future__ import annotations
+import os
+import typing as T
+
+from .. import compilers
+from ..build import (CustomTarget, BuildTarget,
+ CustomTargetIndex, ExtractedObjects, GeneratedList, IncludeDirs,
+ BothLibraries, SharedLibrary, StaticLibrary, Jar, Executable)
+from ..coredata import UserFeatureOption
+from ..dependencies import Dependency, InternalDependency
+from ..interpreterbase import FeatureNew
+from ..interpreterbase.decorators import KwargInfo, ContainerTypeInfo
+from ..mesonlib import (File, FileMode, MachineChoice, listify, has_path_sep,
+ OptionKey, EnvironmentVariables)
+from ..programs import ExternalProgram
+
+# Helper definition for type checks that are `Optional[T]`
+NoneType: T.Type[None] = type(None)
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from ..interpreterbase import TYPE_var
+ from ..interpreterbase.decorators import FeatureCheckBase
+ from ..mesonlib import EnvInitValueType
+
+ _FullEnvInitValueType = T.Union[EnvironmentVariables, T.List[str], T.List[T.List[str]], EnvInitValueType, str, None]
+
+
+def in_set_validator(choices: T.Set[str]) -> T.Callable[[str], T.Optional[str]]:
+ """Check that the choice given was one of the given set."""
+
+ def inner(check: str) -> T.Optional[str]:
+ if check not in choices:
+ return f"must be one of {', '.join(sorted(choices))}, not {check}"
+ return None
+
+ return inner
+
+
+def _language_validator(l: T.List[str]) -> T.Optional[str]:
+ """Validate language keyword argument.
+
+ Particularly for functions like `add_compiler()`, and `add_*_args()`
+ """
+ diff = {a.lower() for a in l}.difference(compilers.all_languages)
+ if diff:
+ return f'unknown languages: {", ".join(diff)}'
+ return None
+
+
+def _install_mode_validator(mode: T.List[T.Union[str, bool, int]]) -> T.Optional[str]:
+ """Validate the `install_mode` keyword argument.
+
+ This is a rather odd thing, it's a scalar, or an array of 3 values in the form:
+ [(str | False), (str | int | False) = False, (str | int | False) = False]
+ where the second and third components are not required and default to False.
+ """
+ if not mode:
+ return None
+ if True in mode:
+ return 'components can only be permission strings, numbers, or False'
+ if len(mode) > 3:
+ return 'may have at most 3 elements'
+
+ perms = mode[0]
+ if not isinstance(perms, (str, bool)):
+ return 'first component must be a permissions string or False'
+
+ if isinstance(perms, str):
+ if not len(perms) == 9:
+ return ('permissions string must be exactly 9 characters in the form rwxr-xr-x,'
+ f' got {len(perms)}')
+ for i in [0, 3, 6]:
+ if perms[i] not in {'-', 'r'}:
+ return f'permissions character {i+1} must be "-" or "r", not {perms[i]}'
+ for i in [1, 4, 7]:
+ if perms[i] not in {'-', 'w'}:
+ return f'permissions character {i+1} must be "-" or "w", not {perms[i]}'
+ for i in [2, 5]:
+ if perms[i] not in {'-', 'x', 's', 'S'}:
+ return f'permissions character {i+1} must be "-", "s", "S", or "x", not {perms[i]}'
+ if perms[8] not in {'-', 'x', 't', 'T'}:
+ return f'permission character 9 must be "-", "t", "T", or "x", not {perms[8]}'
+
+ if len(mode) >= 2 and not isinstance(mode[1], (int, str, bool)):
+ return 'second componenent can only be a string, number, or False'
+ if len(mode) >= 3 and not isinstance(mode[2], (int, str, bool)):
+ return 'third componenent can only be a string, number, or False'
+
+ return None
+
+
+def _install_mode_convertor(mode: T.Optional[T.List[T.Union[str, bool, int]]]) -> FileMode:
+ """Convert the DSL form of the `install_mode` keyword argument to `FileMode`
+
+ This is not required, and if not required returns None
+
+ TODO: It's not clear to me why this needs to be None and not just return an
+ empty FileMode.
+ """
+ # this has already been validated by the validator
+ return FileMode(*(m if isinstance(m, str) else None for m in mode))
+
+
+def _lower_strlist(input: T.List[str]) -> T.List[str]:
+ """Lower a list of strings.
+
+ mypy (but not pyright) gets confused about using a lambda as the convertor function
+ """
+ return [i.lower() for i in input]
+
+
+def variables_validator(contents: T.Union[str, T.List[str], T.Dict[str, str]]) -> T.Optional[str]:
+ if isinstance(contents, str):
+ contents = [contents]
+ if isinstance(contents, dict):
+ variables = contents
+ else:
+ variables = {}
+ for v in contents:
+ try:
+ key, val = v.split('=', 1)
+ except ValueError:
+ return f'variable {v!r} must have a value separated by equals sign.'
+ variables[key.strip()] = val.strip()
+ for k, v in variables.items():
+ if not k:
+ return 'empty variable name'
+ if not v:
+ return 'empty variable value'
+ if any(c.isspace() for c in k):
+ return f'invalid whitespace in variable name {k!r}'
+ return None
+
+
+def variables_convertor(contents: T.Union[str, T.List[str], T.Dict[str, str]]) -> T.Dict[str, str]:
+ if isinstance(contents, str):
+ contents = [contents]
+ if isinstance(contents, dict):
+ return contents
+ variables = {}
+ for v in contents:
+ key, val = v.split('=', 1)
+ variables[key.strip()] = val.strip()
+ return variables
+
+
+NATIVE_KW = KwargInfo(
+ 'native', bool,
+ default=False,
+ convertor=lambda n: MachineChoice.BUILD if n else MachineChoice.HOST)
+
+LANGUAGE_KW = KwargInfo(
+ 'language', ContainerTypeInfo(list, str, allow_empty=False),
+ listify=True,
+ required=True,
+ validator=_language_validator,
+ convertor=_lower_strlist)
+
+INSTALL_MODE_KW: KwargInfo[T.List[T.Union[str, bool, int]]] = KwargInfo(
+ 'install_mode',
+ ContainerTypeInfo(list, (str, bool, int)),
+ listify=True,
+ default=[],
+ validator=_install_mode_validator,
+ convertor=_install_mode_convertor,
+)
+
+REQUIRED_KW: KwargInfo[T.Union[bool, UserFeatureOption]] = KwargInfo(
+ 'required',
+ (bool, UserFeatureOption),
+ default=True,
+ # TODO: extract_required_kwarg could be converted to a convertor
+)
+
+DISABLER_KW: KwargInfo[bool] = KwargInfo('disabler', bool, default=False)
+
+def _env_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Dict[str, 'TYPE_var'], str, None],
+ allow_dict_list: bool = True) -> T.Optional[str]:
+ def _splitter(v: str) -> T.Optional[str]:
+ split = v.split('=', 1)
+ if len(split) == 1:
+ return f'"{v}" is not two string values separated by an "="'
+ return None
+
+ if isinstance(value, str):
+ v = _splitter(value)
+ if v is not None:
+ return v
+ elif isinstance(value, list):
+ for i in listify(value):
+ if not isinstance(i, str):
+ return f"All array elements must be a string, not {i!r}"
+ v = _splitter(i)
+ if v is not None:
+ return v
+ elif isinstance(value, dict):
+ # We don't need to spilt here, just do the type checking
+ for k, dv in value.items():
+ if allow_dict_list:
+ if any(i for i in listify(dv) if not isinstance(i, str)):
+ return f"Dictionary element {k} must be a string or list of strings not {dv!r}"
+ elif not isinstance(dv, str):
+ return f"Dictionary element {k} must be a string not {dv!r}"
+ # We know that otherwise we have an EnvironmentVariables object or None, and
+ # we're okay at this point
+ return None
+
+def _options_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Dict[str, 'TYPE_var'], str, None]) -> T.Optional[str]:
+ # Reusing the env validator is a littl overkill, but nicer than duplicating the code
+ return _env_validator(value, allow_dict_list=False)
+
+def split_equal_string(input: str) -> T.Tuple[str, str]:
+ """Split a string in the form `x=y`
+
+ This assumes that the string has already been validated to split properly.
+ """
+ a, b = input.split('=', 1)
+ return (a, b)
+
+# Split _env_convertor() and env_convertor_with_method() to make mypy happy.
+# It does not want extra arguments in KwargInfo convertor callable.
+def env_convertor_with_method(value: _FullEnvInitValueType,
+ init_method: Literal['set', 'prepend', 'append'] = 'set',
+ separator: str = os.pathsep) -> EnvironmentVariables:
+ if isinstance(value, str):
+ return EnvironmentVariables(dict([split_equal_string(value)]), init_method, separator)
+ elif isinstance(value, list):
+ return EnvironmentVariables(dict(split_equal_string(v) for v in listify(value)), init_method, separator)
+ elif isinstance(value, dict):
+ return EnvironmentVariables(value, init_method, separator)
+ elif value is None:
+ return EnvironmentVariables()
+ return value
+
+def _env_convertor(value: _FullEnvInitValueType) -> EnvironmentVariables:
+ return env_convertor_with_method(value)
+
+ENV_KW: KwargInfo[T.Union[EnvironmentVariables, T.List, T.Dict, str, None]] = KwargInfo(
+ 'env',
+ (EnvironmentVariables, list, dict, str, NoneType),
+ validator=_env_validator,
+ convertor=_env_convertor,
+)
+
+DEPFILE_KW: KwargInfo[T.Optional[str]] = KwargInfo(
+ 'depfile',
+ (str, type(None)),
+ validator=lambda x: 'Depfile must be a plain filename with a subdirectory' if has_path_sep(x) else None
+)
+
+# TODO: CustomTargetIndex should be supported here as well
+DEPENDS_KW: KwargInfo[T.List[T.Union[BuildTarget, CustomTarget]]] = KwargInfo(
+ 'depends',
+ ContainerTypeInfo(list, (BuildTarget, CustomTarget)),
+ listify=True,
+ default=[],
+)
+
+DEPEND_FILES_KW: KwargInfo[T.List[T.Union[str, File]]] = KwargInfo(
+ 'depend_files',
+ ContainerTypeInfo(list, (File, str)),
+ listify=True,
+ default=[],
+)
+
+COMMAND_KW: KwargInfo[T.List[T.Union[str, BuildTarget, CustomTarget, CustomTargetIndex, ExternalProgram, File]]] = KwargInfo(
+ 'command',
+ # TODO: should accept CustomTargetIndex as well?
+ ContainerTypeInfo(list, (str, BuildTarget, CustomTarget, CustomTargetIndex, ExternalProgram, File), allow_empty=False),
+ required=True,
+ listify=True,
+ default=[],
+)
+
+def _override_options_convertor(raw: T.List[str]) -> T.Dict[OptionKey, str]:
+ output: T.Dict[OptionKey, str] = {}
+ for each in raw:
+ k, v = split_equal_string(each)
+ output[OptionKey.from_string(k)] = v
+ return output
+
+
+OVERRIDE_OPTIONS_KW: KwargInfo[T.List[str]] = KwargInfo(
+ 'override_options',
+ ContainerTypeInfo(list, str),
+ listify=True,
+ default=[],
+ validator=_options_validator,
+ convertor=_override_options_convertor,
+)
+
+
+def _output_validator(outputs: T.List[str]) -> T.Optional[str]:
+ for i in outputs:
+ if i == '':
+ return 'Output must not be empty.'
+ elif i.strip() == '':
+ return 'Output must not consist only of whitespace.'
+ elif has_path_sep(i):
+ return f'Output {i!r} must not contain a path segment.'
+ elif '@INPUT' in i:
+ return f'output {i!r} contains "@INPUT", which is invalid. Did you mean "@PLAINNAME@" or "@BASENAME@?'
+
+ return None
+
+MULTI_OUTPUT_KW: KwargInfo[T.List[str]] = KwargInfo(
+ 'output',
+ ContainerTypeInfo(list, str, allow_empty=False),
+ listify=True,
+ required=True,
+ default=[],
+ validator=_output_validator,
+)
+
+OUTPUT_KW: KwargInfo[str] = KwargInfo(
+ 'output',
+ str,
+ required=True,
+ validator=lambda x: _output_validator([x])
+)
+
+CT_INPUT_KW: KwargInfo[T.List[T.Union[str, File, ExternalProgram, BuildTarget, CustomTarget, CustomTargetIndex, ExtractedObjects, GeneratedList]]] = KwargInfo(
+ 'input',
+ ContainerTypeInfo(list, (str, File, ExternalProgram, BuildTarget, CustomTarget, CustomTargetIndex, ExtractedObjects, GeneratedList)),
+ listify=True,
+ default=[],
+)
+
+CT_INSTALL_TAG_KW: KwargInfo[T.List[T.Union[str, bool]]] = KwargInfo(
+ 'install_tag',
+ ContainerTypeInfo(list, (str, bool)),
+ listify=True,
+ default=[],
+ since='0.60.0',
+ convertor=lambda x: [y if isinstance(y, str) else None for y in x],
+)
+
+INSTALL_TAG_KW: KwargInfo[T.Optional[str]] = KwargInfo('install_tag', (str, NoneType))
+
+INSTALL_KW = KwargInfo('install', bool, default=False)
+
+CT_INSTALL_DIR_KW: KwargInfo[T.List[T.Union[str, Literal[False]]]] = KwargInfo(
+ 'install_dir',
+ ContainerTypeInfo(list, (str, bool)),
+ listify=True,
+ default=[],
+ validator=lambda x: 'must be `false` if boolean' if True in x else None,
+)
+
+CT_BUILD_BY_DEFAULT: KwargInfo[T.Optional[bool]] = KwargInfo('build_by_default', (bool, type(None)), since='0.40.0')
+
+CT_BUILD_ALWAYS: KwargInfo[T.Optional[bool]] = KwargInfo(
+ 'build_always', (bool, NoneType),
+ deprecated='0.47.0',
+ deprecated_message='combine build_by_default and build_always_stale instead.',
+)
+
+CT_BUILD_ALWAYS_STALE: KwargInfo[T.Optional[bool]] = KwargInfo(
+ 'build_always_stale', (bool, NoneType),
+ since='0.47.0',
+)
+
+INSTALL_DIR_KW: KwargInfo[T.Optional[str]] = KwargInfo('install_dir', (str, NoneType))
+
+INCLUDE_DIRECTORIES: KwargInfo[T.List[T.Union[str, IncludeDirs]]] = KwargInfo(
+ 'include_directories',
+ ContainerTypeInfo(list, (str, IncludeDirs)),
+ listify=True,
+ default=[],
+)
+
+def include_dir_string_new(val: T.List[T.Union[str, IncludeDirs]]) -> T.Iterable[FeatureCheckBase]:
+ strs = [v for v in val if isinstance(v, str)]
+ if strs:
+ str_msg = ", ".join(f"'{s}'" for s in strs)
+ yield FeatureNew('include_directories kwarg of type string', '1.0.0',
+ f'Use include_directories({str_msg}) instead')
+
+# for cases like default_options and override_options
+DEFAULT_OPTIONS: KwargInfo[T.List[str]] = KwargInfo(
+ 'default_options',
+ ContainerTypeInfo(list, str),
+ listify=True,
+ default=[],
+ validator=_options_validator,
+)
+
+ENV_METHOD_KW = KwargInfo('method', str, default='set', since='0.62.0',
+ validator=in_set_validator({'set', 'prepend', 'append'}))
+
+ENV_SEPARATOR_KW = KwargInfo('separator', str, default=os.pathsep)
+
+DEPENDENCIES_KW: KwargInfo[T.List[Dependency]] = KwargInfo(
+ 'dependencies',
+ # InternalDependency is a subclass of Dependency, but we want to
+ # print it in error messages
+ ContainerTypeInfo(list, (Dependency, InternalDependency)),
+ listify=True,
+ default=[],
+)
+
+D_MODULE_VERSIONS_KW: KwargInfo[T.List[T.Union[str, int]]] = KwargInfo(
+ 'd_module_versions',
+ ContainerTypeInfo(list, (str, int)),
+ listify=True,
+ default=[],
+)
+
+_link_with_error = '''can only be self-built targets, external dependencies (including libraries) must go in "dependencies".'''
+
+# Allow Dependency for the better error message? But then in other cases it will list this as one of the allowed types!
+LINK_WITH_KW: KwargInfo[T.List[T.Union[BothLibraries, SharedLibrary, StaticLibrary, CustomTarget, CustomTargetIndex, Jar, Executable]]] = KwargInfo(
+ 'link_with',
+ ContainerTypeInfo(list, (BothLibraries, SharedLibrary, StaticLibrary, CustomTarget, CustomTargetIndex, Jar, Executable, Dependency)),
+ listify=True,
+ default=[],
+ validator=lambda x: _link_with_error if any(isinstance(i, Dependency) for i in x) else None,
+)
+
+def link_whole_validator(values: T.List[T.Union[StaticLibrary, CustomTarget, CustomTargetIndex, Dependency]]) -> T.Optional[str]:
+ for l in values:
+ if isinstance(l, (CustomTarget, CustomTargetIndex)) and l.links_dynamically():
+ return f'{type(l).__name__} returning a shared library is not allowed'
+ if isinstance(l, Dependency):
+ return _link_with_error
+ return None
+
+LINK_WHOLE_KW: KwargInfo[T.List[T.Union[BothLibraries, StaticLibrary, CustomTarget, CustomTargetIndex]]] = KwargInfo(
+ 'link_whole',
+ ContainerTypeInfo(list, (BothLibraries, StaticLibrary, CustomTarget, CustomTargetIndex, Dependency)),
+ listify=True,
+ default=[],
+ validator=link_whole_validator,
+)
+
+SOURCES_KW: KwargInfo[T.List[T.Union[str, File, CustomTarget, CustomTargetIndex, GeneratedList]]] = KwargInfo(
+ 'sources',
+ ContainerTypeInfo(list, (str, File, CustomTarget, CustomTargetIndex, GeneratedList)),
+ listify=True,
+ default=[],
+)
+
+VARIABLES_KW: KwargInfo[T.Dict[str, str]] = KwargInfo(
+ 'variables',
+ # str is listified by validator/convertor, cannot use listify=True here because
+ # that would listify dict too.
+ (str, ContainerTypeInfo(list, str), ContainerTypeInfo(dict, str)), # type: ignore
+ validator=variables_validator,
+ convertor=variables_convertor,
+ default={},
+)
+
+PRESERVE_PATH_KW: KwargInfo[bool] = KwargInfo('preserve_path', bool, default=False, since='0.63.0')
+
+TEST_KWS: T.List[KwargInfo] = [
+ KwargInfo('args', ContainerTypeInfo(list, (str, File, BuildTarget, CustomTarget, CustomTargetIndex)),
+ listify=True, default=[]),
+ KwargInfo('should_fail', bool, default=False),
+ KwargInfo('timeout', int, default=30),
+ KwargInfo('workdir', (str, NoneType), default=None,
+ validator=lambda x: 'must be an absolute path' if not os.path.isabs(x) else None),
+ KwargInfo('protocol', str,
+ default='exitcode',
+ validator=in_set_validator({'exitcode', 'tap', 'gtest', 'rust'}),
+ since_values={'gtest': '0.55.0', 'rust': '0.57.0'}),
+ KwargInfo('priority', int, default=0, since='0.52.0'),
+ # TODO: env needs reworks of the way the environment variable holder itself works probably
+ ENV_KW,
+ DEPENDS_KW.evolve(since='0.46.0'),
+ KwargInfo('suite', ContainerTypeInfo(list, str), listify=True, default=['']), # yes, a list of empty string
+ KwargInfo('verbose', bool, default=False, since='0.62.0'),
+]
diff --git a/mesonbuild/interpreterbase/__init__.py b/mesonbuild/interpreterbase/__init__.py
new file mode 100644
index 0000000..13f55e5
--- /dev/null
+++ b/mesonbuild/interpreterbase/__init__.py
@@ -0,0 +1,135 @@
+# Copyright 2013-2021 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.
+
+__all__ = [
+ 'InterpreterObject',
+ 'MesonInterpreterObject',
+ 'ObjectHolder',
+ 'IterableObject',
+ 'MutableInterpreterObject',
+
+ 'MesonOperator',
+
+ 'Disabler',
+ 'is_disabled',
+
+ 'InterpreterException',
+ 'InvalidCode',
+ 'InvalidArguments',
+ 'SubdirDoneRequest',
+ 'ContinueRequest',
+ 'BreakRequest',
+
+ 'default_resolve_key',
+ 'flatten',
+ 'resolve_second_level_holders',
+
+ 'noPosargs',
+ 'noKwargs',
+ 'stringArgs',
+ 'noArgsFlattening',
+ 'noSecondLevelHolderResolving',
+ 'unholder_return',
+ 'disablerIfNotFound',
+ 'permittedKwargs',
+ 'typed_operator',
+ 'unary_operator',
+ 'typed_pos_args',
+ 'ContainerTypeInfo',
+ 'KwargInfo',
+ 'typed_kwargs',
+ 'FeatureCheckBase',
+ 'FeatureNew',
+ 'FeatureDeprecated',
+ 'FeatureNewKwargs',
+ 'FeatureDeprecatedKwargs',
+
+ 'InterpreterBase',
+
+ 'SubProject',
+
+ 'TV_fw_var',
+ 'TV_fw_args',
+ 'TV_fw_kwargs',
+ 'TV_func',
+ 'TYPE_elementary',
+ 'TYPE_var',
+ 'TYPE_nvar',
+ 'TYPE_kwargs',
+ 'TYPE_nkwargs',
+ 'TYPE_key_resolver',
+ 'TYPE_HoldableTypes',
+
+ 'HoldableTypes',
+]
+
+from .baseobjects import (
+ InterpreterObject,
+ MesonInterpreterObject,
+ ObjectHolder,
+ IterableObject,
+ MutableInterpreterObject,
+
+ TV_fw_var,
+ TV_fw_args,
+ TV_fw_kwargs,
+ TV_func,
+ TYPE_elementary,
+ TYPE_var,
+ TYPE_nvar,
+ TYPE_kwargs,
+ TYPE_nkwargs,
+ TYPE_key_resolver,
+ TYPE_HoldableTypes,
+
+ SubProject,
+
+ HoldableTypes,
+)
+
+from .decorators import (
+ noPosargs,
+ noKwargs,
+ stringArgs,
+ noArgsFlattening,
+ noSecondLevelHolderResolving,
+ unholder_return,
+ disablerIfNotFound,
+ permittedKwargs,
+ typed_pos_args,
+ ContainerTypeInfo,
+ KwargInfo,
+ typed_operator,
+ unary_operator,
+ typed_kwargs,
+ FeatureCheckBase,
+ FeatureNew,
+ FeatureDeprecated,
+ FeatureNewKwargs,
+ FeatureDeprecatedKwargs,
+)
+
+from .exceptions import (
+ InterpreterException,
+ InvalidCode,
+ InvalidArguments,
+ SubdirDoneRequest,
+ ContinueRequest,
+ BreakRequest,
+)
+
+from .disabler import Disabler, is_disabled
+from .helpers import default_resolve_key, flatten, resolve_second_level_holders
+from .interpreterbase import InterpreterBase
+from .operator import MesonOperator
diff --git a/mesonbuild/interpreterbase/_unholder.py b/mesonbuild/interpreterbase/_unholder.py
new file mode 100644
index 0000000..4f1edc1
--- /dev/null
+++ b/mesonbuild/interpreterbase/_unholder.py
@@ -0,0 +1,35 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+import typing as T
+
+from .baseobjects import InterpreterObject, MesonInterpreterObject, ObjectHolder, HoldableTypes
+from .exceptions import InvalidArguments
+from ..mesonlib import HoldableObject, MesonBugException
+
+if T.TYPE_CHECKING:
+ from .baseobjects import TYPE_var
+
+def _unholder(obj: InterpreterObject) -> TYPE_var:
+ if isinstance(obj, ObjectHolder):
+ assert isinstance(obj.held_object, HoldableTypes)
+ return obj.held_object
+ elif isinstance(obj, MesonInterpreterObject):
+ return obj
+ elif isinstance(obj, HoldableObject):
+ raise MesonBugException(f'Argument {obj} of type {type(obj).__name__} is not held by an ObjectHolder.')
+ elif isinstance(obj, InterpreterObject):
+ raise InvalidArguments(f'Argument {obj} of type {type(obj).__name__} cannot be passed to a method or function')
+ raise MesonBugException(f'Unknown object {obj} of type {type(obj).__name__} in the parameters.')
diff --git a/mesonbuild/interpreterbase/baseobjects.py b/mesonbuild/interpreterbase/baseobjects.py
new file mode 100644
index 0000000..820e091
--- /dev/null
+++ b/mesonbuild/interpreterbase/baseobjects.py
@@ -0,0 +1,182 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+from .. import mparser
+from .exceptions import InvalidCode, InvalidArguments
+from .helpers import flatten, resolve_second_level_holders
+from .operator import MesonOperator
+from ..mesonlib import HoldableObject, MesonBugException
+import textwrap
+
+import typing as T
+from abc import ABCMeta
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Protocol
+
+ # Object holders need the actual interpreter
+ from ..interpreter import Interpreter
+
+ __T = T.TypeVar('__T', bound='TYPE_var', contravariant=True)
+
+ class OperatorCall(Protocol[__T]):
+ def __call__(self, other: __T) -> 'TYPE_var': ...
+
+TV_fw_var = T.Union[str, int, bool, list, dict, 'InterpreterObject']
+TV_fw_args = T.List[T.Union[mparser.BaseNode, TV_fw_var]]
+TV_fw_kwargs = T.Dict[str, T.Union[mparser.BaseNode, TV_fw_var]]
+
+TV_func = T.TypeVar('TV_func', bound=T.Callable[..., T.Any])
+
+TYPE_elementary = T.Union[str, int, bool, T.List[T.Any], T.Dict[str, T.Any]]
+TYPE_var = T.Union[TYPE_elementary, HoldableObject, 'MesonInterpreterObject']
+TYPE_nvar = T.Union[TYPE_var, mparser.BaseNode]
+TYPE_kwargs = T.Dict[str, TYPE_var]
+TYPE_nkwargs = T.Dict[str, TYPE_nvar]
+TYPE_key_resolver = T.Callable[[mparser.BaseNode], str]
+
+SubProject = T.NewType('SubProject', str)
+
+class InterpreterObject:
+ def __init__(self, *, subproject: T.Optional['SubProject'] = None) -> None:
+ self.methods: T.Dict[
+ str,
+ T.Callable[[T.List[TYPE_var], TYPE_kwargs], TYPE_var]
+ ] = {}
+ self.operators: T.Dict[MesonOperator, 'OperatorCall'] = {}
+ self.trivial_operators: T.Dict[
+ MesonOperator,
+ T.Tuple[
+ T.Union[T.Type, T.Tuple[T.Type, ...]],
+ 'OperatorCall'
+ ]
+ ] = {}
+ # Current node set during a method call. This can be used as location
+ # when printing a warning message during a method call.
+ self.current_node: mparser.BaseNode = None
+ self.subproject = subproject or SubProject('')
+
+ # Some default operators supported by all objects
+ self.operators.update({
+ MesonOperator.EQUALS: self.op_equals,
+ MesonOperator.NOT_EQUALS: self.op_not_equals,
+ })
+
+ # The type of the object that can be printed to the user
+ def display_name(self) -> str:
+ return type(self).__name__
+
+ def method_call(
+ self,
+ method_name: str,
+ args: T.List[TYPE_var],
+ kwargs: TYPE_kwargs
+ ) -> TYPE_var:
+ if method_name in self.methods:
+ method = self.methods[method_name]
+ if not getattr(method, 'no-args-flattening', False):
+ args = flatten(args)
+ if not getattr(method, 'no-second-level-holder-flattening', False):
+ args, kwargs = resolve_second_level_holders(args, kwargs)
+ return method(args, kwargs)
+ raise InvalidCode(f'Unknown method "{method_name}" in object {self} of type {type(self).__name__}.')
+
+ def operator_call(self, operator: MesonOperator, other: TYPE_var) -> TYPE_var:
+ if operator in self.trivial_operators:
+ op = self.trivial_operators[operator]
+ if op[0] is None and other is not None:
+ raise MesonBugException(f'The unary operator `{operator.value}` of {self.display_name()} was passed the object {other} of type {type(other).__name__}')
+ if op[0] is not None and not isinstance(other, op[0]):
+ raise InvalidArguments(f'The `{operator.value}` operator of {self.display_name()} does not accept objects of type {type(other).__name__} ({other})')
+ return op[1](other)
+ if operator in self.operators:
+ return self.operators[operator](other)
+ raise InvalidCode(f'Object {self} of type {self.display_name()} does not support the `{operator.value}` operator.')
+
+ # Default comparison operator support
+ def _throw_comp_exception(self, other: TYPE_var, opt_type: str) -> T.NoReturn:
+ raise InvalidArguments(textwrap.dedent(
+ f'''
+ Trying to compare values of different types ({self.display_name()}, {type(other).__name__}) using {opt_type}.
+ This was deprecated and undefined behavior previously and is as of 0.60.0 a hard error.
+ '''
+ ))
+
+ def op_equals(self, other: TYPE_var) -> bool:
+ # We use `type(...) == type(...)` here to enforce an *exact* match for comparison. We
+ # don't want comparisons to be possible where `isinstance(derived_obj, type(base_obj))`
+ # would pass because this comparison must never be true: `derived_obj == base_obj`
+ if type(self) != type(other):
+ self._throw_comp_exception(other, '==')
+ return self == other
+
+ def op_not_equals(self, other: TYPE_var) -> bool:
+ if type(self) != type(other):
+ self._throw_comp_exception(other, '!=')
+ return self != other
+
+class MesonInterpreterObject(InterpreterObject):
+ ''' All non-elementary objects and non-object-holders should be derived from this '''
+
+class MutableInterpreterObject:
+ ''' Dummy class to mark the object type as mutable '''
+
+HoldableTypes = (HoldableObject, int, bool, str, list, dict)
+TYPE_HoldableTypes = T.Union[TYPE_elementary, HoldableObject]
+InterpreterObjectTypeVar = T.TypeVar('InterpreterObjectTypeVar', bound=TYPE_HoldableTypes)
+
+class ObjectHolder(InterpreterObject, T.Generic[InterpreterObjectTypeVar]):
+ def __init__(self, obj: InterpreterObjectTypeVar, interpreter: 'Interpreter') -> None:
+ super().__init__(subproject=interpreter.subproject)
+ # This causes some type checkers to assume that obj is a base
+ # HoldableObject, not the specialized type, so only do this assert in
+ # non-type checking situations
+ if not T.TYPE_CHECKING:
+ assert isinstance(obj, HoldableTypes), f'This is a bug: Trying to hold object of type `{type(obj).__name__}` that is not in `{HoldableTypes}`'
+ self.held_object = obj
+ self.interpreter = interpreter
+ self.env = self.interpreter.environment
+
+ # Hide the object holder abstraction from the user
+ def display_name(self) -> str:
+ return type(self.held_object).__name__
+
+ # Override default comparison operators for the held object
+ def op_equals(self, other: TYPE_var) -> bool:
+ # See the comment from InterpreterObject why we are using `type()` here.
+ if type(self.held_object) != type(other):
+ self._throw_comp_exception(other, '==')
+ return self.held_object == other
+
+ def op_not_equals(self, other: TYPE_var) -> bool:
+ if type(self.held_object) != type(other):
+ self._throw_comp_exception(other, '!=')
+ return self.held_object != other
+
+ def __repr__(self) -> str:
+ return f'<[{type(self).__name__}] holds [{type(self.held_object).__name__}]: {self.held_object!r}>'
+
+class IterableObject(metaclass=ABCMeta):
+ '''Base class for all objects that can be iterated over in a foreach loop'''
+
+ def iter_tuple_size(self) -> T.Optional[int]:
+ '''Return the size of the tuple for each iteration. Returns None if only a single value is returned.'''
+ raise MesonBugException(f'iter_tuple_size not implemented for {self.__class__.__name__}')
+
+ def iter_self(self) -> T.Iterator[T.Union[TYPE_var, T.Tuple[TYPE_var, ...]]]:
+ raise MesonBugException(f'iter not implemented for {self.__class__.__name__}')
+
+ def size(self) -> int:
+ raise MesonBugException(f'size not implemented for {self.__class__.__name__}')
diff --git a/mesonbuild/interpreterbase/decorators.py b/mesonbuild/interpreterbase/decorators.py
new file mode 100644
index 0000000..173cedc
--- /dev/null
+++ b/mesonbuild/interpreterbase/decorators.py
@@ -0,0 +1,791 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+from .. import mesonlib, mlog
+from .disabler import Disabler
+from .exceptions import InterpreterException, InvalidArguments
+from ._unholder import _unholder
+
+from dataclasses import dataclass
+from functools import wraps
+import abc
+import itertools
+import copy
+import typing as T
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Protocol
+
+ from .. import mparser
+ from .baseobjects import InterpreterObject, TV_func, TYPE_var, TYPE_kwargs
+ from .interpreterbase import SubProject
+ from .operator import MesonOperator
+
+ _TV_IntegerObject = T.TypeVar('_TV_IntegerObject', bound=InterpreterObject, contravariant=True)
+ _TV_ARG1 = T.TypeVar('_TV_ARG1', bound=TYPE_var, contravariant=True)
+
+ class FN_Operator(Protocol[_TV_IntegerObject, _TV_ARG1]):
+ def __call__(s, self: _TV_IntegerObject, other: _TV_ARG1) -> TYPE_var: ...
+ _TV_FN_Operator = T.TypeVar('_TV_FN_Operator', bound=FN_Operator)
+
+def get_callee_args(wrapped_args: T.Sequence[T.Any]) -> T.Tuple['mparser.BaseNode', T.List['TYPE_var'], 'TYPE_kwargs', 'SubProject']:
+ # First argument could be InterpreterBase, InterpreterObject or ModuleObject.
+ # In the case of a ModuleObject it is the 2nd argument (ModuleState) that
+ # contains the needed information.
+ s = wrapped_args[0]
+ if not hasattr(s, 'current_node'):
+ s = wrapped_args[1]
+ node = s.current_node
+ subproject = s.subproject
+ args = kwargs = None
+ if len(wrapped_args) >= 3:
+ args = wrapped_args[-2]
+ kwargs = wrapped_args[-1]
+ return node, args, kwargs, subproject
+
+def noPosargs(f: TV_func) -> TV_func:
+ @wraps(f)
+ def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
+ args = get_callee_args(wrapped_args)[1]
+ if args:
+ raise InvalidArguments('Function does not take positional arguments.')
+ return f(*wrapped_args, **wrapped_kwargs)
+ return T.cast('TV_func', wrapped)
+
+def noKwargs(f: TV_func) -> TV_func:
+ @wraps(f)
+ def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
+ kwargs = get_callee_args(wrapped_args)[2]
+ if kwargs:
+ raise InvalidArguments('Function does not take keyword arguments.')
+ return f(*wrapped_args, **wrapped_kwargs)
+ return T.cast('TV_func', wrapped)
+
+def stringArgs(f: TV_func) -> TV_func:
+ @wraps(f)
+ def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
+ args = get_callee_args(wrapped_args)[1]
+ if not isinstance(args, list):
+ mlog.debug('Not a list:', str(args))
+ raise InvalidArguments('Argument not a list.')
+ if not all(isinstance(s, str) for s in args):
+ mlog.debug('Element not a string:', str(args))
+ raise InvalidArguments('Arguments must be strings.')
+ return f(*wrapped_args, **wrapped_kwargs)
+ return T.cast('TV_func', wrapped)
+
+def noArgsFlattening(f: TV_func) -> TV_func:
+ setattr(f, 'no-args-flattening', True) # noqa: B010
+ return f
+
+def noSecondLevelHolderResolving(f: TV_func) -> TV_func:
+ setattr(f, 'no-second-level-holder-flattening', True) # noqa: B010
+ return f
+
+def unholder_return(f: TV_func) -> T.Callable[..., TYPE_var]:
+ @wraps(f)
+ def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
+ res = f(*wrapped_args, **wrapped_kwargs)
+ return _unholder(res)
+ return T.cast('T.Callable[..., TYPE_var]', wrapped)
+
+def disablerIfNotFound(f: TV_func) -> TV_func:
+ @wraps(f)
+ def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
+ kwargs = get_callee_args(wrapped_args)[2]
+ disabler = kwargs.pop('disabler', False)
+ ret = f(*wrapped_args, **wrapped_kwargs)
+ if disabler and not ret.found():
+ return Disabler()
+ return ret
+ return T.cast('TV_func', wrapped)
+
+@dataclass(repr=False, eq=False)
+class permittedKwargs:
+ permitted: T.Set[str]
+
+ def __call__(self, f: TV_func) -> TV_func:
+ @wraps(f)
+ def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
+ kwargs = get_callee_args(wrapped_args)[2]
+ unknowns = set(kwargs).difference(self.permitted)
+ if unknowns:
+ ustr = ', '.join([f'"{u}"' for u in sorted(unknowns)])
+ raise InvalidArguments(f'Got unknown keyword arguments {ustr}')
+ return f(*wrapped_args, **wrapped_kwargs)
+ return T.cast('TV_func', wrapped)
+
+def typed_operator(operator: MesonOperator,
+ types: T.Union[T.Type, T.Tuple[T.Type, ...]]) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']:
+ """Decorator that does type checking for operator calls.
+
+ The principle here is similar to typed_pos_args, however much simpler
+ since only one other object ever is passed
+ """
+ def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator':
+ @wraps(f)
+ def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var:
+ if not isinstance(other, types):
+ raise InvalidArguments(f'The `{operator.value}` of {self.display_name()} does not accept objects of type {type(other).__name__} ({other})')
+ return f(self, other)
+ return T.cast('_TV_FN_Operator', wrapper)
+ return inner
+
+def unary_operator(operator: MesonOperator) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']:
+ """Decorator that does type checking for unary operator calls.
+
+ This decorator is for unary operators that do not take any other objects.
+ It should be impossible for a user to accidentally break this. Triggering
+ this check always indicates a bug in the Meson interpreter.
+ """
+ def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator':
+ @wraps(f)
+ def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var:
+ if other is not None:
+ raise mesonlib.MesonBugException(f'The unary operator `{operator.value}` of {self.display_name()} was passed the object {other} of type {type(other).__name__}')
+ return f(self, other)
+ return T.cast('_TV_FN_Operator', wrapper)
+ return inner
+
+
+def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]],
+ varargs: T.Optional[T.Union[T.Type, T.Tuple[T.Type, ...]]] = None,
+ optargs: T.Optional[T.List[T.Union[T.Type, T.Tuple[T.Type, ...]]]] = None,
+ min_varargs: int = 0, max_varargs: int = 0) -> T.Callable[..., T.Any]:
+ """Decorator that types type checking of positional arguments.
+
+ This supports two different models of optional arguments, the first is the
+ variadic argument model. Variadic arguments are a possibly bounded,
+ possibly unbounded number of arguments of the same type (unions are
+ supported). The second is the standard default value model, in this case
+ a number of optional arguments may be provided, but they are still
+ ordered, and they may have different types.
+
+ This function does not support mixing variadic and default arguments.
+
+ :name: The name of the decorated function (as displayed in error messages)
+ :varargs: They type(s) of any variadic arguments the function takes. If
+ None the function takes no variadic args
+ :min_varargs: the minimum number of variadic arguments taken
+ :max_varargs: the maximum number of variadic arguments taken. 0 means unlimited
+ :optargs: The types of any optional arguments parameters taken. If None
+ then no optional parameters are taken.
+
+ Some examples of usage blow:
+ >>> @typed_pos_args('mod.func', str, (str, int))
+ ... def func(self, state: ModuleState, args: T.Tuple[str, T.Union[str, int]], kwargs: T.Dict[str, T.Any]) -> T.Any:
+ ... pass
+
+ >>> @typed_pos_args('method', str, varargs=str)
+ ... def method(self, node: BaseNode, args: T.Tuple[str, T.List[str]], kwargs: T.Dict[str, T.Any]) -> T.Any:
+ ... pass
+
+ >>> @typed_pos_args('method', varargs=str, min_varargs=1)
+ ... def method(self, node: BaseNode, args: T.Tuple[T.List[str]], kwargs: T.Dict[str, T.Any]) -> T.Any:
+ ... pass
+
+ >>> @typed_pos_args('method', str, optargs=[(str, int), str])
+ ... def method(self, node: BaseNode, args: T.Tuple[str, T.Optional[T.Union[str, int]], T.Optional[str]], kwargs: T.Dict[str, T.Any]) -> T.Any:
+ ... pass
+
+ When should you chose `typed_pos_args('name', varargs=str,
+ min_varargs=1)` vs `typed_pos_args('name', str, varargs=str)`?
+
+ The answer has to do with the semantics of the function, if all of the
+ inputs are the same type (such as with `files()`) then the former is
+ correct, all of the arguments are string names of files. If the first
+ argument is something else the it should be separated.
+ """
+ def inner(f: TV_func) -> TV_func:
+
+ @wraps(f)
+ def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
+ args = get_callee_args(wrapped_args)[1]
+
+ # These are implementation programming errors, end users should never see them.
+ assert isinstance(args, list), args
+ assert max_varargs >= 0, 'max_varags cannot be negative'
+ assert min_varargs >= 0, 'min_varags cannot be negative'
+ assert optargs is None or varargs is None, \
+ 'varargs and optargs not supported together as this would be ambiguous'
+
+ num_args = len(args)
+ num_types = len(types)
+ a_types = types
+
+ if varargs:
+ min_args = num_types + min_varargs
+ max_args = num_types + max_varargs
+ if max_varargs == 0 and num_args < min_args:
+ raise InvalidArguments(f'{name} takes at least {min_args} arguments, but got {num_args}.')
+ elif max_varargs != 0 and (num_args < min_args or num_args > max_args):
+ raise InvalidArguments(f'{name} takes between {min_args} and {max_args} arguments, but got {num_args}.')
+ elif optargs:
+ if num_args < num_types:
+ raise InvalidArguments(f'{name} takes at least {num_types} arguments, but got {num_args}.')
+ elif num_args > num_types + len(optargs):
+ raise InvalidArguments(f'{name} takes at most {num_types + len(optargs)} arguments, but got {num_args}.')
+ # Add the number of positional arguments required
+ if num_args > num_types:
+ diff = num_args - num_types
+ a_types = tuple(list(types) + list(optargs[:diff]))
+ elif num_args != num_types:
+ raise InvalidArguments(f'{name} takes exactly {num_types} arguments, but got {num_args}.')
+
+ for i, (arg, type_) in enumerate(itertools.zip_longest(args, a_types, fillvalue=varargs), start=1):
+ if not isinstance(arg, type_):
+ if isinstance(type_, tuple):
+ shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in type_))
+ else:
+ shouldbe = f'"{type_.__name__}"'
+ raise InvalidArguments(f'{name} argument {i} was of type "{type(arg).__name__}" but should have been {shouldbe}')
+
+ # Ensure that we're actually passing a tuple.
+ # Depending on what kind of function we're calling the length of
+ # wrapped_args can vary.
+ nargs = list(wrapped_args)
+ i = nargs.index(args)
+ if varargs:
+ # if we have varargs we need to split them into a separate
+ # tuple, as python's typing doesn't understand tuples with
+ # fixed elements and variadic elements, only one or the other.
+ # so in that case we need T.Tuple[int, str, float, T.Tuple[str, ...]]
+ pos = args[:len(types)]
+ var = list(args[len(types):])
+ pos.append(var)
+ nargs[i] = tuple(pos)
+ elif optargs:
+ if num_args < num_types + len(optargs):
+ diff = num_types + len(optargs) - num_args
+ nargs[i] = tuple(list(args) + [None] * diff)
+ else:
+ nargs[i] = args
+ else:
+ nargs[i] = tuple(args)
+ return f(*nargs, **wrapped_kwargs)
+
+ return T.cast('TV_func', wrapper)
+ return inner
+
+
+class ContainerTypeInfo:
+
+ """Container information for keyword arguments.
+
+ For keyword arguments that are containers (list or dict), this class encodes
+ that information.
+
+ :param container: the type of container
+ :param contains: the types the container holds
+ :param pairs: if the container is supposed to be of even length.
+ This is mainly used for interfaces that predate the addition of dictionaries, and use
+ `[key, value, key2, value2]` format.
+ :param allow_empty: Whether this container is allowed to be empty
+ There are some cases where containers not only must be passed, but must
+ not be empty, and other cases where an empty container is allowed.
+ """
+
+ def __init__(self, container: T.Type, contains: T.Union[T.Type, T.Tuple[T.Type, ...]], *,
+ pairs: bool = False, allow_empty: bool = True):
+ self.container = container
+ self.contains = contains
+ self.pairs = pairs
+ self.allow_empty = allow_empty
+
+ def check(self, value: T.Any) -> bool:
+ """Check that a value is valid.
+
+ :param value: A value to check
+ :return: True if it is valid, False otherwise
+ """
+ if not isinstance(value, self.container):
+ return False
+ iter_ = iter(value.values()) if isinstance(value, dict) else iter(value)
+ for each in iter_:
+ if not isinstance(each, self.contains):
+ return False
+ if self.pairs and len(value) % 2 != 0:
+ return False
+ if not value and not self.allow_empty:
+ return False
+ return True
+
+ def description(self) -> str:
+ """Human readable description of this container type.
+
+ :return: string to be printed
+ """
+ container = 'dict' if self.container is dict else 'array'
+ if isinstance(self.contains, tuple):
+ contains = ' | '.join([t.__name__ for t in self.contains])
+ else:
+ contains = self.contains.__name__
+ s = f'{container}[{contains}]'
+ if self.pairs:
+ s += ' that has even size'
+ if not self.allow_empty:
+ s += ' that cannot be empty'
+ return s
+
+_T = T.TypeVar('_T')
+
+class _NULL_T:
+ """Special null type for evolution, this is an implementation detail."""
+
+
+_NULL = _NULL_T()
+
+class KwargInfo(T.Generic[_T]):
+
+ """A description of a keyword argument to a meson function
+
+ This is used to describe a value to the :func:typed_kwargs function.
+
+ :param name: the name of the parameter
+ :param types: A type or tuple of types that are allowed, or a :class:ContainerType
+ :param required: Whether this is a required keyword argument. defaults to False
+ :param listify: If true, then the argument will be listified before being
+ checked. This is useful for cases where the Meson DSL allows a scalar or
+ a container, but internally we only want to work with containers
+ :param default: A default value to use if this isn't set. defaults to None,
+ this may be safely set to a mutable type, as long as that type does not
+ itself contain mutable types, typed_kwargs will copy the default
+ :param since: Meson version in which this argument has been added. defaults to None
+ :param since_message: An extra message to pass to FeatureNew when since is triggered
+ :param deprecated: Meson version in which this argument has been deprecated. defaults to None
+ :param deprecated_message: An extra message to pass to FeatureDeprecated
+ when since is triggered
+ :param validator: A callable that does additional validation. This is mainly
+ intended for cases where a string is expected, but only a few specific
+ values are accepted. Must return None if the input is valid, or a
+ message if the input is invalid
+ :param convertor: A callable that converts the raw input value into a
+ different type. This is intended for cases such as the meson DSL using a
+ string, but the implementation using an Enum. This should not do
+ validation, just conversion.
+ :param deprecated_values: a dictionary mapping a value to the version of
+ meson it was deprecated in. The Value may be any valid value for this
+ argument.
+ :param since_values: a dictionary mapping a value to the version of meson it was
+ added in.
+ :param not_set_warning: A warning message that is logged if the kwarg is not
+ set by the user.
+ :param feature_validator: A callable returning an iterable of FeatureNew | FeatureDeprecated objects.
+ """
+ def __init__(self, name: str,
+ types: T.Union[T.Type[_T], T.Tuple[T.Union[T.Type[_T], ContainerTypeInfo], ...], ContainerTypeInfo],
+ *, required: bool = False, listify: bool = False,
+ default: T.Optional[_T] = None,
+ since: T.Optional[str] = None,
+ since_message: T.Optional[str] = None,
+ since_values: T.Optional[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]]] = None,
+ deprecated: T.Optional[str] = None,
+ deprecated_message: T.Optional[str] = None,
+ deprecated_values: T.Optional[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]]] = None,
+ feature_validator: T.Optional[T.Callable[[_T], T.Iterable[FeatureCheckBase]]] = None,
+ validator: T.Optional[T.Callable[[T.Any], T.Optional[str]]] = None,
+ convertor: T.Optional[T.Callable[[_T], object]] = None,
+ not_set_warning: T.Optional[str] = None):
+ self.name = name
+ self.types = types
+ self.required = required
+ self.listify = listify
+ self.default = default
+ self.since = since
+ self.since_message = since_message
+ self.since_values = since_values
+ self.feature_validator = feature_validator
+ self.deprecated = deprecated
+ self.deprecated_message = deprecated_message
+ self.deprecated_values = deprecated_values
+ self.validator = validator
+ self.convertor = convertor
+ self.not_set_warning = not_set_warning
+
+ def evolve(self, *,
+ name: T.Union[str, _NULL_T] = _NULL,
+ required: T.Union[bool, _NULL_T] = _NULL,
+ listify: T.Union[bool, _NULL_T] = _NULL,
+ default: T.Union[_T, None, _NULL_T] = _NULL,
+ since: T.Union[str, None, _NULL_T] = _NULL,
+ since_message: T.Union[str, None, _NULL_T] = _NULL,
+ since_values: T.Union[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]], None, _NULL_T] = _NULL,
+ deprecated: T.Union[str, None, _NULL_T] = _NULL,
+ deprecated_message: T.Union[str, None, _NULL_T] = _NULL,
+ deprecated_values: T.Union[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]], None, _NULL_T] = _NULL,
+ feature_validator: T.Union[T.Callable[[_T], T.Iterable[FeatureCheckBase]], None, _NULL_T] = _NULL,
+ validator: T.Union[T.Callable[[_T], T.Optional[str]], None, _NULL_T] = _NULL,
+ convertor: T.Union[T.Callable[[_T], TYPE_var], None, _NULL_T] = _NULL) -> 'KwargInfo':
+ """Create a shallow copy of this KwargInfo, with modifications.
+
+ This allows us to create a new copy of a KwargInfo with modifications.
+ This allows us to use a shared kwarg that implements complex logic, but
+ has slight differences in usage, such as being added to different
+ functions in different versions of Meson.
+
+ The use the _NULL special value here allows us to pass None, which has
+ meaning in many of these cases. _NULL itself is never stored, always
+ being replaced by either the copy in self, or the provided new version.
+ """
+ return type(self)(
+ name if not isinstance(name, _NULL_T) else self.name,
+ self.types,
+ listify=listify if not isinstance(listify, _NULL_T) else self.listify,
+ required=required if not isinstance(required, _NULL_T) else self.required,
+ default=default if not isinstance(default, _NULL_T) else self.default,
+ since=since if not isinstance(since, _NULL_T) else self.since,
+ since_message=since_message if not isinstance(since_message, _NULL_T) else self.since_message,
+ since_values=since_values if not isinstance(since_values, _NULL_T) else self.since_values,
+ deprecated=deprecated if not isinstance(deprecated, _NULL_T) else self.deprecated,
+ deprecated_message=deprecated_message if not isinstance(deprecated_message, _NULL_T) else self.deprecated_message,
+ deprecated_values=deprecated_values if not isinstance(deprecated_values, _NULL_T) else self.deprecated_values,
+ feature_validator=feature_validator if not isinstance(feature_validator, _NULL_T) else self.feature_validator,
+ validator=validator if not isinstance(validator, _NULL_T) else self.validator,
+ convertor=convertor if not isinstance(convertor, _NULL_T) else self.convertor,
+ )
+
+
+def typed_kwargs(name: str, *types: KwargInfo, allow_unknown: bool = False) -> T.Callable[..., T.Any]:
+ """Decorator for type checking keyword arguments.
+
+ Used to wrap a meson DSL implementation function, where it checks various
+ things about keyword arguments, including the type, and various other
+ information. For non-required values it sets the value to a default, which
+ means the value will always be provided.
+
+ If type tyhpe is a :class:ContainerTypeInfo, then the default value will be
+ passed as an argument to the container initializer, making a shallow copy
+
+ :param name: the name of the function, including the object it's attached to
+ (if applicable)
+ :param *types: KwargInfo entries for each keyword argument.
+ """
+ def inner(f: TV_func) -> TV_func:
+
+ def types_description(types_tuple: T.Tuple[T.Union[T.Type, ContainerTypeInfo], ...]) -> str:
+ candidates = []
+ for t in types_tuple:
+ if isinstance(t, ContainerTypeInfo):
+ candidates.append(t.description())
+ else:
+ candidates.append(t.__name__)
+ shouldbe = 'one of: ' if len(candidates) > 1 else ''
+ shouldbe += ', '.join(candidates)
+ return shouldbe
+
+ def raw_description(t: object) -> str:
+ """describe a raw type (ie, one that is not a ContainerTypeInfo)."""
+ if isinstance(t, list):
+ if t:
+ return f"array[{' | '.join(sorted(mesonlib.OrderedSet(type(v).__name__ for v in t)))}]"
+ return 'array[]'
+ elif isinstance(t, dict):
+ if t:
+ return f"dict[{' | '.join(sorted(mesonlib.OrderedSet(type(v).__name__ for v in t.values())))}]"
+ return 'dict[]'
+ return type(t).__name__
+
+ def check_value_type(types_tuple: T.Tuple[T.Union[T.Type, ContainerTypeInfo], ...],
+ value: T.Any) -> bool:
+ for t in types_tuple:
+ if isinstance(t, ContainerTypeInfo):
+ if t.check(value):
+ return True
+ elif isinstance(value, t):
+ return True
+ return False
+
+ @wraps(f)
+ def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
+
+ def emit_feature_change(values: T.Dict[_T, T.Union[str, T.Tuple[str, str]]], feature: T.Union[T.Type['FeatureDeprecated'], T.Type['FeatureNew']]) -> None:
+ for n, version in values.items():
+ warn = False
+ if isinstance(version, tuple):
+ version, msg = version
+ else:
+ msg = None
+
+ if n in {dict, list}:
+ assert isinstance(n, type), 'for mypy'
+ if isinstance(value, n):
+ feature.single_use(f'"{name}" keyword argument "{info.name}" of type {n.__name__}', version, subproject, msg, location=node)
+ elif isinstance(value, (dict, list)):
+ warn = n in value
+ else:
+ warn = n == value
+
+ if warn:
+ feature.single_use(f'"{name}" keyword argument "{info.name}" value "{n}"', version, subproject, msg, location=node)
+
+ node, _, _kwargs, subproject = get_callee_args(wrapped_args)
+ # Cast here, as the convertor function may place something other than a TYPE_var in the kwargs
+ kwargs = T.cast('T.Dict[str, object]', _kwargs)
+
+ if not allow_unknown:
+ all_names = {t.name for t in types}
+ unknowns = set(kwargs).difference(all_names)
+ if unknowns:
+ ustr = ', '.join([f'"{u}"' for u in sorted(unknowns)])
+ raise InvalidArguments(f'{name} got unknown keyword arguments {ustr}')
+
+ for info in types:
+ types_tuple = info.types if isinstance(info.types, tuple) else (info.types,)
+ value = kwargs.get(info.name)
+ if value is not None:
+ if info.since:
+ feature_name = info.name + ' arg in ' + name
+ FeatureNew.single_use(feature_name, info.since, subproject, info.since_message, location=node)
+ if info.deprecated:
+ feature_name = info.name + ' arg in ' + name
+ FeatureDeprecated.single_use(feature_name, info.deprecated, subproject, info.deprecated_message, location=node)
+ if info.listify:
+ kwargs[info.name] = value = mesonlib.listify(value)
+ if not check_value_type(types_tuple, value):
+ shouldbe = types_description(types_tuple)
+ raise InvalidArguments(f'{name} keyword argument {info.name!r} was of type {raw_description(value)} but should have been {shouldbe}')
+
+ if info.validator is not None:
+ msg = info.validator(value)
+ if msg is not None:
+ raise InvalidArguments(f'{name} keyword argument "{info.name}" {msg}')
+
+ if info.feature_validator is not None:
+ for each in info.feature_validator(value):
+ each.use(subproject, node)
+
+ if info.deprecated_values is not None:
+ emit_feature_change(info.deprecated_values, FeatureDeprecated)
+
+ if info.since_values is not None:
+ emit_feature_change(info.since_values, FeatureNew)
+
+ elif info.required:
+ raise InvalidArguments(f'{name} is missing required keyword argument "{info.name}"')
+ else:
+ # set the value to the default, this ensuring all kwargs are present
+ # This both simplifies the typing checking and the usage
+ assert check_value_type(types_tuple, info.default), f'In funcion {name} default value of {info.name} is not a valid type, got {type(info.default)} expected {types_description(types_tuple)}'
+ # Create a shallow copy of the container. This allows mutable
+ # types to be used safely as default values
+ kwargs[info.name] = copy.copy(info.default)
+ if info.not_set_warning:
+ mlog.warning(info.not_set_warning)
+
+ if info.convertor:
+ kwargs[info.name] = info.convertor(kwargs[info.name])
+
+ return f(*wrapped_args, **wrapped_kwargs)
+ return T.cast('TV_func', wrapper)
+ return inner
+
+
+# This cannot be a dataclass due to https://github.com/python/mypy/issues/5374
+class FeatureCheckBase(metaclass=abc.ABCMeta):
+ "Base class for feature version checks"
+
+ feature_registry: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[T.Tuple[str, T.Optional['mparser.BaseNode']]]]]]
+ emit_notice = False
+
+ def __init__(self, feature_name: str, feature_version: str, extra_message: str = ''):
+ self.feature_name = feature_name # type: str
+ self.feature_version = feature_version # type: str
+ self.extra_message = extra_message # type: str
+
+ @staticmethod
+ def get_target_version(subproject: str) -> str:
+ # Don't do any checks if project() has not been parsed yet
+ if subproject not in mesonlib.project_meson_versions:
+ return ''
+ return mesonlib.project_meson_versions[subproject]
+
+ @staticmethod
+ @abc.abstractmethod
+ def check_version(target_version: str, feature_version: str) -> bool:
+ pass
+
+ def use(self, subproject: 'SubProject', location: T.Optional['mparser.BaseNode'] = None) -> None:
+ tv = self.get_target_version(subproject)
+ # No target version
+ if tv == '':
+ return
+ # Target version is new enough, don't warn
+ if self.check_version(tv, self.feature_version) and not self.emit_notice:
+ return
+ # Feature is too new for target version or we want to emit notices, register it
+ if subproject not in self.feature_registry:
+ self.feature_registry[subproject] = {self.feature_version: set()}
+ register = self.feature_registry[subproject]
+ if self.feature_version not in register:
+ register[self.feature_version] = set()
+
+ feature_key = (self.feature_name, location)
+ if feature_key in register[self.feature_version]:
+ # Don't warn about the same feature multiple times
+ # FIXME: This is needed to prevent duplicate warnings, but also
+ # means we won't warn about a feature used in multiple places.
+ return
+ register[self.feature_version].add(feature_key)
+ # Target version is new enough, don't warn even if it is registered for notice
+ if self.check_version(tv, self.feature_version):
+ return
+ self.log_usage_warning(tv, location)
+
+ @classmethod
+ def report(cls, subproject: str) -> None:
+ if subproject not in cls.feature_registry:
+ return
+ warning_str = cls.get_warning_str_prefix(cls.get_target_version(subproject))
+ notice_str = cls.get_notice_str_prefix(cls.get_target_version(subproject))
+ fv = cls.feature_registry[subproject]
+ tv = cls.get_target_version(subproject)
+ for version in sorted(fv.keys()):
+ if cls.check_version(tv, version):
+ notice_str += '\n * {}: {}'.format(version, {i[0] for i in fv[version]})
+ else:
+ warning_str += '\n * {}: {}'.format(version, {i[0] for i in fv[version]})
+ if '\n' in notice_str:
+ mlog.notice(notice_str, fatal=False)
+ if '\n' in warning_str:
+ mlog.warning(warning_str)
+
+ def log_usage_warning(self, tv: str, location: T.Optional['mparser.BaseNode']) -> None:
+ raise InterpreterException('log_usage_warning not implemented')
+
+ @staticmethod
+ def get_warning_str_prefix(tv: str) -> str:
+ raise InterpreterException('get_warning_str_prefix not implemented')
+
+ @staticmethod
+ def get_notice_str_prefix(tv: str) -> str:
+ raise InterpreterException('get_notice_str_prefix not implemented')
+
+ def __call__(self, f: TV_func) -> TV_func:
+ @wraps(f)
+ def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
+ node, _, _, subproject = get_callee_args(wrapped_args)
+ if subproject is None:
+ raise AssertionError(f'{wrapped_args!r}')
+ self.use(subproject, node)
+ return f(*wrapped_args, **wrapped_kwargs)
+ return T.cast('TV_func', wrapped)
+
+ @classmethod
+ def single_use(cls, feature_name: str, version: str, subproject: 'SubProject',
+ extra_message: str = '', location: T.Optional['mparser.BaseNode'] = None) -> None:
+ """Oneline version that instantiates and calls use()."""
+ cls(feature_name, version, extra_message).use(subproject, location)
+
+
+class FeatureNew(FeatureCheckBase):
+ """Checks for new features"""
+
+ # Class variable, shared across all instances
+ #
+ # Format: {subproject: {feature_version: set(feature_names)}}
+ feature_registry = {} # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[T.Tuple[str, T.Optional[mparser.BaseNode]]]]]]
+
+ @staticmethod
+ def check_version(target_version: str, feature_version: str) -> bool:
+ return mesonlib.version_compare_condition_with_min(target_version, feature_version)
+
+ @staticmethod
+ def get_warning_str_prefix(tv: str) -> str:
+ return f'Project specifies a minimum meson_version \'{tv}\' but uses features which were added in newer versions:'
+
+ @staticmethod
+ def get_notice_str_prefix(tv: str) -> str:
+ return ''
+
+ def log_usage_warning(self, tv: str, location: T.Optional['mparser.BaseNode']) -> None:
+ args = [
+ 'Project targets', f"'{tv}'",
+ 'but uses feature introduced in',
+ f"'{self.feature_version}':",
+ f'{self.feature_name}.',
+ ]
+ if self.extra_message:
+ args.append(self.extra_message)
+ mlog.warning(*args, location=location)
+
+class FeatureDeprecated(FeatureCheckBase):
+ """Checks for deprecated features"""
+
+ # Class variable, shared across all instances
+ #
+ # Format: {subproject: {feature_version: set(feature_names)}}
+ feature_registry = {} # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[T.Tuple[str, T.Optional[mparser.BaseNode]]]]]]
+ emit_notice = True
+
+ @staticmethod
+ def check_version(target_version: str, feature_version: str) -> bool:
+ # For deprecation checks we need to return the inverse of FeatureNew checks
+ return not mesonlib.version_compare_condition_with_min(target_version, feature_version)
+
+ @staticmethod
+ def get_warning_str_prefix(tv: str) -> str:
+ return 'Deprecated features used:'
+
+ @staticmethod
+ def get_notice_str_prefix(tv: str) -> str:
+ return 'Future-deprecated features used:'
+
+ def log_usage_warning(self, tv: str, location: T.Optional['mparser.BaseNode']) -> None:
+ args = [
+ 'Project targets', f"'{tv}'",
+ 'but uses feature deprecated since',
+ f"'{self.feature_version}':",
+ f'{self.feature_name}.',
+ ]
+ if self.extra_message:
+ args.append(self.extra_message)
+ mlog.warning(*args, location=location)
+
+
+# This cannot be a dataclass due to https://github.com/python/mypy/issues/5374
+class FeatureCheckKwargsBase(metaclass=abc.ABCMeta):
+
+ @property
+ @abc.abstractmethod
+ def feature_check_class(self) -> T.Type[FeatureCheckBase]:
+ pass
+
+ def __init__(self, feature_name: str, feature_version: str,
+ kwargs: T.List[str], extra_message: T.Optional[str] = None):
+ self.feature_name = feature_name
+ self.feature_version = feature_version
+ self.kwargs = kwargs
+ self.extra_message = extra_message
+
+ def __call__(self, f: TV_func) -> TV_func:
+ @wraps(f)
+ def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
+ node, _, kwargs, subproject = get_callee_args(wrapped_args)
+ if subproject is None:
+ raise AssertionError(f'{wrapped_args!r}')
+ for arg in self.kwargs:
+ if arg not in kwargs:
+ continue
+ name = arg + ' arg in ' + self.feature_name
+ self.feature_check_class.single_use(
+ name, self.feature_version, subproject, self.extra_message, node)
+ return f(*wrapped_args, **wrapped_kwargs)
+ return T.cast('TV_func', wrapped)
+
+class FeatureNewKwargs(FeatureCheckKwargsBase):
+ feature_check_class = FeatureNew
+
+class FeatureDeprecatedKwargs(FeatureCheckKwargsBase):
+ feature_check_class = FeatureDeprecated
diff --git a/mesonbuild/interpreterbase/disabler.py b/mesonbuild/interpreterbase/disabler.py
new file mode 100644
index 0000000..182bb62
--- /dev/null
+++ b/mesonbuild/interpreterbase/disabler.py
@@ -0,0 +1,45 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+import typing as T
+
+from .baseobjects import MesonInterpreterObject
+
+if T.TYPE_CHECKING:
+ from .baseobjects import TYPE_var, TYPE_kwargs
+
+class Disabler(MesonInterpreterObject):
+ def method_call(self, method_name: str, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> TYPE_var:
+ if method_name == 'found':
+ return False
+ return Disabler()
+
+def _is_arg_disabled(arg: T.Any) -> bool:
+ if isinstance(arg, Disabler):
+ return True
+ if isinstance(arg, list):
+ for i in arg:
+ if _is_arg_disabled(i):
+ return True
+ return False
+
+def is_disabled(args: T.Sequence[T.Any], kwargs: T.Dict[str, T.Any]) -> bool:
+ for i in args:
+ if _is_arg_disabled(i):
+ return True
+ for i in kwargs.values():
+ if _is_arg_disabled(i):
+ return True
+ return False
diff --git a/mesonbuild/interpreterbase/exceptions.py b/mesonbuild/interpreterbase/exceptions.py
new file mode 100644
index 0000000..cdbe0fb
--- /dev/null
+++ b/mesonbuild/interpreterbase/exceptions.py
@@ -0,0 +1,33 @@
+# Copyright 2013-2021 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.
+
+from ..mesonlib import MesonException
+
+class InterpreterException(MesonException):
+ pass
+
+class InvalidCode(InterpreterException):
+ pass
+
+class InvalidArguments(InterpreterException):
+ pass
+
+class SubdirDoneRequest(BaseException):
+ pass
+
+class ContinueRequest(BaseException):
+ pass
+
+class BreakRequest(BaseException):
+ pass
diff --git a/mesonbuild/interpreterbase/helpers.py b/mesonbuild/interpreterbase/helpers.py
new file mode 100644
index 0000000..2196b4e
--- /dev/null
+++ b/mesonbuild/interpreterbase/helpers.py
@@ -0,0 +1,56 @@
+# Copyright 2013-2021 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.
+from __future__ import annotations
+
+from .. import mesonlib, mparser
+from .exceptions import InterpreterException
+
+import collections.abc
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .baseobjects import TYPE_var, TYPE_kwargs
+
+def flatten(args: T.Union['TYPE_var', T.List['TYPE_var']]) -> T.List['TYPE_var']:
+ if isinstance(args, mparser.StringNode):
+ assert isinstance(args.value, str)
+ return [args.value]
+ if not isinstance(args, collections.abc.Sequence):
+ return [args]
+ result: T.List['TYPE_var'] = []
+ for a in args:
+ if isinstance(a, list):
+ rest = flatten(a)
+ result = result + rest
+ elif isinstance(a, mparser.StringNode):
+ result.append(a.value)
+ else:
+ result.append(a)
+ return result
+
+def resolve_second_level_holders(args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> T.Tuple[T.List['TYPE_var'], 'TYPE_kwargs']:
+ def resolver(arg: 'TYPE_var') -> 'TYPE_var':
+ if isinstance(arg, list):
+ return [resolver(x) for x in arg]
+ if isinstance(arg, dict):
+ return {k: resolver(v) for k, v in arg.items()}
+ if isinstance(arg, mesonlib.SecondLevelHolder):
+ return arg.get_default_object()
+ return arg
+ return [resolver(x) for x in args], {k: resolver(v) for k, v in kwargs.items()}
+
+def default_resolve_key(key: mparser.BaseNode) -> str:
+ if not isinstance(key, mparser.IdNode):
+ raise InterpreterException('Invalid kwargs format.')
+ return key.value
diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py
new file mode 100644
index 0000000..f72ddc1
--- /dev/null
+++ b/mesonbuild/interpreterbase/interpreterbase.py
@@ -0,0 +1,604 @@
+# Copyright 2016-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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool.
+from __future__ import annotations
+
+from .. import mparser, mesonlib
+from .. import environment
+
+from .baseobjects import (
+ InterpreterObject,
+ MesonInterpreterObject,
+ MutableInterpreterObject,
+ InterpreterObjectTypeVar,
+ ObjectHolder,
+ IterableObject,
+
+ HoldableTypes,
+)
+
+from .exceptions import (
+ InterpreterException,
+ InvalidCode,
+ InvalidArguments,
+ SubdirDoneRequest,
+ ContinueRequest,
+ BreakRequest
+)
+
+from .decorators import FeatureNew
+from .disabler import Disabler, is_disabled
+from .helpers import default_resolve_key, flatten, resolve_second_level_holders
+from .operator import MesonOperator
+from ._unholder import _unholder
+
+import os, copy, re, pathlib
+import typing as T
+import textwrap
+
+if T.TYPE_CHECKING:
+ from .baseobjects import SubProject, TYPE_kwargs, TYPE_var
+ from ..interpreter import Interpreter
+
+ HolderMapType = T.Dict[
+ T.Union[
+ T.Type[mesonlib.HoldableObject],
+ T.Type[int],
+ T.Type[bool],
+ T.Type[str],
+ T.Type[list],
+ T.Type[dict],
+ ],
+ # For some reason, this has to be a callable and can't just be ObjectHolder[InterpreterObjectTypeVar]
+ T.Callable[[InterpreterObjectTypeVar, 'Interpreter'], ObjectHolder[InterpreterObjectTypeVar]]
+ ]
+
+ FunctionType = T.Dict[
+ str,
+ T.Callable[[mparser.BaseNode, T.List[TYPE_var], T.Dict[str, TYPE_var]], TYPE_var]
+ ]
+
+class InterpreterBase:
+ def __init__(self, source_root: str, subdir: str, subproject: 'SubProject'):
+ self.source_root = source_root
+ self.funcs: FunctionType = {}
+ self.builtin: T.Dict[str, InterpreterObject] = {}
+ # Holder maps store a mapping from an HoldableObject to a class ObjectHolder
+ self.holder_map: HolderMapType = {}
+ self.bound_holder_map: HolderMapType = {}
+ self.subdir = subdir
+ self.root_subdir = subdir
+ self.subproject = subproject
+ self.variables: T.Dict[str, InterpreterObject] = {}
+ self.argument_depth = 0
+ self.current_lineno = -1
+ # Current node set during a function call. This can be used as location
+ # when printing a warning message during a method call.
+ self.current_node = None # type: mparser.BaseNode
+ # This is set to `version_string` when this statement is evaluated:
+ # meson.version().compare_version(version_string)
+ # If it was part of a if-clause, it is used to temporally override the
+ # current meson version target within that if-block.
+ self.tmp_meson_version = None # type: T.Optional[str]
+
+ def load_root_meson_file(self) -> None:
+ mesonfile = os.path.join(self.source_root, self.subdir, environment.build_filename)
+ if not os.path.isfile(mesonfile):
+ raise InvalidArguments(f'Missing Meson file in {mesonfile}')
+ with open(mesonfile, encoding='utf-8') as mf:
+ code = mf.read()
+ if code.isspace():
+ raise InvalidCode('Builder file is empty.')
+ assert isinstance(code, str)
+ try:
+ self.ast = mparser.Parser(code, mesonfile).parse()
+ except mesonlib.MesonException as me:
+ me.file = mesonfile
+ raise me
+
+ def parse_project(self) -> None:
+ """
+ Parses project() and initializes languages, compilers etc. Do this
+ early because we need this before we parse the rest of the AST.
+ """
+ self.evaluate_codeblock(self.ast, end=1)
+
+ def sanity_check_ast(self) -> None:
+ if not isinstance(self.ast, mparser.CodeBlockNode):
+ raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.')
+ if not self.ast.lines:
+ raise InvalidCode('No statements in code.')
+ first = self.ast.lines[0]
+ if not isinstance(first, mparser.FunctionNode) or first.func_name != 'project':
+ p = pathlib.Path(self.source_root).resolve()
+ found = p
+ for parent in p.parents:
+ if (parent / 'meson.build').is_file():
+ with open(parent / 'meson.build', encoding='utf-8') as f:
+ if f.readline().startswith('project('):
+ found = parent
+ break
+ else:
+ break
+
+ error = 'first statement must be a call to project()'
+ if found != p:
+ raise InvalidCode(f'Not the project root: {error}\n\nDid you mean to run meson from the directory: "{found}"?')
+ else:
+ raise InvalidCode(f'Invalid source tree: {error}')
+
+ def run(self) -> None:
+ # Evaluate everything after the first line, which is project() because
+ # we already parsed that in self.parse_project()
+ try:
+ self.evaluate_codeblock(self.ast, start=1)
+ except SubdirDoneRequest:
+ pass
+
+ def evaluate_codeblock(self, node: mparser.CodeBlockNode, start: int = 0, end: T.Optional[int] = None) -> None:
+ if node is None:
+ return
+ if not isinstance(node, mparser.CodeBlockNode):
+ e = InvalidCode('Tried to execute a non-codeblock. Possibly a bug in the parser.')
+ e.lineno = node.lineno
+ e.colno = node.colno
+ raise e
+ statements = node.lines[start:end]
+ i = 0
+ while i < len(statements):
+ cur = statements[i]
+ try:
+ self.current_lineno = cur.lineno
+ self.evaluate_statement(cur)
+ except Exception as e:
+ if getattr(e, 'lineno', None) is None:
+ # We are doing the equivalent to setattr here and mypy does not like it
+ e.lineno = cur.lineno # type: ignore
+ e.colno = cur.colno # type: ignore
+ e.file = os.path.join(self.source_root, self.subdir, environment.build_filename) # type: ignore
+ raise e
+ i += 1 # In THE FUTURE jump over blocks and stuff.
+
+ def evaluate_statement(self, cur: mparser.BaseNode) -> T.Optional[InterpreterObject]:
+ self.current_node = cur
+ if isinstance(cur, mparser.FunctionNode):
+ return self.function_call(cur)
+ elif isinstance(cur, mparser.AssignmentNode):
+ self.assignment(cur)
+ elif isinstance(cur, mparser.MethodNode):
+ return self.method_call(cur)
+ elif isinstance(cur, mparser.StringNode):
+ return self._holderify(cur.value)
+ elif isinstance(cur, mparser.BooleanNode):
+ return self._holderify(cur.value)
+ elif isinstance(cur, mparser.IfClauseNode):
+ return self.evaluate_if(cur)
+ elif isinstance(cur, mparser.IdNode):
+ return self.get_variable(cur.value)
+ elif isinstance(cur, mparser.ComparisonNode):
+ return self.evaluate_comparison(cur)
+ elif isinstance(cur, mparser.ArrayNode):
+ return self.evaluate_arraystatement(cur)
+ elif isinstance(cur, mparser.DictNode):
+ return self.evaluate_dictstatement(cur)
+ elif isinstance(cur, mparser.NumberNode):
+ return self._holderify(cur.value)
+ elif isinstance(cur, mparser.AndNode):
+ return self.evaluate_andstatement(cur)
+ elif isinstance(cur, mparser.OrNode):
+ return self.evaluate_orstatement(cur)
+ elif isinstance(cur, mparser.NotNode):
+ return self.evaluate_notstatement(cur)
+ elif isinstance(cur, mparser.UMinusNode):
+ return self.evaluate_uminusstatement(cur)
+ elif isinstance(cur, mparser.ArithmeticNode):
+ return self.evaluate_arithmeticstatement(cur)
+ elif isinstance(cur, mparser.ForeachClauseNode):
+ self.evaluate_foreach(cur)
+ elif isinstance(cur, mparser.PlusAssignmentNode):
+ self.evaluate_plusassign(cur)
+ elif isinstance(cur, mparser.IndexNode):
+ return self.evaluate_indexing(cur)
+ elif isinstance(cur, mparser.TernaryNode):
+ return self.evaluate_ternary(cur)
+ elif isinstance(cur, mparser.FormatStringNode):
+ if isinstance(cur, mparser.MultilineFormatStringNode):
+ return self.evaluate_multiline_fstring(cur)
+ else:
+ return self.evaluate_fstring(cur)
+ elif isinstance(cur, mparser.ContinueNode):
+ raise ContinueRequest()
+ elif isinstance(cur, mparser.BreakNode):
+ raise BreakRequest()
+ else:
+ raise InvalidCode("Unknown statement.")
+ return None
+
+ def evaluate_arraystatement(self, cur: mparser.ArrayNode) -> InterpreterObject:
+ (arguments, kwargs) = self.reduce_arguments(cur.args)
+ if len(kwargs) > 0:
+ raise InvalidCode('Keyword arguments are invalid in array construction.')
+ return self._holderify([_unholder(x) for x in arguments])
+
+ @FeatureNew('dict', '0.47.0')
+ def evaluate_dictstatement(self, cur: mparser.DictNode) -> InterpreterObject:
+ def resolve_key(key: mparser.BaseNode) -> str:
+ if not isinstance(key, mparser.StringNode):
+ FeatureNew.single_use('Dictionary entry using non literal key', '0.53.0', self.subproject)
+ str_key = _unholder(self.evaluate_statement(key))
+ if not isinstance(str_key, str):
+ raise InvalidArguments('Key must be a string')
+ return str_key
+ arguments, kwargs = self.reduce_arguments(cur.args, key_resolver=resolve_key, duplicate_key_error='Duplicate dictionary key: {}')
+ assert not arguments
+ return self._holderify({k: _unholder(v) for k, v in kwargs.items()})
+
+ def evaluate_notstatement(self, cur: mparser.NotNode) -> InterpreterObject:
+ v = self.evaluate_statement(cur.value)
+ if isinstance(v, Disabler):
+ return v
+ return self._holderify(v.operator_call(MesonOperator.NOT, None))
+
+ def evaluate_if(self, node: mparser.IfClauseNode) -> T.Optional[Disabler]:
+ assert isinstance(node, mparser.IfClauseNode)
+ for i in node.ifs:
+ # Reset self.tmp_meson_version to know if it gets set during this
+ # statement evaluation.
+ self.tmp_meson_version = None
+ result = self.evaluate_statement(i.condition)
+ if isinstance(result, Disabler):
+ return result
+ if not isinstance(result, InterpreterObject):
+ raise mesonlib.MesonBugException(f'Argument to not ({result}) is not an InterpreterObject but {type(result).__name__}.')
+ res = result.operator_call(MesonOperator.BOOL, None)
+ if not isinstance(res, bool):
+ raise InvalidCode(f'If clause {result!r} does not evaluate to true or false.')
+ if res:
+ prev_meson_version = mesonlib.project_meson_versions[self.subproject]
+ if self.tmp_meson_version:
+ mesonlib.project_meson_versions[self.subproject] = self.tmp_meson_version
+ try:
+ self.evaluate_codeblock(i.block)
+ finally:
+ mesonlib.project_meson_versions[self.subproject] = prev_meson_version
+ return None
+ if not isinstance(node.elseblock, mparser.EmptyNode):
+ self.evaluate_codeblock(node.elseblock)
+ return None
+
+ def evaluate_comparison(self, node: mparser.ComparisonNode) -> InterpreterObject:
+ val1 = self.evaluate_statement(node.left)
+ if isinstance(val1, Disabler):
+ return val1
+ val2 = self.evaluate_statement(node.right)
+ if isinstance(val2, Disabler):
+ return val2
+
+ # New code based on InterpreterObjects
+ operator = {
+ 'in': MesonOperator.IN,
+ 'notin': MesonOperator.NOT_IN,
+ '==': MesonOperator.EQUALS,
+ '!=': MesonOperator.NOT_EQUALS,
+ '>': MesonOperator.GREATER,
+ '<': MesonOperator.LESS,
+ '>=': MesonOperator.GREATER_EQUALS,
+ '<=': MesonOperator.LESS_EQUALS,
+ }[node.ctype]
+
+ # Check if the arguments should be reversed for simplicity (this essentially converts `in` to `contains`)
+ if operator in (MesonOperator.IN, MesonOperator.NOT_IN):
+ val1, val2 = val2, val1
+
+ val1.current_node = node
+ return self._holderify(val1.operator_call(operator, _unholder(val2)))
+
+ def evaluate_andstatement(self, cur: mparser.AndNode) -> InterpreterObject:
+ l = self.evaluate_statement(cur.left)
+ if isinstance(l, Disabler):
+ return l
+ l_bool = l.operator_call(MesonOperator.BOOL, None)
+ if not l_bool:
+ return self._holderify(l_bool)
+ r = self.evaluate_statement(cur.right)
+ if isinstance(r, Disabler):
+ return r
+ return self._holderify(r.operator_call(MesonOperator.BOOL, None))
+
+ def evaluate_orstatement(self, cur: mparser.OrNode) -> InterpreterObject:
+ l = self.evaluate_statement(cur.left)
+ if isinstance(l, Disabler):
+ return l
+ l_bool = l.operator_call(MesonOperator.BOOL, None)
+ if l_bool:
+ return self._holderify(l_bool)
+ r = self.evaluate_statement(cur.right)
+ if isinstance(r, Disabler):
+ return r
+ return self._holderify(r.operator_call(MesonOperator.BOOL, None))
+
+ def evaluate_uminusstatement(self, cur: mparser.UMinusNode) -> InterpreterObject:
+ v = self.evaluate_statement(cur.value)
+ if isinstance(v, Disabler):
+ return v
+ v.current_node = cur
+ return self._holderify(v.operator_call(MesonOperator.UMINUS, None))
+
+ def evaluate_arithmeticstatement(self, cur: mparser.ArithmeticNode) -> InterpreterObject:
+ l = self.evaluate_statement(cur.left)
+ if isinstance(l, Disabler):
+ return l
+ r = self.evaluate_statement(cur.right)
+ if isinstance(r, Disabler):
+ return r
+
+ mapping: T.Dict[str, MesonOperator] = {
+ 'add': MesonOperator.PLUS,
+ 'sub': MesonOperator.MINUS,
+ 'mul': MesonOperator.TIMES,
+ 'div': MesonOperator.DIV,
+ 'mod': MesonOperator.MOD,
+ }
+ l.current_node = cur
+ res = l.operator_call(mapping[cur.operation], _unholder(r))
+ return self._holderify(res)
+
+ def evaluate_ternary(self, node: mparser.TernaryNode) -> T.Optional[InterpreterObject]:
+ assert isinstance(node, mparser.TernaryNode)
+ result = self.evaluate_statement(node.condition)
+ if isinstance(result, Disabler):
+ return result
+ result.current_node = node
+ result_bool = result.operator_call(MesonOperator.BOOL, None)
+ if result_bool:
+ return self.evaluate_statement(node.trueblock)
+ else:
+ return self.evaluate_statement(node.falseblock)
+
+ @FeatureNew('multiline format strings', '0.63.0')
+ def evaluate_multiline_fstring(self, node: mparser.MultilineFormatStringNode) -> InterpreterObject:
+ return self.evaluate_fstring(node)
+
+ @FeatureNew('format strings', '0.58.0')
+ def evaluate_fstring(self, node: mparser.FormatStringNode) -> InterpreterObject:
+ assert isinstance(node, mparser.FormatStringNode)
+
+ def replace(match: T.Match[str]) -> str:
+ var = str(match.group(1))
+ try:
+ val = _unholder(self.variables[var])
+ if not isinstance(val, (str, int, float, bool)):
+ raise InvalidCode(f'Identifier "{var}" does not name a formattable variable ' +
+ '(has to be an integer, a string, a floating point number or a boolean).')
+
+ return str(val)
+ except KeyError:
+ raise InvalidCode(f'Identifier "{var}" does not name a variable.')
+
+ res = re.sub(r'@([_a-zA-Z][_0-9a-zA-Z]*)@', replace, node.value)
+ return self._holderify(res)
+
+ def evaluate_foreach(self, node: mparser.ForeachClauseNode) -> None:
+ assert isinstance(node, mparser.ForeachClauseNode)
+ items = self.evaluate_statement(node.items)
+ if not isinstance(items, IterableObject):
+ raise InvalidArguments('Items of foreach loop do not support iterating')
+
+ tsize = items.iter_tuple_size()
+ if len(node.varnames) != (tsize or 1):
+ raise InvalidArguments(f'Foreach expects exactly {tsize or 1} variables for iterating over objects of type {items.display_name()}')
+
+ for i in items.iter_self():
+ if tsize is None:
+ if isinstance(i, tuple):
+ raise mesonlib.MesonBugException(f'Iteration of {items} returned a tuple even though iter_tuple_size() is None')
+ self.set_variable(node.varnames[0], self._holderify(i))
+ else:
+ if not isinstance(i, tuple):
+ raise mesonlib.MesonBugException(f'Iteration of {items} did not return a tuple even though iter_tuple_size() is {tsize}')
+ if len(i) != tsize:
+ raise mesonlib.MesonBugException(f'Iteration of {items} did not return a tuple even though iter_tuple_size() is {tsize}')
+ for j in range(tsize):
+ self.set_variable(node.varnames[j], self._holderify(i[j]))
+ try:
+ self.evaluate_codeblock(node.block)
+ except ContinueRequest:
+ continue
+ except BreakRequest:
+ break
+
+ def evaluate_plusassign(self, node: mparser.PlusAssignmentNode) -> None:
+ assert isinstance(node, mparser.PlusAssignmentNode)
+ varname = node.var_name
+ addition = self.evaluate_statement(node.value)
+
+ # Remember that all variables are immutable. We must always create a
+ # full new variable and then assign it.
+ old_variable = self.get_variable(varname)
+ old_variable.current_node = node
+ new_value = self._holderify(old_variable.operator_call(MesonOperator.PLUS, _unholder(addition)))
+ self.set_variable(varname, new_value)
+
+ def evaluate_indexing(self, node: mparser.IndexNode) -> InterpreterObject:
+ assert isinstance(node, mparser.IndexNode)
+ iobject = self.evaluate_statement(node.iobject)
+ if isinstance(iobject, Disabler):
+ return iobject
+ index = _unholder(self.evaluate_statement(node.index))
+
+ if iobject is None:
+ raise InterpreterException('Tried to evaluate indexing on None')
+ iobject.current_node = node
+ return self._holderify(iobject.operator_call(MesonOperator.INDEX, index))
+
+ def function_call(self, node: mparser.FunctionNode) -> T.Optional[InterpreterObject]:
+ func_name = node.func_name
+ (h_posargs, h_kwargs) = self.reduce_arguments(node.args)
+ (posargs, kwargs) = self._unholder_args(h_posargs, h_kwargs)
+ if is_disabled(posargs, kwargs) and func_name not in {'get_variable', 'set_variable', 'unset_variable', 'is_disabler'}:
+ return Disabler()
+ if func_name in self.funcs:
+ func = self.funcs[func_name]
+ func_args = posargs
+ if not getattr(func, 'no-args-flattening', False):
+ func_args = flatten(posargs)
+ if not getattr(func, 'no-second-level-holder-flattening', False):
+ func_args, kwargs = resolve_second_level_holders(func_args, kwargs)
+ res = func(node, func_args, kwargs)
+ return self._holderify(res) if res is not None else None
+ else:
+ self.unknown_function_called(func_name)
+ return None
+
+ def method_call(self, node: mparser.MethodNode) -> T.Optional[InterpreterObject]:
+ invokable = node.source_object
+ obj: T.Optional[InterpreterObject]
+ if isinstance(invokable, mparser.IdNode):
+ object_display_name = f'variable "{invokable.value}"'
+ obj = self.get_variable(invokable.value)
+ else:
+ object_display_name = invokable.__class__.__name__
+ obj = self.evaluate_statement(invokable)
+ method_name = node.name
+ (h_args, h_kwargs) = self.reduce_arguments(node.args)
+ (args, kwargs) = self._unholder_args(h_args, h_kwargs)
+ if is_disabled(args, kwargs):
+ return Disabler()
+ if not isinstance(obj, InterpreterObject):
+ raise InvalidArguments(f'{object_display_name} is not callable.')
+ # TODO: InterpreterBase **really** shouldn't be in charge of checking this
+ if method_name == 'extract_objects':
+ if isinstance(obj, ObjectHolder):
+ self.validate_extraction(obj.held_object)
+ elif not isinstance(obj, Disabler):
+ raise InvalidArguments(f'Invalid operation "extract_objects" on {object_display_name} of type {type(obj).__name__}')
+ obj.current_node = node
+ res = obj.method_call(method_name, args, kwargs)
+ return self._holderify(res) if res is not None else None
+
+ def _holderify(self, res: T.Union[TYPE_var, InterpreterObject]) -> InterpreterObject:
+ if isinstance(res, HoldableTypes):
+ # Always check for an exact match first.
+ cls = self.holder_map.get(type(res), None)
+ if cls is not None:
+ # Casts to Interpreter are required here since an assertion would
+ # not work for the `ast` module.
+ return cls(res, T.cast('Interpreter', self))
+ # Try the boundary types next.
+ for typ, cls in self.bound_holder_map.items():
+ if isinstance(res, typ):
+ return cls(res, T.cast('Interpreter', self))
+ raise mesonlib.MesonBugException(f'Object {res} of type {type(res).__name__} is neither in self.holder_map nor self.bound_holder_map.')
+ elif isinstance(res, ObjectHolder):
+ raise mesonlib.MesonBugException(f'Returned object {res} of type {type(res).__name__} is an object holder.')
+ elif isinstance(res, MesonInterpreterObject):
+ return res
+ raise mesonlib.MesonBugException(f'Unknown returned object {res} of type {type(res).__name__} in the parameters.')
+
+ def _unholder_args(self,
+ args: T.List[InterpreterObject],
+ kwargs: T.Dict[str, InterpreterObject]) -> T.Tuple[T.List[TYPE_var], TYPE_kwargs]:
+ return [_unholder(x) for x in args], {k: _unholder(v) for k, v in kwargs.items()}
+
+ def unknown_function_called(self, func_name: str) -> None:
+ raise InvalidCode(f'Unknown function "{func_name}".')
+
+ def reduce_arguments(
+ self,
+ args: mparser.ArgumentNode,
+ key_resolver: T.Callable[[mparser.BaseNode], str] = default_resolve_key,
+ duplicate_key_error: T.Optional[str] = None,
+ ) -> T.Tuple[
+ T.List[InterpreterObject],
+ T.Dict[str, InterpreterObject]
+ ]:
+ assert isinstance(args, mparser.ArgumentNode)
+ if args.incorrect_order():
+ raise InvalidArguments('All keyword arguments must be after positional arguments.')
+ self.argument_depth += 1
+ reduced_pos = [self.evaluate_statement(arg) for arg in args.arguments]
+ if any(x is None for x in reduced_pos):
+ raise InvalidArguments('At least one value in the arguments is void.')
+ reduced_kw: T.Dict[str, InterpreterObject] = {}
+ for key, val in args.kwargs.items():
+ reduced_key = key_resolver(key)
+ assert isinstance(val, mparser.BaseNode)
+ reduced_val = self.evaluate_statement(val)
+ if reduced_val is None:
+ raise InvalidArguments(f'Value of key {reduced_key} is void.')
+ if duplicate_key_error and reduced_key in reduced_kw:
+ raise InvalidArguments(duplicate_key_error.format(reduced_key))
+ reduced_kw[reduced_key] = reduced_val
+ self.argument_depth -= 1
+ final_kw = self.expand_default_kwargs(reduced_kw)
+ return reduced_pos, final_kw
+
+ def expand_default_kwargs(self, kwargs: T.Dict[str, T.Optional[InterpreterObject]]) -> T.Dict[str, T.Optional[InterpreterObject]]:
+ if 'kwargs' not in kwargs:
+ return kwargs
+ to_expand = _unholder(kwargs.pop('kwargs'))
+ if not isinstance(to_expand, dict):
+ raise InterpreterException('Value of "kwargs" must be dictionary.')
+ if 'kwargs' in to_expand:
+ raise InterpreterException('Kwargs argument must not contain a "kwargs" entry. Points for thinking meta, though. :P')
+ for k, v in to_expand.items():
+ if k in kwargs:
+ raise InterpreterException(f'Entry "{k}" defined both as a keyword argument and in a "kwarg" entry.')
+ kwargs[k] = self._holderify(v)
+ return kwargs
+
+ def assignment(self, node: mparser.AssignmentNode) -> None:
+ assert isinstance(node, mparser.AssignmentNode)
+ if self.argument_depth != 0:
+ raise InvalidArguments(textwrap.dedent('''\
+ Tried to assign values inside an argument list.
+ To specify a keyword argument, use : instead of =.
+ '''))
+ var_name = node.var_name
+ if not isinstance(var_name, str):
+ raise InvalidArguments('Tried to assign value to a non-variable.')
+ value = self.evaluate_statement(node.value)
+ # For mutable objects we need to make a copy on assignment
+ if isinstance(value, MutableInterpreterObject):
+ value = copy.deepcopy(value)
+ self.set_variable(var_name, value)
+
+ def set_variable(self, varname: str, variable: T.Union[TYPE_var, InterpreterObject], *, holderify: bool = False) -> None:
+ if variable is None:
+ raise InvalidCode('Can not assign None to variable.')
+ if holderify:
+ variable = self._holderify(variable)
+ else:
+ # Ensure that we are always storing ObjectHolders
+ if not isinstance(variable, InterpreterObject):
+ raise mesonlib.MesonBugException(f'set_variable in InterpreterBase called with a non InterpreterObject {variable} of type {type(variable).__name__}')
+ if not isinstance(varname, str):
+ raise InvalidCode('First argument to set_variable must be a string.')
+ if re.match('[_a-zA-Z][_0-9a-zA-Z]*$', varname) is None:
+ raise InvalidCode('Invalid variable name: ' + varname)
+ if varname in self.builtin:
+ raise InvalidCode(f'Tried to overwrite internal variable "{varname}"')
+ self.variables[varname] = variable
+
+ def get_variable(self, varname: str) -> InterpreterObject:
+ if varname in self.builtin:
+ return self.builtin[varname]
+ if varname in self.variables:
+ return self.variables[varname]
+ raise InvalidCode(f'Unknown variable "{varname}".')
+
+ def validate_extraction(self, buildtarget: mesonlib.HoldableObject) -> None:
+ raise InterpreterException('validate_extraction is not implemented in this context (please file a bug)')
diff --git a/mesonbuild/interpreterbase/operator.py b/mesonbuild/interpreterbase/operator.py
new file mode 100644
index 0000000..5dec8d0
--- /dev/null
+++ b/mesonbuild/interpreterbase/operator.py
@@ -0,0 +1,32 @@
+# SPDX-license-identifier: Apache-2.0
+
+from enum import Enum
+
+class MesonOperator(Enum):
+ # Arithmetic
+ PLUS = '+'
+ MINUS = '-'
+ TIMES = '*'
+ DIV = '/'
+ MOD = '%'
+
+ UMINUS = 'uminus'
+
+ # Logic
+ NOT = 'not'
+
+ # Should return the boolsche interpretation of the value (`'' == false` for instance)
+ BOOL = 'bool()'
+
+ # Comparison
+ EQUALS = '=='
+ NOT_EQUALS = '!='
+ GREATER = '>'
+ LESS = '<'
+ GREATER_EQUALS = '>='
+ LESS_EQUALS = '<='
+
+ # Container
+ IN = 'in'
+ NOT_IN = 'not in'
+ INDEX = '[]'
diff --git a/mesonbuild/linkers/__init__.py b/mesonbuild/linkers/__init__.py
new file mode 100644
index 0000000..298e901
--- /dev/null
+++ b/mesonbuild/linkers/__init__.py
@@ -0,0 +1,136 @@
+# Copyright 2012-2021 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.
+
+from .detect import (
+ defaults,
+ guess_win_linker,
+ guess_nix_linker,
+)
+from .linkers import (
+ RSPFileSyntax,
+
+ StaticLinker,
+ VisualStudioLikeLinker,
+ VisualStudioLinker,
+ IntelVisualStudioLinker,
+ AppleArLinker,
+ ArLinker,
+ ArmarLinker,
+ DLinker,
+ CcrxLinker,
+ Xc16Linker,
+ CompCertLinker,
+ C2000Linker,
+ TILinker,
+ AIXArLinker,
+ PGIStaticLinker,
+ NvidiaHPC_StaticLinker,
+
+ DynamicLinker,
+ PosixDynamicLinkerMixin,
+ GnuLikeDynamicLinkerMixin,
+ AppleDynamicLinker,
+ GnuDynamicLinker,
+ GnuGoldDynamicLinker,
+ GnuBFDDynamicLinker,
+ LLVMDynamicLinker,
+ MoldDynamicLinker,
+ WASMDynamicLinker,
+ CcrxDynamicLinker,
+ Xc16DynamicLinker,
+ CompCertDynamicLinker,
+ C2000DynamicLinker,
+ TIDynamicLinker,
+ ArmDynamicLinker,
+ ArmClangDynamicLinker,
+ QualcommLLVMDynamicLinker,
+ PGIDynamicLinker,
+ NvidiaHPC_DynamicLinker,
+ NAGDynamicLinker,
+
+ VisualStudioLikeLinkerMixin,
+ MSVCDynamicLinker,
+ ClangClDynamicLinker,
+ XilinkDynamicLinker,
+ SolarisDynamicLinker,
+ AIXDynamicLinker,
+ OptlinkDynamicLinker,
+ CudaLinker,
+
+ prepare_rpaths,
+ order_rpaths,
+ evaluate_rpath,
+)
+
+__all__ = [
+ # detect.py
+ 'defaults',
+ 'guess_win_linker',
+ 'guess_nix_linker',
+
+ # linkers.py
+ 'RSPFileSyntax',
+
+ 'StaticLinker',
+ 'VisualStudioLikeLinker',
+ 'VisualStudioLinker',
+ 'IntelVisualStudioLinker',
+ 'ArLinker',
+ 'ArmarLinker',
+ 'DLinker',
+ 'CcrxLinker',
+ 'Xc16Linker',
+ 'CompCertLinker',
+ 'C2000Linker',
+ 'TILinker',
+ 'AIXArLinker',
+ 'AppleArLinker',
+ 'PGIStaticLinker',
+ 'NvidiaHPC_StaticLinker',
+
+ 'DynamicLinker',
+ 'PosixDynamicLinkerMixin',
+ 'GnuLikeDynamicLinkerMixin',
+ 'AppleDynamicLinker',
+ 'GnuDynamicLinker',
+ 'GnuGoldDynamicLinker',
+ 'GnuBFDDynamicLinker',
+ 'LLVMDynamicLinker',
+ 'MoldDynamicLinker',
+ 'WASMDynamicLinker',
+ 'CcrxDynamicLinker',
+ 'Xc16DynamicLinker',
+ 'CompCertDynamicLinker',
+ 'C2000DynamicLinker',
+ 'TIDynamicLinker',
+ 'ArmDynamicLinker',
+ 'ArmClangDynamicLinker',
+ 'QualcommLLVMDynamicLinker',
+ 'PGIDynamicLinker',
+ 'NvidiaHPC_DynamicLinker',
+ 'NAGDynamicLinker',
+
+ 'VisualStudioLikeLinkerMixin',
+ 'MSVCDynamicLinker',
+ 'ClangClDynamicLinker',
+ 'XilinkDynamicLinker',
+ 'SolarisDynamicLinker',
+ 'AIXDynamicLinker',
+ 'OptlinkDynamicLinker',
+ 'CudaLinker',
+
+ 'prepare_rpaths',
+ 'order_rpaths',
+ 'evaluate_rpath',
+]
diff --git a/mesonbuild/linkers/detect.py b/mesonbuild/linkers/detect.py
new file mode 100644
index 0000000..97e770c
--- /dev/null
+++ b/mesonbuild/linkers/detect.py
@@ -0,0 +1,258 @@
+# Copyright 2012-2022 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.
+
+from __future__ import annotations
+
+from .. import mlog
+from ..mesonlib import (
+ EnvironmentException,
+ Popen_safe, join_args, search_version
+)
+from .linkers import (
+ AppleDynamicLinker,
+ LLVMLD64DynamicLinker,
+ GnuGoldDynamicLinker,
+ GnuBFDDynamicLinker,
+ MoldDynamicLinker,
+ LLVMDynamicLinker,
+ QualcommLLVMDynamicLinker,
+ MSVCDynamicLinker,
+ ClangClDynamicLinker,
+ SolarisDynamicLinker,
+ AIXDynamicLinker,
+ OptlinkDynamicLinker,
+)
+
+import re
+import shlex
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .linkers import DynamicLinker, GnuDynamicLinker
+ from ..environment import Environment
+ from ..compilers import Compiler
+ from ..mesonlib import MachineChoice
+
+defaults: T.Dict[str, T.List[str]] = {}
+defaults['static_linker'] = ['ar', 'gar']
+defaults['vs_static_linker'] = ['lib']
+defaults['clang_cl_static_linker'] = ['llvm-lib']
+defaults['cuda_static_linker'] = ['nvlink']
+defaults['gcc_static_linker'] = ['gcc-ar']
+defaults['clang_static_linker'] = ['llvm-ar']
+
+def __failed_to_detect_linker(compiler: T.List[str], args: T.List[str], stdout: str, stderr: str) -> 'T.NoReturn':
+ msg = 'Unable to detect linker for compiler `{}`\nstdout: {}\nstderr: {}'.format(
+ join_args(compiler + args), stdout, stderr)
+ raise EnvironmentException(msg)
+
+
+def guess_win_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Type['Compiler'],
+ comp_version: str, for_machine: MachineChoice, *,
+ use_linker_prefix: bool = True, invoked_directly: bool = True,
+ extra_args: T.Optional[T.List[str]] = None) -> 'DynamicLinker':
+ env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env)
+
+ # Explicitly pass logo here so that we can get the version of link.exe
+ if not use_linker_prefix or comp_class.LINKER_PREFIX is None:
+ check_args = ['/logo', '--version']
+ elif isinstance(comp_class.LINKER_PREFIX, str):
+ check_args = [comp_class.LINKER_PREFIX + '/logo', comp_class.LINKER_PREFIX + '--version']
+ elif isinstance(comp_class.LINKER_PREFIX, list):
+ check_args = comp_class.LINKER_PREFIX + ['/logo'] + comp_class.LINKER_PREFIX + ['--version']
+
+ check_args += env.coredata.get_external_link_args(for_machine, comp_class.language)
+
+ override = [] # type: T.List[str]
+ value = env.lookup_binary_entry(for_machine, comp_class.language + '_ld')
+ if value is not None:
+ override = comp_class.use_linker_args(value[0], comp_version)
+ check_args += override
+
+ if extra_args is not None:
+ check_args.extend(extra_args)
+
+ p, o, _ = Popen_safe(compiler + check_args)
+ if 'LLD' in o.split('\n', maxsplit=1)[0]:
+ if '(compatible with GNU linkers)' in o:
+ return LLVMDynamicLinker(
+ compiler, for_machine, comp_class.LINKER_PREFIX,
+ override, version=search_version(o))
+ elif not invoked_directly:
+ return ClangClDynamicLinker(
+ for_machine, override, exelist=compiler, prefix=comp_class.LINKER_PREFIX,
+ version=search_version(o), direct=False, machine=None)
+
+ if value is not None and invoked_directly:
+ compiler = value
+ # We've already hanedled the non-direct case above
+
+ p, o, e = Popen_safe(compiler + check_args)
+ if 'LLD' in o.split('\n', maxsplit=1)[0]:
+ return ClangClDynamicLinker(
+ for_machine, [],
+ prefix=comp_class.LINKER_PREFIX if use_linker_prefix else [],
+ exelist=compiler, version=search_version(o), direct=invoked_directly)
+ elif 'OPTLINK' in o:
+ # Optlink's stdout *may* begin with a \r character.
+ return OptlinkDynamicLinker(compiler, for_machine, version=search_version(o))
+ elif o.startswith('Microsoft') or e.startswith('Microsoft'):
+ out = o or e
+ match = re.search(r'.*(X86|X64|ARM|ARM64).*', out)
+ if match:
+ target = str(match.group(1))
+ else:
+ target = 'x86'
+
+ return MSVCDynamicLinker(
+ for_machine, [], machine=target, exelist=compiler,
+ prefix=comp_class.LINKER_PREFIX if use_linker_prefix else [],
+ version=search_version(out), direct=invoked_directly)
+ elif 'GNU coreutils' in o:
+ import shutil
+ fullpath = shutil.which(compiler[0])
+ raise EnvironmentException(
+ f"Found GNU link.exe instead of MSVC link.exe in {fullpath}.\n"
+ "This link.exe is not a linker.\n"
+ "You may need to reorder entries to your %PATH% variable to resolve this.")
+ __failed_to_detect_linker(compiler, check_args, o, e)
+
+def guess_nix_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Type['Compiler'],
+ comp_version: str, for_machine: MachineChoice, *,
+ extra_args: T.Optional[T.List[str]] = None) -> 'DynamicLinker':
+ """Helper for guessing what linker to use on Unix-Like OSes.
+
+ :compiler: Invocation to use to get linker
+ :comp_class: The Compiler Type (uninstantiated)
+ :comp_version: The compiler version string
+ :for_machine: which machine this linker targets
+ :extra_args: Any additional arguments required (such as a source file)
+ """
+ env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env)
+ extra_args = extra_args or []
+
+ ldflags = env.coredata.get_external_link_args(for_machine, comp_class.language)
+ extra_args += comp_class._unix_args_to_native(ldflags, env.machines[for_machine])
+
+ if isinstance(comp_class.LINKER_PREFIX, str):
+ check_args = [comp_class.LINKER_PREFIX + '--version'] + extra_args
+ else:
+ check_args = comp_class.LINKER_PREFIX + ['--version'] + extra_args
+
+ override = [] # type: T.List[str]
+ value = env.lookup_binary_entry(for_machine, comp_class.language + '_ld')
+ if value is not None:
+ override = comp_class.use_linker_args(value[0], comp_version)
+ check_args += override
+
+ mlog.debug('-----')
+ mlog.debug(f'Detecting linker via: {join_args(compiler + check_args)}')
+ p, o, e = Popen_safe(compiler + check_args)
+ mlog.debug(f'linker returned {p}')
+ mlog.debug(f'linker stdout:\n{o}')
+ mlog.debug(f'linker stderr:\n{e}')
+
+ v = search_version(o + e)
+ linker: DynamicLinker
+ if 'LLD' in o.split('\n', maxsplit=1)[0]:
+ if isinstance(comp_class.LINKER_PREFIX, str):
+ cmd = compiler + override + [comp_class.LINKER_PREFIX + '-v'] + extra_args
+ else:
+ cmd = compiler + override + comp_class.LINKER_PREFIX + ['-v'] + extra_args
+ mlog.debug('-----')
+ mlog.debug(f'Detecting LLD linker via: {join_args(cmd)}')
+ _, newo, newerr = Popen_safe(cmd)
+ mlog.debug(f'linker stdout:\n{newo}')
+ mlog.debug(f'linker stderr:\n{newerr}')
+
+ lld_cls: T.Type[DynamicLinker]
+ if 'ld64.lld' in newerr:
+ lld_cls = LLVMLD64DynamicLinker
+ else:
+ lld_cls = LLVMDynamicLinker
+
+ linker = lld_cls(
+ compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v)
+ elif 'Snapdragon' in e and 'LLVM' in e:
+ linker = QualcommLLVMDynamicLinker(
+ compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v)
+ elif e.startswith('lld-link: '):
+ # The LLD MinGW frontend didn't respond to --version before version 9.0.0,
+ # and produced an error message about failing to link (when no object
+ # files were specified), instead of printing the version number.
+ # Let's try to extract the linker invocation command to grab the version.
+
+ _, o, e = Popen_safe(compiler + check_args + ['-v'])
+
+ try:
+ linker_cmd = re.match(r'.*\n(.*?)\nlld-link: ', e, re.DOTALL).group(1)
+ linker_cmd = shlex.split(linker_cmd)[0]
+ except (AttributeError, IndexError, ValueError):
+ pass
+ else:
+ _, o, e = Popen_safe([linker_cmd, '--version'])
+ v = search_version(o)
+
+ linker = LLVMDynamicLinker(compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v)
+ # first might be apple clang, second is for real gcc, the third is icc
+ elif e.endswith('(use -v to see invocation)\n') or 'macosx_version' in e or 'ld: unknown option:' in e:
+ if isinstance(comp_class.LINKER_PREFIX, str):
+ cmd = compiler + [comp_class.LINKER_PREFIX + '-v'] + extra_args
+ else:
+ cmd = compiler + comp_class.LINKER_PREFIX + ['-v'] + extra_args
+ mlog.debug('-----')
+ mlog.debug(f'Detecting Apple linker via: {join_args(cmd)}')
+ _, newo, newerr = Popen_safe(cmd)
+ mlog.debug(f'linker stdout:\n{newo}')
+ mlog.debug(f'linker stderr:\n{newerr}')
+
+ for line in newerr.split('\n'):
+ if 'PROJECT:ld' in line:
+ v = line.split('-')[1]
+ break
+ else:
+ __failed_to_detect_linker(compiler, check_args, o, e)
+ linker = AppleDynamicLinker(compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v)
+ elif 'GNU' in o or 'GNU' in e:
+ gnu_cls: T.Type[GnuDynamicLinker]
+ # this is always the only thing on stdout, except for swift
+ # which may or may not redirect the linker stdout to stderr
+ if o.startswith('GNU gold') or e.startswith('GNU gold'):
+ gnu_cls = GnuGoldDynamicLinker
+ elif o.startswith('mold') or e.startswith('mold'):
+ gnu_cls = MoldDynamicLinker
+ else:
+ gnu_cls = GnuBFDDynamicLinker
+ linker = gnu_cls(compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v)
+ elif 'Solaris' in e or 'Solaris' in o:
+ for line in (o+e).split('\n'):
+ if 'ld: Software Generation Utilities' in line:
+ v = line.split(':')[2].lstrip()
+ break
+ else:
+ v = 'unknown version'
+ linker = SolarisDynamicLinker(
+ compiler, for_machine, comp_class.LINKER_PREFIX, override,
+ version=v)
+ elif 'ld: 0706-012 The -- flag is not recognized' in e:
+ if isinstance(comp_class.LINKER_PREFIX, str):
+ _, _, e = Popen_safe(compiler + [comp_class.LINKER_PREFIX + '-V'] + extra_args)
+ else:
+ _, _, e = Popen_safe(compiler + comp_class.LINKER_PREFIX + ['-V'] + extra_args)
+ linker = AIXDynamicLinker(
+ compiler, for_machine, comp_class.LINKER_PREFIX, override,
+ version=search_version(e))
+ else:
+ __failed_to_detect_linker(compiler, check_args, o, e)
+ return linker
diff --git a/mesonbuild/linkers/linkers.py b/mesonbuild/linkers/linkers.py
new file mode 100644
index 0000000..5799caf
--- /dev/null
+++ b/mesonbuild/linkers/linkers.py
@@ -0,0 +1,1556 @@
+# Copyright 2012-2022 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.
+from __future__ import annotations
+
+import abc
+import enum
+import os
+import typing as T
+
+from .. import mesonlib
+from ..mesonlib import EnvironmentException, MesonException
+from ..arglist import CompilerArgs
+
+if T.TYPE_CHECKING:
+ from ..coredata import KeyedOptionDictType
+ from ..environment import Environment
+ from ..mesonlib import MachineChoice
+
+
+@enum.unique
+class RSPFileSyntax(enum.Enum):
+
+ """Which RSP file syntax the compiler supports."""
+
+ MSVC = enum.auto()
+ GCC = enum.auto()
+
+
+class StaticLinker:
+
+ id: str
+
+ def __init__(self, exelist: T.List[str]):
+ self.exelist = exelist
+
+ def compiler_args(self, args: T.Optional[T.Iterable[str]] = None) -> CompilerArgs:
+ return CompilerArgs(self, args)
+
+ def can_linker_accept_rsp(self) -> bool:
+ """
+ Determines whether the linker can accept arguments using the @rsp syntax.
+ """
+ return mesonlib.is_windows()
+
+ def get_base_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ """Like compilers.get_base_link_args, but for the static linker."""
+ return []
+
+ def get_exelist(self) -> T.List[str]:
+ return self.exelist.copy()
+
+ def get_std_link_args(self, env: 'Environment', is_thin: bool) -> T.List[str]:
+ return []
+
+ def get_buildtype_linker_args(self, buildtype: str) -> T.List[str]:
+ return []
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return []
+
+ def get_coverage_link_args(self) -> T.List[str]:
+ return []
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ return ([], set())
+
+ def thread_link_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def openmp_flags(self) -> T.List[str]:
+ return []
+
+ def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+ @classmethod
+ def unix_args_to_native(cls, args: T.List[str]) -> T.List[str]:
+ return args[:]
+
+ @classmethod
+ def native_args_to_unix(cls, args: T.List[str]) -> T.List[str]:
+ return args[:]
+
+ def get_link_debugfile_name(self, targetfile: str) -> str:
+ return None
+
+ def get_link_debugfile_args(self, targetfile: str) -> T.List[str]:
+ # Static libraries do not have PDB files
+ return []
+
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+ def get_linker_always_args(self) -> T.List[str]:
+ return []
+
+ def rsp_file_syntax(self) -> RSPFileSyntax:
+ """The format of the RSP file that this compiler supports.
+
+ If `self.can_linker_accept_rsp()` returns True, then this needs to
+ be implemented
+ """
+ assert not self.can_linker_accept_rsp(), f'{self.id} linker accepts RSP, but doesn\' provide a supported format, this is a bug'
+ raise EnvironmentException(f'{self.id} does not implement rsp format, this shouldn\'t be called')
+
+
+class VisualStudioLikeLinker:
+ always_args = ['/NOLOGO']
+
+ def __init__(self, machine: str):
+ self.machine = machine
+
+ def get_always_args(self) -> T.List[str]:
+ return self.always_args.copy()
+
+ def get_linker_always_args(self) -> T.List[str]:
+ return self.always_args.copy()
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ args = [] # type: T.List[str]
+ if self.machine:
+ args += ['/MACHINE:' + self.machine]
+ args += ['/OUT:' + target]
+ return args
+
+ @classmethod
+ def unix_args_to_native(cls, args: T.List[str]) -> T.List[str]:
+ from ..compilers.c import VisualStudioCCompiler
+ return VisualStudioCCompiler.unix_args_to_native(args)
+
+ @classmethod
+ def native_args_to_unix(cls, args: T.List[str]) -> T.List[str]:
+ from ..compilers.c import VisualStudioCCompiler
+ return VisualStudioCCompiler.native_args_to_unix(args)
+
+ def rsp_file_syntax(self) -> RSPFileSyntax:
+ return RSPFileSyntax.MSVC
+
+
+class VisualStudioLinker(VisualStudioLikeLinker, StaticLinker):
+
+ """Microsoft's lib static linker."""
+
+ def __init__(self, exelist: T.List[str], machine: str):
+ StaticLinker.__init__(self, exelist)
+ VisualStudioLikeLinker.__init__(self, machine)
+
+
+class IntelVisualStudioLinker(VisualStudioLikeLinker, StaticLinker):
+
+ """Intel's xilib static linker."""
+
+ def __init__(self, exelist: T.List[str], machine: str):
+ StaticLinker.__init__(self, exelist)
+ VisualStudioLikeLinker.__init__(self, machine)
+
+
+class ArLikeLinker(StaticLinker):
+ # POSIX requires supporting the dash, GNU permits omitting it
+ std_args = ['-csr']
+
+ def can_linker_accept_rsp(self) -> bool:
+ # armar / AIX can't accept arguments using the @rsp syntax
+ # in fact, only the 'ar' id can
+ return False
+
+ def get_std_link_args(self, env: 'Environment', is_thin: bool) -> T.List[str]:
+ return self.std_args
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [target]
+
+ def rsp_file_syntax(self) -> RSPFileSyntax:
+ return RSPFileSyntax.GCC
+
+
+class ArLinker(ArLikeLinker):
+ id = 'ar'
+
+ def __init__(self, for_machine: mesonlib.MachineChoice, exelist: T.List[str]):
+ super().__init__(exelist)
+ stdo = mesonlib.Popen_safe(self.exelist + ['-h'])[1]
+ # Enable deterministic builds if they are available.
+ stdargs = 'csr'
+ thinargs = ''
+ if '[D]' in stdo:
+ stdargs += 'D'
+ if '[T]' in stdo:
+ thinargs = 'T'
+ self.std_args = [stdargs]
+ self.std_thin_args = [stdargs + thinargs]
+ self.can_rsp = '@<' in stdo
+ self.for_machine = for_machine
+
+ def can_linker_accept_rsp(self) -> bool:
+ return self.can_rsp
+
+ def get_std_link_args(self, env: 'Environment', is_thin: bool) -> T.List[str]:
+ # Thin archives are a GNU extension not supported by the system linkers
+ # on Mac OS X, Solaris, or illumos, so don't build them on those OSes.
+ # OS X ld rejects with: "file built for unknown-unsupported file format"
+ # illumos/Solaris ld rejects with: "unknown file type"
+ if is_thin and not env.machines[self.for_machine].is_darwin() \
+ and not env.machines[self.for_machine].is_sunos():
+ return self.std_thin_args
+ else:
+ return self.std_args
+
+
+class AppleArLinker(ArLinker):
+
+ # mostly this is used to determine that we need to call ranlib
+
+ id = 'applear'
+
+
+class ArmarLinker(ArLikeLinker):
+ id = 'armar'
+
+
+class DLinker(StaticLinker):
+ def __init__(self, exelist: T.List[str], arch: str, *, rsp_syntax: RSPFileSyntax = RSPFileSyntax.GCC):
+ super().__init__(exelist)
+ self.id = exelist[0]
+ self.arch = arch
+ self.__rsp_syntax = rsp_syntax
+
+ def get_std_link_args(self, env: 'Environment', is_thin: bool) -> T.List[str]:
+ return ['-lib']
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return ['-of=' + target]
+
+ def get_linker_always_args(self) -> T.List[str]:
+ if mesonlib.is_windows():
+ if self.arch == 'x86_64':
+ return ['-m64']
+ elif self.arch == 'x86_mscoff' and self.id == 'dmd':
+ return ['-m32mscoff']
+ return ['-m32']
+ return []
+
+ def rsp_file_syntax(self) -> RSPFileSyntax:
+ return self.__rsp_syntax
+
+
+class CcrxLinker(StaticLinker):
+
+ def __init__(self, exelist: T.List[str]):
+ super().__init__(exelist)
+ self.id = 'rlink'
+
+ def can_linker_accept_rsp(self) -> bool:
+ return False
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [f'-output={target}']
+
+ def get_linker_always_args(self) -> T.List[str]:
+ return ['-nologo', '-form=library']
+
+
+class Xc16Linker(StaticLinker):
+
+ def __init__(self, exelist: T.List[str]):
+ super().__init__(exelist)
+ self.id = 'xc16-ar'
+
+ def can_linker_accept_rsp(self) -> bool:
+ return False
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [f'{target}']
+
+ def get_linker_always_args(self) -> T.List[str]:
+ return ['rcs']
+
+class CompCertLinker(StaticLinker):
+
+ def __init__(self, exelist: T.List[str]):
+ super().__init__(exelist)
+ self.id = 'ccomp'
+
+ def can_linker_accept_rsp(self) -> bool:
+ return False
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [f'-o{target}']
+
+
+class TILinker(StaticLinker):
+
+ def __init__(self, exelist: T.List[str]):
+ super().__init__(exelist)
+ self.id = 'ti-ar'
+
+ def can_linker_accept_rsp(self) -> bool:
+ return False
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [f'{target}']
+
+ def get_linker_always_args(self) -> T.List[str]:
+ return ['-r']
+
+
+class C2000Linker(TILinker):
+ # Required for backwards compat with projects created before ti-cgt support existed
+ id = 'ar2000'
+
+
+class AIXArLinker(ArLikeLinker):
+ id = 'aixar'
+ std_args = ['-csr', '-Xany']
+
+
+def prepare_rpaths(raw_rpaths: T.Tuple[str, ...], build_dir: str, from_dir: str) -> T.List[str]:
+ # The rpaths we write must be relative if they point to the build dir,
+ # because otherwise they have different length depending on the build
+ # directory. This breaks reproducible builds.
+ internal_format_rpaths = [evaluate_rpath(p, build_dir, from_dir) for p in raw_rpaths]
+ ordered_rpaths = order_rpaths(internal_format_rpaths)
+ return ordered_rpaths
+
+
+def order_rpaths(rpath_list: T.List[str]) -> T.List[str]:
+ # We want rpaths that point inside our build dir to always override
+ # those pointing to other places in the file system. This is so built
+ # binaries prefer our libraries to the ones that may lie somewhere
+ # in the file system, such as /lib/x86_64-linux-gnu.
+ #
+ # The correct thing to do here would be C++'s std::stable_partition.
+ # Python standard library does not have it, so replicate it with
+ # sort, which is guaranteed to be stable.
+ return sorted(rpath_list, key=os.path.isabs)
+
+
+def evaluate_rpath(p: str, build_dir: str, from_dir: str) -> str:
+ if p == from_dir:
+ return '' # relpath errors out in this case
+ elif os.path.isabs(p):
+ return p # These can be outside of build dir.
+ else:
+ return os.path.relpath(os.path.join(build_dir, p), os.path.join(build_dir, from_dir))
+
+class DynamicLinker(metaclass=abc.ABCMeta):
+
+ """Base class for dynamic linkers."""
+
+ _BUILDTYPE_ARGS = {
+ 'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': [],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+ @abc.abstractproperty
+ def id(self) -> str:
+ pass
+
+ def _apply_prefix(self, arg: T.Union[str, T.List[str]]) -> T.List[str]:
+ args = [arg] if isinstance(arg, str) else arg
+ if self.prefix_arg is None:
+ return args
+ elif isinstance(self.prefix_arg, str):
+ return [self.prefix_arg + arg for arg in args]
+ ret = []
+ for arg in args:
+ ret += self.prefix_arg + [arg]
+ return ret
+
+ def __init__(self, exelist: T.List[str],
+ for_machine: mesonlib.MachineChoice, prefix_arg: T.Union[str, T.List[str]],
+ always_args: T.List[str], *, version: str = 'unknown version'):
+ self.exelist = exelist
+ self.for_machine = for_machine
+ self.version = version
+ self.prefix_arg = prefix_arg
+ self.always_args = always_args
+ self.machine = None # type: T.Optional[str]
+
+ def __repr__(self) -> str:
+ return '<{}: v{} `{}`>'.format(type(self).__name__, self.version, ' '.join(self.exelist))
+
+ def get_id(self) -> str:
+ return self.id
+
+ def get_version_string(self) -> str:
+ return f'({self.id} {self.version})'
+
+ def get_exelist(self) -> T.List[str]:
+ return self.exelist.copy()
+
+ def get_accepts_rsp(self) -> bool:
+ # rsp files are only used when building on Windows because we want to
+ # avoid issues with quoting and max argument length
+ return mesonlib.is_windows()
+
+ def rsp_file_syntax(self) -> RSPFileSyntax:
+ """The format of the RSP file that this compiler supports.
+
+ If `self.can_linker_accept_rsp()` returns True, then this needs to
+ be implemented
+ """
+ return RSPFileSyntax.GCC
+
+ def get_always_args(self) -> T.List[str]:
+ return self.always_args.copy()
+
+ def get_lib_prefix(self) -> str:
+ return ''
+
+ # XXX: is use_ldflags a compiler or a linker attribute?
+
+ def get_option_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return []
+
+ def has_multi_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]:
+ raise EnvironmentException(f'Language {self.id} does not support has_multi_link_arguments.')
+
+ def get_debugfile_name(self, targetfile: str) -> str:
+ '''Name of debug file written out (see below)'''
+ return None
+
+ def get_debugfile_args(self, targetfile: str) -> T.List[str]:
+ """Some compilers (MSVC) write debug into a separate file.
+
+ This method takes the target object path and returns a list of
+ commands to append to the linker invocation to control where that
+ file is written.
+ """
+ return []
+
+ def get_std_shared_lib_args(self) -> T.List[str]:
+ return []
+
+ def get_std_shared_module_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return self.get_std_shared_lib_args()
+
+ def get_pie_args(self) -> T.List[str]:
+ # TODO: this really needs to take a boolean and return the args to
+ # disable pie, otherwise it only acts to enable pie if pie *isn't* the
+ # default.
+ raise EnvironmentException(f'Linker {self.id} does not support position-independent executable')
+
+ def get_lto_args(self) -> T.List[str]:
+ return []
+
+ def get_thinlto_cache_args(self, path: str) -> T.List[str]:
+ return []
+
+ def sanitizer_args(self, value: str) -> T.List[str]:
+ return []
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ # We can override these in children by just overriding the
+ # _BUILDTYPE_ARGS value.
+ return self._BUILDTYPE_ARGS[buildtype]
+
+ def get_asneeded_args(self) -> T.List[str]:
+ return []
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ raise EnvironmentException(
+ f'Linker {self.id} does not support link_whole')
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ raise EnvironmentException(
+ f'Linker {self.id} does not support allow undefined')
+
+ @abc.abstractmethod
+ def get_output_args(self, outname: str) -> T.List[str]:
+ pass
+
+ def get_coverage_args(self) -> T.List[str]:
+ raise EnvironmentException(f"Linker {self.id} doesn't implement coverage data generation.")
+
+ @abc.abstractmethod
+ def get_search_args(self, dirname: str) -> T.List[str]:
+ pass
+
+ def export_dynamic_args(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def import_library_args(self, implibname: str) -> T.List[str]:
+ """The name of the outputted import library.
+
+ This implementation is used only on Windows by compilers that use GNU ld
+ """
+ return []
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return []
+
+ def no_undefined_args(self) -> T.List[str]:
+ """Arguments to error if there are any undefined symbols at link time.
+
+ This is the inverse of get_allow_undefined_args().
+
+ TODO: A future cleanup might merge this and
+ get_allow_undefined_args() into a single method taking a
+ boolean
+ """
+ return []
+
+ def fatal_warnings(self) -> T.List[str]:
+ """Arguments to make all warnings errors."""
+ return []
+
+ def headerpad_args(self) -> T.List[str]:
+ # Only used by the Apple linker
+ return []
+
+ def get_gui_app_args(self, value: bool) -> T.List[str]:
+ # Only used by VisualStudioLikeLinkers
+ return []
+
+ def get_win_subsystem_args(self, value: str) -> T.List[str]:
+ # Only used if supported by the dynamic linker and
+ # only when targeting Windows
+ return []
+
+ def bitcode_args(self) -> T.List[str]:
+ raise MesonException('This linker does not support bitcode bundles')
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ return ([], set())
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ return []
+
+
+class PosixDynamicLinkerMixin:
+
+ """Mixin class for POSIX-ish linkers.
+
+ This is obviously a pretty small subset of the linker interface, but
+ enough dynamic linkers that meson supports are POSIX-like but not
+ GNU-like that it makes sense to split this out.
+ """
+
+ def get_output_args(self, outname: str) -> T.List[str]:
+ return ['-o', outname]
+
+ def get_std_shared_lib_args(self) -> T.List[str]:
+ return ['-shared']
+
+ def get_search_args(self, dirname: str) -> T.List[str]:
+ return ['-L' + dirname]
+
+
+class GnuLikeDynamicLinkerMixin:
+
+ """Mixin class for dynamic linkers that provides gnu-like interface.
+
+ This acts as a base for the GNU linkers (bfd and gold), LLVM's lld, and
+ other linkers like GNU-ld.
+ """
+
+ if T.TYPE_CHECKING:
+ for_machine = MachineChoice.HOST
+ def _apply_prefix(self, arg: T.Union[str, T.List[str]]) -> T.List[str]: ...
+
+ _BUILDTYPE_ARGS = {
+ 'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ 'release': ['-O1'],
+ 'minsize': [],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+ _SUBSYSTEMS = {
+ "native": "1",
+ "windows": "windows",
+ "console": "console",
+ "posix": "7",
+ "efi_application": "10",
+ "efi_boot_service_driver": "11",
+ "efi_runtime_driver": "12",
+ "efi_rom": "13",
+ "boot_application": "16",
+ } # type: T.Dict[str, str]
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ # We can override these in children by just overriding the
+ # _BUILDTYPE_ARGS value.
+ return mesonlib.listify([self._apply_prefix(a) for a in self._BUILDTYPE_ARGS[buildtype]])
+
+ def get_pie_args(self) -> T.List[str]:
+ return ['-pie']
+
+ def get_asneeded_args(self) -> T.List[str]:
+ return self._apply_prefix('--as-needed')
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ if not args:
+ return args
+ return self._apply_prefix('--whole-archive') + args + self._apply_prefix('--no-whole-archive')
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return self._apply_prefix('--allow-shlib-undefined')
+
+ def get_lto_args(self) -> T.List[str]:
+ return ['-flto']
+
+ def sanitizer_args(self, value: str) -> T.List[str]:
+ if value == 'none':
+ return []
+ return ['-fsanitize=' + value]
+
+ def get_coverage_args(self) -> T.List[str]:
+ return ['--coverage']
+
+ def export_dynamic_args(self, env: 'Environment') -> T.List[str]:
+ m = env.machines[self.for_machine]
+ if m.is_windows() or m.is_cygwin():
+ return self._apply_prefix('--export-all-symbols')
+ return self._apply_prefix('-export-dynamic')
+
+ def import_library_args(self, implibname: str) -> T.List[str]:
+ return self._apply_prefix('--out-implib=' + implibname)
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ if env.machines[self.for_machine].is_haiku():
+ return []
+ return ['-pthread']
+
+ def no_undefined_args(self) -> T.List[str]:
+ return self._apply_prefix('--no-undefined')
+
+ def fatal_warnings(self) -> T.List[str]:
+ return self._apply_prefix('--fatal-warnings')
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ m = env.machines[self.for_machine]
+ if m.is_windows() or m.is_cygwin():
+ # For PE/COFF the soname argument has no effect
+ return []
+ sostr = '' if soversion is None else '.' + soversion
+ return self._apply_prefix(f'-soname,{prefix}{shlib_name}.{suffix}{sostr}')
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ m = env.machines[self.for_machine]
+ if m.is_windows() or m.is_cygwin():
+ return ([], set())
+ if not rpath_paths and not install_rpath and not build_rpath:
+ return ([], set())
+ args = []
+ origin_placeholder = '$ORIGIN'
+ processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir)
+ # Need to deduplicate rpaths, as macOS's install_name_tool
+ # is *very* allergic to duplicate -delete_rpath arguments
+ # when calling depfixer on installation.
+ all_paths = mesonlib.OrderedSet([os.path.join(origin_placeholder, p) for p in processed_rpaths])
+ rpath_dirs_to_remove = set()
+ for p in all_paths:
+ rpath_dirs_to_remove.add(p.encode('utf8'))
+ # Build_rpath is used as-is (it is usually absolute).
+ if build_rpath != '':
+ all_paths.add(build_rpath)
+ for p in build_rpath.split(':'):
+ rpath_dirs_to_remove.add(p.encode('utf8'))
+
+ # TODO: should this actually be "for (dragonfly|open)bsd"?
+ if mesonlib.is_dragonflybsd() or mesonlib.is_openbsd():
+ # This argument instructs the compiler to record the value of
+ # ORIGIN in the .dynamic section of the elf. On Linux this is done
+ # by default, but is not on dragonfly/openbsd for some reason. Without this
+ # $ORIGIN in the runtime path will be undefined and any binaries
+ # linked against local libraries will fail to resolve them.
+ args.extend(self._apply_prefix('-z,origin'))
+
+ # In order to avoid relinking for RPATH removal, the binary needs to contain just
+ # enough space in the ELF header to hold the final installation RPATH.
+ paths = ':'.join(all_paths)
+ if len(paths) < len(install_rpath):
+ padding = 'X' * (len(install_rpath) - len(paths))
+ if not paths:
+ paths = padding
+ else:
+ paths = paths + ':' + padding
+ args.extend(self._apply_prefix('-rpath,' + paths))
+
+ # TODO: should this actually be "for solaris/sunos"?
+ if mesonlib.is_sunos():
+ return (args, rpath_dirs_to_remove)
+
+ # Rpaths to use while linking must be absolute. These are not
+ # written to the binary. Needed only with GNU ld:
+ # https://sourceware.org/bugzilla/show_bug.cgi?id=16936
+ # Not needed on Windows or other platforms that don't use RPATH
+ # https://github.com/mesonbuild/meson/issues/1897
+ #
+ # In addition, this linker option tends to be quite long and some
+ # compilers have trouble dealing with it. That's why we will include
+ # one option per folder, like this:
+ #
+ # -Wl,-rpath-link,/path/to/folder1 -Wl,-rpath,/path/to/folder2 ...
+ #
+ # ...instead of just one single looooong option, like this:
+ #
+ # -Wl,-rpath-link,/path/to/folder1:/path/to/folder2:...
+ for p in rpath_paths:
+ args.extend(self._apply_prefix('-rpath-link,' + os.path.join(build_dir, p)))
+
+ return (args, rpath_dirs_to_remove)
+
+ def get_win_subsystem_args(self, value: str) -> T.List[str]:
+ # MinGW only directly supports a couple of the possible
+ # PE application types. The raw integer works as an argument
+ # as well, and is always accepted, so we manually map the
+ # other types here. List of all types:
+ # https://github.com/wine-mirror/wine/blob/3ded60bd1654dc689d24a23305f4a93acce3a6f2/include/winnt.h#L2492-L2507
+ versionsuffix = None
+ if ',' in value:
+ value, versionsuffix = value.split(',', 1)
+ newvalue = self._SUBSYSTEMS.get(value)
+ if newvalue is not None:
+ if versionsuffix is not None:
+ newvalue += f':{versionsuffix}'
+ args = [f'--subsystem,{newvalue}']
+ else:
+ raise mesonlib.MesonBugException(f'win_subsystem: {value!r} not handled in MinGW linker. This should not be possible.')
+
+ return self._apply_prefix(args)
+
+
+class AppleDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker):
+
+ """Apple's ld implementation."""
+
+ id = 'ld64'
+
+ def get_asneeded_args(self) -> T.List[str]:
+ return self._apply_prefix('-dead_strip_dylibs')
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return self._apply_prefix('-undefined,dynamic_lookup')
+
+ def get_std_shared_module_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
+ return ['-bundle'] + self._apply_prefix('-undefined,dynamic_lookup')
+
+ def get_pie_args(self) -> T.List[str]:
+ return []
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ result = [] # type: T.List[str]
+ for a in args:
+ result.extend(self._apply_prefix('-force_load'))
+ result.append(a)
+ return result
+
+ def get_coverage_args(self) -> T.List[str]:
+ return ['--coverage']
+
+ def sanitizer_args(self, value: str) -> T.List[str]:
+ if value == 'none':
+ return []
+ return ['-fsanitize=' + value]
+
+ def no_undefined_args(self) -> T.List[str]:
+ return self._apply_prefix('-undefined,error')
+
+ def headerpad_args(self) -> T.List[str]:
+ return self._apply_prefix('-headerpad_max_install_names')
+
+ def bitcode_args(self) -> T.List[str]:
+ return self._apply_prefix('-bitcode_bundle')
+
+ def fatal_warnings(self) -> T.List[str]:
+ return self._apply_prefix('-fatal_warnings')
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ install_name = ['@rpath/', prefix, shlib_name]
+ if soversion is not None:
+ install_name.append('.' + soversion)
+ install_name.append('.dylib')
+ args = ['-install_name', ''.join(install_name)]
+ if darwin_versions:
+ args.extend(['-compatibility_version', darwin_versions[0],
+ '-current_version', darwin_versions[1]])
+ return args
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ if not rpath_paths and not install_rpath and not build_rpath:
+ return ([], set())
+ args = []
+ # @loader_path is the equivalent of $ORIGIN on macOS
+ # https://stackoverflow.com/q/26280738
+ origin_placeholder = '@loader_path'
+ processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir)
+ all_paths = mesonlib.OrderedSet([os.path.join(origin_placeholder, p) for p in processed_rpaths])
+ if build_rpath != '':
+ all_paths.add(build_rpath)
+ for rp in all_paths:
+ args.extend(self._apply_prefix('-rpath,' + rp))
+
+ return (args, set())
+
+ def get_thinlto_cache_args(self, path: str) -> T.List[str]:
+ return ["-Wl,-cache_path_lto," + path]
+
+
+class LLVMLD64DynamicLinker(AppleDynamicLinker):
+
+ id = 'ld64.lld'
+
+
+class GnuDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, DynamicLinker):
+
+ """Representation of GNU ld.bfd and ld.gold."""
+
+ def get_accepts_rsp(self) -> bool:
+ return True
+
+
+class GnuGoldDynamicLinker(GnuDynamicLinker):
+
+ id = 'ld.gold'
+
+ def get_thinlto_cache_args(self, path: str) -> T.List[str]:
+ return ['-Wl,-plugin-opt,cache-dir=' + path]
+
+
+class GnuBFDDynamicLinker(GnuDynamicLinker):
+
+ id = 'ld.bfd'
+
+
+class MoldDynamicLinker(GnuDynamicLinker):
+
+ id = 'ld.mold'
+
+ def get_thinlto_cache_args(self, path: str) -> T.List[str]:
+ return ['-Wl,--thinlto-cache-dir=' + path]
+
+
+class LLVMDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, DynamicLinker):
+
+ """Representation of LLVM's ld.lld linker.
+
+ This is only the gnu-like linker, not the apple like or link.exe like
+ linkers.
+ """
+
+ id = 'ld.lld'
+
+ def __init__(self, exelist: T.List[str],
+ for_machine: mesonlib.MachineChoice, prefix_arg: T.Union[str, T.List[str]],
+ always_args: T.List[str], *, version: str = 'unknown version'):
+ super().__init__(exelist, for_machine, prefix_arg, always_args, version=version)
+
+ # Some targets don't seem to support this argument (windows, wasm, ...)
+ _, _, e = mesonlib.Popen_safe(self.exelist + always_args + self._apply_prefix('--allow-shlib-undefined'))
+ # Versions < 9 do not have a quoted argument
+ self.has_allow_shlib_undefined = ('unknown argument: --allow-shlib-undefined' not in e) and ("unknown argument: '--allow-shlib-undefined'" not in e)
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ if self.has_allow_shlib_undefined:
+ return self._apply_prefix('--allow-shlib-undefined')
+ return []
+
+ def get_thinlto_cache_args(self, path: str) -> T.List[str]:
+ return ['-Wl,--thinlto-cache-dir=' + path]
+
+ def get_win_subsystem_args(self, value: str) -> T.List[str]:
+ # lld does not support a numeric subsystem value
+ version = None
+ if ',' in value:
+ value, version = value.split(',', 1)
+ if value in self._SUBSYSTEMS:
+ if version is not None:
+ value += f':{version}'
+ return self._apply_prefix([f'--subsystem,{value}'])
+ else:
+ raise mesonlib.MesonBugException(f'win_subsystem: {value} not handled in lld linker. This should not be possible.')
+
+
+class WASMDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, DynamicLinker):
+
+ """Emscripten's wasm-ld."""
+
+ id = 'ld.wasm'
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return ['-sERROR_ON_UNDEFINED_SYMBOLS=0']
+
+ def no_undefined_args(self) -> T.List[str]:
+ return ['-sERROR_ON_UNDEFINED_SYMBOLS=1']
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ raise MesonException(f'{self.id} does not support shared libraries.')
+
+ def get_asneeded_args(self) -> T.List[str]:
+ return []
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ return ([], set())
+
+
+class CcrxDynamicLinker(DynamicLinker):
+
+ """Linker for Renesis CCrx compiler."""
+
+ id = 'rlink'
+
+ def __init__(self, for_machine: mesonlib.MachineChoice,
+ *, version: str = 'unknown version'):
+ super().__init__(['rlink.exe'], for_machine, '', [],
+ version=version)
+
+ def get_accepts_rsp(self) -> bool:
+ return False
+
+ def get_lib_prefix(self) -> str:
+ return '-lib='
+
+ def get_std_shared_lib_args(self) -> T.List[str]:
+ return []
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return [f'-output={outputname}']
+
+ def get_search_args(self, dirname: str) -> 'T.NoReturn':
+ raise OSError('rlink.exe does not have a search dir argument')
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return []
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ return []
+
+
+class Xc16DynamicLinker(DynamicLinker):
+
+ """Linker for Microchip XC16 compiler."""
+
+ id = 'xc16-gcc'
+
+ def __init__(self, for_machine: mesonlib.MachineChoice,
+ *, version: str = 'unknown version'):
+ super().__init__(['xc16-gcc'], for_machine, '', [],
+ version=version)
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ if not args:
+ return args
+ return self._apply_prefix('--start-group') + args + self._apply_prefix('--end-group')
+
+ def get_accepts_rsp(self) -> bool:
+ return False
+
+ def get_lib_prefix(self) -> str:
+ return ''
+
+ def get_std_shared_lib_args(self) -> T.List[str]:
+ return []
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return [f'-o{outputname}']
+
+ def get_search_args(self, dirname: str) -> 'T.NoReturn':
+ raise OSError('xc16-gcc does not have a search dir argument')
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return []
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ return []
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ return ([], set())
+
+class CompCertDynamicLinker(DynamicLinker):
+
+ """Linker for CompCert C compiler."""
+
+ id = 'ccomp'
+
+ def __init__(self, for_machine: mesonlib.MachineChoice,
+ *, version: str = 'unknown version'):
+ super().__init__(['ccomp'], for_machine, '', [],
+ version=version)
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ if not args:
+ return args
+ return self._apply_prefix('-Wl,--whole-archive') + args + self._apply_prefix('-Wl,--no-whole-archive')
+
+ def get_accepts_rsp(self) -> bool:
+ return False
+
+ def get_lib_prefix(self) -> str:
+ return ''
+
+ def get_std_shared_lib_args(self) -> T.List[str]:
+ return []
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return [f'-o{outputname}']
+
+ def get_search_args(self, dirname: str) -> T.List[str]:
+ return [f'-L{dirname}']
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return []
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ raise MesonException(f'{self.id} does not support shared libraries.')
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ return ([], set())
+
+class TIDynamicLinker(DynamicLinker):
+
+ """Linker for Texas Instruments compiler family."""
+
+ id = 'ti'
+
+ def __init__(self, exelist: T.List[str], for_machine: mesonlib.MachineChoice,
+ *, version: str = 'unknown version'):
+ super().__init__(exelist, for_machine, '', [],
+ version=version)
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ if not args:
+ return args
+ return self._apply_prefix('--start-group') + args + self._apply_prefix('--end-group')
+
+ def get_accepts_rsp(self) -> bool:
+ return False
+
+ def get_lib_prefix(self) -> str:
+ return '-l='
+
+ def get_std_shared_lib_args(self) -> T.List[str]:
+ return []
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return ['-z', f'--output_file={outputname}']
+
+ def get_search_args(self, dirname: str) -> 'T.NoReturn':
+ raise OSError('TI compilers do not have a search dir argument')
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return []
+
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+
+class C2000DynamicLinker(TIDynamicLinker):
+ # Required for backwards compat with projects created before ti-cgt support existed
+ id = 'cl2000'
+
+
+class ArmDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker):
+
+ """Linker for the ARM compiler."""
+
+ id = 'armlink'
+
+ def __init__(self, for_machine: mesonlib.MachineChoice,
+ *, version: str = 'unknown version'):
+ super().__init__(['armlink'], for_machine, '', [],
+ version=version)
+
+ def get_accepts_rsp(self) -> bool:
+ return False
+
+ def get_std_shared_lib_args(self) -> 'T.NoReturn':
+ raise MesonException('The Arm Linkers do not support shared libraries')
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return []
+
+
+class ArmClangDynamicLinker(ArmDynamicLinker):
+
+ """Linker used with ARM's clang fork.
+
+ The interface is similar enough to the old ARM ld that it inherits and
+ extends a few things as needed.
+ """
+
+ def export_dynamic_args(self, env: 'Environment') -> T.List[str]:
+ return ['--export_dynamic']
+
+ def import_library_args(self, implibname: str) -> T.List[str]:
+ return ['--symdefs=' + implibname]
+
+class QualcommLLVMDynamicLinker(LLVMDynamicLinker):
+
+ """ARM Linker from Snapdragon LLVM ARM Compiler."""
+
+ id = 'ld.qcld'
+
+
+class NAGDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker):
+
+ """NAG Fortran linker, ld via gcc indirection.
+
+ Using nagfor -Wl,foo passes option foo to a backend gcc invocation.
+ (This linking gathers the correct objects needed from the nagfor runtime
+ system.)
+ To pass gcc -Wl,foo options (i.e., to ld) one must apply indirection
+ again: nagfor -Wl,-Wl,,foo
+ """
+
+ id = 'nag'
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ if not rpath_paths and not install_rpath and not build_rpath:
+ return ([], set())
+ args = []
+ origin_placeholder = '$ORIGIN'
+ processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir)
+ all_paths = mesonlib.OrderedSet([os.path.join(origin_placeholder, p) for p in processed_rpaths])
+ if build_rpath != '':
+ all_paths.add(build_rpath)
+ for rp in all_paths:
+ args.extend(self._apply_prefix('-Wl,-Wl,,-rpath,,' + rp))
+
+ return (args, set())
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return []
+
+ def get_std_shared_lib_args(self) -> T.List[str]:
+ from ..compilers.fortran import NAGFortranCompiler
+ return NAGFortranCompiler.get_nagfor_quiet(self.version) + ['-Wl,-shared']
+
+
+class PGIDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker):
+
+ """PGI linker."""
+
+ id = 'pgi'
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return []
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ return []
+
+ def get_std_shared_lib_args(self) -> T.List[str]:
+ # PGI -shared is Linux only.
+ if mesonlib.is_windows():
+ return ['-Bdynamic', '-Mmakedll']
+ elif mesonlib.is_linux():
+ return ['-shared']
+ return []
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ if not env.machines[self.for_machine].is_windows():
+ return (['-R' + os.path.join(build_dir, p) for p in rpath_paths], set())
+ return ([], set())
+
+NvidiaHPC_DynamicLinker = PGIDynamicLinker
+
+
+class PGIStaticLinker(StaticLinker):
+ def __init__(self, exelist: T.List[str]):
+ super().__init__(exelist)
+ self.id = 'ar'
+ self.std_args = ['-r']
+
+ def get_std_link_args(self, env: 'Environment', is_thin: bool) -> T.List[str]:
+ return self.std_args
+
+ def get_output_args(self, target: str) -> T.List[str]:
+ return [target]
+
+NvidiaHPC_StaticLinker = PGIStaticLinker
+
+
+class VisualStudioLikeLinkerMixin:
+
+ """Mixin class for for dynamic linkers that act like Microsoft's link.exe."""
+
+ if T.TYPE_CHECKING:
+ for_machine = MachineChoice.HOST
+ def _apply_prefix(self, arg: T.Union[str, T.List[str]]) -> T.List[str]: ...
+
+ _BUILDTYPE_ARGS = {
+ 'plain': [],
+ 'debug': [],
+ 'debugoptimized': [],
+ # The otherwise implicit REF and ICF linker optimisations are disabled by
+ # /DEBUG. REF implies ICF.
+ 'release': ['/OPT:REF'],
+ 'minsize': ['/INCREMENTAL:NO', '/OPT:REF'],
+ 'custom': [],
+ } # type: T.Dict[str, T.List[str]]
+
+ def __init__(self, exelist: T.List[str], for_machine: mesonlib.MachineChoice,
+ prefix_arg: T.Union[str, T.List[str]], always_args: T.List[str], *,
+ version: str = 'unknown version', direct: bool = True, machine: str = 'x86'):
+ # There's no way I can find to make mypy understand what's going on here
+ super().__init__(exelist, for_machine, prefix_arg, always_args, version=version) # type: ignore
+ self.machine = machine
+ self.direct = direct
+
+ def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+ return mesonlib.listify([self._apply_prefix(a) for a in self._BUILDTYPE_ARGS[buildtype]])
+
+ def invoked_by_compiler(self) -> bool:
+ return not self.direct
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ return self._apply_prefix(['/MACHINE:' + self.machine, '/OUT:' + outputname])
+
+ def get_always_args(self) -> T.List[str]:
+ parent = super().get_always_args() # type: ignore
+ return self._apply_prefix('/nologo') + T.cast('T.List[str]', parent)
+
+ def get_search_args(self, dirname: str) -> T.List[str]:
+ return self._apply_prefix('/LIBPATH:' + dirname)
+
+ def get_std_shared_lib_args(self) -> T.List[str]:
+ return self._apply_prefix('/DLL')
+
+ def get_debugfile_name(self, targetfile: str) -> str:
+ basename = targetfile.rsplit('.', maxsplit=1)[0]
+ return basename + '.pdb'
+
+ def get_debugfile_args(self, targetfile: str) -> T.List[str]:
+ return self._apply_prefix(['/DEBUG', '/PDB:' + self.get_debugfile_name(targetfile)])
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ # Only since VS2015
+ args = mesonlib.listify(args)
+ l = [] # T.List[str]
+ for a in args:
+ l.extend(self._apply_prefix('/WHOLEARCHIVE:' + a))
+ return l
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return []
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ return []
+
+ def import_library_args(self, implibname: str) -> T.List[str]:
+ """The command to generate the import library."""
+ return self._apply_prefix(['/IMPLIB:' + implibname])
+
+ def rsp_file_syntax(self) -> RSPFileSyntax:
+ return RSPFileSyntax.MSVC
+
+
+class MSVCDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker):
+
+ """Microsoft's Link.exe."""
+
+ id = 'link'
+
+ def __init__(self, for_machine: mesonlib.MachineChoice, always_args: T.List[str], *,
+ exelist: T.Optional[T.List[str]] = None,
+ prefix: T.Union[str, T.List[str]] = '',
+ machine: str = 'x86', version: str = 'unknown version',
+ direct: bool = True):
+ super().__init__(exelist or ['link.exe'], for_machine,
+ prefix, always_args, machine=machine, version=version, direct=direct)
+
+ def get_always_args(self) -> T.List[str]:
+ return self._apply_prefix(['/nologo', '/release']) + super().get_always_args()
+
+ def get_gui_app_args(self, value: bool) -> T.List[str]:
+ return self.get_win_subsystem_args("windows" if value else "console")
+
+ def get_win_subsystem_args(self, value: str) -> T.List[str]:
+ return self._apply_prefix([f'/SUBSYSTEM:{value.upper()}'])
+
+
+class ClangClDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker):
+
+ """Clang's lld-link.exe."""
+
+ id = 'lld-link'
+
+ def __init__(self, for_machine: mesonlib.MachineChoice, always_args: T.List[str], *,
+ exelist: T.Optional[T.List[str]] = None,
+ prefix: T.Union[str, T.List[str]] = '',
+ machine: str = 'x86', version: str = 'unknown version',
+ direct: bool = True):
+ super().__init__(exelist or ['lld-link.exe'], for_machine,
+ prefix, always_args, machine=machine, version=version, direct=direct)
+
+ def get_output_args(self, outputname: str) -> T.List[str]:
+ # If we're being driven indirectly by clang just skip /MACHINE
+ # as clang's target triple will handle the machine selection
+ if self.machine is None:
+ return self._apply_prefix([f"/OUT:{outputname}"])
+
+ return super().get_output_args(outputname)
+
+ def get_gui_app_args(self, value: bool) -> T.List[str]:
+ return self.get_win_subsystem_args("windows" if value else "console")
+
+ def get_win_subsystem_args(self, value: str) -> T.List[str]:
+ return self._apply_prefix([f'/SUBSYSTEM:{value.upper()}'])
+
+ def get_thinlto_cache_args(self, path: str) -> T.List[str]:
+ return ["/lldltocache:" + path]
+
+
+class XilinkDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker):
+
+ """Intel's Xilink.exe."""
+
+ id = 'xilink'
+
+ def __init__(self, for_machine: mesonlib.MachineChoice, always_args: T.List[str], *,
+ exelist: T.Optional[T.List[str]] = None,
+ prefix: T.Union[str, T.List[str]] = '',
+ machine: str = 'x86', version: str = 'unknown version',
+ direct: bool = True):
+ super().__init__(['xilink.exe'], for_machine, '', always_args, version=version)
+
+ def get_gui_app_args(self, value: bool) -> T.List[str]:
+ return self.get_win_subsystem_args("windows" if value else "console")
+
+ def get_win_subsystem_args(self, value: str) -> T.List[str]:
+ return self._apply_prefix([f'/SUBSYSTEM:{value.upper()}'])
+
+
+class SolarisDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker):
+
+ """Sys-V derived linker used on Solaris and OpenSolaris."""
+
+ id = 'ld.solaris'
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ if not args:
+ return args
+ return self._apply_prefix('--whole-archive') + args + self._apply_prefix('--no-whole-archive')
+
+ def get_pie_args(self) -> T.List[str]:
+ # Available in Solaris 11.2 and later
+ pc, stdo, stde = mesonlib.Popen_safe(self.exelist + self._apply_prefix('-zhelp'))
+ for line in (stdo + stde).split('\n'):
+ if '-z type' in line:
+ if 'pie' in line:
+ return ['-z', 'type=pie']
+ break
+ return []
+
+ def get_asneeded_args(self) -> T.List[str]:
+ return self._apply_prefix(['-z', 'ignore'])
+
+ def no_undefined_args(self) -> T.List[str]:
+ return ['-z', 'defs']
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return ['-z', 'nodefs']
+
+ def fatal_warnings(self) -> T.List[str]:
+ return ['-z', 'fatal-warnings']
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ if not rpath_paths and not install_rpath and not build_rpath:
+ return ([], set())
+ processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir)
+ all_paths = mesonlib.OrderedSet([os.path.join('$ORIGIN', p) for p in processed_rpaths])
+ rpath_dirs_to_remove = set()
+ for p in all_paths:
+ rpath_dirs_to_remove.add(p.encode('utf8'))
+ if build_rpath != '':
+ all_paths.add(build_rpath)
+ for p in build_rpath.split(':'):
+ rpath_dirs_to_remove.add(p.encode('utf8'))
+
+ # In order to avoid relinking for RPATH removal, the binary needs to contain just
+ # enough space in the ELF header to hold the final installation RPATH.
+ paths = ':'.join(all_paths)
+ if len(paths) < len(install_rpath):
+ padding = 'X' * (len(install_rpath) - len(paths))
+ if not paths:
+ paths = padding
+ else:
+ paths = paths + ':' + padding
+ return (self._apply_prefix(f'-rpath,{paths}'), rpath_dirs_to_remove)
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ sostr = '' if soversion is None else '.' + soversion
+ return self._apply_prefix(f'-soname,{prefix}{shlib_name}.{suffix}{sostr}')
+
+
+class AIXDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker):
+
+ """Sys-V derived linker used on AIX"""
+
+ id = 'ld.aix'
+
+ def get_always_args(self) -> T.List[str]:
+ return self._apply_prefix(['-bnoipath', '-bbigtoc']) + super().get_always_args()
+
+ def no_undefined_args(self) -> T.List[str]:
+ return self._apply_prefix(['-bernotok'])
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return self._apply_prefix(['-berok'])
+
+ def get_link_whole_for(self, args: T.List[str]) -> T.List[str]:
+ # AIX's linker always links the whole archive: "The ld command
+ # processes all input files in the same manner, whether they are
+ # archives or not."
+ return args
+
+ def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str,
+ rpath_paths: T.Tuple[str, ...], build_rpath: str,
+ install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
+ all_paths = mesonlib.OrderedSet() # type: mesonlib.OrderedSet[str]
+ # install_rpath first, followed by other paths, and the system path last
+ if install_rpath != '':
+ all_paths.add(install_rpath)
+ if build_rpath != '':
+ all_paths.add(build_rpath)
+ for p in rpath_paths:
+ all_paths.add(os.path.join(build_dir, p))
+ # We should consider allowing the $LIBPATH environment variable
+ # to override sys_path.
+ sys_path = env.get_compiler_system_dirs(self.for_machine)
+ if len(sys_path) == 0:
+ # get_compiler_system_dirs doesn't support our compiler.
+ # Use the default system library path
+ all_paths.update(['/usr/lib', '/lib'])
+ else:
+ # Include the compiler's default library paths, but filter out paths that don't exist
+ for p in sys_path:
+ if os.path.isdir(p):
+ all_paths.add(p)
+ return (self._apply_prefix('-blibpath:' + ':'.join(all_paths)), set())
+
+ def thread_flags(self, env: 'Environment') -> T.List[str]:
+ return ['-pthread']
+
+
+class OptlinkDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker):
+
+ """Digital Mars dynamic linker for windows."""
+
+ id = 'optlink'
+
+ def __init__(self, exelist: T.List[str], for_machine: mesonlib.MachineChoice,
+ *, version: str = 'unknown version'):
+ # Use optlink instead of link so we don't interfere with other link.exe
+ # implementations.
+ super().__init__(exelist, for_machine, '', [], version=version)
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return []
+
+ def get_debugfile_args(self, targetfile: str) -> T.List[str]:
+ # Optlink does not generate pdb files.
+ return []
+
+ def get_always_args(self) -> T.List[str]:
+ return []
+
+
+class CudaLinker(PosixDynamicLinkerMixin, DynamicLinker):
+ """Cuda linker (nvlink)"""
+
+ id = 'nvlink'
+
+ @staticmethod
+ def parse_version() -> str:
+ version_cmd = ['nvlink', '--version']
+ try:
+ _, out, _ = mesonlib.Popen_safe(version_cmd)
+ except OSError:
+ return 'unknown version'
+ # Output example:
+ # nvlink: NVIDIA (R) Cuda linker
+ # Copyright (c) 2005-2018 NVIDIA Corporation
+ # Built on Sun_Sep_30_21:09:22_CDT_2018
+ # Cuda compilation tools, release 10.0, V10.0.166
+ # we need the most verbose version output. Luckily starting with V
+ return out.strip().rsplit('V', maxsplit=1)[-1]
+
+ def get_accepts_rsp(self) -> bool:
+ # nvcc does not support response files
+ return False
+
+ def get_lib_prefix(self) -> str:
+ # nvcc doesn't recognize Meson's default .a extension for static libraries on
+ # Windows and passes it to cl as an object file, resulting in 'warning D9024 :
+ # unrecognized source file type 'xxx.a', object file assumed'.
+ #
+ # nvcc's --library= option doesn't help: it takes the library name without the
+ # extension and assumes that the extension on Windows is .lib; prefixing the
+ # library with -Xlinker= seems to work.
+ #
+ # On Linux, we have to use rely on -Xlinker= too, since nvcc/nvlink chokes on
+ # versioned shared libraries:
+ #
+ # nvcc fatal : Don't know what to do with 'subprojects/foo/libbar.so.0.1.2'
+ #
+ from ..compilers.cuda import CudaCompiler
+ return CudaCompiler.LINKER_PREFIX
+
+ def fatal_warnings(self) -> T.List[str]:
+ return ['--warning-as-error']
+
+ def get_allow_undefined_args(self) -> T.List[str]:
+ return []
+
+ def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str,
+ suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]:
+ return []
diff --git a/mesonbuild/mcompile.py b/mesonbuild/mcompile.py
new file mode 100644
index 0000000..2f5cee9
--- /dev/null
+++ b/mesonbuild/mcompile.py
@@ -0,0 +1,361 @@
+# Copyright 2020 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.
+from __future__ import annotations
+
+"""Entrypoint script for backend agnostic compile."""
+
+import os
+import json
+import re
+import sys
+import shutil
+import typing as T
+from collections import defaultdict
+from pathlib import Path
+
+from . import mlog
+from . import mesonlib
+from . import coredata
+from .mesonlib import MesonException, RealPathAction, join_args, setup_vsenv
+from mesonbuild.environment import detect_ninja
+from mesonbuild.coredata import UserArrayOption
+from mesonbuild import build
+
+if T.TYPE_CHECKING:
+ import argparse
+
+def array_arg(value: str) -> T.List[str]:
+ return UserArrayOption(None, value, allow_dups=True, user_input=True).value
+
+def validate_builddir(builddir: Path) -> None:
+ if not (builddir / 'meson-private' / 'coredata.dat').is_file():
+ raise MesonException(f'Current directory is not a meson build directory: `{builddir}`.\n'
+ 'Please specify a valid build dir or change the working directory to it.\n'
+ 'It is also possible that the build directory was generated with an old\n'
+ 'meson version. Please regenerate it in this case.')
+
+def parse_introspect_data(builddir: Path) -> T.Dict[str, T.List[dict]]:
+ """
+ Converts a List of name-to-dict to a dict of name-to-dicts (since names are not unique)
+ """
+ path_to_intro = builddir / 'meson-info' / 'intro-targets.json'
+ if not path_to_intro.exists():
+ raise MesonException(f'`{path_to_intro.name}` is missing! Directory is not configured yet?')
+ with path_to_intro.open(encoding='utf-8') as f:
+ schema = json.load(f)
+
+ parsed_data = defaultdict(list) # type: T.Dict[str, T.List[dict]]
+ for target in schema:
+ parsed_data[target['name']] += [target]
+ return parsed_data
+
+class ParsedTargetName:
+ full_name = ''
+ name = ''
+ type = ''
+ path = ''
+
+ def __init__(self, target: str):
+ self.full_name = target
+ split = target.rsplit(':', 1)
+ if len(split) > 1:
+ self.type = split[1]
+ if not self._is_valid_type(self.type):
+ raise MesonException(f'Can\'t invoke target `{target}`: unknown target type: `{self.type}`')
+
+ split = split[0].rsplit('/', 1)
+ if len(split) > 1:
+ self.path = split[0]
+ self.name = split[1]
+ else:
+ self.name = split[0]
+
+ @staticmethod
+ def _is_valid_type(type: str) -> bool:
+ # Amend docs in Commands.md when editing this list
+ allowed_types = {
+ 'executable',
+ 'static_library',
+ 'shared_library',
+ 'shared_module',
+ 'custom',
+ 'run',
+ 'jar',
+ }
+ return type in allowed_types
+
+def get_target_from_intro_data(target: ParsedTargetName, builddir: Path, introspect_data: T.Dict[str, T.Any]) -> T.Dict[str, T.Any]:
+ if target.name not in introspect_data:
+ raise MesonException(f'Can\'t invoke target `{target.full_name}`: target not found')
+
+ intro_targets = introspect_data[target.name]
+ found_targets = [] # type: T.List[T.Dict[str, T.Any]]
+
+ resolved_bdir = builddir.resolve()
+
+ if not target.type and not target.path:
+ found_targets = intro_targets
+ else:
+ for intro_target in intro_targets:
+ if (intro_target['subproject'] or
+ (target.type and target.type != intro_target['type'].replace(' ', '_')) or
+ (target.path
+ and intro_target['filename'] != 'no_name'
+ and Path(target.path) != Path(intro_target['filename'][0]).relative_to(resolved_bdir).parent)):
+ continue
+ found_targets += [intro_target]
+
+ if not found_targets:
+ raise MesonException(f'Can\'t invoke target `{target.full_name}`: target not found')
+ elif len(found_targets) > 1:
+ suggestions: T.List[str] = []
+ for i in found_targets:
+ p = Path(i['filename'][0]).relative_to(resolved_bdir)
+ t = i['type'].replace(' ', '_')
+ suggestions.append(f'- ./{p}:{t}')
+ suggestions_str = '\n'.join(suggestions)
+ raise MesonException(f'Can\'t invoke target `{target.full_name}`: ambiguous name.'
+ f'Add target type and/or path:\n{suggestions_str}')
+
+ return found_targets[0]
+
+def generate_target_names_ninja(target: ParsedTargetName, builddir: Path, introspect_data: dict) -> T.List[str]:
+ intro_target = get_target_from_intro_data(target, builddir, introspect_data)
+
+ if intro_target['type'] == 'run':
+ return [target.name]
+ else:
+ return [str(Path(out_file).relative_to(builddir.resolve())) for out_file in intro_target['filename']]
+
+def get_parsed_args_ninja(options: 'argparse.Namespace', builddir: Path) -> T.Tuple[T.List[str], T.Optional[T.Dict[str, str]]]:
+ runner = detect_ninja()
+ if runner is None:
+ raise MesonException('Cannot find ninja.')
+
+ cmd = runner
+ if not builddir.samefile('.'):
+ cmd.extend(['-C', builddir.as_posix()])
+
+ # If the value is set to < 1 then don't set anything, which let's
+ # ninja/samu decide what to do.
+ if options.jobs > 0:
+ cmd.extend(['-j', str(options.jobs)])
+ if options.load_average > 0:
+ cmd.extend(['-l', str(options.load_average)])
+
+ if options.verbose:
+ cmd.append('-v')
+
+ cmd += options.ninja_args
+
+ # operands must be processed after options/option-arguments
+ if options.targets:
+ intro_data = parse_introspect_data(builddir)
+ for t in options.targets:
+ cmd.extend(generate_target_names_ninja(ParsedTargetName(t), builddir, intro_data))
+ if options.clean:
+ cmd.append('clean')
+
+ return cmd, None
+
+def generate_target_name_vs(target: ParsedTargetName, builddir: Path, introspect_data: dict) -> str:
+ intro_target = get_target_from_intro_data(target, builddir, introspect_data)
+
+ assert intro_target['type'] != 'run', 'Should not reach here: `run` targets must be handle above'
+
+ # Normalize project name
+ # Source: https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-build-specific-targets-in-solutions-by-using-msbuild-exe
+ target_name = re.sub(r"[\%\$\@\;\.\(\)']", '_', intro_target['id']) # type: str
+ rel_path = Path(intro_target['filename'][0]).relative_to(builddir.resolve()).parent
+ if rel_path != Path('.'):
+ target_name = str(rel_path / target_name)
+ return target_name
+
+def get_parsed_args_vs(options: 'argparse.Namespace', builddir: Path) -> T.Tuple[T.List[str], T.Optional[T.Dict[str, str]]]:
+ slns = list(builddir.glob('*.sln'))
+ assert len(slns) == 1, 'More than one solution in a project?'
+ sln = slns[0]
+
+ cmd = ['msbuild']
+
+ if options.targets:
+ intro_data = parse_introspect_data(builddir)
+ has_run_target = any(
+ get_target_from_intro_data(ParsedTargetName(t), builddir, intro_data)['type'] == 'run'
+ for t in options.targets)
+
+ if has_run_target:
+ # `run` target can't be used the same way as other targets on `vs` backend.
+ # They are defined as disabled projects, which can't be invoked as `.sln`
+ # target and have to be invoked directly as project instead.
+ # Issue: https://github.com/microsoft/msbuild/issues/4772
+
+ if len(options.targets) > 1:
+ raise MesonException('Only one target may be specified when `run` target type is used on this backend.')
+ intro_target = get_target_from_intro_data(ParsedTargetName(options.targets[0]), builddir, intro_data)
+ proj_dir = Path(intro_target['filename'][0]).parent
+ proj = proj_dir/'{}.vcxproj'.format(intro_target['id'])
+ cmd += [str(proj.resolve())]
+ else:
+ cmd += [str(sln.resolve())]
+ cmd.extend(['-target:{}'.format(generate_target_name_vs(ParsedTargetName(t), builddir, intro_data)) for t in options.targets])
+ else:
+ cmd += [str(sln.resolve())]
+
+ if options.clean:
+ cmd.extend(['-target:Clean'])
+
+ # In msbuild `-maxCpuCount` with no number means "detect cpus", the default is `-maxCpuCount:1`
+ if options.jobs > 0:
+ cmd.append(f'-maxCpuCount:{options.jobs}')
+ else:
+ cmd.append('-maxCpuCount')
+
+ if options.load_average:
+ mlog.warning('Msbuild does not have a load-average switch, ignoring.')
+
+ if not options.verbose:
+ cmd.append('-verbosity:minimal')
+
+ cmd += options.vs_args
+
+ # Remove platform from env if set so that msbuild does not
+ # pick x86 platform when solution platform is Win32
+ env = os.environ.copy()
+ env.pop('PLATFORM', None)
+
+ return cmd, env
+
+def get_parsed_args_xcode(options: 'argparse.Namespace', builddir: Path) -> T.Tuple[T.List[str], T.Optional[T.Dict[str, str]]]:
+ runner = 'xcodebuild'
+ if not shutil.which(runner):
+ raise MesonException('Cannot find xcodebuild, did you install XCode?')
+
+ # No argument to switch directory
+ os.chdir(str(builddir))
+
+ cmd = [runner, '-parallelizeTargets']
+
+ if options.targets:
+ for t in options.targets:
+ cmd += ['-target', t]
+
+ if options.clean:
+ if options.targets:
+ cmd += ['clean']
+ else:
+ cmd += ['-alltargets', 'clean']
+ # Otherwise xcodebuild tries to delete the builddir and fails
+ cmd += ['-UseNewBuildSystem=FALSE']
+
+ if options.jobs > 0:
+ cmd.extend(['-jobs', str(options.jobs)])
+
+ if options.load_average > 0:
+ mlog.warning('xcodebuild does not have a load-average switch, ignoring')
+
+ if options.verbose:
+ # xcodebuild is already quite verbose, and -quiet doesn't print any
+ # status messages
+ pass
+
+ cmd += options.xcode_args
+ return cmd, None
+
+def add_arguments(parser: 'argparse.ArgumentParser') -> None:
+ """Add compile specific arguments."""
+ parser.add_argument(
+ 'targets',
+ metavar='TARGET',
+ nargs='*',
+ default=None,
+ help='Targets to build. Target has the following format: [PATH_TO_TARGET/]TARGET_NAME[:TARGET_TYPE].')
+ parser.add_argument(
+ '--clean',
+ action='store_true',
+ help='Clean the build directory.'
+ )
+ parser.add_argument('-C', dest='wd', action=RealPathAction,
+ help='directory to cd into before running')
+
+ parser.add_argument(
+ '-j', '--jobs',
+ action='store',
+ default=0,
+ type=int,
+ help='The number of worker jobs to run (if supported). If the value is less than 1 the build program will guess.'
+ )
+ parser.add_argument(
+ '-l', '--load-average',
+ action='store',
+ default=0,
+ type=float,
+ help='The system load average to try to maintain (if supported).'
+ )
+ parser.add_argument(
+ '-v', '--verbose',
+ action='store_true',
+ help='Show more verbose output.'
+ )
+ parser.add_argument(
+ '--ninja-args',
+ type=array_arg,
+ default=[],
+ help='Arguments to pass to `ninja` (applied only on `ninja` backend).'
+ )
+ parser.add_argument(
+ '--vs-args',
+ type=array_arg,
+ default=[],
+ help='Arguments to pass to `msbuild` (applied only on `vs` backend).'
+ )
+ parser.add_argument(
+ '--xcode-args',
+ type=array_arg,
+ default=[],
+ help='Arguments to pass to `xcodebuild` (applied only on `xcode` backend).'
+ )
+
+def run(options: 'argparse.Namespace') -> int:
+ bdir = Path(options.wd)
+ validate_builddir(bdir)
+ if options.targets and options.clean:
+ raise MesonException('`TARGET` and `--clean` can\'t be used simultaneously')
+
+ cdata = coredata.load(options.wd)
+ b = build.load(options.wd)
+ vsenv_active = setup_vsenv(b.need_vsenv)
+ if vsenv_active:
+ mlog.log(mlog.green('INFO:'), 'automatically activated MSVC compiler environment')
+
+ cmd = [] # type: T.List[str]
+ env = None # type: T.Optional[T.Dict[str, str]]
+
+ backend = cdata.get_option(mesonlib.OptionKey('backend'))
+ assert isinstance(backend, str)
+ mlog.log(mlog.green('INFO:'), 'autodetecting backend as', backend)
+ if backend == 'ninja':
+ cmd, env = get_parsed_args_ninja(options, bdir)
+ elif backend.startswith('vs'):
+ cmd, env = get_parsed_args_vs(options, bdir)
+ elif backend == 'xcode':
+ cmd, env = get_parsed_args_xcode(options, bdir)
+ else:
+ raise MesonException(
+ f'Backend `{backend}` is not yet supported by `compile`. Use generated project files directly instead.')
+
+ mlog.log(mlog.green('INFO:'), 'calculating backend command to run:', join_args(cmd))
+ p, *_ = mesonlib.Popen_safe(cmd, stdout=sys.stdout.buffer, stderr=sys.stderr.buffer, env=env)
+
+ return p.returncode
diff --git a/mesonbuild/mconf.py b/mesonbuild/mconf.py
new file mode 100644
index 0000000..46d0463
--- /dev/null
+++ b/mesonbuild/mconf.py
@@ -0,0 +1,331 @@
+# Copyright 2014-2016 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.
+from __future__ import annotations
+
+import itertools
+import shutil
+import os
+import textwrap
+import typing as T
+import collections
+
+from . import build
+from . import coredata
+from . import environment
+from . import mesonlib
+from . import mintro
+from . import mlog
+from .ast import AstIDGenerator
+from .mesonlib import MachineChoice, OptionKey
+
+if T.TYPE_CHECKING:
+ import argparse
+
+def add_arguments(parser: 'argparse.ArgumentParser') -> None:
+ coredata.register_builtin_arguments(parser)
+ parser.add_argument('builddir', nargs='?', default='.')
+ parser.add_argument('--clearcache', action='store_true', default=False,
+ help='Clear cached state (e.g. found dependencies)')
+ parser.add_argument('--no-pager', action='store_false', dest='pager',
+ help='Do not redirect output to a pager')
+
+def stringify(val: T.Any) -> T.Union[str, T.List[T.Any]]: # T.Any because of recursion...
+ if isinstance(val, bool):
+ return str(val).lower()
+ elif isinstance(val, list):
+ s = ', '.join(stringify(i) for i in val)
+ return f'[{s}]'
+ elif val is None:
+ return ''
+ else:
+ return str(val)
+
+
+class ConfException(mesonlib.MesonException):
+ pass
+
+
+class Conf:
+ def __init__(self, build_dir):
+ self.build_dir = os.path.abspath(os.path.realpath(build_dir))
+ if 'meson.build' in [os.path.basename(self.build_dir), self.build_dir]:
+ self.build_dir = os.path.dirname(self.build_dir)
+ self.build = None
+ self.max_choices_line_length = 60
+ self.name_col = []
+ self.value_col = []
+ self.choices_col = []
+ self.descr_col = []
+ self.all_subprojects: T.Set[str] = set()
+
+ if os.path.isdir(os.path.join(self.build_dir, 'meson-private')):
+ self.build = build.load(self.build_dir)
+ self.source_dir = self.build.environment.get_source_dir()
+ self.coredata = coredata.load(self.build_dir)
+ self.default_values_only = False
+ elif os.path.isfile(os.path.join(self.build_dir, environment.build_filename)):
+ # Make sure that log entries in other parts of meson don't interfere with the JSON output
+ mlog.disable()
+ self.source_dir = os.path.abspath(os.path.realpath(self.build_dir))
+ intr = mintro.IntrospectionInterpreter(self.source_dir, '', 'ninja', visitors = [AstIDGenerator()])
+ intr.analyze()
+ # Re-enable logging just in case
+ mlog.enable()
+ self.coredata = intr.coredata
+ self.default_values_only = True
+ else:
+ raise ConfException(f'Directory {build_dir} is neither a Meson build directory nor a project source directory.')
+
+ def clear_cache(self):
+ self.coredata.clear_deps_cache()
+
+ def set_options(self, options):
+ self.coredata.set_options(options)
+
+ def save(self):
+ # Do nothing when using introspection
+ if self.default_values_only:
+ return
+ coredata.save(self.coredata, self.build_dir)
+ # We don't write the build file because any changes to it
+ # are erased when Meson is executed the next time, i.e. when
+ # Ninja is run.
+
+ def print_aligned(self) -> None:
+ """Do the actual printing.
+
+ This prints the generated output in an aligned, pretty form. it aims
+ for a total width of 160 characters, but will use whatever the tty
+ reports it's value to be. Though this is much wider than the standard
+ 80 characters of terminals, and even than the newer 120, compressing
+ it to those lengths makes the output hard to read.
+
+ Each column will have a specific width, and will be line wrapped.
+ """
+ total_width = shutil.get_terminal_size(fallback=(160, 0))[0]
+ _col = max(total_width // 5, 20)
+ last_column = total_width - (3 * _col) - 3
+ four_column = (_col, _col, _col, last_column if last_column > 1 else _col)
+
+ for line in zip(self.name_col, self.value_col, self.choices_col, self.descr_col):
+ if not any(line):
+ mlog.log('')
+ continue
+
+ # This is a header, like `Subproject foo:`,
+ # We just want to print that and get on with it
+ if line[0] and not any(line[1:]):
+ mlog.log(line[0])
+ continue
+
+ def wrap_text(text, width):
+ raw = text.text if isinstance(text, mlog.AnsiDecorator) else text
+ indent = ' ' if raw.startswith('[') else ''
+ wrapped = textwrap.wrap(raw, width, subsequent_indent=indent)
+ if isinstance(text, mlog.AnsiDecorator):
+ wrapped = [mlog.AnsiDecorator(i, text.code) for i in wrapped]
+ # Add padding here to get even rows, as `textwrap.wrap()` will
+ # only shorten, not lengthen each item
+ return [str(i) + ' ' * (width - len(i)) for i in wrapped]
+
+ # wrap will take a long string, and create a list of strings no
+ # longer than the size given. Then that list can be zipped into, to
+ # print each line of the output, such the that columns are printed
+ # to the right width, row by row.
+ name = wrap_text(line[0], four_column[0])
+ val = wrap_text(line[1], four_column[1])
+ choice = wrap_text(line[2], four_column[2])
+ desc = wrap_text(line[3], four_column[3])
+ for l in itertools.zip_longest(name, val, choice, desc, fillvalue=''):
+ items = [l[i] if l[i] else ' ' * four_column[i] for i in range(4)]
+ mlog.log(*items)
+
+ def split_options_per_subproject(self, options: 'coredata.KeyedOptionDictType') -> T.Dict[str, 'coredata.KeyedOptionDictType']:
+ result: T.Dict[str, 'coredata.KeyedOptionDictType'] = {}
+ for k, o in options.items():
+ if k.subproject:
+ self.all_subprojects.add(k.subproject)
+ result.setdefault(k.subproject, {})[k] = o
+ return result
+
+ def _add_line(self, name, value, choices, descr) -> None:
+ if isinstance(name, mlog.AnsiDecorator):
+ name.text = ' ' * self.print_margin + name.text
+ else:
+ name = ' ' * self.print_margin + name
+ self.name_col.append(name)
+ self.value_col.append(value)
+ self.choices_col.append(choices)
+ self.descr_col.append(descr)
+
+ def add_option(self, name, descr, value, choices):
+ value = stringify(value)
+ choices = stringify(choices)
+ self._add_line(mlog.green(name), mlog.yellow(value), mlog.blue(choices), descr)
+
+ def add_title(self, title):
+ title = mlog.cyan(title)
+ descr = mlog.cyan('Description')
+ value = mlog.cyan('Default Value' if self.default_values_only else 'Current Value')
+ choices = mlog.cyan('Possible Values')
+ self._add_line('', '', '', '')
+ self._add_line(title, value, choices, descr)
+ self._add_line('-' * len(title), '-' * len(value), '-' * len(choices), '-' * len(descr))
+
+ def add_section(self, section):
+ self.print_margin = 0
+ self._add_line('', '', '', '')
+ self._add_line(mlog.normal_yellow(section + ':'), '', '', '')
+ self.print_margin = 2
+
+ def print_options(self, title: str, options: 'coredata.KeyedOptionDictType') -> None:
+ if not options:
+ return
+ if title:
+ self.add_title(title)
+ auto = T.cast('coredata.UserFeatureOption', self.coredata.options[OptionKey('auto_features')])
+ for k, o in sorted(options.items()):
+ printable_value = o.printable_value()
+ root = k.as_root()
+ if o.yielding and k.subproject and root in self.coredata.options:
+ printable_value = '<inherited from main project>'
+ if isinstance(o, coredata.UserFeatureOption) and o.is_auto():
+ printable_value = auto.printable_value()
+ self.add_option(str(root), o.description, printable_value, o.choices)
+
+ def print_conf(self, pager: bool):
+ if pager:
+ mlog.start_pager()
+
+ def print_default_values_warning():
+ mlog.warning('The source directory instead of the build directory was specified.')
+ mlog.warning('Only the default values for the project are printed, and all command line parameters are ignored.')
+
+ if self.default_values_only:
+ print_default_values_warning()
+ mlog.log('')
+
+ mlog.log('Core properties:')
+ mlog.log(' Source dir', self.source_dir)
+ if not self.default_values_only:
+ mlog.log(' Build dir ', self.build_dir)
+
+ dir_option_names = set(coredata.BUILTIN_DIR_OPTIONS)
+ test_option_names = {OptionKey('errorlogs'),
+ OptionKey('stdsplit')}
+
+ dir_options: 'coredata.KeyedOptionDictType' = {}
+ test_options: 'coredata.KeyedOptionDictType' = {}
+ core_options: 'coredata.KeyedOptionDictType' = {}
+ module_options: T.Dict[str, 'coredata.KeyedOptionDictType'] = collections.defaultdict(dict)
+ for k, v in self.coredata.options.items():
+ if k in dir_option_names:
+ dir_options[k] = v
+ elif k in test_option_names:
+ test_options[k] = v
+ elif k.module:
+ # Ignore module options if we did not use that module during
+ # configuration.
+ if self.build and k.module not in self.build.modules:
+ continue
+ module_options[k.module][k] = v
+ elif k.is_builtin():
+ core_options[k] = v
+
+ host_core_options = self.split_options_per_subproject({k: v for k, v in core_options.items() if k.machine is MachineChoice.HOST})
+ build_core_options = self.split_options_per_subproject({k: v for k, v in core_options.items() if k.machine is MachineChoice.BUILD})
+ host_compiler_options = self.split_options_per_subproject({k: v for k, v in self.coredata.options.items() if k.is_compiler() and k.machine is MachineChoice.HOST})
+ build_compiler_options = self.split_options_per_subproject({k: v for k, v in self.coredata.options.items() if k.is_compiler() and k.machine is MachineChoice.BUILD})
+ project_options = self.split_options_per_subproject({k: v for k, v in self.coredata.options.items() if k.is_project()})
+ show_build_options = self.default_values_only or self.build.environment.is_cross_build()
+
+ self.add_section('Main project options')
+ self.print_options('Core options', host_core_options[''])
+ if show_build_options:
+ self.print_options('', build_core_options[''])
+ self.print_options('Backend options', {k: v for k, v in self.coredata.options.items() if k.is_backend()})
+ self.print_options('Base options', {k: v for k, v in self.coredata.options.items() if k.is_base()})
+ self.print_options('Compiler options', host_compiler_options.get('', {}))
+ if show_build_options:
+ self.print_options('', build_compiler_options.get('', {}))
+ for mod, mod_options in module_options.items():
+ self.print_options(f'{mod} module options', mod_options)
+ self.print_options('Directories', dir_options)
+ self.print_options('Testing options', test_options)
+ self.print_options('Project options', project_options.get('', {}))
+ for subproject in sorted(self.all_subprojects):
+ if subproject == '':
+ continue
+ self.add_section('Subproject ' + subproject)
+ if subproject in host_core_options:
+ self.print_options('Core options', host_core_options[subproject])
+ if subproject in build_core_options and show_build_options:
+ self.print_options('', build_core_options[subproject])
+ if subproject in host_compiler_options:
+ self.print_options('Compiler options', host_compiler_options[subproject])
+ if subproject in build_compiler_options and show_build_options:
+ self.print_options('', build_compiler_options[subproject])
+ if subproject in project_options:
+ self.print_options('Project options', project_options[subproject])
+ self.print_aligned()
+
+ # Print the warning twice so that the user shouldn't be able to miss it
+ if self.default_values_only:
+ mlog.log('')
+ print_default_values_warning()
+
+ self.print_nondefault_buildtype_options()
+
+ def print_nondefault_buildtype_options(self):
+ mismatching = self.coredata.get_nondefault_buildtype_args()
+ if not mismatching:
+ return
+ mlog.log("\nThe following option(s) have a different value than the build type default\n")
+ mlog.log(' current default')
+ for m in mismatching:
+ mlog.log(f'{m[0]:21}{m[1]:10}{m[2]:10}')
+
+def run(options):
+ coredata.parse_cmd_line_options(options)
+ builddir = os.path.abspath(os.path.realpath(options.builddir))
+ c = None
+ try:
+ c = Conf(builddir)
+ if c.default_values_only:
+ c.print_conf(options.pager)
+ return 0
+
+ save = False
+ if options.cmd_line_options:
+ c.set_options(options.cmd_line_options)
+ coredata.update_cmd_line_file(builddir, options)
+ save = True
+ elif options.clearcache:
+ c.clear_cache()
+ save = True
+ else:
+ c.print_conf(options.pager)
+ if save:
+ c.save()
+ mintro.update_build_options(c.coredata, c.build.environment.info_dir)
+ mintro.write_meson_info_file(c.build, [])
+ except ConfException as e:
+ mlog.log('Meson configurator encountered an error:')
+ if c is not None and c.build is not None:
+ mintro.write_meson_info_file(c.build, [e])
+ raise e
+ except BrokenPipeError:
+ # Pager quit before we wrote everything.
+ pass
+ return 0
diff --git a/mesonbuild/mdevenv.py b/mesonbuild/mdevenv.py
new file mode 100644
index 0000000..05779e8
--- /dev/null
+++ b/mesonbuild/mdevenv.py
@@ -0,0 +1,215 @@
+from __future__ import annotations
+
+import os, subprocess
+import argparse
+import tempfile
+import shutil
+import itertools
+
+from pathlib import Path
+from . import build, minstall, dependencies
+from .mesonlib import (MesonException, is_windows, setup_vsenv, OptionKey,
+ get_wine_shortpath, MachineChoice)
+from . import mlog
+
+import typing as T
+if T.TYPE_CHECKING:
+ from .backends import InstallData
+
+POWERSHELL_EXES = {'pwsh.exe', 'powershell.exe'}
+
+def add_arguments(parser: argparse.ArgumentParser) -> None:
+ parser.add_argument('-C', dest='builddir', type=Path, default='.',
+ help='Path to build directory')
+ parser.add_argument('--workdir', '-w', type=Path, default=None,
+ help='Directory to cd into before running (default: builddir, Since 1.0.0)')
+ parser.add_argument('--dump', action='store_true',
+ help='Only print required environment (Since 0.62.0)')
+ parser.add_argument('devcmd', nargs=argparse.REMAINDER, metavar='command',
+ help='Command to run in developer environment (default: interactive shell)')
+
+def get_windows_shell() -> T.Optional[str]:
+ mesonbuild = Path(__file__).parent
+ script = mesonbuild / 'scripts' / 'cmd_or_ps.ps1'
+ for shell in POWERSHELL_EXES:
+ try:
+ command = [shell, '-noprofile', '-executionpolicy', 'bypass', '-file', str(script)]
+ result = subprocess.check_output(command)
+ return result.decode().strip()
+ except (subprocess.CalledProcessError, OSError):
+ pass
+ return None
+
+def reduce_winepath(env: T.Dict[str, str]) -> None:
+ winepath = env.get('WINEPATH')
+ if not winepath:
+ return
+ winecmd = shutil.which('wine64') or shutil.which('wine')
+ if not winecmd:
+ return
+ env['WINEPATH'] = get_wine_shortpath([winecmd], winepath.split(';'))
+ mlog.log('Meson detected wine and has set WINEPATH accordingly')
+
+def get_env(b: build.Build, dump: bool) -> T.Tuple[T.Dict[str, str], T.Set[str]]:
+ extra_env = build.EnvironmentVariables()
+ extra_env.set('MESON_DEVENV', ['1'])
+ extra_env.set('MESON_PROJECT_NAME', [b.project_name])
+
+ sysroot = b.environment.properties[MachineChoice.HOST].get_sys_root()
+ if sysroot:
+ extra_env.set('QEMU_LD_PREFIX', [sysroot])
+
+ env = {} if dump else os.environ.copy()
+ varnames = set()
+ for i in itertools.chain(b.devenv, {extra_env}):
+ env = i.get_env(env, dump)
+ varnames |= i.get_names()
+
+ reduce_winepath(env)
+
+ return env, varnames
+
+def bash_completion_files(b: build.Build, install_data: 'InstallData') -> T.List[str]:
+ result = []
+ dep = dependencies.PkgConfigDependency('bash-completion', b.environment,
+ {'required': False, 'silent': True, 'version': '>=2.10'})
+ if dep.found():
+ prefix = b.environment.coredata.get_option(OptionKey('prefix'))
+ assert isinstance(prefix, str), 'for mypy'
+ datadir = b.environment.coredata.get_option(OptionKey('datadir'))
+ assert isinstance(datadir, str), 'for mypy'
+ datadir_abs = os.path.join(prefix, datadir)
+ completionsdir = dep.get_variable(pkgconfig='completionsdir', pkgconfig_define=['datadir', datadir_abs])
+ assert isinstance(completionsdir, str), 'for mypy'
+ completionsdir_path = Path(completionsdir)
+ for f in install_data.data:
+ if completionsdir_path in Path(f.install_path).parents:
+ result.append(f.path)
+ return result
+
+def add_gdb_auto_load(autoload_path: Path, gdb_helper: str, fname: Path) -> None:
+ # Copy or symlink the GDB helper into our private directory tree
+ destdir = autoload_path / fname.parent
+ destdir.mkdir(parents=True, exist_ok=True)
+ try:
+ if is_windows():
+ shutil.copy(gdb_helper, str(destdir / os.path.basename(gdb_helper)))
+ else:
+ os.symlink(gdb_helper, str(destdir / os.path.basename(gdb_helper)))
+ except (FileExistsError, shutil.SameFileError):
+ pass
+
+def write_gdb_script(privatedir: Path, install_data: 'InstallData', workdir: Path) -> None:
+ if not shutil.which('gdb'):
+ return
+ bdir = privatedir.parent
+ autoload_basedir = privatedir / 'gdb-auto-load'
+ autoload_path = Path(autoload_basedir, *bdir.parts[1:])
+ have_gdb_helpers = False
+ for d in install_data.data:
+ if d.path.endswith('-gdb.py') or d.path.endswith('-gdb.gdb') or d.path.endswith('-gdb.scm'):
+ # This GDB helper is made for a specific shared library, search if
+ # we have it in our builddir.
+ libname = Path(d.path).name.rsplit('-', 1)[0]
+ for t in install_data.targets:
+ path = Path(t.fname)
+ if path.name == libname:
+ add_gdb_auto_load(autoload_path, d.path, path)
+ have_gdb_helpers = True
+ if have_gdb_helpers:
+ gdbinit_line = f'add-auto-load-scripts-directory {autoload_basedir}\n'
+ gdbinit_path = bdir / '.gdbinit'
+ first_time = False
+ try:
+ with gdbinit_path.open('r+', encoding='utf-8') as f:
+ if gdbinit_line not in f.readlines():
+ f.write(gdbinit_line)
+ first_time = True
+ except FileNotFoundError:
+ gdbinit_path.write_text(gdbinit_line, encoding='utf-8')
+ first_time = True
+ if first_time:
+ gdbinit_path = gdbinit_path.resolve()
+ workdir_path = workdir.resolve()
+ rel_path = gdbinit_path.relative_to(workdir_path)
+ mlog.log('Meson detected GDB helpers and added config in', mlog.bold(str(rel_path)))
+ mlog.log('To load it automatically you might need to:')
+ mlog.log(' - Add', mlog.bold(f'add-auto-load-safe-path {gdbinit_path.parent}'),
+ 'in', mlog.bold('~/.gdbinit'))
+ if gdbinit_path.parent != workdir_path:
+ mlog.log(' - Change current workdir to', mlog.bold(str(rel_path.parent)),
+ 'or use', mlog.bold(f'--init-command {rel_path}'))
+
+def run(options: argparse.Namespace) -> int:
+ privatedir = Path(options.builddir) / 'meson-private'
+ buildfile = privatedir / 'build.dat'
+ if not buildfile.is_file():
+ raise MesonException(f'Directory {options.builddir!r} does not seem to be a Meson build directory.')
+ b = build.load(options.builddir)
+ workdir = options.workdir or options.builddir
+
+ setup_vsenv(b.need_vsenv) # Call it before get_env to get vsenv vars as well
+ devenv, varnames = get_env(b, options.dump)
+ if options.dump:
+ if options.devcmd:
+ raise MesonException('--dump option does not allow running other command.')
+ for name in varnames:
+ print(f'{name}="{devenv[name]}"')
+ print(f'export {name}')
+ return 0
+
+ if b.environment.need_exe_wrapper():
+ m = 'An executable wrapper could be required'
+ exe_wrapper = b.environment.get_exe_wrapper()
+ if exe_wrapper:
+ cmd = ' '.join(exe_wrapper.get_command())
+ m += f': {cmd}'
+ mlog.log(m)
+
+ install_data = minstall.load_install_data(str(privatedir / 'install.dat'))
+ write_gdb_script(privatedir, install_data, workdir)
+
+ args = options.devcmd
+ if not args:
+ prompt_prefix = f'[{b.project_name}]'
+ shell_env = os.environ.get("SHELL")
+ # Prefer $SHELL in a MSYS2 bash despite it being Windows
+ if shell_env and os.path.exists(shell_env):
+ args = [shell_env]
+ elif is_windows():
+ shell = get_windows_shell()
+ if not shell:
+ mlog.warning('Failed to determine Windows shell, fallback to cmd.exe')
+ if shell in POWERSHELL_EXES:
+ args = [shell, '-NoLogo', '-NoExit']
+ prompt = f'function global:prompt {{ "{prompt_prefix} PS " + $PWD + "> "}}'
+ args += ['-Command', prompt]
+ else:
+ args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
+ args += ['/k', f'prompt {prompt_prefix} $P$G']
+ else:
+ args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
+ if "bash" in args[0]:
+ # Let the GC remove the tmp file
+ tmprc = tempfile.NamedTemporaryFile(mode='w')
+ tmprc.write('[ -e ~/.bashrc ] && . ~/.bashrc\n')
+ if not os.environ.get("MESON_DISABLE_PS1_OVERRIDE"):
+ tmprc.write(f'export PS1="{prompt_prefix} $PS1"\n')
+ for f in bash_completion_files(b, install_data):
+ tmprc.write(f'. "{f}"\n')
+ tmprc.flush()
+ args.append("--rcfile")
+ args.append(tmprc.name)
+ else:
+ # Try to resolve executable using devenv's PATH
+ abs_path = shutil.which(args[0], path=devenv.get('PATH', None))
+ args[0] = abs_path or args[0]
+
+ try:
+ return subprocess.call(args, close_fds=False,
+ env=devenv,
+ cwd=workdir)
+ except subprocess.CalledProcessError as e:
+ return e.returncode
+ except FileNotFoundError:
+ raise MesonException(f'Command not found: {args[0]}')
diff --git a/mesonbuild/mdist.py b/mesonbuild/mdist.py
new file mode 100644
index 0000000..073e276
--- /dev/null
+++ b/mesonbuild/mdist.py
@@ -0,0 +1,347 @@
+# Copyright 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.
+from __future__ import annotations
+
+
+import argparse
+import gzip
+import os
+import sys
+import shlex
+import shutil
+import subprocess
+import tarfile
+import tempfile
+import hashlib
+from glob import glob
+from pathlib import Path
+from mesonbuild.environment import detect_ninja
+from mesonbuild.mesonlib import (MesonException, RealPathAction, quiet_git,
+ windows_proof_rmtree, setup_vsenv)
+from mesonbuild.msetup import add_arguments as msetup_argparse
+from mesonbuild.wrap import wrap
+from mesonbuild import mlog, build, coredata
+from .scripts.meson_exe import run_exe
+
+archive_choices = ['gztar', 'xztar', 'zip']
+
+archive_extension = {'gztar': '.tar.gz',
+ 'xztar': '.tar.xz',
+ 'zip': '.zip'}
+
+def add_arguments(parser):
+ parser.add_argument('-C', dest='wd', action=RealPathAction,
+ help='directory to cd into before running')
+ parser.add_argument('--allow-dirty', action='store_true',
+ help='Allow even when repository contains uncommitted changes.')
+ parser.add_argument('--formats', default='xztar',
+ help='Comma separated list of archive types to create. Supports xztar (default), gztar, and zip.')
+ parser.add_argument('--include-subprojects', action='store_true',
+ help='Include source code of subprojects that have been used for the build.')
+ parser.add_argument('--no-tests', action='store_true',
+ help='Do not build and test generated packages.')
+
+
+def create_hash(fname):
+ hashname = fname + '.sha256sum'
+ m = hashlib.sha256()
+ m.update(open(fname, 'rb').read())
+ with open(hashname, 'w', encoding='utf-8') as f:
+ # A space and an asterisk because that is the format defined by GNU coreutils
+ # and accepted by busybox and the Perl shasum tool.
+ f.write('{} *{}\n'.format(m.hexdigest(), os.path.basename(fname)))
+
+
+def copy_git(src, distdir, revision='HEAD', prefix=None, subdir=None):
+ cmd = ['git', 'archive', '--format', 'tar', revision]
+ if prefix is not None:
+ cmd.insert(2, f'--prefix={prefix}/')
+ if subdir is not None:
+ cmd.extend(['--', subdir])
+ with tempfile.TemporaryFile() as f:
+ subprocess.check_call(cmd, cwd=src, stdout=f)
+ f.seek(0)
+ t = tarfile.open(fileobj=f) # [ignore encoding]
+ t.extractall(path=distdir)
+
+msg_uncommitted_changes = 'Repository has uncommitted changes that will not be included in the dist tarball'
+
+def handle_dirty_opt(msg, allow_dirty: bool):
+ if allow_dirty:
+ mlog.warning(msg)
+ else:
+ mlog.error(msg + '\n' + 'Use --allow-dirty to ignore the warning and proceed anyway')
+ sys.exit(1)
+
+def process_submodules(src, distdir, options):
+ module_file = os.path.join(src, '.gitmodules')
+ if not os.path.exists(module_file):
+ return
+ cmd = ['git', 'submodule', 'status', '--cached', '--recursive']
+ modlist = subprocess.check_output(cmd, cwd=src, universal_newlines=True).splitlines()
+ for submodule in modlist:
+ status = submodule[:1]
+ sha1, rest = submodule[1:].split(' ', 1)
+ subpath = rest.rsplit(' ', 1)[0]
+
+ if status == '-':
+ mlog.warning(f'Submodule {subpath!r} is not checked out and cannot be added to the dist')
+ continue
+ elif status in {'+', 'U'}:
+ handle_dirty_opt(f'Submodule {subpath!r} has uncommitted changes that will not be included in the dist tarball', options.allow_dirty)
+
+ copy_git(os.path.join(src, subpath), distdir, revision=sha1, prefix=subpath)
+
+
+def run_dist_scripts(src_root, bld_root, dist_root, dist_scripts, subprojects):
+ assert os.path.isabs(dist_root)
+ env = {}
+ env['MESON_DIST_ROOT'] = dist_root
+ env['MESON_SOURCE_ROOT'] = src_root
+ env['MESON_BUILD_ROOT'] = bld_root
+ for d in dist_scripts:
+ if d.subproject and d.subproject not in subprojects:
+ continue
+ subdir = subprojects.get(d.subproject, '')
+ env['MESON_PROJECT_DIST_ROOT'] = os.path.join(dist_root, subdir)
+ env['MESON_PROJECT_SOURCE_ROOT'] = os.path.join(src_root, subdir)
+ env['MESON_PROJECT_BUILD_ROOT'] = os.path.join(bld_root, subdir)
+ name = ' '.join(d.cmd_args)
+ print(f'Running custom dist script {name!r}')
+ try:
+ rc = run_exe(d, env)
+ if rc != 0:
+ sys.exit('Dist script errored out')
+ except OSError:
+ print(f'Failed to run dist script {name!r}')
+ sys.exit(1)
+
+def git_root(src_root):
+ # Cannot use --show-toplevel here because git in our CI prints cygwin paths
+ # that python cannot resolve. Workaround this by taking parent of src_root.
+ prefix = quiet_git(['rev-parse', '--show-prefix'], src_root, check=True)[1].strip()
+ if not prefix:
+ return Path(src_root)
+ prefix_level = len(Path(prefix).parents)
+ return Path(src_root).parents[prefix_level - 1]
+
+def is_git(src_root):
+ '''
+ Checks if meson.build file at the root source directory is tracked by git.
+ It could be a subproject part of the parent project git repository.
+ '''
+ return quiet_git(['ls-files', '--error-unmatch', 'meson.build'], src_root)[0]
+
+def git_have_dirty_index(src_root):
+ '''Check whether there are uncommitted changes in git'''
+ ret = subprocess.call(['git', '-C', src_root, 'diff-index', '--quiet', 'HEAD'])
+ return ret == 1
+
+def process_git_project(src_root, distdir, options):
+ if git_have_dirty_index(src_root):
+ handle_dirty_opt(msg_uncommitted_changes, options.allow_dirty)
+ if os.path.exists(distdir):
+ windows_proof_rmtree(distdir)
+ repo_root = git_root(src_root)
+ if repo_root.samefile(src_root):
+ os.makedirs(distdir)
+ copy_git(src_root, distdir)
+ else:
+ subdir = Path(src_root).relative_to(repo_root)
+ tmp_distdir = distdir + '-tmp'
+ if os.path.exists(tmp_distdir):
+ windows_proof_rmtree(tmp_distdir)
+ os.makedirs(tmp_distdir)
+ copy_git(repo_root, tmp_distdir, subdir=str(subdir))
+ Path(tmp_distdir, subdir).rename(distdir)
+ windows_proof_rmtree(tmp_distdir)
+ process_submodules(src_root, distdir, options)
+
+def create_dist_git(dist_name, archives, src_root, bld_root, dist_sub, dist_scripts, subprojects, options):
+ distdir = os.path.join(dist_sub, dist_name)
+ process_git_project(src_root, distdir, options)
+ for path in subprojects.values():
+ sub_src_root = os.path.join(src_root, path)
+ sub_distdir = os.path.join(distdir, path)
+ if os.path.exists(sub_distdir):
+ continue
+ if is_git(sub_src_root):
+ process_git_project(sub_src_root, sub_distdir, options)
+ else:
+ shutil.copytree(sub_src_root, sub_distdir)
+ run_dist_scripts(src_root, bld_root, distdir, dist_scripts, subprojects)
+ output_names = []
+ for a in archives:
+ compressed_name = distdir + archive_extension[a]
+ shutil.make_archive(distdir, a, root_dir=dist_sub, base_dir=dist_name)
+ output_names.append(compressed_name)
+ windows_proof_rmtree(distdir)
+ return output_names
+
+def is_hg(src_root):
+ return os.path.isdir(os.path.join(src_root, '.hg'))
+
+def hg_have_dirty_index(src_root):
+ '''Check whether there are uncommitted changes in hg'''
+ out = subprocess.check_output(['hg', '-R', src_root, 'summary'])
+ return b'commit: (clean)' not in out
+
+def create_dist_hg(dist_name, archives, src_root, bld_root, dist_sub, dist_scripts, options):
+ if hg_have_dirty_index(src_root):
+ handle_dirty_opt(msg_uncommitted_changes, options.allow_dirty)
+ if dist_scripts:
+ mlog.warning('dist scripts are not supported in Mercurial projects')
+
+ os.makedirs(dist_sub, exist_ok=True)
+ tarname = os.path.join(dist_sub, dist_name + '.tar')
+ xzname = tarname + '.xz'
+ gzname = tarname + '.gz'
+ zipname = os.path.join(dist_sub, dist_name + '.zip')
+ # Note that -X interprets relative paths using the current working
+ # directory, not the repository root, so this must be an absolute path:
+ # https://bz.mercurial-scm.org/show_bug.cgi?id=6267
+ #
+ # .hg[a-z]* is used instead of .hg* to keep .hg_archival.txt, which may
+ # be useful to link the tarball to the Mercurial revision for either
+ # manual inspection or in case any code interprets it for a --version or
+ # similar.
+ subprocess.check_call(['hg', 'archive', '-R', src_root, '-S', '-t', 'tar',
+ '-X', src_root + '/.hg[a-z]*', tarname])
+ output_names = []
+ if 'xztar' in archives:
+ import lzma
+ with lzma.open(xzname, 'wb') as xf, open(tarname, 'rb') as tf:
+ shutil.copyfileobj(tf, xf)
+ output_names.append(xzname)
+ if 'gztar' in archives:
+ with gzip.open(gzname, 'wb') as zf, open(tarname, 'rb') as tf:
+ shutil.copyfileobj(tf, zf)
+ output_names.append(gzname)
+ os.unlink(tarname)
+ if 'zip' in archives:
+ subprocess.check_call(['hg', 'archive', '-R', src_root, '-S', '-t', 'zip', zipname])
+ output_names.append(zipname)
+ return output_names
+
+def run_dist_steps(meson_command, unpacked_src_dir, builddir, installdir, ninja_args):
+ if subprocess.call(meson_command + ['--backend=ninja', unpacked_src_dir, builddir]) != 0:
+ print('Running Meson on distribution package failed')
+ return 1
+ if subprocess.call(ninja_args, cwd=builddir) != 0:
+ print('Compiling the distribution package failed')
+ return 1
+ if subprocess.call(ninja_args + ['test'], cwd=builddir) != 0:
+ print('Running unit tests on the distribution package failed')
+ return 1
+ myenv = os.environ.copy()
+ myenv['DESTDIR'] = installdir
+ if subprocess.call(ninja_args + ['install'], cwd=builddir, env=myenv) != 0:
+ print('Installing the distribution package failed')
+ return 1
+ return 0
+
+def check_dist(packagename, meson_command, extra_meson_args, bld_root, privdir):
+ print(f'Testing distribution package {packagename}')
+ unpackdir = os.path.join(privdir, 'dist-unpack')
+ builddir = os.path.join(privdir, 'dist-build')
+ installdir = os.path.join(privdir, 'dist-install')
+ for p in (unpackdir, builddir, installdir):
+ if os.path.exists(p):
+ windows_proof_rmtree(p)
+ os.mkdir(p)
+ ninja_args = detect_ninja()
+ shutil.unpack_archive(packagename, unpackdir)
+ unpacked_files = glob(os.path.join(unpackdir, '*'))
+ assert len(unpacked_files) == 1
+ unpacked_src_dir = unpacked_files[0]
+ meson_command += create_cmdline_args(bld_root)
+ meson_command += extra_meson_args
+
+ ret = run_dist_steps(meson_command, unpacked_src_dir, builddir, installdir, ninja_args)
+ if ret > 0:
+ print(f'Dist check build directory was {builddir}')
+ else:
+ windows_proof_rmtree(unpackdir)
+ windows_proof_rmtree(builddir)
+ windows_proof_rmtree(installdir)
+ print(f'Distribution package {packagename} tested')
+ return ret
+
+def create_cmdline_args(bld_root):
+ parser = argparse.ArgumentParser()
+ msetup_argparse(parser)
+ args = parser.parse_args([])
+ coredata.parse_cmd_line_options(args)
+ coredata.read_cmd_line_file(bld_root, args)
+ args.cmd_line_options.pop(coredata.OptionKey('backend'), '')
+ return shlex.split(coredata.format_cmd_line_options(args))
+
+def determine_archives_to_generate(options):
+ result = []
+ for i in options.formats.split(','):
+ if i not in archive_choices:
+ sys.exit(f'Value "{i}" not one of permitted values {archive_choices}.')
+ result.append(i)
+ if len(i) == 0:
+ sys.exit('No archive types specified.')
+ return result
+
+def run(options):
+ buildfile = Path(options.wd) / 'meson-private' / 'build.dat'
+ if not buildfile.is_file():
+ raise MesonException(f'Directory {options.wd!r} does not seem to be a Meson build directory.')
+ b = build.load(options.wd)
+ setup_vsenv(b.need_vsenv)
+ # This import must be load delayed, otherwise it will get the default
+ # value of None.
+ from mesonbuild.mesonlib import get_meson_command
+ src_root = b.environment.source_dir
+ bld_root = b.environment.build_dir
+ priv_dir = os.path.join(bld_root, 'meson-private')
+ dist_sub = os.path.join(bld_root, 'meson-dist')
+
+ dist_name = b.project_name + '-' + b.project_version
+
+ archives = determine_archives_to_generate(options)
+
+ subprojects = {}
+ extra_meson_args = []
+ if options.include_subprojects:
+ subproject_dir = os.path.join(src_root, b.subproject_dir)
+ for sub in b.subprojects:
+ directory = wrap.get_directory(subproject_dir, sub)
+ subprojects[sub] = os.path.join(b.subproject_dir, directory)
+ extra_meson_args.append('-Dwrap_mode=nodownload')
+
+ if is_git(src_root):
+ names = create_dist_git(dist_name, archives, src_root, bld_root, dist_sub, b.dist_scripts, subprojects, options)
+ elif is_hg(src_root):
+ if subprojects:
+ print('--include-subprojects option currently not supported with Mercurial')
+ return 1
+ names = create_dist_hg(dist_name, archives, src_root, bld_root, dist_sub, b.dist_scripts, options)
+ else:
+ print('Dist currently only works with Git or Mercurial repos')
+ return 1
+ if names is None:
+ return 1
+ rc = 0
+ if not options.no_tests:
+ # Check only one.
+ rc = check_dist(names[0], get_meson_command(), extra_meson_args, bld_root, priv_dir)
+ if rc == 0:
+ for name in names:
+ create_hash(name)
+ print('Created', name)
+ return rc
diff --git a/mesonbuild/mesondata.py b/mesonbuild/mesondata.py
new file mode 100644
index 0000000..da641fd
--- /dev/null
+++ b/mesonbuild/mesondata.py
@@ -0,0 +1,48 @@
+# Copyright 2021 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.
+from __future__ import annotations
+
+
+import importlib.resources
+from pathlib import PurePosixPath, Path
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .environment import Environment
+
+class DataFile:
+ def __init__(self, path: str) -> None:
+ self.path = PurePosixPath(path)
+
+ def write_once(self, path: Path) -> None:
+ if not path.exists():
+ data = importlib.resources.read_text( # [ignore encoding] it's on the next lines, Mr. Lint
+ ('mesonbuild' / self.path.parent).as_posix().replace('/', '.'),
+ self.path.name,
+ encoding='utf-8')
+ path.write_text(data, encoding='utf-8')
+
+ def write_to_private(self, env: 'Environment') -> Path:
+ try:
+ resource = importlib.resources.files('mesonbuild') / self.path
+ if isinstance(resource, Path):
+ return resource
+ except AttributeError:
+ # fall through to python 3.7 compatible code
+ pass
+
+ out_file = Path(env.scratch_dir) / 'data' / self.path.name
+ out_file.parent.mkdir(exist_ok=True)
+ self.write_once(out_file)
+ return out_file
diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py
new file mode 100644
index 0000000..be69a12
--- /dev/null
+++ b/mesonbuild/mesonlib.py
@@ -0,0 +1,35 @@
+# SPDX-license-identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# 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.
+
+# pylint: skip-file
+
+"""Helper functions and classes."""
+
+import os
+
+from .utils.core import *
+from .utils.vsenv import *
+
+from .utils.universal import *
+
+# Here we import either the posix implementations, the windows implementations,
+# or a generic no-op implementation
+if os.name == 'posix':
+ from .utils.posix import *
+elif os.name == 'nt':
+ from .utils.win32 import *
+else:
+ from .utils.platform import *
diff --git a/mesonbuild/mesonmain.py b/mesonbuild/mesonmain.py
new file mode 100644
index 0000000..4b4f88c
--- /dev/null
+++ b/mesonbuild/mesonmain.py
@@ -0,0 +1,290 @@
+# Copyright 2012-2021 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.
+from __future__ import annotations
+
+# Work around some pathlib bugs...
+
+from . import _pathlib
+import sys
+sys.modules['pathlib'] = _pathlib
+
+# This file is an entry point for all commands, including scripts. Include the
+# strict minimum python modules for performance reasons.
+import os.path
+import platform
+import importlib
+import argparse
+
+from .utils.core import MesonException, MesonBugException
+from . import mlog
+
+def errorhandler(e, command):
+ import traceback
+ if isinstance(e, MesonException):
+ mlog.exception(e)
+ logfile = mlog.shutdown()
+ if logfile is not None:
+ mlog.log("\nA full log can be found at", mlog.bold(logfile))
+ if os.environ.get('MESON_FORCE_BACKTRACE'):
+ raise e
+ return 1
+ else:
+ # We assume many types of traceback are Meson logic bugs, but most
+ # particularly anything coming from the interpreter during `setup`.
+ # Some things definitely aren't:
+ # - PermissionError is always a problem in the user environment
+ # - runpython doesn't run Meson's own code, even though it is
+ # dispatched by our run()
+ if os.environ.get('MESON_FORCE_BACKTRACE'):
+ raise e
+ traceback.print_exc()
+
+ if command == 'runpython':
+ return 2
+ elif isinstance(e, OSError):
+ mlog.exception("Unhandled python OSError. This is probably not a Meson bug, "
+ "but an issue with your build environment.")
+ return e.errno
+ else: # Exception
+ msg = 'Unhandled python exception'
+ if all(getattr(e, a, None) is not None for a in ['file', 'lineno', 'colno']):
+ e = MesonBugException(msg, e.file, e.lineno, e.colno) # type: ignore
+ else:
+ e = MesonBugException(msg)
+ mlog.exception(e)
+ return 2
+
+# Note: when adding arguments, please also add them to the completion
+# scripts in $MESONSRC/data/shell-completions/
+class CommandLineParser:
+ def __init__(self):
+ # only import these once we do full argparse processing
+ from . import mconf, mdist, minit, minstall, mintro, msetup, mtest, rewriter, msubprojects, munstable_coredata, mcompile, mdevenv
+ from .scripts import env2mfile
+ from .wrap import wraptool
+ import shutil
+
+ self.term_width = shutil.get_terminal_size().columns
+ self.formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=int(self.term_width / 2), width=self.term_width)
+
+ self.commands = {}
+ self.hidden_commands = []
+ self.parser = argparse.ArgumentParser(prog='meson', formatter_class=self.formatter)
+ self.subparsers = self.parser.add_subparsers(title='Commands', dest='command',
+ description='If no command is specified it defaults to setup command.')
+ self.add_command('setup', msetup.add_arguments, msetup.run,
+ help_msg='Configure the project')
+ self.add_command('configure', mconf.add_arguments, mconf.run,
+ help_msg='Change project options',)
+ self.add_command('dist', mdist.add_arguments, mdist.run,
+ help_msg='Generate release archive',)
+ self.add_command('install', minstall.add_arguments, minstall.run,
+ help_msg='Install the project')
+ self.add_command('introspect', mintro.add_arguments, mintro.run,
+ help_msg='Introspect project')
+ self.add_command('init', minit.add_arguments, minit.run,
+ help_msg='Create a new project')
+ self.add_command('test', mtest.add_arguments, mtest.run,
+ help_msg='Run tests')
+ self.add_command('wrap', wraptool.add_arguments, wraptool.run,
+ help_msg='Wrap tools')
+ self.add_command('subprojects', msubprojects.add_arguments, msubprojects.run,
+ help_msg='Manage subprojects')
+ self.add_command('rewrite', lambda parser: rewriter.add_arguments(parser, self.formatter), rewriter.run,
+ help_msg='Modify the project definition')
+ self.add_command('compile', mcompile.add_arguments, mcompile.run,
+ help_msg='Build the project')
+ self.add_command('devenv', mdevenv.add_arguments, mdevenv.run,
+ help_msg='Run commands in developer environment')
+ self.add_command('env2mfile', env2mfile.add_arguments, env2mfile.run,
+ help_msg='Convert current environment to a cross or native file')
+ # Add new commands above this line to list them in help command
+ self.add_command('help', self.add_help_arguments, self.run_help_command,
+ help_msg='Print help of a subcommand')
+
+ # Hidden commands
+ self.add_command('runpython', self.add_runpython_arguments, self.run_runpython_command,
+ help_msg=argparse.SUPPRESS)
+ self.add_command('unstable-coredata', munstable_coredata.add_arguments, munstable_coredata.run,
+ help_msg=argparse.SUPPRESS)
+
+ def add_command(self, name, add_arguments_func, run_func, help_msg, aliases=None):
+ aliases = aliases or []
+ # FIXME: Cannot have hidden subparser:
+ # https://bugs.python.org/issue22848
+ if help_msg == argparse.SUPPRESS:
+ p = argparse.ArgumentParser(prog='meson ' + name, formatter_class=self.formatter)
+ self.hidden_commands.append(name)
+ else:
+ p = self.subparsers.add_parser(name, help=help_msg, aliases=aliases, formatter_class=self.formatter)
+ add_arguments_func(p)
+ p.set_defaults(run_func=run_func)
+ for i in [name] + aliases:
+ self.commands[i] = p
+
+ def add_runpython_arguments(self, parser: argparse.ArgumentParser):
+ parser.add_argument('-c', action='store_true', dest='eval_arg', default=False)
+ parser.add_argument('--version', action='version', version=platform.python_version())
+ parser.add_argument('script_file')
+ parser.add_argument('script_args', nargs=argparse.REMAINDER)
+
+ def run_runpython_command(self, options):
+ sys.argv[1:] = options.script_args
+ if options.eval_arg:
+ exec(options.script_file)
+ else:
+ import runpy
+ sys.path.insert(0, os.path.dirname(options.script_file))
+ runpy.run_path(options.script_file, run_name='__main__')
+ return 0
+
+ def add_help_arguments(self, parser):
+ parser.add_argument('command', nargs='?', choices=list(self.commands.keys()))
+
+ def run_help_command(self, options):
+ if options.command:
+ self.commands[options.command].print_help()
+ else:
+ self.parser.print_help()
+ return 0
+
+ def run(self, args):
+ implicit_setup_command_notice = False
+ # If first arg is not a known command, assume user wants to run the setup
+ # command.
+ known_commands = list(self.commands.keys()) + ['-h', '--help']
+ if not args or args[0] not in known_commands:
+ implicit_setup_command_notice = True
+ args = ['setup'] + args
+
+ # Hidden commands have their own parser instead of using the global one
+ if args[0] in self.hidden_commands:
+ command = args[0]
+ parser = self.commands[command]
+ args = args[1:]
+ else:
+ parser = self.parser
+ command = None
+
+ from . import mesonlib
+ args = mesonlib.expand_arguments(args)
+ options = parser.parse_args(args)
+
+ if command is None:
+ command = options.command
+
+ # Bump the version here in order to add a pre-exit warning that we are phasing out
+ # support for old python. If this is already the oldest supported version, then
+ # this can never be true and does nothing.
+ pending_python_deprecation_notice = \
+ command in {'setup', 'compile', 'test', 'install'} and sys.version_info < (3, 7)
+
+ try:
+ return options.run_func(options)
+ except Exception as e:
+ return errorhandler(e, command)
+ finally:
+ if implicit_setup_command_notice:
+ mlog.warning('Running the setup command as `meson [options]` instead of '
+ '`meson setup [options]` is ambiguous and deprecated.', fatal=False)
+ if pending_python_deprecation_notice:
+ mlog.notice('You are using Python 3.6 which is EOL. Starting with v0.62.0, '
+ 'Meson will require Python 3.7 or newer', fatal=False)
+ mlog.shutdown()
+
+def run_script_command(script_name, script_args):
+ # Map script name to module name for those that doesn't match
+ script_map = {'exe': 'meson_exe',
+ 'install': 'meson_install',
+ 'delsuffix': 'delwithsuffix',
+ 'gtkdoc': 'gtkdochelper',
+ 'hotdoc': 'hotdochelper',
+ 'regencheck': 'regen_checker'}
+ module_name = script_map.get(script_name, script_name)
+
+ try:
+ module = importlib.import_module('mesonbuild.scripts.' + module_name)
+ except ModuleNotFoundError as e:
+ mlog.exception(e)
+ return 1
+
+ try:
+ return module.run(script_args)
+ except MesonException as e:
+ mlog.error(f'Error in {script_name} helper script:')
+ mlog.exception(e)
+ return 1
+
+def ensure_stdout_accepts_unicode():
+ if sys.stdout.encoding and not sys.stdout.encoding.upper().startswith('UTF-'):
+ sys.stdout.reconfigure(errors='surrogateescape')
+
+def set_meson_command(mainfile):
+ # Set the meson command that will be used to run scripts and so on
+ from . import mesonlib
+ mesonlib.set_meson_command(mainfile)
+
+def run(original_args, mainfile):
+ if sys.version_info >= (3, 10) and os.environ.get('MESON_RUNNING_IN_PROJECT_TESTS'):
+ # workaround for https://bugs.python.org/issue34624
+ import warnings
+ warnings.filterwarnings('error', category=EncodingWarning, module='mesonbuild')
+ # python 3.11 adds a warning that in 3.15, UTF-8 mode will be default.
+ # This is fantastic news, we'd love that. Less fantastic: this warning is silly,
+ # we *want* these checks to be affected. Plus, the recommended alternative API
+ # would (in addition to warning people when UTF-8 mode removed the problem) also
+ # require using a minimum python version of 3.11 (in which the warning was added)
+ # or add verbose if/else soup.
+ warnings.filterwarnings('ignore', message="UTF-8 Mode affects .*getpreferredencoding", category=EncodingWarning)
+
+ # Meson gets confused if stdout can't output Unicode, if the
+ # locale isn't Unicode, just force stdout to accept it. This tries
+ # to emulate enough of PEP 540 to work elsewhere.
+ ensure_stdout_accepts_unicode()
+
+ # https://github.com/mesonbuild/meson/issues/3653
+ if sys.platform == 'cygwin' and os.environ.get('MSYSTEM', '') not in ['MSYS', '']:
+ mlog.error('This python3 seems to be msys/python on MSYS2 Windows, but you are in a MinGW environment')
+ mlog.error('Please install and use mingw-w64-x86_64-python3 and/or mingw-w64-x86_64-meson with Pacman')
+ return 2
+
+ args = original_args[:]
+
+ # Special handling of internal commands called from backends, they don't
+ # need to go through argparse.
+ if len(args) >= 2 and args[0] == '--internal':
+ if args[1] == 'regenerate':
+ set_meson_command(mainfile)
+ from . import msetup
+ try:
+ return msetup.run(['--reconfigure'] + args[2:])
+ except Exception as e:
+ return errorhandler(e, 'setup')
+ else:
+ return run_script_command(args[1], args[2:])
+
+ set_meson_command(mainfile)
+ return CommandLineParser().run(args)
+
+def main():
+ # Always resolve the command path so Ninja can find it for regen, tests, etc.
+ if 'meson.exe' in sys.executable:
+ assert os.path.isabs(sys.executable)
+ launcher = sys.executable
+ else:
+ launcher = os.path.realpath(sys.argv[0])
+ return run(sys.argv[1:], launcher)
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/mesonbuild/minit.py b/mesonbuild/minit.py
new file mode 100644
index 0000000..042b586
--- /dev/null
+++ b/mesonbuild/minit.py
@@ -0,0 +1,192 @@
+# Copyright 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.
+from __future__ import annotations
+
+"""Code that creates simple startup projects."""
+
+from pathlib import Path
+from enum import Enum
+import subprocess
+import shutil
+import sys
+import os
+import re
+from glob import glob
+from mesonbuild import mesonlib
+from mesonbuild.coredata import FORBIDDEN_TARGET_NAMES
+from mesonbuild.environment import detect_ninja
+from mesonbuild.templates.samplefactory import sameple_generator
+import typing as T
+
+if T.TYPE_CHECKING:
+ import argparse
+
+'''
+we currently have one meson template at this time.
+'''
+from mesonbuild.templates.mesontemplates import create_meson_build
+
+FORTRAN_SUFFIXES = {'.f', '.for', '.F', '.f90', '.F90'}
+LANG_SUFFIXES = {'.c', '.cc', '.cpp', '.cs', '.cu', '.d', '.m', '.mm', '.rs', '.java', '.vala'} | FORTRAN_SUFFIXES
+LANG_SUPPORTED = {'c', 'cpp', 'cs', 'cuda', 'd', 'fortran', 'java', 'rust', 'objc', 'objcpp', 'vala'}
+
+DEFAULT_PROJECT = 'executable'
+DEFAULT_VERSION = '0.1'
+class DEFAULT_TYPES(Enum):
+ EXE = 'executable'
+ LIB = 'library'
+
+INFO_MESSAGE = '''Sample project created. To build it run the
+following commands:
+
+meson setup builddir
+meson compile -C builddir
+'''
+
+
+def create_sample(options: 'argparse.Namespace') -> None:
+ '''
+ Based on what arguments are passed we check for a match in language
+ then check for project type and create new Meson samples project.
+ '''
+ sample_gen = sameple_generator(options)
+ if options.type == DEFAULT_TYPES['EXE'].value:
+ sample_gen.create_executable()
+ elif options.type == DEFAULT_TYPES['LIB'].value:
+ sample_gen.create_library()
+ else:
+ raise RuntimeError('Unreachable code')
+ print(INFO_MESSAGE)
+
+def autodetect_options(options: 'argparse.Namespace', sample: bool = False) -> None:
+ '''
+ Here we autodetect options for args not passed in so don't have to
+ think about it.
+ '''
+ if not options.name:
+ options.name = Path().resolve().stem
+ if not re.match('[a-zA-Z_][a-zA-Z0-9]*', options.name) and sample:
+ raise SystemExit(f'Name of current directory "{options.name}" is not usable as a sample project name.\n'
+ 'Specify a project name with --name.')
+ print(f'Using "{options.name}" (name of current directory) as project name.')
+ if not options.executable:
+ options.executable = options.name
+ print(f'Using "{options.executable}" (project name) as name of executable to build.')
+ if options.executable in FORBIDDEN_TARGET_NAMES:
+ raise mesonlib.MesonException(f'Executable name {options.executable!r} is reserved for Meson internal use. '
+ 'Refusing to init an invalid project.')
+ if sample:
+ # The rest of the autodetection is not applicable to generating sample projects.
+ return
+ if not options.srcfiles:
+ srcfiles = []
+ for f in (f for f in Path().iterdir() if f.is_file()):
+ if f.suffix in LANG_SUFFIXES:
+ srcfiles.append(f)
+ if not srcfiles:
+ raise SystemExit('No recognizable source files found.\n'
+ 'Run meson init in an empty directory to create a sample project.')
+ options.srcfiles = srcfiles
+ print("Detected source files: " + ' '.join(str(s) for s in srcfiles))
+ options.srcfiles = [Path(f) for f in options.srcfiles]
+ if not options.language:
+ for f in options.srcfiles:
+ if f.suffix == '.c':
+ options.language = 'c'
+ break
+ if f.suffix in {'.cc', '.cpp'}:
+ options.language = 'cpp'
+ break
+ if f.suffix == '.cs':
+ options.language = 'cs'
+ break
+ if f.suffix == '.cu':
+ options.language = 'cuda'
+ break
+ if f.suffix == '.d':
+ options.language = 'd'
+ break
+ if f.suffix in FORTRAN_SUFFIXES:
+ options.language = 'fortran'
+ break
+ if f.suffix == '.rs':
+ options.language = 'rust'
+ break
+ if f.suffix == '.m':
+ options.language = 'objc'
+ break
+ if f.suffix == '.mm':
+ options.language = 'objcpp'
+ break
+ if f.suffix == '.java':
+ options.language = 'java'
+ break
+ if f.suffix == '.vala':
+ options.language = 'vala'
+ break
+ if not options.language:
+ raise SystemExit("Can't autodetect language, please specify it with -l.")
+ print("Detected language: " + options.language)
+
+def add_arguments(parser: 'argparse.ArgumentParser') -> None:
+ '''
+ Here we add args for that the user can passed when making a new
+ Meson project.
+ '''
+ parser.add_argument("srcfiles", metavar="sourcefile", nargs="*", help="source files. default: all recognized files in current directory")
+ parser.add_argument('-C', dest='wd', action=mesonlib.RealPathAction,
+ help='directory to cd into before running')
+ parser.add_argument("-n", "--name", help="project name. default: name of current directory")
+ parser.add_argument("-e", "--executable", help="executable name. default: project name")
+ parser.add_argument("-d", "--deps", help="dependencies, comma-separated")
+ parser.add_argument("-l", "--language", choices=sorted(LANG_SUPPORTED), help="project language. default: autodetected based on source files")
+ parser.add_argument("-b", "--build", action='store_true', help="build after generation")
+ parser.add_argument("--builddir", default='build', help="directory for build")
+ parser.add_argument("-f", "--force", action="store_true", help="force overwrite of existing files and directories.")
+ parser.add_argument('--type', default=DEFAULT_PROJECT, choices=('executable', 'library'), help=f"project type. default: {DEFAULT_PROJECT} based project")
+ parser.add_argument('--version', default=DEFAULT_VERSION, help=f"project version. default: {DEFAULT_VERSION}")
+
+def run(options: 'argparse.Namespace') -> int:
+ '''
+ Here we generate the new Meson sample project.
+ '''
+ if not Path(options.wd).exists():
+ sys.exit('Project source root directory not found. Run this command in source directory root.')
+ os.chdir(options.wd)
+
+ if not glob('*'):
+ autodetect_options(options, sample=True)
+ if not options.language:
+ print('Defaulting to generating a C language project.')
+ options.language = 'c'
+ create_sample(options)
+ else:
+ autodetect_options(options)
+ if Path('meson.build').is_file() and not options.force:
+ raise SystemExit('meson.build already exists. Use --force to overwrite.')
+ create_meson_build(options)
+ if options.build:
+ if Path(options.builddir).is_dir() and options.force:
+ print('Build directory already exists, deleting it.')
+ shutil.rmtree(options.builddir)
+ print('Building...')
+ cmd = mesonlib.get_meson_command() + [options.builddir]
+ ret = subprocess.run(cmd)
+ if ret.returncode:
+ raise SystemExit
+ cmd = detect_ninja() + ['-C', options.builddir]
+ ret = subprocess.run(cmd)
+ if ret.returncode:
+ raise SystemExit
+ return 0
diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py
new file mode 100644
index 0000000..8c74990
--- /dev/null
+++ b/mesonbuild/minstall.py
@@ -0,0 +1,774 @@
+# Copyright 2013-2014 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.
+from __future__ import annotations
+
+from glob import glob
+from pathlib import Path
+import argparse
+import errno
+import os
+import shlex
+import shutil
+import subprocess
+import sys
+import typing as T
+
+from . import build
+from . import environment
+from .backend.backends import InstallData
+from .mesonlib import MesonException, Popen_safe, RealPathAction, is_windows, setup_vsenv, pickle_load, is_osx
+from .scripts import depfixer, destdir_join
+from .scripts.meson_exe import run_exe
+try:
+ from __main__ import __file__ as main_file
+except ImportError:
+ # Happens when running as meson.exe which is native Windows.
+ # This is only used for pkexec which is not, so this is fine.
+ main_file = None
+
+if T.TYPE_CHECKING:
+ from .backend.backends import (
+ ExecutableSerialisation, InstallDataBase, InstallEmptyDir,
+ InstallSymlinkData, TargetInstallData
+ )
+ from .mesonlib import FileMode
+
+ try:
+ from typing import Protocol
+ except AttributeError:
+ from typing_extensions import Protocol # type: ignore
+
+ class ArgumentType(Protocol):
+ """Typing information for the object returned by argparse."""
+ no_rebuild: bool
+ only_changed: bool
+ profile: bool
+ quiet: bool
+ wd: str
+ destdir: str
+ dry_run: bool
+ skip_subprojects: str
+ tags: str
+ strip: bool
+
+
+symlink_warning = '''Warning: trying to copy a symlink that points to a file. This will copy the file,
+but this will be changed in a future version of Meson to copy the symlink as is. Please update your
+build definitions so that it will not break when the change happens.'''
+
+selinux_updates: T.List[str] = []
+
+def add_arguments(parser: argparse.ArgumentParser) -> None:
+ parser.add_argument('-C', dest='wd', action=RealPathAction,
+ help='directory to cd into before running')
+ parser.add_argument('--profile-self', action='store_true', dest='profile',
+ help=argparse.SUPPRESS)
+ parser.add_argument('--no-rebuild', default=False, action='store_true',
+ help='Do not rebuild before installing.')
+ parser.add_argument('--only-changed', default=False, action='store_true',
+ help='Only overwrite files that are older than the copied file.')
+ parser.add_argument('--quiet', default=False, action='store_true',
+ help='Do not print every file that was installed.')
+ parser.add_argument('--destdir', default=None,
+ help='Sets or overrides DESTDIR environment. (Since 0.57.0)')
+ parser.add_argument('--dry-run', '-n', action='store_true',
+ help='Doesn\'t actually install, but print logs. (Since 0.57.0)')
+ parser.add_argument('--skip-subprojects', nargs='?', const='*', default='',
+ help='Do not install files from given subprojects. (Since 0.58.0)')
+ parser.add_argument('--tags', default=None,
+ help='Install only targets having one of the given tags. (Since 0.60.0)')
+ parser.add_argument('--strip', action='store_true',
+ help='Strip targets even if strip option was not set during configure. (Since 0.62.0)')
+
+class DirMaker:
+ def __init__(self, lf: T.TextIO, makedirs: T.Callable[..., None]):
+ self.lf = lf
+ self.dirs: T.List[str] = []
+ self.all_dirs: T.Set[str] = set()
+ self.makedirs_impl = makedirs
+
+ def makedirs(self, path: str, exist_ok: bool = False) -> None:
+ dirname = os.path.normpath(path)
+ self.all_dirs.add(dirname)
+ dirs = []
+ while dirname != os.path.dirname(dirname):
+ if dirname in self.dirs:
+ # In dry-run mode the directory does not exist but we would have
+ # created it with all its parents otherwise.
+ break
+ if not os.path.exists(dirname):
+ dirs.append(dirname)
+ dirname = os.path.dirname(dirname)
+ self.makedirs_impl(path, exist_ok=exist_ok)
+
+ # store the directories in creation order, with the parent directory
+ # before the child directories. Future calls of makedir() will not
+ # create the parent directories, so the last element in the list is
+ # the last one to be created. That is the first one to be removed on
+ # __exit__
+ dirs.reverse()
+ self.dirs += dirs
+
+ def __enter__(self) -> 'DirMaker':
+ return self
+
+ def __exit__(self, exception_type: T.Type[Exception], value: T.Any, traceback: T.Any) -> None:
+ self.dirs.reverse()
+ for d in self.dirs:
+ append_to_log(self.lf, d)
+
+
+def load_install_data(fname: str) -> InstallData:
+ obj = pickle_load(fname, 'InstallData', InstallData)
+ assert isinstance(obj, InstallData), 'fo mypy'
+ return obj
+
+def is_executable(path: str, follow_symlinks: bool = False) -> bool:
+ '''Checks whether any of the "x" bits are set in the source file mode.'''
+ return bool(os.stat(path, follow_symlinks=follow_symlinks).st_mode & 0o111)
+
+
+def append_to_log(lf: T.TextIO, line: str) -> None:
+ lf.write(line)
+ if not line.endswith('\n'):
+ lf.write('\n')
+ lf.flush()
+
+
+def set_chown(path: str, user: T.Union[str, int, None] = None,
+ group: T.Union[str, int, None] = None,
+ dir_fd: T.Optional[int] = None, follow_symlinks: bool = True) -> None:
+ # shutil.chown will call os.chown without passing all the parameters
+ # and particularly follow_symlinks, thus we replace it temporary
+ # with a lambda with all the parameters so that follow_symlinks will
+ # be actually passed properly.
+ # Not nice, but better than actually rewriting shutil.chown until
+ # this python bug is fixed: https://bugs.python.org/issue18108
+ real_os_chown = os.chown
+
+ def chown(path: T.Union[int, str, 'os.PathLike[str]', bytes, 'os.PathLike[bytes]'],
+ uid: int, gid: int, *, dir_fd: T.Optional[int] = dir_fd,
+ follow_symlinks: bool = follow_symlinks) -> None:
+ """Override the default behavior of os.chown
+
+ Use a real function rather than a lambda to help mypy out. Also real
+ functions are faster.
+ """
+ real_os_chown(path, uid, gid, dir_fd=dir_fd, follow_symlinks=follow_symlinks)
+
+ try:
+ os.chown = chown
+ shutil.chown(path, user, group)
+ finally:
+ os.chown = real_os_chown
+
+
+def set_chmod(path: str, mode: int, dir_fd: T.Optional[int] = None,
+ follow_symlinks: bool = True) -> None:
+ try:
+ os.chmod(path, mode, dir_fd=dir_fd, follow_symlinks=follow_symlinks)
+ except (NotImplementedError, OSError, SystemError):
+ if not os.path.islink(path):
+ os.chmod(path, mode, dir_fd=dir_fd)
+
+
+def sanitize_permissions(path: str, umask: T.Union[str, int]) -> None:
+ # TODO: with python 3.8 or typing_extensions we could replace this with
+ # `umask: T.Union[T.Literal['preserve'], int]`, which would be more correct
+ if umask == 'preserve':
+ return
+ assert isinstance(umask, int), 'umask should only be "preserver" or an integer'
+ new_perms = 0o777 if is_executable(path, follow_symlinks=False) else 0o666
+ new_perms &= ~umask
+ try:
+ set_chmod(path, new_perms, follow_symlinks=False)
+ except PermissionError as e:
+ print(f'{path!r}: Unable to set permissions {new_perms!r}: {e.strerror}, ignoring...')
+
+
+def set_mode(path: str, mode: T.Optional['FileMode'], default_umask: T.Union[str, int]) -> None:
+ if mode is None or all(m is None for m in [mode.perms_s, mode.owner, mode.group]):
+ # Just sanitize permissions with the default umask
+ sanitize_permissions(path, default_umask)
+ return
+ # No chown() on Windows, and must set one of owner/group
+ if not is_windows() and (mode.owner is not None or mode.group is not None):
+ try:
+ set_chown(path, mode.owner, mode.group, follow_symlinks=False)
+ except PermissionError as e:
+ print(f'{path!r}: Unable to set owner {mode.owner!r} and group {mode.group!r}: {e.strerror}, ignoring...')
+ except LookupError:
+ print(f'{path!r}: Non-existent owner {mode.owner!r} or group {mode.group!r}: ignoring...')
+ except OSError as e:
+ if e.errno == errno.EINVAL:
+ print(f'{path!r}: Non-existent numeric owner {mode.owner!r} or group {mode.group!r}: ignoring...')
+ else:
+ raise
+ # Must set permissions *after* setting owner/group otherwise the
+ # setuid/setgid bits will get wiped by chmod
+ # NOTE: On Windows you can set read/write perms; the rest are ignored
+ if mode.perms_s is not None:
+ try:
+ set_chmod(path, mode.perms, follow_symlinks=False)
+ except PermissionError as e:
+ print(f'{path!r}: Unable to set permissions {mode.perms_s!r}: {e.strerror}, ignoring...')
+ else:
+ sanitize_permissions(path, default_umask)
+
+
+def restore_selinux_contexts() -> None:
+ '''
+ Restores the SELinux context for files in @selinux_updates
+
+ If $DESTDIR is set, do not warn if the call fails.
+ '''
+ try:
+ subprocess.check_call(['selinuxenabled'])
+ except (FileNotFoundError, NotADirectoryError, OSError, PermissionError, subprocess.CalledProcessError):
+ # If we don't have selinux or selinuxenabled returned 1, failure
+ # is ignored quietly.
+ return
+
+ if not shutil.which('restorecon'):
+ # If we don't have restorecon, failure is ignored quietly.
+ return
+
+ if not selinux_updates:
+ # If the list of files is empty, do not try to call restorecon.
+ return
+
+ proc, out, err = Popen_safe(['restorecon', '-F', '-f-', '-0'], ('\0'.join(f for f in selinux_updates) + '\0'))
+ if proc.returncode != 0:
+ print('Failed to restore SELinux context of installed files...',
+ 'Standard output:', out,
+ 'Standard error:', err, sep='\n')
+
+def get_destdir_path(destdir: str, fullprefix: str, path: str) -> str:
+ if os.path.isabs(path):
+ output = destdir_join(destdir, path)
+ else:
+ output = os.path.join(fullprefix, path)
+ return output
+
+
+def check_for_stampfile(fname: str) -> str:
+ '''Some languages e.g. Rust have output files
+ whose names are not known at configure time.
+ Check if this is the case and return the real
+ file instead.'''
+ if fname.endswith('.so') or fname.endswith('.dll'):
+ if os.stat(fname).st_size == 0:
+ (base, suffix) = os.path.splitext(fname)
+ files = glob(base + '-*' + suffix)
+ if len(files) > 1:
+ print("Stale dynamic library files in build dir. Can't install.")
+ sys.exit(1)
+ if len(files) == 1:
+ return files[0]
+ elif fname.endswith('.a') or fname.endswith('.lib'):
+ if os.stat(fname).st_size == 0:
+ (base, suffix) = os.path.splitext(fname)
+ files = glob(base + '-*' + '.rlib')
+ if len(files) > 1:
+ print("Stale static library files in build dir. Can't install.")
+ sys.exit(1)
+ if len(files) == 1:
+ return files[0]
+ return fname
+
+
+class Installer:
+
+ def __init__(self, options: 'ArgumentType', lf: T.TextIO):
+ self.did_install_something = False
+ self.printed_symlink_error = False
+ self.options = options
+ self.lf = lf
+ self.preserved_file_count = 0
+ self.dry_run = options.dry_run
+ # [''] means skip none,
+ # ['*'] means skip all,
+ # ['sub1', ...] means skip only those.
+ self.skip_subprojects = [i.strip() for i in options.skip_subprojects.split(',')]
+ self.tags = [i.strip() for i in options.tags.split(',')] if options.tags else None
+
+ def remove(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ os.remove(*args, **kwargs)
+
+ def symlink(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ os.symlink(*args, **kwargs)
+
+ def makedirs(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ os.makedirs(*args, **kwargs)
+
+ def copy(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ shutil.copy(*args, **kwargs)
+
+ def copy2(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ shutil.copy2(*args, **kwargs)
+
+ def copyfile(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ shutil.copyfile(*args, **kwargs)
+
+ def copystat(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ shutil.copystat(*args, **kwargs)
+
+ def fix_rpath(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ depfixer.fix_rpath(*args, **kwargs)
+
+ def set_chown(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ set_chown(*args, **kwargs)
+
+ def set_chmod(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ set_chmod(*args, **kwargs)
+
+ def sanitize_permissions(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ sanitize_permissions(*args, **kwargs)
+
+ def set_mode(self, *args: T.Any, **kwargs: T.Any) -> None:
+ if not self.dry_run:
+ set_mode(*args, **kwargs)
+
+ def restore_selinux_contexts(self, destdir: str) -> None:
+ if not self.dry_run and not destdir:
+ restore_selinux_contexts()
+
+ def Popen_safe(self, *args: T.Any, **kwargs: T.Any) -> T.Tuple[int, str, str]:
+ if not self.dry_run:
+ p, o, e = Popen_safe(*args, **kwargs)
+ return p.returncode, o, e
+ return 0, '', ''
+
+ def run_exe(self, *args: T.Any, **kwargs: T.Any) -> int:
+ if not self.dry_run:
+ return run_exe(*args, **kwargs)
+ return 0
+
+ def should_install(self, d: T.Union[TargetInstallData, InstallEmptyDir,
+ InstallDataBase, InstallSymlinkData,
+ ExecutableSerialisation]) -> bool:
+ if d.subproject and (d.subproject in self.skip_subprojects or '*' in self.skip_subprojects):
+ return False
+ if self.tags and d.tag not in self.tags:
+ return False
+ return True
+
+ def log(self, msg: str) -> None:
+ if not self.options.quiet:
+ print(msg)
+
+ def should_preserve_existing_file(self, from_file: str, to_file: str) -> bool:
+ if not self.options.only_changed:
+ return False
+ # Always replace danging symlinks
+ if os.path.islink(from_file) and not os.path.isfile(from_file):
+ return False
+ from_time = os.stat(from_file).st_mtime
+ to_time = os.stat(to_file).st_mtime
+ return from_time <= to_time
+
+ def do_copyfile(self, from_file: str, to_file: str,
+ makedirs: T.Optional[T.Tuple[T.Any, str]] = None) -> bool:
+ outdir = os.path.split(to_file)[0]
+ if not os.path.isfile(from_file) and not os.path.islink(from_file):
+ raise MesonException(f'Tried to install something that isn\'t a file: {from_file!r}')
+ # copyfile fails if the target file already exists, so remove it to
+ # allow overwriting a previous install. If the target is not a file, we
+ # want to give a readable error.
+ if os.path.exists(to_file):
+ if not os.path.isfile(to_file):
+ raise MesonException(f'Destination {to_file!r} already exists and is not a file')
+ if self.should_preserve_existing_file(from_file, to_file):
+ append_to_log(self.lf, f'# Preserving old file {to_file}\n')
+ self.preserved_file_count += 1
+ return False
+ self.remove(to_file)
+ elif makedirs:
+ # Unpack tuple
+ dirmaker, outdir = makedirs
+ # Create dirs if needed
+ dirmaker.makedirs(outdir, exist_ok=True)
+ self.log(f'Installing {from_file} to {outdir}')
+ if os.path.islink(from_file):
+ if not os.path.exists(from_file):
+ # Dangling symlink. Replicate as is.
+ self.copy(from_file, outdir, follow_symlinks=False)
+ else:
+ # Remove this entire branch when changing the behaviour to duplicate
+ # symlinks rather than copying what they point to.
+ print(symlink_warning)
+ self.copy2(from_file, to_file)
+ else:
+ self.copy2(from_file, to_file)
+ selinux_updates.append(to_file)
+ append_to_log(self.lf, to_file)
+ return True
+
+ def do_symlink(self, target: str, link: str, destdir: str, full_dst_dir: str, allow_missing: bool) -> bool:
+ abs_target = target
+ if not os.path.isabs(target):
+ abs_target = os.path.join(full_dst_dir, target)
+ elif not os.path.exists(abs_target) and not allow_missing:
+ abs_target = destdir_join(destdir, abs_target)
+ if not os.path.exists(abs_target) and not allow_missing:
+ raise MesonException(f'Tried to install symlink to missing file {abs_target}')
+ if os.path.exists(link):
+ if not os.path.islink(link):
+ raise MesonException(f'Destination {link!r} already exists and is not a symlink')
+ self.remove(link)
+ if not self.printed_symlink_error:
+ self.log(f'Installing symlink pointing to {target} to {link}')
+ try:
+ self.symlink(target, link, target_is_directory=os.path.isdir(abs_target))
+ except (NotImplementedError, OSError):
+ if not self.printed_symlink_error:
+ print("Symlink creation does not work on this platform. "
+ "Skipping all symlinking.")
+ self.printed_symlink_error = True
+ return False
+ append_to_log(self.lf, link)
+ return True
+
+ def do_copydir(self, data: InstallData, src_dir: str, dst_dir: str,
+ exclude: T.Optional[T.Tuple[T.Set[str], T.Set[str]]],
+ install_mode: 'FileMode', dm: DirMaker) -> None:
+ '''
+ Copies the contents of directory @src_dir into @dst_dir.
+
+ For directory
+ /foo/
+ bar/
+ excluded
+ foobar
+ file
+ do_copydir(..., '/foo', '/dst/dir', {'bar/excluded'}) creates
+ /dst/
+ dir/
+ bar/
+ foobar
+ file
+
+ Args:
+ src_dir: str, absolute path to the source directory
+ dst_dir: str, absolute path to the destination directory
+ exclude: (set(str), set(str)), tuple of (exclude_files, exclude_dirs),
+ each element of the set is a path relative to src_dir.
+ '''
+ if not os.path.isabs(src_dir):
+ raise ValueError(f'src_dir must be absolute, got {src_dir}')
+ if not os.path.isabs(dst_dir):
+ raise ValueError(f'dst_dir must be absolute, got {dst_dir}')
+ if exclude is not None:
+ exclude_files, exclude_dirs = exclude
+ else:
+ exclude_files = exclude_dirs = set()
+ for root, dirs, files in os.walk(src_dir):
+ assert os.path.isabs(root)
+ for d in dirs[:]:
+ abs_src = os.path.join(root, d)
+ filepart = os.path.relpath(abs_src, start=src_dir)
+ abs_dst = os.path.join(dst_dir, filepart)
+ # Remove these so they aren't visited by os.walk at all.
+ if filepart in exclude_dirs:
+ dirs.remove(d)
+ continue
+ if os.path.isdir(abs_dst):
+ continue
+ if os.path.exists(abs_dst):
+ print(f'Tried to copy directory {abs_dst} but a file of that name already exists.')
+ sys.exit(1)
+ dm.makedirs(abs_dst)
+ self.copystat(abs_src, abs_dst)
+ self.sanitize_permissions(abs_dst, data.install_umask)
+ for f in files:
+ abs_src = os.path.join(root, f)
+ filepart = os.path.relpath(abs_src, start=src_dir)
+ if filepart in exclude_files:
+ continue
+ abs_dst = os.path.join(dst_dir, filepart)
+ if os.path.isdir(abs_dst):
+ print(f'Tried to copy file {abs_dst} but a directory of that name already exists.')
+ sys.exit(1)
+ parent_dir = os.path.dirname(abs_dst)
+ if not os.path.isdir(parent_dir):
+ dm.makedirs(parent_dir)
+ self.copystat(os.path.dirname(abs_src), parent_dir)
+ # FIXME: what about symlinks?
+ self.do_copyfile(abs_src, abs_dst)
+ self.set_mode(abs_dst, install_mode, data.install_umask)
+
+ def do_install(self, datafilename: str) -> None:
+ d = load_install_data(datafilename)
+
+ destdir = self.options.destdir
+ if destdir is None:
+ destdir = os.environ.get('DESTDIR')
+ if destdir and not os.path.isabs(destdir):
+ destdir = os.path.join(d.build_dir, destdir)
+ # Override in the env because some scripts could use it and require an
+ # absolute path.
+ if destdir is not None:
+ os.environ['DESTDIR'] = destdir
+ destdir = destdir or ''
+ fullprefix = destdir_join(destdir, d.prefix)
+
+ if d.install_umask != 'preserve':
+ assert isinstance(d.install_umask, int)
+ os.umask(d.install_umask)
+
+ self.did_install_something = False
+ try:
+ with DirMaker(self.lf, self.makedirs) as dm:
+ self.install_subdirs(d, dm, destdir, fullprefix) # Must be first, because it needs to delete the old subtree.
+ self.install_targets(d, dm, destdir, fullprefix)
+ self.install_headers(d, dm, destdir, fullprefix)
+ self.install_man(d, dm, destdir, fullprefix)
+ self.install_emptydir(d, dm, destdir, fullprefix)
+ self.install_data(d, dm, destdir, fullprefix)
+ self.install_symlinks(d, dm, destdir, fullprefix)
+ self.restore_selinux_contexts(destdir)
+ self.run_install_script(d, destdir, fullprefix)
+ if not self.did_install_something:
+ self.log('Nothing to install.')
+ if not self.options.quiet and self.preserved_file_count > 0:
+ self.log('Preserved {} unchanged files, see {} for the full list'
+ .format(self.preserved_file_count, os.path.normpath(self.lf.name)))
+ except PermissionError:
+ if shutil.which('pkexec') is not None and 'PKEXEC_UID' not in os.environ and destdir == '':
+ print('Installation failed due to insufficient permissions.')
+ print('Attempting to use polkit to gain elevated privileges...')
+ os.execlp('pkexec', 'pkexec', sys.executable, main_file, *sys.argv[1:],
+ '-C', os.getcwd())
+ else:
+ raise
+
+ def do_strip(self, strip_bin: T.List[str], fname: str, outname: str) -> None:
+ self.log(f'Stripping target {fname!r}.')
+ if is_osx():
+ # macOS expects dynamic objects to be stripped with -x maximum.
+ # To also strip the debug info, -S must be added.
+ # See: https://www.unix.com/man-page/osx/1/strip/
+ returncode, stdo, stde = self.Popen_safe(strip_bin + ['-S', '-x', outname])
+ else:
+ returncode, stdo, stde = self.Popen_safe(strip_bin + [outname])
+ if returncode != 0:
+ print('Could not strip file.\n')
+ print(f'Stdout:\n{stdo}\n')
+ print(f'Stderr:\n{stde}\n')
+ sys.exit(1)
+
+ def install_subdirs(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
+ for i in d.install_subdirs:
+ if not self.should_install(i):
+ continue
+ self.did_install_something = True
+ full_dst_dir = get_destdir_path(destdir, fullprefix, i.install_path)
+ self.log(f'Installing subdir {i.path} to {full_dst_dir}')
+ dm.makedirs(full_dst_dir, exist_ok=True)
+ self.do_copydir(d, i.path, full_dst_dir, i.exclude, i.install_mode, dm)
+
+ def install_data(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
+ for i in d.data:
+ if not self.should_install(i):
+ continue
+ fullfilename = i.path
+ outfilename = get_destdir_path(destdir, fullprefix, i.install_path)
+ outdir = os.path.dirname(outfilename)
+ if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)):
+ self.did_install_something = True
+ self.set_mode(outfilename, i.install_mode, d.install_umask)
+
+ def install_symlinks(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
+ for s in d.symlinks:
+ if not self.should_install(s):
+ continue
+ full_dst_dir = get_destdir_path(destdir, fullprefix, s.install_path)
+ full_link_name = get_destdir_path(destdir, fullprefix, s.name)
+ dm.makedirs(full_dst_dir, exist_ok=True)
+ if self.do_symlink(s.target, full_link_name, destdir, full_dst_dir, s.allow_missing):
+ self.did_install_something = True
+
+ def install_man(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
+ for m in d.man:
+ if not self.should_install(m):
+ continue
+ full_source_filename = m.path
+ outfilename = get_destdir_path(destdir, fullprefix, m.install_path)
+ outdir = os.path.dirname(outfilename)
+ if self.do_copyfile(full_source_filename, outfilename, makedirs=(dm, outdir)):
+ self.did_install_something = True
+ self.set_mode(outfilename, m.install_mode, d.install_umask)
+
+ def install_emptydir(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
+ for e in d.emptydir:
+ if not self.should_install(e):
+ continue
+ self.did_install_something = True
+ full_dst_dir = get_destdir_path(destdir, fullprefix, e.path)
+ self.log(f'Installing new directory {full_dst_dir}')
+ if os.path.isfile(full_dst_dir):
+ print(f'Tried to create directory {full_dst_dir} but a file of that name already exists.')
+ sys.exit(1)
+ dm.makedirs(full_dst_dir, exist_ok=True)
+ self.set_mode(full_dst_dir, e.install_mode, d.install_umask)
+
+ def install_headers(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
+ for t in d.headers:
+ if not self.should_install(t):
+ continue
+ fullfilename = t.path
+ fname = os.path.basename(fullfilename)
+ outdir = get_destdir_path(destdir, fullprefix, t.install_path)
+ outfilename = os.path.join(outdir, fname)
+ if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)):
+ self.did_install_something = True
+ self.set_mode(outfilename, t.install_mode, d.install_umask)
+
+ def run_install_script(self, d: InstallData, destdir: str, fullprefix: str) -> None:
+ env = {'MESON_SOURCE_ROOT': d.source_dir,
+ 'MESON_BUILD_ROOT': d.build_dir,
+ 'MESON_INSTALL_PREFIX': d.prefix,
+ 'MESON_INSTALL_DESTDIR_PREFIX': fullprefix,
+ 'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in d.mesonintrospect]),
+ }
+ if self.options.quiet:
+ env['MESON_INSTALL_QUIET'] = '1'
+
+ for i in d.install_scripts:
+ if not self.should_install(i):
+ continue
+ name = ' '.join(i.cmd_args)
+ if i.skip_if_destdir and destdir:
+ self.log(f'Skipping custom install script because DESTDIR is set {name!r}')
+ continue
+ self.did_install_something = True # Custom script must report itself if it does nothing.
+ self.log(f'Running custom install script {name!r}')
+ try:
+ rc = self.run_exe(i, env)
+ except OSError:
+ print(f'FAILED: install script \'{name}\' could not be run, stopped')
+ # POSIX shells return 127 when a command could not be found
+ sys.exit(127)
+ if rc != 0:
+ print(f'FAILED: install script \'{name}\' exit code {rc}, stopped')
+ sys.exit(rc)
+
+ def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
+ for t in d.targets:
+ if not self.should_install(t):
+ continue
+ if not os.path.exists(t.fname):
+ # For example, import libraries of shared modules are optional
+ if t.optional:
+ self.log(f'File {t.fname!r} not found, skipping')
+ continue
+ else:
+ raise MesonException(f'File {t.fname!r} could not be found')
+ file_copied = False # not set when a directory is copied
+ fname = check_for_stampfile(t.fname)
+ outdir = get_destdir_path(destdir, fullprefix, t.outdir)
+ outname = os.path.join(outdir, os.path.basename(fname))
+ final_path = os.path.join(d.prefix, t.outdir, os.path.basename(fname))
+ should_strip = t.strip or (t.can_strip and self.options.strip)
+ install_rpath = t.install_rpath
+ install_name_mappings = t.install_name_mappings
+ install_mode = t.install_mode
+ if not os.path.exists(fname):
+ raise MesonException(f'File {fname!r} could not be found')
+ elif os.path.isfile(fname):
+ file_copied = self.do_copyfile(fname, outname, makedirs=(dm, outdir))
+ if should_strip and d.strip_bin is not None:
+ if fname.endswith('.jar'):
+ self.log('Not stripping jar target: {}'.format(os.path.basename(fname)))
+ continue
+ self.do_strip(d.strip_bin, fname, outname)
+ if fname.endswith('.js'):
+ # Emscripten outputs js files and optionally a wasm file.
+ # If one was generated, install it as well.
+ wasm_source = os.path.splitext(fname)[0] + '.wasm'
+ if os.path.exists(wasm_source):
+ wasm_output = os.path.splitext(outname)[0] + '.wasm'
+ file_copied = self.do_copyfile(wasm_source, wasm_output)
+ elif os.path.isdir(fname):
+ fname = os.path.join(d.build_dir, fname.rstrip('/'))
+ outname = os.path.join(outdir, os.path.basename(fname))
+ dm.makedirs(outdir, exist_ok=True)
+ self.do_copydir(d, fname, outname, None, install_mode, dm)
+ else:
+ raise RuntimeError(f'Unknown file type for {fname!r}')
+ if file_copied:
+ self.did_install_something = True
+ try:
+ self.fix_rpath(outname, t.rpath_dirs_to_remove, install_rpath, final_path,
+ install_name_mappings, verbose=False)
+ except SystemExit as e:
+ if isinstance(e.code, int) and e.code == 0:
+ pass
+ else:
+ raise
+ # file mode needs to be set last, after strip/depfixer editing
+ self.set_mode(outname, install_mode, d.install_umask)
+
+def rebuild_all(wd: str) -> bool:
+ if not (Path(wd) / 'build.ninja').is_file():
+ print('Only ninja backend is supported to rebuild the project before installation.')
+ return True
+
+ ninja = environment.detect_ninja()
+ if not ninja:
+ print("Can't find ninja, can't rebuild test.")
+ return False
+
+ ret = subprocess.run(ninja + ['-C', wd]).returncode
+ if ret != 0:
+ print(f'Could not rebuild {wd}')
+ return False
+
+ return True
+
+
+def run(opts: 'ArgumentType') -> int:
+ datafilename = 'meson-private/install.dat'
+ private_dir = os.path.dirname(datafilename)
+ log_dir = os.path.join(private_dir, '../meson-logs')
+ if not os.path.exists(os.path.join(opts.wd, datafilename)):
+ sys.exit('Install data not found. Run this command in build directory root.')
+ if not opts.no_rebuild:
+ b = build.load(opts.wd)
+ setup_vsenv(b.need_vsenv)
+ if not rebuild_all(opts.wd):
+ sys.exit(-1)
+ os.chdir(opts.wd)
+ with open(os.path.join(log_dir, 'install-log.txt'), 'w', encoding='utf-8') as lf:
+ installer = Installer(opts, lf)
+ append_to_log(lf, '# List of files installed by Meson')
+ append_to_log(lf, '# Does not contain files installed by custom scripts.')
+ if opts.profile:
+ import cProfile as profile
+ fname = os.path.join(private_dir, 'profile-installer.log')
+ profile.runctx('installer.do_install(datafilename)', globals(), locals(), filename=fname)
+ else:
+ installer.do_install(datafilename)
+ return 0
diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py
new file mode 100644
index 0000000..2312674
--- /dev/null
+++ b/mesonbuild/mintro.py
@@ -0,0 +1,583 @@
+# Copyright 2014-2016 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.
+from __future__ import annotations
+
+"""This is a helper script for IDE developers. It allows you to
+extract information such as list of targets, files, compiler flags,
+tests and so on. All output is in JSON for simple parsing.
+
+Currently only works for the Ninja backend. Others use generated
+project files and don't need this info."""
+
+import collections
+import json
+import os
+from pathlib import Path, PurePath
+import typing as T
+
+from . import build, mesonlib, mlog, coredata as cdata
+from .ast import IntrospectionInterpreter, BUILD_TARGET_FUNCTIONS, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstJSONPrinter
+from .backend import backends
+from .mesonlib import OptionKey
+from .mparser import FunctionNode, ArrayNode, ArgumentNode, StringNode
+
+if T.TYPE_CHECKING:
+ import argparse
+
+ from .interpreter import Interpreter
+ from .mparser import BaseNode
+
+def get_meson_info_file(info_dir: str) -> str:
+ return os.path.join(info_dir, 'meson-info.json')
+
+def get_meson_introspection_version() -> str:
+ return '1.0.0'
+
+def get_meson_introspection_required_version() -> T.List[str]:
+ return ['>=1.0', '<2.0']
+
+class IntroCommand:
+ def __init__(self,
+ desc: str,
+ func: T.Optional[T.Callable[[], T.Union[dict, list]]] = None,
+ no_bd: T.Optional[T.Callable[[IntrospectionInterpreter], T.Union[dict, list]]] = None) -> None:
+ self.desc = desc + '.'
+ self.func = func
+ self.no_bd = no_bd
+
+def get_meson_introspection_types(coredata: T.Optional[cdata.CoreData] = None,
+ builddata: T.Optional[build.Build] = None,
+ backend: T.Optional[backends.Backend] = None,
+ sourcedir: T.Optional[str] = None) -> 'T.Mapping[str, IntroCommand]':
+ if backend and builddata:
+ benchmarkdata = backend.create_test_serialisation(builddata.get_benchmarks())
+ testdata = backend.create_test_serialisation(builddata.get_tests())
+ installdata = backend.create_install_data()
+ interpreter = backend.interpreter
+ else:
+ benchmarkdata = testdata = installdata = None
+
+ # Enforce key order for argparse
+ return collections.OrderedDict([
+ ('ast', IntroCommand('Dump the AST of the meson file', no_bd=dump_ast)),
+ ('benchmarks', IntroCommand('List all benchmarks', func=lambda: list_benchmarks(benchmarkdata))),
+ ('buildoptions', IntroCommand('List all build options', func=lambda: list_buildoptions(coredata), no_bd=list_buildoptions_from_source)),
+ ('buildsystem_files', IntroCommand('List files that make up the build system', func=lambda: list_buildsystem_files(builddata, interpreter))),
+ ('dependencies', IntroCommand('List external dependencies', func=lambda: list_deps(coredata), no_bd=list_deps_from_source)),
+ ('scan_dependencies', IntroCommand('Scan for dependencies used in the meson.build file', no_bd=list_deps_from_source)),
+ ('installed', IntroCommand('List all installed files and directories', func=lambda: list_installed(installdata))),
+ ('install_plan', IntroCommand('List all installed files and directories with their details', func=lambda: list_install_plan(installdata))),
+ ('projectinfo', IntroCommand('Information about projects', func=lambda: list_projinfo(builddata), no_bd=list_projinfo_from_source)),
+ ('targets', IntroCommand('List top level targets', func=lambda: list_targets(builddata, installdata, backend), no_bd=list_targets_from_source)),
+ ('tests', IntroCommand('List all unit tests', func=lambda: list_tests(testdata))),
+ ])
+
+def add_arguments(parser: argparse.ArgumentParser) -> None:
+ intro_types = get_meson_introspection_types()
+ for key, val in intro_types.items():
+ flag = '--' + key.replace('_', '-')
+ parser.add_argument(flag, action='store_true', dest=key, default=False, help=val.desc)
+
+ parser.add_argument('--backend', choices=sorted(cdata.backendlist), dest='backend', default='ninja',
+ help='The backend to use for the --buildoptions introspection.')
+ parser.add_argument('-a', '--all', action='store_true', dest='all', default=False,
+ help='Print all available information.')
+ parser.add_argument('-i', '--indent', action='store_true', dest='indent', default=False,
+ help='Enable pretty printed JSON.')
+ parser.add_argument('-f', '--force-object-output', action='store_true', dest='force_dict', default=False,
+ help='Always use the new JSON format for multiple entries (even for 0 and 1 introspection commands)')
+ parser.add_argument('builddir', nargs='?', default='.', help='The build directory')
+
+def dump_ast(intr: IntrospectionInterpreter) -> T.Dict[str, T.Any]:
+ printer = AstJSONPrinter()
+ intr.ast.accept(printer)
+ return printer.result
+
+def list_installed(installdata: backends.InstallData) -> T.Dict[str, str]:
+ res = {}
+ if installdata is not None:
+ for t in installdata.targets:
+ res[os.path.join(installdata.build_dir, t.fname)] = \
+ os.path.join(installdata.prefix, t.outdir, os.path.basename(t.fname))
+ for i in installdata.data:
+ res[i.path] = os.path.join(installdata.prefix, i.install_path)
+ for i in installdata.headers:
+ res[i.path] = os.path.join(installdata.prefix, i.install_path, os.path.basename(i.path))
+ for i in installdata.man:
+ res[i.path] = os.path.join(installdata.prefix, i.install_path)
+ for i in installdata.install_subdirs:
+ res[i.path] = os.path.join(installdata.prefix, i.install_path)
+ for s in installdata.symlinks:
+ basename = os.path.basename(s.name)
+ res[basename] = os.path.join(installdata.prefix, s.install_path, basename)
+ return res
+
+def list_install_plan(installdata: backends.InstallData) -> T.Dict[str, T.Dict[str, T.Dict[str, T.Optional[str]]]]:
+ plan = {
+ 'targets': {
+ os.path.join(installdata.build_dir, target.fname): {
+ 'destination': target.out_name,
+ 'tag': target.tag or None,
+ }
+ for target in installdata.targets
+ },
+ } # type: T.Dict[str, T.Dict[str, T.Dict[str, T.Optional[str]]]]
+ for key, data_list in {
+ 'data': installdata.data,
+ 'man': installdata.man,
+ 'headers': installdata.headers,
+ 'install_subdirs': installdata.install_subdirs
+ }.items():
+ # Mypy doesn't recognize SubdirInstallData as a subclass of InstallDataBase
+ for data in data_list: # type: ignore[attr-defined]
+ data_type = data.data_type or key
+ install_path_name = data.install_path_name
+ if key == 'headers': # in the headers, install_path_name is the directory
+ install_path_name = os.path.join(install_path_name, os.path.basename(data.path))
+
+ plan[data_type] = plan.get(data_type, {})
+ plan[data_type][data.path] = {
+ 'destination': install_path_name,
+ 'tag': data.tag or None,
+ }
+ return plan
+
+def get_target_dir(coredata: cdata.CoreData, subdir: str) -> str:
+ if coredata.get_option(OptionKey('layout')) == 'flat':
+ return 'meson-out'
+ else:
+ return subdir
+
+def list_targets_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]]:
+ tlist = [] # type: T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]]
+ root_dir = Path(intr.source_root)
+
+ def nodes_to_paths(node_list: T.List[BaseNode]) -> T.List[Path]:
+ res = [] # type: T.List[Path]
+ for n in node_list:
+ args = [] # type: T.List[BaseNode]
+ if isinstance(n, FunctionNode):
+ args = list(n.args.arguments)
+ if n.func_name in BUILD_TARGET_FUNCTIONS:
+ args.pop(0)
+ elif isinstance(n, ArrayNode):
+ args = n.args.arguments
+ elif isinstance(n, ArgumentNode):
+ args = n.arguments
+ for j in args:
+ if isinstance(j, StringNode):
+ assert isinstance(j.value, str)
+ res += [Path(j.value)]
+ elif isinstance(j, str):
+ res += [Path(j)]
+ res = [root_dir / i['subdir'] / x for x in res]
+ res = [x.resolve() for x in res]
+ return res
+
+ for i in intr.targets:
+ sources = nodes_to_paths(i['sources'])
+ extra_f = nodes_to_paths(i['extra_files'])
+ outdir = get_target_dir(intr.coredata, i['subdir'])
+
+ tlist += [{
+ 'name': i['name'],
+ 'id': i['id'],
+ 'type': i['type'],
+ 'defined_in': i['defined_in'],
+ 'filename': [os.path.join(outdir, x) for x in i['outputs']],
+ 'build_by_default': i['build_by_default'],
+ 'target_sources': [{
+ 'language': 'unknown',
+ 'compiler': [],
+ 'parameters': [],
+ 'sources': [str(x) for x in sources],
+ 'generated_sources': []
+ }],
+ 'extra_files': [str(x) for x in extra_f],
+ 'subproject': None, # Subprojects are not supported
+ 'installed': i['installed']
+ }]
+
+ return tlist
+
+def list_targets(builddata: build.Build, installdata: backends.InstallData, backend: backends.Backend) -> T.List[T.Any]:
+ tlist = [] # type: T.List[T.Any]
+ build_dir = builddata.environment.get_build_dir()
+ src_dir = builddata.environment.get_source_dir()
+
+ # Fast lookup table for installation files
+ install_lookuptable = {}
+ for i in installdata.targets:
+ basename = os.path.basename(i.fname)
+ install_lookuptable[basename] = [str(PurePath(installdata.prefix, i.outdir, basename))]
+ for s in installdata.symlinks:
+ # Symlink's target must already be in the table. They share the same list
+ # to support symlinks to symlinks recursively, such as .so -> .so.0 -> .so.1.2.3
+ basename = os.path.basename(s.name)
+ try:
+ install_lookuptable[basename] = install_lookuptable[os.path.basename(s.target)]
+ install_lookuptable[basename].append(str(PurePath(installdata.prefix, s.install_path, basename)))
+ except KeyError:
+ pass
+
+ for (idname, target) in builddata.get_targets().items():
+ if not isinstance(target, build.Target):
+ raise RuntimeError('The target object in `builddata.get_targets()` is not of type `build.Target`. Please file a bug with this error message.')
+
+ outdir = get_target_dir(builddata.environment.coredata, target.subdir)
+ t = {
+ 'name': target.get_basename(),
+ 'id': idname,
+ 'type': target.get_typename(),
+ 'defined_in': os.path.normpath(os.path.join(src_dir, target.subdir, 'meson.build')),
+ 'filename': [os.path.join(build_dir, outdir, x) for x in target.get_outputs()],
+ 'build_by_default': target.build_by_default,
+ 'target_sources': backend.get_introspection_data(idname, target),
+ 'extra_files': [os.path.normpath(os.path.join(src_dir, x.subdir, x.fname)) for x in target.extra_files],
+ 'subproject': target.subproject or None
+ }
+
+ if installdata and target.should_install():
+ t['installed'] = True
+ ifn = [install_lookuptable.get(x, [None]) for x in target.get_outputs()]
+ t['install_filename'] = [x for sublist in ifn for x in sublist] # flatten the list
+ else:
+ t['installed'] = False
+ tlist.append(t)
+ return tlist
+
+def list_buildoptions_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str, T.Union[str, bool, int, T.List[str]]]]:
+ subprojects = [i['name'] for i in intr.project_data['subprojects']]
+ return list_buildoptions(intr.coredata, subprojects)
+
+def list_buildoptions(coredata: cdata.CoreData, subprojects: T.Optional[T.List[str]] = None) -> T.List[T.Dict[str, T.Union[str, bool, int, T.List[str]]]]:
+ optlist = [] # type: T.List[T.Dict[str, T.Union[str, bool, int, T.List[str]]]]
+ subprojects = subprojects or []
+
+ dir_option_names = set(cdata.BUILTIN_DIR_OPTIONS)
+ test_option_names = {OptionKey('errorlogs'),
+ OptionKey('stdsplit')}
+
+ dir_options: 'cdata.MutableKeyedOptionDictType' = {}
+ test_options: 'cdata.MutableKeyedOptionDictType' = {}
+ core_options: 'cdata.MutableKeyedOptionDictType' = {}
+ for k, v in coredata.options.items():
+ if k in dir_option_names:
+ dir_options[k] = v
+ elif k in test_option_names:
+ test_options[k] = v
+ elif k.is_builtin():
+ core_options[k] = v
+ if not v.yielding:
+ for s in subprojects:
+ core_options[k.evolve(subproject=s)] = v
+
+ def add_keys(options: 'cdata.KeyedOptionDictType', section: str) -> None:
+ for key, opt in sorted(options.items()):
+ optdict = {'name': str(key), 'value': opt.value, 'section': section,
+ 'machine': key.machine.get_lower_case_name() if coredata.is_per_machine_option(key) else 'any'}
+ if isinstance(opt, cdata.UserStringOption):
+ typestr = 'string'
+ elif isinstance(opt, cdata.UserBooleanOption):
+ typestr = 'boolean'
+ elif isinstance(opt, cdata.UserComboOption):
+ optdict['choices'] = opt.choices
+ typestr = 'combo'
+ elif isinstance(opt, cdata.UserIntegerOption):
+ typestr = 'integer'
+ elif isinstance(opt, cdata.UserArrayOption):
+ typestr = 'array'
+ if opt.choices:
+ optdict['choices'] = opt.choices
+ else:
+ raise RuntimeError("Unknown option type")
+ optdict['type'] = typestr
+ optdict['description'] = opt.description
+ optlist.append(optdict)
+
+ add_keys(core_options, 'core')
+ add_keys({k: v for k, v in coredata.options.items() if k.is_backend()}, 'backend')
+ add_keys({k: v for k, v in coredata.options.items() if k.is_base()}, 'base')
+ add_keys(
+ {k: v for k, v in sorted(coredata.options.items(), key=lambda i: i[0].machine) if k.is_compiler()},
+ 'compiler',
+ )
+ add_keys(dir_options, 'directory')
+ add_keys({k: v for k, v in coredata.options.items() if k.is_project()}, 'user')
+ add_keys(test_options, 'test')
+ return optlist
+
+def find_buildsystem_files_list(src_dir: str) -> T.List[str]:
+ # I feel dirty about this. But only slightly.
+ filelist = [] # type: T.List[str]
+ for root, _, files in os.walk(src_dir):
+ for f in files:
+ if f in {'meson.build', 'meson_options.txt'}:
+ filelist.append(os.path.relpath(os.path.join(root, f), src_dir))
+ return filelist
+
+def list_buildsystem_files(builddata: build.Build, interpreter: Interpreter) -> T.List[str]:
+ src_dir = builddata.environment.get_source_dir()
+ filelist = list(interpreter.get_build_def_files())
+ filelist = [PurePath(src_dir, x).as_posix() for x in filelist]
+ return filelist
+
+def list_deps_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str, T.Union[str, bool]]]:
+ result = [] # type: T.List[T.Dict[str, T.Union[str, bool]]]
+ for i in intr.dependencies:
+ keys = [
+ 'name',
+ 'required',
+ 'version',
+ 'has_fallback',
+ 'conditional',
+ ]
+ result += [{k: v for k, v in i.items() if k in keys}]
+ return result
+
+def list_deps(coredata: cdata.CoreData) -> T.List[T.Dict[str, T.Union[str, T.List[str]]]]:
+ result = [] # type: T.List[T.Dict[str, T.Union[str, T.List[str]]]]
+ for d in coredata.deps.host.values():
+ if d.found():
+ result += [{'name': d.name,
+ 'version': d.get_version(),
+ 'compile_args': d.get_compile_args(),
+ 'link_args': d.get_link_args()}]
+ return result
+
+def get_test_list(testdata: T.List[backends.TestSerialisation]) -> T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]]:
+ result = [] # type: T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]]
+ for t in testdata:
+ to = {} # type: T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]
+ if isinstance(t.fname, str):
+ fname = [t.fname]
+ else:
+ fname = t.fname
+ to['cmd'] = fname + t.cmd_args
+ if isinstance(t.env, build.EnvironmentVariables):
+ to['env'] = t.env.get_env({})
+ else:
+ to['env'] = t.env
+ to['name'] = t.name
+ to['workdir'] = t.workdir
+ to['timeout'] = t.timeout
+ to['suite'] = t.suite
+ to['is_parallel'] = t.is_parallel
+ to['priority'] = t.priority
+ to['protocol'] = str(t.protocol)
+ to['depends'] = t.depends
+ result.append(to)
+ return result
+
+def list_tests(testdata: T.List[backends.TestSerialisation]) -> T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]]:
+ return get_test_list(testdata)
+
+def list_benchmarks(benchdata: T.List[backends.TestSerialisation]) -> T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]]:
+ return get_test_list(benchdata)
+
+def list_projinfo(builddata: build.Build) -> T.Dict[str, T.Union[str, T.List[T.Dict[str, str]]]]:
+ result = {'version': builddata.project_version,
+ 'descriptive_name': builddata.project_name,
+ 'subproject_dir': builddata.subproject_dir} # type: T.Dict[str, T.Union[str, T.List[T.Dict[str, str]]]]
+ subprojects = []
+ for k, v in builddata.subprojects.items():
+ c = {'name': k,
+ 'version': v,
+ 'descriptive_name': builddata.projects.get(k)} # type: T.Dict[str, str]
+ subprojects.append(c)
+ result['subprojects'] = subprojects
+ return result
+
+def list_projinfo_from_source(intr: IntrospectionInterpreter) -> T.Dict[str, T.Union[str, T.List[T.Dict[str, str]]]]:
+ sourcedir = intr.source_root
+ files = find_buildsystem_files_list(sourcedir)
+ files = [os.path.normpath(x) for x in files]
+
+ for i in intr.project_data['subprojects']:
+ basedir = os.path.join(intr.subproject_dir, i['name'])
+ i['buildsystem_files'] = [x for x in files if x.startswith(basedir)]
+ files = [x for x in files if not x.startswith(basedir)]
+
+ intr.project_data['buildsystem_files'] = files
+ intr.project_data['subproject_dir'] = intr.subproject_dir
+ return intr.project_data
+
+def print_results(options: argparse.Namespace, results: T.Sequence[T.Tuple[str, T.Union[dict, T.List[T.Any]]]], indent: int) -> int:
+ if not results and not options.force_dict:
+ print('No command specified')
+ return 1
+ elif len(results) == 1 and not options.force_dict:
+ # Make to keep the existing output format for a single option
+ print(json.dumps(results[0][1], indent=indent))
+ else:
+ out = {}
+ for i in results:
+ out[i[0]] = i[1]
+ print(json.dumps(out, indent=indent))
+ return 0
+
+def get_infodir(builddir: T.Optional[str] = None) -> str:
+ infodir = 'meson-info'
+ if builddir is not None:
+ infodir = os.path.join(builddir, infodir)
+ return infodir
+
+def get_info_file(infodir: str, kind: T.Optional[str] = None) -> str:
+ return os.path.join(infodir,
+ 'meson-info.json' if not kind else f'intro-{kind}.json')
+
+def load_info_file(infodir: str, kind: T.Optional[str] = None) -> T.Any:
+ with open(get_info_file(infodir, kind), encoding='utf-8') as fp:
+ return json.load(fp)
+
+def run(options: argparse.Namespace) -> int:
+ datadir = 'meson-private'
+ infodir = get_infodir(options.builddir)
+ if options.builddir is not None:
+ datadir = os.path.join(options.builddir, datadir)
+ indent = 4 if options.indent else None
+ results = [] # type: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]]
+ sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11]
+ intro_types = get_meson_introspection_types(sourcedir=sourcedir)
+
+ if 'meson.build' in [os.path.basename(options.builddir), options.builddir]:
+ # Make sure that log entries in other parts of meson don't interfere with the JSON output
+ mlog.disable()
+ backend = backends.get_backend_from_name(options.backend)
+ assert backend is not None
+ intr = IntrospectionInterpreter(sourcedir, '', backend.name, visitors = [AstIDGenerator(), AstIndentationGenerator(), AstConditionLevel()])
+ intr.analyze()
+ # Re-enable logging just in case
+ mlog.enable()
+ for key, val in intro_types.items():
+ if (not options.all and not getattr(options, key, False)) or not val.no_bd:
+ continue
+ results += [(key, val.no_bd(intr))]
+ return print_results(options, results, indent)
+
+ try:
+ raw = load_info_file(infodir)
+ intro_vers = raw.get('introspection', {}).get('version', {}).get('full', '0.0.0')
+ except FileNotFoundError:
+ if not os.path.isdir(datadir) or not os.path.isdir(infodir):
+ print('Current directory is not a meson build directory.\n'
+ 'Please specify a valid build dir or change the working directory to it.')
+ else:
+ print('Introspection file {} does not exist.\n'
+ 'It is also possible that the build directory was generated with an old\n'
+ 'meson version. Please regenerate it in this case.'.format(get_info_file(infodir)))
+ return 1
+
+ vers_to_check = get_meson_introspection_required_version()
+ for i in vers_to_check:
+ if not mesonlib.version_compare(intro_vers, i):
+ print('Introspection version {} is not supported. '
+ 'The required version is: {}'
+ .format(intro_vers, ' and '.join(vers_to_check)))
+ return 1
+
+ # Extract introspection information from JSON
+ for i, v in intro_types.items():
+ if not v.func:
+ continue
+ if not options.all and not getattr(options, i, False):
+ continue
+ try:
+ results += [(i, load_info_file(infodir, i))]
+ except FileNotFoundError:
+ print('Introspection file {} does not exist.'.format(get_info_file(infodir, i)))
+ return 1
+
+ return print_results(options, results, indent)
+
+updated_introspection_files = [] # type: T.List[str]
+
+def write_intro_info(intro_info: T.Sequence[T.Tuple[str, T.Union[dict, T.List[T.Any]]]], info_dir: str) -> None:
+ for kind, data in intro_info:
+ out_file = os.path.join(info_dir, f'intro-{kind}.json')
+ tmp_file = os.path.join(info_dir, 'tmp_dump.json')
+ with open(tmp_file, 'w', encoding='utf-8') as fp:
+ json.dump(data, fp)
+ fp.flush() # Not sure if this is needed
+ os.replace(tmp_file, out_file)
+ updated_introspection_files.append(kind)
+
+def generate_introspection_file(builddata: build.Build, backend: backends.Backend) -> None:
+ coredata = builddata.environment.get_coredata()
+ intro_types = get_meson_introspection_types(coredata=coredata, builddata=builddata, backend=backend)
+ intro_info = [] # type: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]]
+
+ for key, val in intro_types.items():
+ if not val.func:
+ continue
+ intro_info += [(key, val.func())]
+
+ write_intro_info(intro_info, builddata.environment.info_dir)
+
+def update_build_options(coredata: cdata.CoreData, info_dir: str) -> None:
+ intro_info = [
+ ('buildoptions', list_buildoptions(coredata))
+ ]
+
+ write_intro_info(intro_info, info_dir)
+
+def split_version_string(version: str) -> T.Dict[str, T.Union[str, int]]:
+ vers_list = version.split('.')
+ return {
+ 'full': version,
+ 'major': int(vers_list[0] if len(vers_list) > 0 else 0),
+ 'minor': int(vers_list[1] if len(vers_list) > 1 else 0),
+ 'patch': int(vers_list[2] if len(vers_list) > 2 else 0)
+ }
+
+def write_meson_info_file(builddata: build.Build, errors: list, build_files_updated: bool = False) -> None:
+ info_dir = builddata.environment.info_dir
+ info_file = get_meson_info_file(info_dir)
+ intro_types = get_meson_introspection_types()
+ intro_info = {}
+
+ for i, v in intro_types.items():
+ if not v.func:
+ continue
+ intro_info[i] = {
+ 'file': f'intro-{i}.json',
+ 'updated': i in updated_introspection_files
+ }
+
+ info_data = {
+ 'meson_version': split_version_string(cdata.version),
+ 'directories': {
+ 'source': builddata.environment.get_source_dir(),
+ 'build': builddata.environment.get_build_dir(),
+ 'info': info_dir,
+ },
+ 'introspection': {
+ 'version': split_version_string(get_meson_introspection_version()),
+ 'information': intro_info,
+ },
+ 'build_files_updated': build_files_updated,
+ }
+
+ if errors:
+ info_data['error'] = True
+ info_data['error_list'] = [x if isinstance(x, str) else str(x) for x in errors]
+ else:
+ info_data['error'] = False
+
+ # Write the data to disc
+ tmp_file = os.path.join(info_dir, 'tmp_dump.json')
+ with open(tmp_file, 'w', encoding='utf-8') as fp:
+ json.dump(info_data, fp)
+ fp.flush()
+ os.replace(tmp_file, info_file)
diff --git a/mesonbuild/mlog.py b/mesonbuild/mlog.py
new file mode 100644
index 0000000..e9c4017
--- /dev/null
+++ b/mesonbuild/mlog.py
@@ -0,0 +1,465 @@
+# Copyright 2013-2014 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 io
+import sys
+import time
+import platform
+import shlex
+import subprocess
+import shutil
+import typing as T
+from contextlib import contextmanager
+from pathlib import Path
+
+if T.TYPE_CHECKING:
+ from ._typing import StringProtocol, SizedStringProtocol
+
+"""This is (mostly) a standalone module used to write logging
+information about Meson runs. Some output goes to screen,
+some to logging dir and some goes to both."""
+
+def is_windows() -> bool:
+ platname = platform.system().lower()
+ return platname == 'windows'
+
+def _windows_ansi() -> bool:
+ # windll only exists on windows, so mypy will get mad
+ from ctypes import windll, byref # type: ignore
+ from ctypes.wintypes import DWORD
+
+ kernel = windll.kernel32
+ stdout = kernel.GetStdHandle(-11)
+ mode = DWORD()
+ if not kernel.GetConsoleMode(stdout, byref(mode)):
+ return False
+ # ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0x4
+ # If the call to enable VT processing fails (returns 0), we fallback to
+ # original behavior
+ return bool(kernel.SetConsoleMode(stdout, mode.value | 0x4) or os.environ.get('ANSICON'))
+
+def colorize_console() -> bool:
+ _colorize_console = getattr(sys.stdout, 'colorize_console', None) # type: bool
+ if _colorize_console is not None:
+ return _colorize_console
+
+ try:
+ if is_windows():
+ _colorize_console = os.isatty(sys.stdout.fileno()) and _windows_ansi()
+ else:
+ _colorize_console = os.isatty(sys.stdout.fileno()) and os.environ.get('TERM', 'dumb') != 'dumb'
+ except Exception:
+ _colorize_console = False
+
+ sys.stdout.colorize_console = _colorize_console # type: ignore[attr-defined]
+ return _colorize_console
+
+def setup_console() -> None:
+ # on Windows, a subprocess might call SetConsoleMode() on the console
+ # connected to stdout and turn off ANSI escape processing. Call this after
+ # running a subprocess to ensure we turn it on again.
+ if is_windows():
+ try:
+ delattr(sys.stdout, 'colorize_console')
+ except AttributeError:
+ pass
+
+log_dir = None # type: T.Optional[str]
+log_file = None # type: T.Optional[T.TextIO]
+log_fname = 'meson-log.txt' # type: str
+log_depth = [] # type: T.List[str]
+log_timestamp_start = None # type: T.Optional[float]
+log_fatal_warnings = False # type: bool
+log_disable_stdout = False # type: bool
+log_errors_only = False # type: bool
+_in_ci = 'CI' in os.environ # type: bool
+_logged_once = set() # type: T.Set[T.Tuple[str, ...]]
+log_warnings_counter = 0 # type: int
+log_pager: T.Optional['subprocess.Popen'] = None
+
+def disable() -> None:
+ global log_disable_stdout # pylint: disable=global-statement
+ log_disable_stdout = True
+
+def enable() -> None:
+ global log_disable_stdout # pylint: disable=global-statement
+ log_disable_stdout = False
+
+def set_quiet() -> None:
+ global log_errors_only # pylint: disable=global-statement
+ log_errors_only = True
+
+def set_verbose() -> None:
+ global log_errors_only # pylint: disable=global-statement
+ log_errors_only = False
+
+def initialize(logdir: str, fatal_warnings: bool = False) -> None:
+ global log_dir, log_file, log_fatal_warnings # pylint: disable=global-statement
+ log_dir = logdir
+ log_file = open(os.path.join(logdir, log_fname), 'w', encoding='utf-8')
+ log_fatal_warnings = fatal_warnings
+
+def set_timestamp_start(start: float) -> None:
+ global log_timestamp_start # pylint: disable=global-statement
+ log_timestamp_start = start
+
+def shutdown() -> T.Optional[str]:
+ global log_file # pylint: disable=global-statement
+ if log_file is not None:
+ path = log_file.name
+ exception_around_goer = log_file
+ log_file = None
+ exception_around_goer.close()
+ return path
+ stop_pager()
+ return None
+
+class AnsiDecorator:
+ plain_code = "\033[0m"
+
+ def __init__(self, text: str, code: str, quoted: bool = False):
+ self.text = text
+ self.code = code
+ self.quoted = quoted
+
+ def get_text(self, with_codes: bool) -> str:
+ text = self.text
+ if with_codes and self.code:
+ text = self.code + self.text + AnsiDecorator.plain_code
+ if self.quoted:
+ text = f'"{text}"'
+ return text
+
+ def __len__(self) -> int:
+ return len(self.text)
+
+ def __str__(self) -> str:
+ return self.get_text(colorize_console())
+
+TV_Loggable = T.Union[str, AnsiDecorator, 'StringProtocol']
+TV_LoggableList = T.List[TV_Loggable]
+
+class AnsiText:
+ def __init__(self, *args: 'SizedStringProtocol'):
+ self.args = args
+
+ def __len__(self) -> int:
+ return sum(len(x) for x in self.args)
+
+ def __str__(self) -> str:
+ return ''.join(str(x) for x in self.args)
+
+
+def bold(text: str, quoted: bool = False) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[1m", quoted=quoted)
+
+def italic(text: str, quoted: bool = False) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[3m", quoted=quoted)
+
+def plain(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "")
+
+def red(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[1;31m")
+
+def green(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[1;32m")
+
+def yellow(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[1;33m")
+
+def blue(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[1;34m")
+
+def cyan(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[1;36m")
+
+def normal_red(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[31m")
+
+def normal_green(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[32m")
+
+def normal_yellow(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[33m")
+
+def normal_blue(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[34m")
+
+def normal_cyan(text: str) -> AnsiDecorator:
+ return AnsiDecorator(text, "\033[36m")
+
+# This really should be AnsiDecorator or anything that implements
+# __str__(), but that requires protocols from typing_extensions
+def process_markup(args: T.Sequence[TV_Loggable], keep: bool) -> T.List[str]:
+ arr = [] # type: T.List[str]
+ if log_timestamp_start is not None:
+ arr = ['[{:.3f}]'.format(time.monotonic() - log_timestamp_start)]
+ for arg in args:
+ if arg is None:
+ continue
+ if isinstance(arg, str):
+ arr.append(arg)
+ elif isinstance(arg, AnsiDecorator):
+ arr.append(arg.get_text(keep))
+ else:
+ arr.append(str(arg))
+ return arr
+
+def force_print(*args: str, nested: bool, **kwargs: T.Any) -> None:
+ if log_disable_stdout:
+ return
+ iostr = io.StringIO()
+ kwargs['file'] = iostr
+ print(*args, **kwargs)
+
+ raw = iostr.getvalue()
+ if log_depth:
+ prepend = log_depth[-1] + '| ' if nested else ''
+ lines = []
+ for l in raw.split('\n'):
+ l = l.strip()
+ lines.append(prepend + l if l else '')
+ raw = '\n'.join(lines)
+
+ # _Something_ is going to get printed.
+ try:
+ output = log_pager.stdin if log_pager else None
+ print(raw, end='', file=output)
+ except UnicodeEncodeError:
+ cleaned = raw.encode('ascii', 'replace').decode('ascii')
+ print(cleaned, end='')
+
+# We really want a heterogeneous dict for this, but that's in typing_extensions
+def debug(*args: TV_Loggable, **kwargs: T.Any) -> None:
+ arr = process_markup(args, False)
+ if log_file is not None:
+ print(*arr, file=log_file, **kwargs)
+ log_file.flush()
+
+def _debug_log_cmd(cmd: str, args: T.List[str]) -> None:
+ if not _in_ci:
+ return
+ args = [f'"{x}"' for x in args] # Quote all args, just in case
+ debug('!meson_ci!/{} {}'.format(cmd, ' '.join(args)))
+
+def cmd_ci_include(file: str) -> None:
+ _debug_log_cmd('ci_include', [file])
+
+
+def log(*args: TV_Loggable, is_error: bool = False,
+ once: bool = False, **kwargs: T.Any) -> None:
+ if once:
+ log_once(*args, is_error=is_error, **kwargs)
+ else:
+ _log(*args, is_error=is_error, **kwargs)
+
+
+def _log(*args: TV_Loggable, is_error: bool = False,
+ **kwargs: T.Any) -> None:
+ nested = kwargs.pop('nested', True)
+ arr = process_markup(args, False)
+ if log_file is not None:
+ print(*arr, file=log_file, **kwargs)
+ log_file.flush()
+ if colorize_console():
+ arr = process_markup(args, True)
+ if not log_errors_only or is_error:
+ force_print(*arr, nested=nested, **kwargs)
+
+def log_once(*args: TV_Loggable, is_error: bool = False,
+ **kwargs: T.Any) -> None:
+ """Log variant that only prints a given message one time per meson invocation.
+
+ This considers ansi decorated values by the values they wrap without
+ regard for the AnsiDecorator itself.
+ """
+ def to_str(x: TV_Loggable) -> str:
+ if isinstance(x, str):
+ return x
+ if isinstance(x, AnsiDecorator):
+ return x.text
+ return str(x)
+ t = tuple(to_str(a) for a in args)
+ if t in _logged_once:
+ return
+ _logged_once.add(t)
+ _log(*args, is_error=is_error, **kwargs)
+
+# This isn't strictly correct. What we really want here is something like:
+# class StringProtocol(typing_extensions.Protocol):
+#
+# def __str__(self) -> str: ...
+#
+# This would more accurately embody what this function can handle, but we
+# don't have that yet, so instead we'll do some casting to work around it
+def get_error_location_string(fname: str, lineno: int) -> str:
+ return f'{fname}:{lineno}:'
+
+def _log_error(severity: str, *rargs: TV_Loggable,
+ once: bool = False, fatal: bool = True, **kwargs: T.Any) -> None:
+ from .mesonlib import MesonException, relpath
+
+ # The typing requirements here are non-obvious. Lists are invariant,
+ # therefore T.List[A] and T.List[T.Union[A, B]] are not able to be joined
+ if severity == 'notice':
+ label = [bold('NOTICE:')] # type: TV_LoggableList
+ elif severity == 'warning':
+ label = [yellow('WARNING:')]
+ elif severity == 'error':
+ label = [red('ERROR:')]
+ elif severity == 'deprecation':
+ label = [red('DEPRECATION:')]
+ else:
+ raise MesonException('Invalid severity ' + severity)
+ # rargs is a tuple, not a list
+ args = label + list(rargs)
+
+ location = kwargs.pop('location', None)
+ if location is not None:
+ location_file = relpath(location.filename, os.getcwd())
+ location_str = get_error_location_string(location_file, location.lineno)
+ # Unions are frankly awful, and we have to T.cast here to get mypy
+ # to understand that the list concatenation is safe
+ location_list = T.cast('TV_LoggableList', [location_str])
+ args = location_list + args
+
+ log(*args, once=once, **kwargs)
+
+ global log_warnings_counter # pylint: disable=global-statement
+ log_warnings_counter += 1
+
+ if log_fatal_warnings and fatal:
+ raise MesonException("Fatal warnings enabled, aborting")
+
+def error(*args: TV_Loggable, **kwargs: T.Any) -> None:
+ return _log_error('error', *args, **kwargs, is_error=True)
+
+def warning(*args: TV_Loggable, **kwargs: T.Any) -> None:
+ return _log_error('warning', *args, **kwargs, is_error=True)
+
+def deprecation(*args: TV_Loggable, **kwargs: T.Any) -> None:
+ return _log_error('deprecation', *args, **kwargs, is_error=True)
+
+def notice(*args: TV_Loggable, **kwargs: T.Any) -> None:
+ return _log_error('notice', *args, **kwargs, is_error=False)
+
+def get_relative_path(target: Path, current: Path) -> Path:
+ """Get the path to target from current"""
+ # Go up "current" until we find a common ancestor to target
+ acc = ['.']
+ for part in [current, *current.parents]:
+ try:
+ path = target.relative_to(part)
+ return Path(*acc, path)
+ except ValueError:
+ pass
+ acc += ['..']
+
+ # we failed, should not get here
+ return target
+
+def exception(e: Exception, prefix: T.Optional[AnsiDecorator] = None) -> None:
+ if prefix is None:
+ prefix = red('ERROR:')
+ log()
+ args = [] # type: T.List[T.Union[AnsiDecorator, str]]
+ if all(getattr(e, a, None) is not None for a in ['file', 'lineno', 'colno']):
+ # Mypy doesn't follow hasattr, and it's pretty easy to visually inspect
+ # that this is correct, so we'll just ignore it.
+ path = get_relative_path(Path(e.file), Path(os.getcwd())) # type: ignore
+ args.append(f'{path}:{e.lineno}:{e.colno}:') # type: ignore
+ if prefix:
+ args.append(prefix)
+ args.append(str(e))
+
+ restore = log_disable_stdout
+ if restore:
+ enable()
+ log(*args, is_error=True)
+ if restore:
+ disable()
+
+# Format a list for logging purposes as a string. It separates
+# all but the last item with commas, and the last with 'and'.
+def format_list(input_list: T.List[str]) -> str:
+ l = len(input_list)
+ if l > 2:
+ return ' and '.join([', '.join(input_list[:-1]), input_list[-1]])
+ elif l == 2:
+ return ' and '.join(input_list)
+ elif l == 1:
+ return input_list[0]
+ else:
+ return ''
+
+@contextmanager
+def nested(name: str = '') -> T.Generator[None, None, None]:
+ log_depth.append(name)
+ try:
+ yield
+ finally:
+ log_depth.pop()
+
+def start_pager() -> None:
+ if not colorize_console():
+ return
+ pager_cmd = []
+ if 'PAGER' in os.environ:
+ pager_cmd = shlex.split(os.environ['PAGER'])
+ else:
+ less = shutil.which('less')
+ if not less and is_windows():
+ git = shutil.which('git')
+ if git:
+ path = Path(git).parents[1] / 'usr' / 'bin'
+ less = shutil.which('less', path=str(path))
+ if less:
+ pager_cmd = [less]
+ if not pager_cmd:
+ return
+ global log_pager # pylint: disable=global-statement
+ assert log_pager is None
+ try:
+ # Set 'LESS' environment variable, rather than arguments in
+ # pager_cmd, to also support the case where the user has 'PAGER'
+ # set to 'less'. Arguments set are:
+ # "R" : support color
+ # "X" : do not clear the screen when leaving the pager
+ # "F" : skip the pager if content fits into the screen
+ env = os.environ.copy()
+ if 'LESS' not in env:
+ env['LESS'] = 'RXF'
+ # Set "-c" for lv to support color
+ if 'LV' not in env:
+ env['LV'] = '-c'
+ log_pager = subprocess.Popen(pager_cmd, stdin=subprocess.PIPE,
+ text=True, encoding='utf-8', env=env)
+ except Exception as e:
+ # Ignore errors, unless it is a user defined pager.
+ if 'PAGER' in os.environ:
+ from .mesonlib import MesonException
+ raise MesonException(f'Failed to start pager: {str(e)}')
+
+def stop_pager() -> None:
+ global log_pager # pylint: disable=global-statement
+ if log_pager:
+ try:
+ log_pager.stdin.flush()
+ log_pager.stdin.close()
+ except BrokenPipeError:
+ pass
+ log_pager.wait()
+ log_pager = None
diff --git a/mesonbuild/modules/__init__.py b/mesonbuild/modules/__init__.py
new file mode 100644
index 0000000..b63a5da
--- /dev/null
+++ b/mesonbuild/modules/__init__.py
@@ -0,0 +1,262 @@
+# Copyright 2019 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.
+
+# This file contains the base representation for import('modname')
+
+from __future__ import annotations
+import dataclasses
+import typing as T
+
+from .. import mesonlib
+from ..build import IncludeDirs
+from ..interpreterbase.decorators import noKwargs, noPosargs
+from ..mesonlib import relpath, HoldableObject, MachineChoice
+from ..programs import ExternalProgram
+
+if T.TYPE_CHECKING:
+ from .. import build
+ from ..interpreter import Interpreter
+ from ..interpreter.interpreterobjects import MachineHolder
+ from ..interpreterbase import TYPE_var, TYPE_kwargs
+ from ..programs import OverrideProgram
+ from ..wrap import WrapMode
+ from ..build import EnvironmentVariables, Executable
+ from ..dependencies import Dependency
+
+class ModuleState:
+ """Object passed to all module methods.
+
+ This is a WIP API provided to modules, it should be extended to have everything
+ needed so modules does not touch any other part of Meson internal APIs.
+ """
+
+ def __init__(self, interpreter: 'Interpreter') -> None:
+ # Keep it private, it should be accessed only through methods.
+ self._interpreter = interpreter
+
+ self.source_root = interpreter.environment.get_source_dir()
+ self.build_to_src = relpath(interpreter.environment.get_source_dir(),
+ interpreter.environment.get_build_dir())
+ self.subproject = interpreter.subproject
+ self.subdir = interpreter.subdir
+ self.root_subdir = interpreter.root_subdir
+ self.current_lineno = interpreter.current_lineno
+ self.environment = interpreter.environment
+ self.project_name = interpreter.build.project_name
+ self.project_version = interpreter.build.dep_manifest[interpreter.active_projectname].version
+ # The backend object is under-used right now, but we will need it:
+ # https://github.com/mesonbuild/meson/issues/1419
+ self.backend = interpreter.backend
+ self.targets = interpreter.build.targets
+ self.data = interpreter.build.data
+ self.headers = interpreter.build.get_headers()
+ self.man = interpreter.build.get_man()
+ self.global_args = interpreter.build.global_args.host
+ self.project_args = interpreter.build.projects_args.host.get(interpreter.subproject, {})
+ self.build_machine = T.cast('MachineHolder', interpreter.builtin['build_machine']).held_object
+ self.host_machine = T.cast('MachineHolder', interpreter.builtin['host_machine']).held_object
+ self.target_machine = T.cast('MachineHolder', interpreter.builtin['target_machine']).held_object
+ self.current_node = interpreter.current_node
+
+ def get_include_args(self, include_dirs: T.Iterable[T.Union[str, build.IncludeDirs]], prefix: str = '-I') -> T.List[str]:
+ if not include_dirs:
+ return []
+
+ srcdir = self.environment.get_source_dir()
+ builddir = self.environment.get_build_dir()
+
+ dirs_str: T.List[str] = []
+ for dirs in include_dirs:
+ if isinstance(dirs, str):
+ dirs_str += [f'{prefix}{dirs}']
+ else:
+ dirs_str.extend([f'{prefix}{i}' for i in dirs.to_string_list(srcdir, builddir)])
+ dirs_str.extend([f'{prefix}{i}' for i in dirs.get_extra_build_dirs()])
+
+ return dirs_str
+
+ def find_program(self, prog: T.Union[str, T.List[str]], required: bool = True,
+ version_func: T.Optional[T.Callable[['ExternalProgram'], str]] = None,
+ wanted: T.Optional[str] = None, silent: bool = False,
+ for_machine: MachineChoice = MachineChoice.HOST) -> 'ExternalProgram':
+ return self._interpreter.find_program_impl(prog, required=required, version_func=version_func,
+ wanted=wanted, silent=silent, for_machine=for_machine)
+
+ def find_tool(self, name: str, depname: str, varname: str, required: bool = True,
+ wanted: T.Optional[str] = None) -> T.Union['Executable', ExternalProgram, 'OverrideProgram']:
+ # Look in overrides in case it's built as subproject
+ progobj = self._interpreter.program_from_overrides([name], [])
+ if progobj is not None:
+ return progobj
+
+ # Look in machine file
+ prog_list = self.environment.lookup_binary_entry(MachineChoice.HOST, name)
+ if prog_list is not None:
+ return ExternalProgram.from_entry(name, prog_list)
+
+ # Check if pkgconfig has a variable
+ dep = self.dependency(depname, native=True, required=False, wanted=wanted)
+ if dep.found() and dep.type_name == 'pkgconfig':
+ value = dep.get_variable(pkgconfig=varname)
+ if value:
+ return ExternalProgram(name, [value])
+
+ # Normal program lookup
+ return self.find_program(name, required=required, wanted=wanted)
+
+ def dependency(self, depname: str, native: bool = False, required: bool = True,
+ wanted: T.Optional[str] = None) -> 'Dependency':
+ kwargs = {'native': native, 'required': required}
+ if wanted:
+ kwargs['version'] = wanted
+ # FIXME: Even if we fix the function, mypy still can't figure out what's
+ # going on here. And we really dont want to call interpreter
+ # implementations of meson functions anyway.
+ return self._interpreter.func_dependency(self.current_node, [depname], kwargs) # type: ignore
+
+ def test(self, args: T.Tuple[str, T.Union[build.Executable, build.Jar, 'ExternalProgram', mesonlib.File]],
+ workdir: T.Optional[str] = None,
+ env: T.Union[T.List[str], T.Dict[str, str], str] = None,
+ depends: T.List[T.Union[build.CustomTarget, build.BuildTarget]] = None) -> None:
+ kwargs = {'workdir': workdir,
+ 'env': env,
+ 'depends': depends,
+ }
+ # typed_* takes a list, and gives a tuple to func_test. Violating that constraint
+ # makes the universe (or at least use of this function) implode
+ real_args = list(args)
+ # TODO: Use interpreter internal API, but we need to go through @typed_kwargs
+ self._interpreter.func_test(self.current_node, real_args, kwargs)
+
+ def get_option(self, name: str, subproject: str = '',
+ machine: MachineChoice = MachineChoice.HOST,
+ lang: T.Optional[str] = None,
+ module: T.Optional[str] = None) -> T.Union[str, int, bool, 'WrapMode']:
+ return self.environment.coredata.get_option(mesonlib.OptionKey(name, subproject, machine, lang, module))
+
+ def is_user_defined_option(self, name: str, subproject: str = '',
+ machine: MachineChoice = MachineChoice.HOST,
+ lang: T.Optional[str] = None,
+ module: T.Optional[str] = None) -> bool:
+ key = mesonlib.OptionKey(name, subproject, machine, lang, module)
+ return key in self._interpreter.user_defined_options.cmd_line_options
+
+ def process_include_dirs(self, dirs: T.Iterable[T.Union[str, IncludeDirs]]) -> T.Iterable[IncludeDirs]:
+ """Convert raw include directory arguments to only IncludeDirs
+
+ :param dirs: An iterable of strings and IncludeDirs
+ :return: None
+ :yield: IncludeDirs objects
+ """
+ for d in dirs:
+ if isinstance(d, IncludeDirs):
+ yield d
+ else:
+ yield self._interpreter.build_incdir_object([d])
+
+
+class ModuleObject(HoldableObject):
+ """Base class for all objects returned by modules
+ """
+ def __init__(self) -> None:
+ self.methods: T.Dict[
+ str,
+ T.Callable[[ModuleState, T.List['TYPE_var'], 'TYPE_kwargs'], T.Union[ModuleReturnValue, 'TYPE_var']]
+ ] = {}
+
+
+class MutableModuleObject(ModuleObject):
+ pass
+
+
+@dataclasses.dataclass
+class ModuleInfo:
+
+ """Metadata about a Module."""
+
+ name: str
+ added: T.Optional[str] = None
+ deprecated: T.Optional[str] = None
+ unstable: bool = False
+ stabilized: T.Optional[str] = None
+
+
+class NewExtensionModule(ModuleObject):
+
+ """Class for modern modules
+
+ provides the found method.
+ """
+
+ INFO: ModuleInfo
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.methods.update({
+ 'found': self.found_method,
+ })
+
+ @noPosargs
+ @noKwargs
+ def found_method(self, state: 'ModuleState', args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
+ return self.found()
+
+ @staticmethod
+ def found() -> bool:
+ return True
+
+ def get_devenv(self) -> T.Optional['EnvironmentVariables']:
+ return None
+
+# FIXME: Port all modules to stop using self.interpreter and use API on
+# ModuleState instead. Modules should stop using this class and instead use
+# ModuleObject base class.
+class ExtensionModule(NewExtensionModule):
+ def __init__(self, interpreter: 'Interpreter') -> None:
+ super().__init__()
+ self.interpreter = interpreter
+
+class NotFoundExtensionModule(NewExtensionModule):
+
+ """Class for modern modules
+
+ provides the found method.
+ """
+
+ def __init__(self, name: str) -> None:
+ super().__init__()
+ self.INFO = ModuleInfo(name)
+
+ @staticmethod
+ def found() -> bool:
+ return False
+
+
+def is_module_library(fname):
+ '''
+ Check if the file is a library-like file generated by a module-specific
+ target, such as GirTarget or TypelibTarget
+ '''
+ if hasattr(fname, 'fname'):
+ fname = fname.fname
+ suffix = fname.split('.')[-1]
+ return suffix in {'gir', 'typelib'}
+
+
+class ModuleReturnValue:
+ def __init__(self, return_value: T.Optional['TYPE_var'],
+ new_objects: T.Sequence[T.Union['TYPE_var', 'build.ExecutableSerialisation']]) -> None:
+ self.return_value = return_value
+ assert isinstance(new_objects, list)
+ self.new_objects: T.List[T.Union['TYPE_var', 'build.ExecutableSerialisation']] = new_objects
diff --git a/mesonbuild/modules/cmake.py b/mesonbuild/modules/cmake.py
new file mode 100644
index 0000000..ee40b44
--- /dev/null
+++ b/mesonbuild/modules/cmake.py
@@ -0,0 +1,453 @@
+# Copyright 2018 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.
+
+from __future__ import annotations
+import re
+import os, os.path, pathlib
+import shutil
+import typing as T
+
+from . import ExtensionModule, ModuleReturnValue, ModuleObject, ModuleInfo
+
+from .. import build, mesonlib, mlog, dependencies
+from ..cmake import TargetOptions, cmake_defines_to_args
+from ..interpreter import SubprojectHolder
+from ..interpreter.type_checking import REQUIRED_KW, INSTALL_DIR_KW, NoneType, in_set_validator
+from ..interpreterbase import (
+ FeatureNew,
+ FeatureNewKwargs,
+
+ stringArgs,
+ permittedKwargs,
+ noPosargs,
+ noKwargs,
+
+ InvalidArguments,
+ InterpreterException,
+
+ typed_pos_args,
+ typed_kwargs,
+ KwargInfo,
+ ContainerTypeInfo,
+)
+
+if T.TYPE_CHECKING:
+ from typing_extensions import TypedDict
+
+ from . import ModuleState
+ from ..cmake import SingleTargetOptions
+ from ..interpreter import kwargs
+
+ class WriteBasicPackageVersionFile(TypedDict):
+
+ arch_independent: bool
+ compatibility: str
+ install_dir: T.Optional[str]
+ name: str
+ version: str
+
+ class ConfigurePackageConfigFile(TypedDict):
+
+ configuration: T.Union[build.ConfigurationData, dict]
+ input: T.Union[str, mesonlib.File]
+ install_dir: T.Optional[str]
+ name: str
+
+ class Subproject(kwargs.ExtractRequired):
+
+ options: T.Optional[CMakeSubprojectOptions]
+ cmake_options: T.List[str]
+
+
+COMPATIBILITIES = ['AnyNewerVersion', 'SameMajorVersion', 'SameMinorVersion', 'ExactVersion']
+
+# Taken from https://github.com/Kitware/CMake/blob/master/Modules/CMakePackageConfigHelpers.cmake
+PACKAGE_INIT_BASE = '''
+####### Expanded from \\@PACKAGE_INIT\\@ by configure_package_config_file() #######
+####### Any changes to this file will be overwritten by the next CMake run ####
+####### The input file was @inputFileName@ ########
+
+get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/@PACKAGE_RELATIVE_PATH@" ABSOLUTE)
+'''
+PACKAGE_INIT_EXT = '''
+# Use original install prefix when loaded through a "/usr move"
+# cross-prefix symbolic link such as /lib -> /usr/lib.
+get_filename_component(_realCurr "${CMAKE_CURRENT_LIST_DIR}" REALPATH)
+get_filename_component(_realOrig "@absInstallDir@" REALPATH)
+if(_realCurr STREQUAL _realOrig)
+ set(PACKAGE_PREFIX_DIR "@installPrefix@")
+endif()
+unset(_realOrig)
+unset(_realCurr)
+'''
+PACKAGE_INIT_SET_AND_CHECK = '''
+macro(set_and_check _var _file)
+ set(${_var} "${_file}")
+ if(NOT EXISTS "${_file}")
+ message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !")
+ endif()
+endmacro()
+
+####################################################################################
+'''
+
+class CMakeSubproject(ModuleObject):
+ def __init__(self, subp: SubprojectHolder):
+ assert isinstance(subp, SubprojectHolder)
+ assert subp.cm_interpreter is not None
+ super().__init__()
+ self.subp = subp
+ self.cm_interpreter = subp.cm_interpreter
+ self.methods.update({'get_variable': self.get_variable,
+ 'dependency': self.dependency,
+ 'include_directories': self.include_directories,
+ 'target': self.target,
+ 'target_type': self.target_type,
+ 'target_list': self.target_list,
+ 'found': self.found_method,
+ })
+
+ def _args_to_info(self, args):
+ if len(args) != 1:
+ raise InterpreterException('Exactly one argument is required.')
+
+ tgt = args[0]
+ res = self.cm_interpreter.target_info(tgt)
+ if res is None:
+ raise InterpreterException(f'The CMake target {tgt} does not exist\n' +
+ ' Use the following command in your meson.build to list all available targets:\n\n' +
+ ' message(\'CMake targets:\\n - \' + \'\\n - \'.join(<cmake_subproject>.target_list()))')
+
+ # Make sure that all keys are present (if not this is a bug)
+ assert all(x in res for x in ['inc', 'src', 'dep', 'tgt', 'func'])
+ return res
+
+ @noKwargs
+ @stringArgs
+ def get_variable(self, state, args, kwargs):
+ return self.subp.get_variable_method(args, kwargs)
+
+ @FeatureNewKwargs('dependency', '0.56.0', ['include_type'])
+ @permittedKwargs({'include_type'})
+ @stringArgs
+ def dependency(self, state, args, kwargs):
+ info = self._args_to_info(args)
+ if info['func'] == 'executable':
+ raise InvalidArguments(f'{args[0]} is an executable and does not support the dependency() method. Use target() instead.')
+ orig = self.get_variable(state, [info['dep']], {})
+ assert isinstance(orig, dependencies.Dependency)
+ actual = orig.include_type
+ if 'include_type' in kwargs and kwargs['include_type'] != actual:
+ mlog.debug('Current include type is {}. Converting to requested {}'.format(actual, kwargs['include_type']))
+ return orig.generate_system_dependency(kwargs['include_type'])
+ return orig
+
+ @noKwargs
+ @stringArgs
+ def include_directories(self, state, args, kwargs):
+ info = self._args_to_info(args)
+ return self.get_variable(state, [info['inc']], kwargs)
+
+ @noKwargs
+ @stringArgs
+ def target(self, state, args, kwargs):
+ info = self._args_to_info(args)
+ return self.get_variable(state, [info['tgt']], kwargs)
+
+ @noKwargs
+ @stringArgs
+ def target_type(self, state, args, kwargs):
+ info = self._args_to_info(args)
+ return info['func']
+
+ @noPosargs
+ @noKwargs
+ def target_list(self, state, args, kwargs):
+ return self.cm_interpreter.target_list()
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('CMakeSubproject.found()', '0.53.2')
+ def found_method(self, state, args, kwargs):
+ return self.subp is not None
+
+
+class CMakeSubprojectOptions(ModuleObject):
+ def __init__(self) -> None:
+ super().__init__()
+ self.cmake_options = [] # type: T.List[str]
+ self.target_options = TargetOptions()
+
+ self.methods.update(
+ {
+ 'add_cmake_defines': self.add_cmake_defines,
+ 'set_override_option': self.set_override_option,
+ 'set_install': self.set_install,
+ 'append_compile_args': self.append_compile_args,
+ 'append_link_args': self.append_link_args,
+ 'clear': self.clear,
+ }
+ )
+
+ def _get_opts(self, kwargs: dict) -> SingleTargetOptions:
+ if 'target' in kwargs:
+ return self.target_options[kwargs['target']]
+ return self.target_options.global_options
+
+ @noKwargs
+ def add_cmake_defines(self, state, args, kwargs) -> None:
+ self.cmake_options += cmake_defines_to_args(args)
+
+ @stringArgs
+ @permittedKwargs({'target'})
+ def set_override_option(self, state, args, kwargs) -> None:
+ if len(args) != 2:
+ raise InvalidArguments('set_override_option takes exactly 2 positional arguments')
+ self._get_opts(kwargs).set_opt(args[0], args[1])
+
+ @permittedKwargs({'target'})
+ def set_install(self, state, args, kwargs) -> None:
+ if len(args) != 1 or not isinstance(args[0], bool):
+ raise InvalidArguments('set_install takes exactly 1 boolean argument')
+ self._get_opts(kwargs).set_install(args[0])
+
+ @stringArgs
+ @permittedKwargs({'target'})
+ def append_compile_args(self, state, args, kwargs) -> None:
+ if len(args) < 2:
+ raise InvalidArguments('append_compile_args takes at least 2 positional arguments')
+ self._get_opts(kwargs).append_args(args[0], args[1:])
+
+ @stringArgs
+ @permittedKwargs({'target'})
+ def append_link_args(self, state, args, kwargs) -> None:
+ if not args:
+ raise InvalidArguments('append_link_args takes at least 1 positional argument')
+ self._get_opts(kwargs).append_link_args(args)
+
+ @noPosargs
+ @noKwargs
+ def clear(self, state, args, kwargs) -> None:
+ self.cmake_options.clear()
+ self.target_options = TargetOptions()
+
+
+class CmakeModule(ExtensionModule):
+ cmake_detected = False
+ cmake_root = None
+
+ INFO = ModuleInfo('cmake', '0.50.0')
+
+ def __init__(self, interpreter):
+ super().__init__(interpreter)
+ self.methods.update({
+ 'write_basic_package_version_file': self.write_basic_package_version_file,
+ 'configure_package_config_file': self.configure_package_config_file,
+ 'subproject': self.subproject,
+ 'subproject_options': self.subproject_options,
+ })
+
+ def detect_voidp_size(self, env):
+ compilers = env.coredata.compilers.host
+ compiler = compilers.get('c', None)
+ if not compiler:
+ compiler = compilers.get('cpp', None)
+
+ if not compiler:
+ raise mesonlib.MesonException('Requires a C or C++ compiler to compute sizeof(void *).')
+
+ return compiler.sizeof('void *', '', env)
+
+ def detect_cmake(self, state):
+ if self.cmake_detected:
+ return True
+
+ cmakebin = state.find_program('cmake', silent=False)
+ if not cmakebin.found():
+ return False
+
+ p, stdout, stderr = mesonlib.Popen_safe(cmakebin.get_command() + ['--system-information', '-G', 'Ninja'])[0:3]
+ if p.returncode != 0:
+ mlog.log(f'error retrieving cmake information: returnCode={p.returncode} stdout={stdout} stderr={stderr}')
+ return False
+
+ match = re.search('\nCMAKE_ROOT \\"([^"]+)"\n', stdout.strip())
+ if not match:
+ mlog.log('unable to determine cmake root')
+ return False
+
+ cmakePath = pathlib.PurePath(match.group(1))
+ self.cmake_root = os.path.join(*cmakePath.parts)
+ self.cmake_detected = True
+ return True
+
+ @noPosargs
+ @typed_kwargs(
+ 'cmake.write_basic_package_version_file',
+ KwargInfo('arch_independent', bool, default=False, since='0.62.0'),
+ KwargInfo('compatibility', str, default='AnyNewerVersion', validator=in_set_validator(set(COMPATIBILITIES))),
+ KwargInfo('name', str, required=True),
+ KwargInfo('version', str, required=True),
+ INSTALL_DIR_KW,
+ )
+ def write_basic_package_version_file(self, state, args, kwargs: 'WriteBasicPackageVersionFile'):
+ arch_independent = kwargs['arch_independent']
+ compatibility = kwargs['compatibility']
+ name = kwargs['name']
+ version = kwargs['version']
+
+ if not self.detect_cmake(state):
+ raise mesonlib.MesonException('Unable to find cmake')
+
+ pkgroot = pkgroot_name = kwargs['install_dir']
+ if pkgroot is None:
+ pkgroot = os.path.join(state.environment.coredata.get_option(mesonlib.OptionKey('libdir')), 'cmake', name)
+ pkgroot_name = os.path.join('{libdir}', 'cmake', name)
+
+ template_file = os.path.join(self.cmake_root, 'Modules', f'BasicConfigVersion-{compatibility}.cmake.in')
+ if not os.path.exists(template_file):
+ raise mesonlib.MesonException(f'your cmake installation doesn\'t support the {compatibility} compatibility')
+
+ version_file = os.path.join(state.environment.scratch_dir, f'{name}ConfigVersion.cmake')
+
+ conf = {
+ 'CVF_VERSION': (version, ''),
+ 'CMAKE_SIZEOF_VOID_P': (str(self.detect_voidp_size(state.environment)), ''),
+ 'CVF_ARCH_INDEPENDENT': (arch_independent, ''),
+ }
+ mesonlib.do_conf_file(template_file, version_file, conf, 'meson')
+
+ res = build.Data([mesonlib.File(True, state.environment.get_scratch_dir(), version_file)], pkgroot, pkgroot_name, None, state.subproject)
+ return ModuleReturnValue(res, [res])
+
+ def create_package_file(self, infile, outfile, PACKAGE_RELATIVE_PATH, extra, confdata):
+ package_init = PACKAGE_INIT_BASE.replace('@PACKAGE_RELATIVE_PATH@', PACKAGE_RELATIVE_PATH)
+ package_init = package_init.replace('@inputFileName@', os.path.basename(infile))
+ package_init += extra
+ package_init += PACKAGE_INIT_SET_AND_CHECK
+
+ try:
+ with open(infile, encoding='utf-8') as fin:
+ data = fin.readlines()
+ except Exception as e:
+ raise mesonlib.MesonException(f'Could not read input file {infile}: {e!s}')
+
+ result = []
+ regex = mesonlib.get_variable_regex('cmake@')
+ for line in data:
+ line = line.replace('@PACKAGE_INIT@', package_init)
+ line, _missing = mesonlib.do_replacement(regex, line, 'cmake@', confdata)
+
+ result.append(line)
+
+ outfile_tmp = outfile + "~"
+ with open(outfile_tmp, "w", encoding='utf-8') as fout:
+ fout.writelines(result)
+
+ shutil.copymode(infile, outfile_tmp)
+ mesonlib.replace_if_different(outfile, outfile_tmp)
+
+ @noPosargs
+ @typed_kwargs(
+ 'cmake.configure_package_config_file',
+ KwargInfo('configuration', (build.ConfigurationData, dict), required=True),
+ KwargInfo('input',
+ (str, mesonlib.File, ContainerTypeInfo(list, mesonlib.File)), required=True,
+ validator=lambda x: 'requires exactly one file' if isinstance(x, list) and len(x) != 1 else None,
+ convertor=lambda x: x[0] if isinstance(x, list) else x),
+ KwargInfo('name', str, required=True),
+ INSTALL_DIR_KW,
+ )
+ def configure_package_config_file(self, state, args, kwargs: 'ConfigurePackageConfigFile'):
+ inputfile = kwargs['input']
+ if isinstance(inputfile, str):
+ inputfile = mesonlib.File.from_source_file(state.environment.source_dir, state.subdir, inputfile)
+
+ ifile_abs = inputfile.absolute_path(state.environment.source_dir, state.environment.build_dir)
+
+ name = kwargs['name']
+
+ (ofile_path, ofile_fname) = os.path.split(os.path.join(state.subdir, f'{name}Config.cmake'))
+ ofile_abs = os.path.join(state.environment.build_dir, ofile_path, ofile_fname)
+
+ install_dir = kwargs['install_dir']
+ if install_dir is None:
+ install_dir = os.path.join(state.environment.coredata.get_option(mesonlib.OptionKey('libdir')), 'cmake', name)
+
+ conf = kwargs['configuration']
+ if isinstance(conf, dict):
+ FeatureNew.single_use('cmake.configure_package_config_file dict as configuration', '0.62.0', state.subproject, location=state.current_node)
+ conf = build.ConfigurationData(conf)
+
+ prefix = state.environment.coredata.get_option(mesonlib.OptionKey('prefix'))
+ abs_install_dir = install_dir
+ if not os.path.isabs(abs_install_dir):
+ abs_install_dir = os.path.join(prefix, install_dir)
+
+ PACKAGE_RELATIVE_PATH = os.path.relpath(prefix, abs_install_dir)
+ extra = ''
+ if re.match('^(/usr)?/lib(64)?/.+', abs_install_dir):
+ extra = PACKAGE_INIT_EXT.replace('@absInstallDir@', abs_install_dir)
+ extra = extra.replace('@installPrefix@', prefix)
+
+ self.create_package_file(ifile_abs, ofile_abs, PACKAGE_RELATIVE_PATH, extra, conf)
+ conf.used = True
+
+ conffile = os.path.normpath(inputfile.relative_name())
+ self.interpreter.build_def_files.add(conffile)
+
+ res = build.Data([mesonlib.File(True, ofile_path, ofile_fname)], install_dir, install_dir, None, state.subproject)
+ self.interpreter.build.data.append(res)
+
+ return res
+
+ @FeatureNew('subproject', '0.51.0')
+ @typed_pos_args('cmake.subproject', str)
+ @typed_kwargs(
+ 'cmake.subproject',
+ REQUIRED_KW,
+ KwargInfo('options', (CMakeSubprojectOptions, NoneType), since='0.55.0'),
+ KwargInfo(
+ 'cmake_options',
+ ContainerTypeInfo(list, str),
+ default=[],
+ listify=True,
+ deprecated='0.55.0',
+ deprecated_message='Use options instead',
+ ),
+ )
+ def subproject(self, state: ModuleState, args: T.Tuple[str], kwargs_: Subproject) -> T.Union[SubprojectHolder, CMakeSubproject]:
+ if kwargs_['cmake_options'] and kwargs_['options'] is not None:
+ raise InterpreterException('"options" cannot be used together with "cmake_options"')
+ dirname = args[0]
+ kw: kwargs.DoSubproject = {
+ 'required': kwargs_['required'],
+ 'options': kwargs_['options'],
+ 'cmake_options': kwargs_['cmake_options'],
+ 'default_options': [],
+ 'version': [],
+ }
+ subp = self.interpreter.do_subproject(dirname, 'cmake', kw)
+ if not subp.found():
+ return subp
+ return CMakeSubproject(subp)
+
+ @FeatureNew('subproject_options', '0.55.0')
+ @noKwargs
+ @noPosargs
+ def subproject_options(self, state, args, kwargs) -> CMakeSubprojectOptions:
+ return CMakeSubprojectOptions()
+
+def initialize(*args, **kwargs):
+ return CmakeModule(*args, **kwargs)
diff --git a/mesonbuild/modules/cuda.py b/mesonbuild/modules/cuda.py
new file mode 100644
index 0000000..72ca306
--- /dev/null
+++ b/mesonbuild/modules/cuda.py
@@ -0,0 +1,383 @@
+# Copyright 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.
+from __future__ import annotations
+
+import typing as T
+import re
+
+from ..mesonlib import version_compare
+from ..compilers.cuda import CudaCompiler
+
+from . import NewExtensionModule, ModuleInfo
+
+from ..interpreterbase import (
+ flatten, permittedKwargs, noKwargs,
+ InvalidArguments
+)
+
+if T.TYPE_CHECKING:
+ from . import ModuleState
+ from ..compilers import Compiler
+
+class CudaModule(NewExtensionModule):
+
+ INFO = ModuleInfo('CUDA', '0.50.0', unstable=True)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__()
+ self.methods.update({
+ "min_driver_version": self.min_driver_version,
+ "nvcc_arch_flags": self.nvcc_arch_flags,
+ "nvcc_arch_readable": self.nvcc_arch_readable,
+ })
+
+ @noKwargs
+ def min_driver_version(self, state: 'ModuleState',
+ args: T.Tuple[str],
+ kwargs: T.Dict[str, T.Any]) -> str:
+ argerror = InvalidArguments('min_driver_version must have exactly one positional argument: ' +
+ 'a CUDA Toolkit version string. Beware that, since CUDA 11.0, ' +
+ 'the CUDA Toolkit\'s components (including NVCC) are versioned ' +
+ 'independently from each other (and the CUDA Toolkit as a whole).')
+
+ if len(args) != 1 or not isinstance(args[0], str):
+ raise argerror
+
+ cuda_version = args[0]
+ driver_version_table = [
+ {'cuda_version': '>=12.0.0', 'windows': '527.41', 'linux': '525.60.13'},
+ {'cuda_version': '>=11.8.0', 'windows': '522.06', 'linux': '520.61.05'},
+ {'cuda_version': '>=11.7.1', 'windows': '516.31', 'linux': '515.48.07'},
+ {'cuda_version': '>=11.7.0', 'windows': '516.01', 'linux': '515.43.04'},
+ {'cuda_version': '>=11.6.1', 'windows': '511.65', 'linux': '510.47.03'},
+ {'cuda_version': '>=11.6.0', 'windows': '511.23', 'linux': '510.39.01'},
+ {'cuda_version': '>=11.5.1', 'windows': '496.13', 'linux': '495.29.05'},
+ {'cuda_version': '>=11.5.0', 'windows': '496.04', 'linux': '495.29.05'},
+ {'cuda_version': '>=11.4.3', 'windows': '472.50', 'linux': '470.82.01'},
+ {'cuda_version': '>=11.4.1', 'windows': '471.41', 'linux': '470.57.02'},
+ {'cuda_version': '>=11.4.0', 'windows': '471.11', 'linux': '470.42.01'},
+ {'cuda_version': '>=11.3.0', 'windows': '465.89', 'linux': '465.19.01'},
+ {'cuda_version': '>=11.2.2', 'windows': '461.33', 'linux': '460.32.03'},
+ {'cuda_version': '>=11.2.1', 'windows': '461.09', 'linux': '460.32.03'},
+ {'cuda_version': '>=11.2.0', 'windows': '460.82', 'linux': '460.27.03'},
+ {'cuda_version': '>=11.1.1', 'windows': '456.81', 'linux': '455.32'},
+ {'cuda_version': '>=11.1.0', 'windows': '456.38', 'linux': '455.23'},
+ {'cuda_version': '>=11.0.3', 'windows': '451.82', 'linux': '450.51.06'},
+ {'cuda_version': '>=11.0.2', 'windows': '451.48', 'linux': '450.51.05'},
+ {'cuda_version': '>=11.0.1', 'windows': '451.22', 'linux': '450.36.06'},
+ {'cuda_version': '>=10.2.89', 'windows': '441.22', 'linux': '440.33'},
+ {'cuda_version': '>=10.1.105', 'windows': '418.96', 'linux': '418.39'},
+ {'cuda_version': '>=10.0.130', 'windows': '411.31', 'linux': '410.48'},
+ {'cuda_version': '>=9.2.148', 'windows': '398.26', 'linux': '396.37'},
+ {'cuda_version': '>=9.2.88', 'windows': '397.44', 'linux': '396.26'},
+ {'cuda_version': '>=9.1.85', 'windows': '391.29', 'linux': '390.46'},
+ {'cuda_version': '>=9.0.76', 'windows': '385.54', 'linux': '384.81'},
+ {'cuda_version': '>=8.0.61', 'windows': '376.51', 'linux': '375.26'},
+ {'cuda_version': '>=8.0.44', 'windows': '369.30', 'linux': '367.48'},
+ {'cuda_version': '>=7.5.16', 'windows': '353.66', 'linux': '352.31'},
+ {'cuda_version': '>=7.0.28', 'windows': '347.62', 'linux': '346.46'},
+ ]
+
+ driver_version = 'unknown'
+ for d in driver_version_table:
+ if version_compare(cuda_version, d['cuda_version']):
+ driver_version = d.get(state.host_machine.system, d['linux'])
+ break
+
+ return driver_version
+
+ @permittedKwargs(['detected'])
+ def nvcc_arch_flags(self, state: 'ModuleState',
+ args: T.Tuple[T.Union[Compiler, CudaCompiler, str]],
+ kwargs: T.Dict[str, T.Any]) -> T.List[str]:
+ nvcc_arch_args = self._validate_nvcc_arch_args(args, kwargs)
+ ret = self._nvcc_arch_flags(*nvcc_arch_args)[0]
+ return ret
+
+ @permittedKwargs(['detected'])
+ def nvcc_arch_readable(self, state: 'ModuleState',
+ args: T.Tuple[T.Union[Compiler, CudaCompiler, str]],
+ kwargs: T.Dict[str, T.Any]) -> T.List[str]:
+ nvcc_arch_args = self._validate_nvcc_arch_args(args, kwargs)
+ ret = self._nvcc_arch_flags(*nvcc_arch_args)[1]
+ return ret
+
+ @staticmethod
+ def _break_arch_string(s):
+ s = re.sub('[ \t\r\n,;]+', ';', s)
+ s = s.strip(';').split(';')
+ return s
+
+ @staticmethod
+ def _detected_cc_from_compiler(c):
+ if isinstance(c, CudaCompiler):
+ return c.detected_cc
+ return ''
+
+ @staticmethod
+ def _version_from_compiler(c):
+ if isinstance(c, CudaCompiler):
+ return c.version
+ if isinstance(c, str):
+ return c
+ return 'unknown'
+
+ def _validate_nvcc_arch_args(self, args, kwargs):
+ argerror = InvalidArguments('The first argument must be an NVCC compiler object, or its version string!')
+
+ if len(args) < 1:
+ raise argerror
+ else:
+ compiler = args[0]
+ cuda_version = self._version_from_compiler(compiler)
+ if cuda_version == 'unknown':
+ raise argerror
+
+ arch_list = [] if len(args) <= 1 else flatten(args[1:])
+ arch_list = [self._break_arch_string(a) for a in arch_list]
+ arch_list = flatten(arch_list)
+ if len(arch_list) > 1 and not set(arch_list).isdisjoint({'All', 'Common', 'Auto'}):
+ raise InvalidArguments('''The special architectures 'All', 'Common' and 'Auto' must appear alone, as a positional argument!''')
+ arch_list = arch_list[0] if len(arch_list) == 1 else arch_list
+
+ detected = kwargs.get('detected', self._detected_cc_from_compiler(compiler))
+ detected = flatten([detected])
+ detected = [self._break_arch_string(a) for a in detected]
+ detected = flatten(detected)
+ if not set(detected).isdisjoint({'All', 'Common', 'Auto'}):
+ raise InvalidArguments('''The special architectures 'All', 'Common' and 'Auto' must appear alone, as a positional argument!''')
+
+ return cuda_version, arch_list, detected
+
+ def _filter_cuda_arch_list(self, cuda_arch_list, lo=None, hi=None, saturate=None):
+ """
+ Filter CUDA arch list (no codenames) for >= low and < hi architecture
+ bounds, and deduplicate.
+ If saturate is provided, architectures >= hi are replaced with saturate.
+ """
+
+ filtered_cuda_arch_list = []
+ for arch in cuda_arch_list:
+ if arch:
+ if lo and version_compare(arch, '<' + lo):
+ continue
+ if hi and version_compare(arch, '>=' + hi):
+ if not saturate:
+ continue
+ arch = saturate
+ if arch not in filtered_cuda_arch_list:
+ filtered_cuda_arch_list.append(arch)
+ return filtered_cuda_arch_list
+
+ def _nvcc_arch_flags(self, cuda_version, cuda_arch_list='Auto', detected=''):
+ """
+ Using the CUDA Toolkit version and the target architectures, compute
+ the NVCC architecture flags.
+ """
+
+ # Replicates much of the logic of
+ # https://github.com/Kitware/CMake/blob/master/Modules/FindCUDA/select_compute_arch.cmake
+ # except that a bug with cuda_arch_list="All" is worked around by
+ # tracking both lower and upper limits on GPU architectures.
+
+ cuda_known_gpu_architectures = ['Fermi', 'Kepler', 'Maxwell'] # noqa: E221
+ cuda_common_gpu_architectures = ['3.0', '3.5', '5.0'] # noqa: E221
+ cuda_hi_limit_gpu_architecture = None # noqa: E221
+ cuda_lo_limit_gpu_architecture = '2.0' # noqa: E221
+ cuda_all_gpu_architectures = ['3.0', '3.2', '3.5', '5.0'] # noqa: E221
+
+ if version_compare(cuda_version, '<7.0'):
+ cuda_hi_limit_gpu_architecture = '5.2'
+
+ if version_compare(cuda_version, '>=7.0'):
+ cuda_known_gpu_architectures += ['Kepler+Tegra', 'Kepler+Tesla', 'Maxwell+Tegra'] # noqa: E221
+ cuda_common_gpu_architectures += ['5.2'] # noqa: E221
+
+ if version_compare(cuda_version, '<8.0'):
+ cuda_common_gpu_architectures += ['5.2+PTX'] # noqa: E221
+ cuda_hi_limit_gpu_architecture = '6.0' # noqa: E221
+
+ if version_compare(cuda_version, '>=8.0'):
+ cuda_known_gpu_architectures += ['Pascal', 'Pascal+Tegra'] # noqa: E221
+ cuda_common_gpu_architectures += ['6.0', '6.1'] # noqa: E221
+ cuda_all_gpu_architectures += ['6.0', '6.1', '6.2'] # noqa: E221
+
+ if version_compare(cuda_version, '<9.0'):
+ cuda_common_gpu_architectures += ['6.1+PTX'] # noqa: E221
+ cuda_hi_limit_gpu_architecture = '7.0' # noqa: E221
+
+ if version_compare(cuda_version, '>=9.0'):
+ cuda_known_gpu_architectures += ['Volta', 'Xavier'] # noqa: E221
+ cuda_common_gpu_architectures += ['7.0'] # noqa: E221
+ cuda_all_gpu_architectures += ['7.0', '7.2'] # noqa: E221
+ # https://docs.nvidia.com/cuda/archive/9.0/cuda-toolkit-release-notes/index.html#unsupported-features
+ cuda_lo_limit_gpu_architecture = '3.0' # noqa: E221
+
+ if version_compare(cuda_version, '<10.0'):
+ cuda_common_gpu_architectures += ['7.2+PTX'] # noqa: E221
+ cuda_hi_limit_gpu_architecture = '8.0' # noqa: E221
+
+ if version_compare(cuda_version, '>=10.0'):
+ cuda_known_gpu_architectures += ['Turing'] # noqa: E221
+ cuda_common_gpu_architectures += ['7.5'] # noqa: E221
+ cuda_all_gpu_architectures += ['7.5'] # noqa: E221
+
+ if version_compare(cuda_version, '<11.0'):
+ cuda_common_gpu_architectures += ['7.5+PTX'] # noqa: E221
+ cuda_hi_limit_gpu_architecture = '8.0' # noqa: E221
+
+ if version_compare(cuda_version, '>=11.0'):
+ cuda_known_gpu_architectures += ['Ampere'] # noqa: E221
+ cuda_common_gpu_architectures += ['8.0'] # noqa: E221
+ cuda_all_gpu_architectures += ['8.0'] # noqa: E221
+ # https://docs.nvidia.com/cuda/archive/11.0/cuda-toolkit-release-notes/index.html#deprecated-features
+ cuda_lo_limit_gpu_architecture = '3.5' # noqa: E221
+
+ if version_compare(cuda_version, '<11.1'):
+ cuda_common_gpu_architectures += ['8.0+PTX'] # noqa: E221
+ cuda_hi_limit_gpu_architecture = '8.6' # noqa: E221
+
+ if version_compare(cuda_version, '>=11.1'):
+ cuda_common_gpu_architectures += ['8.6'] # noqa: E221
+ cuda_all_gpu_architectures += ['8.6'] # noqa: E221
+
+ if version_compare(cuda_version, '<11.8'):
+ cuda_common_gpu_architectures += ['8.6+PTX'] # noqa: E221
+ cuda_hi_limit_gpu_architecture = '8.7' # noqa: E221
+
+ if version_compare(cuda_version, '>=11.8'):
+ cuda_known_gpu_architectures += ['Orin', 'Lovelace', 'Hopper'] # noqa: E221
+ cuda_common_gpu_architectures += ['8.9', '9.0', '9.0+PTX'] # noqa: E221
+ cuda_all_gpu_architectures += ['8.7', '8.9', '9.0'] # noqa: E221
+
+ if version_compare(cuda_version, '<12'):
+ cuda_hi_limit_gpu_architecture = '9.1' # noqa: E221
+
+ if version_compare(cuda_version, '>=12.0'):
+ # https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html#deprecated-features (Current)
+ # https://docs.nvidia.com/cuda/archive/12.0/cuda-toolkit-release-notes/index.html#deprecated-features (Eventual?)
+ cuda_lo_limit_gpu_architecture = '5.0' # noqa: E221
+
+ if version_compare(cuda_version, '<13'):
+ cuda_hi_limit_gpu_architecture = '10.0' # noqa: E221
+
+ if not cuda_arch_list:
+ cuda_arch_list = 'Auto'
+
+ if cuda_arch_list == 'All': # noqa: E271
+ cuda_arch_list = cuda_known_gpu_architectures
+ elif cuda_arch_list == 'Common': # noqa: E271
+ cuda_arch_list = cuda_common_gpu_architectures
+ elif cuda_arch_list == 'Auto': # noqa: E271
+ if detected:
+ if isinstance(detected, list):
+ cuda_arch_list = detected
+ else:
+ cuda_arch_list = self._break_arch_string(detected)
+ cuda_arch_list = self._filter_cuda_arch_list(cuda_arch_list,
+ cuda_lo_limit_gpu_architecture,
+ cuda_hi_limit_gpu_architecture,
+ cuda_common_gpu_architectures[-1])
+ else:
+ cuda_arch_list = cuda_common_gpu_architectures
+ elif isinstance(cuda_arch_list, str):
+ cuda_arch_list = self._break_arch_string(cuda_arch_list)
+
+ cuda_arch_list = sorted(x for x in set(cuda_arch_list) if x)
+
+ cuda_arch_bin = []
+ cuda_arch_ptx = []
+ for arch_name in cuda_arch_list:
+ arch_bin = []
+ arch_ptx = []
+ add_ptx = arch_name.endswith('+PTX')
+ if add_ptx:
+ arch_name = arch_name[:-len('+PTX')]
+
+ if re.fullmatch('[0-9]+\\.[0-9](\\([0-9]+\\.[0-9]\\))?', arch_name):
+ arch_bin, arch_ptx = [arch_name], [arch_name]
+ else:
+ arch_bin, arch_ptx = {
+ 'Fermi': (['2.0', '2.1(2.0)'], []),
+ 'Kepler+Tegra': (['3.2'], []),
+ 'Kepler+Tesla': (['3.7'], []),
+ 'Kepler': (['3.0', '3.5'], ['3.5']),
+ 'Maxwell+Tegra': (['5.3'], []),
+ 'Maxwell': (['5.0', '5.2'], ['5.2']),
+ 'Pascal': (['6.0', '6.1'], ['6.1']),
+ 'Pascal+Tegra': (['6.2'], []),
+ 'Volta': (['7.0'], ['7.0']),
+ 'Xavier': (['7.2'], []),
+ 'Turing': (['7.5'], ['7.5']),
+ 'Ampere': (['8.0'], ['8.0']),
+ 'Orin': (['8.7'], []),
+ 'Lovelace': (['8.9'], ['8.9']),
+ 'Hopper': (['9.0'], ['9.0']),
+ }.get(arch_name, (None, None))
+
+ if arch_bin is None:
+ raise InvalidArguments(f'Unknown CUDA Architecture Name {arch_name}!')
+
+ cuda_arch_bin += arch_bin
+
+ if add_ptx:
+ if not arch_ptx:
+ arch_ptx = arch_bin
+ cuda_arch_ptx += arch_ptx
+
+ cuda_arch_bin = sorted(set(cuda_arch_bin))
+ cuda_arch_ptx = sorted(set(cuda_arch_ptx))
+
+ nvcc_flags = []
+ nvcc_archs_readable = []
+
+ for arch in cuda_arch_bin:
+ arch, codev = re.fullmatch(
+ '([0-9]+\\.[0-9])(?:\\(([0-9]+\\.[0-9])\\))?', arch).groups()
+
+ if version_compare(arch, '<' + cuda_lo_limit_gpu_architecture):
+ continue
+ if cuda_hi_limit_gpu_architecture and version_compare(arch, '>=' + cuda_hi_limit_gpu_architecture):
+ continue
+
+ if codev:
+ arch = arch.replace('.', '')
+ codev = codev.replace('.', '')
+ nvcc_flags += ['-gencode', 'arch=compute_' + codev + ',code=sm_' + arch]
+ nvcc_archs_readable += ['sm_' + arch]
+ else:
+ arch = arch.replace('.', '')
+ nvcc_flags += ['-gencode', 'arch=compute_' + arch + ',code=sm_' + arch]
+ nvcc_archs_readable += ['sm_' + arch]
+
+ for arch in cuda_arch_ptx:
+ arch, codev = re.fullmatch(
+ '([0-9]+\\.[0-9])(?:\\(([0-9]+\\.[0-9])\\))?', arch).groups()
+
+ if codev:
+ arch = codev
+
+ if version_compare(arch, '<' + cuda_lo_limit_gpu_architecture):
+ continue
+ if cuda_hi_limit_gpu_architecture and version_compare(arch, '>=' + cuda_hi_limit_gpu_architecture):
+ continue
+
+ arch = arch.replace('.', '')
+ nvcc_flags += ['-gencode', 'arch=compute_' + arch + ',code=compute_' + arch]
+ nvcc_archs_readable += ['compute_' + arch]
+
+ return nvcc_flags, nvcc_archs_readable
+
+def initialize(*args, **kwargs):
+ return CudaModule(*args, **kwargs)
diff --git a/mesonbuild/modules/dlang.py b/mesonbuild/modules/dlang.py
new file mode 100644
index 0000000..b9d4299
--- /dev/null
+++ b/mesonbuild/modules/dlang.py
@@ -0,0 +1,136 @@
+# Copyright 2018 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.
+
+# This file contains the detection logic for external dependencies that
+# are UI-related.
+from __future__ import annotations
+
+import json
+import os
+
+from . import ExtensionModule, ModuleInfo
+from .. import dependencies
+from .. import mlog
+from ..interpreterbase import typed_pos_args
+from ..mesonlib import Popen_safe, MesonException
+
+class DlangModule(ExtensionModule):
+ class_dubbin = None
+ init_dub = False
+
+ INFO = ModuleInfo('dlang', '0.48.0')
+
+ def __init__(self, interpreter):
+ super().__init__(interpreter)
+ self.methods.update({
+ 'generate_dub_file': self.generate_dub_file,
+ })
+
+ def _init_dub(self, state):
+ if DlangModule.class_dubbin is None:
+ self.dubbin = dependencies.DubDependency.class_dubbin
+ DlangModule.class_dubbin = self.dubbin
+ else:
+ self.dubbin = DlangModule.class_dubbin
+
+ if DlangModule.class_dubbin is None:
+ self.dubbin = self.check_dub(state)
+ DlangModule.class_dubbin = self.dubbin
+ else:
+ self.dubbin = DlangModule.class_dubbin
+
+ if not self.dubbin:
+ if not self.dubbin:
+ raise MesonException('DUB not found.')
+
+ @typed_pos_args('dlang.generate_dub_file', str, str)
+ def generate_dub_file(self, state, args, kwargs):
+ if not DlangModule.init_dub:
+ self._init_dub(state)
+
+ config = {
+ 'name': args[0]
+ }
+
+ config_path = os.path.join(args[1], 'dub.json')
+ if os.path.exists(config_path):
+ with open(config_path, encoding='utf-8') as ofile:
+ try:
+ config = json.load(ofile)
+ except ValueError:
+ mlog.warning('Failed to load the data in dub.json')
+
+ warn_publishing = ['description', 'license']
+ for arg in warn_publishing:
+ if arg not in kwargs and \
+ arg not in config:
+ mlog.warning('Without', mlog.bold(arg), 'the DUB package can\'t be published')
+
+ for key, value in kwargs.items():
+ if key == 'dependencies':
+ config[key] = {}
+ if isinstance(value, list):
+ for dep in value:
+ if isinstance(dep, dependencies.Dependency):
+ name = dep.get_name()
+ ret, res = self._call_dubbin(['describe', name])
+ if ret == 0:
+ version = dep.get_version()
+ if version is None:
+ config[key][name] = ''
+ else:
+ config[key][name] = version
+ elif isinstance(value, dependencies.Dependency):
+ name = value.get_name()
+ ret, res = self._call_dubbin(['describe', name])
+ if ret == 0:
+ version = value.get_version()
+ if version is None:
+ config[key][name] = ''
+ else:
+ config[key][name] = version
+ else:
+ config[key] = value
+
+ with open(config_path, 'w', encoding='utf-8') as ofile:
+ ofile.write(json.dumps(config, indent=4, ensure_ascii=False))
+
+ def _call_dubbin(self, args, env=None):
+ p, out = Popen_safe(self.dubbin.get_command() + args, env=env)[0:2]
+ return p.returncode, out.strip()
+
+ def check_dub(self, state):
+ dubbin = state.find_program('dub', silent=True)
+ if dubbin.found():
+ try:
+ p, out = Popen_safe(dubbin.get_command() + ['--version'])[0:2]
+ if p.returncode != 0:
+ mlog.warning('Found dub {!r} but couldn\'t run it'
+ ''.format(' '.join(dubbin.get_command())))
+ # Set to False instead of None to signify that we've already
+ # searched for it and not found it
+ dubbin = False
+ except (FileNotFoundError, PermissionError):
+ dubbin = False
+ else:
+ dubbin = False
+ if dubbin:
+ mlog.log('Found DUB:', mlog.bold(dubbin.get_path()),
+ '(%s)' % out.strip())
+ else:
+ mlog.log('Found DUB:', mlog.red('NO'))
+ return dubbin
+
+def initialize(*args, **kwargs):
+ return DlangModule(*args, **kwargs)
diff --git a/mesonbuild/modules/external_project.py b/mesonbuild/modules/external_project.py
new file mode 100644
index 0000000..c3b01c8
--- /dev/null
+++ b/mesonbuild/modules/external_project.py
@@ -0,0 +1,307 @@
+# Copyright 2020 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.
+
+from pathlib import Path
+import os
+import shlex
+import subprocess
+import typing as T
+
+from . import ExtensionModule, ModuleReturnValue, NewExtensionModule, ModuleInfo
+from .. import mlog, build
+from ..compilers.compilers import CFLAGS_MAPPING
+from ..envconfig import ENV_VAR_PROG_MAP
+from ..dependencies import InternalDependency, PkgConfigDependency
+from ..interpreterbase import FeatureNew
+from ..interpreter.type_checking import ENV_KW, DEPENDS_KW
+from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, typed_kwargs, typed_pos_args
+from ..mesonlib import (EnvironmentException, MesonException, Popen_safe, MachineChoice,
+ get_variable_regex, do_replacement, join_args, OptionKey)
+
+if T.TYPE_CHECKING:
+ from typing_extensions import TypedDict
+
+ from . import ModuleState
+ from ..interpreter import Interpreter
+ from ..interpreterbase import TYPE_var
+ from ..build import BuildTarget, CustomTarget
+
+ class Dependency(TypedDict):
+
+ subdir: str
+
+ class AddProject(TypedDict):
+
+ configure_options: T.List[str]
+ cross_configure_options: T.List[str]
+ verbose: bool
+ env: build.EnvironmentVariables
+ depends: T.List[T.Union[BuildTarget, CustomTarget]]
+
+
+class ExternalProject(NewExtensionModule):
+ def __init__(self,
+ state: 'ModuleState',
+ configure_command: str,
+ configure_options: T.List[str],
+ cross_configure_options: T.List[str],
+ env: build.EnvironmentVariables,
+ verbose: bool,
+ extra_depends: T.List[T.Union['BuildTarget', 'CustomTarget']]):
+ super().__init__()
+ self.methods.update({'dependency': self.dependency_method,
+ })
+
+ self.subdir = Path(state.subdir)
+ self.project_version = state.project_version
+ self.subproject = state.subproject
+ self.env = state.environment
+ self.build_machine = state.build_machine
+ self.host_machine = state.host_machine
+ self.configure_command = configure_command
+ self.configure_options = configure_options
+ self.cross_configure_options = cross_configure_options
+ self.verbose = verbose
+ self.user_env = env
+
+ self.src_dir = Path(self.env.get_source_dir(), self.subdir)
+ self.build_dir = Path(self.env.get_build_dir(), self.subdir, 'build')
+ self.install_dir = Path(self.env.get_build_dir(), self.subdir, 'dist')
+ _p = self.env.coredata.get_option(OptionKey('prefix'))
+ assert isinstance(_p, str), 'for mypy'
+ self.prefix = Path(_p)
+ _l = self.env.coredata.get_option(OptionKey('libdir'))
+ assert isinstance(_l, str), 'for mypy'
+ self.libdir = Path(_l)
+ _i = self.env.coredata.get_option(OptionKey('includedir'))
+ assert isinstance(_i, str), 'for mypy'
+ self.includedir = Path(_i)
+ self.name = self.src_dir.name
+
+ # On Windows if the prefix is "c:/foo" and DESTDIR is "c:/bar", `make`
+ # will install files into "c:/bar/c:/foo" which is an invalid path.
+ # Work around that issue by removing the drive from prefix.
+ if self.prefix.drive:
+ self.prefix = self.prefix.relative_to(self.prefix.drive)
+
+ # self.prefix is an absolute path, so we cannot append it to another path.
+ self.rel_prefix = self.prefix.relative_to(self.prefix.root)
+
+ self._configure(state)
+
+ self.targets = self._create_targets(extra_depends)
+
+ def _configure(self, state: 'ModuleState') -> None:
+ if self.configure_command == 'waf':
+ FeatureNew('Waf external project', '0.60.0').use(self.subproject, state.current_node)
+ waf = state.find_program('waf')
+ configure_cmd = waf.get_command()
+ configure_cmd += ['configure', '-o', str(self.build_dir)]
+ workdir = self.src_dir
+ self.make = waf.get_command() + ['build']
+ else:
+ # Assume it's the name of a script in source dir, like 'configure',
+ # 'autogen.sh', etc).
+ configure_path = Path(self.src_dir, self.configure_command)
+ configure_prog = state.find_program(configure_path.as_posix())
+ configure_cmd = configure_prog.get_command()
+ workdir = self.build_dir
+ self.make = state.find_program('make').get_command()
+
+ d = [('PREFIX', '--prefix=@PREFIX@', self.prefix.as_posix()),
+ ('LIBDIR', '--libdir=@PREFIX@/@LIBDIR@', self.libdir.as_posix()),
+ ('INCLUDEDIR', None, self.includedir.as_posix()),
+ ]
+ self._validate_configure_options(d, state)
+
+ configure_cmd += self._format_options(self.configure_options, d)
+
+ if self.env.is_cross_build():
+ host = '{}-{}-{}'.format(self.host_machine.cpu_family,
+ self.build_machine.system,
+ self.host_machine.system)
+ d = [('HOST', None, host)]
+ configure_cmd += self._format_options(self.cross_configure_options, d)
+
+ # Set common env variables like CFLAGS, CC, etc.
+ link_exelist: T.List[str] = []
+ link_args: T.List[str] = []
+ self.run_env = os.environ.copy()
+ for lang, compiler in self.env.coredata.compilers[MachineChoice.HOST].items():
+ if any(lang not in i for i in (ENV_VAR_PROG_MAP, CFLAGS_MAPPING)):
+ continue
+ cargs = self.env.coredata.get_external_args(MachineChoice.HOST, lang)
+ assert isinstance(cargs, list), 'for mypy'
+ self.run_env[ENV_VAR_PROG_MAP[lang]] = self._quote_and_join(compiler.get_exelist())
+ self.run_env[CFLAGS_MAPPING[lang]] = self._quote_and_join(cargs)
+ if not link_exelist:
+ link_exelist = compiler.get_linker_exelist()
+ _l = self.env.coredata.get_external_link_args(MachineChoice.HOST, lang)
+ assert isinstance(_l, list), 'for mypy'
+ link_args = _l
+ if link_exelist:
+ # FIXME: Do not pass linker because Meson uses CC as linker wrapper,
+ # but autotools often expects the real linker (e.h. GNU ld).
+ # self.run_env['LD'] = self._quote_and_join(link_exelist)
+ pass
+ self.run_env['LDFLAGS'] = self._quote_and_join(link_args)
+
+ self.run_env = self.user_env.get_env(self.run_env)
+ self.run_env = PkgConfigDependency.setup_env(self.run_env, self.env, MachineChoice.HOST,
+ uninstalled=True)
+
+ self.build_dir.mkdir(parents=True, exist_ok=True)
+ self._run('configure', configure_cmd, workdir)
+
+ def _quote_and_join(self, array: T.List[str]) -> str:
+ return ' '.join([shlex.quote(i) for i in array])
+
+ def _validate_configure_options(self, variables: T.List[T.Tuple[str, str, str]], state: 'ModuleState') -> None:
+ # Ensure the user at least try to pass basic info to the build system,
+ # like the prefix, libdir, etc.
+ for key, default, val in variables:
+ if default is None:
+ continue
+ key_format = f'@{key}@'
+ for option in self.configure_options:
+ if key_format in option:
+ break
+ else:
+ FeatureNew('Default configure_option', '0.57.0').use(self.subproject, state.current_node)
+ self.configure_options.append(default)
+
+ def _format_options(self, options: T.List[str], variables: T.List[T.Tuple[str, str, str]]) -> T.List[str]:
+ out: T.List[str] = []
+ missing = set()
+ regex = get_variable_regex('meson')
+ confdata: T.Dict[str, T.Tuple[str, T.Optional[str]]] = {k: (v, None) for k, _, v in variables}
+ for o in options:
+ arg, missing_vars = do_replacement(regex, o, 'meson', confdata)
+ missing.update(missing_vars)
+ out.append(arg)
+ if missing:
+ var_list = ", ".join(repr(m) for m in sorted(missing))
+ raise EnvironmentException(
+ f"Variables {var_list} in configure options are missing.")
+ return out
+
+ def _run(self, step: str, command: T.List[str], workdir: Path) -> None:
+ mlog.log(f'External project {self.name}:', mlog.bold(step))
+ m = 'Running command ' + str(command) + ' in directory ' + str(workdir) + '\n'
+ log_filename = Path(mlog.log_dir, f'{self.name}-{step}.log')
+ output = None
+ if not self.verbose:
+ output = open(log_filename, 'w', encoding='utf-8')
+ output.write(m + '\n')
+ output.flush()
+ else:
+ mlog.log(m)
+ p, *_ = Popen_safe(command, cwd=workdir, env=self.run_env,
+ stderr=subprocess.STDOUT,
+ stdout=output)
+ if p.returncode != 0:
+ m = f'{step} step returned error code {p.returncode}.'
+ if not self.verbose:
+ m += '\nSee logs: ' + str(log_filename)
+ raise MesonException(m)
+
+ def _create_targets(self, extra_depends: T.List[T.Union['BuildTarget', 'CustomTarget']]) -> T.List['TYPE_var']:
+ cmd = self.env.get_build_command()
+ cmd += ['--internal', 'externalproject',
+ '--name', self.name,
+ '--srcdir', self.src_dir.as_posix(),
+ '--builddir', self.build_dir.as_posix(),
+ '--installdir', self.install_dir.as_posix(),
+ '--logdir', mlog.log_dir,
+ '--make', join_args(self.make),
+ ]
+ if self.verbose:
+ cmd.append('--verbose')
+
+ self.target = build.CustomTarget(
+ self.name,
+ self.subdir.as_posix(),
+ self.subproject,
+ self.env,
+ cmd + ['@OUTPUT@', '@DEPFILE@'],
+ [],
+ [f'{self.name}.stamp'],
+ depfile=f'{self.name}.d',
+ console=True,
+ extra_depends=extra_depends,
+ )
+
+ idir = build.InstallDir(self.subdir.as_posix(),
+ Path('dist', self.rel_prefix).as_posix(),
+ install_dir='.',
+ install_dir_name='.',
+ install_mode=None,
+ exclude=None,
+ strip_directory=True,
+ from_source_dir=False,
+ subproject=self.subproject)
+
+ return [self.target, idir]
+
+ @typed_pos_args('external_project.dependency', str)
+ @typed_kwargs('external_project.dependency', KwargInfo('subdir', str, default=''))
+ def dependency_method(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'Dependency') -> InternalDependency:
+ libname = args[0]
+
+ abs_includedir = Path(self.install_dir, self.rel_prefix, self.includedir)
+ if kwargs['subdir']:
+ abs_includedir = Path(abs_includedir, kwargs['subdir'])
+ abs_libdir = Path(self.install_dir, self.rel_prefix, self.libdir)
+
+ version = self.project_version
+ compile_args = [f'-I{abs_includedir}']
+ link_args = [f'-L{abs_libdir}', f'-l{libname}']
+ sources = self.target
+ dep = InternalDependency(version, [], compile_args, link_args, [],
+ [], [sources], [], {}, [], [])
+ return dep
+
+
+class ExternalProjectModule(ExtensionModule):
+
+ INFO = ModuleInfo('External build system', '0.56.0', unstable=True)
+
+ def __init__(self, interpreter: 'Interpreter'):
+ super().__init__(interpreter)
+ self.methods.update({'add_project': self.add_project,
+ })
+
+ @typed_pos_args('external_project_mod.add_project', str)
+ @typed_kwargs(
+ 'external_project.add_project',
+ KwargInfo('configure_options', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('cross_configure_options', ContainerTypeInfo(list, str), default=['--host=@HOST@'], listify=True),
+ KwargInfo('verbose', bool, default=False),
+ ENV_KW,
+ DEPENDS_KW.evolve(since='0.63.0'),
+ )
+ def add_project(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'AddProject') -> ModuleReturnValue:
+ configure_command = args[0]
+ project = ExternalProject(state,
+ configure_command,
+ kwargs['configure_options'],
+ kwargs['cross_configure_options'],
+ kwargs['env'],
+ kwargs['verbose'],
+ kwargs['depends'])
+ return ModuleReturnValue(project, project.targets)
+
+
+def initialize(interp: 'Interpreter') -> ExternalProjectModule:
+ return ExternalProjectModule(interp)
diff --git a/mesonbuild/modules/fs.py b/mesonbuild/modules/fs.py
new file mode 100644
index 0000000..7d96995
--- /dev/null
+++ b/mesonbuild/modules/fs.py
@@ -0,0 +1,315 @@
+# Copyright 2019 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.
+
+from __future__ import annotations
+from pathlib import Path, PurePath, PureWindowsPath
+import hashlib
+import os
+import typing as T
+
+from . import ExtensionModule, ModuleReturnValue, ModuleInfo
+from .. import mlog
+from ..build import CustomTarget, InvalidArguments
+from ..interpreter.type_checking import INSTALL_KW, INSTALL_MODE_KW, INSTALL_TAG_KW, NoneType
+from ..interpreterbase import FeatureNew, KwargInfo, typed_kwargs, typed_pos_args, noKwargs
+from ..mesonlib import (
+ File,
+ MesonException,
+ has_path_sep,
+ path_is_in_root,
+)
+
+if T.TYPE_CHECKING:
+ from . import ModuleState
+ from ..interpreter import Interpreter
+ from ..mesonlib import FileOrString, FileMode
+
+ from typing_extensions import TypedDict
+
+ class ReadKwArgs(TypedDict):
+ """Keyword Arguments for fs.read."""
+
+ encoding: str
+
+ class CopyKw(TypedDict):
+
+ """Kwargs for fs.copy"""
+
+ install: bool
+ install_dir: T.Optional[str]
+ install_mode: FileMode
+ install_tag: T.Optional[str]
+
+
+class FSModule(ExtensionModule):
+
+ INFO = ModuleInfo('fs', '0.53.0')
+
+ def __init__(self, interpreter: 'Interpreter') -> None:
+ super().__init__(interpreter)
+ self.methods.update({
+ 'expanduser': self.expanduser,
+ 'is_absolute': self.is_absolute,
+ 'as_posix': self.as_posix,
+ 'exists': self.exists,
+ 'is_symlink': self.is_symlink,
+ 'is_file': self.is_file,
+ 'is_dir': self.is_dir,
+ 'hash': self.hash,
+ 'size': self.size,
+ 'is_samepath': self.is_samepath,
+ 'replace_suffix': self.replace_suffix,
+ 'parent': self.parent,
+ 'name': self.name,
+ 'stem': self.stem,
+ 'read': self.read,
+ 'copyfile': self.copyfile,
+ })
+
+ def _absolute_dir(self, state: 'ModuleState', arg: 'FileOrString') -> Path:
+ """
+ make an absolute path from a relative path, WITHOUT resolving symlinks
+ """
+ if isinstance(arg, File):
+ return Path(arg.absolute_path(state.source_root, self.interpreter.environment.get_build_dir()))
+ return Path(state.source_root) / Path(state.subdir) / Path(arg).expanduser()
+
+ def _resolve_dir(self, state: 'ModuleState', arg: 'FileOrString') -> Path:
+ """
+ resolves symlinks and makes absolute a directory relative to calling meson.build,
+ if not already absolute
+ """
+ path = self._absolute_dir(state, arg)
+ try:
+ # accommodate unresolvable paths e.g. symlink loops
+ path = path.resolve()
+ except Exception:
+ # return the best we could do
+ pass
+ return path
+
+ @noKwargs
+ @FeatureNew('fs.expanduser', '0.54.0')
+ @typed_pos_args('fs.expanduser', str)
+ def expanduser(self, state: 'ModuleState', args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> str:
+ return str(Path(args[0]).expanduser())
+
+ @noKwargs
+ @FeatureNew('fs.is_absolute', '0.54.0')
+ @typed_pos_args('fs.is_absolute', (str, File))
+ def is_absolute(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> bool:
+ if isinstance(args[0], File):
+ FeatureNew('fs.is_absolute_file', '0.59.0').use(state.subproject)
+ return PurePath(str(args[0])).is_absolute()
+
+ @noKwargs
+ @FeatureNew('fs.as_posix', '0.54.0')
+ @typed_pos_args('fs.as_posix', str)
+ def as_posix(self, state: 'ModuleState', args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> str:
+ """
+ this function assumes you are passing a Windows path, even if on a Unix-like system
+ and so ALL '\' are turned to '/', even if you meant to escape a character
+ """
+ return PureWindowsPath(args[0]).as_posix()
+
+ @noKwargs
+ @typed_pos_args('fs.exists', str)
+ def exists(self, state: 'ModuleState', args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> bool:
+ return self._resolve_dir(state, args[0]).exists()
+
+ @noKwargs
+ @typed_pos_args('fs.is_symlink', (str, File))
+ def is_symlink(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> bool:
+ if isinstance(args[0], File):
+ FeatureNew('fs.is_symlink_file', '0.59.0').use(state.subproject)
+ return self._absolute_dir(state, args[0]).is_symlink()
+
+ @noKwargs
+ @typed_pos_args('fs.is_file', str)
+ def is_file(self, state: 'ModuleState', args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> bool:
+ return self._resolve_dir(state, args[0]).is_file()
+
+ @noKwargs
+ @typed_pos_args('fs.is_dir', str)
+ def is_dir(self, state: 'ModuleState', args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> bool:
+ return self._resolve_dir(state, args[0]).is_dir()
+
+ @noKwargs
+ @typed_pos_args('fs.hash', (str, File), str)
+ def hash(self, state: 'ModuleState', args: T.Tuple['FileOrString', str], kwargs: T.Dict[str, T.Any]) -> str:
+ if isinstance(args[0], File):
+ FeatureNew('fs.hash_file', '0.59.0').use(state.subproject)
+ file = self._resolve_dir(state, args[0])
+ if not file.is_file():
+ raise MesonException(f'{file} is not a file and therefore cannot be hashed')
+ try:
+ h = hashlib.new(args[1])
+ except ValueError:
+ raise MesonException('hash algorithm {} is not available'.format(args[1]))
+ mlog.debug('computing {} sum of {} size {} bytes'.format(args[1], file, file.stat().st_size))
+ h.update(file.read_bytes())
+ return h.hexdigest()
+
+ @noKwargs
+ @typed_pos_args('fs.size', (str, File))
+ def size(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> int:
+ if isinstance(args[0], File):
+ FeatureNew('fs.size_file', '0.59.0').use(state.subproject)
+ file = self._resolve_dir(state, args[0])
+ if not file.is_file():
+ raise MesonException(f'{file} is not a file and therefore cannot be sized')
+ try:
+ return file.stat().st_size
+ except ValueError:
+ raise MesonException('{} size could not be determined'.format(args[0]))
+
+ @noKwargs
+ @typed_pos_args('fs.is_samepath', (str, File), (str, File))
+ def is_samepath(self, state: 'ModuleState', args: T.Tuple['FileOrString', 'FileOrString'], kwargs: T.Dict[str, T.Any]) -> bool:
+ if isinstance(args[0], File) or isinstance(args[1], File):
+ FeatureNew('fs.is_samepath_file', '0.59.0').use(state.subproject)
+ file1 = self._resolve_dir(state, args[0])
+ file2 = self._resolve_dir(state, args[1])
+ if not file1.exists():
+ return False
+ if not file2.exists():
+ return False
+ try:
+ return file1.samefile(file2)
+ except OSError:
+ return False
+
+ @noKwargs
+ @typed_pos_args('fs.replace_suffix', (str, File), str)
+ def replace_suffix(self, state: 'ModuleState', args: T.Tuple['FileOrString', str], kwargs: T.Dict[str, T.Any]) -> str:
+ if isinstance(args[0], File):
+ FeatureNew('fs.replace_suffix_file', '0.59.0').use(state.subproject)
+ original = PurePath(str(args[0]))
+ new = original.with_suffix(args[1])
+ return str(new)
+
+ @noKwargs
+ @typed_pos_args('fs.parent', (str, File))
+ def parent(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> str:
+ if isinstance(args[0], File):
+ FeatureNew('fs.parent_file', '0.59.0').use(state.subproject)
+ original = PurePath(str(args[0]))
+ new = original.parent
+ return str(new)
+
+ @noKwargs
+ @typed_pos_args('fs.name', (str, File))
+ def name(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> str:
+ if isinstance(args[0], File):
+ FeatureNew('fs.name_file', '0.59.0').use(state.subproject)
+ original = PurePath(str(args[0]))
+ new = original.name
+ return str(new)
+
+ @noKwargs
+ @typed_pos_args('fs.stem', (str, File))
+ @FeatureNew('fs.stem', '0.54.0')
+ def stem(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> str:
+ if isinstance(args[0], File):
+ FeatureNew('fs.stem_file', '0.59.0').use(state.subproject)
+ original = PurePath(str(args[0]))
+ new = original.stem
+ return str(new)
+
+ @FeatureNew('fs.read', '0.57.0')
+ @typed_pos_args('fs.read', (str, File))
+ @typed_kwargs('fs.read', KwargInfo('encoding', str, default='utf-8'))
+ def read(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: 'ReadKwArgs') -> str:
+ """Read a file from the source tree and return its value as a decoded
+ string.
+
+ If the encoding is not specified, the file is assumed to be utf-8
+ encoded. Paths must be relative by default (to prevent accidents) and
+ are forbidden to be read from the build directory (to prevent build
+ loops)
+ """
+ path = args[0]
+ encoding = kwargs['encoding']
+ src_dir = self.interpreter.environment.source_dir
+ sub_dir = self.interpreter.subdir
+ build_dir = self.interpreter.environment.get_build_dir()
+
+ if isinstance(path, File):
+ if path.is_built:
+ raise MesonException(
+ 'fs.read_file does not accept built files() objects')
+ path = os.path.join(src_dir, path.relative_name())
+ else:
+ if sub_dir:
+ src_dir = os.path.join(src_dir, sub_dir)
+ path = os.path.join(src_dir, path)
+
+ path = os.path.abspath(path)
+ if path_is_in_root(Path(path), Path(build_dir), resolve=True):
+ raise MesonException('path must not be in the build tree')
+ try:
+ with open(path, encoding=encoding) as f:
+ data = f.read()
+ except UnicodeDecodeError:
+ raise MesonException(f'decoding failed for {path}')
+ # Reconfigure when this file changes as it can contain data used by any
+ # part of the build configuration (e.g. `project(..., version:
+ # fs.read_file('VERSION')` or `configure_file(...)`
+ self.interpreter.add_build_def_file(path)
+ return data
+
+ @FeatureNew('fs.copyfile', '0.64.0')
+ @typed_pos_args('fs.copyfile', (File, str), optargs=[str])
+ @typed_kwargs(
+ 'fs.copyfile',
+ INSTALL_KW,
+ INSTALL_MODE_KW,
+ INSTALL_TAG_KW,
+ KwargInfo('install_dir', (str, NoneType)),
+ )
+ def copyfile(self, state: ModuleState, args: T.Tuple[FileOrString, T.Optional[str]],
+ kwargs: CopyKw) -> ModuleReturnValue:
+ """Copy a file into the build directory at build time."""
+ if kwargs['install'] and not kwargs['install_dir']:
+ raise InvalidArguments('"install_dir" must be specified when "install" is true')
+
+ src = self.interpreter.source_strings_to_files([args[0]])[0]
+
+ # The input is allowed to have path separators, but the output may not,
+ # so use the basename for the default case
+ dest = args[1] if args[1] else os.path.basename(src.fname)
+ if has_path_sep(dest):
+ raise InvalidArguments('Destination path may not have path separators')
+
+ ct = CustomTarget(
+ dest,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ state.environment.get_build_command() + ['--internal', 'copy', '@INPUT@', '@OUTPUT@'],
+ [src],
+ [dest],
+ build_by_default=True,
+ install=kwargs['install'],
+ install_dir=[kwargs['install_dir']],
+ install_mode=kwargs['install_mode'],
+ install_tag=[kwargs['install_tag']],
+ backend=state.backend,
+ )
+
+ return ModuleReturnValue(ct, [ct])
+
+
+def initialize(*args: T.Any, **kwargs: T.Any) -> FSModule:
+ return FSModule(*args, **kwargs)
diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py
new file mode 100644
index 0000000..38a176d
--- /dev/null
+++ b/mesonbuild/modules/gnome.py
@@ -0,0 +1,2165 @@
+# Copyright 2015-2016 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.
+
+'''This module provides helper functions for Gnome/GLib related
+functionality such as gobject-introspection, gresources and gtk-doc'''
+from __future__ import annotations
+
+import copy
+import itertools
+import functools
+import os
+import subprocess
+import textwrap
+import typing as T
+
+from . import ExtensionModule, ModuleInfo
+from . import ModuleReturnValue
+from .. import build
+from .. import interpreter
+from .. import mesonlib
+from .. import mlog
+from ..build import CustomTarget, CustomTargetIndex, Executable, GeneratedList, InvalidArguments
+from ..dependencies import Dependency, PkgConfigDependency, InternalDependency
+from ..interpreter.type_checking import DEPENDS_KW, DEPEND_FILES_KW, INSTALL_DIR_KW, INSTALL_KW, NoneType, SOURCES_KW, in_set_validator
+from ..interpreterbase import noPosargs, noKwargs, FeatureNew, FeatureDeprecated
+from ..interpreterbase import typed_kwargs, KwargInfo, ContainerTypeInfo
+from ..interpreterbase.decorators import typed_pos_args
+from ..mesonlib import (
+ MachineChoice, MesonException, OrderedSet, Popen_safe, join_args,
+)
+from ..programs import OverrideProgram
+from ..scripts.gettext import read_linguas
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Literal, TypedDict
+
+ from . import ModuleState
+ from ..build import BuildTarget
+ from ..compilers import Compiler
+ from ..interpreter import Interpreter
+ from ..interpreterbase import TYPE_var, TYPE_kwargs
+ from ..mesonlib import FileOrString
+ from ..programs import ExternalProgram
+
+ class PostInstall(TypedDict):
+ glib_compile_schemas: bool
+ gio_querymodules: T.List[str]
+ gtk_update_icon_cache: bool
+ update_desktop_database: bool
+ update_mime_database: bool
+
+ class CompileSchemas(TypedDict):
+
+ build_by_default: bool
+ depend_files: T.List[FileOrString]
+
+ class Yelp(TypedDict):
+
+ languages: T.List[str]
+ media: T.List[str]
+ sources: T.List[str]
+ symlink_media: bool
+
+ class CompileResources(TypedDict):
+
+ build_by_default: bool
+ c_name: T.Optional[str]
+ dependencies: T.List[T.Union[mesonlib.File, build.CustomTarget, build.CustomTargetIndex]]
+ export: bool
+ extra_args: T.List[str]
+ gresource_bundle: bool
+ install: bool
+ install_dir: T.Optional[str]
+ install_header: bool
+ source_dir: T.List[str]
+
+ class GenerateGir(TypedDict):
+
+ build_by_default: bool
+ dependencies: T.List[Dependency]
+ export_packages: T.List[str]
+ extra_args: T.List[str]
+ fatal_warnings: bool
+ header: T.List[str]
+ identifier_prefix: T.List[str]
+ include_directories: T.List[T.Union[build.IncludeDirs, str]]
+ includes: T.List[T.Union[str, GirTarget]]
+ install: bool
+ install_dir_gir: T.Optional[str]
+ install_dir_typelib: T.Optional[str]
+ link_with: T.List[T.Union[build.SharedLibrary, build.StaticLibrary]]
+ namespace: str
+ nsversion: str
+ sources: T.List[T.Union[FileOrString, build.GeneratedTypes]]
+ symbol_prefix: T.List[str]
+
+ class GtkDoc(TypedDict):
+
+ src_dir: T.List[T.Union[str, build.IncludeDirs]]
+ main_sgml: str
+ main_xml: str
+ module_version: str
+ namespace: str
+ mode: Literal['xml', 'smgl', 'auto', 'none']
+ html_args: T.List[str]
+ scan_args: T.List[str]
+ scanobjs_args: T.List[str]
+ fixxref_args: T.List[str]
+ mkdb_args: T.List[str]
+ content_files: T.List[T.Union[build.GeneratedTypes, FileOrString]]
+ ignore_headers: T.List[str]
+ install_dir: T.List[str]
+ check: bool
+ install: bool
+ gobject_typesfile: T.List[FileOrString]
+ html_assets: T.List[FileOrString]
+ expand_content_files: T.List[FileOrString]
+ c_args: T.List[str]
+ include_directories: T.List[T.Union[str, build.IncludeDirs]]
+ dependencies: T.List[T.Union[Dependency, build.SharedLibrary, build.StaticLibrary]]
+
+ class GdbusCodegen(TypedDict):
+
+ sources: T.List[FileOrString]
+ extra_args: T.List[str]
+ interface_prefix: T.Optional[str]
+ namespace: T.Optional[str]
+ object_manager: bool
+ build_by_default: bool
+ annotations: T.List[T.List[str]]
+ install_header: bool
+ install_dir: T.Optional[str]
+ docbook: T.Optional[str]
+ autocleanup: Literal['all', 'none', 'objects', 'default']
+
+ class GenMarshal(TypedDict):
+
+ build_always: T.Optional[str]
+ build_always_stale: T.Optional[bool]
+ build_by_default: T.Optional[bool]
+ depend_files: T.List[mesonlib.File]
+ extra_args: T.List[str]
+ install_dir: T.Optional[str]
+ install_header: bool
+ internal: bool
+ nostdinc: bool
+ prefix: T.Optional[str]
+ skip_source: bool
+ sources: T.List[FileOrString]
+ stdinc: bool
+ valist_marshallers: bool
+
+ class GenerateVapi(TypedDict):
+
+ sources: T.List[T.Union[str, GirTarget]]
+ install_dir: T.Optional[str]
+ install: bool
+ vapi_dirs: T.List[str]
+ metadata_dirs: T.List[str]
+ gir_dirs: T.List[str]
+ packages: T.List[T.Union[str, InternalDependency]]
+
+ class _MkEnumsCommon(TypedDict):
+
+ sources: T.List[T.Union[FileOrString, build.GeneratedTypes]]
+ install_header: bool
+ install_dir: T.Optional[str]
+ identifier_prefix: T.Optional[str]
+ symbol_prefix: T.Optional[str]
+
+ class MkEnumsSimple(_MkEnumsCommon):
+
+ header_prefix: str
+ decorator: str
+ function_prefix: str
+ body_prefix: str
+
+ class MkEnums(_MkEnumsCommon):
+
+ c_template: T.Optional[FileOrString]
+ h_template: T.Optional[FileOrString]
+ comments: T.Optional[str]
+ eprod: T.Optional[str]
+ fhead: T.Optional[str]
+ fprod: T.Optional[str]
+ ftail: T.Optional[str]
+ vhead: T.Optional[str]
+ vprod: T.Optional[str]
+ vtail: T.Optional[str]
+ depends: T.List[T.Union[BuildTarget, CustomTarget, CustomTargetIndex]]
+
+
+# Differs from the CustomTarget version in that it straight defaults to True
+_BUILD_BY_DEFAULT: KwargInfo[bool] = KwargInfo(
+ 'build_by_default', bool, default=True,
+)
+
+_EXTRA_ARGS_KW: KwargInfo[T.List[str]] = KwargInfo(
+ 'extra_args',
+ ContainerTypeInfo(list, str),
+ default=[],
+ listify=True,
+)
+
+_MK_ENUMS_COMMON_KWS: T.List[KwargInfo] = [
+ INSTALL_KW.evolve(name='install_header'),
+ INSTALL_DIR_KW,
+ KwargInfo(
+ 'sources',
+ ContainerTypeInfo(list, (str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)),
+ listify=True,
+ required=True,
+ ),
+ KwargInfo('identifier_prefix', (str, NoneType)),
+ KwargInfo('symbol_prefix', (str, NoneType)),
+]
+
+def annotations_validator(annotations: T.List[T.Union[str, T.List[str]]]) -> T.Optional[str]:
+ """Validate gdbus-codegen annotations argument"""
+
+ badlist = 'must be made up of 3 strings for ELEMENT, KEY, and VALUE'
+
+ if not annotations:
+ return None
+ elif all(isinstance(annot, str) for annot in annotations):
+ if len(annotations) == 3:
+ return None
+ else:
+ return badlist
+ elif not all(isinstance(annot, list) for annot in annotations):
+ for c, annot in enumerate(annotations):
+ if not isinstance(annot, list):
+ return f'element {c+1} must be a list'
+ else:
+ for c, annot in enumerate(annotations):
+ if len(annot) != 3 or not all(isinstance(i, str) for i in annot):
+ return f'element {c+1} {badlist}'
+ return None
+
+class GResourceTarget(build.CustomTarget):
+ pass
+
+class GResourceHeaderTarget(build.CustomTarget):
+ pass
+
+class GirTarget(build.CustomTarget):
+ pass
+
+class TypelibTarget(build.CustomTarget):
+ pass
+
+class VapiTarget(build.CustomTarget):
+ pass
+
+# gresource compilation is broken due to the way
+# the resource compiler and Ninja clash about it
+#
+# https://github.com/ninja-build/ninja/issues/1184
+# https://bugzilla.gnome.org/show_bug.cgi?id=774368
+gresource_dep_needed_version = '>= 2.51.1'
+
+class GnomeModule(ExtensionModule):
+
+ INFO = ModuleInfo('gnome')
+
+ def __init__(self, interpreter: 'Interpreter') -> None:
+ super().__init__(interpreter)
+ self.gir_dep: T.Optional[Dependency] = None
+ self.giscanner: T.Optional[T.Union[ExternalProgram, build.Executable, OverrideProgram]] = None
+ self.gicompiler: T.Optional[T.Union[ExternalProgram, build.Executable, OverrideProgram]] = None
+ self.install_glib_compile_schemas = False
+ self.install_gio_querymodules: T.List[str] = []
+ self.install_gtk_update_icon_cache = False
+ self.install_update_desktop_database = False
+ self.install_update_mime_database = False
+ self.devenv: T.Optional[build.EnvironmentVariables] = None
+ self.native_glib_version: T.Optional[str] = None
+ self.methods.update({
+ 'post_install': self.post_install,
+ 'compile_resources': self.compile_resources,
+ 'generate_gir': self.generate_gir,
+ 'compile_schemas': self.compile_schemas,
+ 'yelp': self.yelp,
+ 'gtkdoc': self.gtkdoc,
+ 'gtkdoc_html_dir': self.gtkdoc_html_dir,
+ 'gdbus_codegen': self.gdbus_codegen,
+ 'mkenums': self.mkenums,
+ 'mkenums_simple': self.mkenums_simple,
+ 'genmarshal': self.genmarshal,
+ 'generate_vapi': self.generate_vapi,
+ })
+
+ def _get_native_glib_version(self, state: 'ModuleState') -> str:
+ if self.native_glib_version is None:
+ glib_dep = PkgConfigDependency('glib-2.0', state.environment,
+ {'native': True, 'required': False})
+ if glib_dep.found():
+ self.native_glib_version = glib_dep.get_version()
+ else:
+ mlog.warning('Could not detect glib version, assuming 2.54. '
+ 'You may get build errors if your glib is older.')
+ self.native_glib_version = '2.54'
+ return self.native_glib_version
+
+ @mesonlib.run_once
+ def __print_gresources_warning(self, state: 'ModuleState') -> None:
+ if not mesonlib.version_compare(self._get_native_glib_version(state),
+ gresource_dep_needed_version):
+ mlog.warning('GLib compiled dependencies do not work reliably with \n'
+ 'the current version of GLib. See the following upstream issue:',
+ mlog.bold('https://bugzilla.gnome.org/show_bug.cgi?id=774368'))
+
+ @staticmethod
+ def _print_gdbus_warning() -> None:
+ mlog.warning('Code generated with gdbus_codegen() requires the root directory be added to\n'
+ ' include_directories of targets with GLib < 2.51.3:',
+ mlog.bold('https://github.com/mesonbuild/meson/issues/1387'),
+ once=True)
+
+ @typed_kwargs(
+ 'gnome.post_install',
+ KwargInfo('glib_compile_schemas', bool, default=False),
+ KwargInfo('gio_querymodules', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('gtk_update_icon_cache', bool, default=False),
+ KwargInfo('update_desktop_database', bool, default=False, since='0.59.0'),
+ KwargInfo('update_mime_database', bool, default=False, since='0.64.0'),
+ )
+ @noPosargs
+ @FeatureNew('gnome.post_install', '0.57.0')
+ def post_install(self, state: 'ModuleState', args: T.List['TYPE_var'], kwargs: 'PostInstall') -> ModuleReturnValue:
+ rv: T.List['build.ExecutableSerialisation'] = []
+ datadir_abs = os.path.join(state.environment.get_prefix(), state.environment.get_datadir())
+ if kwargs['glib_compile_schemas'] and not self.install_glib_compile_schemas:
+ self.install_glib_compile_schemas = True
+ prog = state.find_tool('glib-compile-schemas', 'gio-2.0', 'glib_compile_schemas')
+ schemasdir = os.path.join(datadir_abs, 'glib-2.0', 'schemas')
+ script = state.backend.get_executable_serialisation([prog, schemasdir])
+ script.skip_if_destdir = True
+ rv.append(script)
+ for d in kwargs['gio_querymodules']:
+ if d not in self.install_gio_querymodules:
+ self.install_gio_querymodules.append(d)
+ prog = state.find_tool('gio-querymodules', 'gio-2.0', 'gio_querymodules')
+ moduledir = os.path.join(state.environment.get_prefix(), d)
+ script = state.backend.get_executable_serialisation([prog, moduledir])
+ script.skip_if_destdir = True
+ rv.append(script)
+ if kwargs['gtk_update_icon_cache'] and not self.install_gtk_update_icon_cache:
+ self.install_gtk_update_icon_cache = True
+ prog = state.find_program('gtk4-update-icon-cache', required=False)
+ found = isinstance(prog, build.Executable) or prog.found()
+ if not found:
+ prog = state.find_program('gtk-update-icon-cache')
+ icondir = os.path.join(datadir_abs, 'icons', 'hicolor')
+ script = state.backend.get_executable_serialisation([prog, '-q', '-t', '-f', icondir])
+ script.skip_if_destdir = True
+ rv.append(script)
+ if kwargs['update_desktop_database'] and not self.install_update_desktop_database:
+ self.install_update_desktop_database = True
+ prog = state.find_program('update-desktop-database')
+ appdir = os.path.join(datadir_abs, 'applications')
+ script = state.backend.get_executable_serialisation([prog, '-q', appdir])
+ script.skip_if_destdir = True
+ rv.append(script)
+ if kwargs['update_mime_database'] and not self.install_update_mime_database:
+ self.install_update_mime_database = True
+ prog = state.find_program('update-mime-database')
+ appdir = os.path.join(datadir_abs, 'mime')
+ script = state.backend.get_executable_serialisation([prog, appdir])
+ script.skip_if_destdir = True
+ rv.append(script)
+ return ModuleReturnValue(None, rv)
+
+ @typed_pos_args('gnome.compile_resources', str, (str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList))
+ @typed_kwargs(
+ 'gnome.compile_resources',
+ _BUILD_BY_DEFAULT,
+ _EXTRA_ARGS_KW,
+ INSTALL_KW,
+ INSTALL_KW.evolve(name='install_header', since='0.37.0'),
+ INSTALL_DIR_KW,
+ KwargInfo('c_name', (str, NoneType)),
+ KwargInfo('dependencies', ContainerTypeInfo(list, (mesonlib.File, build.CustomTarget, build.CustomTargetIndex)), default=[], listify=True),
+ KwargInfo('export', bool, default=False, since='0.37.0'),
+ KwargInfo('gresource_bundle', bool, default=False, since='0.37.0'),
+ KwargInfo('source_dir', ContainerTypeInfo(list, str), default=[], listify=True),
+ )
+ def compile_resources(self, state: 'ModuleState', args: T.Tuple[str, 'FileOrString'],
+ kwargs: 'CompileResources') -> 'ModuleReturnValue':
+ self.__print_gresources_warning(state)
+ glib_version = self._get_native_glib_version(state)
+
+ glib_compile_resources = state.find_program('glib-compile-resources')
+ cmd: T.List[T.Union[ExternalProgram, str]] = [glib_compile_resources, '@INPUT@']
+
+ source_dirs = kwargs['source_dir']
+ dependencies = kwargs['dependencies']
+
+ target_name, input_file = args
+
+ # Validate dependencies
+ subdirs: T.List[str] = []
+ depends: T.List[T.Union[build.CustomTarget, build.CustomTargetIndex]] = []
+ for dep in dependencies:
+ if isinstance(dep, mesonlib.File):
+ subdirs.append(dep.subdir)
+ else:
+ depends.append(dep)
+ subdirs.append(dep.get_subdir())
+ if not mesonlib.version_compare(glib_version, gresource_dep_needed_version):
+ m = 'The "dependencies" argument of gnome.compile_resources() can not\n' \
+ 'be used with the current version of glib-compile-resources due to\n' \
+ '<https://bugzilla.gnome.org/show_bug.cgi?id=774368>'
+ raise MesonException(m)
+
+ if not mesonlib.version_compare(glib_version, gresource_dep_needed_version):
+ # Resource xml files generated at build-time cannot be used with
+ # gnome.compile_resources() because we need to scan the xml for
+ # dependencies. Use configure_file() instead to generate it at
+ # configure-time
+ if isinstance(input_file, mesonlib.File):
+ # glib-compile-resources will be run inside the source dir,
+ # so we need either 'src_to_build' or the absolute path.
+ # Absolute path is the easiest choice.
+ if input_file.is_built:
+ ifile = os.path.join(state.environment.get_build_dir(), input_file.subdir, input_file.fname)
+ else:
+ ifile = os.path.join(input_file.subdir, input_file.fname)
+
+ elif isinstance(input_file, (build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)):
+ raise MesonException('Resource xml files generated at build-time cannot be used with '
+ 'gnome.compile_resources() in the current version of glib-compile-resources '
+ 'because we need to scan the xml for dependencies due to '
+ '<https://bugzilla.gnome.org/show_bug.cgi?id=774368>\nUse '
+ 'configure_file() instead to generate it at configure-time.')
+ else:
+ ifile = os.path.join(state.subdir, input_file)
+
+ depend_files, depends, subdirs = self._get_gresource_dependencies(
+ state, ifile, source_dirs, dependencies)
+
+ # Make source dirs relative to build dir now
+ source_dirs = [os.path.join(state.build_to_src, state.subdir, d) for d in source_dirs]
+ # Ensure build directories of generated deps are included
+ source_dirs += subdirs
+ # Always include current directory, but after paths set by user
+ source_dirs.append(os.path.join(state.build_to_src, state.subdir))
+
+ for source_dir in OrderedSet(source_dirs):
+ cmd += ['--sourcedir', source_dir]
+
+ if kwargs['c_name']:
+ cmd += ['--c-name', kwargs['c_name']]
+ if not kwargs['export']:
+ cmd += ['--internal']
+
+ cmd += ['--generate', '--target', '@OUTPUT@']
+ cmd += kwargs['extra_args']
+
+ gresource = kwargs['gresource_bundle']
+ if gresource:
+ output = f'{target_name}.gresource'
+ name = f'{target_name}_gresource'
+ else:
+ if 'c' in state.environment.coredata.compilers.host:
+ output = f'{target_name}.c'
+ name = f'{target_name}_c'
+ elif 'cpp' in state.environment.coredata.compilers.host:
+ output = f'{target_name}.cpp'
+ name = f'{target_name}_cpp'
+ else:
+ raise MesonException('Compiling GResources into code is only supported in C and C++ projects')
+
+ if kwargs['install'] and not gresource:
+ raise MesonException('The install kwarg only applies to gresource bundles, see install_header')
+
+ install_header = kwargs['install_header']
+ if install_header and gresource:
+ raise MesonException('The install_header kwarg does not apply to gresource bundles')
+ if install_header and not kwargs['export']:
+ raise MesonException('GResource header is installed yet export is not enabled')
+
+ depfile: T.Optional[str] = None
+ target_cmd: T.List[T.Union[ExternalProgram, str]]
+ if not mesonlib.version_compare(glib_version, gresource_dep_needed_version):
+ # This will eventually go out of sync if dependencies are added
+ target_cmd = cmd
+ else:
+ depfile = f'{output}.d'
+ depend_files = []
+ target_cmd = copy.copy(cmd) + ['--dependency-file', '@DEPFILE@']
+ target_c = GResourceTarget(
+ name,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ target_cmd,
+ [input_file],
+ [output],
+ build_by_default=kwargs['build_by_default'],
+ depfile=depfile,
+ depend_files=depend_files,
+ extra_depends=depends,
+ install=kwargs['install'],
+ install_dir=[kwargs['install_dir']] if kwargs['install_dir'] else [],
+ install_tag=['runtime'],
+ )
+
+ if gresource: # Only one target for .gresource files
+ return ModuleReturnValue(target_c, [target_c])
+
+ install_dir = kwargs['install_dir'] or state.environment.coredata.get_option(mesonlib.OptionKey('includedir'))
+ assert isinstance(install_dir, str), 'for mypy'
+ target_h = GResourceHeaderTarget(
+ f'{target_name}_h',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ cmd,
+ [input_file],
+ [f'{target_name}.h'],
+ build_by_default=kwargs['build_by_default'],
+ extra_depends=depends,
+ install=install_header,
+ install_dir=[install_dir],
+ install_tag=['devel'],
+ )
+ rv = [target_c, target_h]
+ return ModuleReturnValue(rv, rv)
+
+ @staticmethod
+ def _get_gresource_dependencies(
+ state: 'ModuleState', input_file: str, source_dirs: T.List[str],
+ dependencies: T.Sequence[T.Union[mesonlib.File, build.CustomTarget, build.CustomTargetIndex]]
+ ) -> T.Tuple[T.List[mesonlib.FileOrString], T.List[T.Union[build.CustomTarget, build.CustomTargetIndex]], T.List[str]]:
+
+ cmd = ['glib-compile-resources',
+ input_file,
+ '--generate-dependencies']
+
+ # Prefer generated files over source files
+ cmd += ['--sourcedir', state.subdir] # Current build dir
+ for source_dir in source_dirs:
+ cmd += ['--sourcedir', os.path.join(state.subdir, source_dir)]
+
+ try:
+ pc, stdout, stderr = Popen_safe(cmd, cwd=state.environment.get_source_dir())
+ except (FileNotFoundError, PermissionError):
+ raise MesonException('Could not execute glib-compile-resources.')
+ if pc.returncode != 0:
+ m = f'glib-compile-resources failed to get dependencies for {cmd[1]}:\n{stderr}'
+ mlog.warning(m)
+ raise subprocess.CalledProcessError(pc.returncode, cmd)
+
+ raw_dep_files: T.List[str] = stdout.split('\n')[:-1]
+
+ depends: T.List[T.Union[build.CustomTarget, build.CustomTargetIndex]] = []
+ subdirs: T.List[str] = []
+ dep_files: T.List[mesonlib.FileOrString] = []
+ for resfile in raw_dep_files.copy():
+ resbasename = os.path.basename(resfile)
+ for dep in dependencies:
+ if isinstance(dep, mesonlib.File):
+ if dep.fname != resbasename:
+ continue
+ raw_dep_files.remove(resfile)
+ dep_files.append(dep)
+ subdirs.append(dep.subdir)
+ break
+ elif isinstance(dep, (build.CustomTarget, build.CustomTargetIndex)):
+ fname = None
+ outputs = {(o, os.path.basename(o)) for o in dep.get_outputs()}
+ for o, baseo in outputs:
+ if baseo == resbasename:
+ fname = o
+ break
+ if fname is not None:
+ raw_dep_files.remove(resfile)
+ depends.append(dep)
+ subdirs.append(dep.get_subdir())
+ break
+ else:
+ # In generate-dependencies mode, glib-compile-resources doesn't raise
+ # an error for missing resources but instead prints whatever filename
+ # was listed in the input file. That's good because it means we can
+ # handle resource files that get generated as part of the build, as
+ # follows.
+ #
+ # If there are multiple generated resource files with the same basename
+ # then this code will get confused.
+ try:
+ f = mesonlib.File.from_source_file(state.environment.get_source_dir(),
+ ".", resfile)
+ except MesonException:
+ raise MesonException(
+ f'Resource "{resfile}" listed in "{input_file}" was not found. '
+ 'If this is a generated file, pass the target that generates '
+ 'it to gnome.compile_resources() using the "dependencies" '
+ 'keyword argument.')
+ raw_dep_files.remove(resfile)
+ dep_files.append(f)
+ dep_files.extend(raw_dep_files)
+ return dep_files, depends, subdirs
+
+ def _get_link_args(self, state: 'ModuleState',
+ lib: T.Union[build.SharedLibrary, build.StaticLibrary],
+ depends: T.Sequence[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]],
+ include_rpath: bool = False,
+ use_gir_args: bool = False
+ ) -> T.Tuple[T.List[str], T.List[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]]]:
+ link_command: T.List[str] = []
+ new_depends = list(depends)
+ # Construct link args
+ if isinstance(lib, build.SharedLibrary):
+ libdir = os.path.join(state.environment.get_build_dir(), state.backend.get_target_dir(lib))
+ link_command.append('-L' + libdir)
+ if include_rpath:
+ link_command.append('-Wl,-rpath,' + libdir)
+ new_depends.append(lib)
+ # Needed for the following binutils bug:
+ # https://github.com/mesonbuild/meson/issues/1911
+ # However, g-ir-scanner does not understand -Wl,-rpath
+ # so we need to use -L instead
+ for d in state.backend.determine_rpath_dirs(lib):
+ d = os.path.join(state.environment.get_build_dir(), d)
+ link_command.append('-L' + d)
+ if include_rpath:
+ link_command.append('-Wl,-rpath,' + d)
+ if use_gir_args and self._gir_has_option('--extra-library'):
+ link_command.append('--extra-library=' + lib.name)
+ else:
+ link_command.append('-l' + lib.name)
+ return link_command, new_depends
+
+ def _get_dependencies_flags_raw(
+ self, deps: T.Sequence[T.Union['Dependency', build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]],
+ state: 'ModuleState',
+ depends: T.Sequence[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]],
+ include_rpath: bool,
+ use_gir_args: bool,
+ ) -> T.Tuple[OrderedSet[str], OrderedSet[T.Union[str, T.Tuple[str, str]]], OrderedSet[T.Union[str, T.Tuple[str, str]]], OrderedSet[str],
+ T.List[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]]]:
+ cflags: OrderedSet[str] = OrderedSet()
+ # External linker flags that can't be de-duped reliably because they
+ # require two args in order, such as -framework AVFoundation will be stored as a tuple.
+ internal_ldflags: OrderedSet[T.Union[str, T.Tuple[str, str]]] = OrderedSet()
+ external_ldflags: OrderedSet[T.Union[str, T.Tuple[str, str]]] = OrderedSet()
+ gi_includes: OrderedSet[str] = OrderedSet()
+ deps = mesonlib.listify(deps)
+ depends = list(depends)
+
+ for dep in deps:
+ if isinstance(dep, Dependency):
+ girdir = dep.get_variable(pkgconfig='girdir', internal='girdir', default_value='')
+ if girdir:
+ assert isinstance(girdir, str), 'for mypy'
+ gi_includes.update([girdir])
+ if isinstance(dep, InternalDependency):
+ cflags.update(dep.get_compile_args())
+ cflags.update(state.get_include_args(dep.include_directories))
+ for lib in dep.libraries:
+ if isinstance(lib, build.SharedLibrary):
+ _ld, depends = self._get_link_args(state, lib, depends, include_rpath)
+ internal_ldflags.update(_ld)
+ libdepflags = self._get_dependencies_flags_raw(lib.get_external_deps(), state, depends, include_rpath,
+ use_gir_args)
+ cflags.update(libdepflags[0])
+ internal_ldflags.update(libdepflags[1])
+ external_ldflags.update(libdepflags[2])
+ gi_includes.update(libdepflags[3])
+ depends = libdepflags[4]
+ extdepflags = self._get_dependencies_flags_raw(dep.ext_deps, state, depends, include_rpath,
+ use_gir_args)
+ cflags.update(extdepflags[0])
+ internal_ldflags.update(extdepflags[1])
+ external_ldflags.update(extdepflags[2])
+ gi_includes.update(extdepflags[3])
+ depends = extdepflags[4]
+ for source in dep.sources:
+ if isinstance(source, GirTarget):
+ gi_includes.update([os.path.join(state.environment.get_build_dir(),
+ source.get_subdir())])
+ # This should be any dependency other than an internal one.
+ elif isinstance(dep, Dependency):
+ cflags.update(dep.get_compile_args())
+ ldflags = iter(dep.get_link_args(raw=True))
+ for flag in ldflags:
+ if (os.path.isabs(flag) and
+ # For PkgConfigDependency only:
+ getattr(dep, 'is_libtool', False)):
+ lib_dir = os.path.dirname(flag)
+ external_ldflags.update([f'-L{lib_dir}'])
+ if include_rpath:
+ external_ldflags.update([f'-Wl,-rpath {lib_dir}'])
+ libname = os.path.basename(flag)
+ if libname.startswith("lib"):
+ libname = libname[3:]
+ libname = libname.split(".so")[0]
+ flag = f"-l{libname}"
+ # FIXME: Hack to avoid passing some compiler options in
+ if flag.startswith("-W"):
+ continue
+ # If it's a framework arg, slurp the framework name too
+ # to preserve the order of arguments
+ if flag == '-framework':
+ external_ldflags.update([(flag, next(ldflags))])
+ else:
+ external_ldflags.update([flag])
+ elif isinstance(dep, (build.StaticLibrary, build.SharedLibrary)):
+ cflags.update(state.get_include_args(dep.get_include_dirs()))
+ depends.append(dep)
+ else:
+ mlog.log(f'dependency {dep!r} not handled to build gir files')
+ continue
+
+ if use_gir_args and self._gir_has_option('--extra-library'):
+ def fix_ldflags(ldflags: T.Iterable[T.Union[str, T.Tuple[str, str]]]) -> OrderedSet[T.Union[str, T.Tuple[str, str]]]:
+ fixed_ldflags: OrderedSet[T.Union[str, T.Tuple[str, str]]] = OrderedSet()
+ for ldflag in ldflags:
+ if isinstance(ldflag, str) and ldflag.startswith("-l"):
+ ldflag = ldflag.replace('-l', '--extra-library=', 1)
+ fixed_ldflags.add(ldflag)
+ return fixed_ldflags
+ internal_ldflags = fix_ldflags(internal_ldflags)
+ external_ldflags = fix_ldflags(external_ldflags)
+ return cflags, internal_ldflags, external_ldflags, gi_includes, depends
+
+ def _get_dependencies_flags(
+ self, deps: T.Sequence[T.Union['Dependency', build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]],
+ state: 'ModuleState',
+ depends: T.Sequence[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]],
+ include_rpath: bool = False,
+ use_gir_args: bool = False,
+ ) -> T.Tuple[OrderedSet[str], T.List[str], T.List[str], OrderedSet[str],
+ T.List[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]]]:
+
+ cflags, internal_ldflags_raw, external_ldflags_raw, gi_includes, depends = self._get_dependencies_flags_raw(deps, state, depends, include_rpath, use_gir_args)
+ internal_ldflags: T.List[str] = []
+ external_ldflags: T.List[str] = []
+
+ # Extract non-deduplicable argument groups out of the tuples.
+ for ldflag in internal_ldflags_raw:
+ if isinstance(ldflag, str):
+ internal_ldflags.append(ldflag)
+ else:
+ internal_ldflags.extend(ldflag)
+ for ldflag in external_ldflags_raw:
+ if isinstance(ldflag, str):
+ external_ldflags.append(ldflag)
+ else:
+ external_ldflags.extend(ldflag)
+
+ return cflags, internal_ldflags, external_ldflags, gi_includes, depends
+
+ def _unwrap_gir_target(self, girtarget: T.Union[build.Executable, build.StaticLibrary, build.SharedLibrary], state: 'ModuleState'
+ ) -> T.Union[build.Executable, build.StaticLibrary, build.SharedLibrary]:
+ if not isinstance(girtarget, (build.Executable, build.SharedLibrary,
+ build.StaticLibrary)):
+ raise MesonException(f'Gir target must be an executable or library but is "{girtarget}" of type {type(girtarget).__name__}')
+
+ STATIC_BUILD_REQUIRED_VERSION = ">=1.58.1"
+ if isinstance(girtarget, (build.StaticLibrary)) and \
+ not mesonlib.version_compare(
+ self._get_gir_dep(state)[0].get_version(),
+ STATIC_BUILD_REQUIRED_VERSION):
+ raise MesonException('Static libraries can only be introspected with GObject-Introspection ' + STATIC_BUILD_REQUIRED_VERSION)
+
+ return girtarget
+
+ def _devenv_prepend(self, varname: str, value: str) -> None:
+ if self.devenv is None:
+ self.devenv = build.EnvironmentVariables()
+ self.devenv.prepend(varname, [value])
+
+ def get_devenv(self) -> T.Optional[build.EnvironmentVariables]:
+ return self.devenv
+
+ def _get_gir_dep(self, state: 'ModuleState') -> T.Tuple[Dependency, T.Union[build.Executable, 'ExternalProgram', 'OverrideProgram'],
+ T.Union[build.Executable, 'ExternalProgram', 'OverrideProgram']]:
+ if not self.gir_dep:
+ self.gir_dep = state.dependency('gobject-introspection-1.0')
+ self.giscanner = state.find_tool('g-ir-scanner', 'gobject-introspection-1.0', 'g_ir_scanner')
+ self.gicompiler = state.find_tool('g-ir-compiler', 'gobject-introspection-1.0', 'g_ir_compiler')
+ return self.gir_dep, self.giscanner, self.gicompiler
+
+ @functools.lru_cache(maxsize=None)
+ def _gir_has_option(self, option: str) -> bool:
+ exe = self.giscanner
+ if isinstance(exe, OverrideProgram):
+ # Handle overridden g-ir-scanner
+ assert option in {'--extra-library', '--sources-top-dirs'}
+ return True
+ p, o, _ = Popen_safe(exe.get_command() + ['--help'], stderr=subprocess.STDOUT)
+ return p.returncode == 0 and option in o
+
+ # May mutate depends and gir_inc_dirs
+ @staticmethod
+ def _scan_include(state: 'ModuleState', includes: T.List[T.Union[str, GirTarget]]
+ ) -> T.Tuple[T.List[str], T.List[str], T.List[GirTarget]]:
+ ret: T.List[str] = []
+ gir_inc_dirs: T.List[str] = []
+ depends: T.List[GirTarget] = []
+
+ for inc in includes:
+ if isinstance(inc, str):
+ ret += [f'--include={inc}']
+ elif isinstance(inc, GirTarget):
+ gir_inc_dirs .append(os.path.join(state.environment.get_build_dir(), inc.get_subdir()))
+ ret.append(f"--include-uninstalled={os.path.join(inc.get_subdir(), inc.get_basename())}")
+ depends.append(inc)
+
+ return ret, gir_inc_dirs, depends
+
+ @staticmethod
+ def _scan_langs(state: 'ModuleState', langs: T.Iterable[str]) -> T.List[str]:
+ ret: T.List[str] = []
+
+ for lang in langs:
+ link_args = state.environment.coredata.get_external_link_args(MachineChoice.HOST, lang)
+ for link_arg in link_args:
+ if link_arg.startswith('-L'):
+ ret.append(link_arg)
+
+ return ret
+
+ @staticmethod
+ def _scan_gir_targets(state: 'ModuleState', girtargets: T.Sequence[build.BuildTarget]) -> T.List[T.Union[str, build.Executable]]:
+ ret: T.List[T.Union[str, build.Executable]] = []
+
+ for girtarget in girtargets:
+ if isinstance(girtarget, build.Executable):
+ ret += ['--program', girtarget]
+ else:
+ # Because of https://gitlab.gnome.org/GNOME/gobject-introspection/merge_requests/72
+ # we can't use the full path until this is merged.
+ libpath = os.path.join(girtarget.get_subdir(), girtarget.get_filename())
+ # Must use absolute paths here because g-ir-scanner will not
+ # add them to the runtime path list if they're relative. This
+ # means we cannot use @BUILD_ROOT@
+ build_root = state.environment.get_build_dir()
+ if isinstance(girtarget, build.SharedLibrary):
+ # need to put our output directory first as we need to use the
+ # generated libraries instead of any possibly installed system/prefix
+ # ones.
+ ret += ["-L{}/{}".format(build_root, os.path.dirname(libpath))]
+ libname = girtarget.get_basename()
+ else:
+ libname = os.path.join(f"{build_root}/{libpath}")
+ ret += ['--library', libname]
+ # Needed for the following binutils bug:
+ # https://github.com/mesonbuild/meson/issues/1911
+ # However, g-ir-scanner does not understand -Wl,-rpath
+ # so we need to use -L instead
+ for d in state.backend.determine_rpath_dirs(girtarget):
+ d = os.path.join(state.environment.get_build_dir(), d)
+ ret.append('-L' + d)
+
+ return ret
+
+ @staticmethod
+ def _get_girtargets_langs_compilers(girtargets: T.Sequence[build.BuildTarget]) -> T.List[T.Tuple[str, 'Compiler']]:
+ ret: T.List[T.Tuple[str, 'Compiler']] = []
+ for girtarget in girtargets:
+ for lang, compiler in girtarget.compilers.items():
+ # XXX: Can you use g-i with any other language?
+ if lang in {'c', 'cpp', 'objc', 'objcpp', 'd'}:
+ ret.append((lang, compiler))
+ break
+
+ return ret
+
+ @staticmethod
+ def _get_gir_targets_deps(girtargets: T.Sequence[build.BuildTarget]
+ ) -> T.List[T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, Dependency]]:
+ ret: T.List[T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, Dependency]] = []
+ for girtarget in girtargets:
+ ret += girtarget.get_all_link_deps()
+ ret += girtarget.get_external_deps()
+ return ret
+
+ @staticmethod
+ def _get_gir_targets_inc_dirs(girtargets: T.Sequence[build.BuildTarget]) -> OrderedSet[build.IncludeDirs]:
+ ret: OrderedSet = OrderedSet()
+ for girtarget in girtargets:
+ ret.update(girtarget.get_include_dirs())
+ return ret
+
+ @staticmethod
+ def _get_langs_compilers_flags(state: 'ModuleState', langs_compilers: T.List[T.Tuple[str, 'Compiler']]
+ ) -> T.Tuple[T.List[str], T.List[str], T.List[str]]:
+ cflags: T.List[str] = []
+ internal_ldflags: T.List[str] = []
+ external_ldflags: T.List[str] = []
+
+ for lang, compiler in langs_compilers:
+ if state.global_args.get(lang):
+ cflags += state.global_args[lang]
+ if state.project_args.get(lang):
+ cflags += state.project_args[lang]
+ if mesonlib.OptionKey('b_sanitize') in compiler.base_options:
+ sanitize = state.environment.coredata.options[mesonlib.OptionKey('b_sanitize')].value
+ cflags += compiler.sanitizer_compile_args(sanitize)
+ sanitize = sanitize.split(',')
+ # These must be first in ldflags
+ if 'address' in sanitize:
+ internal_ldflags += ['-lasan']
+ if 'thread' in sanitize:
+ internal_ldflags += ['-ltsan']
+ if 'undefined' in sanitize:
+ internal_ldflags += ['-lubsan']
+ # FIXME: Linking directly to lib*san is not recommended but g-ir-scanner
+ # does not understand -f LDFLAGS. https://bugzilla.gnome.org/show_bug.cgi?id=783892
+ # ldflags += compiler.sanitizer_link_args(sanitize)
+
+ return cflags, internal_ldflags, external_ldflags
+
+ @staticmethod
+ def _make_gir_filelist(state: 'ModuleState', srcdir: str, ns: str,
+ nsversion: str, girtargets: T.Sequence[build.BuildTarget],
+ libsources: T.Sequence[T.Union[
+ str, mesonlib.File, build.GeneratedList,
+ build.CustomTarget, build.CustomTargetIndex]]
+ ) -> str:
+ gir_filelist_dir = state.backend.get_target_private_dir_abs(girtargets[0])
+ if not os.path.isdir(gir_filelist_dir):
+ os.mkdir(gir_filelist_dir)
+ gir_filelist_filename = os.path.join(gir_filelist_dir, f'{ns}_{nsversion}_gir_filelist')
+
+ with open(gir_filelist_filename, 'w', encoding='utf-8') as gir_filelist:
+ for s in libsources:
+ if isinstance(s, (build.CustomTarget, build.CustomTargetIndex)):
+ for custom_output in s.get_outputs():
+ gir_filelist.write(os.path.join(state.environment.get_build_dir(),
+ state.backend.get_target_dir(s),
+ custom_output) + '\n')
+ elif isinstance(s, mesonlib.File):
+ gir_filelist.write(s.rel_to_builddir(state.build_to_src) + '\n')
+ elif isinstance(s, build.GeneratedList):
+ for gen_src in s.get_outputs():
+ gir_filelist.write(os.path.join(srcdir, gen_src) + '\n')
+ else:
+ gir_filelist.write(os.path.join(srcdir, s) + '\n')
+
+ return gir_filelist_filename
+
+ @staticmethod
+ def _make_gir_target(
+ state: 'ModuleState',
+ girfile: str,
+ scan_command: T.Sequence[T.Union['FileOrString', Executable, ExternalProgram, OverrideProgram]],
+ generated_files: T.Sequence[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]],
+ depends: T.Sequence[T.Union['FileOrString', build.BuildTarget, 'build.GeneratedTypes', build.StructuredSources]],
+ kwargs: T.Dict[str, T.Any]) -> GirTarget:
+ install = kwargs['install_gir']
+ if install is None:
+ install = kwargs['install']
+
+ install_dir = kwargs['install_dir_gir']
+ if install_dir is None:
+ install_dir = os.path.join(state.environment.get_datadir(), 'gir-1.0')
+ elif install_dir is False:
+ install = False
+
+ # g-ir-scanner uses pkg-config to find libraries such as glib. They could
+ # be built as subproject in which case we need to trick it to use
+ # -uninstalled.pc files Meson generated. It also must respect pkgconfig
+ # settings user could have set in machine file, like PKG_CONFIG_LIBDIR,
+ # SYSROOT, etc.
+ run_env = PkgConfigDependency.get_env(state.environment, MachineChoice.HOST, uninstalled=True)
+
+ return GirTarget(
+ girfile,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ scan_command,
+ generated_files,
+ [girfile],
+ build_by_default=kwargs['build_by_default'],
+ extra_depends=depends,
+ install=install,
+ install_dir=[install_dir],
+ install_tag=['devel'],
+ env=run_env,
+ )
+
+ @staticmethod
+ def _make_typelib_target(state: 'ModuleState', typelib_output: str,
+ typelib_cmd: T.Sequence[T.Union[str, build.Executable, ExternalProgram, build.CustomTarget]],
+ generated_files: T.Sequence[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]],
+ kwargs: T.Dict[str, T.Any]) -> TypelibTarget:
+ install = kwargs['install_typelib']
+ if install is None:
+ install = kwargs['install']
+
+ install_dir = kwargs['install_dir_typelib']
+ if install_dir is None:
+ install_dir = os.path.join(state.environment.get_libdir(), 'girepository-1.0')
+ elif install_dir is False:
+ install = False
+
+ return TypelibTarget(
+ typelib_output,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ typelib_cmd,
+ generated_files,
+ [typelib_output],
+ install=install,
+ install_dir=[install_dir],
+ install_tag=['typelib'],
+ build_by_default=kwargs['build_by_default'],
+ )
+
+ @staticmethod
+ def _gather_typelib_includes_and_update_depends(
+ state: 'ModuleState',
+ deps: T.Sequence[T.Union[Dependency, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]],
+ depends: T.Sequence[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]]
+ ) -> T.Tuple[T.List[str], T.List[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]]]:
+ # Need to recursively add deps on GirTarget sources from our
+ # dependencies and also find the include directories needed for the
+ # typelib generation custom target below.
+ typelib_includes: T.List[str] = []
+ new_depends = list(depends)
+ for dep in deps:
+ # Add a dependency on each GirTarget listed in dependencies and add
+ # the directory where it will be generated to the typelib includes
+ if isinstance(dep, InternalDependency):
+ for source in dep.sources:
+ if isinstance(source, GirTarget) and source not in depends:
+ new_depends.append(source)
+ subdir = os.path.join(state.environment.get_build_dir(),
+ source.get_subdir())
+ if subdir not in typelib_includes:
+ typelib_includes.append(subdir)
+ # Do the same, but for dependencies of dependencies. These are
+ # stored in the list of generated sources for each link dep (from
+ # girtarget.get_all_link_deps() above).
+ # FIXME: Store this in the original form from declare_dependency()
+ # so it can be used here directly.
+ elif isinstance(dep, build.SharedLibrary):
+ for g_source in dep.generated:
+ if isinstance(g_source, GirTarget):
+ subdir = os.path.join(state.environment.get_build_dir(),
+ g_source.get_subdir())
+ if subdir not in typelib_includes:
+ typelib_includes.append(subdir)
+ if isinstance(dep, Dependency):
+ girdir = dep.get_variable(pkgconfig='girdir', internal='girdir', default_value='')
+ assert isinstance(girdir, str), 'for mypy'
+ if girdir and girdir not in typelib_includes:
+ typelib_includes.append(girdir)
+ return typelib_includes, new_depends
+
+ @staticmethod
+ def _get_external_args_for_langs(state: 'ModuleState', langs: T.List[str]) -> T.List[str]:
+ ret: T.List[str] = []
+ for lang in langs:
+ ret += mesonlib.listify(state.environment.coredata.get_external_args(MachineChoice.HOST, lang))
+ return ret
+
+ @staticmethod
+ def _get_scanner_cflags(cflags: T.Iterable[str]) -> T.Iterable[str]:
+ 'g-ir-scanner only accepts -I/-D/-U; must ignore all other flags'
+ for f in cflags:
+ # _FORTIFY_SOURCE depends on / works together with -O, on the other hand this
+ # just invokes the preprocessor anyway
+ if f.startswith(('-D', '-U', '-I')) and not f.startswith('-D_FORTIFY_SOURCE'):
+ yield f
+
+ @staticmethod
+ def _get_scanner_ldflags(ldflags: T.Iterable[str]) -> T.Iterable[str]:
+ 'g-ir-scanner only accepts -L/-l; must ignore -F and other linker flags'
+ for f in ldflags:
+ if f.startswith(('-L', '-l', '--extra-library')):
+ yield f
+
+ @typed_pos_args('gnome.generate_gir', varargs=(build.Executable, build.SharedLibrary, build.StaticLibrary), min_varargs=1)
+ @typed_kwargs(
+ 'gnome.generate_gir',
+ INSTALL_KW,
+ _BUILD_BY_DEFAULT.evolve(since='0.40.0'),
+ _EXTRA_ARGS_KW,
+ KwargInfo('dependencies', ContainerTypeInfo(list, Dependency), default=[], listify=True),
+ KwargInfo('export_packages', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('fatal_warnings', bool, default=False, since='0.55.0'),
+ KwargInfo('header', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('identifier_prefix', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('include_directories', ContainerTypeInfo(list, (str, build.IncludeDirs)), default=[], listify=True),
+ KwargInfo('includes', ContainerTypeInfo(list, (str, GirTarget)), default=[], listify=True),
+ KwargInfo('install_gir', (bool, NoneType), since='0.61.0'),
+ KwargInfo('install_dir_gir', (str, bool, NoneType),
+ deprecated_values={False: ('0.61.0', 'Use install_gir to disable installation')},
+ validator=lambda x: 'as boolean can only be false' if x is True else None),
+ KwargInfo('install_typelib', (bool, NoneType), since='0.61.0'),
+ KwargInfo('install_dir_typelib', (str, bool, NoneType),
+ deprecated_values={False: ('0.61.0', 'Use install_typelib to disable installation')},
+ validator=lambda x: 'as boolean can only be false' if x is True else None),
+ KwargInfo('link_with', ContainerTypeInfo(list, (build.SharedLibrary, build.StaticLibrary)), default=[], listify=True),
+ KwargInfo('namespace', str, required=True),
+ KwargInfo('nsversion', str, required=True),
+ KwargInfo('sources', ContainerTypeInfo(list, (str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex)), default=[], listify=True),
+ KwargInfo('symbol_prefix', ContainerTypeInfo(list, str), default=[], listify=True),
+ )
+ def generate_gir(self, state: 'ModuleState', args: T.Tuple[T.List[T.Union[build.Executable, build.SharedLibrary, build.StaticLibrary]]],
+ kwargs: 'GenerateGir') -> ModuleReturnValue:
+ girtargets = [self._unwrap_gir_target(arg, state) for arg in args[0]]
+ if len(girtargets) > 1 and any(isinstance(el, build.Executable) for el in girtargets):
+ raise MesonException('generate_gir only accepts a single argument when one of the arguments is an executable')
+
+ gir_dep, giscanner, gicompiler = self._get_gir_dep(state)
+
+ ns = kwargs['namespace']
+ nsversion = kwargs['nsversion']
+ libsources = kwargs['sources']
+
+ girfile = f'{ns}-{nsversion}.gir'
+ srcdir = os.path.join(state.environment.get_source_dir(), state.subdir)
+ builddir = os.path.join(state.environment.get_build_dir(), state.subdir)
+
+ depends: T.List[T.Union['FileOrString', 'build.GeneratedTypes', build.BuildTarget, build.StructuredSources]] = []
+ depends.extend(gir_dep.sources)
+ depends.extend(girtargets)
+
+ langs_compilers = self._get_girtargets_langs_compilers(girtargets)
+ cflags, internal_ldflags, external_ldflags = self._get_langs_compilers_flags(state, langs_compilers)
+ deps = self._get_gir_targets_deps(girtargets)
+ deps += kwargs['dependencies']
+ deps += [gir_dep]
+ typelib_includes, depends = self._gather_typelib_includes_and_update_depends(state, deps, depends)
+ # ldflags will be misinterpreted by gir scanner (showing
+ # spurious dependencies) but building GStreamer fails if they
+ # are not used here.
+ dep_cflags, dep_internal_ldflags, dep_external_ldflags, gi_includes, depends = \
+ self._get_dependencies_flags(deps, state, depends, use_gir_args=True)
+ scan_cflags = []
+ scan_cflags += list(self._get_scanner_cflags(cflags))
+ scan_cflags += list(self._get_scanner_cflags(dep_cflags))
+ scan_cflags += list(self._get_scanner_cflags(self._get_external_args_for_langs(state, [lc[0] for lc in langs_compilers])))
+ scan_internal_ldflags = []
+ scan_internal_ldflags += list(self._get_scanner_ldflags(internal_ldflags))
+ scan_internal_ldflags += list(self._get_scanner_ldflags(dep_internal_ldflags))
+ scan_external_ldflags = []
+ scan_external_ldflags += list(self._get_scanner_ldflags(external_ldflags))
+ scan_external_ldflags += list(self._get_scanner_ldflags(dep_external_ldflags))
+ girtargets_inc_dirs = self._get_gir_targets_inc_dirs(girtargets)
+ inc_dirs = kwargs['include_directories']
+
+ gir_inc_dirs: T.List[str] = []
+
+ scan_command: T.List[T.Union[str, build.Executable, 'ExternalProgram', 'OverrideProgram']] = [giscanner]
+ scan_command += ['--quiet']
+ scan_command += ['--no-libtool']
+ scan_command += ['--namespace=' + ns, '--nsversion=' + nsversion]
+ scan_command += ['--warn-all']
+ scan_command += ['--output', '@OUTPUT@']
+ scan_command += [f'--c-include={h}' for h in kwargs['header']]
+ scan_command += kwargs['extra_args']
+ scan_command += ['-I' + srcdir, '-I' + builddir]
+ scan_command += state.get_include_args(girtargets_inc_dirs)
+ scan_command += ['--filelist=' + self._make_gir_filelist(state, srcdir, ns, nsversion, girtargets, libsources)]
+ for l in kwargs['link_with']:
+ _cflags, depends = self._get_link_args(state, l, depends, use_gir_args=True)
+ scan_command.extend(_cflags)
+ _cmd, _ginc, _deps = self._scan_include(state, kwargs['includes'])
+ scan_command.extend(_cmd)
+ gir_inc_dirs.extend(_ginc)
+ depends.extend(_deps)
+
+ scan_command += [f'--symbol-prefix={p}' for p in kwargs['symbol_prefix']]
+ scan_command += [f'--identifier-prefix={p}' for p in kwargs['identifier_prefix']]
+ scan_command += [f'--pkg-export={p}' for p in kwargs['export_packages']]
+ scan_command += ['--cflags-begin']
+ scan_command += scan_cflags
+ scan_command += ['--cflags-end']
+ scan_command += state.get_include_args(inc_dirs)
+ scan_command += state.get_include_args(itertools.chain(gi_includes, gir_inc_dirs, inc_dirs), prefix='--add-include-path=')
+ scan_command += list(scan_internal_ldflags)
+ scan_command += self._scan_gir_targets(state, girtargets)
+ scan_command += self._scan_langs(state, [lc[0] for lc in langs_compilers])
+ scan_command += list(scan_external_ldflags)
+
+ if self._gir_has_option('--sources-top-dirs'):
+ scan_command += ['--sources-top-dirs', os.path.join(state.environment.get_source_dir(), state.root_subdir)]
+ scan_command += ['--sources-top-dirs', os.path.join(state.environment.get_build_dir(), state.root_subdir)]
+
+ if '--warn-error' in scan_command:
+ FeatureDeprecated.single_use('gnome.generate_gir argument --warn-error', '0.55.0',
+ state.subproject, 'Use "fatal_warnings" keyword argument', state.current_node)
+ if kwargs['fatal_warnings']:
+ scan_command.append('--warn-error')
+
+ generated_files = [f for f in libsources if isinstance(f, (GeneratedList, CustomTarget, CustomTargetIndex))]
+
+ scan_target = self._make_gir_target(
+ state, girfile, scan_command, generated_files, depends,
+ # We have to cast here because mypy can't figure this out
+ T.cast('T.Dict[str, T.Any]', kwargs))
+
+ typelib_output = f'{ns}-{nsversion}.typelib'
+ typelib_cmd = [gicompiler, scan_target, '--output', '@OUTPUT@']
+ typelib_cmd += state.get_include_args(gir_inc_dirs, prefix='--includedir=')
+
+ for incdir in typelib_includes:
+ typelib_cmd += ["--includedir=" + incdir]
+
+ typelib_target = self._make_typelib_target(state, typelib_output, typelib_cmd, generated_files, T.cast('T.Dict[str, T.Any]', kwargs))
+
+ self._devenv_prepend('GI_TYPELIB_PATH', os.path.join(state.environment.get_build_dir(), state.subdir))
+
+ rv = [scan_target, typelib_target]
+
+ return ModuleReturnValue(rv, rv)
+
+ @noPosargs
+ @typed_kwargs('gnome.compile_schemas', _BUILD_BY_DEFAULT.evolve(since='0.40.0'), DEPEND_FILES_KW)
+ def compile_schemas(self, state: 'ModuleState', args: T.List['TYPE_var'], kwargs: 'CompileSchemas') -> ModuleReturnValue:
+ srcdir = os.path.join(state.build_to_src, state.subdir)
+ outdir = state.subdir
+
+ cmd: T.List[T.Union[ExternalProgram, str]] = [state.find_program('glib-compile-schemas'), '--targetdir', outdir, srcdir]
+ if state.subdir == '':
+ targetname = 'gsettings-compile'
+ else:
+ targetname = 'gsettings-compile-' + state.subdir.replace('/', '_')
+ target_g = build.CustomTarget(
+ targetname,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ cmd,
+ [],
+ ['gschemas.compiled'],
+ build_by_default=kwargs['build_by_default'],
+ depend_files=kwargs['depend_files'],
+ )
+ self._devenv_prepend('GSETTINGS_SCHEMA_DIR', os.path.join(state.environment.get_build_dir(), state.subdir))
+ return ModuleReturnValue(target_g, [target_g])
+
+ @typed_pos_args('gnome.yelp', str, varargs=str)
+ @typed_kwargs(
+ 'gnome.yelp',
+ KwargInfo(
+ 'languages', ContainerTypeInfo(list, str),
+ listify=True, default=[],
+ deprecated='0.43.0',
+ deprecated_message='Use a LINGUAS file in the source directory instead',
+ ),
+ KwargInfo('media', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('sources', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('symlink_media', bool, default=True),
+ )
+ def yelp(self, state: 'ModuleState', args: T.Tuple[str, T.List[str]], kwargs: 'Yelp') -> ModuleReturnValue:
+ project_id = args[0]
+ sources = kwargs['sources']
+ if args[1]:
+ FeatureDeprecated.single_use('gnome.yelp more than one positional argument', '0.60.0',
+ state.subproject, 'use the "sources" keyword argument instead.', state.current_node)
+ if not sources:
+ sources = args[1]
+ if not sources:
+ raise MesonException('Yelp requires a list of sources')
+ elif args[1]:
+ mlog.warning('"gnome.yelp" ignores positional sources arguments when the "sources" keyword argument is set')
+ sources_files = [mesonlib.File.from_source_file(state.environment.source_dir,
+ os.path.join(state.subdir, 'C'),
+ s) for s in sources]
+
+ langs = kwargs['languages']
+ if not langs:
+ langs = read_linguas(os.path.join(state.environment.source_dir, state.subdir))
+
+ media = kwargs['media']
+ symlinks = kwargs['symlink_media']
+ targets: T.List[T.Union['build.Target', build.Data, build.SymlinkData]] = []
+ potargets: T.List[build.RunTarget] = []
+
+ itstool = state.find_program('itstool')
+ msgmerge = state.find_program('msgmerge')
+ msgfmt = state.find_program('msgfmt')
+
+ install_dir = os.path.join(state.environment.get_datadir(), 'help')
+ c_install_dir = os.path.join(install_dir, 'C', project_id)
+ c_data = build.Data(sources_files, c_install_dir, c_install_dir,
+ mesonlib.FileMode(), state.subproject, install_tag='doc')
+ targets.append(c_data)
+
+ media_files: T.List[mesonlib.File] = []
+ for m in media:
+ f = mesonlib.File.from_source_file(state.environment.source_dir,
+ os.path.join(state.subdir, 'C'), m)
+ media_files.append(f)
+ m_install_dir = os.path.join(c_install_dir, os.path.dirname(m))
+ m_data = build.Data([f], m_install_dir, m_install_dir,
+ mesonlib.FileMode(), state.subproject, install_tag='doc')
+ targets.append(m_data)
+
+ pot_file = os.path.join('@SOURCE_ROOT@', state.subdir, 'C', project_id + '.pot')
+ pot_sources = [os.path.join('@SOURCE_ROOT@', state.subdir, 'C', s) for s in sources]
+ pot_args: T.List[T.Union['ExternalProgram', str]] = [itstool, '-o', pot_file]
+ pot_args.extend(pot_sources)
+ pottarget = build.RunTarget(f'help-{project_id}-pot', pot_args, [],
+ os.path.join(state.subdir, 'C'), state.subproject,
+ state.environment)
+ targets.append(pottarget)
+
+ for l in langs:
+ l_subdir = os.path.join(state.subdir, l)
+ l_install_dir = os.path.join(install_dir, l, project_id)
+
+ for i, m in enumerate(media):
+ m_dir = os.path.dirname(m)
+ m_install_dir = os.path.join(l_install_dir, m_dir)
+ l_data: T.Union[build.Data, build.SymlinkData]
+ if symlinks:
+ link_target = os.path.join(os.path.relpath(c_install_dir, start=m_install_dir), m)
+ l_data = build.SymlinkData(link_target, os.path.basename(m),
+ m_install_dir, state.subproject, install_tag='doc')
+ else:
+ try:
+ m_file = mesonlib.File.from_source_file(state.environment.source_dir, l_subdir, m)
+ except MesonException:
+ m_file = media_files[i]
+ l_data = build.Data([m_file], m_install_dir, m_install_dir,
+ mesonlib.FileMode(), state.subproject, install_tag='doc')
+ targets.append(l_data)
+
+ po_file = l + '.po'
+ po_args: T.List[T.Union['ExternalProgram', str]] = [
+ msgmerge, '-q', '-o',
+ os.path.join('@SOURCE_ROOT@', l_subdir, po_file),
+ os.path.join('@SOURCE_ROOT@', l_subdir, po_file), pot_file]
+ potarget = build.RunTarget(f'help-{project_id}-{l}-update-po',
+ po_args, [pottarget], l_subdir, state.subproject,
+ state.environment)
+ targets.append(potarget)
+ potargets.append(potarget)
+
+ gmo_file = project_id + '-' + l + '.gmo'
+ gmotarget = build.CustomTarget(
+ f'help-{project_id}-{l}-gmo',
+ l_subdir,
+ state.subproject,
+ state.environment,
+ [msgfmt, '@INPUT@', '-o', '@OUTPUT@'],
+ [po_file],
+ [gmo_file],
+ install_tag=['doc'],
+ )
+ targets.append(gmotarget)
+
+ mergetarget = build.CustomTarget(
+ f'help-{project_id}-{l}',
+ l_subdir,
+ state.subproject,
+ state.environment,
+ [itstool, '-m', os.path.join(l_subdir, gmo_file), '--lang', l, '-o', '@OUTDIR@', '@INPUT@'],
+ sources_files,
+ sources,
+ extra_depends=[gmotarget],
+ install=True,
+ install_dir=[l_install_dir],
+ install_tag=['doc'],
+ )
+ targets.append(mergetarget)
+
+ allpotarget = build.AliasTarget(f'help-{project_id}-update-po', potargets,
+ state.subdir, state.subproject, state.environment)
+ targets.append(allpotarget)
+
+ return ModuleReturnValue(None, targets)
+
+ @typed_pos_args('gnome.gtkdoc', str)
+ @typed_kwargs(
+ 'gnome.gtkdoc',
+ KwargInfo('c_args', ContainerTypeInfo(list, str), since='0.48.0', default=[], listify=True),
+ KwargInfo('check', bool, default=False, since='0.52.0'),
+ KwargInfo('content_files', ContainerTypeInfo(list, (str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex)), default=[], listify=True),
+ KwargInfo(
+ 'dependencies',
+ ContainerTypeInfo(list, (Dependency, build.SharedLibrary, build.StaticLibrary)),
+ listify=True, default=[]),
+ KwargInfo('expand_content_files', ContainerTypeInfo(list, (str, mesonlib.File)), default=[], listify=True),
+ KwargInfo('fixxref_args', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('gobject_typesfile', ContainerTypeInfo(list, (str, mesonlib.File)), default=[], listify=True),
+ KwargInfo('html_args', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('html_assets', ContainerTypeInfo(list, (str, mesonlib.File)), default=[], listify=True),
+ KwargInfo('ignore_headers', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo(
+ 'include_directories',
+ ContainerTypeInfo(list, (str, build.IncludeDirs)),
+ listify=True, default=[]),
+ KwargInfo('install', bool, default=True),
+ KwargInfo('install_dir', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('main_sgml', (str, NoneType)),
+ KwargInfo('main_xml', (str, NoneType)),
+ KwargInfo('mkdb_args', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo(
+ 'mode', str, default='auto', since='0.37.0',
+ validator=in_set_validator({'xml', 'sgml', 'none', 'auto'})),
+ KwargInfo('module_version', str, default='', since='0.48.0'),
+ KwargInfo('namespace', str, default='', since='0.37.0'),
+ KwargInfo('scan_args', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('scanobjs_args', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('src_dir', ContainerTypeInfo(list, (str, build.IncludeDirs)), listify=True, required=True),
+ )
+ def gtkdoc(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'GtkDoc') -> ModuleReturnValue:
+ modulename = args[0]
+ main_file = kwargs['main_sgml']
+ main_xml = kwargs['main_xml']
+ if main_xml is not None:
+ if main_file is not None:
+ raise InvalidArguments('gnome.gtkdoc: main_xml and main_sgml are exclusive arguments')
+ main_file = main_xml
+ moduleversion = kwargs['module_version']
+ targetname = modulename + ('-' + moduleversion if moduleversion else '') + '-doc'
+ command = state.environment.get_build_command()
+
+ namespace = kwargs['namespace']
+
+ def abs_filenames(files: T.Iterable['FileOrString']) -> T.Iterator[str]:
+ for f in files:
+ if isinstance(f, mesonlib.File):
+ yield f.absolute_path(state.environment.get_source_dir(), state.environment.get_build_dir())
+ else:
+ yield os.path.join(state.environment.get_source_dir(), state.subdir, f)
+
+ src_dirs = kwargs['src_dir']
+ header_dirs: T.List[str] = []
+ for src_dir in src_dirs:
+ if isinstance(src_dir, build.IncludeDirs):
+ header_dirs.extend(src_dir.to_string_list(state.environment.get_source_dir(),
+ state.environment.get_build_dir()))
+ else:
+ header_dirs.append(src_dir)
+
+ t_args: T.List[str] = [
+ '--internal', 'gtkdoc',
+ '--sourcedir=' + state.environment.get_source_dir(),
+ '--builddir=' + state.environment.get_build_dir(),
+ '--subdir=' + state.subdir,
+ '--headerdirs=' + '@@'.join(header_dirs),
+ '--mainfile=' + main_file,
+ '--modulename=' + modulename,
+ '--moduleversion=' + moduleversion,
+ '--mode=' + kwargs['mode']]
+ for tool in ['scan', 'scangobj', 'mkdb', 'mkhtml', 'fixxref']:
+ program_name = 'gtkdoc-' + tool
+ program = state.find_program(program_name)
+ path = program.get_path()
+ t_args.append(f'--{program_name}={path}')
+ if namespace:
+ t_args.append('--namespace=' + namespace)
+ exe_wrapper = state.environment.get_exe_wrapper()
+ if exe_wrapper:
+ t_args.append('--run=' + ' '.join(exe_wrapper.get_command()))
+ t_args.append(f'--htmlargs={"@@".join(kwargs["html_args"])}')
+ t_args.append(f'--scanargs={"@@".join(kwargs["scan_args"])}')
+ t_args.append(f'--scanobjsargs={"@@".join(kwargs["scanobjs_args"])}')
+ t_args.append(f'--gobjects-types-file={"@@".join(abs_filenames(kwargs["gobject_typesfile"]))}')
+ t_args.append(f'--fixxrefargs={"@@".join(kwargs["fixxref_args"])}')
+ t_args.append(f'--mkdbargs={"@@".join(kwargs["mkdb_args"])}')
+ t_args.append(f'--html-assets={"@@".join(abs_filenames(kwargs["html_assets"]))}')
+
+ depends: T.List['build.GeneratedTypes'] = []
+ content_files = []
+ for s in kwargs['content_files']:
+ if isinstance(s, (build.CustomTarget, build.CustomTargetIndex)):
+ depends.append(s)
+ for o in s.get_outputs():
+ content_files.append(os.path.join(state.environment.get_build_dir(),
+ state.backend.get_target_dir(s),
+ o))
+ elif isinstance(s, mesonlib.File):
+ content_files.append(s.absolute_path(state.environment.get_source_dir(),
+ state.environment.get_build_dir()))
+ elif isinstance(s, build.GeneratedList):
+ depends.append(s)
+ for gen_src in s.get_outputs():
+ content_files.append(os.path.join(state.environment.get_source_dir(),
+ state.subdir,
+ gen_src))
+ else:
+ content_files.append(os.path.join(state.environment.get_source_dir(),
+ state.subdir,
+ s))
+ t_args += ['--content-files=' + '@@'.join(content_files)]
+
+ t_args.append(f'--expand-content-files={"@@".join(abs_filenames(kwargs["expand_content_files"]))}')
+ t_args.append(f'--ignore-headers={"@@".join(kwargs["ignore_headers"])}')
+ t_args.append(f'--installdir={"@@".join(kwargs["install_dir"])}')
+ build_args, new_depends = self._get_build_args(kwargs['c_args'], kwargs['include_directories'],
+ kwargs['dependencies'], state, depends)
+ t_args.extend(build_args)
+ new_depends.extend(depends)
+ custom_target = build.CustomTarget(
+ targetname,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ command + t_args,
+ [],
+ [f'{modulename}-decl.txt'],
+ build_always_stale=True,
+ extra_depends=new_depends,
+ )
+ alias_target = build.AliasTarget(targetname, [custom_target], state.subdir, state.subproject, state.environment)
+ if kwargs['check']:
+ check_cmd = state.find_program('gtkdoc-check')
+ check_env = ['DOC_MODULE=' + modulename,
+ 'DOC_MAIN_SGML_FILE=' + main_file]
+ check_args = (targetname + '-check', check_cmd)
+ check_workdir = os.path.join(state.environment.get_build_dir(), state.subdir)
+ state.test(check_args, env=check_env, workdir=check_workdir, depends=[custom_target])
+ res: T.List[T.Union[build.Target, build.ExecutableSerialisation]] = [custom_target, alias_target]
+ if kwargs['install']:
+ res.append(state.backend.get_executable_serialisation(command + t_args, tag='doc'))
+ return ModuleReturnValue(custom_target, res)
+
+ def _get_build_args(self, c_args: T.List[str], inc_dirs: T.List[T.Union[str, build.IncludeDirs]],
+ deps: T.List[T.Union[Dependency, build.SharedLibrary, build.StaticLibrary]],
+ state: 'ModuleState',
+ depends: T.Sequence[T.Union[build.BuildTarget, 'build.GeneratedTypes']]) -> T.Tuple[
+ T.List[str], T.List[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]]]:
+ args: T.List[str] = []
+ cflags = c_args.copy()
+ deps_cflags, internal_ldflags, external_ldflags, _gi_includes, new_depends = \
+ self._get_dependencies_flags(deps, state, depends, include_rpath=True)
+
+ cflags.extend(deps_cflags)
+ cflags.extend(state.get_include_args(inc_dirs))
+ ldflags: T.List[str] = []
+ ldflags.extend(internal_ldflags)
+ ldflags.extend(external_ldflags)
+
+ cflags.extend(state.environment.coredata.get_external_args(MachineChoice.HOST, 'c'))
+ ldflags.extend(state.environment.coredata.get_external_link_args(MachineChoice.HOST, 'c'))
+ compiler = state.environment.coredata.compilers[MachineChoice.HOST]['c']
+
+ compiler_flags = self._get_langs_compilers_flags(state, [('c', compiler)])
+ cflags.extend(compiler_flags[0])
+ ldflags.extend(compiler_flags[1])
+ ldflags.extend(compiler_flags[2])
+ if compiler:
+ args += ['--cc=%s' % join_args(compiler.get_exelist())]
+ args += ['--ld=%s' % join_args(compiler.get_linker_exelist())]
+ if cflags:
+ args += ['--cflags=%s' % join_args(cflags)]
+ if ldflags:
+ args += ['--ldflags=%s' % join_args(ldflags)]
+
+ return args, new_depends
+
+ @noKwargs
+ @typed_pos_args('gnome.gtkdoc_html_dir', str)
+ def gtkdoc_html_dir(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> str:
+ return os.path.join('share/gtk-doc/html', args[0])
+
+ @typed_pos_args('gnome.gdbus_codegen', str, optargs=[(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)])
+ @typed_kwargs(
+ 'gnome.gdbus_codegen',
+ _BUILD_BY_DEFAULT.evolve(since='0.40.0'),
+ SOURCES_KW.evolve(since='0.46.0'),
+ KwargInfo('extra_args', ContainerTypeInfo(list, str), since='0.47.0', default=[], listify=True),
+ KwargInfo('interface_prefix', (str, NoneType)),
+ KwargInfo('namespace', (str, NoneType)),
+ KwargInfo('object_manager', bool, default=False),
+ KwargInfo(
+ 'annotations', ContainerTypeInfo(list, (list, str)),
+ default=[],
+ validator=annotations_validator,
+ convertor=lambda x: [x] if x and isinstance(x[0], str) else x,
+ ),
+ KwargInfo('install_header', bool, default=False, since='0.46.0'),
+ KwargInfo('docbook', (str, NoneType)),
+ KwargInfo(
+ 'autocleanup', str, default='default', since='0.47.0',
+ validator=in_set_validator({'all', 'none', 'objects'})),
+ INSTALL_DIR_KW.evolve(since='0.46.0')
+ )
+ def gdbus_codegen(self, state: 'ModuleState', args: T.Tuple[str, T.Optional[T.Union['FileOrString', build.GeneratedTypes]]],
+ kwargs: 'GdbusCodegen') -> ModuleReturnValue:
+ namebase = args[0]
+ xml_files: T.List[T.Union['FileOrString', build.GeneratedTypes]] = [args[1]] if args[1] else []
+ cmd: T.List[T.Union['ExternalProgram', str]] = [state.find_program('gdbus-codegen')]
+ cmd.extend(kwargs['extra_args'])
+
+ # Autocleanup supported?
+ glib_version = self._get_native_glib_version(state)
+ if not mesonlib.version_compare(glib_version, '>= 2.49.1'):
+ # Warn if requested, silently disable if not
+ if kwargs['autocleanup'] != 'default':
+ mlog.warning(f'Glib version ({glib_version}) is too old to support the \'autocleanup\' '
+ 'kwarg, need 2.49.1 or newer')
+ else:
+ # Handle legacy glib versions that don't have autocleanup
+ ac = kwargs['autocleanup']
+ if ac == 'default':
+ ac = 'all'
+ cmd.extend(['--c-generate-autocleanup', ac])
+
+ if kwargs['interface_prefix'] is not None:
+ cmd.extend(['--interface-prefix', kwargs['interface_prefix']])
+ if kwargs['namespace'] is not None:
+ cmd.extend(['--c-namespace', kwargs['namespace']])
+ if kwargs['object_manager']:
+ cmd.extend(['--c-generate-object-manager'])
+ xml_files.extend(kwargs['sources'])
+ build_by_default = kwargs['build_by_default']
+
+ # Annotations are a bit ugly in that they are a list of lists of strings...
+ for annot in kwargs['annotations']:
+ cmd.append('--annotate')
+ cmd.extend(annot)
+
+ targets = []
+ install_header = kwargs['install_header']
+ install_dir = kwargs['install_dir'] or state.environment.coredata.get_option(mesonlib.OptionKey('includedir'))
+ assert isinstance(install_dir, str), 'for mypy'
+
+ output = namebase + '.c'
+ # Added in https://gitlab.gnome.org/GNOME/glib/commit/e4d68c7b3e8b01ab1a4231bf6da21d045cb5a816 (2.55.2)
+ # Fixed in https://gitlab.gnome.org/GNOME/glib/commit/cd1f82d8fc741a2203582c12cc21b4dacf7e1872 (2.56.2)
+ if mesonlib.version_compare(glib_version, '>= 2.56.2'):
+ c_cmd = cmd + ['--body', '--output', '@OUTPUT@', '@INPUT@']
+ else:
+ if kwargs['docbook'] is not None:
+ docbook = kwargs['docbook']
+
+ cmd += ['--generate-docbook', docbook]
+
+ # https://git.gnome.org/browse/glib/commit/?id=ee09bb704fe9ccb24d92dd86696a0e6bb8f0dc1a
+ if mesonlib.version_compare(glib_version, '>= 2.51.3'):
+ cmd += ['--output-directory', '@OUTDIR@', '--generate-c-code', namebase, '@INPUT@']
+ else:
+ self._print_gdbus_warning()
+ cmd += ['--generate-c-code', '@OUTDIR@/' + namebase, '@INPUT@']
+ c_cmd = cmd
+
+ cfile_custom_target = build.CustomTarget(
+ output,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ c_cmd,
+ xml_files,
+ [output],
+ build_by_default=build_by_default,
+ )
+ targets.append(cfile_custom_target)
+
+ output = namebase + '.h'
+ if mesonlib.version_compare(glib_version, '>= 2.56.2'):
+ hfile_cmd = cmd + ['--header', '--output', '@OUTPUT@', '@INPUT@']
+ depends = []
+ else:
+ hfile_cmd = cmd
+ depends = [cfile_custom_target]
+
+ hfile_custom_target = build.CustomTarget(
+ output,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ hfile_cmd,
+ xml_files,
+ [output],
+ build_by_default=build_by_default,
+ extra_depends=depends,
+ install=install_header,
+ install_dir=[install_dir],
+ install_tag=['devel'],
+ )
+ targets.append(hfile_custom_target)
+
+ if kwargs['docbook'] is not None:
+ docbook = kwargs['docbook']
+ # The docbook output is always ${docbook}-${name_of_xml_file}
+ output = namebase + '-docbook'
+ outputs = []
+ for f in xml_files:
+ outputs.append('{}-{}'.format(docbook, os.path.basename(str(f))))
+
+ if mesonlib.version_compare(glib_version, '>= 2.56.2'):
+ docbook_cmd = cmd + ['--output-directory', '@OUTDIR@', '--generate-docbook', docbook, '@INPUT@']
+ depends = []
+ else:
+ docbook_cmd = cmd
+ depends = [cfile_custom_target]
+
+ docbook_custom_target = build.CustomTarget(
+ output,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ docbook_cmd,
+ xml_files,
+ outputs,
+ build_by_default=build_by_default,
+ extra_depends=depends,
+ )
+ targets.append(docbook_custom_target)
+
+ return ModuleReturnValue(targets, targets)
+
+ @typed_pos_args('gnome.mkenums', str)
+ @typed_kwargs(
+ 'gnome.mkenums',
+ *_MK_ENUMS_COMMON_KWS,
+ DEPENDS_KW,
+ KwargInfo('c_template', (str, mesonlib.File, NoneType)),
+ KwargInfo('h_template', (str, mesonlib.File, NoneType)),
+ KwargInfo('comments', (str, NoneType)),
+ KwargInfo('eprod', (str, NoneType)),
+ KwargInfo('fhead', (str, NoneType)),
+ KwargInfo('fprod', (str, NoneType)),
+ KwargInfo('ftail', (str, NoneType)),
+ KwargInfo('vhead', (str, NoneType)),
+ KwargInfo('vprod', (str, NoneType)),
+ KwargInfo('vtail', (str, NoneType)),
+ )
+ def mkenums(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'MkEnums') -> ModuleReturnValue:
+ basename = args[0]
+
+ c_template = kwargs['c_template']
+ if isinstance(c_template, mesonlib.File):
+ c_template = c_template.absolute_path(state.environment.source_dir, state.environment.build_dir)
+ h_template = kwargs['h_template']
+ if isinstance(h_template, mesonlib.File):
+ h_template = h_template.absolute_path(state.environment.source_dir, state.environment.build_dir)
+
+ cmd: T.List[str] = []
+ known_kwargs = ['comments', 'eprod', 'fhead', 'fprod', 'ftail',
+ 'identifier_prefix', 'symbol_prefix',
+ 'vhead', 'vprod', 'vtail']
+ for arg in known_kwargs:
+ # mypy can't figure this out
+ if kwargs[arg]: # type: ignore
+ cmd += ['--' + arg.replace('_', '-'), kwargs[arg]] # type: ignore
+
+ targets: T.List[CustomTarget] = []
+
+ h_target: T.Optional[CustomTarget] = None
+ if h_template is not None:
+ h_output = os.path.basename(os.path.splitext(h_template)[0])
+ # We always set template as the first element in the source array
+ # so --template consumes it.
+ h_cmd = cmd + ['--template', '@INPUT@']
+ h_sources: T.List[T.Union[FileOrString, 'build.GeneratedTypes']] = [h_template]
+ h_sources.extend(kwargs['sources'])
+ h_target = self._make_mkenum_impl(
+ state, h_sources, h_output, h_cmd, install=kwargs['install_header'],
+ install_dir=kwargs['install_dir'])
+ targets.append(h_target)
+
+ if c_template is not None:
+ c_output = os.path.basename(os.path.splitext(c_template)[0])
+ # We always set template as the first element in the source array
+ # so --template consumes it.
+ c_cmd = cmd + ['--template', '@INPUT@']
+ c_sources: T.List[T.Union[FileOrString, 'build.GeneratedTypes']] = [c_template]
+ c_sources.extend(kwargs['sources'])
+
+ depends = kwargs['depends'].copy()
+ if h_target is not None:
+ depends.append(h_target)
+ c_target = self._make_mkenum_impl(
+ state, c_sources, c_output, c_cmd, depends=depends)
+ targets.insert(0, c_target)
+
+ if c_template is None and h_template is None:
+ generic_cmd = cmd + ['@INPUT@']
+ target = self._make_mkenum_impl(
+ state, kwargs['sources'], basename, generic_cmd,
+ install=kwargs['install_header'],
+ install_dir=kwargs['install_dir'])
+ return ModuleReturnValue(target, [target])
+ else:
+ return ModuleReturnValue(targets, targets)
+
+ @FeatureNew('gnome.mkenums_simple', '0.42.0')
+ @typed_pos_args('gnome.mkenums_simple', str)
+ @typed_kwargs(
+ 'gnome.mkenums_simple',
+ *_MK_ENUMS_COMMON_KWS,
+ KwargInfo('header_prefix', str, default=''),
+ KwargInfo('function_prefix', str, default=''),
+ KwargInfo('body_prefix', str, default=''),
+ KwargInfo('decorator', str, default=''),
+ )
+ def mkenums_simple(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'MkEnumsSimple') -> ModuleReturnValue:
+ hdr_filename = f'{args[0]}.h'
+ body_filename = f'{args[0]}.c'
+
+ header_prefix = kwargs['header_prefix']
+ decl_decorator = kwargs['decorator']
+ func_prefix = kwargs['function_prefix']
+ body_prefix = kwargs['body_prefix']
+
+ cmd: T.List[str] = []
+ if kwargs['identifier_prefix']:
+ cmd.extend(['--identifier-prefix', kwargs['identifier_prefix']])
+ if kwargs['symbol_prefix']:
+ cmd.extend(['--symbol-prefix', kwargs['symbol_prefix']])
+
+ c_cmd = cmd.copy()
+ # Maybe we should write our own template files into the build dir
+ # instead, but that seems like much more work, nice as it would be.
+ fhead = ''
+ if body_prefix != '':
+ fhead += '%s\n' % body_prefix
+ fhead += '#include "%s"\n' % hdr_filename
+ for hdr in kwargs['sources']:
+ fhead += '#include "{}"\n'.format(os.path.basename(str(hdr)))
+ fhead += textwrap.dedent(
+ '''
+ #define C_ENUM(v) ((gint) v)
+ #define C_FLAGS(v) ((guint) v)
+ ''')
+ c_cmd.extend(['--fhead', fhead])
+
+ c_cmd.append('--fprod')
+ c_cmd.append(textwrap.dedent(
+ '''
+ /* enumerations from "@basename@" */
+ '''))
+
+ c_cmd.append('--vhead')
+ c_cmd.append(textwrap.dedent(
+ f'''
+ GType
+ {func_prefix}@enum_name@_get_type (void)
+ {{
+ static gsize gtype_id = 0;
+ static const G@Type@Value values[] = {{'''))
+
+ c_cmd.extend(['--vprod', ' { C_@TYPE@(@VALUENAME@), "@VALUENAME@", "@valuenick@" },'])
+
+ c_cmd.append('--vtail')
+ c_cmd.append(textwrap.dedent(
+ ''' { 0, NULL, NULL }
+ };
+ if (g_once_init_enter (&gtype_id)) {
+ GType new_type = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values);
+ g_once_init_leave (&gtype_id, new_type);
+ }
+ return (GType) gtype_id;
+ }'''))
+ c_cmd.append('@INPUT@')
+
+ c_file = self._make_mkenum_impl(state, kwargs['sources'], body_filename, c_cmd)
+
+ # .h file generation
+ h_cmd = cmd.copy()
+
+ h_cmd.append('--fhead')
+ h_cmd.append(textwrap.dedent(
+ f'''#pragma once
+
+ #include <glib-object.h>
+ {header_prefix}
+
+ G_BEGIN_DECLS
+ '''))
+
+ h_cmd.append('--fprod')
+ h_cmd.append(textwrap.dedent(
+ '''
+ /* enumerations from "@basename@" */
+ '''))
+
+ h_cmd.append('--vhead')
+ h_cmd.append(textwrap.dedent(
+ f'''
+ {decl_decorator}
+ GType {func_prefix}@enum_name@_get_type (void);
+ #define @ENUMPREFIX@_TYPE_@ENUMSHORT@ ({func_prefix}@enum_name@_get_type())'''))
+
+ h_cmd.append('--ftail')
+ h_cmd.append(textwrap.dedent(
+ '''
+ G_END_DECLS'''))
+ h_cmd.append('@INPUT@')
+
+ h_file = self._make_mkenum_impl(
+ state, kwargs['sources'], hdr_filename, h_cmd,
+ install=kwargs['install_header'],
+ install_dir=kwargs['install_dir'])
+
+ return ModuleReturnValue([c_file, h_file], [c_file, h_file])
+
+ @staticmethod
+ def _make_mkenum_impl(
+ state: 'ModuleState',
+ sources: T.Sequence[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]],
+ output: str,
+ cmd: T.List[str],
+ *,
+ install: bool = False,
+ install_dir: T.Optional[T.Sequence[T.Union[str, bool]]] = None,
+ depends: T.Optional[T.Sequence[T.Union[CustomTarget, CustomTargetIndex, BuildTarget]]] = None
+ ) -> build.CustomTarget:
+ real_cmd: T.List[T.Union[str, ExternalProgram]] = [state.find_program(['glib-mkenums', 'mkenums'])]
+ real_cmd.extend(cmd)
+ _install_dir = install_dir or state.environment.coredata.get_option(mesonlib.OptionKey('includedir'))
+ assert isinstance(_install_dir, str), 'for mypy'
+
+ return build.CustomTarget(
+ output,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ real_cmd,
+ sources,
+ [output],
+ capture=True,
+ install=install,
+ install_dir=[_install_dir],
+ install_tag=['devel'],
+ extra_depends=depends,
+ # https://github.com/mesonbuild/meson/issues/973
+ absolute_paths=True,
+ )
+
+ @typed_pos_args('gnome.genmarshal', str)
+ @typed_kwargs(
+ 'gnome.genmarshal',
+ DEPEND_FILES_KW.evolve(since='0.61.0'),
+ DEPENDS_KW.evolve(since='0.61.0'),
+ INSTALL_KW.evolve(name='install_header'),
+ INSTALL_DIR_KW,
+ KwargInfo('extra_args', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('internal', bool, default=False),
+ KwargInfo('nostdinc', bool, default=False),
+ KwargInfo('prefix', (str, NoneType)),
+ KwargInfo('skip_source', bool, default=False),
+ KwargInfo('sources', ContainerTypeInfo(list, (str, mesonlib.File), allow_empty=False), listify=True, required=True),
+ KwargInfo('stdinc', bool, default=False),
+ KwargInfo('valist_marshallers', bool, default=False),
+ )
+ def genmarshal(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'GenMarshal') -> ModuleReturnValue:
+ output = args[0]
+ sources = kwargs['sources']
+
+ new_genmarshal = mesonlib.version_compare(self._get_native_glib_version(state), '>= 2.53.3')
+
+ cmd: T.List[T.Union['ExternalProgram', str]] = [state.find_program('glib-genmarshal')]
+ if kwargs['prefix']:
+ cmd.extend(['--prefix', kwargs['prefix']])
+ if kwargs['extra_args']:
+ if new_genmarshal:
+ cmd.extend(kwargs['extra_args'])
+ else:
+ mlog.warning('The current version of GLib does not support extra arguments \n'
+ 'for glib-genmarshal. You need at least GLib 2.53.3. See ',
+ mlog.bold('https://github.com/mesonbuild/meson/pull/2049'))
+ for k in ['internal', 'nostdinc', 'skip_source', 'stdinc', 'valist_marshallers']:
+ # Mypy can't figure out that this is correct
+ if kwargs[k]: # type: ignore
+ cmd.append(f'--{k.replace("_", "-")}')
+
+ install_header = kwargs['install_header']
+ capture = False
+
+ # https://github.com/GNOME/glib/commit/0fbc98097fac4d3e647684f344e508abae109fdf
+ if mesonlib.version_compare(self._get_native_glib_version(state), '>= 2.51.0'):
+ cmd += ['--output', '@OUTPUT@']
+ else:
+ capture = True
+
+ header_file = output + '.h'
+ h_cmd = cmd + ['--header', '@INPUT@']
+ if new_genmarshal:
+ h_cmd += ['--pragma-once']
+ header = build.CustomTarget(
+ output + '_h',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ h_cmd,
+ sources,
+ [header_file],
+ install=install_header,
+ install_dir=[kwargs['install_dir']] if kwargs['install_dir'] else [],
+ install_tag=['devel'],
+ capture=capture,
+ depend_files=kwargs['depend_files'],
+ )
+
+ c_cmd = cmd + ['--body', '@INPUT@']
+ extra_deps: T.List[build.CustomTarget] = []
+ if mesonlib.version_compare(self._get_native_glib_version(state), '>= 2.53.4'):
+ # Silence any warnings about missing prototypes
+ c_cmd += ['--include-header', header_file]
+ extra_deps.append(header)
+ body = build.CustomTarget(
+ output + '_c',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ c_cmd,
+ sources,
+ [f'{output}.c'],
+ capture=capture,
+ depend_files=kwargs['depend_files'],
+ extra_depends=extra_deps,
+ )
+
+ rv = [body, header]
+ return ModuleReturnValue(rv, rv)
+
+ def _extract_vapi_packages(self, state: 'ModuleState', packages: T.List[T.Union[InternalDependency, str]],
+ ) -> T.Tuple[T.List[str], T.List[VapiTarget], T.List[str], T.List[str], T.List[str]]:
+ '''
+ Packages are special because we need to:
+ - Get a list of packages for the .deps file
+ - Get a list of depends for any VapiTargets
+ - Get package name from VapiTargets
+ - Add include dirs for any VapiTargets
+ '''
+ if not packages:
+ return [], [], [], [], []
+ vapi_depends: T.List[VapiTarget] = []
+ vapi_packages: T.List[str] = []
+ vapi_includes: T.List[str] = []
+ vapi_args: T.List[str] = []
+ remaining_args = []
+ for arg in packages:
+ if isinstance(arg, InternalDependency):
+ targets = [t for t in arg.sources if isinstance(t, VapiTarget)]
+ for target in targets:
+ srcdir = os.path.join(state.environment.get_source_dir(),
+ target.get_subdir())
+ outdir = os.path.join(state.environment.get_build_dir(),
+ target.get_subdir())
+ outfile = target.get_outputs()[0][:-5] # Strip .vapi
+ vapi_args.append('--vapidir=' + outdir)
+ vapi_args.append('--girdir=' + outdir)
+ vapi_args.append('--pkg=' + outfile)
+ vapi_depends.append(target)
+ vapi_packages.append(outfile)
+ vapi_includes.append(srcdir)
+ else:
+ assert isinstance(arg, str), 'for mypy'
+ vapi_args.append(f'--pkg={arg}')
+ vapi_packages.append(arg)
+ remaining_args.append(arg)
+
+ # TODO: this is supposed to take IncludeDirs, but it never worked
+ return vapi_args, vapi_depends, vapi_packages, vapi_includes, remaining_args
+
+ def _generate_deps(self, state: 'ModuleState', library: str, packages: T.List[str], install_dir: str) -> build.Data:
+ outdir = state.environment.scratch_dir
+ fname = os.path.join(outdir, library + '.deps')
+ with open(fname, 'w', encoding='utf-8') as ofile:
+ for package in packages:
+ ofile.write(package + '\n')
+ return build.Data([mesonlib.File(True, outdir, fname)], install_dir, install_dir, mesonlib.FileMode(), state.subproject)
+
+ def _get_vapi_link_with(self, target: build.CustomTarget) -> T.List[build.LibTypes]:
+ link_with: T.List[build.LibTypes] = []
+ for dep in target.get_target_dependencies():
+ if isinstance(dep, build.SharedLibrary):
+ link_with.append(dep)
+ elif isinstance(dep, GirTarget):
+ link_with += self._get_vapi_link_with(dep)
+ return link_with
+
+ @typed_pos_args('gnome.generate_vapi', str)
+ @typed_kwargs(
+ 'gnome.generate_vapi',
+ INSTALL_KW,
+ INSTALL_DIR_KW,
+ KwargInfo(
+ 'sources',
+ ContainerTypeInfo(list, (str, GirTarget), allow_empty=False),
+ listify=True,
+ required=True,
+ ),
+ KwargInfo('vapi_dirs', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('metadata_dirs', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('gir_dirs', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('packages', ContainerTypeInfo(list, (str, InternalDependency)), listify=True, default=[]),
+ )
+ def generate_vapi(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'GenerateVapi') -> ModuleReturnValue:
+ created_values: T.List[T.Union[Dependency, build.Data]] = []
+ library = args[0]
+ build_dir = os.path.join(state.environment.get_build_dir(), state.subdir)
+ source_dir = os.path.join(state.environment.get_source_dir(), state.subdir)
+ pkg_cmd, vapi_depends, vapi_packages, vapi_includes, packages = self._extract_vapi_packages(state, kwargs['packages'])
+ cmd: T.List[T.Union[str, 'ExternalProgram']]
+ cmd = [state.find_program('vapigen'), '--quiet', f'--library={library}', f'--directory={build_dir}']
+ cmd.extend([f'--vapidir={d}' for d in kwargs['vapi_dirs']])
+ cmd.extend([f'--metadatadir={d}' for d in kwargs['metadata_dirs']])
+ cmd.extend([f'--girdir={d}' for d in kwargs['gir_dirs']])
+ cmd += pkg_cmd
+ cmd += ['--metadatadir=' + source_dir]
+
+ inputs = kwargs['sources']
+
+ link_with: T.List[build.LibTypes] = []
+ for i in inputs:
+ if isinstance(i, str):
+ cmd.append(os.path.join(source_dir, i))
+ elif isinstance(i, GirTarget):
+ link_with += self._get_vapi_link_with(i)
+ subdir = os.path.join(state.environment.get_build_dir(),
+ i.get_subdir())
+ gir_file = os.path.join(subdir, i.get_outputs()[0])
+ cmd.append(gir_file)
+
+ vapi_output = library + '.vapi'
+ datadir = state.environment.coredata.get_option(mesonlib.OptionKey('datadir'))
+ assert isinstance(datadir, str), 'for mypy'
+ install_dir = kwargs['install_dir'] or os.path.join(datadir, 'vala', 'vapi')
+
+ if kwargs['install']:
+ # We shouldn't need this locally but we install it
+ deps_target = self._generate_deps(state, library, vapi_packages, install_dir)
+ created_values.append(deps_target)
+ vapi_target = VapiTarget(
+ vapi_output,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ command=cmd,
+ sources=inputs,
+ outputs=[vapi_output],
+ extra_depends=vapi_depends,
+ install=kwargs['install'],
+ install_dir=[install_dir],
+ install_tag=['devel'],
+ )
+
+ # So to try our best to get this to just work we need:
+ # - link with with the correct library
+ # - include the vapi and dependent vapi files in sources
+ # - add relevant directories to include dirs
+ incs = [build.IncludeDirs(state.subdir, ['.'] + vapi_includes, False)]
+ sources = [vapi_target] + vapi_depends
+ rv = InternalDependency(None, incs, [], [], link_with, [], sources, [], {}, [], [])
+ created_values.append(rv)
+ return ModuleReturnValue(rv, created_values)
+
+def initialize(interp: 'Interpreter') -> GnomeModule:
+ mod = GnomeModule(interp)
+ mod.interpreter.append_holder_map(GResourceTarget, interpreter.CustomTargetHolder)
+ mod.interpreter.append_holder_map(GResourceHeaderTarget, interpreter.CustomTargetHolder)
+ mod.interpreter.append_holder_map(GirTarget, interpreter.CustomTargetHolder)
+ mod.interpreter.append_holder_map(TypelibTarget, interpreter.CustomTargetHolder)
+ mod.interpreter.append_holder_map(VapiTarget, interpreter.CustomTargetHolder)
+ return mod
diff --git a/mesonbuild/modules/hotdoc.py b/mesonbuild/modules/hotdoc.py
new file mode 100644
index 0000000..b73d812
--- /dev/null
+++ b/mesonbuild/modules/hotdoc.py
@@ -0,0 +1,457 @@
+# Copyright 2018 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.
+from __future__ import annotations
+
+'''This module provides helper functions for generating documentation using hotdoc'''
+
+import os
+import subprocess
+
+from mesonbuild import mesonlib
+from mesonbuild import mlog, build
+from mesonbuild.coredata import MesonException
+from . import ModuleReturnValue, ModuleInfo
+from . import ExtensionModule
+from ..dependencies import Dependency, InternalDependency
+from ..interpreterbase import (
+ InvalidArguments, noPosargs, noKwargs, typed_kwargs, FeatureDeprecated,
+ ContainerTypeInfo, KwargInfo, typed_pos_args
+)
+from ..interpreter import CustomTargetHolder
+from ..interpreter.type_checking import NoneType
+from ..programs import ExternalProgram
+
+
+def ensure_list(value):
+ if not isinstance(value, list):
+ return [value]
+ return value
+
+
+MIN_HOTDOC_VERSION = '0.8.100'
+
+file_types = (str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex)
+
+
+class HotdocTargetBuilder:
+
+ def __init__(self, name, state, hotdoc, interpreter, kwargs):
+ self.hotdoc = hotdoc
+ self.build_by_default = kwargs.pop('build_by_default', False)
+ self.kwargs = kwargs
+ self.name = name
+ self.state = state
+ self.interpreter = interpreter
+ self.include_paths = mesonlib.OrderedSet()
+
+ self.builddir = state.environment.get_build_dir()
+ self.sourcedir = state.environment.get_source_dir()
+ self.subdir = state.subdir
+ self.build_command = state.environment.get_build_command()
+
+ self.cmd = ['conf', '--project-name', name, "--disable-incremental-build",
+ '--output', os.path.join(self.builddir, self.subdir, self.name + '-doc')]
+
+ self._extra_extension_paths = set()
+ self.extra_assets = set()
+ self.extra_depends = []
+ self._subprojects = []
+
+ def process_known_arg(self, option, argname=None, value_processor=None):
+ if not argname:
+ argname = option.strip("-").replace("-", "_")
+
+ value = self.kwargs.pop(argname)
+ if value is not None and value_processor:
+ value = value_processor(value)
+
+ self.set_arg_value(option, value)
+
+ def set_arg_value(self, option, value):
+ if value is None:
+ return
+
+ if isinstance(value, bool):
+ if value:
+ self.cmd.append(option)
+ elif isinstance(value, list):
+ # Do not do anything on empty lists
+ if value:
+ # https://bugs.python.org/issue9334 (from 2010 :( )
+ # The syntax with nargs=+ is inherently ambiguous
+ # A workaround for this case is to simply prefix with a space
+ # every value starting with a dash
+ escaped_value = []
+ for e in value:
+ if isinstance(e, str) and e.startswith('-'):
+ escaped_value += [' %s' % e]
+ else:
+ escaped_value += [e]
+ if option:
+ self.cmd.extend([option] + escaped_value)
+ else:
+ self.cmd.extend(escaped_value)
+ else:
+ # argparse gets confused if value(s) start with a dash.
+ # When an option expects a single value, the unambiguous way
+ # to specify it is with =
+ if isinstance(value, str):
+ self.cmd.extend([f'{option}={value}'])
+ else:
+ self.cmd.extend([option, value])
+
+ def check_extra_arg_type(self, arg, value):
+ if isinstance(value, list):
+ for v in value:
+ self.check_extra_arg_type(arg, v)
+ return
+
+ valid_types = (str, bool, mesonlib.File, build.IncludeDirs, build.CustomTarget, build.CustomTargetIndex, build.BuildTarget)
+ if not isinstance(value, valid_types):
+ raise InvalidArguments('Argument "{}={}" should be of type: {}.'.format(
+ arg, value, [t.__name__ for t in valid_types]))
+
+ def process_extra_args(self):
+ for arg, value in self.kwargs.items():
+ option = "--" + arg.replace("_", "-")
+ self.check_extra_arg_type(arg, value)
+ self.set_arg_value(option, value)
+
+ def get_value(self, types, argname, default=None, value_processor=None,
+ mandatory=False, force_list=False):
+ if not isinstance(types, list):
+ types = [types]
+ try:
+ uvalue = value = self.kwargs.pop(argname)
+ if value_processor:
+ value = value_processor(value)
+
+ for t in types:
+ if isinstance(value, t):
+ if force_list and not isinstance(value, list):
+ return [value], uvalue
+ return value, uvalue
+ raise MesonException(f"{argname} field value {value} is not valid,"
+ f" valid types are {types}")
+ except KeyError:
+ if mandatory:
+ raise MesonException(f"{argname} mandatory field not found")
+
+ if default is not None:
+ return default, default
+
+ return None, None
+
+ def add_extension_paths(self, paths):
+ for path in paths:
+ if path in self._extra_extension_paths:
+ continue
+
+ self._extra_extension_paths.add(path)
+ self.cmd.extend(["--extra-extension-path", path])
+
+ def replace_dirs_in_string(self, string):
+ return string.replace("@SOURCE_ROOT@", self.sourcedir).replace("@BUILD_ROOT@", self.builddir)
+
+ def process_gi_c_source_roots(self):
+ if self.hotdoc.run_hotdoc(['--has-extension=gi-extension']) != 0:
+ return
+
+ value = self.kwargs.pop('gi_c_source_roots')
+ value.extend([
+ os.path.join(self.sourcedir, self.state.root_subdir),
+ os.path.join(self.builddir, self.state.root_subdir)
+ ])
+
+ self.cmd += ['--gi-c-source-roots'] + value
+
+ def process_dependencies(self, deps):
+ cflags = set()
+ for dep in mesonlib.listify(ensure_list(deps)):
+ if isinstance(dep, InternalDependency):
+ inc_args = self.state.get_include_args(dep.include_directories)
+ cflags.update([self.replace_dirs_in_string(x)
+ for x in inc_args])
+ cflags.update(self.process_dependencies(dep.libraries))
+ cflags.update(self.process_dependencies(dep.sources))
+ cflags.update(self.process_dependencies(dep.ext_deps))
+ elif isinstance(dep, Dependency):
+ cflags.update(dep.get_compile_args())
+ elif isinstance(dep, (build.StaticLibrary, build.SharedLibrary)):
+ self.extra_depends.append(dep)
+ for incd in dep.get_include_dirs():
+ cflags.update(incd.get_incdirs())
+ elif isinstance(dep, HotdocTarget):
+ # Recurse in hotdoc target dependencies
+ self.process_dependencies(dep.get_target_dependencies())
+ self._subprojects.extend(dep.subprojects)
+ self.process_dependencies(dep.subprojects)
+ self.include_paths.add(os.path.join(self.builddir, dep.hotdoc_conf.subdir))
+ self.cmd += ['--extra-assets=' + p for p in dep.extra_assets]
+ self.add_extension_paths(dep.extra_extension_paths)
+ elif isinstance(dep, (build.CustomTarget, build.BuildTarget)):
+ self.extra_depends.append(dep)
+ elif isinstance(dep, build.CustomTargetIndex):
+ self.extra_depends.append(dep.target)
+
+ return [f.strip('-I') for f in cflags]
+
+ def process_extra_assets(self):
+ self._extra_assets = self.kwargs.pop('extra_assets')
+
+ for assets_path in self._extra_assets:
+ self.cmd.extend(["--extra-assets", assets_path])
+
+ def process_subprojects(self):
+ value = self.kwargs.pop('subprojects')
+
+ self.process_dependencies(value)
+ self._subprojects.extend(value)
+
+ def flatten_config_command(self):
+ cmd = []
+ for arg in mesonlib.listify(self.cmd, flatten=True):
+ if isinstance(arg, mesonlib.File):
+ arg = arg.absolute_path(self.state.environment.get_source_dir(),
+ self.state.environment.get_build_dir())
+ elif isinstance(arg, build.IncludeDirs):
+ for inc_dir in arg.get_incdirs():
+ cmd.append(os.path.join(self.sourcedir, arg.get_curdir(), inc_dir))
+ cmd.append(os.path.join(self.builddir, arg.get_curdir(), inc_dir))
+
+ continue
+ elif isinstance(arg, (build.BuildTarget, build.CustomTarget)):
+ self.extra_depends.append(arg)
+ arg = self.interpreter.backend.get_target_filename_abs(arg)
+ elif isinstance(arg, build.CustomTargetIndex):
+ self.extra_depends.append(arg.target)
+ arg = self.interpreter.backend.get_target_filename_abs(arg)
+
+ cmd.append(arg)
+
+ return cmd
+
+ def generate_hotdoc_config(self):
+ cwd = os.path.abspath(os.curdir)
+ ncwd = os.path.join(self.sourcedir, self.subdir)
+ mlog.log('Generating Hotdoc configuration for: ', mlog.bold(self.name))
+ os.chdir(ncwd)
+ self.hotdoc.run_hotdoc(self.flatten_config_command())
+ os.chdir(cwd)
+
+ def ensure_file(self, value):
+ if isinstance(value, list):
+ res = []
+ for val in value:
+ res.append(self.ensure_file(val))
+ return res
+
+ if isinstance(value, str):
+ return mesonlib.File.from_source_file(self.sourcedir, self.subdir, value)
+
+ return value
+
+ def ensure_dir(self, value):
+ if os.path.isabs(value):
+ _dir = value
+ else:
+ _dir = os.path.join(self.sourcedir, self.subdir, value)
+
+ if not os.path.isdir(_dir):
+ raise InvalidArguments(f'"{_dir}" is not a directory.')
+
+ return os.path.relpath(_dir, os.path.join(self.builddir, self.subdir))
+
+ def check_forbidden_args(self):
+ for arg in ['conf_file']:
+ if arg in self.kwargs:
+ raise InvalidArguments(f'Argument "{arg}" is forbidden.')
+
+ def make_targets(self):
+ self.check_forbidden_args()
+ self.process_known_arg("--index", value_processor=self.ensure_file)
+ self.process_known_arg("--project-version")
+ self.process_known_arg("--sitemap", value_processor=self.ensure_file)
+ self.process_known_arg("--html-extra-theme", value_processor=self.ensure_dir)
+ self.include_paths.update(self.ensure_dir(v) for v in self.kwargs.pop('include_paths'))
+ self.process_known_arg('--c-include-directories', argname="dependencies", value_processor=self.process_dependencies)
+ self.process_gi_c_source_roots()
+ self.process_extra_assets()
+ self.add_extension_paths(self.kwargs.pop('extra_extension_paths'))
+ self.process_subprojects()
+ self.extra_depends.extend(self.kwargs.pop('depends'))
+
+ install = self.kwargs.pop('install')
+ self.process_extra_args()
+
+ fullname = self.name + '-doc'
+ hotdoc_config_name = fullname + '.json'
+ hotdoc_config_path = os.path.join(
+ self.builddir, self.subdir, hotdoc_config_name)
+ with open(hotdoc_config_path, 'w', encoding='utf-8') as f:
+ f.write('{}')
+
+ self.cmd += ['--conf-file', hotdoc_config_path]
+ self.include_paths.add(os.path.join(self.builddir, self.subdir))
+ self.include_paths.add(os.path.join(self.sourcedir, self.subdir))
+
+ depfile = os.path.join(self.builddir, self.subdir, self.name + '.deps')
+ self.cmd += ['--deps-file-dest', depfile]
+
+ for path in self.include_paths:
+ self.cmd.extend(['--include-path', path])
+
+ if self.state.environment.coredata.get_option(mesonlib.OptionKey('werror', subproject=self.state.subproject)):
+ self.cmd.append('--fatal-warnings')
+ self.generate_hotdoc_config()
+
+ target_cmd = self.build_command + ["--internal", "hotdoc"] + \
+ self.hotdoc.get_command() + ['run', '--conf-file', hotdoc_config_name] + \
+ ['--builddir', os.path.join(self.builddir, self.subdir)]
+
+ target = HotdocTarget(fullname,
+ subdir=self.subdir,
+ subproject=self.state.subproject,
+ environment=self.state.environment,
+ hotdoc_conf=mesonlib.File.from_built_file(
+ self.subdir, hotdoc_config_name),
+ extra_extension_paths=self._extra_extension_paths,
+ extra_assets=self._extra_assets,
+ subprojects=self._subprojects,
+ command=target_cmd,
+ extra_depends=self.extra_depends,
+ outputs=[fullname],
+ sources=[],
+ depfile=os.path.basename(depfile),
+ build_by_default=self.build_by_default)
+
+ install_script = None
+ if install:
+ install_script = self.state.backend.get_executable_serialisation(self.build_command + [
+ "--internal", "hotdoc",
+ "--install", os.path.join(fullname, 'html'),
+ '--name', self.name,
+ '--builddir', os.path.join(self.builddir, self.subdir)] +
+ self.hotdoc.get_command() +
+ ['run', '--conf-file', hotdoc_config_name])
+ install_script.tag = 'doc'
+
+ return (target, install_script)
+
+
+class HotdocTargetHolder(CustomTargetHolder):
+ def __init__(self, target, interp):
+ super().__init__(target, interp)
+ self.methods.update({'config_path': self.config_path_method})
+
+ @noPosargs
+ @noKwargs
+ def config_path_method(self, *args, **kwargs):
+ conf = self.held_object.hotdoc_conf.absolute_path(self.interpreter.environment.source_dir,
+ self.interpreter.environment.build_dir)
+ return conf
+
+
+class HotdocTarget(build.CustomTarget):
+ def __init__(self, name, subdir, subproject, hotdoc_conf, extra_extension_paths, extra_assets,
+ subprojects, environment, **kwargs):
+ super().__init__(name, subdir, subproject, environment, **kwargs, absolute_paths=True)
+ self.hotdoc_conf = hotdoc_conf
+ self.extra_extension_paths = extra_extension_paths
+ self.extra_assets = extra_assets
+ self.subprojects = subprojects
+
+ def __getstate__(self):
+ # Make sure we do not try to pickle subprojects
+ res = self.__dict__.copy()
+ res['subprojects'] = []
+
+ return res
+
+
+class HotDocModule(ExtensionModule):
+
+ INFO = ModuleInfo('hotdoc', '0.48.0')
+
+ def __init__(self, interpreter):
+ super().__init__(interpreter)
+ self.hotdoc = ExternalProgram('hotdoc')
+ if not self.hotdoc.found():
+ raise MesonException('hotdoc executable not found')
+ version = self.hotdoc.get_version(interpreter)
+ if not mesonlib.version_compare(version, f'>={MIN_HOTDOC_VERSION}'):
+ raise MesonException(f'hotdoc {MIN_HOTDOC_VERSION} required but not found.)')
+
+ def run_hotdoc(cmd):
+ return subprocess.run(self.hotdoc.get_command() + cmd, stdout=subprocess.DEVNULL).returncode
+
+ self.hotdoc.run_hotdoc = run_hotdoc
+ self.methods.update({
+ 'has_extensions': self.has_extensions,
+ 'generate_doc': self.generate_doc,
+ })
+
+ @noKwargs
+ @typed_pos_args('hotdoc.has_extensions', varargs=str, min_varargs=1)
+ def has_extensions(self, state, args, kwargs):
+ return self.hotdoc.run_hotdoc([f'--has-extension={extension}' for extension in args[0]]) == 0
+
+ @typed_pos_args('hotdoc.generate_doc', str)
+ @typed_kwargs(
+ 'hotdoc.generate_doc',
+ KwargInfo('sitemap', file_types, required=True),
+ KwargInfo('index', file_types, required=True),
+ KwargInfo('project_version', str, required=True),
+ KwargInfo('html_extra_theme', (str, NoneType)),
+ KwargInfo('include_paths', ContainerTypeInfo(list, str), listify=True, default=[]),
+ # --c-include-directories
+ KwargInfo(
+ 'dependencies',
+ ContainerTypeInfo(list, (Dependency, build.StaticLibrary, build.SharedLibrary,
+ build.CustomTarget, build.CustomTargetIndex)),
+ listify=True,
+ default=[],
+ ),
+ KwargInfo(
+ 'depends',
+ ContainerTypeInfo(list, (build.CustomTarget, build.CustomTargetIndex)),
+ listify=True,
+ default=[],
+ since='0.64.1',
+ ),
+ KwargInfo('gi_c_source_roots', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('extra_assets', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('extra_extension_paths', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('subprojects', ContainerTypeInfo(list, HotdocTarget), listify=True, default=[]),
+ KwargInfo('install', bool, default=False),
+ allow_unknown=True
+ )
+ def generate_doc(self, state, args, kwargs):
+ project_name = args[0]
+ if any(isinstance(x, (build.CustomTarget, build.CustomTargetIndex)) for x in kwargs['dependencies']):
+ FeatureDeprecated.single_use('hotdoc.generate_doc dependencies argument with custom_target',
+ '0.64.1', state.subproject, 'use `depends`', state.current_node)
+ builder = HotdocTargetBuilder(project_name, state, self.hotdoc, self.interpreter, kwargs)
+ target, install_script = builder.make_targets()
+ targets = [target]
+ if install_script:
+ targets.append(install_script)
+
+ return ModuleReturnValue(targets[0], targets)
+
+
+def initialize(interpreter):
+ mod = HotDocModule(interpreter)
+ mod.interpreter.append_holder_map(HotdocTarget, HotdocTargetHolder)
+ return mod
diff --git a/mesonbuild/modules/i18n.py b/mesonbuild/modules/i18n.py
new file mode 100644
index 0000000..fcb0aa7
--- /dev/null
+++ b/mesonbuild/modules/i18n.py
@@ -0,0 +1,390 @@
+# Copyright 2016 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.
+from __future__ import annotations
+
+from os import path
+import typing as T
+
+from . import ExtensionModule, ModuleReturnValue, ModuleInfo
+from .. import build
+from .. import mesonlib
+from .. import mlog
+from ..interpreter.type_checking import CT_BUILD_BY_DEFAULT, CT_INPUT_KW, INSTALL_TAG_KW, OUTPUT_KW, INSTALL_DIR_KW, INSTALL_KW, NoneType, in_set_validator
+from ..interpreterbase import FeatureNew
+from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, noPosargs, typed_kwargs, typed_pos_args
+from ..scripts.gettext import read_linguas
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Literal, TypedDict
+
+ from . import ModuleState
+ from ..build import Target
+ from ..interpreter import Interpreter
+ from ..interpreterbase import TYPE_var
+ from ..programs import ExternalProgram
+
+ class MergeFile(TypedDict):
+
+ input: T.List[T.Union[
+ str, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex,
+ build.ExtractedObjects, build.GeneratedList, ExternalProgram,
+ mesonlib.File]]
+ output: str
+ build_by_default: bool
+ install: bool
+ install_dir: T.Optional[str]
+ install_tag: T.Optional[str]
+ args: T.List[str]
+ data_dirs: T.List[str]
+ po_dir: str
+ type: Literal['xml', 'desktop']
+
+ class Gettext(TypedDict):
+
+ args: T.List[str]
+ data_dirs: T.List[str]
+ install: bool
+ install_dir: T.Optional[str]
+ languages: T.List[str]
+ preset: T.Optional[str]
+
+ class ItsJoinFile(TypedDict):
+
+ input: T.List[T.Union[
+ str, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex,
+ build.ExtractedObjects, build.GeneratedList, ExternalProgram,
+ mesonlib.File]]
+ output: str
+ build_by_default: bool
+ install: bool
+ install_dir: T.Optional[str]
+ install_tag: T.Optional[str]
+ its_files: T.List[str]
+ mo_targets: T.List[T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]]
+
+
+_ARGS: KwargInfo[T.List[str]] = KwargInfo(
+ 'args',
+ ContainerTypeInfo(list, str),
+ default=[],
+ listify=True,
+)
+
+_DATA_DIRS: KwargInfo[T.List[str]] = KwargInfo(
+ 'data_dirs',
+ ContainerTypeInfo(list, str),
+ default=[],
+ listify=True
+)
+
+PRESET_ARGS = {
+ 'glib': [
+ '--from-code=UTF-8',
+ '--add-comments',
+
+ # https://developer.gnome.org/glib/stable/glib-I18N.html
+ '--keyword=_',
+ '--keyword=N_',
+ '--keyword=C_:1c,2',
+ '--keyword=NC_:1c,2',
+ '--keyword=g_dcgettext:2',
+ '--keyword=g_dngettext:2,3',
+ '--keyword=g_dpgettext2:2c,3',
+
+ '--flag=N_:1:pass-c-format',
+ '--flag=C_:2:pass-c-format',
+ '--flag=NC_:2:pass-c-format',
+ '--flag=g_dngettext:2:pass-c-format',
+ '--flag=g_strdup_printf:1:c-format',
+ '--flag=g_string_printf:2:c-format',
+ '--flag=g_string_append_printf:2:c-format',
+ '--flag=g_error_new:3:c-format',
+ '--flag=g_set_error:4:c-format',
+ '--flag=g_markup_printf_escaped:1:c-format',
+ '--flag=g_log:3:c-format',
+ '--flag=g_print:1:c-format',
+ '--flag=g_printerr:1:c-format',
+ '--flag=g_printf:1:c-format',
+ '--flag=g_fprintf:2:c-format',
+ '--flag=g_sprintf:2:c-format',
+ '--flag=g_snprintf:3:c-format',
+ ]
+}
+
+
+class I18nModule(ExtensionModule):
+
+ INFO = ModuleInfo('i18n')
+
+ def __init__(self, interpreter: 'Interpreter'):
+ super().__init__(interpreter)
+ self.methods.update({
+ 'merge_file': self.merge_file,
+ 'gettext': self.gettext,
+ 'itstool_join': self.itstool_join,
+ })
+ self.tools: T.Dict[str, T.Optional[ExternalProgram]] = {
+ 'itstool': None,
+ 'msgfmt': None,
+ 'msginit': None,
+ 'msgmerge': None,
+ 'xgettext': None,
+ }
+
+ @staticmethod
+ def _get_data_dirs(state: 'ModuleState', dirs: T.Iterable[str]) -> T.List[str]:
+ """Returns source directories of relative paths"""
+ src_dir = path.join(state.environment.get_source_dir(), state.subdir)
+ return [path.join(src_dir, d) for d in dirs]
+
+ @FeatureNew('i18n.merge_file', '0.37.0')
+ @noPosargs
+ @typed_kwargs(
+ 'i18n.merge_file',
+ CT_BUILD_BY_DEFAULT,
+ CT_INPUT_KW,
+ KwargInfo('install_dir', (str, NoneType)),
+ INSTALL_TAG_KW,
+ OUTPUT_KW,
+ INSTALL_KW,
+ _ARGS.evolve(since='0.51.0'),
+ _DATA_DIRS.evolve(since='0.41.0'),
+ KwargInfo('po_dir', str, required=True),
+ KwargInfo('type', str, default='xml', validator=in_set_validator({'xml', 'desktop'})),
+ )
+ def merge_file(self, state: 'ModuleState', args: T.List['TYPE_var'], kwargs: 'MergeFile') -> ModuleReturnValue:
+ if self.tools['msgfmt'] is None or not self.tools['msgfmt'].found():
+ self.tools['msgfmt'] = state.find_program('msgfmt', for_machine=mesonlib.MachineChoice.BUILD)
+ podir = path.join(state.build_to_src, state.subdir, kwargs['po_dir'])
+
+ ddirs = self._get_data_dirs(state, kwargs['data_dirs'])
+ datadirs = '--datadirs=' + ':'.join(ddirs) if ddirs else None
+
+ command: T.List[T.Union[str, build.BuildTarget, build.CustomTarget,
+ build.CustomTargetIndex, 'ExternalProgram', mesonlib.File]] = []
+ command.extend(state.environment.get_build_command())
+ command.extend([
+ '--internal', 'msgfmthelper',
+ '--msgfmt=' + self.tools['msgfmt'].get_path(),
+ ])
+ if datadirs:
+ command.append(datadirs)
+ command.extend(['@INPUT@', '@OUTPUT@', kwargs['type'], podir])
+ if kwargs['args']:
+ command.append('--')
+ command.extend(kwargs['args'])
+
+ build_by_default = kwargs['build_by_default']
+ if build_by_default is None:
+ build_by_default = kwargs['install']
+
+ install_tag = [kwargs['install_tag']] if kwargs['install_tag'] is not None else None
+
+ ct = build.CustomTarget(
+ '',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ command,
+ kwargs['input'],
+ [kwargs['output']],
+ build_by_default=build_by_default,
+ install=kwargs['install'],
+ install_dir=[kwargs['install_dir']] if kwargs['install_dir'] is not None else None,
+ install_tag=install_tag,
+ )
+
+ return ModuleReturnValue(ct, [ct])
+
+ @typed_pos_args('i81n.gettext', str)
+ @typed_kwargs(
+ 'i18n.gettext',
+ _ARGS,
+ _DATA_DIRS.evolve(since='0.36.0'),
+ INSTALL_KW.evolve(default=True),
+ INSTALL_DIR_KW.evolve(since='0.50.0'),
+ KwargInfo('languages', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo(
+ 'preset',
+ (str, NoneType),
+ validator=in_set_validator(set(PRESET_ARGS)),
+ since='0.37.0',
+ ),
+ )
+ def gettext(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'Gettext') -> ModuleReturnValue:
+ for tool, strict in [('msgfmt', True), ('msginit', False), ('msgmerge', False), ('xgettext', False)]:
+ if self.tools[tool] is None:
+ self.tools[tool] = state.find_program(tool, required=False, for_machine=mesonlib.MachineChoice.BUILD)
+ # still not found?
+ if not self.tools[tool].found():
+ if strict:
+ mlog.warning('Gettext not found, all translation (po) targets will be ignored.',
+ once=True, location=state.current_node)
+ return ModuleReturnValue(None, [])
+ else:
+ mlog.warning(f'{tool!r} not found, maintainer targets will not work',
+ once=True, fatal=False, location=state.current_node)
+ packagename = args[0]
+ pkg_arg = f'--pkgname={packagename}'
+
+ languages = kwargs['languages']
+ lang_arg = '--langs=' + '@@'.join(languages) if languages else None
+
+ _datadirs = ':'.join(self._get_data_dirs(state, kwargs['data_dirs']))
+ datadirs = f'--datadirs={_datadirs}' if _datadirs else None
+
+ extra_args = kwargs['args']
+ targets: T.List['Target'] = []
+ gmotargets: T.List['build.CustomTarget'] = []
+
+ preset = kwargs['preset']
+ if preset:
+ preset_args = PRESET_ARGS[preset]
+ extra_args = list(mesonlib.OrderedSet(preset_args + extra_args))
+
+ extra_arg = '--extra-args=' + '@@'.join(extra_args) if extra_args else None
+
+ source_root = path.join(state.source_root, state.root_subdir)
+ subdir = path.relpath(state.subdir, start=state.root_subdir) if state.subdir else None
+
+ potargs = state.environment.get_build_command() + ['--internal', 'gettext', 'pot', pkg_arg]
+ potargs.append(f'--source-root={source_root}')
+ if subdir:
+ potargs.append(f'--subdir={subdir}')
+ if datadirs:
+ potargs.append(datadirs)
+ if extra_arg:
+ potargs.append(extra_arg)
+ if self.tools['xgettext'].found():
+ potargs.append('--xgettext=' + self.tools['xgettext'].get_path())
+ pottarget = build.RunTarget(packagename + '-pot', potargs, [], state.subdir, state.subproject,
+ state.environment, default_env=False)
+ targets.append(pottarget)
+
+ install = kwargs['install']
+ install_dir = kwargs['install_dir'] or state.environment.coredata.get_option(mesonlib.OptionKey('localedir'))
+ assert isinstance(install_dir, str), 'for mypy'
+ if not languages:
+ languages = read_linguas(path.join(state.environment.source_dir, state.subdir))
+ for l in languages:
+ po_file = mesonlib.File.from_source_file(state.environment.source_dir,
+ state.subdir, l+'.po')
+ gmotarget = build.CustomTarget(
+ f'{packagename}-{l}.mo',
+ path.join(state.subdir, l, 'LC_MESSAGES'),
+ state.subproject,
+ state.environment,
+ [self.tools['msgfmt'], '@INPUT@', '-o', '@OUTPUT@'],
+ [po_file],
+ [f'{packagename}.mo'],
+ install=install,
+ # We have multiple files all installed as packagename+'.mo' in different install subdirs.
+ # What we really wanted to do, probably, is have a rename: kwarg, but that's not available
+ # to custom_targets. Crude hack: set the build target's subdir manually.
+ # Bonus: the build tree has something usable as an uninstalled bindtextdomain() target dir.
+ install_dir=[path.join(install_dir, l, 'LC_MESSAGES')],
+ install_tag=['i18n'],
+ )
+ targets.append(gmotarget)
+ gmotargets.append(gmotarget)
+
+ allgmotarget = build.AliasTarget(packagename + '-gmo', gmotargets, state.subdir, state.subproject,
+ state.environment)
+ targets.append(allgmotarget)
+
+ updatepoargs = state.environment.get_build_command() + ['--internal', 'gettext', 'update_po', pkg_arg]
+ updatepoargs.append(f'--source-root={source_root}')
+ if subdir:
+ updatepoargs.append(f'--subdir={subdir}')
+ if lang_arg:
+ updatepoargs.append(lang_arg)
+ if datadirs:
+ updatepoargs.append(datadirs)
+ if extra_arg:
+ updatepoargs.append(extra_arg)
+ for tool in ['msginit', 'msgmerge']:
+ if self.tools[tool].found():
+ updatepoargs.append(f'--{tool}=' + self.tools[tool].get_path())
+ updatepotarget = build.RunTarget(packagename + '-update-po', updatepoargs, [], state.subdir, state.subproject,
+ state.environment, default_env=False)
+ targets.append(updatepotarget)
+
+ return ModuleReturnValue([gmotargets, pottarget, updatepotarget], targets)
+
+ @FeatureNew('i18n.itstool_join', '0.62.0')
+ @noPosargs
+ @typed_kwargs(
+ 'i18n.itstool_join',
+ CT_BUILD_BY_DEFAULT,
+ CT_INPUT_KW,
+ KwargInfo('install_dir', (str, NoneType)),
+ INSTALL_TAG_KW,
+ OUTPUT_KW,
+ INSTALL_KW,
+ _ARGS.evolve(),
+ KwargInfo('its_files', ContainerTypeInfo(list, str)),
+ KwargInfo('mo_targets', ContainerTypeInfo(list, build.CustomTarget), required=True),
+ )
+ def itstool_join(self, state: 'ModuleState', args: T.List['TYPE_var'], kwargs: 'ItsJoinFile') -> ModuleReturnValue:
+ if self.tools['itstool'] is None:
+ self.tools['itstool'] = state.find_program('itstool', for_machine=mesonlib.MachineChoice.BUILD)
+ mo_targets = kwargs['mo_targets']
+ its_files = kwargs.get('its_files', [])
+
+ mo_fnames = []
+ for target in mo_targets:
+ mo_fnames.append(path.join(target.get_subdir(), target.get_outputs()[0]))
+
+ command: T.List[T.Union[str, build.BuildTarget, build.CustomTarget,
+ build.CustomTargetIndex, 'ExternalProgram', mesonlib.File]] = []
+ command.extend(state.environment.get_build_command())
+ command.extend([
+ '--internal', 'itstool', 'join',
+ '-i', '@INPUT@',
+ '-o', '@OUTPUT@',
+ '--itstool=' + self.tools['itstool'].get_path(),
+ ])
+ if its_files:
+ for fname in its_files:
+ if not path.isabs(fname):
+ fname = path.join(state.environment.source_dir, state.subdir, fname)
+ command.extend(['--its', fname])
+ command.extend(mo_fnames)
+
+ build_by_default = kwargs['build_by_default']
+ if build_by_default is None:
+ build_by_default = kwargs['install']
+
+ install_tag = [kwargs['install_tag']] if kwargs['install_tag'] is not None else None
+
+ ct = build.CustomTarget(
+ '',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ command,
+ kwargs['input'],
+ [kwargs['output']],
+ build_by_default=build_by_default,
+ extra_depends=mo_targets,
+ install=kwargs['install'],
+ install_dir=[kwargs['install_dir']] if kwargs['install_dir'] is not None else None,
+ install_tag=install_tag,
+ )
+
+ return ModuleReturnValue(ct, [ct])
+
+
+def initialize(interp: 'Interpreter') -> I18nModule:
+ return I18nModule(interp)
diff --git a/mesonbuild/modules/icestorm.py b/mesonbuild/modules/icestorm.py
new file mode 100644
index 0000000..c579148
--- /dev/null
+++ b/mesonbuild/modules/icestorm.py
@@ -0,0 +1,131 @@
+# Copyright 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.
+
+from __future__ import annotations
+import itertools
+import typing as T
+
+from . import ExtensionModule, ModuleReturnValue, ModuleInfo
+from .. import build
+from .. import mesonlib
+from ..interpreter.type_checking import CT_INPUT_KW
+from ..interpreterbase.decorators import KwargInfo, typed_kwargs, typed_pos_args
+
+if T.TYPE_CHECKING:
+ from typing_extensions import TypedDict
+
+ from . import ModuleState
+ from ..interpreter import Interpreter
+ from ..programs import ExternalProgram
+
+ class ProjectKwargs(TypedDict):
+
+ sources: T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]
+ constraint_file: T.Union[mesonlib.FileOrString, build.GeneratedTypes]
+
+class IceStormModule(ExtensionModule):
+
+ INFO = ModuleInfo('FPGA/Icestorm', '0.45.0', unstable=True)
+
+ def __init__(self, interpreter: Interpreter) -> None:
+ super().__init__(interpreter)
+ self.tools: T.Dict[str, ExternalProgram] = {}
+ self.methods.update({
+ 'project': self.project,
+ })
+
+ def detect_tools(self, state: ModuleState) -> None:
+ self.tools['yosys'] = state.find_program('yosys')
+ self.tools['arachne'] = state.find_program('arachne-pnr')
+ self.tools['icepack'] = state.find_program('icepack')
+ self.tools['iceprog'] = state.find_program('iceprog')
+ self.tools['icetime'] = state.find_program('icetime')
+
+ @typed_pos_args('icestorm.project', str,
+ varargs=(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex,
+ build.GeneratedList))
+ @typed_kwargs(
+ 'icestorm.project',
+ CT_INPUT_KW.evolve(name='sources'),
+ KwargInfo(
+ 'constraint_file',
+ (str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList),
+ required=True,
+ )
+ )
+ def project(self, state: ModuleState,
+ args: T.Tuple[str, T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]],
+ kwargs: ProjectKwargs) -> ModuleReturnValue:
+ if not self.tools:
+ self.detect_tools(state)
+ proj_name, arg_sources = args
+ all_sources = self.interpreter.source_strings_to_files(
+ list(itertools.chain(arg_sources, kwargs['sources'])))
+
+ blif_target = build.CustomTarget(
+ f'{proj_name}_blif',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ [self.tools['yosys'], '-q', '-p', 'synth_ice40 -blif @OUTPUT@', '@INPUT@'],
+ all_sources,
+ [f'{proj_name}.blif'],
+ )
+
+ asc_target = build.CustomTarget(
+ f'{proj_name}_asc',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ [self.tools['arachne'], '-q', '-d', '1k', '-p', '@INPUT@', '-o', '@OUTPUT@'],
+ [kwargs['constraint_file'], blif_target],
+ [f'{proj_name}.asc'],
+ )
+
+ bin_target = build.CustomTarget(
+ f'{proj_name}_bin',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ [self.tools['icepack'], '@INPUT@', '@OUTPUT@'],
+ [asc_target],
+ [f'{proj_name}.bin'],
+ build_by_default=True,
+ )
+
+ upload_target = build.RunTarget(
+ f'{proj_name}-upload',
+ [self.tools['iceprog'], bin_target],
+ [],
+ state.subdir,
+ state.subproject,
+ state.environment,
+ )
+
+ time_target = build.RunTarget(
+ f'{proj_name}-time',
+ [self.tools['icetime'], bin_target],
+ [],
+ state.subdir,
+ state.subproject,
+ state.environment,
+ )
+
+ return ModuleReturnValue(
+ None,
+ [blif_target, asc_target, bin_target, upload_target, time_target])
+
+
+def initialize(interp: Interpreter) -> IceStormModule:
+ return IceStormModule(interp)
diff --git a/mesonbuild/modules/java.py b/mesonbuild/modules/java.py
new file mode 100644
index 0000000..6861ee0
--- /dev/null
+++ b/mesonbuild/modules/java.py
@@ -0,0 +1,117 @@
+# Copyright 2021 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.
+from __future__ import annotations
+
+import pathlib
+import typing as T
+
+from mesonbuild import mesonlib
+from mesonbuild.build import CustomTarget, CustomTargetIndex, GeneratedList, Target
+from mesonbuild.compilers import detect_compiler_for
+from mesonbuild.interpreterbase.decorators import ContainerTypeInfo, FeatureDeprecated, FeatureNew, KwargInfo, typed_pos_args, typed_kwargs
+from mesonbuild.mesonlib import version_compare, MachineChoice
+from . import NewExtensionModule, ModuleReturnValue, ModuleInfo
+from ..interpreter.type_checking import NoneType
+
+if T.TYPE_CHECKING:
+ from . import ModuleState
+ from ..compilers import Compiler
+ from ..interpreter import Interpreter
+
+class JavaModule(NewExtensionModule):
+
+ INFO = ModuleInfo('java', '0.60.0')
+
+ def __init__(self, interpreter: Interpreter):
+ super().__init__()
+ self.methods.update({
+ 'generate_native_headers': self.generate_native_headers,
+ 'native_headers': self.native_headers,
+ })
+
+ def __get_java_compiler(self, state: ModuleState) -> Compiler:
+ if 'java' not in state.environment.coredata.compilers[MachineChoice.BUILD]:
+ detect_compiler_for(state.environment, 'java', MachineChoice.BUILD)
+ return state.environment.coredata.compilers[MachineChoice.BUILD]['java']
+
+ @FeatureNew('java.generate_native_headers', '0.62.0')
+ @FeatureDeprecated('java.generate_native_headers', '1.0.0')
+ @typed_pos_args(
+ 'java.generate_native_headers',
+ varargs=(str, mesonlib.File, Target, CustomTargetIndex, GeneratedList))
+ @typed_kwargs(
+ 'java.generate_native_headers',
+ KwargInfo('classes', ContainerTypeInfo(list, str), default=[], listify=True, required=True),
+ KwargInfo('package', (str, NoneType), default=None))
+ def generate_native_headers(self, state: ModuleState, args: T.Tuple[T.List[mesonlib.FileOrString]],
+ kwargs: T.Dict[str, T.Optional[str]]) -> ModuleReturnValue:
+ return self.__native_headers(state, args, kwargs)
+
+ @FeatureNew('java.native_headers', '1.0.0')
+ @typed_pos_args(
+ 'java.native_headers',
+ varargs=(str, mesonlib.File, Target, CustomTargetIndex, GeneratedList))
+ @typed_kwargs(
+ 'java.native_headers',
+ KwargInfo('classes', ContainerTypeInfo(list, str), default=[], listify=True, required=True),
+ KwargInfo('package', (str, NoneType), default=None))
+ def native_headers(self, state: ModuleState, args: T.Tuple[T.List[mesonlib.FileOrString]],
+ kwargs: T.Dict[str, T.Optional[str]]) -> ModuleReturnValue:
+ return self.__native_headers(state, args, kwargs)
+
+ def __native_headers(self, state: ModuleState, args: T.Tuple[T.List[mesonlib.FileOrString]],
+ kwargs: T.Dict[str, T.Optional[str]]) -> ModuleReturnValue:
+ classes = T.cast('T.List[str]', kwargs.get('classes'))
+ package = kwargs.get('package')
+
+ if package:
+ sanitized_package = package.replace("-", "_").replace(".", "_")
+
+ headers: T.List[str] = []
+ for clazz in classes:
+ sanitized_clazz = clazz.replace(".", "_")
+ if package:
+ headers.append(f'{sanitized_package}_{sanitized_clazz}.h')
+ else:
+ headers.append(f'{sanitized_clazz}.h')
+
+ javac = self.__get_java_compiler(state)
+
+ command = mesonlib.listify([
+ javac.exelist,
+ '-d',
+ '@PRIVATE_DIR@',
+ '-h',
+ state.subdir,
+ '@INPUT@',
+ ])
+
+ prefix = classes[0] if not package else package
+
+ target = CustomTarget(f'{prefix}-native-headers',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ command,
+ sources=args[0], outputs=headers, backend=state.backend)
+
+ # It is only known that 1.8.0 won't pre-create the directory. 11 and 16
+ # do not exhibit this behavior.
+ if version_compare(javac.version, '1.8.0'):
+ pathlib.Path(state.backend.get_target_private_dir_abs(target)).mkdir(parents=True, exist_ok=True)
+
+ return ModuleReturnValue(target, [target])
+
+def initialize(*args: T.Any, **kwargs: T.Any) -> JavaModule:
+ return JavaModule(*args, **kwargs)
diff --git a/mesonbuild/modules/keyval.py b/mesonbuild/modules/keyval.py
new file mode 100644
index 0000000..1ba2f1c
--- /dev/null
+++ b/mesonbuild/modules/keyval.py
@@ -0,0 +1,75 @@
+# Copyright 2017, 2019 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.
+from __future__ import annotations
+
+import os
+import typing as T
+
+from . import ExtensionModule, ModuleInfo
+from .. import mesonlib
+from ..interpreterbase import noKwargs, typed_pos_args
+
+if T.TYPE_CHECKING:
+ from ..interpreter import Interpreter
+ from . import ModuleState
+
+class KeyvalModule(ExtensionModule):
+
+ INFO = ModuleInfo('keyval', '0.55.0', stabilized='0.56.0')
+
+ def __init__(self, interp: 'Interpreter'):
+ super().__init__(interp)
+ self.methods.update({
+ 'load': self.load,
+ })
+
+ @staticmethod
+ def _load_file(path_to_config: str) -> T.Dict[str, str]:
+ result: T.Dict[str, str] = {}
+ try:
+ with open(path_to_config, encoding='utf-8') as f:
+ for line in f:
+ if '#' in line:
+ comment_idx = line.index('#')
+ line = line[:comment_idx]
+ line = line.strip()
+ try:
+ name, val = line.split('=', 1)
+ except ValueError:
+ continue
+ result[name.strip()] = val.strip()
+ except OSError as e:
+ raise mesonlib.MesonException(f'Failed to load {path_to_config}: {e}')
+
+ return result
+
+ @noKwargs
+ @typed_pos_args('keyval.laod', (str, mesonlib.File))
+ def load(self, state: 'ModuleState', args: T.Tuple['mesonlib.FileOrString'], kwargs: T.Dict[str, T.Any]) -> T.Dict[str, str]:
+ s = args[0]
+ is_built = False
+ if isinstance(s, mesonlib.File):
+ is_built = is_built or s.is_built
+ s = s.absolute_path(self.interpreter.environment.source_dir, self.interpreter.environment.build_dir)
+ else:
+ s = os.path.join(self.interpreter.environment.source_dir, s)
+
+ if not is_built:
+ self.interpreter.build_def_files.add(s)
+
+ return self._load_file(s)
+
+
+def initialize(interp: 'Interpreter') -> KeyvalModule:
+ return KeyvalModule(interp)
diff --git a/mesonbuild/modules/modtest.py b/mesonbuild/modules/modtest.py
new file mode 100644
index 0000000..15f8237
--- /dev/null
+++ b/mesonbuild/modules/modtest.py
@@ -0,0 +1,44 @@
+# Copyright 2015 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.
+
+from __future__ import annotations
+import typing as T
+
+from . import ExtensionModule, ModuleInfo
+from ..interpreterbase import noKwargs, noPosargs
+
+if T.TYPE_CHECKING:
+ from . import ModuleState
+ from ..interpreter.interpreter import Interpreter
+ from ..interpreterbase.baseobjects import TYPE_kwargs, TYPE_var
+
+
+class TestModule(ExtensionModule):
+
+ INFO = ModuleInfo('modtest')
+
+ def __init__(self, interpreter: Interpreter) -> None:
+ super().__init__(interpreter)
+ self.methods.update({
+ 'print_hello': self.print_hello,
+ })
+
+ @noKwargs
+ @noPosargs
+ def print_hello(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> None:
+ print('Hello from a Meson module')
+
+
+def initialize(interp: Interpreter) -> TestModule:
+ return TestModule(interp)
diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py
new file mode 100644
index 0000000..af8467c
--- /dev/null
+++ b/mesonbuild/modules/pkgconfig.py
@@ -0,0 +1,742 @@
+# Copyright 2015-2022 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.
+
+from __future__ import annotations
+from collections import defaultdict
+from dataclasses import dataclass
+from pathlib import PurePath
+import os
+import typing as T
+
+from . import NewExtensionModule, ModuleInfo
+from . import ModuleReturnValue
+from .. import build
+from .. import dependencies
+from .. import mesonlib
+from .. import mlog
+from ..coredata import BUILTIN_DIR_OPTIONS
+from ..dependencies import ThreadDependency
+from ..interpreter.type_checking import D_MODULE_VERSIONS_KW, INSTALL_DIR_KW, VARIABLES_KW, NoneType
+from ..interpreterbase import FeatureNew, FeatureDeprecated
+from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, typed_kwargs, typed_pos_args
+
+if T.TYPE_CHECKING:
+ from typing_extensions import TypedDict
+
+ from . import ModuleState
+ from .. import mparser
+ from ..interpreter import Interpreter
+
+ ANY_DEP = T.Union[dependencies.Dependency, build.BuildTargetTypes, str]
+ LIBS = T.Union[build.LibTypes, str]
+
+ class GenerateKw(TypedDict):
+
+ version: T.Optional[str]
+ name: T.Optional[str]
+ filebase: T.Optional[str]
+ description: T.Optional[str]
+ url: str
+ subdirs: T.List[str]
+ conflicts: T.List[str]
+ dataonly: bool
+ libraries: T.List[ANY_DEP]
+ libraries_private: T.List[ANY_DEP]
+ requires: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]
+ requires_private: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]
+ install_dir: T.Optional[str]
+ d_module_versions: T.List[T.Union[str, int]]
+ extra_cflags: T.List[str]
+ variables: T.Dict[str, str]
+ uninstalled_variables: T.Dict[str, str]
+ unescaped_variables: T.Dict[str, str]
+ unescaped_uninstalled_variables: T.Dict[str, str]
+
+
+_PKG_LIBRARIES: KwargInfo[T.List[T.Union[str, dependencies.Dependency, build.SharedLibrary, build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex]]] = KwargInfo(
+ 'libraries',
+ ContainerTypeInfo(list, (str, dependencies.Dependency,
+ build.SharedLibrary, build.StaticLibrary,
+ build.CustomTarget, build.CustomTargetIndex)),
+ default=[],
+ listify=True,
+)
+
+_PKG_REQUIRES: KwargInfo[T.List[T.Union[str, build.SharedLibrary, build.StaticLibrary, dependencies.Dependency]]] = KwargInfo(
+ 'requires',
+ ContainerTypeInfo(list, (str, build.SharedLibrary, build.StaticLibrary, dependencies.Dependency)),
+ default=[],
+ listify=True,
+)
+
+
+def _as_str(obj: object) -> str:
+ assert isinstance(obj, str)
+ return obj
+
+
+@dataclass
+class MetaData:
+
+ filebase: str
+ display_name: str
+ location: mparser.BaseNode
+ warned: bool = False
+
+
+class DependenciesHelper:
+ def __init__(self, state: ModuleState, name: str, metadata: T.Dict[str, MetaData]) -> None:
+ self.state = state
+ self.name = name
+ self.pub_libs: T.List[LIBS] = []
+ self.pub_reqs: T.List[str] = []
+ self.priv_libs: T.List[LIBS] = []
+ self.priv_reqs: T.List[str] = []
+ self.cflags: T.List[str] = []
+ self.version_reqs: T.DefaultDict[str, T.Set[str]] = defaultdict(set)
+ self.link_whole_targets: T.List[T.Union[build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary]] = []
+ self.metadata = metadata
+
+ def add_pub_libs(self, libs: T.List[ANY_DEP]) -> None:
+ p_libs, reqs, cflags = self._process_libs(libs, True)
+ self.pub_libs = p_libs + self.pub_libs # prepend to preserve dependencies
+ self.pub_reqs += reqs
+ self.cflags += cflags
+
+ def add_priv_libs(self, libs: T.List[ANY_DEP]) -> None:
+ p_libs, reqs, _ = self._process_libs(libs, False)
+ self.priv_libs = p_libs + self.priv_libs
+ self.priv_reqs += reqs
+
+ def add_pub_reqs(self, reqs: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]) -> None:
+ self.pub_reqs += self._process_reqs(reqs)
+
+ def add_priv_reqs(self, reqs: T.List[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]) -> None:
+ self.priv_reqs += self._process_reqs(reqs)
+
+ def _check_generated_pc_deprecation(self, obj: T.Union[build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary, build.SharedLibrary]) -> None:
+ if obj.get_id() in self.metadata:
+ return
+ data = self.metadata[obj.get_id()]
+ if data.warned:
+ return
+ mlog.deprecation('Library', mlog.bold(obj.name), 'was passed to the '
+ '"libraries" keyword argument of a previous call '
+ 'to generate() method instead of first positional '
+ 'argument.', 'Adding', mlog.bold(data.display_name),
+ 'to "Requires" field, but this is a deprecated '
+ 'behaviour that will change in a future version '
+ 'of Meson. Please report the issue if this '
+ 'warning cannot be avoided in your case.',
+ location=data.location)
+ data.warned = True
+
+ def _process_reqs(self, reqs: T.Sequence[T.Union[str, build.StaticLibrary, build.SharedLibrary, dependencies.Dependency]]) -> T.List[str]:
+ '''Returns string names of requirements'''
+ processed_reqs: T.List[str] = []
+ for obj in mesonlib.listify(reqs):
+ if not isinstance(obj, str):
+ FeatureNew.single_use('pkgconfig.generate requirement from non-string object', '0.46.0', self.state.subproject)
+ if (isinstance(obj, (build.CustomTarget, build.CustomTargetIndex, build.SharedLibrary, build.StaticLibrary))
+ and obj.get_id() in self.metadata):
+ self._check_generated_pc_deprecation(obj)
+ processed_reqs.append(self.metadata[obj.get_id()].filebase)
+ elif isinstance(obj, dependencies.PkgConfigDependency):
+ if obj.found():
+ processed_reqs.append(obj.name)
+ self.add_version_reqs(obj.name, obj.version_reqs)
+ elif isinstance(obj, str):
+ name, version_req = self.split_version_req(obj)
+ processed_reqs.append(name)
+ self.add_version_reqs(name, [version_req] if version_req is not None else None)
+ elif isinstance(obj, dependencies.Dependency) and not obj.found():
+ pass
+ elif isinstance(obj, ThreadDependency):
+ pass
+ else:
+ raise mesonlib.MesonException('requires argument not a string, '
+ 'library with pkgconfig-generated file '
+ f'or pkgconfig-dependency object, got {obj!r}')
+ return processed_reqs
+
+ def add_cflags(self, cflags: T.List[str]) -> None:
+ self.cflags += mesonlib.stringlistify(cflags)
+
+ def _process_libs(
+ self, libs: T.List[ANY_DEP], public: bool
+ ) -> T.Tuple[T.List[T.Union[str, build.SharedLibrary, build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex]], T.List[str], T.List[str]]:
+ libs = mesonlib.listify(libs)
+ processed_libs: T.List[T.Union[str, build.SharedLibrary, build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex]] = []
+ processed_reqs: T.List[str] = []
+ processed_cflags: T.List[str] = []
+ for obj in libs:
+ if (isinstance(obj, (build.CustomTarget, build.CustomTargetIndex, build.SharedLibrary, build.StaticLibrary))
+ and obj.get_id() in self.metadata):
+ self._check_generated_pc_deprecation(obj)
+ processed_reqs.append(self.metadata[obj.get_id()].filebase)
+ elif isinstance(obj, dependencies.ValgrindDependency):
+ pass
+ elif isinstance(obj, dependencies.PkgConfigDependency):
+ if obj.found():
+ processed_reqs.append(obj.name)
+ self.add_version_reqs(obj.name, obj.version_reqs)
+ elif isinstance(obj, dependencies.InternalDependency):
+ if obj.found():
+ processed_libs += obj.get_link_args()
+ processed_cflags += obj.get_compile_args()
+ self._add_lib_dependencies(obj.libraries, obj.whole_libraries, obj.ext_deps, public, private_external_deps=True)
+ elif isinstance(obj, dependencies.Dependency):
+ if obj.found():
+ processed_libs += obj.get_link_args()
+ processed_cflags += obj.get_compile_args()
+ elif isinstance(obj, build.SharedLibrary) and obj.shared_library_only:
+ # Do not pull dependencies for shared libraries because they are
+ # only required for static linking. Adding private requires has
+ # the side effect of exposing their cflags, which is the
+ # intended behaviour of pkg-config but force Debian to add more
+ # than needed build deps.
+ # See https://bugs.freedesktop.org/show_bug.cgi?id=105572
+ processed_libs.append(obj)
+ elif isinstance(obj, (build.SharedLibrary, build.StaticLibrary)):
+ processed_libs.append(obj)
+ # If there is a static library in `Libs:` all its deps must be
+ # public too, otherwise the generated pc file will never be
+ # usable without --static.
+ self._add_lib_dependencies(obj.link_targets,
+ obj.link_whole_targets,
+ obj.external_deps,
+ isinstance(obj, build.StaticLibrary) and public)
+ elif isinstance(obj, (build.CustomTarget, build.CustomTargetIndex)):
+ if not obj.is_linkable_target():
+ raise mesonlib.MesonException('library argument contains a not linkable custom_target.')
+ FeatureNew.single_use('custom_target in pkgconfig.generate libraries', '0.58.0', self.state.subproject)
+ processed_libs.append(obj)
+ elif isinstance(obj, str):
+ processed_libs.append(obj)
+ else:
+ raise mesonlib.MesonException(f'library argument of type {type(obj).__name__} not a string, library or dependency object.')
+
+ return processed_libs, processed_reqs, processed_cflags
+
+ def _add_lib_dependencies(
+ self, link_targets: T.Sequence[build.BuildTargetTypes],
+ link_whole_targets: T.Sequence[T.Union[build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex]],
+ external_deps: T.List[dependencies.Dependency],
+ public: bool,
+ private_external_deps: bool = False) -> None:
+ add_libs = self.add_pub_libs if public else self.add_priv_libs
+ # Recursively add all linked libraries
+ for t in link_targets:
+ # Internal libraries (uninstalled static library) will be promoted
+ # to link_whole, treat them as such here.
+ if t.is_internal():
+ # `is_internal` shouldn't return True for anything but a
+ # StaticLibrary, or a CustomTarget that is a StaticLibrary
+ assert isinstance(t, (build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex)), 'for mypy'
+ self._add_link_whole(t, public)
+ else:
+ add_libs([t])
+ for t in link_whole_targets:
+ self._add_link_whole(t, public)
+ # And finally its external dependencies
+ if private_external_deps:
+ self.add_priv_libs(T.cast('T.List[ANY_DEP]', external_deps))
+ else:
+ add_libs(T.cast('T.List[ANY_DEP]', external_deps))
+
+ def _add_link_whole(self, t: T.Union[build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary], public: bool) -> None:
+ # Don't include static libraries that we link_whole. But we still need to
+ # include their dependencies: a static library we link_whole
+ # could itself link to a shared library or an installed static library.
+ # Keep track of link_whole_targets so we can remove them from our
+ # lists in case a library is link_with and link_whole at the same time.
+ # See remove_dups() below.
+ self.link_whole_targets.append(t)
+ if isinstance(t, build.BuildTarget):
+ self._add_lib_dependencies(t.link_targets, t.link_whole_targets, t.external_deps, public)
+
+ def add_version_reqs(self, name: str, version_reqs: T.Optional[T.List[str]]) -> None:
+ if version_reqs:
+ # Note that pkg-config is picky about whitespace.
+ # 'foo > 1.2' is ok but 'foo>1.2' is not.
+ # foo, bar' is ok, but 'foo,bar' is not.
+ self.version_reqs[name].update(version_reqs)
+
+ def split_version_req(self, s: str) -> T.Tuple[str, T.Optional[str]]:
+ for op in ['>=', '<=', '!=', '==', '=', '>', '<']:
+ pos = s.find(op)
+ if pos > 0:
+ return s[0:pos].strip(), s[pos:].strip()
+ return s, None
+
+ def format_vreq(self, vreq: str) -> str:
+ # vreq are '>=1.0' and pkgconfig wants '>= 1.0'
+ for op in ['>=', '<=', '!=', '==', '=', '>', '<']:
+ if vreq.startswith(op):
+ return op + ' ' + vreq[len(op):]
+ return vreq
+
+ def format_reqs(self, reqs: T.List[str]) -> str:
+ result: T.List[str] = []
+ for name in reqs:
+ vreqs = self.version_reqs.get(name, None)
+ if vreqs:
+ result += [name + ' ' + self.format_vreq(vreq) for vreq in vreqs]
+ else:
+ result += [name]
+ return ', '.join(result)
+
+ def remove_dups(self) -> None:
+ # Set of ids that have already been handled and should not be added any more
+ exclude: T.Set[str] = set()
+
+ # We can't just check if 'x' is excluded because we could have copies of
+ # the same SharedLibrary object for example.
+ def _ids(x: T.Union[str, build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary, build.SharedLibrary]) -> T.Iterable[str]:
+ if isinstance(x, str):
+ yield x
+ else:
+ if x.get_id() in self.metadata:
+ yield self.metadata[x.get_id()].display_name
+ yield x.get_id()
+
+ # Exclude 'x' in all its forms and return if it was already excluded
+ def _add_exclude(x: T.Union[str, build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary, build.SharedLibrary]) -> bool:
+ was_excluded = False
+ for i in _ids(x):
+ if i in exclude:
+ was_excluded = True
+ else:
+ exclude.add(i)
+ return was_excluded
+
+ # link_whole targets are already part of other targets, exclude them all.
+ for t in self.link_whole_targets:
+ _add_exclude(t)
+
+ # Mypy thinks these overlap, but since List is invariant they don't,
+ # `List[str]`` is not a valid input to `List[str | BuildTarget]`.
+ # pylance/pyright gets this right, but for mypy we have to ignore the
+ # error
+ @T.overload
+ def _fn(xs: T.List[str], libs: bool = False) -> T.List[str]: ... # type: ignore
+
+ @T.overload
+ def _fn(xs: T.List[LIBS], libs: bool = False) -> T.List[LIBS]: ...
+
+ def _fn(xs: T.Union[T.List[str], T.List[LIBS]], libs: bool = False) -> T.Union[T.List[str], T.List[LIBS]]:
+ # Remove duplicates whilst preserving original order
+ result = []
+ for x in xs:
+ # Don't de-dup unknown strings to avoid messing up arguments like:
+ # ['-framework', 'CoreAudio', '-framework', 'CoreMedia']
+ known_flags = ['-pthread']
+ cannot_dedup = libs and isinstance(x, str) and \
+ not x.startswith(('-l', '-L')) and \
+ x not in known_flags
+ if not cannot_dedup and _add_exclude(x):
+ continue
+ result.append(x)
+ return result
+
+ # Handle lists in priority order: public items can be excluded from
+ # private and Requires can excluded from Libs.
+ self.pub_reqs = _fn(self.pub_reqs)
+ self.pub_libs = _fn(self.pub_libs, True)
+ self.priv_reqs = _fn(self.priv_reqs)
+ self.priv_libs = _fn(self.priv_libs, True)
+ # Reset exclude list just in case some values can be both cflags and libs.
+ exclude = set()
+ self.cflags = _fn(self.cflags)
+
+class PkgConfigModule(NewExtensionModule):
+
+ INFO = ModuleInfo('pkgconfig')
+
+ # Track already generated pkg-config files This is stored as a class
+ # variable so that multiple `import()`s share metadata
+ _metadata: T.ClassVar[T.Dict[str, MetaData]] = {}
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.methods.update({
+ 'generate': self.generate,
+ })
+
+ def _get_lname(self, l: T.Union[build.SharedLibrary, build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex],
+ msg: str, pcfile: str) -> str:
+ if isinstance(l, (build.CustomTargetIndex, build.CustomTarget)):
+ basename = os.path.basename(l.get_filename())
+ name = os.path.splitext(basename)[0]
+ if name.startswith('lib'):
+ name = name[3:]
+ return name
+ # Nothing special
+ if not l.name_prefix_set:
+ return l.name
+ # Sometimes people want the library to start with 'lib' everywhere,
+ # which is achieved by setting name_prefix to '' and the target name to
+ # 'libfoo'. In that case, try to get the pkg-config '-lfoo' arg correct.
+ if l.prefix == '' and l.name.startswith('lib'):
+ return l.name[3:]
+ # If the library is imported via an import library which is always
+ # named after the target name, '-lfoo' is correct.
+ if isinstance(l, build.SharedLibrary) and l.import_filename:
+ return l.name
+ # In other cases, we can't guarantee that the compiler will be able to
+ # find the library via '-lfoo', so tell the user that.
+ mlog.warning(msg.format(l.name, 'name_prefix', l.name, pcfile))
+ return l.name
+
+ def _escape(self, value: T.Union[str, PurePath]) -> str:
+ '''
+ We cannot use quote_arg because it quotes with ' and " which does not
+ work with pkg-config and pkgconf at all.
+ '''
+ # We should always write out paths with / because pkg-config requires
+ # spaces to be quoted with \ and that messes up on Windows:
+ # https://bugs.freedesktop.org/show_bug.cgi?id=103203
+ if isinstance(value, PurePath):
+ value = value.as_posix()
+ return value.replace(' ', r'\ ')
+
+ def _make_relative(self, prefix: T.Union[PurePath, str], subdir: T.Union[PurePath, str]) -> str:
+ prefix = PurePath(prefix)
+ subdir = PurePath(subdir)
+ try:
+ libdir = subdir.relative_to(prefix)
+ except ValueError:
+ libdir = subdir
+ # pathlib joining makes sure absolute libdir is not appended to '${prefix}'
+ return ('${prefix}' / libdir).as_posix()
+
+ def _generate_pkgconfig_file(self, state: ModuleState, deps: DependenciesHelper,
+ subdirs: T.List[str], name: T.Optional[str],
+ description: T.Optional[str], url: str, version: str,
+ pcfile: str, conflicts: T.List[str],
+ variables: T.List[T.Tuple[str, str]],
+ unescaped_variables: T.List[T.Tuple[str, str]],
+ uninstalled: bool = False, dataonly: bool = False,
+ pkgroot: T.Optional[str] = None) -> None:
+ coredata = state.environment.get_coredata()
+ referenced_vars = set()
+ optnames = [x.name for x in BUILTIN_DIR_OPTIONS.keys()]
+
+ if not dataonly:
+ # includedir is always implied, although libdir may not be
+ # needed for header-only libraries
+ referenced_vars |= {'prefix', 'includedir'}
+ if deps.pub_libs or deps.priv_libs:
+ referenced_vars |= {'libdir'}
+ # also automatically infer variables referenced in other variables
+ implicit_vars_warning = False
+ redundant_vars_warning = False
+ varnames = set()
+ varstrings = set()
+ for k, v in variables + unescaped_variables:
+ varnames |= {k}
+ varstrings |= {v}
+ for optname in optnames:
+ optvar = f'${{{optname}}}'
+ if any(x.startswith(optvar) for x in varstrings):
+ if optname in varnames:
+ redundant_vars_warning = True
+ else:
+ # these 3 vars were always "implicit"
+ if dataonly or optname not in {'prefix', 'includedir', 'libdir'}:
+ implicit_vars_warning = True
+ referenced_vars |= {'prefix', optname}
+ if redundant_vars_warning:
+ FeatureDeprecated.single_use('pkgconfig.generate variable for builtin directories', '0.62.0',
+ state.subproject, 'They will be automatically included when referenced',
+ state.current_node)
+ if implicit_vars_warning:
+ FeatureNew.single_use('pkgconfig.generate implicit variable for builtin directories', '0.62.0',
+ state.subproject, location=state.current_node)
+
+ if uninstalled:
+ outdir = os.path.join(state.environment.build_dir, 'meson-uninstalled')
+ if not os.path.exists(outdir):
+ os.mkdir(outdir)
+ prefix = PurePath(state.environment.get_build_dir())
+ srcdir = PurePath(state.environment.get_source_dir())
+ else:
+ outdir = state.environment.scratch_dir
+ prefix = PurePath(_as_str(coredata.get_option(mesonlib.OptionKey('prefix'))))
+ if pkgroot:
+ pkgroot_ = PurePath(pkgroot)
+ if not pkgroot_.is_absolute():
+ pkgroot_ = prefix / pkgroot
+ elif prefix not in pkgroot_.parents:
+ raise mesonlib.MesonException('Pkgconfig prefix cannot be outside of the prefix '
+ 'when pkgconfig.relocatable=true. '
+ f'Pkgconfig prefix is {pkgroot_.as_posix()}.')
+ prefix = PurePath('${pcfiledir}', os.path.relpath(prefix, pkgroot_))
+ fname = os.path.join(outdir, pcfile)
+ with open(fname, 'w', encoding='utf-8') as ofile:
+ for optname in optnames:
+ if optname in referenced_vars - varnames:
+ if optname == 'prefix':
+ ofile.write('prefix={}\n'.format(self._escape(prefix)))
+ else:
+ dirpath = PurePath(_as_str(coredata.get_option(mesonlib.OptionKey(optname))))
+ ofile.write('{}={}\n'.format(optname, self._escape('${prefix}' / dirpath)))
+ if uninstalled and not dataonly:
+ ofile.write('srcdir={}\n'.format(self._escape(srcdir)))
+ if variables or unescaped_variables:
+ ofile.write('\n')
+ for k, v in variables:
+ ofile.write('{}={}\n'.format(k, self._escape(v)))
+ for k, v in unescaped_variables:
+ ofile.write(f'{k}={v}\n')
+ ofile.write('\n')
+ ofile.write(f'Name: {name}\n')
+ if len(description) > 0:
+ ofile.write(f'Description: {description}\n')
+ if len(url) > 0:
+ ofile.write(f'URL: {url}\n')
+ ofile.write(f'Version: {version}\n')
+ reqs_str = deps.format_reqs(deps.pub_reqs)
+ if len(reqs_str) > 0:
+ ofile.write(f'Requires: {reqs_str}\n')
+ reqs_str = deps.format_reqs(deps.priv_reqs)
+ if len(reqs_str) > 0:
+ ofile.write(f'Requires.private: {reqs_str}\n')
+ if len(conflicts) > 0:
+ ofile.write('Conflicts: {}\n'.format(' '.join(conflicts)))
+
+ def generate_libs_flags(libs: T.List[LIBS]) -> T.Iterable[str]:
+ msg = 'Library target {0!r} has {1!r} set. Compilers ' \
+ 'may not find it from its \'-l{2}\' linker flag in the ' \
+ '{3!r} pkg-config file.'
+ Lflags = []
+ for l in libs:
+ if isinstance(l, str):
+ yield l
+ else:
+ install_dir: T.Union[str, bool]
+ if uninstalled:
+ install_dir = os.path.dirname(state.backend.get_target_filename_abs(l))
+ else:
+ _i = l.get_custom_install_dir()
+ install_dir = _i[0] if _i else None
+ if install_dir is False:
+ continue
+ if isinstance(l, build.BuildTarget) and 'cs' in l.compilers:
+ if isinstance(install_dir, str):
+ Lflag = '-r{}/{}'.format(self._escape(self._make_relative(prefix, install_dir)), l.filename)
+ else: # install_dir is True
+ Lflag = '-r${libdir}/%s' % l.filename
+ else:
+ if isinstance(install_dir, str):
+ Lflag = '-L{}'.format(self._escape(self._make_relative(prefix, install_dir)))
+ else: # install_dir is True
+ Lflag = '-L${libdir}'
+ if Lflag not in Lflags:
+ Lflags.append(Lflag)
+ yield Lflag
+ lname = self._get_lname(l, msg, pcfile)
+ # If using a custom suffix, the compiler may not be able to
+ # find the library
+ if isinstance(l, build.BuildTarget) and l.name_suffix_set:
+ mlog.warning(msg.format(l.name, 'name_suffix', lname, pcfile))
+ if isinstance(l, (build.CustomTarget, build.CustomTargetIndex)) or 'cs' not in l.compilers:
+ yield f'-l{lname}'
+
+ def get_uninstalled_include_dirs(libs: T.List[LIBS]) -> T.List[str]:
+ result: T.List[str] = []
+ for l in libs:
+ if isinstance(l, (str, build.CustomTarget, build.CustomTargetIndex)):
+ continue
+ if l.get_subdir() not in result:
+ result.append(l.get_subdir())
+ for i in l.get_include_dirs():
+ curdir = i.get_curdir()
+ for d in i.get_incdirs():
+ path = os.path.join(curdir, d)
+ if path not in result:
+ result.append(path)
+ return result
+
+ def generate_uninstalled_cflags(libs: T.List[LIBS]) -> T.Iterable[str]:
+ for d in get_uninstalled_include_dirs(libs):
+ for basedir in ['${prefix}', '${srcdir}']:
+ path = PurePath(basedir, d)
+ yield '-I%s' % self._escape(path.as_posix())
+
+ if len(deps.pub_libs) > 0:
+ ofile.write('Libs: {}\n'.format(' '.join(generate_libs_flags(deps.pub_libs))))
+ if len(deps.priv_libs) > 0:
+ ofile.write('Libs.private: {}\n'.format(' '.join(generate_libs_flags(deps.priv_libs))))
+
+ cflags: T.List[str] = []
+ if uninstalled:
+ cflags += generate_uninstalled_cflags(deps.pub_libs + deps.priv_libs)
+ else:
+ for d in subdirs:
+ if d == '.':
+ cflags.append('-I${includedir}')
+ else:
+ cflags.append(self._escape(PurePath('-I${includedir}') / d))
+ cflags += [self._escape(f) for f in deps.cflags]
+ if cflags and not dataonly:
+ ofile.write('Cflags: {}\n'.format(' '.join(cflags)))
+
+ @typed_pos_args('pkgconfig.generate', optargs=[(build.SharedLibrary, build.StaticLibrary)])
+ @typed_kwargs(
+ 'pkgconfig.generate',
+ D_MODULE_VERSIONS_KW.evolve(since='0.43.0'),
+ INSTALL_DIR_KW,
+ KwargInfo('conflicts', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('dataonly', bool, default=False, since='0.54.0'),
+ KwargInfo('description', (str, NoneType)),
+ KwargInfo('extra_cflags', ContainerTypeInfo(list, str), default=[], listify=True, since='0.42.0'),
+ KwargInfo('filebase', (str, NoneType), validator=lambda x: 'must not be an empty string' if x == '' else None),
+ KwargInfo('name', (str, NoneType), validator=lambda x: 'must not be an empty string' if x == '' else None),
+ KwargInfo('subdirs', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('url', str, default=''),
+ KwargInfo('version', (str, NoneType)),
+ VARIABLES_KW.evolve(name="unescaped_uninstalled_variables", since='0.59.0'),
+ VARIABLES_KW.evolve(name="unescaped_variables", since='0.59.0'),
+ VARIABLES_KW.evolve(name="uninstalled_variables", since='0.54.0', since_values={dict: '0.56.0'}),
+ VARIABLES_KW.evolve(since='0.41.0', since_values={dict: '0.56.0'}),
+ _PKG_LIBRARIES,
+ _PKG_LIBRARIES.evolve(name='libraries_private'),
+ _PKG_REQUIRES,
+ _PKG_REQUIRES.evolve(name='requires_private'),
+ )
+ def generate(self, state: ModuleState,
+ args: T.Tuple[T.Optional[T.Union[build.SharedLibrary, build.StaticLibrary]]],
+ kwargs: GenerateKw) -> ModuleReturnValue:
+ default_version = state.project_version
+ default_install_dir: T.Optional[str] = None
+ default_description: T.Optional[str] = None
+ default_name: T.Optional[str] = None
+ mainlib: T.Optional[T.Union[build.SharedLibrary, build.StaticLibrary]] = None
+ default_subdirs = ['.']
+ if args[0]:
+ FeatureNew.single_use('pkgconfig.generate optional positional argument', '0.46.0', state.subproject)
+ mainlib = args[0]
+ default_name = mainlib.name
+ default_description = state.project_name + ': ' + mainlib.name
+ install_dir = mainlib.get_custom_install_dir()
+ if install_dir and isinstance(install_dir[0], str):
+ default_install_dir = os.path.join(install_dir[0], 'pkgconfig')
+ else:
+ if kwargs['version'] is None:
+ FeatureNew.single_use('pkgconfig.generate implicit version keyword', '0.46.0', state.subproject)
+ if kwargs['name'] is None:
+ raise build.InvalidArguments(
+ 'pkgconfig.generate: if a library is not passed as a '
+ 'positional argument, the name keyword argument is '
+ 'required.')
+
+ dataonly = kwargs['dataonly']
+ if dataonly:
+ default_subdirs = []
+ blocked_vars = ['libraries', 'libraries_private', 'requires_private', 'extra_cflags', 'subdirs']
+ if any(kwargs[k] for k in blocked_vars): # type: ignore
+ raise mesonlib.MesonException(f'Cannot combine dataonly with any of {blocked_vars}')
+ default_install_dir = os.path.join(state.environment.get_datadir(), 'pkgconfig')
+
+ subdirs = kwargs['subdirs'] or default_subdirs
+ version = kwargs['version'] if kwargs['version'] is not None else default_version
+ name = kwargs['name'] if kwargs['name'] is not None else default_name
+ assert isinstance(name, str), 'for mypy'
+ filebase = kwargs['filebase'] if kwargs['filebase'] is not None else name
+ description = kwargs['description'] if kwargs['description'] is not None else default_description
+ url = kwargs['url']
+ conflicts = kwargs['conflicts']
+
+ # Prepend the main library to public libraries list. This is required
+ # so dep.add_pub_libs() can handle dependency ordering correctly and put
+ # extra libraries after the main library.
+ libraries = kwargs['libraries'].copy()
+ if mainlib:
+ libraries.insert(0, mainlib)
+
+ deps = DependenciesHelper(state, filebase, self._metadata)
+ deps.add_pub_libs(libraries)
+ deps.add_priv_libs(kwargs['libraries_private'])
+ deps.add_pub_reqs(kwargs['requires'])
+ deps.add_priv_reqs(kwargs['requires_private'])
+ deps.add_cflags(kwargs['extra_cflags'])
+
+ dversions = kwargs['d_module_versions']
+ if dversions:
+ compiler = state.environment.coredata.compilers.host.get('d')
+ if compiler:
+ deps.add_cflags(compiler.get_feature_args({'versions': dversions}, None))
+
+ deps.remove_dups()
+
+ def parse_variable_list(vardict: T.Dict[str, str]) -> T.List[T.Tuple[str, str]]:
+ reserved = ['prefix', 'libdir', 'includedir']
+ variables = []
+ for name, value in vardict.items():
+ if not dataonly and name in reserved:
+ raise mesonlib.MesonException(f'Variable "{name}" is reserved')
+ variables.append((name, value))
+ return variables
+
+ variables = parse_variable_list(kwargs['variables'])
+ unescaped_variables = parse_variable_list(kwargs['unescaped_variables'])
+
+ pcfile = filebase + '.pc'
+ pkgroot = pkgroot_name = kwargs['install_dir'] or default_install_dir
+ if pkgroot is None:
+ if mesonlib.is_freebsd():
+ pkgroot = os.path.join(_as_str(state.environment.coredata.get_option(mesonlib.OptionKey('prefix'))), 'libdata', 'pkgconfig')
+ pkgroot_name = os.path.join('{prefix}', 'libdata', 'pkgconfig')
+ elif mesonlib.is_haiku():
+ pkgroot = os.path.join(_as_str(state.environment.coredata.get_option(mesonlib.OptionKey('prefix'))), 'develop', 'lib', 'pkgconfig')
+ pkgroot_name = os.path.join('{prefix}', 'develop', 'lib', 'pkgconfig')
+ else:
+ pkgroot = os.path.join(_as_str(state.environment.coredata.get_option(mesonlib.OptionKey('libdir'))), 'pkgconfig')
+ pkgroot_name = os.path.join('{libdir}', 'pkgconfig')
+ relocatable = state.get_option('relocatable', module='pkgconfig')
+ self._generate_pkgconfig_file(state, deps, subdirs, name, description, url,
+ version, pcfile, conflicts, variables,
+ unescaped_variables, False, dataonly,
+ pkgroot=pkgroot if relocatable else None)
+ res = build.Data([mesonlib.File(True, state.environment.get_scratch_dir(), pcfile)], pkgroot, pkgroot_name, None, state.subproject, install_tag='devel')
+ variables = parse_variable_list(kwargs['uninstalled_variables'])
+ unescaped_variables = parse_variable_list(kwargs['unescaped_uninstalled_variables'])
+
+ pcfile = filebase + '-uninstalled.pc'
+ self._generate_pkgconfig_file(state, deps, subdirs, name, description, url,
+ version, pcfile, conflicts, variables,
+ unescaped_variables, uninstalled=True, dataonly=dataonly)
+ # Associate the main library with this generated pc file. If the library
+ # is used in any subsequent call to the generated, it will generate a
+ # 'Requires:' or 'Requires.private:'.
+ # Backward compatibility: We used to set 'generated_pc' on all public
+ # libraries instead of just the main one. Keep doing that but warn if
+ # anyone is relying on that deprecated behaviour.
+ if mainlib:
+ if mainlib.get_id() not in self._metadata:
+ self._metadata[mainlib.get_id()] = MetaData(
+ filebase, name, state.current_node)
+ else:
+ mlog.warning('Already generated a pkg-config file for', mlog.bold(mainlib.name))
+ else:
+ for lib in deps.pub_libs:
+ if not isinstance(lib, str) and lib.get_id() not in self._metadata:
+ self._metadata[lib.get_id()] = MetaData(
+ filebase, name, state.current_node)
+ return ModuleReturnValue(res, [res])
+
+
+def initialize(interp: Interpreter) -> PkgConfigModule:
+ return PkgConfigModule()
diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py
new file mode 100644
index 0000000..16d3ac4
--- /dev/null
+++ b/mesonbuild/modules/python.py
@@ -0,0 +1,835 @@
+# Copyright 2018 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.
+from __future__ import annotations
+
+from pathlib import Path
+import copy
+import functools
+import json
+import os
+import shutil
+import typing as T
+
+from . import ExtensionModule, ModuleInfo
+from .. import mesonlib
+from .. import mlog
+from ..coredata import UserFeatureOption
+from ..build import known_shmod_kwargs
+from ..dependencies import DependencyMethods, PkgConfigDependency, NotFoundDependency, SystemDependency, ExtraFrameworkDependency
+from ..dependencies.base import process_method_kw
+from ..dependencies.detect import get_dep_identifier
+from ..environment import detect_cpu_family
+from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitted_dependency_kwargs
+from ..interpreter import primitives as P_OBJ
+from ..interpreter.type_checking import NoneType, PRESERVE_PATH_KW
+from ..interpreterbase import (
+ noPosargs, noKwargs, permittedKwargs, ContainerTypeInfo,
+ InvalidArguments, typed_pos_args, typed_kwargs, KwargInfo,
+ FeatureNew, FeatureNewKwargs, disablerIfNotFound
+)
+from ..mesonlib import MachineChoice
+from ..programs import ExternalProgram, NonExistingExternalProgram
+
+if T.TYPE_CHECKING:
+ from typing_extensions import TypedDict
+
+ from . import ModuleState
+ from ..build import SharedModule, Data
+ from ..dependencies import ExternalDependency, Dependency
+ from ..dependencies.factory import DependencyGenerator
+ from ..environment import Environment
+ from ..interpreter import Interpreter
+ from ..interpreter.kwargs import ExtractRequired
+ from ..interpreterbase.interpreterbase import TYPE_var, TYPE_kwargs
+
+ class PythonIntrospectionDict(TypedDict):
+
+ install_paths: T.Dict[str, str]
+ is_pypy: bool
+ is_venv: bool
+ link_libpython: bool
+ sysconfig_paths: T.Dict[str, str]
+ paths: T.Dict[str, str]
+ platform: str
+ suffix: str
+ variables: T.Dict[str, str]
+ version: str
+
+ class PyInstallKw(TypedDict):
+
+ pure: T.Optional[bool]
+ subdir: str
+ install_tag: T.Optional[str]
+
+ class FindInstallationKw(ExtractRequired):
+
+ disabler: bool
+ modules: T.List[str]
+ pure: T.Optional[bool]
+
+ _Base = ExternalDependency
+else:
+ _Base = object
+
+
+mod_kwargs = {'subdir'}
+mod_kwargs.update(known_shmod_kwargs)
+mod_kwargs -= {'name_prefix', 'name_suffix'}
+
+
+class _PythonDependencyBase(_Base):
+
+ def __init__(self, python_holder: 'PythonInstallation', embed: bool):
+ self.embed = embed
+ self.version: str = python_holder.version
+ self.platform = python_holder.platform
+ self.variables = python_holder.variables
+ self.paths = python_holder.paths
+ # The "-embed" version of python.pc / python-config was introduced in 3.8,
+ # and distutils extension linking was changed to be considered a non embed
+ # usage. Before then, this dependency always uses the embed=True handling
+ # because that is the only one that exists.
+ #
+ # On macOS and some Linux distros (Debian) distutils doesn't link extensions
+ # against libpython, even on 3.7 and below. We call into distutils and
+ # mirror its behavior. See https://github.com/mesonbuild/meson/issues/4117
+ self.link_libpython = python_holder.link_libpython or embed
+ self.info: T.Optional[T.Dict[str, str]] = None
+ if mesonlib.version_compare(self.version, '>= 3.0'):
+ self.major_version = 3
+ else:
+ self.major_version = 2
+
+
+class PythonPkgConfigDependency(PkgConfigDependency, _PythonDependencyBase):
+
+ def __init__(self, name: str, environment: 'Environment',
+ kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation',
+ libpc: bool = False):
+ if libpc:
+ mlog.debug(f'Searching for {name!r} via pkgconfig lookup in LIBPC')
+ else:
+ mlog.debug(f'Searching for {name!r} via fallback pkgconfig lookup in default paths')
+
+ PkgConfigDependency.__init__(self, name, environment, kwargs)
+ _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False))
+
+ if libpc and not self.is_found:
+ mlog.debug(f'"python-{self.version}" could not be found in LIBPC, this is likely due to a relocated python installation')
+
+ # pkg-config files are usually accurate starting with python 3.8
+ if not self.link_libpython and mesonlib.version_compare(self.version, '< 3.8'):
+ self.link_args = []
+
+
+class PythonFrameworkDependency(ExtraFrameworkDependency, _PythonDependencyBase):
+
+ def __init__(self, name: str, environment: 'Environment',
+ kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'):
+ ExtraFrameworkDependency.__init__(self, name, environment, kwargs)
+ _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False))
+
+
+class PythonSystemDependency(SystemDependency, _PythonDependencyBase):
+
+ def __init__(self, name: str, environment: 'Environment',
+ kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'):
+ SystemDependency.__init__(self, name, environment, kwargs)
+ _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False))
+
+ if mesonlib.is_windows():
+ self._find_libpy_windows(environment)
+ else:
+ self._find_libpy(installation, environment)
+
+ if not self.link_libpython:
+ # match pkg-config behavior
+ self.link_args = []
+
+ if not self.clib_compiler.has_header('Python.h', '', environment, extra_args=self.compile_args):
+ self.is_found = False
+
+ def _find_libpy(self, python_holder: 'PythonInstallation', environment: 'Environment') -> None:
+ if python_holder.is_pypy:
+ if self.major_version == 3:
+ libname = 'pypy3-c'
+ else:
+ libname = 'pypy-c'
+ libdir = os.path.join(self.variables.get('base'), 'bin')
+ libdirs = [libdir]
+ else:
+ libname = f'python{self.version}'
+ if 'DEBUG_EXT' in self.variables:
+ libname += self.variables['DEBUG_EXT']
+ if 'ABIFLAGS' in self.variables:
+ libname += self.variables['ABIFLAGS']
+ libdirs = []
+
+ largs = self.clib_compiler.find_library(libname, environment, libdirs)
+ if largs is not None:
+ self.link_args = largs
+
+ self.is_found = largs is not None or not self.link_libpython
+
+ inc_paths = mesonlib.OrderedSet([
+ self.variables.get('INCLUDEPY'),
+ self.paths.get('include'),
+ self.paths.get('platinclude')])
+
+ self.compile_args += ['-I' + path for path in inc_paths if path]
+
+ def _get_windows_python_arch(self) -> T.Optional[str]:
+ if self.platform == 'mingw':
+ pycc = self.variables.get('CC')
+ if pycc.startswith('x86_64'):
+ return '64'
+ elif pycc.startswith(('i686', 'i386')):
+ return '32'
+ else:
+ mlog.log(f'MinGW Python built with unknown CC {pycc!r}, please file a bug')
+ return None
+ elif self.platform == 'win32':
+ return '32'
+ elif self.platform in {'win64', 'win-amd64'}:
+ return '64'
+ mlog.log(f'Unknown Windows Python platform {self.platform!r}')
+ return None
+
+ def _get_windows_link_args(self) -> T.Optional[T.List[str]]:
+ if self.platform.startswith('win'):
+ vernum = self.variables.get('py_version_nodot')
+ verdot = self.variables.get('py_version_short')
+ imp_lower = self.variables.get('implementation_lower', 'python')
+ if self.static:
+ libpath = Path('libs') / f'libpython{vernum}.a'
+ else:
+ comp = self.get_compiler()
+ if comp.id == "gcc":
+ if imp_lower == 'pypy' and verdot == '3.8':
+ # The naming changed between 3.8 and 3.9
+ libpath = Path('libpypy3-c.dll')
+ elif imp_lower == 'pypy':
+ libpath = Path(f'libpypy{verdot}-c.dll')
+ else:
+ libpath = Path(f'python{vernum}.dll')
+ else:
+ libpath = Path('libs') / f'python{vernum}.lib'
+ # base_prefix to allow for virtualenvs.
+ lib = Path(self.variables.get('base_prefix')) / libpath
+ elif self.platform == 'mingw':
+ if self.static:
+ libname = self.variables.get('LIBRARY')
+ else:
+ libname = self.variables.get('LDLIBRARY')
+ lib = Path(self.variables.get('LIBDIR')) / libname
+ else:
+ raise mesonlib.MesonBugException(
+ 'On a Windows path, but the OS doesn\'t appear to be Windows or MinGW.')
+ if not lib.exists():
+ mlog.log('Could not find Python3 library {!r}'.format(str(lib)))
+ return None
+ return [str(lib)]
+
+ def _find_libpy_windows(self, env: 'Environment') -> None:
+ '''
+ Find python3 libraries on Windows and also verify that the arch matches
+ what we are building for.
+ '''
+ pyarch = self._get_windows_python_arch()
+ if pyarch is None:
+ self.is_found = False
+ return
+ arch = detect_cpu_family(env.coredata.compilers.host)
+ if arch == 'x86':
+ arch = '32'
+ elif arch == 'x86_64':
+ arch = '64'
+ else:
+ # We can't cross-compile Python 3 dependencies on Windows yet
+ mlog.log(f'Unknown architecture {arch!r} for',
+ mlog.bold(self.name))
+ self.is_found = False
+ return
+ # Pyarch ends in '32' or '64'
+ if arch != pyarch:
+ mlog.log('Need', mlog.bold(self.name), f'for {arch}-bit, but found {pyarch}-bit')
+ self.is_found = False
+ return
+ # This can fail if the library is not found
+ largs = self._get_windows_link_args()
+ if largs is None:
+ self.is_found = False
+ return
+ self.link_args = largs
+ # Compile args
+ inc_paths = mesonlib.OrderedSet([
+ self.variables.get('INCLUDEPY'),
+ self.paths.get('include'),
+ self.paths.get('platinclude')])
+
+ self.compile_args += ['-I' + path for path in inc_paths if path]
+
+ # https://sourceforge.net/p/mingw-w64/mailman/message/30504611/
+ # https://github.com/python/cpython/pull/100137
+ if pyarch == '64' and mesonlib.version_compare(self.version, '<3.12'):
+ self.compile_args += ['-DMS_WIN64']
+
+ self.is_found = True
+
+
+def python_factory(env: 'Environment', for_machine: 'MachineChoice',
+ kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods],
+ installation: 'PythonInstallation') -> T.List['DependencyGenerator']:
+ # We can't use the factory_methods decorator here, as we need to pass the
+ # extra installation argument
+ embed = kwargs.get('embed', False)
+ candidates: T.List['DependencyGenerator'] = []
+ pkg_version = installation.variables.get('LDVERSION') or installation.version
+
+ if DependencyMethods.PKGCONFIG in methods:
+ pkg_libdir = installation.variables.get('LIBPC')
+ pkg_embed = '-embed' if embed and mesonlib.version_compare(installation.version, '>=3.8') else ''
+ pkg_name = f'python-{pkg_version}{pkg_embed}'
+
+ # If python-X.Y.pc exists in LIBPC, we will try to use it
+ def wrap_in_pythons_pc_dir(name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
+ installation: 'PythonInstallation') -> 'ExternalDependency':
+ if not pkg_libdir:
+ # there is no LIBPC, so we can't search in it
+ return NotFoundDependency('python', env)
+
+ old_pkg_libdir = os.environ.pop('PKG_CONFIG_LIBDIR', None)
+ old_pkg_path = os.environ.pop('PKG_CONFIG_PATH', None)
+ os.environ['PKG_CONFIG_LIBDIR'] = pkg_libdir
+ try:
+ return PythonPkgConfigDependency(name, env, kwargs, installation, True)
+ finally:
+ def set_env(name, value):
+ if value is not None:
+ os.environ[name] = value
+ elif name in os.environ:
+ del os.environ[name]
+ set_env('PKG_CONFIG_LIBDIR', old_pkg_libdir)
+ set_env('PKG_CONFIG_PATH', old_pkg_path)
+
+ candidates.append(functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation))
+ # We only need to check both, if a python install has a LIBPC. It might point to the wrong location,
+ # e.g. relocated / cross compilation, but the presence of LIBPC indicates we should definitely look for something.
+ if pkg_libdir is not None:
+ candidates.append(functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation))
+
+ if DependencyMethods.SYSTEM in methods:
+ candidates.append(functools.partial(PythonSystemDependency, 'python', env, kwargs, installation))
+
+ if DependencyMethods.EXTRAFRAMEWORK in methods:
+ nkwargs = kwargs.copy()
+ if mesonlib.version_compare(pkg_version, '>= 3'):
+ # There is a python in /System/Library/Frameworks, but that's python 2.x,
+ # Python 3 will always be in /Library
+ nkwargs['paths'] = ['/Library/Frameworks']
+ candidates.append(functools.partial(PythonFrameworkDependency, 'Python', env, nkwargs, installation))
+
+ return candidates
+
+
+INTROSPECT_COMMAND = '''\
+import os.path
+import sysconfig
+import json
+import sys
+import distutils.command.install
+
+def get_distutils_paths(scheme=None, prefix=None):
+ import distutils.dist
+ distribution = distutils.dist.Distribution()
+ install_cmd = distribution.get_command_obj('install')
+ if prefix is not None:
+ install_cmd.prefix = prefix
+ if scheme:
+ install_cmd.select_scheme(scheme)
+ install_cmd.finalize_options()
+ return {
+ 'data': install_cmd.install_data,
+ 'include': os.path.dirname(install_cmd.install_headers),
+ 'platlib': install_cmd.install_platlib,
+ 'purelib': install_cmd.install_purelib,
+ 'scripts': install_cmd.install_scripts,
+ }
+
+# On Debian derivatives, the Python interpreter shipped by the distribution uses
+# a custom install scheme, deb_system, for the system install, and changes the
+# default scheme to a custom one pointing to /usr/local and replacing
+# site-packages with dist-packages.
+# See https://github.com/mesonbuild/meson/issues/8739.
+# XXX: We should be using sysconfig, but Debian only patches distutils.
+
+if 'deb_system' in distutils.command.install.INSTALL_SCHEMES:
+ paths = get_distutils_paths(scheme='deb_system')
+ install_paths = get_distutils_paths(scheme='deb_system', prefix='')
+else:
+ paths = sysconfig.get_paths()
+ empty_vars = {'base': '', 'platbase': '', 'installed_base': ''}
+ install_paths = sysconfig.get_paths(vars=empty_vars)
+
+def links_against_libpython():
+ from distutils.core import Distribution, Extension
+ cmd = Distribution().get_command_obj('build_ext')
+ cmd.ensure_finalized()
+ return bool(cmd.get_libraries(Extension('dummy', [])))
+
+variables = sysconfig.get_config_vars()
+variables.update({'base_prefix': getattr(sys, 'base_prefix', sys.prefix)})
+
+if sys.version_info < (3, 0):
+ suffix = variables.get('SO')
+elif sys.version_info < (3, 8, 7):
+ # https://bugs.python.org/issue?@action=redirect&bpo=39825
+ from distutils.sysconfig import get_config_var
+ suffix = get_config_var('EXT_SUFFIX')
+else:
+ suffix = variables.get('EXT_SUFFIX')
+
+print(json.dumps({
+ 'variables': variables,
+ 'paths': paths,
+ 'sysconfig_paths': sysconfig.get_paths(),
+ 'install_paths': install_paths,
+ 'version': sysconfig.get_python_version(),
+ 'platform': sysconfig.get_platform(),
+ 'is_pypy': '__pypy__' in sys.builtin_module_names,
+ 'is_venv': sys.prefix != variables['base_prefix'],
+ 'link_libpython': links_against_libpython(),
+ 'suffix': suffix,
+}))
+'''
+
+
+class PythonExternalProgram(ExternalProgram):
+ def __init__(self, name: str, command: T.Optional[T.List[str]] = None,
+ ext_prog: T.Optional[ExternalProgram] = None):
+ if ext_prog is None:
+ super().__init__(name, command=command, silent=True)
+ else:
+ self.name = name
+ self.command = ext_prog.command
+ self.path = ext_prog.path
+
+ # We want strong key values, so we always populate this with bogus data.
+ # Otherwise to make the type checkers happy we'd have to do .get() for
+ # everycall, even though we know that the introspection data will be
+ # complete
+ self.info: 'PythonIntrospectionDict' = {
+ 'install_paths': {},
+ 'is_pypy': False,
+ 'is_venv': False,
+ 'link_libpython': False,
+ 'sysconfig_paths': {},
+ 'paths': {},
+ 'platform': 'sentinal',
+ 'variables': {},
+ 'version': '0.0',
+ }
+ self.pure: bool = True
+
+ def _check_version(self, version: str) -> bool:
+ if self.name == 'python2':
+ return mesonlib.version_compare(version, '< 3.0')
+ elif self.name == 'python3':
+ return mesonlib.version_compare(version, '>= 3.0')
+ return True
+
+ def sanity(self, state: T.Optional['ModuleState'] = None) -> bool:
+ # Sanity check, we expect to have something that at least quacks in tune
+ from tempfile import NamedTemporaryFile
+ with NamedTemporaryFile(suffix='.py', delete=False, mode='w', encoding='utf-8') as tf:
+ tmpfilename = tf.name
+ tf.write(INTROSPECT_COMMAND)
+ cmd = self.get_command() + [tmpfilename]
+ p, stdout, stderr = mesonlib.Popen_safe(cmd)
+ os.unlink(tmpfilename)
+ try:
+ info = json.loads(stdout)
+ except json.JSONDecodeError:
+ info = None
+ mlog.debug('Could not introspect Python (%s): exit code %d' % (str(p.args), p.returncode))
+ mlog.debug('Program stdout:\n')
+ mlog.debug(stdout)
+ mlog.debug('Program stderr:\n')
+ mlog.debug(stderr)
+
+ if info is not None and self._check_version(info['version']):
+ self.info = T.cast('PythonIntrospectionDict', info)
+ self.platlib = self._get_path(state, 'platlib')
+ self.purelib = self._get_path(state, 'purelib')
+ return True
+ else:
+ return False
+
+ def _get_path(self, state: T.Optional['ModuleState'], key: str) -> None:
+ rel_path = self.info['install_paths'][key][1:]
+ if not state:
+ # This happens only from run_project_tests.py
+ return rel_path
+ value = state.get_option(f'{key}dir', module='python')
+ if value:
+ if state.is_user_defined_option('install_env', module='python'):
+ raise mesonlib.MesonException(f'python.{key}dir and python.install_env are mutually exclusive')
+ return value
+
+ install_env = state.get_option('install_env', module='python')
+ if install_env == 'auto':
+ install_env = 'venv' if self.info['is_venv'] else 'system'
+
+ if install_env == 'system':
+ rel_path = os.path.join(self.info['variables']['prefix'], rel_path)
+ elif install_env == 'venv':
+ if not self.info['is_venv']:
+ raise mesonlib.MesonException('python.install_env cannot be set to "venv" unless you are in a venv!')
+ # inside a venv, deb_system is *never* active hence info['paths'] may be wrong
+ rel_path = self.info['sysconfig_paths'][key]
+
+ return rel_path
+
+
+_PURE_KW = KwargInfo('pure', (bool, NoneType))
+_SUBDIR_KW = KwargInfo('subdir', str, default='')
+
+
+class PythonInstallation(ExternalProgramHolder):
+ def __init__(self, python: 'PythonExternalProgram', interpreter: 'Interpreter'):
+ ExternalProgramHolder.__init__(self, python, interpreter)
+ info = python.info
+ prefix = self.interpreter.environment.coredata.get_option(mesonlib.OptionKey('prefix'))
+ assert isinstance(prefix, str), 'for mypy'
+ self.variables = info['variables']
+ self.suffix = info['suffix']
+ self.paths = info['paths']
+ self.pure = python.pure
+ self.platlib_install_path = os.path.join(prefix, python.platlib)
+ self.purelib_install_path = os.path.join(prefix, python.purelib)
+ self.version = info['version']
+ self.platform = info['platform']
+ self.is_pypy = info['is_pypy']
+ self.link_libpython = info['link_libpython']
+ self.methods.update({
+ 'extension_module': self.extension_module_method,
+ 'dependency': self.dependency_method,
+ 'install_sources': self.install_sources_method,
+ 'get_install_dir': self.get_install_dir_method,
+ 'language_version': self.language_version_method,
+ 'found': self.found_method,
+ 'has_path': self.has_path_method,
+ 'get_path': self.get_path_method,
+ 'has_variable': self.has_variable_method,
+ 'get_variable': self.get_variable_method,
+ 'path': self.path_method,
+ })
+
+ @permittedKwargs(mod_kwargs)
+ def extension_module_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'SharedModule':
+ if 'install_dir' in kwargs:
+ if 'subdir' in kwargs:
+ raise InvalidArguments('"subdir" and "install_dir" are mutually exclusive')
+ else:
+ subdir = kwargs.pop('subdir', '')
+ if not isinstance(subdir, str):
+ raise InvalidArguments('"subdir" argument must be a string.')
+
+ kwargs['install_dir'] = self._get_install_dir_impl(False, subdir)
+
+ new_deps = mesonlib.extract_as_list(kwargs, 'dependencies')
+ has_pydep = any(isinstance(dep, _PythonDependencyBase) for dep in new_deps)
+ if not has_pydep:
+ pydep = self._dependency_method_impl({})
+ if not pydep.found():
+ raise mesonlib.MesonException('Python dependency not found')
+ new_deps.append(pydep)
+ FeatureNew.single_use('python_installation.extension_module with implicit dependency on python',
+ '0.63.0', self.subproject, 'use python_installation.dependency()',
+ self.current_node)
+ kwargs['dependencies'] = new_deps
+
+ # msys2's python3 has "-cpython-36m.dll", we have to be clever
+ # FIXME: explain what the specific cleverness is here
+ split, suffix = self.suffix.rsplit('.', 1)
+ args[0] += split
+
+ kwargs['name_prefix'] = ''
+ kwargs['name_suffix'] = suffix
+
+ if 'gnu_symbol_visibility' not in kwargs and \
+ (self.is_pypy or mesonlib.version_compare(self.version, '>=3.9')):
+ kwargs['gnu_symbol_visibility'] = 'inlineshidden'
+
+ return self.interpreter.func_shared_module(None, args, kwargs)
+
+ def _dependency_method_impl(self, kwargs: TYPE_kwargs) -> Dependency:
+ for_machine = self.interpreter.machine_from_native_kwarg(kwargs)
+ identifier = get_dep_identifier(self._full_path(), kwargs)
+
+ dep = self.interpreter.coredata.deps[for_machine].get(identifier)
+ if dep is not None:
+ return dep
+
+ new_kwargs = kwargs.copy()
+ new_kwargs['required'] = False
+ methods = process_method_kw({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM}, kwargs)
+ # it's theoretically (though not practically) possible to not bind dep, let's ensure it is.
+ dep: Dependency = NotFoundDependency('python', self.interpreter.environment)
+ for d in python_factory(self.interpreter.environment, for_machine, new_kwargs, methods, self):
+ dep = d()
+ if dep.found():
+ break
+
+ self.interpreter.coredata.deps[for_machine].put(identifier, dep)
+ return dep
+
+ @disablerIfNotFound
+ @permittedKwargs(permitted_dependency_kwargs | {'embed'})
+ @FeatureNewKwargs('python_installation.dependency', '0.53.0', ['embed'])
+ @noPosargs
+ def dependency_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'Dependency':
+ disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
+ if disabled:
+ mlog.log('Dependency', mlog.bold('python'), 'skipped: feature', mlog.bold(feature), 'disabled')
+ return NotFoundDependency('python', self.interpreter.environment)
+ else:
+ dep = self._dependency_method_impl(kwargs)
+ if required and not dep.found():
+ raise mesonlib.MesonException('Python dependency not found')
+ return dep
+
+ @typed_pos_args('install_data', varargs=(str, mesonlib.File))
+ @typed_kwargs(
+ 'python_installation.install_sources',
+ _PURE_KW,
+ _SUBDIR_KW,
+ PRESERVE_PATH_KW,
+ KwargInfo('install_tag', (str, NoneType), since='0.60.0')
+ )
+ def install_sources_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File]]],
+ kwargs: 'PyInstallKw') -> 'Data':
+ tag = kwargs['install_tag'] or 'python-runtime'
+ pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure
+ install_dir = self._get_install_dir_impl(pure, kwargs['subdir'])
+ return self.interpreter.install_data_impl(
+ self.interpreter.source_strings_to_files(args[0]),
+ install_dir,
+ mesonlib.FileMode(), rename=None, tag=tag, install_data_type='python',
+ install_dir_name=install_dir.optname,
+ preserve_path=kwargs['preserve_path'])
+
+ @noPosargs
+ @typed_kwargs('python_installation.install_dir', _PURE_KW, _SUBDIR_KW)
+ def get_install_dir_method(self, args: T.List['TYPE_var'], kwargs: 'PyInstallKw') -> str:
+ pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure
+ return self._get_install_dir_impl(pure, kwargs['subdir'])
+
+ def _get_install_dir_impl(self, pure: bool, subdir: str) -> P_OBJ.OptionString:
+ if pure:
+ base = self.purelib_install_path
+ name = '{py_purelib}'
+ else:
+ base = self.platlib_install_path
+ name = '{py_platlib}'
+
+ return P_OBJ.OptionString(os.path.join(base, subdir), os.path.join(name, subdir))
+
+ @noPosargs
+ @noKwargs
+ def language_version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return self.version
+
+ @typed_pos_args('python_installation.has_path', str)
+ @noKwargs
+ def has_path_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool:
+ return args[0] in self.paths
+
+ @typed_pos_args('python_installation.get_path', str, optargs=[object])
+ @noKwargs
+ def get_path_method(self, args: T.Tuple[str, T.Optional['TYPE_var']], kwargs: 'TYPE_kwargs') -> 'TYPE_var':
+ path_name, fallback = args
+ try:
+ return self.paths[path_name]
+ except KeyError:
+ if fallback is not None:
+ return fallback
+ raise InvalidArguments(f'{path_name} is not a valid path name')
+
+ @typed_pos_args('python_installation.has_variable', str)
+ @noKwargs
+ def has_variable_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool:
+ return args[0] in self.variables
+
+ @typed_pos_args('python_installation.get_variable', str, optargs=[object])
+ @noKwargs
+ def get_variable_method(self, args: T.Tuple[str, T.Optional['TYPE_var']], kwargs: 'TYPE_kwargs') -> 'TYPE_var':
+ var_name, fallback = args
+ try:
+ return self.variables[var_name]
+ except KeyError:
+ if fallback is not None:
+ return fallback
+ raise InvalidArguments(f'{var_name} is not a valid variable name')
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('Python module path method', '0.50.0')
+ def path_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+ return super().path_method(args, kwargs)
+
+
+class PythonModule(ExtensionModule):
+
+ INFO = ModuleInfo('python', '0.46.0')
+
+ def __init__(self, interpreter: 'Interpreter') -> None:
+ super().__init__(interpreter)
+ self.installations: T.Dict[str, ExternalProgram] = {}
+ self.methods.update({
+ 'find_installation': self.find_installation,
+ })
+
+ # https://www.python.org/dev/peps/pep-0397/
+ @staticmethod
+ def _get_win_pythonpath(name_or_path: str) -> T.Optional[str]:
+ if name_or_path not in ['python2', 'python3']:
+ return None
+ if not shutil.which('py'):
+ # program not installed, return without an exception
+ return None
+ ver = {'python2': '-2', 'python3': '-3'}[name_or_path]
+ cmd = ['py', ver, '-c', "import sysconfig; print(sysconfig.get_config_var('BINDIR'))"]
+ _, stdout, _ = mesonlib.Popen_safe(cmd)
+ directory = stdout.strip()
+ if os.path.exists(directory):
+ return os.path.join(directory, 'python')
+ else:
+ return None
+
+ def _find_installation_impl(self, state: 'ModuleState', display_name: str, name_or_path: str, required: bool) -> ExternalProgram:
+ if not name_or_path:
+ python = PythonExternalProgram('python3', mesonlib.python_command)
+ else:
+ tmp_python = ExternalProgram.from_entry(display_name, name_or_path)
+ python = PythonExternalProgram(display_name, ext_prog=tmp_python)
+
+ if not python.found() and mesonlib.is_windows():
+ pythonpath = self._get_win_pythonpath(name_or_path)
+ if pythonpath is not None:
+ name_or_path = pythonpath
+ python = PythonExternalProgram(name_or_path)
+
+ # Last ditch effort, python2 or python3 can be named python
+ # on various platforms, let's not give up just yet, if an executable
+ # named python is available and has a compatible version, let's use
+ # it
+ if not python.found() and name_or_path in {'python2', 'python3'}:
+ python = PythonExternalProgram('python')
+
+ if python.found():
+ if python.sanity(state):
+ return python
+ else:
+ sanitymsg = f'{python} is not a valid python or it is missing distutils'
+ if required:
+ raise mesonlib.MesonException(sanitymsg)
+ else:
+ mlog.warning(sanitymsg, location=state.current_node)
+
+ return NonExistingExternalProgram()
+
+ @disablerIfNotFound
+ @typed_pos_args('python.find_installation', optargs=[str])
+ @typed_kwargs(
+ 'python.find_installation',
+ KwargInfo('required', (bool, UserFeatureOption), default=True),
+ KwargInfo('disabler', bool, default=False, since='0.49.0'),
+ KwargInfo('modules', ContainerTypeInfo(list, str), listify=True, default=[], since='0.51.0'),
+ _PURE_KW.evolve(default=True, since='0.64.0'),
+ )
+ def find_installation(self, state: 'ModuleState', args: T.Tuple[T.Optional[str]],
+ kwargs: 'FindInstallationKw') -> ExternalProgram:
+ feature_check = FeatureNew('Passing "feature" option to find_installation', '0.48.0')
+ disabled, required, feature = extract_required_kwarg(kwargs, state.subproject, feature_check)
+
+ # FIXME: this code is *full* of sharp corners. It assumes that it's
+ # going to get a string value (or now a list of length 1), of `python2`
+ # or `python3` which is completely nonsense. On windows the value could
+ # easily be `['py', '-3']`, or `['py', '-3.7']` to get a very specific
+ # version of python. On Linux we might want a python that's not in
+ # $PATH, or that uses a wrapper of some kind.
+ np: T.List[str] = state.environment.lookup_binary_entry(MachineChoice.HOST, 'python') or []
+ fallback = args[0]
+ display_name = fallback or 'python'
+ if not np and fallback is not None:
+ np = [fallback]
+ name_or_path = np[0] if np else None
+
+ if disabled:
+ mlog.log('Program', name_or_path or 'python', 'found:', mlog.red('NO'), '(disabled by:', mlog.bold(feature), ')')
+ return NonExistingExternalProgram()
+
+ python = self.installations.get(name_or_path)
+ if not python:
+ python = self._find_installation_impl(state, display_name, name_or_path, required)
+ self.installations[name_or_path] = python
+
+ want_modules = kwargs['modules']
+ found_modules: T.List[str] = []
+ missing_modules: T.List[str] = []
+ if python.found() and want_modules:
+ for mod in want_modules:
+ p, *_ = mesonlib.Popen_safe(
+ python.command +
+ ['-c', f'import {mod}'])
+ if p.returncode != 0:
+ missing_modules.append(mod)
+ else:
+ found_modules.append(mod)
+
+ msg: T.List['mlog.TV_Loggable'] = ['Program', python.name]
+ if want_modules:
+ msg.append('({})'.format(', '.join(want_modules)))
+ msg.append('found:')
+ if python.found() and not missing_modules:
+ msg.extend([mlog.green('YES'), '({})'.format(' '.join(python.command))])
+ else:
+ msg.append(mlog.red('NO'))
+ if found_modules:
+ msg.append('modules:')
+ msg.append(', '.join(found_modules))
+
+ mlog.log(*msg)
+
+ if not python.found():
+ if required:
+ raise mesonlib.MesonException('{} not found'.format(name_or_path or 'python'))
+ return NonExistingExternalProgram()
+ elif missing_modules:
+ if required:
+ raise mesonlib.MesonException('{} is missing modules: {}'.format(name_or_path or 'python', ', '.join(missing_modules)))
+ return NonExistingExternalProgram()
+ else:
+ python = copy.copy(python)
+ python.pure = kwargs['pure']
+ return python
+
+ raise mesonlib.MesonBugException('Unreachable code was reached (PythonModule.find_installation).')
+
+
+def initialize(interpreter: 'Interpreter') -> PythonModule:
+ mod = PythonModule(interpreter)
+ mod.interpreter.append_holder_map(PythonExternalProgram, PythonInstallation)
+ return mod
diff --git a/mesonbuild/modules/python3.py b/mesonbuild/modules/python3.py
new file mode 100644
index 0000000..065e8d7
--- /dev/null
+++ b/mesonbuild/modules/python3.py
@@ -0,0 +1,85 @@
+# Copyright 2016-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.
+from __future__ import annotations
+
+import sysconfig
+from .. import mesonlib
+
+from . import ExtensionModule, ModuleInfo
+from ..interpreterbase import typed_pos_args, noPosargs, noKwargs, permittedKwargs
+from ..build import known_shmod_kwargs
+from ..programs import ExternalProgram
+
+
+class Python3Module(ExtensionModule):
+
+ INFO = ModuleInfo('python3', '0.38.0', deprecated='0.48.0')
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.methods.update({
+ 'extension_module': self.extension_module,
+ 'find_python': self.find_python,
+ 'language_version': self.language_version,
+ 'sysconfig_path': self.sysconfig_path,
+ })
+
+ @permittedKwargs(known_shmod_kwargs)
+ def extension_module(self, state, args, kwargs):
+ if 'name_prefix' in kwargs:
+ raise mesonlib.MesonException('Name_prefix is set automatically, specifying it is forbidden.')
+ if 'name_suffix' in kwargs:
+ raise mesonlib.MesonException('Name_suffix is set automatically, specifying it is forbidden.')
+ host_system = state.host_machine.system
+ if host_system == 'darwin':
+ # Default suffix is 'dylib' but Python does not use it for extensions.
+ suffix = 'so'
+ elif host_system == 'windows':
+ # On Windows the extension is pyd for some unexplainable reason.
+ suffix = 'pyd'
+ else:
+ suffix = []
+ kwargs['name_prefix'] = ''
+ kwargs['name_suffix'] = suffix
+ return self.interpreter.func_shared_module(None, args, kwargs)
+
+ @noPosargs
+ @noKwargs
+ def find_python(self, state, args, kwargs):
+ command = state.environment.lookup_binary_entry(mesonlib.MachineChoice.HOST, 'python3')
+ if command is not None:
+ py3 = ExternalProgram.from_entry('python3', command)
+ else:
+ py3 = ExternalProgram('python3', mesonlib.python_command, silent=True)
+ return py3
+
+ @noPosargs
+ @noKwargs
+ def language_version(self, state, args, kwargs):
+ return sysconfig.get_python_version()
+
+ @noKwargs
+ @typed_pos_args('python3.sysconfig_path', str)
+ def sysconfig_path(self, state, args, kwargs):
+ path_name = args[0]
+ valid_names = sysconfig.get_path_names()
+ if path_name not in valid_names:
+ raise mesonlib.MesonException(f'{path_name} is not a valid path name {valid_names}.')
+
+ # Get a relative path without a prefix, e.g. lib/python3.6/site-packages
+ return sysconfig.get_path(path_name, vars={'base': '', 'platbase': '', 'installed_base': ''})[1:]
+
+
+def initialize(*args, **kwargs):
+ return Python3Module(*args, **kwargs)
diff --git a/mesonbuild/modules/qt.py b/mesonbuild/modules/qt.py
new file mode 100644
index 0000000..73160c0
--- /dev/null
+++ b/mesonbuild/modules/qt.py
@@ -0,0 +1,608 @@
+# Copyright 2015 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# 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.
+from __future__ import annotations
+
+import os
+import shutil
+import typing as T
+import xml.etree.ElementTree as ET
+
+from . import ModuleReturnValue, ExtensionModule
+from .. import build
+from .. import coredata
+from .. import mlog
+from ..dependencies import find_external_dependency, Dependency, ExternalLibrary
+from ..mesonlib import MesonException, File, version_compare, Popen_safe
+from ..interpreter import extract_required_kwarg
+from ..interpreter.type_checking import INSTALL_DIR_KW, INSTALL_KW, NoneType
+from ..interpreterbase import ContainerTypeInfo, FeatureDeprecated, KwargInfo, noPosargs, FeatureNew, typed_kwargs
+from ..programs import NonExistingExternalProgram
+
+if T.TYPE_CHECKING:
+ from . import ModuleState
+ from ..dependencies.qt import QtPkgConfigDependency, QmakeQtDependency
+ from ..interpreter import Interpreter
+ from ..interpreter import kwargs
+ from ..mesonlib import FileOrString
+ from ..programs import ExternalProgram
+
+ QtDependencyType = T.Union[QtPkgConfigDependency, QmakeQtDependency]
+
+ from typing_extensions import TypedDict
+
+ class ResourceCompilerKwArgs(TypedDict):
+
+ """Keyword arguments for the Resource Compiler method."""
+
+ name: T.Optional[str]
+ sources: T.Sequence[T.Union[FileOrString, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]]
+ extra_args: T.List[str]
+ method: str
+
+ class UICompilerKwArgs(TypedDict):
+
+ """Keyword arguments for the Ui Compiler method."""
+
+ sources: T.Sequence[T.Union[FileOrString, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]]
+ extra_args: T.List[str]
+ method: str
+
+ class MocCompilerKwArgs(TypedDict):
+
+ """Keyword arguments for the Moc Compiler method."""
+
+ sources: T.Sequence[T.Union[FileOrString, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]]
+ headers: T.Sequence[T.Union[FileOrString, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]]
+ extra_args: T.List[str]
+ method: str
+ include_directories: T.List[T.Union[str, build.IncludeDirs]]
+ dependencies: T.List[T.Union[Dependency, ExternalLibrary]]
+
+ class PreprocessKwArgs(TypedDict):
+
+ sources: T.List[FileOrString]
+ moc_sources: T.List[T.Union[FileOrString, build.CustomTarget]]
+ moc_headers: T.List[T.Union[FileOrString, build.CustomTarget]]
+ qresources: T.List[FileOrString]
+ ui_files: T.List[T.Union[FileOrString, build.CustomTarget]]
+ moc_extra_arguments: T.List[str]
+ rcc_extra_arguments: T.List[str]
+ uic_extra_arguments: T.List[str]
+ include_directories: T.List[T.Union[str, build.IncludeDirs]]
+ dependencies: T.List[T.Union[Dependency, ExternalLibrary]]
+ method: str
+
+ class HasToolKwArgs(kwargs.ExtractRequired):
+
+ method: str
+
+ class CompileTranslationsKwArgs(TypedDict):
+
+ build_by_default: bool
+ install: bool
+ install_dir: T.Optional[str]
+ method: str
+ qresource: T.Optional[str]
+ rcc_extra_arguments: T.List[str]
+ ts_files: T.List[T.Union[str, File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]]
+
+class QtBaseModule(ExtensionModule):
+ _tools_detected = False
+ _rcc_supports_depfiles = False
+ _moc_supports_depfiles = False
+
+ def __init__(self, interpreter: 'Interpreter', qt_version: int = 5):
+ ExtensionModule.__init__(self, interpreter)
+ self.qt_version = qt_version
+ # It is important that this list does not change order as the order of
+ # the returned ExternalPrograms will change as well
+ self.tools: T.Dict[str, ExternalProgram] = {
+ 'moc': NonExistingExternalProgram('moc'),
+ 'uic': NonExistingExternalProgram('uic'),
+ 'rcc': NonExistingExternalProgram('rcc'),
+ 'lrelease': NonExistingExternalProgram('lrelease'),
+ }
+ self.methods.update({
+ 'has_tools': self.has_tools,
+ 'preprocess': self.preprocess,
+ 'compile_translations': self.compile_translations,
+ 'compile_resources': self.compile_resources,
+ 'compile_ui': self.compile_ui,
+ 'compile_moc': self.compile_moc,
+ })
+
+ def compilers_detect(self, state: 'ModuleState', qt_dep: 'QtDependencyType') -> None:
+ """Detect Qt (4 or 5) moc, uic, rcc in the specified bindir or in PATH"""
+ wanted = f'== {qt_dep.version}'
+
+ def gen_bins() -> T.Generator[T.Tuple[str, str], None, None]:
+ for b in self.tools:
+ if qt_dep.bindir:
+ yield os.path.join(qt_dep.bindir, b), b
+ if qt_dep.libexecdir:
+ yield os.path.join(qt_dep.libexecdir, b), b
+ # prefer the (official) <tool><version> or (unofficial) <tool>-qt<version>
+ # of the tool to the plain one, as we
+ # don't know what the unsuffixed one points to without calling it.
+ yield f'{b}{qt_dep.qtver}', b
+ yield f'{b}-qt{qt_dep.qtver}', b
+ yield b, b
+
+ for b, name in gen_bins():
+ if self.tools[name].found():
+ continue
+
+ if name == 'lrelease':
+ arg = ['-version']
+ elif version_compare(qt_dep.version, '>= 5'):
+ arg = ['--version']
+ else:
+ arg = ['-v']
+
+ # Ensure that the version of qt and each tool are the same
+ def get_version(p: ExternalProgram) -> str:
+ _, out, err = Popen_safe(p.get_command() + arg)
+ if name == 'lrelease' or not qt_dep.version.startswith('4'):
+ care = out
+ else:
+ care = err
+ return care.rsplit(' ', maxsplit=1)[-1].replace(')', '').strip()
+
+ p = state.find_program(b, required=False,
+ version_func=get_version,
+ wanted=wanted)
+ if p.found():
+ self.tools[name] = p
+
+ def _detect_tools(self, state: 'ModuleState', method: str, required: bool = True) -> None:
+ if self._tools_detected:
+ return
+ self._tools_detected = True
+ mlog.log(f'Detecting Qt{self.qt_version} tools')
+ kwargs = {'required': required, 'modules': 'Core', 'method': method}
+ # Just pick one to make mypy happy
+ qt = T.cast('QtPkgConfigDependency', find_external_dependency(f'qt{self.qt_version}', state.environment, kwargs))
+ if qt.found():
+ # Get all tools and then make sure that they are the right version
+ self.compilers_detect(state, qt)
+ if version_compare(qt.version, '>=5.15.0'):
+ self._moc_supports_depfiles = True
+ else:
+ mlog.warning('moc dependencies will not work properly until you move to Qt >= 5.15', fatal=False)
+ if version_compare(qt.version, '>=5.14.0'):
+ self._rcc_supports_depfiles = True
+ else:
+ mlog.warning('rcc dependencies will not work properly until you move to Qt >= 5.14:',
+ mlog.bold('https://bugreports.qt.io/browse/QTBUG-45460'), fatal=False)
+ else:
+ suffix = f'-qt{self.qt_version}'
+ self.tools['moc'] = NonExistingExternalProgram(name='moc' + suffix)
+ self.tools['uic'] = NonExistingExternalProgram(name='uic' + suffix)
+ self.tools['rcc'] = NonExistingExternalProgram(name='rcc' + suffix)
+ self.tools['lrelease'] = NonExistingExternalProgram(name='lrelease' + suffix)
+
+ @staticmethod
+ def _qrc_nodes(state: 'ModuleState', rcc_file: 'FileOrString') -> T.Tuple[str, T.List[str]]:
+ abspath: str
+ if isinstance(rcc_file, str):
+ abspath = os.path.join(state.environment.source_dir, state.subdir, rcc_file)
+ else:
+ abspath = rcc_file.absolute_path(state.environment.source_dir, state.environment.build_dir)
+ rcc_dirname = os.path.dirname(abspath)
+
+ # FIXME: what error are we actually trying to check here? (probably parse errors?)
+ try:
+ tree = ET.parse(abspath)
+ root = tree.getroot()
+ result: T.List[str] = []
+ for child in root[0]:
+ if child.tag != 'file':
+ mlog.warning("malformed rcc file: ", os.path.join(state.subdir, str(rcc_file)))
+ break
+ elif child.text is None:
+ raise MesonException(f'<file> element without a path in {os.path.join(state.subdir, str(rcc_file))}')
+ else:
+ result.append(child.text)
+
+ return rcc_dirname, result
+ except MesonException:
+ raise
+ except Exception:
+ raise MesonException(f'Unable to parse resource file {abspath}')
+
+ def _parse_qrc_deps(self, state: 'ModuleState',
+ rcc_file_: T.Union['FileOrString', build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]) -> T.List[File]:
+ result: T.List[File] = []
+ inputs: T.Sequence['FileOrString'] = []
+ if isinstance(rcc_file_, (str, File)):
+ inputs = [rcc_file_]
+ else:
+ inputs = rcc_file_.get_outputs()
+
+ for rcc_file in inputs:
+ rcc_dirname, nodes = self._qrc_nodes(state, rcc_file)
+ for resource_path in nodes:
+ # We need to guess if the pointed resource is:
+ # a) in build directory -> implies a generated file
+ # b) in source directory
+ # c) somewhere else external dependency file to bundle
+ #
+ # Also from qrc documentation: relative path are always from qrc file
+ # So relative path must always be computed from qrc file !
+ if os.path.isabs(resource_path):
+ # a)
+ if resource_path.startswith(os.path.abspath(state.environment.build_dir)):
+ resource_relpath = os.path.relpath(resource_path, state.environment.build_dir)
+ result.append(File(is_built=True, subdir='', fname=resource_relpath))
+ # either b) or c)
+ else:
+ result.append(File(is_built=False, subdir=state.subdir, fname=resource_path))
+ else:
+ path_from_rcc = os.path.normpath(os.path.join(rcc_dirname, resource_path))
+ # a)
+ if path_from_rcc.startswith(state.environment.build_dir):
+ result.append(File(is_built=True, subdir=state.subdir, fname=resource_path))
+ # b)
+ else:
+ result.append(File(is_built=False, subdir=state.subdir, fname=path_from_rcc))
+ return result
+
+ @FeatureNew('qt.has_tools', '0.54.0')
+ @noPosargs
+ @typed_kwargs(
+ 'qt.has_tools',
+ KwargInfo('required', (bool, coredata.UserFeatureOption), default=False),
+ KwargInfo('method', str, default='auto'),
+ )
+ def has_tools(self, state: 'ModuleState', args: T.Tuple, kwargs: 'HasToolKwArgs') -> bool:
+ method = kwargs.get('method', 'auto')
+ # We have to cast here because TypedDicts are invariant, even though
+ # ExtractRequiredKwArgs is a subset of HasToolKwArgs, type checkers
+ # will insist this is wrong
+ disabled, required, feature = extract_required_kwarg(kwargs, state.subproject, default=False)
+ if disabled:
+ mlog.log('qt.has_tools skipped: feature', mlog.bold(feature), 'disabled')
+ return False
+ self._detect_tools(state, method, required=False)
+ for tool in self.tools.values():
+ if not tool.found():
+ if required:
+ raise MesonException('Qt tools not found')
+ return False
+ return True
+
+ @FeatureNew('qt.compile_resources', '0.59.0')
+ @noPosargs
+ @typed_kwargs(
+ 'qt.compile_resources',
+ KwargInfo('name', (str, NoneType)),
+ KwargInfo(
+ 'sources',
+ ContainerTypeInfo(list, (File, str, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList), allow_empty=False),
+ listify=True,
+ required=True,
+ ),
+ KwargInfo('extra_args', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('method', str, default='auto')
+ )
+ def compile_resources(self, state: 'ModuleState', args: T.Tuple, kwargs: 'ResourceCompilerKwArgs') -> ModuleReturnValue:
+ """Compile Qt resources files.
+
+ Uses CustomTargets to generate .cpp files from .qrc files.
+ """
+ if any(isinstance(s, (build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)) for s in kwargs['sources']):
+ FeatureNew.single_use('qt.compile_resources: custom_target or generator for "sources" keyword argument',
+ '0.60.0', state.subproject, location=state.current_node)
+ out = self._compile_resources_impl(state, kwargs)
+ return ModuleReturnValue(out, [out])
+
+ def _compile_resources_impl(self, state: 'ModuleState', kwargs: 'ResourceCompilerKwArgs') -> T.List[build.CustomTarget]:
+ # Avoid the FeatureNew when dispatching from preprocess
+ self._detect_tools(state, kwargs['method'])
+ if not self.tools['rcc'].found():
+ err_msg = ("{0} sources specified and couldn't find {1}, "
+ "please check your qt{2} installation")
+ raise MesonException(err_msg.format('RCC', f'rcc-qt{self.qt_version}', self.qt_version))
+
+ # List of generated CustomTargets
+ targets: T.List[build.CustomTarget] = []
+
+ # depfile arguments
+ DEPFILE_ARGS: T.List[str] = ['--depfile', '@DEPFILE@'] if self._rcc_supports_depfiles else []
+
+ name = kwargs['name']
+ sources: T.List['FileOrString'] = []
+ for s in kwargs['sources']:
+ if isinstance(s, (str, File)):
+ sources.append(s)
+ else:
+ sources.extend(s.get_outputs())
+ extra_args = kwargs['extra_args']
+
+ # If a name was set generate a single .cpp file from all of the qrc
+ # files, otherwise generate one .cpp file per qrc file.
+ if name:
+ qrc_deps: T.List[File] = []
+ for s in sources:
+ qrc_deps.extend(self._parse_qrc_deps(state, s))
+
+ res_target = build.CustomTarget(
+ name,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ self.tools['rcc'].get_command() + ['-name', name, '-o', '@OUTPUT@'] + extra_args + ['@INPUT@'] + DEPFILE_ARGS,
+ sources,
+ [f'{name}.cpp'],
+ depend_files=qrc_deps,
+ depfile=f'{name}.d',
+ )
+ targets.append(res_target)
+ else:
+ for rcc_file in sources:
+ qrc_deps = self._parse_qrc_deps(state, rcc_file)
+ if isinstance(rcc_file, str):
+ basename = os.path.basename(rcc_file)
+ else:
+ basename = os.path.basename(rcc_file.fname)
+ name = f'qt{self.qt_version}-{basename.replace(".", "_")}'
+ res_target = build.CustomTarget(
+ name,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ self.tools['rcc'].get_command() + ['-name', '@BASENAME@', '-o', '@OUTPUT@'] + extra_args + ['@INPUT@'] + DEPFILE_ARGS,
+ [rcc_file],
+ [f'{name}.cpp'],
+ depend_files=qrc_deps,
+ depfile=f'{name}.d',
+ )
+ targets.append(res_target)
+
+ return targets
+
+ @FeatureNew('qt.compile_ui', '0.59.0')
+ @noPosargs
+ @typed_kwargs(
+ 'qt.compile_ui',
+ KwargInfo(
+ 'sources',
+ ContainerTypeInfo(list, (File, str, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList), allow_empty=False),
+ listify=True,
+ required=True,
+ ),
+ KwargInfo('extra_args', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('method', str, default='auto')
+ )
+ def compile_ui(self, state: 'ModuleState', args: T.Tuple, kwargs: 'UICompilerKwArgs') -> ModuleReturnValue:
+ """Compile UI resources into cpp headers."""
+ if any(isinstance(s, (build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)) for s in kwargs['sources']):
+ FeatureNew.single_use('qt.compile_ui: custom_target or generator for "sources" keyword argument',
+ '0.60.0', state.subproject, location=state.current_node)
+ out = self._compile_ui_impl(state, kwargs)
+ return ModuleReturnValue(out, [out])
+
+ def _compile_ui_impl(self, state: 'ModuleState', kwargs: 'UICompilerKwArgs') -> build.GeneratedList:
+ # Avoid the FeatureNew when dispatching from preprocess
+ self._detect_tools(state, kwargs['method'])
+ if not self.tools['uic'].found():
+ err_msg = ("{0} sources specified and couldn't find {1}, "
+ "please check your qt{2} installation")
+ raise MesonException(err_msg.format('UIC', f'uic-qt{self.qt_version}', self.qt_version))
+
+ # TODO: This generator isn't added to the generator list in the Interpreter
+ gen = build.Generator(
+ self.tools['uic'],
+ kwargs['extra_args'] + ['-o', '@OUTPUT@', '@INPUT@'],
+ ['ui_@BASENAME@.h'],
+ name=f'Qt{self.qt_version} ui')
+ return gen.process_files(kwargs['sources'], state)
+
+ @FeatureNew('qt.compile_moc', '0.59.0')
+ @noPosargs
+ @typed_kwargs(
+ 'qt.compile_moc',
+ KwargInfo(
+ 'sources',
+ ContainerTypeInfo(list, (File, str, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)),
+ listify=True,
+ default=[],
+ ),
+ KwargInfo(
+ 'headers',
+ ContainerTypeInfo(list, (File, str, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)),
+ listify=True,
+ default=[]
+ ),
+ KwargInfo('extra_args', ContainerTypeInfo(list, str), listify=True, default=[]),
+ KwargInfo('method', str, default='auto'),
+ KwargInfo('include_directories', ContainerTypeInfo(list, (build.IncludeDirs, str)), listify=True, default=[]),
+ KwargInfo('dependencies', ContainerTypeInfo(list, (Dependency, ExternalLibrary)), listify=True, default=[]),
+ )
+ def compile_moc(self, state: 'ModuleState', args: T.Tuple, kwargs: 'MocCompilerKwArgs') -> ModuleReturnValue:
+ if any(isinstance(s, (build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)) for s in kwargs['headers']):
+ FeatureNew.single_use('qt.compile_moc: custom_target or generator for "headers" keyword argument',
+ '0.60.0', state.subproject, location=state.current_node)
+ if any(isinstance(s, (build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)) for s in kwargs['sources']):
+ FeatureNew.single_use('qt.compile_moc: custom_target or generator for "sources" keyword argument',
+ '0.60.0', state.subproject, location=state.current_node)
+ out = self._compile_moc_impl(state, kwargs)
+ return ModuleReturnValue(out, [out])
+
+ def _compile_moc_impl(self, state: 'ModuleState', kwargs: 'MocCompilerKwArgs') -> T.List[build.GeneratedList]:
+ # Avoid the FeatureNew when dispatching from preprocess
+ self._detect_tools(state, kwargs['method'])
+ if not self.tools['moc'].found():
+ err_msg = ("{0} sources specified and couldn't find {1}, "
+ "please check your qt{2} installation")
+ raise MesonException(err_msg.format('MOC', f'uic-qt{self.qt_version}', self.qt_version))
+
+ if not (kwargs['headers'] or kwargs['sources']):
+ raise build.InvalidArguments('At least one of the "headers" or "sources" keyword arguments must be provided and not empty')
+
+ inc = state.get_include_args(include_dirs=kwargs['include_directories'])
+ compile_args: T.List[str] = []
+ for dep in kwargs['dependencies']:
+ compile_args.extend([a for a in dep.get_all_compile_args() if a.startswith(('-I', '-D'))])
+
+ output: T.List[build.GeneratedList] = []
+
+ # depfile arguments (defaults to <output-name>.d)
+ DEPFILE_ARGS: T.List[str] = ['--output-dep-file'] if self._moc_supports_depfiles else []
+
+ arguments = kwargs['extra_args'] + DEPFILE_ARGS + inc + compile_args + ['@INPUT@', '-o', '@OUTPUT@']
+ if kwargs['headers']:
+ moc_gen = build.Generator(
+ self.tools['moc'], arguments, ['moc_@BASENAME@.cpp'],
+ depfile='moc_@BASENAME@.cpp.d',
+ name=f'Qt{self.qt_version} moc header')
+ output.append(moc_gen.process_files(kwargs['headers'], state))
+ if kwargs['sources']:
+ moc_gen = build.Generator(
+ self.tools['moc'], arguments, ['@BASENAME@.moc'],
+ depfile='@BASENAME.moc.d@',
+ name=f'Qt{self.qt_version} moc source')
+ output.append(moc_gen.process_files(kwargs['sources'], state))
+
+ return output
+
+ # We can't use typed_pos_args here, the signature is ambiguous
+ @typed_kwargs(
+ 'qt.preprocess',
+ KwargInfo('sources', ContainerTypeInfo(list, (File, str)), listify=True, default=[], deprecated='0.59.0'),
+ KwargInfo('qresources', ContainerTypeInfo(list, (File, str)), listify=True, default=[]),
+ KwargInfo('ui_files', ContainerTypeInfo(list, (File, str, build.CustomTarget)), listify=True, default=[]),
+ KwargInfo('moc_sources', ContainerTypeInfo(list, (File, str, build.CustomTarget)), listify=True, default=[]),
+ KwargInfo('moc_headers', ContainerTypeInfo(list, (File, str, build.CustomTarget)), listify=True, default=[]),
+ KwargInfo('moc_extra_arguments', ContainerTypeInfo(list, str), listify=True, default=[], since='0.44.0'),
+ KwargInfo('rcc_extra_arguments', ContainerTypeInfo(list, str), listify=True, default=[], since='0.49.0'),
+ KwargInfo('uic_extra_arguments', ContainerTypeInfo(list, str), listify=True, default=[], since='0.49.0'),
+ KwargInfo('method', str, default='auto'),
+ KwargInfo('include_directories', ContainerTypeInfo(list, (build.IncludeDirs, str)), listify=True, default=[]),
+ KwargInfo('dependencies', ContainerTypeInfo(list, (Dependency, ExternalLibrary)), listify=True, default=[]),
+ )
+ def preprocess(self, state: 'ModuleState', args: T.List[T.Union[str, File]], kwargs: 'PreprocessKwArgs') -> ModuleReturnValue:
+ _sources = args[1:]
+ if _sources:
+ FeatureDeprecated.single_use('qt.preprocess positional sources', '0.59', state.subproject, location=state.current_node)
+ # List is invariant, os we have to cast...
+ sources = T.cast('T.List[T.Union[str, File, build.GeneratedList, build.CustomTarget]]',
+ _sources + kwargs['sources'])
+ for s in sources:
+ if not isinstance(s, (str, File)):
+ raise build.InvalidArguments('Variadic arguments to qt.preprocess must be Strings or Files')
+ method = kwargs['method']
+
+ if kwargs['qresources']:
+ # custom output name set? -> one output file, multiple otherwise
+ rcc_kwargs: 'ResourceCompilerKwArgs' = {'name': '', 'sources': kwargs['qresources'], 'extra_args': kwargs['rcc_extra_arguments'], 'method': method}
+ if args:
+ name = args[0]
+ if not isinstance(name, str):
+ raise build.InvalidArguments('First argument to qt.preprocess must be a string')
+ rcc_kwargs['name'] = name
+ sources.extend(self._compile_resources_impl(state, rcc_kwargs))
+
+ if kwargs['ui_files']:
+ ui_kwargs: 'UICompilerKwArgs' = {'sources': kwargs['ui_files'], 'extra_args': kwargs['uic_extra_arguments'], 'method': method}
+ sources.append(self._compile_ui_impl(state, ui_kwargs))
+
+ if kwargs['moc_headers'] or kwargs['moc_sources']:
+ moc_kwargs: 'MocCompilerKwArgs' = {
+ 'extra_args': kwargs['moc_extra_arguments'],
+ 'sources': kwargs['moc_sources'],
+ 'headers': kwargs['moc_headers'],
+ 'include_directories': kwargs['include_directories'],
+ 'dependencies': kwargs['dependencies'],
+ 'method': method,
+ }
+ sources.extend(self._compile_moc_impl(state, moc_kwargs))
+
+ return ModuleReturnValue(sources, [sources])
+
+ @FeatureNew('qt.compile_translations', '0.44.0')
+ @noPosargs
+ @typed_kwargs(
+ 'qt.compile_translations',
+ KwargInfo('build_by_default', bool, default=False),
+ INSTALL_KW,
+ INSTALL_DIR_KW,
+ KwargInfo('method', str, default='auto'),
+ KwargInfo('qresource', (str, NoneType), since='0.56.0'),
+ KwargInfo('rcc_extra_arguments', ContainerTypeInfo(list, str), listify=True, default=[], since='0.56.0'),
+ KwargInfo('ts_files', ContainerTypeInfo(list, (str, File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)), listify=True, default=[]),
+ )
+ def compile_translations(self, state: 'ModuleState', args: T.Tuple, kwargs: 'CompileTranslationsKwArgs') -> ModuleReturnValue:
+ ts_files = kwargs['ts_files']
+ if any(isinstance(s, (build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)) for s in ts_files):
+ FeatureNew.single_use('qt.compile_translations: custom_target or generator for "ts_files" keyword argument',
+ '0.60.0', state.subproject, location=state.current_node)
+ if kwargs['install'] and not kwargs['install_dir']:
+ raise MesonException('qt.compile_translations: "install_dir" keyword argument must be set when "install" is true.')
+ qresource = kwargs['qresource']
+ if qresource:
+ if ts_files:
+ raise MesonException('qt.compile_translations: Cannot specify both ts_files and qresource')
+ if os.path.dirname(qresource) != '':
+ raise MesonException('qt.compile_translations: qresource file name must not contain a subdirectory.')
+ qresource_file = File.from_built_file(state.subdir, qresource)
+ infile_abs = os.path.join(state.environment.source_dir, qresource_file.relative_name())
+ outfile_abs = os.path.join(state.environment.build_dir, qresource_file.relative_name())
+ os.makedirs(os.path.dirname(outfile_abs), exist_ok=True)
+ shutil.copy2(infile_abs, outfile_abs)
+ self.interpreter.add_build_def_file(infile_abs)
+
+ _, nodes = self._qrc_nodes(state, qresource_file)
+ for c in nodes:
+ if c.endswith('.qm'):
+ ts_files.append(c.rstrip('.qm') + '.ts')
+ else:
+ raise MesonException(f'qt.compile_translations: qresource can only contain qm files, found {c}')
+ results = self.preprocess(state, [], {'qresources': qresource_file, 'rcc_extra_arguments': kwargs['rcc_extra_arguments']})
+ self._detect_tools(state, kwargs['method'])
+ translations: T.List[build.CustomTarget] = []
+ for ts in ts_files:
+ if not self.tools['lrelease'].found():
+ raise MesonException('qt.compile_translations: ' +
+ self.tools['lrelease'].name + ' not found')
+ if qresource:
+ # In this case we know that ts_files is always a List[str], as
+ # it's generated above and no ts_files are passed in. However,
+ # mypy can't figure that out so we use assert to assure it that
+ # what we're doing is safe
+ assert isinstance(ts, str), 'for mypy'
+ outdir = os.path.dirname(os.path.normpath(os.path.join(state.subdir, ts)))
+ ts = os.path.basename(ts)
+ else:
+ outdir = state.subdir
+ cmd: T.List[T.Union[ExternalProgram, str]] = [self.tools['lrelease'], '@INPUT@', '-qm', '@OUTPUT@']
+ lrelease_target = build.CustomTarget(
+ f'qt{self.qt_version}-compile-{ts}',
+ outdir,
+ state.subproject,
+ state.environment,
+ cmd,
+ [ts],
+ ['@BASENAME@.qm'],
+ install=kwargs['install'],
+ install_dir=[kwargs['install_dir']],
+ install_tag=['i18n'],
+ build_by_default=kwargs['build_by_default'],
+ )
+ translations.append(lrelease_target)
+ if qresource:
+ return ModuleReturnValue(results.return_value[0], [results.new_objects, translations])
+ else:
+ return ModuleReturnValue(translations, [translations])
diff --git a/mesonbuild/modules/qt4.py b/mesonbuild/modules/qt4.py
new file mode 100644
index 0000000..b8948f7
--- /dev/null
+++ b/mesonbuild/modules/qt4.py
@@ -0,0 +1,28 @@
+# Copyright 2015 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.
+
+from .qt import QtBaseModule
+from . import ModuleInfo
+
+
+class Qt4Module(QtBaseModule):
+
+ INFO = ModuleInfo('qt4')
+
+ def __init__(self, interpreter):
+ QtBaseModule.__init__(self, interpreter, qt_version=4)
+
+
+def initialize(*args, **kwargs):
+ return Qt4Module(*args, **kwargs)
diff --git a/mesonbuild/modules/qt5.py b/mesonbuild/modules/qt5.py
new file mode 100644
index 0000000..3933ea0
--- /dev/null
+++ b/mesonbuild/modules/qt5.py
@@ -0,0 +1,28 @@
+# Copyright 2015 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.
+
+from .qt import QtBaseModule
+from . import ModuleInfo
+
+
+class Qt5Module(QtBaseModule):
+
+ INFO = ModuleInfo('qt5')
+
+ def __init__(self, interpreter):
+ QtBaseModule.__init__(self, interpreter, qt_version=5)
+
+
+def initialize(*args, **kwargs):
+ return Qt5Module(*args, **kwargs)
diff --git a/mesonbuild/modules/qt6.py b/mesonbuild/modules/qt6.py
new file mode 100644
index 0000000..66fc43f
--- /dev/null
+++ b/mesonbuild/modules/qt6.py
@@ -0,0 +1,28 @@
+# Copyright 2020 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.
+
+from .qt import QtBaseModule
+from . import ModuleInfo
+
+
+class Qt6Module(QtBaseModule):
+
+ INFO = ModuleInfo('qt6', '0.57.0')
+
+ def __init__(self, interpreter):
+ QtBaseModule.__init__(self, interpreter, qt_version=6)
+
+
+def initialize(*args, **kwargs):
+ return Qt6Module(*args, **kwargs)
diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py
new file mode 100644
index 0000000..42401e4
--- /dev/null
+++ b/mesonbuild/modules/rust.py
@@ -0,0 +1,250 @@
+# Copyright © 2020-2022 Intel Corporation
+
+# 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.
+from __future__ import annotations
+
+import os
+import typing as T
+
+from . import ExtensionModule, ModuleReturnValue, ModuleInfo
+from .. import mlog
+from ..build import BothLibraries, BuildTarget, CustomTargetIndex, Executable, ExtractedObjects, GeneratedList, IncludeDirs, CustomTarget, StructuredSources
+from ..dependencies import Dependency, ExternalLibrary
+from ..interpreter.type_checking import DEPENDENCIES_KW, TEST_KWS, OUTPUT_KW, INCLUDE_DIRECTORIES, include_dir_string_new
+from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noPosargs
+from ..mesonlib import File
+
+if T.TYPE_CHECKING:
+ from . import ModuleState
+ from ..interpreter import Interpreter
+ from ..interpreter import kwargs as _kwargs
+ from ..interpreter.interpreter import SourceInputs, SourceOutputs
+ from ..programs import ExternalProgram
+
+ from typing_extensions import TypedDict
+
+ class FuncTest(_kwargs.BaseTest):
+
+ dependencies: T.List[T.Union[Dependency, ExternalLibrary]]
+ is_parallel: bool
+
+ class FuncBindgen(TypedDict):
+
+ args: T.List[str]
+ c_args: T.List[str]
+ include_directories: T.List[IncludeDirs]
+ input: T.List[SourceInputs]
+ output: str
+ dependencies: T.List[T.Union[Dependency, ExternalLibrary]]
+
+
+class RustModule(ExtensionModule):
+
+ """A module that holds helper functions for rust."""
+
+ INFO = ModuleInfo('rust', '0.57.0', stabilized='1.0.0')
+
+ def __init__(self, interpreter: Interpreter) -> None:
+ super().__init__(interpreter)
+ self._bindgen_bin: T.Optional[ExternalProgram] = None
+ self.methods.update({
+ 'test': self.test,
+ 'bindgen': self.bindgen,
+ })
+
+ @typed_pos_args('rust.test', str, BuildTarget)
+ @typed_kwargs(
+ 'rust.test',
+ *TEST_KWS,
+ DEPENDENCIES_KW,
+ KwargInfo('is_parallel', bool, default=False),
+ )
+ def test(self, state: ModuleState, args: T.Tuple[str, BuildTarget], kwargs: FuncTest) -> ModuleReturnValue:
+ """Generate a rust test target from a given rust target.
+
+ Rust puts it's unitests inside it's main source files, unlike most
+ languages that put them in external files. This means that normally
+ you have to define two separate targets with basically the same
+ arguments to get tests:
+
+ ```meson
+ rust_lib_sources = [...]
+ rust_lib = static_library(
+ 'rust_lib',
+ rust_lib_sources,
+ )
+
+ rust_lib_test = executable(
+ 'rust_lib_test',
+ rust_lib_sources,
+ rust_args : ['--test'],
+ )
+
+ test(
+ 'rust_lib_test',
+ rust_lib_test,
+ protocol : 'rust',
+ )
+ ```
+
+ This is all fine, but not very DRY. This method makes it much easier
+ to define rust tests:
+
+ ```meson
+ rust = import('unstable-rust')
+
+ rust_lib = static_library(
+ 'rust_lib',
+ [sources],
+ )
+
+ rust.test('rust_lib_test', rust_lib)
+ ```
+ """
+ name = args[0]
+ base_target: BuildTarget = args[1]
+ if not base_target.uses_rust():
+ raise InterpreterException('Second positional argument to rustmod.test() must be a rust based target')
+ extra_args = kwargs['args']
+
+ # Delete any arguments we don't want passed
+ if '--test' in extra_args:
+ mlog.warning('Do not add --test to rustmod.test arguments')
+ extra_args.remove('--test')
+ if '--format' in extra_args:
+ mlog.warning('Do not add --format to rustmod.test arguments')
+ i = extra_args.index('--format')
+ # Also delete the argument to --format
+ del extra_args[i + 1]
+ del extra_args[i]
+ for i, a in enumerate(extra_args):
+ if isinstance(a, str) and a.startswith('--format='):
+ del extra_args[i]
+ break
+
+ # We need to cast here, as currently these don't have protocol in them, but test itself does.
+ tkwargs = T.cast('_kwargs.FuncTest', kwargs.copy())
+
+ tkwargs['args'] = extra_args + ['--test', '--format', 'pretty']
+ tkwargs['protocol'] = 'rust'
+
+ new_target_kwargs = base_target.kwargs.copy()
+ # Don't mutate the shallow copied list, instead replace it with a new
+ # one
+ new_target_kwargs['rust_args'] = new_target_kwargs.get('rust_args', []) + ['--test']
+ new_target_kwargs['install'] = False
+ new_target_kwargs['dependencies'] = new_target_kwargs.get('dependencies', []) + kwargs['dependencies']
+
+ sources = T.cast('T.List[SourceOutputs]', base_target.sources.copy())
+ sources.extend(base_target.generated)
+
+ new_target = Executable(
+ name, base_target.subdir, state.subproject, base_target.for_machine,
+ sources, base_target.structured_sources,
+ base_target.objects, base_target.environment, base_target.compilers,
+ new_target_kwargs
+ )
+
+ test = self.interpreter.make_test(
+ self.interpreter.current_node, (name, new_target), tkwargs)
+
+ return ModuleReturnValue(None, [new_target, test])
+
+ @noPosargs
+ @typed_kwargs(
+ 'rust.bindgen',
+ KwargInfo('c_args', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('args', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo(
+ 'input',
+ ContainerTypeInfo(list, (File, GeneratedList, BuildTarget, BothLibraries, ExtractedObjects, CustomTargetIndex, CustomTarget, str), allow_empty=False),
+ default=[],
+ listify=True,
+ required=True,
+ ),
+ INCLUDE_DIRECTORIES.evolve(feature_validator=include_dir_string_new),
+ OUTPUT_KW,
+ DEPENDENCIES_KW.evolve(since='1.0.0'),
+ )
+ def bindgen(self, state: ModuleState, args: T.List, kwargs: FuncBindgen) -> ModuleReturnValue:
+ """Wrapper around bindgen to simplify it's use.
+
+ The main thing this simplifies is the use of `include_directory`
+ objects, instead of having to pass a plethora of `-I` arguments.
+ """
+ header, *_deps = self.interpreter.source_strings_to_files(kwargs['input'])
+
+ # Split File and Target dependencies to add pass to CustomTarget
+ depends: T.List[SourceOutputs] = []
+ depend_files: T.List[File] = []
+ for d in _deps:
+ if isinstance(d, File):
+ depend_files.append(d)
+ else:
+ depends.append(d)
+
+ clang_args: T.List[str] = []
+ for i in state.process_include_dirs(kwargs['include_directories']):
+ # bindgen always uses clang, so it's safe to hardcode -I here
+ clang_args.extend([f'-I{x}' for x in i.to_string_list(
+ state.environment.get_source_dir(), state.environment.get_build_dir())])
+
+ for de in kwargs['dependencies']:
+ for i in de.get_include_dirs():
+ clang_args.extend([f'-I{x}' for x in i.to_string_list(
+ state.environment.get_source_dir(), state.environment.get_build_dir())])
+ clang_args.extend(de.get_all_compile_args())
+ for s in de.get_sources():
+ if isinstance(s, File):
+ depend_files.append(s)
+ elif isinstance(s, CustomTarget):
+ depends.append(s)
+
+ if self._bindgen_bin is None:
+ self._bindgen_bin = state.find_program('bindgen')
+
+ name: str
+ if isinstance(header, File):
+ name = header.fname
+ elif isinstance(header, (BuildTarget, BothLibraries, ExtractedObjects, StructuredSources)):
+ raise InterpreterException('bindgen source file must be a C header, not an object or build target')
+ else:
+ name = header.get_outputs()[0]
+
+ cmd = self._bindgen_bin.get_command() + \
+ [
+ '@INPUT@', '--output',
+ os.path.join(state.environment.build_dir, '@OUTPUT@')
+ ] + \
+ kwargs['args'] + ['--'] + kwargs['c_args'] + clang_args + \
+ ['-MD', '-MQ', '@INPUT@', '-MF', '@DEPFILE@']
+
+ target = CustomTarget(
+ f'rustmod-bindgen-{name}'.replace('/', '_'),
+ state.subdir,
+ state.subproject,
+ state.environment,
+ cmd,
+ [header],
+ [kwargs['output']],
+ depfile='@PLAINNAME@.d',
+ extra_depends=depends,
+ depend_files=depend_files,
+ backend=state.backend,
+ )
+
+ return ModuleReturnValue([target], [target])
+
+
+def initialize(interp: Interpreter) -> RustModule:
+ return RustModule(interp)
diff --git a/mesonbuild/modules/simd.py b/mesonbuild/modules/simd.py
new file mode 100644
index 0000000..3ee0858
--- /dev/null
+++ b/mesonbuild/modules/simd.py
@@ -0,0 +1,88 @@
+# Copyright 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.
+from __future__ import annotations
+
+from .. import mesonlib, compilers, mlog
+from .. import build
+
+from . import ExtensionModule, ModuleInfo
+
+class SimdModule(ExtensionModule):
+
+ INFO = ModuleInfo('SIMD', '0.42.0', unstable=True)
+
+ def __init__(self, interpreter):
+ super().__init__(interpreter)
+ # FIXME add Altivec and AVX512.
+ self.isets = ('mmx',
+ 'sse',
+ 'sse2',
+ 'sse3',
+ 'ssse3',
+ 'sse41',
+ 'sse42',
+ 'avx',
+ 'avx2',
+ 'neon',
+ )
+ self.methods.update({
+ 'check': self.check,
+ })
+
+ def check(self, state, args, kwargs):
+ result = []
+ if len(args) != 1:
+ raise mesonlib.MesonException('Check requires one argument, a name prefix for checks.')
+ prefix = args[0]
+ if not isinstance(prefix, str):
+ raise mesonlib.MesonException('Argument must be a string.')
+ if 'compiler' not in kwargs:
+ raise mesonlib.MesonException('Must specify compiler keyword')
+ if 'sources' in kwargs:
+ raise mesonlib.MesonException('SIMD module does not support the "sources" keyword')
+ basic_kwargs = {}
+ for key, value in kwargs.items():
+ if key not in self.isets and key != 'compiler':
+ basic_kwargs[key] = value
+ compiler = kwargs['compiler']
+ if not isinstance(compiler, compilers.compilers.Compiler):
+ raise mesonlib.MesonException('Compiler argument must be a compiler object.')
+ conf = build.ConfigurationData()
+ for iset in self.isets:
+ if iset not in kwargs:
+ continue
+ iset_fname = kwargs[iset] # Might also be an array or Files. static_library will validate.
+ args = compiler.get_instruction_set_args(iset)
+ if args is None:
+ mlog.log('Compiler supports %s:' % iset, mlog.red('NO'))
+ continue
+ if args:
+ if not compiler.has_multi_arguments(args, state.environment)[0]:
+ mlog.log('Compiler supports %s:' % iset, mlog.red('NO'))
+ continue
+ mlog.log('Compiler supports %s:' % iset, mlog.green('YES'))
+ conf.values['HAVE_' + iset.upper()] = ('1', 'Compiler supports %s.' % iset)
+ libname = prefix + '_' + iset
+ lib_kwargs = {'sources': iset_fname,
+ }
+ lib_kwargs.update(basic_kwargs)
+ langarg_key = compiler.get_language() + '_args'
+ old_lang_args = mesonlib.extract_as_list(lib_kwargs, langarg_key)
+ all_lang_args = old_lang_args + args
+ lib_kwargs[langarg_key] = all_lang_args
+ result.append(self.interpreter.func_static_lib(None, [libname], lib_kwargs))
+ return [result, conf]
+
+def initialize(*args, **kwargs):
+ return SimdModule(*args, **kwargs)
diff --git a/mesonbuild/modules/sourceset.py b/mesonbuild/modules/sourceset.py
new file mode 100644
index 0000000..c35416e
--- /dev/null
+++ b/mesonbuild/modules/sourceset.py
@@ -0,0 +1,307 @@
+# Copyright 2019 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.
+
+from __future__ import annotations
+import typing as T
+
+from . import ExtensionModule, ModuleObject, MutableModuleObject, ModuleInfo
+from .. import build
+from .. import dependencies
+from .. import mesonlib
+from ..interpreterbase import (
+ noPosargs, noKwargs,
+ InterpreterException, InvalidArguments, InvalidCode, FeatureNew,
+)
+from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, typed_kwargs, typed_pos_args
+from ..mesonlib import OrderedSet
+
+if T.TYPE_CHECKING:
+ from typing_extensions import TypedDict
+
+ from . import ModuleState
+ from ..interpreter import Interpreter
+ from ..interpreterbase import TYPE_var, TYPE_kwargs
+
+ class AddKwargs(TypedDict):
+
+ when: T.List[T.Union[str, dependencies.Dependency]]
+ if_true: T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes, dependencies.Dependency]]
+ if_false: T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]
+
+ class AddAllKw(TypedDict):
+
+ when: T.List[T.Union[str, dependencies.Dependency]]
+ if_true: T.List[SourceSetImpl]
+
+ class ApplyKw(TypedDict):
+
+ strict: bool
+
+
+_WHEN_KW: KwargInfo[T.List[T.Union[str, dependencies.Dependency]]] = KwargInfo(
+ 'when',
+ ContainerTypeInfo(list, (str, dependencies.Dependency)),
+ listify=True,
+ default=[],
+)
+
+
+class SourceSetRule(T.NamedTuple):
+ keys: T.List[str]
+ """Configuration keys that enable this rule if true"""
+
+ deps: T.List[dependencies.Dependency]
+ """Dependencies that enable this rule if true"""
+
+ sources: T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]
+ """Source files added when this rule's conditions are true"""
+
+ extra_deps: T.List[dependencies.Dependency]
+ """Dependencies added when this rule's conditions are true, but
+ that do not make the condition false if they're absent."""
+
+ sourcesets: T.List[SourceSetImpl]
+ """Other sourcesets added when this rule's conditions are true"""
+
+ if_false: T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]
+ """Source files added when this rule's conditions are false"""
+
+
+class SourceFiles(T.NamedTuple):
+ sources: OrderedSet[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]
+ deps: OrderedSet[dependencies.Dependency]
+
+
+class SourceSet:
+ """Base class to avoid circular references.
+
+ Because of error messages, this class is called SourceSet, and the actual
+ implementation is an Impl.
+ """
+
+
+class SourceSetImpl(SourceSet, MutableModuleObject):
+ def __init__(self, interpreter: Interpreter):
+ super().__init__()
+ self.rules: T.List[SourceSetRule] = []
+ self.subproject = interpreter.subproject
+ self.environment = interpreter.environment
+ self.subdir = interpreter.subdir
+ self.frozen = False
+ self.methods.update({
+ 'add': self.add_method,
+ 'add_all': self.add_all_method,
+ 'all_sources': self.all_sources_method,
+ 'all_dependencies': self.all_dependencies_method,
+ 'apply': self.apply_method,
+ })
+
+ def check_source_files(self, args: T.Sequence[T.Union[mesonlib.FileOrString, build.GeneratedTypes, dependencies.Dependency]],
+ ) -> T.Tuple[T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]], T.List[dependencies.Dependency]]:
+ sources: T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]] = []
+ deps: T.List[dependencies.Dependency] = []
+ for x in args:
+ if isinstance(x, dependencies.Dependency):
+ deps.append(x)
+ else:
+ sources.append(x)
+ to_check: T.List[str] = []
+
+ # Get the actual output names to check
+ for s in sources:
+ if isinstance(s, str):
+ to_check.append(s)
+ elif isinstance(s, mesonlib.File):
+ to_check.append(s.fname)
+ else:
+ to_check.extend(s.get_outputs())
+ mesonlib.check_direntry_issues(to_check)
+ return sources, deps
+
+ def check_conditions(self, args: T.Sequence[T.Union[str, dependencies.Dependency]]
+ ) -> T.Tuple[T.List[str], T.List[dependencies.Dependency]]:
+ keys: T.List[str] = []
+ deps: T.List[dependencies.Dependency] = []
+ for x in args:
+ if isinstance(x, str):
+ keys.append(x)
+ else:
+ deps.append(x)
+ return keys, deps
+
+ @typed_pos_args('sourceset.add', varargs=(str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex, dependencies.Dependency))
+ @typed_kwargs(
+ 'sourceset.add',
+ _WHEN_KW,
+ KwargInfo(
+ 'if_true',
+ ContainerTypeInfo(list, (str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex, dependencies.Dependency)),
+ listify=True,
+ default=[],
+ ),
+ KwargInfo(
+ 'if_false',
+ ContainerTypeInfo(list, (str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex)),
+ listify=True,
+ default=[],
+ ),
+ )
+ def add_method(self, state: ModuleState,
+ args: T.Tuple[T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes, dependencies.Dependency]]],
+ kwargs: AddKwargs) -> None:
+ if self.frozen:
+ raise InvalidCode('Tried to use \'add\' after querying the source set')
+ when = kwargs['when']
+ if_true = kwargs['if_true']
+ if_false = kwargs['if_false']
+ if not any([when, if_true, if_false]):
+ if_true = args[0]
+ elif args[0]:
+ raise InterpreterException('add called with both positional and keyword arguments')
+ keys, dependencies = self.check_conditions(when)
+ sources, extra_deps = self.check_source_files(if_true)
+ if_false, _ = self.check_source_files(if_false)
+ self.rules.append(SourceSetRule(keys, dependencies, sources, extra_deps, [], if_false))
+
+ @typed_pos_args('sourceset.add_all', varargs=SourceSet)
+ @typed_kwargs(
+ 'sourceset.add_all',
+ _WHEN_KW,
+ KwargInfo(
+ 'if_true',
+ ContainerTypeInfo(list, SourceSet),
+ listify=True,
+ default=[],
+ )
+ )
+ def add_all_method(self, state: ModuleState, args: T.Tuple[T.List[SourceSetImpl]],
+ kwargs: AddAllKw) -> None:
+ if self.frozen:
+ raise InvalidCode('Tried to use \'add_all\' after querying the source set')
+ when = kwargs['when']
+ if_true = kwargs['if_true']
+ if not when and not if_true:
+ if_true = args[0]
+ elif args[0]:
+ raise InterpreterException('add_all called with both positional and keyword arguments')
+ keys, dependencies = self.check_conditions(when)
+ for s in if_true:
+ if not isinstance(s, SourceSetImpl):
+ raise InvalidCode('Arguments to \'add_all\' after the first must be source sets')
+ s.frozen = True
+ self.rules.append(SourceSetRule(keys, dependencies, [], [], if_true, []))
+
+ def collect(self, enabled_fn: T.Callable[[str], bool],
+ all_sources: bool,
+ into: T.Optional['SourceFiles'] = None) -> SourceFiles:
+ if not into:
+ into = SourceFiles(OrderedSet(), OrderedSet())
+ for entry in self.rules:
+ if all(x.found() for x in entry.deps) and \
+ all(enabled_fn(key) for key in entry.keys):
+ into.sources.update(entry.sources)
+ into.deps.update(entry.deps)
+ into.deps.update(entry.extra_deps)
+ for ss in entry.sourcesets:
+ ss.collect(enabled_fn, all_sources, into)
+ if not all_sources:
+ continue
+ into.sources.update(entry.if_false)
+ return into
+
+ @noKwargs
+ @noPosargs
+ def all_sources_method(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs
+ ) -> T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]:
+ self.frozen = True
+ files = self.collect(lambda x: True, True)
+ return list(files.sources)
+
+ @noKwargs
+ @noPosargs
+ @FeatureNew('source_set.all_dependencies() method', '0.52.0')
+ def all_dependencies_method(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs
+ ) -> T.List[dependencies.Dependency]:
+ self.frozen = True
+ files = self.collect(lambda x: True, True)
+ return list(files.deps)
+
+ @typed_pos_args('sourceset.apply', (build.ConfigurationData, dict))
+ @typed_kwargs('sourceset.apply', KwargInfo('strict', bool, default=True))
+ def apply_method(self, state: ModuleState, args: T.Tuple[T.Union[build.ConfigurationData, T.Dict[str, TYPE_var]]], kwargs: ApplyKw) -> SourceFilesObject:
+ config_data = args[0]
+ self.frozen = True
+ strict = kwargs['strict']
+ if isinstance(config_data, dict):
+ def _get_from_config_data(key: str) -> bool:
+ assert isinstance(config_data, dict), 'for mypy'
+ if strict and key not in config_data:
+ raise InterpreterException(f'Entry {key} not in configuration dictionary.')
+ return bool(config_data.get(key, False))
+ else:
+ config_cache: T.Dict[str, bool] = {}
+
+ def _get_from_config_data(key: str) -> bool:
+ assert isinstance(config_data, build.ConfigurationData), 'for mypy'
+ if key not in config_cache:
+ if key in config_data:
+ config_cache[key] = bool(config_data.get(key)[0])
+ elif strict:
+ raise InvalidArguments(f'sourceset.apply: key "{key}" not in passed configuration, and strict set.')
+ else:
+ config_cache[key] = False
+ return config_cache[key]
+
+ files = self.collect(_get_from_config_data, False)
+ res = SourceFilesObject(files)
+ return res
+
+class SourceFilesObject(ModuleObject):
+ def __init__(self, files: SourceFiles):
+ super().__init__()
+ self.files = files
+ self.methods.update({
+ 'sources': self.sources_method,
+ 'dependencies': self.dependencies_method,
+ })
+
+ @noPosargs
+ @noKwargs
+ def sources_method(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs
+ ) -> T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]:
+ return list(self.files.sources)
+
+ @noPosargs
+ @noKwargs
+ def dependencies_method(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs
+ ) -> T.List[dependencies.Dependency]:
+ return list(self.files.deps)
+
+class SourceSetModule(ExtensionModule):
+
+ INFO = ModuleInfo('sourceset', '0.51.0')
+
+ def __init__(self, interpreter: Interpreter):
+ super().__init__(interpreter)
+ self.methods.update({
+ 'source_set': self.source_set,
+ })
+
+ @noKwargs
+ @noPosargs
+ def source_set(self, state: ModuleState, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> SourceSetImpl:
+ return SourceSetImpl(self.interpreter)
+
+def initialize(interp: Interpreter) -> SourceSetModule:
+ return SourceSetModule(interp)
diff --git a/mesonbuild/modules/wayland.py b/mesonbuild/modules/wayland.py
new file mode 100644
index 0000000..99f71d0
--- /dev/null
+++ b/mesonbuild/modules/wayland.py
@@ -0,0 +1,160 @@
+# Copyright 2022 Mark Bolhuis <mark@bolhuis.dev>
+
+# 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.
+
+from __future__ import annotations
+import os
+import typing as T
+
+from . import ExtensionModule, ModuleReturnValue, ModuleInfo
+from ..build import CustomTarget
+from ..interpreter.type_checking import NoneType, in_set_validator
+from ..interpreterbase import typed_pos_args, typed_kwargs, KwargInfo
+from ..mesonlib import File, MesonException
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Literal, TypedDict
+
+ from . import ModuleState
+ from ..build import Executable
+ from ..dependencies import Dependency
+ from ..interpreter import Interpreter
+ from ..programs import ExternalProgram
+ from ..mesonlib import FileOrString
+
+ class ScanXML(TypedDict):
+
+ public: bool
+ client: bool
+ server: bool
+ include_core_only: bool
+
+ class FindProtocol(TypedDict):
+
+ state: Literal['stable', 'staging', 'unstable']
+ version: T.Optional[int]
+
+class WaylandModule(ExtensionModule):
+
+ INFO = ModuleInfo('wayland', '0.62.0', unstable=True)
+
+ def __init__(self, interpreter: Interpreter) -> None:
+ super().__init__(interpreter)
+
+ self.protocols_dep: T.Optional[Dependency] = None
+ self.pkgdatadir: T.Optional[str] = None
+ self.scanner_bin: T.Optional[T.Union[ExternalProgram, Executable]] = None
+
+ self.methods.update({
+ 'scan_xml': self.scan_xml,
+ 'find_protocol': self.find_protocol,
+ })
+
+ @typed_pos_args('wayland.scan_xml', varargs=(str, File), min_varargs=1)
+ @typed_kwargs(
+ 'wayland.scan_xml',
+ KwargInfo('public', bool, default=False),
+ KwargInfo('client', bool, default=True),
+ KwargInfo('server', bool, default=False),
+ KwargInfo('include_core_only', bool, default=True, since='0.64.0'),
+ )
+ def scan_xml(self, state: ModuleState, args: T.Tuple[T.List[FileOrString]], kwargs: ScanXML) -> ModuleReturnValue:
+ if self.scanner_bin is None:
+ # wayland-scanner from BUILD machine must have same version as wayland
+ # libraries from HOST machine.
+ dep = state.dependency('wayland-client')
+ self.scanner_bin = state.find_tool('wayland-scanner', 'wayland-scanner', 'wayland_scanner',
+ wanted=dep.version)
+
+ scope = 'public' if kwargs['public'] else 'private'
+ # We have to cast because mypy can't deduce these are literals
+ sides = [i for i in T.cast("T.List[Literal['client', 'server']]", ['client', 'server']) if kwargs[i]]
+ if not sides:
+ raise MesonException('At least one of client or server keyword argument must be set to true.')
+
+ xml_files = self.interpreter.source_strings_to_files(args[0])
+ targets: T.List[CustomTarget] = []
+ for xml_file in xml_files:
+ name = os.path.splitext(os.path.basename(xml_file.fname))[0]
+
+ code = CustomTarget(
+ f'{name}-protocol',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ [self.scanner_bin, f'{scope}-code', '@INPUT@', '@OUTPUT@'],
+ [xml_file],
+ [f'{name}-protocol.c'],
+ backend=state.backend,
+ )
+ targets.append(code)
+
+ for side in sides:
+ command = [self.scanner_bin, f'{side}-header', '@INPUT@', '@OUTPUT@']
+ if kwargs['include_core_only']:
+ command.append('--include-core-only')
+
+ header = CustomTarget(
+ f'{name}-{side}-protocol',
+ state.subdir,
+ state.subproject,
+ state.environment,
+ command,
+ [xml_file],
+ [f'{name}-{side}-protocol.h'],
+ backend=state.backend,
+ )
+ targets.append(header)
+
+ return ModuleReturnValue(targets, targets)
+
+ @typed_pos_args('wayland.find_protocol', str)
+ @typed_kwargs(
+ 'wayland.find_protocol',
+ KwargInfo('state', str, default='stable', validator=in_set_validator({'stable', 'staging', 'unstable'})),
+ KwargInfo('version', (int, NoneType)),
+ )
+ def find_protocol(self, state: ModuleState, args: T.Tuple[str], kwargs: FindProtocol) -> File:
+ base_name = args[0]
+ xml_state = kwargs['state']
+ version = kwargs['version']
+
+ if xml_state != 'stable' and version is None:
+ raise MesonException(f'{xml_state} protocols require a version number.')
+
+ if xml_state == 'stable' and version is not None:
+ raise MesonException('stable protocols do not require a version number.')
+
+ if self.protocols_dep is None:
+ self.protocols_dep = state.dependency('wayland-protocols')
+
+ if self.pkgdatadir is None:
+ self.pkgdatadir = self.protocols_dep.get_variable(pkgconfig='pkgdatadir', internal='pkgdatadir')
+
+ if xml_state == 'stable':
+ xml_name = f'{base_name}.xml'
+ elif xml_state == 'staging':
+ xml_name = f'{base_name}-v{version}.xml'
+ else:
+ xml_name = f'{base_name}-unstable-v{version}.xml'
+
+ path = os.path.join(self.pkgdatadir, xml_state, base_name, xml_name)
+
+ if not os.path.exists(path):
+ raise MesonException(f'The file {path} does not exist.')
+
+ return File.from_absolute_file(path)
+
+
+def initialize(interpreter: Interpreter) -> WaylandModule:
+ return WaylandModule(interpreter)
diff --git a/mesonbuild/modules/windows.py b/mesonbuild/modules/windows.py
new file mode 100644
index 0000000..494cfbf
--- /dev/null
+++ b/mesonbuild/modules/windows.py
@@ -0,0 +1,212 @@
+# Copyright 2015 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.
+from __future__ import annotations
+
+import enum
+import os
+import re
+import typing as T
+
+
+from . import ExtensionModule, ModuleInfo
+from . import ModuleReturnValue
+from .. import mesonlib, build
+from .. import mlog
+from ..interpreter.type_checking import DEPEND_FILES_KW, DEPENDS_KW, INCLUDE_DIRECTORIES
+from ..interpreterbase.decorators import ContainerTypeInfo, FeatureNew, KwargInfo, typed_kwargs, typed_pos_args
+from ..mesonlib import MachineChoice, MesonException
+from ..programs import ExternalProgram
+
+if T.TYPE_CHECKING:
+ from . import ModuleState
+ from ..compilers import Compiler
+ from ..interpreter import Interpreter
+
+ from typing_extensions import TypedDict
+
+ class CompileResources(TypedDict):
+
+ depend_files: T.List[mesonlib.FileOrString]
+ depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]]
+ include_directories: T.List[T.Union[str, build.IncludeDirs]]
+ args: T.List[str]
+
+ class RcKwargs(TypedDict):
+ output: str
+ input: T.List[T.Union[mesonlib.FileOrString, build.CustomTargetIndex]]
+ depfile: T.Optional[str]
+ depend_files: T.List[mesonlib.FileOrString]
+ depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]]
+ command: T.List[T.Union[str, ExternalProgram]]
+
+class ResourceCompilerType(enum.Enum):
+ windres = 1
+ rc = 2
+ wrc = 3
+
+class WindowsModule(ExtensionModule):
+
+ INFO = ModuleInfo('windows')
+
+ def __init__(self, interpreter: 'Interpreter'):
+ super().__init__(interpreter)
+ self._rescomp: T.Optional[T.Tuple[ExternalProgram, ResourceCompilerType]] = None
+ self.methods.update({
+ 'compile_resources': self.compile_resources,
+ })
+
+ def detect_compiler(self, compilers: T.Dict[str, 'Compiler']) -> 'Compiler':
+ for l in ('c', 'cpp'):
+ if l in compilers:
+ return compilers[l]
+ raise MesonException('Resource compilation requires a C or C++ compiler.')
+
+ def _find_resource_compiler(self, state: 'ModuleState') -> T.Tuple[ExternalProgram, ResourceCompilerType]:
+ # FIXME: Does not handle `native: true` executables, see
+ # See https://github.com/mesonbuild/meson/issues/1531
+ # Take a parameter instead of the hardcoded definition below
+ for_machine = MachineChoice.HOST
+
+ if self._rescomp:
+ return self._rescomp
+
+ # Will try cross / native file and then env var
+ rescomp = ExternalProgram.from_bin_list(state.environment, for_machine, 'windres')
+
+ if not rescomp or not rescomp.found():
+ comp = self.detect_compiler(state.environment.coredata.compilers[for_machine])
+ if comp.id in {'msvc', 'clang-cl', 'intel-cl'}:
+ rescomp = ExternalProgram('rc', silent=True)
+ else:
+ rescomp = ExternalProgram('windres', silent=True)
+
+ if not rescomp.found():
+ raise MesonException('Could not find Windows resource compiler')
+
+ for (arg, match, rc_type) in [
+ ('/?', '^.*Microsoft.*Resource Compiler.*$', ResourceCompilerType.rc),
+ ('--version', '^.*GNU windres.*$', ResourceCompilerType.windres),
+ ('--version', '^.*Wine Resource Compiler.*$', ResourceCompilerType.wrc),
+ ]:
+ p, o, e = mesonlib.Popen_safe(rescomp.get_command() + [arg])
+ m = re.search(match, o, re.MULTILINE)
+ if m:
+ mlog.log('Windows resource compiler: %s' % m.group())
+ self._rescomp = (rescomp, rc_type)
+ break
+ else:
+ raise MesonException('Could not determine type of Windows resource compiler')
+
+ return self._rescomp
+
+ @typed_pos_args('windows.compile_resources', varargs=(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex), min_varargs=1)
+ @typed_kwargs(
+ 'windows.compile_resources',
+ DEPEND_FILES_KW.evolve(since='0.47.0'),
+ DEPENDS_KW.evolve(since='0.47.0'),
+ INCLUDE_DIRECTORIES,
+ KwargInfo('args', ContainerTypeInfo(list, str), default=[], listify=True),
+ )
+ def compile_resources(self, state: 'ModuleState',
+ args: T.Tuple[T.List[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex]]],
+ kwargs: 'CompileResources') -> ModuleReturnValue:
+ extra_args = kwargs['args'].copy()
+ wrc_depend_files = kwargs['depend_files']
+ wrc_depends = kwargs['depends']
+ for d in wrc_depends:
+ if isinstance(d, build.CustomTarget):
+ extra_args += state.get_include_args([
+ build.IncludeDirs('', [], False, [os.path.join('@BUILD_ROOT@', self.interpreter.backend.get_target_dir(d))])
+ ])
+ extra_args += state.get_include_args(kwargs['include_directories'])
+
+ rescomp, rescomp_type = self._find_resource_compiler(state)
+ if rescomp_type == ResourceCompilerType.rc:
+ # RC is used to generate .res files, a special binary resource
+ # format, which can be passed directly to LINK (apparently LINK uses
+ # CVTRES internally to convert this to a COFF object)
+ suffix = 'res'
+ res_args = extra_args + ['/nologo', '/fo@OUTPUT@', '@INPUT@']
+ elif rescomp_type == ResourceCompilerType.windres:
+ # ld only supports object files, so windres is used to generate a
+ # COFF object
+ suffix = 'o'
+ res_args = extra_args + ['@INPUT@', '@OUTPUT@']
+
+ m = 'Argument {!r} has a space which may not work with windres due to ' \
+ 'a MinGW bug: https://sourceware.org/bugzilla/show_bug.cgi?id=4933'
+ for arg in extra_args:
+ if ' ' in arg:
+ mlog.warning(m.format(arg), fatal=False)
+ else:
+ suffix = 'o'
+ res_args = extra_args + ['@INPUT@', '-o', '@OUTPUT@']
+
+ res_targets: T.List[build.CustomTarget] = []
+
+ def get_names() -> T.Iterable[T.Tuple[str, str, T.Union[str, mesonlib.File, build.CustomTargetIndex]]]:
+ for src in args[0]:
+ if isinstance(src, str):
+ yield os.path.join(state.subdir, src), src, src
+ elif isinstance(src, mesonlib.File):
+ yield src.relative_name(), src.fname, src
+ elif isinstance(src, build.CustomTargetIndex):
+ FeatureNew.single_use('windows.compile_resource CustomTargetIndex in positional arguments', '0.61.0',
+ state.subproject, location=state.current_node)
+ # This dance avoids a case where two indexs of the same
+ # target are given as separate arguments.
+ yield (f'{src.get_id()}_{src.target.get_outputs().index(src.output)}',
+ f'windows_compile_resources_{src.get_filename()}', src)
+ else:
+ if len(src.get_outputs()) > 1:
+ FeatureNew.single_use('windows.compile_resource CustomTarget with multiple outputs in positional arguments',
+ '0.61.0', state.subproject, location=state.current_node)
+ for i, out in enumerate(src.get_outputs()):
+ # Chances are that src.get_filename() is already the name of that
+ # target, add a prefix to avoid name clash.
+ yield f'{src.get_id()}_{i}', f'windows_compile_resources_{i}_{out}', src[i]
+
+ for name, name_formatted, src in get_names():
+ # Path separators are not allowed in target names
+ name = name.replace('/', '_').replace('\\', '_').replace(':', '_')
+ name_formatted = name_formatted.replace('/', '_').replace('\\', '_').replace(':', '_')
+ output = f'{name}_@BASENAME@.{suffix}'
+ command: T.List[T.Union[str, ExternalProgram]] = []
+ command.append(rescomp)
+ command.extend(res_args)
+ depfile: T.Optional[str] = None
+ # instruct binutils windres to generate a preprocessor depfile
+ if rescomp_type == ResourceCompilerType.windres:
+ depfile = f'{output}.d'
+ command.extend(['--preprocessor-arg=-MD',
+ '--preprocessor-arg=-MQ@OUTPUT@',
+ '--preprocessor-arg=-MF@DEPFILE@'])
+
+ res_targets.append(build.CustomTarget(
+ name_formatted,
+ state.subdir,
+ state.subproject,
+ state.environment,
+ command,
+ [src],
+ [output],
+ depfile=depfile,
+ depend_files=wrc_depend_files,
+ extra_depends=wrc_depends,
+ ))
+
+ return ModuleReturnValue(res_targets, [res_targets])
+
+def initialize(interp: 'Interpreter') -> WindowsModule:
+ return WindowsModule(interp)
diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py
new file mode 100644
index 0000000..65a5bd2
--- /dev/null
+++ b/mesonbuild/mparser.py
@@ -0,0 +1,839 @@
+# Copyright 2014-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.
+from __future__ import annotations
+
+from dataclasses import dataclass
+import re
+import codecs
+import types
+import typing as T
+from .mesonlib import MesonException
+from . import mlog
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from .ast import AstVisitor
+
+# This is the regex for the supported escape sequences of a regular string
+# literal, like 'abc\x00'
+ESCAPE_SEQUENCE_SINGLE_RE = re.compile(r'''
+ ( \\U[A-Fa-f0-9]{8} # 8-digit hex escapes
+ | \\u[A-Fa-f0-9]{4} # 4-digit hex escapes
+ | \\x[A-Fa-f0-9]{2} # 2-digit hex escapes
+ | \\[0-7]{1,3} # Octal escapes
+ | \\N\{[^}]+\} # Unicode characters by name
+ | \\[\\'abfnrtv] # Single-character escapes
+ )''', re.UNICODE | re.VERBOSE)
+
+class MesonUnicodeDecodeError(MesonException):
+ def __init__(self, match: str) -> None:
+ super().__init__(match)
+ self.match = match
+
+def decode_match(match: T.Match[str]) -> str:
+ try:
+ return codecs.decode(match.group(0).encode(), 'unicode_escape')
+ except UnicodeDecodeError:
+ raise MesonUnicodeDecodeError(match.group(0))
+
+class ParseException(MesonException):
+ def __init__(self, text: str, line: str, lineno: int, colno: int) -> None:
+ # Format as error message, followed by the line with the error, followed by a caret to show the error column.
+ super().__init__("{}\n{}\n{}".format(text, line, '{}^'.format(' ' * colno)))
+ self.lineno = lineno
+ self.colno = colno
+
+class BlockParseException(MesonException):
+ def __init__(
+ self,
+ text: str,
+ line: str,
+ lineno: int,
+ colno: int,
+ start_line: str,
+ start_lineno: int,
+ start_colno: int,
+ ) -> None:
+ # This can be formatted in two ways - one if the block start and end are on the same line, and a different way if they are on different lines.
+
+ if lineno == start_lineno:
+ # If block start and end are on the same line, it is formatted as:
+ # Error message
+ # Followed by the line with the error
+ # Followed by a caret to show the block start
+ # Followed by underscores
+ # Followed by a caret to show the block end.
+ super().__init__("{}\n{}\n{}".format(text, line, '{}^{}^'.format(' ' * start_colno, '_' * (colno - start_colno - 1))))
+ else:
+ # If block start and end are on different lines, it is formatted as:
+ # Error message
+ # Followed by the line with the error
+ # Followed by a caret to show the error column.
+ # Followed by a message saying where the block started.
+ # Followed by the line of the block start.
+ # Followed by a caret for the block start.
+ super().__init__("%s\n%s\n%s\nFor a block that started at %d,%d\n%s\n%s" % (text, line, '%s^' % (' ' * colno), start_lineno, start_colno, start_line, "%s^" % (' ' * start_colno)))
+ self.lineno = lineno
+ self.colno = colno
+
+TV_TokenTypes = T.TypeVar('TV_TokenTypes', int, str, bool)
+
+@dataclass(eq=False)
+class Token(T.Generic[TV_TokenTypes]):
+ tid: str
+ filename: str
+ line_start: int
+ lineno: int
+ colno: int
+ bytespan: T.Tuple[int, int]
+ value: TV_TokenTypes
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, str):
+ return self.tid == other
+ elif isinstance(other, Token):
+ return self.tid == other.tid
+ return NotImplemented
+
+class Lexer:
+ def __init__(self, code: str):
+ self.code = code
+ self.keywords = {'true', 'false', 'if', 'else', 'elif',
+ 'endif', 'and', 'or', 'not', 'foreach', 'endforeach',
+ 'in', 'continue', 'break'}
+ self.future_keywords = {'return'}
+ self.token_specification = [
+ # Need to be sorted longest to shortest.
+ ('ignore', re.compile(r'[ \t]')),
+ ('multiline_fstring', re.compile(r"f'''(.|\n)*?'''", re.M)),
+ ('fstring', re.compile(r"f'([^'\\]|(\\.))*'")),
+ ('id', re.compile('[_a-zA-Z][_0-9a-zA-Z]*')),
+ ('number', re.compile(r'0[bB][01]+|0[oO][0-7]+|0[xX][0-9a-fA-F]+|0|[1-9]\d*')),
+ ('eol_cont', re.compile(r'\\\n')),
+ ('eol', re.compile(r'\n')),
+ ('multiline_string', re.compile(r"'''(.|\n)*?'''", re.M)),
+ ('comment', re.compile(r'#.*')),
+ ('lparen', re.compile(r'\(')),
+ ('rparen', re.compile(r'\)')),
+ ('lbracket', re.compile(r'\[')),
+ ('rbracket', re.compile(r'\]')),
+ ('lcurl', re.compile(r'\{')),
+ ('rcurl', re.compile(r'\}')),
+ ('dblquote', re.compile(r'"')),
+ ('string', re.compile(r"'([^'\\]|(\\.))*'")),
+ ('comma', re.compile(r',')),
+ ('plusassign', re.compile(r'\+=')),
+ ('dot', re.compile(r'\.')),
+ ('plus', re.compile(r'\+')),
+ ('dash', re.compile(r'-')),
+ ('star', re.compile(r'\*')),
+ ('percent', re.compile(r'%')),
+ ('fslash', re.compile(r'/')),
+ ('colon', re.compile(r':')),
+ ('equal', re.compile(r'==')),
+ ('nequal', re.compile(r'!=')),
+ ('assign', re.compile(r'=')),
+ ('le', re.compile(r'<=')),
+ ('lt', re.compile(r'<')),
+ ('ge', re.compile(r'>=')),
+ ('gt', re.compile(r'>')),
+ ('questionmark', re.compile(r'\?')),
+ ]
+
+ def getline(self, line_start: int) -> str:
+ return self.code[line_start:self.code.find('\n', line_start)]
+
+ def lex(self, filename: str) -> T.Generator[Token, None, None]:
+ line_start = 0
+ lineno = 1
+ loc = 0
+ par_count = 0
+ bracket_count = 0
+ curl_count = 0
+ col = 0
+ while loc < len(self.code):
+ matched = False
+ value = None # type: T.Union[str, bool, int]
+ for (tid, reg) in self.token_specification:
+ mo = reg.match(self.code, loc)
+ if mo:
+ curline = lineno
+ curline_start = line_start
+ col = mo.start() - line_start
+ matched = True
+ span_start = loc
+ loc = mo.end()
+ span_end = loc
+ bytespan = (span_start, span_end)
+ match_text = mo.group()
+ if tid in {'ignore', 'comment'}:
+ break
+ elif tid == 'lparen':
+ par_count += 1
+ elif tid == 'rparen':
+ par_count -= 1
+ elif tid == 'lbracket':
+ bracket_count += 1
+ elif tid == 'rbracket':
+ bracket_count -= 1
+ elif tid == 'lcurl':
+ curl_count += 1
+ elif tid == 'rcurl':
+ curl_count -= 1
+ elif tid == 'dblquote':
+ raise ParseException('Double quotes are not supported. Use single quotes.', self.getline(line_start), lineno, col)
+ elif tid in {'string', 'fstring'}:
+ # Handle here and not on the regexp to give a better error message.
+ if match_text.find("\n") != -1:
+ msg = ParseException("Newline character in a string detected, use ''' (three single quotes) "
+ "for multiline strings instead.\n"
+ "This will become a hard error in a future Meson release.",
+ self.getline(line_start), lineno, col)
+ mlog.warning(msg, location=BaseNode(lineno, col, filename))
+ value = match_text[2 if tid == 'fstring' else 1:-1]
+ try:
+ value = ESCAPE_SEQUENCE_SINGLE_RE.sub(decode_match, value)
+ except MesonUnicodeDecodeError as err:
+ raise MesonException(f"Failed to parse escape sequence: '{err.match}' in string:\n {match_text}")
+ elif tid in {'multiline_string', 'multiline_fstring'}:
+ # For multiline strings, parse out the value and pass
+ # through the normal string logic.
+ # For multiline format strings, we have to emit a
+ # different AST node so we can add a feature check,
+ # but otherwise, it follows the normal fstring logic.
+ if tid == 'multiline_string':
+ value = match_text[3:-3]
+ tid = 'string'
+ else:
+ value = match_text[4:-3]
+ lines = match_text.split('\n')
+ if len(lines) > 1:
+ lineno += len(lines) - 1
+ line_start = mo.end() - len(lines[-1])
+ elif tid == 'number':
+ value = int(match_text, base=0)
+ elif tid == 'eol_cont':
+ lineno += 1
+ line_start = loc
+ break
+ elif tid == 'eol':
+ lineno += 1
+ line_start = loc
+ if par_count > 0 or bracket_count > 0 or curl_count > 0:
+ break
+ elif tid == 'id':
+ if match_text in self.keywords:
+ tid = match_text
+ else:
+ if match_text in self.future_keywords:
+ mlog.warning(f"Identifier '{match_text}' will become a reserved keyword in a future release. Please rename it.",
+ location=types.SimpleNamespace(filename=filename, lineno=lineno))
+ value = match_text
+ yield Token(tid, filename, curline_start, curline, col, bytespan, value)
+ break
+ if not matched:
+ raise ParseException('lexer', self.getline(line_start), lineno, col)
+
+@dataclass(eq=False)
+class BaseNode:
+ lineno: int
+ colno: int
+ filename: str
+ end_lineno: T.Optional[int] = None
+ end_colno: T.Optional[int] = None
+
+ def __post_init__(self) -> None:
+ if self.end_lineno is None:
+ self.end_lineno = self.lineno
+ if self.end_colno is None:
+ self.end_colno = self.colno
+
+ # Attributes for the visitors
+ self.level = 0 # type: int
+ self.ast_id = '' # type: str
+ self.condition_level = 0 # type: int
+
+ def accept(self, visitor: 'AstVisitor') -> None:
+ fname = 'visit_{}'.format(type(self).__name__)
+ if hasattr(visitor, fname):
+ func = getattr(visitor, fname)
+ if callable(func):
+ func(self)
+
+class ElementaryNode(T.Generic[TV_TokenTypes], BaseNode):
+ def __init__(self, token: Token[TV_TokenTypes]):
+ super().__init__(token.lineno, token.colno, token.filename)
+ self.value = token.value # type: TV_TokenTypes
+ self.bytespan = token.bytespan # type: T.Tuple[int, int]
+
+class BooleanNode(ElementaryNode[bool]):
+ def __init__(self, token: Token[bool]):
+ super().__init__(token)
+ assert isinstance(self.value, bool)
+
+class IdNode(ElementaryNode[str]):
+ def __init__(self, token: Token[str]):
+ super().__init__(token)
+ assert isinstance(self.value, str)
+
+ def __str__(self) -> str:
+ return "Id node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno)
+
+class NumberNode(ElementaryNode[int]):
+ def __init__(self, token: Token[int]):
+ super().__init__(token)
+ assert isinstance(self.value, int)
+
+class StringNode(ElementaryNode[str]):
+ def __init__(self, token: Token[str]):
+ super().__init__(token)
+ assert isinstance(self.value, str)
+
+ def __str__(self) -> str:
+ return "String node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno)
+
+class FormatStringNode(ElementaryNode[str]):
+ def __init__(self, token: Token[str]):
+ super().__init__(token)
+ assert isinstance(self.value, str)
+
+ def __str__(self) -> str:
+ return f"Format string node: '{self.value}' ({self.lineno}, {self.colno})."
+
+class MultilineFormatStringNode(FormatStringNode):
+ def __str__(self) -> str:
+ return f"Multiline Format string node: '{self.value}' ({self.lineno}, {self.colno})."
+
+class ContinueNode(ElementaryNode):
+ pass
+
+class BreakNode(ElementaryNode):
+ pass
+
+class ArgumentNode(BaseNode):
+ def __init__(self, token: Token[TV_TokenTypes]):
+ super().__init__(token.lineno, token.colno, token.filename)
+ self.arguments = [] # type: T.List[BaseNode]
+ self.commas = [] # type: T.List[Token[TV_TokenTypes]]
+ self.kwargs = {} # type: T.Dict[BaseNode, BaseNode]
+ self.order_error = False
+
+ def prepend(self, statement: BaseNode) -> None:
+ if self.num_kwargs() > 0:
+ self.order_error = True
+ if not isinstance(statement, EmptyNode):
+ self.arguments = [statement] + self.arguments
+
+ def append(self, statement: BaseNode) -> None:
+ if self.num_kwargs() > 0:
+ self.order_error = True
+ if not isinstance(statement, EmptyNode):
+ self.arguments += [statement]
+
+ def set_kwarg(self, name: IdNode, value: BaseNode) -> None:
+ if any((isinstance(x, IdNode) and name.value == x.value) for x in self.kwargs):
+ mlog.warning(f'Keyword argument "{name.value}" defined multiple times.', location=self)
+ mlog.warning('This will be an error in future Meson releases.')
+ self.kwargs[name] = value
+
+ def set_kwarg_no_check(self, name: BaseNode, value: BaseNode) -> None:
+ self.kwargs[name] = value
+
+ def num_args(self) -> int:
+ return len(self.arguments)
+
+ def num_kwargs(self) -> int:
+ return len(self.kwargs)
+
+ def incorrect_order(self) -> bool:
+ return self.order_error
+
+ def __len__(self) -> int:
+ return self.num_args() # Fixme
+
+class ArrayNode(BaseNode):
+ def __init__(self, args: ArgumentNode, lineno: int, colno: int, end_lineno: int, end_colno: int):
+ super().__init__(lineno, colno, args.filename, end_lineno=end_lineno, end_colno=end_colno)
+ self.args = args # type: ArgumentNode
+
+class DictNode(BaseNode):
+ def __init__(self, args: ArgumentNode, lineno: int, colno: int, end_lineno: int, end_colno: int):
+ super().__init__(lineno, colno, args.filename, end_lineno=end_lineno, end_colno=end_colno)
+ self.args = args
+
+class EmptyNode(BaseNode):
+ def __init__(self, lineno: int, colno: int, filename: str):
+ super().__init__(lineno, colno, filename)
+ self.value = None
+
+class OrNode(BaseNode):
+ def __init__(self, left: BaseNode, right: BaseNode):
+ super().__init__(left.lineno, left.colno, left.filename)
+ self.left = left # type: BaseNode
+ self.right = right # type: BaseNode
+
+class AndNode(BaseNode):
+ def __init__(self, left: BaseNode, right: BaseNode):
+ super().__init__(left.lineno, left.colno, left.filename)
+ self.left = left # type: BaseNode
+ self.right = right # type: BaseNode
+
+class ComparisonNode(BaseNode):
+ def __init__(self, ctype: COMPARISONS, left: BaseNode, right: BaseNode):
+ super().__init__(left.lineno, left.colno, left.filename)
+ self.left = left # type: BaseNode
+ self.right = right # type: BaseNode
+ self.ctype = ctype
+
+class ArithmeticNode(BaseNode):
+ def __init__(self, operation: str, left: BaseNode, right: BaseNode):
+ super().__init__(left.lineno, left.colno, left.filename)
+ self.left = left # type: BaseNode
+ self.right = right # type: BaseNode
+ self.operation = operation # type: str
+
+class NotNode(BaseNode):
+ def __init__(self, token: Token[TV_TokenTypes], value: BaseNode):
+ super().__init__(token.lineno, token.colno, token.filename)
+ self.value = value # type: BaseNode
+
+class CodeBlockNode(BaseNode):
+ def __init__(self, token: Token[TV_TokenTypes]):
+ super().__init__(token.lineno, token.colno, token.filename)
+ self.lines = [] # type: T.List[BaseNode]
+
+class IndexNode(BaseNode):
+ def __init__(self, iobject: BaseNode, index: BaseNode):
+ super().__init__(iobject.lineno, iobject.colno, iobject.filename)
+ self.iobject = iobject # type: BaseNode
+ self.index = index # type: BaseNode
+
+class MethodNode(BaseNode):
+ def __init__(self, filename: str, lineno: int, colno: int, source_object: BaseNode, name: str, args: ArgumentNode):
+ super().__init__(lineno, colno, filename)
+ self.source_object = source_object # type: BaseNode
+ self.name = name # type: str
+ assert isinstance(self.name, str)
+ self.args = args # type: ArgumentNode
+
+class FunctionNode(BaseNode):
+ def __init__(self, filename: str, lineno: int, colno: int, end_lineno: int, end_colno: int, func_name: str, args: ArgumentNode):
+ super().__init__(lineno, colno, filename, end_lineno=end_lineno, end_colno=end_colno)
+ self.func_name = func_name # type: str
+ assert isinstance(func_name, str)
+ self.args = args # type: ArgumentNode
+
+class AssignmentNode(BaseNode):
+ def __init__(self, filename: str, lineno: int, colno: int, var_name: str, value: BaseNode):
+ super().__init__(lineno, colno, filename)
+ self.var_name = var_name # type: str
+ assert isinstance(var_name, str)
+ self.value = value # type: BaseNode
+
+class PlusAssignmentNode(BaseNode):
+ def __init__(self, filename: str, lineno: int, colno: int, var_name: str, value: BaseNode):
+ super().__init__(lineno, colno, filename)
+ self.var_name = var_name # type: str
+ assert isinstance(var_name, str)
+ self.value = value # type: BaseNode
+
+class ForeachClauseNode(BaseNode):
+ def __init__(self, token: Token, varnames: T.List[str], items: BaseNode, block: CodeBlockNode):
+ super().__init__(token.lineno, token.colno, token.filename)
+ self.varnames = varnames # type: T.List[str]
+ self.items = items # type: BaseNode
+ self.block = block # type: CodeBlockNode
+
+class IfNode(BaseNode):
+ def __init__(self, linenode: BaseNode, condition: BaseNode, block: CodeBlockNode):
+ super().__init__(linenode.lineno, linenode.colno, linenode.filename)
+ self.condition = condition # type: BaseNode
+ self.block = block # type: CodeBlockNode
+
+class IfClauseNode(BaseNode):
+ def __init__(self, linenode: BaseNode):
+ super().__init__(linenode.lineno, linenode.colno, linenode.filename)
+ self.ifs = [] # type: T.List[IfNode]
+ self.elseblock = None # type: T.Union[EmptyNode, CodeBlockNode]
+
+class UMinusNode(BaseNode):
+ def __init__(self, current_location: Token, value: BaseNode):
+ super().__init__(current_location.lineno, current_location.colno, current_location.filename)
+ self.value = value # type: BaseNode
+
+class TernaryNode(BaseNode):
+ def __init__(self, condition: BaseNode, trueblock: BaseNode, falseblock: BaseNode):
+ super().__init__(condition.lineno, condition.colno, condition.filename)
+ self.condition = condition # type: BaseNode
+ self.trueblock = trueblock # type: BaseNode
+ self.falseblock = falseblock # type: BaseNode
+
+if T.TYPE_CHECKING:
+ COMPARISONS = Literal['==', '!=', '<', '<=', '>=', '>', 'in', 'notin']
+
+comparison_map: T.Mapping[str, COMPARISONS] = {
+ 'equal': '==',
+ 'nequal': '!=',
+ 'lt': '<',
+ 'le': '<=',
+ 'gt': '>',
+ 'ge': '>=',
+ 'in': 'in',
+ 'not in': 'notin',
+}
+
+# Recursive descent parser for Meson's definition language.
+# Very basic apart from the fact that we have many precedence
+# levels so there are not enough words to describe them all.
+# Enter numbering:
+#
+# 1 assignment
+# 2 or
+# 3 and
+# 4 comparison
+# 5 arithmetic
+# 6 negation
+# 7 funcall, method call
+# 8 parentheses
+# 9 plain token
+
+class Parser:
+ def __init__(self, code: str, filename: str):
+ self.lexer = Lexer(code)
+ self.stream = self.lexer.lex(filename)
+ self.current = Token('eof', '', 0, 0, 0, (0, 0), None) # type: Token
+ self.getsym()
+ self.in_ternary = False
+
+ def getsym(self) -> None:
+ try:
+ self.current = next(self.stream)
+ except StopIteration:
+ self.current = Token('eof', '', self.current.line_start, self.current.lineno, self.current.colno + self.current.bytespan[1] - self.current.bytespan[0], (0, 0), None)
+
+ def getline(self) -> str:
+ return self.lexer.getline(self.current.line_start)
+
+ def accept(self, s: str) -> bool:
+ if self.current.tid == s:
+ self.getsym()
+ return True
+ return False
+
+ def accept_any(self, tids: T.Tuple[str, ...]) -> str:
+ tid = self.current.tid
+ if tid in tids:
+ self.getsym()
+ return tid
+ return ''
+
+ def expect(self, s: str) -> bool:
+ if self.accept(s):
+ return True
+ raise ParseException(f'Expecting {s} got {self.current.tid}.', self.getline(), self.current.lineno, self.current.colno)
+
+ def block_expect(self, s: str, block_start: Token) -> bool:
+ if self.accept(s):
+ return True
+ raise BlockParseException(f'Expecting {s} got {self.current.tid}.', self.getline(), self.current.lineno, self.current.colno, self.lexer.getline(block_start.line_start), block_start.lineno, block_start.colno)
+
+ def parse(self) -> CodeBlockNode:
+ block = self.codeblock()
+ self.expect('eof')
+ return block
+
+ def statement(self) -> BaseNode:
+ return self.e1()
+
+ def e1(self) -> BaseNode:
+ left = self.e2()
+ if self.accept('plusassign'):
+ value = self.e1()
+ if not isinstance(left, IdNode):
+ raise ParseException('Plusassignment target must be an id.', self.getline(), left.lineno, left.colno)
+ assert isinstance(left.value, str)
+ return PlusAssignmentNode(left.filename, left.lineno, left.colno, left.value, value)
+ elif self.accept('assign'):
+ value = self.e1()
+ if not isinstance(left, IdNode):
+ raise ParseException('Assignment target must be an id.',
+ self.getline(), left.lineno, left.colno)
+ assert isinstance(left.value, str)
+ return AssignmentNode(left.filename, left.lineno, left.colno, left.value, value)
+ elif self.accept('questionmark'):
+ if self.in_ternary:
+ raise ParseException('Nested ternary operators are not allowed.',
+ self.getline(), left.lineno, left.colno)
+ self.in_ternary = True
+ trueblock = self.e1()
+ self.expect('colon')
+ falseblock = self.e1()
+ self.in_ternary = False
+ return TernaryNode(left, trueblock, falseblock)
+ return left
+
+ def e2(self) -> BaseNode:
+ left = self.e3()
+ while self.accept('or'):
+ if isinstance(left, EmptyNode):
+ raise ParseException('Invalid or clause.',
+ self.getline(), left.lineno, left.colno)
+ left = OrNode(left, self.e3())
+ return left
+
+ def e3(self) -> BaseNode:
+ left = self.e4()
+ while self.accept('and'):
+ if isinstance(left, EmptyNode):
+ raise ParseException('Invalid and clause.',
+ self.getline(), left.lineno, left.colno)
+ left = AndNode(left, self.e4())
+ return left
+
+ def e4(self) -> BaseNode:
+ left = self.e5()
+ for nodename, operator_type in comparison_map.items():
+ if self.accept(nodename):
+ return ComparisonNode(operator_type, left, self.e5())
+ if self.accept('not') and self.accept('in'):
+ return ComparisonNode('notin', left, self.e5())
+ return left
+
+ def e5(self) -> BaseNode:
+ return self.e5addsub()
+
+ def e5addsub(self) -> BaseNode:
+ op_map = {
+ 'plus': 'add',
+ 'dash': 'sub',
+ }
+ left = self.e5muldiv()
+ while True:
+ op = self.accept_any(tuple(op_map.keys()))
+ if op:
+ left = ArithmeticNode(op_map[op], left, self.e5muldiv())
+ else:
+ break
+ return left
+
+ def e5muldiv(self) -> BaseNode:
+ op_map = {
+ 'percent': 'mod',
+ 'star': 'mul',
+ 'fslash': 'div',
+ }
+ left = self.e6()
+ while True:
+ op = self.accept_any(tuple(op_map.keys()))
+ if op:
+ left = ArithmeticNode(op_map[op], left, self.e6())
+ else:
+ break
+ return left
+
+ def e6(self) -> BaseNode:
+ if self.accept('not'):
+ return NotNode(self.current, self.e7())
+ if self.accept('dash'):
+ return UMinusNode(self.current, self.e7())
+ return self.e7()
+
+ def e7(self) -> BaseNode:
+ left = self.e8()
+ block_start = self.current
+ if self.accept('lparen'):
+ args = self.args()
+ self.block_expect('rparen', block_start)
+ if not isinstance(left, IdNode):
+ raise ParseException('Function call must be applied to plain id',
+ self.getline(), left.lineno, left.colno)
+ assert isinstance(left.value, str)
+ left = FunctionNode(left.filename, left.lineno, left.colno, self.current.lineno, self.current.colno, left.value, args)
+ go_again = True
+ while go_again:
+ go_again = False
+ if self.accept('dot'):
+ go_again = True
+ left = self.method_call(left)
+ if self.accept('lbracket'):
+ go_again = True
+ left = self.index_call(left)
+ return left
+
+ def e8(self) -> BaseNode:
+ block_start = self.current
+ if self.accept('lparen'):
+ e = self.statement()
+ self.block_expect('rparen', block_start)
+ return e
+ elif self.accept('lbracket'):
+ args = self.args()
+ self.block_expect('rbracket', block_start)
+ return ArrayNode(args, block_start.lineno, block_start.colno, self.current.lineno, self.current.colno)
+ elif self.accept('lcurl'):
+ key_values = self.key_values()
+ self.block_expect('rcurl', block_start)
+ return DictNode(key_values, block_start.lineno, block_start.colno, self.current.lineno, self.current.colno)
+ else:
+ return self.e9()
+
+ def e9(self) -> BaseNode:
+ t = self.current
+ if self.accept('true'):
+ t.value = True
+ return BooleanNode(t)
+ if self.accept('false'):
+ t.value = False
+ return BooleanNode(t)
+ if self.accept('id'):
+ return IdNode(t)
+ if self.accept('number'):
+ return NumberNode(t)
+ if self.accept('string'):
+ return StringNode(t)
+ if self.accept('fstring'):
+ return FormatStringNode(t)
+ if self.accept('multiline_fstring'):
+ return MultilineFormatStringNode(t)
+ return EmptyNode(self.current.lineno, self.current.colno, self.current.filename)
+
+ def key_values(self) -> ArgumentNode:
+ s = self.statement() # type: BaseNode
+ a = ArgumentNode(self.current)
+
+ while not isinstance(s, EmptyNode):
+ if self.accept('colon'):
+ a.set_kwarg_no_check(s, self.statement())
+ potential = self.current
+ if not self.accept('comma'):
+ return a
+ a.commas.append(potential)
+ else:
+ raise ParseException('Only key:value pairs are valid in dict construction.',
+ self.getline(), s.lineno, s.colno)
+ s = self.statement()
+ return a
+
+ def args(self) -> ArgumentNode:
+ s = self.statement() # type: BaseNode
+ a = ArgumentNode(self.current)
+
+ while not isinstance(s, EmptyNode):
+ potential = self.current
+ if self.accept('comma'):
+ a.commas.append(potential)
+ a.append(s)
+ elif self.accept('colon'):
+ if not isinstance(s, IdNode):
+ raise ParseException('Dictionary key must be a plain identifier.',
+ self.getline(), s.lineno, s.colno)
+ a.set_kwarg(s, self.statement())
+ potential = self.current
+ if not self.accept('comma'):
+ return a
+ a.commas.append(potential)
+ else:
+ a.append(s)
+ return a
+ s = self.statement()
+ return a
+
+ def method_call(self, source_object: BaseNode) -> MethodNode:
+ methodname = self.e9()
+ if not isinstance(methodname, IdNode):
+ raise ParseException('Method name must be plain id',
+ self.getline(), self.current.lineno, self.current.colno)
+ assert isinstance(methodname.value, str)
+ self.expect('lparen')
+ args = self.args()
+ self.expect('rparen')
+ method = MethodNode(methodname.filename, methodname.lineno, methodname.colno, source_object, methodname.value, args)
+ if self.accept('dot'):
+ return self.method_call(method)
+ return method
+
+ def index_call(self, source_object: BaseNode) -> IndexNode:
+ index_statement = self.statement()
+ self.expect('rbracket')
+ return IndexNode(source_object, index_statement)
+
+ def foreachblock(self) -> ForeachClauseNode:
+ t = self.current
+ self.expect('id')
+ assert isinstance(t.value, str)
+ varname = t
+ varnames = [t.value] # type: T.List[str]
+
+ if self.accept('comma'):
+ t = self.current
+ self.expect('id')
+ assert isinstance(t.value, str)
+ varnames.append(t.value)
+
+ self.expect('colon')
+ items = self.statement()
+ block = self.codeblock()
+ return ForeachClauseNode(varname, varnames, items, block)
+
+ def ifblock(self) -> IfClauseNode:
+ condition = self.statement()
+ clause = IfClauseNode(condition)
+ self.expect('eol')
+ block = self.codeblock()
+ clause.ifs.append(IfNode(clause, condition, block))
+ self.elseifblock(clause)
+ clause.elseblock = self.elseblock()
+ return clause
+
+ def elseifblock(self, clause: IfClauseNode) -> None:
+ while self.accept('elif'):
+ s = self.statement()
+ self.expect('eol')
+ b = self.codeblock()
+ clause.ifs.append(IfNode(s, s, b))
+
+ def elseblock(self) -> T.Union[CodeBlockNode, EmptyNode]:
+ if self.accept('else'):
+ self.expect('eol')
+ return self.codeblock()
+ return EmptyNode(self.current.lineno, self.current.colno, self.current.filename)
+
+ def line(self) -> BaseNode:
+ block_start = self.current
+ if self.current == 'eol':
+ return EmptyNode(self.current.lineno, self.current.colno, self.current.filename)
+ if self.accept('if'):
+ ifblock = self.ifblock()
+ self.block_expect('endif', block_start)
+ return ifblock
+ if self.accept('foreach'):
+ forblock = self.foreachblock()
+ self.block_expect('endforeach', block_start)
+ return forblock
+ if self.accept('continue'):
+ return ContinueNode(self.current)
+ if self.accept('break'):
+ return BreakNode(self.current)
+ return self.statement()
+
+ def codeblock(self) -> CodeBlockNode:
+ block = CodeBlockNode(self.current)
+ cond = True
+ while cond:
+ curline = self.line()
+ if not isinstance(curline, EmptyNode):
+ block.lines.append(curline)
+ cond = self.accept('eol')
+ return block
diff --git a/mesonbuild/msetup.py b/mesonbuild/msetup.py
new file mode 100644
index 0000000..793c1b7
--- /dev/null
+++ b/mesonbuild/msetup.py
@@ -0,0 +1,311 @@
+# Copyright 2016-2018 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.
+from __future__ import annotations
+
+import typing as T
+import time
+import sys, stat
+import datetime
+import os.path
+import platform
+import cProfile as profile
+import argparse
+import tempfile
+import shutil
+import glob
+
+from . import environment, interpreter, mesonlib
+from . import build
+from . import mlog, coredata
+from . import mintro
+from .mesonlib import MesonException, MachineChoice
+from .dependencies import PkgConfigDependency
+
+git_ignore_file = '''# This file is autogenerated by Meson. If you change or delete it, it won't be recreated.
+*
+'''
+
+hg_ignore_file = '''# This file is autogenerated by Meson. If you change or delete it, it won't be recreated.
+syntax: glob
+**/*
+'''
+
+
+def add_arguments(parser: argparse.ArgumentParser) -> None:
+ coredata.register_builtin_arguments(parser)
+ parser.add_argument('--native-file',
+ default=[],
+ action='append',
+ help='File containing overrides for native compilation environment.')
+ parser.add_argument('--cross-file',
+ default=[],
+ action='append',
+ help='File describing cross compilation environment.')
+ parser.add_argument('--vsenv', action='store_true',
+ help='Setup Visual Studio environment even when other compilers are found, ' +
+ 'abort if Visual Studio is not found. This option has no effect on other ' +
+ 'platforms than Windows. Defaults to True when using "vs" backend.')
+ parser.add_argument('-v', '--version', action='version',
+ version=coredata.version)
+ parser.add_argument('--profile-self', action='store_true', dest='profile',
+ help=argparse.SUPPRESS)
+ parser.add_argument('--fatal-meson-warnings', action='store_true', dest='fatal_warnings',
+ help='Make all Meson warnings fatal')
+ parser.add_argument('--reconfigure', action='store_true',
+ help='Set options and reconfigure the project. Useful when new ' +
+ 'options have been added to the project and the default value ' +
+ 'is not working.')
+ parser.add_argument('--wipe', action='store_true',
+ help='Wipe build directory and reconfigure using previous command line options. ' +
+ 'Useful when build directory got corrupted, or when rebuilding with a ' +
+ 'newer version of meson.')
+ parser.add_argument('builddir', nargs='?', default=None)
+ parser.add_argument('sourcedir', nargs='?', default=None)
+
+class MesonApp:
+ def __init__(self, options: argparse.Namespace) -> None:
+ (self.source_dir, self.build_dir) = self.validate_dirs(options.builddir,
+ options.sourcedir,
+ options.reconfigure,
+ options.wipe)
+ if options.wipe:
+ # Make a copy of the cmd line file to make sure we can always
+ # restore that file if anything bad happens. For example if
+ # configuration fails we need to be able to wipe again.
+ restore = []
+ with tempfile.TemporaryDirectory() as d:
+ for filename in [coredata.get_cmd_line_file(self.build_dir)] + glob.glob(os.path.join(self.build_dir, environment.Environment.private_dir, '*.ini')):
+ try:
+ restore.append((shutil.copy(filename, d), filename))
+ except FileNotFoundError:
+ raise MesonException(
+ 'Cannot find cmd_line.txt. This is probably because this '
+ 'build directory was configured with a meson version < 0.49.0.')
+
+ coredata.read_cmd_line_file(self.build_dir, options)
+
+ try:
+ # Don't delete the whole tree, just all of the files and
+ # folders in the tree. Otherwise calling wipe form the builddir
+ # will cause a crash
+ for l in os.listdir(self.build_dir):
+ l = os.path.join(self.build_dir, l)
+ if os.path.isdir(l) and not os.path.islink(l):
+ mesonlib.windows_proof_rmtree(l)
+ else:
+ mesonlib.windows_proof_rm(l)
+ finally:
+ self.add_vcs_ignore_files(self.build_dir)
+ for b, f in restore:
+ os.makedirs(os.path.dirname(f), exist_ok=True)
+ shutil.move(b, f)
+
+ self.options = options
+
+ def has_build_file(self, dirname: str) -> bool:
+ fname = os.path.join(dirname, environment.build_filename)
+ return os.path.exists(fname)
+
+ def validate_core_dirs(self, dir1: str, dir2: str) -> T.Tuple[str, str]:
+ if dir1 is None:
+ if dir2 is None:
+ if not os.path.exists('meson.build') and os.path.exists('../meson.build'):
+ dir2 = '..'
+ else:
+ raise MesonException('Must specify at least one directory name.')
+ dir1 = os.getcwd()
+ if dir2 is None:
+ dir2 = os.getcwd()
+ ndir1 = os.path.abspath(os.path.realpath(dir1))
+ ndir2 = os.path.abspath(os.path.realpath(dir2))
+ if not os.path.exists(ndir1):
+ os.makedirs(ndir1)
+ if not os.path.exists(ndir2):
+ os.makedirs(ndir2)
+ if not stat.S_ISDIR(os.stat(ndir1).st_mode):
+ raise MesonException(f'{dir1} is not a directory')
+ if not stat.S_ISDIR(os.stat(ndir2).st_mode):
+ raise MesonException(f'{dir2} is not a directory')
+ if os.path.samefile(ndir1, ndir2):
+ # Fallback to textual compare if undefined entries found
+ has_undefined = any((s.st_ino == 0 and s.st_dev == 0) for s in (os.stat(ndir1), os.stat(ndir2)))
+ if not has_undefined or ndir1 == ndir2:
+ raise MesonException('Source and build directories must not be the same. Create a pristine build directory.')
+ if self.has_build_file(ndir1):
+ if self.has_build_file(ndir2):
+ raise MesonException(f'Both directories contain a build file {environment.build_filename}.')
+ return ndir1, ndir2
+ if self.has_build_file(ndir2):
+ return ndir2, ndir1
+ raise MesonException(f'Neither directory contains a build file {environment.build_filename}.')
+
+ def add_vcs_ignore_files(self, build_dir: str) -> None:
+ if os.listdir(build_dir):
+ return
+ with open(os.path.join(build_dir, '.gitignore'), 'w', encoding='utf-8') as ofile:
+ ofile.write(git_ignore_file)
+ with open(os.path.join(build_dir, '.hgignore'), 'w', encoding='utf-8') as ofile:
+ ofile.write(hg_ignore_file)
+
+ def validate_dirs(self, dir1: str, dir2: str, reconfigure: bool, wipe: bool) -> T.Tuple[str, str]:
+ (src_dir, build_dir) = self.validate_core_dirs(dir1, dir2)
+ self.add_vcs_ignore_files(build_dir)
+ priv_dir = os.path.join(build_dir, 'meson-private/coredata.dat')
+ if os.path.exists(priv_dir):
+ if not reconfigure and not wipe:
+ print('Directory already configured.\n'
+ '\nJust run your build command (e.g. ninja) and Meson will regenerate as necessary.\n'
+ 'If ninja fails, run "ninja reconfigure" or "meson setup --reconfigure"\n'
+ 'to force Meson to regenerate.\n'
+ '\nIf build failures persist, run "meson setup --wipe" to rebuild from scratch\n'
+ 'using the same options as passed when configuring the build.'
+ '\nTo change option values, run "meson configure" instead.')
+ raise SystemExit
+ else:
+ has_cmd_line_file = os.path.exists(coredata.get_cmd_line_file(build_dir))
+ if (wipe and not has_cmd_line_file) or (not wipe and reconfigure):
+ raise SystemExit(f'Directory does not contain a valid build tree:\n{build_dir}')
+ return src_dir, build_dir
+
+ def generate(self) -> None:
+ env = environment.Environment(self.source_dir, self.build_dir, self.options)
+ mlog.initialize(env.get_log_dir(), self.options.fatal_warnings)
+ if self.options.profile:
+ mlog.set_timestamp_start(time.monotonic())
+ with mesonlib.BuildDirLock(self.build_dir):
+ self._generate(env)
+
+ def _generate(self, env: environment.Environment) -> None:
+ # Get all user defined options, including options that have been defined
+ # during a previous invocation or using meson configure.
+ user_defined_options = argparse.Namespace(**vars(self.options))
+ coredata.read_cmd_line_file(self.build_dir, user_defined_options)
+
+ mlog.debug('Build started at', datetime.datetime.now().isoformat())
+ mlog.debug('Main binary:', sys.executable)
+ mlog.debug('Build Options:', coredata.format_cmd_line_options(user_defined_options))
+ mlog.debug('Python system:', platform.system())
+ mlog.log(mlog.bold('The Meson build system'))
+ mlog.log('Version:', coredata.version)
+ mlog.log('Source dir:', mlog.bold(self.source_dir))
+ mlog.log('Build dir:', mlog.bold(self.build_dir))
+ if env.is_cross_build():
+ mlog.log('Build type:', mlog.bold('cross build'))
+ else:
+ mlog.log('Build type:', mlog.bold('native build'))
+ b = build.Build(env)
+
+ intr = interpreter.Interpreter(b, user_defined_options=user_defined_options)
+ if env.is_cross_build():
+ logger_fun = mlog.log
+ else:
+ logger_fun = mlog.debug
+ build_machine = intr.builtin['build_machine']
+ host_machine = intr.builtin['host_machine']
+ target_machine = intr.builtin['target_machine']
+ assert isinstance(build_machine, interpreter.MachineHolder)
+ assert isinstance(host_machine, interpreter.MachineHolder)
+ assert isinstance(target_machine, interpreter.MachineHolder)
+ logger_fun('Build machine cpu family:', mlog.bold(build_machine.cpu_family_method([], {})))
+ logger_fun('Build machine cpu:', mlog.bold(build_machine.cpu_method([], {})))
+ mlog.log('Host machine cpu family:', mlog.bold(host_machine.cpu_family_method([], {})))
+ mlog.log('Host machine cpu:', mlog.bold(host_machine.cpu_method([], {})))
+ logger_fun('Target machine cpu family:', mlog.bold(target_machine.cpu_family_method([], {})))
+ logger_fun('Target machine cpu:', mlog.bold(target_machine.cpu_method([], {})))
+ try:
+ if self.options.profile:
+ fname = os.path.join(self.build_dir, 'meson-private', 'profile-interpreter.log')
+ profile.runctx('intr.run()', globals(), locals(), filename=fname)
+ else:
+ intr.run()
+ except Exception as e:
+ mintro.write_meson_info_file(b, [e])
+ raise
+
+ cdf: T.Optional[str] = None
+ try:
+ dumpfile = os.path.join(env.get_scratch_dir(), 'build.dat')
+ # We would like to write coredata as late as possible since we use the existence of
+ # this file to check if we generated the build file successfully. Since coredata
+ # includes settings, the build files must depend on it and appear newer. However, due
+ # to various kernel caches, we cannot guarantee that any time in Python is exactly in
+ # sync with the time that gets applied to any files. Thus, we dump this file as late as
+ # possible, but before build files, and if any error occurs, delete it.
+ cdf = env.dump_coredata()
+ if self.options.profile:
+ fname = f'profile-{intr.backend.name}-backend.log'
+ fname = os.path.join(self.build_dir, 'meson-private', fname)
+ profile.runctx('intr.backend.generate()', globals(), locals(), filename=fname)
+ else:
+ intr.backend.generate()
+ self._finalize_devenv(b, intr)
+ build.save(b, dumpfile)
+ if env.first_invocation:
+ # Use path resolved by coredata because they could have been
+ # read from a pipe and wrote into a private file.
+ self.options.cross_file = env.coredata.cross_files
+ self.options.native_file = env.coredata.config_files
+ coredata.write_cmd_line_file(self.build_dir, self.options)
+ else:
+ coredata.update_cmd_line_file(self.build_dir, self.options)
+
+ # Generate an IDE introspection file with the same syntax as the already existing API
+ if self.options.profile:
+ fname = os.path.join(self.build_dir, 'meson-private', 'profile-introspector.log')
+ profile.runctx('mintro.generate_introspection_file(b, intr.backend)', globals(), locals(), filename=fname)
+ else:
+ mintro.generate_introspection_file(b, intr.backend)
+ mintro.write_meson_info_file(b, [], True)
+
+ # Post-conf scripts must be run after writing coredata or else introspection fails.
+ intr.backend.run_postconf_scripts()
+
+ # collect warnings about unsupported build configurations; must be done after full arg processing
+ # by Interpreter() init, but this is most visible at the end
+ if env.coredata.options[mesonlib.OptionKey('backend')].value == 'xcode':
+ mlog.warning('xcode backend is currently unmaintained, patches welcome')
+ if env.coredata.options[mesonlib.OptionKey('layout')].value == 'flat':
+ mlog.warning('-Dlayout=flat is unsupported and probably broken. It was a failed experiment at '
+ 'making Windows build artifacts runnable while uninstalled, due to PATH considerations, '
+ 'but was untested by CI and anyways breaks reasonable use of conflicting targets in different subdirs. '
+ 'Please consider using `meson devenv` instead. See https://github.com/mesonbuild/meson/pull/9243 '
+ 'for details.')
+
+ except Exception as e:
+ mintro.write_meson_info_file(b, [e])
+ if cdf is not None:
+ old_cdf = cdf + '.prev'
+ if os.path.exists(old_cdf):
+ os.replace(old_cdf, cdf)
+ else:
+ os.unlink(cdf)
+ raise
+
+ def _finalize_devenv(self, b: build.Build, intr: interpreter.Interpreter) -> None:
+ b.devenv.append(intr.backend.get_devenv())
+ b.devenv.append(PkgConfigDependency.get_env(intr.environment, MachineChoice.HOST, uninstalled=True))
+ for mod in intr.modules.values():
+ devenv = mod.get_devenv()
+ if devenv:
+ b.devenv.append(devenv)
+
+def run(options: T.Union[argparse.Namespace, T.List[str]]) -> int:
+ if not isinstance(options, argparse.Namespace):
+ parser = argparse.ArgumentParser()
+ add_arguments(parser)
+ options = parser.parse_args(options)
+ coredata.parse_cmd_line_options(options)
+ app = MesonApp(options)
+ app.generate()
+ return 0
diff --git a/mesonbuild/msubprojects.py b/mesonbuild/msubprojects.py
new file mode 100755
index 0000000..d6c182a
--- /dev/null
+++ b/mesonbuild/msubprojects.py
@@ -0,0 +1,726 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, InitVar
+import os, subprocess
+import argparse
+import asyncio
+import threading
+import copy
+import shutil
+from concurrent.futures.thread import ThreadPoolExecutor
+from pathlib import Path
+import typing as T
+import tarfile
+import zipfile
+
+from . import mlog
+from .mesonlib import quiet_git, GitException, Popen_safe, MesonException, windows_proof_rmtree
+from .wrap.wrap import (Resolver, WrapException, ALL_TYPES, PackageDefinition,
+ parse_patch_url, update_wrap_file, get_releases)
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Protocol
+
+ SubParsers = argparse._SubParsersAction[argparse.ArgumentParser]
+
+ class Arguments(Protocol):
+ sourcedir: str
+ num_processes: int
+ subprojects: T.List[str]
+ types: str
+ subprojects_func: T.Callable[[], bool]
+ allow_insecure: bool
+
+ class UpdateArguments(Arguments):
+ rebase: bool
+ reset: bool
+
+ class UpdateWrapDBArguments(Arguments):
+ force: bool
+ releases: T.Dict[str, T.Any]
+
+ class CheckoutArguments(Arguments):
+ b: bool
+ branch_name: str
+
+ class ForeachArguments(Arguments):
+ command: str
+ args: T.List[str]
+
+ class PurgeArguments(Arguments):
+ confirm: bool
+ include_cache: bool
+
+ class PackagefilesArguments(Arguments):
+ apply: bool
+ save: bool
+
+ALL_TYPES_STRING = ', '.join(ALL_TYPES)
+
+def read_archive_files(path: Path, base_path: Path) -> T.Set[Path]:
+ if path.suffix == '.zip':
+ with zipfile.ZipFile(path, 'r') as zip_archive:
+ archive_files = {base_path / i.filename for i in zip_archive.infolist()}
+ else:
+ with tarfile.open(path) as tar_archive: # [ignore encoding]
+ archive_files = {base_path / i.name for i in tar_archive}
+ return archive_files
+
+class Logger:
+ def __init__(self, total_tasks: int) -> None:
+ self.lock = threading.Lock()
+ self.total_tasks = total_tasks
+ self.completed_tasks = 0
+ self.running_tasks: T.Set[str] = set()
+ self.should_erase_line = ''
+
+ def flush(self) -> None:
+ if self.should_erase_line:
+ print(self.should_erase_line, end='\r')
+ self.should_erase_line = ''
+
+ def print_progress(self) -> None:
+ line = f'Progress: {self.completed_tasks} / {self.total_tasks}'
+ max_len = shutil.get_terminal_size().columns - len(line)
+ running = ', '.join(self.running_tasks)
+ if len(running) + 3 > max_len:
+ running = running[:max_len - 6] + '...'
+ line = line + f' ({running})'
+ print(self.should_erase_line, line, sep='', end='\r')
+ self.should_erase_line = '\x1b[K'
+
+ def start(self, wrap_name: str) -> None:
+ with self.lock:
+ self.running_tasks.add(wrap_name)
+ self.print_progress()
+
+ def done(self, wrap_name: str, log_queue: T.List[T.Tuple[mlog.TV_LoggableList, T.Any]]) -> None:
+ with self.lock:
+ self.flush()
+ for args, kwargs in log_queue:
+ mlog.log(*args, **kwargs)
+ self.running_tasks.remove(wrap_name)
+ self.completed_tasks += 1
+ self.print_progress()
+
+
+@dataclass(eq=False)
+class Runner:
+ logger: Logger
+ r: InitVar[Resolver]
+ wrap: PackageDefinition
+ repo_dir: str
+ options: 'Arguments'
+
+ def __post_init__(self, r: Resolver) -> None:
+ # FIXME: Do a copy because Resolver.resolve() is stateful method that
+ # cannot be called from multiple threads.
+ self.wrap_resolver = copy.copy(r)
+ self.wrap_resolver.dirname = os.path.join(r.subdir_root, self.wrap.directory)
+ self.wrap_resolver.wrap = self.wrap
+ self.run_method: T.Callable[[], bool] = self.options.subprojects_func.__get__(self)
+ self.log_queue: T.List[T.Tuple[mlog.TV_LoggableList, T.Any]] = []
+
+ def log(self, *args: mlog.TV_Loggable, **kwargs: T.Any) -> None:
+ self.log_queue.append((list(args), kwargs))
+
+ def run(self) -> bool:
+ self.logger.start(self.wrap.name)
+ try:
+ result = self.run_method()
+ except MesonException as e:
+ self.log(mlog.red('Error:'), str(e))
+ result = False
+ self.logger.done(self.wrap.name, self.log_queue)
+ return result
+
+ @staticmethod
+ def pre_update_wrapdb(options: 'UpdateWrapDBArguments') -> None:
+ options.releases = get_releases(options.allow_insecure)
+
+ def update_wrapdb(self) -> bool:
+ self.log(f'Checking latest WrapDB version for {self.wrap.name}...')
+ options = T.cast('UpdateWrapDBArguments', self.options)
+
+ # Check if this wrap is in WrapDB
+ info = options.releases.get(self.wrap.name)
+ if not info:
+ self.log(' -> Wrap not found in wrapdb')
+ return True
+
+ # Determine current version
+ try:
+ wrapdb_version = self.wrap.get('wrapdb_version')
+ branch, revision = wrapdb_version.split('-', 1)
+ except WrapException:
+ # Fallback to parsing the patch URL to determine current version.
+ # This won't work for projects that have upstream Meson support.
+ try:
+ patch_url = self.wrap.get('patch_url')
+ branch, revision = parse_patch_url(patch_url)
+ except WrapException:
+ if not options.force:
+ self.log(' ->', mlog.red('Could not determine current version, use --force to update any way'))
+ return False
+ branch = revision = None
+
+ # Download latest wrap if version differs
+ latest_version = info['versions'][0]
+ new_branch, new_revision = latest_version.rsplit('-', 1)
+ if new_branch != branch or new_revision != revision:
+ filename = self.wrap.filename if self.wrap.has_wrap else f'{self.wrap.filename}.wrap'
+ update_wrap_file(filename, self.wrap.name,
+ new_branch, new_revision,
+ options.allow_insecure)
+ self.log(' -> New version downloaded:', mlog.blue(latest_version))
+ else:
+ self.log(' -> Already at latest version:', mlog.blue(latest_version))
+
+ return True
+
+ def update_file(self) -> bool:
+ options = T.cast('UpdateArguments', self.options)
+ if options.reset:
+ # Delete existing directory and redownload. It is possible that nothing
+ # changed but we have no way to know. Hopefully tarballs are still
+ # cached.
+ windows_proof_rmtree(self.repo_dir)
+ try:
+ self.wrap_resolver.resolve(self.wrap.name, 'meson')
+ self.log(' -> New version extracted')
+ return True
+ except WrapException as e:
+ self.log(' ->', mlog.red(str(e)))
+ return False
+ else:
+ # The subproject has not changed, or the new source and/or patch
+ # tarballs should be extracted in the same directory than previous
+ # version.
+ self.log(' -> Subproject has not changed, or the new source/patch needs to be extracted on the same location.')
+ self.log(' Pass --reset option to delete directory and redownload.')
+ return False
+
+ def git_output(self, cmd: T.List[str]) -> str:
+ return quiet_git(cmd, self.repo_dir, check=True)[1]
+
+ def git_verbose(self, cmd: T.List[str]) -> None:
+ self.log(self.git_output(cmd))
+
+ def git_stash(self) -> None:
+ # That git command return 1 (failure) when there is something to stash.
+ # We don't want to stash when there is nothing to stash because that would
+ # print spurious "No local changes to save".
+ if not quiet_git(['diff', '--quiet', 'HEAD'], self.repo_dir)[0]:
+ # Don't pipe stdout here because we want the user to see their changes have
+ # been saved.
+ self.git_verbose(['stash'])
+
+ def git_show(self) -> None:
+ commit_message = self.git_output(['show', '--quiet', '--pretty=format:%h%n%d%n%s%n[%an]'])
+ parts = [s.strip() for s in commit_message.split('\n')]
+ self.log(' ->', mlog.yellow(parts[0]), mlog.red(parts[1]), parts[2], mlog.blue(parts[3]))
+
+ def git_rebase(self, revision: str) -> bool:
+ try:
+ self.git_output(['-c', 'rebase.autoStash=true', 'rebase', 'FETCH_HEAD'])
+ except GitException as e:
+ self.log(' -> Could not rebase', mlog.bold(self.repo_dir), 'onto', mlog.bold(revision))
+ self.log(mlog.red(e.output))
+ self.log(mlog.red(str(e)))
+ return False
+ return True
+
+ def git_reset(self, revision: str) -> bool:
+ try:
+ # Stash local changes, commits can always be found back in reflog, to
+ # avoid any data lost by mistake.
+ self.git_stash()
+ self.git_output(['reset', '--hard', 'FETCH_HEAD'])
+ self.wrap_resolver.apply_patch()
+ self.wrap_resolver.apply_diff_files()
+ except GitException as e:
+ self.log(' -> Could not reset', mlog.bold(self.repo_dir), 'to', mlog.bold(revision))
+ self.log(mlog.red(e.output))
+ self.log(mlog.red(str(e)))
+ return False
+ return True
+
+ def git_checkout(self, revision: str, create: bool = False) -> bool:
+ cmd = ['checkout', '--ignore-other-worktrees', revision, '--']
+ if create:
+ cmd.insert(1, '-b')
+ try:
+ # Stash local changes, commits can always be found back in reflog, to
+ # avoid any data lost by mistake.
+ self.git_stash()
+ self.git_output(cmd)
+ except GitException as e:
+ self.log(' -> Could not checkout', mlog.bold(revision), 'in', mlog.bold(self.repo_dir))
+ self.log(mlog.red(e.output))
+ self.log(mlog.red(str(e)))
+ return False
+ return True
+
+ def git_checkout_and_reset(self, revision: str) -> bool:
+ # revision could be a branch that already exists but is outdated, so we still
+ # have to reset after the checkout.
+ success = self.git_checkout(revision)
+ if success:
+ success = self.git_reset(revision)
+ return success
+
+ def git_checkout_and_rebase(self, revision: str) -> bool:
+ # revision could be a branch that already exists but is outdated, so we still
+ # have to rebase after the checkout.
+ success = self.git_checkout(revision)
+ if success:
+ success = self.git_rebase(revision)
+ return success
+
+ def update_git(self) -> bool:
+ options = T.cast('UpdateArguments', self.options)
+ if not os.path.exists(os.path.join(self.repo_dir, '.git')):
+ if options.reset:
+ # Delete existing directory and redownload
+ windows_proof_rmtree(self.repo_dir)
+ try:
+ self.wrap_resolver.resolve(self.wrap.name, 'meson')
+ self.update_git_done()
+ return True
+ except WrapException as e:
+ self.log(' ->', mlog.red(str(e)))
+ return False
+ else:
+ self.log(' -> Not a git repository.')
+ self.log('Pass --reset option to delete directory and redownload.')
+ return False
+ revision = self.wrap.values.get('revision')
+ url = self.wrap.values.get('url')
+ push_url = self.wrap.values.get('push-url')
+ if not revision or not url:
+ # It could be a detached git submodule for example.
+ self.log(' -> No revision or URL specified.')
+ return True
+ try:
+ origin_url = self.git_output(['remote', 'get-url', 'origin']).strip()
+ except GitException as e:
+ self.log(' -> Failed to determine current origin URL in', mlog.bold(self.repo_dir))
+ self.log(mlog.red(e.output))
+ self.log(mlog.red(str(e)))
+ return False
+ if options.reset:
+ try:
+ self.git_output(['remote', 'set-url', 'origin', url])
+ if push_url:
+ self.git_output(['remote', 'set-url', '--push', 'origin', push_url])
+ except GitException as e:
+ self.log(' -> Failed to reset origin URL in', mlog.bold(self.repo_dir))
+ self.log(mlog.red(e.output))
+ self.log(mlog.red(str(e)))
+ return False
+ elif url != origin_url:
+ self.log(f' -> URL changed from {origin_url!r} to {url!r}')
+ return False
+ try:
+ # Same as `git branch --show-current` but compatible with older git version
+ branch = self.git_output(['rev-parse', '--abbrev-ref', 'HEAD']).strip()
+ branch = branch if branch != 'HEAD' else ''
+ except GitException as e:
+ self.log(' -> Failed to determine current branch in', mlog.bold(self.repo_dir))
+ self.log(mlog.red(e.output))
+ self.log(mlog.red(str(e)))
+ return False
+ if self.wrap_resolver.is_git_full_commit_id(revision) and \
+ quiet_git(['rev-parse', '--verify', revision + '^{commit}'], self.repo_dir)[0]:
+ # The revision we need is both a commit and available. So we do not
+ # need to fetch it because it cannot be updated. Instead, trick
+ # git into setting FETCH_HEAD just in case, from the local commit.
+ self.git_output(['fetch', '.', revision])
+ else:
+ try:
+ # Fetch only the revision we need, this avoids fetching useless branches.
+ # revision can be either a branch, tag or commit id. In all cases we want
+ # FETCH_HEAD to be set to the desired commit and "git checkout <revision>"
+ # to to either switch to existing/new branch, or detach to tag/commit.
+ # It is more complicated than it first appear, see discussion there:
+ # https://github.com/mesonbuild/meson/pull/7723#discussion_r488816189.
+ heads_refmap = '+refs/heads/*:refs/remotes/origin/*'
+ tags_refmap = '+refs/tags/*:refs/tags/*'
+ self.git_output(['fetch', '--refmap', heads_refmap, '--refmap', tags_refmap, 'origin', revision])
+ except GitException as e:
+ self.log(' -> Could not fetch revision', mlog.bold(revision), 'in', mlog.bold(self.repo_dir))
+ self.log(mlog.red(e.output))
+ self.log(mlog.red(str(e)))
+ return False
+
+ if branch == '':
+ # We are currently in detached mode
+ if options.reset:
+ success = self.git_checkout_and_reset(revision)
+ else:
+ success = self.git_checkout_and_rebase(revision)
+ elif branch == revision:
+ # We are in the same branch. A reset could still be needed in the case
+ # a force push happened on remote repository.
+ if options.reset:
+ success = self.git_reset(revision)
+ else:
+ success = self.git_rebase(revision)
+ else:
+ # We are in another branch, either the user created their own branch and
+ # we should rebase it, or revision changed in the wrap file and we need
+ # to checkout the new branch.
+ if options.reset:
+ success = self.git_checkout_and_reset(revision)
+ else:
+ success = self.git_rebase(revision)
+ if success:
+ self.update_git_done()
+ return success
+
+ def update_git_done(self) -> None:
+ self.git_output(['submodule', 'update', '--checkout', '--recursive'])
+ self.git_show()
+
+ def update_hg(self) -> bool:
+ revno = self.wrap.get('revision')
+ if revno.lower() == 'tip':
+ # Failure to do pull is not a fatal error,
+ # because otherwise you can't develop without
+ # a working net connection.
+ subprocess.call(['hg', 'pull'], cwd=self.repo_dir)
+ else:
+ if subprocess.call(['hg', 'checkout', revno], cwd=self.repo_dir) != 0:
+ subprocess.check_call(['hg', 'pull'], cwd=self.repo_dir)
+ subprocess.check_call(['hg', 'checkout', revno], cwd=self.repo_dir)
+ return True
+
+ def update_svn(self) -> bool:
+ revno = self.wrap.get('revision')
+ _, out, _ = Popen_safe(['svn', 'info', '--show-item', 'revision', self.repo_dir])
+ current_revno = out
+ if current_revno == revno:
+ return True
+ if revno.lower() == 'head':
+ # Failure to do pull is not a fatal error,
+ # because otherwise you can't develop without
+ # a working net connection.
+ subprocess.call(['svn', 'update'], cwd=self.repo_dir)
+ else:
+ subprocess.check_call(['svn', 'update', '-r', revno], cwd=self.repo_dir)
+ return True
+
+ def update(self) -> bool:
+ self.log(f'Updating {self.wrap.name}...')
+ success = False
+ if not os.path.isdir(self.repo_dir):
+ self.log(' -> Not used.')
+ # It is not an error if we are updating all subprojects.
+ success = not self.options.subprojects
+ elif self.wrap.type == 'file':
+ success = self.update_file()
+ elif self.wrap.type == 'git':
+ success = self.update_git()
+ elif self.wrap.type == 'hg':
+ success = self.update_hg()
+ elif self.wrap.type == 'svn':
+ success = self.update_svn()
+ elif self.wrap.type is None:
+ self.log(' -> Cannot update subproject with no wrap file')
+ # It is not an error if we are updating all subprojects.
+ success = not self.options.subprojects
+ else:
+ self.log(' -> Cannot update', self.wrap.type, 'subproject')
+ if success and os.path.isdir(self.repo_dir):
+ self.wrap.update_hash_cache(self.repo_dir)
+ return success
+
+ def checkout(self) -> bool:
+ options = T.cast('CheckoutArguments', self.options)
+
+ if self.wrap.type != 'git' or not os.path.isdir(self.repo_dir):
+ return True
+ branch_name = options.branch_name if options.branch_name else self.wrap.get('revision')
+ if not branch_name:
+ # It could be a detached git submodule for example.
+ return True
+ self.log(f'Checkout {branch_name} in {self.wrap.name}...')
+ if self.git_checkout(branch_name, create=options.b):
+ self.git_show()
+ return True
+ return False
+
+ def download(self) -> bool:
+ self.log(f'Download {self.wrap.name}...')
+ if os.path.isdir(self.repo_dir):
+ self.log(' -> Already downloaded')
+ return True
+ try:
+ self.wrap_resolver.resolve(self.wrap.name, 'meson')
+ self.log(' -> done')
+ except WrapException as e:
+ self.log(' ->', mlog.red(str(e)))
+ return False
+ return True
+
+ def foreach(self) -> bool:
+ options = T.cast('ForeachArguments', self.options)
+
+ self.log(f'Executing command in {self.repo_dir}')
+ if not os.path.isdir(self.repo_dir):
+ self.log(' -> Not downloaded yet')
+ return True
+ cmd = [options.command] + options.args
+ p, out, _ = Popen_safe(cmd, stderr=subprocess.STDOUT, cwd=self.repo_dir)
+ if p.returncode != 0:
+ err_message = "Command '{}' returned non-zero exit status {}.".format(" ".join(cmd), p.returncode)
+ self.log(' -> ', mlog.red(err_message))
+ self.log(out, end='')
+ return False
+
+ self.log(out, end='')
+ return True
+
+ def purge(self) -> bool:
+ options = T.cast('PurgeArguments', self.options)
+
+ # if subproject is not wrap-based, then don't remove it
+ if not self.wrap.type:
+ return True
+
+ if self.wrap.redirected:
+ redirect_file = Path(self.wrap.original_filename).resolve()
+ if options.confirm:
+ redirect_file.unlink()
+ mlog.log(f'Deleting {redirect_file}')
+
+ if self.wrap.type == 'redirect':
+ redirect_file = Path(self.wrap.filename).resolve()
+ if options.confirm:
+ redirect_file.unlink()
+ self.log(f'Deleting {redirect_file}')
+
+ if options.include_cache:
+ packagecache = Path(self.wrap_resolver.cachedir).resolve()
+ try:
+ subproject_cache_file = packagecache / self.wrap.get("source_filename")
+ if subproject_cache_file.is_file():
+ if options.confirm:
+ subproject_cache_file.unlink()
+ self.log(f'Deleting {subproject_cache_file}')
+ except WrapException:
+ pass
+
+ try:
+ subproject_patch_file = packagecache / self.wrap.get("patch_filename")
+ if subproject_patch_file.is_file():
+ if options.confirm:
+ subproject_patch_file.unlink()
+ self.log(f'Deleting {subproject_patch_file}')
+ except WrapException:
+ pass
+
+ # Don't log that we will remove an empty directory. Since purge is
+ # parallelized, another thread could have deleted it already.
+ try:
+ if not any(packagecache.iterdir()):
+ windows_proof_rmtree(str(packagecache))
+ except FileNotFoundError:
+ pass
+
+ # NOTE: Do not use .resolve() here; the subproject directory may be a symlink
+ subproject_source_dir = Path(self.repo_dir)
+ # Resolve just the parent, just to print out the full path
+ subproject_source_dir = subproject_source_dir.parent.resolve() / subproject_source_dir.name
+
+ # Don't follow symlink. This is covered by the next if statement, but why
+ # not be doubly sure.
+ if subproject_source_dir.is_symlink():
+ if options.confirm:
+ subproject_source_dir.unlink()
+ self.log(f'Deleting {subproject_source_dir}')
+ return True
+ if not subproject_source_dir.is_dir():
+ return True
+
+ try:
+ if options.confirm:
+ windows_proof_rmtree(str(subproject_source_dir))
+ self.log(f'Deleting {subproject_source_dir}')
+ except OSError as e:
+ mlog.error(f'Unable to remove: {subproject_source_dir}: {e}')
+ return False
+
+ return True
+
+ @staticmethod
+ def post_purge(options: 'PurgeArguments') -> None:
+ if not options.confirm:
+ mlog.log('')
+ mlog.log('Nothing has been deleted, run again with --confirm to apply.')
+
+ def packagefiles(self) -> bool:
+ options = T.cast('PackagefilesArguments', self.options)
+
+ if options.apply and options.save:
+ # not quite so nice as argparse failure
+ print('error: --apply and --save are mutually exclusive')
+ return False
+ if options.apply:
+ self.log(f'Re-applying patchfiles overlay for {self.wrap.name}...')
+ if not os.path.isdir(self.repo_dir):
+ self.log(' -> Not downloaded yet')
+ return True
+ self.wrap_resolver.apply_patch()
+ return True
+ if options.save:
+ if 'patch_directory' not in self.wrap.values:
+ mlog.error('can only save packagefiles to patch_directory')
+ return False
+ if 'source_filename' not in self.wrap.values:
+ mlog.error('can only save packagefiles from a [wrap-file]')
+ return False
+ archive_path = Path(self.wrap_resolver.cachedir, self.wrap.values['source_filename'])
+ lead_directory_missing = bool(self.wrap.values.get('lead_directory_missing', False))
+ directory = Path(self.repo_dir)
+ packagefiles = Path(self.wrap.filesdir, self.wrap.values['patch_directory'])
+
+ base_path = directory if lead_directory_missing else directory.parent
+ archive_files = read_archive_files(archive_path, base_path)
+ directory_files = set(directory.glob('**/*'))
+
+ self.log(f'Saving {self.wrap.name} to {packagefiles}...')
+ shutil.rmtree(packagefiles)
+ for src_path in directory_files - archive_files:
+ if not src_path.is_file():
+ continue
+ rel_path = src_path.relative_to(directory)
+ dst_path = packagefiles / rel_path
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copyfile(src_path, dst_path)
+ return True
+
+
+def add_common_arguments(p: argparse.ArgumentParser) -> None:
+ p.add_argument('--sourcedir', default='.',
+ help='Path to source directory')
+ p.add_argument('--types', default='',
+ help=f'Comma-separated list of subproject types. Supported types are: {ALL_TYPES_STRING} (default: all)')
+ p.add_argument('--num-processes', default=None, type=int,
+ help='How many parallel processes to use (Since 0.59.0).')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+
+def add_subprojects_argument(p: argparse.ArgumentParser) -> None:
+ p.add_argument('subprojects', nargs='*',
+ help='List of subprojects (default: all)')
+
+def add_wrap_update_parser(subparsers: 'SubParsers') -> argparse.ArgumentParser:
+ p = subparsers.add_parser('update', help='Update wrap files from WrapDB (Since 0.63.0)')
+ p.add_argument('--force', default=False, action='store_true',
+ help='Update wraps that does not seems to come from WrapDB')
+ add_common_arguments(p)
+ add_subprojects_argument(p)
+ p.set_defaults(subprojects_func=Runner.update_wrapdb)
+ p.set_defaults(pre_func=Runner.pre_update_wrapdb)
+ return p
+
+def add_arguments(parser: argparse.ArgumentParser) -> None:
+ subparsers = parser.add_subparsers(title='Commands', dest='command')
+ subparsers.required = True
+
+ p = subparsers.add_parser('update', help='Update all subprojects from wrap files')
+ p.add_argument('--rebase', default=True, action='store_true',
+ help='Rebase your branch on top of wrap\'s revision. ' +
+ 'Deprecated, it is now the default behaviour. (git only)')
+ p.add_argument('--reset', default=False, action='store_true',
+ help='Checkout wrap\'s revision and hard reset to that commit. (git only)')
+ add_common_arguments(p)
+ add_subprojects_argument(p)
+ p.set_defaults(subprojects_func=Runner.update)
+
+ p = subparsers.add_parser('checkout', help='Checkout a branch (git only)')
+ p.add_argument('-b', default=False, action='store_true',
+ help='Create a new branch')
+ p.add_argument('branch_name', nargs='?',
+ help='Name of the branch to checkout or create (default: revision set in wrap file)')
+ add_common_arguments(p)
+ add_subprojects_argument(p)
+ p.set_defaults(subprojects_func=Runner.checkout)
+
+ p = subparsers.add_parser('download', help='Ensure subprojects are fetched, even if not in use. ' +
+ 'Already downloaded subprojects are not modified. ' +
+ 'This can be used to pre-fetch all subprojects and avoid downloads during configure.')
+ add_common_arguments(p)
+ add_subprojects_argument(p)
+ p.set_defaults(subprojects_func=Runner.download)
+
+ p = subparsers.add_parser('foreach', help='Execute a command in each subproject directory.')
+ p.add_argument('command', metavar='command ...',
+ help='Command to execute in each subproject directory')
+ p.add_argument('args', nargs=argparse.REMAINDER,
+ help=argparse.SUPPRESS)
+ add_common_arguments(p)
+ p.set_defaults(subprojects=[])
+ p.set_defaults(subprojects_func=Runner.foreach)
+
+ p = subparsers.add_parser('purge', help='Remove all wrap-based subproject artifacts')
+ add_common_arguments(p)
+ add_subprojects_argument(p)
+ p.add_argument('--include-cache', action='store_true', default=False, help='Remove the package cache as well')
+ p.add_argument('--confirm', action='store_true', default=False, help='Confirm the removal of subproject artifacts')
+ p.set_defaults(subprojects_func=Runner.purge)
+ p.set_defaults(post_func=Runner.post_purge)
+
+ p = subparsers.add_parser('packagefiles', help='Manage the packagefiles overlay')
+ add_common_arguments(p)
+ add_subprojects_argument(p)
+ p.add_argument('--apply', action='store_true', default=False, help='Apply packagefiles to the subproject')
+ p.add_argument('--save', action='store_true', default=False, help='Save packagefiles from the subproject')
+ p.set_defaults(subprojects_func=Runner.packagefiles)
+
+def run(options: 'Arguments') -> int:
+ src_dir = os.path.relpath(os.path.realpath(options.sourcedir))
+ if not os.path.isfile(os.path.join(src_dir, 'meson.build')):
+ mlog.error('Directory', mlog.bold(src_dir), 'does not seem to be a Meson source directory.')
+ return 1
+ subprojects_dir = os.path.join(src_dir, 'subprojects')
+ if not os.path.isdir(subprojects_dir):
+ mlog.log('Directory', mlog.bold(src_dir), 'does not seem to have subprojects.')
+ return 0
+ r = Resolver(src_dir, 'subprojects', wrap_frontend=True, allow_insecure=options.allow_insecure)
+ if options.subprojects:
+ wraps = [wrap for name, wrap in r.wraps.items() if name in options.subprojects]
+ else:
+ wraps = list(r.wraps.values())
+ types = [t.strip() for t in options.types.split(',')] if options.types else []
+ for t in types:
+ if t not in ALL_TYPES:
+ raise MesonException(f'Unknown subproject type {t!r}, supported types are: {ALL_TYPES_STRING}')
+ tasks: T.List[T.Awaitable[bool]] = []
+ task_names: T.List[str] = []
+ loop = asyncio.get_event_loop()
+ executor = ThreadPoolExecutor(options.num_processes)
+ if types:
+ wraps = [wrap for wrap in wraps if wrap.type in types]
+ pre_func = getattr(options, 'pre_func', None)
+ if pre_func:
+ pre_func(options)
+ logger = Logger(len(wraps))
+ for wrap in wraps:
+ dirname = Path(subprojects_dir, wrap.directory).as_posix()
+ runner = Runner(logger, r, wrap, dirname, options)
+ task = loop.run_in_executor(executor, runner.run)
+ tasks.append(task)
+ task_names.append(wrap.name)
+ results = loop.run_until_complete(asyncio.gather(*tasks))
+ logger.flush()
+ post_func = getattr(options, 'post_func', None)
+ if post_func:
+ post_func(options)
+ failures = [name for name, success in zip(task_names, results) if not success]
+ if failures:
+ m = 'Please check logs above as command failed in some subprojects which could have been left in conflict state: '
+ m += ', '.join(failures)
+ mlog.warning(m)
+ return len(failures)
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py
new file mode 100644
index 0000000..50a5f97
--- /dev/null
+++ b/mesonbuild/mtest.py
@@ -0,0 +1,2120 @@
+# Copyright 2016-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.
+
+# A tool to run tests in many different ways.
+from __future__ import annotations
+
+from pathlib import Path
+from collections import deque
+from contextlib import suppress
+from copy import deepcopy
+import argparse
+import asyncio
+import datetime
+import enum
+import json
+import multiprocessing
+import os
+import pickle
+import platform
+import random
+import re
+import signal
+import subprocess
+import shlex
+import sys
+import textwrap
+import time
+import typing as T
+import unicodedata
+import xml.etree.ElementTree as et
+
+from . import build
+from . import environment
+from . import mlog
+from .coredata import major_versions_differ, MesonVersionMismatchException
+from .coredata import version as coredata_version
+from .mesonlib import (MesonException, OrderedSet, RealPathAction,
+ get_wine_shortpath, join_args, split_args, setup_vsenv)
+from .mintro import get_infodir, load_info_file
+from .programs import ExternalProgram
+from .backend.backends import TestProtocol, TestSerialisation
+
+if T.TYPE_CHECKING:
+ TYPE_TAPResult = T.Union['TAPParser.Test',
+ 'TAPParser.Error',
+ 'TAPParser.Version',
+ 'TAPParser.Plan',
+ 'TAPParser.UnknownLine',
+ 'TAPParser.Bailout']
+
+
+# GNU autotools interprets a return code of 77 from tests it executes to
+# mean that the test should be skipped.
+GNU_SKIP_RETURNCODE = 77
+
+# GNU autotools interprets a return code of 99 from tests it executes to
+# mean that the test failed even before testing what it is supposed to test.
+GNU_ERROR_RETURNCODE = 99
+
+# Exit if 3 Ctrl-C's are received within one second
+MAX_CTRLC = 3
+
+def is_windows() -> bool:
+ platname = platform.system().lower()
+ return platname == 'windows'
+
+def is_cygwin() -> bool:
+ return sys.platform == 'cygwin'
+
+UNIWIDTH_MAPPING = {'F': 2, 'H': 1, 'W': 2, 'Na': 1, 'N': 1, 'A': 1}
+def uniwidth(s: str) -> int:
+ result = 0
+ for c in s:
+ w = unicodedata.east_asian_width(c)
+ result += UNIWIDTH_MAPPING[w]
+ return result
+
+def determine_worker_count() -> int:
+ varname = 'MESON_TESTTHREADS'
+ if varname in os.environ:
+ try:
+ num_workers = int(os.environ[varname])
+ except ValueError:
+ print(f'Invalid value in {varname}, using 1 thread.')
+ num_workers = 1
+ else:
+ try:
+ # Fails in some weird environments such as Debian
+ # reproducible build.
+ num_workers = multiprocessing.cpu_count()
+ except Exception:
+ num_workers = 1
+ return num_workers
+
+def add_arguments(parser: argparse.ArgumentParser) -> None:
+ parser.add_argument('--maxfail', default=0, type=int,
+ help='Number of failing tests before aborting the '
+ 'test run. (default: 0, to disable aborting on failure)')
+ parser.add_argument('--repeat', default=1, dest='repeat', type=int,
+ help='Number of times to run the tests.')
+ parser.add_argument('--no-rebuild', default=False, action='store_true',
+ help='Do not rebuild before running tests.')
+ parser.add_argument('--gdb', default=False, dest='gdb', action='store_true',
+ help='Run test under gdb.')
+ parser.add_argument('--gdb-path', default='gdb', dest='gdb_path',
+ help='Path to the gdb binary (default: gdb).')
+ parser.add_argument('--list', default=False, dest='list', action='store_true',
+ help='List available tests.')
+ parser.add_argument('--wrapper', default=None, dest='wrapper', type=split_args,
+ help='wrapper to run tests with (e.g. Valgrind)')
+ parser.add_argument('-C', dest='wd', action=RealPathAction,
+ # https://github.com/python/typeshed/issues/3107
+ # https://github.com/python/mypy/issues/7177
+ type=os.path.abspath, # type: ignore
+ help='directory to cd into before running')
+ parser.add_argument('--suite', default=[], dest='include_suites', action='append', metavar='SUITE',
+ help='Only run tests belonging to the given suite.')
+ parser.add_argument('--no-suite', default=[], dest='exclude_suites', action='append', metavar='SUITE',
+ help='Do not run tests belonging to the given suite.')
+ parser.add_argument('--no-stdsplit', default=True, dest='split', action='store_false',
+ help='Do not split stderr and stdout in test logs.')
+ parser.add_argument('--print-errorlogs', default=False, action='store_true',
+ help="Whether to print failing tests' logs.")
+ parser.add_argument('--benchmark', default=False, action='store_true',
+ help="Run benchmarks instead of tests.")
+ parser.add_argument('--logbase', default='testlog',
+ help="Base name for log file.")
+ parser.add_argument('--num-processes', default=determine_worker_count(), type=int,
+ help='How many parallel processes to use.')
+ parser.add_argument('-v', '--verbose', default=False, action='store_true',
+ help='Do not redirect stdout and stderr')
+ parser.add_argument('-q', '--quiet', default=False, action='store_true',
+ help='Produce less output to the terminal.')
+ parser.add_argument('-t', '--timeout-multiplier', type=float, default=None,
+ help='Define a multiplier for test timeout, for example '
+ ' when running tests in particular conditions they might take'
+ ' more time to execute. (<= 0 to disable timeout)')
+ parser.add_argument('--setup', default=None, dest='setup',
+ help='Which test setup to use.')
+ parser.add_argument('--test-args', default=[], type=split_args,
+ help='Arguments to pass to the specified test(s) or all tests')
+ parser.add_argument('args', nargs='*',
+ help='Optional list of test names to run. "testname" to run all tests with that name, '
+ '"subprojname:testname" to specifically run "testname" from "subprojname", '
+ '"subprojname:" to run all tests defined by "subprojname".')
+
+
+def print_safe(s: str) -> None:
+ end = '' if s[-1] == '\n' else '\n'
+ try:
+ print(s, end=end)
+ except UnicodeEncodeError:
+ s = s.encode('ascii', errors='backslashreplace').decode('ascii')
+ print(s, end=end)
+
+def join_lines(a: str, b: str) -> str:
+ if not a:
+ return b
+ if not b:
+ return a
+ return a + '\n' + b
+
+def dashes(s: str, dash: str, cols: int) -> str:
+ if not s:
+ return dash * cols
+ s = ' ' + s + ' '
+ width = uniwidth(s)
+ first = (cols - width) // 2
+ s = dash * first + s
+ return s + dash * (cols - first - width)
+
+def returncode_to_status(retcode: int) -> str:
+ # Note: We can't use `os.WIFSIGNALED(result.returncode)` and the related
+ # functions here because the status returned by subprocess is munged. It
+ # returns a negative value if the process was killed by a signal rather than
+ # the raw status returned by `wait()`. Also, If a shell sits between Meson
+ # the the actual unit test that shell is likely to convert a termination due
+ # to a signal into an exit status of 128 plus the signal number.
+ if retcode < 0:
+ signum = -retcode
+ try:
+ signame = signal.Signals(signum).name
+ except ValueError:
+ signame = 'SIGinvalid'
+ return f'killed by signal {signum} {signame}'
+
+ if retcode <= 128:
+ return f'exit status {retcode}'
+
+ signum = retcode - 128
+ try:
+ signame = signal.Signals(signum).name
+ except ValueError:
+ signame = 'SIGinvalid'
+ return f'(exit status {retcode} or signal {signum} {signame})'
+
+# TODO for Windows
+sh_quote: T.Callable[[str], str] = lambda x: x
+if not is_windows():
+ sh_quote = shlex.quote
+
+def env_tuple_to_str(env: T.Iterable[T.Tuple[str, str]]) -> str:
+ return ''.join(["{}={} ".format(k, sh_quote(v)) for k, v in env])
+
+
+class TestException(MesonException):
+ pass
+
+
+@enum.unique
+class ConsoleUser(enum.Enum):
+
+ # the logger can use the console
+ LOGGER = 0
+
+ # the console is used by gdb
+ GDB = 1
+
+ # the console is used to write stdout/stderr
+ STDOUT = 2
+
+
+@enum.unique
+class TestResult(enum.Enum):
+
+ PENDING = 'PENDING'
+ RUNNING = 'RUNNING'
+ OK = 'OK'
+ TIMEOUT = 'TIMEOUT'
+ INTERRUPT = 'INTERRUPT'
+ SKIP = 'SKIP'
+ FAIL = 'FAIL'
+ EXPECTEDFAIL = 'EXPECTEDFAIL'
+ UNEXPECTEDPASS = 'UNEXPECTEDPASS'
+ ERROR = 'ERROR'
+
+ @staticmethod
+ def maxlen() -> int:
+ return 14 # len(UNEXPECTEDPASS)
+
+ def is_ok(self) -> bool:
+ return self in {TestResult.OK, TestResult.EXPECTEDFAIL}
+
+ def is_bad(self) -> bool:
+ return self in {TestResult.FAIL, TestResult.TIMEOUT, TestResult.INTERRUPT,
+ TestResult.UNEXPECTEDPASS, TestResult.ERROR}
+
+ def is_finished(self) -> bool:
+ return self not in {TestResult.PENDING, TestResult.RUNNING}
+
+ def was_killed(self) -> bool:
+ return self in (TestResult.TIMEOUT, TestResult.INTERRUPT)
+
+ def colorize(self, s: str) -> mlog.AnsiDecorator:
+ if self.is_bad():
+ decorator = mlog.red
+ elif self in (TestResult.SKIP, TestResult.EXPECTEDFAIL):
+ decorator = mlog.yellow
+ elif self.is_finished():
+ decorator = mlog.green
+ else:
+ decorator = mlog.blue
+ return decorator(s)
+
+ def get_text(self, colorize: bool) -> str:
+ result_str = '{res:{reslen}}'.format(res=self.value, reslen=self.maxlen())
+ return self.colorize(result_str).get_text(colorize)
+
+ def get_command_marker(self) -> str:
+ return str(self.colorize('>>> '))
+
+
+class TAPParser:
+ class Plan(T.NamedTuple):
+ num_tests: int
+ late: bool
+ skipped: bool
+ explanation: T.Optional[str]
+
+ class Bailout(T.NamedTuple):
+ message: str
+
+ class Test(T.NamedTuple):
+ number: int
+ name: str
+ result: TestResult
+ explanation: T.Optional[str]
+
+ def __str__(self) -> str:
+ return f'{self.number} {self.name}'.strip()
+
+ class Error(T.NamedTuple):
+ message: str
+
+ class UnknownLine(T.NamedTuple):
+ message: str
+ lineno: int
+
+ class Version(T.NamedTuple):
+ version: int
+
+ _MAIN = 1
+ _AFTER_TEST = 2
+ _YAML = 3
+
+ _RE_BAILOUT = re.compile(r'Bail out!\s*(.*)')
+ _RE_DIRECTIVE = re.compile(r'(?:\s*\#\s*([Ss][Kk][Ii][Pp]\S*|[Tt][Oo][Dd][Oo])\b\s*(.*))?')
+ _RE_PLAN = re.compile(r'1\.\.([0-9]+)' + _RE_DIRECTIVE.pattern)
+ _RE_TEST = re.compile(r'((?:not )?ok)\s*(?:([0-9]+)\s*)?([^#]*)' + _RE_DIRECTIVE.pattern)
+ _RE_VERSION = re.compile(r'TAP version ([0-9]+)')
+ _RE_YAML_START = re.compile(r'(\s+)---.*')
+ _RE_YAML_END = re.compile(r'\s+\.\.\.\s*')
+
+ found_late_test = False
+ bailed_out = False
+ plan: T.Optional[Plan] = None
+ lineno = 0
+ num_tests = 0
+ yaml_lineno: T.Optional[int] = None
+ yaml_indent = ''
+ state = _MAIN
+ version = 12
+
+ def parse_test(self, ok: bool, num: int, name: str, directive: T.Optional[str], explanation: T.Optional[str]) -> \
+ T.Generator[T.Union['TAPParser.Test', 'TAPParser.Error'], None, None]:
+ name = name.strip()
+ explanation = explanation.strip() if explanation else None
+ if directive is not None:
+ directive = directive.upper()
+ if directive.startswith('SKIP'):
+ if ok:
+ yield self.Test(num, name, TestResult.SKIP, explanation)
+ return
+ elif directive == 'TODO':
+ yield self.Test(num, name, TestResult.UNEXPECTEDPASS if ok else TestResult.EXPECTEDFAIL, explanation)
+ return
+ else:
+ yield self.Error(f'invalid directive "{directive}"')
+
+ yield self.Test(num, name, TestResult.OK if ok else TestResult.FAIL, explanation)
+
+ async def parse_async(self, lines: T.AsyncIterator[str]) -> T.AsyncIterator[TYPE_TAPResult]:
+ async for line in lines:
+ for event in self.parse_line(line):
+ yield event
+ for event in self.parse_line(None):
+ yield event
+
+ def parse(self, io: T.Iterator[str]) -> T.Iterator[TYPE_TAPResult]:
+ for line in io:
+ yield from self.parse_line(line)
+ yield from self.parse_line(None)
+
+ def parse_line(self, line: T.Optional[str]) -> T.Iterator[TYPE_TAPResult]:
+ if line is not None:
+ self.lineno += 1
+ line = line.rstrip()
+
+ # YAML blocks are only accepted after a test
+ if self.state == self._AFTER_TEST:
+ if self.version >= 13:
+ m = self._RE_YAML_START.match(line)
+ if m:
+ self.state = self._YAML
+ self.yaml_lineno = self.lineno
+ self.yaml_indent = m.group(1)
+ return
+ self.state = self._MAIN
+
+ elif self.state == self._YAML:
+ if self._RE_YAML_END.match(line):
+ self.state = self._MAIN
+ return
+ if line.startswith(self.yaml_indent):
+ return
+ yield self.Error(f'YAML block not terminated (started on line {self.yaml_lineno})')
+ self.state = self._MAIN
+
+ assert self.state == self._MAIN
+ if not line or line.startswith('#'):
+ return
+
+ m = self._RE_TEST.match(line)
+ if m:
+ if self.plan and self.plan.late and not self.found_late_test:
+ yield self.Error('unexpected test after late plan')
+ self.found_late_test = True
+ self.num_tests += 1
+ num = self.num_tests if m.group(2) is None else int(m.group(2))
+ if num != self.num_tests:
+ yield self.Error('out of order test numbers')
+ yield from self.parse_test(m.group(1) == 'ok', num,
+ m.group(3), m.group(4), m.group(5))
+ self.state = self._AFTER_TEST
+ return
+
+ m = self._RE_PLAN.match(line)
+ if m:
+ if self.plan:
+ yield self.Error('more than one plan found')
+ else:
+ num_tests = int(m.group(1))
+ skipped = num_tests == 0
+ if m.group(2):
+ if m.group(2).upper().startswith('SKIP'):
+ if num_tests > 0:
+ yield self.Error('invalid SKIP directive for plan')
+ skipped = True
+ else:
+ yield self.Error('invalid directive for plan')
+ self.plan = self.Plan(num_tests=num_tests, late=(self.num_tests > 0),
+ skipped=skipped, explanation=m.group(3))
+ yield self.plan
+ return
+
+ m = self._RE_BAILOUT.match(line)
+ if m:
+ yield self.Bailout(m.group(1))
+ self.bailed_out = True
+ return
+
+ m = self._RE_VERSION.match(line)
+ if m:
+ # The TAP version is only accepted as the first line
+ if self.lineno != 1:
+ yield self.Error('version number must be on the first line')
+ return
+ self.version = int(m.group(1))
+ if self.version < 13:
+ yield self.Error('version number should be at least 13')
+ else:
+ yield self.Version(version=self.version)
+ return
+
+ # unknown syntax
+ yield self.UnknownLine(line, self.lineno)
+ else:
+ # end of file
+ if self.state == self._YAML:
+ yield self.Error(f'YAML block not terminated (started on line {self.yaml_lineno})')
+
+ if not self.bailed_out and self.plan and self.num_tests != self.plan.num_tests:
+ if self.num_tests < self.plan.num_tests:
+ yield self.Error(f'Too few tests run (expected {self.plan.num_tests}, got {self.num_tests})')
+ else:
+ yield self.Error(f'Too many tests run (expected {self.plan.num_tests}, got {self.num_tests})')
+
+class TestLogger:
+ def flush(self) -> None:
+ pass
+
+ def start(self, harness: 'TestHarness') -> None:
+ pass
+
+ def start_test(self, harness: 'TestHarness', test: 'TestRun') -> None:
+ pass
+
+ def log_subtest(self, harness: 'TestHarness', test: 'TestRun', s: str, res: TestResult) -> None:
+ pass
+
+ def log(self, harness: 'TestHarness', result: 'TestRun') -> None:
+ pass
+
+ async def finish(self, harness: 'TestHarness') -> None:
+ pass
+
+ def close(self) -> None:
+ pass
+
+
+class TestFileLogger(TestLogger):
+ def __init__(self, filename: str, errors: str = 'replace') -> None:
+ self.filename = filename
+ self.file = open(filename, 'w', encoding='utf-8', errors=errors)
+
+ def close(self) -> None:
+ if self.file:
+ self.file.close()
+ self.file = None
+
+
+class ConsoleLogger(TestLogger):
+ ASCII_SPINNER = ['..', ':.', '.:']
+ SPINNER = ["\U0001f311", "\U0001f312", "\U0001f313", "\U0001f314",
+ "\U0001f315", "\U0001f316", "\U0001f317", "\U0001f318"]
+
+ SCISSORS = "\u2700 "
+ HLINE = "\u2015"
+ RTRI = "\u25B6 "
+
+ def __init__(self) -> None:
+ self.running_tests = OrderedSet() # type: OrderedSet['TestRun']
+ self.progress_test = None # type: T.Optional['TestRun']
+ self.progress_task = None # type: T.Optional[asyncio.Future]
+ self.max_left_width = 0 # type: int
+ self.stop = False
+ self.update = asyncio.Event()
+ self.should_erase_line = ''
+ self.test_count = 0
+ self.started_tests = 0
+ self.spinner_index = 0
+ try:
+ self.cols, _ = os.get_terminal_size(1)
+ self.is_tty = True
+ except OSError:
+ self.cols = 80
+ self.is_tty = False
+
+ self.output_start = dashes(self.SCISSORS, self.HLINE, self.cols - 2)
+ self.output_end = dashes('', self.HLINE, self.cols - 2)
+ self.sub = self.RTRI
+ self.spinner = self.SPINNER
+ try:
+ self.output_start.encode(sys.stdout.encoding or 'ascii')
+ except UnicodeEncodeError:
+ self.output_start = dashes('8<', '-', self.cols - 2)
+ self.output_end = dashes('', '-', self.cols - 2)
+ self.sub = '| '
+ self.spinner = self.ASCII_SPINNER
+
+ def flush(self) -> None:
+ if self.should_erase_line:
+ print(self.should_erase_line, end='')
+ self.should_erase_line = ''
+
+ def print_progress(self, line: str) -> None:
+ print(self.should_erase_line, line, sep='', end='\r')
+ self.should_erase_line = '\x1b[K'
+
+ def request_update(self) -> None:
+ self.update.set()
+
+ def emit_progress(self, harness: 'TestHarness') -> None:
+ if self.progress_test is None:
+ self.flush()
+ return
+
+ if len(self.running_tests) == 1:
+ count = f'{self.started_tests}/{self.test_count}'
+ else:
+ count = '{}-{}/{}'.format(self.started_tests - len(self.running_tests) + 1,
+ self.started_tests, self.test_count)
+
+ left = '[{}] {} '.format(count, self.spinner[self.spinner_index])
+ self.spinner_index = (self.spinner_index + 1) % len(self.spinner)
+
+ right = '{spaces} {dur:{durlen}}'.format(
+ spaces=' ' * TestResult.maxlen(),
+ dur=int(time.time() - self.progress_test.starttime),
+ durlen=harness.duration_max_len)
+ if self.progress_test.timeout:
+ right += '/{timeout:{durlen}}'.format(
+ timeout=self.progress_test.timeout,
+ durlen=harness.duration_max_len)
+ right += 's'
+ details = self.progress_test.get_details()
+ if details:
+ right += ' ' + details
+
+ line = harness.format(self.progress_test, colorize=True,
+ max_left_width=self.max_left_width,
+ left=left, right=right)
+ self.print_progress(line)
+
+ def start(self, harness: 'TestHarness') -> None:
+ async def report_progress() -> None:
+ loop = asyncio.get_event_loop()
+ next_update = 0.0
+ self.request_update()
+ while not self.stop:
+ await self.update.wait()
+ self.update.clear()
+ # We may get here simply because the progress line has been
+ # overwritten, so do not always switch. Only do so every
+ # second, or if the printed test has finished
+ if loop.time() >= next_update:
+ self.progress_test = None
+ next_update = loop.time() + 1
+ loop.call_at(next_update, self.request_update)
+
+ if (self.progress_test and
+ self.progress_test.res is not TestResult.RUNNING):
+ self.progress_test = None
+
+ if not self.progress_test:
+ if not self.running_tests:
+ continue
+ # Pick a test in round robin order
+ self.progress_test = self.running_tests.pop(last=False)
+ self.running_tests.add(self.progress_test)
+
+ self.emit_progress(harness)
+ self.flush()
+
+ self.test_count = harness.test_count
+ self.cols = max(self.cols, harness.max_left_width + 30)
+
+ if self.is_tty and not harness.need_console:
+ # Account for "[aa-bb/cc] OO " in the progress report
+ self.max_left_width = 3 * len(str(self.test_count)) + 8
+ self.progress_task = asyncio.ensure_future(report_progress())
+
+ def start_test(self, harness: 'TestHarness', test: 'TestRun') -> None:
+ if test.verbose and test.cmdline:
+ self.flush()
+ print(harness.format(test, mlog.colorize_console(),
+ max_left_width=self.max_left_width,
+ right=test.res.get_text(mlog.colorize_console())))
+ print(test.res.get_command_marker() + test.cmdline)
+ if test.direct_stdout:
+ print(self.output_start, flush=True)
+ elif not test.needs_parsing:
+ print(flush=True)
+
+ self.started_tests += 1
+ self.running_tests.add(test)
+ self.running_tests.move_to_end(test, last=False)
+ self.request_update()
+
+ def shorten_log(self, harness: 'TestHarness', result: 'TestRun') -> str:
+ if not result.verbose and not harness.options.print_errorlogs:
+ return ''
+
+ log = result.get_log(mlog.colorize_console(),
+ stderr_only=result.needs_parsing)
+ if result.verbose:
+ return log
+
+ lines = log.splitlines()
+ if len(lines) < 100:
+ return log
+ else:
+ return str(mlog.bold('Listing only the last 100 lines from a long log.\n')) + '\n'.join(lines[-100:])
+
+ def print_log(self, harness: 'TestHarness', result: 'TestRun') -> None:
+ if not result.verbose:
+ cmdline = result.cmdline
+ if not cmdline:
+ print(result.res.get_command_marker() + result.stdo)
+ return
+ print(result.res.get_command_marker() + cmdline)
+
+ log = self.shorten_log(harness, result)
+ if log:
+ print(self.output_start)
+ print_safe(log)
+ print(self.output_end)
+
+ def log_subtest(self, harness: 'TestHarness', test: 'TestRun', s: str, result: TestResult) -> None:
+ if test.verbose or (harness.options.print_errorlogs and result.is_bad()):
+ self.flush()
+ print(harness.format(test, mlog.colorize_console(), max_left_width=self.max_left_width,
+ prefix=self.sub,
+ middle=s,
+ right=result.get_text(mlog.colorize_console())), flush=True)
+
+ self.request_update()
+
+ def log(self, harness: 'TestHarness', result: 'TestRun') -> None:
+ self.running_tests.remove(result)
+ if result.res is TestResult.TIMEOUT and (result.verbose or
+ harness.options.print_errorlogs):
+ self.flush()
+ print(f'{result.name} time out (After {result.timeout} seconds)')
+
+ if not harness.options.quiet or not result.res.is_ok():
+ self.flush()
+ if result.cmdline and result.direct_stdout:
+ print(self.output_end)
+ print(harness.format(result, mlog.colorize_console(), max_left_width=self.max_left_width))
+ else:
+ print(harness.format(result, mlog.colorize_console(), max_left_width=self.max_left_width),
+ flush=True)
+ if result.verbose or result.res.is_bad():
+ self.print_log(harness, result)
+ if result.warnings:
+ print(flush=True)
+ for w in result.warnings:
+ print(w, flush=True)
+ print(flush=True)
+ if result.verbose or result.res.is_bad():
+ print(flush=True)
+
+ self.request_update()
+
+ async def finish(self, harness: 'TestHarness') -> None:
+ self.stop = True
+ self.request_update()
+ if self.progress_task:
+ await self.progress_task
+
+ if harness.collected_failures and \
+ (harness.options.print_errorlogs or harness.options.verbose):
+ print("\nSummary of Failures:\n")
+ for i, result in enumerate(harness.collected_failures, 1):
+ print(harness.format(result, mlog.colorize_console()))
+
+ print(harness.summary())
+
+
+class TextLogfileBuilder(TestFileLogger):
+ def start(self, harness: 'TestHarness') -> None:
+ self.file.write(f'Log of Meson test suite run on {datetime.datetime.now().isoformat()}\n\n')
+ inherit_env = env_tuple_to_str(os.environ.items())
+ self.file.write(f'Inherited environment: {inherit_env}\n\n')
+
+ def log(self, harness: 'TestHarness', result: 'TestRun') -> None:
+ title = f'{result.num}/{harness.test_count}'
+ self.file.write(dashes(title, '=', 78) + '\n')
+ self.file.write('test: ' + result.name + '\n')
+ starttime_str = time.strftime("%H:%M:%S", time.gmtime(result.starttime))
+ self.file.write('start time: ' + starttime_str + '\n')
+ self.file.write('duration: ' + '%.2fs' % result.duration + '\n')
+ self.file.write('result: ' + result.get_exit_status() + '\n')
+ if result.cmdline:
+ self.file.write('command: ' + result.cmdline + '\n')
+ if result.stdo:
+ name = 'stdout' if harness.options.split else 'output'
+ self.file.write(dashes(name, '-', 78) + '\n')
+ self.file.write(result.stdo)
+ if result.stde:
+ self.file.write(dashes('stderr', '-', 78) + '\n')
+ self.file.write(result.stde)
+ self.file.write(dashes('', '=', 78) + '\n\n')
+
+ async def finish(self, harness: 'TestHarness') -> None:
+ if harness.collected_failures:
+ self.file.write("\nSummary of Failures:\n\n")
+ for i, result in enumerate(harness.collected_failures, 1):
+ self.file.write(harness.format(result, False) + '\n')
+ self.file.write(harness.summary())
+
+ print(f'Full log written to {self.filename}')
+
+
+class JsonLogfileBuilder(TestFileLogger):
+ def log(self, harness: 'TestHarness', result: 'TestRun') -> None:
+ jresult = {'name': result.name,
+ 'stdout': result.stdo,
+ 'result': result.res.value,
+ 'starttime': result.starttime,
+ 'duration': result.duration,
+ 'returncode': result.returncode,
+ 'env': result.env,
+ 'command': result.cmd} # type: T.Dict[str, T.Any]
+ if result.stde:
+ jresult['stderr'] = result.stde
+ self.file.write(json.dumps(jresult) + '\n')
+
+
+class JunitBuilder(TestLogger):
+
+ """Builder for Junit test results.
+
+ Junit is impossible to stream out, it requires attributes counting the
+ total number of tests, failures, skips, and errors in the root element
+ and in each test suite. As such, we use a builder class to track each
+ test case, and calculate all metadata before writing it out.
+
+ For tests with multiple results (like from a TAP test), we record the
+ test as a suite with the project_name.test_name. This allows us to track
+ each result separately. For tests with only one result (such as exit-code
+ tests) we record each one into a suite with the name project_name. The use
+ of the project_name allows us to sort subproject tests separately from
+ the root project.
+ """
+
+ def __init__(self, filename: str) -> None:
+ self.filename = filename
+ self.root = et.Element(
+ 'testsuites', tests='0', errors='0', failures='0')
+ self.suites = {} # type: T.Dict[str, et.Element]
+
+ def log(self, harness: 'TestHarness', test: 'TestRun') -> None:
+ """Log a single test case."""
+ if test.junit is not None:
+ for suite in test.junit.findall('.//testsuite'):
+ # Assume that we don't need to merge anything here...
+ suite.attrib['name'] = '{}.{}.{}'.format(test.project, test.name, suite.attrib['name'])
+
+ # GTest can inject invalid attributes
+ for case in suite.findall('.//testcase[@result]'):
+ del case.attrib['result']
+ for case in suite.findall('.//testcase[@timestamp]'):
+ del case.attrib['timestamp']
+ for case in suite.findall('.//testcase[@file]'):
+ del case.attrib['file']
+ for case in suite.findall('.//testcase[@line]'):
+ del case.attrib['line']
+ self.root.append(suite)
+ return
+
+ # In this case we have a test binary with multiple results.
+ # We want to record this so that each result is recorded
+ # separately
+ if test.results:
+ suitename = f'{test.project}.{test.name}'
+ assert suitename not in self.suites or harness.options.repeat > 1, 'duplicate suite'
+
+ suite = self.suites[suitename] = et.Element(
+ 'testsuite',
+ name=suitename,
+ tests=str(len(test.results)),
+ errors=str(sum(1 for r in test.results if r.result in
+ {TestResult.INTERRUPT, TestResult.ERROR})),
+ failures=str(sum(1 for r in test.results if r.result in
+ {TestResult.FAIL, TestResult.UNEXPECTEDPASS, TestResult.TIMEOUT})),
+ skipped=str(sum(1 for r in test.results if r.result is TestResult.SKIP)),
+ time=str(test.duration),
+ )
+
+ for subtest in test.results:
+ # Both name and classname are required. Use the suite name as
+ # the class name, so that e.g. GitLab groups testcases correctly.
+ testcase = et.SubElement(suite, 'testcase', name=str(subtest), classname=suitename)
+ if subtest.result is TestResult.SKIP:
+ et.SubElement(testcase, 'skipped')
+ elif subtest.result is TestResult.ERROR:
+ et.SubElement(testcase, 'error')
+ elif subtest.result is TestResult.FAIL:
+ et.SubElement(testcase, 'failure')
+ elif subtest.result is TestResult.UNEXPECTEDPASS:
+ fail = et.SubElement(testcase, 'failure')
+ fail.text = 'Test unexpected passed.'
+ elif subtest.result is TestResult.INTERRUPT:
+ fail = et.SubElement(testcase, 'error')
+ fail.text = 'Test was interrupted by user.'
+ elif subtest.result is TestResult.TIMEOUT:
+ fail = et.SubElement(testcase, 'error')
+ fail.text = 'Test did not finish before configured timeout.'
+ if subtest.explanation:
+ et.SubElement(testcase, 'system-out').text = subtest.explanation
+ if test.stdo:
+ out = et.SubElement(suite, 'system-out')
+ out.text = test.stdo.rstrip()
+ if test.stde:
+ err = et.SubElement(suite, 'system-err')
+ err.text = test.stde.rstrip()
+ else:
+ if test.project not in self.suites:
+ suite = self.suites[test.project] = et.Element(
+ 'testsuite', name=test.project, tests='1', errors='0',
+ failures='0', skipped='0', time=str(test.duration))
+ else:
+ suite = self.suites[test.project]
+ suite.attrib['tests'] = str(int(suite.attrib['tests']) + 1)
+
+ testcase = et.SubElement(suite, 'testcase', name=test.name,
+ classname=test.project, time=str(test.duration))
+ if test.res is TestResult.SKIP:
+ et.SubElement(testcase, 'skipped')
+ suite.attrib['skipped'] = str(int(suite.attrib['skipped']) + 1)
+ elif test.res is TestResult.ERROR:
+ et.SubElement(testcase, 'error')
+ suite.attrib['errors'] = str(int(suite.attrib['errors']) + 1)
+ elif test.res is TestResult.FAIL:
+ et.SubElement(testcase, 'failure')
+ suite.attrib['failures'] = str(int(suite.attrib['failures']) + 1)
+ if test.stdo:
+ out = et.SubElement(testcase, 'system-out')
+ out.text = test.stdo.rstrip()
+ if test.stde:
+ err = et.SubElement(testcase, 'system-err')
+ err.text = test.stde.rstrip()
+
+ async def finish(self, harness: 'TestHarness') -> None:
+ """Calculate total test counts and write out the xml result."""
+ for suite in self.suites.values():
+ self.root.append(suite)
+ # Skipped is really not allowed in the "testsuits" element
+ for attr in ['tests', 'errors', 'failures']:
+ self.root.attrib[attr] = str(int(self.root.attrib[attr]) + int(suite.attrib[attr]))
+
+ tree = et.ElementTree(self.root)
+ with open(self.filename, 'wb') as f:
+ tree.write(f, encoding='utf-8', xml_declaration=True)
+
+
+class TestRun:
+ TEST_NUM = 0
+ PROTOCOL_TO_CLASS: T.Dict[TestProtocol, T.Type['TestRun']] = {}
+
+ def __new__(cls, test: TestSerialisation, *args: T.Any, **kwargs: T.Any) -> T.Any:
+ return super().__new__(TestRun.PROTOCOL_TO_CLASS[test.protocol])
+
+ def __init__(self, test: TestSerialisation, test_env: T.Dict[str, str],
+ name: str, timeout: T.Optional[int], is_parallel: bool, verbose: bool):
+ self.res = TestResult.PENDING
+ self.test = test
+ self._num = None # type: T.Optional[int]
+ self.name = name
+ self.timeout = timeout
+ self.results = [] # type: T.List[TAPParser.Test]
+ self.returncode = None # type: T.Optional[int]
+ self.starttime = None # type: T.Optional[float]
+ self.duration = None # type: T.Optional[float]
+ self.stdo = ''
+ self.stde = ''
+ self.additional_error = ''
+ self.cmd = None # type: T.Optional[T.List[str]]
+ self.env = test_env # type: T.Dict[str, str]
+ self.should_fail = test.should_fail
+ self.project = test.project_name
+ self.junit = None # type: T.Optional[et.ElementTree]
+ self.is_parallel = is_parallel
+ self.verbose = verbose
+ self.warnings = [] # type: T.List[str]
+
+ def start(self, cmd: T.List[str]) -> None:
+ self.res = TestResult.RUNNING
+ self.starttime = time.time()
+ self.cmd = cmd
+
+ @property
+ def num(self) -> int:
+ if self._num is None:
+ TestRun.TEST_NUM += 1
+ self._num = TestRun.TEST_NUM
+ return self._num
+
+ @property
+ def direct_stdout(self) -> bool:
+ return self.verbose and not self.is_parallel and not self.needs_parsing
+
+ def get_results(self) -> str:
+ if self.results:
+ # running or succeeded
+ passed = sum(x.result.is_ok() for x in self.results)
+ ran = sum(x.result is not TestResult.SKIP for x in self.results)
+ if passed == ran:
+ return f'{passed} subtests passed'
+ else:
+ return f'{passed}/{ran} subtests passed'
+ return ''
+
+ def get_exit_status(self) -> str:
+ return returncode_to_status(self.returncode)
+
+ def get_details(self) -> str:
+ if self.res is TestResult.PENDING:
+ return ''
+ if self.returncode:
+ return self.get_exit_status()
+ return self.get_results()
+
+ def _complete(self) -> None:
+ if self.res == TestResult.RUNNING:
+ self.res = TestResult.OK
+ assert isinstance(self.res, TestResult)
+ if self.should_fail and self.res in (TestResult.OK, TestResult.FAIL):
+ self.res = TestResult.UNEXPECTEDPASS if self.res is TestResult.OK else TestResult.EXPECTEDFAIL
+ if self.stdo and not self.stdo.endswith('\n'):
+ self.stdo += '\n'
+ if self.stde and not self.stde.endswith('\n'):
+ self.stde += '\n'
+ self.duration = time.time() - self.starttime
+
+ @property
+ def cmdline(self) -> T.Optional[str]:
+ if not self.cmd:
+ return None
+ test_only_env = set(self.env.items()) - set(os.environ.items())
+ return env_tuple_to_str(test_only_env) + \
+ ' '.join(sh_quote(x) for x in self.cmd)
+
+ def complete_skip(self) -> None:
+ self.starttime = time.time()
+ self.returncode = GNU_SKIP_RETURNCODE
+ self.res = TestResult.SKIP
+ self._complete()
+
+ def complete(self) -> None:
+ self._complete()
+
+ def get_log(self, colorize: bool = False, stderr_only: bool = False) -> str:
+ stdo = '' if stderr_only else self.stdo
+ if self.stde or self.additional_error:
+ res = ''
+ if stdo:
+ res += mlog.cyan('stdout:').get_text(colorize) + '\n'
+ res += stdo
+ if res[-1:] != '\n':
+ res += '\n'
+ res += mlog.cyan('stderr:').get_text(colorize) + '\n'
+ res += join_lines(self.stde, self.additional_error)
+ else:
+ res = stdo
+ if res and res[-1:] != '\n':
+ res += '\n'
+ return res
+
+ @property
+ def needs_parsing(self) -> bool:
+ return False
+
+ async def parse(self, harness: 'TestHarness', lines: T.AsyncIterator[str]) -> None:
+ async for l in lines:
+ pass
+
+
+class TestRunExitCode(TestRun):
+
+ def complete(self) -> None:
+ if self.res != TestResult.RUNNING:
+ pass
+ elif self.returncode == GNU_SKIP_RETURNCODE:
+ self.res = TestResult.SKIP
+ elif self.returncode == GNU_ERROR_RETURNCODE:
+ self.res = TestResult.ERROR
+ else:
+ self.res = TestResult.FAIL if bool(self.returncode) else TestResult.OK
+ super().complete()
+
+TestRun.PROTOCOL_TO_CLASS[TestProtocol.EXITCODE] = TestRunExitCode
+
+
+class TestRunGTest(TestRunExitCode):
+ def complete(self) -> None:
+ filename = f'{self.test.name}.xml'
+ if self.test.workdir:
+ filename = os.path.join(self.test.workdir, filename)
+
+ try:
+ self.junit = et.parse(filename)
+ except FileNotFoundError:
+ # This can happen if the test fails to run or complete for some
+ # reason, like the rpath for libgtest isn't properly set. ExitCode
+ # will handle the failure, don't generate a stacktrace.
+ pass
+
+ super().complete()
+
+TestRun.PROTOCOL_TO_CLASS[TestProtocol.GTEST] = TestRunGTest
+
+
+class TestRunTAP(TestRun):
+ @property
+ def needs_parsing(self) -> bool:
+ return True
+
+ def complete(self) -> None:
+ if self.returncode != 0 and not self.res.was_killed():
+ self.res = TestResult.ERROR
+ self.stde = self.stde or ''
+ self.stde += f'\n(test program exited with status code {self.returncode})'
+ super().complete()
+
+ async def parse(self, harness: 'TestHarness', lines: T.AsyncIterator[str]) -> None:
+ res = None
+ warnings = [] # type: T.List[TAPParser.UnknownLine]
+ version = 12
+
+ async for i in TAPParser().parse_async(lines):
+ if isinstance(i, TAPParser.Version):
+ version = i.version
+ elif isinstance(i, TAPParser.Bailout):
+ res = TestResult.ERROR
+ harness.log_subtest(self, i.message, res)
+ elif isinstance(i, TAPParser.Test):
+ self.results.append(i)
+ if i.result.is_bad():
+ res = TestResult.FAIL
+ harness.log_subtest(self, i.name or f'subtest {i.number}', i.result)
+ elif isinstance(i, TAPParser.UnknownLine):
+ warnings.append(i)
+ elif isinstance(i, TAPParser.Error):
+ self.additional_error += 'TAP parsing error: ' + i.message
+ res = TestResult.ERROR
+
+ if warnings:
+ unknown = str(mlog.yellow('UNKNOWN'))
+ width = len(str(max(i.lineno for i in warnings)))
+ for w in warnings:
+ self.warnings.append(f'stdout: {w.lineno:{width}}: {unknown}: {w.message}')
+ if version > 13:
+ self.warnings.append('Unknown TAP output lines have been ignored. Please open a feature request to\n'
+ 'implement them, or prefix them with a # if they are not TAP syntax.')
+ else:
+ self.warnings.append(str(mlog.red('ERROR')) + ': Unknown TAP output lines for a supported TAP version.\n'
+ 'This is probably a bug in the test; if they are not TAP syntax, prefix them with a #')
+ if all(t.result is TestResult.SKIP for t in self.results):
+ # This includes the case where self.results is empty
+ res = TestResult.SKIP
+
+ if res and self.res == TestResult.RUNNING:
+ self.res = res
+
+TestRun.PROTOCOL_TO_CLASS[TestProtocol.TAP] = TestRunTAP
+
+
+class TestRunRust(TestRun):
+ @property
+ def needs_parsing(self) -> bool:
+ return True
+
+ async def parse(self, harness: 'TestHarness', lines: T.AsyncIterator[str]) -> None:
+ def parse_res(n: int, name: str, result: str) -> TAPParser.Test:
+ if result == 'ok':
+ return TAPParser.Test(n, name, TestResult.OK, None)
+ elif result == 'ignored':
+ return TAPParser.Test(n, name, TestResult.SKIP, None)
+ elif result == 'FAILED':
+ return TAPParser.Test(n, name, TestResult.FAIL, None)
+ return TAPParser.Test(n, name, TestResult.ERROR,
+ f'Unsupported output from rust test: {result}')
+
+ n = 1
+ async for line in lines:
+ if line.startswith('test ') and not line.startswith('test result'):
+ _, name, _, result = line.rstrip().split(' ')
+ name = name.replace('::', '.')
+ t = parse_res(n, name, result)
+ self.results.append(t)
+ harness.log_subtest(self, name, t.result)
+ n += 1
+
+ res = None
+
+ if all(t.result is TestResult.SKIP for t in self.results):
+ # This includes the case where self.results is empty
+ res = TestResult.SKIP
+ elif any(t.result is TestResult.ERROR for t in self.results):
+ res = TestResult.ERROR
+ elif any(t.result is TestResult.FAIL for t in self.results):
+ res = TestResult.FAIL
+
+ if res and self.res == TestResult.RUNNING:
+ self.res = res
+
+TestRun.PROTOCOL_TO_CLASS[TestProtocol.RUST] = TestRunRust
+
+
+def decode(stream: T.Union[None, bytes]) -> str:
+ if stream is None:
+ return ''
+ try:
+ return stream.decode('utf-8')
+ except UnicodeDecodeError:
+ return stream.decode('iso-8859-1', errors='ignore')
+
+async def read_decode(reader: asyncio.StreamReader,
+ queue: T.Optional['asyncio.Queue[T.Optional[str]]'],
+ console_mode: ConsoleUser) -> str:
+ stdo_lines = []
+ try:
+ while not reader.at_eof():
+ # Prefer splitting by line, as that produces nicer output
+ try:
+ line_bytes = await reader.readuntil(b'\n')
+ except asyncio.IncompleteReadError as e:
+ line_bytes = e.partial
+ except asyncio.LimitOverrunError as e:
+ line_bytes = await reader.readexactly(e.consumed)
+ if line_bytes:
+ line = decode(line_bytes)
+ stdo_lines.append(line)
+ if console_mode is ConsoleUser.STDOUT:
+ print(line, end='', flush=True)
+ if queue:
+ await queue.put(line)
+ return ''.join(stdo_lines)
+ except asyncio.CancelledError:
+ return ''.join(stdo_lines)
+ finally:
+ if queue:
+ await queue.put(None)
+
+def run_with_mono(fname: str) -> bool:
+ return fname.endswith('.exe') and not (is_windows() or is_cygwin())
+
+def check_testdata(objs: T.List[TestSerialisation]) -> T.List[TestSerialisation]:
+ if not isinstance(objs, list):
+ raise MesonVersionMismatchException('<unknown>', coredata_version)
+ for obj in objs:
+ if not isinstance(obj, TestSerialisation):
+ raise MesonVersionMismatchException('<unknown>', coredata_version)
+ if not hasattr(obj, 'version'):
+ raise MesonVersionMismatchException('<unknown>', coredata_version)
+ if major_versions_differ(obj.version, coredata_version):
+ raise MesonVersionMismatchException(obj.version, coredata_version)
+ return objs
+
+# Custom waiting primitives for asyncio
+
+async def queue_iter(q: 'asyncio.Queue[T.Optional[str]]') -> T.AsyncIterator[str]:
+ while True:
+ item = await q.get()
+ q.task_done()
+ if item is None:
+ break
+ yield item
+
+async def complete(future: asyncio.Future) -> None:
+ """Wait for completion of the given future, ignoring cancellation."""
+ try:
+ await future
+ except asyncio.CancelledError:
+ pass
+
+async def complete_all(futures: T.Iterable[asyncio.Future],
+ timeout: T.Optional[T.Union[int, float]] = None) -> None:
+ """Wait for completion of all the given futures, ignoring cancellation.
+ If timeout is not None, raise an asyncio.TimeoutError after the given
+ time has passed. asyncio.TimeoutError is only raised if some futures
+ have not completed and none have raised exceptions, even if timeout
+ is zero."""
+
+ def check_futures(futures: T.Iterable[asyncio.Future]) -> None:
+ # Raise exceptions if needed
+ left = False
+ for f in futures:
+ if not f.done():
+ left = True
+ elif not f.cancelled():
+ f.result()
+ if left:
+ raise asyncio.TimeoutError
+
+ # Python is silly and does not have a variant of asyncio.wait with an
+ # absolute time as deadline.
+ deadline = None if timeout is None else asyncio.get_event_loop().time() + timeout
+ while futures and (timeout is None or timeout > 0):
+ done, futures = await asyncio.wait(futures, timeout=timeout,
+ return_when=asyncio.FIRST_EXCEPTION)
+ check_futures(done)
+ if deadline:
+ timeout = deadline - asyncio.get_event_loop().time()
+
+ check_futures(futures)
+
+
+class TestSubprocess:
+ def __init__(self, p: asyncio.subprocess.Process,
+ stdout: T.Optional[int], stderr: T.Optional[int],
+ postwait_fn: T.Callable[[], None] = None):
+ self._process = p
+ self.stdout = stdout
+ self.stderr = stderr
+ self.stdo_task: T.Optional[asyncio.Task[None]] = None
+ self.stde_task: T.Optional[asyncio.Task[None]] = None
+ self.postwait_fn = postwait_fn # type: T.Callable[[], None]
+ self.all_futures = [] # type: T.List[asyncio.Future]
+ self.queue = None # type: T.Optional[asyncio.Queue[T.Optional[str]]]
+
+ def stdout_lines(self) -> T.AsyncIterator[str]:
+ self.queue = asyncio.Queue()
+ return queue_iter(self.queue)
+
+ def communicate(self,
+ test: 'TestRun',
+ console_mode: ConsoleUser) -> T.Tuple[T.Optional[T.Awaitable[str]],
+ T.Optional[T.Awaitable[str]]]:
+ async def collect_stdo(test: 'TestRun',
+ reader: asyncio.StreamReader,
+ console_mode: ConsoleUser) -> None:
+ test.stdo = await read_decode(reader, self.queue, console_mode)
+
+ async def collect_stde(test: 'TestRun',
+ reader: asyncio.StreamReader,
+ console_mode: ConsoleUser) -> None:
+ test.stde = await read_decode(reader, None, console_mode)
+
+ # asyncio.ensure_future ensures that printing can
+ # run in the background, even before it is awaited
+ if self.stdo_task is None and self.stdout is not None:
+ decode_coro = collect_stdo(test, self._process.stdout, console_mode)
+ self.stdo_task = asyncio.ensure_future(decode_coro)
+ self.all_futures.append(self.stdo_task)
+ if self.stderr is not None and self.stderr != asyncio.subprocess.STDOUT:
+ decode_coro = collect_stde(test, self._process.stderr, console_mode)
+ self.stde_task = asyncio.ensure_future(decode_coro)
+ self.all_futures.append(self.stde_task)
+
+ return self.stdo_task, self.stde_task
+
+ async def _kill(self) -> T.Optional[str]:
+ # Python does not provide multiplatform support for
+ # killing a process and all its children so we need
+ # to roll our own.
+ p = self._process
+ try:
+ if is_windows():
+ subprocess.run(['taskkill', '/F', '/T', '/PID', str(p.pid)])
+ else:
+ # Send a termination signal to the process group that setsid()
+ # created - giving it a chance to perform any cleanup.
+ os.killpg(p.pid, signal.SIGTERM)
+
+ # Make sure the termination signal actually kills the process
+ # group, otherwise retry with a SIGKILL.
+ with suppress(asyncio.TimeoutError):
+ await asyncio.wait_for(p.wait(), timeout=0.5)
+ if p.returncode is not None:
+ return None
+
+ os.killpg(p.pid, signal.SIGKILL)
+
+ with suppress(asyncio.TimeoutError):
+ await asyncio.wait_for(p.wait(), timeout=1)
+ if p.returncode is not None:
+ return None
+
+ # An earlier kill attempt has not worked for whatever reason.
+ # Try to kill it one last time with a direct call.
+ # If the process has spawned children, they will remain around.
+ p.kill()
+ with suppress(asyncio.TimeoutError):
+ await asyncio.wait_for(p.wait(), timeout=1)
+ if p.returncode is not None:
+ return None
+ return 'Test process could not be killed.'
+ except ProcessLookupError:
+ # Sometimes (e.g. with Wine) this happens. There's nothing
+ # we can do, probably the process already died so just wait
+ # for the event loop to pick that up.
+ await p.wait()
+ return None
+ finally:
+ if self.stdo_task:
+ self.stdo_task.cancel()
+ if self.stde_task:
+ self.stde_task.cancel()
+
+ async def wait(self, test: 'TestRun') -> None:
+ p = self._process
+
+ self.all_futures.append(asyncio.ensure_future(p.wait()))
+ try:
+ await complete_all(self.all_futures, timeout=test.timeout)
+ except asyncio.TimeoutError:
+ test.additional_error += await self._kill() or ''
+ test.res = TestResult.TIMEOUT
+ except asyncio.CancelledError:
+ # The main loop must have seen Ctrl-C.
+ test.additional_error += await self._kill() or ''
+ test.res = TestResult.INTERRUPT
+ finally:
+ if self.postwait_fn:
+ self.postwait_fn()
+
+ test.returncode = p.returncode or 0
+
+class SingleTestRunner:
+
+ def __init__(self, test: TestSerialisation, env: T.Dict[str, str], name: str,
+ options: argparse.Namespace):
+ self.test = test
+ self.options = options
+ self.cmd = self._get_cmd()
+
+ if self.cmd and self.test.extra_paths:
+ env['PATH'] = os.pathsep.join(self.test.extra_paths + ['']) + env['PATH']
+ winecmd = []
+ for c in self.cmd:
+ winecmd.append(c)
+ if os.path.basename(c).startswith('wine'):
+ env['WINEPATH'] = get_wine_shortpath(
+ winecmd,
+ ['Z:' + p for p in self.test.extra_paths] + env.get('WINEPATH', '').split(';'),
+ self.test.workdir
+ )
+ break
+
+ # If MALLOC_PERTURB_ is not set, or if it is set to an empty value,
+ # (i.e., the test or the environment don't explicitly set it), set
+ # it ourselves. We do this unconditionally for regular tests
+ # because it is extremely useful to have.
+ # Setting MALLOC_PERTURB_="0" will completely disable this feature.
+ if ('MALLOC_PERTURB_' not in env or not env['MALLOC_PERTURB_']) and not options.benchmark:
+ env['MALLOC_PERTURB_'] = str(random.randint(1, 255))
+
+ if self.options.gdb or self.test.timeout is None or self.test.timeout <= 0:
+ timeout = None
+ elif self.options.timeout_multiplier is None:
+ timeout = self.test.timeout
+ elif self.options.timeout_multiplier <= 0:
+ timeout = None
+ else:
+ timeout = self.test.timeout * self.options.timeout_multiplier
+
+ is_parallel = test.is_parallel and self.options.num_processes > 1 and not self.options.gdb
+ verbose = (test.verbose or self.options.verbose) and not self.options.quiet
+ self.runobj = TestRun(test, env, name, timeout, is_parallel, verbose)
+
+ if self.options.gdb:
+ self.console_mode = ConsoleUser.GDB
+ elif self.runobj.direct_stdout:
+ self.console_mode = ConsoleUser.STDOUT
+ else:
+ self.console_mode = ConsoleUser.LOGGER
+
+ def _get_test_cmd(self) -> T.Optional[T.List[str]]:
+ testentry = self.test.fname[0]
+ if self.options.no_rebuild and self.test.cmd_is_built and not os.path.isfile(testentry):
+ raise TestException(f'The test program {testentry!r} does not exist. Cannot run tests before building them.')
+ if testentry.endswith('.jar'):
+ return ['java', '-jar'] + self.test.fname
+ elif not self.test.is_cross_built and run_with_mono(testentry):
+ return ['mono'] + self.test.fname
+ elif self.test.cmd_is_exe and self.test.is_cross_built and self.test.needs_exe_wrapper:
+ if self.test.exe_wrapper is None:
+ # Can not run test on cross compiled executable
+ # because there is no execute wrapper.
+ return None
+ elif self.test.cmd_is_exe:
+ # If the command is not built (ie, its a python script),
+ # then we don't check for the exe-wrapper
+ if not self.test.exe_wrapper.found():
+ msg = ('The exe_wrapper defined in the cross file {!r} was not '
+ 'found. Please check the command and/or add it to PATH.')
+ raise TestException(msg.format(self.test.exe_wrapper.name))
+ return self.test.exe_wrapper.get_command() + self.test.fname
+ return self.test.fname
+
+ def _get_cmd(self) -> T.Optional[T.List[str]]:
+ test_cmd = self._get_test_cmd()
+ if not test_cmd:
+ return None
+ return TestHarness.get_wrapper(self.options) + test_cmd
+
+ @property
+ def is_parallel(self) -> bool:
+ return self.runobj.is_parallel
+
+ @property
+ def visible_name(self) -> str:
+ return self.runobj.name
+
+ @property
+ def timeout(self) -> T.Optional[int]:
+ return self.runobj.timeout
+
+ async def run(self, harness: 'TestHarness') -> TestRun:
+ if self.cmd is None:
+ self.stdo = 'Not run because can not execute cross compiled binaries.'
+ harness.log_start_test(self.runobj)
+ self.runobj.complete_skip()
+ else:
+ cmd = self.cmd + self.test.cmd_args + self.options.test_args
+ self.runobj.start(cmd)
+ harness.log_start_test(self.runobj)
+ await self._run_cmd(harness, cmd)
+ return self.runobj
+
+ async def _run_subprocess(self, args: T.List[str], *,
+ stdout: T.Optional[int], stderr: T.Optional[int],
+ env: T.Dict[str, str], cwd: T.Optional[str]) -> TestSubprocess:
+ # Let gdb handle ^C instead of us
+ if self.options.gdb:
+ previous_sigint_handler = signal.getsignal(signal.SIGINT)
+ # Make the meson executable ignore SIGINT while gdb is running.
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
+
+ def preexec_fn() -> None:
+ if self.options.gdb:
+ # Restore the SIGINT handler for the child process to
+ # ensure it can handle it.
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+ else:
+ # We don't want setsid() in gdb because gdb needs the
+ # terminal in order to handle ^C and not show tcsetpgrp()
+ # errors avoid not being able to use the terminal.
+ os.setsid()
+
+ def postwait_fn() -> None:
+ if self.options.gdb:
+ # Let us accept ^C again
+ signal.signal(signal.SIGINT, previous_sigint_handler)
+
+ p = await asyncio.create_subprocess_exec(*args,
+ stdout=stdout,
+ stderr=stderr,
+ env=env,
+ cwd=cwd,
+ preexec_fn=preexec_fn if not is_windows() else None)
+ return TestSubprocess(p, stdout=stdout, stderr=stderr,
+ postwait_fn=postwait_fn if not is_windows() else None)
+
+ async def _run_cmd(self, harness: 'TestHarness', cmd: T.List[str]) -> None:
+ if self.console_mode is ConsoleUser.GDB:
+ stdout = None
+ stderr = None
+ else:
+ stdout = asyncio.subprocess.PIPE
+ stderr = asyncio.subprocess.STDOUT \
+ if not self.options.split and not self.runobj.needs_parsing \
+ else asyncio.subprocess.PIPE
+
+ extra_cmd = [] # type: T.List[str]
+ if self.test.protocol is TestProtocol.GTEST:
+ gtestname = self.test.name
+ if self.test.workdir:
+ gtestname = os.path.join(self.test.workdir, self.test.name)
+ extra_cmd.append(f'--gtest_output=xml:{gtestname}.xml')
+
+ p = await self._run_subprocess(cmd + extra_cmd,
+ stdout=stdout,
+ stderr=stderr,
+ env=self.runobj.env,
+ cwd=self.test.workdir)
+
+ if self.runobj.needs_parsing:
+ parse_coro = self.runobj.parse(harness, p.stdout_lines())
+ parse_task = asyncio.ensure_future(parse_coro)
+ else:
+ parse_task = None
+
+ stdo_task, stde_task = p.communicate(self.runobj, self.console_mode)
+ await p.wait(self.runobj)
+
+ if parse_task:
+ await parse_task
+ if stdo_task:
+ await stdo_task
+ if stde_task:
+ await stde_task
+
+ self.runobj.complete()
+
+
+class TestHarness:
+ def __init__(self, options: argparse.Namespace):
+ self.options = options
+ self.collected_failures = [] # type: T.List[TestRun]
+ self.fail_count = 0
+ self.expectedfail_count = 0
+ self.unexpectedpass_count = 0
+ self.success_count = 0
+ self.skip_count = 0
+ self.timeout_count = 0
+ self.test_count = 0
+ self.name_max_len = 0
+ self.is_run = False
+ self.loggers = [] # type: T.List[TestLogger]
+ self.console_logger = ConsoleLogger()
+ self.loggers.append(self.console_logger)
+ self.need_console = False
+ self.ninja = None # type: T.List[str]
+
+ self.logfile_base = None # type: T.Optional[str]
+ if self.options.logbase and not self.options.gdb:
+ namebase = None
+ self.logfile_base = os.path.join(self.options.wd, 'meson-logs', self.options.logbase)
+
+ if self.options.wrapper:
+ namebase = os.path.basename(self.get_wrapper(self.options)[0])
+ elif self.options.setup:
+ namebase = self.options.setup.replace(":", "_")
+
+ if namebase:
+ self.logfile_base += '-' + namebase.replace(' ', '_')
+
+ self.prepare_build()
+ self.load_metadata()
+
+ ss = set()
+ for t in self.tests:
+ for s in t.suite:
+ ss.add(s)
+ self.suites = list(ss)
+
+ def get_console_logger(self) -> 'ConsoleLogger':
+ assert self.console_logger
+ return self.console_logger
+
+ def prepare_build(self) -> None:
+ if self.options.no_rebuild:
+ return
+
+ if not (Path(self.options.wd) / 'build.ninja').is_file():
+ print('Only ninja backend is supported to rebuild tests before running them.')
+ # Disable, no point in trying to build anything later
+ self.options.no_rebuild = True
+ return
+
+ self.ninja = environment.detect_ninja()
+ if not self.ninja:
+ print("Can't find ninja, can't rebuild test.")
+ # If ninja can't be found return exit code 127, indicating command
+ # not found for shell, which seems appropriate here. This works
+ # nicely for `git bisect run`, telling it to abort - no point in
+ # continuing if there's no ninja.
+ sys.exit(127)
+
+ def load_metadata(self) -> None:
+ startdir = os.getcwd()
+ try:
+ os.chdir(self.options.wd)
+
+ # Before loading build / test data, make sure that the build
+ # configuration does not need to be regenerated. This needs to
+ # happen before rebuild_deps(), because we need the correct list of
+ # tests and their dependencies to compute
+ if not self.options.no_rebuild:
+ ret = subprocess.run(self.ninja + ['build.ninja']).returncode
+ if ret != 0:
+ raise TestException(f'Could not configure {self.options.wd!r}')
+
+ self.build_data = build.load(os.getcwd())
+ if not self.options.setup:
+ self.options.setup = self.build_data.test_setup_default_name
+ if self.options.benchmark:
+ self.tests = self.load_tests('meson_benchmark_setup.dat')
+ else:
+ self.tests = self.load_tests('meson_test_setup.dat')
+ finally:
+ os.chdir(startdir)
+
+ def load_tests(self, file_name: str) -> T.List[TestSerialisation]:
+ datafile = Path('meson-private') / file_name
+ if not datafile.is_file():
+ raise TestException(f'Directory {self.options.wd!r} does not seem to be a Meson build directory.')
+ with datafile.open('rb') as f:
+ objs = check_testdata(pickle.load(f))
+ return objs
+
+ def __enter__(self) -> 'TestHarness':
+ return self
+
+ def __exit__(self, exc_type: T.Any, exc_value: T.Any, traceback: T.Any) -> None:
+ self.close_logfiles()
+
+ def close_logfiles(self) -> None:
+ for l in self.loggers:
+ l.close()
+ self.console_logger = None
+
+ def get_test_setup(self, test: T.Optional[TestSerialisation]) -> build.TestSetup:
+ if ':' in self.options.setup:
+ if self.options.setup not in self.build_data.test_setups:
+ sys.exit(f"Unknown test setup '{self.options.setup}'.")
+ return self.build_data.test_setups[self.options.setup]
+ else:
+ full_name = test.project_name + ":" + self.options.setup
+ if full_name not in self.build_data.test_setups:
+ sys.exit(f"Test setup '{self.options.setup}' not found from project '{test.project_name}'.")
+ return self.build_data.test_setups[full_name]
+
+ def merge_setup_options(self, options: argparse.Namespace, test: TestSerialisation) -> T.Dict[str, str]:
+ current = self.get_test_setup(test)
+ if not options.gdb:
+ options.gdb = current.gdb
+ if options.gdb:
+ options.verbose = True
+ if options.timeout_multiplier is None:
+ options.timeout_multiplier = current.timeout_multiplier
+ # if options.env is None:
+ # options.env = current.env # FIXME, should probably merge options here.
+ if options.wrapper is None:
+ options.wrapper = current.exe_wrapper
+ elif current.exe_wrapper:
+ sys.exit('Conflict: both test setup and command line specify an exe wrapper.')
+ return current.env.get_env(os.environ.copy())
+
+ def get_test_runner(self, test: TestSerialisation) -> SingleTestRunner:
+ name = self.get_pretty_suite(test)
+ options = deepcopy(self.options)
+ if self.options.setup:
+ env = self.merge_setup_options(options, test)
+ else:
+ env = os.environ.copy()
+ test_env = test.env.get_env(env)
+ env.update(test_env)
+ if (test.is_cross_built and test.needs_exe_wrapper and
+ test.exe_wrapper and test.exe_wrapper.found()):
+ env['MESON_EXE_WRAPPER'] = join_args(test.exe_wrapper.get_command())
+ return SingleTestRunner(test, env, name, options)
+
+ def process_test_result(self, result: TestRun) -> None:
+ if result.res is TestResult.TIMEOUT:
+ self.timeout_count += 1
+ elif result.res is TestResult.SKIP:
+ self.skip_count += 1
+ elif result.res is TestResult.OK:
+ self.success_count += 1
+ elif result.res in {TestResult.FAIL, TestResult.ERROR, TestResult.INTERRUPT}:
+ self.fail_count += 1
+ elif result.res is TestResult.EXPECTEDFAIL:
+ self.expectedfail_count += 1
+ elif result.res is TestResult.UNEXPECTEDPASS:
+ self.unexpectedpass_count += 1
+ else:
+ sys.exit(f'Unknown test result encountered: {result.res}')
+
+ if result.res.is_bad():
+ self.collected_failures.append(result)
+ for l in self.loggers:
+ l.log(self, result)
+
+ @property
+ def numlen(self) -> int:
+ return len(str(self.test_count))
+
+ @property
+ def max_left_width(self) -> int:
+ return 2 * self.numlen + 2
+
+ def get_test_num_prefix(self, num: int) -> str:
+ return '{num:{numlen}}/{testcount} '.format(numlen=self.numlen,
+ num=num,
+ testcount=self.test_count)
+
+ def format(self, result: TestRun, colorize: bool,
+ max_left_width: int = 0,
+ prefix: str = '',
+ left: T.Optional[str] = None,
+ middle: T.Optional[str] = None,
+ right: T.Optional[str] = None) -> str:
+ if left is None:
+ left = self.get_test_num_prefix(result.num)
+
+ # A non-default max_left_width lets the logger print more stuff before the
+ # name, while ensuring that the rightmost columns remain aligned.
+ max_left_width = max(max_left_width, self.max_left_width)
+
+ if middle is None:
+ middle = result.name
+ extra_mid_width = max_left_width + self.name_max_len + 1 - uniwidth(middle) - uniwidth(left) - uniwidth(prefix)
+ middle += ' ' * max(1, extra_mid_width)
+
+ if right is None:
+ right = '{res} {dur:{durlen}.2f}s'.format(
+ res=result.res.get_text(colorize),
+ dur=result.duration,
+ durlen=self.duration_max_len + 3)
+ details = result.get_details()
+ if details:
+ right += ' ' + details
+ return prefix + left + middle + right
+
+ def summary(self) -> str:
+ return textwrap.dedent('''
+ Ok: {:<4}
+ Expected Fail: {:<4}
+ Fail: {:<4}
+ Unexpected Pass: {:<4}
+ Skipped: {:<4}
+ Timeout: {:<4}
+ ''').format(self.success_count, self.expectedfail_count, self.fail_count,
+ self.unexpectedpass_count, self.skip_count, self.timeout_count)
+
+ def total_failure_count(self) -> int:
+ return self.fail_count + self.unexpectedpass_count + self.timeout_count
+
+ def doit(self) -> int:
+ if self.is_run:
+ raise RuntimeError('Test harness object can only be used once.')
+ self.is_run = True
+ tests = self.get_tests()
+ if not tests:
+ return 0
+ if not self.options.no_rebuild and not rebuild_deps(self.ninja, self.options.wd, tests):
+ # We return 125 here in case the build failed.
+ # The reason is that exit code 125 tells `git bisect run` that the current
+ # commit should be skipped. Thus users can directly use `meson test` to
+ # bisect without needing to handle the does-not-build case separately in a
+ # wrapper script.
+ sys.exit(125)
+
+ self.name_max_len = max(uniwidth(self.get_pretty_suite(test)) for test in tests)
+ self.options.num_processes = min(self.options.num_processes,
+ len(tests) * self.options.repeat)
+ startdir = os.getcwd()
+ try:
+ os.chdir(self.options.wd)
+ runners = [] # type: T.List[SingleTestRunner]
+ for i in range(self.options.repeat):
+ runners.extend(self.get_test_runner(test) for test in tests)
+ if i == 0:
+ self.duration_max_len = max(len(str(int(runner.timeout or 99)))
+ for runner in runners)
+ # Disable the progress report if it gets in the way
+ self.need_console = any(runner.console_mode is not ConsoleUser.LOGGER
+ for runner in runners)
+
+ self.test_count = len(runners)
+ self.run_tests(runners)
+ finally:
+ os.chdir(startdir)
+ return self.total_failure_count()
+
+ @staticmethod
+ def split_suite_string(suite: str) -> T.Tuple[str, str]:
+ if ':' in suite:
+ split = suite.split(':', 1)
+ assert len(split) == 2
+ return split[0], split[1]
+ else:
+ return suite, ""
+
+ @staticmethod
+ def test_in_suites(test: TestSerialisation, suites: T.List[str]) -> bool:
+ for suite in suites:
+ (prj_match, st_match) = TestHarness.split_suite_string(suite)
+ for prjst in test.suite:
+ (prj, st) = TestHarness.split_suite_string(prjst)
+
+ # the SUITE can be passed as
+ # suite_name
+ # or
+ # project_name:suite_name
+ # so we need to select only the test belonging to project_name
+
+ # this if handle the first case (i.e., SUITE == suite_name)
+
+ # in this way we can run tests belonging to different
+ # (sub)projects which share the same suite_name
+ if not st_match and st == prj_match:
+ return True
+
+ # these two conditions are needed to handle the second option
+ # i.e., SUITE == project_name:suite_name
+
+ # in this way we select the only the tests of
+ # project_name with suite_name
+ if prj_match and prj != prj_match:
+ continue
+ if st_match and st != st_match:
+ continue
+ return True
+ return False
+
+ def test_suitable(self, test: TestSerialisation) -> bool:
+ if TestHarness.test_in_suites(test, self.options.exclude_suites):
+ return False
+
+ if self.options.include_suites:
+ # Both force inclusion (overriding add_test_setup) and exclude
+ # everything else
+ return TestHarness.test_in_suites(test, self.options.include_suites)
+
+ if self.options.setup:
+ setup = self.get_test_setup(test)
+ if TestHarness.test_in_suites(test, setup.exclude_suites):
+ return False
+
+ return True
+
+ def tests_from_args(self, tests: T.List[TestSerialisation]) -> T.Generator[TestSerialisation, None, None]:
+ '''
+ Allow specifying test names like "meson test foo1 foo2", where test('foo1', ...)
+
+ Also support specifying the subproject to run tests from like
+ "meson test subproj:" (all tests inside subproj) or "meson test subproj:foo1"
+ to run foo1 inside subproj. Coincidentally also "meson test :foo1" to
+ run all tests with that name across all subprojects, which is
+ identical to "meson test foo1"
+ '''
+ for arg in self.options.args:
+ if ':' in arg:
+ subproj, name = arg.split(':', maxsplit=1)
+ else:
+ subproj, name = '', arg
+ for t in tests:
+ if subproj and t.project_name != subproj:
+ continue
+ if name and t.name != name:
+ continue
+ yield t
+
+ def get_tests(self) -> T.List[TestSerialisation]:
+ if not self.tests:
+ print('No tests defined.')
+ return []
+
+ tests = [t for t in self.tests if self.test_suitable(t)]
+ if self.options.args:
+ tests = list(self.tests_from_args(tests))
+
+ if not tests:
+ print('No suitable tests defined.')
+ return []
+
+ return tests
+
+ def flush_logfiles(self) -> None:
+ for l in self.loggers:
+ l.flush()
+
+ def open_logfiles(self) -> None:
+ if not self.logfile_base:
+ return
+
+ self.loggers.append(JunitBuilder(self.logfile_base + '.junit.xml'))
+ self.loggers.append(JsonLogfileBuilder(self.logfile_base + '.json'))
+ self.loggers.append(TextLogfileBuilder(self.logfile_base + '.txt', errors='surrogateescape'))
+
+ @staticmethod
+ def get_wrapper(options: argparse.Namespace) -> T.List[str]:
+ wrap = [] # type: T.List[str]
+ if options.gdb:
+ wrap = [options.gdb_path, '--quiet']
+ if options.repeat > 1:
+ wrap += ['-ex', 'run', '-ex', 'quit']
+ # Signal the end of arguments to gdb
+ wrap += ['--args']
+ if options.wrapper:
+ wrap += options.wrapper
+ return wrap
+
+ def get_pretty_suite(self, test: TestSerialisation) -> str:
+ if len(self.suites) > 1 and test.suite:
+ rv = TestHarness.split_suite_string(test.suite[0])[0]
+ s = "+".join(TestHarness.split_suite_string(s)[1] for s in test.suite)
+ if s:
+ rv += ":"
+ return rv + s + " / " + test.name
+ else:
+ return test.name
+
+ def run_tests(self, runners: T.List[SingleTestRunner]) -> None:
+ try:
+ self.open_logfiles()
+ # Replace with asyncio.run once we can require Python 3.7
+ loop = asyncio.get_event_loop()
+ loop.run_until_complete(self._run_tests(runners))
+ finally:
+ self.close_logfiles()
+
+ def log_subtest(self, test: TestRun, s: str, res: TestResult) -> None:
+ for l in self.loggers:
+ l.log_subtest(self, test, s, res)
+
+ def log_start_test(self, test: TestRun) -> None:
+ for l in self.loggers:
+ l.start_test(self, test)
+
+ async def _run_tests(self, runners: T.List[SingleTestRunner]) -> None:
+ semaphore = asyncio.Semaphore(self.options.num_processes)
+ futures = deque() # type: T.Deque[asyncio.Future]
+ running_tests = {} # type: T.Dict[asyncio.Future, str]
+ interrupted = False
+ ctrlc_times = deque(maxlen=MAX_CTRLC) # type: T.Deque[float]
+
+ async def run_test(test: SingleTestRunner) -> None:
+ async with semaphore:
+ if interrupted or (self.options.repeat > 1 and self.fail_count):
+ return
+ res = await test.run(self)
+ self.process_test_result(res)
+ maxfail = self.options.maxfail
+ if maxfail and self.fail_count >= maxfail and res.res.is_bad():
+ cancel_all_tests()
+
+ def test_done(f: asyncio.Future) -> None:
+ if not f.cancelled():
+ f.result()
+ futures.remove(f)
+ try:
+ del running_tests[f]
+ except KeyError:
+ pass
+
+ def cancel_one_test(warn: bool) -> None:
+ future = futures.popleft()
+ futures.append(future)
+ if warn:
+ self.flush_logfiles()
+ mlog.warning('CTRL-C detected, interrupting {}'.format(running_tests[future]))
+ del running_tests[future]
+ future.cancel()
+
+ def cancel_all_tests() -> None:
+ nonlocal interrupted
+ interrupted = True
+ while running_tests:
+ cancel_one_test(False)
+
+ def sigterm_handler() -> None:
+ if interrupted:
+ return
+ self.flush_logfiles()
+ mlog.warning('Received SIGTERM, exiting')
+ cancel_all_tests()
+
+ def sigint_handler() -> None:
+ # We always pick the longest-running future that has not been cancelled
+ # If all the tests have been CTRL-C'ed, just stop
+ nonlocal interrupted
+ if interrupted:
+ return
+ ctrlc_times.append(asyncio.get_event_loop().time())
+ if len(ctrlc_times) == MAX_CTRLC and ctrlc_times[-1] - ctrlc_times[0] < 1:
+ self.flush_logfiles()
+ mlog.warning('CTRL-C detected, exiting')
+ cancel_all_tests()
+ elif running_tests:
+ cancel_one_test(True)
+ else:
+ self.flush_logfiles()
+ mlog.warning('CTRL-C detected, exiting')
+ interrupted = True
+
+ for l in self.loggers:
+ l.start(self)
+
+ if sys.platform != 'win32':
+ if os.getpgid(0) == os.getpid():
+ asyncio.get_event_loop().add_signal_handler(signal.SIGINT, sigint_handler)
+ else:
+ asyncio.get_event_loop().add_signal_handler(signal.SIGINT, sigterm_handler)
+ asyncio.get_event_loop().add_signal_handler(signal.SIGTERM, sigterm_handler)
+ try:
+ for runner in runners:
+ if not runner.is_parallel:
+ await complete_all(futures)
+ future = asyncio.ensure_future(run_test(runner))
+ futures.append(future)
+ running_tests[future] = runner.visible_name
+ future.add_done_callback(test_done)
+ if not runner.is_parallel:
+ await complete(future)
+ if self.options.repeat > 1 and self.fail_count:
+ break
+
+ await complete_all(futures)
+ finally:
+ if sys.platform != 'win32':
+ asyncio.get_event_loop().remove_signal_handler(signal.SIGINT)
+ asyncio.get_event_loop().remove_signal_handler(signal.SIGTERM)
+ for l in self.loggers:
+ await l.finish(self)
+
+def list_tests(th: TestHarness) -> bool:
+ tests = th.get_tests()
+ for t in tests:
+ print(th.get_pretty_suite(t))
+ return not tests
+
+def rebuild_deps(ninja: T.List[str], wd: str, tests: T.List[TestSerialisation]) -> bool:
+ def convert_path_to_target(path: str) -> str:
+ path = os.path.relpath(path, wd)
+ if os.sep != '/':
+ path = path.replace(os.sep, '/')
+ return path
+
+ assert len(ninja) > 0
+
+ depends = set() # type: T.Set[str]
+ targets = set() # type: T.Set[str]
+ intro_targets = {} # type: T.Dict[str, T.List[str]]
+ for target in load_info_file(get_infodir(wd), kind='targets'):
+ intro_targets[target['id']] = [
+ convert_path_to_target(f)
+ for f in target['filename']]
+ for t in tests:
+ for d in t.depends:
+ if d in depends:
+ continue
+ depends.update(d)
+ targets.update(intro_targets[d])
+
+ ret = subprocess.run(ninja + ['-C', wd] + sorted(targets)).returncode
+ if ret != 0:
+ print(f'Could not rebuild {wd}')
+ return False
+
+ return True
+
+def run(options: argparse.Namespace) -> int:
+ if options.benchmark:
+ options.num_processes = 1
+
+ if options.verbose and options.quiet:
+ print('Can not be both quiet and verbose at the same time.')
+ return 1
+
+ check_bin = None
+ if options.gdb:
+ options.verbose = True
+ if options.wrapper:
+ print('Must not specify both a wrapper and gdb at the same time.')
+ return 1
+ check_bin = 'gdb'
+
+ if options.wrapper:
+ check_bin = options.wrapper[0]
+
+ if sys.platform == 'win32':
+ loop = asyncio.ProactorEventLoop()
+ asyncio.set_event_loop(loop)
+
+ if check_bin is not None:
+ exe = ExternalProgram(check_bin, silent=True)
+ if not exe.found():
+ print(f'Could not find requested program: {check_bin!r}')
+ return 1
+
+ b = build.load(options.wd)
+ setup_vsenv(b.need_vsenv)
+
+ with TestHarness(options) as th:
+ try:
+ if options.list:
+ return list_tests(th)
+ return th.doit()
+ except TestException as e:
+ print('Meson test encountered an error:\n')
+ if os.environ.get('MESON_FORCE_BACKTRACE'):
+ raise e
+ else:
+ print(e)
+ return 1
+
+def run_with_args(args: T.List[str]) -> int:
+ parser = argparse.ArgumentParser(prog='meson test')
+ add_arguments(parser)
+ options = parser.parse_args(args)
+ return run(options)
diff --git a/mesonbuild/munstable_coredata.py b/mesonbuild/munstable_coredata.py
new file mode 100644
index 0000000..e6c543b
--- /dev/null
+++ b/mesonbuild/munstable_coredata.py
@@ -0,0 +1,115 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+
+from . import coredata as cdata
+from .mesonlib import MachineChoice, OptionKey
+
+import os.path
+import pprint
+import textwrap
+
+def add_arguments(parser):
+ parser.add_argument('--all', action='store_true', dest='all', default=False,
+ help='Show data not used by current backend.')
+
+ parser.add_argument('builddir', nargs='?', default='.', help='The build directory')
+
+
+def dump_compilers(compilers):
+ for lang, compiler in compilers.items():
+ print(' ' + lang + ':')
+ print(' Id: ' + compiler.id)
+ print(' Command: ' + ' '.join(compiler.exelist))
+ if compiler.full_version:
+ print(' Full version: ' + compiler.full_version)
+ if compiler.version:
+ print(' Detected version: ' + compiler.version)
+
+
+def dump_guids(d):
+ for name, value in d.items():
+ print(' ' + name + ': ' + value)
+
+
+def run(options):
+ datadir = 'meson-private'
+ if options.builddir is not None:
+ datadir = os.path.join(options.builddir, datadir)
+ if not os.path.isdir(datadir):
+ print('Current directory is not a build dir. Please specify it or '
+ 'change the working directory to it.')
+ return 1
+
+ all_backends = options.all
+
+ print('This is a dump of the internal unstable cache of meson. This is for debugging only.')
+ print('Do NOT parse, this will change from version to version in incompatible ways')
+ print('')
+
+ coredata = cdata.load(options.builddir)
+ backend = coredata.get_option(OptionKey('backend'))
+ for k, v in sorted(coredata.__dict__.items()):
+ if k in {'backend_options', 'base_options', 'builtins', 'compiler_options', 'user_options'}:
+ # use `meson configure` to view these
+ pass
+ elif k in {'install_guid', 'test_guid', 'regen_guid'}:
+ if all_backends or backend.startswith('vs'):
+ print(k + ': ' + v)
+ elif k == 'target_guids':
+ if all_backends or backend.startswith('vs'):
+ print(k + ':')
+ dump_guids(v)
+ elif k == 'lang_guids':
+ if all_backends or backend.startswith('vs') or backend == 'xcode':
+ print(k + ':')
+ dump_guids(v)
+ elif k == 'meson_command':
+ if all_backends or backend.startswith('vs'):
+ print('Meson command used in build file regeneration: ' + ' '.join(v))
+ elif k == 'pkgconf_envvar':
+ print('Last seen PKGCONFIG environment variable value: ' + v)
+ elif k == 'version':
+ print('Meson version: ' + v)
+ elif k == 'cross_files':
+ if v:
+ print('Cross File: ' + ' '.join(v))
+ elif k == 'config_files':
+ if v:
+ print('Native File: ' + ' '.join(v))
+ elif k == 'compilers':
+ for for_machine in MachineChoice:
+ print('Cached {} machine compilers:'.format(
+ for_machine.get_lower_case_name()))
+ dump_compilers(v[for_machine])
+ elif k == 'deps':
+ def print_dep(dep_key, dep):
+ print(' ' + dep_key[0][1] + ": ")
+ print(' compile args: ' + repr(dep.get_compile_args()))
+ print(' link args: ' + repr(dep.get_link_args()))
+ if dep.get_sources():
+ print(' sources: ' + repr(dep.get_sources()))
+ print(' version: ' + repr(dep.get_version()))
+
+ for for_machine in iter(MachineChoice):
+ items_list = sorted(v[for_machine].items())
+ if items_list:
+ print(f'Cached dependencies for {for_machine.get_lower_case_name()} machine')
+ for dep_key, deps in items_list:
+ for dep in deps:
+ print_dep(dep_key, dep)
+ else:
+ print(k + ':')
+ print(textwrap.indent(pprint.pformat(v), ' '))
diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py
new file mode 100644
index 0000000..549d564
--- /dev/null
+++ b/mesonbuild/optinterpreter.py
@@ -0,0 +1,225 @@
+# Copyright 2013-2014 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 re
+import typing as T
+
+from . import coredata
+from . import mesonlib
+from . import mparser
+from . import mlog
+from .interpreterbase import FeatureNew, typed_pos_args, typed_kwargs, ContainerTypeInfo, KwargInfo, permittedKwargs
+if T.TYPE_CHECKING:
+ from .interpreterbase import TYPE_var, TYPE_kwargs
+ from .interpreterbase import SubProject
+ from typing_extensions import TypedDict
+ FuncOptionArgs = TypedDict('FuncOptionArgs', {
+ 'type': str,
+ 'description': str,
+ 'yield': bool,
+ 'choices': T.Optional[T.List[str]],
+ 'value': object,
+ 'min': T.Optional[int],
+ 'max': T.Optional[int],
+ 'deprecated': T.Union[bool, str, T.Dict[str, str], T.List[str]],
+ })
+ ParserArgs = TypedDict('ParserArgs', {
+ 'yield': bool,
+ 'choices': T.Optional[T.List[str]],
+ 'value': object,
+ 'min': T.Optional[int],
+ 'max': T.Optional[int],
+ })
+
+
+class OptionException(mesonlib.MesonException):
+ pass
+
+
+optname_regex = re.compile('[^a-zA-Z0-9_-]')
+
+
+class OptionInterpreter:
+ def __init__(self, subproject: 'SubProject') -> None:
+ self.options: 'coredata.MutableKeyedOptionDictType' = {}
+ self.subproject = subproject
+ self.option_types = {'string': self.string_parser,
+ 'boolean': self.boolean_parser,
+ 'combo': self.combo_parser,
+ 'integer': self.integer_parser,
+ 'array': self.string_array_parser,
+ 'feature': self.feature_parser,
+ }
+
+ def process(self, option_file: str) -> None:
+ try:
+ with open(option_file, encoding='utf-8') as f:
+ ast = mparser.Parser(f.read(), option_file).parse()
+ except mesonlib.MesonException as me:
+ me.file = option_file
+ raise me
+ if not isinstance(ast, mparser.CodeBlockNode):
+ e = OptionException('Option file is malformed.')
+ e.lineno = ast.lineno()
+ e.file = option_file
+ raise e
+ for cur in ast.lines:
+ try:
+ self.current_node = cur
+ self.evaluate_statement(cur)
+ except mesonlib.MesonException as e:
+ e.lineno = cur.lineno
+ e.colno = cur.colno
+ e.file = option_file
+ raise e
+ except Exception as e:
+ raise mesonlib.MesonException(
+ str(e), lineno=cur.lineno, colno=cur.colno, file=option_file)
+
+ def reduce_single(self, arg: T.Union[str, mparser.BaseNode]) -> 'TYPE_var':
+ if isinstance(arg, str):
+ return arg
+ elif isinstance(arg, (mparser.StringNode, mparser.BooleanNode,
+ mparser.NumberNode)):
+ return arg.value
+ elif isinstance(arg, mparser.ArrayNode):
+ return [self.reduce_single(curarg) for curarg in arg.args.arguments]
+ elif isinstance(arg, mparser.DictNode):
+ d = {}
+ for k, v in arg.args.kwargs.items():
+ if not isinstance(k, mparser.StringNode):
+ raise OptionException('Dictionary keys must be a string literal')
+ d[k.value] = self.reduce_single(v)
+ return d
+ elif isinstance(arg, mparser.UMinusNode):
+ res = self.reduce_single(arg.value)
+ if not isinstance(res, (int, float)):
+ raise OptionException('Token after "-" is not a number')
+ FeatureNew.single_use('negative numbers in meson_options.txt', '0.54.1', self.subproject)
+ return -res
+ elif isinstance(arg, mparser.NotNode):
+ res = self.reduce_single(arg.value)
+ if not isinstance(res, bool):
+ raise OptionException('Token after "not" is not a a boolean')
+ FeatureNew.single_use('negation ("not") in meson_options.txt', '0.54.1', self.subproject)
+ return not res
+ elif isinstance(arg, mparser.ArithmeticNode):
+ l = self.reduce_single(arg.left)
+ r = self.reduce_single(arg.right)
+ if not (arg.operation == 'add' and isinstance(l, str) and isinstance(r, str)):
+ raise OptionException('Only string concatenation with the "+" operator is allowed')
+ FeatureNew.single_use('string concatenation in meson_options.txt', '0.55.0', self.subproject)
+ return l + r
+ else:
+ raise OptionException('Arguments may only be string, int, bool, or array of those.')
+
+ def reduce_arguments(self, args: mparser.ArgumentNode) -> T.Tuple['TYPE_var', 'TYPE_kwargs']:
+ if args.incorrect_order():
+ raise OptionException('All keyword arguments must be after positional arguments.')
+ reduced_pos = [self.reduce_single(arg) for arg in args.arguments]
+ reduced_kw = {}
+ for key in args.kwargs.keys():
+ if not isinstance(key, mparser.IdNode):
+ raise OptionException('Keyword argument name is not a string.')
+ a = args.kwargs[key]
+ reduced_kw[key.value] = self.reduce_single(a)
+ return reduced_pos, reduced_kw
+
+ def evaluate_statement(self, node: mparser.BaseNode) -> None:
+ if not isinstance(node, mparser.FunctionNode):
+ raise OptionException('Option file may only contain option definitions')
+ func_name = node.func_name
+ if func_name != 'option':
+ raise OptionException('Only calls to option() are allowed in option files.')
+ (posargs, kwargs) = self.reduce_arguments(node.args)
+ self.func_option(posargs, kwargs)
+
+ @typed_kwargs('option',
+ KwargInfo('type', str, required=True),
+ KwargInfo('description', str, default=''),
+ KwargInfo('yield', bool, default=coredata.default_yielding, since='0.45.0'),
+ KwargInfo('choices', (ContainerTypeInfo(list, str), type(None))),
+ KwargInfo('value', object),
+ KwargInfo('min', (int, type(None))),
+ KwargInfo('max', (int, type(None))),
+ KwargInfo('deprecated', (bool, str, ContainerTypeInfo(dict, str), ContainerTypeInfo(list, str)),
+ default=False, since='0.60.0')
+ )
+ @typed_pos_args('option', str)
+ def func_option(self, args: T.Tuple[str], kwargs: 'FuncOptionArgs') -> None:
+ opt_name = args[0]
+ if optname_regex.search(opt_name) is not None:
+ raise OptionException('Option names can only contain letters, numbers or dashes.')
+ key = mesonlib.OptionKey.from_string(opt_name).evolve(subproject=self.subproject)
+ if not key.is_project():
+ raise OptionException('Option name %s is reserved.' % opt_name)
+
+ opt_type = kwargs['type']
+ parser = self.option_types.get(opt_type)
+ if not parser:
+ raise OptionException(f'Unknown type {opt_type}.')
+ description = kwargs['description'] or opt_name
+
+ # Only keep in kwargs arguments that are used by option type's parser
+ # because they use @permittedKwargs().
+ known_parser_kwargs = {'value', 'choices', 'yield', 'min', 'max'}
+ parser_kwargs = {k: v for k, v in kwargs.items() if k in known_parser_kwargs and v is not None}
+ opt = parser(description, T.cast('ParserArgs', parser_kwargs))
+ opt.deprecated = kwargs['deprecated']
+ if isinstance(opt.deprecated, str):
+ FeatureNew.single_use('String value to "deprecated" keyword argument', '0.63.0', self.subproject)
+ if key in self.options:
+ mlog.deprecation(f'Option {opt_name} already exists.')
+ self.options[key] = opt
+
+ @permittedKwargs({'value', 'yield'})
+ def string_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption:
+ value = kwargs.get('value', '')
+ return coredata.UserStringOption(description, value, kwargs['yield'])
+
+ @permittedKwargs({'value', 'yield'})
+ def boolean_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption:
+ value = kwargs.get('value', True)
+ return coredata.UserBooleanOption(description, value, kwargs['yield'])
+
+ @permittedKwargs({'value', 'yield', 'choices'})
+ def combo_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption:
+ choices = kwargs.get('choices')
+ if not choices:
+ raise OptionException('Combo option missing "choices" keyword.')
+ value = kwargs.get('value', choices[0])
+ return coredata.UserComboOption(description, choices, value, kwargs['yield'])
+
+ @permittedKwargs({'value', 'min', 'max', 'yield'})
+ def integer_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption:
+ value = kwargs.get('value')
+ if value is None:
+ raise OptionException('Integer option must contain value argument.')
+ inttuple = (kwargs.get('min'), kwargs.get('max'), value)
+ return coredata.UserIntegerOption(description, inttuple, kwargs['yield'])
+
+ @permittedKwargs({'value', 'yield', 'choices'})
+ def string_array_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption:
+ choices = kwargs.get('choices', [])
+ value = kwargs.get('value', choices)
+ if not isinstance(value, list):
+ raise OptionException('Array choices must be passed as an array.')
+ return coredata.UserArrayOption(description, value,
+ choices=choices,
+ yielding=kwargs['yield'])
+
+ @permittedKwargs({'value', 'yield'})
+ def feature_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption:
+ value = kwargs.get('value', 'auto')
+ return coredata.UserFeatureOption(description, value, kwargs['yield'])
diff --git a/mesonbuild/programs.py b/mesonbuild/programs.py
new file mode 100644
index 0000000..64f7c29
--- /dev/null
+++ b/mesonbuild/programs.py
@@ -0,0 +1,375 @@
+# Copyright 2013-2020 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.
+from __future__ import annotations
+
+"""Representations and logic for External and Internal Programs."""
+
+import functools
+import os
+import shutil
+import stat
+import sys
+import re
+import typing as T
+from pathlib import Path
+
+from . import mesonlib
+from . import mlog
+from .mesonlib import MachineChoice
+
+if T.TYPE_CHECKING:
+ from .environment import Environment
+ from .interpreter import Interpreter
+
+
+class ExternalProgram(mesonlib.HoldableObject):
+
+ """A program that is found on the system."""
+
+ windows_exts = ('exe', 'msc', 'com', 'bat', 'cmd')
+ for_machine = MachineChoice.BUILD
+
+ def __init__(self, name: str, command: T.Optional[T.List[str]] = None,
+ silent: bool = False, search_dir: T.Optional[str] = None,
+ extra_search_dirs: T.Optional[T.List[str]] = None):
+ self.name = name
+ self.path: T.Optional[str] = None
+ self.cached_version: T.Optional[str] = None
+ if command is not None:
+ self.command = mesonlib.listify(command)
+ if mesonlib.is_windows():
+ cmd = self.command[0]
+ args = self.command[1:]
+ # Check whether the specified cmd is a path to a script, in
+ # which case we need to insert the interpreter. If not, try to
+ # use it as-is.
+ ret = self._shebang_to_cmd(cmd)
+ if ret:
+ self.command = ret + args
+ else:
+ self.command = [cmd] + args
+ else:
+ all_search_dirs = [search_dir]
+ if extra_search_dirs:
+ all_search_dirs += extra_search_dirs
+ for d in all_search_dirs:
+ self.command = self._search(name, d)
+ if self.found():
+ break
+
+ if self.found():
+ # Set path to be the last item that is actually a file (in order to
+ # skip options in something like ['python', '-u', 'file.py']. If we
+ # can't find any components, default to the last component of the path.
+ for arg in reversed(self.command):
+ if arg is not None and os.path.isfile(arg):
+ self.path = arg
+ break
+ else:
+ self.path = self.command[-1]
+
+ if not silent:
+ # ignore the warning because derived classes never call this __init__
+ # method, and thus only the found() method of this class is ever executed
+ if self.found(): # lgtm [py/init-calls-subclass]
+ mlog.log('Program', mlog.bold(name), 'found:', mlog.green('YES'),
+ '(%s)' % ' '.join(self.command))
+ else:
+ mlog.log('Program', mlog.bold(name), 'found:', mlog.red('NO'))
+
+ def summary_value(self) -> T.Union[str, mlog.AnsiDecorator]:
+ if not self.found():
+ return mlog.red('NO')
+ return self.path
+
+ def __repr__(self) -> str:
+ r = '<{} {!r} -> {!r}>'
+ return r.format(self.__class__.__name__, self.name, self.command)
+
+ def description(self) -> str:
+ '''Human friendly description of the command'''
+ return ' '.join(self.command)
+
+ def get_version(self, interpreter: T.Optional['Interpreter'] = None) -> str:
+ if not self.cached_version:
+ from . import build
+ raw_cmd = self.get_command() + ['--version']
+ if interpreter:
+ res = interpreter.run_command_impl(interpreter.current_node, (self, ['--version']),
+ {'capture': True,
+ 'check': True,
+ 'env': build.EnvironmentVariables()},
+ True)
+ o, e = res.stdout, res.stderr
+ else:
+ p, o, e = mesonlib.Popen_safe(raw_cmd)
+ if p.returncode != 0:
+ cmd_str = mesonlib.join_args(raw_cmd)
+ raise mesonlib.MesonException(f'Command {cmd_str!r} failed with status {p.returncode}.')
+ output = o.strip()
+ if not output:
+ output = e.strip()
+ match = re.search(r'([0-9][0-9\.]+)', output)
+ if not match:
+ raise mesonlib.MesonException(f'Could not find a version number in output of {raw_cmd!r}')
+ self.cached_version = match.group(1)
+ return self.cached_version
+
+ @classmethod
+ def from_bin_list(cls, env: 'Environment', for_machine: MachineChoice, name: str) -> 'ExternalProgram':
+ # There is a static `for_machine` for this class because the binary
+ # always runs on the build platform. (It's host platform is our build
+ # platform.) But some external programs have a target platform, so this
+ # is what we are specifying here.
+ command = env.lookup_binary_entry(for_machine, name)
+ if command is None:
+ return NonExistingExternalProgram()
+ return cls.from_entry(name, command)
+
+ @staticmethod
+ @functools.lru_cache(maxsize=None)
+ def _windows_sanitize_path(path: str) -> str:
+ # Ensure that we use USERPROFILE even when inside MSYS, MSYS2, Cygwin, etc.
+ if 'USERPROFILE' not in os.environ:
+ return path
+ # The WindowsApps directory is a bit of a problem. It contains
+ # some zero-sized .exe files which have "reparse points", that
+ # might either launch an installed application, or might open
+ # a page in the Windows Store to download the application.
+ #
+ # To handle the case where the python interpreter we're
+ # running on came from the Windows Store, if we see the
+ # WindowsApps path in the search path, replace it with
+ # dirname(sys.executable).
+ appstore_dir = Path(os.environ['USERPROFILE']) / 'AppData' / 'Local' / 'Microsoft' / 'WindowsApps'
+ paths = []
+ for each in path.split(os.pathsep):
+ if Path(each) != appstore_dir:
+ paths.append(each)
+ elif 'WindowsApps' in sys.executable:
+ paths.append(os.path.dirname(sys.executable))
+ return os.pathsep.join(paths)
+
+ @staticmethod
+ def from_entry(name: str, command: T.Union[str, T.List[str]]) -> 'ExternalProgram':
+ if isinstance(command, list):
+ if len(command) == 1:
+ command = command[0]
+ # We cannot do any searching if the command is a list, and we don't
+ # need to search if the path is an absolute path.
+ if isinstance(command, list) or os.path.isabs(command):
+ if isinstance(command, str):
+ command = [command]
+ return ExternalProgram(name, command=command, silent=True)
+ assert isinstance(command, str)
+ # Search for the command using the specified string!
+ return ExternalProgram(command, silent=True)
+
+ @staticmethod
+ def _shebang_to_cmd(script: str) -> T.Optional[T.List[str]]:
+ """
+ Check if the file has a shebang and manually parse it to figure out
+ the interpreter to use. This is useful if the script is not executable
+ or if we're on Windows (which does not understand shebangs).
+ """
+ try:
+ with open(script, encoding='utf-8') as f:
+ first_line = f.readline().strip()
+ if first_line.startswith('#!'):
+ # In a shebang, everything before the first space is assumed to
+ # be the command to run and everything after the first space is
+ # the single argument to pass to that command. So we must split
+ # exactly once.
+ commands = first_line[2:].split('#')[0].strip().split(maxsplit=1)
+ if mesonlib.is_windows():
+ # Windows does not have UNIX paths so remove them,
+ # but don't remove Windows paths
+ if commands[0].startswith('/'):
+ commands[0] = commands[0].split('/')[-1]
+ if len(commands) > 0 and commands[0] == 'env':
+ commands = commands[1:]
+ # Windows does not ship python3.exe, but we know the path to it
+ if len(commands) > 0 and commands[0] == 'python3':
+ commands = mesonlib.python_command + commands[1:]
+ elif mesonlib.is_haiku():
+ # Haiku does not have /usr, but a lot of scripts assume that
+ # /usr/bin/env always exists. Detect that case and run the
+ # script with the interpreter after it.
+ if commands[0] == '/usr/bin/env':
+ commands = commands[1:]
+ # We know what python3 is, we're running on it
+ if len(commands) > 0 and commands[0] == 'python3':
+ commands = mesonlib.python_command + commands[1:]
+ else:
+ # Replace python3 with the actual python3 that we are using
+ if commands[0] == '/usr/bin/env' and commands[1] == 'python3':
+ commands = mesonlib.python_command + commands[2:]
+ elif commands[0].split('/')[-1] == 'python3':
+ commands = mesonlib.python_command + commands[1:]
+ return commands + [script]
+ except Exception as e:
+ mlog.debug(str(e))
+ mlog.debug(f'Unusable script {script!r}')
+ return None
+
+ def _is_executable(self, path: str) -> bool:
+ suffix = os.path.splitext(path)[-1].lower()[1:]
+ execmask = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
+ if mesonlib.is_windows():
+ if suffix in self.windows_exts:
+ return True
+ elif os.stat(path).st_mode & execmask:
+ return not os.path.isdir(path)
+ return False
+
+ def _search_dir(self, name: str, search_dir: T.Optional[str]) -> T.Optional[list]:
+ if search_dir is None:
+ return None
+ trial = os.path.join(search_dir, name)
+ if os.path.exists(trial):
+ if self._is_executable(trial):
+ return [trial]
+ # Now getting desperate. Maybe it is a script file that is
+ # a) not chmodded executable, or
+ # b) we are on windows so they can't be directly executed.
+ return self._shebang_to_cmd(trial)
+ else:
+ if mesonlib.is_windows():
+ for ext in self.windows_exts:
+ trial_ext = f'{trial}.{ext}'
+ if os.path.exists(trial_ext):
+ return [trial_ext]
+ return None
+
+ def _search_windows_special_cases(self, name: str, command: str) -> T.List[T.Optional[str]]:
+ '''
+ Lots of weird Windows quirks:
+ 1. PATH search for @name returns files with extensions from PATHEXT,
+ but only self.windows_exts are executable without an interpreter.
+ 2. @name might be an absolute path to an executable, but without the
+ extension. This works inside MinGW so people use it a lot.
+ 3. The script is specified without an extension, in which case we have
+ to manually search in PATH.
+ 4. More special-casing for the shebang inside the script.
+ '''
+ if command:
+ # On Windows, even if the PATH search returned a full path, we can't be
+ # sure that it can be run directly if it's not a native executable.
+ # For instance, interpreted scripts sometimes need to be run explicitly
+ # with an interpreter if the file association is not done properly.
+ name_ext = os.path.splitext(command)[1]
+ if name_ext[1:].lower() in self.windows_exts:
+ # Good, it can be directly executed
+ return [command]
+ # Try to extract the interpreter from the shebang
+ commands = self._shebang_to_cmd(command)
+ if commands:
+ return commands
+ return [None]
+ # Maybe the name is an absolute path to a native Windows
+ # executable, but without the extension. This is technically wrong,
+ # but many people do it because it works in the MinGW shell.
+ if os.path.isabs(name):
+ for ext in self.windows_exts:
+ command = f'{name}.{ext}'
+ if os.path.exists(command):
+ return [command]
+ # On Windows, interpreted scripts must have an extension otherwise they
+ # cannot be found by a standard PATH search. So we do a custom search
+ # where we manually search for a script with a shebang in PATH.
+ search_dirs = self._windows_sanitize_path(os.environ.get('PATH', '')).split(';')
+ for search_dir in search_dirs:
+ commands = self._search_dir(name, search_dir)
+ if commands:
+ return commands
+ return [None]
+
+ def _search(self, name: str, search_dir: T.Optional[str]) -> T.List[T.Optional[str]]:
+ '''
+ Search in the specified dir for the specified executable by name
+ and if not found search in PATH
+ '''
+ commands = self._search_dir(name, search_dir)
+ if commands:
+ return commands
+ # If there is a directory component, do not look in PATH
+ if os.path.dirname(name) and not os.path.isabs(name):
+ return [None]
+ # Do a standard search in PATH
+ path = os.environ.get('PATH', None)
+ if mesonlib.is_windows() and path:
+ path = self._windows_sanitize_path(path)
+ command = shutil.which(name, path=path)
+ if mesonlib.is_windows():
+ return self._search_windows_special_cases(name, command)
+ # On UNIX-like platforms, shutil.which() is enough to find
+ # all executables whether in PATH or with an absolute path
+ return [command]
+
+ def found(self) -> bool:
+ return self.command[0] is not None
+
+ def get_command(self) -> T.List[str]:
+ return self.command[:]
+
+ def get_path(self) -> T.Optional[str]:
+ return self.path
+
+ def get_name(self) -> str:
+ return self.name
+
+
+class NonExistingExternalProgram(ExternalProgram): # lgtm [py/missing-call-to-init]
+ "A program that will never exist"
+
+ def __init__(self, name: str = 'nonexistingprogram') -> None:
+ self.name = name
+ self.command = [None]
+ self.path = None
+
+ def __repr__(self) -> str:
+ r = '<{} {!r} -> {!r}>'
+ return r.format(self.__class__.__name__, self.name, self.command)
+
+ def found(self) -> bool:
+ return False
+
+
+class OverrideProgram(ExternalProgram):
+
+ """A script overriding a program."""
+
+
+def find_external_program(env: 'Environment', for_machine: MachineChoice, name: str,
+ display_name: str, default_names: T.List[str],
+ allow_default_for_cross: bool = True) -> T.Generator['ExternalProgram', None, None]:
+ """Find an external program, chcking the cross file plus any default options."""
+ # Lookup in cross or machine file.
+ potential_cmd = env.lookup_binary_entry(for_machine, name)
+ if potential_cmd is not None:
+ mlog.debug(f'{display_name} binary for {for_machine} specified from cross file, native file, '
+ f'or env var as {potential_cmd}')
+ yield ExternalProgram.from_entry(name, potential_cmd)
+ # We never fallback if the user-specified option is no good, so
+ # stop returning options.
+ return
+ mlog.debug(f'{display_name} binary missing from cross or native file, or env var undefined.')
+ # Fallback on hard-coded defaults, if a default binary is allowed for use
+ # with cross targets, or if this is not a cross target
+ if allow_default_for_cross or not (for_machine is MachineChoice.HOST and env.is_cross_build(for_machine)):
+ for potential_path in default_names:
+ mlog.debug(f'Trying a default {display_name} fallback at', potential_path)
+ yield ExternalProgram(potential_path, silent=True)
+ else:
+ mlog.debug('Default target is not allowed for cross use')
diff --git a/mesonbuild/rewriter.py b/mesonbuild/rewriter.py
new file mode 100644
index 0000000..1497d93
--- /dev/null
+++ b/mesonbuild/rewriter.py
@@ -0,0 +1,1067 @@
+#!/usr/bin/env python3
+# Copyright 2016 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.
+
+# This class contains the basic functionality needed to run any interpreter
+# or an interpreter-based tool.
+
+# This tool is used to manipulate an existing Meson build definition.
+#
+# - add a file to a target
+# - remove files from a target
+# - move targets
+# - reindent?
+from __future__ import annotations
+
+from .ast import IntrospectionInterpreter, BUILD_TARGET_FUNCTIONS, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstPrinter
+from mesonbuild.mesonlib import MesonException
+from . import mlog, environment
+from functools import wraps
+from .mparser import Token, ArrayNode, ArgumentNode, AssignmentNode, BooleanNode, ElementaryNode, IdNode, FunctionNode, StringNode
+import json, os, re, sys
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .mparser import BaseNode
+
+class RewriterException(MesonException):
+ pass
+
+def add_arguments(parser, formatter=None):
+ parser.add_argument('-s', '--sourcedir', type=str, default='.', metavar='SRCDIR', help='Path to source directory.')
+ parser.add_argument('-V', '--verbose', action='store_true', default=False, help='Enable verbose output')
+ parser.add_argument('-S', '--skip-errors', dest='skip', action='store_true', default=False, help='Skip errors instead of aborting')
+ subparsers = parser.add_subparsers(dest='type', title='Rewriter commands', description='Rewrite command to execute')
+
+ # Target
+ tgt_parser = subparsers.add_parser('target', help='Modify a target', formatter_class=formatter)
+ tgt_parser.add_argument('-s', '--subdir', default='', dest='subdir', help='Subdirectory of the new target (only for the "add_target" action)')
+ tgt_parser.add_argument('--type', dest='tgt_type', choices=rewriter_keys['target']['target_type'][2], default='executable',
+ help='Type of the target to add (only for the "add_target" action)')
+ tgt_parser.add_argument('target', help='Name or ID of the target')
+ tgt_parser.add_argument('operation', choices=['add', 'rm', 'add_target', 'rm_target', 'add_extra_files', 'rm_extra_files', 'info'],
+ help='Action to execute')
+ tgt_parser.add_argument('sources', nargs='*', help='Sources to add/remove')
+
+ # KWARGS
+ kw_parser = subparsers.add_parser('kwargs', help='Modify keyword arguments', formatter_class=formatter)
+ kw_parser.add_argument('operation', choices=rewriter_keys['kwargs']['operation'][2],
+ help='Action to execute')
+ kw_parser.add_argument('function', choices=list(rewriter_func_kwargs.keys()),
+ help='Function type to modify')
+ kw_parser.add_argument('id', help='ID of the function to modify (can be anything for "project")')
+ kw_parser.add_argument('kwargs', nargs='*', help='Pairs of keyword and value')
+
+ # Default options
+ def_parser = subparsers.add_parser('default-options', help='Modify the project default options', formatter_class=formatter)
+ def_parser.add_argument('operation', choices=rewriter_keys['default_options']['operation'][2],
+ help='Action to execute')
+ def_parser.add_argument('options', nargs='*', help='Key, value pairs of configuration option')
+
+ # JSON file/command
+ cmd_parser = subparsers.add_parser('command', help='Execute a JSON array of commands', formatter_class=formatter)
+ cmd_parser.add_argument('json', help='JSON string or file to execute')
+
+class RequiredKeys:
+ def __init__(self, keys):
+ self.keys = keys
+
+ def __call__(self, f):
+ @wraps(f)
+ def wrapped(*wrapped_args, **wrapped_kwargs):
+ assert len(wrapped_args) >= 2
+ cmd = wrapped_args[1]
+ for key, val in self.keys.items():
+ typ = val[0] # The type of the value
+ default = val[1] # The default value -- None is required
+ choices = val[2] # Valid choices -- None is for everything
+ if key not in cmd:
+ if default is not None:
+ cmd[key] = default
+ else:
+ raise RewriterException('Key "{}" is missing in object for {}'
+ .format(key, f.__name__))
+ if not isinstance(cmd[key], typ):
+ raise RewriterException('Invalid type of "{}". Required is {} but provided was {}'
+ .format(key, typ.__name__, type(cmd[key]).__name__))
+ if choices is not None:
+ assert isinstance(choices, list)
+ if cmd[key] not in choices:
+ raise RewriterException('Invalid value of "{}": Possible values are {} but provided was "{}"'
+ .format(key, choices, cmd[key]))
+ return f(*wrapped_args, **wrapped_kwargs)
+
+ return wrapped
+
+class MTypeBase:
+ def __init__(self, node: T.Optional[BaseNode] = None):
+ if node is None:
+ self.node = self._new_node() # lgtm [py/init-calls-subclass] (node creation does not depend on base class state)
+ else:
+ self.node = node
+ self.node_type = None
+ for i in self.supported_nodes(): # lgtm [py/init-calls-subclass] (listing nodes does not depend on base class state)
+ if isinstance(self.node, i):
+ self.node_type = i
+
+ def _new_node(self):
+ # Overwrite in derived class
+ raise RewriterException('Internal error: _new_node of MTypeBase was called')
+
+ def can_modify(self):
+ return self.node_type is not None
+
+ def get_node(self):
+ return self.node
+
+ def supported_nodes(self):
+ # Overwrite in derived class
+ return []
+
+ def set_value(self, value):
+ # Overwrite in derived class
+ mlog.warning('Cannot set the value of type', mlog.bold(type(self).__name__), '--> skipping')
+
+ def add_value(self, value):
+ # Overwrite in derived class
+ mlog.warning('Cannot add a value of type', mlog.bold(type(self).__name__), '--> skipping')
+
+ def remove_value(self, value):
+ # Overwrite in derived class
+ mlog.warning('Cannot remove a value of type', mlog.bold(type(self).__name__), '--> skipping')
+
+ def remove_regex(self, value):
+ # Overwrite in derived class
+ mlog.warning('Cannot remove a regex in type', mlog.bold(type(self).__name__), '--> skipping')
+
+class MTypeStr(MTypeBase):
+ def __init__(self, node: T.Optional[BaseNode] = None):
+ super().__init__(node)
+
+ def _new_node(self):
+ return StringNode(Token('', '', 0, 0, 0, None, ''))
+
+ def supported_nodes(self):
+ return [StringNode]
+
+ def set_value(self, value):
+ self.node.value = str(value)
+
+class MTypeBool(MTypeBase):
+ def __init__(self, node: T.Optional[BaseNode] = None):
+ super().__init__(node)
+
+ def _new_node(self):
+ return BooleanNode(Token('', '', 0, 0, 0, None, False))
+
+ def supported_nodes(self):
+ return [BooleanNode]
+
+ def set_value(self, value):
+ self.node.value = bool(value)
+
+class MTypeID(MTypeBase):
+ def __init__(self, node: T.Optional[BaseNode] = None):
+ super().__init__(node)
+
+ def _new_node(self):
+ return IdNode(Token('', '', 0, 0, 0, None, ''))
+
+ def supported_nodes(self):
+ return [IdNode]
+
+ def set_value(self, value):
+ self.node.value = str(value)
+
+class MTypeList(MTypeBase):
+ def __init__(self, node: T.Optional[BaseNode] = None):
+ super().__init__(node)
+
+ def _new_node(self):
+ return ArrayNode(ArgumentNode(Token('', '', 0, 0, 0, None, '')), 0, 0, 0, 0)
+
+ def _new_element_node(self, value):
+ # Overwrite in derived class
+ raise RewriterException('Internal error: _new_element_node of MTypeList was called')
+
+ def _ensure_array_node(self):
+ if not isinstance(self.node, ArrayNode):
+ tmp = self.node
+ self.node = self._new_node()
+ self.node.args.arguments += [tmp]
+
+ def _check_is_equal(self, node, value) -> bool:
+ # Overwrite in derived class
+ return False
+
+ def _check_regex_matches(self, node, regex: str) -> bool:
+ # Overwrite in derived class
+ return False
+
+ def get_node(self):
+ if isinstance(self.node, ArrayNode):
+ if len(self.node.args.arguments) == 1:
+ return self.node.args.arguments[0]
+ return self.node
+
+ def supported_element_nodes(self):
+ # Overwrite in derived class
+ return []
+
+ def supported_nodes(self):
+ return [ArrayNode] + self.supported_element_nodes()
+
+ def set_value(self, value):
+ if not isinstance(value, list):
+ value = [value]
+ self._ensure_array_node()
+ self.node.args.arguments = [] # Remove all current nodes
+ for i in value:
+ self.node.args.arguments += [self._new_element_node(i)]
+
+ def add_value(self, value):
+ if not isinstance(value, list):
+ value = [value]
+ self._ensure_array_node()
+ for i in value:
+ self.node.args.arguments += [self._new_element_node(i)]
+
+ def _remove_helper(self, value, equal_func):
+ def check_remove_node(node):
+ for j in value:
+ if equal_func(i, j):
+ return True
+ return False
+
+ if not isinstance(value, list):
+ value = [value]
+ self._ensure_array_node()
+ removed_list = []
+ for i in self.node.args.arguments:
+ if not check_remove_node(i):
+ removed_list += [i]
+ self.node.args.arguments = removed_list
+
+ def remove_value(self, value):
+ self._remove_helper(value, self._check_is_equal)
+
+ def remove_regex(self, regex: str):
+ self._remove_helper(regex, self._check_regex_matches)
+
+class MTypeStrList(MTypeList):
+ def __init__(self, node: T.Optional[BaseNode] = None):
+ super().__init__(node)
+
+ def _new_element_node(self, value):
+ return StringNode(Token('', '', 0, 0, 0, None, str(value)))
+
+ def _check_is_equal(self, node, value) -> bool:
+ if isinstance(node, StringNode):
+ return node.value == value
+ return False
+
+ def _check_regex_matches(self, node, regex: str) -> bool:
+ if isinstance(node, StringNode):
+ return re.match(regex, node.value) is not None
+ return False
+
+ def supported_element_nodes(self):
+ return [StringNode]
+
+class MTypeIDList(MTypeList):
+ def __init__(self, node: T.Optional[BaseNode] = None):
+ super().__init__(node)
+
+ def _new_element_node(self, value):
+ return IdNode(Token('', '', 0, 0, 0, None, str(value)))
+
+ def _check_is_equal(self, node, value) -> bool:
+ if isinstance(node, IdNode):
+ return node.value == value
+ return False
+
+ def _check_regex_matches(self, node, regex: str) -> bool:
+ if isinstance(node, StringNode):
+ return re.match(regex, node.value) is not None
+ return False
+
+ def supported_element_nodes(self):
+ return [IdNode]
+
+rewriter_keys = {
+ 'default_options': {
+ 'operation': (str, None, ['set', 'delete']),
+ 'options': (dict, {}, None)
+ },
+ 'kwargs': {
+ 'function': (str, None, None),
+ 'id': (str, None, None),
+ 'operation': (str, None, ['set', 'delete', 'add', 'remove', 'remove_regex', 'info']),
+ 'kwargs': (dict, {}, None)
+ },
+ 'target': {
+ 'target': (str, None, None),
+ 'operation': (str, None, ['src_add', 'src_rm', 'target_rm', 'target_add', 'extra_files_add', 'extra_files_rm', 'info']),
+ 'sources': (list, [], None),
+ 'subdir': (str, '', None),
+ 'target_type': (str, 'executable', ['both_libraries', 'executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library']),
+ }
+}
+
+rewriter_func_kwargs = {
+ 'dependency': {
+ 'language': MTypeStr,
+ 'method': MTypeStr,
+ 'native': MTypeBool,
+ 'not_found_message': MTypeStr,
+ 'required': MTypeBool,
+ 'static': MTypeBool,
+ 'version': MTypeStrList,
+ 'modules': MTypeStrList
+ },
+ 'target': {
+ 'build_by_default': MTypeBool,
+ 'build_rpath': MTypeStr,
+ 'dependencies': MTypeIDList,
+ 'gui_app': MTypeBool,
+ 'link_with': MTypeIDList,
+ 'export_dynamic': MTypeBool,
+ 'implib': MTypeBool,
+ 'install': MTypeBool,
+ 'install_dir': MTypeStr,
+ 'install_rpath': MTypeStr,
+ 'pie': MTypeBool
+ },
+ 'project': {
+ 'default_options': MTypeStrList,
+ 'meson_version': MTypeStr,
+ 'license': MTypeStrList,
+ 'subproject_dir': MTypeStr,
+ 'version': MTypeStr
+ }
+}
+
+class Rewriter:
+ def __init__(self, sourcedir: str, generator: str = 'ninja', skip_errors: bool = False):
+ self.sourcedir = sourcedir
+ self.interpreter = IntrospectionInterpreter(sourcedir, '', generator, visitors = [AstIDGenerator(), AstIndentationGenerator(), AstConditionLevel()])
+ self.skip_errors = skip_errors
+ self.modified_nodes = []
+ self.to_remove_nodes = []
+ self.to_add_nodes = []
+ self.functions = {
+ 'default_options': self.process_default_options,
+ 'kwargs': self.process_kwargs,
+ 'target': self.process_target,
+ }
+ self.info_dump = None
+
+ def analyze_meson(self):
+ mlog.log('Analyzing meson file:', mlog.bold(os.path.join(self.sourcedir, environment.build_filename)))
+ self.interpreter.analyze()
+ mlog.log(' -- Project:', mlog.bold(self.interpreter.project_data['descriptive_name']))
+ mlog.log(' -- Version:', mlog.cyan(self.interpreter.project_data['version']))
+
+ def add_info(self, cmd_type: str, cmd_id: str, data: dict):
+ if self.info_dump is None:
+ self.info_dump = {}
+ if cmd_type not in self.info_dump:
+ self.info_dump[cmd_type] = {}
+ self.info_dump[cmd_type][cmd_id] = data
+
+ def print_info(self):
+ if self.info_dump is None:
+ return
+ sys.stderr.write(json.dumps(self.info_dump, indent=2))
+
+ def on_error(self):
+ if self.skip_errors:
+ return mlog.cyan('-->'), mlog.yellow('skipping')
+ return mlog.cyan('-->'), mlog.red('aborting')
+
+ def handle_error(self):
+ if self.skip_errors:
+ return None
+ raise MesonException('Rewriting the meson.build failed')
+
+ def find_target(self, target: str):
+ def check_list(name: str) -> T.List[BaseNode]:
+ result = []
+ for i in self.interpreter.targets:
+ if name in {i['name'], i['id']}:
+ result += [i]
+ return result
+
+ targets = check_list(target)
+ if targets:
+ if len(targets) == 1:
+ return targets[0]
+ else:
+ mlog.error('There are multiple targets matching', mlog.bold(target))
+ for i in targets:
+ mlog.error(' -- Target name', mlog.bold(i['name']), 'with ID', mlog.bold(i['id']))
+ mlog.error('Please try again with the unique ID of the target', *self.on_error())
+ self.handle_error()
+ return None
+
+ # Check the assignments
+ tgt = None
+ if target in self.interpreter.assignments:
+ node = self.interpreter.assignments[target]
+ if isinstance(node, FunctionNode):
+ if node.func_name in {'executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library', 'both_libraries'}:
+ tgt = self.interpreter.assign_vals[target]
+
+ return tgt
+
+ def find_dependency(self, dependency: str):
+ def check_list(name: str):
+ for i in self.interpreter.dependencies:
+ if name == i['name']:
+ return i
+ return None
+
+ dep = check_list(dependency)
+ if dep is not None:
+ return dep
+
+ # Check the assignments
+ if dependency in self.interpreter.assignments:
+ node = self.interpreter.assignments[dependency]
+ if isinstance(node, FunctionNode):
+ if node.func_name == 'dependency':
+ name = self.interpreter.flatten_args(node.args)[0]
+ dep = check_list(name)
+
+ return dep
+
+ @RequiredKeys(rewriter_keys['default_options'])
+ def process_default_options(self, cmd):
+ # First, remove the old values
+ kwargs_cmd = {
+ 'function': 'project',
+ 'id': "/",
+ 'operation': 'remove_regex',
+ 'kwargs': {
+ 'default_options': [f'{x}=.*' for x in cmd['options'].keys()]
+ }
+ }
+ self.process_kwargs(kwargs_cmd)
+
+ # Then add the new values
+ if cmd['operation'] != 'set':
+ return
+
+ kwargs_cmd['operation'] = 'add'
+ kwargs_cmd['kwargs']['default_options'] = []
+
+ cdata = self.interpreter.coredata
+ options = {
+ **{str(k): v for k, v in cdata.options.items()},
+ **{str(k): v for k, v in cdata.options.items()},
+ **{str(k): v for k, v in cdata.options.items()},
+ **{str(k): v for k, v in cdata.options.items()},
+ **{str(k): v for k, v in cdata.options.items()},
+ }
+
+ for key, val in sorted(cmd['options'].items()):
+ if key not in options:
+ mlog.error('Unknown options', mlog.bold(key), *self.on_error())
+ self.handle_error()
+ continue
+
+ try:
+ val = options[key].validate_value(val)
+ except MesonException as e:
+ mlog.error('Unable to set', mlog.bold(key), mlog.red(str(e)), *self.on_error())
+ self.handle_error()
+ continue
+
+ kwargs_cmd['kwargs']['default_options'] += [f'{key}={val}']
+
+ self.process_kwargs(kwargs_cmd)
+
+ @RequiredKeys(rewriter_keys['kwargs'])
+ def process_kwargs(self, cmd):
+ mlog.log('Processing function type', mlog.bold(cmd['function']), 'with id', mlog.cyan("'" + cmd['id'] + "'"))
+ if cmd['function'] not in rewriter_func_kwargs:
+ mlog.error('Unknown function type', cmd['function'], *self.on_error())
+ return self.handle_error()
+ kwargs_def = rewriter_func_kwargs[cmd['function']]
+
+ # Find the function node to modify
+ node = None
+ arg_node = None
+ if cmd['function'] == 'project':
+ # msys bash may expand '/' to a path. It will mangle '//' to '/'
+ # but in order to keep usage shell-agnostic, also allow `//` as
+ # the function ID such that it will work in both msys bash and
+ # other shells.
+ if {'/', '//'}.isdisjoint({cmd['id']}):
+ mlog.error('The ID for the function type project must be "/" or "//" not "' + cmd['id'] + '"', *self.on_error())
+ return self.handle_error()
+ node = self.interpreter.project_node
+ arg_node = node.args
+ elif cmd['function'] == 'target':
+ tmp = self.find_target(cmd['id'])
+ if tmp:
+ node = tmp['node']
+ arg_node = node.args
+ elif cmd['function'] == 'dependency':
+ tmp = self.find_dependency(cmd['id'])
+ if tmp:
+ node = tmp['node']
+ arg_node = node.args
+ if not node:
+ mlog.error('Unable to find the function node')
+ assert isinstance(node, FunctionNode)
+ assert isinstance(arg_node, ArgumentNode)
+ # Transform the key nodes to plain strings
+ arg_node.kwargs = {k.value: v for k, v in arg_node.kwargs.items()}
+
+ # Print kwargs info
+ if cmd['operation'] == 'info':
+ info_data = {}
+ for key, val in sorted(arg_node.kwargs.items()):
+ info_data[key] = None
+ if isinstance(val, ElementaryNode):
+ info_data[key] = val.value
+ elif isinstance(val, ArrayNode):
+ data_list = []
+ for i in val.args.arguments:
+ element = None
+ if isinstance(i, ElementaryNode):
+ element = i.value
+ data_list += [element]
+ info_data[key] = data_list
+
+ self.add_info('kwargs', '{}#{}'.format(cmd['function'], cmd['id']), info_data)
+ return # Nothing else to do
+
+ # Modify the kwargs
+ num_changed = 0
+ for key, val in sorted(cmd['kwargs'].items()):
+ if key not in kwargs_def:
+ mlog.error('Cannot modify unknown kwarg', mlog.bold(key), *self.on_error())
+ self.handle_error()
+ continue
+
+ # Remove the key from the kwargs
+ if cmd['operation'] == 'delete':
+ if key in arg_node.kwargs:
+ mlog.log(' -- Deleting', mlog.bold(key), 'from the kwargs')
+ del arg_node.kwargs[key]
+ num_changed += 1
+ else:
+ mlog.log(' -- Key', mlog.bold(key), 'is already deleted')
+ continue
+
+ if key not in arg_node.kwargs:
+ arg_node.kwargs[key] = None
+ modifyer = kwargs_def[key](arg_node.kwargs[key])
+ if not modifyer.can_modify():
+ mlog.log(' -- Skipping', mlog.bold(key), 'because it is to complex to modify')
+
+ # Apply the operation
+ val_str = str(val)
+ if cmd['operation'] == 'set':
+ mlog.log(' -- Setting', mlog.bold(key), 'to', mlog.yellow(val_str))
+ modifyer.set_value(val)
+ elif cmd['operation'] == 'add':
+ mlog.log(' -- Adding', mlog.yellow(val_str), 'to', mlog.bold(key))
+ modifyer.add_value(val)
+ elif cmd['operation'] == 'remove':
+ mlog.log(' -- Removing', mlog.yellow(val_str), 'from', mlog.bold(key))
+ modifyer.remove_value(val)
+ elif cmd['operation'] == 'remove_regex':
+ mlog.log(' -- Removing all values matching', mlog.yellow(val_str), 'from', mlog.bold(key))
+ modifyer.remove_regex(val)
+
+ # Write back the result
+ arg_node.kwargs[key] = modifyer.get_node()
+ num_changed += 1
+
+ # Convert the keys back to IdNode's
+ arg_node.kwargs = {IdNode(Token('', '', 0, 0, 0, None, k)): v for k, v in arg_node.kwargs.items()}
+ if num_changed > 0 and node not in self.modified_nodes:
+ self.modified_nodes += [node]
+
+ def find_assignment_node(self, node: BaseNode) -> AssignmentNode:
+ if node.ast_id and node.ast_id in self.interpreter.reverse_assignment:
+ return self.interpreter.reverse_assignment[node.ast_id]
+ return None
+
+ @RequiredKeys(rewriter_keys['target'])
+ def process_target(self, cmd):
+ mlog.log('Processing target', mlog.bold(cmd['target']), 'operation', mlog.cyan(cmd['operation']))
+ target = self.find_target(cmd['target'])
+ if target is None and cmd['operation'] != 'target_add':
+ mlog.error('Unknown target', mlog.bold(cmd['target']), *self.on_error())
+ return self.handle_error()
+
+ # Make source paths relative to the current subdir
+ def rel_source(src: str) -> str:
+ subdir = os.path.abspath(os.path.join(self.sourcedir, target['subdir']))
+ if os.path.isabs(src):
+ return os.path.relpath(src, subdir)
+ elif not os.path.exists(src):
+ return src # Trust the user when the source doesn't exist
+ # Make sure that the path is relative to the subdir
+ return os.path.relpath(os.path.abspath(src), subdir)
+
+ if target is not None:
+ cmd['sources'] = [rel_source(x) for x in cmd['sources']]
+
+ # Utility function to get a list of the sources from a node
+ def arg_list_from_node(n):
+ args = []
+ if isinstance(n, FunctionNode):
+ args = list(n.args.arguments)
+ if n.func_name in BUILD_TARGET_FUNCTIONS:
+ args.pop(0)
+ elif isinstance(n, ArrayNode):
+ args = n.args.arguments
+ elif isinstance(n, ArgumentNode):
+ args = n.arguments
+ return args
+
+ to_sort_nodes = []
+
+ if cmd['operation'] == 'src_add':
+ node = None
+ if target['sources']:
+ node = target['sources'][0]
+ else:
+ node = target['node']
+ assert node is not None
+
+ # Generate the current source list
+ src_list = []
+ for i in target['sources']:
+ for j in arg_list_from_node(i):
+ if isinstance(j, StringNode):
+ src_list += [j.value]
+
+ # Generate the new String nodes
+ to_append = []
+ for i in sorted(set(cmd['sources'])):
+ if i in src_list:
+ mlog.log(' -- Source', mlog.green(i), 'is already defined for the target --> skipping')
+ continue
+ mlog.log(' -- Adding source', mlog.green(i), 'at',
+ mlog.yellow(f'{node.filename}:{node.lineno}'))
+ token = Token('string', node.filename, 0, 0, 0, None, i)
+ to_append += [StringNode(token)]
+
+ # Append to the AST at the right place
+ arg_node = None
+ if isinstance(node, (FunctionNode, ArrayNode)):
+ arg_node = node.args
+ elif isinstance(node, ArgumentNode):
+ arg_node = node
+ assert arg_node is not None
+ arg_node.arguments += to_append
+
+ # Mark the node as modified
+ if arg_node not in to_sort_nodes and not isinstance(node, FunctionNode):
+ to_sort_nodes += [arg_node]
+ if node not in self.modified_nodes:
+ self.modified_nodes += [node]
+
+ elif cmd['operation'] == 'src_rm':
+ # Helper to find the exact string node and its parent
+ def find_node(src):
+ for i in target['sources']:
+ for j in arg_list_from_node(i):
+ if isinstance(j, StringNode):
+ if j.value == src:
+ return i, j
+ return None, None
+
+ for i in cmd['sources']:
+ # Try to find the node with the source string
+ root, string_node = find_node(i)
+ if root is None:
+ mlog.warning(' -- Unable to find source', mlog.green(i), 'in the target')
+ continue
+
+ # Remove the found string node from the argument list
+ arg_node = None
+ if isinstance(root, (FunctionNode, ArrayNode)):
+ arg_node = root.args
+ elif isinstance(root, ArgumentNode):
+ arg_node = root
+ assert arg_node is not None
+ mlog.log(' -- Removing source', mlog.green(i), 'from',
+ mlog.yellow(f'{string_node.filename}:{string_node.lineno}'))
+ arg_node.arguments.remove(string_node)
+
+ # Mark the node as modified
+ if arg_node not in to_sort_nodes and not isinstance(root, FunctionNode):
+ to_sort_nodes += [arg_node]
+ if root not in self.modified_nodes:
+ self.modified_nodes += [root]
+
+ elif cmd['operation'] == 'extra_files_add':
+ tgt_function: FunctionNode = target['node']
+ mark_array = True
+ try:
+ node = target['extra_files'][0]
+ except IndexError:
+ # Specifying `extra_files` with a list that flattens to empty gives an empty
+ # target['extra_files'] list, account for that.
+ try:
+ extra_files_key = next(k for k in tgt_function.args.kwargs.keys() if isinstance(k, IdNode) and k.value == 'extra_files')
+ node = tgt_function.args.kwargs[extra_files_key]
+ except StopIteration:
+ # Target has no extra_files kwarg, create one
+ node = ArrayNode(ArgumentNode(Token('', tgt_function.filename, 0, 0, 0, None, '[]')), tgt_function.end_lineno, tgt_function.end_colno, tgt_function.end_lineno, tgt_function.end_colno)
+ tgt_function.args.kwargs[IdNode(Token('string', tgt_function.filename, 0, 0, 0, None, 'extra_files'))] = node
+ mark_array = False
+ if tgt_function not in self.modified_nodes:
+ self.modified_nodes += [tgt_function]
+ target['extra_files'] = [node]
+ if isinstance(node, IdNode):
+ node = self.interpreter.assignments[node.value]
+ target['extra_files'] = [node]
+ if not isinstance(node, ArrayNode):
+ mlog.error('Target', mlog.bold(cmd['target']), 'extra_files argument must be a list', *self.on_error())
+ return self.handle_error()
+
+ # Generate the current extra files list
+ extra_files_list = []
+ for i in target['extra_files']:
+ for j in arg_list_from_node(i):
+ if isinstance(j, StringNode):
+ extra_files_list += [j.value]
+
+ # Generate the new String nodes
+ to_append = []
+ for i in sorted(set(cmd['sources'])):
+ if i in extra_files_list:
+ mlog.log(' -- Extra file', mlog.green(i), 'is already defined for the target --> skipping')
+ continue
+ mlog.log(' -- Adding extra file', mlog.green(i), 'at',
+ mlog.yellow(f'{node.filename}:{node.lineno}'))
+ token = Token('string', node.filename, 0, 0, 0, None, i)
+ to_append += [StringNode(token)]
+
+ # Append to the AST at the right place
+ arg_node = node.args
+ arg_node.arguments += to_append
+
+ # Mark the node as modified
+ if arg_node not in to_sort_nodes:
+ to_sort_nodes += [arg_node]
+ # If the extra_files array is newly created, don't mark it as its parent function node already is,
+ # otherwise this would cause double modification.
+ if mark_array and node not in self.modified_nodes:
+ self.modified_nodes += [node]
+
+ elif cmd['operation'] == 'extra_files_rm':
+ # Helper to find the exact string node and its parent
+ def find_node(src):
+ for i in target['extra_files']:
+ for j in arg_list_from_node(i):
+ if isinstance(j, StringNode):
+ if j.value == src:
+ return i, j
+ return None, None
+
+ for i in cmd['sources']:
+ # Try to find the node with the source string
+ root, string_node = find_node(i)
+ if root is None:
+ mlog.warning(' -- Unable to find extra file', mlog.green(i), 'in the target')
+ continue
+
+ # Remove the found string node from the argument list
+ arg_node = root.args
+ mlog.log(' -- Removing extra file', mlog.green(i), 'from',
+ mlog.yellow(f'{string_node.filename}:{string_node.lineno}'))
+ arg_node.arguments.remove(string_node)
+
+ # Mark the node as modified
+ if arg_node not in to_sort_nodes and not isinstance(root, FunctionNode):
+ to_sort_nodes += [arg_node]
+ if root not in self.modified_nodes:
+ self.modified_nodes += [root]
+
+ elif cmd['operation'] == 'target_add':
+ if target is not None:
+ mlog.error('Can not add target', mlog.bold(cmd['target']), 'because it already exists', *self.on_error())
+ return self.handle_error()
+
+ id_base = re.sub(r'[- ]', '_', cmd['target'])
+ target_id = id_base + '_exe' if cmd['target_type'] == 'executable' else '_lib'
+ source_id = id_base + '_sources'
+ filename = os.path.join(cmd['subdir'], environment.build_filename)
+
+ # Build src list
+ src_arg_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, ''))
+ src_arr_node = ArrayNode(src_arg_node, 0, 0, 0, 0)
+ src_far_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, ''))
+ src_fun_node = FunctionNode(filename, 0, 0, 0, 0, 'files', src_far_node)
+ src_ass_node = AssignmentNode(filename, 0, 0, source_id, src_fun_node)
+ src_arg_node.arguments = [StringNode(Token('string', filename, 0, 0, 0, None, x)) for x in cmd['sources']]
+ src_far_node.arguments = [src_arr_node]
+
+ # Build target
+ tgt_arg_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, ''))
+ tgt_fun_node = FunctionNode(filename, 0, 0, 0, 0, cmd['target_type'], tgt_arg_node)
+ tgt_ass_node = AssignmentNode(filename, 0, 0, target_id, tgt_fun_node)
+ tgt_arg_node.arguments = [
+ StringNode(Token('string', filename, 0, 0, 0, None, cmd['target'])),
+ IdNode(Token('string', filename, 0, 0, 0, None, source_id))
+ ]
+
+ src_ass_node.accept(AstIndentationGenerator())
+ tgt_ass_node.accept(AstIndentationGenerator())
+ self.to_add_nodes += [src_ass_node, tgt_ass_node]
+
+ elif cmd['operation'] == 'target_rm':
+ to_remove = self.find_assignment_node(target['node'])
+ if to_remove is None:
+ to_remove = target['node']
+ self.to_remove_nodes += [to_remove]
+ mlog.log(' -- Removing target', mlog.green(cmd['target']), 'at',
+ mlog.yellow(f'{to_remove.filename}:{to_remove.lineno}'))
+
+ elif cmd['operation'] == 'info':
+ # T.List all sources in the target
+ src_list = []
+ for i in target['sources']:
+ for j in arg_list_from_node(i):
+ if isinstance(j, StringNode):
+ src_list += [j.value]
+ extra_files_list = []
+ for i in target['extra_files']:
+ for j in arg_list_from_node(i):
+ if isinstance(j, StringNode):
+ extra_files_list += [j.value]
+ test_data = {
+ 'name': target['name'],
+ 'sources': src_list,
+ 'extra_files': extra_files_list
+ }
+ self.add_info('target', target['id'], test_data)
+
+ # Sort files
+ for i in to_sort_nodes:
+ convert = lambda text: int(text) if text.isdigit() else text.lower()
+ alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
+ path_sorter = lambda key: ([(key.count('/') <= idx, alphanum_key(x)) for idx, x in enumerate(key.split('/'))])
+
+ unknown = [x for x in i.arguments if not isinstance(x, StringNode)]
+ sources = [x for x in i.arguments if isinstance(x, StringNode)]
+ sources = sorted(sources, key=lambda x: path_sorter(x.value))
+ i.arguments = unknown + sources
+
+ def process(self, cmd):
+ if 'type' not in cmd:
+ raise RewriterException('Command has no key "type"')
+ if cmd['type'] not in self.functions:
+ raise RewriterException('Unknown command "{}". Supported commands are: {}'
+ .format(cmd['type'], list(self.functions.keys())))
+ self.functions[cmd['type']](cmd)
+
+ def apply_changes(self):
+ assert all(hasattr(x, 'lineno') and hasattr(x, 'colno') and hasattr(x, 'filename') for x in self.modified_nodes)
+ assert all(hasattr(x, 'lineno') and hasattr(x, 'colno') and hasattr(x, 'filename') for x in self.to_remove_nodes)
+ assert all(isinstance(x, (ArrayNode, FunctionNode)) for x in self.modified_nodes)
+ assert all(isinstance(x, (ArrayNode, AssignmentNode, FunctionNode)) for x in self.to_remove_nodes)
+ # Sort based on line and column in reversed order
+ work_nodes = [{'node': x, 'action': 'modify'} for x in self.modified_nodes]
+ work_nodes += [{'node': x, 'action': 'rm'} for x in self.to_remove_nodes]
+ work_nodes = sorted(work_nodes, key=lambda x: (x['node'].lineno, x['node'].colno), reverse=True)
+ work_nodes += [{'node': x, 'action': 'add'} for x in self.to_add_nodes]
+
+ # Generating the new replacement string
+ str_list = []
+ for i in work_nodes:
+ new_data = ''
+ if i['action'] == 'modify' or i['action'] == 'add':
+ printer = AstPrinter()
+ i['node'].accept(printer)
+ printer.post_process()
+ new_data = printer.result.strip()
+ data = {
+ 'file': i['node'].filename,
+ 'str': new_data,
+ 'node': i['node'],
+ 'action': i['action']
+ }
+ str_list += [data]
+
+ # Load build files
+ files = {}
+ for i in str_list:
+ if i['file'] in files:
+ continue
+ fpath = os.path.realpath(os.path.join(self.sourcedir, i['file']))
+ fdata = ''
+ # Create an empty file if it does not exist
+ if not os.path.exists(fpath):
+ with open(fpath, 'w', encoding='utf-8'):
+ pass
+ with open(fpath, encoding='utf-8') as fp:
+ fdata = fp.read()
+
+ # Generate line offsets numbers
+ m_lines = fdata.splitlines(True)
+ offset = 0
+ line_offsets = []
+ for j in m_lines:
+ line_offsets += [offset]
+ offset += len(j)
+
+ files[i['file']] = {
+ 'path': fpath,
+ 'raw': fdata,
+ 'offsets': line_offsets
+ }
+
+ # Replace in source code
+ def remove_node(i):
+ offsets = files[i['file']]['offsets']
+ raw = files[i['file']]['raw']
+ node = i['node']
+ line = node.lineno - 1
+ col = node.colno
+ start = offsets[line] + col
+ end = start
+ if isinstance(node, (ArrayNode, FunctionNode)):
+ end = offsets[node.end_lineno - 1] + node.end_colno
+
+ # Only removal is supported for assignments
+ elif isinstance(node, AssignmentNode) and i['action'] == 'rm':
+ if isinstance(node.value, (ArrayNode, FunctionNode)):
+ remove_node({'file': i['file'], 'str': '', 'node': node.value, 'action': 'rm'})
+ raw = files[i['file']]['raw']
+ while raw[end] != '=':
+ end += 1
+ end += 1 # Handle the '='
+ while raw[end] in {' ', '\n', '\t'}:
+ end += 1
+
+ files[i['file']]['raw'] = raw[:start] + i['str'] + raw[end:]
+
+ for i in str_list:
+ if i['action'] in {'modify', 'rm'}:
+ remove_node(i)
+ elif i['action'] == 'add':
+ files[i['file']]['raw'] += i['str'] + '\n'
+
+ # Write the files back
+ for key, val in files.items():
+ mlog.log('Rewriting', mlog.yellow(key))
+ with open(val['path'], 'w', encoding='utf-8') as fp:
+ fp.write(val['raw'])
+
+target_operation_map = {
+ 'add': 'src_add',
+ 'rm': 'src_rm',
+ 'add_target': 'target_add',
+ 'rm_target': 'target_rm',
+ 'add_extra_files': 'extra_files_add',
+ 'rm_extra_files': 'extra_files_rm',
+ 'info': 'info',
+}
+
+def list_to_dict(in_list: T.List[str]) -> T.Dict[str, str]:
+ result = {}
+ it = iter(in_list)
+ try:
+ for i in it:
+ # calling next(it) is not a mistake, we're taking the next element from
+ # the iterator, avoiding the need to preprocess it into a sequence of
+ # key value pairs.
+ result[i] = next(it)
+ except StopIteration:
+ raise TypeError('in_list parameter of list_to_dict must have an even length.')
+ return result
+
+def generate_target(options) -> T.List[dict]:
+ return [{
+ 'type': 'target',
+ 'target': options.target,
+ 'operation': target_operation_map[options.operation],
+ 'sources': options.sources,
+ 'subdir': options.subdir,
+ 'target_type': options.tgt_type,
+ }]
+
+def generate_kwargs(options) -> T.List[dict]:
+ return [{
+ 'type': 'kwargs',
+ 'function': options.function,
+ 'id': options.id,
+ 'operation': options.operation,
+ 'kwargs': list_to_dict(options.kwargs),
+ }]
+
+def generate_def_opts(options) -> T.List[dict]:
+ return [{
+ 'type': 'default_options',
+ 'operation': options.operation,
+ 'options': list_to_dict(options.options),
+ }]
+
+def generate_cmd(options) -> T.List[dict]:
+ if os.path.exists(options.json):
+ with open(options.json, encoding='utf-8') as fp:
+ return json.load(fp)
+ else:
+ return json.loads(options.json)
+
+# Map options.type to the actual type name
+cli_type_map = {
+ 'target': generate_target,
+ 'tgt': generate_target,
+ 'kwargs': generate_kwargs,
+ 'default-options': generate_def_opts,
+ 'def': generate_def_opts,
+ 'command': generate_cmd,
+ 'cmd': generate_cmd,
+}
+
+def run(options):
+ if not options.verbose:
+ mlog.set_quiet()
+
+ try:
+ rewriter = Rewriter(options.sourcedir, skip_errors=options.skip)
+ rewriter.analyze_meson()
+
+ if options.type is None:
+ mlog.error('No command specified')
+ return 1
+
+ commands = cli_type_map[options.type](options)
+
+ if not isinstance(commands, list):
+ raise TypeError('Command is not a list')
+
+ for i in commands:
+ if not isinstance(i, object):
+ raise TypeError('Command is not an object')
+ rewriter.process(i)
+
+ rewriter.apply_changes()
+ rewriter.print_info()
+ return 0
+ except Exception as e:
+ raise e
+ finally:
+ mlog.set_verbose()
diff --git a/mesonbuild/scripts/__init__.py b/mesonbuild/scripts/__init__.py
new file mode 100644
index 0000000..7277771
--- /dev/null
+++ b/mesonbuild/scripts/__init__.py
@@ -0,0 +1,21 @@
+# Copyright 2016 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.
+
+from pathlib import PurePath
+
+def destdir_join(d1: str, d2: str) -> str:
+ if not d1:
+ return d2
+ # c:\destdir + c:\prefix must produce c:\destdir\prefix
+ return str(PurePath(d1, *PurePath(d2).parts[1:]))
diff --git a/mesonbuild/scripts/clangformat.py b/mesonbuild/scripts/clangformat.py
new file mode 100644
index 0000000..a706b76
--- /dev/null
+++ b/mesonbuild/scripts/clangformat.py
@@ -0,0 +1,61 @@
+# Copyright 2018 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.
+from __future__ import annotations
+
+import argparse
+import subprocess
+from pathlib import Path
+
+from .run_tool import run_tool
+from ..environment import detect_clangformat
+from ..mesonlib import version_compare
+from ..programs import ExternalProgram
+import typing as T
+
+def run_clang_format(fname: Path, exelist: T.List[str], check: bool) -> subprocess.CompletedProcess:
+ clangformat_10 = False
+ if check:
+ cformat_ver = ExternalProgram('clang-format', exelist).get_version()
+ if version_compare(cformat_ver, '>=10'):
+ clangformat_10 = True
+ exelist = exelist + ['--dry-run', '--Werror']
+ else:
+ original = fname.read_bytes()
+ before = fname.stat().st_mtime
+ ret = subprocess.run(exelist + ['-style=file', '-i', str(fname)])
+ after = fname.stat().st_mtime
+ if before != after:
+ print('File reformatted: ', fname)
+ if check and not clangformat_10:
+ # Restore the original if only checking.
+ fname.write_bytes(original)
+ ret.returncode = 1
+ return ret
+
+def run(args: T.List[str]) -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--check', action='store_true')
+ parser.add_argument('sourcedir')
+ parser.add_argument('builddir')
+ options = parser.parse_args(args)
+
+ srcdir = Path(options.sourcedir)
+ builddir = Path(options.builddir)
+
+ exelist = detect_clangformat()
+ if not exelist:
+ print('Could not execute clang-format "%s"' % ' '.join(exelist))
+ return 1
+
+ return run_tool('clang-format', srcdir, builddir, run_clang_format, exelist, options.check)
diff --git a/mesonbuild/scripts/clangtidy.py b/mesonbuild/scripts/clangtidy.py
new file mode 100644
index 0000000..324a26e
--- /dev/null
+++ b/mesonbuild/scripts/clangtidy.py
@@ -0,0 +1,35 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+import argparse
+import subprocess
+from pathlib import Path
+
+from .run_tool import run_tool
+import typing as T
+
+def run_clang_tidy(fname: Path, builddir: Path) -> subprocess.CompletedProcess:
+ return subprocess.run(['clang-tidy', '-p', str(builddir), str(fname)])
+
+def run(args: T.List[str]) -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('sourcedir')
+ parser.add_argument('builddir')
+ options = parser.parse_args(args)
+
+ srcdir = Path(options.sourcedir)
+ builddir = Path(options.builddir)
+
+ return run_tool('clang-tidy', srcdir, builddir, run_clang_tidy, builddir)
diff --git a/mesonbuild/scripts/cleantrees.py b/mesonbuild/scripts/cleantrees.py
new file mode 100644
index 0000000..3512f56
--- /dev/null
+++ b/mesonbuild/scripts/cleantrees.py
@@ -0,0 +1,45 @@
+# Copyright 2016 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.
+from __future__ import annotations
+
+import os
+import sys
+import shutil
+import pickle
+import typing as T
+
+def rmtrees(build_dir: str, trees: T.List[str]) -> None:
+ for t in trees:
+ # Never delete trees outside of the builddir
+ if os.path.isabs(t):
+ print(f'Cannot delete dir with absolute path {t!r}')
+ continue
+ bt = os.path.join(build_dir, t)
+ # Skip if it doesn't exist, or if it is not a directory
+ if os.path.isdir(bt):
+ shutil.rmtree(bt, ignore_errors=True)
+
+def run(args: T.List[str]) -> int:
+ if len(args) != 1:
+ print('Cleaner script for Meson. Do not run on your own please.')
+ print('cleantrees.py <data-file>')
+ return 1
+ with open(args[0], 'rb') as f:
+ data = pickle.load(f)
+ rmtrees(data.build_dir, data.trees)
+ # Never fail cleaning
+ return 0
+
+if __name__ == '__main__':
+ run(sys.argv[1:])
diff --git a/mesonbuild/scripts/cmake_run_ctgt.py b/mesonbuild/scripts/cmake_run_ctgt.py
new file mode 100755
index 0000000..a788ba5
--- /dev/null
+++ b/mesonbuild/scripts/cmake_run_ctgt.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import subprocess
+import shutil
+import sys
+from pathlib import Path
+import typing as T
+
+def run(argsv: T.List[str]) -> int:
+ commands = [[]] # type: T.List[T.List[str]]
+ SEPARATOR = ';;;'
+
+ # Generate CMD parameters
+ parser = argparse.ArgumentParser(description='Wrapper for add_custom_command')
+ parser.add_argument('-d', '--directory', type=str, metavar='D', required=True, help='Working directory to cwd to')
+ parser.add_argument('-o', '--outputs', nargs='+', metavar='O', required=True, help='Expected output files')
+ parser.add_argument('-O', '--original-outputs', nargs='*', metavar='O', default=[], help='Output files expected by CMake')
+ parser.add_argument('commands', nargs=argparse.REMAINDER, help=f'A "{SEPARATOR}" separated list of commands')
+
+ # Parse
+ args = parser.parse_args(argsv)
+ directory = Path(args.directory)
+
+ dummy_target = None
+ if len(args.outputs) == 1 and len(args.original_outputs) == 0:
+ dummy_target = Path(args.outputs[0])
+ elif len(args.outputs) != len(args.original_outputs):
+ print('Length of output list and original output list differ')
+ return 1
+
+ for i in args.commands:
+ if i == SEPARATOR:
+ commands += [[]]
+ continue
+
+ i = i.replace('"', '') # Remove lefover quotes
+ commands[-1] += [i]
+
+ # Execute
+ for i in commands:
+ # Skip empty lists
+ if not i:
+ continue
+
+ cmd = []
+ stdout = None
+ stderr = None
+ capture_file = ''
+
+ for j in i:
+ if j in {'>', '>>'}:
+ stdout = subprocess.PIPE
+ continue
+ elif j in {'&>', '&>>'}:
+ stdout = subprocess.PIPE
+ stderr = subprocess.STDOUT
+ continue
+
+ if stdout is not None or stderr is not None:
+ capture_file += j
+ else:
+ cmd += [j]
+
+ try:
+ directory.mkdir(parents=True, exist_ok=True)
+
+ res = subprocess.run(cmd, stdout=stdout, stderr=stderr, cwd=str(directory), check=True)
+ if capture_file:
+ out_file = directory / capture_file
+ out_file.write_bytes(res.stdout)
+ except subprocess.CalledProcessError:
+ return 1
+
+ if dummy_target:
+ dummy_target.touch()
+ return 0
+
+ # Copy outputs
+ zipped_outputs = zip([Path(x) for x in args.outputs], [Path(x) for x in args.original_outputs])
+ for expected, generated in zipped_outputs:
+ do_copy = False
+ if not expected.exists():
+ if not generated.exists():
+ print('Unable to find generated file. This can cause the build to fail:')
+ print(generated)
+ do_copy = False
+ else:
+ do_copy = True
+ elif generated.exists():
+ if generated.stat().st_mtime > expected.stat().st_mtime:
+ do_copy = True
+
+ if do_copy:
+ if expected.exists():
+ expected.unlink()
+ shutil.copyfile(str(generated), str(expected))
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(run(sys.argv[1:]))
diff --git a/mesonbuild/scripts/cmd_or_ps.ps1 b/mesonbuild/scripts/cmd_or_ps.ps1
new file mode 100644
index 0000000..96c32e2
--- /dev/null
+++ b/mesonbuild/scripts/cmd_or_ps.ps1
@@ -0,0 +1,17 @@
+# Copied from GStreamer project
+# Author: Seungha Yang <seungha.yang@navercorp.com>
+# Xavier Claessens <xclaesse@gmail.com>
+
+$i=1
+$ppid=$PID
+do {
+ $ppid=(Get-CimInstance Win32_Process -Filter "ProcessId=$ppid").parentprocessid
+ $pname=(Get-Process -id $ppid).Name
+ if($pname -eq "cmd" -Or $pname -eq "powershell" -Or $pname -eq "pwsh") {
+ Write-Host ("{0}.exe" -f $pname)
+ Break
+ }
+ # not found yet, find grand parent
+ # 10 times iteration seems to be sufficient
+ $i++
+} while ($i -lt 10)
diff --git a/mesonbuild/scripts/copy.py b/mesonbuild/scripts/copy.py
new file mode 100644
index 0000000..dba13a5
--- /dev/null
+++ b/mesonbuild/scripts/copy.py
@@ -0,0 +1,19 @@
+# SPDX-License-Identifer: Apache-2.0
+# Copyright © 2021 Intel Corporation
+from __future__ import annotations
+
+"""Helper script to copy files at build time.
+
+This is easier than trying to detect whether to use copy, cp, or something else.
+"""
+
+import shutil
+import typing as T
+
+
+def run(args: T.List[str]) -> int:
+ try:
+ shutil.copy2(args[0], args[1])
+ except Exception:
+ return 1
+ return 0
diff --git a/mesonbuild/scripts/coverage.py b/mesonbuild/scripts/coverage.py
new file mode 100644
index 0000000..5e78639
--- /dev/null
+++ b/mesonbuild/scripts/coverage.py
@@ -0,0 +1,202 @@
+# Copyright 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.
+from __future__ import annotations
+
+from mesonbuild import environment, mesonlib
+
+import argparse, re, sys, os, subprocess, pathlib, stat
+import typing as T
+
+def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build_root: str, log_dir: str, use_llvm_cov: bool) -> int:
+ outfiles = []
+ exitcode = 0
+
+ (gcovr_exe, gcovr_version, lcov_exe, genhtml_exe, llvm_cov_exe) = environment.find_coverage_tools()
+
+ # load config files for tools if available in the source tree
+ # - lcov requires manually specifying a per-project config
+ # - gcovr picks up the per-project config, and also supports filtering files
+ # so don't exclude subprojects ourselves, if the project has a config,
+ # because they either don't want that, or should set it themselves
+ lcovrc = os.path.join(source_root, '.lcovrc')
+ if os.path.exists(lcovrc):
+ lcov_config = ['--config-file', lcovrc]
+ else:
+ lcov_config = []
+
+ gcovr_config = ['-e', re.escape(subproject_root)]
+
+ # gcovr >= 4.2 requires a different syntax for out of source builds
+ if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=4.2'):
+ gcovr_base_cmd = [gcovr_exe, '-r', source_root, build_root]
+ # it also started supporting the config file
+ if os.path.exists(os.path.join(source_root, 'gcovr.cfg')):
+ gcovr_config = []
+ else:
+ gcovr_base_cmd = [gcovr_exe, '-r', build_root]
+
+ if use_llvm_cov:
+ gcov_exe_args = ['--gcov-executable', llvm_cov_exe + ' gcov']
+ else:
+ gcov_exe_args = []
+
+ if not outputs or 'xml' in outputs:
+ if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'):
+ subprocess.check_call(gcovr_base_cmd + gcovr_config +
+ ['-x',
+ '-o', os.path.join(log_dir, 'coverage.xml')
+ ] + gcov_exe_args)
+ outfiles.append(('Xml', pathlib.Path(log_dir, 'coverage.xml')))
+ elif outputs:
+ print('gcovr >= 3.3 needed to generate Xml coverage report')
+ exitcode = 1
+
+ if not outputs or 'sonarqube' in outputs:
+ if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=4.2'):
+ subprocess.check_call(gcovr_base_cmd + gcovr_config +
+ ['--sonarqube',
+ '-o', os.path.join(log_dir, 'sonarqube.xml'),
+ ] + gcov_exe_args)
+ outfiles.append(('Sonarqube', pathlib.Path(log_dir, 'sonarqube.xml')))
+ elif outputs:
+ print('gcovr >= 4.2 needed to generate Xml coverage report')
+ exitcode = 1
+
+ if not outputs or 'text' in outputs:
+ if gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'):
+ subprocess.check_call(gcovr_base_cmd + gcovr_config +
+ ['-o', os.path.join(log_dir, 'coverage.txt')] +
+ gcov_exe_args)
+ outfiles.append(('Text', pathlib.Path(log_dir, 'coverage.txt')))
+ elif outputs:
+ print('gcovr >= 3.3 needed to generate text coverage report')
+ exitcode = 1
+
+ if not outputs or 'html' in outputs:
+ if lcov_exe and genhtml_exe:
+ htmloutdir = os.path.join(log_dir, 'coveragereport')
+ covinfo = os.path.join(log_dir, 'coverage.info')
+ initial_tracefile = covinfo + '.initial'
+ run_tracefile = covinfo + '.run'
+ raw_tracefile = covinfo + '.raw'
+ if use_llvm_cov:
+ # Create a shim to allow using llvm-cov as a gcov tool.
+ if mesonlib.is_windows():
+ llvm_cov_shim_path = os.path.join(log_dir, 'llvm-cov.bat')
+ with open(llvm_cov_shim_path, 'w', encoding='utf-8') as llvm_cov_bat:
+ llvm_cov_bat.write(f'@"{llvm_cov_exe}" gcov %*')
+ else:
+ llvm_cov_shim_path = os.path.join(log_dir, 'llvm-cov.sh')
+ with open(llvm_cov_shim_path, 'w', encoding='utf-8') as llvm_cov_sh:
+ llvm_cov_sh.write(f'#!/usr/bin/env sh\nexec "{llvm_cov_exe}" gcov $@')
+ os.chmod(llvm_cov_shim_path, os.stat(llvm_cov_shim_path).st_mode | stat.S_IEXEC)
+ gcov_tool_args = ['--gcov-tool', llvm_cov_shim_path]
+ else:
+ gcov_tool_args = []
+ subprocess.check_call([lcov_exe,
+ '--directory', build_root,
+ '--capture',
+ '--initial',
+ '--output-file',
+ initial_tracefile] +
+ lcov_config +
+ gcov_tool_args)
+ subprocess.check_call([lcov_exe,
+ '--directory', build_root,
+ '--capture',
+ '--output-file', run_tracefile,
+ '--no-checksum',
+ '--rc', 'lcov_branch_coverage=1'] +
+ lcov_config +
+ gcov_tool_args)
+ # Join initial and test results.
+ subprocess.check_call([lcov_exe,
+ '-a', initial_tracefile,
+ '-a', run_tracefile,
+ '--rc', 'lcov_branch_coverage=1',
+ '-o', raw_tracefile] + lcov_config)
+ # Remove all directories outside the source_root from the covinfo
+ subprocess.check_call([lcov_exe,
+ '--extract', raw_tracefile,
+ os.path.join(source_root, '*'),
+ '--rc', 'lcov_branch_coverage=1',
+ '--output-file', covinfo] + lcov_config)
+ # Remove all directories inside subproject dir
+ subprocess.check_call([lcov_exe,
+ '--remove', covinfo,
+ os.path.join(subproject_root, '*'),
+ '--rc', 'lcov_branch_coverage=1',
+ '--output-file', covinfo] + lcov_config)
+ subprocess.check_call([genhtml_exe,
+ '--prefix', build_root,
+ '--prefix', source_root,
+ '--output-directory', htmloutdir,
+ '--title', 'Code coverage',
+ '--legend',
+ '--show-details',
+ '--branch-coverage',
+ covinfo])
+ outfiles.append(('Html', pathlib.Path(htmloutdir, 'index.html')))
+ elif gcovr_exe and mesonlib.version_compare(gcovr_version, '>=3.3'):
+ htmloutdir = os.path.join(log_dir, 'coveragereport')
+ if not os.path.isdir(htmloutdir):
+ os.mkdir(htmloutdir)
+ subprocess.check_call(gcovr_base_cmd + gcovr_config +
+ ['--html',
+ '--html-details',
+ '--print-summary',
+ '-o', os.path.join(htmloutdir, 'index.html'),
+ ])
+ outfiles.append(('Html', pathlib.Path(htmloutdir, 'index.html')))
+ elif outputs:
+ print('lcov/genhtml or gcovr >= 3.3 needed to generate Html coverage report')
+ exitcode = 1
+
+ if not outputs and not outfiles:
+ print('Need gcovr or lcov/genhtml to generate any coverage reports')
+ exitcode = 1
+
+ if outfiles:
+ print('')
+ for (filetype, path) in outfiles:
+ print(filetype + ' coverage report can be found at', path.as_uri())
+
+ return exitcode
+
+def run(args: T.List[str]) -> int:
+ if not os.path.isfile('build.ninja'):
+ print('Coverage currently only works with the Ninja backend.')
+ return 1
+ parser = argparse.ArgumentParser(description='Generate coverage reports')
+ parser.add_argument('--text', dest='outputs', action='append_const',
+ const='text', help='generate Text report')
+ parser.add_argument('--xml', dest='outputs', action='append_const',
+ const='xml', help='generate Xml report')
+ parser.add_argument('--sonarqube', dest='outputs', action='append_const',
+ const='sonarqube', help='generate Sonarqube Xml report')
+ parser.add_argument('--html', dest='outputs', action='append_const',
+ const='html', help='generate Html report')
+ parser.add_argument('--use_llvm_cov', action='store_true',
+ help='use llvm-cov')
+ parser.add_argument('source_root')
+ parser.add_argument('subproject_root')
+ parser.add_argument('build_root')
+ parser.add_argument('log_dir')
+ options = parser.parse_args(args)
+ return coverage(options.outputs, options.source_root,
+ options.subproject_root, options.build_root,
+ options.log_dir, options.use_llvm_cov)
+
+if __name__ == '__main__':
+ sys.exit(run(sys.argv[1:]))
diff --git a/mesonbuild/scripts/delwithsuffix.py b/mesonbuild/scripts/delwithsuffix.py
new file mode 100644
index 0000000..f58b19c
--- /dev/null
+++ b/mesonbuild/scripts/delwithsuffix.py
@@ -0,0 +1,37 @@
+# Copyright 2013 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.
+from __future__ import annotations
+
+import os, sys
+import typing as T
+
+def run(args: T.List[str]) -> int:
+ if len(args) != 2:
+ print('delwithsuffix.py <root of subdir to process> <suffix to delete>')
+ sys.exit(1)
+
+ topdir = args[0]
+ suffix = args[1]
+ if suffix[0] != '.':
+ suffix = '.' + suffix
+
+ for (root, _, files) in os.walk(topdir):
+ for f in files:
+ if f.endswith(suffix):
+ fullname = os.path.join(root, f)
+ os.unlink(fullname)
+ return 0
+
+if __name__ == '__main__':
+ run(sys.argv[1:])
diff --git a/mesonbuild/scripts/depfixer.py b/mesonbuild/scripts/depfixer.py
new file mode 100644
index 0000000..ae18594
--- /dev/null
+++ b/mesonbuild/scripts/depfixer.py
@@ -0,0 +1,505 @@
+# Copyright 2013-2016 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.
+from __future__ import annotations
+
+
+import sys
+import os
+import stat
+import struct
+import shutil
+import subprocess
+import typing as T
+
+from ..mesonlib import OrderedSet, generate_list, Popen_safe
+
+SHT_STRTAB = 3
+DT_NEEDED = 1
+DT_RPATH = 15
+DT_RUNPATH = 29
+DT_STRTAB = 5
+DT_SONAME = 14
+DT_MIPS_RLD_MAP_REL = 1879048245
+
+# Global cache for tools
+INSTALL_NAME_TOOL = False
+
+class DataSizes:
+ def __init__(self, ptrsize: int, is_le: bool) -> None:
+ if is_le:
+ p = '<'
+ else:
+ p = '>'
+ self.Half = p + 'h'
+ self.HalfSize = 2
+ self.Word = p + 'I'
+ self.WordSize = 4
+ self.Sword = p + 'i'
+ self.SwordSize = 4
+ if ptrsize == 64:
+ self.Addr = p + 'Q'
+ self.AddrSize = 8
+ self.Off = p + 'Q'
+ self.OffSize = 8
+ self.XWord = p + 'Q'
+ self.XWordSize = 8
+ self.Sxword = p + 'q'
+ self.SxwordSize = 8
+ else:
+ self.Addr = p + 'I'
+ self.AddrSize = 4
+ self.Off = p + 'I'
+ self.OffSize = 4
+
+class DynamicEntry(DataSizes):
+ def __init__(self, ifile: T.BinaryIO, ptrsize: int, is_le: bool) -> None:
+ super().__init__(ptrsize, is_le)
+ self.ptrsize = ptrsize
+ if ptrsize == 64:
+ self.d_tag = struct.unpack(self.Sxword, ifile.read(self.SxwordSize))[0]
+ self.val = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
+ else:
+ self.d_tag = struct.unpack(self.Sword, ifile.read(self.SwordSize))[0]
+ self.val = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
+
+ def write(self, ofile: T.BinaryIO) -> None:
+ if self.ptrsize == 64:
+ ofile.write(struct.pack(self.Sxword, self.d_tag))
+ ofile.write(struct.pack(self.XWord, self.val))
+ else:
+ ofile.write(struct.pack(self.Sword, self.d_tag))
+ ofile.write(struct.pack(self.Word, self.val))
+
+class SectionHeader(DataSizes):
+ def __init__(self, ifile: T.BinaryIO, ptrsize: int, is_le: bool) -> None:
+ super().__init__(ptrsize, is_le)
+ is_64 = ptrsize == 64
+
+# Elf64_Word
+ self.sh_name = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
+# Elf64_Word
+ self.sh_type = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
+# Elf64_Xword
+ if is_64:
+ self.sh_flags = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
+ else:
+ self.sh_flags = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
+# Elf64_Addr
+ self.sh_addr = struct.unpack(self.Addr, ifile.read(self.AddrSize))[0]
+# Elf64_Off
+ self.sh_offset = struct.unpack(self.Off, ifile.read(self.OffSize))[0]
+# Elf64_Xword
+ if is_64:
+ self.sh_size = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
+ else:
+ self.sh_size = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
+# Elf64_Word
+ self.sh_link = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
+# Elf64_Word
+ self.sh_info = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
+# Elf64_Xword
+ if is_64:
+ self.sh_addralign = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
+ else:
+ self.sh_addralign = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
+# Elf64_Xword
+ if is_64:
+ self.sh_entsize = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
+ else:
+ self.sh_entsize = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
+
+class Elf(DataSizes):
+ def __init__(self, bfile: str, verbose: bool = True) -> None:
+ self.bfile = bfile
+ self.verbose = verbose
+ self.sections = [] # type: T.List[SectionHeader]
+ self.dynamic = [] # type: T.List[DynamicEntry]
+ self.open_bf(bfile)
+ try:
+ (self.ptrsize, self.is_le) = self.detect_elf_type()
+ super().__init__(self.ptrsize, self.is_le)
+ self.parse_header()
+ self.parse_sections()
+ self.parse_dynamic()
+ except (struct.error, RuntimeError):
+ self.close_bf()
+ raise
+
+ def open_bf(self, bfile: str) -> None:
+ self.bf = None
+ self.bf_perms = None
+ try:
+ self.bf = open(bfile, 'r+b')
+ except PermissionError as e:
+ self.bf_perms = stat.S_IMODE(os.lstat(bfile).st_mode)
+ os.chmod(bfile, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
+ try:
+ self.bf = open(bfile, 'r+b')
+ except Exception:
+ os.chmod(bfile, self.bf_perms)
+ self.bf_perms = None
+ raise e
+
+ def close_bf(self) -> None:
+ if self.bf is not None:
+ if self.bf_perms is not None:
+ os.fchmod(self.bf.fileno(), self.bf_perms)
+ self.bf_perms = None
+ self.bf.close()
+ self.bf = None
+
+ def __enter__(self) -> 'Elf':
+ return self
+
+ def __del__(self) -> None:
+ self.close_bf()
+
+ def __exit__(self, exc_type: T.Any, exc_value: T.Any, traceback: T.Any) -> None:
+ self.close_bf()
+
+ def detect_elf_type(self) -> T.Tuple[int, bool]:
+ data = self.bf.read(6)
+ if data[1:4] != b'ELF':
+ # This script gets called to non-elf targets too
+ # so just ignore them.
+ if self.verbose:
+ print(f'File {self.bfile!r} is not an ELF file.')
+ sys.exit(0)
+ if data[4] == 1:
+ ptrsize = 32
+ elif data[4] == 2:
+ ptrsize = 64
+ else:
+ sys.exit(f'File {self.bfile!r} has unknown ELF class.')
+ if data[5] == 1:
+ is_le = True
+ elif data[5] == 2:
+ is_le = False
+ else:
+ sys.exit(f'File {self.bfile!r} has unknown ELF endianness.')
+ return ptrsize, is_le
+
+ def parse_header(self) -> None:
+ self.bf.seek(0)
+ self.e_ident = struct.unpack('16s', self.bf.read(16))[0]
+ self.e_type = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
+ self.e_machine = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
+ self.e_version = struct.unpack(self.Word, self.bf.read(self.WordSize))[0]
+ self.e_entry = struct.unpack(self.Addr, self.bf.read(self.AddrSize))[0]
+ self.e_phoff = struct.unpack(self.Off, self.bf.read(self.OffSize))[0]
+ self.e_shoff = struct.unpack(self.Off, self.bf.read(self.OffSize))[0]
+ self.e_flags = struct.unpack(self.Word, self.bf.read(self.WordSize))[0]
+ self.e_ehsize = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
+ self.e_phentsize = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
+ self.e_phnum = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
+ self.e_shentsize = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
+ self.e_shnum = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
+ self.e_shstrndx = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
+
+ def parse_sections(self) -> None:
+ self.bf.seek(self.e_shoff)
+ for _ in range(self.e_shnum):
+ self.sections.append(SectionHeader(self.bf, self.ptrsize, self.is_le))
+
+ def read_str(self) -> bytes:
+ arr = []
+ x = self.bf.read(1)
+ while x != b'\0':
+ arr.append(x)
+ x = self.bf.read(1)
+ if x == b'':
+ raise RuntimeError('Tried to read past the end of the file')
+ return b''.join(arr)
+
+ def find_section(self, target_name: bytes) -> T.Optional[SectionHeader]:
+ section_names = self.sections[self.e_shstrndx]
+ for i in self.sections:
+ self.bf.seek(section_names.sh_offset + i.sh_name)
+ name = self.read_str()
+ if name == target_name:
+ return i
+ return None
+
+ def parse_dynamic(self) -> None:
+ sec = self.find_section(b'.dynamic')
+ if sec is None:
+ return
+ self.bf.seek(sec.sh_offset)
+ while True:
+ e = DynamicEntry(self.bf, self.ptrsize, self.is_le)
+ self.dynamic.append(e)
+ if e.d_tag == 0:
+ break
+
+ @generate_list
+ def get_section_names(self) -> T.Generator[str, None, None]:
+ section_names = self.sections[self.e_shstrndx]
+ for i in self.sections:
+ self.bf.seek(section_names.sh_offset + i.sh_name)
+ yield self.read_str().decode()
+
+ def get_soname(self) -> T.Optional[str]:
+ soname = None
+ strtab = None
+ for i in self.dynamic:
+ if i.d_tag == DT_SONAME:
+ soname = i
+ if i.d_tag == DT_STRTAB:
+ strtab = i
+ if soname is None or strtab is None:
+ return None
+ self.bf.seek(strtab.val + soname.val)
+ return self.read_str().decode()
+
+ def get_entry_offset(self, entrynum: int) -> T.Optional[int]:
+ sec = self.find_section(b'.dynstr')
+ for i in self.dynamic:
+ if i.d_tag == entrynum:
+ res = sec.sh_offset + i.val
+ assert isinstance(res, int)
+ return res
+ return None
+
+ def get_rpath(self) -> T.Optional[str]:
+ offset = self.get_entry_offset(DT_RPATH)
+ if offset is None:
+ return None
+ self.bf.seek(offset)
+ return self.read_str().decode()
+
+ def get_runpath(self) -> T.Optional[str]:
+ offset = self.get_entry_offset(DT_RUNPATH)
+ if offset is None:
+ return None
+ self.bf.seek(offset)
+ return self.read_str().decode()
+
+ @generate_list
+ def get_deps(self) -> T.Generator[str, None, None]:
+ sec = self.find_section(b'.dynstr')
+ for i in self.dynamic:
+ if i.d_tag == DT_NEEDED:
+ offset = sec.sh_offset + i.val
+ self.bf.seek(offset)
+ yield self.read_str().decode()
+
+ def fix_deps(self, prefix: bytes) -> None:
+ sec = self.find_section(b'.dynstr')
+ deps = []
+ for i in self.dynamic:
+ if i.d_tag == DT_NEEDED:
+ deps.append(i)
+ for i in deps:
+ offset = sec.sh_offset + i.val
+ self.bf.seek(offset)
+ name = self.read_str()
+ if name.startswith(prefix):
+ basename = name.rsplit(b'/', maxsplit=1)[-1]
+ padding = b'\0' * (len(name) - len(basename))
+ newname = basename + padding
+ assert len(newname) == len(name)
+ self.bf.seek(offset)
+ self.bf.write(newname)
+
+ def fix_rpath(self, fname: str, rpath_dirs_to_remove: T.Set[bytes], new_rpath: bytes) -> None:
+ # The path to search for can be either rpath or runpath.
+ # Fix both of them to be sure.
+ self.fix_rpathtype_entry(fname, rpath_dirs_to_remove, new_rpath, DT_RPATH)
+ self.fix_rpathtype_entry(fname, rpath_dirs_to_remove, new_rpath, DT_RUNPATH)
+
+ def fix_rpathtype_entry(self, fname: str, rpath_dirs_to_remove: T.Set[bytes], new_rpath: bytes, entrynum: int) -> None:
+ rp_off = self.get_entry_offset(entrynum)
+ if rp_off is None:
+ if self.verbose:
+ print(f'File {fname!r} does not have an rpath. It should be a fully static executable.')
+ return
+ self.bf.seek(rp_off)
+
+ old_rpath = self.read_str()
+ # Some rpath entries may come from multiple sources.
+ # Only add each one once.
+ new_rpaths = OrderedSet() # type: OrderedSet[bytes]
+ if new_rpath:
+ new_rpaths.update(new_rpath.split(b':'))
+ if old_rpath:
+ # Filter out build-only rpath entries
+ # added by get_link_dep_subdirs() or
+ # specified by user with build_rpath.
+ for rpath_dir in old_rpath.split(b':'):
+ if not (rpath_dir in rpath_dirs_to_remove or
+ rpath_dir == (b'X' * len(rpath_dir))):
+ if rpath_dir:
+ new_rpaths.add(rpath_dir)
+
+ # Prepend user-specified new entries while preserving the ones that came from pkgconfig etc.
+ new_rpath = b':'.join(new_rpaths)
+
+ if len(old_rpath) < len(new_rpath):
+ msg = "New rpath must not be longer than the old one.\n Old: {}\n New: {}".format(old_rpath.decode('utf-8'), new_rpath.decode('utf-8'))
+ sys.exit(msg)
+ # The linker does read-only string deduplication. If there is a
+ # string that shares a suffix with the rpath, they might get
+ # dedupped. This means changing the rpath string might break something
+ # completely unrelated. This has already happened once with X.org.
+ # Thus we want to keep this change as small as possible to minimize
+ # the chance of obliterating other strings. It might still happen
+ # but our behavior is identical to what chrpath does and it has
+ # been in use for ages so based on that this should be rare.
+ if not new_rpath:
+ self.remove_rpath_entry(entrynum)
+ else:
+ self.bf.seek(rp_off)
+ self.bf.write(new_rpath)
+ self.bf.write(b'\0')
+
+ def remove_rpath_entry(self, entrynum: int) -> None:
+ sec = self.find_section(b'.dynamic')
+ if sec is None:
+ return None
+ for (i, entry) in enumerate(self.dynamic):
+ if entry.d_tag == entrynum:
+ rpentry = self.dynamic[i]
+ rpentry.d_tag = 0
+ self.dynamic = self.dynamic[:i] + self.dynamic[i + 1:] + [rpentry]
+ break
+ # DT_MIPS_RLD_MAP_REL is relative to the offset of the tag. Adjust it consequently.
+ for entry in self.dynamic[i:]:
+ if entry.d_tag == DT_MIPS_RLD_MAP_REL:
+ entry.val += 2 * (self.ptrsize // 8)
+ break
+ self.bf.seek(sec.sh_offset)
+ for entry in self.dynamic:
+ entry.write(self.bf)
+ return None
+
+def fix_elf(fname: str, rpath_dirs_to_remove: T.Set[bytes], new_rpath: T.Optional[bytes], verbose: bool = True) -> None:
+ if new_rpath is not None:
+ with Elf(fname, verbose) as e:
+ # note: e.get_rpath() and e.get_runpath() may be useful
+ e.fix_rpath(fname, rpath_dirs_to_remove, new_rpath)
+
+def get_darwin_rpaths_to_remove(fname: str) -> T.List[str]:
+ p, out, _ = Popen_safe(['otool', '-l', fname], stderr=subprocess.DEVNULL)
+ if p.returncode != 0:
+ raise subprocess.CalledProcessError(p.returncode, p.args, out)
+ result = []
+ current_cmd = 'FOOBAR'
+ for line in out.split('\n'):
+ line = line.strip()
+ if ' ' not in line:
+ continue
+ key, value = line.strip().split(' ', 1)
+ if key == 'cmd':
+ current_cmd = value
+ if key == 'path' and current_cmd == 'LC_RPATH':
+ rp = value.split('(', 1)[0].strip()
+ result.append(rp)
+ return result
+
+def fix_darwin(fname: str, new_rpath: str, final_path: str, install_name_mappings: T.Dict[str, str]) -> None:
+ try:
+ rpaths = get_darwin_rpaths_to_remove(fname)
+ except subprocess.CalledProcessError:
+ # Otool failed, which happens when invoked on a
+ # non-executable target. Just return.
+ return
+ try:
+ args = []
+ if rpaths:
+ # TODO: fix this properly, not totally clear how
+ #
+ # removing rpaths from binaries on macOS has tons of
+ # weird edge cases. For instance, if the user provided
+ # a '-Wl,-rpath' argument in LDFLAGS that happens to
+ # coincide with an rpath generated from a dependency,
+ # this would cause installation failures, as meson would
+ # generate install_name_tool calls with two identical
+ # '-delete_rpath' arguments, which install_name_tool
+ # fails on. Because meson itself ensures that it never
+ # adds duplicate rpaths, duplicate rpaths necessarily
+ # come from user variables. The idea of using OrderedSet
+ # is to remove *at most one* duplicate RPATH entry. This
+ # is not optimal, as it only respects the user's choice
+ # partially: if they provided a non-duplicate '-Wl,-rpath'
+ # argument, it gets removed, if they provided a duplicate
+ # one, it remains in the final binary. A potentially optimal
+ # solution would split all user '-Wl,-rpath' arguments from
+ # LDFLAGS, and later add them back with '-add_rpath'.
+ for rp in OrderedSet(rpaths):
+ args += ['-delete_rpath', rp]
+ subprocess.check_call(['install_name_tool', fname] + args,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+ args = []
+ if new_rpath:
+ args += ['-add_rpath', new_rpath]
+ # Rewrite -install_name @rpath/libfoo.dylib to /path/to/libfoo.dylib
+ if fname.endswith('dylib'):
+ args += ['-id', final_path]
+ if install_name_mappings:
+ for old, new in install_name_mappings.items():
+ args += ['-change', old, new]
+ if args:
+ subprocess.check_call(['install_name_tool', fname] + args,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+ except Exception as err:
+ raise SystemExit(err)
+
+def fix_jar(fname: str) -> None:
+ subprocess.check_call(['jar', 'xf', fname, 'META-INF/MANIFEST.MF'])
+ with open('META-INF/MANIFEST.MF', 'r+', encoding='utf-8') as f:
+ lines = f.readlines()
+ f.seek(0)
+ for line in lines:
+ if not line.startswith('Class-Path:'):
+ f.write(line)
+ f.truncate()
+ # jar -um doesn't allow removing existing attributes. Use -uM instead,
+ # which a) removes the existing manifest from the jar and b) disables
+ # special-casing for the manifest file, so we can re-add it as a normal
+ # archive member. This puts the manifest at the end of the jar rather
+ # than the beginning, but the spec doesn't forbid that.
+ subprocess.check_call(['jar', 'ufM', fname, 'META-INF/MANIFEST.MF'])
+
+def fix_rpath(fname: str, rpath_dirs_to_remove: T.Set[bytes], new_rpath: T.Union[str, bytes], final_path: str, install_name_mappings: T.Dict[str, str], verbose: bool = True) -> None:
+ global INSTALL_NAME_TOOL # pylint: disable=global-statement
+ # Static libraries, import libraries, debug information, headers, etc
+ # never have rpaths
+ # DLLs and EXE currently do not need runtime path fixing
+ if fname.endswith(('.a', '.lib', '.pdb', '.h', '.hpp', '.dll', '.exe')):
+ return
+ try:
+ if fname.endswith('.jar'):
+ fix_jar(fname)
+ return
+ if isinstance(new_rpath, str):
+ new_rpath = new_rpath.encode('utf8')
+ fix_elf(fname, rpath_dirs_to_remove, new_rpath, verbose)
+ return
+ except SystemExit as e:
+ if isinstance(e.code, int) and e.code == 0:
+ pass
+ else:
+ raise
+ # We don't look for this on import because it will do a useless PATH lookup
+ # on non-mac platforms. That can be expensive on some Windows machines
+ # (up to 30ms), which is significant with --only-changed. For details, see:
+ # https://github.com/mesonbuild/meson/pull/6612#discussion_r378581401
+ if INSTALL_NAME_TOOL is False:
+ INSTALL_NAME_TOOL = bool(shutil.which('install_name_tool'))
+ if INSTALL_NAME_TOOL:
+ if isinstance(new_rpath, bytes):
+ new_rpath = new_rpath.decode('utf8')
+ fix_darwin(fname, new_rpath, final_path, install_name_mappings)
diff --git a/mesonbuild/scripts/depscan.py b/mesonbuild/scripts/depscan.py
new file mode 100644
index 0000000..3ae14c0
--- /dev/null
+++ b/mesonbuild/scripts/depscan.py
@@ -0,0 +1,208 @@
+# Copyright 2020 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.
+from __future__ import annotations
+
+import json
+import os
+import pathlib
+import pickle
+import re
+import sys
+import typing as T
+
+from ..backend.ninjabackend import ninja_quote
+from ..compilers.compilers import lang_suffixes
+
+if T.TYPE_CHECKING:
+ from ..backend.ninjabackend import TargetDependencyScannerInfo
+
+CPP_IMPORT_RE = re.compile(r'\w*import ([a-zA-Z0-9]+);')
+CPP_EXPORT_RE = re.compile(r'\w*export module ([a-zA-Z0-9]+);')
+
+FORTRAN_INCLUDE_PAT = r"^\s*include\s*['\"](\w+\.\w+)['\"]"
+FORTRAN_MODULE_PAT = r"^\s*\bmodule\b\s+(\w+)\s*(?:!+.*)*$"
+FORTRAN_SUBMOD_PAT = r"^\s*\bsubmodule\b\s*\((\w+:?\w+)\)\s*(\w+)"
+FORTRAN_USE_PAT = r"^\s*use,?\s*(?:non_intrinsic)?\s*(?:::)?\s*(\w+)"
+
+FORTRAN_MODULE_RE = re.compile(FORTRAN_MODULE_PAT, re.IGNORECASE)
+FORTRAN_SUBMOD_RE = re.compile(FORTRAN_SUBMOD_PAT, re.IGNORECASE)
+FORTRAN_USE_RE = re.compile(FORTRAN_USE_PAT, re.IGNORECASE)
+
+class DependencyScanner:
+ def __init__(self, pickle_file: str, outfile: str, sources: T.List[str]):
+ with open(pickle_file, 'rb') as pf:
+ self.target_data: TargetDependencyScannerInfo = pickle.load(pf)
+ self.outfile = outfile
+ self.sources = sources
+ self.provided_by: T.Dict[str, str] = {}
+ self.exports: T.Dict[str, str] = {}
+ self.needs: T.Dict[str, T.List[str]] = {}
+ self.sources_with_exports: T.List[str] = []
+
+ def scan_file(self, fname: str) -> None:
+ suffix = os.path.splitext(fname)[1][1:]
+ if suffix != 'C':
+ suffix = suffix.lower()
+ if suffix in lang_suffixes['fortran']:
+ self.scan_fortran_file(fname)
+ elif suffix in lang_suffixes['cpp']:
+ self.scan_cpp_file(fname)
+ else:
+ sys.exit(f'Can not scan files with suffix .{suffix}.')
+
+ def scan_fortran_file(self, fname: str) -> None:
+ fpath = pathlib.Path(fname)
+ modules_in_this_file = set()
+ for line in fpath.read_text(encoding='utf-8', errors='ignore').split('\n'):
+ import_match = FORTRAN_USE_RE.match(line)
+ export_match = FORTRAN_MODULE_RE.match(line)
+ submodule_export_match = FORTRAN_SUBMOD_RE.match(line)
+ if import_match:
+ needed = import_match.group(1).lower()
+ # In Fortran you have an using declaration also for the module
+ # you define in the same file. Prevent circular dependencies.
+ if needed not in modules_in_this_file:
+ if fname in self.needs:
+ self.needs[fname].append(needed)
+ else:
+ self.needs[fname] = [needed]
+ if export_match:
+ exported_module = export_match.group(1).lower()
+ assert exported_module not in modules_in_this_file
+ modules_in_this_file.add(exported_module)
+ if exported_module in self.provided_by:
+ raise RuntimeError(f'Multiple files provide module {exported_module}.')
+ self.sources_with_exports.append(fname)
+ self.provided_by[exported_module] = fname
+ self.exports[fname] = exported_module
+ if submodule_export_match:
+ # Store submodule "Foo" "Bar" as "foo:bar".
+ # A submodule declaration can be both an import and an export declaration:
+ #
+ # submodule (a1:a2) a3
+ # - requires a1@a2.smod
+ # - produces a1@a3.smod
+ parent_module_name_full = submodule_export_match.group(1).lower()
+ parent_module_name = parent_module_name_full.split(':')[0]
+ submodule_name = submodule_export_match.group(2).lower()
+ concat_name = f'{parent_module_name}:{submodule_name}'
+ self.sources_with_exports.append(fname)
+ self.provided_by[concat_name] = fname
+ self.exports[fname] = concat_name
+ # Fortran requires that the immediate parent module must be built
+ # before the current one. Thus:
+ #
+ # submodule (parent) parent <- requires parent.mod (really parent.smod, but they are created at the same time)
+ # submodule (a1:a2) a3 <- requires a1@a2.smod
+ #
+ # a3 does not depend on the a1 parent module directly, only transitively.
+ if fname in self.needs:
+ self.needs[fname].append(parent_module_name_full)
+ else:
+ self.needs[fname] = [parent_module_name_full]
+
+ def scan_cpp_file(self, fname: str) -> None:
+ fpath = pathlib.Path(fname)
+ for line in fpath.read_text(encoding='utf-8', errors='ignore').split('\n'):
+ import_match = CPP_IMPORT_RE.match(line)
+ export_match = CPP_EXPORT_RE.match(line)
+ if import_match:
+ needed = import_match.group(1)
+ if fname in self.needs:
+ self.needs[fname].append(needed)
+ else:
+ self.needs[fname] = [needed]
+ if export_match:
+ exported_module = export_match.group(1)
+ if exported_module in self.provided_by:
+ raise RuntimeError(f'Multiple files provide module {exported_module}.')
+ self.sources_with_exports.append(fname)
+ self.provided_by[exported_module] = fname
+ self.exports[fname] = exported_module
+
+ def objname_for(self, src: str) -> str:
+ objname = self.target_data.source2object[src]
+ assert isinstance(objname, str)
+ return objname
+
+ def module_name_for(self, src: str) -> str:
+ suffix = os.path.splitext(src)[1][1:].lower()
+ if suffix in lang_suffixes['fortran']:
+ exported = self.exports[src]
+ # Module foo:bar goes to a file name foo@bar.smod
+ # Module Foo goes to a file name foo.mod
+ namebase = exported.replace(':', '@')
+ if ':' in exported:
+ extension = 'smod'
+ else:
+ extension = 'mod'
+ return os.path.join(self.target_data.private_dir, f'{namebase}.{extension}')
+ elif suffix in lang_suffixes['cpp']:
+ return '{}.ifc'.format(self.exports[src])
+ else:
+ raise RuntimeError('Unreachable code.')
+
+ def scan(self) -> int:
+ for s in self.sources:
+ self.scan_file(s)
+ with open(self.outfile, 'w', encoding='utf-8') as ofile:
+ ofile.write('ninja_dyndep_version = 1\n')
+ for src in self.sources:
+ objfilename = self.objname_for(src)
+ mods_and_submods_needed = []
+ module_files_generated = []
+ module_files_needed = []
+ if src in self.sources_with_exports:
+ module_files_generated.append(self.module_name_for(src))
+ if src in self.needs:
+ for modname in self.needs[src]:
+ if modname not in self.provided_by:
+ # Nothing provides this module, we assume that it
+ # comes from a dependency library somewhere and is
+ # already built by the time this compilation starts.
+ pass
+ else:
+ mods_and_submods_needed.append(modname)
+
+ for modname in mods_and_submods_needed:
+ provider_src = self.provided_by[modname]
+ provider_modfile = self.module_name_for(provider_src)
+ # Prune self-dependencies
+ if provider_src != src:
+ module_files_needed.append(provider_modfile)
+
+ quoted_objfilename = ninja_quote(objfilename, True)
+ quoted_module_files_generated = [ninja_quote(x, True) for x in module_files_generated]
+ quoted_module_files_needed = [ninja_quote(x, True) for x in module_files_needed]
+ if quoted_module_files_generated:
+ mod_gen = '| ' + ' '.join(quoted_module_files_generated)
+ else:
+ mod_gen = ''
+ if quoted_module_files_needed:
+ mod_dep = '| ' + ' '.join(quoted_module_files_needed)
+ else:
+ mod_dep = ''
+ build_line = 'build {} {}: dyndep {}'.format(quoted_objfilename,
+ mod_gen,
+ mod_dep)
+ ofile.write(build_line + '\n')
+ return 0
+
+def run(args: T.List[str]) -> int:
+ assert len(args) == 3, 'got wrong number of arguments!'
+ pickle_file, outfile, jsonfile = args
+ with open(jsonfile, encoding='utf-8') as f:
+ sources = json.load(f)
+ scanner = DependencyScanner(pickle_file, outfile, sources)
+ return scanner.scan()
diff --git a/mesonbuild/scripts/dirchanger.py b/mesonbuild/scripts/dirchanger.py
new file mode 100644
index 0000000..60c4f12
--- /dev/null
+++ b/mesonbuild/scripts/dirchanger.py
@@ -0,0 +1,30 @@
+# Copyright 2015-2016 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.
+from __future__ import annotations
+
+'''CD into dir given as first argument and execute
+the command given in the rest of the arguments.'''
+
+import os, subprocess, sys
+import typing as T
+
+def run(args: T.List[str]) -> int:
+ dirname = args[0]
+ command = args[1:]
+
+ os.chdir(dirname)
+ return subprocess.call(command)
+
+if __name__ == '__main__':
+ sys.exit(run(sys.argv[1:]))
diff --git a/mesonbuild/scripts/env2mfile.py b/mesonbuild/scripts/env2mfile.py
new file mode 100755
index 0000000..af7ffc6
--- /dev/null
+++ b/mesonbuild/scripts/env2mfile.py
@@ -0,0 +1,368 @@
+# Copyright 2022 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.
+from __future__ import annotations
+
+import sys, os, subprocess, shutil
+import shlex
+import typing as T
+
+from .. import envconfig
+from .. import mlog
+from ..compilers import compilers
+from ..compilers.detect import defaults as compiler_names
+
+if T.TYPE_CHECKING:
+ import argparse
+
+def has_for_build() -> bool:
+ for cenv in envconfig.ENV_VAR_COMPILER_MAP.values():
+ if os.environ.get(cenv + '_FOR_BUILD'):
+ return True
+ return False
+
+def add_arguments(parser: 'argparse.ArgumentParser') -> None:
+ parser.add_argument('--debarch', default=None,
+ help='The dpkg architecture to generate.')
+ parser.add_argument('--gccsuffix', default="",
+ help='A particular gcc version suffix if necessary.')
+ parser.add_argument('-o', required=True, dest='outfile',
+ help='The output file.')
+ parser.add_argument('--cross', default=False, action='store_true',
+ help='Generate a cross compilation file.')
+ parser.add_argument('--native', default=False, action='store_true',
+ help='Generate a native compilation file.')
+ parser.add_argument('--system', default=None,
+ help='Define system for cross compilation.')
+ parser.add_argument('--cpu', default=None,
+ help='Define cpu for cross compilation.')
+ parser.add_argument('--cpu-family', default=None,
+ help='Define cpu family for cross compilation.')
+ parser.add_argument('--endian', default='little', choices=['big', 'little'],
+ help='Define endianness for cross compilation.')
+
+class MachineInfo:
+ def __init__(self) -> None:
+ self.compilers: T.Dict[str, T.List[str]] = {}
+ self.binaries: T.Dict[str, T.List[str]] = {}
+ self.properties: T.Dict[str, T.Union[str, T.List[str]]] = {}
+ self.compile_args: T.Dict[str, T.List[str]] = {}
+ self.link_args: T.Dict[str, T.List[str]] = {}
+ self.cmake: T.Dict[str, T.Union[str, T.List[str]]] = {}
+
+ self.system: T.Optional[str] = None
+ self.cpu: T.Optional[str] = None
+ self.cpu_family: T.Optional[str] = None
+ self.endian: T.Optional[str] = None
+
+#parser = argparse.ArgumentParser(description='''Generate cross compilation definition file for the Meson build system.
+#
+#If you do not specify the --arch argument, Meson assumes that running
+#plain 'dpkg-architecture' will return correct information for the
+#host system.
+#
+#This script must be run in an environment where CPPFLAGS et al are set to the
+#same values used in the actual compilation.
+#'''
+#)
+
+def locate_path(program: str) -> T.List[str]:
+ if os.path.isabs(program):
+ return [program]
+ for d in os.get_exec_path():
+ f = os.path.join(d, program)
+ if os.access(f, os.X_OK):
+ return [f]
+ raise ValueError("%s not found on $PATH" % program)
+
+def write_args_line(ofile: T.TextIO, name: str, args: T.Union[str, T.List[str]]) -> None:
+ if len(args) == 0:
+ return
+ if isinstance(args, str):
+ ostr = name + "= '" + args + "'\n"
+ else:
+ ostr = name + ' = ['
+ ostr += ', '.join("'" + i + "'" for i in args)
+ ostr += ']\n'
+ ofile.write(ostr)
+
+def get_args_from_envvars(infos: MachineInfo) -> None:
+ cppflags = shlex.split(os.environ.get('CPPFLAGS', ''))
+ cflags = shlex.split(os.environ.get('CFLAGS', ''))
+ cxxflags = shlex.split(os.environ.get('CXXFLAGS', ''))
+ objcflags = shlex.split(os.environ.get('OBJCFLAGS', ''))
+ objcxxflags = shlex.split(os.environ.get('OBJCXXFLAGS', ''))
+ ldflags = shlex.split(os.environ.get('LDFLAGS', ''))
+
+ c_args = cppflags + cflags
+ cpp_args = cppflags + cxxflags
+ c_link_args = cflags + ldflags
+ cpp_link_args = cxxflags + ldflags
+
+ objc_args = cppflags + objcflags
+ objcpp_args = cppflags + objcxxflags
+ objc_link_args = objcflags + ldflags
+ objcpp_link_args = objcxxflags + ldflags
+
+ if c_args:
+ infos.compile_args['c'] = c_args
+ if c_link_args:
+ infos.link_args['c'] = c_link_args
+ if cpp_args:
+ infos.compile_args['cpp'] = cpp_args
+ if cpp_link_args:
+ infos.link_args['cpp'] = cpp_link_args
+ if objc_args:
+ infos.compile_args['objc'] = objc_args
+ if objc_link_args:
+ infos.link_args['objc'] = objc_link_args
+ if objcpp_args:
+ infos.compile_args['objcpp'] = objcpp_args
+ if objcpp_link_args:
+ infos.link_args['objcpp'] = objcpp_link_args
+
+cpu_family_map = {
+ 'mips64el': 'mips64',
+ 'i686': 'x86',
+}
+cpu_map = {
+ 'armhf': 'arm7hlf',
+ 'mips64el': 'mips64'
+}
+
+def deb_detect_cmake(infos: MachineInfo, data: T.Dict[str, str]) -> None:
+ system_name_map = {'linux': 'Linux', 'kfreebsd': 'kFreeBSD', 'hurd': 'GNU'}
+ system_processor_map = {'arm': 'armv7l', 'mips64el': 'mips64', 'powerpc64le': 'ppc64le'}
+
+ infos.cmake["CMAKE_C_COMPILER"] = infos.compilers['c']
+ infos.cmake["CMAKE_CXX_COMPILER"] = infos.compilers['cpp']
+ infos.cmake["CMAKE_SYSTEM_NAME"] = system_name_map[data['DEB_HOST_ARCH_OS']]
+ infos.cmake["CMAKE_SYSTEM_PROCESSOR"] = system_processor_map.get(data['DEB_HOST_GNU_CPU'],
+ data['DEB_HOST_GNU_CPU'])
+
+def deb_compiler_lookup(infos: MachineInfo, compilerstems: T.List[T.Tuple[str, str]], host_arch: str, gccsuffix: str) -> None:
+ for langname, stem in compilerstems:
+ compilername = f'{host_arch}-{stem}{gccsuffix}'
+ try:
+ p = locate_path(compilername)
+ infos.compilers[langname] = p
+ except ValueError:
+ pass
+
+def detect_cross_debianlike(options: T.Any) -> MachineInfo:
+ if options.debarch is None:
+ cmd = ['dpkg-architecture']
+ else:
+ cmd = ['dpkg-architecture', '-a' + options.debarch]
+ output = subprocess.check_output(cmd, universal_newlines=True,
+ stderr=subprocess.DEVNULL)
+ data = {}
+ for line in output.split('\n'):
+ line = line.strip()
+ if line == '':
+ continue
+ k, v = line.split('=', 1)
+ data[k] = v
+ host_arch = data['DEB_HOST_GNU_TYPE']
+ host_os = data['DEB_HOST_ARCH_OS']
+ host_cpu_family = cpu_family_map.get(data['DEB_HOST_GNU_CPU'],
+ data['DEB_HOST_GNU_CPU'])
+ host_cpu = cpu_map.get(data['DEB_HOST_ARCH'],
+ data['DEB_HOST_ARCH'])
+ host_endian = data['DEB_HOST_ARCH_ENDIAN']
+
+ compilerstems = [('c', 'gcc'),
+ ('cpp', 'g++'),
+ ('objc', 'gobjc'),
+ ('objcpp', 'gobjc++')]
+ infos = MachineInfo()
+ deb_compiler_lookup(infos, compilerstems, host_arch, options.gccsuffix)
+ if len(infos.compilers) == 0:
+ print('Warning: no compilers were detected.')
+ infos.binaries['ar'] = locate_path("%s-ar" % host_arch)
+ infos.binaries['strip'] = locate_path("%s-strip" % host_arch)
+ infos.binaries['objcopy'] = locate_path("%s-objcopy" % host_arch)
+ infos.binaries['ld'] = locate_path("%s-ld" % host_arch)
+ try:
+ infos.binaries['cmake'] = locate_path("cmake")
+ deb_detect_cmake(infos, data)
+ except ValueError:
+ pass
+ try:
+ infos.binaries['pkgconfig'] = locate_path("%s-pkg-config" % host_arch)
+ except ValueError:
+ pass # pkg-config is optional
+ try:
+ infos.binaries['cups-config'] = locate_path("cups-config")
+ except ValueError:
+ pass
+ infos.system = host_os
+ infos.cpu_family = host_cpu_family
+ infos.cpu = host_cpu
+ infos.endian = host_endian
+
+ get_args_from_envvars(infos)
+ return infos
+
+def write_machine_file(infos: MachineInfo, ofilename: str, write_system_info: bool) -> None:
+ tmpfilename = ofilename + '~'
+ with open(tmpfilename, 'w', encoding='utf-8') as ofile:
+ ofile.write('[binaries]\n')
+ ofile.write('# Compilers\n')
+ for langname in sorted(infos.compilers.keys()):
+ compiler = infos.compilers[langname]
+ write_args_line(ofile, langname, compiler)
+ ofile.write('\n')
+
+ ofile.write('# Other binaries\n')
+ for exename in sorted(infos.binaries.keys()):
+ exe = infos.binaries[exename]
+ write_args_line(ofile, exename, exe)
+ ofile.write('\n')
+
+ ofile.write('[properties]\n')
+ all_langs = list(set(infos.compile_args.keys()).union(set(infos.link_args.keys())))
+ all_langs.sort()
+ for lang in all_langs:
+ if lang in infos.compile_args:
+ write_args_line(ofile, lang + '_args', infos.compile_args[lang])
+ if lang in infos.link_args:
+ write_args_line(ofile, lang + '_link_args', infos.link_args[lang])
+ for k, v in infos.properties.items():
+ write_args_line(ofile, k, v)
+ ofile.write('\n')
+
+ if infos.cmake:
+ ofile.write('[cmake]\n\n')
+ for k, v in infos.cmake.items():
+ write_args_line(ofile, k, v)
+ ofile.write('\n')
+
+ if write_system_info:
+ ofile.write('[host_machine]\n')
+ ofile.write(f"cpu = '{infos.cpu}'\n")
+ ofile.write(f"cpu_family = '{infos.cpu_family}'\n")
+ ofile.write(f"endian = '{infos.endian}'\n")
+ ofile.write(f"system = '{infos.system}'\n")
+ os.replace(tmpfilename, ofilename)
+
+def detect_language_args_from_envvars(langname: str, envvar_suffix: str = '') -> T.Tuple[T.List[str], T.List[str]]:
+ ldflags = tuple(shlex.split(os.environ.get('LDFLAGS' + envvar_suffix, '')))
+ compile_args = shlex.split(os.environ.get(compilers.CFLAGS_MAPPING[langname] + envvar_suffix, ''))
+ if langname in compilers.LANGUAGES_USING_CPPFLAGS:
+ cppflags = tuple(shlex.split(os.environ.get('CPPFLAGS' + envvar_suffix, '')))
+ lang_compile_args = list(cppflags) + compile_args
+ else:
+ lang_compile_args = compile_args
+ lang_link_args = list(ldflags) + compile_args
+ return (lang_compile_args, lang_link_args)
+
+def detect_compilers_from_envvars(envvar_suffix: str = '') -> MachineInfo:
+ infos = MachineInfo()
+ for langname, envvarname in envconfig.ENV_VAR_COMPILER_MAP.items():
+ compilerstr = os.environ.get(envvarname + envvar_suffix)
+ if not compilerstr:
+ continue
+ compiler = shlex.split(compilerstr)
+ infos.compilers[langname] = compiler
+ lang_compile_args, lang_link_args = detect_language_args_from_envvars(langname, envvar_suffix)
+ if lang_compile_args:
+ infos.compile_args[langname] = lang_compile_args
+ if lang_link_args:
+ infos.link_args[langname] = lang_link_args
+ return infos
+
+def detect_binaries_from_envvars(infos: MachineInfo, envvar_suffix: str = '') -> None:
+ for binname, envvar_base in envconfig.ENV_VAR_TOOL_MAP.items():
+ envvar = envvar_base + envvar_suffix
+ binstr = os.environ.get(envvar)
+ if binstr:
+ infos.binaries[binname] = shlex.split(binstr)
+
+def detect_cross_system(infos: MachineInfo, options: T.Any) -> None:
+ for optname in ('system', 'cpu', 'cpu_family', 'endian'):
+ v = getattr(options, optname)
+ if not v:
+ mlog.error(f'Cross property "{optname}" missing, set it with --{optname.replace("_", "-")}.')
+ sys.exit(1)
+ setattr(infos, optname, v)
+
+def detect_cross_env(options: T.Any) -> MachineInfo:
+ if options.debarch:
+ print('Detecting cross environment via dpkg-reconfigure.')
+ infos = detect_cross_debianlike(options)
+ else:
+ print('Detecting cross environment via environment variables.')
+ infos = detect_compilers_from_envvars()
+ detect_cross_system(infos, options)
+ return infos
+
+def add_compiler_if_missing(infos: MachineInfo, langname: str, exe_names: T.List[str]) -> None:
+ if langname in infos.compilers:
+ return
+ for exe_name in exe_names:
+ lookup = shutil.which(exe_name)
+ if not lookup:
+ continue
+ compflags, linkflags = detect_language_args_from_envvars(langname)
+ infos.compilers[langname] = [lookup]
+ if compflags:
+ infos.compile_args[langname] = compflags
+ if linkflags:
+ infos.link_args[langname] = linkflags
+ return
+
+def detect_missing_native_compilers(infos: MachineInfo) -> None:
+ # T.Any per-platform special detection should go here.
+ for langname, exes in compiler_names.items():
+ if langname not in envconfig.ENV_VAR_COMPILER_MAP:
+ continue
+ add_compiler_if_missing(infos, langname, exes)
+
+def detect_missing_native_binaries(infos: MachineInfo) -> None:
+ # T.Any per-platform special detection should go here.
+ for toolname in sorted(envconfig.ENV_VAR_TOOL_MAP.keys()):
+ if toolname in infos.binaries:
+ continue
+ exe = shutil.which(toolname)
+ if exe:
+ infos.binaries[toolname] = [exe]
+
+def detect_native_env(options: T.Any) -> MachineInfo:
+ use_for_build = has_for_build()
+ if use_for_build:
+ mlog.log('Using FOR_BUILD envvars for detection')
+ esuffix = '_FOR_BUILD'
+ else:
+ mlog.log('Using regular envvars for detection.')
+ esuffix = ''
+ infos = detect_compilers_from_envvars(esuffix)
+ detect_missing_native_compilers(infos)
+ detect_binaries_from_envvars(infos, esuffix)
+ detect_missing_native_binaries(infos)
+ return infos
+
+def run(options: T.Any) -> None:
+ if options.cross and options.native:
+ sys.exit('You can only specify either --cross or --native, not both.')
+ if not options.cross and not options.native:
+ sys.exit('You must specify --cross or --native.')
+ mlog.notice('This functionality is experimental and subject to change.')
+ detect_cross = options.cross
+ if detect_cross:
+ infos = detect_cross_env(options)
+ write_system_info = True
+ else:
+ infos = detect_native_env(options)
+ write_system_info = False
+ write_machine_file(infos, options.outfile, write_system_info)
diff --git a/mesonbuild/scripts/externalproject.py b/mesonbuild/scripts/externalproject.py
new file mode 100644
index 0000000..17c2251
--- /dev/null
+++ b/mesonbuild/scripts/externalproject.py
@@ -0,0 +1,116 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+import os
+import argparse
+import multiprocessing
+import subprocess
+from pathlib import Path
+import typing as T
+
+from ..mesonlib import Popen_safe, split_args
+
+class ExternalProject:
+ def __init__(self, options: argparse.Namespace):
+ self.name = options.name
+ self.src_dir = options.srcdir
+ self.build_dir = options.builddir
+ self.install_dir = options.installdir
+ self.log_dir = options.logdir
+ self.verbose = options.verbose
+ self.stampfile = options.stampfile
+ self.depfile = options.depfile
+ self.make = split_args(options.make)
+
+ def write_depfile(self) -> None:
+ with open(self.depfile, 'w', encoding='utf-8') as f:
+ f.write(f'{self.stampfile}: \\\n')
+ for dirpath, dirnames, filenames in os.walk(self.src_dir):
+ dirnames[:] = [d for d in dirnames if not d.startswith('.')]
+ for fname in filenames:
+ if fname.startswith('.'):
+ continue
+ path = Path(dirpath, fname)
+ f.write(' {} \\\n'.format(path.as_posix().replace(' ', '\\ ')))
+
+ def write_stampfile(self) -> None:
+ with open(self.stampfile, 'w', encoding='utf-8'):
+ pass
+
+ def supports_jobs_flag(self) -> bool:
+ p, o, e = Popen_safe(self.make + ['--version'])
+ if p.returncode == 0 and ('GNU Make' in o or 'waf' in o):
+ return True
+ return False
+
+ def build(self) -> int:
+ make_cmd = self.make.copy()
+ if self.supports_jobs_flag():
+ make_cmd.append(f'-j{multiprocessing.cpu_count()}')
+ rc = self._run('build', make_cmd)
+ if rc != 0:
+ return rc
+
+ install_cmd = self.make.copy()
+ install_env = {}
+ install_env['DESTDIR'] = self.install_dir
+ install_cmd.append('install')
+ rc = self._run('install', install_cmd, install_env)
+ if rc != 0:
+ return rc
+
+ self.write_depfile()
+ self.write_stampfile()
+
+ return 0
+
+ def _run(self, step: str, command: T.List[str], env: T.Optional[T.Dict[str, str]] = None) -> int:
+ m = 'Running command ' + str(command) + ' in directory ' + str(self.build_dir) + '\n'
+ log_filename = Path(self.log_dir, f'{self.name}-{step}.log')
+ output = None
+ if not self.verbose:
+ output = open(log_filename, 'w', encoding='utf-8')
+ output.write(m + '\n')
+ output.flush()
+ else:
+ print(m)
+ run_env = os.environ.copy()
+ if env:
+ run_env.update(env)
+ p, o, e = Popen_safe(command, stderr=subprocess.STDOUT, stdout=output,
+ cwd=self.build_dir,
+ env=run_env)
+ if p.returncode != 0:
+ m = f'{step} step returned error code {p.returncode}.'
+ if not self.verbose:
+ m += '\nSee logs: ' + str(log_filename)
+ print(m)
+ return p.returncode
+
+def run(args: T.List[str]) -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--name')
+ parser.add_argument('--srcdir')
+ parser.add_argument('--builddir')
+ parser.add_argument('--installdir')
+ parser.add_argument('--logdir')
+ parser.add_argument('--make')
+ parser.add_argument('--verbose', action='store_true')
+ parser.add_argument('stampfile')
+ parser.add_argument('depfile')
+
+ options = parser.parse_args(args)
+ ep = ExternalProject(options)
+ return ep.build()
diff --git a/mesonbuild/scripts/gettext.py b/mesonbuild/scripts/gettext.py
new file mode 100644
index 0000000..4a6bb9c
--- /dev/null
+++ b/mesonbuild/scripts/gettext.py
@@ -0,0 +1,96 @@
+# Copyright 2016 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.
+from __future__ import annotations
+
+import os
+import argparse
+import subprocess
+import typing as T
+
+parser = argparse.ArgumentParser()
+parser.add_argument('command')
+parser.add_argument('--pkgname', default='')
+parser.add_argument('--datadirs', default='')
+parser.add_argument('--langs', default='')
+parser.add_argument('--localedir', default='')
+parser.add_argument('--source-root', default='')
+parser.add_argument('--subdir', default='')
+parser.add_argument('--xgettext', default='xgettext')
+parser.add_argument('--msgmerge', default='msgmerge')
+parser.add_argument('--msginit', default='msginit')
+parser.add_argument('--extra-args', default='')
+
+def read_linguas(src_sub: str) -> T.List[str]:
+ # Syntax of this file is documented here:
+ # https://www.gnu.org/software/gettext/manual/html_node/po_002fLINGUAS.html
+ linguas = os.path.join(src_sub, 'LINGUAS')
+ try:
+ langs = []
+ with open(linguas, encoding='utf-8') as f:
+ for line in f:
+ line = line.strip()
+ if line and not line.startswith('#'):
+ langs += line.split()
+ return langs
+ except (FileNotFoundError, PermissionError):
+ print(f'Could not find file LINGUAS in {src_sub}')
+ return []
+
+def run_potgen(src_sub: str, xgettext: str, pkgname: str, datadirs: str, args: T.List[str], source_root: str) -> int:
+ listfile = os.path.join(src_sub, 'POTFILES.in')
+ if not os.path.exists(listfile):
+ listfile = os.path.join(src_sub, 'POTFILES')
+ if not os.path.exists(listfile):
+ print('Could not find file POTFILES in %s' % src_sub)
+ return 1
+
+ child_env = os.environ.copy()
+ if datadirs:
+ child_env['GETTEXTDATADIRS'] = datadirs
+
+ ofile = os.path.join(src_sub, pkgname + '.pot')
+ return subprocess.call([xgettext, '--package-name=' + pkgname, '-p', src_sub, '-f', listfile,
+ '-D', source_root, '-k_', '-o', ofile] + args,
+ env=child_env)
+
+def update_po(src_sub: str, msgmerge: str, msginit: str, pkgname: str, langs: T.List[str]) -> int:
+ potfile = os.path.join(src_sub, pkgname + '.pot')
+ for l in langs:
+ pofile = os.path.join(src_sub, l + '.po')
+ if os.path.exists(pofile):
+ subprocess.check_call([msgmerge, '-q', '-o', pofile, pofile, potfile])
+ else:
+ subprocess.check_call([msginit, '--input', potfile, '--output-file', pofile, '--locale', l, '--no-translator'])
+ return 0
+
+def run(args: T.List[str]) -> int:
+ options = parser.parse_args(args)
+ subcmd = options.command
+ langs = options.langs.split('@@') if options.langs else None
+ extra_args = options.extra_args.split('@@') if options.extra_args else []
+ subdir = options.subdir
+ src_sub = os.path.join(options.source_root, subdir)
+
+ if not langs:
+ langs = read_linguas(src_sub)
+
+ if subcmd == 'pot':
+ return run_potgen(src_sub, options.xgettext, options.pkgname, options.datadirs, extra_args, options.source_root)
+ elif subcmd == 'update_po':
+ if run_potgen(src_sub, options.xgettext, options.pkgname, options.datadirs, extra_args, options.source_root) != 0:
+ return 1
+ return update_po(src_sub, options.msgmerge, options.msginit, options.pkgname, langs)
+ else:
+ print('Unknown subcommand.')
+ return 1
diff --git a/mesonbuild/scripts/gtkdochelper.py b/mesonbuild/scripts/gtkdochelper.py
new file mode 100644
index 0000000..ded952d
--- /dev/null
+++ b/mesonbuild/scripts/gtkdochelper.py
@@ -0,0 +1,296 @@
+# Copyright 2015-2016 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.
+from __future__ import annotations
+
+import sys, os
+import subprocess
+import shutil
+import argparse
+from ..mesonlib import MesonException, Popen_safe, is_windows, is_cygwin, split_args
+from . import destdir_join
+import typing as T
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument('--sourcedir', dest='sourcedir')
+parser.add_argument('--builddir', dest='builddir')
+parser.add_argument('--subdir', dest='subdir')
+parser.add_argument('--headerdirs', dest='headerdirs')
+parser.add_argument('--mainfile', dest='mainfile')
+parser.add_argument('--modulename', dest='modulename')
+parser.add_argument('--moduleversion', dest='moduleversion')
+parser.add_argument('--htmlargs', dest='htmlargs', default='')
+parser.add_argument('--scanargs', dest='scanargs', default='')
+parser.add_argument('--scanobjsargs', dest='scanobjsargs', default='')
+parser.add_argument('--gobjects-types-file', dest='gobject_typesfile', default='')
+parser.add_argument('--fixxrefargs', dest='fixxrefargs', default='')
+parser.add_argument('--mkdbargs', dest='mkdbargs', default='')
+parser.add_argument('--ld', dest='ld', default='')
+parser.add_argument('--cc', dest='cc', default='')
+parser.add_argument('--ldflags', dest='ldflags', default='')
+parser.add_argument('--cflags', dest='cflags', default='')
+parser.add_argument('--content-files', dest='content_files', default='')
+parser.add_argument('--expand-content-files', dest='expand_content_files', default='')
+parser.add_argument('--html-assets', dest='html_assets', default='')
+parser.add_argument('--ignore-headers', dest='ignore_headers', default='')
+parser.add_argument('--namespace', dest='namespace', default='')
+parser.add_argument('--mode', dest='mode', default='')
+parser.add_argument('--installdir', dest='install_dir')
+parser.add_argument('--run', dest='run', default='')
+for tool in ['scan', 'scangobj', 'mkdb', 'mkhtml', 'fixxref']:
+ program_name = 'gtkdoc-' + tool
+ parser.add_argument('--' + program_name, dest=program_name.replace('-', '_'))
+
+def gtkdoc_run_check(cmd: T.List[str], cwd: str, library_paths: T.Optional[T.List[str]] = None) -> None:
+ if library_paths is None:
+ library_paths = []
+
+ env = dict(os.environ)
+ if is_windows() or is_cygwin():
+ if 'PATH' in env:
+ library_paths.extend(env['PATH'].split(os.pathsep))
+ env['PATH'] = os.pathsep.join(library_paths)
+ else:
+ if 'LD_LIBRARY_PATH' in env:
+ library_paths.extend(env['LD_LIBRARY_PATH'].split(os.pathsep))
+ env['LD_LIBRARY_PATH'] = os.pathsep.join(library_paths)
+
+ if is_windows():
+ cmd.insert(0, sys.executable)
+
+ # Put stderr into stdout since we want to print it out anyway.
+ # This preserves the order of messages.
+ p, out = Popen_safe(cmd, cwd=cwd, env=env, stderr=subprocess.STDOUT)[0:2]
+ if p.returncode != 0:
+ err_msg = [f"{cmd!r} failed with status {p.returncode:d}"]
+ if out:
+ err_msg.append(out)
+ raise MesonException('\n'.join(err_msg))
+ elif out:
+ # Unfortunately Windows cmd.exe consoles may be using a codepage
+ # that might choke print() with a UnicodeEncodeError, so let's
+ # ignore such errors for now, as a compromise as we are outputting
+ # console output here...
+ try:
+ print(out)
+ except UnicodeEncodeError:
+ pass
+
+def build_gtkdoc(source_root: str, build_root: str, doc_subdir: str, src_subdirs: T.List[str],
+ main_file: str, module: str, module_version: str,
+ html_args: T.List[str], scan_args: T.List[str], fixxref_args: T.List[str], mkdb_args: T.List[str],
+ gobject_typesfile: str, scanobjs_args: T.List[str], run: str, ld: str, cc: str, ldflags: str, cflags: str,
+ html_assets: T.List[str], content_files: T.List[str], ignore_headers: T.List[str], namespace: str,
+ expand_content_files: T.List[str], mode: str, options: argparse.Namespace) -> None:
+ print("Building documentation for %s" % module)
+
+ src_dir_args = []
+ for src_dir in src_subdirs:
+ if not os.path.isabs(src_dir):
+ dirs = [os.path.join(source_root, src_dir),
+ os.path.join(build_root, src_dir)]
+ else:
+ dirs = [src_dir]
+ src_dir_args += ['--source-dir=' + d for d in dirs]
+
+ doc_src = os.path.join(source_root, doc_subdir)
+ abs_out = os.path.join(build_root, doc_subdir)
+ htmldir = os.path.join(abs_out, 'html')
+
+ content_files += [main_file]
+ sections = os.path.join(doc_src, module + "-sections.txt")
+ if os.path.exists(sections):
+ content_files.append(sections)
+
+ overrides = os.path.join(doc_src, module + "-overrides.txt")
+ if os.path.exists(overrides):
+ content_files.append(overrides)
+
+ # Copy files to build directory
+ for f in content_files:
+ # FIXME: Use mesonlib.File objects so we don't need to do this
+ if not os.path.isabs(f):
+ f = os.path.join(doc_src, f)
+ elif os.path.commonpath([f, build_root]) == build_root:
+ continue
+ shutil.copyfile(f, os.path.join(abs_out, os.path.basename(f)))
+
+ shutil.rmtree(htmldir, ignore_errors=True)
+ try:
+ os.mkdir(htmldir)
+ except Exception:
+ pass
+
+ for f in html_assets:
+ f_abs = os.path.join(doc_src, f)
+ shutil.copyfile(f_abs, os.path.join(htmldir, os.path.basename(f_abs)))
+
+ scan_cmd = [options.gtkdoc_scan, '--module=' + module] + src_dir_args
+ if ignore_headers:
+ scan_cmd.append('--ignore-headers=' + ' '.join(ignore_headers))
+ # Add user-specified arguments
+ scan_cmd += scan_args
+ gtkdoc_run_check(scan_cmd, abs_out)
+
+ # Use the generated types file when available, otherwise gobject_typesfile
+ # would often be a path to source dir instead of build dir.
+ if '--rebuild-types' in scan_args:
+ gobject_typesfile = os.path.join(abs_out, module + '.types')
+
+ if gobject_typesfile:
+ scanobjs_cmd = [options.gtkdoc_scangobj] + scanobjs_args
+ scanobjs_cmd += ['--types=' + gobject_typesfile,
+ '--module=' + module,
+ '--run=' + run,
+ '--cflags=' + cflags,
+ '--ldflags=' + ldflags,
+ '--cc=' + cc,
+ '--ld=' + ld,
+ '--output-dir=' + abs_out]
+
+ library_paths = []
+ for ldflag in split_args(ldflags):
+ if ldflag.startswith('-Wl,-rpath,'):
+ library_paths.append(ldflag[11:])
+
+ gtkdoc_run_check(scanobjs_cmd, build_root, library_paths)
+
+ # Make docbook files
+ if mode == 'auto':
+ # Guessing is probably a poor idea but these keeps compat
+ # with previous behavior
+ if main_file.endswith('sgml'):
+ modeflag = '--sgml-mode'
+ else:
+ modeflag = '--xml-mode'
+ elif mode == 'xml':
+ modeflag = '--xml-mode'
+ elif mode == 'sgml':
+ modeflag = '--sgml-mode'
+ else: # none
+ modeflag = None
+
+ mkdb_cmd = [options.gtkdoc_mkdb,
+ '--module=' + module,
+ '--output-format=xml',
+ '--expand-content-files=' + ' '.join(expand_content_files),
+ ] + src_dir_args
+ if namespace:
+ mkdb_cmd.append('--name-space=' + namespace)
+ if modeflag:
+ mkdb_cmd.append(modeflag)
+ if main_file:
+ # Yes, this is the flag even if the file is in xml.
+ mkdb_cmd.append('--main-sgml-file=' + main_file)
+ # Add user-specified arguments
+ mkdb_cmd += mkdb_args
+ gtkdoc_run_check(mkdb_cmd, abs_out)
+
+ # Make HTML documentation
+ mkhtml_cmd = [options.gtkdoc_mkhtml,
+ '--path=' + os.pathsep.join((doc_src, abs_out)),
+ module,
+ ] + html_args
+ if main_file:
+ mkhtml_cmd.append('../' + main_file)
+ else:
+ mkhtml_cmd.append('%s-docs.xml' % module)
+ # html gen must be run in the HTML dir
+ gtkdoc_run_check(mkhtml_cmd, htmldir)
+
+ # Fix cross-references in HTML files
+ fixref_cmd = [options.gtkdoc_fixxref,
+ '--module=' + module,
+ '--module-dir=html'] + fixxref_args
+ gtkdoc_run_check(fixref_cmd, abs_out)
+
+ if module_version:
+ shutil.move(os.path.join(htmldir, f'{module}.devhelp2'),
+ os.path.join(htmldir, f'{module}-{module_version}.devhelp2'))
+
+def install_gtkdoc(build_root: str, doc_subdir: str, install_prefix: str, datadir: str, module: str) -> None:
+ source = os.path.join(build_root, doc_subdir, 'html')
+ final_destination = os.path.join(install_prefix, datadir, module)
+ shutil.rmtree(final_destination, ignore_errors=True)
+ shutil.copytree(source, final_destination)
+
+def run(args: T.List[str]) -> int:
+ options = parser.parse_args(args)
+ if options.htmlargs:
+ htmlargs = options.htmlargs.split('@@')
+ else:
+ htmlargs = []
+ if options.scanargs:
+ scanargs = options.scanargs.split('@@')
+ else:
+ scanargs = []
+ if options.scanobjsargs:
+ scanobjsargs = options.scanobjsargs.split('@@')
+ else:
+ scanobjsargs = []
+ if options.fixxrefargs:
+ fixxrefargs = options.fixxrefargs.split('@@')
+ else:
+ fixxrefargs = []
+ if options.mkdbargs:
+ mkdbargs = options.mkdbargs.split('@@')
+ else:
+ mkdbargs = []
+ build_gtkdoc(
+ options.sourcedir,
+ options.builddir,
+ options.subdir,
+ options.headerdirs.split('@@'),
+ options.mainfile,
+ options.modulename,
+ options.moduleversion,
+ htmlargs,
+ scanargs,
+ fixxrefargs,
+ mkdbargs,
+ options.gobject_typesfile,
+ scanobjsargs,
+ options.run,
+ options.ld,
+ options.cc,
+ options.ldflags,
+ options.cflags,
+ options.html_assets.split('@@') if options.html_assets else [],
+ options.content_files.split('@@') if options.content_files else [],
+ options.ignore_headers.split('@@') if options.ignore_headers else [],
+ options.namespace,
+ options.expand_content_files.split('@@') if options.expand_content_files else [],
+ options.mode,
+ options)
+
+ if 'MESON_INSTALL_PREFIX' in os.environ:
+ destdir = os.environ.get('DESTDIR', '')
+ install_prefix = destdir_join(destdir, os.environ['MESON_INSTALL_PREFIX'])
+ if options.install_dir:
+ install_dir = options.install_dir
+ else:
+ install_dir = options.modulename
+ if options.moduleversion:
+ install_dir += '-' + options.moduleversion
+ if os.path.isabs(install_dir):
+ install_dir = destdir_join(destdir, install_dir)
+ install_gtkdoc(options.builddir,
+ options.subdir,
+ install_prefix,
+ 'share/gtk-doc/html',
+ install_dir)
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(run(sys.argv[1:]))
diff --git a/mesonbuild/scripts/hotdochelper.py b/mesonbuild/scripts/hotdochelper.py
new file mode 100644
index 0000000..2c3b85d
--- /dev/null
+++ b/mesonbuild/scripts/hotdochelper.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+import os
+import shutil
+import subprocess
+
+from . import destdir_join
+
+import argparse
+import typing as T
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--install')
+parser.add_argument('--extra-extension-path', action="append", default=[])
+parser.add_argument('--name')
+parser.add_argument('--builddir')
+parser.add_argument('--project-version')
+
+
+def run(argv: T.List[str]) -> int:
+ options, args = parser.parse_known_args(argv)
+ subenv = os.environ.copy()
+
+ for ext_path in options.extra_extension_path:
+ subenv['PYTHONPATH'] = subenv.get('PYTHONPATH', '') + ':' + ext_path
+
+ res = subprocess.call(args, cwd=options.builddir, env=subenv)
+ if res != 0:
+ return res
+
+ if options.install:
+ source_dir = os.path.join(options.builddir, options.install)
+ destdir = os.environ.get('DESTDIR', '')
+ installdir = destdir_join(destdir,
+ os.path.join(os.environ['MESON_INSTALL_PREFIX'],
+ 'share/doc/', options.name, "html"))
+
+ shutil.rmtree(installdir, ignore_errors=True)
+ shutil.copytree(source_dir, installdir)
+ return 0
diff --git a/mesonbuild/scripts/itstool.py b/mesonbuild/scripts/itstool.py
new file mode 100644
index 0000000..0bfcaf9
--- /dev/null
+++ b/mesonbuild/scripts/itstool.py
@@ -0,0 +1,86 @@
+# Copyright 2016 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.
+from __future__ import annotations
+
+import os
+import argparse
+import subprocess
+import tempfile
+import shutil
+import typing as T
+
+parser = argparse.ArgumentParser()
+parser.add_argument('command')
+parser.add_argument('--build-dir', default='')
+parser.add_argument('-i', '--input', default='')
+parser.add_argument('-o', '--output', default='')
+parser.add_argument('--itstool', default='itstool')
+parser.add_argument('--its', action='append', default=[])
+parser.add_argument('mo_files', nargs='+')
+
+
+def run_join(build_dir: str, itstool: str, its_files: T.List[str], mo_files: T.List[str],
+ in_fname: str, out_fname: str) -> int:
+ if not mo_files:
+ print('No mo files specified to use for translation.')
+ return 1
+
+ with tempfile.TemporaryDirectory(prefix=os.path.basename(in_fname), dir=build_dir) as tmp_dir:
+ # copy mo files to have the right names so itstool can infer their locale
+ locale_mo_files = []
+ for mo_file in mo_files:
+ if not os.path.exists(mo_file):
+ print(f'Could not find mo file {mo_file}')
+ return 1
+ if not mo_file.endswith('.mo'):
+ print(f'File is not a mo file: {mo_file}')
+ return 1
+ # determine locale of this mo file
+ parts = mo_file.partition('LC_MESSAGES')
+ if parts[0].endswith((os.sep, '/')):
+ locale = os.path.basename(parts[0][:-1])
+ else:
+ locale = os.path.basename(parts[0])
+ tmp_mo_fname = os.path.join(tmp_dir, locale + '.mo')
+ shutil.copy(mo_file, tmp_mo_fname)
+ locale_mo_files.append(tmp_mo_fname)
+
+ cmd = [itstool]
+ if its_files:
+ for fname in its_files:
+ cmd.extend(['-i', fname])
+ cmd.extend(['-j', in_fname,
+ '-o', out_fname])
+ cmd.extend(locale_mo_files)
+
+ return subprocess.call(cmd)
+
+
+def run(args: T.List[str]) -> int:
+ options = parser.parse_args(args)
+ command = options.command
+ build_dir = os.environ.get('MESON_BUILD_ROOT', os.getcwd())
+ if options.build_dir:
+ build_dir = options.build_dir
+
+ if command == 'join':
+ return run_join(build_dir,
+ options.itstool,
+ options.its,
+ options.mo_files,
+ options.input,
+ options.output)
+ else:
+ print('Unknown subcommand.')
+ return 1
diff --git a/mesonbuild/scripts/meson_exe.py b/mesonbuild/scripts/meson_exe.py
new file mode 100644
index 0000000..33408d8
--- /dev/null
+++ b/mesonbuild/scripts/meson_exe.py
@@ -0,0 +1,124 @@
+# Copyright 2013-2016 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.
+from __future__ import annotations
+
+import os
+import sys
+import argparse
+import pickle
+import subprocess
+import typing as T
+import locale
+
+from ..utils.core import ExecutableSerialisation
+
+def buildparser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description='Custom executable wrapper for Meson. Do not run on your own, mmm\'kay?')
+ parser.add_argument('--unpickle')
+ parser.add_argument('--capture')
+ parser.add_argument('--feed')
+ return parser
+
+def run_exe(exe: ExecutableSerialisation, extra_env: T.Optional[T.Dict[str, str]] = None) -> int:
+ if exe.exe_wrapper:
+ if not exe.exe_wrapper.found():
+ raise AssertionError('BUG: Can\'t run cross-compiled exe {!r} with not-found '
+ 'wrapper {!r}'.format(exe.cmd_args[0], exe.exe_wrapper.get_path()))
+ cmd_args = exe.exe_wrapper.get_command() + exe.cmd_args
+ else:
+ cmd_args = exe.cmd_args
+ child_env = os.environ.copy()
+ if extra_env:
+ child_env.update(extra_env)
+ if exe.env:
+ child_env = exe.env.get_env(child_env)
+ if exe.extra_paths:
+ child_env['PATH'] = (os.pathsep.join(exe.extra_paths + ['']) +
+ child_env['PATH'])
+ if exe.exe_wrapper and any('wine' in i for i in exe.exe_wrapper.get_command()):
+ from .. import mesonlib
+ child_env['WINEPATH'] = mesonlib.get_wine_shortpath(
+ exe.exe_wrapper.get_command(),
+ ['Z:' + p for p in exe.extra_paths] + child_env.get('WINEPATH', '').split(';'),
+ exe.workdir
+ )
+
+ stdin = None
+ if exe.feed:
+ stdin = open(exe.feed, 'rb')
+
+ pipe = subprocess.PIPE
+ if exe.verbose:
+ assert not exe.capture, 'Cannot capture and print to console at the same time'
+ pipe = None
+
+ p = subprocess.Popen(cmd_args, env=child_env, cwd=exe.workdir,
+ close_fds=False, stdin=stdin, stdout=pipe, stderr=pipe)
+ stdout, stderr = p.communicate()
+
+ if stdin is not None:
+ stdin.close()
+
+ if p.returncode == 0xc0000135:
+ # STATUS_DLL_NOT_FOUND on Windows indicating a common problem that is otherwise hard to diagnose
+ raise FileNotFoundError('due to missing DLLs')
+
+ if p.returncode != 0:
+ if exe.pickled:
+ print(f'while executing {cmd_args!r}')
+ if exe.verbose:
+ return p.returncode
+ encoding = locale.getpreferredencoding()
+ if not exe.capture:
+ print('--- stdout ---')
+ print(stdout.decode(encoding=encoding, errors='replace'))
+ print('--- stderr ---')
+ print(stderr.decode(encoding=encoding, errors='replace'))
+ return p.returncode
+
+ if exe.capture:
+ skip_write = False
+ try:
+ with open(exe.capture, 'rb') as cur:
+ skip_write = cur.read() == stdout
+ except OSError:
+ pass
+ if not skip_write:
+ with open(exe.capture, 'wb') as output:
+ output.write(stdout)
+
+ return 0
+
+def run(args: T.List[str]) -> int:
+ parser = buildparser()
+ options, cmd_args = parser.parse_known_args(args)
+ # argparse supports double dash to separate options and positional arguments,
+ # but the user has to remove it manually.
+ if cmd_args and cmd_args[0] == '--':
+ cmd_args = cmd_args[1:]
+ if not options.unpickle and not cmd_args:
+ parser.error('either --unpickle or executable and arguments are required')
+ if options.unpickle:
+ if cmd_args or options.capture or options.feed:
+ parser.error('no other arguments can be used with --unpickle')
+ with open(options.unpickle, 'rb') as f:
+ exe = pickle.load(f)
+ exe.pickled = True
+ else:
+ exe = ExecutableSerialisation(cmd_args, capture=options.capture, feed=options.feed)
+
+ return run_exe(exe)
+
+if __name__ == '__main__':
+ sys.exit(run(sys.argv[1:]))
diff --git a/mesonbuild/scripts/msgfmthelper.py b/mesonbuild/scripts/msgfmthelper.py
new file mode 100644
index 0000000..28bcc8b
--- /dev/null
+++ b/mesonbuild/scripts/msgfmthelper.py
@@ -0,0 +1,39 @@
+# Copyright 2016 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.
+from __future__ import annotations
+
+import argparse
+import subprocess
+import os
+import typing as T
+
+parser = argparse.ArgumentParser()
+parser.add_argument('input')
+parser.add_argument('output')
+parser.add_argument('type')
+parser.add_argument('podir')
+parser.add_argument('--msgfmt', default='msgfmt')
+parser.add_argument('--datadirs', default='')
+parser.add_argument('args', default=[], metavar='extra msgfmt argument', nargs='*')
+
+
+def run(args: T.List[str]) -> int:
+ options = parser.parse_args(args)
+ env = None
+ if options.datadirs:
+ env = os.environ.copy()
+ env.update({'GETTEXTDATADIRS': options.datadirs})
+ return subprocess.call([options.msgfmt, '--' + options.type, '-d', options.podir,
+ '--template', options.input, '-o', options.output] + options.args,
+ env=env)
diff --git a/mesonbuild/scripts/regen_checker.py b/mesonbuild/scripts/regen_checker.py
new file mode 100644
index 0000000..f3a6f3c
--- /dev/null
+++ b/mesonbuild/scripts/regen_checker.py
@@ -0,0 +1,65 @@
+# Copyright 2015-2016 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.
+from __future__ import annotations
+
+import sys, os
+import pickle, subprocess
+import typing as T
+from ..coredata import CoreData
+from ..backend.backends import RegenInfo
+from ..mesonlib import OptionKey
+
+# This could also be used for XCode.
+
+def need_regen(regeninfo: RegenInfo, regen_timestamp: float) -> bool:
+ for i in regeninfo.depfiles:
+ curfile = os.path.join(regeninfo.build_dir, i)
+ curtime = os.stat(curfile).st_mtime
+ if curtime > regen_timestamp:
+ return True
+ # The timestamp file gets automatically deleted by MSBuild during a 'Clean' build.
+ # We must make sure to recreate it, even if we do not regenerate the solution.
+ # Otherwise, Visual Studio will always consider the REGEN project out of date.
+ print("Everything is up-to-date, regeneration of build files is not needed.")
+ from ..backend.vs2010backend import Vs2010Backend
+ Vs2010Backend.touch_regen_timestamp(regeninfo.build_dir)
+ return False
+
+def regen(regeninfo: RegenInfo, meson_command: T.List[str], backend: str) -> None:
+ cmd = meson_command + ['--internal',
+ 'regenerate',
+ regeninfo.build_dir,
+ regeninfo.source_dir,
+ '--backend=' + backend]
+ subprocess.check_call(cmd)
+
+def run(args: T.List[str]) -> int:
+ private_dir = args[0]
+ dumpfile = os.path.join(private_dir, 'regeninfo.dump')
+ coredata_file = os.path.join(private_dir, 'coredata.dat')
+ with open(dumpfile, 'rb') as f:
+ regeninfo = pickle.load(f)
+ assert isinstance(regeninfo, RegenInfo)
+ with open(coredata_file, 'rb') as f:
+ coredata = pickle.load(f)
+ assert isinstance(coredata, CoreData)
+ backend = coredata.get_option(OptionKey('backend'))
+ assert isinstance(backend, str)
+ regen_timestamp = os.stat(dumpfile).st_mtime
+ if need_regen(regeninfo, regen_timestamp):
+ regen(regeninfo, coredata.meson_command, backend)
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(run(sys.argv[1:]))
diff --git a/mesonbuild/scripts/run_tool.py b/mesonbuild/scripts/run_tool.py
new file mode 100644
index 0000000..88376dd
--- /dev/null
+++ b/mesonbuild/scripts/run_tool.py
@@ -0,0 +1,68 @@
+# Copyright 2018 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.
+from __future__ import annotations
+
+import itertools
+import fnmatch
+from pathlib import Path
+from concurrent.futures import ThreadPoolExecutor
+
+from ..compilers import lang_suffixes
+from ..mesonlib import Popen_safe
+import typing as T
+
+if T.TYPE_CHECKING:
+ import subprocess
+
+def parse_pattern_file(fname: Path) -> T.List[str]:
+ patterns = []
+ try:
+ with fname.open(encoding='utf-8') as f:
+ for line in f:
+ pattern = line.strip()
+ if pattern and not pattern.startswith('#'):
+ patterns.append(pattern)
+ except FileNotFoundError:
+ pass
+ return patterns
+
+def run_tool(name: str, srcdir: Path, builddir: Path, fn: T.Callable[..., subprocess.CompletedProcess], *args: T.Any) -> int:
+ patterns = parse_pattern_file(srcdir / f'.{name}-include')
+ globs: T.Union[T.List[T.List[Path]], T.List[T.Generator[Path, None, None]]]
+ if patterns:
+ globs = [srcdir.glob(p) for p in patterns]
+ else:
+ p, o, _ = Popen_safe(['git', 'ls-files'], cwd=srcdir)
+ if p.returncode == 0:
+ globs = [[Path(srcdir, f) for f in o.splitlines()]]
+ else:
+ globs = [srcdir.glob('**/*')]
+ patterns = parse_pattern_file(srcdir / f'.{name}-ignore')
+ ignore = [str(builddir / '*')]
+ ignore.extend([str(srcdir / p) for p in patterns])
+ suffixes = set(lang_suffixes['c']).union(set(lang_suffixes['cpp']))
+ suffixes.add('h')
+ suffixes = {f'.{s}' for s in suffixes}
+ futures = []
+ returncode = 0
+ with ThreadPoolExecutor() as e:
+ for f in itertools.chain(*globs):
+ strf = str(f)
+ if f.is_dir() or f.suffix not in suffixes or \
+ any(fnmatch.fnmatch(strf, i) for i in ignore):
+ continue
+ futures.append(e.submit(fn, f, *args))
+ if futures:
+ returncode = max(x.result().returncode for x in futures)
+ return returncode
diff --git a/mesonbuild/scripts/scanbuild.py b/mesonbuild/scripts/scanbuild.py
new file mode 100644
index 0000000..9cfc75d
--- /dev/null
+++ b/mesonbuild/scripts/scanbuild.py
@@ -0,0 +1,66 @@
+# Copyright 2016 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.
+from __future__ import annotations
+
+import subprocess
+import shutil
+import tempfile
+from ..environment import detect_ninja, detect_scanbuild
+from ..coredata import get_cmd_line_file, CmdLineFileParser
+from ..mesonlib import windows_proof_rmtree
+from pathlib import Path
+import typing as T
+from ast import literal_eval
+import os
+
+def scanbuild(exelist: T.List[str], srcdir: Path, blddir: Path, privdir: Path, logdir: Path, args: T.List[str]) -> int:
+ # In case of problems leave the temp directory around
+ # so it can be debugged.
+ scandir = tempfile.mkdtemp(dir=str(privdir))
+ meson_cmd = exelist + args
+ build_cmd = exelist + ['-o', str(logdir)] + detect_ninja() + ['-C', scandir]
+ rc = subprocess.call(meson_cmd + [str(srcdir), scandir])
+ if rc != 0:
+ return rc
+ rc = subprocess.call(build_cmd)
+ if rc == 0:
+ windows_proof_rmtree(scandir)
+ return rc
+
+def run(args: T.List[str]) -> int:
+ srcdir = Path(args[0])
+ bldpath = Path(args[1])
+ blddir = args[1]
+ meson_cmd = args[2:]
+ privdir = bldpath / 'meson-private'
+ logdir = bldpath / 'meson-logs' / 'scanbuild'
+ shutil.rmtree(str(logdir), ignore_errors=True)
+
+ # if any cross or native files are specified we should use them
+ cmd = get_cmd_line_file(blddir)
+ data = CmdLineFileParser()
+ data.read(cmd)
+
+ if 'cross_file' in data['properties']:
+ meson_cmd.extend([f'--cross-file={os.path.abspath(f)}' for f in literal_eval(data['properties']['cross_file'])])
+
+ if 'native_file' in data['properties']:
+ meson_cmd.extend([f'--native-file={os.path.abspath(f)}' for f in literal_eval(data['properties']['native_file'])])
+
+ exelist = detect_scanbuild()
+ if not exelist:
+ print('Could not execute scan-build "%s"' % ' '.join(exelist))
+ return 1
+
+ return scanbuild(exelist, srcdir, bldpath, privdir, logdir, meson_cmd)
diff --git a/mesonbuild/scripts/symbolextractor.py b/mesonbuild/scripts/symbolextractor.py
new file mode 100644
index 0000000..08d839b
--- /dev/null
+++ b/mesonbuild/scripts/symbolextractor.py
@@ -0,0 +1,333 @@
+# Copyright 2013-2016 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.
+
+# This script extracts the symbols of a given shared library
+# into a file. If the symbols have not changed, the file is not
+# touched. This information is used to skip link steps if the
+# ABI has not changed.
+
+# This file is basically a reimplementation of
+# http://cgit.freedesktop.org/libreoffice/core/commit/?id=3213cd54b76bc80a6f0516aac75a48ff3b2ad67c
+from __future__ import annotations
+
+import typing as T
+import os, sys
+from .. import mesonlib
+from .. import mlog
+from ..mesonlib import Popen_safe
+import argparse
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument('--cross-host', default=None, dest='cross_host',
+ help='cross compilation host platform')
+parser.add_argument('args', nargs='+')
+
+TOOL_WARNING_FILE = None
+RELINKING_WARNING = 'Relinking will always happen on source changes.'
+
+def dummy_syms(outfilename: str) -> None:
+ """Just touch it so relinking happens always."""
+ with open(outfilename, 'w', encoding='utf-8'):
+ pass
+
+def write_if_changed(text: str, outfilename: str) -> None:
+ try:
+ with open(outfilename, encoding='utf-8') as f:
+ oldtext = f.read()
+ if text == oldtext:
+ return
+ except FileNotFoundError:
+ pass
+ with open(outfilename, 'w', encoding='utf-8') as f:
+ f.write(text)
+
+def print_tool_warning(tools: T.List[str], msg: str, stderr: T.Optional[str] = None) -> None:
+ if os.path.exists(TOOL_WARNING_FILE):
+ return
+ m = f'{tools!r} {msg}. {RELINKING_WARNING}'
+ if stderr:
+ m += '\n' + stderr
+ mlog.warning(m)
+ # Write it out so we don't warn again
+ with open(TOOL_WARNING_FILE, 'w', encoding='utf-8'):
+ pass
+
+def get_tool(name: str) -> T.List[str]:
+ evar = name.upper()
+ if evar in os.environ:
+ import shlex
+ return shlex.split(os.environ[evar])
+ return [name]
+
+def call_tool(name: str, args: T.List[str], **kwargs: T.Any) -> str:
+ tool = get_tool(name)
+ try:
+ p, output, e = Popen_safe(tool + args, **kwargs)
+ except FileNotFoundError:
+ print_tool_warning(tool, 'not found')
+ return None
+ except PermissionError:
+ print_tool_warning(tool, 'not usable')
+ return None
+ if p.returncode != 0:
+ print_tool_warning(tool, 'does not work', e)
+ return None
+ return output
+
+def call_tool_nowarn(tool: T.List[str], **kwargs: T.Any) -> T.Tuple[str, str]:
+ try:
+ p, output, e = Popen_safe(tool, **kwargs)
+ except FileNotFoundError:
+ return None, '{!r} not found\n'.format(tool[0])
+ except PermissionError:
+ return None, '{!r} not usable\n'.format(tool[0])
+ if p.returncode != 0:
+ return None, e
+ return output, None
+
+def gnu_syms(libfilename: str, outfilename: str) -> None:
+ # Get the name of the library
+ output = call_tool('readelf', ['-d', libfilename])
+ if not output:
+ dummy_syms(outfilename)
+ return
+ result = [x for x in output.split('\n') if 'SONAME' in x]
+ assert len(result) <= 1
+ # Get a list of all symbols exported
+ output = call_tool('nm', ['--dynamic', '--extern-only', '--defined-only',
+ '--format=posix', libfilename])
+ if not output:
+ dummy_syms(outfilename)
+ return
+ for line in output.split('\n'):
+ if not line:
+ continue
+ line_split = line.split()
+ entry = line_split[0:2]
+ # Store the size of symbols pointing to data objects so we relink
+ # when those change, which is needed because of copy relocations
+ # https://github.com/mesonbuild/meson/pull/7132#issuecomment-628353702
+ if line_split[1].upper() in {'B', 'G', 'D'} and len(line_split) >= 4:
+ entry += [line_split[3]]
+ result += [' '.join(entry)]
+ write_if_changed('\n'.join(result) + '\n', outfilename)
+
+def solaris_syms(libfilename: str, outfilename: str) -> None:
+ # gnu_syms() works with GNU nm & readelf, not Solaris nm & elfdump
+ origpath = os.environ['PATH']
+ try:
+ os.environ['PATH'] = '/usr/gnu/bin:' + origpath
+ gnu_syms(libfilename, outfilename)
+ finally:
+ os.environ['PATH'] = origpath
+
+def osx_syms(libfilename: str, outfilename: str) -> None:
+ # Get the name of the library
+ output = call_tool('otool', ['-l', libfilename])
+ if not output:
+ dummy_syms(outfilename)
+ return
+ arr = output.split('\n')
+ for (i, val) in enumerate(arr):
+ if 'LC_ID_DYLIB' in val:
+ match = i
+ break
+ result = [arr[match + 2], arr[match + 5]] # Libreoffice stores all 5 lines but the others seem irrelevant.
+ # Get a list of all symbols exported
+ output = call_tool('nm', ['--extern-only', '--defined-only',
+ '--format=posix', libfilename])
+ if not output:
+ dummy_syms(outfilename)
+ return
+ result += [' '.join(x.split()[0:2]) for x in output.split('\n')]
+ write_if_changed('\n'.join(result) + '\n', outfilename)
+
+def openbsd_syms(libfilename: str, outfilename: str) -> None:
+ # Get the name of the library
+ output = call_tool('readelf', ['-d', libfilename])
+ if not output:
+ dummy_syms(outfilename)
+ return
+ result = [x for x in output.split('\n') if 'SONAME' in x]
+ assert len(result) <= 1
+ # Get a list of all symbols exported
+ output = call_tool('nm', ['-D', '-P', '-g', libfilename])
+ if not output:
+ dummy_syms(outfilename)
+ return
+ # U = undefined (cope with the lack of --defined-only option)
+ result += [' '.join(x.split()[0:2]) for x in output.split('\n') if x and not x.endswith('U ')]
+ write_if_changed('\n'.join(result) + '\n', outfilename)
+
+def freebsd_syms(libfilename: str, outfilename: str) -> None:
+ # Get the name of the library
+ output = call_tool('readelf', ['-d', libfilename])
+ if not output:
+ dummy_syms(outfilename)
+ return
+ result = [x for x in output.split('\n') if 'SONAME' in x]
+ assert len(result) <= 1
+ # Get a list of all symbols exported
+ output = call_tool('nm', ['--dynamic', '--extern-only', '--defined-only',
+ '--format=posix', libfilename])
+ if not output:
+ dummy_syms(outfilename)
+ return
+
+ result += [' '.join(x.split()[0:2]) for x in output.split('\n')]
+ write_if_changed('\n'.join(result) + '\n', outfilename)
+
+def cygwin_syms(impfilename: str, outfilename: str) -> None:
+ # Get the name of the library
+ output = call_tool('dlltool', ['-I', impfilename])
+ if not output:
+ dummy_syms(outfilename)
+ return
+ result = [output]
+ # Get the list of all symbols exported
+ output = call_tool('nm', ['--extern-only', '--defined-only',
+ '--format=posix', impfilename])
+ if not output:
+ dummy_syms(outfilename)
+ return
+ for line in output.split('\n'):
+ if ' T ' not in line:
+ continue
+ result.append(line.split(maxsplit=1)[0])
+ write_if_changed('\n'.join(result) + '\n', outfilename)
+
+def _get_implib_dllname(impfilename: str) -> T.Tuple[T.List[str], str]:
+ all_stderr = ''
+ # First try lib.exe, which is provided by MSVC. Then llvm-lib.exe, by LLVM
+ # for clang-cl.
+ #
+ # We cannot call get_tool on `lib` because it will look at the `LIB` env
+ # var which is the list of library paths MSVC will search for import
+ # libraries while linking.
+ for lib in (['lib'], get_tool('llvm-lib')):
+ output, e = call_tool_nowarn(lib + ['-list', impfilename])
+ if output:
+ # The output is a list of DLLs that each symbol exported by the import
+ # library is available in. We only build import libraries that point to
+ # a single DLL, so we can pick any of these. Pick the last one for
+ # simplicity. Also skip the last line, which is empty.
+ return output.split('\n')[-2:-1], None
+ all_stderr += e
+ # Next, try dlltool.exe which is provided by MinGW
+ output, e = call_tool_nowarn(get_tool('dlltool') + ['-I', impfilename])
+ if output:
+ return [output], None
+ all_stderr += e
+ return ([], all_stderr)
+
+def _get_implib_exports(impfilename: str) -> T.Tuple[T.List[str], str]:
+ all_stderr = ''
+ # Force dumpbin.exe to use en-US so we can parse its output
+ env = os.environ.copy()
+ env['VSLANG'] = '1033'
+ output, e = call_tool_nowarn(get_tool('dumpbin') + ['-exports', impfilename], env=env)
+ if output:
+ lines = output.split('\n')
+ start = lines.index('File Type: LIBRARY')
+ end = lines.index(' Summary')
+ return lines[start:end], None
+ all_stderr += e
+ # Next, try llvm-nm.exe provided by LLVM, then nm.exe provided by MinGW
+ for nm in ('llvm-nm', 'nm'):
+ output, e = call_tool_nowarn(get_tool(nm) + ['--extern-only', '--defined-only',
+ '--format=posix', impfilename])
+ if output:
+ result = []
+ for line in output.split('\n'):
+ if ' T ' not in line or line.startswith('.text'):
+ continue
+ result.append(line.split(maxsplit=1)[0])
+ return result, None
+ all_stderr += e
+ return ([], all_stderr)
+
+def windows_syms(impfilename: str, outfilename: str) -> None:
+ # Get the name of the library
+ result, e = _get_implib_dllname(impfilename)
+ if not result:
+ print_tool_warning(['lib', 'llvm-lib', 'dlltool'], 'do not work or were not found', e)
+ dummy_syms(outfilename)
+ return
+ # Get a list of all symbols exported
+ symbols, e = _get_implib_exports(impfilename)
+ if not symbols:
+ print_tool_warning(['dumpbin', 'llvm-nm', 'nm'], 'do not work or were not found', e)
+ dummy_syms(outfilename)
+ return
+ result += symbols
+ write_if_changed('\n'.join(result) + '\n', outfilename)
+
+def gen_symbols(libfilename: str, impfilename: str, outfilename: str, cross_host: str) -> None:
+ if cross_host is not None:
+ # In case of cross builds just always relink. In theory we could
+ # determine the correct toolset, but we would need to use the correct
+ # `nm`, `readelf`, etc, from the cross info which requires refactoring.
+ dummy_syms(outfilename)
+ elif mesonlib.is_linux() or mesonlib.is_hurd():
+ gnu_syms(libfilename, outfilename)
+ elif mesonlib.is_osx():
+ osx_syms(libfilename, outfilename)
+ elif mesonlib.is_openbsd():
+ openbsd_syms(libfilename, outfilename)
+ elif mesonlib.is_freebsd():
+ freebsd_syms(libfilename, outfilename)
+ elif mesonlib.is_netbsd():
+ freebsd_syms(libfilename, outfilename)
+ elif mesonlib.is_windows():
+ if os.path.isfile(impfilename):
+ windows_syms(impfilename, outfilename)
+ else:
+ # No import library. Not sure how the DLL is being used, so just
+ # rebuild everything that links to it every time.
+ dummy_syms(outfilename)
+ elif mesonlib.is_cygwin():
+ if os.path.isfile(impfilename):
+ cygwin_syms(impfilename, outfilename)
+ else:
+ # No import library. Not sure how the DLL is being used, so just
+ # rebuild everything that links to it every time.
+ dummy_syms(outfilename)
+ elif mesonlib.is_sunos():
+ solaris_syms(libfilename, outfilename)
+ else:
+ if not os.path.exists(TOOL_WARNING_FILE):
+ mlog.warning('Symbol extracting has not been implemented for this '
+ 'platform. ' + RELINKING_WARNING)
+ # Write it out so we don't warn again
+ with open(TOOL_WARNING_FILE, 'w', encoding='utf-8'):
+ pass
+ dummy_syms(outfilename)
+
+def run(args: T.List[str]) -> int:
+ global TOOL_WARNING_FILE # pylint: disable=global-statement
+ options = parser.parse_args(args)
+ if len(options.args) != 4:
+ print('symbolextractor.py <shared library file> <import library> <output file>')
+ sys.exit(1)
+ privdir = os.path.join(options.args[0], 'meson-private')
+ TOOL_WARNING_FILE = os.path.join(privdir, 'symbolextractor_tool_warning_printed')
+ libfile = options.args[1]
+ impfile = options.args[2] # Only used on Windows
+ outfile = options.args[3]
+ gen_symbols(libfile, impfile, outfile, options.cross_host)
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(run(sys.argv[1:]))
diff --git a/mesonbuild/scripts/tags.py b/mesonbuild/scripts/tags.py
new file mode 100644
index 0000000..c856807
--- /dev/null
+++ b/mesonbuild/scripts/tags.py
@@ -0,0 +1,54 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+import os
+import subprocess
+from pathlib import Path
+import typing as T
+
+def ls_as_bytestream() -> bytes:
+ if os.path.exists('.git'):
+ return subprocess.run(['git', 'ls-tree', '-r', '--name-only', 'HEAD'],
+ stdout=subprocess.PIPE).stdout
+
+ files = [str(p) for p in Path('.').glob('**/*')
+ if not p.is_dir() and
+ not next((x for x in p.parts if x.startswith('.')), None)]
+ return '\n'.join(files).encode()
+
+
+def cscope() -> int:
+ ls = b'\n'.join([b'"%s"' % f for f in ls_as_bytestream().split()])
+ return subprocess.run(['cscope', '-v', '-b', '-i-'], input=ls).returncode
+
+
+def ctags() -> int:
+ ls = ls_as_bytestream()
+ return subprocess.run(['ctags', '-L-'], input=ls).returncode
+
+
+def etags() -> int:
+ ls = ls_as_bytestream()
+ return subprocess.run(['etags', '-'], input=ls).returncode
+
+
+def run(args: T.List[str]) -> int:
+ tool_name = args[0]
+ srcdir_name = args[1]
+ os.chdir(srcdir_name)
+ assert tool_name in {'cscope', 'ctags', 'etags'}
+ res = globals()[tool_name]()
+ assert isinstance(res, int)
+ return res
diff --git a/mesonbuild/scripts/test_loaded_modules.py b/mesonbuild/scripts/test_loaded_modules.py
new file mode 100644
index 0000000..b3547be
--- /dev/null
+++ b/mesonbuild/scripts/test_loaded_modules.py
@@ -0,0 +1,11 @@
+import sys
+import json
+import typing as T
+from . import meson_exe
+
+# This script is used by run_unittests.py to verify we don't load too many
+# modules when executing a wrapped command.
+def run(args: T.List[str]) -> int:
+ meson_exe.run(args)
+ print(json.dumps(list(sys.modules.keys())))
+ return 0
diff --git a/mesonbuild/scripts/uninstall.py b/mesonbuild/scripts/uninstall.py
new file mode 100644
index 0000000..8548766
--- /dev/null
+++ b/mesonbuild/scripts/uninstall.py
@@ -0,0 +1,51 @@
+# Copyright 2016 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.
+from __future__ import annotations
+
+import os
+import typing as T
+
+logfile = 'meson-logs/install-log.txt'
+
+def do_uninstall(log: str) -> None:
+ failures = 0
+ successes = 0
+ for line in open(log, encoding='utf-8'):
+ if line.startswith('#'):
+ continue
+ fname = line.strip()
+ try:
+ if os.path.isdir(fname) and not os.path.islink(fname):
+ os.rmdir(fname)
+ else:
+ os.unlink(fname)
+ print('Deleted:', fname)
+ successes += 1
+ except Exception as e:
+ print(f'Could not delete {fname}: {e}.')
+ failures += 1
+ print('\nUninstall finished.\n')
+ print('Deleted:', successes)
+ print('Failed:', failures)
+ print('\nRemember that files created by custom scripts have not been removed.')
+
+def run(args: T.List[str]) -> int:
+ if args:
+ print('Weird error.')
+ return 1
+ if not os.path.exists(logfile):
+ print('Log file does not exist, no installation has been done.')
+ return 0
+ do_uninstall(logfile)
+ return 0
diff --git a/mesonbuild/scripts/vcstagger.py b/mesonbuild/scripts/vcstagger.py
new file mode 100644
index 0000000..c484ee1
--- /dev/null
+++ b/mesonbuild/scripts/vcstagger.py
@@ -0,0 +1,45 @@
+# Copyright 2015-2016 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.
+from __future__ import annotations
+
+import sys, os, subprocess, re
+import typing as T
+
+def config_vcs_tag(infile: str, outfile: str, fallback: str, source_dir: str, replace_string: str, regex_selector: str, cmd: T.List[str]) -> None:
+ try:
+ output = subprocess.check_output(cmd, cwd=source_dir)
+ new_string = re.search(regex_selector, output.decode()).group(1).strip()
+ except Exception:
+ new_string = fallback
+
+ with open(infile, encoding='utf-8') as f:
+ new_data = f.read().replace(replace_string, new_string)
+ if os.path.exists(outfile):
+ with open(outfile, encoding='utf-8') as f:
+ needs_update = f.read() != new_data
+ else:
+ needs_update = True
+ if needs_update:
+ with open(outfile, 'w', encoding='utf-8') as f:
+ f.write(new_data)
+
+
+def run(args: T.List[str]) -> int:
+ infile, outfile, fallback, source_dir, replace_string, regex_selector = args[0:6]
+ command = args[6:]
+ config_vcs_tag(infile, outfile, fallback, source_dir, replace_string, regex_selector, command)
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(run(sys.argv[1:]))
diff --git a/mesonbuild/scripts/yasm.py b/mesonbuild/scripts/yasm.py
new file mode 100644
index 0000000..730ff3e
--- /dev/null
+++ b/mesonbuild/scripts/yasm.py
@@ -0,0 +1,22 @@
+import argparse
+import subprocess
+import typing as T
+
+def run(args: T.List[str]) -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--depfile')
+ options, yasm_cmd = parser.parse_known_args(args)
+
+ # Compile
+ returncode = subprocess.call(yasm_cmd)
+ if returncode != 0:
+ return returncode
+
+ # Capture and write depfile
+ ret = subprocess.run(yasm_cmd + ['-M'], capture_output=True)
+ if ret.returncode != 0:
+ return ret.returncode
+ with open(options.depfile, 'wb') as f:
+ f.write(ret.stdout)
+
+ return 0
diff --git a/mesonbuild/templates/__init__.py b/mesonbuild/templates/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/mesonbuild/templates/__init__.py
diff --git a/mesonbuild/templates/cpptemplates.py b/mesonbuild/templates/cpptemplates.py
new file mode 100644
index 0000000..6e97761
--- /dev/null
+++ b/mesonbuild/templates/cpptemplates.py
@@ -0,0 +1,187 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+
+hello_cpp_template = '''#include <iostream>
+
+#define PROJECT_NAME "{project_name}"
+
+int main(int argc, char **argv) {{
+ if(argc != 1) {{
+ std::cout << argv[0] << "takes no arguments.\\n";
+ return 1;
+ }}
+ std::cout << "This is project " << PROJECT_NAME << ".\\n";
+ return 0;
+}}
+'''
+
+hello_cpp_meson_template = '''project('{project_name}', 'cpp',
+ version : '{version}',
+ default_options : ['warning_level=3',
+ 'cpp_std=c++14'])
+
+exe = executable('{exe_name}', '{source_name}',
+ install : true)
+
+test('basic', exe)
+'''
+
+lib_hpp_template = '''#pragma once
+#if defined _WIN32 || defined __CYGWIN__
+ #ifdef BUILDING_{utoken}
+ #define {utoken}_PUBLIC __declspec(dllexport)
+ #else
+ #define {utoken}_PUBLIC __declspec(dllimport)
+ #endif
+#else
+ #ifdef BUILDING_{utoken}
+ #define {utoken}_PUBLIC __attribute__ ((visibility ("default")))
+ #else
+ #define {utoken}_PUBLIC
+ #endif
+#endif
+
+namespace {namespace} {{
+
+class {utoken}_PUBLIC {class_name} {{
+
+public:
+ {class_name}();
+ int get_number() const;
+
+private:
+
+ int number;
+
+}};
+
+}}
+
+'''
+
+lib_cpp_template = '''#include <{header_file}>
+
+namespace {namespace} {{
+
+{class_name}::{class_name}() {{
+ number = 6;
+}}
+
+int {class_name}::get_number() const {{
+ return number;
+}}
+
+}}
+'''
+
+lib_cpp_test_template = '''#include <{header_file}>
+#include <iostream>
+
+int main(int argc, char **argv) {{
+ if(argc != 1) {{
+ std::cout << argv[0] << " takes no arguments.\\n";
+ return 1;
+ }}
+ {namespace}::{class_name} c;
+ return c.get_number() != 6;
+}}
+'''
+
+lib_cpp_meson_template = '''project('{project_name}', 'cpp',
+ version : '{version}',
+ default_options : ['warning_level=3', 'cpp_std=c++14'])
+
+# These arguments are only used to build the shared library
+# not the executables that use the library.
+lib_args = ['-DBUILDING_{utoken}']
+
+shlib = shared_library('{lib_name}', '{source_file}',
+ install : true,
+ cpp_args : lib_args,
+ gnu_symbol_visibility : 'hidden',
+)
+
+test_exe = executable('{test_exe_name}', '{test_source_file}',
+ link_with : shlib)
+test('{test_name}', test_exe)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : shlib)
+
+# Make this library usable from the system's
+# package manager.
+install_headers('{header_file}', subdir : '{header_dir}')
+
+pkg_mod = import('pkgconfig')
+pkg_mod.generate(
+ name : '{project_name}',
+ filebase : '{ltoken}',
+ description : 'Meson sample project.',
+ subdirs : '{header_dir}',
+ libraries : shlib,
+ version : '{version}',
+)
+'''
+
+
+class CppProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ source_name = lowercase_token + '.cpp'
+ open(source_name, 'w', encoding='utf-8').write(hello_cpp_template.format(project_name=self.name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_cpp_meson_template.format(project_name=self.name,
+ exe_name=lowercase_token,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ class_name = uppercase_token[0] + lowercase_token[1:]
+ test_exe_name = lowercase_token + '_test'
+ namespace = lowercase_token
+ lib_hpp_name = lowercase_token + '.hpp'
+ lib_cpp_name = lowercase_token + '.cpp'
+ test_cpp_name = lowercase_token + '_test.cpp'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'header_dir': lowercase_token,
+ 'class_name': class_name,
+ 'namespace': namespace,
+ 'header_file': lib_hpp_name,
+ 'source_file': lib_cpp_name,
+ 'test_source_file': test_cpp_name,
+ 'test_exe_name': test_exe_name,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_hpp_name, 'w', encoding='utf-8').write(lib_hpp_template.format(**kwargs))
+ open(lib_cpp_name, 'w', encoding='utf-8').write(lib_cpp_template.format(**kwargs))
+ open(test_cpp_name, 'w', encoding='utf-8').write(lib_cpp_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_cpp_meson_template.format(**kwargs))
diff --git a/mesonbuild/templates/cstemplates.py b/mesonbuild/templates/cstemplates.py
new file mode 100644
index 0000000..df09f61
--- /dev/null
+++ b/mesonbuild/templates/cstemplates.py
@@ -0,0 +1,136 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+
+hello_cs_template = '''using System;
+
+public class {class_name} {{
+ const String PROJECT_NAME = "{project_name}";
+
+ static int Main(String[] args) {{
+ if (args.Length > 0) {{
+ System.Console.WriteLine(String.Format("{project_name} takes no arguments.."));
+ return 1;
+ }}
+ Console.WriteLine(String.Format("This is project {{0}}.", PROJECT_NAME));
+ return 0;
+ }}
+}}
+
+'''
+
+hello_cs_meson_template = '''project('{project_name}', 'cs',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+exe = executable('{exe_name}', '{source_name}',
+ install : true)
+
+test('basic', exe)
+'''
+
+lib_cs_template = '''
+public class {class_name} {{
+ private const int number = 6;
+
+ public int get_number() {{
+ return number;
+ }}
+}}
+
+'''
+
+lib_cs_test_template = '''using System;
+
+public class {class_test} {{
+ static int Main(String[] args) {{
+ if (args.Length > 0) {{
+ System.Console.WriteLine("{project_name} takes no arguments..");
+ return 1;
+ }}
+ {class_name} c = new {class_name}();
+ Boolean result = true;
+ return result.CompareTo(c.get_number() != 6);
+ }}
+}}
+
+'''
+
+lib_cs_meson_template = '''project('{project_name}', 'cs',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+stlib = shared_library('{lib_name}', '{source_file}',
+ install : true,
+)
+
+test_exe = executable('{test_exe_name}', '{test_source_file}',
+ link_with : stlib)
+test('{test_name}', test_exe)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : stlib)
+
+'''
+
+
+class CSharpProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ class_name = uppercase_token[0] + lowercase_token[1:]
+ source_name = uppercase_token[0] + lowercase_token[1:] + '.cs'
+ open(source_name, 'w', encoding='utf-8').write(
+ hello_cs_template.format(project_name=self.name,
+ class_name=class_name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_cs_meson_template.format(project_name=self.name,
+ exe_name=self.name,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ class_name = uppercase_token[0] + lowercase_token[1:]
+ class_test = uppercase_token[0] + lowercase_token[1:] + '_test'
+ project_test = lowercase_token + '_test'
+ lib_cs_name = uppercase_token[0] + lowercase_token[1:] + '.cs'
+ test_cs_name = uppercase_token[0] + lowercase_token[1:] + '_test.cs'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'class_test': class_test,
+ 'class_name': class_name,
+ 'source_file': lib_cs_name,
+ 'test_source_file': test_cs_name,
+ 'test_exe_name': project_test,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_cs_name, 'w', encoding='utf-8').write(lib_cs_template.format(**kwargs))
+ open(test_cs_name, 'w', encoding='utf-8').write(lib_cs_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_cs_meson_template.format(**kwargs))
diff --git a/mesonbuild/templates/ctemplates.py b/mesonbuild/templates/ctemplates.py
new file mode 100644
index 0000000..0c7141a
--- /dev/null
+++ b/mesonbuild/templates/ctemplates.py
@@ -0,0 +1,168 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+
+lib_h_template = '''#pragma once
+#if defined _WIN32 || defined __CYGWIN__
+ #ifdef BUILDING_{utoken}
+ #define {utoken}_PUBLIC __declspec(dllexport)
+ #else
+ #define {utoken}_PUBLIC __declspec(dllimport)
+ #endif
+#else
+ #ifdef BUILDING_{utoken}
+ #define {utoken}_PUBLIC __attribute__ ((visibility ("default")))
+ #else
+ #define {utoken}_PUBLIC
+ #endif
+#endif
+
+int {utoken}_PUBLIC {function_name}();
+
+'''
+
+lib_c_template = '''#include <{header_file}>
+
+/* This function will not be exported and is not
+ * directly callable by users of this library.
+ */
+int internal_function() {{
+ return 0;
+}}
+
+int {function_name}() {{
+ return internal_function();
+}}
+'''
+
+lib_c_test_template = '''#include <{header_file}>
+#include <stdio.h>
+
+int main(int argc, char **argv) {{
+ if(argc != 1) {{
+ printf("%s takes no arguments.\\n", argv[0]);
+ return 1;
+ }}
+ return {function_name}();
+}}
+'''
+
+lib_c_meson_template = '''project('{project_name}', 'c',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+# These arguments are only used to build the shared library
+# not the executables that use the library.
+lib_args = ['-DBUILDING_{utoken}']
+
+shlib = shared_library('{lib_name}', '{source_file}',
+ install : true,
+ c_args : lib_args,
+ gnu_symbol_visibility : 'hidden',
+)
+
+test_exe = executable('{test_exe_name}', '{test_source_file}',
+ link_with : shlib)
+test('{test_name}', test_exe)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : shlib)
+
+# Make this library usable from the system's
+# package manager.
+install_headers('{header_file}', subdir : '{header_dir}')
+
+pkg_mod = import('pkgconfig')
+pkg_mod.generate(
+ name : '{project_name}',
+ filebase : '{ltoken}',
+ description : 'Meson sample project.',
+ subdirs : '{header_dir}',
+ libraries : shlib,
+ version : '{version}',
+)
+'''
+
+hello_c_template = '''#include <stdio.h>
+
+#define PROJECT_NAME "{project_name}"
+
+int main(int argc, char **argv) {{
+ if(argc != 1) {{
+ printf("%s takes no arguments.\\n", argv[0]);
+ return 1;
+ }}
+ printf("This is project %s.\\n", PROJECT_NAME);
+ return 0;
+}}
+'''
+
+hello_c_meson_template = '''project('{project_name}', 'c',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+exe = executable('{exe_name}', '{source_name}',
+ install : true)
+
+test('basic', exe)
+'''
+
+
+class CProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ source_name = lowercase_token + '.c'
+ open(source_name, 'w', encoding='utf-8').write(hello_c_template.format(project_name=self.name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_c_meson_template.format(project_name=self.name,
+ exe_name=lowercase_token,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ function_name = lowercase_token[0:3] + '_func'
+ test_exe_name = lowercase_token + '_test'
+ lib_h_name = lowercase_token + '.h'
+ lib_c_name = lowercase_token + '.c'
+ test_c_name = lowercase_token + '_test.c'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'header_dir': lowercase_token,
+ 'function_name': function_name,
+ 'header_file': lib_h_name,
+ 'source_file': lib_c_name,
+ 'test_source_file': test_c_name,
+ 'test_exe_name': test_exe_name,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_h_name, 'w', encoding='utf-8').write(lib_h_template.format(**kwargs))
+ open(lib_c_name, 'w', encoding='utf-8').write(lib_c_template.format(**kwargs))
+ open(test_c_name, 'w', encoding='utf-8').write(lib_c_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_c_meson_template.format(**kwargs))
diff --git a/mesonbuild/templates/cudatemplates.py b/mesonbuild/templates/cudatemplates.py
new file mode 100644
index 0000000..63abd2b
--- /dev/null
+++ b/mesonbuild/templates/cudatemplates.py
@@ -0,0 +1,187 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+
+hello_cuda_template = '''#include <iostream>
+
+#define PROJECT_NAME "{project_name}"
+
+int main(int argc, char **argv) {{
+ if(argc != 1) {{
+ std::cout << argv[0] << " takes no arguments.\\n";
+ return 1;
+ }}
+ std::cout << "This is project " << PROJECT_NAME << ".\\n";
+ return 0;
+}}
+'''
+
+hello_cuda_meson_template = '''project('{project_name}', ['cuda', 'cpp'],
+ version : '{version}',
+ default_options : ['warning_level=3',
+ 'cpp_std=c++14'])
+
+exe = executable('{exe_name}', '{source_name}',
+ install : true)
+
+test('basic', exe)
+'''
+
+lib_h_template = '''#pragma once
+#if defined _WIN32 || defined __CYGWIN__
+ #ifdef BUILDING_{utoken}
+ #define {utoken}_PUBLIC __declspec(dllexport)
+ #else
+ #define {utoken}_PUBLIC __declspec(dllimport)
+ #endif
+#else
+ #ifdef BUILDING_{utoken}
+ #define {utoken}_PUBLIC __attribute__ ((visibility ("default")))
+ #else
+ #define {utoken}_PUBLIC
+ #endif
+#endif
+
+namespace {namespace} {{
+
+class {utoken}_PUBLIC {class_name} {{
+
+public:
+ {class_name}();
+ int get_number() const;
+
+private:
+
+ int number;
+
+}};
+
+}}
+
+'''
+
+lib_cuda_template = '''#include <{header_file}>
+
+namespace {namespace} {{
+
+{class_name}::{class_name}() {{
+ number = 6;
+}}
+
+int {class_name}::get_number() const {{
+ return number;
+}}
+
+}}
+'''
+
+lib_cuda_test_template = '''#include <{header_file}>
+#include <iostream>
+
+int main(int argc, char **argv) {{
+ if(argc != 1) {{
+ std::cout << argv[0] << " takes no arguments.\\n";
+ return 1;
+ }}
+ {namespace}::{class_name} c;
+ return c.get_number() != 6;
+}}
+'''
+
+lib_cuda_meson_template = '''project('{project_name}', ['cuda', 'cpp'],
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+# These arguments are only used to build the shared library
+# not the executables that use the library.
+lib_args = ['-DBUILDING_{utoken}']
+
+shlib = shared_library('{lib_name}', '{source_file}',
+ install : true,
+ cpp_args : lib_args,
+ gnu_symbol_visibility : 'hidden',
+)
+
+test_exe = executable('{test_exe_name}', '{test_source_file}',
+ link_with : shlib)
+test('{test_name}', test_exe)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : shlib)
+
+# Make this library usable from the system's
+# package manager.
+install_headers('{header_file}', subdir : '{header_dir}')
+
+pkg_mod = import('pkgconfig')
+pkg_mod.generate(
+ name : '{project_name}',
+ filebase : '{ltoken}',
+ description : 'Meson sample project.',
+ subdirs : '{header_dir}',
+ libraries : shlib,
+ version : '{version}',
+)
+'''
+
+
+class CudaProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ source_name = lowercase_token + '.cu'
+ open(source_name, 'w', encoding='utf-8').write(hello_cuda_template.format(project_name=self.name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_cuda_meson_template.format(project_name=self.name,
+ exe_name=lowercase_token,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ class_name = uppercase_token[0] + lowercase_token[1:]
+ test_exe_name = lowercase_token + '_test'
+ namespace = lowercase_token
+ lib_h_name = lowercase_token + '.h'
+ lib_cuda_name = lowercase_token + '.cu'
+ test_cuda_name = lowercase_token + '_test.cu'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'header_dir': lowercase_token,
+ 'class_name': class_name,
+ 'namespace': namespace,
+ 'header_file': lib_h_name,
+ 'source_file': lib_cuda_name,
+ 'test_source_file': test_cuda_name,
+ 'test_exe_name': test_exe_name,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_h_name, 'w', encoding='utf-8').write(lib_h_template.format(**kwargs))
+ open(lib_cuda_name, 'w', encoding='utf-8').write(lib_cuda_template.format(**kwargs))
+ open(test_cuda_name, 'w', encoding='utf-8').write(lib_cuda_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_cuda_meson_template.format(**kwargs))
diff --git a/mesonbuild/templates/dlangtemplates.py b/mesonbuild/templates/dlangtemplates.py
new file mode 100644
index 0000000..81840fe
--- /dev/null
+++ b/mesonbuild/templates/dlangtemplates.py
@@ -0,0 +1,145 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+
+hello_d_template = '''module main;
+import std.stdio;
+
+enum PROJECT_NAME = "{project_name}";
+
+int main(string[] args) {{
+ if (args.length != 1){{
+ writefln("%s takes no arguments.\\n", args[0]);
+ return 1;
+ }}
+ writefln("This is project %s.\\n", PROJECT_NAME);
+ return 0;
+}}
+'''
+
+hello_d_meson_template = '''project('{project_name}', 'd',
+ version : '{version}',
+ default_options: ['warning_level=3'])
+
+exe = executable('{exe_name}', '{source_name}',
+ install : true)
+
+test('basic', exe)
+'''
+
+lib_d_template = '''module {module_file};
+
+/* This function will not be exported and is not
+ * directly callable by users of this library.
+ */
+int internal_function() {{
+ return 0;
+}}
+
+int {function_name}() {{
+ return internal_function();
+}}
+'''
+
+lib_d_test_template = '''module {module_file}_test;
+import std.stdio;
+import {module_file};
+
+
+int main(string[] args) {{
+ if (args.length != 1){{
+ writefln("%s takes no arguments.\\n", args[0]);
+ return 1;
+ }}
+ return {function_name}();
+}}
+'''
+
+lib_d_meson_template = '''project('{project_name}', 'd',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+stlib = static_library('{lib_name}', '{source_file}',
+ install : true,
+ gnu_symbol_visibility : 'hidden',
+)
+
+test_exe = executable('{test_exe_name}', '{test_source_file}',
+ link_with : stlib)
+test('{test_name}', test_exe)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : stlib)
+
+# Make this library usable from the Dlang
+# build system.
+dlang_mod = import('dlang')
+if find_program('dub', required: false).found()
+ dlang_mod.generate_dub_file(meson.project_name().to_lower(), meson.source_root(),
+ name : meson.project_name(),
+ license: meson.project_license(),
+ sourceFiles : '{source_file}',
+ description : 'Meson sample project.',
+ version : '{version}',
+ )
+endif
+'''
+
+
+class DlangProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ source_name = lowercase_token + '.d'
+ open(source_name, 'w', encoding='utf-8').write(hello_d_template.format(project_name=self.name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_d_meson_template.format(project_name=self.name,
+ exe_name=lowercase_token,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ function_name = lowercase_token[0:3] + '_func'
+ test_exe_name = lowercase_token + '_test'
+ lib_m_name = lowercase_token
+ lib_d_name = lowercase_token + '.d'
+ test_d_name = lowercase_token + '_test.d'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'header_dir': lowercase_token,
+ 'function_name': function_name,
+ 'module_file': lib_m_name,
+ 'source_file': lib_d_name,
+ 'test_source_file': test_d_name,
+ 'test_exe_name': test_exe_name,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_d_name, 'w', encoding='utf-8').write(lib_d_template.format(**kwargs))
+ open(test_d_name, 'w', encoding='utf-8').write(lib_d_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_d_meson_template.format(**kwargs))
diff --git a/mesonbuild/templates/fortrantemplates.py b/mesonbuild/templates/fortrantemplates.py
new file mode 100644
index 0000000..00cd509
--- /dev/null
+++ b/mesonbuild/templates/fortrantemplates.py
@@ -0,0 +1,142 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+lib_fortran_template = '''
+! This procedure will not be exported and is not
+! directly callable by users of this library.
+
+module modfoo
+
+implicit none
+private
+public :: {function_name}
+
+contains
+
+integer function internal_function()
+ internal_function = 0
+end function internal_function
+
+integer function {function_name}()
+ {function_name} = internal_function()
+end function {function_name}
+
+end module modfoo
+'''
+
+lib_fortran_test_template = '''
+use modfoo
+
+print *,{function_name}()
+
+end program
+'''
+
+lib_fortran_meson_template = '''project('{project_name}', 'fortran',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+# These arguments are only used to build the shared library
+# not the executables that use the library.
+lib_args = ['-DBUILDING_{utoken}']
+
+shlib = shared_library('{lib_name}', '{source_file}',
+ install : true,
+ fortran_args : lib_args,
+ gnu_symbol_visibility : 'hidden',
+)
+
+test_exe = executable('{test_exe_name}', '{test_source_file}',
+ link_with : shlib)
+test('{test_name}', test_exe)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : shlib)
+
+pkg_mod = import('pkgconfig')
+pkg_mod.generate(
+ name : '{project_name}',
+ filebase : '{ltoken}',
+ description : 'Meson sample project.',
+ subdirs : '{header_dir}',
+ libraries : shlib,
+ version : '{version}',
+)
+'''
+
+hello_fortran_template = '''
+implicit none
+
+character(len=*), parameter :: PROJECT_NAME = "{project_name}"
+
+print *,"This is project ", PROJECT_NAME
+
+end program
+'''
+
+hello_fortran_meson_template = '''project('{project_name}', 'fortran',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+exe = executable('{exe_name}', '{source_name}',
+ install : true)
+
+test('basic', exe)
+'''
+
+
+class FortranProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ source_name = lowercase_token + '.f90'
+ open(source_name, 'w', encoding='utf-8').write(hello_fortran_template.format(project_name=self.name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_fortran_meson_template.format(project_name=self.name,
+ exe_name=lowercase_token,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ function_name = lowercase_token[0:3] + '_func'
+ test_exe_name = lowercase_token + '_test'
+ lib_fortran_name = lowercase_token + '.f90'
+ test_fortran_name = lowercase_token + '_test.f90'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'header_dir': lowercase_token,
+ 'function_name': function_name,
+ 'source_file': lib_fortran_name,
+ 'test_source_file': test_fortran_name,
+ 'test_exe_name': test_exe_name,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_fortran_name, 'w', encoding='utf-8').write(lib_fortran_template.format(**kwargs))
+ open(test_fortran_name, 'w', encoding='utf-8').write(lib_fortran_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_fortran_meson_template.format(**kwargs))
diff --git a/mesonbuild/templates/javatemplates.py b/mesonbuild/templates/javatemplates.py
new file mode 100644
index 0000000..58d48ba
--- /dev/null
+++ b/mesonbuild/templates/javatemplates.py
@@ -0,0 +1,138 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+
+hello_java_template = '''
+
+public class {class_name} {{
+ final static String PROJECT_NAME = "{project_name}";
+
+ public static void main (String args[]) {{
+ if(args.length != 0) {{
+ System.out.println(args + " takes no arguments.");
+ System.exit(0);
+ }}
+ System.out.println("This is project " + PROJECT_NAME + ".");
+ System.exit(0);
+ }}
+}}
+
+'''
+
+hello_java_meson_template = '''project('{project_name}', 'java',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+exe = jar('{exe_name}', '{source_name}',
+ main_class : '{exe_name}',
+ install : true)
+
+test('basic', exe)
+'''
+
+lib_java_template = '''
+
+public class {class_name} {{
+ final static int number = 6;
+
+ public final int get_number() {{
+ return number;
+ }}
+}}
+
+'''
+
+lib_java_test_template = '''
+
+public class {class_test} {{
+ public static void main (String args[]) {{
+ if(args.length != 0) {{
+ System.out.println(args + " takes no arguments.");
+ System.exit(1);
+ }}
+
+ {class_name} c = new {class_name}();
+ Boolean result = true;
+ System.exit(result.compareTo(c.get_number() != 6));
+ }}
+}}
+
+'''
+
+lib_java_meson_template = '''project('{project_name}', 'java',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+jarlib = jar('{class_name}', '{source_file}',
+ main_class : '{class_name}',
+ install : true,
+)
+
+test_jar = jar('{class_test}', '{test_source_file}',
+ main_class : '{class_test}',
+ link_with : jarlib)
+test('{test_name}', test_jar)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : jarlib)
+'''
+
+
+class JavaProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ class_name = uppercase_token[0] + lowercase_token[1:]
+ source_name = uppercase_token[0] + lowercase_token[1:] + '.java'
+ open(source_name, 'w', encoding='utf-8').write(
+ hello_java_template.format(project_name=self.name,
+ class_name=class_name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_java_meson_template.format(project_name=self.name,
+ exe_name=class_name,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ class_name = uppercase_token[0] + lowercase_token[1:]
+ class_test = uppercase_token[0] + lowercase_token[1:] + '_test'
+ lib_java_name = uppercase_token[0] + lowercase_token[1:] + '.java'
+ test_java_name = uppercase_token[0] + lowercase_token[1:] + '_test.java'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'class_test': class_test,
+ 'class_name': class_name,
+ 'source_file': lib_java_name,
+ 'test_source_file': test_java_name,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_java_name, 'w', encoding='utf-8').write(lib_java_template.format(**kwargs))
+ open(test_java_name, 'w', encoding='utf-8').write(lib_java_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_java_meson_template.format(**kwargs))
diff --git a/mesonbuild/templates/mesontemplates.py b/mesonbuild/templates/mesontemplates.py
new file mode 100644
index 0000000..2868f7b
--- /dev/null
+++ b/mesonbuild/templates/mesontemplates.py
@@ -0,0 +1,77 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+import argparse
+
+meson_executable_template = '''project('{project_name}', {language},
+ version : '{version}',
+ default_options : [{default_options}])
+
+executable('{executable}',
+ {sourcespec},{depspec}
+ install : true)
+'''
+
+
+meson_jar_template = '''project('{project_name}', '{language}',
+ version : '{version}',
+ default_options : [{default_options}])
+
+jar('{executable}',
+ {sourcespec},{depspec}
+ main_class: '{main_class}',
+ install : true)
+'''
+
+
+def create_meson_build(options: argparse.Namespace) -> None:
+ if options.type != 'executable':
+ raise SystemExit('\nGenerating a meson.build file from existing sources is\n'
+ 'supported only for project type "executable".\n'
+ 'Run meson init in an empty directory to create a sample project.')
+ default_options = ['warning_level=3']
+ if options.language == 'cpp':
+ # This shows how to set this very common option.
+ default_options += ['cpp_std=c++14']
+ # If we get a meson.build autoformatter one day, this code could
+ # be simplified quite a bit.
+ formatted_default_options = ', '.join(f"'{x}'" for x in default_options)
+ sourcespec = ',\n '.join(f"'{x}'" for x in options.srcfiles)
+ depspec = ''
+ if options.deps:
+ depspec = '\n dependencies : [\n '
+ depspec += ',\n '.join(f"dependency('{x}')"
+ for x in options.deps.split(','))
+ depspec += '],'
+ if options.language != 'java':
+ language = f"'{options.language}'" if options.language != 'vala' else ['c', 'vala']
+ content = meson_executable_template.format(project_name=options.name,
+ language=language,
+ version=options.version,
+ executable=options.executable,
+ sourcespec=sourcespec,
+ depspec=depspec,
+ default_options=formatted_default_options)
+ else:
+ content = meson_jar_template.format(project_name=options.name,
+ language=options.language,
+ version=options.version,
+ executable=options.executable,
+ main_class=options.name,
+ sourcespec=sourcespec,
+ depspec=depspec,
+ default_options=formatted_default_options)
+ open('meson.build', 'w', encoding='utf-8').write(content)
+ print('Generated meson.build file:\n\n' + content)
diff --git a/mesonbuild/templates/objcpptemplates.py b/mesonbuild/templates/objcpptemplates.py
new file mode 100644
index 0000000..450f2b0
--- /dev/null
+++ b/mesonbuild/templates/objcpptemplates.py
@@ -0,0 +1,168 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+
+lib_h_template = '''#pragma once
+#if defined _WIN32 || defined __CYGWIN__
+ #ifdef BUILDING_{utoken}
+ #define {utoken}_PUBLIC __declspec(dllexport)
+ #else
+ #define {utoken}_PUBLIC __declspec(dllimport)
+ #endif
+#else
+ #ifdef BUILDING_{utoken}
+ #define {utoken}_PUBLIC __attribute__ ((visibility ("default")))
+ #else
+ #define {utoken}_PUBLIC
+ #endif
+#endif
+
+int {utoken}_PUBLIC {function_name}();
+
+'''
+
+lib_objcpp_template = '''#import <{header_file}>
+
+/* This function will not be exported and is not
+ * directly callable by users of this library.
+ */
+int internal_function() {{
+ return 0;
+}}
+
+int {function_name}() {{
+ return internal_function();
+}}
+'''
+
+lib_objcpp_test_template = '''#import <{header_file}>
+#import <iostream>
+
+int main(int argc, char **argv) {{
+ if(argc != 1) {{
+ std::cout << argv[0] << " takes no arguments." << std::endl;
+ return 1;
+ }}
+ return {function_name}();
+}}
+'''
+
+lib_objcpp_meson_template = '''project('{project_name}', 'objcpp',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+# These arguments are only used to build the shared library
+# not the executables that use the library.
+lib_args = ['-DBUILDING_{utoken}']
+
+shlib = shared_library('{lib_name}', '{source_file}',
+ install : true,
+ objcpp_args : lib_args,
+ gnu_symbol_visibility : 'hidden',
+)
+
+test_exe = executable('{test_exe_name}', '{test_source_file}',
+ link_with : shlib)
+test('{test_name}', test_exe)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : shlib)
+
+# Make this library usable from the system's
+# package manager.
+install_headers('{header_file}', subdir : '{header_dir}')
+
+pkg_mod = import('pkgconfig')
+pkg_mod.generate(
+ name : '{project_name}',
+ filebase : '{ltoken}',
+ description : 'Meson sample project.',
+ subdirs : '{header_dir}',
+ libraries : shlib,
+ version : '{version}',
+)
+'''
+
+hello_objcpp_template = '''#import <iostream>
+
+#define PROJECT_NAME "{project_name}"
+
+int main(int argc, char **argv) {{
+ if(argc != 1) {{
+ std::cout << argv[0] << " takes no arguments." << std::endl;
+ return 1;
+ }}
+ std::cout << "This is project " << PROJECT_NAME << "." << std::endl;
+ return 0;
+}}
+'''
+
+hello_objcpp_meson_template = '''project('{project_name}', 'objcpp',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+exe = executable('{exe_name}', '{source_name}',
+ install : true)
+
+test('basic', exe)
+'''
+
+
+class ObjCppProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ source_name = lowercase_token + '.mm'
+ open(source_name, 'w', encoding='utf-8').write(hello_objcpp_template.format(project_name=self.name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_objcpp_meson_template.format(project_name=self.name,
+ exe_name=lowercase_token,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ function_name = lowercase_token[0:3] + '_func'
+ test_exe_name = lowercase_token + '_test'
+ lib_h_name = lowercase_token + '.h'
+ lib_objcpp_name = lowercase_token + '.mm'
+ test_objcpp_name = lowercase_token + '_test.mm'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'header_dir': lowercase_token,
+ 'function_name': function_name,
+ 'header_file': lib_h_name,
+ 'source_file': lib_objcpp_name,
+ 'test_source_file': test_objcpp_name,
+ 'test_exe_name': test_exe_name,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_h_name, 'w', encoding='utf-8').write(lib_h_template.format(**kwargs))
+ open(lib_objcpp_name, 'w', encoding='utf-8').write(lib_objcpp_template.format(**kwargs))
+ open(test_objcpp_name, 'w', encoding='utf-8').write(lib_objcpp_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_objcpp_meson_template.format(**kwargs))
diff --git a/mesonbuild/templates/objctemplates.py b/mesonbuild/templates/objctemplates.py
new file mode 100644
index 0000000..2e03526
--- /dev/null
+++ b/mesonbuild/templates/objctemplates.py
@@ -0,0 +1,168 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+
+lib_h_template = '''#pragma once
+#if defined _WIN32 || defined __CYGWIN__
+ #ifdef BUILDING_{utoken}
+ #define {utoken}_PUBLIC __declspec(dllexport)
+ #else
+ #define {utoken}_PUBLIC __declspec(dllimport)
+ #endif
+#else
+ #ifdef BUILDING_{utoken}
+ #define {utoken}_PUBLIC __attribute__ ((visibility ("default")))
+ #else
+ #define {utoken}_PUBLIC
+ #endif
+#endif
+
+int {utoken}_PUBLIC {function_name}();
+
+'''
+
+lib_objc_template = '''#import <{header_file}>
+
+/* This function will not be exported and is not
+ * directly callable by users of this library.
+ */
+int internal_function() {{
+ return 0;
+}}
+
+int {function_name}() {{
+ return internal_function();
+}}
+'''
+
+lib_objc_test_template = '''#import <{header_file}>
+#import <stdio.h>
+
+int main(int argc, char **argv) {{
+ if(argc != 1) {{
+ printf("%s takes no arguments.\\n", argv[0]);
+ return 1;
+ }}
+ return {function_name}();
+}}
+'''
+
+lib_objc_meson_template = '''project('{project_name}', 'objc',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+# These arguments are only used to build the shared library
+# not the executables that use the library.
+lib_args = ['-DBUILDING_{utoken}']
+
+shlib = shared_library('{lib_name}', '{source_file}',
+ install : true,
+ objc_args : lib_args,
+ gnu_symbol_visibility : 'hidden',
+)
+
+test_exe = executable('{test_exe_name}', '{test_source_file}',
+ link_with : shlib)
+test('{test_name}', test_exe)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : shlib)
+
+# Make this library usable from the system's
+# package manager.
+install_headers('{header_file}', subdir : '{header_dir}')
+
+pkg_mod = import('pkgconfig')
+pkg_mod.generate(
+ name : '{project_name}',
+ filebase : '{ltoken}',
+ description : 'Meson sample project.',
+ subdirs : '{header_dir}',
+ libraries : shlib,
+ version : '{version}',
+)
+'''
+
+hello_objc_template = '''#import <stdio.h>
+
+#define PROJECT_NAME "{project_name}"
+
+int main(int argc, char **argv) {{
+ if(argc != 1) {{
+ printf("%s takes no arguments.\\n", argv[0]);
+ return 1;
+ }}
+ printf("This is project %s.\\n", PROJECT_NAME);
+ return 0;
+}}
+'''
+
+hello_objc_meson_template = '''project('{project_name}', 'objc',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+exe = executable('{exe_name}', '{source_name}',
+ install : true)
+
+test('basic', exe)
+'''
+
+
+class ObjCProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ source_name = lowercase_token + '.m'
+ open(source_name, 'w', encoding='utf-8').write(hello_objc_template.format(project_name=self.name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_objc_meson_template.format(project_name=self.name,
+ exe_name=lowercase_token,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ function_name = lowercase_token[0:3] + '_func'
+ test_exe_name = lowercase_token + '_test'
+ lib_h_name = lowercase_token + '.h'
+ lib_objc_name = lowercase_token + '.m'
+ test_objc_name = lowercase_token + '_test.m'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'header_dir': lowercase_token,
+ 'function_name': function_name,
+ 'header_file': lib_h_name,
+ 'source_file': lib_objc_name,
+ 'test_source_file': test_objc_name,
+ 'test_exe_name': test_exe_name,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_h_name, 'w', encoding='utf-8').write(lib_h_template.format(**kwargs))
+ open(lib_objc_name, 'w', encoding='utf-8').write(lib_objc_template.format(**kwargs))
+ open(test_objc_name, 'w', encoding='utf-8').write(lib_objc_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_objc_meson_template.format(**kwargs))
diff --git a/mesonbuild/templates/rusttemplates.py b/mesonbuild/templates/rusttemplates.py
new file mode 100644
index 0000000..0dde547
--- /dev/null
+++ b/mesonbuild/templates/rusttemplates.py
@@ -0,0 +1,115 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+
+lib_rust_template = '''#![crate_name = "{crate_file}"]
+
+/* This function will not be exported and is not
+ * directly callable by users of this library.
+ */
+fn internal_function() -> i32 {{
+ return 0;
+}}
+
+pub fn {function_name}() -> i32 {{
+ return internal_function();
+}}
+'''
+
+lib_rust_test_template = '''extern crate {crate_file};
+
+fn main() {{
+ println!("printing: {{}}", {crate_file}::{function_name}());
+}}
+'''
+
+
+lib_rust_meson_template = '''project('{project_name}', 'rust',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+shlib = static_library('{lib_name}', '{source_file}', install : true)
+
+test_exe = executable('{test_exe_name}', '{test_source_file}',
+ link_with : shlib)
+test('{test_name}', test_exe)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : shlib)
+'''
+
+hello_rust_template = '''
+fn main() {{
+ let project_name = "{project_name}";
+ println!("This is project {{}}.\\n", project_name);
+}}
+'''
+
+hello_rust_meson_template = '''project('{project_name}', 'rust',
+ version : '{version}',
+ default_options : ['warning_level=3'])
+
+exe = executable('{exe_name}', '{source_name}',
+ install : true)
+
+test('basic', exe)
+'''
+
+
+class RustProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ source_name = lowercase_token + '.rs'
+ open(source_name, 'w', encoding='utf-8').write(hello_rust_template.format(project_name=self.name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_rust_meson_template.format(project_name=self.name,
+ exe_name=lowercase_token,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ function_name = lowercase_token[0:3] + '_func'
+ test_exe_name = lowercase_token + '_test'
+ lib_crate_name = lowercase_token
+ lib_rs_name = lowercase_token + '.rs'
+ test_rs_name = lowercase_token + '_test.rs'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'header_dir': lowercase_token,
+ 'function_name': function_name,
+ 'crate_file': lib_crate_name,
+ 'source_file': lib_rs_name,
+ 'test_source_file': test_rs_name,
+ 'test_exe_name': test_exe_name,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_rs_name, 'w', encoding='utf-8').write(lib_rust_template.format(**kwargs))
+ open(test_rs_name, 'w', encoding='utf-8').write(lib_rust_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_rust_meson_template.format(**kwargs))
diff --git a/mesonbuild/templates/samplefactory.py b/mesonbuild/templates/samplefactory.py
new file mode 100644
index 0000000..1950837
--- /dev/null
+++ b/mesonbuild/templates/samplefactory.py
@@ -0,0 +1,44 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.valatemplates import ValaProject
+from mesonbuild.templates.fortrantemplates import FortranProject
+from mesonbuild.templates.objcpptemplates import ObjCppProject
+from mesonbuild.templates.dlangtemplates import DlangProject
+from mesonbuild.templates.rusttemplates import RustProject
+from mesonbuild.templates.javatemplates import JavaProject
+from mesonbuild.templates.cudatemplates import CudaProject
+from mesonbuild.templates.objctemplates import ObjCProject
+from mesonbuild.templates.cpptemplates import CppProject
+from mesonbuild.templates.cstemplates import CSharpProject
+from mesonbuild.templates.ctemplates import CProject
+from mesonbuild.templates.sampleimpl import SampleImpl
+
+import argparse
+
+def sameple_generator(options: argparse.Namespace) -> SampleImpl:
+ return {
+ 'c': CProject,
+ 'cpp': CppProject,
+ 'cs': CSharpProject,
+ 'cuda': CudaProject,
+ 'objc': ObjCProject,
+ 'objcpp': ObjCppProject,
+ 'java': JavaProject,
+ 'd': DlangProject,
+ 'rust': RustProject,
+ 'fortran': FortranProject,
+ 'vala': ValaProject
+ }[options.language](options)
diff --git a/mesonbuild/templates/sampleimpl.py b/mesonbuild/templates/sampleimpl.py
new file mode 100644
index 0000000..9702ae8
--- /dev/null
+++ b/mesonbuild/templates/sampleimpl.py
@@ -0,0 +1,22 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+
+class SampleImpl:
+ def create_executable(self) -> None:
+ raise NotImplementedError('Sample implementation for "executable" not implemented!')
+
+ def create_library(self) -> None:
+ raise NotImplementedError('Sample implementation for "library" not implemented!')
diff --git a/mesonbuild/templates/valatemplates.py b/mesonbuild/templates/valatemplates.py
new file mode 100644
index 0000000..ef9794d
--- /dev/null
+++ b/mesonbuild/templates/valatemplates.py
@@ -0,0 +1,125 @@
+# Copyright 2019 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.
+from __future__ import annotations
+
+from mesonbuild.templates.sampleimpl import SampleImpl
+import re
+
+
+hello_vala_template = '''void main (string[] args) {{
+ stdout.printf ("Hello {project_name}!\\n");
+}}
+'''
+
+hello_vala_meson_template = '''project('{project_name}', ['c', 'vala'],
+ version : '{version}')
+
+dependencies = [
+ dependency('glib-2.0'),
+ dependency('gobject-2.0'),
+]
+
+exe = executable('{exe_name}', '{source_name}', dependencies : dependencies,
+ install : true)
+
+test('basic', exe)
+'''
+
+
+lib_vala_template = '''namespace {namespace} {{
+ public int sum(int a, int b) {{
+ return(a + b);
+ }}
+
+ public int square(int a) {{
+ return(a * a);
+ }}
+}}
+'''
+
+lib_vala_test_template = '''using {namespace};
+
+public void main() {{
+ stdout.printf("\nTesting shlib");
+ stdout.printf("\n\t2 + 3 is %d", sum(2, 3));
+ stdout.printf("\n\t8 squared is %d\\n", square(8));
+}}
+'''
+
+lib_vala_meson_template = '''project('{project_name}', ['c', 'vala'],
+ version : '{version}')
+
+dependencies = [
+ dependency('glib-2.0'),
+ dependency('gobject-2.0'),
+]
+
+# These arguments are only used to build the shared library
+# not the executables that use the library.
+shlib = shared_library('foo', '{source_file}',
+ dependencies: dependencies,
+ install: true,
+ install_dir: [true, true, true])
+
+test_exe = executable('{test_exe_name}', '{test_source_file}', dependencies : dependencies,
+ link_with : shlib)
+test('{test_name}', test_exe)
+
+# Make this library usable as a Meson subproject.
+{ltoken}_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ link_with : shlib)
+'''
+
+
+class ValaProject(SampleImpl):
+ def __init__(self, options):
+ super().__init__()
+ self.name = options.name
+ self.version = options.version
+
+ def create_executable(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ source_name = lowercase_token + '.vala'
+ open(source_name, 'w', encoding='utf-8').write(hello_vala_template.format(project_name=self.name))
+ open('meson.build', 'w', encoding='utf-8').write(
+ hello_vala_meson_template.format(project_name=self.name,
+ exe_name=lowercase_token,
+ source_name=source_name,
+ version=self.version))
+
+ def create_library(self) -> None:
+ lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower())
+ uppercase_token = lowercase_token.upper()
+ class_name = uppercase_token[0] + lowercase_token[1:]
+ test_exe_name = lowercase_token + '_test'
+ namespace = lowercase_token
+ lib_vala_name = lowercase_token + '.vala'
+ test_vala_name = lowercase_token + '_test.vala'
+ kwargs = {'utoken': uppercase_token,
+ 'ltoken': lowercase_token,
+ 'header_dir': lowercase_token,
+ 'class_name': class_name,
+ 'namespace': namespace,
+ 'source_file': lib_vala_name,
+ 'test_source_file': test_vala_name,
+ 'test_exe_name': test_exe_name,
+ 'project_name': self.name,
+ 'lib_name': lowercase_token,
+ 'test_name': lowercase_token,
+ 'version': self.version,
+ }
+ open(lib_vala_name, 'w', encoding='utf-8').write(lib_vala_template.format(**kwargs))
+ open(test_vala_name, 'w', encoding='utf-8').write(lib_vala_test_template.format(**kwargs))
+ open('meson.build', 'w', encoding='utf-8').write(lib_vala_meson_template.format(**kwargs))
diff --git a/mesonbuild/utils/__init__.py b/mesonbuild/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/mesonbuild/utils/__init__.py
diff --git a/mesonbuild/utils/core.py b/mesonbuild/utils/core.py
new file mode 100644
index 0000000..81f4d40
--- /dev/null
+++ b/mesonbuild/utils/core.py
@@ -0,0 +1,151 @@
+# Copyright 2012-2022 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.
+
+"""
+Contains the strict minimum to run scripts.
+
+When the backend needs to call back into Meson during compilation for running
+scripts or wrapping commands, it is important to load as little python modules
+as possible for performance reasons.
+"""
+
+from __future__ import annotations
+from dataclasses import dataclass
+import os
+import abc
+import typing as T
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Literal
+ from ..mparser import BaseNode
+ from . import programs
+
+ EnvInitValueType = T.Dict[str, T.Union[str, T.List[str]]]
+
+
+class MesonException(Exception):
+ '''Exceptions thrown by Meson'''
+
+ def __init__(self, *args: object, file: T.Optional[str] = None,
+ lineno: T.Optional[int] = None, colno: T.Optional[int] = None):
+ super().__init__(*args)
+ self.file = file
+ self.lineno = lineno
+ self.colno = colno
+
+ @classmethod
+ def from_node(cls, *args: object, node: BaseNode) -> MesonException:
+ """Create a MesonException with location data from a BaseNode
+
+ :param node: A BaseNode to set location data from
+ :return: A Meson Exception instance
+ """
+ return cls(*args, file=node.filename, lineno=node.lineno, colno=node.colno)
+
+class MesonBugException(MesonException):
+ '''Exceptions thrown when there is a clear Meson bug that should be reported'''
+
+ def __init__(self, msg: str, file: T.Optional[str] = None,
+ lineno: T.Optional[int] = None, colno: T.Optional[int] = None):
+ super().__init__(msg + '\n\n This is a Meson bug and should be reported!',
+ file=file, lineno=lineno, colno=colno)
+
+class HoldableObject(metaclass=abc.ABCMeta):
+ ''' Dummy base class for all objects that can be
+ held by an interpreter.baseobjects.ObjectHolder '''
+
+class EnvironmentVariables(HoldableObject):
+ def __init__(self, values: T.Optional[EnvInitValueType] = None,
+ init_method: Literal['set', 'prepend', 'append'] = 'set', separator: str = os.pathsep) -> None:
+ self.envvars: T.List[T.Tuple[T.Callable[[T.Dict[str, str], str, T.List[str], str], str], str, T.List[str], str]] = []
+ # The set of all env vars we have operations for. Only used for self.has_name()
+ self.varnames: T.Set[str] = set()
+
+ if values:
+ init_func = getattr(self, init_method)
+ for name, value in values.items():
+ v = value if isinstance(value, list) else [value]
+ init_func(name, v, separator)
+
+ def __repr__(self) -> str:
+ repr_str = "<{0}: {1}>"
+ return repr_str.format(self.__class__.__name__, self.envvars)
+
+ def hash(self, hasher: T.Any):
+ myenv = self.get_env({})
+ for key in sorted(myenv.keys()):
+ hasher.update(bytes(key, encoding='utf-8'))
+ hasher.update(b',')
+ hasher.update(bytes(myenv[key], encoding='utf-8'))
+ hasher.update(b';')
+
+ def has_name(self, name: str) -> bool:
+ return name in self.varnames
+
+ def get_names(self) -> T.Set[str]:
+ return self.varnames
+
+ def set(self, name: str, values: T.List[str], separator: str = os.pathsep) -> None:
+ self.varnames.add(name)
+ self.envvars.append((self._set, name, values, separator))
+
+ def append(self, name: str, values: T.List[str], separator: str = os.pathsep) -> None:
+ self.varnames.add(name)
+ self.envvars.append((self._append, name, values, separator))
+
+ def prepend(self, name: str, values: T.List[str], separator: str = os.pathsep) -> None:
+ self.varnames.add(name)
+ self.envvars.append((self._prepend, name, values, separator))
+
+ @staticmethod
+ def _set(env: T.Dict[str, str], name: str, values: T.List[str], separator: str, default_value: T.Optional[str]) -> str:
+ return separator.join(values)
+
+ @staticmethod
+ def _append(env: T.Dict[str, str], name: str, values: T.List[str], separator: str, default_value: T.Optional[str]) -> str:
+ curr = env.get(name, default_value)
+ return separator.join(values if curr is None else [curr] + values)
+
+ @staticmethod
+ def _prepend(env: T.Dict[str, str], name: str, values: T.List[str], separator: str, default_value: T.Optional[str]) -> str:
+ curr = env.get(name, default_value)
+ return separator.join(values if curr is None else values + [curr])
+
+ def get_env(self, full_env: T.MutableMapping[str, str], dump: bool = False) -> T.Dict[str, str]:
+ env = full_env.copy()
+ for method, name, values, separator in self.envvars:
+ default_value = f'${name}' if dump else None
+ env[name] = method(env, name, values, separator, default_value)
+ return env
+
+
+@dataclass(eq=False)
+class ExecutableSerialisation:
+
+ # XXX: should capture and feed default to False, instead of None?
+
+ cmd_args: T.List[str]
+ env: T.Optional[EnvironmentVariables] = None
+ exe_wrapper: T.Optional['programs.ExternalProgram'] = None
+ workdir: T.Optional[str] = None
+ extra_paths: T.Optional[T.List] = None
+ capture: T.Optional[bool] = None
+ feed: T.Optional[bool] = None
+ tag: T.Optional[str] = None
+ verbose: bool = False
+
+ def __post_init__(self) -> None:
+ self.pickled = False
+ self.skip_if_destdir = False
+ self.subproject = ''
diff --git a/mesonbuild/utils/platform.py b/mesonbuild/utils/platform.py
new file mode 100644
index 0000000..4a3927d
--- /dev/null
+++ b/mesonbuild/utils/platform.py
@@ -0,0 +1,38 @@
+# SPDX-license-identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# 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.
+from __future__ import annotations
+
+"""base classes providing no-op functionality.."""
+
+import os
+import typing as T
+
+from .. import mlog
+
+__all__ = ['BuildDirLock']
+
+# This needs to be inherited by the specific implementations to make type
+# checking happy
+class BuildDirLock:
+
+ def __init__(self, builddir: str) -> None:
+ self.lockfilename = os.path.join(builddir, 'meson-private/meson.lock')
+
+ def __enter__(self) -> None:
+ mlog.debug('Calling the no-op version of BuildDirLock')
+
+ def __exit__(self, *args: T.Any) -> None:
+ pass
diff --git a/mesonbuild/utils/posix.py b/mesonbuild/utils/posix.py
new file mode 100644
index 0000000..51c3cd0
--- /dev/null
+++ b/mesonbuild/utils/posix.py
@@ -0,0 +1,43 @@
+# SPDX-license-identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# 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.
+from __future__ import annotations
+
+"""Posix specific implementations of mesonlib functionality."""
+
+import fcntl
+import typing as T
+
+from .universal import MesonException
+from .platform import BuildDirLock as BuildDirLockBase
+
+__all__ = ['BuildDirLock']
+
+class BuildDirLock(BuildDirLockBase):
+
+ def __enter__(self) -> None:
+ self.lockfile = open(self.lockfilename, 'w', encoding='utf-8')
+ try:
+ fcntl.flock(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except (BlockingIOError, PermissionError):
+ self.lockfile.close()
+ raise MesonException('Some other Meson process is already using this build directory. Exiting.')
+ except OSError as e:
+ self.lockfile.close()
+ raise MesonException(f'Failed to lock the build directory: {e.strerror}')
+
+ def __exit__(self, *args: T.Any) -> None:
+ fcntl.flock(self.lockfile, fcntl.LOCK_UN)
+ self.lockfile.close()
diff --git a/mesonbuild/utils/universal.py b/mesonbuild/utils/universal.py
new file mode 100644
index 0000000..270ec2a
--- /dev/null
+++ b/mesonbuild/utils/universal.py
@@ -0,0 +1,2365 @@
+# Copyright 2012-2020 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.
+
+"""A library of random helper functionality."""
+
+from __future__ import annotations
+from pathlib import Path
+import argparse
+import enum
+import sys
+import stat
+import time
+import abc
+import platform, subprocess, operator, os, shlex, shutil, re
+import collections
+from functools import lru_cache, wraps, total_ordering
+from itertools import tee
+from tempfile import TemporaryDirectory, NamedTemporaryFile
+import typing as T
+import textwrap
+import copy
+import pickle
+import errno
+
+from mesonbuild import mlog
+from .core import MesonException, HoldableObject
+
+if T.TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from .._typing import ImmutableListProtocol
+ from ..build import ConfigurationData
+ from ..coredata import KeyedOptionDictType, UserOption
+ from ..compilers.compilers import Compiler
+
+FileOrString = T.Union['File', str]
+
+_T = T.TypeVar('_T')
+_U = T.TypeVar('_U')
+
+__all__ = [
+ 'GIT',
+ 'python_command',
+ 'project_meson_versions',
+ 'SecondLevelHolder',
+ 'File',
+ 'FileMode',
+ 'GitException',
+ 'LibType',
+ 'MachineChoice',
+ 'EnvironmentException',
+ 'FileOrString',
+ 'GitException',
+ 'OptionKey',
+ 'dump_conf_header',
+ 'OptionOverrideProxy',
+ 'OptionType',
+ 'OrderedSet',
+ 'PerMachine',
+ 'PerMachineDefaultable',
+ 'PerThreeMachine',
+ 'PerThreeMachineDefaultable',
+ 'ProgressBar',
+ 'RealPathAction',
+ 'TemporaryDirectoryWinProof',
+ 'Version',
+ 'check_direntry_issues',
+ 'classify_unity_sources',
+ 'current_vs_supports_modules',
+ 'darwin_get_object_archs',
+ 'default_libdir',
+ 'default_libexecdir',
+ 'default_prefix',
+ 'default_datadir',
+ 'default_includedir',
+ 'default_infodir',
+ 'default_localedir',
+ 'default_mandir',
+ 'default_sbindir',
+ 'default_sysconfdir',
+ 'detect_subprojects',
+ 'detect_vcs',
+ 'do_conf_file',
+ 'do_conf_str',
+ 'do_replacement',
+ 'exe_exists',
+ 'expand_arguments',
+ 'extract_as_list',
+ 'first',
+ 'generate_list',
+ 'get_compiler_for_source',
+ 'get_filenames_templates_dict',
+ 'get_variable_regex',
+ 'get_wine_shortpath',
+ 'git',
+ 'has_path_sep',
+ 'is_aix',
+ 'is_android',
+ 'is_ascii_string',
+ 'is_cygwin',
+ 'is_debianlike',
+ 'is_dragonflybsd',
+ 'is_freebsd',
+ 'is_haiku',
+ 'is_hurd',
+ 'is_irix',
+ 'is_linux',
+ 'is_netbsd',
+ 'is_openbsd',
+ 'is_osx',
+ 'is_qnx',
+ 'is_sunos',
+ 'is_windows',
+ 'is_wsl',
+ 'iter_regexin_iter',
+ 'join_args',
+ 'listify',
+ 'partition',
+ 'path_is_in_root',
+ 'pickle_load',
+ 'Popen_safe',
+ 'quiet_git',
+ 'quote_arg',
+ 'relative_to_if_possible',
+ 'relpath',
+ 'replace_if_different',
+ 'run_once',
+ 'get_meson_command',
+ 'set_meson_command',
+ 'split_args',
+ 'stringlistify',
+ 'substitute_values',
+ 'substring_is_in_list',
+ 'typeslistify',
+ 'verbose_git',
+ 'version_compare',
+ 'version_compare_condition_with_min',
+ 'version_compare_many',
+ 'search_version',
+ 'windows_detect_native_arch',
+ 'windows_proof_rm',
+ 'windows_proof_rmtree',
+]
+
+
+# TODO: this is such a hack, this really should be either in coredata or in the
+# interpreter
+# {subproject: project_meson_version}
+project_meson_versions = collections.defaultdict(str) # type: T.DefaultDict[str, str]
+
+
+from glob import glob
+
+if os.path.basename(sys.executable) == 'meson.exe':
+ # In Windows and using the MSI installed executable.
+ python_command = [sys.executable, 'runpython']
+else:
+ python_command = [sys.executable]
+_meson_command: T.Optional['ImmutableListProtocol[str]'] = None
+
+
+class EnvironmentException(MesonException):
+ '''Exceptions thrown while processing and creating the build environment'''
+
+class GitException(MesonException):
+ def __init__(self, msg: str, output: T.Optional[str] = None):
+ super().__init__(msg)
+ self.output = output.strip() if output else ''
+
+GIT = shutil.which('git')
+def git(cmd: T.List[str], workingdir: T.Union[str, bytes, os.PathLike], check: bool = False, **kwargs: T.Any) -> T.Tuple[subprocess.Popen, str, str]:
+ cmd = [GIT] + cmd
+ p, o, e = Popen_safe(cmd, cwd=workingdir, **kwargs)
+ if check and p.returncode != 0:
+ raise GitException('Git command failed: ' + str(cmd), e)
+ return p, o, e
+
+def quiet_git(cmd: T.List[str], workingdir: T.Union[str, bytes, os.PathLike], check: bool = False) -> T.Tuple[bool, str]:
+ if not GIT:
+ m = 'Git program not found.'
+ if check:
+ raise GitException(m)
+ return False, m
+ p, o, e = git(cmd, workingdir, check)
+ if p.returncode != 0:
+ return False, e
+ return True, o
+
+def verbose_git(cmd: T.List[str], workingdir: T.Union[str, bytes, os.PathLike], check: bool = False) -> bool:
+ if not GIT:
+ m = 'Git program not found.'
+ if check:
+ raise GitException(m)
+ return False
+ p, _, _ = git(cmd, workingdir, check, stdout=None, stderr=None)
+ return p.returncode == 0
+
+def set_meson_command(mainfile: str) -> None:
+ global _meson_command # pylint: disable=global-statement
+ # On UNIX-like systems `meson` is a Python script
+ # On Windows `meson` and `meson.exe` are wrapper exes
+ if not mainfile.endswith('.py'):
+ _meson_command = [mainfile]
+ elif os.path.isabs(mainfile) and mainfile.endswith('mesonmain.py'):
+ # Can't actually run meson with an absolute path to mesonmain.py, it must be run as -m mesonbuild.mesonmain
+ _meson_command = python_command + ['-m', 'mesonbuild.mesonmain']
+ else:
+ # Either run uninstalled, or full path to meson-script.py
+ _meson_command = python_command + [mainfile]
+ # We print this value for unit tests.
+ if 'MESON_COMMAND_TESTS' in os.environ:
+ mlog.log(f'meson_command is {_meson_command!r}')
+
+
+def get_meson_command() -> T.Optional['ImmutableListProtocol[str]']:
+ return _meson_command
+
+
+def is_ascii_string(astring: T.Union[str, bytes]) -> bool:
+ try:
+ if isinstance(astring, str):
+ astring.encode('ascii')
+ elif isinstance(astring, bytes):
+ astring.decode('ascii')
+ except UnicodeDecodeError:
+ return False
+ return True
+
+
+def check_direntry_issues(direntry_array: T.Union[T.Iterable[T.Union[str, bytes]], str, bytes]) -> None:
+ import locale
+ # Warn if the locale is not UTF-8. This can cause various unfixable issues
+ # such as os.stat not being able to decode filenames with unicode in them.
+ # There is no way to reset both the preferred encoding and the filesystem
+ # encoding, so we can just warn about it.
+ e = locale.getpreferredencoding()
+ if e.upper() != 'UTF-8' and not is_windows():
+ if isinstance(direntry_array, (str, bytes)):
+ direntry_array = [direntry_array]
+ for de in direntry_array:
+ if is_ascii_string(de):
+ continue
+ mlog.warning(textwrap.dedent(f'''
+ You are using {e!r} which is not a Unicode-compatible
+ locale but you are trying to access a file system entry called {de!r} which is
+ not pure ASCII. This may cause problems.
+ '''), file=sys.stderr)
+
+class SecondLevelHolder(HoldableObject, metaclass=abc.ABCMeta):
+ ''' A second level object holder. The primary purpose
+ of such objects is to hold multiple objects with one
+ default option. '''
+
+ @abc.abstractmethod
+ def get_default_object(self) -> HoldableObject: ...
+
+class FileMode:
+ # The first triad is for owner permissions, the second for group permissions,
+ # and the third for others (everyone else).
+ # For the 1st character:
+ # 'r' means can read
+ # '-' means not allowed
+ # For the 2nd character:
+ # 'w' means can write
+ # '-' means not allowed
+ # For the 3rd character:
+ # 'x' means can execute
+ # 's' means can execute and setuid/setgid is set (owner/group triads only)
+ # 'S' means cannot execute and setuid/setgid is set (owner/group triads only)
+ # 't' means can execute and sticky bit is set ("others" triads only)
+ # 'T' means cannot execute and sticky bit is set ("others" triads only)
+ # '-' means none of these are allowed
+ #
+ # The meanings of 'rwx' perms is not obvious for directories; see:
+ # https://www.hackinglinuxexposed.com/articles/20030424.html
+ #
+ # For information on this notation such as setuid/setgid/sticky bits, see:
+ # https://en.wikipedia.org/wiki/File_system_permissions#Symbolic_notation
+ symbolic_perms_regex = re.compile('[r-][w-][xsS-]' # Owner perms
+ '[r-][w-][xsS-]' # Group perms
+ '[r-][w-][xtT-]') # Others perms
+
+ def __init__(self, perms: T.Optional[str] = None, owner: T.Union[str, int, None] = None,
+ group: T.Union[str, int, None] = None):
+ self.perms_s = perms
+ self.perms = self.perms_s_to_bits(perms)
+ self.owner = owner
+ self.group = group
+
+ def __repr__(self) -> str:
+ ret = '<FileMode: {!r} owner={} group={}'
+ return ret.format(self.perms_s, self.owner, self.group)
+
+ @classmethod
+ def perms_s_to_bits(cls, perms_s: T.Optional[str]) -> int:
+ '''
+ Does the opposite of stat.filemode(), converts strings of the form
+ 'rwxr-xr-x' to st_mode enums which can be passed to os.chmod()
+ '''
+ if perms_s is None:
+ # No perms specified, we will not touch the permissions
+ return -1
+ eg = 'rwxr-xr-x'
+ if not isinstance(perms_s, str):
+ raise MesonException(f'Install perms must be a string. For example, {eg!r}')
+ if len(perms_s) != 9 or not cls.symbolic_perms_regex.match(perms_s):
+ raise MesonException(f'File perms {perms_s!r} must be exactly 9 chars. For example, {eg!r}')
+ perms = 0
+ # Owner perms
+ if perms_s[0] == 'r':
+ perms |= stat.S_IRUSR
+ if perms_s[1] == 'w':
+ perms |= stat.S_IWUSR
+ if perms_s[2] == 'x':
+ perms |= stat.S_IXUSR
+ elif perms_s[2] == 'S':
+ perms |= stat.S_ISUID
+ elif perms_s[2] == 's':
+ perms |= stat.S_IXUSR
+ perms |= stat.S_ISUID
+ # Group perms
+ if perms_s[3] == 'r':
+ perms |= stat.S_IRGRP
+ if perms_s[4] == 'w':
+ perms |= stat.S_IWGRP
+ if perms_s[5] == 'x':
+ perms |= stat.S_IXGRP
+ elif perms_s[5] == 'S':
+ perms |= stat.S_ISGID
+ elif perms_s[5] == 's':
+ perms |= stat.S_IXGRP
+ perms |= stat.S_ISGID
+ # Others perms
+ if perms_s[6] == 'r':
+ perms |= stat.S_IROTH
+ if perms_s[7] == 'w':
+ perms |= stat.S_IWOTH
+ if perms_s[8] == 'x':
+ perms |= stat.S_IXOTH
+ elif perms_s[8] == 'T':
+ perms |= stat.S_ISVTX
+ elif perms_s[8] == 't':
+ perms |= stat.S_IXOTH
+ perms |= stat.S_ISVTX
+ return perms
+
+dot_C_dot_H_warning = """You are using .C or .H files in your project. This is deprecated.
+ Currently, Meson treats this as C++ code, but they
+ used to be treated as C code.
+ Note that the situation is a bit more complex if you are using the
+ Visual Studio compiler, as it treats .C files as C code, unless you add
+ the /TP compiler flag, but this is unreliable.
+ See https://github.com/mesonbuild/meson/pull/8747 for the discussions."""
+class File(HoldableObject):
+ def __init__(self, is_built: bool, subdir: str, fname: str):
+ if fname.endswith(".C") or fname.endswith(".H"):
+ mlog.warning(dot_C_dot_H_warning, once=True)
+ self.is_built = is_built
+ self.subdir = subdir
+ self.fname = fname
+ self.hash = hash((is_built, subdir, fname))
+
+ def __str__(self) -> str:
+ return self.relative_name()
+
+ def __repr__(self) -> str:
+ ret = '<File: {0}'
+ if not self.is_built:
+ ret += ' (not built)'
+ ret += '>'
+ return ret.format(self.relative_name())
+
+ @staticmethod
+ @lru_cache(maxsize=None)
+ def from_source_file(source_root: str, subdir: str, fname: str) -> 'File':
+ if not os.path.isfile(os.path.join(source_root, subdir, fname)):
+ raise MesonException(f'File {fname} does not exist.')
+ return File(False, subdir, fname)
+
+ @staticmethod
+ def from_built_file(subdir: str, fname: str) -> 'File':
+ return File(True, subdir, fname)
+
+ @staticmethod
+ def from_built_relative(relative: str) -> 'File':
+ dirpart, fnamepart = os.path.split(relative)
+ return File(True, dirpart, fnamepart)
+
+ @staticmethod
+ def from_absolute_file(fname: str) -> 'File':
+ return File(False, '', fname)
+
+ @lru_cache(maxsize=None)
+ def rel_to_builddir(self, build_to_src: str) -> str:
+ if self.is_built:
+ return self.relative_name()
+ else:
+ return os.path.join(build_to_src, self.subdir, self.fname)
+
+ @lru_cache(maxsize=None)
+ def absolute_path(self, srcdir: str, builddir: str) -> str:
+ absdir = srcdir
+ if self.is_built:
+ absdir = builddir
+ return os.path.join(absdir, self.relative_name())
+
+ @property
+ def suffix(self) -> str:
+ return os.path.splitext(self.fname)[1][1:].lower()
+
+ def endswith(self, ending: T.Union[str, T.Tuple[str, ...]]) -> bool:
+ return self.fname.endswith(ending)
+
+ def split(self, s: str, maxsplit: int = -1) -> T.List[str]:
+ return self.fname.split(s, maxsplit=maxsplit)
+
+ def rsplit(self, s: str, maxsplit: int = -1) -> T.List[str]:
+ return self.fname.rsplit(s, maxsplit=maxsplit)
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, File):
+ return NotImplemented
+ if self.hash != other.hash:
+ return False
+ return (self.fname, self.subdir, self.is_built) == (other.fname, other.subdir, other.is_built)
+
+ def __hash__(self) -> int:
+ return self.hash
+
+ @lru_cache(maxsize=None)
+ def relative_name(self) -> str:
+ return os.path.join(self.subdir, self.fname)
+
+
+def get_compiler_for_source(compilers: T.Iterable['Compiler'], src: 'FileOrString') -> 'Compiler':
+ """Given a set of compilers and a source, find the compiler for that source type."""
+ for comp in compilers:
+ if comp.can_compile(src):
+ return comp
+ raise MesonException(f'No specified compiler can handle file {src!s}')
+
+
+def classify_unity_sources(compilers: T.Iterable['Compiler'], sources: T.Sequence['FileOrString']) -> T.Dict['Compiler', T.List['FileOrString']]:
+ compsrclist: T.Dict['Compiler', T.List['FileOrString']] = {}
+ for src in sources:
+ comp = get_compiler_for_source(compilers, src)
+ if comp not in compsrclist:
+ compsrclist[comp] = [src]
+ else:
+ compsrclist[comp].append(src)
+ return compsrclist
+
+
+class MachineChoice(enum.IntEnum):
+
+ """Enum class representing one of the two abstract machine names used in
+ most places: the build, and host, machines.
+ """
+
+ BUILD = 0
+ HOST = 1
+
+ def get_lower_case_name(self) -> str:
+ return PerMachine('build', 'host')[self]
+
+ def get_prefix(self) -> str:
+ return PerMachine('build.', '')[self]
+
+
+class PerMachine(T.Generic[_T]):
+ def __init__(self, build: _T, host: _T) -> None:
+ self.build = build
+ self.host = host
+
+ def __getitem__(self, machine: MachineChoice) -> _T:
+ return {
+ MachineChoice.BUILD: self.build,
+ MachineChoice.HOST: self.host,
+ }[machine]
+
+ def __setitem__(self, machine: MachineChoice, val: _T) -> None:
+ setattr(self, machine.get_lower_case_name(), val)
+
+ def miss_defaulting(self) -> "PerMachineDefaultable[T.Optional[_T]]":
+ """Unset definition duplicated from their previous to None
+
+ This is the inverse of ''default_missing''. By removing defaulted
+ machines, we can elaborate the original and then redefault them and thus
+ avoid repeating the elaboration explicitly.
+ """
+ unfreeze = PerMachineDefaultable() # type: PerMachineDefaultable[T.Optional[_T]]
+ unfreeze.build = self.build
+ unfreeze.host = self.host
+ if unfreeze.host == unfreeze.build:
+ unfreeze.host = None
+ return unfreeze
+
+ def __repr__(self) -> str:
+ return f'PerMachine({self.build!r}, {self.host!r})'
+
+
+class PerThreeMachine(PerMachine[_T]):
+ """Like `PerMachine` but includes `target` too.
+
+ It turns out just one thing do we need track the target machine. There's no
+ need to computer the `target` field so we don't bother overriding the
+ `__getitem__`/`__setitem__` methods.
+ """
+ def __init__(self, build: _T, host: _T, target: _T) -> None:
+ super().__init__(build, host)
+ self.target = target
+
+ def miss_defaulting(self) -> "PerThreeMachineDefaultable[T.Optional[_T]]":
+ """Unset definition duplicated from their previous to None
+
+ This is the inverse of ''default_missing''. By removing defaulted
+ machines, we can elaborate the original and then redefault them and thus
+ avoid repeating the elaboration explicitly.
+ """
+ unfreeze = PerThreeMachineDefaultable() # type: PerThreeMachineDefaultable[T.Optional[_T]]
+ unfreeze.build = self.build
+ unfreeze.host = self.host
+ unfreeze.target = self.target
+ if unfreeze.target == unfreeze.host:
+ unfreeze.target = None
+ if unfreeze.host == unfreeze.build:
+ unfreeze.host = None
+ return unfreeze
+
+ def matches_build_machine(self, machine: MachineChoice) -> bool:
+ return self.build == self[machine]
+
+ def __repr__(self) -> str:
+ return f'PerThreeMachine({self.build!r}, {self.host!r}, {self.target!r})'
+
+
+class PerMachineDefaultable(PerMachine[T.Optional[_T]]):
+ """Extends `PerMachine` with the ability to default from `None`s.
+ """
+ def __init__(self, build: T.Optional[_T] = None, host: T.Optional[_T] = None) -> None:
+ super().__init__(build, host)
+
+ def default_missing(self) -> "PerMachine[_T]":
+ """Default host to build
+
+ This allows just specifying nothing in the native case, and just host in the
+ cross non-compiler case.
+ """
+ freeze = PerMachine(self.build, self.host)
+ if freeze.host is None:
+ freeze.host = freeze.build
+ return freeze
+
+ def __repr__(self) -> str:
+ return f'PerMachineDefaultable({self.build!r}, {self.host!r})'
+
+ @classmethod
+ def default(cls, is_cross: bool, build: _T, host: _T) -> PerMachine[_T]:
+ """Easy way to get a defaulted value
+
+ This allows simplifying the case where you can control whether host and
+ build are separate or not with a boolean. If the is_cross value is set
+ to true then the optional host value will be used, otherwise the host
+ will be set to the build value.
+ """
+ m = cls(build)
+ if is_cross:
+ m.host = host
+ return m.default_missing()
+
+
+class PerThreeMachineDefaultable(PerMachineDefaultable, PerThreeMachine[T.Optional[_T]]):
+ """Extends `PerThreeMachine` with the ability to default from `None`s.
+ """
+ def __init__(self) -> None:
+ PerThreeMachine.__init__(self, None, None, None)
+
+ def default_missing(self) -> "PerThreeMachine[T.Optional[_T]]":
+ """Default host to build and target to host.
+
+ This allows just specifying nothing in the native case, just host in the
+ cross non-compiler case, and just target in the native-built
+ cross-compiler case.
+ """
+ freeze = PerThreeMachine(self.build, self.host, self.target)
+ if freeze.host is None:
+ freeze.host = freeze.build
+ if freeze.target is None:
+ freeze.target = freeze.host
+ return freeze
+
+ def __repr__(self) -> str:
+ return f'PerThreeMachineDefaultable({self.build!r}, {self.host!r}, {self.target!r})'
+
+
+def is_sunos() -> bool:
+ return platform.system().lower() == 'sunos'
+
+
+def is_osx() -> bool:
+ return platform.system().lower() == 'darwin'
+
+
+def is_linux() -> bool:
+ return platform.system().lower() == 'linux'
+
+
+def is_android() -> bool:
+ return platform.system().lower() == 'android'
+
+
+def is_haiku() -> bool:
+ return platform.system().lower() == 'haiku'
+
+
+def is_openbsd() -> bool:
+ return platform.system().lower() == 'openbsd'
+
+
+def is_windows() -> bool:
+ platname = platform.system().lower()
+ return platname == 'windows'
+
+def is_wsl() -> bool:
+ return is_linux() and 'microsoft' in platform.release().lower()
+
+def is_cygwin() -> bool:
+ return sys.platform == 'cygwin'
+
+
+def is_debianlike() -> bool:
+ return os.path.isfile('/etc/debian_version')
+
+
+def is_dragonflybsd() -> bool:
+ return platform.system().lower() == 'dragonfly'
+
+
+def is_netbsd() -> bool:
+ return platform.system().lower() == 'netbsd'
+
+
+def is_freebsd() -> bool:
+ return platform.system().lower() == 'freebsd'
+
+def is_irix() -> bool:
+ return platform.system().startswith('irix')
+
+def is_hurd() -> bool:
+ return platform.system().lower() == 'gnu'
+
+def is_qnx() -> bool:
+ return platform.system().lower() == 'qnx'
+
+def is_aix() -> bool:
+ return platform.system().lower() == 'aix'
+
+def exe_exists(arglist: T.List[str]) -> bool:
+ try:
+ if subprocess.run(arglist, timeout=10).returncode == 0:
+ return True
+ except (FileNotFoundError, subprocess.TimeoutExpired):
+ pass
+ return False
+
+
+@lru_cache(maxsize=None)
+def darwin_get_object_archs(objpath: str) -> 'ImmutableListProtocol[str]':
+ '''
+ For a specific object (executable, static library, dylib, etc), run `lipo`
+ to fetch the list of archs supported by it. Supports both thin objects and
+ 'fat' objects.
+ '''
+ _, stdo, stderr = Popen_safe(['lipo', '-info', objpath])
+ if not stdo:
+ mlog.debug(f'lipo {objpath}: {stderr}')
+ return None
+ stdo = stdo.rsplit(': ', 1)[1]
+ # Convert from lipo-style archs to meson-style CPUs
+ stdo = stdo.replace('i386', 'x86')
+ stdo = stdo.replace('arm64', 'aarch64')
+ stdo = stdo.replace('ppc7400', 'ppc')
+ stdo = stdo.replace('ppc970', 'ppc')
+ # Add generic name for armv7 and armv7s
+ if 'armv7' in stdo:
+ stdo += ' arm'
+ return stdo.split()
+
+def windows_detect_native_arch() -> str:
+ """
+ The architecture of Windows itself: x86, amd64 or arm64
+ """
+ if sys.platform != 'win32':
+ return ''
+ try:
+ import ctypes
+ process_arch = ctypes.c_ushort()
+ native_arch = ctypes.c_ushort()
+ kernel32 = ctypes.windll.kernel32
+ process = ctypes.c_void_p(kernel32.GetCurrentProcess())
+ # This is the only reliable way to detect an arm system if we are an x86/x64 process being emulated
+ if kernel32.IsWow64Process2(process, ctypes.byref(process_arch), ctypes.byref(native_arch)):
+ # https://docs.microsoft.com/en-us/windows/win32/sysinfo/image-file-machine-constants
+ if native_arch.value == 0x8664:
+ return 'amd64'
+ elif native_arch.value == 0x014C:
+ return 'x86'
+ elif native_arch.value == 0xAA64:
+ return 'arm64'
+ elif native_arch.value == 0x01C4:
+ return 'arm'
+ except (OSError, AttributeError):
+ pass
+ # These env variables are always available. See:
+ # https://msdn.microsoft.com/en-us/library/aa384274(VS.85).aspx
+ # https://blogs.msdn.microsoft.com/david.wang/2006/03/27/howto-detect-process-bitness/
+ arch = os.environ.get('PROCESSOR_ARCHITEW6432', '').lower()
+ if not arch:
+ try:
+ # If this doesn't exist, something is messing with the environment
+ arch = os.environ['PROCESSOR_ARCHITECTURE'].lower()
+ except KeyError:
+ raise EnvironmentException('Unable to detect native OS architecture')
+ return arch
+
+def detect_vcs(source_dir: T.Union[str, Path]) -> T.Optional[T.Dict[str, str]]:
+ vcs_systems = [
+ {
+ 'name': 'git',
+ 'cmd': 'git',
+ 'repo_dir': '.git',
+ 'get_rev': 'git describe --dirty=+',
+ 'rev_regex': '(.*)',
+ 'dep': '.git/logs/HEAD'
+ },
+ {
+ 'name': 'mercurial',
+ 'cmd': 'hg',
+ 'repo_dir': '.hg',
+ 'get_rev': 'hg id -i',
+ 'rev_regex': '(.*)',
+ 'dep': '.hg/dirstate'
+ },
+ {
+ 'name': 'subversion',
+ 'cmd': 'svn',
+ 'repo_dir': '.svn',
+ 'get_rev': 'svn info',
+ 'rev_regex': 'Revision: (.*)',
+ 'dep': '.svn/wc.db'
+ },
+ {
+ 'name': 'bazaar',
+ 'cmd': 'bzr',
+ 'repo_dir': '.bzr',
+ 'get_rev': 'bzr revno',
+ 'rev_regex': '(.*)',
+ 'dep': '.bzr'
+ },
+ ]
+ if isinstance(source_dir, str):
+ source_dir = Path(source_dir)
+
+ parent_paths_and_self = collections.deque(source_dir.parents)
+ # Prepend the source directory to the front so we can check it;
+ # source_dir.parents doesn't include source_dir
+ parent_paths_and_self.appendleft(source_dir)
+ for curdir in parent_paths_and_self:
+ for vcs in vcs_systems:
+ if Path.is_dir(curdir.joinpath(vcs['repo_dir'])) and shutil.which(vcs['cmd']):
+ vcs['wc_dir'] = str(curdir)
+ return vcs
+ return None
+
+def current_vs_supports_modules() -> bool:
+ vsver = os.environ.get('VSCMD_VER', '')
+ nums = vsver.split('.', 2)
+ major = int(nums[0])
+ if major >= 17:
+ return True
+ if major == 16 and int(nums[1]) >= 10:
+ return True
+ return vsver.startswith('16.9.0') and '-pre.' in vsver
+
+# a helper class which implements the same version ordering as RPM
+class Version:
+ def __init__(self, s: str) -> None:
+ self._s = s
+
+ # split into numeric, alphabetic and non-alphanumeric sequences
+ sequences1 = re.finditer(r'(\d+|[a-zA-Z]+|[^a-zA-Z\d]+)', s)
+
+ # non-alphanumeric separators are discarded
+ sequences2 = [m for m in sequences1 if not re.match(r'[^a-zA-Z\d]+', m.group(1))]
+
+ # numeric sequences are converted from strings to ints
+ sequences3 = [int(m.group(1)) if m.group(1).isdigit() else m.group(1) for m in sequences2]
+
+ self._v = sequences3
+
+ def __str__(self) -> str:
+ return '{} (V={})'.format(self._s, str(self._v))
+
+ def __repr__(self) -> str:
+ return f'<Version: {self._s}>'
+
+ def __lt__(self, other: object) -> bool:
+ if isinstance(other, Version):
+ return self.__cmp(other, operator.lt)
+ return NotImplemented
+
+ def __gt__(self, other: object) -> bool:
+ if isinstance(other, Version):
+ return self.__cmp(other, operator.gt)
+ return NotImplemented
+
+ def __le__(self, other: object) -> bool:
+ if isinstance(other, Version):
+ return self.__cmp(other, operator.le)
+ return NotImplemented
+
+ def __ge__(self, other: object) -> bool:
+ if isinstance(other, Version):
+ return self.__cmp(other, operator.ge)
+ return NotImplemented
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, Version):
+ return self._v == other._v
+ return NotImplemented
+
+ def __ne__(self, other: object) -> bool:
+ if isinstance(other, Version):
+ return self._v != other._v
+ return NotImplemented
+
+ def __cmp(self, other: 'Version', comparator: T.Callable[[T.Any, T.Any], bool]) -> bool:
+ # compare each sequence in order
+ for ours, theirs in zip(self._v, other._v):
+ # sort a non-digit sequence before a digit sequence
+ ours_is_int = isinstance(ours, int)
+ theirs_is_int = isinstance(theirs, int)
+ if ours_is_int != theirs_is_int:
+ return comparator(ours_is_int, theirs_is_int)
+
+ if ours != theirs:
+ return comparator(ours, theirs)
+
+ # if equal length, all components have matched, so equal
+ # otherwise, the version with a suffix remaining is greater
+ return comparator(len(self._v), len(other._v))
+
+
+def _version_extract_cmpop(vstr2: str) -> T.Tuple[T.Callable[[T.Any, T.Any], bool], str]:
+ if vstr2.startswith('>='):
+ cmpop = operator.ge
+ vstr2 = vstr2[2:]
+ elif vstr2.startswith('<='):
+ cmpop = operator.le
+ vstr2 = vstr2[2:]
+ elif vstr2.startswith('!='):
+ cmpop = operator.ne
+ vstr2 = vstr2[2:]
+ elif vstr2.startswith('=='):
+ cmpop = operator.eq
+ vstr2 = vstr2[2:]
+ elif vstr2.startswith('='):
+ cmpop = operator.eq
+ vstr2 = vstr2[1:]
+ elif vstr2.startswith('>'):
+ cmpop = operator.gt
+ vstr2 = vstr2[1:]
+ elif vstr2.startswith('<'):
+ cmpop = operator.lt
+ vstr2 = vstr2[1:]
+ else:
+ cmpop = operator.eq
+
+ return (cmpop, vstr2)
+
+
+def version_compare(vstr1: str, vstr2: str) -> bool:
+ (cmpop, vstr2) = _version_extract_cmpop(vstr2)
+ return cmpop(Version(vstr1), Version(vstr2))
+
+
+def version_compare_many(vstr1: str, conditions: T.Union[str, T.Iterable[str]]) -> T.Tuple[bool, T.List[str], T.List[str]]:
+ if isinstance(conditions, str):
+ conditions = [conditions]
+ found = []
+ not_found = []
+ for req in conditions:
+ if not version_compare(vstr1, req):
+ not_found.append(req)
+ else:
+ found.append(req)
+ return not not_found, not_found, found
+
+
+# determine if the minimum version satisfying the condition |condition| exceeds
+# the minimum version for a feature |minimum|
+def version_compare_condition_with_min(condition: str, minimum: str) -> bool:
+ if condition.startswith('>='):
+ cmpop = operator.le
+ condition = condition[2:]
+ elif condition.startswith('<='):
+ return False
+ elif condition.startswith('!='):
+ return False
+ elif condition.startswith('=='):
+ cmpop = operator.le
+ condition = condition[2:]
+ elif condition.startswith('='):
+ cmpop = operator.le
+ condition = condition[1:]
+ elif condition.startswith('>'):
+ cmpop = operator.lt
+ condition = condition[1:]
+ elif condition.startswith('<'):
+ return False
+ else:
+ cmpop = operator.le
+
+ # Declaring a project(meson_version: '>=0.46') and then using features in
+ # 0.46.0 is valid, because (knowing the meson versioning scheme) '0.46.0' is
+ # the lowest version which satisfies the constraint '>=0.46'.
+ #
+ # But this will fail here, because the minimum version required by the
+ # version constraint ('0.46') is strictly less (in our version comparison)
+ # than the minimum version needed for the feature ('0.46.0').
+ #
+ # Map versions in the constraint of the form '0.46' to '0.46.0', to embed
+ # this knowledge of the meson versioning scheme.
+ condition = condition.strip()
+ if re.match(r'^\d+.\d+$', condition):
+ condition += '.0'
+
+ return T.cast('bool', cmpop(Version(minimum), Version(condition)))
+
+def search_version(text: str) -> str:
+ # Usually of the type 4.1.4 but compiler output may contain
+ # stuff like this:
+ # (Sourcery CodeBench Lite 2014.05-29) 4.8.3 20140320 (prerelease)
+ # Limiting major version number to two digits seems to work
+ # thus far. When we get to GCC 100, this will break, but
+ # if we are still relevant when that happens, it can be
+ # considered an achievement in itself.
+ #
+ # This regex is reaching magic levels. If it ever needs
+ # to be updated, do not complexify but convert to something
+ # saner instead.
+ # We'll demystify it a bit with a verbose definition.
+ version_regex = re.compile(r"""
+ (?<! # Zero-width negative lookbehind assertion
+ (
+ \d # One digit
+ | \. # Or one period
+ ) # One occurrence
+ )
+ # Following pattern must not follow a digit or period
+ (
+ \d{1,2} # One or two digits
+ (
+ \.\d+ # Period and one or more digits
+ )+ # One or more occurrences
+ (
+ -[a-zA-Z0-9]+ # Hyphen and one or more alphanumeric
+ )? # Zero or one occurrence
+ ) # One occurrence
+ """, re.VERBOSE)
+ match = version_regex.search(text)
+ if match:
+ return match.group(0)
+
+ # try a simpler regex that has like "blah 2020.01.100 foo" or "blah 2020.01 foo"
+ version_regex = re.compile(r"(\d{1,4}\.\d{1,4}\.?\d{0,4})")
+ match = version_regex.search(text)
+ if match:
+ return match.group(0)
+
+ return 'unknown version'
+
+
+def default_libdir() -> str:
+ if is_debianlike():
+ try:
+ pc = subprocess.Popen(['dpkg-architecture', '-qDEB_HOST_MULTIARCH'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL)
+ (stdo, _) = pc.communicate()
+ if pc.returncode == 0:
+ archpath = stdo.decode().strip()
+ return 'lib/' + archpath
+ except Exception:
+ pass
+ if is_freebsd() or is_irix():
+ return 'lib'
+ if os.path.isdir('/usr/lib64') and not os.path.islink('/usr/lib64'):
+ return 'lib64'
+ return 'lib'
+
+
+def default_libexecdir() -> str:
+ if is_haiku():
+ return 'lib'
+ # There is no way to auto-detect this, so it must be set at build time
+ return 'libexec'
+
+
+def default_prefix() -> str:
+ if is_windows():
+ return 'c:/'
+ if is_haiku():
+ return '/boot/system/non-packaged'
+ return '/usr/local'
+
+
+def default_datadir() -> str:
+ if is_haiku():
+ return 'data'
+ return 'share'
+
+
+def default_includedir() -> str:
+ if is_haiku():
+ return 'develop/headers'
+ return 'include'
+
+
+def default_infodir() -> str:
+ if is_haiku():
+ return 'documentation/info'
+ return 'share/info'
+
+
+def default_localedir() -> str:
+ if is_haiku():
+ return 'data/locale'
+ return 'share/locale'
+
+
+def default_mandir() -> str:
+ if is_haiku():
+ return 'documentation/man'
+ return 'share/man'
+
+
+def default_sbindir() -> str:
+ if is_haiku():
+ return 'bin'
+ return 'sbin'
+
+
+def default_sysconfdir() -> str:
+ if is_haiku():
+ return 'settings'
+ return 'etc'
+
+
+def has_path_sep(name: str, sep: str = '/\\') -> bool:
+ 'Checks if any of the specified @sep path separators are in @name'
+ for each in sep:
+ if each in name:
+ return True
+ return False
+
+
+if is_windows():
+ # shlex.split is not suitable for splitting command line on Window (https://bugs.python.org/issue1724822);
+ # shlex.quote is similarly problematic. Below are "proper" implementations of these functions according to
+ # https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments and
+ # https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
+
+ _whitespace = ' \t\n\r'
+ _find_unsafe_char = re.compile(fr'[{_whitespace}"]').search
+
+ def quote_arg(arg: str) -> str:
+ if arg and not _find_unsafe_char(arg):
+ return arg
+
+ result = '"'
+ num_backslashes = 0
+ for c in arg:
+ if c == '\\':
+ num_backslashes += 1
+ else:
+ if c == '"':
+ # Escape all backslashes and the following double quotation mark
+ num_backslashes = num_backslashes * 2 + 1
+
+ result += num_backslashes * '\\' + c
+ num_backslashes = 0
+
+ # Escape all backslashes, but let the terminating double quotation
+ # mark we add below be interpreted as a metacharacter
+ result += (num_backslashes * 2) * '\\' + '"'
+ return result
+
+ def split_args(cmd: str) -> T.List[str]:
+ result = []
+ arg = ''
+ num_backslashes = 0
+ num_quotes = 0
+ in_quotes = False
+ for c in cmd:
+ if c == '\\':
+ num_backslashes += 1
+ else:
+ if c == '"' and not num_backslashes % 2:
+ # unescaped quote, eat it
+ arg += (num_backslashes // 2) * '\\'
+ num_quotes += 1
+ in_quotes = not in_quotes
+ elif c in _whitespace and not in_quotes:
+ if arg or num_quotes:
+ # reached the end of the argument
+ result.append(arg)
+ arg = ''
+ num_quotes = 0
+ else:
+ if c == '"':
+ # escaped quote
+ num_backslashes = (num_backslashes - 1) // 2
+
+ arg += num_backslashes * '\\' + c
+
+ num_backslashes = 0
+
+ if arg or num_quotes:
+ result.append(arg)
+
+ return result
+else:
+ def quote_arg(arg: str) -> str:
+ return shlex.quote(arg)
+
+ def split_args(cmd: str) -> T.List[str]:
+ return shlex.split(cmd)
+
+
+def join_args(args: T.Iterable[str]) -> str:
+ return ' '.join([quote_arg(x) for x in args])
+
+
+def do_replacement(regex: T.Pattern[str], line: str,
+ variable_format: Literal['meson', 'cmake', 'cmake@'],
+ confdata: T.Union[T.Dict[str, T.Tuple[str, T.Optional[str]]], 'ConfigurationData']) -> T.Tuple[str, T.Set[str]]:
+ missing_variables = set() # type: T.Set[str]
+ if variable_format == 'cmake':
+ start_tag = '${'
+ backslash_tag = '\\${'
+ else:
+ start_tag = '@'
+ backslash_tag = '\\@'
+
+ def variable_replace(match: T.Match[str]) -> str:
+ # Pairs of escape characters before '@' or '\@'
+ if match.group(0).endswith('\\'):
+ num_escapes = match.end(0) - match.start(0)
+ return '\\' * (num_escapes // 2)
+ # Single escape character and '@'
+ elif match.group(0) == backslash_tag:
+ return start_tag
+ # Template variable to be replaced
+ else:
+ varname = match.group(1)
+ var_str = ''
+ if varname in confdata:
+ var, _ = confdata.get(varname)
+ if isinstance(var, str):
+ var_str = var
+ elif isinstance(var, int):
+ var_str = str(var)
+ else:
+ msg = f'Tried to replace variable {varname!r} value with ' \
+ f'something other than a string or int: {var!r}'
+ raise MesonException(msg)
+ else:
+ missing_variables.add(varname)
+ return var_str
+ return re.sub(regex, variable_replace, line), missing_variables
+
+def do_define(regex: T.Pattern[str], line: str, confdata: 'ConfigurationData',
+ variable_format: Literal['meson', 'cmake', 'cmake@']) -> str:
+ def get_cmake_define(line: str, confdata: 'ConfigurationData') -> str:
+ arr = line.split()
+ define_value = []
+ for token in arr[2:]:
+ try:
+ (v, desc) = confdata.get(token)
+ define_value += [str(v)]
+ except KeyError:
+ define_value += [token]
+ return ' '.join(define_value)
+
+ arr = line.split()
+ if variable_format == 'meson' and len(arr) != 2:
+ raise MesonException('#mesondefine does not contain exactly two tokens: %s' % line.strip())
+
+ varname = arr[1]
+ try:
+ (v, desc) = confdata.get(varname)
+ except KeyError:
+ return '/* #undef %s */\n' % varname
+ if isinstance(v, bool):
+ if v:
+ return '#define %s\n' % varname
+ else:
+ return '#undef %s\n' % varname
+ elif isinstance(v, int):
+ return '#define %s %d\n' % (varname, v)
+ elif isinstance(v, str):
+ if variable_format == 'meson':
+ result = v
+ else:
+ result = get_cmake_define(line, confdata)
+ result = f'#define {varname} {result}\n'
+ (result, missing_variable) = do_replacement(regex, result, variable_format, confdata)
+ return result
+ else:
+ raise MesonException('#mesondefine argument "%s" is of unknown type.' % varname)
+
+def get_variable_regex(variable_format: Literal['meson', 'cmake', 'cmake@'] = 'meson') -> T.Pattern[str]:
+ # Only allow (a-z, A-Z, 0-9, _, -) as valid characters for a define
+ # Also allow escaping '@' with '\@'
+ if variable_format in {'meson', 'cmake@'}:
+ regex = re.compile(r'(?:\\\\)+(?=\\?@)|\\@|@([-a-zA-Z0-9_]+)@')
+ else:
+ regex = re.compile(r'(?:\\\\)+(?=\\?\$)|\\\${|\${([-a-zA-Z0-9_]+)}')
+ return regex
+
+def do_conf_str(src: str, data: list, confdata: 'ConfigurationData',
+ variable_format: Literal['meson', 'cmake', 'cmake@'],
+ encoding: str = 'utf-8') -> T.Tuple[T.List[str], T.Set[str], bool]:
+ def line_is_valid(line: str, variable_format: str) -> bool:
+ if variable_format == 'meson':
+ if '#cmakedefine' in line:
+ return False
+ else: # cmake format
+ if '#mesondefine' in line:
+ return False
+ return True
+
+ regex = get_variable_regex(variable_format)
+
+ search_token = '#mesondefine'
+ if variable_format != 'meson':
+ search_token = '#cmakedefine'
+
+ result = []
+ missing_variables = set()
+ # Detect when the configuration data is empty and no tokens were found
+ # during substitution so we can warn the user to use the `copy:` kwarg.
+ confdata_useless = not confdata.keys()
+ for line in data:
+ if line.startswith(search_token):
+ confdata_useless = False
+ line = do_define(regex, line, confdata, variable_format)
+ else:
+ if not line_is_valid(line, variable_format):
+ raise MesonException(f'Format error in {src}: saw "{line.strip()}" when format set to "{variable_format}"')
+ line, missing = do_replacement(regex, line, variable_format, confdata)
+ missing_variables.update(missing)
+ if missing:
+ confdata_useless = False
+ result.append(line)
+
+ return result, missing_variables, confdata_useless
+
+def do_conf_file(src: str, dst: str, confdata: 'ConfigurationData',
+ variable_format: Literal['meson', 'cmake', 'cmake@'],
+ encoding: str = 'utf-8') -> T.Tuple[T.Set[str], bool]:
+ try:
+ with open(src, encoding=encoding, newline='') as f:
+ data = f.readlines()
+ except Exception as e:
+ raise MesonException(f'Could not read input file {src}: {e!s}')
+
+ (result, missing_variables, confdata_useless) = do_conf_str(src, data, confdata, variable_format, encoding)
+ dst_tmp = dst + '~'
+ try:
+ with open(dst_tmp, 'w', encoding=encoding, newline='') as f:
+ f.writelines(result)
+ except Exception as e:
+ raise MesonException(f'Could not write output file {dst}: {e!s}')
+ shutil.copymode(src, dst_tmp)
+ replace_if_different(dst, dst_tmp)
+ return missing_variables, confdata_useless
+
+CONF_C_PRELUDE = '''/*
+ * Autogenerated by the Meson build system.
+ * Do not edit, your changes will be lost.
+ */
+
+#pragma once
+
+'''
+
+CONF_NASM_PRELUDE = '''; Autogenerated by the Meson build system.
+; Do not edit, your changes will be lost.
+
+'''
+
+def dump_conf_header(ofilename: str, cdata: 'ConfigurationData', output_format: T.Literal['c', 'nasm']) -> None:
+ if output_format == 'c':
+ prelude = CONF_C_PRELUDE
+ prefix = '#'
+ else:
+ prelude = CONF_NASM_PRELUDE
+ prefix = '%'
+
+ ofilename_tmp = ofilename + '~'
+ with open(ofilename_tmp, 'w', encoding='utf-8') as ofile:
+ ofile.write(prelude)
+ for k in sorted(cdata.keys()):
+ (v, desc) = cdata.get(k)
+ if desc:
+ if output_format == 'c':
+ ofile.write('/* %s */\n' % desc)
+ elif output_format == 'nasm':
+ for line in desc.split('\n'):
+ ofile.write('; %s\n' % line)
+ if isinstance(v, bool):
+ if v:
+ ofile.write(f'{prefix}define {k}\n\n')
+ else:
+ ofile.write(f'{prefix}undef {k}\n\n')
+ elif isinstance(v, (int, str)):
+ ofile.write(f'{prefix}define {k} {v}\n\n')
+ else:
+ raise MesonException('Unknown data type in configuration file entry: ' + k)
+ replace_if_different(ofilename, ofilename_tmp)
+
+
+def replace_if_different(dst: str, dst_tmp: str) -> None:
+ # If contents are identical, don't touch the file to prevent
+ # unnecessary rebuilds.
+ different = True
+ try:
+ with open(dst, 'rb') as f1, open(dst_tmp, 'rb') as f2:
+ if f1.read() == f2.read():
+ different = False
+ except FileNotFoundError:
+ pass
+ if different:
+ os.replace(dst_tmp, dst)
+ else:
+ os.unlink(dst_tmp)
+
+
+def listify(item: T.Any, flatten: bool = True) -> T.List[T.Any]:
+ '''
+ Returns a list with all args embedded in a list if they are not a list.
+ This function preserves order.
+ @flatten: Convert lists of lists to a flat list
+ '''
+ if not isinstance(item, list):
+ return [item]
+ result = [] # type: T.List[T.Any]
+ for i in item:
+ if flatten and isinstance(i, list):
+ result += listify(i, flatten=True)
+ else:
+ result.append(i)
+ return result
+
+
+def extract_as_list(dict_object: T.Dict[_T, _U], key: _T, pop: bool = False) -> T.List[_U]:
+ '''
+ Extracts all values from given dict_object and listifies them.
+ '''
+ fetch: T.Callable[[_T], _U] = dict_object.get
+ if pop:
+ fetch = dict_object.pop
+ # If there's only one key, we don't return a list with one element
+ return listify(fetch(key) or [], flatten=True)
+
+
+def typeslistify(item: 'T.Union[_T, T.Sequence[_T]]',
+ types: 'T.Union[T.Type[_T], T.Tuple[T.Type[_T]]]') -> T.List[_T]:
+ '''
+ Ensure that type(@item) is one of @types or a
+ list of items all of which are of type @types
+ '''
+ if isinstance(item, types):
+ item = T.cast('T.List[_T]', [item])
+ if not isinstance(item, list):
+ raise MesonException('Item must be a list or one of {!r}, not {!r}'.format(types, type(item)))
+ for i in item:
+ if i is not None and not isinstance(i, types):
+ raise MesonException('List item must be one of {!r}, not {!r}'.format(types, type(i)))
+ return item
+
+
+def stringlistify(item: T.Union[T.Any, T.Sequence[T.Any]]) -> T.List[str]:
+ return typeslistify(item, str)
+
+
+def expand_arguments(args: T.Iterable[str]) -> T.Optional[T.List[str]]:
+ expended_args = [] # type: T.List[str]
+ for arg in args:
+ if not arg.startswith('@'):
+ expended_args.append(arg)
+ continue
+
+ args_file = arg[1:]
+ try:
+ with open(args_file, encoding='utf-8') as f:
+ extended_args = f.read().split()
+ expended_args += extended_args
+ except Exception as e:
+ mlog.error('Expanding command line arguments:', args_file, 'not found')
+ mlog.exception(e)
+ return None
+ return expended_args
+
+
+def partition(pred: T.Callable[[_T], object], iterable: T.Iterable[_T]) -> T.Tuple[T.Iterator[_T], T.Iterator[_T]]:
+ """Use a predicate to partition entries into false entries and true
+ entries.
+
+ >>> x, y = partition(is_odd, range(10))
+ >>> (list(x), list(y))
+ ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9])
+ """
+ t1, t2 = tee(iterable)
+ return (t for t in t1 if not pred(t)), (t for t in t2 if pred(t))
+
+
+def Popen_safe(args: T.List[str], write: T.Optional[str] = None,
+ stdin: T.Union[T.TextIO, T.BinaryIO, int] = subprocess.DEVNULL,
+ stdout: T.Union[T.TextIO, T.BinaryIO, int] = subprocess.PIPE,
+ stderr: T.Union[T.TextIO, T.BinaryIO, int] = subprocess.PIPE,
+ **kwargs: T.Any) -> T.Tuple['subprocess.Popen[str]', str, str]:
+ import locale
+ encoding = locale.getpreferredencoding()
+ # Stdin defaults to DEVNULL otherwise the command run by us here might mess
+ # up the console and ANSI colors will stop working on Windows.
+ # If write is not None, set stdin to PIPE so data can be sent.
+ if write is not None:
+ stdin = subprocess.PIPE
+
+ try:
+ if not sys.stdout.encoding or encoding.upper() != 'UTF-8':
+ p, o, e = Popen_safe_legacy(args, write=write, stdin=stdin, stdout=stdout, stderr=stderr, **kwargs)
+ else:
+ p = subprocess.Popen(args, universal_newlines=True, encoding=encoding, close_fds=False,
+ stdin=stdin, stdout=stdout, stderr=stderr, **kwargs)
+ o, e = p.communicate(write)
+ except OSError as oserr:
+ if oserr.errno == errno.ENOEXEC:
+ raise MesonException(f'Failed running {args[0]!r}, binary or interpreter not executable.\n'
+ 'Possibly wrong architecture or the executable bit is not set.')
+ raise
+ # Sometimes the command that we run will call another command which will be
+ # without the above stdin workaround, so set the console mode again just in
+ # case.
+ mlog.setup_console()
+ return p, o, e
+
+
+def Popen_safe_legacy(args: T.List[str], write: T.Optional[str] = None,
+ stdin: T.Union[T.TextIO, T.BinaryIO, int] = subprocess.DEVNULL,
+ stdout: T.Union[T.TextIO, T.BinaryIO, int] = subprocess.PIPE,
+ stderr: T.Union[T.TextIO, T.BinaryIO, int] = subprocess.PIPE,
+ **kwargs: T.Any) -> T.Tuple['subprocess.Popen[str]', str, str]:
+ p = subprocess.Popen(args, universal_newlines=False, close_fds=False,
+ stdin=stdin, stdout=stdout, stderr=stderr, **kwargs)
+ input_ = None # type: T.Optional[bytes]
+ if write is not None:
+ input_ = write.encode('utf-8')
+ o, e = p.communicate(input_)
+ if o is not None:
+ if sys.stdout.encoding is not None:
+ o = o.decode(encoding=sys.stdout.encoding, errors='replace').replace('\r\n', '\n')
+ else:
+ o = o.decode(errors='replace').replace('\r\n', '\n')
+ if e is not None:
+ if sys.stderr is not None and sys.stderr.encoding:
+ e = e.decode(encoding=sys.stderr.encoding, errors='replace').replace('\r\n', '\n')
+ else:
+ e = e.decode(errors='replace').replace('\r\n', '\n')
+ return p, o, e
+
+
+def iter_regexin_iter(regexiter: T.Iterable[str], initer: T.Iterable[str]) -> T.Optional[str]:
+ '''
+ Takes each regular expression in @regexiter and tries to search for it in
+ every item in @initer. If there is a match, returns that match.
+ Else returns False.
+ '''
+ for regex in regexiter:
+ for ii in initer:
+ if not isinstance(ii, str):
+ continue
+ match = re.search(regex, ii)
+ if match:
+ return match.group()
+ return None
+
+
+def _substitute_values_check_errors(command: T.List[str], values: T.Dict[str, T.Union[str, T.List[str]]]) -> None:
+ # Error checking
+ inregex = ['@INPUT([0-9]+)?@', '@PLAINNAME@', '@BASENAME@'] # type: T.List[str]
+ outregex = ['@OUTPUT([0-9]+)?@', '@OUTDIR@'] # type: T.List[str]
+ if '@INPUT@' not in values:
+ # Error out if any input-derived templates are present in the command
+ match = iter_regexin_iter(inregex, command)
+ if match:
+ raise MesonException(f'Command cannot have {match!r}, since no input files were specified')
+ else:
+ if len(values['@INPUT@']) > 1:
+ # Error out if @PLAINNAME@ or @BASENAME@ is present in the command
+ match = iter_regexin_iter(inregex[1:], command)
+ if match:
+ raise MesonException(f'Command cannot have {match!r} when there is '
+ 'more than one input file')
+ # Error out if an invalid @INPUTnn@ template was specified
+ for each in command:
+ if not isinstance(each, str):
+ continue
+ match2 = re.search(inregex[0], each)
+ if match2 and match2.group() not in values:
+ m = 'Command cannot have {!r} since there are only {!r} inputs'
+ raise MesonException(m.format(match2.group(), len(values['@INPUT@'])))
+ if '@OUTPUT@' not in values:
+ # Error out if any output-derived templates are present in the command
+ match = iter_regexin_iter(outregex, command)
+ if match:
+ raise MesonException(f'Command cannot have {match!r} since there are no outputs')
+ else:
+ # Error out if an invalid @OUTPUTnn@ template was specified
+ for each in command:
+ if not isinstance(each, str):
+ continue
+ match2 = re.search(outregex[0], each)
+ if match2 and match2.group() not in values:
+ m = 'Command cannot have {!r} since there are only {!r} outputs'
+ raise MesonException(m.format(match2.group(), len(values['@OUTPUT@'])))
+
+
+def substitute_values(command: T.List[str], values: T.Dict[str, T.Union[str, T.List[str]]]) -> T.List[str]:
+ '''
+ Substitute the template strings in the @values dict into the list of
+ strings @command and return a new list. For a full list of the templates,
+ see get_filenames_templates_dict()
+
+ If multiple inputs/outputs are given in the @values dictionary, we
+ substitute @INPUT@ and @OUTPUT@ only if they are the entire string, not
+ just a part of it, and in that case we substitute *all* of them.
+
+ The typing of this function is difficult, as only @OUTPUT@ and @INPUT@ can
+ be lists, everything else is a string. However, TypeDict cannot represent
+ this, as you can have optional keys, but not extra keys. We end up just
+ having to us asserts to convince type checkers that this is okay.
+
+ https://github.com/python/mypy/issues/4617
+ '''
+
+ def replace(m: T.Match[str]) -> str:
+ v = values[m.group(0)]
+ assert isinstance(v, str), 'for mypy'
+ return v
+
+ # Error checking
+ _substitute_values_check_errors(command, values)
+
+ # Substitution
+ outcmd = [] # type: T.List[str]
+ rx_keys = [re.escape(key) for key in values if key not in ('@INPUT@', '@OUTPUT@')]
+ value_rx = re.compile('|'.join(rx_keys)) if rx_keys else None
+ for vv in command:
+ more: T.Optional[str] = None
+ if not isinstance(vv, str):
+ outcmd.append(vv)
+ elif '@INPUT@' in vv:
+ inputs = values['@INPUT@']
+ if vv == '@INPUT@':
+ outcmd += inputs
+ elif len(inputs) == 1:
+ outcmd.append(vv.replace('@INPUT@', inputs[0]))
+ else:
+ raise MesonException("Command has '@INPUT@' as part of a "
+ "string and more than one input file")
+ elif '@OUTPUT@' in vv:
+ outputs = values['@OUTPUT@']
+ if vv == '@OUTPUT@':
+ outcmd += outputs
+ elif len(outputs) == 1:
+ outcmd.append(vv.replace('@OUTPUT@', outputs[0]))
+ else:
+ raise MesonException("Command has '@OUTPUT@' as part of a "
+ "string and more than one output file")
+
+ # Append values that are exactly a template string.
+ # This is faster than a string replace.
+ elif vv in values:
+ o = values[vv]
+ assert isinstance(o, str), 'for mypy'
+ more = o
+ # Substitute everything else with replacement
+ elif value_rx:
+ more = value_rx.sub(replace, vv)
+ else:
+ more = vv
+
+ if more is not None:
+ outcmd.append(more)
+
+ return outcmd
+
+
+def get_filenames_templates_dict(inputs: T.List[str], outputs: T.List[str]) -> T.Dict[str, T.Union[str, T.List[str]]]:
+ '''
+ Create a dictionary with template strings as keys and values as values for
+ the following templates:
+
+ @INPUT@ - the full path to one or more input files, from @inputs
+ @OUTPUT@ - the full path to one or more output files, from @outputs
+ @OUTDIR@ - the full path to the directory containing the output files
+
+ If there is only one input file, the following keys are also created:
+
+ @PLAINNAME@ - the filename of the input file
+ @BASENAME@ - the filename of the input file with the extension removed
+
+ If there is more than one input file, the following keys are also created:
+
+ @INPUT0@, @INPUT1@, ... one for each input file
+
+ If there is more than one output file, the following keys are also created:
+
+ @OUTPUT0@, @OUTPUT1@, ... one for each output file
+ '''
+ values = {} # type: T.Dict[str, T.Union[str, T.List[str]]]
+ # Gather values derived from the input
+ if inputs:
+ # We want to substitute all the inputs.
+ values['@INPUT@'] = inputs
+ for (ii, vv) in enumerate(inputs):
+ # Write out @INPUT0@, @INPUT1@, ...
+ values[f'@INPUT{ii}@'] = vv
+ if len(inputs) == 1:
+ # Just one value, substitute @PLAINNAME@ and @BASENAME@
+ values['@PLAINNAME@'] = plain = os.path.basename(inputs[0])
+ values['@BASENAME@'] = os.path.splitext(plain)[0]
+ if outputs:
+ # Gather values derived from the outputs, similar to above.
+ values['@OUTPUT@'] = outputs
+ for (ii, vv) in enumerate(outputs):
+ values[f'@OUTPUT{ii}@'] = vv
+ # Outdir should be the same for all outputs
+ values['@OUTDIR@'] = os.path.dirname(outputs[0])
+ # Many external programs fail on empty arguments.
+ if values['@OUTDIR@'] == '':
+ values['@OUTDIR@'] = '.'
+ return values
+
+
+def _make_tree_writable(topdir: str) -> None:
+ # Ensure all files and directories under topdir are writable
+ # (and readable) by owner.
+ for d, _, files in os.walk(topdir):
+ os.chmod(d, os.stat(d).st_mode | stat.S_IWRITE | stat.S_IREAD)
+ for fname in files:
+ fpath = os.path.join(d, fname)
+ if os.path.isfile(fpath):
+ os.chmod(fpath, os.stat(fpath).st_mode | stat.S_IWRITE | stat.S_IREAD)
+
+
+def windows_proof_rmtree(f: str) -> None:
+ # On Windows if anyone is holding a file open you can't
+ # delete it. As an example an anti virus scanner might
+ # be scanning files you are trying to delete. The only
+ # way to fix this is to try again and again.
+ delays = [0.1, 0.1, 0.2, 0.2, 0.2, 0.5, 0.5, 1, 1, 1, 1, 2]
+ writable = False
+ for d in delays:
+ try:
+ # Start by making the tree writable.
+ if not writable:
+ _make_tree_writable(f)
+ writable = True
+ except PermissionError:
+ time.sleep(d)
+ continue
+ try:
+ shutil.rmtree(f)
+ return
+ except FileNotFoundError:
+ return
+ except OSError:
+ time.sleep(d)
+ # Try one last time and throw if it fails.
+ shutil.rmtree(f)
+
+
+def windows_proof_rm(fpath: str) -> None:
+ """Like windows_proof_rmtree, but for a single file."""
+ if os.path.isfile(fpath):
+ os.chmod(fpath, os.stat(fpath).st_mode | stat.S_IWRITE | stat.S_IREAD)
+ delays = [0.1, 0.1, 0.2, 0.2, 0.2, 0.5, 0.5, 1, 1, 1, 1, 2]
+ for d in delays:
+ try:
+ os.unlink(fpath)
+ return
+ except FileNotFoundError:
+ return
+ except OSError:
+ time.sleep(d)
+ os.unlink(fpath)
+
+
+class TemporaryDirectoryWinProof(TemporaryDirectory):
+ """
+ Like TemporaryDirectory, but cleans things up using
+ windows_proof_rmtree()
+ """
+
+ def __exit__(self, exc: T.Any, value: T.Any, tb: T.Any) -> None:
+ try:
+ super().__exit__(exc, value, tb)
+ except OSError:
+ windows_proof_rmtree(self.name)
+
+ def cleanup(self) -> None:
+ try:
+ super().cleanup()
+ except OSError:
+ windows_proof_rmtree(self.name)
+
+
+def detect_subprojects(spdir_name: str, current_dir: str = '',
+ result: T.Optional[T.Dict[str, T.List[str]]] = None) -> T.Dict[str, T.List[str]]:
+ if result is None:
+ result = {}
+ spdir = os.path.join(current_dir, spdir_name)
+ if not os.path.exists(spdir):
+ return result
+ for trial in glob(os.path.join(spdir, '*')):
+ basename = os.path.basename(trial)
+ if trial == 'packagecache':
+ continue
+ append_this = True
+ if os.path.isdir(trial):
+ detect_subprojects(spdir_name, trial, result)
+ elif trial.endswith('.wrap') and os.path.isfile(trial):
+ basename = os.path.splitext(basename)[0]
+ else:
+ append_this = False
+ if append_this:
+ if basename in result:
+ result[basename].append(trial)
+ else:
+ result[basename] = [trial]
+ return result
+
+
+def substring_is_in_list(substr: str, strlist: T.List[str]) -> bool:
+ for s in strlist:
+ if substr in s:
+ return True
+ return False
+
+
+class OrderedSet(T.MutableSet[_T]):
+ """A set that preserves the order in which items are added, by first
+ insertion.
+ """
+ def __init__(self, iterable: T.Optional[T.Iterable[_T]] = None):
+ self.__container: T.OrderedDict[_T, None] = collections.OrderedDict()
+ if iterable:
+ self.update(iterable)
+
+ def __contains__(self, value: object) -> bool:
+ return value in self.__container
+
+ def __iter__(self) -> T.Iterator[_T]:
+ return iter(self.__container.keys())
+
+ def __len__(self) -> int:
+ return len(self.__container)
+
+ def __repr__(self) -> str:
+ # Don't print 'OrderedSet("")' for an empty set.
+ if self.__container:
+ return 'OrderedSet("{}")'.format(
+ '", "'.join(repr(e) for e in self.__container.keys()))
+ return 'OrderedSet()'
+
+ def __reversed__(self) -> T.Iterator[_T]:
+ return reversed(self.__container.keys())
+
+ def add(self, value: _T) -> None:
+ self.__container[value] = None
+
+ def discard(self, value: _T) -> None:
+ if value in self.__container:
+ del self.__container[value]
+
+ def move_to_end(self, value: _T, last: bool = True) -> None:
+ self.__container.move_to_end(value, last)
+
+ def pop(self, last: bool = True) -> _T:
+ item, _ = self.__container.popitem(last)
+ return item
+
+ def update(self, iterable: T.Iterable[_T]) -> None:
+ for item in iterable:
+ self.__container[item] = None
+
+ def difference(self, set_: T.Iterable[_T]) -> 'OrderedSet[_T]':
+ return type(self)(e for e in self if e not in set_)
+
+ def difference_update(self, iterable: T.Iterable[_T]) -> None:
+ for item in iterable:
+ self.discard(item)
+
+def relpath(path: str, start: str) -> str:
+ # On Windows a relative path can't be evaluated for paths on two different
+ # drives (i.e. c:\foo and f:\bar). The only thing left to do is to use the
+ # original absolute path.
+ try:
+ return os.path.relpath(path, start)
+ except (TypeError, ValueError):
+ return path
+
+def path_is_in_root(path: Path, root: Path, resolve: bool = False) -> bool:
+ # Check whether a path is within the root directory root
+ try:
+ if resolve:
+ path.resolve().relative_to(root.resolve())
+ else:
+ path.relative_to(root)
+ except ValueError:
+ return False
+ return True
+
+def relative_to_if_possible(path: Path, root: Path, resolve: bool = False) -> Path:
+ try:
+ if resolve:
+ return path.resolve().relative_to(root.resolve())
+ else:
+ return path.relative_to(root)
+ except ValueError:
+ return path
+
+class LibType(enum.IntEnum):
+
+ """Enumeration for library types."""
+
+ SHARED = 0
+ STATIC = 1
+ PREFER_SHARED = 2
+ PREFER_STATIC = 3
+
+
+class ProgressBarFallback: # lgtm [py/iter-returns-non-self]
+ '''
+ Fallback progress bar implementation when tqdm is not found
+
+ Since this class is not an actual iterator, but only provides a minimal
+ fallback, it is safe to ignore the 'Iterator does not return self from
+ __iter__ method' warning.
+ '''
+ def __init__(self, iterable: T.Optional[T.Iterable[str]] = None, total: T.Optional[int] = None,
+ bar_type: T.Optional[str] = None, desc: T.Optional[str] = None):
+ if iterable is not None:
+ self.iterable = iter(iterable)
+ return
+ self.total = total
+ self.done = 0
+ self.printed_dots = 0
+ if self.total and bar_type == 'download':
+ print('Download size:', self.total)
+ if desc:
+ print(f'{desc}: ', end='')
+
+ # Pretend to be an iterator when called as one and don't print any
+ # progress
+ def __iter__(self) -> T.Iterator[str]:
+ return self.iterable
+
+ def __next__(self) -> str:
+ return next(self.iterable)
+
+ def print_dot(self) -> None:
+ print('.', end='')
+ sys.stdout.flush()
+ self.printed_dots += 1
+
+ def update(self, progress: int) -> None:
+ self.done += progress
+ if not self.total:
+ # Just print one dot per call if we don't have a total length
+ self.print_dot()
+ return
+ ratio = int(self.done / self.total * 10)
+ while self.printed_dots < ratio:
+ self.print_dot()
+
+ def close(self) -> None:
+ print('')
+
+try:
+ from tqdm import tqdm
+except ImportError:
+ # ideally we would use a typing.Protocol here, but it's part of typing_extensions until 3.8
+ ProgressBar = ProgressBarFallback # type: T.Union[T.Type[ProgressBarFallback], T.Type[ProgressBarTqdm]]
+else:
+ class ProgressBarTqdm(tqdm):
+ def __init__(self, *args: T.Any, bar_type: T.Optional[str] = None, **kwargs: T.Any) -> None:
+ if bar_type == 'download':
+ kwargs.update({'unit': 'bytes', 'leave': True})
+ else:
+ kwargs.update({'leave': False})
+ kwargs['ncols'] = 100
+ super().__init__(*args, **kwargs)
+
+ ProgressBar = ProgressBarTqdm
+
+
+class RealPathAction(argparse.Action):
+ def __init__(self, option_strings: T.List[str], dest: str, default: str = '.', **kwargs: T.Any):
+ default = os.path.abspath(os.path.realpath(default))
+ super().__init__(option_strings, dest, nargs=None, default=default, **kwargs)
+
+ def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace,
+ values: T.Union[str, T.Sequence[T.Any], None], option_string: str = None) -> None:
+ assert isinstance(values, str)
+ setattr(namespace, self.dest, os.path.abspath(os.path.realpath(values)))
+
+
+def get_wine_shortpath(winecmd: T.List[str], wine_paths: T.List[str],
+ workdir: T.Optional[str] = None) -> str:
+ '''
+ WINEPATH size is limited to 1024 bytes which can easily be exceeded when
+ adding the path to every dll inside build directory. See
+ https://bugs.winehq.org/show_bug.cgi?id=45810.
+
+ To shorten it as much as possible we use path relative to `workdir`
+ where possible and convert absolute paths to Windows shortpath (e.g.
+ "/usr/x86_64-w64-mingw32/lib" to "Z:\\usr\\X86_~FWL\\lib").
+
+ This limitation reportedly has been fixed with wine >= 6.4
+ '''
+
+ # Remove duplicates
+ wine_paths = list(OrderedSet(wine_paths))
+
+ # Check if it's already short enough
+ wine_path = ';'.join(wine_paths)
+ if len(wine_path) <= 1024:
+ return wine_path
+
+ # Check if we have wine >= 6.4
+ from ..programs import ExternalProgram
+ wine = ExternalProgram('wine', winecmd, silent=True)
+ if version_compare(wine.get_version(), '>=6.4'):
+ return wine_path
+
+ # Check paths that can be reduced by making them relative to workdir.
+ rel_paths = []
+ if workdir:
+ abs_paths = []
+ for p in wine_paths:
+ try:
+ rel = Path(p).relative_to(workdir)
+ rel_paths.append(str(rel))
+ except ValueError:
+ abs_paths.append(p)
+ wine_paths = abs_paths
+
+ if wine_paths:
+ # BAT script that takes a list of paths in argv and prints semi-colon separated shortpaths
+ with NamedTemporaryFile('w', suffix='.bat', encoding='utf-8', delete=False) as bat_file:
+ bat_file.write('''
+ @ECHO OFF
+ for %%x in (%*) do (
+ echo|set /p=;%~sx
+ )
+ ''')
+ try:
+ stdout = subprocess.check_output(winecmd + ['cmd', '/C', bat_file.name] + wine_paths,
+ encoding='utf-8', stderr=subprocess.DEVNULL)
+ stdout = stdout.strip(';')
+ if stdout:
+ wine_paths = stdout.split(';')
+ else:
+ mlog.warning('Could not shorten WINEPATH: empty stdout')
+ except subprocess.CalledProcessError as e:
+ mlog.warning(f'Could not shorten WINEPATH: {str(e)}')
+ finally:
+ os.unlink(bat_file.name)
+ wine_path = ';'.join(rel_paths + wine_paths)
+ if len(wine_path) > 1024:
+ mlog.warning('WINEPATH exceeds 1024 characters which could cause issues')
+ return wine_path
+
+
+def run_once(func: T.Callable[..., _T]) -> T.Callable[..., _T]:
+ ret = [] # type: T.List[_T]
+
+ @wraps(func)
+ def wrapper(*args: T.Any, **kwargs: T.Any) -> _T:
+ if ret:
+ return ret[0]
+
+ val = func(*args, **kwargs)
+ ret.append(val)
+ return val
+
+ return wrapper
+
+
+def generate_list(func: T.Callable[..., T.Generator[_T, None, None]]) -> T.Callable[..., T.List[_T]]:
+ @wraps(func)
+ def wrapper(*args: T.Any, **kwargs: T.Any) -> T.List[_T]:
+ return list(func(*args, **kwargs))
+
+ return wrapper
+
+
+class OptionOverrideProxy(collections.abc.Mapping):
+ '''Mimic an option list but transparently override selected option
+ values.
+ '''
+
+ # TODO: the typing here could be made more explicit using a TypeDict from
+ # python 3.8 or typing_extensions
+
+ def __init__(self, overrides: T.Dict['OptionKey', T.Any], options: 'KeyedOptionDictType',
+ subproject: T.Optional[str] = None):
+ self.overrides = overrides
+ self.options = options
+ self.subproject = subproject
+
+ def __getitem__(self, key: 'OptionKey') -> 'UserOption':
+ # FIXME: This is fundamentally the same algorithm than interpreter.get_option_internal().
+ # We should try to share the code somehow.
+ key = key.evolve(subproject=self.subproject)
+ if not key.is_project():
+ opt = self.options.get(key)
+ if opt is None or opt.yielding:
+ opt = self.options[key.as_root()]
+ else:
+ opt = self.options[key]
+ if opt.yielding:
+ opt = self.options.get(key.as_root(), opt)
+ override_value = self.overrides.get(key.as_root())
+ if override_value is not None:
+ opt = copy.copy(opt)
+ opt.set_value(override_value)
+ return opt
+
+ def __iter__(self) -> T.Iterator['OptionKey']:
+ return iter(self.options)
+
+ def __len__(self) -> int:
+ return len(self.options)
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, OptionOverrideProxy):
+ return NotImplemented
+ t1 = (self.overrides, self.subproject, self.options)
+ t2 = (other.overrides, other.subproject, other.options)
+ return t1 == t2
+
+
+class OptionType(enum.IntEnum):
+
+ """Enum used to specify what kind of argument a thing is."""
+
+ BUILTIN = 0
+ BACKEND = 1
+ BASE = 2
+ COMPILER = 3
+ PROJECT = 4
+
+# This is copied from coredata. There is no way to share this, because this
+# is used in the OptionKey constructor, and the coredata lists are
+# OptionKeys...
+_BUILTIN_NAMES = {
+ 'prefix',
+ 'bindir',
+ 'datadir',
+ 'includedir',
+ 'infodir',
+ 'libdir',
+ 'libexecdir',
+ 'localedir',
+ 'localstatedir',
+ 'mandir',
+ 'sbindir',
+ 'sharedstatedir',
+ 'sysconfdir',
+ 'auto_features',
+ 'backend',
+ 'buildtype',
+ 'debug',
+ 'default_library',
+ 'errorlogs',
+ 'install_umask',
+ 'layout',
+ 'optimization',
+ 'prefer_static',
+ 'stdsplit',
+ 'strip',
+ 'unity',
+ 'unity_size',
+ 'warning_level',
+ 'werror',
+ 'wrap_mode',
+ 'force_fallback_for',
+ 'pkg_config_path',
+ 'cmake_prefix_path',
+}
+
+
+def _classify_argument(key: 'OptionKey') -> OptionType:
+ """Classify arguments into groups so we know which dict to assign them to."""
+
+ if key.name.startswith('b_'):
+ return OptionType.BASE
+ elif key.lang is not None:
+ return OptionType.COMPILER
+ elif key.name in _BUILTIN_NAMES or key.module:
+ return OptionType.BUILTIN
+ elif key.name.startswith('backend_'):
+ assert key.machine is MachineChoice.HOST, str(key)
+ return OptionType.BACKEND
+ else:
+ assert key.machine is MachineChoice.HOST, str(key)
+ return OptionType.PROJECT
+
+
+@total_ordering
+class OptionKey:
+
+ """Represents an option key in the various option dictionaries.
+
+ This provides a flexible, powerful way to map option names from their
+ external form (things like subproject:build.option) to something that
+ internally easier to reason about and produce.
+ """
+
+ __slots__ = ['name', 'subproject', 'machine', 'lang', '_hash', 'type', 'module']
+
+ name: str
+ subproject: str
+ machine: MachineChoice
+ lang: T.Optional[str]
+ _hash: int
+ type: OptionType
+ module: T.Optional[str]
+
+ def __init__(self, name: str, subproject: str = '',
+ machine: MachineChoice = MachineChoice.HOST,
+ lang: T.Optional[str] = None,
+ module: T.Optional[str] = None,
+ _type: T.Optional[OptionType] = None):
+ # the _type option to the constructor is kinda private. We want to be
+ # able tos ave the state and avoid the lookup function when
+ # pickling/unpickling, but we need to be able to calculate it when
+ # constructing a new OptionKey
+ object.__setattr__(self, 'name', name)
+ object.__setattr__(self, 'subproject', subproject)
+ object.__setattr__(self, 'machine', machine)
+ object.__setattr__(self, 'lang', lang)
+ object.__setattr__(self, 'module', module)
+ object.__setattr__(self, '_hash', hash((name, subproject, machine, lang, module)))
+ if _type is None:
+ _type = _classify_argument(self)
+ object.__setattr__(self, 'type', _type)
+
+ def __setattr__(self, key: str, value: T.Any) -> None:
+ raise AttributeError('OptionKey instances do not support mutation.')
+
+ def __getstate__(self) -> T.Dict[str, T.Any]:
+ return {
+ 'name': self.name,
+ 'subproject': self.subproject,
+ 'machine': self.machine,
+ 'lang': self.lang,
+ '_type': self.type,
+ 'module': self.module,
+ }
+
+ def __setstate__(self, state: T.Dict[str, T.Any]) -> None:
+ """De-serialize the state of a pickle.
+
+ This is very clever. __init__ is not a constructor, it's an
+ initializer, therefore it's safe to call more than once. We create a
+ state in the custom __getstate__ method, which is valid to pass
+ splatted to the initializer.
+ """
+ # Mypy doesn't like this, because it's so clever.
+ self.__init__(**state) # type: ignore
+
+ def __hash__(self) -> int:
+ return self._hash
+
+ def _to_tuple(self) -> T.Tuple[str, OptionType, str, str, MachineChoice, str]:
+ return (self.subproject, self.type, self.lang or '', self.module or '', self.machine, self.name)
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, OptionKey):
+ return self._to_tuple() == other._to_tuple()
+ return NotImplemented
+
+ def __lt__(self, other: object) -> bool:
+ if isinstance(other, OptionKey):
+ return self._to_tuple() < other._to_tuple()
+ return NotImplemented
+
+ def __str__(self) -> str:
+ out = self.name
+ if self.lang:
+ out = f'{self.lang}_{out}'
+ if self.machine is MachineChoice.BUILD:
+ out = f'build.{out}'
+ if self.module:
+ out = f'{self.module}.{out}'
+ if self.subproject:
+ out = f'{self.subproject}:{out}'
+ return out
+
+ def __repr__(self) -> str:
+ return f'OptionKey({self.name!r}, {self.subproject!r}, {self.machine!r}, {self.lang!r}, {self.module!r}, {self.type!r})'
+
+ @classmethod
+ def from_string(cls, raw: str) -> 'OptionKey':
+ """Parse the raw command line format into a three part tuple.
+
+ This takes strings like `mysubproject:build.myoption` and Creates an
+ OptionKey out of them.
+ """
+ try:
+ subproject, raw2 = raw.split(':')
+ except ValueError:
+ subproject, raw2 = '', raw
+
+ module = None
+ for_machine = MachineChoice.HOST
+ try:
+ prefix, raw3 = raw2.split('.')
+ if prefix == 'build':
+ for_machine = MachineChoice.BUILD
+ else:
+ module = prefix
+ except ValueError:
+ raw3 = raw2
+
+ from ..compilers import all_languages
+ if any(raw3.startswith(f'{l}_') for l in all_languages):
+ lang, opt = raw3.split('_', 1)
+ else:
+ lang, opt = None, raw3
+ assert ':' not in opt
+ assert '.' not in opt
+
+ return cls(opt, subproject, for_machine, lang, module)
+
+ def evolve(self, name: T.Optional[str] = None, subproject: T.Optional[str] = None,
+ machine: T.Optional[MachineChoice] = None, lang: T.Optional[str] = '',
+ module: T.Optional[str] = '') -> 'OptionKey':
+ """Create a new copy of this key, but with alterted members.
+
+ For example:
+ >>> a = OptionKey('foo', '', MachineChoice.Host)
+ >>> b = OptionKey('foo', 'bar', MachineChoice.Host)
+ >>> b == a.evolve(subproject='bar')
+ True
+ """
+ # We have to be a little clever with lang here, because lang is valid
+ # as None, for non-compiler options
+ return OptionKey(
+ name if name is not None else self.name,
+ subproject if subproject is not None else self.subproject,
+ machine if machine is not None else self.machine,
+ lang if lang != '' else self.lang,
+ module if module != '' else self.module
+ )
+
+ def as_root(self) -> 'OptionKey':
+ """Convenience method for key.evolve(subproject='')."""
+ return self.evolve(subproject='')
+
+ def as_build(self) -> 'OptionKey':
+ """Convenience method for key.evolve(machine=MachinceChoice.BUILD)."""
+ return self.evolve(machine=MachineChoice.BUILD)
+
+ def as_host(self) -> 'OptionKey':
+ """Convenience method for key.evolve(machine=MachinceChoice.HOST)."""
+ return self.evolve(machine=MachineChoice.HOST)
+
+ def is_backend(self) -> bool:
+ """Convenience method to check if this is a backend option."""
+ return self.type is OptionType.BACKEND
+
+ def is_builtin(self) -> bool:
+ """Convenience method to check if this is a builtin option."""
+ return self.type is OptionType.BUILTIN
+
+ def is_compiler(self) -> bool:
+ """Convenience method to check if this is a builtin option."""
+ return self.type is OptionType.COMPILER
+
+ def is_project(self) -> bool:
+ """Convenience method to check if this is a project option."""
+ return self.type is OptionType.PROJECT
+
+ def is_base(self) -> bool:
+ """Convenience method to check if this is a base option."""
+ return self.type is OptionType.BASE
+
+def pickle_load(filename: str, object_name: str, object_type: T.Type) -> T.Any:
+ load_fail_msg = f'{object_name} file {filename!r} is corrupted. Try with a fresh build tree.'
+ try:
+ with open(filename, 'rb') as f:
+ obj = pickle.load(f)
+ except (pickle.UnpicklingError, EOFError):
+ raise MesonException(load_fail_msg)
+ except (TypeError, ModuleNotFoundError, AttributeError):
+ build_dir = os.path.dirname(os.path.dirname(filename))
+ raise MesonException(
+ f"{object_name} file {filename!r} references functions or classes that don't "
+ "exist. This probably means that it was generated with an old "
+ "version of meson. Try running from the source directory "
+ f'meson setup {build_dir} --wipe')
+ if not isinstance(obj, object_type):
+ raise MesonException(load_fail_msg)
+ from ..coredata import version as coredata_version
+ from ..coredata import major_versions_differ, MesonVersionMismatchException
+ version = getattr(obj, 'version', None)
+ if version is None:
+ version = obj.environment.coredata.version
+ if major_versions_differ(version, coredata_version):
+ raise MesonVersionMismatchException(version, coredata_version)
+ return obj
+
+
+def first(iter: T.Iterable[_T], predicate: T.Callable[[_T], bool]) -> T.Optional[_T]:
+ """Find the first entry in an iterable where the given predicate is true
+
+ :param iter: The iterable to search
+ :param predicate: A finding function that takes an element from the iterable
+ and returns True if found, otherwise False
+ :return: The first found element, or None if it is not found
+ """
+ for i in iter:
+ if predicate(i):
+ return i
+ return None
diff --git a/mesonbuild/utils/vsenv.py b/mesonbuild/utils/vsenv.py
new file mode 100644
index 0000000..d862e5a
--- /dev/null
+++ b/mesonbuild/utils/vsenv.py
@@ -0,0 +1,123 @@
+from __future__ import annotations
+
+import os
+import subprocess
+import json
+import pathlib
+import shutil
+import tempfile
+
+from .. import mlog
+from .universal import MesonException, is_windows, windows_detect_native_arch
+
+
+__all__ = [
+ 'setup_vsenv',
+]
+
+
+bat_template = '''@ECHO OFF
+
+call "{}"
+
+ECHO {}
+SET
+'''
+
+# If on Windows and VS is installed but not set up in the environment,
+# set it to be runnable. In this way Meson can be directly invoked
+# from any shell, VS Code etc.
+def _setup_vsenv(force: bool) -> bool:
+ if not is_windows():
+ return False
+ if os.environ.get('OSTYPE') == 'cygwin':
+ return False
+ if 'MESON_FORCE_VSENV_FOR_UNITTEST' not in os.environ:
+ # VSINSTALL is set when running setvars from a Visual Studio installation
+ # Tested with Visual Studio 2012 and 2017
+ if 'VSINSTALLDIR' in os.environ:
+ return False
+ # Check explicitly for cl when on Windows
+ if shutil.which('cl.exe'):
+ return False
+ if not force:
+ if shutil.which('cc'):
+ return False
+ if shutil.which('gcc'):
+ return False
+ if shutil.which('clang'):
+ return False
+ if shutil.which('clang-cl'):
+ return False
+
+ root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
+ bat_locator_bin = pathlib.Path(root, 'Microsoft Visual Studio/Installer/vswhere.exe')
+ if not bat_locator_bin.exists():
+ raise MesonException(f'Could not find {bat_locator_bin}')
+ bat_json = subprocess.check_output(
+ [
+ str(bat_locator_bin),
+ '-latest',
+ '-prerelease',
+ '-requiresAny',
+ '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
+ '-requires', 'Microsoft.VisualStudio.Workload.WDExpress',
+ '-products', '*',
+ '-utf8',
+ '-format',
+ 'json'
+ ]
+ )
+ bat_info = json.loads(bat_json)
+ if not bat_info:
+ # VS installer instelled but not VS itself maybe?
+ raise MesonException('Could not parse vswhere.exe output')
+ bat_root = pathlib.Path(bat_info[0]['installationPath'])
+ if windows_detect_native_arch() == 'arm64':
+ bat_path = bat_root / 'VC/Auxiliary/Build/vcvarsarm64.bat'
+ if not bat_path.exists():
+ bat_path = bat_root / 'VC/Auxiliary/Build/vcvarsx86_arm64.bat'
+ else:
+ bat_path = bat_root / 'VC/Auxiliary/Build/vcvars64.bat'
+ # if VS is not found try VS Express
+ if not bat_path.exists():
+ bat_path = bat_root / 'VC/Auxiliary/Build/vcvarsx86_amd64.bat'
+ if not bat_path.exists():
+ raise MesonException(f'Could not find {bat_path}')
+
+ mlog.log('Activating VS', bat_info[0]['catalog']['productDisplayVersion'])
+ bat_separator = '---SPLIT---'
+ bat_contents = bat_template.format(bat_path, bat_separator)
+ bat_file = tempfile.NamedTemporaryFile('w', suffix='.bat', encoding='utf-8', delete=False)
+ bat_file.write(bat_contents)
+ bat_file.flush()
+ bat_file.close()
+ bat_output = subprocess.check_output(bat_file.name, universal_newlines=True)
+ os.unlink(bat_file.name)
+ bat_lines = bat_output.split('\n')
+ bat_separator_seen = False
+ for bat_line in bat_lines:
+ if bat_line == bat_separator:
+ bat_separator_seen = True
+ continue
+ if not bat_separator_seen:
+ continue
+ if not bat_line:
+ continue
+ try:
+ k, v = bat_line.split('=', 1)
+ except ValueError:
+ # there is no "=", ignore junk data
+ pass
+ else:
+ os.environ[k] = v
+ return True
+
+def setup_vsenv(force: bool = False) -> bool:
+ try:
+ return _setup_vsenv(force)
+ except MesonException as e:
+ if force:
+ raise
+ mlog.warning('Failed to activate VS environment:', str(e))
+ return False
diff --git a/mesonbuild/utils/win32.py b/mesonbuild/utils/win32.py
new file mode 100644
index 0000000..2bd4cba
--- /dev/null
+++ b/mesonbuild/utils/win32.py
@@ -0,0 +1,40 @@
+# SPDX-license-identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# 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.
+from __future__ import annotations
+
+"""Windows specific implementations of mesonlib functionality."""
+
+import msvcrt
+import typing as T
+
+from .universal import MesonException
+from .platform import BuildDirLock as BuildDirLockBase
+
+__all__ = ['BuildDirLock']
+
+class BuildDirLock(BuildDirLockBase):
+
+ def __enter__(self) -> None:
+ self.lockfile = open(self.lockfilename, 'w', encoding='utf-8')
+ try:
+ msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1)
+ except (BlockingIOError, PermissionError):
+ self.lockfile.close()
+ raise MesonException('Some other Meson process is already using this build directory. Exiting.')
+
+ def __exit__(self, *args: T.Any) -> None:
+ msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1)
+ self.lockfile.close()
diff --git a/mesonbuild/wrap/__init__.py b/mesonbuild/wrap/__init__.py
new file mode 100644
index 0000000..653f42a
--- /dev/null
+++ b/mesonbuild/wrap/__init__.py
@@ -0,0 +1,59 @@
+from enum import Enum
+
+# Used for the --wrap-mode command-line argument
+#
+# Special wrap modes:
+# nofallback: Don't download wraps for dependency() fallbacks
+# nodownload: Don't download wraps for all subproject() calls
+#
+# subprojects are used for two purposes:
+# 1. To download and build dependencies by using .wrap
+# files if they are not provided by the system. This is
+# usually expressed via dependency(..., fallback: ...).
+# 2. To download and build 'copylibs' which are meant to be
+# used by copying into your project. This is always done
+# with an explicit subproject() call.
+#
+# --wrap-mode=nofallback will never do (1)
+# --wrap-mode=nodownload will do neither (1) nor (2)
+#
+# If you are building from a release tarball, you should be
+# able to safely use 'nodownload' since upstream is
+# expected to ship all required sources with the tarball.
+#
+# If you are building from a git repository, you will want
+# to use 'nofallback' so that any 'copylib' wraps will be
+# download as subprojects.
+#
+# --wrap-mode=forcefallback will ignore external dependencies,
+# even if they match the version requirements, and automatically
+# use the fallback if one was provided. This is useful for example
+# to make sure a project builds when using the fallbacks.
+#
+# Note that these options do not affect subprojects that
+# are git submodules since those are only usable in git
+# repositories, and you almost always want to download them.
+
+# This did _not_ work when inside the WrapMode class.
+# I don't know why. If you can fix this, patches welcome.
+string_to_value = {'default': 1,
+ 'nofallback': 2,
+ 'nodownload': 3,
+ 'forcefallback': 4,
+ 'nopromote': 5,
+ }
+
+class WrapMode(Enum):
+ default = 1
+ nofallback = 2
+ nodownload = 3
+ forcefallback = 4
+ nopromote = 5
+
+ def __str__(self) -> str:
+ return self.name
+
+ @staticmethod
+ def from_string(mode_name: str) -> 'WrapMode':
+ g = string_to_value[mode_name]
+ return WrapMode(g)
diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py
new file mode 100644
index 0000000..9b1795b
--- /dev/null
+++ b/mesonbuild/wrap/wrap.py
@@ -0,0 +1,833 @@
+# Copyright 2015 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.
+from __future__ import annotations
+
+from .. import mlog
+import contextlib
+from dataclasses import dataclass
+import urllib.request
+import urllib.error
+import urllib.parse
+import os
+import hashlib
+import shutil
+import tempfile
+import stat
+import subprocess
+import sys
+import configparser
+import time
+import typing as T
+import textwrap
+import json
+
+from base64 import b64encode
+from netrc import netrc
+from pathlib import Path
+
+from . import WrapMode
+from .. import coredata
+from ..mesonlib import quiet_git, GIT, ProgressBar, MesonException, windows_proof_rmtree, Popen_safe
+from ..interpreterbase import FeatureNew
+from ..interpreterbase import SubProject
+from .. import mesonlib
+
+if T.TYPE_CHECKING:
+ import http.client
+
+try:
+ # Importing is just done to check if SSL exists, so all warnings
+ # regarding 'imported but unused' can be safely ignored
+ import ssl # noqa
+ has_ssl = True
+except ImportError:
+ has_ssl = False
+
+REQ_TIMEOUT = 600.0
+SSL_WARNING_PRINTED = False
+WHITELIST_SUBDOMAIN = 'wrapdb.mesonbuild.com'
+
+ALL_TYPES = ['file', 'git', 'hg', 'svn']
+
+PATCH = shutil.which('patch')
+
+def whitelist_wrapdb(urlstr: str) -> urllib.parse.ParseResult:
+ """ raises WrapException if not whitelisted subdomain """
+ url = urllib.parse.urlparse(urlstr)
+ if not url.hostname:
+ raise WrapException(f'{urlstr} is not a valid URL')
+ if not url.hostname.endswith(WHITELIST_SUBDOMAIN):
+ raise WrapException(f'{urlstr} is not a whitelisted WrapDB URL')
+ if has_ssl and not url.scheme == 'https':
+ raise WrapException(f'WrapDB did not have expected SSL https url, instead got {urlstr}')
+ return url
+
+def open_wrapdburl(urlstring: str, allow_insecure: bool = False, have_opt: bool = False) -> 'http.client.HTTPResponse':
+ if have_opt:
+ insecure_msg = '\n\n To allow connecting anyway, pass `--allow-insecure`.'
+ else:
+ insecure_msg = ''
+
+ url = whitelist_wrapdb(urlstring)
+ if has_ssl:
+ try:
+ return T.cast('http.client.HTTPResponse', urllib.request.urlopen(urllib.parse.urlunparse(url), timeout=REQ_TIMEOUT))
+ except urllib.error.URLError as excp:
+ msg = f'WrapDB connection failed to {urlstring} with error {excp}.'
+ if isinstance(excp.reason, ssl.SSLCertVerificationError):
+ if allow_insecure:
+ mlog.warning(f'{msg}\n\n Proceeding without authentication.')
+ else:
+ raise WrapException(f'{msg}{insecure_msg}')
+ else:
+ raise WrapException(msg)
+ elif not allow_insecure:
+ raise WrapException(f'SSL module not available in {sys.executable}: Cannot contact the WrapDB.{insecure_msg}')
+ else:
+ # following code is only for those without Python SSL
+ global SSL_WARNING_PRINTED # pylint: disable=global-statement
+ if not SSL_WARNING_PRINTED:
+ mlog.warning(f'SSL module not available in {sys.executable}: WrapDB traffic not authenticated.')
+ SSL_WARNING_PRINTED = True
+
+ # If we got this far, allow_insecure was manually passed
+ nossl_url = url._replace(scheme='http')
+ try:
+ return T.cast('http.client.HTTPResponse', urllib.request.urlopen(urllib.parse.urlunparse(nossl_url), timeout=REQ_TIMEOUT))
+ except urllib.error.URLError as excp:
+ raise WrapException(f'WrapDB connection failed to {urlstring} with error {excp}')
+
+def get_releases_data(allow_insecure: bool) -> bytes:
+ url = open_wrapdburl('https://wrapdb.mesonbuild.com/v2/releases.json', allow_insecure, True)
+ return url.read()
+
+def get_releases(allow_insecure: bool) -> T.Dict[str, T.Any]:
+ data = get_releases_data(allow_insecure)
+ return T.cast('T.Dict[str, T.Any]', json.loads(data.decode()))
+
+def update_wrap_file(wrapfile: str, name: str, new_version: str, new_revision: str, allow_insecure: bool) -> None:
+ url = open_wrapdburl(f'https://wrapdb.mesonbuild.com/v2/{name}_{new_version}-{new_revision}/{name}.wrap',
+ allow_insecure, True)
+ with open(wrapfile, 'wb') as f:
+ f.write(url.read())
+
+def parse_patch_url(patch_url: str) -> T.Tuple[str, str]:
+ u = urllib.parse.urlparse(patch_url)
+ if u.netloc != 'wrapdb.mesonbuild.com':
+ raise WrapException(f'URL {patch_url} does not seems to be a wrapdb patch')
+ arr = u.path.strip('/').split('/')
+ if arr[0] == 'v1':
+ # e.g. https://wrapdb.mesonbuild.com/v1/projects/zlib/1.2.11/5/get_zip
+ return arr[-3], arr[-2]
+ elif arr[0] == 'v2':
+ # e.g. https://wrapdb.mesonbuild.com/v2/zlib_1.2.11-5/get_patch
+ tag = arr[-2]
+ _, version = tag.rsplit('_', 1)
+ version, revision = version.rsplit('-', 1)
+ return version, revision
+ else:
+ raise WrapException(f'Invalid wrapdb URL {patch_url}')
+
+class WrapException(MesonException):
+ pass
+
+class WrapNotFoundException(WrapException):
+ pass
+
+class PackageDefinition:
+ def __init__(self, fname: str, subproject: str = ''):
+ self.filename = fname
+ self.subproject = SubProject(subproject)
+ self.type = None # type: T.Optional[str]
+ self.values = {} # type: T.Dict[str, str]
+ self.provided_deps = {} # type: T.Dict[str, T.Optional[str]]
+ self.provided_programs = [] # type: T.List[str]
+ self.diff_files = [] # type: T.List[Path]
+ self.basename = os.path.basename(fname)
+ self.has_wrap = self.basename.endswith('.wrap')
+ self.name = self.basename[:-5] if self.has_wrap else self.basename
+ # must be lowercase for consistency with dep=variable assignment
+ self.provided_deps[self.name.lower()] = None
+ # What the original file name was before redirection
+ self.original_filename = fname
+ self.redirected = False
+ if self.has_wrap:
+ self.parse_wrap()
+ with open(fname, 'r', encoding='utf-8') as file:
+ self.wrapfile_hash = hashlib.sha256(file.read().encode('utf-8')).hexdigest()
+ self.directory = self.values.get('directory', self.name)
+ if os.path.dirname(self.directory):
+ raise WrapException('Directory key must be a name and not a path')
+ if self.type and self.type not in ALL_TYPES:
+ raise WrapException(f'Unknown wrap type {self.type!r}')
+ self.filesdir = os.path.join(os.path.dirname(self.filename), 'packagefiles')
+
+ def parse_wrap(self) -> None:
+ try:
+ config = configparser.ConfigParser(interpolation=None)
+ config.read(self.filename, encoding='utf-8')
+ except configparser.Error as e:
+ raise WrapException(f'Failed to parse {self.basename}: {e!s}')
+ self.parse_wrap_section(config)
+ if self.type == 'redirect':
+ # [wrap-redirect] have a `filename` value pointing to the real wrap
+ # file we should parse instead. It must be relative to the current
+ # wrap file location and must be in the form foo/subprojects/bar.wrap.
+ dirname = Path(self.filename).parent
+ fname = Path(self.values['filename'])
+ for i, p in enumerate(fname.parts):
+ if i % 2 == 0:
+ if p == '..':
+ raise WrapException('wrap-redirect filename cannot contain ".."')
+ else:
+ if p != 'subprojects':
+ raise WrapException('wrap-redirect filename must be in the form foo/subprojects/bar.wrap')
+ if fname.suffix != '.wrap':
+ raise WrapException('wrap-redirect filename must be a .wrap file')
+ fname = dirname / fname
+ if not fname.is_file():
+ raise WrapException(f'wrap-redirect {fname} filename does not exist')
+ self.filename = str(fname)
+ self.parse_wrap()
+ self.redirected = True
+ else:
+ self.parse_provide_section(config)
+ if 'patch_directory' in self.values:
+ FeatureNew('Wrap files with patch_directory', '0.55.0').use(self.subproject)
+ for what in ['patch', 'source']:
+ if f'{what}_filename' in self.values and f'{what}_url' not in self.values:
+ FeatureNew(f'Local wrap patch files without {what}_url', '0.55.0').use(self.subproject)
+
+ def parse_wrap_section(self, config: configparser.ConfigParser) -> None:
+ if len(config.sections()) < 1:
+ raise WrapException(f'Missing sections in {self.basename}')
+ self.wrap_section = config.sections()[0]
+ if not self.wrap_section.startswith('wrap-'):
+ raise WrapException(f'{self.wrap_section!r} is not a valid first section in {self.basename}')
+ self.type = self.wrap_section[5:]
+ self.values = dict(config[self.wrap_section])
+ if 'diff_files' in self.values:
+ FeatureNew('Wrap files with diff_files', '0.63.0').use(self.subproject)
+ for s in self.values['diff_files'].split(','):
+ path = Path(s.strip())
+ if path.is_absolute():
+ raise WrapException('diff_files paths cannot be absolute')
+ if '..' in path.parts:
+ raise WrapException('diff_files paths cannot contain ".."')
+ self.diff_files.append(path)
+
+ def parse_provide_section(self, config: configparser.ConfigParser) -> None:
+ if config.has_section('provide'):
+ for k, v in config['provide'].items():
+ if k == 'dependency_names':
+ # A comma separated list of dependency names that does not
+ # need a variable name; must be lowercase for consistency with
+ # dep=variable assignment
+ names_dict = {n.strip().lower(): None for n in v.split(',')}
+ self.provided_deps.update(names_dict)
+ continue
+ if k == 'program_names':
+ # A comma separated list of program names
+ names_list = [n.strip() for n in v.split(',')]
+ self.provided_programs += names_list
+ continue
+ if not v:
+ m = (f'Empty dependency variable name for {k!r} in {self.basename}. '
+ 'If the subproject uses meson.override_dependency() '
+ 'it can be added in the "dependency_names" special key.')
+ raise WrapException(m)
+ self.provided_deps[k] = v
+
+ def get(self, key: str) -> str:
+ try:
+ return self.values[key]
+ except KeyError:
+ raise WrapException(f'Missing key {key!r} in {self.basename}')
+
+ def get_hashfile(self, subproject_directory: str) -> str:
+ return os.path.join(subproject_directory, '.meson-subproject-wrap-hash.txt')
+
+ def update_hash_cache(self, subproject_directory: str) -> None:
+ if self.has_wrap:
+ with open(self.get_hashfile(subproject_directory), 'w', encoding='utf-8') as file:
+ file.write(self.wrapfile_hash + '\n')
+
+def get_directory(subdir_root: str, packagename: str) -> str:
+ fname = os.path.join(subdir_root, packagename + '.wrap')
+ if os.path.isfile(fname):
+ wrap = PackageDefinition(fname)
+ return wrap.directory
+ return packagename
+
+def verbose_git(cmd: T.List[str], workingdir: str, check: bool = False) -> bool:
+ '''
+ Wrapper to convert GitException to WrapException caught in interpreter.
+ '''
+ try:
+ return mesonlib.verbose_git(cmd, workingdir, check=check)
+ except mesonlib.GitException as e:
+ raise WrapException(str(e))
+
+@dataclass(eq=False)
+class Resolver:
+ source_dir: str
+ subdir: str
+ subproject: str = ''
+ wrap_mode: WrapMode = WrapMode.default
+ wrap_frontend: bool = False
+ allow_insecure: bool = False
+
+ def __post_init__(self) -> None:
+ self.subdir_root = os.path.join(self.source_dir, self.subdir)
+ self.cachedir = os.path.join(self.subdir_root, 'packagecache')
+ self.wraps = {} # type: T.Dict[str, PackageDefinition]
+ self.netrc: T.Optional[netrc] = None
+ self.provided_deps = {} # type: T.Dict[str, PackageDefinition]
+ self.provided_programs = {} # type: T.Dict[str, PackageDefinition]
+ self.wrapdb: T.Dict[str, T.Any] = {}
+ self.wrapdb_provided_deps: T.Dict[str, str] = {}
+ self.wrapdb_provided_programs: T.Dict[str, str] = {}
+ self.load_wraps()
+ self.load_netrc()
+ self.load_wrapdb()
+
+ def load_netrc(self) -> None:
+ try:
+ self.netrc = netrc()
+ except FileNotFoundError:
+ return
+ except Exception as e:
+ mlog.warning(f'failed to process netrc file: {e}.', fatal=False)
+
+ def load_wraps(self) -> None:
+ if not os.path.isdir(self.subdir_root):
+ return
+ root, dirs, files = next(os.walk(self.subdir_root))
+ ignore_dirs = {'packagecache', 'packagefiles'}
+ for i in files:
+ if not i.endswith('.wrap'):
+ continue
+ fname = os.path.join(self.subdir_root, i)
+ wrap = PackageDefinition(fname, self.subproject)
+ self.wraps[wrap.name] = wrap
+ ignore_dirs |= {wrap.directory, wrap.name}
+ # Add dummy package definition for directories not associated with a wrap file.
+ for i in dirs:
+ if i in ignore_dirs:
+ continue
+ fname = os.path.join(self.subdir_root, i)
+ wrap = PackageDefinition(fname, self.subproject)
+ self.wraps[wrap.name] = wrap
+
+ for wrap in self.wraps.values():
+ self.add_wrap(wrap)
+
+ def add_wrap(self, wrap: PackageDefinition) -> None:
+ for k in wrap.provided_deps.keys():
+ if k in self.provided_deps:
+ prev_wrap = self.provided_deps[k]
+ m = f'Multiple wrap files provide {k!r} dependency: {wrap.basename} and {prev_wrap.basename}'
+ raise WrapException(m)
+ self.provided_deps[k] = wrap
+ for k in wrap.provided_programs:
+ if k in self.provided_programs:
+ prev_wrap = self.provided_programs[k]
+ m = f'Multiple wrap files provide {k!r} program: {wrap.basename} and {prev_wrap.basename}'
+ raise WrapException(m)
+ self.provided_programs[k] = wrap
+
+ def load_wrapdb(self) -> None:
+ try:
+ with Path(self.subdir_root, 'wrapdb.json').open('r', encoding='utf-8') as f:
+ self.wrapdb = json.load(f)
+ except FileNotFoundError:
+ return
+ for name, info in self.wrapdb.items():
+ self.wrapdb_provided_deps.update({i: name for i in info.get('dependency_names', [])})
+ self.wrapdb_provided_programs.update({i: name for i in info.get('program_names', [])})
+
+ def get_from_wrapdb(self, subp_name: str) -> PackageDefinition:
+ info = self.wrapdb.get(subp_name)
+ if not info:
+ return None
+ self.check_can_download()
+ latest_version = info['versions'][0]
+ version, revision = latest_version.rsplit('-', 1)
+ url = urllib.request.urlopen(f'https://wrapdb.mesonbuild.com/v2/{subp_name}_{version}-{revision}/{subp_name}.wrap')
+ fname = Path(self.subdir_root, f'{subp_name}.wrap')
+ with fname.open('wb') as f:
+ f.write(url.read())
+ mlog.log(f'Installed {subp_name} version {version} revision {revision}')
+ wrap = PackageDefinition(str(fname))
+ self.wraps[wrap.name] = wrap
+ self.add_wrap(wrap)
+ return wrap
+
+ def merge_wraps(self, other_resolver: 'Resolver') -> None:
+ for k, v in other_resolver.wraps.items():
+ self.wraps.setdefault(k, v)
+ for k, v in other_resolver.provided_deps.items():
+ self.provided_deps.setdefault(k, v)
+ for k, v in other_resolver.provided_programs.items():
+ self.provided_programs.setdefault(k, v)
+
+ def find_dep_provider(self, packagename: str) -> T.Tuple[T.Optional[str], T.Optional[str]]:
+ # Python's ini parser converts all key values to lowercase.
+ # Thus the query name must also be in lower case.
+ packagename = packagename.lower()
+ wrap = self.provided_deps.get(packagename)
+ if wrap:
+ dep_var = wrap.provided_deps.get(packagename)
+ return wrap.name, dep_var
+ wrap_name = self.wrapdb_provided_deps.get(packagename)
+ return wrap_name, None
+
+ def get_varname(self, subp_name: str, depname: str) -> T.Optional[str]:
+ wrap = self.wraps.get(subp_name)
+ return wrap.provided_deps.get(depname) if wrap else None
+
+ def find_program_provider(self, names: T.List[str]) -> T.Optional[str]:
+ for name in names:
+ wrap = self.provided_programs.get(name)
+ if wrap:
+ return wrap.name
+ wrap_name = self.wrapdb_provided_programs.get(name)
+ if wrap_name:
+ return wrap_name
+ return None
+
+ def resolve(self, packagename: str, method: str) -> str:
+ self.packagename = packagename
+ self.directory = packagename
+ self.wrap = self.wraps.get(packagename)
+ if not self.wrap:
+ self.wrap = self.get_from_wrapdb(packagename)
+ if not self.wrap:
+ m = f'Neither a subproject directory nor a {self.packagename}.wrap file was found.'
+ raise WrapNotFoundException(m)
+ self.directory = self.wrap.directory
+
+ if self.wrap.has_wrap:
+ # We have a .wrap file, use directory relative to the location of
+ # the wrap file if it exists, otherwise source code will be placed
+ # into main project's subproject_dir even if the wrap file comes
+ # from another subproject.
+ self.dirname = os.path.join(os.path.dirname(self.wrap.filename), self.wrap.directory)
+ if not os.path.exists(self.dirname):
+ self.dirname = os.path.join(self.subdir_root, self.directory)
+ # Check if the wrap comes from the main project.
+ main_fname = os.path.join(self.subdir_root, self.wrap.basename)
+ if self.wrap.filename != main_fname:
+ rel = os.path.relpath(self.wrap.filename, self.source_dir)
+ mlog.log('Using', mlog.bold(rel))
+ # Write a dummy wrap file in main project that redirect to the
+ # wrap we picked.
+ with open(main_fname, 'w', encoding='utf-8') as f:
+ f.write(textwrap.dedent('''\
+ [wrap-redirect]
+ filename = {}
+ '''.format(os.path.relpath(self.wrap.filename, self.subdir_root))))
+ else:
+ # No wrap file, it's a dummy package definition for an existing
+ # directory. Use the source code in place.
+ self.dirname = self.wrap.filename
+ rel_path = os.path.relpath(self.dirname, self.source_dir)
+
+ if method == 'meson':
+ buildfile = os.path.join(self.dirname, 'meson.build')
+ elif method == 'cmake':
+ buildfile = os.path.join(self.dirname, 'CMakeLists.txt')
+ else:
+ raise WrapException('Only the methods "meson" and "cmake" are supported')
+
+ # The directory is there and has meson.build? Great, use it.
+ if os.path.exists(buildfile):
+ self.validate()
+ return rel_path
+
+ # Check if the subproject is a git submodule
+ self.resolve_git_submodule()
+
+ if os.path.exists(self.dirname):
+ if not os.path.isdir(self.dirname):
+ raise WrapException('Path already exists but is not a directory')
+ else:
+ if self.wrap.type == 'file':
+ self.get_file()
+ else:
+ self.check_can_download()
+ if self.wrap.type == 'git':
+ self.get_git()
+ elif self.wrap.type == "hg":
+ self.get_hg()
+ elif self.wrap.type == "svn":
+ self.get_svn()
+ else:
+ raise WrapException(f'Unknown wrap type {self.wrap.type!r}')
+ try:
+ self.apply_patch()
+ self.apply_diff_files()
+ except Exception:
+ windows_proof_rmtree(self.dirname)
+ raise
+
+ # A meson.build or CMakeLists.txt file is required in the directory
+ if not os.path.exists(buildfile):
+ raise WrapException(f'Subproject exists but has no {os.path.basename(buildfile)} file')
+
+ # At this point, the subproject has been successfully resolved for the
+ # first time so save off the hash of the entire wrap file for future
+ # reference.
+ self.wrap.update_hash_cache(self.dirname)
+
+ return rel_path
+
+ def check_can_download(self) -> None:
+ # Don't download subproject data based on wrap file if requested.
+ # Git submodules are ok (see above)!
+ if self.wrap_mode is WrapMode.nodownload:
+ m = 'Automatic wrap-based subproject downloading is disabled'
+ raise WrapException(m)
+
+ def resolve_git_submodule(self) -> bool:
+ # Is git installed? If not, we're probably not in a git repository and
+ # definitely cannot try to conveniently set up a submodule.
+ if not GIT:
+ return False
+ # Does the directory exist? Even uninitialised submodules checkout an
+ # empty directory to work in
+ if not os.path.isdir(self.dirname):
+ return False
+ # Are we in a git repository?
+ ret, out = quiet_git(['rev-parse'], Path(self.dirname).parent)
+ if not ret:
+ return False
+ # Is `dirname` a submodule?
+ ret, out = quiet_git(['submodule', 'status', '.'], self.dirname)
+ if not ret:
+ return False
+ # Submodule has not been added, add it
+ if out.startswith('+'):
+ mlog.warning('git submodule might be out of date')
+ return True
+ elif out.startswith('U'):
+ raise WrapException('git submodule has merge conflicts')
+ # Submodule exists, but is deinitialized or wasn't initialized
+ elif out.startswith('-'):
+ if verbose_git(['submodule', 'update', '--init', '.'], self.dirname):
+ return True
+ raise WrapException('git submodule failed to init')
+ # Submodule looks fine, but maybe it wasn't populated properly. Do a checkout.
+ elif out.startswith(' '):
+ verbose_git(['submodule', 'update', '.'], self.dirname)
+ verbose_git(['checkout', '.'], self.dirname)
+ # Even if checkout failed, try building it anyway and let the user
+ # handle any problems manually.
+ return True
+ elif out == '':
+ # It is not a submodule, just a folder that exists in the main repository.
+ return False
+ raise WrapException(f'Unknown git submodule output: {out!r}')
+
+ def get_file(self) -> None:
+ path = self.get_file_internal('source')
+ extract_dir = self.subdir_root
+ # Some upstreams ship packages that do not have a leading directory.
+ # Create one for them.
+ if 'lead_directory_missing' in self.wrap.values:
+ os.mkdir(self.dirname)
+ extract_dir = self.dirname
+ shutil.unpack_archive(path, extract_dir)
+
+ def get_git(self) -> None:
+ if not GIT:
+ raise WrapException(f'Git program not found, cannot download {self.packagename}.wrap via git.')
+ revno = self.wrap.get('revision')
+ checkout_cmd = ['-c', 'advice.detachedHead=false', 'checkout', revno, '--']
+ is_shallow = False
+ depth_option = [] # type: T.List[str]
+ if self.wrap.values.get('depth', '') != '':
+ is_shallow = True
+ depth_option = ['--depth', self.wrap.values.get('depth')]
+ # for some reason git only allows commit ids to be shallowly fetched by fetch not with clone
+ if is_shallow and self.is_git_full_commit_id(revno):
+ # git doesn't support directly cloning shallowly for commits,
+ # so we follow https://stackoverflow.com/a/43136160
+ verbose_git(['-c', 'init.defaultBranch=meson-dummy-branch', 'init', self.directory], self.subdir_root, check=True)
+ verbose_git(['remote', 'add', 'origin', self.wrap.get('url')], self.dirname, check=True)
+ revno = self.wrap.get('revision')
+ verbose_git(['fetch', *depth_option, 'origin', revno], self.dirname, check=True)
+ verbose_git(checkout_cmd, self.dirname, check=True)
+ if self.wrap.values.get('clone-recursive', '').lower() == 'true':
+ verbose_git(['submodule', 'update', '--init', '--checkout',
+ '--recursive', *depth_option], self.dirname, check=True)
+ push_url = self.wrap.values.get('push-url')
+ if push_url:
+ verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True)
+ else:
+ if not is_shallow:
+ verbose_git(['clone', self.wrap.get('url'), self.directory], self.subdir_root, check=True)
+ if revno.lower() != 'head':
+ if not verbose_git(checkout_cmd, self.dirname):
+ verbose_git(['fetch', self.wrap.get('url'), revno], self.dirname, check=True)
+ verbose_git(checkout_cmd, self.dirname, check=True)
+ else:
+ args = ['-c', 'advice.detachedHead=false', 'clone', *depth_option]
+ if revno.lower() != 'head':
+ args += ['--branch', revno]
+ args += [self.wrap.get('url'), self.directory]
+ verbose_git(args, self.subdir_root, check=True)
+ if self.wrap.values.get('clone-recursive', '').lower() == 'true':
+ verbose_git(['submodule', 'update', '--init', '--checkout', '--recursive', *depth_option],
+ self.dirname, check=True)
+ push_url = self.wrap.values.get('push-url')
+ if push_url:
+ verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True)
+
+ def validate(self) -> None:
+ # This check is only for subprojects with wraps.
+ if not self.wrap.has_wrap:
+ return
+
+ # Retrieve original hash, if it exists.
+ hashfile = self.wrap.get_hashfile(self.dirname)
+ if os.path.isfile(hashfile):
+ with open(hashfile, 'r', encoding='utf-8') as file:
+ expected_hash = file.read().strip()
+ else:
+ # If stored hash doesn't exist then don't warn.
+ return
+
+ actual_hash = self.wrap.wrapfile_hash
+
+ # Compare hashes and warn the user if they don't match.
+ if expected_hash != actual_hash:
+ mlog.warning(f'Subproject {self.wrap.name}\'s revision may be out of date; its wrap file has changed since it was first configured')
+
+ def is_git_full_commit_id(self, revno: str) -> bool:
+ result = False
+ if len(revno) in {40, 64}: # 40 for sha1, 64 for upcoming sha256
+ result = all(ch in '0123456789AaBbCcDdEeFf' for ch in revno)
+ return result
+
+ def get_hg(self) -> None:
+ revno = self.wrap.get('revision')
+ hg = shutil.which('hg')
+ if not hg:
+ raise WrapException('Mercurial program not found.')
+ subprocess.check_call([hg, 'clone', self.wrap.get('url'),
+ self.directory], cwd=self.subdir_root)
+ if revno.lower() != 'tip':
+ subprocess.check_call([hg, 'checkout', revno],
+ cwd=self.dirname)
+
+ def get_svn(self) -> None:
+ revno = self.wrap.get('revision')
+ svn = shutil.which('svn')
+ if not svn:
+ raise WrapException('SVN program not found.')
+ subprocess.check_call([svn, 'checkout', '-r', revno, self.wrap.get('url'),
+ self.directory], cwd=self.subdir_root)
+
+ def get_netrc_credentials(self, netloc: str) -> T.Optional[T.Tuple[str, str]]:
+ if self.netrc is None or netloc not in self.netrc.hosts:
+ return None
+
+ login, account, password = self.netrc.authenticators(netloc)
+ if account is not None:
+ login = account
+
+ return login, password
+
+ def get_data(self, urlstring: str) -> T.Tuple[str, str]:
+ blocksize = 10 * 1024
+ h = hashlib.sha256()
+ tmpfile = tempfile.NamedTemporaryFile(mode='wb', dir=self.cachedir, delete=False)
+ url = urllib.parse.urlparse(urlstring)
+ if url.hostname and url.hostname.endswith(WHITELIST_SUBDOMAIN):
+ resp = open_wrapdburl(urlstring, allow_insecure=self.allow_insecure, have_opt=self.wrap_frontend)
+ elif WHITELIST_SUBDOMAIN in urlstring:
+ raise WrapException(f'{urlstring} may be a WrapDB-impersonating URL')
+ else:
+ headers = {'User-Agent': f'mesonbuild/{coredata.version}'}
+ creds = self.get_netrc_credentials(url.netloc)
+
+ if creds is not None and '@' not in url.netloc:
+ login, password = creds
+ if url.scheme == 'https':
+ enc_creds = b64encode(f'{login}:{password}'.encode()).decode()
+ headers.update({'Authorization': f'Basic {enc_creds}'})
+ elif url.scheme == 'ftp':
+ urlstring = urllib.parse.urlunparse(url._replace(netloc=f'{login}:{password}@{url.netloc}'))
+ else:
+ mlog.warning('Meson is not going to use netrc credentials for protocols other than https/ftp',
+ fatal=False)
+
+ try:
+ req = urllib.request.Request(urlstring, headers=headers)
+ resp = urllib.request.urlopen(req, timeout=REQ_TIMEOUT)
+ except urllib.error.URLError as e:
+ mlog.log(str(e))
+ raise WrapException(f'could not get {urlstring} is the internet available?')
+ with contextlib.closing(resp) as resp, tmpfile as tmpfile:
+ try:
+ dlsize = int(resp.info()['Content-Length'])
+ except TypeError:
+ dlsize = None
+ if dlsize is None:
+ print('Downloading file of unknown size.')
+ while True:
+ block = resp.read(blocksize)
+ if block == b'':
+ break
+ h.update(block)
+ tmpfile.write(block)
+ hashvalue = h.hexdigest()
+ return hashvalue, tmpfile.name
+ sys.stdout.flush()
+ progress_bar = ProgressBar(bar_type='download', total=dlsize,
+ desc='Downloading')
+ while True:
+ block = resp.read(blocksize)
+ if block == b'':
+ break
+ h.update(block)
+ tmpfile.write(block)
+ progress_bar.update(len(block))
+ progress_bar.close()
+ hashvalue = h.hexdigest()
+ return hashvalue, tmpfile.name
+
+ def check_hash(self, what: str, path: str, hash_required: bool = True) -> None:
+ if what + '_hash' not in self.wrap.values and not hash_required:
+ return
+ expected = self.wrap.get(what + '_hash').lower()
+ h = hashlib.sha256()
+ with open(path, 'rb') as f:
+ h.update(f.read())
+ dhash = h.hexdigest()
+ if dhash != expected:
+ raise WrapException(f'Incorrect hash for {what}:\n {expected} expected\n {dhash} actual.')
+
+ def get_data_with_backoff(self, urlstring: str) -> T.Tuple[str, str]:
+ delays = [1, 2, 4, 8, 16]
+ for d in delays:
+ try:
+ return self.get_data(urlstring)
+ except Exception as e:
+ mlog.warning(f'failed to download with error: {e}. Trying after a delay...', fatal=False)
+ time.sleep(d)
+ return self.get_data(urlstring)
+
+ def download(self, what: str, ofname: str, fallback: bool = False) -> None:
+ self.check_can_download()
+ srcurl = self.wrap.get(what + ('_fallback_url' if fallback else '_url'))
+ mlog.log('Downloading', mlog.bold(self.packagename), what, 'from', mlog.bold(srcurl))
+ try:
+ dhash, tmpfile = self.get_data_with_backoff(srcurl)
+ expected = self.wrap.get(what + '_hash').lower()
+ if dhash != expected:
+ os.remove(tmpfile)
+ raise WrapException(f'Incorrect hash for {what}:\n {expected} expected\n {dhash} actual.')
+ except WrapException:
+ if not fallback:
+ if what + '_fallback_url' in self.wrap.values:
+ return self.download(what, ofname, fallback=True)
+ mlog.log('A fallback URL could be specified using',
+ mlog.bold(what + '_fallback_url'), 'key in the wrap file')
+ raise
+ os.rename(tmpfile, ofname)
+
+ def get_file_internal(self, what: str) -> str:
+ filename = self.wrap.get(what + '_filename')
+ if what + '_url' in self.wrap.values:
+ cache_path = os.path.join(self.cachedir, filename)
+
+ if os.path.exists(cache_path):
+ self.check_hash(what, cache_path)
+ mlog.log('Using', mlog.bold(self.packagename), what, 'from cache.')
+ return cache_path
+
+ os.makedirs(self.cachedir, exist_ok=True)
+ self.download(what, cache_path)
+ return cache_path
+ else:
+ path = Path(self.wrap.filesdir) / filename
+
+ if not path.exists():
+ raise WrapException(f'File "{path}" does not exist')
+ self.check_hash(what, path.as_posix(), hash_required=False)
+
+ return path.as_posix()
+
+ def apply_patch(self) -> None:
+ if 'patch_filename' in self.wrap.values and 'patch_directory' in self.wrap.values:
+ m = f'Wrap file {self.wrap.basename!r} must not have both "patch_filename" and "patch_directory"'
+ raise WrapException(m)
+ if 'patch_filename' in self.wrap.values:
+ path = self.get_file_internal('patch')
+ try:
+ shutil.unpack_archive(path, self.subdir_root)
+ except Exception:
+ with tempfile.TemporaryDirectory() as workdir:
+ shutil.unpack_archive(path, workdir)
+ self.copy_tree(workdir, self.subdir_root)
+ elif 'patch_directory' in self.wrap.values:
+ patch_dir = self.wrap.values['patch_directory']
+ src_dir = os.path.join(self.wrap.filesdir, patch_dir)
+ if not os.path.isdir(src_dir):
+ raise WrapException(f'patch directory does not exist: {patch_dir}')
+ self.copy_tree(src_dir, self.dirname)
+
+ def apply_diff_files(self) -> None:
+ for filename in self.wrap.diff_files:
+ mlog.log(f'Applying diff file "{filename}"')
+ path = Path(self.wrap.filesdir) / filename
+ if not path.exists():
+ raise WrapException(f'Diff file "{path}" does not exist')
+ relpath = os.path.relpath(str(path), self.dirname)
+ if PATCH:
+ cmd = [PATCH, '-f', '-p1', '-i', relpath]
+ elif GIT:
+ # If the `patch` command is not available, fall back to `git
+ # apply`. The `--work-tree` is necessary in case we're inside a
+ # Git repository: by default, Git will try to apply the patch to
+ # the repository root.
+ cmd = [GIT, '--work-tree', '.', 'apply', '-p1', relpath]
+ else:
+ raise WrapException('Missing "patch" or "git" commands to apply diff files')
+
+ p, out, _ = Popen_safe(cmd, cwd=self.dirname, stderr=subprocess.STDOUT)
+ if p.returncode != 0:
+ mlog.log(out.strip())
+ raise WrapException(f'Failed to apply diff file "{filename}"')
+
+ def copy_tree(self, root_src_dir: str, root_dst_dir: str) -> None:
+ """
+ Copy directory tree. Overwrites also read only files.
+ """
+ for src_dir, _, files in os.walk(root_src_dir):
+ dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
+ if not os.path.exists(dst_dir):
+ os.makedirs(dst_dir)
+ for file_ in files:
+ src_file = os.path.join(src_dir, file_)
+ dst_file = os.path.join(dst_dir, file_)
+ if os.path.exists(dst_file):
+ try:
+ os.remove(dst_file)
+ except PermissionError:
+ os.chmod(dst_file, stat.S_IWUSR)
+ os.remove(dst_file)
+ shutil.copy2(src_file, dst_dir)
diff --git a/mesonbuild/wrap/wraptool.py b/mesonbuild/wrap/wraptool.py
new file mode 100644
index 0000000..c009aa1
--- /dev/null
+++ b/mesonbuild/wrap/wraptool.py
@@ -0,0 +1,231 @@
+# Copyright 2015-2016 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.
+from __future__ import annotations
+
+import sys, os
+import configparser
+import shutil
+import typing as T
+
+from glob import glob
+from .wrap import (open_wrapdburl, WrapException, get_releases, get_releases_data,
+ update_wrap_file, parse_patch_url)
+from pathlib import Path
+
+from .. import mesonlib, msubprojects
+
+if T.TYPE_CHECKING:
+ import argparse
+
+def add_arguments(parser: 'argparse.ArgumentParser') -> None:
+ subparsers = parser.add_subparsers(title='Commands', dest='command')
+ subparsers.required = True
+
+ p = subparsers.add_parser('list', help='show all available projects')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.set_defaults(wrap_func=list_projects)
+
+ p = subparsers.add_parser('search', help='search the db by name')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.add_argument('name')
+ p.set_defaults(wrap_func=search)
+
+ p = subparsers.add_parser('install', help='install the specified project')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.add_argument('name')
+ p.set_defaults(wrap_func=install)
+
+ p = msubprojects.add_wrap_update_parser(subparsers)
+ p.set_defaults(wrap_func=msubprojects.run)
+
+ p = subparsers.add_parser('info', help='show available versions of a project')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.add_argument('name')
+ p.set_defaults(wrap_func=info)
+
+ p = subparsers.add_parser('status', help='show installed and available versions of your projects')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.set_defaults(wrap_func=status)
+
+ p = subparsers.add_parser('promote', help='bring a subsubproject up to the master project')
+ p.add_argument('project_path')
+ p.set_defaults(wrap_func=promote)
+
+ p = subparsers.add_parser('update-db', help='Update list of projects available in WrapDB (Since 0.61.0)')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.set_defaults(wrap_func=update_db)
+
+def list_projects(options: 'argparse.Namespace') -> None:
+ releases = get_releases(options.allow_insecure)
+ for p in releases.keys():
+ print(p)
+
+def search(options: 'argparse.Namespace') -> None:
+ name = options.name
+ releases = get_releases(options.allow_insecure)
+ for p, info in releases.items():
+ if p.find(name) != -1:
+ print(p)
+ else:
+ for dep in info.get('dependency_names', []):
+ if dep.find(name) != -1:
+ print(f'Dependency {dep} found in wrap {p}')
+
+def get_latest_version(name: str, allow_insecure: bool) -> T.Tuple[str, str]:
+ releases = get_releases(allow_insecure)
+ info = releases.get(name)
+ if not info:
+ raise WrapException(f'Wrap {name} not found in wrapdb')
+ latest_version = info['versions'][0]
+ version, revision = latest_version.rsplit('-', 1)
+ return version, revision
+
+def install(options: 'argparse.Namespace') -> None:
+ name = options.name
+ if not os.path.isdir('subprojects'):
+ raise SystemExit('Subprojects dir not found. Run this script in your source root directory.')
+ if os.path.isdir(os.path.join('subprojects', name)):
+ raise SystemExit('Subproject directory for this project already exists.')
+ wrapfile = os.path.join('subprojects', name + '.wrap')
+ if os.path.exists(wrapfile):
+ raise SystemExit('Wrap file already exists.')
+ (version, revision) = get_latest_version(name, options.allow_insecure)
+ url = open_wrapdburl(f'https://wrapdb.mesonbuild.com/v2/{name}_{version}-{revision}/{name}.wrap', options.allow_insecure, True)
+ with open(wrapfile, 'wb') as f:
+ f.write(url.read())
+ print(f'Installed {name} version {version} revision {revision}')
+
+def get_current_version(wrapfile: str) -> T.Tuple[str, str, str, str, T.Optional[str]]:
+ cp = configparser.ConfigParser(interpolation=None)
+ cp.read(wrapfile)
+ try:
+ wrap_data = cp['wrap-file']
+ except KeyError:
+ raise WrapException('Not a wrap-file, cannot have come from the wrapdb')
+ try:
+ patch_url = wrap_data['patch_url']
+ except KeyError:
+ # We assume a wrap without a patch_url is probably just an pointer to upstream's
+ # build files. The version should be in the tarball filename, even if it isn't
+ # purely guaranteed. The wrapdb revision should be 1 because it just needs uploading once.
+ branch = mesonlib.search_version(wrap_data['source_filename'])
+ revision, patch_filename = '1', None
+ else:
+ branch, revision = parse_patch_url(patch_url)
+ patch_filename = wrap_data['patch_filename']
+ return branch, revision, wrap_data['directory'], wrap_data['source_filename'], patch_filename
+
+def update(options: 'argparse.Namespace') -> None:
+ name = options.name
+ if not os.path.isdir('subprojects'):
+ raise SystemExit('Subprojects dir not found. Run this command in your source root directory.')
+ wrapfile = os.path.join('subprojects', name + '.wrap')
+ if not os.path.exists(wrapfile):
+ raise SystemExit('Project ' + name + ' is not in use.')
+ (branch, revision, subdir, src_file, patch_file) = get_current_version(wrapfile)
+ (new_branch, new_revision) = get_latest_version(name, options.allow_insecure)
+ if new_branch == branch and new_revision == revision:
+ print('Project ' + name + ' is already up to date.')
+ raise SystemExit
+ update_wrap_file(wrapfile, name, new_branch, new_revision, options.allow_insecure)
+ shutil.rmtree(os.path.join('subprojects', subdir), ignore_errors=True)
+ try:
+ os.unlink(os.path.join('subprojects/packagecache', src_file))
+ except FileNotFoundError:
+ pass
+ if patch_file is not None:
+ try:
+ os.unlink(os.path.join('subprojects/packagecache', patch_file))
+ except FileNotFoundError:
+ pass
+ print(f'Updated {name} version {new_branch} revision {new_revision}')
+
+def info(options: 'argparse.Namespace') -> None:
+ name = options.name
+ releases = get_releases(options.allow_insecure)
+ info = releases.get(name)
+ if not info:
+ raise WrapException(f'Wrap {name} not found in wrapdb')
+ print(f'Available versions of {name}:')
+ for v in info['versions']:
+ print(' ', v)
+
+def do_promotion(from_path: str, spdir_name: str) -> None:
+ if os.path.isfile(from_path):
+ assert from_path.endswith('.wrap')
+ shutil.copy(from_path, spdir_name)
+ elif os.path.isdir(from_path):
+ sproj_name = os.path.basename(from_path)
+ outputdir = os.path.join(spdir_name, sproj_name)
+ if os.path.exists(outputdir):
+ raise SystemExit(f'Output dir {outputdir} already exists. Will not overwrite.')
+ shutil.copytree(from_path, outputdir, ignore=shutil.ignore_patterns('subprojects'))
+
+def promote(options: 'argparse.Namespace') -> None:
+ argument = options.project_path
+ spdir_name = 'subprojects'
+ sprojs = mesonlib.detect_subprojects(spdir_name)
+
+ # check if the argument is a full path to a subproject directory or wrap file
+ system_native_path_argument = argument.replace('/', os.sep)
+ for matches in sprojs.values():
+ if system_native_path_argument in matches:
+ do_promotion(system_native_path_argument, spdir_name)
+ return
+
+ # otherwise the argument is just a subproject basename which must be unambiguous
+ if argument not in sprojs:
+ raise SystemExit(f'Subproject {argument} not found in directory tree.')
+ matches = sprojs[argument]
+ if len(matches) > 1:
+ print(f'There is more than one version of {argument} in tree. Please specify which one to promote:\n', file=sys.stderr)
+ for s in matches:
+ print(s, file=sys.stderr)
+ raise SystemExit(1)
+ do_promotion(matches[0], spdir_name)
+
+def status(options: 'argparse.Namespace') -> None:
+ print('Subproject status')
+ for w in glob('subprojects/*.wrap'):
+ name = os.path.basename(w)[:-5]
+ try:
+ (latest_branch, latest_revision) = get_latest_version(name, options.allow_insecure)
+ except Exception:
+ print('', name, 'not available in wrapdb.', file=sys.stderr)
+ continue
+ try:
+ (current_branch, current_revision, _, _, _) = get_current_version(w)
+ except Exception:
+ print('', name, 'Wrap file not from wrapdb.', file=sys.stderr)
+ continue
+ if current_branch == latest_branch and current_revision == latest_revision:
+ print('', name, f'up to date. Branch {current_branch}, revision {current_revision}.')
+ else:
+ print('', name, f'not up to date. Have {current_branch} {current_revision}, but {latest_branch} {latest_revision} is available.')
+
+def update_db(options: 'argparse.Namespace') -> None:
+ data = get_releases_data(options.allow_insecure)
+ Path('subprojects').mkdir(exist_ok=True)
+ with Path('subprojects/wrapdb.json').open('wb') as f:
+ f.write(data)
+
+def run(options: 'argparse.Namespace') -> int:
+ options.wrap_func(options)
+ return 0