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..541281b0 --- /dev/null +++ b/assets/installer.py @@ -0,0 +1,393 @@ +#!/usr/bin/env python + +from __future__ import print_function +import math +import os +import shutil +import sys +import tempfile +from subprocess import call + +try: + from shutil import which +except ImportError: + from distutils.spawn import find_executable as which + +try: + from urllib.request import urlretrieve +except ImportError: + from urllib import urlretrieve + +IS_WIN = sys.platform == "win32" + +if IS_WIN: + try: + import winreg + except ImportError: + import _winreg as winreg + from ctypes import windll, wintypes + + +VIRTUALENV_URL = "https://bootstrap.pypa.io/virtualenv.pyz" + +# this difference is for backwards-compatibility with the previous installer +APP_NAME = "lektor" if not IS_WIN else "lektor-cli" + +# where to search for a writable bin directory on *nix. +# this order makes sure we try a system install first. +POSIX_BIN_DIRS = [ + "/usr/local/bin", "/opt/local/bin", + "{home}/.bin", "{home}/.local/bin", +] + +SILENT = ( + os.environ.get("LEKTOR_SILENT", "").lower() + not in ("", "0", "off", "false") +) + +if not os.isatty(sys.stdin.fileno()): + # the script is being piped, we need to reset stdin + sys.stdin = open("CON:" if IS_WIN else "/dev/tty") + +if sys.version_info.major == 2: + input = raw_input + + +def get_confirmation(): + if SILENT: + 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, file=sys.stderr) + sys.exit(1) + + +def multiprint(*lines, **kwargs): + for line in lines: + print(line, **kwargs) + + +def rm_recursive(*paths): + def _error(path): + multiprint( + "Problem deleting {}".format(path), + "Please try and delete {} manually".format(path), + "Aborted!", + file=sys.stderr, + ) + sys.exit(1) + + def _rm(path): + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) + + for path in paths: + if not os.path.lexists(path): + continue + try: + _rm(path) + except: + _error(path) + + +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}%".format(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() + + +class FetchTemp(object): + """ + Fetches the given URL into a temporary file. + To be used as a context manager. + """ + + def __init__(self, url): + self.url = url + + fname = os.path.basename(url) + root, ext = os.path.splitext(fname) + self.filename = tempfile.mktemp(prefix=root + "-", suffix=ext) + + def fetch(self): + with self.Progress() as hook: + urlretrieve(self.url, self.filename, reporthook=hook) + + def cleanup(self): + os.remove(self.filename) + + def __enter__(self): + self.fetch() + + return self.filename + + def __exit__(self, exc_type, exc_value, traceback): + self.cleanup() + + +def create_virtualenv(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. + """ + + def use_venv(): + try: + import venv + except ImportError: + return + + # on Debian and Ubuntu systems Python is missing `ensurepip`, + # prompting the user to install `python3-venv` instead. + # + # we could handle this, but we'll just let the command fail + # and have the users install the package themselves. + + return call([sys.executable, "-m", "venv", target_dir]) + + def use_virtualenv(): + venv_exec = which("virtualenv") + if not venv_exec: + return + + return call([venv_exec, "-p", sys.executable, target_dir]) + + def use_zipapp(): + print("Downloading virtualenv: ", end="") + with FetchTemp(VIRTUALENV_URL) as zipapp: + return call([sys.executable, zipapp, target_dir]) + + print("Installing virtual environment...") + for func in use_venv, use_virtualenv, use_zipapp: + retval = func() + if retval is None: + # command did not run + continue + if retval == 0: + # command successful + return + # else... + sys.exit(1) + + +def get_pip(lib_dir): + return ( + os.path.join(lib_dir, "Scripts", "pip.exe") if IS_WIN + else os.path.join(lib_dir, "bin", "pip") + ) + + +def install_lektor(lib_dir): + create_virtualenv(lib_dir) + + pip = get_pip(lib_dir) + + args = [pip, "install"] + if IS_WIN: + # avoid fail due to PEP 517 on windows + args.append("--prefer-binary") + args.extend(["--upgrade", "Lektor"]) + + return call(args) + + +def posix_find_bin_dir(): + home = os.environ["HOME"] + preferred = [d.format(home=home) for d in POSIX_BIN_DIRS] + + # look for writable directories in the user's $PATH + # (that are not sbin) + dirs = [ + item + for item in os.environ["PATH"].split(":") + if not item.endswith("/sbin") and os.access(item, os.W_OK) + ] + + if not dirs: + fail( + "None of the items in $PATH are writable. Run with " + "sudo or add a $PATH item that you have access to." + ) + + # ... and prioritize them according to our preferences + def _sorter(path): + try: + return preferred.index(path) + except ValueError: + return float("inf") + + dirs.sort(key=_sorter) + return dirs[0] + + +def posix_find_lib_dir(bin_dir): + # the chosen lib_dir depends on the bin_dir found: + home = os.environ["HOME"] + + if bin_dir.startswith(home): + # this is a local install + return os.path.join(home, ".local", "lib", APP_NAME) + + # else, it's a system install + parent = os.path.dirname(bin_dir) + return os.path.join(parent, "lib", APP_NAME) + + +def windows_create_link(lib_dir, target_dir): + exe = os.path.join(lib_dir, "Scripts", "lektor.exe") + link = os.path.join(target_dir, "lektor.cmd") + + with open(link, "w") as link_file: + link_file.write("@echo off\n") + link_file.write('"{}" %*'.format(exe)) + + +def windows_add_to_path(location): + HWND_BROADCAST = 0xFFFF + WM_SETTINGCHANGE = 0x1A + + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, "Environment", 0, winreg.KEY_ALL_ACCESS + ) + + try: + value, _ = winreg.QueryValueEx(key, "Path") + except WindowsError: + value = "" + + paths = [path for path in value.split(";") if path != ""] + + if location not in paths: + paths.append(location) + value = ";".join(paths) + winreg.SetValueEx( + key, "Path", 0, winreg.REG_EXPAND_SZ, value + ) + + SendMessage = windll.user32.SendMessageW + SendMessage.argtypes = ( + wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPVOID + ) + SendMessage.restype = wintypes.LPARAM + SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, "Environment") + + # also add the path to the environment, + # so it's available in the current console + os.environ['Path'] += ";%s" % location + + key.Close() + + +def posix_install(): + bin_dir = posix_find_bin_dir() + lib_dir = posix_find_lib_dir(bin_dir) + symlink_path = os.path.join(bin_dir, APP_NAME) + + multiprint( + "Installing at:", + " bin: %s" % bin_dir, + " app: %s" % lib_dir, + "", + ) + + if os.path.exists(lib_dir) or os.path.lexists(symlink_path): + multiprint( + "An existing installation was detected. This will be removed!", + "", + ) + + get_confirmation() + rm_recursive(lib_dir, symlink_path) + install_lektor(lib_dir) + + os.symlink(os.path.join(lib_dir, "bin", "lektor"), symlink_path) + + +def windows_install(): + install_dir = os.path.join(os.environ["LocalAppData"], APP_NAME) + lib_dir = os.path.join(install_dir, "lib") + + multiprint( + "Installing at:", + " %s" % install_dir, + "", + ) + + if os.path.exists(install_dir): + multiprint( + "An existing installation was detected. This will be removed!", + "", + ) + + get_confirmation() + rm_recursive(install_dir) + install_lektor(lib_dir) + + windows_create_link(lib_dir, install_dir) + windows_add_to_path(install_dir) + + +def install(): + multiprint( + "", + "Welcome to Lektor", + "This script will install Lektor on your computer.", + "", + ) + + if IS_WIN: + windows_install() + else: + posix_install() + + multiprint( + "", + "All done!", + ) + + +if __name__ == "__main__": + install() diff --git a/content/docs/installation/contents.lr b/content/docs/installation/contents.lr index 18f52090..02171a0b 100644 --- a/content/docs/installation/contents.lr +++ b/content/docs/installation/contents.lr @@ -32,9 +32,9 @@ the installation is a bit more involved. You need to make sure you have the following software installed on your computer: -* Python 2.7 or above (also `python-dev`, `libssl-dev` and - `libffi-dev` is required on Ubuntu) - `sudo apt-get install python-dev libssl-dev libffi-dev` +* Python 3 is recommended (but 2.7 is also supported) + On Ubuntu `python3-dev`, `libssl-dev` and `libffi-dev` are also required + `sudo apt-get install python3-dev libssl-dev libffi-dev` * ImageMagick (`brew install imagemagick` can get you this on OS X and `sudo apt-get install imagemagick` on Ubuntu the `imagemagick` package needs to be installed. On Windows do `choco install imagemagick`, which requires [chocolatey :ext](https://chocolatey.org/), @@ -44,24 +44,30 @@ Once you have those installed and have made sure that they are on your `PATH`, y get Lektor installed with our installation script: ``` -# curl -sf https://www.getlektor.com/install.sh | sh +# curl -sf https://www.getlektor.com/installer.py | python3 ``` -If you are not logged in as superuser, instead you should try this: + +This will attempt to install lektor in your user's `HOME`. If you want a system-wide installation, try this instead: + ``` -$ curl -sf https://www.getlektor.com/install.sh | sudo sh +$ curl -sf https://www.getlektor.com/installer.py | sudo python3 ``` + If you would like to install Lektor without being prompted, set LEKTOR_SILENT before running the prior command. -For Windows you can use the `command prompt`: +For Windows, make sure that Python is in your `PATH` and run in `Powershell`: ``` -C:\> @powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://getlektor.com/install.ps1'))" && SET PATH=%PATH%;%LocalAppData%\lektor-cli +PS C:\> (new-object net.webclient).DownloadString('https://www.getlektor.com/installer.py') | python ``` -but you can also do it directly in `Powershell`: + +or you can use the `command prompt` instead: + ``` -PS C:\> iex ((new-object net.webclient).DownloadString('https://getlektor.com/install.ps1')) +C:\> @powershell -NoProfile -Command "(new-object net.webclient).DownloadString('https://www.getlektor.com/installer.py') | python" ``` + ## pip Alternatively you can manually install the command line version with diff --git a/content/downloads/contents.lr b/content/downloads/contents.lr index 1b6e26e7..ce42f148 100644 --- a/content/downloads/contents.lr +++ b/content/downloads/contents.lr @@ -23,16 +23,16 @@ If you are on Linux or Mac you can install the command line version of Lektor by copy/pasting a command into your terminal. !!! Scared about copy/pasting this into a terminal? We will not do anything -before asking you for confirmation and you can download the script upfront -to see what it's doing. +before asking you for confirmation and you can download [the script](https://www.getlektor.com/installer.py) +upfront to see what it's doing. ### Mac/Linux -This will install Lektor for you but you might have to run it with `sudo` if -your current user does not have rights to write into `/usr/local`. +This will install Lektor in your `HOME`. Running it with `sudo` will result in +a system installation instead. ``` -curl -sf https://www.getlektor.com/install.sh | sh +curl -sf https://www.getlektor.com/installer.py | python3 ``` You might need additional dependencies for this installation. For more @@ -40,18 +40,19 @@ information see [Installation](../docs/installation/). ### Windows -If you are on Windows copy/paste this command into the `command prompt`: +If you are on Windows copy/paste this command into `Powershell`: ``` -@powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://getlektor.com/install.ps1'))" && SET PATH=%PATH%;%LocalAppData%\lektor-cli +(new-object net.webclient).DownloadString('https://www.getlektor.com/installer.py') | python ``` -alternatively use this command in your `Powershell`: +or alternatively use this in `command prompt`: ``` -iex ((new-object net.webclient).DownloadString('https://getlektor.com/install.ps1')) +@powershell -NoProfile -Command "(new-object net.webclient).DownloadString('https://www.getlektor.com/installer.py') | python" ``` + ## Desktop Application Lektor supported an installable version of Lektor on OSX. The current build process for these installers is [old and in need of refactoring](https://github.com/lektor/lektor/issues/420). Temporarily until this is resolved, as of version 3.1, this installer is no longer supported.