1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
4
5import argparse
6import glob
7import json
8import os
9import re
10import runpy
11import shlex
12import shutil
13import subprocess
14import sys
15import sysconfig
16import platform
17import time
18from contextlib import suppress
19from functools import partial
20from pathlib import Path
21from typing import (
22    Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional,
23    Sequence, Set, Tuple, Union
24)
25
26from glfw import glfw  # noqa
27
28if sys.version_info[:2] < (3, 6):
29    raise SystemExit('kitty requires python >= 3.6')
30base = os.path.dirname(os.path.abspath(__file__))
31sys.path.insert(0, base)
32del sys.path[0]
33
34verbose = False
35build_dir = 'build'
36constants = os.path.join('kitty', 'constants.py')
37with open(constants, 'rb') as f:
38    constants = f.read().decode('utf-8')
39appname = re.search(r"^appname: str = '([^']+)'", constants, re.MULTILINE).group(1)  # type: ignore
40version = tuple(
41    map(
42        int,
43        re.search(  # type: ignore
44            r"^version: Version = Version\((\d+), (\d+), (\d+)\)", constants, re.MULTILINE
45        ).group(1, 2, 3)
46    )
47)
48_plat = sys.platform.lower()
49is_macos = 'darwin' in _plat
50is_openbsd = 'openbsd' in _plat
51is_freebsd = 'freebsd' in _plat
52is_netbsd = 'netbsd' in _plat
53is_dragonflybsd = 'dragonfly' in _plat
54is_bsd = is_freebsd or is_netbsd or is_dragonflybsd or is_openbsd
55is_arm = platform.processor() == 'arm' or platform.machine() == 'arm64'
56Env = glfw.Env
57env = Env()
58PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config')
59
60
61class Options(argparse.Namespace):
62    action: str = 'build'
63    debug: bool = False
64    verbose: int = 0
65    sanitize: bool = False
66    prefix: str = './linux-package'
67    incremental: bool = True
68    profile: bool = False
69    libdir_name: str = 'lib'
70    extra_logging: List[str] = []
71    extra_include_dirs: List[str] = []
72    link_time_optimization: bool = 'KITTY_NO_LTO' not in os.environ
73    update_check_interval: float = 24
74    egl_library: Optional[str] = os.getenv('KITTY_EGL_LIBRARY')
75    startup_notification_library: Optional[str] = os.getenv('KITTY_STARTUP_NOTIFICATION_LIBRARY')
76    canberra_library: Optional[str] = os.getenv('KITTY_CANBERRA_LIBRARY')
77
78
79class CompileKey(NamedTuple):
80    src: str
81    dest: str
82
83
84class Command(NamedTuple):
85    desc: str
86    cmd: Sequence[str]
87    is_newer_func: Callable[[], bool]
88    on_success: Callable[[], None]
89    key: Optional[CompileKey]
90    keyfile: Optional[str]
91
92
93def emphasis(text: str) -> str:
94    if sys.stdout.isatty():
95        text = '\033[32m' + text + '\033[39m'
96    return text
97
98
99def error(text: str) -> str:
100    if sys.stdout.isatty():
101        text = '\033[91m' + text + '\033[39m'
102    return text
103
104
105def pkg_config(pkg: str, *args: str) -> List[str]:
106    try:
107        return list(
108            filter(
109                None,
110                shlex.split(
111                    subprocess.check_output([PKGCONFIG, pkg] + list(args))
112                    .decode('utf-8')
113                )
114            )
115        )
116    except subprocess.CalledProcessError:
117        raise SystemExit('The package {} was not found on your system'.format(error(pkg)))
118
119
120def pkg_version(package: str) -> Optional[Tuple[int, int]]:
121    ver = subprocess.check_output([
122        PKGCONFIG, package, '--modversion']).decode('utf-8').strip()
123    m = re.match(r'(\d+).(\d+)', ver)
124    if m is not None:
125        qmajor, qminor = map(int, m.groups())
126        return qmajor, qminor
127    return None
128
129
130def at_least_version(package: str, major: int, minor: int = 0) -> None:
131    q = '{}.{}'.format(major, minor)
132    if subprocess.run([PKGCONFIG, package, '--atleast-version=' + q]
133                      ).returncode != 0:
134        qmajor = qminor = 0
135        try:
136            ver = subprocess.check_output([PKGCONFIG, package, '--modversion']
137                                          ).decode('utf-8').strip()
138            m = re.match(r'(\d+).(\d+)', ver)
139            if m is not None:
140                qmajor, qminor = map(int, m.groups())
141        except Exception:
142            ver = 'not found'
143        if qmajor < major or (qmajor == major and qminor < minor):
144            raise SystemExit(
145                '{} >= {}.{} is required, found version: {}'.format(
146                    error(package), major, minor, ver
147                )
148            )
149
150
151def cc_version() -> Tuple[str, Tuple[int, int]]:
152    if 'CC' in os.environ:
153        cc = os.environ['CC']
154    else:
155        if is_macos:
156            cc = 'clang'
157        else:
158            if shutil.which('gcc'):
159                cc = 'gcc'
160            elif shutil.which('clang'):
161                cc = 'clang'
162            else:
163                cc = 'cc'
164    raw = subprocess.check_output([cc, '-dumpversion']).decode('utf-8')
165    ver_ = raw.strip().split('.')[:2]
166    try:
167        if len(ver_) == 1:
168            ver = int(ver_[0]), 0
169        else:
170            ver = int(ver_[0]), int(ver_[1])
171    except Exception:
172        ver = (0, 0)
173    return cc, ver
174
175
176def get_python_include_paths() -> List[str]:
177    ans = []
178    for name in sysconfig.get_path_names():
179        if 'include' in name:
180            ans.append(name)
181
182    def gp(x: str) -> Optional[str]:
183        return sysconfig.get_path(x)
184
185    return sorted(frozenset(filter(None, map(gp, sorted(ans)))))
186
187
188def get_python_flags(cflags: List[str]) -> List[str]:
189    cflags.extend('-I' + x for x in get_python_include_paths())
190    libs: List[str] = []
191    libs += (sysconfig.get_config_var('LIBS') or '').split()
192    libs += (sysconfig.get_config_var('SYSLIBS') or '').split()
193    fw = sysconfig.get_config_var('PYTHONFRAMEWORK')
194    if fw:
195        for var in 'data include stdlib'.split():
196            val = sysconfig.get_path(var)
197            if val and '/{}.framework'.format(fw) in val:
198                fdir = val[:val.index('/{}.framework'.format(fw))]
199                if os.path.isdir(
200                    os.path.join(fdir, '{}.framework'.format(fw))
201                ):
202                    framework_dir = fdir
203                    break
204        else:
205            raise SystemExit('Failed to find Python framework')
206        ldlib = sysconfig.get_config_var('LDLIBRARY')
207        if ldlib:
208            libs.append(os.path.join(framework_dir, ldlib))
209    else:
210        ldlib = sysconfig.get_config_var('LIBDIR')
211        if ldlib:
212            libs += ['-L' + ldlib]
213        ldlib = sysconfig.get_config_var('VERSION')
214        if ldlib:
215            libs += ['-lpython' + ldlib + sys.abiflags]
216        libs += (sysconfig.get_config_var('LINKFORSHARED') or '').split()
217    return libs
218
219
220def get_sanitize_args(cc: str, ccver: Tuple[int, int]) -> List[str]:
221    sanitize_args = ['-fsanitize=address']
222    if ccver >= (5, 0):
223        sanitize_args.append('-fsanitize=undefined')
224        # if cc == 'gcc' or (cc == 'clang' and ccver >= (4, 2)):
225        #     sanitize_args.append('-fno-sanitize-recover=all')
226    sanitize_args.append('-fno-omit-frame-pointer')
227    return sanitize_args
228
229
230def test_compile(cc: str, *cflags: str, src: Optional[str] = None, lang: str = 'c') -> bool:
231    src = src or 'int main(void) { return 0; }'
232    p = subprocess.Popen(
233        [cc] + list(cflags) + ['-x', lang, '-o', os.devnull, '-'],
234        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.PIPE,
235    )
236    stdin = p.stdin
237    assert stdin is not None
238    try:
239        stdin.write(src.encode('utf-8'))
240        stdin.close()
241    except BrokenPipeError:
242        return False
243    return p.wait() == 0
244
245
246def first_successful_compile(cc: str, *cflags: str, src: Optional[str] = None, lang: str = 'c') -> str:
247    for x in cflags:
248        if test_compile(cc, *shlex.split(x), src=src, lang=lang):
249            return x
250    return ''
251
252
253def set_arches(flags: List[str], arches: Iterable[str] = ('x86_64', 'arm64')) -> None:
254    while True:
255        try:
256            idx = flags.index('-arch')
257        except ValueError:
258            break
259        del flags[idx]
260        del flags[idx]
261    for arch in arches:
262        flags.extend(('-arch', arch))
263
264
265def init_env(
266    debug: bool = False,
267    sanitize: bool = False,
268    native_optimizations: bool = True,
269    link_time_optimization: bool = True,
270    profile: bool = False,
271    egl_library: Optional[str] = None,
272    startup_notification_library: Optional[str] = None,
273    canberra_library: Optional[str] = None,
274    extra_logging: Iterable[str] = (),
275    extra_include_dirs: Iterable[str] = (),
276    ignore_compiler_warnings: bool = False,
277    build_universal_binary: bool = False
278) -> Env:
279    native_optimizations = native_optimizations and not sanitize and not debug
280    if native_optimizations and is_macos and is_arm:
281        # see https://github.com/kovidgoyal/kitty/issues/3126
282        # -march=native is not supported when targeting Apple Silicon
283        native_optimizations = False
284    cc, ccver = cc_version()
285    print('CC:', cc, ccver)
286    stack_protector = first_successful_compile(cc, '-fstack-protector-strong', '-fstack-protector')
287    missing_braces = ''
288    if ccver < (5, 2) and cc == 'gcc':
289        missing_braces = '-Wno-missing-braces'
290    df = '-g3'
291    float_conversion = ''
292    if ccver >= (5, 0):
293        df += ' -Og'
294        float_conversion = '-Wfloat-conversion'
295    fortify_source = '' if sanitize and is_macos else '-D_FORTIFY_SOURCE=2'
296    optimize = df if debug or sanitize else '-O3'
297    sanitize_args = get_sanitize_args(cc, ccver) if sanitize else set()
298    cppflags_ = os.environ.get(
299        'OVERRIDE_CPPFLAGS', '-D{}DEBUG'.format('' if debug else 'N'),
300    )
301    cppflags = shlex.split(cppflags_)
302    for el in extra_logging:
303        cppflags.append('-DDEBUG_{}'.format(el.upper().replace('-', '_')))
304    werror = '' if ignore_compiler_warnings else '-pedantic-errors -Werror'
305    std = '' if is_openbsd else '-std=c11'
306    sanitize_flag = ' '.join(sanitize_args)
307    march = '-march=native' if native_optimizations else ''
308    cflags_ = os.environ.get(
309        'OVERRIDE_CFLAGS', (
310            f'-Wextra {float_conversion} -Wno-missing-field-initializers -Wall -Wstrict-prototypes {std}'
311            f' {werror} {optimize} {sanitize_flag} -fwrapv {stack_protector} {missing_braces}'
312            f' -pipe {march} -fvisibility=hidden {fortify_source}'
313        )
314    )
315    cflags = shlex.split(cflags_) + shlex.split(
316        sysconfig.get_config_var('CCSHARED') or ''
317    )
318    ldflags_ = os.environ.get(
319        'OVERRIDE_LDFLAGS',
320        '-Wall ' + ' '.join(sanitize_args) + ('' if debug else ' -O3')
321    )
322    ldflags = shlex.split(ldflags_)
323    ldflags.append('-shared')
324    cppflags += shlex.split(os.environ.get('CPPFLAGS', ''))
325    cflags += shlex.split(os.environ.get('CFLAGS', ''))
326    ldflags += shlex.split(os.environ.get('LDFLAGS', ''))
327    if not debug and not sanitize and not is_openbsd and link_time_optimization:
328        # See https://github.com/google/sanitizers/issues/647
329        cflags.append('-flto')
330        ldflags.append('-flto')
331
332    if debug:
333        cflags.append('-DKITTY_DEBUG_BUILD')
334
335    if profile:
336        cppflags.append('-DWITH_PROFILER')
337        cflags.append('-g3')
338        ldflags.append('-lprofiler')
339
340    library_paths = {}
341
342    if egl_library is not None:
343        assert('"' not in egl_library)
344        library_paths['glfw/egl_context.c'] = ['_GLFW_EGL_LIBRARY="' + egl_library + '"']
345
346    desktop_libs = []
347    if startup_notification_library is not None:
348        assert('"' not in startup_notification_library)
349        desktop_libs = ['_KITTY_STARTUP_NOTIFICATION_LIBRARY="' + startup_notification_library + '"']
350
351    if canberra_library is not None:
352        assert('"' not in canberra_library)
353        desktop_libs += ['_KITTY_CANBERRA_LIBRARY="' + canberra_library + '"']
354
355    if desktop_libs != []:
356        library_paths['kitty/desktop.c'] = desktop_libs
357
358    for path in extra_include_dirs:
359        cflags.append(f'-I{path}')
360
361    if build_universal_binary:
362        set_arches(cflags)
363        set_arches(ldflags)
364
365    return Env(cc, cppflags, cflags, ldflags, library_paths, ccver=ccver)
366
367
368def kitty_env() -> Env:
369    ans = env.copy()
370    cflags = ans.cflags
371    cflags.append('-pthread')
372    # We add 4000 to the primary version because vim turns on SGR mouse mode
373    # automatically if this version is high enough
374    cppflags = ans.cppflags
375    cppflags.append('-DPRIMARY_VERSION={}'.format(version[0] + 4000))
376    cppflags.append('-DSECONDARY_VERSION={}'.format(version[1]))
377    cppflags.append('-DXT_VERSION="{}"'.format('.'.join(map(str, version))))
378    at_least_version('harfbuzz', 1, 5)
379    cflags.extend(pkg_config('libpng', '--cflags-only-I'))
380    cflags.extend(pkg_config('lcms2', '--cflags-only-I'))
381    if is_macos:
382        platform_libs = [
383            '-framework', 'CoreText', '-framework', 'CoreGraphics',
384        ]
385        test_program_src = '''#include <UserNotifications/UserNotifications.h>
386        int main(void) { return 0; }\n'''
387        user_notifications_framework = first_successful_compile(
388            ans.cc, '-framework UserNotifications', src=test_program_src, lang='objective-c')
389        if user_notifications_framework:
390            platform_libs.extend(shlex.split(user_notifications_framework))
391        else:
392            cppflags.append('-DKITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API')
393        # Apple deprecated OpenGL in Mojave (10.14) silence the endless
394        # warnings about it
395        cppflags.append('-DGL_SILENCE_DEPRECATION')
396    else:
397        cflags.extend(pkg_config('fontconfig', '--cflags-only-I'))
398        platform_libs = pkg_config('fontconfig', '--libs')
399    cflags.extend(pkg_config('harfbuzz', '--cflags-only-I'))
400    platform_libs.extend(pkg_config('harfbuzz', '--libs'))
401    pylib = get_python_flags(cflags)
402    gl_libs = ['-framework', 'OpenGL'] if is_macos else pkg_config('gl', '--libs')
403    libpng = pkg_config('libpng', '--libs')
404    lcms2 = pkg_config('lcms2', '--libs')
405    ans.ldpaths += pylib + platform_libs + gl_libs + libpng + lcms2
406    if is_macos:
407        ans.ldpaths.extend('-framework Cocoa'.split())
408    elif not is_openbsd:
409        ans.ldpaths += ['-lrt']
410        if '-ldl' not in ans.ldpaths:
411            ans.ldpaths.append('-ldl')
412    if '-lz' not in ans.ldpaths:
413        ans.ldpaths.append('-lz')
414
415    with suppress(FileExistsError):
416        os.mkdir(build_dir)
417    return ans
418
419
420def define(x: str) -> str:
421    return '-D' + x
422
423
424def run_tool(cmd: Union[str, List[str]], desc: Optional[str] = None) -> None:
425    if isinstance(cmd, str):
426        cmd = shlex.split(cmd[0])
427    if verbose:
428        desc = None
429    print(desc or ' '.join(cmd))
430    p = subprocess.Popen(cmd)
431    ret = p.wait()
432    if ret != 0:
433        if desc:
434            print(' '.join(cmd))
435        raise SystemExit(ret)
436
437
438def get_vcs_rev_defines(env: Env, src: str) -> List[str]:
439    ans = []
440    if os.path.exists('.git'):
441        try:
442            rev = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8')
443        except FileNotFoundError:
444            try:
445                with open('.git/refs/heads/master') as f:
446                    rev = f.read()
447            except NotADirectoryError:
448                with open('.git') as f:
449                    gitloc = f.read()
450                with open(os.path.join(gitloc, 'refs/heads/master')) as f:
451                    rev = f.read()
452
453        ans.append('KITTY_VCS_REV="{}"'.format(rev.strip()))
454    return ans
455
456
457def get_library_defines(env: Env, src: str) -> Optional[List[str]]:
458    try:
459        return env.library_paths[src]
460    except KeyError:
461        return None
462
463
464SPECIAL_SOURCES: Dict[str, Tuple[str, Union[List[str], Callable[[Env, str], Union[Optional[List[str]], Iterator[str]]]]]] = {
465    'glfw/egl_context.c': ('glfw/egl_context.c', get_library_defines),
466    'kitty/desktop.c': ('kitty/desktop.c', get_library_defines),
467    'kitty/parser_dump.c': ('kitty/parser.c', ['DUMP_COMMANDS']),
468    'kitty/data-types.c': ('kitty/data-types.c', get_vcs_rev_defines),
469}
470
471
472def newer(dest: str, *sources: str) -> bool:
473    try:
474        dtime = os.path.getmtime(dest)
475    except OSError:
476        return True
477    for s in sources:
478        with suppress(FileNotFoundError):
479            if os.path.getmtime(s) >= dtime:
480                return True
481    return False
482
483
484def dependecies_for(src: str, obj: str, all_headers: Iterable[str]) -> Iterable[str]:
485    dep_file = obj.rpartition('.')[0] + '.d'
486    try:
487        with open(dep_file) as f:
488            deps = f.read()
489    except FileNotFoundError:
490        yield src
491        yield from iter(all_headers)
492    else:
493        RE_INC = re.compile(
494            r'^(?P<target>.+?):\s+(?P<deps>.+?)$', re.MULTILINE
495        )
496        SPACE_TOK = '\x1B'
497
498        text = deps.replace('\\\n', ' ').replace('\\ ', SPACE_TOK)
499        for match in RE_INC.finditer(text):
500            files = (
501                f.replace(SPACE_TOK, ' ') for f in match.group('deps').split()
502            )
503            for path in files:
504                path = os.path.abspath(path)
505                if path.startswith(base):
506                    yield path
507
508
509def parallel_run(items: List[Command]) -> None:
510    try:
511        num_workers = max(2, os.cpu_count() or 1)
512    except Exception:
513        num_workers = 2
514    items = list(reversed(items))
515    workers: Dict[int, Tuple[Optional[Command], Optional[subprocess.Popen]]] = {}
516    failed = None
517    num, total = 0, len(items)
518
519    def wait() -> None:
520        nonlocal failed
521        if not workers:
522            return
523        pid, s = os.wait()
524        compile_cmd, w = workers.pop(pid, (None, None))
525        if compile_cmd is None:
526            return
527        if ((s & 0xff) != 0 or ((s >> 8) & 0xff) != 0):
528            if failed is None:
529                failed = compile_cmd
530        elif compile_cmd.on_success is not None:
531            compile_cmd.on_success()
532
533    printed = False
534    isatty = sys.stdout.isatty()
535    while items and failed is None:
536        while len(workers) < num_workers and items:
537            compile_cmd = items.pop()
538            num += 1
539            if verbose:
540                print(' '.join(compile_cmd.cmd))
541            elif isatty:
542                print('\r\x1b[K[{}/{}] {}'.format(num, total, compile_cmd.desc), end='')
543            else:
544                print('[{}/{}] {}'.format(num, total, compile_cmd.desc), flush=True)
545            printed = True
546            w = subprocess.Popen(compile_cmd.cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
547            workers[w.pid] = compile_cmd, w
548        wait()
549    while len(workers):
550        wait()
551    if not verbose and printed:
552        print(' done')
553    if failed:
554        print(failed.desc)
555        run_tool(list(failed.cmd))
556
557
558class CompilationDatabase:
559
560    def __init__(self, incremental: bool):
561        self.incremental = incremental
562        self.compile_commands: List[Command] = []
563        self.link_commands: List[Command] = []
564
565    def add_command(
566        self,
567        desc: str,
568        cmd: List[str],
569        is_newer_func: Callable,
570        key: Optional[CompileKey] = None,
571        on_success: Optional[Callable] = None,
572        keyfile: Optional[str] = None
573    ) -> None:
574        def no_op() -> None:
575            pass
576
577        queue = self.link_commands if keyfile is None else self.compile_commands
578        queue.append(Command(desc, cmd, is_newer_func, on_success or no_op, key, keyfile))
579
580    def build_all(self) -> None:
581
582        def sort_key(compile_cmd: Command) -> int:
583            if compile_cmd.keyfile:
584                return os.path.getsize(compile_cmd.keyfile)
585            return 0
586
587        items = []
588        for compile_cmd in self.compile_commands:
589            if not self.incremental or self.cmd_changed(compile_cmd) or compile_cmd.is_newer_func():
590                items.append(compile_cmd)
591        items.sort(key=sort_key, reverse=True)
592        parallel_run(items)
593
594        items = []
595        for compile_cmd in self.link_commands:
596            if not self.incremental or compile_cmd.is_newer_func():
597                items.append(compile_cmd)
598        parallel_run(items)
599
600    def cmd_changed(self, compile_cmd: Command) -> bool:
601        key, cmd = compile_cmd.key, compile_cmd.cmd
602        return bool(self.db.get(key) != cmd)
603
604    def __enter__(self) -> 'CompilationDatabase':
605        self.all_keys: Set[CompileKey] = set()
606        self.dbpath = os.path.abspath('compile_commands.json')
607        self.linkdbpath = os.path.join(os.path.dirname(self.dbpath), 'link_commands.json')
608        try:
609            with open(self.dbpath) as f:
610                compilation_database = json.load(f)
611        except FileNotFoundError:
612            compilation_database = []
613        try:
614            with open(self.linkdbpath) as f:
615                link_database = json.load(f)
616        except FileNotFoundError:
617            link_database = []
618        compilation_database = {
619            CompileKey(k['file'], k['output']): k['arguments'] for k in compilation_database
620        }
621        self.db = compilation_database
622        self.linkdb = {tuple(k['output']): k['arguments'] for k in link_database}
623        return self
624
625    def __exit__(self, *a: object) -> None:
626        cdb = self.db
627        for key in set(cdb) - self.all_keys:
628            del cdb[key]
629        compilation_database = [
630            {'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
631        ]
632        with open(self.dbpath, 'w') as f:
633            json.dump(compilation_database, f, indent=2, sort_keys=True)
634        with open(self.linkdbpath, 'w') as f:
635            json.dump([{'output': c.key, 'arguments': c.cmd, 'directory': base} for c in self.link_commands], f, indent=2, sort_keys=True)
636
637
638def compile_c_extension(
639    kenv: Env,
640    module: str,
641    compilation_database: CompilationDatabase,
642    sources: List[str],
643    headers: List[str],
644    desc_prefix: str = ''
645) -> None:
646    prefix = os.path.basename(module)
647    objects = [
648        os.path.join(build_dir, prefix + '-' + os.path.basename(src) + '.o')
649        for src in sources
650    ]
651
652    for original_src, dest in zip(sources, objects):
653        src = original_src
654        cppflags = kenv.cppflags[:]
655        is_special = src in SPECIAL_SOURCES
656        if is_special:
657            src, defines_ = SPECIAL_SOURCES[src]
658            defines = defines_(kenv, src) if callable(defines_) else defines_
659            if defines is not None:
660                cppflags.extend(map(define, defines))
661
662        cmd = [kenv.cc, '-MMD'] + cppflags + kenv.cflags
663        cmd += ['-c', src] + ['-o', dest]
664        key = CompileKey(original_src, os.path.basename(dest))
665        desc = 'Compiling {} ...'.format(emphasis(desc_prefix + src))
666        compilation_database.add_command(desc, cmd, partial(newer, dest, *dependecies_for(src, dest, headers)), key=key, keyfile=src)
667    dest = os.path.join(build_dir, module + '.so')
668    real_dest = module + '.so'
669    os.makedirs(os.path.dirname(dest), exist_ok=True)
670    desc = 'Linking {} ...'.format(emphasis(desc_prefix + module))
671    # Old versions of clang don't like -pthread being passed to the linker
672    # Don't treat linker warnings as errors (linker generates spurious
673    # warnings on some old systems)
674    unsafe = {'-pthread', '-Werror', '-pedantic-errors'}
675    linker_cflags = list(filter(lambda x: x not in unsafe, kenv.cflags))
676    cmd = [kenv.cc] + linker_cflags + kenv.ldflags + objects + kenv.ldpaths + ['-o', dest]
677
678    def on_success() -> None:
679        os.rename(dest, real_dest)
680
681    compilation_database.add_command(desc, cmd, partial(newer, real_dest, *objects), on_success=on_success, key=CompileKey('', module + '.so'))
682
683
684def find_c_files() -> Tuple[List[str], List[str]]:
685    ans, headers = [], []
686    d = 'kitty'
687    exclude = {
688        'fontconfig.c', 'freetype.c', 'desktop.c', 'freetype_render_ui_text.c'
689    } if is_macos else {
690        'core_text.m', 'cocoa_window.m', 'macos_process_info.c'
691    }
692    for x in sorted(os.listdir(d)):
693        ext = os.path.splitext(x)[1]
694        if ext in ('.c', '.m') and os.path.basename(x) not in exclude:
695            ans.append(os.path.join('kitty', x))
696        elif ext == '.h':
697            headers.append(os.path.join('kitty', x))
698    ans.append('kitty/parser_dump.c')
699    return ans, headers
700
701
702def compile_glfw(compilation_database: CompilationDatabase) -> None:
703    modules = 'cocoa' if is_macos else 'x11 wayland'
704    for module in modules.split():
705        try:
706            genv = glfw.init_env(env, pkg_config, pkg_version, at_least_version, test_compile, module)
707        except SystemExit as err:
708            if module != 'wayland':
709                raise
710            print(err, file=sys.stderr)
711            print(error('Disabling building of wayland backend'), file=sys.stderr)
712            continue
713        sources = [os.path.join('glfw', x) for x in genv.sources]
714        all_headers = [os.path.join('glfw', x) for x in genv.all_headers]
715        if module == 'wayland':
716            try:
717                glfw.build_wayland_protocols(genv, Command, parallel_run, emphasis, newer, 'glfw')
718            except SystemExit as err:
719                print(err, file=sys.stderr)
720                print(error('Disabling building of wayland backend'), file=sys.stderr)
721                continue
722        compile_c_extension(
723            genv, 'kitty/glfw-' + module, compilation_database,
724            sources, all_headers, desc_prefix='[{}] '.format(module))
725
726
727def kittens_env() -> Env:
728    kenv = env.copy()
729    cflags = kenv.cflags
730    cflags.append('-pthread')
731    cflags.append('-Ikitty')
732    pylib = get_python_flags(cflags)
733    kenv.ldpaths += pylib
734    return kenv
735
736
737def compile_kittens(compilation_database: CompilationDatabase) -> None:
738    kenv = kittens_env()
739
740    def list_files(q: str) -> List[str]:
741        return sorted(glob.glob(q))
742
743    def files(
744            kitten: str,
745            output: str,
746            extra_headers: Sequence[str] = (),
747            extra_sources: Sequence[str] = (),
748            filter_sources: Optional[Callable[[str], bool]] = None
749    ) -> Tuple[List[str], List[str], str]:
750        sources = list(filter(filter_sources, list(extra_sources) + list_files(os.path.join('kittens', kitten, '*.c'))))
751        headers = list_files(os.path.join('kittens', kitten, '*.h')) + list(extra_headers)
752        return (sources, headers, 'kittens/{}/{}'.format(kitten, output))
753
754    for sources, all_headers, dest in (
755        files('unicode_input', 'unicode_names'),
756        files('diff', 'diff_speedup'),
757        files(
758            'choose', 'subseq_matcher',
759            extra_headers=('kitty/charsets.h',),
760            extra_sources=('kitty/charsets.c',),
761            filter_sources=lambda x: 'windows_compat.c' not in x),
762    ):
763        compile_c_extension(
764            kenv, dest, compilation_database, sources, all_headers + ['kitty/data-types.h'])
765
766
767def init_env_from_args(args: Options, native_optimizations: bool = False) -> None:
768    global env
769    env = init_env(
770        args.debug, args.sanitize, native_optimizations, args.link_time_optimization, args.profile,
771        args.egl_library, args.startup_notification_library, args.canberra_library,
772        args.extra_logging, args.extra_include_dirs, args.ignore_compiler_warnings,
773        args.build_universal_binary
774    )
775
776
777def build(args: Options, native_optimizations: bool = True, call_init: bool = True) -> None:
778    if call_init:
779        init_env_from_args(args, native_optimizations)
780    sources, headers = find_c_files()
781    compile_c_extension(
782        kitty_env(), 'kitty/fast_data_types', args.compilation_database, sources, headers
783    )
784    compile_glfw(args.compilation_database)
785    compile_kittens(args.compilation_database)
786
787
788def safe_makedirs(path: str) -> None:
789    os.makedirs(path, exist_ok=True)
790
791
792def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 'source') -> None:
793    werror = '' if args.ignore_compiler_warnings else '-pedantic-errors -Werror'
794    cflags = f'-Wall {werror} -fpie'.split()
795    if args.build_universal_binary:
796        cflags += '-arch x86_64 -arch arm64'.split()
797    cppflags = []
798    libs: List[str] = []
799    if args.profile or args.sanitize:
800        if args.sanitize:
801            cflags.append('-g3')
802            cflags.extend(get_sanitize_args(env.cc, env.ccver))
803            libs += ['-lasan'] if env.cc == 'gcc' and not is_macos else []
804        else:
805            cflags.append('-g')
806        if args.profile:
807            libs.append('-lprofiler')
808    else:
809        cflags.append('-O3')
810    if bundle_type.endswith('-freeze'):
811        cppflags.append('-DFOR_BUNDLE')
812        cppflags.append('-DPYVER="{}"'.format(sysconfig.get_python_version()))
813        cppflags.append('-DKITTY_LIB_DIR_NAME="{}"'.format(args.libdir_name))
814    elif bundle_type == 'source':
815        cppflags.append('-DFROM_SOURCE')
816    if bundle_type.startswith('macos-'):
817        klp = '../Resources/kitty'
818    elif bundle_type.startswith('linux-'):
819        klp = '../{}/kitty'.format(args.libdir_name.strip('/'))
820    elif bundle_type == 'source':
821        klp = os.path.relpath('.', launcher_dir)
822    else:
823        raise SystemExit('Unknown bundle type: {}'.format(bundle_type))
824    cppflags.append('-DKITTY_LIB_PATH="{}"'.format(klp))
825    pylib = get_python_flags(cflags)
826    cppflags += shlex.split(os.environ.get('CPPFLAGS', ''))
827    cflags += shlex.split(os.environ.get('CFLAGS', ''))
828    ldflags = shlex.split(os.environ.get('LDFLAGS', ''))
829    for path in args.extra_include_dirs:
830        cflags.append(f'-I{path}')
831    if bundle_type == 'linux-freeze':
832        ldflags += ['-Wl,-rpath,$ORIGIN/../lib']
833    os.makedirs(launcher_dir, exist_ok=True)
834    dest = os.path.join(launcher_dir, 'kitty')
835    src = 'launcher.c'
836    cmd = [env.cc] + cppflags + cflags + [
837           src, '-o', dest] + ldflags + libs + pylib
838    key = CompileKey('launcher.c', 'kitty')
839    desc = 'Building {}...'.format(emphasis('launcher'))
840    args.compilation_database.add_command(desc, cmd, partial(newer, dest, src), key=key, keyfile=src)
841    args.compilation_database.build_all()
842
843
844# Packaging {{{
845
846
847def copy_man_pages(ddir: str) -> None:
848    mandir = os.path.join(ddir, 'share', 'man')
849    safe_makedirs(mandir)
850    man_levels = '15'
851    with suppress(FileNotFoundError):
852        for x in man_levels:
853            shutil.rmtree(os.path.join(mandir, f'man{x}'))
854    src = 'docs/_build/man'
855    if not os.path.exists(src):
856        raise SystemExit('''\
857The kitty man pages are missing. If you are building from git then run:
858make && make docs
859(needs the sphinx documentation system to be installed)
860''')
861    for x in man_levels:
862        os.makedirs(os.path.join(mandir, f'man{x}'))
863        for y in glob.glob(os.path.join(src, f'*.{x}')):
864            shutil.copy2(y, os.path.join(mandir, f'man{x}'))
865
866
867def copy_html_docs(ddir: str) -> None:
868    htmldir = os.path.join(ddir, 'share', 'doc', appname, 'html')
869    safe_makedirs(os.path.dirname(htmldir))
870    with suppress(FileNotFoundError):
871        shutil.rmtree(htmldir)
872    src = 'docs/_build/html'
873    if not os.path.exists(src):
874        raise SystemExit('''\
875The kitty html docs are missing. If you are building from git then run:
876make && make docs
877(needs the sphinx documentation system to be installed)
878''')
879    shutil.copytree(src, htmldir)
880
881
882def compile_python(base_path: str) -> None:
883    import compileall
884    import py_compile
885    try:
886        num_workers = max(1, os.cpu_count() or 1)
887    except Exception:
888        num_workers = 1
889    for root, dirs, files in os.walk(base_path):
890        for f in files:
891            if f.rpartition('.')[-1] in ('pyc', 'pyo'):
892                os.remove(os.path.join(root, f))
893
894    def c(base_path: str, **kw: object) -> None:
895        try:
896            kw['invalidation_mode'] = py_compile.PycInvalidationMode.UNCHECKED_HASH
897        except AttributeError:
898            pass
899        compileall.compile_dir(base_path, **kw)  # type: ignore
900
901    for optimize in (0, 1, 2):
902        c(base_path, ddir='', force=True, optimize=optimize, quiet=1, workers=num_workers)
903
904
905def create_linux_bundle_gunk(ddir: str, libdir_name: str) -> None:
906    if not os.path.exists('docs/_build/html'):
907        make = "gmake" if (is_freebsd or is_dragonflybsd) else "make"
908        run_tool([make, 'docs'])
909    copy_man_pages(ddir)
910    copy_html_docs(ddir)
911    for (icdir, ext) in {'256x256': 'png', 'scalable': 'svg'}.items():
912        icdir = os.path.join(ddir, 'share', 'icons', 'hicolor', icdir, 'apps')
913        safe_makedirs(icdir)
914        shutil.copy2(f'logo/kitty.{ext}', icdir)
915    deskdir = os.path.join(ddir, 'share', 'applications')
916    safe_makedirs(deskdir)
917    with open(os.path.join(deskdir, 'kitty.desktop'), 'w') as f:
918        f.write(
919            '''\
920[Desktop Entry]
921Version=1.0
922Type=Application
923Name=kitty
924GenericName=Terminal emulator
925Comment=Fast, feature-rich, GPU based terminal
926TryExec=kitty
927Exec=kitty
928Icon=kitty
929Categories=System;TerminalEmulator;
930'''
931            )
932
933    base = Path(ddir)
934    in_src_launcher = base / (libdir_name + '/kitty/kitty/launcher/kitty')
935    launcher = base / 'bin/kitty'
936    if os.path.exists(in_src_launcher):
937        os.remove(in_src_launcher)
938    os.makedirs(os.path.dirname(in_src_launcher), exist_ok=True)
939    os.symlink(os.path.relpath(launcher, os.path.dirname(in_src_launcher)), in_src_launcher)
940
941
942def macos_info_plist() -> bytes:
943    import plistlib
944    VERSION = '.'.join(map(str, version))
945
946    def access(what: str, verb: str = 'would like to access') -> str:
947        return f'A program running inside kitty {verb} {what}'
948
949    docs = [
950        {
951            'CFBundleTypeName': 'Terminal scripts',
952            'CFBundleTypeExtensions': ['command', 'sh', 'zsh', 'bash', 'fish', 'tool'],
953            'CFBundleTypeIconFile': appname + '.icns',
954            'CFBundleTypeRole': 'Editor',
955        },
956        {
957            'CFBundleTypeName': 'Folders',
958            'CFBundleTypeOSTypes': ['fold'],
959            'CFBundleTypeRole': 'Editor',
960        },
961        {
962            'LSItemContentTypes': ['public.unix-executable'],
963            'CFBundleTypeRole': 'Shell',
964        },
965    ]
966
967    pl = dict(
968        # see https://github.com/kovidgoyal/kitty/issues/1233
969        CFBundleDevelopmentRegion='English',
970        CFBundleAllowMixedLocalizations=True,
971
972        CFBundleDisplayName=appname,
973        CFBundleName=appname,
974        CFBundleIdentifier='net.kovidgoyal.' + appname,
975        CFBundleVersion=VERSION,
976        CFBundleShortVersionString=VERSION,
977        CFBundlePackageType='APPL',
978        CFBundleSignature='????',
979        CFBundleExecutable=appname,
980        CFBundleDocumentTypes=docs,
981        LSMinimumSystemVersion='10.12.0',
982        LSRequiresNativeExecution=True,
983        NSAppleScriptEnabled=False,
984        # Needed for dark mode in Mojave when linking against older SDKs
985        NSRequiresAquaSystemAppearance='NO',
986        NSHumanReadableCopyright=time.strftime(
987            'Copyright %Y, Kovid Goyal'),
988        CFBundleGetInfoString='kitty, an OpenGL based terminal emulator https://sw.kovidgoyal.net/kitty/',
989        CFBundleIconFile=appname + '.icns',
990        NSHighResolutionCapable=True,
991        NSSupportsAutomaticGraphicsSwitching=True,
992        LSApplicationCategoryType='public.app-category.utilities',
993        LSEnvironment={'KITTY_LAUNCHED_BY_LAUNCH_SERVICES': '1'},
994        NSServices=[
995            {
996                'NSMenuItem': {'default': 'New ' + appname + ' Tab Here'},
997                'NSMessage': 'openTab',
998                'NSRequiredContext': {'NSTextContent': 'FilePath'},
999                'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
1000            },
1001            {
1002                'NSMenuItem': {'default': 'New ' + appname + ' Window Here'},
1003                'NSMessage': 'openOSWindow',
1004                'NSRequiredContext': {'NSTextContent': 'FilePath'},
1005                'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
1006            },
1007        ],
1008        NSAppleEventsUsageDescription=access('AppleScript.'),
1009        NSCalendarsUsageDescription=access('your calendar data.'),
1010        NSCameraUsageDescription=access('the camera.'),
1011        NSContactsUsageDescription=access('your contacts.'),
1012        NSLocationAlwaysUsageDescription=access('your location information, even in the background.'),
1013        NSLocationUsageDescription=access('your location information.'),
1014        NSLocationWhenInUseUsageDescription=access('your location while active.'),
1015        NSMicrophoneUsageDescription=access('your microphone.'),
1016        NSRemindersUsageDescription=access('your reminders.'),
1017        NSSystemAdministrationUsageDescription=access('elevated privileges.', 'requires'),
1018    )
1019    return plistlib.dumps(pl)
1020
1021
1022def create_macos_app_icon(where: str = 'Resources') -> None:
1023    iconset_dir = os.path.abspath(os.path.join('logo', appname + '.iconset'))
1024    icns_dir = os.path.join(where, appname + '.icns')
1025    try:
1026        subprocess.check_call([
1027            'iconutil', '-c', 'icns', iconset_dir, '-o', icns_dir
1028        ])
1029    except FileNotFoundError:
1030        print(error('iconutil not found') + ', using png2icns (without retina support) to convert the logo', file=sys.stderr)
1031        subprocess.check_call([
1032            'png2icns', icns_dir
1033        ] + [os.path.join(iconset_dir, logo) for logo in [
1034            # png2icns does not support retina icons, so only pass the non-retina icons
1035            'icon_16x16.png',
1036            'icon_32x32.png',
1037            'icon_128x128.png',
1038            'icon_256x256.png',
1039            'icon_512x512.png',
1040        ]])
1041
1042
1043def create_minimal_macos_bundle(args: Options, where: str) -> None:
1044    if os.path.exists(where):
1045        shutil.rmtree(where)
1046    bin_dir = os.path.join(where, 'kitty.app/Contents/MacOS')
1047    resources_dir = os.path.join(where, 'kitty.app/Contents/Resources')
1048    os.makedirs(resources_dir)
1049    os.makedirs(bin_dir)
1050    with open(os.path.join(where, 'kitty.app/Contents/Info.plist'), 'wb') as f:
1051        f.write(macos_info_plist())
1052    build_launcher(args, bin_dir)
1053    os.symlink(
1054        os.path.join(os.path.relpath(bin_dir, where), appname),
1055        os.path.join(where, appname))
1056    create_macos_app_icon(resources_dir)
1057
1058
1059def create_macos_bundle_gunk(dest: str) -> None:
1060    ddir = Path(dest)
1061    os.mkdir(ddir / 'Contents')
1062    with open(ddir / 'Contents/Info.plist', 'wb') as fp:
1063        fp.write(macos_info_plist())
1064    os.rename(ddir / 'share', ddir / 'Contents/Resources')
1065    os.rename(ddir / 'bin', ddir / 'Contents/MacOS')
1066    os.rename(ddir / 'lib', ddir / 'Contents/Frameworks')
1067    os.rename(ddir / 'Contents/Frameworks/kitty', ddir / 'Contents/Resources/kitty')
1068    launcher = ddir / 'Contents/MacOS/kitty'
1069    in_src_launcher = ddir / 'Contents/Resources/kitty/kitty/launcher/kitty'
1070    if os.path.exists(in_src_launcher):
1071        os.remove(in_src_launcher)
1072    os.makedirs(os.path.dirname(in_src_launcher), exist_ok=True)
1073    os.symlink(os.path.relpath(launcher, os.path.dirname(in_src_launcher)), in_src_launcher)
1074    create_macos_app_icon(os.path.join(ddir, 'Contents', 'Resources'))
1075
1076
1077def package(args: Options, bundle_type: str) -> None:
1078    ddir = args.prefix
1079    for_freeze = bundle_type.endswith('-freeze')
1080    if bundle_type == 'linux-freeze':
1081        args.libdir_name = 'lib'
1082    libdir = os.path.join(ddir, args.libdir_name.strip('/'), 'kitty')
1083    if os.path.exists(libdir):
1084        shutil.rmtree(libdir)
1085    launcher_dir = os.path.join(ddir, 'bin')
1086    safe_makedirs(launcher_dir)
1087    if for_freeze:  # freeze launcher is built separately
1088        args.compilation_database.build_all()
1089    else:
1090        build_launcher(args, launcher_dir, bundle_type)
1091    os.makedirs(os.path.join(libdir, 'logo'))
1092    build_terminfo = runpy.run_path('build-terminfo', run_name='import_build')
1093    for x in (libdir, os.path.join(ddir, 'share')):
1094        odir = os.path.join(x, 'terminfo')
1095        safe_makedirs(odir)
1096        build_terminfo['compile_terminfo'](odir)
1097    shutil.copy2('__main__.py', libdir)
1098    shutil.copy2('logo/kitty-128.png', os.path.join(libdir, 'logo'))
1099    shutil.copy2('logo/kitty.png', os.path.join(libdir, 'logo'))
1100    shutil.copy2('logo/beam-cursor.png', os.path.join(libdir, 'logo'))
1101    shutil.copy2('logo/beam-cursor@2x.png', os.path.join(libdir, 'logo'))
1102    allowed_extensions = frozenset('py glsl so'.split())
1103
1104    def src_ignore(parent: str, entries: Iterable[str]) -> List[str]:
1105        return [
1106            x for x in entries
1107            if '.' in x and x.rpartition('.')[2] not in
1108            allowed_extensions
1109        ]
1110
1111    shutil.copytree('kitty', os.path.join(libdir, 'kitty'), ignore=src_ignore)
1112    shutil.copytree('kittens', os.path.join(libdir, 'kittens'), ignore=src_ignore)
1113    if for_freeze:
1114        shutil.copytree('kitty_tests', os.path.join(libdir, 'kitty_tests'))
1115    if args.update_check_interval != 24.0:
1116        with open(os.path.join(libdir, 'kitty/options/types.py'), 'r+', encoding='utf-8') as f:
1117            raw = f.read()
1118            nraw = raw.replace('update_check_interval: float = 24.0', f'update_check_interval: float = {args.update_check_interval!r}', 1)
1119            if nraw == raw:
1120                raise SystemExit('Failed to change the value of update_check_interval')
1121            f.seek(0), f.truncate(), f.write(nraw)
1122    compile_python(libdir)
1123    for root, dirs, files in os.walk(libdir):
1124        for f_ in files:
1125            path = os.path.join(root, f_)
1126            os.chmod(path, 0o755 if f_.endswith('.so') else 0o644)
1127    if not is_macos:
1128        create_linux_bundle_gunk(ddir, args.libdir_name)
1129
1130    if bundle_type.startswith('macos-'):
1131        create_macos_bundle_gunk(ddir)
1132# }}}
1133
1134
1135def clean() -> None:
1136
1137    def safe_remove(*entries: str) -> None:
1138        for x in entries:
1139            if os.path.exists(x):
1140                if os.path.isdir(x):
1141                    shutil.rmtree(x)
1142                else:
1143                    os.unlink(x)
1144
1145    safe_remove(
1146        'build', 'compile_commands.json', 'link_commands.json',
1147        'linux-package', 'kitty.app', 'asan-launcher',
1148        'kitty-profile', 'kitty/launcher')
1149
1150    def excluded(root: str, d: str) -> bool:
1151        q = os.path.relpath(os.path.join(root, d), base).replace(os.sep, '/')
1152        return q in ('.git', 'bypy/b')
1153
1154    for root, dirs, files in os.walk(base, topdown=True):
1155        dirs[:] = [d for d in dirs if not excluded(root, d)]
1156        remove_dirs = {d for d in dirs if d == '__pycache__' or d.endswith('.dSYM')}
1157        for d in remove_dirs:
1158            shutil.rmtree(os.path.join(root, d))
1159            dirs.remove(d)
1160        for f in files:
1161            ext = f.rpartition('.')[-1]
1162            if ext in ('so', 'dylib', 'pyc', 'pyo'):
1163                os.unlink(os.path.join(root, f))
1164    for x in glob.glob('glfw/wayland-*-protocol.[ch]'):
1165        os.unlink(x)
1166
1167
1168def option_parser() -> argparse.ArgumentParser:  # {{{
1169    p = argparse.ArgumentParser()
1170    p.add_argument(
1171        'action',
1172        nargs='?',
1173        default=Options.action,
1174        choices='build test linux-package kitty.app linux-freeze macos-freeze build-launcher build-frozen-launcher clean export-ci-bundles'.split(),
1175        help='Action to perform (default is build)'
1176    )
1177    p.add_argument(
1178        '--debug',
1179        default=Options.debug,
1180        action='store_true',
1181        help='Build extension modules with debugging symbols'
1182    )
1183    p.add_argument(
1184        '-v', '--verbose',
1185        default=Options.verbose,
1186        action='count',
1187        help='Be verbose'
1188    )
1189    p.add_argument(
1190        '--sanitize',
1191        default=Options.sanitize,
1192        action='store_true',
1193        help='Turn on sanitization to detect memory access errors and undefined behavior. This is a big performance hit.'
1194    )
1195    p.add_argument(
1196        '--prefix',
1197        default=Options.prefix,
1198        help='Where to create the linux package'
1199    )
1200    p.add_argument(
1201        '--full',
1202        dest='incremental',
1203        default=Options.incremental,
1204        action='store_false',
1205        help='Do a full build, even for unchanged files'
1206    )
1207    p.add_argument(
1208        '--profile',
1209        default=Options.profile,
1210        action='store_true',
1211        help='Use the -pg compile flag to add profiling information'
1212    )
1213    p.add_argument(
1214        '--libdir-name',
1215        default=Options.libdir_name,
1216        help='The name of the directory inside --prefix in which to store compiled files. Defaults to "lib"'
1217    )
1218    p.add_argument(
1219        '--extra-logging',
1220        action='append',
1221        default=Options.extra_logging,
1222        choices=('event-loop',),
1223        help='Turn on extra logging for debugging in this build. Can be specified multiple times, to turn'
1224        ' on different types of logging.'
1225    )
1226    p.add_argument(
1227        '--extra-include-dirs',
1228        action='append',
1229        default=Options.extra_include_dirs,
1230        help='Extra include directories to use while compiling'
1231    )
1232    p.add_argument(
1233        '--update-check-interval',
1234        type=float,
1235        default=Options.update_check_interval,
1236        help='When building a package, the default value for the update_check_interval setting will'
1237        ' be set to this number. Use zero to disable update checking.'
1238    )
1239    p.add_argument(
1240        '--egl-library',
1241        type=str,
1242        default=Options.egl_library,
1243        help='The filename argument passed to dlopen for libEGL.'
1244        ' This can be used to change the name of the loaded library or specify an absolute path.'
1245    )
1246    p.add_argument(
1247        '--startup-notification-library',
1248        type=str,
1249        default=Options.startup_notification_library,
1250        help='The filename argument passed to dlopen for libstartup-notification-1.'
1251        ' This can be used to change the name of the loaded library or specify an absolute path.'
1252    )
1253    p.add_argument(
1254        '--canberra-library',
1255        type=str,
1256        default=Options.canberra_library,
1257        help='The filename argument passed to dlopen for libcanberra.'
1258        ' This can be used to change the name of the loaded library or specify an absolute path.'
1259    )
1260    p.add_argument(
1261        '--disable-link-time-optimization',
1262        dest='link_time_optimization',
1263        default=Options.link_time_optimization,
1264        action='store_false',
1265        help='Turn off Link Time Optimization (LTO).'
1266    )
1267    p.add_argument(
1268        '--ignore-compiler-warnings',
1269        default=False, action='store_true',
1270        help='Ignore any warnings from the compiler while building'
1271    )
1272    p.add_argument(
1273        '--build-universal-binary',
1274        default=False, action='store_true',
1275        help='Build a universal binary (ARM + Intel on macOS, ignored on other platforms)'
1276    )
1277    return p
1278# }}}
1279
1280
1281def main() -> None:
1282    global verbose
1283    args = option_parser().parse_args(namespace=Options())
1284    if not is_macos:
1285        args.build_universal_binary = False
1286    verbose = args.verbose > 0
1287    args.prefix = os.path.abspath(args.prefix)
1288    os.chdir(base)
1289    if args.action == 'test':
1290        os.execlp(
1291            sys.executable, sys.executable, 'test.py'
1292        )
1293    if args.action == 'clean':
1294        clean()
1295        return
1296    launcher_dir = 'kitty/launcher'
1297
1298    with CompilationDatabase(args.incremental) as cdb:
1299        args.compilation_database = cdb
1300        if args.action == 'build':
1301            build(args)
1302            if is_macos:
1303                create_minimal_macos_bundle(args, launcher_dir)
1304            else:
1305                build_launcher(args, launcher_dir=launcher_dir)
1306        elif args.action == 'build-launcher':
1307            init_env_from_args(args, False)
1308            build_launcher(args, launcher_dir=launcher_dir)
1309        elif args.action == 'build-frozen-launcher':
1310            init_env_from_args(args, False)
1311            bundle_type = ('macos' if is_macos else 'linux') + '-freeze'
1312            build_launcher(args, launcher_dir=os.path.join(args.prefix, 'bin'), bundle_type=bundle_type)
1313        elif args.action == 'linux-package':
1314            build(args, native_optimizations=False)
1315            package(args, bundle_type='linux-package')
1316        elif args.action == 'linux-freeze':
1317            build(args, native_optimizations=False)
1318            package(args, bundle_type='linux-freeze')
1319        elif args.action == 'macos-freeze':
1320            init_env_from_args(args, native_optimizations=False)
1321            build_launcher(args, launcher_dir=launcher_dir)
1322            build(args, native_optimizations=False, call_init=False)
1323            package(args, bundle_type='macos-freeze')
1324        elif args.action == 'kitty.app':
1325            args.prefix = 'kitty.app'
1326            if os.path.exists(args.prefix):
1327                shutil.rmtree(args.prefix)
1328            build(args)
1329            package(args, bundle_type='macos-package')
1330            print('kitty.app successfully built!')
1331        elif args.action == 'export-ci-bundles':
1332            cmd = [sys.executable, '../bypy', 'export']
1333            dest = ['download.calibre-ebook.com:/srv/download/ci/kitty']
1334            subprocess.check_call(cmd + ['linux'] + dest)
1335            subprocess.check_call(cmd + ['macos'] + dest)
1336            subprocess.check_call(cmd + ['linux', '32'] + dest)
1337
1338
1339if __name__ == '__main__':
1340    main()
1341