All defs are now typed

This commit is contained in:
Kovid Goyal
2020-03-14 14:37:11 +05:30
parent d3f37eeba4
commit 5956277863
5 changed files with 272 additions and 191 deletions

232
setup.py
View File

@@ -14,20 +14,20 @@ import subprocess
import sys
import sysconfig
import time
from collections import namedtuple
from contextlib import suppress
from functools import partial
from pathlib import Path
from typing import (
Callable, Dict, Iterator, List, NamedTuple, Optional, Sequence, Tuple,
Union
Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional,
Sequence, Set, Tuple, Union
)
from glfw import glfw # noqa
if sys.version_info[:2] < (3, 6):
raise SystemExit('kitty requires python >= 3.6')
base = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, base)
from glfw import glfw # noqa
del sys.path[0]
verbose = False
@@ -46,25 +46,28 @@ version = tuple(
)
_plat = sys.platform.lower()
is_macos = 'darwin' in _plat
class Env:
def __init__(
self, cc: str = '', cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
ldpaths: Optional[List[str]] = None, ccver: Optional[Tuple[int, int]] = None
):
self.cc, self.cppflags, self.cflags, self.ldflags, self.ldpaths = cc, cppflags, cflags, ldflags, [] if ldpaths is None else ldpaths
self.ccver = ccver
def copy(self):
return Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), list(self.ldpaths), self.ccver)
Env = glfw.Env
env = Env()
PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config')
CompileKey = namedtuple('CompileKey', 'src dest')
class Options(argparse.Namespace):
action: str = 'build'
debug: bool = False
verbose: int = 0
sanitize: bool = False
prefix: str = './linux-package'
incremental: bool = True
profile: bool = False
for_freeze: bool = False
libdir_name: str = 'lib'
extra_logging: List[str] = []
update_check_interval: float = 24
class CompileKey(NamedTuple):
src: str
dest: str
class Command(NamedTuple):
@@ -76,19 +79,19 @@ class Command(NamedTuple):
keyfile: Optional[str]
def emphasis(text):
def emphasis(text: str) -> str:
if sys.stdout.isatty():
text = '\033[32m' + text + '\033[39m'
return text
def error(text):
def error(text: str) -> str:
if sys.stdout.isatty():
text = '\033[91m' + text + '\033[39m'
return text
def pkg_config(pkg, *args):
def pkg_config(pkg: str, *args: str) -> List[str]:
try:
return list(
filter(
@@ -103,7 +106,7 @@ def pkg_config(pkg, *args):
raise SystemExit('The package {} was not found on your system'.format(error(pkg)))
def at_least_version(package, major, minor=0):
def at_least_version(package: str, major: int, minor: int = 0) -> None:
q = '{}.{}'.format(major, minor)
if subprocess.run([PKGCONFIG, package, '--atleast-version=' + q]
).returncode != 0:
@@ -146,15 +149,19 @@ def cc_version() -> Tuple[str, Tuple[int, int]]:
return cc, ver
def get_python_include_paths():
def get_python_include_paths() -> List[str]:
ans = []
for name in sysconfig.get_path_names():
if 'include' in name:
ans.append(name)
return sorted(frozenset(map(sysconfig.get_path, sorted(ans))))
def gp(x: str) -> Optional[str]:
return sysconfig.get_path(x)
return sorted(frozenset(filter(None, map(gp, sorted(ans)))))
def get_python_flags(cflags):
def get_python_flags(cflags: List[str]) -> List[str]:
cflags.extend('-I' + x for x in get_python_include_paths())
libs: List[str] = []
libs += (sysconfig.get_config_var('LIBS') or '').split()
@@ -186,7 +193,7 @@ def get_python_flags(cflags):
return libs
def get_sanitize_args(cc, ccver):
def get_sanitize_args(cc: str, ccver: Tuple[int, int]) -> List[str]:
sanitize_args = ['-fsanitize=address']
if ccver >= (5, 0):
sanitize_args.append('-fsanitize=undefined')
@@ -209,7 +216,7 @@ def test_compile(cc: str, *cflags: str, src: Optional[str] = None) -> bool:
return p.wait() == 0
def first_successful_compile(cc, *cflags, src=None):
def first_successful_compile(cc: str, *cflags: str, src: Optional[str] = None) -> str:
for x in cflags:
if test_compile(cc, *shlex.split(x), src=src):
return x
@@ -217,9 +224,12 @@ def first_successful_compile(cc, *cflags, src=None):
def init_env(
debug=False, sanitize=False, native_optimizations=True, profile=False,
extra_logging=()
):
debug: bool = False,
sanitize: bool = False,
native_optimizations: bool = True,
profile: bool = False,
extra_logging: Iterable[str] = ()
) -> Env:
native_optimizations = native_optimizations and not sanitize and not debug
cc, ccver = cc_version()
print('CC:', cc, ccver)
@@ -279,7 +289,7 @@ def init_env(
return Env(cc, cppflags, cflags, ldflags, ccver=ccver)
def kitty_env():
def kitty_env() -> Env:
ans = env.copy()
cflags = ans.cflags
cflags.append('-pthread')
@@ -318,11 +328,11 @@ def kitty_env():
return ans
def define(x):
def define(x: str) -> str:
return '-D' + x
def run_tool(cmd, desc=None):
def run_tool(cmd: Union[str, List[str]], desc: Optional[str] = None) -> None:
if isinstance(cmd, str):
cmd = shlex.split(cmd[0])
if verbose:
@@ -336,7 +346,7 @@ def run_tool(cmd, desc=None):
raise SystemExit(ret)
def get_vcs_rev_defines():
def get_vcs_rev_defines() -> List[str]:
ans = []
if os.path.exists('.git'):
try:
@@ -355,13 +365,13 @@ def get_vcs_rev_defines():
return ans
SPECIAL_SOURCES: Dict[str, Tuple[str, Union[List[str], Callable[[], Iterator[str]]]]] = {
SPECIAL_SOURCES: Dict[str, Tuple[str, Union[List[str], Callable[[], Union[List[str], Iterator[str]]]]]] = {
'kitty/parser_dump.c': ('kitty/parser.c', ['DUMP_COMMANDS']),
'kitty/data-types.c': ('kitty/data-types.c', get_vcs_rev_defines),
}
def newer(dest, *sources):
def newer(dest: str, *sources: str) -> bool:
try:
dtime = os.path.getmtime(dest)
except OSError:
@@ -373,7 +383,7 @@ def newer(dest, *sources):
return False
def dependecies_for(src, obj, all_headers):
def dependecies_for(src: str, obj: str, all_headers: Iterable[str]) -> Iterable[str]:
dep_file = obj.rpartition('.')[0] + '.d'
try:
with open(dep_file) as f:
@@ -398,7 +408,7 @@ def dependecies_for(src, obj, all_headers):
yield path
def parallel_run(items):
def parallel_run(items: List[Command]) -> None:
try:
num_workers = max(2, os.cpu_count() or 1)
except Exception:
@@ -408,7 +418,7 @@ def parallel_run(items):
failed = None
num, total = 0, len(items)
def wait():
def wait() -> None:
nonlocal failed
if not workers:
return
@@ -441,22 +451,33 @@ def parallel_run(items):
print(' done')
if failed:
print(failed.desc)
run_tool(failed.cmd)
run_tool(list(failed.cmd))
class CompilationDatabase:
def __init__(self, incremental):
def __init__(self, incremental: bool):
self.incremental = incremental
self.compile_commands = []
self.link_commands = []
self.compile_commands: List[Command] = []
self.link_commands: List[Command] = []
def add_command(
self,
desc: str,
cmd: List[str],
is_newer_func: Callable,
key: Optional[CompileKey] = None,
on_success: Optional[Callable] = None,
keyfile: Optional[str] = None
) -> None:
def no_op() -> None:
pass
def add_command(self, desc, cmd, is_newer_func, key=None, on_success=None, keyfile=None):
queue = self.link_commands if keyfile is None else self.compile_commands
queue.append(Command(desc, cmd, is_newer_func, on_success, key, keyfile))
queue.append(Command(desc, cmd, is_newer_func, on_success or no_op, key, keyfile))
def build_all(self):
def sort_key(compile_cmd):
def build_all(self) -> None:
def sort_key(compile_cmd: Command) -> int:
if compile_cmd.keyfile:
return os.path.getsize(compile_cmd.keyfile)
return 0
@@ -474,12 +495,12 @@ class CompilationDatabase:
items.append(compile_cmd)
parallel_run(items)
def cmd_changed(self, compile_cmd):
def cmd_changed(self, compile_cmd: Command) -> bool:
key, cmd = compile_cmd.key, compile_cmd.cmd
return self.db.get(key) != cmd
return bool(self.db.get(key) != cmd)
def __enter__(self):
self.all_keys = set()
def __enter__(self) -> 'CompilationDatabase':
self.all_keys: Set[CompileKey] = set()
self.dbpath = os.path.abspath('compile_commands.json')
self.linkdbpath = os.path.join(os.path.dirname(self.dbpath), 'link_commands.json')
try:
@@ -499,12 +520,12 @@ class CompilationDatabase:
self.linkdb = {k['output']: k['arguments'] for k in link_database}
return self
def __exit__(self, *a):
def __exit__(self, *a: object) -> None:
cdb = self.db
for key in set(cdb) - self.all_keys:
del cdb[key]
compilation_database = [
{'file': c.key.src, 'arguments': c.cmd, 'directory': base, 'output': c.key.dest} for c in self.compile_commands
{'file': c.key.src, 'arguments': c.cmd, 'directory': base, 'output': c.key.dest} for c in self.compile_commands if c.key is not None
]
with open(self.dbpath, 'w') as f:
json.dump(compilation_database, f, indent=2, sort_keys=True)
@@ -512,7 +533,14 @@ class CompilationDatabase:
json.dump([{'output': c.key, 'arguments': c.cmd, 'directory': base} for c in self.link_commands], f, indent=2, sort_keys=True)
def compile_c_extension(kenv, module, compilation_database, sources, headers, desc_prefix=''):
def compile_c_extension(
kenv: Env,
module: str,
compilation_database: CompilationDatabase,
sources: List[str],
headers: List[str],
desc_prefix: str = ''
) -> None:
prefix = os.path.basename(module)
objects = [
os.path.join(build_dir, prefix + '-' + os.path.basename(src) + '.o')
@@ -547,13 +575,13 @@ def compile_c_extension(kenv, module, compilation_database, sources, headers, de
linker_cflags = list(filter(lambda x: x not in unsafe, kenv.cflags))
cmd = [kenv.cc] + linker_cflags + kenv.ldflags + objects + kenv.ldpaths + ['-o', dest]
def on_success():
def on_success() -> None:
os.rename(dest, real_dest)
compilation_database.add_command(desc, cmd, partial(newer, real_dest, *objects), on_success=on_success, key=module + '.so')
compilation_database.add_command(desc, cmd, partial(newer, real_dest, *objects), on_success=on_success, key=CompileKey('', module + '.so'))
def find_c_files():
def find_c_files() -> Tuple[List[str], List[str]]:
ans, headers = [], []
d = 'kitty'
exclude = {'fontconfig.c', 'freetype.c', 'desktop.c'} if is_macos else {'core_text.m', 'cocoa_window.m', 'macos_process_info.c'}
@@ -564,10 +592,10 @@ def find_c_files():
elif ext == '.h':
headers.append(os.path.join('kitty', x))
ans.append('kitty/parser_dump.c')
return tuple(ans), tuple(headers)
return ans, headers
def compile_glfw(compilation_database):
def compile_glfw(compilation_database: CompilationDatabase) -> None:
modules = 'cocoa' if is_macos else 'x11 wayland'
for module in modules.split():
try:
@@ -592,7 +620,7 @@ def compile_glfw(compilation_database):
sources, all_headers, desc_prefix='[{}] '.format(module))
def kittens_env():
def kittens_env() -> Env:
kenv = env.copy()
cflags = kenv.cflags
cflags.append('-pthread')
@@ -602,10 +630,10 @@ def kittens_env():
return kenv
def compile_kittens(compilation_database):
def compile_kittens(compilation_database: CompilationDatabase) -> None:
kenv = kittens_env()
def list_files(q):
def list_files(q: str) -> List[str]:
return sorted(glob.glob(q))
def files(
@@ -632,21 +660,22 @@ def compile_kittens(compilation_database):
kenv, dest, compilation_database, sources, all_headers + ['kitty/data-types.h'])
def build(args, native_optimizations=True):
def build(args: Options, native_optimizations: bool = True) -> None:
global env
env = init_env(args.debug, args.sanitize, native_optimizations, args.profile, args.extra_logging)
sources, headers = find_c_files()
compile_c_extension(
kitty_env(), 'kitty/fast_data_types', args.compilation_database, *find_c_files()
kitty_env(), 'kitty/fast_data_types', args.compilation_database, sources, headers
)
compile_glfw(args.compilation_database)
compile_kittens(args.compilation_database)
def safe_makedirs(path):
def safe_makedirs(path: str) -> None:
os.makedirs(path, exist_ok=True)
def build_launcher(args, launcher_dir='.', bundle_type='source'):
def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 'source') -> None:
cflags = '-Wall -Werror -fpie'.split()
cppflags = []
libs: List[str] = []
@@ -696,7 +725,7 @@ def build_launcher(args, launcher_dir='.', bundle_type='source'):
# Packaging {{{
def copy_man_pages(ddir):
def copy_man_pages(ddir: str) -> None:
mandir = os.path.join(ddir, 'share', 'man')
safe_makedirs(mandir)
with suppress(FileNotFoundError):
@@ -711,7 +740,7 @@ make && make docs
shutil.copytree(src, os.path.join(mandir, 'man1'))
def copy_html_docs(ddir):
def copy_html_docs(ddir: str) -> None:
htmldir = os.path.join(ddir, 'share', 'doc', appname, 'html')
safe_makedirs(os.path.dirname(htmldir))
with suppress(FileNotFoundError):
@@ -726,7 +755,7 @@ make && make docs
shutil.copytree(src, htmldir)
def compile_python(base_path):
def compile_python(base_path: str) -> None:
import compileall
import py_compile
try:
@@ -738,18 +767,18 @@ def compile_python(base_path):
if f.rpartition('.')[-1] in ('pyc', 'pyo'):
os.remove(os.path.join(root, f))
def c(base_path: str, **kw) -> None:
def c(base_path: str, **kw: object) -> None:
try:
kw['invalidation_mode'] = py_compile.PycInvalidationMode.UNCHECKED_HASH
except AttributeError:
pass
compileall.compile_dir(base_path, **kw)
compileall.compile_dir(base_path, **kw) # type: ignore
for optimize in (0, 1, 2):
c(base_path, ddir='', force=True, optimize=optimize, quiet=1, workers=num_workers)
def create_linux_bundle_gunk(ddir, libdir_name):
def create_linux_bundle_gunk(ddir: str, libdir_name: str) -> None:
if not os.path.exists('docs/_build/html'):
run_tool(['make', 'docs'])
copy_man_pages(ddir)
@@ -774,16 +803,17 @@ Icon=kitty
Categories=System;TerminalEmulator;
'''
)
ddir = Path(ddir)
in_src_launcher = ddir / (libdir_name + '/kitty/kitty/launcher/kitty')
launcher = ddir / 'bin/kitty'
base = Path(ddir)
in_src_launcher = base / (libdir_name + '/kitty/kitty/launcher/kitty')
launcher = base / 'bin/kitty'
if os.path.exists(in_src_launcher):
os.remove(in_src_launcher)
os.makedirs(os.path.dirname(in_src_launcher), exist_ok=True)
os.symlink(os.path.relpath(launcher, os.path.dirname(in_src_launcher)), in_src_launcher)
def macos_info_plist():
def macos_info_plist() -> bytes:
import plistlib
VERSION = '.'.join(map(str, version))
pl = dict(
@@ -827,7 +857,7 @@ def macos_info_plist():
return plistlib.dumps(pl)
def create_macos_app_icon(where='Resources'):
def create_macos_app_icon(where: str = 'Resources') -> None:
logo_dir = os.path.abspath(os.path.join('logo', appname + '.iconset'))
subprocess.check_call([
'iconutil', '-c', 'icns', logo_dir, '-o',
@@ -835,7 +865,7 @@ def create_macos_app_icon(where='Resources'):
])
def create_minimal_macos_bundle(args, where):
def create_minimal_macos_bundle(args: Options, where: str) -> None:
if os.path.exists(where):
shutil.rmtree(where)
bin_dir = os.path.join(where, 'kitty.app/Contents/MacOS')
@@ -851,8 +881,8 @@ def create_minimal_macos_bundle(args, where):
create_macos_app_icon(resources_dir)
def create_macos_bundle_gunk(ddir):
ddir = Path(ddir)
def create_macos_bundle_gunk(dest: str) -> None:
ddir = Path(dest)
os.mkdir(ddir / 'Contents')
with open(ddir / 'Contents/Info.plist', 'wb') as fp:
fp.write(macos_info_plist())
@@ -869,7 +899,7 @@ def create_macos_bundle_gunk(ddir):
create_macos_app_icon(os.path.join(ddir, 'Contents', 'Resources'))
def package(args, bundle_type):
def package(args: Options, bundle_type: str) -> None:
ddir = args.prefix
if bundle_type == 'linux-freeze':
args.libdir_name = 'lib'
@@ -891,7 +921,7 @@ def package(args, bundle_type):
shutil.copy2('logo/beam-cursor.png', os.path.join(libdir, 'logo'))
shutil.copy2('logo/beam-cursor@2x.png', os.path.join(libdir, 'logo'))
def src_ignore(parent, entries):
def src_ignore(parent: str, entries: Iterable[str]) -> List[str]:
return [
x for x in entries
if '.' in x and x.rpartition('.')[2] not in
@@ -920,9 +950,9 @@ def package(args, bundle_type):
# }}}
def clean():
def clean() -> None:
def safe_remove(*entries):
def safe_remove(*entries: str) -> None:
for x in entries:
if os.path.exists(x):
if os.path.isdir(x):
@@ -949,66 +979,66 @@ def clean():
os.unlink(x)
def option_parser(): # {{{
def option_parser() -> argparse.ArgumentParser: # {{{
p = argparse.ArgumentParser()
p.add_argument(
'action',
nargs='?',
default='build',
default=Options.action,
choices='build test linux-package kitty.app linux-freeze macos-freeze clean'.split(),
help='Action to perform (default is build)'
)
p.add_argument(
'--debug',
default=False,
default=Options.debug,
action='store_true',
help='Build extension modules with debugging symbols'
)
p.add_argument(
'-v', '--verbose',
default=0,
default=Options.verbose,
action='count',
help='Be verbose'
)
p.add_argument(
'--sanitize',
default=False,
default=Options.sanitize,
action='store_true',
help='Turn on sanitization to detect memory access errors and undefined behavior. This is a big performance hit.'
)
p.add_argument(
'--prefix',
default='./linux-package',
default=Options.prefix,
help='Where to create the linux package'
)
p.add_argument(
'--full',
dest='incremental',
default=True,
default=Options.incremental,
action='store_false',
help='Do a full build, even for unchanged files'
)
p.add_argument(
'--profile',
default=False,
default=Options.profile,
action='store_true',
help='Use the -pg compile flag to add profiling information'
)
p.add_argument(
'--for-freeze',
default=False,
default=Options.for_freeze,
action='store_true',
help='Internal use'
)
p.add_argument(
'--libdir-name',
default='lib',
default=Options.libdir_name,
help='The name of the directory inside --prefix in which to store compiled files. Defaults to "lib"'
)
p.add_argument(
'--extra-logging',
action='append',
default=[],
default=Options.extra_logging,
choices=('event-loop',),
help='Turn on extra logging for debugging in this build. Can be specified multiple times, to turn'
' on different types of logging.'
@@ -1016,7 +1046,7 @@ def option_parser(): # {{{
p.add_argument(
'--update-check-interval',
type=float,
default=24,
default=Options.update_check_interval,
help='When building a package, the default value for the update_check_interval setting will'
' be set to this number. Use zero to disable update checking.'
)
@@ -1024,11 +1054,11 @@ def option_parser(): # {{{
# }}}
def main():
def main() -> None:
global verbose
if sys.version_info < (3, 5):
raise SystemExit('python >= 3.5 required')
args = option_parser().parse_args()
args = option_parser().parse_args(namespace=Options())
verbose = args.verbose > 0
args.prefix = os.path.abspath(args.prefix)
os.chdir(base)