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:
- PYTHON: "C:/Python27-x64"
- PYTHON: "C:/Python35-x64"
- PYTHON: "C:/Python36-x64"
- PYTHON: "C:/Python37-x64"
install:
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
......@@ -15,8 +17,8 @@ install:
# about it being out of date.
- "python -m pip install --disable-pip-version-check --user --upgrade pip"
# Create poetry virtualenv
- "python -m pip install poetry --pre"
# Installing Poetry
- "python -m pip install poetry --pre -U"
# Install dependencies
- "poetry install -v"
......
......@@ -29,9 +29,9 @@ setup.cfg
MANIFEST.in
/setup.py
/docs/site/*
pyproject.lock
/tests/fixtures/simple_project/setup.py
/tests/fixtures/project_with_extras/setup.py
.mypy_cache
.venv
/releases/*
......@@ -7,24 +7,23 @@ stages:
cache:
pip: true
directories:
- $HOME/.cache/pypoetry
- $HOME/.cache/pre-commit
- "$HOME/.cache/pypoetry"
- "$HOME/.cache/pre-commit"
install:
- pip install poetry --pre
- pip install pip -U
- pip install poetry --pre -U
- poetry install -v
script: pytest -q tests/
jobs:
include:
- python: "2.7"
- python: "3.4"
- python: "3.5"
- python: "3.6"
- python: '2.7'
- python: '3.5'
- python: '3.6'
- stage: linting
python: "3.6"
python: '3.6'
install:
- pip install pre-commit
- pre-commit install-hooks
......
# 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`.
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
python get-poetry.py --preview
```
The installer installs the `poetry` tool to Poetry's `bin` directory.
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
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
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
pip install --user poetry
python get-poetry.py --preview
```
Be aware, however, that it will also install poetry's dependencies
which might cause conflicts.
Similarly, if you want to install a specific version, you can use `--version`:
```bash
python get-poetry.py --version 0.7.0
```
Note that the installer does not support Poetry releases < 12.0.
### 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`
......@@ -67,6 +107,11 @@ to `self:update`.
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
......
#!/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
_ROOT = os.path.dirname(os.path.realpath(__file__))
_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.
sys.path.insert(0, _VENDOR)
sys.path.insert(0, _CURRENT_VENDOR)
from .__version__ import __version__ # noqa
import hashlib
import os
import shutil
import subprocess
import sys
from email.parser import Parser
import tarfile
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
......@@ -19,12 +27,41 @@ class SelfUpdateCommand(Command):
{ --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):
from poetry.__version__ import __version__
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.semver import Version
from poetry.utils._compat import Path
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")
if not version:
version = ">=" + __version__
......@@ -83,99 +120,104 @@ class SelfUpdateCommand(Command):
return e.returncode
def update(self, release):
from poetry.utils._compat import Path
from poetry.utils.helpers import temporary_directory
version = release.version
self.line("Updating to <info>{}</info>".format(version))
prefix = sys.prefix
base_prefix = getattr(sys, "base_prefix", None)
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),
if self.lib_backup.exists():
shutil.rmtree(str(self.lib_backup))
# Backup the current installation
if self.lib.exists():
shutil.copytree(str(self.lib), str(self.lib_backup))
shutil.rmtree(str(self.lib))
try:
self._update(version)
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
)
)
self.line(" - Vendorizing dependencies")
def _update(self, version):
from poetry.utils.helpers import temporary_directory
poetry_dir = dist / "poetry"
vendor_dir = poetry_dir / "_vendor"
checksum = "poetry-{}-{}.sha256sum".format(version, sys.platform)
# Everything, except poetry itself, should
# be put in the _vendor directory
for file in dist.glob("*"):
if file.name.startswith("poetry"):
continue
try:
r = urlopen(self.BASE_URL + "/releases/{}".format(checksum))
except HTTPError as e:
if e.code == 404:
raise RuntimeError("Could not find {} file".format(checksum))
dest = vendor_dir / file.name
if file.is_dir():
shutil.copytree(str(file), str(dest))
shutil.rmtree(str(file))
else:
shutil.copy(str(file), str(dest))
os.unlink(str(file))
raise
wheel_data = dist / "poetry-{}.dist-info".format(version) / "WHEEL"
with wheel_data.open() as f:
wheel_data = Parser().parsestr(f.read())
checksum = r.read().decode()
tag = wheel_data["Tag"]
# We get the payload from the remote host
name = "poetry-{}-{}.tar.gz".format(version, sys.platform)
try:
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
# Repack everything and install
self.line(" - Updating <info>poetry</info>")
current += len(buffer)
f.write(buffer)
sha.update(buffer)
shutil.make_archive(
str(temp_dir / "poetry-{}-{}".format(version, tag)),
format="zip",
root_dir=str(dist),
)
bar.set_progress(current)
os.rename(
str(temp_dir / "poetry-{}-{}.zip".format(version, tag)),
str(temp_dir / "poetry-{}-{}.whl".format(version, tag)),
)
bar.finish()
self.process(
str(pip),
"install",
"--upgrade",
"--no-deps",
str(temp_dir / "poetry-{}-{}.whl".format(version, tag)),
# Checking hashes
if checksum != sha.hexdigest():
raise RuntimeError(
"Hashes for {} do not match: {} != {}".format(
name, checksum, sha.hexdigest()
)
self.line("")
self.line(
"<info>poetry</> (<comment>{}</>) "
"successfully installed!".format(version)
)
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):
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