Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/ansible
Path: blob/devel/setup.py
4751 views
1
2
from __future__ import print_function
3
4
import json
5
import os
6
import os.path
7
import re
8
import sys
9
import warnings
10
11
from collections import defaultdict
12
from distutils.command.build_scripts import build_scripts as BuildScripts
13
from distutils.command.sdist import sdist as SDist
14
15
try:
16
from setuptools import setup, find_packages
17
from setuptools.command.build_py import build_py as BuildPy
18
from setuptools.command.install_lib import install_lib as InstallLib
19
from setuptools.command.install_scripts import install_scripts as InstallScripts
20
except ImportError:
21
print("Ansible now needs setuptools in order to build. Install it using"
22
" your package manager (usually python-setuptools) or via pip (pip"
23
" install setuptools).", file=sys.stderr)
24
sys.exit(1)
25
26
sys.path.insert(0, os.path.abspath('lib'))
27
from ansible.release import __version__, __author__
28
29
30
SYMLINK_CACHE = 'SYMLINK_CACHE.json'
31
32
33
def _find_symlinks(topdir, extension=''):
34
"""Find symlinks that should be maintained
35
36
Maintained symlinks exist in the bin dir or are modules which have
37
aliases. Our heuristic is that they are a link in a certain path which
38
point to a file in the same directory.
39
40
.. warn::
41
42
We want the symlinks in :file:`bin/` that link into :file:`lib/ansible/*` (currently,
43
:command:`ansible`, :command:`ansible-test`, and :command:`ansible-connection`) to become
44
real files on install. Updates to the heuristic here *must not* add them to the symlink
45
cache.
46
"""
47
symlinks = defaultdict(list)
48
for base_path, dirs, files in os.walk(topdir):
49
for filename in files:
50
filepath = os.path.join(base_path, filename)
51
if os.path.islink(filepath) and filename.endswith(extension):
52
target = os.readlink(filepath)
53
if target.startswith('/'):
54
# We do not support absolute symlinks at all
55
continue
56
57
if os.path.dirname(target) == '':
58
link = filepath[len(topdir):]
59
if link.startswith('/'):
60
link = link[1:]
61
symlinks[os.path.basename(target)].append(link)
62
else:
63
# Count how many directory levels from the topdir we are
64
levels_deep = os.path.dirname(filepath).count('/')
65
66
# Count the number of directory levels higher we walk up the tree in target
67
target_depth = 0
68
for path_component in target.split('/'):
69
if path_component == '..':
70
target_depth += 1
71
# If we walk past the topdir, then don't store
72
if target_depth >= levels_deep:
73
break
74
else:
75
target_depth -= 1
76
else:
77
# If we managed to stay within the tree, store the symlink
78
link = filepath[len(topdir):]
79
if link.startswith('/'):
80
link = link[1:]
81
symlinks[target].append(link)
82
83
return symlinks
84
85
86
def _cache_symlinks(symlink_data):
87
with open(SYMLINK_CACHE, 'w') as f:
88
json.dump(symlink_data, f)
89
90
91
def _maintain_symlinks(symlink_type, base_path):
92
"""Switch a real file into a symlink"""
93
try:
94
# Try the cache first because going from git checkout to sdist is the
95
# only time we know that we're going to cache correctly
96
with open(SYMLINK_CACHE, 'r') as f:
97
symlink_data = json.load(f)
98
except (IOError, OSError) as e:
99
# IOError on py2, OSError on py3. Both have errno
100
if e.errno == 2:
101
# SYMLINKS_CACHE doesn't exist. Fallback to trying to create the
102
# cache now. Will work if we're running directly from a git
103
# checkout or from an sdist created earlier.
104
library_symlinks = _find_symlinks('lib', '.py')
105
library_symlinks.update(_find_symlinks('test/lib'))
106
107
symlink_data = {'script': _find_symlinks('bin'),
108
'library': library_symlinks,
109
}
110
111
# Sanity check that something we know should be a symlink was
112
# found. We'll take that to mean that the current directory
113
# structure properly reflects symlinks in the git repo
114
if 'ansible-playbook' in symlink_data['script']['ansible']:
115
_cache_symlinks(symlink_data)
116
else:
117
raise RuntimeError(
118
"Pregenerated symlink list was not present and expected "
119
"symlinks in ./bin were missing or broken. "
120
"Perhaps this isn't a git checkout?"
121
)
122
else:
123
raise
124
symlinks = symlink_data[symlink_type]
125
126
for source in symlinks:
127
for dest in symlinks[source]:
128
dest_path = os.path.join(base_path, dest)
129
if not os.path.islink(dest_path):
130
try:
131
os.unlink(dest_path)
132
except OSError as e:
133
if e.errno == 2:
134
# File does not exist which is all we wanted
135
pass
136
os.symlink(source, dest_path)
137
138
139
class BuildPyCommand(BuildPy):
140
def run(self):
141
BuildPy.run(self)
142
_maintain_symlinks('library', self.build_lib)
143
144
145
class BuildScriptsCommand(BuildScripts):
146
def run(self):
147
BuildScripts.run(self)
148
_maintain_symlinks('script', self.build_dir)
149
150
151
class InstallLibCommand(InstallLib):
152
def run(self):
153
InstallLib.run(self)
154
_maintain_symlinks('library', self.install_dir)
155
156
157
class InstallScriptsCommand(InstallScripts):
158
def run(self):
159
InstallScripts.run(self)
160
_maintain_symlinks('script', self.install_dir)
161
162
163
class SDistCommand(SDist):
164
def run(self):
165
# have to generate the cache of symlinks for release as sdist is the
166
# only command that has access to symlinks from the git repo
167
library_symlinks = _find_symlinks('lib', '.py')
168
library_symlinks.update(_find_symlinks('test/lib'))
169
170
symlinks = {'script': _find_symlinks('bin'),
171
'library': library_symlinks,
172
}
173
_cache_symlinks(symlinks)
174
175
SDist.run(self)
176
177
# Print warnings at the end because no one will see warnings before all the normal status
178
# output
179
if os.environ.get('_ANSIBLE_SDIST_FROM_MAKEFILE', False) != '1':
180
warnings.warn('When setup.py sdist is run from outside of the Makefile,'
181
' the generated tarball may be incomplete. Use `make snapshot`'
182
' to create a tarball from an arbitrary checkout or use'
183
' `cd packaging/release && make release version=[..]` for official builds.',
184
RuntimeWarning)
185
186
187
def read_file(file_name):
188
"""Read file and return its contents."""
189
with open(file_name, 'r') as f:
190
return f.read()
191
192
193
def read_requirements(file_name):
194
"""Read requirements file as a list."""
195
reqs = read_file(file_name).splitlines()
196
if not reqs:
197
raise RuntimeError(
198
"Unable to read requirements from the %s file"
199
"That indicates this copy of the source code is incomplete."
200
% file_name
201
)
202
return reqs
203
204
205
PYCRYPTO_DIST = 'pycrypto'
206
207
208
def get_crypto_req():
209
"""Detect custom crypto from ANSIBLE_CRYPTO_BACKEND env var.
210
211
pycrypto or cryptography. We choose a default but allow the user to
212
override it. This translates into pip install of the sdist deciding what
213
package to install and also the runtime dependencies that pkg_resources
214
knows about.
215
"""
216
crypto_backend = os.environ.get('ANSIBLE_CRYPTO_BACKEND', '').strip()
217
218
if crypto_backend == PYCRYPTO_DIST:
219
# Attempt to set version requirements
220
return '%s >= 2.6' % PYCRYPTO_DIST
221
222
return crypto_backend or None
223
224
225
def substitute_crypto_to_req(req):
226
"""Replace crypto requirements if customized."""
227
crypto_backend = get_crypto_req()
228
229
if crypto_backend is None:
230
return req
231
232
def is_not_crypto(r):
233
CRYPTO_LIBS = PYCRYPTO_DIST, 'cryptography'
234
return not any(r.lower().startswith(c) for c in CRYPTO_LIBS)
235
236
return [r for r in req if is_not_crypto(r)] + [crypto_backend]
237
238
239
def get_dynamic_setup_params():
240
"""Add dynamically calculated setup params to static ones."""
241
return {
242
# Retrieve the long description from the README
243
'long_description': read_file('README.rst'),
244
'install_requires': substitute_crypto_to_req(
245
read_requirements('requirements.txt'),
246
),
247
}
248
249
250
static_setup_params = dict(
251
# Use the distutils SDist so that symlinks are not expanded
252
# Use a custom Build for the same reason
253
cmdclass={
254
'build_py': BuildPyCommand,
255
'build_scripts': BuildScriptsCommand,
256
'install_lib': InstallLibCommand,
257
'install_scripts': InstallScriptsCommand,
258
'sdist': SDistCommand,
259
},
260
name='ansible',
261
version=__version__,
262
description='Radically simple IT automation',
263
author=__author__,
264
author_email='[email protected]',
265
url='https://ansible.com/',
266
project_urls={
267
'Bug Tracker': 'https://github.com/ansible/ansible/issues',
268
'CI: Shippable': 'https://app.shippable.com/github/ansible/ansible',
269
'Code of Conduct': 'https://docs.ansible.com/ansible/latest/community/code_of_conduct.html',
270
'Documentation': 'https://docs.ansible.com/ansible/',
271
'Mailing lists': 'https://docs.ansible.com/ansible/latest/community/communication.html#mailing-list-information',
272
'Source Code': 'https://github.com/ansible/ansible',
273
},
274
license='GPLv3+',
275
# Ansible will also make use of a system copy of python-six and
276
# python-selectors2 if installed but use a Bundled copy if it's not.
277
python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*',
278
package_dir={'': 'lib',
279
'ansible_test': 'test/lib/ansible_test'},
280
packages=find_packages('lib') + find_packages('test/lib'),
281
include_package_data=True,
282
classifiers=[
283
'Development Status :: 5 - Production/Stable',
284
'Environment :: Console',
285
'Intended Audience :: Developers',
286
'Intended Audience :: Information Technology',
287
'Intended Audience :: System Administrators',
288
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
289
'Natural Language :: English',
290
'Operating System :: POSIX',
291
'Programming Language :: Python :: 2',
292
'Programming Language :: Python :: 2.7',
293
'Programming Language :: Python :: 3',
294
'Programming Language :: Python :: 3.5',
295
'Programming Language :: Python :: 3.6',
296
'Programming Language :: Python :: 3.7',
297
'Programming Language :: Python :: 3.8',
298
'Topic :: System :: Installation/Setup',
299
'Topic :: System :: Systems Administration',
300
'Topic :: Utilities',
301
],
302
scripts=[
303
'bin/ansible',
304
'bin/ansible-playbook',
305
'bin/ansible-pull',
306
'bin/ansible-doc',
307
'bin/ansible-galaxy',
308
'bin/ansible-console',
309
'bin/ansible-connection',
310
'bin/ansible-vault',
311
'bin/ansible-config',
312
'bin/ansible-inventory',
313
'bin/ansible-test',
314
],
315
data_files=[],
316
# Installing as zip files would break due to references to __file__
317
zip_safe=False
318
)
319
320
321
def main():
322
"""Invoke installation process using setuptools."""
323
setup_params = dict(static_setup_params, **get_dynamic_setup_params())
324
ignore_warning_regex = (
325
r"Unknown distribution option: '(project_urls|python_requires)'"
326
)
327
warnings.filterwarnings(
328
'ignore',
329
message=ignore_warning_regex,
330
category=UserWarning,
331
module='distutils.dist',
332
)
333
setup(**setup_params)
334
warnings.resetwarnings()
335
336
337
if __name__ == '__main__':
338
main()
339
340