From 4a1f13b0c8a4c3dc8693efc610527cd8bdf43bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionu=C8=9B=20Cioc=C3=AErlan?= Date: Fri, 20 Mar 2020 14:40:10 +0200 Subject: [PATCH] unified installer --- assets/install.ps1 | 173 -------------------- assets/install.sh | 174 -------------------- assets/installer.py | 383 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 383 insertions(+), 347 deletions(-) delete mode 100644 assets/install.ps1 delete mode 100644 assets/install.sh create mode 100755 assets/installer.py diff --git a/assets/install.ps1 b/assets/install.ps1 deleted file mode 100644 index 0a81498a..00000000 --- a/assets/install.ps1 +++ /dev/null @@ -1,173 +0,0 @@ -$InstallScript = @" -import os -import sys -import json -import tempfile -import tarfile -import shutil -from subprocess import Popen -try: # py3 - from urllib.request import urlopen - from winreg import OpenKey, CloseKey, QueryValueEx, SetValueEx, \ - HKEY_CURRENT_USER, KEY_ALL_ACCESS, REG_EXPAND_SZ -except ImportError: # py2 - from urllib import urlopen - from _winreg import OpenKey, CloseKey, QueryValueEx, SetValueEx, \ - HKEY_CURRENT_USER, KEY_ALL_ACCESS, REG_EXPAND_SZ - -import ctypes -from ctypes.wintypes import HWND, UINT, WPARAM, LPARAM, LPVOID - - -VENV_URL = 'https://pypi.python.org/pypi/virtualenv/json' -APPDATA = os.environ['LocalAppData'] -APP = 'lektor-cli' -LIB = 'lib' -ROOT_KEY = HKEY_CURRENT_USER -SUB_KEY = 'Environment' -LRESULT = LPARAM -HWND_BROADCAST = 0xFFFF -WM_SETTINGCHANGE = 0x1A - -PY2 = sys.version_info[0] == 2 -if PY2: - input = raw_input - - -def get_confirmation(): - while 1: - user_input = input('Continue? [Yn] ').lower().strip() - if user_input in ('', 'y'): - break - elif user_input == 'n': - print() - print('Aborted!') - sys.exit() - -def find_location(): - install_dir = os.path.join(APPDATA, APP) - return install_dir, os.path.join(install_dir, LIB) - -def deletion_error(func, path, excinfo): - print('Problem deleting {}'.format(path)) - print('Please try and delete {} manually'.format(path)) - print('Aborted!') - sys.exit() - -def wipe_installation(install_dir): - shutil.rmtree(install_dir, onerror=deletion_error) - -def check_installation(install_dir): - if os.path.exists(install_dir): - print(' Lektor seems to be installed already.') - print(' Continuing will delete:') - print(' %s' % install_dir) - print() - get_confirmation() - print() - wipe_installation(install_dir) - -def fail(message): - print('Error: %s' % message) - sys.exit(1) - -def add_to_path(location): - reg_key = OpenKey(ROOT_KEY, SUB_KEY, 0, KEY_ALL_ACCESS) - - try: - path_value, _ = QueryValueEx(reg_key, 'Path') - except WindowsError: - path_value = '' - - paths = path_value.split(';') - if location not in paths: - paths.append(location) - path_value = ';'.join(paths) - SetValueEx(reg_key, 'Path', 0, REG_EXPAND_SZ, path_value) - - SendMessage = ctypes.windll.user32.SendMessageW - SendMessage.argtypes = HWND, UINT, WPARAM, LPVOID - SendMessage.restype = LRESULT - SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, u'Environment') - -def _fetch_virtualenv(): - for url in json.load(urlopen(VENV_URL))['urls']: - if url['python_version'] == 'source': - virtualenv_url = url['url'] - #stripping '.tar.gz' - virtualenv_filename = url['filename'][:-7] - break - else: - fail('Could not find virtualenv') - - t = tempfile.mkdtemp() - with open(os.path.join(t, 'virtualenv.tar.gz'), 'wb') as f: - download = urlopen(virtualenv_url) - f.write(download.read()) - download.close() - with tarfile.open(os.path.join(t, 'virtualenv.tar.gz'), 'r:gz') as tar: - tar.extractall(path=t) - - return os.path.join(t, virtualenv_filename) - -def install_virtualenv(target_dir): - # recent python versions include virtualenv - cmd = [sys.executable, '-m', 'venv', target_dir] - - try: - import venv - except ImportError: - venv_dir = _fetch_virtualenv() - venv_file = os.path.join(venv_dir, 'virtualenv.py') - # in recent versions "virtualenv.py" moved to the "src" subdirectory - if not os.path.exists(venv_file): - venv_file = os.path.join(venv_dir, 'src', 'virtualenv.py') - - cmd = [sys.executable, venv_file, target_dir] - - Popen(cmd).wait() - -def install(install_dir, lib_dir): - os.makedirs(install_dir) - os.makedirs(lib_dir) - - install_virtualenv(lib_dir) - - scripts = os.path.join(lib_dir, 'Scripts') - Popen([os.path.join(scripts, 'pip.exe'), - 'install', '--upgrade', 'Lektor'], - cwd=scripts).wait() - - with open(os.path.join(install_dir, 'lektor.cmd'), 'w') as link_file: - link_file.write('@echo off\n') - link_file.write('\"' + os.path.join(scripts, 'lektor.exe') + '\"' + ' %*') - - add_to_path(install_dir) - - -def main(): - print() - print('Welcome to Lektor') - print() - print('This script will install Lektor on your computer.') - print() - - install_dir, lib_dir = find_location() - - check_installation(install_dir) - - print(' Installing at:') - print(' %s' % install_dir) - print() - get_confirmation() - - install(install_dir, lib_dir) - - print() - print('All done!') - -main() -"@ - - -if (Get-Command python) { python -c $InstallScript } else { "To use this script you need to have Python installed"; exit } diff --git a/assets/install.sh b/assets/install.sh deleted file mode 100644 index 9608579d..00000000 --- a/assets/install.sh +++ /dev/null @@ -1,174 +0,0 @@ -#!/bin/sh -# This script helps you install Lektor on your computer. Right now it -# only supports Linux and OS X and only on OS X will it install the -# desktop version. -# -# For more information see https://www.getlektor.com/ - -# Wrap everything in a function so that we do not accidentally execute -# something we should not in case a truncated version of the script -# is executed. -I() { - set -u - - if ! hash python 2> /dev/null; then - echo "Error: To use this script you need to have Python installed" - exit 1 - fi - - python - <<'EOF' -if 1: - - import os - import sys - import json - import tempfile - import shutil - from subprocess import CalledProcessError, check_output, Popen - try: - from urllib.request import urlopen - except ImportError: - from urllib import urlopen - - PY2 = sys.version_info[0] == 2 - if PY2: - input = raw_input - - sys.stdin = open('/dev/tty', 'r') - - VENV_URL = "https://pypi.python.org/pypi/virtualenv/json" - KNOWN_BINS = ['/usr/local/bin', '/opt/local/bin', - os.path.join(os.environ['HOME'], '.bin'), - os.path.join(os.environ['HOME'], '.local', 'bin')] - - if os.environ.get('LEKTOR_SILENT') == None: - prompt = True - else: - prompt = False - - def find_user_paths(): - rv = [] - for item in os.environ['PATH'].split(':'): - if os.access(item, os.W_OK) \ - and item not in rv \ - and '/sbin' not in item: - rv.append(item) - return rv - - def bin_sort_key(path): - try: - return KNOWN_BINS.index(path) - except ValueError: - return float('inf') - - def find_locations(paths): - paths.sort(key=bin_sort_key) - for path in paths: - if path.startswith(os.environ['HOME']): - return path, os.path.join(os.environ['HOME'], - '.local', 'lib', 'lektor') - elif path.endswith('/bin'): - return path, os.path.join( - os.path.dirname(path), 'lib', 'lektor') - None, None - - def get_confirmation(): - while 1: - user_input = input('Continue? [Yn] ').lower().strip() - if user_input in ('', 'y'): - break - elif user_input == 'n': - print('') - print('Aborted!') - sys.exit() - - def deletion_error(func, path, excinfo): - print('Problem deleting {}'.format(path)) - print('Please try and delete {} manually'.format(path)) - print('Aborted!') - sys.exit() - - def wipe_installation(lib_dir, symlink_path): - if os.path.lexists(symlink_path): - os.remove(symlink_path) - if os.path.exists(lib_dir): - shutil.rmtree(lib_dir, onerror=deletion_error) - - def check_installation(lib_dir, bin_dir): - symlink_path = os.path.join(bin_dir, 'lektor') - if os.path.exists(lib_dir) or os.path.lexists(symlink_path): - print(' Lektor seems to be installed already.') - print(' Continuing will delete:') - print(' %s' % lib_dir) - print(' and remove this symlink:') - print(' %s' % symlink_path) - print('') - if prompt: - get_confirmation() - print('') - wipe_installation(lib_dir, symlink_path) - - def fail(message): - print('Error: %s' % message) - sys.exit(1) - - def install(virtualenv_url, lib_dir, bin_dir): - t = tempfile.mkdtemp() - Popen('curl -sf "%s" | tar -xzf - --strip-components=1' % - virtualenv_url, shell=True, cwd=t).wait() - - try: - os.makedirs(lib_dir) - except OSError: - pass - try: # virtualenv 16.1.0, 17+ - check_output([sys.executable, './src/virtualenv.py', lib_dir], cwd=t) - except CalledProcessError: # older virtualenv - Popen([sys.executable, './virtualenv.py', lib_dir], cwd=t).wait() - Popen([os.path.join(lib_dir, 'bin', 'pip'), - 'install', '--upgrade', 'Lektor']).wait() - os.symlink(os.path.join(lib_dir, 'bin', 'lektor'), - os.path.join(bin_dir, 'lektor')) - - def main(): - print('') - print('Welcome to Lektor') - print('') - print('This script will install Lektor on your computer.') - print('') - - paths = find_user_paths() - if not paths: - fail('None of the items in $PATH are writable. Run with ' - 'sudo or add a $PATH item that you have access to.') - - bin_dir, lib_dir = find_locations(paths) - if bin_dir is None or lib_dir is None: - fail('Could not determine installation location for Lektor.') - - check_installation(lib_dir, bin_dir) - - print('Installing at:') - print(' bin: %s' % bin_dir) - print(' app: %s' % lib_dir) - print('') - - if prompt: get_confirmation() - - for url in json.loads(urlopen(VENV_URL).read().decode('utf-8'))['urls']: - if url['python_version'] == 'source': - virtualenv = url['url'] - break - else: - fail('Could not find virtualenv') - - install(virtualenv, lib_dir, bin_dir) - - print('') - print('All done!') - - main() -EOF -} - -I diff --git a/assets/installer.py b/assets/installer.py new file mode 100755 index 00000000..90d929cc --- /dev/null +++ b/assets/installer.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python + +import math +import os +import shutil +import sys +import tempfile +from subprocess import call + + +_is_win = sys.platform == 'win32' + +if not _is_win: + sys.stdin = open('/dev/tty', 'r') + +if sys.version_info[0] == 2: + input = raw_input + +prompt = os.environ.get('LEKTOR_SILENT') is None + +def get_confirmation(): + if prompt is False: + return + + while True: + user_input = input('Continue? [Yn] ').lower().strip() + + if user_input in ('', 'y'): + print('') + return + + if user_input == 'n': + print('') + print('Aborted!') + sys.exit() + +def fail(message): + print('Error: %s' % message) + sys.exit(1) + +def multiprint(*lines): + for line in lines: + print(line) + + +class Installer(object): + APP_NAME = 'lektor' + VIRTUALENV_URL = 'https://bootstrap.pypa.io/virtualenv.pyz' + + def __init__(self): + multiprint('', + 'Welcome to Lektor', + '', + 'This script will install Lektor on your computer.', + '') + + self.compute_location() + + self.prompt_installation() + get_confirmation() + + if self.check_installation(): + self.prompt_wipe() + get_confirmation() + + self.wipe_installation() + + self.create_virtualenv() + self.install_lektor() + + multiprint('', + 'All done!') + + def compute_location(self): + # this method must set self.lib_dir + raise NotImplementedError() + + def check_installation(self): + raise NotImplementedError() + + def wipe_installation(self): + raise NotImplementedError() + + def prompt_installation(self): + raise NotImplementedError() + + def prompt_wipe(self): + raise NotImplementedError() + + def create_virtualenv(self): + self.mkvirtualenv(self.lib_dir) + + def install_lektor(self): + call([self.get_pip(), 'install', '--upgrade', 'Lektor']) + + def get_pip(self): + raise NotImplementedError() + + @classmethod + def mkvirtualenv(cls, target_dir): + """ + Tries to create a virtualenv by using the built-in `venv` module, + or using the `virtualenv` executable if present, or falling back + to downloading the official zipapp. + """ + + created = False + + try: + from venv import EnvBuilder + except ImportError: + pass + else: + try: + # TODO: design decision needed: + # on Debian and Ubuntu systems Python is missing `ensurepip`, + # prompting the user to install `python3-venv` instead. + # we could do the same, or go the download route... + import ensurepip + except ImportError: + pass + else: + venv = EnvBuilder(with_pip=True, + symlinks=False if _is_win else True) + venv.create(target_dir) + created = True + + if not created: + try: + from shutil import which + except ImportError: + from distutils.spawn import find_executable as which + + venv_exec = which("virtualenv") + if venv_exec: + retval = call([venv_exec, '-p', sys.executable, target_dir]) + if retval: + sys.exit(1) + created = True + + if not created: + zipapp = cls.fetch_virtualenv() + retval = call([sys.executable, zipapp, target_dir]) + os.unlink(zipapp) + if retval: + sys.exit(1) + + @classmethod + def fetch_virtualenv(cls): + try: + from urllib.request import urlretrieve + except ImportError: + from urllib import urlretrieve + + fname = os.path.basename(cls.VIRTUALENV_URL) + root, ext = os.path.splitext(fname) + + zipapp = tempfile.mktemp(prefix=root+"-", suffix=ext) + + with Progress() as hook: + sys.stdout.write("Downloading virtualenv: ") + urlretrieve(cls.VIRTUALENV_URL, zipapp, reporthook=hook) + + return zipapp + + @staticmethod + def deletion_error(func, path, excinfo): + print('Problem deleting {}'.format(path)) + print('Please try and delete {} manually'.format(path)) + print('Aborted!') + sys.exit(1) + + +_home = os.environ.get('HOME') + +class PosixInstaller(Installer): + KNOWN_BIN_PATHS = ['/usr/local/bin', '/opt/local/bin'] + if _home: # this is always true, but it needs not blow up on windows + KNOWN_BIN_PATHS.extend([ + os.path.join(_home, '.bin'), + os.path.join(_home, '.local', 'bin'), + ]) + + def prompt_installation(self): + multiprint('Installing at:', + ' bin: %s' % self.bin_dir, + ' app: %s' % self.lib_dir, + '') + + def prompt_wipe(self): + multiprint('Lektor seems to be installed already.', + 'Continuing will delete:', + ' %s' % self.lib_dir, + 'and remove this symlink:', + ' %s' % self.symlink_path, + '') + + def compute_location(self): + """ + Finds the preferred directory in the user's $PATH, + and derives the lib dir from it. + """ + + paths = [ + item for item in os.environ['PATH'].split(':') + if not item.endswith('/sbin') and os.access(item, os.W_OK) + ] + + if not paths: + fail('None of the items in $PATH are writable. Run with ' + 'sudo or add a $PATH item that you have access to.') + + def _sorter(path): + try: + return self.KNOWN_BIN_PATHS.index(path) + except ValueError: + return float('inf') + + paths.sort(key=_sorter) + + lib_dir = None + + home = os.environ['HOME'] + for path in paths: + if path.startswith(home): + lib_dir = os.path.join(home, '.local', 'lib', self.APP_NAME) + break + + if path.endswith('/bin'): + parent = os.path.dirname(path) + lib_dir = os.path.join(parent, 'lib', self.APP_NAME) + break + + if lib_dir is None: + fail('Could not determine installation location for Lektor.') + + self.bin_dir = path + self.lib_dir = lib_dir + + @property + def symlink_path(self): + return os.path.join(self.bin_dir, self.APP_NAME) + + def check_installation(self): + return os.path.exists(self.lib_dir) \ + or os.path.lexists(self.symlink_path) + + def wipe_installation(self): + if os.path.lexists(self.symlink_path): + os.remove(self.symlink_path) + if os.path.exists(self.lib_dir): + shutil.rmtree(self.lib_dir, onerror=self.deletion_error) + + def get_pip(self): + return os.path.join(self.lib_dir, 'bin', 'pip') + + def install_lektor(self): + import pdb + super(PosixInstaller, self).install_lektor() + + bin = os.path.join(self.lib_dir, 'bin', 'lektor') + os.symlink(bin, self.symlink_path) + + +class WindowsInstaller(Installer): + APP_NAME = 'lektor-cli' # backwards-compatibility with previous installer + LIB_DIR = 'lib' + + def prompt_installation(self): + multiprint('Installing at:', + ' %s' % self.install_dir, + '') + + def prompt_wipe(self): + multiprint('Lektor seems to be installed already.', + 'Continuing will delete:', + ' %s' % self.install_dir, + '') + + def compute_location(self): + install_dir = os.path.join(os.environ['LocalAppData'], self.APP_NAME) + lib_dir = os.path.join(install_dir, self.LIB_DIR) + + self.install_dir = install_dir + self.lib_dir = lib_dir + + def check_installation(self): + return os.path.exists(self.install_dir) + + def wipe_installation(self): + shutil.rmtree(self.install_dir, onerror=self.deletion_error) + + def get_pip(self): + return os.path.join(self.lib_dir, 'Scripts', 'pip.exe') + + def install_lektor(self): + super(WindowsInstaller, self).install_lektor() + + exe = os.path.join(self.lib_dir, 'Scripts', 'lektor.exe') + link = os.path.join(self.install_dir, 'lektor.cmd') + + with open(link, 'w') as link_file: + link_file.write('@echo off\n') + link_file.write('"%s" %%*' % exe) + + self.add_to_path(self.install_dir) + + @staticmethod + def add_to_path(location): + try: + from winreg import ( + OpenKey, CloseKey, QueryValueEx, SetValueEx, + HKEY_CURRENT_USER, KEY_ALL_ACCESS, REG_EXPAND_SZ, + ) + except ImportError: # py2 + from _winreg import ( + OpenKey, CloseKey, QueryValueEx, SetValueEx, + HKEY_CURRENT_USER, KEY_ALL_ACCESS, REG_EXPAND_SZ, + ) + import ctypes + from ctypes.wintypes import HWND, UINT, WPARAM, LPARAM, LPVOID + + HWND_BROADCAST = 0xFFFF + WM_SETTINGCHANGE = 0x1A + + reg_key = OpenKey(HKEY_CURRENT_USER, 'Environment', 0, KEY_ALL_ACCESS) + + try: + path_value, _ = QueryValueEx(reg_key, 'Path') + except WindowsError: + path_value = '' + + paths = path_value.split(';') + if location not in paths: + paths.append(location) + path_value = ';'.join(paths) + SetValueEx(reg_key, 'Path', 0, REG_EXPAND_SZ, path_value) + + SendMessage = ctypes.windll.user32.SendMessageW + SendMessage.argtypes = HWND, UINT, WPARAM, LPVOID + SendMessage.restype = LPARAM + SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, u'Environment') + + +class Progress(object): + "A context manager to be used as a urlretrieve reporthook." + + def __init__(self): + self.started = False + + def progress(self, count, bsize, total): + size = count * bsize + + if size > total: + progress = 100 + else: + progress = math.floor(100 * size / total) + + out = sys.stdout + if self.started: + out.write("\b" * 4) + + out.write("%3d" % progress + "%") + out.flush() + + self.started = True + + def finish(self): + sys.stdout.write("\n") + + def __enter__(self): + return self.progress + + def __exit__(self, exc_type, exc_value, traceback): + self.finish() + + +install = WindowsInstaller if _is_win \ + else PosixInstaller + + +if __name__ == '__main__': + install()