setup.py 87 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286
  1. #!/usr/bin/env python
  2. # License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
  3. import argparse
  4. import glob
  5. import json
  6. import os
  7. import platform
  8. import re
  9. import runpy
  10. import shlex
  11. import shutil
  12. import struct
  13. import subprocess
  14. import sys
  15. import sysconfig
  16. import tempfile
  17. import textwrap
  18. import time
  19. from contextlib import suppress
  20. from functools import lru_cache, partial
  21. from pathlib import Path
  22. from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Union, cast
  23. from glfw import glfw
  24. from glfw.glfw import ISA, BinaryArch, Command, CompileKey, CompilerType
  25. src_base = os.path.dirname(os.path.abspath(__file__))
  26. def check_version_info() -> None:
  27. with open(os.path.join(src_base, 'pyproject.toml')) as f:
  28. raw = f.read()
  29. m = re.search(r'''^requires-python\s*=\s*['"](.+?)['"]''', raw, flags=re.MULTILINE)
  30. assert m is not None
  31. minver = m.group(1)
  32. match = re.match(r'(>=?)(\d+)\.(\d+)', minver)
  33. assert match is not None
  34. q = int(match.group(2)), int(match.group(3))
  35. if match.group(1) == '>=':
  36. is_ok = sys.version_info >= q
  37. else:
  38. is_ok = sys.version_info > q
  39. if not is_ok:
  40. exit(f'calibre requires Python {minver}. Current Python version: {".".join(map(str, sys.version_info[:3]))}')
  41. check_version_info()
  42. verbose = False
  43. build_dir = 'build'
  44. constants = os.path.join('kitty', 'constants.py')
  45. with open(constants, 'rb') as f:
  46. constants = f.read().decode('utf-8')
  47. appname = re.search(r"^appname: str = '([^']+)'", constants, re.MULTILINE).group(1) # type: ignore
  48. version = tuple(
  49. map(
  50. int,
  51. re.search( # type: ignore
  52. r"^version: Version = Version\((\d+), (\d+), (\d+)\)", constants, re.MULTILINE
  53. ).group(1, 2, 3)
  54. )
  55. )
  56. _plat = sys.platform.lower()
  57. is_macos = 'darwin' in _plat
  58. is_openbsd = 'openbsd' in _plat
  59. is_freebsd = 'freebsd' in _plat
  60. is_netbsd = 'netbsd' in _plat
  61. is_dragonflybsd = 'dragonfly' in _plat
  62. is_bsd = is_freebsd or is_netbsd or is_dragonflybsd or is_openbsd
  63. is_arm = platform.processor() == 'arm' or platform.machine() in ('arm64', 'aarch64')
  64. c_std = '' if is_openbsd else '-std=c11'
  65. Env = glfw.Env
  66. env = Env()
  67. PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config')
  68. link_targets: List[str] = []
  69. macos_universal_arches = ('arm64', 'x86_64') if is_arm else ('x86_64', 'arm64')
  70. def LinkKey(output: str) -> CompileKey:
  71. return CompileKey('', output)
  72. class CompilationDatabase:
  73. def __init__(self, incremental: bool = False):
  74. self.incremental = incremental
  75. self.compile_commands: List[Command] = []
  76. self.link_commands: List[Command] = []
  77. self.post_link_commands: List[Command] = []
  78. def add_command(
  79. self,
  80. desc: str,
  81. cmd: List[str],
  82. is_newer_func: Callable[[], bool],
  83. key: Optional[CompileKey] = None,
  84. on_success: Optional[Callable[[], None]] = None,
  85. keyfile: Optional[str] = None,
  86. is_post_link: bool = False,
  87. ) -> None:
  88. def no_op() -> None:
  89. pass
  90. if is_post_link:
  91. queue = self.post_link_commands
  92. else:
  93. queue = self.link_commands if keyfile is None else self.compile_commands
  94. queue.append(Command(desc, cmd, is_newer_func, on_success or no_op, key, keyfile))
  95. def build_all(self) -> None:
  96. def sort_key(compile_cmd: Command) -> int:
  97. if compile_cmd.keyfile:
  98. return os.path.getsize(compile_cmd.keyfile)
  99. return 0
  100. items = []
  101. for compile_cmd in self.compile_commands:
  102. if not self.incremental or self.cmd_changed(compile_cmd) or compile_cmd.is_newer_func():
  103. items.append(compile_cmd)
  104. items.sort(key=sort_key, reverse=True)
  105. parallel_run(items)
  106. items = []
  107. for compile_cmd in self.link_commands:
  108. if not self.incremental or compile_cmd.is_newer_func():
  109. items.append(compile_cmd)
  110. parallel_run(items)
  111. items = []
  112. for compile_cmd in self.post_link_commands:
  113. if not self.incremental or compile_cmd.is_newer_func():
  114. items.append(compile_cmd)
  115. parallel_run(items)
  116. def cmd_changed(self, compile_cmd: Command) -> bool:
  117. key, cmd = compile_cmd.key, compile_cmd.cmd
  118. dkey = self.db.get(key)
  119. if dkey != cmd:
  120. return True
  121. return False
  122. def __enter__(self) -> 'CompilationDatabase':
  123. self.all_keys: Set[CompileKey] = set()
  124. self.dbpath = os.path.abspath(os.path.join(build_dir, 'compile_commands.json'))
  125. self.linkdbpath = os.path.join(os.path.dirname(self.dbpath), 'link_commands.json')
  126. try:
  127. with open(self.dbpath) as f:
  128. compilation_database = json.load(f)
  129. except FileNotFoundError:
  130. compilation_database = []
  131. try:
  132. with open(self.linkdbpath) as f:
  133. link_database = json.load(f)
  134. except FileNotFoundError:
  135. link_database = []
  136. compilation_database = {
  137. CompileKey(k['file'], k['output']): k['arguments'] for k in compilation_database
  138. }
  139. self.db = compilation_database
  140. self.linkdb = {tuple(k['output']): k['arguments'] for k in link_database}
  141. return self
  142. def __exit__(self, *a: object) -> None:
  143. cdb = self.db
  144. for key in set(cdb) - self.all_keys:
  145. del cdb[key]
  146. compilation_database = [
  147. {'file': c.key.src, 'arguments': c.cmd, 'directory': src_base, 'output': c.key.dest} for c in self.compile_commands if c.key is not None
  148. ]
  149. with suppress(FileNotFoundError):
  150. with open(self.dbpath, 'w') as f:
  151. json.dump(compilation_database, f, indent=2, sort_keys=True)
  152. with open(self.linkdbpath, 'w') as f:
  153. json.dump([{'output': c.key, 'arguments': c.cmd, 'directory': src_base} for c in self.link_commands], f, indent=2, sort_keys=True)
  154. class Options:
  155. action: str = 'build'
  156. debug: bool = False
  157. verbose: int = 0
  158. sanitize: bool = False
  159. prefix: str = './linux-package'
  160. dir_for_static_binaries: str = 'build/static'
  161. skip_code_generation: bool = False
  162. skip_building_kitten: bool = False
  163. clean_for_cross_compile: bool = False
  164. python_compiler_flags: str = ''
  165. python_linker_flags: str = ''
  166. incremental: bool = True
  167. build_dsym: bool = False
  168. ignore_compiler_warnings: bool = False
  169. profile: bool = False
  170. libdir_name: str = 'lib'
  171. extra_logging: List[str] = []
  172. extra_include_dirs: List[str] = []
  173. extra_library_dirs: List[str] = []
  174. link_time_optimization: bool = 'KITTY_NO_LTO' not in os.environ
  175. update_check_interval: float = 24.0
  176. shell_integration: str = 'enabled'
  177. egl_library: Optional[str] = os.getenv('KITTY_EGL_LIBRARY')
  178. startup_notification_library: Optional[str] = os.getenv('KITTY_STARTUP_NOTIFICATION_LIBRARY')
  179. canberra_library: Optional[str] = os.getenv('KITTY_CANBERRA_LIBRARY')
  180. systemd_library: Optional[str] = os.getenv('KITTY_SYSTEMD_LIBRARY')
  181. fontconfig_library: Optional[str] = os.getenv('KITTY_FONTCONFIG_LIBRARY')
  182. building_arch: str = ''
  183. # Extras
  184. compilation_database: CompilationDatabase = CompilationDatabase()
  185. vcs_rev: str = ''
  186. def emphasis(text: str) -> str:
  187. if sys.stdout.isatty():
  188. text = f'\033[32m{text}\033[39m'
  189. return text
  190. def error(text: str) -> str:
  191. if sys.stdout.isatty():
  192. text = f'\033[91m{text}\033[39m'
  193. return text
  194. def pkg_config(pkg: str, *args: str, extra_pc_dir: str = '', fatal: bool = True) -> List[str]:
  195. env = os.environ.copy()
  196. if extra_pc_dir:
  197. pp = env.get('PKG_CONFIG_PATH', '')
  198. if pp:
  199. pp += os.pathsep
  200. env['PKG_CONFIG_PATH'] = f'{pp}{extra_pc_dir}'
  201. cmd = [PKGCONFIG, pkg] + list(args)
  202. try:
  203. return list(
  204. filter(
  205. None,
  206. shlex.split(
  207. subprocess.check_output(cmd, env=env, stderr=None if fatal else subprocess.DEVNULL).decode('utf-8')
  208. )
  209. )
  210. )
  211. except subprocess.CalledProcessError:
  212. if fatal:
  213. raise SystemExit(f'The package {error(pkg)} was not found on your system')
  214. raise
  215. def pkg_version(package: str) -> Tuple[int, int]:
  216. ver = subprocess.check_output([
  217. PKGCONFIG, package, '--modversion']).decode('utf-8').strip()
  218. m = re.match(r'(\d+).(\d+)', ver)
  219. if m is not None:
  220. qmajor, qminor = map(int, m.groups())
  221. return qmajor, qminor
  222. return -1, -1
  223. def libcrypto_flags() -> Tuple[List[str], List[str]]:
  224. # Apple use their special snowflake TLS libraries and additionally
  225. # have an ancient broken system OpenSSL, so we need to check for one
  226. # installed by all the various macOS package managers.
  227. extra_pc_dir = ''
  228. try:
  229. cflags = pkg_config('libcrypto', '--cflags-only-I', fatal=False)
  230. except subprocess.CalledProcessError:
  231. if is_macos:
  232. import ssl
  233. v = ssl.OPENSSL_VERSION_INFO
  234. pats = f'{v[0]}.{v[1]}', f'{v[0]}'
  235. for pat in pats:
  236. q = f'opt/openssl@{pat}/lib/pkgconfig'
  237. openssl_dirs = glob.glob(f'/opt/homebrew/{q}') + glob.glob(f'/usr/local/{q}')
  238. if openssl_dirs:
  239. break
  240. else:
  241. raise SystemExit(f'Failed to find OpenSSL version {v[0]}.{v[1]} on your system')
  242. extra_pc_dir = os.pathsep.join(openssl_dirs)
  243. cflags = pkg_config('libcrypto', '--cflags-only-I', extra_pc_dir=extra_pc_dir)
  244. ldflags = pkg_config('libcrypto', '--libs', extra_pc_dir=extra_pc_dir)
  245. # Workaround bug in homebrew openssl package. This bug appears in CI only
  246. if is_macos and ldflags and 'homebrew/Cellar' in ldflags[0] and not ldflags[0].endswith('/lib'):
  247. ldflags.insert(0, ldflags[0] + '/lib')
  248. return cflags, ldflags
  249. @lru_cache(maxsize=2)
  250. def xxhash_flags() -> tuple[list[str], list[str]]:
  251. return pkg_config('libxxhash', '--cflags-only-I'), pkg_config('libxxhash', '--libs')
  252. def at_least_version(package: str, major: int, minor: int = 0) -> None:
  253. q = f'{major}.{minor}'
  254. if subprocess.run([PKGCONFIG, package, f'--atleast-version={q}']
  255. ).returncode != 0:
  256. qmajor = qminor = 0
  257. try:
  258. ver = subprocess.check_output([PKGCONFIG, package, '--modversion']
  259. ).decode('utf-8').strip()
  260. m = re.match(r'(\d+).(\d+)', ver)
  261. if m is not None:
  262. qmajor, qminor = map(int, m.groups())
  263. except Exception:
  264. ver = 'not found'
  265. if qmajor < major or (qmajor == major and qminor < minor):
  266. raise SystemExit(f'{error(package)} >= {major}.{minor} is required, found version: {ver}')
  267. def cc_version() -> Tuple[List[str], Tuple[int, int]]:
  268. if 'CC' in os.environ:
  269. q = os.environ['CC']
  270. else:
  271. if is_macos:
  272. q = 'clang'
  273. else:
  274. if shutil.which('gcc'):
  275. q = 'gcc'
  276. elif shutil.which('clang'):
  277. q = 'clang'
  278. else:
  279. q = 'cc'
  280. cc = shlex.split(q)
  281. raw = subprocess.check_output(cc + ['-dumpversion']).decode('utf-8')
  282. ver_ = raw.strip().split('.')[:2]
  283. try:
  284. if len(ver_) == 1:
  285. ver = int(ver_[0]), 0
  286. else:
  287. ver = int(ver_[0]), int(ver_[1])
  288. except Exception:
  289. ver = (0, 0)
  290. return cc, ver
  291. def get_python_include_paths() -> List[str]:
  292. ans = []
  293. for name in sysconfig.get_path_names():
  294. if 'include' in name:
  295. ans.append(name)
  296. def gp(x: str) -> Optional[str]:
  297. return sysconfig.get_path(x)
  298. return sorted(frozenset(filter(None, map(gp, sorted(ans)))))
  299. def get_python_flags(args: Options, cflags: List[str], for_main_executable: bool = False) -> List[str]:
  300. if args.python_compiler_flags:
  301. cflags.extend(shlex.split(args.python_compiler_flags))
  302. else:
  303. cflags.extend(f'-I{x}' for x in get_python_include_paths())
  304. if args.python_linker_flags:
  305. return shlex.split(args.python_linker_flags)
  306. libs: List[str] = []
  307. libs += (sysconfig.get_config_var('LIBS') or '').split()
  308. libs += (sysconfig.get_config_var('SYSLIBS') or '').split()
  309. fw = sysconfig.get_config_var('PYTHONFRAMEWORK')
  310. if fw:
  311. for var in 'data include stdlib'.split():
  312. val = sysconfig.get_path(var)
  313. if val and f'/{fw}.framework' in val:
  314. fdir = val[:val.index(f'/{fw}.framework')]
  315. if os.path.isdir(
  316. os.path.join(fdir, f'{fw}.framework')
  317. ):
  318. framework_dir = fdir
  319. break
  320. else:
  321. raise SystemExit('Failed to find Python framework')
  322. ldlib = sysconfig.get_config_var('LDLIBRARY')
  323. if ldlib:
  324. libs.append(os.path.join(framework_dir, ldlib))
  325. else:
  326. ldlib = sysconfig.get_config_var('LIBDIR')
  327. if ldlib:
  328. libs += [f'-L{ldlib}']
  329. ldlib = sysconfig.get_config_var('VERSION')
  330. if ldlib:
  331. libs += [f'-lpython{ldlib}{sys.abiflags}']
  332. lval = sysconfig.get_config_var('LINKFORSHARED') or ''
  333. if not for_main_executable:
  334. # Python sets the stack size on macOS which is not allowed unless
  335. # compiling an executable https://github.com/kovidgoyal/kitty/issues/289
  336. lval = re.sub(r'-Wl,-stack_size,\d+', '', lval)
  337. libs += list(filter(None, lval.split()))
  338. return libs
  339. def get_sanitize_args(cc: List[str], ccver: Tuple[int, int]) -> List[str]:
  340. return ['-fsanitize=address,undefined', '-fno-omit-frame-pointer']
  341. def get_binary_arch(path: str) -> BinaryArch:
  342. with open(path, 'rb') as f:
  343. sig = f.read(64)
  344. if sig.startswith(b'\x7fELF'): # ELF
  345. bits = {1: 32, 2: 64}[sig[4]]
  346. endian = {1: '<', 2: '>'}[sig[5]]
  347. machine, = struct.unpack_from(endian + 'H', sig, 0x12)
  348. isa = {i.value:i for i in ISA}.get(machine, ISA.Other)
  349. elif sig[:4] in (b'\xcf\xfa\xed\xfe', b'\xce\xfa\xed\xfe'): # Mach-O
  350. s, cpu_type, = struct.unpack_from('<II', sig, 0)
  351. bits = {0xfeedface: 32, 0xfeedfacf: 64}[s]
  352. cpu_type &= 0xff
  353. isa = {0x7: ISA.AMD64, 0xc: ISA.ARM64}[cpu_type]
  354. else:
  355. raise SystemExit(f'Unknown binary format with signature: {sig[:4]!r}')
  356. return BinaryArch(bits=bits, isa=isa)
  357. def test_compile(
  358. cc: List[str], *cflags: str,
  359. src: str = '',
  360. source_ext: str = 'c',
  361. link_also: bool = True,
  362. show_stderr: bool = False,
  363. libraries: Iterable[str] = (),
  364. ldflags: Iterable[str] = (),
  365. get_output_arch: bool = False,
  366. ) -> Union[bool, BinaryArch]:
  367. src = src or 'int main(void) { return 0; }'
  368. with tempfile.TemporaryDirectory(prefix='kitty-test-compile-') as tdir:
  369. with open(os.path.join(tdir, f'source.{source_ext}'), 'w', encoding='utf-8') as srcf:
  370. print(src, file=srcf)
  371. output = os.path.join(tdir, 'source.output')
  372. ret = subprocess.Popen(
  373. cc + ['-Werror=implicit-function-declaration'] + list(cflags) + ([] if link_also else ['-c']) +
  374. ['-o', output, srcf.name] +
  375. [f'-l{x}' for x in libraries] + list(ldflags),
  376. stdout=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
  377. stderr=None if show_stderr else subprocess.DEVNULL
  378. ).wait()
  379. if get_output_arch:
  380. if ret != 0:
  381. raise SystemExit(f'Failed to determine target architecture compiling test program failed with exit code: {ret}')
  382. return get_binary_arch(output)
  383. return ret == 0
  384. def first_successful_compile(cc: List[str], *cflags: str, src: str = '', source_ext: str = 'c') -> str:
  385. for x in cflags:
  386. if test_compile(cc, *shlex.split(x), src=src, source_ext=source_ext):
  387. return x
  388. return ''
  389. def set_arches(flags: List[str], *arches: str) -> None:
  390. while True:
  391. try:
  392. idx = flags.index('-arch')
  393. except ValueError:
  394. break
  395. del flags[idx]
  396. del flags[idx]
  397. for arch in arches:
  398. flags.extend(('-arch', arch))
  399. def init_env(
  400. debug: bool = False,
  401. sanitize: bool = False,
  402. native_optimizations: bool = True,
  403. link_time_optimization: bool = True,
  404. profile: bool = False,
  405. egl_library: Optional[str] = None,
  406. startup_notification_library: Optional[str] = None,
  407. canberra_library: Optional[str] = None,
  408. systemd_library: Optional[str] = None,
  409. fontconfig_library: Optional[str] = None,
  410. extra_logging: Iterable[str] = (),
  411. extra_include_dirs: Iterable[str] = (),
  412. ignore_compiler_warnings: bool = False,
  413. building_arch: str = '',
  414. extra_library_dirs: Iterable[str] = (),
  415. verbose: bool = True,
  416. vcs_rev: str = '',
  417. ) -> Env:
  418. native_optimizations = native_optimizations and not sanitize
  419. cc, ccver = cc_version()
  420. if verbose:
  421. print('CC:', cc, ccver)
  422. stack_protector = first_successful_compile(cc, '-fstack-protector-strong', '-fstack-protector')
  423. missing_braces = ''
  424. if ccver < (5, 2):
  425. missing_braces = '-Wno-missing-braces'
  426. df = '-g3'
  427. float_conversion = ''
  428. if ccver >= (5, 0):
  429. df += ' -Og'
  430. float_conversion = '-Wfloat-conversion'
  431. fortify_source = '' if sanitize and is_macos else '-D_FORTIFY_SOURCE=2'
  432. optimize = df if debug or sanitize else '-O3'
  433. sanitize_args = get_sanitize_args(cc, ccver) if sanitize else []
  434. cppflags_ = os.environ.get(
  435. 'OVERRIDE_CPPFLAGS', '-D{}DEBUG'.format('' if debug else 'N'),
  436. )
  437. cppflags = shlex.split(cppflags_)
  438. for el in extra_logging:
  439. cppflags.append('-DDEBUG_{}'.format(el.upper().replace('-', '_')))
  440. has_copy_file_range = test_compile(cc, src='#define _GNU_SOURCE 1\n#include <unistd.h>\nint main() { copy_file_range(1, NULL, 2, NULL, 0, 0); return 0; }')
  441. werror = '' if ignore_compiler_warnings else '-pedantic-errors -Werror'
  442. sanitize_flag = ' '.join(sanitize_args)
  443. env_cflags = shlex.split(os.environ.get('CFLAGS', ''))
  444. env_cppflags = shlex.split(os.environ.get('CPPFLAGS', ''))
  445. env_ldflags = shlex.split(os.environ.get('LDFLAGS', ''))
  446. # Newer clang does not use -fno-plt leading to an error
  447. no_plt = '-fno-plt' if test_compile(cc, '-fno-plt', '-Werror') else ''
  448. cflags_ = os.environ.get(
  449. 'OVERRIDE_CFLAGS', (
  450. f'-Wextra {float_conversion} -Wno-missing-field-initializers -Wall -Wstrict-prototypes {c_std}'
  451. f' {werror} {optimize} {sanitize_flag} -fwrapv {stack_protector} {missing_braces}'
  452. f' -pipe -fvisibility=hidden {no_plt}'
  453. )
  454. )
  455. cflags = shlex.split(cflags_) + shlex.split(
  456. sysconfig.get_config_var('CCSHARED') or ''
  457. )
  458. ldflags_ = os.environ.get(
  459. 'OVERRIDE_LDFLAGS',
  460. '-Wall ' + ' '.join(sanitize_args) + ('' if debug else ' -O3')
  461. )
  462. ldflags = shlex.split(ldflags_)
  463. ldflags.append('-shared')
  464. cppflags += env_cppflags
  465. cflags += env_cflags
  466. if fortify_source:
  467. for x in cflags:
  468. if '_FORTIFY_SOURCE' in x:
  469. break
  470. else:
  471. cflags.append(fortify_source)
  472. ldflags += env_ldflags
  473. if not debug and not sanitize and not is_openbsd and link_time_optimization:
  474. # See https://github.com/google/sanitizers/issues/647
  475. cflags.append('-flto')
  476. ldflags.append('-flto')
  477. if debug:
  478. cflags.append('-DKITTY_DEBUG_BUILD')
  479. if profile:
  480. cppflags.append('-DWITH_PROFILER')
  481. cflags.append('-g3')
  482. ldflags.append('-lprofiler')
  483. if debug or profile:
  484. cflags.append('-fno-omit-frame-pointer')
  485. library_paths: Dict[str, List[str]] = {}
  486. def add_lpath(which: str, name: str, val: Optional[str]) -> None:
  487. if val:
  488. if '"' in val:
  489. raise SystemExit(f'Cannot have quotes in library paths: {val}')
  490. library_paths.setdefault(which, []).append(f'{name}="{val}"')
  491. add_lpath('glfw/egl_context.c', '_GLFW_EGL_LIBRARY', egl_library)
  492. add_lpath('kitty/desktop.c', '_KITTY_STARTUP_NOTIFICATION_LIBRARY', startup_notification_library)
  493. add_lpath('kitty/desktop.c', '_KITTY_CANBERRA_LIBRARY', canberra_library)
  494. add_lpath('kitty/systemd.c', '_KITTY_SYSTEMD_LIBRARY', systemd_library)
  495. add_lpath('kitty/fontconfig.c', '_KITTY_FONTCONFIG_LIBRARY', fontconfig_library)
  496. for path in extra_include_dirs:
  497. cflags.append(f'-I{path}')
  498. ldpaths = []
  499. for path in extra_library_dirs:
  500. ldpaths.append(f'-L{path}')
  501. if os.environ.get("DEVELOP_ROOT"):
  502. cflags.insert(0, f'-I{os.environ["DEVELOP_ROOT"]}/include')
  503. ldpaths.insert(0, f'-L{os.environ["DEVELOP_ROOT"]}/lib')
  504. if building_arch:
  505. set_arches(cflags, building_arch)
  506. set_arches(ldflags, building_arch)
  507. ba = test_compile(cc, *(cppflags + cflags), ldflags=ldflags, get_output_arch=True)
  508. assert isinstance(ba, BinaryArch)
  509. if ba.isa not in (ISA.AMD64, ISA.X86, ISA.ARM64):
  510. cppflags.append('-DKITTY_NO_SIMD')
  511. control_flow_protection = ''
  512. if ba.isa == ISA.AMD64:
  513. control_flow_protection = '-fcf-protection=full' if ccver >= (9, 0) else ''
  514. elif ba.isa == ISA.ARM64:
  515. # Using -mbranch-protection=standard causes crashes on Linux ARM, reported
  516. # in https://github.com/kovidgoyal/kitty/issues/6845#issuecomment-1835886938
  517. if is_macos:
  518. control_flow_protection = '-mbranch-protection=standard'
  519. if control_flow_protection:
  520. cflags.append(control_flow_protection)
  521. if native_optimizations and ba.isa in (ISA.AMD64, ISA.X86):
  522. cflags.extend('-march=native -mtune=native'.split())
  523. ans = Env(
  524. cc, cppflags, cflags, ldflags, library_paths, binary_arch=ba, native_optimizations=native_optimizations,
  525. ccver=ccver, ldpaths=ldpaths, vcs_rev=vcs_rev,
  526. )
  527. ans.has_copy_file_range = bool(has_copy_file_range)
  528. if ans.compiler_type is CompilerType.gcc:
  529. cflags.append('-Wno-packed-bitfield-compat')
  530. if verbose:
  531. print(ans.cc_version_string.strip())
  532. print('Detected:', ans.compiler_type)
  533. return ans
  534. def kitty_env(args: Options) -> Env:
  535. ans = env.copy()
  536. cflags = ans.cflags
  537. cflags.append('-pthread')
  538. cppflags = ans.cppflags
  539. # We add 4000 to the primary version because vim turns on SGR mouse mode
  540. # automatically if this version is high enough
  541. ans.primary_version = version[0] + 4000
  542. ans.secondary_version = version[1]
  543. ans.xt_version = '.'.join(map(str, version))
  544. xxhash = xxhash_flags()
  545. at_least_version('harfbuzz', 1, 5)
  546. cflags.extend(pkg_config('libpng', '--cflags-only-I'))
  547. cflags.extend(pkg_config('lcms2', '--cflags-only-I'))
  548. cflags.extend(xxhash[0])
  549. # simde doesnt come with pkg-config files but some Linux distros add
  550. # them and on macOS when building with homebrew it is required
  551. with suppress(SystemExit, subprocess.CalledProcessError):
  552. cflags.extend(pkg_config('simde', '--cflags-only-I', fatal=False))
  553. libcrypto_cflags, libcrypto_ldflags = libcrypto_flags()
  554. cflags.extend(libcrypto_cflags)
  555. if is_macos:
  556. platform_libs = [
  557. '-framework', 'Carbon', '-framework', 'CoreText', '-framework', 'CoreGraphics',
  558. '-framework', 'AudioToolbox',
  559. ]
  560. test_program_src = '''#include <UserNotifications/UserNotifications.h>
  561. int main(void) { return 0; }\n'''
  562. user_notifications_framework = first_successful_compile(
  563. ans.cc, '-framework UserNotifications', src=test_program_src, source_ext='m')
  564. if user_notifications_framework:
  565. platform_libs.extend(shlex.split(user_notifications_framework))
  566. else:
  567. raise SystemExit('UserNotifications framework missing')
  568. # Apple deprecated OpenGL in Mojave (10.14) silence the endless
  569. # warnings about it
  570. cppflags.append('-DGL_SILENCE_DEPRECATION')
  571. else:
  572. cflags.extend(pkg_config('cairo-fc', '--cflags-only-I'))
  573. platform_libs = []
  574. platform_libs.extend(pkg_config('cairo-fc', '--libs'))
  575. cflags.extend(pkg_config('harfbuzz', '--cflags-only-I'))
  576. platform_libs.extend(pkg_config('harfbuzz', '--libs'))
  577. pylib = get_python_flags(args, cflags)
  578. gl_libs = ['-framework', 'OpenGL'] if is_macos else pkg_config('gl', '--libs')
  579. libpng = pkg_config('libpng', '--libs')
  580. lcms2 = pkg_config('lcms2', '--libs')
  581. ans.ldpaths += pylib + platform_libs + gl_libs + libpng + lcms2 + libcrypto_ldflags + xxhash[1]
  582. if is_macos:
  583. ans.ldpaths.extend('-framework Cocoa'.split())
  584. elif not is_openbsd:
  585. ans.ldpaths += ['-lrt']
  586. if '-ldl' not in ans.ldpaths:
  587. ans.ldpaths.append('-ldl')
  588. if '-lz' not in ans.ldpaths:
  589. ans.ldpaths.append('-lz')
  590. return ans
  591. def define(x: str) -> str:
  592. return f'-D{x}'
  593. def run_tool(cmd: Union[str, List[str]], desc: Optional[str] = None) -> None:
  594. if isinstance(cmd, str):
  595. cmd = shlex.split(cmd[0])
  596. if verbose:
  597. desc = None
  598. print(desc or ' '.join(cmd))
  599. p = subprocess.Popen(cmd)
  600. ret = p.wait()
  601. if ret != 0:
  602. if desc:
  603. print(' '.join(cmd))
  604. raise SystemExit(ret)
  605. @lru_cache
  606. def get_vcs_rev() -> str:
  607. ans = ''
  608. if os.path.exists('.git'):
  609. try:
  610. rev = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8')
  611. except FileNotFoundError:
  612. try:
  613. with open('.git/refs/heads/master') as f:
  614. rev = f.read()
  615. except NotADirectoryError:
  616. with open('.git') as f:
  617. gitloc = f.read()
  618. with open(os.path.join(gitloc, 'refs/heads/master')) as f:
  619. rev = f.read()
  620. ans = rev.strip()
  621. return ans
  622. @lru_cache
  623. def base64_defines(isa: ISA) -> List[str]:
  624. defs = {
  625. 'HAVE_AVX512': 0,
  626. 'HAVE_AVX2': 0,
  627. 'HAVE_NEON32': 0,
  628. 'HAVE_NEON64': 0,
  629. 'HAVE_SSSE3': 0,
  630. 'HAVE_SSE41': 0,
  631. 'HAVE_SSE42': 0,
  632. 'HAVE_AVX': 0,
  633. }
  634. if isa == ISA.ARM64:
  635. defs['HAVE_NEON64'] = 1
  636. elif isa == ISA.AMD64:
  637. defs['HAVE_AVX2'] = 1
  638. defs['HAVE_AVX'] = 1
  639. defs['HAVE_SSE42'] = 1
  640. defs['HAVE_SSE41'] = 1
  641. defs['HAVE_SSE3'] = 1
  642. elif isa == ISA.X86:
  643. defs['HAVE_SSE42'] = 1
  644. defs['HAVE_SSE41'] = 1
  645. defs['HAVE_SSE3'] = 1
  646. return [f'{k}={v}' for k, v in defs.items()]
  647. def get_source_specific_defines(env: Env, src: str) -> Tuple[str, List[str], Optional[List[str]]]:
  648. if src == 'kitty/vt-parser-dump.c':
  649. return 'kitty/vt-parser.c', [], ['DUMP_COMMANDS']
  650. if src == 'kitty/data-types.c':
  651. if not env.vcs_rev:
  652. env.vcs_rev = get_vcs_rev()
  653. return src, [], [f'KITTY_VCS_REV="{env.vcs_rev}"', f'WRAPPED_KITTENS="{wrapped_kittens()}"']
  654. if src.startswith('3rdparty/base64/'):
  655. return src, ['3rdparty/base64',], base64_defines(env.binary_arch.isa)
  656. if src == 'kitty/screen.c':
  657. return src, [], [f'PRIMARY_VERSION={env.primary_version}', f'SECONDARY_VERSION={env.secondary_version}', f'XT_VERSION="{env.xt_version}"']
  658. if src == 'kitty/fast-file-copy.c':
  659. return src, [], (['HAS_COPY_FILE_RANGE'] if env.has_copy_file_range else None)
  660. try:
  661. return src, [], env.library_paths[src]
  662. except KeyError:
  663. return src, [], None
  664. def get_source_specific_cflags(env: Env, src: str) -> List[str]:
  665. ans = list(env.cflags)
  666. # SIMD specific flags
  667. if src in ('kitty/simd-string-128.c', 'kitty/simd-string-256.c'):
  668. # simde recommends these are used for best performance
  669. ans.extend(('-fopenmp-simd', '-DSIMDE_ENABLE_OPENMP'))
  670. if env.binary_arch.isa in (ISA.AMD64, ISA.X86):
  671. ans.append('-msse4.2' if '128' in src else '-mavx2')
  672. if '256' in src:
  673. # We have manual vzeroupper so prevent compiler from emitting it causing duplicates
  674. if env.compiler_type is CompilerType.clang:
  675. ans.append('-mllvm')
  676. ans.append('-x86-use-vzeroupper=0')
  677. else:
  678. ans.append('-mno-vzeroupper')
  679. elif src.startswith('3rdparty/base64/lib/arch/'):
  680. if env.binary_arch.isa in (ISA.AMD64, ISA.X86):
  681. q = src.split(os.path.sep)
  682. if 'sse3' in q:
  683. ans.append('-msse3')
  684. elif 'sse41' in q:
  685. ans.append('-msse4.1')
  686. elif 'sse42' in q:
  687. ans.append('-msse4.2')
  688. elif 'avx' in q:
  689. ans.append('-mavx')
  690. elif 'avx2' in q:
  691. ans.append('-mavx2')
  692. return ans
  693. def newer(dest: str, *sources: str) -> bool:
  694. try:
  695. dtime = os.path.getmtime(dest)
  696. except OSError:
  697. return True
  698. for s in sources:
  699. with suppress(FileNotFoundError):
  700. if os.path.getmtime(s) >= dtime:
  701. return True
  702. return False
  703. def dependecies_for(src: str, obj: str, all_headers: Iterable[str]) -> Iterable[str]:
  704. dep_file = obj.rpartition('.')[0] + '.d'
  705. try:
  706. with open(dep_file) as f:
  707. deps = f.read()
  708. except FileNotFoundError:
  709. yield src
  710. yield from iter(all_headers)
  711. else:
  712. RE_INC = re.compile(
  713. r'^(?P<target>.+?):\s+(?P<deps>.+?)$', re.MULTILINE
  714. )
  715. SPACE_TOK = '\x1B'
  716. text = deps.replace('\\\n', ' ').replace('\\ ', SPACE_TOK)
  717. for match in RE_INC.finditer(text):
  718. files = (
  719. f.replace(SPACE_TOK, ' ') for f in match.group('deps').split()
  720. )
  721. for path in files:
  722. path = os.path.abspath(path)
  723. if path.startswith(src_base):
  724. yield path
  725. def parallel_run(items: List[Command]) -> None:
  726. try:
  727. num_workers = max(2, os.cpu_count() or 1)
  728. except Exception:
  729. num_workers = 2
  730. items = list(reversed(items))
  731. workers: Dict[int, Tuple[Optional[Command], Optional['subprocess.Popen[bytes]']]] = {}
  732. failed = None
  733. num, total = 0, len(items)
  734. def wait() -> None:
  735. nonlocal failed
  736. if not workers:
  737. return
  738. pid, s = os.wait()
  739. compile_cmd, w = workers.pop(pid, (None, None))
  740. if compile_cmd is None:
  741. return
  742. if ((s & 0xff) != 0 or ((s >> 8) & 0xff) != 0):
  743. if failed is None:
  744. failed = compile_cmd
  745. elif compile_cmd.on_success is not None:
  746. compile_cmd.on_success()
  747. printed = False
  748. isatty = sys.stdout.isatty()
  749. while items and failed is None:
  750. while len(workers) < num_workers and items:
  751. compile_cmd = items.pop()
  752. num += 1
  753. if verbose:
  754. print(' '.join(compile_cmd.cmd))
  755. elif isatty:
  756. print(f'\r\x1b[K[{num}/{total}] {compile_cmd.desc}', end='') # ]]
  757. else:
  758. print(f'[{num}/{total}] {compile_cmd.desc}', flush=True)
  759. printed = True
  760. w = subprocess.Popen(compile_cmd.cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
  761. workers[w.pid] = compile_cmd, w
  762. wait()
  763. while len(workers):
  764. wait()
  765. if not verbose and printed:
  766. print(' done')
  767. if failed:
  768. print(failed.desc)
  769. run_tool(list(failed.cmd))
  770. def add_builtin_fonts(args: Options) -> None:
  771. fonts_dir = os.path.join(src_base, 'fonts')
  772. os.makedirs(fonts_dir, exist_ok=True)
  773. for psname, (filename, human_name) in {
  774. 'SymbolsNFM': ('SymbolsNerdFontMono-Regular.ttf', 'Symbols NERD Font Mono')
  775. }.items():
  776. dest = os.path.join(fonts_dir, filename)
  777. if os.path.exists(dest):
  778. continue
  779. font_file = ''
  780. if is_macos:
  781. for candidate in (os.path.expanduser('~/Library/Fonts'), '/Library/Fonts', '/System/Library/Fonts', '/Network/Library/Fonts'):
  782. q = os.path.join(candidate, filename)
  783. if os.path.exists(q):
  784. font_file = q
  785. break
  786. else:
  787. lines = subprocess.check_output([
  788. 'fc-match', '--format', '%{file}\n%{postscriptname}', f'term:postscriptname={psname}', 'file', 'postscriptname']).decode().splitlines()
  789. if len(lines) != 2:
  790. raise SystemExit(f'fc-match returned unexpected output: {lines}')
  791. if lines[1] != psname:
  792. raise SystemExit(f'The font {human_name!r} was not found on your system, please install it')
  793. font_file = lines[0]
  794. if not font_file:
  795. raise SystemExit(f'The font {human_name!r} was not found on your system, please install it')
  796. print(f'Copying {human_name!r} from {font_file}')
  797. shutil.copy(font_file, dest)
  798. os.chmod(dest, 0o644)
  799. def compile_c_extension(
  800. kenv: Env,
  801. module: str,
  802. compilation_database: CompilationDatabase,
  803. sources: List[str],
  804. headers: List[str],
  805. desc_prefix: str = '',
  806. build_dsym: bool = False,
  807. ) -> None:
  808. prefix = os.path.basename(module)
  809. objects = [
  810. os.path.join(build_dir, f'{prefix}-{src.replace("/", "-")}.o')
  811. for src in sources
  812. ]
  813. for original_src, dest in zip(sources, objects):
  814. src = original_src
  815. cppflags = kenv.cppflags[:]
  816. src, include_paths, defines = get_source_specific_defines(kenv, src)
  817. if defines is not None:
  818. cppflags.extend(map(define, defines))
  819. cflags = get_source_specific_cflags(kenv, src)
  820. cmd = kenv.cc + ['-MMD'] + cppflags + [f'-I{x}' for x in include_paths] + cflags
  821. cmd += ['-c', src] + ['-o', dest]
  822. key = CompileKey(original_src, os.path.basename(dest))
  823. desc = f'Compiling {emphasis(desc_prefix + src)} ...'
  824. compilation_database.add_command(desc, cmd, partial(newer, dest, *dependecies_for(src, dest, headers)), key=key, keyfile=src)
  825. dest = os.path.join(build_dir, f'{module}.so')
  826. real_dest = f'{module}.so'
  827. link_targets.append(os.path.abspath(real_dest))
  828. os.makedirs(os.path.dirname(dest), exist_ok=True)
  829. desc = f'Linking {emphasis(desc_prefix + module)} ...'
  830. # Old versions of clang don't like -pthread being passed to the linker
  831. # Don't treat linker warnings as errors (linker generates spurious
  832. # warnings on some old systems)
  833. unsafe = {'-pthread', '-Werror', '-pedantic-errors'}
  834. linker_cflags = list(filter(lambda x: x not in unsafe, kenv.cflags))
  835. cmd = kenv.cc + linker_cflags + kenv.ldflags + objects + kenv.ldpaths + ['-o', dest]
  836. def on_success() -> None:
  837. os.rename(dest, real_dest)
  838. compilation_database.add_command(desc, cmd, partial(newer, real_dest, *objects), on_success=on_success, key=LinkKey(f'{module}.so'))
  839. if is_macos and build_dsym:
  840. real_dest = os.path.abspath(real_dest)
  841. desc = f'Linking dSYM {emphasis(desc_prefix + module)} ...'
  842. dsym = f'{real_dest}.dSYM/Contents/Resources/DWARF/{os.path.basename(real_dest)}'
  843. compilation_database.add_command(desc, ['dsymutil', real_dest], partial(newer, dsym, real_dest), key=LinkKey(dsym), is_post_link=True)
  844. def find_c_files() -> Tuple[List[str], List[str]]:
  845. ans, headers = [], []
  846. d = 'kitty'
  847. exclude = {
  848. 'fontconfig.c', 'freetype.c', 'desktop.c', 'freetype_render_ui_text.c'
  849. } if is_macos else {
  850. 'core_text.m', 'cocoa_window.m', 'macos_process_info.c'
  851. }
  852. for x in sorted(os.listdir(d)):
  853. ext = os.path.splitext(x)[1]
  854. if ext in ('.c', '.m') and os.path.basename(x) not in exclude:
  855. ans.append(os.path.join('kitty', x))
  856. elif ext == '.h':
  857. headers.append(os.path.join('kitty', x))
  858. ans.append('kitty/vt-parser-dump.c')
  859. # ringbuf
  860. ans.append('3rdparty/ringbuf/ringbuf.c')
  861. # base64
  862. ans.extend(glob.glob('3rdparty/base64/lib/arch/*/codec.c'))
  863. ans.append('3rdparty/base64/lib/tables/tables.c')
  864. ans.append('3rdparty/base64/lib/codec_choose.c')
  865. ans.append('3rdparty/base64/lib/lib.c')
  866. return ans, headers
  867. def compile_glfw(compilation_database: CompilationDatabase, build_dsym: bool = False) -> None:
  868. modules = 'cocoa' if is_macos else 'x11 wayland'
  869. for module in modules.split():
  870. try:
  871. genv = glfw.init_env(env, pkg_config, pkg_version, at_least_version, test_compile, module)
  872. except SystemExit as err:
  873. if module != 'wayland':
  874. raise
  875. print(err, file=sys.stderr)
  876. print(error('Disabling building of wayland backend'), file=sys.stderr)
  877. continue
  878. sources = [os.path.join('glfw', x) for x in genv.sources]
  879. all_headers = [os.path.join('glfw', x) for x in genv.all_headers]
  880. if module == 'wayland':
  881. try:
  882. glfw.build_wayland_protocols(genv, parallel_run, emphasis, newer, 'glfw')
  883. except SystemExit as err:
  884. print(err, file=sys.stderr)
  885. print(error('Disabling building of wayland backend'), file=sys.stderr)
  886. continue
  887. compile_c_extension(
  888. genv, f'kitty/glfw-{module}', compilation_database,
  889. sources, all_headers, desc_prefix=f'[{module}] ', build_dsym=build_dsym)
  890. def kittens_env(args: Options) -> Env:
  891. kenv = env.copy()
  892. cflags = kenv.cflags
  893. cflags.append('-pthread')
  894. cflags.append('-Ikitty')
  895. pylib = get_python_flags(args, cflags)
  896. kenv.ldpaths += pylib
  897. return kenv
  898. def compile_kittens(args: Options) -> None:
  899. kenv = kittens_env(args)
  900. def list_files(q: str) -> List[str]:
  901. return sorted(glob.glob(q))
  902. def files(
  903. kitten: str,
  904. output: str,
  905. extra_headers: Sequence[str] = (),
  906. extra_sources: Sequence[str] = (),
  907. filter_sources: Optional[Callable[[str], bool]] = None,
  908. includes: Sequence[str] = (), libraries: Sequence[str] = (),
  909. ) -> Tuple[str, List[str], List[str], str, Sequence[str], Sequence[str]]:
  910. sources = list(filter(filter_sources, list(extra_sources) + list_files(os.path.join('kittens', kitten, '*.c'))))
  911. headers = list_files(os.path.join('kittens', kitten, '*.h')) + list(extra_headers)
  912. return kitten, sources, headers, f'kittens/{kitten}/{output}', includes, libraries
  913. xxhash = xxhash_flags()
  914. for kitten, sources, all_headers, dest, includes, libraries in (
  915. files('transfer', 'rsync', libraries=xxhash[1], includes=xxhash[0]),
  916. ):
  917. final_env = kenv.copy()
  918. final_env.cflags.extend(includes)
  919. final_env.ldpaths[:0] = list(libraries)
  920. compile_c_extension(
  921. final_env, dest, args.compilation_database, sources, all_headers + ['kitty/data-types.h'], build_dsym=args.build_dsym)
  922. def init_env_from_args(args: Options, native_optimizations: bool = False) -> None:
  923. global env
  924. env = init_env(
  925. args.debug, args.sanitize, native_optimizations, args.link_time_optimization, args.profile,
  926. args.egl_library, args.startup_notification_library, args.canberra_library, args.systemd_library, args.fontconfig_library,
  927. args.extra_logging, args.extra_include_dirs, args.ignore_compiler_warnings,
  928. args.building_arch, args.extra_library_dirs, verbose=args.verbose > 0, vcs_rev=args.vcs_rev,
  929. )
  930. @lru_cache
  931. def extract_rst_targets() -> Dict[str, Dict[str, str]]:
  932. m = runpy.run_path('docs/extract-rst-targets.py')
  933. return cast(Dict[str, Dict[str, str]], m['main']())
  934. def update_if_changed(path: str, text: str) -> None:
  935. q = ''
  936. with suppress(FileNotFoundError), open(path) as f:
  937. q = f.read()
  938. if q != text:
  939. with open(path, 'w') as f:
  940. f.write(text)
  941. def build_ref_map(skip_generation: bool = False) -> str:
  942. dest = 'kitty/docs_ref_map_generated.h'
  943. if not skip_generation:
  944. d = extract_rst_targets()
  945. h = 'static const char docs_ref_map[] = {\n' + textwrap.fill(', '.join(map(str, bytearray(json.dumps(d, sort_keys=True).encode('utf-8'))))) + '\n};\n'
  946. update_if_changed(dest, h)
  947. return dest
  948. def build_cli_parser_specs(skip_generation: bool = False) -> str:
  949. dest = 'kitty/launcher/cli-parser-data_generated.h'
  950. if not skip_generation:
  951. m = runpy.run_path('kitty/simple_cli_definitions.py', {'appname': appname})
  952. h = '\n'.join(m['generate_c_parsers']())
  953. update_if_changed(dest, h)
  954. return dest
  955. def build_uniforms_header(skip_generation: bool = False) -> str:
  956. dest = 'kitty/uniforms_generated.h'
  957. if skip_generation:
  958. return dest
  959. lines = ['#include "gl.h"', '']
  960. a = lines.append
  961. uniform_names: Dict[str, Tuple[str, ...]] = {}
  962. class_names = {}
  963. function_names = {}
  964. def find_uniform_names(raw: str) -> Iterator[str]:
  965. for m in re.finditer(r'^uniform\s+\S+\s+(.+?);', raw, flags=re.MULTILINE):
  966. for x in m.group(1).split(','):
  967. yield x.strip().partition('[')[0]
  968. for x in sorted(glob.glob('kitty/*.glsl')):
  969. name = os.path.basename(x).partition('.')[0]
  970. name, sep, shader_type = name.partition('_')
  971. if not sep or shader_type not in ('fragment', 'vertex'):
  972. continue
  973. class_names[name] = f'{name.capitalize()}Uniforms'
  974. function_names[name] = f'get_uniform_locations_{name}'
  975. with open(x) as f:
  976. raw = f.read()
  977. uniform_names[name] = uniform_names.setdefault(name, ()) + tuple(find_uniform_names(raw))
  978. for name in sorted(class_names):
  979. class_name, function_name, uniforms = class_names[name], function_names[name], uniform_names[name]
  980. a(f'typedef struct {class_name} ''{')
  981. for n in uniforms:
  982. a(f' GLint {n};')
  983. a('}'f' {class_name};')
  984. a('')
  985. a(f'static inline void\n{function_name}(int program, {class_name} *ans) ''{')
  986. for n in uniforms:
  987. a(f' ans->{n} = get_uniform_location(program, "{n}");')
  988. a('}')
  989. a('')
  990. src = '\n'.join(lines)
  991. try:
  992. with open(dest) as f:
  993. current = f.read()
  994. except FileNotFoundError:
  995. current = ''
  996. if src != current:
  997. with open(dest, 'w') as f:
  998. f.write(src)
  999. return dest
  1000. @lru_cache
  1001. def wrapped_kittens() -> str:
  1002. with open('shell-integration/ssh/kitty') as f:
  1003. for line in f:
  1004. if line.startswith(' wrapped_kittens="'):
  1005. val = line.strip().partition('"')[2][:-1]
  1006. return ' '.join(sorted(filter(None, val.split())))
  1007. raise Exception('Failed to read wrapped kittens from kitty wrapper script')
  1008. def build(args: Options, native_optimizations: bool = True, call_init: bool = True) -> None:
  1009. if call_init:
  1010. init_env_from_args(args, native_optimizations)
  1011. sources, headers = find_c_files()
  1012. headers.append(build_ref_map(args.skip_code_generation))
  1013. headers.append(build_cli_parser_specs(args.skip_code_generation))
  1014. headers.append(build_uniforms_header(args.skip_code_generation))
  1015. compile_c_extension(
  1016. kitty_env(args), 'kitty/fast_data_types', args.compilation_database, sources, headers,
  1017. build_dsym=args.build_dsym,
  1018. )
  1019. compile_glfw(args.compilation_database, args.build_dsym)
  1020. compile_kittens(args)
  1021. add_builtin_fonts(args)
  1022. def safe_makedirs(path: str) -> None:
  1023. os.makedirs(path, exist_ok=True)
  1024. def update_go_generated_files(args: Options, kitty_exe: str) -> None:
  1025. if args.skip_code_generation:
  1026. print('Skipping generation of Go files due to command line option', flush=True)
  1027. return
  1028. # update all the various auto-generated go files, if needed
  1029. if args.verbose:
  1030. print('Updating Go generated files...', flush=True)
  1031. env = os.environ.copy()
  1032. env['ASAN_OPTIONS'] = 'detect_leaks=0'
  1033. cp = subprocess.run([kitty_exe, '+launch', os.path.join(src_base, 'gen/go_code.py')], stdout=subprocess.DEVNULL, env=env)
  1034. if cp.returncode != 0:
  1035. if os.environ.get('CI') == 'true' and cp.returncode < 0 and shutil.which('coredumpctl'):
  1036. subprocess.run(['sh', '-c', 'echo bt | coredumpctl debug'])
  1037. raise SystemExit(f'Generating go code failed with exit code: {cp.returncode}')
  1038. def parse_go_version(x: str) -> Tuple[int, int, int]:
  1039. def safe_int(x: str) -> int:
  1040. with suppress(ValueError):
  1041. return int(x)
  1042. return int(re.split(r'[-a-zA-Z]', x)[0])
  1043. ans = list(map(safe_int, x.split('.')))
  1044. while len(ans) < 3:
  1045. ans.append(0)
  1046. return ans[0], ans[1], ans[2]
  1047. def build_static_kittens(
  1048. args: Options, launcher_dir: str, destination_dir: str = '', for_freeze: bool = False,
  1049. for_platform: Optional[Tuple[str, str]] = None
  1050. ) -> str:
  1051. sys.stdout.flush()
  1052. sys.stderr.flush()
  1053. go = shutil.which('go')
  1054. if not go:
  1055. raise SystemExit('The go tool was not found on this system. Install Go')
  1056. required_go_version = subprocess.check_output([go] + 'list -f {{.GoVersion}} -m'.split(), env=dict(os.environ, GO111MODULE="on")).decode().strip()
  1057. go_version_raw = subprocess.check_output([go, 'version']).decode().strip().split()
  1058. if go_version_raw[2] != "devel":
  1059. current_go_version = go_version_raw[2][2:]
  1060. else:
  1061. current_go_version = go_version_raw[3][2:]
  1062. if parse_go_version(required_go_version) > parse_go_version(current_go_version):
  1063. raise SystemExit(f'The version of go on this system ({current_go_version}) is too old. go >= {required_go_version} is needed')
  1064. if not for_platform:
  1065. update_go_generated_files(args, os.path.join(launcher_dir, appname))
  1066. if args.skip_building_kitten:
  1067. print('Skipping building of the kitten binary because of a command line option. Build is incomplete', file=sys.stderr)
  1068. return ''
  1069. cmd = [go, 'build', '-v']
  1070. vcs_rev = args.vcs_rev or get_vcs_rev()
  1071. ld_flags: List[str] = []
  1072. binary_data_flags = [f"-X kitty.VCSRevision={vcs_rev}"]
  1073. if for_freeze:
  1074. binary_data_flags.append("-X kitty.IsFrozenBuild=true")
  1075. if for_platform:
  1076. binary_data_flags.append("-X kitty.IsStandaloneBuild=true")
  1077. if not args.debug:
  1078. ld_flags.append('-s')
  1079. ld_flags.append('-w')
  1080. cmd += ['-ldflags', ' '.join(binary_data_flags + ld_flags)]
  1081. dest = os.path.join(destination_dir or launcher_dir, 'kitten')
  1082. if for_platform:
  1083. dest += f'-{for_platform[0]}-{for_platform[1]}'
  1084. src = os.path.abspath('tools/cmd')
  1085. def run_one(dest: str) -> None:
  1086. c = cmd + ['-o', dest, src]
  1087. if args.verbose:
  1088. print(shlex.join(c))
  1089. e = os.environ.copy()
  1090. # https://github.com/kovidgoyal/kitty/issues/6051#issuecomment-1441369828
  1091. e.pop('PWD', None)
  1092. if for_platform:
  1093. e['CGO_ENABLED'] = '0'
  1094. e['GOOS'] = for_platform[0]
  1095. e['GOARCH'] = for_platform[1]
  1096. elif args.building_arch:
  1097. e['GOARCH'] = {'x86_64': 'amd64', 'arm64': 'arm64'}[args.building_arch]
  1098. cp = subprocess.run(c, env=e)
  1099. if cp.returncode != 0:
  1100. raise SystemExit(cp.returncode)
  1101. if is_macos and for_freeze and not for_platform:
  1102. adests = []
  1103. for arch in macos_universal_arches:
  1104. args.building_arch = arch
  1105. adest = dest + '-' + arch
  1106. adests.append(adest)
  1107. run_one(adest)
  1108. lipo({dest: adests})
  1109. else:
  1110. run_one(dest)
  1111. return dest
  1112. def build_static_binaries(args: Options, launcher_dir: str) -> None:
  1113. arches = 'amd64', 'arm64'
  1114. for os_, arches_ in {
  1115. 'darwin': arches, 'linux': arches + ('arm', '386'), 'freebsd': arches, 'netbsd': arches, 'openbsd': arches,
  1116. 'dragonfly': ('amd64',),
  1117. }.items():
  1118. for arch in arches_:
  1119. print('Cross compiling static kitten for:', os_, arch)
  1120. build_static_kittens(args, launcher_dir, args.dir_for_static_binaries, for_platform=(os_, arch))
  1121. def read_bool_options(path: str = 'kitty/cli.py') -> Tuple[str, ...]:
  1122. with open(os.path.join(src_base, path)) as f:
  1123. raw = f.read()
  1124. m = re.search(r"^\s*OPTIONS = r?'''(.+?)'''", raw, flags=re.MULTILINE | re.DOTALL)
  1125. assert m is not None
  1126. ans: List[str] = []
  1127. in_option: List[str] = []
  1128. prev_line_was_blank = False
  1129. for line in m.group(1).splitlines():
  1130. if in_option:
  1131. is_blank = not line.strip()
  1132. if is_blank:
  1133. if prev_line_was_blank:
  1134. in_option = []
  1135. prev_line_was_blank = is_blank
  1136. if line.startswith('type=bool-'):
  1137. ans.extend(x.lstrip('-') for x in in_option)
  1138. else:
  1139. if line.startswith('-'):
  1140. in_option = line.strip().split()
  1141. return tuple(ans)
  1142. def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 'source') -> str:
  1143. werror = '' if args.ignore_compiler_warnings else '-pedantic-errors -Werror'
  1144. cflags = f'-Wall {werror} -fpie {c_std}'.strip().split()
  1145. cppflags = [define(f'WRAPPED_KITTENS=" {wrapped_kittens()} "')]
  1146. ldflags = shlex.split(os.environ.get('LDFLAGS', ''))
  1147. xxhash = xxhash_flags()
  1148. cppflags.extend(xxhash[0])
  1149. libs: list[str] = xxhash[1]
  1150. if args.profile or args.sanitize:
  1151. cflags.append('-g3')
  1152. if args.sanitize:
  1153. sanitize_args = get_sanitize_args(env.cc, env.ccver)
  1154. cflags.extend(sanitize_args)
  1155. ldflags.extend(sanitize_args)
  1156. libs += ['-lasan'] if not is_macos and env.compiler_type is not CompilerType.clang else []
  1157. if args.profile:
  1158. libs.append('-lprofiler')
  1159. else:
  1160. cflags.append('-g3' if args.debug else '-O3')
  1161. if bundle_type.endswith('-freeze'):
  1162. cppflags.append('-DFOR_BUNDLE')
  1163. cppflags.append(f'-DPYVER="{sysconfig.get_python_version()}"')
  1164. cppflags.append(f'-DKITTY_LIB_DIR_NAME="{args.libdir_name}"')
  1165. elif bundle_type == 'source':
  1166. cppflags.append('-DFROM_SOURCE')
  1167. elif bundle_type == 'develop':
  1168. cppflags.append('-DFROM_SOURCE')
  1169. ph = os.path.relpath(os.environ["DEVELOP_ROOT"], '.')
  1170. cppflags.append(f'-DSET_PYTHON_HOME="{ph}"')
  1171. if not is_macos:
  1172. ldflags += ['-Wl,--disable-new-dtags', f'-Wl,-rpath,$ORIGIN/../../{ph}/lib']
  1173. if bundle_type.startswith('macos-'):
  1174. klp = '../Resources/kitty'
  1175. elif bundle_type.startswith('linux-'):
  1176. klp = '../{}/kitty'.format(args.libdir_name.strip('/'))
  1177. elif bundle_type == 'source':
  1178. klp = os.path.relpath('.', launcher_dir)
  1179. elif bundle_type == 'develop':
  1180. # make the kitty executable relocatable
  1181. klp = src_base
  1182. else:
  1183. raise SystemExit(f'Unknown bundle type: {bundle_type}')
  1184. cppflags.append(f'-DKITTY_LIB_PATH="{klp}"')
  1185. pylib = get_python_flags(args, cflags, for_main_executable=True)
  1186. cppflags += shlex.split(os.environ.get('CPPFLAGS', ''))
  1187. cflags += shlex.split(os.environ.get('CFLAGS', ''))
  1188. for path in args.extra_include_dirs:
  1189. cflags.append(f'-I{path}')
  1190. if args.building_arch:
  1191. set_arches(cflags, args.building_arch)
  1192. set_arches(ldflags, args.building_arch)
  1193. if bundle_type == 'linux-freeze':
  1194. # --disable-new-dtags prevents -rpath from generating RUNPATH instead of
  1195. # RPATH entries in the launcher. The ld dynamic linker does not search
  1196. # RUNPATH locations for transitive dependencies, unlike RPATH.
  1197. ldflags += ['-Wl,--disable-new-dtags', '-Wl,-rpath,$ORIGIN/../lib']
  1198. os.makedirs(launcher_dir, exist_ok=True)
  1199. os.makedirs(build_dir, exist_ok=True)
  1200. objects = []
  1201. headers = glob.glob('kitty/launcher/*.h')
  1202. cppflags.append('-DKITTY_VERSION="' + '.'.join(map(str, version)) + '"')
  1203. for src in ('kitty/launcher/main.c', 'kitty/launcher/single-instance.c', 'kitty/launcher/cmdline.c'):
  1204. obj = os.path.join(build_dir, src.replace('/', '-').replace('.c', '.o'))
  1205. objects.append(obj)
  1206. cmd = env.cc + cppflags + cflags + ['-c', src, '-o', obj]
  1207. key = CompileKey(src, os.path.basename(obj))
  1208. args.compilation_database.add_command(
  1209. f'Compiling {emphasis(src)} ...', cmd, partial(newer, obj, src, *dependecies_for(src, obj, headers)), key=key, keyfile=src)
  1210. dest = kitty_exe = os.path.join(launcher_dir, 'kitty')
  1211. link_targets.append(os.path.abspath(dest))
  1212. desc = f'Linking {emphasis("launcher")} ...'
  1213. cmd = env.cc + ldflags + objects + libs + pylib + ['-o', dest]
  1214. args.compilation_database.add_command(desc, cmd, partial(newer, dest, *objects), key=LinkKey('kitty'))
  1215. if args.build_dsym and is_macos:
  1216. desc = f'Linking dSYM {emphasis("launcher")} ...'
  1217. dsym = f'{dest}.dSYM/Contents/Resources/DWARF/{os.path.basename(dest)}'
  1218. args.compilation_database.add_command(desc, ['dsymutil', dest], partial(newer, dsym, dest), key=LinkKey(dsym), is_post_link=True)
  1219. args.compilation_database.build_all()
  1220. return kitty_exe
  1221. # Packaging {{{
  1222. def copy_man_pages(ddir: str) -> None:
  1223. mandir = os.path.join(ddir, 'share', 'man')
  1224. safe_makedirs(mandir)
  1225. man_levels = '15'
  1226. with suppress(FileNotFoundError):
  1227. for x in man_levels:
  1228. shutil.rmtree(os.path.join(mandir, f'man{x}'))
  1229. src = 'docs/_build/man'
  1230. if not os.path.exists(src):
  1231. raise SystemExit('''\
  1232. The kitty man pages are missing. If you are building from git then run:
  1233. make && make docs
  1234. (needs the sphinx documentation system to be installed)
  1235. ''')
  1236. for x in man_levels:
  1237. os.makedirs(os.path.join(mandir, f'man{x}'))
  1238. for y in glob.glob(os.path.join(src, f'*.{x}')):
  1239. shutil.copy2(y, os.path.join(mandir, f'man{x}'))
  1240. def copy_html_docs(ddir: str) -> None:
  1241. htmldir = os.path.join(ddir, 'share', 'doc', appname, 'html')
  1242. safe_makedirs(os.path.dirname(htmldir))
  1243. with suppress(FileNotFoundError):
  1244. shutil.rmtree(htmldir)
  1245. src = 'docs/_build/html'
  1246. if not os.path.exists(src):
  1247. raise SystemExit('''\
  1248. The kitty html docs are missing. If you are building from git then run:
  1249. make && make docs
  1250. (needs the sphinx documentation system to be installed)
  1251. ''')
  1252. shutil.copytree(src, htmldir)
  1253. def compile_python(base_path: str) -> None:
  1254. import compileall
  1255. import py_compile
  1256. for root, dirs, files in os.walk(base_path):
  1257. for f in files:
  1258. if f.rpartition('.')[-1] in ('pyc', 'pyo'):
  1259. os.remove(os.path.join(root, f))
  1260. exclude = re.compile('.*/shell-integration/ssh/bootstrap.py')
  1261. compileall.compile_dir(
  1262. base_path, rx=exclude, force=True, optimize=(0, 1, 2), quiet=1, workers=0, # type: ignore
  1263. invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH, ddir='')
  1264. def create_linux_bundle_gunk(ddir: str, args: Options) -> None:
  1265. libdir_name = args.libdir_name
  1266. base = Path(ddir)
  1267. in_src_launcher = base / (f'{libdir_name}/kitty/kitty/launcher/kitty')
  1268. launcher = base / 'bin/kitty'
  1269. skip_docs = False
  1270. if not os.path.exists('docs/_build/html'):
  1271. kitten_exe = os.path.join(os.path.dirname(str(launcher)), 'kitten')
  1272. if os.path.exists(kitten_exe):
  1273. os.environ['KITTEN_EXE_FOR_DOCS'] = kitten_exe
  1274. make = 'gmake' if is_freebsd else 'make'
  1275. run_tool([make, 'docs'])
  1276. else:
  1277. if args.skip_building_kitten:
  1278. skip_docs = True
  1279. print('WARNING: You have chosen to skip building kitten.'
  1280. ' This means docs could not be generated and will not be included in the linux package.'
  1281. ' You should build kitten and then re-run this build.', file=sys.stderr)
  1282. else:
  1283. raise SystemExit(f'kitten binary not found at: {kitten_exe}')
  1284. if not skip_docs:
  1285. copy_man_pages(ddir)
  1286. copy_html_docs(ddir)
  1287. for (icdir, ext) in {'256x256': 'png', 'scalable': 'svg'}.items():
  1288. icdir = os.path.join(ddir, 'share', 'icons', 'hicolor', icdir, 'apps')
  1289. safe_makedirs(icdir)
  1290. shutil.copy2(f'logo/kitty.{ext}', icdir)
  1291. deskdir = os.path.join(ddir, 'share', 'applications')
  1292. safe_makedirs(deskdir)
  1293. with open(os.path.join(deskdir, 'kitty.desktop'), 'w') as f:
  1294. f.write(
  1295. '''\
  1296. [Desktop Entry]
  1297. Version=1.0
  1298. Type=Application
  1299. Name=kitty
  1300. GenericName=Terminal emulator
  1301. Comment=Fast, feature-rich, GPU based terminal
  1302. TryExec=kitty
  1303. StartupNotify=true
  1304. Exec=kitty
  1305. Icon=kitty
  1306. Categories=System;TerminalEmulator;
  1307. X-TerminalArgExec=--
  1308. X-TerminalArgTitle=--title
  1309. X-TerminalArgAppId=--class
  1310. X-TerminalArgDir=--working-directory
  1311. X-TerminalArgHold=--hold
  1312. ''')
  1313. with open(os.path.join(deskdir, 'kitty-open.desktop'), 'w') as f:
  1314. f.write(
  1315. '''\
  1316. [Desktop Entry]
  1317. Version=1.0
  1318. Type=Application
  1319. Name=kitty URL Launcher
  1320. GenericName=Terminal emulator
  1321. Comment=Open URLs with kitty
  1322. StartupNotify=true
  1323. TryExec=kitty
  1324. Exec=kitty +open %U
  1325. Icon=kitty
  1326. Categories=System;TerminalEmulator;
  1327. NoDisplay=true
  1328. MimeType=image/*;application/x-sh;application/x-shellscript;inode/directory;text/*;x-scheme-handler/kitty;x-scheme-handler/ssh;
  1329. ''')
  1330. if os.path.exists(in_src_launcher):
  1331. os.remove(in_src_launcher)
  1332. os.makedirs(os.path.dirname(in_src_launcher), exist_ok=True)
  1333. os.symlink(os.path.relpath(launcher, os.path.dirname(in_src_launcher)), in_src_launcher)
  1334. def macos_info_plist(for_quake: str = '') -> bytes:
  1335. import plistlib
  1336. VERSION = '.'.join(map(str, version))
  1337. def access(what: str, verb: str = 'would like to access') -> str:
  1338. return f'A program running inside kitty {verb} {what}'
  1339. docs = [] if for_quake else [
  1340. {
  1341. 'CFBundleTypeName': 'Terminal scripts',
  1342. 'CFBundleTypeExtensions': ['command', 'sh', 'zsh', 'bash', 'fish', 'tool'],
  1343. 'CFBundleTypeIconFile': f'{appname}.icns',
  1344. 'CFBundleTypeRole': 'Editor',
  1345. },
  1346. {
  1347. 'CFBundleTypeName': 'Folders',
  1348. 'LSItemContentTypes': ['public.directory'],
  1349. 'CFBundleTypeRole': 'Editor',
  1350. 'LSHandlerRank': 'Alternate',
  1351. },
  1352. {
  1353. 'LSItemContentTypes': ['public.unix-executable'],
  1354. 'CFBundleTypeRole': 'Shell',
  1355. },
  1356. {
  1357. 'CFBundleTypeName': 'Text files',
  1358. 'LSItemContentTypes': ['public.text'],
  1359. 'CFBundleTypeRole': 'Editor',
  1360. 'LSHandlerRank': 'Alternate',
  1361. },
  1362. {
  1363. 'CFBundleTypeName': 'Image files',
  1364. 'LSItemContentTypes': ['public.image'],
  1365. 'CFBundleTypeRole': 'Viewer',
  1366. 'LSHandlerRank': 'Alternate',
  1367. },
  1368. # Allows dragging arbitrary files to kitty Dock icon, and list kitty in the Open With context menu.
  1369. {
  1370. 'CFBundleTypeName': 'All files',
  1371. 'LSItemContentTypes': ['public.archive', 'public.content', 'public.data'],
  1372. 'CFBundleTypeRole': 'Editor',
  1373. 'LSHandlerRank': 'Alternate',
  1374. },
  1375. ]
  1376. url_schemes = [] if for_quake else [
  1377. {
  1378. 'CFBundleURLName': 'File URL',
  1379. 'CFBundleURLSchemes': ['file'],
  1380. },
  1381. {
  1382. 'CFBundleURLName': 'FTP URL',
  1383. 'CFBundleURLSchemes': ['ftp', 'ftps'],
  1384. },
  1385. {
  1386. 'CFBundleURLName': 'Gemini URL',
  1387. 'CFBundleURLSchemes': ['gemini'],
  1388. },
  1389. {
  1390. 'CFBundleURLName': 'Git URL',
  1391. 'CFBundleURLSchemes': ['git'],
  1392. },
  1393. {
  1394. 'CFBundleURLName': 'Gopher URL',
  1395. 'CFBundleURLSchemes': ['gopher'],
  1396. },
  1397. {
  1398. 'CFBundleURLName': 'HTTP URL',
  1399. 'CFBundleURLSchemes': ['http', 'https'],
  1400. },
  1401. {
  1402. 'CFBundleURLName': 'IRC URL',
  1403. 'CFBundleURLSchemes': ['irc', 'irc6', 'ircs'],
  1404. },
  1405. {
  1406. 'CFBundleURLName': 'kitty URL',
  1407. 'CFBundleURLSchemes': ['kitty'],
  1408. 'LSHandlerRank': 'Owner',
  1409. 'LSIsAppleDefaultForScheme': True,
  1410. },
  1411. {
  1412. 'CFBundleURLName': 'Mail Address URL',
  1413. 'CFBundleURLSchemes': ['mailto'],
  1414. },
  1415. {
  1416. 'CFBundleURLName': 'News URL',
  1417. 'CFBundleURLSchemes': ['news', 'nntp'],
  1418. },
  1419. {
  1420. 'CFBundleURLName': 'SSH and SFTP URL',
  1421. 'CFBundleURLSchemes': ['ssh', 'sftp'],
  1422. },
  1423. {
  1424. 'CFBundleURLName': 'Telnet URL',
  1425. 'CFBundleURLSchemes': ['telnet'],
  1426. },
  1427. ]
  1428. services = [
  1429. {
  1430. 'NSMenuItem': {'default': for_quake},
  1431. 'NSMessage': 'quickAccessTerminal',
  1432. 'NSRequiredContext': {'NSServiceCategory': 'None'},
  1433. },
  1434. ] if for_quake else [
  1435. {
  1436. 'NSMenuItem': {'default': f'New {appname} Tab Here'},
  1437. 'NSMessage': 'openTab',
  1438. 'NSRequiredContext': {'NSTextContent': 'FilePath'},
  1439. 'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
  1440. },
  1441. {
  1442. 'NSMenuItem': {'default': f'New {appname} Window Here'},
  1443. 'NSMessage': 'openOSWindow',
  1444. 'NSRequiredContext': {'NSTextContent': 'FilePath'},
  1445. 'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
  1446. },
  1447. {
  1448. 'NSMenuItem': {'default': f'Open with {appname}'},
  1449. 'NSMessage': 'openFileURLs',
  1450. 'NSRequiredContext': {'NSTextContent': 'FilePath'},
  1451. 'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
  1452. },
  1453. ]
  1454. pl = dict(
  1455. # Naming
  1456. CFBundleName=f'{appname}-quick-access' if for_quake else appname,
  1457. CFBundleDisplayName=f'{appname}-quick-access' if for_quake else appname,
  1458. # Identification
  1459. CFBundleIdentifier=f'net.kovidgoyal.{appname}' + ('-quick-access' if for_quake else ''),
  1460. # Bundle Version Info
  1461. CFBundleVersion=VERSION,
  1462. CFBundleShortVersionString=VERSION,
  1463. CFBundleInfoDictionaryVersion='6.0',
  1464. NSHumanReadableCopyright=time.strftime('Copyright %Y, Kovid Goyal'),
  1465. CFBundleGetInfoString='kitty - The fast, feature-rich, GPU based terminal emulator. https://sw.kovidgoyal.net/kitty/',
  1466. # Operating System Version
  1467. LSMinimumSystemVersion='11.0.0',
  1468. # Categorization
  1469. CFBundlePackageType='APPL',
  1470. CFBundleSignature='????',
  1471. LSApplicationCategoryType='public.app-category.utilities',
  1472. # App Execution
  1473. CFBundleExecutable=quake_name if for_quake else appname,
  1474. LSEnvironment={'KITTY_LAUNCHED_BY_LAUNCH_SERVICES': '1'},
  1475. LSRequiresNativeExecution=True,
  1476. NSSupportsSuddenTermination=False,
  1477. # Localization
  1478. # see https://github.com/kovidgoyal/kitty/issues/1233
  1479. CFBundleDevelopmentRegion='English',
  1480. CFBundleAllowMixedLocalizations=True,
  1481. TICapsLockLanguageSwitchCapable=True,
  1482. # User Interface and Graphics
  1483. CFBundleIconFile=f'{appname}.icns',
  1484. NSHighResolutionCapable=True,
  1485. NSSupportsAutomaticGraphicsSwitching=True,
  1486. # Needed for dark mode in Mojave when linking against older SDKs
  1487. NSRequiresAquaSystemAppearance='NO',
  1488. # Document and URL Types
  1489. CFBundleDocumentTypes=docs,
  1490. CFBundleURLTypes=url_schemes,
  1491. # Services
  1492. NSServices=services,
  1493. # Calendar and Reminders
  1494. NSCalendarsUsageDescription=access('your calendar data.'),
  1495. NSRemindersUsageDescription=access('your reminders.'),
  1496. # Camera and Microphone
  1497. NSCameraUsageDescription=access('the camera.'),
  1498. NSMicrophoneUsageDescription=access('the microphone.'),
  1499. # Contacts
  1500. NSContactsUsageDescription=access('your contacts.'),
  1501. # Location
  1502. NSLocationUsageDescription=access('your location information.'),
  1503. NSLocationTemporaryUsageDescriptionDictionary=access('your location temporarily.'),
  1504. # Motion
  1505. NSMotionUsageDescription=access('motion data.'),
  1506. # Networking
  1507. NSLocalNetworkUsageDescription=access('local network.'),
  1508. # Photos
  1509. NSPhotoLibraryUsageDescription=access('your photo library.'),
  1510. # Scripting
  1511. NSAppleScriptEnabled=False,
  1512. # Security
  1513. NSAppleEventsUsageDescription=access('AppleScript.'),
  1514. NSSystemAdministrationUsageDescription=access('elevated privileges.', 'requires'),
  1515. NSBluetoothAlwaysUsageDescription=access('Bluetooth.'),
  1516. # Speech
  1517. NSSpeechRecognitionUsageDescription=access('speech recognition.'),
  1518. )
  1519. if for_quake:
  1520. # exclude from dock and menubar
  1521. pl['LSBackgroundOnly'] = True
  1522. return plistlib.dumps(pl)
  1523. def create_macos_app_icon(where: str = 'Resources') -> None:
  1524. iconset_dir = os.path.abspath(os.path.join('logo', f'{appname}.iconset'))
  1525. icns_dir = os.path.join(where, f'{appname}.icns')
  1526. try:
  1527. subprocess.check_call([
  1528. 'iconutil', '-c', 'icns', iconset_dir, '-o', icns_dir
  1529. ])
  1530. except FileNotFoundError:
  1531. print(f'{error("iconutil not found")}, using png2icns (without retina support) to convert the logo', file=sys.stderr)
  1532. subprocess.check_call([
  1533. 'png2icns', icns_dir
  1534. ] + [os.path.join(iconset_dir, logo) for logo in [
  1535. # png2icns does not support retina icons, so only pass the non-retina icons
  1536. 'icon_16x16.png',
  1537. 'icon_32x32.png',
  1538. 'icon_128x128.png',
  1539. 'icon_256x256.png',
  1540. 'icon_512x512.png',
  1541. ]])
  1542. quake_name = f'{appname}-quick-access'
  1543. def create_quick_access_bundle(kapp: str, quake_desc: str = 'Quick access to kitty') -> None:
  1544. qapp = os.path.join(kapp, 'Contents', f'{quake_name}.app')
  1545. base_exe_dir = os.path.join(kapp, 'Contents/MacOS')
  1546. if os.path.exists(qapp):
  1547. shutil.rmtree(qapp)
  1548. bin_dir = os.path.join(qapp, 'Contents/MacOS')
  1549. os.makedirs(bin_dir)
  1550. with open(os.path.join(qapp, 'Contents/Info.plist'), 'wb') as f:
  1551. f.write(macos_info_plist(quake_desc))
  1552. for exe in os.listdir(base_exe_dir):
  1553. os.symlink(f'../../../MacOS/{exe}', os.path.join(bin_dir, exe))
  1554. base_exe = os.path.join(base_exe_dir, 'kitty')
  1555. if os.path.exists(base_exe): # during freeze launcher is built after bundle is created
  1556. shutil.copy2(base_exe, os.path.join(bin_dir, quake_name))
  1557. for x in ('Frameworks', 'Resources'):
  1558. os.symlink(f'../../{x}', os.path.join(qapp, 'Contents', x))
  1559. def create_minimal_macos_bundle(args: Options, launcher_dir: str, relocate: bool = False) -> None:
  1560. kapp = os.path.join(launcher_dir, 'kitty.app')
  1561. if os.path.exists(kapp):
  1562. shutil.rmtree(kapp)
  1563. bin_dir = os.path.join(kapp, 'Contents/MacOS')
  1564. resources_dir = os.path.join(kapp, 'Contents/Resources')
  1565. os.makedirs(resources_dir)
  1566. os.makedirs(bin_dir)
  1567. with open(os.path.join(kapp, 'Contents/Info.plist'), 'wb') as f:
  1568. f.write(macos_info_plist())
  1569. if relocate:
  1570. shutil.copy2(os.path.join(launcher_dir, "kitty"), bin_dir)
  1571. shutil.copy2(os.path.join(launcher_dir, "kitten"), bin_dir)
  1572. else:
  1573. build_launcher(args, bin_dir)
  1574. build_static_kittens(args, launcher_dir=bin_dir)
  1575. kitty_exe = os.path.join(launcher_dir, appname)
  1576. with suppress(FileNotFoundError):
  1577. os.remove(kitty_exe)
  1578. os.symlink(os.path.join(os.path.relpath(bin_dir, launcher_dir), appname), kitty_exe)
  1579. create_macos_app_icon(resources_dir)
  1580. create_quick_access_bundle(kapp, 'Quick access to kitty built from source')
  1581. def create_macos_bundle_gunk(dest: str, for_freeze: bool, args: Options) -> str:
  1582. ddir = Path(dest)
  1583. os.mkdir(ddir / 'Contents')
  1584. with open(ddir / 'Contents/Info.plist', 'wb') as fp:
  1585. fp.write(macos_info_plist())
  1586. copy_man_pages(str(ddir))
  1587. copy_html_docs(str(ddir))
  1588. os.rename(ddir / 'share', ddir / 'Contents/Resources')
  1589. os.rename(ddir / 'bin', ddir / 'Contents/MacOS')
  1590. os.rename(ddir / 'lib', ddir / 'Contents/Frameworks')
  1591. os.rename(ddir / 'Contents/Frameworks/kitty', ddir / 'Contents/Resources/kitty')
  1592. kitty_exe = ddir / 'Contents/MacOS/kitty'
  1593. in_src_launcher = ddir / 'Contents/Resources/kitty/kitty/launcher/kitty'
  1594. if os.path.exists(in_src_launcher):
  1595. os.remove(in_src_launcher)
  1596. os.makedirs(os.path.dirname(in_src_launcher), exist_ok=True)
  1597. os.symlink(os.path.relpath(kitty_exe, os.path.dirname(in_src_launcher)), in_src_launcher)
  1598. create_macos_app_icon(os.path.join(ddir, 'Contents', 'Resources'))
  1599. if not for_freeze:
  1600. kitten_exe = build_static_kittens(args, launcher_dir=os.path.dirname(kitty_exe))
  1601. if not kitten_exe:
  1602. raise SystemExit('kitten not built cannot create macOS bundle')
  1603. os.symlink(os.path.relpath(kitten_exe, os.path.dirname(in_src_launcher)),
  1604. os.path.join(os.path.dirname(in_src_launcher), os.path.basename(kitten_exe)))
  1605. create_quick_access_bundle(dest)
  1606. return str(kitty_exe)
  1607. def package(args: Options, bundle_type: str, do_build_all: bool = True) -> None:
  1608. ddir = args.prefix
  1609. for_freeze = bundle_type.endswith('-freeze')
  1610. if bundle_type == 'linux-freeze':
  1611. args.libdir_name = 'lib'
  1612. libdir = os.path.join(ddir, args.libdir_name.strip('/'), 'kitty')
  1613. if os.path.exists(libdir):
  1614. shutil.rmtree(libdir)
  1615. launcher_dir = os.path.join(ddir, 'bin')
  1616. safe_makedirs(launcher_dir)
  1617. if for_freeze: # freeze launcher is built separately
  1618. if do_build_all:
  1619. args.compilation_database.build_all()
  1620. else:
  1621. build_launcher(args, launcher_dir, bundle_type)
  1622. os.makedirs(os.path.join(libdir, 'logo'))
  1623. build_terminfo = runpy.run_path('build-terminfo', run_name='import_build')
  1624. for x in (libdir, os.path.join(ddir, 'share')):
  1625. odir = os.path.join(x, 'terminfo')
  1626. safe_makedirs(odir)
  1627. build_terminfo['compile_terminfo'](odir)
  1628. shutil.copy2('terminfo/kitty.terminfo', os.path.join(libdir, 'terminfo'))
  1629. shutil.copy2('terminfo/kitty.termcap', os.path.join(libdir, 'terminfo'))
  1630. shutil.copy2('__main__.py', libdir)
  1631. shutil.copy2('logo/kitty-128.png', os.path.join(libdir, 'logo'))
  1632. shutil.copy2('logo/kitty.png', os.path.join(libdir, 'logo'))
  1633. shutil.copy2('logo/beam-cursor.png', os.path.join(libdir, 'logo'))
  1634. shutil.copy2('logo/beam-cursor@2x.png', os.path.join(libdir, 'logo'))
  1635. shutil.copytree('shell-integration', os.path.join(libdir, 'shell-integration'), dirs_exist_ok=True)
  1636. shutil.copytree('fonts', os.path.join(libdir, 'fonts'), dirs_exist_ok=True)
  1637. allowed_extensions = frozenset('py glsl so'.split())
  1638. def src_ignore(parent: str, entries: Iterable[str]) -> List[str]:
  1639. return [
  1640. x for x in entries
  1641. if '.' in x and x.rpartition('.')[2] not in
  1642. allowed_extensions
  1643. ]
  1644. shutil.copytree('kitty', os.path.join(libdir, 'kitty'), ignore=src_ignore)
  1645. shutil.copytree('kittens', os.path.join(libdir, 'kittens'), ignore=src_ignore)
  1646. if for_freeze:
  1647. shutil.copytree('kitty_tests', os.path.join(libdir, 'kitty_tests'))
  1648. def repl(name: str, raw: str, defval: Union[str, float, FrozenSet[str]], val: Union[str, float, FrozenSet[str]]) -> str:
  1649. if defval == val:
  1650. return raw
  1651. tname = type(defval).__name__
  1652. if tname == 'frozenset':
  1653. tname = 'frozenset[str]'
  1654. prefix = f'{name}: {tname} ='
  1655. nraw = raw.replace(f'{prefix} {defval!r}', f'{prefix} {val!r}', 1)
  1656. if nraw == raw:
  1657. raise SystemExit(f'Failed to change the value of {name}')
  1658. return nraw
  1659. with open(os.path.join(libdir, 'kitty/options/types.py'), 'r+', encoding='utf-8') as f:
  1660. oraw = raw = f.read()
  1661. raw = repl('update_check_interval', raw, Options.update_check_interval, args.update_check_interval)
  1662. raw = repl('shell_integration', raw, frozenset(Options.shell_integration.split()), frozenset(args.shell_integration.split()))
  1663. if raw != oraw:
  1664. f.seek(0), f.truncate(), f.write(raw)
  1665. compile_python(libdir)
  1666. def should_be_executable(path: str) -> bool:
  1667. if path.endswith('.so'):
  1668. return True
  1669. q = path.split(os.sep)[-2:]
  1670. if len(q) == 2 and q[0] == 'ssh' and q[1] in ('kitty', 'kitten'):
  1671. return True
  1672. return False
  1673. for root, dirs, files in os.walk(libdir):
  1674. for f_ in files:
  1675. path = os.path.join(root, f_)
  1676. os.chmod(path, 0o755 if should_be_executable(path) else 0o644)
  1677. if not for_freeze and not bundle_type.startswith('macos-'):
  1678. build_static_kittens(args, launcher_dir=launcher_dir)
  1679. if not is_macos:
  1680. create_linux_bundle_gunk(ddir, args)
  1681. if bundle_type.startswith('macos-'):
  1682. create_macos_bundle_gunk(ddir, for_freeze, args)
  1683. # }}}
  1684. def clean_launcher_dir(launcher_dir: str) -> None:
  1685. for x in glob.glob(os.path.join(launcher_dir, 'kitt*')):
  1686. if os.path.isdir(x):
  1687. shutil.rmtree(x)
  1688. else:
  1689. os.remove(x)
  1690. def clean(for_cross_compile: bool = False) -> None:
  1691. def safe_remove(*entries: str) -> None:
  1692. for x in entries:
  1693. if os.path.exists(x):
  1694. if os.path.isdir(x):
  1695. shutil.rmtree(x)
  1696. else:
  1697. os.unlink(x)
  1698. safe_remove(
  1699. 'build', 'compile_commands.json', 'link_commands.json',
  1700. 'linux-package', 'kitty.app', 'asan-launcher',
  1701. 'kitty-profile') # no fonts as that is not generated by build
  1702. if not for_cross_compile:
  1703. safe_remove('docs/generated')
  1704. clean_launcher_dir('kitty/launcher')
  1705. def excluded(root: str, d: str) -> bool:
  1706. q = os.path.relpath(os.path.join(root, d), src_base).replace(os.sep, '/')
  1707. return q in ('.git', 'bypy/b', 'dependencies')
  1708. def is_generated(f: str) -> bool:
  1709. e = f.endswith
  1710. return (
  1711. e('_generated.h') or e('_generated.go') or e('_generated.bin') or
  1712. e('_generated.s') or e('_generated_test.s') or e('_generated_test.go')
  1713. )
  1714. for root, dirs, files in os.walk(src_base, topdown=True):
  1715. dirs[:] = [d for d in dirs if not excluded(root, d)]
  1716. remove_dirs = {d for d in dirs if d == '__pycache__' or d.endswith('.dSYM')}
  1717. for d in remove_dirs:
  1718. shutil.rmtree(os.path.join(root, d))
  1719. dirs.remove(d)
  1720. for f in files:
  1721. ext = f.rpartition('.')[-1]
  1722. if ext in ('so', 'dylib', 'pyc', 'pyo') or (not for_cross_compile and is_generated(f)):
  1723. os.unlink(os.path.join(root, f))
  1724. for x in glob.glob('glfw/wayland-*-protocol.[ch]'):
  1725. os.unlink(x)
  1726. for x in glob.glob('kittens/*'):
  1727. if os.path.isdir(x) and not os.path.exists(os.path.join(x, '__init__.py')):
  1728. shutil.rmtree(x)
  1729. subprocess.check_call(['go', 'clean', '-cache', '-testcache', '-modcache', '-fuzzcache'])
  1730. def option_parser() -> argparse.ArgumentParser: # {{{
  1731. p = argparse.ArgumentParser()
  1732. p.add_argument(
  1733. 'action',
  1734. nargs='?',
  1735. default=Options.action,
  1736. choices=('build',
  1737. 'test',
  1738. 'develop',
  1739. 'linux-package',
  1740. 'kitty.app',
  1741. 'linux-freeze',
  1742. 'macos-freeze',
  1743. 'build-launcher',
  1744. 'build-frozen-launcher',
  1745. 'build-frozen-tools',
  1746. 'clean',
  1747. 'export-ci-bundles',
  1748. 'build-dep',
  1749. 'build-static-binaries',
  1750. ),
  1751. help='Action to perform (default is build)'
  1752. )
  1753. p.add_argument(
  1754. '--debug',
  1755. default=Options.debug,
  1756. action='store_true',
  1757. help='Build extension modules with debugging symbols'
  1758. )
  1759. p.add_argument(
  1760. '-v', '--verbose',
  1761. default=Options.verbose,
  1762. action='count',
  1763. help='Be verbose'
  1764. )
  1765. p.add_argument(
  1766. '--sanitize',
  1767. default=Options.sanitize,
  1768. action='store_true',
  1769. help='Turn on sanitization to detect memory access errors and undefined behavior. This is a big performance hit.'
  1770. )
  1771. p.add_argument(
  1772. '--prefix',
  1773. default=Options.prefix,
  1774. help='Where to create the linux package'
  1775. )
  1776. p.add_argument(
  1777. '--dir-for-static-binaries',
  1778. default=Options.dir_for_static_binaries,
  1779. help='Where to create the static kitten binary'
  1780. )
  1781. p.add_argument(
  1782. '--skip-code-generation',
  1783. default=Options.skip_code_generation,
  1784. action='store_true',
  1785. help='Do not create the *_generated.* source files. This is useful if they'
  1786. ' have already been generated by a previous build, for example during a two-stage cross compilation.'
  1787. )
  1788. p.add_argument(
  1789. '--skip-building-kitten',
  1790. default=Options.skip_building_kitten,
  1791. action='store_true',
  1792. help='Do not build the kitten binary. Useful if you want to build it separately.'
  1793. )
  1794. p.add_argument(
  1795. '--clean-for-cross-compile',
  1796. default=Options.clean_for_cross_compile,
  1797. action='store_true',
  1798. help='Do not clean generated Go source files. Useful for cross-compilation.'
  1799. )
  1800. p.add_argument(
  1801. '--python-compiler-flags', default=Options.python_compiler_flags,
  1802. help='Compiler flags for compiling against Python. Typically include directives. If not set'
  1803. ' the Python used to run setup.py is queried for these.'
  1804. )
  1805. p.add_argument(
  1806. '--python-linker-flags', default=Options.python_linker_flags,
  1807. help='Linker flags for linking against Python. Typically dynamic library names and search paths directives. If not set'
  1808. ' the Python used to run setup.py is queried for these.'
  1809. )
  1810. p.add_argument(
  1811. '--full',
  1812. dest='incremental',
  1813. default=Options.incremental,
  1814. action='store_false',
  1815. help='Do a full build, even for unchanged files'
  1816. )
  1817. p.add_argument(
  1818. '--profile',
  1819. default=Options.profile,
  1820. action='store_true',
  1821. help='Use the -pg compile flag to add profiling information'
  1822. )
  1823. p.add_argument(
  1824. '--libdir-name',
  1825. default=Options.libdir_name,
  1826. help='The name of the directory inside --prefix in which to store compiled files. Defaults to "lib"'
  1827. )
  1828. p.add_argument(
  1829. '--vcs-rev', default='',
  1830. help='The VCS revision to embed in the binary. The default is to read it from the .git directory when present.'
  1831. )
  1832. p.add_argument(
  1833. '--extra-logging',
  1834. action='append',
  1835. default=Options.extra_logging,
  1836. choices=('event-loop',),
  1837. help='Turn on extra logging for debugging in this build. Can be specified multiple times, to turn'
  1838. ' on different types of logging.'
  1839. )
  1840. p.add_argument(
  1841. '--extra-include-dirs', '-I',
  1842. action='append',
  1843. default=Options.extra_include_dirs,
  1844. help='Extra include directories to use while compiling'
  1845. )
  1846. p.add_argument(
  1847. '--extra-library-dirs', '-L',
  1848. action='append',
  1849. default=Options.extra_library_dirs,
  1850. help='Extra library directories to use while linking'
  1851. )
  1852. p.add_argument(
  1853. '--update-check-interval',
  1854. type=float,
  1855. default=Options.update_check_interval,
  1856. help='When building a package, the default value for the update_check_interval setting will'
  1857. ' be set to this number. Use zero to disable update checking.'
  1858. )
  1859. p.add_argument(
  1860. '--shell-integration',
  1861. type=str,
  1862. default=Options.shell_integration,
  1863. help='When building a package, the default value for the shell_integration setting will'
  1864. ' be set to this. Use "enabled no-rc" if you intend to install the shell integration scripts system wide.'
  1865. )
  1866. p.add_argument(
  1867. '--egl-library',
  1868. type=str,
  1869. default=Options.egl_library,
  1870. help='The filename argument passed to dlopen for libEGL.'
  1871. ' This can be used to change the name of the loaded library or specify an absolute path.'
  1872. )
  1873. p.add_argument(
  1874. '--startup-notification-library',
  1875. type=str,
  1876. default=Options.startup_notification_library,
  1877. help='The filename argument passed to dlopen for libstartup-notification-1.'
  1878. ' This can be used to change the name of the loaded library or specify an absolute path.'
  1879. )
  1880. p.add_argument(
  1881. '--canberra-library',
  1882. type=str,
  1883. default=Options.canberra_library,
  1884. help='The filename argument passed to dlopen for libcanberra.'
  1885. ' This can be used to change the name of the loaded library or specify an absolute path.'
  1886. )
  1887. p.add_argument(
  1888. '--systemd-library',
  1889. type=str,
  1890. default=Options.systemd_library,
  1891. help='The filename argument passed to dlopen for libsystemd.'
  1892. ' This can be used to change the name of the loaded library or specify an absolute path.'
  1893. )
  1894. p.add_argument(
  1895. '--fontconfig-library',
  1896. type=str,
  1897. default=Options.fontconfig_library,
  1898. help='The filename argument passed to dlopen for libfontconfig.'
  1899. ' This can be used to change the name of the loaded library or specify an absolute path.'
  1900. )
  1901. p.add_argument(
  1902. '--disable-link-time-optimization',
  1903. dest='link_time_optimization',
  1904. default=Options.link_time_optimization,
  1905. action='store_false',
  1906. help='Turn off Link Time Optimization (LTO).'
  1907. )
  1908. p.add_argument(
  1909. '--ignore-compiler-warnings',
  1910. default=Options.ignore_compiler_warnings, action='store_true',
  1911. help='Ignore any warnings from the compiler while building'
  1912. )
  1913. p.add_argument(
  1914. '--build-dSYM', dest='build_dsym',
  1915. default=Options.build_dsym, action='store_true',
  1916. help='Build the dSYM bundle on macOS, ignored on other platforms'
  1917. )
  1918. return p
  1919. # }}}
  1920. def build_dep() -> None:
  1921. class Options:
  1922. platform: str = 'all'
  1923. deps: List[str] = []
  1924. p = argparse.ArgumentParser(prog=f'{sys.argv[0]} build-dep', description='Build dependencies for the kitty binary packages')
  1925. p.add_argument(
  1926. '--platform',
  1927. default=Options.platform,
  1928. choices='all macos linux linux-arm64 linux-64'.split(),
  1929. help='Platforms to build the dep for'
  1930. )
  1931. p.add_argument(
  1932. 'deps',
  1933. nargs='*',
  1934. default=Options.deps,
  1935. help='Names of the dependencies, if none provided, build all'
  1936. )
  1937. args = p.parse_args(sys.argv[2:], namespace=Options())
  1938. linux_platforms = [
  1939. ['linux', '--arch=64'],
  1940. ['linux', '--arch=arm64'],
  1941. ]
  1942. if args.platform == 'all':
  1943. platforms = linux_platforms + [['macos']]
  1944. elif args.platform == 'linux':
  1945. platforms = linux_platforms
  1946. elif args.platform == 'macos':
  1947. platforms = [['macos']]
  1948. elif '-' in args.platform:
  1949. parts = args.platform.split('-')
  1950. platforms = [[parts[0], f'--arch={parts[1]}']]
  1951. else:
  1952. raise SystemExit(f'Unknown platform: {args.platform}')
  1953. base = [sys.executable, '../bypy']
  1954. for pf in platforms:
  1955. cmd = base + pf + ['dependencies'] + args.deps
  1956. run_tool(cmd)
  1957. def lipo(target_map: Dict[str, List[str]]) -> None:
  1958. print(f'Using lipo to generate {len(target_map)} universal binaries...')
  1959. for dest, inputs in target_map.items():
  1960. cmd = ['lipo', '-create', '-output', dest] + inputs
  1961. subprocess.check_call(cmd)
  1962. for x in inputs:
  1963. os.remove(x)
  1964. def macos_freeze(args: Options, launcher_dir: str, only_frozen_launcher: bool = False) -> None:
  1965. global build_dir
  1966. # Need to build a universal binary in two stages
  1967. orig_build_dir = build_dir
  1968. link_target_map: Dict[str, List[str]] = {}
  1969. bundle_type = 'macos-freeze'
  1970. for arch in macos_universal_arches:
  1971. args.building_arch = arch
  1972. build_dir = os.path.join(orig_build_dir, arch)
  1973. os.makedirs(build_dir, exist_ok=True)
  1974. print('Building for arch:', arch, 'in', build_dir)
  1975. if arch is not macos_universal_arches[0]:
  1976. args.skip_code_generation = True # cant run kitty as its not a native arch
  1977. link_targets.clear()
  1978. with CompilationDatabase() as cdb:
  1979. args.compilation_database = cdb
  1980. init_env_from_args(args, native_optimizations=False)
  1981. if only_frozen_launcher:
  1982. kitty_exe_path = build_launcher(args, launcher_dir=launcher_dir, bundle_type=bundle_type)
  1983. else:
  1984. build_launcher(args, launcher_dir=launcher_dir)
  1985. build(args, native_optimizations=False, call_init=False)
  1986. cdb.build_all()
  1987. for x in link_targets:
  1988. arch_specific = x + '-' + arch
  1989. link_target_map.setdefault(x, []).append(arch_specific)
  1990. os.rename(x, arch_specific)
  1991. build_dir = orig_build_dir
  1992. lipo(link_target_map)
  1993. if only_frozen_launcher:
  1994. if is_macos:
  1995. shutil.copy2(kitty_exe_path, os.path.dirname(kitty_exe_path) + f'/../Contents/{quake_name}.app/Contents/MacOS/{quake_name}')
  1996. else:
  1997. package(args, bundle_type=bundle_type, do_build_all=False)
  1998. def do_build(args: Options) -> None:
  1999. launcher_dir = 'kitty/launcher'
  2000. if args.action == 'test':
  2001. texe = os.path.abspath(os.path.join(launcher_dir, 'kitty'))
  2002. os.execl(texe, texe, '+launch', 'test.py')
  2003. if args.action == 'clean':
  2004. clean(for_cross_compile=args.clean_for_cross_compile)
  2005. return
  2006. if args.action == 'macos-freeze':
  2007. return macos_freeze(args, launcher_dir)
  2008. if args.action == 'build-frozen-launcher' and is_macos:
  2009. launcher_dir=os.path.join(args.prefix, 'bin')
  2010. return macos_freeze(args, launcher_dir, only_frozen_launcher=True)
  2011. with CompilationDatabase(args.incremental) as cdb:
  2012. args.compilation_database = cdb
  2013. if args.action == 'build':
  2014. build(args)
  2015. if is_macos:
  2016. create_minimal_macos_bundle(args, launcher_dir)
  2017. else:
  2018. build_launcher(args, launcher_dir=launcher_dir)
  2019. build_static_kittens(args, launcher_dir=launcher_dir)
  2020. elif args.action == 'develop':
  2021. build(args)
  2022. build_launcher(args, launcher_dir=launcher_dir, bundle_type='develop')
  2023. build_static_kittens(args, launcher_dir=launcher_dir)
  2024. if is_macos:
  2025. create_minimal_macos_bundle(args, launcher_dir, relocate=True)
  2026. elif args.action == 'build-launcher':
  2027. init_env_from_args(args, False)
  2028. build_launcher(args, launcher_dir=launcher_dir)
  2029. build_static_kittens(args, launcher_dir=launcher_dir)
  2030. elif args.action == 'build-frozen-launcher':
  2031. init_env_from_args(args, False)
  2032. bundle_type = ('macos' if is_macos else 'linux') + '-freeze'
  2033. build_launcher(args, launcher_dir=os.path.join(args.prefix, 'bin'), bundle_type=bundle_type)
  2034. elif args.action == 'build-frozen-tools':
  2035. build_static_kittens(args, launcher_dir=args.prefix, for_freeze=True)
  2036. elif args.action == 'linux-package':
  2037. build(args, native_optimizations=False)
  2038. package(args, bundle_type='linux-package')
  2039. elif args.action == 'linux-freeze':
  2040. build(args, native_optimizations=False)
  2041. package(args, bundle_type='linux-freeze')
  2042. elif args.action == 'kitty.app':
  2043. args.prefix = 'kitty.app'
  2044. if os.path.exists(args.prefix):
  2045. shutil.rmtree(args.prefix)
  2046. build(args)
  2047. package(args, bundle_type='macos-package')
  2048. print('kitty.app successfully built!')
  2049. elif args.action == 'export-ci-bundles':
  2050. cmd = [sys.executable, '../bypy', 'export', 'download.calibre-ebook.com:/srv/download/ci/kitty']
  2051. subprocess.check_call(cmd + ['linux'])
  2052. subprocess.check_call(cmd + ['macos'])
  2053. elif args.action == 'build-static-binaries':
  2054. build_static_binaries(args, launcher_dir)
  2055. def main() -> None:
  2056. global verbose, build_dir
  2057. if len(sys.argv) > 1 and sys.argv[1] == 'build-dep':
  2058. return build_dep()
  2059. args = option_parser().parse_args(namespace=Options())
  2060. verbose = args.verbose > 0
  2061. args.prefix = os.path.abspath(args.prefix)
  2062. os.chdir(src_base)
  2063. os.makedirs(build_dir, exist_ok=True)
  2064. do_build(args)
  2065. if __name__ == '__main__':
  2066. main()