diff --git a/docs/installer.py b/docs/installer.py index 3aea3ae70..360233c05 100644 --- a/docs/installer.py +++ b/docs/installer.py @@ -78,7 +78,9 @@ def get_nightly_url(): if is_macos: return base + '.dmg' arch = 'x86_64' if is64bit else 'i686' - return base + '-' + arch + '.txz' + url = base + '-' + arch + '.txz' + i = urllib.urlopen(url) + return url, int(i.getheader('content-length')) def get_latest_release_data(): @@ -203,7 +205,8 @@ def main(dest=None, launch=True, installer=None): installer = download_installer(url, size) else: if installer == 'nightly': - url = get_nightly_url() + url, size = get_nightly_url() + installer = download_installer(url, size) else: installer = os.path.abspath(installer) if not os.access(installer, os.R_OK): diff --git a/docs/installer.sh b/docs/installer.sh index 850fa0419..a29b78c4f 100644 --- a/docs/installer.sh +++ b/docs/installer.sh @@ -25,6 +25,270 @@ echo Using python executable: $python $python -c "import sys; script_launch=lambda:sys.exit('Download of installer failed!'); exec(sys.stdin.read()); script_launch()" "$@" <<'INSTALLER_HEREDOC' # {{{ # HEREDOC_START +#!/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_nightly_url(): + base = 'https://github.com/kovidgoyal/kitty/releases/download/nightly/kitty-nightly' + if is_macos: + return base + '.dmg' + arch = 'x86_64' if is64bit else 'i686' + url = base + '-' + arch + '.txz' + i = urllib.urlopen(url) + return url, int(i.getheader('content-length')) + + +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: + if installer == 'nightly': + url, size = get_nightly_url() + 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() + # HEREDOC_END # }}} INSTALLER_HEREDOC