From 66a83314b9d30c6a139de960e6da8d5554c28544 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 19 Oct 2021 14:24:57 -0500 Subject: Modernize install (#76021) Co-authored-by: Matt Clay Co-authored-by: Matt Davis Co-authored-by: Sviatoslav Sydorenko --- setup.py | 403 ++++----------------------------------------------------------- 1 file changed, 20 insertions(+), 383 deletions(-) (limited to 'setup.py') diff --git a/setup.py b/setup.py index 11f88c6afa..b17ae8db83 100644 --- a/setup.py +++ b/setup.py @@ -1,394 +1,31 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import json -import os -import os.path -import re -import sys -import warnings +import pathlib -from collections import defaultdict +from setuptools import find_packages, setup -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) +here = pathlib.Path(__file__).parent.resolve() -# `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 +install_requires = (here / 'requirements.txt').read_text(encoding='utf-8').splitlines() - -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_core(): - """Validate that we can install ansible-core. This checks if - ansible<=2.9 or ansible-base>=2.10 are installed. - """ - # 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, 11): - return - elif version_tuple == (2, 10): - ansible_name = 'ansible-base' - else: - ansible_name = 'ansible' - - stars = '*' * 76 - raise RuntimeError( - ''' - - %s - - Cannot install ansible-core with a pre-existing %s==%s - installation. - - Installing ansible-core with ansible-2.9 or older, or ansible-base-2.10 - currently installed with pip is known to cause problems. Please uninstall - %s and install the new version: - - pip uninstall %s - pip install ansible-core - - 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-core - - %s - ''' % (stars, ansible_name, __version__, ansible_name, ansible_name, 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_core() - - -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 - - -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': 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-core', - 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: Azure Pipelines': 'https://dev.azure.com/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='>=3.8', +setup( + install_requires=install_requires, 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 :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - '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 + entry_points={ + 'console_scripts': [ + 'ansible=ansible.cli.adhoc:main', + 'ansible-config=ansible.cli.config:main', + 'ansible-console=ansible.cli.console:main', + 'ansible-doc=ansible.cli.doc:main', + 'ansible-galaxy=ansible.cli.galaxy:main', + 'ansible-inventory=ansible.cli.inventory:main', + 'ansible-playbook=ansible.cli.playbook:main', + 'ansible-pull=ansible.cli.pull:main', + 'ansible-vault=ansible.cli.vault:main', + 'ansible-connection=ansible.cli.scripts.ansible_connection_cli_stub:main', + ], + }, ) - - -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() -- cgit v1.2.1