mirror of
https://github.com/MaskRay/ccls.git
synced 2024-11-22 07:35:08 +00:00
426 lines
17 KiB
Python
426 lines
17 KiB
Python
#!/usr/bin/env python
|
|
# encoding: utf-8
|
|
|
|
try:
|
|
from urllib2 import urlopen # Python 2
|
|
except ImportError:
|
|
from urllib.request import urlopen # Python 3
|
|
|
|
import os.path
|
|
import string
|
|
import subprocess
|
|
import sys
|
|
import re
|
|
import ctypes
|
|
import shutil
|
|
|
|
VERSION = '0.0.1'
|
|
APPNAME = 'cquery'
|
|
|
|
top = '.'
|
|
out = 'build'
|
|
|
|
CLANG_TARBALL_NAME = None
|
|
CLANG_TARBALL_EXT = '.tar.xz'
|
|
if sys.platform == 'darwin':
|
|
CLANG_TARBALL_NAME = 'clang+llvm-$version-x86_64-apple-darwin'
|
|
elif sys.platform.startswith('freebsd'):
|
|
CLANG_TARBALL_NAME = 'clang+llvm-$version-amd64-unknown-freebsd10'
|
|
# It is either 'linux2' or 'linux3' before Python 3.3
|
|
elif sys.platform.startswith('linux'):
|
|
# These executable depend on libtinfo.so.5
|
|
CLANG_TARBALL_NAME = 'clang+llvm-$version-x86_64-linux-gnu-ubuntu-14.04'
|
|
elif sys.platform == 'win32':
|
|
CLANG_TARBALL_NAME = 'LLVM-$version-win64'
|
|
CLANG_TARBALL_EXT = '.exe'
|
|
|
|
from waflib.Tools.compiler_cxx import cxx_compiler
|
|
cxx_compiler['linux'] = ['clang++', 'g++']
|
|
|
|
# Creating symbolic link on Windows requires a special priviledge SeCreateSymboliclinkPrivilege,
|
|
# which an non-elevated process lacks. Starting with Windows 10 build 14972, this got relaxed
|
|
# when Developer Mode is enabled. Triggering this new behaviour requires a new flag.
|
|
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2
|
|
|
|
# Python 2 has no symlink function and Python 3's symlink function does not allow creation of
|
|
# symlinks without admin rights (even when Developer Mode is enabled) so we define our own
|
|
# symlink function when on Windows.
|
|
if sys.platform == 'win32':
|
|
kdll = ctypes.windll.kernel32
|
|
def symlink(source, link_name, target_is_directory=False):
|
|
# SYMBOLIC_LINK_FLAG_DIRECTORY: 0x1
|
|
flags = int(target_is_directory) | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
|
|
|
|
# Use unicode version (W suffix) of Windows symbolic link function and convert strings to
|
|
# unicode if Python 2 is used (strings are unicode by default in Python 3).
|
|
if sys.version_info < (3, 0):
|
|
ret = kdll.CreateSymbolicLinkW(unicode(link_name), unicode(source), flags)
|
|
else:
|
|
ret = kdll.CreateSymbolicLinkW(link_name, source, flags)
|
|
|
|
if ret == 0:
|
|
err = ctypes.WinError()
|
|
raise err
|
|
os.symlink = symlink
|
|
elif sys.version_info.major < 3:
|
|
os_symlink_bak = os.symlink
|
|
os.symlink = lambda src, dst, target_is_directory=False: os_symlink_bak(src, dst)
|
|
|
|
# There is a null pointer dereference issue in tools/libclang/CXIndexDataConsumer.cpp handleReference.
|
|
def patch_byte_in_libclang(filename, offset, old, new):
|
|
with open(filename, 'rb+') as f:
|
|
f.seek(offset)
|
|
t = f.read(1)
|
|
if t == old:
|
|
f.seek(offset)
|
|
f.write(new)
|
|
print('Applied byte patch hack at 0x{:x}'.format(offset))
|
|
print('This is a makeshift for indexing default arguments of template template parameters, which otherwise would crash libclang')
|
|
print('See https://github.com/jacobdufault/cquery/issues/219 for details')
|
|
else:
|
|
assert t == new
|
|
|
|
def options(opt):
|
|
opt.load('compiler_cxx')
|
|
grp = opt.add_option_group('Configuration options related to use of clang from the system (not recommended)')
|
|
grp.add_option('--enable-assert', action='store_true')
|
|
grp.add_option('--use-clang-cxx', dest='use_clang_cxx', default=False, action='store_true',
|
|
help='use clang C++ API')
|
|
grp.add_option('--bundled-clang', dest='bundled_clang', default='5.0.1',
|
|
help='bundled clang version, downloaded from https://releases.llvm.org/ , e.g. 4.0.0 5.0.1')
|
|
grp.add_option('--llvm-config', dest='llvm_config',
|
|
help='path to llvm-config to use system libclang, e.g. llvm-config llvm-config-6.0')
|
|
grp.add_option('--clang-prefix', dest='clang_prefix',
|
|
help='enable fallback configuration method by specifying a clang installation prefix (e.g. /opt/llvm)')
|
|
grp.add_option('--variant', default='release',
|
|
help='variant name for saving configuration and build results. Variants other than "debug" turn on -O3')
|
|
|
|
def download_and_extract(destdir, url, ext):
|
|
dest = destdir + ext
|
|
|
|
# Extract the tarball.
|
|
if not os.path.isdir(os.path.join(destdir, 'include')):
|
|
# Download and save the compressed tarball as |compressed_file_name|.
|
|
if not os.path.isfile(dest):
|
|
print('Downloading tarball')
|
|
print(' destination: {0}'.format(dest))
|
|
print(' source: {0}'.format(url))
|
|
# TODO: verify checksum
|
|
response = urlopen(url, timeout=60)
|
|
with open(dest, 'wb') as f:
|
|
f.write(response.read())
|
|
else:
|
|
print('Found tarball at {0}'.format(dest))
|
|
|
|
print('Extracting {0}'.format(dest))
|
|
# TODO: make portable.
|
|
if ext == '.exe':
|
|
subprocess.call(['7z', 'x', '-o{0}'.format(destdir), '-xr!$PLUGINSDIR', dest])
|
|
else:
|
|
subprocess.call(['tar', '-x', '-C', out, '-f', dest])
|
|
# TODO Remove after migrating to a clang release newer than 5.0.1
|
|
# For 5.0.1 Mac OS X, the directory and the tarball have different name
|
|
if destdir == 'build/clang+llvm-5.0.1-x86_64-apple-darwin':
|
|
os.rename(destdir.replace('5.0.1', '5.0.1-final'), destdir)
|
|
# TODO Byte patch hack for other prebuilt binaries
|
|
elif destdir == 'build/clang+llvm-4.0.0-x86_64-linux-gnu-ubuntu-14.04':
|
|
patch_byte_in_libclang(os.path.join(destdir, 'lib/libclang.so.4.0'),
|
|
0x4172b5, b'\x48', b'\x4d')
|
|
elif destdir == 'build/clang+llvm-5.0.1-x86_64-linux-gnu-ubuntu-14.04':
|
|
patch_byte_in_libclang(os.path.join(destdir, 'lib/libclang.so.5.0'),
|
|
0x47aece, b'\x48', b'\x4d')
|
|
else:
|
|
print('Found extracted at {0}'.format(destdir))
|
|
|
|
def copy_or_symlink(src, dst):
|
|
print('copy_or_symlink src={0}, dst={1}'.format(src, dst))
|
|
try:
|
|
os.makedirs(os.path.dirname(dst))
|
|
except OSError:
|
|
pass
|
|
|
|
try:
|
|
os.symlink(src, dst)
|
|
except (OSError, NotImplementedError):
|
|
shutil.copy(src, dst)
|
|
|
|
def configure(ctx):
|
|
ctx.resetenv(ctx.options.variant)
|
|
|
|
ctx.load('compiler_cxx')
|
|
if ctx.env.CXX_NAME == 'msvc':
|
|
# /Zi: -g, /WX: -Werror, /W3: roughly -Wall, there is no -std=c++11 equivalent in MSVC.
|
|
# /wd4722: ignores warning C4722 (destructor never returns) in loguru
|
|
# /wd4267: ignores warning C4267 (conversion from 'size_t' to 'type'), roughly -Wno-sign-compare
|
|
# /MD: use multithread c library from DLL
|
|
cxxflags = ['/nologo', '/FS', '/EHsc', '/Zi', '/W3', '/wd4996', '/wd4722', '/wd4267', '/wd4800', '/MD']
|
|
ldflags = []
|
|
if 'release' in ctx.options.variant:
|
|
cxxflags.append('/O2') # There is no O3
|
|
else:
|
|
cxxflags += ['/Zi', '/FS']
|
|
ldflags += ['/DEBUG']
|
|
else:
|
|
# So that dladdr() called in loguru.hpp gives symbol names in main executable
|
|
ldflags = ['-rdynamic']
|
|
if ctx.env.CXXFLAGS:
|
|
cxxflags = ctx.env.CXXFLAGS
|
|
else:
|
|
cxxflags = ['-g', '-Wall', '-Wno-sign-compare', '-Werror']
|
|
if ctx.env.CXX_NAME == 'gcc':
|
|
cxxflags.append('-Wno-return-type')
|
|
# Otherwise (void)write(...) => -Werror=unused-result
|
|
cxxflags.append('-Wno-unused-result')
|
|
|
|
if all(not x.startswith('-std=') for x in ctx.env.CXXFLAGS):
|
|
cxxflags.append('-std=c++14')
|
|
|
|
if ctx.options.use_clang_cxx:
|
|
# include/clang/Format/Format.h error: multi-line comment
|
|
cxxflags.append('-Wno-comment')
|
|
# Without -fno-rtti, some Clang C++ functions may report `undefined references to typeinfo`
|
|
cxxflags.append('-fno-rtti')
|
|
|
|
if 'asan' in ctx.options.variant:
|
|
cxxflags.append('-fsanitize=address,undefined')
|
|
ldflags.append('-fsanitize=address,undefined')
|
|
if 'release' in ctx.options.variant:
|
|
cxxflags.append('-O' if 'asan' in ctx.options.variant else '-O3')
|
|
|
|
if ctx.options.enable_assert is None:
|
|
if 'debug' in ctx.options.variant:
|
|
ctx.options.enable_assert = True
|
|
if not ctx.options.enable_assert:
|
|
ctx.define('NDEBUG', 1)
|
|
|
|
if ctx.env.CXX_NAME == 'clang' and 'debug' in ctx.options.variant:
|
|
cxxflags.append('-fno-limit-debug-info')
|
|
|
|
ctx.env.CXXFLAGS = cxxflags
|
|
if not ctx.env.LDFLAGS:
|
|
ctx.env.LDFLAGS = ldflags
|
|
|
|
ctx.check(header_name='stdio.h', features='cxx cxxprogram', mandatory=True)
|
|
|
|
ctx.load('clang_compilation_database', tooldir='.')
|
|
|
|
ctx.env['use_clang_cxx'] = ctx.options.use_clang_cxx
|
|
ctx.env['llvm_config'] = ctx.options.llvm_config
|
|
ctx.env['bundled_clang'] = ctx.options.bundled_clang
|
|
def libname(lib):
|
|
# Newer MinGW and MSVC both wants full file name
|
|
if sys.platform == 'win32':
|
|
return 'lib' + lib
|
|
return lib
|
|
|
|
# Do not use bundled clang+llvm
|
|
if ctx.options.llvm_config is not None or ctx.options.clang_prefix is not None:
|
|
if ctx.options.llvm_config is not None:
|
|
# Ask llvm-config for cflags and ldflags
|
|
ctx.find_program(ctx.options.llvm_config, msg='checking for llvm-config', var='LLVM_CONFIG', mandatory=False)
|
|
|
|
ctx.env.rpath = [str(subprocess.check_output(
|
|
[ctx.options.llvm_config, '--libdir'],
|
|
stderr=subprocess.STDOUT).decode()).strip()]
|
|
|
|
if ctx.options.clang_prefix:
|
|
ctx.start_msg('Checking for clang prefix')
|
|
prefix = ctx.root.find_node(ctx.options.clang_prefix)
|
|
if not prefix:
|
|
raise ctx.errors.ConfigurationError('clang prefix not found: "%s"'%ctx.options.clang_prefix)
|
|
ctx.end_msg(prefix)
|
|
|
|
includes = [ n.abspath() for n in [ prefix.find_node('include') ] if n ]
|
|
libpath = [ n.abspath() for n in [ prefix.find_node(l) for l in ('lib', 'lib64')] if n ]
|
|
ctx.check_cxx(msg='Checking for library clang', lib=libname('clang'), uselib_store='clang', includes=includes, libpath=libpath)
|
|
else:
|
|
ctx.check_cfg(msg='Checking for clang flags',
|
|
path=ctx.env.LLVM_CONFIG,
|
|
package='',
|
|
uselib_store='clang',
|
|
args='--cppflags --ldflags')
|
|
# llvm-config does not provide the actual library we want so we check for it
|
|
# using the provided info so far.
|
|
ctx.check_cxx(lib=libname('clang'), uselib_store='clang', use='clang')
|
|
|
|
|
|
# Use CXX set by --check-cxx-compiler if it is "clang".
|
|
# See https://github.com/jacobdufault/cquery/issues/237
|
|
clang = ctx.env.get_flat('CXX')
|
|
if 'clang' not in clang:
|
|
# Otherwise, infer the clang executable path with llvm-config --bindir
|
|
output = str(subprocess.check_output(
|
|
[ctx.options.llvm_config, '--bindir'],
|
|
stderr=subprocess.STDOUT).decode()).strip()
|
|
clang = os.path.join(output, 'clang')
|
|
|
|
# Use the detected clang executable to infer resource directory
|
|
# Use `clang -### -xc /dev/null` instead of `clang -print-resource-dir` because the option is unavailable in 4.0.0
|
|
devnull = '/dev/null' if sys.platform != 'win32' else 'NUL'
|
|
output = str(subprocess.check_output(
|
|
[clang, '-###', '-xc', devnull],
|
|
stderr=subprocess.STDOUT).decode())
|
|
match = re.search(r'"-resource-dir" "([^"]*)"', output, re.M)
|
|
if match:
|
|
ctx.env.default_resource_directory = match.group(1)
|
|
else:
|
|
bld.fatal("Failed to found system clang resource directory.")
|
|
|
|
else:
|
|
global CLANG_TARBALL_NAME, CLANG_TARBALL_EXT
|
|
|
|
if CLANG_TARBALL_NAME is None:
|
|
sys.stderr.write('ERROR: releases.llvm.org does not provide prebuilt binary for your platform {0}\n'.format(sys.platform))
|
|
sys.exit(1)
|
|
|
|
# TODO Remove after 5.0.1 is stable
|
|
if ctx.options.bundled_clang == '5.0.0' and sys.platform.startswith('linux'):
|
|
CLANG_TARBALL_NAME = 'clang+llvm-$version-linux-x86_64-ubuntu14.04'
|
|
|
|
CLANG_TARBALL_NAME = string.Template(CLANG_TARBALL_NAME).substitute(version=ctx.options.bundled_clang)
|
|
# Directory clang has been extracted to.
|
|
CLANG_DIRECTORY = '{0}/{1}'.format(out, CLANG_TARBALL_NAME)
|
|
# URL of the tarball to download.
|
|
CLANG_TARBALL_URL = 'http://releases.llvm.org/{0}/{1}{2}'.format(ctx.options.bundled_clang, CLANG_TARBALL_NAME, CLANG_TARBALL_EXT)
|
|
|
|
print('Checking for clang')
|
|
download_and_extract(CLANG_DIRECTORY, CLANG_TARBALL_URL, CLANG_TARBALL_EXT)
|
|
|
|
bundled_clang_dir = os.path.join(out, ctx.options.variant, 'lib', CLANG_TARBALL_NAME)
|
|
try:
|
|
os.makedirs(os.path.dirname(bundled_clang_dir))
|
|
except OSError:
|
|
pass
|
|
clang_dir = os.path.normpath('../../' + CLANG_TARBALL_NAME)
|
|
try:
|
|
if not os.path.exists(bundled_clang_dir):
|
|
os.symlink(clang_dir, bundled_clang_dir, target_is_directory=True)
|
|
except (OSError, NotImplementedError):
|
|
# Copying the whole directory instead.
|
|
print ('shutil.copytree({0}, {1})'.format(os.path.join(out, CLANG_TARBALL_NAME), bundled_clang_dir))
|
|
try:
|
|
shutil.copytree(os.path.join(out, CLANG_TARBALL_NAME), bundled_clang_dir)
|
|
except Exception as e:
|
|
print('Failed to copy tree, ', e)
|
|
|
|
clang_node = ctx.path.find_dir(bundled_clang_dir)
|
|
ctx.check_cxx(uselib_store='clang',
|
|
includes=clang_node.find_dir('include').abspath(),
|
|
libpath=clang_node.find_dir('lib').abspath(),
|
|
lib=libname('clang'))
|
|
|
|
clang_tarball_name = os.path.basename(os.path.dirname(ctx.env['LIBPATH_clang'][0]))
|
|
ctx.env.clang_tarball_name = clang_tarball_name
|
|
ctx.env.default_resource_directory = '../lib/{}/lib/clang/{}'.format(clang_tarball_name, ctx.env.bundled_clang)
|
|
|
|
if sys.platform.startswith('freebsd') or sys.platform.startswith('linux'):
|
|
ctx.env.rpath = ['$ORIGIN/../lib/' + clang_tarball_name + '/lib']
|
|
elif sys.platform == 'darwin':
|
|
ctx.env.rpath = ['@loader_path/../lib/' + clang_tarball_name + '/lib']
|
|
elif sys.platform == 'win32':
|
|
# Poor Windows users' RPATH - copy libclang.lib and libclang.dll to the build directory.
|
|
ctx.env.rpath = [] # Unsupported
|
|
clang_dir = os.path.dirname(ctx.env['LIBPATH_clang'][0])
|
|
dest_dir = os.path.join(ctx.path.get_bld().abspath(), ctx.options.variant, 'bin')
|
|
# copy_or_symlink(os.path.join(clang_dir, 'lib', 'libclang.lib'), os.path.join(dest_dir, 'libclang.lib'))
|
|
copy_or_symlink(os.path.join(clang_dir, 'bin', 'libclang.dll'), os.path.join(dest_dir, 'libclang.dll'))
|
|
else:
|
|
ctx.env.rpath = ctx.env['LIBPATH_clang']
|
|
|
|
ctx.msg('Clang includes', ctx.env.INCLUDES_clang)
|
|
ctx.msg('Clang library dir', ctx.env.LIBPATH_clang)
|
|
|
|
def build(bld):
|
|
cc_files = bld.path.ant_glob(['src/*.cc', 'src/messages/*.cc', 'third_party/*.cc'])
|
|
if bld.env['use_clang_cxx']:
|
|
cc_files += bld.path.ant_glob(['src/clang_cxx/*.cc'])
|
|
|
|
lib = []
|
|
if sys.platform.startswith('linux'):
|
|
# For __atomic_* when lock free instructions are unavailable
|
|
# (either through hardware or OS support)
|
|
lib.append('atomic')
|
|
lib.append('pthread')
|
|
# loguru calls dladdr
|
|
lib.append('dl')
|
|
elif sys.platform.startswith('freebsd'):
|
|
# loguru::stacktrace_as_stdstring calls backtrace_symbols
|
|
lib.append('execinfo')
|
|
# sparsepp/spp_memory.h uses libkvm
|
|
lib.append('kvm')
|
|
|
|
lib.append('pthread')
|
|
lib.append('thr')
|
|
elif sys.platform == 'darwin':
|
|
lib.append('pthread')
|
|
elif sys.platform == 'msys':
|
|
lib.append('psapi') # GetProcessMemoryInfo
|
|
|
|
if bld.env['use_clang_cxx']:
|
|
# -fno-rtti is required for object files using clang/llvm C++ API
|
|
|
|
# The order is derived by topological sorting LINK_LIBS in clang/lib/*/CMakeLists.txt
|
|
lib.append('clangFormat')
|
|
lib.append('clangToolingCore')
|
|
lib.append('clangRewrite')
|
|
lib.append('clangAST')
|
|
lib.append('clangLex')
|
|
lib.append('clangBasic')
|
|
|
|
# The order is derived from llvm-config --libs core
|
|
lib.append('LLVMCore')
|
|
lib.append('LLVMBinaryFormat')
|
|
lib.append('LLVMSupport')
|
|
lib.append('LLVMDemangle')
|
|
|
|
lib.append('ncurses')
|
|
|
|
# https://waf.io/apidocs/tools/c_aliases.html#waflib.Tools.c_aliases.program
|
|
bld.program(
|
|
source=cc_files,
|
|
use=['clang'],
|
|
includes=[
|
|
'src/',
|
|
'third_party/',
|
|
'third_party/doctest/',
|
|
'third_party/loguru/',
|
|
'third_party/msgpack-c/include',
|
|
'third_party/rapidjson/include/',
|
|
'third_party/sparsepp/'] +
|
|
(['libclang'] if bld.env['use_clang_cxx'] else []),
|
|
defines=[
|
|
'LOGURU_WITH_STREAMS=1',
|
|
'LOGURU_FILENAME_WIDTH=18',
|
|
'LOGURU_THREADNAME_WIDTH=13',
|
|
'DEFAULT_RESOURCE_DIRECTORY="' + bld.env.get_flat('default_resource_directory') + '"'] +
|
|
(['USE_CLANG_CXX=1', 'LOGURU_RTTI=0']
|
|
if bld.env['use_clang_cxx']
|
|
else []),
|
|
lib=lib,
|
|
rpath=bld.env.rpath,
|
|
target='bin/cquery')
|
|
|
|
if bld.cmd == 'install' and 'clang_tarball_name' in bld.env:
|
|
clang_tarball_name = bld.env.clang_tarball_name
|
|
if sys.platform != 'win32':
|
|
bld.install_files('${PREFIX}/lib/' + clang_tarball_name + '/lib', bld.path.get_bld().ant_glob('lib/' + clang_tarball_name + '/lib/libclang.(dylib|so.[4-9])', quiet=True))
|
|
# TODO This may be cached and cannot be re-triggered. Use proper shell escape.
|
|
bld(rule='rsync -rtR {}/./lib/{}/lib/clang/*/include {}/'.format(bld.path.get_bld(), clang_tarball_name, bld.env['PREFIX']))
|
|
|
|
def init(ctx):
|
|
from waflib.Build import BuildContext, CleanContext, InstallContext, UninstallContext
|
|
for y in (BuildContext, CleanContext, InstallContext, UninstallContext):
|
|
class tmp(y):
|
|
variant = ctx.options.variant
|
|
|
|
# This is needed because waf initializes the ConfigurationContext with
|
|
# an arbitrary setenv('') which would rewrite the previous configuration
|
|
# cache for the default variant if the configure step finishes.
|
|
# Ideally ConfigurationContext should just let us override this at class
|
|
# level like the other Context subclasses do with variant
|
|
from waflib.Configure import ConfigurationContext
|
|
class cctx(ConfigurationContext):
|
|
def resetenv(self, name):
|
|
self.all_envs = {}
|
|
self.setenv(name)
|