kitty/setup.py

606 lines
19 KiB
Python
Raw Normal View History

2016-11-21 07:55:53 +03:00
#!/usr/bin/env python3
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import argparse
2017-11-19 20:54:36 +03:00
import importlib
import json
import os
import re
import shlex
import shutil
import subprocess
import sys
import sysconfig
base = os.path.dirname(os.path.abspath(__file__))
2017-11-19 20:54:36 +03:00
sys.path.insert(0, os.path.join(base, 'glfw'))
glfw = importlib.import_module('glfw')
2017-11-20 12:56:27 +03:00
verbose = False
2017-11-19 20:54:36 +03:00
del sys.path[0]
build_dir = os.path.join(base, 'build')
constants = os.path.join(base, 'kitty', 'constants.py')
with open(constants, 'rb') as f:
constants = f.read().decode('utf-8')
appname = re.search(r"^appname = '([^']+)'", constants, re.MULTILINE).group(1)
2017-02-09 21:34:05 +03:00
version = tuple(
map(
int,
re.search(
r"^version = \((\d+), (\d+), (\d+)\)", constants, re.MULTILINE
).group(1, 2, 3)
)
)
2017-01-07 10:31:32 +03:00
_plat = sys.platform.lower()
isosx = 'darwin' in _plat
2016-11-12 06:09:23 +03:00
is_travis = os.environ.get('TRAVIS') == 'true'
2017-11-19 10:24:02 +03:00
env = None
PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config')
2016-11-02 18:57:20 +03:00
def pkg_config(pkg, *args):
2017-02-09 21:34:05 +03:00
return list(
filter(
None,
shlex.split(
subprocess.check_output([PKGCONFIG, pkg] + list(args))
.decode('utf-8')
)
)
)
2016-11-02 18:57:20 +03:00
2017-11-05 06:52:15 +03:00
def at_least_version(package, major, minor=0):
q = '{}.{}'.format(major, minor)
if subprocess.run([PKGCONFIG, package, '--atleast-version=' + q]
).returncode != 0:
try:
ver = subprocess.check_output([PKGCONFIG, package, '--modversion']
).decode('utf-8').strip()
qmajor, qminor = map(int, re.match(r'(\d+).(\d+)', ver).groups())
except Exception:
ver = 'not found'
qmajor = qminor = 0
if qmajor < major or (qmajor == major and qminor < minor):
raise SystemExit(
'{} >= {}.{} is required, found version: {}'.format(
package, major, minor, ver
)
)
def cc_version():
2017-11-08 15:00:55 +03:00
cc = os.environ.get('CC', 'clang' if isosx else 'gcc')
raw = subprocess.check_output([cc, '-dumpversion']).decode('utf-8')
ver = raw.split('.')[:2]
try:
ver = tuple(map(int, ver))
except Exception:
ver = (0, 0)
return cc, ver
def get_python_flags(cflags):
2017-02-09 21:34:05 +03:00
cflags.extend(
'-I' + sysconfig.get_path(x) for x in 'include platinclude'.split()
)
libs = []
libs += sysconfig.get_config_var('LIBS').split()
libs += sysconfig.get_config_var('SYSLIBS').split()
2017-01-10 07:25:44 +03:00
fw = sysconfig.get_config_var('PYTHONFRAMEWORK')
if fw:
2017-01-10 10:36:48 +03:00
for var in 'data include stdlib'.split():
val = sysconfig.get_path(var)
2017-01-10 07:25:44 +03:00
if val and '/{}.framework'.format(fw) in val:
fdir = val[:val.index('/{}.framework'.format(fw))]
2017-02-09 21:34:05 +03:00
if os.path.isdir(
os.path.join(fdir, '{}.framework'.format(fw))
):
framework_dir = fdir
2017-01-10 10:23:23 +03:00
break
else:
raise SystemExit('Failed to find Python framework')
2017-02-09 21:34:05 +03:00
libs.append(
os.path.join(framework_dir, sysconfig.get_config_var('LDLIBRARY'))
)
else:
2017-01-10 07:19:53 +03:00
libs += ['-L' + sysconfig.get_config_var('LIBDIR')]
2017-02-09 21:34:05 +03:00
libs += [
'-lpython' + sysconfig.get_config_var('VERSION') + sys.abiflags
]
libs += sysconfig.get_config_var('LINKFORSHARED').split()
return libs
def get_sanitize_args(cc, ccver):
2017-11-01 12:05:57 +03:00
sanitize_args = ['-fsanitize=address']
2017-11-08 15:20:32 +03:00
if ccver >= (5, 0):
2017-11-01 12:05:57 +03:00
sanitize_args.append('-fsanitize=undefined')
# if cc == 'gcc' or (cc == 'clang' and ccver >= (4, 2)):
2017-11-01 12:05:57 +03:00
# sanitize_args.append('-fno-sanitize-recover=all')
sanitize_args.append('-fno-omit-frame-pointer')
return sanitize_args
2017-11-19 10:24:02 +03:00
class Env:
def __init__(self, cc, cflags, ldflags, ldpaths=[]):
self.cc, self.cflags, self.ldflags, self.ldpaths = cc, cflags, ldflags, ldpaths
def copy(self):
return Env(self.cc, list(self.cflags), list(self.ldflags), list(self.ldflags))
2017-11-05 06:52:15 +03:00
def init_env(
debug=False, sanitize=False, native_optimizations=True, profile=False
):
native_optimizations = native_optimizations and not sanitize and not debug
cc, ccver = cc_version()
print('CC:', cc, ccver)
stack_protector = '-fstack-protector'
if ccver >= (4, 9) and cc == 'gcc':
stack_protector += '-strong'
missing_braces = ''
if ccver < (5, 2) and cc == 'gcc':
missing_braces = '-Wno-missing-braces'
2017-11-08 13:43:21 +03:00
df = '-g3'
if ccver >= (5, 0):
df += ' -Og'
optimize = df if debug or sanitize else '-O3'
sanitize_args = get_sanitize_args(cc, ccver) if sanitize else set()
2017-02-09 21:34:05 +03:00
cflags = os.environ.get(
'OVERRIDE_CFLAGS', (
'-Wextra -Wno-missing-field-initializers -Wall -std=c99 -D_XOPEN_SOURCE=700'
2017-06-07 08:49:53 +03:00
' -pedantic-errors -Werror {} {} -D{}DEBUG -fwrapv {} {} -pipe {} -fvisibility=hidden'
2017-02-21 14:05:25 +03:00
).format(
2017-11-05 06:52:15 +03:00
optimize,
' '.join(sanitize_args),
('' if debug else 'N'),
stack_protector,
missing_braces,
'-march=native' if native_optimizations else '',
2017-02-21 14:05:25 +03:00
)
2017-02-09 21:34:05 +03:00
)
2017-11-05 06:52:15 +03:00
cflags = shlex.split(cflags) + shlex.split(
sysconfig.get_config_var('CCSHARED')
)
2017-02-09 21:34:05 +03:00
ldflags = os.environ.get(
2017-11-05 06:52:15 +03:00
'OVERRIDE_LDFLAGS',
'-Wall ' + ' '.join(sanitize_args) + ('' if debug else ' -O3')
2017-02-09 21:34:05 +03:00
)
ldflags = shlex.split(ldflags)
2017-11-19 20:54:36 +03:00
ldflags.append('-shared')
cflags += shlex.split(os.environ.get('CFLAGS', ''))
ldflags += shlex.split(os.environ.get('LDFLAGS', ''))
2017-11-01 12:05:57 +03:00
if not debug and not sanitize:
# See https://github.com/google/sanitizers/issues/647
2017-10-05 18:48:21 +03:00
cflags.append('-flto'), ldflags.append('-flto')
if profile:
cflags.append('-DWITH_PROFILER')
2017-11-01 12:05:57 +03:00
cflags.append('-g3')
ldflags.append('-lprofiler')
2017-11-19 10:24:02 +03:00
return Env(cc, cflags, ldflags)
def kitty_env():
ans = env.copy()
2017-11-19 20:54:36 +03:00
cflags = ans.cflags
cflags.append('-pthread')
# We add 4000 to the primary version because vim turns on SGR mouse mode
# automatically if this version is high enough
cflags.append('-DPRIMARY_VERSION={}'.format(version[0] + 4000))
cflags.append('-DSECONDARY_VERSION={}'.format(version[1]))
at_least_version('harfbuzz', 1, 5)
cflags.extend(pkg_config('libpng', '--cflags-only-I'))
if isosx:
font_libs = ['-framework', 'CoreText', '-framework', 'CoreGraphics']
cflags.extend(pkg_config('freetype2', '--cflags-only-I'))
font_libs += pkg_config('freetype2', '--libs')
else:
cflags.extend(pkg_config('fontconfig', '--cflags-only-I'))
font_libs = pkg_config('fontconfig', '--libs')
2017-10-30 19:54:01 +03:00
cflags.extend(pkg_config('harfbuzz', '--cflags-only-I'))
font_libs.extend(pkg_config('harfbuzz', '--libs'))
pylib = get_python_flags(cflags)
2017-11-20 08:11:19 +03:00
gl_libs = ['-framework', 'OpenGL'] if isosx else pkg_config('gl', '--libs')
libpng = pkg_config('libpng', '--libs')
2017-11-20 08:11:19 +03:00
ans.ldpaths += pylib + font_libs + gl_libs + libpng + [
2017-11-05 06:52:15 +03:00
'-lunistring'
]
2017-11-20 11:39:13 +03:00
if isosx:
ans.ldpaths.extend('-framework Cocoa'.split())
if is_travis and 'SW' in os.environ:
cflags.append('-I{}/include'.format(os.environ['SW']))
ans.ldpaths.append('-L{}/lib'.format(os.environ['SW']))
2017-11-20 11:39:13 +03:00
else:
2017-11-19 10:24:02 +03:00
ans.ldpaths += ['-lrt']
if '-ldl' not in ans.ldpaths:
ans.ldpaths.append('-ldl')
if '-lz' not in ans.ldpaths:
ans.ldpaths.append('-lz')
try:
os.mkdir(build_dir)
except FileExistsError:
pass
2017-11-19 10:24:02 +03:00
return ans
def define(x):
return '-D' + x
2017-11-19 09:08:35 +03:00
def run_tool(cmd, desc=None):
if isinstance(cmd, str):
cmd = shlex.split(cmd[0])
2017-11-20 12:56:27 +03:00
if verbose:
desc = None
2017-11-19 09:08:35 +03:00
print(desc or ' '.join(cmd))
p = subprocess.Popen(cmd)
ret = p.wait()
if ret != 0:
2017-11-19 20:54:36 +03:00
if desc:
print(' '.join(cmd))
raise SystemExit(ret)
SPECIAL_SOURCES = {
'kitty/parser_dump.c': ('kitty/parser.c', ['DUMP_COMMANDS']),
}
def newer(dest, *sources):
try:
dtime = os.path.getmtime(dest)
except EnvironmentError:
return True
for s in sources:
if os.path.getmtime(s) >= dtime:
return True
return False
def dependecies_for(src, obj, all_headers):
dep_file = obj.rpartition('.')[0] + '.d'
try:
2017-09-30 09:55:03 +03:00
deps = open(dep_file).read()
except FileNotFoundError:
yield src
yield from iter(all_headers)
else:
2017-11-05 06:52:15 +03:00
RE_INC = re.compile(
r'^(?P<target>.+?):\s+(?P<deps>.+?)$', re.MULTILINE
)
2017-09-30 09:55:03 +03:00
SPACE_TOK = '\x1B'
text = deps.replace('\\\n', ' ').replace('\\ ', SPACE_TOK)
for match in RE_INC.finditer(text):
2017-11-05 06:52:15 +03:00
files = (
f.replace(SPACE_TOK, ' ') for f in match.group('deps').split()
)
2017-09-30 09:55:03 +03:00
for path in files:
path = os.path.abspath(path)
if path.startswith(base):
yield path
2017-11-19 09:08:35 +03:00
def emphasis(text):
if sys.stdout.isatty():
text = '\033[32m' + text + '\033[39m'
return text
def parallel_run(todo, desc='Compiling {} ...'):
try:
from multiprocessing import cpu_count
num_workers = max(1, cpu_count())
except Exception:
num_workers = 2
2017-11-19 09:08:35 +03:00
items = list(todo.items())
workers = {}
failed = None
def wait():
nonlocal failed
if not workers:
return
pid, s = os.wait()
name, cmd, w = workers.pop(pid, (None, None, None))
if name is not None and ((s & 0xff) != 0 or ((s >> 8) & 0xff) != 0) and failed is None:
failed = name, cmd
while items and failed is None:
while len(workers) < num_workers and items:
name, cmd = items.pop()
2017-11-20 12:56:27 +03:00
if verbose:
print(' '.join(cmd))
else:
print(desc.format(emphasis(name)))
2017-11-19 09:08:35 +03:00
w = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
workers[w.pid] = name, cmd, w
wait()
while len(workers):
wait()
if failed:
run_tool(failed[1])
2017-11-19 20:54:36 +03:00
def compile_c_extension(kenv, module, incremental, compilation_database, sources, headers):
prefix = os.path.basename(module)
2017-02-09 21:34:05 +03:00
objects = [
os.path.join(build_dir, prefix + '-' + os.path.basename(src) + '.o')
for src in sources
]
2017-11-19 09:08:35 +03:00
todo = {}
for original_src, dest in zip(sources, objects):
src = original_src
2017-11-19 10:24:02 +03:00
cflgs = kenv.cflags[:]
is_special = src in SPECIAL_SOURCES
if is_special:
src, defines = SPECIAL_SOURCES[src]
cflgs.extend(map(define, defines))
2017-11-19 10:24:02 +03:00
cmd = [kenv.cc, '-MMD'] + cflgs
cmd_changed = compilation_database.get(original_src, [])[:-4] != cmd
must_compile = not incremental or cmd_changed
src = os.path.join(base, src)
if must_compile or newer(
2017-11-05 06:52:15 +03:00
dest, *dependecies_for(src, dest, headers)
):
cmd += ['-c', src] + ['-o', dest]
compilation_database[original_src] = cmd
2017-11-19 09:08:35 +03:00
todo[original_src] = cmd
if todo:
parallel_run(todo)
dest = os.path.join(base, module + '.so')
if not incremental or newer(dest, *objects):
2017-11-19 10:24:02 +03:00
run_tool([kenv.cc] + kenv.ldflags + objects + kenv.ldpaths + ['-o', dest], desc='Linking {} ...'.format(emphasis(module)))
def option_parser():
p = argparse.ArgumentParser()
2017-02-09 21:34:05 +03:00
p.add_argument(
'action',
nargs='?',
default='build',
2017-10-17 11:05:54 +03:00
choices='build test linux-package osx-bundle clean'.split(),
2017-02-09 21:34:05 +03:00
help='Action to perform (default is build)'
)
p.add_argument(
'--debug',
default=False,
action='store_true',
help='Build extension modules with debugging symbols'
)
2017-11-20 12:56:27 +03:00
p.add_argument(
'-v', '--verbose',
default=0,
action='count',
help='Be verbose'
)
2017-02-09 21:34:05 +03:00
p.add_argument(
'--sanitize',
2017-02-09 21:34:05 +03:00
default=False,
action='store_true',
help='Turn on sanitization to detect memory access errors and undefined behavior. Note that if you do turn it on,'
' a special executable will be built for running the test suite. If you want to run normal kitty'
' with sanitization, use LD_PRELOAD=libasan.so (for gcc) and'
' LD_PRELOAD=/usr/lib/clang/4.0.0/lib/linux/libclang_rt.asan-x86_64.so (for clang, changing path as appropriate).'
2017-02-09 21:34:05 +03:00
)
p.add_argument(
'--prefix',
default='./linux-package',
help='Where to create the linux package'
)
p.add_argument(
2017-11-14 07:17:08 +03:00
'--full',
dest='incremental',
default=True,
action='store_false',
help='Do a full build, even for unchanged files'
2017-02-09 21:34:05 +03:00
)
p.add_argument(
'--profile',
default=False,
action='store_true',
help='Use the -pg compile flag to add profiling information'
)
return p
2016-11-10 05:30:06 +03:00
2016-11-11 19:41:40 +03:00
def find_c_files():
ans, headers = [], []
2016-11-11 19:41:40 +03:00
d = os.path.join(base, 'kitty')
exclude = {'fontconfig.c', 'desktop.c'} if isosx else {'core_text.m', 'cocoa_window.m'}
2016-11-11 19:41:40 +03:00
for x in os.listdir(d):
ext = os.path.splitext(x)[1]
if ext in ('.c', '.m') and os.path.basename(x) not in exclude:
2016-11-13 07:57:24 +03:00
ans.append(os.path.join('kitty', x))
elif ext == '.h':
headers.append(os.path.join('kitty', x))
2017-02-09 21:34:05 +03:00
ans.sort(
key=lambda x: os.path.getmtime(os.path.join(base, x)), reverse=True
)
2016-11-13 07:57:24 +03:00
ans.append('kitty/parser_dump.c')
return tuple(ans), tuple(headers)
2016-11-11 19:41:40 +03:00
2017-11-19 20:54:36 +03:00
def compile_glfw(incremental, compilation_database):
modules = 'cocoa' if isosx else 'x11'
for module in modules.split():
genv = glfw.init_env(env, pkg_config, at_least_version, module)
sources = [os.path.join('glfw', x) for x in genv.sources]
all_headers = [os.path.join('glfw', x) for x in genv.all_headers]
compile_c_extension(genv, 'kitty/glfw-' + module, incremental, compilation_database, sources, all_headers)
2017-02-21 14:05:25 +03:00
def build(args, native_optimizations=True):
2017-11-19 10:24:02 +03:00
global env
try:
with open('compile_commands.json') as f:
compilation_database = json.load(f)
except FileNotFoundError:
compilation_database = []
compilation_database = {
k['file']: k['arguments'] for k in compilation_database
}
2017-11-19 10:24:02 +03:00
env = init_env(args.debug, args.sanitize, native_optimizations, args.profile)
2017-02-09 21:34:05 +03:00
compile_c_extension(
2017-11-19 20:54:36 +03:00
kitty_env(), 'kitty/fast_data_types', args.incremental, compilation_database, *find_c_files()
2017-02-09 21:34:05 +03:00
)
2017-11-20 12:56:27 +03:00
compile_glfw(args.incremental, compilation_database)
compilation_database = [
{'file': k, 'arguments': v, 'directory': base} for k, v in compilation_database.items()
]
with open('compile_commands.json', 'w') as f:
json.dump(compilation_database, f, indent=2, sort_keys=True)
def safe_makedirs(path):
os.makedirs(path, exist_ok=True)
def build_asan_launcher(args):
dest = 'asan-launcher'
src = 'asan-launcher.c'
if args.incremental and not newer(dest, src):
return
cc, ccver = cc_version()
2017-11-01 12:05:57 +03:00
cflags = '-g3 -Wall -Werror -fpie -std=c99'.split()
pylib = get_python_flags(cflags)
2017-11-01 09:42:15 +03:00
sanitize_lib = ['-lasan'] if cc == 'gcc' and not isosx else []
cflags.extend(get_sanitize_args(cc, ccver))
cmd = [cc] + cflags + [src, '-o', dest] + sanitize_lib + pylib
2017-11-19 20:54:36 +03:00
run_tool(cmd, desc='Creating {} ...'.format(emphasis('asan-launcher')))
def build_linux_launcher(args, launcher_dir='.', for_bundle=False):
cflags = '-Wall -Werror -fpie'.split()
libs = []
if args.profile:
cflags.append('-DWITH_PROFILER'), cflags.append('-g')
libs.append('-lprofiler')
else:
cflags.append('-O3')
if for_bundle:
cflags.append('-DFOR_BUNDLE')
cflags.append('-DPYVER="{}"'.format(sysconfig.get_python_version()))
pylib = get_python_flags(cflags)
exe = 'kitty-profile' if args.profile else 'kitty'
2017-11-19 10:24:02 +03:00
cmd = [env.cc] + cflags + [
2017-11-05 06:52:15 +03:00
'linux-launcher.c', '-o',
os.path.join(launcher_dir, exe)
] + libs + pylib
run_tool(cmd)
def package(args, for_bundle=False): # {{{
ddir = args.prefix
libdir = os.path.join(ddir, 'lib', 'kitty')
if os.path.exists(libdir):
shutil.rmtree(libdir)
2017-01-18 18:22:06 +03:00
os.makedirs(os.path.join(libdir, 'logo'))
for x in (libdir, os.path.join(ddir, 'share')):
odir = os.path.join(x, 'terminfo')
safe_makedirs(odir)
subprocess.check_call(['tic', '-o' + odir, 'terminfo/kitty.terminfo'])
shutil.copy2('__main__.py', libdir)
2017-01-18 18:22:06 +03:00
shutil.copy2('logo/kitty.rgba', os.path.join(libdir, 'logo'))
def src_ignore(parent, entries):
2017-02-09 21:34:05 +03:00
return [
x for x in entries
2017-11-05 06:52:15 +03:00
if '.' in x and x.rpartition('.')[2] not in
('py', 'so', 'conf', 'glsl')
2017-02-09 21:34:05 +03:00
]
shutil.copytree('kitty', os.path.join(libdir, 'kitty'), ignore=src_ignore)
import compileall
compileall.compile_dir(ddir, quiet=1, workers=4)
for root, dirs, files in os.walk(ddir):
for f in files:
path = os.path.join(root, f)
os.chmod(path, 0o755 if f.endswith('.so') else 0o644)
launcher_dir = os.path.join(ddir, 'bin')
safe_makedirs(launcher_dir)
build_linux_launcher(args, launcher_dir, for_bundle)
if not isosx: # {{{ linux desktop gunk
icdir = os.path.join(ddir, 'share', 'icons', 'hicolor', '256x256', 'apps')
safe_makedirs(icdir)
shutil.copy2('logo/kitty.png', icdir)
deskdir = os.path.join(ddir, 'share', 'applications')
safe_makedirs(deskdir)
with open(os.path.join(deskdir, 'kitty.desktop'), 'w') as f:
2017-02-09 21:34:05 +03:00
f.write(
'''\
[Desktop Entry]
Version=1.0
Type=Application
Name=kitty
GenericName=Terminal emulator
Comment=A modern, hackable, featureful, OpenGL based terminal emulator
TryExec=kitty
Exec=kitty
Icon=kitty
Categories=System;
2017-02-09 21:34:05 +03:00
'''
)
# }}}
if for_bundle: # OS X bundle gunk {{{
os.chdir(ddir)
os.mkdir('Contents')
os.chdir('Contents')
os.rename('../share', 'Resources')
os.rename('../bin', 'MacOS')
os.rename('../lib', 'Frameworks')
# }}}
2017-02-09 21:34:05 +03:00
# }}}
2017-10-17 11:05:54 +03:00
def clean():
2017-11-05 06:52:15 +03:00
for f in subprocess.check_output(
'git ls-files --others --ignored --exclude-from=.gitignore'.split()
).decode('utf-8').splitlines():
2017-10-17 11:05:54 +03:00
if f.startswith('logo/kitty.iconset') or f.startswith('dev/'):
continue
os.unlink(f)
if os.sep in f and not os.listdir(os.path.dirname(f)):
os.rmdir(os.path.dirname(f))
def main():
2017-11-20 12:56:27 +03:00
global verbose
if sys.version_info < (3, 5):
raise SystemExit('python >= 3.5 required')
args = option_parser().parse_args()
2017-11-20 12:56:27 +03:00
verbose = args.verbose > 0
args.prefix = os.path.abspath(args.prefix)
os.chdir(os.path.dirname(os.path.abspath(__file__)))
if args.action == 'build':
build(args)
build_asan_launcher(args)
if args.profile:
build_linux_launcher(args)
print('kitty profile executable is', 'kitty-profile')
elif args.action == 'test':
2017-02-09 21:34:05 +03:00
os.execlp(
sys.executable, sys.executable, os.path.join(base, 'test.py')
)
elif args.action == 'linux-package':
2017-02-21 14:05:25 +03:00
build(args, native_optimizations=False)
package(args)
elif args.action == 'osx-bundle':
2017-02-21 14:05:25 +03:00
build(args, native_optimizations=False)
package(args, for_bundle=True)
2017-10-17 11:05:54 +03:00
elif args.action == 'clean':
clean()
if __name__ == '__main__':
main()