1from __future__ import (absolute_import, division, print_function) 2__metaclass__ = type 3 4import json 5import os 6import os.path 7import re 8import sys 9import warnings 10 11from collections import defaultdict 12 13try: 14 from setuptools import setup, find_packages 15 from setuptools.command.build_py import build_py as BuildPy 16 from setuptools.command.install_lib import install_lib as InstallLib 17 from setuptools.command.install_scripts import install_scripts as InstallScripts 18except ImportError: 19 print("Ansible now needs setuptools in order to build. Install it using" 20 " your package manager (usually python-setuptools) or via pip (pip" 21 " install setuptools).", file=sys.stderr) 22 sys.exit(1) 23 24# `distutils` must be imported after `setuptools` or it will cause explosions 25# with `setuptools >=48.0.0, <49.1`. 26# Refs: 27# * https://github.com/ansible/ansible/issues/70456 28# * https://github.com/pypa/setuptools/issues/2230 29# * https://github.com/pypa/setuptools/commit/bd110264 30from distutils.command.build_scripts import build_scripts as BuildScripts 31from distutils.command.sdist import sdist as SDist 32 33 34def find_package_info(*file_paths): 35 try: 36 with open(os.path.join(*file_paths), 'r') as f: 37 info_file = f.read() 38 except Exception: 39 raise RuntimeError("Unable to find package info.") 40 41 # The version line must have the form 42 # __version__ = 'ver' 43 version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 44 info_file, re.M) 45 author_match = re.search(r"^__author__ = ['\"]([^'\"]*)['\"]", 46 info_file, re.M) 47 48 if version_match and author_match: 49 return version_match.group(1), author_match.group(1) 50 raise RuntimeError("Unable to find package info.") 51 52 53def _validate_install_ansible_core(): 54 """Validate that we can install ansible-core. This checks if 55 ansible<=2.9 or ansible-base>=2.10 are installed. 56 """ 57 # Skip common commands we can ignore 58 # Do NOT add bdist_wheel here, we don't ship wheels 59 # and bdist_wheel is the only place we can prevent pip 60 # from installing, as pip creates a wheel, and installs the wheel 61 # and we have no influence over installation within a wheel 62 if set(('sdist', 'egg_info')).intersection(sys.argv): 63 return 64 65 if os.getenv('ANSIBLE_SKIP_CONFLICT_CHECK', '') not in ('', '0'): 66 return 67 68 # Save these for later restoring things to pre invocation 69 sys_modules = sys.modules.copy() 70 sys_modules_keys = set(sys_modules) 71 72 # Make sure `lib` isn't in `sys.path` that could confuse this 73 sys_path = sys.path[:] 74 abspath = os.path.abspath 75 sys.path[:] = [p for p in sys.path if abspath(p) != abspath('lib')] 76 77 try: 78 from ansible.release import __version__ 79 except ImportError: 80 pass 81 else: 82 version_tuple = tuple(int(v) for v in __version__.split('.')[:2]) 83 if version_tuple >= (2, 11): 84 return 85 elif version_tuple == (2, 10): 86 ansible_name = 'ansible-base' 87 else: 88 ansible_name = 'ansible' 89 90 stars = '*' * 76 91 raise RuntimeError( 92 ''' 93 94 %s 95 96 Cannot install ansible-core with a pre-existing %s==%s 97 installation. 98 99 Installing ansible-core with ansible-2.9 or older, or ansible-base-2.10 100 currently installed with pip is known to cause problems. Please uninstall 101 %s and install the new version: 102 103 pip uninstall %s 104 pip install ansible-core 105 106 If you want to skip the conflict checks and manually resolve any issues 107 afterwards, set the ANSIBLE_SKIP_CONFLICT_CHECK environment variable: 108 109 ANSIBLE_SKIP_CONFLICT_CHECK=1 pip install ansible-core 110 111 %s 112 ''' % (stars, ansible_name, __version__, ansible_name, ansible_name, stars)) 113 finally: 114 sys.path[:] = sys_path 115 for key in sys_modules_keys.symmetric_difference(sys.modules): 116 sys.modules.pop(key, None) 117 sys.modules.update(sys_modules) 118 119 120_validate_install_ansible_core() 121 122 123SYMLINK_CACHE = 'SYMLINK_CACHE.json' 124 125 126def _find_symlinks(topdir, extension=''): 127 """Find symlinks that should be maintained 128 129 Maintained symlinks exist in the bin dir or are modules which have 130 aliases. Our heuristic is that they are a link in a certain path which 131 point to a file in the same directory. 132 133 .. warn:: 134 135 We want the symlinks in :file:`bin/` that link into :file:`lib/ansible/*` (currently, 136 :command:`ansible`, :command:`ansible-test`, and :command:`ansible-connection`) to become 137 real files on install. Updates to the heuristic here *must not* add them to the symlink 138 cache. 139 """ 140 symlinks = defaultdict(list) 141 for base_path, dirs, files in os.walk(topdir): 142 for filename in files: 143 filepath = os.path.join(base_path, filename) 144 if os.path.islink(filepath) and filename.endswith(extension): 145 target = os.readlink(filepath) 146 if target.startswith('/'): 147 # We do not support absolute symlinks at all 148 continue 149 150 if os.path.dirname(target) == '': 151 link = filepath[len(topdir):] 152 if link.startswith('/'): 153 link = link[1:] 154 symlinks[os.path.basename(target)].append(link) 155 else: 156 # Count how many directory levels from the topdir we are 157 levels_deep = os.path.dirname(filepath).count('/') 158 159 # Count the number of directory levels higher we walk up the tree in target 160 target_depth = 0 161 for path_component in target.split('/'): 162 if path_component == '..': 163 target_depth += 1 164 # If we walk past the topdir, then don't store 165 if target_depth >= levels_deep: 166 break 167 else: 168 target_depth -= 1 169 else: 170 # If we managed to stay within the tree, store the symlink 171 link = filepath[len(topdir):] 172 if link.startswith('/'): 173 link = link[1:] 174 symlinks[target].append(link) 175 176 return symlinks 177 178 179def _cache_symlinks(symlink_data): 180 with open(SYMLINK_CACHE, 'w') as f: 181 json.dump(symlink_data, f) 182 183 184def _maintain_symlinks(symlink_type, base_path): 185 """Switch a real file into a symlink""" 186 try: 187 # Try the cache first because going from git checkout to sdist is the 188 # only time we know that we're going to cache correctly 189 with open(SYMLINK_CACHE, 'r') as f: 190 symlink_data = json.load(f) 191 except (IOError, OSError) as e: 192 # IOError on py2, OSError on py3. Both have errno 193 if e.errno == 2: 194 # SYMLINKS_CACHE doesn't exist. Fallback to trying to create the 195 # cache now. Will work if we're running directly from a git 196 # checkout or from an sdist created earlier. 197 library_symlinks = _find_symlinks('lib', '.py') 198 library_symlinks.update(_find_symlinks('test/lib')) 199 200 symlink_data = {'script': _find_symlinks('bin'), 201 'library': library_symlinks, 202 } 203 204 # Sanity check that something we know should be a symlink was 205 # found. We'll take that to mean that the current directory 206 # structure properly reflects symlinks in the git repo 207 if 'ansible-playbook' in symlink_data['script']['ansible']: 208 _cache_symlinks(symlink_data) 209 else: 210 raise RuntimeError( 211 "Pregenerated symlink list was not present and expected " 212 "symlinks in ./bin were missing or broken. " 213 "Perhaps this isn't a git checkout?" 214 ) 215 else: 216 raise 217 symlinks = symlink_data[symlink_type] 218 219 for source in symlinks: 220 for dest in symlinks[source]: 221 dest_path = os.path.join(base_path, dest) 222 if not os.path.islink(dest_path): 223 try: 224 os.unlink(dest_path) 225 except OSError as e: 226 if e.errno == 2: 227 # File does not exist which is all we wanted 228 pass 229 os.symlink(source, dest_path) 230 231 232class BuildPyCommand(BuildPy): 233 def run(self): 234 BuildPy.run(self) 235 _maintain_symlinks('library', self.build_lib) 236 237 238class BuildScriptsCommand(BuildScripts): 239 def run(self): 240 BuildScripts.run(self) 241 _maintain_symlinks('script', self.build_dir) 242 243 244class InstallLibCommand(InstallLib): 245 def run(self): 246 InstallLib.run(self) 247 _maintain_symlinks('library', self.install_dir) 248 249 250class InstallScriptsCommand(InstallScripts): 251 def run(self): 252 InstallScripts.run(self) 253 _maintain_symlinks('script', self.install_dir) 254 255 256class SDistCommand(SDist): 257 def run(self): 258 # have to generate the cache of symlinks for release as sdist is the 259 # only command that has access to symlinks from the git repo 260 library_symlinks = _find_symlinks('lib', '.py') 261 library_symlinks.update(_find_symlinks('test/lib')) 262 263 symlinks = {'script': _find_symlinks('bin'), 264 'library': library_symlinks, 265 } 266 _cache_symlinks(symlinks) 267 268 SDist.run(self) 269 270 # Print warnings at the end because no one will see warnings before all the normal status 271 # output 272 if os.environ.get('_ANSIBLE_SDIST_FROM_MAKEFILE', False) != '1': 273 warnings.warn('When setup.py sdist is run from outside of the Makefile,' 274 ' the generated tarball may be incomplete. Use `make snapshot`' 275 ' to create a tarball from an arbitrary checkout or use' 276 ' `cd packaging/release && make release version=[..]` for official builds.', 277 RuntimeWarning) 278 279 280def read_file(file_name): 281 """Read file and return its contents.""" 282 with open(file_name, 'r') as f: 283 return f.read() 284 285 286def read_requirements(file_name): 287 """Read requirements file as a list.""" 288 reqs = read_file(file_name).splitlines() 289 if not reqs: 290 raise RuntimeError( 291 "Unable to read requirements from the %s file" 292 "That indicates this copy of the source code is incomplete." 293 % file_name 294 ) 295 return reqs 296 297 298PYCRYPTO_DIST = 'pycrypto' 299 300 301def get_crypto_req(): 302 """Detect custom crypto from ANSIBLE_CRYPTO_BACKEND env var. 303 304 pycrypto or cryptography. We choose a default but allow the user to 305 override it. This translates into pip install of the sdist deciding what 306 package to install and also the runtime dependencies that pkg_resources 307 knows about. 308 """ 309 crypto_backend = os.environ.get('ANSIBLE_CRYPTO_BACKEND', '').strip() 310 311 if crypto_backend == PYCRYPTO_DIST: 312 # Attempt to set version requirements 313 return '%s >= 2.6' % PYCRYPTO_DIST 314 315 return crypto_backend or None 316 317 318def substitute_crypto_to_req(req): 319 """Replace crypto requirements if customized.""" 320 crypto_backend = get_crypto_req() 321 322 if crypto_backend is None: 323 return req 324 325 def is_not_crypto(r): 326 CRYPTO_LIBS = PYCRYPTO_DIST, 'cryptography' 327 return not any(r.lower().startswith(c) for c in CRYPTO_LIBS) 328 329 return [r for r in req if is_not_crypto(r)] + [crypto_backend] 330 331 332def get_dynamic_setup_params(): 333 """Add dynamically calculated setup params to static ones.""" 334 return { 335 # Retrieve the long description from the README 336 'long_description': read_file('README.rst'), 337 'install_requires': substitute_crypto_to_req( 338 read_requirements('requirements.txt'), 339 ), 340 } 341 342 343here = os.path.abspath(os.path.dirname(__file__)) 344__version__, __author__ = find_package_info(here, 'lib', 'ansible', 'release.py') 345static_setup_params = dict( 346 # Use the distutils SDist so that symlinks are not expanded 347 # Use a custom Build for the same reason 348 cmdclass={ 349 'build_py': BuildPyCommand, 350 'build_scripts': BuildScriptsCommand, 351 'install_lib': InstallLibCommand, 352 'install_scripts': InstallScriptsCommand, 353 'sdist': SDistCommand, 354 }, 355 name='ansible-core', 356 version=__version__, 357 description='Radically simple IT automation', 358 author=__author__, 359 author_email='info@ansible.com', 360 url='https://ansible.com/', 361 project_urls={ 362 'Bug Tracker': 'https://github.com/ansible/ansible/issues', 363 'CI: Azure Pipelines': 'https://dev.azure.com/ansible/ansible/', 364 'Code of Conduct': 'https://docs.ansible.com/ansible/latest/community/code_of_conduct.html', 365 'Documentation': 'https://docs.ansible.com/ansible/', 366 'Mailing lists': 'https://docs.ansible.com/ansible/latest/community/communication.html#mailing-list-information', 367 'Source Code': 'https://github.com/ansible/ansible', 368 }, 369 license='GPLv3+', 370 # Ansible will also make use of a system copy of python-six and 371 # python-selectors2 if installed but use a Bundled copy if it's not. 372 python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', 373 package_dir={'': 'lib', 374 'ansible_test': 'test/lib/ansible_test'}, 375 packages=find_packages('lib') + find_packages('test/lib'), 376 include_package_data=True, 377 classifiers=[ 378 'Development Status :: 5 - Production/Stable', 379 'Environment :: Console', 380 'Intended Audience :: Developers', 381 'Intended Audience :: Information Technology', 382 'Intended Audience :: System Administrators', 383 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 384 'Natural Language :: English', 385 'Operating System :: POSIX', 386 'Programming Language :: Python :: 2', 387 'Programming Language :: Python :: 2.7', 388 'Programming Language :: Python :: 3', 389 'Programming Language :: Python :: 3.5', 390 'Programming Language :: Python :: 3.6', 391 'Programming Language :: Python :: 3.7', 392 'Programming Language :: Python :: 3.8', 393 'Programming Language :: Python :: 3.9', 394 'Topic :: System :: Installation/Setup', 395 'Topic :: System :: Systems Administration', 396 'Topic :: Utilities', 397 ], 398 scripts=[ 399 'bin/ansible', 400 'bin/ansible-playbook', 401 'bin/ansible-pull', 402 'bin/ansible-doc', 403 'bin/ansible-galaxy', 404 'bin/ansible-console', 405 'bin/ansible-connection', 406 'bin/ansible-vault', 407 'bin/ansible-config', 408 'bin/ansible-inventory', 409 'bin/ansible-test', 410 ], 411 data_files=[], 412 # Installing as zip files would break due to references to __file__ 413 zip_safe=False 414) 415 416 417def main(): 418 """Invoke installation process using setuptools.""" 419 setup_params = dict(static_setup_params, **get_dynamic_setup_params()) 420 ignore_warning_regex = ( 421 r"Unknown distribution option: '(project_urls|python_requires)'" 422 ) 423 warnings.filterwarnings( 424 'ignore', 425 message=ignore_warning_regex, 426 category=UserWarning, 427 module='distutils.dist', 428 ) 429 setup(**setup_params) 430 warnings.resetwarnings() 431 432 433if __name__ == '__main__': 434 main() 435