diff --git a/MANIFEST.in b/MANIFEST.in index 3bbaceac44..160e1d2d42 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include LICENSE include *.txt include *.md +include pact/bin/* prune pact/test -prune pact/bin prune e2e diff --git a/README.md b/README.md index b1c3d1587e..2a73c65757 100644 --- a/README.md +++ b/README.md @@ -437,7 +437,7 @@ To setup a development environment: 1. If you want to run tests for all Python versions, install 2.7, 3.3, 3.4, 3.5, and 3.6 from source or using a tool like [pyenv] 2. Its recommended to create a Python [virtualenv] for the project -The setup the environment, run tests, and package the application, run: +To setup the environment, run tests, and package the application, run: `make release` If you are just interested in packaging pact-python so you can install it using pip: @@ -449,6 +449,15 @@ From there you can use pip to install it: `pip install ./dist/pact-python-N.N.N.tar.gz` +## Offline Installation of Standalone Packages + +Although all Ruby standalone applications are predownloaded into the wheel artifact, it may be useful, for development, purposes to install custom Ruby binaries. In which case, use the `bin-path` flag. +``` +pip install pact-python --bin-path=/absolute/path/to/folder/containing/pact/binaries/for/your/os +``` + +Pact binaries can be found at [Pact Ruby Releases](https://github.com/pact-foundation/pact-ruby-standalone/releases). + ## Testing This project has unit and end to end tests, which can both be run from make: diff --git a/setup.py b/setup.py index 6eb0565d18..5ff027fcfa 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ import os import platform +import shutil import sys import tarfile @@ -10,11 +11,15 @@ from setuptools import setup from setuptools.command.develop import develop from setuptools.command.install import install +from distutils.command.sdist import sdist as sdist_orig IS_64 = sys.maxsize > 2 ** 32 PACT_STANDALONE_VERSION = '1.88.51' - +PACT_STANDALONE_SUFFIXES = ['osx.tar.gz', + 'linux-x86_64.tar.gz', + 'linux-x86.tar.gz', + 'win32.zip'] here = os.path.abspath(os.path.dirname(__file__)) @@ -22,6 +27,23 @@ with open(os.path.join(here, "pact", "__version__.py")) as f: exec(f.read(), about) +class sdist(sdist_orig): + """ + Subclass sdist so that we can download all standalone ruby applications + into ./pact/bin so our users receive all the binaries on pip install. + """ + def run(self): + package_bin_path = os.path.join(os.path.dirname(__file__), 'pact', 'bin') + + if os.path.exists(package_bin_path): + shutil.rmtree(package_bin_path, ignore_errors=True) + os.mkdir(package_bin_path) + + for suffix in PACT_STANDALONE_SUFFIXES: + filename = ('pact-{version}-{suffix}').format(version=PACT_STANDALONE_VERSION, suffix=suffix) + download_ruby_app_binary(package_bin_path, filename, suffix) + super().run() + class PactPythonDevelopCommand(develop): """ @@ -35,11 +57,11 @@ class PactPythonDevelopCommand(develop): def run(self): """Install ruby command.""" develop.run(self) - bin_path = os.path.join(os.path.dirname(__file__), 'pact', 'bin') - if not os.path.exists(bin_path): - os.mkdir(bin_path) + package_bin_path = os.path.join(os.path.dirname(__file__), 'pact', 'bin') + if not os.path.exists(package_bin_path): + os.mkdir(package_bin_path) - install_ruby_app(bin_path) + install_ruby_app(package_bin_path, download_bin_path=None) class PactPythonInstallCommand(install): @@ -48,25 +70,66 @@ class PactPythonInstallCommand(install): Installs the Python package and unpacks the platform appropriate version of the Ruby mock service and provider verifier. + + User Options: + --bin-path An absolute folder path containing predownloaded pact binaries + that should be used instead of fetching from the internet. """ + user_options = install.user_options + [('bin-path=', None, None)] + + def initialize_options(self): + """Load our preconfigured options""" + install.initialize_options(self) + self.bin_path = None + + def finalize_options(self): + """Load provided CLI arguments into our options""" + install.finalize_options(self) + def run(self): """Install python binary.""" install.run(self) - bin_path = os.path.join(self.install_lib, 'pact', 'bin') - os.mkdir(bin_path) - install_ruby_app(bin_path) + package_bin_path = os.path.join(self.install_lib, 'pact', 'bin') + if not os.path.exists(package_bin_path): + os.mkdir(package_bin_path) + install_ruby_app(package_bin_path, self.bin_path) -def install_ruby_app(bin_path): +def install_ruby_app(package_bin_path, download_bin_path): """ - Download a Ruby application and install it for use. + Installs the ruby standalone application for this OS. - :param bin_path: The path where binaries should be installed. + :param package_bin_path: The path where we want our pact binaries unarchived. + :param download_bin_path: An optional path containing pre-downloaded pact binaries. + """ + + binary = ruby_app_binary() + if download_bin_path is None: + download_bin_path = package_bin_path + + path = os.path.join(download_bin_path, binary['filename']) + + if os.path.isfile(path) is True: + extract_ruby_app_binary(download_bin_path, package_bin_path, binary['filename']) + else: + if download_bin_path is not None: + if os.path.isfile(path) is not True: + raise RuntimeError('Could not find {} binary.'.format(path)) + extract_ruby_app_binary(download_bin_path, package_bin_path, binary['filename']) + else: + download_ruby_app_binary(package_bin_path, binary['filename'], binary['suffix']) + extract_ruby_app_binary(package_bin_path, package_bin_path, binary['filename']) + +def ruby_app_binary(): + """ + Determines the ruby app binary required for this OS. + + :return A dictionary of type {'filename': string, 'version': string, 'suffix': string } """ target_platform = platform.platform().lower() - uri = ('https://github.com/pact-foundation/pact-ruby-standalone/releases' - '/download/v{version}/pact-{version}-{suffix}') + + binary = ('pact-{version}-{suffix}') if 'darwin' in target_platform or 'macos' in target_platform: suffix = 'osx.tar.gz' @@ -82,12 +145,26 @@ def install_ruby_app(bin_path): platform.platform()) raise Exception(msg) + binary = binary.format(version=PACT_STANDALONE_VERSION, suffix=suffix) + return {'filename': binary, 'version': PACT_STANDALONE_VERSION, 'suffix': suffix} + +def download_ruby_app_binary(path_to_download_to, filename, suffix): + """ + Downloads `binary` into `path_to_download_to`. + + :param path_to_download_to: The path where binaries should be downloaded. + :param filename: The filename that should be installed. + :param suffix: The suffix of the standalone app to install. + """ + uri = ('https://github.com/pact-foundation/pact-ruby-standalone/releases' + '/download/v{version}/pact-{version}-{suffix}') + if sys.version_info.major == 2: from urllib import urlopen else: from urllib.request import urlopen - path = os.path.join(bin_path, suffix) + path = os.path.join(path_to_download_to, filename) resp = urlopen(uri.format(version=PACT_STANDALONE_VERSION, suffix=suffix)) with open(path, 'wb') as f: if resp.code == 200: @@ -97,12 +174,21 @@ def install_ruby_app(bin_path): 'Received HTTP {} when downloading {}'.format( resp.code, resp.url)) +def extract_ruby_app_binary(source, destination, binary): + """ + Extracts the ruby app binary from `source` into `destination`. + + :param source: The location of the binary to unarchive. + :param destination: The location to unarchive to. + :param binary: The binary that needs to be unarchived. + """ + path = os.path.join(source, binary) if 'windows' in platform.platform().lower(): with ZipFile(path) as f: - f.extractall(bin_path) + f.extractall(destination) else: with tarfile.open(path) as f: - f.extractall(bin_path) + f.extractall(destination) def read(filename): @@ -126,7 +212,8 @@ def read(filename): setup( cmdclass={ 'develop': PactPythonDevelopCommand, - 'install': PactPythonInstallCommand}, + 'install': PactPythonInstallCommand, + 'sdist': sdist}, name='pact-python', version=about['__version__'], description=(