1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2011 OpenStack LLC.
4
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
7
# Licensed under the Apache License, Version 2.0 (the "License"); you may
8
# not use this file except in compliance with the License. You may obtain
9
# a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
# License for the specific language governing permissions and limitations
20
Utilities with minimum-depends for use in setup.py
29
from setuptools.command import sdist
32
def parse_mailmap(mailmap='.mailmap'):
34
if os.path.exists(mailmap):
35
with open(mailmap, 'r') as fp:
38
canonical_email, alias = re.match(
39
r'[^#]*?(<.+>).*(<.+>).*', l).groups()
40
except AttributeError:
42
mapping[alias] = canonical_email
46
def canonicalize_emails(changelog, mapping):
47
"""Takes in a string and an email alias mapping and replaces all
48
instances of the aliases in the string with their real email.
50
for alias, email_address in mapping.iteritems():
51
changelog = changelog.replace(alias, email_address)
55
# Get requirements from the first file that exists
56
def get_reqs_from_files(requirements_files):
57
for requirements_file in requirements_files:
58
if os.path.exists(requirements_file):
59
with open(requirements_file, 'r') as fil:
60
return fil.read().split('\n')
64
def parse_requirements(requirements_files=['requirements.txt',
65
'tools/pip-requires']):
67
for line in get_reqs_from_files(requirements_files):
68
# For the requirements list, we need to inject only the portion
69
# after egg= so that distutils knows the package it's looking for
71
# -e git://github.com/openstack/nova/master#egg=nova
72
if re.match(r'\s*-e\s+', line):
73
requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1',
76
# http://github.com/openstack/nova/zipball/master#egg=nova
77
elif re.match(r'\s*https?:', line):
78
requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1',
80
# -f lines are for index locations, and don't get used here
81
elif re.match(r'\s*-f\s+', line):
83
# argparse is part of the standard library starting with 2.7
84
# adding it to the requirements list screws distro installs
85
elif line == 'argparse' and sys.version_info >= (2, 7):
88
requirements.append(line)
93
def parse_dependency_links(requirements_files=['requirements.txt',
94
'tools/pip-requires']):
96
# dependency_links inject alternate locations to find packages listed
98
for line in get_reqs_from_files(requirements_files):
99
# skip comments and blank lines
100
if re.match(r'(\s*#)|(\s*$)', line):
102
# lines with -e or -f need the whole line, minus the flag
103
if re.match(r'\s*-[ef]\s+', line):
104
dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line))
105
# lines that are only urls can go in unmolested
106
elif re.match(r'\s*https?:', line):
107
dependency_links.append(line)
108
return dependency_links
111
def _run_shell_command(cmd, throw_on_error=False):
113
output = subprocess.Popen(["cmd.exe", "/C", cmd],
114
stdout=subprocess.PIPE,
115
stderr=subprocess.PIPE)
117
output = subprocess.Popen(["/bin/sh", "-c", cmd],
118
stdout=subprocess.PIPE,
119
stderr=subprocess.PIPE)
120
if output.returncode and throw_on_error:
121
raise Exception("%s returned %d" % cmd, output.returncode)
122
out = output.communicate()
125
if len(out[0].strip()) == 0:
127
return out[0].strip()
130
def write_git_changelog():
131
"""Write a changelog based on the git changelog."""
132
new_changelog = 'ChangeLog'
133
if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'):
134
if os.path.isdir('.git'):
135
git_log_cmd = 'git log --stat'
136
changelog = _run_shell_command(git_log_cmd)
137
mailmap = parse_mailmap()
138
with open(new_changelog, "w") as changelog_file:
139
changelog_file.write(canonicalize_emails(changelog, mailmap))
141
open(new_changelog, 'w').close()
144
def generate_authors():
145
"""Create AUTHORS file using git commits."""
146
jenkins_email = 'jenkins@review.(openstack|stackforge).org'
147
old_authors = 'AUTHORS.in'
148
new_authors = 'AUTHORS'
149
if not os.getenv('SKIP_GENERATE_AUTHORS'):
150
if os.path.isdir('.git'):
151
# don't include jenkins email address in AUTHORS file
152
git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | "
153
"egrep -v '" + jenkins_email + "'")
154
changelog = _run_shell_command(git_log_cmd)
155
mailmap = parse_mailmap()
156
with open(new_authors, 'w') as new_authors_fh:
157
new_authors_fh.write(canonicalize_emails(changelog, mailmap))
158
if os.path.exists(old_authors):
159
with open(old_authors, "r") as old_authors_fh:
160
new_authors_fh.write('\n' + old_authors_fh.read())
162
open(new_authors, 'w').close()
165
_rst_template = """%(heading)s
168
.. automodule:: %(module)s
176
"""Return dict of commands to run from setup.py."""
180
def _find_modules(arg, dirname, files):
181
for filename in files:
182
if filename.endswith('.py') and filename != '__init__.py':
183
arg["%s.%s" % (dirname.replace('/', '.'),
184
filename[:-3])] = True
186
class LocalSDist(sdist.sdist):
187
"""Builds the ChangeLog and Authors files from VC first."""
190
write_git_changelog()
192
# sdist.sdist is an old style class, can't use super()
193
sdist.sdist.run(self)
195
cmdclass['sdist'] = LocalSDist
197
# If Sphinx is installed on the box running setup.py,
198
# enable setup.py to build the documentation, otherwise,
201
from sphinx.setup_command import BuildDoc
203
class LocalBuildDoc(BuildDoc):
205
builders = ['html', 'man']
207
def generate_autoindex(self):
208
print "**Autodocumenting from %s" % os.path.abspath(os.curdir)
210
option_dict = self.distribution.get_option_dict('build_sphinx')
211
source_dir = os.path.join(option_dict['source_dir'][1], 'api')
212
if not os.path.exists(source_dir):
213
os.makedirs(source_dir)
214
for pkg in self.distribution.packages:
216
os.path.walk(pkg, _find_modules, modules)
217
module_list = modules.keys()
219
autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
220
with open(autoindex_filename, 'w') as autoindex:
221
autoindex.write(""".. toctree::
225
for module in module_list:
226
output_filename = os.path.join(source_dir,
228
heading = "The :mod:`%s` Module" % module
229
underline = "=" * len(heading)
230
values = dict(module=module, heading=heading,
233
print "Generating %s" % output_filename
234
with open(output_filename, 'w') as output_file:
235
output_file.write(_rst_template % values)
236
autoindex.write(" %s.rst\n" % module)
239
if not os.getenv('SPHINX_DEBUG'):
240
self.generate_autoindex()
242
for builder in self.builders:
243
self.builder = builder
244
self.finalize_options()
245
self.project = self.distribution.get_name()
246
self.version = self.distribution.get_version()
247
self.release = self.distribution.get_version()
250
class LocalBuildLatex(LocalBuildDoc):
253
cmdclass['build_sphinx'] = LocalBuildDoc
254
cmdclass['build_sphinx_latex'] = LocalBuildLatex
262
"""Return the number of commits since the most recent tag.
264
We use git-describe to find this out, but if there are no
265
tags then we fall back to counting commits since the beginning
268
describe = _run_shell_command("git describe --always")
270
return describe.rsplit("-", 2)[-2]
273
revlist = _run_shell_command("git rev-list --abbrev-commit HEAD")
274
return len(revlist.splitlines())
277
def get_version_from_git(pre_version):
278
"""Return a version which is equal to the tag that's on the current
279
revision if there is one, or tag plus number of additional revisions
280
if the current revision has no tag."""
282
if os.path.isdir('.git'):
285
return _run_shell_command(
286
"git describe --exact-match",
287
throw_on_error=True).replace('-', '.')
289
sha = _run_shell_command("git log -n1 --pretty=format:%h")
290
return "%s.a%s.g%s" % (pre_version, _get_revno(), sha)
292
return _run_shell_command(
293
"git describe --always").replace('-', '.')
297
def get_version_from_pkg_info(package_name):
298
"""Get the version from PKG-INFO file if we can."""
300
pkg_info_file = open('PKG-INFO', 'r')
301
except (IOError, OSError):
304
pkg_info = email.message_from_file(pkg_info_file)
305
except email.MessageError:
307
# Check to make sure we're in our own dir
308
if pkg_info.get('Name', None) != package_name:
310
return pkg_info.get('Version', None)
313
def get_version(package_name, pre_version=None):
314
"""Get the version of the project. First, try getting it from PKG-INFO, if
315
it exists. If it does, that means we're in a distribution tarball or that
316
install has happened. Otherwise, if there is no PKG-INFO file, pull the
319
We do not support setup.py version sanity in git archive tarballs, nor do
320
we support packagers directly sucking our git repo into theirs. We expect
321
that a source tarball be made from our git repo - or that if someone wants
322
to make a source tarball from a fork of our repo with additional tags in it
323
that they understand and desire the results of doing that.
325
version = os.environ.get("OSLO_PACKAGE_VERSION", None)
328
version = get_version_from_pkg_info(package_name)
331
version = get_version_from_git(pre_version)
334
raise Exception("Versioning for this project requires either an sdist"
335
" tarball, or access to an upstream git repository.")