From 62bddefc2eb4dc6cd36597ef7cae892e3939681c Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Fri, 19 Apr 2024 16:41:04 -0700 Subject: [PATCH] dependencies/clang: Add a dependency for Clang Clang has two ways to be found, CMake, and by hand. Clang has some hurdles of use because of the way it's installed on many Linux distros, either in a separate prefix, in a prefix with LLVM, or in a common prefix, which requires some amount of effort to make it work. --- docs/markdown/Dependencies.md | 35 ++++ docs/markdown/snippets/clang_dependency.md | 5 + mesonbuild/dependencies/__init__.py | 2 + mesonbuild/dependencies/dev.py | 173 +++++++++++++++++++ test cases/frameworks/38 clang/main.c | 33 ++++ test cases/frameworks/38 clang/main.cpp | 89 ++++++++++ test cases/frameworks/38 clang/meson.build | 42 +++++ test cases/frameworks/38 clang/meson.options | 19 ++ test cases/frameworks/38 clang/test.cpp | 8 + test cases/frameworks/38 clang/test.json | 30 ++++ 10 files changed, 436 insertions(+) create mode 100644 docs/markdown/snippets/clang_dependency.md create mode 100644 test cases/frameworks/38 clang/main.c create mode 100644 test cases/frameworks/38 clang/main.cpp create mode 100644 test cases/frameworks/38 clang/meson.build create mode 100644 test cases/frameworks/38 clang/meson.options create mode 100644 test cases/frameworks/38 clang/test.cpp create mode 100644 test cases/frameworks/38 clang/test.json diff --git a/docs/markdown/Dependencies.md b/docs/markdown/Dependencies.md index d91582523d44..346e7402e14b 100644 --- a/docs/markdown/Dependencies.md +++ b/docs/markdown/Dependencies.md @@ -602,6 +602,41 @@ llvm_dep = dependency('llvm', version : ['>= 8', '< 9']) llvm_link = find_program(llvm_dep.get_variable(configtool: 'bindir') / 'llvm-link') ``` +## Clang + +*(since 1.6.0)* + +Meson has native support for Clang, as well as support for using CMake to find Clang. +Because of the tight coupling between Clang and LLVM, the Clang dependency has a +specific argument to select the LLVM to use, or an internal version will be used +(When using the system based finder). This argument is unused with the CMake finder: + +```meson +llvm = dependency('llvm', version : ['>= 16', '< 17']) +clang = dependency('clang', version : ['>= 16', '< 17'], llvm : llvm, method : 'system') +``` + +Both libclang (the C interface) and the C++ interfaces are supported via the +`language` keyword. The default is to search for the `C` interface. + +If the `language` is `c`, then `libclang` will be searched for. This may be +built static or shared, and is a Clang configuration option. + +Otherwise, if the dependency may be shared, `clang-cpp` will be searched for +before loose clang libraries. It is always considered to have all of the modules +included. + +`method` may be `auto`, `system`, or `cmake`. + +### Modules + +Clang modules are supported, and must be passed in the format `clangBasic`, with +proper capitalization and the `clang` prepended. + +```meson +clang = dependency('clang', static : true, modules : ['clangBasic', 'clangIndex']) +``` + ## MPI *(added 0.42.0)* diff --git a/docs/markdown/snippets/clang_dependency.md b/docs/markdown/snippets/clang_dependency.md new file mode 100644 index 000000000000..467b57619ac6 --- /dev/null +++ b/docs/markdown/snippets/clang_dependency.md @@ -0,0 +1,5 @@ +## A Clang dependency + +This helps to simplify the use of libclang, removing the need to try cmake and +then falling back to not cmake. It also transparently handles the issues +associated with different paths to find Clang on different OSes. diff --git a/mesonbuild/dependencies/__init__.py b/mesonbuild/dependencies/__init__.py index 89d2285ba3a6..eb54eafe42a7 100644 --- a/mesonbuild/dependencies/__init__.py +++ b/mesonbuild/dependencies/__init__.py @@ -188,6 +188,7 @@ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T. # - a string naming the submodule that should be imported from `mesonbuild.dependencies` to populate the dependency packages.defaults.update({ # From dev: + 'clang': 'dev', 'gtest': 'dev', 'gmock': 'dev', 'llvm': 'dev', @@ -246,6 +247,7 @@ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T. 'qt6': 'qt', }) _packages_accept_language.update({ + 'clang', 'hdf5', 'mpi', 'netcdf', diff --git a/mesonbuild/dependencies/dev.py b/mesonbuild/dependencies/dev.py index de85516feb64..b20aca1ee6ff 100644 --- a/mesonbuild/dependencies/dev.py +++ b/mesonbuild/dependencies/dev.py @@ -506,6 +506,172 @@ def _original_module_name(self, module: str) -> str: return module +class ClangSystemDependency(SystemDependency): + + def __init__(self, name: str, env: Environment, kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None: + language = kwargs.get('language', language) + if language not in {None, 'c', 'cpp'}: + raise DependencyException('Clang only provides C and C++ language support') + + super().__init__(name, env, kwargs, language) + self.feature_since = ('1.6.0', '') + self.module_details: T.List[str] = [] + + # Clang may be installed a number of different ways: + # + # 1. Clang is installed directly in a common search path + # 2. Clang is installed alongside LLVM in a separate path to allow multiple versions + # to be co-installed. (Debian and Gentoo do this) + # 3. LLVM and Clang are installed in separate, default search paths. (NixOS does this) + # + # In order to accommodate all three of these we need to search both in + # the LLVM directory and outside of it. Start with the LLVM dir to avoid + # a situation where there is Clang next to LLVM and a different one in a + # common path + # + # Try to handle the combinations of CMake and config-tool LLVM with this + # method, even though it probably doesn't make sense to use the system + # finder for Clang with CMake LLVM + llvm = T.cast('T.Optional[ExternalDependency]', kwargs.get('llvm')) + if llvm is not None: + if not llvm.found(): + mlog.debug('Passed LLVM was not found, treating Clang as not found') + return + if self.version_reqs and not mesonlib.version_compare_many(llvm.version, self.version_reqs): + mlog.debug('Passed LLVMs version does not match the version required for Clang, treating it as not found') + return + self.ext_deps.append(llvm) + else: + if not self._add_sub_dependency( + llvm_factory( + env, self.for_machine, {'required': False, 'version': kwargs.get('version'), 'method': 'config-tool'})): + return + llvm = T.cast('ExternalDependency', self.ext_deps[0]) + # Clang and LLVM need to have the same version + self.version = llvm.version + + # libclang-cpp.so does not require modules, but there is no static equivalent + modules = stringlistify(extract_as_list(kwargs, 'modules')) + if not modules and language == 'cpp': + mlog.warning('Clang C++ dependency without modules works correctly for dynamically linked Clang, ' + 'but will fail to find a statically linked Clang', once=True, fatal=False) + + dirs: T.List[T.List[str]] = [[llvm.get_variable(configtool='libdir', cmake='LLVM_LIBRARY_DIR')], []] + + # Clang provides up to two interfaces for C++ code, and only one for C + # + # For C++ you can use libclang-cpp.so, or you can use loose static + # archives (This is just like LLVM). + # + # For C you use libclang which may be built static or shared, depending + # on configuration. + if not self.static or language == 'c': + if language == 'cpp': + # Use strict libtypes for C++ since we can fall through to + # individual libs if we can't find what + libtype = mesonlib.LibType.SHARED + libname = 'clang-cpp' + else: + libtype = mesonlib.LibType.PREFER_STATIC if self.static else mesonlib.LibType.PREFER_SHARED + libname = 'clang' + + for search in dirs: + lib = self.clib_compiler.find_library(libname, env, search, libtype=libtype) + if lib: + # Version.h is a C++ header, and this will fail if we look + # for clang-c. The inc is just the basic + version = self.clib_compiler.get_define('CLANG_VERSION', '#include ', env, lib, self.ext_deps)[0] + if not version: + mlog.debug(f'Could not find Clang in {search}, Becuase Version header was not found') + continue + + if not self.version_reqs or mesonlib.version_compare_many(version, self.version_reqs): + self.version = version + self.link_args = lib + self.is_found = True + return + + # If we don't have modules, or we're looking for C we're done, it's not going to find anything anyway + if not modules or language != 'cpp': + return + + opt_modules = stringlistify(extract_as_list(kwargs, 'optional_modules')) + + libtype = mesonlib.LibType.PREFER_STATIC if self.static else mesonlib.LibType.PREFER_SHARED + + for search in dirs: + self.module_details.clear() + libs: T.List[str] = [] + for m in modules: + lib = self.clib_compiler.find_library(m, env, search, libtype) + if lib: + libs.extend(lib) + self.module_details.append(m) + else: + self.module_details.append(f'{m} (missing)') + # Intentionally do not break here so that we can get an + # accurate count of missing modules + if len(modules) != len(libs): + mlog.debug(f'Could not find Clang in {search}, ' + f'because of missing modules: {self.module_details}') + continue + + for m in opt_modules: + lib = self.clib_compiler.find_library(m, env, search, libtype) + if lib: + libs.extend(lib) + self.module_details.append(m) + else: + self.module_details.append(f'{m} (missing but optional)') + + version = self.clib_compiler.get_define('CLANG_VERSION', '#include ', env, libs, self.ext_deps)[0] + if not version: + mlog.debug(f'Could not find Clang in {search}, Becuase Version header was not found') + continue + + if not self.version_reqs or mesonlib.version_compare_many(version, self.version_reqs): + self.version = version + self.link_args = libs + self.is_found = True + return + + mlog.debug(f'Could not find Clang in {search}, because of version mismatch, ' + f'required {", ".join(self.version_reqs)}, version: {version}') + + def log_details(self) -> str: + if self.module_details: + return 'modules: ' + ', '.join(self.module_details) + return '' + + +class ClangCMakeDependency(CMakeDependency): + + 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: + language = kwargs.get('language', language) + + # libclang-cpp.so does not require modules, but there is no static equivalent + if not kwargs.get('modules') and language == 'cpp': + mlog.warning('Clang C++ dependency without modules works correctly for dynamically linked Clang, ' + 'but will fail to find a statically linked Clang', once=True, fatal=False) + + # There are no loose libs for the C api, only libclang + if language != 'cpp': + kwargs['modules'] = ['libclang'] + elif not kwargs.get('static', False): + # XXX: We really need to try twice here, once for clang-cpp and once + # for individual libs. We're probably going to need a custom + # factory… + kwargs['modules'] = ['clang-cpp'] + + # The C compiler is required for C++ mode, otherwise it will fail. + # Setting this option will add the C compielr if it's enabled + if language == 'cpp': + force_use_global_compilers = True + + super().__init__(name, environment, kwargs, language, force_use_global_compilers) + + class ValgrindDependency(PkgConfigDependency): ''' Consumers of Valgrind usually only need the compile args and do not want to @@ -699,6 +865,13 @@ def __init__(self, environment: 'Environment', kwargs: JNISystemDependencyKW): packages['jdk'] = JDKSystemDependency +packages['clang'] = DependencyFactory( + 'clang', + [DependencyMethods.CMAKE, DependencyMethods.SYSTEM], + cmake_class=ClangCMakeDependency, + cmake_name='Clang', + system_class=ClangSystemDependency, +) packages['llvm'] = llvm_factory = DependencyFactory( 'LLVM', diff --git a/test cases/frameworks/38 clang/main.c b/test cases/frameworks/38 clang/main.c new file mode 100644 index 000000000000..804a0683077d --- /dev/null +++ b/test cases/frameworks/38 clang/main.c @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright © 2024 Intel Corporation + */ + +#include + +#include + +int main(int argc, char * argv[]) { + if (argc < 2) { + fprintf(stderr, "At least one argument is required!\n"); + return 1; + } + + const char * file = argv[1]; + + CXIndex index = clang_createIndex(0, 0); + CXTranslationUnit unit = clang_parseTranslationUnit( + index, + file, NULL, 0, + NULL, 0, + CXTranslationUnit_None); + + if (unit == NULL) { + return 1; + } + + clang_disposeTranslationUnit(unit); + clang_disposeIndex(index); + + return 0; +} diff --git a/test cases/frameworks/38 clang/main.cpp b/test cases/frameworks/38 clang/main.cpp new file mode 100644 index 000000000000..7f79f1ad7a00 --- /dev/null +++ b/test cases/frameworks/38 clang/main.cpp @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * Copyright (c) 2010, Larry Olson + * Copyright © 2024 Intel Corporation + * + * Taken from: https://github.com/loarabia/Clang-tutorial/blob/master/CItutorial2.cpp + * + * With updates for LLVM and Clang 18 + */ + +#include "llvm/Config/llvm-config.h" + +#if LLVM_VERSION_MAJOR >= 18 +#include "llvm/TargetParser/Host.h" +#else +#include "llvm/Support/Host.h" +#endif +#include "llvm/ADT/IntrusiveRefCntPtr.h" + +#include "clang/Basic/DiagnosticOptions.h" +#include "clang/Frontend/TextDiagnosticPrinter.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Basic/TargetOptions.h" +#include "clang/Basic/TargetInfo.h" +#include "clang/Basic/FileManager.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Lex/Preprocessor.h" +#include "clang/Basic/Diagnostic.h" + +#include + +/****************************************************************************** + * + *****************************************************************************/ +int main(int argc, const char * argv[]) +{ + using clang::CompilerInstance; + using clang::TargetOptions; + using clang::TargetInfo; +#if LLVM_VERSION_MAJOR >= 18 + using clang::FileEntryRef; +#else + using clang::FileEntry; +#endif + using clang::Token; + using clang::DiagnosticOptions; + using clang::TextDiagnosticPrinter; + + if (argc != 2) { + std::cerr << "Need exactly 2 arguments." << std::endl; + return 1; + } + + CompilerInstance ci; + DiagnosticOptions diagnosticOptions; + ci.createDiagnostics(); + + std::shared_ptr pto = std::make_shared(); + pto->Triple = llvm::sys::getDefaultTargetTriple(); + TargetInfo *pti = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), pto); + ci.setTarget(pti); + + ci.createFileManager(); + ci.createSourceManager(ci.getFileManager()); + ci.createPreprocessor(clang::TU_Complete); + + +#if LLVM_VERSION_MAJOR >= 18 + const FileEntryRef pFile = ci.getFileManager().getFileRef(argv[1]).get(); +#else + const FileEntry *pFile = ci.getFileManager().getFile(argv[1]).get(); +#endif + ci.getSourceManager().setMainFileID( ci.getSourceManager().createFileID( pFile, clang::SourceLocation(), clang::SrcMgr::C_User)); + ci.getPreprocessor().EnterMainSourceFile(); + ci.getDiagnosticClient().BeginSourceFile(ci.getLangOpts(), + &ci.getPreprocessor()); + Token tok; + bool err; + do { + ci.getPreprocessor().Lex(tok); + err = ci.getDiagnostics().hasErrorOccurred(); + if (err) break; + ci.getPreprocessor().DumpToken(tok); + std::cerr << std::endl; + } while ( tok.isNot(clang::tok::eof)); + ci.getDiagnosticClient().EndSourceFile(); + + return err ? 1 : 0; +} diff --git a/test cases/frameworks/38 clang/meson.build b/test cases/frameworks/38 clang/meson.build new file mode 100644 index 000000000000..a0d5be2af721 --- /dev/null +++ b/test cases/frameworks/38 clang/meson.build @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2024 Intel Corporation + +# C and C++ for are needed for cmake always +project('clangtest', 'c', 'cpp', default_options : ['c_std=c99', 'cpp_std=c++17']) + +method = get_option('method') +static = get_option('link-static') +language = get_option('language') + +test_file = files('test.cpp') + +if language == 'c' + dep_clang_c = dependency('clang', method : method, static : static, language : 'c', required : false) + if not dep_clang_c.found() + error('MESON_SKIP_TEST Could not find Clang for C language') + endif + exe = executable('parser', 'main.c', dependencies : dep_clang_c) + test('C API', exe, args : [test_file]) +else + modules_to_find = [ + 'clangAST', + 'clangASTMatchers', + 'clangAnalysis', + 'clangBasic', + 'clangDriver', + 'clangEdit', + 'clangFrontend', + 'clangLex', + 'clangParse', + 'clangSema', + 'clangSerialization', + 'clangSupport', + ] + + dep_clang_cpp = dependency('clang', modules : modules_to_find, method : method, static : static, language : 'cpp', required : false) + if not dep_clang_cpp.found() + error('MESON_SKIP_TEST Could not find Clang for C++ language') + endif + exe = executable('cpp-parser', 'main.cpp', dependencies : dep_clang_cpp) + test('C++ API', exe, args : [test_file]) +endif diff --git a/test cases/frameworks/38 clang/meson.options b/test cases/frameworks/38 clang/meson.options new file mode 100644 index 000000000000..023933888934 --- /dev/null +++ b/test cases/frameworks/38 clang/meson.options @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2024 Intel Corporation + +option( + 'method', + type : 'combo', + choices : ['system', 'cmake'] +) +option( + 'link-static', + type : 'boolean', + value : false, +) +option( + 'language', + type : 'combo', + choices : ['c', 'cpp'], + value : 'c', +) diff --git a/test cases/frameworks/38 clang/test.cpp b/test cases/frameworks/38 clang/test.cpp new file mode 100644 index 000000000000..396ba0757cfd --- /dev/null +++ b/test cases/frameworks/38 clang/test.cpp @@ -0,0 +1,8 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright © 2024 Intel Corporation + */ + +int func() { + return 1; +} diff --git a/test cases/frameworks/38 clang/test.json b/test cases/frameworks/38 clang/test.json new file mode 100644 index 000000000000..133aace5aaee --- /dev/null +++ b/test cases/frameworks/38 clang/test.json @@ -0,0 +1,30 @@ +{ + "matrix": { + "options": { + "method": [ + { "val": "system", "expect_skip_on_jobname": ["msys2-gccx64ninja", "msys2-gccx86ninja", "azure-vc2019x64vs", "azure-vc2019x64ninja"] }, + { "val": "cmake", "expect_skip_on_jobname": ["msys2-gccx64ninja", "msys2-gccx86ninja", "azure-vc2019x64vs", "azure-vc2019x64ninja"] } + ], + "link-static": [ + { + "val": true, + "expect_skip_on_jobname": [ + "linux-arch-gcc", "linux-arch-gcc-pypy", "linux-opensuse-gcc", + "linux-fedora-gcc", "linux-gentoo-gcc" + ] + }, + { "val": false } + ], + "language": [ + { "val": "c" }, + { "val": "cpp" } + ] + }, + "exclude": [ + { "link-static": true, "language": "c" } + ] + }, + "tools": { + "cmake": ">=3.11" + } +}