blob: 80a0ecee88d37a89b99154e84b294d35af6baf98 [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
Matthew Treinisheaf3fe52013-02-25 18:15:36 -050046def _parse_git_mailmap(git_dir, mailmap='.mailmap'):
47 mailmap = os.path.join(os.path.dirname(git_dir), mailmap)
48 return parse_mailmap(mailmap)
49
50
Attila Fazekas5abb2532012-12-04 11:30:49 +010051def canonicalize_emails(changelog, mapping):
52 """Takes in a string and an email alias mapping and replaces all
53 instances of the aliases in the string with their real email.
54 """
Joe Gordon2b0591d2013-02-14 23:18:39 +000055 for alias, email_address in mapping.iteritems():
56 changelog = changelog.replace(alias, email_address)
Attila Fazekas5abb2532012-12-04 11:30:49 +010057 return changelog
58
59
60# Get requirements from the first file that exists
61def get_reqs_from_files(requirements_files):
62 for requirements_file in requirements_files:
63 if os.path.exists(requirements_file):
64 with open(requirements_file, 'r') as fil:
65 return fil.read().split('\n')
66 return []
67
68
69def parse_requirements(requirements_files=['requirements.txt',
70 'tools/pip-requires']):
71 requirements = []
72 for line in get_reqs_from_files(requirements_files):
73 # For the requirements list, we need to inject only the portion
74 # after egg= so that distutils knows the package it's looking for
75 # such as:
76 # -e git://github.com/openstack/nova/master#egg=nova
77 if re.match(r'\s*-e\s+', line):
78 requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1',
79 line))
80 # such as:
81 # http://github.com/openstack/nova/zipball/master#egg=nova
82 elif re.match(r'\s*https?:', line):
83 requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1',
84 line))
85 # -f lines are for index locations, and don't get used here
86 elif re.match(r'\s*-f\s+', line):
87 pass
88 # argparse is part of the standard library starting with 2.7
89 # adding it to the requirements list screws distro installs
90 elif line == 'argparse' and sys.version_info >= (2, 7):
91 pass
92 else:
93 requirements.append(line)
94
95 return requirements
96
97
98def parse_dependency_links(requirements_files=['requirements.txt',
99 'tools/pip-requires']):
100 dependency_links = []
101 # dependency_links inject alternate locations to find packages listed
102 # in requirements
103 for line in get_reqs_from_files(requirements_files):
104 # skip comments and blank lines
105 if re.match(r'(\s*#)|(\s*$)', line):
106 continue
107 # lines with -e or -f need the whole line, minus the flag
108 if re.match(r'\s*-[ef]\s+', line):
109 dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line))
110 # lines that are only urls can go in unmolested
111 elif re.match(r'\s*https?:', line):
112 dependency_links.append(line)
113 return dependency_links
114
115
Joe Gordon2b0591d2013-02-14 23:18:39 +0000116def _run_shell_command(cmd, throw_on_error=False):
Attila Fazekas5abb2532012-12-04 11:30:49 +0100117 if os.name == 'nt':
118 output = subprocess.Popen(["cmd.exe", "/C", cmd],
Joe Gordon2b0591d2013-02-14 23:18:39 +0000119 stdout=subprocess.PIPE,
120 stderr=subprocess.PIPE)
Attila Fazekas5abb2532012-12-04 11:30:49 +0100121 else:
122 output = subprocess.Popen(["/bin/sh", "-c", cmd],
Joe Gordon2b0591d2013-02-14 23:18:39 +0000123 stdout=subprocess.PIPE,
124 stderr=subprocess.PIPE)
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500125 out = output.communicate()
Joe Gordon2b0591d2013-02-14 23:18:39 +0000126 if output.returncode and throw_on_error:
127 raise Exception("%s returned %d" % cmd, output.returncode)
Attila Fazekas5abb2532012-12-04 11:30:49 +0100128 if len(out) == 0:
129 return None
130 if len(out[0].strip()) == 0:
131 return None
132 return out[0].strip()
133
134
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500135def _get_git_directory():
136 parent_dir = os.path.dirname(__file__)
137 while True:
138 git_dir = os.path.join(parent_dir, '.git')
139 if os.path.exists(git_dir):
140 return git_dir
141 parent_dir, child = os.path.split(parent_dir)
142 if not child: # reached to root dir
143 return None
144
145
Attila Fazekas5abb2532012-12-04 11:30:49 +0100146def write_git_changelog():
147 """Write a changelog based on the git changelog."""
148 new_changelog = 'ChangeLog'
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500149 git_dir = _get_git_directory()
Attila Fazekas5abb2532012-12-04 11:30:49 +0100150 if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'):
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500151 if git_dir:
152 git_log_cmd = 'git --git-dir=%s log --stat' % git_dir
Attila Fazekas5abb2532012-12-04 11:30:49 +0100153 changelog = _run_shell_command(git_log_cmd)
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500154 mailmap = _parse_git_mailmap(git_dir)
Attila Fazekas5abb2532012-12-04 11:30:49 +0100155 with open(new_changelog, "w") as changelog_file:
156 changelog_file.write(canonicalize_emails(changelog, mailmap))
157 else:
158 open(new_changelog, 'w').close()
159
160
161def generate_authors():
162 """Create AUTHORS file using git commits."""
163 jenkins_email = 'jenkins@review.(openstack|stackforge).org'
164 old_authors = 'AUTHORS.in'
165 new_authors = 'AUTHORS'
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500166 git_dir = _get_git_directory()
Attila Fazekas5abb2532012-12-04 11:30:49 +0100167 if not os.getenv('SKIP_GENERATE_AUTHORS'):
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500168 if git_dir:
Attila Fazekas5abb2532012-12-04 11:30:49 +0100169 # don't include jenkins email address in AUTHORS file
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500170 git_log_cmd = ("git --git-dir=" + git_dir +
171 " log --format='%aN <%aE>' | sort -u | "
Attila Fazekas5abb2532012-12-04 11:30:49 +0100172 "egrep -v '" + jenkins_email + "'")
173 changelog = _run_shell_command(git_log_cmd)
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500174 mailmap = _parse_git_mailmap(git_dir)
Attila Fazekas5abb2532012-12-04 11:30:49 +0100175 with open(new_authors, 'w') as new_authors_fh:
176 new_authors_fh.write(canonicalize_emails(changelog, mailmap))
177 if os.path.exists(old_authors):
178 with open(old_authors, "r") as old_authors_fh:
179 new_authors_fh.write('\n' + old_authors_fh.read())
180 else:
181 open(new_authors, 'w').close()
182
183
184_rst_template = """%(heading)s
185%(underline)s
186
187.. automodule:: %(module)s
188 :members:
189 :undoc-members:
190 :show-inheritance:
191"""
192
193
Attila Fazekas5abb2532012-12-04 11:30:49 +0100194def get_cmdclass():
195 """Return dict of commands to run from setup.py."""
196
197 cmdclass = dict()
198
199 def _find_modules(arg, dirname, files):
200 for filename in files:
201 if filename.endswith('.py') and filename != '__init__.py':
202 arg["%s.%s" % (dirname.replace('/', '.'),
203 filename[:-3])] = True
204
205 class LocalSDist(sdist.sdist):
206 """Builds the ChangeLog and Authors files from VC first."""
207
208 def run(self):
209 write_git_changelog()
210 generate_authors()
211 # sdist.sdist is an old style class, can't use super()
212 sdist.sdist.run(self)
213
214 cmdclass['sdist'] = LocalSDist
215
216 # If Sphinx is installed on the box running setup.py,
217 # enable setup.py to build the documentation, otherwise,
218 # just ignore it
219 try:
220 from sphinx.setup_command import BuildDoc
221
222 class LocalBuildDoc(BuildDoc):
Joe Gordon2b0591d2013-02-14 23:18:39 +0000223
224 builders = ['html', 'man']
225
Attila Fazekas5abb2532012-12-04 11:30:49 +0100226 def generate_autoindex(self):
227 print "**Autodocumenting from %s" % os.path.abspath(os.curdir)
228 modules = {}
229 option_dict = self.distribution.get_option_dict('build_sphinx')
230 source_dir = os.path.join(option_dict['source_dir'][1], 'api')
231 if not os.path.exists(source_dir):
232 os.makedirs(source_dir)
233 for pkg in self.distribution.packages:
234 if '.' not in pkg:
235 os.path.walk(pkg, _find_modules, modules)
236 module_list = modules.keys()
237 module_list.sort()
238 autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
239 with open(autoindex_filename, 'w') as autoindex:
240 autoindex.write(""".. toctree::
241 :maxdepth: 1
242
243""")
244 for module in module_list:
245 output_filename = os.path.join(source_dir,
246 "%s.rst" % module)
247 heading = "The :mod:`%s` Module" % module
248 underline = "=" * len(heading)
249 values = dict(module=module, heading=heading,
250 underline=underline)
251
252 print "Generating %s" % output_filename
253 with open(output_filename, 'w') as output_file:
254 output_file.write(_rst_template % values)
255 autoindex.write(" %s.rst\n" % module)
256
257 def run(self):
258 if not os.getenv('SPHINX_DEBUG'):
259 self.generate_autoindex()
260
Joe Gordon2b0591d2013-02-14 23:18:39 +0000261 for builder in self.builders:
Attila Fazekas5abb2532012-12-04 11:30:49 +0100262 self.builder = builder
263 self.finalize_options()
264 self.project = self.distribution.get_name()
265 self.version = self.distribution.get_version()
266 self.release = self.distribution.get_version()
267 BuildDoc.run(self)
Joe Gordon2b0591d2013-02-14 23:18:39 +0000268
269 class LocalBuildLatex(LocalBuildDoc):
270 builders = ['latex']
271
Attila Fazekas5abb2532012-12-04 11:30:49 +0100272 cmdclass['build_sphinx'] = LocalBuildDoc
Joe Gordon2b0591d2013-02-14 23:18:39 +0000273 cmdclass['build_sphinx_latex'] = LocalBuildLatex
Attila Fazekas5abb2532012-12-04 11:30:49 +0100274 except ImportError:
275 pass
276
277 return cmdclass
278
279
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500280def _get_revno(git_dir):
Joe Gordon2b0591d2013-02-14 23:18:39 +0000281 """Return the number of commits since the most recent tag.
282
283 We use git-describe to find this out, but if there are no
284 tags then we fall back to counting commits since the beginning
285 of time.
286 """
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500287 describe = _run_shell_command(
288 "git --git-dir=%s describe --always" % git_dir)
Joe Gordon2b0591d2013-02-14 23:18:39 +0000289 if "-" in describe:
290 return describe.rsplit("-", 2)[-2]
291
292 # no tags found
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500293 revlist = _run_shell_command(
294 "git --git-dir=%s rev-list --abbrev-commit HEAD" % git_dir)
Joe Gordon2b0591d2013-02-14 23:18:39 +0000295 return len(revlist.splitlines())
Attila Fazekas5abb2532012-12-04 11:30:49 +0100296
297
Joe Gordon2b0591d2013-02-14 23:18:39 +0000298def _get_version_from_git(pre_version):
Attila Fazekas5abb2532012-12-04 11:30:49 +0100299 """Return a version which is equal to the tag that's on the current
300 revision if there is one, or tag plus number of additional revisions
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500301 if the current revision has no tag."""
Attila Fazekas5abb2532012-12-04 11:30:49 +0100302
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500303 git_dir = _get_git_directory()
304 if git_dir:
Joe Gordon2b0591d2013-02-14 23:18:39 +0000305 if pre_version:
306 try:
307 return _run_shell_command(
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500308 "git --git-dir=" + git_dir + " describe --exact-match",
Joe Gordon2b0591d2013-02-14 23:18:39 +0000309 throw_on_error=True).replace('-', '.')
310 except Exception:
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500311 sha = _run_shell_command(
312 "git --git-dir=" + git_dir + " log -n1 --pretty=format:%h")
313 return "%s.a%s.g%s" % (pre_version, _get_revno(git_dir), sha)
Joe Gordon2b0591d2013-02-14 23:18:39 +0000314 else:
315 return _run_shell_command(
Matthew Treinisheaf3fe52013-02-25 18:15:36 -0500316 "git --git-dir=" + git_dir + " describe --always").replace(
317 '-', '.')
Joe Gordon2b0591d2013-02-14 23:18:39 +0000318 return None
319
320
321def _get_version_from_pkg_info(package_name):
322 """Get the version from PKG-INFO file if we can."""
323 try:
324 pkg_info_file = open('PKG-INFO', 'r')
325 except (IOError, OSError):
326 return None
327 try:
328 pkg_info = email.message_from_file(pkg_info_file)
329 except email.MessageError:
330 return None
331 # Check to make sure we're in our own dir
332 if pkg_info.get('Name', None) != package_name:
333 return None
334 return pkg_info.get('Version', None)
335
336
337def get_version(package_name, pre_version=None):
338 """Get the version of the project. First, try getting it from PKG-INFO, if
339 it exists. If it does, that means we're in a distribution tarball or that
340 install has happened. Otherwise, if there is no PKG-INFO file, pull the
341 version from git.
342
343 We do not support setup.py version sanity in git archive tarballs, nor do
344 we support packagers directly sucking our git repo into theirs. We expect
345 that a source tarball be made from our git repo - or that if someone wants
346 to make a source tarball from a fork of our repo with additional tags in it
347 that they understand and desire the results of doing that.
348 """
349 version = os.environ.get("OSLO_PACKAGE_VERSION", None)
350 if version:
Attila Fazekas5abb2532012-12-04 11:30:49 +0100351 return version
Joe Gordon2b0591d2013-02-14 23:18:39 +0000352 version = _get_version_from_pkg_info(package_name)
353 if version:
354 return version
355 version = _get_version_from_git(pre_version)
356 if version:
357 return version
358 raise Exception("Versioning for this project requires either an sdist"
359 " tarball, or access to an upstream git repository.")