~tnurlygayanov/murano/reliase-0.1a

« back to all changes in this revision

Viewing changes to murano-conductor/conductor/openstack/common/setup.py

  • Committer: Timur Nurlygayanov
  • Date: 2013-05-30 22:18:42 UTC
  • Revision ID: tnulrygayanov@mirantis.com-20130530221842-8e7xd8e89e99n3xz
murano-0.1a

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
 
 
3
# Copyright 2011 OpenStack Foundation.
 
4
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
 
5
# 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
"""
 
20
Utilities with minimum-depends for use in setup.py
 
21
"""
 
22
 
 
23
import email
 
24
import os
 
25
import re
 
26
import subprocess
 
27
import sys
 
28
 
 
29
from setuptools.command import sdist
 
30
 
 
31
 
 
32
def parse_mailmap(mailmap='.mailmap'):
 
33
    mapping = {}
 
34
    if os.path.exists(mailmap):
 
35
        with open(mailmap, 'r') as fp:
 
36
            for l in fp:
 
37
                try:
 
38
                    canonical_email, alias = re.match(
 
39
                        r'[^#]*?(<.+>).*(<.+>).*', l).groups()
 
40
                except AttributeError:
 
41
                    continue
 
42
                mapping[alias] = canonical_email
 
43
    return mapping
 
44
 
 
45
 
 
46
def _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
 
 
51
def 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
    """
 
55
    for alias, email_address in mapping.iteritems():
 
56
        changelog = changelog.replace(alias, email_address)
 
57
    return changelog
 
58
 
 
59
 
 
60
# Get requirements from the first file that exists
 
61
def 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
 
 
69
def 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
 
 
98
def 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
 
 
116
def _run_shell_command(cmd, throw_on_error=False):
 
117
    if os.name == 'nt':
 
118
        output = subprocess.Popen(["cmd.exe", "/C", cmd],
 
119
                                  stdout=subprocess.PIPE,
 
120
                                  stderr=subprocess.PIPE)
 
121
    else:
 
122
        output = subprocess.Popen(["/bin/sh", "-c", cmd],
 
123
                                  stdout=subprocess.PIPE,
 
124
                                  stderr=subprocess.PIPE)
 
125
    out = output.communicate()
 
126
    if output.returncode and throw_on_error:
 
127
        raise Exception("%s returned %d" % cmd, output.returncode)
 
128
    if len(out) == 0:
 
129
        return None
 
130
    if len(out[0].strip()) == 0:
 
131
        return None
 
132
    return out[0].strip()
 
133
 
 
134
 
 
135
def _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
 
 
146
def write_git_changelog():
 
147
    """Write a changelog based on the git changelog."""
 
148
    new_changelog = 'ChangeLog'
 
149
    git_dir = _get_git_directory()
 
150
    if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'):
 
151
        if git_dir:
 
152
            git_log_cmd = 'git --git-dir=%s log' % git_dir
 
153
            changelog = _run_shell_command(git_log_cmd)
 
154
            mailmap = _parse_git_mailmap(git_dir)
 
155
            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
 
 
161
def 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'
 
166
    git_dir = _get_git_directory()
 
167
    if not os.getenv('SKIP_GENERATE_AUTHORS'):
 
168
        if git_dir:
 
169
            # don't include jenkins email address in AUTHORS file
 
170
            git_log_cmd = ("git --git-dir=" + git_dir +
 
171
                           " log --format='%aN <%aE>' | sort -u | "
 
172
                           "egrep -v '" + jenkins_email + "'")
 
173
            changelog = _run_shell_command(git_log_cmd)
 
174
            signed_cmd = ("git log --git-dir=" + git_dir +
 
175
                          " | grep -i Co-authored-by: | sort -u")
 
176
            signed_entries = _run_shell_command(signed_cmd)
 
177
            if signed_entries:
 
178
                new_entries = "\n".join(
 
179
                    [signed.split(":", 1)[1].strip()
 
180
                     for signed in signed_entries.split("\n") if signed])
 
181
                changelog = "\n".join((changelog, new_entries))
 
182
            mailmap = _parse_git_mailmap(git_dir)
 
183
            with open(new_authors, 'w') as new_authors_fh:
 
184
                new_authors_fh.write(canonicalize_emails(changelog, mailmap))
 
185
                if os.path.exists(old_authors):
 
186
                    with open(old_authors, "r") as old_authors_fh:
 
187
                        new_authors_fh.write('\n' + old_authors_fh.read())
 
188
    else:
 
189
        open(new_authors, 'w').close()
 
190
 
 
191
 
 
192
_rst_template = """%(heading)s
 
193
%(underline)s
 
194
 
 
195
.. automodule:: %(module)s
 
196
  :members:
 
197
  :undoc-members:
 
198
  :show-inheritance:
 
199
"""
 
200
 
 
201
 
 
202
def get_cmdclass():
 
203
    """Return dict of commands to run from setup.py."""
 
204
 
 
205
    cmdclass = dict()
 
206
 
 
207
    def _find_modules(arg, dirname, files):
 
208
        for filename in files:
 
209
            if filename.endswith('.py') and filename != '__init__.py':
 
210
                arg["%s.%s" % (dirname.replace('/', '.'),
 
211
                               filename[:-3])] = True
 
212
 
 
213
    class LocalSDist(sdist.sdist):
 
214
        """Builds the ChangeLog and Authors files from VC first."""
 
215
 
 
216
        def run(self):
 
217
            write_git_changelog()
 
218
            generate_authors()
 
219
            # sdist.sdist is an old style class, can't use super()
 
220
            sdist.sdist.run(self)
 
221
 
 
222
    cmdclass['sdist'] = LocalSDist
 
223
 
 
224
    # If Sphinx is installed on the box running setup.py,
 
225
    # enable setup.py to build the documentation, otherwise,
 
226
    # just ignore it
 
227
    try:
 
228
        from sphinx.setup_command import BuildDoc
 
229
 
 
230
        class LocalBuildDoc(BuildDoc):
 
231
 
 
232
            builders = ['html', 'man']
 
233
 
 
234
            def generate_autoindex(self):
 
235
                print "**Autodocumenting from %s" % os.path.abspath(os.curdir)
 
236
                modules = {}
 
237
                option_dict = self.distribution.get_option_dict('build_sphinx')
 
238
                source_dir = os.path.join(option_dict['source_dir'][1], 'api')
 
239
                if not os.path.exists(source_dir):
 
240
                    os.makedirs(source_dir)
 
241
                for pkg in self.distribution.packages:
 
242
                    if '.' not in pkg:
 
243
                        os.path.walk(pkg, _find_modules, modules)
 
244
                module_list = modules.keys()
 
245
                module_list.sort()
 
246
                autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
 
247
                with open(autoindex_filename, 'w') as autoindex:
 
248
                    autoindex.write(""".. toctree::
 
249
   :maxdepth: 1
 
250
 
 
251
""")
 
252
                    for module in module_list:
 
253
                        output_filename = os.path.join(source_dir,
 
254
                                                       "%s.rst" % module)
 
255
                        heading = "The :mod:`%s` Module" % module
 
256
                        underline = "=" * len(heading)
 
257
                        values = dict(module=module, heading=heading,
 
258
                                      underline=underline)
 
259
 
 
260
                        print "Generating %s" % output_filename
 
261
                        with open(output_filename, 'w') as output_file:
 
262
                            output_file.write(_rst_template % values)
 
263
                        autoindex.write("   %s.rst\n" % module)
 
264
 
 
265
            def run(self):
 
266
                if not os.getenv('SPHINX_DEBUG'):
 
267
                    self.generate_autoindex()
 
268
 
 
269
                for builder in self.builders:
 
270
                    self.builder = builder
 
271
                    self.finalize_options()
 
272
                    self.project = self.distribution.get_name()
 
273
                    self.version = self.distribution.get_version()
 
274
                    self.release = self.distribution.get_version()
 
275
                    BuildDoc.run(self)
 
276
 
 
277
        class LocalBuildLatex(LocalBuildDoc):
 
278
            builders = ['latex']
 
279
 
 
280
        cmdclass['build_sphinx'] = LocalBuildDoc
 
281
        cmdclass['build_sphinx_latex'] = LocalBuildLatex
 
282
    except ImportError:
 
283
        pass
 
284
 
 
285
    return cmdclass
 
286
 
 
287
 
 
288
def _get_revno(git_dir):
 
289
    """Return the number of commits since the most recent tag.
 
290
 
 
291
    We use git-describe to find this out, but if there are no
 
292
    tags then we fall back to counting commits since the beginning
 
293
    of time.
 
294
    """
 
295
    describe = _run_shell_command(
 
296
        "git --git-dir=%s describe --always" % git_dir)
 
297
    if "-" in describe:
 
298
        return describe.rsplit("-", 2)[-2]
 
299
 
 
300
    # no tags found
 
301
    revlist = _run_shell_command(
 
302
        "git --git-dir=%s rev-list --abbrev-commit HEAD" % git_dir)
 
303
    return len(revlist.splitlines())
 
304
 
 
305
 
 
306
def _get_version_from_git(pre_version):
 
307
    """Return a version which is equal to the tag that's on the current
 
308
    revision if there is one, or tag plus number of additional revisions
 
309
    if the current revision has no tag."""
 
310
 
 
311
    git_dir = _get_git_directory()
 
312
    if git_dir:
 
313
        if pre_version:
 
314
            try:
 
315
                return _run_shell_command(
 
316
                    "git --git-dir=" + git_dir + " describe --exact-match",
 
317
                    throw_on_error=True).replace('-', '.')
 
318
            except Exception:
 
319
                sha = _run_shell_command(
 
320
                    "git --git-dir=" + git_dir + " log -n1 --pretty=format:%h")
 
321
                return "%s.a%s.g%s" % (pre_version, _get_revno(git_dir), sha)
 
322
        else:
 
323
            return _run_shell_command(
 
324
                "git --git-dir=" + git_dir + " describe --always").replace(
 
325
                    '-', '.')
 
326
    return None
 
327
 
 
328
 
 
329
def _get_version_from_pkg_info(package_name):
 
330
    """Get the version from PKG-INFO file if we can."""
 
331
    try:
 
332
        pkg_info_file = open('PKG-INFO', 'r')
 
333
    except (IOError, OSError):
 
334
        return None
 
335
    try:
 
336
        pkg_info = email.message_from_file(pkg_info_file)
 
337
    except email.MessageError:
 
338
        return None
 
339
    # Check to make sure we're in our own dir
 
340
    if pkg_info.get('Name', None) != package_name:
 
341
        return None
 
342
    return pkg_info.get('Version', None)
 
343
 
 
344
 
 
345
def get_version(package_name, pre_version=None):
 
346
    """Get the version of the project. First, try getting it from PKG-INFO, if
 
347
    it exists. If it does, that means we're in a distribution tarball or that
 
348
    install has happened. Otherwise, if there is no PKG-INFO file, pull the
 
349
    version from git.
 
350
 
 
351
    We do not support setup.py version sanity in git archive tarballs, nor do
 
352
    we support packagers directly sucking our git repo into theirs. We expect
 
353
    that a source tarball be made from our git repo - or that if someone wants
 
354
    to make a source tarball from a fork of our repo with additional tags in it
 
355
    that they understand and desire the results of doing that.
 
356
    """
 
357
    version = os.environ.get("OSLO_PACKAGE_VERSION", None)
 
358
    if version:
 
359
        return version
 
360
    version = _get_version_from_pkg_info(package_name)
 
361
    if version:
 
362
        return version
 
363
    version = _get_version_from_git(pre_version)
 
364
    if version:
 
365
        return version
 
366
    raise Exception("Versioning for this project requires either an sdist"
 
367
                    " tarball, or access to an upstream git repository.")