blob: 2be72e61a1cdd7e84383bd76b9c4524ae1369e57 [file] [log] [blame]
Monty Taylorda3bada2012-11-22 09:38:22 -08001# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 OpenStack LLC.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18"""
19Utilities with minimum-depends for use in setup.py
20"""
21
22import datetime
23import os
24import re
25import subprocess
26import sys
27
28from setuptools.command import sdist
29
30
31def parse_mailmap(mailmap='.mailmap'):
32 mapping = {}
33 if os.path.exists(mailmap):
34 fp = open(mailmap, 'r')
35 for l in fp:
36 l = l.strip()
37 if not l.startswith('#') and ' ' in l:
38 canonical_email, alias = [x for x in l.split(' ')
39 if x.startswith('<')]
40 mapping[alias] = canonical_email
41 return mapping
42
43
44def canonicalize_emails(changelog, mapping):
45 """Takes in a string and an email alias mapping and replaces all
46 instances of the aliases in the string with their real email.
47 """
48 for alias, email in mapping.iteritems():
49 changelog = changelog.replace(alias, email)
50 return changelog
51
52
53# Get requirements from the first file that exists
54def get_reqs_from_files(requirements_files):
55 reqs_in = []
56 for requirements_file in requirements_files:
57 if os.path.exists(requirements_file):
58 return open(requirements_file, 'r').read().split('\n')
59 return []
60
61
62def parse_requirements(requirements_files=['requirements.txt',
63 'tools/pip-requires']):
64 requirements = []
65 for line in get_reqs_from_files(requirements_files):
66 # For the requirements list, we need to inject only the portion
67 # after egg= so that distutils knows the package it's looking for
68 # such as:
69 # -e git://github.com/openstack/nova/master#egg=nova
70 if re.match(r'\s*-e\s+', line):
71 requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1',
72 line))
73 # such as:
74 # http://github.com/openstack/nova/zipball/master#egg=nova
75 elif re.match(r'\s*https?:', line):
76 requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1',
77 line))
78 # -f lines are for index locations, and don't get used here
79 elif re.match(r'\s*-f\s+', line):
80 pass
81 # argparse is part of the standard library starting with 2.7
82 # adding it to the requirements list screws distro installs
83 elif line == 'argparse' and sys.version_info >= (2, 7):
84 pass
85 else:
86 requirements.append(line)
87
88 return requirements
89
90
91def parse_dependency_links(requirements_files=['requirements.txt',
92 'tools/pip-requires']):
93 dependency_links = []
94 # dependency_links inject alternate locations to find packages listed
95 # in requirements
96 for line in get_reqs_from_files(requirements_files):
97 # skip comments and blank lines
98 if re.match(r'(\s*#)|(\s*$)', line):
99 continue
100 # lines with -e or -f need the whole line, minus the flag
101 if re.match(r'\s*-[ef]\s+', line):
102 dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line))
103 # lines that are only urls can go in unmolested
104 elif re.match(r'\s*https?:', line):
105 dependency_links.append(line)
106 return dependency_links
107
108
109def write_requirements():
110 venv = os.environ.get('VIRTUAL_ENV', None)
111 if venv is not None:
112 with open("requirements.txt", "w") as req_file:
113 output = subprocess.Popen(["pip", "-E", venv, "freeze", "-l"],
114 stdout=subprocess.PIPE)
115 requirements = output.communicate()[0].strip()
116 req_file.write(requirements)
117
118
119def _run_shell_command(cmd):
120 output = subprocess.Popen(["/bin/sh", "-c", cmd],
121 stdout=subprocess.PIPE)
122 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
130def _get_git_next_version_suffix(branch_name):
131 datestamp = datetime.datetime.now().strftime('%Y%m%d')
132 if branch_name == 'milestone-proposed':
133 revno_prefix = "r"
134 else:
135 revno_prefix = ""
136 _run_shell_command("git fetch origin +refs/meta/*:refs/remotes/meta/*")
137 milestone_cmd = "git show meta/openstack/release:%s" % branch_name
138 milestonever = _run_shell_command(milestone_cmd)
139 if not milestonever:
140 milestonever = ""
141 post_version = _get_git_post_version()
142 # post version should look like:
143 # 0.1.1.4.gcc9e28a
144 # where the bit after the last . is the short sha, and the bit between
145 # the last and second to last is the revno count
146 (revno, sha) = post_version.split(".")[-2:]
147 first_half = "%(milestonever)s~%(datestamp)s" % locals()
148 second_half = "%(revno_prefix)s%(revno)s.%(sha)s" % locals()
149 return ".".join((first_half, second_half))
150
151
152def _get_git_current_tag():
153 return _run_shell_command("git tag --contains HEAD")
154
155
156def _get_git_tag_info():
157 return _run_shell_command("git describe --tags")
158
159
160def _get_git_post_version():
161 current_tag = _get_git_current_tag()
162 if current_tag is not None:
163 return current_tag
164 else:
165 tag_info = _get_git_tag_info()
166 if tag_info is None:
167 base_version = "0.0"
168 cmd = "git --no-pager log --oneline"
169 out = _run_shell_command(cmd)
170 revno = len(out.split("\n"))
171 sha = _run_shell_command("git describe --always")
172 else:
173 tag_infos = tag_info.split("-")
174 base_version = "-".join(tag_infos[:-2])
175 (revno, sha) = tag_infos[-2:]
176 return "%s.%s.%s" % (base_version, revno, sha)
177
178
179def write_git_changelog():
180 """Write a changelog based on the git changelog."""
181 if os.path.isdir('.git'):
182 git_log_cmd = 'git log --stat'
183 changelog = _run_shell_command(git_log_cmd)
184 mailmap = parse_mailmap()
185 with open("ChangeLog", "w") as changelog_file:
186 changelog_file.write(canonicalize_emails(changelog, mailmap))
187
188
189def generate_authors():
190 """Create AUTHORS file using git commits."""
191 jenkins_email = 'jenkins@review.openstack.org'
192 old_authors = 'AUTHORS.in'
193 new_authors = 'AUTHORS'
194 if os.path.isdir('.git'):
195 # don't include jenkins email address in AUTHORS file
196 git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | "
197 "grep -v " + jenkins_email)
198 changelog = _run_shell_command(git_log_cmd)
199 mailmap = parse_mailmap()
200 with open(new_authors, 'w') as new_authors_fh:
201 new_authors_fh.write(canonicalize_emails(changelog, mailmap))
202 if os.path.exists(old_authors):
203 with open(old_authors, "r") as old_authors_fh:
204 new_authors_fh.write('\n' + old_authors_fh.read())
205
206_rst_template = """%(heading)s
207%(underline)s
208
209.. automodule:: %(module)s
210 :members:
211 :undoc-members:
212 :show-inheritance:
213"""
214
215
216def read_versioninfo(project):
217 """Read the versioninfo file. If it doesn't exist, we're in a github
218 zipball, and there's really no way to know what version we really
219 are, but that should be ok, because the utility of that should be
220 just about nil if this code path is in use in the first place."""
221 versioninfo_path = os.path.join(project, 'versioninfo')
222 if os.path.exists(versioninfo_path):
223 with open(versioninfo_path, 'r') as vinfo:
224 version = vinfo.read().strip()
225 else:
226 version = "0.0.0"
227 return version
228
229
230def write_versioninfo(project, version):
231 """Write a simple file containing the version of the package."""
232 open(os.path.join(project, 'versioninfo'), 'w').write("%s\n" % version)
233
234
235def get_cmdclass():
236 """Return dict of commands to run from setup.py."""
237
238 cmdclass = dict()
239
240 def _find_modules(arg, dirname, files):
241 for filename in files:
242 if filename.endswith('.py') and filename != '__init__.py':
243 arg["%s.%s" % (dirname.replace('/', '.'),
244 filename[:-3])] = True
245
246 class LocalSDist(sdist.sdist):
247 """Builds the ChangeLog and Authors files from VC first."""
248
249 def run(self):
250 write_git_changelog()
251 generate_authors()
252 # sdist.sdist is an old style class, can't use super()
253 sdist.sdist.run(self)
254
255 cmdclass['sdist'] = LocalSDist
256
257 # If Sphinx is installed on the box running setup.py,
258 # enable setup.py to build the documentation, otherwise,
259 # just ignore it
260 try:
261 from sphinx.setup_command import BuildDoc
262
263 class LocalBuildDoc(BuildDoc):
264 def generate_autoindex(self):
265 print "**Autodocumenting from %s" % os.path.abspath(os.curdir)
266 modules = {}
267 option_dict = self.distribution.get_option_dict('build_sphinx')
268 source_dir = os.path.join(option_dict['source_dir'][1], 'api')
269 if not os.path.exists(source_dir):
270 os.makedirs(source_dir)
271 for pkg in self.distribution.packages:
272 if '.' not in pkg:
273 os.path.walk(pkg, _find_modules, modules)
274 module_list = modules.keys()
275 module_list.sort()
276 autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
277 with open(autoindex_filename, 'w') as autoindex:
278 autoindex.write(""".. toctree::
279 :maxdepth: 1
280
281""")
282 for module in module_list:
283 output_filename = os.path.join(source_dir,
284 "%s.rst" % module)
285 heading = "The :mod:`%s` Module" % module
286 underline = "=" * len(heading)
287 values = dict(module=module, heading=heading,
288 underline=underline)
289
290 print "Generating %s" % output_filename
291 with open(output_filename, 'w') as output_file:
292 output_file.write(_rst_template % values)
293 autoindex.write(" %s.rst\n" % module)
294
295 def run(self):
296 if not os.getenv('SPHINX_DEBUG'):
297 self.generate_autoindex()
298
299 for builder in ['html', 'man']:
300 self.builder = builder
301 self.finalize_options()
302 self.project = self.distribution.get_name()
303 self.version = self.distribution.get_version()
304 self.release = self.distribution.get_version()
305 BuildDoc.run(self)
306 cmdclass['build_sphinx'] = LocalBuildDoc
307 except ImportError:
308 pass
309
310 return cmdclass
311
312
313def get_git_branchname():
314 for branch in _run_shell_command("git branch --color=never").split("\n"):
315 if branch.startswith('*'):
316 _branch_name = branch.split()[1].strip()
317 if _branch_name == "(no":
318 _branch_name = "no-branch"
319 return _branch_name
320
321
322def get_pre_version(projectname, base_version):
323 """Return a version which is leading up to a version that will
324 be released in the future."""
325 if os.path.isdir('.git'):
326 current_tag = _get_git_current_tag()
327 if current_tag is not None:
328 version = current_tag
329 else:
330 branch_name = os.getenv('BRANCHNAME',
331 os.getenv('GERRIT_REFNAME',
332 get_git_branchname()))
333 version_suffix = _get_git_next_version_suffix(branch_name)
334 version = "%s~%s" % (base_version, version_suffix)
335 write_versioninfo(projectname, version)
336 return version
337 else:
338 version = read_versioninfo(projectname)
339 return version
340
341
342def get_post_version(projectname):
343 """Return a version which is equal to the tag that's on the current
344 revision if there is one, or tag plus number of additional revisions
345 if the current revision has no tag."""
346
347 if os.path.isdir('.git'):
348 version = _get_git_post_version()
349 write_versioninfo(projectname, version)
350 return version
351 return read_versioninfo(projectname)