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