ci.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #!/usr/bin/env python
  2. # vim:fileencoding=utf-8
  3. # License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
  4. import glob
  5. import io
  6. import os
  7. import shlex
  8. import shutil
  9. import subprocess
  10. import sys
  11. import tarfile
  12. import time
  13. from urllib.request import urlopen
  14. BUNDLE_URL = 'https://download.calibre-ebook.com/ci/kitty/{}-64.tar.xz'
  15. FONTS_URL = 'https://download.calibre-ebook.com/ci/fonts.tar.xz'
  16. NERD_URL = 'https://github.com/ryanoasis/nerd-fonts/releases/latest/download/NerdFontsSymbolsOnly.tar.xz'
  17. is_bundle = os.environ.get('KITTY_BUNDLE') == '1'
  18. is_macos = 'darwin' in sys.platform.lower()
  19. SW = ''
  20. def do_print_crash_reports() -> None:
  21. print('Printing available crash reports...')
  22. if is_macos:
  23. end_time = time.monotonic() + 90
  24. while time.monotonic() < end_time:
  25. time.sleep(1)
  26. items = glob.glob(os.path.join(os.path.expanduser('~/Library/Logs/DiagnosticReports'), 'kitty-*.ips'))
  27. if items:
  28. break
  29. if items:
  30. time.sleep(1)
  31. print(os.path.basename(items[0]))
  32. sdir = os.path.dirname(os.path.abspath(__file__))
  33. subprocess.check_call([sys.executable, os.path.join(sdir, 'macos_crash_report.py'), items[0]])
  34. else:
  35. run('sh -c "echo bt | coredumpctl debug"')
  36. print(flush=True)
  37. def run(*a: str, print_crash_reports: bool = False) -> None:
  38. if len(a) == 1:
  39. a = tuple(shlex.split(a[0]))
  40. cmd = ' '.join(map(shlex.quote, a))
  41. print(cmd)
  42. sys.stdout.flush()
  43. ret = subprocess.Popen(a).wait()
  44. if ret != 0:
  45. if ret < 0:
  46. import signal
  47. try:
  48. sig = signal.Signals(-ret)
  49. except ValueError:
  50. pass
  51. else:
  52. if print_crash_reports:
  53. do_print_crash_reports()
  54. raise SystemExit(f'The following process was killed by signal: {sig.name}:\n{cmd}')
  55. raise SystemExit(f'The following process failed with exit code: {ret}:\n{cmd}')
  56. def install_fonts() -> None:
  57. with urlopen(FONTS_URL) as f:
  58. data = f.read()
  59. fonts_dir = os.path.expanduser('~/Library/Fonts' if is_macos else '~/.local/share/fonts')
  60. os.makedirs(fonts_dir, exist_ok=True)
  61. with tarfile.open(fileobj=io.BytesIO(data), mode='r:xz') as tf:
  62. try:
  63. tf.extractall(fonts_dir, filter='fully_trusted')
  64. except TypeError:
  65. tf.extractall(fonts_dir)
  66. with urlopen(NERD_URL) as f:
  67. data = f.read()
  68. with tarfile.open(fileobj=io.BytesIO(data), mode='r:xz') as tf:
  69. try:
  70. tf.extractall(fonts_dir, filter='fully_trusted')
  71. except TypeError:
  72. tf.extractall(fonts_dir)
  73. def install_deps() -> None:
  74. print('Installing kitty dependencies...')
  75. sys.stdout.flush()
  76. if is_macos:
  77. items = [x.split()[1].strip('"') for x in open('Brewfile').readlines() if x.strip().startswith('brew ')]
  78. openssl = 'openssl'
  79. items.remove('go') # already installed by ci.yml
  80. import ssl
  81. if ssl.OPENSSL_VERSION_INFO[0] == 1:
  82. openssl += '@1.1'
  83. run('brew', 'install', 'fish', openssl, *items)
  84. else:
  85. run('sudo apt-get update')
  86. run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev ca-certificates'
  87. ' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libx11-xcb-dev zsh'
  88. ' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev libxxhash-dev uuid-dev'
  89. ' libsimde-dev libsystemd-dev zsh bash dash systemd-coredump gdb')
  90. # for some reason these directories are world writable which causes zsh
  91. # compinit to break
  92. run('sudo chmod -R og-w /usr/share/zsh')
  93. if is_bundle:
  94. install_bundle()
  95. else:
  96. cmd = 'python3 -m pip install Pillow pygments'
  97. if sys.version_info[:2] < (3, 7):
  98. cmd += ' importlib-resources dataclasses'
  99. run(cmd)
  100. install_fonts()
  101. def build_kitty() -> None:
  102. python = shutil.which('python3') if is_bundle else sys.executable
  103. cmd = f'{python} setup.py build --verbose'
  104. if is_macos:
  105. cmd += ' --debug' # for better crash report to debug SIGILL issue
  106. if os.environ.get('KITTY_SANITIZE') == '1':
  107. cmd += ' --debug --sanitize'
  108. run(cmd)
  109. def test_kitty() -> None:
  110. if is_macos:
  111. run('ulimit -c unlimited')
  112. run('sudo chmod -R 777 /cores')
  113. run('./test.py', print_crash_reports=True)
  114. def package_kitty() -> None:
  115. python = 'python3' if is_macos else 'python'
  116. run(f'{python} setup.py linux-package --update-check-interval=0 --verbose')
  117. if is_macos:
  118. run('python3 setup.py kitty.app --update-check-interval=0 --verbose')
  119. run('kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"')
  120. def replace_in_file(path: str, src: str, dest: str) -> None:
  121. with open(path, 'r+') as f:
  122. n = f.read().replace(src, dest)
  123. f.seek(0), f.truncate()
  124. f.write(n)
  125. def setup_bundle_env() -> None:
  126. global SW
  127. os.environ['SW'] = SW = '/Users/Shared/kitty-build/sw/sw' if is_macos else os.path.join(os.environ['GITHUB_WORKSPACE'], 'sw')
  128. os.environ['PKG_CONFIG_PATH'] = os.path.join(SW, 'lib', 'pkgconfig')
  129. if is_macos:
  130. os.environ['PATH'] = '{}:{}'.format('/usr/local/opt/sphinx-doc/bin', os.environ['PATH'])
  131. else:
  132. os.environ['LD_LIBRARY_PATH'] = os.path.join(SW, 'lib')
  133. os.environ['PYTHONHOME'] = SW
  134. os.environ['PATH'] = '{}:{}'.format(os.path.join(SW, 'bin'), os.environ['PATH'])
  135. def install_bundle() -> None:
  136. cwd = os.getcwd()
  137. os.makedirs(SW)
  138. os.chdir(SW)
  139. with urlopen(BUNDLE_URL.format('macos' if is_macos else 'linux')) as f:
  140. data = f.read()
  141. with tarfile.open(fileobj=io.BytesIO(data), mode='r:xz') as tf:
  142. try:
  143. tf.extractall(filter='fully_trusted')
  144. except TypeError:
  145. tf.extractall()
  146. if not is_macos:
  147. replaced = 0
  148. for dirpath, dirnames, filenames in os.walk('.'):
  149. for f in filenames:
  150. if f.endswith('.pc') or (f.endswith('.py') and f.startswith('_sysconfig')):
  151. replace_in_file(os.path.join(dirpath, f), '/sw/sw', SW)
  152. replaced += 1
  153. if replaced < 2:
  154. raise SystemExit('Failed to replace path to SW in bundle')
  155. os.chdir(cwd)
  156. def main() -> None:
  157. if is_bundle:
  158. setup_bundle_env()
  159. else:
  160. if not is_macos and 'pythonLocation' in os.environ:
  161. os.environ['LD_LIBRARY_PATH'] = os.path.join(os.environ['pythonLocation'], 'lib')
  162. action = sys.argv[-1]
  163. if action in ('build', 'package'):
  164. install_deps()
  165. if action == 'build':
  166. build_kitty()
  167. elif action == 'package':
  168. package_kitty()
  169. elif action == 'test':
  170. test_kitty()
  171. elif action == 'gofmt':
  172. q = subprocess.check_output('gofmt -s -l tools'.split()).decode()
  173. if q.strip():
  174. q = '\n'.join(filter(lambda x: not x.rstrip().endswith('_generated.go'), q.strip().splitlines())).strip()
  175. if q:
  176. raise SystemExit(q)
  177. else:
  178. raise SystemExit(f'Unknown action: {action}')
  179. if __name__ == '__main__':
  180. main()