diff options
Diffstat (limited to 'setup.py')
-rw-r--r-- | setup.py | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..b15d4c96 --- /dev/null +++ b/setup.py @@ -0,0 +1,428 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json +import os +import os.path +import re +import sys +import warnings + +from collections import defaultdict + +try: + from setuptools import setup, find_packages + from setuptools.command.build_py import build_py as BuildPy + from setuptools.command.install_lib import install_lib as InstallLib + from setuptools.command.install_scripts import install_scripts as InstallScripts +except ImportError: + print("Ansible now needs setuptools in order to build. Install it using" + " your package manager (usually python-setuptools) or via pip (pip" + " install setuptools).", file=sys.stderr) + sys.exit(1) + +# `distutils` must be imported after `setuptools` or it will cause explosions +# with `setuptools >=48.0.0, <49.1`. +# Refs: +# * https://github.com/ansible/ansible/issues/70456 +# * https://github.com/pypa/setuptools/issues/2230 +# * https://github.com/pypa/setuptools/commit/bd110264 +from distutils.command.build_scripts import build_scripts as BuildScripts +from distutils.command.sdist import sdist as SDist + + +def find_package_info(*file_paths): + try: + with open(os.path.join(*file_paths), 'r') as f: + info_file = f.read() + except Exception: + raise RuntimeError("Unable to find package info.") + + # The version line must have the form + # __version__ = 'ver' + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + info_file, re.M) + author_match = re.search(r"^__author__ = ['\"]([^'\"]*)['\"]", + info_file, re.M) + + if version_match and author_match: + return version_match.group(1), author_match.group(1) + raise RuntimeError("Unable to find package info.") + + +def _validate_install_ansible_base(): + """Validate that we can install ansible-base. Currently this only + cares about upgrading to ansible-base from ansible<2.10 + """ + # Skip common commands we can ignore + # Do NOT add bdist_wheel here, we don't ship wheels + # and bdist_wheel is the only place we can prevent pip + # from installing, as pip creates a wheel, and installs the wheel + # and we have no influence over installation within a wheel + if set(('sdist', 'egg_info')).intersection(sys.argv): + return + + if os.getenv('ANSIBLE_SKIP_CONFLICT_CHECK', '') not in ('', '0'): + return + + # Save these for later restoring things to pre invocation + sys_modules = sys.modules.copy() + sys_modules_keys = set(sys_modules) + + # Make sure `lib` isn't in `sys.path` that could confuse this + sys_path = sys.path[:] + abspath = os.path.abspath + sys.path[:] = [p for p in sys.path if abspath(p) != abspath('lib')] + + try: + from ansible.release import __version__ + except ImportError: + pass + else: + version_tuple = tuple(int(v) for v in __version__.split('.')[:2]) + if version_tuple < (2, 10): + stars = '*' * 76 + raise RuntimeError( + ''' + + %s + + Cannot install ansible-base with a pre-existing ansible==%s + installation. + + Installing ansible-base with ansible-2.9 or older currently installed with + pip is known to cause problems. Please uninstall ansible and install the new + version: + + pip uninstall ansible + pip install ansible-base + + If you want to skip the conflict checks and manually resolve any issues + afterwards, set the ANSIBLE_SKIP_CONFLICT_CHECK environment variable: + + ANSIBLE_SKIP_CONFLICT_CHECK=1 pip install ansible-base + + %s + ''' % (stars, __version__, stars) + ) + finally: + sys.path[:] = sys_path + for key in sys_modules_keys.symmetric_difference(sys.modules): + sys.modules.pop(key, None) + sys.modules.update(sys_modules) + + +_validate_install_ansible_base() + + +SYMLINK_CACHE = 'SYMLINK_CACHE.json' + + +def _find_symlinks(topdir, extension=''): + """Find symlinks that should be maintained + + Maintained symlinks exist in the bin dir or are modules which have + aliases. Our heuristic is that they are a link in a certain path which + point to a file in the same directory. + + .. warn:: + + We want the symlinks in :file:`bin/` that link into :file:`lib/ansible/*` (currently, + :command:`ansible`, :command:`ansible-test`, and :command:`ansible-connection`) to become + real files on install. Updates to the heuristic here *must not* add them to the symlink + cache. + """ + symlinks = defaultdict(list) + for base_path, dirs, files in os.walk(topdir): + for filename in files: + filepath = os.path.join(base_path, filename) + if os.path.islink(filepath) and filename.endswith(extension): + target = os.readlink(filepath) + if target.startswith('/'): + # We do not support absolute symlinks at all + continue + + if os.path.dirname(target) == '': + link = filepath[len(topdir):] + if link.startswith('/'): + link = link[1:] + symlinks[os.path.basename(target)].append(link) + else: + # Count how many directory levels from the topdir we are + levels_deep = os.path.dirname(filepath).count('/') + + # Count the number of directory levels higher we walk up the tree in target + target_depth = 0 + for path_component in target.split('/'): + if path_component == '..': + target_depth += 1 + # If we walk past the topdir, then don't store + if target_depth >= levels_deep: + break + else: + target_depth -= 1 + else: + # If we managed to stay within the tree, store the symlink + link = filepath[len(topdir):] + if link.startswith('/'): + link = link[1:] + symlinks[target].append(link) + + return symlinks + + +def _cache_symlinks(symlink_data): + with open(SYMLINK_CACHE, 'w') as f: + json.dump(symlink_data, f) + + +def _maintain_symlinks(symlink_type, base_path): + """Switch a real file into a symlink""" + try: + # Try the cache first because going from git checkout to sdist is the + # only time we know that we're going to cache correctly + with open(SYMLINK_CACHE, 'r') as f: + symlink_data = json.load(f) + except (IOError, OSError) as e: + # IOError on py2, OSError on py3. Both have errno + if e.errno == 2: + # SYMLINKS_CACHE doesn't exist. Fallback to trying to create the + # cache now. Will work if we're running directly from a git + # checkout or from an sdist created earlier. + library_symlinks = _find_symlinks('lib', '.py') + library_symlinks.update(_find_symlinks('test/lib')) + + symlink_data = {'script': _find_symlinks('bin'), + 'library': library_symlinks, + } + + # Sanity check that something we know should be a symlink was + # found. We'll take that to mean that the current directory + # structure properly reflects symlinks in the git repo + if 'ansible-playbook' in symlink_data['script']['ansible']: + _cache_symlinks(symlink_data) + else: + raise RuntimeError( + "Pregenerated symlink list was not present and expected " + "symlinks in ./bin were missing or broken. " + "Perhaps this isn't a git checkout?" + ) + else: + raise + symlinks = symlink_data[symlink_type] + + for source in symlinks: + for dest in symlinks[source]: + dest_path = os.path.join(base_path, dest) + if not os.path.islink(dest_path): + try: + os.unlink(dest_path) + except OSError as e: + if e.errno == 2: + # File does not exist which is all we wanted + pass + os.symlink(source, dest_path) + + +class BuildPyCommand(BuildPy): + def run(self): + BuildPy.run(self) + _maintain_symlinks('library', self.build_lib) + + +class BuildScriptsCommand(BuildScripts): + def run(self): + BuildScripts.run(self) + _maintain_symlinks('script', self.build_dir) + + +class InstallLibCommand(InstallLib): + def run(self): + InstallLib.run(self) + _maintain_symlinks('library', self.install_dir) + + +class InstallScriptsCommand(InstallScripts): + def run(self): + InstallScripts.run(self) + _maintain_symlinks('script', self.install_dir) + + +class SDistCommand(SDist): + def run(self): + # have to generate the cache of symlinks for release as sdist is the + # only command that has access to symlinks from the git repo + library_symlinks = _find_symlinks('lib', '.py') + library_symlinks.update(_find_symlinks('test/lib')) + + symlinks = {'script': _find_symlinks('bin'), + 'library': library_symlinks, + } + _cache_symlinks(symlinks) + + SDist.run(self) + + # Print warnings at the end because no one will see warnings before all the normal status + # output + if os.environ.get('_ANSIBLE_SDIST_FROM_MAKEFILE', False) != '1': + warnings.warn('When setup.py sdist is run from outside of the Makefile,' + ' the generated tarball may be incomplete. Use `make snapshot`' + ' to create a tarball from an arbitrary checkout or use' + ' `cd packaging/release && make release version=[..]` for official builds.', + RuntimeWarning) + + +def read_file(file_name): + """Read file and return its contents.""" + with open(file_name, 'r') as f: + return f.read() + + +def read_requirements(file_name): + """Read requirements file as a list.""" + reqs = read_file(file_name).splitlines() + if not reqs: + raise RuntimeError( + "Unable to read requirements from the %s file" + "That indicates this copy of the source code is incomplete." + % file_name + ) + return reqs + + +PYCRYPTO_DIST = 'pycrypto' + + +def get_crypto_req(): + """Detect custom crypto from ANSIBLE_CRYPTO_BACKEND env var. + + pycrypto or cryptography. We choose a default but allow the user to + override it. This translates into pip install of the sdist deciding what + package to install and also the runtime dependencies that pkg_resources + knows about. + """ + crypto_backend = os.environ.get('ANSIBLE_CRYPTO_BACKEND', '').strip() + + if crypto_backend == PYCRYPTO_DIST: + # Attempt to set version requirements + return '%s >= 2.6' % PYCRYPTO_DIST + + return crypto_backend or None + + +def substitute_crypto_to_req(req): + """Replace crypto requirements if customized.""" + crypto_backend = get_crypto_req() + + if crypto_backend is None: + return req + + def is_not_crypto(r): + CRYPTO_LIBS = PYCRYPTO_DIST, 'cryptography' + return not any(r.lower().startswith(c) for c in CRYPTO_LIBS) + + return [r for r in req if is_not_crypto(r)] + [crypto_backend] + + +def get_dynamic_setup_params(): + """Add dynamically calculated setup params to static ones.""" + return { + # Retrieve the long description from the README + 'long_description': read_file('README.rst'), + 'install_requires': substitute_crypto_to_req( + read_requirements('requirements.txt'), + ), + } + + +here = os.path.abspath(os.path.dirname(__file__)) +__version__, __author__ = find_package_info(here, 'lib', 'ansible', 'release.py') +static_setup_params = dict( + # Use the distutils SDist so that symlinks are not expanded + # Use a custom Build for the same reason + cmdclass={ + 'build_py': BuildPyCommand, + 'build_scripts': BuildScriptsCommand, + 'install_lib': InstallLibCommand, + 'install_scripts': InstallScriptsCommand, + 'sdist': SDistCommand, + }, + name='ansible-base', + version=__version__, + description='Radically simple IT automation', + author=__author__, + author_email='info@ansible.com', + url='https://ansible.com/', + project_urls={ + 'Bug Tracker': 'https://github.com/ansible/ansible/issues', + 'CI: Shippable': 'https://app.shippable.com/github/ansible/ansible', + 'Code of Conduct': 'https://docs.ansible.com/ansible/latest/community/code_of_conduct.html', + 'Documentation': 'https://docs.ansible.com/ansible/', + 'Mailing lists': 'https://docs.ansible.com/ansible/latest/community/communication.html#mailing-list-information', + 'Source Code': 'https://github.com/ansible/ansible', + }, + license='GPLv3+', + # Ansible will also make use of a system copy of python-six and + # python-selectors2 if installed but use a Bundled copy if it's not. + python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', + package_dir={'': 'lib', + 'ansible_test': 'test/lib/ansible_test'}, + packages=find_packages('lib') + find_packages('test/lib'), + include_package_data=True, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Natural Language :: English', + 'Operating System :: POSIX', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Topic :: System :: Installation/Setup', + 'Topic :: System :: Systems Administration', + 'Topic :: Utilities', + ], + scripts=[ + 'bin/ansible', + 'bin/ansible-playbook', + 'bin/ansible-pull', + 'bin/ansible-doc', + 'bin/ansible-galaxy', + 'bin/ansible-console', + 'bin/ansible-connection', + 'bin/ansible-vault', + 'bin/ansible-config', + 'bin/ansible-inventory', + 'bin/ansible-test', + ], + data_files=[], + # Installing as zip files would break due to references to __file__ + zip_safe=False +) + + +def main(): + """Invoke installation process using setuptools.""" + setup_params = dict(static_setup_params, **get_dynamic_setup_params()) + ignore_warning_regex = ( + r"Unknown distribution option: '(project_urls|python_requires)'" + ) + warnings.filterwarnings( + 'ignore', + message=ignore_warning_regex, + category=UserWarning, + module='distutils.dist', + ) + setup(**setup_params) + warnings.resetwarnings() + + +if __name__ == '__main__': + main() |