Merge pull request #298 from lektor/unified-installer

Unified installer

closes lektor/lektor#720
closes lektor/lektor#635
closes #267
This commit is contained in:
Ionuț Ciocîrlan 2020-04-24 01:06:46 +03:00 committed by GitHub
commit 1a059fa700
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 419 additions and 366 deletions

View File

@ -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 }

View File

@ -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

393
assets/installer.py Executable file
View File

@ -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()

View File

@ -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: 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 * Python 3 is recommended (but 2.7 is also supported)
`libffi-dev` is required on Ubuntu) On Ubuntu `python3-dev`, `libssl-dev` and `libffi-dev` are also required
`sudo apt-get install python-dev libssl-dev libffi-dev` `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` * 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 Ubuntu the `imagemagick` package needs to be installed.
On Windows do `choco install imagemagick`, which requires [chocolatey :ext](https://chocolatey.org/), 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: 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. 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 ## pip
Alternatively you can manually install the command line version with Alternatively you can manually install the command line version with

View File

@ -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. Lektor by copy/pasting a command into your terminal.
!!! Scared about copy/pasting this into a terminal? We will not do anything !!! 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 before asking you for confirmation and you can download [the script](https://www.getlektor.com/installer.py)
to see what it's doing. upfront to see what it's doing.
### Mac/Linux ### Mac/Linux
This will install Lektor for you but you might have to run it with `sudo` if This will install Lektor in your `HOME`. Running it with `sudo` will result in
your current user does not have rights to write into `/usr/local`. 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 You might need additional dependencies for this installation. For more
@ -40,18 +40,19 @@ information see [Installation](../docs/installation/).
### Windows ### 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 ## 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. 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.