diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-29 04:41:38 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-29 04:41:38 +0000 |
commit | 7b6e527f440cd7e6f8be2b07cee320ee6ca18786 (patch) | |
tree | 4a2738d69fa2814659fdadddf5826282e73d81f4 /mesonbuild/dependencies/dev.py | |
parent | Initial commit. (diff) | |
download | meson-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 'mesonbuild/dependencies/dev.py')
-rw-r--r-- | mesonbuild/dependencies/dev.py | 679 |
1 files changed, 679 insertions, 0 deletions
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, +) |