__main__.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. #!/usr/bin/env python
  2. # vim:fileencoding=utf-8
  3. # License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
  4. import errno
  5. import os
  6. import shutil
  7. import stat
  8. import subprocess
  9. import tarfile
  10. import time
  11. from bypy.constants import OUTPUT_DIR, PREFIX, python_major_minor_version
  12. from bypy.freeze import extract_extension_modules, freeze_python, path_to_freeze_dir
  13. from bypy.utils import get_dll_path, mkdtemp, py_compile, walk
  14. j = os.path.join
  15. machine = (os.uname()[4] or '').lower()
  16. self_dir = os.path.dirname(os.path.abspath(__file__))
  17. py_ver = '.'.join(map(str, python_major_minor_version()))
  18. iv = globals()['init_env']
  19. kitty_constants = iv['kitty_constants']
  20. def binary_includes():
  21. return tuple(map(get_dll_path, (
  22. 'expat', 'sqlite3', 'ffi', 'z', 'lzma', 'png16', 'lcms2', 'ssl', 'crypto', 'crypt',
  23. 'iconv', 'pcre2-8', 'graphite2', 'glib-2.0', 'freetype', 'xxhash',
  24. 'harfbuzz', 'xkbcommon', 'xkbcommon-x11',
  25. # fontconfig is not bundled because in typical brain dead Linux
  26. # distro fashion, different distros use different default config
  27. # paths for fontconfig.
  28. 'ncursesw', 'readline', 'brotlicommon', 'brotlienc', 'brotlidec',
  29. 'wayland-client', 'wayland-cursor',
  30. ))) + (
  31. get_dll_path('bz2', 2),
  32. get_dll_path(f'python{py_ver}', 2),
  33. )
  34. class Env:
  35. def __init__(self, package_dir):
  36. self.base = package_dir
  37. self.lib_dir = j(self.base, 'lib')
  38. self.py_dir = j(self.lib_dir, f'python{py_ver}')
  39. os.makedirs(self.py_dir)
  40. self.bin_dir = j(self.base, 'bin')
  41. self.obj_dir = mkdtemp('launchers-')
  42. def ignore_in_lib(base, items, ignored_dirs=None):
  43. ans = []
  44. if ignored_dirs is None:
  45. ignored_dirs = {'.svn', '.bzr', '.git', 'test', 'tests', 'testing'}
  46. for name in items:
  47. path = j(base, name)
  48. if os.path.isdir(path):
  49. if name in ignored_dirs or not os.path.exists(j(path, '__init__.py')):
  50. if name != 'plugins':
  51. ans.append(name)
  52. else:
  53. if name.rpartition('.')[-1] not in ('so', 'py'):
  54. ans.append(name)
  55. return ans
  56. def import_site_packages(srcdir, dest):
  57. if not os.path.exists(dest):
  58. os.mkdir(dest)
  59. for x in os.listdir(srcdir):
  60. ext = x.rpartition('.')[-1]
  61. f = j(srcdir, x)
  62. if ext in ('py', 'so'):
  63. shutil.copy2(f, dest)
  64. elif ext == 'pth' and x != 'setuptools.pth':
  65. for line in open(f):
  66. src = os.path.abspath(j(srcdir, line))
  67. if os.path.exists(src) and os.path.isdir(src):
  68. import_site_packages(src, dest)
  69. elif os.path.exists(j(f, '__init__.py')):
  70. shutil.copytree(f, j(dest, x), ignore=ignore_in_lib)
  71. def copy_libs(env):
  72. print('Copying libs...')
  73. for x in binary_includes():
  74. dest = env.bin_dir if '/bin/' in x else env.lib_dir
  75. shutil.copy2(x, dest)
  76. dest = os.path.join(dest, os.path.basename(x))
  77. subprocess.check_call(['chrpath', '-d', dest])
  78. def add_ca_certs(env):
  79. print('Downloading CA certs...')
  80. from urllib.request import urlopen
  81. cdata = urlopen(kitty_constants['cacerts_url']).read()
  82. dest = os.path.join(env.lib_dir, 'cacert.pem')
  83. with open(dest, 'wb') as f:
  84. f.write(cdata)
  85. def copy_python(env):
  86. print('Copying python...')
  87. srcdir = j(PREFIX, f'lib/python{py_ver}')
  88. for x in os.listdir(srcdir):
  89. y = j(srcdir, x)
  90. ext = os.path.splitext(x)[1]
  91. if os.path.isdir(y) and x not in ('test', 'hotshot', 'distutils', 'tkinter', 'turtledemo',
  92. 'site-packages', 'idlelib', 'lib2to3', 'dist-packages'):
  93. shutil.copytree(y, j(env.py_dir, x), ignore=ignore_in_lib)
  94. if os.path.isfile(y) and ext in ('.py', '.so'):
  95. shutil.copy2(y, env.py_dir)
  96. srcdir = j(srcdir, 'site-packages')
  97. import_site_packages(srcdir, env.py_dir)
  98. pdir = os.path.join(env.lib_dir, 'kitty-extensions')
  99. os.makedirs(pdir, exist_ok=True)
  100. kitty_dir = os.path.join(env.lib_dir, 'kitty')
  101. bases = ('kitty', 'kittens', 'kitty_tests')
  102. for x in bases:
  103. dest = os.path.join(env.py_dir, x)
  104. os.rename(os.path.join(kitty_dir, x), dest)
  105. if x == 'kitty':
  106. shutil.rmtree(os.path.join(dest, 'launcher'))
  107. os.rename(os.path.join(kitty_dir, '__main__.py'), os.path.join(env.py_dir, 'kitty_main.py'))
  108. shutil.rmtree(os.path.join(kitty_dir, '__pycache__'))
  109. print('Extracting extension modules from', env.py_dir, 'to', pdir)
  110. ext_map = extract_extension_modules(env.py_dir, pdir)
  111. shutil.copy(os.path.join(os.path.dirname(self_dir), 'site.py'), os.path.join(env.py_dir, 'site.py'))
  112. for x in bases:
  113. iv['sanitize_source_folder'](os.path.join(env.py_dir, x))
  114. py_compile(env.py_dir)
  115. freeze_python(env.py_dir, pdir, env.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM', remove_pyc_files=True)
  116. shutil.rmtree(env.py_dir)
  117. def build_launcher(env):
  118. iv['build_frozen_launcher']([path_to_freeze_dir(), env.obj_dir])
  119. def is_elf(path):
  120. with open(path, 'rb') as f:
  121. return f.read(4) == b'\x7fELF'
  122. def fix_permissions(files):
  123. for path in files:
  124. os.chmod(path, 0o755)
  125. STRIPCMD = ['strip']
  126. def find_binaries(env):
  127. files = {j(env.bin_dir, x) for x in os.listdir(env.bin_dir)} | {
  128. x for x in {
  129. j(os.path.dirname(env.bin_dir), x) for x in os.listdir(env.bin_dir)} if os.path.exists(x)}
  130. for x in walk(env.lib_dir):
  131. x = os.path.realpath(x)
  132. if x not in files and is_elf(x):
  133. files.add(x)
  134. return files
  135. def strip_files(files, argv_max=(256 * 1024)):
  136. """ Strip a list of files """
  137. while files:
  138. cmd = list(STRIPCMD)
  139. pathlen = sum(len(s) + 1 for s in cmd)
  140. while pathlen < argv_max and files:
  141. f = files.pop()
  142. cmd.append(f)
  143. pathlen += len(f) + 1
  144. if len(cmd) > len(STRIPCMD):
  145. all_files = cmd[len(STRIPCMD):]
  146. unwritable_files = tuple(filter(None, (None if os.access(x, os.W_OK) else (x, os.stat(x).st_mode) for x in all_files)))
  147. [os.chmod(x, stat.S_IWRITE | old_mode) for x, old_mode in unwritable_files]
  148. subprocess.check_call(cmd)
  149. [os.chmod(x, old_mode) for x, old_mode in unwritable_files]
  150. def strip_binaries(files):
  151. print(f'Stripping {len(files)} files...')
  152. before = sum(os.path.getsize(x) for x in files)
  153. strip_files(files)
  154. after = sum(os.path.getsize(x) for x in files)
  155. print('Stripped {:.1f} MB'.format((before - after) / (1024 * 1024.)))
  156. def create_tarfile(env, compression_level='9'):
  157. print('Creating archive...')
  158. base = OUTPUT_DIR
  159. arch = 'arm64' if 'arm64' in os.environ['BYPY_ARCH'] else ('i686' if 'i386' in os.environ['BYPY_ARCH'] else 'x86_64')
  160. try:
  161. shutil.rmtree(base)
  162. except OSError as err:
  163. if err.errno not in (errno.ENOENT, errno.EBUSY): # EBUSY when the directory is mountpoint
  164. raise
  165. os.makedirs(base, exist_ok=True)
  166. dist = os.path.join(base, f'{kitty_constants["appname"]}-{kitty_constants["version"]}-{arch}.tar')
  167. with tarfile.open(dist, mode='w', format=tarfile.PAX_FORMAT) as tf:
  168. cwd = os.getcwd()
  169. os.chdir(env.base)
  170. try:
  171. for x in os.listdir('.'):
  172. tf.add(x)
  173. finally:
  174. os.chdir(cwd)
  175. print('Compressing archive...')
  176. ans = f'{dist.rpartition(".")[0]}.txz'
  177. start_time = time.time()
  178. threads = 4 if arch == 'i686' else 0
  179. subprocess.check_call(['xz', '--verbose', f'--threads={threads}', '-f', f'-{compression_level}', dist])
  180. secs = time.time() - start_time
  181. print('Compressed in {} minutes {} seconds'.format(secs // 60, secs % 60))
  182. os.rename(f'{dist}.xz', ans)
  183. print('Archive {} created: {:.2f} MB'.format(
  184. os.path.basename(ans), os.stat(ans).st_size / (1024.**2)))
  185. def main():
  186. args = globals()['args']
  187. ext_dir = globals()['ext_dir']
  188. env = Env(os.path.join(ext_dir, kitty_constants['appname']))
  189. copy_libs(env)
  190. copy_python(env)
  191. build_launcher(env)
  192. files = find_binaries(env)
  193. fix_permissions(files)
  194. add_ca_certs(env)
  195. kitty_exe = os.path.join(env.base, 'bin', 'kitty')
  196. iv['build_frozen_tools'](kitty_exe)
  197. if not args.dont_strip:
  198. strip_binaries(files)
  199. if not args.skip_tests:
  200. iv['run_tests'](kitty_exe)
  201. create_tarfile(env, args.compression_level)
  202. if __name__ == '__main__':
  203. main()