#!/usr/bin/env python3 # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2018, Kovid Goyal from __future__ import ( absolute_import, division, print_function, unicode_literals ) import atexit import json import os import platform import re import shlex import shutil import subprocess import sys import tempfile py3 = sys.version_info[0] > 2 is64bit = platform.architecture()[0] == '64bit' is_macos = 'darwin' in sys.platform.lower() if is_macos: mac_ver = tuple(map(int, platform.mac_ver()[0].split('.'))) if mac_ver[:2] < (10, 12): raise SystemExit('Your version of macOS is too old, at least 10.12 is required') try: __file__ from_file = True except NameError: from_file = False if py3: unicode = str raw_input = input import urllib.request as urllib def encode_for_subprocess(x): return x else: from future_builtins import map import urllib2 as urllib def encode_for_subprocess(x): if isinstance(x, unicode): x = x.encode('utf-8') return x def run(*args): if len(args) == 1: args = shlex.split(args[0]) args = list(map(encode_for_subprocess, args)) ret = subprocess.Popen(args).wait() if ret != 0: raise SystemExit(ret) class Reporter: # {{{ def __init__(self, fname): self.fname = fname self.last_percent = 0 def __call__(self, blocks, block_size, total_size): percent = (blocks*block_size)/float(total_size) report = '\rDownloaded {:.1%} '.format(percent) if percent - self.last_percent > 0.05: self.last_percent = percent print(report, end='') sys.stdout.flush() # }}} def get_latest_release_data(): print('Checking for latest release on GitHub...') req = urllib.Request('https://api.github.com/repos/kovidgoyal/kitty/releases/latest', headers={'Accept': 'application/vnd.github.v3+json'}) try: res = urllib.urlopen(req).read().decode('utf-8') except Exception as err: raise SystemExit('Failed to contact {} with error: {}'.format(req.get_full_url(), err)) data = json.loads(res) html_url = data['html_url'].replace('/tag/', '/download/').rstrip('/') for asset in data.get('assets', ()): name = asset['name'] if is_macos: if name.endswith('.dmg'): return html_url + '/' + name, asset['size'] else: if name.endswith('.txz'): if is64bit: if name.endswith('-x86_64.txz'): return html_url + '/' + name, asset['size'] else: if name.endswith('-i686.txz'): return html_url + '/' + name, asset['size'] raise SystemExit('Failed to find the installer package on github') def do_download(url, size, dest): print('Will download and install', os.path.basename(dest)) reporter = Reporter(os.path.basename(dest)) # Get content length and check if range is supported rq = urllib.urlopen(url) headers = rq.info() sent_size = int(headers['content-length']) if sent_size != size: raise SystemExit('Failed to download from {} Content-Length ({}) != {}'.format(url, sent_size, size)) with open(dest, 'wb') as f: while f.tell() < size: raw = rq.read(8192) if not raw: break f.write(raw) reporter(f.tell(), 1, size) rq.close() if os.path.getsize(dest) < size: raise SystemExit('Download failed, try again later') print('\rDownloaded {} bytes'.format(os.path.getsize(dest))) def clean_cache(cache, fname): for x in os.listdir(cache): if fname not in x: os.remove(os.path.join(cache, x)) def download_installer(url, size): fname = url.rpartition('/')[-1] tdir = tempfile.gettempdir() cache = os.path.join(tdir, 'kitty-installer-cache') if not os.path.exists(cache): os.makedirs(cache) clean_cache(cache, fname) dest = os.path.join(cache, fname) if os.path.exists(dest) and os.path.getsize(dest) == size: print('Using previously downloaded', fname) return dest if os.path.exists(dest): os.remove(dest) do_download(url, size, dest) return dest def macos_install(dmg, dest='/Applications', launch=True): mp = tempfile.mkdtemp() atexit.register(shutil.rmtree, mp) run('hdiutil', 'attach', dmg, '-mountpoint', mp) try: os.chdir(mp) app = 'kitty.app' d = os.path.join(dest, app) if os.path.exists(d): shutil.rmtree(d) dest = os.path.join(dest, app) run('ditto', '-v', app, dest) print('Successfully installed kitty into', dest) if launch: run('open', dest) finally: os.chdir('/') run('hdiutil', 'detach', mp) def linux_install(installer, dest=os.path.expanduser('~/.local'), launch=True): dest = os.path.join(dest, 'kitty.app') if os.path.exists(dest): shutil.rmtree(dest) os.makedirs(dest) print('Extracting tarball...') run('tar', '-C', dest, '-xJof', installer) print('kitty successfully installed to', dest) kitty = os.path.join(dest, 'bin', 'kitty') print('Use', kitty, 'to run kitty') if launch: run(kitty, '--detach') def main(dest=None, launch=True, installer=None): if not dest: if is_macos: dest = '/Applications' else: dest = os.path.expanduser('~/.local') machine = os.uname()[4] if machine and machine.lower().startswith('arm'): raise SystemExit( 'You are running on an ARM system. The kitty binaries are only' ' available for x86 systems. You will have to build from' ' source.') if not installer: url, size = get_latest_release_data() installer = download_installer(url, size) else: installer = os.path.abspath(installer) if not os.access(installer, os.R_OK): raise SystemExit('Could not read from: {}'.format(installer)) if is_macos: macos_install(installer, dest=dest, launch=launch) else: linux_install(installer, dest=dest, launch=launch) def script_launch(): # To test: python3 -c "import runpy; runpy.run_path('installer.py', run_name='script_launch')" def path(x): return os.path.expandvars(os.path.expanduser(x)) def to_bool(x): return x.lower() in {'y', 'yes', '1', 'true'} type_map = {x: path for x in 'dest installer'.split()} type_map['launch'] = to_bool kwargs = {} for arg in sys.argv[1:]: if arg: m = re.match('([a-z_]+)=(.+)', arg) if m is None: raise SystemExit('Unrecognized command line argument: ' + arg) k = m.group(1) if k not in type_map: raise SystemExit('Unrecognized command line argument: ' + arg) kwargs[k] = type_map[k](m.group(2)) main(**kwargs) def update_intaller_wrapper(): # To run: python3 -c "import runpy; runpy.run_path('installer.py', run_name='update_wrapper')" installer.sh with open(__file__, 'rb') as f: src = f.read().decode('utf-8') wrapper = sys.argv[-1] with open(wrapper, 'r+b') as f: raw = f.read().decode('utf-8') nraw = re.sub(r'^# HEREDOC_START.+^# HEREDOC_END', lambda m: '# HEREDOC_START\n{}\n# HEREDOC_END'.format(src), raw, flags=re.MULTILINE | re.DOTALL) if 'update_intaller_wrapper()' not in nraw: raise SystemExit('regex substitute of HEREDOC failed') f.seek(0), f.truncate() f.write(nraw.encode('utf-8')) if __name__ == '__main__' and from_file: main() elif __name__ == 'update_wrapper': update_intaller_wrapper() elif __name__ == 'script_launch': script_launch()