| Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 1 | # 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 | """ | 
 | 19 | Utilities with minimum-depends for use in setup.py | 
 | 20 | """ | 
 | 21 |  | 
 | 22 | import datetime | 
 | 23 | import os | 
 | 24 | import re | 
 | 25 | import subprocess | 
 | 26 | import sys | 
 | 27 |  | 
 | 28 | from setuptools.command import sdist | 
 | 29 |  | 
 | 30 |  | 
 | 31 | def 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 |  | 
 | 44 | def 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 | 
 | 54 | def 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 |  | 
 | 62 | def 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 |  | 
 | 91 | def 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 |  | 
 | 109 | def 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 |  | 
 | 119 | def _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 |  | 
 | 130 | def _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 |  | 
 | 152 | def _get_git_current_tag(): | 
 | 153 |     return _run_shell_command("git tag --contains HEAD") | 
 | 154 |  | 
 | 155 |  | 
 | 156 | def _get_git_tag_info(): | 
 | 157 |     return _run_shell_command("git describe --tags") | 
 | 158 |  | 
 | 159 |  | 
 | 160 | def _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 |  | 
 | 179 | def 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 |  | 
 | 189 | def 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 |  | 
 | 216 | def 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 |  | 
 | 230 | def 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 |  | 
 | 235 | def 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 |  | 
 | 313 | def 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 |  | 
 | 322 | def 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 |  | 
 | 342 | def 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) |