Skip to content

Commit

Permalink
dependencies/clang: Add a dependency for Clang
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dcbaker committed Jul 17, 2024
1 parent b7efc2e commit 62bddef
Show file tree
Hide file tree
Showing 10 changed files with 436 additions and 0 deletions.
35 changes: 35 additions & 0 deletions docs/markdown/Dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)*
Expand Down
5 changes: 5 additions & 0 deletions docs/markdown/snippets/clang_dependency.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions mesonbuild/dependencies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
173 changes: 173 additions & 0 deletions mesonbuild/dependencies/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <clang/Basic/Version.inc>', 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 <clang/Basic/Version.h>', 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
Expand Down Expand Up @@ -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',
Expand Down
33 changes: 33 additions & 0 deletions test cases/frameworks/38 clang/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright © 2024 Intel Corporation
*/

#include <clang-c/Index.h>

#include <stdio.h>

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;
}
89 changes: 89 additions & 0 deletions test cases/frameworks/38 clang/main.cpp
Original file line number Diff line number Diff line change
@@ -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 <iostream>

/******************************************************************************
*
*****************************************************************************/
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<clang::TargetOptions> pto = std::make_shared<clang::TargetOptions>();
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;
}
Loading

0 comments on commit 62bddef

Please sign in to comment.