blob: 2fb9cf273bdfc6c3b48911a9340aaa23a3ff7322 [file] [log] [blame]
Attila Fazekas5abb2532012-12-04 11:30:49 +01001# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 OpenStack LLC.
Joe Gordon2b0591d2013-02-14 23:18:39 +00004# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
Attila Fazekas5abb2532012-12-04 11:30:49 +01005# All Rights Reserved.
6#
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
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
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
17# under the License.
18
19"""
20Utilities with minimum-depends for use in setup.py
21"""
22
Joe Gordon2b0591d2013-02-14 23:18:39 +000023import email
Attila Fazekas5abb2532012-12-04 11:30:49 +010024import os
25import re
26import subprocess
27import sys
28
29from setuptools.command import sdist
30
31
32def parse_mailmap(mailmap='.mailmap'):
33 mapping = {}
34 if os.path.exists(mailmap):
35 with open(mailmap, 'r') as fp:
36 for l in fp:
Joe Gordon2b0591d2013-02-14 23:18:39 +000037 try:
38 canonical_email, alias = re.match(
39 r'[^#]*?(<.+>).*(<.+>).*', l).groups()
40 except AttributeError:
41 continue
42 mapping[alias] = canonical_email
Attila Fazekas5abb2532012-12-04 11:30:49 +010043 return mapping
44
45
46def 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.
49 """
Joe Gordon2b0591d2013-02-14 23:18:39 +000050 for alias, email_address in mapping.iteritems():
51 changelog = changelog.replace(alias, email_address)
Attila Fazekas5abb2532012-12-04 11:30:49 +010052 return changelog
53
54
55# Get requirements from the first file that exists
56def 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')
61 return []
62
63
64def parse_requirements(requirements_files=['requirements.txt',
65 'tools/pip-requires']):
66 requirements = []
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
70 # such as:
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',
74 line))
75 # such as:
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',
79 line))
80 # -f lines are for index locations, and don't get used here
81 elif re.match(r'\s*-f\s+', line):
82 pass
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):
86 pass
87 else:
88 requirements.append(line)
89
90 return requirements
91
92
93def parse_dependency_links(requirements_files=['requirements.txt',
94 'tools/pip-requires']):
95 dependency_links = []
96 # dependency_links inject alternate locations to find packages listed
97 # in requirements
98 for line in get_reqs_from_files(requirements_files):
99 # skip comments and blank lines
100 if re.match(r'(\s*#)|(\s*$)', line):
101 continue
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
109
110
Joe Gordon2b0591d2013-02-14 23:18:39 +0000111def _run_shell_command(cmd, throw_on_error=False):
Attila Fazekas5abb2532012-12-04 11:30:49 +0100112 if os.name == 'nt':
113 output = subprocess.Popen(["cmd.exe", "/C", cmd],
Joe Gordon2b0591d2013-02-14 23:18:39 +0000114 stdout=subprocess.PIPE,
115 stderr=subprocess.PIPE)
Attila Fazekas5abb2532012-12-04 11:30:49 +0100116 else:
117 output = subprocess.Popen(["/bin/sh", "-c", cmd],
Joe Gordon2b0591d2013-02-14 23:18:39 +0000118 stdout=subprocess.PIPE,
119 stderr=subprocess.PIPE)
120 if output.returncode and throw_on_error:
121 raise Exception("%s returned %d" % cmd, output.returncode)
Attila Fazekas5abb2532012-12-04 11:30:49 +0100122 out = output.communicate()
123 if len(out) == 0:
124 return None
125 if len(out[0].strip()) == 0:
126 return None
127 return out[0].strip()
128
129
Attila Fazekas5abb2532012-12-04 11:30:49 +0100130def 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))
140 else:
141 open(new_changelog, 'w').close()
142
143
144def 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())
161 else:
162 open(new_authors, 'w').close()
163
164
165_rst_template = """%(heading)s
166%(underline)s
167
168.. automodule:: %(module)s
169 :members:
170 :undoc-members:
171 :show-inheritance:
172"""
173
174
Attila Fazekas5abb2532012-12-04 11:30:49 +0100175def get_cmdclass():
176 """Return dict of commands to run from setup.py."""
177
178 cmdclass = dict()
179
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
185
186 class LocalSDist(sdist.sdist):
187 """Builds the ChangeLog and Authors files from VC first."""
188
189 def run(self):
190 write_git_changelog()
191 generate_authors()
192 # sdist.sdist is an old style class, can't use super()
193 sdist.sdist.run(self)
194
195 cmdclass['sdist'] = LocalSDist
196
197 # If Sphinx is installed on the box running setup.py,
198 # enable setup.py to build the documentation, otherwise,
199 # just ignore it
200 try:
201 from sphinx.setup_command import BuildDoc
202
203 class LocalBuildDoc(BuildDoc):
Joe Gordon2b0591d2013-02-14 23:18:39 +0000204
205 builders = ['html', 'man']
206
Attila Fazekas5abb2532012-12-04 11:30:49 +0100207 def generate_autoindex(self):
208 print "**Autodocumenting from %s" % os.path.abspath(os.curdir)
209 modules = {}
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:
215 if '.' not in pkg:
216 os.path.walk(pkg, _find_modules, modules)
217 module_list = modules.keys()
218 module_list.sort()
219 autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
220 with open(autoindex_filename, 'w') as autoindex:
221 autoindex.write(""".. toctree::
222 :maxdepth: 1
223
224""")
225 for module in module_list:
226 output_filename = os.path.join(source_dir,
227 "%s.rst" % module)
228 heading = "The :mod:`%s` Module" % module
229 underline = "=" * len(heading)
230 values = dict(module=module, heading=heading,
231 underline=underline)
232
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)
237
238 def run(self):
239 if not os.getenv('SPHINX_DEBUG'):
240 self.generate_autoindex()
241
Joe Gordon2b0591d2013-02-14 23:18:39 +0000242 for builder in self.builders:
Attila Fazekas5abb2532012-12-04 11:30:49 +0100243 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()
248 BuildDoc.run(self)
Joe Gordon2b0591d2013-02-14 23:18:39 +0000249
250 class LocalBuildLatex(LocalBuildDoc):
251 builders = ['latex']
252
Attila Fazekas5abb2532012-12-04 11:30:49 +0100253 cmdclass['build_sphinx'] = LocalBuildDoc
Joe Gordon2b0591d2013-02-14 23:18:39 +0000254 cmdclass['build_sphinx_latex'] = LocalBuildLatex
Attila Fazekas5abb2532012-12-04 11:30:49 +0100255 except ImportError:
256 pass
257
258 return cmdclass
259
260
Joe Gordon2b0591d2013-02-14 23:18:39 +0000261def _get_revno():
262 """Return the number of commits since the most recent tag.
263
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
266 of time.
267 """
268 describe = _run_shell_command("git describe --always")
269 if "-" in describe:
270 return describe.rsplit("-", 2)[-2]
271
272 # no tags found
273 revlist = _run_shell_command("git rev-list --abbrev-commit HEAD")
274 return len(revlist.splitlines())
Attila Fazekas5abb2532012-12-04 11:30:49 +0100275
276
Joe Gordon2b0591d2013-02-14 23:18:39 +0000277def _get_version_from_git(pre_version):
Attila Fazekas5abb2532012-12-04 11:30:49 +0100278 """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
Attila Fazekasb2902af2013-02-16 16:22:44 +0100280 if the current revision has no tag.
281 """
Attila Fazekas5abb2532012-12-04 11:30:49 +0100282
283 if os.path.isdir('.git'):
Joe Gordon2b0591d2013-02-14 23:18:39 +0000284 if pre_version:
285 try:
286 return _run_shell_command(
287 "git describe --exact-match",
288 throw_on_error=True).replace('-', '.')
289 except Exception:
290 sha = _run_shell_command("git log -n1 --pretty=format:%h")
291 return "%s.a%s.g%s" % (pre_version, _get_revno(), sha)
292 else:
293 return _run_shell_command(
294 "git describe --always").replace('-', '.')
295 return None
296
297
298def _get_version_from_pkg_info(package_name):
299 """Get the version from PKG-INFO file if we can."""
300 try:
301 pkg_info_file = open('PKG-INFO', 'r')
302 except (IOError, OSError):
303 return None
304 try:
305 pkg_info = email.message_from_file(pkg_info_file)
306 except email.MessageError:
307 return None
308 # Check to make sure we're in our own dir
309 if pkg_info.get('Name', None) != package_name:
310 return None
311 return pkg_info.get('Version', None)
312
313
314def get_version(package_name, pre_version=None):
315 """Get the version of the project. First, try getting it from PKG-INFO, if
316 it exists. If it does, that means we're in a distribution tarball or that
317 install has happened. Otherwise, if there is no PKG-INFO file, pull the
318 version from git.
319
320 We do not support setup.py version sanity in git archive tarballs, nor do
321 we support packagers directly sucking our git repo into theirs. We expect
322 that a source tarball be made from our git repo - or that if someone wants
323 to make a source tarball from a fork of our repo with additional tags in it
324 that they understand and desire the results of doing that.
325 """
326 version = os.environ.get("OSLO_PACKAGE_VERSION", None)
327 if version:
Attila Fazekas5abb2532012-12-04 11:30:49 +0100328 return version
Joe Gordon2b0591d2013-02-14 23:18:39 +0000329 version = _get_version_from_pkg_info(package_name)
330 if version:
331 return version
332 version = _get_version_from_git(pre_version)
333 if version:
334 return version
335 raise Exception("Versioning for this project requires either an sdist"
336 " tarball, or access to an upstream git repository.")