Commit 23667d28 by Sébastien Eustace Committed by GitHub

New installation method (#378)

* Implement the new installer (UNIX)

* Add a -y/—yes option to the installer

* Update Travis config

* Add Windows support to the custom installer

* Build releases automatically and use them to install

* Do not build automatically for now

* Use docker to build releases for linux

* Update documentation

* Use Github to download releases
parent 004514d7
...@@ -7,6 +7,8 @@ environment: ...@@ -7,6 +7,8 @@ environment:
- PYTHON: "C:/Python27-x64" - PYTHON: "C:/Python27-x64"
- PYTHON: "C:/Python35-x64" - PYTHON: "C:/Python35-x64"
- PYTHON: "C:/Python36-x64" - PYTHON: "C:/Python36-x64"
- PYTHON: "C:/Python37-x64"
install: install:
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
...@@ -15,8 +17,8 @@ install: ...@@ -15,8 +17,8 @@ install:
# about it being out of date. # about it being out of date.
- "python -m pip install --disable-pip-version-check --user --upgrade pip" - "python -m pip install --disable-pip-version-check --user --upgrade pip"
# Create poetry virtualenv # Installing Poetry
- "python -m pip install poetry --pre" - "python -m pip install poetry --pre -U"
# Install dependencies # Install dependencies
- "poetry install -v" - "poetry install -v"
......
...@@ -29,9 +29,9 @@ setup.cfg ...@@ -29,9 +29,9 @@ setup.cfg
MANIFEST.in MANIFEST.in
/setup.py /setup.py
/docs/site/* /docs/site/*
pyproject.lock
/tests/fixtures/simple_project/setup.py /tests/fixtures/simple_project/setup.py
/tests/fixtures/project_with_extras/setup.py /tests/fixtures/project_with_extras/setup.py
.mypy_cache .mypy_cache
.venv .venv
/releases/*
...@@ -7,26 +7,25 @@ stages: ...@@ -7,26 +7,25 @@ stages:
cache: cache:
pip: true pip: true
directories: directories:
- $HOME/.cache/pypoetry - "$HOME/.cache/pypoetry"
- $HOME/.cache/pre-commit - "$HOME/.cache/pre-commit"
install: install:
- pip install poetry --pre - pip install pip -U
- pip install poetry --pre -U
- poetry install -v - poetry install -v
script: pytest -q tests/ script: pytest -q tests/
jobs: jobs:
include: include:
- python: "2.7" - python: '2.7'
- python: "3.4" - python: '3.5'
- python: "3.5" - python: '3.6'
- python: "3.6" - stage: linting
python: '3.6'
- stage: linting install:
python: "3.6" - pip install pre-commit
install: - pre-commit install-hooks
- pip install pre-commit script:
- pre-commit install-hooks - pre-commit run --all-files
script:
- pre-commit run --all-files
# This file is part of Poetry
# https://github.com/sdispater/poetry
# Licensed under the MIT license:
# http://www.opensource.org/licenses/MIT-license
# Copyright (c) 2018 Sébastien Eustace
POETRY_RELEASE := $$(sed -n -E "s/__version__ = '(.+)'/\1/p" poetry/__version__.py)
# lists all available targets
list:
@sh -c "$(MAKE) -p no_targets__ | \
awk -F':' '/^[a-zA-Z0-9][^\$$#\/\\t=]*:([^=]|$$)/ {\
split(\$$1,A,/ /);for(i in A)print A[i]\
}' | grep -v '__\$$' | grep -v 'make\[1\]' | grep -v 'Makefile' | sort"
# required for list
no_targets__:
# install all dependencies
setup: setup-python
# test your application (tests in the tests/ directory)
test:
@py.test --cov=poetry --cov-config .coveragerc tests/ -sq
release: build linux_release osx_release
build:
@poetry build
@python sonnet make:release
publish:
@poetry publish
wheel:
@poetry build -v
linux_release:
docker pull quay.io/pypa/manylinux1_x86_64
docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/make-linux-release.sh
# run tests against all supported python versions
tox:
@tox
...@@ -20,30 +20,70 @@ recommended way of installing `poetry`. ...@@ -20,30 +20,70 @@ recommended way of installing `poetry`.
curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python
``` ```
Alternatively, you can download the `get-poetry.py` file and execute it separately. !!! note
If you want to install prerelease versions, you can do so by passing `--preview` to `get-poetry.py`: You only need to install Poetry once. It will automatically pick up the current
Python version and use it to [create virtualenvs](/docs/basic-usage/#poetry-and-virtualenvs) accordingly.
```bash The installer installs the `poetry` tool to Poetry's `bin` directory.
python get-poetry.py --preview On Unix it is located at `$HOME/.poetry/bin` and on Windows at `%USERPROFILE%\.poetry\bin`.
```
Similarly, if you want to install a specific version, you can use `--version`: This directory will be in your `$PATH` environment variable,
which means you can run them from the shell without further configuration.
Open a new shell and type the following:
```bash ```bash
python get-poetry.py --version 0.7.0 poetry --version
``` ```
If you see something like `Poetry 0.11.4` then you are ready to use Poetry.
If you decide Poetry isn't your thing, you can completely remove it from your system
by running the installer again with the `--uninstall` option.
!!!note !!!note
Using `pip` to install `poetry` is also possible. Alternatively, you can download the `get-poetry.py` file and execute it separately.
If you want to install prerelease versions, you can do so by passing `--preview` to `get-poetry.py`:
```bash
python get-poetry.py --preview
```
Similarly, if you want to install a specific version, you can use `--version`:
```bash ```bash
pip install --user poetry python get-poetry.py --version 0.7.0
``` ```
Be aware, however, that it will also install poetry's dependencies Note that the installer does not support Poetry releases < 12.0.
which might cause conflicts.
### Alternative installation methods (not recommended)
#### Installing with `pip`
Using `pip` to install Poetry is possible.
```bash
pip install --user poetry
```
!!!warning
Be aware that it will also install Poetry's dependencies
which might cause conflicts with other packages.
#### Installing with `pipsi`
Using [`pipsi`](https://github.com/mitsuhiko/pipsi) to install Poetry is also possible.
```bash
pipsi install poetry
```
Make sure your installed version of `pipsi` is at least version `0.10`,
otherwise Poetry will not function properly. You can get it from its
[Github repository](https://github.com/mitsuhiko/pipsi).
## Updating `poetry` ## Updating `poetry`
...@@ -67,6 +107,11 @@ to `self:update`. ...@@ -67,6 +107,11 @@ to `self:update`.
poetry self:update 0.8.0 poetry self:update 0.8.0
``` ```
!!!note
The `self:update` command will only work if you used the recommended
installer to install Poetry.
## Enable tab completion for Bash, Fish, or Zsh ## Enable tab completion for Bash, Fish, or Zsh
...@@ -91,7 +136,7 @@ poetry completions zsh > ~/.zfunc/_poetry ...@@ -91,7 +136,7 @@ poetry completions zsh > ~/.zfunc/_poetry
!!! note !!! note
You may need to restart your shell in order for the changes to take effect. You may need to restart your shell in order for the changes to take effect.
For `zsh`, you must then add the following line in your `~/.zshrc` before `compinit`: For `zsh`, you must then add the following line in your `~/.zshrc` before `compinit`:
```bash ```bash
......
#!/bin/bash
PYTHON_VERSIONS="cp27-cp27m cp34-cp34m cp35-cp35m cp36-cp36m cp37-cp37m"
cd /io
/opt/python/cp37-cp37m/bin/pip install poetry --pre -U
/opt/python/cp37-cp37m/bin/poetry config settings.virtualenvs.create false
/opt/python/cp37-cp37m/bin/poetry install --no-dev
/opt/python/cp37-cp37m/bin/python sonnet make:release \
-P "2.7:/opt/python/cp27-cp27m/bin/python" \
-P "3.4:/opt/python/cp34-cp34m/bin/python" \
-P "3.5:/opt/python/cp35-cp35m/bin/python" \
-P "3.6:/opt/python/cp36-cp36m/bin/python" \
-P "3.7:/opt/python/cp37-cp37m/bin/python"
cd -
...@@ -3,8 +3,11 @@ import sys ...@@ -3,8 +3,11 @@ import sys
_ROOT = os.path.dirname(os.path.realpath(__file__)) _ROOT = os.path.dirname(os.path.realpath(__file__))
_VENDOR = os.path.join(_ROOT, "_vendor") _VENDOR = os.path.join(_ROOT, "_vendor")
_CURRENT_VENDOR = os.path.join(
_VENDOR, "py{}".format(".".join(str(v) for v in sys.version_info[:2]))
)
# Add vendored dependencies to path. # Add vendored dependencies to path.
sys.path.insert(0, _VENDOR) sys.path.insert(0, _CURRENT_VENDOR)
from .__version__ import __version__ # noqa from .__version__ import __version__ # noqa
import hashlib
import os import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tarfile
from email.parser import Parser
from functools import cmp_to_key from functools import cmp_to_key
from gzip import GzipFile
try:
from urllib.error import HTTPError
from urllib.request import urlopen
except ImportError:
from urllib2 import HTTPError
from urllib2 import urlopen
from ..command import Command from ..command import Command
...@@ -19,12 +27,41 @@ class SelfUpdateCommand(Command): ...@@ -19,12 +27,41 @@ class SelfUpdateCommand(Command):
{ --preview : Install prereleases. } { --preview : Install prereleases. }
""" """
BASE_URL = "https://poetry.eustace.io"
@property
def home(self):
from poetry.utils._compat import Path
from poetry.utils.appdirs import expanduser
home = Path(expanduser("~"))
return home / ".poetry"
@property
def lib(self):
return self.home / "lib"
@property
def lib_backup(self):
return self.home / "lib-backup"
def handle(self): def handle(self):
from poetry.__version__ import __version__ from poetry.__version__ import __version__
from poetry.repositories.pypi_repository import PyPiRepository from poetry.repositories.pypi_repository import PyPiRepository
from poetry.semver import Version from poetry.semver import Version
from poetry.utils._compat import Path
from poetry.utils._compat import decode from poetry.utils._compat import decode
current = Path(__file__)
try:
current.relative_to(self.home)
except ValueError:
raise RuntimeError(
"Poetry was not installed with the recommended installer. "
"Cannot update automatically."
)
version = self.argument("version") version = self.argument("version")
if not version: if not version:
version = ">=" + __version__ version = ">=" + __version__
...@@ -83,98 +120,103 @@ class SelfUpdateCommand(Command): ...@@ -83,98 +120,103 @@ class SelfUpdateCommand(Command):
return e.returncode return e.returncode
def update(self, release): def update(self, release):
from poetry.utils._compat import Path
from poetry.utils.helpers import temporary_directory
version = release.version version = release.version
self.line("Updating to <info>{}</info>".format(version)) self.line("Updating to <info>{}</info>".format(version))
prefix = sys.prefix if self.lib_backup.exists():
base_prefix = getattr(sys, "base_prefix", None) shutil.rmtree(str(self.lib_backup))
real_prefix = getattr(sys, "real_prefix", None)
prefix_poetry = self._bin_path(Path(prefix), "poetry")
if prefix_poetry.exists():
pip = self._bin_path(prefix_poetry.parent.parent, "pip").resolve()
elif (
base_prefix
and base_prefix != prefix
and self._bin_path(Path(base_prefix), "poetry").exists()
):
pip = self._bin_path(Path(base_prefix), "pip")
elif real_prefix:
pip = self._bin_path(Path(real_prefix), "pip")
else:
pip = self._bin_path(Path(prefix), "pip")
if not pip.exists():
raise RuntimeError("Unable to determine poetry's path")
with temporary_directory(prefix="poetry-update-") as temp_dir:
temp_dir = Path(temp_dir)
dist = temp_dir / "dist"
self.line(" - Getting dependencies")
self.process(
str(pip),
"install",
"-U",
"poetry=={}".format(release.version),
"--target",
str(dist),
)
self.line(" - Vendorizing dependencies")
poetry_dir = dist / "poetry"
vendor_dir = poetry_dir / "_vendor"
# Everything, except poetry itself, should
# be put in the _vendor directory
for file in dist.glob("*"):
if file.name.startswith("poetry"):
continue
dest = vendor_dir / file.name # Backup the current installation
if file.is_dir(): if self.lib.exists():
shutil.copytree(str(file), str(dest)) shutil.copytree(str(self.lib), str(self.lib_backup))
shutil.rmtree(str(file)) shutil.rmtree(str(self.lib))
else:
shutil.copy(str(file), str(dest))
os.unlink(str(file))
wheel_data = dist / "poetry-{}.dist-info".format(version) / "WHEEL" try:
with wheel_data.open() as f: self._update(version)
wheel_data = Parser().parsestr(f.read()) except Exception:
if not self.lib_backup.exists():
raise
shutil.copytree(str(self.lib_backup), str(self.lib))
shutil.rmtree(str(self.lib_backup))
raise
finally:
if self.lib_backup.exists():
shutil.rmtree(str(self.lib_backup))
self.line("")
self.line(
"<info>Poetry</info> (<comment>{}</comment>) is installed now. Great!".format(
version
)
)
tag = wheel_data["Tag"] def _update(self, version):
from poetry.utils.helpers import temporary_directory
# Repack everything and install checksum = "poetry-{}-{}.sha256sum".format(version, sys.platform)
self.line(" - Updating <info>poetry</info>")
shutil.make_archive( try:
str(temp_dir / "poetry-{}-{}".format(version, tag)), r = urlopen(self.BASE_URL + "/releases/{}".format(checksum))
format="zip", except HTTPError as e:
root_dir=str(dist), if e.code == 404:
) raise RuntimeError("Could not find {} file".format(checksum))
os.rename( raise
str(temp_dir / "poetry-{}-{}.zip".format(version, tag)),
str(temp_dir / "poetry-{}-{}.whl".format(version, tag)),
)
self.process( checksum = r.read().decode()
str(pip),
"install",
"--upgrade",
"--no-deps",
str(temp_dir / "poetry-{}-{}.whl".format(version, tag)),
)
self.line("") # We get the payload from the remote host
self.line( name = "poetry-{}-{}.tar.gz".format(version, sys.platform)
"<info>poetry</> (<comment>{}</>) " try:
"successfully installed!".format(version) r = urlopen(self.BASE_URL + "/releases/{}".format(name))
) except HTTPError as e:
if e.code == 404:
raise RuntimeError("Could not find {} file".format(name))
raise
meta = r.info()
size = int(meta["Content-Length"])
current = 0
block_size = 8192
bar = self.progress_bar(max=size)
bar.set_format(" - Downloading <info>{}</> <comment>%percent%%</>".format(name))
bar.start()
sha = hashlib.sha256()
with temporary_directory(prefix="poetry-updater-") as dir_:
tar = os.path.join(dir_, name)
with open(tar, "wb") as f:
while True:
buffer = r.read(block_size)
if not buffer:
break
current += len(buffer)
f.write(buffer)
sha.update(buffer)
bar.set_progress(current)
bar.finish()
# Checking hashes
if checksum != sha.hexdigest():
raise RuntimeError(
"Hashes for {} do not match: {} != {}".format(
name, checksum, sha.hexdigest()
)
)
gz = GzipFile(tar, mode="rb")
try:
with tarfile.TarFile(tar, fileobj=gz, format=tarfile.PAX_FORMAT) as f:
f.extractall(str(self.lib))
finally:
gz.close()
def process(self, *args): def process(self, *args):
return subprocess.check_output(list(args), stderr=subprocess.STDOUT) return subprocess.check_output(list(args), stderr=subprocess.STDOUT)
......
This diff is collapsed. Click to expand it.
#!/usr/bin/env python
import hashlib
import os
import shutil
import subprocess
import sys
import tarfile
from gzip import GzipFile
from cleo import Application
from cleo import Command
WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt")
class MakeReleaseCommand(Command):
"""
Makes a self-contained package of Poetry.
make:release
{--P|python=?* : Python version to use}
"""
PYTHON = {
"2.7": "python2.7",
"3.4": "python3.4",
"3.5": "python3.5",
"3.6": "python3.6",
"3.7": "python3.7",
}
def handle(self):
pythons = self.PYTHON
if self.option("python"):
pythons = {}
for python in self.option("python"):
parts = python.split(":", 1)
if len(parts) == 1:
pythons[parts[0]] = self.PYTHON[parts[0]]
version, python = parts
pythons[version] = python
self.check_system(pythons)
from poetry import __version__
from poetry.poetry import Poetry
from poetry.puzzle import Solver
from poetry.repositories.pool import Pool
from poetry.repositories.repository import Repository
from poetry.utils._compat import Path
from poetry.utils.helpers import temporary_directory
project = Poetry.create(Path.cwd())
package = project.package
del package.dev_requires[:]
# We only use the lock file to resolve the dependencies
pool = Pool()
pool.add_repository(project.locker.locked_repository(with_dev_reqs=True))
with temporary_directory() as tmp_dir:
# Copy poetry to tmp dir
poetry_dir = os.path.join(tmp_dir, "poetry")
shutil.copytree(
os.path.join(os.path.dirname(__file__), "poetry"), poetry_dir
)
for version, python in sorted(pythons.items()):
self.line(
"<info>Preparing files for Python <comment>{}</comment></info>".format(
version
)
)
with package.with_python_versions("^{}".format(version)):
solver = Solver(
package, pool, Repository(), Repository(), self.output
)
ops = solver.solve()
self.vendorize_for_python(
python, [op.package for op in ops], poetry_dir, version
)
self.line("")
self.line("<info>Packaging files</info>")
with temporary_directory() as tmp_dir2:
base_name = "poetry-{}-{}".format(__version__, sys.platform)
name = "{}.tar.gz".format(base_name)
gz = GzipFile(os.path.join(tmp_dir2, name), mode="wb")
try:
with tarfile.TarFile(
os.path.join(tmp_dir2, name),
mode="w",
fileobj=gz,
format=tarfile.PAX_FORMAT,
) as tar:
for root, dirs, files in os.walk(tmp_dir):
for f in files:
if f.endswith(".pyc"):
continue
path = os.path.join(os.path.realpath(root), f)
relpath = os.path.relpath(
path, os.path.realpath(tmp_dir)
)
tar_info = tar.gettarinfo(str(path), arcname=relpath)
if tar_info.isreg():
with open(path, "rb") as f:
tar.addfile(tar_info, f)
else:
tar.addfile(tar_info) # Symlinks & ?
finally:
gz.close()
releases_dir = os.path.join(os.path.dirname(__file__), "releases")
if not os.path.exists(releases_dir):
os.mkdir(releases_dir)
shutil.copyfile(
os.path.join(tmp_dir2, name), os.path.join(releases_dir, name)
)
# Compute hash
sha = hashlib.sha256()
with open(os.path.join(releases_dir, name), "rb") as f:
while True:
buffer = f.read(8192)
if not buffer:
break
sha.update(buffer)
with open(
os.path.join(releases_dir, "{}.sha256sum".format(base_name)), "w"
) as f:
f.write(sha.hexdigest())
self.line("<info>Built <comment>{}</comment></info>".format(name))
def check_system(self, pythons):
for version, python in sorted(pythons.items()):
try:
subprocess.check_output(
[python, "-V"], stderr=subprocess.STDOUT, shell=WINDOWS
)
except subprocess.CalledProcessError:
raise RuntimeError("Python {} is not available".format(version))
def vendorize_for_python(self, python, packages, dest, python_version):
vendor_dir = os.path.join(dest, "_vendor", "py{}".format(python_version))
bar = self.progress_bar(max=len(packages))
bar.set_format("%message% %current%/%max%")
bar.set_message(
"<info>Vendorizing dependencies for Python <comment>{}</comment></info>".format(
python_version
)
)
bar.start()
for package in packages:
subprocess.check_output(
[
python,
"-m",
"pip",
"install",
package.name,
"--no-deps",
"--target",
vendor_dir,
],
stderr=subprocess.STDOUT,
shell=WINDOWS,
)
bar.advance()
bar.finish()
self.line("")
app = Application("Sonnet")
app.add(MakeReleaseCommand())
if __name__ == "__main__":
app.run()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment